From dc21ea4235adb8719bd6d6350dc40231a8a49637 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 13 Jun 2026 02:40:31 +0300 Subject: [PATCH] Revert "Fix stale generate input and live preview visibility." This reverts commit 1a259bd56a941aedf722fa03dfaaa833f92adb08. --- movie_editor/backend/builder.py | 13 ++- movie_editor/backend/live_preview.py | 35 ++------ movie_editor/frontend/inspector.js | 3 +- movie_editor/frontend/player.js | 54 ++++--------- movie_editor/frontend/store.js | 117 ++++++++------------------- movie_editor/frontend/styles.css | 2 +- movie_editor/server.py | 36 ++++----- samplers.py | 9 +-- 8 files changed, 87 insertions(+), 182 deletions(-) diff --git a/movie_editor/backend/builder.py b/movie_editor/backend/builder.py index 3c21a1ef..7050ce90 100644 --- a/movie_editor/backend/builder.py +++ b/movie_editor/backend/builder.py @@ -23,7 +23,6 @@ import json from typing import Any, Optional from . import config -from . import live_preview from . import pipeline_wiring from .nodes import WIDGET_PRIMITIVES, connection_inputs, node_outputs, _combo_default @@ -203,20 +202,26 @@ def _wire_live_preview_vae( slot_def: dict, report: dict, ) -> None: - """Wire preview VAE slot -> Chain Sampler preview_vae when configured.""" + """Wire models.live_preview slot VAE output -> Chain Sampler preview_vae.""" + lp = (models_config or {}).get("live_preview") or {} + if not lp.get("enabled"): + return if "sampler" not in graph: return existing = graph["sampler"].get("inputs", {}).get("preview_vae") if isinstance(existing, list): return - sid, out_name = live_preview.preview_vae_wire(models_config) + sid = lp.get("slot_id") if not sid: + report["unsatisfied"].append( + "Live preview is enabled but no preview VAE is selected (View → Choose preview VAE…)." + ) return nid = slot_node_id.get(sid) if not nid or nid not in graph: report["unsatisfied"].append(f"Live preview VAE slot '{sid}' is not in the graph.") return - out_name = str(out_name or "VAE") + out_name = str(lp.get("vae_output") or "VAE") nd = slot_def.get(sid) or {} outs = node_outputs(nd) oidx = _output_index(nd, out_name) diff --git a/movie_editor/backend/live_preview.py b/movie_editor/backend/live_preview.py index 7712f0f3..2811ac5c 100644 --- a/movie_editor/backend/live_preview.py +++ b/movie_editor/backend/live_preview.py @@ -95,12 +95,6 @@ def preview_vae_wire(models: Any) -> tuple[Optional[str], Optional[str]]: for out_name, raw in (slot.get("wires") or {}).items(): if PREVIEW_VAE_WIRE in _wire_targets(raw): return str(sid), str(out_name or "VAE") - co = (models.get("core_overrides") or {}).get("sampler") or {} - src = str(co.get("preview_vae") or "") - if src.startswith("out:"): - parts = src.split(":", 2) - if len(parts) >= 3 and parts[1]: - return str(parts[1]), str(parts[2] or "VAE") lp = models.get("live_preview") or {} sid = lp.get("slot_id") if sid: @@ -109,27 +103,10 @@ def preview_vae_wire(models: Any) -> tuple[Optional[str], Optional[str]]: def models_enabled(models: Any) -> bool: + if not isinstance(models, dict): + return False sid, _ = preview_vae_wire(models) - return bool(sid) - - -def api_payload(ui_prefix: str = "/funpack/movie") -> dict[str, Any]: - """Status dict for /api/live-preview and /api/progress.""" - import os - - snap = snapshot() - if not snap.get("active"): - return {"active": False} - frame_path = snap.get("frame_path") or "" - ready = bool(frame_path and os.path.isfile(frame_path)) - payload: dict[str, Any] = { - "active": True, - "ready": ready, - "scene_index": snap.get("scene_index", -1), - "scene_count": snap.get("scene_count", 0), - "updated_at": snap.get("updated_at", 0), - } - if ready: - ts = int(float(snap.get("updated_at") or 0) * 1000) - payload["url"] = f"{ui_prefix}/api/live-preview/frame?t={ts}" - return payload + if sid: + return True + lp = models.get("live_preview") or {} + return bool(lp.get("enabled") and lp.get("slot_id")) diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index 0307c2ad..b22d9efe 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -920,7 +920,8 @@ if (_editing) return; const k = t.dataset?.k; if (k === "global-prompt") { - S.flushPendingEdits().then(() => render(S.get())); + S.flushSave(); + render(S.get()); return; } if (!S.flushSave()) render(S.get()); diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index 80e4fdcd..659a9b40 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -877,36 +877,7 @@ let _tcEl = null; // timecode span (updated live, not rebuilt) let _playBtnEl = null; // play/pause button (glyph updated live, not rebuilt) let _genMsgEl = null, _genBarEl = null, _genFillEl = null; // gen readout, updated live - let _livePreviewEl = null, _livePreviewBadgeEl = null, _livePreviewLayerEl = null; - - function _livePreviewConfigured(st) { - const m = st?.models || {}; - const lp = m.live_preview || {}; - if (lp.enabled || lp.slot_id) return true; - if ((m.core_overrides?.sampler?.preview_vae || "").startsWith("out:")) return true; - const port = "port:FunPackLTXAVSceneChainSampler.preview_vae"; - return (m.slots || []).some((s) => - Object.values(s.wires || {}).some((raw) => { - const arr = Array.isArray(raw) ? raw : raw ? [raw] : []; - return arr.includes(port); - })); - } - - function _ensureLivePreviewDom() { - if (_livePreviewLayerEl && _livePreviewEl) return; - const canvas = document.querySelector(".pm-canvas"); - if (!canvas) return; - const lpLayer = el("div", "pm-live-preview"); - _livePreviewEl = el("img", "pm-live-preview-img"); - _livePreviewEl.alt = "Live preview"; - _livePreviewEl.draggable = false; - _livePreviewEl.style.display = "none"; - _livePreviewBadgeEl = el("div", "pm-live-preview-badge"); - _livePreviewBadgeEl.style.display = "none"; - lpLayer.append(_livePreviewEl, _livePreviewBadgeEl); - canvas.append(lpLayer); - _livePreviewLayerEl = lpLayer; - } + let _livePreviewEl = null, _livePreviewBadgeEl = null; // Update the generation readout in place on progress ticks (no store re-render, so the // editor stays interactive — frames can be saved, effects added — while a job runs). @@ -919,11 +890,9 @@ _genFillEl.style.width = Math.min(100, Math.round((g.step / g.maxStep) * 100)) + "%"; } } - if (g.livePreviewUrl) { - _ensureLivePreviewDom(); - if (_livePreviewLayerEl) _livePreviewLayerEl.style.display = ""; - if (_livePreviewEl && _livePreviewEl.src !== g.livePreviewUrl) _livePreviewEl.src = g.livePreviewUrl; - if (_livePreviewEl) _livePreviewEl.style.display = ""; + if (_livePreviewEl && g.livePreviewUrl) { + if (_livePreviewEl.src !== g.livePreviewUrl) _livePreviewEl.src = g.livePreviewUrl; + _livePreviewEl.style.display = ""; } if (_livePreviewBadgeEl && g.liveScene >= 0 && g.liveSceneCount > 0) { _livePreviewBadgeEl.textContent = `Scene ${g.liveScene + 1}/${g.liveSceneCount}`; @@ -1143,7 +1112,7 @@ // store render; live progress ticks update it in place (see the gen-progress listener), // so the rest of the editor isn't rebuilt while a generation runs. _genMsgEl = _genBarEl = _genFillEl = null; - _livePreviewEl = _livePreviewBadgeEl = _livePreviewLayerEl = null; + _livePreviewEl = _livePreviewBadgeEl = null; if (["queuing", "running", "pending", "error"].includes(gen.state)) { const busy = gen.state !== "error"; const ro = el("div", "gen-readout" + (gen.state === "error" ? " error" : "")); @@ -1162,9 +1131,17 @@ } canvas.append(ro); } - if (["queuing", "running", "pending"].includes(gen.state) && _livePreviewConfigured(st)) { + if (["queuing", "running", "pending"].includes(gen.state)) { + const lp = S.get().models?.live_preview || {}; + const lpPort = "port:FunPackLTXAVSceneChainSampler.preview_vae"; + const lpWired = (S.get().models?.slots || []).some((s) => + Object.values(s.wires || {}).some((raw) => { + const arr = Array.isArray(raw) ? raw : raw ? [raw] : []; + return arr.includes(lpPort); + })); + const showLp = lp.enabled || lp.slot_id || lpWired; + if (showLp) { const lpLayer = el("div", "pm-live-preview"); - _livePreviewLayerEl = lpLayer; _livePreviewEl = el("img", "pm-live-preview-img"); _livePreviewEl.alt = "Live preview"; _livePreviewEl.draggable = false; @@ -1179,6 +1156,7 @@ lpLayer.append(_livePreviewEl, _livePreviewBadgeEl); canvas.append(lpLayer); } + } if (previewAsset?.kind === "image") { const layer = el("div", "pm-media-preview"); const label = el("div", "pm-media-preview-label", previewAsset.name || "Media preview"); diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index 97123fb5..6bdf08a4 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -69,8 +69,7 @@ function _livePreviewConfigured() { const lp = state.models?.live_preview || {}; - if (lp.slot_id || lp.enabled) return true; - if ((state.models?.core_overrides?.sampler?.preview_vae || "").startsWith("out:")) return true; + if (lp.slot_id) return true; return (state.models?.slots || []).some((s) => Object.values(s.wires || {}).some((raw) => _modelWireTargets(raw).includes(LIVE_PREVIEW_PORT))); } @@ -253,36 +252,13 @@ } let _globalApplyTimer = null; - let _globalApplyPromise = null; - let _pendingGlobalPromptText = null; - function scheduleGlobalPromptApply(text) { - _pendingGlobalPromptText = text; - const trimmed = String(text || "").trim(); - if (trimmed && trimmed !== buildGlobalPromptFromTimeline(state.project)) { - _localDirty = true; - _notifySaveChip(); - } clearTimeout(_globalApplyTimer); _globalApplyTimer = setTimeout(() => { - _globalApplyTimer = null; applyGlobalPromptQuiet(text).catch((e) => console.warn("Global prompt apply failed:", e)); }, 500); } - async function _flushPendingGlobalPrompt() { - clearTimeout(_globalApplyTimer); - _globalApplyTimer = null; - if (_globalApplyPromise) await _globalApplyPromise; - const pending = (_pendingGlobalPromptText || "").trim(); - if (!pending) return; - if (pending === buildGlobalPromptFromTimeline(state.project) && !_pinnedGlobalPrompt) { - _pendingGlobalPromptText = null; - return; - } - await applyGlobalPromptQuiet(pending, { force: true }); - } - function _normalizeSceneText(t) { return String(t || "").trim().replace(/\s+/g, " "); } @@ -530,53 +506,30 @@ return true; } - async function applyGlobalPromptQuiet(text, opts = {}) { + async function applyGlobalPromptQuiet(text) { if (!state.project) return false; const trimmed = String(text || "").trim(); - const force = !!opts.force; - if (!trimmed) { - _pendingGlobalPromptText = null; + if (!trimmed) return false; + if (trimmed === buildGlobalPromptFromTimeline(state.project) && !_pinnedGlobalPrompt) return true; + _historyRecord(); + let res; + try { res = await API.parsePrompt(state.project.id, trimmed); } + catch (e) { + console.warn("Global prompt parse failed:", e); return false; } - if (trimmed === buildGlobalPromptFromTimeline(state.project) && !_pinnedGlobalPrompt) { - _pendingGlobalPromptText = null; - return true; - } - if (_globalApplyPromise) return _globalApplyPromise; - _globalApplyPromise = (async () => { - try { - if (!force && _pendingGlobalPromptText != null && String(_pendingGlobalPromptText).trim() !== trimmed) { - return false; - } - _historyRecord(); - let res; - try { res = await API.parsePrompt(state.project.id, trimmed); } - catch (e) { - console.warn("Global prompt parse failed:", e); - return false; - } - if (!force && _pendingGlobalPromptText != null && String(_pendingGlobalPromptText).trim() !== trimmed) { - return false; - } - if (!await _distributeGlobalPrompt(trimmed, res)) return false; - clearTimeout(saveTimer); - saveTimer = null; - _localDirty = true; - scheduleSaveSilent(); - refreshPreview(true); - return true; - } finally { - _globalApplyPromise = null; - if (_pendingGlobalPromptText === trimmed) _pendingGlobalPromptText = null; - } - })(); - return _globalApplyPromise; + if (!await _distributeGlobalPrompt(trimmed, res)) return false; + clearTimeout(saveTimer); + saveTimer = null; + _localDirty = true; + scheduleSaveSilent(); + refreshPreview(true); + return true; } function _syncPromptPreview() { return (async () => { try { - await _flushPendingGlobalPrompt(); await flushSave(); await refreshPreview(true); } catch (e) { console.error("prompt preview sync failed", e); } @@ -2428,13 +2381,8 @@ throw last; } - async function flushPendingEdits() { - await _flushPendingGlobalPrompt(); - return flushSave(); - } - async function _flushSaveForGenerate() { - await flushPendingEdits(); + await flushSave(); if (_localDirty) { throw new Error( "Could not save your latest edits before generate (tunnel may have dropped). " @@ -2776,19 +2724,23 @@ msg: `${head} · sampling ${pr.value}/${pr.max}`, }); } - const lp = pr?.live_preview; - if (lp?.ready && lp.url) { - const sceneNote = (lp.scene_index >= 0 && lp.scene_count > 0) - ? ` · scene ${lp.scene_index + 1}/${lp.scene_count}` - : ""; - updateGenProgress({ - livePreviewUrl: lp.url, - liveScene: lp.scene_index, - liveSceneCount: lp.scene_count, - msg: `${(state.gen.msg || prefix).replace(/\s*·\s*scene \d+\/\d+$/, "")}${sceneNote}`, - }); - } } catch (_) {} + if (_livePreviewConfigured()) { + try { + const lp = await API.livePreview(); + if (lp?.ready && lp.url) { + const sceneNote = (lp.scene_index >= 0 && lp.scene_count > 0) + ? ` · scene ${lp.scene_index + 1}/${lp.scene_count}` + : ""; + updateGenProgress({ + livePreviewUrl: lp.url, + liveScene: lp.scene_index, + liveSceneCount: lp.scene_count, + msg: `${(state.gen.msg || prefix).replace(/\s*·\s*scene \d+\/\d+$/, "")}${sceneNote}`, + }); + } + } catch (_) {} + } }, 700); pollTimer = setInterval(async () => { if (_interrupted) { @@ -2905,7 +2857,6 @@ 10, ); if (!r.prompt_id) { set({ gen: { ...state.gen, state: "error", msg: "No prompt id returned." } }); return false; } - if (r.live_preview_note) console.warn("[live preview]", r.live_preview_note); _queueRenderPrompts(sceneIds); if (r.validation && state.project) { state.project.generation_meta = { @@ -3727,7 +3678,7 @@ isOverlayAudioTrack, isSeparatedAudioTrack, resizeScene, setSceneGapAfter, splitScene, snapFrames, snapFramesFloor, snapFramesCeil, sceneEffFrames, sceneEffFps, setSourceTrim, trimSceneLeft, slipScene, applyEnginePreset, ENGINE_PRESETS, undo, redo, - refreshPreview, syncFromPreview, applyGlobalPromptQuiet, scheduleGlobalPromptApply, buildGlobalPromptFromTimeline, syncGlobalPromptFromTimeline, flushPendingEdits, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, saveSelectedToMediaBin, clipSaveableToMediaBin, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, setLivePreviewEnabled, setLivePreviewSlot, clearNotice, + refreshPreview, syncFromPreview, applyGlobalPromptQuiet, scheduleGlobalPromptApply, buildGlobalPromptFromTimeline, syncGlobalPromptFromTimeline, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, saveSelectedToMediaBin, clipSaveableToMediaBin, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, setLivePreviewEnabled, setLivePreviewSlot, clearNotice, setConditioningSlot, setSamplerSlot, setSamplerInput, setSamplerInputNow, unsetSamplerInput, setStudioInput, setStudioInputNow, loadMedia, uploadMedia, deleteMedia, deleteMediaMany, renameMedia, previewMedia, clearMediaPreview, assignMediaToScene, exportMediaAsset, loadShortcuts, saveShortcut, deleteShortcut, importShortcuts, diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index 11fb8a18..770e646a 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -448,7 +448,7 @@ input[type=number] { font-family: var(--font-mono); } .gen-readout .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--amber); animation: pulse 1s infinite; flex: 0 0 auto; } .gen-readout.error { color: var(--danger); } .gen-readout.error .pulse { background: var(--danger); animation: none; } -.pm-live-preview { position: absolute; inset: 12px 12px 52px 12px; display: flex; align-items: center; justify-content: center; pointer-events: none; z-index: 3; } +.pm-live-preview { position: absolute; inset: 12px 12px 52px 12px; display: flex; align-items: center; justify-content: center; pointer-events: none; z-index: 1; } .pm-live-preview-img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 6px; border: 1px solid var(--line); box-shadow: 0 8px 32px #000a; background: #0006; } .pm-live-preview-badge { position: absolute; top: 8px; left: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--amber-2); background: #000b; border: 1px solid var(--line); padding: 3px 8px; border-radius: 12px; } .gen-bar { width: 90px; height: 5px; background: #ffffff22; border-radius: 3px; overflow: hidden; flex: 0 0 auto; } diff --git a/movie_editor/server.py b/movie_editor/server.py index 17716f48..317b1796 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -1466,16 +1466,6 @@ if web is not None and PromptServer is not None: if report["blocking"]: detail = "Generation blocked — " + "; ".join(report["blocking"]) return web.json_response({"detail": detail, "report": report}, status=400) - live_preview_note = None - if live_preview.models_enabled(models_cfg): - samp_in = (graph.get("sampler") or {}).get("inputs") or {} - if not isinstance(samp_in.get("preview_vae"), list): - live_preview_note = ( - "Live preview VAE is configured but preview_vae is not wired — " - "wire a VAE loader to Chain Sampler · live preview in Models, then restart ComfyUI if you recently updated FunPack." - ) - elif not samp_in.get("funpack_live_preview"): - live_preview_note = "Live preview directory was not passed to the Chain Sampler." try: result = await bridge.queue_prompt(graph) except Exception as e: # noqa: BLE001 @@ -1493,8 +1483,6 @@ if web is not None and PromptServer is not None: "validation": validation, "prompt_repairs_cleared": prompt_changed, "reset_session": reset_session, - "live_preview": live_preview.models_enabled(models_cfg), - "live_preview_note": live_preview_note, }) @routes.get(UI_PREFIX + "/api/projects/{pid}/status/{prompt_id}") @@ -1544,15 +1532,27 @@ if web is not None and PromptServer is not None: @routes.get(UI_PREFIX + "/api/progress") async def _progress(_req): - payload = bridge.current_progress() - lp = live_preview.api_payload(UI_PREFIX) - if lp.get("active"): - payload["live_preview"] = lp - return web.json_response(payload) + return web.json_response(bridge.current_progress()) @routes.get(UI_PREFIX + "/api/live-preview") async def _live_preview_status(_req): - return web.json_response(live_preview.api_payload(UI_PREFIX)) + import os + snap = live_preview.snapshot() + if not snap.get("active"): + return web.json_response({"active": False}) + frame_path = snap.get("frame_path") or "" + ready = bool(frame_path and os.path.isfile(frame_path)) + payload = { + "active": True, + "ready": ready, + "scene_index": snap.get("scene_index", -1), + "scene_count": snap.get("scene_count", 0), + "updated_at": snap.get("updated_at", 0), + } + if ready: + ts = int(float(snap.get("updated_at") or 0) * 1000) + payload["url"] = f"{UI_PREFIX}/api/live-preview/frame?t={ts}" + return web.json_response(payload) @routes.get(UI_PREFIX + "/api/live-preview/frame") async def _live_preview_frame(_req): diff --git a/samplers.py b/samplers.py index 3ae41239..97fda2af 100644 --- a/samplers.py +++ b/samplers.py @@ -3705,11 +3705,7 @@ class FunPackLTXAVSceneChainSampler: return os.makedirs(preview_dir, exist_ok=True) tile = int(cfg.get("decode_tile_size") or 256) - try: - decoded = self._decode_for_preview(preview_vae, scene_latent, decode_tile_size=tile) - except Exception as e: - print(f"[FunPackSceneChain] live preview decode failed (scene {scene_index + 1}): {e}") - return + decoded = self._decode_for_preview(preview_vae, scene_latent, decode_tile_size=tile) path = os.path.join(preview_dir, f"scene_{int(scene_index):03d}.webp") current = os.path.join(preview_dir, "current.webp") if self._save_batch_preview( @@ -3723,9 +3719,6 @@ class FunPackLTXAVSceneChainSampler: except Exception: current = path _lp.publish(int(scene_index), current, scene_count) - print(f"[FunPackSceneChain] live preview scene {scene_index + 1}/{scene_count} -> {current}") - else: - print(f"[FunPackSceneChain] live preview save failed (scene {scene_index + 1})") def _save_batch_preview(self, decoded, path, max_frames=16, width=256): """Save a decoded video tensor [T,H,W,C] in 0..1 as a downscaled animated webp."""