Text-to-Video
Diffusers
Safetensors
MiniMax H3
MiniMaxH3ModularPipeline
image-to-video
audio-video-generation
sparse-attention
block-sparse
inference-acceleration
triton
comfyui
Instructions to use Aazeus/Spark-H3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use Aazeus/Spark-H3 with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("Aazeus/Spark-H3", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Download scripts/ref2va_zero_origin_resample.py from Aazeus/Spark-H3: direct link, hf CLI and curl.
- Browser
- Download file 8.74 kB
-
https://huggingface.co/Aazeus/Spark-H3/resolve/main/scripts/ref2va_zero_origin_resample.py
- Command line
-
hf download hf://Aazeus/Spark-H3/scripts/ref2va_zero_origin_resample.py
-
curl -L -o ref2va_zero_origin_resample.py https://huggingface.co/Aazeus/Spark-H3/resolve/main/scripts/ref2va_zero_origin_resample.py
8.74 kB
| #!/usr/bin/env python3 | |
| """Repository-local MiniMax-H3 video resampling and RoPE time helpers. | |
| The video normalizer is a drop-in alternative to | |
| ``MiniMaxH3Ref2VASetupStep._normalize_video_condition``. It retains the | |
| upstream input conversion, output-length rounding, truncation and LANCZOS | |
| resize policy. Its default ``zero`` mode anchors the target frame grid at | |
| media time zero; ``legacy_round`` reproduces Diffusers' current slot-rounding | |
| behavior for compatibility. | |
| The upstream implementation is Apache-2.0 licensed and lives in | |
| ``diffusers/modular_pipelines/minimax_h3/before_encoder.py``. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from typing import Literal | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from diffusers.modular_pipelines.minimax_h3.modular_pipeline import resolve_canvas_size | |
| ResampleStartMode = Literal["zero", "legacy_round"] | |
| ROPE_TIME_UNITS_PER_SECOND = 40.0 | |
| def resample_frame_indices( | |
| num_source_frames: int, | |
| source_fps: float, | |
| target_fps: float, | |
| start_mode: ResampleStartMode = "zero", | |
| ) -> np.ndarray: | |
| """Return source indices on either a zero-origin or legacy rounded grid.""" | |
| if num_source_frames < 0: | |
| raise ValueError(f"num_source_frames must be non-negative, got {num_source_frames}.") | |
| if source_fps <= 0 or target_fps <= 0: | |
| raise ValueError( | |
| f"source_fps and target_fps must be positive, got {source_fps} and {target_fps}." | |
| ) | |
| if start_mode not in ("zero", "legacy_round"): | |
| raise ValueError(f"Unknown resampling start mode {start_mode!r}.") | |
| count = math.floor(num_source_frames * target_fps / source_fps + 0.5) | |
| if count == 0: | |
| return np.empty(0, dtype=np.int64) | |
| if start_mode == "legacy_round": | |
| # This is Diffusers' current implementation expressed as indices: | |
| # every source frame is repeated by the change in its rounded output | |
| # slot. For 24 -> 8 it retains 1,4,7,...; for 24 -> 12 it retains | |
| # 0,2,4,... due to the half-up boundary. | |
| scale = target_fps / source_fps | |
| slots = np.floor(np.arange(num_source_frames) * scale + 0.5).astype(np.int64) | |
| repeats = np.diff(slots, append=count) | |
| return np.repeat(np.arange(num_source_frames, dtype=np.int64), repeats) | |
| indices = np.floor( | |
| np.arange(count, dtype=np.float64) * source_fps / target_fps + 1e-9 | |
| ).astype(np.int64) | |
| return np.minimum(indices, num_source_frames - 1) | |
| def zero_origin_frame_indices( | |
| num_source_frames: int, | |
| source_fps: float, | |
| target_fps: float, | |
| ) -> np.ndarray: | |
| """Return source indices for a target grid beginning at exactly ``t=0``. | |
| The output count deliberately matches Diffusers' current stream-end | |
| rounding. Each target timestamp ``k / target_fps`` selects the source | |
| frame whose interval contains it, namely | |
| ``floor(k * source_fps / target_fps)``. | |
| """ | |
| return resample_frame_indices(num_source_frames, source_fps, target_fps, "zero") | |
| def sampling_time_origin_seconds(indices: np.ndarray, source_fps: float) -> float: | |
| """Return the media timestamp represented by the first sampled frame.""" | |
| indices = np.asarray(indices) | |
| if indices.ndim != 1 or len(indices) == 0: | |
| raise ValueError("indices must be a non-empty one-dimensional array.") | |
| if source_fps <= 0: | |
| raise ValueError(f"source_fps must be positive, got {source_fps}.") | |
| return float(indices[0]) / float(source_fps) | |
| def normalize_video_condition( | |
| frames, | |
| fps: float, | |
| num_frames: int, | |
| canvas_multiple: int, | |
| canvas_short_edge: int, | |
| canvas_max_pixels: int, | |
| target_fps: float, | |
| start_mode: ResampleStartMode = "zero", | |
| ) -> np.ndarray: | |
| """Normalize a MiniMax-H3 video reference on a selectable time grid. | |
| The signature intentionally matches Diffusers' private normalization | |
| helper for its first seven arguments. The repository-local eighth argument | |
| selects zero-origin behavior by default or exact upstream compatibility. | |
| """ | |
| if isinstance(frames, list): | |
| frames = np.stack([np.asarray(frame.convert("RGB")) for frame in frames]) | |
| if isinstance(frames, torch.Tensor): | |
| frames = frames.movedim(-3, -1).cpu().numpy() | |
| frames = np.asarray(frames) | |
| if frames.dtype != np.uint8: | |
| frames = (frames * 255.0).round().clip(0, 255).astype(np.uint8) | |
| if frames.ndim != 4 or frames.shape[3] != 3: | |
| raise ValueError( | |
| "A reference video must be `(num_frames, height, width, 3)` RGB frames, " | |
| f"got {tuple(frames.shape)}." | |
| ) | |
| if fps <= 0 or target_fps <= 0: | |
| raise ValueError(f"Video frame rates must be positive, got {fps} and {target_fps}.") | |
| if num_frames <= 0: | |
| raise ValueError(f"num_frames must be positive, got {num_frames}.") | |
| if fps != target_fps: | |
| indices = resample_frame_indices(frames.shape[0], fps, target_fps, start_mode) | |
| frames = frames[indices] | |
| frames = frames[:num_frames] | |
| if len(frames) == 0: | |
| raise ValueError("Video resampling produced no frames.") | |
| height, width = resolve_canvas_size( | |
| frames.shape[2], | |
| frames.shape[1], | |
| canvas_multiple, | |
| canvas_short_edge, | |
| canvas_max_pixels, | |
| ) | |
| if frames.shape[1:3] == (height, width): | |
| return frames | |
| return np.stack( | |
| [np.asarray(Image.fromarray(frame).resize((width, height), Image.Resampling.LANCZOS)) for frame in frames] | |
| ) | |
| def normalize_video_condition_zero_origin( | |
| frames, | |
| fps: float, | |
| num_frames: int, | |
| canvas_multiple: int, | |
| canvas_short_edge: int, | |
| canvas_max_pixels: int, | |
| target_fps: float, | |
| ) -> np.ndarray: | |
| """Backward-compatible convenience wrapper fixed to zero-origin mode.""" | |
| return normalize_video_condition( | |
| frames, | |
| fps, | |
| num_frames, | |
| canvas_multiple, | |
| canvas_short_edge, | |
| canvas_max_pixels, | |
| target_fps, | |
| start_mode="zero", | |
| ) | |
| def transform_reference_video_rope_time( | |
| position_ids: torch.Tensor, | |
| condition_video_indices: torch.Tensor, | |
| *, | |
| video_span_rope_units: float, | |
| time_scale: float = 1.0, | |
| time_origin_seconds: float = 0.0, | |
| audio_span_rope_units: float = 0.0, | |
| following_start_index: int | None = None, | |
| rope_units_per_second: float = ROPE_TIME_UNITS_PER_SECOND, | |
| ) -> torch.Tensor: | |
| """Apply explicit scale and media-time origin to one reference video. | |
| Audio coordinates are intentionally untouched. If rows follow the | |
| reference block, ``following_start_index`` shifts their time coordinates | |
| by the change in ``max(audio_end, video_end)`` so subsequent blocks begin | |
| after the corrected reference end. | |
| """ | |
| if position_ids.ndim != 2 or position_ids.shape[1] != 3: | |
| raise ValueError(f"position_ids must have shape (rows, 3), got {tuple(position_ids.shape)}.") | |
| if condition_video_indices.ndim != 1 or condition_video_indices.numel() == 0: | |
| raise ValueError("condition_video_indices must be a non-empty one-dimensional tensor.") | |
| values = (video_span_rope_units, time_scale, time_origin_seconds, audio_span_rope_units, rope_units_per_second) | |
| if not all(math.isfinite(float(value)) for value in values): | |
| raise ValueError("RoPE time parameters must be finite.") | |
| if video_span_rope_units < 0 or audio_span_rope_units < 0: | |
| raise ValueError("Video and audio spans must be non-negative.") | |
| if time_scale <= 0 or rope_units_per_second <= 0: | |
| raise ValueError("time_scale and rope_units_per_second must be positive.") | |
| if following_start_index is not None and not 0 <= following_start_index <= len(position_ids): | |
| raise ValueError(f"following_start_index is out of range: {following_start_index}.") | |
| result = position_ids.clone() | |
| origin = result[condition_video_indices[0], 0].clone() | |
| phase = float(time_origin_seconds) * float(rope_units_per_second) | |
| old_time = result[condition_video_indices, 0] | |
| result[condition_video_indices, 0] = origin + (old_time - origin) * time_scale + phase | |
| old_block_span = max(float(audio_span_rope_units), float(video_span_rope_units)) | |
| new_video_span = phase + float(video_span_rope_units) * float(time_scale) | |
| new_block_span = max(float(audio_span_rope_units), new_video_span) | |
| if following_start_index is not None: | |
| result[following_start_index:, 0] += new_block_span - old_block_span | |
| return result | |
| __all__ = [ | |
| "ROPE_TIME_UNITS_PER_SECOND", | |
| "ResampleStartMode", | |
| "normalize_video_condition", | |
| "normalize_video_condition_zero_origin", | |
| "resample_frame_indices", | |
| "sampling_time_origin_seconds", | |
| "transform_reference_video_rope_time", | |
| "zero_origin_frame_indices", | |
| ] | |