tts-voice-creator-clone-and.../aligner/tests/test_app.py
2026-10-03 00:37:40 +02:00

85 lines
3.7 KiB
Python

"""Validation and text-handling tests for the aligner; they never load the model."""
import io
import json
import os
import sys
import unittest
import wave
import numpy as np
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from fastapi.testclient import TestClient # noqa: E402
import app as aligner # noqa: E402
def wav_bytes(seconds=1.0, sr=16000):
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
w.writeframes((np.zeros(int(seconds * sr))).astype("<i2").tobytes())
return buf.getvalue()
class NormalizeTests(unittest.TestCase):
def test_romanizes_german(self):
self.assertEqual(aligner.normalize_word("Größe,"), "grosse")
self.assertEqual(aligner.normalize_word("Über"), "uber")
self.assertEqual(aligner.normalize_word("2024."), "")
self.assertEqual(aligner.normalize_word("don't"), "don't")
def test_digits_get_the_gap_between_neighbours(self):
fake = aligner.align_samples(np.zeros(16000, dtype=np.float32), ["2024", "3"])
self.assertEqual([(a, ok) for a, _, ok in fake], [(0.0, False), (0.0, False)])
self.assertAlmostEqual(fake[0][1], 1.0)
class ValidationTests(unittest.TestCase):
def setUp(self):
self.c = TestClient(aligner.app)
def post(self, url="/align", audio=None, **data):
files = {"audio": ("a.wav", audio if audio is not None else wav_bytes(), "audio/wav")}
return self.c.post(url, files=files, data=data)
def test_health_works_without_the_model(self):
r = self.c.get("/health")
self.assertEqual(r.status_code, 200)
self.assertFalse(r.json()["model_loaded"])
def test_bad_language_text_and_audio_are_rejected(self):
self.assertEqual(self.post(text="Hallo Welt", language="../x").status_code, 422)
self.assertEqual(self.post(text=" ", language="de").status_code, 422)
self.assertEqual(self.post(text="1234 5678", language="de").status_code, 422)
self.assertEqual(self.post(text="x" * (aligner.MAX_TEXT_CHARS + 1), language="de").status_code, 422)
self.assertEqual(self.post(audio=b"not audio", text="Hallo", language="de").status_code, 415)
def test_span_must_lie_inside_the_audio(self):
self.assertEqual(self.post(text="Hallo", language="de", start="5", end="6").status_code, 422)
self.assertEqual(self.post(text="Hallo", language="de", start="0.5", end="0.2").status_code, 422)
def test_oversized_audio_is_rejected(self):
old = aligner.MAX_AUDIO_BYTES
aligner.MAX_AUDIO_BYTES = 100
try:
self.assertEqual(self.post(text="Hallo", language="de").status_code, 413)
finally:
aligner.MAX_AUDIO_BYTES = old
def test_batch_validates_spans(self):
self.assertEqual(self.post("/align/batch", spans="nonsense", language="de").status_code, 422)
self.assertEqual(self.post("/align/batch", spans="[]", language="de").status_code, 422)
self.assertEqual(self.post("/align/batch", spans=json.dumps([{"start": 0, "end": 1}]), language="de").status_code, 422)
many = json.dumps([{"start": 0, "end": 1, "text": "a"}] * (aligner.MAX_SPANS + 1))
self.assertEqual(self.post("/align/batch", spans=many, language="de").status_code, 422)
def test_letterless_sentence_in_a_batch_needs_no_model(self):
# no letters: handled by gap interpolation, but the model still loads first; only check validation passes
r = self.post("/align/batch", spans=json.dumps([{"start": 0, "end": 1, "text": "2024"}]), language="de")
self.assertNotEqual(r.status_code, 422)
if __name__ == "__main__":
unittest.main()