按用户拍板的四点定型 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>
This commit is contained in:
@@ -382,21 +382,105 @@ class TestCoverImage:
|
||||
assert provider_with(handler).fetch("x").cover_url == "https://f.png"
|
||||
|
||||
|
||||
class TestPixelDimensionsAreLeftBlankOnPurpose:
|
||||
def test_no_invented_dimensions(self):
|
||||
"""火山只返回 ratio 与 resolution,不返回像素值。
|
||||
class TestPixelDimensions:
|
||||
"""厂商只返回 ratio 与 resolution,像素值由静态对照表换算。
|
||||
|
||||
换算需要一张随模型族变化的对照表;在 V7 目录种子定下模型族之前,
|
||||
留空比编一个可能错的尺寸好。
|
||||
"""
|
||||
不需要下载产物、不需要解码、不依赖存储方案——这是 widthPx/heightPx 在 AI
|
||||
视频产物上能零成本填真值的依据(ADR-023 §11)。
|
||||
"""
|
||||
|
||||
def test_derived_from_ratio_and_resolution(self):
|
||||
# 官方表:2.0 系列 720p + 16:9 = 1280×720。
|
||||
result = provider_with(
|
||||
lambda r: httpx.Response(200, json=OFFICIAL_SUCCESS_PAYLOAD)
|
||||
).fetch("x")
|
||||
assert result.width_px is None
|
||||
assert result.height_px is None
|
||||
# 但原始信息要透出,供上层或后续实现换算。
|
||||
assert (result.width_px, result.height_px) == (1280, 720)
|
||||
# 原始信息同时透出,便于排查与将来复核。
|
||||
assert (result.ratio, result.resolution) == ("16:9", "720p")
|
||||
|
||||
def test_the_two_cells_where_the_families_differ(self):
|
||||
"""2.5 与 2.0 的 24 格里只有两格不同:480p 的 16:9 与 9:16。
|
||||
|
||||
这两格若搞错,客户端会按错误比例渲染 480p 视频且看不出是错的。
|
||||
"""
|
||||
|
||||
def payload(model: str) -> dict:
|
||||
return {
|
||||
"id": "x",
|
||||
"model": model,
|
||||
"status": "succeeded",
|
||||
"resolution": "480p",
|
||||
"ratio": "16:9",
|
||||
"content": {"video_url": "https://v"},
|
||||
}
|
||||
|
||||
result_2_5 = provider_with(
|
||||
lambda r: httpx.Response(200, json=payload("doubao-seedance-2-5-260628"))
|
||||
).fetch("x")
|
||||
result_2_0 = provider_with(
|
||||
lambda r: httpx.Response(200, json=payload("doubao-seedance-2-0-260128"))
|
||||
).fetch("x")
|
||||
assert (result_2_5.width_px, result_2_5.height_px) == (854, 480)
|
||||
assert (result_2_0.width_px, result_2_0.height_px) == (864, 496)
|
||||
|
||||
def test_4k_resolves_only_on_2_0(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "x",
|
||||
"model": "doubao-seedance-2-0-260128",
|
||||
"status": "succeeded",
|
||||
"resolution": "4k",
|
||||
"ratio": "16:9",
|
||||
"content": {"video_url": "https://v"},
|
||||
},
|
||||
)
|
||||
|
||||
result = provider_with(handler).fetch("x")
|
||||
assert (result.width_px, result.height_px) == (3840, 2160)
|
||||
|
||||
def test_adaptive_ratio_yields_none_rather_than_a_guess(self):
|
||||
"""`adaptive` 时厂商自动适配首帧比例,本表无从查起。
|
||||
|
||||
留空而不猜:一个错的尺寸会让客户端按错误比例渲染,而且看不出是错的。
|
||||
"""
|
||||
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "x",
|
||||
"model": "doubao-seedance-2-5-260628",
|
||||
"status": "succeeded",
|
||||
"resolution": "720p",
|
||||
"ratio": "adaptive",
|
||||
"content": {"video_url": "https://v"},
|
||||
},
|
||||
)
|
||||
|
||||
result = provider_with(handler).fetch("x")
|
||||
assert (result.width_px, result.height_px) == (None, None)
|
||||
# 但原始值仍透出,上层至少知道厂商说了什么。
|
||||
assert result.ratio == "adaptive"
|
||||
|
||||
def test_unknown_model_family_yields_none(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "x",
|
||||
"model": "some-future-model",
|
||||
"status": "succeeded",
|
||||
"resolution": "720p",
|
||||
"ratio": "16:9",
|
||||
"content": {"video_url": "https://v"},
|
||||
},
|
||||
)
|
||||
|
||||
result = provider_with(handler).fetch("x")
|
||||
assert (result.width_px, result.height_px) == (None, None)
|
||||
|
||||
|
||||
class TestPerModelLimitsAreEnforcedBeforeCallingTheVendor:
|
||||
"""超范围的请求本地就拦下,不花一次往返去换一个厂商 400。
|
||||
|
||||
Reference in New Issue
Block a user