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()