larryvrh commited on
Commit
bdb6eee
·
verified ·
1 Parent(s): f4986f1

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Comfy-Org/MiniMax-H3
4
+ tags:
5
+ - text-to-video
6
+ - text-to-audio
7
+ - audio-video
8
+ - lora
9
+ - minimax-h3
10
+ pipeline_tag: text-to-video
11
+ ---
12
+
13
+ # MiniMax-H3 Turbo LoRA — 4-step audio-video generation (early preview)
14
+
15
+ A LoRA for [MiniMax-H3](https://huggingface.co/Comfy-Org/MiniMax-H3) that renders
16
+ joint **video + synchronized stereo audio** in **4 sampling steps** instead of
17
+ the usual ~20 — roughly a 5× speedup in sampling wall-clock.
18
+
19
+ > ⚠️ **This is a demo / preview checkpoint, not a finished model.** It is an
20
+ > early snapshot from an in-progress run — under-trained, the time-averaged
21
+ > (EMA) variant hasn't matured, and overall quality is **nowhere near what a
22
+ > full run will produce**. What it *does* already show is a clear improvement
23
+ > over the base model at 4 steps: sharper detail and cleaner, better-synced
24
+ > audio than the base gives you in the same 4 steps. Treat it as a taste of the
25
+ > direction, not the destination.
26
+
27
+ Two weight files are included:
28
+
29
+ | file | notes |
30
+ |---|---|
31
+ | `minimax_h3_turbo_4step.safetensors` | trained weights — crisper, holds up better on fast motion |
32
+ | `minimax_h3_turbo_4step_ema.safetensors` | time-averaged weights — smoother, but **immature at this checkpoint** (EMA hasn't warmed up), so it can look soft |
33
+
34
+ Both are bf16, ~744 MB, apply as a standard low-rank update
35
+ (`W_eff = W + lora_B @ lora_A`, alpha = rank so no extra scaling).
36
+
37
+ ## Quick start
38
+
39
+ The base model, VAEs and text encoder come from the official MiniMax-H3 release,
40
+ and the model definitions live in ComfyUI, so you need a ComfyUI checkout:
41
+
42
+ ```bash
43
+ # 1. ComfyUI (pinned to the commit these weights were validated against)
44
+ git clone https://github.com/comfyanonymous/ComfyUI
45
+ cd ComfyUI && git checkout 14b05228cef127ce529bc0c08660770d4af3e9a8
46
+ pip install -r requirements.txt && cd ..
47
+
48
+ # 2. this release
49
+ pip install -r requirements.txt # torch, safetensors, imageio-ffmpeg, ...
50
+
51
+ # 3. base weights (from Comfy-Org/MiniMax-H3): the bf16 DiT, the int8 Qwen3-VL
52
+ # text encoder, and the video + audio VAEs, into a models/ tree.
53
+
54
+ # 4. generate
55
+ python generate.py \
56
+ --comfyui ./ComfyUI \
57
+ --base models/diffusion_models/minimax_h3_fl2va_bf16.safetensors \
58
+ --lora minimax_h3_turbo_4step.safetensors \
59
+ --te models/text_encoders/qwen3vl_32b_minimax_h3_int8_convrot.safetensors \
60
+ --video-vae models/vae/minimax_h3_video_vae_fp16.safetensors \
61
+ --audio-vae models/vae/minimax_h3_audio_vae_fp32.safetensors \
62
+ --prompt "A corgi in a chef hat flipping a pancake, sizzling sounds and a cheerful bark." \
63
+ --width 1344 --height 768 --frames 124 --out corgi.mp4
64
+ ```
65
+
66
+ `generate.py` is a single self-contained file: it loads the base DiT and merges
67
+ the LoRA, encodes the prompt with Qwen3-VL, runs 4-step sampling on the model's
68
+ native dual video/audio schedule, decodes both streams and muxes an mp4.
69
+
70
+ ## Notes
71
+
72
+ - **Resolution / duration**: width and height are multiples of 16 (the canvas is
73
+ 32-based); the short edge is typically 768. Frame count is at 24 fps and snaps
74
+ up to the model's 17·k+5 grid (124 ≈ 5 s). The validated range is ~124–362
75
+ frames (~5–15 s).
76
+ - **VRAM**: the base model is large (~33 B). `--offload-adaln` keeps the biggest
77
+ timestep-conditioned weights in CPU fp32, saving ~13 GB; an 80 GB GPU runs
78
+ comfortably, smaller cards can try with offload on.
79
+ - **Steps**: 4 is the design point. `--steps 8` is a little cleaner if you have
80
+ the budget; the schedule is the same either way.
81
+ - **Audio**: the model emits 32 kHz stereo aligned to the video; the two streams
82
+ ride different flow schedules and `generate.py` integrates each on its own.
generate.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MiniMax-H3 Turbo LoRA — 4-step text-to-audio-video generation.
2
+
3
+ A lightweight LoRA that lets MiniMax-H3 render joint video + stereo audio in
4
+ **4 sampling steps** instead of the usual ~20, at a fraction of the wall-clock
5
+ cost. This single file is a self-contained generator: it loads the base H3 DiT
6
+ plus this LoRA, encodes the prompt with the Qwen3-VL text encoder, runs the
7
+ model's native dual-schedule sampler for 4 steps, decodes both streams and muxes
8
+ a playable mp4.
9
+
10
+ The audio stream runs on its own shifted flow schedule (video shift 12, audio
11
+ shift 3); each stream is integrated on its own clock, which is the schedule
12
+ semantics MiniMax-H3 was designed around. That is the only non-obvious part of
13
+ sampling — everything else is a plain Euler flow sampler.
14
+
15
+ Dependencies (see requirements.txt), plus a ComfyUI checkout for the H3 model /
16
+ VAE / text-encoder module definitions:
17
+
18
+ git clone https://github.com/comfyanonymous/ComfyUI
19
+ cd ComfyUI && git checkout 14b05228cef127ce529bc0c08660770d4af3e9a8
20
+
21
+ Base weights come from the official MiniMax-H3 release
22
+ (Comfy-Org/MiniMax-H3 on the Hugging Face Hub): the bf16 DiT, the int8 Qwen3-VL
23
+ text encoder, and the video + audio VAEs.
24
+
25
+ Usage:
26
+ python generate.py \
27
+ --comfyui /path/to/ComfyUI \
28
+ --base models/diffusion_models/minimax_h3_fl2va_bf16.safetensors \
29
+ --lora minimax_h3_turbo_4step.safetensors \
30
+ --te models/text_encoders/qwen3vl_32b_minimax_h3_int8_convrot.safetensors \
31
+ --video-vae models/vae/minimax_h3_video_vae_fp16.safetensors \
32
+ --audio-vae models/vae/minimax_h3_audio_vae_fp32.safetensors \
33
+ --prompt "A corgi in a tiny chef hat flipping a pancake, sizzling sounds." \
34
+ --width 1344 --height 768 --frames 124 --out corgi.mp4
35
+
36
+ `minimax_h3_turbo_4step.safetensors` is the trained LoRA; the accompanying
37
+ `minimax_h3_turbo_4step_ema.safetensors` is a time-averaged variant — try both,
38
+ the trained one tends to be crisper on fast motion, the averaged one smoother.
39
+ """
40
+
41
+ import argparse
42
+ import math
43
+ import os
44
+ import subprocess
45
+ import sys
46
+ import time
47
+ import wave
48
+
49
+ import torch
50
+ import torch.nn.functional as F
51
+
52
+
53
+ def log(msg):
54
+ print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
55
+
56
+
57
+ # ======================================================================
58
+ # Flow schedule (video shift 12 / audio shift 3, closed-form dual mapping)
59
+ # ======================================================================
60
+ SHIFT_VIDEO = 12.0
61
+ SHIFT_AUDIO = 3.0
62
+
63
+
64
+ def shift_sigma(u, shift):
65
+ return shift * u / (1.0 + (shift - 1.0) * u)
66
+
67
+
68
+ def time_shift_sigma(sigma, from_shift, to_shift):
69
+ base = sigma / (from_shift + sigma * (1.0 - from_shift))
70
+ return to_shift * base / (1.0 + (to_shift - 1.0) * base)
71
+
72
+
73
+ def time_shift_slope(sigma, from_shift, to_shift):
74
+ base = sigma / (from_shift + sigma * (1.0 - from_shift))
75
+ return (to_shift * (1.0 + (from_shift - 1.0) * base) ** 2) / (
76
+ from_shift * (1.0 + (to_shift - 1.0) * base) ** 2)
77
+
78
+
79
+ def timesteps(n, shift=SHIFT_VIDEO):
80
+ """n-step video sigma grid: ts[0]=1 (pure noise) > ... > ts[n]=0."""
81
+ return [shift_sigma(1.0 - i / n, shift) for i in range(n + 1)]
82
+
83
+
84
+ def audio_sigma(sigma_v):
85
+ return time_shift_sigma(sigma_v, SHIFT_VIDEO, SHIFT_AUDIO)
86
+
87
+
88
+ def audio_slope(sigma_v):
89
+ return time_shift_slope(sigma_v, SHIFT_VIDEO, SHIFT_AUDIO)
90
+
91
+
92
+ @torch.no_grad()
93
+ def sample(vfn, xv, xa, ts):
94
+ """4-step Euler on the joint flow. The model returns the audio velocity
95
+ already scaled by d(sigma_a)/d(sigma_v), so video steps on its own sigma
96
+ delta while audio steps on its own schedule's delta (recovering the raw
97
+ audio velocity by dividing out the slope). This dual-clock stepping is the
98
+ schedule MiniMax-H3 expects; a single flat step on the video clock would
99
+ over/under-shoot the audio stream badly at 4 steps.
100
+ """
101
+ for i in range(len(ts) - 1):
102
+ ov, oa = vfn(xv, xa, ts[i])
103
+ hv = ts[i + 1] - ts[i]
104
+ sl = audio_slope(max(ts[i], 1e-6))
105
+ ha = audio_sigma(ts[i + 1]) - audio_sigma(ts[i])
106
+ xv = xv + hv * ov
107
+ xa = xa + ha * (oa / sl)
108
+ return xv, xa
109
+
110
+
111
+ # ======================================================================
112
+ # Functional forward (out-of-place, mirrors the reference module math)
113
+ # ======================================================================
114
+ def _rms(x, weight, eps):
115
+ return F.rms_norm(x, (x.shape[-1],), weight, eps)
116
+
117
+
118
+ def _attn(attn, x, rope_cos, rope_sin):
119
+ s = x.shape[0]
120
+ heads, hd = attn.heads, attn.head_dim
121
+ q, k, v = attn.qkv_proj(x).split(heads * hd, dim=-1)
122
+ q = _rms(q.view(s, heads, hd), attn.q_norm.weight, attn.q_norm.eps)
123
+ k = _rms(k.view(s, heads, hd), attn.k_norm.weight, attn.k_norm.eps)
124
+ v = v.view(s, heads, hd)
125
+ if rope_cos is not None:
126
+ c, si = rope_cos[:, None, :], rope_sin[:, None, :]
127
+
128
+ def rot(t):
129
+ t96 = t[..., :96].float()
130
+ x1, x2 = t96[..., :48], t96[..., 48:]
131
+ return torch.cat([(x1 * c - x2 * si).to(t.dtype),
132
+ (x1 * si + x2 * c).to(t.dtype),
133
+ t[..., 96:]], dim=-1)
134
+
135
+ q, k = rot(q), rot(k)
136
+ q, k, v = (t.transpose(0, 1).unsqueeze(0) for t in (q, k, v))
137
+ out = F.scaled_dot_product_attention(q, k, v)
138
+ return attn.out_proj(out.squeeze(0).transpose(0, 1).reshape(s, heads * hd))
139
+
140
+
141
+ def _mlp(mlp, x):
142
+ x1, x2 = mlp.fc1(x).chunk(2, dim=-1)
143
+ return mlp.fc2(F.silu(x1) * x2)
144
+
145
+
146
+ def _refiner(refiner, x):
147
+ for blk in refiner.blocks:
148
+ x = x + _attn(blk.attn, _rms(x, blk.norm1.weight, blk.norm1.eps),
149
+ None, None)
150
+ x = x + _mlp(blk.mlp, _rms(x, blk.norm2.weight, blk.norm2.eps))
151
+ return _rms(x, refiner.final_norm.weight, refiner.final_norm.eps)
152
+
153
+
154
+ def _apply_mod(h, shift, scale, segments):
155
+ parts = []
156
+ for a, b, row in segments:
157
+ parts.append(h[a:b] * (1.0 + scale[row].to(h.dtype)) + shift[row].to(h.dtype))
158
+ return torch.cat(parts)
159
+
160
+
161
+ def _apply_gate(x, gate, other, segments):
162
+ parts = []
163
+ for a, b, row in segments:
164
+ parts.append(x[a:b] + other[a:b] * gate[row].to(x.dtype))
165
+ return torch.cat(parts)
166
+
167
+
168
+ def _block(blk, h, mods, segments, rope_cos, rope_sin):
169
+ sh_msa, sc_msa, g_msa, sh_mlp, sc_mlp, g_mlp = mods.unbind(dim=1)
170
+ hn = _apply_mod(_rms(h, blk.norm1.weight, blk.norm1.eps), sh_msa, sc_msa, segments)
171
+ h = _apply_gate(h, g_msa, _attn(blk.attn, hn, rope_cos, rope_sin), segments)
172
+ hn = _apply_mod(_rms(h, blk.norm2.weight, blk.norm2.eps), sh_mlp, sc_mlp, segments)
173
+ return _apply_gate(h, g_mlp, _mlp(blk.mlp, hn), segments)
174
+
175
+
176
+ # ======================================================================
177
+ # Model load + LoRA merge
178
+ # ======================================================================
179
+ def load_model(comfyui, base_path, lora_path, device, offload_adaln):
180
+ import comfy.ldm.minimax.model as h3ref
181
+ import comfy.ops
182
+ import comfy.utils
183
+ from safetensors.torch import load_file
184
+
185
+ log(f"loading base DiT: {base_path}")
186
+ sd = comfy.utils.load_torch_file(base_path)
187
+ model = h3ref.MiniMaxH3Model(dtype=torch.bfloat16, device="cpu",
188
+ operations=comfy.ops.disable_weight_init)
189
+ missing, unexpected = model.load_state_dict(sd, strict=True, assign=True)
190
+ assert not missing and not unexpected, (missing[:3], unexpected[:3])
191
+ model.requires_grad_(False)
192
+ model.eval()
193
+
194
+ for i, blk in enumerate(model.blocks):
195
+ blk.to(device)
196
+ for mod in (model.token_refiner, model.final_layer, model.condition_proj,
197
+ model.video_patch_proj, model.audio_patch_proj,
198
+ model.time_embedder, model.rope):
199
+ mod.to(device)
200
+
201
+ log(f"merging LoRA: {lora_path}")
202
+ lora = load_file(lora_path)
203
+ mods = sorted({k.rsplit(".lora_", 1)[0] for k in lora})
204
+ for name in mods:
205
+ lin = model.get_submodule(name)
206
+ a = lora[name + ".lora_A.weight"].to(device, torch.float32)
207
+ b = lora[name + ".lora_B.weight"].to(device, torch.float32)
208
+ # every module is injected with alpha == rank, so scale is 1.0
209
+ delta = (b @ a).to(lin.weight.dtype)
210
+ lin.weight.data = lin.weight.data.to(device) + delta
211
+ log(f"merged {len(mods)} modules")
212
+
213
+ if offload_adaln:
214
+ # The per-layer adaLN projection is huge (2688 -> 96768) but depends only
215
+ # on the timestep, of which there are a handful per denoise. Keep it in
216
+ # CPU fp32 to save ~13 GB of VRAM; the matmul is cheap at 4 steps.
217
+ for blk in model.blocks:
218
+ lin = blk.adaln_proj.linear
219
+ lin.weight.data = lin.weight.data.float().cpu()
220
+ lin.bias.data = lin.bias.data.float().cpu()
221
+ return model, h3ref
222
+
223
+
224
+ VISUAL_COND_T = 0.999
225
+
226
+
227
+ def timestep_rows(model, sigma_v):
228
+ sigma_v = float(max(sigma_v, 1e-6))
229
+ t_v = 1.0 - sigma_v
230
+ t_a = 1.0 - time_shift_sigma(sigma_v, model.sigma_shift_video,
231
+ model.sigma_shift_audio)
232
+ seg_t = {"text": t_v, "video": t_v, "audio": t_a}
233
+ unique_t = sorted({t_v, t_a})
234
+ return seg_t, unique_t, {t: i for i, t in enumerate(unique_t)}
235
+
236
+
237
+ def adaln_mods(model, unique_t, device, offload, cache):
238
+ key = tuple(round(t, 9) for t in unique_t)
239
+ if key in cache:
240
+ return cache[key]
241
+ ts = torch.tensor(unique_t, dtype=torch.float32, device=device)
242
+ with torch.no_grad():
243
+ temb = model.time_embedder(ts).float() # [M, 2688] GPU
244
+ si = F.silu(temb)
245
+ si = si.cpu() if offload else si.to(torch.bfloat16)
246
+ outs = torch.stack([F.linear(si, b.adaln_proj.linear.weight,
247
+ b.adaln_proj.linear.bias)
248
+ for b in model.blocks]) # [50, M, 96768]
249
+ M, H = len(unique_t), model.hidden_size
250
+ mods = outs.view(len(model.blocks), M, 3, 6, H).reshape(
251
+ len(model.blocks), M * 3, 6, H).to(device, torch.bfloat16)
252
+ temb_bf = temb.to(torch.bfloat16)
253
+ cache[key] = (mods, temb_bf)
254
+ return mods, temb_bf
255
+
256
+
257
+ class Prepared:
258
+ """Static packed-sequence structure for one (text_len, shape) signature."""
259
+
260
+ def __init__(self, model, h3ref, text_len, video_shape, audio_t, tags,
261
+ device):
262
+ _, _, lt, lh, lw = video_shape
263
+ self.video_shape = tuple(video_shape)
264
+ self.lat_pad = ((lh + 1) // 2 * 2, (lw + 1) // 2 * 2)
265
+ self.layout = h3ref.PackedLayout(text_len, lt, *self.lat_pad, audio_t)
266
+ pos = self.layout.position_ids.to(torch.float32).to(device)
267
+ inv = model.rope.inv_freq.to(device)
268
+ ang = (pos.unsqueeze(-1) * inv.view(1, 1, -1)).flatten(1)
269
+ self.rope_cos, self.rope_sin = torch.cos(ang), torch.sin(ang)
270
+
271
+ segs = []
272
+ for a, b, kind in self.layout.segments:
273
+ if kind == "text" and tags is not None:
274
+ tg = tags.view(-1).tolist()
275
+ run = 0
276
+ for i in range(1, b - a + 1):
277
+ if i == b - a or tg[i] != tg[run]:
278
+ segs.append((a + run, a + i, int(tg[run]), kind))
279
+ run = i
280
+ else:
281
+ tag = {"text": 1, "video": 0, "audio": 2}[kind]
282
+ segs.append((a, b, tag, kind))
283
+ self.seg_template = segs
284
+ (self.video_seg,) = [(a, b) for a, b, k in self.layout.segments if k == "video"]
285
+ (self.audio_seg,) = [(a, b) for a, b, k in self.layout.segments if k == "audio"]
286
+
287
+
288
+ @torch.no_grad()
289
+ def forward(model, h3ref, prep, video_x, audio_x, sigma_v, context, device,
290
+ offload, cache):
291
+ """One denoise evaluation in the sigma_v domain. Returns
292
+ (video_velocity, audio_velocity * slope), matching what the sampler wants."""
293
+ import comfy.ldm.common_dit
294
+ video_x = comfy.ldm.common_dit.pad_to_patch_size(video_x, model.patch_size)
295
+ orig_t, orig_h, orig_w = prep.video_shape[2:]
296
+
297
+ sigma_v = float(max(sigma_v, 1e-6))
298
+ seg_t, unique_t, t_row = timestep_rows(model, sigma_v)
299
+ segments = [(a, b, t_row[seg_t[k]] * 3 + tag)
300
+ for a, b, tag, k in prep.seg_template]
301
+
302
+ base_mods, t_emb = adaln_mods(model, unique_t, device, offload, cache)
303
+ silu_temb = F.silu(t_emb)
304
+
305
+ video_rows = h3ref.patchify_video(video_x.to(torch.float32), model.patch_size)
306
+ audio_rows = h3ref.pack_audio(audio_x.to(torch.float32))
307
+ video_embed = model.video_patch_proj(video_rows).to(torch.bfloat16)
308
+ audio_embed = model.audio_patch_proj(audio_rows).to(torch.bfloat16)
309
+
310
+ with torch.autocast("cuda", dtype=torch.bfloat16):
311
+ text_states = context[0]
312
+ if text_states.shape[-1] != model.hidden_size:
313
+ text_states = _refiner(model.token_refiner,
314
+ model.condition_proj(text_states))
315
+ pieces = []
316
+ for a, b, kind in prep.layout.segments:
317
+ if kind == "text":
318
+ pieces.append(text_states)
319
+ elif kind == "video":
320
+ pieces.append(video_embed)
321
+ else:
322
+ pieces.append(audio_embed)
323
+ h = torch.cat(pieces)
324
+ for i, blk in enumerate(model.blocks):
325
+ h = _block(blk, h, base_mods[i], segments,
326
+ prep.rope_cos, prep.rope_sin)
327
+
328
+ fl = model.final_layer
329
+ with torch.autocast("cuda", dtype=torch.bfloat16):
330
+ f_mod = fl.adaln_proj.linear(F.silu(t_emb))
331
+ f_shift, f_scale = f_mod.view(len(unique_t), 2, model.hidden_size).unbind(1)
332
+ (va, vb), (aa, ab) = prep.video_seg, prep.audio_seg
333
+ vrow, arow = t_row[seg_t["video"]], t_row[seg_t["audio"]]
334
+ hn = _rms(h, fl.norm.weight, fl.norm.eps)
335
+ hv = (hn[va:vb] * (1.0 + f_scale[vrow]) + f_shift[vrow]).to(torch.float32)
336
+ ha = (hn[aa:ab] * (1.0 + f_scale[arow]) + f_shift[arow]).to(torch.float32)
337
+ v_rows, a_rows = fl.video_out(hv), fl.audio_out(ha)
338
+
339
+ lt = video_x.shape[2]
340
+ video_out = h3ref.unpatchify_video(v_rows, lt, prep.lat_pad[0] // 2,
341
+ prep.lat_pad[1] // 2, model.latents_dim,
342
+ model.patch_size)[:, :, :orig_t, :orig_h, :orig_w]
343
+ audio_out = h3ref.unpack_audio(a_rows)
344
+ slope_a = time_shift_slope(sigma_v, model.sigma_shift_video,
345
+ model.sigma_shift_audio)
346
+ return -video_out.to(video_x.dtype), (-slope_a) * audio_out.to(audio_x.dtype)
347
+
348
+
349
+ # ======================================================================
350
+ # Text encode / decode / mux
351
+ # ======================================================================
352
+ def encode_prompt(comfyui, te_path, prompt, device):
353
+ import comfy.model_management
354
+ import comfy.sd
355
+ log(f"loading text encoder: {te_path}")
356
+ clip = comfy.sd.load_clip([te_path], clip_type=comfy.sd.CLIPType.MINIMAX)
357
+ cond = clip.encode_from_tokens_scheduled(clip.tokenize(prompt))
358
+ ca, ex = cond[0][0], cond[0][1]
359
+ tags = ex.get("minimax_token_tags")
360
+ ctx = ca.to(device, torch.bfloat16)
361
+ tags = tags.to(device) if torch.is_tensor(tags) else tags
362
+ del clip
363
+ comfy.model_management.unload_all_models()
364
+ comfy.model_management.soft_empty_cache()
365
+ return ctx, tags
366
+
367
+
368
+ def _write_wav(path, waveform, sr):
369
+ w = waveform.detach().cpu().float()
370
+ if w.ndim == 3:
371
+ w = w[0]
372
+ w = w.clamp(-1.0, 1.0)
373
+ ch = w.shape[0]
374
+ pcm = (w.transpose(0, 1).contiguous().numpy() * 32767.0).astype("<i2")
375
+ with wave.open(path, "wb") as f:
376
+ f.setnchannels(ch)
377
+ f.setsampwidth(2)
378
+ f.setframerate(int(sr))
379
+ f.writeframes(pcm.tobytes())
380
+
381
+
382
+ def save_mp4(images, waveform, sr, fps, out_path):
383
+ import imageio.v2 as imageio
384
+ import imageio_ffmpeg
385
+ frames = images.detach().cpu().float().clamp(0, 1).mul(255).round().to(
386
+ torch.uint8).numpy()
387
+ tv, ta = out_path + ".v.mp4", out_path + ".a.wav"
388
+ writer = imageio.get_writer(tv, fps=fps, codec="libx264", quality=8,
389
+ pixelformat="yuv420p", macro_block_size=1,
390
+ ffmpeg_log_level="error")
391
+ for fr in frames:
392
+ writer.append_data(fr)
393
+ writer.close()
394
+ _write_wav(ta, waveform, sr)
395
+ ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
396
+ subprocess.run([ffmpeg, "-y", "-loglevel", "error", "-i", tv, "-i", ta,
397
+ "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-shortest",
398
+ out_path], check=True)
399
+ os.remove(tv)
400
+ os.remove(ta)
401
+
402
+
403
+ # ======================================================================
404
+ # Main
405
+ # ======================================================================
406
+ def main():
407
+ ap = argparse.ArgumentParser(description="MiniMax-H3 Turbo LoRA 4-step generator")
408
+ ap.add_argument("--comfyui", required=True, help="path to a ComfyUI checkout @14b05228")
409
+ ap.add_argument("--base", required=True, help="H3 bf16 DiT safetensors")
410
+ ap.add_argument("--lora", required=True, help="turbo LoRA safetensors")
411
+ ap.add_argument("--te", required=True, help="Qwen3-VL text encoder safetensors")
412
+ ap.add_argument("--video-vae", required=True)
413
+ ap.add_argument("--audio-vae", required=True)
414
+ ap.add_argument("--prompt", required=True)
415
+ ap.add_argument("--out", default="out.mp4")
416
+ ap.add_argument("--width", type=int, default=1344, help="multiple of 16 (canvas is 32-based)")
417
+ ap.add_argument("--height", type=int, default=768)
418
+ ap.add_argument("--frames", type=int, default=124, help="24 fps; snaps to the 17k+5 grid")
419
+ ap.add_argument("--steps", type=int, default=4)
420
+ ap.add_argument("--seed", type=int, default=42)
421
+ ap.add_argument("--offload-adaln", action="store_true",
422
+ help="keep the timestep-projection weights in CPU fp32 (saves ~13GB VRAM)")
423
+ args = ap.parse_args()
424
+ sys.path.insert(0, args.comfyui) # ComfyUI supplies the H3 module definitions
425
+
426
+ dev = "cuda"
427
+ frames = args.frames
428
+ while frames % 17 != 5:
429
+ frames += 1
430
+ lt = (frames - 5) // 17 * 5 + 2
431
+ lh, lw = args.height // 16, args.width // 16
432
+ audio_t = round(frames / 24 * 40)
433
+ v_shape, a_shape = (1, 24, lt, lh, lw), (1, 32, 2, audio_t)
434
+ ts = timesteps(args.steps)
435
+ log(f"{args.width}x{args.height}x{frames}f ({frames/24:.1f}s) -> "
436
+ f"video{v_shape} audio{a_shape}; {args.steps}-step grid "
437
+ f"{['%.3f' % t for t in ts]}")
438
+
439
+ ctx, tags = encode_prompt(args.comfyui, args.te, args.prompt, dev)
440
+ model, h3ref = load_model(args.comfyui, args.base, args.lora, dev,
441
+ args.offload_adaln)
442
+ prep = Prepared(model, h3ref, ctx.shape[1], v_shape, audio_t, tags, dev)
443
+
444
+ g = torch.Generator(dev).manual_seed(args.seed)
445
+ ga = torch.Generator(dev).manual_seed(args.seed + 1)
446
+ nv = torch.randn(v_shape, generator=g, device=dev, dtype=torch.bfloat16)
447
+ na = torch.randn(a_shape, generator=ga, device=dev, dtype=torch.bfloat16)
448
+
449
+ cache = {}
450
+
451
+ def vfn(xv, xa, sv):
452
+ return forward(model, h3ref, prep, xv, xa, sv, ctx, dev,
453
+ args.offload_adaln, cache)
454
+
455
+ log("sampling ...")
456
+ t0 = time.time()
457
+ with torch.inference_mode():
458
+ zv, za = sample(vfn, nv, na, ts)
459
+ log(f"sampled in {time.time()-t0:.1f}s")
460
+
461
+ import comfy.sd
462
+ import comfy.utils
463
+ video_vae = comfy.sd.VAE(sd=comfy.utils.load_torch_file(args.video_vae))
464
+ audio_vae = comfy.sd.VAE(sd=comfy.utils.load_torch_file(args.audio_vae))
465
+ with torch.inference_mode():
466
+ images = video_vae.decode(zv.float())
467
+ if images.ndim == 5:
468
+ images = images.reshape(-1, *images.shape[-3:])
469
+ waveform = audio_vae.decode(za.float()).movedim(-1, 1)
470
+ std = torch.std(waveform, dim=[1, 2], keepdim=True) * 5.0
471
+ std[std < 1.0] = 1.0
472
+ waveform = waveform / std
473
+ sr = getattr(audio_vae, "audio_sample_rate_output",
474
+ getattr(audio_vae, "audio_sample_rate", 44100))
475
+ save_mp4(images, waveform, sr, 24, args.out)
476
+ log(f"done -> {args.out} ({os.path.getsize(args.out)/2**20:.1f}MB)")
477
+
478
+
479
+ if __name__ == "__main__":
480
+ main()
minimax_h3_turbo_4step.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c468c61ebf715699b5a710fed016654e4a244195ce2e1c4f9f84dc63f82da905
3
+ size 779849872
minimax_h3_turbo_4step_ema.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8d645b67e606874e9179b277cea721c1f1e75830532fcc2206e23353cb33edc5
3
+ size 779849872
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # MiniMax-H3 Turbo LoRA 4-step generator.
2
+ # The base model / VAE / text-encoder module definitions come from a ComfyUI
3
+ # checkout (see generate.py header): git clone + checkout 14b05228, then install
4
+ # ComfyUI's own requirements. The packages below are what generate.py itself
5
+ # needs on top of that.
6
+ torch>=2.4
7
+ safetensors>=0.4
8
+ imageio>=2.34
9
+ imageio-ffmpeg>=0.5
10
+ numpy>=1.24
Free AI Image Generator No sign-up. Instant results. Open Now