记录从官方文档实际读到的事实,供实现与排查引用,并把开工时「待服务商文档确认 的五项」逐条落定。实现在 patbond-ai(58 测试)。 要点(详见正文): - **不支持幂等键**:ADR-023 §4 设想的「透传为厂商幂等头」对本供应商不成立, 风险窗口只能由 max_attempts 兜住。但厂商支持 callback_url,孤儿任务会通过 回调把 ID 送回——回调在此供应商上是**孤儿回收通道**而非仅延迟优化, §4「M4 只做轮询」值得据此复评。 - **产物 URL 仅 24 小时有效**(2.5 另有 100 次下载上限):厂商 URL 不能当存储 用,存成 external_url 会变死链。**这对挂起中的存储决策是硬约束**——无论选 本地还是云,都必须在 24 小时内把文件复制到我们自己的存储。 - **两处文档自身的矛盾**已登记并按宽实现:状态枚举在查询文档与回调说明里 不一致(后者多一个 expired);QuotaExceeded 一码两义(排队超限 vs 额度耗尽, 只能按 message 判别)。 - 两个免费收益:last_frame_url 可直接作视频封面(R13 把缩略图列为视频成本项, 实际免费);查询返回 ratio/resolution/fps/duration,Worker 写产物时维度与 时长在手、无需解码,widthPx/heightPx 债在 AI 产物上零成本可修。 - 取消语义:厂商只支持取消 queued 状态,已 running 的任务取消后仍计费。 - 用户提供的两份文档**文件名与内容装反了**(火山错误码.md 里是查询 API), 已按实际内容归档并注明,避免他人按文件名读错。 另:V7 目录种子仍待确认账号实际开通的 model ID——文档里的示例值不能直接当种子, 否则 provider_model_name 会指向无权访问的模型(InvalidEndpointOrModel.NotFound)。 验证:secret-scan exit 0;mkdocs build --strict exit 0(已挂导航)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
火山方舟 Seedance 视频生成:接口事实与映射
取证日期:2026-09-20 来源:用户提供的三份官方文档(
doc_temp/火山视频请求.md、doc_temp/火山错误码.md、doc_temp/火山查询.md) 实现位置:patbond-ai的app/providers/seedance.py与app/providers/errors.py
本页记录从官方文档实际读到的事实,供实现与排查引用。凡本页与代码不一致, 以代码为准并回改本页;凡本页与厂商文档不一致,以厂商文档为准并回改两处。
文档文件名与内容对不上:用户提供的
火山错误码.md里装的是查询 API 文档,火山查询.md里装的是错误码表。按文件名取用会读错,已按实际内容归档。
1. 三个端点
| 用途 | 方法与路径 |
|---|---|
| 提交任务 | POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks |
| 查询任务 | GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id} |
| 鉴权 | Authorization: Bearer $ARK_API_KEY |
提交是异步接口:响应只有 {"id": ...},状态靠查询接口拿。这与 ADR-023 §4
「Java↔Python 边界一律异步」天然吻合,无需在 patbond-ai 内部做后台任务承载。
2. 开工前问的五项,逐条答案
① 是否支持幂等键:不支持
请求参数全集为 model、content、omni_reference_task_type、resolution、
ratio、duration、generate_audio、watermark、output_format、
return_last_frame、callback_url、execution_expires_after、priority、
safety_identifier。没有任何幂等相关字段。
后果:ADR-023 §4 设想的「把我方 idempotency_key 透传为厂商幂等头」这条
缓解手段对本供应商不成立。「提交超时后不知道有没有扣费」的窗口只能由
max_attempts 兜住。如实登记,不假装已解决。
两点减轻(非消除):
- 提交是异步的,只入队即返回,耗时短、超时概率低——与同步长连接型供应商 的风险量级不同。
- 厂商支持
callback_url,任务状态变化时 POST 推送(请求体与查询接口返回体 一致)。一个「提交超时但实际已创建」的孤儿任务会通过回调把 ID 送回来, 因此回调在此供应商上不只是延迟优化,还是孤儿任务回收通道。这是 ADR-023 §4「M4 只做轮询、回调仅预留字段」之外的一个新论据,值得复评。
② 查询接口形态与频率限制
- 形态:
GET .../tasks/{id},返回体见 §3。 - 任务记录只保留最近 7 天(区间
[T-7天, T))。更早的任务查询不到。 - 频率限制未给具体数值,但错误码表里有多个 429 分支(见 §4),说明确实有限。
- 官方 SDK 示例用 10 秒轮询 + 30 分钟截止。我们的 3s/10s/30s 退避阶梯与之 兼容,且厂商自己的示例也用 10 秒——说明这个量级是预期用法。
③ 单任务最长时长与超时行为
execution_expires_after:默认 172800 秒(48 小时),取值范围[3600, 259200](1~72 小时),从created_at起算。- 超时任务被自动终止并标记为
expired。 - 实现取下限 3600:48 小时对交互式应用过长,被放弃的任务会在厂商侧挂两天。 注意无法设得更短——1 小时是厂商下限。
- 官方 SDK 示例的客户端截止时间是 30 分钟。ADR-023 §10 原先给视频客户端 硬超时排的是 15~20 分钟,建议按 30 分钟对齐厂商示例。
④ 产物返回形式:有效期 24 小时的 URL
content.video_url:生成视频 URL,有效期 24 小时;Seedance 2.5 另有 下载次数上限 100 次。content.last_frame_url:尾帧图(png、尺寸与视频一致、无水印),仅在提交 时return_last_frame: true才返回。
这一条对挂起中的存储决策是硬约束:厂商 URL 不能当存储用。把它存进
media.assets.external_url 会在 24 小时后变成死链,因此无论最终选本地存储
还是云存储,都必须在 24 小时内把文件复制到我们自己的存储。「直接存厂商 URL」
不是一个可选项。
两个顺带的好处:
last_frame_url可直接用作视频封面图,省掉我们自己抽帧。iteration-4/03R13 把「缩略图」列为视频进 M4 的成本项之一,这一项实际是免费的。- 查询返回
ratio、resolution、framespersecond、duration/frames, Worker 写产物时维度与时长都在手里、无需解码。这正是 ADR-023 §11 所说 「AI 输出路径不需要图片解码」的具体依据,widthPx/heightPx债在 AI 产物 上可零成本修掉。
一个精度陷阱:查询返回的 duration 是向下取整的约数——官方公式为
duration = 实际总帧数 / 24 向下取整,例如 133 帧实际 5.54 秒会返回 5。且
duration 与 frames 只会返回其中一个(提交时指定了 frames 才返回
frames)。故实现优先按 frames / framespersecond 精确换算,仅在只有
duration 时退回整数秒并明确标注为约数。
⑤ 限流与配额响应形态
见 §4 的完整映射。核心结论:两族必须分开,且 QuotaExceeded 一码两义。
3. 查询返回体的关键字段
{
"id": "cgt-2026****-****",
"model": "doubao-seedance-2-0-260128",
"status": "succeeded",
"content": { "video_url": "https://...", "last_frame_url": "https://...png" },
"usage": { "completion_tokens": 108900, "total_tokens": 108900 },
"created_at": 1779348818,
"updated_at": 1779348874,
"resolution": "720p", "ratio": "16:9",
"duration": 5, "framespersecond": 24,
"execution_expires_after": 172800,
"error": null
}
usage.completion_tokens 可作计费对账依据(视频生成不统计输入 token,故
total_tokens == completion_tokens)。Seedance 2.0 系列有最低 token 用量,
实际低于下限时按下限计费。
⚠️ 状态枚举在两份文档里不一致,必须按宽的实现
| 来源 | 列出的状态 |
|---|---|
| 查询接口文档 | queued / running / cancelled / succeeded / failed |
创建接口的 callback_url 一节 |
queued / running / succeeded / failed / expired |
只按查询文档实现,线上遇到超时任务会掉进未知分支。 实现按六个状态处理,
并把 expired 当作可重试的失败(是超时而非内容或参数问题);另注意厂商在
expired 时不一定给 error.code,故单独分类而非走通用路径。
另:cancelled 只支持取消 queued 状态的任务,且取消状态 24 小时自动删除。
这意味着我方「取消正在 running 的任务」无法向厂商传导——已 running 的任务取消
后仍会计费。ADR-023 §6 关于取消的说明据此可以定稿:取消只改我方任务记录。
4. 错误码映射
实现在 app/providers/errors.py,测试在 tests/test_errors.py(32 项)。
🔴 QuotaExceeded 一码两义
官方错误码表里有两行 code 完全相同的 QuotaExceeded:
| message | 含义 | 处置 |
|---|---|---|
The request has exceeded the quota. |
当前账号排队中任务数超限 | 等几秒,退避重试 |
...exhausted its free trial quota... / ...exceeded the 5-hour/weekly/monthly usage quota... |
额度耗尽 | 等到明天或充值,重试无用 |
按 code 一刀切必然有一半场景的文案和出口是错的。 实现只能按 message 判别 (没有别的字段可依据),判别式与两条用例都在测试里钉住。
分族
| 族 | 代表码 | 重试 | 用户侧 |
|---|---|---|---|
| 内容被拒 | *SensitiveContentDetected*(输入/输出 × 文本/图片/视频/音频 × PolicyViolation / PrivacyInformation / DeepFake 等子类)、*RiskDetection |
NEVER | 换提示词或换图 |
| 限流 | RateLimitExceeded.Endpoint{RPM,TPM,FlexTPM}Exceeded、ModelAccount{Rpm,Tpm,FlexTpm,Ipm}RateLimitExceeded、APIAccountRpmRateLimitExceeded、AccountRateLimitExceeded、ServerOverloaded、RequestBurstTooFast |
BACKOFF | 稍后重试(SnackBar) |
| 配额/计费 | SetLimitExceeded、InflightBatchsizeExceeded、AccountOverdueError、OperationDenied.Service{Overdue,NotOpen}、ModelNotOpen |
QUOTA | 去充值/开通(sheet) |
| 我方故障 | AuthenticationError、InvalidEndpointOrModel.NotFound、InvalidParameter*、MissingParameter*、InvalidParameter.TaskType{Constraint,Mismatch} |
NEVER | 通用失败 |
| 上游内部 | InternalServiceError |
BACKOFF | 稍后重试 |
两处容易搞错的地方,实现里各有测试钉住:
ContentSecurityDetectionError不属于内容被拒——它是风险识别产品自身请求 失败,不是内容真的违规,可重试。- 内容安全族用前缀匹配而非穷举:该族的码在持续增加(Input/Output × 媒体类型 × 子原因),穷举必然漏,而整族处置方式完全一致。
- 未知码一律按可退避重试处理,不判死:厂商在持续新增码,把未知码当终态失败 会让一次可恢复的抖动变成用户看到的失败;重试预算本身有上限,最坏只多试两次。
403 的欠费/未开通归入配额族:HTTP 状态码不同(403 而非 429),但用户要做的 事一样——去充值。按状态码分族会把它们错分。
5. 模型与能力矩阵(供 V7 目录种子取用)
| 模型 | 文生视频 | 图生视频-首帧 | 首尾帧 | 全模态参考 | 有声 |
|---|---|---|---|---|---|
| Seedance 2.5 | ✓ | ✓ | ✓ | ✓(图 0-30 / 视频 0-10 / 音频 0-10,可仅音频) | ✓ |
| Seedance 2.0 系列 | ✓ | ✓ | ✓ | ✓(图 0-9 / 视频 0-3 / 音频 0-3,不可仅音频) | ✓ |
| Seedance 1.5 pro | ✓ | ✓ | ✓ | — | ✓ |
| Seedance 1.0 pro | ✓ | ✓ | ✓ | — | — |
| Seedance 1.0 pro fast | ✓ | ✓ | — | — | — |
分辨率与宽高比的可选值随模型不同(2.5 无 4k,2.0 fast/mini 只到 720p;
2.5 在首帧/首尾帧、视频编辑、视频延长任务中 ratio 只支持 adaptive)。
时长范围:2.5 为 [4, 30],2.0 系列为 [4, 15],均支持 -1 智能选择。
文档出现过的 model ID 示例:doubao-seedance-2-5-260628、
doubao-seedance-2-0-260128、doubao-seedance-1-0-pro-250528、
doubao-seedance-1-0-lite-i2v-250428。
V7 种子待定项:实际可用的 model ID 取决于账号开通与推理接入点配置, 文档示例值不能直接当种子。需确认账号实际开通的模型后再写 V7,否则种子里 的
provider_model_name会指向一个不存在或无权访问的模型 (对应错误码InvalidEndpointOrModel.NotFound/ModelNotOpen)。
6. 实现状态
| 项 | 状态 |
|---|---|
错误分类(errors.py) |
✅ 32 测试 |
提交与查询客户端(seedance.py) |
✅ 26 测试,全部离线(MockTransport) |
| 产物转存 | ⏸ 阻塞于存储决策(ADR-023 §7)。注意 §2-④ 的 24 小时硬约束 |
| V7 目录种子 | ⏸ 待确认账号实际开通的 model ID |
| Java 侧 provider 适配 | ⏸ 待本服务的 HTTP 端点与 openapi-internal.yaml 落地 |