refactor(ui): use zod for all redux state (wip)

needed for confidence w/ state rehydration logic
This commit is contained in:
psychedelicious
2025-07-24 18:52:51 +10:00
parent 7e59d040aa
commit 6962536b4a
16 changed files with 577 additions and 505 deletions

View File

@@ -1,10 +1,19 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Slice } from '@reduxjs/toolkit';
import type { UndoableOptions } from 'redux-undo';
import type { ZodType } from 'zod';
type StateFromSlice<T extends Slice> = T extends Slice<infer U> ? U : never;
export type SliceConfig<T extends Slice = Slice> = {
export type SliceConfig<T extends Slice> = {
/**
* The redux slice (return of createSlice).
*/
slice: T;
/**
* The zod schema for the slice.
*/
zSchema: ZodType<StateFromSlice<T>>;
/**
* A function that returns the initial state of the slice.
*/
@@ -18,7 +27,7 @@ export type SliceConfig<T extends Slice = Slice> = {
* @param state The rehydrated state.
* @returns A correctly-shaped state.
*/
migrate: (state: unknown) => StateFromSlice<T>;
migrate: (state: any) => StateFromSlice<T>;
/**
* Keys to omit from the persisted state.
*/

View File

@@ -1,130 +1,300 @@
import type { FilterType } from 'features/controlLayers/store/filters';
import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas';
import type { TabName } from 'features/ui/store/uiTypes';
import { zFilterType } from 'features/controlLayers/store/filters';
import { zParameterPrecision, zParameterScheduler } from 'features/parameters/types/parameterSchemas';
import { zTabName } from 'features/ui/store/uiTypes';
import type { PartialDeep } from 'type-fest';
import z from 'zod';
/**
* A disable-able application feature
*/
export type AppFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'aboutModal'
| 'localization'
| 'consoleLogging'
| 'dynamicPrompting'
| 'batches'
| 'syncModels'
| 'multiselect'
| 'pauseQueue'
| 'resumeQueue'
| 'invocationCache'
| 'modelCache'
| 'bulkDownload'
| 'starterModels'
| 'hfToken'
| 'retryQueueItem'
| 'cancelAndClearAll'
| 'chatGPT4oHigh'
| 'modelRelationships';
/**
* A disable-able Stable Diffusion feature
*/
export type SDFeature =
| 'controlNet'
| 'noise'
| 'perlinNoise'
| 'noiseThreshold'
| 'variation'
| 'symmetry'
| 'seamless'
| 'hires'
| 'lora'
| 'embedding'
| 'vae'
| 'hrf';
const zAppFeature = z.enum([
'faceRestore',
'upscaling',
'lightbox',
'modelManager',
'githubLink',
'discordLink',
'bugLink',
'aboutModal',
'localization',
'consoleLogging',
'dynamicPrompting',
'batches',
'syncModels',
'multiselect',
'pauseQueue',
'resumeQueue',
'invocationCache',
'modelCache',
'bulkDownload',
'starterModels',
'hfToken',
'retryQueueItem',
'cancelAndClearAll',
'chatGPT4oHigh',
'modelRelationships',
]);
export type AppFeature = z.infer<typeof zAppFeature>;
export type NumericalParameterConfig = {
initial: number;
sliderMin: number;
sliderMax: number;
numberInputMin: number;
numberInputMax: number;
fineStep: number;
coarseStep: number;
};
const zSDFeature = z.enum([
'controlNet',
'noise',
'perlinNoise',
'noiseThreshold',
'variation',
'symmetry',
'seamless',
'hires',
'lora',
'embedding',
'vae',
'hrf',
]);
export type SDFeature = z.infer<typeof zSDFeature>;
const zNumericalParameterConfig = z.object({
initial: z.number().default(512),
sliderMin: z.number().default(64),
sliderMax: z.number().default(1536),
numberInputMin: z.number().default(64),
numberInputMax: z.number().default(4096),
fineStep: z.number().default(8),
coarseStep: z.number().default(64),
});
export type NumericalParameterConfig = z.infer<typeof zNumericalParameterConfig>;
/**
* Configuration options for the InvokeAI UI.
* Distinct from system settings which may be changed inside the app.
*/
export type AppConfig = {
export const zAppConfig = z.object({
/**
* Whether or not we should update image urls when image loading errors
*/
shouldUpdateImagesOnConnect: boolean;
shouldFetchMetadataFromApi: boolean;
shouldUpdateImagesOnConnect: z.boolean(),
shouldFetchMetadataFromApi: z.boolean(),
/**
* Sets a size limit for outputs on the upscaling tab. This is a maximum dimension, so the actual max number of pixels
* will be the square of this value.
*/
maxUpscaleDimension?: number;
allowPrivateBoards: boolean;
allowPrivateStylePresets: boolean;
allowClientSideUpload: boolean;
allowPublishWorkflows: boolean;
allowPromptExpansion: boolean;
disabledTabs: TabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
nodesAllowlist: string[] | undefined;
nodesDenylist: string[] | undefined;
metadataFetchDebounce?: number;
workflowFetchDebounce?: number;
isLocal?: boolean;
shouldShowCredits: boolean;
sd: {
defaultModel?: string;
disabledControlNetModels: string[];
disabledControlNetProcessors: FilterType[];
maxUpscaleDimension: z.number().optional(),
allowPrivateBoards: z.boolean(),
allowPrivateStylePresets: z.boolean(),
allowClientSideUpload: z.boolean(),
allowPublishWorkflows: z.boolean(),
allowPromptExpansion: z.boolean(),
disabledTabs: z.array(zTabName),
disabledFeatures: z.array(zAppFeature),
disabledSDFeatures: z.array(zSDFeature),
nodesAllowlist: z.array(z.string()).optional(),
nodesDenylist: z.array(z.string()).optional(),
metadataFetchDebounce: z.number().int().optional(),
workflowFetchDebounce: z.number().int().optional(),
isLocal: z.boolean().optional(),
shouldShowCredits: z.boolean().optional(),
sd: z.object({
defaultModel: z.string().optional(),
disabledControlNetModels: z.array(z.string()),
disabledControlNetProcessors: z.array(zFilterType),
// Core parameters
iterations: NumericalParameterConfig;
width: NumericalParameterConfig; // initial value comes from model
height: NumericalParameterConfig; // initial value comes from model
steps: NumericalParameterConfig;
guidance: NumericalParameterConfig;
cfgRescaleMultiplier: NumericalParameterConfig;
img2imgStrength: NumericalParameterConfig;
scheduler?: ParameterScheduler;
vaePrecision?: ParameterPrecision;
iterations: zNumericalParameterConfig,
width: zNumericalParameterConfig,
height: zNumericalParameterConfig,
steps: zNumericalParameterConfig,
guidance: zNumericalParameterConfig,
cfgRescaleMultiplier: zNumericalParameterConfig,
img2imgStrength: zNumericalParameterConfig,
scheduler: zParameterScheduler.optional(),
vaePrecision: zParameterPrecision.optional(),
// Canvas
boundingBoxHeight: NumericalParameterConfig; // initial value comes from model
boundingBoxWidth: NumericalParameterConfig; // initial value comes from model
scaledBoundingBoxHeight: NumericalParameterConfig; // initial value comes from model
scaledBoundingBoxWidth: NumericalParameterConfig; // initial value comes from model
canvasCoherenceStrength: NumericalParameterConfig;
canvasCoherenceEdgeSize: NumericalParameterConfig;
infillTileSize: NumericalParameterConfig;
infillPatchmatchDownscaleSize: NumericalParameterConfig;
boundingBoxHeight: zNumericalParameterConfig,
boundingBoxWidth: zNumericalParameterConfig,
scaledBoundingBoxHeight: zNumericalParameterConfig,
scaledBoundingBoxWidth: zNumericalParameterConfig,
canvasCoherenceStrength: zNumericalParameterConfig,
canvasCoherenceEdgeSize: zNumericalParameterConfig,
infillTileSize: zNumericalParameterConfig,
infillPatchmatchDownscaleSize: zNumericalParameterConfig,
// Misc advanced
clipSkip: NumericalParameterConfig; // slider and input max are ignored for this, because the values depend on the model
maskBlur: NumericalParameterConfig;
hrfStrength: NumericalParameterConfig;
dynamicPrompts: {
maxPrompts: NumericalParameterConfig;
};
ca: {
weight: NumericalParameterConfig;
};
};
flux: {
guidance: NumericalParameterConfig;
};
};
clipSkip: zNumericalParameterConfig, // slider and input max are ignored for this, because the values depend on the model
maskBlur: zNumericalParameterConfig,
hrfStrength: zNumericalParameterConfig,
dynamicPrompts: z.object({
maxPrompts: zNumericalParameterConfig,
}),
ca: z.object({
weight: zNumericalParameterConfig,
}),
}),
flux: z.object({
guidance: zNumericalParameterConfig,
}),
});
export type AppConfig = z.infer<typeof zAppConfig>;
export type PartialAppConfig = PartialDeep<AppConfig>;
export const getDefaultAppConfig = (): AppConfig => ({
isLocal: true,
shouldUpdateImagesOnConnect: false,
shouldFetchMetadataFromApi: false,
allowPrivateBoards: false,
allowPrivateStylePresets: false,
allowClientSideUpload: false,
allowPublishWorkflows: false,
allowPromptExpansion: false,
shouldShowCredits: false,
disabledTabs: [],
disabledFeatures: ['lightbox', 'faceRestore', 'batches'] satisfies AppFeature[],
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'] satisfies SDFeature[],
sd: {
disabledControlNetModels: [],
disabledControlNetProcessors: [],
iterations: {
initial: 1,
sliderMin: 1,
sliderMax: 1000,
numberInputMin: 1,
numberInputMax: 10000,
fineStep: 1,
coarseStep: 1,
},
width: zNumericalParameterConfig.parse({}), // initial value comes from model
height: zNumericalParameterConfig.parse({}), // initial value comes from model
boundingBoxWidth: zNumericalParameterConfig.parse({}), // initial value comes from model
boundingBoxHeight: zNumericalParameterConfig.parse({}), // initial value comes from model
scaledBoundingBoxWidth: zNumericalParameterConfig.parse({}), // initial value comes from model
scaledBoundingBoxHeight: zNumericalParameterConfig.parse({}), // initial value comes from model
scheduler: 'dpmpp_3m_k' as const,
vaePrecision: 'fp32' as const,
steps: {
initial: 30,
sliderMin: 1,
sliderMax: 100,
numberInputMin: 1,
numberInputMax: 500,
fineStep: 1,
coarseStep: 1,
},
guidance: {
initial: 7,
sliderMin: 1,
sliderMax: 20,
numberInputMin: 1,
numberInputMax: 200,
fineStep: 0.1,
coarseStep: 0.5,
},
img2imgStrength: {
initial: 0.7,
sliderMin: 0,
sliderMax: 1,
numberInputMin: 0,
numberInputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
canvasCoherenceStrength: {
initial: 0.3,
sliderMin: 0,
sliderMax: 1,
numberInputMin: 0,
numberInputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
hrfStrength: {
initial: 0.45,
sliderMin: 0,
sliderMax: 1,
numberInputMin: 0,
numberInputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
canvasCoherenceEdgeSize: {
initial: 16,
sliderMin: 0,
sliderMax: 128,
numberInputMin: 0,
numberInputMax: 1024,
fineStep: 8,
coarseStep: 16,
},
cfgRescaleMultiplier: {
initial: 0,
sliderMin: 0,
sliderMax: 0.99,
numberInputMin: 0,
numberInputMax: 0.99,
fineStep: 0.05,
coarseStep: 0.1,
},
clipSkip: {
initial: 0,
sliderMin: 0,
sliderMax: 12, // determined by model selection, unused in practice
numberInputMin: 0,
numberInputMax: 12, // determined by model selection, unused in practice
fineStep: 1,
coarseStep: 1,
},
infillPatchmatchDownscaleSize: {
initial: 1,
sliderMin: 1,
sliderMax: 10,
numberInputMin: 1,
numberInputMax: 10,
fineStep: 1,
coarseStep: 1,
},
infillTileSize: {
initial: 32,
sliderMin: 16,
sliderMax: 64,
numberInputMin: 16,
numberInputMax: 256,
fineStep: 1,
coarseStep: 1,
},
maskBlur: {
initial: 16,
sliderMin: 0,
sliderMax: 128,
numberInputMin: 0,
numberInputMax: 512,
fineStep: 1,
coarseStep: 1,
},
ca: {
weight: {
initial: 1,
sliderMin: 0,
sliderMax: 2,
numberInputMin: -1,
numberInputMax: 2,
fineStep: 0.01,
coarseStep: 0.05,
},
},
dynamicPrompts: {
maxPrompts: {
initial: 100,
sliderMin: 1,
sliderMax: 1000,
numberInputMin: 1,
numberInputMax: 10000,
fineStep: 1,
coarseStep: 10,
},
},
},
flux: {
guidance: {
initial: 4,
sliderMin: 2,
sliderMax: 6,
numberInputMin: 1,
numberInputMax: 20,
fineStep: 0.1,
coarseStep: 0.5,
},
},
});

View File

@@ -1,6 +0,0 @@
import type { ChangeBoardModalState } from './types';
export const initialState: ChangeBoardModalState = {
isModalOpen: false,
image_names: [],
};

View File

@@ -2,15 +2,19 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { SliceConfig } from 'app/store/types';
import { deepClone } from 'common/util/deepClone';
import z from 'zod';
import { initialState } from './initialState';
const zChangeBoardModalState = z.object({
isModalOpen: z.boolean().default(false),
image_names: z.array(z.string()).default(() => []),
});
type ChangeBoardModalState = z.infer<typeof zChangeBoardModalState>;
const getInitialState = () => deepClone(initialState);
const getInitialState = (): ChangeBoardModalState => zChangeBoardModalState.parse({});
const slice = createSlice({
name: 'changeBoardModal',
initialState,
initialState: getInitialState(),
reducers: {
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
state.isModalOpen = action.payload;
@@ -31,5 +35,6 @@ export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoa
export const changeBoardModalSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zChangeBoardModalState,
getInitialState,
};

View File

@@ -1,4 +0,0 @@
export type ChangeBoardModalState = {
isModalOpen: boolean;
image_names: string[];
};

View File

@@ -12,32 +12,32 @@ const zCanvasSettingsState = z.object({
/**
* Whether to show HUD (Heads-Up Display) on the canvas.
*/
showHUD: z.boolean().default(true),
showHUD: z.boolean(),
/**
* Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to
* the canvas bounds.
*/
clipToBbox: z.boolean().default(false),
clipToBbox: z.boolean(),
/**
* Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead.
*/
dynamicGrid: z.boolean().default(false),
dynamicGrid: z.boolean(),
/**
* Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel.
*/
invertScrollForToolWidth: z.boolean().default(false),
invertScrollForToolWidth: z.boolean(),
/**
* The width of the brush tool.
*/
brushWidth: z.int().gt(0).default(50),
brushWidth: z.int().gt(0),
/**
* The width of the eraser tool.
*/
eraserWidth: z.int().gt(0).default(50),
eraserWidth: z.int().gt(0),
/**
* The color to use when drawing lines or filling shapes.
*/
color: zRgbaColor.default({ r: 31, g: 160, b: 224, a: 1 }), // invokeBlue.500
color: zRgbaColor,
/**
* Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations.
*
@@ -45,55 +45,75 @@ const zCanvasSettingsState = z.object({
*
* When `sendToCanvas` is disabled, this setting is ignored, masked regions will always be composited.
*/
outputOnlyMaskedRegions: z.boolean().default(true),
outputOnlyMaskedRegions: z.boolean(),
/**
* Whether to automatically process the operations like filtering and auto-masking.
*/
autoProcess: z.boolean().default(true),
autoProcess: z.boolean(),
/**
* The snap-to-grid setting for the canvas.
*/
snapToGrid: z.boolean().default(true),
snapToGrid: z.boolean(),
/**
* Whether to show progress on the canvas when generating images.
*/
showProgressOnCanvas: z.boolean().default(true),
showProgressOnCanvas: z.boolean(),
/**
* Whether to show the bounding box overlay on the canvas.
*/
bboxOverlay: z.boolean().default(false),
bboxOverlay: z.boolean(),
/**
* Whether to preserve the masked region instead of inpainting it.
*/
preserveMask: z.boolean().default(false),
preserveMask: z.boolean(),
/**
* Whether to show only raster layers while staging.
*/
isolatedStagingPreview: z.boolean().default(true),
isolatedStagingPreview: z.boolean(),
/**
* Whether to show only the selected layer while filtering, transforming, or doing other operations.
*/
isolatedLayerPreview: z.boolean().default(true),
isolatedLayerPreview: z.boolean(),
/**
* Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used.
*/
pressureSensitivity: z.boolean().default(true),
pressureSensitivity: z.boolean(),
/**
* Whether to show the rule of thirds composition guide overlay on the canvas.
*/
ruleOfThirds: z.boolean().default(false),
ruleOfThirds: z.boolean(),
/**
* Whether to save all staging images to the gallery instead of keeping them as intermediate images.
*/
saveAllImagesToGallery: z.boolean().default(false),
saveAllImagesToGallery: z.boolean(),
/**
* The auto-switch mode for the canvas staging area.
*/
stagingAreaAutoSwitch: zAutoSwitchMode.default('switch_on_start'),
stagingAreaAutoSwitch: zAutoSwitchMode,
});
type CanvasSettingsState = z.infer<typeof zCanvasSettingsState>;
const getInitialState = () => zCanvasSettingsState.parse({});
const getInitialState = (): CanvasSettingsState => ({
showHUD: true,
clipToBbox: false,
dynamicGrid: false,
invertScrollForToolWidth: false,
brushWidth: 50,
eraserWidth: 50,
color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
outputOnlyMaskedRegions: true,
autoProcess: true,
snapToGrid: true,
showProgressOnCanvas: true,
bboxOverlay: false,
preserveMask: false,
isolatedStagingPreview: true,
isolatedLayerPreview: true,
pressureSensitivity: true,
ruleOfThirds: false,
saveAllImagesToGallery: false,
stagingAreaAutoSwitch: 'switch_on_start' as const,
});
const slice = createSlice({
name: 'canvasSettings',
@@ -187,16 +207,12 @@ export const {
settingsStagingAreaAutoSwitchChanged,
} = slice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const canvasSettingsSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zCanvasSettingsState,
getInitialState,
persistConfig: {
migrate,
migrate: (state) => zCanvasSettingsState.parse(state),
},
};

View File

@@ -80,6 +80,7 @@ import {
isFLUXReduxConfig,
isImagenAspectRatioID,
isIPAdapterConfig,
zCanvasState,
} from './types';
import {
converters,
@@ -1677,11 +1678,6 @@ export const {
// inpaintMaskRecalled,
} = slice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
const syncScaledSize = (state: CanvasState) => {
if (API_BASE_MODELS.includes(state.bbox.modelBase)) {
// Imagen3 has fixed sizes. Scaled bbox is not supported.
@@ -1724,8 +1720,9 @@ const canvasUndoableConfig: UndoableOptions<CanvasState, UnknownAction> = {
export const canvasSliceConfig: SliceConfig<typeof slice> = {
slice,
getInitialState: getInitialCanvasState,
zSchema: zCanvasState,
persistConfig: {
migrate,
migrate: (state) => zCanvasState.parse(state),
},
undoableConfig: {
reduxUndoOptions: canvasUndoableConfig,

View File

@@ -3,24 +3,19 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import type { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import type { SliceConfig } from 'app/store/types';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { useMemo } from 'react';
import { queueApi } from 'services/api/endpoints/queue';
import z from 'zod';
type CanvasStagingAreaState = {
_version: 1;
canvasSessionId: string;
canvasDiscardedQueueItems: number[];
};
const zCanvasStagingAreaState = z.object({
_version: z.literal(1).default(1),
canvasSessionId: z.string().default(() => getPrefixedId('canvas')),
canvasDiscardedQueueItems: z.array(z.number().int()).default(() => []),
});
type CanvasStagingAreaState = z.infer<typeof zCanvasStagingAreaState>;
const INITIAL_STATE: CanvasStagingAreaState = {
_version: 1,
canvasSessionId: getPrefixedId('canvas'),
canvasDiscardedQueueItems: [],
};
const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE);
const getInitialState = (): CanvasStagingAreaState => zCanvasStagingAreaState.parse({});
const slice = createSlice({
name: 'canvasSession',
@@ -51,20 +46,22 @@ const slice = createSlice({
export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
if (!('_version' in state)) {
state._version = 1;
state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas');
}
return state;
};
export const canvasSessionSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zCanvasStagingAreaState,
getInitialState,
persistConfig: { migrate },
persistConfig: {
migrate: (state) => {
{
if (!('_version' in state)) {
state._version = 1;
state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas');
}
return zCanvasStagingAreaState.parse(state);
}
},
},
};
export const selectCanvasSessionSlice = (s: RootState) => s[slice.name];

View File

@@ -166,7 +166,7 @@ const _zFilterConfig = z.discriminatedUnion('type', [
]);
export type FilterConfig = z.infer<typeof _zFilterConfig>;
const zFilterType = z.enum([
export const zFilterType = z.enum([
'adjust_image',
'canny_edge_detection',
'color_map',

View File

@@ -16,6 +16,7 @@ import {
isChatGPT4oAspectRatioID,
isFluxKontextAspectRatioID,
isImagenAspectRatioID,
zParamsState,
} from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
@@ -400,15 +401,13 @@ export const {
paramsReset,
} = slice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const paramsSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zParamsState,
getInitialState: getInitialParamsState,
persistConfig: { migrate },
persistConfig: {
migrate: (state) => zParamsState.parse(state),
},
};
export const selectParamsSlice = (state: RootState) => state.params;

View File

@@ -19,7 +19,7 @@ import { assert } from 'tsafe';
import type { PartialDeep } from 'type-fest';
import type { CLIPVisionModelV2, IPMethodV2, RefImageState } from './types';
import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig } from './types';
import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig, zRefImagesState } from './types';
import {
getReferenceImageState,
imageDTOToImageWithDims,
@@ -273,6 +273,7 @@ const migrate = (state: any): any => {
export const refImagesSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zRefImagesState,
getInitialState: getInitialRefImagesState,
persistConfig: {
migrate,

View File

@@ -522,62 +522,108 @@ const zDimensionsState = z.object({
aspectRatio: zAspectRatioConfig,
});
const zParamsState = z.object({
maskBlur: z.number().default(16),
maskBlurMethod: zParameterMaskBlurMethod.default('box'),
canvasCoherenceMode: zParameterCanvasCoherenceMode.default('Gaussian Blur'),
canvasCoherenceMinDenoise: zParameterStrength.default(0),
canvasCoherenceEdgeSize: z.number().default(16),
infillMethod: z.string().default('lama'),
infillTileSize: z.number().default(32),
infillPatchmatchDownscaleSize: z.number().default(1),
infillColorValue: zRgbaColor.default({ r: 0, g: 0, b: 0, a: 1 }),
cfgScale: zParameterCFGScale.default(7.5),
cfgRescaleMultiplier: zParameterCFGRescaleMultiplier.default(0),
guidance: zParameterGuidance.default(4),
img2imgStrength: zParameterStrength.default(0.75),
optimizedDenoisingEnabled: z.boolean().default(true),
iterations: z.number().default(1),
scheduler: zParameterScheduler.default('dpmpp_3m_k'),
upscaleScheduler: zParameterScheduler.default('kdpm_2'),
upscaleCfgScale: zParameterCFGScale.default(2),
seed: zParameterSeed.default(0),
shouldRandomizeSeed: z.boolean().default(true),
steps: zParameterSteps.default(30),
model: zParameterModel.nullable().default(null),
vae: zParameterVAEModel.nullable().default(null),
vaePrecision: zParameterPrecision.default('fp32'),
fluxVAE: zParameterVAEModel.nullable().default(null),
seamlessXAxis: z.boolean().default(false),
seamlessYAxis: z.boolean().default(false),
clipSkip: z.number().default(0),
shouldUseCpuNoise: z.boolean().default(true),
positivePrompt: zParameterPositivePrompt.default(''),
// Negative prompt may be disabled, in which case it will be null
negativePrompt: zParameterNegativePrompt.default(null),
positivePrompt2: zParameterPositiveStylePromptSDXL.default(''),
negativePrompt2: zParameterNegativeStylePromptSDXL.default(''),
shouldConcatPrompts: z.boolean().default(true),
refinerModel: zParameterSDXLRefinerModel.nullable().default(null),
refinerSteps: z.number().default(20),
refinerCFGScale: z.number().default(7.5),
refinerScheduler: zParameterScheduler.default('euler'),
refinerPositiveAestheticScore: z.number().default(6),
refinerNegativeAestheticScore: z.number().default(2.5),
refinerStart: z.number().default(0.8),
t5EncoderModel: zParameterT5EncoderModel.nullable().default(null),
clipEmbedModel: zParameterCLIPEmbedModel.nullable().default(null),
clipLEmbedModel: zParameterCLIPLEmbedModel.nullable().default(null),
clipGEmbedModel: zParameterCLIPGEmbedModel.nullable().default(null),
controlLora: zParameterControlLoRAModel.nullable().default(null),
dimensions: zDimensionsState.default({
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG,
}),
export const zParamsState = z.object({
maskBlur: z.number(),
maskBlurMethod: zParameterMaskBlurMethod,
canvasCoherenceMode: zParameterCanvasCoherenceMode,
canvasCoherenceMinDenoise: zParameterStrength,
canvasCoherenceEdgeSize: z.number(),
infillMethod: z.string(),
infillTileSize: z.number(),
infillPatchmatchDownscaleSize: z.number(),
infillColorValue: zRgbaColor,
cfgScale: zParameterCFGScale,
cfgRescaleMultiplier: zParameterCFGRescaleMultiplier,
guidance: zParameterGuidance,
img2imgStrength: zParameterStrength,
optimizedDenoisingEnabled: z.boolean(),
iterations: z.number(),
scheduler: zParameterScheduler,
upscaleScheduler: zParameterScheduler,
upscaleCfgScale: zParameterCFGScale,
seed: zParameterSeed,
shouldRandomizeSeed: z.boolean(),
steps: zParameterSteps,
model: zParameterModel.nullable(),
vae: zParameterVAEModel.nullable(),
vaePrecision: zParameterPrecision,
fluxVAE: zParameterVAEModel.nullable(),
seamlessXAxis: z.boolean(),
seamlessYAxis: z.boolean(),
clipSkip: z.number(),
shouldUseCpuNoise: z.boolean(),
positivePrompt: zParameterPositivePrompt,
negativePrompt: zParameterNegativePrompt,
positivePrompt2: zParameterPositiveStylePromptSDXL,
negativePrompt2: zParameterNegativeStylePromptSDXL,
shouldConcatPrompts: z.boolean(),
refinerModel: zParameterSDXLRefinerModel.nullable(),
refinerSteps: z.number(),
refinerCFGScale: z.number(),
refinerScheduler: zParameterScheduler,
refinerPositiveAestheticScore: z.number(),
refinerNegativeAestheticScore: z.number(),
refinerStart: z.number(),
t5EncoderModel: zParameterT5EncoderModel.nullable(),
clipEmbedModel: zParameterCLIPEmbedModel.nullable(),
clipLEmbedModel: zParameterCLIPLEmbedModel.nullable(),
clipGEmbedModel: zParameterCLIPGEmbedModel.nullable(),
controlLora: zParameterControlLoRAModel.nullable(),
dimensions: zDimensionsState,
});
export type ParamsState = z.infer<typeof zParamsState>;
const INITIAL_PARAMS_STATE = zParamsState.parse({});
export const getInitialParamsState = () => deepClone(INITIAL_PARAMS_STATE);
export const getInitialParamsState = (): ParamsState => ({
maskBlur: 16,
maskBlurMethod: 'box' as const,
canvasCoherenceMode: 'Gaussian Blur' as const,
canvasCoherenceMinDenoise: 0,
canvasCoherenceEdgeSize: 16,
infillMethod: 'lama' as const,
infillTileSize: 32,
infillPatchmatchDownscaleSize: 1,
infillColorValue: { r: 0, g: 0, b: 0, a: 1 },
cfgScale: 7.5,
cfgRescaleMultiplier: 0,
guidance: 4,
img2imgStrength: 0.75,
optimizedDenoisingEnabled: true,
iterations: 1,
scheduler: 'dpmpp_3m_k' as const,
upscaleScheduler: 'kdpm_2' as const,
upscaleCfgScale: 2,
seed: 0,
shouldRandomizeSeed: true,
steps: 30,
model: null,
vae: null,
vaePrecision: 'fp32' as const,
fluxVAE: null,
seamlessXAxis: false,
seamlessYAxis: false,
clipSkip: 0,
shouldUseCpuNoise: true,
positivePrompt: '',
negativePrompt: null,
positivePrompt2: '',
negativePrompt2: '',
shouldConcatPrompts: true,
refinerModel: null,
refinerSteps: 20,
refinerCFGScale: 7.5,
refinerScheduler: 'euler' as const,
refinerPositiveAestheticScore: 6,
refinerNegativeAestheticScore: 2.5,
refinerStart: 0.8,
t5EncoderModel: null,
clipEmbedModel: null,
clipLEmbedModel: null,
clipGEmbedModel: null,
controlLora: null,
dimensions: {
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG),
},
});
const zInpaintMasks = z.object({
isHidden: z.boolean(),
@@ -595,38 +641,45 @@ const zRegionalGuidance = z.object({
isHidden: z.boolean(),
entities: z.array(zCanvasRegionalGuidanceState),
});
const zCanvasState = z.object({
_version: z.literal(3).default(3),
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
inpaintMasks: zInpaintMasks.default({ isHidden: false, entities: [] }),
rasterLayers: zRasterLayers.default({ isHidden: false, entities: [] }),
controlLayers: zControlLayers.default({ isHidden: false, entities: [] }),
regionalGuidance: zRegionalGuidance.default({ isHidden: false, entities: [] }),
bbox: zBboxState.default({
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG,
scaleMethod: 'auto',
scaledSize: { width: 512, height: 512 },
modelBase: 'sd-1',
}),
export const zCanvasState = z.object({
_version: z.literal(3),
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable(),
bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable(),
inpaintMasks: zInpaintMasks,
rasterLayers: zRasterLayers,
controlLayers: zControlLayers,
regionalGuidance: zRegionalGuidance,
bbox: zBboxState,
});
export type CanvasState = z.infer<typeof zCanvasState>;
export const getInitialCanvasState = (): CanvasState => ({
_version: 3 as const,
selectedEntityIdentifier: null,
bookmarkedEntityIdentifier: null,
inpaintMasks: { isHidden: false, entities: [] },
rasterLayers: { isHidden: false, entities: [] },
controlLayers: { isHidden: false, entities: [] },
regionalGuidance: { isHidden: false, entities: [] },
bbox: {
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG),
scaleMethod: 'auto' as const,
scaledSize: { width: 512, height: 512 },
modelBase: 'sd-1' as const,
},
});
const zRefImagesState = z.object({
selectedEntityId: z.string().nullable().default(null),
isPanelOpen: z.boolean().default(false),
entities: z.array(zRefImageState).default(() => []),
export const zRefImagesState = z.object({
selectedEntityId: z.string().nullable(),
isPanelOpen: z.boolean(),
entities: z.array(zRefImageState),
});
export type RefImagesState = z.infer<typeof zRefImagesState>;
const INITIAL_REF_IMAGES_STATE = zRefImagesState.parse({});
export const getInitialRefImagesState = () => deepClone(INITIAL_REF_IMAGES_STATE);
/**
* Gets a fresh canvas initial state with no references in memory to existing objects.
*/
const CANVAS_INITIAL_STATE = zCanvasState.parse({});
export const getInitialCanvasState = () => deepClone(CANVAS_INITIAL_STATE);
export const getInitialRefImagesState = (): RefImagesState => ({
selectedEntityId: null,
isPanelOpen: false,
entities: [],
});
export const zCanvasReferenceImageState_OLD = zCanvasEntityBase.extend({
type: z.literal('reference_image'),

View File

@@ -9,16 +9,17 @@ const zSeedBehaviour = z.enum(['PER_ITERATION', 'PER_PROMPT']);
export const isSeedBehaviour = buildZodTypeGuard(zSeedBehaviour);
export type SeedBehaviour = z.infer<typeof zSeedBehaviour>;
export interface DynamicPromptsState {
_version: 1;
maxPrompts: number;
combinatorial: boolean;
prompts: string[];
parsingError: string | undefined | null;
isError: boolean;
isLoading: boolean;
seedBehaviour: SeedBehaviour;
}
const zDynamicPromptsState = z.object({
_version: z.literal(1),
maxPrompts: z.number().int().min(1).max(1000),
combinatorial: z.boolean(),
prompts: z.array(z.string()),
parsingError: z.string().nullish(),
isError: z.boolean(),
isLoading: z.boolean(),
seedBehaviour: zSeedBehaviour,
});
type DynamicPromptsState = z.infer<typeof zDynamicPromptsState>;
const getInitialState = (): DynamicPromptsState => ({
_version: 1,
@@ -66,19 +67,17 @@ export const {
seedBehaviourChanged,
} = slice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
if (!('_version' in state)) {
state._version = 1;
}
return state;
};
export const dynamicPromptsSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zDynamicPromptsState,
getInitialState,
persistConfig: {
migrate,
migrate: (state) => {
if (!('_version' in state)) {
state._version = 1;
}
return zDynamicPromptsState.parse(state);
},
persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'],
},
};

View File

@@ -2,190 +2,19 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { SliceConfig } from 'app/store/types';
import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai';
import { getDefaultAppConfig, type PartialAppConfig, zAppConfig } from 'app/types/invokeai';
import { merge } from 'es-toolkit/compat';
import z from 'zod';
const baseDimensionConfig: NumericalParameterConfig = {
initial: 512, // determined by model selection, unused in practice
sliderMin: 64,
sliderMax: 1536,
numberInputMin: 64,
numberInputMax: 4096,
fineStep: 8,
coarseStep: 64,
};
type ConfigState = AppConfig & { didLoad: boolean };
const zConfigState = z.object({
...zAppConfig.shape,
didLoad: z.boolean(),
});
type ConfigState = z.infer<typeof zConfigState>;
const getInitialState = (): ConfigState => ({
...getDefaultAppConfig(),
didLoad: false,
isLocal: true,
shouldUpdateImagesOnConnect: false,
shouldFetchMetadataFromApi: false,
allowPrivateBoards: false,
allowPrivateStylePresets: false,
allowClientSideUpload: false,
allowPublishWorkflows: false,
allowPromptExpansion: false,
shouldShowCredits: false,
disabledTabs: [],
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],
nodesAllowlist: undefined,
nodesDenylist: undefined,
sd: {
disabledControlNetModels: [],
disabledControlNetProcessors: [],
iterations: {
initial: 1,
sliderMin: 1,
sliderMax: 1000,
numberInputMin: 1,
numberInputMax: 10000,
fineStep: 1,
coarseStep: 1,
},
width: { ...baseDimensionConfig },
height: { ...baseDimensionConfig },
boundingBoxWidth: { ...baseDimensionConfig },
boundingBoxHeight: { ...baseDimensionConfig },
scaledBoundingBoxWidth: { ...baseDimensionConfig },
scaledBoundingBoxHeight: { ...baseDimensionConfig },
scheduler: 'dpmpp_3m_k',
vaePrecision: 'fp32',
steps: {
initial: 30,
sliderMin: 1,
sliderMax: 100,
numberInputMin: 1,
numberInputMax: 500,
fineStep: 1,
coarseStep: 1,
},
guidance: {
initial: 7,
sliderMin: 1,
sliderMax: 20,
numberInputMin: 1,
numberInputMax: 200,
fineStep: 0.1,
coarseStep: 0.5,
},
img2imgStrength: {
initial: 0.7,
sliderMin: 0,
sliderMax: 1,
numberInputMin: 0,
numberInputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
canvasCoherenceStrength: {
initial: 0.3,
sliderMin: 0,
sliderMax: 1,
numberInputMin: 0,
numberInputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
hrfStrength: {
initial: 0.45,
sliderMin: 0,
sliderMax: 1,
numberInputMin: 0,
numberInputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
canvasCoherenceEdgeSize: {
initial: 16,
sliderMin: 0,
sliderMax: 128,
numberInputMin: 0,
numberInputMax: 1024,
fineStep: 8,
coarseStep: 16,
},
cfgRescaleMultiplier: {
initial: 0,
sliderMin: 0,
sliderMax: 0.99,
numberInputMin: 0,
numberInputMax: 0.99,
fineStep: 0.05,
coarseStep: 0.1,
},
clipSkip: {
initial: 0,
sliderMin: 0,
sliderMax: 12, // determined by model selection, unused in practice
numberInputMin: 0,
numberInputMax: 12, // determined by model selection, unused in practice
fineStep: 1,
coarseStep: 1,
},
infillPatchmatchDownscaleSize: {
initial: 1,
sliderMin: 1,
sliderMax: 10,
numberInputMin: 1,
numberInputMax: 10,
fineStep: 1,
coarseStep: 1,
},
infillTileSize: {
initial: 32,
sliderMin: 16,
sliderMax: 64,
numberInputMin: 16,
numberInputMax: 256,
fineStep: 1,
coarseStep: 1,
},
maskBlur: {
initial: 16,
sliderMin: 0,
sliderMax: 128,
numberInputMin: 0,
numberInputMax: 512,
fineStep: 1,
coarseStep: 1,
},
ca: {
weight: {
initial: 1,
sliderMin: 0,
sliderMax: 2,
numberInputMin: -1,
numberInputMax: 2,
fineStep: 0.01,
coarseStep: 0.05,
},
},
dynamicPrompts: {
maxPrompts: {
initial: 100,
sliderMin: 1,
sliderMax: 1000,
numberInputMin: 1,
numberInputMax: 10000,
fineStep: 1,
coarseStep: 10,
},
},
},
flux: {
guidance: {
initial: 4,
sliderMin: 2,
sliderMax: 6,
numberInputMin: 1,
numberInputMax: 20,
fineStep: 0.1,
coarseStep: 0.5,
},
},
});
const slice = createSlice({
@@ -203,6 +32,7 @@ export const { configChanged } = slice.actions;
export const configSliceConfig: SliceConfig<typeof slice> = {
slice,
zSchema: zConfigState,
getInitialState,
};

View File

@@ -2,15 +2,12 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { SliceConfig } from 'app/store/types';
import { deepClone } from 'common/util/deepClone';
import { INITIAL_STATE, type UIState } from './uiTypes';
const getInitialState = (): UIState => deepClone(INITIAL_STATE);
import { getInitialUIState, type UIState, zUIState } from './uiTypes';
const slice = createSlice({
name: 'ui',
initialState: getInitialState(),
initialState: getInitialUIState(),
reducers: {
setActiveTab: (state, action: PayloadAction<UIState['activeTab']>) => {
state.activeTab = action.payload;
@@ -88,27 +85,25 @@ export const {
export const selectUiSlice = (state: RootState) => state.ui;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
if (!('_version' in state)) {
state._version = 1;
}
if (state._version === 1) {
state.activeTab = 'generation';
state._version = 2;
}
if (state._version === 2) {
state.activeTab = 'canvas';
state._version = 3;
}
return state;
};
export const uiSliceConfig: SliceConfig<typeof slice> = {
slice,
getInitialState,
zSchema: zUIState,
getInitialState: getInitialUIState,
persistConfig: {
migrate,
migrate: (state) => {
if (!('_version' in state)) {
state._version = 1;
}
if (state._version === 1) {
state.activeTab = 'generation';
state._version = 2;
}
if (state._version === 2) {
state.activeTab = 'canvas';
state._version = 3;
}
return zUIState.parse(state);
},
persistDenylist: ['shouldShowImageDetails'],
},
};

View File

@@ -1,7 +1,7 @@
import { isPlainObject } from 'es-toolkit';
import { z } from 'zod';
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
export const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
export type TabName = z.infer<typeof zTabName>;
const zPartialDimensions = z.object({
@@ -12,17 +12,28 @@ const zPartialDimensions = z.object({
const zSerializable = z.any().refine(isPlainObject);
export type Serializable = z.infer<typeof zSerializable>;
const zUIState = z.object({
_version: z.literal(3).default(3),
activeTab: zTabName.default('generate'),
shouldShowImageDetails: z.boolean().default(false),
shouldShowProgressInViewer: z.boolean().default(true),
accordions: z.record(z.string(), z.boolean()).default(() => ({})),
expanders: z.record(z.string(), z.boolean()).default(() => ({})),
textAreaSizes: z.record(z.string(), zPartialDimensions).default({}),
panels: z.record(z.string(), zSerializable).default({}),
shouldShowNotificationV2: z.boolean().default(true),
pickerCompactViewStates: z.record(z.string(), z.boolean()).default(() => ({})),
export const zUIState = z.object({
_version: z.literal(3),
activeTab: zTabName,
shouldShowImageDetails: z.boolean(),
shouldShowProgressInViewer: z.boolean(),
accordions: z.record(z.string(), z.boolean()),
expanders: z.record(z.string(), z.boolean()),
textAreaSizes: z.record(z.string(), zPartialDimensions),
panels: z.record(z.string(), zSerializable),
shouldShowNotificationV2: z.boolean(),
pickerCompactViewStates: z.record(z.string(), z.boolean()),
});
export const INITIAL_STATE = zUIState.parse({});
export type UIState = z.infer<typeof zUIState>;
export const getInitialUIState = (): UIState => ({
_version: 3 as const,
activeTab: 'generate' as const,
shouldShowImageDetails: false,
shouldShowProgressInViewer: true,
accordions: {},
expanders: {},
textAreaSizes: {},
panels: {},
shouldShowNotificationV2: true,
pickerCompactViewStates: {},
});