+27
-17
@@ -198,6 +198,7 @@ export class VoiceLoop extends EventEmitter {
|
||||
async speak(text) {
|
||||
const generation = this._generation || 0;
|
||||
if (this.tts && !this.status.tts && typeof this.tts.start === 'function') await this.ensureTts();
|
||||
if (generation !== this._generation) return;
|
||||
if (!isSpeakable(text) || !this.tts?.speak || !this.status.tts) {
|
||||
this._releaseSpeaking();
|
||||
return;
|
||||
@@ -209,32 +210,41 @@ export class VoiceLoop extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
this._speechQueue = this._speechQueue.catch(() => {}).then(async () => {
|
||||
let pending;
|
||||
const prepare = (sentence) => Promise.resolve().then(async () => {
|
||||
if (generation !== this._generation) return {};
|
||||
const started = this.now();
|
||||
const audio = await this.tts.speak(speakableForTts(sentence));
|
||||
this.metrics.synthesis(this.now() - started, audio.samples.length * 1000 / audio.sampleRate);
|
||||
return { audio };
|
||||
}).catch((error) => ({ error }));
|
||||
try {
|
||||
if (generation !== this._generation) return;
|
||||
for (const sentence of sentences) {
|
||||
this.isSpeaking = true;
|
||||
this.wake.pause();
|
||||
pending = prepare(sentences[0]);
|
||||
for (let i = 0; i < sentences.length; i++) {
|
||||
const { audio, error } = await pending;
|
||||
if (generation !== this._generation) return;
|
||||
await this.speakSentence(sentence, generation);
|
||||
if (error) throw error;
|
||||
// Only one sentence ahead: overlap synthesis with playback, without
|
||||
// parallel TTS requests or buffering an entire reply's audio.
|
||||
pending = i + 1 < sentences.length ? prepare(sentences[i + 1]) : null;
|
||||
await this.playback.play(audio.samples, audio.sampleRate);
|
||||
try { this.daemon?.emit('SpeakingLevel', 0); } catch {}
|
||||
}
|
||||
} finally {
|
||||
if (generation === this._generation) this._releaseSpeaking();
|
||||
// A failed/interrupted playback may leave one synthesis in flight.
|
||||
// Drain it before model teardown or the next queued utterance.
|
||||
await pending;
|
||||
if (generation === this._generation) {
|
||||
this.cooldownUntil = this.now() + this.cooldownMs;
|
||||
this._releaseSpeaking();
|
||||
}
|
||||
}
|
||||
});
|
||||
return this._speechQueue;
|
||||
}
|
||||
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, audio.sampleRate);
|
||||
try { this.daemon?.emit('SpeakingLevel', 0); } catch {}
|
||||
} finally {
|
||||
this.isSpeaking = false;
|
||||
this.cooldownUntil = this.now() + this.cooldownMs;
|
||||
this.wake.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SentenceBuffer };
|
||||
|
||||
Reference in New Issue
Block a user