/** * Gespraechsmodus / Wake Word Service * * Drei Zustaende: * off — Ohr aus, nichts laeuft * armed — Ohr aktiv, wartet auf Wake Word ("ARIA"). Mikro IST AUS. * (Sobald Porcupine integriert ist, hoert hier der Wake-Word- * Detektor passiv mit. Aktuell ist das gleichbedeutend mit "off" * bis der User wieder tippt — Stub fuer spaeter.) * conversing — Wake Word getriggert / Ohr-Tap ohne Wake Word: aktive Konvers- * ation mit ARIA. Mikro oeffnet nach jeder ARIA-Antwort fuer X * Sekunden (Conversation-Window). Spricht der User nichts in dem * Fenster → zurueck auf armed (kein erneuter Tap noetig sobald * Porcupine drin ist). * * Aktuell ohne Porcupine: armed wird nur als Lifecycle-State gefuehrt; bei * Conversation-Ende geht's direkt auf 'off' damit User klares Feedback bekommt. */ type WakeWordCallback = () => void; type StateCallback = (state: WakeWordState) => void; export type WakeWordState = 'off' | 'armed' | 'conversing'; class WakeWordService { private state: WakeWordState = 'off'; private wakeCallbacks: WakeWordCallback[] = []; private stateCallbacks: StateCallback[] = []; private wakeWordSupported: boolean = false; // wird gesetzt wenn Porcupine spaeter integriert ist /** Ohr-Button gedrueckt — startet Konversation (oder armed wenn Wake-Word verfuegbar) */ async start(): Promise { if (this.state !== 'off') return true; if (this.wakeWordSupported) { // Spaeter: Porcupine starten und auf "ARIA" warten console.log('[WakeWord] armed — warte auf Wake Word'); this.setState('armed'); } else { // Heute: direkt in die Konversation console.log('[WakeWord] Konversation startet sofort (kein Wake-Word)'); this.setState('conversing'); setTimeout(() => { if (this.state === 'conversing') { this.wakeCallbacks.forEach(cb => cb()); } }, 500); } return true; } /** Komplett ausschalten (Ohr abschalten) */ stop(): void { console.log('[WakeWord] Ohr deaktiviert'); this.setState('off'); } /** Konversation beenden — User hat im Window nichts gesagt. * Mit Porcupine: zurueck zu 'armed'. Ohne: zurueck zu 'off'. */ endConversation(): void { if (this.state !== 'conversing') return; if (this.wakeWordSupported) { console.log('[WakeWord] Konversation zu Ende — zurueck zu armed (warte auf Wake Word)'); this.setState('armed'); } else { console.log('[WakeWord] Konversation zu Ende — Ohr aus (kein Wake Word verfuegbar)'); this.setState('off'); } } /** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten */ async resume(): Promise { if (this.state !== 'conversing') return; // 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()); } } /** True solange das Ohr aktiv ist (armed ODER conversing). */ isActive(): boolean { return this.state !== 'off'; } /** True wenn gerade aktiv aufgenommen / mit ARIA gesprochen wird. */ isConversing(): boolean { return this.state === 'conversing'; } // --- Callbacks --- onWakeWord(callback: WakeWordCallback): () => void { this.wakeCallbacks.push(callback); return () => { this.wakeCallbacks = this.wakeCallbacks.filter(cb => cb !== callback); }; } onStateChange(callback: StateCallback): () => void { this.stateCallbacks.push(callback); return () => { this.stateCallbacks = this.stateCallbacks.filter(cb => cb !== callback); }; } getState(): WakeWordState { return this.state; } private setState(state: WakeWordState): void { if (this.state !== state) { this.state = state; this.stateCallbacks.forEach(cb => cb(state)); } } } const wakeWordService = new WakeWordService(); export default wakeWordService;