import io import unittest from pydub import AudioSegment from pydub.generators import Sine from core import ssml_lite as S from core.magazine import split_sentences_for_tts def texts(segs): return [(s["text"], s["speed"], s["pause_ms"]) for s in segs] class ParseTests(unittest.TestCase): def test_plain_text_is_one_segment(self): self.assertEqual(texts(S.parse("Hello there.")), [("Hello there.", 1.0, 0)]) def test_pause_units_and_split(self): self.assertEqual(texts(S.parse("A [pause 500ms] B [pause 1.2s] C")), [("A ", 1.0, 0), ("", 1.0, 500), (" B ", 1.0, 0), ("", 1.0, 1200), (" C", 1.0, 0)]) def test_pause_caps(self): self.assertEqual(S.parse("A[pause 99s]B")[1]["pause_ms"], 10_000) many = "A" + "[pause 10s]B" * 10 self.assertEqual(sum(s["pause_ms"] for s in S.parse(many)), 60_000) def test_bad_pause_is_stripped_not_spoken(self): self.assertEqual(S.plain_text("A [pause soon] B"), "A B") def test_speed_nesting_innermost_wins(self): segs = S.parse("a [slow]b [fast]c[/fast] d[/slow] e") self.assertEqual([(s["text"].strip(), s["speed"]) for s in segs], [("a", 1.0), ("b", 0.85), ("c", 1.15), ("d", 0.85), ("e", 1.0)]) def test_unclosed_runs_to_paragraph_end(self): segs = S.parse("[slow]one\n\ntwo") self.assertEqual([(s["text"].strip(), s["speed"]) for s in segs], [("one", 0.85), ("two", 1.0)]) def test_unknown_control_tags_stripped_citations_kept(self): self.assertEqual(S.plain_text("Hi [laugh] there [/emphasis] [sic] cited [1]."), "Hi there [sic] cited [1].") self.assertNotIn("[", S.plain_text("[whisper]psst[/whisper]")) def test_spell_english_and_german(self): self.assertEqual(S.plain_text("[spell]USA[/spell]", "en"), "U S A") self.assertEqual(S.plain_text("[spell]USA[/spell]", "de"), "Uh Ess Ah") self.assertEqual(S.plain_text("[spell]ZDF[/spell] Nachrichten über Straße"), "Zett De Eff Nachrichten über Straße") self.assertEqual(S.plain_text("[spell]ab", "en"), "a b") def test_segments_never_contain_tags_and_chunker_keeps_them_whole(self): text = "[slow]First. Second sentence here.[/slow] Third. [pause 1s] Fourth." for seg in S.parse(text): self.assertNotIn("[", seg["text"]) slow = [s for s in S.parse(text) if s["speed"] != 1.0][0] self.assertEqual(slow["speed"], 0.85) self.assertIn("First.", slow["text"]) self.assertIn("Second", slow["text"]) self.assertGreaterEqual(len(split_sentences_for_tts(slow["text"])), 1) def test_has_tags(self): self.assertTrue(S.has_tags("a [pause 1s]")) self.assertFalse(S.has_tags("see [1] and [sic]")) class RenderTests(unittest.TestCase): def test_render_joins_with_silence_and_stretch(self): tone = Sine(440).to_audio_segment(duration=1000).set_frame_rate(24000) buf = io.BytesIO() tone.export(buf, format="wav") wav = buf.getvalue() def tempo(b, speed): # fake: just trims, proves it was called with the factor tempo.calls.append(speed) return b tempo.calls = [] out, media = S.render("A [pause 500ms] [slow]B[/slow]", lambda t: (wav, "audio/wav"), tempo, "en") dur = len(AudioSegment.from_file(io.BytesIO(out), format="wav")) self.assertEqual(media, "audio/wav") self.assertEqual(tempo.calls, [0.85]) self.assertAlmostEqual(dur, 2500, delta=30) if __name__ == "__main__": unittest.main()