Junho de 2019
Volume 34 – Número 6
[Fala]
Síntese de conversão de texto em fala no .NET
Por Ilia Smirnov | Junho de 2019
Eu costumo ir de avião para a Finlândia para visitar minha mãe. Toda vez que o avião pousa no aeroporto de Vantaa, fico surpreso, pois são poucos os passageiros se dirigem para a saída do aeroporto. A grande maioria parte conexões com destino em toda a Europa Central e Oriental. Não é de admirar, então, que quando o avião começa a descida, haja uma avalanche de anúncios sobre voos de conexão. “Se o seu destino é Tallinn, procure pelo portão 123”, “Para o voo XYZ para São Petersburgo, siga para o portão 234”, e assim por diante. Claro, os comissários de bordo normalmente não falam vários idiomas, então eles usam o inglês, que não é o idioma nativo da maioria dos passageiros. Considerando a qualidade dos sistemas de anúncio público (PA) nas aeronaves, além do ruído da turbina, o choro de bebês e outras perturbações, como as informações poderiam ser transmitidas de forma eficaz?
Bem, cada assento está equipado com fones de ouvido. Muitos, se não todos, os aviões de longa distância têm telas individuais atualmente (e os voos locais têm pelo menos diferentes canais de áudio). E se os passageiros pudessem escolher o idioma para os anúncios e um sistema de computador a bordo permitisse aos comissários criar e enviar mensagens de voz dinâmicas (isto é, mensagens não pré-gravadas)? O principal desafio aqui é a natureza dinâmica das mensagens. É fácil pré-gravar as instruções de segurança, as opções de alimentação e assim por diante, porque elas raramente mudam. Mas precisamos criar mensagens na hora.
Felizmente, existe uma tecnologia bem desenvolvida que pode ajudar: a síntese de conversão de texto em fala (TTS). Raramente observamos esses sistemas, mas eles estão em todos os lugares: anúncios públicos, avisos em centrais de atendimento, dispositivos de navegação, jogos, dispositivos inteligentes e outros aplicativos são exemplos em que instruções pré-gravadas não são suficientes ou que o uso de uma forma de onda digitalizada é prescrito devido limitações de memória (um texto lido por um mecanismo de TTS ocupa muito menos espaço de armazenamento do que uma forma de onda digitalizada).
A síntese de fala baseada em computador não é novidade. As empresas de telecomunicações investiram em TTS para superar as limitações das mensagens pré-gravadas, e pesquisadores militares já experimentaram alertas de voz e alertas para simplificar interfaces de controle complexas. Sintetizadores portáteis também foram desenvolvidos para pessoas com deficiências. Para se ter uma ideia do que esses aparelhos eram capazes de fazer há 25 anos, ouça a faixa “Keep Talking”, do álbum The Division Bell, de 1994, do Pink Floyd, em que Stephen Hawking diz sua famosa frase: “All we need to do is to make sure we keep talking” (Tudo o que precisamos fazer é ter certeza de que continuamos falando).
As APIs de TTS geralmente são fornecidas junto com a contraparte dessa tecnologia — o reconhecimento de fala. Embora você precise da interação efetiva entre humanos e computadores, essa exploração é focada especificamente na síntese de fala. Usarei a API de TTS do Microsoft .NET para criar um protótipo de um sistema de PA para avião. Também vou procurar entender o básico da abordagem de "seleção de unidades" para TTS. E embora eu esteja trabalhando no desenvolvimento de um aplicativo de desktop, os princípios aqui se aplicam diretamente a soluções baseadas em nuvem.
Implemente seu próprio sistema de fala
Antes de criar um protótipo do sistema de anúncios em voo, vamos explorar a API com um programa simples. Inicie o Visual Studio e crie um aplicativo de console. Adicione uma referência ao System.Speech e implemente o método na 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");
}
}
}
Agora basta compilar e executar. Com apenas algumas linhas de código, você replicou a famosa frase Hawking.
Quando você estava digitando o código, o IntelliSense abriu uma janela com todos os métodos e propriedades públicos da classe SpeechSynthesizer. Se você perdeu essa parte, use o atalho de teclado “Ctrl + Espaço” ou “ponto” (ou consulte bit.ly/2PCWpat). O que há de interessante aqui?
Primeiramente, você pode definir diferentes metas de saída. Pode ser um arquivo de áudio ou um fluxo ou até mesmo nulo. Em segundo lugar, você tem tanto a saída síncrona (como no exemplo anterior) quanto a saída assíncrona. Você também pode ajustar o volume e a taxa de fala, pausá-lo, retomá-lo e receber eventos. Você também pode selecionar vozes. Esse recurso é importante aqui, porque você vai usá-lo para gerar resultados em diferentes idiomas. Mas quais vozes estão disponíveis? Vamos descobrir, usando o código da Figura 2.
Figura 2 Código de informações 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();
}
}
}
No meu computador, com Windows 10 Home, a saída resultante da Figura 2 é:
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
Há apenas duas vozes em inglês disponíveis, e quanto a outros idiomas? Bem, cada voz ocupa certo espaço em disco, portanto, elas não são instaladas por padrão. Para adicioná-las, navegue até Iniciar | Configurações | Tempo e idioma | Região e idioma e clique em Adicionar um idioma, certificando-se de selecionar Fala nos recursos opcionais. Embora o Windows dê suporte a mais de 100 idiomas, apenas cerca de 50 dão suporte a TTS. Você pode consultar a lista de idiomas com suporte em bit.ly/2UNNvba.
Após reiniciar o computador, um novo pacote de idioma deve estar disponível. No meu caso, depois de adicionar o russo, obtive uma nova voz instalada:
Id: TTS_MS_RU-RU_IRINA_11.0 | Name: Microsoft Irina Desktop |
Age: Adult | Gender: Female | Culture: ru-RU
Agora você pode retornar para o primeiro programa e adicionar essas duas linhas em vez da chamada de synthesizer.Speak:
synthesizer.SelectVoice("Microsoft Irina Desktop");
synthesizer.Speak("Всё, что нам нужно сделать, это продолжать говорить");
Se você quiser alternar entre os idiomas, você pode inserir chamadas SelectVoice aqui e lá. Mas uma opção melhor é adicionar uma estrutura para fala. Para fazer isso, vamos usar a classe PromptBuilder, conforme mostrado na Figura 3.
Figura 3 A classe 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);
}
}
}
Observe que você precisa chamar EndVoice, caso contrário, você terá um erro de tempo de execução. Além disso, eu usei CultureInfo como outra maneira de especificar o idioma. O PromptBuilder tem muitos métodos úteis, mas quero chamar sua atenção para o AppendTextWithHint. Experimente este código:
var builder = new PromptBuilder();
builder.AppendTextWithHint("3rd", SayAs.NumberOrdinal);
builder.AppendBreak();
builder.AppendTextWithHint("3rd", SayAs.NumberCardinal);
synthesizer.Speak(builder);
Outra maneira de estruturar a entrada e especificar como lê-la é usar o SSML (Speech Synthesis Markup Language), que é uma recomendação de plataforma cruzada desenvolvida pelo Voice Browser Working Group (w3.org/TR/speech-synthesis). Os mecanismos de TTS da Microsoft fornecem suporte abrangente para SSML. Veja como usar:
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);
Observe que ele emprega uma chamada diferente na classe SpeechSynthesizer.
Agora você está pronto para trabalhar no protótipo. Desta vez, crie um novo projeto do Windows Presentation Foundation (WPF). Adicione um formulário e alguns botões para prompts em dois idiomas diferentes. Edite os manipuladores de cliques, conforme mostrado no XAML na Figura 4.
Figura 4 O 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, isso é apenas um pequeno protótipo. Na vida real, o PopulateMessages provavelmente será lido de um recurso externo. Por exemplo, um comissário de bordo pode gerar um arquivo com mensagens em vários idiomas usando o aplicativo que chama um serviço como o Bing Translator (bing.com/translator). O formulário será muito mais sofisticado e gerado dinamicamente com base nos idiomas disponíveis. Haverá tratamento de erros e assim por diante. Mas o ponto aqui é ilustrar a funcionalidade principal.
Desconstrução da fala
Até agora alcançamos nosso objetivo com uma base de código surpreendentemente pequena. Vamos aproveitar a oportunidade para analisar e entender melhor como os mecanismos de TTS funcionam.
Existem muitas abordagens para desenvolver um sistema TTS. Historicamente, os pesquisadores tentaram descobrir um conjunto de regras de pronúncia para construir algoritmos. Se você já estudou uma língua estrangeira, está familiarizado com regras como "Letra c" antes de "e", "i", "y" é pronunciado como "s" como em "cidade", mas antes “a”, “o”, “u”, como “k” como em “carro”. Infelizmente, há tantas exceções e casos especiais, como mudanças de pronúncia em palavras consecutivas, que a construção de um conjunto abrangente de regras se torna difícil. Além disso, a maioria desses sistemas tende a produzir uma voz distinta de "máquina" — imagine um iniciante em uma língua estrangeira pronunciando uma palavra letra por letra.
Para uma fala com um tom mais natural, a pesquisa mudou para sistemas baseados em grandes bancos de dados de fragmentos de voz gravados, e esses mecanismos agora dominam o mercado. Comumente conhecido como seleção de unidade de concatenação de TTS, esses mecanismos selecionam amostras de fala (unidades) com base no texto de entrada e as concatenam em frases. Normalmente, os mecanismos usam o processamento de dois estágios, lembrando muito os compiladores: Primeiramente, analisam a entrada em uma estrutura interna de lista ou árvore com transcrição fonética e metadados adicionais e, em seguida, sintetizam o som com base nessa estrutura.
Como estamos lidando com linguagens naturais, os analisadores são mais sofisticados do que nas linguagens de programação. Portanto, além da geração de tokens (encontrar limites de sentenças e palavras), os analisadores devem corrigir erros de digitação, identificar partes da fala, analisar a pontuação e decodificar abreviações, contrações e símbolos especiais. O resultado apresentado pelo analisador é tipicamente dividido em frases ou sentenças, em grupos que descrevem palavras que agrupam e transportam metadados, como parte da fala, pronúncia, acentuação e assim por diante.
Os analisadores são responsáveis por resolver ambiguidades na entrada. Por exemplo, o que é "Dr."? Significa “doutor” como em "Dr. João" ou é a abreviação de “drive”, como em “pen drive”? E “Dr.” é considerado uma sentença por começar com letra maiúscula e por terminar com ponto final? “Sabia” é um verbo ou o nome de um passarinho? Isso é importante saber, pois a diferença entre os dois é a sílaba tônica.
Essas perguntas nem sempre são fáceis de responder e muitos sistemas de TTS têm analisadores separados para domínios específicos: numerais, datas, abreviações, acrônimos, nomes geográficos, formas especiais de texto, como URLs, e assim por diante. Eles também são específicos por idioma e região. Felizmente, esses problemas têm sido estudados há muito tempo e temos estruturas e bibliotecas bem desenvolvidas para nos ajudar.
O próximo passo é gerar formas de pronúncia, como marcar a árvore com símbolos de som (como transformar “escola” em “es kó la”). Isso é feito por algoritmos especiais de grafema-fonema. Para idiomas como o espanhol, algumas regras relativamente simples podem ser aplicadas. Mas para outros, como o inglês, a pronúncia difere significativamente da forma escrita. Métodos estatísticos são então empregados junto com bancos de dados para palavras conhecidas. Depois disso, um processamento pós-lexical adicional é necessário, porque a pronúncia das palavras pode mudar quando combinadas em uma frase.
Embora os analisadores tentem extrair todas as informações possíveis do texto, há algo tão enganoso que não é extraível: prosódia ou entonação. Ao falar, usamos a prosódia para enfatizar certas palavras, para transmitir emoção e para indicar frases, comandos e perguntas afirmativas. Mas o texto escrito não possui símbolos para indicar prosódia. Claro, a pontuação oferece algum contexto: Uma vírgula significa uma pequena pausa, enquanto um ponto significa uma pausa maior. Já um ponto de interrogação significa que você aumenta sua entonação no final de uma frase. Mas se você já leu uma história de ninar para seus filhos, sabe o quão difícil isso é.
Além disso, é comum que duas pessoas diferentes leiam o mesmo texto de formas diferentes (pergunte a seus filhos quem é melhor em ler histórias de ninar, você ou seu cônjuge). Por causa disso, você não pode usar métodos estatísticos confiáveis, pois diferentes especialistas produzirão rótulos diferentes para o aprendizado supervisionado. Esse problema é complexo e, apesar da pesquisa intensiva, está longe de ser resolvido. O melhor que os programadores podem fazer é usar o SSML, que possui algumas marcações para prosódia.
Redes neurais no TTS
Métodos estatísticos ou de aprendizado de máquina têm sido aplicados por anos em todas as etapas do processamento de TTS. Por exemplo, os modelos ocultos de Markov são usados para criar analisadores que produzem a análise mais provável ou para rotular os bancos de dados de amostras de fala. As árvores de decisão são usadas na seleção de unidades ou em algoritmos grafema-fonema, enquanto as redes neurais e o aprendizado profundo surgiram a pouco tempo na pesquisa TTS.
Podemos considerar uma amostra de áudio como uma série temporal de amostragem de forma de onda. Ao criar um modelo autorregressivo, é possível prever a próxima amostra. Como resultado, o modelo gera propagação de fala, como um bebê aprendendo a falar, imitando sons. Se condicionarmos ainda mais este modelo na transcrição de áudio ou na saída de pré-processamento de um sistema TTS existente, obtemos um modelo parametrizado de fala. A saída do modelo descreve um espectrograma para um vocoder produzindo formas de onda reais. Como esse processo não depende de um banco de dados com amostras registradas, mas é generativo, o modelo tem um pequeno espaço de memória e permite o ajuste de parâmetros.
Como o modelo é treinado em fala natural, o resultado retém todas as suas características, incluindo respiração, tensões e entonação (de modo que as redes neurais podem potencialmente resolver o problema da prosódia). É possível também ajustar o tom, criar uma voz completamente diferente e até imitar o canto.
Enquanto escrevo esse artigo, a Microsoft já está oferecendo sua versão de pré-visualização de uma rede neural de TTS (bit.ly/2PAYXWN). Ele fornece quatro vozes com melhor qualidade e desempenho quase instantâneo.
Geração de fala
Agora que temos a árvore com metadados, voltamos para a geração de fala. Os sistemas originais de TTS tentaram sintetizar sinais combinando sinusóides. Outra abordagem interessante foi o desenvolvimento de um sistema de equações diferenciais descrevendo o trato vocal humano como vários tubos conectados de diferentes diâmetros e comprimentos. Tais soluções são muito compactas, mas infelizmente soam bastante mecânicas. Assim como aconteceu com os sintetizadores musicais, o foco gradualmente mudou para soluções baseadas em amostras, que requerem um espaço significativo, mas essencialmente naturais.
Para construir um sistema desse tipo, você precisa ter muitas horas de gravação de alta qualidade de um ator profissional lendo um texto especialmente desenvolvido. Este texto é dividido em unidades, rotulado e armazenado em um banco de dados. A geração de fala se torna uma tarefa de selecionar unidades adequadas e colocá-las juntas.
Como você não está sintetizando a fala, não é possível ajustar significativamente os parâmetros no tempo de execução. Se você precisa de vozes masculinas e femininas, ou se precisar fornecer sotaques regionais (digamos, portugueses e brasileiros), elas devem ser gravadas separadamente. O texto deve ser desenvolvido com o objetivo de abranger todas as unidades de som possíveis. E os atores devem ler em um tom neutro para facilitar a concatenação.
Divisão e rotulagem também são tarefas não triviais. Isso costumava ser feito manualmente, levando semanas de trabalho tedioso. Felizmente, o aprendizado de máquina agora está sendo aplicado nessa área.
O tamanho da unidade é provavelmente o parâmetro mais importante para um sistema de TTS. Obviamente, usando frases inteiras, poderíamos produzir os sons mais naturais mesmo com a prosódia correta, mas o registro e armazenamento de tantos dados é impossível. Podemos dividir em palavras? Provavelmente, mas quanto tempo levará para um ator ler um dicionário inteiro? E quais limitações de tamanho de banco de dados enfrentaremos? Por outro lado, não podemos simplesmente gravar o alfabeto — isso seria útil somente para algum concurso de soletração. Então, geralmente as unidades são selecionadas como dois grupos de três letras. Elas não são necessariamente sílabas, pois grupos que abrangem fronteiras de sílabas podem ser concatenados muito melhor.
Agora é a última etapa. Tendo um banco de dados de unidades de fala, precisamos lidar com a concatenação. Infelizmente, por mais neutra que a entonação tenha sido na gravação original, as unidades de conexão ainda precisam de ajustes para evitar saltos de volume, frequência e fase. Isso é feito com processamento digital de sinais (DSP). Esse recurso também pode ser usado para adicionar entonação a frases, como aumentar ou diminuir a voz gerada para afirmações ou perguntas.
Conclusão
Neste artigo, abordei apenas a API de .NET. Outras plataformas fornecem funcionalidade semelhante. O MacOS tem o NSSpeechSynthesizer no Cocoa, com recursos comparáveis, e a maioria das distribuições do Linux inclui o mecanismo eSpeak. Todas essas APIs são acessíveis por meio de código nativo, portanto, você precisa usar C#, C++ ou Swift. Para ecossistemas de plataforma cruzada, como o Python, existem algumas pontes como o Pyttsx, mas elas geralmente têm certas limitações.
Os fornecedores de nuvem, por outro lado, têm como alvo públicos amplos e oferecem serviços para os idiomas e plataformas mais populares. Embora a funcionalidade seja comparável entre os fornecedores, o suporte para tags SSML pode diferir, portanto, verifique a documentação antes de escolher uma solução.
A Microsoft oferece um serviço de conversão de texto em fala como parte dos seus Serviços Cognitivos (bit.ly/2XWorku). Ele não só oferece 75 vozes em 45 idiomas, mas também permite que você crie suas próprias vozes. Para isso, o serviço precisa de arquivos de áudio com uma transcrição correspondente. Você pode escrever seu texto primeiro e depois pedir para alguém lê-lo, ou pegar uma gravação existente e escrever sua transcrição. Depois de carregar esses conjuntos de dados no Azure, um algoritmo de aprendizado de máquina treina um modelo para sua própria "fonte de voz" exclusiva. Um bom guia passo-a-passo pode ser encontrado em bit.ly/2VE8th4.
Uma maneira muito conveniente de acessar o Cognitive Speech Services é usando o Speech Software Development Kit (bit.ly/2DDTh9I). Ele fornece suporte ao reconhecimento de fala e síntese de fala e está disponível para todas as principais plataformas desktop e móveis, e para os idiomas mais conhecidos. No GitHub você encontra toda a documentação e vários códigos de exemplo.
TTS continua a ser de grande ajuda para pessoas com necessidades especiais. Por exemplo, acesse o linka.su, um site criado por um talentoso programador com paralisia cerebral para ajudar pessoas com distúrbios de fala e musculoesqueléticos, autismo ou aqueles que estão se recuperando de um derrame. Sabendo por experiência própria que limitações eles enfrentam, o autor criou uma série de aplicativos para pessoas que não conseguem digitar em um teclado comum, pessoas podem selecionar apenas uma letra por vez ou apenas tocar em imagens no tablet. Graças a TTS, ele literalmente dá voz àqueles que não têm uma. Eu gostaria que todos nós, como programadores, pudéssemos ser úteis para as outras pessoas.
Ilia Smirnov tem mais de 20 anos de experiência no desenvolvimento de aplicativos corporativos nas principais plataformas, principalmente em Java e .NET. Na última década, ele se especializou em simulação de riscos financeiros. Ele tem três mestrados, FRM e outras certificações profissionais.
Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Sheng Zhao (Sheng.Zhao@microsoft.com)
Sheng Zhao é o principal engenheiro de software do grupo de STCA Speech em Pequim