diff --git a/README.md b/README.md index cc30907..463d7a6 100644 --- a/README.md +++ b/README.md @@ -851,6 +851,8 @@ docker exec aria-core ssh aria-wohnung hostname - [x] Sprachnachrichten-Bubble: audioRequestId statt Substring-Match — keine vertauschten Bubbles mehr bei parallelen Aufnahmen - [x] Bereit-Sound (Airplane Ding-Dong) wenn Mikro nach Wake-Word offen ist — akustische Bestaetigung, in Settings abschaltbar - [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] 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/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 1b66e90..562a516 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -142,9 +142,10 @@ const ChatScreen: React.FC = () => { return `msg_${Date.now()}_${messageIdCounter.current}`; }; - // TTS-Settings beim Mount + bei Screen-Fokus neu laden (damit Settings-Toggle sofort greift) + // TTS- + GPS-Settings beim Mount + alle 2s neu laden (damit Settings-Toggle + // sofort greift, ohne Context- oder Event-System) useEffect(() => { - const loadTtsSettings = async () => { + const loadSettings = async () => { const enabled = await AsyncStorage.getItem('aria_tts_enabled'); setTtsDeviceEnabled(enabled !== 'false'); // default true const muted = await AsyncStorage.getItem('aria_tts_muted'); @@ -152,10 +153,11 @@ const ChatScreen: React.FC = () => { const voice = await AsyncStorage.getItem('aria_xtts_voice'); localXttsVoiceRef.current = voice || ''; ttsSpeedRef.current = await loadTtsSpeed(); + const gps = await AsyncStorage.getItem('aria_gps_enabled'); + setGpsEnabled(gps === 'true'); }; - loadTtsSettings(); - // Poll alle 2s um Settings-Aenderung mitzubekommen (einfache Loesung ohne Context) - const interval = setInterval(loadTtsSettings, 2000); + loadSettings(); + const interval = setInterval(loadSettings, 2000); return () => clearInterval(interval); }, []); diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 068c138..5121632 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -159,6 +159,9 @@ const SettingsScreen: React.FC = () => { AsyncStorage.getItem('aria_tts_enabled').then(saved => { if (saved !== null) setTtsEnabled(saved === 'true'); }); + AsyncStorage.getItem('aria_gps_enabled').then(saved => { + if (saved !== null) setGpsEnabled(saved === 'true'); + }); AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseFloat(saved); @@ -437,7 +440,7 @@ const SettingsScreen: React.FC = () => { const handleGPSToggle = useCallback((value: boolean) => { setGpsEnabled(value); - // In Produktion: Wert in AsyncStorage persistieren + AsyncStorage.setItem('aria_gps_enabled', String(value)).catch(() => {}); }, []); // --- XTTS Voice --- @@ -661,7 +664,11 @@ const SettingsScreen: React.FC = () => { GPS-Position mitsenden - Standort wird automatisch an Nachrichten angehaengt + Position (lat/lon) wird mit jeder Nachricht an ARIA mitgeschickt. + Sie sieht's nur intern und nutzt es bei standortbezogenen Fragen + ("wo bin ich?", "Wetter hier?"), erwaehnt es sonst nicht. + Im Chat-Verlauf bleibt die Bubble unveraendert — nur ARIAs + Antwort kann darauf eingehen. str: + """Baut den Text fuer aria-core mit allen relevanten Hints (Barge-In, + GPS-Position). Hints sind in eckigen Klammern, der eigentliche User- + Text folgt unverandert.""" + parts: list[str] = [] + if interrupted: + parts.append( + "[Hinweis: Stefan hat dich gerade unterbrochen waehrend du noch " + "gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur, " + "Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.]" + ) + if location and isinstance(location, dict): + lat = location.get("lat") + lon = location.get("lon") or location.get("lng") + if lat is not None and lon is not None: + parts.append( + f"[Stefans aktuelle GPS-Position: {float(lat):.6f}, {float(lon):.6f}. " + f"Nutze die nur wenn die Frage sich auf seinen Standort bezieht. " + f"Erwaehne sie nicht von dir aus, ausser er fragt explizit danach.]" + ) + if parts: + return " ".join(parts) + " " + text + return text + def _build_pending_files_message(self, user_text: str) -> str: """Baut eine Anweisung an aria-core aus den gepufferten Files + optionalem User-Text. user_text leer → 'warte auf Anweisung'-Variante.""" @@ -1236,6 +1261,7 @@ class ARIABridge: self._next_speed_override = None if text: interrupted = bool(payload.get("interrupted", False)) + location = payload.get("location") or None # Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig # gesendet), mergen wir sie zu einer einzigen Anfrage statt # zwei separater send_to_core-Calls. @@ -1243,15 +1269,11 @@ class ARIABridge: if merged: logger.info("[rvs] App-Chat (mit Anhaengen): '%s'", text[:80]) else: - core_text = ( - f"[Hinweis: Stefan hat dich gerade unterbrochen waehrend du noch " - f"gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur, " - f"Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.] " - f"{text}" - if interrupted else text - ) - logger.info("[rvs] App-Chat%s: '%s'", - " [BARGE-IN]" if interrupted else "", text[:80]) + core_text = self._build_core_text(text, interrupted, location) + logger.info("[rvs] App-Chat%s%s: '%s'", + " [BARGE-IN]" if interrupted else "", + " [GPS]" if location else "", + text[:80]) await self.send_to_core(core_text, source="app" + (" [barge-in]" if interrupted else "")) return @@ -1511,11 +1533,14 @@ class ARIABridge: self._next_speed_override = None interrupted = bool(payload.get("interrupted", False)) audio_request_id = payload.get("audioRequestId", "") or "" - logger.info("[rvs] Audio empfangen: %s, %dms, %dKB%s%s", + location = payload.get("location") or None + logger.info("[rvs] Audio empfangen: %s, %dms, %dKB%s%s%s", mime_type, duration_ms, len(audio_b64) // 1365, " [BARGE-IN]" if interrupted else "", + " [GPS]" if location else "", f" reqId={audio_request_id[:16]}" if audio_request_id else "") - asyncio.create_task(self._process_app_audio(audio_b64, mime_type, interrupted, audio_request_id)) + asyncio.create_task(self._process_app_audio( + audio_b64, mime_type, interrupted, audio_request_id, location)) elif msg_type == "stt_response": # Antwort der whisper-bridge auf unseren stt_request @@ -1573,7 +1598,8 @@ class ARIABridge: async def _process_app_audio(self, audio_b64: str, mime_type: str, interrupted: bool = False, - audio_request_id: str = "") -> None: + audio_request_id: str = "", + location: Optional[dict] = None) -> None: """App-Audio → STT → aria-core. Primaer via whisper-bridge (RVS), Fallback lokal. interrupted=True wenn der User waehrend ARIA noch sprach/dachte aufgenommen hat @@ -1583,7 +1609,10 @@ class ARIABridge: audio_request_id: Korrelations-ID die die App im audio-Event mitschickt — wird unveraendert ans STT-Result zurueckgegeben damit die App die EXAKT richtige - 'wird verarbeitet'-Bubble ersetzen kann (auch bei mehreren parallelen Aufnahmen).""" + 'wird verarbeitet'-Bubble ersetzen kann (auch bei mehreren parallelen Aufnahmen). + + location: Optional GPS-Position {lat, lon} — wird als Hinweis-Praefix mitgegeben + damit ARIA bei standortbezogenen Fragen sie nutzen kann.""" # Erst Remote versuchen text = await self._stt_remote(audio_b64, mime_type) if text is None: @@ -1595,15 +1624,9 @@ class ARIABridge: if text.strip(): logger.info("[rvs] STT Ergebnis: '%s'", text[:80]) - # Barge-In-Hinweis: gibt ARIA den Kontext dass sie unterbrochen wurde - # und dies eine Korrektur/Aenderung der vorherigen Anweisung sein kann. - core_text = ( - f"[Hinweis: Stefan hat dich gerade unterbrochen waehrend du noch " - f"gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur, " - f"Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.] " - f"{text}" - if interrupted else text - ) + # Hints (Barge-In, GPS) als Praefix vorschalten — gemeinsamer Helper + # mit dem chat-Pfad damit das Verhalten konsistent ist. + core_text = self._build_core_text(text, interrupted, location) # ERST an aria-core senden (wichtigster Schritt) await self.send_to_core(core_text, source="app-voice" + (" [barge-in]" if interrupted else "")) # STT-Text an RVS senden (fuer Anzeige in App + Diagnostic) diff --git a/issue.md b/issue.md index 830d9d8..1e6eea2 100644 --- a/issue.md +++ b/issue.md @@ -108,6 +108,9 @@ - [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] 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 ## Offen