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:
co-authored by
Claude Sonnet 4.6
parent
d9bf41b6dd
commit
fbc6e1c7c0
+14
-57
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user