[feat]: add Pika H3 Flex inference support
Add Pika H3 Flex (MiniMax-H3 plus the released fl2va/ref2va rank-128 adapters) as a FastVideo model family: registry/config/presets, bucket geometry with tiered reference sizing, automatic adapter selection and manifest validation, the fixed six-forward re-noise sampler, T2VA/I2VA/L2VA/FL2VA/Ref2VA pipeline support, an example, docs, and CPU unit tests. GPU end-to-end validation is still pending. This change does not add PDD, VSA, or new kernel paths.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Pika H3 Flex
|
||||
|
||||
Pika H3 Flex is a post-trained MiniMax-H3 model that keeps H3's packed
|
||||
video/audio transformer, Qwen3-VL conditioner, and video/audio VAEs. Pika ships
|
||||
two rank-128 PEFT adapters instead of a second base checkpoint:
|
||||
|
||||
| Adapter | Trunk | Tasks |
|
||||
|---|---|---|
|
||||
| `fl2va/adapter.safetensors` | `transformer` | T2VA, I2VA, L2VA, FL2VA |
|
||||
| `ref2va/adapter.safetensors` | `transformer_ref` | Ref2VA |
|
||||
|
||||
FastVideo detects `Pika-Labs/Pika-H3-Flex`, downloads only the selected task's
|
||||
adapter, and applies it as an ordinary LoRA. The released adapter is selected
|
||||
automatically; pass `lora_path` only when overriding it with a compatible
|
||||
Pika adapter.
|
||||
|
||||
## Sampling contract
|
||||
|
||||
Pika's sampler is fixed, not a tunable step count:
|
||||
|
||||
* 6 transformer forwards over clean-time rungs
|
||||
`1000, 750, 500, 250, 100, 40, 0`;
|
||||
* video shift `12.0`, audio shift `3.0`;
|
||||
* no CFG, `guidance_scale=1.0`, empty negative prompt;
|
||||
* each step re-noises the predicted clean sample with fresh Gaussian noise from
|
||||
a generator seeded `seed + 1`.
|
||||
|
||||
`num_inference_steps` must therefore be `6`; other values are rejected. This is
|
||||
not a PDD checkpoint and it has no trained VSA gates, so PDD fused blocks and
|
||||
VSA sparsity are not used.
|
||||
|
||||
## Tasks
|
||||
|
||||
```bash
|
||||
# text to video + audio
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
|
||||
--prompt "a paper boat drifting down a rainy street"
|
||||
|
||||
# first frame, last frame, or both
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task i2va \
|
||||
--image first.png --prompt "the camera slowly pushes in"
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task fl2va \
|
||||
--image first.png --last-image last.png --prompt "the flower opens"
|
||||
|
||||
# ordered references, including reference video and audio
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
|
||||
--reference image:person.png --reference video:dance.mp4 \
|
||||
--reference audio:voice.wav --prompt "the person dances to the music"
|
||||
```
|
||||
|
||||
Ref2VA accepts up to 12 references: 9 images, 3 videos, and 3 audio. Each video
|
||||
and audio reference is 2–15 s; video/audio totals are capped at 15 s per
|
||||
modality. A video reference needs at least 22 decoded frames at 24 fps. At
|
||||
least one image or video is required, and the Ref2VA pipeline must be selected
|
||||
with `PikaH3FlexRef2VAModularPipeline` (the example does this).
|
||||
|
||||
## Canvas and frames
|
||||
|
||||
Pika uses an exact 32-pixel bucket lattice. A request that matches a bucket is
|
||||
kept exactly; otherwise FastVideo picks the tier from the requested short edge
|
||||
(`<= 480` → p480, else p768) and snaps to the nearest aspect bucket in that tier.
|
||||
|
||||
| Tier | Short edge | Max pixels | Reference-image short edge |
|
||||
|---|---|---|---|
|
||||
| p480 | 480 | 832×480 | 1024 |
|
||||
| p768 | 768 | 1344×768 | 2048 |
|
||||
|
||||
Frame counts follow Pika's `17n + 5` grid at 24 fps (5–15 s, 120–362 aligned
|
||||
frames). The default 16:9 p768 canvas is 1344×768 and 124 frames.
|
||||
|
||||
Pika center-crops both keyframes to the bucket. MiniMax-H3 stretches its first
|
||||
frame instead; do not reuse that fit for Pika.
|
||||
|
||||
## Training-free acceleration
|
||||
|
||||
The 6 forwards already use FastVideo's normal inference path. Available
|
||||
lossless switches:
|
||||
|
||||
* `FASTVIDEO_MINIMAX_H3_EXACT_KERNELS=all` — exact fused H3 kernels;
|
||||
* `FASTVIDEO_H3_REF2VA_MEMO_ENTRIES=16` — cache reference conditioning
|
||||
(Ref2VA);
|
||||
* `FASTVIDEO_H3_VAE_TILE_PARALLEL=1` — parallel VAE tiles;
|
||||
* `FASTVIDEO_ULYSSES_A2A=auto` — Ulysses all-to-all for multi-GPU sequence
|
||||
parallelism;
|
||||
* `OffloadConfig(lazy_module_load=True)` and `dit_layerwise=True` for smaller
|
||||
GPUs;
|
||||
* `CompileConfig(enabled=True)` for `torch.compile`;
|
||||
* the standard FlashAttention/SDPA backend selection.
|
||||
|
||||
`--lossless-accel` in the example enables the first four before the generator
|
||||
starts. Kernel work beyond the existing exact kernels is intentionally not part
|
||||
of this integration.
|
||||
|
||||
## Hardware
|
||||
|
||||
The selected H3 trunk plus Qwen3-VL does not fit comfortably on a 24 GB card
|
||||
without offload. A single 80 GB GPU can run with text-encoder offload; use
|
||||
layerwise DiT offload or multiple GPUs for smaller devices. Ref2VA encodes
|
||||
reference images at up to a 2048 short edge, so its conditioning memory is
|
||||
higher than T2VA's.
|
||||
@@ -68,6 +68,7 @@ column links a runnable script in `examples/inference/basic/` where one exists.
|
||||
| matrixgame | `FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers`<br />`FastVideo/Matrix-Game-2.0-GTA-Distilled-Diffusers`<br />`FastVideo/Matrix-Game-2.0-TempleRun-Distilled-Diffusers`<br />`FastVideo/Matrix-Game-2.0-Base-Diffusers`<br />`FastVideo/Matrix-Game-2.0-GTA-Diffusers`<br />`FastVideo/Matrix-Game-2.0-TempleRun-Diffusers`<br />`mignonjia/mg_longtuning_distilled_zelda`<br />`mignonjia/mg_sf_distilled_zelda_1k_steps`<br />`mignonjia/mg_sf_distilled_zelda`<br />`mignonjia/mg_causal_zelda`<br />`mignonjia/mg_bidirectional_zelda` | I2V | [basic_matrixgame2.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_matrixgame2.py) |
|
||||
| matrixgame | `FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers` | I2V | [basic_matrixgame3.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_matrixgame3.py) |
|
||||
| minimax_h3 | `MiniMaxAI/MiniMax-H3` | T2V, I2V | [T2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_minimax_h3_t2v.py)<br />[FL2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_minimax_h3_fl2va.py)<br />[Ref2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_minimax_h3_ref2va.py) |
|
||||
| pika_h3_flex | `Pika-Labs/Pika-H3-Flex` | T2V, I2V | [T2VA/I2VA/L2VA/FL2VA/Ref2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_pika_h3_flex.py) |
|
||||
| sd35 | `stabilityai/stable-diffusion-3.5-medium` | T2I | [basic_sd35_t2i.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_sd35_t2i.py) |
|
||||
| qwen_image21 | `Qwen/Qwen-Image-2.1` | T2I, I2I | [basic_qwen_image21.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_qwen_image21.py) |
|
||||
| stable_audio | `FastVideo/stable-audio-open-1.0-Diffusers` | T2V | [basic_stable_audio.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_stable_audio.py) |
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Generate synchronized video/audio with Pika H3 Flex.
|
||||
|
||||
Five task shapes share one model release:
|
||||
|
||||
``t2va`` text only
|
||||
``i2va`` first image
|
||||
``l2va`` last image
|
||||
``fl2va`` first and last images
|
||||
``ref2va`` ordered image/video/audio references
|
||||
|
||||
The six-forward ladder is part of the adapter contract; this script deliberately
|
||||
does not expose a step-count flag. ``--lossless-accel`` turns on the existing
|
||||
training-free FastVideo switches (bit-identical kernels, reference memo,
|
||||
parallel VAE tiles) before the generator starts. Kernel/VSA work is separate.
|
||||
|
||||
Examples:
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
|
||||
--prompt "a paper boat drifting down a rainy street"
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task fl2va \
|
||||
--image first.png --last-image last.png --prompt "the flower opens"
|
||||
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
|
||||
--reference image:person.png --reference video:dance.mp4 \
|
||||
--reference audio:voice.wav --prompt "the person dances to the music"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.api import (
|
||||
CompileConfig,
|
||||
ComponentConfig,
|
||||
EngineConfig,
|
||||
GenerationRequest,
|
||||
GeneratorConfig,
|
||||
InputConfig,
|
||||
OffloadConfig,
|
||||
OutputConfig,
|
||||
ParallelismConfig,
|
||||
PipelineSelection,
|
||||
SamplingConfig,
|
||||
)
|
||||
from fastvideo.pipelines.basic.minimax_h3 import MiniMaxH3Reference
|
||||
|
||||
# Training-free switches FastVideo already exposes. Workers inherit the
|
||||
# environment, so they must be set before VideoGenerator starts.
|
||||
LOSSLESS_ACCEL_ENV = {
|
||||
"FASTVIDEO_MINIMAX_H3_EXACT_KERNELS": "all",
|
||||
"FASTVIDEO_H3_REF2VA_MEMO_ENTRIES": "16",
|
||||
"FASTVIDEO_H3_VAE_TILE_PARALLEL": "1",
|
||||
"FASTVIDEO_ULYSSES_A2A": "auto",
|
||||
}
|
||||
|
||||
TASKS = ("t2va", "i2va", "l2va", "fl2va", "ref2va")
|
||||
|
||||
|
||||
def _reference(value: str) -> MiniMaxH3Reference:
|
||||
try:
|
||||
media_type, source = value.split(":", 1)
|
||||
except ValueError as error:
|
||||
raise argparse.ArgumentTypeError("A reference must be image:PATH, video:PATH, or audio:PATH.") from error
|
||||
if media_type not in ("image", "video", "audio"):
|
||||
raise argparse.ArgumentTypeError(f"Unknown reference type {media_type!r}.")
|
||||
return MiniMaxH3Reference(source=source, media_type=media_type)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--model-path", default="Pika-Labs/Pika-H3-Flex")
|
||||
parser.add_argument("--task", choices=TASKS, default="t2va")
|
||||
parser.add_argument("--prompt", required=True)
|
||||
parser.add_argument("--image", help="first frame (i2va/fl2va)")
|
||||
parser.add_argument("--last-image", help="last frame (l2va/fl2va)")
|
||||
parser.add_argument("--reference",
|
||||
action="append",
|
||||
type=_reference,
|
||||
default=[],
|
||||
help="ordered reference (repeatable; ref2va)")
|
||||
parser.add_argument("--height", type=int, default=768)
|
||||
parser.add_argument("--width", type=int, default=1344)
|
||||
parser.add_argument("--num-frames", type=int, default=124)
|
||||
parser.add_argument("--seed", type=int, default=0)
|
||||
parser.add_argument("--num-gpus", type=int, default=1)
|
||||
parser.add_argument("--compile", action="store_true", help="torch.compile the DiT")
|
||||
parser.add_argument("--lossless-accel", action="store_true", help="enable bit-identical speed-ups")
|
||||
parser.add_argument("--low-vram", action="store_true", help="stream the DiT layerwise")
|
||||
parser.add_argument("--output", default="outputs/pika_h3_flex")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.task in ("i2va", "fl2va") and not args.image:
|
||||
parser.error(f"--task {args.task} needs --image")
|
||||
if args.task in ("l2va", "fl2va") and not args.last_image:
|
||||
parser.error(f"--task {args.task} needs --last-image")
|
||||
if args.task == "ref2va" and not args.reference:
|
||||
parser.error("--task ref2va needs at least one --reference")
|
||||
if args.task != "ref2va" and args.reference:
|
||||
parser.error("--reference is only valid with --task ref2va")
|
||||
return args
|
||||
|
||||
|
||||
def _inputs(args: argparse.Namespace) -> InputConfig:
|
||||
if args.task == "ref2va":
|
||||
return InputConfig(references=args.reference)
|
||||
return InputConfig(image_path=args.image, last_image=args.last_image)
|
||||
|
||||
|
||||
def _pipeline(args: argparse.Namespace) -> PipelineSelection:
|
||||
components = ComponentConfig()
|
||||
if args.task == "ref2va":
|
||||
components.override_pipeline_cls_name = "PikaH3FlexRef2VAModularPipeline"
|
||||
return PipelineSelection(
|
||||
workload_type="t2v" if args.task == "t2va" else "i2v",
|
||||
preset=f"pika_h3_flex_{args.task}",
|
||||
components=components,
|
||||
vae_tiling=True,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.lossless_accel:
|
||||
for key, value in LOSSLESS_ACCEL_ENV.items():
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
output_path = Path(args.output) / f"pika_h3_flex_{args.task}.mp4"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
offload = OffloadConfig(
|
||||
dit=False,
|
||||
dit_layerwise=args.low_vram,
|
||||
text_encoder=True,
|
||||
vae=True,
|
||||
pin_cpu_memory=not args.low_vram,
|
||||
lazy_module_load=True,
|
||||
)
|
||||
generator = VideoGenerator.from_config(
|
||||
GeneratorConfig(
|
||||
model_path=args.model_path,
|
||||
engine=EngineConfig(
|
||||
num_gpus=args.num_gpus,
|
||||
use_fsdp_inference=args.num_gpus > 1,
|
||||
parallelism=ParallelismConfig(tp_size=1, sp_size=args.num_gpus),
|
||||
offload=offload,
|
||||
compile=CompileConfig(enabled=args.compile),
|
||||
),
|
||||
pipeline=_pipeline(args),
|
||||
))
|
||||
try:
|
||||
result = generator.generate(
|
||||
GenerationRequest(
|
||||
prompt=args.prompt,
|
||||
negative_prompt="",
|
||||
inputs=_inputs(args),
|
||||
sampling=SamplingConfig(
|
||||
height=args.height,
|
||||
width=args.width,
|
||||
num_frames=args.num_frames,
|
||||
fps=24,
|
||||
num_inference_steps=6,
|
||||
guidance_scale=1.0,
|
||||
batch_cfg=False,
|
||||
seed=args.seed,
|
||||
),
|
||||
output=OutputConfig(
|
||||
output_path=str(output_path),
|
||||
save_video=True,
|
||||
return_frames=False,
|
||||
),
|
||||
))
|
||||
print(f"Output written to: {result.video_path}")
|
||||
finally:
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Sampling defaults for Pika H3 Flex."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastvideo.api.sampling_param import SamplingParam
|
||||
|
||||
|
||||
@dataclass
|
||||
class PikaH3FlexSamplingParam(SamplingParam):
|
||||
"""Six re-noise forwards, no guidance, and the 768p bucket by default."""
|
||||
|
||||
num_inference_steps: int = 6
|
||||
guidance_scale: float = 1.0
|
||||
batch_cfg: bool = False
|
||||
negative_prompt: str = ""
|
||||
height: int = 768
|
||||
width: int = 1344
|
||||
num_frames: int = 124
|
||||
fps: int = 24
|
||||
seed: int = 0
|
||||
|
||||
|
||||
__all__ = ["PikaH3FlexSamplingParam"]
|
||||
@@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex transformer configuration.
|
||||
|
||||
The released Pika adapters do not change MiniMax-H3's transformer: the
|
||||
checkpoint keeps the same packed video/audio trunk, Qwen3-VL conditioner, and
|
||||
VAEs, and applies a rank-128 PEFT adapter at inference. This subclass exists so
|
||||
the Pika pipeline names its own component config without duplicating the H3
|
||||
architecture contract.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastvideo.configs.models.dits.minimax_h3 import MiniMaxH3Config
|
||||
|
||||
|
||||
@dataclass
|
||||
class PikaH3FlexDiTConfig(MiniMaxH3Config):
|
||||
"""The released MiniMax-H3 transformer, sampled through Pika's adapters."""
|
||||
|
||||
|
||||
__all__ = ["PikaH3FlexDiTConfig"]
|
||||
@@ -0,0 +1,86 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pipeline configuration for Pika H3 Flex.
|
||||
|
||||
Pika H3 Flex is a post-trained MiniMax-H3: the transformer, Qwen3-VL
|
||||
conditioner, and video/audio VAEs are the released H3 components, and a rank-128
|
||||
adapter is merged at load time. What changes is the sampler and the geometry
|
||||
contract:
|
||||
|
||||
* six transformer forwards over the fixed clean-time knots
|
||||
``1000, 750, 500, 250, 100, 40``, with a fresh-noise re-noise step between
|
||||
forwards (the terminal ``0`` knot is the output, never a forward);
|
||||
* video and audio clock shifts of 12 and 3;
|
||||
* 480p and 768p canvas buckets instead of a free-form canvas;
|
||||
* center-cropped keyframes and tier-dependent reference-image sizing.
|
||||
|
||||
Everything else, including CFG being off and the 24 fps frame grid, is inherited
|
||||
from MiniMax-H3.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastvideo.configs.models.dits.pika_h3_flex import PikaH3FlexDiTConfig
|
||||
from fastvideo.configs.pipelines.minimax_h3 import MiniMaxH3PipelineConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.api.sampling_param import SamplingParam
|
||||
from fastvideo.api.schema import GenerationRequest
|
||||
|
||||
# The released re-noise ladder. The terminal 0 is a sigma-grid point, not a
|
||||
# transformer forward: six forwards, seven sigmas.
|
||||
PIKA_DENOISING_STEPS: tuple[int, ...] = (1000, 750, 500, 250, 100, 40)
|
||||
PIKA_VIDEO_SCHEDULER_SHIFT = 12.0
|
||||
PIKA_AUDIO_SCHEDULER_SHIFT = 3.0
|
||||
PIKA_RESOLUTION_TIERS: tuple[str, ...] = ("p480", "p768")
|
||||
PIKA_DEFAULT_RESOLUTION_TIER = "p768"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PikaH3FlexPipelineConfig(MiniMaxH3PipelineConfig):
|
||||
"""MiniMax-H3 components under Pika's adapter, sampler, and bucket contract."""
|
||||
|
||||
dit_config: PikaH3FlexDiTConfig = field(default_factory=PikaH3FlexDiTConfig)
|
||||
# Pika's CLI calls this ``--resolution``; the API has no dedicated tier
|
||||
# field, so it is a pipeline config knob rather than a per-request one.
|
||||
pika_resolution_tier: str = PIKA_DEFAULT_RESOLUTION_TIER
|
||||
pika_denoising_steps: tuple[int, ...] = PIKA_DENOISING_STEPS
|
||||
|
||||
def check_pipeline_config(self) -> None:
|
||||
super().check_pipeline_config()
|
||||
if self.pika_resolution_tier not in PIKA_RESOLUTION_TIERS:
|
||||
raise ValueError(f"Pika H3 Flex supports resolution tiers {list(PIKA_RESOLUTION_TIERS)}; got "
|
||||
f"{self.pika_resolution_tier!r}.")
|
||||
if tuple(self.pika_denoising_steps) != PIKA_DENOISING_STEPS:
|
||||
raise ValueError(f"Pika H3 Flex's adapter contract fixes the denoising ladder at "
|
||||
f"{list(PIKA_DENOISING_STEPS)}; got {list(self.pika_denoising_steps)}.")
|
||||
if self.dmd_denoising_steps is not None:
|
||||
raise ValueError("Pika H3 Flex uses its own six-forward re-noise schedule; dmd_denoising_steps must "
|
||||
"remain unset.")
|
||||
if self.pdd_step_indices is not None:
|
||||
raise ValueError("Pika H3 Flex is not a FastH3 PDD student; pdd_step_indices must remain unset.")
|
||||
|
||||
def apply_request_constraints(self, request: GenerationRequest, sampling_param: SamplingParam) -> SamplingParam:
|
||||
"""Fix ``num_inference_steps`` to the adapter's six-forward ladder."""
|
||||
from fastvideo.api.compat import explicit_request_updates
|
||||
|
||||
steps = len(PIKA_DENOISING_STEPS)
|
||||
requested = explicit_request_updates(request).get("num_inference_steps", steps)
|
||||
if requested != steps:
|
||||
raise ValueError(f"Pika H3 Flex runs exactly {steps} transformer forwards; the request sets "
|
||||
f"num_inference_steps={requested}. The six-step ladder is part of the adapter "
|
||||
f"contract, not a quality/speed knob.")
|
||||
sampling_param.num_inference_steps = steps
|
||||
return sampling_param
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PIKA_AUDIO_SCHEDULER_SHIFT",
|
||||
"PIKA_DEFAULT_RESOLUTION_TIER",
|
||||
"PIKA_DENOISING_STEPS",
|
||||
"PIKA_RESOLUTION_TIERS",
|
||||
"PIKA_VIDEO_SCHEDULER_SHIFT",
|
||||
"PikaH3FlexPipelineConfig",
|
||||
]
|
||||
@@ -21,13 +21,31 @@ class MiniMaxH3Scheduler(SchedulerMixin, ConfigMixin):
|
||||
order = 1
|
||||
|
||||
@register_to_config
|
||||
def __init__(self, shift: float = 12.0) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
shift: float = 12.0,
|
||||
stochastic_sampling: bool = False,
|
||||
shift_provided_sigmas: bool = False,
|
||||
) -> None:
|
||||
"""H3's rectified-flow scheduler, with Pika's re-noise step as an option.
|
||||
|
||||
``stochastic_sampling`` switches the Euler update to the re-noise
|
||||
contract: draw fresh Gaussian noise at the next sigma and form
|
||||
``(1 - sigma_next) * x0 + sigma_next * noise``. The terminal sigma
|
||||
always returns ``x0`` without drawing noise. ``shift_provided_sigmas``
|
||||
records that explicit sigmas are already shifted (Pika's release uses
|
||||
it); FastVideo always treats explicit ``set_timesteps(sigmas=...)``
|
||||
values that way, so it is informational.
|
||||
"""
|
||||
if shift <= 0:
|
||||
raise ValueError(f"`shift` must be positive, got {shift}.")
|
||||
self.num_inference_steps: int | None = None
|
||||
self.sigmas: torch.Tensor | None = None
|
||||
self.timesteps: torch.Tensor | None = None
|
||||
self._shift = float(shift)
|
||||
self._stochastic_sampling = bool(stochastic_sampling)
|
||||
self._shift_provided_sigmas = bool(shift_provided_sigmas)
|
||||
self._noise_generator: torch.Generator | None = None
|
||||
self._step_index: int | None = None
|
||||
self._begin_index: int | None = None
|
||||
|
||||
@@ -35,6 +53,18 @@ class MiniMaxH3Scheduler(SchedulerMixin, ConfigMixin):
|
||||
def shift(self) -> float:
|
||||
return self._shift
|
||||
|
||||
@property
|
||||
def stochastic_sampling(self) -> bool:
|
||||
return self._stochastic_sampling
|
||||
|
||||
@property
|
||||
def shift_provided_sigmas(self) -> bool:
|
||||
return self._shift_provided_sigmas
|
||||
|
||||
def set_noise_generator(self, generator: torch.Generator | None) -> None:
|
||||
"""Set the generator used by stochastic (re-noise) steps."""
|
||||
self._noise_generator = generator
|
||||
|
||||
@property
|
||||
def step_index(self) -> int | None:
|
||||
return self._step_index
|
||||
@@ -127,11 +157,29 @@ class MiniMaxH3Scheduler(SchedulerMixin, ConfigMixin):
|
||||
denoised = sample + sigma_from_timestep * model_output
|
||||
|
||||
compute_dtype = torch.float32 if sample.dtype in (torch.float16, torch.bfloat16) else sample.dtype
|
||||
sigma = self.sigmas[self._step_index].to(device=sample.device, dtype=compute_dtype)
|
||||
sigma_next = self.sigmas[self._step_index + 1].to(device=sample.device, dtype=compute_dtype)
|
||||
ratio = sigma_next / sigma
|
||||
prev_sample = ratio * sample.to(compute_dtype) + (1.0 - ratio) * denoised.to(compute_dtype)
|
||||
prev_sample = prev_sample.to(sample.dtype)
|
||||
if self._stochastic_sampling:
|
||||
if self._noise_generator is None:
|
||||
raise ValueError("Stochastic MiniMax-H3 sampling needs a noise generator; call "
|
||||
"`set_noise_generator` before `step`.")
|
||||
if float(sigma_next) == 0.0:
|
||||
# The terminal ladder point is the generated sample. Pika's
|
||||
# sampler returns x0 here instead of drawing a final noise.
|
||||
prev_sample = denoised.to(sample.dtype)
|
||||
else:
|
||||
noise = torch.randn(
|
||||
sample.shape,
|
||||
generator=self._noise_generator,
|
||||
device=sample.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
prev_sample = ((1.0 - sigma_next) * denoised.to(compute_dtype) +
|
||||
sigma_next * noise).to(sample.dtype)
|
||||
else:
|
||||
sigma = self.sigmas[self._step_index].to(device=sample.device, dtype=compute_dtype)
|
||||
ratio = sigma_next / sigma
|
||||
prev_sample = ratio * sample.to(compute_dtype) + (1.0 - ratio) * denoised.to(compute_dtype)
|
||||
prev_sample = prev_sample.to(sample.dtype)
|
||||
self._step_index += 1
|
||||
|
||||
if not return_dict:
|
||||
|
||||
@@ -218,6 +218,10 @@ class MiniMaxH3BasePipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
|
||||
pipeline_config_cls: type[MiniMaxH3PipelineConfig] = MiniMaxH3PipelineConfig
|
||||
_ref2va_default = False
|
||||
# Family subclasses override these to swap geometry and sampler stages
|
||||
# without copying the whole stage assembly.
|
||||
input_preparation_stage_cls: type[MiniMaxH3InputPreparationStage] = MiniMaxH3InputPreparationStage
|
||||
denoising_stage_cls: type[MiniMaxH3DenoisingStage] = MiniMaxH3DenoisingStage
|
||||
_required_config_modules = [
|
||||
"text_encoder",
|
||||
"tokenizer",
|
||||
@@ -686,7 +690,7 @@ class MiniMaxH3BasePipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
def _add_input_stage(self, fastvideo_args: FastVideoArgs, *, ref2va: bool) -> None:
|
||||
self.add_stage(
|
||||
"input_preparation_stage",
|
||||
MiniMaxH3InputPreparationStage(
|
||||
self.input_preparation_stage_cls(
|
||||
vae=self._input_video_geometry(fastvideo_args),
|
||||
audio_vae=self._input_audio_vae(fastvideo_args, ref2va=ref2va),
|
||||
ref2va=ref2va,
|
||||
@@ -734,7 +738,7 @@ class MiniMaxH3BasePipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
)
|
||||
self.add_stage(
|
||||
"denoising_stage",
|
||||
MiniMaxH3DenoisingStage(
|
||||
self.denoising_stage_cls(
|
||||
transformer=transformer,
|
||||
scheduler=scheduler,
|
||||
audio_scheduler=audio_scheduler,
|
||||
|
||||
@@ -236,20 +236,33 @@ def build_packed_batch_sequence(documents: tuple[MiniMaxH3PackedLayout, ...], )
|
||||
)
|
||||
|
||||
|
||||
def resolve_canvas_size(aspect_width: float, aspect_height: float) -> tuple[int, int]:
|
||||
def resolve_canvas_size(
|
||||
aspect_width: float,
|
||||
aspect_height: float,
|
||||
*,
|
||||
short_edge: int = MINIMAX_H3_SHORT_EDGE,
|
||||
max_pixels: int = MINIMAX_H3_MAX_PIXELS,
|
||||
) -> tuple[int, int]:
|
||||
"""Resolve a canvas from an aspect ratio and a tier's short-edge/area rules.
|
||||
|
||||
The defaults are the released MiniMax-H3 768p rule. Pika H3 Flex passes its
|
||||
own 480p/768p tier values through the keyword arguments.
|
||||
"""
|
||||
if aspect_width <= 0 or aspect_height <= 0:
|
||||
raise ValueError(f"The aspect ratio must be positive, got {aspect_width}:{aspect_height}.")
|
||||
if short_edge <= 0 or max_pixels <= 0:
|
||||
raise ValueError(f"Canvas short_edge and max_pixels must be positive, got {short_edge}, {max_pixels}.")
|
||||
ratio = aspect_width / aspect_height
|
||||
if not MINIMAX_H3_MIN_ASPECT_RATIO <= ratio <= MINIMAX_H3_MAX_ASPECT_RATIO:
|
||||
raise ValueError(f"MiniMax-H3 supports aspect ratios from 1:4 to 4:1, got {aspect_width}:{aspect_height}.")
|
||||
|
||||
if ratio >= 1:
|
||||
width, height = MINIMAX_H3_SHORT_EDGE * ratio, float(MINIMAX_H3_SHORT_EDGE)
|
||||
width, height = short_edge * ratio, float(short_edge)
|
||||
else:
|
||||
width, height = float(MINIMAX_H3_SHORT_EDGE), MINIMAX_H3_SHORT_EDGE / ratio
|
||||
width, height = float(short_edge), short_edge / ratio
|
||||
area = width * height
|
||||
if area > MINIMAX_H3_MAX_PIXELS:
|
||||
scale = (MINIMAX_H3_MAX_PIXELS / area)**0.5
|
||||
if area > max_pixels:
|
||||
scale = (max_pixels / area)**0.5
|
||||
width, height = width * scale, height * scale
|
||||
multiple = MINIMAX_H3_CANVAS_MULTIPLE
|
||||
return max(multiple, round(height / multiple) * multiple), max(multiple, round(width / multiple) * multiple)
|
||||
|
||||
@@ -22,6 +22,8 @@ from fastvideo.pipelines.basic.minimax_h3.packing import (
|
||||
MINIMAX_H3_FRAMES_PER_CHUNK,
|
||||
MINIMAX_H3_LATENTS_PER_CHUNK,
|
||||
MINIMAX_H3_MAX_DURATION,
|
||||
MINIMAX_H3_MAX_PIXELS,
|
||||
MINIMAX_H3_SHORT_EDGE,
|
||||
resolve_canvas_size,
|
||||
)
|
||||
|
||||
@@ -180,13 +182,20 @@ def decode_reference_video(source: str | os.PathLike[str]) -> tuple[np.ndarray,
|
||||
return result, float(rate), soundtrack
|
||||
|
||||
|
||||
def resolve_reference_image_size(width: int, height: int) -> tuple[int, int]:
|
||||
"""Resolve the released 2048-short-edge reference-image canvas."""
|
||||
def resolve_reference_image_size(
|
||||
width: int,
|
||||
height: int,
|
||||
*,
|
||||
short_edge: int = MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE,
|
||||
) -> tuple[int, int]:
|
||||
"""Resolve the released reference-image canvas at ``short_edge``."""
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError(f"A reference image must have a positive size, got {width}x{height}.")
|
||||
if width > 4 * height or height > 4 * width:
|
||||
raise ValueError(f"A reference image must be within 1:4 and 4:1, got {width}x{height}.")
|
||||
scale = MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE / min(width, height)
|
||||
if short_edge <= 0:
|
||||
raise ValueError(f"A reference-image short edge must be positive, got {short_edge}.")
|
||||
scale = short_edge / min(width, height)
|
||||
multiple = MINIMAX_H3_CANVAS_MULTIPLE
|
||||
return (
|
||||
max(multiple,
|
||||
@@ -231,12 +240,23 @@ def resample_reference_frames(frames: np.ndarray, fps: float) -> np.ndarray:
|
||||
return np.repeat(frames, repeats, axis=0)
|
||||
|
||||
|
||||
def prepare_reference_frames(frames: np.ndarray, num_frames: int) -> np.ndarray:
|
||||
def prepare_reference_frames(
|
||||
frames: np.ndarray,
|
||||
num_frames: int,
|
||||
*,
|
||||
short_edge: int = MINIMAX_H3_SHORT_EDGE,
|
||||
max_pixels: int = MINIMAX_H3_MAX_PIXELS,
|
||||
) -> np.ndarray:
|
||||
"""Trim a reference video to the request and resize every RGB frame."""
|
||||
if frames.ndim != 4 or frames.shape[-1] != 3 or frames.shape[0] == 0:
|
||||
raise ValueError(f"A reference video must be non-empty RGB frames, got {tuple(frames.shape)}.")
|
||||
frames = frames[:num_frames]
|
||||
height, width = resolve_canvas_size(frames.shape[2], frames.shape[1])
|
||||
height, width = resolve_canvas_size(
|
||||
frames.shape[2],
|
||||
frames.shape[1],
|
||||
short_edge=short_edge,
|
||||
max_pixels=max_pixels,
|
||||
)
|
||||
if frames.shape[1:3] == (height, width):
|
||||
return frames
|
||||
return np.stack(
|
||||
@@ -319,8 +339,17 @@ def prepare_reference(
|
||||
reference: MiniMaxH3Reference,
|
||||
num_frames: int,
|
||||
target_sample_rate: int,
|
||||
*,
|
||||
reference_short_edge: int = MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE,
|
||||
reference_canvas_short_edge: int = MINIMAX_H3_SHORT_EDGE,
|
||||
reference_canvas_max_pixels: int = MINIMAX_H3_MAX_PIXELS,
|
||||
) -> MiniMaxH3PreparedReference:
|
||||
"""Decode and normalize one deferred Ref2VA medium."""
|
||||
"""Decode and normalize one deferred Ref2VA medium.
|
||||
|
||||
The keyword arguments let a family use its own tier's reference-image short
|
||||
edge and reference-video canvas rule. MiniMax-H3's released values are the
|
||||
defaults; Pika H3 Flex passes its 480p/768p tier values.
|
||||
"""
|
||||
prepared = MiniMaxH3PreparedReference(media_type=reference.media_type)
|
||||
if reference.media_type == "image":
|
||||
source = reference.source
|
||||
@@ -331,7 +360,7 @@ def prepare_reference(
|
||||
raise ValueError(f"An image reference must be RGB, got {tuple(pixels.shape)}.")
|
||||
image = Image.fromarray(pixels)
|
||||
image = ImageOps.exif_transpose(image).convert("RGB")
|
||||
height, width = resolve_reference_image_size(*image.size)
|
||||
height, width = resolve_reference_image_size(*image.size, short_edge=reference_short_edge)
|
||||
prepared.image = prepare_reference_image(image, height, width)
|
||||
return prepared
|
||||
|
||||
@@ -346,7 +375,12 @@ def prepare_reference(
|
||||
frames = reference_media_to_uint8(reference.source)
|
||||
decoded_fps = float(MINIMAX_H3_FPS)
|
||||
fps = float(reference.fps if reference.fps is not None else decoded_fps)
|
||||
prepared.frames = prepare_reference_frames(resample_reference_frames(frames, fps), num_frames)
|
||||
prepared.frames = prepare_reference_frames(
|
||||
resample_reference_frames(frames, fps),
|
||||
num_frames,
|
||||
short_edge=reference_canvas_short_edge,
|
||||
max_pixels=reference_canvas_max_pixels,
|
||||
)
|
||||
|
||||
if reference.soundtrack is not None:
|
||||
waveform, source_rate = _resolve_audio_source(reference.soundtrack)
|
||||
|
||||
@@ -133,14 +133,21 @@ class MiniMaxH3DenoisingStage(PipelineStage):
|
||||
self.scheduler = scheduler
|
||||
self.audio_scheduler = audio_scheduler
|
||||
|
||||
def _set_dmd_schedule(self, steps: list[int], grid_points: int, device: torch.device) -> None:
|
||||
"""Run the trained rungs, shifting the shared noise clock once per modality."""
|
||||
def _set_shifted_schedule(
|
||||
self,
|
||||
steps: list[int],
|
||||
grid_points: int,
|
||||
device: torch.device,
|
||||
*,
|
||||
label: str = "MiniMax-H3 DMD",
|
||||
) -> None:
|
||||
"""Build one modality-shifted sigma grid from explicit clean-time rungs."""
|
||||
if (not steps or any(type(step) is not int or not 0 < step <= 1000 for step in steps)
|
||||
or any(left <= right for left, right in zip(steps, steps[1:], strict=False))):
|
||||
raise ValueError("MiniMax-H3 DMD rungs must be strictly decreasing integers in (0, 1000].")
|
||||
raise ValueError(f"{label} rungs must be strictly decreasing integers in (0, 1000].")
|
||||
if grid_points != len(steps) + 1:
|
||||
raise ValueError("MiniMax-H3 num_inference_steps counts sigma-grid points: "
|
||||
f"{len(steps)} DMD forwards require {len(steps) + 1} grid points, got {grid_points}.")
|
||||
raise ValueError(f"{label} num_inference_steps counts sigma-grid points: "
|
||||
f"{len(steps)} forwards require {len(steps) + 1} grid points, got {grid_points}.")
|
||||
base = torch.tensor([step / 1000.0 for step in steps] + [0.0], dtype=torch.float32)
|
||||
for scheduler in (self.scheduler, self.audio_scheduler):
|
||||
shift = float(scheduler.shift)
|
||||
@@ -149,6 +156,38 @@ class MiniMaxH3DenoisingStage(PipelineStage):
|
||||
# clean time (1 - sigma); passing integer rungs to step() is wrong.
|
||||
scheduler.set_timesteps(sigmas=sigmas, device=device)
|
||||
|
||||
def _set_dmd_schedule(self, steps: list[int], grid_points: int, device: torch.device) -> None:
|
||||
"""Run the trained rungs, shifting the shared noise clock once per modality."""
|
||||
self._set_shifted_schedule(steps, grid_points, device, label="MiniMax-H3 DMD")
|
||||
|
||||
def _configure_schedules(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
device: torch.device,
|
||||
) -> PDDSamplingPlan | None:
|
||||
"""Install this checkpoint's sigma schedules and return a PDD plan, if any.
|
||||
|
||||
Families with a fixed sampler (Pika H3 Flex) override this hook; the
|
||||
default keeps MiniMax-H3's PDD and DMD checkpoint contracts.
|
||||
"""
|
||||
dmd_steps = fastvideo_args.pipeline_config.dmd_denoising_steps
|
||||
pdd_plan = self._pdd_sampling_plan(fastvideo_args, device)
|
||||
if pdd_plan is not None:
|
||||
# One runtime step fuses one block of heads into their weighted
|
||||
# mean velocity; the ordinary Euler step over the block's two node
|
||||
# sigmas then applies the block's total increment per modality.
|
||||
self.scheduler.set_timesteps(sigmas=pdd_plan.node_sigmas["video"].to(torch.float32), device=device)
|
||||
self.audio_scheduler.set_timesteps(sigmas=pdd_plan.node_sigmas["audio"].to(torch.float32), device=device)
|
||||
logger.info("MiniMax-H3 PDD denoising: %d fused block(s) %s over a %d-interval fine grid.",
|
||||
pdd_plan.num_steps, pdd_plan.block_sizes, pdd_plan.pdd_steps)
|
||||
elif dmd_steps is None:
|
||||
self.scheduler.set_timesteps(batch.num_inference_steps, device=device)
|
||||
self.audio_scheduler.set_timesteps(batch.num_inference_steps, device=device)
|
||||
else:
|
||||
self._set_dmd_schedule(dmd_steps, batch.num_inference_steps, device)
|
||||
return pdd_plan
|
||||
|
||||
def _pdd_sampling_plan(self, fastvideo_args: FastVideoArgs, device: torch.device) -> PDDSamplingPlan | None:
|
||||
"""The fused-block plan of a PDD checkpoint, or None for any other checkpoint.
|
||||
|
||||
@@ -207,21 +246,7 @@ class MiniMaxH3DenoisingStage(PipelineStage):
|
||||
and not fastvideo_args.use_fsdp_inference)
|
||||
device = get_local_torch_device()
|
||||
|
||||
dmd_steps = fastvideo_args.pipeline_config.dmd_denoising_steps
|
||||
pdd_plan = self._pdd_sampling_plan(fastvideo_args, device)
|
||||
if pdd_plan is not None:
|
||||
# One runtime step fuses one block of heads into their weighted
|
||||
# mean velocity; the ordinary Euler step over the block's two node
|
||||
# sigmas then applies the block's total increment per modality.
|
||||
self.scheduler.set_timesteps(sigmas=pdd_plan.node_sigmas["video"].to(torch.float32), device=device)
|
||||
self.audio_scheduler.set_timesteps(sigmas=pdd_plan.node_sigmas["audio"].to(torch.float32), device=device)
|
||||
logger.info("MiniMax-H3 PDD denoising: %d fused block(s) %s over a %d-interval fine grid.",
|
||||
pdd_plan.num_steps, pdd_plan.block_sizes, pdd_plan.pdd_steps)
|
||||
elif dmd_steps is None:
|
||||
self.scheduler.set_timesteps(batch.num_inference_steps, device=device)
|
||||
self.audio_scheduler.set_timesteps(batch.num_inference_steps, device=device)
|
||||
else:
|
||||
self._set_dmd_schedule(dmd_steps, batch.num_inference_steps, device)
|
||||
pdd_plan = self._configure_schedules(batch, fastvideo_args, device)
|
||||
video_timesteps = self.scheduler.timesteps
|
||||
audio_timesteps = self.audio_scheduler.timesteps
|
||||
if video_timesteps is None or audio_timesteps is None:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.pipeline import (
|
||||
EntryClass,
|
||||
PikaH3FlexModularPipeline,
|
||||
PikaH3FlexRef2VAModularPipeline,
|
||||
)
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.adapter import PikaAdapter
|
||||
|
||||
__all__ = [
|
||||
"EntryClass",
|
||||
"PikaAdapter",
|
||||
"PikaH3FlexModularPipeline",
|
||||
"PikaH3FlexRef2VAModularPipeline",
|
||||
]
|
||||
@@ -0,0 +1,264 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Locate and validate Pika H3 Flex's released LoRA adapters.
|
||||
|
||||
Pika H3 Flex is MiniMax-H3 plus one of two rank-128 PEFT adapters: ``fl2va``
|
||||
for text/first-frame/last-frame generation and ``ref2va`` for ordered
|
||||
image/video/audio references. The adapter is part of the model contract, not a
|
||||
user-tunable accelerator, so the pipeline loads it automatically and refuses a
|
||||
manifest whose rank, trunk, or sampler does not match the release.
|
||||
|
||||
This module stays torch-free; the manifest is a plain JSON document and the
|
||||
only optional dependency is ``huggingface_hub`` for a repo-id source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from collections.abc import Mapping
|
||||
|
||||
from fastvideo.configs.pipelines.pika_h3_flex import (
|
||||
PIKA_AUDIO_SCHEDULER_SHIFT,
|
||||
PIKA_DENOISING_STEPS,
|
||||
PIKA_VIDEO_SCHEDULER_SHIFT,
|
||||
)
|
||||
|
||||
PIKA_ADAPTER_FORMAT_VERSION = 1
|
||||
PIKA_ADAPTER_RANK = 128
|
||||
PIKA_ADAPTER_ALPHA = 128
|
||||
PIKA_BASE_REPO = "MiniMaxAI/MiniMax-H3"
|
||||
PIKA_ADAPTER_FILENAME = "adapter.safetensors"
|
||||
PIKA_MANIFEST_FILENAME = "manifest.json"
|
||||
|
||||
PIKA_SAMPLING_CONTRACT = {
|
||||
"knots": (*PIKA_DENOISING_STEPS, 0),
|
||||
"video_shift": PIKA_VIDEO_SCHEDULER_SHIFT,
|
||||
"audio_shift": PIKA_AUDIO_SCHEDULER_SHIFT,
|
||||
"method": "renoise",
|
||||
}
|
||||
PIKA_TARGET_MODULES = (
|
||||
"to_q",
|
||||
"to_k",
|
||||
"to_v",
|
||||
"to_out.0",
|
||||
"ff.net.0.proj",
|
||||
"ff.net.2",
|
||||
)
|
||||
|
||||
TASK_TO_TRUNK = {"fl2va": "transformer", "ref2va": "transformer_ref"}
|
||||
|
||||
|
||||
class PikaAdapterError(ValueError):
|
||||
"""The Pika adapter is missing or does not describe the released model."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PikaAdapter:
|
||||
"""One validated released adapter."""
|
||||
|
||||
task: str
|
||||
trunk: str
|
||||
path: str
|
||||
manifest_path: str
|
||||
manifest: dict[str, Any]
|
||||
|
||||
@property
|
||||
def directory(self) -> str:
|
||||
return str(Path(self.path).parent)
|
||||
|
||||
|
||||
def task_for_pipeline(ref2va: bool) -> str:
|
||||
return "ref2va" if ref2va else "fl2va"
|
||||
|
||||
|
||||
def expected_trunk(task: str) -> str:
|
||||
try:
|
||||
return TASK_TO_TRUNK[task]
|
||||
except KeyError as error:
|
||||
raise PikaAdapterError(
|
||||
f"Unknown Pika H3 Flex task {task!r}; expected one of {sorted(TASK_TO_TRUNK)}.") from error
|
||||
|
||||
|
||||
def _as_int(value: Any, field: str) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
raise PikaAdapterError(f"Pika adapter manifest field {field!r} must be an integer, got {value!r}.")
|
||||
return value
|
||||
|
||||
|
||||
def _as_float(value: Any, field: str) -> float:
|
||||
if isinstance(value, bool) or not isinstance(value, int | float):
|
||||
raise PikaAdapterError(f"Pika adapter manifest field {field!r} must be a number, got {value!r}.")
|
||||
return float(value)
|
||||
|
||||
|
||||
def validate_adapter_manifest(
|
||||
manifest: Mapping[str, Any],
|
||||
*,
|
||||
task: str,
|
||||
pipeline_trunk: str | None = None,
|
||||
) -> None:
|
||||
"""Reject a manifest that is not the released Pika adapter for ``task``."""
|
||||
trunk = expected_trunk(task)
|
||||
if pipeline_trunk is not None and pipeline_trunk != trunk:
|
||||
raise PikaAdapterError(f"Pika H3 Flex task {task!r} loads trunk {trunk!r}, but the pipeline requested "
|
||||
f"{pipeline_trunk!r}.")
|
||||
|
||||
format_version = _as_int(manifest.get("format_version"), "format_version")
|
||||
if format_version != PIKA_ADAPTER_FORMAT_VERSION:
|
||||
raise PikaAdapterError(f"Unsupported Pika adapter manifest format {format_version}; expected "
|
||||
f"{PIKA_ADAPTER_FORMAT_VERSION}.")
|
||||
manifest_task = manifest.get("task")
|
||||
if manifest_task is not None and manifest_task != task:
|
||||
raise PikaAdapterError(f"The adapter at this path is for task {manifest_task!r}, not {task!r}.")
|
||||
rank = _as_int(manifest.get("rank"), "rank")
|
||||
alpha = _as_int(manifest.get("alpha"), "alpha")
|
||||
if rank != PIKA_ADAPTER_RANK or alpha != PIKA_ADAPTER_ALPHA:
|
||||
raise PikaAdapterError(f"Pika H3 Flex adapters are rank {PIKA_ADAPTER_RANK} with alpha "
|
||||
f"{PIKA_ADAPTER_ALPHA}; this manifest says rank {rank}, alpha {alpha}.")
|
||||
manifest_trunk = manifest.get("trunk")
|
||||
if manifest_trunk != trunk:
|
||||
raise PikaAdapterError(f"Pika H3 Flex task {task!r} expects adapter trunk {trunk!r}, got "
|
||||
f"{manifest_trunk!r}.")
|
||||
|
||||
sampling = manifest.get("sampling")
|
||||
if not isinstance(sampling, Mapping):
|
||||
raise PikaAdapterError("Pika adapter manifest is missing its `sampling` contract.")
|
||||
knots = sampling.get("knots")
|
||||
if not isinstance(knots, list | tuple) or tuple(knots) != PIKA_SAMPLING_CONTRACT["knots"]:
|
||||
raise PikaAdapterError(f"Pika adapter sampling knots must be "
|
||||
f"{list(PIKA_SAMPLING_CONTRACT['knots'])}, got {knots!r}.")
|
||||
for field in ("video_shift", "audio_shift"):
|
||||
value = _as_float(sampling.get(field), f"sampling.{field}")
|
||||
expected = float(PIKA_SAMPLING_CONTRACT[field])
|
||||
if abs(value - expected) > 1e-9:
|
||||
raise PikaAdapterError(f"Pika adapter sampling.{field} must be {expected:g}, got {value:g}.")
|
||||
if sampling.get("method") != PIKA_SAMPLING_CONTRACT["method"]:
|
||||
raise PikaAdapterError(f"Pika adapter sampling.method must be "
|
||||
f"{PIKA_SAMPLING_CONTRACT['method']!r}, got {sampling.get('method')!r}.")
|
||||
|
||||
target_modules = manifest.get("target_modules")
|
||||
if not isinstance(target_modules, list | tuple) or tuple(target_modules) != PIKA_TARGET_MODULES:
|
||||
raise PikaAdapterError(f"Pika adapter target_modules must be {list(PIKA_TARGET_MODULES)}, got "
|
||||
f"{target_modules!r}.")
|
||||
|
||||
base = manifest.get("base")
|
||||
if not isinstance(base, Mapping) or base.get("repo_id") != PIKA_BASE_REPO:
|
||||
raise PikaAdapterError(f"Pika adapter base must name {PIKA_BASE_REPO!r}, got {base!r}.")
|
||||
|
||||
|
||||
def _load_manifest(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
except OSError as error:
|
||||
raise PikaAdapterError(f"Cannot read Pika adapter manifest {str(path)!r}: {error}") from error
|
||||
except json.JSONDecodeError as error:
|
||||
raise PikaAdapterError(f"Pika adapter manifest {str(path)!r} is not valid JSON: {error}") from error
|
||||
if not isinstance(manifest, dict):
|
||||
raise PikaAdapterError(f"Pika adapter manifest {str(path)!r} must be a JSON object.")
|
||||
return manifest
|
||||
|
||||
|
||||
def _adapter_in_directory(directory: Path, task: str) -> tuple[Path, Path] | None:
|
||||
nested = directory / task / PIKA_ADAPTER_FILENAME
|
||||
if nested.is_file():
|
||||
manifest = directory / task / PIKA_MANIFEST_FILENAME
|
||||
return nested, manifest
|
||||
flat = directory / PIKA_ADAPTER_FILENAME
|
||||
if flat.is_file():
|
||||
manifest = directory / PIKA_MANIFEST_FILENAME
|
||||
return flat, manifest
|
||||
return None
|
||||
|
||||
|
||||
def _looks_like_repo_id(source: str) -> bool:
|
||||
parts = source.split("/")
|
||||
return len(parts) == 2 and all(parts) and "\\" not in source
|
||||
|
||||
|
||||
def _download_adapter(source: str, task: str, revision: str | None) -> tuple[Path, Path]:
|
||||
try:
|
||||
from huggingface_hub import hf_hub_download
|
||||
except ImportError as error: # pragma: no cover - huggingface_hub is a runtime dependency
|
||||
raise PikaAdapterError("Downloading a Pika adapter by repo id requires huggingface_hub.") from error
|
||||
try:
|
||||
adapter = Path(hf_hub_download(
|
||||
repo_id=source,
|
||||
filename=f"{task}/{PIKA_ADAPTER_FILENAME}",
|
||||
revision=revision,
|
||||
))
|
||||
manifest = Path(
|
||||
hf_hub_download(
|
||||
repo_id=source,
|
||||
filename=f"{task}/{PIKA_MANIFEST_FILENAME}",
|
||||
revision=revision,
|
||||
))
|
||||
except Exception as error:
|
||||
raise PikaAdapterError(f"Could not download the Pika H3 Flex {task} adapter from {source!r}"
|
||||
f"{f' at revision {revision!r}' if revision else ''}: {error}") from error
|
||||
return adapter, manifest
|
||||
|
||||
|
||||
def load_adapter(source: str | os.PathLike[str], *, task: str, revision: str | None = None) -> PikaAdapter:
|
||||
"""Resolve ``source`` (adapter file, release directory, task directory, or repo id)."""
|
||||
trunk = expected_trunk(task)
|
||||
raw = os.fspath(source)
|
||||
path = Path(raw).expanduser()
|
||||
if path.is_file():
|
||||
adapter_path, manifest_path = path, path.with_name(PIKA_MANIFEST_FILENAME)
|
||||
elif path.is_dir():
|
||||
found = _adapter_in_directory(path, task)
|
||||
if found is None:
|
||||
raise PikaAdapterError(f"{raw!r} has no {task}/{PIKA_ADAPTER_FILENAME} or {PIKA_ADAPTER_FILENAME}.")
|
||||
adapter_path, manifest_path = found
|
||||
elif _looks_like_repo_id(raw):
|
||||
adapter_path, manifest_path = _download_adapter(raw, task, revision)
|
||||
else:
|
||||
raise PikaAdapterError(f"Pika adapter path {raw!r} does not exist and is not a Hub repo id.")
|
||||
|
||||
if not adapter_path.is_file():
|
||||
raise PikaAdapterError(f"Pika adapter weights are missing at {str(adapter_path)!r}.")
|
||||
if not manifest_path.is_file():
|
||||
raise PikaAdapterError(f"Pika adapter manifest is missing at {str(manifest_path)!r}; a bare "
|
||||
f"{PIKA_ADAPTER_FILENAME} cannot be checked against the release contract.")
|
||||
manifest = _load_manifest(manifest_path)
|
||||
validate_adapter_manifest(manifest, task=task, pipeline_trunk=trunk)
|
||||
return PikaAdapter(
|
||||
task=task,
|
||||
trunk=trunk,
|
||||
path=str(adapter_path),
|
||||
manifest_path=str(manifest_path),
|
||||
manifest=manifest,
|
||||
)
|
||||
|
||||
|
||||
def adapter_hf_patterns(task: str | None = None) -> list[str]:
|
||||
"""The adapter files the pipeline must include in a partial Hub snapshot."""
|
||||
tasks = tuple(TASK_TO_TRUNK) if task is None else (task, )
|
||||
patterns: list[str] = []
|
||||
for item in tasks:
|
||||
expected_trunk(item)
|
||||
patterns.extend((f"{item}/{PIKA_ADAPTER_FILENAME}", f"{item}/{PIKA_MANIFEST_FILENAME}"))
|
||||
return patterns
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PIKA_ADAPTER_ALPHA",
|
||||
"PIKA_ADAPTER_FILENAME",
|
||||
"PIKA_ADAPTER_FORMAT_VERSION",
|
||||
"PIKA_ADAPTER_RANK",
|
||||
"PIKA_BASE_REPO",
|
||||
"PIKA_MANIFEST_FILENAME",
|
||||
"PIKA_SAMPLING_CONTRACT",
|
||||
"PIKA_TARGET_MODULES",
|
||||
"PikaAdapter",
|
||||
"PikaAdapterError",
|
||||
"TASK_TO_TRUNK",
|
||||
"adapter_hf_patterns",
|
||||
"expected_trunk",
|
||||
"load_adapter",
|
||||
"task_for_pipeline",
|
||||
"validate_adapter_manifest",
|
||||
]
|
||||
@@ -0,0 +1,294 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex resolution buckets and frame grid.
|
||||
|
||||
Pika H3 Flex keeps MiniMax-H3's canvas rule: sides sit on a 32-pixel lattice,
|
||||
a fixed short edge bounds the bucket, and an area cap keeps the token count
|
||||
finite. The open-weight release samples at two of the released tiers, 480p and
|
||||
768p; the hosted API's larger tiers are intentionally not exposed here because
|
||||
the local checkpoints were not trained for them.
|
||||
|
||||
A request selects a bucket, not a free-form canvas. ``height`` and ``width``
|
||||
are therefore aspect hints: an exact bucket is honoured, anything else is
|
||||
snapped to the nearest aspect family of the tier implied by the requested
|
||||
short edge. Keeping this arithmetic in a torch-free module lets the geometry
|
||||
contract be tested on a CPU-only host.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Iterable
|
||||
|
||||
PIKA_FPS = 24
|
||||
PIKA_CANVAS_MULTIPLE = 32
|
||||
PIKA_MIN_ASPECT_RATIO = 0.25
|
||||
PIKA_MAX_ASPECT_RATIO = 4.0
|
||||
|
||||
# Landscape families first, then square, then the mirrored portraits.
|
||||
PIKA_ASPECTS: tuple[str, ...] = (
|
||||
"21:9",
|
||||
"16:9",
|
||||
"3:2",
|
||||
"4:3",
|
||||
"5:4",
|
||||
"1:1",
|
||||
"4:5",
|
||||
"3:4",
|
||||
"2:3",
|
||||
"9:16",
|
||||
"9:21",
|
||||
)
|
||||
|
||||
# 17n + 5 from 2.3 s to 15.1 s at 24 fps. Open-weight requests are limited to
|
||||
# the documented 5-15 s range; the two smaller grid points remain part of the
|
||||
# released grid but are rejected by the request validator.
|
||||
PIKA_FRAME_GRID: tuple[int, ...] = tuple(17 * n + 5 for n in range(3, 22))
|
||||
PIKA_MIN_DURATION = 5.0
|
||||
PIKA_MAX_DURATION = 15.0
|
||||
PIKA_MIN_ALIGNED_FRAMES = 120
|
||||
PIKA_MAX_ALIGNED_FRAMES = 362
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PikaTier:
|
||||
"""One released resolution tier."""
|
||||
|
||||
name: str
|
||||
short_edge: int
|
||||
max_pixels: int
|
||||
reference_short_edge: int
|
||||
|
||||
|
||||
PIKA_TIERS: dict[str, PikaTier] = {
|
||||
"p480": PikaTier("p480", short_edge=480, max_pixels=832 * 480, reference_short_edge=1024),
|
||||
"p768": PikaTier("p768", short_edge=768, max_pixels=1344 * 768, reference_short_edge=2048),
|
||||
}
|
||||
DEFAULT_TIER = "p768"
|
||||
DEFAULT_ASPECT: tuple[int, int] = (16, 9)
|
||||
|
||||
_TIER_ALIASES = {
|
||||
"480p": "p480",
|
||||
"p480": "p480",
|
||||
"480": "p480",
|
||||
"768p": "p768",
|
||||
"p768": "p768",
|
||||
"768": "p768",
|
||||
}
|
||||
|
||||
|
||||
def resolve_tier(value: str | None) -> str:
|
||||
"""Normalize ``480p`` / ``p480`` / ``768p`` / ``p768`` to a released tier name."""
|
||||
if value is None:
|
||||
return DEFAULT_TIER
|
||||
key = str(value).strip().lower()
|
||||
tier = _TIER_ALIASES.get(key)
|
||||
if tier is None:
|
||||
raise ValueError(f"Pika H3 Flex only samples the released 480p and 768p tiers; got {value!r}.")
|
||||
return tier
|
||||
|
||||
|
||||
def tier_rule(tier: str | None) -> PikaTier:
|
||||
name = resolve_tier(tier)
|
||||
return PIKA_TIERS[name]
|
||||
|
||||
|
||||
def _parse_aspect(aspect: str | tuple[int, int] | tuple[float, float]) -> tuple[float, float]:
|
||||
if isinstance(aspect, str):
|
||||
parts = aspect.split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"An aspect ratio must be W:H, got {aspect!r}.")
|
||||
width, height = float(parts[0]), float(parts[1])
|
||||
elif isinstance(aspect, tuple) and len(aspect) == 2:
|
||||
width, height = float(aspect[0]), float(aspect[1])
|
||||
else:
|
||||
raise TypeError(f"An aspect ratio must be a 'W:H' string or a (width, height) pair, got {aspect!r}.")
|
||||
if not (math.isfinite(width) and math.isfinite(height)) or width <= 0 or height <= 0:
|
||||
raise ValueError(f"An aspect ratio must be finite and positive, got {aspect!r}.")
|
||||
return width, height
|
||||
|
||||
|
||||
def aspect_ratio(width: float, height: float) -> float:
|
||||
width, height = _parse_aspect((width, height))
|
||||
return width / height
|
||||
|
||||
|
||||
def canvas_for_aspect(aspect: str | tuple[float, float], tier: str | None = None) -> tuple[int, int]:
|
||||
"""``(height, width)`` of one tier's bucket for an aspect family."""
|
||||
rule = tier_rule(tier)
|
||||
aspect_width, aspect_height = _parse_aspect(aspect)
|
||||
ratio = aspect_width / aspect_height
|
||||
if not PIKA_MIN_ASPECT_RATIO <= ratio <= PIKA_MAX_ASPECT_RATIO:
|
||||
raise ValueError(f"Pika H3 Flex supports aspect ratios from 1:4 to 4:1, got {aspect!r}.")
|
||||
|
||||
if ratio >= 1:
|
||||
width, height = rule.short_edge * ratio, float(rule.short_edge)
|
||||
else:
|
||||
width, height = float(rule.short_edge), rule.short_edge / ratio
|
||||
area = width * height
|
||||
if area > rule.max_pixels:
|
||||
scale = (rule.max_pixels / area)**0.5
|
||||
width, height = width * scale, height * scale
|
||||
multiple = PIKA_CANVAS_MULTIPLE
|
||||
return (
|
||||
max(multiple,
|
||||
round(height / multiple) * multiple),
|
||||
max(multiple,
|
||||
round(width / multiple) * multiple),
|
||||
)
|
||||
|
||||
|
||||
def aspect_families(tier: str | None = None) -> tuple[str, ...]:
|
||||
del tier
|
||||
return PIKA_ASPECTS
|
||||
|
||||
|
||||
def bucket_table(tier: str | None = None) -> tuple[tuple[int, int], ...]:
|
||||
"""All ``(width, height)`` buckets of one tier, in :data:`PIKA_ASPECTS` order."""
|
||||
return tuple((canvas_for_aspect(aspect, tier)[1], canvas_for_aspect(aspect, tier)[0]) for aspect in PIKA_ASPECTS)
|
||||
|
||||
|
||||
def iter_buckets() -> Iterable[tuple[str, str, int, int]]:
|
||||
"""Yield ``(tier, aspect, width, height)`` for every released bucket."""
|
||||
for tier in PIKA_TIERS:
|
||||
for aspect in PIKA_ASPECTS:
|
||||
height, width = canvas_for_aspect(aspect, tier)
|
||||
yield tier, aspect, width, height
|
||||
|
||||
|
||||
def exact_bucket(height: int, width: int) -> tuple[str, str] | None:
|
||||
"""The ``(tier, aspect)`` whose bucket is exactly ``height x width``, if any."""
|
||||
for tier, aspect, bucket_width, bucket_height in iter_buckets():
|
||||
if (bucket_height, bucket_width) == (int(height), int(width)):
|
||||
return tier, aspect
|
||||
return None
|
||||
|
||||
|
||||
def tier_for_canvas(height: int, width: int) -> str:
|
||||
"""The smallest released tier whose short edge covers the canvas."""
|
||||
if height <= 0 or width <= 0:
|
||||
raise ValueError(f"A canvas must be positive, got {height}x{width}.")
|
||||
short = min(int(height), int(width))
|
||||
for tier in sorted(PIKA_TIERS.values(), key=lambda rule: rule.short_edge):
|
||||
if short <= tier.short_edge:
|
||||
return tier.name
|
||||
return max(PIKA_TIERS.values(), key=lambda rule: rule.short_edge).name
|
||||
|
||||
|
||||
def reference_short_edge_for_canvas(height: int, width: int) -> int:
|
||||
"""Reference-image short edge the released tier rule uses for an output canvas."""
|
||||
return PIKA_TIERS[tier_for_canvas(height, width)].reference_short_edge
|
||||
|
||||
|
||||
def _nearest_aspect(ratio: float, tier: str) -> tuple[int, int, str]:
|
||||
best: tuple[float, str, tuple[int, int]] | None = None
|
||||
for aspect in PIKA_ASPECTS:
|
||||
family_ratio = aspect_ratio(*_parse_aspect(aspect))
|
||||
distance = abs(math.log(ratio / family_ratio))
|
||||
if best is None or distance < best[0]:
|
||||
best = (distance, aspect, canvas_for_aspect(aspect, tier))
|
||||
# PIKA_ASPECTS is ordered by family; equal distances keep the earlier one.
|
||||
assert best is not None
|
||||
height, width = best[2]
|
||||
return height, width, best[1]
|
||||
|
||||
|
||||
def nearest_bucket(height: int, width: int, tier: str) -> tuple[int, int, str]:
|
||||
"""Snap ``height x width`` to the closest aspect family of ``tier``."""
|
||||
if height <= 0 or width <= 0:
|
||||
raise ValueError(f"A canvas must be positive, got {height}x{width}.")
|
||||
ratio = width / height
|
||||
if not PIKA_MIN_ASPECT_RATIO - 1e-9 <= ratio <= PIKA_MAX_ASPECT_RATIO + 1e-9:
|
||||
raise ValueError(f"Pika H3 Flex supports aspect ratios from 1:4 to 4:1, got {width}x{height}.")
|
||||
return _nearest_aspect(ratio, tier)
|
||||
|
||||
|
||||
def resolve_canvas(
|
||||
height: int | None,
|
||||
width: int | None,
|
||||
*,
|
||||
default_aspect: str | tuple[float, float] = DEFAULT_ASPECT,
|
||||
default_tier: str | None = DEFAULT_TIER,
|
||||
) -> tuple[int, int, str, str]:
|
||||
"""Resolve a request canvas to ``(height, width, tier, aspect)``.
|
||||
|
||||
With neither side set, the default tier and aspect are used. With both
|
||||
sides set, an exact released bucket is honoured; otherwise the tier follows
|
||||
the requested short edge (``<= 480`` is 480p, everything else 768p) and the
|
||||
canvas snaps to the nearest aspect family. One-sided requests are rejected.
|
||||
"""
|
||||
if (height is None) != (width is None):
|
||||
raise ValueError("Pika H3 Flex `height` and `width` must be passed together, or neither.")
|
||||
if height is None:
|
||||
tier = resolve_tier(default_tier)
|
||||
aspect_width, aspect_height = _parse_aspect(default_aspect)
|
||||
bucket_height, bucket_width = canvas_for_aspect((aspect_width, aspect_height), tier)
|
||||
aspect = PIKA_ASPECTS[0]
|
||||
for family in PIKA_ASPECTS:
|
||||
if canvas_for_aspect(family, tier) == (bucket_height, bucket_width):
|
||||
aspect = family
|
||||
break
|
||||
return bucket_height, bucket_width, tier, aspect
|
||||
|
||||
if not isinstance(height, int) or isinstance(height, bool) or not isinstance(width, int) or isinstance(width, bool):
|
||||
raise TypeError("Pika H3 Flex `height` and `width` must be integers.")
|
||||
if height <= 0 or width <= 0:
|
||||
raise ValueError(f"Pika H3 Flex `height` and `width` must be positive, got {height}x{width}.")
|
||||
|
||||
exact = exact_bucket(height, width)
|
||||
if exact is not None:
|
||||
tier, aspect = exact
|
||||
return height, width, tier, aspect
|
||||
|
||||
tier = "p480" if min(height, width) <= PIKA_TIERS["p480"].short_edge else "p768"
|
||||
bucket_height, bucket_width, aspect = nearest_bucket(height, width, tier)
|
||||
return bucket_height, bucket_width, tier, aspect
|
||||
|
||||
|
||||
def resolve_frames(num_frames: int) -> int:
|
||||
"""Round a request up to Pika's ``17n + 5`` grid inside the 5-15 s range."""
|
||||
if not isinstance(num_frames, int) or isinstance(num_frames, bool):
|
||||
raise TypeError(f"`num_frames` must be an integer, got {type(num_frames).__name__}.")
|
||||
if num_frames < 1:
|
||||
raise ValueError(f"`num_frames` must be positive, got {num_frames}.")
|
||||
aligned = num_frames
|
||||
while aligned % 17 != 5:
|
||||
aligned += 1
|
||||
if not PIKA_MIN_ALIGNED_FRAMES <= aligned <= PIKA_MAX_ALIGNED_FRAMES:
|
||||
raise ValueError(f"Pika H3 Flex generates {PIKA_MIN_DURATION:g}-{PIKA_MAX_DURATION:g} seconds at "
|
||||
f"{PIKA_FPS} fps; aligned num_frames={aligned} is outside the accepted "
|
||||
f"{PIKA_MIN_ALIGNED_FRAMES}-{PIKA_MAX_ALIGNED_FRAMES} frame range.")
|
||||
return aligned
|
||||
|
||||
|
||||
def duration_seconds(num_frames: int) -> float:
|
||||
return int(num_frames) / PIKA_FPS
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_ASPECT",
|
||||
"DEFAULT_TIER",
|
||||
"PIKA_ASPECTS",
|
||||
"PIKA_CANVAS_MULTIPLE",
|
||||
"PIKA_FPS",
|
||||
"PIKA_FRAME_GRID",
|
||||
"PIKA_MAX_ALIGNED_FRAMES",
|
||||
"PIKA_MAX_DURATION",
|
||||
"PIKA_MIN_ALIGNED_FRAMES",
|
||||
"PIKA_MIN_DURATION",
|
||||
"PIKA_TIERS",
|
||||
"PikaTier",
|
||||
"aspect_ratio",
|
||||
"bucket_table",
|
||||
"canvas_for_aspect",
|
||||
"duration_seconds",
|
||||
"exact_bucket",
|
||||
"iter_buckets",
|
||||
"nearest_bucket",
|
||||
"reference_short_edge_for_canvas",
|
||||
"resolve_canvas",
|
||||
"resolve_frames",
|
||||
"resolve_tier",
|
||||
"tier_for_canvas",
|
||||
"tier_rule",
|
||||
]
|
||||
@@ -0,0 +1,107 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex pipeline: MiniMax-H3 components, Pika adapter and sampler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from fastvideo.configs.pipelines.pika_h3_flex import (
|
||||
PIKA_AUDIO_SCHEDULER_SHIFT,
|
||||
PIKA_VIDEO_SCHEDULER_SHIFT,
|
||||
PikaH3FlexPipelineConfig,
|
||||
)
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines.basic.minimax_h3.minimax_h3_pipeline import MiniMaxH3BasePipeline
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.adapter import adapter_hf_patterns, load_adapter, task_for_pipeline
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.stages.denoising import PikaH3FlexDenoisingStage
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.stages.input_preparation import PikaH3FlexInputPreparationStage
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class PikaH3FlexBasePipeline(MiniMaxH3BasePipeline):
|
||||
"""Pika H3 Flex's adapter, bucket geometry, and six-forward re-noise sampler."""
|
||||
|
||||
pipeline_config_cls = PikaH3FlexPipelineConfig
|
||||
input_preparation_stage_cls = PikaH3FlexInputPreparationStage
|
||||
denoising_stage_cls = PikaH3FlexDenoisingStage
|
||||
_ref2va_default = False
|
||||
_pika_adapter: Any = None
|
||||
|
||||
@classmethod
|
||||
def get_hf_download_allow_patterns(cls) -> list[str]:
|
||||
patterns = super().get_hf_download_allow_patterns()
|
||||
# The concrete class picks the adapter, so a T2VA download does not
|
||||
# fetch the 2.7 GB Ref2VA adapter (and vice versa).
|
||||
task = "ref2va" if issubclass(cls, PikaH3FlexRefPipeline) else "fl2va"
|
||||
return [*patterns, *adapter_hf_patterns(task)]
|
||||
|
||||
def _resolve_pika_adapter(self) -> None:
|
||||
"""Point ``lora_path`` at the released adapter before LoRA layers attach.
|
||||
|
||||
``_load_config`` runs before ``LoRAPipeline`` reads ``lora_path``, so the
|
||||
automatic adapter behaves exactly like an explicitly passed one,
|
||||
including the lazy/sequential transformer paths.
|
||||
"""
|
||||
task = task_for_pipeline(self._ref2va)
|
||||
explicit = self.fastvideo_args.lora_path
|
||||
source = explicit if explicit is not None else self.model_path
|
||||
revision = None if explicit is not None else self.fastvideo_args.revision
|
||||
adapter = load_adapter(source, task=task, revision=revision)
|
||||
source_kind = "explicit" if explicit is not None else "released"
|
||||
logger.info("Pika H3 Flex: loaded the %s %s adapter from %s", source_kind, task, adapter.path)
|
||||
self._pika_adapter = adapter
|
||||
self.fastvideo_args.lora_path = adapter.path
|
||||
|
||||
def _load_config(self, model_path: str) -> dict[str, Any]:
|
||||
config = super()._load_config(model_path)
|
||||
self._resolve_pika_adapter()
|
||||
return config
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs) -> None:
|
||||
super().initialize_pipeline(fastvideo_args)
|
||||
expected = {"video": PIKA_VIDEO_SCHEDULER_SHIFT, "audio": PIKA_AUDIO_SCHEDULER_SHIFT}
|
||||
for module_name, modality in (("scheduler", "video"), ("audio_scheduler", "audio")):
|
||||
actual = float(self.get_module(module_name).shift)
|
||||
target = expected[modality]
|
||||
if not math.isfinite(actual) or abs(actual - target) > 1e-9:
|
||||
raise ValueError(f"Pika H3 Flex samples with {modality} shift {target:g}, but "
|
||||
f"{module_name}/scheduler_config.json declares {actual:g}.")
|
||||
|
||||
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs) -> None:
|
||||
self._add_stages(fastvideo_args, ref2va=self._ref2va_default)
|
||||
|
||||
|
||||
class PikaH3FlexPipeline(PikaH3FlexBasePipeline):
|
||||
"""T2VA/I2VA/L2VA/FL2VA pipeline through the ``fl2va`` adapter."""
|
||||
|
||||
_ref2va_default = False
|
||||
|
||||
|
||||
class PikaH3FlexRefPipeline(PikaH3FlexBasePipeline):
|
||||
"""Ordered-reference pipeline through the ``ref2va`` adapter and ``transformer_ref``."""
|
||||
|
||||
_extra_config_module_map = {"transformer": "transformer_ref"}
|
||||
_ref2va_default = True
|
||||
|
||||
|
||||
class PikaH3FlexModularPipeline(PikaH3FlexPipeline):
|
||||
"""Public entry matching the released ``modular_model_index.json`` class name."""
|
||||
|
||||
|
||||
class PikaH3FlexRef2VAModularPipeline(PikaH3FlexRefPipeline):
|
||||
"""Public Ref2VA entry matching the released modular class name."""
|
||||
|
||||
|
||||
EntryClass = [PikaH3FlexModularPipeline, PikaH3FlexRef2VAModularPipeline]
|
||||
|
||||
__all__ = [
|
||||
"EntryClass",
|
||||
"PikaH3FlexBasePipeline",
|
||||
"PikaH3FlexModularPipeline",
|
||||
"PikaH3FlexPipeline",
|
||||
"PikaH3FlexRef2VAModularPipeline",
|
||||
"PikaH3FlexRefPipeline",
|
||||
]
|
||||
@@ -0,0 +1,65 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Inference presets for Pika H3 Flex joint video/audio generation."""
|
||||
|
||||
from fastvideo.api.presets import InferencePreset, PresetStageSpec
|
||||
|
||||
# The six-forward ladder is the adapter's training contract. Exposing a stage
|
||||
# override would let a request silently sample the adapter outside its
|
||||
# distribution, so the denoise stage accepts no overrides.
|
||||
_DENOISE_STAGE = PresetStageSpec(
|
||||
name="denoise",
|
||||
kind="denoising",
|
||||
description="Pika's fixed six-forward re-noise ladder",
|
||||
allowed_overrides=frozenset(),
|
||||
)
|
||||
|
||||
_SHARED_DEFAULTS = {
|
||||
"fps": 24,
|
||||
"guidance_scale": 1.0,
|
||||
"batch_cfg": False,
|
||||
"negative_prompt": "",
|
||||
"num_inference_steps": 6,
|
||||
"seed": 0,
|
||||
"height": 768,
|
||||
"width": 1344,
|
||||
"num_frames": 124,
|
||||
}
|
||||
|
||||
PIKA_H3_FLEX_T2VA = InferencePreset(
|
||||
name="pika_h3_flex_t2va",
|
||||
version=1,
|
||||
model_family="pika_h3_flex",
|
||||
description="Pika H3 Flex text-to-video with synchronized stereo audio at 768p",
|
||||
workload_type="t2v",
|
||||
stage_schemas=(_DENOISE_STAGE, ),
|
||||
defaults=dict(_SHARED_DEFAULTS),
|
||||
)
|
||||
|
||||
PIKA_H3_FLEX_FL2VA = InferencePreset(
|
||||
name="pika_h3_flex_fl2va",
|
||||
version=1,
|
||||
model_family="pika_h3_flex",
|
||||
description="Pika H3 Flex first/last-frame-to-video with synchronized stereo audio",
|
||||
workload_type="i2v",
|
||||
stage_schemas=(_DENOISE_STAGE, ),
|
||||
defaults=dict(_SHARED_DEFAULTS),
|
||||
)
|
||||
|
||||
PIKA_H3_FLEX_REF2VA = InferencePreset(
|
||||
name="pika_h3_flex_ref2va",
|
||||
version=1,
|
||||
model_family="pika_h3_flex",
|
||||
description="Pika H3 Flex ordered image/video/audio references to joint video and stereo audio",
|
||||
workload_type="i2v",
|
||||
stage_schemas=(_DENOISE_STAGE, ),
|
||||
defaults=dict(_SHARED_DEFAULTS),
|
||||
)
|
||||
|
||||
ALL_PRESETS = (PIKA_H3_FLEX_T2VA, PIKA_H3_FLEX_FL2VA, PIKA_H3_FLEX_REF2VA)
|
||||
|
||||
__all__ = [
|
||||
"ALL_PRESETS",
|
||||
"PIKA_H3_FLEX_FL2VA",
|
||||
"PIKA_H3_FLEX_REF2VA",
|
||||
"PIKA_H3_FLEX_T2VA",
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.stages.denoising import PikaH3FlexDenoisingStage
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.stages.input_preparation import PikaH3FlexInputPreparationStage
|
||||
|
||||
__all__ = ["PikaH3FlexDenoisingStage", "PikaH3FlexInputPreparationStage"]
|
||||
@@ -0,0 +1,59 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex's fixed six-forward re-noise denoiser."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.pipelines.basic.minimax_h3.stages.minimax_h3_denoising import MiniMaxH3DenoisingStage
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
|
||||
|
||||
def request_seed(batch: ForwardBatch) -> int:
|
||||
"""The request seed, falling back to the generator's initial seed."""
|
||||
if batch.seed is not None:
|
||||
return int(batch.seed)
|
||||
generator = batch.generator
|
||||
if isinstance(generator, list):
|
||||
generator = generator[0] if generator else None
|
||||
if generator is None:
|
||||
return 0
|
||||
return int(generator.initial_seed())
|
||||
|
||||
|
||||
class PikaH3FlexDenoisingStage(MiniMaxH3DenoisingStage):
|
||||
"""Six full-model forwards over Pika's clean-time ladder.
|
||||
|
||||
Pika's adapter is not a PDD student: every rung is an ordinary transformer
|
||||
forward. What differs from MiniMax-H3 is the ladder and the re-noise step,
|
||||
which draws fresh Gaussian noise from a generator seeded ``seed + 1``.
|
||||
"""
|
||||
|
||||
def _configure_schedules(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
device: torch.device,
|
||||
):
|
||||
steps = list(fastvideo_args.pipeline_config.pika_denoising_steps)
|
||||
if batch.num_inference_steps != len(steps):
|
||||
raise ValueError(f"Pika H3 Flex needs {len(steps)} transformer forwards plus the terminal zero, got "
|
||||
f"num_inference_steps={batch.num_inference_steps}.")
|
||||
for scheduler in (self.scheduler, self.audio_scheduler):
|
||||
if not getattr(scheduler, "stochastic_sampling", False):
|
||||
raise ValueError("Pika H3 Flex needs the released stochastic (re-noise) scheduler; "
|
||||
f"{type(scheduler).__name__} did not load stochastic_sampling=true.")
|
||||
self._set_shifted_schedule(steps, len(steps) + 1, device, label="Pika H3 Flex")
|
||||
return None
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
|
||||
generator = torch.Generator(device=get_local_torch_device()).manual_seed(request_seed(batch) + 1)
|
||||
self.scheduler.set_noise_generator(generator)
|
||||
self.audio_scheduler.set_noise_generator(generator)
|
||||
return super().forward(batch, fastvideo_args)
|
||||
|
||||
|
||||
__all__ = ["PikaH3FlexDenoisingStage", "request_seed"]
|
||||
@@ -0,0 +1,142 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex input preparation: bucket canvas, cropped keyframes, tiered references."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.vision_utils import load_image
|
||||
from fastvideo.pipelines.basic.minimax_h3.packing import prepare_keyframe_image
|
||||
from fastvideo.pipelines.basic.minimax_h3.reference import (
|
||||
MiniMaxH3PreparedReference,
|
||||
prepare_reference,
|
||||
validate_references,
|
||||
)
|
||||
from fastvideo.pipelines.basic.minimax_h3.stages.minimax_h3_input_preparation import (
|
||||
MINIMAX_H3_KEYFRAME_ANCHORS_KEY,
|
||||
MINIMAX_H3_KEYFRAMES_KEY,
|
||||
MiniMaxH3InputPreparationStage,
|
||||
prepare_common_request,
|
||||
)
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.buckets import (
|
||||
PIKA_TIERS,
|
||||
resolve_canvas,
|
||||
resolve_frames,
|
||||
resolve_tier,
|
||||
)
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Pika's released reference-video floor: two seconds at 24 fps is 48 frames,
|
||||
# but the adapter's own validator requires at least 22 decoded frames.
|
||||
PIKA_MIN_REFERENCE_VIDEO_FRAMES = 22
|
||||
|
||||
|
||||
class PikaH3FlexInputPreparationStage(MiniMaxH3InputPreparationStage):
|
||||
"""Prepare T2VA/I2VA/L2VA/FL2VA or Ref2VA inputs on a Pika bucket."""
|
||||
|
||||
def __init__(self, vae: Any, audio_vae: Any | None = None, *, ref2va: bool = False) -> None:
|
||||
super().__init__(vae, audio_vae, ref2va=ref2va)
|
||||
self._tier = "p768"
|
||||
|
||||
def _request_tier(self, fastvideo_args: FastVideoArgs) -> str:
|
||||
return resolve_tier(getattr(fastvideo_args.pipeline_config, "pika_resolution_tier", None))
|
||||
|
||||
def _resolve_canvas(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
default_aspect: tuple[int, int],
|
||||
) -> tuple[int, int, str, str]:
|
||||
height, width, tier, aspect = resolve_canvas(
|
||||
batch.height,
|
||||
batch.width,
|
||||
default_aspect=default_aspect,
|
||||
default_tier=self._tier,
|
||||
)
|
||||
if (batch.height, batch.width) != (height, width):
|
||||
logger.info("Pika H3 Flex snapped canvas %sx%s to %sx%s (%s %s).", batch.height, batch.width, height, width,
|
||||
tier, aspect)
|
||||
ratio = int(self.vae.spatial_compression_ratio)
|
||||
if height % ratio or width % ratio:
|
||||
raise ValueError(f"Pika H3 Flex canvas {height}x{width} is not divisible by VAE ratio {ratio}.")
|
||||
return height, width, tier, aspect
|
||||
|
||||
def _prepare_fl2va(self, batch: ForwardBatch) -> None:
|
||||
if batch.references:
|
||||
raise ValueError("Pika H3 Flex references belong to the Ref2VA pipeline.")
|
||||
|
||||
if batch.image_path is not None:
|
||||
batch.pil_image = load_image(batch.image_path)
|
||||
for name, image in (("pil_image", batch.pil_image), ("last_image", batch.last_image)):
|
||||
if image is not None and not isinstance(image, Image.Image):
|
||||
raise TypeError(f"Pika H3 Flex `{name}` must be a PIL image, got {type(image).__name__}.")
|
||||
|
||||
raw_keyframes = [(anchor, ImageOps.exif_transpose(image).convert("RGB"))
|
||||
for anchor, image in (("first", batch.pil_image), ("last", batch.last_image))
|
||||
if image is not None]
|
||||
default_aspect = raw_keyframes[0][1].size if raw_keyframes else (16, 9)
|
||||
height, width, _, _ = self._resolve_canvas(batch, default_aspect)
|
||||
num_frames = resolve_frames(batch.num_frames)
|
||||
ratio = int(self.vae.spatial_compression_ratio)
|
||||
self._write_target_geometry(batch, height, width, ratio, num_frames)
|
||||
|
||||
batch.extra[MINIMAX_H3_KEYFRAME_ANCHORS_KEY] = tuple(anchor for anchor, _ in raw_keyframes)
|
||||
# Pika crops both endpoints. MiniMax-H3 stretches the first frame; using
|
||||
# that fit here would distort every Pika image on a bucketed canvas.
|
||||
batch.extra[MINIMAX_H3_KEYFRAMES_KEY] = [
|
||||
prepare_keyframe_image(image, height, width, stretch=False) for _, image in raw_keyframes
|
||||
]
|
||||
|
||||
def _prepare_ref2va(self, batch: ForwardBatch) -> None:
|
||||
if batch.image_path is not None or batch.pil_image is not None or batch.last_image is not None:
|
||||
raise ValueError("Pika H3 Flex Ref2VA accepts media through `references`, not FL2VA keyframe fields.")
|
||||
references = validate_references(list(batch.references or []))
|
||||
if self.audio_vae is None:
|
||||
raise RuntimeError("Pika H3 Flex Ref2VA input preparation has no audio VAE.")
|
||||
|
||||
height, width, tier, _ = self._resolve_canvas(batch, (16, 9))
|
||||
num_frames = resolve_frames(batch.num_frames)
|
||||
target_sample_rate = int(self.audio_vae.sampling_rate)
|
||||
rule = PIKA_TIERS[tier]
|
||||
prepared: list[MiniMaxH3PreparedReference] = []
|
||||
for reference in references:
|
||||
item = prepare_reference(
|
||||
reference,
|
||||
num_frames,
|
||||
target_sample_rate,
|
||||
reference_short_edge=rule.reference_short_edge,
|
||||
reference_canvas_short_edge=rule.short_edge,
|
||||
reference_canvas_max_pixels=rule.max_pixels,
|
||||
)
|
||||
if (item.media_type == "video" and item.frames is not None
|
||||
and item.frames.shape[0] < PIKA_MIN_REFERENCE_VIDEO_FRAMES):
|
||||
raise ValueError(f"A Pika H3 Flex reference video needs at least "
|
||||
f"{PIKA_MIN_REFERENCE_VIDEO_FRAMES} decoded frames at 24 fps; got "
|
||||
f"{item.frames.shape[0]}.")
|
||||
prepared.append(item)
|
||||
if not prepared:
|
||||
raise ValueError("Pika H3 Flex Ref2VA requires at least one non-audio reference.")
|
||||
|
||||
batch.references = prepared
|
||||
self._write_target_geometry(batch, height, width, int(self.vae.spatial_compression_ratio), num_frames)
|
||||
batch.extra[MINIMAX_H3_KEYFRAMES_KEY] = []
|
||||
batch.extra[MINIMAX_H3_KEYFRAME_ANCHORS_KEY] = ()
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
|
||||
prepare_common_request(batch)
|
||||
self._tier = self._request_tier(fastvideo_args)
|
||||
if self.ref2va:
|
||||
self._prepare_ref2va(batch)
|
||||
else:
|
||||
self._prepare_fl2va(batch)
|
||||
return batch
|
||||
|
||||
|
||||
__all__ = ["PIKA_MIN_REFERENCE_VIDEO_FRAMES", "PikaH3FlexInputPreparationStage"]
|
||||
@@ -42,6 +42,7 @@ from fastvideo.configs.pipelines.flux_2 import (
|
||||
from fastvideo.configs.pipelines.matrixgame2 import MatrixGame2I2V480PConfig
|
||||
from fastvideo.configs.pipelines.matrixgame3 import MatrixGame3I2V720PConfig
|
||||
from fastvideo.configs.pipelines.minimax_h3 import MiniMaxH3PipelineConfig
|
||||
from fastvideo.configs.pipelines.pika_h3_flex import PikaH3FlexPipelineConfig
|
||||
from fastvideo.configs.pipelines.mmaudio import (
|
||||
MMAudioLarge44kV2PipelineConfig,
|
||||
MMAudioLarge44kV2AConfig,
|
||||
@@ -60,6 +61,7 @@ from fastvideo.api.sampling_param import SamplingParam
|
||||
from fastvideo.api.matrixgame2 import MatrixGame2SamplingParam
|
||||
from fastvideo.api.matrixgame3 import MatrixGame3SamplingParam
|
||||
from fastvideo.api.flux import FluxSamplingParam
|
||||
from fastvideo.api.pika_h3_flex import PikaH3FlexSamplingParam
|
||||
|
||||
from fastvideo.fastvideo_args import WorkloadType
|
||||
from fastvideo.logger import init_logger
|
||||
@@ -1152,6 +1154,26 @@ def _register_configs() -> None:
|
||||
|
||||
_register_wan_configs(WAN_MODEL_DEFINITIONS)
|
||||
|
||||
# Pika H3 Flex. It reuses MiniMax-H3's components, but its six-forward
|
||||
# re-noise sampler and bucket geometry are a separate family contract.
|
||||
register_configs(
|
||||
sampling_param_cls=PikaH3FlexSamplingParam,
|
||||
pipeline_config_cls=PikaH3FlexPipelineConfig,
|
||||
workload_types=(WorkloadType.T2V, WorkloadType.I2V),
|
||||
hf_model_paths=["Pika-Labs/Pika-H3-Flex"],
|
||||
model_detectors=[
|
||||
lambda path: any(token in path.lower() for token in (
|
||||
"pika-h3-flex",
|
||||
"pika_h3_flex",
|
||||
"pikah3flexmodularpipeline",
|
||||
"pikah3flexref2vamodularpipeline",
|
||||
)),
|
||||
],
|
||||
model_family="pika_h3_flex",
|
||||
default_preset="pika_h3_flex_t2va",
|
||||
pipeline_cls_name="PikaH3FlexModularPipeline",
|
||||
)
|
||||
|
||||
# MiniMax H3
|
||||
register_configs(
|
||||
sampling_param_cls=None,
|
||||
@@ -1350,6 +1372,8 @@ def _register_presets() -> None:
|
||||
ALL_PRESETS as MATRIXGAME3_PRESETS, )
|
||||
from fastvideo.pipelines.basic.minimax_h3.presets import (
|
||||
ALL_PRESETS as MINIMAX_H3_PRESETS, )
|
||||
from fastvideo.pipelines.basic.pika_h3_flex.presets import (
|
||||
ALL_PRESETS as PIKA_H3_FLEX_PRESETS, )
|
||||
from fastvideo.pipelines.basic.mmaudio.presets import (
|
||||
ALL_PRESETS as MMAUDIO_PRESETS, )
|
||||
from fastvideo.pipelines.basic.sd35.presets import (
|
||||
@@ -1383,6 +1407,7 @@ def _register_presets() -> None:
|
||||
MATRIXGAME2_PRESETS,
|
||||
MATRIXGAME3_PRESETS,
|
||||
MINIMAX_H3_PRESETS,
|
||||
PIKA_H3_FLEX_PRESETS,
|
||||
MMAUDIO_PRESETS,
|
||||
SD35_PRESETS,
|
||||
STABLE_AUDIO_PRESETS,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex adapter discovery and manifest contract."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fastvideo.pipelines.basic.pika_h3_flex import adapter
|
||||
|
||||
|
||||
def _manifest(task: str = "fl2va") -> dict:
|
||||
return {
|
||||
"format_version": 1,
|
||||
"model": "pika-flex",
|
||||
"task": task,
|
||||
"release_id": "pika-flex-fl2va-v1",
|
||||
"rank": 128,
|
||||
"alpha": 128,
|
||||
"trunk": "transformer_ref" if task == "ref2va" else "transformer",
|
||||
"sampling": {
|
||||
"knots": [1000, 750, 500, 250, 100, 40, 0],
|
||||
"video_shift": 12.0,
|
||||
"audio_shift": 3.0,
|
||||
"method": "renoise",
|
||||
},
|
||||
"base": {"repo_id": "MiniMaxAI/MiniMax-H3", "revision": "42ed227ee7df40d41602854ae760620d6eb651fe"},
|
||||
"target_modules": ["to_q", "to_k", "to_v", "to_out.0", "ff.net.0.proj", "ff.net.2"],
|
||||
"sha256": "0" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _write_release(root: Path, task: str = "fl2va") -> Path:
|
||||
task_dir = root / task
|
||||
task_dir.mkdir(parents=True)
|
||||
(task_dir / "adapter.safetensors").write_bytes(b"")
|
||||
(task_dir / "manifest.json").write_text(json.dumps(_manifest(task)), encoding="utf-8")
|
||||
return task_dir
|
||||
|
||||
|
||||
def test_manifest_matches_released_contract() -> None:
|
||||
adapter.validate_adapter_manifest(_manifest(), task="fl2va")
|
||||
adapter.validate_adapter_manifest(_manifest("ref2va"), task="ref2va")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value", "message"),
|
||||
[
|
||||
("rank", 64, "rank"),
|
||||
("alpha", 64, "alpha"),
|
||||
("trunk", "transformer_ref", "trunk"),
|
||||
("task", "ref2va", "task"),
|
||||
("format_version", 2, "format"),
|
||||
],
|
||||
)
|
||||
def test_manifest_rejects_wrong_release(field: str, value: object, message: str) -> None:
|
||||
manifest = _manifest()
|
||||
manifest[field] = value
|
||||
with pytest.raises(adapter.PikaAdapterError, match=message):
|
||||
adapter.validate_adapter_manifest(manifest, task="fl2va")
|
||||
|
||||
|
||||
def test_manifest_rejects_a_different_sampler() -> None:
|
||||
manifest = _manifest()
|
||||
manifest["sampling"]["knots"] = [1000, 500, 0]
|
||||
with pytest.raises(adapter.PikaAdapterError, match="sampling knots"):
|
||||
adapter.validate_adapter_manifest(manifest, task="fl2va")
|
||||
|
||||
|
||||
def test_load_adapter_finds_the_task_directory(tmp_path: Path) -> None:
|
||||
task_dir = _write_release(tmp_path)
|
||||
loaded = adapter.load_adapter(tmp_path, task="fl2va")
|
||||
assert loaded.path == str(task_dir / "adapter.safetensors")
|
||||
assert loaded.trunk == "transformer"
|
||||
assert loaded.manifest["task"] == "fl2va"
|
||||
|
||||
|
||||
def test_load_adapter_finds_a_task_directory_or_adapter_file(tmp_path: Path) -> None:
|
||||
task_dir = _write_release(tmp_path)
|
||||
assert adapter.load_adapter(task_dir, task="fl2va").path == str(task_dir / "adapter.safetensors")
|
||||
assert adapter.load_adapter(task_dir / "adapter.safetensors", task="fl2va").path == str(
|
||||
task_dir / "adapter.safetensors")
|
||||
|
||||
|
||||
def test_load_adapter_rejects_a_missing_task(tmp_path: Path) -> None:
|
||||
_write_release(tmp_path, "fl2va")
|
||||
with pytest.raises(adapter.PikaAdapterError, match="ref2va"):
|
||||
adapter.load_adapter(tmp_path, task="ref2va")
|
||||
|
||||
|
||||
def test_load_adapter_requires_the_manifest(tmp_path: Path) -> None:
|
||||
task_dir = _write_release(tmp_path)
|
||||
(task_dir / "manifest.json").unlink()
|
||||
with pytest.raises(adapter.PikaAdapterError, match="manifest"):
|
||||
adapter.load_adapter(tmp_path, task="fl2va")
|
||||
@@ -0,0 +1,77 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex bucket geometry contract."""
|
||||
|
||||
import pytest
|
||||
|
||||
from fastvideo.pipelines.basic.pika_h3_flex import buckets
|
||||
|
||||
|
||||
def test_released_bucket_tables_match_pika() -> None:
|
||||
assert buckets.bucket_table("p480") == (
|
||||
(960, 416),
|
||||
(832, 480),
|
||||
(704, 480),
|
||||
(640, 480),
|
||||
(608, 480),
|
||||
(480, 480),
|
||||
(480, 608),
|
||||
(480, 640),
|
||||
(480, 704),
|
||||
(480, 832),
|
||||
(416, 960),
|
||||
)
|
||||
assert buckets.bucket_table("p768") == (
|
||||
(1536, 672),
|
||||
(1344, 768),
|
||||
(1152, 768),
|
||||
(1024, 768),
|
||||
(960, 768),
|
||||
(768, 768),
|
||||
(768, 960),
|
||||
(768, 1024),
|
||||
(768, 1152),
|
||||
(768, 1344),
|
||||
(672, 1536),
|
||||
)
|
||||
|
||||
|
||||
def test_unset_canvas_uses_default_tier_and_aspect() -> None:
|
||||
assert buckets.resolve_canvas(None, None) == (768, 1344, "p768", "16:9")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("height", "width", "expected"),
|
||||
[
|
||||
(720, 1280, (768, 1344, "p768", "16:9")),
|
||||
(1080, 1920, (768, 1344, "p768", "16:9")),
|
||||
(480, 832, (480, 832, "p480", "16:9")),
|
||||
(1024, 768, (1024, 768, "p768", "3:4")),
|
||||
(960, 416, (960, 416, "p480", "9:21")),
|
||||
],
|
||||
)
|
||||
def test_requested_canvas_snaps_to_bucket(height: int, width: int, expected: tuple[int, int, str, str]) -> None:
|
||||
assert buckets.resolve_canvas(height, width) == expected
|
||||
|
||||
|
||||
def test_one_sided_canvas_is_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="together"):
|
||||
buckets.resolve_canvas(720, None)
|
||||
|
||||
|
||||
def test_tier_reference_short_edge_follows_the_output_canvas() -> None:
|
||||
assert buckets.reference_short_edge_for_canvas(480, 832) == 1024
|
||||
assert buckets.reference_short_edge_for_canvas(768, 1344) == 2048
|
||||
|
||||
|
||||
def test_resolve_frames_aligns_to_the_17n_plus_5_grid() -> None:
|
||||
assert buckets.resolve_frames(120) == 124
|
||||
assert buckets.resolve_frames(124) == 124
|
||||
assert buckets.resolve_frames(345) == 345
|
||||
assert buckets.resolve_frames(360) == 362
|
||||
with pytest.raises(ValueError, match="Pika H3 Flex generates"):
|
||||
buckets.resolve_frames(107)
|
||||
|
||||
|
||||
def test_unknown_tier_is_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="released 480p and 768p"):
|
||||
buckets.resolve_tier("1080p")
|
||||
@@ -0,0 +1,44 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Pika H3 Flex's re-noise scheduler step."""
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.models.schedulers.scheduling_minimax_h3 import MiniMaxH3Scheduler
|
||||
|
||||
|
||||
def _scheduler() -> MiniMaxH3Scheduler:
|
||||
scheduler = MiniMaxH3Scheduler(shift=12.0, stochastic_sampling=True, shift_provided_sigmas=True)
|
||||
scheduler.set_timesteps(sigmas=[1.0, 0.5, 0.0], device="cpu")
|
||||
scheduler.set_noise_generator(torch.Generator().manual_seed(7))
|
||||
return scheduler
|
||||
|
||||
|
||||
def test_renoised_step_uses_the_next_sigma_and_fresh_noise() -> None:
|
||||
scheduler = _scheduler()
|
||||
sample = torch.zeros(2, 3)
|
||||
velocity = torch.ones(2, 3)
|
||||
timestep = scheduler.timesteps[0]
|
||||
output = scheduler.step(velocity, timestep, sample).prev_sample
|
||||
|
||||
expected_noise = torch.randn(sample.shape, generator=torch.Generator().manual_seed(7), dtype=torch.float32)
|
||||
torch.testing.assert_close(output, 0.5 * torch.ones_like(sample) + 0.5 * expected_noise)
|
||||
|
||||
|
||||
def test_terminal_renoised_step_returns_x0_without_drawing_noise() -> None:
|
||||
scheduler = _scheduler()
|
||||
_ = scheduler.step(torch.ones(2, 3), scheduler.timesteps[0], torch.zeros(2, 3))
|
||||
state_before = scheduler._noise_generator.get_state()
|
||||
output = scheduler.step(torch.ones(2, 3), scheduler.timesteps[1], torch.zeros(2, 3)).prev_sample
|
||||
state_after = scheduler._noise_generator.get_state()
|
||||
torch.testing.assert_close(output, torch.full((2, 3), 0.5))
|
||||
assert torch.equal(state_before, state_after)
|
||||
|
||||
|
||||
def test_euler_step_is_unchanged_when_stochastic_sampling_is_off() -> None:
|
||||
scheduler = MiniMaxH3Scheduler(shift=12.0)
|
||||
scheduler.set_timesteps(sigmas=[1.0, 0.5, 0.0], device="cpu")
|
||||
sample = torch.full((2, 3), 2.0)
|
||||
velocity = torch.full((2, 3), 4.0)
|
||||
output = scheduler.step(velocity, scheduler.timesteps[0], sample).prev_sample
|
||||
# x0 = 2 + 1 * 4 = 6; ratio = 0.5, so prev = 0.5 * 2 + 0.5 * 6 = 4.
|
||||
torch.testing.assert_close(output, torch.full((2, 3), 4.0))
|
||||
Reference in New Issue
Block a user