tts-voice-creator-clone-and.../tests/test_ssml_lite.py
mARTin-B78 8b51487a64 feat(tts): inline pause, spell and speed tags
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:52:23 +02:00

85 lines
3.5 KiB
Python

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()