ARIA-AGENT/android/src/services/wakeword.ts

122 lines
4.1 KiB
TypeScript

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