Files
patbond-doc/docs/development/providers/volcengine-seedance.md
T
lixi de4e02f805
CI / docs-build (push) Successful in 33s
docs: 新增 OpenAI gpt-image-2 取证页,并修火山页三处过期状态
取证页记录从两份官方文档实际读到的事实:两个端点、参数取舍、尺寸四条算术规则、
响应字段映射、错误映射、同步接口的两笔代价。

两处如实标注的不确定性:
- 尺寸上限「3840x2160」文档未说清逐边还是按像素,本页写明采用的读法与理由
  (逐边理解会禁掉所有高于 2160 的竖图,而 1024x1536 是文档明示的标准竖图)。
- 两份文档都没有错误码表,故错误映射按状态码为主推断,整节挂 ⚠️ 未经证实,
  并写明拿到真实错误数据后的校正路径。

火山页三处已过期的状态:图片适配层已落地(但产物转存仍阻塞,故仍不播种模型)、
HTTP 端点已实现、Java 侧 provider 适配已实现。

mkdocs build --strict 通过。
2026-09-21 12:10:20 +08:00

14 KiB
Raw Blame History

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

{
  "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 的适配层已落地(见 OpenAI 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 ✅ 提交/查询 + 服务身份 JWT 校验
Java 侧 provider 适配 ✅ patbond-creation 的 provider/ai/(Feign + snake_case)

一处跨语言约定,两侧都有测试钉住:目录表的 provider_code 必须与 patbond-ai 适配器的 PROVIDER_CODE 逐字一致(volcengine-seedance)。 对不上时 Worker 按任务行冻结的快照查不到适配器,任务会永远停在排队——而那看起来 像队列坏了,不像两个字符串不一致。