diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7b1e24457f..0460ab34fe 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -770,6 +770,7 @@ "allPrompts": "All Prompts", "cfgScale": "CFG scale", "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "clipSkip": "$t(parameters.clipSkip)", "createdBy": "Created By", "generationMode": "Generation Mode", "guidance": "Guidance", @@ -1290,6 +1291,7 @@ "remixImage": "Remix Image", "usePrompt": "Use Prompt", "useSeed": "Use Seed", + "useClipSkip": "Use CLIP Skip", "width": "Width", "gaussianBlur": "Gaussian Blur", "boxBlur": "Box Blur", diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts index f164b1f2f1..5d3f79cf5f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts @@ -16,6 +16,7 @@ import { rgRefImageImageChanged, } from 'features/controlLayers/store/canvasSlice'; import { + selectCLIPSkip, selectMainModelConfig, selectNegativePrompt, selectPositivePrompt, @@ -81,6 +82,7 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit if (withMetadata) { metadata = selectCanvasMetadata(state); + metadata.clip_skip = selectCLIPSkip(state); metadata.positive_prompt = selectPositivePrompt(state); metadata.negative_prompt = selectNegativePrompt(state); metadata.seed = selectSeed(state); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 2a5820ffd1..8f14bbceff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -484,7 +484,7 @@ export const selectCFGScale = createParamsSelector((params) => params.cfgScale); export const selectGuidance = createParamsSelector((params) => params.guidance); export const selectSteps = createParamsSelector((params) => params.steps); export const selectCFGRescaleMultiplier = createParamsSelector((params) => params.cfgRescaleMultiplier); -export const selectCLIPSKip = createParamsSelector((params) => params.clipSkip); +export const selectCLIPSkip = createParamsSelector((params) => params.clipSkip); export const selectCanvasCoherenceEdgeSize = createParamsSelector((params) => params.canvasCoherenceEdgeSize); export const selectCanvasCoherenceMinDenoise = createParamsSelector((params) => params.canvasCoherenceMinDenoise); export const selectCanvasCoherenceMode = createParamsSelector((params) => params.canvasCoherenceMode); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs.tsx index e02701b4e2..9f1cd668d5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs.tsx @@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext'; import { useRecallAll } from 'features/gallery/hooks/useRecallAll'; +import { useRecallCLIPSkip } from 'features/gallery/hooks/useRecallCLIPSkip'; import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions'; import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts'; import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix'; @@ -28,6 +29,7 @@ export const ImageMenuItemMetadataRecallActionsCanvasGenerateTabs = memo(() => { const recallPrompts = useRecallPrompts(imageDTO); const recallSeed = useRecallSeed(imageDTO); const recallDimensions = useRecallDimensions(imageDTO); + const recallCLIPSkip = useRecallCLIPSkip(imageDTO); return ( }> @@ -55,6 +57,9 @@ export const ImageMenuItemMetadataRecallActionsCanvasGenerateTabs = memo(() => { } onClick={recallDimensions.recall} isDisabled={!recallDimensions.isEnabled}> {t('parameters.useSize')} + } onClick={recallCLIPSkip.recall} isDisabled={!recallCLIPSkip.isEnabled}> + {t('parameters.useClipSkip')} + diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts new file mode 100644 index 0000000000..478703c461 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts @@ -0,0 +1,65 @@ +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import type { TabName } from 'features/ui/store/uiTypes'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; +import type { ImageDTO } from 'services/api/types'; + +const ALLOWED_TABS: TabName[] = ['canvas', 'generate', 'upscaling']; + +export const useRecallCLIPSkip = (imageDTO: ImageDTO) => { + const store = useAppStore(); + const tab = useAppSelector(selectActiveTab); + const [hasCLIPSkip, setCLIPSkip] = useState(false); + + const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name); + + useEffect(() => { + const parse = async () => { + try { + await MetadataHandlers.CLIPSkip.parse(metadata, store); + setCLIPSkip(true); + } catch { + setCLIPSkip(false); + } + }; + + parse(); + }, [metadata, store]); + + const isEnabled = useMemo(() => { + if (isLoading) { + return false; + } + + if (!ALLOWED_TABS.includes(tab)) { + return false; + } + + if (!metadata) { + return false; + } + + if (!hasCLIPSkip) { + return false; + } + + return true; + }, [hasCLIPSkip, isLoading, metadata, tab]); + + const recall = useCallback(() => { + if (!metadata) { + return; + } + if (!isEnabled) { + return; + } + MetadataUtils.recallByHandler({ metadata, handler: MetadataHandlers.CLIPSkip, store }); + }, [metadata, isEnabled, store]); + + return { + recall, + isEnabled, + }; +}; diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 3db8c3022c..c703dd0a1b 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -15,6 +15,7 @@ import { selectBase, setCfgRescaleMultiplier, setCfgScale, + setClipSkip, setGuidance, setImg2imgStrength, setRefinerCFGScale, @@ -41,6 +42,7 @@ import { modelSelected } from 'features/parameters/store/actions'; import type { ParameterCFGRescaleMultiplier, ParameterCFGScale, + ParameterCLIPSkip, ParameterGuidance, ParameterHeight, ParameterModel, @@ -63,6 +65,7 @@ import { zLoRAWeight, zParameterCFGRescaleMultiplier, zParameterCFGScale, + zParameterCLIPSkip, zParameterGuidance, zParameterImageDimension, zParameterNegativePrompt, @@ -321,6 +324,24 @@ const CFGRescaleMultiplier: SingleMetadataHandler }; //#endregion CFG Rescale Multiplier +//#region CLIP Skip +const CLIPSkip: SingleMetadataHandler = { + [SingleMetadataKey]: true, + type: 'CLIPSkip', + parse: (metadata, _store) => { + const raw = getProperty(metadata, 'clip_skip'); + const parsed = zParameterCLIPSkip.parse(raw); + return Promise.resolve(parsed); + }, + recall: (value, store) => { + store.dispatch(setClipSkip(value)); + }, + i18nKey: 'metadata.clipSkip', + LabelComponent: MetadataLabel, + ValueComponent: ({ value }: SingleMetadataValueProps) => , +}; +//#endregion CLIP Skip + //#region Guidance const Guidance: SingleMetadataHandler = { [SingleMetadataKey]: true, @@ -883,6 +904,7 @@ export const MetadataHandlers = { NegativePrompt, CFGScale, CFGRescaleMultiplier, + CLIPSkip, Guidance, Scheduler, Width, diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx index 1b437fb22f..f7abd6f86e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCLIPSKip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice'; +import { selectCLIPSkip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import { selectCLIPSkipConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamClipSkip = () => { - const clipSkip = useAppSelector(selectCLIPSKip); + const clipSkip = useAppSelector(selectCLIPSkip); const config = useAppSelector(selectCLIPSkipConfig); const model = useAppSelector(selectModel); diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index b54ce3270f..14c45b61a8 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -4,8 +4,6 @@ import { buildZodTypeGuard } from 'common/util/zodUtils'; import { zModelIdentifierField, zSchedulerField } from 'features/nodes/types/common'; import { z } from 'zod'; -import { CLIP_SKIP_MAP } from './constants'; - /** * Schemas, types and type guards for parameters. * @@ -196,20 +194,21 @@ export const [zLoRAWeight, isParameterLoRAWeight] = buildParameter(z.number()); export type ParameterLoRAWeight = z.infer; // #endregion -// #region Max. CLIP -export const [zParameterMaxCLIP, isParameterMaxCLIP] = buildParameter( - z.union([ - z.discriminatedUnion('model', [ - z.object({ model: z.literal('sd-1'), maxClip: z.number().min(0).max(CLIP_SKIP_MAP['sd-1'].maxClip) }), - z.object({ model: z.literal('sd-2'), maxClip: z.number().min(0).max(CLIP_SKIP_MAP['sd-2'].maxClip) }), - z.object({ model: z.literal('sdxl'), maxClip: z.number().min(0).max(CLIP_SKIP_MAP['sdxl'].maxClip) }), - z.object({ - model: z.literal('sdxl-refiner'), - maxClip: z.number().min(0).max(CLIP_SKIP_MAP['sdxl-refiner'].maxClip), - }), - ]), - z.object({ model: z.string(), maxClip: z.number().min(0).max(0) }), - ]) -); -export type ParameterMaxCLIP = z.infer; +// #region CLIP skip +// export const [zParameterCLIPSkip, isParameterCLIPSkip] = buildParameter( +// z.union([ +// z.discriminatedUnion('model', [ +// z.object({ model: z.literal('sd-1'), clipSkip: z.number().min(0).max(CLIP_SKIP_MAP['sd-1'].maxClip) }), +// z.object({ model: z.literal('sd-2'), clipSkip: z.number().min(0).max(CLIP_SKIP_MAP['sd-2'].maxClip) }), +// z.object({ model: z.literal('sdxl'), clipSkip: z.number().min(0).max(CLIP_SKIP_MAP['sdxl'].maxClip) }), +// z.object({ +// model: z.literal('sdxl-refiner'), +// clipSkip: z.number().min(0).max(CLIP_SKIP_MAP['sdxl-refiner'].maxClip), +// }), +// ]), +// z.object({ model: z.string(), clipSkip: z.number().min(0).max(0) }), +// ]) +// ); +export const [zParameterCLIPSkip, isParameterCLIPSkip] = buildParameter(z.number().int().min(0)); +export type ParameterCLIPSkip = z.infer; // #endregion diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index c8d48a674c..fdc6561869 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -10292,6 +10292,11 @@ export type components = { * @description The id of the board the image belongs to, if one exists. */ board_id?: string | null; + /** + * Clip Skip + * @description The number of skipped CLIP layers + */ + clip_skip?: number | null; }; /** * ImageField diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 98f0d98034..3dfde9610c 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -61,6 +61,7 @@ const _zImageDTO = z.object({ starred: z.boolean(), has_workflow: z.boolean(), board_id: z.string().nullish(), + clip_skip: z.number().int().min(0).nullish(), }); export type ImageDTO = z.infer; assert>();