RK3588 离线视觉语音助手优化实战:从几十秒等待到秒级首声

引言

本文记录一次运行在 RK3588S 平台上的离线视觉语音助手优化过程。项目把 C310 摄像头、麦克风、KWS 唤醒、VAD、SenseVoice ASR、Qwen3-VL 视觉问答、VITS TTS、ALSA 播放和 RTMP 推流组合在一起,目标是完成一条可以持续运行的本地多模态交互链路:

用户说话
  → KWS 唤醒
  → 确认音“我在”
  → VAD 采集命令
  → SenseVoice ASR
  → Qwen3-VL 读取画面并生成回答
  → TTS 分段合成
  → ALSA 播放

摄像头推流作为独立并行链路运行:

C310 1280×720 MJPEG
  → 解码与色彩转换
  → h264_rkmpp 硬编码
  → SRS 推流

这次工作的重点放在时间测量、进程交接、队列可靠性、图像特征复用和回声处理,保留 Qwen3-VL-2B、SenseVoiceSmall 和离线 VITS。8GB 级别的板卡内存和 NPU 资源有限,模型、运行库和摄像头链路都需要保持稳定。
github可以部署尝试: https://github.com/johnjiamzhong-project/EdgeSightAssistant

一、硬件和软件约束

目标设备为 RK3588S 板卡,视觉模型通过 RKNN/RKLLM 运行,ASR 和 TTS 使用 sherpa-onnx C API。设备上的资源约束决定了几个设计边界:

  1. 图像编码、语言模型推理和视频编码都要共享板端计算资源,需要关注 NPU 竞争和推流稳定性。
  2. 当前 VITS 入口属于 offline TTS,输入是一段文本,输出是一段完整 PCM,无法直接把 LLM 的每个 token 变成即时音频。
  3. C310 摄像头和麦克风需要持续工作,视觉问答和 TTS 播放期间不能暂停视频推流。
  4. 目标板上的模型和运行库放在仓库外部,代码仓库只保存配置示例、脚本、测试和文档。

因此,本次采用句子级模拟流式 TTS、唤醒阶段图像预编码、有限队列和单调时钟埋点,保持内存边界和进程边界清晰。

二、先统一计时口径

完整交互可以从唤醒开始计时,也可以从用户说完开始计时。两者回答的问题不同:

指标 起点 终点 用途
唤醒到首声 wake_detected tts_playback_started 衡量用户整体等待感受
录音结束到首声 command_ended tts_playback_started 衡量系统处理速度
录音结束到播完 command_ended tts_finished 衡量完整回答交付时间
ASR 收尾 command_ended asr_final 衡量识别收尾开销

本文主要采用“录音结束到首声”。command_ended 是 VAD 判定用户停止讲话的时间点,tts_playback_started 是回答 PCM 开始送入播放会话的时间点。所有进程使用 Linux CLOCK_MONOTONIC 域的 monotonic_ms 记录事件,跨进程计算时不会受到系统时间校准影响。

早期版本

早期链路中,图像编码、视觉推理、完整文本生成、TTS 合成和播放存在较强的串行等待。按同一口径折算,录音结束后约 55 秒才能听到第一句话,约 88 秒完成全部播放。这个数字描述的是旧链路历史基线,保留它的意义在于展示优化前后的量级变化。

最新短回答

问题为“最上面的药盒是什么颜色?”,ASR 识别正确,视觉回答为“白色”。

command_ended              → 32214583 ms
asr_final                  → 32214797 ms
tts_playback_started       → 32216176 ms
tts_finished               → 32216828 ms

由此得到:

  • 录音结束到 ASR 最终结果:214 ms;
  • 录音结束到回答首声:1593 ms,约 1.6 秒;
  • 录音结束到播放结束:2245 ms,约 2.2 秒。

最新长回答

问题为“请描述画面中所有物体,并说明它们的颜色和位置”。回答包含黑色手套、绿色手机、两支白色药膏管和蓝色封面的说明书。

command_ended              → 32153465 ms
asr_final                  → 32153880 ms
vision_first_token         → 32154794 ms
tts_request_sent           → 32155753 ms
vision_finished            → 32157232 ms
tts_playback_started       → 32159336 ms
tts_finished               → 32167896 ms

按录音结束计算:

  • 录音结束到 ASR 最终结果:415 ms;
  • 录音结束到回答首声:5871 ms,约 5.9 秒;
  • 录音结束到播放结束:14431 ms,约 14.4 秒;
  • TTS 两段合成约 7127 ms,实际播放约 8109 ms。

长回答的首声等待明显高于短回答,主要受首段文本生成、VITS 合成量和段数影响。两类问题需要分别统计,单个百分比无法覆盖所有文本长度。

三、ASR:收窄过滤规则,保留有效短问句

项目的命令 ASR 使用离线 SenseVoiceSmall。离线模型在 command_ended 后解码最终文本,当前链路还没有 n-best 候选和声学置信度输出,因此文本过滤必须保持保守。

早期版本根据归一化后的字符数量过滤单 token。这个规则会把“谁?”“几?”等合法的单字中文问题误判为噪声。现在 AsrTextFilter 采用以下顺序:

  1. 去除空白和标点,空结果标记为 empty_asr;
  2. 与配置的唤醒确认语完全相同时标记为 ack_echo;
  3. 对“嗯、啊、呃、哦”等明确语气词进行过滤;
  4. 仅把一个字节的 ASCII 碎片标记为 single_token;
  5. 其他中文文本全部接受。

被过滤的结果仍输出结构化日志,便于区分用户确实说了语气词、确认音回声进入了麦克风,还是模型发生了近音误识别。后续如果运行库提供 n-best 或分数,可以在“垫子/电子”这类可疑词上增加确认流程。

四、VAD 和确认音期间的边播边录

唤醒后,音频状态机先进入等待命令状态。确认音发送成功后会记录:

tts_ack_sent
ack_input_gate_armed
asr_started
command_ended
asr_final

当前目标板默认 wake_ack_guard_ms=0,确认音播放期间仍将麦克风数据送入 VAD/ASR。这个选择可以减少用户等待,也会把扬声器回声带入采集端,所以需要 AEC 处理。现场如果要求确认音完全结束后再说话,可以临时启用时间门控;项目当前保留边播边录作为默认测试路径。

五、视觉预编码和“帧龄”定义

检测到“你好小视”后,视觉 worker 立即从 latest-frame store 取出当前画面并执行图像编码。用户说完问题时,如果准备好的 embedding 仍然有效,语言模型可以直接使用它。

代码中的上限为:

constexpr std::uint64_t kPreparedImageMaxAgeMs = 10000;

这里的“帧龄”指推理开始时间减去摄像头帧采集时间。准备帧帧龄不超过 10 秒时复用 embedding;超过 10 秒时丢弃准备结果,改用最新帧重新编码。

最近几轮日志中的准备帧帧龄约为 4~8 秒,图像编码约 2.1 秒,通常可以在用户说话期间完成。image_encoder_reused=true 表示问句处理阶段复用了唤醒时的图像特征。

这个优化适合静态桌面场景。帧龄 4~8 秒说明模型实际看到的画面距采集已有几秒,并不等同于画面实时更新。快速移动物体、数量统计和小字识别可以缩短复用窗口,或者在问句到达后重新取帧。预编码还会占用 NPU 时间,因此当前采用“唤醒阶段准备、问句阶段复用”的串接策略,持续观察帧龄、视觉延迟和视频 FPS。

六、视觉回答约束和安全分段

Qwen3-VL 的回答需要适合语音播放。当前提示词要求模型先给出核心结论、减少重复和展开说明,并限制回答长度。这样可以减少 VITS 合成量,也能控制 TTS 输入队列。

文本分段器有几条具体规则:

  • 所有切分位置都落在完整 UTF-8 字符边界;
  • 短回答优先等到句末,避免把最后几个字拆成孤立短尾;
  • 较长回答优先在句号、问号、逗号和顿号等安全位置切分;
  • 引号、括号、书名号内部尽量等待成对结构闭合;
  • 纯标点段不进入 VITS;
  • 首段和后续段共用一个持久 ALSA 播放会话。

这些规则解决了两类听感问题:首声等待过长,以及句末标点单独合成后产生明显停顿。当前 VITS 仍然按文本段生成完整 PCM,句子级切分属于兼容现有模型的过渡架构。

七、流式 TTS 协议:从“发出”变成“确认接收”

原始 Unix datagram 调用只能知道本地 sendto() 返回成功,发送端无法确认 TTS 服务是否真正接受了文本。当 TTS 输入队列已满时,文本可能在应用层被拒绝,回答尾段就会出现缺失。

现在的流式控制消息仍然很小:

__edgesight_tts_stream_begin__:<首段文本>
__edgesight_tts_stream_segment__:<后续文本>
__edgesight_tts_stream_end__

视觉端为每次控制消息创建临时 Unix datagram 回包地址,TTS 收到消息后返回三种结果:

__edgesight_tts_ack__
__edgesight_tts_retry__
__edgesight_tts_reject__

关键参数如下:

ACK 等待超时:1000 ms
重试间隔:20 ms
最大重试次数:250
TTS 尚未取出的输入片段:最多 2 段
已合成待播放片段:最多 2 段

收到 retry 后,发送端保留当前文本,等待队列腾出空间再发送。收到 reject 或重试耗尽时,当前流会记录失败原因,避免把未完成的文本当成完整回答结束。

目标板真实测试中,三段测试文本收到的控制结果为:

ACK、ACK、RETRY、ACK、ACK

TTS 日志随后出现 segments=3,三段均完成合成和播放,队列满造成的瞬时压力没有转化为尾段丢失。

八、AEC:发送播放参考信号

AEC 需要知道扬声器正在播放什么内容。TTS 侧会把即将播放的 44.1 kHz PCM 重采样为 16 kHz,再按 160 samples 一帧发送到音频端:

采样率:16000 Hz
单帧:160 samples
单帧时长:10 ms
SpeexDSP 自适应滤波:约 200 ms
默认播放/采集参考偏移:20 ms

参考包携带单调时钟时间戳,音频端按照采集帧时间戳取相应的 far-end PCM。参考发送使用可靠路径,发送失败会记录 errno;--aec-reference-delay-ms 和 EDGESIGHT_AEC_REFERENCE_DELAY_MS 可以调节板端实际声学延迟。

真人回归时,用户在“我在”播放期间立即说“手套是什么颜色的?”。日志同时出现 aec=enabled、aec_reference=enabled、正确的 asr_final 和视觉回答“黑色”。确认音与回答音的参考帧发送均为 errno=0。

这个结果证明基础链路可工作,声学效果仍需要在不同距离、音量和环境噪声下继续做 A/B。评估指标应包含有效语音识别率、确认音回声误识别率和残余回声能量。

九、TTS 合成线程和端到端收益

同一段文本的无播放合成测试中,TTS 推理线程从 2 个调整到 4 个后,合成耗时由约 13.8 秒降到约 7.7 秒,下降约 44%。这个参数保留在启动脚本中,目标板也完成了实际播放验证。

当前最新短回答的处理路径大致为:

录音结束
  → 214 ms:ASR 收尾
  → 约 892 ms:视觉首 token
  → 约 1.6 s:回答开始播放
  → 约 2.2 s:回答播放完成

长回答则受到两段 VITS 合成影响:

录音结束
  → 415 ms:ASR 收尾
  → 914 ms:视觉首 token
  → 约 5.9 s:第一段开始播放
  → 约 14.4 s:两段全部播放完成

从这些数据可以看出,短回答的主要体验已经进入秒级;长回答的关键瓶颈集中在首段生成、VITS 合成和音频播放长度。后续继续优化时,需要分别优化首段交接和完整回答吞吐,不能只看单个总耗时。

十、测试和验收结果

主机测试

执行 scripts/test.sh,CTest 共 8 项:

edgesight_config_test
edgesight_activation_test
edgesight_command_vad_test
edgesight_asr_text_filter_test
edgesight_wake_ack_gate_test
edgesight_vision_image_test
edgesight_latest_frame_test
edgesight_tts_text_segmentation_test

结果为 8/8 通过。新增测试覆盖单字中文问题、TTS 分段安全边界、纯标点过滤和唤醒确认门控。

目标板测试

目标板完成 Audio、TTS 和视觉构建,完整链路使用 start_all.sh 启动,stop_all.sh 收尾。测试覆盖:

  • C310 真人唤醒和命令 ASR;
  • 确认音播放期间立即说有效问题;
  • 短回答实际扬声器播放;
  • 长回答两段连续播放;
  • TTS 队列满时的 retry;
  • AEC 参考帧发送;
  • 视觉问答期间视频持续推流。

测试期间视频统计保持约 29.9 FPS,编码错误为 0。目标板日志能同时观察到音频、视觉和 TTS 的单调时间戳,便于复核每一个等待阶段。

十一、当前边界和后续方向

  1. VITS 仍然是离线入口。想继续压缩首声,需要评估支持增量输入和增量音频输出的端侧 TTS。
  2. 准备帧允许复用 10 秒,静态桌面效果较好,动态目标和细小文字需要更严格的取帧策略。
  3. SenseVoice 当前输出最终文本,没有 n-best 和声学置信度;近音词纠错需要增加上下文确认。
  4. Qwen3-VL-2B 的数量统计和小目标识别仍有波动,后续可以加入逐项列举、再计数的提示和校验。
  5. AEC 已完成基础真人验证,还需要系统比较 0、20、40、60 ms 等延迟参数,并测试不同扬声器音量和麦克风距离。
  6. 本轮重点是低延迟和可靠交接,尚未把长时间稳定性、自动断流重连和 GUI 纳入同一验收范围。

总结

这次优化的核心工作集中在四个方向:统一时钟、唤醒阶段预编码、TTS 分段可靠交接、播放参考回声抵消。

统一计时让“等待很久”变成可定位的阶段;预编码把图像处理移到用户说话期间;ACK 和 retry 让队列压力不会静默丢掉回答;AEC 让确认音播放和命令采集可以在同一时间窗口内工作。

对于端侧多模态系统,模块平均速度无法完整代表交互体验。真正需要关注的是用户说完之后多久听到第一声、较长回答能否完整播完、回声是否会污染下一轮 ASR,以及并行优化是否影响视频推流。把这些指标写进日志并用目标板真人测试复核,优化结果才具备可重复性。

项目代码、测试脚本和技术记录保存在仓库中,模型、运行库和真实设备凭据保持在仓库之外。

posted @ 2026-08-15 23:11  rambos1996  阅读(41)  评论(0)    收藏  举报