# 语音助手子模块架构文档

## 背景与目标

本次工作围绕 `voice-assistant` 子工程进行。项目中原本存在两套“语音助手”相关实现：

- `kylin-aiassistant/voicechat`：主工程内已有完整语音通话链路，包含语音识别、AI 问答、语音朗读，以及大量与主工程状态机、模型配置、历史库、全局音频抢占等逻辑的交互。
- `voice-assistant`：独立 qmake Widgets 应用，起初只有 UI 和硬编码假数据演示，没有真实语音、问答或朗读能力。

目标是把 `voicechat` 中可复用的语音识别和朗读能力复刻到 `voice-assistant`，并让它作为独立工程运行。复刻过程中只保留独立语音助手需要的核心能力，不迁移主工程里的打断回复、全局音频抢占、数据库、模型配置 UI 联动等耦合逻辑。

最终链路为：

1. 麦克风语音实时识别成文字。
2. 最终识别文本写入 `ChatWidget` 用户气泡。
3. 最终识别文本发送到 `/home/kylin/Dev/kybot` 后端。
4. kybot Gateway 流式返回 AI 回答，写入 `ChatWidget` AI 气泡。
5. AI 回答完成后通过 TTS 合成并朗读。
6. AI 朗读期间临时停止语音识别，避免扬声器声音被麦克风再次识别造成循环对话。

## 第一阶段：语音识别复刻

第一阶段目标是移除 `voice-assistant` 的假 QA 演示，接入真实语音识别。

新增与改动：

- 新增 `voice-assistant/speechrecognitioncontroller.h`
- 新增 `voice-assistant/speechrecognitioncontroller.cpp`
- 修改 `voice-assistant/mainwindow.h`
- 修改 `voice-assistant/mainwindow.cpp`
- 修改 `voice-assistant/chatwidget.h`
- 修改 `voice-assistant/chatwidget.cpp`
- 修改 `voice-assistant/voice-assistant.pro`

核心技术：

- 使用 `kylin-ai/coreai/speech/recognizer.h`
- 使用 `kylin-ai/coreai/speech/audioconfig.h`
- 使用 `speech_recognizer_create_session`
- 使用 `speech_recognizer_init_session`
- 使用 `speech_recognizer_result_set_callback`
- 使用 `audio_config_create_continuous_audio_input_from_default_microphone`
- 使用 `speech_recognizer_start_continuous_recognition_async`

实现方式：

- `SpeechRecognitionController` 负责封装识别会话生命周期。
- `SPEECH_RECOGNIZING` 映射为实时中间识别文本。
- `SPEECH_RECOGNIZED` 映射为最终识别文本。
- `ChatWidget` 增加 `setLatestUserBubbleText()`，用于实时覆盖最后一个用户气泡，避免 partial 文本重复追加。
- 删除 `MainWindow` 中原有 `QAPair`、`m_qaPairs`、`m_roundTimer`、`m_streamTimer`、`startTest()`、`onRoundTimeout()`、`onStreamTick()` 等假数据演示链路。

UI 行为调整：

- 程序启动后默认显示 `page_listening`。
- `ChatWidget` 在后台记录语音识别内容。
- 只有按下 `pushButton_subtitle` 时才切换到 `ChatWidget` 查看对话。
- 松开字幕按钮后回到 `page_listening`。
- 语音识别到文字时不会自动跳转到 `ChatWidget`。

与原 `voicechat` 的区别：

- 没有迁移 `VoiceThread` 的完整状态机。
- 没有迁移 `VoiceUsingManager::sigInterruptUsing` 全局音频抢占。
- 没有迁移 `Globals::getErrorMsg`、cmark Markdown 错误说明、模型配置页面引导等主工程错误展示逻辑。
- 没有迁移数据库、历史记录、主窗口生命周期、模型列表等主工程耦合逻辑。

## 第二阶段：kybot Gateway 流式问答

第二阶段目标是在最终 ASR 文本产生后，向后端大模型提问，并把流式回答显示在 AI 气泡中。

新增与改动：

- 新增 `voice-assistant/gatewaychatclient.h`
- 新增 `voice-assistant/gatewaychatclient.cpp`
- 修改 `voice-assistant/mainwindow.h`
- 修改 `voice-assistant/mainwindow.cpp`
- 修改 `voice-assistant/voice-assistant.pro`

后端确认：

- 目标后端源码位于 `/home/kylin/Dev/kybot`，不是当前仓库内置代码。
- 后端编译可执行 `/home/kylin/Dev/kybot/build_all.sh`。
- 后端运行命令为 `/home/kylin/Dev/kybot/target/debug/kylinbot gateway start`。
- 默认监听 `127.0.0.1:42617`。
- WebSocket 聊天入口是 `/ws/chat`，子协议是 `kylinbot.v1`。
- 客户端发送 `{ "type": "message", "content": "..." }`。
- 服务端返回 `session_start`、`chunk`、`done`、`error` 等 JSON 消息。

当前实现方式：

- `GatewayChatClient` 从 `~/.kylinbot/config.toml` 读取 `[gateway] host`、`port` 和可选 `path_prefix`。
- WebSocket 地址使用 `ws://<host>:<port>/ws/chat?session_id=...&name=voice-assistant`。
- 发送问题时直接发送 `type = message` 的 JSON 帧。
- `chunk` 的 `content` 作为增量文本追加到 AI 气泡。
- `done` 的 `full_response` 作为完整文本触发后续朗读。

与原 `voicechat` 的区别：

- 原 `voicechat` 的 NLP 能力主要通过 `kyai::assistant::OsAssistant` 封装调用 AI 子系统。
- `voice-assistant` 没有复用 `OsAssistant`，而是作为独立应用直接连接 kybot Gateway。
- 没有引用主工程 `ConfigManager`、`InnerMessage`、`GatewayWebSocketManager` 或 `MsgPane`。
- 端口不写死到主工程配置，而是读取 kybot 自己的 `~/.kylinbot/config.toml`。

## 第三阶段：AI 回答朗读

第三阶段目标是复刻 `voicechat` 的 TTS 和 PCM 播放能力，让 AI 回答可以朗读。

新增与改动：

- 新增 `voice-assistant/speechsynthesiscontroller.h`
- 新增 `voice-assistant/speechsynthesiscontroller.cpp`
- 新增 `voice-assistant/sdl2audioplayerthread.h`
- 新增 `voice-assistant/sdl2audioplayerthread.cpp`
- 修改 `voice-assistant/mainwindow.h`
- 修改 `voice-assistant/mainwindow.cpp`
- 修改 `voice-assistant/voice-assistant.pro`

核心技术：

- 使用 `kylin-ai/coreai/speech/synthesizer.h`
- 使用 `speech_synthesizer_create_session`
- 使用 `speech_synthesizer_init_session`
- 使用 `speech_synthesizer_result_set_callback`
- 使用 `audio_config_create_audio_output_from_pcm_data`
- 使用 `speech_synthesizer_synthesize_text_async`
- 使用 SDL2 播放合成返回的 PCM 数据

实现方式：

- `SpeechSynthesisController` 负责 TTS 会话管理。
- `synthesisCallback` 从 `SpeechSynthesisResult` 中读取 PCM 数据。
- `SDL2AudioPlayerThread` 负责 SDL2 音频初始化、PCM 队列和播放。
- `GatewayChatClient::replyFinished` 到达后，`MainWindow::handleAiReplyFinished()` 调用 `SpeechSynthesisController::speak()` 朗读完整 AI 回答。

与原 `voicechat` 的区别：

- 没有迁移 `VoiceChatReader::interruptReading()` 的用户打断朗读逻辑。
- 没有迁移朗读结束后驱动 `VoiceThread` 状态机的逻辑。
- 没有迁移朗读失败后通过主工程 JSON 协议给 QML 发错误气泡的逻辑。
- 没有迁移等待朗读队列、逐句追加朗读、索引重置、主工程 `sigReadEnd(index)` 编排。
- 当前实现更简单：AI 回答完成后朗读完整回答。

## 防止自循环识别

在完整链路跑通后发现一个关键问题：AI 朗读的扬声器声音会被麦克风再次识别，导致“用户说一句话，应用自己不断对话”的循环。

当前处理方式：

- `SpeechSynthesisController` 发出 `readingStarted` 时，`MainWindow` 调用 `pauseRecognitionForPlayback()`。
- 如果 ASR 正在运行，则临时停止识别，并记录 `m_resumeRecognitionAfterPlayback = true`。
- `readingFinished` 或 TTS 错误时调用 `resumeRecognitionAfterPlayback()`。
- 只有确实是因为朗读临时暂停的场景，才恢复 ASR。
- 用户关闭窗口或整体停止识别时，会清掉恢复标记，避免销毁时误启动识别。

这属于 `voice-assistant` 内部的本地防回声处理，不依赖主工程的 `VoiceUsingManager`，也不是原 `voicechat` 中的“用户打断朗读”逻辑。

## 工程依赖变化

`voice-assistant/voice-assistant.pro` 当前新增了这些依赖：

- `QT += multimedia`
- `QT += websockets`
- `QT += network`
- `INCLUDEPATH += /usr/include/kylin-ai`
- `LIBS += -lkysdk-coreai-speech`
- `LIBS += -lkysdk-ai-common`
- `LIBS += -lSDL2`

新增源码已加入 `SOURCES` 和 `HEADERS`：

- `speechrecognitioncontroller`
- `gatewaychatclient`
- `sdl2audioplayerthread`
- `speechsynthesiscontroller`

## 当前主要数据流

语音识别阶段：

```text
默认麦克风
  -> kylin-ai speech recognizer
  -> partialText / finalText
  -> MainWindow
  -> ChatWidget 用户气泡
```

问答阶段：

```text
ASR final text
  -> GatewayChatClient
  -> kybot /ws/chat
  -> {"type":"message","content":"..."}
  -> chunk / done
  -> ChatWidget AI 气泡
```

朗读阶段：

```text
AI final answer
  -> SpeechSynthesisController
  -> kylin-ai speech synthesizer
  -> PCM data callback
  -> SDL2AudioPlayerThread
  -> 扬声器播放
```

防回声阶段：

```text
readingStarted
  -> stop ASR
  -> SDL2 播放 AI 回答
  -> readingFinished / synthesis error
  -> restart ASR
```

## 联调与验证情况

已验证：

- `voice-assistant` 子工程 qmake 构建通过。
- Node/NVM 环境中的 OpenClaw 服务已停止、禁用并卸载。
- `/home/kylin/Dev/kybot/target/debug/kylinbot gateway start` 可启动 kybot Gateway。
- 实机确认 kybot 监听端口为 `127.0.0.1:42617`。
- 使用 WebSocket 脚本验证 `kylinbot.v1` + `/ws/chat` 协议可用。
- `message` 发送成功。
- 能收到 `chunk` 和 `done`。
- `voice-assistant` 代码已按 kybot 真实协议实现。

构建命令：

```bash
mkdir -p target/voice-assistant-build
cd target/voice-assistant-build
qmake ../../voice-assistant/voice-assistant.pro
make -j2
```

已知非阻塞 warning：

- `minipanel.cpp` 中几个空槽函数存在 unused parameter warning。
- 这些 warning 是原 UI 代码遗留，不影响语音链路。

## 后续维护注意事项

1. 当前后端目标是 `/home/kylin/Dev/kybot`，不是 Node/NVM 全局安装的 OpenClaw。启动后端使用 `/home/kylin/Dev/kybot/target/debug/kylinbot gateway start`。

2. `GatewayChatClient` 当前读取 `~/.kylinbot/config.toml` 的 `[gateway] host`、`port` 和 `path_prefix`。如果后续部署环境中 kybot 配置路径变化，需要在该类里集中调整。

3. 当前 `session_id` 在 `GatewayChatClient` 初始化时生成。如果后续希望每次提问独立会话、隔离上下文或清理历史，应扩展 `GatewayChatClient` 的 session 策略。

4. 当前朗读策略是“AI 回答完成后朗读整段”。如果要边流式回答边按句朗读，需要新增句子切分和朗读队列，但要小心不要重新引入原 `voicechat` 里复杂的打断/状态机耦合。

5. 朗读期间停止 ASR 是必须的，否则会出现扬声器声音被麦克风识别的自循环问题。

6. 当前没有做回声消除、VAD、端点检测或设备选择。这些属于后续体验优化，不在本次复刻范围内。

7. 当前没有引入主工程数据库和历史记录。如果需要持久化语音助手对话，建议在 `voice-assistant` 内新增独立存储，不直接复用主工程 `ChatDatabase`。

8. 当前没有迁移用户主动打断朗读/回答的交互。如果后续需要该能力，应设计为 `voice-assistant` 本地取消和停止逻辑，不建议直接搬 `VoiceThread` 的主工程状态机。

## 总结

本次复刻把 `voice-assistant` 从一个只有假数据演示的 UI 子工程，扩展为可独立运行的语音助手应用。核心能力包括真实语音识别、kybot 流式问答、AI 回答朗读，以及朗读期间暂停 ASR 的防回声处理。

实现上尽量复用 `voicechat` 中成熟的底层技术路线，包括 `kylin-ai` speech SDK 和 SDL2 PCM 播放；同时刻意避免迁移主工程中的全局状态、打断逻辑、数据库、模型配置和 QML 协议耦合，使 `voice-assistant` 保持相对独立、边界清晰，便于后续维护和继续演进。
