🚀 Auto-deploy: Camila atualizado em 03/06/2026 10:52:52
This commit is contained in:
+152
-34
@@ -814,23 +814,79 @@ const initApp = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Falar texto com TTS nativa
|
let currentSpeakingBtn = null;
|
||||||
const speakText = (text, btn) => {
|
let currentAudio = null;
|
||||||
|
let ttsFallbackTimeout = null;
|
||||||
|
|
||||||
|
// Função auxiliar para parar qualquer áudio em andamento (tanto nativo quanto do backend)
|
||||||
|
const stopAllSpeech = () => {
|
||||||
|
// Parar áudio do backend
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
}
|
||||||
|
// Parar SpeechSynthesis nativo
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
|
||||||
|
// Limpar timeouts pendentes
|
||||||
|
if (ttsFallbackTimeout) {
|
||||||
|
clearTimeout(ttsFallbackTimeout);
|
||||||
|
ttsFallbackTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resetar estado do botão ativo anterior
|
||||||
|
if (currentSpeakingBtn) {
|
||||||
|
resetSpeakBtnState(currentSpeakingBtn);
|
||||||
|
currentSpeakingBtn = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const speakTextLocalFallback = (cleanText, btn) => {
|
||||||
|
console.log('[TTS] Acionando fallback local (voz nativa do navegador)...');
|
||||||
|
|
||||||
|
// Configura SpeechSynthesis nativo
|
||||||
|
const utterance = new SpeechSynthesisUtterance(cleanText);
|
||||||
|
utterance.lang = 'pt-BR';
|
||||||
|
utterance.rate = 1.15;
|
||||||
|
utterance.pitch = 1.0;
|
||||||
|
|
||||||
|
const voice = getBestVoice();
|
||||||
|
if (voice) utterance.voice = voice;
|
||||||
|
|
||||||
|
utterance.onstart = () => {
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.add('speaking');
|
||||||
|
const span = btn.querySelector('span');
|
||||||
|
if (span) span.textContent = 'Parar';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = (event) => {
|
||||||
|
console.log('TTS nativo finalizado ou com erro:', event.type);
|
||||||
|
if (currentSpeakingBtn === btn) {
|
||||||
|
resetSpeakBtnState(btn);
|
||||||
|
currentSpeakingBtn = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
utterance.onend = handleStop;
|
||||||
|
utterance.onerror = handleStop;
|
||||||
|
|
||||||
|
window.speechSynthesis.speak(utterance);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Falar texto com reprodução híbrida (prioriza Opção A: MsEdgeTTS com fallback local)
|
||||||
|
const speakText = async (text, btn) => {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
// Se clicar no botão que já está ativamente falando, parar a reprodução
|
// Se clicar no botão que já está ativo, para tudo e retorna
|
||||||
if (window.speechSynthesis.speaking && currentSpeakingBtn === btn) {
|
if (currentSpeakingBtn === btn) {
|
||||||
window.speechSynthesis.cancel();
|
stopAllSpeech();
|
||||||
resetSpeakBtnState(btn);
|
|
||||||
currentSpeakingBtn = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se houver outra fala em andamento ou fila travada, limpa tudo e reseta o botão antigo
|
// Para qualquer reprodução anterior
|
||||||
window.speechSynthesis.cancel();
|
stopAllSpeech();
|
||||||
if (currentSpeakingBtn) {
|
|
||||||
resetSpeakBtnState(currentSpeakingBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSpeakingBtn = btn;
|
currentSpeakingBtn = btn;
|
||||||
|
|
||||||
@@ -849,34 +905,96 @@ const initApp = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const utterance = new SpeechSynthesisUtterance(clean);
|
// Coloca o botão em estado de "carregando"
|
||||||
utterance.lang = 'pt-BR';
|
if (btn) {
|
||||||
utterance.rate = 1.15;
|
btn.classList.add('speaking');
|
||||||
utterance.pitch = 1.0;
|
const span = btn.querySelector('span');
|
||||||
|
if (span) span.textContent = 'Carregando...';
|
||||||
|
}
|
||||||
|
|
||||||
const voice = getBestVoice();
|
let fallbackTriggered = false;
|
||||||
if (voice) utterance.voice = voice;
|
|
||||||
|
|
||||||
utterance.onstart = () => {
|
// Configura o timeout de 15 segundos para o fallback nativo
|
||||||
if (btn) {
|
ttsFallbackTimeout = setTimeout(() => {
|
||||||
btn.classList.add('speaking');
|
|
||||||
const span = btn.querySelector('span');
|
|
||||||
if (span) span.textContent = 'Parar';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStop = (event) => {
|
|
||||||
console.log('TTS finalizado ou com erro:', event.type);
|
|
||||||
if (currentSpeakingBtn === btn) {
|
if (currentSpeakingBtn === btn) {
|
||||||
resetSpeakBtnState(btn);
|
fallbackTriggered = true;
|
||||||
currentSpeakingBtn = null;
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
}
|
||||||
|
speakTextLocalFallback(clean, btn);
|
||||||
}
|
}
|
||||||
};
|
}, 15000);
|
||||||
|
|
||||||
utterance.onend = handleStop;
|
try {
|
||||||
utterance.onerror = handleStop;
|
console.log('[TTS] Solicitando voz neural de alta qualidade ao backend...');
|
||||||
|
const response = await fetch('/api/tts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: clean })
|
||||||
|
});
|
||||||
|
|
||||||
window.speechSynthesis.speak(utterance);
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erro no servidor: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Se o fallback já foi acionado durante a espera, ignora o retorno do áudio do backend
|
||||||
|
if (fallbackTriggered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpar o timeout de fallback já que o áudio respondeu a tempo
|
||||||
|
if (ttsFallbackTimeout) {
|
||||||
|
clearTimeout(ttsFallbackTimeout);
|
||||||
|
ttsFallbackTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioUrl = URL.createObjectURL(blob);
|
||||||
|
currentAudio = new Audio(audioUrl);
|
||||||
|
|
||||||
|
currentAudio.onplay = () => {
|
||||||
|
if (currentSpeakingBtn === btn) {
|
||||||
|
const span = btn.querySelector('span');
|
||||||
|
if (span) span.textContent = 'Parar';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAudioStop = () => {
|
||||||
|
if (currentSpeakingBtn === btn) {
|
||||||
|
resetSpeakBtnState(btn);
|
||||||
|
currentSpeakingBtn = null;
|
||||||
|
currentAudio = null;
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(audioUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
currentAudio.onended = handleAudioStop;
|
||||||
|
currentAudio.onerror = (e) => {
|
||||||
|
console.error('[TTS] Erro ao reproduzir áudio do backend:', e);
|
||||||
|
if (currentSpeakingBtn === btn && !fallbackTriggered) {
|
||||||
|
fallbackTriggered = true;
|
||||||
|
speakTextLocalFallback(clean, btn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await currentAudio.play();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[TTS] Falha ao obter áudio do backend, acionando fallback local imediatamente:', err);
|
||||||
|
// Limpa timeout e ativa fallback local imediatamente se der erro de rede
|
||||||
|
if (ttsFallbackTimeout) {
|
||||||
|
clearTimeout(ttsFallbackTimeout);
|
||||||
|
ttsFallbackTimeout = null;
|
||||||
|
}
|
||||||
|
if (currentSpeakingBtn === btn && !fallbackTriggered) {
|
||||||
|
fallbackTriggered = true;
|
||||||
|
speakTextLocalFallback(clean, btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const express = require('express');
|
|||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
const { MsEdgeTTS, OUTPUT_FORMAT } = require('msedge-tts');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@@ -182,6 +183,33 @@ async function getKnowledgeAsText() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rota POST para sintetizar fala com MsEdgeTTS e retornar áudio
|
||||||
|
app.post('/api/tts', requireAuth, async (req, res) => {
|
||||||
|
const { text } = req.body;
|
||||||
|
if (!text) return res.status(400).json({ error: 'Texto é obrigatório' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tts = new MsEdgeTTS();
|
||||||
|
await tts.setMetadata("pt-BR-FranciscaNeural", OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
||||||
|
const { audioStream } = tts.toStream(text);
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'audio/mpeg');
|
||||||
|
audioStream.pipe(res);
|
||||||
|
|
||||||
|
audioStream.on('error', (err) => {
|
||||||
|
console.error('Erro na stream do MsEdgeTTS:', err);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: 'Erro ao processar áudio' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar TTS:', error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: error.message || 'Erro ao gerar áudio' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Rota GET para obter configurações do agente
|
// Rota GET para obter configurações do agente
|
||||||
app.get('/api/agent-config', requireAuth, (req, res) => {
|
app.get('/api/agent-config', requireAuth, (req, res) => {
|
||||||
const config = readAgentConfig();
|
const config = readAgentConfig();
|
||||||
|
|||||||
Reference in New Issue
Block a user