saliacoel commited on
Commit
ab78416
·
verified ·
1 Parent(s): b9174bf

Upload JSONRUNNER_X.py

Browse files
Files changed (1) hide show
  1. JSONRUNNER_X.py +582 -0
JSONRUNNER_X.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import uuid
5
+ import ast
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional, Literal, Tuple
9
+
10
+ import nodes
11
+ from server import PromptServer
12
+
13
+
14
+ # Fixed workflow path (no UI input)
15
+ _WORKFLOW_JSON_PATH = "/ComfyUI/custom_nodes/comfyui-salia_online/assets/workflow_1.json"
16
+
17
+
18
+ MARKER_PREFIX = "{{{VAR=="
19
+ MARKER_SUFFIX = "==/VAR}}}"
20
+ VAR_ID_RE = re.compile(r"^(INT|FLOAT|STR)_(\d+)$", re.IGNORECASE)
21
+
22
+
23
+ @dataclass
24
+ class VarOccurrence:
25
+ node_id: str
26
+ input_key: str
27
+
28
+
29
+ @dataclass
30
+ class VarSpec:
31
+ var_id: str
32
+ var_type: Literal["INT", "FLOAT", "STR"]
33
+ min_val: Optional[float] = None
34
+ max_val: Optional[float] = None
35
+ max_len: Optional[int] = None
36
+ occurrences: List[VarOccurrence] = field(default_factory=list)
37
+ default_value: Any = None
38
+
39
+
40
+ def _load_prompt_json_fixed() -> Dict[str, Any]:
41
+ """
42
+ Loads workflow JSON from the fixed path.
43
+
44
+ Accepts either:
45
+ { ...prompt dict... }
46
+ or:
47
+ {"prompt": { ...prompt dict... }}
48
+ """
49
+ with open(_WORKFLOW_JSON_PATH, "r", encoding="utf-8") as f:
50
+ data = json.load(f)
51
+
52
+ # Accept either raw prompt dict or {"prompt": {...}}
53
+ if isinstance(data, dict) and "prompt" in data and isinstance(data["prompt"], dict):
54
+ data = data["prompt"]
55
+
56
+ if not isinstance(data, dict):
57
+ raise ValueError("Workflow JSON must be a dict mapping node_id -> node_info (ComfyUI prompt format).")
58
+
59
+ return data
60
+
61
+
62
+ def _extract_marker_text(
63
+ text: str,
64
+ *,
65
+ allow_empty_actual_key: bool = False
66
+ ) -> Optional[Tuple[str, str, Optional[float], Optional[float], Optional[int], str]]:
67
+ """
68
+ Returns (var_id, var_type, min_val, max_val, max_len, actual_key) if text starts with a marker.
69
+
70
+ For input-key markers, actual_key is required, e.g.:
71
+ "{{{VAR==STR_3, MAXLEN==80==/VAR}}}value"
72
+
73
+ For meta-title markers, actual_key may be empty, e.g.:
74
+ "{{{VAR==STR_3, MAXLEN==80==/VAR}}}"
75
+ """
76
+ if not isinstance(text, str):
77
+ return None
78
+ if not text.startswith(MARKER_PREFIX):
79
+ return None
80
+
81
+ end = text.find(MARKER_SUFFIX)
82
+ if end == -1:
83
+ return None
84
+
85
+ inner = text[len(MARKER_PREFIX):end]
86
+ actual_key = text[end + len(MARKER_SUFFIX):]
87
+
88
+ if not actual_key and not allow_empty_actual_key:
89
+ raise ValueError("Marker key missing actual input name after marker (e.g. ...}}}seed).")
90
+
91
+ parts = [p.strip() for p in inner.split(",") if p.strip()]
92
+ if not parts:
93
+ raise ValueError("Empty VAR marker.")
94
+
95
+ var_id = parts[0].strip().upper()
96
+ m = VAR_ID_RE.match(var_id)
97
+ if not m:
98
+ raise ValueError(f"Invalid VAR id '{var_id}'. Use INT_1 / STR_2 / FLOAT_3 ...")
99
+
100
+ var_type = m.group(1).upper()
101
+
102
+ constraints: Dict[str, str] = {}
103
+ for p in parts[1:]:
104
+ if "==" not in p:
105
+ raise ValueError(f"Invalid constraint '{p}' in marker for {var_id}. Use KEY==VALUE.")
106
+ k, v = p.split("==", 1)
107
+ constraints[k.strip().upper()] = v.strip()
108
+
109
+ min_val = max_val = None
110
+ max_len = None
111
+
112
+ if var_type in ("INT", "FLOAT"):
113
+ # ints/floats require MIN and MAX
114
+ if "MIN" not in constraints or "MAX" not in constraints:
115
+ raise ValueError(f"{var_id} missing MIN==... and/or MAX==... in marker.")
116
+ try:
117
+ min_val = float(constraints["MIN"])
118
+ max_val = float(constraints["MAX"])
119
+ except Exception:
120
+ raise ValueError(f"{var_id} has non-numeric MIN/MAX in marker.")
121
+ if min_val > max_val:
122
+ raise ValueError(f"{var_id} has MIN > MAX in marker.")
123
+ else:
124
+ # STR requires max length
125
+ for k in ("MAXLEN", "MAX_CHARS", "MAXCHARS", "MAX"):
126
+ if k in constraints:
127
+ try:
128
+ max_len = int(constraints[k])
129
+ except Exception:
130
+ raise ValueError(f"{var_id} has non-integer {k} in marker.")
131
+ break
132
+ if max_len is None:
133
+ raise ValueError(f"{var_id} missing string max length (MAXLEN==... or MAX==...) in marker.")
134
+ if max_len < 0:
135
+ raise ValueError(f"{var_id} has negative max length in marker.")
136
+
137
+ return (var_id, var_type, min_val, max_val, max_len, actual_key)
138
+
139
+
140
+ def _extract_marker(key: str) -> Optional[Tuple[str, str, Optional[float], Optional[float], Optional[int], str]]:
141
+ """
142
+ Marker extraction for INPUT KEYS (requires an actual key after the marker).
143
+ """
144
+ return _extract_marker_text(key, allow_empty_actual_key=False)
145
+
146
+
147
+ def _default_target_input_key(inputs: Dict[str, Any]) -> str:
148
+ """
149
+ For meta-title markers (marker has no trailing 'actual_key'), choose which input key to override.
150
+
151
+ Rule:
152
+ 1) If "value" exists in inputs -> use it (matches your example and common ComfyUI string/int/float nodes).
153
+ 2) Else if inputs has exactly one key -> use it.
154
+ 3) Else error (ambiguous).
155
+ """
156
+ if "value" in inputs:
157
+ return "value"
158
+ if len(inputs) == 1:
159
+ return next(iter(inputs.keys()))
160
+ raise ValueError(
161
+ "Meta marker found in _meta.title but cannot infer which input to override. "
162
+ "Add the marker to the input key instead ({{{...}}}value), or ensure the node has a 'value' input "
163
+ "or only a single input."
164
+ )
165
+
166
+
167
+ def _collect_and_strip_markers(prompt: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, VarSpec]]:
168
+ """
169
+ Collect markers from:
170
+ A) input keys:
171
+ "{{{VAR==INT_1,...==/VAR}}}seed" becomes "seed" in-memory
172
+ B) _meta.title:
173
+ "_meta": {"title": "{{{VAR==STR_3,...==/VAR}}}"} (title is NOT modified)
174
+ will bind STR_3 to an inferred input key (usually "value").
175
+ """
176
+ specs: Dict[str, VarSpec] = {}
177
+
178
+ for node_id, node in prompt.items():
179
+ if not isinstance(node, dict):
180
+ continue
181
+
182
+ inputs = node.get("inputs")
183
+ if not isinstance(inputs, dict):
184
+ continue
185
+
186
+ # ---- A) input-key markers (existing behavior) ----
187
+ new_inputs: Dict[str, Any] = {}
188
+ for k, v in inputs.items():
189
+ marker = _extract_marker(k)
190
+ if marker is None:
191
+ new_inputs[k] = v
192
+ continue
193
+
194
+ var_id, var_type, min_val, max_val, max_len, actual_key = marker
195
+ if actual_key in new_inputs:
196
+ raise ValueError(f"Conflict in node {node_id}: input '{actual_key}' already exists.")
197
+
198
+ # Strip marker from the key in-memory
199
+ new_inputs[actual_key] = v
200
+
201
+ spec = specs.get(var_id)
202
+ if spec is None:
203
+ spec = VarSpec(
204
+ var_id=var_id,
205
+ var_type=var_type,
206
+ min_val=min_val,
207
+ max_val=max_val,
208
+ max_len=max_len,
209
+ default_value=v,
210
+ )
211
+ specs[var_id] = spec
212
+ else:
213
+ if (
214
+ spec.var_type != var_type
215
+ or spec.min_val != min_val
216
+ or spec.max_val != max_val
217
+ or spec.max_len != max_len
218
+ ):
219
+ raise ValueError(f"Inconsistent constraints for {var_id} across markers.")
220
+
221
+ spec.occurrences.append(VarOccurrence(node_id=str(node_id), input_key=actual_key))
222
+
223
+ node["inputs"] = new_inputs
224
+
225
+ # ---- B) meta-title markers (NEW behavior you requested) ----
226
+ meta = node.get("_meta")
227
+ if isinstance(meta, dict):
228
+ title = meta.get("title")
229
+ marker2 = _extract_marker_text(title, allow_empty_actual_key=True) if isinstance(title, str) else None
230
+ if marker2 is not None:
231
+ var_id, var_type, min_val, max_val, max_len, actual_key = marker2
232
+
233
+ # Determine which input key to override.
234
+ target_key = actual_key.strip() if isinstance(actual_key, str) else ""
235
+ if not target_key:
236
+ target_key = _default_target_input_key(node["inputs"])
237
+
238
+ if target_key not in node["inputs"]:
239
+ raise ValueError(
240
+ f"Meta marker in node {node_id} points to input '{target_key}', but that input does not exist."
241
+ )
242
+
243
+ default_val = node["inputs"].get(target_key)
244
+
245
+ spec = specs.get(var_id)
246
+ if spec is None:
247
+ spec = VarSpec(
248
+ var_id=var_id,
249
+ var_type=var_type,
250
+ min_val=min_val,
251
+ max_val=max_val,
252
+ max_len=max_len,
253
+ default_value=default_val,
254
+ )
255
+ specs[var_id] = spec
256
+ else:
257
+ if (
258
+ spec.var_type != var_type
259
+ or spec.min_val != min_val
260
+ or spec.max_val != max_val
261
+ or spec.max_len != max_len
262
+ ):
263
+ raise ValueError(f"Inconsistent constraints for {var_id} across markers.")
264
+
265
+ # Avoid duplicate occurrences (optional but clean)
266
+ already = any(o.node_id == str(node_id) and o.input_key == target_key for o in spec.occurrences)
267
+ if not already:
268
+ spec.occurrences.append(VarOccurrence(node_id=str(node_id), input_key=target_key))
269
+
270
+ return prompt, specs
271
+
272
+
273
+ # --- Legacy command parsing helpers (kept in case you reuse them later) ---
274
+
275
+ def _split_command_entries(command: str) -> List[str]:
276
+ s = (command or "").strip()
277
+ if not s:
278
+ return []
279
+
280
+ entries: List[str] = []
281
+ buf: List[str] = []
282
+ quote: Optional[str] = None
283
+ escape = False
284
+
285
+ for ch in s:
286
+ if escape:
287
+ buf.append(ch)
288
+ escape = False
289
+ continue
290
+
291
+ if ch == "\\":
292
+ buf.append(ch)
293
+ escape = True
294
+ continue
295
+
296
+ if quote is not None:
297
+ buf.append(ch)
298
+ if ch == quote:
299
+ quote = None
300
+ continue
301
+
302
+ if ch in ("'", '"'):
303
+ buf.append(ch)
304
+ quote = ch
305
+ continue
306
+
307
+ if ch in (" ", "\t", "\n", "\r", ",", ";"):
308
+ token = "".join(buf).strip()
309
+ if token:
310
+ entries.append(token)
311
+ buf = []
312
+ continue
313
+
314
+ buf.append(ch)
315
+
316
+ token = "".join(buf).strip()
317
+ if token:
318
+ entries.append(token)
319
+
320
+ return entries
321
+
322
+
323
+ def _parse_command(command: str) -> Dict[str, Any]:
324
+ out: Dict[str, Any] = {}
325
+
326
+ for entry in _split_command_entries(command):
327
+ if "==" not in entry:
328
+ raise ValueError(f"Bad command entry '{entry}'. Expected VAR==VALUE.")
329
+
330
+ var, raw = entry.split("==", 1)
331
+ var = var.strip().upper()
332
+ raw = raw.strip()
333
+
334
+ if not VAR_ID_RE.match(var):
335
+ raise ValueError(f"Bad variable name '{var}'. Use INT_1 / STR_2 / FLOAT_3 ...")
336
+
337
+ if raw == "":
338
+ raise ValueError(f"Missing value for {var}.")
339
+
340
+ if raw[0] in ("'", '"'):
341
+ if len(raw) < 2 or raw[-1] != raw[0]:
342
+ raise ValueError(f"Unterminated quoted string for {var}.")
343
+ try:
344
+ val = ast.literal_eval(raw)
345
+ except Exception as e:
346
+ raise ValueError(f"Invalid quoted string for {var}: {e}")
347
+ else:
348
+ val = raw
349
+
350
+ if var in out:
351
+ raise ValueError(f"Duplicate assignment for {var}.")
352
+ out[var] = val
353
+
354
+ return out
355
+
356
+
357
+ def _convert_and_validate(var_id: str, spec: VarSpec, raw_val: Any) -> Any:
358
+ if spec.var_type == "INT":
359
+ try:
360
+ val = int(str(raw_val).strip())
361
+ except Exception:
362
+ raise ValueError(f"{var_id} must be an integer.")
363
+ if spec.min_val is not None and val < spec.min_val:
364
+ raise ValueError(f"{var_id} out of range: {val} < MIN {int(spec.min_val)}")
365
+ if spec.max_val is not None and val > spec.max_val:
366
+ raise ValueError(f"{var_id} out of range: {val} > MAX {int(spec.max_val)}")
367
+ return val
368
+
369
+ if spec.var_type == "FLOAT":
370
+ try:
371
+ val = float(str(raw_val).strip())
372
+ except Exception:
373
+ raise ValueError(f"{var_id} must be a float.")
374
+ if spec.min_val is not None and val < spec.min_val:
375
+ raise ValueError(f"{var_id} out of range: {val} < MIN {spec.min_val}")
376
+ if spec.max_val is not None and val > spec.max_val:
377
+ raise ValueError(f"{var_id} out of range: {val} > MAX {spec.max_val}")
378
+ return val
379
+
380
+ if spec.var_type == "STR":
381
+ val = str(raw_val)
382
+ if spec.max_len is not None and len(val) > spec.max_len:
383
+ raise ValueError(f"{var_id} too long: length {len(val)} > MAXLEN {spec.max_len}")
384
+ return val
385
+
386
+ raise ValueError(f"Unsupported var type for {var_id}: {spec.var_type}")
387
+
388
+
389
+ def _apply_assignments(prompt: Dict[str, Any], specs: Dict[str, VarSpec], assigns: Dict[str, Any]) -> None:
390
+ for var in assigns:
391
+ if var not in specs:
392
+ raise ValueError(f"Command references {var} but no matching marker exists in the workflow JSON.")
393
+
394
+ for var_id, spec in specs.items():
395
+ raw_val = assigns.get(var_id, spec.default_value)
396
+ val = _convert_and_validate(var_id, spec, raw_val)
397
+
398
+ for occ in spec.occurrences:
399
+ prompt[occ.node_id]["inputs"][occ.input_key] = val
400
+
401
+
402
+ def _infer_outputs_to_execute(prompt: Dict[str, Any]) -> List[str]:
403
+ outputs: List[str] = []
404
+ for node_id, node in prompt.items():
405
+ if not isinstance(node, dict) or "class_type" not in node:
406
+ continue
407
+ class_type = node["class_type"]
408
+ cls = nodes.NODE_CLASS_MAPPINGS.get(class_type)
409
+ if cls is None:
410
+ raise ValueError(f"Unknown node class_type '{class_type}' (node {node_id}).")
411
+ if getattr(cls, "OUTPUT_NODE", False) is True:
412
+ outputs.append(str(node_id))
413
+ if not outputs:
414
+ raise ValueError("Loaded workflow has no OUTPUT_NODE nodes (e.g. SaveImage/Preview/etc).")
415
+ return outputs
416
+
417
+
418
+ def _queue_prompt(prompt: Dict[str, Any], outputs_to_execute: List[str]) -> Tuple[str, float]:
419
+ ps = PromptServer.instance
420
+ prompt_id = str(uuid.uuid4())
421
+
422
+ if hasattr(ps, "number"):
423
+ number = float(ps.number)
424
+ ps.number += 1
425
+ else:
426
+ number = float(time.time() * 1000.0)
427
+
428
+ extra_data: Dict[str, Any] = {}
429
+ extra_data["create_time"] = int(time.time() * 1000)
430
+
431
+ sensitive: Dict[str, Any] = {}
432
+
433
+ ps.prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive))
434
+ return prompt_id, number
435
+
436
+
437
+ def _float_is_no_input(v: Any) -> bool:
438
+ """
439
+ Special float sentinel rule:
440
+ If v is in [-2.1, -1.9], treat it as "not provided".
441
+ """
442
+ try:
443
+ f = float(v)
444
+ except Exception:
445
+ return False
446
+ return -2.1 <= f <= -1.9
447
+
448
+
449
+ class JSONRUNNER_X:
450
+ """
451
+ Loads the workflow JSON from a fixed path,
452
+ strips {{{VAR==...==/VAR}}} markers from INPUT KEYS,
453
+ ALSO supports marker stored in _meta.title (your requested behavior),
454
+ applies overrides from separate typed inputs,
455
+ and queues it like normal /prompt.
456
+
457
+ Sentinel rules:
458
+ STR_* == "" -> ignore (act like not provided)
459
+ INT_* == -1 -> ignore
460
+ FLOAT_* in [-2.1, -1.9] -> ignore
461
+ """
462
+
463
+ OUTPUT_NODE = True
464
+ CATEGORY = "utils/workflow"
465
+
466
+ RETURN_TYPES = ("STRING",)
467
+ RETURN_NAMES = ("status",)
468
+ FUNCTION = "run"
469
+
470
+ @classmethod
471
+ def INPUT_TYPES(cls):
472
+ # Note on INT max:
473
+ # Use JS safe integer max (2^53-1) to avoid frontend precision issues.
474
+ JS_SAFE_INT_MAX = 9007199254740991
475
+
476
+ return {
477
+ "required": {
478
+ # --- STR_1 .. STR_7 (default "" => ignored) ---
479
+ "STR_1": ("STRING", {"multiline": False, "default": ""}),
480
+ "STR_2": ("STRING", {"multiline": False, "default": ""}),
481
+ "STR_3": ("STRING", {"multiline": False, "default": ""}),
482
+ "STR_4": ("STRING", {"multiline": False, "default": ""}),
483
+ "STR_5": ("STRING", {"multiline": False, "default": ""}),
484
+ "STR_6": ("STRING", {"multiline": False, "default": ""}),
485
+ "STR_7": ("STRING", {"multiline": False, "default": ""}),
486
+
487
+ # --- INT_1 .. INT_5 (default -1 => ignored) ---
488
+ "INT_1": ("INT", {"default": -1, "min": -1, "max": JS_SAFE_INT_MAX}),
489
+ "INT_2": ("INT", {"default": -1, "min": -1, "max": JS_SAFE_INT_MAX}),
490
+ "INT_3": ("INT", {"default": -1, "min": -1, "max": JS_SAFE_INT_MAX}),
491
+ "INT_4": ("INT", {"default": -1, "min": -1, "max": JS_SAFE_INT_MAX}),
492
+ "INT_5": ("INT", {"default": -1, "min": -1, "max": JS_SAFE_INT_MAX}),
493
+
494
+ # --- FLOAT_1 .. FLOAT_5 (default -2.0 => ignored if in [-2.1, -1.9]) ---
495
+ "FLOAT_1": ("FLOAT", {"default": -2.0, "min": -1.0e9, "max": 1.0e9, "step": 0.01}),
496
+ "FLOAT_2": ("FLOAT", {"default": -2.0, "min": -1.0e9, "max": 1.0e9, "step": 0.01}),
497
+ "FLOAT_3": ("FLOAT", {"default": -2.0, "min": -1.0e9, "max": 1.0e9, "step": 0.01}),
498
+ "FLOAT_4": ("FLOAT", {"default": -2.0, "min": -1.0e9, "max": 1.0e9, "step": 0.01}),
499
+ "FLOAT_5": ("FLOAT", {"default": -2.0, "min": -1.0e9, "max": 1.0e9, "step": 0.01}),
500
+ }
501
+ }
502
+
503
+ @classmethod
504
+ def IS_CHANGED(cls, *args, **kwargs):
505
+ # Force re-run each time.
506
+ return uuid.uuid4().hex
507
+
508
+ def run(
509
+ self,
510
+ STR_1: str = "",
511
+ STR_2: str = "",
512
+ STR_3: str = "",
513
+ STR_4: str = "",
514
+ STR_5: str = "",
515
+ STR_6: str = "",
516
+ STR_7: str = "",
517
+ INT_1: int = -1,
518
+ INT_2: int = -1,
519
+ INT_3: int = -1,
520
+ INT_4: int = -1,
521
+ INT_5: int = -1,
522
+ FLOAT_1: float = -2.0,
523
+ FLOAT_2: float = -2.0,
524
+ FLOAT_3: float = -2.0,
525
+ FLOAT_4: float = -2.0,
526
+ FLOAT_5: float = -2.0,
527
+ ):
528
+ try:
529
+ # 1) Load fixed prompt
530
+ prompt = _load_prompt_json_fixed()
531
+
532
+ # 2) Strip marker keys -> real keys + collect specs
533
+ # ALSO collects meta-title markers and binds them to a target input key.
534
+ prompt, specs = _collect_and_strip_markers(prompt)
535
+
536
+ # 3) Build assignments dict from separate inputs (skip sentinel values)
537
+ assigns: Dict[str, Any] = {}
538
+
539
+ # Strings: skip exact ""
540
+ str_vals = [STR_1, STR_2, STR_3, STR_4, STR_5, STR_6, STR_7]
541
+ for i, v in enumerate(str_vals, start=1):
542
+ if isinstance(v, str) and v == "":
543
+ continue
544
+ assigns[f"STR_{i}"] = v
545
+
546
+ # Ints: skip -1
547
+ int_vals = [INT_1, INT_2, INT_3, INT_4, INT_5]
548
+ for i, v in enumerate(int_vals, start=1):
549
+ if v == -1:
550
+ continue
551
+ assigns[f"INT_{i}"] = v
552
+
553
+ # Floats: skip sentinel range [-2.1, -1.9]
554
+ float_vals = [FLOAT_1, FLOAT_2, FLOAT_3, FLOAT_4, FLOAT_5]
555
+ for i, v in enumerate(float_vals, start=1):
556
+ if _float_is_no_input(v):
557
+ continue
558
+ assigns[f"FLOAT_{i}"] = v
559
+
560
+ # 4) Apply + validate constraints
561
+ _apply_assignments(prompt, specs, assigns)
562
+
563
+ # 5) Determine outputs like ComfyUI does (OUTPUT_NODE nodes)
564
+ outputs_to_execute = _infer_outputs_to_execute(prompt)
565
+
566
+ # 6) Queue prompt
567
+ prompt_id, number = _queue_prompt(prompt, outputs_to_execute)
568
+
569
+ return (f"Queued workflow as prompt_id={prompt_id} (number={number})",)
570
+
571
+ except Exception as e:
572
+ # Do not crash the graph; output the error as the node's status string.
573
+ return (f"ERROR: {e}",)
574
+
575
+
576
+ NODE_CLASS_MAPPINGS = {
577
+ "JSONRUNNER_X": JSONRUNNER_X,
578
+ }
579
+
580
+ NODE_DISPLAY_NAME_MAPPINGS = {
581
+ "JSONRUNNER_X": "JSONRUNNER_X",
582
+ }