异步视频生成 API:任务 ID、轮询与按秒计费

视频生成是任务队列而不是请求/响应调用,并按视频秒数计费。本文给出完整契约、轮询规则,以及决定你的队列是否会重复付费的三种失败情况。

发布 2026-09-12 · 更新 2026-09-29 · KeepRouter Editorial · 7 分钟阅读

视频异步任务流程:提交调用进入任务画布并返回任务 ID,虚线轮询回路把结果带回
一次提交创建任务,渲染异步完成,再通过轮询取回结果。

先给结论:视频生成不适合用一次 HTTP 请求/响应来完成。一次渲染要几分钟,所以接口本质是任务队列:提交任务、拿到任务 ID、轮询直到视频就绪。KeepRouter 在 POST /v1/video/generations 暴露这套流程,并按视频秒数而不是 token 计费。文本转语音的音频输出也按时长计费,但文本输入费用需要另外计算。

这会直接影响你写的代码,而且多数问题不是在第一次测试调用时出现,而是在那之后。

为什么同步调用行不通

一段 5 秒的 1080p 视频可能占用 GPU 一分钟以上。如果提交接口一直挂着连接,客户端就需要远超常规请求预算的超时,而你与模型之间的任何代理最终都会把一次健康的渲染从中间切断。拆成「提交 + 轮询」能让每次 HTTP 调用都很短,并让进度可观测。

代价是状态。任务 ID 是指向仍在进行的工作的句柄,因此它必须能跨进程重启、跨重新部署,也要能应对客户端内存里的映射丢失。把任务 ID 与你应用的「工作单元」(任务行、订单、草稿)存在一起,把轮询当作可恢复的一步,而不是活在某个函数调用里的循环。

完整契约

提交渲染。响应里包含任务 ID 和初始状态,继续下去不需要别的东西:

curl https://keeprouter.com/v1/video/generations \
  -H "Authorization: Bearer $KEEPROUTER_KEY" -H "Content-Type: application/json" \
  -d '{"model":"wan2.7-t2v","prompt":"a paper plane gliding over a sunlit desk","duration":5}'
{
  "id": "task_QeWgyFbrwjInjPMERqtdyFvehlMM4qgX",
  "task_id": "task_QeWgyFbrwjInjPMERqtdyFvehlMM4qgX",
  "object": "video",
  "status": "queued",
  "progress": 0,
  "created_at": 1789197093,
  "completed_at": null,
  "url": null,
  "error": null
}

然后轮询。GET /v1/videos/{id} 与 GET /v1/video/generations/{id} 是同一个操作,因为不同客户端沿用了不同约定;选一个并固定下来即可。status 会从 queued、in_progress 走到 completed、failed 或 canceled,progress 在上游提供时是百分比。url 只在视频存在后出现,而且是会过期的签名链接,第一次看到时就把它转存到自己的存储,不要把上游链接直接交给终端用户。

轮询是读取操作:和所有调用一样需要鉴权,但不计费,也不占你的模型预算。5 秒一次是合理默认值;更密的循环基本只是在增加请求数。

你完全可以不轮询。 提交时带上 webhook_url,任务进入终态后网关会 POST 一条签名事件,字段与轮询返回一致。投递使用时间戳与原始请求体的 HMAC 签名,因此你可以先验证再信任;接收端不可用时按固定计划重试(约 1 分钟、15 分钟、1 小时、6 小时)。只接受公网 https 地址:内网、回环与云元数据地址会在任务提交前就被拒绝,重定向也不会被跟随。

无论如何都保留轮询作为兜底。webhook 可能丢失,而轮询响应会报告投递状态(webhook.status、attempts、last_error),因此坏掉的目标地址是可见的而不是静默的。每次轮询还会返回 expires_at,即网关停止跟踪该任务、签名链接同时失效的时刻,客户端据此即可判断最晚何时必须取走文件,不必自己推算窗口。

计费数量就是视频秒数

视频输出按视频秒数计量;文本转语音的音频输出按生成音频秒数计量,另加目录中公布的文本输入费用。不能把这两类按时长定价的输出费率当作每百万文本 token 价格。模型页会标出输出单位(per second of video、per second of audio),机器可读价格表也提供明确的 output_price_unit 列。

当上游对更高分辨率收费更高时,KeepRouter 会按档位分别发布模型 ID:wan2.7-t2v 是 720p 档,wan2.7-t2v-1080p 是 1080p 档,各有自己的费率。网关会钉住它计费的那个档位,因此请求不可能被渲染成高于你付费档位的规格,如果你请求了该 ID 未定价的分辨率,会以 resolution_not_priced 被拒绝,而不是被静默升档或降档。省略 duration 时会钉一个默认值并据此计费,所以你拿到的片段就是你付费的那个片段。

最后一点最值得记住:金额在渲染开始前就已确定,因为上游在提交时就按你请求的时长收费。因此你的成本核算可以在提交那一刻就精确,而不必事后对账。

不重复扣费的失败处理

有三种情况需要不同处理,把它们混为一谈正是队列重复付费的常见原因。

提交调用超时。 你并不知道任务是否已创建。不要盲目重试:重试会创建第二次渲染并产生第二笔费用。正确做法是在自己这一层把提交做成幂等,一行任务记录对应一次提交;如果确实必须在超时后重试,就把「可能多出一个任务」当成这次决定的代价,而不是默认路径。

任务失败。 上游在接受任务时就收费,之后判定失败也不退款,网关如实映射这一行为而不是自行吸收。轮询会明确告诉你发生了什么:status 变为 failed,error.message 带上上游自己的原因,例如时长超出该模型允许的范围。先读它,再判断是不是网关把任务弄丢了。

任务 ID 不再可解析。 任务只能被提交它的账户读取,其他账户会得到与「不存在」相同的 404,这是刻意的,因为轮询响应里带有签名下载地址。网关还会在 24 小时后停止跟踪(与签名链接有效期一致),此后轮询返回 task_expired;更早的每次轮询响应里,expires_at 就精确标出了这个时刻。所以要存的是视频,不是任务 ID。

用任务 ID 设计等待界面

让一个应用任务始终关联返回的任务 ID。浏览器重载后恢复查询该 ID,不要重新提交渲染。排队、运行、完成与失败应显示为不同状态,等待时间不能证明进度。KeepRouter API 参考定义了提交与状态操作。结果就绪后,按返回地址的有效期保存,不把临时下载地址当成永久存储。

上线前该确认什么

用你生产环境真正要用的时长和分辨率跑一次渲染,确认四件事:任务能到 completed;视频能从返回的链接下载下来;费用等于「时长 × 该档位公布的每秒单价」;以及一个故意写错的请求会在任何上游调用发生之前就被拒绝。最后一项决定了你的校验是否足够早,能不能保护账户。

模型目录 是当前可调用的视频 ID 与每秒费率来源,快速开始 覆盖账户与 Key 的基础配置。如果你正在按这类端点比较网关,多模态模型 API 说明 解释了为什么端点是按模型分别发布的,而不是被压成一个万能请求;错误参考 记录了上面提到的每个错误码。

常见问题

一次视频渲染要多久?

短片段通常一分钟内完成,更长或更高分辨率的渲染需要更久。由于提交调用会立即返回,渲染时长不影响你的请求超时;每隔几秒轮询一次,并把任务 ID 持久化,避免重启后丢失。

任务失败还会收费吗?

会。上游在接受任务时即收费,之后判定失败也不退款,网关如实映射而不是自行吸收差额。重试前请先轮询任务并读取 error.message,那里有上游给出的原因。

为什么有两个轮询路径?

GET /v1/videos/{id} 与 GET /v1/video/generations/{id} 是同一个操作。现有视频 API 两种约定都有,网关两者都接受,你不必为此改写代码。

参考的一手资料

本文最近复核 2026-09-29

  1. [1] Google Cloud text-to-speech pricing (audio is billed per token, with a documented tokens-per-second rate)
  2. [2] Google Gemini API pricing (duration-metered video and audio models)
  3. [3] OpenAI API pricing (audio token rates for speech models)
  4. [4] KeepRouter API reference

继续阅读

← 全部文章 · 模型与价格 · 获取 API Key