diff --git a/movie_editor/backend/builder.py b/movie_editor/backend/builder.py index 256c498c..44676c34 100644 --- a/movie_editor/backend/builder.py +++ b/movie_editor/backend/builder.py @@ -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(): diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index 5a071605..a27da420 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -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); }); diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index 64d3850e..a835ca35 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -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})` } }); } diff --git a/movie_editor/server.py b/movie_editor/server.py index 07ce6ea2..30094f2f 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -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)