feat: GPS-Position bei jeder Nachricht an aria-core (still, nur bei Bedarf)
App: GPS-Toggle in Settings → Allgemein → Standort wird jetzt korrekt
in AsyncStorage persistiert (key: aria_gps_enabled). ChatScreen pollt
den Wert mit den anderen Settings im 2s-Intervall.
Bridge: chat/audio-Handler nutzen jetzt einen gemeinsamen _build_core_text
Helper, der je nach Kontext einen Hint vorschaltet:
- Barge-In ("[Hinweis: Stefan hat dich unterbrochen ...]")
- GPS ("[Stefans aktuelle GPS-Position: lat, lon. Nutze die nur wenn
die Frage sich auf seinen Standort bezieht. Erwaehne sie nicht
von dir aus, ausser er fragt explizit danach.]")
ARIA weiss bei "wo bin ich?" / "Wetter hier?" automatisch was zu tun ist
— bei normalen Fragen kommt die Position aber nicht ungefragt vor. Der
User sieht im Chat-Verlauf nichts von der GPS-Info, nur ARIAs Antwort
kann darauf eingehen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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] 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] 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] 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] Disk-Voll Banner in Diagnostic mit copy-baren Cleanup-Befehlen
|
||||||
- [x] Wake-Word on-device via openWakeWord (ONNX Runtime, kein API-Key) + State-Icon
|
- [x] Wake-Word on-device via openWakeWord (ONNX Runtime, kein API-Key) + State-Icon
|
||||||
|
|
||||||
|
|||||||
@@ -142,9 +142,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
return `msg_${Date.now()}_${messageIdCounter.current}`;
|
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(() => {
|
useEffect(() => {
|
||||||
const loadTtsSettings = async () => {
|
const loadSettings = async () => {
|
||||||
const enabled = await AsyncStorage.getItem('aria_tts_enabled');
|
const enabled = await AsyncStorage.getItem('aria_tts_enabled');
|
||||||
setTtsDeviceEnabled(enabled !== 'false'); // default true
|
setTtsDeviceEnabled(enabled !== 'false'); // default true
|
||||||
const muted = await AsyncStorage.getItem('aria_tts_muted');
|
const muted = await AsyncStorage.getItem('aria_tts_muted');
|
||||||
@@ -152,10 +153,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
const voice = await AsyncStorage.getItem('aria_xtts_voice');
|
const voice = await AsyncStorage.getItem('aria_xtts_voice');
|
||||||
localXttsVoiceRef.current = voice || '';
|
localXttsVoiceRef.current = voice || '';
|
||||||
ttsSpeedRef.current = await loadTtsSpeed();
|
ttsSpeedRef.current = await loadTtsSpeed();
|
||||||
|
const gps = await AsyncStorage.getItem('aria_gps_enabled');
|
||||||
|
setGpsEnabled(gps === 'true');
|
||||||
};
|
};
|
||||||
loadTtsSettings();
|
loadSettings();
|
||||||
// Poll alle 2s um Settings-Aenderung mitzubekommen (einfache Loesung ohne Context)
|
const interval = setInterval(loadSettings, 2000);
|
||||||
const interval = setInterval(loadTtsSettings, 2000);
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.getItem('aria_tts_enabled').then(saved => {
|
AsyncStorage.getItem('aria_tts_enabled').then(saved => {
|
||||||
if (saved !== null) setTtsEnabled(saved === 'true');
|
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 => {
|
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
const n = parseFloat(saved);
|
const n = parseFloat(saved);
|
||||||
@@ -437,7 +440,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
|
|
||||||
const handleGPSToggle = useCallback((value: boolean) => {
|
const handleGPSToggle = useCallback((value: boolean) => {
|
||||||
setGpsEnabled(value);
|
setGpsEnabled(value);
|
||||||
// In Produktion: Wert in AsyncStorage persistieren
|
AsyncStorage.setItem('aria_gps_enabled', String(value)).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// --- XTTS Voice ---
|
// --- XTTS Voice ---
|
||||||
@@ -661,7 +664,11 @@ const SettingsScreen: React.FC = () => {
|
|||||||
<View style={styles.toggleInfo}>
|
<View style={styles.toggleInfo}>
|
||||||
<Text style={styles.toggleLabel}>GPS-Position mitsenden</Text>
|
<Text style={styles.toggleLabel}>GPS-Position mitsenden</Text>
|
||||||
<Text style={styles.toggleHint}>
|
<Text style={styles.toggleHint}>
|
||||||
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.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
+45
-22
@@ -1028,6 +1028,31 @@ class ARIABridge:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("[session] Diagnostic nicht erreichbar (%s) — nutze '%s'", e, self._session_key)
|
logger.debug("[session] Diagnostic nicht erreichbar (%s) — nutze '%s'", e, self._session_key)
|
||||||
|
|
||||||
|
def _build_core_text(self, text: str, interrupted: bool = False,
|
||||||
|
location: Optional[dict] = None) -> 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:
|
def _build_pending_files_message(self, user_text: str) -> str:
|
||||||
"""Baut eine Anweisung an aria-core aus den gepufferten Files + optionalem
|
"""Baut eine Anweisung an aria-core aus den gepufferten Files + optionalem
|
||||||
User-Text. user_text leer → 'warte auf Anweisung'-Variante."""
|
User-Text. user_text leer → 'warte auf Anweisung'-Variante."""
|
||||||
@@ -1236,6 +1261,7 @@ class ARIABridge:
|
|||||||
self._next_speed_override = None
|
self._next_speed_override = None
|
||||||
if text:
|
if text:
|
||||||
interrupted = bool(payload.get("interrupted", False))
|
interrupted = bool(payload.get("interrupted", False))
|
||||||
|
location = payload.get("location") or None
|
||||||
# Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig
|
# Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig
|
||||||
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
||||||
# zwei separater send_to_core-Calls.
|
# zwei separater send_to_core-Calls.
|
||||||
@@ -1243,15 +1269,11 @@ class ARIABridge:
|
|||||||
if merged:
|
if merged:
|
||||||
logger.info("[rvs] App-Chat (mit Anhaengen): '%s'", text[:80])
|
logger.info("[rvs] App-Chat (mit Anhaengen): '%s'", text[:80])
|
||||||
else:
|
else:
|
||||||
core_text = (
|
core_text = self._build_core_text(text, interrupted, location)
|
||||||
f"[Hinweis: Stefan hat dich gerade unterbrochen waehrend du noch "
|
logger.info("[rvs] App-Chat%s%s: '%s'",
|
||||||
f"gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur, "
|
" [BARGE-IN]" if interrupted else "",
|
||||||
f"Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.] "
|
" [GPS]" if location else "",
|
||||||
f"{text}"
|
text[:80])
|
||||||
if interrupted else text
|
|
||||||
)
|
|
||||||
logger.info("[rvs] App-Chat%s: '%s'",
|
|
||||||
" [BARGE-IN]" if interrupted else "", text[:80])
|
|
||||||
await self.send_to_core(core_text, source="app" + (" [barge-in]" if interrupted else ""))
|
await self.send_to_core(core_text, source="app" + (" [barge-in]" if interrupted else ""))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1511,11 +1533,14 @@ class ARIABridge:
|
|||||||
self._next_speed_override = None
|
self._next_speed_override = None
|
||||||
interrupted = bool(payload.get("interrupted", False))
|
interrupted = bool(payload.get("interrupted", False))
|
||||||
audio_request_id = payload.get("audioRequestId", "") or ""
|
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,
|
mime_type, duration_ms, len(audio_b64) // 1365,
|
||||||
" [BARGE-IN]" if interrupted else "",
|
" [BARGE-IN]" if interrupted else "",
|
||||||
|
" [GPS]" if location else "",
|
||||||
f" reqId={audio_request_id[:16]}" if audio_request_id 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":
|
elif msg_type == "stt_response":
|
||||||
# Antwort der whisper-bridge auf unseren stt_request
|
# 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,
|
async def _process_app_audio(self, audio_b64: str, mime_type: str,
|
||||||
interrupted: bool = False,
|
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.
|
"""App-Audio → STT → aria-core. Primaer via whisper-bridge (RVS), Fallback lokal.
|
||||||
|
|
||||||
interrupted=True wenn der User waehrend ARIA noch sprach/dachte aufgenommen hat
|
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
|
audio_request_id: Korrelations-ID die die App im audio-Event mitschickt — wird
|
||||||
unveraendert ans STT-Result zurueckgegeben damit die App die EXAKT richtige
|
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
|
# Erst Remote versuchen
|
||||||
text = await self._stt_remote(audio_b64, mime_type)
|
text = await self._stt_remote(audio_b64, mime_type)
|
||||||
if text is None:
|
if text is None:
|
||||||
@@ -1595,15 +1624,9 @@ class ARIABridge:
|
|||||||
|
|
||||||
if text.strip():
|
if text.strip():
|
||||||
logger.info("[rvs] STT Ergebnis: '%s'", text[:80])
|
logger.info("[rvs] STT Ergebnis: '%s'", text[:80])
|
||||||
# Barge-In-Hinweis: gibt ARIA den Kontext dass sie unterbrochen wurde
|
# Hints (Barge-In, GPS) als Praefix vorschalten — gemeinsamer Helper
|
||||||
# und dies eine Korrektur/Aenderung der vorherigen Anweisung sein kann.
|
# mit dem chat-Pfad damit das Verhalten konsistent ist.
|
||||||
core_text = (
|
core_text = self._build_core_text(text, interrupted, location)
|
||||||
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
|
|
||||||
)
|
|
||||||
# ERST an aria-core senden (wichtigster Schritt)
|
# ERST an aria-core senden (wichtigster Schritt)
|
||||||
await self.send_to_core(core_text, source="app-voice" + (" [barge-in]" if interrupted else ""))
|
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)
|
# STT-Text an RVS senden (fuer Anzeige in App + Diagnostic)
|
||||||
|
|||||||
@@ -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] 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] **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] **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
|
## Offen
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user