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:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user