fix: voz TTS no sonaba en display_global por voz online y falta de activación

- getVozES() prefiere voces locales (offline) sobre Microsoft Online que
  fallan silenciosamente sin internet o con latencia
- cancel() antes de speak() para limpiar cola atascada
- keepalive sin check !speaking, cada 10s
- sonidoActivo persiste en localStorage; banner rojo si no activado
- AudioContext se resume si suspended antes de reproducir

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-28 06:47:19 -05:00
co-authored by Claude Sonnet 4.6
parent 4b054c5afc
commit cbcc0a9d8d
+37 -13
View File
@@ -373,6 +373,10 @@ $_hasVideo = (bool)$_tvVideo;
<div class="dot"></div>
</div>
<div id="sonido-banner" onclick="activarSonido()" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:9999;background:rgba(239,68,68,.92);color:#fff;padding:.6rem 1.2rem;font-size:1rem;font-weight:600;cursor:pointer;align-items:center;justify-content:center;gap:.6rem">
<i class="fas fa-volume-mute"></i> Haz clic aquí para activar el sonido
</div>
<div id="ann-overlay">
<div class="ann-bg"></div>
<div class="ann-card">
@@ -466,21 +470,30 @@ let audioCtx = null, sonidoActivo = false;
function activarSonido() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} else if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
const g = audioCtx.createGain(); g.gain.setValueAtTime(0.001, audioCtx.currentTime);
const o = audioCtx.createOscillator(); o.connect(g); g.connect(audioCtx.destination);
o.start(); o.stop(audioCtx.currentTime + 0.05);
} catch (_) {}
sonidoActivo = true;
localStorage.setItem('turneroSonido', '1');
const btn = document.getElementById('btn-sonido');
if (btn) { btn.innerHTML = '<i class="fas fa-volume-up"></i>'; btn.classList.add('on'); }
const banner = document.getElementById('sonido-banner');
if (banner) banner.style.display = 'none';
setTimeout(playBeep, 100);
}
document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); }, { once: true });
document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); }, { once: true });
// Sin once:true para que cualquier clic reactive si la página recargó
document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); });
document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); });
function playBeep() {
if (!sonidoActivo || !audioCtx) return;
if (audioCtx.state === 'suspended') { audioCtx.resume().then(playBeep); return; }
try {
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
@@ -496,13 +509,21 @@ function playBeep() {
} catch (_) {}
}
// Selección de voz española — cargada una vez, reutilizada en cada anuncio
// Selección de voz española — preferir locales (offline) sobre online para evitar fallos de red
let _vozES = null;
function getVozES() {
if (_vozES) return _vozES;
const voices = window.speechSynthesis.getVoices();
// Preferencia: es-CO → es-419 → es-MX → es-US → es-ES → cualquier es-*
for (const lang of ['es-CO','es-419','es-MX','es-US','es-ES']) {
// 1) Voz local (localService=true) en orden de preferencia
for (const lang of ['es-CO','es-MX','es-US','es-ES']) {
const v = voices.find(v => v.lang === lang && v.localService);
if (v) { _vozES = v; return v; }
}
// 2) Cualquier voz local en español
const vLocal = voices.find(v => v.lang.startsWith('es') && v.localService);
if (vLocal) { _vozES = vLocal; return vLocal; }
// 3) Fallback: voz online (puede fallar sin internet)
for (const lang of ['es-CO','es-MX','es-US','es-ES']) {
const v = voices.find(v => v.lang === lang);
if (v) { _vozES = v; return v; }
}
@@ -512,13 +533,16 @@ function getVozES() {
}
if ('speechSynthesis' in window) {
window.speechSynthesis.onvoiceschanged = () => { _vozES = null; getVozES(); };
// Keepalive: Chrome pausa speechSynthesis tras ~15 min sin interacción del usuario
// Keepalive: Chrome pausa speechSynthesis tras ~15 min — correr siempre, no solo cuando !speaking
setInterval(() => {
if (!window.speechSynthesis.speaking) {
window.speechSynthesis.pause();
window.speechSynthesis.resume();
}
}, 14000);
window.speechSynthesis.pause();
window.speechSynthesis.resume();
}, 10000);
}
// Auto-restore sonido si estaba activo antes de un reload
if (localStorage.getItem('turneroSonido') === '1') {
const banner = document.getElementById('sonido-banner');
if (banner) banner.style.display = 'flex';
}
function anunciarTurno(codigo, destino, paciente) {
@@ -534,7 +558,7 @@ function anunciarTurno(codigo, destino, paciente) {
const voz = getVozES();
if (voz) utt.voice = voz;
utt.rate = 0.85; utt.pitch = 1.05; utt.volume = 1;
setTimeout(() => window.speechSynthesis.speak(utt), 800);
setTimeout(() => { window.speechSynthesis.cancel(); window.speechSynthesis.speak(utt); }, 300);
return utt;
}