Commit Graph

84 Commits

Author SHA1 Message Date
duffyduck 8359500476 feat(skills): P3 config_schema + P4 Versionierung mit Rollback
P3 — Skill-Configuration
- aria-brain/skills.py: SKILL_CONFIGS_FILE (/shared/config/skill_configs.json)
  als zentrale Werte-Persistenz. _normalize_config_schema validiert die
  Schema-Felder (name/type/label/secret/description/default), CFG_<UPPER_NAME>
  ENV beim run_skill. create_skill + update_skill akzeptieren config_schema.
- agent.py: skill_set_config Brain-Tool fuer ARIA. skill_create/update um
  config_schema-Property erweitert.
- main.py: GET/POST /skills/{name}/config — secret-Werte in Antwort gemaskt.

P4 — Versionierung mit Rollback
- aria-brain/skills.py: archive_current_version archiviert nach
  versions/v_<ts>/ (ohne venv/logs). update_skill ruft das automatisch auf
  bevor strukturelle Aenderungen passieren. list_skill_versions,
  rollback_skill (mit Safety-Snapshot + automatischem venv-Rebuild),
  delete_skill_version.
- agent.py: skill_list_versions, skill_rollback Brain-Tools.
- main.py: GET /skills/{name}/versions, POST /skills/{name}/rollback,
  DELETE /skills/{name}/versions/{version_id}.

UI
- diagnostic/index.html: Skill-Detail um Config-Form (typ-spezifisch,
  Secrets als password-Input mit ***SET***-Hinweis) und Versions-Liste
  mit Rollback-/Delete-Button.
- android SkillBrowser: SkillDetailModal laedt config_schema + versions
  on-mount. Config-Form (TextInput + Switch fuer boolean), Versionen mit
  Rollback-Confirm. brainApi um SkillConfigField/SkillVersion +
  getSkillConfig/setSkillConfig/listSkillVersions/rollbackSkill/
  deleteSkillVersion erweitert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:52:46 +02:00
duffyduck ad87c807de fix(app): App-Reconnect nach Hintergrund — Sticky-Fallback, Zombie-WS, AppState-Hook
Stefan musste seit der HTTPS-Umstellung nach jedem Hintergrund-Rueckkehr
manuell auf "Verbinden" tippen, meist 3x bis es ging. Gleiche Bug-Klasse
wie auf der Bridge davor (Sticky-Fallback), plus zwei App-spezifische
Symptome.

Drei Ursachen:

1. usingTLSFallback klebt: einmal nach onerror auf true gesetzt, blieb
   es bei allen folgenden Reconnects → App versuchte ws://...:443 gegen
   den TLS-only Caddy → HTTP 400 → endlos. Reset war NUR im manuellen
   connect(), nicht in onclose oder scheduleReconnect.
   Fix: in onclose `usingTLSFallback = false` damit der naechste
   Reconnect wieder primary (wss://) probiert.

2. Zombie-WebSocket: Android kann den TCP-Socket im Background still
   killen, der JS-State zeigt aber noch readyState === OPEN. Stefans
   manueller "Verbinden"-Klick rief connect() → "Bereits verbunden"
   No-Op statt sich neu aufzubauen.
   Fix: connect(force=true) optional, bestehendes WS-Objekt wird hart
   geschlossen (mit onclose=null gegen Doppel-Reconnect) bevor neuer
   Aufbau startet.

3. Keine aktive Reconnect-Sequence bei Foreground-Resume: App war
   abhaengig von onclose-Events die bei Zombie-WS nicht zwingend
   feuern.
   Fix: AppState-Listener in App.tsx, bei background → active
   automatischer rvs.connect(true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:09:30 +02:00
duffyduck 13e87fb083 feat(oauth): ARIA kann Provider selbst registrieren + Custom-Provider in Diagnostic & App
ARIA hat jetzt das META-Tool oauth_register_provider. Wenn Stefan einen
Service nutzen will, der nicht in den (auf Spotify reduzierten) Defaults
ist, kann sie auth_url/token_url/scopes/client_auth selbst eintragen —
ARIA kennt typische OAuth-Endpunkte (Dropbox, Discord, Notion, Slack,
Zoom, Trello, LinkedIn, Reddit, Twitch) aus ihrem Training. Sie traegt
NUR die URLs ein, client_id/secret bleiben Stefans Job (Diagnostic /
App-UI) — bewusste Trennung damit Credentials nicht im Chat-Verlauf
landen.

DEFAULT_PROVIDERS auf Spotify reduziert — Rest war aktuell ungenutzt
und macht den Code unnoetig "groß". ARIA registriert on-demand.

Diagnostic-UI:
- Custom-Provider zeigen auth_url/token_url/scopes als sichtbare Felder
- Defaults verstecken die Felder hinter "Default-URLs ueberschreiben
  (advanced)" damit man die Spotify-URLs nicht versehentlich loescht
- "+ Custom OAuth-Provider hinzufuegen" Button mit Prompts fuer
  Name/URLs/Scopes
- 🗑-Icon bei Custom-Services (Service komplett entfernen)

App-UI (neu fuer unterwegs):
- Settings → Sektion 🔑 "OAuth-Apps" zwischen Skills und Protokoll
- OAuthBrowser-Komponente analog zu Trigger/Skill-Browser:
  Liste mit Status, Tap → Edit-Modal mit client_id/secret +
  Advanced-Toggle fuer URLs. "Autorisieren ↗" oeffnet System-Browser
  via Linking.openURL, redirected zur RVS-Callback-Page,
  Status-Refresh nach 8s.
- "+ Custom"-Button → Full-Screen-Modal fuer Service-Anlage.
- brainApi um listOAuthServices/getOAuthApps/saveOAuthApp/
  deleteOAuthApp/authorizeOAuth/revokeOAuth erweitert.

Workflow ist jetzt: "verbinde mich mit Dropbox" → ARIA registriert
Provider → "trag client_id/secret in Settings ein" → Stefan macht das
in App oder Diagnostic → "Autorisieren ↗" → fertig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:16:31 +02:00
duffyduck 30c1dd7473 feat(app+brain): App-Bugfixes + Skill-Mgmt-Tools + Voice-Speed persistent + Skill-Browser
App-Bugs:
- Trigger-Liste war leer: brainApi.listTriggers() cast'te {triggers: [...]}
  direkt als Array, t.sort() warf — TriggerBrowser blieb leer. Fix: unwrap.
- GPS-Tracking startete erst bei SettingsScreen-Mount, nicht beim App-Boot.
  Wenn Stefan direkt in den Chat ging, blieb GPS aus. Fix: restoreFromStorage()
  in App.tsx useEffect.
- Text in Chat-Bubbles nicht markierbar / kein Copy-Mechanismus: Bubble jetzt
  Pressable mit onLongPress + neues ⎘-Icon in Status-Row → openBubbleActions().
  Alert-Menu mit "Ganzen Text teilen" + pro extrahierte URL/Mail/Tel eine
  eigene Option. Share.share() — keine neuen Native-Deps noetig.

Brain — Skill-Mgmt:
- ARIA legte beim Skill-Umbau neue Versionen mit Suffix an (Skill-Friedhof),
  weil sie kein Update/Delete-Tool kannte. Zwei neue META_TOOLS in agent.py:
  skill_update (kann entry_code, readme, pip_packages, args, description,
  active patchen — venv wird bei pip_packages-Aenderung rebuilt) + skill_delete.
- skills.py update_skill um entry_code/readme/pip_packages erweitert,
  venv-Rebuild bei pip-Aenderung.

Bridge — Voice-Speed persistent:
- _next_speed_override war pro-Request-Override ohne Persistenz. Bei
  Diagnostic-Chats / Trigger-Replies ohne vorherigen App-Chat fiel der Speed
  auf 1.0 zurueck, ebenso nach Bridge-Restart. Jetzt: _persistent_xtts_speed
  aus voice_config.json (xttsSpeed), wird nach jedem App-chat mit speed
  autopersistiert. TTS-Generation faellt zurueck: per-Request > persistent > 1.0.

App — Feature 6:
- SkillBrowser.tsx: Liste aller Skills, Toggle aktiv/inaktiv, Detail-Modal
  mit Args-Inputs, Ausfuehren mit Live-stdout/stderr, Logs der letzten 20
  Runs, Loeschen. Settings-Sektion "Skills" (🛠️) zwischen Trigger und
  Protokoll. brainApi.listSkills/getSkill/runSkill/updateSkill/deleteSkill/
  getSkillLogs ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:24:03 +02:00
duffyduck 0044e222db fix(phone): Anruf-Erkennung im Hintergrund + bei gesperrtem Display
Symptom: App bekommt im minimierten oder display-gesperrten Zustand
nicht mit ob ein Anruf angefangen oder beendet wurde — TTS spricht
weiter waehrend Telefon klingelt, oder bleibt stumm nach Auflegen.

Zwei Ursachen:

1) Kotlin: TelephonyCallback war auf reactApplicationContext.mainExecutor
   registriert. Wenn die Activity pausiert ist (display aus, App im
   Hintergrund), wird der mainExecutor verzoegert oder gar nicht
   abgearbeitet — Call-State-Events kommen nicht durch.
   Fix: eigener Executors.newSingleThreadExecutor() — laeuft unabhaengig
   vom UI-Thread solange der App-Prozess lebt (Foreground-Service
   garantiert das).

2) TS: TelephonyManager-Listener kann nach laengerer Hintergrund-Zeit
   verloren gehen (React-Bridge-Context recreated nach Resume).
   Fix: neue refresh()-Methode in phoneCallService, AppState-Resume
   ruft sie auf — wenn telephonyAttached=false ist, wird der Native-
   Listener neu attached.

Plus: Status-Property telephonyAttached macht in Logs sichtbar ob
Pfad 1 (TelephonyManager) wirklich greift. Pfad 2 (AudioFocus fuer
VoIP) war nie betroffen, der laeuft komplett im Native-Code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:59:55 +02:00
duffyduck 048d231b60 fix(wake): false-positive nach langer Hintergrund-Pause verwerfen
Symptom: Ohr aktiv, App im Hintergrund (jetzt mit Foreground-Service
permanent lebendig), nach laengerer Zeit oeffnet Stefan die App und sie
nimmt schon auf — angeblich Wake-Word getriggert. War aber TV/Husten/
sonstige Hintergrund-Geraeusche waehrend Stefan nicht da war.

Mit dem neuen Hintergrund-Modus laeuft openWakeWord jetzt permanent und
faengt jedes False-Positive im Hintergrund auf. Ohne dieser Fall war
das nicht moeglich weil die JS-Engine pausiert war.

Fix: Heuristik beim AppState-Resume in ChatScreen.tsx
- backgroundDauer wird gemerkt (lastBackgroundAt vs Resume-Zeit)
- Wenn >30s im Hintergrund UND state='conversing' UND letzter Wake-
  Trigger juenger als 15s: false-positive — Aufnahme abbrechen + zurueck
  zu armed
- Resume-Cooldown 1500 → 3000 ms (Audio-Spikes beim AppState-Switch
  haben gelegentlich nach 1.5s noch nicht verklungen)

Neue Methoden:
- wakeword.ts: lastTriggerAt-Tracking + discardIfFreshlyTriggered(maxAge)
- audio.ts: cancelRecording() — bricht recorder ab ohne Result zu
  emittieren, loescht die Audio-Datei

Setzt voraus dass Stefan nicht laenger als 30s im Hintergrund mit ARIA
spricht ueber Wake-Word. Falls doch: bei Resume waere die Aufnahme weg
und er muesste nochmal triggern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:54:07 +02:00
duffyduck cb0e879118 feat(app): Hintergrund-Modus — App laeuft weiter wenn minimiert
Bisher pausierte Android nach ~30s im Hintergrund die JS-Engine.
WebSocket schlief ein, Trigger-Replies vom Brain kamen nicht durch,
Timer-Erinnerungen feuerten in der App nicht obwohl im Brain
ausgeloest. Nach laengerer Hintergrund-Pause warf Android den
Prozess ganz raus → beim Wiedereroeffnen Cold-Start, sah aus wie Crash.

Loesung: Foreground-Service mit persistenter Notification — die ist
ohnehin schon da fuer TTS/Mic-Aktivitaet (`AriaPlaybackService`).
Wir erweitern das Slot-System um einen `background`-Slot der dauerhaft
aktiv ist (Settings-Toggle, default an). Notification zeigt "ARIA aktiv
— Hintergrund-Modus" wenn nichts spezifisches laeuft, escaliert zu
"ARIA spricht/hoert" bei TTS/Mic. Tap → App.

Drei Dateien:
- services/backgroundAudio.ts: 'background' als 4. Slot (niedrigste
  Prio, Fallback-Notification). Bestehende tts/rec/wake unveraendert.
- App.tsx: beim Start `acquireBackgroundAudio('background')` aufrufen
  wenn Settings nicht explizit deaktiviert. Plus POST_NOTIFICATIONS-
  Permission-Request (Android 13+).
- screens/SettingsScreen.tsx: neuer Toggle in Allgemein-Section.
  Plus Hinweis auf Android-Akku-Optimierung-Whitelist falls trotzdem
  was klemmt (manche Hersteller-ROMs killen aggressiv).

AndroidManifest unveraendert — foregroundServiceType="mediaPlayback|
microphone" deckt unseren Use-Case ab (ARIA spielt regelmaessig TTS
ab, was den Type rechtfertigt). Service stoppt sich selbst wenn alle
Slots leer sind, das passiert nur wenn der User in Settings den
Hintergrund-Modus deaktiviert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:27:01 +02:00
duffyduck 7093ebaf0b feat(app): Trigger-CRUD-Section in Settings + nested-Scroll-Fix
Settings hatte zwei Probleme:

1) Gedächtnis-Liste scrollte nur runter, nicht hoch. Klassisches Android
   nested-Scroll-Problem: aeussere ScrollView + innere FlatList mit
   fixer height:600 = nur eine Richtung wird respektiert.

   Fix: outer ScrollView mit scrollEnabled=false wenn die Section eine
   eigene voll-hoch-scrollende Sub-Liste hat (memory/triggers). Plus
   dynamische Hoehe via useWindowDimensions (winHeight - 220 statt
   hardcoded 600) damit MemoryBrowser sauber den verfuegbaren Platz
   nutzt.

2) Trigger waren bisher nur via Diagnostic-Tab editierbar — keine App-
   side CRUD. Stefan wollte das.

   Neu: TriggerBrowser-Komponente (analog MemoryBrowser-Struktur)
   - Liste aller Trigger mit Filter (alle/aktive/inaktive)
   - Toggle aktiv/inaktiv via Switch direkt in der Zeile
   - Tap oeffnet TriggerEditModal (Nachricht/Condition/fires_at/intervals
     editieren, Loeschen-Knopf mit Confirm)
   - "+ Neu"-Knopf oeffnet TriggerNewModal mit Type-Switch (Watcher/Timer),
     Watcher zeigt Hinweis auf verfuegbare Funktionen + Variablen
   - Live Reload-Button, Meta-Info (fire_count, last_fired_at, ...)

   brainApi um Trigger-Endpoints erweitert: listTriggers, getTrigger,
   createTimer, createWatcher, updateTrigger (patch), deleteTrigger,
   getTriggerConditions, getTriggerLogs. Plus Trigger-Type-Definition.

Settings-Liste hat eine neue Section " Trigger" zwischen Gedaechtnis
und Protokoll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:44:24 +02:00
duffyduck b510ccd93a fix(app): Such-Reihenfolge + About-Escape + GPS-Heartbeat fuer near()
(1) Such-Treffer jetzt neueste zuerst (analog WhatsApp/Telegram). User
    ist visuell unten, der erste Sprung landet meist im Viewport ohne
    weiten Pre-Scroll (= weniger Cold-Start-Fail-Risiko). „Naechster"
    geht in die Vergangenheit. Plus Pre-Scroll-Wartezeit 80→200 ms damit
    FlatList beim ersten Versuch wirklich Zeit zum Rendern hat.

(2) SettingsScreen Ueber-Text: `—` wurde literal gerendert weil
    JSX-Text-Knoten keine JS-String-Escapes interpretieren. Fix:
    `{'—'}` als JS-Expression-Block.

(3) GPS-Tracking sendete nach der initialen Position nichts mehr wenn
    der User stationaer war — `distanceFilter: 30` blockiert
    watchPosition-Updates ohne Bewegung. Nach 5 min (NEAR_MAX_AGE_SEC)
    verwirft das Brain die Position als veraltet → near()-Watcher feuern
    nie. Stefan's DRK-Trigger waren so chronisch tot.

    Fix: zusaetzlich zum watchPosition laeuft ein setInterval(60s)
    Heartbeat der die zuletzt empfangene Position erneut sendet. Kein
    extra GPS-Wakeup — akkufreundlich. Damit bleibt der Brain-State
    frisch auch bei stationaerem User; near() funktioniert sobald der
    User tatsaechlich im Radius ist.

Anmerkung zu Stefan's konkretem Test: er war 1.5–2 km von den DRK-
Triggern entfernt (Radius je 300 m) — selbst mit frischen GPS-Updates
haetten die nicht gefeuert. Der Heartbeat-Fix ist trotzdem noetig
damit Trigger ueberhaupt eine Chance haben wenn er tatsaechlich dort
vorbeifaehrt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:30:51 +02:00
duffyduck e74e1eaf70 fix(app): URLSearchParams crasht in Hermes — durch Mini-Query-Builder ersetzt
Inbox-Crash gefunden via App-Crash-Reporter (commit 21a315c):

  "URLSearchParams.set is not implemented"
  at MemoryBrowser → brainApi.listMemories

React Native's Hermes-Polyfill kennt zwar new URLSearchParams() aber
nicht die .set()-Methode darauf. Pickup-Bug — auf iOS / aelteren
Versionen geht's, Stefan's Android-Build crasht.

Fix: kleine _qs()-Helper im brainApi.ts der einen Query-String aus
einem flachen Object baut, ohne URLSearchParams:
  _qs({q:'cessna', k:5, type:'fact'}) → "?q=cessna&k=5&type=fact"

Plus: undefined/null/empty Werte werden ausgelassen — saubererer als
URLSearchParams.set wo man manuell prefilten muss.

ErrorBoundary aus 21a315c hat den Crash sauber abgefangen, statt der
App-Tot war ne Error-Box im Inbox-Modal mit der vollen Stack-Trace.
Stefan konnte den Log via tools/fetch-app-logs.sh holen ohne ADB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:06:21 +02:00
duffyduck 21a315ca71 feat(debug): App-Crash-Reporting via RVS — Logs in der Diagnostic-UI
Stefan ist unterwegs, ADB-Zugriff nicht moeglich. Loesung: die App
loggt ihre eigenen Crashes via RVS, Bridge sammelt sie in
/shared/logs/app.log, Diagnostic-Server liefert sie als JSON.
Damit braucht's keinen ADB mehr — Crashes sind sofort vom Browser
(oder Claude per curl) lesbar.

Komponenten:

1. App components/ErrorBoundary.tsx
   - React-ErrorBoundary fuer kritische Sections
   - componentDidCatch → reportAppError (RVS-Send)
   - UI zeigt Error-Box statt White-Screen + Reset-Button

2. App services/logger.ts
   - reportAppError(scope, message, stack) → rvs.send('app_log', ...)
   - installGlobalCrashReporter() haengt sich an ErrorUtils.setGlobalHandler
     UND HermesInternal.enablePromiseRejectionTracker — fangt sowohl
     ungefangene Errors als auch unhandled Promise-Rejections
   - Konsole bleibt parallel aktiv (damit ADB im Dev-Build weiter
     was sieht)

3. App App.tsx: installGlobalCrashReporter() im useEffect zusammen
   mit initLogger.

4. App ChatScreen.tsx:
   - Inbox-Modal mit ErrorBoundary umschlossen (scope: InboxModal,
     onReset schliesst Modal)
   - MemoryDetailModal mit ErrorBoundary umschlossen
   - DetailModal wird nur noch konditional gerendert (memoryDetailId
     != null) statt immer visible-toggle — vermeidet potentielles
     Modal-Stacking-Problem

5. RVS server.js: ALLOWED_TYPES += "app_log"

6. Bridge aria_bridge.py:
   - elif msg_type == "app_log": haengt eine Zeile an
     /shared/logs/app.log (JSONL, jedes Item {ts, platform, level,
     scope, message, stack})
   - Plus log.info Hinweis fuer das normale Bridge-Log

7. Diagnostic server.js:
   - GET /api/app-log[?limit=N] → letzte N Eintraege als JSON
   - POST /api/app-log/clear → log-Datei loeschen

Workflow zum Debuggen des Inbox-Crashes:
  Stefan rebuilded App → drueckt Inbox → ErrorBoundary fangt den
  Crash (oder Global-Handler bei ungefangenem Error) → reportAppError
  → RVS → Bridge schreibt nach /shared/logs/app.log → Stefan
  oder Claude rufen GET /api/app-log auf → sehen Stacktrace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:42:55 +02:00
duffyduck f71936da86 feat(memory): Tap auf Memory-Bubble oeffnet Detail+Edit-Modal in der App (Etappen 2+3)
Stefans naechste Wunsch-Etappe — komplettes Edit eines Memory-Eintrags
aus der App heraus, inkl. Anhang-Upload, ohne Diagnostic-Browser
auszuklappen.

Backend-Fundament (Phase A):
- Brain bekommt GET /memory/get/{id} fuer Einzel-Lookup mit allen Feldern
- RVS ALLOWED_TYPES um brain_request + brain_response erweitert
- Bridge implementiert generischen RVS-Brain-Proxy:
  payload {requestId, method, path, body|bodyBase64, contentType}
  → ruft Brain-HTTP-API → broadcastet brain_response {requestId,
  status, json|text|base64+contentType}. Damit kann die App
  beliebige Brain-Endpoints ueber RVS adressieren — nicht nur Memory.

App-Service (Phase B):
- services/brainApi.ts: Promise-basierter Client. _send() schickt
  brain_request mit requestId, _ensureListener() filtert die passende
  brain_response. Methoden: getMemory, listMemories, searchText,
  searchSemantic, saveMemory, updateMemory, deleteMemory,
  uploadAttachment (Base64), deleteAttachment, getAttachmentBytes.

App-UI (Phasen C+D):
- components/MemoryDetailModal.tsx: Modal mit zwei Modi.
  - Read: Titel, Type, Category, Tags, voller Content, Anhang-Liste
    (Tap = Bild im Vollbild oder Datei-Info), Stift-Icon → Edit.
  - Edit: Titel/Content/Category/Tags/Pinned editierbar, Save via
    brainApi.updateMemory.
  - DocumentPicker + RNFS.readFile(base64) → uploadAttachment(...).
  - Anhang loeschen, kompletter Memory loeschen (mit Alert-confirm).
- ChatScreen: TouchableOpacity-Wrapper um die memorySaved-Bubble,
  Tap setzt memoryDetailId → Modal oeffnet. Hint im Footer
  "tippen für Details" wenn die Bubble eine ID hat.

Etappen 4 (Notizen-Inbox neben Lupe) + 5 (Memory-Editor in App-
Settings) folgen — diese nutzen die gleiche MemoryDetailModal-
Komponente, sind also schnell aufgesetzt sobald 2+3 verifiziert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:26:02 +02:00
duffyduck fa47068d6d feat(gps): kontinuierliches GPS-Tracking — Blitzer-Warner-Pipeline komplett
ARIA kann jetzt GPS-Watcher mit near() effektiv nutzen: die App liefert
kontinuierliche Position, Brain wertet sie in den Background-Triggers aus.

rvs/server.js
  ALLOWED_TYPES: location_update (App→Bridge) + location_tracking (Brain→App).

bridge/aria_bridge.py
  location_update Handler: persistiert {lat, lon} via _persist_location in
  /shared/state/location.json — selber Pfad wie chat/audio-events, aber als
  eigenes Event ohne Chat-Overhead.

aria-brain/agent.py
  Neues Meta-Tool request_location_tracking(on, reason). Dispatcher fuegt
  {type: "location_tracking", on, reason} zu _pending_events hinzu →
  Bridge forwarded als RVS-Message zur App.

aria-brain/prompts.py
  Trigger-Section bekam neuen Block "GPS-Watcher mit near()": ARIA wird
  angewiesen request_location_tracking(on=true) zu rufen wenn sie einen
  near()-Watcher anlegt, und wieder false beim Loeschen des letzten.

android/src/services/gpsTracking.ts (NEU)
  Singleton-Service. start(reason) → Geolocation.watchPosition mit
  distanceFilter 30m + interval 15s, sendet location_update an RVS.
  stop(reason) → clearWatch. Persistiert Status in 'aria_gps_tracking',
  restoreFromStorage() beim Settings-Mount. Permission-Request fuer
  ACCESS_FINE_LOCATION + Toast-Benachrichtigung bei An/Aus.

android/src/screens/SettingsScreen.tsx
  Neuer Switch im "Standort"-Block: "GPS-Tracking (kontinuierlich)" mit
  Hinweis-Text. Subscribe auf gpsTrackingService.onChange damit Toggle
  reflektiert wenn ARIA das per Tool umschaltet.
  RVS-Handler: location_tracking → gpsTrackingService.start/stop mit
  Reason aus Brain-Tool.

Ablauf Stefan→ARIA→Blitzer:
  1. Stefan: "Warn mich vor Blitzern auf Route nach Rhauderfehn"
  2. ARIA: skill_create("blitzer-warner") falls noch nicht da
  3. ARIA: run_blitzer-warner → Liste {lat,lon,name}
  4. ARIA: pro Eintrag trigger_watcher mit near(lat,lon,500)
  5. ARIA: request_location_tracking(on=true, reason="Blitzer-Warner aktiv")
  6. App: GPS-Tracking startet, sendet alle 15s location_update
  7. Bridge: /shared/state/location.json wird aktuell gehalten
  8. Brain-Background-Loop: alle 30s near()-Check pro Trigger
  9. Bei Erfolg: ARIA spricht "Blitzer A31 km 12 in 500m"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:02:05 +02:00
duffyduck 62018b3e51 revert(audio): kickReleaseMedia raus — bricht Spotify's Auto-Resume
Logs zeigen jetzt KEINEN haengenden RNSound-Focus mehr (Library-Version
oder Sound-Lifecycle hat sich geaendert). Der Kick mit AUDIOFOCUS_GAIN
(permanent) sagte Spotify "user hat manuell etwas anderes gestartet" →
Spotify resumed nicht automatisch.

Ohne Kick: unser Focus war AUDIOFOCUS_GAIN_TRANSIENT (USAGE_ASSISTANT) —
beim release bekommt Spotify einen sauberen GAIN nach TRANSIENT-Loss
und resumed automatisch.

Native kickReleaseMedia bleibt fuer den Fall dass es nochmal gebraucht
wird, wird aber nicht mehr gerufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:24:55 +02:00
duffyduck f023ba0ac5 fix(audio): Mute-Button = Stop fuer aktuelle Antwort, nie Resume
Bisheriges Verhalten: Mute drueckt → stopPlayback. Mute zurueck → noch
eingehende chunks der gleichen Antwort starteten einen neuen Stream und
ARIA redete weiter wo sie war. Funktionierte nur 2x weil dann isFinal
schon kam und keine chunks mehr fluten.

Stefan: "Mund verbieten = Stop, fertig". Neue Antworten sollen normal
spielen.

Fix: _stoppedMessageId-Tracking. Bei Mute=true wird die aktuelle msgId
gemerkt — alle weiteren chunks dieser msgId bleiben silent, auch wenn
Mute zurueckgenommen wird. Reset bei neuer msgId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:20:29 +02:00
duffyduck facde1fef7 fix(audio): kickReleaseMedia auch im PCM-Pfad — re-renderte Antworten muteten Spotify dauerhaft
Stefan: ältere Nachrichten (deren Cache-WAV weg ist) gehen ueber
tts_request neu rendern → kommen als PCM-Stream zurueck → werden ueber
PcmStreamPlayer abgespielt. Beim Mute lief stopPlayback aber ohne den
Spotify-resume-Kick weil hadRnSound=false war (kein currentSound).

Jetzt: kickReleaseMedia immer in stopPlayback rufen — kostet nichts,
deckt PCM- und RNSound-Pfad ab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:13:12 +02:00
duffyduck 3bc490b485 fix(audio): stopPlayback idempotent — kein doppelter Focus-Kick
Re-Renders / setInterval(loadSettings) triggern setMuted(true) oft
mehrfach hintereinander → jeder weitere stopPlayback rief erneut
kickReleaseMedia, Spotify pausierte+resumte mehrfach (Stefan: "spielt
kurz und pausiert dann wieder").

Fix: stopPlayback returnt sofort wenn nichts mehr aktiv ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:02:01 +02:00
duffyduck b1eaf42fef fix(audio): Spotify resumed nach Mute — RNSound's haengenden Focus loesen
Logs zeigten: react-native-sound requestet beim Sound.play() einen
EIGENEN AudioFocus mit USAGE_MEDIA, released den aber bei Sound.stop()/
release() NICHT (bekanntes RN-sound-Bug). Spotify sieht den haengenden
Media-Focus → bleibt pausiert.

Workaround: Native-Methode kickReleaseMedia() macht einen request+abandon-
Cycle mit USAGE_MEDIA, das System raeumt damit den Focus-Stack auf und
Spotify bekommt sauberen GAIN-Event. stopPlayback ruft das jetzt nach
Sound.release() wenn vorher ein RNSound aktiv war.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:57:52 +02:00
duffyduck fb9e5dcd10 feat(logger): Verbose-Logging-Toggle in Settings → Protokoll
console.log wird global stummgeschaltet wenn aus — spart adb-logcat-
Speicher wenn alles laeuft. console.warn/error bleiben immer aktiv.
Default an.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:52:25 +02:00
duffyduck 1088bff43d fix(chat): Play-Button rendert neu wenn Cache-Datei weg
Vorher: Button checkte nur ob audioPath gesetzt ist — auf eine geloeschte
Cache-Datei hat aber nichts geprueft. playFromPath warntete nur und
returnte stumm. Jetzt wird VOR playFromPath die Existenz geprueft, sonst
geht's ueber tts_request an die Bridge zum Neu-Rendern.

Plus: Logs in Sound.play-Callback und _releaseFocusDeferred fuer den
"Spotify resumed nicht nach Replay"-Bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:42:38 +02:00
duffyduck 50b10c8ac0 feat(audio): Cache-Cleanup beim App-Start + TTS-Cache-Settings-Button
- App-Start raeumt orphane aria_tts_*.wav (>5min) aus dem Cache —
  Wiedergaben die durch Anruf/Mute/Barge-In abgebrochen wurden
  hinterliessen sonst Files, weil der completion-Callback nicht feuert.
- Neuer Settings-Button "TTS-Cache leeren" mit Live-Groessenanzeige —
  parallel zum bestehenden "Update-Cache leeren".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:36:01 +02:00
duffyduck 632e1e4fa1 fix(audio): pauseForCall setzt isPlaying zurueck — Playback nach Anruf nicht mehr tot
pauseForCall stoppte zwar currentSound + setzte ihn auf null, hat aber
isPlaying=true gelassen. Folge: nach dem Anruf war jeder weitere Play-
Button-Klick wirkungslos, weil playAudio bei isPlaying=true den
_playNext-Pfad ueberspringt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:28:42 +02:00
duffyduck 8f64f8fb30 fix(phone): 800ms-Delay vor Auto-Resume — Spotify kommt zum Atmen
Wenn ARIA's Resume-Pfad direkt nach Anruf-Ende den AudioFocus requestet,
kollidiert das mit Spotify's eigenem Auto-Resume. System haengt noch im
IN_CALL-Mode-Uebergang, Spotify sieht "Loss → Loss" und bleibt pausiert
statt kurz zu resumen.

Mit 800ms-Delay: Spotify schafft den Resume-Schritt, dann pausiert ARIA
wieder ordnungsgemaess. Wenn ARIA nichts pending hatte, bleibt Spotify
einfach weiter an.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:20:48 +02:00
duffyduck 31aa82b68c debug+fix(audio): Mute-Logs + resumeSound auch in stopPlayback stoppen
stopPlayback stoppte bisher nur currentSound, nicht resumeSound — wenn
nach einem Anruf der Auto-Resume laeuft und der User Mute drueckt, bleibt
der Resume-Sound weiter spielen.

Plus Logs in setMuted/stopPlayback um zu sehen warum Stefans Mute beim
Replay nicht greift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:55:51 +02:00
duffyduck f5970ce700 fix(audio): _firePlaybackStarted ueberschrieb playFromPath-Tracking mit leerem pcmMessageId
Logs zeigten: playFromPath setzt currentPlaybackMsgId='db710ff3-...', 9s
spaeter beim Anruf war captureInterruption msgId=(leer). Ursache:
_firePlaybackStarted setzt currentPlaybackMsgId blind aus pcmMessageId —
das ist beim Play-Button leer.

Jetzt nur noch setzen wenn ein PCM-Stream laeuft. Play-Button und Resume-
Sound setzen ihr Tracking selber im Caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:44:28 +02:00
duffyduck 3dcd2ae0b4 fix(audio): msgId-Regex liberaler — auch nicht-UUID-Dateinamen werden erkannt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:36:43 +02:00
duffyduck 2dfd21d1d0 fix(audio): Play-Button setzt jetzt auch Wiedergabe-Tracking — Anruf-Test via Playback funktioniert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:22:21 +02:00
duffyduck 9d9ddc730b debug+fix(audio): mehr Anruf-Logs + Tracking auch beim Resume-Sound
Im Test 2 (zweiter Anruf in derselben Antwort) kam weder captureInterruption
noch resumeFromInterruption als Log — beide returnen frueh ohne Hinweis warum.
Jetzt loggen sie auch den Skip-Pfad damit man sieht ob's der idempotent-Guard
oder fehlende playbackStartTime ist.

Plus: _playFromPathAtPosition aktualisiert jetzt currentPlaybackMsgId und
playbackStartTime — sonst stehen die auf den Werten der ersten TTS-Wiedergabe
und ein zweiter Anruf-captureInterruption wuerde mit veraltetem Stand laufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:20:43 +02:00
duffyduck 175dcdf225 fix(audio): Auto-Resume nach Anruf — pcmBuffer bleibt erhalten
Logs zeigten "WAV nicht binnen 30000ms verfuegbar" — pcmBuffer wurde von
haltAllPlayback geleert, isFinal schrieb daher eine leere WAV (oder gar
keine, weil pcmMessageId leer war).

Neue Methode pauseForCall (statt haltAllPlayback im Anruf-Pfad):
- AudioTrack stoppt + AudioFocus release (Spotify resumed)
- pcmBuffer + pcmMessageId BLEIBEN — Bridge-Chunks werden weiter gesammelt
- _pausedForCall macht weitere Chunks "silent" (kein writeChunk, nur Cache)
- isFinal schreibt WAV trotz Anruf → resumeFromInterruption findet sie

Plus captureInterruption idempotent gemacht: ringing→offhook ueberschreibt
die Position vom ersten Halt nicht mehr (Date.now-Tracking laeuft stumpf
weiter obwohl Audio gestoppt ist — der erste Halt ist die echte Position).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:11:46 +02:00
duffyduck f066a2a555 fix(audio): Mute-Button stoppt jetzt auch laufenden PCM-Stream
pcmStreamActive wurde beim isFinal-Chunk schon auf false gesetzt, der
AudioTrack spielte aber noch aus seinem Buffer (kann sekundenlang sein).
stopPlayback() uebersprang darum PcmStreamPlayer.stop() — ARIA redete
weiter obwohl Spotify schon resumed war.

Fix: stop() immer rufen, der Flag-Check faellt weg (ist eh idempotent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:39:27 +02:00
duffyduck 73b7a76ea8 fix(phone-call): kein VoIP-Toast bei Play-Button — AudioMode pruefen
Stefan: 'Möchte ich mir playbacks anhören egal welches kommt die toast
nachricht voip anruf und danach aria wieder aktiv'.

Ursache: AUDIOFOCUS_LOSS feuert bei jedem Audio-Player-Wechsel
(Spotify, andere Apps, sogar unsere eigenen Sound-Calls). Wir
interpretierten das blind als VoIP-Anruf.

Fix: vor dem Halt fragen wir AudioFocus.getMode() ab — nur wenn
mode == 2 (IN_CALL) oder 3 (IN_COMMUNICATION) ist's wirklich ein
Anruf. Bei NORMAL (0) wird der Loss ignoriert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:43:40 +02:00
duffyduck 4feaacc7e4 feat(update): APK-Cache robuster + manueller 'Update-Cache leeren' Button
Stefan: 'app blaeht sich auf durch heruntergeladene Update-Versionen'.

updater.ts:
- cleanupOldApks durchsucht jetzt 4 Pfade (Caches, Documents, ExternalCaches,
  ExternalDir) statt nur CachesDirectoryPath
- Public gemacht + returnt {removed, freedMB}
- getApkCacheSize() neu — listet count + totalMB

SettingsScreen → Speicher:
- Neue Sektion 'Update-Cache' mit Live-Groessenanzeige
- Button 'Update-Cache leeren' triggert cleanup + Toast mit Ergebnis
- Beim Mount wird die Groesse einmal geladen

Auto-Cleanup laeuft weiterhin beim App-Start + vor jedem Download —
der Button ist fuer den Notfall (haengender Download, alte Pfade,
defekte APKs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:49:22 +02:00
duffyduck 97442198ec fix(audio): neue ARIA-Antwort verwirft pending Auto-Resume
Stefans Edge-Case: waehrend des Telefonats stellt der User eine neue
Text-Frage. Die neue ARIA-Antwort startet sofort (offhook→offhook
loest keinen halt aus). Vorher haette resumeFromInterruption nach
Anruf-Ende noch die ALTE Antwort (die unterbrochen wurde) ab
Position spielen wollen — Konflikt mit der neuen Antwort.

Fix: in _handlePcmChunkImpl beim Wechsel zu einer neuen messageId:
- laufenden resumeSound stoppen
- pausedMessageId = '' wenn != neue messageId

Damit gewinnt immer die neueste Antwort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:39:33 +02:00
duffyduck e3e841f2ab feat(audio): Auto-Resume nach Anruf ab der gemerkten Position
Stefans Idee: Position beim Halt merken (Date.now() - playbackStart -
leadingSilence), nach dem Auflegen ab da weitermachen. Wenn der Cache
noch nicht komplett ist (final-Marker kam waehrend Anruf), warten wir
bis zu 30s auf das WAV — meistens ist's schon da weil das Telefonat
laenger als die Antwort dauerte.

audio.ts:
- captureInterruption(): merkt position + messageId, returnt Sekunden
- resumeFromInterruption(maxWaitMs): wartet auf WAV-Cache, lädt mit
  Sound, setCurrentTime(position), play
- Tracking-Felder: playbackStartTime, currentPlaybackMsgId, pausedX

phoneCall.ts:
- _haltForCall ruft captureInterruption() VOR haltAllPlayback
- _resumeAfterCall triggert resumeFromInterruption(30s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:37:35 +02:00
duffyduck 33185de42b fix(audio): AudioFocus erst beim NATIVEN Playback-Finished-Event released
Logcat-Befund:
12:22:54.860 — final-Chunk + Cache geschrieben
12:22:55.402 — abandonAudioFocus (~0.5s spaeter)
12:22:55     — Spotify resumed (Atlas: TotalTime 93s)
12:23:27.064 — Playback fertig (32s spaeter!)

→ ARIA spricht 32s parallel zu Spotify weil end() viel zu frueh
returnt. Stefans 'Spotify resumed obwohl ARIA noch redet'.

Fix:
- PcmStreamPlayerModule emittiert 'PcmPlaybackFinished' RN-Event nach
  dem finally{}-Block im Writer-Thread (= AudioTrack hat alle Samples
  wirklich durchgespielt)
- audioService subscribed im constructor → ruft erst dann
  _releaseFocusDeferred()
- _handlePcmChunkImpl bei isFinal triggert NICHT mehr direkt das
  Release — nur die playbackFinished-Listener (UI-Logic)

So bleibt Spotify pausiert bis ARIA tatsaechlich fertig ist, egal
wie viel Audio im AudioTrack-Buffer wartet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:29:55 +02:00
duffyduck 4d0b9e0d78 fix: dB-Range -85, Mute haert auch laufende TTS, VoIP-Anrufe + Bild-Bubble
Bug 1 — dB-Range erweitert:
VAD_SILENCE_DB_MIN von -55 auf -85 dB. Damit hat Stefan einen weiten
Regler-Spielraum wenn die adaptive Auto-Erkennung in seiner Umgebung
nicht zuverlaessig greift.

Bug 5 — Mute-Button stoppt laufende TTS nicht:
audioService bekommt jetzt einen internen _muted-Flag. handlePcmChunk
setzt silent automatisch wenn _muted true ist, playAudio kehrt frueh
zurueck. Verhindert Race zwischen User-Klick auf Mute und einem
TTS-Chunk der im selben JS-Tick ankommt (vorher: Ref-Update via
useEffect erst nach dem Re-Render → Chunks "rutschten durch"). Plus
ttsCanPlayRef wird im toggleMute-Handler synchron aktualisiert.

Bug 4 — VoIP/Messenger-Anrufe erkennen:
AudioFocusModule emittiert jetzt "AudioFocusChanged" Events mit type
"loss"/"loss_transient"/"gain". WhatsApp/Signal/Discord/etc. requestn
AudioFocus_GAIN_TRANSIENT_EXCLUSIVE wenn ein Anruf reinkommt — wir
fangen das in phoneCall.ts ab und rufen halt + pauseForCall genau
wie beim klassischen Anruf. Plus getMode() Polling-Fallback (alle 3s)
weil GAIN nicht zuverlaessig kommt wenn wir den Focus selbst released
haben — sobald AudioMode wieder NORMAL ist, resumeFromCall.

Bug 6 — Bilder als "Strich":
attachmentImage hatte width: '100%' in einer Bubble mit maxWidth: '80%'
ohne explizite Parent-Breite → RN rendert auf 0px Breite. Neue ChatImage-
Komponente nutzt Image.getSize um die echte aspectRatio zu messen + setzt
sie dynamisch. Bubble passt sich dem Bild an.

Bugs 2 (lange Texte mid-cutoff) + 3 (Spotify resumed) — brauchen ADB-Logs.
ADB-WLAN ueber 192.168.177.22:5555 schlaegt fehl (refused) — bei Android
11+ braucht's Wireless-Debugging-Pairing-Code. Stefan kann den nennen
sobald er soweit ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:28:52 +02:00
duffyduck 5bdcc3c65b feat(vad): Stille-Pegel manuell in Settings + Info-Modal
Wenn die adaptive Baseline-Logik in einer Umgebung nicht zuverlaessig
greift (Stefan: "manchmal funktioniert die Stille-Erkennung nicht"),
kann der User die Schwelle jetzt manuell setzen.

Settings → Spracheingabe:
- "Stille-Pegel (dB)" mit −1/+1 Buttons + "Auf automatisch zuruecksetzen"
- Range −55 bis −15 dB, default "auto" (= adaptive Baseline)
- Info-Icon (i) oeffnet Modal mit Erklaerung:
  • dB-Skala (negativ, naeher 0 = lauter)
  • Faustregel-Pegel mit Farb-Code (−45 sensibel, −38 ausgewogen, −25 robust)
  • Klarstellung "niedrigere Zahl = sensibler"

audio.ts:
- VAD_SILENCE_DB_OVERRIDE_KEY in AsyncStorage
- loadVadSilenceDbOverride() liefert null oder Zahl
- startRecording: wenn Override gesetzt, Adaptive-Baseline uebersteuert.
  Speech-Schwelle wird auf Override + 10 dB gesetzt. Toast zeigt
  "VAD: manuell stille>-XX dB"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 08:24:26 +02:00
duffyduck 52795530f9 fix(audio): Wake-Word-Anruf-Pause + Resume-Cooldown + Background-Mic-Order
Bug 4 — Wake-Word laeuft bei Anruf weiter:
phoneCall ruft jetzt wakeWordService.pauseForCall bei RINGING/OFFHOOK
und resumeFromCall bei IDLE. Telefonie-App belegt das Mikro waehrend
des Anrufs, openWakeWord muss daher pausieren. Pre-Call-State wird
gemerkt — armed bleibt armed, conversing degraded zu armed (sonst
landet der User nach Auflegen in einem halben Dialog).

Bug 3 — App-Resume triggert faelschlich Wake-Word:
Beim Wechsel von Background nach Foreground gibt's Audio-Pegel-Spikes
(AudioFocus-Switch, AudioTrack re-route), die openWakeWord als Wake-
Word interpretiert. Neuer Cooldown-Mechanismus: AppState-Listener im
ChatScreen ruft wakeWordService.setResumeCooldown(1500) — Detections
in der Phase werden in onWakeDetected verworfen.

Bug 1 — Background-Aufnahme klappt nicht:
acquireBackgroundAudio('rec') wird jetzt VOR audioService.startRecorder
gerufen, acquireBackgroundAudio('wake') VOR OpenWakeWord.start. Sonst
greifen Androids Background-Mic-Restrictions (ab 11+) — der Service mit
foregroundServiceType=microphone muss zum Zeitpunkt des AudioRecord-
Starts schon aktiv sein, nicht erst per state-change-Listener
asynchron danach.

Bug 2 (VAD manchmal nicht): nicht in diesem Commit, vermutlich
umgebungsabhaengig. Toast zeigt die kalibrierten Schwellen — wenn
das nochmal auftritt, schick mir die Werte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:49:02 +02:00
duffyduck d6b54d3247 feat(audio): Background-Service auch fuer Wake-Word + Aufnahme + Doku-Split
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) <noreply@anthropic.com>
2026-05-06 23:43:24 +02:00
duffyduck ead28cf09a feat(audio): Foreground-Service haelt TTS am Leben bei minimierter App
ARIAs Antwort wird jetzt auch dann fertig vorgelesen wenn der User die
App im Hintergrund schickt. Vorher hat Android den Prozess kurz nach
dem Minimieren eingefroren — TTS verstummte mitten im Satz.

Native:
- AriaPlaybackService.kt: Service mit foregroundServiceType=mediaPlayback,
  zeigt persistente Notification "ARIA spricht — antippen oeffnet die App"
  (channel low-priority, ongoing, tap → MainActivity)
- BackgroundAudioModule.kt: RN-Bridge mit start()/stop()
- AndroidManifest: FOREGROUND_SERVICE + FOREGROUND_SERVICE_MEDIA_PLAYBACK
  + POST_NOTIFICATIONS Permissions, Service deklariert

JS:
- backgroundAudio.ts: idempotenter Wrapper (active-Flag verhindert
  doppelte start/stop calls)
- ChatScreen onPlaybackStarted → startBackgroundAudio
- ChatScreen onPlaybackFinished → stopBackgroundAudio
- audio.ts stopPlayback ruft auch stopBackgroundAudio damit die
  Notification bei Cancel/Barge-In/Anruf nicht haengen bleibt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:37:46 +02:00
duffyduck da5579038e fix(vad): adaptive Baseline robuster — minimum + Cap-Bereich
Bug: Wenn beim Aufnahmestart sofort gesprochen wurde (z.B. Wake-Word-
Echo noch im Mikro) ODER der Hintergrund vorruebergehend laut war,
verschob die avg-basierte Baseline die Stille-Schwelle so weit nach
oben, dass normale Hintergrundgeraeusche dauerhaft als "Sprache"
zaehlten — VAD feuerte nie, Aufnahme lief unendlich.

Fix:
- Baseline = MINIMUM der 5 Samples statt Mittelwert (ruhigster Moment)
- Cap auf sinnvollen Bereich:
  - Silence-Schwelle: -50dB bis -28dB (vorher unbegrenzt)
  - Speech-Schwelle:  -40dB bis -18dB
- Erweitertes Log: zeigt sowohl raw als auch geclamp-te Werte

Damit gibt's keine "tote" VAD-Konfiguration mehr — selbst wenn die
Baseline-Messung Schrott ist, bleiben die Schwellen praktikabel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:05:08 +02:00
duffyduck 6651f5937d feat(audio): Wake-Word parallel zu TTS mit AcousticEchoCanceler
Du kannst jetzt "Computer" sagen waehrend ARIA noch redet — TTS
verstummt, neue Aufnahme startet. Vorher musste man warten oder
manuell den Voice-Button tappen.

Native (OpenWakeWordModule.kt):
- AudioRecord-Source von MIC auf VOICE_COMMUNICATION (aktiviert auf
  den meisten Geraeten Echo-Cancellation + Noise-Suppression)
- Zusaetzlich AcousticEchoCanceler/NoiseSuppressor/AutomaticGainControl
  explizit aktiviert wenn vorhanden — robuster auf Geraeten wo die
  VOICE_COMMUNICATION-Source die Effects nicht automatisch mitbringt
- releaseAudioEffects() im stop/dispose

JS (wakeword.ts):
- Neue API: startBargeListening / stopBargeListening — Wake-Word
  parallel aktivieren, ohne den State 'conversing' zu verlassen
- onWakeDetected unterscheidet jetzt: in 'conversing' → barge-in-
  Callback (nicht der normale wake-callback). Sonst Standard-Pfad.
- onBargeIn-Subscriber-API + isBargeListening-Getter

Lifecycle-Wiring (audio.ts + ChatScreen):
- audioService.onPlaybackStarted callback (neu)
- ChatScreen: Bei TTS-Start → wakeWord.startBargeListening
- ChatScreen: Bei TTS-Ende → wakeWord.stopBargeListening (sonst kein
  AudioRecord fuer die naechste Aufnahme)
- ChatScreen: Bei BargeIn → haltAllPlayback + cancel_request +
  150ms-Pause + neue Aufnahme starten

issue.md + README aktualisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:50:09 +02:00
duffyduck 97cb7be313 feat(audio): "Bereit"-Sound (Ding-Dong) wenn Mikro nach Wake-Word offen ist
Kurzer akustischer Hinweis (Airplane Ding-Dong, 20KB MP3) bei
audioService.startRecording-Erfolg im Wake-Word-Pfad — User weiss
exakt ab wann er reden darf, statt das Toast nur zu sehen.

Quelldatei: android/sounds/Airplane-ding-dong.mp2 → ffmpeg-konvertiert
zu MP3 64kbps, abgelegt in android/app/src/main/res/raw/ damit Android
sie als Resource laden kann.

Toggle in App-Settings → Wake-Word, default aktiv. Bei Aktivierung
spielt direkt eine Vorschau ab damit man weiss wie's klingt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:40:45 +02:00
duffyduck 77e927ffcd fix(audio): Placeholder-Race per audioRequestId + Mikro-Offen-Toast erst nach Start
Bug: Bei zwei Sprachnachrichten kurz hintereinander wurde der STT-Text
der zweiten in die Bubble der ersten geschrieben. Ursache: findIndex
matchte ueber Substring "Spracheingabe wird verarbeitet" → bei zwei
offenen Placeholders nahm er immer die ERSTE, egal welches STT-Result
gerade kam.

Fix: jede Aufnahme bekommt eine eindeutige audioRequestId, App pusht
sie in die Placeholder-Bubble + ans audio-Event. Bridge gibt sie
unveraendert ans STT-Result zurueck. App matcht primaer per ID, fallback
auf Substring (Kompatibilitaet zu alten Bridge-Versionen).

Bonus: Toast "Wake-Word erkannt" entfernt, dafuer "🎤 Mikro offen —
sprich jetzt" erst wenn audioService.startRecording wirklich erfolgreich
war. So weiss der User exakt ab wann er reden darf — vorher war der Toast
schon ~400ms vorher da.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:33:26 +02:00
duffyduck 31ff20c846 feat: Max-Aufnahmedauer konfigurierbar + Barge-In gibt aria-core Kontext
Max-Aufnahme:
Default rauf von 2 auf 5 Minuten, in den App-Settings konfigurierbar
zwischen 1 und 30 Minuten (loadMaxRecordingMs aus AsyncStorage,
Storage-Key aria_max_recording_sec). Notbremse-Verhalten bleibt:
nach Ablauf wird die Aufnahme automatisch beendet und gesendet.

Barge-In Kontext:
Wenn der User waehrend ARIA noch redet/arbeitet eine neue Sprach-
oder Text-Nachricht sendet, geht jetzt ein 'interrupted: true' Flag
mit. Bridge praefixed den Text fuer aria-core dann mit:

  "[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.]"

So weiss ARIA dass die neue Message KEINE eigenstaendige Folgefrage
ist sondern auf den abgebrochenen Run bezogen. Der User sieht in
seinem Chat nur den reinen Text — der Hint geht nur an aria-core.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:58:11 +02:00
duffyduck 406f4cb3cc fix: Textauswahl, adaptive VAD-Schwelle + Barge-In bei Sprachaufnahme
Bug 1 — Textauswahl in Bubbles ging nicht mehr:
MessageText hatte verschachtelte <Text onPress={...}> fuer Custom-Link-
Styling. Das fing die Long-Press-Geste ab, daher kein Markieren+Kopieren
mehr. Jetzt nur noch ein einzelnes <Text selectable dataDetectorType="all">,
Android macht URLs/Telefonnummern/Emails per System-Detection klickbar.

Bug 2 — VAD erkannte Stille nicht zuverlaessig (Aufnahme lief endlos):
Festwerte (-45dB Stille / -28dB Sprache) passten nicht zu jeder Umgebung.
In lauteren Raeumen lag der Hintergrundpegel ueber der Stille-Schwelle,
lastSpeechTime wurde dauerhaft aktualisiert → VAD feuerte nie, Aufnahme
lief bis 120s Max-Duration.

Jetzt adaptiv: erste 5 Mic-Samples (~500ms) bilden die Baseline; Stille-
Schwelle = baseline+6dB, Sprache-Schwelle = baseline+12dB. Toast zeigt
die kalibrierten Werte beim Aufnahmestart. Fallback auf -38dB/-22dB falls
das Mikro keine Metering-Updates liefert.

Bug 3 — Barge-In ("ach vergiss es"):
Wenn waehrend ARIAs Antwort eine neue Sprachnachricht aufgenommen wird,
wird ARIAs aktuelle Aktivitaet (TTS + thinking/tool) sofort abgebrochen
bevor die neue Message gesendet wird — wie in einem echten Gespraech wo
man den anderen unterbrechen darf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:49:48 +02:00
duffyduck fec8aa977b feat(audio): TTS pausiert bei Anruf + Conversation-Focus haelt Spotify durchgehend gepaust
Bug 1a — Anruf-Pause:
Neues PhoneCallModule.kt nutzt TelephonyCallback (API 31+) bzw.
PhoneStateListener (Pre-12) um auf RINGING/OFFHOOK/IDLE zu reagieren.
Bei Klingeln/Gespraech ruft phoneCall.ts → audioService.haltAllPlayback,
ARIA verstummt sofort. READ_PHONE_STATE Permission wird beim ersten
Start angefragt; ohne Permission failt der Listener leise.

Bug 1b — Spotify-Resume:
AudioFocus wird jetzt an den Conversation-Lifecycle gekoppelt statt an
einzelne Streams. Solange wakeWordState 'conversing' ist, blockt
acquireConversationFocus() jeden per-Stream-Release. Erst beim Wechsel
auf 'armed'/'off' darf der Focus tatsaechlich freigegeben werden.
Verhindert das "Spotify kommt nach 10s wieder hoch"-Phaenomen auch
ueber Render-Pausen + zwischen mehreren ARIA-Antworten hinweg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:44:58 +02:00
duffyduck 309df9d851 fix(wake-word): Embedding-Output ist rank-4, nicht rank-2 — Trigger funktioniert jetzt
Hauptursache warum kein Wake-Word je triggerte: das Google-Speech-
Embedding-Modell liefert (1,1,1,96), nicht (1,96). Der Cast
`as Array<FloatArray>` warf eine ClassCastException, die vom try/catch
geschluckt wurde — Pipeline lief still ins Leere.

Zusaetzlich:
- WW-Input-Frame-Count wird jetzt aus den Modell-Metadaten gelesen
  (variiert pro Keyword; hey_jarvis=16, computer_v2evtl. anders)
- "Computer" als Wake-Word erweitert (Community-Modell aus
  fwartner/home-assistant-wakewords-collection)

"ARIA" als Wake-Word: gibt's nicht fertig trainiert. Muesste ueber
das openWakeWord Colab-Notebook trainiert werden (~1h auf gratis-GPU).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:24:47 +02:00
duffyduck 55cfb752a2 feat(app): Wake-Word komplett on-device via openWakeWord (ONNX)
Picovoice/Porcupine raus — neuer Stack ist openWakeWord (Apache 2.0,
on-device, ONNX Runtime). Kein API-Key, keine Lizenzgebuehren, Audio
verlaesst das Geraet nicht. Eigene Wake-Words sind via openWakeWord-
Notebook gratis trainierbar.

Pipeline (alles im OpenWakeWordModule.kt):
  1. AudioRecord 16kHz mono int16 in 1280-Sample-Chunks (80ms)
  2. melspectrogram.onnx → 32-mel Frames (mel/10 + 2 wie in Python)
  3. embedding_model.onnx, 76-Frame Sliding Window (stride 8) → 96-dim
  4. hey_jarvis.onnx (oder anderes Keyword) auf letzten 16 Embeddings
  5. Sigmoid-Score, threshold/patience/debounce-Filter
  6. RN-Event "WakeWordDetected" raus

Mitgelieferte Modelle in assets/openwakeword/: hey_jarvis (Default),
alexa, hey_mycroft, hey_rhasspy. Externe Service-API (start/stop/
configure/onWakeWord/...) bleibt identisch — ChatScreen unveraendert.

build.gradle: com.microsoft.onnxruntime:onnxruntime-android:1.17.1
package.json: @picovoice/porcupine-react-native + voice-processor raus
SettingsScreen: AccessKey-Feld weg, neue Keyword-Liste mit Labels
README: Wake-Word-Sektion komplett umgeschrieben (kein Picovoice mehr)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:56:33 +02:00
duffyduck 44d2c6b4fe fix(app): Spotify-Bounce zwischen ARIA-Antworten + Wake-Word-Doku
AudioFocus wird jetzt mit 800ms Verzoegerung freigegeben — wenn ARIA
direkt eine zweite Antwort hinterherschickt oder das Recording ins TTS
uebergeht, wird das Release abgebrochen. Spotify/YouTube haben damit
keine Mikro-Sekunden-Luecke mehr zum Hochkommen waehrend ARIA spricht.

README: neue Sektion zur Wake-Word-Einrichtung mit Picovoice
(7-Tage-Trial, Console-Link, Anleitung fuer eigene Keywords) und
veraltete Wake-Word-Limitation entfernt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:49:45 +02:00