视频生成模型 API 对比:实测每秒成本、首帧延迟与失败码
视频生成模型 API 对比:实测每秒成本、首帧延迟与失败码
一、营销页上看不到的四个指标缺口
规格上限不等于实际产出。 文档写“最高支持 1080p/30fps”,这是模型能力上限。同一个模型,请求参数里 resolution 填 720p、时长 5s,产出可能就是 1280x720/24fps。核验方式是拿实际文件跑 ffprobe:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate \
-show_entries format=duration -of json out.mp4
逐条对齐请求参数与输出,别把上限当默认值。
计费单位不可比。 可灵按 credit、Runway 按 credit/秒、部分国产接口按次,充值档位还分折扣。统一换算成“元 / 输出秒”:单次调用实付金额 ÷ format=duration 给出的秒数。按次计费的接口在不同时长下单价差别很大,要按你实际使用的时长算。
延迟口径要拆开。 创建接口返回 200 只代表 task_id 已入队,不等于可播放。分开记录 t_create → 首个可播放 URL(首帧延迟),以及 t_create → 文件下载完成(总延迟)。两个值混在一起看,就无法判断瓶颈在排队、生成还是 CDN。
失败码容易被吞。 返回体里的 code 区分 content_policy(审核拦截)、rate_limit(并发超限)、internal_error(生成中途失败)。审核拦截要改 prompt 或输入图,限流要退避重试,内部错误重试未必有效,还要确认是否照常计费。
二、固定测试集与 curl 调用模板
固定 6 条 prompt,只改 prompt 文本,其余参数一致,用来区分不同失败模式:
| prompt_id | 内容要点 | 期望暴露的问题 |
|---|---|---|
| p1 | 单主体匀速直线运动 | 基础可用性与首帧延迟 |
| p2 | 快速镜头推拉 | 运动模糊、镜头控制 |
| p3 | 画面内出现清晰文字 | 文字渲染准确度 |
| p4 | 双人交互并有遮挡 | 人物一致性与遮挡处理 |
| p5 | 一秒内多次场景切换 | 时序一致性与跳帧 |
| p6 | 10 秒长时长输出 | 长时长稳定性与超时 |
每条统一 aspect_ratio=16:9、duration=5、seed=42,p6 按接口支持的最长时长提交,超出上限按失败记录。
创建请求用同一个模板,-D headers.txt 保留 HTTP 状态码与 x-request-id,便于按请求追溯:
curl -sS -D headers.txt -o resp.json -X POST "$BASE/v1/videos" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"'"$MODEL"'","prompt":"'"$PROMPT"'","aspect_ratio":"16:9","duration":5,"seed":42}'
轮询每 2 秒一次,上限 300 秒,超时计入失败:
for i in $(seq 1 150); do
curl -sS "$BASE/v1/videos/$TASK_ID" -H "Authorization: Bearer $API_KEY" -o status.json
grep -q '"status":"succeeded"' status.json && break
sleep 2
done
一次测试要留三个时间戳:t_create(发出创建请求)、t_status_succeeded(首次轮询到成功)、t_download_done(文件写盘完成)。首帧延迟按 t_status_succeeded - t_create 算,端到端按 t_download_done - t_create 算。
落盘命名固定为 {vendor}_{model}_{prompt_id}_{timestamp}.mp4,同名 .json 写请求体、响应体、三个时间戳和 HTTP 状态码。汇总时直接遍历 JSON,不用回头翻终端日志。
三、用 ffprobe 校验真实产出,而不是相信文档
文档写的分辨率和帧率不一定是真实产出。拿到 out.mp4 先探针:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate,avg_frame_rate,nb_frames,duration,bit_rate,codec_name \
-of json out.mp4
把 width、height、duration 与请求参数逐条比对。r_frame_rate、avg_frame_rate 是分数,常见 30000/1001、24000/1001,脚本要按 / 切分做除法转成小数(≈29.97、≈23.98),直接与 30、24 做字符串比对会把正常结果判成不一致:
ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 out.mp4 \
| awk -F/ '{printf "%.3f\n", $1/$2}'
抽帧检查画面是否有效:
ffmpeg -ss 0 -i out.mp4 -frames:v 1 first.png
ffmpeg -ss 2 -i out.mp4 -frames:v 1 mid.png
任务返回成功但首帧纯黑、静帧或与 prompt 无关,只能靠看图发现。时长偏差要核账:实测 duration 与请求 duration 相差超过 ±0.5s,用 ffprobe 实测秒数重算成本,按秒计费的平台通常按实际输出计费。
四、指标口径定义与成本、延迟、失败码对比表
每秒成本 = 单次调用实付金额 ÷ ffprobe 实测 duration。credit 制按最近一次充值档位折成人民币;按次计费的模型也照样除以实测秒数,避免包月折算和按秒计费混在一起比较。
成功样本要同时满足三条:HTTP 200、响应体里 status=succeeded、ffprobe 能解析出视频流。提交成功但轮询超时、返回空 URL、文件下不下来,一律记失败。分辨率、帧率、时长用同一条命令取:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,avg_frame_rate:format=duration \
-of default=nw=1 out.mp4
avg_frame_rate 是分数,帧率 = 分子 ÷ 分母。
对比表每个 vendor/model 组合一行,各列取同批样本的实测值:
| vendor | model | 实测分辨率 | 实测帧率 | 实测时长(s) | 每秒成本(元) | P50 首帧延迟(s) | P95 总延迟(s) | 成功率 | 主要失败码 |
|---|---|---|---|---|---|---|---|---|---|
| — | — | — | — | — | — | — | — | — | — |
延迟口径:首帧延迟 = 从发送创建请求到轮询首次拿到可播放地址;总延迟 = 到 ffprobe 下载并解析成功。同一模型至少跑 6 个 prompt × 3 次 = 18 次调用,同区域、同网络、同一时段完成。只跑一次的延迟数据没有参考价值,P50 和 P95 才能用于比较。
五、按 HTTP 状态码与业务 code 分类排查
视频生成接口的报错排查,先看 HTTP 状态码,再看响应体里的业务 code。
- 401 / 403:key 失效、项目未开通该模型、model 字符串写错。有些平台 model 不存在时返回 403 而非 404,所以先用只读的模型列表接口核对,别靠猜。
- 400 + content_policy / sensitive:prompt 被审核拦截。保留响应体的
request_id和原始 prompt,改写后重试,不要原样重试同一条。 - 429:并发或配额超限。读
Retry-After与x-ratelimit-remaining,按 1s / 2s / 4s 指数退避;统计时把首次 429 单独计数,不计入生成失败。 - 5xx 与轮询超时:区分创建失败(响应无
task_id)和生成中途失败(有task_id但status=failed)。后者记下task_id作为向平台侧排查的证据。
# 创建失败:响应里取不到 task_id
curl -sS -X POST "$BASE/v1/video/generations" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"MODEL","prompt":"PROMPT"}' | jq '{code,message,request_id,task_id}'
flowchart LR
A["POST 创建任务"] --> B["轮询 status"]
B --> C["下载视频落盘"]
A -.-> F1["失败点1 创建失败 无 task_id"]
A -.-> F2["失败点2 审核拦截 400 content_policy"]
B -.-> F3["失败点3 中途失败 有 task_id status=failed"]
指数退避要有上限和总时限,否则长任务容易在 429 上反复重试、把配额更快耗尽。只读列表接口能确认 model 名时,优先用它替代试错式创建请求。
六、复测清单与适用边界
最小复测清单:
- prompt 固定 6 条,覆盖不同时长与运动幅度,例如 5s 静态人像、5s 快速运动、10s 镜头推进等。
- 每个模型每条 prompt 跑 3 次,共 18 次调用,写入独立
.json。 - 每次记录 5 个字段:HTTP 状态码、业务 code、首帧延迟、实测 duration、每秒成本。
版本漂移容易被忽略。部分平台不公告模型小版本更新,用响应体里的 model 字段或 x-model-version 响应头做比对;一旦发现变化,把该模型此前记录的数据标记为失效,重跑再入表。
适用边界:本方法只量化分辨率、帧率、时长、延迟、成本、失败率这些可观测指标,不评估画面美感、语义一致性和人物稳定性。主观质量需要另做人工看片或 VBench 类评测,两者不能互相替代。
汇总 18 次结果:
duckdb -c "
CREATE TABLE r AS SELECT * FROM read_json_auto('runs/*.json');
SELECT model, count(*) n,
quantile_cont(first_frame_ms, 0.5) p50,
quantile_cont(first_frame_ms, 0.95) p95
FROM r GROUP BY model;"
CSV 列名与上面 5 个字段保持一致,后续换模型或换版本可直接重跑。