From 3f6bf53a7f5a0fea7ea48536a85644c1223a5a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Sat, 20 Jun 2026 22:26:20 +0200 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20Ger=C3=A4te-STT=20entfernt=20+?= =?UTF-8?q?=20Men=C3=BC-Redesign=20(schlanke=20Kopfzeile=20+=20=E2=8B=AE-S?= =?UTF-8?q?heet)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Geräte-STT (Web Speech API) entfernt — auf Android nur Cloud, Qualität < Whisper, verwirrend. STT läuft jetzt überall serverseitig (Whisper). Geräte-TTS bleibt. - Backend: allow_cloud_stt (config/runtime_config/api me) + Tests entfernt. - Frontend: SpeechRecognition-Code, STT-Dropdown, "Lokal"-Button raus. Menü-Redesign (verständlicher + passt auf Handys): - Kopfzeile schlank: Mund-Icon + Titel + Sprache + ⋮-Menübutton (kein Überlauf mehr). - ⋮ öffnet ein Einstellungs-Sheet: Ton als 3 Presets (📱 Im Gerät / ☁ Server / ✨ Beste Qualität) statt zweier kryptischer "Gerät"-Dropdowns; Neues Gespräch; Tag-/Nachtmodus; Konto (Identität, Admin, Abmelden). - #tts bleibt als verborgenes Quell-Select -> bestehende TTS-Logik unverändert. - Doku §5.1.2 neu, §6.3 STT-Hinweis. 167 grün. Co-Authored-By: Claude Opus 4.8 --- BEDIENUNGSANLEITUNG.md | 82 +++++++--------- app/api/me.py | 2 - app/config.py | 4 - app/runtime_config.py | 1 - app/web/app.js | 213 +++++++++++------------------------------ app/web/index.html | 97 +++++++++++++------ tests/test_audit.py | 14 --- 7 files changed, 160 insertions(+), 253 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 99dc59c..4c7630b 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -774,29 +774,43 @@ https://va.beispiel.de/ ← remote über Reverse-Proxy (alles, inkl. Mikr ``` ┌─────────────────────────────────────────────────────────┐ -│ Voice Assistant [🇩🇪 ▾] [Qualität ▾] [⚙️] [🌙] │ -├─────────────────────────────────────────────────────────┤ -│ │ -│ Nachrichtenverlauf │ -│ (eigene Nachrichten: blaue Blase rechts) │ -│ (Assistent: graue Blase links) │ -│ │ -├─────────────────────────────┬───────────────────────────┤ -│ Texteingabe … [Senden] │ [🎤] │ -└─────────────────────────────┴───────────────────────────┘ - Statuszeile: „denkt …" / „verarbeite Sprache …" / leer +│ 👄 Voice Assistant ............... [🇩🇪 ▾] [ ⋮ ] │ +├─────────────────────────────────────────────────────────┤ ┌─ Menü (⋮) ──────────────┐ +│ │ │ VORLESEN │ +│ Nachrichtenverlauf │ │ ▣ 📱 Im Gerät │ +│ (eigene Nachrichten: blaue Blase rechts) │ │ ▢ ☁ Server │ +│ (Assistent: graue Blase links) │ │ ▢ ✨ Beste Qualität │ +│ │ │ ────────────────────── │ +├─────────────────────────────┬───────────────────────────┤ │ ✎ Neues Gespräch │ +│ Texteingabe … [Senden] │ [🎤] │ │ 🌙 Tag-/Nachtmodus │ +└─────────────────────────────┴───────────────────────────┘ │ ────────────────────── │ + Statuszeile: „denkt …" / „verarbeite Sprache …" / leer │ Angemeldet · Admin · Ab │ + └──────────────────────────┘ ``` +**Kopfzeile (immer schlank):** + | Element | Funktion | |---------|----------| +| **👄 / Titel** | Marke; Mund-Symbol als Wiedererkennung | +| **Sprache ▾** | Antwortsprache (→ § 6.6): `🔄 Flex` = folgt der gesprochenen Sprache · feste Sprache (🇩🇪/🇬🇧/…) = Antwort + Stimme in dieser Sprache | +| **⋮ Menü** | Öffnet die Einstellungen (unten) | + +**Im ⋮-Menü:** + +| Element | Funktion | +|---------|----------| +| **Vorlesen** | Wie wird die Antwort vorgelesen? `📱 Im Gerät` = das Handy liest selbst vor (kein Server-Audio → spart Daten) · `☁ Server` = piper (lokal, zuverlässig) · `✨ Beste Qualität` = chatterbox (natürliche Stimme). „Im Gerät" erscheint nur, wo der Browser es unterstützt. | +| **✎ Neues Gespräch** | Frische Sitzung (Verlauf zurücksetzen) | +| **🌙 Tag-/Nachtmodus** | Heller/dunkler Modus; folgt sonst dem Betriebssystem | +| **Konto** | „Angemeldet als …", **⚙ Admin** (nur Admins → § 7.5), **Abmelden** | + +**Unten:** + +| Element | Funktion | +|---------|----------| +| **🎤 Mikrofon** | **grün:** tippen → Aufnahme · **rot:** tippen → stoppt & sendet · **amber ⏹:** tippen → KI unterbrechen (Barge-in). STT läuft serverseitig (Whisper). | | **Texteingabe + Senden** | Text tippen, dann Enter oder „Senden" | -| **🎤 Mikrofon-Button** | **Idle (grün 🎤):** Tippen → Aufnahme startet · **Aufnahme (rot pulsierend 🎤):** Tippen → Aufnahme stoppt und wird gesendet · **KI antwortet (amber ⏹):** Tippen → Antwort sofort unterbrechen (Barge-in) | -| **Sprache ▾** | Antwortsprache wählen (→ § 6.6): `🔄 Flex` = folgt automatisch der gesprochenen Sprache · feste Sprache (🇩🇪/🇬🇧/…) = Eingabe wird in diese Sprache übersetzt. Die vorlesende Stimme folgt der Auswahl. | -| **Qualität ▾** (Ausgabe/TTS) | Vorlese-Quelle: `📱 Gerät` = das Handy/der Browser liest selbst vor (Web Speech API, kein Server-Audio → spart Daten/Kosten) · `Schnell` = piper (lokal) · `Hoch` = chatterbox (neuronal) · `Cloud` = openrouter | -| **STT ▾** (Eingabe) | *Nur sichtbar auf Geräten mit lokaler Spracherkennung* (iOS/Android): `🎙 Gerät` = Spracherkennung im Gerät (nur Text wird gesendet) · `☁ Server` = Audio-Upload + Whisper (wie am PC/Laptop). Getrennt von der Ausgabe wählbar (→ § 6.3.1). | -| **☀️ / 🌙** | Tag-/Nacht-Modus; folgt sonst automatisch dem Betriebssystem | -| **⚙️** (Admin) | Öffnet das Admin-Panel — nur für Admin-Nutzer sichtbar (→ § 7.5) | -| **Angemeldet als …** | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie | #### 5.1.3 Typischer Ablauf — Textchat @@ -1013,34 +1027,10 @@ FASTER_WHISPER_DEVICE=cuda FASTER_WHISPER_COMPUTE_TYPE=float16 ``` -#### 6.3.1 Geräte-STT (Web Speech API) — „🎙 Gerät" - -Auf **Mobilgeräten** (iOS/Android) kann die Spracherkennung **im Gerät** laufen: Das -Handy wandelt Sprache in Text um und sendet nur den **Text** an den Server — kein -Audio-Upload, keine Server-STT-Last. Gegenstück zum Geräte-TTS (§ 6.5.0), aber -**getrennt** wählbar (STT-Dropdown in der Kopfzeile). - -- **Erscheint nur**, wenn das Gerät lokale Spracherkennung bietet. **Terminal, Desktop- - Browser und Laptop** zeigen die Option nicht — dort läuft STT wie bisher über den - Server (Whisper). -- **Datenschutz (Linie C):** Standardmäßig nur bei **nachweislich lokaler** Erkennung - (iOS-Safari; Chrome mit On-Device-Modus). **Chrome am Desktop** würde das Audio zur - Erkennung an Google schicken — das ist nur erlaubt, wenn ein Admin - `ALLOW_CLOUD_STT=true` setzt (Admin → Einstellungen → „Geräte-STT (mobil)"). -- **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. -- **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). +> **Hinweis:** Die Spracherkennung läuft auf **allen** Geräten serverseitig (Whisper) — +> einheitlich und zuverlässig. (Ein früher erprobtes Geräte-STT über die Web Speech API +> wurde wieder entfernt: Auf Android-Chrome lief es nur über die Google-Cloud, und die +> Erkennungsqualität war Whisper unterlegen. Das **Vorlesen** im Gerät (§ 6.5.0) bleibt.) --- diff --git a/app/api/me.py b/app/api/me.py index de0c35a..f270820 100644 --- a/app/api/me.py +++ b/app/api/me.py @@ -3,7 +3,6 @@ from fastapi import APIRouter, Depends, HTTPException from app.auth import require_user, is_admin_user from app.config import settings from app.dependencies import get_store -from app.runtime_config import runtime_settings from app.schemas import UserPrefs, MemoryCreate, MemoryOut from app.store import User @@ -19,7 +18,6 @@ async def get_me(user: User = Depends(require_user)): "is_admin": user.is_admin or is_admin_user(user), "prefs": user.prefs, "sso_logout_url": settings.sso_logout_url, - "allow_cloud_stt": bool(runtime_settings.allow_cloud_stt), } diff --git a/app/config.py b/app/config.py index 56397f7..14ce23f 100644 --- a/app/config.py +++ b/app/config.py @@ -135,10 +135,6 @@ class Settings(BaseSettings): faster_whisper_model: str = "base" # tiny|base|small|medium|large-v3 faster_whisper_device: str = "auto" # auto|cpu|cuda faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16 - # Geräte-STT (Web Speech API) nur bei nachweislich lokaler Erkennung (iOS, Chrome - # on-device). True erlaubt es auch bei Cloud-Erkennung (z. B. Chrome-Desktop -> - # Audio geht zu Google). Default False = strenger Datenschutz (Linie C). - allow_cloud_stt: bool = False # --- Lokales TTS (piper) ------------------------------------------------- piper_bin: str = "piper" # Pfad/Name des piper-Binaries piper_voices_dir: str = str(Path.home() / ".local" / "share" / "piper" / "voices") diff --git a/app/runtime_config.py b/app/runtime_config.py index cb809bc..231e413 100644 --- a/app/runtime_config.py +++ b/app/runtime_config.py @@ -34,7 +34,6 @@ RUNTIME_SETTABLE: dict[str, tuple[str, str, str]] = { "memory_extraction_enabled": ("Erinnerungs-Extraktion", "bool", "true | false"), "memory_extraction_every_n_turns": ("Extraktion alle N Turns", "int", "z.B. 3"), "daily_request_limit": ("Tageskontingent (global)", "int", "0 = unbegrenzt"), - "allow_cloud_stt": ("Geräte-STT via Cloud erlauben", "bool", "true = auch Cloud-Erkennung (z.B. Chrome-Desktop -> Audio zu Google); false = nur on-device"), } _TTL = 30.0 diff --git a/app/web/app.js b/app/web/app.js index 6ad9a9f..999fa3d 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -9,8 +9,6 @@ const formEl = $("#prompt-form"); 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). @@ -105,153 +103,18 @@ function unlockTTS() { try { speechSynthesis.speak(new SpeechSynthesisUtterance("")); _ttsUnlocked = true; } catch (e) {} } -// ---------- Geräte-STT (Web Speech API SpeechRecognition) ---------- -// Gegenstück zum Geräte-TTS: das Gerät erkennt Sprache lokal und schickt nur Text. -// Linie C: standardmäßig nur bei nachweislich lokaler Erkennung (iOS, Chrome on-device); -// Cloud-Erkennung (z. B. Chrome-Desktop -> Audio zu Google) nur, wenn der Admin -// allow_cloud_stt aktiviert hat. Sonst greift weiter der Server-STT (Whisper). -const SR = window.SpeechRecognition || window.webkitSpeechRecognition; -const STT_SUPPORTED = !!SR; -const STT_KEY = "va-stt"; // geräte-lokal: "device" wenn Geräte-STT gewählt -let meAllowCloudStt = false; // aus /api/me (Admin-Flag) - -function isIOS() { - const ua = navigator.userAgent || ""; - return /iPhone|iPad|iPod/i.test(ua) || - (navigator.maxTouchPoints > 1 && /Macintosh/.test(ua)); -} -// Heuristik: iOS erkennt on-device. Chrome mit processLocally-Unterstützung gilt -// ebenfalls als lokal. Sonst (Cloud) nur erlaubt, wenn der Admin es freigibt. -function sttIsLocal() { - if (isIOS()) return true; - try { return SR && "processLocally" in SR.prototype; } catch (e) { return false; } -} -function sttDeviceAllowed() { - return STT_SUPPORTED && (sttIsLocal() || meAllowCloudStt); -} -function isSttDeviceMode() { return !!sttSel && sttSel.value === "device"; } - -// Geräte-STT für DIESEN Turn nutzen? Nur Fix-Sprache (SpeechRecognition braucht einen -// Sprach-Hint); im Flex-Modus auf Server-STT (Whisper-Auto-Erkennung) zurückfallen. -function useDeviceStt() { - return isSttDeviceMode() && sttDeviceAllowed() && (langSel && langSel.value !== "flex"); -} - -// Stellt das STT-Dropdown beim Laden ein: Option ausblenden wenn nicht erlaubt; -// sonst lokale Wahl bzw. Mobil-Default. -function applySttChoice() { - if (!sttSel) return; - if (!sttDeviceAllowed()) { - sttSel.classList.add("hidden"); // kein Geräte-STT -> Server wie bisher - sttSel.value = "server"; - return; - } - sttSel.classList.remove("hidden"); - const local = localStorage.getItem(STT_KEY); - 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; - -// 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) { - // Chrome (neue API): prüft, ob das On-Device-Sprachpaket vorhanden ist. - 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) {} - } - return meAllowCloudStt ? "cloud" : "server"; - } catch (e) { /* API-Fehler -> unten */ } - } - // Keine available()-API: iOS-Safari erkennt on-device (mit Live-Vorschau) -> lokal. - if (isIOS()) return "local"; - // Sonst (z. B. älteres Chrome): SpeechRecognition liefe über die Cloud. - return meAllowCloudStt ? "cloud" : "server"; -} - -const SERVER_STT_MSG = "Server-Erkennung. Live-Vorschau nur mit lokalem/Cloud-STT"; - -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 - startRecording(SERVER_STT_MSG); - return; - } - const rec = new SR(); - rec.lang = bcp; - rec.interimResults = true; - rec.continuous = false; - if (mode === "local") { try { rec.processLocally = true; } catch (e) {} } - recognition = rec; - let finalText = ""; - setMicRecording(); - statusEl.textContent = "höre zu … (zum Stoppen erneut tippen)"; - rec.onresult = (e) => { - let interim = ""; - for (let i = e.resultIndex; i < e.results.length; i++) { - const t = e.results[i][0].transcript; - if (e.results[i].isFinal) finalText += t; else interim += t; - } - promptEl.value = (finalText + interim).trim(); // Live-Feedback im Eingabefeld - }; - 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") { - startRecording(SERVER_STT_MSG); - return; - } - setMicIdle(); - statusEl.textContent = "Spracherkennung fehlgeschlagen (" + err + ")"; - }; - rec.onend = () => { - recognition = null; - setMicIdle(); - const text = (promptEl.value || "").trim(); - promptEl.value = ""; - if (text) sendText(text); // -> vorhandener Text-Turn (inkl. Geräte-/Server-TTS) - }; - try { rec.start(); } catch (e) { - recognition = null; - startRecording(SERVER_STT_MSG); // Start scheiterte -> Server-Fallback - } -} -function stopDeviceStt() { if (recognition) { try { recognition.stop(); } catch (e) {} } } - const PLAYBACK_KEY = "va-playback"; // geräte-lokal: "device" wenn Geräte-TTS gewählt -// Setzt das Qualität-Dropdown beim Laden. Vorrang: -// 1. lokale Wahl (localStorage), 2. Server-Pref (tts_provider), 3. Mobil-Default = Gerät. +// Setzt das verborgene Quell-Select (#tts) beim Laden + spiegelt es in die Presets. +// Vorrang: 1. lokale Wahl (localStorage), 2. Server-Pref (tts_provider), 3. Mobil-Default = Gerät. function applyPlaybackChoice(serverProvider) { if (!ttsSel) return; - // "Gerät"-Option ausblenden, wenn der Browser keine Web Speech API hat. + // "Im Gerät"-Preset ausblenden, wenn der Browser keine Web Speech API hat. if (!TTS_SUPPORTED) { const opt = ttsSel.querySelector('option[value="device"]'); if (opt) opt.remove(); + const btn = document.querySelector('.tts-preset[data-tts="device"]'); + if (btn) btn.classList.add("hidden"); } const local = localStorage.getItem(PLAYBACK_KEY); if (local === "device" && TTS_SUPPORTED) { @@ -261,7 +124,34 @@ function applyPlaybackChoice(serverProvider) { } else if (TTS_SUPPORTED && isMobile() && local === null) { ttsSel.value = "device"; // Mobil-Default, solange nichts gewählt wurde localStorage.setItem(PLAYBACK_KEY, "device"); + } else if (!ttsSel.value) { + ttsSel.value = "piper"; // Fallback: Server } + syncPresetUI(); +} + +// Markiert das aktive Ton-Preset (Häkchen + Rahmen) passend zu #tts. +function syncPresetUI() { + const val = ttsSel ? ttsSel.value : ""; + document.querySelectorAll(".tts-preset").forEach((btn) => { + const active = btn.dataset.tts === val || + (val === "openrouter" && btn.dataset.tts === "chatterbox"); // Cloud -> "Beste Qualität" + btn.classList.toggle("ring-2", active); + btn.classList.toggle("ring-blue-500", active); + btn.classList.toggle("border-blue-500", active); + const chk = btn.querySelector(".check"); + if (chk) chk.style.opacity = active ? "1" : "0"; + }); +} + +// ---------- Einstellungs-Menü (⋮) ---------- +function openMenu() { + $("#settings-sheet").classList.remove("hidden"); + $("#settings-backdrop").classList.remove("hidden"); +} +function closeMenu() { + $("#settings-sheet").classList.add("hidden"); + $("#settings-backdrop").classList.add("hidden"); } let busy = false; @@ -306,8 +196,6 @@ function loadMe() { : (prefs.language || "flex"); } applyPlaybackChoice(prefs.tts_provider); - meAllowCloudStt = !!me.allow_cloud_stt; - applySttChoice(); return me; } catch (e) { $("#identity").textContent = "Verbindung fehlgeschlagen"; @@ -666,18 +554,14 @@ micBtn.addEventListener("click", () => { stopAudio(); statusEl.textContent = "Unterbrochen"; // busy + Button-Reset erfolgen sobald die WS schliesst (-> runTurn resolve -> sendVoice/sendText) - } else if (recognition) { - stopDeviceStt(); // Geräte-STT läuft -> stoppen (onend sendet den Text) } else if (mediaRecorder && mediaRecorder.state === "recording") { stopRecording(); - } else if (useDeviceStt()) { - startDeviceStt(); // lokale Spracherkennung (nur Fix + erlaubt) } else { - startRecording(); // Server-STT (Upload) — wie bisher, auch am Desktop + startRecording(); // Server-STT (Upload) — überall gleich } }); -$("#admin-toggle").addEventListener("click", openAdminPanel); +$("#admin-toggle").addEventListener("click", () => { closeMenu(); openAdminPanel(); }); // ---------- Neues Gespräch ---------- // Startet eine frische Session (neuer session_id) -> der bisherige Verlauf wird nicht @@ -694,12 +578,29 @@ function newConversation() { if (statusEl.textContent === "Neues Gespräch begonnen") statusEl.textContent = ""; }, 2500); } -$("#new-chat").addEventListener("click", newConversation); +$("#new-chat").addEventListener("click", () => { closeMenu(); newConversation(); }); + +// ---------- Menü (⋮) verdrahten ---------- +$("#menu-toggle").addEventListener("click", () => + $("#settings-sheet").classList.contains("hidden") ? openMenu() : closeMenu()); +$("#settings-backdrop").addEventListener("click", closeMenu); +document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeMenu(); }); +// Ton-Presets: setzen das verborgene #tts + lösen die bestehende Persistenz-Logik aus. +document.querySelectorAll(".tts-preset").forEach((btn) => + btn.addEventListener("click", () => { + unlockTTS(); + ttsSel.value = btn.dataset.tts; + ttsSel.dispatchEvent(new Event("change")); + syncPresetUI(); + }) +); // Tag/Nacht: Umschalter (manuell) + automatisch dem System folgen, solange nichts gewählt. const themeBtn = $("#theme-toggle"); -const setThemeIcon = () => - (themeBtn.textContent = document.documentElement.classList.contains("dark") ? "☀️" : "🌙"); +const setThemeIcon = () => { + const icon = $("#theme-icon"); + if (icon) icon.textContent = document.documentElement.classList.contains("dark") ? "☀️" : "🌙"; +}; themeBtn.addEventListener("click", () => { const dark = document.documentElement.classList.toggle("dark"); localStorage.setItem("theme", dark ? "dark" : "light"); @@ -734,9 +635,6 @@ 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(); @@ -1541,8 +1439,6 @@ const FIELD_META = { labels: { "true":"Aktiviert","false":"Deaktiviert" } }, memory_extraction_every_n_turns: { ui: "number", min: 1, max: 20 }, daily_request_limit: { ui: "number", min: 0 }, - allow_cloud_stt: { ui: "select", opts: ["true","false"], - labels: { "true":"Erlaubt (auch Cloud-Erkennung)","false":"Nur on-device (Datenschutz)" } }, }; const FIELD_GROUPS = [ @@ -1553,7 +1449,6 @@ const FIELD_GROUPS = [ { label: "TTS-Verarbeitung", keys: ["tts_normalize_level","audio_stream_default"] }, { label: "Gedächtnis", keys: ["memory_extraction_enabled","memory_extraction_every_n_turns"] }, { label: "Limits", keys: ["daily_request_limit"] }, - { label: "Geräte-STT (mobil)", keys: ["allow_cloud_stt"] }, ]; let _settingsData = []; diff --git a/app/web/index.html b/app/web/index.html index e408da1..44634b0 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -201,8 +201,9 @@
+

Voice Assistant

- - - - - - - +
-
- lade … - + + + + + +
@@ -257,6 +300,6 @@
- + diff --git a/tests/test_audit.py b/tests/test_audit.py index 972f85d..b5415e8 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -44,17 +44,3 @@ def test_backend_switch_rejection_is_audited(caplog): assert "action=llm_backend_switch_rejected" in "\n".join(caplog.messages) -def test_me_exposes_allow_cloud_stt(): - # /api/me liefert das Datenschutz-Flag (Client-Gate für Geräte-STT). - d = client.get("/api/me").json() - assert "allow_cloud_stt" in d - assert isinstance(d["allow_cloud_stt"], bool) - - -def test_allow_cloud_stt_runtime_settable(): - from app.runtime_config import RUNTIME_SETTABLE - assert "allow_cloud_stt" in RUNTIME_SETTABLE - r = client.put("/api/admin/config/allow_cloud_stt", json={"value": "true"}, headers=ADM_HDR) - assert r.status_code == 200 - assert client.get("/api/me").json()["allow_cloud_stt"] is True - client.delete("/api/admin/config/allow_cloud_stt", headers=ADM_HDR)