// TTS worker:独立进程加载 sherpa-onnx-node,避免与主进程的 onnxruntime-node 发生 DLL 冲突。 // 以 ELECTRON_RUN_AS_NODE=1 启动,不能依赖 electron.app。 // // IPC 协议(JSON 消息): // -> { cmd: 'init', modelDir, dataDir, numThreads } 初始化模型(约 4 秒,之后常驻) // <- { type: 'ready', sampleRate } | { type: 'error', error } // -> { cmd: 'synth', dir, segments: string[], speed, gain } 逐段合成到 dir/seg-i.pcm // <- { type: 'segment', index } 每段完成(失败段写空文件保证顺序推进) // <- { type: 'synthDone' } | { type: 'error', error } // -> { cmd: 'cancel' } 停止后续分段合成(当前段做完为止,模型保持加载) // -> { cmd: 'quit' } const fs = require('fs') const path = require('path') let tts = null let cancelled = false function send(msg) { if (process.send) { process.send(msg) } } async function handleInit(msg) { const sherpa = require('sherpa-onnx-node') tts = new sherpa.OfflineTts({ model: { vits: { model: path.join(msg.modelDir, 'zh_CN-xiao_ya-medium.onnx'), lexicon: path.join(msg.modelDir, 'lexicon.txt'), tokens: path.join(msg.modelDir, 'tokens.txt'), // sherpa-onnx 校验要求 dataDir 指向 espeak-ng-data(由主进程指定) dataDir: msg.dataDir }, numThreads: msg.numThreads || 2, debug: false, provider: 'cpu' }, maxNumSentences: 1 }) send({ type: 'ready', sampleRate: tts.sampleRate }) } async function handleSynth(msg) { if (!tts) { send({ type: 'error', error: 'not initialized' }) return } cancelled = false const speed = typeof msg.speed === 'number' ? msg.speed : 1 const gain = typeof msg.gain === 'number' ? msg.gain : 1 for (let i = 0; i < msg.segments.length; i++) { if (cancelled) break const file = path.join(msg.dir, 'seg-' + i + '.pcm') try { // Electron(V8 沙箱)下必须以拷贝方式返回音频,禁用外部缓冲 const audio = await tts.generateAsync({ text: msg.segments[i], sid: 0, speed, enableExternalBuffer: false }) if (cancelled) break const samples = audio.samples const pcm = Buffer.alloc(samples.length * 2) for (let j = 0; j < samples.length; j++) { const s = Math.max(-1, Math.min(1, samples[j] * gain)) pcm.writeInt16LE(Math.round(s * 32767), j * 2) } fs.writeFileSync(file, pcm) } catch (e) { if (cancelled && e.code === 'ENOENT') break // 失败段写空文件,保证主进程喂流顺序不被卡住 console.error('[tts-worker] synth segment failed:', e) try { fs.writeFileSync(file, Buffer.alloc(0)) } catch { // ignore } } send({ type: 'segment', index: i }) if (cancelled) break } send({ type: 'synthDone' }) } process.on('message', (msg) => { if (!msg || typeof msg !== 'object') return if (msg.cmd === 'init') { handleInit(msg).catch((e) => send({ type: 'error', error: String(e) })) } else if (msg.cmd === 'synth') { handleSynth(msg).catch((e) => send({ type: 'error', error: String(e) })) } else if (msg.cmd === 'cancel') { cancelled = true } else if (msg.cmd === 'quit') { process.exit(0) } }) process.on('disconnect', () => { process.exit(0) })