# 火山方舟 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` 兜住。**如实登记,不假装已解决。** **两点减轻**(非消除): 1. 提交是异步的,只入队即返回,**耗时短、超时概率低**——与同步长连接型供应商 的风险量级不同。 2. 厂商支持 `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/03` R13 把「缩略图」列为视频进 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. 查询返回体的关键字段 ```json { "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 按任务行冻结的快照查不到适配器,任务会永远停在排队——而那看起来 像队列坏了,不像两个字符串不一致。