mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): workflow schema v3 (WIP)
The changes aim to deduplicate data between workflows and node templates, decoupling workflows from internal implementation details. A good amount of data that was needlessly duplicated from the node template to the workflow is removed. These changes substantially reduce the file size of workflows (and therefore the images with embedded workflows): - Default T2I SD1.5 workflow JSON is reduced from 23.7kb (798 lines) to 10.9kb (407 lines). - Default tiled upscale workflow JSON is reduced from 102.7kb (3341 lines) to 51.9kb (1774 lines). The trade-off is that we need to reference node templates to get things like the field type and other things. In practice, this is a non-issue, because we need a node template to do anything with a node anyways. - Field types are not included in the workflow. They are always pulled from the node templates. The field type is now properly an internal implementation detail and we can change it as needed. Previously this would require a migration for the workflow itself. With the v3 schema, the structure of a field type is an internal implementation detail that we are free to change as we see fit. - Workflow nodes no long have an `outputs` property and there is no longer such a thing as a `FieldOutputInstance`. These are only on the templates. These were never referenced at a time when we didn't also have the templates available, and there'd be no reason to do so. - Node width and height are no longer stored in the node. These weren't used. Also, per https://reactflow.dev/api-reference/types/node, we shouldn't be programmatically changing these properties. A future enhancement can properly add node resizing. - `nodeTemplates` slice is merged back into `nodesSlice` as `nodes.templates`. Turns out it's just a hassle having these separate in separate slices. - Workflow migration logic updated to support the new schema. V1 workflows migrate all the way to v3 now. - Changes throughout the nodes code to accommodate the above changes.
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||
import type { FieldInputInstance, FieldOutputInstance } from 'features/nodes/types/field';
|
||||
import type { FieldInputInstance } from 'features/nodes/types/field';
|
||||
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
|
||||
import { buildFieldInputInstance } from 'features/nodes/util/schema/buildFieldInputInstance';
|
||||
import { reduce } from 'lodash-es';
|
||||
@ -24,25 +24,6 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe
|
||||
{} as Record<string, FieldInputInstance>
|
||||
);
|
||||
|
||||
const outputs = reduce(
|
||||
template.outputs,
|
||||
(outputsAccumulator, outputTemplate, outputName) => {
|
||||
const fieldId = uuidv4();
|
||||
|
||||
const outputFieldValue: FieldOutputInstance = {
|
||||
id: fieldId,
|
||||
name: outputName,
|
||||
type: outputTemplate.type,
|
||||
fieldKind: 'output',
|
||||
};
|
||||
|
||||
outputsAccumulator[outputName] = outputFieldValue;
|
||||
|
||||
return outputsAccumulator;
|
||||
},
|
||||
{} as Record<string, FieldOutputInstance>
|
||||
);
|
||||
|
||||
const node: InvocationNode = {
|
||||
...SHARED_NODE_PROPERTIES,
|
||||
id: nodeId,
|
||||
@ -58,7 +39,6 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe
|
||||
isIntermediate: type === 'save_image' ? false : true,
|
||||
useCache: template.useCache,
|
||||
inputs,
|
||||
outputs,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -54,6 +54,5 @@ export const updateNode = (node: InvocationNode, template: InvocationTemplate):
|
||||
|
||||
// Remove any fields that are not in the template
|
||||
clone.data.inputs = pick(clone.data.inputs, keys(defaults.data.inputs));
|
||||
clone.data.outputs = pick(clone.data.outputs, keys(defaults.data.outputs));
|
||||
return clone;
|
||||
};
|
||||
|
@ -23,11 +23,8 @@ const FIELD_VALUE_FALLBACK_MAP: Record<StatefulFieldType['name'], FieldValue> =
|
||||
|
||||
export const buildFieldInputInstance = (id: string, template: FieldInputTemplate): FieldInputInstance => {
|
||||
const fieldInstance: FieldInputInstance = {
|
||||
id,
|
||||
name: template.name,
|
||||
type: template.type,
|
||||
label: '',
|
||||
fieldKind: 'input' as const,
|
||||
value: template.default ?? get(FIELD_VALUE_FALLBACK_MAP, template.type.name),
|
||||
};
|
||||
|
||||
|
@ -2,8 +2,8 @@ import { logger } from 'app/logging/logger';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
|
||||
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
||||
import type { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import i18n from 'i18n';
|
||||
import { cloneDeep, pick } from 'lodash-es';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
@ -25,14 +25,14 @@ const workflowKeys = [
|
||||
'exposedFields',
|
||||
'meta',
|
||||
'id',
|
||||
] satisfies (keyof WorkflowV2)[];
|
||||
] satisfies (keyof WorkflowV3)[];
|
||||
|
||||
export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2;
|
||||
export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3;
|
||||
|
||||
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV2 => {
|
||||
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => {
|
||||
const clonedWorkflow = pick(cloneDeep(workflow), workflowKeys);
|
||||
|
||||
const newWorkflow: WorkflowV2 = {
|
||||
const newWorkflow: WorkflowV3 = {
|
||||
...clonedWorkflow,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
@ -45,8 +45,6 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
|
||||
type: node.type,
|
||||
data: cloneDeep(node.data),
|
||||
position: { ...node.position },
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
});
|
||||
} else if (isNotesNode(node) && node.type) {
|
||||
newWorkflow.nodes.push({
|
||||
@ -54,8 +52,6 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
|
||||
type: node.type,
|
||||
data: cloneDeep(node.data),
|
||||
position: { ...node.position },
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -83,12 +79,12 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
|
||||
return newWorkflow;
|
||||
};
|
||||
|
||||
export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV2 | null => {
|
||||
export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 | null => {
|
||||
// builds what really, really should be a valid workflow
|
||||
const workflowToValidate = buildWorkflowFast({ nodes, edges, workflow });
|
||||
|
||||
// but bc we are storing this in the DB, let's be extra sure
|
||||
const result = zWorkflowV2.safeParse(workflowToValidate);
|
||||
const result = zWorkflowV3.safeParse(workflowToValidate);
|
||||
|
||||
if (!result.success) {
|
||||
const { message } = fromZodError(result.error, {
|
||||
|
@ -6,8 +6,10 @@ import { zSemVer } from 'features/nodes/types/semver';
|
||||
import { FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING } from 'features/nodes/types/v1/fieldTypeMap';
|
||||
import type { WorkflowV1 } from 'features/nodes/types/v1/workflowV1';
|
||||
import { zWorkflowV1 } from 'features/nodes/types/v1/workflowV1';
|
||||
import type { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import type { WorkflowV2 } from 'features/nodes/types/v2/workflow';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/v2/workflow';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { t } from 'i18next';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { z } from 'zod';
|
||||
@ -30,7 +32,7 @@ const zWorkflowMetaVersion = z.object({
|
||||
* - Workflow schema version bumped to 2.0.0
|
||||
*/
|
||||
const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
||||
const invocationTemplates = $store.get()?.getState().nodeTemplates.templates;
|
||||
const invocationTemplates = $store.get()?.getState().nodes.templates;
|
||||
|
||||
if (!invocationTemplates) {
|
||||
throw new Error(t('app.storeNotInitialized'));
|
||||
@ -70,26 +72,34 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
||||
return zWorkflowV2.parse(workflowToMigrate);
|
||||
};
|
||||
|
||||
const migrateV2toV3 = (workflowToMigrate: WorkflowV2): WorkflowV3 => {
|
||||
// Bump version
|
||||
(workflowToMigrate as unknown as WorkflowV3).meta.version = '3.0.0';
|
||||
// Parsing strips out any extra properties not in the latest version
|
||||
return zWorkflowV3.parse(workflowToMigrate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a workflow and migrates it to the latest version if necessary.
|
||||
*/
|
||||
export const parseAndMigrateWorkflow = (data: unknown): WorkflowV2 => {
|
||||
export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => {
|
||||
const workflowVersionResult = zWorkflowMetaVersion.safeParse(data);
|
||||
|
||||
if (!workflowVersionResult.success) {
|
||||
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
|
||||
}
|
||||
|
||||
const { version } = workflowVersionResult.data.meta;
|
||||
let workflow = data as WorkflowV1 | WorkflowV2 | WorkflowV3;
|
||||
|
||||
if (version === '1.0.0') {
|
||||
const v1 = zWorkflowV1.parse(data);
|
||||
return migrateV1toV2(v1);
|
||||
if (workflow.meta.version === '1.0.0') {
|
||||
const v1 = zWorkflowV1.parse(workflow);
|
||||
workflow = migrateV1toV2(v1);
|
||||
}
|
||||
|
||||
if (version === '2.0.0') {
|
||||
return zWorkflowV2.parse(data);
|
||||
if (workflow.meta.version === '2.0.0') {
|
||||
const v2 = zWorkflowV2.parse(workflow);
|
||||
workflow = migrateV2toV3(v2);
|
||||
}
|
||||
|
||||
throw new WorkflowVersionError(t('nodes.unrecognizedWorkflowVersion', { version }));
|
||||
return workflow as WorkflowV3;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { InvocationTemplate } from 'features/nodes/types/invocation';
|
||||
import type { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { isWorkflowInvocationNode } from 'features/nodes/types/workflow';
|
||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||
import { t } from 'i18next';
|
||||
@ -16,7 +16,7 @@ type WorkflowWarning = {
|
||||
};
|
||||
|
||||
type ValidateWorkflowResult = {
|
||||
workflow: WorkflowV2;
|
||||
workflow: WorkflowV3;
|
||||
warnings: WorkflowWarning[];
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user