From 3d0b86e3be758ddda5a6177d2febe2f6e9cce5d8 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:39:25 +0300 Subject: [PATCH] Movie Editor: fix COMBO inputs mistaken as connections; auto-wire output on input source set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nodes.py: "COMBO" (V1/new-style dropdown widget) is a string so it passed the WIDGET_PRIMITIVES exclusion check and was incorrectly returned as a graph-connection socket in connection_inputs() and _all_input_types(). Added _WIDGET_TYPES = WIDGET_PRIMITIVES | {"COMBO"} and use it in both functions. Old-style list combos (spec[0] is a list) were already excluded correctly. models.js: when the user assigns an input source for a connection input of type T, auto-wire the first unwired output of the same type T to the sole pipeline destination of that type. Applies only when: (1) the selected source is non-empty, (2) the slot has exactly one output of that type, (3) that output has no wire set yet, and (4) there is exactly one destination available. This covers the common image-processor case: set image input → IMAGE output auto-wired to studio.source_image. Co-Authored-By: Claude Sonnet 4.6 --- movie_editor/backend/nodes.py | 8 ++++++-- movie_editor/frontend/models.js | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) 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); });