SRT conversion previously rejected otherwise valid word timestamps when a start preceded the previous word's start. Clamp regressing starts to the preceding nonempty word's start and extend ends only when necessary. Preserve transcript order and the original response while continuing to reject malformed timestamps. Document the adjustment and cover regressions, cue boundaries, blank words, and invalid input. Test Plan: - python3 -B -m unittest -v: all eight tests passed. - CLI conversion of a synthetic regressing response: passed. - git diff --cached --check: passed. - Actual recording verification blocked by unavailable 1Password auth.
118 lines
4.6 KiB
Python
118 lines
4.6 KiB
Python
import copy
|
|
import unittest
|
|
|
|
from subtitles import convert, timestamp
|
|
|
|
|
|
class SubtitleTests(unittest.TestCase):
|
|
def test_minute_long_segment_becomes_short_cues(self):
|
|
text = (
|
|
"Okay, all right, brilliant. Oh dude, we've got it this time. "
|
|
"You know what? I can feel it. Okay, we found the longer self "
|
|
"tappers that are for these little SSD cooling fans. "
|
|
) * 4
|
|
tokens = text.split()
|
|
words = [
|
|
{"word": word, "start": 971.68 + i * 0.4, "end": 971.68 + i * 0.4 + 0.35}
|
|
for i, word in enumerate(tokens)
|
|
]
|
|
result = convert(
|
|
{
|
|
"text": text,
|
|
"words": words,
|
|
"segments": [{"text": text, "start": 971.68, "end": 1037.42}],
|
|
}
|
|
)
|
|
cues = result.strip().split("\n\n")
|
|
self.assertGreater(len(cues), 10)
|
|
recovered = []
|
|
for index, cue in enumerate(cues, 1):
|
|
number, timing, *body = cue.splitlines()
|
|
self.assertEqual(int(number), index)
|
|
self.assertLessEqual(len(body), 2)
|
|
self.assertTrue(all(len(line) <= 42 for line in body))
|
|
|
|
def seconds(value):
|
|
h, m, s = value.replace(",", ".").split(":")
|
|
return int(h) * 3600 + int(m) * 60 + float(s)
|
|
|
|
start, end = map(seconds, timing.split(" --> "))
|
|
self.assertLessEqual(end - start, 6.001)
|
|
recovered.extend(" ".join(body).split())
|
|
self.assertEqual(recovered, tokens)
|
|
self.assertIn("00:16:11,680", result)
|
|
|
|
def test_pause_sentence_and_speaker_boundaries(self):
|
|
result = convert(
|
|
{
|
|
"words": [
|
|
{"word": "First sentence.", "start": 0, "end": 1.2, "speaker": 0},
|
|
{"word": "Next", "start": 1.3, "end": 1.6, "speaker": 0},
|
|
{"word": "pause", "start": 3, "end": 3.4, "speaker": 0},
|
|
{"word": "speaker", "start": 3.5, "end": 4, "speaker": 1},
|
|
]
|
|
}
|
|
)
|
|
self.assertEqual(len(result.strip().split("\n\n")), 4)
|
|
|
|
def test_missing_and_invalid_timestamps(self):
|
|
for response in (
|
|
{"text": "speech"},
|
|
{"text": "speech", "words": []},
|
|
{"words": [{"word": "bad", "start": 2, "end": 1}]},
|
|
{"words": [{"word": "bad", "start": float("nan"), "end": 1}]},
|
|
{"words": [{"word": "bad", "start": -1, "end": 1}]},
|
|
{"words": [{"word": "bad", "start": 0, "end": float("inf")}]},
|
|
{"words": [{"word": "bad", "start": True, "end": 1}]},
|
|
{"error": {"message": "failed"}},
|
|
):
|
|
with self.assertRaises(ValueError):
|
|
convert(response)
|
|
|
|
def test_regressing_timestamps_preserve_text_and_response(self):
|
|
response = {"words": [
|
|
{"word": "Glasfaser", "start": 10, "end": 10.4},
|
|
{"word": "mit", "start": 9.8, "end": 10.2},
|
|
{"word": "10", "start": 9.7, "end": 9.9},
|
|
{"word": "Gbit/s", "start": 10.5, "end": 11},
|
|
]}
|
|
original = copy.deepcopy(response)
|
|
self.assertEqual(
|
|
convert(response),
|
|
"1\n00:00:10,000 --> 00:00:11,000\nGlasfaser mit 10 Gbit/s\n\n",
|
|
)
|
|
self.assertEqual(response, original)
|
|
|
|
def test_regression_across_cues_and_zero_duration(self):
|
|
self.assertEqual(convert({"words": [
|
|
{"word": "First", "start": 10, "end": 11, "speaker": 0},
|
|
{"word": "Second", "start": 9, "end": 9.5, "speaker": 1},
|
|
{"word": "Third", "start": 12, "end": 13, "speaker": 0},
|
|
]}), (
|
|
"1\n00:00:10,000 --> 00:00:11,000\nFirst\n\n"
|
|
"2\n00:00:10,000 --> 00:00:10,001\nSecond\n\n"
|
|
"3\n00:00:12,000 --> 00:00:13,000\nThird\n\n"
|
|
))
|
|
|
|
def test_blank_words_do_not_shift_timing(self):
|
|
self.assertEqual(convert({"words": [
|
|
{"word": " ", "start": 20, "end": 21},
|
|
{"word": "Speech", "start": 10, "end": 11},
|
|
]}), "1\n00:00:10,000 --> 00:00:11,000\nSpeech\n\n")
|
|
|
|
def test_regression_does_not_hide_invalid_duration(self):
|
|
with self.assertRaises(ValueError):
|
|
convert({"words": [
|
|
{"word": "valid", "start": 10, "end": 11},
|
|
{"word": "invalid", "start": 9, "end": 8},
|
|
]})
|
|
|
|
def test_empty_and_rounding(self):
|
|
self.assertEqual(convert({"words": [], "text": ""}), "")
|
|
self.assertEqual(timestamp(59.9996), "00:01:00,000")
|
|
self.assertEqual(timestamp(3601.234), "01:00:01,234")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|