From bb13477ef96091b4d782ea7f80c1f94424baa635 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 23:31:25 +0200 Subject: [PATCH] fix(wake): Race zwischen endConversation und stopBargeListening killt Wake-Word-Listener nach jeder Konversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aus dem Log diagnostiziert: zwei onPlaybackFinished-Listener feuern direkt hintereinander wenn TTS endet: 1. mein neuer Listener (Background): endConversation() → state=armed, OpenWakeWord.start() (idempotent) 2. existierender Listener: stopBargeListening() → bargeListening=true → OpenWakeWord.stop() ← killt re-armed Listener State zeigte 'armed' (UI: Ohr-Icon ausgefuellt, sieht aktiv aus), aber das Native-Modul war gestoppt → Stefan's "Computer" verpufft. Fix: endConversation setzt bargeListening=false BEVOR Native gerufen wird. stopBargeListening checkt das Flag oben: async stopBargeListening() { if (!this.bargeListening) return; ... } → wird zum No-Op wenn endConversation schon gelaufen ist. Bonus: OpenWakeWord.start() darf jetzt auch gerufen werden wenn der Listener via barge-listening schon lief — Kotlin checkt running.get() und resolved idempotent. Sicherer als state-vorher-Check. --- android/src/services/wakeword.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/android/src/services/wakeword.ts b/android/src/services/wakeword.ts index bfd8886..e535fae 100644 --- a/android/src/services/wakeword.ts +++ b/android/src/services/wakeword.ts @@ -344,23 +344,38 @@ class WakeWordService { /** Konversation beenden — User hat im Window nichts gesagt. * Mit Wake-Word: zurueck zu 'armed' (Listener wieder an). * Ohne: zurueck zu 'off'. + * + * WICHTIG: setzt bargeListening=false BEVOR OpenWakeWord.start() laeuft. + * Grund: wenn endConversation aus dem onPlaybackFinished-Handler kommt, + * feuert direkt danach ein zweiter Listener (stopBargeListening) — der + * wuerde sonst OpenWakeWord.stop() rufen weil bargeListening noch true + * ist, und unseren frisch re-armierten Listener killen. */ async endConversation(): Promise { if (this.state !== 'conversing') { - // Nicht in conversing — typ. nach App-Resume bevor Streaming endete. - // Trotzdem loggen damit wir's im Diagnostic sehen. import('./logger').then(m => m.reportAppDebug('wake.end', `endConversation called but state=${this.state} → noop`)).catch(()=>{}); return; } + const wasBarge = this.bargeListening; + // Flag NULLEN bevor wir die Listener triggern. Sonst killt der parallele + // stopBargeListening-Listener (TTS-end) gleich danach unseren Native- + // OpenWakeWord, weil er bargeListening=true sieht und annimmt er muss + // den Listener stoppen. + this.bargeListening = false; import('./logger').then(m => m.reportAppDebug('wake.end', - `endConversation called, nativeReady=${this.nativeReady}, calling OpenWakeWord.start()`)).catch(()=>{}); + `endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{}); if (this.nativeReady && OpenWakeWord) { + // Wenn wakeword schon laeuft (war Barge-Listener waehrend TTS): + // OpenWakeWord.start() ist idempotent (Kotlin checkt running.get() + // und resolved sofort). Wir koennen es trotzdem rufen — billiger + // als state extra zu fragen, garantiert dass nach diesem Pfad + // Native auch wirklich an ist falls es out-of-band gestoppt wurde. try { await OpenWakeWord.start(); - console.log('[WakeWord] Konversation zu Ende — zurueck zu armed'); + console.log('[WakeWord] Konversation zu Ende — zurueck zu armed (wasBarge=%s)', wasBarge); import('./logger').then(m => m.reportAppDebug('wake.end', - `OpenWakeWord.start() OK → state=armed, keyword=${this.keyword}`)).catch(()=>{}); + `OpenWakeWord.start() OK → state=armed, wasBarge=${wasBarge}`)).catch(()=>{}); ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT); this.setState('armed'); return;