From d6b54d324735f3999816e5f82a87f5d5d2caed66 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 6 May 2026 23:43:24 +0200 Subject: [PATCH] feat(audio): Background-Service auch fuer Wake-Word + Aufnahme + Doku-Split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erweitert den Foreground-Service um den microphone-Type damit nicht nur TTS, sondern auch Wake-Word-Lauschen und aktive Aufnahmen weiterlaufen wenn die App im Hintergrund ist. Slot-System (backgroundAudio.ts): - 'tts' : ARIA spricht - 'rec' : Aufnahme laeuft - 'wake' : Wake-Word lauscht passiv (Ohr aktiv) Mehrere Slots koennen unabhaengig acquired/released werden, der Service laeuft solange mindestens einer aktiv ist. Notification-Text passt sich dynamisch an den hoechstprioren Slot an (tts > rec > wake). Wiring (ChatScreen): - onPlaybackStarted/Finished → 'tts' Slot - audioService.onStateChange (recording) → 'rec' Slot - wakeWordService.onStateChange (off→armed/conversing) → 'wake' Slot AndroidManifest: - foregroundServiceType="mediaPlayback|microphone" (Pflicht ab Android 14 fuer Background-Mic-Zugriff) - FOREGROUND_SERVICE_MICROPHONE Permission Doku: - issue.md Erledigt-Sektion in "Bugs / Fixes", "App Features" und "Infrastruktur" gesplittet - README: Background-Service-Beschreibung erweitert Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- .../android/app/src/main/AndroidManifest.xml | 8 +- .../com/ariacockpit/AriaPlaybackService.kt | 22 ++++-- .../com/ariacockpit/BackgroundAudioModule.kt | 3 +- android/src/screens/ChatScreen.tsx | 29 +++++-- android/src/services/backgroundAudio.ts | 77 ++++++++++++------ issue.md | 79 ++++++++++--------- 7 files changed, 147 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index c2fc41b..450f21f 100644 --- a/README.md +++ b/README.md @@ -853,7 +853,7 @@ docker exec aria-core ssh aria-wohnung hostname - [x] Wake-Word parallel zu TTS mit AcousticEchoCanceler — "Computer" sagen waehrend ARIA spricht stoppt sie und oeffnet Mikro - [x] GPS-Position mit Nachrichten mitsenden (Toggle in Settings) — ARIA nutzt sie nur bei standortbezogenen Fragen, im Chat sichtbar nur in ihrer Antwort - [x] Sprachnachrichten ohne STT-Result werden nach Timeout automatisch entfernt (skaliert mit Aufnahmedauer) -- [x] Background Audio Service: TTS laeuft auch bei minimierter App weiter (Foreground-Service mit MediaPlayback-Notification) +- [x] Background Audio Service: TTS, Wake-Word-Lauschen + Aufnahme laufen auch bei minimierter App weiter (Foreground-Service mit mediaPlayback|microphone, dynamische Notification) - [x] Disk-Voll Banner in Diagnostic mit copy-baren Cleanup-Befehlen - [x] Wake-Word on-device via openWakeWord (ONNX Runtime, kein API-Key) + State-Icon diff --git a/android/android/app/src/main/AndroidManifest.xml b/android/android/app/src/main/AndroidManifest.xml index ae06edb..d463ac7 100644 --- a/android/android/app/src/main/AndroidManifest.xml +++ b/android/android/app/src/main/AndroidManifest.xml @@ -6,9 +6,13 @@ - + + + android:foregroundServiceType="mediaPlayback|microphone" /> diff --git a/android/android/app/src/main/java/com/ariacockpit/AriaPlaybackService.kt b/android/android/app/src/main/java/com/ariacockpit/AriaPlaybackService.kt index f02addf..4f2121f 100644 --- a/android/android/app/src/main/java/com/ariacockpit/AriaPlaybackService.kt +++ b/android/android/app/src/main/java/com/ariacockpit/AriaPlaybackService.kt @@ -27,17 +27,22 @@ class AriaPlaybackService : Service() { private const val TAG = "AriaPlaybackService" private const val CHANNEL_ID = "aria_playback" private const val NOTIFICATION_ID = 1042 + const val EXTRA_REASON = "reason" // "tts" | "wake" | "rec" | "" } + private var currentReason: String = "" + override fun onCreate() { super.onCreate() ensureNotificationChannel() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.i(TAG, "Foreground-Service gestartet") + val reason = intent?.getStringExtra(EXTRA_REASON) ?: "" + currentReason = reason + Log.i(TAG, "Foreground-Service start/update (reason=$reason)") try { - startForeground(NOTIFICATION_ID, buildNotification()) + startForeground(NOTIFICATION_ID, buildNotification(reason)) } catch (e: Exception) { Log.e(TAG, "startForeground fehlgeschlagen", e) stopSelf() @@ -71,7 +76,7 @@ class AriaPlaybackService : Service() { } } - private fun buildNotification(): Notification { + private fun buildNotification(reason: String): Notification { val launchIntent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } @@ -81,9 +86,16 @@ class AriaPlaybackService : Service() { PendingIntent.FLAG_UPDATE_CURRENT val pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, pendingFlags) + val (title, body) = when (reason) { + "tts" -> "ARIA spricht" to "Antwort wird abgespielt — antippen oeffnet die App" + "rec" -> "ARIA hoert zu" to "Sprachaufnahme laeuft — antippen oeffnet die App" + "wake" -> "ARIA bereit" to "Wake-Word lauscht passiv — antippen oeffnet die App" + else -> "ARIA aktiv" to "Hintergrund-Modus — antippen oeffnet die App" + } + return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("ARIA spricht") - .setContentText("Antwort wird abgespielt — antippen oeffnet die App") + .setContentTitle(title) + .setContentText(body) .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingIntent) .setOngoing(true) diff --git a/android/android/app/src/main/java/com/ariacockpit/BackgroundAudioModule.kt b/android/android/app/src/main/java/com/ariacockpit/BackgroundAudioModule.kt index 2361527..838b344 100644 --- a/android/android/app/src/main/java/com/ariacockpit/BackgroundAudioModule.kt +++ b/android/android/app/src/main/java/com/ariacockpit/BackgroundAudioModule.kt @@ -25,10 +25,11 @@ class BackgroundAudioModule(reactContext: ReactApplicationContext) : ReactContex companion object { private const val TAG = "BackgroundAudio" } @ReactMethod - fun start(promise: Promise) { + fun start(reason: String, promise: Promise) { try { val ctx = reactApplicationContext val intent = Intent(ctx, AriaPlaybackService::class.java) + intent.putExtra(AriaPlaybackService.EXTRA_REASON, reason ?: "") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ctx.startForegroundService(intent) } else { diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index cda9626..0a5c748 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -27,7 +27,10 @@ import audioService from '../services/audio'; import wakeWordService from '../services/wakeword'; import phoneCallService from '../services/phoneCall'; import { playWakeReadySound } from '../services/wakeReadySound'; -import { startBackgroundAudio, stopBackgroundAudio } from '../services/backgroundAudio'; +import { + acquireBackgroundAudio, + releaseBackgroundAudio, +} from '../services/backgroundAudio'; import updateService from '../services/updater'; import VoiceButton from '../components/VoiceButton'; import FileUpload, { FileData } from '../components/FileUpload'; @@ -174,6 +177,11 @@ const ChatScreen: React.FC = () => { // 'armed' oder 'off' fallen, darf Spotify wieder. if (s === 'conversing') audioService.acquireConversationFocus(); else audioService.releaseConversationFocus(); + // Foreground-Service-Slot 'wake' — solange das Ohr ueberhaupt aktiv ist + // (armed oder conversing), soll der App-Prozess im Hintergrund am Leben + // bleiben damit Mikro-Lauschen + Aufnahme weiterlaufen. + if (s !== 'off') acquireBackgroundAudio('wake').catch(() => {}); + else releaseBackgroundAudio('wake').catch(() => {}); }); return () => unsub(); }, []); @@ -185,6 +193,17 @@ const ChatScreen: React.FC = () => { return () => { phoneCallService.stop().catch(() => {}); }; }, []); + // Recording-State an Background-Service-Slot 'rec' koppeln — damit das Mikro + // auch im Hintergrund weiter aufnehmen darf (Android killt den App-Prozess + // sonst und die Aufnahme bricht ab). + useEffect(() => { + const unsub = audioService.onStateChange((s) => { + if (s === 'recording') acquireBackgroundAudio('rec').catch(() => {}); + else releaseBackgroundAudio('rec').catch(() => {}); + }); + return () => unsub(); + }, []); + // ttsCanPlayRef live aktuell halten — Closure in onMessage unten liest // darueber statt direkt ttsDeviceEnabled/ttsMuted (sonst stale). useEffect(() => { @@ -569,16 +588,16 @@ const ChatScreen: React.FC = () => { // TTS-Lifecycle: solange ARIA spricht und Wake-Word verfuegbar ist, // parallel mitlauschen — User kann "Computer" sagen statt manuell tappen. - // PLUS: Foreground-Service starten damit Android den App-Prozess nicht - // killt wenn die App im Hintergrund ist (TTS waere sonst mitten im Satz weg). + // PLUS: Foreground-Service-Slot 'tts' belegen damit Android den App- + // Prozess nicht killt wenn die App im Hintergrund ist. const unsubTtsStart = audioService.onPlaybackStarted(() => { - startBackgroundAudio().catch(() => {}); + acquireBackgroundAudio('tts').catch(() => {}); if (wakeWordService.isConversing() && wakeWordService.hasWakeWord()) { wakeWordService.startBargeListening().catch(() => {}); } }); const unsubTtsEnd = audioService.onPlaybackFinished(() => { - stopBackgroundAudio().catch(() => {}); + releaseBackgroundAudio('tts').catch(() => {}); // Vor naechster Aufnahme: barge-listening aus damit der AudioRecorder // das Mikro greifen kann. wakeWordService.stopBargeListening().catch(() => {}); diff --git a/android/src/services/backgroundAudio.ts b/android/src/services/backgroundAudio.ts index 59b1393..a5bd62c 100644 --- a/android/src/services/backgroundAudio.ts +++ b/android/src/services/backgroundAudio.ts @@ -1,45 +1,76 @@ /** - * Background-Audio: ARIAs TTS soll auch bei minimierter App weiterlaufen. - * Wir starten dafuer einen Foreground-Service mit foregroundServiceType= - * mediaPlayback, der eine persistente Notification zeigt waehrend ARIA spricht. + * Background-Audio: ARIAs TTS, Mic-Aufnahme und Wake-Word-Lauschen sollen + * auch bei minimierter App weiterlaufen. Wir starten dafuer einen Foreground- + * Service mit foregroundServiceType=mediaPlayback|microphone, der eine + * persistente Notification zeigt waehrend irgendein Audio-Slot aktiv ist. * - * API ist intentional simpel — start() vor TTS-Wiedergabe, stop() danach. - * Idempotent: mehrfaches start/stop ist sicher. + * Mehrere Komponenten koennen den Service unabhaengig "halten": + * - 'tts' : ARIA spricht + * - 'rec' : Aufnahme laeuft + * - 'wake' : Wake-Word lauscht passiv (Ohr aktiv) + * + * Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle + * Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an + * den hoechstprioren Slot an (tts > rec > wake). */ import { NativeModules } from 'react-native'; interface BackgroundAudioNative { - start(): Promise; + start(reason: string): Promise; stop(): Promise; } const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative }; -let active = false; +type Slot = 'tts' | 'rec' | 'wake'; -export async function startBackgroundAudio(): Promise { - if (active || !BackgroundAudio) return; +const slots = new Set(); + +// Prioritaet fuer den Notification-Text — hoechste zuerst. +const PRIORITY: Slot[] = ['tts', 'rec', 'wake']; + +function topReason(): string { + for (const s of PRIORITY) { + if (slots.has(s)) return s; + } + return ''; +} + +async function applyState(): Promise { + if (!BackgroundAudio) return; + if (slots.size === 0) { + try { await BackgroundAudio.stop(); } catch {} + console.log('[BackgroundAudio] Service gestoppt (keine Slots)'); + return; + } + const reason = topReason(); try { - await BackgroundAudio.start(); - active = true; - console.log('[BackgroundAudio] Foreground-Service gestartet'); + await BackgroundAudio.start(reason); + console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)', + reason, [...slots].join('+')); } catch (err: any) { console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err); } } -export async function stopBackgroundAudio(): Promise { - if (!active || !BackgroundAudio) return; - try { - await BackgroundAudio.stop(); - active = false; - console.log('[BackgroundAudio] Foreground-Service gestoppt'); - } catch (err: any) { - console.warn('[BackgroundAudio] stop fehlgeschlagen:', err?.message || err); - } +export async function acquireBackgroundAudio(slot: Slot): Promise { + if (slots.has(slot)) return; + slots.add(slot); + await applyState(); } -export function isBackgroundAudioActive(): boolean { - return active; +export async function releaseBackgroundAudio(slot: Slot): Promise { + if (!slots.has(slot)) return; + slots.delete(slot); + await applyState(); } + +export function backgroundAudioActive(): boolean { + return slots.size > 0; +} + +// --- Legacy API (nur tts-Slot) — fuer Aufruf-Sites die noch nichts vom Slot- +// system wissen. Mappt auf den 'tts'-Slot. --- +export const startBackgroundAudio = () => acquireBackgroundAudio('tts'); +export const stopBackgroundAudio = () => releaseBackgroundAudio('tts'); diff --git a/issue.md b/issue.md index 1a44f7f..4ffb53b 100644 --- a/issue.md +++ b/issue.md @@ -2,6 +2,37 @@ ## Erledigt +### Bugs / Fixes + +- [x] Diagnostic: "ARIA denkt..." bleibt nicht mehr stehen +- [x] App: "ARIA denkt..." Indicator + Abbrechen-Button (Bridge spiegelt agent_activity via RVS) +- [x] Textnachrichten werden von ARIA beantwortet (Bridge chat handler fix) +- [x] Voice-Auswahl funktioniert wieder: speaker_wav als Basename statt Pfad fuer daswer123 local-Mode +- [x] Diagnostic-Voice-Wechsel resettet alle App-lokalen Voice-Overrides via type "config" +- [x] Streaming TTS Stop-Race: Writer wartet auf playbackHeadPosition vor stop()/release() — keine abgeschnittenen Saetze mehr +- [x] App: Audioausgabe hoert nicht mehr mitten im Satz auf (playbackHeadPosition wait + Stop-Race fix) +- [x] AudioFocus.release wartet auf echten Playback-Ende — kein Volume-Hochfahren mehr mid-Antwort +- [x] App Mute-/Auto-Playback-Bug: Closure-Bug geloest (ttsCanPlayRef live-gespiegelt, nicht mehr stale) +- [x] App Zombie-Recording: Ohr-aus kill laufende Aufnahme damit der Aufnahme-Button weiter funktioniert +- [x] Whisper transkribiert Voice-Uploads nicht mehr mit hardcoded "small" — aktuelles Modell wird behalten, kein unnoetiger Modell-Swap +- [x] RVS/WebSocket maxPayload 50MB: voice_upload mit WAV als base64 sprengt kein Frame-Limit mehr +- [x] Wake-Word Embedding rank-4 Fix (Pipeline-Bug der das Triggern verhinderte) + Frame-Count aus Modell-Metadaten lesen +- [x] PCM-Underrun-Schutz: Stille-Fill in Render-Pausen verhindert Spotify-Auto-Resume nach 10s Stillstand +- [x] Conversation-Focus-Lifecycle: AudioFocus haengt am Wake-Word-State 'conversing' statt an einzelnen Streams — Spotify bleibt durchgehend gepaust, auch zwischen mehreren Antworten +- [x] Voice-Override behaelt Stimme ueber alle TTS-Calls einer Antwort (vorher: nach erstem TTS-Call zurueck auf Default) +- [x] Sprachnachricht-Bubble defensiv: STT-Result fuegt neue Bubble hinzu wenn Placeholder fehlt (Race-Schutz) +- [x] Bild + Text als EINE Anfrage: Bridge buffert files 800ms, merged mit folgendem chat-Text zu einem send_to_core (statt zwei getrennten ARIA-Antworten) +- [x] Diagnostic→App: persistente RVS-Connection statt frische pro Send (Race-Probleme mit Zombie-WS geloest) +- [x] Textauswahl in Bubbles wieder funktional (nested Text+onPress raus, dataDetectorType="all" macht Links automatisch klickbar) +- [x] **Placeholder-Race bei parallelen Sprachnachrichten geloest**: jede Aufnahme bekommt eine eindeutige audioRequestId, Bridge gibt sie ans STT-Result zurueck — App matcht jetzt punktgenau die richtige Bubble statt per Substring +- [x] Mikro-Offen-Toast "🎤 sprich jetzt" erscheint erst wenn audioService.startRecording wirklich erfolgreich war (statt ~400ms vorher beim Wake-Word-Detect) +- [x] Sprachnachrichten ohne STT-Result werden nach 60s+Aufnahmedauer automatisch entfernt (sicher genug fuer 5-30min-Aufnahmen, schnell genug fuer leere Wake-Word-Echos) +- [x] VAD adaptive Baseline robuster: minimum statt avg + Cap auf -50dB bis -28dB (Stille) / -40dB bis -18dB (Speech) — keine "tote" VAD-Konfiguration mehr bei lauter Umgebung oder Wake-Word-Echo +- [x] Push-to-Talk raus, nur noch Tap-to-Talk (verhinderte Touch-Race-Probleme) +- [x] Manueller Mikro-Stop beendet Wake-Word-Konversation: Tap auf Mikro-Knopf waehrend conversing → audio raus + zurueck zu armed (= Wake-Word lauscht wieder, kein Auto-Mikro nach ARIAs Antwort). VAD-Auto-Stop bleibt bei Multi-Turn + +### App Features + - [x] Bildupload funktioniert (Shared Volume /shared/uploads/) - [x] Sprachnachrichten werden als Text angezeigt (STT → Chat-Bubble) - [x] Cache leeren + Auto-Download von Anhaengen @@ -11,11 +42,7 @@ - [x] Ohr-Button → Gespraechsmodus (Auto-Aufnahme nach ARIA-Antwort) - [x] Play-Button in ARIA-Nachrichten fuer Sprachwiedergabe - [x] Chat-Suche in der App (Lupe in Statusleiste) -- [x] Watchdog mit Container-Restart (2min Warnung → 5min doctor --fix → 8min Restart) - [x] Abbrechen-Button im Diagnostic Chat -- [x] Nachrichten Backup on-the-fly (/shared/config/chat_backup.jsonl) -- [x] Grosse Nachrichten satzweise aufteilen fuer TTS -- [x] RVS Nachrichten vom Smartphone gehen durch - [x] Stimmen-Einstellungen (Ramona/Thorsten, Speed pro Stimme — durch XTTS/F5-TTS ersetzt) - [x] Highlight-Trigger konfigurierbar in Diagnostic - [x] XTTS v2 Integration (Gaming-PC, GPU, Voice Cloning) — durch F5-TTS ersetzt @@ -25,16 +52,12 @@ - [x] Auto-Update: APK-Installation via FileProvider - [x] Auto-Update: "Auf Updates pruefen" Button in App-Einstellungen - [x] Audio-Queue (sequentielle Wiedergabe, kein Ueberlappen) -- [x] Textnachrichten werden von ARIA beantwortet (Bridge chat handler fix) - [x] Mehrere Anhaenge + Text vor dem Senden (Pending-Vorschau) - [x] Paste-Support fuer Bilder in Diagnostic Chat - [x] Markdown-Bereinigung fuer TTS (fett, kursiv, code, links, etc.) -- [x] SSH Volume read-write fuer Proxy (kein -F Workaround mehr) - [x] Diagnostic: Sessions als Markdown exportieren (Download-Button) - [x] Speech Gate: Aufnahme wird verworfen wenn keine Sprache erkannt - [x] Session-Persistenz: Gewaehlte Session bleibt ueber Container-Restarts erhalten -- [x] Diagnostic: "ARIA denkt..." bleibt nicht mehr stehen -- [x] App: "ARIA denkt..." Indicator + Abbrechen-Button (Bridge spiegelt agent_activity via RVS) - [x] Whisper STT: Model-Auswahl in Diagnostic (tiny/base/small/medium/large-v3), Hot-Reload - [x] App: Audio-Aufnahme explizit 16kHz mono (spart Resample, optimal fuer Whisper) - [x] Streaming TTS: PCM-Stream → AudioTrack MODE_STREAM, keine WAV-Gaps @@ -51,14 +74,11 @@ - [x] Disk-Voll Banner in Diagnostic: rotes Overlay + copy-baren Cleanup-Befehlen (safe + aggressiv) - [x] cleanup.sh: kombinierter Docker-Aufraeum-Befehl (safe / --full) - [x] Streaming TTS Pre-Roll: AudioTrack play() startet erst wenn 2.5s gepuffert sind -- [x] Streaming TTS Stop-Race: Writer wartet auf playbackHeadPosition vor stop()/release() — keine abgeschnittenen Saetze mehr - [x] Leading-Silence (200ms) am Stream-Anfang — AudioTrack faehrt sauber an - [x] Pre-Roll-Buffer einstellbar in App-Settings (1.0-6.0s, Default 3.5s) - [x] Fade-In auf erstem PCM-Chunk (120ms) — versteckt XTTS/F5-TTS Warmup-Glitches - [x] Decimal-zu-Worte fuer TTS (0.1 → null komma eins, mit IP-Schutz-Lookahead) - [x] Generic Acronym-Buchstabieren (XTTS → X T T S, USB → U S B, ueber expliziter Liste) -- [x] Voice-Auswahl funktioniert wieder: speaker_wav als Basename statt Pfad fuer daswer123 local-Mode -- [x] Diagnostic-Voice-Wechsel resettet alle App-lokalen Voice-Overrides via type "config" - [x] voice_preload/voice_ready: Stille Mini-Render bei Voice-Wechsel + Toast/Status "bereit" - [x] Whisper STT auf die Gamebox ausgelagert (faster-whisper CUDA, float16) — neuer aria-whisper-bridge Container - [x] aria-bridge: STT primaer remote (Gamebox), Fallback lokal nach 45s Timeout @@ -66,53 +86,40 @@ - [x] **F5-TTS ersetzt XTTS komplett** — neuer aria-f5tts-bridge Container, Voice Cloning, satzweises Streaming - [x] Voice-Upload mit Whisper-Auto-Transkription — User muss keinen Referenz-Text eintippen - [x] Audio-Pause statt Ducking: Spotify/YouTube pausieren komplett waehrend TTS (TRANSIENT statt MAY_DUCK) -- [x] AudioFocus.release wartet auf echten Playback-Ende — kein Volume-Hochfahren mehr mid-Antwort - [x] VAD-Stille einstellbar in App-Settings (1.0-8.0s, Default 2.8s) - [x] MAX_RECORDING auf 120s — laengere Erklaerungen moeglich -- [x] App: Audioausgabe hoert nicht mehr mitten im Satz auf (playbackHeadPosition wait + Stop-Race fix) - [x] F5-TTS: Referenz-WAV-Preprocessing — Loudness-Normalisierung -16 LUFS + Silence-Trim + 10s Clip fuer konsistente Cloning-Quali - [x] F5-TTS: deutsches Fine-Tune (aihpi/F5-TTS-German, Vocos-Variante) via hf:// Pfad in Diagnostic konfigurierbar -- [x] Whisper transkribiert Voice-Uploads nicht mehr mit hardcoded "small" — aktuelles Modell wird behalten, kein unnoetiger Modell-Swap -- [x] RVS/WebSocket maxPayload 50MB: voice_upload mit WAV als base64 sprengt kein Frame-Limit mehr - [x] Dynamischer STT-Timeout in aria-bridge: 300s waehrend whisper-bridge 'loading', 45s wenn 'ready' - [x] service_status Broadcasts: f5tts/whisper melden Lade-Status, Banner in Diagnostic (unten rechts) + App (oben) - [x] config_request Pattern: Bridges fragen beim Connect die aktuelle Voice-Config an, aria-bridge antwortet - [x] F5-TTS Tuning via Diagnostic (Modell-ID, Checkpoint, cfg_strength, nfe_step) statt ENV-Vars — Hot-Reload bei Modell-Wechsel - [x] Conversation-Window: Gespraechsmodus endet nach X Sekunden Stille (1.0-20.0s, Default 8s, einstellbar in Settings) -- [x] Porcupine Wake-Word-Integration in der App (Built-In Keywords + Custom spaeter, per Geraet einstellbar) +- [x] Porcupine Wake-Word-Integration in der App (durch openWakeWord ersetzt) - [x] HF-Cache als Bind-Mount statt Docker Volume — kein .vhdx-Bloat auf Docker Desktop / Windows - [x] cleanup-windows.ps1 / .bat: VHDX-Cleanup via diskpart (ohne Hyper-V) mit Self-Elevation -- [x] App Mute-/Auto-Playback-Bug: Closure-Bug geloest (ttsCanPlayRef live-gespiegelt, nicht mehr stale) -- [x] App Zombie-Recording: Ohr-aus kill laufende Aufnahme damit der Aufnahme-Button weiter funktioniert - [x] App Text-Rendering: Nachrichten selektierbar + Autolink fuer URLs/E-Mails/Telefonnummern (Browser/Mail/Dialer) - [x] TTS-Wiedergabegeschwindigkeit pro Geraet einstellbar (Settings → 0.5-2.0x in 0.1-Schritten, Default 1.0) - [x] Diagnostic: Voice-Preview-Modal (Play-Icon vor Delete-X, Textfeld mit Default, WAV im Browser abspielen) - [x] **Wake-Word komplett on-device via openWakeWord (ONNX Runtime)** — Porcupine raus, kein API-Key/keine Lizenzgebuehren mehr. Mitgelieferte Keywords: hey_jarvis, computer, alexa, hey_mycroft, hey_rhasspy -- [x] Wake-Word Embedding rank-4 Fix (Pipeline-Bug der das Triggern verhinderte) + Frame-Count aus Modell-Metadaten lesen - [x] APK ABI-Split auf arm64-v8a — von ~136 MB auf ~35 MB, Auto-Update-Downloads aufs Phone deutlich kleiner -- [x] PCM-Underrun-Schutz: Stille-Fill in Render-Pausen verhindert Spotify-Auto-Resume nach 10s Stillstand -- [x] Conversation-Focus-Lifecycle: AudioFocus haengt am Wake-Word-State 'conversing' statt an einzelnen Streams — Spotify bleibt durchgehend gepaust, auch zwischen mehreren Antworten - [x] PhoneStateListener: TTS pausiert bei eingehendem Anruf (READ_PHONE_STATE Permission) -- [x] Voice-Override behaelt Stimme ueber alle TTS-Calls einer Antwort (vorher: nach erstem TTS-Call zurueck auf Default) -- [x] Sprachnachricht-Bubble defensiv: STT-Result fuegt neue Bubble hinzu wenn Placeholder fehlt (Race-Schutz) -- [x] Bild + Text als EINE Anfrage: Bridge buffert files 800ms, merged mit folgendem chat-Text zu einem send_to_core (statt zwei getrennten ARIA-Antworten) - [x] Diagnostic-Chat: bubblige Formatierung, mehrzeiliges Eingabefeld (textarea, Enter sendet, Shift+Enter neue Zeile) -- [x] Diagnostic→App: persistente RVS-Connection statt frische pro Send (Race-Probleme mit Zombie-WS geloest) -- [x] Adaptive VAD-Schwelle: Baseline aus den ersten 500ms Mic-Pegel, Stille = baseline+6dB / Sprache = baseline+12dB. Funktioniert in lauten wie leisen Umgebungen +- [x] Adaptive VAD-Schwelle: Baseline aus den ersten 500ms Mic-Pegel, Stille = baseline+6dB / Sprache = baseline+12dB - [x] Max-Aufnahmedauer konfigurierbar in Settings (1-30 min, Default 5 min) — laengere Diktate moeglich - [x] Barge-In: User kann ARIA waehrend Antwort/Tool-Use unterbrechen, alte Aktivitaet wird abgebrochen, Bridge gibt aria-core einen Kontext-Hint dass es eine Korrektur ist -- [x] Push-to-Talk raus, nur noch Tap-to-Talk (verhinderte Touch-Race-Probleme) - [x] Settings-Sub-Screens: 8 Kategorien (Verbindung, Allgemein, Spracheingabe, Wake-Word, Sprachausgabe, Speicher, Protokoll, Ueber) statt langer Liste -- [x] Textauswahl in Bubbles wieder funktional (nested Text+onPress raus, dataDetectorType="all" macht Links automatisch klickbar) -- [x] **Placeholder-Race bei parallelen Sprachnachrichten geloest**: jede Aufnahme bekommt eine eindeutige audioRequestId, Bridge gibt sie ans STT-Result zurueck — App matcht jetzt punktgenau die richtige Bubble statt per Substring "Spracheingabe wird verarbeitet" -- [x] Mikro-Offen-Toast "🎤 sprich jetzt" erscheint erst wenn audioService.startRecording wirklich erfolgreich war (statt ~400ms vorher beim Wake-Word-Detect) - [x] **Bereit-Sound (Airplane Ding-Dong) wenn Mikro nach Wake-Word offen** — akustische Bestaetigung statt nur Toast. Toggle in Settings → Wake-Word, default aktiv -- [x] **Wake-Word parallel zu TTS** mit AcousticEchoCanceler: User sagt "Computer" waehrend ARIA spricht → TTS verstummt sofort, neue Aufnahme startet. Native AEC verhindert dass ARIAs eigene Stimme das Wake-Word triggert. Audio-Source ist VOICE_COMMUNICATION + zusaetzlich AEC/NS/AGC-Effekte aktiviert -- [x] **GPS-Position mitsenden**: Toggle in Settings → Allgemein → Standort, persistiert in AsyncStorage, ChatScreen pollt den Wert. Wenn aktiv wird lat/lon mit jeder chat/audio-Message mitgegeben. Bridge prefixed den Text fuer aria-core mit GPS-Hint (mit Anweisung dass die Position nur bei Bedarf erwaehnt wird, nicht automatisch). Im App-Chat sieht man die Position nicht, nur ARIAs Antwort kann darauf eingehen -- [x] **Background Audio Service**: TTS laeuft auch bei minimierter App weiter. Foreground-Service mit foregroundServiceType=mediaPlayback haelt den Prozess am Leben, persistente Notification "ARIA spricht — antippen oeffnet die App". Service startet bei TTS-Beginn, stoppt bei Ende oder Cancel/Barge-In/Anruf -- [x] Manueller Mikro-Stop beendet Wake-Word-Konversation: Tap auf Mikro-Knopf waehrend conversing → audio raus + zurueck zu armed (= Wake-Word lauscht wieder, kein Auto-Mikro nach ARIAs Antwort). VAD-Auto-Stop bleibt bei Multi-Turn -- [x] Sprachnachrichten ohne STT-Result werden nach 60s+Aufnahmedauer automatisch entfernt (sicher genug fuer 5-30min-Aufnahmen, schnell genug fuer leere Wake-Word-Echos) -- [x] VAD adaptive Baseline robuster: minimum statt avg + Cap auf -50dB bis -28dB (Stille) / -40dB bis -18dB (Speech) — keine "tote" VAD-Konfiguration mehr bei lauter Umgebung oder Wake-Word-Echo +- [x] **Wake-Word parallel zu TTS** mit AcousticEchoCanceler: User sagt "Computer" waehrend ARIA spricht → TTS verstummt sofort, neue Aufnahme startet +- [x] **GPS-Position mitsenden**: Toggle in Settings → Allgemein → Standort, persistiert in AsyncStorage. Wenn aktiv wird lat/lon mit jeder chat/audio-Message mitgegeben. Bridge prefixed den Text fuer aria-core mit GPS-Hint (mit Anweisung dass die Position nur bei Bedarf erwaehnt wird) +- [x] **Background Audio Service**: TTS, Wake-Word-Lauschen UND Aufnahme laufen auch bei minimierter App weiter. Foreground-Service mit foregroundServiceType=mediaPlayback|microphone, persistente Notification mit dynamischem Text ("ARIA spricht" / "ARIA hoert zu" / "ARIA bereit") + +### Infrastruktur + +- [x] Watchdog mit Container-Restart (2min Warnung → 5min doctor --fix → 8min Restart) +- [x] Nachrichten Backup on-the-fly (/shared/config/chat_backup.jsonl) +- [x] RVS Nachrichten vom Smartphone gehen durch +- [x] SSH Volume read-write fuer Proxy (kein -F Workaround mehr) ## Offen