返回 JoyAI-Echo
video_vae.py
根目录 / ltx-core / src / ltx_core / model / video_vae / video_vae.py
1 import logging
2 from dataclasses import replace
3 from typing import Any, Callable, Iterator, List, Tuple
4
5 import torch
6 from einops import rearrange
7 from torch import nn
8
9 from ltx_core.model.common.normalization import PixelNorm
10 from ltx_core.model.transformer.timestep_embedding import PixArtAlphaCombinedTimestepSizeEmbeddings
11 from ltx_core.model.video_vae.convolution import make_conv_nd
12 from ltx_core.model.video_vae.enums import LogVarianceType, NormLayerType, PaddingModeType
13 from ltx_core.model.video_vae.ops import PerChannelStatistics, patchify, unpatchify
14 from ltx_core.model.video_vae.resnet import ResnetBlock3D, UNetMidBlock3D
15 from ltx_core.model.video_vae.sampling import DepthToSpaceUpsample, SpaceToDepthDownsample
16 from ltx_core.model.video_vae.tiling import (
17 DEFAULT_MAPPING_OPERATION,
18 DEFAULT_SPLIT_OPERATION,
19 DimensionIntervals,
20 MappingOperation,
21 SplitOperation,
22 Tile,
23 TilingConfig,
24 compute_rectangular_mask_1d,
25 compute_trapezoidal_mask_1d,
26 create_tiles,
27 )
28 from ltx_core.types import VIDEO_SCALE_FACTORS, SpatioTemporalScaleFactors, VideoLatentShape
29
30 logger: logging.Logger = logging.getLogger(__name__)
31
32
33 def _make_encoder_block(
34 block_name: str,
35 block_config: dict[str, Any],
36 in_channels: int,
37 convolution_dimensions: int,
38 norm_layer: NormLayerType,
39 norm_num_groups: int,
40 spatial_padding_mode: PaddingModeType,
41 ) -> Tuple[nn.Module, int]:
42 out_channels = in_channels
43
44 if block_name == "res_x":
45 block = UNetMidBlock3D(
46 dims=convolution_dimensions,
47 in_channels=in_channels,
48 num_layers=block_config["num_layers"],
49 resnet_eps=1e-6,
50 resnet_groups=norm_num_groups,
51 norm_layer=norm_layer,
52 spatial_padding_mode=spatial_padding_mode,
53 )
54 elif block_name == "res_x_y":
55 out_channels = in_channels * block_config.get("multiplier", 2)
56 block = ResnetBlock3D(
57 dims=convolution_dimensions,
58 in_channels=in_channels,
59 out_channels=out_channels,
60 eps=1e-6,
61 groups=norm_num_groups,
62 norm_layer=norm_layer,
63 spatial_padding_mode=spatial_padding_mode,
64 )
65 elif block_name == "compress_time":
66 block = make_conv_nd(
67 dims=convolution_dimensions,
68 in_channels=in_channels,
69 out_channels=out_channels,
70 kernel_size=3,
71 stride=(2, 1, 1),
72 causal=True,
73 spatial_padding_mode=spatial_padding_mode,
74 )
75 elif block_name == "compress_space":
76 block = make_conv_nd(
77 dims=convolution_dimensions,
78 in_channels=in_channels,
79 out_channels=out_channels,
80 kernel_size=3,
81 stride=(1, 2, 2),
82 causal=True,
83 spatial_padding_mode=spatial_padding_mode,
84 )
85 elif block_name == "compress_all":
86 block = make_conv_nd(
87 dims=convolution_dimensions,
88 in_channels=in_channels,
89 out_channels=out_channels,
90 kernel_size=3,
91 stride=(2, 2, 2),
92 causal=True,
93 spatial_padding_mode=spatial_padding_mode,
94 )
95 elif block_name == "compress_all_x_y":
96 out_channels = in_channels * block_config.get("multiplier", 2)
97 block = make_conv_nd(
98 dims=convolution_dimensions,
99 in_channels=in_channels,
100 out_channels=out_channels,
101 kernel_size=3,
102 stride=(2, 2, 2),
103 causal=True,
104 spatial_padding_mode=spatial_padding_mode,
105 )
106 elif block_name == "compress_all_res":
107 out_channels = in_channels * block_config.get("multiplier", 2)
108 block = SpaceToDepthDownsample(
109 dims=convolution_dimensions,
110 in_channels=in_channels,
111 out_channels=out_channels,
112 stride=(2, 2, 2),
113 spatial_padding_mode=spatial_padding_mode,
114 )
115 elif block_name == "compress_space_res":
116 out_channels = in_channels * block_config.get("multiplier", 2)
117 block = SpaceToDepthDownsample(
118 dims=convolution_dimensions,
119 in_channels=in_channels,
120 out_channels=out_channels,
121 stride=(1, 2, 2),
122 spatial_padding_mode=spatial_padding_mode,
123 )
124 elif block_name == "compress_time_res":
125 out_channels = in_channels * block_config.get("multiplier", 2)
126 block = SpaceToDepthDownsample(
127 dims=convolution_dimensions,
128 in_channels=in_channels,
129 out_channels=out_channels,
130 stride=(2, 1, 1),
131 spatial_padding_mode=spatial_padding_mode,
132 )
133 else:
134 raise ValueError(f"unknown block: {block_name}")
135
136 return block, out_channels
137
138
139 class VideoEncoder(nn.Module):
140 _DEFAULT_NORM_NUM_GROUPS = 32
141 """
142 Variational Autoencoder Encoder. Encodes video frames into a latent representation.
143 The encoder compresses the input video through a series of downsampling operations controlled by
144 patch_size and encoder_blocks. The output is a normalized latent tensor with shape (B, 128, F', H', W').
145 Compression Behavior:
146 The total compression is determined by:
147 1. Initial spatial compression via patchify: H -> H/4, W -> W/4 (patch_size=4)
148 2. Sequential compression through encoder_blocks based on their stride patterns
149 Compression blocks apply 2x compression in specified dimensions:
150 - "compress_time" / "compress_time_res": temporal only
151 - "compress_space" / "compress_space_res": spatial only (H and W)
152 - "compress_all" / "compress_all_res": all dimensions (F, H, W)
153 - "res_x" / "res_x_y": no compression
154 Standard LTX Video configuration:
155 - patch_size=4
156 - encoder_blocks: 1x compress_space_res, 1x compress_time_res, 2x compress_all_res
157 - Final dimensions: F' = 1 + (F-1)/8, H' = H/32, W' = W/32
158 - Example: (B, 3, 33, 512, 512) -> (B, 128, 5, 16, 16)
159 - Note: Input must have 1 + 8*k frames (e.g., 1, 9, 17, 25, 33...)
160 Args:
161 convolution_dimensions: The number of dimensions to use in convolutions (2D or 3D).
162 in_channels: The number of input channels. For RGB images, this is 3.
163 out_channels: The number of output channels (latent channels). For latent channels, this is 128.
164 encoder_blocks: The list of blocks to construct the encoder. Each block is a tuple of (block_name, params)
165 where params is either an int (num_layers) or a dict with configuration.
166 patch_size: The patch size for initial spatial compression. Should be a power of 2.
167 norm_layer: The normalization layer to use. Can be either `group_norm` or `pixel_norm`.
168 latent_log_var: The log variance mode. Can be either `per_channel`, `uniform`, `constant` or `none`.
169 """
170
171 def __init__(
172 self,
173 convolution_dimensions: int = 3,
174 in_channels: int = 3,
175 out_channels: int = 128,
176 encoder_blocks: List[Tuple[str, int]] | List[Tuple[str, dict[str, Any]]] = [], # noqa: B006
177 patch_size: int = 4,
178 norm_layer: NormLayerType = NormLayerType.PIXEL_NORM,
179 latent_log_var: LogVarianceType = LogVarianceType.UNIFORM,
180 encoder_spatial_padding_mode: PaddingModeType = PaddingModeType.ZEROS,
181 ):
182 super().__init__()
183
184 self.patch_size = patch_size
185 self.norm_layer = norm_layer
186 self.latent_channels = out_channels
187 self.latent_log_var = latent_log_var
188 self._norm_num_groups = self._DEFAULT_NORM_NUM_GROUPS
189
190 # Per-channel statistics for normalizing latents
191 self.per_channel_statistics = PerChannelStatistics(latent_channels=out_channels)
192
193 in_channels = in_channels * patch_size**2
194 feature_channels = out_channels
195
196 self.conv_in = make_conv_nd(
197 dims=convolution_dimensions,
198 in_channels=in_channels,
199 out_channels=feature_channels,
200 kernel_size=3,
201 stride=1,
202 padding=1,
203 causal=True,
204 spatial_padding_mode=encoder_spatial_padding_mode,
205 )
206
207 self.down_blocks = nn.ModuleList([])
208
209 for block_name, block_params in encoder_blocks:
210 # Convert int to dict format for uniform handling
211 block_config = {"num_layers": block_params} if isinstance(block_params, int) else block_params
212
213 block, feature_channels = _make_encoder_block(
214 block_name=block_name,
215 block_config=block_config,
216 in_channels=feature_channels,
217 convolution_dimensions=convolution_dimensions,
218 norm_layer=norm_layer,
219 norm_num_groups=self._norm_num_groups,
220 spatial_padding_mode=encoder_spatial_padding_mode,
221 )
222
223 self.down_blocks.append(block)
224
225 # out
226 if norm_layer == NormLayerType.GROUP_NORM:
227 self.conv_norm_out = nn.GroupNorm(num_channels=feature_channels, num_groups=self._norm_num_groups, eps=1e-6)
228 elif norm_layer == NormLayerType.PIXEL_NORM:
229 self.conv_norm_out = PixelNorm()
230
231 self.conv_act = nn.SiLU()
232
233 conv_out_channels = out_channels
234 if latent_log_var == LogVarianceType.PER_CHANNEL:
235 conv_out_channels *= 2
236 elif latent_log_var in {LogVarianceType.UNIFORM, LogVarianceType.CONSTANT}:
237 conv_out_channels += 1
238 elif latent_log_var != LogVarianceType.NONE:
239 raise ValueError(f"Invalid latent_log_var: {latent_log_var}")
240
241 self.conv_out = make_conv_nd(
242 dims=convolution_dimensions,
243 in_channels=feature_channels,
244 out_channels=conv_out_channels,
245 kernel_size=3,
246 padding=1,
247 causal=True,
248 spatial_padding_mode=encoder_spatial_padding_mode,
249 )
250
251 def forward(self, sample: torch.Tensor) -> torch.Tensor:
252 r"""
253 Encode video frames into normalized latent representation.
254 Args:
255 sample: Input video (B, C, F, H, W). F should be 1 + 8*k (e.g., 1, 9, 17, 25, 33...).
256 If not, the encoder crops the last frames to the nearest valid length.
257 Returns:
258 Normalized latent means (B, 128, F', H', W') where F' = 1+(F-1)/8, H' = H/32, W' = W/32.
259 Example: (B, 3, 33, 512, 512) -> (B, 128, 5, 16, 16).
260 """
261 # Validate frame count (crop to nearest valid length if needed)
262 frames_count = sample.shape[2]
263 if ((frames_count - 1) % 8) != 0:
264 frames_to_crop = (frames_count - 1) % 8
265 logger.warning(
266 "Invalid number of frames %s for encode; cropping last %s frames to satisfy 1 + 8*k.",
267 frames_count,
268 frames_to_crop,
269 )
270 sample = sample[:, :, :-frames_to_crop, ...]
271
272 # Initial spatial compression: trade spatial resolution for channel depth
273 # This reduces H,W by patch_size and increases channels, making convolutions more efficient
274 # Example: (B, 3, F, 512, 512) -> (B, 48, F, 128, 128) with patch_size=4
275 sample = patchify(sample, patch_size_hw=self.patch_size, patch_size_t=1)
276 sample = self.conv_in(sample)
277
278 for down_block in self.down_blocks:
279 sample = down_block(sample)
280
281 sample = self.conv_norm_out(sample)
282 sample = self.conv_act(sample)
283 sample = self.conv_out(sample)
284
285 if self.latent_log_var == LogVarianceType.UNIFORM:
286 # Uniform Variance: model outputs N means and 1 shared log-variance channel.
287 # We need to expand the single logvar to match the number of means channels
288 # to create a format compatible with PER_CHANNEL (means + logvar, each with N channels).
289 # Sample shape: (B, N+1, ...) where N = latent_channels (e.g., 128 means + 1 logvar = 129)
290 # Target shape: (B, 2*N, ...) where first N are means, last N are logvar
291
292 if sample.shape[1] < 2:
293 raise ValueError(
294 f"Invalid channel count for UNIFORM mode: expected at least 2 channels "
295 f"(N means + 1 logvar), got {sample.shape[1]}"
296 )
297
298 # Extract means (first N channels) and logvar (last 1 channel)
299 means = sample[:, :-1, ...] # (B, N, ...)
300 logvar = sample[:, -1:, ...] # (B, 1, ...)
301
302 # Repeat logvar N times to match means channels
303 # Use expand/repeat pattern that works for both 4D and 5D tensors
304 num_channels = means.shape[1]
305 repeat_shape = [1, num_channels] + [1] * (sample.ndim - 2)
306 repeated_logvar = logvar.repeat(*repeat_shape) # (B, N, ...)
307
308 # Concatenate to create (B, 2*N, ...) format: [means, repeated_logvar]
309 sample = torch.cat([means, repeated_logvar], dim=1)
310 elif self.latent_log_var == LogVarianceType.CONSTANT:
311 sample = sample[:, :-1, ...]
312 approx_ln_0 = -30 # this is the minimal clamp value in DiagonalGaussianDistribution objects
313 sample = torch.cat(
314 [sample, torch.ones_like(sample, device=sample.device) * approx_ln_0],
315 dim=1,
316 )
317
318 # Split into means and logvar, then normalize means
319 means, _ = torch.chunk(sample, 2, dim=1)
320 return self.per_channel_statistics.normalize(means)
321
322 def tiled_encode(
323 self,
324 video: torch.Tensor,
325 tiling_config: TilingConfig | None = None,
326 ) -> torch.Tensor:
327 """Encode video to latent using tiled processing of the given video tensor.
328 Device Handling:
329 - Input video can be on CPU or GPU
330 - Accumulation buffers are created on model's device
331 - Each tile is automatically moved to model's device before encoding
332 - Output latent is returned on model's device
333 Args:
334 video: Input video tensor (B, 3, F, H, W) in range [-1, 1]
335 tiling_config: Tiling configuration for the video tensor
336 Returns:
337 Latent tensor (B, 128, F', H', W') on model's device
338 where F' = 1 + (F-1)/8, H' = H/32, W' = W/32
339 """
340 # Detect model device and dtype
341 model_device = next(self.parameters()).device
342 model_dtype = next(self.parameters()).dtype
343
344 # Extract shape components
345 batch, _, frames, height, width = video.shape
346
347 # Check frame count and crop if needed
348 if (frames - 1) % VIDEO_SCALE_FACTORS.time != 0:
349 frames_to_crop = (frames - 1) % VIDEO_SCALE_FACTORS.time
350 logger.warning(
351 f"Number of frames {frames} of input video is not ({VIDEO_SCALE_FACTORS.time} * k + 1), "
352 f"last {frames_to_crop} frames will be cropped"
353 )
354 video = video[:, :, :-frames_to_crop, ...]
355 # Update frames after cropping
356 frames = video.shape[2]
357
358 # Calculate output latent shape (inverse of upscale)
359 latent_shape = VideoLatentShape(
360 batch=batch,
361 channels=self.latent_channels, # 128 for standard VAE
362 frames=(frames - 1) // VIDEO_SCALE_FACTORS.time + 1,
363 height=height // VIDEO_SCALE_FACTORS.height,
364 width=width // VIDEO_SCALE_FACTORS.width,
365 )
366
367 # Prepare tiles (operates on VIDEO dimensions)
368 tiles = prepare_tiles_for_encoding(video, tiling_config)
369
370 # Initialize accumulation buffers on model device
371 latent_buffer = torch.zeros(
372 latent_shape.to_torch_shape(),
373 device=model_device,
374 dtype=model_dtype,
375 )
376 weights_buffer = torch.zeros_like(latent_buffer)
377
378 # Process each tile
379 for tile in tiles:
380 # Extract video tile from input (may be on CPU)
381 video_tile = video[tile.in_coords]
382
383 # Move tile to model device if needed
384 if video_tile.device != model_device or video_tile.dtype != model_dtype:
385 video_tile = video_tile.to(device=model_device, dtype=model_dtype)
386
387 # Encode tile to latent (output on model device)
388 latent_tile = self.forward(video_tile)
389
390 # Move blend mask to model device
391 mask = tile.blend_mask.to(
392 device=model_device,
393 dtype=model_dtype,
394 )
395
396 # Weighted accumulation in latent space
397 latent_buffer[tile.out_coords] += latent_tile * mask
398 weights_buffer[tile.out_coords] += mask
399
400 del latent_tile, mask, video_tile
401
402 # Normalize by accumulated weights
403 weights_buffer = weights_buffer.clamp(min=1e-8)
404 return latent_buffer / weights_buffer
405
406
407 def prepare_tiles_for_encoding(
408 video: torch.Tensor,
409 tiling_config: TilingConfig | None = None,
410 ) -> List[Tile]:
411 """Prepare tiles for VAE encoding.
412 Args:
413 video: Input video tensor (B, 3, F, H, W) in range [-1, 1]
414 tiling_config: Tiling configuration for the video tensor
415 Returns:
416 List of tiles for the video tensor
417 """
418
419 splitters = [DEFAULT_SPLIT_OPERATION] * len(video.shape)
420 mappers = [DEFAULT_MAPPING_OPERATION] * len(video.shape)
421 minimum_spatial_overlap_px = 64
422 minimum_temporal_overlap_frames = 16
423
424 if tiling_config is not None and tiling_config.spatial_config is not None:
425 cfg = tiling_config.spatial_config
426
427 tile_size_px = cfg.tile_size_in_pixels
428 overlap_px = cfg.tile_overlap_in_pixels
429
430 # Set minimum spatial overlap to 64 pixels in order to allow cutting padding from
431 # the front and back of the tiles and concatenate tiles without artifacts.
432 # The encoder uses symmetric padding (pad=1) in H and W at each conv layer. At tile
433 # boundaries, convs see padding (zeros/reflect) instead of real neighbor pixels, causing
434 # incorrect context near edges.
435 # For each overlap we discard 1 latent per edge (32px at scale 32) and concatenate tiles at a
436 # shared region with the next tile.
437 if overlap_px < minimum_spatial_overlap_px:
438 logger.warning(
439 f"Overlap pixels {overlap_px} in spatial tiling is less than \
440 {minimum_spatial_overlap_px}, setting to minimum required {minimum_spatial_overlap_px}"
441 )
442 overlap_px = minimum_spatial_overlap_px
443
444 # Define split and map operations for the spatial dimensions
445
446 # Height axis (H)
447 splitters[3] = split_with_symmetric_overlaps(tile_size_px, overlap_px)
448 mappers[3] = make_mapping_operation(map_spatial_interval_to_latent, scale=VIDEO_SCALE_FACTORS.height)
449
450 # Width axis (W)
451 splitters[4] = split_with_symmetric_overlaps(tile_size_px, overlap_px)
452 mappers[4] = make_mapping_operation(map_spatial_interval_to_latent, scale=VIDEO_SCALE_FACTORS.width)
453
454 if tiling_config is not None and tiling_config.temporal_config is not None:
455 cfg = tiling_config.temporal_config
456 tile_size_frames = cfg.tile_size_in_frames
457 overlap_frames = cfg.tile_overlap_in_frames
458
459 if overlap_frames < minimum_temporal_overlap_frames:
460 logger.warning(f"Overlap frames {overlap_frames} is less than 16, setting to minimum required 16")
461 overlap_frames = minimum_temporal_overlap_frames
462
463 splitters[2] = split_temporal_frames(tile_size_frames, overlap_frames)
464 mappers[2] = make_mapping_operation(map_temporal_interval_to_latent, scale=VIDEO_SCALE_FACTORS.time)
465
466 return create_tiles(video.shape, splitters, mappers)
467
468
469 def _make_decoder_block(
470 block_name: str,
471 block_config: dict[str, Any],
472 in_channels: int,
473 convolution_dimensions: int,
474 norm_layer: NormLayerType,
475 timestep_conditioning: bool,
476 norm_num_groups: int,
477 spatial_padding_mode: PaddingModeType,
478 ) -> Tuple[nn.Module, int]:
479 out_channels = in_channels
480 if block_name == "res_x":
481 block = UNetMidBlock3D(
482 dims=convolution_dimensions,
483 in_channels=in_channels,
484 num_layers=block_config["num_layers"],
485 resnet_eps=1e-6,
486 resnet_groups=norm_num_groups,
487 norm_layer=norm_layer,
488 inject_noise=block_config.get("inject_noise", False),
489 timestep_conditioning=timestep_conditioning,
490 spatial_padding_mode=spatial_padding_mode,
491 )
492 elif block_name == "attn_res_x":
493 block = UNetMidBlock3D(
494 dims=convolution_dimensions,
495 in_channels=in_channels,
496 num_layers=block_config["num_layers"],
497 resnet_groups=norm_num_groups,
498 norm_layer=norm_layer,
499 inject_noise=block_config.get("inject_noise", False),
500 timestep_conditioning=timestep_conditioning,
501 attention_head_dim=block_config["attention_head_dim"],
502 spatial_padding_mode=spatial_padding_mode,
503 )
504 elif block_name == "res_x_y":
505 out_channels = in_channels // block_config.get("multiplier", 2)
506 block = ResnetBlock3D(
507 dims=convolution_dimensions,
508 in_channels=in_channels,
509 out_channels=out_channels,
510 eps=1e-6,
511 groups=norm_num_groups,
512 norm_layer=norm_layer,
513 inject_noise=block_config.get("inject_noise", False),
514 timestep_conditioning=False,
515 spatial_padding_mode=spatial_padding_mode,
516 )
517 elif block_name == "compress_time":
518 out_channels = in_channels // block_config.get("multiplier", 1)
519 block = DepthToSpaceUpsample(
520 dims=convolution_dimensions,
521 in_channels=in_channels,
522 stride=(2, 1, 1),
523 out_channels_reduction_factor=block_config.get("multiplier", 1),
524 spatial_padding_mode=spatial_padding_mode,
525 )
526 elif block_name == "compress_space":
527 out_channels = in_channels // block_config.get("multiplier", 1)
528 block = DepthToSpaceUpsample(
529 dims=convolution_dimensions,
530 in_channels=in_channels,
531 stride=(1, 2, 2),
532 out_channels_reduction_factor=block_config.get("multiplier", 1),
533 spatial_padding_mode=spatial_padding_mode,
534 )
535 elif block_name == "compress_all":
536 out_channels = in_channels // block_config.get("multiplier", 1)
537 block = DepthToSpaceUpsample(
538 dims=convolution_dimensions,
539 in_channels=in_channels,
540 stride=(2, 2, 2),
541 residual=block_config.get("residual", False),
542 out_channels_reduction_factor=block_config.get("multiplier", 1),
543 spatial_padding_mode=spatial_padding_mode,
544 )
545 else:
546 raise ValueError(f"unknown layer: {block_name}")
547
548 return block, out_channels
549
550
551 class VideoDecoder(nn.Module):
552 _DEFAULT_NORM_NUM_GROUPS = 32
553 """
554 Variational Autoencoder Decoder. Decodes latent representation into video frames.
555 The decoder upsamples latents through a series of upsampling operations (inverse of encoder).
556 Output dimensions: F = 8x(F'-1) + 1, H = 32xH', W = 32xW' for standard LTX Video configuration.
557 Upsampling blocks expand dimensions by 2x in specified dimensions:
558 - "compress_time": temporal only
559 - "compress_space": spatial only (H and W)
560 - "compress_all": all dimensions (F, H, W)
561 - "res_x" / "res_x_y" / "attn_res_x": no upsampling
562 Causal Mode:
563 causal=False (standard): Symmetric padding, allows future frame dependencies.
564 causal=True: Causal padding, each frame depends only on past/current frames.
565 First frame removed after temporal upsampling in both modes. Output shape unchanged.
566 Example: (B, 128, 5, 16, 16) -> (B, 3, 33, 512, 512) for both modes.
567 Args:
568 convolution_dimensions: The number of dimensions to use in convolutions (2D or 3D).
569 in_channels: The number of input channels (latent channels). Default is 128.
570 out_channels: The number of output channels. For RGB images, this is 3.
571 decoder_blocks: The list of blocks to construct the decoder. Each block is a tuple of (block_name, params)
572 where params is either an int (num_layers) or a dict with configuration.
573 patch_size: Final spatial expansion factor. For standard LTX Video, use 4 for 4x spatial expansion:
574 H -> Hx4, W -> Wx4. Should be a power of 2.
575 norm_layer: The normalization layer to use. Can be either `group_norm` or `pixel_norm`.
576 causal: Whether to use causal convolutions. For standard LTX Video, use False for symmetric padding.
577 When True, uses causal padding (past/current frames only).
578 timestep_conditioning: Whether to condition the decoder on timestep for denoising.
579 """
580
581 def __init__(
582 self,
583 convolution_dimensions: int = 3,
584 in_channels: int = 128,
585 out_channels: int = 3,
586 decoder_blocks: List[Tuple[str, int | dict]] = [], # noqa: B006
587 patch_size: int = 4,
588 norm_layer: NormLayerType = NormLayerType.PIXEL_NORM,
589 causal: bool = False,
590 timestep_conditioning: bool = False,
591 decoder_spatial_padding_mode: PaddingModeType = PaddingModeType.REFLECT,
592 base_channels: int = 128,
593 ):
594 super().__init__()
595
596 # Spatiotemporal downscaling between decoded video space and VAE latents.
597 # According to the LTXV paper, the standard configuration downsamples
598 # video inputs by a factor of 8 in the temporal dimension and 32 in
599 # each spatial dimension (height and width). This parameter determines how
600 # many video frames and pixels correspond to a single latent cell.
601 self.video_downscale_factors = SpatioTemporalScaleFactors(
602 time=8,
603 width=32,
604 height=32,
605 )
606
607 self.patch_size = patch_size
608 out_channels = out_channels * patch_size**2
609 self.causal = causal
610 self.timestep_conditioning = timestep_conditioning
611 self._norm_num_groups = self._DEFAULT_NORM_NUM_GROUPS
612
613 # Per-channel statistics for denormalizing latents
614 self.per_channel_statistics = PerChannelStatistics(latent_channels=in_channels)
615
616 # Noise and timestep parameters for decoder conditioning
617 self.decode_noise_scale = 0.025
618 self.decode_timestep = 0.05
619
620 # LTX VAE decoder architecture uses 3 upsampler blocks with multiplier equals to 2.
621 # Hence the total feature_channels is multiplied by 8 (2^3).
622 feature_channels = base_channels * 8
623
624 self.conv_in = make_conv_nd(
625 dims=convolution_dimensions,
626 in_channels=in_channels,
627 out_channels=feature_channels,
628 kernel_size=3,
629 stride=1,
630 padding=1,
631 causal=True,
632 spatial_padding_mode=decoder_spatial_padding_mode,
633 )
634
635 self.up_blocks = nn.ModuleList([])
636
637 for block_name, block_params in list(reversed(decoder_blocks)):
638 # Convert int to dict format for uniform handling
639 block_config = {"num_layers": block_params} if isinstance(block_params, int) else block_params
640
641 block, feature_channels = _make_decoder_block(
642 block_name=block_name,
643 block_config=block_config,
644 in_channels=feature_channels,
645 convolution_dimensions=convolution_dimensions,
646 norm_layer=norm_layer,
647 timestep_conditioning=timestep_conditioning,
648 norm_num_groups=self._norm_num_groups,
649 spatial_padding_mode=decoder_spatial_padding_mode,
650 )
651
652 self.up_blocks.append(block)
653
654 if norm_layer == NormLayerType.GROUP_NORM:
655 self.conv_norm_out = nn.GroupNorm(num_channels=feature_channels, num_groups=self._norm_num_groups, eps=1e-6)
656 elif norm_layer == NormLayerType.PIXEL_NORM:
657 self.conv_norm_out = PixelNorm()
658
659 self.conv_act = nn.SiLU()
660 self.conv_out = make_conv_nd(
661 dims=convolution_dimensions,
662 in_channels=feature_channels,
663 out_channels=out_channels,
664 kernel_size=3,
665 padding=1,
666 causal=True,
667 spatial_padding_mode=decoder_spatial_padding_mode,
668 )
669
670 if timestep_conditioning:
671 self.timestep_scale_multiplier = nn.Parameter(torch.tensor(1000.0))
672 self.last_time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(
673 embedding_dim=feature_channels * 2, size_emb_dim=0
674 )
675 self.last_scale_shift_table = nn.Parameter(torch.empty(2, feature_channels))
676
677 def forward(
678 self,
679 sample: torch.Tensor,
680 timestep: torch.Tensor | None = None,
681 generator: torch.Generator | None = None,
682 ) -> torch.Tensor:
683 r"""
684 Decode latent representation into video frames.
685 Args:
686 sample: Latent tensor (B, 128, F', H', W').
687 timestep: Timestep for conditioning (if timestep_conditioning=True). Uses default 0.05 if None.
688 generator: Random generator for deterministic noise injection (if inject_noise=True in blocks).
689 Returns:
690 Decoded video (B, 3, F, H, W) where F = 8x(F'-1) + 1, H = 32xH', W = 32xW'.
691 Example: (B, 128, 5, 16, 16) -> (B, 3, 33, 512, 512).
692 Note: First frame is removed after temporal upsampling regardless of causal mode.
693 When causal=False, allows future frame dependencies in convolutions but maintains same output shape.
694 """
695 batch_size = sample.shape[0]
696
697 # Add noise if timestep conditioning is enabled
698 if self.timestep_conditioning:
699 noise = (
700 torch.randn(
701 sample.size(),
702 generator=generator,
703 dtype=sample.dtype,
704 device=sample.device,
705 )
706 * self.decode_noise_scale
707 )
708
709 sample = noise + (1.0 - self.decode_noise_scale) * sample
710
711 # Denormalize latents
712 sample = self.per_channel_statistics.un_normalize(sample)
713
714 # Use default decode_timestep if timestep not provided
715 if timestep is None and self.timestep_conditioning:
716 timestep = torch.full((batch_size,), self.decode_timestep, device=sample.device, dtype=sample.dtype)
717
718 sample = self.conv_in(sample, causal=self.causal)
719
720 scaled_timestep = None
721 if self.timestep_conditioning:
722 if timestep is None:
723 raise ValueError("'timestep' parameter must be provided when 'timestep_conditioning' is True")
724 scaled_timestep = timestep * self.timestep_scale_multiplier.to(sample)
725
726 for up_block in self.up_blocks:
727 if isinstance(up_block, UNetMidBlock3D):
728 block_kwargs = {
729 "causal": self.causal,
730 "timestep": scaled_timestep if self.timestep_conditioning else None,
731 "generator": generator,
732 }
733 sample = up_block(sample, **block_kwargs)
734 elif isinstance(up_block, ResnetBlock3D):
735 sample = up_block(sample, causal=self.causal, generator=generator)
736 else:
737 sample = up_block(sample, causal=self.causal)
738
739 sample = self.conv_norm_out(sample)
740
741 if self.timestep_conditioning:
742 embedded_timestep = self.last_time_embedder(
743 timestep=scaled_timestep.flatten(),
744 hidden_dtype=sample.dtype,
745 )
746 embedded_timestep = embedded_timestep.view(batch_size, embedded_timestep.shape[-1], 1, 1, 1)
747 ada_values = self.last_scale_shift_table[None, ..., None, None, None].to(
748 device=sample.device, dtype=sample.dtype
749 ) + embedded_timestep.reshape(
750 batch_size,
751 2,
752 -1,
753 embedded_timestep.shape[-3],
754 embedded_timestep.shape[-2],
755 embedded_timestep.shape[-1],
756 )
757 shift, scale = ada_values.unbind(dim=1)
758 sample = sample * (1 + scale) + shift
759
760 sample = self.conv_act(sample)
761 sample = self.conv_out(sample, causal=self.causal)
762
763 # Final spatial expansion: reverse the initial patchify from encoder
764 # Moves pixels from channels back to spatial dimensions
765 # Example: (B, 48, F, 128, 128) -> (B, 3, F, 512, 512) with patch_size=4
766 sample = unpatchify(sample, patch_size_hw=self.patch_size, patch_size_t=1)
767
768 return sample
769
770 def _prepare_tiles(
771 self,
772 latent: torch.Tensor,
773 tiling_config: TilingConfig | None = None,
774 ) -> List[Tile]:
775 splitters = [DEFAULT_SPLIT_OPERATION] * len(latent.shape)
776 mappers = [DEFAULT_MAPPING_OPERATION] * len(latent.shape)
777 if tiling_config is not None and tiling_config.spatial_config is not None:
778 cfg = tiling_config.spatial_config
779 long_side = max(latent.shape[3], latent.shape[4])
780
781 def enable_on_axis(axis_idx: int, factor: int) -> None:
782 size = cfg.tile_size_in_pixels // factor
783 overlap = cfg.tile_overlap_in_pixels // factor
784 axis_length = latent.shape[axis_idx]
785 lower_threshold = max(2, overlap + 1)
786 tile_size = max(lower_threshold, round(size * axis_length / long_side))
787 splitters[axis_idx] = split_with_symmetric_overlaps(tile_size, overlap)
788 mappers[axis_idx] = make_mapping_operation(map_spatial_interval_to_pixel, scale=factor)
789
790 enable_on_axis(3, self.video_downscale_factors.height)
791 enable_on_axis(4, self.video_downscale_factors.width)
792
793 if tiling_config is not None and tiling_config.temporal_config is not None:
794 cfg = tiling_config.temporal_config
795 tile_size = cfg.tile_size_in_frames // self.video_downscale_factors.time
796 overlap = cfg.tile_overlap_in_frames // self.video_downscale_factors.time
797 splitters[2] = split_temporal_latents(tile_size, overlap)
798 mappers[2] = make_mapping_operation(map_temporal_interval_to_frame, scale=self.video_downscale_factors.time)
799
800 return create_tiles(latent.shape, splitters, mappers)
801
802 def tiled_decode(
803 self,
804 latent: torch.Tensor,
805 tiling_config: TilingConfig | None = None,
806 timestep: torch.Tensor | None = None,
807 generator: torch.Generator | None = None,
808 ) -> Iterator[torch.Tensor]:
809 """
810 Decode a latent tensor into video frames using tiled processing.
811 Splits the latent tensor into tiles, decodes each tile individually,
812 and yields video chunks as they become available.
813 Args:
814 latent: Input latent tensor (B, C, F', H', W').
815 tiling_config: Tiling configuration for the latent tensor.
816 timestep: Optional timestep for decoder conditioning.
817 generator: Optional random generator for deterministic decoding.
818 Yields:
819 Video chunks (B, C, T, H, W) by temporal slices;
820 """
821
822 # Calculate full video shape from latent shape to get spatial dimensions
823 full_video_shape = VideoLatentShape.from_torch_shape(latent.shape).upscale(self.video_downscale_factors)
824 tiles = self._prepare_tiles(latent, tiling_config)
825
826 temporal_groups = self._group_tiles_by_temporal_slice(tiles)
827
828 # State for temporal overlap handling
829 previous_chunk = None
830 previous_weights = None
831 previous_temporal_slice = None
832
833 for temporal_group_tiles in temporal_groups:
834 curr_temporal_slice = temporal_group_tiles[0].out_coords[2]
835
836 # Calculate the shape of the temporal buffer for this group of tiles.
837 # The temporal length depends on whether this is the first tile (starts at 0) or not.
838 # - First tile: (frames - 1) * scale + 1
839 # - Subsequent tiles: frames * scale
840 # This logic is handled by TemporalAxisMapping and reflected in out_coords.
841 temporal_tile_buffer_shape = full_video_shape._replace(
842 frames=curr_temporal_slice.stop - curr_temporal_slice.start,
843 )
844
845 buffer = torch.zeros(
846 temporal_tile_buffer_shape.to_torch_shape(),
847 device=latent.device,
848 dtype=latent.dtype,
849 )
850
851 curr_weights = self._accumulate_temporal_group_into_buffer(
852 group_tiles=temporal_group_tiles,
853 buffer=buffer,
854 latent=latent,
855 timestep=timestep,
856 generator=generator,
857 )
858
859 # Blend with previous temporal chunk if it exists
860 if previous_chunk is not None:
861 # Check if current temporal slice overlaps with previous temporal slice
862 if previous_temporal_slice.stop > curr_temporal_slice.start:
863 overlap_len = previous_temporal_slice.stop - curr_temporal_slice.start
864 temporal_overlap_slice = slice(curr_temporal_slice.start - previous_temporal_slice.start, None)
865
866 # The overlap is already masked before it reaches this step. Each tile is accumulated into buffer
867 # with its trapezoidal mask, and curr_weights accumulates the same mask. In the overlap blend we add
868 # the masked values (buffer[...]) and the corresponding weights (curr_weights[...]) into the
869 # previous buffers, then later normalize by weights.
870 previous_chunk[:, :, temporal_overlap_slice, :, :] += buffer[:, :, slice(0, overlap_len), :, :]
871 previous_weights[:, :, temporal_overlap_slice, :, :] += curr_weights[
872 :, :, slice(0, overlap_len), :, :
873 ]
874
875 buffer[:, :, slice(0, overlap_len), :, :] = previous_chunk[:, :, temporal_overlap_slice, :, :]
876 curr_weights[:, :, slice(0, overlap_len), :, :] = previous_weights[
877 :, :, temporal_overlap_slice, :, :
878 ]
879
880 # Yield the non-overlapping part of the previous chunk
881 previous_weights = previous_weights.clamp(min=1e-8)
882 yield_len = curr_temporal_slice.start - previous_temporal_slice.start
883 yield (previous_chunk / previous_weights)[:, :, :yield_len, :, :]
884
885 # Update state for next iteration
886 previous_chunk = buffer
887 previous_weights = curr_weights
888 previous_temporal_slice = curr_temporal_slice
889
890 # Yield any remaining chunk
891 if previous_chunk is not None:
892 previous_weights = previous_weights.clamp(min=1e-8)
893 yield previous_chunk / previous_weights
894
895 def _group_tiles_by_temporal_slice(self, tiles: List[Tile]) -> List[List[Tile]]:
896 """Group tiles by their temporal output slice."""
897 if not tiles:
898 return []
899
900 groups = []
901 current_slice = tiles[0].out_coords[2]
902 current_group = []
903
904 for tile in tiles:
905 tile_slice = tile.out_coords[2]
906 if tile_slice == current_slice:
907 current_group.append(tile)
908 else:
909 groups.append(current_group)
910 current_slice = tile_slice
911 current_group = [tile]
912
913 # Add the final group
914 if current_group:
915 groups.append(current_group)
916
917 return groups
918
919 def _accumulate_temporal_group_into_buffer(
920 self,
921 group_tiles: List[Tile],
922 buffer: torch.Tensor,
923 latent: torch.Tensor,
924 timestep: torch.Tensor | None,
925 generator: torch.Generator | None,
926 ) -> torch.Tensor:
927 """
928 Decode and accumulate all tiles of a temporal group into a local buffer.
929 The buffer is local to the group and always starts at time 0; temporal coordinates
930 are rebased by subtracting temporal_slice.start.
931 """
932 temporal_slice = group_tiles[0].out_coords[2]
933
934 weights = torch.zeros_like(buffer)
935
936 for tile in group_tiles:
937 decoded_tile = self.forward(latent[tile.in_coords], timestep, generator)
938 mask = tile.blend_mask.to(device=buffer.device, dtype=buffer.dtype)
939 temporal_offset = tile.out_coords[2].start - temporal_slice.start
940 # Use the tile's output coordinate length, not the decoded tile's length,
941 # as the decoder may produce a different number of frames than expected
942 expected_temporal_len = tile.out_coords[2].stop - tile.out_coords[2].start
943 decoded_temporal_len = decoded_tile.shape[2]
944
945 # Ensure we don't exceed the buffer or decoded tile bounds
946 actual_temporal_len = min(expected_temporal_len, decoded_temporal_len, buffer.shape[2] - temporal_offset)
947
948 chunk_coords = (
949 slice(None), # batch
950 slice(None), # channels
951 slice(temporal_offset, temporal_offset + actual_temporal_len),
952 tile.out_coords[3], # height
953 tile.out_coords[4], # width
954 )
955
956 # Slice decoded_tile and mask to match the actual length we're writing
957 decoded_slice = decoded_tile[:, :, :actual_temporal_len, :, :]
958 mask_slice = mask[:, :, :actual_temporal_len, :, :] if mask.shape[2] > 1 else mask
959
960 buffer[chunk_coords] += decoded_slice * mask_slice
961 weights[chunk_coords] += mask_slice
962
963 return weights
964
965
966 def decode_video(
967 latent: torch.Tensor,
968 video_decoder: VideoDecoder,
969 tiling_config: TilingConfig | None = None,
970 generator: torch.Generator | None = None,
971 ) -> Iterator[torch.Tensor]:
972 """
973 Decode a video latent tensor with the given decoder.
974 Args:
975 latent: Tensor [c, f, h, w]
976 video_decoder: Decoder module.
977 tiling_config: Optional tiling settings.
978 generator: Optional random generator for deterministic decoding.
979 Yields:
980 Decoded chunk [f, h, w, c], uint8 in [0, 255].
981 """
982
983 def convert_to_uint8(frames: torch.Tensor) -> torch.Tensor:
984 frames = (((frames + 1.0) / 2.0).clamp(0.0, 1.0) * 255.0).to(torch.uint8)
985 frames = rearrange(frames[0], "c f h w -> f h w c")
986 return frames
987
988 if tiling_config is not None:
989 for frames in video_decoder.tiled_decode(latent, tiling_config, generator=generator):
990 yield convert_to_uint8(frames)
991 else:
992 decoded_video = video_decoder(latent, generator=generator)
993 yield convert_to_uint8(decoded_video)
994
995
996 def get_video_chunks_number(num_frames: int, tiling_config: TilingConfig | None = None) -> int:
997 """
998 Get the number of video chunks for a given number of frames and tiling configuration.
999 Args:
1000 num_frames: Number of frames in the video.
1001 tiling_config: Tiling configuration.
1002 Returns:
1003 Number of video chunks.
1004 """
1005 if not tiling_config or not tiling_config.temporal_config:
1006 return 1
1007 cfg = tiling_config.temporal_config
1008 frame_stride = cfg.tile_size_in_frames - cfg.tile_overlap_in_frames
1009 return (num_frames - 1 + frame_stride - 1) // frame_stride
1010
1011
1012 def split_with_symmetric_overlaps(size: int, overlap: int) -> SplitOperation:
1013 def split(dimension_size: int) -> DimensionIntervals:
1014 if dimension_size <= size:
1015 return DEFAULT_SPLIT_OPERATION(dimension_size)
1016 amount = (dimension_size + size - 2 * overlap - 1) // (size - overlap)
1017 starts = [i * (size - overlap) for i in range(amount)]
1018 ends = [start + size for start in starts]
1019 ends[-1] = dimension_size
1020 left_ramps = [0] + [overlap] * (amount - 1)
1021 right_ramps = [overlap] * (amount - 1) + [0]
1022 return DimensionIntervals(starts=starts, ends=ends, left_ramps=left_ramps, right_ramps=right_ramps)
1023
1024 return split
1025
1026
1027 def split_temporal_latents(size: int, overlap: int) -> SplitOperation:
1028 """Split a temporal axis into overlapping tiles with causal handling.
1029 Example with size=24, overlap=8 (units are whatever axis you split):
1030 Non-causal split would produce:
1031 Tile 0: [0, 24), left_ramp=0, right_ramp=8
1032 Tile 1: [16, 40), left_ramp=8, right_ramp=8
1033 Tile 2: [32, 56), left_ramp=8, right_ramp=0
1034 Causal split produces:
1035 Tile 0: [0, 24), left_ramp=0, right_ramp=8 (unchanged - starts at anchor)
1036 Tile 1: [15, 40), left_ramp=9, right_ramp=8 (shifted back 1, ramp +1)
1037 Tile 2: [31, 56), left_ramp=9, right_ramp=0 (shifted back 1, ramp +1)
1038 This ensures each tile can causally depend on frames from previous tiles while maintaining
1039 proper temporal continuity through the blend ramps.
1040 Args:
1041 size: Tile size in *axis units* (latent steps for LTX time tiling)
1042 overlap: Overlap between tiles in the same units
1043 Returns:
1044 Split operation that divides temporal dimension with causal handling
1045 """
1046 non_causal_split = split_with_symmetric_overlaps(size, overlap)
1047
1048 def split(dimension_size: int) -> DimensionIntervals:
1049 if dimension_size <= size:
1050 return DEFAULT_SPLIT_OPERATION(dimension_size)
1051 intervals = non_causal_split(dimension_size)
1052
1053 starts = intervals.starts
1054 starts[1:] = [s - 1 for s in starts[1:]]
1055
1056 # Extend blend ramps by 1 for non-first tiles to blend over the extra frame
1057 left_ramps = intervals.left_ramps
1058 left_ramps[1:] = [r + 1 for r in left_ramps[1:]]
1059
1060 return replace(intervals, starts=starts, left_ramps=left_ramps)
1061
1062 return split
1063
1064
1065 def split_temporal_frames(tile_size_frames: int, overlap_frames: int) -> SplitOperation:
1066 """Split a temporal axis in video frame space into overlapping tiles.
1067 Args:
1068 tile_size_frames: Tile length in frames.
1069 overlap_frames: Overlap between consecutive tiles in frames.
1070 Returns:
1071 Split operation that takes frame count and returns DimensionIntervals in frame indices.
1072 """
1073 non_causal_split = split_with_symmetric_overlaps(tile_size_frames, overlap_frames)
1074
1075 def split(dimension_size: int) -> DimensionIntervals:
1076 if dimension_size <= tile_size_frames:
1077 return DEFAULT_SPLIT_OPERATION(dimension_size)
1078 intervals = non_causal_split(dimension_size)
1079 ends = intervals.ends
1080 ends[:-1] = [e + 1 for e in ends[:-1]]
1081 right_ramps = [0] * len(intervals.right_ramps)
1082 return replace(intervals, ends=ends, right_ramps=right_ramps)
1083
1084 return split
1085
1086
1087 def make_mapping_operation(
1088 map_func: Callable[[int, int, int, int, int], Tuple[slice, torch.Tensor | None]],
1089 scale: int,
1090 ) -> MappingOperation:
1091 """Create a mapping operation over a set of tiling intervals.
1092 The given mapping function is applied to each interval in the input dimension. The result function is used for
1093 creating tiles in the output dimension.
1094 Args:
1095 map_func: Mapping function to create the mapping operation from
1096 scale: Scale factor for the transformation, used as an argument for the mapping function
1097 Returns:
1098 Mapping operation that takes a set of tiling intervals and returns a set of slices and masks in the output
1099 dimension.
1100 """
1101
1102 def map_op(intervals: DimensionIntervals) -> tuple[list[slice], list[torch.Tensor | None]]:
1103 output_slices: list[slice] = []
1104 masks_1d: list[torch.Tensor | None] = []
1105 number_of_slices = len(intervals.starts)
1106 for i in range(number_of_slices):
1107 start = intervals.starts[i]
1108 end = intervals.ends[i]
1109 left_ramp = intervals.left_ramps[i]
1110 right_ramp = intervals.right_ramps[i]
1111 output_slice, mask_1d = map_func(start, end, left_ramp, right_ramp, scale)
1112 output_slices.append(output_slice)
1113 masks_1d.append(mask_1d)
1114 return output_slices, masks_1d
1115
1116 return map_op
1117
1118
1119 def map_temporal_interval_to_frame(
1120 begin: int,
1121 end: int,
1122 left_ramp: int,
1123 right_ramp: int,
1124 scale: int,
1125 ) -> Tuple[slice, torch.Tensor]:
1126 """Map temporal interval in latent space to video frame space.
1127 Args:
1128 begin: Start position in latent space
1129 end: End position in latent space
1130 left_ramp: Left ramp size in latent space
1131 right_ramp: Right ramp size in latent space
1132 scale: Scale factor for transformation
1133 Returns:
1134 Tuple of (output_slice, blend_mask)
1135 """
1136 start = begin * scale
1137 stop = 1 + (end - 1) * scale
1138
1139 left_ramp_frames = 0 if left_ramp == 0 else 1 + (left_ramp - 1) * scale
1140 right_ramp_frames = right_ramp * scale
1141
1142 mask_1d = compute_trapezoidal_mask_1d(stop - start, left_ramp_frames, right_ramp_frames, True)
1143 return slice(start, stop), mask_1d
1144
1145
1146 def map_temporal_interval_to_latent(
1147 begin: int, end: int, left_ramp: int, right_ramp: int | None = None, scale: int = 1
1148 ) -> Tuple[slice, torch.Tensor]:
1149 """
1150 Map temporal interval in video frame space to latent space.
1151 Args:
1152 begin: Start position in video frame space
1153 end: End position in video frame space
1154 left_ramp: Left ramp size in video frame space
1155 right_ramp: Right ramp size in video frame space
1156 scale: Scale factor for transformation
1157 Returns:
1158 Tuple of (output_slice, blend_mask)
1159 """
1160 start = begin // scale
1161 stop = (end - 1) // scale + 1
1162
1163 left_ramp_latents = 0 if left_ramp == 0 else 1 + (left_ramp - 1) // scale
1164 right_ramp_latents = right_ramp // scale
1165
1166 if right_ramp_latents != 0:
1167 raise ValueError("For tiled encoding, temporal tiles are expected to have a right ramp equal to 0")
1168
1169 mask_1d = compute_rectangular_mask_1d(stop - start, left_ramp_latents, right_ramp_latents)
1170
1171 return slice(start, stop), mask_1d
1172
1173
1174 def map_spatial_interval_to_pixel(
1175 begin: int,
1176 end: int,
1177 left_ramp: int,
1178 right_ramp: int,
1179 scale: int,
1180 ) -> Tuple[slice, torch.Tensor]:
1181 """Map spatial interval in latent space to pixel space.
1182 Args:
1183 begin: Start position in latent space
1184 end: End position in latent space
1185 left_ramp: Left ramp size in latent space
1186 right_ramp: Right ramp size in latent space
1187 scale: Scale factor for transformation
1188 """
1189 start = begin * scale
1190 stop = end * scale
1191 mask_1d = compute_trapezoidal_mask_1d(stop - start, left_ramp * scale, right_ramp * scale, False)
1192 return slice(start, stop), mask_1d
1193
1194
1195 def map_spatial_interval_to_latent(
1196 begin: int,
1197 end: int,
1198 left_ramp: int,
1199 right_ramp: int,
1200 scale: int,
1201 ) -> Tuple[slice, torch.Tensor]:
1202 """Map spatial interval in pixel space to latent space.
1203 Args:
1204 begin: Start position in pixel space
1205 end: End position in pixel space
1206 left_ramp: Left ramp size in pixel space
1207 right_ramp: Right ramp size in pixel space
1208 scale: Scale factor for transformation
1209 Returns:
1210 Tuple of (output_slice, blend_mask)
1211 """
1212 start = begin // scale
1213 stop = end // scale
1214 left_ramp = max(0, left_ramp // scale - 1)
1215
1216 right_ramp = 0 if right_ramp == 0 else 1
1217
1218 mask_1d = compute_rectangular_mask_1d(stop - start, left_ramp, right_ramp)
1219 return slice(start, stop), mask_1d
1220
1220 lines PYTHON