ssurui 417fbbd9f5 fix: 修正音频通路卡死导致语音输入输出长时间运行后失效的问题
用户反馈 pad 长时间运行后语音输入输出都不再工作,拿到的现场诊断显示:
pulseaudio 占 49% CPU,系统里积压了 70 个从 7月5日就存在的
`aplay -t raw` 残留进程。

根因:systemTts.ts 每次播报一个分句都会 spawn 一个 aplay 子进程,
全部数据写完后调用 stdin.end(),然后完全依赖 aplay 自己触发 'exit'
事件才清理状态(isSpeaking = false)。这台设备的 PulseAudio 大约空闲
5 秒就会把音频设备挂起(代码里已有相关注释),长时间运行后一旦某次
"唤醒音频通路"没成功,aplay 就会卡在往挂死的通路写数据,stdin.end()
之后永远等不到 'exit'——播放侧完全没有超时兜底(合成侧 vitsWorker 倒是
已经有 60 秒超时)。isSpeaking 永久卡 true,playNext() 再不会处理
队列里的后续句子,整个 TTS 直接哑掉;ASR 走的是渲染进程里
getUserMedia/AudioContext,用的是同一个被拖垮的 PulseAudio 实例,
所以语音输入也跟着失效。更麻烦的是 systemTtsStop() 只发 SIGTERM,
卡在系统调用里的 aplay 收不到效果杀不掉——每次打断播报(每次提问前
都会调用一次)都可能在后台攒下一个杀不死的僵尸进程,日积月累到 70 个,
把 PulseAudio 彻底拖垮。

修复(src/main/tts/systemTts.ts):
1. speakWithVits / speakWithPiperStreaming 的播放阶段新增
   PLAYER_EXIT_TIMEOUT_MS(8 秒)看门狗:stdin.end() 后超时未触发
   'exit' 就判定音频通路卡死,SIGKILL 强杀并以 done(error) 收尾,
   让队列能继续推进而不是永久卡死。
2. systemTtsStop() 新增 killWithEscalation():先 SIGTERM,1.5 秒
   宽限期后进程还没退出再 SIGKILL,避免每次打断播报都在后台攒一个
   杀不掉的僵尸进程。

验证:真机上先用 pkill 清掉 70 个残留 aplay、重启 pulseaudio
(CPU 49% 降到 1.6%),部署修复后:
- 正常播报全程无异常,结束后无残留进程
- 用 kill -STOP 冻结 pulseaudio 制造"音频通路卡死"的真实场景,
  看门狗在 7.8 秒(吻合 8 秒超时设定)后正确强杀卡住的 aplay,
  队列自动继续播放下一句,全程结束后无残留进程

过程中还排查了一次 pad 网络连不上的问题:本机 Clash for Windows
开着 TUN 模式且运行在全局模式(mode: global),局域网流量也会被
接管,与此次 TTS/ASR bug 无关,已切回 rule 模式;pad 本身 IP 没变,
是 ARP 缓存过期,pad 主动 ping 一次后双方缓存刷新即恢复,供以后
排查类似"局域网设备连不上"问题参考。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeoAy5KmkHuKWHAktpEsaN
2026-10-06 10:34:21 +08:00

elec_rag_ui — 智能电子手册前端

基于 Electron + Vue3 + TypeScript + LanceDB 构建的本地化 RAG(检索增强生成)桌面应用。

仅支持 Linux(银河麒麟 Kylin V10 / Ubuntu),适配 ER160(揽月 M50 NPU)和 ER25(芯动力 RPP)计算卡。服务进程管理委托给 er_backend(HTTP 17700),前端专注 RAG 逻辑与用户交互。

功能

  • 文档问答:导入 Markdown/文本手册,提问后检索相关片段并生成带引用标号的回答,点引用可查看原文片段
  • 语音输入:Paraformer 流式识别,说话即转文字填入输入框
  • 语音输出:边生成边播报,不用等整段答案写完
  • 拍照提问:用摄像头拍下实物,先由多模态模型转成一句话描述,再接入同一套知识库检索——所以答案同样基于手册内容、带引用,而不是模型凭空描述图片。需要 er_backend 当前加载的是多模态模型,否则按钮会自动置灰(换回多模态模型后自动恢复)
  • 批量测试:按预设问题列表循环提问,用于压测和效果回归

技术栈

  • 框架:Electron + electron-vite + Vue3 + TypeScript
  • UI:Naive UI(弹窗/通知)+ 自定义工业风 CSS
  • 状态管理:Pinia
  • 向量数据库:LanceDB(嵌入式,无需额外服务)
  • 本地向量化:@huggingface/transformers + onnxruntime-node
  • 生成引擎:llama.cpp server(通过 er_backend 管理)
  • Markdown 渲染:marked

支持硬件

计算卡 嵌入方式 生成方式
ER160(揽月 M50 NPU) llamacpp NPU(默认)或本地 CPU llamacpp
ER25(芯动力 RPP) 本地 CPU(固定) llamacpp

推理请求(生成 + 嵌入)统一经 er_backend 代理转发(http://127.0.0.1:17700),实际 llama-server 监听哪个端口由 er_backend 后端配置决定,前端不感知、也不需要按计算卡型号推导端口。

目录结构

elec_rag_ui/
├── src/
│   ├── main/               # Electron 主进程
│   │   ├── index.ts        # 入口:全屏启动、菜单禁用、信号处理
│   │   ├── config.ts       # 配置读写(userData/config.json)
│   │   ├── ipc/            # IPC 处理器
│   │   └── rag/            # RAG 核心逻辑
│   ├── preload/index.ts    # contextBridge
│   ├── renderer/           # Vue3 前端
│   │   └── src/
│   │       ├── App.vue     # 根布局(顶栏 + 主区)
│   │       ├── components/ # ChatPanel / IndexPanel / CameraCapture / SettingsModal 等
│   │       ├── stores/     # Pinia store
│   │       ├── utils/      # TTS 播放器、音频采集、Markdown 处理
│   │       └── types/      # TypeScript 类型定义
│   └── batch_questions.txt # 批量测试问题示例
├── models/                 # 本地模型(onnx 由 Git LFS 管理)
│   ├── bge-small-zh-v1.5/  # 嵌入模型(512 维,~24MB),本地 CPU 向量化回退用
│   └── asr/                # Paraformer 流式语音识别模型(~376MB)
├── docs/                   # 示例文档(用于测试索引与问答)
├── resources/              # 应用图标、TTS/ASR worker
├── stubs/sharp/            # sharp 原生模块 stub
├── package.json
├── electron-builder.config.ts
└── electron.vite.config.ts

依赖:er_backend

本应用依赖 er_backend(端口 17700)管理 llama-server 推理服务进程。启动前请确保 er_backend 已运行:

# PAD 上
cd /opt/er-backend
nohup ./node/node manager/index.js &

# 或通过 systemd(deb 安装后自动配置)
systemctl status er-backend

er_backend 管理界面:http://<PAD-IP>:17700(配置模型路径、启动/停止服务)

启用拍照提问

需要两个条件同时满足:

  1. er_backend 加载的是多模态模型——在管理界面把生成模型配成带 mmproj 的视觉模型(启动命令里应带 --mmproj)。应用会自动探测:不是多模态模型时拍照按钮置灰,换回多模态模型后 5 秒内自动恢复可用,不用重启应用。
  2. 在「设置 → 基本设置 → 拍照提问」里打开开关(默认关闭,因为不是所有部署都会配视觉模型)。

摄像头用标准 USB UVC 设备即可,无需额外驱动。有多个摄像头时,拍照弹层右上角可以切换。

模型文件

models/ 目录中的 .onnx 文件通过 Git LFS 管理。克隆后拉取实体文件:

git lfs install
GIT_SSL_NO_VERIFY=true git lfs pull

语音识别默认不带自动加标点(标点恢复模型未随包分发);如需要可自行准备 sherpa-onnx punct 模型, 在设置里把"标点恢复模型目录"指过去即可,代码本身已支持该功能。

开发模式

npm install
npm run dev:linux    # Linux(--no-sandbox,开发专用)

开发模式下需要设置 DISPLAY 环境变量(有图形界面时自动可用)。

打包

npm run package:kylin            # Linux x64,产出 AppImage + .deb
npm run package:kylin:arm64:deb  # Linux ARM64 deb(推荐,须在 ARM64 机器/真机上执行,已验证可行)
npm run package:kylin:arm64      # Linux ARM64(AppImage + deb),真机 Kylin 上 deb 目标
                                  # 走 electron-builder 自带 fpm 必现 tar failed,
                                  # 用 package:kylin:arm64:deb 代替

数据存储

内容 路径
配置文件 ~/.config/elec_rag_ui/config.json
向量数据库 ~/.config/elec_rag_ui/lancedb/

许可证

MIT License

Description
No description provided
Readme 646 MiB
Languages
F* 42.2%
TypeScript 33.8%
Vue 18.5%
JavaScript 2.4%
CSS 2%
Other 1.1%