feat: Piper-Fallback für Cartesia-unsupported Sprachen (AR/IT/RU/PL)

PiperTTSProvider.synthesize() erkennt jetzt Gender-Codes ("f"/"m") und
löst sie per language-Parameter in echte Stimmmodell-Namen auf. Dadurch
greift FallbackTTSProvider transparent auf Piper durch, wenn Cartesia
eine Sprache nicht unterstützt.

tts_fallback = "piper" muss in voice-assistant.toml gesetzt sein
(nicht versioniert; siehe DEPLOYMENT.md Abschnitt 2.5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-24 23:47:36 +02:00
commit 35c08074cf

View file

@ -21,6 +21,8 @@ from pathlib import Path
from app.providers.tts.base import TTSProvider
_GENDER_CODES = {"f", "m", "any"}
try: # bevorzugter Pfad: in-process, Modell bleibt geladen
from piper import PiperVoice # type: ignore
@ -120,11 +122,23 @@ class PiperTTSProvider(TTSProvider):
text: str,
voice: str | None = None,
audio_format: str = "pcm",
language: str | None = None, # ignoriert: die Piper-Stimme kodiert die Sprache
language: str | None = None,
) -> bytes:
if not text or not text.strip():
raise ValueError("TTS input text is empty")
# Gender-Code (z. B. "f"/"m" aus Cartesia-Fallback) → echter Piper-Stimmname
if voice in _GENDER_CODES and language:
from app.dependencies import PIPER_VOICE_GENDERED, LANG_TO_PIPER_VOICE
lang = language.lower()
candidates = [voice, "f" if voice == "m" else "m"] if voice in ("f", "m") else ["f", "m"]
resolved = next(
(PIPER_VOICE_GENDERED.get((lang, g)) for g in candidates if PIPER_VOICE_GENDERED.get((lang, g))),
LANG_TO_PIPER_VOICE.get(lang),
)
if resolved:
voice = resolved
voice_name, speaker_id = self._parse_speaker(voice)
model, config = self._model_paths(voice_name)
pcm, native_rate = await asyncio.to_thread(