diff --git a/nodes.py b/nodes.py index 09d6e3b..9a0e4cd 100644 --- a/nodes.py +++ b/nodes.py @@ -201,6 +201,9 @@ class DragosSceneCompiler: "required": {}, "optional": { "input_1": (PROMPT_VAR_TYPE, {"forceInput": True}), + }, + "hidden": { + "output_json_string": ("STRING", {"multiline": True}), } } @@ -214,31 +217,24 @@ class DragosSceneCompiler: """Recursively unwrap tuples from ComfyUI PROMPT_VAR_TYPE nodes.""" depth = 0 while isinstance(v, tuple) and len(v) == 1: - #print(f"[unwrap] depth {depth}: tuple -> {v}") v = v[0] depth += 1 - #print(f"[unwrap] final value: {v}") return v def compile_json(self, **kwargs): + # If hidden output exists, use it directly + hidden_json = kwargs.get("output_json_string") + if hidden_json: + return (hidden_json,) + + # Otherwise, build from inputs as before scene = {} - - #print("=== DragosSceneCompiler: compile_json ===") for key, v in kwargs.items(): - #print(f"Input '{key}' raw value: {v}") unwrapped = self._unwrap_prompt_var(v) - #print(f"Input '{key}' unwrapped: {unwrapped}") - if isinstance(unwrapped, dict) and "name" in unwrapped and "value" in unwrapped: scene[unwrapped["name"]] = unwrapped["value"] - #print(f"Added to scene: {unwrapped['name']} -> {unwrapped['value']}") - else: - #print(f"Skipped input '{key}': not a valid prompt var") - pass json_out = json.dumps(scene, indent="\t", ensure_ascii=False) - #print("=== compile_json result ===") - #print(json_out) return (json_out,) class DragosStructuredBuilderNode: @@ -259,13 +255,17 @@ class DragosStructuredBuilderNode: } def build(self, category, json_data): - + try: parsed = json.loads(json_data) if json_data else {} except Exception: parsed = {} - + + meta = parsed.get("_meta", {}) + data = parsed.get("data", {}) + return ({ - "name": category, - "value": parsed + "name": meta.get("category", category), + "value": data, + "_meta": meta },) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f16f538..ccc61b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "Dragos-SceneBuilder" description = "This node pack offers various ways to build json strings that is ment to be used with a LLM to generate Prompts. It also have a node that can load different prompts so that a LLM can use the json string to make the Image Prompt." -version = "1.0.4" +version = "1.1.0" license = { file = "LICENSE" } [project.urls] diff --git a/web/dragos_dynamic.js b/web/dragos_dynamic.js index be1d3a9..a5e7ef9 100644 --- a/web/dragos_dynamic.js +++ b/web/dragos_dynamic.js @@ -95,88 +95,376 @@ function getValueRecursive(node, graph) function refreshNode(node, graph, previewWidget, syncPreviewHeight) { - if (!node.inputs) return; + if (!node || !graph || !node.inputs) + return; - if (previewWidget) + + //////////////////////////////////////////////////////////// + // STEP 1 — Collect ordered origin nodes (stable order) + //////////////////////////////////////////////////////////// + + const orderedOrigins = []; + + for (let i = 0; i < node.inputs.length; i++) + { + const input = node.inputs[i]; + + if (!input || input.link == null) + continue; + + const link = graph.links[input.link]; + if (!link) + continue; + + const origin = graph.getNodeById(link.origin_id); + + if (!origin) + continue; + + if (!isNodeActive(origin)) + continue; + + orderedOrigins.push(origin); + } + + + + //////////////////////////////////////////////////////////// + // STEP 2 — Build intermediate structure + //////////////////////////////////////////////////////////// + + const built = []; + + for (let i = 0; i < orderedOrigins.length; i++) + { + const origin = orderedOrigins[i]; + + const value = + getValueRecursive(origin, graph); + + if ( + value == null || + (typeof value === "object" && + Object.keys(value).length === 0) + ) + continue; + + + let category = ""; + let extension = ""; + + + // Read _meta from json_data widget ONCE + const jsonWidget = + origin.widgets?.find( + w => w.name === "json_data" + ); + + if (jsonWidget?.value) + { + try + { + const parsed = + JSON.parse(jsonWidget.value); + + const meta = parsed?._meta; + + if (meta) + { + category = + String( + meta.display_name ?? + meta.displayName ?? + meta.category ?? + "" + ).toLowerCase(); + + extension = + String( + meta.extension ?? "" + ).toLowerCase(); + } + } + catch {} + } + + + // Fallback category detection + if (!category) + { + category = + String( + getWidgetValue( + origin, + ["category", "obj_name", "var_name"] + ) || "" + ).toLowerCase(); + } + + + built.push({ + category, + extension, + value, + sceneKey: null + }); + } + + + + //////////////////////////////////////////////////////////// + // STEP 3 — Merge into final scene + //////////////////////////////////////////////////////////// + + const scene = {}; + + let characterCount = 0; + + + for (let i = 0; i < built.length; i++) + { + const entry = built[i]; + + //////////////////////////////////////////////////////// + // TOP-LEVEL + //////////////////////////////////////////////////////// + + if (!entry.extension) + { + if (entry.category === "character") + { + characterCount++; + + const key = + `character_${characterCount}`; + + scene[key] = entry.value; + + entry.sceneKey = key; + } + else if (entry.category) + { + const key = entry.category; + + scene[key] = entry.value; + + entry.sceneKey = key; + } + + continue; + } + + + //////////////////////////////////////////////////////// + // EXTENSION MERGE + //////////////////////////////////////////////////////// + + const targetCategory = + entry.extension; + + for (let j = i - 1; j >= 0; j--) + { + const candidate = built[j]; + + if ( + candidate.category !== targetCategory + ) + continue; + + const targetKey = + candidate.sceneKey; + + if (!targetKey) + break; + + if (!scene[targetKey]) + scene[targetKey] = {}; + + Object.assign( + scene[targetKey], + entry.value + ); + + break; + } + } + + + + //////////////////////////////////////////////////////////// + // STEP 4 — Remove empty objects (deep clean) + //////////////////////////////////////////////////////////// + + function removeEmpty(obj) + { + if (!obj || typeof obj !== "object") + return obj; + + for (const key in obj) + { + const val = + removeEmpty(obj[key]); + + if ( + val == null || + (typeof val === "object" && + Object.keys(val).length === 0) + ) + { + delete obj[key]; + } + } + + return obj; + } + + removeEmpty(scene); + + + + //////////////////////////////////////////////////////////// + // STEP 5 — Convert to JSON + //////////////////////////////////////////////////////////// + + const json = + JSON.stringify(scene, null, "\t"); + + + + //////////////////////////////////////////////////////////// + // STEP 6 — Update preview widget (only if changed) + //////////////////////////////////////////////////////////// + + if ( + previewWidget && + previewWidget.value !== json + ) + { + previewWidget.value = json; + + if (syncPreviewHeight) + syncPreviewHeight(); + } + + + + //////////////////////////////////////////////////////////// + // STEP 7 — Hidden output widget + //////////////////////////////////////////////////////////// + + let hiddenOutput = + node.widgets.find( + w => w.name === "output_json_string" + ); + + if (!hiddenOutput) + { + hiddenOutput = + node.addWidget( + "text", + "output_json_string", + "", + () => {} + ); + + hiddenOutput.hidden = true; + hiddenOutput.computeSize = + () => [0, -8]; + hiddenOutput.draw = + () => {}; + } + + hiddenOutput.value = json; + + + + //////////////////////////////////////////////////////////// + // STEP 8 — Dynamic inputs (CORRECT INDEX-SAFE VERSION) + //////////////////////////////////////////////////////////// + + const dynamicInputs = node.inputs + .map((slot, realIndex) => ({ + slot, + realIndex, + index: ( + slot && + slot.name && + slot.name.startsWith("input_") + ) + ? parseInt(slot.name.substring(6)) || 0 + : -1 + })) + .filter(x => x.index >= 0) + .sort((a, b) => a.index - b.index); + + + if (dynamicInputs.length === 0) + return; + + + let changed = false; + + + //////////////////////////////////////////////////////////// + // Add new slot if last is connected + //////////////////////////////////////////////////////////// + + const last = dynamicInputs[dynamicInputs.length - 1]; + + if (last.slot.link != null) { - const scene = {}; - let characterCount = 1; - - for (const input of node.inputs) - { - if (input.link == null) continue; - - const link = graph.links[input.link]; - if (!link) continue; - - const origin = graph.getNodeById(link.origin_id); - if (!origin) continue; - if (!isNodeActive(origin)) continue; - - let key = null; - - if (origin.comfyClass === "DragosStructuredBuilder") - { - const categoryWidget = - origin.widgets?.find(w => w.name === "category"); - - const category = categoryWidget?.value; - - if (category === "camera") - key = "camera"; - else if (category && category.startsWith("character")) - key = `character_${characterCount++}`; - else - key = category ?? input.name; - } - else - { - key = - getWidgetValue(origin, ["var_name", "obj_name"]) - ?? input.name; - } - - const value = getValueRecursive(origin, graph); - - if (value !== undefined) - scene[key] = value; - } - - const json = JSON.stringify(scene, null, "\t"); - - if (previewWidget.value !== json) - { - previewWidget.value = json; - - if (syncPreviewHeight) - syncPreviewHeight(); - } + node.addInput( + `input_${last.index + 1}`, + "PROMPT_VAR" + ); + + changed = true; } - - // dynamic input slots - - const inputs = node.inputs; - if (!inputs.length) return; - - const last = inputs[inputs.length - 1]; - - if (last.link != null) + + + //////////////////////////////////////////////////////////// + // Remove trailing empty slots SAFELY using REAL INDEX + //////////////////////////////////////////////////////////// + + while (dynamicInputs.length > 1) + { + const lastEntry = + dynamicInputs[dynamicInputs.length - 1]; + + const prevEntry = + dynamicInputs[dynamicInputs.length - 2]; + + if ( + lastEntry.slot.link == null && + prevEntry.slot.link == null + ) + { + // find CURRENT real index (important) + const currentRealIndex = + node.inputs.findIndex( + s => s === lastEntry.slot + ); + + if (currentRealIndex !== -1) + { + node.removeInput(currentRealIndex); + + dynamicInputs.pop(); + + changed = true; + + continue; + } + } + + break; + } + + + if (changed) { - node.addInput(`input_${inputs.length + 1}`, "PROMPT_VAR"); node.setDirtyCanvas(true, true); } - - if (inputs.length > 1) - { - const last = inputs[inputs.length - 1]; - const prev = inputs[inputs.length - 2]; - - if (last.link == null && prev.link == null) - { - node.removeInput(inputs.length - 1); - node.setDirtyCanvas(true, true); - } - } } diff --git a/web/dragos_structured_builder.js b/web/dragos_structured_builder.js index 7e54c6d..711067c 100644 --- a/web/dragos_structured_builder.js +++ b/web/dragos_structured_builder.js @@ -221,6 +221,23 @@ async function rebuild(node) { // const data = buildNestedObjectFromWidgets(node); + const schemaMeta = schema._meta || {}; + + const displayName = + schemaMeta.display_name || + schemaMeta.displayName || + categoryWidget.value || + ""; + + const final = { + _meta: { + category: String(displayName).toLowerCase(), + extension: String(schemaMeta.extension || "").toLowerCase(), + schema: String(categoryWidget.value || "").toLowerCase() + }, + data: data + }; + let jsonWidget = node.widgets.find(w => w.name === "json_data"); if (!jsonWidget) @@ -232,13 +249,15 @@ async function rebuild(node) { () => {} ); - // Fully hide visually and from layout jsonWidget.hidden = true; jsonWidget.computeSize = () => [0, -8]; jsonWidget.draw = () => {}; } - jsonWidget.value = JSON.stringify(data); + jsonWidget.value = JSON.stringify(final); + + node.properties = node.properties || {}; + node.properties.json_data = jsonWidget.value; // Also store in properties for persistence safety node.properties = node.properties || {}; diff --git a/web/schema/actions.json b/web/schema/actions.json new file mode 100644 index 0000000..c6eeeed --- /dev/null +++ b/web/schema/actions.json @@ -0,0 +1,11 @@ +{ + "_meta": { + "extension": "character" + }, + "actions": { + "primary": "string", + "secondary": "string", + "tertiary": "string", + "quaternary": "string" + } +} \ No newline at end of file diff --git a/web/schema/character_female.json b/web/schema/character_female.json index 68bd78f..19aee66 100644 --- a/web/schema/character_female.json +++ b/web/schema/character_female.json @@ -1,6 +1,6 @@ { "_meta": { - "display_name": "Character - Female", + "display_name": "Character", "gender": "Female" }, "name":"string", @@ -79,11 +79,5 @@ "shape":["round","teardrop","wide","narrow"], "symmetry":["symmetric","slightly asymmetric","asymmetric"] } - }, - "actions":{ - "primary":"string", - "secondary":"string", - "tertiary":"string", - "quaternary":"string" } } \ No newline at end of file diff --git a/web/schema/character_male.json b/web/schema/character_male.json index c60516a..259d2a8 100644 --- a/web/schema/character_male.json +++ b/web/schema/character_male.json @@ -1,6 +1,6 @@ { "_meta": { - "display_name": "Character - Male", + "display_name": "Character", "gender": "Male" }, "name":"string", @@ -48,11 +48,5 @@ }, "musculature":["slim","average","athletic","muscular"] - }, - "actions":{ - "primary":"string", - "secondary":"string", - "tertiary":"string", - "quaternary":"string" } } \ No newline at end of file diff --git a/web/schema/clothes.json b/web/schema/clothes.json new file mode 100644 index 0000000..b5726fb --- /dev/null +++ b/web/schema/clothes.json @@ -0,0 +1,15 @@ +{ + "_meta": { + "extension": "character" + }, + "clothing": { + "top": "string", + "bottom": "string", + "footwear": "string", + "accessories": "string", + "underwear": { + "bra": "string", + "panties": "string" + } + } +} \ No newline at end of file