From 57c59cb1124f37b56c0d27afa8960c2b4a9c01a3 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 18:40:34 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test(wave186):=20subtitle=20=E5=AD=97?= =?UTF-8?q?=E5=B9=95=E6=97=B6=E9=97=B4=E8=BD=B4=E6=A8=A1=E5=9E=8B=20+52?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/domain/test_subtitle.py | 498 +++++++++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100755 tests/unit/domain/test_subtitle.py diff --git a/tests/unit/domain/test_subtitle.py b/tests/unit/domain/test_subtitle.py new file mode 100755 index 000000000..16f506064 --- /dev/null +++ b/tests/unit/domain/test_subtitle.py @@ -0,0 +1,498 @@ +"""subtitle 字幕时间轴领域模型单测.""" + +import pytest + +from domain.subtitle import SubtitleSegment, SubtitleTimeline, SubtitleWord + +# ── SubtitleWord ───────────────────────────────────────────────────────────── + + +class TestSubtitleWord: + """SubtitleWord 词级字幕单元""" + + def test_basic(self): + w = SubtitleWord(text="你好", start=1.0, end=1.5) + assert w.text == "你好" + assert w.start == 1.0 + assert w.end == 1.5 + + def test_duration(self): + w = SubtitleWord(text="test", start=0.0, end=2.5) + assert w.duration == 2.5 + + def test_duration_zero(self): + w = SubtitleWord(text="x", start=5.0, end=5.0) + assert w.duration == 0.0 + + def test_duration_negative_becomes_zero(self): + w = SubtitleWord(text="x", start=3.0, end=2.0) + assert w.duration == 0.0 + + +# ── SubtitleSegment ────────────────────────────────────────────────────────── + + +class TestSubtitleSegment: + """SubtitleSegment 字幕片段""" + + def test_basic(self): + s = SubtitleSegment(text="你好世界", start=0.0, end=2.0) + assert s.text == "你好世界" + assert s.start == 0.0 + assert s.end == 2.0 + assert s.words == [] + + def test_with_words(self): + words = [ + SubtitleWord("你好", 0.0, 0.5), + SubtitleWord("世界", 0.5, 1.0), + ] + s = SubtitleSegment(text="你好世界", start=0.0, end=1.0, words=words) + assert len(s.words) == 2 + assert s.words[0].text == "你好" + + def test_duration(self): + s = SubtitleSegment(text="test", start=1.5, end=3.5) + assert s.duration == 2.0 + + def test_duration_negative_becomes_zero(self): + s = SubtitleSegment(text="test", start=5.0, end=3.0) + assert s.duration == 0.0 + + def test_char_count(self): + s = SubtitleSegment(text="你好世界", start=0, end=1) + assert s.char_count == 4 + + def test_char_count_empty(self): + s = SubtitleSegment(text="", start=0, end=1) + assert s.char_count == 0 + + def test_char_count_mixed(self): + s = SubtitleSegment(text="Hello 世界", start=0, end=1) + assert s.char_count == 8 # H-e-l-l-o- -世-界 + + +# ── SubtitleTimeline 基础 ──────────────────────────────────────────────────── + + +class TestSubtitleTimelineBasics: + """SubtitleTimeline 基础属性""" + + def test_defaults(self): + tl = SubtitleTimeline() + assert tl.segments == [] + assert tl.language == "zh" + assert tl.total_duration == 0.0 + + def test_custom_language(self): + tl = SubtitleTimeline(language="en") + assert tl.language == "en" + + def test_segment_count(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("a", 0, 1), + SubtitleSegment("b", 1, 2), + ] + ) + assert tl.segment_count == 2 + + def test_segment_count_empty(self): + tl = SubtitleTimeline() + assert tl.segment_count == 0 + + def test_total_chars(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("你好", 0, 1), + SubtitleSegment("世界", 1, 2), + ] + ) + assert tl.total_chars == 4 + + def test_total_chars_empty(self): + tl = SubtitleTimeline() + assert tl.total_chars == 0 + + +# ── merge_short_segments ───────────────────────────────────────────────────── + + +class TestMergeShortSegments: + """merge_short_segments 合并过短片段""" + + def test_empty_timeline(self): + tl = SubtitleTimeline() + result = tl.merge_short_segments() + assert result.segment_count == 0 + + def test_single_segment(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("短", 0, 1), + ] + ) + result = tl.merge_short_segments(min_chars=8) + assert result.segment_count == 1 + assert result.segments[0].text == "短" + + def test_two_short_segments_merged(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("你好", 0, 1), # 2 + SubtitleSegment("世界", 1, 2), # 2 + ] + ) + result = tl.merge_short_segments(min_chars=3) + assert result.segment_count == 1 + assert result.segments[0].text == "你好世界" + assert result.segments[0].start == 0.0 + assert result.segments[0].end == 2.0 + + def test_multiple_short_merged(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("一", 0, 0.5), # 1 + SubtitleSegment("二", 0.5, 1.0), # 1 + SubtitleSegment("三", 1.0, 1.5), # 1 + SubtitleSegment("四", 1.5, 2.0), # 1 + SubtitleSegment("五", 2.0, 2.5), # 1 + SubtitleSegment("六七八", 2.5, 3.5), # 3 + SubtitleSegment("八九十", 3.5, 4.5), # 3 + ] + ) + result = tl.merge_short_segments(min_chars=5) + # 一二三四五 5个=5 → 合并为1段 + # 六七八+八九十 3+3=6 → 合并为1段 + assert result.segment_count == 2 + assert result.segments[0].text == "一二三四五" + assert result.segments[1].text == "六七八八九十" + + def test_long_segment_stays_alone(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("这是一段很长的字幕内容", 0, 2), # 11 + SubtitleSegment("短", 2, 2.5), # 1 + SubtitleSegment("语", 2.5, 3.0), # 1 + ] + ) + result = tl.merge_short_segments(min_chars=8) + # 第一段11字>=8,单独输出;后两段加起来2字<8,合并到上一段 + assert result.segment_count == 1 + assert result.segments[0].text == "这是一段很长的字幕内容短语" + + def test_tail_short_merged_with_previous(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("一二三四五六七八", 0, 2), # 8 + SubtitleSegment("尾", 2, 2.5), # 1,太短了 + ] + ) + result = tl.merge_short_segments(min_chars=5) + assert result.segment_count == 1 + assert result.segments[0].text == "一二三四五六七八尾" + + def test_preserves_language_and_duration(self): + tl = SubtitleTimeline( + segments=[SubtitleSegment("a", 0, 1)], + language="en", + total_duration=10.0, + ) + result = tl.merge_short_segments() + assert result.language == "en" + assert result.total_duration == 10.0 + + def test_default_min_chars_is_8(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("一二三四五", 0, 1), # 5 < 8 + SubtitleSegment("六七八", 1, 2), # 3 → 5+3=8 + ] + ) + result = tl.merge_short_segments() + assert result.segment_count == 1 + + def test_merges_words(self): + words1 = [SubtitleWord("你", 0.0, 0.3), SubtitleWord("好", 0.3, 0.6)] + words2 = [SubtitleWord("世", 1.0, 1.3), SubtitleWord("界", 1.3, 1.6)] + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("你好", 0.0, 0.6, words=words1), + SubtitleSegment("世界", 1.0, 1.6, words=words2), + ] + ) + result = tl.merge_short_segments(min_chars=3) + assert result.segment_count == 1 + assert len(result.segments[0].words) == 4 + assert result.segments[0].words[0].text == "你" + assert result.segments[0].words[3].text == "界" + + def test_does_not_mutate_original(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("a", 0, 1), + SubtitleSegment("b", 1, 2), + ] + ) + original_count = tl.segment_count + tl.merge_short_segments(min_chars=5) + assert tl.segment_count == original_count + + +# ── split_long_segments ────────────────────────────────────────────────────── + + +class TestSplitLongSegments: + """split_long_segments 拆分过长片段""" + + def test_short_segment_no_split(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("短文本", 0, 1), + ] + ) + result = tl.split_long_segments(max_chars=20) + assert result.segment_count == 1 + assert result.segments[0].text == "短文本" + + def test_empty_timeline(self): + tl = SubtitleTimeline() + result = tl.split_long_segments() + assert result.segment_count == 0 + + def test_split_by_sentence_end(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment( + "这是第一句话。这是第二句话。这是第三句话。", + start=0.0, + end=9.0, + ), + ] + ) + result = tl.split_long_segments(max_chars=10) + assert result.segment_count >= 2 + # 第一句应该是完整的 + assert result.segments[0].text.endswith("。") + + def test_split_preserves_total_text(self): + original = "这是第一句话。这是第二句话。这是第三句话,很长的一句话。" + tl = SubtitleTimeline( + segments=[ + SubtitleSegment(original, start=0.0, end=10.0), + ] + ) + result = tl.split_long_segments(max_chars=8) + # 拆分后所有片段拼起来应该等于原文 + combined = "".join(s.text for s in result.segments) + assert combined == original + + def test_split_time_proportional(self): + text = "一二三四五六七八九十。一二三四五六七八九十。" + tl = SubtitleTimeline( + segments=[ + SubtitleSegment(text=text, start=0.0, end=10.0), + ] + ) + result = tl.split_long_segments(max_chars=12) + assert result.segment_count >= 2 + # 第一段结束时间应该早于总时长 + assert result.segments[0].end < 10.0 + # 最后一段结束应该等于原结束时间 + assert abs(result.segments[-1].end - 10.0) < 0.01 + + def test_no_punctuation_hard_split(self): + text = "一二三四五六七八九十一二三四五六七八九十一二三四五" + tl = SubtitleTimeline( + segments=[ + SubtitleSegment(text=text, start=0.0, end=10.0), + ] + ) + result = tl.split_long_segments(max_chars=10) + assert result.segment_count >= 3 + combined = "".join(s.text for s in result.segments) + assert combined == text + + def test_multiple_mixed_segments(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("短", 0, 1), + SubtitleSegment("这是一段非常非常长的字幕文本内容需要拆分", 1, 5), + SubtitleSegment("短的", 5, 6), + ] + ) + result = tl.split_long_segments(max_chars=10) + # 第一个和第三个保持不变,中间被拆分 + assert result.segment_count > 3 + assert result.segments[0].text == "短" + assert result.segments[-1].text == "短的" + + def test_preserves_language_and_total_duration(self): + tl = SubtitleTimeline( + segments=[SubtitleSegment("a" * 30, 0, 10)], + language="ja", + total_duration=20.0, + ) + result = tl.split_long_segments(max_chars=10) + assert result.language == "ja" + assert result.total_duration == 20.0 + + def test_default_max_chars_is_20(self): + text = "一" * 25 + tl = SubtitleTimeline( + segments=[ + SubtitleSegment(text=text, start=0, end=5), + ] + ) + result = tl.split_long_segments() + assert result.segment_count >= 2 + + def test_split_with_words(self): + words = [SubtitleWord(f"w{i}", i * 0.5, i * 0.5 + 0.4) for i in range(20)] + text = "".join(w.text for w in words) + tl = SubtitleTimeline( + segments=[ + SubtitleSegment(text=text, start=0.0, end=10.0, words=words), + ] + ) + result = tl.split_long_segments(max_chars=10) + assert result.segment_count >= 2 + # 所有片段的词数之和应该等于原词数 + total_words = sum(len(s.words) for s in result.segments) + assert total_words <= len(words) + 1 # 可能有边界误差 + + def test_does_not_mutate_original(self): + tl = SubtitleTimeline( + segments=[ + SubtitleSegment("a" * 30, 0, 10), + ] + ) + original_count = tl.segment_count + tl.split_long_segments(max_chars=10) + assert tl.segment_count == original_count + + +# ── _split_text_by_punctuation 静态方法 ───────────────────────────────────── + + +class TestSplitTextByPunctuation: + """_split_text_by_punctuation 静态方法""" + + def test_short_text_no_split(self): + result = SubtitleTimeline._split_text_by_punctuation("短文本", max_chars=20) + assert len(result) == 1 + assert result[0] == "短文本" + + def test_sentence_end_punctuation_split(self): + result = SubtitleTimeline._split_text_by_punctuation( + "第一句。第二句。第三句。", + max_chars=5, + ) + assert len(result) >= 2 + assert result[0] == "第一句。" + + def test_clause_pause_punctuation(self): + result = SubtitleTimeline._split_text_by_punctuation( + "今天天气很好,阳光明媚,适合出去玩。", + max_chars=8, + ) + assert len(result) >= 2 + + def test_exclamation_mark(self): + result = SubtitleTimeline._split_text_by_punctuation( + "太精彩了!真的很棒!", + max_chars=5, + ) + assert len(result) >= 2 + + def test_question_mark(self): + result = SubtitleTimeline._split_text_by_punctuation( + "你是谁?从哪里来?", + max_chars=5, + ) + assert len(result) >= 2 + + def test_english_punctuation(self): + result = SubtitleTimeline._split_text_by_punctuation( + "Hello, world! How are you?", + max_chars=10, + ) + assert len(result) >= 2 + + def test_no_punctuation_hard_split(self): + text = "一" * 25 + result = SubtitleTimeline._split_text_by_punctuation(text, max_chars=10) + assert len(result) >= 3 + assert "".join(result) == text + + def test_empty_string(self): + result = SubtitleTimeline._split_text_by_punctuation("", max_chars=10) + assert len(result) == 0 or (len(result) == 1 and result[0] == "") + + def test_semicolon_colon(self): + result = SubtitleTimeline._split_text_by_punctuation( + "注意事项:第一,要认真;第二,要仔细。", + max_chars=8, + ) + assert len(result) >= 2 + + +# ── _merge_segments 静态方法 ──────────────────────────────────────────────── + + +class TestMergeSegmentsStatic: + """_merge_segments 静态方法""" + + def test_empty_list(self): + result = SubtitleTimeline._merge_segments([]) + assert result.text == "" + assert result.start == 0 + assert result.end == 0 + + def test_single_segment(self): + seg = SubtitleSegment("hello", 1.0, 2.0) + result = SubtitleTimeline._merge_segments([seg]) + assert result.text == "hello" + assert result.start == 1.0 + assert result.end == 2.0 + + def test_two_segments(self): + s1 = SubtitleSegment("你好", 0.0, 1.0) + s2 = SubtitleSegment("世界", 1.0, 2.0) + result = SubtitleTimeline._merge_segments([s1, s2]) + assert result.text == "你好世界" + assert result.start == 0.0 + assert result.end == 2.0 + + def test_merges_words(self): + w1 = [SubtitleWord("你", 0, 0.5)] + w2 = [SubtitleWord("好", 0.5, 1.0)] + s1 = SubtitleSegment("你", 0, 0.5, words=w1) + s2 = SubtitleSegment("好", 0.5, 1.0, words=w2) + result = SubtitleTimeline._merge_segments([s1, s2]) + assert len(result.words) == 2 + assert result.words[0].text == "你" + assert result.words[1].text == "好" + + +# ── 端到端:先合并再拆分 ──────────────────────────────────────────────────── + + +class TestMergeAndSplit: + """合并和拆分组合使用""" + + def test_merge_then_split_roundtrip(self): + # 很多短句先合并,再按合理长度拆分 + segments = [ + SubtitleSegment("你好", 0, 0.5), + SubtitleSegment("我是小明", 0.5, 1.5), + SubtitleSegment("今天天气真好。", 1.5, 3.0), + SubtitleSegment("我们出去玩吧。", 3.0, 5.0), + ] + tl = SubtitleTimeline(segments=segments) + merged = tl.merge_short_segments(min_chars=5) + split = merged.split_long_segments(max_chars=15) + # 结果应该合理(不保证完全一样,但文本应该完整) + original_text = "".join(s.text for s in segments) + result_text = "".join(s.text for s in split.segments) + assert original_text == result_text -- 2.54.0 From 9603ce2b7ef2fcf338e4b1f5bd8af7dc16fbb987 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 29 Jul 2026 10:49:51 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/domain/test_subtitle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/domain/test_subtitle.py b/tests/unit/domain/test_subtitle.py index 16f506064..c5f9fa6da 100755 --- a/tests/unit/domain/test_subtitle.py +++ b/tests/unit/domain/test_subtitle.py @@ -1,7 +1,6 @@ """subtitle 字幕时间轴领域模型单测.""" import pytest - from domain.subtitle import SubtitleSegment, SubtitleTimeline, SubtitleWord # ── SubtitleWord ───────────────────────────────────────────────────────────── -- 2.54.0