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>
This commit is contained in:
2026-05-16 15:54:07 +02:00
parent 2bac9c26ca
commit 048d231b60
3 changed files with 84 additions and 5 deletions
+25
View File
@@ -727,6 +727,31 @@ class AudioService {
}
}
/** Aufnahme abbrechen ohne RecordingResult zu emittieren — z.B. bei
* Wake-Word-False-Positive beim App-Resume aus laengerem Hintergrund.
* Aufgenommene Datei wird sofort verworfen. */
async cancelRecording(): Promise<void> {
if (this.recordingState !== 'recording') return;
console.log('[Audio] Aufnahme abgebrochen (cancel)');
this.vadEnabled = false;
if (this.vadTimer) { clearInterval(this.vadTimer); this.vadTimer = null; }
if (this.maxDurationTimer) { clearTimeout(this.maxDurationTimer); this.maxDurationTimer = null; }
if (this.noSpeechTimer) { clearTimeout(this.noSpeechTimer); this.noSpeechTimer = null; }
try {
const path = await this.recorder.stopRecorder();
this.recorder.removeRecordBackListener();
// Datei loeschen wenn da
if (path && path !== 'Already stopped') {
const local = path.replace(/^file:\/\//, '');
try { await RNFS.unlink(local); } catch {}
}
} catch (err) {
console.warn('[Audio] cancelRecording stop fehlgeschlagen:', err);
}
this._releaseFocusDeferred();
this.setState('idle');
}
/** Aufnahme stoppen und Ergebnis zurueckgeben */
async stopRecording(): Promise<RecordingResult | null> {
if (this.recordingState !== 'recording') {