Files
patbond-doc/docs/development/providers/volcengine-seedance.md
T
lixi 8cf8ba41c9
CI / docs-build (push) Successful in 1m18s
docs: Seedance 取证页补实际开通的两个模型 + 修重构后的过期路径
- 新增「账号实际开通的模型」一节(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>
2026-09-20 15:14:18 +08:00

242 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 火山方舟 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 按任务行冻结的快照查不到适配器,任务会永远停在排队——而那看起来
像队列坏了,不像两个字符串不一致。