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`); }