IoT-For-Beginners 消费级项目实战:在 Wio Terminal 上实现文本转语音(Text to Speech)
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本文基于 Azure IoT 教育项目IoT-For-Beginners的 "消费级 IoT(Consumer IoT)" 单元第 23 课——"设置定时器并提供语音反馈"(课程目录),完整讲解如何在Wio Terminal上利用 Azure Speech 服务将文本合成为语音,并通过 ReSpeaker 扬声器播放。你将掌握:为嵌入式设备搭建"语音列表查询 + 文本转语音"两套 Azure Functions 无服务器函数、用 librosa 解决 44.1KHz 采样率重采样问题、把合成音频流式写入 SD 卡,以及用 Docker 容器将函数应用部署到云端并完成 TLS 证书迁移。读完即可在本地构建出一个能"开口说话"的智能定时器。
文本转语音(Text to Speech)概述
上一课中,你用 Azure Speech 服务把语音(Speech)转换成了文本(Text),即"语音转文本(Speech to Text)";而本课要做的是反方向的"文本转语音(Text to Speech,TTS)"——把一段文本合成为包含语音的音频数据。
整个智能定时器的交互闭环是:用户对着麦克风说出"set a 3 minute timer"→ 语音转文本 → LUIS 语言理解提取定时时长 → 设备设置定时器 →文本转语音把"timer started / Times up"等提示文本合成为语音播放出来。本文聚焦最后一环:文本转语音。
在前一课中你已经使用过 Speech 服务 SDK 完成语音转文本,而同一个 Speech 服务同样可用于把文本转回语音,无需额外引入新的云服务。
获取可用语音列表
调用文本转语音 API 时,必须显式指定使用哪个"语音(voice)"来合成,因为每种语言都支持一批不同的发音人(不同音色、性别、风格)。每种语言支持的语音列表可以通过 Speech 服务提供的 REST 接口获取。
为什么不能直接在 Wio Terminal 上获取语音列表
这里正是微控制器(MCU)的局限所在:获取文本转语音服务所支持的全部语音列表,返回的是一份超过 77KB 的 JSON 文档,对 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" }这是一份Aria语音的元数据,包含多个语音风格(StyleList)。而真正用于文本转语音请求的,其实只有它的短名称(ShortName)en-US-AriaNeural。
因此正确的做法不是把整份列表下载到单片机解码,而是编写无服务器(Serverless)代码:由云函数按你使用的语言把列表过滤、裁剪到极小体积,Wio Terminal 再通过 HTTP 调用这个云函数获取精简后的语音列表,并从列表中挑选合适的语音(例如第一个)。
任务一:创建获取语音列表的无服务器函数(get-voices)
仓库中已提供完整参考实现:functions/smart-timer-trigger/get-voices。
1. 打开smart-timer-trigger函数项目并激活虚拟环境
在 VS Code 中打开smart-timer-trigger项目并打开终端,确认虚拟环境已激活;如果没有,杀掉终端重新创建。
2. 在local.settings.json中配置 Speech 密钥与区域
打开local.settings.json,加入语音 API 密钥和区域设置(仓库参考文件见 local.settings.json):
"SPEECH_KEY": "<key>", "SPEECH_LOCATION": "<location>"- 将
<key>替换为你的 Speech 服务资源的 API 密钥; - 将
<location>替换为你创建 Speech 服务资源时使用的区域,如eastasia、westeurope。
该文件中的Values是 Azure Functions 运行时注入的环境变量,Python 代码里通过os.environ['SPEECH_KEY']/os.environ['SPEECH_LOCATION']读取。
3. 新建名为get-voices的 HTTP 触发器
在函数应用项目根目录下的 VS Code 终端中执行:
func new --name get-voices --template "HTTP trigger"这会在项目中创建get-voices文件夹及对应的__init__.py、function.json。
4. 用以下代码替换get-voices/__init__.py的全部内容
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 GET 请求。返回的语音列表是包含所有语言语音的一大块 JSON,于是代码先按请求体中传入的language用filter过滤出该语言的语音,再用map提取出每个语音的ShortName,最后以 JSON 数组形式返回。因为文本转语音请求只需要短名称,所以只返回这一个字段,把数据体积从 77KB(撰写时)大幅压缩——例如仅返回美国英语语音时只有408 字节。
💁 提示:你可以按需调整过滤条件,只挑选你偏好的语音。
5. 本地运行并用 curl 测试
启动函数应用后,用 curl 这类工具调用(测试方式与text-to-timerHTTP 触发器相同),注意把语言作为 JSON 请求体传入:
{ "language":"<language>" }将<language>替换为你的语言代码,例如en-GB、zh-CN或en-US。
任务二:从 Wio Terminal 获取语音
1. 打开smart-timer工程并配置函数 URL
在 VS Code 中打开smart-timer工程(仓库参考工程见 code-spoken-response/wio-terminal/smart-timer)。打开config.h头文件,添加函数应用 URL:
const char *GET_VOICES_FUNCTION_URL = "<URL>";将<URL>替换为你函数应用中get-voicesHTTP 触发器的 URL。它和TEXT_TO_TIMER_FUNCTION_URL基本一致,只是函数名从text-to-timer换成get-voices。仓库的 config.h 中三个函数 URL 并列展示如下:
const char *TEXT_TO_TIMER_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/text-to-timer"; const char *GET_VOICES_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/get-voices"; const char *TEXT_TO_SPEECH_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/text-to-speech";2. 新建src/text_to_speech.h头文件
在src文件夹下新建text_to_speech.h,用于定义"文本转语音"类。文件顶部加入以下 include 指令:
#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"其中ArduinoJson用于构造/解析 JSON,HTTPClient用于调用无服务器函数,Seeed_FS.h与SD/Seeed_SD.h用于把音频写入 SD 卡,WiFiClient.h/WiFiClientSecure.h用于本地 HTTP 与云端 HTTPS 两种场景(仓库参考实现见 text_to_speech.h,其中仅保留了config.h的包含)。
3. 声明TextToSpeech类与全局实例
class TextToSpeech { public: private: }; TextToSpeech textToSpeech;textToSpeech作为全局实例供应用其余部分使用。
4. 声明 WiFi 客户端
调用函数应用需要 WiFi 客户端,在类的private区添加:
WiFiClient _client;5. 添加所选语音字段
在private区添加保存选中语音的字段:
String _voice;6. 添加init函数获取第一个语音
在public区添加一个init函数,用于获取第一个语音:
void init() { }7. 构造携带语言的 JSON 文档
语音列表接口需要语言参数,在init中构造 JSON 并序列化:
DynamicJsonDocument doc(1024); doc["language"] = LANGUAGE; String body; serializeJson(doc, body);LANGUAGE是config.h中定义的宏(如"en-US"),DynamicJsonDocument(1024)预留了 1KB 动态内存——考虑到 Wio Terminal 的 SRAM 有限,文档按需分配内存是这里的关键设计。
8. 创建 HTTPClient 并 POST JSON
HTTPClient httpClient; httpClient.begin(_client, GET_VOICES_FUNCTION_URL); int httpResponseCode = httpClient.POST(body);9. 检查响应码并取出第一个语音
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); }响应为 200(成功)时,把返回的语音短名称 JSON 数组解析出来,取第 0 项存入_voice;失败则打印错误码便于排查。
10. 关闭 HTTP 连接
httpClient.end();11. 在main.cpp中集成
打开main.cpp,在文件顶部添加:
#include "text_to_speech.h"并在setup函数中、speechToText.init();调用之下初始化:
textToSpeech.init();仓库 main.cpp 的setup顺序是:连接 WiFi → 初始化 Flash(sfud_init)→ 初始化麦克风mic.init()→speechToText.init()→textToSpeech.init(),最后打印Ready.。
12. 构建、上传并观察串口输出
构建代码、上传到 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.可以看到,串口输出了 19 个 en-US 语音的短名称列表,设备自动选中第一个en-US-JennyNeural作为合成语音。
将文本转换为语音
拿到语音后,就可以用它把文本合成为音频了。语音列表遇到的内存限制同样存在于语音合成环节——合成返回的音频不能直接缓存在内存中,因此需要把音频写入 SD 卡,再通过 ReSpeaker 播放。
💁 本课程项目前面的课程里,你用 Flash 存储器保存从麦克风采集到的语音;本课改用 SD 卡,是因为用 Seeed 音频库从 SD 卡播放音频更简单。
采样率不匹配:44.1KHz 限制
还有一个必须考虑的限制:Speech 服务返回的音频格式与 Wio Terminal 支持的格式不一致。与完整计算机不同,微控制器的音频库对音频格式的支持非常有限。例如,能在 ReSpeaker 上播放声音的Seeed Arduino Audio 库只支持 44.1KHz 采样率的音频;而 Azure Speech 服务虽然能以多种格式提供音频,但只用8KHz、16KHz、24KHz 和 48KHz这些采样率。这意味着音频必须被重采样到 44.1KHz——这项工作对 Wio Terminal 来说资源开销过大,尤其是内存。
处理这类数据变换时,更好的做法是交给无服务器代码,特别是数据本身就来自 Web 调用时:Wio Terminal 调用一个无服务器函数,把要转换的文本传过去,函数内既调用 Speech 服务完成文本转语音,又顺便把音频重采样到所需采样率,最后把 Wio Terminal 需要的音频形式返回,由设备存入 SD 卡、经 ReSpeaker 播放。这样把重采样的计算压力从单片机转移到了云端。
任务三:创建文本转语音的无服务器函数(text-to-speech)
仓库参考实现见 functions/smart-timer-trigger/text-to-speech。
1. 新建text-to-speechHTTP 触发器
在smart-timer-trigger项目根目录的 VS Code 终端中执行:
func new --name text-to-speech --template "HTTP trigger"2. 在requirements.txt中加入 librosa
librosa):
librosa随后安装依赖:
pip install -r requirements.txt⚠️ 如果你使用 Linux(包括 Raspberry Pi OS),可能需要先安装
libsndfile:sudo apt update sudo apt install libsndfile1-dev
3. 用访问令牌替代 API 密钥
与"获取语音列表"不同,调用文本转语音 REST API 时不能直接使用 API 密钥,必须先向令牌端点换取一个访问令牌(Access Token),再用令牌认证 TTS 请求。打开text-to-speech文件夹中的__init__.py,用以下代码替换全部内容:
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函数:向区域专属的 STS 令牌端点发起 POST,把 API 密钥放在Ocp-Apim-Subscription-Key头中,返回响应体即访问令牌。
4. 添加主处理函数:SSML 合成 + librosa 重采样
在这段代码下方继续添加:
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)关键点拆解:
playback_format = 'riff-48khz-16bit-mono-pcm'声明了期望的输出格式:16 位、48KHz、单声道、RIFF(WAV)封装,通过X-Microsoft-OutputFormat头传给 REST API;- HTTP 触发器从 JSON 请求体中提取
language、voice、text三个字段; - 构造 SSML(语音合成标记语言):
<speak>根节点声明语言,<voice name='...'>指定发音人,文本放在其中; - 携带
Authorization: Bearer <token>访问令牌调用 TTS REST API; - 用
librosa.load(..., sr=48000)以 48KHz 载入返回的音频,再通过librosa.resample重采样到44.1KHz; - 最后用
soundfile(sf.write)把重采样后的音频以 16 位 PCM WAV 格式写入二进制缓冲区返回。
5. 本地运行并用 curl 测试
本地运行函数应用(或部署到云端),用 curl 调用。请求体必须包含语言、语音和文本三个字段:
{ "language": "<language>", "voice": "<voice>", "text": "<text>" }<language>填语言代码,如en-GB、zh-CN;<voice>填你想用的语音短名称;<text>填要转换为语音的文本。
你可以把输出保存成文件,用任意能播放 WAV 的音频播放器试听。例如,用 Jenny Neural 语音把 "Hello" 合成为美式英语语音(函数应用本地运行时):
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。
任务四:从 Wio Terminal 获取语音
1. 配置TEXT_TO_SPEECH_FUNCTION_URL
打开config.h,添加函数应用 URL:
const char *TEXT_TO_SPEECH_FUNCTION_URL = "<URL>";将<URL>替换为函数应用中text-to-speechHTTP 触发器的 URL,它与TEXT_TO_TIMER_FUNCTION_URL一致,仅函数名不同。
2. 添加convertTextToSpeech方法
打开text_to_speech.h,在TextToSpeech类的public区添加:
void convertTextToSpeech(String text) { }3. 构造请求 JSON
在方法内添加代码,生成要发给函数应用的 JSON:
DynamicJsonDocument doc(1024); doc["language"] = LANGUAGE; doc["voice"] = _voice; doc["text"] = text; String body; serializeJson(doc, body);这里把语言、语音(来自_voice字段)和文本写入 JSON 文档并序列化为字符串。
4. 调用函数应用
HTTPClient httpClient; httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL); int httpResponseCode = httpClient.POST(body);创建 HTTPClient 并用 JSON 文档向text-to-speechHTTP 触发器发起 POST。
5. 把返回的二进制音频流式写入 SD 卡
调用成功时,把函数应用返回的原始二进制数据流式写入 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文件。writeToStream是HTTPClient提供的流式写接口,避免把整段音频读进内存——这正是应对内存限制的关键手法。
6. 关闭 HTTP 连接
在方法末尾:
httpClient.end();7. 在say函数中触发合成
在main.cpp的say函数末尾添加一行,把要说的文本转换为音频(仓库 main.cpp 中完整实现如下):
void say(String text) { Serial.println(text); textToSpeech.convertTextToSpeech(text); }say是整个语音反馈的入口:定时器启动时调用say(begin_message)播报"timer started",定时器到期时通过timer.in(total_seconds * 1000, timerExpired, ...)注册的回调timerExpired调用say(end_message)播报"Times up on your ... timer"。
播放音频
在本文对应的课程节点中,通过 ReSpeaker 播放SPEECH.WAV的内容被标注为"Coming soon"(即将推出),仓库代码中尚未包含完整的播放实现。可以参考课程说明中"用 Seeed 音频库从 SD 卡播放音频更容易"的提示,在后续课程内容中补充播放环节。
将函数应用部署到云端
为什么需要 Docker 容器
本地运行时,librosa这个 Pip 包在 Linux 上有一个默认不会安装的依赖库(即libsndfile),必须在函数应用运行前先安装。而函数应用是无服务器的——没有你可以自己管理的服务器,自然也就无法预先安装这个库。
解决方案是用 Docker 容器部署函数应用:这个容器由云平台按需部署,每当需要为函数应用启动一个新实例时(例如需求超过可用资源时,或函数应用长时间未用被关闭后再被唤起时),都会基于你的自定义镜像拉起,从而保证libsndfile等依赖始终就绪。创建函数应用并通过 Docker 部署的完整步骤可参考 Microsoft Docs 中"使用 Linux 自定义容器创建函数"的文档。
部署后把 Wio Terminal 代码切换到 HTTPS
部署完成后,函数应用会提供 HTTPS 地址,Wio Terminal 代码需要做以下迁移:
1. 把 Azure Functions 证书加入config.h
const char *FUNCTIONS_CERTIFICATE = "-----BEGIN CERTIFICATE-----\r\n" "MIIFWjCCBEKgAwIBAgIQDxSWXyAgaZlP1ceseIlB4jANBgkqhkiG9w0BAQsFADBa\r\n" "MQswCQYDVQQGEwJJRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJl\r\n" "clRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTIw\r\n" "MDcyMTIzMDAwMFoXDTI0MTAwODA3MDAwMFowTzELMAkGA1UEBhMCVVMxHjAcBgNV\r\n" "BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEgMB4GA1UEAxMXTWljcm9zb2Z0IFJT\r\n" "QSBUTFMgQ0EgMDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqYnfP\r\n" "mmOyBoTzkDb0mfMUUavqlQo7Rgb9EUEf/lsGWMk4bgj8T0RIzTqk970eouKVuL5R\r\n" "IMW/snBjXXgMQ8ApzWRJCZbar879BV8rKpHoAW4uGJssnNABf2n17j9TiFy6BWy+\r\n" "IhVnFILyLNK+W2M3zK9gheiWa2uACKhuvgCca5Vw/OQYErEdG7LBEzFnMzTmJcli\r\n" "W1iCdXby/vI/OxbfqkKD4zJtm45DJvC9Dh+hpzqvLMiK5uo/+aXSJY+SqhoIEpz+\r\n" "rErHw+uAlKuHFtEjSeeku8eR3+Z5ND9BSqc6JtLqb0bjOHPm5dSRrgt4nnil75bj\r\n" "c9j3lWXpBb9PXP9Sp/nPCK+nTQmZwHGjUnqlO9ebAVQD47ZisFonnDAmjrZNVqEX\r\n" "F3p7laEHrFMxttYuD81BdOzxAbL9Rb/8MeFGQjE2Qx65qgVfhH+RsYuuD9dUw/3w\r\n" "ZAhq05yO6nk07AM9c+AbNtRoEcdZcLCHfMDcbkXKNs5DJncCqXAN6LhXVERCw/us\r\n" "G2MmCMLSIx9/kwt8bwhUmitOXc6fpT7SmFvRAtvxg84wUkg4Y/Gx++0j0z6StSeN\r\n" "0EJz150jaHG6WV4HUqaWTb98Tm90IgXAU4AW2GBOlzFPiU5IY9jt+eXC2Q6yC/Zp\r\n" "TL1LAcnL3Qa/OgLrHN0wiw1KFGD51WRPQ0Sh7QIDAQABo4IBJTCCASEwHQYDVR0O\r\n" "BBYEFLV2DDARzseSQk1Mx1wsyKkM6AtkMB8GA1UdIwQYMBaAFOWdWTCCR1jMrPoI\r\n" "VDaGezq1BE3wMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYI\r\n" "KwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADA0BggrBgEFBQcBAQQoMCYwJAYI\r\n" "KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTA6BgNVHR8EMzAxMC+g\r\n" "LaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vT21uaXJvb3QyMDI1LmNybDAq\r\n" "BgNVHSAEIzAhMAgGBmeBDAECATAIBgZngQwBAgIwCwYJKwYBBAGCNyoBMA0GCSqG\r\n" "SIb3DQEBCwUAA4IBAQCfK76SZ1vae4qt6P+dTQUO7bYNFUHR5hXcA2D59CJWnEj5\r\n" "na7aKzyowKvQupW4yMH9fGNxtsh6iJswRqOOfZYC4/giBO/gNsBvwr8uDW7t1nYo\r\n" "DYGHPpvnpxCM2mYfQFHq576/TmeYu1RZY29C4w8xYBlkAA8mDJfRhMCmehk7cN5F\r\n" "JtyWRj2cZj/hOoI45TYDBChXpOlLZKIYiG1giY16vhCRi6zmPzEwv+tk156N6cGS\r\n" "Vm44jTQ/rs1sa0JSYjzUaYngoFdZC4OfxnIkQvUIA4TOFmPzNPEFdjcZsgbeEz4T\r\n" "cGHTBPK4R28F44qIMCtHRV55VMX53ev6P3hRddJb\r\n" "-----END CERTIFICATE-----\r\n";这是用于验证 Azure Functions 服务端证书的 CA 证书链,是 TLS 握手验证所必需的公钥材料。Wio Terminal 的 mbedTLS 栈需要把根证书以 PEM 格式内嵌到固件中才能建立可信的 HTTPS 连接。
2. 将<WiFiClient.h>包含全部改为<WiFiClientSecure.h>
3. 把所有WiFiClient字段改为WiFiClientSecure
4. 在每个含WiFiClientSecure字段的类中添加构造函数并设置证书
_client.setCACert(FUNCTIONS_CERTIFICATE);在构造函数里调用setCACert把内嵌的 CA 证书绑定到安全客户端上,此后HTTPClient便可通过该安全客户端访问 HTTPS 的云端函数应用。这组证书迁移操作适用于TextToSpeech、SpeechToText等所有需要调用云端函数的类。
💁 本地开发时函数应用走
http://明文地址,用普通WiFiClient即可;部署到云端后必须切换为WiFiClientSecure并携带证书。仓库 config.h 中还保留了本地调用 Speech 服务令牌端点所需的TOKEN_CERTIFICATE与SPEECH_CERTIFICATE,供对比参考。
小结与参考实现
本文完整复现了 Wio Terminal 文本转语音的整条链路:
- 语音列表裁剪:用
get-voices无服务器函数把 77KB 的全量语音列表按语言过滤成几百字节的短名称数组(get-voices/init.py); - 云端合成与重采样:用
text-to-speech无服务器函数申请访问令牌、构造 SSML、调用 TTS REST API,再用 librosa 把 48KHz 音频重采样为 44.1KHz WAV(text-to-speech/init.py); - 设备端播放准备:Wio Terminal 通过
TextToSpeech类获取语音、合成并把音频流式写入 SD 卡SPEECH.WAV(text_to_speech.h); - 云端部署:用 Docker 容器解决
librosa的libsndfile依赖,并把 Wio Terminal 切换到 HTTPS +WiFiClientSecure+ CA 证书。
工程依赖(platformio.ini)主要包括Seeed Arduino FS、Seeed Arduino rpcWiFi、Seeed_Arduino_mbedtls、ArduinoJson 6.17.3、arduino-timer 2.3.0等库,整体在 PlatformIO + Arduino 框架下构建(板型seeed_wio_terminal)。想动手复现的读者,可以直接对照 code-spoken-response 目录中的函数端与设备端代码,结合本文步骤逐步搭建自己的"会说话的智能定时器"。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考