85 lines
3.5 KiB
Python
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()
|