+21
-4
@@ -22,7 +22,7 @@ export function fastCommand(text) { return FAST_COMMANDS.get(String(text || '').
|
||||
export class VoiceLoop extends EventEmitter {
|
||||
constructor({ daemon, capture = new PipeWireCapture(), playback = new PipeWirePlayback(), wake = new WakeEngine(), vad = new VadSegmenter(), asr, tts, cooldownMs = POST_PLAYBACK_COOLDOWN_MS, now = () => Date.now() } = {}) {
|
||||
super(); this.daemon = daemon; this.capture = capture; this.playback = playback; this.wake = wake; this.vad = vad; this.asr = asr; this.tts = tts; this.cooldownMs = cooldownMs; this.now = now;
|
||||
this.isSpeaking = false; this.cooldownUntil = 0; this.running = false; this.ptt = false; this._speechQueue = Promise.resolve(); this.metrics = new VoiceMetrics();
|
||||
this.isSpeaking = false; this.cooldownUntil = 0; this.running = false; this.ptt = false; this._generation = 0; this._speechQueue = Promise.resolve(); this.metrics = new VoiceMetrics();
|
||||
capture.on('audio', (chunk) => this.pushAudio(chunk));
|
||||
capture.on('error', (error) => this.emit('error', error));
|
||||
wake.on('wake', (phrase) => this.wakeHeard(phrase));
|
||||
@@ -58,12 +58,29 @@ export class VoiceLoop extends EventEmitter {
|
||||
if (command === 'screen') { await this.daemon?.ask?.('What is on my screen?'); return; }
|
||||
await this.daemon?.ask?.(text);
|
||||
}
|
||||
interrupt() {
|
||||
this._generation += 1;
|
||||
this.playback.stop();
|
||||
this.isSpeaking = false;
|
||||
this.wake.resume();
|
||||
}
|
||||
_releaseSpeaking() {
|
||||
this.isSpeaking = false;
|
||||
this.wake.resume();
|
||||
if (this.daemon?.state === 'SPEAKING') { this.daemon.voice?.finishSpeaking?.(); this.daemon.setState?.('LISTENING'); }
|
||||
}
|
||||
async speak(text) {
|
||||
if (!isMeaningfulTranscript(text) || !this.tts?.speak) return;
|
||||
const generation = this._generation || 0;
|
||||
if (!isMeaningfulTranscript(text) || !this.tts?.speak) { this._releaseSpeaking(); return; }
|
||||
this.metrics.reply();
|
||||
const sentences = String(text).match(/[^.!?]+[.!?]+|[^.!?]+$/g)?.map((part) => part.trim()).filter(isMeaningfulTranscript) || [];
|
||||
this._speechQueue = this._speechQueue.then(async () => {
|
||||
for (const sentence of sentences) await this.speakSentence(sentence);
|
||||
if (!sentences.length) { this._releaseSpeaking(); return; }
|
||||
this._speechQueue = this._speechQueue.catch(() => {}).then(async () => {
|
||||
if (generation !== this._generation) return;
|
||||
for (const sentence of sentences) {
|
||||
if (generation !== this._generation) return;
|
||||
await this.speakSentence(sentence);
|
||||
}
|
||||
});
|
||||
return this._speechQueue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user