从零开始玩人工智能—语音API-03

还在担心自己的英语发音不标准?请个外教教发音太贵?有语音认知服务还要啥自行车啊~ 既然放音和录音我们都尝试过了,那么来一个更有难度的实验吧。

发音评估

实际上,语音转文本的服务中,提供了一个发音评估参数。利用这个参数,就能够对发送的语音进行发音评估。很有趣吧?我们看看Speech-to-Text REST API是怎么说明的。

要实现发音评估功能,只需简单在提交语音转文本请求的时候,在头部header中添加 ‘Pronunciation-Assessment‘ 这个字段即可。该字段指定用于在识别结果中显示发音评分的参数,这些参数可评估语音输入的发音质量,并显示准确性、熟练、完整性等。此参数是 base64 编码的 json,其中包含多个详细参数。

和前面的内容一样,我们首先做些准备工作,首先把代码环境设置好。

import requests
import pyaudio, wave
import os, json, base64
from xml.etree import ElementTree

# constents for WAV file
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
RECORD_SECONDS = 5

# speech service information
subscription_key = input("Please input Service Key: ")
service_region = input("Please input Service Region: ")
#发音评估功能当前仅适用于 `westus` `eastasia` 和 `centralindia` 区域。 此功能目前仅适用于 `en-US` 语言。

# generate speech service 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)
if response.status_code == 200:
    access_token = str(response.text)
    print("Access token granted.")
else:
    print("\nStatus code: " + str(response.status_code) + "\nSomething went wrong. Check your subscription key and region.\n")
    print("Reason: " + str(response.reason) + "\n")

RECORD_SECONDS 代表的是后面录制声音的时长,可以自行调整。如果需要的时间特别长,建议参考文档对音频进行分块发送。分块发送的音频在处理响应上会有更好的表现。 音频WAV文件的参数设置好了,认知服务需要的服务订阅密钥和服务区域也设置好了,访问语音服务的 token 也成功生成了,接下里我们输入一句英文的文本,看看人工智能如何朗读。

sentence_str = input("Please enter the sentence to test: ")

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‘
        }
xml_body = ElementTree.Element(‘speak‘, version=‘1.0‘)
xml_body.set(‘{http://www.w3.org/XML/1998/namespace}lang‘, ‘en-us‘)
voice = ElementTree.SubElement(xml_body, ‘voice‘)
voice.set(‘{http://www.w3.org/XML/1998/namespace}lang‘, ‘en-US‘)
voice.set(‘name‘, ‘en-US-AriaRUS‘) 
voice.text = sentence_str
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>
  • 然后需要对具体使用的语音进行描述。由于目前发音评估只支持英文,按照认知语音服务对于区域所支持的语音,我们选择标准语音的Aria的声音en-US-AriaRUS
    <voice name="zh-CN-HuihuiRUS"></voice>
  • 最后,我们传递之前输入的文本到这个XML文件中。最终的文档就形如:
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="string">
    <voice name="en-US-AriaRUS">
        Hello World!
    </voice>
</speak>

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

response = requests.post(constructed_url, headers=headers, data=body)
if response.status_code == 200:
    with open(‘text.wav‘, ‘wb‘) as audio:
        audio.write(response.content)
        print("Please listen AI speak the sentence.")
        audio.close
    wf = wave.open(‘text.wav‘, ‘rb‘)
    p = pyaudio.PyAudio()
    stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                    channels=wf.getnchannels(),
                    rate=wf.getframerate(),
                    output=True)
    data = wf.readframes(CHUNK)
#    while data != ‘‘:
    while len(data) > 0:
        stream.write(data)
        data = wf.readframes(CHUNK)
    stream.stop_stream()
    stream.close()
    p.terminate()
else:
    print("\nStatus code: " + str(response.status_code) + "\nSomething went wrong. Check your subscription key and headers.\n")
    print("Reason: " + str(response.reason) + "\n")

听完了 Aria 的示范朗读,接下来该我们自己读一边句子了。代码将会提示我们前面设置的录音时长,重复一遍,如果时间不够,可以回到之前的代码,修改 RECORD_SECONDS 的值。

# Recording 
p = pyaudio.PyAudio()
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)
print("Please speak this sentence yourself.\n")
print("You have "+str(RECORD_SECONDS)+" seconds to record...")
frames = []
for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
    data = stream.read(CHUNK)
    frames.append(data)
print("Recording end. Please wait...\n")
stream.stop_stream()
stream.close()
p.terminate()
wf = wave.open(‘test.wav‘, ‘wb‘)
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
wf.writeframes(b‘‘.join(frames))
wf.close()

一切顺利的话,录制完毕就可以得到一个WAV文件。 在macOS比如Catalina上,如果使用VS Code直接运行代码而又无法录制到声音,一个可以参考的临时做法是以管理权限运行VS Code。

sudo /Applications/Visual\ Studio\ Code.app/Contents/MacOS/Electron

获得录制的WAV文件之后,我们就可以向语音认知服务提交发音评估的请求了。

发音评估有个不一样的要求:提交的 ‘Pronunciation-Assessment‘ 必须是一个基于BASE64编码的JSON数据,而JSON本身包含了几个发音评估参数。

参数 说明 必需/可选
ReferenceText 将对发音进行计算的文本 必选
GradingSystem 用于分数校准的点系统。 接受的值为 FivePoint 和 HundredMark。 默认设置为 FivePoint。 可选
粒度 计算粒度。 接受的值为 Phoneme ,其中显示了全文本、单词和音素级别上的分数, Word 其中显示了整个文本和 word 级别的分数, FullText 只显示了完整文本级别的分数。 默认设置为 Phoneme。 可选
维度 定义输出条件。 接受的值为 Basic ,只显示精确度评分, Comprehensive 显示更多维度上的分数 (例如,熟练分数和完整文本级别的完整性分数,word 级别上的错误类型) 。 检查响应参数以查看不同分数维度和 word 错误类型的定义。 默认设置为 Basic。 可选
EnableMiscue 启用 miscue 计算。 启用此功能后,会将发音为的单词与引用文本进行比较,并根据比较结果标记为省略/插入。 接受的值为 False 和 True。 默认设置为 False。 可选
ScenarioId 指示自定义点系统的 GUID。 可选

下面就是个常用的例子,‘Hello World!‘ 就是用于评估发音的文本:

{
  "ReferenceText": "Good morning.",
  "GradingSystem": "HundredMark",
  "Granularity": "FullText",
  "Dimension": "Comprehensive"
}

我们需要构造这个JSON,然后使之采用标准的UTF8编码,再对其进行BASE64编码。

# Pronunciation Assessment request
paJson = {‘ReferenceText‘: sentence_str, 
        ‘GradingSystem‘:‘HundredMark‘, 
        ‘Granularity‘:‘FullText‘,
        ‘Dimension‘:‘Comprehensive‘
        }
paHead = base64.b64encode(json.dumps(paJson).encode(‘utf8‘)).decode(‘ascii‘)

有了这个头部header字段,我们就可以像其他语音转文本请求一样,调用语音认知服务了。

rs=‘‘

base_url = "https://"+service_region+".stt.speech.microsoft.com/"
path = ‘speech/recognition/conversation/cognitiveservices/v1‘
constructed_url = base_url + path
params = {
        ‘language‘: ‘en-US‘,
        ‘format‘: ‘detailed‘
        }
headers = {
        ‘Authorization‘: ‘Bearer ‘ + access_token,
        ‘Content-Type‘: ‘audio/wav; codecs=audio/pcm; samplerate=16000‘,
        ‘Accept‘: ‘application/json;text/xml‘,
        ‘Pronunciation-Assessment‘: paHead
        }
body = open(‘test.wav‘,‘rb‘).read()
response = requests.post(constructed_url, params=params, headers=headers, data=body)
if response.status_code == 200:
    rs = response.json()
else:
    print("\nStatus code: " + str(response.status_code) + "\nSomething went wrong. Check your subscription key and headers.\n")
    print("Reason: " + str(response.reason) + "\n")

if rs != ‘‘:
    print(rs)

顺利的话,运行到这里已经能够看到语音认知服务返回的发音评估结果了。可是一堆JSON看着很混乱不是吗?没关系,我们对信息做一些规范化。

if rs != ‘‘:
    print(" The testing sentence: "+rs[‘NBest‘][0][‘Display‘])
    print("       Accuracy Score: "+str(rs[‘NBest‘][0][‘AccuracyScore‘]))
    print("        Fluency Score: "+str(rs[‘NBest‘][0][‘FluencyScore‘]))
    print("   Completeness Score: "+str(rs[‘NBest‘][0][‘CompletenessScore‘]))
    print("  Pronunciation Score: "+str(rs[‘NBest‘][0][‘PronScore‘]))   

怎么样?一个简单的听读英语句子并对发音进行评估打分的代码就这么简单的实现了吧。 最后,需要的话,清理一下生成的WAV文件。

delfile = input("Delete the WAV file? (y/n):")
if delfile==‘y‘:
    os.remove(‘test.wav‘)
    os.remove(‘text.wav‘)

本文代码已经使用 jupyter notebook 提交到 Github/HaoHoo/F02AI,可访问下载。

从零开始玩人工智能—语音API-03

上一篇:一文梳理REST API的设计原则


下一篇:.Net Core应用部署在Win Server的IIS服务器操作