Run H3 with a server and playground
Start FastH3 on CUDA, one DGX Spark, or Apple Silicon MLX, then iterate on prompts in a browser, with cURL, or from your app. The server and clients can run on the same machine.
The server supports the OpenAI-compatible video-job API. The Python and JavaScript client examples use that interface, but requests go to FastVideo. You do not need an OpenAI account or cloud key.
The H3 recipe selector provides the same workflow with runtime selection. This guide covers FastH3 V1 (four forwards) and FastH3 V2 (eight forwards). Start a server, then iterate in the playground or with the OpenAI Python client. Other H3 recipes keep their direct Python commands.
CUDA requests reuse one loaded VideoGenerator. The Python SDK can do the same
when you reuse the generator across generate() calls. MLX keeps one
MiniMaxH3MLXPipeline and a prompt-embedding cache, but still loads and releases
components between phases to fit unified memory. MLX serving does not keep all
weights resident or remove phase-loading time. Use a server when you want
separate clients to share the process without restarting scripts.
Install and start the server
CUDA
Use a FastVideo clone and an activated Python environment. Complete the CUDA installation requirements before running these commands:
UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"
fastvideo serve --config examples/serving/openai_fasth3.yaml --server.host 127.0.0.1
The configuration loads FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2 and
advertises it as fasth3. It configures four CUDA GPUs but does not record
a GPU model or VRAM requirement. This is a source-backed server profile, not
the measured GB200 Python performance profile. Compilation is disabled.
For FastH3 V2, use the same install and the 8-step config (nine sigma points, eight DiT forwards, VSA sparsity 0.8):
UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"
fastvideo serve --config examples/serving/openai_fasth3_8step.yaml --server.host 127.0.0.1
Keep the server running. In another terminal, check readiness:
curl --fail-with-body http://127.0.0.1:8000/health
After model loading completes, the response is {"status":"ok"}.
NVIDIA DGX Spark
Complete the DGX Spark installation before running these commands. GB10 has no FA4 / sm_100a VSA kernel:
UV_TORCH_BACKEND=cu130 uv pip install -e .
FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 \
fastvideo serve --config examples/serving/openai_fasth3_spark.yaml --server.host 127.0.0.1
The configuration loads FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree
on one GB10 and advertises it as fasth3. Lazy module load still reloads
Qwen3-VL and the DiT between phases of each request. Legal num_frames values
are 17n+5, capped at 362 (15.08 s); a 345-frame request on one Spark can OOM.
There is no cookbook server for two Sparks; use the generate YAML after
pairing two Sparks.
Keep the server running. In another terminal, check readiness:
curl --fail-with-body http://127.0.0.1:8000/health
After model loading completes, the response is {"status":"ok"}.
Apple Silicon MLX
Complete the MLX install,
including ffmpeg. From your FastVideo clone, install the MLX extra:
uv pip install -e ".[mlx]"
Download and convert the weights once. Skip this step if you already have the snapshot and converted DiT:
hf download FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2 --local-dir ./FastH3-Preview-v0.2
python scripts/checkpoint_conversion/convert_minimax_h3_mlx.py --model-root ./FastH3-Preview-v0.2/transformer --out ./FastH3-MLX --formats "int6"
Edit generator.model_root and generator.mlx_checkpoint in
examples/serving/mlx_fasth3.yaml if your weights are elsewhere. Paths are
relative to the directory where you launch the server. Start it with:
python -m fastvideo.entrypoints.openai.mlx_server --config examples/serving/mlx_fasth3.yaml
This uses the native MLX pipeline, not PyTorch MPS. The default output is
832 × 480, 124 frames at 24 fps, with four DiT forwards and the full H3 VAE.
The HTTP field num_inference_steps=5 describes five sigma points; the adapter
passes num_steps=4 to MLX. Temporal/spatial fast modes, VSA, reference inputs,
LoRA selection, and alternate decoders are not exposed by this server adapter.
Unsupported request options return HTTP 400 before a job starts.
The server binds to 127.0.0.1:8000 and advertises fasth3, so the playground
and clients below work unchanged. Do not run CUDA, Spark, and MLX servers on the
same port. MLX readiness means the pipeline is initialized; components load during
generation. The first request can take longer than a repeated cached prompt.
The MLX server has no recorded device or unified-memory requirement. The direct Python recipe’s M4 Max measurements are not a server benchmark or a minimum-memory claim.
For FastH3 V2, convert that checkpoint’s transformer with --include-vsa
and start the 8-step config. Do not overwrite a V1 export. The
MiniMax H3 cookbook has the same download and convert
commands as the Python recipe.
hf download FastVideo/FastVideo-FastH3-8-Step-V2 --local-dir ./FastH3-8-Step-V2
python scripts/checkpoint_conversion/convert_minimax_h3_mlx.py --model-root ./FastH3-8-Step-V2/transformer --out ./FastH3-8-Step-V2-MLX --formats "int8" --include-vsa
python -m fastvideo.entrypoints.openai.mlx_server --config examples/serving/mlx_fasth3_8step.yaml
That adapter passes num_steps=8 when the HTTP field is num_inference_steps=9,
and it enables the trained VSA recipe (sparsity 0.8, 64-token tiles). Reuse the
preview VAE, audio VAE, text encoder, and tokenizer if those directories already
exist; edit the YAML paths if your files live elsewhere.
Open the playground
Open the local H3 playground after startup. Write a prompt, optionally set a seed, then select Generate video. The page checks the job status and shows the completed video with a download link. Edit the prompt and generate again without restarting the server.
The playground sends requests to /v1/videos on the same server that serves
the page. Use this prompt with cURL shows the equivalent submission. Recent
jobs include requests from other clients, so a script and the playground can
use the same model process. Opening the page or copying a command does not
start generation.
The page URL includes the active job ID. Reloading that URL resumes status checks without resubmitting the prompt. A failed connection or a 30-minute polling timeout does not cancel execution. Select Check status to reconnect. If submission itself is interrupted, check Recent jobs before submitting again.
This first playground supports H3 text-to-video/audio. Reference-media inputs remain available through the API, not through the playground. It does not start or manage a GPU server for you.
Generate with cURL or an SDK
These examples use the server’s resolution, frame count, and sampling defaults.
Do not copy Sora-specific durations or resolutions onto H3. V1 configs use
124 frames, 24 fps, and the five-point distilled sigma schedule with four DiT
forwards. V2 configs use nine sigma points and eight DiT forwards. CUDA
and one Spark use 1344 × 768; MLX uses 832 × 480. The OpenAI Python client is
the same for every FastH3 server that advertises fasth3.
Each client submits a job, checks for completion or failure, and downloads an MP4 named after the job ID. Polling stops after 30 minutes; a timeout does not cancel GPU execution. Keep the printed job ID to retrieve its status later. Transport retries are disabled to avoid accidental duplicate submissions.
OpenAI Python
Install the tested client, then run the checked-in example:
python -m pip install openai==3.6.0
python examples/serving/clients/video.py
# SPDX-License-Identifier: Apache-2.0
"""Generate with a local FastVideo server through the OpenAI Python client.
Install the tested client with: python -m pip install openai==3.6.0
Start the server first. No OpenAI account or cloud API key is needed.
"""
import os
import time
from pathlib import Path
from openai import OpenAI
def main() -> None:
with OpenAI(
base_url=os.environ.get("FASTVIDEO_BASE_URL", "http://127.0.0.1:8000/v1"),
api_key=os.environ.get("FASTVIDEO_API_KEY", "local"),
timeout=60.0,
max_retries=0,
) as client:
video = client.videos.create(
model=os.environ.get("FASTVIDEO_MODEL", "fasth3"),
prompt="A fox runs through fresh snow.",
)
print(f"Submitted {video.id}")
# This deadline limits polling, not GPU execution. Increase it for
# longer jobs. On timeout, use the printed ID to retrieve the job.
deadline = time.monotonic() + 1800
while video.status in {"queued", "in_progress"}:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError(f"Polling timed out; job {video.id} may still be running")
time.sleep(min(2, remaining))
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError(f"Polling timed out; job {video.id} may still be running")
video = client.videos.retrieve(video.id, timeout=min(60, remaining))
if video.status != "completed":
message = video.error.message if video.error else video.status
raise RuntimeError(f"Video {video.id} failed: {message}")
output = Path(f"{video.id}.mp4")
content = client.videos.download_content(video.id)
content.write_to_file(output)
print(f"Saved {output}")
if __name__ == "__main__":
main()
OpenAI JavaScript
Use Node.js 22 or later on your computer or in your webapp’s backend. Do not put a private server key in browser code.
npm ci --prefix examples/serving/clients
node examples/serving/clients/video.mjs
// Run in Node.js 22+ or your webapp's backend, not in a browser with a private key.
import OpenAI from "openai";
import { writeFile } from "node:fs/promises";
const client = new OpenAI({
baseURL: process.env.FASTVIDEO_BASE_URL || "http://127.0.0.1:8000/v1",
apiKey: process.env.FASTVIDEO_API_KEY || "local",
timeout: 60_000,
maxRetries: 0,
});
let video = await client.videos.create({
model: process.env.FASTVIDEO_MODEL || "fasth3",
prompt: "A fox runs through fresh snow.",
});
console.log(`Submitted ${video.id}`);
// The deadline limits polling, not GPU execution. Keep the ID to resume later.
const deadline = performance.now() + 1_800_000;
while (video.status === "queued" || video.status === "in_progress") {
let remaining = deadline - performance.now();
if (remaining <= 0) throw new Error(`Polling timed out; job ${video.id} may still be running`);
await new Promise((resolve) => setTimeout(resolve, Math.min(2000, remaining)));
remaining = deadline - performance.now();
if (remaining <= 0) throw new Error(`Polling timed out; job ${video.id} may still be running`);
video = await client.videos.retrieve(video.id, { timeout: Math.min(60_000, remaining) });
}
if (video.status !== "completed") {
throw new Error(`Video ${video.id} failed: ${video.error?.message || video.status}`);
}
const content = await client.videos.downloadContent(video.id);
const output = `${video.id}.mp4`;
await writeFile(output, Buffer.from(await content.arrayBuffer()));
console.log(`Saved ${output}`);
cURL
Install curl and jq, then run:
bash examples/serving/clients/video.sh
#!/usr/bin/env bash
# Requires curl and jq. Run after starting the FastVideo server.
set -euo pipefail
base_url="${FASTVIDEO_BASE_URL:-http://127.0.0.1:8000/v1}"
api_key="${FASTVIDEO_API_KEY:-local}"
model="${FASTVIDEO_MODEL:-fasth3}"
payload=$(jq -n --arg model "$model" \
'{model: $model, prompt: "A fox runs through fresh snow."}')
job=$(curl --fail-with-body --silent --show-error --max-time 60 \
"$base_url/videos" \
-H "Authorization: Bearer $api_key" \
-H 'Content-Type: application/json' \
-d "$payload")
job_id=$(jq -er '.id' <<< "$job")
printf 'Submitted %s\n' "$job_id"
deadline=$((SECONDS + 1800))
while true; do
status=$(jq -er '.status' <<< "$job")
case "$status" in
completed) break ;;
failed) jq -r '.error.message' <<< "$job" >&2; exit 1 ;;
queued|in_progress) ;;
*) printf 'Unexpected job status: %s\n' "$status" >&2; exit 1 ;;
esac
remaining=$((deadline - SECONDS))
if (( remaining <= 0 )); then
printf 'Polling timed out; job %s may still be running\n' "$job_id" >&2
exit 1
fi
if (( remaining > 2 )); then sleep 2; else sleep "$remaining"; fi
remaining=$((deadline - SECONDS))
if (( remaining <= 0 )); then continue; fi
if (( remaining > 60 )); then remaining=60; fi
job=$(curl --fail-with-body --silent --show-error --max-time "$remaining" \
-H "Authorization: Bearer $api_key" "$base_url/videos/$job_id")
done
curl --fail-with-body --silent --show-error --max-time 300 \
-H "Authorization: Bearer $api_key" \
"$base_url/videos/$job_id/content" --output "$job_id.mp4"
printf 'Saved %s.mp4\n' "$job_id"
Connect your app
Set FASTVIDEO_BASE_URL to the FastVideo endpoint, including /v1, and
FASTVIDEO_MODEL to its advertised model alias. The examples default to
http://127.0.0.1:8000/v1 and fasth3.
For a remote GPU machine, keep the server bound to loopback and forward the
port. Replace user@gpu-host with your SSH destination:
ssh -N -L 8000:127.0.0.1:8000 user@gpu-host
Then open http://127.0.0.1:8000/playground/ on your computer. The same forwarded
address works for the cURL and SDK clients. If local port 8000 is occupied, use
-L 8001:127.0.0.1:8000, open port 8001, and set FASTVIDEO_BASE_URL to
http://127.0.0.1:8001/v1 for the example clients.
Your webapp backend can submit a job and return its ID to the browser. Poll from the backend, then proxy the completed download or store it in your own artifact store. Do not hold a browser request open for the entire generation.
The client key local is a placeholder required by the SDK, not server
authentication. FastVideo’s HTTP server has no built-in API-key check. Before
public deployment, put it behind an authenticated TLS proxy with restricted
origins, request limits, and access controls. If your proxy uses bearer tokens,
set FASTVIDEO_API_KEY on your backend. Never reuse an OpenAI cloud key here.
Compatibility and limits
The client examples target the OpenAI video-job API, not Chat Completions. The compatibility tests cover real HTTP requests with the pinned SDKs and a fake generator. They establish client and protocol behavior, not GPU generation quality, latency, or memory use. The server configuration still needs a recorded H3 hardware run before it can be marked Verified in the cookbook.
| Operation | Endpoint |
|---|---|
| List served models | GET /v1/models |
| Create a video job | POST /v1/videos |
| Retrieve status, including a failed job | GET /v1/videos/{id} |
| List jobs | GET /v1/videos |
| Download the MP4 | GET /v1/videos/{id}/content |
| Delete a job and its artifact | DELETE /v1/videos/{id} |
Failed jobs return HTTP 200 with status: "failed" and an error object so
SDK polling can stop. Invalid requests return HTTP 400; missing jobs return
HTTP 404. The download endpoint supports only the video variant, not
thumbnails or spritesheets. Remix, extensions, characters, and an OpenAI Files
store are not implemented. /v1/videos/sync is a FastVideo extension and is
not used by these client examples.
OpenAI marks its hosted Sora API as deprecated. FastVideo runs its own models; the OpenAI video API reference describes the client interface, not FastVideo model availability. The examples pin tested SDK versions because future SDKs may change or remove video helpers.
The server serializes generation through one loaded pipeline. Job metadata is in memory and is lost on restart. Async artifacts remain on disk until deleted through the API; establish a retention policy for production use. Deleting an in-progress job does not interrupt an already running CUDA call.
For model-specific request fields and reference media, see the HTTP contract.