Files
patbond-ai/app/providers/volcengine/dimensions.py
T
lixi fd23dfc0b9
CI / ai-gates (push) Successful in 33s
feat(ai): 内部契约 openapi-internal.yaml + 像素对照表(127 测试)
按用户拍板的四点定型 Java↔Python 的 JSON 契约。

**① 字段命名 snake_case**(与对客户端的 camelCase 契约不同,有意为之):本服务是
Python,snake_case 使 JSON 与领域 dataclass 字段逐一对应、无需转换层;Java 侧只需
在这一个 Feign 客户端上配置命名策略。用 camelCase 反而要在两端都加转换。

**② 两类失败分开**,判别字段是 `error.kind` 而非 HTTP 状态码:
- `service`(Java↔Python):unauthenticated / invalid_request / task_not_found /
  provider_unavailable / internal_error。属我方 bug 或配置问题。
- `provider`(Python↔服务商):非 2xx + 完整分类(retry / user_facing /
  provider_code / message),字段与 Failure 一致,故 Java 侧可复用同一段处置
  逻辑,不必为「提交时被限流」和「任务失败于限流」写两套分支。
- **任务失败是第三类,走 200 + state=failed + failure**:查询一个失败的任务,
  HTTP 调用本身是成功的。做成非 2xx 会让「查不到任务」与「任务失败了」难以区分。

**③ 像素对照表已实现**——此处需更正一个理解:该表**不需要查询任何东西、也不依赖
存储方案**。它是纯静态数据(官方文档「不同宽高比对应的宽高像素值」一节),厂商
查询响应已返回 ratio 与 resolution,查表即可,无需下载产物或解码。故
widthPx/heightPx 在 AI 视频产物上可零成本填真值(ADR-023 §11)。

实现上一个关键观察:**2.5 与 2.0 的 24 格里只有两格不同**(480p 的 16:9 与
9:16),其余完全一致。故写成「共享表 + 两格覆盖」,而非抄两份 24 行——后者每次
改动要改两处,迟早只改一处。另 4k 六格仅 2.0 有。
查不到时返回 None 而不猜(ratio=adaptive、未知模型族、该模型不支持的组合三种
情形):一个错的尺寸会让客户端按错误比例渲染,且看不出是错的。
连带给 SeedanceTask 补解析响应里的 model 字段——同一适配器实例可能同时处理两个
模型族的任务,按响应里的实际模型定族才对。

**④ 路径参数用服务商任务 ID**,并在契约里写明原因:本服务不持有状态,唯一能用来
问服务商的就是这个 ID;用 job_id 就得存一份映射,「无状态」不再成立,且会多出
一个「Java 有 ID 但查不到任何东西」的中间态。你指出的那点完全对——submit 会阻塞
一次到服务商的短往返才能拿到 ID,但服务商的提交接口本身是异步的(只入队即返回),
所以这一跳很快。契约里也如实登记了那个**无法消除的窗口**:该调用超时时 Java 拿
不到 ID 而服务商可能已创建任务,无幂等键可用,只能由 max_attempts 兜住。

测试 123 → 127:旧的「像素刻意留空」断言已作废,换成断言真实换算,并覆盖两族
差异的那两格、4k 仅 2.0、adaptive 与未知模型族的留空回退。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 16:26:49 +08:00

86 lines
3.6 KiB
Python
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.
"""`resolution` × `ratio` → 像素宽高的对照表。
**这是纯静态数据,不需要查询任何东西、也不依赖存储方案。** 厂商的查询接口会返回
`ratio` 与 `resolution`(但不返回像素值),查本表即可换算——无需下载产物、无需
探测文件。数值逐格取自官方文档「不同宽高比对应的宽高像素值」一节。
有了它,`widthPx`/`heightPx` 在 AI 视频产物上就能填真实值,而不必等到把文件
下载下来解码——这也正是 ADR-023 §11 所说「AI 输出路径不需要解码」的落地方式。
**实现上的一个关键观察**:Seedance 2.5 与 2.0 系列的 24 格里**只有两格不同**
(480p 的 `16:9` 与 `9:16`),其余完全一致。故实现为「共享表 + 两格覆盖」,
而不是抄两份 24 行——后者每次改动都要改两处,迟早只改一处。
"""
from __future__ import annotations
from .capabilities import SEEDANCE_2_0, SEEDANCE_2_5, ModelLimits
#: (resolution, ratio) → (width, height)。两个模型族共用的 22 格。
_SHARED: dict[tuple[str, str], tuple[int, int]] = {
("480p", "4:3"): (752, 560),
("480p", "1:1"): (640, 640),
("480p", "3:4"): (560, 752),
("480p", "21:9"): (992, 432),
("720p", "16:9"): (1280, 720),
("720p", "4:3"): (1112, 834),
("720p", "1:1"): (960, 960),
("720p", "3:4"): (834, 1112),
("720p", "9:16"): (720, 1280),
("720p", "21:9"): (1470, 630),
("1080p", "16:9"): (1920, 1080),
("1080p", "4:3"): (1664, 1248),
("1080p", "1:1"): (1440, 1440),
("1080p", "3:4"): (1248, 1664),
("1080p", "9:16"): (1080, 1920),
("1080p", "21:9"): (2206, 946),
}
#: 2.5 与 2.0 系列**唯一**不同的两格:480p 的 16:9 与 9:16。
_OVERRIDES_2_5: dict[tuple[str, str], tuple[int, int]] = {
("480p", "16:9"): (854, 480),
("480p", "9:16"): (480, 854),
}
_OVERRIDES_2_0: dict[tuple[str, str], tuple[int, int]] = {
("480p", "16:9"): (864, 496),
("480p", "9:16"): (496, 864),
# 4k 仅 2.0 系列支持。
("4k", "16:9"): (3840, 2160),
("4k", "4:3"): (3326, 2494),
("4k", "1:1"): (2880, 2880),
("4k", "3:4"): (2494, 3326),
("4k", "9:16"): (2160, 3840),
("4k", "21:9"): (4398, 1886),
}
_BY_FAMILY: dict[int, dict[tuple[str, str], tuple[int, int]]] = {
id(SEEDANCE_2_5): {**_SHARED, **_OVERRIDES_2_5},
id(SEEDANCE_2_0): {**_SHARED, **_OVERRIDES_2_0},
}
def pixels_for(
limits: ModelLimits | None, resolution: str | None, ratio: str | None
) -> tuple[int | None, int | None]:
"""查像素宽高;查不到返回 `(None, None)`。
查不到的三种情形都**返回 None 而不猜**——一个错的尺寸比留空更糟,它会让
客户端按错误的比例渲染,而且看不出是错的:
1. **`ratio` 为 `adaptive`**:厂商在图生视频时自动适配首帧图片的比例。查询
响应是否把它解析成具体比例、抑或原样返回 `adaptive`,文档未明确说明;
原样返回时本表无从查起。
2. **未知模型族**:能力表放行了未知模型(见 `capabilities.py`),但像素值
无法凭空推断。
3. **该模型不支持的组合**:如 2.5 的 4k——本不该发生(提交前已预校验),
出现即说明厂商行为与文档不符,留空并让上层看到缺值。
"""
if limits is None or not resolution or not ratio:
return (None, None)
table = _BY_FAMILY.get(id(limits))
if table is None:
return (None, None)
found = table.get((resolution, ratio))
return found if found else (None, None)