Behebt den Konversations-Mischmasch: (1) Barge-in-Einstellung (Default AUS = Halb-Duplex) gated das Mikro-Lauschen waehrend TTS -> ARIA spricht ungestoert zu Ende. (2) interruptAriaIfBusy bricht die Brain-Antwort WIRKLICH ab (cancel_request) statt nur TTS zu muten -> sie antwortet nicht mehr weiter waehrend man redet. (3) Stop-Button stoppt vorhersehbar alles (TTS + Brain-Cancel). Musik: nativer Focus ist schon GAIN_TRANSIENT (pausiert) -> der saubere Flow behebt das Focus-Geflacker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
STT_ENDPOINT_MAX_MS 4000->8000. Der (in a) auf den aktiven Endpoint umgeklemmte 'Stille-Toleranz'-Regler geht damit bis 8s statt nur 4s — genug Zeit zum Nachdenken, ohne dass die Aufnahme endet. Fliesst per endpointMs an Voxtral/Whisper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feste RMS-Grenze (0.012) durch rauschboden-relativen Schwellwert ersetzt (fast-down/slow-up, geklammert). Leises/entferntes Sprechen gilt nicht mehr faelschlich als Stille und wird nicht mitten im Satz gecuttet. audio.ts: Fallback-endpointMs 1500->2400 vereinheitlicht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher war die serialisierte Sprachausgabe zweier fast gleichzeitig fertiger
Antworten Timing-Glueck: PcmStreamPlayer.start() ruft stopInternal() (flush+
release), eine neue Antwort haette die laufende also abgeschnitten, sobald ihr
Audio waehrend der Wiedergabe der ersten ankam.
Jetzt echte Abspiel-Queue im audioService: kommt eine neue HOERBARE Antwort
waehrend eine andere noch hoerbar spielt (pcmAudiblePlaying bis
PcmPlaybackFinished, nicht nur bis Stream-Ende), werden ihre PCM-Chunks
gepuffert und erst nach dem Drain der laufenden nachgespielt. Bei wartender
Antwort meldet PcmPlaybackFinished NICHT 'fertig' (kein Wake-Word-Re-Arm).
Harter Stop/Barge-In/Mute verwirft die Queue. Race gegen gleichzeitige Chunks
einer dritten Antwort geschlossen (Flags vor await gesetzt). onPcmCached meldet
den WAV-Pfad nachgespielter Antworten fuer Mund-Button-Replay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Fehler: (1) War der PCM-Stream schon komplett empfangen (isFinal durch),
sind pcmStreamActive+isPlaying false — der AudioTrack spielt aber seinen
Buffer noch sekundenlang aus. stopPlayback() returnte dann früh ("nichts
aktiv") und rief PcmStreamPlayer.stop() NIE → Mund-Button wirkungslos.
(2) stopPlayback() wirft pcmBuffer weg → die Cache-WAV waere unvollstaendig,
Nachhoeren via Lautsprecher-Symbol kaputt.
Neue _silenceAudibleOutput(): stoppt den AudioTrack IMMER (auch im Drain-Fall),
laesst aber pcmBuffer/pcmMessageId/pcmStreamActive stehen — restliche Chunks
cachen stumm weiter, isFinal schreibt die vollstaendige WAV. setMuted(true)
nutzt das statt stopPlayback(). stopPlayback bleibt fuer Barge-In/Cancel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#2 Vorzeitiges Absenden: endpointMs war hart 1500ms — im Auto (Sprechpausen)
schnitt das mitten im Satz ab (die 11.8s-Frage wurde bei "…ohne dass ein"
gekappt). Jetzt konfigurierbar (aria_stt_endpoint_ms, Default 2400, 1000-4000).
#3 Ohr bleibt ausgegraut: beim Re-Arm rief der Wake-Word-Service
OpenWakeWord.start(), waehrend die passive Streaming-Aufnahme noch das Mikro
hielt → start() schlug fehl → state=off. Neuer micReleaseHook cancelt die
Aufnahme VOR start() (endConversation / exitPassiveListening /
discardIfFreshlyTriggered). ChatScreen registriert den Hook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native AudioFocus: dispatchMediaPlay() (echter KEYCODE_MEDIA_PLAY an die aktive
MediaSession, wie die Kopfhoerer-Play-Taste) + isMusicActive() zum Gaten.
audio.ts merkt vor dem Focus-Grab ob Musik lief und resumt am Dialog-Ende nur
dann — deterministisch statt des auf OnePlus flakigen nudgeMediaResume.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Bugs aus dem ersten Live-Test des Multi-Threading-Designs.
Bug 1 — Voice ignorierte App-Focus:
Stefan hat in Projekt X reingeguckt und was reingesagt — Message landete
im Hauptchat statt in X. Der Voice-Router auf der Bridge kannte den
sichtbaren Kontext der App nicht.
Fix:
- audio.ts.startStreamingRecording nimmt neuen opts.projectId und
schickt es im stt_stream_start-Payload mit.
- ChatScreen.tsx: alle 4 startStreamingRecording-Callsites (wake,
barge-in, passive, manuell) uebergeben focusedProjectIdRef.current.
Neuer useRef-Spiegel damit die Focus-ID auch in useCallbacks/
useEffects mit alten Closures aktuell bleibt.
- aria_bridge.py: neuer Handler fuer stt_stream_start speichert die
projectId in self._stt_stream_projects[requestId], stt_stream_end
loescht wieder. Beim stt_endpoint wird sie an _process_endpoint_text
weitergereicht und dort als default_project_id in den Voice-Router.
- _apply_voice_router bekommt neuen Prio-Rank 4: „App-Focus als
Default" — greift wenn kein Meta, kein Prefix und kein aktiver Sticky.
So folgt Voice ohne extra Marker dem sichtbaren Kontext.
Bug 2 — Back-to-Main-Regex zu eng:
„zurück ins hauptmenü" wurde nicht als Meta erkannt (Regex matchte nur
„zurück zum hauptchat") und landete deshalb im aktiven Sticky-Projekt.
Fix: Regex akzeptiert jetzt auch hauptmenü, menü, haupt, main mit
Praepositionen „zum/zur/ins/in den".
Bonus — Burger-Button heller:
Stefan konnte den ☰-Toggle im Header kaum sehen. Farbe von Default
(dunkelgrau) auf #E0E0F0 (hell) mit fontWeight 700 gesetzt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stefan testet Spotify-Resume nach TTS, klappt noch nicht. Aktuell sehe ich
in den App-Logs nur 'PcmPlaybackFinished native event' aber NICHT ob
requestDuck / release / nudgeMediaResume jemals laufen.
Logge jetzt:
audio.focus: 'TTS-start: requestDuck() called + canceled pending release'
audio.focus: '_releaseFocusDeferred SKIPPED (conversation active)' (skip case)
audio.focus: '_releaseFocusDeferred scheduled in 800ms'
audio.focus: 'release timer fired but conversation now active → SKIP' (race)
audio.focus: 'AudioFocus.release() now'
audio.focus: 'nudgeMediaResume() now (50ms after release)'
Damit beim naechsten Stefan-Test eindeutig zuordenbar wo der Resume-
Pfad genau klemmt — feuern beide native Calls? Werden sie geskipped?
Greift der Cancel zu frueh? etc.
Stefan-Symptom: Spotify pausiert wenn ARIA zuhoert/spricht ✓, aber
resumed nach TTS-Ende NICHT (oder nur unzuverlaessig).
Diagnose aus dem Log-Trace:
Manual-Button-Flow:
recording start → AudioFocus.requestExclusive (Spotify pausiert)
STT-Endpoint → _cleanupStreamLocal → _releaseFocusDeferred → 800ms
Timer → release+nudge (Spotify wuerde resumen)
Brain processing 50s lang...
TTS-Start → KEIN expliziter Focus-Request! AudioTrack-USAGE_
ASSISTANT pausiert Spotify nur IMPLIZIT (versions-
abhaengig). Wir wissen nicht ob Spotify gerade
gepaust ist.
TTS-End → release+nudge — aber wenn Spotify implizit-paused
ist, hat es keinen sauberen Focus-Owner gesehen
und nudge alleine reicht nicht zum Resume.
Wake-Word-Flow:
Aehnliches Problem wenn state schon armed ist beim TTS-Ende
(vom 'no-speech in conv-window'-Pfad), dann ist mein
endConversation ein noop → releaseConversationFocus laeuft nie,
nur die PcmPlaybackFinished-Direkt-Release greift, hat aber
dasselbe nudge-zu-schwach-Problem.
Fix in _firePlaybackStarted (audio.ts): EXPLIZITES AudioFocus.requestDuck
beim ersten TTS-PCM-Chunk. Damit IST Spotify ueber unseren Focus
gepaused, statt nur implizit. Der spaetere release beim Pcm-Playback-
Finished ist dann das normale 'Owner ist fertig'-Pattern das Spotify
zuverlaessig zum Resume triggert.
Idempotenz: requestDuck released vorher den vorigen Focus (in Kotlin),
also harmlos wenn wake-word-acquireConversationFocus eh schon requestDuck
gerufen hat. Plus _cancelDeferredFocusRelease vorne, damit kein noch
pendender 800ms-Timer mitten in der TTS Spotify falsch resumed.
Stefan testet im Auto und auf dem Tisch — beide Spotify-Versionen
sollten mit explizitem Focus-Owner-Wechsel sauber pausieren+resumen.
Race-Condition entdeckt im Log: nach jeder ARIA-Antwort lief
endConversation 5s nach TTS-Start (= "letzter Chunk eingetroffen"),
nicht wenn der AudioTrack-Hardware-Buffer wirklich am Ende war. ARIA
sprach also noch hoerbar, waehrend OpenWakeWord schon re-armte.
Folge: ARIAs eigene Stimme ging direkt nach AudioRecord.startRecording
ins Mikro. Die OpenWakeWord-Sessions von AudioRecord und AudioTrack
sind verschieden → AcousticEchoCanceler kann den Output nicht
subtrahieren (kein gemeinsamer Reference-Stream). Threshold +
Patience-State der Wake-Word-Inferenz wird durch ARIAs konstante
Audio-Eingabe verwirrt, der naechste echte "Computer"-Trigger geht
unter.
Fix: Listener-Fire aus handlePcmChunk(isFinal=true) raus, dafuer in
den schon existierenden PcmPlaybackFinished-Native-Event-Handler
rein. Die Kotlin-Seite emittiert das Event aus dem Writer-Thread-
finally-Block — also genau dann wenn AudioTrack alle Samples
durchgeschrieben hat.
Side-Effect: UI-Konsumenten von onPlaybackFinished sehen den
"finished"-State jetzt 1-2s spaeter (= ehrlicher zur Realitaet,
ist eigentlich eine UX-Verbesserung).
streamEndpointFired-Latch + neue _fireEndpoint(ev)-Methode konsolidieren
die drei Pfade die den Endpoint-Listener feuern (RVS-stt_endpoint, cancel,
neuer Fallback). Listener feuert pro Session-Cycle maximal einmal.
stopStreamingRecording bekommt einen 3-Sekunden-Watchdog: kommt in dem
Fenster keine echte stt_endpoint-Antwort der Bridge, feuert der
Listener mit text='' (reason=stop:...:no-response) damit ChatScreen
die "wird verarbeitet"-Bubble unstickt + endConversation aufruft.
Greift praktisch in zwei Faellen:
- Whisper-Bridge laeuft alte/keine Streaming-Version (Stefan Gamebox-
Restart vergessen) → wir bleiben sonst bis zur 60s-Hardcap haengen
- User-initiated Stop + Whisper langsam/crashed
audio.ts:
- neue Methoden startStreamingRecording / stopStreamingRecording /
cancelStreamingRecording mit PcmStreamRecorder als AudioRecord-Source
- permanenter RVS-Listener fuer stt_partial / stt_endpoint / stt_stream_done,
Filterung ueber streamRequestId-Match
- Callbacks onSttEndpoint(SttEndpointEvent) + onSttPartial(text)
- No-Speech-Watchdog + App-seitiger Hard-Cap (+2s Toleranz gegen Bridge)
- cancelStreamingRecording feuert onSttEndpoint mit text='' damit
ChatScreen den No-Speech-Fall behandeln kann (wie frueher
onSilenceDetected -> stopRecording() -> null)
- Legacy startRecording / stopRecording / onSilenceDetected unangetastet
-- VoiceButton (manuelle Aufnahme) nutzt das weiterhin
ChatScreen.tsx:
- Wake-Callback: startRecording -> startStreamingRecording
- Bubble wird sofort gebaut, audioRequestId landet via
stt_endpoint -> chat(sender=stt) im chat-Handler-Update-Pfad wie bisher
- onSilenceDetected entfernt, ersetzt durch onSttEndpoint:
text != '' -> log, aria-bridge triggert Brain selbst (Phase-2-Shortcut)
text == '' -> endConversation (No-Speech-Fall)
- Barge-In via Wake-Word: ebenfalls auf Streaming umgestellt
- AppState-resume + toggleWakeWord-off pruefen jetzt isStreamingRecording()
und nutzen passenden Cancel
Damit: kein dB/VAD mehr im Hot-Path. Whisper hoert auf semantische
Stille (kein neuer Text), Brain bekommt den Text direkt von aria-bridge,
Audio-Roundtrip App->aria->whisper->aria->App entfaellt komplett.
Stefan-Bug-Report: ARIA liest Nachricht vor, Spotify pausiert korrekt,
ARIA spricht durch — aber Spotify spielt danach NICHT automatisch
weiter. Sollte mit GAIN_TRANSIENT auto-resumen, tut es aber bei
manchen Spotify-Versionen/Geraeten nicht zuverlaessig.
Hintergrund: alte kickReleaseMedia() mit AUDIOFOCUS_GAIN (permanent)
war zu aggressiv (Spotify interpretierte als "user stoppte" =
Auto-Resume kaputt). Wurde entfernt. Jetzt ist das Pendel andersrum
zu weit: ohne Nudge keine Resume.
Sanfter Mittelweg: nudgeMediaResume() mit GAIN_TRANSIENT statt
GAIN-permanent. 100ms hold, abandon. Spotify bekommt Focus-Wechsel-
Hint ohne "user stopped"-Effekt.
audio.ts: nach AudioFocus.release() 50ms warten, dann nudgeMediaResume.
AudioFocusModule.kt: neue Methode + alte kickReleaseMedia bleibt mit
⚠️-Markierung fuer andere Use-Cases.
APK neu bauen erforderlich.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Bug 2: STT-Result ueberschrieb beide noch unaufgeloeste Audio-Bubbles
mit gleichem Text. Fix: nur die ERSTE matchende Bubble aktualisieren
(findIndex + index-Update statt map). Reihenfolge ist FIFO weil Whisper
sequenziell verarbeitet.
Bug 3: Speed-Param wird nun in jedem Hop geloggt:
- ChatScreen: "[Chat] sende mit voice=X speed=Y"
- aria-bridge: "XTTS-Request gesendet (voice=X, speed=Y.YYx)"
- f5tts-bridge: "F5-TTS: N Satz(e), voice=X, speed=Y.YYx"
Damit kann man im logcat/docker-logs eindeutig sehen wo speed evtl.
verloren geht oder ob die Stimme einfach von Natur aus schnell ist.
Bug 4: VAD-Trigger-Reason mit Schwelle: "VAD NNN ms Stille (Schwelle=NNN ms)".
Plus startRecording loggt jetzt VAD-Stille + MAX-Recording.
Bug 1 (Porcupine): mehr Debug + Toast-Meldungen.
- init failure: err.name/code/stack ins Log
- start() ohne Porcupine: Toast "Access Key in Settings setzen"
- start() Fehler: Toast mit Fehlermeldung
- configure(): Toast wenn init scheitert
- Erfolgreiches arming: Toast "Lausche auf X"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bei kurzen Saetzen (nur ein paar Chunks + sofort final) konnten die
async handlePcmChunk-Calls parallel laufen. Der final-Chunk konnte
native end() aufrufen BEVOR der vorherige Chunk seinen native start()
abgeschlossen hatte. Der Writer-Thread startete dann mit endRequested
bereits true, verarbeitete keine Chunks sauber → Audio ging verloren.
Fix: Wrapper chaint alle Chunk-Calls an eine Promise-Queue:
_pcmChunkQueue = Promise.resolve()
handlePcmChunk → _pcmChunkQueue.then(() => _handlePcmChunkImpl(p))
So werden start/writeChunk/end garantiert in der richtigen Reihenfolge
verarbeitet. Der API-Contract bleibt (gleiches return-Promise).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
F5-TTS ist schnell genug dass der Puffer bei kurzen Saetzen eher
schadet als nuetzt — er verzoegert den play()-Start fuer Sekunden die
dann als Wartezeit auffallen.
Aenderungen:
- audio.ts: TTS_PREROLL_MIN_SEC 1.0 → 0 (Einstellbar in Settings)
- PcmStreamPlayerModule.kt: MIN_PREROLL_SECONDS auf 0.0, Fallback-
Logic respektiert jetzt 0 als gueltigen Wert (vorher hat der
.let { if (it > 0) it else DEFAULT } 0 zu 3.5s umgebogen).
Bei preroll=0 greift der Leading-Silence von 200ms immer noch, d.h.
AudioTrack-Startup bleibt sauber. play() wird dann beim allerersten
echten PCM-Chunk aufgerufen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TTS_SPEED_MIN 0.5 → 0.1, TTS_SPEED_MAX 2.0 → 5.0.
Bridge-seitige Validierungen (aria_bridge.py + f5tts/bridge.py) mit-
gezogen auf den gleichen Bereich.
Hinweis: Extremwerte (unter 0.5 oder ueber 2.0) koennen bei F5-TTS
verzerrte Ausgaben produzieren — Stefan bekommt die Freiheit zum
Experimentieren.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drei zusammenhaengende Bugs:
1. VAD-Timer feuerte im 200ms setInterval WEITER nachdem die Stille-
Schwelle erreicht war — listeners wurden pro Aufnahme bis zu 5x
getriggert. Parallel laufende stopRecording()-Calls lieferten
audio-recorder-player's nativen Layer OOM / Crash.
Fix: silenceFired-Latch + Timer-Clear SOFORT beim ersten Feuer
(fireSilenceOnce-Helper). Gleiche Logik fuer Max-Dauer + Conv-Window.
2. VoiceButton silence-listener re-registrierte bei jedem isRecording-
Flip (deps [isRecording, onRecordingComplete]). Closure-State war
stale, und bei schnellen flips gabs register/unregister-Races.
Fix: empty deps, state direkt vom audioService via getRecordingState()
lesen. onRecordingComplete via Ref (damit der Callback aktuell bleibt
ohne re-register).
3. handleTap las den Button-State aus React (isRecording), der bei
schnellen Taps stale sein konnte — "erst zweiter Tap geht" Symptom.
Fix: audioService.getRecordingState() als Source-of-Truth, plus
tapBusy-Ref als Anti-Doppel-Tap-Guard waehrend asyncer start/stop.
'processing'-State wird korrekt ignoriert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stefan berichtet dass Auto-Playback trotz Closure-Fix nicht greift.
Zwei neue Log-Zeilen die beim naechsten Test direkt zeigen was schief
laeuft:
- ChatScreen: "[Chat] audio-msg canPlay=X (enabled=Y muted=Z)"
- audio.ts: "[Audio] PCM-Stream start: silent=X messageId=Y ..."
Ausreichend um zu unterscheiden:
* canPlay=false trotz Mund-an → ttsMuted bleibt im State haengen
* canPlay=true aber silent=true in audio.ts → Ref-Bug oder race
* silent=false aber nichts hoerbar → native-module oder audio-routing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bugs:
- App Mute-/Auto-Playback: onMessage-Closure hielt stale ttsDeviceEnabled/
ttsMuted → Mute wurde ignoriert + AsyncStorage-Load kam nicht durch.
Fix via ttsCanPlayRef (live gespiegelt) statt Closure-Variablen.
- App Zombie-Recording: toggleWakeWord hat die laufende Aufnahme nicht
gestoppt → audioService.recordingState blieb 'recording' → normaler
Aufnahme-Button wirkungslos. Fix: await stopRecording() vor stop().
- Porcupine robuster: BuiltInKeywords-Enum Mapping mit String-Fallback,
errorCallback fuer Runtime-Crashes (state zurueck auf off statt
App-Crash), mehr Logging damit man beim naechsten Issue debuggen kann.
App-Features:
- MessageText Komponente: Text ist durchgehend selektierbar, erkennt
URLs (http/https), E-Mails, Telefonnummern und macht sie anklickbar
(oeffnet Browser / Mail-App / Android-Dialer via Linking).
- TTS-Wiedergabegeschwindigkeit pro Geraet einstellbar (Settings ->
"Sprechgeschwindigkeit", 0.5-2.0 in 0.1-Schritten, Default 1.0).
Wird als speed-Param an die F5-TTS-Bridge durchgereicht.
Bridge-Durchreichen:
- ChatScreen: speed aus AsyncStorage via ttsSpeedRef, an chat/audio/
tts_request mitgeschickt
- aria-bridge: _next_speed_override wie voice_override, an xtts_request
weitergereicht
- f5tts-bridge: speed-Param an F5TTS.infer() durchgereicht
Diagnostic-Feature:
- Voice-Preview-Button (Play-Icon) vor dem Delete-X in der Stimmen-Liste
- Modal mit Textfeld (Default-Beispieltext wird bei jedem Oeffnen neu
gesetzt) und Play-Button
- Server sammelt audio_pcm Frames der Preview-Anfrage, baut WAV,
schickt base64 zurueck, Browser spielt im <audio>-Tag ab
- 60s Timeout-Safety-Net
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>