Fix TTS
Rolling release / release (push) Successful in 14m2s

This commit is contained in:
2026-09-11 21:56:55 -04:00
parent 324f0d5e3a
commit e4546f8e95
11 changed files with 182 additions and 67 deletions
+13 -7
View File
@@ -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, SentenceBuffer } from './transcript.js';
import { isMeaningfulTranscript, isSpeakable, SentenceBuffer } from './transcript.js';
import { VoiceMetrics } from './voice-metrics.js';
export const POST_PLAYBACK_COOLDOWN_MS = 400;
@@ -37,8 +37,14 @@ export class VoiceLoop extends EventEmitter {
async start() {
if (this.running) return;
for (const [name, adapter] of [['tts', this.tts], ['asr', this.asr]]) {
try { if (adapter) { await adapter.start?.(); this.status[name] = true; } }
catch (error) { this.status.errors[name] = error.message; this.emit('error', new Error(`${name.toUpperCase()}: ${error.message}`)); }
try {
if (adapter) { await adapter.start?.(); this.status[name] = true; }
else if (name === 'tts') this.status.errors.tts = 'Spoken replies are disabled';
} catch (error) {
const message = String(error?.message || error);
this.status.errors[name] = message;
this.emit('error', new Error(`${name.toUpperCase()}: ${message}`));
}
}
this.running = true;
if (this.status.asr) {
@@ -53,7 +59,7 @@ export class VoiceLoop extends EventEmitter {
pushAudio(chunk) {
if (!this.running || this.daemon?.locked || this.daemon?.state === 'SLEEPING') return;
if (this.isSpeaking || this.now() < this.cooldownUntil) { this.metrics.feedbackDrop(); return; }
this.daemon?.emit('ListeningLevel', Math.min(1, Math.max(0, chunk.length ? 0.01 : 0)));
try { this.daemon?.emit('ListeningLevel', Math.min(1, Math.max(0, chunk?.length ? 0.01 : 0))); } catch {}
if (!this.ptt) this.wake.push(chunk);
if (this.ptt || this.daemon?.state === 'LISTENING') this.vad.push(chunk);
}
@@ -89,9 +95,9 @@ export class VoiceLoop extends EventEmitter {
}
async speak(text) {
const generation = this._generation || 0;
if (!isMeaningfulTranscript(text) || !this.tts?.speak) { 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(isMeaningfulTranscript) || [];
const sentences = String(text).match(/[^.!?]+[.!?]+|[^.!?]+$/g)?.map((part) => part.trim()).filter(isSpeakable) || [];
if (!sentences.length) { this._releaseSpeaking(); return; }
this._speechQueue = this._speechQueue.catch(() => {}).then(async () => {
if (generation !== this._generation) return;
@@ -104,7 +110,7 @@ export class VoiceLoop extends EventEmitter {
}
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); this.daemon?.emit('SpeakingLevel', 0); }
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'); }