🚀 Auto-deploy: Camila atualizado em 26/05/2026 14:20:35

This commit is contained in:
2026-05-26 14:20:35 +00:00
parent 71e633ce71
commit ea3487b417
2 changed files with 86 additions and 139 deletions
+86 -79
View File
@@ -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);
};
// ==========================================================================