视频生成 API 怎么用 Python 轮询?间隔、超时、一直 PENDING 与不重复扣费(2026)
视频生成接口是异步的:提交拿任务 ID,再轮询查询任务状态。本文按 cocodot 视频接口的实际行为讲轮询间隔、总截止时间、提交超时后怎么避免一条视频付两次钱、没有回调和取消时怎么设计脚本,并给出一个可断点续查的 Python 轮询器。
1. 先把接口的形状记清楚
cocodot 的视频生成接口分三件:提交 POST https://cocodot.co/api/ai/video/generations,查单个任务 GET https://cocodot.co/api/ai/video/generations/{id},查最近的任务列表 GET https://cocodot.co/api/ai/video/generations。三个都用 Authorization: Bearer cocodot-… 的 API Key,也接受登录 token。提交成功返回任务的 id、status、model、createdAt,此时 status 是 PENDING。查单个任务时,PENDING 表示还在生成;SUCCEEDED 时带 videoUrl;FAILED 时带 error,是一句中文原因。任务列表返回你账户最近 50 条任务,新的在前,每条有 id、model、status、提示词前 300 字(用 content 传文本提示词时才有)、resolution、videoUrl、createdAt 和这条实际收了多少钱的 costUsd。可用的型号和每个型号能选的分辨率、时长、档位,以 GET https://cocodot.co/api/ai/video/models 返回为准,在你的代码里先按它校验参数,不合法的请求根本不要发出去。调用前须完成实名 L1,视频不能使用 $0.5 体验额度。三家请求体字段各不一样,写法和每类报错怎么处理见 /hub/video-api-error-codes-fix,本文只讲轮询这一段怎么写才稳。
2. 轮询间隔:10 秒内重复查拿不到新东西
很多人写轮询的第一反应是「查得越勤,拿到视频越快」。在 cocodot 上不是这样:按当前接口实现,同一个任务距离上一次实际查询不到 10 秒时,接口不会去取新状态,直接返回 PENDING。所以 1 秒一次的轮询和 10 秒一次的轮询,拿到 SUCCEEDED 的时间几乎一样,前者只是白白多发了很多请求,还更容易被你自己的网络抖动打断。建议:提交后先等 15 秒再查第一次,之后固定每 10–15 秒查一次;如果你一次跑很多条,可以把间隔放宽到 20–30 秒,对出片时间影响很小。另外,后台也会每隔几分钟自动查一遍所有还在生成中的任务,所以就算你的脚本暂时没在查,任务的状态也会继续往前走。生成要多久取决于型号、分辨率和时长,我们不给固定数字,自己跑几条记下实际用时,再据此设截止时间。
3. 一直 PENDING:先分清「还在生成」和「暂时查不到」
PENDING 不等于「一定在跑」。按当前实现,有几种情况都会让你看到 PENDING:任务确实还在生成;离上一次实际查询不到 10 秒;服务端这一次去查结果时出了临时问题,接口不报错,只返回 PENDING;提交时结果未知,任务还在等确认。所以轮询脚本不能只靠「等到不是 PENDING 为止」,必须自己设一个总截止时间,按你的业务能等多久来定,比如 30 分钟或 1 小时。到点还是 PENDING 时,不要重新提交 —— 重投等于再建一条新任务、再预扣一次钱。正确做法是把任务 ID 记下来,脚本退出,过一段时间再用单任务查询或任务列表补查。服务端也有兜底:提交结果未知、2 小时内没能确认受理的任务,会自动转成 FAILED 并把预扣全额退回;已经受理但 72 小时还没出结果的,同样自动转 FAILED、全额退回。两种情况 error 都是「视频生成失败,本次未扣费,请稍后重试;如多次失败,请检查参数或内容。」看到这句再决定要不要重投。
4. 提交超时:先查列表,别急着重投
这是轮询脚本里最容易多花钱的地方。cocodot 收到提交后,会先建好任务、按保守预估把钱预扣下来,再去提交生成;同一账户的提交是一条一条处理的,单次提交可能要等比较久。如果你在 requests.post 里把 timeout 设成 10 秒,很可能你这边已经报超时,服务端那条任务其实已经建好、钱也预扣了。这时候脚本自动重试,就是第二条任务、第二次扣费。建议三件事:第一,提交的 timeout 给足,120 秒左右;第二,提交超时或断线时不自动重试,而是调 GET https://cocodot.co/api/ai/video/generations 看最近的任务,按 model、createdAt 和提示词前缀找有没有刚建起来的那一条,找到了就接着轮询它;第三,多线程同时提交并不会更快,因为同一账户的提交本来就是排队处理的,反而让超时更难判断。还有一种情况:提交时如果生成服务那边的结果未知,接口仍然会返回任务 ID 和 PENDING,这条任务照常轮询即可,2 小时内没能确认的会按上一节说的自动退回。
5. 没有回调、没有取消:脚本停了,钱照样结算
有的视频平台支持在提交时填一个回调地址,出片后主动通知你。cocodot 的视频接口目前不做回调:请求体里带 callback_url、webhook 之类的字段不会报错,但会被直接忽略,不会有任何通知,所以只能轮询。也没有取消任务的接口。这一点对写脚本的人很重要:你按 Ctrl+C 停掉脚本、关掉服务器、或者轮询超时退出,都只是你这边不再查了,任务本身还在生成,出片后后台会照常结算,按实际用量多退少补,视频会出现在任务列表里,costUsd 就是这条实际收的钱。反过来说,如果你发现参数填错了,停脚本不能止损,只能等这条结束。所以批量跑之前,先用一两条低规格的请求把参数、提示词和下载流程走通,再放大规模。任务失败的不扣费,预扣全额退回。
6. 批量出片:同时最多 3 条,在自己代码里排队
同一账户同时在生成中的任务最多 3 条,第 4 条提交会直接返回 400「同时最多 3 条视频在生成,请等已提交的完成后再提交」,不建任务、不扣费。另外每次提交都会先检查余额:预扣是按这条视频的预估金额算的,正在生成的任务已经各自预扣过,余额不够时会返回「钱包余额不足」并附上这条的预估金额。批量时的写法是:把待生成的请求放进一个队列,同时最多保持 3 条在生成;每提交成功一条,立刻把任务 ID、型号、提交时间追加写进本地文件;某条结束(成功或失败)再从队列里取下一条。不要用「提交失败就 sleep 一下再提交」来硬顶并发上限,那样一旦遇到上一节说的超时,很难分清哪条真的没建起来。本地记录文件就是你的账本:脚本重启时先读它,再和任务列表对一遍,只补查、不重投。
7. 拿到 SUCCEEDED 之后:下载、转存、对账
SUCCEEDED 的返回里有 videoUrl,它指向生成结果的下载地址,不在你自己的存储里,能保留多久我们不做承诺,拿到后应尽快下载到你自己的存储里。下载建议用流式写文件,不要一次读进内存;下载失败时先重新 GET 一次任务再下:按当前实现,海螺型号的成片是带签名的临时链接,对已成功的海螺任务再查一次,接口会在需要时重新获取链接;其它型号再查一次返回的是同一条链接,如果已经打不开,就记下任务 ID 联系客服。对账方面,提交时预扣的是保守预估金额,出片后按实际用量结算、多退少补;每条最终收了多少钱,以任务列表里的 costUsd 为准,不要拿提交时报错里的预估金额当实际成本。建议把任务 ID、型号、规格、costUsd、本地文件路径写进同一条记录,以后算单条成本或者找客服核对都用得上。各型号的计费方式和价格见 /hub/ai-video-api-price-compare 和价格页。
8. 完整脚本:可断点续查的 Python 轮询器
下面这个脚本把前面几条都落实了:提交超时给足、成功后立刻记本地文件;查询间隔 15 秒、有总截止时间;网络抖动时接着查同一个任务,绝不重新提交;下载前重新取一次链接;脚本重启或提交超时后用 resume() 和任务列表对账,只查不重投。它只依赖 requests,把 Bearer 后面换成你自己的 cocodot API Key 即可。用法:提交拿到任务 ID 后调用 wait(),成功再调用 download();出了任何意外,先跑 resume() 看清楚服务端的状态,再决定下一步。提交被拒(400 / 401 / 403 / 429 / 503)和任务 FAILED 时具体该改什么、哪些值得重试,按 /hub/video-api-error-codes-fix 的对照表处理,本脚本示范记录已知任务 ID 和避免自动重投,不能保证恢复返回 ID 之前中断的请求。任务列表只显示最近 50 条,看不到匹配项也不能证明未创建;无法确认时保留未知状态并联系客服。
import json, time, pathlib, requests
API = "https://cocodot.co/api/ai/video/generations"
H = {"Authorization": "Bearer cocodot-..."}
LOG = pathlib.Path("video_tasks.jsonl") # 每提交成功一条就追加一行,脚本断了也能续查
def submit(body):
# 同一账户的提交逐条处理,单次可能要等较久:超时给足,超时后不要自动重投
r = requests.post(API, json=body, headers=H, timeout=120)
if not r.ok:
raise RuntimeError(f"{r.status_code} {r.text}") # 各类报错的处理见 /hub/video-api-error-codes-fix
tid = r.json()["id"]
with LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps({"id": tid, "model": body["model"], "at": time.time()}, ensure_ascii=False) + "\n")
return tid
def wait(tid, interval=15, deadline_s=3600):
end = time.time() + deadline_s
while time.time() < end:
time.sleep(interval) # 同一任务 10 秒内重复查不会拿到新状态
try:
r = requests.get(f"{API}/{tid}", headers=H, timeout=90)
r.raise_for_status()
t = r.json()
except requests.RequestException:
continue # 网络抖动:接着查同一个任务,绝不重新提交
if t["status"] == "SUCCEEDED":
return t
if t["status"] == "FAILED":
raise RuntimeError(f"{tid} FAILED: {t.get('error')}")
raise TimeoutError(f"{tid} 到截止时间仍是 PENDING:稍后用 resume() 补查,不要重投")
def download(tid, path):
for _ in range(2):
response = requests.get(f"{API}/{tid}", headers=H, timeout=90)
response.raise_for_status()
t = response.json() # 每次下载前重新取链接
r = requests.get(t["videoUrl"], stream=True, timeout=300)
if r.ok:
with open(path, "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
return path
time.sleep(5)
raise RuntimeError(f"{tid} 下载失败,记下任务 ID 稍后再试")
def resume():
"""脚本重启或提交超时后:先对账,只查不重投。"""
response = requests.get(API, headers=H, timeout=60)
response.raise_for_status()
recent = response.json() # 最近 50 条,并非完整历史
for x in recent:
if x["status"] == "PENDING":
print("仍在生成:", x["id"], x["model"], x["createdAt"], (x.get("prompt") or "")[:40])
known = {json.loads(l)["id"] for l in LOG.read_text(encoding="utf-8").splitlines()} if LOG.exists() else set()
for x in recent:
if x["id"] not in known:
print("本地没记、服务端有:", x["id"], x["status"], x["createdAt"])轮询脚本要处理的情况(按 cocodot 视频接口当前行为整理)
| 情况 | 接口实际行为 | 代码里怎么写 |
|---|---|---|
| 10 秒内重复查同一任务 | 不去取新状态,直接返回 PENDING | 间隔设 10–15 秒,再密没有用 |
| 查询过程中服务端暂时查不到结果 | 返回 PENDING,不报错 | 自己设总截止时间,到点停下改为补查 |
| POST 在你这边超时或断线 | 任务可能已经建好并预扣 | 先查任务列表;没看到匹配项也不能证明未创建,保留未知状态并联系客服确认后再决定 |
| 请求体里带 callback / webhook 字段 | 被忽略,不会回调 | 只用轮询 |
| 想中途取消 | 没有取消接口,停止轮询不会停止生成 | 提交前把参数核对好 |
| 已有 3 条在生成又提交第 4 条 | 返回 400,不建任务 | 用队列把同时生成数控制在 3 以内 |
| 提交结果未知,2 小时内没确认受理 | 自动转 FAILED,预扣全额退回 | 按 error 原文决定要不要重投 |
| 已受理但 72 小时没出结果 | 自动转 FAILED,预扣全额退回 | 同上 |
| 海螺成片链接过期 | 对已成功的海螺任务再 GET 一次,返回重新获取的链接 | 下载前先 GET 一次拿最新链接 |