Voz del televisor y demora de RIPS: análisis de fondo de los dos "a veces"
── Voz que a veces no suena o se corta ── Dos causas nuevas, ambas intermitentes por naturaleza: 1. Defecto conocido de Chrome: si nada referencia la SpeechSynthesisUtterance, el recolector de basura puede llevársela A MITAD DE FRASE. El audio se corta en seco y 'end' no llega. Depende de cuándo pase el recolector, así que nunca se reproduce a voluntad. Cada locución queda anclada en un Set hasta terminar (_uttAncladas). 2. Tras un cancel(), en Windows y ChromeOS el motor puede quedar en pausa: speak() encola y jamás suena, sin error alguno. resume() antes de cada speak() lo destranca, y sobre un motor sano no hace nada. Y porque lo intermitente no se depura mirando la pantalla, queda una bitácora: cada intento de hablar registra voz elegida, si es local, cuánto tardó en arrancar y cómo terminó — en localStorage.tvVozLog (últimos 200) y en la tabla turnero_tv_log vía sendBeacon (endpoint log_tv.php, insert-only, eventos de lista cerrada). La próxima vez que reporten "ayer a las 10 no sonó", se consulta la tabla y se ve qué pasó exactamente. ── Servicios de RIPS que "se demoran" ── Medido en datos, no en hipótesis: la facturación en el sistema del laboratorio ocurre MIENTRAS la recepcionista atiende. La consulta a RIPS se hacía UNA sola vez, al vincular al paciente — y en 7 días, 547 de 549 registros llegaron DESPUÉS de esa única consulta. Nadie volvía a preguntar: eso es lo que se percibía como lentitud de RIPS. (El scheduler en sí tarda segundos; de paso: el reloj del equipo legado está ~1 hora atrasado, visible en hora_recepcion.) Ahora la ficha sondea cada 10 s hasta 10 minutos y se detiene al encontrar, al limpiar la ficha o al agotar los intentos. Si la recepcionista ya marcó exámenes a mano, no se le pisan: se le ofrece el banner en vez de autocargar. ── Pruebas (22, todas contra el código real extraído de las vistas) ── scripts/test_tv_voz.js 9 anclaje GC, resume(), bloqueo, voz lenta, bitácora scripts/test_rips_sondeo.js 7 reintentos, paradas, tope, sin duplicar scripts/test_rips_ventana.php 6 ventana SQL en tabla TEMPORARY, sin tocar datos Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8218daaff7
commit
a28ae82402
@@ -651,6 +651,35 @@ if ('speechSynthesis' in window) {
|
||||
// A nivel de archivo porque showAnnouncement() también lo necesita.
|
||||
const BEEP_MS = 700;
|
||||
|
||||
// ── Anclaje contra el recolector de basura ──
|
||||
// Defecto conocido de Chrome: si nada referencia la SpeechSynthesisUtterance,
|
||||
// el recolector puede llevársela A MITAD DE FRASE. El audio se corta en seco y
|
||||
// 'end' no llega nunca. Como depende de cuándo pase el recolector, ocurre "a
|
||||
// veces" y nunca se reproduce a voluntad. Cada locución queda anclada aquí
|
||||
// hasta que termina.
|
||||
const _uttAncladas = new Set();
|
||||
|
||||
// ── Bitácora de la voz ──
|
||||
// Los fallos intermitentes no se pueden depurar mirando la pantalla: hay que
|
||||
// registrarlos cuando ocurren. Cada evento va a un anillo local (últimos 200,
|
||||
// consultable con localStorage.tvVozLog en la consola del televisor) y al
|
||||
// servidor con sendBeacon, que no bloquea ni exige respuesta.
|
||||
function vozLog(evento, datos) {
|
||||
const fila = Object.assign({ t: new Date().toISOString().slice(0, 19), e: evento }, datos || {});
|
||||
try {
|
||||
const ring = JSON.parse(localStorage.getItem('tvVozLog') || '[]');
|
||||
ring.push(fila);
|
||||
while (ring.length > 200) ring.shift();
|
||||
localStorage.setItem('tvVozLog', JSON.stringify(ring));
|
||||
} catch (_) {}
|
||||
try {
|
||||
navigator.sendBeacon(
|
||||
'<?= defined('BASE_URL') ? BASE_URL : '/' ?>modules/turnero/api/log_tv.php',
|
||||
new Blob([JSON.stringify(fila)], { type: 'application/json' })
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dice el llamado en voz alta.
|
||||
*
|
||||
@@ -685,10 +714,28 @@ function anunciarTurno(codigo, destino, paciente, onFin) {
|
||||
utt.lang = 'es-CO';
|
||||
utt.rate = 0.95; utt.pitch = 1.05; utt.volume = 1;
|
||||
|
||||
_uttAncladas.add(utt);
|
||||
const soltar = () => _uttAncladas.delete(utt);
|
||||
const pedido = Date.now();
|
||||
|
||||
let arranco = false;
|
||||
utt.addEventListener('start', () => { arranco = true; });
|
||||
utt.addEventListener('end', avisar);
|
||||
utt.addEventListener('error', () => esReintento ? avisar() : decir(true));
|
||||
utt.addEventListener('start', () => {
|
||||
arranco = true;
|
||||
vozLog('start', { cod: codigo, ms: Date.now() - pedido,
|
||||
voz: utt.voice ? utt.voice.name : '(navegador)',
|
||||
local: utt.voice ? !!utt.voice.localService : null,
|
||||
re: esReintento ? 1 : 0 });
|
||||
});
|
||||
utt.addEventListener('end', () => {
|
||||
soltar();
|
||||
vozLog('end', { cod: codigo, dur: Date.now() - pedido });
|
||||
avisar();
|
||||
});
|
||||
utt.addEventListener('error', (ev) => {
|
||||
soltar();
|
||||
vozLog('error', { cod: codigo, err: ev.error || '?', re: esReintento ? 1 : 0 });
|
||||
esReintento ? avisar() : decir(true);
|
||||
});
|
||||
|
||||
// Que speak() no lance nada no significa que vaya a sonar: si el
|
||||
// navegador bloquea el audio, o la voz no sirve, no pasa absolutamente
|
||||
@@ -701,7 +748,12 @@ function anunciarTurno(codigo, destino, paciente, onFin) {
|
||||
// cancelarla aquí era cortarle la frase y repetirla con otra voz.
|
||||
// De ahí que a veces se oyera media frase y luego otra distinta.
|
||||
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) return;
|
||||
if (esReintento) { avisar(); return; }
|
||||
soltar();
|
||||
if (esReintento) {
|
||||
vozLog('mudo', { cod: codigo }); // ni la voz del navegador arrancó
|
||||
avisar(); return;
|
||||
}
|
||||
vozLog('sin_start', { cod: codigo }); // la elegida nunca arrancó: reintento
|
||||
window.speechSynthesis.cancel();
|
||||
decir(true); // segunda oportunidad, con la voz del navegador
|
||||
}, margen);
|
||||
@@ -721,6 +773,10 @@ function anunciarTurno(codigo, destino, paciente, onFin) {
|
||||
const voz = getVozES();
|
||||
if (voz) utt.voice = voz;
|
||||
}
|
||||
// Tras un cancel(), en Windows y ChromeOS el motor puede quedar en
|
||||
// pausa: speak() encola y jamás suena, sin error alguno. resume()
|
||||
// sobre un motor sano no hace nada, así que se llama siempre.
|
||||
window.speechSynthesis.resume();
|
||||
window.speechSynthesis.speak(utt);
|
||||
}, esReintento ? 0 : BEEP_MS);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user