import { WebSocket } from 'ws'; const VIBEVOICE_HOST = process.env.VIBEVOICE_HOST || 'localhost:3001'; const VOICE = process.env.VIBEVOICE_VOICE || 'en-Emma_woman'; const MAX_BUSY_RETRIES = 30; const BUSY_RETRY_DELAY_MS = 1000; export async function synthesize(text: string): Promise { for (let attempt = 0; attempt <= MAX_BUSY_RETRIES; attempt++) { try { return await _synthesizeOnce(text); } catch (err: any) { if (err.message === 'VibeVoice: server busy' && attempt < MAX_BUSY_RETRIES) { await new Promise(r => setTimeout(r, BUSY_RETRY_DELAY_MS)); continue; } throw err; } } throw new Error('VibeVoice: server busy after max retries'); } function _synthesizeOnce(text: string): Promise { const params = new URLSearchParams({ text, voice: VOICE, cfg: '1.5' }); const wsUrl = `ws://${VIBEVOICE_HOST}/stream?${params}`; return new Promise((resolve, reject) => { const ws = new WebSocket(wsUrl); const chunks: Buffer[] = []; let done = false; const finish = (err?: Error) => { if (done) return; done = true; ws.terminate(); if (err) { reject(err); } else { resolve(Buffer.concat(chunks)); } }; ws.on('message', (data: Buffer, isBinary: boolean) => { if (isBinary) { chunks.push(Buffer.from(data)); } else { try { const msg = JSON.parse(data.toString()); if (msg.event === 'backend_stream_complete') { finish(); } else if (msg.event === 'backend_error') { finish(new Error(`VibeVoice: ${msg.data?.message || 'generation error'}`)); } else if (msg.event === 'backend_busy') { finish(new Error('VibeVoice: server busy')); } } catch {} } }); ws.on('error', (err) => finish(new Error(`VibeVoice WebSocket: ${err.message}`))); ws.on('close', () => { if (!done) { if (chunks.length > 0) finish(); else finish(new Error('VibeVoice: closed without audio')); } }); setTimeout(() => { if (!done) finish(new Error('VibeVoice: timeout (60s)')); }, 60000); }); }