- 新增「账号实际开通的模型」一节(2026-09-20 确认):两个 model ID 的关键限制 对照表,以及为何限制差异不进目录表而放在 patbond-ai 的 capabilities.py (正典 generation_models 无能力列,不发明超出已评审模型的列)。 记下两处刻意设计:按模型族前缀匹配使小版本升级不必改表;未知模型一律放行, 使「开通即可用」不退化为「开通还得改代码发版」。 - 说明风格表与图片模型刻意未播种的原因。 - 实现状态表按重构后的分层重写,并记下一处**跨语言约定**:目录表的 provider_code 必须与适配器的 PROVIDER_CODE 逐字一致,两侧均有测试钉住。 - **修过期路径**:文档头与 §4 还指向重构前已删除的 app/providers/seedance.py 与 tests/test_errors.py。这正是本项目反复被咬的文档漂移,趁热修掉。 验证:mkdocs build --strict exit 0;secret-scan exit 0。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
火山方舟 Seedance 视频生成:接口事实与映射
取证日期:2026-09-20 来源:用户提供的三份官方文档(
doc_temp/火山视频请求.md、doc_temp/火山错误码.md、doc_temp/火山查询.md) 实现位置:patbond-ai的app/providers/volcengine/(client.py传输 /errors.py错误映射 /models.py厂商 DTO /video.py视频业务 /capabilities.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/volcengine/errors.py,测试在
tests/providers/volcengine/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. 模型与能力矩阵
| 模型 | 文生视频 | 图生视频-首帧 | 首尾帧 | 全模态参考 | 有声 |
|---|---|---|---|---|---|
| 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。
账号实际开通的模型(2026-09-20 确认)
| model ID | 目录 code | 关键限制 |
|---|---|---|
doubao-seedance-2-5-260628 |
seedance-2-5 |
时长 [4,30];分辨率 480p/720p/1080p(无 4k);图生视频时 ratio 只支持 adaptive;支持 output_format mp4/mov;1080p 为 10bit + H.265/HEVC |
doubao-seedance-2-0-260128 |
seedance-2-0 |
时长 [4,15];分辨率 480p/720p/1080p/4k;ratio 可指定;不支持 output_format;4k 为 10bit + H.265/HEVC |
已由 V7 目录种子播种(provider_code = 'volcengine-seedance')。注意与目标
模型 patbond_postgresql.sql:1495 的种子有意不同——正典那份是服务商未定时
写下的 provider_code='fixture' 占位。
两个模型的限制差异不进目录表(正典的 generation_models 没有能力列,本迭代
不发明超出已评审模型的列),而是由 patbond-ai 的
app/providers/volcengine/capabilities.py 承载并在提交前本地预校验。这样
超范围的请求不必花一次往返去换一个厂商 400——而那一次往返还会计入 RPM 并表现为
一个失败的任务。按模型族前缀匹配而非完整 ID,故一次小版本升级(如
2-5-261231)不需要改表;未知模型一律放行,使「账号开通即可用」不退化为
「开通还得改代码发版」。
同一组参数在两个模型上一个合法一个不合法(如 20 秒时长、4k 分辨率),这正是 需要这张表的理由,测试里两个方向都钉住了。
风格表(generation_styles)刻意未播种:风格是产品内容决策(提供哪些艺术
风格、叫什么、配图是什么),无法从服务商文档推导;正典那 4 条风格均为 image
类且其 preview_asset_id 指向 demo 资产,而 V1..V7 链路不含该 demo 种子,照抄
会违反外键。generation_jobs.style_id 可空,故空风格表不阻塞任何功能。
图片模型同样未播种:gpt-image-2 的适配层尚未落地,播种一个没有适配器的模型
会让客户端渲染出一个点了没反应的入口(目录端点按 media_kind 聚合可用模型,
故当前只返回 video)。
6. 实现状态
patbond-ai 已按高内聚低耦合分层(config → assembly → providers → domain),
分层约束由 tests/test_layering.py 用 AST 自动守着。以后接火山的图片/语音接口
应新建兄弟模块复用同一个 ArkClient,而不是再抄一遍鉴权与错误解包。
| 项 | 状态 |
|---|---|
错误分类(providers/volcengine/errors.py) |
✅ 32 测试 |
HTTP 传输与鉴权(providers/volcengine/client.py) |
✅ 15 测试,全部离线(MockTransport) |
视频业务(providers/volcengine/video.py) |
✅ 提交与查询 + 状态映射 |
模型限制预校验(providers/volcengine/capabilities.py) |
✅ 23 测试 |
| V7 目录种子(两个已开通模型) | ✅ 6 测试(含 provider_code 跨语言一致性) |
| 产物转存 | ⏸ 阻塞于存储决策(ADR-023 §7)。注意 §2-④ 的 24 小时硬约束 |
HTTP 端点与 openapi-internal.yaml |
⏸ 未开始 |
| Java 侧 provider 适配 | ⏸ 待上一项落地 |
一处跨语言约定,两侧都有测试钉住:目录表的 provider_code 必须与
patbond-ai 适配器的 PROVIDER_CODE 逐字一致(volcengine-seedance)。
对不上时 Worker 按任务行冻结的快照查不到适配器,任务会永远停在排队——而那看起来
像队列坏了,不像两个字符串不一致。