From 548fc9beb568c45ac81905ebf33976f49cf5ec78 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sat, 25 Jul 2026 10:04:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?test(unit):=20=E7=AC=AC70=E6=B3=A2=20-=20?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E7=AE=97=E6=B3=95=20+=20=E5=AD=97=E5=B9=95?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=20+=20TTS=E5=BC=95=E6=93=8E=E7=BA=AF?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dedup: hamming_distance汉明距离(8种场景) + VideoFingerprint数据结构 + _average_histogram_similarity直方图相似度(10种边界) - render_subtitles: 颜色转换/位置映射/样式构建/文本转义/时间格式化 + generate_ass_subtitles文件生成(12种场景) - tts_engine: VoiceoverSegment/VoiceoverResult数据结构 + generate_full_voiceover边界(7种) + generate_subtitle_voiceover边界(3种) +73 tests 全绿 --- tests/unit/test_dedup_pure.py | 174 +++++++++++++ tests/unit/test_render_subtitles_pure.py | 311 +++++++++++++++++++++++ tests/unit/test_tts_engine_pure.py | 184 ++++++++++++++ 3 files changed, 669 insertions(+) create mode 100755 tests/unit/test_dedup_pure.py create mode 100755 tests/unit/test_render_subtitles_pure.py create mode 100755 tests/unit/test_tts_engine_pure.py diff --git a/tests/unit/test_dedup_pure.py b/tests/unit/test_dedup_pure.py new file mode 100755 index 000000000..49264c5c7 --- /dev/null +++ b/tests/unit/test_dedup_pure.py @@ -0,0 +1,174 @@ +"""去重纯算法测试 — hamming_distance + histogram_similarity + VideoFingerprint.""" + +from __future__ import annotations + +import sys +from unittest.mock import MagicMock + +import numpy as np +import pytest + +# 模块级mock cv2(dedup模块import时需要) +sys.modules["cv2"] = MagicMock() + +from video_processing.dedup import ( # noqa: E402 + VideoDeduplicator, + VideoFingerprint, + hamming_distance, +) + + +class TestHammingDistance: + """hamming_distance 汉明距离计算测试.""" + + def test_identical_hashes_zero(self): + """相同哈希距离为0.""" + assert hamming_distance("ff", "ff") == 0 + assert hamming_distance("00", "00") == 0 + + def test_all_different(self): + """全不同的8bit哈希距离为8.""" + assert hamming_distance("00", "ff") == 8 + + def test_single_bit_diff(self): + """1个bit不同.""" + # 0x01 = 00000001, 0x00 = 00000000 → 1 bit不同 + assert hamming_distance("01", "00") == 1 + + def test_four_bits_diff(self): + """4个bit不同.""" + # 0x0F = 00001111, 0xF0 = 11110000 → 8 bits都不同 + assert hamming_distance("0f", "f0") == 8 + + def test_longer_hashes(self): + """更长的哈希(如64-bit pHash).""" + # 两个完全不同的64-bit哈希 + assert hamming_distance("0000000000000000", "ffffffffffffffff") == 64 + + def test_partial_difference(self): + """部分bit不同.""" + # a = 1010, 5 = 0101 → 4 bits不同(每个hex digit) + assert hamming_distance("aa", "55") == 8 + + def test_case_insensitive(self): + """十六进制不区分大小写.""" + assert hamming_distance("FF", "ff") == 0 + assert hamming_distance("AbC123", "aBc123") == 0 + + def test_different_length_hashes(self): + """不同长度的哈希(短的前补零).""" + # "ff" = 0xff = 255, "0ff" = 0x0ff = 255 + # int("ff", 16) = 255, int("0ff", 16) = 255 + assert hamming_distance("ff", "0ff") == 0 + + +class TestVideoFingerprint: + """VideoFingerprint 数据结构测试.""" + + def test_to_dict_contains_all_fields(self): + """to_dict返回完整字典.""" + fp = VideoFingerprint( + md5="abc123", + keyframe_phashes=["hash1", "hash2"], + color_histograms=[[0.1, 0.2], [0.3, 0.4]], + duration=30.5, + resolution=(1920, 1080), + ) + d = fp.to_dict() + assert d["md5"] == "abc123" + assert d["keyframe_phashes"] == ["hash1", "hash2"] + assert d["duration"] == 30.5 + assert d["resolution"] == [1920, 1080] + assert "color_histograms" in d + + def test_empty_phashes(self): + """空关键帧列表.""" + fp = VideoFingerprint( + md5="test", + keyframe_phashes=[], + color_histograms=[], + duration=0.0, + resolution=(0, 0), + ) + d = fp.to_dict() + assert d["keyframe_phashes"] == [] + assert d["color_histograms"] == [] + + +class TestAverageHistogramSimilarity: + """_average_histogram_similarity 直方图相似度测试.""" + + def test_identical_histograms(self): + """完全相同的直方图相似度为1.0.""" + hist = [[0.5, 0.5, 0.0], [0.3, 0.4, 0.3]] + sim = VideoDeduplicator._average_histogram_similarity(hist, hist) + assert sim == pytest.approx(1.0) + + def test_empty_first_list(self): + """第一组为空返回0.""" + sim = VideoDeduplicator._average_histogram_similarity( + [], [[0.5, 0.5]] + ) + assert sim == 0.0 + + def test_empty_second_list(self): + """第二组为空返回0.""" + sim = VideoDeduplicator._average_histogram_similarity( + [[0.5, 0.5]], [] + ) + assert sim == 0.0 + + def test_both_empty(self): + """两组都为空返回0.""" + sim = VideoDeduplicator._average_histogram_similarity([], []) + assert sim == 0.0 + + def test_orthogonal_histograms(self): + """正交直方图相似度为0.""" + # [1, 0] 和 [0, 1] 正交 + sim = VideoDeduplicator._average_histogram_similarity( + [[1.0, 0.0]], [[0.0, 1.0]] + ) + assert sim == pytest.approx(0.0) + + def test_partial_similarity(self): + """部分相似.""" + # [1, 1] 和 [1, 0] 的余弦相似度 = 1/√2 ≈ 0.707 + sim = VideoDeduplicator._average_histogram_similarity( + [[1.0, 1.0]], [[1.0, 0.0]] + ) + assert sim == pytest.approx(1.0 / (2 ** 0.5), rel=0.01) + + def test_multiple_frames_best_match(self): + """多帧时取最佳匹配.""" + # 第一帧完全不同,第二帧完全相同 → 平均 best = (0 + 1) / 2 = 0.5 + sim = VideoDeduplicator._average_histogram_similarity( + [[1.0, 0.0], [0.0, 1.0]], + [[0.0, 1.0]], # 只有一帧,和第一帧0相似,和第二帧1相似 + ) + # 第一帧最佳匹配=0,第二帧最佳匹配=1,平均=0.5 + assert sim == pytest.approx(0.5) + + def test_zero_norm_histogram_skipped(self): + """零范数直方图被跳过.""" + sim = VideoDeduplicator._average_histogram_similarity( + [[0.0, 0.0]], [[1.0, 1.0]] + ) + # 第一组的零范数被跳过,similarities为空,返回0 + assert sim == 0.0 + + def test_different_length_histograms(self): + """不同长度的直方图取最小长度对齐.""" + sim = VideoDeduplicator._average_histogram_similarity( + [[1.0, 1.0, 0.0, 0.0]], # 4维 + [[1.0, 1.0]], # 2维 + ) + # 对齐到前2维,都是[1,1],相似度1.0 + assert sim == pytest.approx(1.0) + + def test_similarity_in_zero_one_range(self): + """相似度在[0, 1]范围内.""" + hist_a = [np.random.rand(96).tolist() for _ in range(5)] + hist_b = [np.random.rand(96).tolist() for _ in range(5)] + sim = VideoDeduplicator._average_histogram_similarity(hist_a, hist_b) + assert 0.0 <= sim <= 1.0 diff --git a/tests/unit/test_render_subtitles_pure.py b/tests/unit/test_render_subtitles_pure.py new file mode 100755 index 000000000..331d4b6f1 --- /dev/null +++ b/tests/unit/test_render_subtitles_pure.py @@ -0,0 +1,311 @@ +"""字幕渲染纯函数测试 — _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 diff --git a/tests/unit/test_tts_engine_pure.py b/tests/unit/test_tts_engine_pure.py new file mode 100755 index 000000000..e5923bda3 --- /dev/null +++ b/tests/unit/test_tts_engine_pure.py @@ -0,0 +1,184 @@ +"""TTS配音引擎纯逻辑测试 — 数据结构 + 边界情况(mock TTS服务).""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from packages.domain.tts_config import TtsConfig +from packages.ports.tts_service import TtsError +from video_processing.tts_engine import ( + TtsEngine, + VoiceoverResult, + VoiceoverSegment, +) + + +class TestVoiceoverSegment: + """VoiceoverSegment 数据结构测试.""" + + def test_default_values(self): + """默认值正确.""" + seg = VoiceoverSegment(text="你好") + assert seg.text == "你好" + assert seg.start_time == 0.0 + assert seg.end_time == 0.0 + assert seg.audio_path is None + assert seg.duration == 0.0 + + def test_full_values(self): + """完整字段.""" + seg = VoiceoverSegment( + text="测试", + start_time=1.5, + end_time=3.5, + audio_path=Path("/tmp/test.wav"), + duration=2.0, + ) + assert seg.text == "测试" + assert seg.start_time == 1.5 + assert seg.end_time == 3.5 + assert seg.audio_path == Path("/tmp/test.wav") + assert seg.duration == 2.0 + + +class TestVoiceoverResult: + """VoiceoverResult 数据结构测试.""" + + def test_failure_default(self): + """失败结果默认值.""" + result = VoiceoverResult(success=False) + assert result.success is False + assert result.segments == [] + assert result.total_duration == 0.0 + assert result.error_message == "" + + def test_success_with_segments(self): + """成功结果带片段.""" + seg = VoiceoverSegment(text="hi", duration=5.0) + result = VoiceoverResult( + success=True, + segments=[seg], + total_duration=5.0, + ) + assert result.success is True + assert len(result.segments) == 1 + assert result.total_duration == 5.0 + + def test_failure_with_message(self): + """失败带错误信息.""" + result = VoiceoverResult(success=False, error_message="TTS出错") + assert result.success is False + assert result.error_message == "TTS出错" + + +class TestTtsEngineFullVoiceover: + """TtsEngine.generate_full_voiceover 整段配音测试(mock TTS).""" + + def _make_engine(self, tmp_path: Path, tts: MagicMock | None = None) -> TtsEngine: + """创建测试用TtsEngine.""" + if tts is None: + tts = MagicMock() + tts.synthesize.return_value = str(tmp_path / "out.wav") + return TtsEngine(tts_service=tts, work_dir=tmp_path) + + def test_disabled_returns_failure(self, tmp_path): + """配音未启用→失败.""" + engine = self._make_engine(tmp_path) + config = TtsConfig(enabled=False, text="测试") + result = engine.generate_full_voiceover(config) + assert result.success is False + assert "未启用" in result.error_message + assert len(result.segments) == 0 + + def test_empty_text_returns_failure(self, tmp_path): + """文本为空→失败.""" + engine = self._make_engine(tmp_path) + config = TtsConfig(enabled=True, text=" ") + result = engine.generate_full_voiceover(config) + assert result.success is False + assert "文本为空" in result.error_message + + def test_success_creates_segment(self, tmp_path): + """成功合成返回正确结构.""" + mock_tts = MagicMock() + output_file = tmp_path / "voiceover_full.wav" + output_file.write_bytes(b"fake audio") + mock_tts.synthesize.return_value = str(output_file) + + engine = self._make_engine(tmp_path, mock_tts) + config = TtsConfig(enabled=True, text="测试文本", voice_id="female_warm", speed=1.0) + + result = engine.generate_full_voiceover(config) + + assert result.success is True + assert len(result.segments) == 1 + assert result.segments[0].text == "测试文本" + assert result.segments[0].start_time == 0.0 + assert result.total_duration > 0 + mock_tts.synthesize.assert_called_once() + + def test_tts_error_returns_failure_gracefully(self, tmp_path): + """TTS抛错→优雅降级返回失败.""" + mock_tts = MagicMock() + mock_tts.synthesize.side_effect = TtsError("合成失败") + + engine = self._make_engine(tmp_path, mock_tts) + config = TtsConfig(enabled=True, text="测试") + + result = engine.generate_full_voiceover(config) + assert result.success is False + assert "合成失败" in result.error_message + + def test_generic_exception_returns_failure(self, tmp_path): + """其他异常也降级返回失败.""" + mock_tts = MagicMock() + mock_tts.synthesize.side_effect = RuntimeError("未知错误") + + engine = self._make_engine(tmp_path, mock_tts) + config = TtsConfig(enabled=True, text="测试") + + result = engine.generate_full_voiceover(config) + assert result.success is False + assert "未知错误" in result.error_message + + def test_work_dir_created(self, tmp_path): + """工作目录自动创建.""" + new_dir = tmp_path / "nested" / "tts" + mock_tts = MagicMock() + TtsEngine(tts_service=mock_tts, work_dir=new_dir) + assert new_dir.exists() + + +class TestTtsEngineSubtitleVoiceover: + """TtsEngine.generate_subtitle_voiceover 字幕配音测试(mock TTS).""" + + def _make_engine(self, tmp_path: Path, tts: MagicMock | None = None) -> TtsEngine: + if tts is None: + tts = MagicMock() + return TtsEngine(tts_service=tts, work_dir=tmp_path) + + def test_disabled_returns_failure(self, tmp_path): + """配音未启用→失败.""" + engine = self._make_engine(tmp_path) + config = TtsConfig(enabled=False, text="") + result = engine.generate_subtitle_voiceover(config, [{"text": "hi", "start_time": 0, "end_time": 1}]) + assert result.success is False + assert "未启用" in result.error_message + + def test_empty_subtitles_returns_failure(self, tmp_path): + """字幕列表为空→失败.""" + engine = self._make_engine(tmp_path) + config = TtsConfig(enabled=True, text="") + result = engine.generate_subtitle_voiceover(config, []) + assert result.success is False + assert "字幕为空" in result.error_message + + def test_none_subtitles_returns_failure(self, tmp_path): + """None字幕也失败.""" + engine = self._make_engine(tmp_path) + config = TtsConfig(enabled=True, text="") + result = engine.generate_subtitle_voiceover(config, None) # type: ignore + assert result.success is False -- 2.54.0 From 9dd2226eb658ae7746290ccd882f8809b39f853b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 25 Jul 2026 12:03:40 +0800 Subject: [PATCH 2/3] style: fix isort import order --- tests/unit/test_render_subtitles_pure.py | 1 - tests/unit/test_tts_engine_pure.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) mode change 100755 => 100644 tests/unit/test_render_subtitles_pure.py mode change 100755 => 100644 tests/unit/test_tts_engine_pure.py diff --git a/tests/unit/test_render_subtitles_pure.py b/tests/unit/test_render_subtitles_pure.py old mode 100755 new mode 100644 index 331d4b6f1..2b4c53e04 --- a/tests/unit/test_render_subtitles_pure.py +++ b/tests/unit/test_render_subtitles_pure.py @@ -5,7 +5,6 @@ from __future__ import annotations from pathlib import Path import pytest - from video_processing.render_subtitles import ( _build_ass_style, _escape_ass_text, diff --git a/tests/unit/test_tts_engine_pure.py b/tests/unit/test_tts_engine_pure.py old mode 100755 new mode 100644 index e5923bda3..a43346610 --- a/tests/unit/test_tts_engine_pure.py +++ b/tests/unit/test_tts_engine_pure.py @@ -6,15 +6,15 @@ from pathlib import Path from unittest.mock import MagicMock import pytest - -from packages.domain.tts_config import TtsConfig -from packages.ports.tts_service import TtsError from video_processing.tts_engine import ( TtsEngine, VoiceoverResult, VoiceoverSegment, ) +from packages.domain.tts_config import TtsConfig +from packages.ports.tts_service import TtsError + class TestVoiceoverSegment: """VoiceoverSegment 数据结构测试.""" -- 2.54.0 From cfdf7fcefa3757271e15c75ef4e6b8a15aa8d3aa Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 25 Jul 2026 12:51:47 +0800 Subject: [PATCH 3/3] style: fix black formatting --- tests/unit/test_dedup_pure.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/unit/test_dedup_pure.py b/tests/unit/test_dedup_pure.py index 49264c5c7..9c82021b8 100755 --- a/tests/unit/test_dedup_pure.py +++ b/tests/unit/test_dedup_pure.py @@ -106,16 +106,12 @@ class TestAverageHistogramSimilarity: def test_empty_first_list(self): """第一组为空返回0.""" - sim = VideoDeduplicator._average_histogram_similarity( - [], [[0.5, 0.5]] - ) + sim = VideoDeduplicator._average_histogram_similarity([], [[0.5, 0.5]]) assert sim == 0.0 def test_empty_second_list(self): """第二组为空返回0.""" - sim = VideoDeduplicator._average_histogram_similarity( - [[0.5, 0.5]], [] - ) + sim = VideoDeduplicator._average_histogram_similarity([[0.5, 0.5]], []) assert sim == 0.0 def test_both_empty(self): @@ -126,18 +122,14 @@ class TestAverageHistogramSimilarity: def test_orthogonal_histograms(self): """正交直方图相似度为0.""" # [1, 0] 和 [0, 1] 正交 - sim = VideoDeduplicator._average_histogram_similarity( - [[1.0, 0.0]], [[0.0, 1.0]] - ) + sim = VideoDeduplicator._average_histogram_similarity([[1.0, 0.0]], [[0.0, 1.0]]) assert sim == pytest.approx(0.0) def test_partial_similarity(self): """部分相似.""" # [1, 1] 和 [1, 0] 的余弦相似度 = 1/√2 ≈ 0.707 - sim = VideoDeduplicator._average_histogram_similarity( - [[1.0, 1.0]], [[1.0, 0.0]] - ) - assert sim == pytest.approx(1.0 / (2 ** 0.5), rel=0.01) + sim = VideoDeduplicator._average_histogram_similarity([[1.0, 1.0]], [[1.0, 0.0]]) + assert sim == pytest.approx(1.0 / (2**0.5), rel=0.01) def test_multiple_frames_best_match(self): """多帧时取最佳匹配.""" @@ -151,9 +143,7 @@ class TestAverageHistogramSimilarity: def test_zero_norm_histogram_skipped(self): """零范数直方图被跳过.""" - sim = VideoDeduplicator._average_histogram_similarity( - [[0.0, 0.0]], [[1.0, 1.0]] - ) + sim = VideoDeduplicator._average_histogram_similarity([[0.0, 0.0]], [[1.0, 1.0]]) # 第一组的零范数被跳过,similarities为空,返回0 assert sim == 0.0 -- 2.54.0