"""字幕领域模型单元测试.""" from __future__ import annotations from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline, SubtitleWord class TestSubtitleWord: """SubtitleWord 测试.""" def test_basic_properties(self): word = SubtitleWord(text="你好", start=1.0, end=1.5) assert word.text == "你好" assert word.start == 1.0 assert word.end == 1.5 assert word.duration == 0.5 def test_duration_zero_when_end_before_start(self): word = SubtitleWord(text="test", start=2.0, end=1.0) assert word.duration == 0.0 def test_duration_zero_when_same_time(self): word = SubtitleWord(text="test", start=1.0, end=1.0) assert word.duration == 0.0 class TestSubtitleSegment: """SubtitleSegment 测试.""" def test_basic_properties(self): seg = SubtitleSegment(text="大家好", start=0.0, end=2.0) assert seg.text == "大家好" assert seg.start == 0.0 assert seg.end == 2.0 assert seg.duration == 2.0 assert seg.char_count == 3 assert seg.words == [] def test_duration_with_words(self): words = [ SubtitleWord(text="大", start=0.0, end=0.5), SubtitleWord(text="家", start=0.5, end=1.0), SubtitleWord(text="好", start=1.0, end=1.5), ] seg = SubtitleSegment(text="大家好", start=0.0, end=1.5, words=words) assert seg.duration == 1.5 assert seg.char_count == 3 assert len(seg.words) == 3 def test_duration_zero_when_end_before_start(self): seg = SubtitleSegment(text="test", start=3.0, end=1.0) assert seg.duration == 0.0 class TestSubtitleTimelineBasics: """SubtitleTimeline 基础属性测试.""" def test_empty_timeline(self): tl = SubtitleTimeline() assert tl.segment_count == 0 assert tl.total_chars == 0 assert tl.language == "zh" assert tl.total_duration == 0.0 def test_single_segment(self): seg = SubtitleSegment(text="测试", start=0.0, end=1.0) tl = SubtitleTimeline(segments=[seg]) assert tl.segment_count == 1 assert tl.total_chars == 2 def test_multiple_segments(self): segs = [ SubtitleSegment(text="第一句", start=0.0, end=1.0), SubtitleSegment(text="第二句", start=1.0, end=2.0), SubtitleSegment(text="第三句", start=2.0, end=3.0), ] tl = SubtitleTimeline(segments=segs, total_duration=3.0) assert tl.segment_count == 3 assert tl.total_chars == 9 assert tl.total_duration == 3.0 def test_custom_language(self): tl = SubtitleTimeline(language="en") assert tl.language == "en" class TestSubtitleTimelineMergeShort: """合并短字幕片段测试.""" def test_empty_or_single_no_change(self): tl = SubtitleTimeline() result = tl.merge_short_segments() assert result.segment_count == 0 seg = SubtitleSegment(text="短", start=0.0, end=0.5) tl2 = SubtitleTimeline(segments=[seg]) result2 = tl2.merge_short_segments() assert result2.segment_count == 1 def test_merge_short_segments(self): segs = [ SubtitleSegment(text="你好", start=0.0, end=0.5), SubtitleSegment(text="世界", start=0.5, end=1.0), SubtitleSegment(text="今天天气很好", start=1.0, end=2.0), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=4) # "你好"+"世界"=4字,合并;"今天天气很好"=6字,保留 assert result.segment_count == 2 assert result.segments[0].text == "你好世界" assert result.segments[0].start == 0.0 assert result.segments[0].end == 1.0 assert result.segments[1].text == "今天天气很好" def test_merge_trailing_short_to_last(self): segs = [ SubtitleSegment(text="一二三四五六七八", start=0.0, end=1.0), SubtitleSegment(text="短", start=1.0, end=1.2), SubtitleSegment(text="尾", start=1.2, end=1.4), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=4) # "一二三四五六七八"=8字 → 保留 # "短"+"尾"=2字 < 4 → 合并到上一段 assert result.segment_count == 1 assert result.segments[0].text == "一二三四五六七八短尾" def test_merge_with_words(self): words1 = [SubtitleWord(text="你", start=0.0, end=0.25), SubtitleWord(text="好", start=0.25, end=0.5)] words2 = [SubtitleWord(text="世", start=0.5, end=0.75), SubtitleWord(text="界", start=0.75, end=1.0)] segs = [ SubtitleSegment(text="你好", start=0.0, end=0.5, words=words1), SubtitleSegment(text="世界", start=0.5, end=1.0, words=words2), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=4) assert result.segment_count == 1 assert len(result.segments[0].words) == 4 def test_preserves_language_and_duration(self): segs = [SubtitleSegment(text="短", start=0.0, end=0.5)] tl = SubtitleTimeline(segments=segs, language="ja", total_duration=0.5) result = tl.merge_short_segments(min_chars=4) assert result.language == "ja" assert result.total_duration == 0.5 class TestSubtitleTimelineSplitLong: """拆分长字幕片段测试.""" def test_short_segments_no_change(self): segs = [SubtitleSegment(text="短句", start=0.0, end=1.0)] tl = SubtitleTimeline(segments=segs) result = tl.split_long_segments(max_chars=10) assert result.segment_count == 1 def test_split_by_sentence_punctuation(self): text = "今天天气很好。我们出去散步吧!" seg = SubtitleSegment(text=text, start=0.0, end=10.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=8) assert result.segment_count >= 2 assert result.segments[0].text.endswith("。") assert result.total_chars == len(text) def test_split_long_text_no_punctuation_hard_cut(self): text = "一二三四五六七八九十十一十二十三十四十五十六十七十八" seg = SubtitleSegment(text=text, start=0.0, end=10.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=8) assert result.segment_count > 1 # 所有片段都不超过 max_chars for s in result.segments: assert s.char_count <= 8 def test_split_time_proportional(self): text = "一二三四。五六七八。" seg = SubtitleSegment(text=text, start=0.0, end=10.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=4) assert result.segment_count >= 2 # 总时长保持一致 assert abs(result.segments[-1].end - 10.0) < 0.01 def test_split_with_words(self): words = [ SubtitleWord(text="一", start=0.0, end=0.5), SubtitleWord(text="二", start=0.5, end=1.0), SubtitleWord(text="三", start=1.0, end=1.5), SubtitleWord(text="四", start=1.5, end=2.0), ] text = "一二三四五六七八" seg = SubtitleSegment(text=text, start=0.0, end=4.0, words=words) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=4) assert result.segment_count >= 2 # 词的总数应该不变 total_words = sum(len(s.words) for s in result.segments) assert total_words == 4 def test_split_preserves_language(self): seg = SubtitleSegment(text="test", start=0.0, end=1.0) tl = SubtitleTimeline(segments=[seg], language="en") result = tl.split_long_segments(max_chars=2) assert result.language == "en" class TestSplitTextByPunctuation: """标点拆分静态方法测试.""" def test_empty_text(self): result = SubtitleTimeline._split_text_by_punctuation("", 10) assert result == [] def test_short_text_no_split(self): result = SubtitleTimeline._split_text_by_punctuation("短文本", 10) assert len(result) == 1 def test_split_by_period(self): result = SubtitleTimeline._split_text_by_punctuation("第一句。第二句。", 4) assert len(result) >= 2 assert "。" in result[0] def test_split_by_exclamation(self): result = SubtitleTimeline._split_text_by_punctuation("你好!世界!", 3) assert len(result) >= 2 def test_split_by_comma_when_long(self): text = "这是一个很长的句子,中间有逗号分隔,后面还有内容" result = SubtitleTimeline._split_text_by_punctuation(text, 8) assert len(result) >= 2 def test_no_punctuation_hard_cut(self): text = "一二三四五六七八九十十一十二十三十四十五" result = SubtitleTimeline._split_text_by_punctuation(text, 8) assert len(result) > 1 for part in result: assert len(part) <= 8 def test_sentence_end_triggers_split_when_half_max(self): # 句末标点在 max_chars//2 以上就拆分 text = "你好世界。abcdefghij" result = SubtitleTimeline._split_text_by_punctuation(text, 10) # "你好世界。"=5字 < 10但>=5(half),应该拆分 assert len(result) >= 2 class TestMergeSegments: """_merge_segments 静态方法测试.""" def test_merge_empty(self): result = SubtitleTimeline._merge_segments([]) assert result.text == "" assert result.start == 0 assert result.end == 0 def test_merge_single(self): seg = SubtitleSegment(text="test", start=1.0, end=2.0) result = SubtitleTimeline._merge_segments([seg]) assert result.text == "test" assert result.start == 1.0 assert result.end == 2.0 def test_merge_multiple(self): segs = [ SubtitleSegment(text="第一", start=0.0, end=1.0), SubtitleSegment(text="第二", start=1.0, end=2.0), ] result = SubtitleTimeline._merge_segments(segs) assert result.text == "第一第二" assert result.start == 0.0 assert result.end == 2.0 # ── merge_short_segments 更多边界 ────────────────────────────────────── class TestMergeShortSegmentsMore: """merge_short_segments 补充边界测试.""" def test_all_short_segments_merged_into_one(self): """所有片段都很短,合并成一段.""" segs = [ SubtitleSegment(text="你", start=0.0, end=0.1), SubtitleSegment(text="好", start=0.1, end=0.2), SubtitleSegment(text="世", start=0.2, end=0.3), SubtitleSegment(text="界", start=0.3, end=0.4), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=8) assert result.segment_count == 1 assert result.segments[0].text == "你好世界" assert result.segments[0].start == 0.0 assert result.segments[0].end == 0.4 def test_min_chars_very_small(self): """min_chars 很小(1),几乎不合并.""" segs = [ SubtitleSegment(text="一", start=0.0, end=0.5), SubtitleSegment(text="二", start=0.5, end=1.0), SubtitleSegment(text="三", start=1.0, end=1.5), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=1) # 每个1字 >= min_chars(1),所以都独立成段 assert result.segment_count == 3 def test_min_chars_very_large(self): """min_chars 很大,全部合并.""" segs = [ SubtitleSegment(text="第一句很长的内容", start=0.0, end=1.0), SubtitleSegment(text="第二句也不短", start=1.0, end=2.0), SubtitleSegment(text="第三句还行", start=2.0, end=3.0), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=100) assert result.segment_count == 1 assert result.total_chars == tl.total_chars def test_merge_preserves_total_duration(self): """合并后总时长不变.""" segs = [ SubtitleSegment(text="短1", start=0.0, end=0.5), SubtitleSegment(text="短2", start=0.5, end=1.0), SubtitleSegment(text="很长的一段内容", start=1.0, end=3.0), ] tl = SubtitleTimeline(segments=segs, total_duration=3.0) result = tl.merge_short_segments(min_chars=4) assert result.total_duration == 3.0 def test_merge_preserves_word_level_info(self): """合并后词级信息完整保留.""" w1 = [SubtitleWord(text="你", start=0.0, end=0.3)] w2 = [SubtitleWord(text="好", start=0.3, end=0.6)] segs = [ SubtitleSegment(text="你", start=0.0, end=0.3, words=w1), SubtitleSegment(text="好", start=0.3, end=0.6, words=w2), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=4) assert result.segment_count == 1 assert len(result.segments[0].words) == 2 assert result.segments[0].words[0].text == "你" assert result.segments[0].words[1].text == "好" def test_trailing_short_merged_to_last(self): """尾部极短段合并到最后一段.""" segs = [ SubtitleSegment(text="一二三四五六七八九十", start=0.0, end=1.0), SubtitleSegment(text="尾", start=1.0, end=1.1), ] tl = SubtitleTimeline(segments=segs) result = tl.merge_short_segments(min_chars=5) # 第一段10字 >= 5 → 先入merged # "尾" = 1字 < 5 → 进入buffer # 循环结束,buffer非空,检查是否合并到最后一段 # merged[-1] = 10字 + 1字 = 11字 <= max_chars(这里没限制) assert result.segment_count == 1 assert result.segments[0].text == "一二三四五六七八九十尾" # ── split_long_segments 更多边界 ────────────────────────────────────── class TestSplitLongSegmentsMore: """split_long_segments 补充边界测试.""" def test_exactly_max_chars_no_split(self): """恰好等于 max_chars 不拆分.""" text = "一二三四五六七八九十" # 10字 seg = SubtitleSegment(text=text, start=0.0, end=5.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=10) assert result.segment_count == 1 def test_one_over_max_chars_splits(self): """超过1个字符就拆分.""" text = "一二三四五六七八九十一" # 11字 seg = SubtitleSegment(text=text, start=0.0, end=5.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=10) assert result.segment_count >= 2 assert result.total_chars == 11 def test_mixed_short_and_long_segments(self): """长短片段混合,只拆分长的.""" segs = [ SubtitleSegment(text="短", start=0.0, end=0.5), SubtitleSegment(text="很长很长很长的一段文字内容超过限制", start=0.5, end=3.0), SubtitleSegment(text="也短", start=3.0, end=3.5), ] tl = SubtitleTimeline(segments=segs) result = tl.split_long_segments(max_chars=10) # 第1段和第3段保持,第2段被拆分 assert result.segment_count > 3 assert result.segments[0].text == "短" assert result.segments[-1].text == "也短" def test_total_chars_preserved_after_split(self): """拆分后总字符数不变.""" text = "这是第一段。这是第二段很长的内容。还有第三段。" seg = SubtitleSegment(text=text, start=0.0, end=10.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=8) assert result.total_chars == len(text) def test_total_duration_preserved(self): """拆分后最后一段的结束时间等于原始结束时间.""" text = "一二三四。五六七八。九十一二。" seg = SubtitleSegment(text=text, start=1.0, end=5.0) tl = SubtitleTimeline(segments=[seg]) result = tl.split_long_segments(max_chars=5) assert abs(result.segments[-1].end - 5.0) < 0.01 assert abs(result.segments[0].start - 1.0) < 0.01 # ── _split_text_by_punctuation 更多边界 ─────────────────────────────── class TestSplitTextByPunctuationMore: """_split_text_by_punctuation 补充边界测试.""" def test_comma_splits_when_over_max(self): """逗号在超长时触发拆分.""" text = "一二三四五六七八,八七六五四三二一" result = SubtitleTimeline._split_text_by_punctuation(text, 10) assert len(result) >= 2 assert "," in result[0] def test_colon_semicolon_splits(self): """冒号分号也能触发拆分.""" text = "首先:第一点内容;第二点内容更多一些。" result = SubtitleTimeline._split_text_by_punctuation(text, 8) assert len(result) >= 2 def test_punctuation_at_start(self): """标点在开头位置.""" text = "。这是正文内容开始了" result = SubtitleTimeline._split_text_by_punctuation(text, 10) # 开头的句号在 max_chars//2 之前,不会触发句末标点拆分 # 但整个文本不长,可能不拆 assert len(result) >= 1 def test_consecutive_punctuation(self): """连续多个标点符号.""" text = "你好!!!世界???" result = SubtitleTimeline._split_text_by_punctuation(text, 5) assert len(result) >= 1 def test_english_sentence_end(self): """英文句末标点也触发拆分.""" text = "Hello world. This is a test sentence with enough words." result = SubtitleTimeline._split_text_by_punctuation(text, 15) assert len(result) >= 2 def test_no_punctuation_hard_cut_evenly(self): """没有标点时均匀硬切.""" text = "一二三四五六七八九十一二三四五六七八九十一二三四五" # 25字 result = SubtitleTimeline._split_text_by_punctuation(text, 10) assert len(result) == 3 assert len(result[0]) == 10 assert len(result[1]) == 10 assert len(result[2]) == 5 def test_single_character_text(self): """单字符文本.""" result = SubtitleTimeline._split_text_by_punctuation("好", 10) assert result == ["好"] def test_only_punctuation(self): """只有标点符号.""" result = SubtitleTimeline._split_text_by_punctuation("。!?", 10) assert len(result) == 1 def test_mixed_fullwidth_halfwidth_punctuation(self): """全角半角标点混合.""" text = "你好。再见!谢谢?不用谢。" result = SubtitleTimeline._split_text_by_punctuation(text, 5) assert len(result) >= 2