Files
patbond-doc/docs/development/providers/volcengine-seedance.md
T
lixi 0b0317415b
CI / docs-build (push) Successful in 39s
docs: 新增火山方舟 Seedance 接口事实与错误码映射取证页
记录从官方文档实际读到的事实,供实现与排查引用,并把开工时「待服务商文档确认
的五项」逐条落定。实现在 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>
2026-09-20 11:52:01 +08:00

11 KiB
Raw Blame History

火山方舟 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. 查询返回体的关键字段

{
  "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 落地