🚀 Auto-deploy: Camila atualizado em 26/05/2026 13:59:50

This commit is contained in:
2026-05-26 13:59:50 +00:00
parent ace6c53448
commit 71e633ce71
316 changed files with 59354 additions and 86 deletions
+71 -84
View File
@@ -218,109 +218,96 @@ const initApp = () => {
});
// ==========================================================================
// TTS — Speech Synthesis (ouvir respostas)
// TTS — Voz Neural Thalita via servidor (Microsoft Edge TTS)
// ==========================================================================
// Carregar vozes disponíveis (pode ser assíncrono em alguns navegadores)
const loadVoices = () => {
return new Promise((resolve) => {
const voices = window.speechSynthesis.getVoices();
if (voices.length > 0) {
resolve(voices);
} else {
window.speechSynthesis.addEventListener('voiceschanged', () => {
resolve(window.speechSynthesis.getVoices());
}, { once: true });
}
});
let currentAudio = null; // controlar áudio em andamento
let currentSpeakBtn = null;
// 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();
};
let cachedVoices = [];
loadVoices().then(voices => { cachedVoices = voices; });
// Escolher melhor voz feminina brasileira disponível
// Prioridade: Google pt-BR > Microsoft Natural > qualquer feminina > qualquer pt-BR
const getBestBRVoice = () => {
const brVoices = cachedVoices.filter(v => v.lang === 'pt-BR' || v.lang === 'pt_BR');
if (brVoices.length === 0) {
// Fallback: qualquer voz portuguesa
const ptVoices = cachedVoices.filter(v => v.lang.startsWith('pt'));
return ptVoices[0] || null;
}
// 1. Melhor: vozes Google (muito naturais no Chrome/Android)
const google = brVoices.find(v => /google/i.test(v.name) && /femin|female/i.test(v.name));
if (google) return google;
const googleAny = brVoices.find(v => /google/i.test(v.name));
if (googleAny) return googleAny;
// 2. Microsoft Natural / Online (Edge tem vozes excelentes como Francisca)
const msNatural = brVoices.find(v => /natural|online|francisca|thalita|giovanna/i.test(v.name));
if (msNatural) return msNatural;
// 3. Qualquer voz feminina explícita
const female = brVoices.find(v => /female|femin|maria|luciana|fernanda|raquel|camila/i.test(v.name));
if (female) return female;
// 4. Vozes locais (melhor que remotas genéricas)
const local = brVoices.find(v => v.localService === true);
if (local) return local;
// 5. Qualquer voz pt-BR disponível
return brVoices[0];
};
// Falar texto com TTS nativo
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');
// 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;
}
// Limpar markdown básico antes de falar
const clean = text
.replace(/```[\s\S]*?```/g, '') // remover blocos de código inteiros
.replace(/`[^`]+`/g, '') // remover inline code
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links → só texto
.replace(/[*_#~\[\]>]/g, '') // remover formatação
.replace(/\n+/g, '. ') // quebras viram pausas naturais
.replace(/\s{2,}/g, ' ')
.trim();
const clean = cleanTextForSpeech(text);
if (!clean) return;
const utterance = new window.SpeechSynthesisUtterance(clean);
utterance.lang = 'pt-BR';
utterance.rate = 1.12; // levemente mais rápida que o padrão
utterance.pitch = 1.0; // tom natural sem distorção
// Feedback visual imediato
if (btn) {
btn.classList.add('speaking');
const span = btn.querySelector('span');
if (span) span.textContent = 'Carregando...';
}
currentSpeakBtn = btn;
const voice = getBestBRVoice();
if (voice) utterance.voice = voice;
try {
const response = await fetch('/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: clean })
});
utterance.onstart = () => {
if (btn) {
btn.classList.add('speaking');
const span = btn.querySelector('span');
if (span) span.textContent = 'Parar';
}
};
if (!response.ok) throw new Error('Erro no TTS');
utterance.onend = utterance.onerror = () => {
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);
if (btn) {
btn.classList.remove('speaking');
const span = btn.querySelector('span');
if (span) span.textContent = 'Ouvir';
}
};
window.speechSynthesis.speak(utterance);
currentSpeakBtn = null;
}
};
// ==========================================================================