From 6161ecdc353fa46e348d68841887ecfd4480a544 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 3 Oct 2026 01:17:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(points)=20P0:=20=E5=88=86=E8=BE=A8=E7=8E=87?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E6=94=B9=E4=B8=BA=E7=9F=AD=E8=BE=B9=E8=A7=84?= =?UTF-8?q?=E5=88=99=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=AB=96=E5=B1=8F=E7=A7=AF?= =?UTF-8?q?=E5=88=86=E5=B0=91=E7=AE=9768%=E7=9A=84=E8=AE=A1=E8=B4=B9bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原逻辑把 p 值当 height,9:16 竖屏时返回 (405,720),实际 Seedance 720p 9:16 输出是 (720,1280)。 正确规则:p 值=短边像素数。 - 横屏16:9:短边是height,width=ceil(short*16/9)偶对齐(720p→1280×720、1080p→1920×1080、480p→854×480) - 竖屏9:16:短边是width,height=ceil(short*16/9)偶对齐(720p→720×1280、1080p→1080×1920、480p→480×854) - 方屏1:1:w=h=short - _RESOLUTION_HEIGHT 改名 _RESOLUTION_SHORT_SIDE 语义更准确 - _infer_resolution_key 改为接收(w,h)按 min(w,h) 短边推断(settle 用实际输出尺寸查价目表) - w/h max(1,...) 防御前置,避免 UnboundLocalError 修复前后价格对比: - 15s/720p/9:16:9.52 → 29.68(+211%,原少收68%) - 30s/1080p/9:16:46.34 → 146.14 - 15s/720p/16:9 与 15s/720p/9:16 同价(像素数相同,合理) - 15s/720p/1:1:16.78 新增单测: - 9组(resolution,ratio)→(w,h)精确断言(含480p ceil+偶对齐) - 所有组合偶数校验 - _infer_resolution_key 按短边推断(含竖屏720×1280→720p回归测试,防止重蹈覆辙) - 3组灵应指定的精确积分数值断言(29.68/146.14/横竖同价) 全量单测 16407 passed。 --- packages/domain/points_rules.py | 46 +++++++++----- tests/unit/test_points_rules.py | 109 +++++++++++++++++++++++++------- 2 files changed, 117 insertions(+), 38 deletions(-) diff --git a/packages/domain/points_rules.py b/packages/domain/points_rules.py index d1a53697b..2b0763563 100644 --- a/packages/domain/points_rules.py +++ b/packages/domain/points_rules.py @@ -48,23 +48,36 @@ _RESOLUTION_ALIASES: dict[str, str] = { "全能": "1080p", "fhd": "1080p", } -# 分辨率 -> 高度 -_RESOLUTION_HEIGHT: dict[str, int] = {"480p": 480, "720p": 720, "1080p": 1080} +# 分辨率 -> 短边像素数(p 值代表短边,不是 height) +_RESOLUTION_SHORT_SIDE: dict[str, int] = {"480p": 480, "720p": 720, "1080p": 1080} def resolve_video_dimensions(resolution: str, ratio: str) -> tuple[int, int]: - """把 (resolution, ratio) 解析为 (width, height)。""" + """把 (resolution, ratio) 解析为 (width, height)。 + + resolution 数字代表短边像素数(480p/720p/1080p 等): + - 横屏 16:9:短边是 height,width = short * 16/9 + - 竖屏 9:16:短边是 width,height = short * 16/9 + - 方屏 1:1:width = height = short + """ key = str(resolution or "").strip() key_l = key.lower() res_key = _RESOLUTION_ALIASES.get(key_l) or _RESOLUTION_ALIASES.get(key) or "720p" - h = _RESOLUTION_HEIGHT.get(res_key, 720) + short = _RESOLUTION_SHORT_SIDE.get(res_key, 720) r = str(ratio or "").strip().lower() if r == "16:9": - w = h * 16 // 9 + # 横屏:短边是 height,width 向上取整并对齐偶数 + w = math.ceil(short * 16 / 9) + h = short elif r == "1:1": - w = h + w, h = short, short else: - w = h * 9 // 16 + # 9:16 竖屏(默认):短边是 width,height 向上取整并对齐偶数 + w = short + h = math.ceil(short * 16 / 9) + # 对齐到偶数(视频编码要求) + w = w + (w % 2) + h = h + (h % 2) return int(w), int(h) @@ -77,11 +90,12 @@ def _match_model_prefix(model: str) -> str: return "seedance-2.5" -def _infer_resolution_key(height: int) -> str: - """从像素高度推断 resolution key。""" - if height >= 1000: +def _infer_resolution_key(width: int, height: int) -> str: + """从实际 (width, height) 用短边推断 resolution key。""" + short = min(int(width or 720), int(height or 720)) + if short >= 1000: return "1080p" - if height >= 650: + if short >= 650: return "720p" return "480p" @@ -109,16 +123,16 @@ def calculate_viral_video_credits_with_breakdown( - breakdown: dict,包含 tokens / video_cost / fixed_cost / profit_multiplier / model_price / width / height / fps 字段,便于前端展示计费明细。 """ + w = max(1, int(width or 1)) + h = max(1, int(height or 1)) + effective_fps = int(fps or VIRAL_VIDEO_FPS) + prefix = _match_model_prefix(model) - res_key = _infer_resolution_key(int(height or 720)) + res_key = _infer_resolution_key(w, h) key = (prefix, res_key, bool(has_video_input)) price = VIRAL_VIDEO_MODEL_PRICES.get(key) if price is None: price = VIRAL_VIDEO_MODEL_PRICES.get(("seedance-2.5", res_key, False), 70.0) - - w = max(1, int(width or 1)) - h = max(1, int(height or 1)) - effective_fps = int(fps or VIRAL_VIDEO_FPS) if actual_tokens is not None and actual_tokens > 0: tokens = float(actual_tokens) else: diff --git a/tests/unit/test_points_rules.py b/tests/unit/test_points_rules.py index d3b29598a..7881c26ca 100644 --- a/tests/unit/test_points_rules.py +++ b/tests/unit/test_points_rules.py @@ -151,12 +151,11 @@ class TestResolveVideoDimensions: assert (w, h) == (1920, 1080) def test_480p_16_9(self): - """480p + 16:9 → h=480, w 按 16//9 计算。""" + """480p + 16:9 → 854×480(ceil(480*16/9)=854,偶对齐)。""" from packages.domain.points_rules import resolve_video_dimensions w, h = resolve_video_dimensions("480p", "16:9") - assert h == 480 - assert w == 480 * 16 // 9 + assert (w, h) == (854, 480) def test_720p_1_1(self): """1:1 正方形 → w == h。""" @@ -202,20 +201,48 @@ class TestResolveVideoDimensions: assert w == 720 def test_empty_resolution_defaults_to_720p_9_16(self): - """空 resolution + 空 ratio → 默认 720p + 9:16。""" + """空 resolution + 空 ratio → 默认 720p + 9:16 竖屏 (720×1280)。""" from packages.domain.points_rules import resolve_video_dimensions w, h = resolve_video_dimensions("", "") - assert h == 720 - assert w == 720 * 9 // 16 + assert (w, h) == (720, 1280) def test_none_resolution_default_ratio(self): - """None resolution + None ratio → 720p + 9:16 默认。""" + """None resolution + None ratio → 720p + 9:16 竖屏默认。""" from packages.domain.points_rules import resolve_video_dimensions w, h = resolve_video_dimensions(None, None) - assert h == 720 - assert w == 720 * 9 // 16 + assert (w, h) == (720, 1280) + + def test_720p_9_16_portrait(self): + """720p + 9:16 竖屏 → 短边是 width=720,height=1280(v10实测)。""" + from packages.domain.points_rules import resolve_video_dimensions + + w, h = resolve_video_dimensions("720p", "9:16") + assert (w, h) == (720, 1280) + + def test_1080p_9_16_portrait(self): + """1080p + 9:16 竖屏 → 1080×1920。""" + from packages.domain.points_rules import resolve_video_dimensions + + w, h = resolve_video_dimensions("1080p", "9:16") + assert (w, h) == (1080, 1920) + + def test_480p_9_16_portrait(self): + """480p + 9:16 竖屏 → 480×854。""" + from packages.domain.points_rules import resolve_video_dimensions + + w, h = resolve_video_dimensions("480p", "9:16") + assert (w, h) == (480, 854) + + def test_all_dimensions_even(self): + """所有返回尺寸都应是偶数(视频编码要求)。""" + from packages.domain.points_rules import resolve_video_dimensions + + for res in ("480p", "720p", "1080p", "普清", "高清", "超清"): + for ratio in ("16:9", "9:16", "1:1"): + w, h = resolve_video_dimensions(res, ratio) + assert w % 2 == 0 and h % 2 == 0, f"{res}/{ratio} -> ({w},{h}) not even" def test_whitespace_resolution_case_insensitive(self): """前后空格 + 大写应被规范化处理。""" @@ -264,28 +291,36 @@ class TestMatchModelPrefix: class TestInferResolutionKey: - """_infer_resolution_key(): 1000+/650-999/<650 三个分支。""" + """_infer_resolution_key(w, h): 按短边 1000+/650-999/<650 三档。""" - def test_height_ge_1000_is_1080p(self): + def test_short_side_ge_1000_is_1080p(self): from packages.domain.points_rules import _infer_resolution_key - assert _infer_resolution_key(1000) == "1080p" - assert _infer_resolution_key(1080) == "1080p" - assert _infer_resolution_key(2160) == "1080p" + assert _infer_resolution_key(1920, 1080) == "1080p" # 横屏 + assert _infer_resolution_key(1080, 1920) == "1080p" # 竖屏 + assert _infer_resolution_key(1080, 1080) == "1080p" # 方屏 - def test_height_650_to_999_is_720p(self): + def test_short_side_650_to_999_is_720p(self): from packages.domain.points_rules import _infer_resolution_key - assert _infer_resolution_key(650) == "720p" - assert _infer_resolution_key(720) == "720p" - assert _infer_resolution_key(999) == "720p" + assert _infer_resolution_key(1280, 720) == "720p" + assert _infer_resolution_key(720, 1280) == "720p" + assert _infer_resolution_key(720, 720) == "720p" - def test_height_lt_650_is_480p(self): + def test_short_side_lt_650_is_480p(self): from packages.domain.points_rules import _infer_resolution_key - assert _infer_resolution_key(480) == "480p" - assert _infer_resolution_key(649) == "480p" - assert _infer_resolution_key(0) == "480p" + assert _infer_resolution_key(854, 480) == "480p" + assert _infer_resolution_key(480, 854) == "480p" + assert _infer_resolution_key(480, 480) == "480p" + # 极小值兜底 + assert _infer_resolution_key(1, 1) == "480p" + + def test_portrait_1280_height_is_720p_short_side(self): + """竖屏 720×1280 短边=720,应识别为 720p 而非 1080p(老bug回归)。""" + from packages.domain.points_rules import _infer_resolution_key + + assert _infer_resolution_key(720, 1280) == "720p" class TestCalculateViralVideoCredits: @@ -411,6 +446,36 @@ class TestCalculateViralVideoCredits: assert c_zero == c_default assert c_none == c_default + # ──────── P0 计费回归:短边规则 + 价格精确断言 ──────── + + def test_15s_720p_portrait_is_29_68(self): + """P0 回归:15s/720p/9:16 竖屏 (720×1280) 必须 =29.68 积分。""" + from packages.domain.points_rules import ( + calculate_viral_video_credits, + resolve_video_dimensions, + ) + + w, h = resolve_video_dimensions("720p", "9:16") + assert (w, h) == (720, 1280) + assert calculate_viral_video_credits(15, w, h) == 29.68 + + def test_30s_1080p_portrait_is_146_14(self): + """P0 回归:30s/1080p/9:16 竖屏 (1080×1920) =146.14 积分。""" + from packages.domain.points_rules import ( + calculate_viral_video_credits, + resolve_video_dimensions, + ) + + w, h = resolve_video_dimensions("1080p", "9:16") + assert (w, h) == (1080, 1920) + assert calculate_viral_video_credits(30, w, h) == 146.14 + + def test_portrait_landscape_same_pixels_same_price(self): + """相同像素数(横竖屏旋转)积分一致。""" + from packages.domain.points_rules import calculate_viral_video_credits + + assert calculate_viral_video_credits(15, 1280, 720) == calculate_viral_video_credits(15, 720, 1280) + class TestViralVideoCreditsWithBreakdown: """calculate_viral_video_credits_with_breakdown:返回 (credits, breakdown_dict)。""" -- 2.54.0