Spaces:
Runtime error
Runtime error
| const { spawn } = require("child_process"); | |
| const path = require("path"); | |
| const os = require("os"); | |
| const modelPath = path.resolve(__dirname, "pt_BR", "pt_BR-faber-medium.onnx"); | |
| const piperExecutable = path.resolve(__dirname, "./piper"); | |
| // Pool de processos para paralelizar | |
| const MAX_CONCURRENT = Math.min(os.cpus().length, 4); | |
| let activeProcesses = 0; | |
| const requestQueue = []; | |
| // Removido cache para simplificar | |
| function cleanTextForTTS(text) { | |
| if (!text || typeof text !== 'string') { | |
| return ''; | |
| } | |
| return text | |
| .replace(/\*\*(.*?)\*\*/g, '$1') | |
| .replace(/__(.*?)__/g, '$1') | |
| .replace(/\*(.*?)\*/g, '$1') | |
| .replace(/_([^_]+)_/g, '$1') | |
| .replace(/\[(.*?)\]\([^\)]*\)/g, '$1') | |
| .replace(/`([^`]+)`/g, '$1') | |
| .replace(/^#{1,6}\s*(.+)$/gm, '$1') | |
| .replace(/^[-*+]\s*/gm, '') | |
| .replace(/^\d+\.\s*/gm, '') | |
| .replace(/\n{2,}/g, '. ') | |
| .replace(/\n/g, '. ') | |
| .replace(/[\p{So}\p{Cn}]/gu, '') | |
| .replace(/\[\]/g, '') | |
| .replace(/\$\]/g, '') | |
| .replace(/[\x00-\x1F\x7F-\x9F]/g, '') | |
| .replace(/\s+/g, ' ') | |
| .trim(); | |
| } | |
| async function processQueue() { | |
| if (requestQueue.length === 0 || activeProcesses >= MAX_CONCURRENT) { | |
| return; | |
| } | |
| const { resolve, reject, cleanText, lengthScale } = requestQueue.shift(); | |
| activeProcesses++; | |
| try { | |
| const result = await _synthesizeAudioDirect(cleanText, lengthScale); | |
| resolve(result); | |
| } catch (error) { | |
| reject(error); | |
| } finally { | |
| activeProcesses--; | |
| setImmediate(processQueue); // Processa pr贸ximo na fila | |
| } | |
| } | |
| function _synthesizeAudioDirect(cleanText, lengthScale) { | |
| return new Promise((resolve, reject) => { | |
| const lengthScaleAdjusted = Math.min(Math.max(lengthScale, 0.8), 1.1); | |
| // Usar configura莽玫es otimizadas para velocidade | |
| const piper = spawn(piperExecutable, [ | |
| "--model", modelPath, | |
| "--output_file", "-", | |
| "--noise_scale", "0.2", // Reduzido para maior velocidade | |
| "--length_scale", lengthScaleAdjusted.toString(), | |
| "--noise_w", "0.3" // Reduzido para maior velocidade | |
| ], { | |
| stdio: ['pipe', 'pipe', 'pipe'] | |
| }); | |
| const chunks = []; | |
| piper.stdout.on("data", (chunk) => { | |
| chunks.push(chunk); | |
| }); | |
| piper.stderr.on("data", (data) => { | |
| console.error("Piper STDERR:", data.toString()); | |
| }); | |
| piper.on("close", (code) => { | |
| if (code !== 0) { | |
| return reject(new Error(`Piper saiu com c贸digo ${code}`)); | |
| } | |
| resolve(Buffer.concat(chunks)); | |
| }); | |
| piper.on("error", (err) => { | |
| reject(new Error("Erro ao iniciar o Piper: " + err.message)); | |
| }); | |
| piper.stdin.on('error', (err) => { | |
| if (err.code === 'EPIPE') { | |
| reject(new Error('Processo Piper foi terminado inesperadamente')); | |
| } | |
| }); | |
| try { | |
| piper.stdin.write(cleanText); | |
| piper.stdin.end(); | |
| } catch (err) { | |
| reject(new Error('Erro ao escrever no stdin: ' + err.message)); | |
| } | |
| }); | |
| } | |
| function synthesizeAudio(text, lengthScale = 1.1) { | |
| return new Promise((resolve, reject) => { | |
| const cleanText = cleanTextForTTS(text); | |
| if (!cleanText || cleanText.length === 0) { | |
| return reject(new Error('Texto vazio ap贸s limpeza')); | |
| } | |
| console.log(`TTS: Texto limpo: "${cleanText.substring(0, 50)}..."`); | |
| console.log(`TTS com length_scale: ${lengthScale}`); | |
| // Adicionar 脿 fila de processamento | |
| requestQueue.push({ resolve, reject, cleanText, lengthScale }); | |
| processQueue(); | |
| }); | |
| } | |
| // Streaming otimizado (sem fila, direto) | |
| function streamAudio(text, lengthScale = 1.0, res) { | |
| const cleanText = cleanTextForTTS(text); | |
| const lengthScaleAdjusted = Math.min(Math.max(lengthScale, 0.8), 1.1); | |
| const piper = spawn(piperExecutable, [ | |
| "--model", modelPath, | |
| "--output_raw", | |
| "--noise_scale", "0.2", | |
| "--length_scale", lengthScaleAdjusted.toString(), | |
| "--noise_w", "0.3" | |
| ]); | |
| res.set({ | |
| "Content-Type": "audio/L16; rate=22050; channels=1", | |
| "Transfer-Encoding": "chunked", | |
| "Cache-Control": "no-cache" | |
| }); | |
| res.on('close', () => { | |
| piper.kill('SIGTERM'); | |
| }); | |
| piper.stdout.pipe(res); | |
| piper.stderr.on("data", (data) => { | |
| console.error("Piper STDERR:", data.toString()); | |
| }); | |
| piper.on("close", (code) => { | |
| if (code !== 0) { | |
| console.error(`Piper saiu com c贸digo ${code}`); | |
| } | |
| res.end(); | |
| }); | |
| piper.on("error", (err) => { | |
| console.error("Erro ao iniciar o Piper:", err.message); | |
| res.end(); | |
| }); | |
| piper.stdin.on('error', (err) => { | |
| if (err.code === 'EPIPE') { | |
| console.error('馃挜 Erro cr铆tico: write EPIPE'); | |
| } | |
| }); | |
| try { | |
| piper.stdin.write(cleanText); | |
| piper.stdin.end(); | |
| } catch (err) { | |
| console.error('Erro ao escrever no stdin:', err.message); | |
| res.end(); | |
| } | |
| } | |
| module.exports = { synthesizeAudio, streamAudio }; | |
| /* | |
| ffmpeg pra "limpar" o audio! | |
| function synthesizeAudio(text, lengthScale = 1.1) { | |
| return new Promise((resolve, reject) => { | |
| // Limpar o texto antes de processar | |
| const cleanText = cleanTextForTTS(text); | |
| if (!cleanText || cleanText.length === 0) { | |
| return reject(new Error('Texto vazio ap贸s limpeza')); | |
| } | |
| console.log(`TTS: Texto original: "${text.substring(0, 50)}..."`); | |
| console.log(`TTS: Texto limpo: "${cleanText.substring(0, 50)}..."`); | |
| const env = { ...process.env }; | |
| env.ESPEAK_DATA_PATH = "/opt/homebrew/share/espeak-ng-data"; | |
| // Validar o length_scale | |
| const validLengthScale = Math.max(0.1, Math.min(10.0, parseFloat(lengthScale) || 1.1)); | |
| console.log(`TTS com length_scale: ${validLengthScale}`); | |
| const piper = spawn(piperExecutable, [ | |
| "--model", modelPath, | |
| "--output_file", "-", | |
| "--noise_scale", "0.8", | |
| "--length_scale", validLengthScale.toString(), | |
| "--noise_w", "0.6" | |
| ], { env }); | |
| // Spawn ffmpeg process for equalization | |
| const ffmpeg = spawn(ffmpegPath, [ | |
| "-f", "wav", // formato da entrada | |
| "-i", "pipe:0", // entrada do pipe (stdin) | |
| "-af", "equalizer=f=300:t=q:w=1:g=4", // filtro de 谩udio | |
| "-f", "wav", // formato da sa铆da | |
| "pipe:1" // sa铆da via stdout | |
| ]); | |
| // Encadeia a sa铆da do Piper para o FFMPEG | |
| piper.stdout.pipe(ffmpeg.stdin); | |
| let finalAudio = Buffer.alloc(0); | |
| ffmpeg.stdout.on("data", (chunk) => { | |
| finalAudio = Buffer.concat([finalAudio, chunk]); | |
| }); | |
| ffmpeg.stderr.on("data", (data) => { | |
| console.error("FFmpeg STDERR:", data.toString()); | |
| }); | |
| ffmpeg.on("close", (code) => { | |
| if (code !== 0) { | |
| return reject(new Error(`FFmpeg saiu com c贸digo ${code}`)); | |
| } | |
| resolve(finalAudio); | |
| }); | |
| ffmpeg.on("error", (err) => { | |
| reject(new Error("Erro ao iniciar o FFmpeg: " + err.message)); | |
| }); | |
| piper.stdin.write(cleanText); | |
| piper.stdin.end(); | |
| }); | |
| } | |
| */ |