OmniVoice-Studio / tests /test_subtitle_segmenter.py
LΓͺ Phi Nam
Deploy to HF Space
94004a2
Raw
History Blame Contribute Delete
7.55 kB
"""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