.> 硬件与系统:Quectel Pi M2(RK3576)/ Debian 13(trixie)aarch64 / Windows 10/11
项目目标:在 M2 上搭建语音采集、唤醒、识别、问答和播报链路,并通过独立的 Windows 控制台统一查看状态和调试服务。
当前进度:Windows 控制台及 Agent 软件链路已完成验证;M2 已完成页面显示和历史音频采集验证,完整语音闭环仍待现场验收。
这篇文章记录的不是一个已经完成验收的语音产品,而是一次端云协同语音系统的阶段性工程实践。重点包括:如何拆分语音链路、如何区分演示数据和真机数据、如何解决 Windows 打包问题,以及如何为后续真机验收建立明确标准。
一、项目背景
端侧语音助手表面上只是“说一句话,设备回答一句话”,实际需要多条链路共同工作:
麦克风采集 → 唤醒词检测 → ASR 语音识别 → Agent 问答 → TTS 合成 → 扬声器播放
↓
页面状态 / HTTP API / Windows 控制台
调试过程中,页面、SSH、日志、音频命令和模型请求分散在不同入口。页面能显示,不等于语音服务在线;接口返回成功,不等于扬声器确实出声;Windows 界面出现传感器数值,也不一定代表数据来自 M2。
因此,本阶段完成了两项工作:
- 将采集、唤醒、识别、问答、合成和播放拆成可独立观察的环节;
- 开发 Windows 原生控制台,统一处理连接测试、状态查看、文字问答和调试反馈。
二、系统组成与 M2 的角色
| 模块 | 当前方案 | 运行位置 |
|---|---|---|
| 语音采集 | 板载麦克风,PipeWire/arecord | M2 |
| 唤醒与 ASR | Vosk 本地模型 | M2 |
| 开放域问答 | DeepSeek API | 云端 |
| 本地状态问答 | 规则与本地降级逻辑 | M2/客户端 |
| 语音合成 | Azure Speech,缺少配置时回退本地 TTS | 云端/M2 |
| 状态与问答接口 | HTTP 服务 | M2 |
| 调试控制台 | PySide6 桌面程序 | Windows |
| 联网搜索 | DuckDuckGo HTML 搜索,可选启用 | Windows |
M2 在当前版本中主要承担音频输入、本地 Vosk 推理、页面与接口服务以及音频输出。RK3576 的 NPU 尚未接入当前推理链路,因此本文不把“6 TOPS NPU”作为已经使用或验证的成果。后续若迁移到支持 RKNN 的模型,还需要单独补充模型转换、精度和性能测试。
板端服务默认只监听 127.0.0.1:8787。当前路由分为两类:
- 页面路由:
/screen、/window; - API:
/health、/window.json、/chat。
Windows 访问开发板时使用 SSH 本地端口转发,避免将调试接口直接暴露到公网或不可信局域网。
三、先定义什么才算“通过”
为了避免把单点成功误认为整条链路完成,我为每项能力设置了独立的验收条件。
| 能力 | 当前状态 | 通过标准 |
|---|---|---|
| Windows 控制台启动 | 已通过 | EXE 可启动并正常显示中文界面 |
| Agent 软件装配 | 已通过 | 动态模块加载完成,engine errors=0 |
| 演示问答 | 已通过 | 使用合成数据返回正确的本地状态回答 |
| M2 页面显示 | 已通过 | HDMI 页面可打开并提交文字问题 |
| M2 麦克风采集 | 历史单次通过 | 录音存在有效电平且无明显削波 |
| 唤醒词 | 待真机验收 | 真人说出唤醒词后 wake_count 增长 |
| ASR | 待真机验收 | asr_count 增长且文本与语音一致 |
| TTS 与扬声器 | 待真机验收 | tts_count 增长且现场听到播报 |
| 完整语音闭环 | 未完成 | 唤醒、识别、回答、播报连续完成 |
这个状态表也是本文的结论边界:软件模块通过测试,不等同于硬件现场闭环已经完成。
四、Windows 控制台
4.1 区分演示模式和开发板模式
控制台使用 PySide6 原生控件实现,没有嵌入 WebView。界面包含三个入口:
- 对话:发送文字问题,显示回答、搜索状态和播报状态;
- 设备概览:查看连接、模型、音频链路和运行组件;
- 连接设置:在“本机演示”和“M2 开发板”之间切换。
默认进入本机演示模式,并注入合成传感器数据。只有切换到“M2 开发板”并通过连接测试后,界面数据才可能来自板端。
!
图 1:控制台连接设置。运行目标和实际服务地址同时显示,避免把本机演示误认为真机连接。
4.2 请求过程可观察
模型和搜索请求运行在独立 worker 中,界面会按阶段显示:
正在联网搜索...
正在思考...
正在通过开发板播报...
板端 /window.json 同时提供 thinking、speaking 等状态,便于页面轮询。这样可以区分“程序卡死”“模型仍在请求”和“正在等待板端播放”。
4.3 快速问答策略
为了缩短交互等待时间,快速问答场景采用以下约束:
- 关闭 DeepSeek Thinking Mode;
- 回答最多 128 tokens;
temperature=0.35;- 模型调用失败时不自动重复请求;
- 提示词要求直接回答,通常不超过三句话。
这些设置减少的是生成量和重试次数,不能消除网络抖动,也不属于模型训练中的 L1/L2 正则化。
4.4 联网搜索与语音配置
启用搜索后,Windows 客户端会先查询 DuckDuckGo HTML 页面,将限长后的摘要和来源交给 Agent。搜索超时为 5 秒;超时或无结果时继续使用模型知识回答,并明确标记降级状态。该能力运行在 Windows 客户端,不代表 M2 本身实现了联网搜索。
目标云端语音配置为:
voice: zh-CN-XiaoxiaoNeural
style: affectionate
Azure Key 和区域只从环境变量读取,缺少配置时回退本地 TTS。配置读取和 SSML 构造已有单元测试,但 Azure 请求成功、M2 播放和现场听音仍需要一次完整验收。
五、两个值得记录的 Windows 打包问题
5.1 Qt DLL 加载失败
初版单文件 EXE 启动时报 PySide6.QtGui DLL load failure。排查后发现,单文件解包产生的 Qt/Shiboken DLL 目录没有稳定加入 Windows DLL 搜索路径,同时机器中其他 Qt/ICU DLL 可能发生冲突。
最终处理方式包括:
- 在导入 PySide6 前通过 runtime hook 注册 bundled DLL 目录;
- 过滤 PATH 中可能冲突的外部 Qt 目录;
- 预加载所需 ICU;
- 重新构建单文件产物。
修复后的 EXE 已在当前 Windows 机器上完成启动验证。其他 Windows 版本和全新机器的兼容性仍需要单独测试。
5.2 窗口正常,但 Agent 能力缺失
PyInstaller 可以发现静态 import,但项目中的 Skills、drivers 和 adapters 由运行时动态装配。初版产物因此出现“窗口能打开、Agent 回答为空”的情况。
在构建脚本中显式收集动态模块后,自检结果为:
Skills: 43
Agents: 7
engine errors: 0
本机演示模式下询问“当前骑行速度是多少?”,可以返回合成状态数据:
当前配速 18.5 公里每小时。
这项测试证明桌面端问答和动态模块装配工作正常,但不用于证明 M2 传感器已经接入。
六、M2 音频排查
6.1 一次有效的麦克风采集
历史诊断中记录过一次 5 秒、48 kHz、单声道采集:
samples: 240000
RMS: 3300.3
peak: 16392
non-zero samples: 99.88%
这次数据说明当时录到了有效声音,峰值也未达到 16-bit PCM 的削波上限。但单次录音不能替代持续服务测试,更不能证明唤醒识别已经成功。
遇到录音失败时,可以按以下顺序确认音频服务和设备:
systemctl --user status pipewire pipewire-pulse wireplumber
pactl list short sources
pactl get-default-source
arecord -l
调试中 hw:0,0 曾因设备被占用而无法打开,后续改为通过 PipeWire 共享输入。如果 PyAudio 仍选择错误设备,可固定 capture_backend=arecord,并核对实际 card/device 编号。
6.2 为什么唤醒仍不能标记为通过
默认唤醒词配置包含“小爱小爱”的常见识别变体,识别使用本地 Vosk,不依赖云端 Key。但最近留存的 health 数据为:
wake_count = 0
asr_count = 0
tts_count = 0
因此目前只能确认模型、配置和模拟测试已经准备好,不能声称真人唤醒已稳定工作。真正的通过标准是:现场说出唤醒词后,计数增长、ASR 文本正确,并最终从扬声器听到回答。
6.3 本地 ASR 不等于全链路离线
Vosk 可以在 M2 本地完成唤醒和语音转文字,但开放域问答仍可能调用 DeepSeek,Azure TTS 也依赖网络。网络断开后,系统只保留本地状态问答和基础降级回答,因此当前方案应描述为“端云协同”,而不是“端到端离线大模型”。
七、板端连接与复现步骤
开发板 HDMI 页面已能够显示问答界面。下面这张照片记录的是早期联调状态,证明页面已在板端显示,但不作为完整语音链路的验收证据。
图 2:M2 板端 HDMI 页面早期联调画面。
首先在 M2 上确认服务和端口:
sudo systemctl status voice-agent.service
ss -lntp | grep 8787
curl http://127.0.0.1:8787/health
curl http://127.0.0.1:8787/window.json
然后在 Windows 建立本地端口转发:
ssh -L 8787:127.0.0.1:8787 <user>@<M2_IP>
最后在控制台中选择“M2 开发板”,将地址设置为 http://127.0.0.1:8787 并执行连接测试。只有完成这些步骤后,控制台中的板端状态才具备真机含义。
八、截至 2026-10-20 的验证结果
| 项目 | 结果 | 说明 |
|---|---|---|
| PySide6 窗口 | 通过 | 当前 Windows 机器可正常显示 |
| 单文件 EXE | 通过 | 尚未完成多机器兼容性矩阵 |
| 动态模块装配 | 通过 | 43 Skills、7 Agents、engine errors=0 |
| 演示问答 | 通过 | 使用合成数据 |
Python compileall |
通过 | 仅代表语法编译成功 |
| 单元/集成测试 | 199 项通过、5 项跳过 | 属于留存的软件测试记录,不等同于硬件验收 |
| M2 页面显示 | 通过 | 已有 HDMI 联调画面 |
| M2 麦克风 | 历史单次通过 | 需要在当前服务状态下复验 |
| M2 语音闭环 | 未完成 | 缺少现场唤醒、识别和播报证据 |
| M2 HTTP 服务 | 最近一次检查不可达 | 需要恢复连接后重新读取 health |
九、总结
本阶段真正完成的成果,是建立了一套可观察、可拆分的端云语音调试链路:Windows 控制台可以统一查看状态和发起问答,Qt DLL 与动态模块打包问题已经解决,本地演示数据和 M2 真机数据也有了明确边界。

