cmf-animate / app.py
infosave's picture
0.5.61 declines the software rasteriser itself
50fce84 verified
Raw
History Blame Contribute Delete
9.96 kB
"""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()