test: Coverage für Gender-/Stimm-System + hermetische Suite

Schließt die Test-Lücken der Gender-/Stimm-Arbeit der letzten Tage:
- test_routing.py: voice_for_route über alle Sprachen × Geschlechter
  (inkl. ES Multi-Speaker #0/#1, "any"→weiblich, Gegengeschlecht-Fallback,
  AR/PT/ZH Einzelstimmen, unbekannte Sprache → None).
- test_piper_tts.py: _parse_speaker (#id) + Gender-Code-Auflösung in
  synthesize() + speaker_id-Durchreichung für Multi-Speaker.
- test_cartesia_tts.py (neu): Voice-Map-Parsing, _resolve_voice_id-Präzedenz,
  nicht unterstützte Sprachen, direkte-UUID-Durchreichung, Cartesia→Piper-Fallback.

Hermetik-Fix in conftest.py: Deployment-Fallback-Ketten (stt/llm/tts_fallback)
werden pro Test auf "" gesetzt. Vorher schlugen 3 WS-Streaming-Tests fehl, weil
llm_fallback="openrouter" aus der TOML durchsickerte und Stub-LLMs ohne stream()
auf den echten OpenRouter durchfielen ("OPENROUTER_API_KEY is empty").

Ergebnis: 213 passed (vorher 164 passed / 3 failed). DEPLOYMENT.md Teil 4.0
dokumentiert den Pflicht-Testlauf vor jedem Deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 00:32:47 +02:00
commit 3c549a05b1
5 changed files with 280 additions and 1 deletions

View file

@ -64,3 +64,71 @@ def test_registry_unknown_provider_raises():
def test_registry_known_provider():
assert type(get_tts_provider("piper")).__name__ == "PiperTTSProvider"
# ---------------------------------------------------------------------------
# voice_for_route: Stimm-/Geschlechts-Auswahl je Provider und Sprache.
# ---------------------------------------------------------------------------
from app.dependencies import voice_for_route, PIPER_VOICE_GENDERED
@pytest.mark.parametrize("lang,gender,expected", [
("de", "f", "de_DE-kerstin-low"),
("de", "m", "de_DE-thorsten-high"),
("en", "f", "en_US-amy-medium"),
("en", "m", "en_US-lessac-high"),
("fr", "f", "fr_FR-siwis-medium"),
("fr", "m", "fr_FR-tom-medium"),
("es", "f", "es_ES-sharvard-medium#1"), # Multi-Speaker: #1 = weiblich
("es", "m", "es_ES-sharvard-medium#0"), # Multi-Speaker: #0 = maennlich
("it", "f", "it_IT-paola-medium"),
("it", "m", "it_IT-riccardo-x_low"),
("ru", "f", "ru_RU-irina-medium"),
("ru", "m", "ru_RU-ruslan-medium"),
("pl", "f", "pl_PL-gosia-medium"),
("pl", "m", "pl_PL-darkman-medium"),
])
def test_piper_gendered_voice_exact(lang, gender, expected):
assert voice_for_route("piper", lang, gender) == expected
@pytest.mark.parametrize("lang,expected", [
("ar", "ar_JO-kareem-medium"),
("pt", "pt_BR-faber-medium"),
("zh", "zh_CN-huayan-medium"),
])
def test_piper_single_voice_languages_ignore_gender(lang, expected):
# Diese Sprachen haben keinen Geschlechtspartner -> selbe Stimme fuer m und f.
assert voice_for_route("piper", lang, "f") == expected
assert voice_for_route("piper", lang, "m") == expected
def test_piper_any_prefers_female():
# 'any' (und unbekannte Codes) bevorzugen weiblich, konsistent mit Cartesia.
assert voice_for_route("piper", "de", "any") == "de_DE-kerstin-low"
assert voice_for_route("piper", "fr", "any") == "fr_FR-siwis-medium"
def test_piper_unknown_language_returns_none():
assert voice_for_route("piper", "xx", "f") is None
def test_piper_without_language_returns_none():
assert voice_for_route("piper", None, "f") is None
def test_cartesia_passes_gender_code_through():
# Cartesia loest die UUID selbst auf -> nur der Gender-Code wird durchgereicht.
assert voice_for_route("cartesia", "de", "f") == "f"
assert voice_for_route("cartesia", "de", "any") == "any"
def test_other_provider_returns_none():
assert voice_for_route("openrouter", "de", "f") is None
def test_piper_falls_back_to_opposite_gender(monkeypatch):
# Fehlt die angeforderte Richtung, wird die vorhandene Gegenrichtung genommen
# (vor dem gender-agnostischen LANG_TO_PIPER_VOICE-Fallback).
monkeypatch.delitem(PIPER_VOICE_GENDERED, ("de", "m"), raising=False)
assert voice_for_route("piper", "de", "m") == "de_DE-kerstin-low"