Added functionality for users to add a _meta extension so that they can optionally add information. See action.json and clothes.json they are optional but only makes sense when connected to a character.
extra input nodes will now purge faster
This commit is contained in:
drago87
2026-02-24 14:00:44 +01:00
parent 47a860c7cd
commit 0bd1dccc2e
8 changed files with 431 additions and 110 deletions
+17 -17
View File
@@ -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
},)
+1 -1
View File
@@ -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]
+364 -76
View File
@@ -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);
}
}
}
+21 -2
View File
@@ -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 || {};
+11
View File
@@ -0,0 +1,11 @@
{
"_meta": {
"extension": "character"
},
"actions": {
"primary": "string",
"secondary": "string",
"tertiary": "string",
"quaternary": "string"
}
}
+1 -7
View File
@@ -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"
}
}
+1 -7
View File
@@ -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"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"_meta": {
"extension": "character"
},
"clothing": {
"top": "string",
"bottom": "string",
"footwear": "string",
"accessories": "string",
"underwear": {
"bra": "string",
"panties": "string"
}
}
}