multimodalart HF Staff commited on
Commit
3f814cb
·
verified ·
1 Parent(s): f461095

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +11 -6
  2. app.py +359 -0
  3. requirements.txt +16 -0
README.md CHANGED
@@ -1,13 +1,18 @@
1
  ---
2
- title: Plaguekind Minimax H3
3
- emoji: 🐠
4
  colorFrom: gray
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.22.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
  ---
2
+ title: MiniMax-H3
3
+ emoji: 🎬
4
  colorFrom: gray
5
+ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.22.0
 
8
  app_file: app.py
9
+ short_description: MiniMax-H3 video generation with synchronized audio
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
  ---
13
 
14
+ # MiniMax-H3
15
+
16
+ MiniMax-H3 is a 33B parameter state-of-the-art video generation model that produces video and a fully synchronized soundtrack (ambience, foley, speech). This Space runs the FL2VA variant with Int8 weight-only quantization on ZeroGPU.
17
+
18
+ Based on [Plaguekind/Minimax-H3](https://huggingface.co/Plaguekind/Minimax-H3) (ComfyUI workflow wrapper) and [MiniMaxAI/MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) (original model).
app.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MiniMax-H3 video generation with synchronized audio — FL2VA (text / first-last-frame to video+audio).
2
+
3
+ Based on the diffusers MiniMax-H3 pipeline. Uses Int8 weight-only quantization on both the
4
+ 33B transformer and the 32B Qwen3-VL text encoder to fit within ZeroGPU xlarge (96 GB VRAM).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ # Allocator config for memory pressure (video DiTs have large transient allocations)
12
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
13
+
14
+ import spaces # MUST come before torch / any CUDA-touching import
15
+ import gradio as gr
16
+
17
+ import tempfile
18
+ import time
19
+ import traceback
20
+
21
+ MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
22
+ GPU_SIZE = "xlarge"
23
+
24
+ # Canvas presets matching the original model's supported resolutions
25
+ CANVASES = {
26
+ # 16:9
27
+ "960x544 · 16:9 fast": (544, 960),
28
+ "1024x576 · 16:9 fast": (576, 1024),
29
+ "1152x640 · 16:9": (640, 1152),
30
+ "1280x704 · 16:9": (704, 1280),
31
+ "1344x768 · 16:9 full": (768, 1344),
32
+ # 9:16
33
+ "544x960 · 9:16 fast": (960, 544),
34
+ "640x1152 · 9:16": (1152, 640),
35
+ "768x1344 · 9:16 full": (1344, 768),
36
+ # 1:1
37
+ "544x544 · 1:1 fast": (544, 544),
38
+ "768x768 · 1:1 full": (768, 768),
39
+ # 4:3 / 3:4
40
+ "768x576 · 4:3 fast": (576, 768),
41
+ "1024x768 · 4:3 full": (768, 1024),
42
+ "576x768 · 3:4 fast": (768, 576),
43
+ "768x1024 · 3:4 full": (1024, 768),
44
+ # 21:9
45
+ "1152x512 · 21:9 fast": (512, 1152),
46
+ "1536x672 · 21:9 full": (672, 1536),
47
+ }
48
+ DEFAULT_CANVAS = "960x544 · 16:9 fast"
49
+ FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5
50
+ MIN_UI_DURATION, MAX_UI_DURATION = 2, 10
51
+
52
+
53
+ def snap_frames(seconds: float) -> int:
54
+ """The frame count MiniMax-H3's video VAE can decode: the next 17*n+5 at 24 fps."""
55
+ frames = max(1, round(float(seconds) * FPS))
56
+ while frames % FRAMES_PER_CHUNK != LATENTS_PER_CHUNK:
57
+ frames += 1
58
+ return frames
59
+
60
+
61
+ def lower_duration_floor(seconds: float = MIN_UI_DURATION) -> None:
62
+ """Let the pipeline generate below its 5 s floor."""
63
+ from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
64
+
65
+ MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(seconds))
66
+
67
+
68
+ PIPE = None
69
+ LOAD_ERROR: str | None = None
70
+ LOADED_IN: float | None = None
71
+
72
+
73
+ def load_models() -> str | None:
74
+ """Load the full MiniMax-H3 FL2VA pipeline with Int8 quantization at startup.
75
+
76
+ Int8 weight-only quantization roughly halves VRAM for both the 33B transformer and
77
+ the 32B Qwen3-VL text encoder, bringing the total to ~63 GB — fits in xlarge (96 GB).
78
+ """
79
+ global PIPE, LOAD_ERROR, LOADED_IN
80
+
81
+ if PIPE is not None or LOAD_ERROR is not None:
82
+ return LOAD_ERROR
83
+
84
+ started = time.time()
85
+ try:
86
+ import torch
87
+ from diffusers import ModularPipeline, MiniMaxH3Transformer3DModel, TorchAoConfig
88
+ from transformers import Qwen3VLForConditionalGeneration
89
+ from transformers import TorchAoConfig as TransformersTorchAoConfig
90
+ from torchao.quantization import Int8WeightOnlyConfig
91
+
92
+ lower_duration_floor()
93
+
94
+ print(f"[load] loading MiniMax-H3 from {MODEL_REPO} with Int8 quantization ...", flush=True)
95
+
96
+ pipe = ModularPipeline.from_pretrained(MODEL_REPO)
97
+
98
+ # Quantize the 33B transformer with Int8 weight-only quantization
99
+ pipe.update_components(
100
+ transformer=MiniMaxH3Transformer3DModel.from_pretrained(
101
+ MODEL_REPO,
102
+ subfolder="transformer",
103
+ dtype=torch.bfloat16,
104
+ quantization_config=TorchAoConfig(
105
+ Int8WeightOnlyConfig(version=2),
106
+ modules_to_not_convert=[
107
+ "proj_in", "audio_proj_in", "context_embedder", "time_embedder", "time_proj",
108
+ "token_refiner", "norm_out", "proj_out", "audio_proj_out",
109
+ ],
110
+ ),
111
+ low_cpu_mem_usage=False,
112
+ ),
113
+ # Quantize the 32B Qwen3-VL text encoder with Int8 weight-only quantization
114
+ text_encoder=Qwen3VLForConditionalGeneration.from_pretrained(
115
+ MODEL_REPO,
116
+ subfolder="text_encoder",
117
+ dtype=torch.bfloat16,
118
+ quantization_config=TransformersTorchAoConfig(
119
+ Int8WeightOnlyConfig(version=2),
120
+ modules_to_not_convert=[
121
+ "model.visual",
122
+ "model.language_model.embed_tokens",
123
+ "model.language_model.norm",
124
+ "lm_head",
125
+ ],
126
+ ),
127
+ ),
128
+ )
129
+ pipe.load_components(dtype=torch.bfloat16)
130
+
131
+ # VAEs stay full precision — a bfloat16 audio VAE decodes the soundtrack ~20 dB too quiet.
132
+ # VAEs are small (~6 GB) so we can pack them at startup; the quantized transformer (~31 GB) and
133
+ # text encoder (~26 GB) are too large to pack alongside the BF16 download on disk, so they stay
134
+ # as CPU tensors and move to CUDA on the first GPU call.
135
+ pipe.vae.to("cuda")
136
+ pipe.audio_vae.to("cuda")
137
+
138
+ # Use cuDNN fused attention (10-20% faster than SDPA, no extra deps)
139
+ try:
140
+ pipe.transformer.set_attention_backend("_native_cudnn")
141
+ except Exception:
142
+ pipe.transformer.set_attention_backend("sdpa")
143
+
144
+ PIPE = pipe
145
+ LOADED_IN = time.time() - started
146
+ print(f"[load] ready in {LOADED_IN:.0f}s", flush=True)
147
+ except Exception as error:
148
+ traceback.print_exc()
149
+ LOAD_ERROR = f"**Loading `{MODEL_REPO}` failed** after {time.time() - started:.0f}s: `{type(error).__name__}: {error}`"
150
+ return LOAD_ERROR
151
+
152
+
153
+ def status() -> str:
154
+ if LOAD_ERROR:
155
+ return LOAD_ERROR
156
+ if PIPE is None:
157
+ return f"Loading `{MODEL_REPO}` (~119 GB BF16, Int8-quantized at load). Watch the Space logs."
158
+ return f"Ready · Int8 quantized · loaded in {LOADED_IN:.0f}s"
159
+
160
+
161
+ # Duration estimation: linear in rows (matmuls) + quadratic (attention) + decode cost
162
+ _DUR_B, _DUR_C = 1.5e-4, 5.0e-9
163
+ _DECODE_BASE, _DECODE_PER_DEFAULT_CANVAS, _DEFAULT_CANVAS_PIXELS = 15, 15, 960 * 544 * 124
164
+ _PAD = 15
165
+
166
+
167
+ def get_duration(prompt, image, last_image, height, width, num_frames, steps, seed, *a, **k):
168
+ """Estimate GPU seconds needed for this request."""
169
+ height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
170
+ latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
171
+ patches = (height // 32) * (width // 32)
172
+ rows = latent_frames * patches + (int(image is not None) + int(last_image is not None)) * patches
173
+ denoise = steps * (_DUR_B * rows + _DUR_C * rows**2)
174
+ decode = _DECODE_BASE + _DECODE_PER_DEFAULT_CANVAS * (height * width * num_frames) / _DEFAULT_CANVAS_PIXELS
175
+ # Extra allowance for quantized model being somewhat slower per step + cold-start weight transfer
176
+ quant_allowance = 30
177
+ return max(60, int(denoise + decode) + quant_allowance + _PAD)
178
+
179
+
180
+ @spaces.GPU(duration=get_duration, size=GPU_SIZE)
181
+ def generate(prompt, image, last_image, height, width, num_frames, steps, seed, progress=gr.Progress(track_tqdm=True)):
182
+ """Generate video with synchronized audio from text and optional keyframes.
183
+
184
+ Args:
185
+ prompt: Text description of the video to generate.
186
+ image: Optional first frame image (PIL Image).
187
+ last_image: Optional last frame image (PIL Image).
188
+ height: Output video height in pixels.
189
+ width: Output video width in pixels.
190
+ num_frames: Number of frames to generate (must be 17*n+5).
191
+ steps: Number of denoising steps.
192
+ seed: Random seed for reproducibility.
193
+ """
194
+ import torch
195
+ from diffusers.utils import encode_video
196
+
197
+ # Move the quantized transformer and text encoder to CUDA on each cold worker.
198
+ # They were kept as CPU tensors at startup to avoid exceeding the 150 GB disk
199
+ # quota (BF16 download ~119 GB + packed copy would be too large).
200
+ PIPE.transformer.to("cuda")
201
+ PIPE.text_encoder.to("cuda")
202
+
203
+ state = PIPE(
204
+ prompt=prompt,
205
+ image=image,
206
+ last_image=last_image,
207
+ height=int(height),
208
+ width=int(width),
209
+ num_frames=int(num_frames),
210
+ num_inference_steps=int(steps),
211
+ generator=torch.Generator("cpu").manual_seed(int(seed)),
212
+ )
213
+
214
+ videos = state.get("videos")
215
+ audio = state.get("audio")
216
+ sampling_rate = state.get("sampling_rate")
217
+
218
+ directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
219
+ os.makedirs(directory, exist_ok=True)
220
+ path = os.path.join(directory, f"h3-{int(time.time() * 1000)}.mp4")
221
+ encode_video(
222
+ videos[0],
223
+ fps=FPS,
224
+ output_path=path,
225
+ audio=audio[0].cpu(),
226
+ audio_sample_rate=sampling_rate,
227
+ )
228
+ return path
229
+
230
+
231
+ def run_generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS,
232
+ duration=5, steps=28, seed=42, progress=gr.Progress(track_tqdm=True)):
233
+ """Handle a generation request from the Gradio UI."""
234
+ if LOAD_ERROR:
235
+ raise gr.Error(LOAD_ERROR)
236
+ if PIPE is None:
237
+ raise gr.Error("The model is still loading. Please wait a moment and try again.")
238
+ if not prompt or not prompt.strip():
239
+ raise gr.Error("MiniMax-H3 always takes a prompt, keyframes or not.")
240
+
241
+ from PIL import Image, ImageOps
242
+
243
+ num_frames = snap_frames(duration)
244
+ height, width = CANVASES[canvas]
245
+
246
+ def keyframe(path):
247
+ return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
248
+
249
+ first_frame = keyframe(image_path) if image_path else None
250
+ final_frame = keyframe(last_image_path) if last_image_path else None
251
+
252
+ progress(0.05, desc=f"Denoising {steps} steps at {width}x{height}, {num_frames} frames ...")
253
+ path = generate(prompt, first_frame, final_frame, height, width, num_frames, steps, seed, progress)
254
+ return path
255
+
256
+
257
+ def _fit_keyframe(image_path, current_canvas):
258
+ """Cover-crop an uploaded keyframe to the closest supported aspect ratio."""
259
+ if not image_path:
260
+ return gr.update(), gr.update()
261
+ from PIL import Image as _Image
262
+
263
+ img = _Image.open(image_path)
264
+ aspect = img.width / img.height
265
+ fastest = {}
266
+ for label, (h, w) in CANVASES.items():
267
+ r = w / h
268
+ if r not in fastest or w * h < fastest[r][1][0] * fastest[r][1][1]:
269
+ fastest[r] = (label, (h, w))
270
+ ratio = min(fastest, key=lambda r: abs(r - aspect))
271
+ label, (h, w) = fastest[ratio]
272
+
273
+ cur_h, cur_w = CANVASES[current_canvas]
274
+ if abs(cur_w / cur_h - aspect) <= abs(ratio - aspect):
275
+ label = current_canvas
276
+ h, w = cur_h, cur_w
277
+
278
+ target = w / h
279
+ if abs(img.width / img.height - target) <= 1e-3:
280
+ return gr.update(), gr.update(value=label)
281
+ if img.width / img.height > target:
282
+ new_w = int(img.height * target)
283
+ left = (img.width - new_w) // 2
284
+ img = img.crop((left, 0, left + new_w, img.height))
285
+ else:
286
+ new_h = int(img.width / target)
287
+ top = (img.height - new_h) // 2
288
+ img = img.crop((0, top, img.width, top + new_h))
289
+ img.save(image_path)
290
+ return gr.update(value=image_path), gr.update(value=label)
291
+
292
+
293
+ load_models()
294
+
295
+ INTRO = """# MiniMax-H3
296
+
297
+ <div align="center">
298
+ <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener"><strong>[ model ]</strong></a> &nbsp;
299
+ <a href="https://www.minimax.io/blog/minimax-h3" target="_blank" rel="noopener"><strong>[ blog ]</strong></a> &nbsp;
300
+ <a href="https://huggingface.co/Plaguekind/Minimax-H3" target="_blank" rel="noopener"><strong>[ ComfyUI weights ]</strong></a>
301
+ </div>
302
+
303
+ **MiniMax-H3** is a 33B parameter state-of-the-art video generation model that produces video and a
304
+ fully synchronized soundtrack (ambience, foley, speech). Supports text-to-video and first/last-frame-to-video.
305
+ """
306
+
307
+ CSS = """
308
+ .main.fillable {max-width: 1250px !important}
309
+ .dark .gradio-container { color: var(--body-text-color); }
310
+ """
311
+
312
+ with gr.Blocks(title="MiniMax-H3") as demo:
313
+ gr.Markdown(INTRO)
314
+
315
+ with gr.Row():
316
+ with gr.Column():
317
+ prompt = gr.Textbox(
318
+ label="Prompt",
319
+ lines=3,
320
+ value="A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot",
321
+ )
322
+ with gr.Row():
323
+ image = gr.Image(label="First frame (optional)", type="filepath")
324
+ last_image = gr.Image(label="Last frame (optional)", type="filepath")
325
+ run = gr.Button("Generate", variant="primary")
326
+ with gr.Accordion("Advanced options", open=False):
327
+ canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
328
+ duration = gr.Slider(label="Duration (s)", minimum=MIN_UI_DURATION, maximum=MAX_UI_DURATION, step=1, value=5)
329
+ steps = gr.Slider(label="Steps", minimum=10, maximum=40, step=1, value=28)
330
+ seed = gr.Number(label="Seed", value=42, precision=0)
331
+
332
+ with gr.Column():
333
+ video = gr.Video(label="Video + soundtrack")
334
+
335
+ image.upload(_fit_keyframe, [image, canvas], [image, canvas])
336
+
337
+ gr.Examples(
338
+ examples=[
339
+ ["A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot", None, None, "960x544 · 16:9 fast"],
340
+ ["A busy night market, neon signs reflecting in puddles, sizzling street food", None, None, "544x960 · 9:16 fast"],
341
+ ["A cellist playing a slow melody in an empty concert hall", None, None, "544x544 · 1:1 fast"],
342
+ ],
343
+ inputs=[prompt, image, last_image, canvas],
344
+ outputs=[video],
345
+ fn=run_generate,
346
+ cache_examples=True,
347
+ cache_mode="lazy",
348
+ )
349
+
350
+ run.click(
351
+ run_generate,
352
+ [prompt, image, last_image, canvas, duration, steps, seed],
353
+ [video],
354
+ api_name="generate",
355
+ )
356
+
357
+
358
+ if __name__ == "__main__":
359
+ demo.queue().launch(show_error=True, theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # diffusers is installed from the canonical MiniMax-H3 PR
2
+ # https://github.com/huggingface/diffusers/pull/14371 ("Minimax h3 follow up (review & refactor)")
3
+ --extra-index-url https://download.pytorch.org/whl/cu130
4
+ diffusers @ git+https://github.com/huggingface/diffusers.git@665f578278365ea4a3318cb8c9b66ce6c01204b9
5
+ torch==2.11.0
6
+ torchvision==0.26.0
7
+ # The Qwen3-VL processor decides the vision patch count, so a different minor changes the conditioning.
8
+ transformers==5.8.0
9
+ accelerate==1.14.0
10
+ # Int8 weight-only quantization for the 33B transformer and 32B text encoder
11
+ torchao==0.18.0
12
+ # PyAV muxes the generated soundtrack onto the frames (encode_video)
13
+ av
14
+ pillow
15
+ numpy
16
+ safetensors>=0.8.0