En este post vamos a ver cómo usar la API de speechSynthesis con JavaScript, presente en la mayoría de los navegadores web. Esto permitirá hacer un conversor de texto a voz con JS, sin librerías externas ni frameworks.
La tecnología TTS, Text to speech, texto a voz o síntesis de voz es algo que permite convertir texto a habla, es decir, ingresamos el texto y luego escuchamos una voz que lo “lee”.
Gracias a esta API de speechSynthesis con JavaScript podemos agregar más funcionalidades a nuestras aplicaciones web, y hay buenas noticias, pues funciona (al momento de escribir esto) en Chrome, Firefox y Edge. También en Android usando Chrome y Firefox.
Veamos entonces cómo consumir esta API.
Puedes ver el vídeo en mi canal de YT (si vas por ahí, suscríbete): https://youtu.be/BlUOl6ze2Aw
También puedes probar la webapp directamente aquí, y ver el código fuente completo en el repositorio de GitHub (durante el post mostraré solamente las partes más importantes).
Para saber si existe la API en el navegador del usuario comprobamos que "speechSynthesis"
sea una propiedad de window
, usamos el operador in
aunque podríamos usar un if(window.speechSynthesis)
if (!'speechSynthesis' in window) return alert("Lo siento, tu navegador no soporta esta tecnología");
Al inicio comprobamos que la variable esté en window
usando in
, pero más tarde al usar la API no usamos window.speechSynthesis
, sino simplemente speechSynthesis
.
Esto es porque cualquier cosa que le pertenece a window
es global, así que podemos usar a speechSynthesis
sin acceder a window
.
Dependiendo del navegador web, idiomas, sistemas operativos y todas esas cosas habrán más o menos lenguajes. Por ejemplo, Chrome tiene más idiomas que los que ofrece Edge. Desconozco la razón, pero las voces no siempre serán las mismas.
Para obtener las voces que podemos usar llamamos a getVoices
. Eso regresará un arreglo de objetos que tienen la siguiente estructura:
{
default: false
lang: "es-US"
localService: false
name: "Google español de Estados Unidos"
voiceURI: "Google español de Estados Unidos"
}
Al indicarle a speechSynthesis
que “hable” debemos indicarle el idioma obtenido de esta lista de getVoices
.
En el ejemplo obtenemos las voces y las ponemos en un select
.
// Función que pone las voces dentro del select
const cargarVoces = () => {
if (vocesDisponibles.length > 0) {
console.log("No se cargan las voces porque ya existen: ", vocesDisponibles);
return;
}
vocesDisponibles = speechSynthesis.getVoices();
console.log({ vocesDisponibles })
posibleIndice = vocesDisponibles.findIndex(voz => IDIOMAS_PREFERIDOS.includes(voz.lang));
if (posibleIndice === -1) posibleIndice = 0;
vocesDisponibles.forEach((voz, indice) => {
const opcion = document.createElement("option");
opcion.value = indice;
opcion.innerHTML = voz.name;
opcion.selected = indice === posibleIndice;
$voces.appendChild(opcion);
});
};
Las voces, en algunos navegadores, son cargadas de manera asíncrona para no interrumpir el hilo principal ni congelar la app.
Por ello es que en ocasiones getVoices
devolverá un arreglo vacío, pues todavía no hay voces. Para asegurarnos de que hay voces esperamos el evento onvoiceschanged
:
// No preguntes por qué pongo esto que se ejecuta dos veces
// en determinados casos, solo así funciona en algunos casos
cargarVoces();
// Si hay evento, entonces lo esperamos
if ('onvoiceschanged' in speechSynthesis) {
speechSynthesis.onvoiceschanged = function () {
cargarVoces();
};
}
Tal vez te preguntes ¿por qué llamar a getVoices
dos veces, no se supone que debemos llamarlo hasta que el evento se dispare? sí y no. JavaScript y los navegadores son caprichosos, así que debemos aplicar trucos.
Por ejemplo, en algunos navegadores el evento no se dispara hasta que se intenta usar la API, es como una paradoja porque, ¿cómo vas a usar la API de TTS si estás esperando el evento, pero para que se dispare el evento debes usar la API?
Así que intentamos cargar las voces, el navegador dirá: oh, quieres cargar voces, seguramente quieres usar la API de speechSynthesis así que voy a cargarla y a desencadenar el evento.
Por otro lado, en algunos navegadores como Safari (me gusta llamarlo El nuevo internet explorer, porque da tantos problemas ahora como Explorer en su tiempo) el evento no existe, así que por eso el código que comprueba y escucha.
Después de obtener las voces podemos construir un objeto que es instancia de SpeechSynthesisUtterance
.
Tiene algunos valores que le establecemos después de crearlo, por ejemplo, la voz o el volumen. A continuación trato de explicar los valores:
Después de crear nuestro objeto llamamos al método speak
y entonces el texto será convertido a audio.
let textoAEscuchar = $mensaje.value;
if (!textoAEscuchar) return alert("Escribe el texto");
let mensaje = new SpeechSynthesisUtterance();
mensaje.voice = vocesDisponibles[$voces.value];
mensaje.volume = 1;
mensaje.rate = 1;
mensaje.text = textoAEscuchar;
mensaje.pitch = 1;
// ¡Parla!
speechSynthesis.speak(mensaje);
El código es llamado desde el click de un botón.
Según las investigaciones y pruebas que he hecho no se puede convertir texto a voz sin interacción del usuario. Es decir, no se puede usar inmediatamente al cargar la página, tiene que ser en el click de un botón o algo con lo que el usuario haga interacción.
Me parece que esa limitante es para no entrar a una página y escuchar “Has ganado un premio” o cosas de esas. Sigue las mismas razones por las que un vídeo no puede tener sonido sin interacción del usuario.
Otra limitante es que el audio generado a partir del texto no puede ser guardado como un archivo.
Ahora el chiste de:
¿Puedes hacer que el sistema diga “Bienvenido” y después mi nombre, al iniciar sesión?
Ya no será más un chiste.
Con esto podemos agregar unas pequeñas características a nuestras webapps. Esperemos que estas APIs evolucionen y se queden, porque aunque existen todavía son calificadas como experimentales.
Mira más cosas sobre JavaScript en mi blog.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.
Ver comentarios
Sigo sin entender por que hacen esto "mensaje.voice = vocesDisponibles[$voces.value];",a osea se que quieres cambiar la voz pero no entiendo el $voces.value ya que no veo declarado $voces antes.