feat: Conversation mode (ear button) - auto-record after ARIA speaks

- 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) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-11 11:40:55 +02:00
parent 51b9512f4e
commit 2929749314
3 changed files with 47 additions and 23 deletions

View File

@ -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) {

View File

@ -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<void> {
if (this.audioQueue.length === 0) {
this.isPlaying = false;
// Alle Audio-Teile abgespielt → Listener benachrichtigen
this.playbackFinishedListeners.forEach(cb => cb());
return;
}

View File

@ -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<boolean> {
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<void> {
// 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 ---