From ea3487b417e06b410664142568e682c1f152b694 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Tue, 26 May 2026 14:20:35 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20Camila=20atualiza?= =?UTF-8?q?do=20em=2026/05/2026=2014:20:35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/app.js | 165 ++++++++++++++++++++++++++------------------------ server.js | 60 ------------------ 2 files changed, 86 insertions(+), 139 deletions(-) diff --git a/public/app.js b/public/app.js index 47e881d..3f23270 100644 --- a/public/app.js +++ b/public/app.js @@ -218,96 +218,103 @@ const initApp = () => { }); // ========================================================================== - // TTS — Voz Neural Thalita via servidor (Microsoft Edge TTS) + // TTS — Speech Synthesis nativa (voz do navegador) // ========================================================================== - let currentAudio = null; // controlar áudio em andamento - let currentSpeakBtn = null; + let cachedVoices = []; - // Limpar markdown e preparar texto para fala - const cleanTextForSpeech = (text) => { - return text - .replace(/```[\s\S]*?```/g, '') // remover blocos de código - .replace(/`[^`]+`/g, '') // remover inline code - .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links → só texto - .replace(/[*_#~\[\]>]/g, '') // remover formatação - .replace(/\n+/g, '. ') // quebras → pausas - .replace(/\s{2,}/g, ' ') - .trim(); + const loadVoices = () => { + const voices = window.speechSynthesis.getVoices(); + if (voices.length > 0) { + cachedVoices = voices; + console.log('Vozes pt-BR disponíveis:', cachedVoices.filter(v => v.lang.startsWith('pt')).map(v => v.name)); + } else { + window.speechSynthesis.addEventListener('voiceschanged', () => { + cachedVoices = window.speechSynthesis.getVoices(); + console.log('Vozes pt-BR disponíveis:', cachedVoices.filter(v => v.lang.startsWith('pt')).map(v => v.name)); + }, { once: true }); + } + }; + loadVoices(); + + // Selecionar melhor voz pt-BR disponível no dispositivo + const getBestVoice = () => { + const ptBR = cachedVoices.filter(v => v.lang === 'pt-BR' || v.lang === 'pt_BR'); + if (ptBR.length === 0) { + const pt = cachedVoices.filter(v => v.lang.startsWith('pt')); + return pt[0] || null; + } + + // 1. Microsoft Neural (Edge): Thalita, Francisca, Giovanna + const msNeural = ptBR.find(v => /thalita|francisca|giovanna|natural|neural/i.test(v.name)); + if (msNeural) return msNeural; + + // 2. Google (Chrome/Android): vozes naturais + const google = ptBR.find(v => /google/i.test(v.name)); + if (google) return google; + + // 3. Apple (Safari/iOS): Luciana é excelente + const apple = ptBR.find(v => /luciana|fernanda/i.test(v.name)); + if (apple) return apple; + + // 4. Qualquer feminina + const female = ptBR.find(v => /female|femin|maria|raquel|camila/i.test(v.name)); + if (female) return female; + + // 5. Primeira disponível + return ptBR[0]; }; - // Falar texto usando TTS neural do servidor - const speakText = async (text, btn) => { - // Se já está reproduzindo, parar - if (currentAudio && !currentAudio.paused) { - currentAudio.pause(); - currentAudio.currentTime = 0; - currentAudio = null; - if (currentSpeakBtn) { - currentSpeakBtn.classList.remove('speaking'); - const span = currentSpeakBtn.querySelector('span'); - if (span) span.textContent = 'Ouvir'; - } - currentSpeakBtn = null; - return; - } - - const clean = cleanTextForSpeech(text); - if (!clean) return; - - // Feedback visual imediato - if (btn) { - btn.classList.add('speaking'); - const span = btn.querySelector('span'); - if (span) span.textContent = 'Carregando...'; - } - currentSpeakBtn = btn; - - try { - const response = await fetch('/api/tts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: clean }) - }); - - if (!response.ok) throw new Error('Erro no TTS'); - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - - currentAudio = new Audio(url); - currentAudio.playbackRate = 1.05; // levemente mais rápida - - currentAudio.onplay = () => { - if (btn) { - btn.classList.add('speaking'); - const span = btn.querySelector('span'); - if (span) span.textContent = 'Parar'; - } - }; - - currentAudio.onended = currentAudio.onerror = () => { - if (btn) { - btn.classList.remove('speaking'); - const span = btn.querySelector('span'); - if (span) span.textContent = 'Ouvir'; - } - URL.revokeObjectURL(url); - currentAudio = null; - currentSpeakBtn = null; - }; - - await currentAudio.play(); - - } catch (error) { - console.error('Erro TTS:', error); + // Falar texto com TTS nativa + const speakText = (text, btn) => { + // Se já está falando, parar + if (window.speechSynthesis.speaking) { + window.speechSynthesis.cancel(); if (btn) { btn.classList.remove('speaking'); const span = btn.querySelector('span'); if (span) span.textContent = 'Ouvir'; } - currentSpeakBtn = null; + return; } + + // Limpar markdown antes de falar + const clean = text + .replace(/```[\s\S]*?```/g, '') + .replace(/`[^`]+`/g, '') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/[*_#~\[\]>]/g, '') + .replace(/\n+/g, '. ') + .replace(/\s{2,}/g, ' ') + .trim(); + + if (!clean) return; + + const utterance = new SpeechSynthesisUtterance(clean); + 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'; + } + }; + + utterance.onend = utterance.onerror = () => { + if (btn) { + btn.classList.remove('speaking'); + const span = btn.querySelector('span'); + if (span) span.textContent = 'Ouvir'; + } + }; + + window.speechSynthesis.speak(utterance); }; // ========================================================================== diff --git a/server.js b/server.js index 99dea24..e633c02 100644 --- a/server.js +++ b/server.js @@ -343,66 +343,6 @@ app.post('/api/chat', requireAuth, async (req, res) => { } }); -// ========================================================================== -// TTS — Text-to-Speech com voz neural Microsoft (Thalita) -// ========================================================================== - -const { MsEdgeTTS } = require('msedge-tts'); -const TTS_VOICE = 'pt-BR-ThalitaMultilingualNeural'; -const TTS_FORMAT = 'audio-24khz-96kbitrate-mono-mp3'; - -// Instância TTS reutilizável (inicializada no primeiro uso) -let ttsInstance = null; -const getTTS = async () => { - if (!ttsInstance) { - ttsInstance = new MsEdgeTTS(); - await ttsInstance.setMetadata(TTS_VOICE, TTS_FORMAT); - } - return ttsInstance; -}; - -// Pré-inicializar TTS ao iniciar o servidor -getTTS().then(() => console.log(`TTS inicializado com voz: ${TTS_VOICE}`)) - .catch(err => console.warn('Aviso: TTS não pôde ser pré-inicializado:', err.message)); - -app.post('/api/tts', requireAuth, async (req, res) => { - const { text } = req.body; - - if (!text || typeof text !== 'string') { - return res.status(400).json({ error: 'Texto é obrigatório' }); - } - - // Limitar tamanho do texto para evitar abusos - const cleanText = text.substring(0, 5000); - - try { - const tts = await getTTS(); - const result = await tts.toStream(cleanText); - - res.setHeader('Content-Type', 'audio/mpeg'); - res.setHeader('Cache-Control', 'no-cache'); - - result.audioStream.pipe(res); - - result.audioStream.on('error', (err) => { - console.error('Erro no stream TTS:', err); - // Tentar reinicializar a instância TTS na próxima vez - ttsInstance = null; - if (!res.headersSent) { - res.status(500).json({ error: 'Erro ao gerar áudio' }); - } - }); - - } catch (error) { - console.error('Erro na rota TTS:', error); - // Reinicializar instância para a próxima tentativa - ttsInstance = null; - if (!res.headersSent) { - res.status(500).json({ error: 'Erro ao gerar áudio' }); - } - } -}); - // Rota coringa para redirecionar para index.html app.get('*', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html'));