拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Wio Terminal 文本转语音实战:Azure 语音服务与 Serverless 音频重采样

Wio Terminal 文本转语音实战Azure 语音服务与 Serverless 音频重采样【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本篇文章基于 IoT-For-Beginners 项目「消费类设备」模块第 23 课Spoken Feedback 语音反馈聚焦Wio Terminal 如何借助 Azure 语音服务与 Azure FunctionsServerless实现文本转语音Text to Speech。你将掌握为什么 77KB 的语音列表 JSON 必须由 Serverless 函数瘦身、为什么 48KHz 的 TTS 音频必须重采样到 44.1KHz、以及如何在 Wio Terminal 上把语音流式写入 SD 卡并通过 ReSpeaker 播放的完整链路。一、背景在 Wio Terminal 上实现语音反馈在上一课中你已经通过 Azure 语音服务实现了语音转文本Speech to Text按住 Wio Terminal 的 C 键录音将语音交给云端识别为文字再由 Language Understanding 服务解析出定时请求。本课要做的是反向链路——把文字转换回语音Text to Speech简称 TTS让设备用自然语音向你确认定时器已启动、并在倒计时结束时提醒你。TTS 系统的典型工作流程分为三个阶段6-consumer/lessons/3-spoken-feedback/README.md文本分析Text analysis把原始文本规范化为可发音的词语例如把数字1234按语境读成 One thousand, two hundred thirty four 或 One, two, three, four同时受语言区域影响美式英语 One hundred twenty vs 英式英语 One hundred and twenty。语言分析Linguistic analysis把词语拆解为音素phoneme并补充语调、音长、停顿等韵律信息例如句尾升调把陈述句变成疑问句。波形生成Wave-form generation早期方案拼接预录音素音色机械单调现代方案使用深度学习神经网络生成近乎真人、甚至难以与人类区分的自然语音。Azure 语音服务提供的 SDK 既能做语音转文本也能做文本转语音。但在 Wio Terminal 这类资源受限的微控制器上直接调用服务会遇到两个硬性瓶颈语音列表 JSON 过大、音频采样率不匹配。下文将逐一拆解并给出 Serverless 化的解决方案。二、瓶颈一77KB 的语音列表Wio Terminal 吃不消生成语音时必须指定使用哪个声音voice因为每种语言都支持多个声音。你可以通过语音服务 SDK 查询每种语言支持的语音列表但这里就是微控制器的第一个瓶颈查询文本转语音服务支持的语音列表返回的是一个超过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 }上面这段 JSON 描述的是Aria这个声音它带有多种说话风格chat、cheerful、empathetic 等。而真正做文本转语音时你只需要其中的短名ShortName即en-US-AriaNeural。所以正确策略是不要让微控制器下载并解析整个 77KB 列表而是写一个 Serverless 函数Azure Functions HTTP 触发器让它按语言过滤出短名列表再把瘦身后的结果返回给 Wio Terminal由设备端直接取第一个可用的语音。三、创建 Serverless 函数get-voices为 Wio Terminal 瘦身语音列表3.1 配置语音服务凭据在 VS Code 中打开你的smart-timer-trigger函数项目本项目参考实现位于 code-spoken-response/functions/smart-timer-trigger并确保终端已激活虚拟环境。然后打开local.settings.json加入语音服务的 API 密钥与区域设置完整参考见 local.settings.jsonSPEECH_KEY: key, SPEECH_LOCATION: location将key替换为你语音服务资源的 API 密钥将location替换为你创建语音服务资源时使用的区域例如eastus、westeurope该值会拼进 API 域名。3.2 新建 HTTP 触发器在函数项目根目录的 VS Code 终端中执行func new --name get-voices --template HTTP trigger这会创建一个名为get-voices的 HTTP 触发器。仓库中的 get-voices/function.json 显示该触发器同时接受get与post方法。3.3 编写过滤逻辑用以下代码替换get-voices文件夹中的__init__.py与仓库中的 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)这段代码的工作方式从环境变量读取区域与密钥拼接语音列表 REST 端点https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list携带Ocp-Apim-Subscription-Key请求头发起 GET 请求拿到包含所有语言的庞大 JSON用filter按请求体中的language不区分大小写筛选出该语言的全部语音用map只提取每个语音的ShortName——这是文本转语音唯一需要的值返回 JSON 数组。文档指出以美国英语为例过滤后的响应仅408 字节相比 77KB 缩减了约 190 倍。 如需只选特定语音可按需修改过滤条件。3.4 本地运行与 curl 测试启动函数应用后用 curl 测试与之前测试text-to-timer触发器的方式相同注意请求体必须携带语言{ language:language }将language替换为你的语言代码例如en-GB或zh-CN。四、Wio Terminal 侧TextToSpeech类与init()拉取语音在 Wio Terminal 的smart-timer工程PlatformIO 项目见 code-spoken-response/wio-terminal/smart-timer中你需要新增一个头文件来封装 TTS 逻辑。4.1 配置函数 URL在 config.h 中加入函数应用的 URLconst char *GET_VOICES_FUNCTION_URL URL;将URL替换为get-voicesHTTP 触发器的访问地址。它与你已有的TEXT_TO_TIMER_FUNCTION_URL结构相同只是函数名从text-to-timer换成get-voices。本地调试时参考 config.h 的写法形如http://IP_ADDRESS:7071/api/get-voices。4.2 新建text_to_speech.h在src文件夹下创建text_to_speech.h用于定义文本转语音类。首先加入头文件包含仓库版本见 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 客户端与选中的语音字段WiFiClient _client; String _voice;4.3 实现init()请求并选取第一个语音在公有区添加init函数。它的职责是向get-voices函数 POST 一个 JSON携带LANGUAGE宏定义的语言解析返回的短名数组并取第一个void init() { DynamicJsonDocument doc(1024); doc[language] LANGUAGE; String body; serializeJson(doc, body); HTTPClient httpClient; httpClient.begin(_client, GET_VOICES_FUNCTION_URL); int httpResponseCode httpClient.POST(body); if (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); } httpClient.end(); }要点说明使用ArduinoJson的DynamicJsonDocument容量 1024 字节足够构造请求体并序列化LANGUAGE是 config.h 中定义的宏如en-US响应码为 200 时把返回的 JSON 数组反序列化_voice obj[0]直接取第一个语音作为默认语音非 200 时打印错误码便于排查。4.4 接入main.cpp并烧录验证打开 main.cpp在文件顶部加入#include text_to_speech.h在setup()函数中、speechToText.init();调用之后初始化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: 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.可以看到函数应用返回了美式英语的全部语音短名列表设备选取了第一个en-US-JennyNeural作为默认语音。五、瓶颈二44.1KHz vs 48KHz采样率不匹配拿到语音之后就可以进行真正的文本转语音了。但这里又遇到两个限制限制一存储介质。与语音转文本时的内存限制相同转换出的音频无法常驻内存必须写入SD 卡再由 ReSpeaker 播放。文档特别说明前面课程用闪存flash保存麦克风录制的音频本课改用 SD 卡是因为用 Seeed 音频库从 SD 卡播放更简单。限制二采样率。微控制器的音频库支持的格式非常有限。例如通过 ReSpeaker 播放声音所用的Seeed Arduino Audio 库只支持 44.1KHz 采样率的音频而 Azure 语音服务提供的音频格式只有8KHz、16KHz、24KHz 和 48KHz恰好没有 44.1KHz。这意味着音频必须重采样到 44.1KHz——这项工作在 Wio Terminal 上做会消耗远超其可用资源尤其是内存的算力。文档给出的架构决策很明确凡是涉及这类数据操作且数据来源于 Web 请求的场景优先交给 Serverless 代码处理。Wio Terminal 只需把要转换的文本发给 Serverless 函数函数负责两件事——调用语音服务完成 TTS、把音频重采样为 44.1KHz然后返回设备可以直接存入 SD 卡并通过 ReSpeaker 播放的 WAV 音频。六、创建 Serverless 函数text-to-speechTTS 重采样6.1 新建触发器与依赖在函数项目根目录的 VS Code 终端执行func new --name text-to-speech --template HTTP trigger该触发器在仓库中的绑定配置同样支持 GET/POST见 text-to-speech/function.json。音频重采样使用 Python 库librosa把它加入requirements.txt仓库完整依赖见 requirements.txtlibrosa然后安装依赖pip install -r requirements.txt⚠️ 如果使用 Linux包括 Raspberry Pi OS可能需要先安装libsndfilesudo apt update sudo apt install libsndfile1-dev6.2 获取访问令牌文本转语音与语音转文本不同不能直接使用 API 密钥调用 TTS REST 接口而要先凭密钥换取访问令牌access token。用以下代码替换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函数向令牌端点https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken发起携带Ocp-Apim-Subscription-Key的 POST 请求返回令牌字符串。6.3 实现主函数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三个字段构造SSMLSpeech Synthesis Markup Language文档——一种基于 XML 的语音合成标记语言可同时指定语言、语音名称与要朗读的文本speak根元素 voice元素携带Authorization: Bearer token、Content-Type: application/ssmlxml与X-Microsoft-OutputFormat: riff-48khz-16bit-mono-pcm请求头发起 POST。playback_format指定了音频输出格式16 位、48KHz、单声道 WAVRIFF用librosa.load(io.BytesIO(...), sr48000)把返回的二进制音频载入内存用librosa.resample(raw_audio, sample_rate, 44100)把音频从 48KHz重采样到 44.1KHzReSpeaker/Seeed 音频库要求的采样率用soundfilesf.write把重采样结果以PCM_16编码写成 WAV写入内存缓冲BytesIO返回该二进制内容状态码 200。6.4 curl 端到端测试本地运行或部署到云端函数应用后用 curl 测试。请求体需携带语言、语音与文本{ language: language, voice: voice, text: text }language语言代码如en-GB或zh-CNvoice要使用的语音短名如en-US-JennyNeuraltext要转为语音的文本。例如把 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任何支持 WAV 的播放器都可以播放验证效果。七、Wio Terminal 侧convertTextToSpeech()写入 SD 卡7.1 配置函数 URL在 config.h 中加入const char *TEXT_TO_SPEECH_FUNCTION_URL URL;将URL替换为text-to-speech触发器的地址结构同TEXT_TO_TIMER_FUNCTION_URL只是函数名换成text-to-speech。7.2 实现转换方法在TextToSpeech类的公有区添加方法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);若请求成功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); }注意这里使用的是httpClient.writeToStream()直接把响应流写入文件避免把整段音频先缓存到 Wio Terminal 有限的内存中。方法末尾记得关闭 HTTP 连接httpClient.end();7.3 在say()中接入在 main.cpp 的say函数末尾把要朗读的文本转为音频void say(String text) { Serial.println(text); textToSpeech.convertTextToSpeech(text); }从 main.cpp 可以看到完整的闭环逻辑processAudio()把识别出的语音文本交给languageUnderstanding.GetTimerDuration()解析时长然后调用say(begin_message)播报定时器已启动再用timer.in(total_seconds * 1000, timerExpired, ...)注册倒计时回调超时后再次say(end_message)提醒时间到。7.4 播放音频文档中「从 Wio Terminal 播放音频」一节标注为Coming soon即将推出——即本课仓库中尚未包含通过 ReSpeaker 实际播放SPEECH.WAV的代码。目前链路做到语音已生成并落盘到 SD 卡为止后续播放逻辑可参考 Seeed 音频库的 API 自行扩展。八、部署到云端Docker 容器 证书配置前面一直让你本地运行函数应用是有原因的librosa这个 Pip 包在 Linux 上依赖一个默认未安装的系统库即前文提到的libsndfile。函数应用是 Serverless 的——没有你可以直接登录管理的服务器也就无法预先安装该库。解决办法是用 Docker 容器来部署函数应用云平台在需要扩容新实例例如请求量超过现有资源或应用闲置后被回收再拉起时会基于该容器镜像拉起实例。创建函数应用并通过自定义容器镜像部署的详细步骤见 Microsoft Docs 的「create a function on Linux using a custom container」文档。部署完成后Wio Terminal 访问云上 HTTPS 端点时需要把Azure Functions 的 CA 证书内置到设备代码中否则WiFiClientSecure的 TLS 握手会因不信任服务器证书而失败。修改步骤如下在config.h中加入证书字符串仓库 config.h 已包含TOKEN_CERTIFICATE、SPEECH_CERTIFICATE两个证书云部署时还需按文档加入FUNCTIONS_CERTIFICATEconst char *FUNCTIONS_CERTIFICATE -----BEGIN CERTIFICATE-----\r\n MIIFWjCCBEKgAwIBAgIQDxSWXyAgaZlP1ceseIlB4jANBgkqhkiG9w0BAQsFADBa\r\n ...完整 PEM 证书内容...\r\n -----END CERTIFICATE-----\r\n;把所有#include WiFiClient.h替换为#include WiFiClientSecure.h把所有WiFiClient字段类型改为WiFiClientSecure在每个包含WiFiClientSecure字段的类中添加构造函数并在其中设置 CA 证书_client.setCACert(FUNCTIONS_CERTIFICATE);这样设备就能以 TLS 方式安全访问部署在云端的get-voices与text-to-speech触发器。九、小结与可复用的工程模式本课 Wio Terminal 文本转语音的实现本质上是一条「瘦客户端 Serverless 重负载」的典型 IoT 架构链路值得复用环节瓶颈/约束Serverless 解决方案仓库参考实现语音列表获取完整列表 JSON 超过 77KB215 个语音get-voices函数按Locale过滤只返回ShortName美式英语仅 408 字节get-voices/init.py语音合成需要 SSML、令牌鉴权、48KHz 输出text-to-speech函数构造 SSML、换取 access token 并调用 REST APItext-to-speech/init.py采样率适配Seeed 音频库仅支持 44.1KHzlibrosa将 48KHz 重采样为 44.1KHzsoundfile编码为 16 位 WAVrequirements.txt设备端落盘内存有限无法缓存整段音频HTTPClient.writeToStream()直接流式写入 SD 卡SPEECH.WAVtext_to_speech.h设备端完整实现可对照 text_to_speech.hTextToSpeech类的init与convertTextToSpeech两个方法、config.hURL、密钥、语言与证书配置以及 main.cppsay()的调用时机。在微控制器上遇到大数据量 / 高算力操作时把处理逻辑搬到 Serverless 层、只让设备收发最小化数据是本课最重要的工程启示。若需继续扩展可以尝试用 SSML 的节奏、音量、语调标签如prosody、break让播报更自然这也是本课课后挑战的进阶方向。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门