+11
-12
@@ -2,6 +2,7 @@ import { EventEmitter } from 'node:events';
|
||||
import { acquireQvac, releaseQvac, loadAuxiliaryModel, unloadAuxiliaryModel, qvacSdk, withQvacMaster, assertSdkVersion } from './qvac-master.js';
|
||||
|
||||
export function pcmS16le(samples, sampleRate = 44_100) {
|
||||
if (Array.isArray(samples)) return { samples: Int16Array.from(samples), sampleRate };
|
||||
if (samples instanceof Int16Array) return { samples, sampleRate };
|
||||
const bytes = toUint8(samples);
|
||||
const even = bytes.byteLength - (bytes.byteLength % 2);
|
||||
@@ -19,35 +20,33 @@ function toUint8(samples) {
|
||||
}
|
||||
|
||||
export class QvacVoiceAdapter extends EventEmitter {
|
||||
constructor({ asrModel = process.env.JARVIS_ASR_MODEL || 'WHISPER_TINY', ttsModel = process.env.JARVIS_TTS_MODEL || 'TTS_EN_SUPERTONIC_Q8_0' } = {}) {
|
||||
super(); this.asrModel = asrModel; this.ttsModel = ttsModel; this.asrId = null; this.ttsId = null; this.asrSession = null; this.acquired = false;
|
||||
constructor({ role = 'both', asrModel = process.env.JARVIS_ASR_MODEL || 'WHISPER_TINY', ttsModel = process.env.JARVIS_TTS_MODEL || 'TTS_EN_SUPERTONIC_Q8_0' } = {}) {
|
||||
super(); this.role = role; this.asrModel = asrModel; this.ttsModel = ttsModel; this.asrId = null; this.ttsId = null; this.asrSession = null; this.acquired = false;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.acquired) return;
|
||||
assertSdkVersion();
|
||||
await acquireQvac(); this.acquired = true;
|
||||
await acquireQvac({ auxiliaryOnly: true }); this.acquired = true;
|
||||
try {
|
||||
this.asrId = await loadAuxiliaryModel(this.asrModel, { vadModelSrc: 'VAD_SILERO_5_1_2', audio_format: 's16le', language: 'en', no_timestamps: true, vad_params: { threshold: 0.6, min_speech_duration_ms: 300, min_silence_duration_ms: 700, max_speech_duration_s: 15, speech_pad_ms: 200 } }, 'whisper');
|
||||
this.ttsId = await loadAuxiliaryModel(this.ttsModel, { ttsEngine: 'supertonic', language: 'en', voice: 'F1', ttsSpeed: 1.05, ttsNumInferenceSteps: 5 }, 'tts');
|
||||
if (this.role !== 'tts') this.asrId = await loadAuxiliaryModel(this.asrModel, { vadModelSrc: 'VAD_SILERO_5_1_2', audio_format: 's16le', language: 'en', no_timestamps: true, vad_params: { threshold: 0.6, min_speech_duration_ms: 300, min_silence_duration_ms: 700, max_speech_duration_s: 15, speech_pad_ms: 200 } }, 'whisper');
|
||||
if (this.role !== 'asr') this.ttsId = await loadAuxiliaryModel(this.ttsModel, { ttsEngine: 'supertonic', language: 'en', voice: 'F1', ttsSpeed: 1.05, ttsNumInferenceSteps: 5 }, 'tts');
|
||||
} catch (error) { await this.stop(); throw error; }
|
||||
}
|
||||
|
||||
writeAudio(chunk) { this.asrSession?.write(Buffer.from(chunk)); }
|
||||
async transcribeAudio(audio) {
|
||||
if (!this.asrId) throw new Error('Speech recognition is unavailable');
|
||||
const sdk = await qvacSdk();
|
||||
return withQvacMaster(async () => {
|
||||
const session = await sdk.transcribeStream({ modelId: this.asrId, emitVadEvents: true });
|
||||
session.write(Buffer.from(audio)); session.end();
|
||||
const parts = [];
|
||||
for await (const event of session) parts.push(typeof event === 'string' ? event : event?.text || '');
|
||||
return parts.join(' ').trim();
|
||||
});
|
||||
// Local VAD has already bounded this utterance. Avoid a second streaming
|
||||
// VAD gate, which can discard a complete short push-to-talk recording.
|
||||
return withQvacMaster(() => sdk.transcribe({ modelId: this.asrId, audioChunk: Buffer.from(audio) }));
|
||||
}
|
||||
async *transcripts() { if (!this.asrSession) throw new Error('voice adapter is not started'); yield* this.asrSession; }
|
||||
endAudio() { this.asrSession?.end(); }
|
||||
|
||||
async speak(text) {
|
||||
if (!this.ttsId) throw new Error('Speech output is unavailable');
|
||||
const sdk = await qvacSdk();
|
||||
const samples = await withQvacMaster(async () => {
|
||||
const result = await sdk.textToSpeech({ modelId: this.ttsId, text: String(text), inputType: 'text', stream: false });
|
||||
|
||||
Reference in New Issue
Block a user