Spaces:
Running on Zero
Running on Zero
Download diffusers/modular_pipelines/ltx2/decoders.py from diffusers-modular/flux3-image-klein-bbox: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/diffusers-modular/flux3-image-klein-bbox/resolve/main/diffusers/modular_pipelines/ltx2/decoders.py
- Command line
-
hf download hf://spaces/diffusers-modular/flux3-image-klein-bbox/diffusers/modular_pipelines/ltx2/decoders.py
-
curl -L -o decoders.py https://huggingface.co/spaces/diffusers-modular/flux3-image-klein-bbox/resolve/main/diffusers/modular_pipelines/ltx2/decoders.py
16.2 kB
| # Copyright 2026 The HuggingFace Team. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| from typing import Any | |
| import torch | |
| from ...configuration_utils import FrozenDict | |
| from ...models import AutoencoderKLLTX2Audio, AutoencoderKLLTX2Video, LTX2VideoDiffusionDecoderModel | |
| # NOTE (modular.md gotcha #1): `LTX2Vocoder` / `LTX2VocoderWithBWE` currently live under | |
| # `diffusers.pipelines.ltx2.vocoder`, and modular blocks must not import from `diffusers.pipelines.*`. | |
| # They are already `ModelMixin` / `ConfigMixin` model classes, so the clean fix is to relocate them to | |
| # `src/diffusers/models/` and re-export from `diffusers.models` before this lands. Imported from the | |
| # pipelines path here only so the draft is runnable; switch to the models path once moved. | |
| from ...pipelines.ltx2.vocoder import LTX2Vocoder | |
| from ...utils import logging | |
| from ...utils.torch_utils import randn_tensor | |
| from ...video_processor import VideoProcessor | |
| from ..modular_pipeline import ModularPipelineBlocks, PipelineState | |
| from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam | |
| logger = logging.get_logger(__name__) | |
| # The pack/unpack/denormalize helpers below mirror the static methods on | |
| # `diffusers.pipelines.ltx2.pipeline_ltx2.LTX2Pipeline`. They are redefined here (rather than imported) | |
| # because modular blocks must not import from `diffusers.pipelines.*` (modular.md gotcha #1); this follows | |
| # the same redefinition pattern as `modular_pipelines/ltx/decoders.py`. | |
| def _denormalize_latents( | |
| latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0 | |
| ) -> torch.Tensor: | |
| # Denormalize video latents across the channel dimension [B, C, F, H, W]. | |
| latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype) | |
| latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype) | |
| latents = latents * latents_std / scaling_factor + latents_mean | |
| return latents | |
| def _unpack_latents( | |
| latents: torch.Tensor, num_frames: int, height: int, width: int, patch_size: int = 1, patch_size_t: int = 1 | |
| ) -> torch.Tensor: | |
| # Packed video latents of shape [B, S, D] are unpacked into a video tensor of shape [B, C, F, H, W]. | |
| batch_size = latents.size(0) | |
| latents = latents.reshape(batch_size, num_frames, height, width, -1, patch_size_t, patch_size, patch_size) | |
| latents = latents.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3) | |
| return latents | |
| def _denormalize_audio_latents( | |
| latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor | |
| ) -> torch.Tensor: | |
| latents_mean = latents_mean.to(latents.device, latents.dtype) | |
| latents_std = latents_std.to(latents.device, latents.dtype) | |
| return (latents * latents_std) + latents_mean | |
| def _unpack_audio_latents( | |
| latents: torch.Tensor, | |
| latent_length: int, | |
| num_mel_bins: int, | |
| patch_size: int | None = None, | |
| patch_size_t: int | None = None, | |
| ) -> torch.Tensor: | |
| # Unpacks an audio patch sequence of shape [B, S, D] into a latent spectrogram tensor [B, C, L, M]. | |
| if patch_size is not None and patch_size_t is not None: | |
| batch_size = latents.size(0) | |
| latents = latents.reshape(batch_size, latent_length, num_mel_bins, -1, patch_size_t, patch_size) | |
| latents = latents.permute(0, 3, 1, 4, 2, 5).flatten(4, 5).flatten(2, 3) | |
| else: | |
| # Assume [B, S, D] = [B, L, C * M], i.e. a (mel) patch_size of M and a patch_size_t of 1. | |
| latents = latents.unflatten(2, (-1, num_mel_bins)).transpose(1, 2) | |
| return latents | |
| class LTX2TrimConditionTokensStep(ModularPipelineBlocks): | |
| model_name = "ltx2" | |
| def description(self) -> str: | |
| return ( | |
| "Drops the appended keyframe-condition tokens from the denoised latents, leaving only the " | |
| "generated-video tokens for the decoders." | |
| ) | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam.template("latents", required=True), | |
| InputParam( | |
| "base_token_count", | |
| type_hint=int, | |
| required=True, | |
| description="Number of generated-video tokens, i.e. the sequence length before appended tokens.", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam( | |
| "latents", | |
| type_hint=torch.Tensor, | |
| description="Denoised latents for the generated video, with condition tokens removed.", | |
| ) | |
| ] | |
| def __call__(self, components, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| block_state.latents = block_state.latents[:, : block_state.base_token_count] | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class LTX2DiffusionVaeDecoderStep(ModularPipelineBlocks): | |
| model_name = "ltx2" | |
| def description(self) -> str: | |
| return ( | |
| "Step that unpacks and decodes the denoised video latents with the LTX-2 diffusion decoder (or returns " | |
| "latents). Swap this in for `LTX2VaeDecoderStep` on checkpoints that ship the diffusion decoder, which " | |
| "from LTX-2.5 on is the native default. The decoder denoises rather than deterministically decoding, so " | |
| "it draws its own noise from `generator` and takes no decode timestep." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("diffusion_decoder", LTX2VideoDiffusionDecoderModel), | |
| ComponentSpec( | |
| "video_processor", | |
| VideoProcessor, | |
| config=FrozenDict({"vae_scale_factor": 32}), | |
| default_creation_method="from_config", | |
| ), | |
| ] | |
| def inputs(self) -> list[tuple[str, Any]]: | |
| return [ | |
| InputParam.template("latents", required=True), | |
| InputParam.template("output_type", default="pil"), | |
| InputParam.template("height", default=512), | |
| InputParam.template("width", default=704), | |
| InputParam( | |
| "num_frames", type_hint=int, default=121, description="The number of frames in the generated video." | |
| ), | |
| InputParam.template("generator"), | |
| InputParam.template("dtype", required=True), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [OutputParam.template("videos")] | |
| def __call__(self, components, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| decoder = components.diffusion_decoder | |
| latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1 | |
| latent_height = block_state.height // components.vae_spatial_compression_ratio | |
| latent_width = block_state.width // components.vae_spatial_compression_ratio | |
| latents = _unpack_latents( | |
| block_state.latents, | |
| latent_num_frames, | |
| latent_height, | |
| latent_width, | |
| components.transformer_spatial_patch_size, | |
| components.transformer_temporal_patch_size, | |
| ) | |
| if block_state.output_type == "latent": | |
| block_state.videos = _denormalize_latents( | |
| latents, components.latents_mean, components.latents_std, components.vae_scaling_factor | |
| ) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| latents = latents.to(block_state.dtype) | |
| latents = _denormalize_latents( | |
| latents, components.latents_mean, components.latents_std, components.vae_scaling_factor | |
| ) | |
| latents = latents.to(decoder.dtype) | |
| # It samples the noise it denoises, so pass the generator to keep decoding reproducible. | |
| video = decoder.decode(latents, generator=block_state.generator, return_dict=False)[0] | |
| block_state.videos = components.video_processor.postprocess_video(video, output_type=block_state.output_type) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class LTX2VaeDecoderStep(ModularPipelineBlocks): | |
| model_name = "ltx2" | |
| def description(self) -> str: | |
| return "Step that unpacks and decodes the denoised video latents into videos (or returns latents)." | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("vae", AutoencoderKLLTX2Video), | |
| ComponentSpec( | |
| "video_processor", | |
| VideoProcessor, | |
| config=FrozenDict({"vae_scale_factor": 32}), | |
| default_creation_method="from_config", | |
| ), | |
| ] | |
| def inputs(self) -> list[tuple[str, Any]]: | |
| return [ | |
| InputParam.template("latents", required=True), | |
| InputParam.template("output_type", default="pil"), | |
| InputParam.template("height", default=512), | |
| InputParam.template("width", default=704), | |
| InputParam( | |
| "num_frames", | |
| type_hint=int, | |
| default=None, | |
| description=( | |
| "The number of frames in the generated video. Omit to auto-predict via the `duration_head` " | |
| "(see `LTX2AutoDurationStep`)." | |
| ), | |
| ), | |
| InputParam( | |
| "decode_timestep", default=0.0, description="The timestep at which the VAE decodes the final latents." | |
| ), | |
| InputParam( | |
| "decode_noise_scale", | |
| default=None, | |
| description="Noise interpolation factor applied to the latents at the decode timestep.", | |
| ), | |
| InputParam.template("generator"), | |
| InputParam.template("batch_size"), | |
| InputParam.template("dtype", required=True), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [OutputParam.template("videos")] | |
| def __call__(self, components, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| vae = components.vae | |
| latents = block_state.latents | |
| height = block_state.height | |
| width = block_state.width | |
| num_frames = block_state.num_frames | |
| latent_num_frames = (num_frames - 1) // components.vae_temporal_compression_ratio + 1 | |
| latent_height = height // components.vae_spatial_compression_ratio | |
| latent_width = width // components.vae_spatial_compression_ratio | |
| latents = _unpack_latents( | |
| latents, | |
| latent_num_frames, | |
| latent_height, | |
| latent_width, | |
| components.transformer_spatial_patch_size, | |
| components.transformer_temporal_patch_size, | |
| ) | |
| if block_state.output_type == "latent": | |
| block_state.videos = _denormalize_latents( | |
| latents, components.latents_mean, components.latents_std, components.vae_scaling_factor | |
| ) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| # LTX-2 applies the optional decode-time noise on the *normalized* latents, then denormalizes | |
| # (the reverse of LTX-1's decoder order). | |
| latents = latents.to(block_state.dtype) | |
| if not vae.config.timestep_conditioning: | |
| timestep = None | |
| else: | |
| device = latents.device | |
| batch_size = block_state.batch_size | |
| decode_timestep = block_state.decode_timestep | |
| decode_noise_scale = block_state.decode_noise_scale | |
| noise = randn_tensor(latents.shape, generator=block_state.generator, device=device, dtype=latents.dtype) | |
| if not isinstance(decode_timestep, list): | |
| decode_timestep = [decode_timestep] * batch_size | |
| if decode_noise_scale is None: | |
| decode_noise_scale = decode_timestep | |
| elif not isinstance(decode_noise_scale, list): | |
| decode_noise_scale = [decode_noise_scale] * batch_size | |
| timestep = torch.tensor(decode_timestep, device=device, dtype=latents.dtype) | |
| decode_noise_scale = torch.tensor(decode_noise_scale, device=device, dtype=latents.dtype)[ | |
| :, None, None, None, None | |
| ] | |
| latents = (1 - decode_noise_scale) * latents + decode_noise_scale * noise | |
| latents = _denormalize_latents( | |
| latents, components.latents_mean, components.latents_std, components.vae_scaling_factor | |
| ) | |
| latents = latents.to(vae.dtype) | |
| video = vae.decode(latents, timestep, return_dict=False)[0] | |
| block_state.videos = components.video_processor.postprocess_video(video, output_type=block_state.output_type) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class LTX2AudioDecoderStep(ModularPipelineBlocks): | |
| model_name = "ltx2" | |
| def description(self) -> str: | |
| return ( | |
| "Step that unpacks and decodes the denoised audio latents into a waveform via the audio VAE and vocoder " | |
| "(or returns the unpacked audio latents when `output_type='latent'`)." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| # The checkpoint may ship either `LTX2Vocoder` or `LTX2VocoderWithBWE`; the concrete class is resolved from | |
| # the vocoder subfolder's config at load time. `LTX2Vocoder` is declared here as the representative type. | |
| return [ | |
| ComponentSpec("audio_vae", AutoencoderKLLTX2Audio), | |
| ComponentSpec("vocoder", LTX2Vocoder), | |
| ] | |
| def inputs(self) -> list[tuple[str, Any]]: | |
| return [ | |
| InputParam("audio_latents", type_hint=torch.Tensor, required=True, description="Denoised audio latents."), | |
| InputParam( | |
| "audio_num_frames", | |
| type_hint=int, | |
| required=True, | |
| description="Number of audio latent frames, used to unpack the audio latent sequence.", | |
| ), | |
| InputParam.template("output_type", default="pil"), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam("audio", type_hint=torch.Tensor, description="The generated audio waveform."), | |
| ] | |
| def __call__(self, components, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| audio_vae = components.audio_vae | |
| num_mel_bins = audio_vae.config.mel_bins | |
| latent_mel_bins = num_mel_bins // components.audio_vae_mel_compression_ratio | |
| audio_latents = _denormalize_audio_latents( | |
| block_state.audio_latents, components.audio_latents_mean, components.audio_latents_std | |
| ) | |
| audio_latents = _unpack_audio_latents( | |
| audio_latents, block_state.audio_num_frames, num_mel_bins=latent_mel_bins | |
| ) | |
| if block_state.output_type == "latent": | |
| block_state.audio = audio_latents | |
| else: | |
| audio_latents = audio_latents.to(audio_vae.dtype) | |
| generated_mel_spectrograms = audio_vae.decode(audio_latents, return_dict=False)[0] | |
| block_state.audio = components.vocoder(generated_mel_spectrograms) | |
| self.set_block_state(state, block_state) | |
| return components, state | |