feat(ui): add workflow saving/loading (wip)

Adds loading workflows with exhaustive validation via `zod`.

There is a load button but no dedicated save/load UI yet. Also need to add versioning to the workflow format itself.
This commit is contained in:
psychedelicious 2023-08-22 21:14:11 +10:00
parent 38b2dedc1d
commit ce7172d78c
26 changed files with 611 additions and 397 deletions

View File

@ -114,7 +114,8 @@
"use-debounce": "^9.0.4", "use-debounce": "^9.0.4",
"use-image": "^1.1.1", "use-image": "^1.1.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"zod": "^3.22.2" "zod": "^3.22.2",
"zod-validation-error": "^1.5.0"
}, },
"peerDependencies": { "peerDependencies": {
"@chakra-ui/cli": "^2.4.0", "@chakra-ui/cli": "^2.4.0",

View File

@ -32,7 +32,7 @@ interface Props {
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
const language = useAppSelector(languageSelector); const language = useAppSelector(languageSelector);
const logger = useLogger(); const logger = useLogger('system');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
localStorage.clear(); localStorage.clear();
@ -46,7 +46,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
useEffect(() => { useEffect(() => {
if (size(config)) { if (size(config)) {
logger.info({ namespace: 'App', config }, 'Received config'); logger.info({ config }, 'Received config');
dispatch(configChanged(config)); dispatch(configChanged(config));
} }
}, [dispatch, config, logger]); }, [dispatch, config, logger]);

View File

@ -9,7 +9,7 @@ export const log = Roarr.child(BASE_CONTEXT);
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT)); export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
type LoggerNamespace = export type LoggerNamespace =
| 'images' | 'images'
| 'models' | 'models'
| 'config' | 'config'

View File

@ -1,12 +1,17 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { createLogWriter } from '@roarr/browser-log-writer'; import { createLogWriter } from '@roarr/browser-log-writer';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useEffect } from 'react'; import { useEffect, useMemo } from 'react';
import { ROARR, Roarr } from 'roarr'; import { ROARR, Roarr } from 'roarr';
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP } from './logger'; import {
$logger,
BASE_CONTEXT,
LOG_LEVEL_MAP,
LoggerNamespace,
logger,
} from './logger';
const selector = createSelector( const selector = createSelector(
systemSelector, systemSelector,
@ -25,7 +30,7 @@ const selector = createSelector(
} }
); );
export const useLogger = () => { export const useLogger = (namespace: LoggerNamespace) => {
const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector); const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector);
// The provided Roarr browser log writer uses localStorage to config logging to console // The provided Roarr browser log writer uses localStorage to config logging to console
@ -57,7 +62,7 @@ export const useLogger = () => {
$logger.set(Roarr.child(newContext)); $logger.set(Roarr.child(newContext));
}, []); }, []);
const logger = useStore($logger); const log = useMemo(() => logger(namespace), [namespace]);
return logger; return log;
}; };

View File

@ -5,6 +5,7 @@ import { modelsApi } from 'services/api/endpoints/models';
import { receivedOpenAPISchema } from 'services/api/thunks/schema'; import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { appSocketConnected, socketConnected } from 'services/events/actions'; import { appSocketConnected, socketConnected } from 'services/events/actions';
import { startAppListening } from '../..'; import { startAppListening } from '../..';
import { size } from 'lodash-es';
export const addSocketConnectedEventListener = () => { export const addSocketConnectedEventListener = () => {
startAppListening({ startAppListening({
@ -18,7 +19,7 @@ export const addSocketConnectedEventListener = () => {
const { disabledTabs } = config; const { disabledTabs } = config;
if (!nodes.schema && !disabledTabs.includes('nodes')) { if (!size(nodes.nodeTemplates) && !disabledTabs.includes('nodes')) {
dispatch(receivedOpenAPISchema()); dispatch(receivedOpenAPISchema());
} }

View File

@ -0,0 +1,2 @@
export const colorTokenToCssVar = (colorToken: string) =>
`var(--invokeai-colors-${colorToken.split('.').join('-')}`;

View File

@ -8,10 +8,12 @@ type Props = {
label: string; label: string;
data: object | string; data: object | string;
fileName?: string; fileName?: string;
withDownload?: boolean;
withCopy?: boolean;
}; };
const DataViewer = (props: Props) => { const DataViewer = (props: Props) => {
const { label, data, fileName } = props; const { label, data, fileName, withDownload = true, withCopy = true } = props;
const dataString = useMemo( const dataString = useMemo(
() => (isString(data) ? data : JSON.stringify(data, null, 2)), () => (isString(data) ? data : JSON.stringify(data, null, 2)),
[data] [data]
@ -70,24 +72,28 @@ const DataViewer = (props: Props) => {
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
</Box> </Box>
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}> <Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
<Tooltip label={`Save ${label} JSON`}> {withDownload && (
<IconButton <Tooltip label={`Save ${label} JSON`}>
aria-label={`Save ${label} JSON`} <IconButton
icon={<FaSave />} aria-label={`Save ${label} JSON`}
variant="ghost" icon={<FaSave />}
opacity={0.7} variant="ghost"
onClick={handleSave} opacity={0.7}
/> onClick={handleSave}
</Tooltip> />
<Tooltip label={`Copy ${label} JSON`}> </Tooltip>
<IconButton )}
aria-label={`Copy ${label} JSON`} {withCopy && (
icon={<FaCopy />} <Tooltip label={`Copy ${label} JSON`}>
variant="ghost" <IconButton
opacity={0.7} aria-label={`Copy ${label} JSON`}
onClick={handleCopy} icon={<FaCopy />}
/> variant="ghost"
</Tooltip> opacity={0.7}
onClick={handleCopy}
/>
</Tooltip>
)}
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -1,9 +1,10 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { ConnectionLineComponentProps, getBezierPath } from 'reactflow'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { FIELDS, colorTokenToCssVar } from '../../../types/constants'; import { FIELDS } from 'features/nodes/types/constants';
import { memo } from 'react'; import { memo } from 'react';
import { ConnectionLineComponentProps, getBezierPath } from 'reactflow';
const selector = createSelector(stateSelector, ({ nodes }) => { const selector = createSelector(stateSelector, ({ nodes }) => {
const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } = const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } =

View File

@ -1,7 +1,8 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { FIELDS, colorTokenToCssVar } from 'features/nodes/types/constants'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { FIELDS } from 'features/nodes/types/constants';
import { isInvocationNode } from 'features/nodes/types/types'; import { isInvocationNode } from 'features/nodes/types/types';
export const makeEdgeSelector = ( export const makeEdgeSelector = (

View File

@ -1,15 +1,15 @@
import { Tooltip } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/react';
import { CSSProperties, memo, useMemo } from 'react'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { Handle, HandleType, Position } from 'reactflow';
import { import {
FIELDS, FIELDS,
HANDLE_TOOLTIP_OPEN_DELAY, HANDLE_TOOLTIP_OPEN_DELAY,
colorTokenToCssVar, } from 'features/nodes/types/constants';
} from '../../../../../types/constants';
import { import {
InputFieldTemplate, InputFieldTemplate,
OutputFieldTemplate, OutputFieldTemplate,
} from '../../../../../types/types'; } from 'features/nodes/types/types';
import { CSSProperties, memo, useMemo } from 'react';
import { Handle, HandleType, Position } from 'reactflow';
export const handleBaseStyles: CSSProperties = { export const handleBaseStyles: CSSProperties = {
position: 'absolute', position: 'absolute',

View File

@ -0,0 +1,28 @@
import { FileButton } from '@mantine/core';
import IAIIconButton from 'common/components/IAIIconButton';
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
import { memo, useRef } from 'react';
import { FaUpload } from 'react-icons/fa';
const LoadWorkflowButton = () => {
const resetRef = useRef<() => void>(null);
const loadWorkflowFromFile = useLoadWorkflowFromFile();
return (
<FileButton
resetRef={resetRef}
accept="application/json"
onChange={loadWorkflowFromFile}
>
{(props) => (
<IAIIconButton
icon={<FaUpload />}
tooltip="Load Workflow"
aria-label="Load Workflow"
{...props}
/>
)}
</FileButton>
);
};
export default memo(LoadWorkflowButton);

View File

@ -6,6 +6,7 @@ import NodeEditorSettings from './NodeEditorSettings';
import ClearGraphButton from './ClearGraphButton'; import ClearGraphButton from './ClearGraphButton';
import NodeInvokeButton from './NodeInvokeButton'; import NodeInvokeButton from './NodeInvokeButton';
import ReloadSchemaButton from './ReloadSchemaButton'; import ReloadSchemaButton from './ReloadSchemaButton';
import LoadWorkflowButton from './LoadWorkflowButton';
const TopCenterPanel = () => { const TopCenterPanel = () => {
return ( return (
@ -16,6 +17,7 @@ const TopCenterPanel = () => {
<ReloadSchemaButton /> <ReloadSchemaButton />
<ClearGraphButton /> <ClearGraphButton />
<NodeEditorSettings /> <NodeEditorSettings />
<LoadWorkflowButton />
</HStack> </HStack>
</Panel> </Panel>
); );

View File

@ -31,7 +31,7 @@ const WorkflowJSONTab = () => {
h: 'full', h: 'full',
}} }}
> >
<DataViewer data={workflow} label="Workflow" fileName={workflow.name} /> <DataViewer data={workflow} label="Workflow" />
</Flex> </Flex>
); );
}; };

View File

@ -0,0 +1,107 @@
import { ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { useLogger } from 'app/logging/useLogger';
import { useAppDispatch } from 'app/store/storeHooks';
import { parseify } from 'common/util/serialize';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { zWorkflow } from 'features/nodes/types/types';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { memo, useCallback } from 'react';
import { flushSync } from 'react-dom';
import { useReactFlow } from 'reactflow';
import { ZodError } from 'zod';
import { fromZodError, fromZodIssue } from 'zod-validation-error';
export const useLoadWorkflowFromFile = () => {
const dispatch = useAppDispatch();
const { fitView } = useReactFlow();
const logger = useLogger('nodes');
const loadWorkflowFromFile = useCallback(
(file: File | null) => {
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = async () => {
const rawJSON = reader.result;
try {
const parsedJSON = JSON.parse(String(rawJSON));
const result = zWorkflow.safeParse(parsedJSON);
if (!result.success) {
const message = fromZodError(result.error, {
prefix: 'Workflow Validation Error',
}).toString();
logger.error({ error: parseify(result.error) }, message);
dispatch(
addToast(
makeToast({
title: 'Unable to Validate Workflow',
description: (
<WorkflowValidationErrorContent error={result.error} />
),
status: 'error',
duration: 5000,
})
)
);
return;
}
dispatch(workflowLoaded(result.data));
flushSync(fitView);
dispatch(
addToast(
makeToast({
title: 'Workflow Loaded',
status: 'success',
})
)
);
reader.abort();
} catch (error) {
// file reader error
if (error) {
dispatch(
addToast(
makeToast({
title: 'Unable to Load Workflow',
status: 'error',
})
)
);
}
}
};
reader.readAsText(file);
},
[dispatch, fitView, logger]
);
return loadWorkflowFromFile;
};
const WorkflowValidationErrorContent = memo((props: { error: ZodError }) => {
if (props.error.issues[0]) {
return (
<Text>
{fromZodIssue(props.error.issues[0], { prefix: null }).toString()}
</Text>
);
}
return (
<UnorderedList>
{props.error.issues.map((issue, i) => (
<ListItem key={i}>
<Text>{fromZodIssue(issue, { prefix: null }).toString()}</Text>
</ListItem>
))}
</UnorderedList>
);
});
WorkflowValidationErrorContent.displayName = 'WorkflowValidationErrorContent';

View File

@ -4,7 +4,6 @@ import { NodesState } from './types';
* Nodes slice persist denylist * Nodes slice persist denylist
*/ */
export const nodesPersistDenylist: (keyof NodesState)[] = [ export const nodesPersistDenylist: (keyof NodesState)[] = [
'schema',
'nodeTemplates', 'nodeTemplates',
'connectionStartParams', 'connectionStartParams',
'currentConnectionFieldType', 'currentConnectionFieldType',

View File

@ -56,6 +56,8 @@ import {
import { NodesState } from './types'; import { NodesState } from './types';
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';
export const WORKFLOW_FORMAT_VERSION = '1.0.0';
const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = { const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
status: NodeStatus.PENDING, status: NodeStatus.PENDING,
error: null, error: null,
@ -64,10 +66,23 @@ const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
outputs: [], outputs: [],
}; };
export const initialWorkflow = {
meta: {
version: WORKFLOW_FORMAT_VERSION,
},
name: '',
author: '',
description: '',
notes: '',
tags: '',
contact: '',
version: '',
exposedFields: [],
};
export const initialNodesState: NodesState = { export const initialNodesState: NodesState = {
nodes: [], nodes: [],
edges: [], edges: [],
schema: null,
nodeTemplates: {}, nodeTemplates: {},
isReady: false, isReady: false,
connectionStartParams: null, connectionStartParams: null,
@ -82,16 +97,7 @@ export const initialNodesState: NodesState = {
nodeOpacity: 1, nodeOpacity: 1,
selectedNodes: [], selectedNodes: [],
selectedEdges: [], selectedEdges: [],
workflow: { workflow: initialWorkflow,
name: '',
author: '',
description: '',
notes: '',
tags: '',
contact: '',
version: '',
exposedFields: [],
},
nodeExecutionStates: {}, nodeExecutionStates: {},
viewport: { x: 0, y: 0, zoom: 1 }, viewport: { x: 0, y: 0, zoom: 1 },
mouseOverField: null, mouseOverField: null,
@ -570,15 +576,6 @@ const nodesSlice = createSlice({
nodeOpacityChanged: (state, action: PayloadAction<number>) => { nodeOpacityChanged: (state, action: PayloadAction<number>) => {
state.nodeOpacity = action.payload; state.nodeOpacity = action.payload;
}, },
loadFileNodes: (
state,
action: PayloadAction<Node<InvocationNodeData>[]>
) => {
state.nodes = action.payload;
},
loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
state.edges = action.payload;
},
workflowNameChanged: (state, action: PayloadAction<string>) => { workflowNameChanged: (state, action: PayloadAction<string>) => {
state.workflow.name = action.payload; state.workflow.name = action.payload;
}, },
@ -616,6 +613,9 @@ const nodesSlice = createSlice({
[] []
); );
}, },
workflowReset: (state) => {
state.workflow = cloneDeep(initialWorkflow);
},
viewportChanged: (state, action: PayloadAction<Viewport>) => { viewportChanged: (state, action: PayloadAction<Viewport>) => {
state.viewport = action.payload; state.viewport = action.payload;
}, },
@ -726,9 +726,6 @@ const nodesSlice = createSlice({
builder.addCase(receivedOpenAPISchema.pending, (state) => { builder.addCase(receivedOpenAPISchema.pending, (state) => {
state.isReady = false; state.isReady = false;
}); });
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
state.schema = action.payload;
});
builder.addCase(appSocketInvocationStarted, (state, action) => { builder.addCase(appSocketInvocationStarted, (state, action) => {
const { source_node_id } = action.payload.data; const { source_node_id } = action.payload.data;
const node = state.nodeExecutionStates[source_node_id]; const node = state.nodeExecutionStates[source_node_id];
@ -792,8 +789,6 @@ export const {
nodeTemplatesBuilt, nodeTemplatesBuilt,
nodeEditorReset, nodeEditorReset,
imageCollectionFieldValueChanged, imageCollectionFieldValueChanged,
loadFileNodes,
loadFileEdges,
fieldStringValueChanged, fieldStringValueChanged,
fieldNumberValueChanged, fieldNumberValueChanged,
fieldBooleanValueChanged, fieldBooleanValueChanged,

View File

@ -1,4 +1,3 @@
import { OpenAPIV3 } from 'openapi-types';
import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow'; import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow';
import { import {
FieldIdentifier, FieldIdentifier,
@ -13,7 +12,6 @@ import {
export type NodesState = { export type NodesState = {
nodes: Node<NodeData>[]; nodes: Node<NodeData>[];
edges: Edge<InvocationEdgeExtra>[]; edges: Edge<InvocationEdgeExtra>[];
schema: OpenAPIV3.Document | null;
nodeTemplates: Record<string, InvocationTemplate>; nodeTemplates: Record<string, InvocationTemplate>;
connectionStartParams: OnConnectStartParams | null; connectionStartParams: OnConnectStartParams | null;
currentConnectionFieldType: FieldType | null; currentConnectionFieldType: FieldType | null;

View File

@ -23,9 +23,6 @@ export const COLLECTION_TYPES: FieldType[] = [
'ImageCollection', 'ImageCollection',
]; ];
export const colorTokenToCssVar = (colorToken: string) =>
`var(--invokeai-colors-${colorToken.split('.').join('-')}`;
export const FIELDS: Record<FieldType, FieldUIConfig> = { export const FIELDS: Record<FieldType, FieldUIConfig> = {
integer: { integer: {
title: 'Integer', title: 'Integer',

View File

@ -1,23 +1,13 @@
import { import {
ControlNetModelParam,
LoRAModelParam,
MainModelParam,
OnnxModelParam,
SchedulerParam, SchedulerParam,
VaeModelParam, zBaseModel,
zMainOrOnnxModel,
zScheduler,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { RgbaColor } from 'react-colorful'; import { RgbaColor } from 'react-colorful';
import { Edge, Node } from 'reactflow'; import { Node } from 'reactflow';
import { components } from 'services/api/schema'; import { Graph, ImageDTO, _InputField, _OutputField } from 'services/api/types';
import {
Graph,
GraphExecutionState,
ImageDTO,
ImageField,
_InputField,
_OutputField,
} from 'services/api/types';
import { import {
AnyInvocationType, AnyInvocationType,
AnyResult, AnyResult,
@ -118,40 +108,6 @@ export type FieldType = z.infer<typeof zFieldType>;
export const isFieldType = (value: unknown): value is FieldType => export const isFieldType = (value: unknown): value is FieldType =>
zFieldType.safeParse(value).success; zFieldType.safeParse(value).success;
/**
* An input field is persisted across reloads as part of the user's local state.
*
* An input field has three properties:
* - `id` a unique identifier
* - `name` the name of the field, which comes from the python dataclass
* - `value` the field's value
*/
export type InputFieldValue =
| IntegerInputFieldValue
| SeedInputFieldValue
| FloatInputFieldValue
| StringInputFieldValue
| BooleanInputFieldValue
| ImageInputFieldValue
| LatentsInputFieldValue
| ConditioningInputFieldValue
| UNetInputFieldValue
| ClipInputFieldValue
| VaeInputFieldValue
| ControlInputFieldValue
| EnumInputFieldValue
| MainModelInputFieldValue
| SDXLMainModelInputFieldValue
| SDXLRefinerModelInputFieldValue
| VaeModelInputFieldValue
| LoRAModelInputFieldValue
| ControlNetModelInputFieldValue
| CollectionInputFieldValue
| CollectionItemInputFieldValue
| ColorInputFieldValue
| ImageCollectionInputFieldValue
| SchedulerInputFieldValue;
/** /**
* An input field template is generated on each page load from the OpenAPI schema. * An input field template is generated on each page load from the OpenAPI schema.
* *
@ -183,6 +139,19 @@ export type InputFieldTemplate =
| ImageCollectionInputFieldTemplate | ImageCollectionInputFieldTemplate
| SchedulerInputFieldTemplate; | SchedulerInputFieldTemplate;
/**
* Indicates the kind of input(s) this field may have.
*/
export const zInputKind = z.enum(['connection', 'direct', 'any']);
export type InputKind = z.infer<typeof zInputKind>;
export const zFieldValueBase = z.object({
id: z.string().trim().min(1),
name: z.string().trim().min(1),
type: zFieldType,
});
export type FieldValueBase = z.infer<typeof zFieldValueBase>;
/** /**
* An output field is persisted across as part of the user's local state. * An output field is persisted across as part of the user's local state.
* *
@ -190,7 +159,11 @@ export type InputFieldTemplate =
* - `id` a unique identifier * - `id` a unique identifier
* - `name` the name of the field, which comes from the python dataclass * - `name` the name of the field, which comes from the python dataclass
*/ */
export type OutputFieldValue = FieldValueBase & { fieldKind: 'output' };
export const zOutputFieldValue = zFieldValueBase.extend({
fieldKind: z.literal('output'),
});
export type OutputFieldValue = z.infer<typeof zOutputFieldValue>;
/** /**
* An output field template is generated on each page load from the OpenAPI schema. * An output field template is generated on each page load from the OpenAPI schema.
@ -205,141 +178,307 @@ export type OutputFieldTemplate = {
description: string; description: string;
} & _OutputField; } & _OutputField;
/** export const zInputFieldValueBase = zFieldValueBase.extend({
* Indicates the kind of input(s) this field may have. fieldKind: z.literal('input'),
*/ label: z.string(),
export type InputKind = 'connection' | 'direct' | 'any'; });
export type InputFieldValueBase = z.infer<typeof zInputFieldValueBase>;
export type FieldValueBase = { export const zModelIdentifier = z.object({
id: string; model_name: z.string().trim().min(1),
name: string; base_model: zBaseModel,
type: FieldType; });
};
export type InputFieldValueBase = FieldValueBase & { export const zImageField = z.object({
fieldKind: 'input'; image_name: z.string().trim().min(1),
label: string; });
}; export type ImageField = z.infer<typeof zImageField>;
export type IntegerInputFieldValue = InputFieldValueBase & { export const zLatentsField = z.object({
type: 'integer'; latents_name: z.string().trim().min(1),
value?: number; seed: z.number().int().optional(),
}; });
export type LatentsField = z.infer<typeof zLatentsField>;
export type FloatInputFieldValue = InputFieldValueBase & { export const zConditioningField = z.object({
type: 'float'; conditioning_name: z.string().trim().min(1),
value?: number; });
}; export type ConditioningField = z.infer<typeof zConditioningField>;
export type SeedInputFieldValue = InputFieldValueBase & { export const zIntegerInputFieldValue = zInputFieldValueBase.extend({
type: 'Seed'; type: z.literal('integer'),
value?: number; value: z.number().optional(),
}; });
export type IntegerInputFieldValue = z.infer<typeof zIntegerInputFieldValue>;
export type StringInputFieldValue = InputFieldValueBase & { export const zFloatInputFieldValue = zInputFieldValueBase.extend({
type: 'string'; type: z.literal('float'),
value?: string; value: z.number().optional(),
}; });
export type FloatInputFieldValue = z.infer<typeof zFloatInputFieldValue>;
export type BooleanInputFieldValue = InputFieldValueBase & { export const zStringInputFieldValue = zInputFieldValueBase.extend({
type: 'boolean'; type: z.literal('string'),
value?: boolean; value: z.string().optional(),
}; });
export type StringInputFieldValue = z.infer<typeof zStringInputFieldValue>;
export type EnumInputFieldValue = InputFieldValueBase & { export const zBooleanInputFieldValue = zInputFieldValueBase.extend({
type: 'enum'; type: z.literal('boolean'),
value?: number | string; value: z.boolean().optional(),
}; });
export type BooleanInputFieldValue = z.infer<typeof zBooleanInputFieldValue>;
export type LatentsInputFieldValue = InputFieldValueBase & { export const zEnumInputFieldValue = zInputFieldValueBase.extend({
type: 'LatentsField'; type: z.literal('enum'),
value?: undefined; value: z.union([z.string(), z.number()]).optional(),
}; });
export type EnumInputFieldValue = z.infer<typeof zEnumInputFieldValue>;
export type ConditioningInputFieldValue = InputFieldValueBase & { export const zLatentsInputFieldValue = zInputFieldValueBase.extend({
type: 'ConditioningField'; type: z.literal('LatentsField'),
value?: string; value: zLatentsField.optional(),
}; });
export type LatentsInputFieldValue = z.infer<typeof zLatentsInputFieldValue>;
export type ControlInputFieldValue = InputFieldValueBase & { export const zConditioningInputFieldValue = zInputFieldValueBase.extend({
type: 'ControlField'; type: z.literal('ConditioningField'),
value?: undefined; value: zConditioningField.optional(),
}; });
export type ConditioningInputFieldValue = z.infer<
typeof zConditioningInputFieldValue
>;
export type UNetInputFieldValue = InputFieldValueBase & { export const zControlNetModel = zModelIdentifier;
type: 'UNetField'; export type ControlNetModel = z.infer<typeof zControlNetModel>;
value?: undefined;
};
export type ClipInputFieldValue = InputFieldValueBase & { export const zControlField = zInputFieldValueBase.extend({
type: 'ClipField'; image: zImageField,
value?: undefined; control_model: zControlNetModel,
}; control_weight: z.union([z.number(), z.array(z.number())]).optional(),
begin_step_percent: z.number().optional(),
end_step_percent: z.number().optional(),
control_mode: z
.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced'])
.optional(),
resize_mode: z
.enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple'])
.optional(),
});
export type ControlField = z.infer<typeof zControlField>;
export type VaeInputFieldValue = InputFieldValueBase & { export const zControlInputFieldTemplate = zInputFieldValueBase.extend({
type: 'VaeField'; type: z.literal('ControlField'),
value?: undefined; value: zControlField.optional(),
}; });
export type ControlInputFieldValue = z.infer<typeof zControlInputFieldTemplate>;
export type ImageInputFieldValue = InputFieldValueBase & { export const zModelType = z.enum([
type: 'ImageField'; 'onnx',
value?: ImageField; 'main',
}; 'vae',
'lora',
'controlnet',
'embedding',
]);
export type ModelType = z.infer<typeof zModelType>;
export type ImageCollectionInputFieldValue = InputFieldValueBase & { export const zSubModelType = z.enum([
type: 'ImageCollection'; 'unet',
value?: ImageField[]; 'text_encoder',
}; 'text_encoder_2',
'tokenizer',
'tokenizer_2',
'vae',
'vae_decoder',
'vae_encoder',
'scheduler',
'safety_checker',
]);
export type SubModelType = z.infer<typeof zSubModelType>;
export type MainModelInputFieldValue = InputFieldValueBase & { export const zModelInfo = zModelIdentifier.extend({
type: 'MainModelField'; model_type: zModelType,
value?: MainModelParam | OnnxModelParam; submodel: zSubModelType.optional(),
}; });
export type ModelInfo = z.infer<typeof zModelInfo>;
export type SDXLMainModelInputFieldValue = InputFieldValueBase & { export const zLoraInfo = zModelInfo.extend({
type: 'SDXLMainModelField'; weight: z.number().optional(),
value?: MainModelParam | OnnxModelParam; });
}; export type LoraInfo = z.infer<typeof zLoraInfo>;
export type SDXLRefinerModelInputFieldValue = InputFieldValueBase & { export const zUNetField = z.object({
type: 'SDXLRefinerModelField'; unet: zModelInfo,
value?: MainModelParam | OnnxModelParam; scheduler: zModelInfo,
}; loras: z.array(zLoraInfo),
});
export type UNetField = z.infer<typeof zUNetField>;
export type VaeModelInputFieldValue = InputFieldValueBase & { export const zUNetInputFieldValue = zInputFieldValueBase.extend({
type: 'VaeModelField'; type: z.literal('UNetField'),
value?: VaeModelParam; value: zUNetField.optional(),
}; });
export type UNetInputFieldValue = z.infer<typeof zUNetInputFieldValue>;
export type LoRAModelInputFieldValue = InputFieldValueBase & { export const zClipField = z.object({
type: 'LoRAModelField'; tokenizer: zModelInfo,
value?: LoRAModelParam; text_encoder: zModelInfo,
}; skipped_layers: z.number(),
loras: z.array(zLoraInfo),
});
export type ClipField = z.infer<typeof zClipField>;
export type ControlNetModelInputFieldValue = InputFieldValueBase & { export const zClipInputFieldValue = zInputFieldValueBase.extend({
type: 'ControlNetModelField'; type: z.literal('ClipField'),
value?: ControlNetModelParam; value: zClipField.optional(),
}; });
export type ClipInputFieldValue = z.infer<typeof zClipInputFieldValue>;
export type CollectionInputFieldValue = InputFieldValueBase & { export const zVaeField = z.object({
type: 'Collection'; vae: zModelInfo,
value?: (string | number)[]; });
}; export type VaeField = z.infer<typeof zVaeField>;
export type CollectionItemInputFieldValue = InputFieldValueBase & { export const zVaeInputFieldValue = zInputFieldValueBase.extend({
type: 'CollectionItem'; type: z.literal('VaeField'),
value?: undefined; value: zVaeField.optional(),
}; });
export type VaeInputFieldValue = z.infer<typeof zVaeInputFieldValue>;
export type ColorInputFieldValue = InputFieldValueBase & { export const zImageInputFieldValue = zInputFieldValueBase.extend({
type: 'ColorField'; type: z.literal('ImageField'),
value?: RgbaColor; value: zImageField.optional(),
}; });
export type ImageInputFieldValue = z.infer<typeof zImageInputFieldValue>;
export type SchedulerInputFieldValue = InputFieldValueBase & { export const zImageCollectionInputFieldValue = zInputFieldValueBase.extend({
type: 'Scheduler'; type: z.literal('ImageCollection'),
value?: SchedulerParam; value: z.array(zImageField).optional(),
}; });
export type ImageCollectionInputFieldValue = z.infer<
typeof zImageCollectionInputFieldValue
>;
export const zMainModelInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('MainModelField'),
value: zMainOrOnnxModel.optional(),
});
export type MainModelInputFieldValue = z.infer<
typeof zMainModelInputFieldValue
>;
export const zSDXLMainModelInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('SDXLMainModelField'),
value: zMainOrOnnxModel.optional(),
});
export type SDXLMainModelInputFieldValue = z.infer<
typeof zSDXLMainModelInputFieldValue
>;
export const zSDXLRefinerModelInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('SDXLRefinerModelField'),
value: zMainOrOnnxModel.optional(), // TODO: should narrow this down to a refiner model
});
export type SDXLRefinerModelInputFieldValue = z.infer<
typeof zSDXLRefinerModelInputFieldValue
>;
export const zVaeModelField = zModelIdentifier;
export const zVaeModelInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('VaeModelField'),
value: zVaeModelField.optional(),
});
export type VaeModelInputFieldValue = z.infer<typeof zVaeModelInputFieldValue>;
export const zLoRAModelField = zModelIdentifier;
export type LoRAModelField = z.infer<typeof zLoRAModelField>;
export const zLoRAModelInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('LoRAModelField'),
value: zLoRAModelField.optional(),
});
export type LoRAModelInputFieldValue = z.infer<
typeof zLoRAModelInputFieldValue
>;
export const zControlNetModelField = zModelIdentifier;
export type ControlNetModelField = z.infer<typeof zControlNetModelField>;
export const zControlNetModelInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('ControlNetModelField'),
value: zControlNetModelField.optional(),
});
export type ControlNetModelInputFieldValue = z.infer<
typeof zControlNetModelInputFieldValue
>;
export const zCollectionInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('Collection'),
value: z.array(z.any()).optional(), // TODO: should this field ever have a value?
});
export type CollectionInputFieldValue = z.infer<
typeof zCollectionInputFieldValue
>;
export const zCollectionItemInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('CollectionItem'),
value: z.any().optional(), // TODO: should this field ever have a value?
});
export type CollectionItemInputFieldValue = z.infer<
typeof zCollectionItemInputFieldValue
>;
export const zColorField = z.object({
r: z.number().int().min(0).max(255),
g: z.number().int().min(0).max(255),
b: z.number().int().min(0).max(255),
a: z.number().int().min(0).max(255),
});
export type ColorField = z.infer<typeof zColorField>;
export const zColorInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('ColorField'),
value: zColorField.optional(),
});
export type ColorInputFieldValue = z.infer<typeof zColorInputFieldValue>;
export const zSchedulerInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('Scheduler'),
value: zScheduler.optional(),
});
export type SchedulerInputFieldValue = z.infer<
typeof zSchedulerInputFieldValue
>;
export const zInputFieldValue = z.discriminatedUnion('type', [
zIntegerInputFieldValue,
zFloatInputFieldValue,
zStringInputFieldValue,
zBooleanInputFieldValue,
zImageInputFieldValue,
zLatentsInputFieldValue,
zConditioningInputFieldValue,
zUNetInputFieldValue,
zClipInputFieldValue,
zVaeInputFieldValue,
zControlInputFieldTemplate,
zEnumInputFieldValue,
zMainModelInputFieldValue,
zSDXLMainModelInputFieldValue,
zSDXLRefinerModelInputFieldValue,
zVaeModelInputFieldValue,
zLoRAModelInputFieldValue,
zControlNetModelInputFieldValue,
zCollectionInputFieldValue,
zCollectionItemInputFieldValue,
zColorInputFieldValue,
zImageCollectionInputFieldValue,
zSchedulerInputFieldValue,
]);
export type InputFieldValue = z.infer<typeof zInputFieldValue>;
export type InputFieldTemplateBase = { export type InputFieldTemplateBase = {
name: string; name: string;
@ -576,24 +715,26 @@ export const isInvocationFieldSchema = (
export type InvocationEdgeExtra = { type: 'default' | 'collapsed' }; export type InvocationEdgeExtra = { type: 'default' | 'collapsed' };
export const zInputFieldValue = z.object({
id: z.string().trim().min(1),
name: z.string().trim().min(1),
type: zFieldType,
label: z.string(),
isExposed: z.boolean(),
});
export const zInvocationNodeData = z.object({ export const zInvocationNodeData = z.object({
id: z.string().trim().min(1), id: z.string().trim().min(1),
// no easy way to build this dynamically, and we don't want to anyways, because this will be used
// to validate incoming workflows, and we want to allow community nodes.
type: z.string().trim().min(1), type: z.string().trim().min(1),
inputs: z.record(z.any()), inputs: z.record(zInputFieldValue),
outputs: z.record(z.any()), outputs: z.record(zOutputFieldValue),
label: z.string(), label: z.string(),
isOpen: z.boolean(), isOpen: z.boolean(),
notes: z.string(), notes: z.string(),
}); });
// Massage this to get better type safety while developing
export type InvocationNodeData = Omit<
z.infer<typeof zInvocationNodeData>,
'type'
> & {
type: AnyInvocationType;
};
export const zNotesNodeData = z.object({ export const zNotesNodeData = z.object({
id: z.string().trim().min(1), id: z.string().trim().min(1),
type: z.literal('notes'), type: z.literal('notes'),
@ -602,6 +743,69 @@ export const zNotesNodeData = z.object({
notes: z.string(), notes: z.string(),
}); });
export type NotesNodeData = z.infer<typeof zNotesNodeData>;
export const zWorkflowInvocationNode = z.object({
id: z.string().trim().min(1),
type: z.literal('invocation'),
data: zInvocationNodeData,
width: z.number().gt(0),
height: z.number().gt(0),
position: z.object({
x: z.number(),
y: z.number(),
}),
});
export const zWorkflowNotesNode = z.object({
id: z.string().trim().min(1),
type: z.literal('notes'),
data: zNotesNodeData,
width: z.number().gt(0),
height: z.number().gt(0),
position: z.object({
x: z.number(),
y: z.number(),
}),
});
export const zWorkflowNode = z.discriminatedUnion('type', [
zWorkflowInvocationNode,
zWorkflowNotesNode,
]);
export type WorkflowNode = z.infer<typeof zWorkflowNode>;
export const zWorkflowEdge = z.object({
source: z.string().trim().min(1),
sourceHandle: z.string().trim().min(1),
target: z.string().trim().min(1),
targetHandle: z.string().trim().min(1),
id: z.string().trim().min(1),
type: z.enum(['default', 'collapsed']),
});
export const zFieldIdentifier = z.object({
nodeId: z.string().trim().min(1),
fieldName: z.string().trim().min(1),
});
export type FieldIdentifier = z.infer<typeof zFieldIdentifier>;
export const zSemVer = z.string().refine((val) => {
const [major, minor, patch] = val.split('.');
return (
major !== undefined &&
minor !== undefined &&
patch !== undefined &&
Number.isInteger(Number(major)) &&
Number.isInteger(Number(minor)) &&
Number.isInteger(Number(patch))
);
});
export type SemVer = z.infer<typeof zSemVer>;
export const zWorkflow = z.object({ export const zWorkflow = z.object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
author: z.string(), author: z.string(),
@ -610,67 +814,12 @@ export const zWorkflow = z.object({
contact: z.string(), contact: z.string(),
tags: z.string(), tags: z.string(),
notes: z.string(), notes: z.string(),
nodes: z.array( nodes: z.array(zWorkflowNode),
z.object({ edges: z.array(zWorkflowEdge),
id: z.string().trim().min(1), exposedFields: z.array(zFieldIdentifier),
type: z.string().trim().min(1),
data: z.union([zInvocationNodeData, zNotesNodeData]),
width: z.number().gt(0),
height: z.number().gt(0),
position: z.object({
x: z.number(),
y: z.number(),
}),
})
),
edges: z.array(
z.object({
source: z.string().trim().min(1),
sourceHandle: z.string().trim().min(1),
target: z.string().trim().min(1),
targetHandle: z.string().trim().min(1),
id: z.string().trim().min(1),
type: z.string().trim().min(1),
})
),
}); });
export type Workflow = { export type Workflow = z.infer<typeof zWorkflow>;
name: string;
author: string;
description: string;
version: string;
contact: string;
tags: string;
notes: string;
nodes: Pick<
Node<InvocationNodeData | NotesNodeData>,
'id' | 'type' | 'data' | 'width' | 'height' | 'position'
>[];
edges: Pick<
Edge<InvocationEdgeExtra>,
'source' | 'sourceHandle' | 'target' | 'targetHandle' | 'id' | 'type'
>[];
exposedFields: FieldIdentifier[];
};
export type InvocationNodeData = {
id: string;
type: AnyInvocationType;
inputs: Record<string, InputFieldValue>;
outputs: Record<string, OutputFieldValue>;
label: string;
isOpen: boolean;
notes: string;
};
export type NotesNodeData = {
id: string;
type: 'notes';
label: string;
notes: string;
isOpen: boolean;
};
export type CurrentImageNodeData = { export type CurrentImageNodeData = {
id: string; id: string;
@ -710,25 +859,6 @@ export enum NodeStatus {
FAILED, FAILED,
} }
type SavedOutput =
| components['schemas']['StringOutput']
| components['schemas']['IntegerOutput']
| components['schemas']['FloatOutput']
| components['schemas']['ImageOutput'];
export const isSavedOutput = (
output: GraphExecutionState['results'][string]
): output is SavedOutput =>
Boolean(
output &&
[
'string_output',
'integer_output',
'float_output',
'image_output',
].includes(output?.type)
);
export type NodeExecutionState = { export type NodeExecutionState = {
nodeId: string; nodeId: string;
status: NodeStatus; status: NodeStatus;
@ -738,11 +868,6 @@ export type NodeExecutionState = {
outputs: AnyResult[]; outputs: AnyResult[];
}; };
export type FieldIdentifier = {
nodeId: string;
fieldName: string;
};
export type FieldComponentProps< export type FieldComponentProps<
V extends InputFieldValue, V extends InputFieldValue,
T extends InputFieldTemplate, T extends InputFieldTemplate,

View File

@ -1,8 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { pick } from 'lodash-es';
import { NodesState } from '../store/types'; import { NodesState } from '../store/types';
import { Workflow, isInvocationNode, isNotesNode } from '../types/types'; import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types';
export const buildWorkflow = (nodesState: NodesState): Workflow => { export const buildWorkflow = (nodesState: NodesState): Workflow => {
const { workflow: workflowMeta, nodes, edges } = nodesState; const { workflow: workflowMeta, nodes, edges } = nodesState;
@ -13,25 +12,19 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
}; };
nodes.forEach((node) => { nodes.forEach((node) => {
if (!isInvocationNode(node) && !isNotesNode(node)) { const result = zWorkflowNode.safeParse(node);
if (!result.success) {
return; return;
} }
workflow.nodes.push( workflow.nodes.push(result.data);
pick(node, ['id', 'type', 'position', 'width', 'height', 'data'])
);
}); });
edges.forEach((edge) => { edges.forEach((edge) => {
workflow.edges.push( const result = zWorkflowEdge.safeParse(edge);
pick(edge, [ if (!result.success) {
'source', return;
'sourceHandle', }
'target', workflow.edges.push(result.data);
'targetHandle',
'id',
'type',
])
);
}); });
return workflow; return workflow;

View File

@ -210,7 +210,7 @@ export type HeightParam = z.infer<typeof zHeight>;
export const isValidHeight = (val: unknown): val is HeightParam => export const isValidHeight = (val: unknown): val is HeightParam =>
zHeight.safeParse(val).success; zHeight.safeParse(val).success;
const zBaseModel = z.enum(['sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); export const zBaseModel = z.enum(['sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']);
export type BaseModelParam = z.infer<typeof zBaseModel>; export type BaseModelParam = z.infer<typeof zBaseModel>;

View File

@ -1,38 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useMemo } from 'react';
import { configSelector } from '../store/configSelectors';
import { systemSelector } from '../store/systemSelectors';
const isApplicationReadySelector = createSelector(
[systemSelector, configSelector],
(system, config) => {
const { wasSchemaParsed } = system;
const { disabledTabs } = config;
return {
disabledTabs,
wasSchemaParsed,
};
}
);
/**
* Checks if the application is ready to be used, i.e. if the initial startup process is finished.
*/
export const useIsApplicationReady = () => {
const { disabledTabs, wasSchemaParsed } = useAppSelector(
isApplicationReadySelector
);
const isApplicationReady = useMemo(() => {
if (!disabledTabs.includes('nodes') && !wasSchemaParsed) {
return false;
}
return true;
}, [disabledTabs, wasSchemaParsed]);
return isApplicationReady;
};

View File

@ -16,7 +16,6 @@ export const systemPersistDenylist: (keyof SystemState)[] = [
'isCancelScheduled', 'isCancelScheduled',
'progressImage', 'progressImage',
'wereModelsReceived', 'wereModelsReceived',
'wasSchemaParsed',
'isPersisted', 'isPersisted',
'isUploading', 'isUploading',
]; ];

View File

@ -2,7 +2,6 @@ import { UseToastOptions } from '@chakra-ui/react';
import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { InvokeLogLevel } from 'app/logging/logger'; import { InvokeLogLevel } from 'app/logging/logger';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { startCase, upperFirst } from 'lodash-es'; import { startCase, upperFirst } from 'lodash-es';
import { LogLevelName } from 'roarr'; import { LogLevelName } from 'roarr';
@ -68,10 +67,6 @@ export interface SystemState {
* Whether or not the available models were received * Whether or not the available models were received
*/ */
wereModelsReceived: boolean; wereModelsReceived: boolean;
/**
* Whether or not the OpenAPI schema was received and parsed
*/
wasSchemaParsed: boolean;
/** /**
* The console output logging level * The console output logging level
*/ */
@ -112,7 +107,6 @@ export const initialSystemState: SystemState = {
isCancelScheduled: false, isCancelScheduled: false,
subscribedNodeIds: [], subscribedNodeIds: [],
wereModelsReceived: false, wereModelsReceived: false,
wasSchemaParsed: false,
consoleLogLevel: 'debug', consoleLogLevel: 'debug',
shouldLogToConsole: true, shouldLogToConsole: true,
statusTranslationKey: 'common.statusDisconnected', statusTranslationKey: 'common.statusDisconnected',
@ -339,13 +333,6 @@ export const systemSlice = createSlice({
); );
}); });
/**
* OpenAPI schema was parsed
*/
builder.addCase(nodeTemplatesBuilt, (state) => {
state.wasSchemaParsed = true;
});
// *** Matchers - must be after all cases *** // *** Matchers - must be after all cases ***
/** /**

View File

@ -6238,12 +6238,6 @@ export type components = {
* @enum {string} * @enum {string}
*/ */
StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* ControlNetModelFormat
* @description An enumeration.
* @enum {string}
*/
ControlNetModelFormat: "checkpoint" | "diffusers";
/** /**
* StableDiffusionXLModelFormat * StableDiffusionXLModelFormat
* @description An enumeration. * @description An enumeration.
@ -6256,6 +6250,12 @@ export type components = {
* @enum {string} * @enum {string}
*/ */
StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/**
* ControlNetModelFormat
* @description An enumeration.
* @enum {string}
*/
ControlNetModelFormat: "checkpoint" | "diffusers";
/** /**
* StableDiffusionOnnxModelFormat * StableDiffusionOnnxModelFormat
* @description An enumeration. * @description An enumeration.

View File

@ -7063,6 +7063,11 @@ z-schema@~5.0.2:
optionalDependencies: optionalDependencies:
commander "^10.0.0" commander "^10.0.0"
zod-validation-error@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-1.5.0.tgz#2b355007a1c3b7fb04fa476bfad4e7b3fd5491e3"
integrity sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==
zod@^3.22.2: zod@^3.22.2:
version "3.22.2" version "3.22.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b"