记录从官方文档实际读到的事实,供实现与排查引用,并把开工时「待服务商文档确认 的五项」逐条落定。实现在 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>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
# 火山方舟 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` 兜住。**如实登记,不假装已解决。**
|
||||
|
||||
**两点减轻**(非消除):
|
||||
|
||||
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/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` 落地 |
|
||||
Reference in New Issue
Block a user