fix(stt): processLocally-Feinschliff (Android-Bug "language not supported") + "Alles lokal"
- Geräte-STT prüft vor lokaler Erkennung das On-Device-Sprachpaket via
SpeechRecognition.available(); fehlt es, install() im Hintergrund + Fallback auf
Cloud (falls erlaubt) bzw. Server. processLocally wird nur bei "available" gesetzt
-> behebt "language not supported" auf Android-Chrome.
- onerror/Start-Fehler (language-not-supported/network/service) -> sauberer
Server-STT-Fallback statt Fehlermeldung.
- Schnellwahl "📴 Lokal": setzt STT + TTS gemeinsam auf Gerät (nur Text).
- Doku §6.3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ab9c4f4938
commit
dc597bd77b
3 changed files with 69 additions and 8 deletions
|
|
@ -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
|
- **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
|
Sprach-Dropdown gewählte feste Sprache. Im **Flex-Modus** fällt es automatisch auf
|
||||||
Server-STT (Whisper-Auto-Erkennung) zurück.
|
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
|
Server-STT-Pfad genutzt. Während der Erkennung erscheint der Text **live** im
|
||||||
Eingabefeld; erneutes Tippen aufs Mikrofon beendet und sendet.
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const micBtn = $("#mic");
|
||||||
const ttsSel = $("#tts");
|
const ttsSel = $("#tts");
|
||||||
const langSel = $("#lang-sel");
|
const langSel = $("#lang-sel");
|
||||||
const sttSel = $("#stt-sel");
|
const sttSel = $("#stt-sel");
|
||||||
|
const allLocalBtn = $("#all-local");
|
||||||
|
|
||||||
// "Flex" ist im Sprachmenü nur ein weiterer Wert: keine feste Sprache, sondern
|
// "Flex" ist im Sprachmenü nur ein weiterer Wert: keine feste Sprache, sondern
|
||||||
// automatische Erkennung. Eine konkrete Sprache = Fix-Modus (Eingabe wird übersetzt).
|
// automatische Erkennung. Eine konkrete Sprache = Fix-Modus (Eingabe wird übersetzt).
|
||||||
|
|
@ -150,15 +151,53 @@ function applySttChoice() {
|
||||||
if (local === "device") sttSel.value = "device";
|
if (local === "device") sttSel.value = "device";
|
||||||
else if (local === "server") sttSel.value = "server";
|
else if (local === "server") sttSel.value = "server";
|
||||||
else if (isMobile()) { sttSel.value = "device"; localStorage.setItem(STT_KEY, "device"); }
|
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;
|
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();
|
const rec = new SR();
|
||||||
rec.lang = LANG_BCP47[langSel.value] || "de-DE";
|
rec.lang = bcp;
|
||||||
rec.interimResults = true;
|
rec.interimResults = true;
|
||||||
rec.continuous = false;
|
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;
|
recognition = rec;
|
||||||
let finalText = "";
|
let finalText = "";
|
||||||
setMicRecording();
|
setMicRecording();
|
||||||
|
|
@ -173,9 +212,16 @@ function startDeviceStt() {
|
||||||
};
|
};
|
||||||
rec.onerror = (e) => {
|
rec.onerror = (e) => {
|
||||||
recognition = null;
|
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();
|
setMicIdle();
|
||||||
statusEl.textContent = e.error === "no-speech" ? "nichts gehört — bitte erneut"
|
statusEl.textContent = "Spracherkennung fehlgeschlagen (" + err + ")";
|
||||||
: "Spracherkennung fehlgeschlagen (" + (e.error || "?") + ")";
|
|
||||||
};
|
};
|
||||||
rec.onend = () => {
|
rec.onend = () => {
|
||||||
recognition = null;
|
recognition = null;
|
||||||
|
|
@ -184,7 +230,11 @@ function startDeviceStt() {
|
||||||
promptEl.value = "";
|
promptEl.value = "";
|
||||||
if (text) sendText(text); // -> vorhandener Text-Turn (inkl. Geräte-/Server-TTS)
|
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) {} } }
|
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.
|
// STT-Wahl ist geräte-lokal (nicht server-seitig) -> nur in localStorage.
|
||||||
if (sttSel) sttSel.addEventListener("change", () => localStorage.setItem(STT_KEY, sttSel.value));
|
if (sttSel) sttSel.addEventListener("change", () => localStorage.setItem(STT_KEY, sttSel.value));
|
||||||
|
if (allLocalBtn) allLocalBtn.addEventListener("click", setAllLocal);
|
||||||
|
|
||||||
loadMe();
|
loadMe();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,8 @@
|
||||||
<option value="device">🎙 Gerät</option>
|
<option value="device">🎙 Gerät</option>
|
||||||
<option value="server">☁ Server</option>
|
<option value="server">☁ Server</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="all-local" title="Alles lokal: Spracherkennung + Vorlesen im Gerät (nur Text wird gesendet)"
|
||||||
|
class="hidden text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors whitespace-nowrap">📴 Lokal</button>
|
||||||
<button id="new-chat" title="Neues Gespräch (Verlauf zurücksetzen)"
|
<button id="new-chat" title="Neues Gespräch (Verlauf zurücksetzen)"
|
||||||
class="h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
class="h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
||||||
|
|
@ -255,6 +257,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=32"></script>
|
<script src="/app.js?v=33"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue