Fix stale generate input and live preview visibility.
Flush pending global prompt edits before save/generate so runs use the latest timeline text and anchors. Improve live preview wiring detection, progress polling, overlay stacking, and diagnostics when a VAE is configured.
This commit is contained in:
@@ -23,6 +23,7 @@ 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
|
||||
|
||||
@@ -202,26 +203,20 @@ def _wire_live_preview_vae(
|
||||
slot_def: dict,
|
||||
report: dict,
|
||||
) -> None:
|
||||
"""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
|
||||
"""Wire preview VAE slot -> Chain Sampler preview_vae when configured."""
|
||||
if "sampler" not in graph:
|
||||
return
|
||||
existing = graph["sampler"].get("inputs", {}).get("preview_vae")
|
||||
if isinstance(existing, list):
|
||||
return
|
||||
sid = lp.get("slot_id")
|
||||
sid, out_name = live_preview.preview_vae_wire(models_config)
|
||||
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(lp.get("vae_output") or "VAE")
|
||||
out_name = str(out_name or "VAE")
|
||||
nd = slot_def.get(sid) or {}
|
||||
outs = node_outputs(nd)
|
||||
oidx = _output_index(nd, out_name)
|
||||
|
||||
@@ -95,6 +95,12 @@ 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:
|
||||
@@ -103,10 +109,27 @@ 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)
|
||||
if sid:
|
||||
return True
|
||||
lp = models.get("live_preview") or {}
|
||||
return bool(lp.get("enabled") and lp.get("slot_id"))
|
||||
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
|
||||
|
||||
@@ -920,8 +920,7 @@
|
||||
if (_editing) return;
|
||||
const k = t.dataset?.k;
|
||||
if (k === "global-prompt") {
|
||||
S.flushSave();
|
||||
render(S.get());
|
||||
S.flushPendingEdits().then(() => render(S.get()));
|
||||
return;
|
||||
}
|
||||
if (!S.flushSave()) render(S.get());
|
||||
|
||||
@@ -877,7 +877,36 @@
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -890,9 +919,11 @@
|
||||
_genFillEl.style.width = Math.min(100, Math.round((g.step / g.maxStep) * 100)) + "%";
|
||||
}
|
||||
}
|
||||
if (_livePreviewEl && g.livePreviewUrl) {
|
||||
if (_livePreviewEl.src !== g.livePreviewUrl) _livePreviewEl.src = g.livePreviewUrl;
|
||||
_livePreviewEl.style.display = "";
|
||||
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 (_livePreviewBadgeEl && g.liveScene >= 0 && g.liveSceneCount > 0) {
|
||||
_livePreviewBadgeEl.textContent = `Scene ${g.liveScene + 1}/${g.liveSceneCount}`;
|
||||
@@ -1112,7 +1143,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 = null;
|
||||
_livePreviewEl = _livePreviewBadgeEl = _livePreviewLayerEl = null;
|
||||
if (["queuing", "running", "pending", "error"].includes(gen.state)) {
|
||||
const busy = gen.state !== "error";
|
||||
const ro = el("div", "gen-readout" + (gen.state === "error" ? " error" : ""));
|
||||
@@ -1131,17 +1162,9 @@
|
||||
}
|
||||
canvas.append(ro);
|
||||
}
|
||||
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) {
|
||||
if (["queuing", "running", "pending"].includes(gen.state) && _livePreviewConfigured(st)) {
|
||||
const lpLayer = el("div", "pm-live-preview");
|
||||
_livePreviewLayerEl = lpLayer;
|
||||
_livePreviewEl = el("img", "pm-live-preview-img");
|
||||
_livePreviewEl.alt = "Live preview";
|
||||
_livePreviewEl.draggable = false;
|
||||
@@ -1156,7 +1179,6 @@
|
||||
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,7 +69,8 @@
|
||||
|
||||
function _livePreviewConfigured() {
|
||||
const lp = state.models?.live_preview || {};
|
||||
if (lp.slot_id) return true;
|
||||
if (lp.slot_id || lp.enabled) return true;
|
||||
if ((state.models?.core_overrides?.sampler?.preview_vae || "").startsWith("out:")) return true;
|
||||
return (state.models?.slots || []).some((s) =>
|
||||
Object.values(s.wires || {}).some((raw) => _modelWireTargets(raw).includes(LIVE_PREVIEW_PORT)));
|
||||
}
|
||||
@@ -252,13 +253,36 @@
|
||||
}
|
||||
|
||||
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, " ");
|
||||
}
|
||||
@@ -506,30 +530,53 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
async function applyGlobalPromptQuiet(text) {
|
||||
async function applyGlobalPromptQuiet(text, opts = {}) {
|
||||
if (!state.project) return false;
|
||||
const trimmed = String(text || "").trim();
|
||||
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);
|
||||
const force = !!opts.force;
|
||||
if (!trimmed) {
|
||||
_pendingGlobalPromptText = null;
|
||||
return false;
|
||||
}
|
||||
if (!await _distributeGlobalPrompt(trimmed, res)) return false;
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = null;
|
||||
_localDirty = true;
|
||||
scheduleSaveSilent();
|
||||
refreshPreview(true);
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
||||
function _syncPromptPreview() {
|
||||
return (async () => {
|
||||
try {
|
||||
await _flushPendingGlobalPrompt();
|
||||
await flushSave();
|
||||
await refreshPreview(true);
|
||||
} catch (e) { console.error("prompt preview sync failed", e); }
|
||||
@@ -2381,8 +2428,13 @@
|
||||
throw last;
|
||||
}
|
||||
|
||||
async function flushPendingEdits() {
|
||||
await _flushPendingGlobalPrompt();
|
||||
return flushSave();
|
||||
}
|
||||
|
||||
async function _flushSaveForGenerate() {
|
||||
await flushSave();
|
||||
await flushPendingEdits();
|
||||
if (_localDirty) {
|
||||
throw new Error(
|
||||
"Could not save your latest edits before generate (tunnel may have dropped). "
|
||||
@@ -2724,23 +2776,19 @@
|
||||
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) {
|
||||
@@ -2857,6 +2905,7 @@
|
||||
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 = {
|
||||
@@ -3678,7 +3727,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, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, saveSelectedToMediaBin, clipSaveableToMediaBin, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, setLivePreviewEnabled, setLivePreviewSlot, clearNotice,
|
||||
refreshPreview, syncFromPreview, applyGlobalPromptQuiet, scheduleGlobalPromptApply, buildGlobalPromptFromTimeline, syncGlobalPromptFromTimeline, flushPendingEdits, 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,
|
||||
|
||||
@@ -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: 1; }
|
||||
.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-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; }
|
||||
|
||||
+18
-18
@@ -1466,6 +1466,16 @@ 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
|
||||
@@ -1483,6 +1493,8 @@ 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}")
|
||||
@@ -1532,27 +1544,15 @@ if web is not None and PromptServer is not None:
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/progress")
|
||||
async def _progress(_req):
|
||||
return web.json_response(bridge.current_progress())
|
||||
payload = bridge.current_progress()
|
||||
lp = live_preview.api_payload(UI_PREFIX)
|
||||
if lp.get("active"):
|
||||
payload["live_preview"] = lp
|
||||
return web.json_response(payload)
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/live-preview")
|
||||
async def _live_preview_status(_req):
|
||||
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)
|
||||
return web.json_response(live_preview.api_payload(UI_PREFIX))
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/live-preview/frame")
|
||||
async def _live_preview_frame(_req):
|
||||
|
||||
+8
-1
@@ -3705,7 +3705,11 @@ class FunPackLTXAVSceneChainSampler:
|
||||
return
|
||||
os.makedirs(preview_dir, exist_ok=True)
|
||||
tile = int(cfg.get("decode_tile_size") or 256)
|
||||
decoded = self._decode_for_preview(preview_vae, scene_latent, decode_tile_size=tile)
|
||||
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
|
||||
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(
|
||||
@@ -3719,6 +3723,9 @@ 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."""
|
||||
|
||||
Reference in New Issue
Block a user