# 语音助手静音功能实现方案

## 一、需求来源

根据《V11-语音助手测试用例-最新.xlsx》中静音相关测试用例，提炼出以下核心需求：

| 用例        | 核心要求                          |
| --------- | ----------------------------- |
| 静音按钮图标验证  | 默认麦克风开启图标；点击后切换图标；通话结束后重置为未静音 |
| 静音按钮基础功能  | 静音后语音输入停止采集；取消静音后恢复           |
| 静音按钮三态样式  | 默认/悬浮/按下三态样式正常；未建立通话时禁用       |
| 朗读中操作静音   | 静音仅控制麦克风输入，不影响朗读音频输出          |
| 通话重置后状态验证 | 挂断后静音状态自动重置，新通话默认麦克风开启        |
| 评审意见      | "正在说话的过程中点静音，当前这段话会发出去吗？"     |

## 二、改动前现状分析

### 已实现

- `pushButton_mute` 按钮的 UI 图标和三态样式
- `m_muted` 标志位维护
- 静音时停止 ASR，取消静音时重启 ASR
- 静音时停止语音波动画
- 静音仅停 ASR 不停 TTS（朗读中静音逻辑正确）
- `muteToggled` 信号从 MiniPanel 到 MainWindow 的传递

### 未实现/有问题

1. **说话中静音**：静音时立即 `m_speechController->stop()` 丢弃 partial 文本
2. **挂断重置**：`stopRecognition()` 未重置 `m_muted`，挂断后静音状态残留
3. **静音状态下 ASR 误重启**：`resumeRecognitionAfterPlayback()`、`handleRecognitionError()` 中的延迟重启未检查 `m_muted`
4. **快速 mute→unmute 竞态**：ASR `stop()` 有 2 秒异步窗口（`m_stopping=true`），此期间 `start()` 被拒绝
5. **状态文字**：静音后 label 仍显示 "Listening"，未显示 "已静音"
6. **MiniPanel 缺少编程接口**：无 `setMuteChecked()` / `muteChecked()` 方法，MainWindow 无法同步 UI 状态

## 三、设计方案

### 3.1 说话中静音：partial→final 发送

**决策**：静音时立即停止 ASR，将当前 partial 文本当作 final 发送给 AI。

**流程**：
```
用户说话中点静音
  → m_muted = true
  → m_speechController->stop()
  → 用 m_lastPartialText 调用 handleFinalText() 流程
  → partial 文本发送给 gateway
  → AI 回复 + TTS 朗读
  → 朗读完成后因 m_muted==true 不重启 ASR
  → 进入 Idle("已静音")
```

**防重复发送**：在 `handleFinalText()` 入口增加 `m_waitingGatewayReply` 守卫，防止 ASR 异步回调触发重复发送。

### 3.2 Pending Start 机制

**问题**：`SpeechRecognitionController::stop()` 是异步操作，2 秒后销毁 session。在此期间 `m_stopping==true`，`start()` 直接返回 false。

**方案**：在 `SpeechRecognitionController` 中增加 `m_pendingStart` 标记：

```
start() 被调用时：
  如果 m_stopping==true：
    设 m_pendingStart = true，返回 false
  正常场景：
    正常初始化 session 并启动

stop() 的 2 秒 timer 回调中：
  销毁旧 session 后：
    如果 m_pendingStart==true：
      重置 m_pendingStart
      自动调用 start()
```

MainWindow 侧配合：
- `startRecognition()` 中如果 `start()` 返回 false 且 `isStopping()==true`，跳过 retry/error 逻辑，直接 return（等待 pending start 自动回调）
- `recognitionStarted` handler 增加状态同步：收到信号后 `setState(Listening)` + 重置 retry count

### 3.3 挂断重置

在 `stopRecognition()` 中增加：

```cpp
m_muted = false;
m_lastPartialText.clear();
ui->bottomWidget->setMuteChecked(true);  // true=按钮按下=麦克风开启=未静音
```

### 3.4 静音状态下 ASR 重启拦截

在以下位置增加 `!m_muted` 条件：

| 位置 | 改动 |
|------|------|
| `startRecognition()` 入口 | 如果 `m_muted==true`，`setState(Idle); return;` |
| `resumeRecognitionAfterPlayback()` | 如果 `m_muted==true`，直接 return，不重启 ASR |
| `handleRecognitionError()` 延迟重启 | 条件增加 `!m_muted` |

### 3.5 状态文字联动

在 `setState()` 中：

| 状态 | m_muted | label 文字 |
|------|---------|-----------|
| Idle | true | "Muted" |
| Idle | false | "Listening" |
| Listening | - | "Listening" |

### 3.6 MiniPanel 编程接口

新增两个方法（参照已有的 `setSubtitleChecked()` 模式）：

```cpp
void setMuteChecked(bool checked);  // blockSignals + 同步图标
bool muteChecked() const;           // 返回按钮 checked 状态
```

## 四、文件改动详情

### 4.1 speechrecognitioncontroller.h

新增：
- `bool isStopping() const;`
- `bool m_pendingStart = false;` 成员变量

### 4.2 speechrecognitioncontroller.cpp

改动：
- 构造函数初始化列表增加 `m_pendingStart(false)`
- `start()` 中：`m_stopping==true` 时设 `m_pendingStart=true`（而非直接 return false）
- `stop()` 的 2 秒 timer 回调中：检测 `m_pendingStart`，自动调用 `start()`
- 新增 `isStopping()` 实现

### 4.3 minipanel.h

新增：
- `void setMuteChecked(bool checked);`
- `bool muteChecked() const;`

### 4.4 minipanel.cpp

新增：
- `setMuteChecked()` 实现（blockSignals + 同步图标）
- `muteChecked()` 实现

### 4.5 mainwindow.h

新增：
- `QString m_lastPartialText;` 成员变量

### 4.6 mainwindow.cpp

改动点（共 7 处）：

1. **muteToggled 槽重写**：partial→final 发送、isStopping 兜底、label 更新
2. **handlePartialText**：缓存 `m_lastPartialText`
3. **startRecognition**：静音拦截 + isStopping 兜底
4. **resumeRecognitionAfterPlayback**：静音拦截
5. **handleRecognitionError**：延迟重启静音拦截
6. **stopRecognition**：挂断重置静音状态 + 同步 UI
7. **recognitionStarted handler**：状态同步增强
8. **setState**：标签联动
9. **handleFinalText**：`m_waitingGatewayReply` 防重复发送守卫

## 五、关键流程图

### 5.1 静音流程（无 partial）

```
用户点击静音按钮
  → MiniPanel::on_pushButton_mute_clicked → emit muteToggled(true)
  → MainWindow::muteToggled(true)
    → m_muted = true
    → 语音波动画停止
    → ASR isRunning? → stop()
    → state==Listening? → setState(Idle) → label "Muted"
```

### 5.2 静音流程（有 partial）

```
用户说话中点击静音按钮
  → MiniPanel::on_pushButton_mute_clicked → emit muteToggled(true)
  → MainWindow::muteToggled(true)
    → m_muted = true
    → 语音波动画停止
    → m_asrListeningRoundActive==true
      → m_speechController->stop()
      → handleFinalText(m_lastPartialText)
        → 发送给 gateway
        → setState(Processing)
        → AI 回复 → TTS 朗读
        → 朗读完成 → resumeRecognitionAfterPlayback()
          → m_muted==true → 不重启 ASR
          → setState(Idle) → label "Muted"
```

### 5.3 取消静音流程

```
用户再次点击静音按钮
  → MiniPanel::on_pushButton_mute_clicked → emit muteToggled(false)
  → MainWindow::muteToggled(false)
    → m_muted = false
    → state==Idle && ASR 未运行?
      → label "Listening"
      → isStopping? → m_resumeRecognitionAfterPlayback = true (等 pending start)
      → 否则 → startRecognition()
    → state!=Idle? → m_resumeRecognitionAfterPlayback = true
```

### 5.4 挂断重置流程

```
用户点击挂断按钮
  → MiniPanel::cutoffClicked → MainWindow::stopRecognition()
    → m_speechController->stop()
    → m_muted = false
    → m_lastPartialText.clear()
    → ui->bottomWidget->setMuteChecked(true)  // 重置为未静音
    → setState(Idle) → label "Listening"
```

### 5.5 朗读中静音流程

```
AI 朗读中用户点击静音
  → muteToggled(true)
    → m_muted = true
    → ASR 已因朗读暂停（不在运行）
    → m_asrListeningRoundActive==false
    → 直接进入 Idle → label "Muted"
  → TTS 朗读继续不受影响
  → 朗读完成 → resumeRecognitionAfterPlayback()
    → m_muted==true → 不重启 ASR → setState(Idle) → label "Muted"
```

## 六、测试用例覆盖

| 测试用例 | 覆盖情况 |
|----------|----------|
| 静音按钮图标验证 | 图标切换已有（本次不改动），通话结束后重置 → stopRecognition 中 setMuteChecked(true) |
| 静音按钮基础功能 | 静音停 ASR + 取消静音重启 ASR → muteToggled 槽完整覆盖 |
| 静音按钮三态样式 | UI 不变；未通话禁用 → 暂不实现（当前无非通话态） |
| 朗读中操作静音 | 静音仅停 ASR 不停 TTS → resumeRecognitionAfterPlayback 中 m_muted 拦截 |
| 通话重置后状态验证 | 挂断后 m_muted=false + setMuteChecked(true) |
| 说话中静音 | partial→final 发送 + m_waitingGatewayReply 防重复 |

## 七、风险与缓解

| 风险 | 缓解措施 |
|------|----------|
| ASR stop 异步窗口（2秒） | pending start 机制：stop 完成后自动 start |
| partial→final 异步回调重复 | handleFinalText 增加 m_waitingGatewayReply 守卫 |
| partial 为空时的处理 | 检查 `!m_lastPartialText.trimmed().isEmpty()`，为空则走直接停止路径 |
| 静音状态下 recognitionStarted 回调 | handler 中检查 `!m_muted`，静音时不切换到 Listening |
