从零开始AI的有关介绍中,我们将使用Azure认知服务的标准REST API服务接口,尝试认知服务的一个类别——语音服务。

和其他认知服务一样,语音服务也是一种运行在Azure的服务。为了使用Speech API,您需要一个订阅密钥 (subscription key)。别担心,您可以在这里获取免费的订阅密钥。或者在这里获取试用的订阅密钥。

获取订阅密钥后,请记录密钥及分配的Azure服务区域。后面我们调用这些API时,必须提供这两种信息。


关于播放声音的说明

在示例的原始代码中,通过认知服务会生成WAV声音文件,然后需要手动播放。为了能更好地和代码交互,我们会尝试让代码直接播放声音。这部分的代码对jupyter notebook来说,需要安装用于支持声音播放的的库。相对代码简单的播放声音的库可以选择 ‘playsound’。如果是在自己搭建的jupyter notebook环境中运行这个记事本,可以返回到文件列表的界面,在右上角找到‘新建’,然后选择‘终端’。在打开的终端中,使用如下命令安装需要的库:

pip install playsound

'playsound'能够跨平台实现非常简单的声音播放,代码简单到只需要两行

from playsound import playsound
playsound('/path/to/a/sound/file/you/want/to/play.mp3')

源码可以在Github上访问 playsound。作者说明了不同系统下工作的机制。

On Windows, uses windll.winmm. WAVE and MP3 have been tested and are known to work. Other file formats may work as well.
On OS X, uses AppKit.NSSound. WAVE and MP3 have been tested and are known to work. In general, anything QuickTime can play, playsound should be able to play, for OS X.
On Linux, uses GStreamer. Known to work on Ubuntu 14.04 and ElementaryOS Loki. Support for the block argument is currently not implemented.|

需要说明的是,由于这个库本身存在一些bug,我们可能需要自行做一点修复。

  • 如果是Windows系统,建议安装之后,打开 playsound.py 找到如下代码并添加差异部分:
if block:
    sleep(float(durationInMS) / 1000.0)
    winCommand('close', alias)
  • 如果是Mac OS系统,运行代码可能会看到需要AppKit之类的报错。安装XCode Command Line Tool并执行解决这个问题:
pip install pyobjc

如果发现不能播放路径中包含空格的文件,可以打开 playsound.py 找到如下代码并添加差异部分:

        sound = 'file://' + sound
        sound = sound.replace(' ', '%20')
    url   = NSURL.URLWithString_(sound)

如果您使用免费的Azure Notebooks在线环境运行这个笔记本,由于没有权限运行'pip'安装库文件,您可以按照代码中的提示注释掉或删除用于播放的代码,然后将生成的WAV文件使用其他方式进行播放。


准备好了吗?让我们正式开始认知服务从文字到语音的尝试吧。首先需要引入必要的库,然后按照提示输入从免费认知服务试用、免费Azure试用或已有Azure订阅中Speech服务资源里提供的订阅密钥。密钥在我们使用REST API调用时,需要在用于验证的Header中提供,也被称为Ocp-Apim-Subscription-Key

实际上,以下的代码基本来自于微软的示例代码,您可以访问 Azure Cognitive TTS Samples 来查看简介及下载源代码。为了适合运行在笔记本中,并且直接与认知服务交互,代码做了修改调整。

import os, requests, time
from xml.etree import ElementTree
#Delete next line if use Azure Notebooks
from playsound import playsound

subscription_key = input('Please input your Service Key:')

您如果还记得之前我们使用认知服务的经历的话,应该对服务区域 Service Region 有些印象。有关文本转语音服务的服务区域,可以查看对应的 Speech-to-text REST API 文档中服务区域的介绍。服务区域也就是提供认知服务的区域,一般我们获得的订阅密钥,是和服务区域关联的。 因此,需要在查看服务密钥的地方,确认对应的服务区域识别字符串,并按照以下代码提示输入。

service_region = input('please input your Service Region:')

对于基于语音的认知服务,由于很多时候对声音的处理是相对异步的,所以每个服务调用的会话,需要生成一个token来进行验证和返回。获取这个token的方法非常简单,只需要使用POST方法向认知服务生成token的服务终结点URL提交一个含有订阅密钥头部header的请求即可。返回的字符串就是我们后续调用服务需要的token。为了看看token到底长什么样,我们可以将其显示出来。

fetch_token_url = "https://"+service_region+".api.cognitive.microsoft.com/sts/v1.0/issueToken"
headers = {
            'Ocp-Apim-Subscription-Key': subscription_key
        }
response = requests.post(fetch_token_url, headers=headers)
access_token = str(response.text)

拿到了token,我们就可以尝试调用语音认知服务了。对于文本转语音的服务,有专门的服务终结点URL,我们需要按照REST API的要求,构造这个服务终结点的URL并发起REST API调用。

与之前我们尝试过的计算机视觉等认知服务不同,提交一个文本转语音的请求时,并不是直接使用订阅密钥,而是使用由其生成的token。用法是在头部header中包含一个Authorization字段,内容是以Bearer开头并加上token的字段。

让我们先尝试一个简单的服务调用:拿到支持的语音清单。通过这个API,我们可以得到在当前服务区域可供使用的各种语言以及语言对应的语音模型。

base_url = "https://"+service_region+".tts.speech.microsoft.com/"
path = 'cognitiveservices/voices/list'
constructed_url = base_url + path
headers = {
            'Authorization': 'Bearer ' + access_token,
        }
response = requests.get(constructed_url, headers=headers)
if response.status_code == 200:
    print("\nAvailable voices: \n" + response.text)
else:
    print("\nStatus code: " + str(response.status_code) +  \
          "\nSomething went wrong. Check your subscription key and headers.\n")

返回的是标准的JSON格式数据。我们可以找一下,对于简体中文 zh-CN,认知服务提供了慧慧 Huihui、康康 Kangkang和瑶瑶 Yaoyao。这几个标准语音有性别区分。注意看有个数据字段叫做 ShortName,后面我们选择文本转语音使用的标准语音类型,就可以通过这个名称来选择。除了标准语音,在部分区域例如westus2等,还支持神经语音标准,能够提供更加自然顺畅的语音,并且可以使用语音合成标记语言来配置和调整神经语音。

语音服务能支持的语言范围,可以在语音服务的语言和语音支持这一文档中查看。

返回的数据中还有一个参数SampleRateHertz采样率,这与认知服务支持的音频格式有关,我们将在POST代码部分时候介绍支持的音频格式。

接下来,我们开始为文本转语音做些准备工作。首先做一个时间戳,提供给WAV文件生成时用来产生文件名。然后,我们需要按照提示,输入用于转变成语音的文本。

timestr = time.strftime("%Y%m%d-%H%M")
ttsstr = input("What would you like to convert to speech: ")

首先我们生成一个用于给WAV文件命名的时间戳。

然后,我们按照提示输入需要进行语音转换的文本。

base_url = "https://"+service_region+".tts.speech.microsoft.com/"
path = 'cognitiveservices/v1'
constructed_url = base_url + path
headers = {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type': 'application/ssml+xml',
        'X-Microsoft-OutputFormat': 'riff-24khz-16bit-mono-pcm',
        'User-Agent': 'YOUR_RESOURCE_NAME'
        }

正式POST一个文本转语音的请求时,API的服务终结点需要指定为特定的路径。同时,头部header除了之前使用的token之外,还需要说明后续bady的内容类型,以及需要返回的声音输出格式。

语音认知服务目前能够支持的声音类型如下:

raw-16khz-16bit-mono-pcm            raw-8khz-8bit-mono-mulaw
riff-8khz-8bit-mono-alaw            riff-8khz-8bit-mono-mulaw
riff-16khz-16bit-mono-pcm           audio-16khz-128kbitrate-mono-mp3
audio-16khz-64kbitrate-mono-mp3     audio-16khz-32kbitrate-mono-mp3
raw-24khz-16bit-mono-pcm            riff-24khz-16bit-mono-pcm
audio-24khz-160kbitrate-mono-mp3    audio-24khz-96kbitrate-mono-mp3
audio-24khz-48kbitrate-mono-mp3     ogg-24khz-16bit-mono-opus

此外,提交请求的bady部分需要满足Speech Synthesis Markup Language (SSML)的格式要求。

xml_body = ElementTree.Element('speak', version='1.0')
# xml_body.set('{http://www.w3.org/XML/1998/namespace}lang', 'en-us')
xml_body.set('{http://www.w3.org/XML/1998/namespace}lang', 'zh-cn')
voice = ElementTree.SubElement(xml_body, 'voice')
# voice.set('{http://www.w3.org/XML/1998/namespace}lang', 'en-US')
voice.set('{http://www.w3.org/XML/1998/namespace}lang', 'zh-cn')
# voice.set('name', 'en-US-Guy24kRUS') 
# Short name for 'Microsoft Server Speech Text to Speech Voice (en-US, Guy24KRUS)'
voice.set('name', 'zh-CN-HuihuiRUS') 
# Short name for 'Microsoft Server Speech Text to Speech Voice (zh-CN, HuihuiRUS)'
voice.text = ttsstr
body = ElementTree.tostring(xml_body)

提供一个SSML格式body,有两部分。

  • 首先需要按照 w3.org 的规范,给出文档的描述。
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="string"></speak>
  • 然后需要对具体使用的语音进行描述。在上述代码示例中,我们选择使用简体中文,并按照认知语音服务对于区域所支持的语音,选择标准语音的慧慧的声音zh-CN-HuihuiRUS
    <voice name="zh-CN-HuihuiRUS"></voice>
  • 最后,我们传递之前输入的文本到这个XML文件中。最终的文档就形如:
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="string">
    <voice name="zh-CN-HuihuiRUS">
        认识服务是简单而神奇的人工智能体现。
    </voice>
</speak>

接下来的工作,就是把构造好的头部headers、包含SSML的bady提交到REST API的服务终结点地址了。

response = requests.post(constructed_url, headers=headers, data=body)
if response.status_code == 200:
    with open('sample-' + timestr + '.wav', 'wb') as audio:
        audio.write(response.content)
        print("\nStatus code: " + str(response.status_code) + "\nYour TTS is ready for playback.\n")
#Delete next line if use Azure Notebooks
        playsound('sample-' + timestr + '.wav')
else:
    print("\nStatus code: " + str(response.status_code) + "\nSomething went wrong. Check your subscription key and headers.\n")
    print("Reason: " + str(response.reason) + "\n")

假如一切顺利,上述代码运行后,就能听到由您输入的文本生成的语音了。提交请求后,语音认知服务会返回声音的内容,将返回的数据写入一个WAV文件,就可以进行播放了。

当jupyter notebook能够使用终端运行pip导入库时,就可以通过playsound直接播放这个文件了。

播放结束后,可使用以下代码输入 y 或 n 来决定是否删除生成的WAV文件。

delfile = input("Delete the WAV file? (y/n):")
if delfile=='y':
    os.remove('sample-' + timestr + '.wav')

如果您选择了支持神经语音的服务区域,例如westus2,强烈建议您返回到构造SSML的代码块,将“慧慧”的标准语音对应的"zh-CN-HuihuiRUS"改为“晓悠“的神经语音"zh-CN-XiaoyouNeural",然后好好的比较两者之间的差异。