Spaces:
Paused
Paused
File size: 9,964 Bytes
e43f223 50fce84 6ebecec e43f223 bd1aa2e e43f223 5363bcd 2a15c46 e43f223 2a15c46 e43f223 bd1aa2e e43f223 2a15c46 e43f223 2a15c46 e43f223 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | """MiniMax-H3 Turbo: video AND its soundtrack, from one .cmf file.
Two things make this Space unusual, and one of them is a warning.
The good one: video and synchronized stereo audio are denoised together,
in one packed sequence, by one transformer, in four steps β and the whole
thing (33 B DiT, a Qwen3-VL-32B prompt encoder, a ViT3D video decoder and
a BigVGAN vocoder) is one 23.9 GB file read by one 8 MB Rust binary. The
AVI and the WAV are written by that binary too; there is no ffmpeg in
this pipeline, and the GIF preview below is assembled here by pulling the
JPEG frames straight out of the AVI rather than shelling out to one.
The warning: this is a CPU Space. On 48 cores a 512x288 clip takes 346 s
and on one RTX PRO 6000 it takes 172 s. On the hardware here even the
smallest setting is minutes. So the gallery comes first β those clips
were rendered on the real thing β and generating your own is opt-in,
with the cost stated before you press the button.
"""
import os
import subprocess
import tarfile
import tempfile
import time
import urllib.request
import uuid
from pathlib import Path
import gradio as gr
from huggingface_hub import hf_hub_download
from PIL import Image
RELEASE = (
"https://github.com/infosave2007/cmf/releases/latest/download/"
"cortiq-x86_64-unknown-linux-gnu.tar.gz"
)
REPO = "infosave/MiniMax-H3-Turbo-cmf"
FILE = "mmh3-turbo-q4tp.cmf" # t2va: text in, video and audio out
BIN = Path("bin/cortiq")
WORK = Path(tempfile.gettempdir()) / "animate"
WORK.mkdir(parents=True, exist_ok=True)
# No CMF_GPU here on purpose. Mesa ships a SOFTWARE Vulkan driver
# (lavapipe/llvmpipe) in most container images, and until 0.5.61 wgpu
# would enumerate it, the engine would report "GPU path: on", and every
# shader would run through an LLVM rasteriser on the same cores the
# native kernels were already using. 0.5.61 declines a DeviceType::Cpu
# adapter and keeps the CPU path, so this Space now exercises that
# decision on a real container rather than pinning it with an env var.
_model: str | None = None
def cores() -> str:
"""How many cores this container may actually use.
`os.cpu_count()` reports the HOST's, which on a Space is wildly wrong:
a cpu-basic container saw 16 while its quota was 2. The truth is the
cgroup v2 quota β "<quota> <period>" in microseconds, or "max" when
uncapped.
"""
try:
q, p = Path("/sys/fs/cgroup/cpu.max").read_text().split()
if q != "max":
return f"{int(q) / int(p):g} vCPU"
except Exception: # noqa: BLE001 β cgroup v1, macOS, anything else
pass
return f"{os.cpu_count()} vCPU"
def binary() -> str:
if not BIN.exists():
BIN.parent.mkdir(parents=True, exist_ok=True)
tgz = BIN.parent / "c.tar.gz"
urllib.request.urlretrieve(RELEASE, tgz)
with tarfile.open(tgz) as t:
t.extractall(BIN.parent)
tgz.unlink()
BIN.chmod(0o755)
return str(BIN)
# ZeroGPU was tried and does not work for this binary, which is worth
# recording because the reason is not the obvious one. A startup probe on
# `zero-a10g` found the GPUs present (/dev/nvidia0..7 on a 192-core host)
# but the Vulkan ICD directory holding only MESA drivers β
# intel_icd, intel_hasvk_icd, radeon_icd and lvp_icd (a software
# rasteriser). There is no nvidia_icd.json, and NVIDIA_DRIVER_CAPABILITIES
# is unset, so the container runtime never injected the graphics half of
# the driver. cortiq reaches a GPU through wgpu -> Vulkan, not CUDA, so a
# card it cannot enumerate is a card it cannot use: `@spaces.GPU` would
# hand this process a device that the only API it speaks cannot see.
# CUDA being present says nothing. Hence CPU.
def sample(name: str) -> str:
return hf_hub_download(REPO, f"samples/{name}")
def avi_to_gif(avi: Path, out: Path, fps: int = 12) -> Path | None:
"""Pull the JPEG frames out of an MJPEG AVI without a decoder.
Every frame in this file is a complete JPEG, so the frames are just
the byte ranges between each SOI (FFD8FF) and the EOI (FFD9) that
follows it. Scanning for them is a few lines and keeps this page
honest: a demo whose whole claim is "no ffmpeg" should not need one
to show its own output.
"""
data = avi.read_bytes()
frames, i = [], 0
while True:
s = data.find(b"\xff\xd8\xff", i)
if s < 0:
break
e = data.find(b"\xff\xd9", s + 3)
if e < 0:
break
try:
frames.append(Image.open(__import__("io").BytesIO(data[s : e + 2])).convert("RGB"))
except Exception: # noqa: BLE001 β a stray marker is not fatal
pass
i = e + 2
if not frames:
return None
frames[0].save(
out, save_all=True, append_images=frames[1:],
duration=int(1000 / fps), loop=0, optimize=True)
return out
def model() -> str:
global _model
if _model is None:
_model = hf_hub_download(REPO, FILE)
return _model
def animate(prompt, width, height, frames, steps, progress=gr.Progress()):
if not (prompt or "").strip():
yield None, None, None, "Write a prompt first."
return
progress(0.02, desc="binary")
exe = binary()
progress(0.05, desc="model β 23.9 GB on a cold start, this is the slow part")
path = model()
tag = uuid.uuid4().hex[:8]
out = WORK / f"{tag}.avi"
cmd = [
exe, "animate", path, "--prompt", prompt,
"--width", str(int(width)), "--height", str(int(height)),
"--frames", str(int(frames)), "--steps", str(int(steps)),
"--out", str(out),
]
log = f"$ cortiq animate {FILE} --prompt {prompt!r} \\\n"
log += f" --width {int(width)} --height {int(height)} --frames {int(frames)} --steps {int(steps)}\n\n"
yield None, None, None, log
t0 = time.time()
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
for line in p.stdout:
log += line
yield None, None, None, log
p.wait()
if p.returncode != 0 or not out.exists():
yield None, None, None, log + f"\nRender failed (exit {p.returncode}).\n"
return
wav = out.with_suffix(".wav")
gif = avi_to_gif(out, WORK / f"{tag}.gif")
log += f"\n{time.time() - t0:.0f} s on {cores()}.\n"
yield str(gif) if gif else None, str(wav) if wav.exists() else None, str(out), log
with gr.Blocks(title="MiniMax-H3 Turbo from one .cmf") as demo:
gr.Markdown(
"""# Video **and its soundtrack**, from one file
One prompt, one transformer, four steps β and out comes a clip *and* the
stereo audio that goes with it, denoised together in one packed sequence
on two different flow schedules.
Four files and a ComfyUI checkout β 124.4 GB β become one **23.9 GB
`.cmf`**: the 33 B DiT, its Qwen3-VL-32B prompt encoder, the ViT3D video
decoder, the BigVGAN vocoder. The 4-step Turbo LoRA is merged in, so the
file *is* the turbo model. The AVI and the WAV are written by the same
8 MB binary β no ffmpeg anywhere in the pipeline.
[Model](https://huggingface.co/infosave/MiniMax-H3-Turbo-cmf) Β·
[Format](https://huggingface.co/infosave/cmf) Β·
[Source](https://github.com/infosave2007/cmf)
"""
)
with gr.Tab("Gallery"):
gr.Markdown(
"Rendered on the real hardware β 512Γ288, 39 frames, 4 steps, "
"**172 s** on one RTX PRO 6000 (346 s on 48 CPU cores)."
)
with gr.Row():
with gr.Column():
gr.Markdown("### Text to video, with sound")
gr.Image(sample("corgi_512x288_4step.gif"), label="corgi_512x288_4step", height=300)
gr.Audio(sample("corgi_512x288_4step.wav"), label="its soundtrack")
with gr.Column():
gr.Markdown("### Keyframe to video β one picture in, a clip out")
gr.Image(sample("i2v_corgi_flip.gif"), label="i2v_corgi_flip", height=300)
gr.Audio(sample("i2v_corgi_flip.wav"), label="its soundtrack")
gr.Markdown("### Four bits against two β same prompt, same seed, same steps")
with gr.Row():
gr.Image(sample("ab_q4tp.gif"), label="q4tp β 23.9 GB, recommended", height=260)
gr.Image(sample("ab_q2tp.gif"), label="q2tp β 18.7 GB, faster, stops following the prompt", height=260)
with gr.Tab("Render your own"):
gr.Markdown(
"""**Read this before pressing the button.** This Space has no GPU.
The model is 23.9 GB and is fetched on the first render of a cold Space,
which alone takes a few minutes; the render itself is several more, even
at the smallest size. The settings below are capped accordingly.
On your own machine there is no cap:
```sh
cargo install cortiq-cli
hf download infosave/MiniMax-H3-Turbo-cmf mmh3-turbo-q4tp.cmf --local-dir .
CMF_MMH3_GPU=1 cortiq animate mmh3-turbo-q4tp.cmf \\
--prompt "a corgi in a chef hat flipping a pancake" --out clip.avi
```
"""
)
prompt = gr.Textbox(
label="Prompt",
lines=2,
value="A corgi in a chef hat flipping a pancake, sizzling sounds and a cheerful bark.")
with gr.Row():
width = gr.Radio([256], value=256, label="Width")
height = gr.Radio([160], value=160, label="Height")
nframes = gr.Radio([13, 22], value=13, label="Frames")
steps = gr.Radio([4], value=4, label="Steps (the LoRA is trained for 4)")
go = gr.Button("Render β expect minutes", variant="primary")
with gr.Row():
gif = gr.Image(label="Clip", height=300)
wav = gr.Audio(label="Soundtrack")
avi = gr.File(label="The AVI, as the binary wrote it")
log = gr.Textbox(label="Log", lines=14, max_lines=14)
go.click(animate, [prompt, width, height, nframes, steps], [gif, wav, avi, log])
demo.queue(max_size=4).launch()
|