Pantalla TV: el llamado por voz salía entrecortado y con retraso

El troceo lo causaba el keepalive del sintetizador: corría cada 10 segundos sin
verificar si estaba hablando, y pause() a mitad de frase la corta. Como un
anuncio con nombre y destino supera los 10 segundos, lo alcanzaba siempre. Ahora
solo actúa con el sintetizador en reposo, que es para lo que existe: evitar que
Chrome lo suspenda tras un rato sin uso.

El cartel se cerraba a los 5 segundos aunque la voz siguiera sonando, y al
cerrarse pasaba al siguiente de la cola, que cancelaba el anuncio en curso. Si
dos pacientes se llamaban seguido, el primero quedaba a media frase. Ahora se
cierra cuando terminó de hablar y se cumplió el mínimo visual, con una red de
seguridad por si el navegador no emite el evento de fin.

Sobre la demora: se quita la espera fija de 300 ms antes de hablar —solo se
mantiene, y reducida, cuando de verdad hay que cancelar algo en curso— y el
sondeo baja de 2 s a 1 s. El retraso desde el clic pasa de 0,3–2,3 s a 0–1 s.

La velocidad sube de 0.85 a 0.95: acorta el anuncio sin perder claridad.

Validado con una simulación del reloj y del sintetizador: reproduce el corte
con el código anterior y no ocurre con el nuevo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-11 10:05:56 -05:00
co-authored by Claude Opus 5
parent c00faa2b06
commit 4bc22bd00c
+36 -7
View File
@@ -558,8 +558,12 @@ function getVozES() {
}
if ('speechSynthesis' in window) {
window.speechSynthesis.onvoiceschanged = () => { _vozES = null; getVozES(); };
// Keepalive: Chrome pausa speechSynthesis tras ~15 min — correr siempre, no solo cuando !speaking
// Keepalive: Chrome suspende speechSynthesis tras un rato sin uso.
// Solo debe correr con el sintetizador en reposo: pause() a mitad de una
// frase la corta, y como el anuncio dura más de 10 segundos, antes lo
// troceaba siempre.
setInterval(() => {
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) return;
window.speechSynthesis.pause();
window.speechSynthesis.resume();
}, 10000);
@@ -582,8 +586,17 @@ function anunciarTurno(codigo, destino, paciente) {
utt.lang = 'es-CO';
const voz = getVozES();
if (voz) utt.voice = voz;
utt.rate = 0.85; utt.pitch = 1.05; utt.volume = 1;
setTimeout(() => { window.speechSynthesis.cancel(); window.speechSynthesis.speak(utt); }, 300);
utt.rate = 0.95; utt.pitch = 1.05; utt.volume = 1;
// Con el sintetizador libre se habla de inmediato. Solo cuando hay algo en
// curso hace falta cancelar y esperar un instante: Chrome ignora un speak()
// encadenado a un cancel() en el mismo ciclo.
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) {
window.speechSynthesis.cancel();
setTimeout(() => window.speechSynthesis.speak(utt), 80);
} else {
window.speechSynthesis.speak(utt);
}
return utt;
}
@@ -645,9 +658,24 @@ function showAnnouncement({ codigo, destino, paciente, color }) {
ov.classList.remove('visible');
setTimeout(processQueue, 370);
}
// Cierra al terminar el TTS; mínimo 5s visual
if (utt) utt.addEventListener('end', () => setTimeout(dismiss, 500));
setTimeout(dismiss, 5000);
// El cartel se cierra cuando terminó de hablar Y se cumplió el mínimo
// visual. Cerrar a los 5 s sin esperar la voz hacía que el siguiente
// llamado cancelara al anterior a media frase.
const MIN_VISIBLE = 5000;
const abiertoEn = Date.now();
const cerrarCuandoToque = () => {
setTimeout(dismiss, Math.max(0, MIN_VISIBLE - (Date.now() - abiertoEn)));
};
if (utt) {
utt.addEventListener('end', () => setTimeout(cerrarCuandoToque, 400));
utt.addEventListener('error', cerrarCuandoToque);
// Red de seguridad: Chrome a veces no emite 'end' y el cartel quedaría fijo
setTimeout(dismiss, 25000);
} else {
setTimeout(dismiss, MIN_VISIBLE);
}
}
/* ── State ── */
@@ -797,7 +825,8 @@ async function cargarSnapshot() {
}
cargarSnapshot();
setInterval(cargarSnapshot, 2000);
// 1 s en vez de 2: el llamado salía con hasta 2 s de retraso desde el clic
setInterval(cargarSnapshot, 1000);
document.addEventListener('visibilitychange', () => { if (!document.hidden) cargarSnapshot(); });
window.addEventListener('focus', cargarSnapshot);