Movie Editor: force split_by_transitions; fix COMBO; remove auto-wire; surface errors

builder.py: force split_by_transitions=True on FunPackStudio — this was the
root cause of single-scene generation. Studio defaults to False (single-scene
mode); without this flag it ignores the multi-scene prompt structure entirely
and outputs only one conditioning regardless of how many scenes were split.
The flag is now forced and blocked from being overridden via studio_inputs.

nodes.py: add _WIDGET_TYPES = WIDGET_PRIMITIVES | {"COMBO"} and use it in
connection_inputs() and _all_input_types(). "COMBO" (V1 new-style dropdown)
was previously passing the WIDGET_PRIMITIVES exclusion check and appearing as
a graph-connection socket in the UI.

models.js: revert auto-wire-output-on-input-source logic (previous commit).
The heuristic was too aggressive and would pick wrong destinations when
multiple IMAGE ports exist.

server.py + store.js: surface ComfyUI execution errors. Previously a node
failure inside ComfyUI returned state="completed" with no media and the UI
showed "Completed (no media found)." — unhelpful. The status endpoint now
reads history[id].status.messages for execution_error entries and returns
state="error" + error string. The frontend poll shows the node type and
exception message so the user knows what actually failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-07 00:48:55 +03:00
co-authored by Claude Sonnet 4.6
parent 3d0b86e3be
commit fbf7ac5ac0
4 changed files with 29 additions and 26 deletions
+7
View File
@@ -223,9 +223,16 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
# the result must be written to the output dir so the editor can fetch it back
# (the reference graph used preview-only save_output=False).
graph["vhs"]["inputs"]["save_output"] = True
# split_by_transitions MUST be True — Studio defaults to False (single-scene mode)
# but the Movie Editor always builds multi-scene combined prompts. Force it here
# BEFORE the user's studio_inputs override block so it cannot be accidentally
# overwritten to False via the sampler panel.
graph["studio"]["inputs"]["split_by_transitions"] = True
# 2b. project-level widget overrides for the built-in FunPack nodes.
for k, v in (params.get("studio_inputs") or {}).items():
if k == "split_by_transitions":
continue # never allow overriding — multi-scene mode is mandatory
if k not in graph["studio"]["inputs"] or not isinstance(graph["studio"]["inputs"][k], list):
graph["studio"]["inputs"][k] = v
for k, v in (params.get("sampler_inputs") or {}).items():
+1 -21
View File
@@ -269,27 +269,7 @@
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;
// 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();
};
sel.onchange = async () => { slot.input_sources[ci.name] = sel.value; await persist(); };
row.append(sel);
sbox.append(row);
});
+6 -2
View File
@@ -286,9 +286,13 @@
pollTimer = setInterval(async () => {
try {
const s = await API.status(state.project.id, promptId);
if (s.state === "completed") {
if (s.state === "error") {
clearInterval(pollTimer);
set({ gen: { state: "done", promptId, media: s.media, msg: s.media.length ? "" : "Completed (no media found)." } });
const msg = s.error ? `ComfyUI error: ${s.error}` : "Generation failed inside ComfyUI — check the ComfyUI terminal for details.";
set({ gen: { state: "error", promptId, media: [], msg } });
} else if (s.state === "completed") {
clearInterval(pollTimer);
set({ gen: { state: "done", promptId, media: s.media, msg: s.media.length ? "" : "Completed but no output media found — check ComfyUI terminal." } });
} else {
set({ gen: { ...state.gen, state: s.state, msg: `Generating… (${s.state})` } });
}
+15 -3
View File
@@ -398,10 +398,22 @@ if web is not None and PromptServer is not None:
raise web.HTTPBadGateway(reason=f"history unavailable: {e}")
if prompt_id in hist:
entry = hist[prompt_id]
raw_status = entry.get("status") or {}
media = _media_from_history(entry)
# Extract ComfyUI execution errors from history so the frontend can show them.
exec_error: Optional[str] = None
for _kind, payload in raw_status.get("messages") or []:
if _kind == "execution_error" and isinstance(payload, dict):
node_type = payload.get("node_type", "unknown node")
exc = payload.get("exception_message") or payload.get("traceback") or "unknown error"
exec_error = f"{node_type}: {exc}"
break
is_error = raw_status.get("status_str") == "error"
return web.json_response({
"state": "completed",
"media": _media_from_history(entry),
"status": entry.get("status", {}),
"state": "error" if (is_error and not media) else "completed",
"media": media,
"error": exec_error,
"status": raw_status,
})
try:
running = await bridge.is_running(prompt_id)