🔊 TTS Ouvir: botão em cada resposta da Kemily com Speech Synthesis nativo
This commit is contained in:
+100
@@ -90,6 +90,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let isGenerating = false;
|
||||
let searchQuery = '';
|
||||
let activeTag = 'all';
|
||||
let currentUtterance = null; // para controlar TTS em andamento
|
||||
|
||||
// Variáveis de Reconhecimento de Voz
|
||||
let recognition = null;
|
||||
@@ -216,6 +217,90 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// TTS — Speech Synthesis (ouvir respostas)
|
||||
// ==========================================================================
|
||||
|
||||
// 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 cachedVoices = [];
|
||||
loadVoices().then(voices => { cachedVoices = voices; });
|
||||
|
||||
// Escolher melhor voz feminina brasileira disponível
|
||||
const getBestBRVoice = () => {
|
||||
const brVoices = cachedVoices.filter(v => v.lang.includes('pt'));
|
||||
// Prioridade: feminina > masculino, local > remote
|
||||
const female = brVoices.find(v =>
|
||||
/female|brasil|maria|luciana|fernanda|virtual/i.test(v.name)
|
||||
);
|
||||
if (female) return female;
|
||||
// fallback: qualquer voz brasileira
|
||||
const brazil = brVoices.find(v => /brasil|brazil|pt-BR/i.test(v.lang));
|
||||
if (brazil) return brazil;
|
||||
// fallback: qualquer voz portuguesa
|
||||
return brVoices[0] || null;
|
||||
};
|
||||
|
||||
// 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');
|
||||
if (span) span.textContent = 'Ouvir';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Limpar markdown básico antes de falar
|
||||
const clean = text
|
||||
.replace(/```[\s\S]*?```/g, 'trecho de código ignorado')
|
||||
.replace(/`[^`]+`/g, '')
|
||||
.replace(/[*_#~\[\]]/g, '')
|
||||
.replace(/\n+/g, ' ')
|
||||
.trim();
|
||||
|
||||
const utterance = new window.SpeechSynthesisUtterance(clean);
|
||||
utterance.lang = 'pt-BR';
|
||||
utterance.rate = 0.95;
|
||||
utterance.pitch = 1.05;
|
||||
|
||||
const voice = getBestBRVoice();
|
||||
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);
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// ATALHOS DE TECLADO
|
||||
// ==========================================================================
|
||||
@@ -519,6 +604,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</svg>
|
||||
<span>Regenerar</span>
|
||||
</button>
|
||||
<button class="msg-action-btn btn-speak" title="Ouvir resposta" data-content="${escapeHtml(content).replace(/"/g, '"')}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
|
||||
</svg>
|
||||
<span>Ouvir</span>
|
||||
</button>
|
||||
</div>
|
||||
` : `
|
||||
<div class="message-actions">
|
||||
@@ -602,6 +693,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Event listener para ouvir resposta (TTS nativo)
|
||||
const speakBtn = row.querySelector('.btn-speak');
|
||||
if (speakBtn) {
|
||||
speakBtn.addEventListener('click', () => {
|
||||
const text = speakBtn.dataset.content;
|
||||
speakText(text, speakBtn);
|
||||
});
|
||||
}
|
||||
|
||||
// Event listener para regenerar resposta
|
||||
const regenBtn = row.querySelector('.btn-regenerate');
|
||||
if (regenBtn) {
|
||||
|
||||
Reference in New Issue
Block a user