如何将 Riva TTS API 与开箱即用模型结合使用?#

本教程将引导您了解 Riva TTS 服务的基础知识,特别介绍如何将 Riva TTS API 与 OOTB(开箱即用)模型结合使用。

NVIDIA Riva 概述#

NVIDIA Riva 是一个 GPU 加速的 SDK,用于构建为您的用例定制并提供实时性能的语音 AI 应用程序。
Riva 提供了一系列丰富的语音和自然语言理解服务,例如

  • 自动语音识别 (ASR)

  • 文本到语音合成 (TTS)

  • 一系列自然语言处理 (NLP) 服务,例如命名实体识别 (NER)、标点符号和意图分类。

在本教程中,我们将与文本到语音合成 (TTS) API 交互,并使用 SSML 自定义 Riva TTS 音频输出。

有关 Riva 的更多信息,请参阅 Riva 开发者网站

基础知识:使用 Riva TTS API 生成语音#

Riva TTS 服务基于两阶段管道:Riva 使用第一个模型生成梅尔频谱图,然后使用梅尔频谱图使用第二个模型生成语音。此管道构成了一个文本到语音系统,使您能够从原始文本记录合成听起来自然的语音,而无需任何其他信息,例如语音的模式或节奏。

Riva 为英语提供了两种最先进的声音(一种男性和一种女性),可以使用 Riva 快速入门脚本轻松部署。Riva 还支持以各种方式轻松自定义 TTS,以满足您的特定需求。
后续的 Riva 版本将包含诸如模型注册之类的功能,以支持使用相同 API 的多种语言/声音,并支持重采样到替代采样率。
有关更多信息,请参阅 Riva TTS 文档

现在,让我们使用 Riva API 和 OOTB 英语 TTS 管道生成音频。

要求和设置#

  1. 启动 Riva 服务器。在运行本教程之前,请按照《Riva 快速入门指南》中的说明在 Riva 服务器上部署 OOTB TTS 模型。默认情况下,仅部署英语模型。

  2. 安装额外的 Python 库以运行本教程。运行以下命令来安装库

# We need numpy to read the output from the Riva TTS request.
pip install numpy
pip install nvidia-riva-client

导入 Riva 客户端库#

我们首先导入一些必需的库,包括 Riva 客户端库。

import numpy as np
import IPython.display as ipd
import riva.client

创建 Riva 客户端并连接到 Riva 服务器#

以下 URI 假定 Riva 服务器在默认端口上的本地部署。如果服务器部署在不同的主机上,或者通过 Kubernetes 上的 Helm Chart 进行部署,请使用适当的 URI。

auth = riva.client.Auth(uri='localhost:50051')

riva_tts = riva.client.SpeechSynthesisService(auth)

TTS 模式#

Riva TTS 支持流式和批量推理模式。在批量模式下,音频在生成请求文本的完整音频序列之前不会返回,并且可以实现更高的吞吐量。但是,在发出流式请求时,音频块会在生成后立即返回,从而显着减少大型请求的延迟(以首次音频的时间衡量)。

设置 TTS API 参数#

sample_rate_hz = 44100
req = { 
        "language_code"  : "en-US",
        "encoding"       : riva.client.AudioEncoding.LINEAR_PCM ,   # LINEAR_PCM and OGGOPUS encodings are supported
        "sample_rate_hz" : sample_rate_hz,                          # Generate 44.1KHz audio
        "voice_name"     : "English-US.Female-1"                    # The name of the voice to generate
}

理解 TTS API 参数#

如上所示,Riva TTS 在向 gRPC 端点发出文本到语音请求时支持许多选项。让我们详细了解这些参数

  • language_code - 生成音频的语言。en-US 代表英语(美国),目前是唯一支持 OOTB 的语言。

  • encoding - 要生成的音频编码类型。支持 LINEAR_PCMOGGOPUS 编码。

  • sample_rate_hz - 生成音频的采样率。取决于麦克风,通常为 22khz44khz

  • voice_name - 用于合成音频的声音。目前,Riva 提供两种 OOTB 声音(English-US.Female-1English-US.Male-1)。

向 Riva 服务器发出 gRPC 请求#

对于批量推理模式,请使用 synthesize。当整个音频合成后,将返回结果。

req["text"] = "Is it recognize speech or wreck a nice beach?"
resp = riva_tts.synthesize(**req)
audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
ipd.Audio(audio_samples, rate=sample_rate_hz)

对于在线推理,请使用 synthesize_online。结果会在合成时分块返回。

req["text"] = "Is it recognize speech or wreck a nice beach?"
resp = riva_tts.synthesize_online(**req)
empty = np.array([])
for i, rep in enumerate(resp):
    audio_samples = np.frombuffer(rep.audio, dtype=np.int16) / (2**15)
    print("Chunk: ",i)
    ipd.display(ipd.Audio(audio_samples, rate=44100))
    empty = np.concatenate((empty, audio_samples))

print("Final synthesis:")
ipd.display(ipd.Audio(empty, rate=44100))

使用 SSML 自定义 Riva TTS 音频输出#

语音合成标记语言 (SSML) 规范是一种用于指导虚拟扬声器性能的标记。Riva 支持 SSML 的一部分,允许您调整生成的音频的音调、语速和发音。

所有 SSML 输入都必须是有效的 XML 文档,并使用根标记。所有无效的 XML 和所有具有不同根标记的有效 XML 都被视为原始输入文本。

Riva TTS 支持以下 SSML 标记

  • prosody 标签,它支持属性 ratepitchvolume,通过这些属性我们可以控制生成的音频的语速、音调和音量。

  • phoneme 标签,它允许我们控制生成的音频的发音。

  • sub 标签,它允许我们用不同的单词或短语替换指定单词或短语的发音。

让我们详细了解如何使用这些 SSML 标签自定义 Riva TTS。

使用 prosody 标签自定义语速、音调和音量#

音调属性#

Riva 支持对音调进行附加的相对更改。pitch 属性的范围为 [-3, 3] 或 [-150, 150] Hz。超出此范围的值会导致记录错误且不返回音频。

当使用不以 Hz 结尾的绝对值时,音调会按该值乘以模型配置中定义的说话人音调标准差进行偏移。对于在 LJSpeech 上训练的预训练检查点,标准差为 52.185。例如,1.25 的音调偏移会导致 1.25*52.185=~65.23 Hz 的音调向上偏移。

Riva 还支持以下标签(根据 SSML 规范):x-lowlowmediumhighx-highdefault

pitch 属性以以下格式表示

  • pitch="1"

  • pitch="95hZ"

  • pitch="+1.8"

  • pitch="-0.65"

  • pitch="+75Hz"

  • pitch="-84.5Hz"

  • pitch="high"

  • pitch="default"

对于预训练的 Female-1 检查点,标准差为 53.33 Hz。对于预训练的 Male-1 检查点,标准差为 47.15 Hz。

pitch 属性不支持 st% 更改。

在 FastPitch 中,音调的处理方式与 RadTTS 不同。虽然这两种模型都接受两种音调格式,但在内部,FastPitch 使用归一化音调,而 RadTTS 使用非归一化音调。如果 TTS 请求使用 RadTTS 模型,并且音调属性以 [-3, 3] 格式提供,则 Riva 会使用模型的音调标准差将其转换为非归一化音调偏移。如果 TTS 请求使用 FastPitch 模型,并且音调属性以 [-150, 150] Hz 格式提供,则 Riva 会使用模型的音调标准差将其转换为归一化音调偏移。在 Riva 从 NeMo 模型配置确定音调标准差的情况下,使用 59.02 Hz 的值作为音调标准差。

语速属性#

Riva 支持对语速进行百分比相对更改。rate 属性的范围为 [25%, 250%]。超出此范围的值会导致记录错误且不返回音频。Riva 还支持以下标签(根据 SSML 规范):x-lowlowmediumhighx-highdefault

rate 属性以以下格式表示

  • rate="35%"

  • rate="+200%"

  • rate="low"

  • rate="default"

音量属性#

Riva 支持 SSML 规范中描述的音量属性。音量属性支持 [-13, 8]dB 的范围。超出此范围的值会导致记录错误且不返回音频。支持标签 silentx-softsoftmediumloudx-louddefault

音量属性以以下格式表示

  • volume="+1dB"

  • volume="-5.7dB"

  • volume="x-loud"

  • volume="default"

情感属性#

Riva 在 Beta 版中支持情感混合,情感属性如 SSML 规范中所述。情感属性会覆盖请求中默认的子语音情感,并支持 [0.0, 1.0] 浮动范围内的混合权重。支持混合权重标签 xlowlowmediumveryextreme。目前,情感混合仅在 RadTTS++ 模型中受支持。

当选择情感时,它会根据指定的权重与中性情感混合,以对其进行量化。例如,快乐且混合权重为 0.5 时,快乐的极端情感与中性情感以 1:1 的比例混合,以获得快乐:0.5。

情感属性以以下格式表示

  • emotion="sad:1.0,fearful:0.7"

  • emotion="happy:extreme,calm:low"

让我们看一个示例,展示 Riva TTS 的音调、语速和音量自定义

"""
    Raw text is "Today is a sunny day. But it might rain tomorrow."
    We are updating this raw text with SSML:
    1. Envelope raw text in '<speak>' tags as is required for SSML
    2. Add '<prosody>' tag with 'pitch' attribute set to '2.5'
    3. Add '<prosody>' tag with 'rate' attribute set to 'high'
    4. Add '<volume>' tag with 'volume' attribute set to '+1dB'
"""
raw_text = "Today is a sunny day. But it might rain tomorrow."
ssml_text = """<speak><prosody pitch='2.5'>Today is a sunny day</prosody>. <prosody rate='high' volume='+1dB'>But it might rain tomorrow.</prosody></speak>"""
print("Raw Text: ", raw_text)
print("SSML Text: ", ssml_text)


req["text"] = ssml_text
# Request to Riva TTS to synthesize audio
resp = riva_tts.synthesize(**req)

# Playing the generated audio from Riva TTS request
audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))

运行教程的预期结果:#

<prosody pitch='2.5'>今天 晴天</prosody>。 <prosody rate='high' volume='+1dB'>但 明天 可能会 下雨。</prosody>

注意#

如果在整个笔记本中看不到音频控件。请在 github dev 中打开笔记本,或在 riva 文档中查看

以下是更多示例,展示了音调、语速和情感属性值的更改对生成音频的影响

# SSML texts we want to try
ssml_texts = [
  """<speak>This is a normal sentence</speak>""",
  """<speak><prosody pitch="0." rate="100%">This is also a normal sentence</prosody></speak>""",
  """<speak><prosody rate="200%">This is a fast sentence</prosody></speak>""",
  """<speak><prosody rate="60%">This is a slow sentence</prosody></speak>""",
  """<speak><prosody pitch="+1.0">Now, I'm speaking a bit higher</prosody></speak>""",
  """<speak><prosody pitch="-0.5">And now, I'm speaking a bit lower</prosody></speak>""",
  """<speak>S S M L supports <prosody pitch="-1">nested tags. So I can speak <prosody rate="150%">faster</prosody>, <prosody rate="75%">or slower</prosody>, as desired.</prosody></speak>""",
  """<speak><prosody volume='x-soft'>I'm speaking softly.</prosody><prosody volume='x-loud'> And now, This is loud.</prosody></speak>""",
]

# Loop through 'ssml_texts' list and synthesize audio with Riva TTS for each entry 'ssml_texts'
for ssml_text in ssml_texts:
    req["text"] = ssml_text
    resp = riva_tts.synthesize(**req)
    audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
    print("SSML Text: ", ssml_text)
    ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))
    print("--------------------------------------------")

运行教程的预期结果:#

这是一个 正常的 句子

<prosody pitch="0." rate="100%">这也是 一个 正常的 句子</prosody>

<prosody rate="200%">这是一个 快速的 句子</prosody>

<prosody rate="60%">这是一个 慢速的 句子</prosody>

<prosody pitch="+1.0">现在, 说话的 声音 有点 高</prosody>

<prosody pitch="-0.5">现在, 说话的 声音 有点 低</prosody>

SSML 支持 <prosody pitch="-1">嵌套 标签。 因此 可以 <prosody rate="150%">更快</prosody>, <prosody rate="75%">或 更慢</prosody> 的速度 说话, 需。</prosody>

<prosody volume='x-soft'>我 说话 轻。</prosody><prosody volume='x-loud'> 现在, 声音 很大。</prosody>

# Note: This code segment uses the beta radtts model which supports emotion mixing, in case of other models the emotions will be ignored except set via voice_name.

req_emotion = { 
        "language_code"  : "en-US",
        "encoding"       : riva.client.AudioEncoding.LINEAR_PCM ,   # LINEAR_PCM and OGGOPUS encodings are supported
        "sample_rate_hz" : sample_rate_hz,                          # Generate 44.1KHz audio
        "voice_name"     : "English-US-RadTTSpp.Male.happy"                    # The name of the voice to generate
}

ssml_text="""<speak> I am happy.<prosody emotion="sad:very"> And now, I am sad.</prosody><prosody emotion="angry:extreme"> This makes me angry.</prosody><prosody emotion="calm:extreme"> And now, I am calm.</prosody></speak>"""
print("SSML Text: ", ssml_text)


req_emotion["text"] = ssml_text
# Request to Riva TTS to synthesize audio
resp = riva_tts.synthesize(**req_emotion)

# Playing the generated audio from Riva TTS request
audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))

运行教程的预期结果:#

很高兴。<prosody emotion="sad:very"> 现在, 伤心。</prosody><prosody emotion="angry:extreme"> 生气了。</prosody><prosody emotion="calm:extreme"> 现在, 平静。</prosody>

使用 phoneme 标签自定义发音#

我们可以使用 phoneme 标签来覆盖预测发音中单词的发音。对于给定的单词或单词序列,使用 ph 属性提供显式发音,并使用 alphabet 属性提供音素集。

从 Riva 2.8.0 版本开始,ipa 将是 TTS 模型唯一支持的发音字母表。较旧的 Riva 模型仅支持 x-arpabet

IPA#

有关支持的 ipa 音素的完整列表,请参阅 Riva TTS 音素支持页面。

Arpabet#

CMUdict 中的音素完整列表可在 cmudict.phone 中找到。带有重音的支持符号列表可在 cmudict.symbols 中找到。有关这些音素到英语声音的映射,请参阅 ARPABET 维基百科页面

让我们看一个示例,展示 Riva TTS 的此自定义发音

# Setting up Riva TTS request with SynthesizeSpeechRequest
"""
    Raw text is "You say tomato, I say tomato."
    We are updating this raw text with SSML:
    1. Envelope raw text in '<speak>' tags as is required for SSML
    2. For a substring in the raw text, add '<phoneme>' tags with 'alphabet' attribute set to 'x-arpabet' 
       (currently the only supported value) and 'ph' attribute set to a custom pronunciation based on CMUdict and ARPABET

"""
raw_text = "You say tomato, I say tomato."
ssml_text = '<speak>You say <phoneme alphabet="ipa" ph="təˈmeɪˌtoʊ">tomato</phoneme>, I say <phoneme alphabet="ipa" ph="təˈmɑˌtoʊ">tomato</phoneme>.</speak>'
# Older arpabet version
# ssml_text = '<speak>You say <phoneme alphabet="x-arpabet" ph="{@T}{@AH0}{@M}{@EY1}{@T}{@OW2}">tomato</phoneme>, I say <phoneme alphabet="x-arpabet" ph="{@T}{@AH0}{@M}{@AA1}{@T}{@OW2}">tomato</phoneme>.</speak>'

print("Raw Text: ", raw_text)
print("SSML Text: ", ssml_text)

req["text"] = ssml_text
# Request to Riva TTS to synthesize audio
resp = riva_tts.synthesize(**req)

# Playing the generated audio from Riva TTS request
audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))

运行教程的预期结果:#

你说 <phoneme alphabet="ipa" ph="təˈmeɪˌtoʊ">tomato</phoneme>, 我说 <phoneme alphabet="ipa" ph="təˈmɑˌtoʊ">tomato</phoneme>。

以下是更多示例,展示了生成音频中发音的自定义

# SSML texts we want to try
ssml_texts = [
  """<speak>You say <phoneme ph="ˈdeɪtə">data</phoneme>, I say <phoneme ph="ˈdætə">data</phoneme>.</speak>""",
  """<speak>Some people say <phoneme ph="ˈɹut">route</phoneme> and some say <phoneme ph="ˈɹaʊt">route</phoneme>.</speak>""",
]
# Older arpabet version
# ssml_texts = [
#   """<speak>You say <phoneme alphabet="x-arpabet" ph="{@D}{@EY1}{@T}{@AH0}">data</phoneme>, I say <phoneme alphabet="x-arpabet" ph="{@D}{@AE1}{@T}{@AH0}">data</phoneme>.</speak>""",
#   """<speak>Some people say <phoneme alphabet="x-arpabet" ph="{@R}{@UW1}{@T}">route</phoneme> and some say <phoneme alphabet="x-arpabet" ph="{@R}{@AW1}{@T}">route</phoneme>.</speak>""",
# ]

# Loop through 'ssml_texts' list and synthesize audio with Riva TTS for each entry 'ssml_texts'
for ssml_text in ssml_texts:
    req["text"] = ssml_text
    resp = riva_tts.synthesize(**req)
    audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
    print("SSML Text: ", ssml_text)
    ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))
    print("--------------------------------------------")

运行教程的预期结果:#

你说 <phoneme ph="ˈdeɪtə">data</phoneme>, 我说 <phoneme ph="ˈdætə">data</phoneme>。

有些人 <phoneme ph="ˈɹut">route</phoneme> ,有些人 <phoneme ph="ˈɹaʊt">route</phoneme>。

使用 sub 标签替换发音#

我们可以使用 sub 标签来替换指定单词或短语的发音。您可以使用 alias 属性指定要替换的发音。

# Setting up Riva TTS request with SynthesizeSpeechRequest
"""
    Raw text is "WWW is know as the web"
    We are updating this raw text with SSML:
    1. Envelope raw text in '<speak>' tags as is required for SSML
    2. Add '<sub>' tag with 'alias' attribute set to replace www with `World Wide Web`

"""
raw_text = "WWW is know as the web."
ssml_text = '<speak><sub alias="World Wide Web">WWW</sub> is known as the web.</speak>'

print("Raw Text: ", raw_text)
print("SSML Text: ", ssml_text)

req["text"] = ssml_text
# Request to Riva TTS to synthesize audio
resp = riva_tts.synthesize(**req)
# Playing the generated audio from Riva TTS request
audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))

运行教程的预期结果:#

<sub alias="万维网">WWW</sub> 被称为 web。

使用 emphasis 标签强调词语#

使用 emphasis 标签来强调词语。将 riva-build 命令与 enable_emphasis_tagstart_of_emphasis_tokenend_of_emphasis_token 标签一起使用,以启用强调功能。emphasis 标签应按单词使用。如果单词以标点符号结尾,则只会强调单词,而不会强调标点符号。

局限性#

emphasis 标签依赖于训练数据,并且仅在 English-US 模型中可用。在没有训练数据中 emphasis 标签的情况下训练的模型不会产生强调的语音。包含用 emphasis 标签包裹的多个单词的输入文本是无效输入。在 emphasis 标签内包裹空格也是无效输入。

警告:emphasis 标签功能不支持在其内部嵌套其他 SSML 标签。emphasis 标签不支持 level 属性。
# Setting up Riva TTS request with SynthesizeSpeechRequest
"""
    Raw text is "Hello World"
    We are updating this raw text with SSML:
    1. Envelope raw text in '<speak>' tags as is required for SSML
    2. Add '<emphasis>' tag around `love`

"""
ssml_texts = [
   """<speak>I would <emphasis>love</emphasis> to try that.</speak>""",
   """<speak><emphasis>Wow!</emphasis> Thats really cool.</speak>"""
]

print("Raw Text: ", raw_text)
print("SSML Text: ", ssml_text)

for ssml_text in ssml_texts:
    req["text"] = ssml_text
    resp = riva_tts.synthesize(**req)
    # Playing the generated audio from Riva TTS request
    audio_samples = np.frombuffer(resp.audio, dtype=np.int16)
    print("SSML Text: ", ssml_text)
    ipd.display(ipd.Audio(audio_samples, rate=sample_rate_hz))
    print("--------------------------------------------")

运行教程的预期结果:#

很<emphasis>想</emphasis> 试试 那个。

<emphasis>哇!</emphasis> 那真 很酷。