API.md
4.88 KB
MiniMax-H3 视频生成 API
服务在服务器上:/data/models/minimax-h3/api.py + api-run.sh(本地是副本)。
为什么是异步任务式,不是同步接口
| 事实 | 后果 |
|---|---|
| 模型加载 240 s(首次,fp32 权重从机械盘读;page cache 命中 20 s) | 不能每请求加载 |
| 单条视频 Turbo 4 步约 7 分钟,50 步约 90 分钟 | 同步 HTTP 必然超时 |
| 一条生成占 12.3 G 显存 + 约 32 G 内存 | 22 G 卡上同时只能跑一条 |
所以接口是:提交任务拿 id → 轮询状态 → 下载文件。并发提交会排队,不会 OOM。
启动
cd /data/models/minimax-h3
./api-run.sh 1 8010 # GPU_ID=1, PORT=8010
# 后台跑:
setsid nohup ./api-run.sh 1 8010 > logs/api.log 2>&1 < /dev/null &
⚠️ 显存前提:这条服务常驻约 12.3 G。llm38 跑 256K 上下文时占 57.3 G(四卡各约 14.3 G),
两者在同一张卡上放不下。跑视频前先 docker stop llm38(这是测试机,见工作区 CLAUDE.md),
或把 H3 放到 llm38 没用满的卡上并自行核算。恢复用 qwen38-deploy/RESTORE-GPU.md。
接口
GET /health
{"status":"ok","model":"ready","gpu":"1","queued":0,"running":0}
status 在模型加载完成前是 loading。别在 loading 时提交任务——会排队但看不出进度。
POST /v1/videos → 202
curl -X POST http://192.168.110.67:8010/v1/videos \
-H "Content-Type: application/json" \
-d '{
"prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼,尾巴轻轻摆动",
"width": 832, "height": 480, "frames": 124,
"steps": 4, "turbo": true, "seed": 7
}'
{"id":"ca0f2f095ddd","status":"queued","queue_position":1,
"eta_seconds":428,"poll":"/v1/videos/ca0f2f095ddd"}
| 字段 | 默认 | 范围 | 说明 |
|---|---|---|---|
prompt |
必填 | 1–2000 字符 | 中英文都行;含人物台词时用英文引号包住那句 |
width |
832 | 64–832 | |
height |
480 | 64–480 | |
frames |
124 | 8–124 | 24 fps,124 帧 ≈ 5.17 秒 |
steps |
4 | 1–50 | turbo 时应 ≤8 |
turbo |
true | true=4 步蒸馏 LoRA;false=全质量多步 | |
seed |
0 | 0–2³¹−1 | 同 seed 同参数结果可复现 |
flow_shift |
自动 | 不传则 turbo=6.0 / 非 turbo=12.0 |
上界不是随手写的:832×480×124f 是实测跑通的最高档(峰值 12.2 G)。再大在 22 G 卡上 OOM, 所以在入口就挡住,而不是让它跑 7 分钟后崩掉。
错误码:
422参数越界或缺失(pydantic 校验)400turbo=true且steps>8(Turbo LoRA 是 4 步蒸馏的,多步没意义)
GET /v1/videos/{id}
{"id":"ca0f2f095ddd","status":"running","step":0,"total_steps":4,
"elapsed_seconds":132, ...}
status: queued → running → done / failed。
完成后多出 file、size_bytes、peak_vram_gb、download。
GET /v1/videos/{id}/file
返回 mp4。任务未完成时返回 409。
GET /v1/videos?limit=50
列出最近任务。
完整调用示例(Python)
import time, requests
API = "http://192.168.110.67:8010"
r = requests.post(f"{API}/v1/videos", json={
"prompt": "清晨的中式庭院,一位女子坐在长廊上喝茶,阳光透过竹叶",
"steps": 4, "turbo": True, "seed": 42,
}, timeout=30)
r.raise_for_status()
jid = r.json()["id"]
print("任务", jid, "预计", r.json()["eta_seconds"], "秒")
while True:
j = requests.get(f"{API}/v1/videos/{jid}", timeout=30).json()
if j["status"] in ("done", "failed"):
break
print(j["status"], j.get("elapsed_seconds", 0), "s")
time.sleep(20)
if j["status"] == "failed":
raise SystemExit("失败: " + j["error"])
open(f"{jid}.mp4", "wb").write(
requests.get(f"{API}/v1/videos/{jid}/file", timeout=300).content)
print("已保存", j["size_bytes"], "字节,峰值显存", j["peak_vram_gb"], "G")
服务端会自己验产物
写出文件不等于成功——fp16 时代踩过:全黑视频照样写出 24 KB 的 mp4 并返回退出码 0。 所以 worker 在写文件前会检查:
- 抽 8 帧算像素
std,< 1.0判为纯色 → 任务标failed - 音频张量含非有限值 → 任务标
failed
这两条不通过时不会给你一个"成功但没用"的文件。
已知限制
- 无鉴权,和这台机器上其他服务一致。放到公网前必须加。
- 单 worker 串行。要并发就每张空闲卡起一个实例(不同 PORT),自己在上层做负载。
- 任务状态存在内存里,进程重启就丢;已生成的 mp4 文件还在
outputs/。 真要持久化就把JOBS换成 sqlite——目前按需再说。 turbo=false时会clear_lora(),任务结束后再挂回来。混合提交 turbo/非 turbo 时 每次切换都有 LoRA 装卸开销,批量跑同一种模式更快。