🚀 Auto-deploy: Camila atualizado em 26/05/2026 14:20:35
This commit is contained in:
+86
-79
@@ -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);
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user