diff --git a/invokeai/app/invocations/spandrel_image_to_image.py b/invokeai/app/invocations/spandrel_image_to_image.py index bbe31af6448..3282106a427 100644 --- a/invokeai/app/invocations/spandrel_image_to_image.py +++ b/invokeai/app/invocations/spandrel_image_to_image.py @@ -1,3 +1,5 @@ +from typing import Callable + import numpy as np import torch from PIL import Image @@ -21,7 +23,7 @@ from invokeai.backend.tiles.utils import TBLR, Tile -@invocation("spandrel_image_to_image", title="Image-to-Image", tags=["upscale"], category="upscale", version="1.1.0") +@invocation("spandrel_image_to_image", title="Image-to-Image", tags=["upscale"], category="upscale", version="1.2.0") class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel).""" @@ -34,8 +36,19 @@ class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard): tile_size: int = InputField( default=512, description="The tile size for tiled image-to-image. Set to 0 to disable tiling." ) + scale: float = InputField( + default=4.0, + gt=0.0, + le=16.0, + description="The final scale of the output image. If the model does not upscale the image, this will be ignored.", + ) + fit_to_multiple_of_8: bool = InputField( + default=False, + description="If true, the output image will be resized to the nearest multiple of 8 in both dimensions.", + ) - def _scale_tile(self, tile: Tile, scale: int) -> Tile: + @classmethod + def scale_tile(cls, tile: Tile, scale: int) -> Tile: return Tile( coords=TBLR( top=tile.coords.top * scale, @@ -51,20 +64,22 @@ def _scale_tile(self, tile: Tile, scale: int) -> Tile: ), ) - @torch.inference_mode() - def invoke(self, context: InvocationContext) -> ImageOutput: - # Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to - # revisit this. - image = context.images.get_pil(self.image.image_name, mode="RGB") - + @classmethod + def upscale_image( + cls, + image: Image.Image, + tile_size: int, + spandrel_model: SpandrelImageToImageModel, + is_canceled: Callable[[], bool], + ) -> Image.Image: # Compute the image tiles. - if self.tile_size > 0: + if tile_size > 0: min_overlap = 20 tiles = calc_tiles_min_overlap( image_height=image.height, image_width=image.width, - tile_height=self.tile_size, - tile_width=self.tile_size, + tile_height=tile_size, + tile_width=tile_size, min_overlap=min_overlap, ) else: @@ -85,60 +100,123 @@ def invoke(self, context: InvocationContext) -> ImageOutput: # Prepare input image for inference. image_tensor = SpandrelImageToImageModel.pil_to_tensor(image) - # Load the model. - spandrel_model_info = context.models.load(self.image_to_image_model) - - # Run the model on each tile. - with spandrel_model_info as spandrel_model: - assert isinstance(spandrel_model, SpandrelImageToImageModel) + # Scale the tiles for re-assembling the final image. + scale = spandrel_model.scale + scaled_tiles = [cls.scale_tile(tile, scale=scale) for tile in tiles] - # Scale the tiles for re-assembling the final image. - scale = spandrel_model.scale - scaled_tiles = [self._scale_tile(tile, scale=scale) for tile in tiles] + # Prepare the output tensor. + _, channels, height, width = image_tensor.shape + output_tensor = torch.zeros( + (height * scale, width * scale, channels), dtype=torch.uint8, device=torch.device("cpu") + ) - # Prepare the output tensor. - _, channels, height, width = image_tensor.shape - output_tensor = torch.zeros( - (height * scale, width * scale, channels), dtype=torch.uint8, device=torch.device("cpu") - ) + image_tensor = image_tensor.to(device=spandrel_model.device, dtype=spandrel_model.dtype) - image_tensor = image_tensor.to(device=spandrel_model.device, dtype=spandrel_model.dtype) - - for tile, scaled_tile in tqdm(list(zip(tiles, scaled_tiles, strict=True)), desc="Upscaling Tiles"): - # Exit early if the invocation has been canceled. - if context.util.is_canceled(): - raise CanceledException - - # Extract the current tile from the input tensor. - input_tile = image_tensor[ - :, :, tile.coords.top : tile.coords.bottom, tile.coords.left : tile.coords.right - ].to(device=spandrel_model.device, dtype=spandrel_model.dtype) - - # Run the model on the tile. - output_tile = spandrel_model.run(input_tile) - - # Convert the output tile into the output tensor's format. - # (N, C, H, W) -> (C, H, W) - output_tile = output_tile.squeeze(0) - # (C, H, W) -> (H, W, C) - output_tile = output_tile.permute(1, 2, 0) - output_tile = output_tile.clamp(0, 1) - output_tile = (output_tile * 255).to(dtype=torch.uint8, device=torch.device("cpu")) - - # Merge the output tile into the output tensor. - # We only keep half of the overlap on the top and left side of the tile. We do this in case there are - # edge artifacts. We don't bother with any 'blending' in the current implementation - for most upscalers - # it seems unnecessary, but we may find a need in the future. - top_overlap = scaled_tile.overlap.top // 2 - left_overlap = scaled_tile.overlap.left // 2 - output_tensor[ - scaled_tile.coords.top + top_overlap : scaled_tile.coords.bottom, - scaled_tile.coords.left + left_overlap : scaled_tile.coords.right, - :, - ] = output_tile[top_overlap:, left_overlap:, :] + # Run the model on each tile. + for tile, scaled_tile in tqdm(list(zip(tiles, scaled_tiles, strict=True)), desc="Upscaling Tiles"): + # Exit early if the invocation has been canceled. + if is_canceled(): + raise CanceledException + + # Extract the current tile from the input tensor. + input_tile = image_tensor[ + :, :, tile.coords.top : tile.coords.bottom, tile.coords.left : tile.coords.right + ].to(device=spandrel_model.device, dtype=spandrel_model.dtype) + + # Run the model on the tile. + output_tile = spandrel_model.run(input_tile) + + # Convert the output tile into the output tensor's format. + # (N, C, H, W) -> (C, H, W) + output_tile = output_tile.squeeze(0) + # (C, H, W) -> (H, W, C) + output_tile = output_tile.permute(1, 2, 0) + output_tile = output_tile.clamp(0, 1) + output_tile = (output_tile * 255).to(dtype=torch.uint8, device=torch.device("cpu")) + + # Merge the output tile into the output tensor. + # We only keep half of the overlap on the top and left side of the tile. We do this in case there are + # edge artifacts. We don't bother with any 'blending' in the current implementation - for most upscalers + # it seems unnecessary, but we may find a need in the future. + top_overlap = scaled_tile.overlap.top // 2 + left_overlap = scaled_tile.overlap.left // 2 + output_tensor[ + scaled_tile.coords.top + top_overlap : scaled_tile.coords.bottom, + scaled_tile.coords.left + left_overlap : scaled_tile.coords.right, + :, + ] = output_tile[top_overlap:, left_overlap:, :] # Convert the output tensor to a PIL image. np_image = output_tensor.detach().numpy().astype(np.uint8) pil_image = Image.fromarray(np_image) + + return pil_image + + @torch.inference_mode() + def invoke(self, context: InvocationContext) -> ImageOutput: + # Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to + # revisit this. + image = context.images.get_pil(self.image.image_name, mode="RGB") + + # Load the model. + spandrel_model_info = context.models.load(self.image_to_image_model) + + # The target size of the image, determined by the provided scale. We'll run the upscaler until we hit this size. + # Later, we may mutate this value if the model doesn't upscale the image or if the user requested a multiple of 8. + target_width = int(image.width * self.scale) + target_height = int(image.height * self.scale) + + # Do the upscaling. + with spandrel_model_info as spandrel_model: + assert isinstance(spandrel_model, SpandrelImageToImageModel) + + # First pass of upscaling. Note: `pil_image` will be mutated. + pil_image = self.upscale_image(image, self.tile_size, spandrel_model, context.util.is_canceled) + + # Some models don't upscale the image, but we have no way to know this in advance. We'll check if the model + # upscaled the image and run the loop below if it did. We'll require the model to upscale both dimensions + # to be considered an upscale model. + is_upscale_model = pil_image.width > image.width and pil_image.height > image.height + + if is_upscale_model: + # This is an upscale model, so we should keep upscaling until we reach the target size. + iterations = 1 + while pil_image.width < target_width or pil_image.height < target_height: + pil_image = self.upscale_image(pil_image, self.tile_size, spandrel_model, context.util.is_canceled) + iterations += 1 + + # Sanity check to prevent excessive or infinite loops. All known upscaling models are at least 2x. + # Our max scale is 16x, so with a 2x model, we should never exceed 16x == 2^4 -> 4 iterations. + # We'll allow one extra iteration "just in case" and bail at 5 upscaling iterations. In practice, + # we should never reach this limit. + if iterations >= 5: + context.logger.warning( + "Upscale loop reached maximum iteration count of 5, stopping upscaling early." + ) + break + else: + # This model doesn't upscale the image. We should ignore the scale parameter, modifying the output size + # to be the same as the processed image size. + + # The output size is now the size of the processed image. + target_width = pil_image.width + target_height = pil_image.height + + # Warn the user if they requested a scale greater than 1. + if self.scale > 1: + context.logger.warning( + "Model does not increase the size of the image, but a greater scale than 1 was requested. Image will not be scaled." + ) + + # We may need to resize the image to a multiple of 8. Use floor division to ensure we don't scale the image up + # in the final resize + if self.fit_to_multiple_of_8: + target_width = int(target_width // 8 * 8) + target_height = int(target_height // 8 * 8) + + # Final resize. Per PIL documentation, Lanczos provides the best quality for both upscale and downscale. + # See: https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters-comparison-table + pil_image = pil_image.resize((target_width, target_height), resample=Image.Resampling.LANCZOS) + image_dto = context.images.save(image=pil_image) return ImageOutput.build(image_dto) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 91a36bcee32..8fc600d6c96 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1027,6 +1027,7 @@ "imageActions": "Image Actions", "sendToImg2Img": "Send to Image to Image", "sendToUnifiedCanvas": "Send To Unified Canvas", + "sendToUpscale": "Send To Upscale", "showOptionsPanel": "Show Side Panel (O or T)", "shuffle": "Shuffle Seed", "steps": "Steps", @@ -1640,6 +1641,19 @@ "layers_one": "Layer", "layers_other": "Layers" }, + "upscaling": { + "creativity": "Creativity", + "structure": "Structure", + "upscaleModel": "Upscale Model", + "scale": "Scale", + "missingModelsWarning": "Visit the Model Manager to install the required models:", + "mainModelDesc": "Main model (SD1.5 or SDXL architecture)", + "tileControlNetModelDesc": "Tile ControlNet model for the chosen main model architecture", + "upscaleModelDesc": "Upscale (image to image) model", + "missingUpscaleInitialImage": "Missing initial image for upscaling", + "missingUpscaleModel": "Missing upscale model", + "missingTileControlNetModel": "No valid tile ControlNet models installed" + }, "ui": { "tabs": { "generation": "Generation", @@ -1651,7 +1665,9 @@ "models": "Models", "modelsTab": "$t(ui.tabs.models) $t(common.tab)", "queue": "Queue", - "queueTab": "$t(ui.tabs.queue) $t(common.tab)" + "queueTab": "$t(ui.tabs.queue) $t(common.tab)", + "upscaling": "Upscaling", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)" } } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 9698f85219a..aad9a2a289f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -52,6 +52,7 @@ import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerM import type { AppDispatch, RootState } from 'app/store/store'; import { addArchivedOrDeletedBoardListener } from './listeners/addArchivedOrDeletedBoardListener'; +import { addEnqueueRequestedUpscale } from './listeners/enqueueRequestedUpscale'; export const listenerMiddleware = createListenerMiddleware(); @@ -85,6 +86,7 @@ addGalleryOffsetChangedListener(startAppListening); addEnqueueRequestedCanvasListener(startAppListening); addEnqueueRequestedNodes(startAppListening); addEnqueueRequestedLinear(startAppListening); +addEnqueueRequestedUpscale(startAppListening); addAnyEnqueuedListener(startAppListening); addBatchEnqueuedListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts new file mode 100644 index 00000000000..dc870a9f8b5 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts @@ -0,0 +1,36 @@ +import { enqueueRequested } from 'app/store/actions'; +import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; +import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; +import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph'; +import { queueApi } from 'services/api/endpoints/queue'; + +export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) => { + startAppListening({ + predicate: (action): action is ReturnType => + enqueueRequested.match(action) && action.payload.tabName === 'upscaling', + effect: async (action, { getState, dispatch }) => { + const state = getState(); + const { shouldShowProgressInViewer } = state.ui; + const { prepend } = action.payload; + + const graph = await buildMultidiffusionUpscaleGraph(state); + + const batchConfig = prepareLinearUIBatch(state, graph, prepend); + + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(batchConfig, { + fixedCacheKey: 'enqueueBatch', + }) + ); + try { + await req.unwrap(); + if (shouldShowProgressInViewer) { + dispatch(isImageViewerOpenChanged(true)); + } + } finally { + req.reset(); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 0cd77dc2e75..a65c31b7cdc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -23,6 +23,7 @@ import { } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { imagesApi } from 'services/api/endpoints/images'; export const dndDropped = createAction<{ @@ -243,6 +244,20 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on upscale initial image + */ + if ( + overData.actionType === 'SET_UPSCALE_INITIAL_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { imageDTO } = activeData.payload; + + dispatch(upscaleInitialImageChanged(imageDTO)); + return; + } + /** * Multiple images dropped on user board */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index bd1ee478255..1aa47345e18 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -14,6 +14,7 @@ import { import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { omit } from 'lodash-es'; @@ -89,6 +90,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } + if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') { + dispatch(upscaleInitialImageChanged(imageDTO)); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: 'set as upscale initial image', + }); + return; + } + if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') { const { id } = postUploadAction; dispatch( diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index eb86f54c84f..2ace69c54ea 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -10,6 +10,7 @@ import { heightChanged, widthChanged } from 'features/controlLayers/store/contro import { loraRemoved } from 'features/lora/store/loraSlice'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; +import { upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice'; @@ -17,7 +18,12 @@ import { forEach } from 'lodash-es'; import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; -import { isNonRefinerMainModelConfig, isRefinerMainModelModelConfig, isVAEModelConfig } from 'services/api/types'; +import { + isNonRefinerMainModelConfig, + isRefinerMainModelModelConfig, + isSpandrelImageToImageModelConfig, + isVAEModelConfig, +} from 'services/api/types'; export const addModelsLoadedListener = (startAppListening: AppStartListening) => { startAppListening({ @@ -36,6 +42,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) => handleVAEModels(models, state, dispatch, log); handleLoRAModels(models, state, dispatch, log); handleControlAdapterModels(models, state, dispatch, log); + handleSpandrelImageToImageModels(models, state, dispatch, log); }, }); }; @@ -177,3 +184,23 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) dispatch(controlAdapterModelCleared({ id: ca.id })); }); }; + +const handleSpandrelImageToImageModels: ModelHandler = (models, state, dispatch, _log) => { + const currentUpscaleModel = state.upscale.upscaleModel; + const upscaleModels = models.filter(isSpandrelImageToImageModelConfig); + + if (currentUpscaleModel) { + const isCurrentUpscaleModelAvailable = upscaleModels.some((m) => m.key === currentUpscaleModel.key); + if (isCurrentUpscaleModelAvailable) { + return; + } + } + + const firstModel = upscaleModels[0]; + if (firstModel) { + dispatch(upscaleModelChanged(firstModel)); + return; + } + + dispatch(upscaleModelChanged(null)); +}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 062cdc1cbf4..1a4093dfc51 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -26,6 +26,7 @@ import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/n import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; +import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice'; import { queueSlice } from 'features/queue/store/queueSlice'; import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { configSlice } from 'features/system/store/configSlice'; @@ -69,6 +70,7 @@ const allReducers = { [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [api.reducerPath]: api.reducer, + [upscaleSlice.name]: upscaleSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -114,6 +116,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [hrfPersistConfig.name]: hrfPersistConfig, [controlLayersPersistConfig.name]: controlLayersPersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, + [upscalePersistConfig.name]: upscalePersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 5b1bf1f5b39..d8e7d70a8c5 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -21,6 +21,10 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; } + if (activeTabName === 'upscaling') { + postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' }; + } + return postUploadAction; }); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index dbf3c414807..ba2117f2075 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -15,6 +15,7 @@ import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; +import { selectUpscalelice } from 'features/parameters/store/upscaleSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; @@ -40,8 +41,19 @@ const createSelector = (templates: Templates) => selectDynamicPromptsSlice, selectControlLayersSlice, activeTabNameSelector, + selectUpscalelice, ], - (controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => { + ( + controlAdapters, + generation, + system, + nodes, + workflowSettings, + dynamicPrompts, + controlLayers, + activeTabName, + upscale + ) => { const { model } = generation; const { size } = controlLayers.present; const { positivePrompt } = controlLayers.present; @@ -194,6 +206,16 @@ const createSelector = (templates: Templates) => reasons.push({ prefix, content }); } }); + } else if (activeTabName === 'upscaling') { + if (!upscale.upscaleInitialImage) { + reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') }); + } + if (!upscale.upscaleModel) { + reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') }); + } + if (!upscale.tileControlnetModel) { + reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') }); + } } else { // Handling for all other tabs selectControlAdapterAll(controlAdapters) diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 6fcf18421ef..93bde117a11 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -62,6 +62,10 @@ export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; +type UpscaleInitialImageDropData = BaseDropData & { + actionType: 'SET_UPSCALE_INITIAL_IMAGE'; +}; + type NodesImageDropData = BaseDropData & { actionType: 'SET_NODES_IMAGE'; context: { @@ -98,7 +102,8 @@ export type TypesafeDroppableData = | IPALayerImageDropData | RGLayerIPAdapterImageDropData | IILayerImageDropData - | SelectForCompareDropData; + | SelectForCompareDropData + | UpscaleInitialImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 6dec862345a..3f8fe5ab734 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -27,6 +27,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SET_CANVAS_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SET_UPSCALE_INITIAL_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 31df113115a..ab12684c114 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -13,6 +13,7 @@ import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/ac import { imageToCompareChanged } from 'features/gallery/store/gallerySlice'; import { $templates } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { toast } from 'features/toast/toast'; import { setActiveTab } from 'features/ui/store/uiSlice'; @@ -124,6 +125,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { dispatch(imageToCompareChanged(imageDTO)); }, [dispatch, imageDTO]); + const handleSendToUpscale = useCallback(() => { + dispatch(upscaleInitialImageChanged(imageDTO)); + dispatch(setActiveTab('upscaling')); + }, [dispatch, imageDTO]); + return ( <> }> @@ -185,6 +191,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { {t('parameters.sendToUnifiedCanvas')} )} + } onClickCapture={handleSendToUpscale} id="send-to-upscale"> + {t('parameters.sendToUpscale')} + } onClickCapture={handleChangeBoard}> {t('boards.changeBoard')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx index e610ca00779..dfc131c87cb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx @@ -9,7 +9,13 @@ import CurrentImageButtons from './CurrentImageButtons'; import { ViewerToggleMenu } from './ViewerToggleMenu'; export const ViewerToolbar = memo(() => { - const tab = useAppSelector(activeTabNameSelector); + const showToggle = useAppSelector((s) => { + const tab = activeTabNameSelector(s); + if (tab === 'upscaling' || tab === 'workflows') { + return false; + } + return true; + }); return ( @@ -23,7 +29,7 @@ export const ViewerToolbar = memo(() => { - {tab !== 'workflows' && } + {showToggle && } diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx index 6da320aa0b7..101394f85ac 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx @@ -1,5 +1,6 @@ import { Button, Text, useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useCallback, useEffect, useState } from 'react'; @@ -44,6 +45,7 @@ const ToastDescription = () => { const onClick = useCallback(() => { dispatch(setActiveTab('models')); + $installModelsTab.set(3); toast.close(TOAST_ID); }, [dispatch, toast]); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx index 98e1e396404..754cbbd25ab 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx @@ -30,7 +30,7 @@ export const StarterModelsResultItem = ({ result }: Props) => { - {result.type.replace('_', ' ')} + {result.type.replaceAll('_', ' ')} {result.name} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx index 7aa05af3004..ccaa29d5e25 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx @@ -18,14 +18,17 @@ export const StarterModelsResults = ({ results }: StarterModelsResultsProps) => const filteredResults = useMemo(() => { return results.filter((result) => { - const name = result.name.toLowerCase(); - const type = result.type.toLowerCase(); - return name.includes(searchTerm.toLowerCase()) || type.includes(searchTerm.toLowerCase()); + const trimmedSearchTerm = searchTerm.trim().toLowerCase(); + const matchStrings = [result.name.toLowerCase(), result.type.toLowerCase(), result.description.toLowerCase()]; + if (result.type === 'spandrel_image_to_image') { + matchStrings.push('upscale'); + } + return matchStrings.some((matchString) => matchString.includes(trimmedSearchTerm)); }); }, [results, searchTerm]); const handleSearch: ChangeEventHandler = useCallback((e) => { - setSearchTerm(e.target.value.trim()); + setSearchTerm(e.target.value); }, []); const clearSearch = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx index d09ab67fa4a..b5110722d5c 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx @@ -1,28 +1,28 @@ import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm'; -import { useMemo } from 'react'; +import { atom } from 'nanostores'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMainModels } from 'services/api/hooks/modelsByType'; import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm'; import { InstallModelForm } from './AddModelPanel/InstallModelForm'; import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue'; import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm'; +export const $installModelsTab = atom(0); + export const InstallModels = () => { const { t } = useTranslation(); - const [mainModels, { data }] = useMainModels(); - const defaultIndex = useMemo(() => { - if (data && mainModels.length) { - return 0; - } - return 3; - }, [data, mainModels.length]); + const index = useStore($installModelsTab); + const onChange = useCallback((index: number) => { + $installModelsTab.set(index); + }, []); return ( {t('modelManager.addModel')} - + {t('modelManager.urlOrLocalPath')} {t('modelManager.huggingFace')} diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts new file mode 100644 index 00000000000..1516a3fae3b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts @@ -0,0 +1,246 @@ +import type { RootState } from 'app/store/store'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { isNonRefinerMainModelConfig, isSpandrelImageToImageModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { + CLIP_SKIP, + CONTROL_NET_COLLECT, + IMAGE_TO_LATENTS, + LATENTS_TO_IMAGE, + MAIN_MODEL_LOADER, + NEGATIVE_CONDITIONING, + NOISE, + POSITIVE_CONDITIONING, + SDXL_MODEL_LOADER, + SPANDREL, + TILED_MULTI_DIFFUSION_DENOISE_LATENTS, + UNSHARP_MASK, + VAE_LOADER, +} from './constants'; +import { addLoRAs } from './generation/addLoRAs'; +import { addSDXLLoRas } from './generation/addSDXLLoRAs'; +import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils'; + +export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise => { + const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.generation; + const { positivePrompt, negativePrompt } = state.controlLayers.present; + const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale; + + assert(model, 'No model found in state'); + assert(upscaleModel, 'No upscale model found in state'); + assert(upscaleInitialImage, 'No initial image found in state'); + assert(tileControlnetModel, 'Tile controlnet is required'); + + const g = new Graph(); + + const upscaleNode = g.addNode({ + id: SPANDREL, + type: 'spandrel_image_to_image', + image: upscaleInitialImage, + image_to_image_model: upscaleModel, + fit_to_multiple_of_8: true, + scale, + }); + + const unsharpMaskNode2 = g.addNode({ + id: `${UNSHARP_MASK}_2`, + type: 'unsharp_mask', + radius: 2, + strength: 60, + }); + + g.addEdge(upscaleNode, 'image', unsharpMaskNode2, 'image'); + + const noiseNode = g.addNode({ + id: NOISE, + type: 'noise', + seed, + }); + + g.addEdge(unsharpMaskNode2, 'width', noiseNode, 'width'); + g.addEdge(unsharpMaskNode2, 'height', noiseNode, 'height'); + + const i2lNode = g.addNode({ + id: IMAGE_TO_LATENTS, + type: 'i2l', + fp32: vaePrecision === 'fp32', + tiled: true, + }); + + g.addEdge(unsharpMaskNode2, 'image', i2lNode, 'image'); + + const l2iNode = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE, + fp32: vaePrecision === 'fp32', + tiled: true, + board: getBoardField(state), + is_intermediate: false, + }); + + const tiledMultidiffusionNode = g.addNode({ + id: TILED_MULTI_DIFFUSION_DENOISE_LATENTS, + type: 'tiled_multi_diffusion_denoise_latents', + tile_height: 1024, // is this dependent on base model + tile_width: 1024, // is this dependent on base model + tile_overlap: 128, + steps, + cfg_scale, + scheduler, + denoising_start: ((creativity * -1 + 10) * 4.99) / 100, + denoising_end: 1, + }); + + let posCondNode; + let negCondNode; + let modelNode; + + if (model.base === 'sdxl') { + const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state); + + posCondNode = g.addNode({ + type: 'sdxl_compel_prompt', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + style: positiveStylePrompt, + }); + negCondNode = g.addNode({ + type: 'sdxl_compel_prompt', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + style: negativeStylePrompt, + }); + modelNode = g.addNode({ + type: 'sdxl_model_loader', + id: SDXL_MODEL_LOADER, + model, + }); + g.addEdge(modelNode, 'clip', posCondNode, 'clip'); + g.addEdge(modelNode, 'clip', negCondNode, 'clip'); + g.addEdge(modelNode, 'clip2', posCondNode, 'clip2'); + g.addEdge(modelNode, 'clip2', negCondNode, 'clip2'); + g.addEdge(modelNode, 'unet', tiledMultidiffusionNode, 'unet'); + addSDXLLoRas(state, g, tiledMultidiffusionNode, modelNode, null, posCondNode, negCondNode); + + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + + g.upsertMetadata({ + cfg_scale, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + positive_style_prompt: positiveStylePrompt, + negative_style_prompt: negativeStylePrompt, + model: Graph.getModelMetadataField(modelConfig), + seed, + steps, + scheduler, + vae: vae ?? undefined, + }); + } else { + posCondNode = g.addNode({ + type: 'compel', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + }); + negCondNode = g.addNode({ + type: 'compel', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + }); + modelNode = g.addNode({ + type: 'main_model_loader', + id: MAIN_MODEL_LOADER, + model, + }); + const clipSkipNode = g.addNode({ + type: 'clip_skip', + id: CLIP_SKIP, + }); + + g.addEdge(modelNode, 'clip', clipSkipNode, 'clip'); + g.addEdge(clipSkipNode, 'clip', posCondNode, 'clip'); + g.addEdge(clipSkipNode, 'clip', negCondNode, 'clip'); + g.addEdge(modelNode, 'unet', tiledMultidiffusionNode, 'unet'); + addLoRAs(state, g, tiledMultidiffusionNode, modelNode, null, clipSkipNode, posCondNode, negCondNode); + + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + const upscaleModelConfig = await fetchModelConfigWithTypeGuard(upscaleModel.key, isSpandrelImageToImageModelConfig); + + g.upsertMetadata({ + cfg_scale, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: Graph.getModelMetadataField(modelConfig), + seed, + steps, + scheduler, + vae: vae ?? undefined, + upscale_model: Graph.getModelMetadataField(upscaleModelConfig), + creativity, + structure, + }); + } + + g.setMetadataReceivingNode(l2iNode); + g.addEdgeToMetadata(upscaleNode, 'width', 'width'); + g.addEdgeToMetadata(upscaleNode, 'height', 'height'); + + let vaeNode; + if (vae) { + vaeNode = g.addNode({ + id: VAE_LOADER, + type: 'vae_loader', + vae_model: vae, + }); + } + + g.addEdge(vaeNode || modelNode, 'vae', i2lNode, 'vae'); + g.addEdge(vaeNode || modelNode, 'vae', l2iNode, 'vae'); + + g.addEdge(noiseNode, 'noise', tiledMultidiffusionNode, 'noise'); + g.addEdge(i2lNode, 'latents', tiledMultidiffusionNode, 'latents'); + g.addEdge(posCondNode, 'conditioning', tiledMultidiffusionNode, 'positive_conditioning'); + g.addEdge(negCondNode, 'conditioning', tiledMultidiffusionNode, 'negative_conditioning'); + + g.addEdge(tiledMultidiffusionNode, 'latents', l2iNode, 'latents'); + + const controlnetNode1 = g.addNode({ + id: 'controlnet_1', + type: 'controlnet', + control_model: tileControlnetModel, + control_mode: 'balanced', + resize_mode: 'just_resize', + control_weight: (structure + 10) * 0.0325 + 0.3, + begin_step_percent: 0, + end_step_percent: (structure + 10) * 0.025 + 0.3, + }); + + g.addEdge(unsharpMaskNode2, 'image', controlnetNode1, 'image'); + + const controlnetNode2 = g.addNode({ + id: 'controlnet_2', + type: 'controlnet', + control_model: tileControlnetModel, + control_mode: 'balanced', + resize_mode: 'just_resize', + control_weight: ((structure + 10) * 0.0325 + 0.15) * 0.45, + begin_step_percent: (structure + 10) * 0.025 + 0.3, + end_step_percent: 0.85, + }); + + g.addEdge(unsharpMaskNode2, 'image', controlnetNode2, 'image'); + + const collectNode = g.addNode({ + id: CONTROL_NET_COLLECT, + type: 'collect', + }); + g.addEdge(controlnetNode1, 'control', collectNode, 'item'); + g.addEdge(controlnetNode2, 'control', collectNode, 'item'); + + g.addEdge(collectNode, 'collection', tiledMultidiffusionNode, 'control'); + + return g.getGraph(); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts index 53d7d742ab0..200b8305e33 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts @@ -37,6 +37,7 @@ export const IP_ADAPTER_COLLECT = 'ip_adapter_collect'; export const T2I_ADAPTER_COLLECT = 't2i_adapter_collect'; export const METADATA = 'core_metadata'; export const ESRGAN = 'esrgan'; +export const SPANDREL = 'spandrel'; export const SDXL_MODEL_LOADER = 'sdxl_model_loader'; export const SDXL_DENOISE_LATENTS = 'sdxl_denoise_latents'; export const SDXL_REFINER_MODEL_LOADER = 'sdxl_refiner_model_loader'; @@ -53,6 +54,8 @@ export const PROMPT_REGION_NEGATIVE_COND_PREFIX = 'prompt_region_negative_cond'; export const PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX = 'prompt_region_positive_cond_inverted'; export const POSITIVE_CONDITIONING_COLLECT = 'positive_conditioning_collect'; export const NEGATIVE_CONDITIONING_COLLECT = 'negative_conditioning_collect'; +export const UNSHARP_MASK = 'unsharp_mask'; +export const TILED_MULTI_DIFFUSION_DENOISE_LATENTS = 'tiled_multi_diffusion_denoise_latents'; // friendly graph ids export const CONTROL_LAYERS_GRAPH = 'control_layers_graph'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts index a8be96e4847..e5a97bb50b9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts @@ -522,6 +522,21 @@ describe('Graph', () => { }); }); + describe('addEdgeToMetadata', () => { + it('should add an edge to the metadata node', () => { + const g = new Graph(); + const n1 = g.addNode({ + id: 'n1', + type: 'img_resize', + }); + g.upsertMetadata({ test: 'test' }); + g.addEdgeToMetadata(n1, 'width', 'width'); + const metadata = g._getMetadataNode(); + expect(g.getEdgesFrom(n1).length).toBe(1); + expect(g.getEdgesTo(metadata as unknown as AnyInvocation).length).toBe(1); + }); + }); + describe('setMetadataReceivingNode', () => { it('should set the metadata receiving node', () => { const g = new Graph(); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts index 008f86918a9..41142e56285 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts @@ -372,6 +372,21 @@ export class Graph { return metadataNode; } + /** + * Adds an edge from a node to a metadata field. Use this when the metadata value is dynamic depending on a node. + * @param fromNode The node to add an edge from + * @param fromField The field of the node to add an edge from + * @param metadataField The metadata field to add an edge to (will overwrite hard-coded metadata) + * @returns + */ + addEdgeToMetadata( + fromNode: TFrom, + fromField: OutputFields, + metadataField: string + ): Edge { + // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing + return this.addEdge(fromNode, fromField, this._getMetadataNode(), metadataField); + } /** * Set the node that should receive metadata. All other edges from the metadata node are deleted. * @param node The node to set as the receiving node diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts index 36233433674..3335e0f80d6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts @@ -8,7 +8,7 @@ import type { Invocation, S } from 'services/api/types'; export const addLoRAs = ( state: RootState, g: Graph, - denoise: Invocation<'denoise_latents'>, + denoise: Invocation<'denoise_latents'> | Invocation<'tiled_multi_diffusion_denoise_latents'>, modelLoader: Invocation<'main_model_loader'>, seamless: Invocation<'seamless'> | null, clipSkip: Invocation<'clip_skip'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index f38e8de570b..3125ab5ac3e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -8,7 +8,7 @@ import type { Invocation, S } from 'services/api/types'; export const addSDXLLoRas = ( state: RootState, g: Graph, - denoise: Invocation<'denoise_latents'>, + denoise: Invocation<'denoise_latents'> | Invocation<'tiled_multi_diffusion_denoise_latents'>, modelLoader: Invocation<'sdxl_model_loader'>, seamless: Invocation<'seamless'> | null, posCond: Invocation<'sdxl_compel_prompt'>, diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx new file mode 100644 index 00000000000..955c3ded5aa --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx @@ -0,0 +1,52 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { creativityChanged } from 'features/parameters/store/upscaleSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const ParamCreativity = () => { + const creativity = useAppSelector((s) => s.upscale.creativity); + const initial = 0; + const sliderMin = -10; + const sliderMax = 10; + const numberInputMin = -10; + const numberInputMax = 10; + const coarseStep = 1; + const fineStep = 1; + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const marks = useMemo(() => [sliderMin, 0, sliderMax], [sliderMax, sliderMin]); + const onChange = useCallback( + (v: number) => { + dispatch(creativityChanged(v)); + }, + [dispatch] + ); + + return ( + + {t('upscaling.creativity')} + + + + ); +}; + +export default memo(ParamCreativity); diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx index 6c1a3ab3a7e..d02bfd2b038 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx @@ -63,7 +63,7 @@ const ParamESRGANModel = () => { return ( - {t('models.esrganModel')} + {t('models.esrganModel')} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx new file mode 100644 index 00000000000..11216692ecb --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx @@ -0,0 +1,56 @@ +import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useModelCombobox } from 'common/hooks/useModelCombobox'; +import { upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSpandrelImageToImageModels } from 'services/api/hooks/modelsByType'; +import type { SpandrelImageToImageModelConfig } from 'services/api/types'; + +const ParamSpandrelModel = () => { + const { t } = useTranslation(); + const [modelConfigs, { isLoading }] = useSpandrelImageToImageModels(); + + const model = useAppSelector((s) => s.upscale.upscaleModel); + const dispatch = useAppDispatch(); + + const tooltipLabel = useMemo(() => { + if (!modelConfigs.length || !model) { + return; + } + return modelConfigs.find((m) => m.key === model?.key)?.description; + }, [modelConfigs, model]); + + const _onChange = useCallback( + (v: SpandrelImageToImageModelConfig | null) => { + dispatch(upscaleModelChanged(v)); + }, + [dispatch] + ); + + const { options, value, onChange, placeholder, noOptionsMessage } = useModelCombobox({ + modelConfigs, + onChange: _onChange, + selectedModel: model, + isLoading, + }); + + return ( + + {t('upscaling.upscaleModel')} + + + + + + + ); +}; + +export default memo(ParamSpandrelModel); diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx new file mode 100644 index 00000000000..07c543f5968 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx @@ -0,0 +1,52 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { structureChanged } from 'features/parameters/store/upscaleSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const ParamStructure = () => { + const structure = useAppSelector((s) => s.upscale.structure); + const initial = 0; + const sliderMin = -10; + const sliderMax = 10; + const numberInputMin = -10; + const numberInputMax = 10; + const coarseStep = 1; + const fineStep = 1; + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const marks = useMemo(() => [sliderMin, 0, sliderMax], [sliderMax, sliderMin]); + const onChange = useCallback( + (v: number) => { + dispatch(structureChanged(v)); + }, + [dispatch] + ); + + return ( + + {t('upscaling.structure')} + + + + ); +}; + +export default memo(ParamStructure); diff --git a/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts b/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts new file mode 100644 index 00000000000..f0b8d81ad82 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts @@ -0,0 +1,76 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import type { ParameterSpandrelImageToImageModel } from 'features/parameters/types/parameterSchemas'; +import type { ControlNetModelConfig, ImageDTO } from 'services/api/types'; + +interface UpscaleState { + _version: 1; + upscaleModel: ParameterSpandrelImageToImageModel | null; + upscaleInitialImage: ImageDTO | null; + structure: number; + creativity: number; + tileControlnetModel: ControlNetModelConfig | null; + scale: number; +} + +const initialUpscaleState: UpscaleState = { + _version: 1, + upscaleModel: null, + upscaleInitialImage: null, + structure: 0, + creativity: 0, + tileControlnetModel: null, + scale: 4, +}; + +export const upscaleSlice = createSlice({ + name: 'upscale', + initialState: initialUpscaleState, + reducers: { + upscaleModelChanged: (state, action: PayloadAction) => { + state.upscaleModel = action.payload; + }, + upscaleInitialImageChanged: (state, action: PayloadAction) => { + state.upscaleInitialImage = action.payload; + }, + structureChanged: (state, action: PayloadAction) => { + state.structure = action.payload; + }, + creativityChanged: (state, action: PayloadAction) => { + state.creativity = action.payload; + }, + tileControlnetModelChanged: (state, action: PayloadAction) => { + state.tileControlnetModel = action.payload; + }, + scaleChanged: (state, action: PayloadAction) => { + state.scale = action.payload; + }, + }, +}); + +export const { + upscaleModelChanged, + upscaleInitialImageChanged, + structureChanged, + creativityChanged, + tileControlnetModelChanged, + scaleChanged, +} = upscaleSlice.actions; + +export const selectUpscalelice = (state: RootState) => state.upscale; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrateUpscaleState = (state: any): any => { + if (!('_version' in state)) { + state._version = 1; + } + return state; +}; + +export const upscalePersistConfig: PersistConfig = { + name: upscaleSlice.name, + initialState: initialUpscaleState, + migrate: migrateUpscaleState, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index 8a808ed0c55..82620e488e3 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -126,6 +126,11 @@ const zParameterT2IAdapterModel = zModelIdentifierField; export type ParameterT2IAdapterModel = z.infer; // #endregion +// #region VAE Model +const zParameterSpandrelImageToImageModel = zModelIdentifierField; +export type ParameterSpandrelImageToImageModel = z.infer; +// #endregion + // #region Strength (l2l strength) export const zParameterStrength = z.number().min(0).max(1); export type ParameterStrength = z.infer; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 087efba616a..682878187f3 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -7,10 +7,14 @@ import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/P import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip'; import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis'; import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis'; +import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; +import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; +import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect'; import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; @@ -26,6 +30,8 @@ const formLabelProps2: FormLabelProps = { export const AdvancedSettingsAccordion = memo(() => { const vaeKey = useAppSelector((state) => state.generation.vae?.key); const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); + const activeTabName = useAppSelector(activeTabNameSelector); + const selectBadges = useMemo( () => createMemoizedSelector(selectGenerationSlice, (generation) => { @@ -48,9 +54,12 @@ export const AdvancedSettingsAccordion = memo(() => { if (generation.seamlessXAxis || generation.seamlessYAxis) { badges.push('seamless'); } + if (activeTabName === 'upscaling' && !generation.shouldRandomizeSeed) { + badges.push('Manual Seed'); + } return badges; }), - [vaeConfig] + [vaeConfig, activeTabName] ); const badges = useAppSelector(selectBadges); const { t } = useTranslation(); @@ -66,16 +75,27 @@ export const AdvancedSettingsAccordion = memo(() => { - - - - - - - - - - + {activeTabName === 'upscaling' && ( + + + + + + )} + {activeTabName !== 'upscaling' && ( + <> + + + + + + + + + + + + )} ); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/MultidiffusionWarning.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/MultidiffusionWarning.tsx new file mode 100644 index 00000000000..f3e2aa66048 --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/MultidiffusionWarning.tsx @@ -0,0 +1,68 @@ +import { Button, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; +import { tileControlnetModelChanged } from 'features/parameters/store/upscaleSlice'; +import { setActiveTab } from 'features/ui/store/uiSlice'; +import { useCallback, useEffect, useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useControlNetModels } from 'services/api/hooks/modelsByType'; + +export const MultidiffusionWarning = () => { + const { t } = useTranslation(); + const model = useAppSelector((s) => s.generation.model); + const { tileControlnetModel, upscaleModel } = useAppSelector((s) => s.upscale); + const dispatch = useAppDispatch(); + const [modelConfigs, { isLoading }] = useControlNetModels(); + const disabledTabs = useAppSelector((s) => s.config.disabledTabs); + const shouldShowButton = useMemo(() => !disabledTabs.includes('models'), [disabledTabs]); + + useEffect(() => { + const validModel = modelConfigs.find((cnetModel) => { + return cnetModel.base === model?.base && cnetModel.name.toLowerCase().includes('tile'); + }); + dispatch(tileControlnetModelChanged(validModel || null)); + }, [model?.base, modelConfigs, dispatch]); + + const warnings = useMemo(() => { + const _warnings: string[] = []; + if (!model) { + _warnings.push(t('upscaling.mainModelDesc')); + } + if (!tileControlnetModel) { + _warnings.push(t('upscaling.tileControlNetModelDesc')); + } + if (!upscaleModel) { + _warnings.push(t('upscaling.upscaleModelDesc')); + } + return _warnings; + }, [model, upscaleModel, tileControlnetModel, t]); + + const handleGoToModelManager = useCallback(() => { + dispatch(setActiveTab('models')); + $installModelsTab.set(3); + }, [dispatch]); + + if (!warnings.length || isLoading || !shouldShowButton) { + return null; + } + + return ( + + + + ), + }} + /> + + + {warnings.map((warning) => ( + {warning} + ))} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx new file mode 100644 index 00000000000..4f40adfdb3c --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx @@ -0,0 +1,55 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import type { TypesafeDroppableData } from 'features/dnd/types'; +import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; +import { t } from 'i18next'; +import { useCallback, useMemo } from 'react'; +import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; +import type { PostUploadAction } from 'services/api/types'; + +export const UpscaleInitialImage = () => { + const dispatch = useAppDispatch(); + const imageDTO = useAppSelector((s) => s.upscale.upscaleInitialImage); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_UPSCALE_INITIAL_IMAGE', + id: 'upscale-intial-image', + }), + [] + ); + + const postUploadAction = useMemo( + () => ({ + type: 'SET_UPSCALE_INITIAL_IMAGE', + }), + [] + ); + + const onReset = useCallback(() => { + dispatch(upscaleInitialImageChanged(null)); + }, [dispatch]); + + return ( + + + + {imageDTO && ( + + } + tooltip={t('controlnet.resetControlImage')} + /> + + )} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx new file mode 100644 index 00000000000..385add771aa --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx @@ -0,0 +1,50 @@ +import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { scaleChanged } from 'features/parameters/store/upscaleSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const marks = [2, 4, 8]; + +const formatValue = (val: string | number) => `${val}x`; + +export const UpscaleScaleSlider = memo(() => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const scale = useAppSelector((s) => s.upscale.scale); + + const onChange = useCallback( + (val: number) => { + dispatch(scaleChanged(val)); + }, + [dispatch] + ); + + return ( + + {t('upscaling.scale')} + + + + + + ); +}); + +UpscaleScaleSlider.displayName = 'UpscaleScaleSlider'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx new file mode 100644 index 00000000000..6002b76521d --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx @@ -0,0 +1,74 @@ +import { Expander, Flex, StandaloneAccordion } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import ParamCreativity from 'features/parameters/components/Upscale/ParamCreativity'; +import ParamSpandrelModel from 'features/parameters/components/Upscale/ParamSpandrelModel'; +import ParamStructure from 'features/parameters/components/Upscale/ParamStructure'; +import { selectUpscalelice } from 'features/parameters/store/upscaleSlice'; +import { UpscaleScaleSlider } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider'; +import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; +import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MultidiffusionWarning } from './MultidiffusionWarning'; +import { UpscaleInitialImage } from './UpscaleInitialImage'; + +const selector = createMemoizedSelector([selectUpscalelice], (upscaleSlice) => { + const { upscaleModel, upscaleInitialImage, scale } = upscaleSlice; + + const badges: string[] = []; + + if (upscaleModel) { + badges.push(upscaleModel.name); + } + + if (upscaleInitialImage) { + // Output height and width are scaled and rounded down to the nearest multiple of 8 + const outputWidth = Math.floor((upscaleInitialImage.width * scale) / 8) * 8; + const outputHeight = Math.floor((upscaleInitialImage.height * scale) / 8) * 8; + + badges.push(`${outputWidth}×${outputHeight}`); + } + + return { badges }; +}); + +export const UpscaleSettingsAccordion = memo(() => { + const { t } = useTranslation(); + const { badges } = useAppSelector(selector); + const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({ + id: 'upscale-settings', + defaultIsOpen: true, + }); + + const { isOpen: isOpenExpander, onToggle: onToggleExpander } = useExpanderToggle({ + id: 'upscale-settings-advanced', + defaultIsOpen: false, + }); + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}); + +UpscaleSettingsAccordion.displayName = 'UpscaleSettingsAccordion'; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 1968c64161f..b98d713b80d 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -11,7 +11,7 @@ import StatusIndicator from 'features/system/components/StatusIndicator'; import { selectConfigSlice } from 'features/system/store/configSlice'; import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; -import ParametersPanelTextToImage from 'features/ui/components/ParametersPanelTextToImage'; +import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage'; import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab'; import NodesTab from 'features/ui/components/tabs/NodesTab'; import QueueTab from 'features/ui/components/tabs/QueueTab'; @@ -28,19 +28,23 @@ import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; +import { MdZoomOutMap } from 'react-icons/md'; import { PiFlowArrowBold } from 'react-icons/pi'; import { RiBox2Line, RiBrushLine, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels'; -import ParametersPanel from './ParametersPanel'; +import ParametersPanelCanvas from './ParametersPanels/ParametersPanelCanvas'; +import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale'; import ResizeHandle from './tabs/ResizeHandle'; +import UpscalingTab from './tabs/UpscalingTab'; type TabData = { id: InvokeTabName; translationKey: string; icon: ReactElement; content: ReactNode; + parametersPanel?: ReactNode; }; const TAB_DATA: Record = { @@ -49,18 +53,28 @@ const TAB_DATA: Record = { translationKey: 'ui.tabs.generation', icon: , content: , + parametersPanel: , }, canvas: { id: 'canvas', translationKey: 'ui.tabs.canvas', icon: , content: , + parametersPanel: , + }, + upscaling: { + id: 'upscaling', + translationKey: 'ui.tabs.upscaling', + icon: , + content: , + parametersPanel: , }, workflows: { id: 'workflows', translationKey: 'ui.tabs.workflows', icon: , content: , + parametersPanel: , }, models: { id: 'models', @@ -81,7 +95,6 @@ const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) = ); const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['models', 'queue']; -const NO_OPTIONS_PANEL_TABS: InvokeTabName[] = ['models', 'queue']; const panelStyles: CSSProperties = { height: '100%', width: '100%' }; const GALLERY_MIN_SIZE_PX = 310; const GALLERY_MIN_SIZE_PCT = 20; @@ -103,7 +116,6 @@ const InvokeTabs = () => { e.target.blur(); } }, []); - const shouldShowOptionsPanel = useMemo(() => !NO_OPTIONS_PANEL_TABS.includes(activeTabName), [activeTabName]); const shouldShowGalleryPanel = useMemo(() => !NO_GALLERY_PANEL_TABS.includes(activeTabName), [activeTabName]); const tabs = useMemo( @@ -232,7 +244,7 @@ const InvokeTabs = () => { style={panelStyles} storage={panelStorage} > - {shouldShowOptionsPanel && ( + {!!TAB_DATA[activeTabName].parametersPanel && ( <> { onExpand={optionsPanel.onExpand} collapsible > - + {TAB_DATA[activeTabName].parametersPanel} { )} - {shouldShowOptionsPanel && } + {!!TAB_DATA[activeTabName].parametersPanel && } {shouldShowGalleryPanel && } ); }; export default memo(InvokeTabs); - -const ParametersPanelComponent = memo(() => { - const activeTabName = useAppSelector(activeTabNameSelector); - - if (activeTabName === 'workflows') { - return ; - } - if (activeTabName === 'generation') { - return ; - } - return ; -}); -ParametersPanelComponent.displayName = 'ParametersPanelComponent'; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx similarity index 86% rename from invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx rename to invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx index e8f73fd7868..622ed966961 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx @@ -10,7 +10,6 @@ import { ControlSettingsAccordion } from 'features/settingsAccordions/components import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion'; import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import type { CSSProperties } from 'react'; import { memo } from 'react'; @@ -20,8 +19,7 @@ const overlayScrollbarsStyles: CSSProperties = { width: '100%', }; -const ParametersPanel = () => { - const activeTabName = useAppSelector(activeTabNameSelector); +const ParametersPanelCanvas = () => { const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); return ( @@ -34,8 +32,8 @@ const ParametersPanel = () => { {isSDXL ? : } - {activeTabName !== 'generation' && } - {activeTabName === 'canvas' && } + + {isSDXL && } @@ -46,4 +44,4 @@ const ParametersPanel = () => { ); }; -export default memo(ParametersPanel); +export default memo(ParametersPanelCanvas); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx similarity index 100% rename from invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx rename to invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx new file mode 100644 index 00000000000..19979dea2f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx @@ -0,0 +1,41 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; +import { Prompts } from 'features/parameters/components/Prompts/Prompts'; +import QueueControls from 'features/queue/components/QueueControls'; +import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts'; +import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; +import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; +import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import type { CSSProperties } from 'react'; +import { memo } from 'react'; + +const overlayScrollbarsStyles: CSSProperties = { + height: '100%', + width: '100%', +}; + +const ParametersPanelUpscale = () => { + const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); + + return ( + + + + + + + {isSDXL ? : } + + + + + + + + + ); +}; + +export default memo(ParametersPanelUpscale); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UpscalingTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UpscalingTab.tsx new file mode 100644 index 00000000000..e2da68ceb7f --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UpscalingTab.tsx @@ -0,0 +1,13 @@ +import { Box } from '@invoke-ai/ui-library'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; +import { memo } from 'react'; + +const UpscalingTab = () => { + return ( + + + + ); +}; + +export default memo(UpscalingTab); diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx index 526a55b069f..5cf97b2d3e2 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx @@ -1,3 +1,3 @@ -export const TAB_NUMBER_MAP = ['generation', 'canvas', 'workflows', 'models', 'queue'] as const; +export const TAB_NUMBER_MAP = ['generation', 'canvas', 'upscaling', 'workflows', 'models', 'queue'] as const; export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number]; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 2040021d6d4..6f36866dce6 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -104,6 +104,10 @@ export const imagesApi = api.injectEndpoints({ type: 'Board', id: boardId, }, + { + type: 'BoardImagesTotal', + id: boardId, + }, ]; }, }), @@ -136,6 +140,10 @@ export const imagesApi = api.injectEndpoints({ type: 'Board', id: boardId, }, + { + type: 'BoardImagesTotal', + id: boardId, + }, ]; return tags; @@ -169,6 +177,10 @@ export const imagesApi = api.injectEndpoints({ type: 'Board', id: boardId, }, + { + type: 'BoardImagesTotal', + id: boardId, + }, ]; }, }), @@ -300,6 +312,10 @@ export const imagesApi = api.injectEndpoints({ type: 'Board', id: boardId, }, + { + type: 'BoardImagesTotal', + id: boardId, + }, ]; }, }), @@ -362,6 +378,10 @@ export const imagesApi = api.injectEndpoints({ }, { type: 'Board', id: board_id }, { type: 'Board', id: imageDTO.board_id ?? 'none' }, + { + type: 'BoardImagesTotal', + id: imageDTO.board_id ?? 'none', + }, ]; }, }), @@ -393,6 +413,11 @@ export const imagesApi = api.injectEndpoints({ }, { type: 'Board', id: imageDTO.board_id ?? 'none' }, { type: 'Board', id: 'none' }, + { + type: 'BoardImagesTotal', + id: imageDTO.board_id ?? 'none', + }, + { type: 'BoardImagesTotal', id: 'none' }, ]; }, }), @@ -434,6 +459,10 @@ export const imagesApi = api.injectEndpoints({ tags.push({ type: 'Image', id: imageDTO.image_name }); } tags.push({ type: 'Board', id: board_id }); + tags.push({ + type: 'BoardImagesTotal', + id: board_id ?? 'none', + }); return tags; }, }), @@ -480,6 +509,10 @@ export const imagesApi = api.injectEndpoints({ } tags.push({ type: 'Image', id: image_name }); tags.push({ type: 'Board', id: board_id }); + tags.push({ + type: 'BoardImagesTotal', + id: board_id ?? 'none', + }); }); return tags; diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 890de69e351..9f1e2b3bd2e 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -6568,7 +6568,7 @@ export type components = { tiled?: boolean; /** * Tile Size - * @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the + * @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage. * @default 0 */ tile_size?: number; @@ -7304,146 +7304,146 @@ export type components = { project_id: string | null; }; InvocationOutputMap: { - noise: components["schemas"]["NoiseOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - color_correct: components["schemas"]["ImageOutput"]; + merge_metadata: components["schemas"]["MetadataOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + color_map_image_processor: components["schemas"]["ImageOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + img_scale: components["schemas"]["ImageOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; - rand_int: components["schemas"]["IntegerOutput"]; - latents: components["schemas"]["LatentsOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; - controlnet: components["schemas"]["ControlOutput"]; img_blur: components["schemas"]["ImageOutput"]; - freeu: components["schemas"]["UNetOutput"]; - string: components["schemas"]["StringOutput"]; - boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - boolean: components["schemas"]["BooleanOutput"]; - lresize: components["schemas"]["LatentsOutput"]; - mask_from_id: components["schemas"]["ImageOutput"]; - string_split: components["schemas"]["String2Output"]; - create_gradient_mask: components["schemas"]["GradientMaskOutput"]; - seamless: components["schemas"]["SeamlessModeOutput"]; merge_tiles_to_image: components["schemas"]["ImageOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; - mask_edge: components["schemas"]["ImageOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; img_mul: components["schemas"]["ImageOutput"]; - spandrel_image_to_image: components["schemas"]["ImageOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; + lresize: components["schemas"]["LatentsOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; tomask: components["schemas"]["ImageOutput"]; - color_map_image_processor: components["schemas"]["ImageOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - infill_rgba: components["schemas"]["ImageOutput"]; - model_identifier: components["schemas"]["ModelIdentifierOutput"]; - metadata: components["schemas"]["MetadataOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; + mul: components["schemas"]["IntegerOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; add: components["schemas"]["IntegerOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; - integer: components["schemas"]["IntegerOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; integer_collection: components["schemas"]["IntegerCollectionOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - show_image: components["schemas"]["ImageOutput"]; - string_replace: components["schemas"]["StringOutput"]; - prompt_from_file: components["schemas"]["StringCollectionOutput"]; - string_join: components["schemas"]["StringOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + string_split: components["schemas"]["String2Output"]; + tile_image_processor: components["schemas"]["ImageOutput"]; infill_cv2: components["schemas"]["ImageOutput"]; + tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + save_image: components["schemas"]["ImageOutput"]; + controlnet: components["schemas"]["ControlOutput"]; + float_math: components["schemas"]["FloatOutput"]; sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - content_shuffle_image_processor: components["schemas"]["ImageOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + sub: components["schemas"]["IntegerOutput"]; + div: components["schemas"]["IntegerOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + esrgan: components["schemas"]["ImageOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + rand_int: components["schemas"]["IntegerOutput"]; + lora_selector: components["schemas"]["LoRASelectorOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; lineart_anime_image_processor: components["schemas"]["ImageOutput"]; - string_split_neg: components["schemas"]["StringPosNegOutput"]; - round_float: components["schemas"]["FloatOutput"]; - rand_float: components["schemas"]["FloatOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + rectangle_mask: components["schemas"]["MaskOutput"]; lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - sub: components["schemas"]["IntegerOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; + freeu: components["schemas"]["UNetOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + string_split_neg: components["schemas"]["StringPosNegOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + noise: components["schemas"]["NoiseOutput"]; float_range: components["schemas"]["FloatCollectionOutput"]; - save_image: components["schemas"]["ImageOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; - hed_image_processor: components["schemas"]["ImageOutput"]; dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + latents: components["schemas"]["LatentsOutput"]; + leres_image_processor: components["schemas"]["ImageOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + string_join: components["schemas"]["StringOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; lineart_image_processor: components["schemas"]["ImageOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + string: components["schemas"]["StringOutput"]; + integer: components["schemas"]["IntegerOutput"]; + string_replace: components["schemas"]["StringOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; image: components["schemas"]["ImageOutput"]; - merge_metadata: components["schemas"]["MetadataOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - img_watermark: components["schemas"]["ImageOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - lora_selector: components["schemas"]["LoRASelectorOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - img_conv: components["schemas"]["ImageOutput"]; - face_mask_detection: components["schemas"]["FaceMaskOutput"]; - infill_patchmatch: components["schemas"]["ImageOutput"]; - rectangle_mask: components["schemas"]["MaskOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; - tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; step_param_easing: components["schemas"]["FloatCollectionOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; - mediapipe_face_processor: components["schemas"]["ImageOutput"]; - calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; - lscale: components["schemas"]["LatentsOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + float: components["schemas"]["FloatOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + boolean: components["schemas"]["BooleanOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; color: components["schemas"]["ColorOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; - conditioning: components["schemas"]["ConditioningOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - img_pad_crop: components["schemas"]["ImageOutput"]; - mul: components["schemas"]["IntegerOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + rand_float: components["schemas"]["FloatOutput"]; create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; - img_chan: components["schemas"]["ImageOutput"]; - leres_image_processor: components["schemas"]["ImageOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; + spandrel_image_to_image: components["schemas"]["ImageOutput"]; + show_image: components["schemas"]["ImageOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; infill_tile: components["schemas"]["ImageOutput"]; - i2l: components["schemas"]["LatentsOutput"]; - string_join_three: components["schemas"]["StringOutput"]; - ip_adapter: components["schemas"]["IPAdapterOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - float: components["schemas"]["FloatOutput"]; - compel: components["schemas"]["ConditioningOutput"]; - range_of_size: components["schemas"]["IntegerCollectionOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; + clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; ideal_size: components["schemas"]["IdealSizeOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; l2i: components["schemas"]["ImageOutput"]; - latents_collection: components["schemas"]["LatentsCollectionOutput"]; - float_math: components["schemas"]["FloatOutput"]; - img_hue_adjust: components["schemas"]["ImageOutput"]; - img_scale: components["schemas"]["ImageOutput"]; - esrgan: components["schemas"]["ImageOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; vae_loader: components["schemas"]["VAEOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - range: components["schemas"]["IntegerCollectionOutput"]; - calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; - image_mask_to_tensor: components["schemas"]["MaskOutput"]; - blank_image: components["schemas"]["ImageOutput"]; - div: components["schemas"]["IntegerOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + model_identifier: components["schemas"]["ModelIdentifierOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + round_float: components["schemas"]["FloatOutput"]; }; /** * InvocationStartedEvent @@ -7783,7 +7783,7 @@ export type components = { tiled?: boolean; /** * Tile Size - * @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the + * @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage. * @default 0 */ tile_size?: number; @@ -11982,6 +11982,24 @@ export type components = { * @default null */ image_to_image_model?: components["schemas"]["ModelIdentifierField"]; + /** + * Tile Size + * @description The tile size for tiled image-to-image. Set to 0 to disable tiling. + * @default 512 + */ + tile_size?: number; + /** + * Scale + * @description The final scale of the output image. If the model does not upscale the image, this will be ignored. + * @default 4 + */ + scale?: number; + /** + * Fit To Multiple Of 8 + * @description If true, the output image will be resized to the nearest multiple of 8 in both dimensions. + * @default false + */ + fit_to_multiple_of_8?: boolean; /** * type * @default spandrel_image_to_image diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index b8ffa46c82c..5255e5964ac 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -205,6 +205,10 @@ type CanvasInitialImageAction = { type: 'SET_CANVAS_INITIAL_IMAGE'; }; +type UpscaleInitialImageAction = { + type: 'SET_UPSCALE_INITIAL_IMAGE'; +}; + type ToastAction = { type: 'TOAST'; title?: string; @@ -223,4 +227,5 @@ export type PostUploadAction = | CALayerImagePostUploadAction | IPALayerImagePostUploadAction | RGLayerIPAdapterImagePostUploadAction - | IILayerImagePostUploadAction; + | IILayerImagePostUploadAction + | UpscaleInitialImageAction; diff --git a/pyproject.toml b/pyproject.toml index 9953c1c1a04..9acaa17e44d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "transformers==4.41.1", # Core application dependencies, pinned for reproducible builds. - "fastapi-events==0.11.0", + "fastapi-events==0.11.1", "fastapi==0.111.0", "huggingface-hub==0.23.1", "pydantic-settings==2.2.1",