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:
@@ -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."
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user