From 35c08074cf7c2b4886a1ca00577068014625fd97 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 24 Jun 2026 23:47:36 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Piper-Fallback=20f=C3=BCr=20Cartesia-un?= =?UTF-8?q?supported=20Sprachen=20(AR/IT/RU/PL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/providers/tts/piper.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/providers/tts/piper.py b/app/providers/tts/piper.py index f19cd04..75d2d59 100644 --- a/app/providers/tts/piper.py +++ b/app/providers/tts/piper.py @@ -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(