diff --git a/movie_editor/backend/nodes.py b/movie_editor/backend/nodes.py index 7d72a714..dc9dd5a1 100644 --- a/movie_editor/backend/nodes.py +++ b/movie_editor/backend/nodes.py @@ -20,6 +20,10 @@ LINK_TYPES = { "GUIDER", "NOISE", "UPSCALE_MODEL", "PHOTOMAKER", "WEBCAM", } WIDGET_PRIMITIVES = {"INT", "FLOAT", "STRING", "BOOLEAN"} +# "COMBO" is the V1/new-style dropdown widget type; it is NOT a graph connection even +# though it is a string. Old-style combos (spec[0] is a list) are already excluded by +# the isinstance(t, str) guard, but new-style ones need an explicit check. +_WIDGET_TYPES = WIDGET_PRIMITIVES | {"COMBO"} # ComfyUI V3 dynamic match types that are semantically IMAGE-compatible. _MATCHTYPE_ALIASES: dict[str, str] = {} # populated on first lookup — patterns are prefix-matched @@ -79,7 +83,7 @@ def _all_input_types(node_def: dict) -> list[str]: for group in ("required", "optional"): for spec in (inp.get(group) or {}).values(): t = spec[0] if isinstance(spec, list) and spec else None - if isinstance(t, str): + if isinstance(t, str) and t not in _WIDGET_TYPES: types.append(_normalize_type(t)) return types @@ -109,7 +113,7 @@ 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_PRIMITIVES: + if not isinstance(t, str) or t in _WIDGET_TYPES: continue opts = spec[1] if len(spec) > 1 and isinstance(spec[1], dict) else {} # V3 nodes can mark a required-group input as optional via the flag. diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index a27da420..5a071605 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -269,7 +269,27 @@ const cur = slot.input_sources[ci.name] || ""; srcs.forEach((s) => { const o = el("option", null, s.label); o.value = s.value; if (s.value === cur) o.selected = true; sel.append(o); }); if (cur && !srcs.some((s) => s.value === cur)) { const o = el("option", null, cur + " (missing)"); o.value = cur; o.selected = true; sel.append(o); } - sel.onchange = async () => { slot.input_sources[ci.name] = sel.value; await persist(); }; + sel.onchange = async () => { + slot.input_sources[ci.name] = sel.value; + // Auto-wire: if the user just assigned a non-auto source and this slot has + // exactly one output of the same type that is not yet wired, and there is + // exactly one pipeline destination for that type, wire it automatically. + if (sel.value && sel.value !== "") { + const matchOuts = (cand.outputs || []).filter((o) => o.type === ci.type); + if (matchOuts.length === 1) { + const outName = matchOuts[0].name; + const existing = wireTargets(slot.wires?.[outName]).filter(Boolean); + if (!existing.length) { + const dests = destinations(slot, ci.type).filter((d) => d.value); + if (dests.length === 1) { + slot.wires = slot.wires || {}; + slot.wires[outName] = [dests[0].value]; + } + } + } + } + await persist(); + }; row.append(sel); sbox.append(row); });