javascript

Texto a voz con JavaScript y la API de speechSynthesis

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”.

Conversor texto a audio con JavaScript

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

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.

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.