Pantalla TV: el pito tapaba la primera palabra, y página para elegir voz
El llamado se oía "entrecortado" por dos motivos distintos. playBeep() y speak() salían en el mismo instante, y el pito dura 0,65 s con volumen 0,35: "Turno A-01" se pronunciaba por debajo del tono y desde lejos parecía que el anuncio empezaba ya empezado. Se vuelve a esperar los 700 ms que el código tenía antes de que se quitaran. El otro motivo es la voz. getVozES() pide es-CO, que en Chrome es la de Google: remota. Cada llamado la baja de internet, así que con wifi flojo se corta o no suena. Las voces disponibles las decide el televisor, no el ERP, así que en vez de adivinar se agrega /erp.php?m=turnero&v=voces: lista las voces del equipo, marca cuáles son locales y cuáles se bajan de internet, señala la que está sonando hoy y deja oír cada una con el texto real de un llamado. Hay que abrirla en el televisor. Queda descartado el eco entre pantallas: hay un solo televisor con sonido. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cd087f65af
commit
db133d9b5d
@@ -588,15 +588,17 @@ function anunciarTurno(codigo, destino, paciente) {
|
||||
if (voz) utt.voice = voz;
|
||||
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.
|
||||
// Se espera a que el pito termine (dura 0,65 s) antes de hablar. Hablando
|
||||
// encima, el tono tapaba "Turno X" y desde lejos parecía que el anuncio
|
||||
// empezaba ya empezado.
|
||||
const BEEP_MS = 700;
|
||||
|
||||
// Solo cuando hay algo en curso hace falta cancelar: 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);
|
||||
}
|
||||
setTimeout(() => window.speechSynthesis.speak(utt), BEEP_MS);
|
||||
return utt;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/views/voces.php
|
||||
* Prueba de voces del televisor — /erp.php?m=turnero&v=voces
|
||||
*
|
||||
* Las voces disponibles no las decide el ERP sino el equipo: dependen del
|
||||
* sistema operativo y del navegador del televisor. Por eso esta página hay que
|
||||
* abrirla EN EL TELEVISOR, no en el computador de la oficina: cada equipo
|
||||
* ofrece una lista distinta.
|
||||
*
|
||||
* Sirve para oír cada voz con el texto real de un llamado y elegir la que
|
||||
* quede. Distingue las locales de las que se bajan de internet, que son las
|
||||
* que se entrecortan cuando el wifi flaquea.
|
||||
*/
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Prueba de voces · Turnero</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 24px;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
background: #0f172a; color: #e2e8f0;
|
||||
}
|
||||
h1 { font-size: 1.5rem; margin: 0 0 4px; }
|
||||
.sub { color: #94a3b8; font-size: .95rem; margin-bottom: 20px; }
|
||||
.aviso {
|
||||
background: #1e293b; border-left: 4px solid #38bdf8;
|
||||
padding: 12px 16px; border-radius: 6px; margin-bottom: 22px;
|
||||
font-size: .92rem; line-height: 1.5;
|
||||
}
|
||||
.fila {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
background: #1e293b; border-radius: 8px;
|
||||
padding: 14px 16px; margin-bottom: 10px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
.fila.elegida { border-color: #38bdf8; background: #1e3a52; }
|
||||
.nom { font-weight: 600; flex: 1; min-width: 0; }
|
||||
.nom small { display: block; font-weight: 400; color: #94a3b8; font-size: .8rem; }
|
||||
.tag {
|
||||
font-size: .72rem; padding: 3px 9px; border-radius: 99px;
|
||||
white-space: nowrap; font-weight: 600;
|
||||
}
|
||||
.local { background: #14532d; color: #86efac; }
|
||||
.remota { background: #7c2d12; color: #fdba74; }
|
||||
.actual { background: #0c4a6e; color: #7dd3fc; }
|
||||
button {
|
||||
background: #0284c7; color: #fff; border: 0;
|
||||
padding: 10px 20px; border-radius: 6px; cursor: pointer;
|
||||
font-size: .95rem; font-weight: 600; white-space: nowrap;
|
||||
}
|
||||
button:hover { background: #0369a1; }
|
||||
#vacio { color: #94a3b8; padding: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Prueba de voces</h1>
|
||||
<div class="sub">Ábrala <strong>en el televisor</strong>: cada equipo ofrece voces distintas.</div>
|
||||
|
||||
<div class="aviso">
|
||||
Las <span class="tag local">local</span> salen del propio equipo: nunca se cortan, aunque suenen
|
||||
algo más robóticas.<br>
|
||||
Las <span class="tag remota">de internet</span> suenan más naturales, pero se bajan de los
|
||||
servidores de Google en cada llamado. Con wifi débil se entrecortan o no suenan.
|
||||
<br><br>
|
||||
Oiga las que aparezcan y dígame cuál prefiere.
|
||||
</div>
|
||||
|
||||
<div id="lista"><div id="vacio">Cargando voces…</div></div>
|
||||
|
||||
<script>
|
||||
// El mismo texto que dice la pantalla de turnos, para juzgarlas en condiciones
|
||||
const TEXTO = 'Turno A 0 1, María Fernanda Gómez, pase a Consultorio 2';
|
||||
|
||||
// Réplica de la preferencia que usa display_global.php hoy, para señalar
|
||||
// cuál está sonando en este momento en el televisor.
|
||||
function vozActual(voces) {
|
||||
for (const lang of ['es-CO','es-419','es-MX','es-US','es-ES']) {
|
||||
const v = voces.find(v => v.lang === lang);
|
||||
if (v) return v;
|
||||
}
|
||||
return voces.find(v => v.lang.startsWith('es')) || null;
|
||||
}
|
||||
|
||||
function probar(voz) {
|
||||
window.speechSynthesis.cancel();
|
||||
const u = new SpeechSynthesisUtterance(TEXTO);
|
||||
u.voice = voz;
|
||||
u.lang = voz.lang;
|
||||
u.rate = 0.95;
|
||||
u.pitch = 1.05;
|
||||
window.speechSynthesis.speak(u);
|
||||
}
|
||||
|
||||
function pintar() {
|
||||
const todas = window.speechSynthesis.getVoices();
|
||||
const es = todas.filter(v => v.lang && v.lang.toLowerCase().startsWith('es'));
|
||||
const lista = document.getElementById('lista');
|
||||
const actual = vozActual(es);
|
||||
|
||||
if (!es.length) {
|
||||
lista.innerHTML = '<div id="vacio">Este equipo no tiene ninguna voz en español instalada. '
|
||||
+ 'Habría que instalarle una desde la configuración del sistema, '
|
||||
+ 'o dejar el llamado solo con el pito y el cartel.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Las locales primero: son las que interesan
|
||||
es.sort((a, b) => (b.localService - a.localService) || a.name.localeCompare(b.name));
|
||||
|
||||
lista.innerHTML = '';
|
||||
es.forEach((v, i) => {
|
||||
const fila = document.createElement('div');
|
||||
fila.className = 'fila' + (v === actual ? ' elegida' : '');
|
||||
fila.innerHTML =
|
||||
'<div class="nom">' + v.name + '<small>' + v.lang + '</small></div>'
|
||||
+ (v.localService ? '<span class="tag local">local</span>'
|
||||
: '<span class="tag remota">de internet</span>')
|
||||
+ (v === actual ? '<span class="tag actual">la de ahora</span>' : '');
|
||||
const b = document.createElement('button');
|
||||
b.textContent = 'Oír';
|
||||
b.onclick = () => probar(v);
|
||||
fila.appendChild(b);
|
||||
lista.appendChild(fila);
|
||||
});
|
||||
}
|
||||
|
||||
// getVoices() suele venir vacío en la primera llamada: el navegador las carga aparte
|
||||
window.speechSynthesis.onvoiceschanged = pintar;
|
||||
pintar();
|
||||
setTimeout(pintar, 600);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user