From e537de2f6d39f9c33dc289167a8e882ac8628d26 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 7 May 2024 17:47:09 +1000 Subject: [PATCH] feat(ui): layers recall This still needs some finessing - needs logic depending on the tab... --- invokeai/frontend/web/public/locales/en.json | 4 ++- .../controlLayers/store/controlLayersSlice.ts | 14 +++++++++ .../ImageMetadataActions.tsx | 1 + .../src/features/metadata/util/handlers.ts | 31 +++++++++++++++++-- .../web/src/features/metadata/util/parsers.ts | 16 ++++++++++ .../src/features/metadata/util/recallers.ts | 24 ++++++++++++++ .../src/features/metadata/util/validators.ts | 25 +++++++++++++++ 7 files changed, 112 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 83e80e8a81..0c7c6cd6e1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1559,7 +1559,9 @@ "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults", - "noLayersAdded": "No Layers Added" + "noLayersAdded": "No Layers Added", + "layers_one": "Layer", + "layers_other": "Layers" }, "ui": { "tabs": { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index bbe0464aa7..6f6176c242 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -255,6 +255,10 @@ export const controlLayersSlice = createSlice({ payload: { layerId: uuidv4(), controlAdapter }, }), }, + caLayerRecalled: (state, action: PayloadAction) => { + state.layers.push({ ...action.payload, isSelected: true }); + state.selectedLayerId = action.payload.id; + }, caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectCALayerOrThrow(state, layerId); @@ -368,6 +372,9 @@ export const controlLayersSlice = createSlice({ }, prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), }, + ipaLayerRecalled: (state, action: PayloadAction) => { + state.layers.push(action.payload); + }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); @@ -462,6 +469,10 @@ export const controlLayersSlice = createSlice({ }, prepare: () => ({ payload: { layerId: uuidv4() } }), }, + rgLayerRecalled: (state, action: PayloadAction) => { + state.layers.push({ ...action.payload, isSelected: true }); + state.selectedLayerId = action.payload.id; + }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; const layer = selectRGLayerOrThrow(state, layerId); @@ -805,6 +816,7 @@ export const { allLayersDeleted, // CA Layers caLayerAdded, + caLayerRecalled, caLayerImageChanged, caLayerProcessedImageChanged, caLayerModelChanged, @@ -817,6 +829,7 @@ export const { caLayerT2IAdaptersDeleted, // IPA Layers ipaLayerAdded, + ipaLayerRecalled, ipaLayerImageChanged, ipaLayerMethodChanged, ipaLayerModelChanged, @@ -827,6 +840,7 @@ export const { caOrIPALayerBeginEndStepPctChanged, // RG Layers rgLayerAdded, + rgLayerRecalled, rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, rgLayerPreviewColorChanged, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index c73f5b1817..7dd2be55b0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -51,6 +51,7 @@ const ImageMetadataActions = (props: Props) => { + {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 467f702cea..4cbe69668f 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -1,5 +1,6 @@ import { objectKeys } from 'common/util/objectKeys'; import { toast } from 'common/util/toast'; +import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, @@ -52,6 +53,9 @@ const renderControlAdapterValueV2: MetadataRenderValueFunc = async (value) => { + return `${value.length} ${t('controlLayers.layers', { count: value.length })}`; +}; const parameterSetToast = (parameter: string, description?: string) => { toast({ @@ -171,6 +175,7 @@ const buildHandlers: BuildMetadataHandlers = ({ itemValidator, renderValue, renderItemValue, + getIsVisible, }) => ({ parse: buildParse({ parser, getLabel }), parseItem: itemParser ? buildParseItem({ itemParser, getLabel }) : undefined, @@ -179,6 +184,7 @@ const buildHandlers: BuildMetadataHandlers = ({ getLabel, renderValue: renderValue ?? resolveToString, renderItemValue: renderItemValue ?? resolveToString, + getIsVisible, }); export const handlers = { @@ -380,6 +386,14 @@ export const handlers = { itemValidator: validators.t2iAdapterV2, renderItemValue: renderControlAdapterValueV2, }), + layers: buildHandlers({ + getLabel: () => t('controlLayers.layers_other'), + parser: parsers.layers, + recaller: recallers.layers, + validator: validators.layers, + renderValue: renderLayersValue, + getIsVisible: (value) => value.length > 0, + }), } as const; export const parseAndRecallPrompts = async (metadata: unknown) => { @@ -435,9 +449,22 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => { }; // These handlers should be omitted when recalling to control layers -const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters']; +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [ + 'controlNets', + 'ipAdapters', + 't2iAdapters', + 'controlNetsV2', + 'ipAdaptersV2', + 't2iAdaptersV2', +]; // These handlers should be omitted when recalling to the rest of the app -const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNetsV2', 'ipAdaptersV2', 't2iAdaptersV2']; +const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [ + 'controlNetsV2', + 'ipAdaptersV2', + 't2iAdaptersV2', + 'initialImage', + 'layers', +]; export const parseAndRecallAllMetadata = async ( metadata: unknown, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 8641977b1f..25ab72536a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -5,6 +5,8 @@ import { initialT2IAdapter, } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; +import type { Layer } from 'features/controlLayers/store/types'; +import { zLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -623,6 +625,19 @@ const parseIPAdapterV2: MetadataParseFunc = async (me return ipAdapter; }; +const parseLayers: MetadataParseFunc = async (metadata) => { + try { + const layersRaw = await getProperty(metadata, 'layers', isArray); + const parseResults = await Promise.allSettled(layersRaw.map((layerRaw) => zLayer.parseAsync(layerRaw))); + const layers = parseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + return layers; + } catch { + return []; + } +}; + const parseAllIPAdaptersV2: MetadataParseFunc = async (metadata) => { try { const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); @@ -678,4 +693,5 @@ export const parsers = { t2iAdaptersV2: parseAllT2IAdaptersV2, ipAdapterV2: parseIPAdapterV2, ipAdaptersV2: parseAllIPAdaptersV2, + layers: parseLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index b29d937159..3782c789e0 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -6,19 +6,24 @@ import { t2iAdaptersReset, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { + allLayersDeleted, caLayerAdded, caLayerControlNetsDeleted, + caLayerRecalled, caLayerT2IAdaptersDeleted, heightChanged, iiLayerAdded, ipaLayerAdded, + ipaLayerRecalled, ipaLayersDeleted, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, positivePromptChanged, + rgLayerRecalled, widthChanged, } from 'features/controlLayers/store/controlLayersSlice'; +import type { Layer } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; @@ -290,6 +295,24 @@ const recallIPAdaptersV2: MetadataRecallFunc = (ipA }); }; +const recallLayers: MetadataRecallFunc = (layers) => { + const { dispatch } = getStore(); + dispatch(allLayersDeleted()); + for (const l of layers) { + switch (l.type) { + case 'control_adapter_layer': + dispatch(caLayerRecalled(l)); + break; + case 'ip_adapter_layer': + dispatch(ipaLayerRecalled(l)); + break; + case 'regional_guidance_layer': + dispatch(rgLayerRecalled(l)); + break; + } + } +}; + export const recallers = { positivePrompt: recallPositivePrompt, negativePrompt: recallNegativePrompt, @@ -330,4 +353,5 @@ export const recallers = { t2iAdaptersV2: recallT2IAdaptersV2, ipAdapterV2: recallIPAdapterV2, ipAdaptersV2: recallIPAdaptersV2, + layers: recallLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index d09321003f..aca988f85a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, @@ -165,6 +166,29 @@ const validateIPAdaptersV2: MetadataValidateFunc = return new Promise((resolve) => resolve(validatedIPAdapters)); }; +const validateLayers: MetadataValidateFunc = (layers) => { + const validatedLayers: Layer[] = []; + for (const l of layers) { + try { + if (l.type === 'control_adapter_layer') { + validateBaseCompatibility(l.controlAdapter.model?.base, 'Layer incompatible with currently-selected model'); + } + if (l.type === 'ip_adapter_layer') { + validateBaseCompatibility(l.ipAdapter.model?.base, 'Layer incompatible with currently-selected model'); + } + if (l.type === 'regional_guidance_layer') { + for (const ipa of l.ipAdapters) { + validateBaseCompatibility(ipa.model?.base, 'Layer incompatible with currently-selected model'); + } + } + validatedLayers.push(l); + } catch { + // This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid. + } + } + return new Promise((resolve) => resolve(validatedLayers)); +}; + export const validators = { refinerModel: validateRefinerModel, vaeModel: validateVAEModel, @@ -182,4 +206,5 @@ export const validators = { t2iAdaptersV2: validateT2IAdaptersV2, ipAdapterV2: validateIPAdapterV2, ipAdaptersV2: validateIPAdaptersV2, + layers: validateLayers, } as const;