5b967cf74d
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
330 lines
12 KiB
Python
Executable File
330 lines
12 KiB
Python
Executable File
"""字幕领域模型单元测试 - 纯逻辑部分。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from domain.subtitle import SubtitleSegment, SubtitleTimeline, SubtitleWord
|
|
|
|
|
|
class TestSubtitleWord:
|
|
"""单个词级别字幕单元。"""
|
|
|
|
def test_basic_creation(self):
|
|
word = SubtitleWord(text="你好", start=0.0, end=0.5)
|
|
assert word.text == "你好"
|
|
assert word.start == 0.0
|
|
assert word.end == 0.5
|
|
|
|
def test_duration_positive(self):
|
|
word = SubtitleWord(text="test", start=1.0, end=2.5)
|
|
assert word.duration == pytest.approx(1.5)
|
|
|
|
def test_duration_zero(self):
|
|
word = SubtitleWord(text="x", start=3.0, end=3.0)
|
|
assert word.duration == 0.0
|
|
|
|
def test_duration_negative_returns_zero(self):
|
|
"""end < start 时 duration 返回 0,不抛异常。"""
|
|
word = SubtitleWord(text="x", start=5.0, end=3.0)
|
|
assert word.duration == 0.0
|
|
|
|
|
|
class TestSubtitleSegment:
|
|
"""字幕段(一句话)。"""
|
|
|
|
def test_basic_creation(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.words == []
|
|
|
|
def test_duration_positive(self):
|
|
seg = SubtitleSegment(text="test", start=1.0, end=3.5)
|
|
assert seg.duration == pytest.approx(2.5)
|
|
|
|
def test_duration_zero(self):
|
|
seg = SubtitleSegment(text="x", start=5.0, end=5.0)
|
|
assert seg.duration == 0.0
|
|
|
|
def test_duration_negative_returns_zero(self):
|
|
seg = SubtitleSegment(text="x", start=10.0, end=5.0)
|
|
assert seg.duration == 0.0
|
|
|
|
def test_char_count(self):
|
|
seg = SubtitleSegment(text="你好世界", start=0, end=1)
|
|
assert seg.char_count == 4
|
|
|
|
def test_char_count_empty(self):
|
|
seg = SubtitleSegment(text="", start=0, end=1)
|
|
assert seg.char_count == 0
|
|
|
|
def test_char_count_mixed_languages(self):
|
|
seg = SubtitleSegment(text="你好hello世界", start=0, end=1)
|
|
assert seg.char_count == 9 # 2中 + 5英 + 2中 = 9
|
|
|
|
def test_with_words(self):
|
|
words = [
|
|
SubtitleWord(text="你好", start=0.0, end=0.5),
|
|
SubtitleWord(text="世界", start=0.5, end=1.0),
|
|
]
|
|
seg = SubtitleSegment(text="你好世界", start=0.0, end=1.0, words=words)
|
|
assert len(seg.words) == 2
|
|
assert seg.words[0].text == "你好"
|
|
|
|
|
|
class TestSubtitleTimelineBasics:
|
|
"""字幕时间轴基础属性。"""
|
|
|
|
def test_empty_timeline(self):
|
|
tl = SubtitleTimeline()
|
|
assert tl.segments == []
|
|
assert tl.language == "zh"
|
|
assert tl.total_duration == 0.0
|
|
assert tl.segment_count == 0
|
|
assert tl.total_chars == 0
|
|
|
|
def test_single_segment(self):
|
|
tl = SubtitleTimeline(
|
|
segments=[SubtitleSegment(text="你好", start=0, end=1)],
|
|
language="zh",
|
|
total_duration=1.0,
|
|
)
|
|
assert tl.segment_count == 1
|
|
assert tl.total_chars == 2
|
|
|
|
def test_multiple_segments(self):
|
|
segments = [
|
|
SubtitleSegment(text="第一句", start=0, end=1),
|
|
SubtitleSegment(text="第二句更长一点", start=1, end=3),
|
|
SubtitleSegment(text="第三句", start=3, end=4),
|
|
]
|
|
tl = SubtitleTimeline(segments=segments, total_duration=4.0)
|
|
assert tl.segment_count == 3
|
|
assert tl.total_chars == 3 + 7 + 3 # 13
|
|
|
|
|
|
class TestMergeShortSegments:
|
|
"""合并过短字幕片段。"""
|
|
|
|
def test_empty_timeline_unchanged(self):
|
|
tl = SubtitleTimeline()
|
|
result = tl.merge_short_segments()
|
|
assert result.segment_count == 0
|
|
|
|
def test_single_segment_unchanged(self):
|
|
tl = SubtitleTimeline(segments=[SubtitleSegment(text="短", start=0, end=0.5)])
|
|
result = tl.merge_short_segments()
|
|
assert result.segment_count == 1
|
|
assert result.segments[0].text == "短"
|
|
|
|
def test_all_short_merged_into_one(self):
|
|
"""多个短片段合并成一个。"""
|
|
segments = [
|
|
SubtitleSegment(text="一", start=0, end=0.2),
|
|
SubtitleSegment(text="二", start=0.2, end=0.4),
|
|
SubtitleSegment(text="三", start=0.4, end=0.6),
|
|
]
|
|
tl = SubtitleTimeline(segments=segments)
|
|
result = tl.merge_short_segments(min_chars=3)
|
|
assert result.segment_count == 1
|
|
assert result.segments[0].text == "一二三"
|
|
assert result.segments[0].start == 0
|
|
assert result.segments[0].end == 0.6
|
|
|
|
def test_mixed_lengths(self):
|
|
"""长短混合,中间短的会合并成一段。"""
|
|
segments = [
|
|
SubtitleSegment(text="这是比较长的第一句", start=0, end=2), # 10字
|
|
SubtitleSegment(text="第一小段", start=2, end=2.4), # 4字
|
|
SubtitleSegment(text="第二小段", start=2.4, end=2.8), # 4字
|
|
SubtitleSegment(text="这是比较长的第四句", start=2.8, end=5), # 10字
|
|
]
|
|
tl = SubtitleTimeline(segments=segments)
|
|
result = tl.merge_short_segments(min_chars=8)
|
|
# 第一句10字够长单独保留;短1+短2=8字刚好够一段;第四句10字够长单独保留
|
|
assert result.segment_count == 3
|
|
assert result.segments[0].text == "这是比较长的第一句"
|
|
assert result.segments[1].text == "第一小段第二小段"
|
|
assert result.segments[2].text == "这是比较长的第四句"
|
|
|
|
def test_preserves_language_and_duration(self):
|
|
segments = [
|
|
SubtitleSegment(text="a", start=0, end=0.1),
|
|
SubtitleSegment(text="b", start=0.1, end=0.2),
|
|
]
|
|
tl = SubtitleTimeline(segments=segments, language="en", total_duration=10.0)
|
|
result = tl.merge_short_segments(min_chars=5)
|
|
assert result.language == "en"
|
|
assert result.total_duration == 10.0
|
|
|
|
def test_last_short_merged_to_previous(self):
|
|
"""最后剩余的短片段且不够min_chars,合并到上一段。"""
|
|
segments = [
|
|
SubtitleSegment(text="这是一句比较长的话", start=0, end=1.5), # 10字
|
|
SubtitleSegment(text="尾", start=1.5, end=1.6), # 1字
|
|
]
|
|
tl = SubtitleTimeline(segments=segments)
|
|
result = tl.merge_short_segments(min_chars=8)
|
|
# 第一句够长(10>=8),但尾只有1字不够,合并到上一句
|
|
assert result.segment_count == 1
|
|
assert result.segments[0].text == "这是一句比较长的话尾"
|
|
|
|
def test_original_not_modified(self):
|
|
segments = [SubtitleSegment(text="a", start=0, end=0.1)]
|
|
tl = SubtitleTimeline(segments=segments)
|
|
tl.merge_short_segments(min_chars=5)
|
|
assert len(tl.segments) == 1 # 原对象不变
|
|
|
|
|
|
class TestSplitLongSegments:
|
|
"""拆分过长字幕片段。"""
|
|
|
|
def test_short_segments_unchanged(self):
|
|
segments = [
|
|
SubtitleSegment(text="短句", start=0, end=1),
|
|
SubtitleSegment(text="另一句", start=1, end=2),
|
|
]
|
|
tl = SubtitleTimeline(segments=segments)
|
|
result = tl.split_long_segments(max_chars=20)
|
|
assert result.segment_count == 2
|
|
assert result.segments[0].text == "短句"
|
|
|
|
def test_split_by_sentence_punctuation(self):
|
|
"""按句末标点拆分。"""
|
|
text = "这是第一句话。这是第二句话!这是第三句话?"
|
|
seg = SubtitleSegment(text=text, start=0, end=3.0)
|
|
tl = SubtitleTimeline(segments=[seg])
|
|
result = tl.split_long_segments(max_chars=8)
|
|
assert result.segment_count >= 2
|
|
# 合并起来应该等于原文
|
|
merged_text = "".join(s.text for s in result.segments)
|
|
assert merged_text == text
|
|
|
|
def test_split_time_proportional(self):
|
|
"""拆分后的时间按字数比例分配。"""
|
|
text = "一二三四五六七八。二二三四五六七八。"
|
|
seg = SubtitleSegment(text=text, start=0, end=10.0)
|
|
tl = SubtitleTimeline(segments=[seg])
|
|
result = tl.split_long_segments(max_chars=8)
|
|
assert len(result.segments) >= 2
|
|
# 总时长不超过原时长
|
|
assert result.segments[-1].end <= seg.end
|
|
# 第一个片段的开始时间正确
|
|
assert result.segments[0].start == 0.0
|
|
|
|
def test_no_punctuation_hard_split(self):
|
|
"""没有标点时硬切。"""
|
|
text = "一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十"
|
|
seg = SubtitleSegment(text=text, start=0, end=5.0)
|
|
tl = SubtitleTimeline(segments=[seg])
|
|
result = tl.split_long_segments(max_chars=10)
|
|
assert len(result.segments) >= 2
|
|
merged = "".join(s.text for s in result.segments)
|
|
assert merged == text
|
|
|
|
def test_preserves_language_and_duration(self):
|
|
seg = SubtitleSegment(text="a" * 30, start=0, end=5)
|
|
tl = SubtitleTimeline(segments=[seg], language="en", total_duration=100.0)
|
|
result = tl.split_long_segments(max_chars=10)
|
|
assert result.language == "en"
|
|
assert result.total_duration == 100.0
|
|
|
|
def test_empty_timeline_unchanged(self):
|
|
tl = SubtitleTimeline()
|
|
result = tl.split_long_segments()
|
|
assert result.segment_count == 0
|
|
|
|
|
|
class TestSplitTextByPunctuation:
|
|
"""_split_text_by_punctuation 静态方法。"""
|
|
|
|
def test_short_text_unchanged(self):
|
|
result = SubtitleTimeline._split_text_by_punctuation("你好", 10)
|
|
assert result == ["你好"]
|
|
|
|
def test_split_by_period(self):
|
|
text = "这是第一句话。这是第二句话。"
|
|
result = SubtitleTimeline._split_text_by_punctuation(text, 8)
|
|
assert len(result) == 2
|
|
assert result[0] == "这是第一句话。"
|
|
assert result[1] == "这是第二句话。"
|
|
|
|
def test_split_by_exclamation(self):
|
|
text = "你好世界大家好!再见世界朋友们!"
|
|
result = SubtitleTimeline._split_text_by_punctuation(text, 8)
|
|
assert len(result) >= 2
|
|
|
|
def test_split_by_question(self):
|
|
text = "今天天气好不好呢?今天天气很好呀。"
|
|
result = SubtitleTimeline._split_text_by_punctuation(text, 8)
|
|
assert len(result) >= 2
|
|
|
|
def test_split_by_comma_when_long(self):
|
|
"""超过max_chars时,遇到逗号也会断开。"""
|
|
text = "这是很长的一句话,中间有个逗号,后面还有内容继续。"
|
|
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
|
|
assert len(result) >= 2
|
|
|
|
def test_empty_text(self):
|
|
result = SubtitleTimeline._split_text_by_punctuation("", 10)
|
|
assert result == []
|
|
|
|
def test_no_punctuation_hard_cut(self):
|
|
text = "一二三四五六七八九十一二三四五六七八九十"
|
|
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
|
|
assert len(result) == 2
|
|
assert len(result[0]) == 10
|
|
assert len(result[1]) == 10
|
|
|
|
def test_english_punctuation(self):
|
|
result = SubtitleTimeline._split_text_by_punctuation("Hello, world! How are you?", 15)
|
|
assert len(result) >= 2
|
|
|
|
|
|
class TestMergeSegments:
|
|
"""_merge_segments 静态方法。"""
|
|
|
|
def test_merge_two_segments(self):
|
|
segs = [
|
|
SubtitleSegment(text="你好", start=0.0, end=0.5),
|
|
SubtitleSegment(text="世界", start=0.5, end=1.0),
|
|
]
|
|
result = SubtitleTimeline._merge_segments(segs)
|
|
assert result.text == "你好世界"
|
|
assert result.start == 0.0
|
|
assert result.end == 1.0
|
|
|
|
def test_merge_empty_list(self):
|
|
result = SubtitleTimeline._merge_segments([])
|
|
assert result.text == ""
|
|
assert result.start == 0
|
|
assert result.end == 0
|
|
|
|
def test_merge_single_segment(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_preserves_words(self):
|
|
segs = [
|
|
SubtitleSegment(
|
|
text="你好",
|
|
start=0.0,
|
|
end=0.5,
|
|
words=[SubtitleWord(text="你好", start=0.0, end=0.5)],
|
|
),
|
|
SubtitleSegment(
|
|
text="世界",
|
|
start=0.5,
|
|
end=1.0,
|
|
words=[SubtitleWord(text="世界", start=0.5, end=1.0)],
|
|
),
|
|
]
|
|
result = SubtitleTimeline._merge_segments(segs)
|
|
assert len(result.words) == 2
|
|
assert result.words[0].text == "你好"
|
|
assert result.words[1].text == "世界"
|