本文记录 EdgeSight Assistant 截至 2026-08-14 的架构、目标板测试和近期修改。文章中的地址、密钥、模型路径和日志文件均已省略。
一、项目背景
这个项目的目标是在一块 8GB 内存的 Firefly ROC-RK3588S-PC 上,搭建一个可以持续推送视频、通过语音唤醒、理解当前画面并播报答案的离线多模态助手。
当前硬件组成如下:
- Firefly ROC-RK3588S-PC;
- Logitech C310 USB 摄像头,提供视频和麦克风;
- 板载 ES8323 音频输出,连接 3.5mm 设备;
- 外部 SRS 服务器;
- Win11 主机上的 RambosPlayer。
Win11 主机只负责从 SRS 拉流播放视频。目标板负责摄像头采集、视频编码推流、语音唤醒、语音识别、视觉推理和语音播放。这样划分以后,视频播放端和边缘计算端相互独立,目标板无需连接显示屏,也无需在板端重复实现播放器。
github:https://github.com/johnjiamzhong-project/EdgeSightAssistant
二、总体架构
系统由四条主要链路组成:视频推流链路、音频激活链路、视觉问答链路和 TTS 播放链路。
┌──────────────────────────────┐│ Win11 RambosPlayer ││ 从 SRS 拉流播放 │└──────────────▲───────────────┘│
C310 摄像头 ─> V4L2 MJPEG ─> FFmpeg 解码 ─> NV12│┌────────────────┴───────────────┐│ │h264_rkmpp 硬编 latest-frame 缓冲│ │FLV / RTMP 视觉 worker│ │▼ ▼SRS RKNN 图像编码器│▼RKLLM Qwen3-VL-2B│▼
C310 麦克风 ─> ALSA ─> KWS ─> 状态机 ─> VAD ─> SenseVoice ─> asr_final│▼Unix datagram│▼edgesight_tts│VITS + ALSA 播放
1. 视频推流模块
视频模块的核心数据流是:
C310 MJPEG 1280x720@30-> V4L2-> FFmpeg MJPEG decoder-> reusable NV12 frame-> h264_rkmpp-> FLV muxer-> RTMP/SRS
项目强制使用 h264_rkmpp,硬编码器不可用时直接报告错误,不回退到 CPU 软件编码。C310 启动时会关闭动态曝光降帧控制,避免摄像头在暗光场景下从 30 FPS 降到约 15 FPS。
推流器同时按固定间隔生成一张较小的 RGB 最新帧,放入容量为 1 的 latest-frame 缓冲。视觉请求到来时只取最近的一帧,不建立历史图片队列,也不让视觉推理阻塞视频发布循环。
2. 音频激活模块
音频模块使用 C310 的 USB 麦克风,默认参数为 16 kHz、单声道、S16_LE。流程为:
ALSA PCM-> sherpa-onnx Zipformer KWS-> ActivationController-> 能量 VAD-> SenseVoiceSmall int8-> 结构化 asr_final 事件
固定唤醒词为“你好小视”。命中后,程序通过 TTS 播放“我在”,并等待用户开始说命令:
- 5 秒只限制“开始说话”的时间;
- 一旦检测到语音,命令可以继续超过 5 秒;
- 尾部静音达到 1 秒后结束命令;
- 5 秒内没有开始说话时产生
activation_timeout; - 唤醒后会先等待一个安静音频块,降低唤醒词和“我在”的回声误触发。
所有关键状态都写成结构化事件,例如:
wake_detected
asr_started
command_ended
asr_final
activation_timeout
wake_rejected
3. 视觉问答模块
在 8GB 目标板上,首轮固定使用 Qwen3-VL-2B。视觉模型由两个部分组成:
- RKNN 图像编码器,把 RGB 图片转换成视觉 embedding;
- RKLLM 语言模型,把 embedding 和用户问题组合成回答。
视觉 worker 在后台加载一次模型。收到 asr_final 后,从 latest-frame 缓冲取一张当前画面,再执行图像编码和语言推理。视觉模块只在用户提出问题时运行,当前没有让 VLM 按 30 FPS 持续分析视频。
模型文件、RKNN/RKLLM 运行库和转换工具均放在目标板外部目录,仓库只保存构建代码和配置模板。当前使用的 RKLLM runtime 与 Qwen3-VL-2B 模型保持匹配,4B 模型暂不进入这块 8GB 板的主路径。
4. TTS 模块
TTS 运行在独立的 edgesight_tts 进程中:
VisionWorker-> Unix datagram-> edgesight_tts-> sherpa-onnx Offline VITS-> 持久 ALSA 会话-> 3.5mm 输出
当前使用 VITS Melo 中英文模型。一个回答会按中文或英文句末标点拆分,最多缓存两个待合成片段。第一段合成完成后即可开始播放,后续片段继续合成和播放。
TTS 对外保留两种协议:普通完整文本请求,以及 stream_begin、stream_segment、stream_end 组成的句子流。请求级仍然采用单请求 busy 拒绝策略,避免无界音频队列把延迟越积越长。
三、启动和进程边界
完整链路通过脚本启动:
scripts/start_all.sh config/config.yaml
启动顺序为:
- 初始化板载音频输出;
- 启动
edgesight_tts; - 启动启用视觉旁路的
edgesight_streamer; - 启动
edgesight_audio。
停止时按相反顺序清理:
scripts/stop_all.sh
当前项目采用前台 CLI 和脚本控制,暂不配置 systemd、桌面启动项、常驻 WebSocket/HTTP 控制服务或 GUI。错误主要通过 logs/ 中的结构化事件和统计信息定位。
四、已经完成的目标板验证
视频链路
- C310 实际枚举为 MJPEG 1280×720、30 FPS;
h264_rkmpp硬编码路径可用;- SRS RTMP 推流完成 600 秒连续运行;
- VLC/RambosPlayer 人工观看画面正常;
- 最终验证约 29.9 FPS,发布器错误数为 0。
音频和唤醒链路
- C310 实时麦克风可以命中“你好小视”;
- 唤醒后“我在”可以通过 3.5mm 输出播放;
wake_detected → asr_started → command_ended → asr_final已通过真人测试;activation_timeout、活动中重复唤醒拒绝和冷却窗口已完成回归;- 真人唤醒正例使用实时 C310 麦克风,保存 WAV 只用于确定性回归。
视觉和 TTS 并发链路
- Qwen3-VL-2B 单图和 latest-frame 旁路已在目标板运行;
- 视觉回答可以经 Unix datagram 交给 TTS;
- TTS 真实 3.5mm 播放成功;
- 视频、音频、视觉和 TTS 同时运行时,视频仍保持约 30 FPS;
start_all.sh和stop_all.sh烟测通过,结束后没有残留 EdgeSight 进程和 Unix socket。
开发机无硬件回归也已固定下来:
CTest: 5/5 passed
bash -n scripts/*.sh: passed
五、一次 TTS 竞态故障的定位和修复
多轮真人测试时,日志出现过以下异常顺序:
tts_request_accepted
没有 tts_started
后续请求全部 tts_rejected reason=busy
原因在于普通文本 worker 和流式 worker 共用了一个条件变量,但使用了不同的等待谓词。普通请求路径调用 notify_one() 时,可能唤醒了流式 worker。流式 worker 发现条件不满足后继续等待,普通 worker 没有收到通知,普通文本就一直留在 pending 状态。
修复方式是把普通请求路径的 notify_one() 改成 notify_all()。两个 worker 都被唤醒后,会分别检查自己的谓词,只有条件满足的 worker 才继续处理。
修复后进行了三步验证:
- 独立 socket 下发送一个流式请求;
- 连续发送两次普通“我在”请求;
- 确认三次都完成
accepted → started → finished。
随后只重启 TTS 和音频监听进程,保持视频推流进程继续运行。C310 真人唤醒、确认语播放、ASR、视觉回答和最终 TTS 均成功,推流仍约 29.9 FPS。
这个问题的修复很小,但也说明了多 worker 程序中,条件变量、谓词和通知方式必须一起设计,不能只看消息队列本身。
六、端到端延迟统计
最初的测试只能得到一个模糊的“从唤醒到回答很慢”。后来给音频、视觉和 TTS 统一加入 Linux 单调时钟,并记录 tts_playback_started、synthesis_ms、playback_ms 等字段。
在 13 条修复后事件链中得到的 p50/p95 如下:
| 阶段 | P50 | P95 | 解释 |
|---|---|---|---|
| 唤醒到开始采集 | 859 ms | 1000 ms | 包含确认语回声保护的等待 |
| 命令采集 | 2376 ms | 3752 ms | 用户说话时间 + 1 秒尾部静音 |
| 命令结束到 ASR 结果 | 176 ms | 246 ms | ASR 解码收尾 |
| 图像编码 | 3092 ms | 3096 ms | 当前第一大固定瓶颈 |
| VLM 语言推理 | 1988 ms | 3016 ms | 当前第二大固定瓶颈 |
| 视觉总耗时 | 5080 ms | 6107 ms | 从 ASR 完成到视觉结束 |
| 视觉结束到回答开始播放 | 3100 ms | 5667 ms | TTS 首声路径 |
| 唤醒到最终回答首声 | 11292 ms | 13899 ms | 用户感知的完整延迟 |
这组数据说明,ASR 解码只有约 0.18 秒,当前最值得优化的区域是图像编码、VLM 语言阶段和 TTS 首声。
在更早的一次单轮基线中,唤醒到首声曾达到 58.959 秒,播放结束达到 91.576 秒。后来增加简短回答约束、将 TTS 默认推理线程从 2 个调整到 4 个、完善分段播放后,一次真人样本降到首声 11.986 秒、播放结束 17.891 秒。两次测试的输入和运行状态并不完全相同,因此这组结果用于判断方向,不能替代严格的单变量基准。
七、当前的流式优化状态
视觉 worker 已经可以从 RKLLM 回调中接收文本片段,在句末组装出完整句后立即发送给 TTS。目标是让语言生成、TTS 合成和物理播放互相重叠。
两句文本探针已经观察到第二句在第一句合成期间到达,协议层面的重叠成立。当前真人回答经常只有一句话,首句与完整回答几乎同时产生,所以实际首声收益还没有形成稳定的 p50/p95 结论。另一个限制是当前 VITS 仍然属于 offline TTS,句子队列属于兼容现有模型的过渡方案。
八、现在还剩什么问题
当前项目已经完成“视频 + 音频 + 视觉 + TTS”目标板联调,但仍有几项工程工作需要继续:
1. 视觉编码约 3 秒
下一步先对同一暖机模型、同一输入帧测试 rknn_core_num=0/1/2/3,确认 NPU 核心调度、输入预处理和运行库配置是否真正生效。
2. latest-frame 预编码
如果核心 A/B 不能解决图像编码耗时,再评估让视觉 worker 在后台持续编码最新帧。实现时要同时观察 NPU 争用、画面时效、内存和视频 FPS,不能只看单次视觉耗时。
3. TTS 首声
继续记录 RKLLM 首 token、首分句、首完整句、TTS 接收和开始播放的时间;随后评估目标板兼容的真正流式 TTS。现有 VITS 需要继续保留为稳定回退。
4. 确认语回声
多轮日志中出现过“我在”“嗯”“3”等很短的 ASR 结果,它们可能继续触发视觉请求。后续需要加入短确认语过滤或 ACK 播放期间的输入门控,AEC 则单独评估。
5. 更快的任务路径
简单的“桌面有什么”“有没有人”等问题,可以评估 detector/OCR 等专用路径;复杂问题继续交给 VLM。如果板端完整 VLM 仍无法满足目标,再考虑把复杂视觉推理卸载到 Win11 主机。
九、当前状态与范围
当前系统按以下方式运行:
- 完整链路由
start_all.sh和stop_all.sh控制,开机自动启动留待后续处理; - 真实配置、RTMP key、模型和运行库保存在目标板外部环境,仓库只维护示例配置和程序代码;
- 固定唤醒词的真人验收使用 C310 实时麦克风,保存 WAV 用于确定性回归;
- 视频编码固定采用
h264_rkmpp,摄像头、推流、音频监听和视觉推理各自保持清晰的进程/线程职责; - 视觉推理运行在旁路中,持续视频推流和音频监听保持独立;
- GUI、断流自动重连和长时间稳定性纳入后续阶段。
十、总结
这个项目经历了几个阶段:先确认 C310 能稳定推到 SRS,再接入唤醒词和 ASR,然后加入 latest-frame 视觉问答,最后把 TTS 和完整并发链路接起来。
最近一次工作重点从“把功能接上”转向“把耗时拆开”。通过统一计时和结构化事件日志,已经确认 ASR 并非主要瓶颈,图像编码、VLM 推理和 TTS 首声才是后续重点。TTS 竞态修复也验证了一个经验:边缘设备上的多进程系统,稳定性问题往往藏在进程间协议、队列边界和条件变量上。
目前系统已经具备可重复启动、可观测、可回归的基础。后续优化会严格遵循单变量 A/B,每完成一项都同时检查语音链路、实际播放和视频 30 FPS,避免只优化某个数字,却破坏整个系统。
