Convirtiendo texto a voz usando Javascript

Conviertiendo texto a voz usando Javascript

No cabe duda, y podemos confirmar que a día de hoy Javascript es un lenguaje de programación muy potente que nos permite crear «casi» cualquier cosa.

Javascript junto a otras tecnologías a conseguido dar forma a lo que hoy es internet, pasando de un entorno «offline» a otro completamente «online».

El navegador web, esa aplicación que hasta hace unos años era una más, hoy se ha convertido en la «aplicación» casi indispensable y necesaria para realizar gran parte de nuestro trabajo.

Junto a la evolución del propio lenguaje, el navegador web ha ido evolucionando ofreciendo características que hasta hace unos años ni si quiera se hubieran planteado.

Esa evolución se debe en parte a las mejoras que se han ido introduciendo en la API del navegador.

Algunas de esas características todavía están en fase experimental y en algunos casos solo con soporte para navegadores como Google Chrome y/o Mozilla Firefox.

Una de esas características es la Web Speech API, la cual nos permite añadir voz a nuestro navegador pudiendo escuchar cualquier texto exponiendo la interface SpeechSynthesis.

Escuchando a nuestro navegador

Ya que este ejemplo es muy sencillo voy a usar HTML, Javascript y CSS sin apoyarme en ningún framework o librería, pues me interesa mostrar la configuración y el flujo mínimo necesario para escuchar a nuestro navegador.

Para nuestro ejemplo podemos usar el siguiente código

index.html

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TEXT TO VOICE - JAVASCRIPT</title>
  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous" />
  <link rel="stylesheet" href="./css/index.css">
</head>
<body>
  <div class="container">
    <input type="text" placeholder="Texto to Speech" id="text">
    <div class="conversation"></div>
    <div class="container-button">
      <select id="voices"></select>
      <button id="btnSave"><i class="fas fa-upload"></i>Save text</button>
      <button id="btnLoad" disabled><i class="fas fa-download"></i>Load text</button>
      <button id="btnPlay" disabled><i class="fas fa-play-circle"></i>Play text</button>
    </div>
  </div>
  <div class="notification"></div>
  <script src="./js/index.js"></script>
</body>
</html>

index.css

body {
  font-family: 'Montserrat', sans-serif;
  overflow: hidden;
}
input[type='text'] {
  border: 0;
  border-bottom: 1px solid #cccccc;
  width: calc(100% - 20px);
  margin: 10px 0;
  padding: 10px;
}
button,
select {
  border: 0;
  padding: 10px;
  background: #3988f2;
  color: #ffffff;
  box-shadow: 0 0 3px 1px rgb(0, 0, 0, 0.2);
  cursor: pointer;
}
button:focus,
input:focus {
  outline: -webkit-focus-ring-color auto 0px;
}
button[disabled] {
  background: #aaaaaa;
  color: #000000;
  cursor: not-allowed;
}
button i {
  margin-right: 10px;
}
select {
  padding: 9px;
}
.conversation {
  width: calc(100% - 20px);
  height: 300px;
  box-shadow: 0 0 7px 2px rgb(0, 0, 0, 0.2);
  margin: 15px 0;
  padding: 10px;
  overflow: auto;
}
.container-button {
  text-align: right;
}
.notification {
  width: 200px;
  padding: 10px;
  border-radius: 2px;
  position: absolute;
  left: calc(50% - 100px);
  bottom: -100px;
  transition: 450ms ease-in-out;
  text-align: center;
  line-height: 25px;
}
.notification.show {
  bottom: 50px;
  transition: 450ms ease-in-out;
}
.info {
  background: #3988f2;
  color: #ffffff;
}
.warning {
  background: #fe8c8c;
  color: #000000;
}
.success {
  background: #60c356;
  color: #000000;
}

Bien, de momento con los dos ficheros anteriores solo hemos creado la vista de nuestra pequeña aplicación.

A continuación veremos la lógica que nos permite escuchar a nuestro navegador.

Inicializando nuestra voz en Javascript

Como la finalidad de este proyecto es sentar las bases solo usare alguno de los métodos que ofrece esta API del navegador, y el resto lo dejamos a la imaginación de cada uno.

Lo primero que haremos es comprobar que nuestro navegador tiene soporte para esta API.

if (!'speechSynthesis' in window) {
  showNotify({
    msn: 'Your browser not support tha Web Speech API',
    show: true,
    type: 'success'
  });
  return false;
}

Ahora vamos a obtener la interface de la API, ya que necesitaremos manipular algunas de sus propiedades

let voice = new SpeechSynthesisUtterance();

A continuación guardaremos el objeto que nos permite acceder a la funcionalidad de la API.

let jarvis = window.speechSynthesis;

Bien, pues con estas dos líneas de código ya tendríamos preparado nuestro navegador para que nos hable.

Añadiendo un poco de lógica

Con las dos líneas anteriores ya tenemos preparado todo, pero claro…, antes de hacer la magia vamos a explicar algunos conceptos básicos.

Usando el objeto que almacenamos en la variable jarvis podemos hacer algunas cosas bastante curiosas usando algunos de los métodos que tiene

    • cancel(): Elimina cualquier reproducción que este en curso.
    • getVoices(): Nos devuelve un listado de todas las voces que soporta el dispositivo.
    • pause(): Pausa la reproducción.
    • resume(): Continua con la reproducción.
    • speak(): Añade un nuevo texto a la cola de reproducción quedando a la espera según vaya vaciando la cola.

Como sucede con el objeto almacenado en jarvis, ahora vamos a ver que podemos hacer por ejemplo con las propiedades del interface almacenado en voice.

    • interface.lang: Podemos modificar y obtener el idioma.
    • interface.pitch: Podemos modificar y obtener el tono de la voz.
    • interface.rate: Podemos modificar y obtener la velocidad de la voz.
    • interface.text: Podemos modificar y obtener el texto que se convierte a voz.
    • interface.voice: Podemos modificar y obtener la voz.
    • interface.volume: Podemos modificar y obtener el volumen de la voz.

Ahora si!!!!, vamos a ampliar nuestro Javascript.

const init = () => {

  // Comprobamos si tenemos soporte en nuestro navegador
  if (!'speechSynthesis' in window) {
    showNotify({
      msn: 'Your browser not support tha Web Speech API',
      show: true,
      type: 'success'
    });
    return false; 
  }
  
  // Interface de la API
  let voice = new SpeechSynthesisUtterance();

  // Objeto de la API
  let jarvis = window.speechSynthesis;

  let disabledPlay = true;

  const store = window.localStorage.getItem('dataSave') || '';
  const text = document.querySelector('#text');
  const btnSave = document.querySelector('#btnSave');
  const btnLoad = document.querySelector('#btnLoad');
  const btnPlay = document.querySelector('#btnPlay');
  const renderText = document.querySelector('.conversation');
  const select = document.getElementById("voices");

  // Controlamos el botón para cargar datos del localstorage
  btnLoad[store ? 'removeAttribute' : 'setAttribute']('disabled', true);

  // Observamos la propiedad 'innerHTML' para observar los cambios que se producen
  const SET = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML').set;
  const handler = {
    set(value) {
      const hasConversation = Object.values(this.classList).includes('conversation') && disabledPlay;
      if (hasConversation) {
        btnPlay.removeAttribute('disabled');
        disabledPlay = false;
      }
      return SET.call(this, value);
    }
  };
  Object.defineProperty(Element.prototype, 'innerHTML', handler);

  text.addEventListener('keydown', function({ code }) {
    if (['Enter', 'NumpadEnter'].includes(code)) {
      const lastWord = this.value.split('\n');
      renderText.innerHTML += `<div>${this.value}</div>`;
      this.value = '';
      // Convertimos el texto a voz
      playVoice(lastWord[lastWord.length - 1]);
    }
  });

  btnSave.addEventListener('click', () => {
    showNotify({
      msn: 'The text has been saved success',
      show: true,
      type: 'success'
    });
    const dataSave = renderText.innerHTML;
    // Guardamos los datos en el localStorage
    storeData(dataSave);
  });

  btnLoad.addEventListener('click', () => {
    showNotify({
      msn: 'The text has been loaded success',
      show: true,
      type: 'success'
    });
    // Recuperamos los daots del localStorage
    const text = storeData('', false);
    renderText.innerHTML = text;
  });

  btnPlay.addEventListener('click', () => {
    // Reproducimos la voz
    pauseVoice();
    stopVoice();
    showNotify({
      msn: 'The text is playing',
      show: true,
      type: 'success'
    });
    playVoice(renderText.innerHTML);
  });

  const playVoice = text => {
    // Reproduce la voz
    voice.text = text;
    jarvis.speak(voice);
  };

  const stopVoice = () => {
    // Cancela la reproducción de la voz
    jarvis.cancel()
  };

  const pauseVoice = () => {
    // Pausa la reproducción de la voz
    jarvis.resume();
  }

  // Obtenemos todas las voces soportadas
  const getVoices = function() {
    const voices = jarvis.getVoices();
    voices.forEach(item => {
      const { name, lang } = item;
      const option = document.createElement('option');
      option.textContent = `${name} - [${lang}]`;
      option.setAttribute('data-language', lang);
      option.setAttribute('data-name', name);
      select.appendChild(option);
    });
    voice.lang = this.selectedOptions?.[0]?.dataset.language.split('-')[0] || 'es';
  };

  getVoices();
  jarvis.onvoiceschanged = getVoices;
  select.addEventListener('input', getVoices);

  // Función extra, no es necesario para el funcionamiento
  const storeData = (data = '', save = true) => {
    return window.localStorage[save ? 'setItem' : 'getItem']('dataSave', data);
  };
};

// Mostra una notificación
const showNotify = ({ msn = '', duration = 3000, show = false, type = '' }) => {
  if (show) {
    const notification = document.querySelector('.notification');
    const bgNotification = ['info', 'warning', 'success'].includes(type) ? type : 'info';
    notification.innerHTML = msn;
    notification.classList.add('show', bgNotification);
    setTimeout(() => {
      notification.classList.remove('show');
    }, duration);
  }
};

document.addEventListener('DOMContentLoaded', init);

Con esto tendríamos una base para entender como funciona y las posibilidades que nos ofrece esta API.

En el código he añadido la opción de guardar y cargar todo el texto que vamos escribiendo en el localStorage, por si en un futuro quisiéramos escuchar el texto que escribimos.

Para finalizar, en Github puedes encontrar el repositorio y desde aquí puedes acceder al ejemplo.