H3 Console API 文档
给要接入 MiniMax-H3 文生视频能力的项目用。这是 console 层的接口(8080 局域网 / 8081
公网 /console 子路径),不是底层 raylight 的接口。
⚠️ 务必通过 console 提交任务,不要绕过 console 直连 raylight(内网 8100 端口)。 raylight 是纯计算队列,没有鉴权、没有任务持久化(进程重启就丢)、没有软删除/回收站, console 才是任务的唯一权威记录(sqlite)。2026-09-05 曾发生过一次事故:外部脚本直连 8100 提交任务,console 的后台发现逻辑和这些直连任务的簿记之间出现竞态,同一个真实生成 任务在 console 的任务列表里出现了两条记录。所有集成都应该只打 console 的
/v1/*。
1. 基础信息
| 项目 | 值 |
|---|---|
| 局域网 Base URL | http://192.168.110.67:8080 |
| 公网 Base URL | http://qwen.wudaima.com/console(frp 反代,注意所有路径都要带 /console 前缀) |
| 内容类型 | 请求/响应都是 application/json,上传接口是 multipart/form-data |
| 字符编码 | UTF-8,中文 prompt 直接传,不需要额外转义 |
| 鉴权 | 见下 |
鉴权
/v1/* 下所有接口都可能要求 Bearer token(页面本身 /、/task.html/* 不需要)。两种传法二选一,服务端逻辑完全等价:
Authorization: Bearer <token>
或者拼在查询串里(给 <video src>、<a download> 这类浏览器原生请求用,因为它们加不了自定义 header):
GET /v1/videos/{id}/file?token=<token>
token 是否强制取决于部署时是否设了 H3_CONSOLE_AUTH_TOKEN 环境变量;公网实例目前已启用,请找服务器管理员要 token。带错/不带 token 会收到:
// HTTP 401
{"detail": "缺少或错误的访问令牌(Authorization: Bearer <token> 或 ?token=)"}
2. 核心概念
任务 id:console 生成自己的 12 位十六进制 id(跟底层 raylight 的任务 id 是两个不同的值),所有 /v1/videos/{id}/* 接口都用这个 console id,不要用别处看到的 raylight id。
三种生成模式,不是显式传的参数,是 console 按你传了哪些字段自动判断的:
| 模式 | 判断依据 |
|---|---|
text(文生视频) |
没传 first_frame_url/last_frame_url,也没传任何 reference_* |
fl2va(首尾帧) |
传了 first_frame_url 或 last_frame_url |
ref2va(参考图/参考视频/参考音频) |
传了 reference_image_urls/reference_video_urls/reference_audio_urls 任意一个非空 |
任务状态机:queued → running → done / failed / cancelled。另外有一个独立维度 deleted_at(软删除/回收站),跟 status 正交——一条 done 的任务一样可以被删进回收站,不影响它已经生成好的文件。
并发模型(重要):4 张 GPU 是一个 FSDP 集群,不是 4 个独立 worker。任何时刻只有 1 条任务处于 running,其余全部在队列里排队等这一条跑完。这不是接口限流——可以放心一次提交很多条,它们会老老实实排队,但不要指望"提交越多并行跑得越快",吞吐量就是"一条接一条"。
3. 接口一览
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /v1/videos |
提交生成任务 |
| GET | /v1/videos |
任务列表(分页/筛选/排序) |
| GET | /v1/videos/{id} |
单条任务详情(会顺带触发一次状态刷新) |
| GET | /v1/videos/{id}/file |
下载/播放生成的视频(支持 Range) |
| GET | /v1/videos/{id}/comfy_history |
排查用:原始 ComfyUI 执行记录 |
| POST | /v1/videos/{id}/cancel |
取消排队中的任务 / 停止运行中的任务 |
| POST | /v1/videos/{id}/prioritize |
插队到最前面(仅排队中的任务) |
| DELETE | /v1/videos/{id} |
软删除(移入回收站) |
| POST | /v1/videos/{id}/restore |
从回收站恢复 |
| POST | /v1/videos/batch-cancel |
批量取消/停止 |
| POST | /v1/videos/batch-delete |
批量软删除 |
| POST | /v1/assets |
上传参考图/参考视频/参考音频,换取可用的 URL |
| GET | /v1/backends |
健康检查 + 队列深度 + GPU 显存/利用率 |
| GET | /v1/models |
支持的分辨率档位(含实测帧数上限) |
| POST | /v1/admin/reset-gpu |
硬重启 GPU 服务(危险操作,见下) |
4. 提交生成任务
POST /v1/videos
⚠️ 这个接口永远返回 HTTP 202,哪怕参数校验直接失败也是 202——必须检查响应体里的
status字段,不能只看 HTTP 状态码判断成功与否。校验失败时status会是"failed",error字段里是失败原因(原样透传自底层校验器,格式见下面例子,不算稳定 API,只用来人读)。
请求体字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
prompt |
string | 必填 | 生成提示词,支持中英文/超长文本 |
width |
int | 832 | 64–2048。跟 height 的组合决定能用哪些帧数(见下面「分辨率档位」) |
height |
int | 480 | 同上 |
frames |
int | 124 | 5–3000。总帧数,按 24fps 折算成秒数 |
steps |
int | 20 | 1–50 |
turbo |
bool | false | 当前是 no-op,Raylight 还没接 Turbo LoRA,传了不生效 |
seed |
int | 0 | 0–2147483647 |
cfg_scale |
float\ | null | null |
negative_prompt |
string\ | null | null |
first_frame_url |
string\ | null | null |
last_frame_url |
string\ | null | null |
reference_image_urls |
string[] | [] |
ref2va 模式:参考图,数量没有上限 |
reference_video_urls |
string[] | [] |
ref2va 模式:参考视频,最多 3 个,2–15 秒,音轨自动带上 |
reference_audio_urls |
string[] | [] |
ref2va 模式:独立音频参考,最多 3 个,不跟参考视频绑定 |
图片/视频/音频字段填的都是 URL,不是文件内容本身——本地文件先调 POST /v1/assets 上传换 URL(见第 8 节),或者直接给一个公网可访问的图床/视频 URL 也行(底层是 urllib.urlopen 直接下载,服务器要能访问到这个地址)。
分辨率档位与帧数上限(超出直接判失败,不会真的提交给 GPU)
只有下表这 8 个(宽,高)组合是实测过真实显存上限的,其它组合不拦(能提交但没人验证过会不会 OOM):
| 分辨率 | 帧数上限 | 约合秒数(24fps) |
|---|---|---|
| 832×480 / 480×832 | 1125 | ≈46.9s |
| 448×256 / 256×448 | 1200 | ≈50.0s(这个是画质上限,不是显存上限) |
| 1024×576 / 576×1024 | 743 | ≈31.0s |
| 1344×768 / 768×1344 | 412 | ≈17.2s |
实时以 GET /v1/models 返回的 presets 为准,这份表可能会随实测结果调整。
示例:文生视频(text)
curl -X POST http://192.168.110.67:8080/v1/videos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "清晨的中式庭院,一位女子坐在长廊上喝茶……",
"width": 480, "height": 832, "frames": 243,
"steps": 20, "seed": 0
}'
示例:首尾帧(fl2va)
curl -X POST http://192.168.110.67:8080/v1/videos \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"prompt": "……",
"first_frame_url": "http://192.168.110.67:8080/assets/xxx__first.png",
"last_frame_url": "http://192.168.110.67:8080/assets/xxx__last.png",
"width": 480, "height": 832, "frames": 124
}'
示例:参考图/参考视频(ref2va)
curl -X POST http://192.168.110.67:8080/v1/videos \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"prompt": "……",
"reference_image_urls": ["http://.../a.png", "http://.../b.png"],
"reference_video_urls": ["http://.../ref.mp4"],
"width": 480, "height": 832, "frames": 240
}'
成功响应(202,status 为 queued/running/failed 之一,见下方“Job 对象”)
校验失败也是 202,靠 status 字段识别——真实例子
字段范围校验失败(frames 超过硬上限 3000):
{
"id": "7df2a3c356b1",
"status": "failed",
"error": "{\"detail\":[{\"type\":\"less_than_equal\",\"loc\":[\"body\",\"frames\"],\"msg\":\"Input should be less than or equal to 3000\",\"input\":99999,\"ctx\":{\"le\":3000}}]}",
"req": { "...": "..." },
"partition": "text",
"created_at": 1788597597.86,
"backend": null,
"deleted_at": null
}
分辨率实测上限校验失败(832×480 传了 1200 帧,超过表里的 1125):
{
"id": "e545a8e5aa24",
"status": "failed",
"error": "{\"detail\":[{\"type\":\"value_error\",\"loc\":[\"body\"],\"msg\":\"Value error, 832x480 实测上限是 1125 帧,1200 帧会真实CUDA OOM(或448x256画质会退化,见notes.md),不接受这个请求\",...}]}",
"req": { "...": "..." },
"partition": "text"
}
error 字段是底层校验器原样透传的 JSON 字符串,不是稳定的 API 契约,只建议展示给人看/记日志,不要用程序解析它的结构去做分支逻辑;要判断"这次提交有没有被接受",只看 status !== "failed" 就够。
其它错误
| HTTP 状态 | 场景 |
|---|---|
| 401 | token 缺失/错误 |
| 409 | GPU 正在重置/硬重启窗口期(约 25 秒),{"detail": "GPU正在重置/服务重启中,还剩约N秒,请稍后重试"} |
5. 查询任务
GET /v1/videos — 列表
query 参数(全部可选):
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
limit |
int | 20 | 每页条数 |
offset |
int | 0 | 偏移量,配合 limit 做分页 |
trash |
bool | false | true 只看回收站里的任务 |
q |
string | "" | 按任务 id 或 prompt 子串搜索(LIKE %q%) |
status |
string | "" | queued/running/done/failed/cancelled 精确匹配 |
kind |
string | "" | text/fl2va/ref2va 精确匹配 |
sort |
string | desc | desc(默认,按创建时间倒序)/ asc |
响应:
{ "jobs": [ /* Job 对象数组,见第 6 节 */ ], "total": 850 }
total 是当前筛选条件下匹配的总数(不受 limit/offset 影响),用来算分页页数。
GET /v1/videos/{id} — 单条详情
会顺带触发一次状态同步:如果任务还在本地排队没提交上去,顺手再试一次提交;如果已经提交了,去底层问一次最新状态再返回。频繁轮询(比如每 2-3 秒一次)是安全的,但没必要比这更快——状态变化本来就不会快过底层真实的执行速度。
GET /v1/videos/{id}/file — 下载/播放视频
只有 status="done" 的任务能下载。原生支持 HTTP Range(浏览器 <video> 标签直接能用来拖进度条)。返回 Content-Disposition: attachment; filename="h3-{id}.mp4"。
任务还没跑完/没有 backend 时返回 409 {"detail": "任务还在本地排队,尚未分配到可用后端"}。
GET /v1/videos/{id}/comfy_history — 排查用
返回 ComfyUI 侧这条任务的原始执行记录(完整 workflow graph + 节点输出),排查生成问题时用,不建议在正常业务逻辑里依赖它的结构。
6. Job 对象字段
POST /v1/videos、GET /v1/videos/{id}、GET /v1/videos 里的每个元素,返回的都是同一种结构:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | console 任务 id,所有 /v1/videos/{id}/* 接口都用这个 |
status |
string | queued / running / done / failed / cancelled |
req |
object | 你提交时传的原始请求体(回显) |
partition |
string | text / fl2va / ref2va,见第 2 节 |
backend |
string\ | null |
created_at |
float | 提交时间,unix 时间戳(秒,带小数) |
started_at |
float\ | null |
finished_at |
float\ | null |
step / total_steps |
int | 当前采样进度(running 时有意义;total_steps 未必等于请求的 steps,初始化阶段也会打印进度) |
file |
string\ | null |
size_bytes |
int\ | null |
error |
string\ | null |
comfy_prompt_id |
string\ | null |
queue_seq |
int | 排队顺序号,越小越靠前 |
queue_position |
int | 仅列表接口返回:当前排在第几位(1-based,只有 queued 状态才有意义) |
deleted_at |
float\ | null |
poll / download |
string | 底层任务原样带回来的字段,console 已经改写成用自己的 id 拼路径,可以直接用 |
7. 任务管理
POST /v1/videos/{id}/cancel — 取消/停止
行为取决于当前状态:
queued:直接标记cancelled,从底层队列里真正撤掉,不会再消耗 GPU 去跑它。running:⚠️ 会硬重启整个 raylight 服务。真实计算跑在 Raylight 的 Ray 子进程里,收不到普通的打断信号,唯一能立刻把 GPU 收回来的办法就是重启整个后端——重启期间(约 15-20 秒)所有任务(不只是被停的这条)都无法提交/无法查询进度,会影响其它并发调用方。不是"优雅停止",请求前想清楚代价。- 已经是终态(
done/failed/cancelled):409 {"detail": "任务当前状态是 xxx,已经是终态,取消不了"}
POST /v1/videos/{id}/prioritize — 插队
只能对 status="queued" 的任务调用,把它挪到排队最前面。非 queued 状态返回 409。
响应里的 queue_position 是插队后的新位置。
DELETE /v1/videos/{id} — 软删除
移入回收站(deleted_at 设成当前时间)。不影响底层任务本身——如果这条任务还在排队/运行中,它会照常跑完,只是从 console 的主列表里消失了。真要连底层计算一起停掉,配合 cancel 一起调(顺序不影响结果)。
{"id": "xxx", "deleted": true}
对已经在回收站里的任务重复调用,或 id 不存在:404 {"detail": "无此任务,或已经在回收站里"}。
POST /v1/videos/{id}/restore — 恢复
{"id": "xxx", "deleted": false}
对不在回收站里的任务调用:404 {"detail": "无此任务,或不在回收站里"}。
POST /v1/videos/batch-cancel / POST /v1/videos/batch-delete — 批量操作
2026-09-05 新增:给脚本化批量清理用,避免大批量提交/清理时只能一个个调用单条接口。
请求体二选一:
// 方式一:显式给 id 列表,没有数量上限(调用方明确知道要动哪些)
{ "ids": ["id1", "id2", "id3"] }
// 方式二:筛选条件,跟 GET /v1/videos 的 status/kind/q 语义一致,至少给一个
{ "status": "queued", "kind": "ref2va", "q": "某个关键词" }
两种方式不能都不给——空 body 会被拒绝:
// HTTP 400
{"detail": "必须提供 ids 列表,或至少一个筛选条件(status/kind/q),避免误操作全部任务"}
筛选条件模式有安全上限:命中数超过 300 条直接拒绝,不会静默截断:
// HTTP 400
{"detail": "筛选条件命中 320 条,超过单次批量操作上限 300,请缩小筛选范围(加 status/kind/q)或改成显式 ids 列表"}
响应:逐条处理,一条失败不影响其它条,成功/失败分别列出来:
{
"requested": 3,
"succeeded": ["id1", "id3"],
"failed": [{"id": "id2", "error": "任务当前状态是 done,已经是终态,取消不了"}]
}
batch-cancel 对批量里混进的 running 任务一样会触发硬重启(见上面单条 cancel 的说明)——如果批量目标里可能包含正在运行的任务,先用筛选条件把 status 限定成 queued,避免误停正在跑的任务。
8. 上传参考素材
POST /v1/assets
multipart/form-data,字段名 file。按扩展名分流校验:
| 类型 | 允许扩展名 | 大小上限 |
|---|---|---|
| 图片 | jpg/jpeg/png/webp | 20MB(且必须是能被解码的有效图片,否则 400) |
| 视频 | mp4/mov/webm/mkv | 100MB |
| 音频 | mp3/wav/m4a/ogg/flac | 30MB |
curl -X POST http://192.168.110.67:8080/v1/assets \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/reference.png"
响应:
{"id": "31417985029a4f60", "url": "http://192.168.110.67:8080/assets/31417985029a4f60__reference.png", "kind": "image"}
拿到的 url 直接填进 first_frame_url/reference_image_urls 等字段即可。也可以跳过这一步,直接给一个服务器能访问到的公网 URL(图床等)——底层是 urllib.urlopen 直接下载,本地上传出来的 URL 和外部 URL 对它没有区别,只是不做 URL 可达性预检,下载失败会在任务执行阶段才报错。
9. 系统状态
GET /v1/backends
{
"status": "ok",
"ready": 1, "total": 1,
"queued": 174, "running": 1,
"backends": [
{"status": "ok", "model": "ready", "gpu": "0,1,2,3", "partition": "raylight-fsdp",
"offload": "fsdp", "queued": 174, "running": 1, "backend": "http://127.0.0.1:8100"}
],
"gpus": [
{"index": 0, "memory_used_mb": 5970, "memory_total_mb": 22528, "utilization_pct": 0},
{"index": 1, "memory_used_mb": 9068, "memory_total_mb": 22528, "utilization_pct": 99}
]
}
total 恒为 1(架构上就是一个集群,不是"N 个 worker 里 ready 了几个")。queued/running 是当前排队深度和是否有任务在跑,轮询这个可以判断"提交了大概要等多久"(没有精确 ETA,只能凭经验:running=1 时前面每条排队任务预计几十秒到几分钟不等,取决于分辨率/帧数/步数)。
GET /v1/models
{
"partitions": {
"fl2va": {"dit": "minimax_h3_fl2va_pruned_int8_convrot.safetensors", "desc": "首末帧条件生成"},
"ref2va": {"dit": "minimax_h3_ref2va_pruned_int8_convrot.safetensors", "desc": "文生视频 / 多参考图生成"}
},
"presets": [
{"label": "832×480,最多 1125 帧(≈46.9s@24fps)", "width": 832, "height": 480, "max_frames": 1125}
/* ...其余档位见第 4 节表格... */
]
}
presets 是实时查底层拿到的,跟第 4 节的表格应该始终一致——集成时建议用这个接口取分辨率档位,不要在自己代码里硬编码一份可能过期的表。
POST /v1/admin/reset-gpu ⚠️ 危险操作
不挂在具体任务上的兜底重置——硬重启整个 raylight 服务,回收可能因为崩溃/OOM 卡住不释放的显存。会打断当前正在运行的任务(如果有),效果和代价跟单条 cancel 停止 running 任务完全一样(15-20 秒全局不可用)。只在确认 GPU 显存异常卡住时用,不要当成常规操作调用。
10. 错误码约定
| HTTP 状态 | 含义 |
|---|---|
| 200 / 202 | 成功(POST /v1/videos 固定 202,见第 4 节警告) |
| 400 | 请求参数问题(批量接口的筛选条件缺失/命中过多,上传文件不是有效图片等) |
| 401 | token 缺失或错误 |
| 404 | 任务 id 不存在,或者已经在回收站/不在回收站(视接口而定) |
| 409 | 状态冲突(GPU 重置中;任务已是终态无法取消;非 queued 状态无法插队等) |
| 413 | 上传文件超过大小上限 |
| 502 | console 连不上底层 raylight(网络问题,重试即可) |
除 POST /v1/videos 外,其它接口的错误都会原样反映在 HTTP 状态码上,body 统一是 FastAPI 默认的 {"detail": "..."} 形状。
11. 集成注意事项(读完再接)
- 只走 console,不要直连 raylight(8100)。8100 没有鉴权、没有持久化、没有软删除,绕过 console 提交的任务 console 事后只能靠"扫描发现"机制捞回来,捞回来的记录用的是 raylight 自己的 id(跟正常提交产生的 console id 是同一种格式,肉眼无法区分),管理起来处处受限,2026-09-05 还实测触发过一次真实的重复记录 bug。
- 批量提交请加节流,别用 fire-and-forget 式的无限制并发。console 是单进程 FastAPI + sqlite,扛得住合理的并发(个位数到几十 QPS 级别的短请求),但成百上千个并发 POST 会把它的 HTTP 层和数据库拖到不可用(2026-09-05 实测:150 个并发提交导致后续 11 小时
GET /v1/backends响应时间从 0.3 秒退化到 38 秒)。建议:每条提交间隔 100-200ms,或者控制并发数在个位数。 POST /v1/videos永远 202,靠status字段判断成功/失败,见第 4 节。- 提交前用
GET /v1/models查当前支持的分辨率档位,不要硬编码——档位可能随实测结果调整。 turbo/cfg_scale/negative_prompt目前都是 no-op,传了不会报错但也不会生效,别把它们当成可以调节生成效果的旋钮。- 停止
running任务(cancel)= 硬重启整个服务,会影响所有并发调用方 15-20 秒,不是低成本操作。 - 任何时刻只有 1 条任务在真正计算,其余全部排队——设计并发预期时按"单机吞吐"算,不要假设 4 张卡能并行跑 4 条不同任务。