Revert "Add Chain Sampler live preview with View menu VAE picker."
This reverts commit 4e69545f0c2cbd2780982b4f01dc8db681e1bf99.
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
@@ -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")),
|
||||
|
||||
@@ -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__ ? [
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user