ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

IoT-For-Beginners 实战:在 Wio Terminal 上实现文本转语音(Text to Speech)语音反馈

2026/9/14 4:47:51 拓冰建站 浏览量
IoT-For-Beginners 实战:在 Wio Terminal 上实现文本转语音(Text to Speech)语音反馈 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本篇技术指南基于IoT-For-Beginners仓库第 23 课「设置定时器并提供语音反馈」中 Wio Terminal 硬件版本的章节完整讲解如何在资源受限的微控制器上实现文本转语音TTS功能。你将掌握用 Azure Functions 无服务器函数解决设备内存不足的难题语音列表从 77KB 压缩到约 408 字节、用 librosa 将 48kHz 语音重采样为 ReSpeaker 播放器所需的 44.1kHz、在 Wio Terminal 上通过 SD 卡存储并播放语音以及将函数应用部署到云端时所需的 Docker 容器与 TLS 证书配置。一、为什么要在微控制器上做文本转语音在本项目前两课中你已经实现了语音 → 文本语音识别和文本 → 意图语言理解智能定时器已经能听懂用户说设置一个 3 分钟定时器。而本课要补齐交互闭环的最后一环——反馈把确认信息用语音说给用户听。文本转语音Text to Speech的基本原理是把文本拆解为构成单词的基本音素phonemes再通过预录音频或 AI 模型生成的音频拼接出语音。TTS 系统通常包含三个阶段文本分析Text analysis、语言分析Linguistic analysis和波形生成Wave-form generation即上图images/tts-overview.png所示。本课使用的服务端仍是 Azure 认知服务语音服务但客户端从功能完整的电脑换成了Wio Terminal微控制器这带来两个棘手约束内存限制语音服务的接口返回数据可能高达几十 KB远超 Wio Terminal 能处理的范围音频格式不兼容Seeed 音频库只支持 44.1kHz 采样率而语音服务不提供该采样率。因此本课的核心架构思路是把重活全部交给无服务器函数Serverless FunctionsWio Terminal 只负责发起 HTTP 调用、接收结果、写 SD 卡。二、获取语音列表无服务器函数 get-voices2.1 问题根源215 个语音的 77KB JSON调用 TTS 时你必须指定使用哪个声音voice。每种语言都支持多个不同的声音而语音服务的 SDK 会返回一份全语言的语音列表。在本文档编写时完整列表包含 215 个语音JSON 文档体积超过77KB——这对 Wio Terminal 来说太大无法解析。列表中每个语音由一段 JSON 描述例如Aria声音{ 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 }注意关键字段ShortName如en-US-AriaNeural——调用 TTS 时只需要它其余字段都是元数据。Aria 还带有StyleListchat、customerservice、cheerful 等风格说明神经语音可以表达多种情感语调。正确的做法是编写一段无服务器代码在云端按语言过滤语音列表只把每个声音的 ShortName 返回给 Wio Terminal。设备端拿到这份精简列表后直接取第一个语音使用即可。2.2 创建 get-voices HTTP 触发器前提是你已完成前几课创建的smart-timer-trigger函数应用项目。打开 VS Code确保终端已激活虚拟环境未激活则重启终端。第一步在local.settings.json中添加语音服务配置SPEECH_KEY: key, SPEECH_LOCATION: locationkey替换为语音服务资源的 API 密钥location替换为创建语音服务资源时使用的区域如eastus。仓库中的实际模板文件 local.settings.json 还包含前几课的 LUIS 配置项完整结构如下{ IsEncrypted: false, Values: { FUNCTIONS_WORKER_RUNTIME: python, AzureWebJobsStorage: , LUIS_KEY: primary key, LUIS_ENDPOINT_URL: endpoint url, LUIS_APP_ID: app id, SPEECH_KEY: key, SPEECH_LOCATION: location } }第二步用 func CLI 创建 HTTP 触发器在函数应用项目的根目录下执行func new --name get-voices --template HTTP trigger第三步替换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 fhttps://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list headers { Ocp-Apim-Subscription-Key: speech_key } response requests.get(url, headersheaders) 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_code200)这份代码与仓库中 get-voices/init.py 的实现完全一致。其执行逻辑是从环境变量读取区域与密钥向https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list发起请求用Ocp-Apim-Subscription-Key请求头做订阅密钥认证用filter按请求体中的language字段大小写不敏感筛选出该语言的语音用map只提取每个语音的ShortName字段——这是 TTS 唯一需要的值把精简列表序列化为 JSON 返回。效果对比数据从77KB 骤降到极小体积——例如美式英语语音列表只有408 字节。 如果需要可以自定义filter条件只挑选你想要的特定语音。第四步本地运行并测试启动函数应用后用 curl 测试测试方式与之前测试text-to-timer触发器相同请求体为 JSON{ language:language }language替换为你的语言代码例如en-GB或zh-CN。 完整代码可在 code-spoken-response/functions 目录查看。2.3 Wio Terminal 端创建 TextToSpeech 类打开smart-timer项目在config.h中新增函数应用 URLconst char *GET_VOICES_FUNCTION_URL URL;URL是get-voices触发器的地址与TEXT_TO_TIMER_FUNCTION_URL类似只是函数名改为get-voices本地运行时类似http://IP_ADDRESS:7071/api/get-voices参见仓库 config.h。然后在src目录下新建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;在private区域声明 WiFi 客户端和选中语音字段WiFiClient _client; String _voice;在public区域添加init函数用于获取第一个可用语音void init() { }在init中构建语言 JSON 文档DynamicJsonDocument doc(1024); doc[language] LANGUAGE; String body; serializeJson(doc, body);创建HTTPClient并 POST 该 JSON 到函数应用HTTPClient httpClient; httpClient.begin(_client, GET_VOICES_FUNCTION_URL); int httpResponseCode httpClient.POST(body);检查响应码若为 200解析返回的 JSON 数组并取第一个语音作为_voiceif (httpResponseCode 200) { String result httpClient.getString(); Serial.println(result); DynamicJsonDocument doc(1024); deserializeJson(doc, result.c_str()); JsonArray obj doc.asJsonArray(); _voice obj[0].asString(); 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函数中speechToText.init();之后调用textToSpeech.init();。构建并上传到 Wio Terminal通过串口监视器PlatformIO Serial Monitor观察结果前提是函数应用正在运行。你会看到类似下面的输出——先打印函数应用返回的该语言全部语音 ShortName 列表再打印选中的语音--- 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: CtrlC | Menu: CtrlT | Help: CtrlT followed by CtrlH --- 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.三、把文本变成语音无服务器函数 text-to-speech3.1 为什么音频处理必须在云端做即使拿到了语音直接在 Wio Terminal 上合成语音仍然不可行存在两个限制存储限制与前面课程用闪存flash memory保存麦克风录音不同本课把生成的语音写到SD 卡因为用 Seeed 音频库从 SD 卡播放音频更简单采样率不匹配这是决定性的技术障碍——Seeed Arduino Audio 库通过 ReSpeaker 播放音频只支持 44.1kHz 采样率的音频而 Azure 语音服务只提供8kHz、16kHz、24kHz、48kHz四种采样率没有 44.1kHz。要把 48kHz 音频重采样到 44.1kHz需要远超 Wio Terminal 可用内存的计算资源。解决方案由无服务器函数同时完成 TTS 合成和音频重采样。Wio Terminal 只做三件事把文本发给函数 → 接收重采样后的 WAV 二进制数据 → 写入 SD 卡。3.2 创建 text-to-speech HTTP 触发器在函数应用根目录执行func new --name text-to-speech --template HTTP trigger添加依赖librosa 与 libsndfilelibrosa 是 Python 音频处理库提供重采样函数。把它加入requirements.txtlibrosa仓库中的实际 requirements.txt 内容如下注意librosa依赖soundfile代码里会用到soundfile as sf# Do not include azure-functions-worker as it may conflict with the Azure Functions platform azure-functions azure-cognitiveservices-language-luis librosa安装依赖pip install -r requirements.txt⚠️Linux 用户包括 Raspberry Pi OS注意librosa 依赖libsndfile系统库默认未安装需要先执行sudo apt update sudo apt install libsndfile1-dev这正是本课最后要求用 Docker 容器部署函数应用的根本原因——无服务器环境不允许你预装系统库。3.3 TTS 认证机制先用 API Key 换取访问令牌与get-voices直接用订阅密钥不同TTS REST API 需要Bearer 访问令牌。打开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 fhttps://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken response requests.post(token_endpoint, headersheaders) return str(response.text)get_access_token用 API 密钥向令牌端点https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken发起 POST返回的响应体就是访问令牌字符串。3.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 fhttps://{location}.tts.speech.microsoft.com/cognitiveservices/v1 headers { Authorization: Bearer get_access_token(), Content-Type: application/ssmlxml, X-Microsoft-OutputFormat: playback_format } ssml fspeak version\1.0\ xml:lang\{language}\ ssml fvoice xml:lang\{language}\ name\{voice}\ ssml text ssml /voice ssml /speak response requests.post(url, headersheaders, datassml.encode(utf-8)) raw_audio, sample_rate librosa.load(io.BytesIO(response.content), sr48000) resampled librosa.resample(raw_audio, sample_rate, 44100) output_buffer io.BytesIO() sf.write(output_buffer, resampled, 44100, PCM_16, formatwav) output_buffer.seek(0) return func.HttpResponse(output_buffer.read(), status_code200)这段代码与仓库 text-to-speech/init.py 完全对应核心流程分四步提取参数从请求 JSON 中取出language、voice、text三个字段构造 SSMLSSML语音合成标记语言用speak包裹voice name...指定说话的语言、声音和要朗读的文本以 UTF-8 编码作为 POST 请求体发送指定输出格式playback_format riff-48khz-16bit-mono-pcm通过X-Microsoft-OutputFormat请求头告知服务端返回48kHz、16bit、单声道的 RIFF/WAV 音频重采样librosa.load(..., sr48000)按 48kHz 读入音频librosa.resample重采样到44.1kHz最后用soundfile以PCM_16编码写成 WAV 并写入内存缓冲区返回。从源码实现看这四步形成一个完整的请求 → 合成 → 重采样 → 编码流水线返回给调用方的永远是 44.1kHz 单声道 16bit 的 WAV 二进制流——正是 Seeed 音频库可播放的格式。3.5 本地测试 text-to-speech 函数运行函数应用本地或云端均可用 curl 测试请求体需包含三个字段{ language: language, voice: voice, text: text }language语言代码如en-GB、zh-CNvoice要使用的语音 ShortNametext要转成语音的文本。输出可以保存为文件用任意支持 WAV 的播放器播放。例如本地运行函数应用时把 Hello 用美式英语的 Jenny Neural 语音合成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。3.6 Wio Terminal 端拉取语音写入 SD 卡在config.h中新增 URLconst char *TEXT_TO_SPEECH_FUNCTION_URL URL;在text_to_speech.h的public区域添加方法void convertTextToSpeech(String text) { }先构造要发送的 JSON语言、语音、文本DynamicJsonDocument doc(1024); doc[language] LANGUAGE; doc[voice] _voice; doc[text] text; String body; serializeJson(doc, body);创建 HTTPClient 并 POSTHTTPClient httpClient; httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL); int httpResponseCode httpClient.POST(body);调用成功HTTP 200时把函数应用返回的原始二进制数据流式写入 SD 卡根目录下的SPEECH.WAV文件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); }writeToStream直接把 HTTP 响应体逐块写入 SD 卡文件避免在有限的内存中缓存整个 WAV 文件——这是微控制器场景下处理较大二进制数据的标准做法。最后关闭连接httpClient.end();把convertTextToSpeech接入main.cpp的say函数末尾让要朗读的文本真正变成音频textToSpeech.convertTextToSpeech(text);至此完整的语音反馈链路已打通语音识别 → 意图理解 → 设置定时器 → 生成反馈文本 → TTS 函数合成并重采样 → WAV 写入 SD 卡。文档中从 Wio Terminal 播放音频的小节标注为即将推出Coming soon也就是说通过 ReSpeaker 实际播放SPEECH.WAV的代码尚未在本文档中给出读者可自行结合 Seeed 音频库的TMRpcm等播放接口进行扩展。四、部署到云端Docker 容器与 TLS 证书4.1 为什么必须用 Docker 部署之前一直本地运行函数应用是因为 librosa 在 Linux 上依赖libsndfile系统库该库在默认镜像中并未安装必须在函数应用运行前装好。但函数应用是无服务器的——没有可以自行管理的服务器无法预装系统库。解决办法是把函数应用打包成Docker 容器部署。云平台每次需要启动新实例时例如请求量超出当前资源、或函数闲置一段时间被回收后再启动都会拉起这个容器从而保证libsndfile环境就绪。4.2 部署后的设备端调整Docker 部署后的函数应用走 HTTPSWio Terminal 需要校验 Azure Functions 的 TLS 证书。按以下步骤修改设备代码在config.h中添加 Azure Functions 证书常量FUNCTIONS_CERTIFICATE包含完整的 PEM 格式证书字符串把所有#include WiFiClient.h替换为#include WiFiClientSecure.h把所有WiFiClient类型字段改为WiFiClientSecure在每个包含WiFiClientSecure字段的类中添加构造函数并在构造函数中设置证书_client.setCACert(FUNCTIONS_CERTIFICATE);仓库中的 config.h 同时定义了三个函数 URL本地 HTTP 地址形式TEXT_TO_TIMER_FUNCTION_URL、GET_VOICES_FUNCTION_URL与TEXT_TO_SPEECH_FUNCTION_URL并预置了TOKEN_CERTIFICATE与SPEECH_CERTIFICATE两个用于语音服务 TLS 握手的证书常量——部署到云端后只需把 URL 改为 HTTPS 地址并启用对应证书即可。五、总结与关键技术要点回顾本课在 Wio Terminal 上实现 TTS 的完整方案有几个值得记住的设计决策技术难点解决方案关键证据位置语音列表 JSON 超 77KB设备无法解析无服务器函数get-voices按语言过滤只返回 ShortName美式英语仅 408 字节get-voices/init.py语音服务无 44.1kHzSeeed 音频库只支持 44.1kHz无服务器函数用 librosa 把 48kHz 重采样为 44.1kHztext-to-speech/init.pyTTS REST API 需要 Bearer 令牌用 API Key 调用sts/v1.0/issuetoken换取访问令牌同上get_access_token()大音频文件无法装入内存HTTP 响应直接writeToStream流式写入 SD 卡SPEECH.WAVtext_to_speech.h无服务器环境无法预装 libsndfile用 Docker 容器部署函数应用本课部署章节这套瘦客户端 无服务器函数的架构是物联网设备接入云端 AI 服务的典型模式凡是涉及大响应体、重计算或系统级依赖的功能都收敛到云端函数中完成设备端只保留最轻量的 HTTP 客户端与存储逻辑。掌握这种模式你就能在类似的内存受限嵌入式设备上复用 Azure 语音服务的全部能力。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考