From 3eef6de2b8803cd9f2a5236acd51761bef05748b Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:16:46 +0300 Subject: [PATCH] Movie Editor: flag slots with unfed required inputs; only for active slots; honest 'ready' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - validateSlot now checks required connection INPUTS are actually fed (explicit input source, incoming wire, or a single auto-wire producer) — matching the builder's blocking — so a node like ImageTransform with an unwired 'image' input is flagged instead of showing 'ready'. Flagged slots get a clear red highlight (bg + ring). - 'Pipeline ready' banner now reads 'Not ready — N slot error(s) to fix' when errors. - Only ACTIVE slots are validated/blocking: a slot that feeds nothing (no output wire, not referenced by a core override) is inert and no longer blocks generation or gets highlighted — builder skips inert slots in auto-wire, frontend mirrors the rule. Co-Authored-By: Claude Opus 4.8 --- movie_editor/backend/builder.py | 17 +++++++++++++++-- movie_editor/frontend/models.js | 30 +++++++++++++++++++++++++++++- movie_editor/frontend/styles.css | 4 +++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/movie_editor/backend/builder.py b/movie_editor/backend/builder.py index db75a648..fe378bf6 100644 --- a/movie_editor/backend/builder.py +++ b/movie_editor/backend/builder.py @@ -361,7 +361,18 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No # 5. auto-wire remaining unbound typed inputs by unique producer. producers = _producers(graph, slots, slot_node_id, slot_def, object_info) - _autowire(graph, slots, slot_node_id, slot_def, object_info, producers, report) + # A slot only matters if it's wired into the pipeline (an output wire, or referenced + # as a source by a core override). Inert/unused slots are NOT validated or auto-wired, + # so an unused node's required input never blocks generation. + active_slots = set() + for s in slots: + if any(t for tg in (s.get("wires") or {}).values() for t in (tg if isinstance(tg, list) else [tg]) if t): + active_slots.add(s["id"]) + for ovs in ((models_config or {}).get("core_overrides") or {}).values(): + for src in (ovs or {}).values(): + if isinstance(src, str) and src.startswith("out:"): + active_slots.add(src.split(":", 2)[1]) + _autowire(graph, slots, slot_node_id, slot_def, object_info, producers, report, active_slots) return graph, report @@ -510,12 +521,14 @@ def _node_labels(slots, slot_node_id, object_info): return label -def _autowire(graph, slots, slot_node_id, slot_def, object_info, producers, report): +def _autowire(graph, slots, slot_node_id, slot_def, object_info, producers, report, active_slots=None): label = _node_labels(slots, slot_node_id, object_info) L = lambda nid: label.get(nid, nid) targets = list(OPEN_PORTS) # (core_id, input, type, required) for s in slots: # slot connection inputs (e.g. image-proc vae/image/length) + if active_slots is not None and s["id"] not in active_slots: + continue # inert slot (feeds nothing) — don't auto-wire or block on its inputs nd = slot_def[s["id"]] for ci in connection_inputs(nd or {}): targets.append((slot_node_id[s["id"]], ci["name"], ci["type"], ci.get("required", False))) diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index baa2339d..960daaee 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -113,6 +113,15 @@ // Roles the fixed FunPack path cannot generate without (at least one slot each). const ESSENTIAL = [["unet", "Unet / Diffusion Model"], ["clip", "CLIP / Text Encoder"], ["video_vae", "Video VAE"]]; + // A slot is "active" (part of the pipeline) if it wires an output somewhere or a core + // override sources from it. Inert slots are left alone (no required-input errors). + function slotIsActive(slot) { + if (Object.values(slot.wires || {}).some((tg) => wireTargets(tg).some(Boolean))) return true; + const ov = config.core_overrides || {}; + return Object.values(ov).some((ins) => + Object.values(ins || {}).some((src) => typeof src === "string" && src.startsWith(`out:${slot.id}:`))); + } + function validateSlot(slot) { const issues = []; const spec = specFor(slot); @@ -143,6 +152,25 @@ issues.push({ level: "error", msg: `"${o.name}" is wired to a destination that no longer exists.` }); }); }); + + // Required connection INPUTS must actually be fed (matches the builder's blocking): + // explicit input source, an incoming wire from another node, or a unique auto-wire + // producer. Only checked for ACTIVE slots (wired into the pipeline) — an unused node + // that feeds nothing must not be flagged or block generation. + if (slotIsActive(slot)) (spec.connection_inputs || []).forEach((ci) => { + if (!ci.required) return; + const src = (slot.input_sources || {})[ci.name]; + if (src && src !== "auto") return; // explicitly sourced (incl. "timeline") + const incoming = (config.slots || []).some((s2) => + s2.id !== slot.id && Object.values(s2.wires || {}).some((tg) => wireTargets(tg).includes(`node:${slot.id}:${ci.name}`))); + if (incoming) return; + const prod = sources(slot, ci.type).filter((o) => o.value && o.value !== "timeline").length; + if (prod === 1) return; // a single producer auto-wires + if (prod > 1) + issues.push({ level: "error", msg: `Input "${ci.name}" (${ci.type}): ${prod} possible sources — set its Input source.` }); + else + issues.push({ level: "error", msg: `Input "${ci.name}" (${ci.type}) has no source — set its Input source or add a node that outputs ${ci.type}.` }); + }); return issues; } @@ -559,7 +587,7 @@ if (v.warns) bits.push(`${v.warns} warning${v.warns > 1 ? "s" : ""}`); const bar = el("div", "valid-banner " + (v.errors ? "bad" : (v.warns ? "warn" : "ok"))); bar.append(el("span", "valid-dot", v.errors ? "!" : "✓")); - bar.append(el("span", null, v.errors ? `Pipeline ready but ${bits.join(", ")} in slot config.` + bar.append(el("span", null, v.errors ? `Not ready — ${bits.join(", ")} to fix.` : (v.warns ? `Pipeline ready — ${bits.join(", ")}.` : "Pipeline ready."))); sec.append(bar); } diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index 0b52389e..54e0e216 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -437,7 +437,9 @@ input[type=number] { font-family: var(--font-mono); } .slot-card { background: var(--ink-1); border: 1px solid var(--line); border-radius: 10px; padding: 12px; } .slot-card { border-left: 3px solid var(--amber); } .slot-card.slot-warn { border-left-color: var(--amber); } -.slot-card.slot-bad { border-left-color: var(--danger); } +.slot-card.slot-bad { border-color: var(--danger); border-left-color: var(--danger); + background: color-mix(in srgb, var(--danger) 9%, var(--ink-1)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--danger) 35%, transparent); } .slot-head { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; } .slot-card.open .slot-head { margin-bottom: 10px; } .slot-chev { color: var(--faint); width: 12px; flex: 0 0 auto; font-size: 11px; }