From 2929749314353e30fc346e9c5f852a51f0e6b498 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Apr 2026 11:40:55 +0200 Subject: [PATCH] feat: Conversation mode (ear button) - auto-record after ARIA speaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ear button activates conversation mode (green dot) - After TTS playback finishes → 800ms pause → auto-start recording - VAD stops recording on silence → sends to ARIA → ARIA answers → TTS → loop - Like a natural conversation / walkie-talkie mode - Audio service fires onPlaybackFinished when queue empty Co-Authored-By: Claude Opus 4.6 (1M context) --- android/src/screens/ChatScreen.tsx | 16 +++++++++--- android/src/services/audio.ts | 12 +++++++++ android/src/services/wakeword.ts | 42 ++++++++++++++++-------------- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 45ec12f..98ec9a1 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -275,12 +275,20 @@ const ChatScreen: React.FC = () => { return () => { unsubUpdate(); clearTimeout(timer); }; }, []); - // Wake Word: "ARIA" Erkennung → Auto-Aufnahme starten + // Gespraechsmodus: Nach TTS-Wiedergabe automatisch Aufnahme starten + useEffect(() => { + const unsubPlayback = audioService.onPlaybackFinished(() => { + if (wakeWordService.isActive()) { + wakeWordService.resume(); + } + }); + return () => unsubPlayback(); + }, []); + + // Wake Word / Gespraechsmodus: Auto-Aufnahme starten useEffect(() => { const unsubWake = wakeWordService.onWakeWord(async () => { - console.log('[Chat] Wake Word erkannt — starte Auto-Aufnahme'); - // TTS stoppen damit ARIA sich nicht selbst hoert - audioService.stopPlayback(); + console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme'); // Aufnahme mit Auto-Stop (VAD) starten const started = await audioService.startRecording(true); if (!started) { diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index c484f74..e51feb4 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -214,10 +214,22 @@ class AudioService { } } + // Callback wenn alle Audio-Teile abgespielt sind + private playbackFinishedListeners: (() => void)[] = []; + + onPlaybackFinished(callback: () => void): () => void { + this.playbackFinishedListeners.push(callback); + return () => { + this.playbackFinishedListeners = this.playbackFinishedListeners.filter(cb => cb !== callback); + }; + } + /** Naechstes Audio aus der Queue abspielen */ private async _playNext(): Promise { if (this.audioQueue.length === 0) { this.isPlaying = false; + // Alle Audio-Teile abgespielt → Listener benachrichtigen + this.playbackFinishedListeners.forEach(cb => cb()); return; } diff --git a/android/src/services/wakeword.ts b/android/src/services/wakeword.ts index b50a833..a99a528 100644 --- a/android/src/services/wakeword.ts +++ b/android/src/services/wakeword.ts @@ -1,10 +1,11 @@ /** - * Wake Word Service — "ARIA" Erkennung + * Gespraechsmodus — "Ohr-Button" * - * Phase 1: Deaktiviert — react-native-live-audio-stream hat native Bridge-Probleme. - * Nutzt stattdessen Tap-to-Talk (VoiceButton) als primaeren Eingabemodus. + * Wenn aktiv: Nach jeder ARIA-Antwort (TTS fertig) startet automatisch die Aufnahme. + * Wie ein Walkie-Talkie / natuerliches Gespraech: + * ARIA spricht → Aufnahme startet → User spricht → VAD stoppt → ARIA antwortet → ... * - * Phase 2: Porcupine on-device "ARIA" Keyword (geplant). + * Phase 2 (geplant): Porcupine "ARIA" Wake Word fuer passives Lauschen. */ type WakeWordCallback = () => void; @@ -17,30 +18,33 @@ class WakeWordService { private wakeCallbacks: WakeWordCallback[] = []; private stateCallbacks: StateCallback[] = []; - /** Wake Word Erkennung starten */ + /** Gespraechsmodus starten */ async start(): Promise { if (this.state === 'listening') return true; - - try { - // Phase 1: LiveAudioStream deaktiviert (native Bridge instabil) - // Stattdessen: Tap-to-Talk als primaerer Modus - console.log('[WakeWord] Wake Word ist in Phase 1 noch nicht verfuegbar — nutze Tap-to-Talk'); - this.setState('listening'); - return true; - } catch (err) { - console.error('[WakeWord] Start fehlgeschlagen:', err); - return false; - } + console.log('[WakeWord] Gespraechsmodus aktiviert — Aufnahme startet nach ARIA-Antwort'); + this.setState('listening'); + return true; } - /** Wake Word Erkennung stoppen */ + /** Gespraechsmodus stoppen */ stop(): void { + console.log('[WakeWord] Gespraechsmodus deaktiviert'); this.setState('off'); } - /** Nach Aufnahme erneut starten */ + /** Nach ARIA-Antwort (TTS fertig): Aufnahme automatisch starten */ async resume(): Promise { - // Nichts zu tun in Phase 1 + if (this.state !== 'listening') return; + // Kurze Pause damit TTS-Audio nicht ins Mikrofon geht + await new Promise(resolve => setTimeout(resolve, 800)); + if (this.state === 'listening') { + console.log('[WakeWord] TTS fertig — starte automatisch Aufnahme'); + this.wakeCallbacks.forEach(cb => cb()); + } + } + + isActive(): boolean { + return this.state === 'listening'; } // --- Callbacks ---