Spaces:
Paused
Paused
| # Copyright 2026 The MiniMax and HuggingFace Teams. 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. | |
| import torch | |
| from ...schedulers import MiniMaxH3Scheduler | |
| from ...utils import logging | |
| from ...utils.torch_utils import randn_tensor | |
| from ..modular_pipeline import ModularPipelineBlocks, PipelineState | |
| from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam | |
| from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline | |
| from .packing import ( | |
| MINIMAX_H3_AUDIO_CHANNELS, | |
| MINIMAX_H3_KEYFRAME_NOISE_AUG, | |
| MiniMaxH3PackedSequence, | |
| build_packed_sequence, | |
| build_row_timesteps, | |
| patchify_video_latents, | |
| ) | |
| from .packing_ref2va import MiniMaxH3PreparedReference, build_ref2va_packed_sequence | |
| logger = logging.get_logger(__name__) # pylint: disable=invalid-name | |
| def _layout_inputs() -> list[InputParam]: | |
| r"""What both packed layouts are built from, beyond the conditioning of the task itself.""" | |
| return [ | |
| InputParam( | |
| name="text_token_tags", | |
| type_hint=torch.Tensor, | |
| required=True, | |
| description="The per-row modality tag of every row of `prompt_embeds`.", | |
| ), | |
| InputParam( | |
| name="num_latent_frames", type_hint=int, required=True, description="Number of video latent frames." | |
| ), | |
| InputParam(name="latent_height", type_hint=int, required=True, description="Height of the video latents."), | |
| InputParam(name="latent_width", type_hint=int, required=True, description="Width of the video latents."), | |
| InputParam( | |
| name="num_audio_latents", | |
| type_hint=int, | |
| required=True, | |
| description="Number of audio latents per channel.", | |
| ), | |
| ] | |
| def _layout_outputs() -> list[OutputParam]: | |
| r"""The row layout of the packed sequence, shared by the two tasks.""" | |
| return [ | |
| OutputParam( | |
| "layout", | |
| type_hint=MiniMaxH3PackedSequence, | |
| description="The structural description of the packed sequence.", | |
| ), | |
| OutputParam( | |
| "position_ids", | |
| type_hint=torch.Tensor, | |
| description="The `(t, h, w)` rotary coordinate of every row, in float64.", | |
| ), | |
| OutputParam("token_tags", type_hint=torch.Tensor, description="The modality tag of every row."), | |
| OutputParam( | |
| "video_indices", | |
| type_hint=torch.Tensor, | |
| description="Sequence positions of the video rows, conditioning rows first.", | |
| ), | |
| OutputParam( | |
| "audio_indices", | |
| type_hint=torch.Tensor, | |
| description="Sequence positions of the audio rows, reference rows first.", | |
| ), | |
| OutputParam("text_indices", type_hint=torch.Tensor, description="Sequence positions of the text rows."), | |
| OutputParam( | |
| "num_condition_video_rows", | |
| type_hint=int, | |
| description="How many leading video rows are conditioning rows rather than generated rows.", | |
| ), | |
| OutputParam( | |
| "num_condition_audio_rows", | |
| type_hint=int, | |
| description="How many leading audio rows are reference rows rather than generated rows.", | |
| ), | |
| ] | |
| def _set_layout_state(block_state, layout: MiniMaxH3PackedSequence, device: torch.device) -> None: | |
| block_state.layout = layout | |
| block_state.position_ids = layout.position_ids.to(device) | |
| block_state.token_tags = layout.token_tags.to(device) | |
| block_state.video_indices = layout.video_indices.to(device) | |
| block_state.audio_indices = layout.audio_indices.to(device) | |
| block_state.text_indices = layout.text_indices.to(device) | |
| block_state.num_condition_video_rows = layout.num_condition_video_rows | |
| block_state.num_condition_audio_rows = layout.num_condition_audio_rows | |
| class MiniMaxH3PrepareLayoutStep(ModularPipelineBlocks): | |
| model_name = "minimax-h3" | |
| def description(self) -> str: | |
| return ( | |
| "Builds the packed layout of a `t2va` / `fl2va` request — `[text | keyframe conditions | target audio | " | |
| "target video]` — and its fp64 rotary grid. MiniMax-H3 runs full self-attention over this one sequence, " | |
| "so the layout is what every later block addresses rows through." | |
| ) | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| *_layout_inputs(), | |
| InputParam( | |
| name="keyframe_anchors", | |
| type_hint=tuple, | |
| default=(), | |
| description="Which end of the video every keyframe is anchored to, in packed order.", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return _layout_outputs() | |
| def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| layout = build_packed_sequence( | |
| block_state.text_token_tags, | |
| block_state.num_latent_frames, | |
| block_state.latent_height, | |
| block_state.latent_width, | |
| block_state.num_audio_latents, | |
| components.patch_size, | |
| block_state.keyframe_anchors, | |
| ) | |
| _set_layout_state(block_state, layout, components._execution_device) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class MiniMaxH3Ref2VAPrepareLayoutStep(ModularPipelineBlocks): | |
| model_name = "minimax-h3-ref2va" | |
| def description(self) -> str: | |
| return ( | |
| "Builds the packed layout of a `ref2va` request — `[text | reference blocks | target audio | target " | |
| "video]` — and its fp64 rotary grid. The reference order advances the shared audio/video rotary clock, so " | |
| "it is part of the layout rather than a detail of the presentation." | |
| ) | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| *_layout_inputs(), | |
| InputParam( | |
| name="prepared_references", | |
| type_hint=list[MiniMaxH3PreparedReference], | |
| required=True, | |
| description="The prepared references, in packed order, with their latent geometry filled in.", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return _layout_outputs() | |
| def __call__(self, components: MiniMaxH3Ref2VAModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| layout = build_ref2va_packed_sequence( | |
| block_state.text_token_tags, | |
| block_state.prepared_references, | |
| block_state.num_latent_frames, | |
| block_state.latent_height, | |
| block_state.latent_width, | |
| block_state.num_audio_latents, | |
| components.patch_size, | |
| ) | |
| _set_layout_state(block_state, layout, components._execution_device) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class MiniMaxH3PrepareLatentsStep(ModularPipelineBlocks): | |
| model_name = "minimax-h3" | |
| def description(self) -> str: | |
| return ( | |
| "Draws the initial noise of the generated rows and prepends the conditioning rows. MiniMax-H3 draws the " | |
| "video noise as a latent tensor and patchifies it afterwards, then the audio noise directly in row " | |
| "layout — both off the request's generator, after the conditioning noise of the encoder step." | |
| ) | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam( | |
| name="num_latent_frames", type_hint=int, required=True, description="Number of video latent frames." | |
| ), | |
| InputParam(name="latent_height", type_hint=int, required=True, description="Height of the video latents."), | |
| InputParam(name="latent_width", type_hint=int, required=True, description="Width of the video latents."), | |
| InputParam( | |
| name="num_audio_latents", | |
| type_hint=int, | |
| required=True, | |
| description="Number of audio latents per channel.", | |
| ), | |
| InputParam.template( | |
| "generator", | |
| description=( | |
| "The generator of the request. The video noise is drawn from it first, then the audio noise." | |
| ), | |
| ), | |
| InputParam( | |
| name="latents", | |
| type_hint=torch.Tensor, | |
| description=( | |
| "Pre-generated video noise of shape `(1, 24, num_latent_frames, latent_height, latent_width)`, " | |
| "used instead of the draw." | |
| ), | |
| ), | |
| InputParam( | |
| name="audio_latents", | |
| type_hint=torch.Tensor, | |
| description="Pre-generated audio noise of shape `(2, 32, num_audio_latents)`.", | |
| ), | |
| InputParam( | |
| name="condition_latents", | |
| type_hint=torch.Tensor, | |
| description="The video conditioning rows to prepend, or None for a request that has none.", | |
| ), | |
| InputParam( | |
| name="audio_condition_latents", | |
| type_hint=torch.Tensor, | |
| description="The audio conditioning rows to prepend, or None for a request that has none.", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam( | |
| "latents", | |
| type_hint=torch.Tensor, | |
| description="The video rows of the packed sequence, conditioning rows first.", | |
| ), | |
| OutputParam( | |
| "audio_latents", | |
| type_hint=torch.Tensor, | |
| description="The channel-major audio rows of the packed sequence, reference rows first.", | |
| ), | |
| ] | |
| def prepare_latents( | |
| components, | |
| num_latent_frames: int, | |
| latent_height: int, | |
| latent_width: int, | |
| num_audio_latents: int, | |
| device: torch.device, | |
| generator: torch.Generator | list[torch.Generator] | None = None, | |
| latents: torch.Tensor | None = None, | |
| audio_latents: torch.Tensor | None = None, | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| r""" | |
| Draw the initial noise of both modalities and pack it into transformer rows. | |
| A request draws every stream from the one generator it is given, and the order is part of what that generator | |
| reproduces: the conditioning noise of the keyframes or references first (one draw per condition, in | |
| [`~modular_pipelines.minimax_h3.packing.keyframe_condition_noise`]), then the video noise here, as a latent tensor | |
| that is patchified afterwards, then the audio noise, directly in row layout. Passing `latents` or | |
| `audio_latents` skips its draw and shifts the ones after it. | |
| Args: | |
| num_latent_frames (`int`): Number of video latent frames. | |
| latent_height (`int`): Latent height. | |
| latent_width (`int`): Latent width. | |
| num_audio_latents (`int`): Number of audio latents per channel. | |
| device (`torch.device`): The device the rows are drawn on. | |
| generator (`torch.Generator`, *optional*): The generator of the request. | |
| latents (`torch.Tensor`, *optional*): | |
| Pre-generated video noise of shape `(1, latent_channels, num_latent_frames, latent_height, | |
| latent_width)`, used instead of the draw. | |
| audio_latents (`torch.Tensor`, *optional*): | |
| Pre-generated audio noise of shape `(2, audio_latent_channels, num_audio_latents)`. | |
| Returns: | |
| `tuple[torch.Tensor, torch.Tensor]`: the video rows and the channel-major audio rows. | |
| """ | |
| if latents is None: | |
| latents = randn_tensor( | |
| (1, components.vae_latent_channels, num_latent_frames, latent_height, latent_width), | |
| generator=generator, | |
| device=device, | |
| dtype=torch.float32, | |
| ) | |
| video_rows = patchify_video_latents(latents.to(torch.float32), components.patch_size) | |
| if audio_latents is None: | |
| audio_rows = randn_tensor( | |
| (num_audio_latents * MINIMAX_H3_AUDIO_CHANNELS, components.audio_latent_channels), | |
| generator=generator, | |
| device=device, | |
| dtype=torch.float32, | |
| ) | |
| else: | |
| audio_rows = audio_latents.to(torch.float32).permute(0, 2, 1).reshape(-1, components.audio_latent_channels) | |
| return video_rows.to(device), audio_rows.to(device) | |
| def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| latents, audio_latents = self.prepare_latents( | |
| components, | |
| block_state.num_latent_frames, | |
| block_state.latent_height, | |
| block_state.latent_width, | |
| block_state.num_audio_latents, | |
| components._execution_device, | |
| block_state.generator, | |
| block_state.latents, | |
| block_state.audio_latents, | |
| ) | |
| if block_state.condition_latents is not None: | |
| latents = torch.cat([block_state.condition_latents, latents]) | |
| if block_state.audio_condition_latents is not None: | |
| audio_latents = torch.cat([block_state.audio_condition_latents, audio_latents]) | |
| block_state.latents, block_state.audio_latents = latents, audio_latents | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class MiniMaxH3SetTimestepsStep(ModularPipelineBlocks): | |
| model_name = "minimax-h3" | |
| def description(self) -> str: | |
| return ( | |
| "Initializes the two schedules — `shift = 12.0` for video, `shift = 3.0` for audio — and stages the " | |
| "row-to-timestep plan of every step. One forward serves every modality and every noise level at once: " | |
| "the generated rows step down their own schedule while the conditioning rows stay pinned at their " | |
| "noise-augmentation level, and that assignment is static per step." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("scheduler", MiniMaxH3Scheduler), | |
| ComponentSpec("audio_scheduler", MiniMaxH3Scheduler), | |
| ] | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam.template("num_inference_steps", required=True), | |
| InputParam( | |
| name="layout", | |
| type_hint=MiniMaxH3PackedSequence, | |
| required=True, | |
| description="The structural description of the packed sequence.", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam("timesteps", type_hint=torch.Tensor, description="Timesteps of the video schedule."), | |
| OutputParam("audio_timesteps", type_hint=torch.Tensor, description="Timesteps of the audio schedule."), | |
| OutputParam( | |
| "row_timestep_plan", | |
| type_hint=list, | |
| description=( | |
| "One `(timestep, timestep_indices)` pair per step: the distinct timesteps of the sequence and the " | |
| "index of every row into them." | |
| ), | |
| ), | |
| ] | |
| def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| device = components._execution_device | |
| components.scheduler.set_timesteps(block_state.num_inference_steps, device=device) | |
| components.audio_scheduler.set_timesteps(block_state.num_inference_steps, device=device) | |
| block_state.timesteps = components.scheduler.timesteps | |
| block_state.audio_timesteps = components.audio_scheduler.timesteps | |
| block_state.row_timestep_plan = [ | |
| tuple( | |
| tensor.to(device) | |
| for tensor in build_row_timesteps( | |
| block_state.layout, | |
| float(timestep), | |
| float(audio_timestep), | |
| max(float(timestep), MINIMAX_H3_KEYFRAME_NOISE_AUG), | |
| 1.0, | |
| ) | |
| ) | |
| for timestep, audio_timestep in zip(block_state.timesteps, block_state.audio_timesteps) | |
| ] | |
| self.set_block_state(state, block_state) | |
| return components, state | |