|
| 1 | +# SPDX-License-Identifier: Apache-2.0 |
| 2 | +"""End-to-end MiniMax-H3 (FastH3) generation with the Apple Silicon MLX runtime. |
| 3 | +
|
| 4 | +Accepts a text prompt and produces an MP4 with H.264 video at 24 fps and |
| 5 | +stereo AAC audio at 32 kHz. One heavyweight model phase is resident at a time. |
| 6 | +
|
| 7 | + python examples/inference/basic/mlx_fasth3.py \ |
| 8 | + --model-root ~/models/FastH3-Preview-v0.2 \ |
| 9 | + --mlx-checkpoint ~/models/FastH3-MLX/int8 \ |
| 10 | + --prompt '(S1) A red panda says <d>[English] Fast H3 is amazing.</d>' \ |
| 11 | + --height 480 --width 832 --num-frames 124 --seed 2026 \ |
| 12 | + --output-path ~/fasth3_outputs/int8.mp4 |
| 13 | +
|
| 14 | +Conditioning uses the streamed Qwen3-VL text encoder on first use and caches |
| 15 | +the resulting embeddings under --prompt-cache-dir for instant reuse. |
| 16 | +
|
| 17 | +``--fast`` is temporal fast mode. It keeps full-duration audio while |
| 18 | +denoising fewer video frames, then uses MLX RIFE 4.25 to reconstruct the |
| 19 | +requested frame count. A 1280x720 request runs on H3's 1280x736 grid and is |
| 20 | +center-cropped after decode. |
| 21 | +
|
| 22 | +This entrypoint currently supports text-to-video-with-audio only. It does not |
| 23 | +yet wire FL2VA, Ref2VA, spatial fast mode, or two-pass refinement. |
| 24 | +""" |
| 25 | + |
| 26 | +from __future__ import annotations |
| 27 | + |
| 28 | +import argparse |
| 29 | +import json |
| 30 | +from pathlib import Path |
| 31 | + |
| 32 | + |
| 33 | +def parse_args() -> argparse.Namespace: |
| 34 | + parser = argparse.ArgumentParser(description=__doc__, |
| 35 | + formatter_class=argparse.RawDescriptionHelpFormatter) |
| 36 | + parser.add_argument("--model-root", type=Path, default=Path.home() / "models/FastH3-Preview-v0.2", |
| 37 | + help="H3 snapshot root (vae/, audio_vae/, text_encoder/, tokenizer/)") |
| 38 | + parser.add_argument("--mlx-checkpoint", type=Path, required=True, |
| 39 | + help="pre-quantized MLX DiT directory (int8/int6/int4 mlx_h3_dit format)") |
| 40 | + parser.add_argument( |
| 41 | + "--prompt", |
| 42 | + required=True, |
| 43 | + help="H3 text prompt; use (S1) and <d>[Language] words</d> for explicit dialogue", |
| 44 | + ) |
| 45 | + parser.add_argument("--output-path", type=Path, required=True) |
| 46 | + parser.add_argument("--height", type=int, default=480) |
| 47 | + parser.add_argument("--width", type=int, default=832) |
| 48 | + parser.add_argument("--num-frames", type=int, default=124) |
| 49 | + parser.add_argument("--seed", type=int, default=0) |
| 50 | + parser.add_argument("--steps", type=int, default=4, help="denoise steps (trained ladder = 4)") |
| 51 | + parser.add_argument( |
| 52 | + "--fast", |
| 53 | + action=argparse.BooleanOptionalAction, |
| 54 | + default=False, |
| 55 | + help="denoise fewer video frames, then use MLX RIFE to restore the target frame count; audio stays full length", |
| 56 | + ) |
| 57 | + parser.add_argument("--fast-factor", type=int, default=2, |
| 58 | + help="temporal reduction target for --fast (default: 2)") |
| 59 | + parser.add_argument("--fast-sharpen", type=float, default=0.6, |
| 60 | + help="unsharp strength after RIFE interpolation (0 disables)") |
| 61 | + parser.add_argument("--rife-weights-dir", type=Path, default=None, |
| 62 | + help="optional local mlx-community/RIFE-4.25 snapshot") |
| 63 | + parser.add_argument("--vae-dtype", choices=("fp32", "fp16", "bf16"), default="fp32") |
| 64 | + parser.add_argument("--prompt-cache-dir", type=Path, default=None, |
| 65 | + help="directory for reusable prompt embedding caches") |
| 66 | + parser.add_argument( |
| 67 | + "--tiled-video-decode", |
| 68 | + action=argparse.BooleanOptionalAction, |
| 69 | + default=True, |
| 70 | + help="decode with the reference 256px overlapping VAE tiles (disable only for diagnostics)", |
| 71 | + ) |
| 72 | + return parser.parse_args() |
| 73 | + |
| 74 | + |
| 75 | +def main() -> None: |
| 76 | + args = parse_args() |
| 77 | + from fastvideo.mlx_runtime.minimax_h3_pipeline import MiniMaxH3MLXPipeline |
| 78 | + |
| 79 | + pipeline = MiniMaxH3MLXPipeline( |
| 80 | + model_root=args.model_root, |
| 81 | + mlx_dit_checkpoint=args.mlx_checkpoint, |
| 82 | + vae_dtype=args.vae_dtype, |
| 83 | + prompt_cache_dir=args.prompt_cache_dir, |
| 84 | + ) |
| 85 | + result = pipeline.generate( |
| 86 | + args.prompt, |
| 87 | + output_path=args.output_path, |
| 88 | + height=args.height, |
| 89 | + width=args.width, |
| 90 | + num_frames=args.num_frames, |
| 91 | + seed=args.seed, |
| 92 | + num_steps=args.steps, |
| 93 | + tiled_video_decode=args.tiled_video_decode, |
| 94 | + fast=args.fast, |
| 95 | + fast_factor=args.fast_factor, |
| 96 | + fast_sharpen=args.fast_sharpen, |
| 97 | + rife_weights_dir=args.rife_weights_dir, |
| 98 | + ) |
| 99 | + print(json.dumps({ |
| 100 | + "video_path": result.video_path, |
| 101 | + "timings_s": {k: round(v, 2) for k, v in result.timings.items()}, |
| 102 | + "peak_memory_gib": {k: round(v, 2) for k, v in result.peak_memory_gib.items()}, |
| 103 | + "audio_samples": int(result.waveform.shape[-1]), |
| 104 | + }, indent=2)) |
| 105 | + |
| 106 | + |
| 107 | +if __name__ == "__main__": |
| 108 | + main() |
0 commit comments