+13
-7
@@ -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'); }
|
||||
|
||||
Reference in New Issue
Block a user