fix(tts): Geräte-TTS-Regression für Sprachen ohne Geschlechtsstimme beheben

Commit 5c63f7b hatte deviceGenderReady als harte Bedingung eingeführt: das
Gerät musste eine Stimme des WAHLgeschlechts haben, sonst Server-Fallback.
Die konservative Geschlechts-Heuristik liefert für viele Geräte-Stimmen null
(z.B. NL am iPhone: nur eine Stimme ohne erkennbares Geschlecht) -> der
Server-Fallback griff und lieferte eine schlechte/fremdsprachige Aussprache.
Für NL kam (im noch nicht neu gestarteten Prozess) die deutsche Default-
Stimme -> "deutscher Akzent".

Lösung: deviceGenderReady -> deviceVoiceReady. Eine sprachrichtige Stimme
reicht (pickVoice wählt cands[0] als Best-Effort-Fallback). Die m/f-Wahl
wirkt am Gerät nur für Sprachen mit mehreren Stimmen (DE, EN), sonst
folgenlos — aber eine falsche Sprache ist schlechter als ein folgenloser
Schalter.

+ Test: NL in test_piper_single_voice_languages_ignore_gender aufgenommen
(serverseitige Komplementär-Logik; sichert voice_for_route NL-Auflösung).
Der client-seitige app.js-Fix ist mangels JS-Test-Framework manuell
verifiziert (Geräte-TTS für NL spricht niederländisch, DE/EN behalten
Geschlechtswahl).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-28 16:15:04 +02:00
commit e814b5548d
2 changed files with 16 additions and 8 deletions

View file

@ -95,10 +95,15 @@ def test_piper_gendered_voice_exact(lang, gender, expected):
@pytest.mark.parametrize("lang,expected", [
("ar", "ar_JO-kareem-medium"),
("pt", "pt_BR-faber-medium"),
("nl", "nl_NL-mls-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.
# Wichtig fuer NL: die m/f-Wahl greift hier folgenlos (einzig verfuegbare Stimme);
# die Geraete-TTS darf deshalb nicht auf Server-Fallback ausweichen (s. app.js
# deviceVoiceReady). Ohne diesen Eintrag wuerde voice_for_route None liefern und
# den Server-Default (deutsche Stimme) nach sich ziehen.
assert voice_for_route("piper", lang, "f") == expected
assert voice_for_route("piper", lang, "m") == expected