From b744b71619d2ab0b7b5b6f2dacbbbc09932e29fb Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Wed, 3 Jun 2026 10:52:52 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20Camila=20atualiza?= =?UTF-8?q?do=20em=2003/06/2026=2010:52:52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/app.js | 186 +++++++++++++++++++++++++++++++++++++++++--------- server.js | 28 ++++++++ 2 files changed, 180 insertions(+), 34 deletions(-) diff --git a/public/app.js b/public/app.js index 6990081..8e69fa6 100644 --- a/public/app.js +++ b/public/app.js @@ -814,23 +814,79 @@ const initApp = () => { } }; - // Falar texto com TTS nativa - const speakText = (text, btn) => { + let currentSpeakingBtn = null; + 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; - // Se clicar no botão que já está ativamente falando, parar a reprodução - if (window.speechSynthesis.speaking && currentSpeakingBtn === btn) { - window.speechSynthesis.cancel(); - resetSpeakBtnState(btn); - currentSpeakingBtn = null; + // Se clicar no botão que já está ativo, para tudo e retorna + if (currentSpeakingBtn === btn) { + stopAllSpeech(); return; } - // Se houver outra fala em andamento ou fila travada, limpa tudo e reseta o botão antigo - window.speechSynthesis.cancel(); - if (currentSpeakingBtn) { - resetSpeakBtnState(currentSpeakingBtn); - } + // Para qualquer reprodução anterior + stopAllSpeech(); currentSpeakingBtn = btn; @@ -849,34 +905,96 @@ const initApp = () => { return; } - const utterance = new SpeechSynthesisUtterance(clean); - utterance.lang = 'pt-BR'; - utterance.rate = 1.15; - utterance.pitch = 1.0; + // Coloca o botão em estado de "carregando" + if (btn) { + btn.classList.add('speaking'); + const span = btn.querySelector('span'); + if (span) span.textContent = 'Carregando...'; + } - const voice = getBestVoice(); - if (voice) utterance.voice = voice; + let fallbackTriggered = false; - utterance.onstart = () => { - if (btn) { - 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); + // Configura o timeout de 15 segundos para o fallback nativo + ttsFallbackTimeout = setTimeout(() => { if (currentSpeakingBtn === btn) { - resetSpeakBtnState(btn); - currentSpeakingBtn = null; + fallbackTriggered = true; + if (currentAudio) { + currentAudio.pause(); + currentAudio = null; + } + speakTextLocalFallback(clean, btn); } - }; + }, 15000); - utterance.onend = handleStop; - utterance.onerror = handleStop; + try { + 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); + } + } }; // ========================================================================== diff --git a/server.js b/server.js index 778d0b7..f663d0d 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ const express = require('express'); const cookieParser = require('cookie-parser'); const path = require('path'); const multer = require('multer'); +const { MsEdgeTTS, OUTPUT_FORMAT } = require('msedge-tts'); const app = express(); 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 app.get('/api/agent-config', requireAuth, (req, res) => { const config = readAgentConfig();