From 3c2e537420340c3f66aa4531f0d5c6f7334983f6 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 22:23:13 +0200 Subject: [PATCH] fix(wake): kein Conversation-Window-Resume wenn JS-Thread verspaetet aufwacht MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: User sagt "Naechstes Lied bitte", ARIA spielt Track, Display geht aus, User holt 10s spaeter die App vor und sieht "Aufnahme laeuft" — als haette er Wake-Word gesagt. Klassisches Doze-Throttling: nach TTS-Ende schedulet resume() einen setTimeout(800ms) der den Conversation- Window-Callback feuert. Im Hintergrund parkt der JS-Thread, der Timer feuert erst beim App-Resume — gefuehlt ein Phantom-Trigger. Fix: scheduledAt-Timestamp messen, Delay nach dem setTimeout pruefen. Wenn der Timer >2.8s ueberfaellig ist (Schwelle = 800ms + 2000ms Toleranz), JS war im Background → endConversation statt Mikro-oeffnen. Wenn der User wirklich nachfragen will sagt er einfach nochmal "Computer". --- android/src/services/wakeword.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/android/src/services/wakeword.ts b/android/src/services/wakeword.ts index 5d32f1f..b9c53e1 100644 --- a/android/src/services/wakeword.ts +++ b/android/src/services/wakeword.ts @@ -390,15 +390,35 @@ class WakeWordService { return true; } - /** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten */ + /** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten. + * + * WICHTIG: setTimeout(800ms) kann im Hintergrund (Display aus) verspaetet + * feuern — JS-Thread ist geparkt. Wenn der Timer >2s ueberfaellig ist, + * hat der User offensichtlich die App verlassen und kommt erst spaeter + * wieder — wir oeffnen das Mikro dann NICHT, sondern beenden die + * Konversation. Sonst sieht der User nach dem App-Resume "Mikro plus- + * aufnahme laeuft" obwohl er gar nichts gesagt hat → wirkt wie Phantom- + * Wake-Word. Klassische Doze-Throttling-Falle wie bei wake.detect frueher. */ async resume(): Promise { if (this.state !== 'conversing') return; + const scheduledAt = Date.now(); // Kurze Pause damit TTS-Audio nicht ins Mikrofon geht await new Promise(resolve => setTimeout(resolve, 800)); - if (this.state === 'conversing') { - console.log('[WakeWord] TTS fertig — naechste Aufnahme im Conversation-Window'); - this.wakeCallbacks.forEach(cb => cb()); + if (this.state !== 'conversing') return; + const delay = Date.now() - scheduledAt; + if (delay > 2800) { + // Timer war stark verspaetet — JS-Thread war im Hintergrund geparkt. + // Conversation als beendet behandeln statt das Mikro zu oeffnen. + console.log('[WakeWord] resume(): %dms statt ~800ms — App war im Background. endConversation statt mic-open', delay); + import('./logger').then(m => m.reportAppDebug('wake.resume', + `delayed ${delay}ms (>2800) — endConversation statt mic-open`)).catch(()=>{}); + // Asynchroner Aufruf — endConversation ist async, kein await damit wir + // hier nicht in einem Promise-Chain haengen. + this.endConversation().catch(() => {}); + return; } + console.log('[WakeWord] TTS fertig — naechste Aufnahme im Conversation-Window (delay=%dms)', delay); + this.wakeCallbacks.forEach(cb => cb()); } /** True solange das Ohr aktiv ist (armed ODER conversing). */