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.
Código completo y demostración
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).
Comprobar si existe la API de speechSynthesis
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");
Pequeña aclaración de window.speechSynthesis y speechSyntesis
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
.
Las voces
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);
});
};
El evento onvoiceschanged
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.
Parla
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:
- text: el texto que convertirá a audio.
- voice: la voz en la que “hablará”.
- rate: velocidad con la que se dice el texto. Puede ir desde 0.1 hasta 10. Entre más alto sea el valor, más alta la velocidad. Si se pone muy bajo, la reproducción será lenta.
- pitch: tono de voz. Puede ir desde 0 hasta 2, se permiten los valores flotantes.
- volume: el volumen. Puede estar entre 0 y 1, se permiten los valores flotantes.
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.
Limitaciones
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.
Conclusión
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.
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.