| import logging
|
|
|
| import torch
|
|
|
| from .nodes_registry import comfy_node
|
|
|
|
|
| @comfy_node(
|
| name="LTXVTiledVAEDecode",
|
| )
|
| class LTXVTiledVAEDecode:
|
|
|
| @classmethod
|
| def INPUT_TYPES(s):
|
| return {
|
| "required": {
|
| "vae": ("VAE",),
|
| "latents": ("LATENT",),
|
| "horizontal_tiles": ("INT", {"default": 1, "min": 1, "max": 6}),
|
| "vertical_tiles": ("INT", {"default": 1, "min": 1, "max": 6}),
|
| "overlap": ("INT", {"default": 1, "min": 1, "max": 8}),
|
| "last_frame_fix": ("BOOLEAN", {"default": False}),
|
| },
|
| "optional": {
|
| "working_device": (["cpu", "auto"], {"default": "auto"}),
|
| "working_dtype": (["float16", "float32", "auto"], {"default": "auto"}),
|
| },
|
| }
|
|
|
| RETURN_TYPES = ("IMAGE",)
|
| RETURN_NAMES = ("image",)
|
|
|
| FUNCTION = "decode"
|
|
|
| CATEGORY = "latent"
|
|
|
| def decode(
|
| self,
|
| vae,
|
| latents,
|
| horizontal_tiles,
|
| vertical_tiles,
|
| overlap,
|
| last_frame_fix,
|
| working_device="auto",
|
| working_dtype="auto",
|
| ):
|
|
|
| samples = latents["samples"]
|
|
|
| if last_frame_fix:
|
|
|
|
|
| last_frame = samples[
|
| :, :, -1:, :, :
|
| ]
|
| samples = torch.cat([samples, last_frame], dim=2)
|
|
|
| batch, channels, frames, height, width = samples.shape
|
| time_scale_factor, width_scale_factor, height_scale_factor = (
|
| vae.downscale_index_formula
|
| )
|
| image_frames = 1 + (frames - 1) * time_scale_factor
|
|
|
|
|
| output_height = height * height_scale_factor
|
| output_width = width * width_scale_factor
|
|
|
|
|
| base_tile_height = (height + (vertical_tiles - 1) * overlap) // vertical_tiles
|
| base_tile_width = (width + (horizontal_tiles - 1) * overlap) // horizontal_tiles
|
|
|
|
|
|
|
| output = None
|
| weights = None
|
|
|
| target_device = samples.device if working_device == "auto" else working_device
|
| if working_dtype == "auto":
|
| target_dtype = samples.dtype
|
| elif working_dtype == "float16":
|
| target_dtype = torch.float16
|
| elif working_dtype == "float32":
|
| target_dtype = torch.float32
|
|
|
| output = torch.zeros(
|
| (
|
| batch,
|
| image_frames,
|
| output_height,
|
| output_width,
|
| 3,
|
| ),
|
| device=target_device,
|
| dtype=target_dtype,
|
| )
|
| weights = torch.zeros(
|
| (batch, image_frames, output_height, output_width, 1),
|
| device=target_device,
|
| dtype=target_dtype,
|
| )
|
|
|
|
|
| for v in range(vertical_tiles):
|
| for h in range(horizontal_tiles):
|
|
|
| h_start = h * (base_tile_width - overlap)
|
| v_start = v * (base_tile_height - overlap)
|
|
|
|
|
| h_end = (
|
| min(h_start + base_tile_width, width)
|
| if h < horizontal_tiles - 1
|
| else width
|
| )
|
| v_end = (
|
| min(v_start + base_tile_height, height)
|
| if v < vertical_tiles - 1
|
| else height
|
| )
|
|
|
|
|
| tile_height = v_end - v_start
|
| tile_width = h_end - h_start
|
|
|
| logging.info(f"Processing VAE decode tile at row {v}, col {h}:")
|
| logging.info(f" Position: ({v_start}:{v_end}, {h_start}:{h_end})")
|
| logging.info(f" Size: {tile_height}x{tile_width}")
|
|
|
|
|
| tile = samples[:, :, :, v_start:v_end, h_start:h_end]
|
|
|
|
|
| tile_latents = {"samples": tile}
|
|
|
|
|
| decoded_tile = vae.decode(tile_latents["samples"])
|
|
|
|
|
| out_h_start = v_start * height_scale_factor
|
| out_h_end = v_end * height_scale_factor
|
| out_w_start = h_start * width_scale_factor
|
| out_w_end = h_end * width_scale_factor
|
|
|
|
|
| tile_out_height = out_h_end - out_h_start
|
| tile_out_width = out_w_end - out_w_start
|
| tile_weights = torch.ones(
|
| (batch, image_frames, tile_out_height, tile_out_width, 1),
|
| device=decoded_tile.device,
|
| dtype=decoded_tile.dtype,
|
| )
|
|
|
|
|
| overlap_out_h = overlap * height_scale_factor
|
| overlap_out_w = overlap * width_scale_factor
|
|
|
|
|
| if h > 0:
|
| h_blend = torch.linspace(
|
| 0, 1, overlap_out_w, device=decoded_tile.device
|
| )
|
| tile_weights[:, :, :, :overlap_out_w, :] *= h_blend.view(
|
| 1, 1, 1, -1, 1
|
| )
|
| if h < horizontal_tiles - 1:
|
| h_blend = torch.linspace(
|
| 1, 0, overlap_out_w, device=decoded_tile.device
|
| )
|
| tile_weights[:, :, :, -overlap_out_w:, :] *= h_blend.view(
|
| 1, 1, 1, -1, 1
|
| )
|
|
|
|
|
| if v > 0:
|
| v_blend = torch.linspace(
|
| 0, 1, overlap_out_h, device=decoded_tile.device
|
| )
|
| tile_weights[:, :, :overlap_out_h, :, :] *= v_blend.view(
|
| 1, 1, -1, 1, 1
|
| )
|
| if v < vertical_tiles - 1:
|
| v_blend = torch.linspace(
|
| 1, 0, overlap_out_h, device=decoded_tile.device
|
| )
|
| tile_weights[:, :, -overlap_out_h:, :, :] *= v_blend.view(
|
| 1, 1, -1, 1, 1
|
| )
|
|
|
|
|
| output[:, :, out_h_start:out_h_end, out_w_start:out_w_end, :] += (
|
| decoded_tile * tile_weights
|
| ).to(target_device, target_dtype)
|
|
|
|
|
| weights[
|
| :, :, out_h_start:out_h_end, out_w_start:out_w_end, :
|
| ] += tile_weights.to(target_device, target_dtype)
|
|
|
|
|
| output /= weights + 1e-8
|
|
|
|
|
| output = output.view(
|
| batch * image_frames, output_height, output_width, output.shape[-1]
|
| )
|
|
|
| if last_frame_fix:
|
| output = output[:-time_scale_factor, :, :]
|
|
|
| return (output,)
|
|
|
|
|
| def compute_chunk_boundaries(
|
| chunk_start: int,
|
| temporal_tile_length: int,
|
| temporal_overlap: int,
|
| total_latent_frames: int,
|
| ):
|
| """Compute chunk boundaries for temporal tiling.
|
|
|
| Args:
|
| chunk_start: Starting frame index for the current chunk
|
| temporal_tile_length: Length of each temporal tile
|
| temporal_overlap: Number of frames to overlap between chunks
|
| total_latent_frames: Total number of latent frames
|
|
|
| Returns:
|
| Tuple of (overlap_start, chunk_end)
|
| """
|
| if chunk_start == 0:
|
|
|
| chunk_end = min(chunk_start + temporal_tile_length, total_latent_frames)
|
| overlap_start = chunk_start
|
| else:
|
|
|
|
|
|
|
| overlap_start = max(1, chunk_start - temporal_overlap - 1)
|
| extra_frames = chunk_start - overlap_start
|
| chunk_end = min(
|
| chunk_start + temporal_tile_length - extra_frames,
|
| total_latent_frames,
|
| )
|
|
|
| return overlap_start, chunk_end
|
|
|
|
|
| def calculate_temporal_output_boundaries(
|
| overlap_start: int, time_scale_factor: int, tile_out_frames: int
|
| ):
|
| """Calculate temporal output boundaries for the decoded tile.
|
|
|
| Args:
|
| overlap_start: Starting frame index including overlap
|
| time_scale_factor: Time scaling factor from VAE
|
| tile_out_frames: Number of frames in the decoded tile
|
|
|
| Returns:
|
| Tuple of (out_t_start, out_t_end)
|
| """
|
|
|
| out_t_start = 1 + overlap_start * time_scale_factor
|
|
|
|
|
| out_t_end = out_t_start + tile_out_frames
|
|
|
| return out_t_start, out_t_end
|
|
|
|
|
| @comfy_node(
|
| name="LTXVSpatioTemporalTiledVAEDecode",
|
| )
|
| class LTXVSpatioTemporalTiledVAEDecode(LTXVTiledVAEDecode):
|
|
|
| @classmethod
|
| def INPUT_TYPES(s):
|
| return {
|
| "required": {
|
| "vae": ("VAE", {"tooltip": "The VAE to use."}),
|
| "latents": ("LATENT", {"tooltip": "The latent samples to decode."}),
|
| "spatial_tiles": (
|
| "INT",
|
| {
|
| "default": 4,
|
| "min": 1,
|
| "max": 8,
|
| "tooltip": "The number of spatial tiles to use, horizontal and vertical.",
|
| },
|
| ),
|
| "spatial_overlap": (
|
| "INT",
|
| {
|
| "default": 1,
|
| "min": 0,
|
| "max": 8,
|
| "tooltip": "The overlap between the spatial tiles. (in latent frames)",
|
| },
|
| ),
|
| "temporal_tile_length": (
|
| "INT",
|
| {
|
| "default": 16,
|
| "min": 2,
|
| "max": 1000,
|
| "tooltip": "The length of the temporal tile to use for the sampling, in latent frames, including the overlapping region.",
|
| },
|
| ),
|
| "temporal_overlap": (
|
| "INT",
|
| {
|
| "default": 1,
|
| "min": 0,
|
| "max": 8,
|
| "tooltip": "The overlap between the temporal tiles, in latent frames.",
|
| },
|
| ),
|
| "last_frame_fix": (
|
| "BOOLEAN",
|
| {
|
| "default": False,
|
| "tooltip": "If true, the last frame will be repeated and discarded after the decoding.",
|
| },
|
| ),
|
| "working_device": (
|
| ["cpu", "auto"],
|
| {
|
| "default": "auto",
|
| "tooltip": "The device to use for the decoding. auto->same as the latents.",
|
| },
|
| ),
|
| "working_dtype": (
|
| ["float16", "float32", "auto"],
|
| {
|
| "default": "auto",
|
| "tooltip": "The data type to use for the decoding. auto->same as the latents.",
|
| },
|
| ),
|
| },
|
| }
|
|
|
| RETURN_TYPES = ("IMAGE",)
|
| RETURN_NAMES = ("image",)
|
|
|
| FUNCTION = "decode_spatial_temporal"
|
|
|
| CATEGORY = "latent"
|
|
|
| def decode_spatial_temporal(
|
| self,
|
| vae,
|
| latents,
|
| spatial_tiles=4,
|
| spatial_overlap=1,
|
| temporal_tile_length=16,
|
| temporal_overlap=1,
|
| last_frame_fix=False,
|
| working_device="auto",
|
| working_dtype="auto",
|
| ):
|
| if temporal_tile_length < temporal_overlap + 1:
|
| raise ValueError(
|
| "Temporal tile length must be greater than temporal overlap + 1"
|
| )
|
|
|
|
|
| samples = latents["samples"]
|
|
|
| batch, channels, frames, height, width = samples.shape
|
| time_scale_factor, width_scale_factor, height_scale_factor = (
|
| vae.downscale_index_formula
|
| )
|
| image_frames = 1 + (frames - 1) * time_scale_factor
|
|
|
|
|
| output_height = height * height_scale_factor
|
| output_width = width * width_scale_factor
|
|
|
| target_device = samples.device if working_device == "auto" else working_device
|
| if working_dtype == "auto":
|
| target_dtype = samples.dtype
|
| elif working_dtype == "float16":
|
| target_dtype = torch.float16
|
| elif working_dtype == "float32":
|
| target_dtype = torch.float32
|
|
|
|
|
| output = torch.empty(
|
| (
|
| batch,
|
| image_frames,
|
| output_height,
|
| output_width,
|
| 3,
|
| ),
|
| device=target_device,
|
| dtype=target_dtype,
|
| )
|
|
|
|
|
| total_latent_frames = frames
|
| chunk_start = 0
|
|
|
| while chunk_start < total_latent_frames:
|
|
|
| overlap_start, chunk_end = compute_chunk_boundaries(
|
| chunk_start, temporal_tile_length, temporal_overlap, total_latent_frames
|
| )
|
|
|
|
|
| chunk_frames = chunk_end - overlap_start
|
| logging.info(
|
| f"Processing temporal chunk: {overlap_start}:{chunk_end} ({chunk_frames} latent frames)"
|
| )
|
|
|
|
|
| tile = samples[:, :, overlap_start:chunk_end]
|
|
|
|
|
| tile_latents = {"samples": tile}
|
|
|
|
|
| decoded_tile = self.decode(
|
| vae=vae,
|
| latents=tile_latents,
|
| vertical_tiles=spatial_tiles,
|
| horizontal_tiles=spatial_tiles,
|
| overlap=spatial_overlap,
|
| last_frame_fix=last_frame_fix,
|
| working_device=working_device,
|
| working_dtype=working_dtype,
|
| )[0][None]
|
|
|
| if chunk_start == 0:
|
| output[:, : decoded_tile.shape[1]] = decoded_tile
|
|
|
|
|
| else:
|
| if decoded_tile.shape[1] == 1:
|
| raise ValueError("Dropping first frame but tile has only 1 frame")
|
| decoded_tile = decoded_tile[:, 1:]
|
|
|
|
|
| out_t_start, out_t_end = calculate_temporal_output_boundaries(
|
| overlap_start, time_scale_factor, decoded_tile.shape[1]
|
| )
|
|
|
|
|
| overlap_frames = temporal_overlap * time_scale_factor
|
| frame_weights = torch.linspace(
|
| 0,
|
| 1,
|
| overlap_frames + 2,
|
| device=decoded_tile.device,
|
| dtype=decoded_tile.dtype,
|
| )[1:-1]
|
| tile_weights = frame_weights.view(1, -1, 1, 1, 1)
|
| after_overlap_frames_start = out_t_start + overlap_frames
|
|
|
| overlap_output = decoded_tile[:, :overlap_frames]
|
| output[:, out_t_start:after_overlap_frames_start] *= 1 - tile_weights
|
| output[:, out_t_start:after_overlap_frames_start] += (
|
| tile_weights * overlap_output
|
| )
|
| output[:, after_overlap_frames_start:out_t_end] = decoded_tile[
|
| :, overlap_frames:
|
| ]
|
|
|
|
|
| chunk_start = chunk_end
|
|
|
|
|
| output = output.view(
|
| batch * image_frames, output_height, output_width, output.shape[-1]
|
| )
|
|
|
| return (output,)
|
|
|