Remove mid-chain VAE decode previews that slowed multi-scene runs.

Progressive quick preview decoded the full cumulative latent after each scene inside the sampler loop, adding large overhead without fitting ComfyUI's single-pass workflow. Preview now arrives only when the run completes.
This commit is contained in:
digital-garbage
2026-06-11 15:34:54 +03:00
parent e90e46f3a6
commit 022b5e7870
5 changed files with 1 additions and 228 deletions
-72
View File
@@ -1,72 +0,0 @@
"""In-flight chain sampler progress for the Movie Editor.
While a multi-scene FunPackLTXAVSceneChainSampler run is executing, partial decodes
are published here so the editor can show finished scenes before the full run ends.
"""
from __future__ import annotations
import threading
import time
from typing import Any, Optional
_lock = threading.Lock()
_by_key: dict[str, dict[str, Any]] = {}
_prompt_to_key: dict[str, str] = {}
def begin(prompt_id: str, progress_key: str, scene_count: int) -> None:
if not prompt_id or not progress_key or scene_count <= 1:
return
with _lock:
_prompt_to_key[prompt_id] = progress_key
_by_key[progress_key] = {
"scene_count": int(scene_count),
"completed_scenes": 0,
"media": None,
"ts": time.time(),
}
def update(progress_key: str, completed_scenes: int, media: dict, scene_layout: Optional[list] = None) -> None:
if not progress_key or not media:
return
with _lock:
slot = _by_key.get(progress_key)
if not slot:
return
slot["completed_scenes"] = max(int(completed_scenes), int(slot.get("completed_scenes") or 0))
slot["media"] = dict(media)
if scene_layout:
slot["scene_layout"] = list(scene_layout)
slot["ts"] = time.time()
def read_for_prompt(prompt_id: str) -> Optional[dict]:
with _lock:
key = _prompt_to_key.get(prompt_id)
if not key:
return None
slot = _by_key.get(key)
if not slot or not slot.get("media"):
return None
completed = int(slot.get("completed_scenes") or 0)
total = int(slot.get("scene_count") or 0)
if completed <= 0 or total <= 1 or completed >= total:
return None
out = {
"partial": True,
"completed_scenes": completed,
"scene_count": total,
"media": [dict(slot["media"])],
}
layout = slot.get("scene_layout")
if layout:
out["scene_layout"] = list(layout)
return out
def finish(prompt_id: str) -> None:
with _lock:
key = _prompt_to_key.pop(prompt_id, None)
if key:
_by_key.pop(key, None)
-13
View File
@@ -1396,7 +1396,6 @@
// Poll a single queued prompt to completion. Resolves true on success, false on error.
function _pollPromise(promptId, targetSceneIds, prefix) {
prefix = prefix || "Generating…";
let lastPartialScenes = 0;
return new Promise((resolve) => {
_clearGenTimers();
let pendingStreak = 0;
@@ -1437,18 +1436,6 @@
} else {
// "pending" after "running" means the job left the queue without a history
// entry — it likely crashed or was interrupted by ComfyUI.
if (s.partial?.media?.length && s.partial.completed_scenes > lastPartialScenes) {
lastPartialScenes = s.partial.completed_scenes;
_recordSegment(s.partial.media, targetSceneIds, {
completedScenes: lastPartialScenes,
sceneLayout: s.partial.scene_layout,
});
notify();
const step = (state.gen.maxStep > 0) ? ` · sampling ${state.gen.step}/${state.gen.maxStep}` : "";
updateGenProgress({
msg: `${prefix} ${_elapsed()} · ${lastPartialScenes}/${targetSceneIds.length} scene(s) ready${step}`,
});
}
if (s.state === "pending") pendingStreak++; else pendingStreak = 0;
if (pendingStreak >= 3) {
_clearGenTimers();
+1 -16
View File
@@ -860,11 +860,6 @@ if web is not None and PromptServer is not None:
media_pack = _prepare_media(target, continuity_media_refs(p, target))
sampler_inputs = _run_sampler_inputs(target, active_scene_count, full=p)
_attach_scene_anchors(sampler_inputs, media_pack, target)
progress_key = ""
if active_scene_count > 1:
import uuid
progress_key = uuid.uuid4().hex
sampler_inputs["chain_progress_key"] = progress_key
graph, report = builder.build(oi, _project_models(target), {
"prompt": prompt, "seed": _resolve_run_seed(target),
"num_frames_per_scene": effective_frames,
@@ -884,9 +879,6 @@ if web is not None and PromptServer is not None:
except Exception as e: # noqa: BLE001
return web.json_response({"detail": f"Failed to queue with ComfyUI: {e}"}, status=502)
prompt_id = result.get("prompt_id")
if progress_key and prompt_id:
from .backend import chain_progress
chain_progress.begin(prompt_id, progress_key, active_scene_count)
p.generation_meta = {
**(p.generation_meta or {}),
"prompt_hash": validation["prompt_hash"],
@@ -924,8 +916,6 @@ if web is not None and PromptServer is not None:
exec_error = f"{node_type}: {exc}"
break
is_error = raw_status.get("status_str") == "error"
from .backend import chain_progress
chain_progress.finish(prompt_id)
payload = {
"state": "error" if (is_error and not media) else "completed",
"media": media,
@@ -939,12 +929,7 @@ if web is not None and PromptServer is not None:
running = await bridge.is_running(prompt_id)
except Exception: # noqa: BLE001
running = True
from .backend import chain_progress
partial = chain_progress.read_for_prompt(prompt_id)
payload = {"state": "running" if running else "pending", "media": []}
if partial:
payload["partial"] = partial
return web.json_response(payload)
return web.json_response({"state": "running" if running else "pending", "media": []})
@routes.get(UI_PREFIX + "/api/rating-labels")
async def _rating_labels(_req):
-24
View File
@@ -1,24 +0,0 @@
from movie_editor.backend.chain_progress import begin, finish, read_for_prompt, update
def test_chain_progress_partial_read():
begin("pid1", "run-a", 3)
layout = [{"scene_index": 0, "start_frame": 0, "in_sec": 0.0}]
update("run-a", 1, {"kind": "videos", "filename": "partial_s1.mp4", "subfolder": "funpack_chain_progress/run-a", "type": "temp"}, scene_layout=layout)
snap = read_for_prompt("pid1")
assert snap is not None
assert snap["partial"] is True
assert snap["completed_scenes"] == 1
assert snap["scene_count"] == 3
assert snap["media"][0]["filename"] == "partial_s1.mp4"
assert snap["scene_layout"] == layout
update("run-a", 3, {"kind": "videos", "filename": "partial_s3.mp4", "subfolder": "x", "type": "temp"})
assert read_for_prompt("pid1") is None
finish("pid1")
assert read_for_prompt("pid1") is None
def test_chain_progress_single_scene_skipped():
begin("pid2", "run-b", 1)
assert read_for_prompt("pid2") is None
finish("pid2")
-103
View File
@@ -2095,11 +2095,6 @@ class FunPackLTXAVSceneChainSampler:
"multiline": True,
"tooltip": "Optional media_ref → filename map for image-type i2v guides in custom guide stacks.",
}),
"chain_progress_key": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "Movie Editor only: token for publishing partial scene previews while a multi-scene chain run is in flight.",
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
@@ -3302,7 +3297,6 @@ class FunPackLTXAVSceneChainSampler:
refinement_key_input="", funpack_scene_guides="",
funpack_scene_anchors="",
funpack_scene_media_refs="",
chain_progress_key="",
unique_id=None, prompt=None):
if not isinstance(positive, list) or not positive:
raise ValueError("positive conditioning must contain at least one scene entry.")
@@ -3530,16 +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 chain_progress_key and scene_count > 1:
self._save_chain_progress_preview(
vae, output, chain_progress_key, scene_index, scene_count,
decode_tile_size=decode_tile_size,
frame_rate=24,
boundary_entries=boundary_entries,
num_frames_per_scene=int(num_frames_per_scene),
frame_overlap=int(frame_overlap),
time_scale=time_scale,
)
del scene_cond, scene_positive, scene_negative, scene_conditionings, chunk, sampled
@@ -3667,93 +3651,6 @@ class FunPackLTXAVSceneChainSampler:
return None
return [groups[k] for k in sorted(groups)]
def _save_chain_progress_preview(self, vae, output, progress_key, scene_index, scene_count,
decode_tile_size=0, frame_rate=24,
boundary_entries=None, num_frames_per_scene=0,
frame_overlap=0, time_scale=1):
"""Decode cumulative chain output to a small temp mp4 for Movie Editor preview."""
if not progress_key or scene_count <= 1 or scene_index >= scene_count - 1:
return
try:
try:
from movie_editor.backend import chain_progress as _cp
except ImportError:
from .movie_editor.backend import chain_progress as _cp # type: ignore
except Exception:
return
import os
import re as _re
import shutil
import subprocess
import tempfile
try:
import folder_paths
base = folder_paths.get_temp_directory()
except Exception:
return
rel_dir = os.path.join("funpack_chain_progress", _re.sub(r"[^A-Za-z0-9_.-]", "_", progress_key))
out_dir = os.path.join(base, rel_dir)
os.makedirs(out_dir, exist_ok=True)
fn = f"partial_s{scene_index + 1}.mp4"
out_path = os.path.join(out_dir, fn)
try:
decoded = self._decode_for_preview(vae, output, decode_tile_size=decode_tile_size or 512)
if decoded is None:
return
t = decoded.detach().float().clamp(0, 1).cpu()
if t.dim() == 4 and t.shape[-1] not in (1, 3):
t = t.permute(0, 2, 3, 1)
frames = t.numpy()
tmp = tempfile.mkdtemp(prefix="funpack_chain_prev_")
try:
from PIL import Image
import numpy as _np
for i, f in enumerate(frames):
arr = (_np.asarray(f) * 255).astype(_np.uint8)
if arr.shape[-1] == 1:
arr = arr.repeat(3, axis=-1)
Image.fromarray(arr).save(os.path.join(tmp, f"{i:04d}.png"))
ff = shutil.which("ffmpeg")
if not ff:
return
subprocess.run(
[
ff, "-y", "-hide_banner", "-loglevel", "error",
"-framerate", str(max(1, int(frame_rate))),
"-i", os.path.join(tmp, "%04d.png"),
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
out_path,
],
check=True,
capture_output=True,
)
finally:
shutil.rmtree(tmp, ignore_errors=True)
scene_layout = None
try:
try:
from movie_editor.backend.chain_layout import scene_playback_layout
except ImportError:
from .movie_editor.backend.chain_layout import scene_playback_layout # type: ignore
scene_layout = scene_playback_layout(
scene_index + 1,
fps=frame_rate,
num_frames_per_scene=num_frames_per_scene,
frame_overlap=frame_overlap,
time_scale=time_scale,
boundaries=boundary_entries,
)
except Exception:
scene_layout = None
_cp.update(progress_key, scene_index + 1, {
"kind": "videos",
"filename": fn,
"subfolder": rel_dir.replace("\\", "/"),
"type": "temp",
}, scene_layout=scene_layout)
except Exception as e:
print(f"[FunPackSceneChain] chain progress preview failed (scene {scene_index + 1}): {e}")
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: