feat(tree): 字幕模块 — SRT 解析 + 完整性检查 + Voronoi 分配

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 01:54:09 -04:00
parent e3b027ce34
commit fb6f9964d8
2 changed files with 426 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
"""字幕模块单元测试。"""
from __future__ import annotations
from app.tree.subtitle import (
SRTEntry,
assign_subtitles_voronoi,
check_subtitle_completeness,
extract_subtitle_for_range,
parse_srt,
)
_SAMPLE_SRT = """\
1
00:00:01,000 --> 00:00:03,500
Hello world.
2
00:00:05,000 --> 00:00:08,000
<i>This is italic</i> text.
3
00:00:10,000 --> 00:00:12,000
Final line.
"""
class TestParseSrt:
def test_basic_parse(self, tmp_path):
srt_file = tmp_path / "test.srt"
srt_file.write_text(_SAMPLE_SRT, encoding="utf-8")
entries = parse_srt(str(srt_file))
assert len(entries) == 3
assert entries[0] == SRTEntry(start=1.0, end=3.5, text="Hello world.")
assert entries[1].text == "This is italic text."
def test_empty_srt(self, tmp_path):
srt_file = tmp_path / "empty.srt"
srt_file.write_text("", encoding="utf-8")
entries = parse_srt(str(srt_file))
assert entries == []
def test_malformed_srt_skips_bad_blocks(self, tmp_path):
bad_srt = "garbage\n\n1\n00:00:01,000 --> 00:00:02,000\nGood line.\n"
srt_file = tmp_path / "bad.srt"
srt_file.write_text(bad_srt, encoding="utf-8")
entries = parse_srt(str(srt_file))
assert len(entries) == 1
assert entries[0].text == "Good line."
class TestCompletenessCheck:
def test_good_coverage(self):
entries = [SRTEntry(0.0, 5.0, "a"), SRTEntry(5.0, 10.0, "b")]
report = check_subtitle_completeness(entries, duration=10.0, min_coverage=0.5)
assert report.usable is True
assert report.coverage_ratio >= 0.5
def test_poor_coverage(self):
entries = [SRTEntry(0.0, 1.0, "short")]
report = check_subtitle_completeness(entries, duration=100.0, min_coverage=0.3)
assert report.usable is False
def test_max_gap(self):
entries = [SRTEntry(0.0, 1.0, "a"), SRTEntry(50.0, 51.0, "b")]
report = check_subtitle_completeness(entries, duration=60.0)
assert report.max_gap_sec >= 49.0
class TestExtractForRange:
def test_overlap(self):
entries = [
SRTEntry(0.0, 5.0, "first"),
SRTEntry(4.0, 8.0, "second"),
SRTEntry(10.0, 12.0, "third"),
]
text = extract_subtitle_for_range(entries, (3.0, 9.0))
assert "first" in text
assert "second" in text
assert "third" not in text
class TestVoronoiAssign:
def test_assigns_to_l3_nodes(self):
from app.tree.index import (
IndexMeta,
L1Card,
L1Node,
L2Card,
L2Node,
L3Card,
L3Node,
TreeIndex,
)
l3_0 = L3Node(id="l1_0_l2_0_l3_0", card=L3Card("desc0", [], [], [], "", {}), timestamp=2.0)
l3_1 = L3Node(id="l1_0_l2_0_l3_1", card=L3Card("desc1", [], [], [], "", {}), timestamp=6.0)
l2 = L2Node(
id="l1_0_l2_0",
card=L2Card("evt", [], [], [], [], "", None),
time_range=(0.0, 10.0),
children=[l3_0, l3_1],
)
l1 = L1Node(
id="l1_0",
card=L1Card("scene", "", [], [], [], [], ""),
time_range=(0.0, 10.0),
children=[l2],
)
index = TreeIndex(metadata=IndexMeta("/test.mp4", "video"), roots=[l1])
entries = [SRTEntry(1.0, 3.0, "hello"), SRTEntry(5.0, 7.0, "world")]
assign_subtitles_voronoi(index, entries)
assert l3_0.subtitle is not None
assert "hello" in l3_0.subtitle
assert l3_1.subtitle is not None
assert "world" in l3_1.subtitle