# 语音助手打断功能实现方案

## 一、需求描述

1. **触发条件**: 仅在朗读（TTS 播放）状态时允许触发打断
2. **打断行为**: 立即停止当前 TTS 朗读，进入下一轮对话（自动进入聆听状态）
3. **UI 要求**:
   - 非朗读状态：显示音波动画 + `label_tip` 提示文本
   - 朗读状态：音波动画隐藏（控件保持占位不塌陷）、`label_tip` 隐藏、显示带停止图标的打断按钮（图标左侧 + "点击打断回答" 文字右侧）

## 二、状态流转

```
Listening → ... → Answering → Reading ──[朗读完成]──→ readingFinished → Idle → Listening
                                    │
                                    └──[用户点击打断]──→ close() → resumeRecognitionAfterPlayback()
                                                                   → Idle → 2100ms → Listening
```

## 三、UI 状态对照

| 状态 | 音波动画 | label_tip | interruptButton |
|------|---------|-----------|-----------------|
| Idle（未静音） | 绘制但不动 | "Listening" | 隐藏 |
| Idle（静音） | 绘制但不动 | "Muted" | 隐藏 |
| Listening | 动画中 | "Listening" | 隐藏 |
| Processing | 绘制但不动 | 保持上态 | 隐藏 |
| Answering | 绘制但不动 | 保持上态 | 隐藏 |
| **Reading** | **不绘制（占位）** | **隐藏** | **显示** |

## 四、代码改动清单

### 4.1 资源注册 — `src.qrc`

添加停止图标到 Qt 资源系统：

```xml
<file>image/icon/stop.svg</file>
```

### 4.2 UI 布局 — `mainwindow.ui`

1. **修复命名**: `name="label"` → `name="label_tip"`，与代码中 `ui->label_tip` 对齐
2. **新增控件**: 在 `page_listening` 页面 `label_tip` 下方添加 `QToolButton`，命名 `interruptButton`：
   - 默认隐藏（`visible=false`）
   - `autoRaise=true`（无边框扁平按钮）
   - `text` 属性设为英文源文本 `"Tap to interrupt response"`

### 4.3 音频波动画控件 — `voicewavewidget.h/.cpp`

新增 `setDrawable()` 方法控制是否绘制音波条：

- **头文件**：添加 `setDrawable(bool)`、`isDrawable()`、`m_drawable` 成员（默认 `true`）
- **实现文件**：`paintEvent` 开头检查 `if (!m_drawable) return;`，不绘制但仍保持 44×24 控件尺寸占位

### 4.4 核心逻辑 — `mainwindow.cpp`

#### a) 构造函数初始化打断按钮

```cpp
// 配置图标（从资源加载 stop.svg）
QIcon stopIcon;
stopIcon.addFile(QStringLiteral(":/image/icon/stop.svg"));
ui->interruptButton->setIcon(stopIcon);
ui->interruptButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

// 连接点击信号
connect(ui->interruptButton, &QToolButton::clicked, this, [this]() {
    if (m_state != Reading) return;       // 仅朗读状态可打断
    m_synthesisController->close();       // 停止 TTS 合成和播放
    resumeRecognitionAfterPlayback();     // 进入下一轮聆听
});
```

#### b) 状态 UI 更新函数改造

`updateVoiceStateUi()` 新增 `Reading` 状态分支：

```cpp
void MainWindow::updateVoiceStateUi()
{
    const bool isReading = (m_state == Reading);

    ui->widget_voice_logo->setActive(!m_muted && m_state == Listening);
    ui->widget_voice_logo->setDrawable(!isReading);  // 朗读时隐藏音波

    if (isReading) {
        ui->label_tip->hide();
        ui->interruptButton->show();
        return;
    }

    ui->interruptButton->hide();

    if (m_state == Idle && m_muted) {
        ui->label_tip->setText(tr("Muted"));
        ui->label_tip->show();
    } else if (m_state == Idle || m_state == Listening) {
        ui->label_tip->setText(tr("Listening"));
        ui->label_tip->show();
    }
}
```

### 4.5 国际化翻译

- **源文本**（英文）：`"Tap to interrupt response"`
- **简体中文**（`voice_assistant_zh_CN.ts`）：`"点击打断回答"`
- **繁体中文**（`voice_assistant_zh_TW.ts`）：`"點擊打斷回答"`

## 五、关键技术点

### 5.1 打断执行链

```
用户点击打断按钮
  → m_synthesisController->close()
    → destroyAudioConfig()     // 销毁音频输出配置
    → 销毁 synthesis session   // 终止云端/本地合成
    → m_player->clearQueue()   // 清空播放队列
    → m_player->stop()         // 停止 SDL 音频播放
  → resumeRecognitionAfterPlayback()
    → setState(Idle)           // 状态回到空闲
    → QTimer 2100ms 后 startRecognition()
    → setState(Listening)      // 进入聆听状态
```

### 5.2 音波动画占位方案

通过 `VoiceWaveWidget::setDrawable(false)` 隐藏音波条的绘制，但控件保持 44×24 的固定最小/最大尺寸，确保布局不塌陷。

### 5.3 与挂断（cutoff）的区别

| | 打断（interruptButton） | 挂断（cutoffClicked） |
|---|---|---|
| 触发条件 | 仅 Reading 状态 | 任意状态 |
| 行为 | 停止 TTS → 进入下一轮 | 停止全部 → 关闭窗口 |
| 网关处理 | 不取消（网关已返回） | 取消网关应答 |
| 窗口 | 保持打开 | 关闭 |

## 六、涉及文件汇总

| 文件 | 改动类型 |
|------|---------|
| `src.qrc` | 新增资源引用 |
| `mainwindow.ui` | UI 布局修改 + 命名修复 |
| `mainwindow.cpp` | 核心逻辑新增 |
| `voicewavewidget.h` | API 新增 |
| `voicewavewidget.cpp` | 实现新增 |
| `voice_assistant_zh_CN.ts` | 新增翻译 |
| `voice_assistant_zh_TW.ts` | 新增翻译 |
