fix(tts): Geräte-TTS fällt auf Server-Audio zurück, wenn keine Stimmen vorhanden

Linux-Desktop-Browser haben oft keine speechSynthesis-Stimmen -> "Im Gerät" blieb
stumm (auto + Replay). deviceVoicesReady() prüft getVoices(); fehlen Stimmen, holt
serverSpeakFallback() Audio via /api/speak (Server-Default, nie "device") und spielt es.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-21 01:02:03 +02:00
commit d39fc40e94
2 changed files with 35 additions and 5 deletions

View file

@ -79,16 +79,46 @@ function _makeUtterance(text, lang) {
return u; return u;
} }
// Hat der Browser nutzbare Sprachstimmen? (Linux-Desktops haben oft KEINE -> stumm.)
function deviceVoicesReady() {
if (!TTS_SUPPORTED) return false;
if (!_voices.length) loadVoices();
return _voices.length > 0;
}
// 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) {
try {
const body = { text };
if (lang) body.language = lang;
const r = await fetch("/api/speak", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error("HTTP " + r.status);
startReplay([await r.arrayBuffer()], btn);
} catch (e) {
setReplayPlaying(btn, false);
statusEl.textContent = "Vorlesen fehlgeschlagen";
}
}
// Vorlesen ohne Button-Status (automatisches Vorlesen der Antwort). // Vorlesen ohne Button-Status (automatisches Vorlesen der Antwort).
function deviceSpeak(text, lang) { function deviceSpeak(text, lang) {
if (!TTS_SUPPORTED || !text) return; if (!text) return;
speechSynthesis.cancel(); if (deviceVoicesReady()) {
speechSynthesis.speak(_makeUtterance(text, lang)); speechSynthesis.cancel();
speechSynthesis.speak(_makeUtterance(text, lang));
} else {
serverSpeakFallback(text, lang, null); // keine Stimmen -> Server-Audio
}
} }
// Vorlesen mit Button-Status (Replay): Icon -> ⏹ während des Sprechens, danach zurück. // Vorlesen mit Button-Status (Replay): Icon -> ⏹ während des Sprechens, danach zurück.
function deviceSpeakBtn(text, lang, btn) { function deviceSpeakBtn(text, lang, btn) {
if (!TTS_SUPPORTED || !text) { setReplayPlaying(btn, false); return; } if (!text) { setReplayPlaying(btn, false); return; }
if (!deviceVoicesReady()) { serverSpeakFallback(text, lang, btn); return; }
speechSynthesis.cancel(); speechSynthesis.cancel();
const u = _makeUtterance(text, lang); const u = _makeUtterance(text, lang);
u.onend = u.onerror = () => setReplayPlaying(btn, false); u.onend = u.onerror = () => setReplayPlaying(btn, false);

View file

@ -318,6 +318,6 @@
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div> <div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
</footer> </footer>
<script src="/app.js?v=38"></script> <script src="/app.js?v=39"></script>
</body> </body>
</html> </html>