var speechVoices = []; var speechVoicesLoaded = false; var speechTimer = null; var piperFallbackTimer = null; function refreshSpeechVoices() { if (!window.speechSynthesis) return; var voices = window.speechSynthesis.getVoices(); if (voices && voices.length) { speechVoices = voices; speechVoicesLoaded = true; } return speechVoices; } function pickSpeechVoice(lang) { refreshSpeechVoices(); var target = lang.toLowerCase(); var exact = speechVoices.find(function(voice) { return String(voice.lang || '').toLowerCase() === target; }); if (exact) return exact; var prefix = target.split('-')[0]; return speechVoices.find(function(voice) { return String(voice.lang || '').toLowerCase().split('-')[0] === prefix; }) || null; } function buildSpeechUtterance(text, lang) { var utterance = new SpeechSynthesisUtterance(text); var langMap = { 'es': 'es-ES', 'en': 'en-US', 'zh': 'zh-CN' }; utterance.lang = langMap[lang] || lang; var voice = pickSpeechVoice(utterance.lang); if (voice) utterance.voice = voice; utterance.rate = 0.9; utterance.pitch = 1; return utterance; } function doSpeak(text, lang) { if (!window.speechSynthesis || !window.SpeechSynthesisUtterance || !text) return; window.speechSynthesis.cancel(); if (speechTimer) clearTimeout(speechTimer); var utterance = buildSpeechUtterance(text, lang); speechTimer = setTimeout(function() { window.speechSynthesis.speak(utterance); if (window.speechSynthesis.paused) window.speechSynthesis.resume(); }, 60); } function speakText(text, lang) { if (!text) return; if (piperFallbackTimer) clearTimeout(piperFallbackTimer); if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) { playPiperAudio(text, lang); return; } refreshSpeechVoices(); doSpeak(text, lang); window.speechSynthesis.getVoices(); if (!speechVoicesLoaded) { if (piperFallbackTimer) clearTimeout(piperFallbackTimer); piperFallbackTimer = setTimeout(function() { refreshSpeechVoices(); if (!speechVoicesLoaded) playPiperAudio(text, lang); }, 900); } } function playPiperAudio(text, lang) { if (!text) return; fetch('/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text, lang: lang || 'es' }) }).then(function(res) { if (!res.ok) throw new Error('tts failed'); return res.blob(); }).then(function(blob) { var url = URL.createObjectURL(blob); var audio = new Audio(url); audio.onended = function() { URL.revokeObjectURL(url); }; audio.play(); }).catch(function() {}); } function initSpeech() { if (!window.speechSynthesis) return; refreshSpeechVoices(); window.speechSynthesis.onvoiceschanged = function() { refreshSpeechVoices(); }; } if (document.readyState === 'complete') initSpeech(); else document.addEventListener('DOMContentLoaded', initSpeech); function warmSpeechVoices() { if (!window.speechSynthesis) return; window.speechSynthesis.getVoices(); refreshSpeechVoices(); } document.addEventListener('touchstart', warmSpeechVoices, { once: true, passive: true }); document.addEventListener('pointerdown', warmSpeechVoices, { once: true, passive: true });