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:
@@ -275,12 +275,20 @@ const ChatScreen: React.FC = () => {
|
|||||||
return () => { unsubUpdate(); clearTimeout(timer); };
|
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(() => {
|
useEffect(() => {
|
||||||
const unsubWake = wakeWordService.onWakeWord(async () => {
|
const unsubWake = wakeWordService.onWakeWord(async () => {
|
||||||
console.log('[Chat] Wake Word erkannt — starte Auto-Aufnahme');
|
console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme');
|
||||||
// TTS stoppen damit ARIA sich nicht selbst hoert
|
|
||||||
audioService.stopPlayback();
|
|
||||||
// Aufnahme mit Auto-Stop (VAD) starten
|
// Aufnahme mit Auto-Stop (VAD) starten
|
||||||
const started = await audioService.startRecording(true);
|
const started = await audioService.startRecording(true);
|
||||||
if (!started) {
|
if (!started) {
|
||||||
|
|||||||
@@ -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 */
|
/** Naechstes Audio aus der Queue abspielen */
|
||||||
private async _playNext(): Promise<void> {
|
private async _playNext(): Promise<void> {
|
||||||
if (this.audioQueue.length === 0) {
|
if (this.audioQueue.length === 0) {
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
|
// Alle Audio-Teile abgespielt → Listener benachrichtigen
|
||||||
|
this.playbackFinishedListeners.forEach(cb => cb());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Wake Word Service — "ARIA" Erkennung
|
* Gespraechsmodus — "Ohr-Button"
|
||||||
*
|
*
|
||||||
* Phase 1: Deaktiviert — react-native-live-audio-stream hat native Bridge-Probleme.
|
* Wenn aktiv: Nach jeder ARIA-Antwort (TTS fertig) startet automatisch die Aufnahme.
|
||||||
* Nutzt stattdessen Tap-to-Talk (VoiceButton) als primaeren Eingabemodus.
|
* 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;
|
type WakeWordCallback = () => void;
|
||||||
@@ -17,30 +18,33 @@ class WakeWordService {
|
|||||||
private wakeCallbacks: WakeWordCallback[] = [];
|
private wakeCallbacks: WakeWordCallback[] = [];
|
||||||
private stateCallbacks: StateCallback[] = [];
|
private stateCallbacks: StateCallback[] = [];
|
||||||
|
|
||||||
/** Wake Word Erkennung starten */
|
/** Gespraechsmodus starten */
|
||||||
async start(): Promise<boolean> {
|
async start(): Promise<boolean> {
|
||||||
if (this.state === 'listening') return true;
|
if (this.state === 'listening') return true;
|
||||||
|
console.log('[WakeWord] Gespraechsmodus aktiviert — Aufnahme startet nach ARIA-Antwort');
|
||||||
try {
|
this.setState('listening');
|
||||||
// Phase 1: LiveAudioStream deaktiviert (native Bridge instabil)
|
return true;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wake Word Erkennung stoppen */
|
/** Gespraechsmodus stoppen */
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
console.log('[WakeWord] Gespraechsmodus deaktiviert');
|
||||||
this.setState('off');
|
this.setState('off');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nach Aufnahme erneut starten */
|
/** Nach ARIA-Antwort (TTS fertig): Aufnahme automatisch starten */
|
||||||
async resume(): Promise<void> {
|
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 ---
|
// --- Callbacks ---
|
||||||
|
|||||||
Reference in New Issue
Block a user