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 class="dot"></div>
</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 id="ann-overlay">
<div class="ann-bg"></div> <div class="ann-bg"></div>
<div class="ann-card"> <div class="ann-card">
@@ -466,21 +470,30 @@ let audioCtx = null, sonidoActivo = false;
function activarSonido() { function activarSonido() {
try { 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 g = audioCtx.createGain(); g.gain.setValueAtTime(0.001, audioCtx.currentTime);
const o = audioCtx.createOscillator(); o.connect(g); g.connect(audioCtx.destination); const o = audioCtx.createOscillator(); o.connect(g); g.connect(audioCtx.destination);
o.start(); o.stop(audioCtx.currentTime + 0.05); o.start(); o.stop(audioCtx.currentTime + 0.05);
} catch (_) {} } catch (_) {}
sonidoActivo = true; sonidoActivo = true;
localStorage.setItem('turneroSonido', '1');
const btn = document.getElementById('btn-sonido'); const btn = document.getElementById('btn-sonido');
if (btn) { btn.innerHTML = '<i class="fas fa-volume-up"></i>'; btn.classList.add('on'); } 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); setTimeout(playBeep, 100);
} }
document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); }, { once: true }); // Sin once:true para que cualquier clic reactive si la página recargó
document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); }, { once: true }); document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); });
document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); });
function playBeep() { function playBeep() {
if (!sonidoActivo || !audioCtx) return; if (!sonidoActivo || !audioCtx) return;
if (audioCtx.state === 'suspended') { audioCtx.resume().then(playBeep); return; }
try { try {
const osc = audioCtx.createOscillator(); const osc = audioCtx.createOscillator();
const g = audioCtx.createGain(); const g = audioCtx.createGain();
@@ -496,13 +509,21 @@ function playBeep() {
} catch (_) {} } 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; let _vozES = null;
function getVozES() { function getVozES() {
if (_vozES) return _vozES; if (_vozES) return _vozES;
const voices = window.speechSynthesis.getVoices(); const voices = window.speechSynthesis.getVoices();
// Preferencia: es-CO → es-419 → es-MX → es-US → es-ES → cualquier es-* // 1) Voz local (localService=true) en orden de preferencia
for (const lang of ['es-CO','es-419','es-MX','es-US','es-ES']) { 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); const v = voices.find(v => v.lang === lang);
if (v) { _vozES = v; return v; } if (v) { _vozES = v; return v; }
} }
@@ -512,13 +533,16 @@ function getVozES() {
} }
if ('speechSynthesis' in window) { if ('speechSynthesis' in window) {
window.speechSynthesis.onvoiceschanged = () => { _vozES = null; getVozES(); }; 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(() => { setInterval(() => {
if (!window.speechSynthesis.speaking) { window.speechSynthesis.pause();
window.speechSynthesis.pause(); window.speechSynthesis.resume();
window.speechSynthesis.resume(); }, 10000);
} }
}, 14000); // 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) { function anunciarTurno(codigo, destino, paciente) {
@@ -534,7 +558,7 @@ function anunciarTurno(codigo, destino, paciente) {
const voz = getVozES(); const voz = getVozES();
if (voz) utt.voice = voz; if (voz) utt.voice = voz;
utt.rate = 0.85; utt.pitch = 1.05; utt.volume = 1; 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; return utt;
} }