文本转语音 - Wio Terminal 在本课的这一部分,你将学习如何将文本转换为语音,以提供口头反馈。 文本转语音 在上一课中用于将语音转换为文本的语音服务SDK也可以用来将文本转换回语音。 获取语音列表 当请求语音时,需要提供要使用的语音,因为语音可以通过多种不同的语音生成。每种语言都支持一系列不同的语音,你可以从语音服务SDK中获取每种语言支持的语音列表。然而,微控制器的限制在这里显现出来——调用以获取文本到语音服务支持的语音列表的JSON文档大小超过77KB,这远远超过了Wio Terminal可以处理的范围。截至写作时,完整列表包含215个语音,每个语音由一个像下面这样的JSON文档定义: 这个JSON是针对 Aria 语音的,它有多种语音风格。
在本课的这一部分,你将学习如何将文本转换为语音,以提供口头反馈。
在上一课中用于将语音转换为文本的语音服务SDK也可以用来将文本转换回语音。
当请求语音时,需要提供要使用的语音,因为语音可以通过多种不同的语音生成。每种语言都支持一系列不同的语音,你可以从语音服务SDK中获取每种语言支持的语音列表。然而,微控制器的限制在这里显现出来——调用以获取文本到语音服务支持的语音列表的JSON文档大小超过77KB,这远远超过了Wio Terminal可以处理的范围。截至写作时,完整列表包含215个语音,每个语音由一个像下面这样的JSON文档定义:
{ "Name": "Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)", "DisplayName": "Aria", "LocalName": "Aria", "ShortName": "en-US-AriaNeural", "Gender": "Female", "Locale": "en-US", "StyleList": [ "chat", "customerservice", "narration-professional", "newscast-casual", "newscast-formal", "cheerful", "empathetic" ], "SampleRateHertz": "24000", "VoiceType": "Neural", "Status": "GA" }
这个JSON是针对 Aria 语音的,它有多种语音风格。在将文本转换为语音时,只需要短名称 en-US-AriaNeural.
Instead of downloading and decoding this entire list on your microcontroller, you will need to write some more serverless code to retrieve the list of voices for the language you are using, and call this from your Wio Terminal. Your code can then pick an appropriate voice from the list, such as the first one it finds.
Open your smart-timer-trigger project in VS Code, and open the terminal ensuring the virtual environment is activated. If not, kill and re-create the terminal.
Open the local.settings.json 文件,并添加语音API密钥和位置:
"SPEECH_KEY": "<key>", "SPEECH_LOCATION": "<location>"
使用以下命令替换 <key> with the API key for your speech service resource. Replace <location> with the location you used when you created the speech service resource.
Add a new HTTP trigger to this app called get-voices,在函数应用项目的根目录内的VS Code终端中执行该命令:
func new --name get-voices --template "HTTP trigger"
这将在 get-voices.
Replace the contents of the __init__.py file in the get-voices 文件夹中创建一个HTTP触发器,内容如下:
import json import os import requests import azure.functions as func def main(req: func.HttpRequest) -> func.HttpResponse: location = os.environ['SPEECH_LOCATION'] speech_key = os.environ['SPEECH_KEY'] req_body = req.get_json() language = req_body['language'] url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list' headers = { 'Ocp-Apim-Subscription-Key': speech_key } response = requests.get(url, headers=headers) voices_json = json.loads(response.text) voices = filter(lambda x: x['Locale'].lower() == language.lower(), voices_json) voices = map(lambda x: x['ShortName'], voices) return func.HttpResponse(json.dumps(list(voices)), status_code=200)
这段代码会向端点发出HTTP请求以获取语音列表。这个语音列表是一个包含所有语言语音的大块JSON文档,因此需要过滤出请求体中指定语言的语音,然后提取短名称并返回为JSON列表。所需的只是短名称,因此只返回这个值。
可以根据需要更改筛选条件以选择特定的语音。
这将数据的大小从77KB(在写作时)减少到更小的JSON文档。例如,对于美国英语,这只有408字节。
在本地运行你的函数应用。然后可以使用像curl这样的工具来调用它,就像测试你的 text-to-timer HTTP触发器一样。确保传递语言作为JSON主体:
{ "language":"<language>" }
将 <language> with your language, such as en-GB, or zh-CN.
You can find this code in the code-spoken-response/functions folder.
Open the smart-timer project in VS Code if it is not already open.
Open the config.h 标头文件并添加函数应用的URL:
const char *GET_VOICES_FUNCTION_URL = "<URL>";
将 <URL> with the URL for the get-voices HTTP trigger on your function app. This will be the same as the value for TEXT_TO_TIMER_FUNCTION_URL, except with a function name of get-voices instead of text-to-timer.
Create a new file in the src folder called text_to_speech.h. This will be used to define a class to convert from text to speech.
Add the following include directives to the top of the new text_to_speech.h 文件:
#pragma once #include <Arduino.h> #include <ArduinoJson.h> #include <HTTPClient.h> #include <Seeed_FS.h> #include <SD/Seeed_SD.h> #include <WiFiClient.h> #include <WiFiClientSecure.h> #include "config.h" #include "speech_to_text.h"
添加以下代码以声明 TextToSpeech 类,以及可以在应用程序其余部分使用的实例:
class TextToSpeech { public: private: }; TextToSpeech textToSpeech;
要调用你的函数应用,需要声明一个WiFi客户端。在类的 private 部分添加以下内容:
WiFiClient _client;
在 private 部分,添加一个字段来保存选定的语音:
String _voice;
在 public section, add an init 函数中,获取第一个语音:
void init() { }
获取语音时,需要发送带有语言的JSON文档。在 init 函数中添加以下代码以创建此JSON文档:
DynamicJsonDocument doc(1024); doc["language"] = LANGUAGE; String body; serializeJson(doc, body);
接下来创建一个 HTTPClient,然后使用它来调用函数应用以获取语音,发送JSON文档:
HTTPClient httpClient; httpClient.begin(_client, GET_VOICES_FUNCTION_URL); int httpResponseCode = httpClient.POST(body);
在此之后,添加代码检查响应码,如果它是200(成功),则提取语音列表,从列表中检索第一个语音:
if (httpResponseCode == 200) { String result = httpClient.getString(); Serial.println(result); DynamicJsonDocument doc(1024); deserializeJson(doc, result.c_str()); JsonArray obj = doc.as<JsonArray>(); _voice = obj[0].as<String>(); Serial.print("Using voice "); Serial.println(_voice); } else { Serial.print("Failed to get voices - error "); Serial.println(httpResponseCode); }
在此之后,结束HTTP客户端连接:
httpClient.end();
打开 main.cpp 文件,在顶部添加以下包含指令以包含此新头文件:
#include "text_to_speech.h"
在 setup function, underneath the call to speechToText.init();, add the following to initialize the TextToSpeech 类:
textToSpeech.init();
构建此代码,上传到你的Wio Terminal并通过串行监视器进行测试。确保你的函数应用正在运行。
你将看到来自函数应用的可用语音列表,以及所选的语音。
--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time --- More details at http://bit.ly/pio-monitor-filters --- Miniterm on /dev/cu.usbmodem1101 9600,8,N,1 --- --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- Connecting to WiFi.. Connected! Got access token. ["en-US-JennyNeural", "en-US-JennyMultilingualNeural", "en-US-GuyNeural", "en-US-AriaNeural", "en-US-AmberNeural", "en-US-AnaNeural", "en-US-AshleyNeural", "en-US-BrandonNeural", "en-US-ChristopherNeural", "en-US-CoraNeural", "en-US-ElizabethNeural", "en-US-EricNeural", "en-US-JacobNeural", "en-US-MichelleNeural", "en-US-MonicaNeural", "en-US-AriaRUS", "en-US-BenjaminRUS", "en-US-GuyRUS", "en-US-ZiraRUS"] Using voice en-US-JennyNeural Ready.
一旦有了要使用的语音,就可以将其用于将文本转换为语音。在将语音转换为文本时,与语音相关的内存限制也同样适用,因此需要将语音写入SD卡,以便通过ReSpeaker播放。
在本项目之前的课程中,你使用闪存存储了来自麦克风的语音。本课程使用SD卡,因为它更容易使用Seeed音频库播放音频。
还需要考虑另一个限制:语音服务提供的可用音频数据以及Wio Terminal支持的格式。与全功能计算机不同,微控制器上的音频库可能仅支持有限的音频格式。例如,可以使用Seeed Arduino Audio库通过ReSpeaker播放声音,但仅支持44.1KHz采样率的音频。Azure语音服务可以提供多种格式的音频,但它们都不使用这种采样率,仅提供8KHz、16KHz、24KHz和48KHz的音频。这意味着音频需要重新采样到44.1KHz,这需要Wio Terminal更多的资源,尤其是内存。
当需要处理此类数据时,通常最好使用无服务器代码,尤其是在数据通过Web调用获取的情况下。Wio Terminal可以调用无服务器函数,传入要转换的文本,无服务器函数可以同时调用语音服务以将文本转换为语音,并重新采样音频以达到所需采样率。然后它可以将音频以Wio Terminal需要的形式返回,存储在SD卡上并使用ReSpeaker播放。
打开你的 smart-timer-trigger project in VS Code, and open the terminal ensuring the virtual environment is activated. If not, kill and re-create the terminal.
Add a new HTTP trigger to this app called text-to-speech 使用以下命令从函数应用项目的根目录内的VS Code终端中执行:
func new --name text-to-speech --template "HTTP trigger"
这将在 text-to-speech.
The librosa Pip package has functions to re-sample audio, so add this to the requirements.txt 文件:
librosa
添加完这些后,使用以下命令从VS Code终端安装Pip包:
```sh pip install -r requirements.txt ```
⚠️ 如果你使用的是Linux,包括Raspberry Pi OS,你可能需要安装
libsndfile,使用以下命令:
> sudo apt update > sudo apt install libsndfile1-dev > ```
要将文本转换为语音,不能直接使用语音API密钥,而是需要请求访问令牌,使用API密钥对访问令牌请求进行身份验证。打开 __init__.py file from the text-to-speech 文件夹并替换其中的所有代码为以下内容:
import io import os import requests import librosa import soundfile as sf import azure.functions as func location = os.environ['SPEECH_LOCATION'] speech_key = os.environ['SPEECH_KEY'] def get_access_token(): headers = { 'Ocp-Apim-Subscription-Key': speech_key } token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken' response = requests.post(token_endpoint, headers=headers) return str(response.text)
这为位置和语音密钥定义了常量,这些密钥将从设置中读取。然后定义了一个 get_access_token 函数,用于获取语音服务的访问令牌。
在此代码下方,添加以下内容:
playback_format = 'riff-48khz-16bit-mono-pcm' def main(req: func.HttpRequest) -> func.HttpResponse: req_body = req.get_json() language = req_body['language'] voice = req_body['voice'] text = req_body['text'] url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/v1' headers = { 'Authorization': 'Bearer ' + get_access_token(), 'Content-Type': 'application/ssml+xml', 'X-Microsoft-OutputFormat': playback_format } ssml = f'<speak version=\'1.0\' xml:lang=\'{language}\'>' ssml += f'<voice xml:lang=\'{language}\' name=\'{voice}\'>' ssml += text ssml += '</voice>' ssml += '</speak>' response = requests.post(url, headers=headers, data=ssml.encode('utf-8')) raw_audio, sample_rate = librosa.load(io.BytesIO(response.content), sr=48000) resampled = librosa.resample(raw_audio, sample_rate, 44100) output_buffer = io.BytesIO() sf.write(output_buffer, resampled, 44100, 'PCM_16', format='wav') output_buffer.seek(0) return func.HttpResponse(output_buffer.read(), status_code=200)
这定义了一个HTTP触发器,将文本转换为语音。它从请求的JSON主体中提取要转换的文本、语言和语音,构建一些SSML以请求语音,然后使用访问令牌调用相关的REST API。此REST API调用返回编码为16位、48KHz单声道WAV文件的音频,由 playback_format, which is sent to the REST API call.
This is then re-sampled by `librosa` from a sample rate of 48KHz to a sample rate of 44.1KHz, then this audio is saved to a binary buffer that is then returned.
Run your function app locally, or deploy it to the cloud. You can then call this using a tool like curl in the same way that you tested your text-to-timer HTTP触发器。请确保以语言、语音和文本作为JSON主体传递:
{ "language": "<language>", "voice": "<voice>", "text": "<text>" }
将 <language> with your language, such as en-GB, or zh-CN. Replace <voice> with the voice you want to use. Replace <text> 替换为你想要转换成语音的文本。你可以将输出保存到文件并使用任何可以播放WAV文件的音频播放器播放。
例如,要使用US英语和Jenny Neural语音将“Hello”转换为语音,函数应用在本地运行时,可以使用以下curl命令:
```sh curl -X GET 'http://localhost:7071/api/text-to-speech' \ -H 'Content-Type: application/json' \ -o hello.wav \ -d '{ "language":"en-US", "voice": "en-US-JennyNeural", "text": "Hello" }' ```
这将音频保存到 hello.wav in the current directory.
You can find this code in the code-spoken-response/functions folder.
Open the smart-timer project in VS Code if it is not already open.
Open the config.h 标头文件并添加函数应用的URL:
const char *TEXT_TO_SPEECH_FUNCTION_URL = "<URL>";
将 <URL> with the URL for the text-to-speech HTTP trigger on your function app. This will be the same as the value for TEXT_TO_TIMER_FUNCTION_URL, except with a function name of text-to-speech instead of text-to-timer.
Open the text_to_speech.h header file, and add the following method to the public section of the TextToSpeech 类:
void convertTextToSpeech(String text) { }
在 convertTextToSpeech 方法中,添加以下代码以创建要发送给函数应用的JSON:
DynamicJsonDocument doc(1024); doc["language"] = LANGUAGE; doc["voice"] = _voice; doc["text"] = text; String body; serializeJson(doc, body);
这将语言、语音和文本写入JSON文档,然后序列化为字符串。
在此之下,添加以下代码以调用函数应用:
HTTPClient httpClient; httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL); int httpResponseCode = httpClient.POST(body);
这创建了一个HTTPClient,然后使用JSON文档向文本到语音HTTP触发器发出POST请求。
如果调用成功,可以从函数应用调用返回的原始二进制数据流式传输到SD卡上的文件。添加以下代码以实现此目的:
if (httpResponseCode == 200) { File wav_file = SD.open("SPEECH.WAV", FILE_WRITE); httpClient.writeToStream(&wav_file); wav_file.close(); } else { Serial.print("Failed to get speech - error "); Serial.println(httpResponseCode); }
这段代码检查响应,如果是200(成功),则将二进制数据流式传输到SD卡根目录中的 SPEECH.WAV 文件。
在此方法的末尾,关闭HTTP连接:
httpClient.end();
现在可以将要说话的文本转换为音频。在 main.cpp file, add the following line to the end of the say 函数中,将文本转换为音频:
textToSpeech.convertTextToSpeech(text);
即将推出
之所以运行本地函数应用是因为 librosa Pip package on linux has a dependency on a library that is not installed by default, and will need to be installed before the function app can run. Function apps are serverless - there are no servers you can manage yourself, so no way to install this library up front.
The way to do this is instead to deploy your functions app using a Docker container. This container is deployed by the cloud whenever it needs to spin up a new instance of your function app (such as when the demand exceeds the available resources, or if the function app hasn't been used for a while and is closed down).
You can find the instructions to set up a function app and deploy via Docker in the create a function on Linux using a custom container documentation on Microsoft Docs.
Once this has been deployed, you can port your Wio Terminal code to access this function:
Add the Azure Functions certificate to config.h:
const char *FUNCTIONS_CERTIFICATE = "-----BEGIN CERTIFICATE-----\r\n" "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\r\n" "-----END CERTIFICATE-----\r\n";
更改所有 <WiFiClient.h> to <WiFiClientSecure.h>.
Change all WiFiClient fields to WiFiClientSecure.
In every class that has a WiFiClientSecure 字段,在构造函数中添加证书:
_client.setCACert(FUNCTIONS_CERTIFICATE);
声明:
本文件灏天文库团队进行了翻译。尽管我们力求准确,但请注意,翻译可能包含错误或不准确之处。原文档以其原始语言为准。我们不对因使用此翻译而产生的任何误解或误译负责。