Simplify IMAGES decode: full latent in one pass, then apply transitions

Removed per-scene slicing (_decode_video/_decode_tensor) and the aggressive
del-before-decode GC block. Now: whole sampling loop runs to completion →
full latent decoded in one pass → transition pixel effects applied after.
LATENT output is unchanged. Per-scene decode was causing seam cross-contamination
anyway and contradicted the post-process-only contract for transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-05-26 12:15:04 +03:00
co-authored by Claude Sonnet 4.6
parent d9bf41b6dd
commit fbc6e1c7c0
+14 -57
View File
@@ -1193,45 +1193,6 @@ class FunPackLTXAVSceneChainSampler:
if sigma > 0:
frames[i] = self._blur_pixel_frame(frames[i], sigma)
def _decode_tensor(self, video_tensor, vae, tile_size=0):
"""Decode a single video tensor to [N, H, W, C] pixel frames."""
if tile_size > 0:
try:
decoded = vae.decode_tiled(video_tensor, tile_x=tile_size // 8, tile_y=tile_size // 8)
except Exception:
decoded = vae.decode(video_tensor)
else:
decoded = vae.decode(video_tensor)
if decoded.dim() == 5:
b, t, h, w, c = decoded.shape
decoded = decoded.reshape(b * t, h, w, c)
return decoded
def _decode_video(self, output, vae, tile_size=0, scene_boundaries=None):
"""Decode video latent to [N, H, W, C] pixel frames.
When scene_boundaries is provided (list of latent frame positions where
each new scene starts), each scene slice is decoded independently.
Decoding the full concatenated latent in one pass lets the VAE's temporal
attention spread seam artifacts forward across scenes — per-scene decode
eliminates that cross-contamination.
"""
video_tensor = self._latent_tensors(output)[0]
if not scene_boundaries:
return self._decode_tensor(video_tensor, vae, tile_size)
# Build scene segments: [0, b0, b1, ..., total]
total = self._tensor_frames(video_tensor)
edges = [0] + list(scene_boundaries) + [total]
parts = []
for start, end in zip(edges, edges[1:]):
if start >= end:
continue
sliced = self._time_slice(video_tensor, start, end)
parts.append(self._decode_tensor(sliced, vae, tile_size))
if not parts:
return self._decode_tensor(video_tensor, vae, tile_size)
return torch.cat(parts, dim=0)
def _apply_transitions_pixel(self, decoded, boundary_entries, transition_duration):
"""Apply visual transitions on a full decoded [N, H, W, C] frame tensor."""
active = [e for e in boundary_entries if e.get("effect") and e["effect"] != "none"]
@@ -1341,31 +1302,27 @@ class FunPackLTXAVSceneChainSampler:
report_lines.append(f"Scene {scene_index + 1}: seed={scene_seed}, text={self._scene_text(scene_cond, scene_index)}")
# RETURN_TYPES slot indices: 0=latent, 1=images
# Sampling is fully complete. Latent is untouched and returned as-is.
# IMAGES: decode the whole latent in one pass, then apply transition effects.
want_image = self._output_connected(prompt, unique_id, 1)
# Release all generation-side references so ComfyUI can unload the diffusion
# model and text encoder before VAE decode runs. scene_conditionings is a
# slice of positive; scene_positive/negative are loop-scoped conditioning
# copies — all keep the LTXAVTEModel alive through their embedding tensors.
del model, positive, negative, scene_conditionings, scene_cond, scene_positive, scene_negative, chunk, sampled
# Scene boundaries for per-scene decode: each entry's boundary_latent is
# where the next scene's new content starts in the concatenated latent.
scene_boundaries = [int(e["boundary_latent"]) for e in boundary_entries] if boundary_entries else None
images = None
if want_image:
# Decode each scene slice independently so the VAE's temporal attention
# never crosses a seam — full-video decode spreads seam artifacts forward.
# Transitions are pure post-process on the concatenated pixel frames.
decoded = self._decode_video(output, vae, decode_tile_size, scene_boundaries)
if boundary_entries:
images = self._apply_transitions_pixel(decoded, boundary_entries, transition_duration)
video_tensor = self._latent_tensors(output)[0]
if decode_tile_size > 0:
try:
decoded = vae.decode_tiled(video_tensor, tile_x=decode_tile_size // 8, tile_y=decode_tile_size // 8)
except Exception:
decoded = vae.decode(video_tensor)
else:
images = decoded
decoded = vae.decode(video_tensor)
if decoded.dim() == 5:
b, t, h, w, c = decoded.shape
decoded = decoded.reshape(b * t, h, w, c)
images = self._apply_transitions_pixel(decoded, boundary_entries, transition_duration)
if images is None:
images = torch.zeros(1, 8, 8, 3) # placeholder when IMAGE not connected
images = torch.zeros(1, 8, 8, 3)
import json as _json
final_frames = self._tensor_frames(self._latent_tensors(output)[0])