Movie Editor: coerce stale LoRA/combo slot values to live choices

A renamed or removed file (e.g. an old distilled LoRA) left a slot's saved
combo value out of sync with object_info's current choices. The Models menu
select then showed the live first choice (browser default for "no matching
option") while the saved config still held the stale string, which the
builder passed straight through to ComfyUI and failed validation.

Coerce in both places: the builder falls back unknown combo values to the
node's current default/first choice, and the Models menu select persists
what it actually displays instead of leaving the stale value on disk.
This commit is contained in:
digital-garbage
2026-06-16 00:23:31 +03:00
parent cddb928608
commit cb25c14e8d
3 changed files with 49 additions and 3 deletions
+35 -3
View File
@@ -24,7 +24,7 @@ from typing import Any, Optional
from . import config
from . import pipeline_wiring
from .nodes import WIDGET_PRIMITIVES, connection_inputs, node_outputs, _combo_default
from .nodes import WIDGET_PRIMITIVES, connection_inputs, node_outputs, _combo_default, _combo_choices
# ── fixed core ────────────────────────────────────────────────────────────────
# logical id -> class_type
@@ -172,6 +172,32 @@ def _widget_defaults(node_def: Optional[dict]) -> dict:
return out
def _widget_choices(node_def: Optional[dict]) -> dict:
"""{name: [choices]} for combo widgets, per the LIVE object_info (e.g. installed
LoRA files). Used to coerce a saved slot value that no longer exists (renamed/
removed file) back to a current choice instead of being sent to ComfyUI as-is."""
out = {}
if not node_def:
return out
inp = node_def.get("input") or {}
for group in ("required", "optional"):
for name, spec in (inp.get(group) or {}).items():
if not isinstance(spec, (list, tuple)) or not spec:
continue
t = spec[0]
opts = spec[1] if len(spec) > 1 and isinstance(spec[1], dict) else {}
if opts.get("forceInput"):
continue
if isinstance(t, list):
if t:
out[name] = t
elif isinstance(t, str) and "COMBO" in t.upper():
choices = _combo_choices(opts)
if choices:
out[name] = choices
return out
# ── reference workflow ────────────────────────────────────────────────────────
def load_reference() -> dict:
@@ -311,12 +337,18 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
slot_def[s["id"]] = nd
inputs = _widget_defaults(nd)
defaults = dict(inputs)
choices = _widget_choices(nd)
inputs.update(s.get("inputs") or {})
# Coerce stale/invalid combo values (e.g. a saved "[object Object]" or a dict from
# an earlier bug) back to the node's default key.
# Coerce stale/invalid combo values back to the node's current default key:
# a saved "[object Object]" or dict from an earlier bug, or a value (e.g. a
# LoRA filename) that was renamed/removed and no longer appears in the live
# choices list — the Models menu shows the live choices, so the saved value
# should fall back to match what the selector actually displays.
for wname, wval in list(inputs.items()):
if isinstance(wval, dict) or wval == "[object Object]":
inputs[wname] = defaults.get(wname, "")
elif wname in choices and wval not in choices[wname]:
inputs[wname] = defaults.get(wname, choices[wname][0])
graph[sid] = {"class_type": cls, "inputs": inputs}
if cls not in object_info:
msg = f"Slot node '{cls}' is not installed in ComfyUI."
+4
View File
@@ -240,6 +240,10 @@
ctrl = el("select");
(spec.choices || []).forEach((c) => { const o = el("option", null, String(c)); o.value = c; if (c === value) o.selected = true; ctrl.append(o); });
if (!spec.choices || !spec.choices.length) { ctrl.append(el("option", null, "(none installed)")); ctrl.disabled = true; }
// A saved value that no longer exists (renamed/removed file) leaves nothing
// selected — the browser then shows the first option without persisting it,
// so the stale value silently goes to generation. Persist what's displayed.
else if (value != null && !spec.choices.includes(value)) onChange(spec.choices[0]);
ctrl.onchange = () => onChange(ctrl.value);
} else if (spec.kind === "boolean") {
ctrl = el("input"); ctrl.type = "checkbox"; ctrl.checked = !!value; ctrl.style.width = "auto";
+10
View File
@@ -115,6 +115,16 @@ def test_explicit_wires_and_autowire():
assert report["ambiguous"] == []
def test_stale_combo_value_coerced_to_live_choice():
# "old.safetensors" was renamed/removed; the live spec only has "m.safetensors".
models = {"full_control": True, "slots": [
{"id": "u", "node_class": "UnetLoader", "inputs": {"unet_name": "old.safetensors"},
"wires": {"MODEL": "port:FunPackStudio.model"}},
]}
graph, _ = builder.build(OI, models, PARAMS)
assert graph["slot_u"]["inputs"]["unet_name"] == "m.safetensors"
def test_linked_inputs_drive_multiple_node_values():
models = {
"slots": [