Revert "Add Chain Sampler live preview with View menu VAE picker."

This reverts commit 4e69545f0c2cbd2780982b4f01dc8db681e1bf99.
This commit is contained in:
digital-garbage
2026-06-13 02:40:31 +03:00
parent fddd38270f
commit 3bf69fefe1
10 changed files with 5 additions and 403 deletions
-46
View File
@@ -194,47 +194,6 @@ def _output_index(node_def: Optional[dict], out_name: str) -> int:
return 0
def _wire_live_preview_vae(
models_config: dict,
graph: dict,
slot_node_id: dict,
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
if "sampler" not in graph:
return
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(lp.get("vae_output") or "VAE")
nd = slot_def.get(sid) or {}
outs = node_outputs(nd)
oidx = _output_index(nd, out_name)
if not outs or oidx >= len(outs) or outs[oidx].get("type") != "VAE":
report["unsatisfied"].append(
f"Live preview slot '{sid}' has no VAE output '{out_name}'."
)
return
graph["sampler"]["inputs"]["preview_vae"] = [nid, oidx]
slot_lbl = next(
(s.get("label") or s.get("node_class") or sid
for s in ((models_config or {}).get("slots") or []) if s.get("id") == sid),
sid,
)
report["wired"].append(f"{slot_lbl}.{out_name} -> sampler.preview_vae (live preview)")
def build(object_info: dict, models_config: dict, params: dict, media: dict | None = None) -> tuple[dict, dict]:
"""Return (graph, report). `params`: prompt, negative_prompt, seed,
num_frames_per_scene, frame_rate, width, height. `models_config`: {"slots":[...],
@@ -466,8 +425,6 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
report["unsatisfied"].append(
f"core override {cid}.{inp}: '{source}' could not be resolved.")
_wire_live_preview_vae(models_config, graph, slot_node_id, slot_def, report)
# 5. auto-wire remaining unbound typed inputs by unique producer.
producers = _producers(graph, slots, slot_node_id, slot_def, object_info)
# Drop producers whose node isn't in the graph (core producers vanish when the built-in
@@ -490,9 +447,6 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
for src in (ovs or {}).values():
if isinstance(src, str) and src.startswith("out:"):
active_slots.add(src.split(":", 2)[1])
lp_sid = ((models_config or {}).get("live_preview") or {}).get("slot_id")
if lp_sid:
active_slots.add(lp_sid)
_autowire(graph, slots, slot_node_id, slot_def, object_info, producers, report, active_slots)
for msg in pipeline_wiring.validate_models_wiring(models_config):
-79
View File
@@ -1,79 +0,0 @@
"""In-process live preview state for Cutting Room generation runs.
The Chain Sampler writes per-scene preview frames here; the Movie Editor server
serves them while a job is running.
"""
from __future__ import annotations
import json
import threading
import time
from pathlib import Path
from typing import Any, Optional
_lock = threading.Lock()
_state: dict[str, Any] = {
"active": False,
"dir": "",
"scene_index": -1,
"scene_count": 0,
"updated_at": 0.0,
"frame_path": "",
}
def reset(preview_dir: str, scene_count: int = 0) -> None:
with _lock:
_state["active"] = True
_state["dir"] = str(preview_dir or "")
_state["scene_index"] = -1
_state["scene_count"] = int(scene_count or 0)
_state["updated_at"] = time.time()
_state["frame_path"] = ""
if preview_dir:
Path(preview_dir).mkdir(parents=True, exist_ok=True)
def clear() -> None:
with _lock:
_state["active"] = False
_state["dir"] = ""
_state["scene_index"] = -1
_state["scene_count"] = 0
_state["updated_at"] = time.time()
_state["frame_path"] = ""
def publish(scene_index: int, frame_path: str, scene_count: Optional[int] = None) -> None:
with _lock:
if not _state.get("active"):
return
_state["scene_index"] = int(scene_index)
if scene_count is not None:
_state["scene_count"] = int(scene_count)
_state["frame_path"] = str(frame_path or "")
_state["updated_at"] = time.time()
def snapshot() -> dict[str, Any]:
with _lock:
return dict(_state)
def parse_config(raw: Any) -> dict[str, Any]:
if not raw:
return {}
if isinstance(raw, dict):
return raw
try:
out = json.loads(str(raw))
return out if isinstance(out, dict) else {}
except Exception:
return {}
def models_enabled(models: Any) -> bool:
if not isinstance(models, dict):
return False
lp = models.get("live_preview") or {}
return bool(lp.get("enabled") and lp.get("slot_id"))
-1
View File
@@ -134,7 +134,6 @@
j("POST", API(`/projects/${id}/generate`), { only_scene: onlyScene || null, scene_ids: sceneIds || null, reset_session: !!resetSession }),
status: (id, promptId) => j("GET", API(`/projects/${id}/status/${promptId}`)),
progress: () => j("GET", API("/progress")),
livePreview: () => j("GET", API("/live-preview")),
ratingLabels: () => j("GET", API("/rating-labels")),
log: (limit) => j("GET", API("/log" + (limit ? `?limit=${limit}` : ""))),
interrupt: () => j("POST", API("/interrupt")),
-43
View File
@@ -156,42 +156,6 @@
});
}
async function _pickPreviewVae(st) {
closeAll();
const slots = st.models?.slots || [];
const options = [];
for (const s of slots) {
let spec;
try { spec = await window.MovieEditorAPI.nodeSpec(s.node_class); } catch (_) { continue; }
const vaeOuts = (spec.outputs || []).filter((o) => o.type === "VAE");
if (!vaeOuts.length) continue;
const outName = vaeOuts.find((o) => o.name === "VAE")?.name || vaeOuts[0].name;
options.push({
value: `${s.id}\x1f${outName}`,
label: s.label || s.node_class || s.id,
hint: `${outName} · ${s.node_class}`,
});
}
if (!options.length) {
alert("No Models slot outputs VAE.\nAdd a VAE loader in Settings → Models (e.g. taehv for fast preview).");
return;
}
const cur = st.models?.live_preview || {};
if (!window.SlotPicker) return;
window.SlotPicker.open({
title: "Choose preview VAE",
options: options.map((o) => {
const [sid, out] = o.value.split("\x1f");
const isCur = sid === cur.slot_id && out === (cur.vae_output || "VAE");
return { ...o, hint: isCur ? "current" : o.hint };
}),
onPick: (val) => {
const [slotId, outName] = val.split("\x1f");
S.setLivePreviewSlot(slotId, outName);
},
});
}
let _gitStatus = null;
function promptNewProject() {
@@ -251,13 +215,6 @@
View: [
{ label: "Refresh Preview", action: () => S.refreshPreview() },
{ sep: true },
{ label: st.models?.live_preview?.enabled ? "Disable live preview ✓" : "Enable live preview",
disabled: !hasProject() || !chainOn,
action: () => S.setLivePreviewEnabled(!st.models?.live_preview?.enabled) },
{ label: "Choose preview VAE…",
disabled: !hasProject() || !chainOn,
action: () => _pickPreviewVae(st) },
{ sep: true },
{ label: "Reset Layout", action: () => { const r = document.documentElement; r.style.removeProperty("--media-w"); r.style.removeProperty("--timeline-h"); } },
],
Help: window.__FUNPACK_TOUR__ ? [
-26
View File
@@ -877,7 +877,6 @@
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;
// 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,14 +889,6 @@
_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 (_livePreviewBadgeEl && g.liveScene >= 0 && g.liveSceneCount > 0) {
_livePreviewBadgeEl.textContent = `Scene ${g.liveScene + 1}/${g.liveSceneCount}`;
_livePreviewBadgeEl.style.display = "";
}
});
// Lightweight per-tick update: never rebuilds the buttons (rebuilding mid-click ate the
@@ -1112,7 +1103,6 @@
// 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;
if (["queuing", "running", "pending", "error"].includes(gen.state)) {
const busy = gen.state !== "error";
const ro = el("div", "gen-readout" + (gen.state === "error" ? " error" : ""));
@@ -1131,22 +1121,6 @@
}
canvas.append(ro);
}
if (["queuing", "running", "pending"].includes(gen.state) && S.get().models?.live_preview?.enabled) {
const lpLayer = el("div", "pm-live-preview");
_livePreviewEl = el("img", "pm-live-preview-img");
_livePreviewEl.alt = "Live preview";
_livePreviewEl.draggable = false;
if (gen.livePreviewUrl) _livePreviewEl.src = gen.livePreviewUrl;
else _livePreviewEl.style.display = "none";
_livePreviewBadgeEl = el("div", "pm-live-preview-badge");
if (gen.liveScene >= 0 && gen.liveSceneCount > 0) {
_livePreviewBadgeEl.textContent = `Scene ${gen.liveScene + 1}/${gen.liveSceneCount}`;
} else {
_livePreviewBadgeEl.style.display = "none";
}
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");
+3 -63
View File
@@ -25,7 +25,7 @@
notice: "",
saving: false,
unsaved: false,
models: { slots: [], live_preview: { enabled: false, slot_id: null, vae_output: "VAE" } }, // pluggable node config (shared with Models modal)
models: { slots: [] }, // pluggable node config (shared with Models modal)
mediaBin: [], // uploaded assets [{id,name,kind,...}]
mediaPreviewId: null, // transient image preview in the player (not scene assignment)
shortcuts: [], // prompt shortcut library
@@ -2689,22 +2689,6 @@
});
}
} catch (_) {}
if (state.models?.live_preview?.enabled) {
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) {
@@ -2814,7 +2798,7 @@
async function _generateRun(sceneIds, onlyScene, prefix, resetSession) {
_interrupted = false;
_markGenInFlight(sceneIds);
set({ gen: { state: "queuing", promptId: null, media: [], msg: `${prefix}: queuing…`, step: 0, maxStep: 0, livePreviewUrl: null, liveScene: -1, liveSceneCount: 0 } });
set({ gen: { state: "queuing", promptId: null, media: [], msg: `${prefix}: queuing…`, step: 0, maxStep: 0 } });
try {
const r = await _retryOnTunnel(
() => API.generate(state.project.id, onlyScene || null, onlyScene ? null : sceneIds, !!resetSession),
@@ -3277,12 +3261,6 @@
// ── pluggable models / exposed controls ──────────────────────────────────────
async function loadModels() {
try { state.models = await API.getModels(state.project?.id); } catch (_) { state.models = { slots: [] }; }
state.models.live_preview = {
enabled: false,
slot_id: null,
vae_output: "VAE",
...(state.models.live_preview || {}),
};
if (state.project) {
state.project.models = JSON.parse(JSON.stringify(state.models || { slots: [] }));
}
@@ -3322,44 +3300,6 @@
catch (e) { console.error("saveModels failed", e); }
}
async function setLivePreviewEnabled(enabled) {
state.models.live_preview = {
enabled: false,
slot_id: null,
vae_output: "VAE",
...(state.models.live_preview || {}),
enabled: !!enabled,
};
if (state.project) {
state.project.models = JSON.parse(JSON.stringify(state.models));
}
notify();
try {
state.models = await API.saveModels(state.project?.id, state.models);
notify();
} catch (e) { console.error("saveModels failed", e); }
}
async function setLivePreviewSlot(slotId, vaeOutput = "VAE") {
state.models.live_preview = {
enabled: false,
slot_id: null,
vae_output: "VAE",
...(state.models.live_preview || {}),
slot_id: slotId,
vae_output: vaeOutput || "VAE",
enabled: true,
};
if (state.project) {
state.project.models = JSON.parse(JSON.stringify(state.models));
}
notify();
try {
state.models = await API.saveModels(state.project?.id, state.models);
notify();
} catch (e) { console.error("saveModels failed", e); }
}
// ── media bin + libraries ─────────────────────────────────────────────────────
async function loadMedia() { try { state.mediaBin = (await API.listMedia()).media || []; } catch (_) { state.mediaBin = []; } notify(); }
async function uploadMedia(files) {
@@ -3631,7 +3571,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, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, saveSelectedToMediaBin, clipSaveableToMediaBin, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, clearNotice,
setConditioningSlot, setSamplerSlot, setSamplerInput, setSamplerInputNow, unsetSamplerInput, setStudioInput, setStudioInputNow,
loadMedia, uploadMedia, deleteMedia, deleteMediaMany, renameMedia, previewMedia, clearMediaPreview, assignMediaToScene, exportMediaAsset,
loadShortcuts, saveShortcut, deleteShortcut, importShortcuts,
-3
View File
@@ -448,9 +448,6 @@ 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-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; }
.gen-bar-fill { height: 100%; background: var(--amber); border-radius: 3px; transition: width .2s; }
.gen-interrupt { font-family: var(--font-mono); font-size: 11px; color: var(--danger); background: transparent; border: 1px solid color-mix(in srgb, var(--danger) 50%, var(--line)); border-radius: 12px; padding: 2px 9px; cursor: pointer; flex: 0 0 auto; }
+1 -44
View File
@@ -19,7 +19,7 @@ except Exception: # pragma: no cover - only available inside ComfyUI
web = None
PromptServer = None
from .backend import bridge, builder, config, git_update, live_preview, media, nodes, pipeline_caps, pipeline_deps, pipeline_wiring, projects, workflow_import
from .backend import bridge, builder, config, git_update, media, nodes, pipeline_caps, pipeline_deps, pipeline_wiring, projects, workflow_import
from .backend.nle_effects import zoompan_z_expr
from .backend.nle_overlays import build_overlay_video_filter, prepare_overlay_export
from .backend.timeline import (
@@ -1434,20 +1434,6 @@ if web is not None and PromptServer is not None:
sampler_inputs = _run_sampler_inputs(target, active_scene_count, full=p, models=models_cfg)
if caps["chain_sampler"]:
_attach_scene_anchors(sampler_inputs, media_pack, target)
import json as _json
import tempfile as _tempfile
if live_preview.models_enabled(models_cfg):
lp_dir = _tempfile.mkdtemp(prefix="funpack_live_")
live_preview.reset(lp_dir, active_scene_count)
sampler_inputs = dict(sampler_inputs or {})
sampler_inputs["funpack_live_preview"] = _json.dumps({
"dir": lp_dir,
"width": 384,
"max_frames": 24,
"decode_tile_size": 256,
})
else:
live_preview.clear()
graph, report = builder.build(oi, models_cfg, {
"prompt": prompt, "seed": _resolve_run_seed(target),
"num_frames_per_scene": effective_frames,
@@ -1534,35 +1520,6 @@ if web is not None and PromptServer is not None:
async def _progress(_req):
return web.json_response(bridge.current_progress())
@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)
@routes.get(UI_PREFIX + "/api/live-preview/frame")
async def _live_preview_frame(_req):
import os
snap = live_preview.snapshot()
path = snap.get("frame_path") or ""
if not path or not os.path.isfile(path):
raise web.HTTPNotFound()
return web.FileResponse(path, headers={"Cache-Control": "no-store"})
@routes.get(UI_PREFIX + "/api/log")
async def _log(req):
try:
-26
View File
@@ -211,29 +211,3 @@ def test_movie_editor_scene_ratings_in_studio_settings():
{"index": 2, "rating": "Missing action"},
]
assert "_movie_editor_scene_ratings" not in graph["studio"]["inputs"]
def test_live_preview_wires_preview_vae_separately():
models = {
"full_control": True,
"live_preview": {"enabled": True, "slot_id": "pv", "vae_output": "VAE"},
"slots": [
{"id": "u", "node_class": "UnetLoader", "inputs": {}, "wires": {"MODEL": "port:FunPackStudio.model"}},
{"id": "c", "node_class": "ClipLoader", "inputs": {}, "wires": {"CLIP": "port:FunPackStudio.clip"}},
{"id": "v", "node_class": "VaeLoader", "inputs": {}, "wires": {
"VAE": ["port:FunPackLTXAVSceneChainSampler.vae", "port:LTXVAudioVAEDecode.audio_vae"],
}},
{"id": "pv", "node_class": "VaeLoader", "inputs": {"vae_name": "taehv.safetensors"}, "wires": {}},
{"id": "ip", "node_class": "ImgProc", "inputs": {},
"wires": {"latent": "port:FunPackStudio.latent",
"Latent": "port:LTXVConcatAVLatent.audio_latent",
"output_image": "port:FunPackStudio.source_image"},
"input_sources": {"vae": "out:v:VAE"}},
{"id": "li", "node_class": "LoadImage", "inputs": {}, "wires": {"IMAGE": "node:ip:image"}},
],
}
graph, report = builder.build(OI, models, PARAMS)
assert graph["sampler"]["inputs"]["vae"] == ["slot_v", 0]
assert graph["sampler"]["inputs"]["preview_vae"] == ["slot_pv", 0]
assert any("preview_vae" in w for w in report["wired"])
assert report["blocking"] == []
+1 -72
View File
@@ -2095,14 +2095,6 @@ class FunPackLTXAVSceneChainSampler:
"multiline": True,
"tooltip": "Optional media_ref → filename map for image-type i2v guides in custom guide stacks.",
}),
"preview_vae": ("VAE", {
"tooltip": "Optional fast preview VAE (e.g. taehv). When wired, Movie Editor can show per-scene animated previews while the chain runs.",
}),
"funpack_live_preview": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "Internal JSON from Movie Editor: preview output directory and encode options.",
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
@@ -3305,7 +3297,6 @@ class FunPackLTXAVSceneChainSampler:
refinement_key_input="", funpack_scene_guides="",
funpack_scene_anchors="",
funpack_scene_media_refs="",
preview_vae=None, funpack_live_preview="",
unique_id=None, prompt=None):
if not isinstance(positive, list) or not positive:
raise ValueError("positive conditioning must contain at least one scene entry.")
@@ -3328,15 +3319,11 @@ class FunPackLTXAVSceneChainSampler:
carry_i2v_guides, mid_scene_guide, mid_scene_guide_strength,
embed_guidance, embed_guidance_strength, transition_duration,
decode_tile_size, refinement_key_input, embed_guidance_source,
preview_vae=preview_vae, funpack_live_preview=funpack_live_preview,
)
max_scene_count = max(1, int(max_scenes))
scene_conditionings = positive[:max_scene_count]
scene_count = len(scene_conditionings)
live_cfg = self._parse_live_preview_config(funpack_live_preview)
if preview_vae is not None and live_cfg.get("dir"):
self._init_live_preview(live_cfg, scene_count)
time_scale = self._time_scale(vae)
video_frames = self._validate_template_length(latent_template, num_frames_per_scene, time_scale)
video_overlap = self._overlap_frames(latent_template, frame_overlap, time_scale)
@@ -3537,10 +3524,6 @@ class FunPackLTXAVSceneChainSampler:
f"Scene {scene_index + 1}: seed={scene_seed}, text={scene_meta['text']}"
+ (f" | encode≠text" if scene_meta["encode_text"] != scene_meta["text"] else "")
)
if preview_vae is not None and live_cfg.get("dir"):
self._emit_live_scene_preview(
preview_vae, sampled, scene_index, scene_count, live_cfg,
)
del scene_cond, scene_positive, scene_negative, scene_conditionings, chunk, sampled
@@ -3668,58 +3651,6 @@ class FunPackLTXAVSceneChainSampler:
return None
return [groups[k] for k in sorted(groups)]
def _parse_live_preview_config(self, raw):
if not raw:
return {}
if isinstance(raw, dict):
return raw
try:
import json as _json
out = _json.loads(str(raw))
return out if isinstance(out, dict) else {}
except Exception:
return {}
def _init_live_preview(self, cfg, scene_count):
try:
from movie_editor.backend import live_preview as _lp
except Exception:
try:
from .movie_editor.backend import live_preview as _lp # type: ignore
except Exception:
return
_lp.reset(cfg.get("dir", ""), scene_count)
def _emit_live_scene_preview(self, preview_vae, scene_latent, scene_index, scene_count, cfg):
import os
import shutil
try:
from movie_editor.backend import live_preview as _lp
except Exception:
try:
from .movie_editor.backend import live_preview as _lp # type: ignore
except Exception:
return
preview_dir = str(cfg.get("dir") or "")
if not preview_dir:
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)
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(
decoded,
path,
max_frames=int(cfg.get("max_frames") or 24),
width=int(cfg.get("width") or 384),
):
try:
shutil.copy2(path, current)
except Exception:
current = path
_lp.publish(int(scene_index), current, scene_count)
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."""
try:
@@ -3769,8 +3700,7 @@ class FunPackLTXAVSceneChainSampler:
latent_template, num_frames_per_scene, frame_overlap, cfg, max_scenes,
use_same_seed, carry_i2v_guides, mid_scene_guide, mid_scene_guide_strength,
embed_guidance, embed_guidance_strength, transition_duration,
decode_tile_size, refinement_key_input, embed_guidance_source="relative",
preview_vae=None, funpack_live_preview=""):
decode_tile_size, refinement_key_input, embed_guidance_source="relative"):
"""Sample one chain per Studio-packed variant entry (seed + index), persisting each result
(latent + preview + per-entry cond + manifest) under ComfyUI temp for rating in Studio.
Reuses sample() per entry with only the seed changed, so each entry is a clean generation."""
@@ -3811,7 +3741,6 @@ class FunPackLTXAVSceneChainSampler:
embed_guidance_strength=embed_guidance_strength, embed_guidance_source=embed_guidance_source,
transition_duration=transition_duration,
decode_tile_size=decode_tile_size, refinement_key_input=key,
preview_vae=preview_vae, funpack_live_preview=funpack_live_preview,
unique_id=None, prompt=None,
)
last = out