Files
xiaoxia-saas/tests/unit/test_render_subtitles_pure.py
T
xiaoxia dc62884c5f
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 / Frontend Lint (push) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m38s
CI/CD Pipeline / Build Production API Image (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 / Validate - Code Quality (push) Failing after 2m59s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m49s
CI/CD Pipeline / Unit Tests (push) Failing after 4m29s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 15s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m38s
test(unit): 第70波 - 去重算法 + 字幕渲染 + TTS引擎纯逻辑 (+73) (#868)
2026-07-25 14:18:22 +08:00

311 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_style + generate_ass_subtitles."""
from __future__ import annotations
from pathlib import Path
import pytest
from video_processing.render_subtitles import (
_build_ass_style,
_escape_ass_text,
_format_ass_time,
_hex_to_ass_color,
_position_to_ass_alignment,
generate_ass_subtitles,
)
class TestHexToAssColor:
"""_hex_to_ass_color 颜色转换测试."""
def test_white(self):
"""白色 #FFFFFF → &HFFFFFF (ASS BGR格式)."""
assert _hex_to_ass_color("#FFFFFF") == "&HFFFFFF"
def test_black(self):
"""黑色 #000000 → &H000000."""
assert _hex_to_ass_color("#000000") == "&H000000"
def test_red(self):
"""红色 #FF0000 → ASS是BGR顺序 → &H0000FF."""
assert _hex_to_ass_color("#FF0000") == "&H0000FF"
def test_blue(self):
"""蓝色 #0000FF → BGR → &HFF0000."""
assert _hex_to_ass_color("#0000FF") == "&HFF0000"
def test_green(self):
"""绿色 #00FF00 → BGR → &H00FF00."""
assert _hex_to_ass_color("#00FF00") == "&H00FF00"
def test_lowercase(self):
"""小写hex也支持."""
assert _hex_to_ass_color("#ff0000") == "&H0000FF"
def test_no_hash_prefix(self):
"""不带#的颜色."""
assert _hex_to_ass_color("FF0000") == "&H0000FF"
def test_invalid_length_fallback(self):
"""长度不对返回默认黑色."""
assert _hex_to_ass_color("#FFF") == "&H000000"
class TestPositionToAssAlignment:
"""_position_to_ass_alignment 位置映射测试."""
def test_top_center(self):
"""top → 上中(8)."""
assert _position_to_ass_alignment("top") == 8
def test_bottom_center(self):
"""bottom → 下中(2)."""
assert _position_to_ass_alignment("bottom") == 2
def test_center_middle(self):
"""center → 居中(5)."""
assert _position_to_ass_alignment("center") == 5
def test_unknown_defaults_to_top(self):
"""未知位置默认顶部(8)."""
assert _position_to_ass_alignment("unknown") == 8
assert _position_to_ass_alignment("top_left") == 8
assert _position_to_ass_alignment("bottom_right") == 8
def test_empty_string_defaults_to_top(self):
"""空字符串默认顶部."""
assert _position_to_ass_alignment("") == 8
class TestBuildAssStyle:
"""_build_ass_style ASS样式行构建测试."""
def test_basic_style_line(self):
"""基本样式行包含关键字段."""
line = _build_ass_style("Default")
assert line.startswith("Style: Default,")
assert "思源黑体" in line
assert "48" in line # font_size
def test_bold_enabled(self):
"""加粗时Bold=-1."""
line = _build_ass_style("Bold", bold=True)
assert "Style: Bold," in line
# Bold字段位置:第7个逗号分隔字段=Bold=-1
parts = line.split(",")
# Name, Fontname, Fontsize, Primary, Secondary, Outline, Back, Bold, ...
assert parts[7] == "-1" # Bold
def test_bold_disabled(self):
"""不加粗时Bold=0."""
line = _build_ass_style("Normal", bold=False)
parts = line.split(",")
assert parts[7] == "0"
def test_italic_enabled(self):
"""斜体时Italic=-1."""
line = _build_ass_style("Italic", italic=True)
parts = line.split(",")
assert parts[8] == "-1" # Italic
def test_custom_font_size(self):
"""自定义字号."""
line = _build_ass_style("Big", font_size=72)
parts = line.split(",")
assert parts[2] == "72" # Fontsize
def test_custom_alignment(self):
"""自定义对齐方式."""
line = _build_ass_style("Bottom", alignment=2)
# Alignment是第16个字段(数一下)
# Name, Fontname, Fontsize, Primary, Secondary, Outline, Back, Bold, Italic, Underline, Strikeout, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, ...
parts = line.split(",")
assert parts[18] == "2" # Alignment (0-indexed: 18)
def test_outline_width(self):
"""描边宽度."""
line = _build_ass_style("Outline", outline_width=3.0)
parts = line.split(",")
assert parts[16] == "3.0" # Outline (BorderStyle后是Outline)
class TestEscapeAssText:
"""_escape_ass_text 文本转义测试."""
def test_plain_text_unchanged(self):
"""普通文本不变."""
assert _escape_ass_text("hello world") == "hello world"
def test_newline_converted(self):
"""换行符转成\\N."""
assert _escape_ass_text("line1\nline2") == "line1\\Nline2"
def test_crlf_converted(self):
"""CRLF转成\\N."""
assert _escape_ass_text("a\r\nb") == "a\\Nb"
def test_carriage_return_converted(self):
"""纯\\r转成\\N."""
assert _escape_ass_text("a\rb") == "a\\Nb"
def test_curly_braces_replaced(self):
"""大括号转成圆括号(防止ASS样式注入)."""
assert _escape_ass_text("{text}") == "(text)"
def test_mixed_special_chars(self):
"""混合特殊字符."""
result = _escape_ass_text("line1\nline2 {bold}\rlast")
assert "line1\\Nline2 (bold)\\Nlast" == result
class TestFormatAssTime:
"""_format_ass_time 时间格式化测试."""
def test_zero_seconds(self):
"""0秒."""
assert _format_ass_time(0) == "0:00:00.00"
def test_seconds_only(self):
"""只有秒."""
assert _format_ass_time(5.5) == "0:00:05.50"
def test_minutes_and_seconds(self):
"""几分几秒."""
assert _format_ass_time(65.25) == "0:01:05.25"
def test_hours(self):
"""几小时."""
assert _format_ass_time(3661.5) == "1:01:01.50"
def test_always_two_decimal_places(self):
"""总是两位小数."""
assert _format_ass_time(1.0) == "0:00:01.00"
assert _format_ass_time(1.1) == "0:00:01.10"
class TestGenerateAssSubtitles:
"""generate_ass_subtitles ASS字幕文件生成测试."""
def test_no_subtitles_empty_file(self, tmp_path):
"""没有字幕生成空文件."""
output = tmp_path / "empty.ass"
result = generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=10.0,
)
assert result == output
assert output.exists()
assert output.read_text(encoding="utf-8") == ""
def test_title_only(self, tmp_path):
"""只有标题."""
output = tmp_path / "title.ass"
result = generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=30.0,
title_text="测试标题",
)
assert result == output
content = output.read_text(encoding="utf-8")
assert "Script Info" in content
assert "PlayResX: 1080" in content
assert "PlayResY: 1920" in content
assert "测试标题" in content
assert "V4+ Styles" in content
assert "Events" in content
def test_subtitle_only(self, tmp_path):
"""只有底部字幕."""
output = tmp_path / "sub.ass"
result = generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=15.0,
subtitle_text="这是字幕",
)
assert result == output
content = output.read_text(encoding="utf-8")
assert "这是字幕" in content
assert "PlayResX: 1080" in content
def test_both_title_and_subtitle(self, tmp_path):
"""标题+字幕都有."""
output = tmp_path / "both.ass"
result = generate_ass_subtitles(
output,
video_width=720,
video_height=1280,
video_duration=20.0,
title_text="大标题",
subtitle_text="底部字幕",
)
content = output.read_text(encoding="utf-8")
assert "大标题" in content
assert "底部字幕" in content
# 应该有两种样式(title和subtitle
assert content.count("Style:") >= 2
def test_title_disabled_by_config(self, tmp_path):
"""通过config禁用标题."""
output = tmp_path / "disabled_title.ass"
result = generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="标题",
title_config={"enabled": False},
subtitle_text="字幕",
)
content = output.read_text(encoding="utf-8")
assert "标题" not in content
assert "字幕" in content
def test_empty_title_text_not_rendered(self, tmp_path):
"""空标题文本不渲染."""
output = tmp_path / "empty_title.ass"
result = generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text=" ",
subtitle_text="有字幕",
)
content = output.read_text(encoding="utf-8")
assert "有字幕" in content
def test_custom_title_color(self, tmp_path):
"""自定义标题颜色."""
output = tmp_path / "color.ass"
result = generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="红色标题",
title_config={"color": "#FF0000"},
)
content = output.read_text(encoding="utf-8")
# 红色 → ASS BGR格式 &H0000FF
assert "&H0000FF" in content
def test_dialogue_line_format(self, tmp_path):
"""Dialogue行格式正确."""
output = tmp_path / "dialogue.ass"
generate_ass_subtitles(
output,
video_width=1080,
video_height=1920,
video_duration=5.0,
subtitle_text="测试字幕文本",
)
content = output.read_text(encoding="utf-8")
assert "Dialogue:" in content
assert "测试字幕文本" in content