Files
xiaoxia-saas/tests/unit/test_title_render_consistency.py
T
xiaoxia 323bd2da5e
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 46s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m52s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m7s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m56s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
AI Code Review / AI Code Review (pull_request) Successful in 5m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m9s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m40s
CI/CD Pipeline / Integration Tests (push) Successful in 4m40s
CI/CD Pipeline / Unit Tests (push) Successful in 14m37s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m58s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m59s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
fix: 标题/换行 + TTS replace模式静音原音 + 配音素材库静音原音 (#1504)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 23:06:00 +08:00

280 lines
10 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.
"""标题渲染前后端一致性测试。
验证 build_ass_content 生成的 ASS 样式参数与前端 drawTitleOnCanvas.ts 一致:
- 字号不再设置上限,由前端/调用方控制
- 描边宽度 2px
- 阴影 blur=4, offset=2
- boolean stroke/shadow 自动转换
"""
import sys
from pathlib import Path
import pytest
# Ensure packages is importable
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "packages"))
from domain.ass_subtitle_builder import build_ass_content, build_ass_style
class TestFontSize:
"""字号处理:默认值与保留逻辑,不再做上限截断。"""
def test_default_font_size_is_36(self):
"""无 size 字段时,默认字号应为 36。"""
config = {"text": "test"}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
assert ",36," in content, f"默认字号应为36,实际内容: {content}"
def test_size_32_preserved(self):
"""size=32 应原样使用。"""
config = {"size": 32}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
assert ",32," in content
def test_size_60_preserved(self):
"""size=60 应原样保留(字号上限已移除)。"""
config = {"size": 60}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
font_size = int(fields[2])
assert font_size == 60, f"字号60应原样保留, 实际={font_size}"
def test_font_size_alias_normalized(self):
"""前端传 font_size 应归一化为 size。"""
config = {"font_size": 52}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
assert fields[2] == "52", f"font_size=52 应归一化, 实际={fields[2]}"
def test_font_color_alias_normalized(self):
"""前端传 font_color 应归一化为 color。"""
config = {"font_color": "#00FF00"}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
# 绿色 → &H00FF00
assert "&H00FF00" in content
def test_size_24_preserved(self):
"""size=24 应原样使用。"""
config = {"size": 24}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
assert ",24," in content
class TestBooleanStrokeNormalization:
"""前端 stroke=true/false 应自动转换为标准 dict。"""
def test_stroke_true_enables_outline(self):
"""stroke=true 应生成 outline_width=2 的样式。"""
config = {"stroke": True}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
# 解析 Style 行的 Outline 字段(第17个字段,索引16)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
outline_width = float(fields[16])
assert outline_width == 2.0, f"stroke=true 应产生 outline_width=2, 实际={outline_width}"
def test_stroke_false_no_outline(self):
"""stroke=false 应生成 outline_width=0 的样式。"""
config = {"stroke": False}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
outline_width = float(fields[16])
assert outline_width == 0.0, f"stroke=false 应产生 outline_width=0, 实际={outline_width}"
def test_stroke_dict_still_works(self):
"""stroke={enabled:true, width:3} 仍应正常工作。"""
config = {"stroke": {"enabled": True, "width": 3, "color": "#FF0000"}}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
outline_width = float(fields[16])
assert outline_width == 3.0, f"自定义stroke width=3 应保留, 实际={outline_width}"
class TestBooleanShadowNormalization:
"""前端 shadow=true/false 应自动转换为标准 dict。"""
def test_shadow_true_enables_shadow(self):
"""shadow=true 应生成 shadow_depth=2 的样式。"""
config = {"shadow": True}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
# Shadow 字段是第18个(索引17
shadow_depth = int(fields[17])
assert shadow_depth == 2, f"shadow=true 应产生 shadow_depth=2, 实际={shadow_depth}"
def test_shadow_false_no_shadow(self):
"""shadow=false 应生成 shadow_depth=0 的样式。"""
config = {"shadow": False}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
shadow_depth = int(fields[17])
assert shadow_depth == 0, f"shadow=false 应产生 shadow_depth=0, 实际={shadow_depth}"
def test_shadow_dict_still_works(self):
"""shadow={enabled:true, blur:8} 仍应正常工作。"""
config = {"shadow": {"enabled": True, "blur": 8, "offset_x": 3, "offset_y": 3}}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
shadow_depth = int(fields[17])
assert shadow_depth == 3, f"自定义shadow offset_y=3 应保留, 实际={shadow_depth}"
class TestFullStyleConsistency:
"""完整样式参数一致性测试。"""
def test_frontend_default_style_matches_backend(self):
"""前端默认样式参数应在后端产生一致的 ASS 输出。
前端默认:font_size=24(或用户设置), bold=false, stroke=true, shadow=true, color=#FFFFFF
"""
config = {
"text": "标题文本",
"font": "思源黑体",
"size": 28,
"color": "#FFFFFF",
"bold": True,
"italic": False,
"stroke": True,
"shadow": True,
"position": "top",
}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="标题文本",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
# Fontname
assert fields[1] == "思源黑体"
# Fontsize = 28
assert fields[2] == "28"
# Bold = -1 (True)
assert fields[7] == "-1"
# Outline width = 2 (前端默认 stroke width)
assert float(fields[16]) == 2.0
# Shadow depth = 2 (offset_y)
assert int(fields[17]) == 2
# Alignment = 8 (top)
assert int(fields[18]) == 8
class TestTitleSlashNewline:
"""用户输入 / 或 应触发标题换行。"""
def test_halfwidth_slash_in_title(self):
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="第一行/第二行",
title_config={"size": 48},
)
for line in content.splitlines():
if "Dialogue" in line and "TitleStyle" in line:
assert "\\N" in line, f"斜杠应转为换行: {line}"
assert "第一行" in line
assert "第二行" in line
break
else:
pytest.fail("未找到 TitleStyle Dialogue 行")
def test_fullwidth_slash_in_title(self):
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="第一行/第二行",
title_config={"size": 48},
)
for line in content.splitlines():
if "Dialogue" in line and "TitleStyle" in line:
assert "\\N" in line, f"全角斜杠应转为换行: {line}"
break
else:
pytest.fail("未找到 TitleStyle Dialogue 行")