Files
BlackVortexAI-bv_nodepack/scripts/generate_example_workflows.mjs
T

166 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import path from "node:path";
const root = path.resolve(import.meta.dirname, "..");
const out = path.join(root, "examples", "workflows");
fs.mkdirSync(out, { recursive: true });
const base = (nodes, links = [], groups = [], extra = {}) => ({
id: crypto.randomUUID(), revision: 0,
last_node_id: Math.max(...nodes.map((node) => Number(node.id)), 0),
last_link_id: Math.max(...links.map((link) => Number(link[0])), 0),
nodes, links, groups, config: {},
extra: { ds: { scale: 1, offset: [80, 80] }, frontendVersion: "1.48.7", ...extra },
version: 0.4,
});
const note = (id, title, text, pos, size = [380, 180]) => ({
id, type: "MarkdownNote", pos, size, flags: {}, order: id, mode: 0,
inputs: [], outputs: [], title, properties: {}, widgets_values: [text], color: "#223", bgcolor: "#112",
});
const props = (name) => ({ "Node name for S&R": name, cnr_id: "bv_nodepack", ver: "0.1.0" });
const input = (name, type, link = null, widget = false, label = undefined) => ({ name, type, link, ...(widget ? { widget: { name } } : {}), ...(label ? { label } : {}) });
const output = (name, type, links = null) => ({ name, type, links });
const save = (name, workflow) => fs.writeFileSync(path.join(out, name), `${JSON.stringify(workflow, null, 2)}\n`);
const showText = (id, pos, link, value = "") => ({
id, type: "ShowText|pysssss", pos, size: [260, 90], flags: {}, order: id, mode: 0,
inputs: [{ localized_name: "text", name: "text", type: "STRING", link }],
outputs: [{ localized_name: "STRING", name: "STRING", shape: 6, type: "STRING", links: null }],
properties: { cnr_id: "comfyui-custom-scripts", ver: "1.2.5", "Node name for S&R": "ShowText|pysssss" },
widgets_values: [value],
});
// Prompt AST: a real category workflow with two independent consumers.
save("prompt-ast-categories.json", base([
note(1, "Prompt AST – categories instead of string surgery", "## Practical use\nMaintain one structured prompt, enable or disable optional blocks, then decode only the categories required by a downstream encoder. The Debug node keeps the AST visible while building the workflow.", [20, 20], [430, 210]),
{ id: 2, type: "BV Prompt Encode", pos: [500, 40], size: [360, 240], flags: {}, order: 2, mode: 0,
inputs: [input("prompt", "STRING", null, true)], outputs: [output("ast", "BV_AST", [1, 2]), output("cleaned_prompt", "STRING")], properties: props("BV Prompt Encode"),
widgets_values: ["A cinematic portrait of an astronaut\n@@lighting\nsoft rim light, volumetric haze\n@@detail\nhighly detailed helmet reflections\n@@negative\ntext, watermark"] },
{ id: 3, type: "BV Prompt Category Switch", pos: [930, 40], size: [340, 190], flags: {}, order: 3, mode: 0,
inputs: [input("ast", "BV_AST", 1), input("enable_categories", "STRING", null, true), input("disable_categories", "STRING", null, true), input("inherit", "BOOLEAN", null, true), input("mode", "BOOLEAN", null, true)],
outputs: [output("ast", "BV_AST", [3]), output("report", "STRING")], properties: props("BV Prompt Category Switch"), widgets_values: ["lighting\ndetail", "negative", true, true] },
{ id: 4, type: "BV Prompt Decode", pos: [1340, 40], size: [340, 220], flags: {}, order: 4, mode: 0,
inputs: [input("ast", "BV_AST", 3), input("category_filter", "STRING", null, true), input("with_comments", "BOOLEAN", null, true), input("inherit", "BOOLEAN", null, true), input("mode", "BOOLEAN", null, true), input("prettify", "BOOLEAN", null, true)],
outputs: [output("ast", "BV_AST"), output("filtered_prompt", "STRING", [4])], properties: props("BV Prompt Decode"), widgets_values: ["default, lighting, detail", false, true, true, true] },
{ id: 5, type: "BV Prompt AST Debug", pos: [930, 300], size: [750, 230], flags: {}, order: 5, mode: 0,
inputs: [input("ast", "BV_AST", 2), input("pretty", "BOOLEAN", null, true), input("indent", "INT", null, true), input("sort_keys", "BOOLEAN", null, true)],
outputs: [output("ast_json", "STRING")], properties: props("BV Prompt AST Debug"), widgets_values: [true, 2, false] },
showText(6, [1740, 80], 4, "A cinematic portrait of an astronaut\nsoft rim light, volumetric haze\nhighly detailed helmet reflections"),
], [[1,2,0,3,0,"BV_AST"],[2,2,0,5,0,"BV_AST"],[3,3,0,4,0,"BV_AST"],[4,4,1,6,0,"STRING"]]));
// Random latent: configured for a realistic portrait workflow.
save("empty-latent-random-ratio.json", base([
note(1, "Random ratio latent – reproducible composition variants", "## Practical use\nGenerate a deterministic mix of landscape, portrait and square canvases for prompt exploration. Standard ratios are one-click toggles; project-specific ratios stay in the text field. Connect **latent** directly to your sampler.", [20, 20], [430, 220]),
{ id: 2, type: "BV Empty Latent Random Ratio", pos: [500, 20], size: [420, 430], flags: {}, order: 2, mode: 0,
inputs: [input("seed","INT",null,true),input("resolution","COMBO",null,true),input("custom_resolution","INT",null,true),input("alignment","COMBO",null,true),
...["use_1_1","use_3_2","use_2_3","use_4_3","use_3_4","use_16_9","use_9_16","use_21_9","use_9_21"].map((name)=>input(name,"BOOLEAN",null,true)),input("ratios","STRING",null,true),input("batch_size","INT",null,true)],
outputs: [output("latent","LATENT"),output("width","INT"),output("height","INT"),output("picked_ratio","STRING")], properties: props("BV Empty Latent Random Ratio"),
widgets_values: [42,"1024",1024,"64",true,true,true,false,false,true,true,false,false,"5:4\n4:5",1] },
note(3, "Suggested downstream wiring", "**latent → KSampler.latent_image**\n\nThe additional width, height and picked_ratio outputs are useful for filenames, metadata and resolution-aware post-processing.", [980, 80], [360, 190]),
]));
// Subgraph UI building blocks, laid out in the same order they should be exposed.
save("subgraph-ui-layout.json", base([
note(1, "Build a clean Subgraph interface", "## Practical use\nExpose these inputs in the shown order. The outer Subgraph node becomes a small purpose-built UI: heading, model preset, divider, numeric settings and intentional spacing.", [20, 20], [420, 210]),
{ id: 2, type: "BV Subgraph Heading", pos: [500, 20], size: [330, 150], flags: {}, order: 2, mode: 0,
inputs: [input("value","STRING",null,true),input("font_size","INT",null,true),input("show_divider","BOOLEAN",null,true)], outputs:[output("_","STRING")], properties:props("BV Subgraph Heading"), widgets_values:["Generation Settings",26,false] },
{ id: 3, type: "BV Dynamic Combo", pos: [500, 205], size: [330, 210], flags: {}, order: 3, mode: 0,
inputs:[input("value","STRING",null,true),input("options","STRING",null,true)], outputs:[output("value_str","STRING"),output("value_int","INT"),output("value_float","FLOAT")], properties:props("BV Dynamic Combo"), widgets_values:["Balanced","Fast\nBalanced\nQuality"] },
{ id: 4, type: "BV Subgraph Divider", pos: [900, 70], size: [300, 145], flags: {}, order: 4, mode: 0,
inputs:[input("thickness","INT",null,true),input("padding","INT",null,true),input("alpha","FLOAT",null,true)], outputs:[output("_","STRING")], properties:props("BV Subgraph Divider"), widgets_values:[2,10,0.35] },
{ id: 5, type: "BV Subgraph Spacer", pos: [900, 250], size: [300, 125], flags: {}, order: 5, mode: 0,
inputs:[input("height","INT",null,true),input("scale","FLOAT",null,true)], outputs:[output("_","STRING")], properties:props("BV Subgraph Spacer"), widgets_values:[24,1] },
note(6, "Expose to the Subgraph input", "Connect each presentation output to the Subgraph input bracket, then expose the functional values between them. Stable identities preserve this order across save/load and input changes.", [1260, 90], [380, 210]),
]));
// Use the validated live test graphs for the complex stateful examples.
for (const [source, target] of [
["Dev Pipe Test.json", "smart-pipe-wireless-merge.json"],
["BV Control Center Test.json", "control-center-workflow-states.json"],
]) {
const sourcePath = path.resolve(root, "..", "..", "user", "default", "workflows", source);
if (!fs.existsSync(sourcePath)) continue;
const workflow = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
if (target === "smart-pipe-wireless-merge.json") {
const positions = new Map([[204,[20,30]],[199,[320,20]],[205,[20,310]],[198,[320,300]],[200,[820,170]],[201,[1260,150]]]);
workflow.nodes = workflow.nodes.filter((node) => positions.has(node.id));
for (const node of workflow.nodes) node.pos = positions.get(node.id);
const ids = new Set(workflow.nodes.map((node) => node.id));
workflow.links = workflow.links.filter((link) => ids.has(link[1]) && ids.has(link[3]));
const first = workflow.nodes.find((node) => node.id === 199);
const second = workflow.nodes.find((node) => node.id === 198);
const afterMerge = workflow.nodes.find((node) => node.id === 201);
workflow.nodes.find((node) => node.id === 204).widgets_values = ["A lighthouse above a stormy sea"];
workflow.nodes.find((node) => node.id === 205).widgets_values = ["text, watermark"];
const firstSlot = first.properties.bvSmartPipe.localSlots[0];
const secondSlot = second.properties.bvSmartPipe.localSlots[0];
Object.assign(firstSlot, { name: "prompt" });
Object.assign(first.properties.bvSmartPipe.resolvedSlots[0], { name: "prompt" });
Object.assign(first.inputs[1], { label: "prompt", localized_name: "prompt" });
Object.assign(secondSlot, { name: "negative_prompt" });
Object.assign(second.properties.bvSmartPipe.resolvedSlots[0], { name: "negative_prompt" });
Object.assign(second.inputs[1], { label: "negative_prompt", localized_name: "negative_prompt" });
const styleSlot = { id: "5c881b13-e3ab-4caf-9a09-92659fba6419", name: "style", ordinal: 2, type: "STRING", showInput: true, showOutput: false };
first.properties.bvSmartPipe.localSlots.push(styleSlot);
first.properties.bvSmartPipe.resolvedSlots.push({ ...styleSlot, missing: false, dormant: false });
first.properties.bvSmartPipe.nextOrdinal = 3;
first.inputs.splice(2, 0, { label: "style", localized_name: "style", name: "v_002", shape: 7, type: "STRING", link: 188 });
first.widgets_values[0] = JSON.stringify([
{ ...first.properties.bvSmartPipe.resolvedSlots[0], connected: true },
{ ...first.properties.bvSmartPipe.resolvedSlots[1], connected: true },
]);
second.widgets_values[0] = JSON.stringify([{ ...second.properties.bvSmartPipe.resolvedSlots[0], connected: true }]);
const mergedSlots = [
{ ...first.properties.bvSmartPipe.resolvedSlots[0], showInput: false, showOutput: true },
{ ...first.properties.bvSmartPipe.resolvedSlots[1], showInput: false, showOutput: true },
{ ...second.properties.bvSmartPipe.resolvedSlots[0], showInput: false, showOutput: true },
];
afterMerge.properties.bvSmartPipe.inheritedSlots = mergedSlots.map((slot) => ({ ...slot }));
afterMerge.properties.bvSmartPipe.resolvedSlots = mergedSlots.map((slot) => ({ ...slot }));
afterMerge.outputs = [afterMerge.outputs[0], ...mergedSlots.map((slot, index) => ({ localized_name: slot.name, name: slot.name, type: "STRING", links: [189 + index] }))];
afterMerge.widgets_values[0] = JSON.stringify(mergedSlots.map((slot) => ({ ...slot, connected: true })));
const routeKey = Object.keys(workflow.extra.bvSmartPipeRoutes).find((key) => key.endsWith(afterMerge.properties.bvSmartPipe.routing.nodeId));
workflow.extra.bvSmartPipeRoutes[routeKey].projection = { version: 1, inheritedSlots: mergedSlots.map((slot) => ({ ...slot })), resolvedSlots: mergedSlots.map((slot) => ({ ...slot })) };
const styleValue = JSON.parse(JSON.stringify(workflow.nodes.find((node) => node.id === 204)));
Object.assign(styleValue, { id: 206, pos: [20, 150], order: 11, widgets_values: ["cinematic"] });
styleValue.outputs[0].links = [188];
workflow.nodes.push(styleValue, showText(207, [1740, 40], 189, "A lighthouse above a stormy sea"), showText(208, [1740, 170], 190, "cinematic"), showText(209, [1740, 300], 191, "text, watermark"));
workflow.links.push([188,206,0,199,2,"STRING"],[189,201,1,207,0,"STRING"],[190,201,2,208,0,"STRING"],[191,201,3,209,0,"STRING"]);
workflow.last_node_id = 209; workflow.last_link_id = 191;
workflow.extra.ds = { scale: 0.72, offset: [100, 250] };
}
if (target === "control-center-workflow-states.json") {
const keep = new Set([6, 2, 3]);
workflow.nodes = workflow.nodes.filter((node) => keep.has(node.id));
const control = workflow.nodes.find((node) => node.id === 6);
const generate = workflow.nodes.find((node) => node.id === 2);
const upscale = workflow.nodes.find((node) => node.id === 3);
control.pos = [30, 150]; control.size = [330, 130]; control.title = "🌀 BV Control Center";
const controlConfig = JSON.parse(control.widgets_values[0]);
controlConfig.controls[0].name = "Generate only";
controlConfig.controls[0].assignments = controlConfig.controls[0].assignments.filter((assignment) => assignment.groupTitle === "Upscale");
controlConfig.controls[1].name = "Full pipeline";
controlConfig.controls[1].assignments = controlConfig.controls[1].assignments.map((assignment) => ({ ...assignment, groupId: "db2a373c-9542-4657-ad6d-1a2dd6b3757c", groupPath: "Generate", groupTitle: "Generate", action: "activate" }));
controlConfig.controls[0].enabled = true;
controlConfig.controls[1].enabled = false;
control.widgets_values[0] = JSON.stringify(controlConfig);
control.widgets_values[2] = true;
control.widgets_values[3] = false;
control.inputs[1].label = "Generate only";
control.inputs[2].label = "Full pipeline";
workflow.extra.bv_settings.controlConfig = controlConfig;
generate.pos = [540, 80]; generate.size = [270, 106]; generate.mode = 0;
upscale.pos = [540, 310]; upscale.size = [270, 106]; upscale.mode = 4;
workflow.groups = workflow.groups.filter((group) => group.id === 2 || group.id === 4);
const generateGroup = workflow.groups.find((group) => group.id === 4);
const upscaleGroup = workflow.groups.find((group) => group.id === 2);
generateGroup.bounding = [500, 20, 350, 205];
upscaleGroup.bounding = [500, 250, 350, 205];
workflow.last_node_id = 6; workflow.last_link_id = 0;
workflow.extra.ds = { scale: 0.9, offset: [140, 170] };
}
fs.writeFileSync(path.join(out, target), `${JSON.stringify(workflow, null, 2)}\n`);
}