tts-voice-creator-clone-and.../tests/test_tts_health_probe.py
mARTin-B78 0551b6651f feat(voices): redesigned My Voices list, working copy, engine-aware TTS availability
My Voices
- Top bar with search, filter toggle (active-count badge), synth-source drawer, group and table buttons;
  filters/sort in a collapsible labelled panel; one-line sort header with select-all/none checkbox
- Groups chip list is a collapsed accordion; bottom bar is Refresh/Sync/Import plus one Actions (N) menu
  (Edit / Audio / Share / Danger), disabled until voices are checked; touch targets stay 44px
- Copy selected works again (copyText was no longer exported), shows "N voice names copied to clipboard"
  and falls back to a copy dialog when the async clipboard is unavailable

TTS / engines
- Health probe works for /v1 card URLs and, behind a LiteLLM gateway, checks the selected model's own
  upstream so stopped containers are not reported as available
- Unhiding a voice publishes a missing entry to the engine's voices.json (no more silent default-voice fallback)
- Status-bar TTS picker also drives the My Voices and STT-test selectors; Use as TTS refreshes the status bar

Includes the engine connect/disconnect, custom TTS, STT footer and magazine auth work that was already
in the working tree, with their tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 04:44:27 +02:00

100 lines
4.9 KiB
Python

"""TTS reachability probing must work for OpenAI-style card URLs that end in /v1."""
import os
import tempfile
import unittest
from unittest.mock import patch
os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="tts-health-tests-"))
from routes import tts
class TtsHealthProbeTests(unittest.TestCase):
def test_v1_card_url_is_probed_from_the_server_root(self):
with patch("routes.tts.probe_provider", return_value={"ok": True}) as probe:
tts._backend_health_detail("http://gateway:4000/v1/", "key")
base, checks = probe.call_args.args[:2]
self.assertEqual(base, "http://gateway:4000")
self.assertIn(("/health/liveliness", None), checks)
self.assertEqual(probe.call_args.kwargs["headers"], {"Authorization": "Bearer key"})
def test_plain_host_url_is_unchanged(self):
with patch("routes.tts.probe_provider", return_value={"ok": False}) as probe:
tts._backend_health_detail("http://host:8005")
self.assertEqual(probe.call_args.args[0], "http://host:8005")
class PublishUnhiddenVoiceTests(unittest.TestCase):
def setUp(self):
from pathlib import Path
self.dir = Path(tempfile.mkdtemp(prefix="publish-voice-"))
self.voices = self.dir / "voices.json"
self.settings = {"voices_scan_dir": str(self.dir), "tts_config_dir": str(self.dir)}
self.audio = self.dir / "DE_F_New.wav"
self.audio.write_bytes(b"RIFF")
def run_publish(self):
return tts.publish_voice_clone_if_missing(self.audio, "DE", self.settings)
def test_unknown_voice_with_transcript_is_published(self):
import json
self.voices.write_text(json.dumps({"other": {"ref_text": "x"}}))
self.audio.with_suffix(".reference.txt").write_text("Hallo Welt")
with patch("routes.tts.time.sleep"):
self.assertTrue(self.run_publish())
entry = json.loads(self.voices.read_text())["DE_F_New"]
self.assertEqual((entry["ref_text"], entry["language"]), ("Hallo Welt", "German"))
def test_known_voice_keeps_its_cached_embedding(self):
import json
self.voices.write_text(json.dumps({"DE_F_New": {"ref_text": "a", "speaker_embeddings": "/x.pt"}}))
self.audio.with_suffix(".reference.txt").write_text("Hallo")
self.assertFalse(self.run_publish())
self.assertIn("speaker_embeddings", json.loads(self.voices.read_text())["DE_F_New"])
def test_no_transcript_or_no_voices_file_is_a_no_op(self):
self.assertFalse(self.run_publish()) # no voices.json
self.voices.write_text("{}")
self.assertFalse(self.run_publish()) # no transcript
self.assertEqual(self.voices.read_text(), "{}")
class GatewayUpstreamProbeTests(unittest.TestCase):
URL = "http://gateway:4000/v1"
def test_running_upstream_is_available(self):
with patch("routes.tts._gateway_upstream", return_value="http://host:8023/v1"), \
patch("routes.tts.probe_provider", return_value={"ok": True}) as probe:
self.assertTrue(tts._backend_health_detail(self.URL, "k", "m")["ok"])
self.assertEqual(probe.call_count, 1)
self.assertEqual(probe.call_args.args[0], "http://host:8023")
def test_stopped_upstream_is_not_rescued_by_a_live_gateway(self):
refused = {"ok": False, "error": "Connection refused"}
with patch("routes.tts._gateway_upstream", return_value="http://host:8020/v1"), \
patch("routes.tts.probe_provider", return_value=refused) as probe:
self.assertFalse(tts._backend_health_detail(self.URL, "k", "m")["ok"])
self.assertEqual(probe.call_count, 1) # the gateway's /health/liveliness is not consulted
def test_unroutable_upstream_falls_back_to_the_gateway(self):
results = [{"ok": False, "error": "Timeout (2 s)"}, {"ok": True, "endpoint": "/health/liveliness"}]
with patch("routes.tts._gateway_upstream", return_value="http://internal:8020/v1"), \
patch("routes.tts.probe_provider", side_effect=results) as probe:
self.assertTrue(tts._backend_health_detail(self.URL, "k", "m")["ok"])
self.assertEqual(probe.call_args_list[1].args[0], "http://gateway:4000")
def test_upstream_lookup_needs_key_and_model_and_reads_model_info(self):
tts._GATEWAY_UPSTREAMS.clear()
self.assertEqual(tts._gateway_upstream("http://gw", "", "m"), "")
info = {"data": [{"model_name": "m", "litellm_params": {"api_base": "http://h:1/v1"}}, {"model_name": "x"}]}
resp = type("R", (), {"status_code": 200, "json": lambda self: info})()
with patch("routes.tts.requests.get", return_value=resp) as get:
self.assertEqual(tts._gateway_upstream("http://gw", "k", "m"), "http://h:1/v1")
self.assertEqual(tts._gateway_upstream("http://gw", "k", "other"), "")
self.assertEqual(get.call_count, 1) # second lookup served from the cache
if __name__ == "__main__":
unittest.main()