import path from 'node:path'; import fs from 'node:fs/promises'; import {spawn} from 'node:child_process'; import {clean} from './seguridad.mjs'; function execute(cmd,args,timeoutMs) { return new Promise((resolve,reject)=>{ let ended=false; const p=spawn(cmd,args,{windowsHide:true,stdio:['ignore','pipe','pipe']}); let detail=''; p.stderr.on('data',c=>detail+=String(c).slice(0,1800)); p.stdout.on('data',c=>detail+=String(c).slice(0,500)); const timer=setTimeout(()=>{if(!ended){p.kill();reject(new Error(`Tiempo agotado (${cmd})`));}},timeoutMs); p.once('error',e=>{if(!ended){ended=true;clearTimeout(timer);reject(e);}}); p.once('close',code=>{if(!ended){ended=true;clearTimeout(timer);code===0?resolve():reject(new Error(`Error ${code} al ejecutar ${cmd}: ${detail.slice(-1500)}`));}}); }); } export async function sintetizarYReproducir({config,texto}) { if(!config.piper.enabled){console.log('[VOZ DESACTIVADA]',texto);return;} if(process.platform!=='win32') throw new Error('El reproductor SoundPlayer configurado requiere Windows. Usa --demo fuera de Windows.'); const python=path.resolve(config.piper.pythonPath); const dir=path.resolve(config.piper.voiceDir); const model=path.join(dir,config.piper.voice+'.onnx'); await fs.access(python);await fs.access(model); const output=path.resolve('data/audio',`respuesta_${Date.now()}.wav`); await fs.mkdir(path.dirname(output),{recursive:true}); const speech=clean(texto,config.behavior.maxSpeechChars); try { await execute(python,['-m','piper','--data-dir',dir,'-m',config.piper.voice,'-f',output,'--',speech],config.piper.timeoutMs); if(config.piper.playAudio){ // SoundPlayer.PlaySync reproduce WAV en el dispositivo de audio predeterminado de Windows. const escaped=output.replace(/'/g,"''"); await execute('powershell.exe',['-NoProfile','-NonInteractive','-Command',`$player = New-Object System.Media.SoundPlayer '${escaped}'; $player.PlaySync(); $player.Dispose()`],config.piper.timeoutMs); } } finally {await fs.unlink(output).catch(()=>{});} }