122 lines
4.1 KiB
TypeScript
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;
|