| | |
| | |
| |
|
| | const { start, createDefaultWorkflow, getNodeDef, checkBeforeAndAfterReload } = require("../utils"); |
| | const lg = require("../utils/litegraph"); |
| |
|
| | describe("group node", () => { |
| | beforeEach(() => { |
| | lg.setup(global); |
| | }); |
| |
|
| | afterEach(() => { |
| | lg.teardown(global); |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async function convertToGroup(app, graph, name, nodes) { |
| | |
| | for (const n of nodes) { |
| | n.select(true); |
| | } |
| |
|
| | expect(Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b)).toEqual( |
| | nodes.map((n) => n.id + "").sort((a, b) => +a - +b) |
| | ); |
| |
|
| | global.prompt = jest.fn().mockImplementation(() => name); |
| | const groupNode = await nodes[0].menu["Convert to Group Node"].call(false); |
| |
|
| | |
| | expect(window.prompt).toHaveBeenCalled(); |
| |
|
| | |
| | for (const n of nodes) { |
| | expect(n.isRemoved).toBeTruthy(); |
| | } |
| |
|
| | expect(groupNode.type).toEqual("workflow/" + name); |
| |
|
| | return graph.find(groupNode); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function getOutput(idMap = {}, valueMap = {}) { |
| | if (idMap instanceof Array) { |
| | idMap = idMap.reduce((p, n) => { |
| | p[n] = n + ""; |
| | return p; |
| | }, {}); |
| | } |
| | const expected = { |
| | 1: { inputs: { ckpt_name: "model1.safetensors", ...valueMap?.[1] }, class_type: "CheckpointLoaderSimple" }, |
| | 2: { inputs: { text: "positive", clip: ["1", 1], ...valueMap?.[2] }, class_type: "CLIPTextEncode" }, |
| | 3: { inputs: { text: "negative", clip: ["1", 1], ...valueMap?.[3] }, class_type: "CLIPTextEncode" }, |
| | 4: { inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] }, class_type: "EmptyLatentImage" }, |
| | 5: { |
| | inputs: { |
| | seed: 0, |
| | steps: 20, |
| | cfg: 8, |
| | sampler_name: "euler", |
| | scheduler: "normal", |
| | denoise: 1, |
| | model: ["1", 0], |
| | positive: ["2", 0], |
| | negative: ["3", 0], |
| | latent_image: ["4", 0], |
| | ...valueMap?.[5], |
| | }, |
| | class_type: "KSampler", |
| | }, |
| | 6: { inputs: { samples: ["5", 0], vae: ["1", 2], ...valueMap?.[6] }, class_type: "VAEDecode" }, |
| | 7: { inputs: { filename_prefix: "ComfyUI", images: ["6", 0], ...valueMap?.[7] }, class_type: "SaveImage" }, |
| | }; |
| |
|
| | |
| | const mapped = {}; |
| | for (const oldId in idMap) { |
| | mapped[idMap[oldId]] = expected[oldId]; |
| | delete expected[oldId]; |
| | } |
| | Object.assign(mapped, expected); |
| |
|
| | |
| | for (const k in mapped) { |
| | for (const input in mapped[k].inputs) { |
| | const v = mapped[k].inputs[input]; |
| | if (v instanceof Array) { |
| | if (v[0] in idMap) { |
| | v[0] = idMap[v[0]] + ""; |
| | } |
| | } |
| | } |
| | } |
| |
|
| | return mapped; |
| | } |
| |
|
| | test("can be created from selected nodes", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty]); |
| |
|
| | |
| | expect(group.inputs).toHaveLength(2); |
| | expect(group.outputs).toHaveLength(3); |
| |
|
| | expect(group.inputs.map((i) => i.input.name)).toEqual(["clip", "CLIPTextEncode clip"]); |
| | expect(group.outputs.map((i) => i.output.name)).toEqual(["LATENT", "CONDITIONING", "CLIPTextEncode CONDITIONING"]); |
| |
|
| | |
| | expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [group.id, 0], |
| | [group.id, 1], |
| | ]); |
| |
|
| | |
| | expect(group.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [nodes.sampler.id, 1], |
| | ]); |
| | |
| | expect( |
| | group.outputs["CLIPTextEncode CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index]) |
| | ).toEqual([[nodes.sampler.id, 2]]); |
| | |
| | expect(group.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [nodes.sampler.id, 3], |
| | ]); |
| | }); |
| |
|
| | test("maintains all output links on conversion", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const save2 = ez.SaveImage(...nodes.decode.outputs); |
| | const save3 = ez.SaveImage(...nodes.decode.outputs); |
| | |
| | const group = await convertToGroup(app, graph, "test", [nodes.sampler, nodes.decode]); |
| | expect(group.outputs[0].connections.length).toBe(3); |
| | expect(group.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id); |
| | expect(group.outputs[0].connections[1].targetNode.id).toBe(save2.id); |
| | expect(group.outputs[0].connections[2].targetNode.id).toBe(save3.id); |
| |
|
| | |
| | const newNodes = group.menu["Convert to nodes"].call(); |
| | const decode = graph.find(newNodes.find((n) => n.type === "VAEDecode")); |
| | expect(decode.outputs[0].connections.length).toBe(3); |
| | expect(decode.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id); |
| | expect(decode.outputs[0].connections[1].targetNode.id).toBe(save2.id); |
| | expect(decode.outputs[0].connections[2].targetNode.id).toBe(save3.id); |
| | }); |
| | test("can be be converted back to nodes", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const toConvert = [nodes.pos, nodes.neg, nodes.empty, nodes.sampler]; |
| | const group = await convertToGroup(app, graph, "test", toConvert); |
| |
|
| | |
| | expect(group.widgets["text"].value).toBe("positive"); |
| | group.widgets["text"].value = "pos"; |
| | expect(group.widgets["CLIPTextEncode text"].value).toBe("negative"); |
| | group.widgets["CLIPTextEncode text"].value = "neg"; |
| | expect(group.widgets["width"].value).toBe(512); |
| | group.widgets["width"].value = 1024; |
| | expect(group.widgets["sampler_name"].value).toBe("euler"); |
| | group.widgets["sampler_name"].value = "ddim"; |
| | expect(group.widgets["control_after_generate"].value).toBe("randomize"); |
| | group.widgets["control_after_generate"].value = "fixed"; |
| |
|
| | |
| | group.menu["Convert to nodes"].call(); |
| |
|
| | |
| | const pos = graph.find(nodes.pos.id); |
| | expect(pos.node.type).toBe("CLIPTextEncode"); |
| | expect(pos.widgets["text"].value).toBe("pos"); |
| | const neg = graph.find(nodes.neg.id); |
| | expect(neg.node.type).toBe("CLIPTextEncode"); |
| | expect(neg.widgets["text"].value).toBe("neg"); |
| | const empty = graph.find(nodes.empty.id); |
| | expect(empty.node.type).toBe("EmptyLatentImage"); |
| | expect(empty.widgets["width"].value).toBe(1024); |
| | const sampler = graph.find(nodes.sampler.id); |
| | expect(sampler.node.type).toBe("KSampler"); |
| | expect(sampler.widgets["sampler_name"].value).toBe("ddim"); |
| | expect(sampler.widgets["control_after_generate"].value).toBe("fixed"); |
| |
|
| | |
| | expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [pos.id, 0], |
| | [neg.id, 0], |
| | ]); |
| |
|
| | expect(pos.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [nodes.sampler.id, 1], |
| | ]); |
| |
|
| | expect(neg.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [nodes.sampler.id, 2], |
| | ]); |
| |
|
| | expect(empty.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ |
| | [nodes.sampler.id, 3], |
| | ]); |
| | }); |
| | test("it can embed reroutes as inputs", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | |
| | const reroute = ez.Reroute(); |
| | nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]); |
| | reroute.outputs[0].connectTo(nodes.pos.inputs[0]); |
| | reroute.outputs[0].connectTo(nodes.neg.inputs[0]); |
| |
|
| | |
| | const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty, reroute]); |
| | expect(group.inputs).toHaveLength(1); |
| | expect(group.inputs[0].input.type).toEqual("CLIP"); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual(getOutput()); |
| | }); |
| | test("it can embed reroutes as outputs", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | |
| | const reroute = ez.Reroute(); |
| | nodes.decode.outputs.IMAGE.connectTo(reroute.inputs[0]); |
| |
|
| | |
| | const group = await convertToGroup(app, graph, "test", [nodes.decode, nodes.save, reroute]); |
| | expect(group.outputs).toHaveLength(1); |
| | expect(group.outputs[0].output.type).toEqual("IMAGE"); |
| | expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.decode.id, nodes.save.id])); |
| | }); |
| | test("it can embed reroutes as pipes", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | |
| | const rerouteModel = ez.Reroute(); |
| | const rerouteClip = ez.Reroute(); |
| | const rerouteVae = ez.Reroute(); |
| | nodes.ckpt.outputs.MODEL.connectTo(rerouteModel.inputs[0]); |
| | nodes.ckpt.outputs.CLIP.connectTo(rerouteClip.inputs[0]); |
| | nodes.ckpt.outputs.VAE.connectTo(rerouteVae.inputs[0]); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [rerouteModel, rerouteClip, rerouteVae]); |
| |
|
| | expect(group.outputs).toHaveLength(3); |
| | expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]); |
| |
|
| | expect(group.outputs).toHaveLength(3); |
| | expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]); |
| |
|
| | group.outputs[0].connectTo(nodes.sampler.inputs.model); |
| | group.outputs[1].connectTo(nodes.pos.inputs.clip); |
| | group.outputs[1].connectTo(nodes.neg.inputs.clip); |
| | }); |
| | test("can handle reroutes used internally", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | let reroutes = []; |
| | let prevNode = nodes.ckpt; |
| | for (let i = 0; i < 5; i++) { |
| | const reroute = ez.Reroute(); |
| | prevNode.outputs[0].connectTo(reroute.inputs[0]); |
| | prevNode = reroute; |
| | reroutes.push(reroute); |
| | } |
| | prevNode.outputs[0].connectTo(nodes.sampler.inputs.model); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [...reroutes, ...Object.values(nodes)]); |
| | expect((await graph.toPrompt()).output).toEqual(getOutput()); |
| |
|
| | group.menu["Convert to nodes"].call(); |
| | expect((await graph.toPrompt()).output).toEqual(getOutput()); |
| | }); |
| | test("creates with widget values from inner nodes", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | nodes.ckpt.widgets.ckpt_name.value = "model2.ckpt"; |
| | nodes.pos.widgets.text.value = "hello"; |
| | nodes.neg.widgets.text.value = "world"; |
| | nodes.empty.widgets.width.value = 256; |
| | nodes.empty.widgets.height.value = 1024; |
| | nodes.sampler.widgets.seed.value = 1; |
| | nodes.sampler.widgets.control_after_generate.value = "increment"; |
| | nodes.sampler.widgets.steps.value = 8; |
| | nodes.sampler.widgets.cfg.value = 4.5; |
| | nodes.sampler.widgets.sampler_name.value = "uni_pc"; |
| | nodes.sampler.widgets.scheduler.value = "karras"; |
| | nodes.sampler.widgets.denoise.value = 0.9; |
| |
|
| | const group = await convertToGroup(app, graph, "test", [ |
| | nodes.ckpt, |
| | nodes.pos, |
| | nodes.neg, |
| | nodes.empty, |
| | nodes.sampler, |
| | ]); |
| |
|
| | expect(group.widgets["ckpt_name"].value).toEqual("model2.ckpt"); |
| | expect(group.widgets["text"].value).toEqual("hello"); |
| | expect(group.widgets["CLIPTextEncode text"].value).toEqual("world"); |
| | expect(group.widgets["width"].value).toEqual(256); |
| | expect(group.widgets["height"].value).toEqual(1024); |
| | expect(group.widgets["seed"].value).toEqual(1); |
| | expect(group.widgets["control_after_generate"].value).toEqual("increment"); |
| | expect(group.widgets["steps"].value).toEqual(8); |
| | expect(group.widgets["cfg"].value).toEqual(4.5); |
| | expect(group.widgets["sampler_name"].value).toEqual("uni_pc"); |
| | expect(group.widgets["scheduler"].value).toEqual("karras"); |
| | expect(group.widgets["denoise"].value).toEqual(0.9); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual( |
| | getOutput([nodes.ckpt.id, nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id], { |
| | [nodes.ckpt.id]: { ckpt_name: "model2.ckpt" }, |
| | [nodes.pos.id]: { text: "hello" }, |
| | [nodes.neg.id]: { text: "world" }, |
| | [nodes.empty.id]: { width: 256, height: 1024 }, |
| | [nodes.sampler.id]: { |
| | seed: 1, |
| | steps: 8, |
| | cfg: 4.5, |
| | sampler_name: "uni_pc", |
| | scheduler: "karras", |
| | denoise: 0.9, |
| | }, |
| | }) |
| | ); |
| | }); |
| | test("group inputs can be reroutes", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); |
| |
|
| | const reroute = ez.Reroute(); |
| | nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]); |
| |
|
| | reroute.outputs[0].connectTo(group.inputs[0]); |
| | reroute.outputs[0].connectTo(group.inputs[1]); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id])); |
| | }); |
| | test("group outputs can be reroutes", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); |
| |
|
| | const reroute1 = ez.Reroute(); |
| | const reroute2 = ez.Reroute(); |
| | group.outputs[0].connectTo(reroute1.inputs[0]); |
| | group.outputs[1].connectTo(reroute2.inputs[0]); |
| |
|
| | reroute1.outputs[0].connectTo(nodes.sampler.inputs.positive); |
| | reroute2.outputs[0].connectTo(nodes.sampler.inputs.negative); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id])); |
| | }); |
| | test("groups can connect to each other", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const group1 = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); |
| | const group2 = await convertToGroup(app, graph, "test2", [nodes.empty, nodes.sampler]); |
| |
|
| | group1.outputs[0].connectTo(group2.inputs["positive"]); |
| | group1.outputs[1].connectTo(group2.inputs["negative"]); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual( |
| | getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id]) |
| | ); |
| | }); |
| | test("groups can connect to each other via internal reroutes", async () => { |
| | const { ez, graph, app } = await start(); |
| |
|
| | const latent = ez.EmptyLatentImage(); |
| | const vae = ez.VAELoader(); |
| | const latentReroute = ez.Reroute(); |
| | const vaeReroute = ez.Reroute(); |
| |
|
| | latent.outputs[0].connectTo(latentReroute.inputs[0]); |
| | vae.outputs[0].connectTo(vaeReroute.inputs[0]); |
| |
|
| | const group1 = await convertToGroup(app, graph, "test", [latentReroute, vaeReroute]); |
| | group1.menu.Clone.call(); |
| | expect(app.graph._nodes).toHaveLength(4); |
| | const group2 = graph.find(app.graph._nodes[3]); |
| | expect(group2.node.type).toEqual("workflow/test"); |
| | expect(group2.id).not.toEqual(group1.id); |
| |
|
| | group1.outputs.VAE.connectTo(group2.inputs.VAE); |
| | group1.outputs.LATENT.connectTo(group2.inputs.LATENT); |
| |
|
| | const decode = ez.VAEDecode(group2.outputs.LATENT, group2.outputs.VAE); |
| | const preview = ez.PreviewImage(decode.outputs[0]); |
| |
|
| | const output = { |
| | [latent.id]: { inputs: { width: 512, height: 512, batch_size: 1 }, class_type: "EmptyLatentImage" }, |
| | [vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" }, |
| | [decode.id]: { inputs: { samples: [latent.id + "", 0], vae: [vae.id + "", 0] }, class_type: "VAEDecode" }, |
| | [preview.id]: { inputs: { images: [decode.id + "", 0] }, class_type: "PreviewImage" }, |
| | }; |
| | expect((await graph.toPrompt()).output).toEqual(output); |
| |
|
| | |
| | group2.inputs.VAE.disconnect(); |
| | delete output[decode.id].inputs.vae; |
| | expect((await graph.toPrompt()).output).toEqual(output); |
| | }); |
| | test("displays generated image on group node", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | let group = await convertToGroup(app, graph, "test", [ |
| | nodes.pos, |
| | nodes.neg, |
| | nodes.empty, |
| | nodes.sampler, |
| | nodes.decode, |
| | nodes.save, |
| | ]); |
| |
|
| | const { api } = require("../../web/scripts/api"); |
| |
|
| | api.dispatchEvent(new CustomEvent("execution_start", {})); |
| | api.dispatchEvent(new CustomEvent("executing", { detail: `${nodes.save.id}` })); |
| | |
| | expect(+app.runningNodeId).toEqual(group.id); |
| | expect(group.node["imgs"]).toBeFalsy(); |
| | api.dispatchEvent( |
| | new CustomEvent("executed", { |
| | detail: { |
| | node: `${nodes.save.id}`, |
| | output: { |
| | images: [ |
| | { |
| | filename: "test.png", |
| | type: "output", |
| | }, |
| | ], |
| | }, |
| | }, |
| | }) |
| | ); |
| |
|
| | |
| | group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas); |
| |
|
| | expect(group.node["images"]).toEqual([ |
| | { |
| | filename: "test.png", |
| | type: "output", |
| | }, |
| | ]); |
| |
|
| | |
| | const workflow = JSON.stringify((await graph.toPrompt()).workflow); |
| | await app.loadGraphData(JSON.parse(workflow)); |
| | group = graph.find(group); |
| |
|
| | |
| | group.node["getInnerNodes"](); |
| |
|
| | |
| | api.dispatchEvent(new CustomEvent("execution_start", {})); |
| | api.dispatchEvent(new CustomEvent("executing", { detail: `${group.id}:5` })); |
| | |
| | expect(+app.runningNodeId).toEqual(group.id); |
| | expect(group.node["imgs"]).toBeFalsy(); |
| | api.dispatchEvent( |
| | new CustomEvent("executed", { |
| | detail: { |
| | node: `${group.id}:5`, |
| | output: { |
| | images: [ |
| | { |
| | filename: "test2.png", |
| | type: "output", |
| | }, |
| | ], |
| | }, |
| | }, |
| | }) |
| | ); |
| |
|
| | |
| | group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas); |
| |
|
| | expect(group.node["images"]).toEqual([ |
| | { |
| | filename: "test2.png", |
| | type: "output", |
| | }, |
| | ]); |
| | }); |
| | test("allows widgets to be converted to inputs", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); |
| | group.widgets[0].convertToInput(); |
| |
|
| | const primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(group.inputs["text"]); |
| | primitive.widgets[0].value = "hello"; |
| |
|
| | expect((await graph.toPrompt()).output).toEqual( |
| | getOutput([nodes.pos.id, nodes.neg.id], { |
| | [nodes.pos.id]: { text: "hello" }, |
| | }) |
| | ); |
| | }); |
| | test("can be copied", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | const group1 = await convertToGroup(app, graph, "test", [ |
| | nodes.pos, |
| | nodes.neg, |
| | nodes.empty, |
| | nodes.sampler, |
| | nodes.decode, |
| | nodes.save, |
| | ]); |
| |
|
| | group1.widgets["text"].value = "hello"; |
| | group1.widgets["width"].value = 256; |
| | group1.widgets["seed"].value = 1; |
| |
|
| | |
| | group1.menu.Clone.call(); |
| | expect(app.graph._nodes).toHaveLength(3); |
| | const group2 = graph.find(app.graph._nodes[2]); |
| | expect(group2.node.type).toEqual("workflow/test"); |
| | expect(group2.id).not.toEqual(group1.id); |
| |
|
| | |
| | nodes.ckpt.outputs.MODEL.connectTo(group2.inputs["model"]); |
| | nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["clip"]); |
| | nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["CLIPTextEncode clip"]); |
| | nodes.ckpt.outputs.VAE.connectTo(group2.inputs["vae"]); |
| |
|
| | group2.widgets["text"].value = "world"; |
| | group2.widgets["width"].value = 1024; |
| | group2.widgets["seed"].value = 100; |
| |
|
| | let i = 0; |
| | expect((await graph.toPrompt()).output).toEqual({ |
| | ...getOutput([nodes.empty.id, nodes.pos.id, nodes.neg.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], { |
| | [nodes.empty.id]: { width: 256 }, |
| | [nodes.pos.id]: { text: "hello" }, |
| | [nodes.sampler.id]: { seed: 1 }, |
| | }), |
| | ...getOutput( |
| | { |
| | [nodes.empty.id]: `${group2.id}:${i++}`, |
| | [nodes.pos.id]: `${group2.id}:${i++}`, |
| | [nodes.neg.id]: `${group2.id}:${i++}`, |
| | [nodes.sampler.id]: `${group2.id}:${i++}`, |
| | [nodes.decode.id]: `${group2.id}:${i++}`, |
| | [nodes.save.id]: `${group2.id}:${i++}`, |
| | }, |
| | { |
| | [nodes.empty.id]: { width: 1024 }, |
| | [nodes.pos.id]: { text: "world" }, |
| | [nodes.sampler.id]: { seed: 100 }, |
| | } |
| | ), |
| | }); |
| |
|
| | graph.arrange(); |
| | }); |
| | test("is embedded in workflow", async () => { |
| | let { ez, graph, app } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| | let group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); |
| | const workflow = JSON.stringify((await graph.toPrompt()).workflow); |
| |
|
| | |
| | ({ ez, graph, app } = await start({ |
| | resetEnv: true, |
| | })); |
| | |
| | expect(() => ez["workflow/test"]).toThrow(); |
| |
|
| | |
| | await app.loadGraphData(JSON.parse(workflow)); |
| |
|
| | |
| | group = graph.find(group); |
| |
|
| | |
| | expect((await graph.toPrompt()).output).toEqual( |
| | getOutput({ |
| | [nodes.pos.id]: `${group.id}:0`, |
| | [nodes.neg.id]: `${group.id}:1`, |
| | }) |
| | ); |
| | }); |
| | test("shows missing node error on missing internal node when loading graph data", async () => { |
| | const { graph } = await start(); |
| |
|
| | const dialogShow = jest.spyOn(graph.app.ui.dialog, "show"); |
| | await graph.app.loadGraphData({ |
| | last_node_id: 3, |
| | last_link_id: 1, |
| | nodes: [ |
| | { |
| | id: 3, |
| | type: "workflow/testerror", |
| | }, |
| | ], |
| | links: [], |
| | groups: [], |
| | config: {}, |
| | extra: { |
| | groupNodes: { |
| | testerror: { |
| | nodes: [ |
| | { |
| | type: "NotKSampler", |
| | }, |
| | { |
| | type: "NotVAEDecode", |
| | }, |
| | ], |
| | }, |
| | }, |
| | }, |
| | }); |
| |
|
| | expect(dialogShow).toBeCalledTimes(1); |
| | const call = dialogShow.mock.calls[0][0].innerHTML; |
| | expect(call).toContain("the following node types were not found"); |
| | expect(call).toContain("NotKSampler"); |
| | expect(call).toContain("NotVAEDecode"); |
| | expect(call).toContain("workflow/testerror"); |
| | }); |
| | test("maintains widget inputs on conversion back to nodes", async () => { |
| | const { ez, graph, app } = await start(); |
| | let pos = ez.CLIPTextEncode({ text: "positive" }); |
| | pos.node.title = "Positive"; |
| | let neg = ez.CLIPTextEncode({ text: "negative" }); |
| | neg.node.title = "Negative"; |
| | pos.widgets.text.convertToInput(); |
| | neg.widgets.text.convertToInput(); |
| |
|
| | let primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(pos.inputs.text); |
| | primitive.outputs[0].connectTo(neg.inputs.text); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [pos, neg, primitive]); |
| | |
| | expect(group.widgets.length).toBe(1); |
| | expect(group.widgets["value"].value).toBe("positive"); |
| |
|
| | const newNodes = group.menu["Convert to nodes"].call(); |
| | pos = graph.find(newNodes.find((n) => n.title === "Positive")); |
| | neg = graph.find(newNodes.find((n) => n.title === "Negative")); |
| | primitive = graph.find(newNodes.find((n) => n.type === "PrimitiveNode")); |
| |
|
| | expect(pos.inputs).toHaveLength(2); |
| | expect(neg.inputs).toHaveLength(2); |
| | expect(primitive.outputs[0].connections).toHaveLength(2); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual({ |
| | 1: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" }, |
| | 2: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" }, |
| | }); |
| | }); |
| | test("correctly handles widget inputs", async () => { |
| | const { ez, graph, app } = await start(); |
| | const upscaleMethods = (await getNodeDef("ImageScaleBy")).input.required["upscale_method"][0]; |
| |
|
| | const image = ez.LoadImage(); |
| | const scale1 = ez.ImageScaleBy(image.outputs[0]); |
| | const scale2 = ez.ImageScaleBy(image.outputs[0]); |
| | const preview1 = ez.PreviewImage(scale1.outputs[0]); |
| | const preview2 = ez.PreviewImage(scale2.outputs[0]); |
| | scale1.widgets.upscale_method.value = upscaleMethods[1]; |
| | scale1.widgets.upscale_method.convertToInput(); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [scale1, scale2]); |
| | expect(group.inputs.length).toBe(3); |
| | expect(group.inputs[0].input.type).toBe("IMAGE"); |
| | expect(group.inputs[1].input.type).toBe("IMAGE"); |
| | expect(group.inputs[2].input.type).toBe("COMBO"); |
| |
|
| | |
| | expect(group.inputs[0].connection?.originNode?.id).toBe(image.id); |
| | expect(group.inputs[1].connection?.originNode?.id).toBe(image.id); |
| | expect(group.inputs[2].connection).toBeFalsy(); |
| |
|
| | |
| | const primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(group.inputs[2]); |
| | expect(primitive.widgets.value.widget.options.values).toBe(upscaleMethods); |
| | expect(primitive.widgets.value.value).toBe(upscaleMethods[1]); |
| | primitive.widgets.value.value = upscaleMethods[1]; |
| | |
| | await checkBeforeAndAfterReload(graph, async (r) => { |
| | const scale1id = r ? `${group.id}:0` : scale1.id; |
| | const scale2id = r ? `${group.id}:1` : scale2.id; |
| | |
| | expect((await graph.toPrompt()).output).toStrictEqual({ |
| | [image.id]: { inputs: { image: "example.png", upload: "image" }, class_type: "LoadImage" }, |
| | [scale1id]: { |
| | inputs: { upscale_method: upscaleMethods[1], scale_by: 1, image: [`${image.id}`, 0] }, |
| | class_type: "ImageScaleBy", |
| | }, |
| | [scale2id]: { |
| | inputs: { upscale_method: "nearest-exact", scale_by: 1, image: [`${image.id}`, 0] }, |
| | class_type: "ImageScaleBy", |
| | }, |
| | [preview1.id]: { inputs: { images: [`${scale1id}`, 0] }, class_type: "PreviewImage" }, |
| | [preview2.id]: { inputs: { images: [`${scale2id}`, 0] }, class_type: "PreviewImage" }, |
| | }); |
| | }); |
| | }); |
| | test("adds widgets in node execution order", async () => { |
| | const { ez, graph, app } = await start(); |
| | const scale = ez.LatentUpscale(); |
| | const save = ez.SaveImage(); |
| | const empty = ez.EmptyLatentImage(); |
| | const decode = ez.VAEDecode(); |
| |
|
| | scale.outputs.LATENT.connectTo(decode.inputs.samples); |
| | decode.outputs.IMAGE.connectTo(save.inputs.images); |
| | empty.outputs.LATENT.connectTo(scale.inputs.samples); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [scale, save, empty, decode]); |
| | const widgets = group.widgets.map((w) => w.widget.name); |
| | expect(widgets).toStrictEqual([ |
| | "width", |
| | "height", |
| | "batch_size", |
| | "upscale_method", |
| | "LatentUpscale width", |
| | "LatentUpscale height", |
| | "crop", |
| | "filename_prefix", |
| | ]); |
| | }); |
| | test("adds output for external links when converting to group", async () => { |
| | const { ez, graph, app } = await start(); |
| | const img = ez.EmptyLatentImage(); |
| | let decode = ez.VAEDecode(...img.outputs); |
| | const preview1 = ez.PreviewImage(...decode.outputs); |
| | const preview2 = ez.PreviewImage(...decode.outputs); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [img, decode, preview1]); |
| |
|
| | |
| | expect(group.outputs.length).toBe(1); |
| | expect(group.outputs[0].connections.length).toBe(1); |
| | expect(group.outputs[0].connections[0].targetNode.id).toBe(preview2.id); |
| |
|
| | |
| | group.menu["Convert to nodes"].call(); |
| | decode = graph.find(decode); |
| | expect(decode.outputs[0].connections.length).toBe(2); |
| | expect(decode.outputs[0].connections[0].targetNode.id).toBe(preview1.id); |
| | expect(decode.outputs[0].connections[1].targetNode.id).toBe(preview2.id); |
| | }); |
| | test("adds output for external links when converting to group when nodes are not in execution order", async () => { |
| | const { ez, graph, app } = await start(); |
| | const sampler = ez.KSampler(); |
| | const ckpt = ez.CheckpointLoaderSimple(); |
| | const empty = ez.EmptyLatentImage(); |
| | const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" }); |
| | const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" }); |
| | const decode1 = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE); |
| | const save = ez.SaveImage(decode1.outputs.IMAGE); |
| | ckpt.outputs.MODEL.connectTo(sampler.inputs.model); |
| | pos.outputs.CONDITIONING.connectTo(sampler.inputs.positive); |
| | neg.outputs.CONDITIONING.connectTo(sampler.inputs.negative); |
| | empty.outputs.LATENT.connectTo(sampler.inputs.latent_image); |
| |
|
| | const encode = ez.VAEEncode(decode1.outputs.IMAGE); |
| | const vae = ez.VAELoader(); |
| | const decode2 = ez.VAEDecode(encode.outputs.LATENT, vae.outputs.VAE); |
| | const preview = ez.PreviewImage(decode2.outputs.IMAGE); |
| | vae.outputs.VAE.connectTo(encode.inputs.vae); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [vae, decode1, encode, sampler]); |
| |
|
| | expect(group.outputs.length).toBe(3); |
| | expect(group.outputs[0].output.name).toBe("VAE"); |
| | expect(group.outputs[0].output.type).toBe("VAE"); |
| | expect(group.outputs[1].output.name).toBe("IMAGE"); |
| | expect(group.outputs[1].output.type).toBe("IMAGE"); |
| | expect(group.outputs[2].output.name).toBe("LATENT"); |
| | expect(group.outputs[2].output.type).toBe("LATENT"); |
| |
|
| | expect(group.outputs[0].connections.length).toBe(1); |
| | expect(group.outputs[0].connections[0].targetNode.id).toBe(decode2.id); |
| | expect(group.outputs[0].connections[0].targetInput.index).toBe(1); |
| |
|
| | expect(group.outputs[1].connections.length).toBe(1); |
| | expect(group.outputs[1].connections[0].targetNode.id).toBe(save.id); |
| | expect(group.outputs[1].connections[0].targetInput.index).toBe(0); |
| |
|
| | expect(group.outputs[2].connections.length).toBe(1); |
| | expect(group.outputs[2].connections[0].targetNode.id).toBe(decode2.id); |
| | expect(group.outputs[2].connections[0].targetInput.index).toBe(0); |
| |
|
| | expect((await graph.toPrompt()).output).toEqual({ |
| | ...getOutput({ 1: ckpt.id, 2: pos.id, 3: neg.id, 4: empty.id, 5: sampler.id, 6: decode1.id, 7: save.id }), |
| | [vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: vae.node.type }, |
| | [encode.id]: { inputs: { pixels: ["6", 0], vae: [vae.id + "", 0] }, class_type: encode.node.type }, |
| | [decode2.id]: { inputs: { samples: [encode.id + "", 0], vae: [vae.id + "", 0] }, class_type: decode2.node.type }, |
| | [preview.id]: { inputs: { images: [decode2.id + "", 0] }, class_type: preview.node.type }, |
| | }); |
| | }); |
| | test("works with IMAGEUPLOAD widget", async () => { |
| | const { ez, graph, app } = await start(); |
| | const img = ez.LoadImage(); |
| | const preview1 = ez.PreviewImage(img.outputs[0]); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [img, preview1]); |
| | const widget = group.widgets["upload"]; |
| | expect(widget).toBeTruthy(); |
| | expect(widget.widget.type).toBe("button"); |
| | }); |
| | test("internal primitive populates widgets for all linked inputs", async () => { |
| | const { ez, graph, app } = await start(); |
| | const img = ez.LoadImage(); |
| | const scale1 = ez.ImageScale(img.outputs[0]); |
| | const scale2 = ez.ImageScale(img.outputs[0]); |
| | ez.PreviewImage(scale1.outputs[0]); |
| | ez.PreviewImage(scale2.outputs[0]); |
| |
|
| | scale1.widgets.width.convertToInput(); |
| | scale2.widgets.height.convertToInput(); |
| |
|
| | const primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(scale1.inputs.width); |
| | primitive.outputs[0].connectTo(scale2.inputs.height); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [img, primitive, scale1, scale2]); |
| | group.widgets.value.value = 100; |
| | expect((await graph.toPrompt()).output).toEqual({ |
| | 1: { |
| | inputs: { image: img.widgets.image.value, upload: "image" }, |
| | class_type: "LoadImage", |
| | }, |
| | 2: { |
| | inputs: { upscale_method: "nearest-exact", width: 100, height: 512, crop: "disabled", image: ["1", 0] }, |
| | class_type: "ImageScale", |
| | }, |
| | 3: { |
| | inputs: { upscale_method: "nearest-exact", width: 512, height: 100, crop: "disabled", image: ["1", 0] }, |
| | class_type: "ImageScale", |
| | }, |
| | 4: { inputs: { images: ["2", 0] }, class_type: "PreviewImage" }, |
| | 5: { inputs: { images: ["3", 0] }, class_type: "PreviewImage" }, |
| | }); |
| | }); |
| | test("primitive control widgets values are copied on convert", async () => { |
| | const { ez, graph, app } = await start(); |
| | const sampler = ez.KSampler(); |
| | sampler.widgets.seed.convertToInput(); |
| | sampler.widgets.sampler_name.convertToInput(); |
| |
|
| | let p1 = ez.PrimitiveNode(); |
| | let p2 = ez.PrimitiveNode(); |
| | p1.outputs[0].connectTo(sampler.inputs.seed); |
| | p2.outputs[0].connectTo(sampler.inputs.sampler_name); |
| |
|
| | p1.widgets.control_after_generate.value = "increment"; |
| | p2.widgets.control_after_generate.value = "decrement"; |
| | p2.widgets.control_filter_list.value = "/.*/"; |
| |
|
| | p2.node.title = "p2"; |
| |
|
| | const group = await convertToGroup(app, graph, "test", [sampler, p1, p2]); |
| | expect(group.widgets.control_after_generate.value).toBe("increment"); |
| | expect(group.widgets["p2 control_after_generate"].value).toBe("decrement"); |
| | expect(group.widgets["p2 control_filter_list"].value).toBe("/.*/"); |
| |
|
| | group.widgets.control_after_generate.value = "fixed"; |
| | group.widgets["p2 control_after_generate"].value = "randomize"; |
| | group.widgets["p2 control_filter_list"].value = "/.+/"; |
| |
|
| | group.menu["Convert to nodes"].call(); |
| | p1 = graph.find(p1); |
| | p2 = graph.find(p2); |
| |
|
| | expect(p1.widgets.control_after_generate.value).toBe("fixed"); |
| | expect(p2.widgets.control_after_generate.value).toBe("randomize"); |
| | expect(p2.widgets.control_filter_list.value).toBe("/.+/"); |
| | }); |
| | test("internal reroutes work with converted inputs and merge options", async () => { |
| | const { ez, graph, app } = await start(); |
| | const vae = ez.VAELoader(); |
| | const latent = ez.EmptyLatentImage(); |
| | const decode = ez.VAEDecode(latent.outputs.LATENT, vae.outputs.VAE); |
| | const scale = ez.ImageScale(decode.outputs.IMAGE); |
| | ez.PreviewImage(scale.outputs.IMAGE); |
| |
|
| | const r1 = ez.Reroute(); |
| | const r2 = ez.Reroute(); |
| |
|
| | latent.widgets.width.value = 64; |
| | latent.widgets.height.value = 128; |
| |
|
| | latent.widgets.width.convertToInput(); |
| | latent.widgets.height.convertToInput(); |
| | latent.widgets.batch_size.convertToInput(); |
| |
|
| | scale.widgets.width.convertToInput(); |
| | scale.widgets.height.convertToInput(); |
| |
|
| | r1.inputs[0].input.label = "hbw"; |
| | r1.outputs[0].connectTo(latent.inputs.height); |
| | r1.outputs[0].connectTo(latent.inputs.batch_size); |
| | r1.outputs[0].connectTo(scale.inputs.width); |
| |
|
| | r2.inputs[0].input.label = "wh"; |
| | r2.outputs[0].connectTo(latent.inputs.width); |
| | r2.outputs[0].connectTo(scale.inputs.height); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [r1, r2, latent, decode, scale]); |
| |
|
| | expect(group.inputs[0].input.type).toBe("VAE"); |
| | expect(group.inputs[1].input.type).toBe("INT"); |
| | expect(group.inputs[2].input.type).toBe("INT"); |
| |
|
| | const p1 = ez.PrimitiveNode(); |
| | const p2 = ez.PrimitiveNode(); |
| | p1.outputs[0].connectTo(group.inputs[1]); |
| | p2.outputs[0].connectTo(group.inputs[2]); |
| |
|
| | expect(p1.widgets.value.widget.options?.min).toBe(16); |
| | expect(p1.widgets.value.widget.options?.max).toBe(4096); |
| | expect(p1.widgets.value.widget.options?.step).toBe(80); |
| |
|
| | expect(p2.widgets.value.widget.options?.min).toBe(16); |
| | expect(p2.widgets.value.widget.options?.max).toBe(16384); |
| | expect(p2.widgets.value.widget.options?.step).toBe(80); |
| |
|
| | expect(p1.widgets.value.value).toBe(128); |
| | expect(p2.widgets.value.value).toBe(64); |
| |
|
| | p1.widgets.value.value = 16; |
| | p2.widgets.value.value = 32; |
| |
|
| | await checkBeforeAndAfterReload(graph, async (r) => { |
| | const id = (v) => (r ? `${group.id}:` : "") + v; |
| | expect((await graph.toPrompt()).output).toStrictEqual({ |
| | 1: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" }, |
| | [id(2)]: { inputs: { width: 32, height: 16, batch_size: 16 }, class_type: "EmptyLatentImage" }, |
| | [id(3)]: { inputs: { samples: [id(2), 0], vae: ["1", 0] }, class_type: "VAEDecode" }, |
| | [id(4)]: { |
| | inputs: { upscale_method: "nearest-exact", width: 16, height: 32, crop: "disabled", image: [id(3), 0] }, |
| | class_type: "ImageScale", |
| | }, |
| | 5: { inputs: { images: [id(4), 0] }, class_type: "PreviewImage" }, |
| | }); |
| | }); |
| | }); |
| | test("converted inputs with linked widgets map values correctly on creation", async () => { |
| | const { ez, graph, app } = await start(); |
| | const k1 = ez.KSampler(); |
| | const k2 = ez.KSampler(); |
| | k1.widgets.seed.convertToInput(); |
| | k2.widgets.seed.convertToInput(); |
| |
|
| | const rr = ez.Reroute(); |
| | rr.outputs[0].connectTo(k1.inputs.seed); |
| | rr.outputs[0].connectTo(k2.inputs.seed); |
| |
|
| | const group = await convertToGroup(app, graph, "test", [k1, k2, rr]); |
| | expect(group.widgets.steps.value).toBe(20); |
| | expect(group.widgets.cfg.value).toBe(8); |
| | expect(group.widgets.scheduler.value).toBe("normal"); |
| | expect(group.widgets["KSampler steps"].value).toBe(20); |
| | expect(group.widgets["KSampler cfg"].value).toBe(8); |
| | expect(group.widgets["KSampler scheduler"].value).toBe("normal"); |
| | }); |
| | test("allow multiple of the same node type to be added", async () => { |
| | const { ez, graph, app } = await start(); |
| | const nodes = [...Array(10)].map(() => ez.ImageScaleBy()); |
| | const group = await convertToGroup(app, graph, "test", nodes); |
| | expect(group.inputs.length).toBe(10); |
| | expect(group.outputs.length).toBe(10); |
| | expect(group.widgets.length).toBe(20); |
| | expect(group.widgets.map((w) => w.widget.name)).toStrictEqual( |
| | [...Array(10)] |
| | .map((_, i) => `${i > 0 ? "ImageScaleBy " : ""}${i > 1 ? i + " " : ""}`) |
| | .flatMap((p) => [`${p}upscale_method`, `${p}scale_by`]) |
| | ); |
| | }); |
| | }); |
| |
|