Files
Lizandro GuarnizoandClaude Opus 5 7764f88df3 Pantalla TV: preferir Sabina y Paulina, que son locales
La voz es-CO de Chrome es de Google y se baja de internet en cada llamado:
con wifi flojo se corta. Una colombiana local no existe, así que se prefieren
las mexicanas del propio equipo —Sabina en Windows, Paulina en macOS—, que
para leer letras, números y un nombre propio se oyen naturales aquí.

El orden queda: las dos por nombre, luego cualquier local latinoamericana,
luego cualquier local aunque sea de España (peor acento, pero no se corta), y
la remota solo si el equipo no tiene ninguna instalada.

Comprobado contra cinco inventarios de voces: Windows con Sabina, macOS con
Paulina, solo España local, sin ninguna local, y sin voces en español.

voces.php replica el mismo orden para señalar cuál está sonando.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 07:57:33 -05:00

155 lines
5.6 KiB
PHP

<?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 de display_global.php, para señalar cuál está
// sonando. Si allá se cambia el orden, hay que cambiarlo aquí también.
const VOCES_PREFERIDAS = ['sabina', 'paulina'];
function vozActual(voces) {
if (!voces.length) return null;
const locales = voces.filter(v => v.localService);
for (const nombre of VOCES_PREFERIDAS) {
const v = locales.find(v => v.name.toLowerCase().includes(nombre));
if (v) return v;
}
for (const lang of ['es-MX','es-419','es-US','es-CO']) {
const v = locales.find(v => v.lang === lang);
if (v) return v;
}
if (locales.length) return locales[0];
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[0];
}
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>