Movie Editor: emit widget defaults, wireable STRING/forceInput sockets

- builder now emits a value for EVERY widget input (type-appropriate empty when no
  default is declared), matching ComfyUI's frontend. Fixes generation refusing to
  start on required widgets with no default (e.g. ImageTransform's 'bboxes',
  refinement key fields) — they were dropped from the graph and ComfyUI rejected the
  prompt for a missing required input. Empty required widgets are now a warning, not
  a blocking error, in the Models modal.
- connection_inputs + ports_from_input_types now include forceInput STRING sockets,
  so refinement_key_input on Studio and the Chain Sampler is visible and can be wired
  manually (was invisible because STRING is a widget type). Lets a custom STRING
  source override the default keyloader wire.
- Refinement key stays optional: the keyloader defaults to the keyless 'none' key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-07 12:56:27 +03:00
co-authored by Claude Opus 4.8
parent d4c1f7f72e
commit c0dff7893d
3 changed files with 28 additions and 7 deletions
+10 -2
View File
@@ -92,6 +92,8 @@ CORE_PRODUCERS: list[tuple[str, int, str]] = [ # (core_id, output_index, type)
]
CONTROL_VALUES = {"fixed", "randomize", "increment", "decrement"}
# Type-appropriate empty values for widgets that declare no default.
_WIDGET_EMPTY = {"STRING": "", "INT": 0, "FLOAT": 0.0, "BOOLEAN": False}
_VHS_NON_INPUT = {"videopreview"}
@@ -156,9 +158,15 @@ def _widget_defaults(node_def: Optional[dict]) -> dict:
continue
if isinstance(t, list):
out[name] = opts.get("default", t[0] if t else None)
elif t == "COMBO":
choices = opts.get("options") or []
out[name] = opts.get("default", choices[0] if choices else "")
elif t in WIDGET_PRIMITIVES:
if "default" in opts:
out[name] = opts["default"]
# Always emit a value for every widget — ComfyUI's frontend does, and a
# required widget with no declared default (e.g. ImageTransform's bboxes)
# otherwise goes missing and ComfyUI rejects the prompt. Fall back to a
# type-appropriate empty so generation isn't blocked.
out[name] = opts.get("default", _WIDGET_EMPTY.get(t, ""))
return out
+15 -4
View File
@@ -113,9 +113,14 @@ def connection_inputs(node_def: dict) -> list[dict]:
if not isinstance(spec, list) or not spec:
continue
t = spec[0]
if not isinstance(t, str) or t in _WIDGET_TYPES:
if not isinstance(t, str):
continue
opts = spec[1] if len(spec) > 1 and isinstance(spec[1], dict) else {}
# A widget-typed input is a SOCKET (wireable) only when forceInput is set —
# e.g. refinement_key_input is ("STRING", {"forceInput": True}). Plain widgets
# are handled by widget_inputs, not here.
if t in _WIDGET_TYPES and not opts.get("forceInput"):
continue
# V3 nodes can mark a required-group input as optional via the flag.
is_required = group == "required" and not opts.get("optional", False)
out.append({"name": name, "type": _normalize_type(t), "required": is_required})
@@ -254,9 +259,15 @@ def ports_from_input_types(label: str, node_key: str, input_types: dict) -> list
if not isinstance(spec, (list, tuple)) or not spec:
continue
t = spec[0]
if isinstance(t, str) and t not in WIDGET_PRIMITIVES:
ports.append({"id": f"{node_key}.{name}", "node": label, "input": name,
"type": t, "label": f"{label} · {name}"})
if not isinstance(t, str):
continue
opts = spec[1] if len(spec) > 1 and isinstance(spec[1], dict) else {}
# Expose typed sockets, plus forceInput widget sockets (e.g. the STRING
# refinement_key_input on Studio/Sampler) so they can be wired manually.
if t in WIDGET_PRIMITIVES and not opts.get("forceInput"):
continue
ports.append({"id": f"{node_key}.{name}", "node": label, "input": name,
"type": t, "label": f"{label} · {name}"})
return ports
+3 -1
View File
@@ -124,7 +124,9 @@
if (w.kind === "combo" && (!w.choices || !w.choices.length)) {
issues.push({ level: "error", msg: `"${w.name}" has no installed options to pick from.` });
} else if (v == null || v === "") {
issues.push({ level: "error", msg: `Required field "${w.name}" is empty.` });
// Not blocking: the builder emits a type-appropriate empty for any widget left
// blank (matching ComfyUI's frontend), so an empty field won't stall generation.
issues.push({ level: "warn", msg: `"${w.name}" is empty — using its default.` });
}
});
const outs = spec.outputs || [];