Spaces:
Running
Running
| """Unit tests for services/subtitle_segmenter.py β Phase 1.2.""" | |
| import pytest | |
| import os | |
| os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1") | |
| from services.subtitle_segmenter import ( | |
| segment_for_subtitles, | |
| format_subtitle_lines, | |
| MAX_CHARS_PER_LINE, | |
| MAX_CHARS_TOTAL, | |
| MAX_CPS, | |
| ) | |
| # ββ Pass-throughs (segment already fits) βββββββββββββββββββββββββββββββββββββ | |
| def test_short_segment_unchanged(): | |
| segs = [{"start": 0.0, "end": 2.0, "text": "Hello there."}] | |
| out = segment_for_subtitles(segs) | |
| assert out == [{"start": 0.0, "end": 2.0, "text": "Hello there."}] | |
| def test_preserves_extra_keys(): | |
| segs = [{ | |
| "start": 0.0, "end": 2.0, "text": "Hello.", | |
| "speaker_id": "SPK-01", "id": "s001", "text_original": "Hello.", | |
| }] | |
| out = segment_for_subtitles(segs) | |
| assert out[0]["speaker_id"] == "SPK-01" | |
| assert out[0]["id"] == "s001" | |
| assert out[0]["text_original"] == "Hello." | |
| # ββ Sentence-level splits ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_splits_at_sentence_boundary(): | |
| text = "This is the first sentence. This is the second one." * 2 | |
| segs = [{"start": 0.0, "end": 6.0, "text": text}] | |
| out = segment_for_subtitles(segs) | |
| assert len(out) > 1 | |
| # No piece exceeds the total char cap. | |
| for s in out: | |
| assert len(s["text"]) <= MAX_CHARS_TOTAL, s["text"] | |
| # Splits land at sentence ends β "." should close each segment. | |
| assert all(s["text"].rstrip().endswith(".") for s in out) | |
| # ββ Clause-level splits when no sentence terminator ββββββββββββββββββββββββββ | |
| def test_splits_at_clause_boundary(): | |
| text = "The quick brown fox, jumping over fences and dodging rocks, evaded the lazy dog completely." | |
| segs = [{"start": 0.0, "end": 5.0, "text": text}] | |
| out = segment_for_subtitles(segs) | |
| assert len(out) >= 2 | |
| for s in out: | |
| assert len(s["text"]) <= MAX_CHARS_TOTAL | |
| def test_splits_at_conjunction(): | |
| # Long enough to force a split (> MAX_CHARS_TOTAL = 84). | |
| text = ( | |
| "I walked down the hill that winds through the forest and then I saw " | |
| "a very large friendly dog that wagged its tail at me." | |
| ) | |
| segs = [{"start": 0.0, "end": 6.0, "text": text}] | |
| out = segment_for_subtitles(segs) | |
| # The word "and" should be a valid split point. | |
| assert len(out) >= 2 | |
| for s in out: | |
| assert len(s["text"]) <= MAX_CHARS_TOTAL | |
| # ββ CPS enforcement ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_cps_enforcement(): | |
| # 35 chars in 1 second = 35 CPS β above the 17 limit; segmenter must split | |
| # OR flag it. Under the rule, any segment above MAX_CPS should be split | |
| # unless it's already a single word. | |
| text = "A cascade of rapidly-delivered syllables sprinted." | |
| segs = [{"start": 0.0, "end": 1.0, "text": text}] | |
| out = segment_for_subtitles(segs) | |
| # At least one split; no piece both over MAX_CHARS_TOTAL AND over MAX_CPS. | |
| for s in out: | |
| dur = max(1e-3, s["end"] - s["start"]) | |
| assert (len(s["text"]) <= MAX_CHARS_TOTAL) or (len(s["text"]) / dur <= MAX_CPS) | |
| # ββ Word-level timings βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_uses_word_timings_when_present(): | |
| # Long enough to force a split; the clause comma is the prime candidate, | |
| # and with word timings the split should land right at the comma's time. | |
| words = [ | |
| {"text": "This", "start": 0.0, "end": 0.2}, | |
| {"text": "is", "start": 0.2, "end": 0.4}, | |
| {"text": "an", "start": 0.4, "end": 0.55}, | |
| {"text": "extended", "start": 0.55, "end": 1.0}, | |
| {"text": "monologue,", "start": 1.0, "end": 1.8}, | |
| {"text": "spoken", "start": 1.8, "end": 2.1}, | |
| {"text": "with", "start": 2.1, "end": 2.3}, | |
| {"text": "deliberate", "start": 2.3, "end": 2.9}, | |
| {"text": "pacing", "start": 2.9, "end": 3.3}, | |
| {"text": "to", "start": 3.3, "end": 3.4}, | |
| {"text": "illustrate", "start": 3.4, "end": 4.0}, | |
| {"text": "clause", "start": 4.0, "end": 4.4}, | |
| {"text": "splitting.", "start": 4.4, "end": 5.0}, | |
| ] | |
| text = " ".join(w["text"] for w in words) | |
| segs = [{"start": 0.0, "end": 5.0, "text": text, "words": words}] | |
| out = segment_for_subtitles(segs) | |
| # Two segments, split at the comma around 1.8s. | |
| assert len(out) >= 2 | |
| assert 1.5 <= out[0]["end"] <= 2.0 | |
| # Last segment should end exactly at the original 5.0s. | |
| assert abs(out[-1]["end"] - 5.0) < 0.1 | |
| # ββ Merger: tiny segments fold up ββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_merges_tiny_neighbour(): | |
| segs = [ | |
| {"start": 0.0, "end": 1.5, "text": "A long enough starter."}, | |
| {"start": 1.5, "end": 1.8, "text": "Ok."}, # tiny | |
| ] | |
| out = segment_for_subtitles(segs) | |
| # "Ok." should merge into the starter because its duration < 1.2s and it's | |
| # short. But we don't merge across sentence terminators β so this stays. | |
| # ("A long enough starter." ends in '.') | |
| assert len(out) == 2 # respected sentence boundary | |
| def test_merges_tiny_into_neighbour_when_no_sentence_break(): | |
| segs = [ | |
| {"start": 0.0, "end": 0.6, "text": "Um"}, # tiny + no terminator | |
| {"start": 0.6, "end": 2.5, "text": "let me think about it."}, | |
| ] | |
| out = segment_for_subtitles(segs) | |
| assert len(out) == 1 | |
| assert out[0]["text"].startswith("Um") | |
| def test_does_not_merge_across_speakers(): | |
| segs = [ | |
| {"start": 0.0, "end": 0.6, "text": "Yes", "speaker_id": "A"}, | |
| {"start": 0.6, "end": 1.2, "text": "no thanks.", "speaker_id": "B"}, | |
| ] | |
| out = segment_for_subtitles(segs) | |
| assert len(out) == 2 | |
| assert out[0]["speaker_id"] == "A" | |
| assert out[1]["speaker_id"] == "B" | |
| # ββ format_subtitle_lines ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_format_single_line_fits(): | |
| lines = format_subtitle_lines("Hello world.") | |
| assert lines == ["Hello world."] | |
| def test_format_wraps_to_two_lines(): | |
| long = "A long sentence that definitely will not fit in forty-two chars total length." | |
| lines = format_subtitle_lines(long, max_chars=MAX_CHARS_PER_LINE) | |
| assert 1 <= len(lines) <= 2 | |
| assert all(len(l) <= MAX_CHARS_PER_LINE or l == lines[-1] for l in lines) | |
| # ββ No-op edge cases βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_empty_input(): | |
| assert segment_for_subtitles([]) == [] | |
| def test_single_word_rumble_accepted(): | |
| # A 30-char single word in 1s is 30 CPS β over limit, but un-splittable. | |
| segs = [{"start": 0.0, "end": 1.0, "text": "Antidisestablishmentarianism!"}] | |
| out = segment_for_subtitles(segs) | |
| assert len(out) == 1 # no natural cut available | |