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>
155 lines
5.6 KiB
PHP
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>
|