Revert preview segment cache and live preview UI changes.

Roll back segment cache busting, player refresh-on-render, and gen overlay behavior while keeping timeline snapshot verification before generate.
This commit is contained in:
digital-garbage
2026-06-13 02:05:10 +03:00
parent f4d381c44c
commit 301c978dd2
6 changed files with 44 additions and 129 deletions
-1
View File
@@ -161,7 +161,6 @@
type: m.type || "output",
render_in: String(spec.renderIn != null ? spec.renderIn : 0),
});
if (spec.cacheBust != null) q.set("v", String(spec.cacheBust));
u += "?" + q.toString();
}
return u;
+18 -47
View File
@@ -158,8 +158,7 @@
const sc = S.scene(c.sceneId);
const sourceIn = sc?.source_in || 0;
const renderIn = Math.max(0, (c.inSec || 0) - sourceIn);
const cacheBust = (st && st._previewGenToken) || Date.now();
c.streamUrl = API.previewSegmentUrl(pid, c.sceneId, { media: c.media, renderIn, cacheBust });
c.streamUrl = API.previewSegmentUrl(pid, c.sceneId, { media: c.media, renderIn });
}
}
}
@@ -878,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).
@@ -920,13 +890,9 @@
_genFillEl.style.width = Math.min(100, Math.round((g.step / g.maxStep) * 100)) + "%";
}
}
if (g.livePreviewUrl) {
_ensureLivePreviewDom();
const canvas = document.querySelector(".pm-canvas");
if (canvas && !canvas.classList.contains("gen-live-preview")) canvas.classList.add("gen-live-preview");
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}`;
@@ -1098,9 +1064,6 @@
// Rebuilding the monitor DOM during playback detaches <video> nodes and kills multi-scene
// chain preview mid-stream (often visible from scene 3 onward once store ticks catch up).
// Still mount live preview during generation so sampling frames stay visible while playing.
const genActive = ["queuing", "running", "pending"].includes(st.gen?.state);
if (genActive && _livePreviewConfigured(st)) _ensureLivePreviewDom();
if (_scrubDepth > 0 || _playing) {
_ensureStageMounted();
return;
@@ -1114,8 +1077,7 @@
const previewOverlay = previewAsset && (previewAsset.kind === "image" || previewAsset.kind === "audio" || previewAsset.kind === "video");
// ── canvas (video + placeholder) ────────────────────────────────────────
const genLive = ["queuing", "running", "pending"].includes(gen.state) && _livePreviewConfigured(st);
const canvas = el("div", "pm-canvas" + (previewOverlay ? " media-previewing" : "") + (genLive ? " gen-live-preview" : ""));
const canvas = el("div", "pm-canvas" + (previewOverlay ? " media-previewing" : ""));
_slateEl = el("div", "pm-slate");
_slateEl.style.display = "none";
_slateEl.append(el("div", "pm-slate-title"));
@@ -1150,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" : ""));
@@ -1169,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;
@@ -1186,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");
+17 -18
View File
@@ -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)));
}
@@ -2474,7 +2473,6 @@
"Timeline did not save before generate. Wait for the save indicator to clear, then try again."
);
}
state._previewGenToken = Date.now();
return localSnap;
}
@@ -2786,10 +2784,8 @@
if (recordedSceneIds.length) {
_validateRendersToken++;
state.notice = "";
state._previewGenToken = Date.now();
_syncEditorStateToProject();
scheduleSaveSilent();
notify();
}
}
@@ -2813,19 +2809,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) {
@@ -2949,7 +2949,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 = {
+1 -3
View File
@@ -448,9 +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-canvas.gen-live-preview .pm-stage { opacity: 0.12; }
.pm-canvas.gen-live-preview .pm-live-preview { z-index: 4; }
.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; }
+7 -36
View File
@@ -200,33 +200,10 @@ def _scene_playback_clip_spec(
_preview_segment_cache: dict[str, str] = {}
def _invalidate_preview_segment_cache(pid: str) -> None:
"""Drop cached ffmpeg trims when a project re-generates or source files change."""
import os
prefix = f"{pid}:"
for key in list(_preview_segment_cache.keys()):
if not key.startswith(prefix):
continue
cached = _preview_segment_cache.pop(key, None)
if cached:
try:
os.unlink(cached)
except OSError:
pass
def _source_mtime(path: str) -> int:
import os
try:
return int(os.path.getmtime(path))
except OSError:
return 0
def _preview_segment_cache_key(pid: str, scene_id: str, clip: dict, *, src_mtime: int = 0) -> str:
def _preview_segment_cache_key(pid: str, scene_id: str, clip: dict) -> str:
return (
f"{pid}:{scene_id}:{clip.get('filename')}:{clip.get('subfolder') or ''}:"
f"{clip.get('type') or 'output'}:{clip.get('in')}:{clip.get('dur')}:mt{int(src_mtime or 0)}"
f"{clip.get('type') or 'output'}:{clip.get('in')}:{clip.get('dur')}"
)
@@ -1421,9 +1398,7 @@ if web is not None and PromptServer is not None:
# --- API: generate / status / result ---
@routes.post(UI_PREFIX + "/api/projects/{pid}/generate")
async def _generate(req):
pid = req.match_info["pid"]
_invalidate_preview_segment_cache(pid)
p = _project_or_404(pid)
p = _project_or_404(req.match_info["pid"])
body = await req.json() if req.can_read_body else {}
scene_ids = body.get("scene_ids")
if scene_ids:
@@ -1714,15 +1689,10 @@ if web is not None and PromptServer is not None:
clip = _scene_playback_clip_spec(proj, scene_id, render_override=render_override)
except KeyError as e:
return web.json_response({"detail": str(e)}, status=404)
try:
src_path = _resolve_clip_src_path(clip)
except FileNotFoundError as e:
return web.json_response({"detail": str(e)}, status=400)
src_mtime = _source_mtime(src_path)
key = _preview_segment_cache_key(pid, scene_id, clip, src_mtime=src_mtime)
key = _preview_segment_cache_key(pid, scene_id, clip)
cached = _preview_segment_cache.get(key)
if cached and os.path.isfile(cached):
return web.FileResponse(cached, headers={"Cache-Control": "private, max-age=60"})
return web.FileResponse(cached, headers={"Cache-Control": "private, max-age=3600"})
try:
import folder_paths
tempdir = folder_paths.get_temp_directory()
@@ -1734,13 +1704,14 @@ if web is not None and PromptServer is not None:
f"funpack_preview_{pid[:8]}_{scene_id}_{int(_time.time() * 1000)}.mp4",
)
try:
src_path = _resolve_clip_src_path(clip)
_ffmpeg_trim_clip(src_path, out_path, clip.get("in"), clip.get("dur"))
except FileNotFoundError as e:
return web.json_response({"detail": str(e)}, status=400)
except RuntimeError as e:
return web.json_response({"detail": str(e)}, status=503)
_preview_segment_cache[key] = out_path
return web.FileResponse(out_path, headers={"Cache-Control": "private, max-age=60"})
return web.FileResponse(out_path, headers={"Cache-Control": "private, max-age=3600"})
# --- API: export one clip (ffmpeg trim from a chain output using in/dur) ---
@routes.post(UI_PREFIX + "/api/projects/{pid}/export-clip")
+1 -24
View File
@@ -400,27 +400,4 @@ def test_timeline_snapshot_mismatch_none_when_aligned():
{"id": "s2", "text": "two", "source": {"type": "carry", "media_ref": None}},
],
}
assert _timeline_snapshot_mismatch(snap, p) is None
def test_preview_segment_cache_key_includes_source_mtime():
from movie_editor.server import _preview_segment_cache_key
clip = {"filename": "chain.mp4", "subfolder": "", "type": "temp", "in": 0.0, "dur": 2.6}
k1 = _preview_segment_cache_key("p1", "s1", clip, src_mtime=100)
k2 = _preview_segment_cache_key("p1", "s1", clip, src_mtime=200)
assert k1 != k2
def test_invalidate_preview_segment_cache_drops_project_entries(tmp_path, monkeypatch):
from movie_editor import server
stale = tmp_path / "stale.mp4"
stale.write_bytes(b"x")
server._preview_segment_cache.clear()
server._preview_segment_cache["p1:s1:chain.mp4:::temp:0:2.6:mt1"] = str(stale)
server._preview_segment_cache["p2:s1:other.mp4:::temp:0:1:mt1"] = str(tmp_path / "keep.mp4")
server._invalidate_preview_segment_cache("p1")
assert not any(k.startswith("p1:") for k in server._preview_segment_cache)
assert any(k.startswith("p2:") for k in server._preview_segment_cache)
assert not stale.exists()
assert _timeline_snapshot_mismatch(snap, p) is None