IoT-For-Beginners 实战:在树莓派上使用 Azure 语音服务实现文本转语音(Text to Speech)
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本指南对应 IoT-For-Beginners 项目 "6-consumer" 单元第 3 课《设置定时器并提供语音反馈》中的树莓派(Raspberry Pi)硬件路径,讲解如何基于smart-timer定时器项目,通过 Azure 认知服务语音服务的 REST API 将文本转换为语音,并通过 PyAudio 在扬声器上播放。读完本指南后,你将掌握"获取语音列表并挑选合适嗓音"、"构造 SSML 请求文本转语音"、"在树莓派上播放返回的 WAV 音频"三条完整链路,让设备能用自然语音回答用户。
背景:为什么需要语音输出
在前面两课中,设备已经实现了"语音转文字"(Speech to Text)和"从文本提取定时请求"(通过 LUIS 理解语言)。但智能助手不是单向通信设备——用户说"设置一个 3 分钟定时器",设备应当回应"好的,您的定时器已设置为 3 分钟",并在定时结束时提醒用户。本课要做的,就是调用与语音识别相同的语音服务,把要回复的文本合成为音频并播放出来。
文本转语音的基本原理
文本转语音(Text to Speech)是把文本拆解为构成音素(phonemes)的过程,再用预录音频或 AI 模型生成的音频拼接成语音。典型系统包含三个阶段:
- 文本分析(Text analysis):把原始文本转换为可用于语音合成的词。例如
1234可能读作 "One thousand, two hundred thirty four",也可能读作 "One, two, three, four",取决于上下文;不同地区(locale)的读法也有差异,如美式英语 "One hundred twenty" 与英式英语 "One hundred and twenty"。 - 语言分析(Linguistic analysis):把词拆解为音素,并结合上下文补充语调信息,例如句尾升调可以把陈述句变成疑问句。
- 波形生成(Wave-form generation):早期系统用单个音素录音拼接,声音单调机械;现代系统使用基于深度学习的 ML 模型生成接近真人的语音。
发送合成请求时,使用语音合成标记语言(Speech Synthesis Markup Language,SSML),这是一种基于 XML 的标记语言,不仅定义要转换的文本,还定义文本的语言、使用的嗓音,甚至能控制部分或全部词语的语速、音量和音调。例如,以下 SSML 使用英式英语嗓音en-GB-MiaNeural合成一句提示语:
<speak version='1.0' xml:lang='en-GB'> <voice xml:lang='en-GB' name='en-GB-MiaNeural'> Your 3 minute 5 second time has been set </voice> </speak>说明:文本转语音系统的完整背景、三个阶段的详细讲解与挑战练习,参见本课总览文档 README.md。
整体思路:树莓派如何调用语音服务
树莓派属于全功能单板计算机,拥有完整的内存与音频栈,因此可以直接调用语音服务的 REST API 完成合成与播放,无需像 Wio Terminal 那样借助服务端函数代劳重采样(对比可参考 wio-terminal-text-to-speech.md)。核心调用链如下:
- 用 API Key 换取访问令牌(
get_access_token); - 查询指定语言的可用嗓音(
get_voice); - 构造 SSML,调用合成 REST 接口(
get_speech),拿到 WAV 二进制音频; - 用 PyAudio 输出流播放(
play_speech)。
本课涉及的完整可运行代码位于 code-spoken-response/pi/smart-timer/app.py。
前置条件与项目准备
在开始前,请确认:
- 已完成本课之前的步骤:语音识别(
speech_api_key、location、language三个变量已配置为你的语音服务资源值)、已通过无服务器代码(Azure Functions)实现text-to-timer意图解析(参考 single-board-computer-set-timer.md); - 树莓派上已安装
pyaudio、requests、wave等 Python 依赖; - 已接入 Grove 按钮(用于触发录音)和 USB 声卡/扬声器;
- 在 VS Code 中打开
smart-timer项目。
任务一:获取一个嗓音
不同语言支持不同的嗓音,语音服务提供了 REST 接口用于查询每个语言支持的嗓音列表。在say函数上方添加如下代码:
def get_voice(): url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list' headers = { 'Authorization': 'Bearer ' + get_access_token() } response = requests.get(url, headers=headers) voices_json = json.loads(response.text) first_voice = next(x for x in voices_json if x['Locale'].lower() == language.lower() and x['VoiceType'] == 'Neural') return first_voice['ShortName'] voice = get_voice() print(f'Using voice {voice}')这段代码的工作方式:
- 向
/cognitiveservices/voices/list端点发起 GET 请求,认证方式为Authorization: Bearer <token>,其中令牌由get_access_token()获取; - 用
json.loads解析返回的嗓音 JSON 列表,用next()找到第一个Locale与当前language匹配、且VoiceType为Neural(神经语音,音质更自然)的条目; - 返回该条目的
ShortName(如en-US-JennyNeural),这是合成请求中真正需要的标识; - 函数调用后把嗓音存入全局变量
voice并打印到控制台。该值只需请求一次,之后每次文本转语音调用都可复用。
提示:完整的支持嗓音列表可查阅语音服务的"语言和嗓音支持"官方文档。如果希望固定使用某个特定嗓音,可以删掉该函数,直接把
ShortName硬编码,例如voice = 'hi-IN-SwaraNeural'。
从源码结构看,仓库在无服务器端也提供了等价的嗓音筛选逻辑:get-voicesHTTP 触发器(get-voices/init.py)同样调用/voices/list接口,并按请求体中的language过滤、只返回ShortName列表——这正是树莓派版在设备端直接完成的事情。
任务二:将文本转换为语音
2.1 定义音频输出格式常量
向语音服务请求音频时,可以指定多种输出格式。在get_voice相关代码下方定义常量:
playback_format = 'riff-48khz-16bit-mono-pcm'选用哪种格式取决于你的硬件。如果播放音频时遇到Invalid sample rate错误,就换一个值。合成 REST API 支持多种输出格式,但树莓派场景必须使用riff格式的 WAV 音频,可依次尝试的值包括:
| 格式值 | 采样率 | 位深 | 声道 |
|---|---|---|---|
riff-16khz-16bit-mono-pcm | 16 kHz | 16 bit | 单声道 |
riff-24khz-16bit-mono-pcm | 24 kHz | 16 bit | 单声道 |
riff-48khz-16bit-mono-pcm | 48 kHz | 16 bit | 单声道 |
2.2 声明合成函数并配置请求头
声明get_speech函数,它负责通过 REST API 把文本转换为语音:
def get_speech(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 }三个请求头的含义:
Authorization:使用动态获取的访问令牌(而不是直接暴露 API Key);Content-Type:声明请求体是application/ssml+xml,即 SSML 内容;X-Microsoft-OutputFormat:指定期望返回的音频格式,即上面定义的playback_format。
2.3 构造 SSML 请求体
ssml = f'<speak version=\'1.0\' xml:lang=\'{language}\'>' ssml += f'<voice xml:lang=\'{language}\' name=\'{voice}\'>' ssml += text ssml += '</voice>' ssml += '</speak>'这段 SSML 指定了四要素:SSML 版本(1.0)、整个文档的语言(xml:lang)、要使用的嗓音(<voice name='...'>)以及真正要朗读的文本(text)。从仓库源码 app.py 可以看到,实际代码逐行拼接出完全一致的 SSML 结构。
2.4 发起请求并返回二进制音频
response = requests.post(url, headers=headers, data=ssml.encode('utf-8')) return io.BytesIO(response.content)ssml.encode('utf-8')把 SSML 字符串编码为 UTF-8 字节流后作为 POST 请求体发送;- 响应
response.content是合成的音频二进制数据(WAV 格式); - 用
io.BytesIO包装成内存中的类文件对象返回,方便后续用wave模块解析。
注意:本项目中的无服务器函数版本(text-to-speech/init.py)展示了同样的请求头、SSML 构造与 POST 调用方式,区别仅在于它额外用
librosa把 48 kHz 音频重采样到 44.1 kHz(供 Wio Terminal 的 ReSpeaker 播放)。树莓派因为音频栈完整,可以直接使用返回的原始 WAV,无需重采样。
任务三:播放音频
3.1 定义播放函数
在get_speech函数下方定义play_speech,播放 REST API 返回的音频:
def play_speech(speech): with wave.open(speech, 'rb') as wave_file: stream = audio.open(format=audio.get_format_from_width(wave_file.getsampwidth()), channels=wave_file.getnchannels(), rate=wave_file.getframerate(), output_device_index=speaker_card_number, output=True) data = wave_file.readframes(4096) while len(data) > 0: stream.write(data) data = wave_file.readframes(4096) stream.stop_stream() stream.close()这段代码的关键点:
wave.open(speech, 'rb')把get_speech返回的io.BytesIO对象当作 WAV 文件打开读取;- 与录音时创建 PyAudio 输入流类似,这里创建的是输出流(
output=True),并通过output_device_index=speaker_card_number指定输出声卡(该值在文件头部定义,与麦克风卡号microphone_card_number相对); - 流参数(采样宽度、声道数、采样率)不是硬编码的,而是从 WAV 文件头中读取(
getsampwidth()、getnchannels()、getframerate()),保证与合成音频完全匹配; - 每次读取 4096 帧数据写入流,循环直到读完整个文件,最后停止并关闭流。
3.2 更新say函数
用以下代码替换say函数的内容:
def say(text): speech = get_speech(text) play_speech(speech)say函数现在成为整个语音回复链路的总入口:把文本合成为二进制音频数据,然后播放。
3.3 完整的定时器语音闭环
在 app.py 中可以看到,say被两处调用:
create_timer(total_seconds):设置定时器时,拼出"X minute Y second timer started." 并立即语音播报;announce_timer(minutes, seconds):定时结束后由threading.Timer触发,播报"Times up on your X minute Y second timer."。
这与前面课程实现的语音识别、text-to-timer意图解析形成完整闭环:用户说话 → 识别成文本 → 服务端解析出秒数 → 树莓派设置threading.Timer→ 语音播报确认与到期提醒。
运行与验证
运行应用前,请确保:
- 函数应用(Azure Functions)仍在运行,
text-to-timer等 HTTP 触发器可访问; - 树莓派已连接按钮与扬声器;
- 在终端执行
python3 app.py。
随后按下按钮说出定时需求(例如"set a 2 minute timer"),你将先听到设备用选定嗓音播报"2 minute timer started.",等定时结束后,再听到"Times up on your 2 minute timer."。
故障排查:如果播放时出现Invalid sample rate错误,说明声卡不支持当前playback_format指定的采样率,请按上文表格依次改用riff-24khz-16bit-mono-pcm或riff-16khz-16bit-mono-pcm后重试。
与其他硬件路径的对照
为了帮助你理解树莓派方案的取舍,这里对照本课另外两条路径:
- 虚拟 IoT 设备(virtual-device-text-to-speech.md):使用语音服务 Python SDK 的
SpeechSynthesizer,调用get_voices_async()获取嗓音、speak_ssml()合成。一个关键细节是:播放语音前要recognizer.stop_continuous_recognition()暂停连续识别,播完再start_continuous_recognition(),否则播报内容可能被识别器捕获、被 LUIS 误判为新的定时请求,导致定时器无限嵌套。树莓派路径因为按键触发录音,天然规避了此问题。 - Wio Terminal(wio-terminal-text-to-speech.md):受单片机内存限制,必须把嗓音列表查询(77 KB 以上 JSON)和音频重采样(48 kHz → 44.1 kHz)都放到无服务器函数中完成,设备端只负责把返回的音频写入 SD 卡。
小结
通过本指南,你在树莓派上完成了文本转语音的完整实现:先通过 REST API 查询并选定与language匹配的 Neural 嗓音,再构造 SSML 请求合成 WAV 音频,最后用 PyAudio 输出流播放,让smart-timer项目能够用自然语音播报定时器状态。完整的可运行代码参见 code-spoken-response/pi。后续可以挑战进阶任务(assignment.md),例如通过 LUIS 识别"取消定时器"的意图,并用同样的语音链路给出回应。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考