Merge branch 'main' into stalker7779/modular_rescale_cfg

This commit is contained in:
Ryan Dick 2024-07-23 09:34:22 -04:00 committed by GitHub
commit d014dc94fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1393 additions and 259 deletions

View File

@ -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.tiles import calc_tiles_min_overlap
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 @@ class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
),
)
@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 @@ class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
# 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)
# 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]
# 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)
# Run the model on each tile.
with spandrel_model_info as spandrel_model:
assert isinstance(spandrel_model, SpandrelImageToImageModel)
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
# 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]
# 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)
# 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")
)
# Run the model on the tile.
output_tile = spandrel_model.run(input_tile)
image_tensor = image_tensor.to(device=spandrel_model.device, dtype=spandrel_model.dtype)
# 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"))
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:, :]
# 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)

View File

@ -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 <LinkComponent>Model Manager</LinkComponent> 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)"
}
}
}

View File

@ -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);

View File

@ -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<typeof enqueueRequested> =>
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();
}
},
});
};

View File

@ -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
*/

View File

@ -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(

View File

@ -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));
};

View File

@ -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) => {

View File

@ -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;
});

View File

@ -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)

View File

@ -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;

View File

@ -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':

View File

@ -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 (
<>
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
@ -185,6 +191,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
)}
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToUpscale} id="send-to-upscale">
{t('parameters.sendToUpscale')}
</MenuItem>
<MenuDivider />
<MenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
{t('boards.changeBoard')}

View File

@ -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 (
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
@ -23,7 +29,7 @@ export const ViewerToolbar = memo(() => {
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
{tab !== 'workflows' && <ViewerToggleMenu />}
{showToggle && <ViewerToggleMenu />}
</Flex>
</Flex>
</Flex>

View File

@ -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]);

View File

@ -30,7 +30,7 @@ export const StarterModelsResultItem = ({ result }: Props) => {
<Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}>
<Flex fontSize="sm" flexDir="column">
<Flex gap={3}>
<Badge h="min-content">{result.type.replace('_', ' ')}</Badge>
<Badge h="min-content">{result.type.replaceAll('_', ' ')}</Badge>
<ModelBaseBadge base={result.base} />
<Text fontWeight="semibold">{result.name}</Text>
</Flex>

View File

@ -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<HTMLInputElement> = useCallback((e) => {
setSearchTerm(e.target.value.trim());
setSearchTerm(e.target.value);
}, []);
const clearSearch = useCallback(() => {

View File

@ -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 (
<Flex layerStyle="first" borderRadius="base" w="full" h="full" flexDir="column" gap={4}>
<Heading fontSize="xl">{t('modelManager.addModel')}</Heading>
<Tabs variant="collapse" height="50%" display="flex" flexDir="column" defaultIndex={defaultIndex}>
<Tabs variant="collapse" height="50%" display="flex" flexDir="column" index={index} onChange={onChange}>
<TabList>
<Tab>{t('modelManager.urlOrLocalPath')}</Tab>
<Tab>{t('modelManager.huggingFace')}</Tab>

View File

@ -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<GraphType> => {
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();
};

View File

@ -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';

View File

@ -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();

View File

@ -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<TFrom extends AnyInvocation>(
fromNode: TFrom,
fromField: OutputFields<TFrom>,
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

View File

@ -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'>,

View File

@ -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'>,

View File

@ -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 (
<FormControl>
<FormLabel>{t('upscaling.creativity')}</FormLabel>
<CompositeSlider
value={creativity}
defaultValue={initial}
min={sliderMin}
max={sliderMax}
step={coarseStep}
fineStep={fineStep}
onChange={onChange}
marks={marks}
/>
<CompositeNumberInput
value={creativity}
defaultValue={initial}
min={numberInputMin}
max={numberInputMax}
step={coarseStep}
fineStep={fineStep}
onChange={onChange}
/>
</FormControl>
);
};
export default memo(ParamCreativity);

View File

@ -63,7 +63,7 @@ const ParamESRGANModel = () => {
return (
<FormControl orientation="vertical">
<FormLabel>{t('models.esrganModel')} </FormLabel>
<FormLabel>{t('models.esrganModel')}</FormLabel>
<Combobox value={value} onChange={onChange} options={options} />
</FormControl>
);

View File

@ -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 (
<FormControl orientation="vertical">
<FormLabel>{t('upscaling.upscaleModel')}</FormLabel>
<Tooltip label={tooltipLabel}>
<Box w="full">
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</Box>
</Tooltip>
</FormControl>
);
};
export default memo(ParamSpandrelModel);

View File

@ -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 (
<FormControl>
<FormLabel>{t('upscaling.structure')}</FormLabel>
<CompositeSlider
value={structure}
defaultValue={initial}
min={sliderMin}
max={sliderMax}
step={coarseStep}
fineStep={fineStep}
onChange={onChange}
marks={marks}
/>
<CompositeNumberInput
value={structure}
defaultValue={initial}
min={numberInputMin}
max={numberInputMax}
step={coarseStep}
fineStep={fineStep}
onChange={onChange}
/>
</FormControl>
);
};
export default memo(ParamStructure);

View File

@ -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<ParameterSpandrelImageToImageModel | null>) => {
state.upscaleModel = action.payload;
},
upscaleInitialImageChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.upscaleInitialImage = action.payload;
},
structureChanged: (state, action: PayloadAction<number>) => {
state.structure = action.payload;
},
creativityChanged: (state, action: PayloadAction<number>) => {
state.creativity = action.payload;
},
tileControlnetModelChanged: (state, action: PayloadAction<ControlNetModelConfig | null>) => {
state.tileControlnetModel = action.payload;
},
scaleChanged: (state, action: PayloadAction<number>) => {
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<UpscaleState> = {
name: upscaleSlice.name,
initialState: initialUpscaleState,
migrate: migrateUpscaleState,
persistDenylist: [],
};

View File

@ -126,6 +126,11 @@ const zParameterT2IAdapterModel = zModelIdentifierField;
export type ParameterT2IAdapterModel = z.infer<typeof zParameterT2IAdapterModel>;
// #endregion
// #region VAE Model
const zParameterSpandrelImageToImageModel = zModelIdentifierField;
export type ParameterSpandrelImageToImageModel = z.infer<typeof zParameterSpandrelImageToImageModel>;
// #endregion
// #region Strength (l2l strength)
export const zParameterStrength = z.number().min(0).max(1);
export type ParameterStrength = z.infer<typeof zParameterStrength>;

View File

@ -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(() => {
<ParamVAEModelSelect />
<ParamVAEPrecision />
</Flex>
<FormControlGroup formLabelProps={formLabelProps}>
<ParamClipSkip />
<ParamCFGRescaleMultiplier />
</FormControlGroup>
<Flex gap={4} w="full">
<FormControlGroup formLabelProps={formLabelProps2}>
<ParamSeamlessXAxis />
<ParamSeamlessYAxis />
</FormControlGroup>
</Flex>
{activeTabName === 'upscaling' && (
<Flex gap={4} alignItems="center">
<ParamSeedNumberInput />
<ParamSeedShuffle />
<ParamSeedRandomize />
</Flex>
)}
{activeTabName !== 'upscaling' && (
<>
<FormControlGroup formLabelProps={formLabelProps}>
<ParamClipSkip />
<ParamCFGRescaleMultiplier />
</FormControlGroup>
<Flex gap={4} w="full">
<FormControlGroup formLabelProps={formLabelProps2}>
<ParamSeamlessXAxis />
<ParamSeamlessYAxis />
</FormControlGroup>
</Flex>
</>
)}
</Flex>
</StandaloneAccordion>
);

View File

@ -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 (
<Flex bg="error.500" borderRadius="base" padding={4} direction="column" fontSize="sm" gap={2}>
<Text>
<Trans
i18nKey="upscaling.missingModelsWarning"
components={{
LinkComponent: (
<Button size="sm" flexGrow={0} variant="link" color="base.50" onClick={handleGoToModelManager} />
),
}}
/>
</Text>
<UnorderedList>
{warnings.map((warning) => (
<ListItem key={warning}>{warning}</ListItem>
))}
</UnorderedList>
</Flex>
);
};

View File

@ -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<TypesafeDroppableData | undefined>(
() => ({
actionType: 'SET_UPSCALE_INITIAL_IMAGE',
id: 'upscale-intial-image',
}),
[]
);
const postUploadAction = useMemo<PostUploadAction>(
() => ({
type: 'SET_UPSCALE_INITIAL_IMAGE',
}),
[]
);
const onReset = useCallback(() => {
dispatch(upscaleInitialImageChanged(null));
}, [dispatch]);
return (
<Flex justifyContent="flex-start">
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
<IAIDndImage
droppableData={droppableData}
imageDTO={imageDTO || undefined}
postUploadAction={postUploadAction}
/>
{imageDTO && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={onReset}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('controlnet.resetControlImage')}
/>
</Flex>
)}
</Flex>
</Flex>
);
};

View File

@ -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 (
<FormControl orientation="vertical" gap={0}>
<FormLabel m={0}>{t('upscaling.scale')}</FormLabel>
<Flex w="full" gap={4}>
<CompositeSlider
min={2}
max={8}
value={scale}
onChange={onChange}
marks={marks}
formatValue={formatValue}
defaultValue={4}
/>
<CompositeNumberInput
maxW={20}
value={scale}
onChange={onChange}
defaultValue={4}
min={2}
max={16}
format={formatValue}
/>
</Flex>
</FormControl>
);
});
UpscaleScaleSlider.displayName = 'UpscaleScaleSlider';

View File

@ -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 (
<StandaloneAccordion label="Upscale" badges={badges} isOpen={isOpenAccordion} onToggle={onToggleAccordion}>
<Flex pt={4} px={4} w="full" h="full" flexDir="column" data-testid="upscale-settings-accordion">
<Flex flexDir="column" gap={4}>
<Flex gap={4}>
<UpscaleInitialImage />
<Flex direction="column" w="full" alignItems="center" gap={2}>
<ParamSpandrelModel />
<UpscaleScaleSlider />
</Flex>
</Flex>
<MultidiffusionWarning />
</Flex>
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<Flex gap={4} pb={4} flexDir="column">
<ParamCreativity />
<ParamStructure />
</Flex>
</Expander>
</Flex>
</StandaloneAccordion>
);
});
UpscaleSettingsAccordion.displayName = 'UpscaleSettingsAccordion';

View File

@ -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<InvokeTabName, TabData> = {
@ -49,18 +53,28 @@ const TAB_DATA: Record<InvokeTabName, TabData> = {
translationKey: 'ui.tabs.generation',
icon: <RiInputMethodLine />,
content: <TextToImageTab />,
parametersPanel: <ParametersPanelTextToImage />,
},
canvas: {
id: 'canvas',
translationKey: 'ui.tabs.canvas',
icon: <RiBrushLine />,
content: <UnifiedCanvasTab />,
parametersPanel: <ParametersPanelCanvas />,
},
upscaling: {
id: 'upscaling',
translationKey: 'ui.tabs.upscaling',
icon: <MdZoomOutMap />,
content: <UpscalingTab />,
parametersPanel: <ParametersPanelUpscale />,
},
workflows: {
id: 'workflows',
translationKey: 'ui.tabs.workflows',
icon: <PiFlowArrowBold />,
content: <NodesTab />,
parametersPanel: <NodeEditorPanelGroup />,
},
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 && (
<>
<Panel
id="options-panel"
@ -244,7 +256,7 @@ const InvokeTabs = () => {
onExpand={optionsPanel.onExpand}
collapsible
>
<ParametersPanelComponent />
{TAB_DATA[activeTabName].parametersPanel}
</Panel>
<ResizeHandle
id="options-main-handle"
@ -280,23 +292,10 @@ const InvokeTabs = () => {
</>
)}
</PanelGroup>
{shouldShowOptionsPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
{!!TAB_DATA[activeTabName].parametersPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
{shouldShowGalleryPanel && <FloatingGalleryButton panelApi={galleryPanel} />}
</Tabs>
);
};
export default memo(InvokeTabs);
const ParametersPanelComponent = memo(() => {
const activeTabName = useAppSelector(activeTabNameSelector);
if (activeTabName === 'workflows') {
return <NodeEditorPanelGroup />;
}
if (activeTabName === 'generation') {
return <ParametersPanelTextToImage />;
}
return <ParametersPanel />;
});
ParametersPanelComponent.displayName = 'ParametersPanelComponent';

View File

@ -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 ? <SDXLPrompts /> : <Prompts />}
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
{activeTabName !== 'generation' && <ControlSettingsAccordion />}
{activeTabName === 'canvas' && <CompositingSettingsAccordion />}
<ControlSettingsAccordion />
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</Flex>
@ -46,4 +44,4 @@ const ParametersPanel = () => {
);
};
export default memo(ParametersPanel);
export default memo(ParametersPanelCanvas);

View File

@ -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 (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
{isSDXL ? <SDXLPrompts /> : <Prompts />}
<UpscaleSettingsAccordion />
<GenerationSettingsAccordion />
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
</Box>
</Flex>
</Flex>
);
};
export default memo(ParametersPanelUpscale);

View File

@ -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 (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ImageViewer />
</Box>
);
};
export default memo(UpscalingTab);

View File

@ -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];

View File

@ -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;

View File

@ -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"];
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"];
img_mul: components["schemas"]["ImageOutput"];
spandrel_image_to_image: components["schemas"]["ImageOutput"];
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"];
add: components["schemas"]["IntegerOutput"];
img_channel_multiply: components["schemas"]["ImageOutput"];
integer: components["schemas"]["IntegerOutput"];
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"];
infill_cv2: components["schemas"]["ImageOutput"];
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"];
lineart_anime_image_processor: components["schemas"]["ImageOutput"];
string_split_neg: components["schemas"]["StringPosNegOutput"];
round_float: components["schemas"]["FloatOutput"];
rand_float: components["schemas"]["FloatOutput"];
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"];
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"];
lineart_image_processor: components["schemas"]["ImageOutput"];
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"];
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"];
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"];
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"];
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
img_chan: components["schemas"]["ImageOutput"];
leres_image_processor: components["schemas"]["ImageOutput"];
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"];
ideal_size: components["schemas"]["IdealSizeOutput"];
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
depth_anything_image_processor: components["schemas"]["ImageOutput"];
mask_combine: 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"];
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"];
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
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"];
img_blur: components["schemas"]["ImageOutput"];
merge_tiles_to_image: components["schemas"]["ImageOutput"];
latents_collection: components["schemas"]["LatentsCollectionOutput"];
img_mul: 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"];
mul: components["schemas"]["IntegerOutput"];
seamless: components["schemas"]["SeamlessModeOutput"];
canny_image_processor: components["schemas"]["ImageOutput"];
metadata_item: components["schemas"]["MetadataItemOutput"];
add: components["schemas"]["IntegerOutput"];
crop_latents: components["schemas"]["LatentsOutput"];
integer_collection: components["schemas"]["IntegerCollectionOutput"];
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"];
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"];
compel: components["schemas"]["ConditioningOutput"];
rectangle_mask: components["schemas"]["MaskOutput"];
lora_collection_loader: components["schemas"]["LoRALoaderOutput"];
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"];
dw_openpose_image_processor: components["schemas"]["ImageOutput"];
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"];
prompt_from_file: components["schemas"]["StringCollectionOutput"];
sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"];
mask_from_id: components["schemas"]["ImageOutput"];
normalbae_image_processor: components["schemas"]["ImageOutput"];
infill_rgba: components["schemas"]["ImageOutput"];
step_param_easing: components["schemas"]["FloatCollectionOutput"];
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"];
calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"];
scheduler: components["schemas"]["SchedulerOutput"];
rand_float: components["schemas"]["FloatOutput"];
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
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"];
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"];
ideal_size: components["schemas"]["IdealSizeOutput"];
img_lerp: components["schemas"]["ImageOutput"];
l2i: components["schemas"]["ImageOutput"];
create_gradient_mask: components["schemas"]["GradientMaskOutput"];
vae_loader: components["schemas"]["VAEOutput"];
calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"];
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
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

View File

@ -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;

View File

@ -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",