Movie Editor: flag slots with unfed required inputs; only for active slots; honest 'ready'

- 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 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-07 13:16:46 +03:00
co-authored by Claude Opus 4.8
parent 3ae218c5bf
commit 3eef6de2b8
3 changed files with 47 additions and 4 deletions
+15 -2
View File
@@ -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)))
+29 -1
View File
@@ -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);
}
+3 -1
View File
@@ -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; }