.NET 的文本转语音合成

我经常飞去芬兰见我的妈妈。每次飞机降落在万塔机场时,我都会对鲜有旅客前往机场出口感到惊讶。绝大多数的旅客会转机到跨越所有中欧及东欧的目的地。所以难怪在飞机开始下降时,会发出一大堆有关转机的公告。“如果你的目的地是塔林,请到 123 号登机口登机”,“如果是飞往圣彼德堡的 XYZ 次航班,请到 234 号登机口登机”等。当然,乘务员通常不会讲十几种语言,因此他们使用英语,而英语不是大多数旅客的本地语言。鉴于客机上的公告 (PA) 系统的质量,以及引擎噪音、哭闹的婴儿和其他干扰,如何有效地传达信息?

嗯,每个座位都配备有耳机。现在,即使不是全部,但起码有很多长途飞机配备有单独的屏幕(当地飞机至少具有不同的音频通道)。如果旅客可以选择公告的语言,并且载入计算机系统允许乘务员创建和发送动态(即,非预先录制)语音消息会怎么样?此处的关键挑战是消息的动态特性。预先录制安全说明、饮食选项等非常简单,因为它们很少更新。但实际上,我们需要动态创建消息。

幸运的是,有一种成熟的技术可提供帮助:文本转语音合成 (TTS)。我们很少注意到此类系统,但它们无处不在:公告、呼叫中心的提示、导航设备、游戏、智能设备和其他应用程序都是预先录制的提示无法满足需求或由于内存限制而禁止使用数字化波形(由 TTS 引擎读取的文本远小于数字化波形)的示例。

基于计算机的语音合成已经不是什么新鲜事了。电信公司对 TTS 进行了投资来克服预先录制的消息的限制,并且军事研究人员试用了语音提示和警报来简化复杂的控制接口。同样,还为残障人士开发了便携式合成器。如果你想知道此类设备在 25 年前有什么作用,可以听一听 1994 年 Pink Floyd 专辑“藩篱之钟”中的“Keep Talking”曲目,其中包含 Stephen Hawking 的名言:“我们需要做的只是确保我们一直在说话”。

TTS API 通常与其“对立面”(语音识别)一起提供。虽然需要上述两者来实现有效的人机交互,但此次探索侧重于语音合成。我将使用 Microsoft .NET TTS API 构建客机 PA 系统的原型。我还会深入研究以了解 TTS 的“单位选择”方法的基础知识。当我介绍桌面应用程序的构造时,此处的原则直接适用于基于云的解决方案。

自创语音系统

在构建飞行公告系统的原型之前,让我们通过一个简单的程序来探讨一下 API。启动 Visual Studio 并创建控制台应用程序。添加对 System.Speech 的引用并实现图 1 中的方法。

图 1 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");
    }
  }
}

立即编译并运行。只需几行代码即可复制 Hawking 的名言。

键入此代码后,IntelliSense 打开了包含 SpeechSynthesizer 类的所有公共方法和属性的窗口。如果你错过了它,请使用“Control-Space”或“dot”键盘快捷方式(或访问 bit.ly/2PCWpat)。有何有趣之处?

首先,你可以设置不同的输出目标。它可以是音频文件或流,甚至是 null。其次,你同时具有同步输出(如上一个示例中所示)和异步输出。你还可以调整语音音量和语速,对其进行暂停和继续,以及接收事件。你还可以选择语音。此功能在这里很重要,因为你将能够以不同的语言生成输出。但哪些语音可用呢?让我们使用图 2 中的代码了解详情。

图 2 语音信息代码

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();
    }
  }
}

在具有 Windows 10 家庭版的计算机上,图 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

只有两个英语语音可用,其他语言怎么样呢?嗯,每种语音都需要一些磁盘空间,因此默认情况下不会安装这些语音。若要添加它们,请导航到“开始”|“设置”|“时间和语言”|“区域和语言”,然后单击“添加语言”,确保在可选功能中选择“语音”。虽然 Windows 支持 100 多种语言,但只有大约 50 种语言支持 TTS。可以在 bit.ly/2UNNvba 中查看支持的语言列表。

重新启动计算机后,应提供新的语言包。在本例中,添加俄语后,会安装有新的语音:

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

现在可以返回到第一个程序并添加以下两行,而不是 synthesizer.Speak 调用:

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

如果你想要切换语言,可到处插入 SelectVoice 调用。但最好将某种结构添加到语音。为此,让我们使用 PromptBuilder 类,如图 3 所示。

图 3 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);
    }
  }
}

请注意,必须调用 EndVoice,否则会收到运行时错误。此外,我使用 CultureInfo 作为指定语言的另一种方法。PromptBuilder 有许多有用的方法,但我希望你将注意力集中在 AppendTextWithHint 上。尝试以下代码:

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

安排输入并指定如何读出该输入的另一种方法是使用语音合成标记语言 (SSML),这是由国际语音浏览器工作组提出的跨平台建议 (w3.org/TR/speech-synthesis)。Microsoft TTS 引擎提供了对 SSML 的全面支持。以下就是使用方法:

string phrase = @"<speak version=""1.0""
  xmlns=""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);

请注意,它使用 SpeechSynthesizer 类上的不同调用。

现在你已准备好构建原型。这次创建一个新的 Windows Presentation Foundation (WPF) 项目。为两种不同语言的提示添加一个窗体和几个按钮。然后如图 4 中的 XAML 所示添加单击处理程序。

图 4 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";
    }
  }
}

显然,这只不过是一个极小的原型。在现实生活中,可能会从外部资源读取 PopulateMessages。例如,乘务员可以使用调用必应在线翻译 (bing.com/translator) 等服务的应用程序生成包含多种语言的消息的文件。窗体将更为复杂,且基于可用的语言动态生成。其中包含错误处理等。但重点是阐述核心功能。

析构语音

到目前为止我们已实现目标,获得了相当小的代码库。让我们借此机会来深入研究并更好地了解 TTS 引擎的工作原理。

有许多方法可以构造 TTS 系统。以前,研究人员已尝试探索构建算法所依据的一组发音规则。如果你学习过外语,那么你会熟悉“‘e’、‘i’、‘y’ 之前的字母 ‘c’ 发音为 ‘city’ 中的 ‘s’,但 ‘a’、‘o’、’u’ 之前的字母 ‘c’ 发音为 ‘cat’ 中的 ‘k’”等规则。但是,存在很多例外和特殊情况(例如,连词中的发音变化),因此构造一系列全面的规则非常困难。此外,大多数此类系统往往会生成不同的“机器”语音(设想一下外语初学者按字母逐个读出单词)。

为了获得发音更自然的语音,研究已转向基于录制语音片段的大型数据库的系统,这些引擎现在已占领市场。这些引擎通常称为连接单位选择 TTS,它们基于输入文本选择语音样本(单位)并将其连接到短语中。通常情况下,引擎使用与编译器非常相似的两阶段处理方式:首先,将输入分析到包含音标和其他元数据的内部列表或树型结构中,然后基于此结构合成声音。

由于我们处理的是自然语言,因此其分析器会比编程语言的分析器更复杂。因此除了词汇切分(查找句子和单词的边界)之外,分析器还必须更正拼写错误、识别词类、分析标点符号,以及解码缩写形式、缩约形式和特殊符号。分析器输出通常按短语或句子拆分,并形成描述对词类、标点符号、重音等元数据进行分组和执行的单词的集合。

分析器负责解决输入中的歧义。例如,“Dr.”是什么?是“Dr. Smith”中的“doctor”,还是“Privet Drive”中的“drive”?“Dr.”以大写字母开头并以句点结尾,那么它是一个句子吗?“project”是名词还是动词?因为重音会在不同的音节上,所以知道这一点非常重要。

这些问题并不总是容易回答,并且许多 TTS 系统对特定域使用不同的分析器:数字、日期、缩写、首字母缩略词、地理名称、URL 等文本的特殊形式。它们也特定于语言和区域。幸运的是,我们已对此类问题研究了很长一段时间,因此可以依靠相当成熟的框架和库。

下一步是生成发音形式,例如使用音标标记树(例如,将“school”转换为“s k uh l”)。这是通过特殊的字形转音素算法完成的。对于西班牙语等语言,可以应用一些相对简单的规则。但对于其他语言(例如英语),发音与书写形式大不相同。然后使用统计方法以及已知单词的数据库。之后,需要额外的后置词汇处理,因为在单词组合为一个句子时,其发音可能会发生变化。

虽然分析器尝试从文本中提取所有可能的信息,但有些内容难以提取:韵律或声调。说话时,我们使用韵律强调某些单词,以便传达情绪并表明肯定句、祈使句和疑问句。但书写文本没有用于表明韵律的符号。当然,标点符号提供一些上下文:逗号表示轻微的暂停,而句号表示更长的暂停,问号表示将声调提高至句子末尾。但如果你曾为孩子读过睡前故事,那么你会知道这些规则在实际阅读中的影响有多大。

此外,两个不同的人通常会以不同的方式阅读相同的文本(询问你的孩子谁最擅长阅读睡前故事,你还是你的配偶)。因此统计方法没那么可靠,不同的专家将为监督学习生成不同的标签。此问题非常复杂,尽管进行了深入研究,但还远远不能得到解决。最佳程序员可以执行的操作是使用 SSML,它对韵律进行了一些标记。

TTS 中的神经网络

统计或机器学习方法多年以来一直应用于 TTS 处理的所有阶段。例如,隐马尔可夫模型用于创建分析器,生成最可能的分析,或为语音样本数据库执行标记。决策树用于单位选择或字形转音素算法,而神经网络和深度学习已处在 TTS 研究的最前沿。

我们可以将音频样本视为波形采样的时序。通过创建自动回归模型,就可以预测下一个样本。因此,该模型生成类似说话的发音,就像婴儿通过模拟声音来学说话一样。如果我们在音频脚本或来自现有 TTS 系统的预处理输出中进一步对此模型设定条件,我们会获得语音的参数化模型。该模型的输出说明生成实际波形的声码器的声谱图。由于此过程不依赖于具有录制的样本的数据库(但它是生成式的),因此该模型具有较小的内存占用量并允许调整参数。

由于该模型是根据自然语音训练的,因此输出将保留其所有特征,包括呼吸、重音和声调(因此,神经网络将可能解决韵律问题)。还可以调整音调,创建完全不同的声音,甚至模拟唱歌。

在撰写本文时,Microsoft 将提供神经网络 TTS 的预览版本 (bit.ly/2PAYXWN)。提供的四种声音具有增强的质量和近乎即时的性能。

语音生成

现在我们具有包含元数据的树,将转为语音生成。原始 TTS 系统已尝试通过组合正弦曲线来合成信号。另一个有趣的方法是构造微分方程的系统,即将人类声道描述为多个具有不同直径和长度的相连管道。此类解决方案非常简洁,但遗憾的是听起来很机械。因此,正如音乐合成器一样,焦点逐渐转向基于样本的解决方案,这需要大量空间,但实质上听起来很自然。

若要构建此类系统,必须花数小时高质量录制专业演员阅读特殊构造的文本。此文本拆分为多个单位,进行标记并存储到数据库中。语音生成将变为选择正确的单位并将其集合在一起的任务。

由于不会合成语音,因此无法显著调整运行时中的参数。如果同时需要男性声音和女性声音,或者必须提供地方口音(例如,苏格兰语或爱尔兰语),则必须单独进行录制。必须将文本构造为涵盖所有可能需要的声音单位。演员必须用中性音调阅读才能轻松连接。

拆分和标记也是重要的任务。过去是通过手动完成的,需要执行数周的繁琐工作。幸运的是,现已应用机器学习。

单位大小可能是 TTS 系统最重要的参数。显然,通过使用整个句子,我们可以发出最自然的声音,甚至使用正确的韵律,但无法录制和存储那么多数据。我们是否可以将其拆分为多个单词?或许可以,但演员需要多长时间才能读完整本字典?我们所要面临的数据库大小限制有哪些?另一方面,我们不能只是录制字母表,这只够参加拼字比赛。因此,通常单位选为由两三个字母组成的组合。它们不一定是音节,因此跨越音节边界的组合可以更好地集合在一起。

接下来是最后一个步骤。我们拥有语音单位的数据库,因此需要处理连接问题。唉,无论原始录音中的声调有多中性,仍需要调整连接单位以避免音量、频率和阶段中的跳转。这是通过数字信号处理 (DSP) 完成的。还可用于向短语添加某些声调,如提高或降低断言或问题的生成语音。

总结

本文仅介绍了 .NET API。其他平台提供类似的功能。MacOS 在 Cocoa 中具有功能不相上下的 NSSpeechSynthesizer,并且大多数 Linux 分发版包括 eSpeak 引擎。可通过本机代码访问所有这些 API,因此必须使用 C#、C++ 或 Swift。对于 Python 等跨平台生态系统,存在一些桥(如 Pyttsx),但它们通常具有某些限制。

另一方面,云供应商面向广大群众,并为最流行的语言和平台提供服务。虽然功能在各个供应商之间具有可比性,但对 SSML 标记的支持可能不同,因此在选择解决方案之前检查文档。

Microsoft 提供作为认知服务的一部分的文本转语音服务 (bit.ly/2XWorku)。不仅为你提供采用 45 种语言的 75 种声音,而且还允许你创建自己的声音。为此,服务需要具有相应脚本的音频文件。你可以先撰写文本,然后让其他人阅读,或使用现有录音并编写其脚本。将这些数据集上载到 Azure 后,机器学习算法为自己唯一的“语音字体”定型模型。可在 bit.ly/2VE8th4 中找到很好的分步指南。

访问认知语音服务的一种非常便捷方式是使用语音软件开发工具包 (bit.ly/2DDTh9I)。它支持语音识别和语音合成,并且适用于所有主要桌面和移动平台以及最流行的语言。文档已完备,并且 GitHub 上有多个代码示例。

TTS 仍可为具有特殊需求的人提供极大的帮助。例如,访问 linka.su(由患有脑中风的优秀程序员创建的网站)可为患有语言和肌肉骨骼障碍、自闭症或从中风恢复的人提供帮助。从个人经验中了解到用户所要面临的限制,作者为以下人员创建了一系列应用:不能在常规键盘上打字的人,一次只能选择一个字母的人或只能触摸平板电脑上的图片的人。多亏了 TTS,他实际上为没有声音的人员提供声音。我希望我们所有程序员都可以为其他人提供帮助。

原文发布于微信公众号 - DotNet Core圈圈(gh_2705b57d3bc5)

原文发表时间:2019-07-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券