Files
xiaoxia-saas/tests/unit/test_render_subtitles_pure.py
xiaoxia e86f137c3d
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API 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 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
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
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 20s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
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 / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m5s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m8s
CI/CD Pipeline / Integration Tests (push) Successful in 2m33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m20s
CI/CD Pipeline / Validate - Security (push) Successful in 6m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m59s
CI/CD Pipeline / Unit Tests (push) Failing after 8m16s
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 / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
fix: 标题默认位置 fallback 改为 bottom,与前端 DEFAULT_TITLE_SETTINGS 对齐 (#1691)
2026-09-04 15:19:34 +08:00

311 lines
10 KiB
Python
Raw Permalink 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_bottom(self):
"""未知位置默认底部(2)."""
assert _position_to_ass_alignment("unknown") == 2
assert _position_to_ass_alignment("top_left") == 2
assert _position_to_ass_alignment("bottom_right") == 2
def test_empty_string_defaults_to_bottom(self):
"""空字符串默认底部."""
assert _position_to_ass_alignment("") == 2
class TestBuildAssStyle:
"""_build_ass_style ASS样式行构建测试."""
def test_basic_style_line(self):
"""基本样式行包含关键字段."""
line = _build_ass_style("Default")
assert line.startswith("Style: Default,")
assert "Noto Sans SC" in line
assert "65" in line # font_size 48*1.35=65
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] == "97" # Fontsize 72*1.35=97
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_title_color_overlay(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