fix(tts): Geräte-TTS folgt der Geschlechtswahl (m/f), sonst Server-Fallback
Der Geräte-TTS-Pfad (Browser-SpeechSynthesis) wählte die Stimme bisher nur nach Sprache -> der m/f-Schalter blieb dort folgenlos. pickVoice bevorzugt jetzt die geschlechtspassende Stimme (Heuristik über Name/voiceURI, da die Web Speech API kein Geschlechtsfeld kennt). Findet das Gerät keine passende Stimme, weichen deviceSpeak/deviceSpeakBtn auf Server-TTS aus (echte Piper-m/f-Stimmen), damit die Wahl hörbar wirkt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
25f35ad2cb
commit
5c63f7b6cc
1 changed files with 52 additions and 8 deletions
|
|
@ -88,19 +88,55 @@ if (TTS_SUPPORTED) {
|
|||
setTimeout(() => refreshDevicePreset(), 1500); // Backstop, falls kein voiceschanged kommt
|
||||
}
|
||||
|
||||
function pickVoice(bcp) {
|
||||
// Die Web Speech API kennt KEIN Standard-Geschlechtsfeld an einer Stimme -> Heuristik
|
||||
// über Stimmname/voiceURI. Liefert 'm', 'f' oder null (unbekannt). Bewusst konservativ:
|
||||
// im Zweifel null, damit wir lieber auf Server-TTS ausweichen als falsch zu raten.
|
||||
const _VOICE_F = ["female", "weiblich", "femme", "mujer", "femminile", "feminino",
|
||||
"anna", "helena", "petra", "samantha", "victoria", "karen", "moira", "tessa", "fiona",
|
||||
"susan", "zoe", "amelie", "amélie", "audrey", "aurelie", "aurélie", "marie", "alice",
|
||||
"federica", "elsa", "monica", "mónica", "paulina", "luciana", "joana", "catarina",
|
||||
"milena", "alena", "irina", "tatyana", "ting-ting", "mei-jia", "sin-ji", "yaoyao",
|
||||
"huihui", "hedda", "katja", "hazel", "zira", "heami"];
|
||||
const _VOICE_M = ["male", "männlich", "homme", "hombre", "maschile", "masculino",
|
||||
"markus", "yannick", "reed", "aaron", "fred", "daniel", "oliver", "arthur", "rishi",
|
||||
"gordon", "conrad", "thomas", "luca", "cosimo", "diego", "jorge", "carlos", "felipe",
|
||||
"bruno", "yuri", "pavel", "maxim", "liang", "kangkang", "stefan", "david", "mark",
|
||||
"george", "pablo", "ravi"];
|
||||
|
||||
function _voiceGenderOf(v) {
|
||||
if (!v) return null;
|
||||
if (v.gender === "male") return "m"; // nicht-Standard, aber manche Engines liefern es
|
||||
if (v.gender === "female") return "f";
|
||||
const s = ((v.name || "") + " " + (v.voiceURI || "")).toLowerCase();
|
||||
if (_VOICE_F.some((n) => s.includes(n))) return "f";
|
||||
if (_VOICE_M.some((n) => s.includes(n))) return "m";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sprach- UND (wenn erkennbar) geschlechtspassende Gerätestimme.
|
||||
// Rückgabe: { voice, genderMatch }. genderMatch=false signalisiert dem Aufrufer, dass
|
||||
// keine Stimme des Wunschgeschlechts gefunden wurde (-> Server-Fallback ist sinnvoll).
|
||||
function pickVoice(bcp, gender) {
|
||||
if (!_voices.length) loadVoices();
|
||||
const lc = bcp.toLowerCase();
|
||||
const two = lc.slice(0, 2);
|
||||
return _voices.find((v) => v.lang && v.lang.toLowerCase() === lc) ||
|
||||
_voices.find((v) => v.lang && v.lang.toLowerCase().startsWith(two)) || null;
|
||||
const cands = _voices.filter(
|
||||
(v) => v.lang && (v.lang.toLowerCase() === lc || v.lang.toLowerCase().startsWith(two)),
|
||||
);
|
||||
if (!cands.length) return { voice: null, genderMatch: false };
|
||||
if (gender === "m" || gender === "f") {
|
||||
const hit = cands.find((v) => _voiceGenderOf(v) === gender);
|
||||
if (hit) return { voice: hit, genderMatch: true };
|
||||
return { voice: cands[0], genderMatch: false }; // keine passende -> Caller weicht aus
|
||||
}
|
||||
return { voice: cands[0], genderMatch: true };
|
||||
}
|
||||
|
||||
function _makeUtterance(text, lang) {
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
u.lang = LANG_BCP47[lang] || lang || "de-DE";
|
||||
const v = pickVoice(u.lang);
|
||||
if (v) u.voice = v;
|
||||
const { voice } = pickVoice(u.lang, _voiceGender);
|
||||
if (voice) u.voice = voice;
|
||||
return u;
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +147,14 @@ function deviceVoicesReady() {
|
|||
return _voices.length > 0;
|
||||
}
|
||||
|
||||
// Hat das Gerät eine zum Wunschgeschlecht passende Stimme für diese Sprache? Wenn nicht,
|
||||
// soll auf Server-TTS ausgewichen werden (dort gibt es echte m/f-Stimmen), damit die
|
||||
// Geschlechtswahl hörbar wirkt, statt am Gerät folgenlos zu bleiben.
|
||||
function deviceGenderReady(lang) {
|
||||
const bcp = LANG_BCP47[lang] || lang || "de-DE";
|
||||
return pickVoice(bcp, _voiceGender).genderMatch;
|
||||
}
|
||||
|
||||
// Server-Fallback: holt Audio via /api/speak (ohne tts_provider -> Server-Default,
|
||||
// niemals "device") und spielt es ab. Greift, wenn Geräte-TTS keine Stimme hat.
|
||||
async function serverSpeakFallback(text, lang, btn) {
|
||||
|
|
@ -132,18 +176,18 @@ async function serverSpeakFallback(text, lang, btn) {
|
|||
// Vorlesen ohne Button-Status (automatisches Vorlesen der Antwort).
|
||||
function deviceSpeak(text, lang) {
|
||||
if (!text) return;
|
||||
if (deviceVoicesReady()) {
|
||||
if (deviceVoicesReady() && deviceGenderReady(lang)) {
|
||||
speechSynthesis.cancel();
|
||||
speechSynthesis.speak(_makeUtterance(text, lang));
|
||||
} else {
|
||||
serverSpeakFallback(text, lang, null); // keine Stimmen -> Server-Audio
|
||||
serverSpeakFallback(text, lang, null); // keine (passende) Stimme -> Server-Audio
|
||||
}
|
||||
}
|
||||
|
||||
// Vorlesen mit Button-Status (Replay): Icon -> ⏹ während des Sprechens, danach zurück.
|
||||
function deviceSpeakBtn(text, lang, btn) {
|
||||
if (!text) { setReplayPlaying(btn, false); return; }
|
||||
if (!deviceVoicesReady()) { serverSpeakFallback(text, lang, btn); return; }
|
||||
if (!deviceVoicesReady() || !deviceGenderReady(lang)) { serverSpeakFallback(text, lang, btn); return; }
|
||||
speechSynthesis.cancel();
|
||||
const u = _makeUtterance(text, lang);
|
||||
u.onend = u.onerror = () => setReplayPlaying(btn, false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue