1document.addEventListener('DOMContentLoaded', function () { 2 'use strict'; 3 4 const playButton = document.querySelector('.play-button'); 5 const speedButtons = { 6 s05: document.querySelector('.s05'), 7 s10: document.querySelector('.s10'), 8 s15: document.querySelector('.s15'), 9 s20: document.querySelector('.s20') 10 }; 11 12 if (!window.speechSynthesis) { 13 console.warn('API Speech não suportada.'); 14 if (playButton) playButton.disabled = true; 15 return; 16 } 17 18 // --- Obtém e sanitiza o texto --- 19 function getTextToSpeak() { 20 const elements = document.querySelectorAll('.titulo-speak, .txt-speak'); 21 let fullText = ''; 22 23 elements.forEach(el => { 24 let text = el.innerText || ''; 25 text = text.replace(/\s+/g, ' ').trim(); 26 // Sanitização rigorosa (remove emojis, caracteres de controle, etc.) 27 text = text.replace(/[^a-zA-Záà âãéèêÃïóôõöúçñÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃ0-9\s.,;:!?()\-"']/g, ''); 28 text = text.replace(/\s+/g, ' ').trim(); 29 if (text) { 30 fullText += text + ' '; 31 } 32 }); 33 34 return fullText.trim(); 35 } 36 37 // --- Estado --- 38 let isSpeaking = false; 39 let isPaused = false; 40 let currentRate = 1.0; 41 let currentChunks = []; 42 let currentChunkIndex = 0; 43 let currentUtterance = null; 44 45 // --- Obtém uma voz em português --- 46 function getPortugueseVoice() { 47 const voices = window.speechSynthesis.getVoices(); 48 return voices.find(v => v.lang.startsWith('pt')) || null; 49 } 50 51 // --- Função para falar em chunks --- 52 function speakChunks(text, rate) { 53 const chunkSize = 250; 54 const chunks = []; 55 for (let i = 0; i < text.length; i += chunkSize) { 56 chunks.push(text.substring(i, i + chunkSize)); 57 } 58 if (chunks.length === 0) return; 59 60 currentChunks = chunks; 61 currentChunkIndex = 0; 62 isSpeaking = true; 63 isPaused = false; 64 if (playButton) playButton.classList.add('pause'); 65 66 speakNextChunk(rate); 67 } 68 69 function speakNextChunk(rate) { 70 if (currentChunkIndex >= currentChunks.length) { 71 // Finalizou 72 isSpeaking = false; 73 isPaused = false; 74 if (playButton) playButton.classList.remove('pause'); 75 return; 76 } 77 78 const chunk = currentChunks[currentChunkIndex]; 79 const utterance = new SpeechSynthesisUtterance(chunk); 80 utterance.rate = rate; 81 utterance.lang = 'pt-BR'; 82 83 const voice = getPortugueseVoice(); 84 if (voice) utterance.voice = voice; 85 86 utterance.onend = function () { 87 currentChunkIndex++; 88 speakNextChunk(rate); 89 }; 90 91 utterance.onerror = function (event) { 92 if (event.error !== 'canceled') { 93 console.error('Erro na sÃntese:', event.error); 94 } 95 // Em caso de erro, paramos tudo 96 isSpeaking = false; 97 isPaused = false; 98 if (playButton) playButton.classList.remove('pause'); 99 }; 100 101 currentUtterance = utterance; 102 window.speechSynthesis.speak(utterance); 103 } 104 105 // --- Pausar / retomar --- 106 function pauseSpeech() { 107 if (isSpeaking && !isPaused) { 108 window.speechSynthesis.pause(); 109 isPaused = true; 110 } 111 } 112 113 function resumeSpeech() { 114 if (isSpeaking && isPaused) { 115 window.speechSynthesis.resume(); 116 isPaused = false; 117 } 118 } 119 120 function cancelSpeech() { 121 if (isSpeaking) { 122 window.speechSynthesis.cancel(); 123 isSpeaking = false; 124 isPaused = false; 125 if (playButton) playButton.classList.remove('pause'); 126 currentUtterance = null; 127 } 128 } 129 130 // --- Iniciar com nova velocidade --- 131 function restartWithRate(newRate) { 132 const text = getTextToSpeak(); 133 if (!text) { 134 console.warn('Nenhum texto válido para ler.'); 135 return; 136 } 137 currentRate = newRate; 138 cancelSpeech(); 139 // Aguarda um tick para garantir que o cancel foi processado 140 setTimeout(() => { 141 speakChunks(text, newRate); 142 }, 50); 143 } 144 145 // --- Evento Play/Pause --- 146 if (playButton) { 147 playButton.addEventListener('click', function () { 148 const text = getTextToSpeak(); 149 if (!text) { 150 console.warn('Nenhum texto válido para ler.'); 151 return; 152 } 153 154 if (!isSpeaking) { 155 // Inicia nova leitura 156 speakChunks(text, currentRate); 157 return; 158 } 159 160 // Se já está falando 161 if (isPaused) { 162 resumeSpeech(); 163 } else { 164 pauseSpeech(); 165 } 166 });
167 } 168 169 // --- Botões de velocidade --- 170 if (speedButtons.s05) speedButtons.s05.addEventListener('click', () => restartWithRate(0.5)); 171 if (speedButtons.s10) speedButtons.s10.addEventListener('click', () => restartWithRate(1.0)); 172 if (speedButtons.s15) speedButtons.s15.addEventListener('click', () => restartWithRate(1.5)); 173 if (speedButtons.s20) speedButtons.s20.addEventListener('click', () => restartWithRate(2.0)); 174 175 // --- Limpeza ao sair --- 176 window.addEventListener('beforeunload', function () { 177 if (window.speechSynthesis.speaking) window.speechSynthesis.cancel(); 178 }); 179 180 console.log('Speech Synthesis carregada com sanitização e chunks.'); 181});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.