API.md 6.09 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

这两条不通过时不会给你一个"成功但没用"的文件。

任务记录会落盘(2026-08-20 起)

JOBS 曾经只在内存里,进程重启(比如 h3-console 弹性调度器切 fl2va/ref2va 分区时的 kill+restart)就把这张表清空——视频文件在 outputs/ 没事,但 id→文件路径这层映射没了, GET /v1/videos/{id}、下载都会 404。现在改成每次状态变化都往 sqlite 写一份:

  • 落盘文件是 jobs_gpu{GPU_ID}.sqlite3(跟 api.py 同目录),四张卡各一份,不共享—— 一个进程只会在同一张 GPU 上起来,per-GPU 分文件天然不会有另一个进程来抢,不用处理 跨进程写锁竞争。
  • 进程启动时会把这个文件读回内存,日志能看到 [api] 从磁盘恢复了 N 条历史任务记录。
  • in-flight(queued/running)的记录没法真正恢复——GPU 上下文和排队队列本来就不落盘, 重启后没有任何东西能"接着跑"。这类记录读回来会被改判为 failed,error 字段写明 "进程重启导致这条任务的执行状态丢失"。只有 done/failed 这两个终态是完整保留的。
  • 只影响直接调用 api.py/gateway.py 的场景——如果你是通过 h3-console(端口 8080) 提交的任务,即使碰到上面这种真正没法恢复的 in-flight 丢失,console 自己也存了一份 影子记录,历史和已完成任务的文件不受影响。

已知限制

  • 无鉴权,和这台机器上其他服务一致。放到公网前必须加。
  • 单 worker 串行。要并发就每张空闲卡起一个实例(不同 PORT),自己在上层做负载。
  • turbo=false 时会 clear_lora(),任务结束后再挂回来。混合提交 turbo/非 turbo 时 每次切换都有 LoRA 装卸开销,批量跑同一种模式更快。