diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index de7ee82..99dc59c 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -1030,9 +1030,17 @@ Audio-Upload, keine Server-STT-Last. Gegenstück zum Geräte-TTS (§ 6.5.0), abe - **Nur Fix-Sprache:** Geräte-STT braucht einen Sprach-Hint → es nutzt die im Sprach-Dropdown gewählte feste Sprache. Im **Flex-Modus** fällt es automatisch auf Server-STT (Whisper-Auto-Erkennung) zurück. -- **Fallback:** Ohne Unterstützung (z. B. Firefox) oder bei Erkennungsfehlern wird der +- **On-Device-Sprachpakete (Chrome):** Vor der lokalen Erkennung wird per + `SpeechRecognition.available()` geprüft, ob das Sprachpaket vorhanden ist. Fehlt es, + wird es im Hintergrund heruntergeladen (`install()`) und für **diesen** Turn auf + Cloud (falls erlaubt) bzw. Server zurückgefallen — so erscheint kein + „language not supported" mehr. +- **Fallback:** Ohne Unterstützung (z. B. Firefox), bei fehlendem Sprachpaket oder + Erkennungsfehlern (`language-not-supported`, `network` …) wird automatisch der Server-STT-Pfad genutzt. Während der Erkennung erscheint der Text **live** im Eingabefeld; erneutes Tippen aufs Mikrofon beendet und sendet. +- **Schnellwahl „📴 Lokal":** Setzt mit einem Tipp **STT + TTS** auf „Gerät" — dann + sendet/empfängt das Handy nur noch Text (maximale Daten-/Kostenersparnis). --- diff --git a/app/web/app.js b/app/web/app.js index 135587a..b8615df 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -10,6 +10,7 @@ const micBtn = $("#mic"); const ttsSel = $("#tts"); const langSel = $("#lang-sel"); const sttSel = $("#stt-sel"); +const allLocalBtn = $("#all-local"); // "Flex" ist im Sprachmenü nur ein weiterer Wert: keine feste Sprache, sondern // automatische Erkennung. Eine konkrete Sprache = Fix-Modus (Eingabe wird übersetzt). @@ -150,15 +151,53 @@ function applySttChoice() { if (local === "device") sttSel.value = "device"; else if (local === "server") sttSel.value = "server"; else if (isMobile()) { sttSel.value = "device"; localStorage.setItem(STT_KEY, "device"); } + // Schnellwahl "Alles lokal" nur zeigen, wenn STT- UND TTS-on-device möglich sind. + if (allLocalBtn && TTS_SUPPORTED) allLocalBtn.classList.remove("hidden"); +} + +// Schnellwahl: STT + TTS auf "Gerät" (nur Text zum Server). Setzt beide Schalter. +function setAllLocal() { + unlockTTS(); + if (sttSel) { sttSel.value = "device"; localStorage.setItem(STT_KEY, "device"); } + if (ttsSel) { ttsSel.value = "device"; localStorage.setItem(PLAYBACK_KEY, "device"); } + statusEl.textContent = "Alles lokal: Erkennung + Vorlesen im Gerät"; + setTimeout(() => { + if (statusEl.textContent.startsWith("Alles lokal")) statusEl.textContent = ""; + }, 2500); } let recognition = null; -function startDeviceStt() { + +// Bestimmt den besten STT-Modus für eine Sprache: "local" (on-device), "cloud" oder +// "server" (Whisper-Fallback). Nutzt die Chrome-API available()/install() für die +// On-Device-Sprachpakete — verhindert "language not supported", wenn das Paket fehlt. +async function resolveSttMode(bcp) { + if (SR && typeof SR.available === "function") { + try { + const a = await SR.available({ langs: [bcp], processLocally: true }); + if (a === "available") return "local"; + if (a === "downloadable" || a === "downloading") { + // On-Device-Paket anstoßen (für künftige Turns); jetzt Cloud/Server. + try { if (typeof SR.install === "function") SR.install({ langs: [bcp], processLocally: true }); } catch (e) {} + } + } catch (e) { /* API-Fehler -> unten */ } + } + return meAllowCloudStt ? "cloud" : "server"; +} + +async function startDeviceStt() { + const bcp = LANG_BCP47[langSel.value] || "de-DE"; + const mode = await resolveSttMode(bcp); + if (mode === "server") { // lokal (für diese Sprache) nicht da & Cloud verboten + statusEl.textContent = "erkenne über Server …"; + startRecording(); + return; + } const rec = new SR(); - rec.lang = LANG_BCP47[langSel.value] || "de-DE"; + rec.lang = bcp; rec.interimResults = true; rec.continuous = false; - try { if (sttIsLocal() && "processLocally" in rec) rec.processLocally = true; } catch (e) {} + if (mode === "local") { try { rec.processLocally = true; } catch (e) {} } recognition = rec; let finalText = ""; setMicRecording(); @@ -173,9 +212,16 @@ function startDeviceStt() { }; rec.onerror = (e) => { recognition = null; + const err = e.error || "?"; + if (err === "no-speech") { setMicIdle(); statusEl.textContent = "nichts gehört — bitte erneut"; return; } + // Sprache/Dienst/Netz nicht verfügbar -> sauber auf Server-STT zurückfallen statt Fehler. + if (err === "language-not-supported" || err === "service-not-allowed" || err === "network") { + statusEl.textContent = "erkenne über Server …"; + startRecording(); + return; + } setMicIdle(); - statusEl.textContent = e.error === "no-speech" ? "nichts gehört — bitte erneut" - : "Spracherkennung fehlgeschlagen (" + (e.error || "?") + ")"; + statusEl.textContent = "Spracherkennung fehlgeschlagen (" + err + ")"; }; rec.onend = () => { recognition = null; @@ -184,7 +230,11 @@ function startDeviceStt() { promptEl.value = ""; if (text) sendText(text); // -> vorhandener Text-Turn (inkl. Geräte-/Server-TTS) }; - try { rec.start(); } catch (e) { recognition = null; setMicIdle(); statusEl.textContent = "STT-Start fehlgeschlagen"; } + try { rec.start(); } catch (e) { + recognition = null; + statusEl.textContent = "erkenne über Server …"; + startRecording(); // Start scheiterte -> Server-Fallback + } } function stopDeviceStt() { if (recognition) { try { recognition.stop(); } catch (e) {} } } @@ -682,6 +732,7 @@ if (ttsSel) { } // STT-Wahl ist geräte-lokal (nicht server-seitig) -> nur in localStorage. if (sttSel) sttSel.addEventListener("change", () => localStorage.setItem(STT_KEY, sttSel.value)); +if (allLocalBtn) allLocalBtn.addEventListener("click", setAllLocal); loadMe(); diff --git a/app/web/index.html b/app/web/index.html index 4224c85..6d5fae0 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -226,6 +226,8 @@ +