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 校验)
  • 400 turbo=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 装卸开销,批量跑同一种模式更快。