ComEleE5Small / textToSpeech /textToSpeech.js
plvictor's picture
Update textToSpeech/textToSpeech.js
a444030 verified
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();
});
}
*/