+32
-13
@@ -3,7 +3,7 @@ import { PipeWireCapture } from './audio-pipewire.js';
|
||||
import { PipeWirePlayback } from './audio-playback.js';
|
||||
import { WakeEngine } from './wake-engine.js';
|
||||
import { VadSegmenter } from './vad.js';
|
||||
import { isMeaningfulTranscript, isSpeakable, SentenceBuffer } from './transcript.js';
|
||||
import { isMeaningfulTranscript, isSpeakable, speakableForTts, SentenceBuffer } from './transcript.js';
|
||||
import { VoiceMetrics } from './voice-metrics.js';
|
||||
|
||||
export const POST_PLAYBACK_COOLDOWN_MS = 400;
|
||||
@@ -91,29 +91,48 @@ export class VoiceLoop extends EventEmitter {
|
||||
_releaseSpeaking() {
|
||||
this.isSpeaking = false;
|
||||
this.wake.resume();
|
||||
if (this.daemon?.state === 'SPEAKING') { this.daemon.voice?.finishSpeaking?.(); this.daemon.setState?.('LISTENING'); }
|
||||
if (this.daemon?.state === 'SPEAKING') {
|
||||
try { this.daemon.voice?.finishSpeaking?.(); } catch {}
|
||||
this.daemon.setState?.('LISTENING');
|
||||
}
|
||||
}
|
||||
async speak(text) {
|
||||
const generation = this._generation || 0;
|
||||
if (!isSpeakable(text) || !this.tts?.speak || !this.status.tts) { this._releaseSpeaking(); return; }
|
||||
if (!isSpeakable(text) || !this.tts?.speak || !this.status.tts) {
|
||||
this._releaseSpeaking();
|
||||
return;
|
||||
}
|
||||
this.metrics.reply();
|
||||
const sentences = String(text).match(/[^.!?]+[.!?]+|[^.!?]+$/g)?.map((part) => part.trim()).filter(isSpeakable) || [];
|
||||
if (!sentences.length) { this._releaseSpeaking(); return; }
|
||||
if (!sentences.length) {
|
||||
this._releaseSpeaking();
|
||||
return;
|
||||
}
|
||||
this._speechQueue = this._speechQueue.catch(() => {}).then(async () => {
|
||||
if (generation !== this._generation) return;
|
||||
for (const sentence of sentences) {
|
||||
try {
|
||||
if (generation !== this._generation) return;
|
||||
await this.speakSentence(sentence);
|
||||
for (const sentence of sentences) {
|
||||
if (generation !== this._generation) return;
|
||||
await this.speakSentence(sentence, generation);
|
||||
}
|
||||
} finally {
|
||||
if (generation === this._generation) this._releaseSpeaking();
|
||||
}
|
||||
});
|
||||
return this._speechQueue;
|
||||
}
|
||||
async speakSentence(text) {
|
||||
this.isSpeaking = true; this.wake.pause(); this.daemon?.emit('StateChanged', 'SPEAKING');
|
||||
try { const audio = await this.tts.speak(text); await this.playback.play(audio.samples); try { this.daemon?.emit('SpeakingLevel', 0); } catch {} }
|
||||
finally {
|
||||
this.isSpeaking = false; this.cooldownUntil = this.now() + this.cooldownMs; this.wake.resume();
|
||||
if (this.daemon?.state === 'SPEAKING') { this.daemon.voice?.finishSpeaking?.(); this.daemon.setState?.('LISTENING'); }
|
||||
async speakSentence(text, generation) {
|
||||
this.isSpeaking = true;
|
||||
this.wake.pause();
|
||||
try {
|
||||
const audio = await this.tts.speak(speakableForTts(text));
|
||||
if (generation !== this._generation) return;
|
||||
await this.playback.play(audio.samples);
|
||||
try { this.daemon?.emit('SpeakingLevel', 0); } catch {}
|
||||
} finally {
|
||||
this.isSpeaking = false;
|
||||
this.cooldownUntil = this.now() + this.cooldownMs;
|
||||
this.wake.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user