无需云端:在高通 QCS8550 上实现 ASR、LLM、TTS 全离线语音交互

司红颜 2026-09-19 12:45:22

本文记录一个在 QCS8550 开发板上跑通的端侧语音助手:科大讯飞 M260C 环形麦克风负责采集语音,Sherpa-ONNX 在 CPU 上完成中文识别与语音合成,Qwen3-VL-8B 通过 QNN248 在 NPU 上常驻运行。整条交互链不调用云端 ASR、LLM 或 TTS 接口。诚然这个链路还有很多问题,比如说识别不够精准,会出现断尾,语音处理过慢等,希望能得到大家的指导。

1. 最终实现

一条可以连续使用的本地语音链路:

  1. 按住空格开始录音;
  2. 松开空格后完成整句识别;
  3. 可选择直接发送、跳过或手工修改识别结果;
  4. 将文本交给常驻的 Qwen3-VL-8B;
  5. 流式显示模型回答;
  6. 清理 Markdown 后由本地 TTS 播报;
  7. 回到待录音状态,继续下一轮对话。

实际终端交互类似这样:

=== M260C 本地语音助手已就绪 ===
音频输入:M260C / 16000 Hz
识别器:Sherpa-ONNX 本地模型
对话模型:Qwen3-VL-8B / QCS8550 NPU
语音播报:Sherpa-ONNX 本地中英双语
转发模式:人工确认(回车/y 发送,n 跳过,其他文字作为修正版)

录音中......
语音识别中......
识别结果:介绍一下端侧人工智能的优势
转发给模型?[Y/n/直接输入修改]

Qwen:……
语音播报中……
回答完毕

2. 项目分工

本项目做了简单的异构分工:

模块执行位置本项目实现
音频采集Linux 音频服务/CPUPulseAudio parec
ASRCPUSherpa-ONNX Online Transducer
LLMQCS8550 NPUQwen3-VL-8B、AidGen/QNN248
TTSCPUSherpa-ONNX VITS
流程协调CPUPython 子进程、线程、队列和事件

Qualcomm 官方资料说明,QAIRT/QNN 可以面向 CPU、GPU、NPU 等不同执行单元;在这个项目中,使用 QNN248/NPU 的是 Qwen,ASR 与 TTS 运行在 CPU 上。Qualcomm On-Device AI Development

3. 系统架构

完整数据流如下:

┌──────────────────────┐
│ 科大讯飞 M260C 六麦阵列 │
└──────────┬───────────┘
           │ USB Audio
           ▼
┌──────────────────────┐
│ PulseAudio / parec    │
│ 16 kHz / Mono / S16LE │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Sherpa-ONNX ASR      │  CPU
│ 最终整句文本           │
└──────────┬───────────┘
           │ 自动转发或人工修正
           ▼
┌──────────────────────┐
│ Qwen3-VL-8B          │  QCS8550 NPU
│ AidGen / QNN248      │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Markdown/代码清洗      │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Sherpa-ONNX VITS TTS │  CPU
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ PulseAudio / paplay  │ → USB 扬声器
└──────────────────────┘

Sherpa-ONNX 官方文档明确支持完全本地的语音识别,并提供流式 ASR、非流式 ASR 和离线 TTS 等接口,适合 ARM64 Linux 上的嵌入式组合。Sherpa-ONNX 文档

项目保持为两个主要入口,没有引入 Web 服务或额外框架:

m260c-asr/
├── m260c_asr.py          # 独立 ASR 与音频采集
├── m260c-chat            # ASR、Qwen、TTS 编排
├── test_m260c_chat.py    # 最小关键行为检查
├── model/                # ASR 模型
└── vits-melo-tts-zh_en/  # TTS 模型

4. 环境与准备工作

本次环境包括:

  • QCS8550 开发板;
  • Ubuntu/AidLux Linux 环境;
  • 科大讯飞 M260C USB 环形麦克风;
  • USB 扬声器;
  • Python 3.10;
  • Sherpa-ONNX 与 NumPy;
  • PulseAudio 工具;
  • 已独立验证可用的 Qwen3-VL-8B QNN248 本地部署。

Python 和音频工具的最小准备示例:

sudo apt install pulseaudio-utils

python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install numpy sherpa-onnx

模型文件需要分别按照其来源和许可证准备。ASR 目录包含:

model/
├── encoder.int8.onnx
├── decoder.int8.onnx
├── joiner.int8.onnx
└── tokens.txt

TTS 目录至少包含:

vits-melo-tts-zh_en/
├── model.onnx
├── lexicon.txt
├── tokens.txt
├── date.fst
└── number.fst

笔者在本文不展开 Qwen3-VL-8B 的模型部署过程。语音助手只要求有一个能够“加载一次、循环读取标准输入、连续输出回答”的本地聊天入口。当前项目通过 CHAT 常量调用之前部署模型时已经验证的 chat.sh,迁移到其他环境时应改成自己的实际路径。

5. 先确认 M260C 到底有没有被系统识别

出现录音问题时,不要一开始就安装或重装驱动。先分层检查:

cat /proc/asound/cards
arecord -l
pactl list short sources

如果 /proc/asound/cards 和 arecord -l 能看到 M260C,说明 USB Audio 和 ALSA 枚举已经工作;如果 pactl list short sources 也能看到对应 source,则 PulseAudio 已经接管它。

M260C 在不同资料包或固件版本中可能显示为不同名称。当前实现匹配这些已见过的标识:

M260C_NAMES = (
    "xfmdpv",
    "xfm-dp",
    "l6microphone",
    "listengo",
    "circular_6-microphone",
)

程序优先查找 PulseAudio source;系统没有 PulseAudio 或没有匹配 source 时,才回退到 ALSA:

PulseAudio source 找到 → 使用 parec
          │
          └─ 未找到 → 从 /proc/asound/cards 匹配 ALSA 设备
                               │
                               └─ 仍未找到 → 要求 --device 手动指定

这比写死 card 0 或 card 2 更可靠,因为 USB 声卡编号会在插拔和启动后发生变化。

6. 关键排障:设备已识别,为什么仍然提示 Device or resource busy

本项目容易被误判成“缺驱动”的错误是:

Device or resource busy

不是所有 Linux 音频问题都来自 PulseAudio,也不能看到 busy 就直接下结论。本项目有以下证据链:

  1. M260C 已同时出现在 ALSA 和 PulseAudio 设备列表中;
  2. 旧程序通过 plughw 直接打开 ALSA 设备时返回忙;
  3. 当时 PulseAudio 已经打开该麦克风;
  4. 改为从对应 PulseAudio source 采集后,能够稳定读取每块 40 ms、640 个采样点;
  5. 因此本次故障的根因是访问路径冲突,而不是驱动缺失。

原来的访问方式相当于:

桌面程序 ─┐
浏览器   ─┼→ PulseAudio → ALSA → M260C
ASR 程序 ─┘ 另一路直接打开 plughw,尝试独占同一设备

修复后,ASR 也通过 PulseAudio 读取:

parec \
  --device=<M260C_SOURCE> \
  --raw \
  --format=s16le \
  --rate=16000 \
  --channels=1

Python 端将 parec 作为子进程,从标准输出读取定长 PCM:

self.process = subprocess.Popen(
    [
        "parec",
        f"--device={source}",
        "--raw",
        "--format=s16le",
        "--rate=16000",
        "--channels=1",
    ],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)

PulseAudio 官方排障文档同样建议用 fuser /dev/snd/* 查找占用声卡的程序,并用 paplay 区分 PulseAudio 路径与更底层的硬件问题。PulseAudio Troubleshooting

建议的判断顺序是:

# 1. 驱动/枚举层
cat /proc/asound/cards

# 2. PulseAudio 是否已接管
pactl list short sources

# 3. 谁正在使用声音设备
fuser -v /dev/snd/*

# 4. 用 PulseAudio 路径直接验证
parec --device=<SOURCE> --file-format=wav /tmp/m260c.wav

只有在设备已经枚举、直接 ALSA 报忙、PulseAudio source 存在且 parec 能工作时,才能把问题归因到这类访问冲突。

7. 录音边界

最初版本虽然可以录音,但实际使用会遇到两个问题:

  • 按下按键后才开始保留音频,第一个字可能被截掉;
  • 松开按键立即停止,最后一个字或尾音可能不完整。

如果把音频块设得很大,按键边界的误差还会进一步增加。最终使用的参数是:

参数当前值作用
采样率16 kHz匹配 ASR 输入
音频块640 samples / 40 ms降低按键边界误差
预录缓冲1600 samples / 100 ms保留按键前的起始语音
松键尾录5 blocks / 200 ms保留松键后的尾音

时间关系如下:

              按下空格                         松开空格
                  │                               │
背景环形缓存 ──────┼────── 正式录音 ───────────────┼── 尾部保护 ─→ 整句识别
             100 ms                             200 ms

未录音时,程序持续把最近的音频块放入固定长度 deque。收到 start 后,先把最后 100 ms 加入本轮录音;收到 stop 后,再读取 5 个音频块,然后一次性解码完整语句。

CHUNK_SAMPLES = 640       # 40 ms @ 16 kHz
PRE_ROLL_SAMPLES = 1600   # 100 ms
POST_ROLL_BLOCKS = 5      # 200 ms

这里仍然复用 Sherpa-ONNX Online Transducer,但按键模式只发送最终整句,不把连续变化的中间识别结果交给大模型。

8. ASR 独立运行与参数

ASR 使用三个 INT8 ONNX 文件组成的 Transducer:encoder、decoder 和 joiner。核心配置如下:

recognizer = sherpa_onnx.OnlineRecognizer.from_transducer(
    tokens="model/tokens.txt",
    encoder="model/encoder.int8.onnx",
    decoder="model/decoder.int8.onnx",
    joiner="model/joiner.int8.onnx",
    num_threads=2,
    provider="cpu",
    sample_rate=16000,
    feature_dim=80,
    decoding_method="greedy_search",
)

先独立验证 ASR,避免把音频设备问题和 Qwen/NPU 问题混在一起:

# 识别已有 WAV
.venv/bin/python m260c_asr.py test-audio/funasr-zh.wav

# 使用自动发现的 M260C
.venv/bin/python m260c_asr.py

# 输入信号偏小时尝试增益 2
.venv/bin/python m260c_asr.py --gain 2

# 必要时手动指定 ALSA 设备
.venv/bin/python m260c_asr.py \
  --device plughw:CARD=L6Microphone,DEV=0

--gain 是数字增益

9. Qwen 常驻

模型不适合采用“每识别一句就启动一次”的方式:

一句语音 → 加载数 GB 模型 → 回答 → 退出 → 下一句重新加载

这会把模型加载时间和内存分配成本重复放大,也无法自然保留上下文。项目采用一个常驻聊天进程:

model = subprocess.Popen(
    [CHAT],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1,
)

启动阶段同时拉起 Qwen 和 ASR,但只有满足以下条件后才开放录音:

  • Qwen 输出就绪标志;
  • Qwen 已进入等待输入状态;
  • ASR 已完成模型和麦克风初始化;
  • TTS 已成功初始化,或者明确降级为只显示文本。

之后每轮只向模型标准输入写入一行文本,通过标准输出读取流式回答。/reset 清理上下文,退出时优先发送 /quit,超时才终止进程。

10. 用状态机阻止录音、推理和播报互相yi影响

STARTING
   │ ASR、Qwen、TTS 就绪
   ▼
READY
   │ 按下空格
   ▼
RECORDING
   │ 松开空格
   ▼
RECOGNIZING
   │ 得到最终文本
   ▼
CONFIRMING(可选)
   │ 发送
   ▼
QWEN_ANSWERING
   │ 回答结束
   ▼
TTS_PLAYING
   │ 播放结束
   └────────────→ READY

当前实现用几个 threading.Event 表达关键状态:

  • model_ready:模型完成加载;
  • model_idle:模型正在等待下一条输入;
  • can_record:允许开始新录音;
  • asr_ready:ASR 与输入设备已就绪;
  • model_failed:检测到 QNN 图执行失败。

基本规则很简单:

  1. 模型回答期间关闭 can_record;
  2. TTS 播放完成后才重新开放录音;
  3. 每轮只处理一条最终识别结果;
  4. ASR 或 Qwen 意外退出时,不继续运行残缺链路;
  5. /reset 必须等模型重新回到空闲状态。

这些规则也防止本机扬声器播出的 Qwen 回答马上被 M260C 再次识别,形成自问自答循环。

11. 人工确认

自动模式直接把识别结果发给 Qwen:

./m260c-chat

也可以开启确认模式:

./m260c-chat --confirm

确认规则如下:

输入行为
回车、y、yes发送原识别文本
n、no跳过本轮
其他文字用输入内容替换识别结果后发送
/reset清空 Qwen 上下文

例如 ASR 把“高通端侧人工智能”识别错时,无需重新说一遍,可以直接输入修正后的完整句子。

12. TTS:生成声音之前先清洗模型输出

Qwen 的回答可能包含 Markdown、代码、表格和链接。直接交给 TTS 会把符号、URL 甚至代码逐字朗读,因此先做最小清洗:

  • 删除 fenced code block;
  • Markdown 链接只保留显示文字;
  • 删除裸 URL;
  • 去掉标题、引用和列表标记;
  • 跳过表格分隔行。

TTS 使用 Sherpa-ONNX Offline VITS:

config = sherpa_onnx.OfflineTtsConfig(
    model=sherpa_onnx.OfflineTtsModelConfig(
        vits=sherpa_onnx.OfflineTtsVitsModelConfig(
            model="vits-melo-tts-zh_en/model.onnx",
            lexicon="vits-melo-tts-zh_en/lexicon.txt",
            tokens="vits-melo-tts-zh_en/tokens.txt",
        ),
        num_threads=4,
    ),
)

生成的浮点采样被裁剪并转换成 16 位 PCM,然后交给 paplay:

paplay \
  --raw \
  --device=@DEFAULT_SINK@ \
  --format=s16le \
  --rate=<TTS_SAMPLE_RATE> \
  --channels=1

这里也踩过与录音相似的问题:录音已经通过 PulseAudio 共享,播放却仍用 aplay/plughw 直接访问 ALSA,导致“设备或资源忙”。统一改成 paplay 后,TTS、桌面声音和系统音量控制由同一个声音服务协调。

Sherpa-ONNX 对词典外词汇会输出 OOV 提示。当前实现只在一次合成调用期间隐藏这类底层提示,真正的模型配置错误、空音频或播放失败仍然上报,不会全局吞掉标准错误。

13. 使用方法

先分别确认 ASR 和 Qwen 能独立运行,再启动完整链路。

cd m260c-asr

# 查看参数
./m260c-chat --help

# 自动发送识别结果并播报
./m260c-chat

# 识别后人工确认或修改
./m260c-chat --confirm

# 输入信号偏小时尝试数字增益
./m260c-chat --confirm --gain 2

# 只显示回答,不做 TTS
./m260c-chat --no-tts

交互命令:

/reset   清空 Qwen 上下文
Ctrl+C   停止 ASR、TTS 与模型进程

由于笔者是转发画面到笔记本的,当前空格键监听通过 X11 XGrabKey 实现,适用于本机 Ubuntu桌面环境。在纯终端、Wayland 或无图形会话中,应把这一层替换成终端按键、GPIO 按键或其他输入事件,ASR 和模型管线本身不需要重写。

14. 如何验证

建议按层验证:

14.1 音频输入

pactl list short sources
parec --device=<SOURCE> --file-format=wav /tmp/m260c.wav

确认文件有正常波形、采样没有持续为零,再测试 ASR。

14.2 ASR

.venv/bin/python m260c_asr.py test-audio/funasr-zh.wav
.venv/bin/python m260c_asr.py

分别验证文件输入和实时输入。

14.3 TTS 与输出设备

先用系统声音确认 PulseAudio sink:

pactl list short sinks
paplay /usr/share/sounds/freedesktop/stereo/complete.oga

再运行完整助手,避免把扬声器或 sink 问题误判成 TTS 模型问题。

14.4 关键行为检查

项目保留了一个不依赖完整推理的最小检查:

.venv/bin/python test_m260c_chat.py

覆盖:

  • QNN 错误标志检测;
  • Markdown/代码清洗;
  • 确认、跳过和手工修正;
  • TTS PCM 转换;
  • paplay 参数;
  • M260C PulseAudio source 匹配。

15. 下一步优化

  1. 接入 M260C 波束与声源方向
  2. 增加录音时长保护
  3. 失败样本保留
  4. 加入领域热词
  5. 考虑唤醒词与打断

参考资料

...全文
149 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文系统研究了基于雅可比伪逆迭代的双臂协同轨迹规划与跟踪误差收敛性问题,结合机械臂正向与逆向运动学建模、蒙特卡洛法分析工作空间、笛卡尔空间内的圆弧轨迹生成等关键技术,利用Matlab实现了双臂系统的协同控制与轨迹跟踪仿真。重点探讨了雅可比矩阵伪逆法在解决冗余自由度机械臂逆运动学问题中的应用,深入分析了轨迹跟踪过程中误差的收敛特性,并通过仿真实验验证了该方法在复杂任务场景下的有效性、稳定性与鲁棒性,为多臂机器人在工业自动化、精密操作等领域的协同作业提供了理论依据与技术支持。; 适合人群:具备机器人学、自动控制理论基础及Matlab编程能力的科研人员与工程技术人员,特别适合从事机械臂控制、轨迹规划、多机器人协同等方向的硕士、博士研究生及相关领域研究人员。; 使用场景及目标:①掌握基于雅可比伪逆法的机械臂逆运动学求解原理与实现方法;②理解双臂系统在笛卡尔空间中协同轨迹规划的机制;③分析并优化轨迹跟踪过程中的误差收敛性能;④为多机器人系统在复杂动态环境下的高精度协同控制提供算法设计与仿真验证支持。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,按照文档目录循序渐进地学习,重点关注算法实现细节、仿真参数设置与结果分析部分,同时可进一步探索将智能优化算法或深度学习方法引入轨迹规划与误差补偿的研究方向。

7,832

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、IoT、PC 、XR、Auto等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
物联网人工智能开源 企业社区 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧