Spaces:
Paused
Paused
| """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() | |