"""Seed auto-pick: scoring, selection and the endpoint with all I/O mocked.""" import asyncio import os import tempfile import io import sys import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-autopick-test-")) from fastapi import HTTPException from pydub.generators import Sine from core import seed_score as sc import routes.tts as tts def wav(gain=-20, gap=False): seg = Sine(300, sample_rate=24000).to_audio_segment(1500).apply_gain(gain) if gap: seg = seg + seg.__class__.silent(2000, 24000) + seg buf = io.BytesIO() seg.export(buf, format="wav") return buf.getvalue() class FakeReq: def __init__(self, body, disconnect_after=None): self.body, self.calls, self.after = body, 0, disconnect_after async def json(self): return self.body async def is_disconnected(self): self.calls += 1 return self.after is not None and self.calls > self.after def run(req, gen, stt): with mock.patch.object(tts, "_seed_finder_request_audio", side_effect=gen), \ mock.patch.object(tts, "_load_settings", return_value={}), \ mock.patch("routes.stt._transcribe_audio", side_effect=stt): return asyncio.run(tts.seed_autopick(req)) class ScoringTests(unittest.TestCase): def test_combine_and_degraded(self): self.assertEqual(sc.combine(100, 100, 100), 100.0) self.assertEqual(sc.combine(None, 100, 50), 70.0) # (0.2*100+0.3*50)/0.5 self.assertLess(sc.loudness_score({"dbfs": -5, "peak_dbfs": 0, "target_dbfs": -20}), 20) self.assertEqual(sc.loudness_score({"dbfs": None}), 0.0) self.assertEqual(sc.problems_score([{"kind": "silence"}] * 5), 0.0) def test_winner_tiebreak_and_errors(self): c = [{"seed": 3, "score": 80}, {"seed": 2, "score": 80}, {"seed": 1, "error": "x"}] self.assertEqual(sc.pick_winner(c), 2) self.assertIsNone(sc.pick_winner([{"seed": 1, "error": "x"}])) def test_seed_list_bounds(self): self.assertEqual(sc.seed_list(None, None), list(range(1, 9))) self.assertEqual(sc.seed_list([5, 5, 7], None), [5, 7]) for bad in ((None, 17), (None, 0), ([], None), ([-1], None), (list(range(17)), None), (["a"], None)): with self.assertRaises(ValueError): sc.seed_list(*bad) class EndpointTests(unittest.TestCase): body = {"voice": "DE_x", "text": "hallo welt", "seeds": [1, 2, 3]} def test_picks_best_and_never_saves(self): audios = {1: wav(-20, gap=True), 2: wav(-20), 3: wav(-3)} gen = lambda text, voice, settings, seed: (audios[seed], "audio/wav") stt = lambda *a, **k: ("hallo welt", "x") out = run(FakeReq(self.body), gen, stt) self.assertEqual(out["winner"], 2) self.assertFalse(out["degraded"]) self.assertEqual([c["seed"] for c in out["candidates"]], [1, 2, 3]) self.assertTrue(all("audio_b64" in c and "problems" in c for c in out["candidates"])) self.assertGreater(out["candidates"][1]["score"], out["candidates"][0]["score"]) def test_stt_down_degrades(self): def stt(*a, **k): raise RuntimeError("no stt") out = run(FakeReq(self.body), lambda *a: (wav(), "audio/wav"), stt) self.assertTrue(out["degraded"]) self.assertIn("no stt", out["stt"]["note"]) self.assertIsNotNone(out["winner"]) self.assertIsNone(out["candidates"][0]["accuracy"]) def test_error_candidate_and_cancel(self): def gen(text, voice, settings, seed): if seed == 1: raise RuntimeError("boom") return wav(), "audio/wav" out = run(FakeReq(self.body, disconnect_after=2), gen, lambda *a, **k: ("hallo welt", "x")) self.assertEqual(out["candidates"][0]["error"], "boom") self.assertEqual(out["winner"], 2) self.assertTrue(out["cancelled"]) self.assertEqual(len(out["candidates"]), 2) def test_invalid_input(self): for body in ({"voice": "", "text": "a"}, {"voice": "v", "text": " "}, {"voice": "v", "text": "a", "count": 99}): with self.assertRaises(HTTPException): run(FakeReq(body), lambda *a: (wav(), "audio/wav"), lambda *a, **k: ("", "")) if __name__ == "__main__": unittest.main()