Files
gnome-jarvis/daemon/voice-adapters.js
T
2026-09-11 14:09:04 -04:00

57 lines
2.9 KiB
JavaScript

import { createRequire } from 'node:module';
import { EventEmitter } from 'node:events';
import path from 'node:path';
import { acquireQvac, releaseQvac, loadAuxiliaryModel, unloadAuxiliaryModel, qvacSdk, withQvacMaster } from './qvac-master.js';
const require = createRequire(import.meta.url);
const harnessPath = process.env.JARVIS_HARNESS_PATH || path.resolve(new URL('../vendor/agent-harness', import.meta.url).pathname);
const sdkPackage = require(path.join(harnessPath, 'node_modules/@qvac/sdk/package.json'));
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;
}
async start() {
if (this.acquired) return;
if (!/^0\.19\./.test(sdkPackage.version)) throw new Error(`Jarvis voice adapter requires QVAC 0.19.x; found ${sdkPackage.version}`);
await acquireQvac(); 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 } });
this.ttsId = await loadAuxiliaryModel(this.ttsModel, { ttsEngine: 'supertonic', language: 'en', voice: 'F1', ttsSpeed: 1.05, ttsNumInferenceSteps: 5 });
} catch (error) { await this.stop(); throw error; }
}
writeAudio(chunk) { this.asrSession?.write(Buffer.from(chunk)); }
async transcribeAudio(audio) {
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();
});
}
async *transcripts() { if (!this.asrSession) throw new Error('voice adapter is not started'); yield* this.asrSession; }
endAudio() { this.asrSession?.end(); }
async speak(text) {
const sdk = await qvacSdk();
const samples = await withQvacMaster(async () => {
const result = sdk.textToSpeech({ modelId: this.ttsId, text: String(text), inputType: 'text', stream: false });
return result.buffer;
});
return { samples: Int16Array.from(samples), sampleRate: 44_100 };
}
async stop() {
try { this.asrSession?.destroy?.(); } catch {}
this.asrSession = null;
if (this.ttsId) await unloadAuxiliaryModel(this.ttsId).catch(() => {});
if (this.asrId) await unloadAuxiliaryModel(this.asrId).catch(() => {});
this.ttsId = this.asrId = null;
if (this.acquired) { this.acquired = false; releaseQvac(); }
}
}