Files
xiaoxia-saas/tests/unit/test_subtitle_domain.py
T
CI Bot fe204a3fec
AI Code Review / AI Code Review (pull_request) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 31s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m0s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m31s
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 / PR Build Web Image (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 51s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 44s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m23s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m25s
CI/CD Pipeline / Frontend Unit Tests (pull_request) 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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m23s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m12s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
test: wave80 - module_registry深度 + subtitle_domain + voice_library补充 (+74)
- module_registry: 新增42个测试
  - Module状态转换(6个)
  - 注册补充场景(5个)
  - 能力查询补充(9个)
  - 依赖检查补充(4个)
  - list_modules补充(3个)
  - QuotaRule补充(3个)
  - ModuleCapability补充(3个)
- subtitle_domain: 新增19个测试
  - merge_short_segments补充(6个)
  - split_long_segments补充(5个)
  - _split_text_by_punctuation补充(8个)
- voice_library_domain: 新增13个测试
  - 各种status状态
  - 零值/大值边界
  - 标签、元数据、project_id
2026-07-25 15:23:48 +08:00

480 lines
19 KiB
Python
Executable File
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.
"""字幕领域模型单元测试."""
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