diff --git a/package-lock.json b/package-lock.json index 0f4dc8702..38de8a256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "comfyui-frontend", "version": "0.0.0", + "dependencies": { + "zod": "^3.23.8", + "zod-validation-error": "^3.3.0" + }, "devDependencies": { "@babel/core": "^7.24.7", "@babel/preset-env": "^7.22.20", @@ -8694,6 +8698,25 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.0.tgz", + "integrity": "sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } } } diff --git a/package.json b/package.json index f202add72..86ecc277e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "npm run typecheck && vite build", "typecheck": "tsc --noEmit", - "test": "jest", + "test": "npm run build && jest", "test:generate": "npx tsx tests-ui/setup", "preview": "vite preview" }, @@ -24,5 +24,9 @@ "typescript": "^5.4.5", "vite": "^5.2.0", "vite-plugin-static-copy": "^1.0.5" + }, + "dependencies": { + "zod": "^3.23.8", + "zod-validation-error": "^3.3.0" } } diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 712e33bb4..e4c5a3b5c 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -10,6 +10,7 @@ import { DraggableList } from "./ui/draggableList"; import { applyTextReplacements, addStylesheet } from "./utils"; import type { ComfyExtension } from "/types/comfy"; import type { LGraph, LGraphCanvas, LGraphNode } from "/types/litegraph"; +import { type ComfyWorkflow, parseComfyWorkflow } from "../types/comfyWorkflow"; export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview" @@ -973,16 +974,18 @@ export class ComfyApp { // No image found. Look for node data data = data.getData("text/plain"); - let workflow; + let workflow: ComfyWorkflow; try { data = data.slice(data.indexOf("{")); - workflow = JSON.parse(data); + workflow = await parseComfyWorkflow(data); } catch (err) { try { data = data.slice(data.indexOf("workflow\n")); data = data.slice(data.indexOf("{")); - workflow = JSON.parse(data); - } catch (error) {} + workflow = await parseComfyWorkflow(data); + } catch (error) { + console.error(error); + } } if (workflow && workflow.version && workflow.nodes && workflow.extra) { @@ -1652,8 +1655,7 @@ export class ComfyApp { try { const loadWorkflow = async (json) => { if (json) { - const workflow = JSON.parse(json); - await this.loadGraphData(workflow); + await this.loadGraphData(await parseComfyWorkflow(json)); return true; } }; @@ -1895,7 +1897,7 @@ export class ComfyApp { * @param {*} graphData A serialized graph object * @param { boolean } clean If the graph state, e.g. images, should be cleared */ - async loadGraphData(graphData?, clean: boolean = true, restore_view: boolean = true) { + async loadGraphData(graphData?: ComfyWorkflow, clean: boolean = true, restore_view: boolean = true) { if (clean !== false) { this.clean(); } @@ -1932,6 +1934,9 @@ export class ComfyApp { try { this.graph.configure(graphData); if (restore_view && this.enableWorkflowViewRestore.value && graphData.extra?.ds) { + // @ts-ignore + // Need to set strict: true for zod to match the type [number, number] + // https://github.com/colinhacks/zod/issues/3056 this.canvas.ds.offset = graphData.extra.ds.offset; this.canvas.ds.scale = graphData.extra.ds.scale; } @@ -2273,7 +2278,7 @@ export class ComfyApp { if (file.type === "image/png") { const pngInfo = await getPngMetadata(file); if (pngInfo?.workflow) { - await this.loadGraphData(JSON.parse(pngInfo.workflow)); + await this.loadGraphData(await parseComfyWorkflow(pngInfo.workflow)); } else if (pngInfo?.prompt) { this.loadApiJson(JSON.parse(pngInfo.prompt)); } else if (pngInfo?.parameters) { @@ -2288,7 +2293,7 @@ export class ComfyApp { const prompt = pngInfo?.prompt || pngInfo?.Prompt; if (workflow) { - this.loadGraphData(JSON.parse(workflow)); + this.loadGraphData(await parseComfyWorkflow(workflow)); } else if (prompt) { this.loadApiJson(JSON.parse(prompt)); } else { @@ -2297,13 +2302,14 @@ export class ComfyApp { } else if (file.type === "application/json" || file.name?.endsWith(".json")) { const reader = new FileReader(); reader.onload = async () => { - const jsonContent = JSON.parse(reader.result as string); + const readerResult = reader.result as string; + const jsonContent = JSON.parse(readerResult); if (jsonContent?.templates) { this.loadTemplateData(jsonContent); } else if(this.isApiJson(jsonContent)) { this.loadApiJson(jsonContent); } else { - await this.loadGraphData(jsonContent); + await this.loadGraphData(await parseComfyWorkflow(readerResult)); } }; reader.readAsText(file); @@ -2313,7 +2319,7 @@ export class ComfyApp { // @ts-ignore if (info.workflow) { // @ts-ignore - await this.loadGraphData(JSON.parse(info.workflow)); + await this.loadGraphData(await parseComfyWorkflow(info.workflow)); // @ts-ignore } else if (info.prompt) { // @ts-ignore diff --git a/src/scripts/defaultGraph.ts b/src/scripts/defaultGraph.ts index 9b3cb4a7e..7f2422b5b 100644 --- a/src/scripts/defaultGraph.ts +++ b/src/scripts/defaultGraph.ts @@ -1,4 +1,6 @@ -export const defaultGraph = { +import type { ComfyWorkflow } from "/types/comfyWorkflow"; + +export const defaultGraph: ComfyWorkflow = { last_node_id: 9, last_link_id: 9, nodes: [ diff --git a/src/types/comfyWorkflow.ts b/src/types/comfyWorkflow.ts new file mode 100644 index 000000000..ef75f6046 --- /dev/null +++ b/src/types/comfyWorkflow.ts @@ -0,0 +1,118 @@ +import { z } from 'zod'; +import { fromZodError } from 'zod-validation-error'; + +const zComfyLink = z.tuple([ + z.number(), // Link id + z.number(), // Node id of source node + z.number(), // Output slot# of source node + z.number(), // Node id of destination node + z.number(), // Input slot# of destination node + z.string(), // Data type +]); + +const zNodeOutput = z.object({ + name: z.string(), + type: z.string(), + links: z.array(z.number()).nullable(), + slot_index: z.number().optional(), +}).passthrough(); + +const zNodeInput = z.object({ + name: z.string(), + type: z.string(), + link: z.number().nullable(), + slot_index: z.number().optional(), +}).passthrough(); + +const zFlags = z.object({ + collapsed: z.boolean().optional(), + pinned: z.boolean().optional(), + allow_interaction: z.boolean().optional(), + horizontal: z.boolean().optional(), + skip_repeated_outputs: z.boolean().optional(), +}).passthrough(); + +const zProperties = z.object({ + ["Node name for S&R"]: z.string().optional(), +}).passthrough(); + +const zVector2 = z.union([ + z.object({ 0: z.number(), 1: z.number() }), + z.tuple([z.number(), z.number()]), +]); + +const zComfyNode = z.object({ + id: z.number(), + type: z.string(), + pos: z.tuple([z.number(), z.number()]), + size: zVector2, + flags: zFlags, + order: z.number(), + mode: z.number(), + inputs: z.array(zNodeInput).optional(), + outputs: z.array(zNodeOutput).optional(), + properties: zProperties, + widgets_values: z.array(z.any()).optional(), // This could contain mixed types + color: z.string().optional(), + bgcolor: z.string().optional(), +}).passthrough(); + +const zGroup = z.object({ + title: z.string(), + bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]), + color: z.string(), + font_size: z.number(), + locked: z.boolean(), +}).passthrough(); + +const zInfo = z.object({ + name: z.string(), + author: z.string(), + description: z.string(), + version: z.string(), + created: z.string(), + modified: z.string(), + software: z.string(), +}).passthrough(); + +const zDS = z.object({ + scale: z.number(), + offset: zVector2, +}).passthrough(); + +const zConfig = z.object({ + links_ontop: z.boolean().optional(), + align_to_grid: z.boolean().optional(), +}).passthrough(); + +const zExtra = z.object({ + ds: zDS.optional(), + info: zInfo.optional(), +}).passthrough(); + +const zComfyWorkflow = z.object({ + last_node_id: z.number(), + last_link_id: z.number(), + nodes: z.array(zComfyNode), + links: z.array(zComfyLink), + groups: z.array(zGroup).optional(), + config: zConfig.optional().nullable(), + extra: zExtra.optional().nullable(), + version: z.number(), +}).passthrough(); + +export type NodeInput = z.infer; +export type NodeOutput = z.infer; +export type ComfyLink = z.infer; +export type ComfyNode = z.infer; +export type ComfyWorkflow = z.infer; + + +export async function parseComfyWorkflow(data: string): Promise { + // Validate + const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data)); + if (!result.success) { + throw fromZodError(result.error); + } + return result.data; +} diff --git a/tests-ui/tests/comfyWorkflow.test.ts b/tests-ui/tests/comfyWorkflow.test.ts new file mode 100644 index 000000000..79a219e49 --- /dev/null +++ b/tests-ui/tests/comfyWorkflow.test.ts @@ -0,0 +1,55 @@ +import { parseComfyWorkflow } from "../../src/types/comfyWorkflow"; +import { defaultGraph } from "../../src/scripts/defaultGraph"; +import fs from "fs"; + +const WORKFLOW_DIR = "tests-ui/workflows"; + +describe("parseComfyWorkflow", () => { + it("parses valid workflow", async () => { + fs.readdirSync(WORKFLOW_DIR).forEach(async (file) => { + if (file.endsWith(".json")) { + const data = fs.readFileSync(`${WORKFLOW_DIR}/${file}`, "utf-8"); + await expect(parseComfyWorkflow(data)).resolves.not.toThrow(); + } + }); + }); + + it("workflow.nodes", async () => { + const workflow = JSON.parse(JSON.stringify(defaultGraph)); + workflow.nodes = undefined; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow(); + + workflow.nodes = null; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow(); + + workflow.nodes = []; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow(); + }); + + it("workflow.version", async () => { + const workflow = JSON.parse(JSON.stringify(defaultGraph)); + workflow.version = undefined; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow(); + + workflow.version = "1.0.1"; // Invalid format. + await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow(); + + workflow.version = 1; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow(); + }); + + it("workflow.extra", async () => { + const workflow = JSON.parse(JSON.stringify(defaultGraph)); + workflow.extra = undefined; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow(); + + workflow.extra = null; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow(); + + workflow.extra = {}; + await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow(); + + workflow.extra = { foo: "bar" }; // Should accept extra fields. + await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow(); + }); +}); diff --git a/tests-ui/workflows/default_workflow.json b/tests-ui/workflows/default_workflow.json new file mode 100644 index 000000000..811b6f030 --- /dev/null +++ b/tests-ui/workflows/default_workflow.json @@ -0,0 +1,385 @@ +{ + "last_node_id": 9, + "last_link_id": 9, + "nodes": [ + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 413, + 389 + ], + "size": { + "0": 425.27801513671875, + "1": 180.6060791015625 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "text, watermark" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 415, + 186 + ], + "size": { + "0": 422.84503173828125, + "1": 164.31304931640625 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 3 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 4 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + 473, + 609 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 1 + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 863, + 186 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 1 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 7 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 156680208700286, + "randomize", + 20, + 8, + "euler", + "normal", + 1 + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1209, + 188 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 9 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 9, + "type": "SaveImage", + "pos": [ + 1451, + 189 + ], + "size": { + "0": 210, + "1": 58 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 9 + } + ], + "properties": { + "Node name for S&R": "SaveImage" + }, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 26, + 474 + ], + "size": { + "0": 315, + "1": 98 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 1 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 3, + 5 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 8 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "3Guofeng3_v32Light.safetensors" + ] + } + ], + "links": [ + [ + 1, + 4, + 0, + 3, + 0, + "MODEL" + ], + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 3, + 4, + 1, + 6, + 0, + "CLIP" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 5, + 4, + 1, + 7, + 0, + "CLIP" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 7, + 3, + 0, + 8, + 0, + "LATENT" + ], + [ + 8, + 4, + 2, + 8, + 1, + "VAE" + ], + [ + 9, + 8, + 0, + 9, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.8264462809917354, + "offset": [ + 565.6800000000005, + -43.919999999999995 + ] + }, + "info": { + "name": "workflow", + "author": "", + "description": "", + "version": "1", + "created": "2024-06-02T20:17:02.243Z", + "modified": "2024-06-02T20:17:11.438Z", + "software": "ComfyUI" + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/tests-ui/workflows/workflow_disconnected.json b/tests-ui/workflows/workflow_disconnected.json new file mode 100644 index 000000000..48cba0b0f --- /dev/null +++ b/tests-ui/workflows/workflow_disconnected.json @@ -0,0 +1,378 @@ +{ + "last_node_id": 9, + "last_link_id": 9, + "nodes": [ + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 413, + 389 + ], + "size": { + "0": 425.27801513671875, + "1": 180.6060791015625 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "text, watermark" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 406.3210576748693, + 165.8590597338342 + ], + "size": { + "0": 422.84503173828125, + "1": 164.31304931640625 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 3 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 4 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + 473, + 609 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 1 + ] + }, + { + "id": 9, + "type": "SaveImage", + "pos": [ + 1451, + 189 + ], + "size": { + "0": 210, + "1": 58 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 9 + } + ], + "properties": { + "Node name for S&R": "SaveImage" + }, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 26, + 474 + ], + "size": { + "0": 315, + "1": 98 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 1 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 3, + 5 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 8 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "3Guofeng3_v32Light.safetensors" + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 863, + 186 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 1 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": null + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 156680208700286, + "randomize", + 20, + 8, + "euler", + "normal", + 1 + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1209, + 188 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 9 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + } + ], + "links": [ + [ + 1, + 4, + 0, + 3, + 0, + "MODEL" + ], + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 3, + 4, + 1, + 6, + 0, + "CLIP" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 5, + 4, + 1, + 7, + 0, + "CLIP" + ], + [ + 8, + 4, + 2, + 8, + 1, + "VAE" + ], + [ + 9, + 8, + 0, + 9, + 0, + "IMAGE" + ] + ], + "groups": [ + { + "title": "Group", + "bounding": [ + 489, + 95, + 140, + 80 + ], + "color": "#3f789e", + "font_size": 24, + "locked": false + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 1.3513057093104008, + "offset": [ + 127.07026983402625, + 138.77779138162384 + ] + }, + "info": { + "name": "workflow", + "author": "", + "description": "", + "version": "1", + "created": "2024-06-11T23:37:08.326Z", + "modified": "2024-06-12T13:25:28.857Z", + "software": "ComfyUI" + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/tests-ui/workflows/workflow_with_group.json b/tests-ui/workflows/workflow_with_group.json new file mode 100644 index 000000000..900b8612b --- /dev/null +++ b/tests-ui/workflows/workflow_with_group.json @@ -0,0 +1,398 @@ +{ + "last_node_id": 9, + "last_link_id": 9, + "nodes": [ + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 413, + 389 + ], + "size": { + "0": 425.27801513671875, + "1": 180.6060791015625 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "text, watermark" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 406.3210576748693, + 165.8590597338342 + ], + "size": { + "0": 422.84503173828125, + "1": 164.31304931640625 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 3 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 4 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + 473, + 609 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 1 + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 863, + 186 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 1 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 7 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 156680208700286, + "randomize", + 20, + 8, + "euler", + "normal", + 1 + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1209, + 188 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 9 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 9, + "type": "SaveImage", + "pos": [ + 1451, + 189 + ], + "size": { + "0": 210, + "1": 58 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 9 + } + ], + "properties": { + "Node name for S&R": "SaveImage" + }, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 26, + 474 + ], + "size": { + "0": 315, + "1": 98 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 1 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 3, + 5 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 8 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "3Guofeng3_v32Light.safetensors" + ] + } + ], + "links": [ + [ + 1, + 4, + 0, + 3, + 0, + "MODEL" + ], + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 3, + 4, + 1, + 6, + 0, + "CLIP" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 5, + 4, + 1, + 7, + 0, + "CLIP" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 7, + 3, + 0, + 8, + 0, + "LATENT" + ], + [ + 8, + 4, + 2, + 8, + 1, + "VAE" + ], + [ + 9, + 8, + 0, + 9, + 0, + "IMAGE" + ] + ], + "groups": [ + { + "title": "Group", + "bounding": [ + 489, + 95, + 140, + 80 + ], + "color": "#3f789e", + "font_size": 24, + "locked": false + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 1.11678157794248, + "offset": [ + 323.87991710157155, + 235.75066665118254 + ] + }, + "info": { + "name": "workflow", + "author": "", + "description": "", + "version": "1", + "created": "2024-06-11T23:37:08.326Z", + "modified": "2024-06-11T23:37:08.327Z", + "software": "ComfyUI" + } + }, + "version": 0.4 +} \ No newline at end of file