Junio de 2019

Volumen 34, número 6

[Speech]

Síntesis de texto a voz en .NET

Por Ilia Smirnov | Junio de 2019

Suelo volar a Finlandia para visitar a mi madre. Cada vez que el avión aterriza en el aeropuerto de Vantaa, me sorprende la cantidad de pasajeros que se dirigen a la salida del aeropuerto. La gran mayoría tienen conexiones a destinos de toda Europa Central y Oriental. No es de extrañar, pues, que cuando el avión inicia su descenso, comience un alud de anuncios sobre conexiones. "Si su destino es Tallin, busque la puerta 123", "Para el vuelo XYZ a San Petersburgo, diríjase a la puerta 234", etc. Por supuesto, los tripulantes de cabina de pasajeros no suelen hablar una decena de idiomas, por lo que usan el inglés, que no es el idioma nativo de la mayoría de los pasajeros. Teniendo en cuenta la calidad de los sistemas de anuncios públicos (PA) de las aerolíneas, además del ruido de los motores, el llanto de los bebés y otras perturbaciones, ¿cómo se puede transmitir la información de manera eficaz?

Bueno, cada puesto está equipado con auriculares. Muchos aviones de larga distancia, si no todos, tienen pantallas individuales hoy en día (y los locales tienen al menos distintos canales de audio). ¿Qué ocurriría si un pasajero pudiera elegir el idioma de los anuncios y un sistema informático a bordo permitiera a los tripulantes de cabina de pasajeros crear y enviar mensajes de voz dinámicos (es decir, no pregrabados)? Aquí, el principal desafío es la naturaleza dinámica de los mensajes. Es fácil registrar previamente las instrucciones de seguridad, las opciones de catering, etc, porque se actualizan con poca frecuencia. Pero tenemos que crear mensajes literalmente sobre la marcha.

Afortunadamente, existe una tecnología madura que resultar útil: la síntesis de texto a voz (TTS). Rara vez vemos estos sistemas, pero están muy extendidos: anuncios públicos, indicaciones en centros de llamadas, dispositivos de navegación, juegos, dispositivos inteligentes y otras aplicaciones son ejemplos donde los mensajes pregrabados no son suficientes o el uso de una forma de onda digitalizada se excluye debido a las limitaciones de memoria (un texto leído por un motor TTS ocupa mucho menos que el almacenamiento de una forma de onda digitalizada).

La síntesis de voz asistida por PC es una tecnología prácticamente nueva. La empresas de telecomunicaciones han invertido en TTS para superar las limitaciones de los mensajes pregrabados. Los investigadores militares han experimentado con mensajes y alertas de voz para simplificar las interfaces de control complejas. Asimismo, se han desarrollado sintetizadores portátiles para personas con discapacidades. Para que se haga una idea de lo que estos dispositivos eran capaces hace 25 años, escuche la pista "Keep Talking" del álbum "The Division Bell" de Pink Floyd de 1994, donde Stephen Hawking decía su famosa frase: "All we need to do is to make sure we keep talking" (solo debemos asegurarnos de seguir hablando).

Las API de TTS suelen proporcionarse junto con su "opuesto", el reconocimiento de voz. Aunque ambos son necesarios para la interacción hombre-máquina efectiva, esta exploración se centra específicamente en la síntesis de voz. Para crear un prototipo de sistema PA para una aerolínea, usaré la API de TTS de Microsoft .NET. También investigaré los conceptos básicos del enfoque "selección de unidad" para TTS. Aunque explicaré la creación de una aplicación de escritorio, aquí los principios se aplican directamente a las soluciones basadas en la nube.

Implementar un sistema de voz propio

Antes de crear un prototipo de sistema de anuncios de vuelo, vamos a explorar la API con un sencillo programa. Inicie Visual Studio y cree una aplicación de consola. Agregue una referencia a System.Speech e implemente el método de la Figura 1.

Figura 1 Método System.Speech.Synthesis

using System.Speech.Synthesis;
namespace KeepTalking
{
  class Program
  {
    static void Main(string[] args)
    {
      var synthesizer = new SpeechSynthesizer();
      synthesizer.SetOutputToDefaultAudioDevice();
      synthesizer.Speak("All we need to do is to make sure we keep talking");
    }
  }
}

Ahora realice la compilación y ejecútela. Con unas pocas líneas de código, habrá replicado la famosa frase de Hawking.

Mientras escribía este código, IntelliSense abrió una ventana con todos los métodos públicos y las propiedades de la clase SpeechSynthesizer. Si lo omitió, use "Control-barra espaciadora" o el método abreviado de teclado "punto" (o bien consulte bit.ly/2PCWpat). ¿Qué es interesante aquí?

En primer lugar, puede establecer distintos destinos de salida. Puede ser un archivo de audio, un flujo o incluso null. En segundo lugar, tiene ambas salidas sincrónica (como se muestra en el ejemplo anterior) y asincrónica. También puede ajustar el volumen y la velocidad de la voz, pausarla y reanudarla, y recibir eventos. También puede seleccionar voces. Esta característica es importante porque la usará para generar la salida en distintos idiomas. Pero, ¿qué voces están disponibles? Veámoslo utilizando el código de la Figura 2.

Figura 2 Código de información de voz

using System;
using System.Speech.Synthesis;
namespace KeepTalking
{
  class Program
  {
    static void Main(string[] args)
    {
      var synthesizer = new SpeechSynthesizer();
      foreach (var voice in synthesizer.GetInstalledVoices())
      {
        var info = voice.VoiceInfo;
        Console.WriteLine($"Id: {info.Id} | Name: {info.Name} |
          Age: {info.Age} | Gender: {info.Gender} | Culture: {info.Culture}");
      }
      Console.ReadKey();
    }
  }
}

En mi máquina con Windows 10 Home, la salida resultante de la Figura 2 es la siguiente:

Id: TTS_MS_EN-US_DAVID_11.0 | Name: Microsoft David Desktop |
  Age: Adult | Gender: Male | Culture: en-US
Id: TTS_MS_EN-US_ZIRA_11.0 | Name: Microsoft Zira Desktop |
  Age: Adult | Gender: Female | Culture: en-US

Solo hay dos voces en inglés disponibles. ¿Qué tenemos para otros idiomas? Bien, cada voz ocupa algo de espacio en disco, por lo que no están instalados de forma predeterminada. Para agregarlos, vaya a Inicio | Configuración | Hora e idioma | Región e idioma, y haga clic en Agregar un idioma. Asegúrese de seleccionar Voz en las características opcionales. Mientras que Windows admite más de 100 idiomas, solo unos 50 admiten TTS. Puede revisar la lista de idiomas admitidos en bit.ly/2UNNvba.

Después de reiniciar el equipo, debería tener un nuevo paquete de idioma disponible. En mi caso, después de agregar el paquete de ruso, tengo una nueva voz instalada:

Id: TTS_MS_RU-RU_IRINA_11.0 | Name: Microsoft Irina Desktop |
  Age: Adult | Gender: Female | Culture: ru-RU

Ahora puede volver al primer programa y agregar estas dos líneas en lugar de la llamada a synthesizer.Speak:

synthesizer.SelectVoice("Microsoft Irina Desktop");
synthesizer.Speak("Всё, что нам нужно сделать, это продолжать говорить");

Si desea cambiar entre idiomas, puede insertar llamadas a SelectVoice aquí y allá. Pero es mejor agregar alguna estructura a la voz. Para ello, vamos a usar la clase PromptBuilder, como se muestra en la Figura 3.

Figura 3 Clase PromptBuilder

using System.Globalization;
using System.Speech.Synthesis;
namespace KeepTalking
{
  class Program
  {
    static void Main(string[] args)
    {
      var synthesizer = new SpeechSynthesizer();
      synthesizer.SetOutputToDefaultAudioDevice();
      var builder = new PromptBuilder();
      builder.StartVoice(new CultureInfo("en-US"));
      builder.AppendText("All we need to do is to keep talking.");
      builder.EndVoice();
      builder.StartVoice(new CultureInfo("ru-RU"));
      builder.AppendText("Всё, что нам нужно сделать, это продолжать говорить");
      builder.EndVoice();
      synthesizer.Speak(builder);
    }
  }
}

Tenga en cuenta que debe llamar a EndVoice. De lo contrario, obtendrá un error en tiempo de ejecución. También usé CultureInfo como otro método para especificar un idioma. PromptBuilder tiene muchos métodos útiles, pero quiero que se centre en AppendTextWithHint. Pruebe este código:

var builder = new PromptBuilder();
builder.AppendTextWithHint("3rd", SayAs.NumberOrdinal);
builder.AppendBreak();
builder.AppendTextWithHint("3rd", SayAs.NumberCardinal);
synthesizer.Speak(builder);

Otra manera de estructurar la entrada y especificar cómo leerla es usar el lenguaje de marcado de síntesis de voz (SSML), que es una recomendación multiplataforma desarrollada por el grupo internacional Voice Browser Working Group (w3.org/TR/speech-synthesis). Los motores de TTS de Microsoft ofrecen soporte técnico completo para SSML. Aquí vemos cómo usarlo:

string phrase = @"<speak version=""1.0""
  http://www.w3.org/2001/10/synthesis""
  xml:lang=""en-US"">";
phrase += @"<say-as interpret-as=""ordinal"">3rd</say-as>";
phrase += @"<break time=""1s""/>";
phrase += @"<say-as interpret-as=""cardinal"">3rd</say-as>";
phrase += @"</speak>";
synthesizer.SpeakSsml(phrase);

Tenga en cuenta que emplea una llamada diferente en la clase SpeechSynthesizer.

Ahora está listo para trabajar en el prototipo. Esta vez cree un nuevo proyecto de Windows Presentation Foundation (WPF). Agregue un formulario y un par de botones para los mensajes en dos idiomas distintos. A continuación, agregue controladores de clic, tal como se aprecia en la Figura 4.

Figura 4 Código XAML

using System.Collections.Generic;
using System.Globalization;
using System.Speech.Synthesis;
using System.Windows;
namespace GuiTTS
{
  public partial class MainWindow : Window
  {
    private const string en = "en-US";
    private const string ru = "ru-RU";
    private readonly IDictionary<string, string> _messagesByCulture =
      new Dictionary<string, string>();
    public MainWindow()
    {
      InitializeComponent();
      PopulateMessages();
    }
    private void PromptInEnglish(object sender, RoutedEventArgs e)
    {
      DoPrompt(en);
    }
    private void PromptInRussian(object sender, RoutedEventArgs e)
    {
      DoPrompt(ru);
    }
    private void DoPrompt(string culture)
    {
      var synthesizer = new SpeechSynthesizer();
      synthesizer.SetOutputToDefaultAudioDevice();
      var builder = new PromptBuilder();
      builder.StartVoice(new CultureInfo(culture));
      builder.AppendText(_messagesByCulture[culture]);
      builder.EndVoice();
      synthesizer.Speak(builder);
    }
    private void PopulateMessages()
    {
      _messagesByCulture[en] = "For the connection flight 123 to
        Saint Petersburg, please, proceed to gate A1";
      _messagesByCulture[ru] =
        "Для пересадки на рейс 123 в  Санкт-Петербург, пожалуйста, пройдите к выходу A1";
    }
  }
}

Obviamente, se trata solo de un pequeño prototipo. En la vida real, la lectura de PopulateMessages probablemente se realizará desde un recurso externo. Por ejemplo, un tripulante de cabina de pasajeros puede generar un archivo con los mensajes en varios idiomas mediante una aplicación que llame a un servicio como Traductor de Bing (bing.com/translator). El formulario será mucho más sofisticado y se generará de forma dinámica en función de los idiomas disponibles. Habrá control de errores, etc. Pero lo principal aquí es ilustrar la funcionalidad básica.

Deconstrucción de la voz

Hasta ahora, hemos logrado nuestro objetivo con una base de código increíblemente pequeña. Aprovechemos la oportunidad de profundizar y comprender mejor cómo funcionan los motores de TTS.

Existen muchos enfoques para construir un sistema TTS. Históricamente, los investigadores han intentado descubrir un conjunto de reglas de pronunciación para crear algoritmos. Si alguna vez ha estudiado un idioma extranjero, está familiarizado con reglas como "la letra 'c' antes de 'e', 'i' e 'y' se pronuncia como 's', como en"city", pero antes de 'a', 'o' y 'u' se pronuncia como 'k', como en 'cat'". Lamentablemente, hay tantos casos especiales y excepciones, como cambios de pronunciación en palabras consecutivas, que es difícil construir un conjunto de reglas completo. Además, la mayoría de estos sistemas tienden a producir una voz "automática"distinta. Imagine un principiante en un idioma extranjero pronunciando una palabra letra a letra.

Para conseguir una voz que suene más natural, la investigación ha cambiado a sistemas basados en grandes bases de datos de fragmentos de voz grabados. Estos motores ahora dominan el mercado. Estos motores, que normalmente se conocen TTS de selección de unidad de concatenación, seleccionan ejemplos de voz (unidades) según el texto de entrada y los concatenan en frases. Normalmente, los motores usan el procesamiento en dos fases, que funciona de manera muy parecida a los compiladores: En primer lugar, analizan la entrada en una lista interna o una estructura de árbol con transcripción fonética y metadatos adicionales y, a continuación, sintetizan sonido en función de esta estructura.

Dado que tratamos con lenguajes naturales, los analizadores son más sofisticados que con los lenguajes de programación. Por tanto, más allá de tokenización (búsqueda de límites de palabras y frases), los analizadores deben corregir errores tipográficos, identificar categorías gramaticales, analizar los signos de puntuación y descodificar abreviaturas, contracciones y símbolos especiales. Normalmente, la salida del analizador se divide en frases u oraciones, y se agrupa en colecciones, que describen palabras que se agrupan y contienen metadatos, como la categoría gramatical, la pronunciación, el acento, etc.

Los analizadores son responsables de la resolución de ambigüedades en la entrada. Por ejemplo, ¿a qué hace referencia "Dr."? ¿Significa "doctor", como en "Dr. Smith", o "unidad", como en "unidad privada?" ¿Es "Dr." una frase porque comienza con mayúscula y termina con un punto? ¿Es "project" un verbo (proyectar) o un sustantivo (proyecto)? Es importante saberlo porque el acento recae en sílabas diferentes.

Estas preguntas no siempre son fáciles de responder. Muchos sistemas TTS tienen analizadores diferentes para dominios específicos: números, fechas, abreviaturas, acrónimos, topónimos, formatos de texto especiales, como URL, etc. También son específicos del idioma y de la región. Afortunadamente, hace mucho tiempo que se estudian estos problemas y tenemos marcos y bibliotecas bien desarrollados en los que apoyarnos.

El paso siguiente consiste en generar formas de pronunciación, como etiquetar el árbol con símbolos de sonido (por ejemplo, transformar "school" en "s k uh l"). Para ello, se usan algoritmos especiales de grafema a fonema. Para idiomas como el español, se pueden aplicar algunas reglas relativamente sencillas. Pero en otros casos, como el inglés, la pronunciación difiere significativamente de la forma escrita. En este caso se usan métodos estadísticos con bases de datos de palabras conocidas. Después, se requiere procesamiento posterior al léxico adicional, porque la pronunciación de palabras puede cambiar cuando se combina en una oración.

Aunque los analizadores intentan extraer toda la información posible del texto, existe algo tan impreciso que no es extraíble: la prosodia o entonación. Al hablar, usamos la prosodia para enfatizar ciertas palabras, transmitir emociones o emitir frases afirmativas, órdenes y preguntas. Pero el texto escrito no tiene símbolos para expresar la prosodia. Es cierto que los signos de puntuación ofrecen algo de contexto: Una coma significa una ligera pausa, mientras que un punto inserta una más prolongada y un signo de interrogación implica una mayor entonación hacia el final de una frase. Pero si alguna vez ha leído a sus hijos un cuento antes de acostarse, sabe cuanto se alejan estas reglas de la lectura real.

Además, dos personas diferentes suelen leer de forma diferente el mismo texto (pregunte a sus hijos quién leer mejor los cuentos, usted o su cónyuge). Por este motivo, no puede usar métodos estadísticos de forma confiable, ya que distintos expertos generarán etiquetas diferentes para el aprendizaje supervisado. Este problema es complejo y, a pesar de la investigación intensiva, su resolución queda lejos. Lo mejor que pueden hacer los programadores es usar SSML, que tiene algunas etiquetas para la prosodia.

Redes neuronales en TTS

Los métodos de Machine Learning o estadísticos hace años que se aplican en todas las fases del procesamiento de TTS. Por ejemplo, los modelos ocultos de Markov se usan para crear analizadores que produzcan el análisis más probable o para realizar el etiquetado de bases de datos de ejemplos de voz. Los árboles de decisión se usan en la selección de unidad o en los algoritmos de grafema a fonema, mientras que las redes neuronales y el aprendizaje profundo han surgido en la vanguardia de la investigación de TTS.

Podemos considerar que un ejemplo de audio es una serie temporal de muestreo en forma de onda. Con la creación de un modelo de regresión automática, es posible predecir el siguiente ejemplo. Como resultado, el modelo genera la propagación del tipo de voz, como un bebé que aprende a hablar imitando sonidos. Si condicionamos aún más este modelo en la transcripción de audio o la salida de preprocesamiento de un sistema TTS existente, obtenemos un modelo de voz parametrizado. La salida del modelo describe un espectrograma para un vocodificador que genera formas de onda reales. Dado que este proceso no depende de una base de datos con ejemplos grabados, sino que es generativo, el modelo tiene una pequeña superficie de memoria y permite ajustar los parámetros.

Dado que el modelo está entrenado con voz natural, la salida conserva todas sus características, incluidas la respiración, la acentuación y la entonación (de modo que las redes neuronales pueden resolver el problema de la prosodia). También es posible ajustar el tono, crear una voz completamente diferente e incluso imitar el canto.

En el momento de redactar este artículo, Microsoft ofrece la versión preliminar de un sistema TTS de red neuronal (bit.ly/2PAYXWN). Proporciona cuatro voces con calidad mejorada y un rendimiento casi instantáneo.

Generación de voz

Ahora que tenemos el árbol con metadatos, nos centraremos en la generación de voz. Los sistemas TTS originales intentaban sintetizar señales mediante la combinación de sinusoides. Otro enfoque interesante era construir un sistema de ecuaciones diferenciales que describiera el tracto vocal humano como varios tubos de diferentes diámetros y longitudes conectados. Estas soluciones son muy compactas, pero lamentablemente parecen bastante mecánicas. Por lo tanto, al igual que con los sintetizadores musicales, el foco se desplaza gradualmente a soluciones basadas en ejemplos, que requieren un espacio considerable, pero suenan prácticamente naturales.

Para crear un sistema de este tipo, debe tener muchas horas de grabaciones de alta calidad de un actor profesional que lea texto construido especialmente. Este texto se divide en unidades, se etiqueta y se almacena en una base de datos. La generación de voz se convierte en una tarea de selección y unión de las unidades adecuadas.

Dado que no está sintetizando la voz, no puede ajustar significativamente los parámetros en tiempo de ejecución. Si necesita voces de hombre y mujer o debe proporcionar acentos regionales (por ejemplo, gaélico o irlandés), deben registrarse por separado. El texto debe crearse para cubrir todas las unidades de sonido posibles que necesitará. Los actores deben leer en un tono neutro para facilitar la concatenación.

La división y el etiquetado también son tareas no triviales. Se solían realizar manualmente y suponían semanas de trabajo tedioso. Afortunadamente, ahora se aplica la tecnología Machine Learning a este proceso.

El tamaño de la unidad es probablemente el parámetro más importante para un sistema TTS. Obviamente, al usar oraciones completas, podíamos crear los sonidos más naturales incluso con la prosodia adecuada, pero es imposible grabar y almacenar tal cantidad de datos. ¿Se puede dividir en palabras? Probablemente, pero ¿cuánto tardaría un actor en leer un diccionario completo? ¿Y a qué limitaciones de tamaño de base de datos nos enfrentamos? Por otro lado, no podemos grabar simplemente el alfabeto. Eso sería suficiente solo para un concurso de ortografía. Por tanto, las unidades suelen seleccionarse como dos grupos de tres letras. No son necesariamente sílabas, ya que los grupos que abarcan límites de sílabas pueden unirse mucho mejor.

Ahora viene el último paso. Al tener una base de datos de unidades de voz, tenemos que lidiar con la concatenación. Independientemente de la neutralidad de la entonación en la grabación original, la conexión de unidades sigue exigiendo ajustes para evitar saltos en la fase, la frecuencia y el volumen. Esto se lleva a cabo mediante el procesamiento de señales digital (DSP). También puede usarse para agregar algo de entonación a las frases, como, por ejemplo, subir o bajar el tono de la voz generada para aserciones o preguntas.

Resumen

En este artículo solo traté solo la API de .NET. Otras plataformas proporcionan una funcionalidad similar. MacOS tiene el objeto NSSpeechSynthesizer en la interfaz Cocoa con funcionalidades comparables. La mayoría de las distribuciones de Linux incluyen el motor eSpeak. Todas estas API son accesibles a través de código nativo, por lo que debe usar C#, C++ o Swift. Para los ecosistemas multiplataforma como Python, existen algunos puentes, como Pyttsx, pero normalmente presentan ciertas limitaciones.

Los proveedores de nube, por otro lado, se dirigen a públicos amplios, y ofrecen servicios para plataformas y lenguajes más populares. Aunque la funcionalidad es comparable entre proveedores, la compatibilidad con las etiquetas SSML pueden diferir, por lo que debe consultar la documentación antes de elegir una solución.

Microsoft ofrece un servicio de texto a voz como parte de Cognitive Services (bit.ly/2XWorku). No solo le proporciona 75 voces en 45 idiomas, sino que también le permite crear las suyas propias. Para ello, el servicio necesita archivos de audio con la transcripción correspondiente. Puede escribir el texto en primer lugar y luego pedir a alguien que lo lea, o bien tomar una grabación existente y escribir su transcripción. Después de cargar estos conjuntos de datos en Azure, un algoritmo de Machine Learning entrena un modelo para su propia "fuente de voz" única. Puede consultar una buena guía paso a paso en bit.ly/2VE8th4.

Una manera muy práctica de acceder a los Servicios de Voz de Cognitive Services es mediante el SDK Speech Software Development Kit (bit.ly/2DDTh9I). Es compatible con el reconocimiento de voz y la síntesis de voz, y está disponible para las principales plataformas de escritorio y móviles, así como los lenguajes más populares. Está bien documentada y existen numerosos ejemplos de código en GitHub.

TTS sigue siendo una ayuda enorme para personas con necesidades especiales. Por ejemplo, consulte linka.su, un sitio web creado por un programador experto con parálisis cerebral para ayudar a personas con trastornos del habla y musculoesqueléticos, con autismo o en proceso de recuperación de un derrame cerebral. Al saber por experiencia personal a qué limitaciones se enfrentaban, el autor creó una gama de aplicaciones para personas que no pueden escribir con un teclado normal o que solo pueden seleccionar una letra a la vez o tocar una imagen en una tableta. Gracias a TTS, da voz literalmente a todos aquellos que no la tienen. Espero que todos, como programadores, podamos ser tan útiles para los demás.


Ilia Smirnov tiene más de 20 años de experiencia en el desarrollo de aplicaciones empresariales en las principales plataformas, principalmente en Java y .NET. Durante la última década, se ha especializado en la simulación de riesgos financieros. Posee tres másteres, FRM y otras certificaciones profesionales.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Sheng Zhao (Sheng.Zhao@microsoft.com)
Sheng Zhao es administrador de ingeniería de software de grupo principal en STCA Speech en Beijing


Comente este artículo en el foro de MSDN Magazine