""" 1080P 分辨率支持的计费逻辑测试 — 严格对齐用户三原则: 1. 不兜底/静默降级 2. 钱的计算绝对准确(纯官方公式) 3. 不隐藏 bug(非法组合 fail loud) 运行方式: cd backend && source venv/Scripts/activate && python -m pytest tests/test_1080p_billing.py -v 或 Django test runner: python manage.py test tests.test_1080p_billing """ import os import sys import django # Django setup os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) django.setup() import unittest from utils.billing import ( RESOLUTION_MAP, get_resolution, estimate_tokens, calculate_cost, calculate_base_cost, ) class TestResolutionMap(unittest.TestCase): """验证 RESOLUTION_MAP 的 18 个组合像素值与官方文档一致。""" def test_1080p_pixels(self): # 来自 docs/API文档/创建视频生成任务API.md Seedance 2.0 & 2.0 fast 列 self.assertEqual(get_resolution('16:9', '1080p'), (1920, 1080)) self.assertEqual(get_resolution('9:16', '1080p'), (1080, 1920)) self.assertEqual(get_resolution('4:3', '1080p'), (1664, 1248)) self.assertEqual(get_resolution('1:1', '1080p'), (1440, 1440)) self.assertEqual(get_resolution('3:4', '1080p'), (1248, 1664)) # 21:9 特别注意:是 2206×946,不是 seedance 1.0 的 2176×928 self.assertEqual(get_resolution('21:9', '1080p'), (2206, 946)) def test_720p_pixels(self): self.assertEqual(get_resolution('16:9', '720p'), (1280, 720)) self.assertEqual(get_resolution('21:9', '720p'), (1470, 630)) def test_480p_pixels(self): self.assertEqual(get_resolution('16:9', '480p'), (864, 496)) self.assertEqual(get_resolution('21:9', '480p'), (992, 432)) def test_invalid_combo_raises(self): """原则 1:非法组合必须 fail loud,不静默降级。""" with self.assertRaises(KeyError): get_resolution('adaptive', '720p') # adaptive 不在 map with self.assertRaises(KeyError): get_resolution('16:9', '4K') # 不存在的 tier with self.assertRaises(KeyError): get_resolution('unknown', 'unknown') def test_tier_required(self): """tier 参数必填,不允许默认 720p 静默降级。""" with self.assertRaises(TypeError): get_resolution('16:9') # type: ignore - 故意漏参数 class TestEstimateTokens(unittest.TestCase): """ 严格对齐官方公式:`(输入视频时长+输出时长) × 宽 × 高 × 帧率 / 1024` 预估端不做最低 token 修正(那是火山计费侧逻辑)。 """ def test_formula_no_input_video(self): # 720P 16:9 (1280×720), 5s 输出, 24fps # 1280 × 720 × 24 × 5 / 1024 = 108000 self.assertEqual(estimate_tokens(1280, 720, 5), 108000) def test_formula_with_input_video(self): # 720P 16:9, 5s 输出 + 5s 输入 = 10s 总时长 # 1280 × 720 × 24 × 10 / 1024 = 216000 self.assertEqual(estimate_tokens(1280, 720, 5, input_video_duration=5), 216000) def test_1080p_formula(self): # 1080P 16:9 (1920×1080), 5s 输出, 无输入视频 # 1920 × 1080 × 24 × 5 / 1024 = 243000 self.assertEqual(estimate_tokens(1920, 1080, 5), 243000) def test_1080p_with_input_video(self): # 1080P 16:9, 5s 输出 + 2s 输入 = 7s # 1920 × 1080 × 24 × 7 / 1024 = 340200 self.assertEqual(estimate_tokens(1920, 1080, 5, input_video_duration=2), 340200) def test_no_silent_min_token_adjustment(self): """原则 2:预估端严格按公式,不做最低 token 修正。 火山文档说 1080p 5s 输入含视频最低 437400 tokens,但那是火山计费侧的事, 我们预估就老老实实按公式算 (5s+2s)×1920×1080×24/1024 = 340200,不擅自拉高。 """ # 1080p 5s 输出 + 2s 输入 = 7s 总时长 # 公式值 340200,官方最低 437400 # 我们应该返回公式值,不主动调到最低值 result = estimate_tokens(1920, 1080, 5, input_video_duration=2) self.assertEqual(result, 340200, "预估端不应主动修正到火山最低 token") def test_float_input_duration(self): """输入视频时长可能是浮点数(前端 getMediaInfo 读取),要正确累加。""" # 720P 16:9, 5s 输出 + 3.5s 输入 = 8.5s # 1280 × 720 × 24 × 8.5 / 1024 = 183600 self.assertEqual(estimate_tokens(1280, 720, 5, input_video_duration=3.5), 183600) class TestGetTokenPrice(unittest.TestCase): """验证单价选择逻辑 — 4 种模型×视频组合 + 1080p 独立单价 + Fast+1080P fail loud。""" def setUp(self): # Mock QuotaConfig — 用官方文档默认值 from types import SimpleNamespace from decimal import Decimal self.config = SimpleNamespace( base_token_price=Decimal('46'), base_token_price_video=Decimal('28'), base_token_price_fast=Decimal('37'), base_token_price_fast_video=Decimal('22'), base_token_price_1080p=Decimal('51'), base_token_price_1080p_video=Decimal('31'), ) from apps.generation.views import _get_token_price self._get_token_price = _get_token_price def test_seedance_2_0_720p_no_video(self): """AirDrama 720P 不含视频 = 46 元/百万 tokens.""" price = self._get_token_price(self.config, 'seedance_2.0', False, '720p') self.assertEqual(price, 46) def test_seedance_2_0_720p_with_video(self): """AirDrama 720P 含视频 = 28.""" price = self._get_token_price(self.config, 'seedance_2.0', True, '720p') self.assertEqual(price, 28) def test_seedance_2_0_480p_same_as_720p(self): """480p 和 720p 共享同一单价(官方价格一致)。""" price_480 = self._get_token_price(self.config, 'seedance_2.0', False, '480p') price_720 = self._get_token_price(self.config, 'seedance_2.0', False, '720p') self.assertEqual(price_480, price_720) def test_seedance_2_0_1080p_no_video(self): """AirDrama 1080P 不含视频 = 51(独立单价,不是 720p 的 46).""" price = self._get_token_price(self.config, 'seedance_2.0', False, '1080p') self.assertEqual(price, 51) def test_seedance_2_0_1080p_with_video(self): """AirDrama 1080P 含视频 = 31(独立单价,不是 720p 的 28).""" price = self._get_token_price(self.config, 'seedance_2.0', True, '1080p') self.assertEqual(price, 31) def test_fast_720p_no_video(self): """Fast 720P 不含视频 = 37.""" price = self._get_token_price(self.config, 'seedance_2.0_fast', False, '720p') self.assertEqual(price, 37) def test_fast_720p_with_video(self): """Fast 720P 含视频 = 22.""" price = self._get_token_price(self.config, 'seedance_2.0_fast', True, '720p') self.assertEqual(price, 22) def test_fast_480p_uses_fast_price(self): """Fast 不分 480p/720p,都用 fast 单价。""" price_480 = self._get_token_price(self.config, 'seedance_2.0_fast', False, '480p') price_720 = self._get_token_price(self.config, 'seedance_2.0_fast', False, '720p') self.assertEqual(price_480, price_720) self.assertEqual(price_480, 37) def test_fast_1080p_raises_value_error(self): """原则 1 + 3:Fast + 1080P 必须 fail loud,不能静默按 720p 价(欺骗用户)。""" with self.assertRaises(ValueError) as ctx: self._get_token_price(self.config, 'seedance_2.0_fast', False, '1080p') self.assertIn('1080p', str(ctx.exception).lower()) with self.assertRaises(ValueError): self._get_token_price(self.config, 'seedance_2.0_fast', True, '1080p') class TestCalculateCost(unittest.TestCase): """验证扣费金额计算 = tokens × 单价 × (1 + 加价%),精确到分.""" def test_720p_cost_matches_official_example(self): """官方示例:720P 5s 16:9 = 4.97 元(无加价).""" # 720p 5s 公式值 108000 tokens tokens = estimate_tokens(1280, 720, 5) # 46 元/百万 × 108000 / 1000000 = 4.968 ≈ 4.97 cost = calculate_cost(tokens, 46, 0) self.assertEqual(str(cost), '4.97') def test_1080p_no_video_cost(self): """1080P 5s 16:9 不含视频 = 1920×1080×24×5/1024 × 51 / 1000000 = 12.393 ≈ 12.39 元.""" tokens = estimate_tokens(1920, 1080, 5) cost = calculate_cost(tokens, 51, 0) self.assertEqual(str(cost), '12.39') def test_markup_applied(self): """团队加价 20% 的情况。""" tokens = estimate_tokens(1280, 720, 5) # 108000 cost = calculate_cost(tokens, 46, 20) # 4.968 × 1.2 = 5.9616 → 5.96 self.assertEqual(str(cost), '5.96') if __name__ == '__main__': unittest.main(verbosity=2)