# RealtimeAsrPipeline 说明文档

`RealtimeAsrPipeline` 是浏览器端实时 ASR（自动语音识别）的完整管线实现。它将麦克风/系统音频采集、FunASR WebSocket 推流、回包解析、LLM 文本矫正与页面展示串联在一起，且**不依赖 Vue**——页面层通过 getter 与 callback 注入依赖，便于从 `main.js` 中独立维护。

## 整体架构

```
┌─────────────────────────────────────────────────────────────────────────┐
│                           RealtimeAsrPipeline                              │
├─────────────────────────────────────────────────────────────────────────┤
│  音频采集                                                                 │
│    麦克风 MediaStream ──┐                                                 │
│    系统音 MediaStream ──┼──► AudioContext 混流 ──► Recorder (PCM 16kHz)   │
│                         │         │                    │                 │
│                         │    online / offline 各一套    │                 │
├─────────────────────────┼─────────┼────────────────────┼─────────────────┤
│  推流                   │         │                    ▼                 │
│                         │         │              sampleBuf 分片发送       │
│                         │         │                    │                 │
│                         │         │         ┌──────────┴──────────┐      │
│                         │         │         ▼                     ▼      │
│                         │         │   wsOnline (2pass-online)  wsOffline │
│                         │         │         │                     │      │
├─────────────────────────┼─────────┼─────────┼─────────────────────┼──────┤
│  回包处理               │         │         ▼                     ▼      │
│                         │         │   低延迟预览区          正式转录路径    │
│                         │         │   recTextOnline      pending 队列     │
│                         │         │         │              │ LLM 矫正     │
│                         │         │         │              ▼             │
│                         │         │         │         segmentList        │
│                         │         │         │              │             │
├─────────────────────────┼─────────┼─────────┼──────────────┼─────────────┤
│  页面回调               │         │         └──────┬───────┘             │
│                         │         │                ▼                     │
│                         │         │   onPreviewTextChange (预览)          │
│                         │         │   onMainTextChange (正式文本)         │
└─────────────────────────┴─────────┴────────────────────────────────────┘
```

## 设计原则

| 原则 | 说明 |
|------|------|
| 与 Vue 解耦 | 不直接读写 Vue `ref`；通过构造函数注入 getter/callback |
| 双通道并行 | `online` 负责低延迟预览，`offline` 负责正式句子与 LLM 矫正 |
| 顺序保证 | LLM 请求可乱序返回，但句子必须按队首顺序写入 `segmentList` |
| 竞态防护 | 使用 `epoch` 与时间戳屏障过滤旧录音周期、编辑点之前的迟到的回包 |
| 职责分离 | 本类内的 Recorder 仅用于实时 PCM 推流；页面用于保存/上传的 `MediaRecorder` 是另一套 |

## 关键常量

| 常量 | 值 | 含义 |
|------|-----|------|
| `DEFAULT_CHUNK_SIZE` | 960 | 每帧 PCM 样本数。16 kHz 下约 60 ms，符合 FunASR 实时 WebSocket 对短帧的持续输入要求 |
| `DEFAULT_BUFFER_WARN_SAMPLES` | 20000 | 软告警阈值：发送速度落后于采集时仅 `console.warn` |
| `DEFAULT_BUFFER_HARD_LIMIT_SAMPLES` | 80000 (5 s) | 硬上限：超出后丢弃最旧音频，防止内存无限增长 |

## 依赖注入（构造函数）

```js
new RealtimeAsrPipeline({
  // 媒体流（由页面持有）
  getMicStream,
  getSystemStream,

  // 暂停/草稿文本
  getPausedTranscription,
  setPausedTranscription,

  // 展示配置
  getShowSpeaker,
  getConfigWords,
  getLastTimer,
  setLastTimer,

  // 页面回写回调
  onMainTextChange,              // 正式转录区（asrResult）
  onPreviewTextChange,           // 实时预览区（asrResultOnline）
  onOfflineTextChange,           // offline 中间态（asrResultOffline）
  onActiveChange,                // 管线是否活跃（isRec）
  onPendingCorrectionCountChange,// 进行中的 LLM 矫正数量
  onError,

  // 可选
  fetchImpl,                     // 默认 window.fetch
  correctUrl,                    // 默认 "/tool/speakr/api/asr/correct"
})
```

在 `main.js` 中，上述 callback 分别映射到 `asrResult`、`asrResultOnline`、`pausedTranscription` 等 Vue 状态。

## 生命周期 API

| 方法 | 用途 |
|------|------|
| `start()` | 创建 Recorder、启动 online/offline 两条 WebSocket。返回 `1` 表示至少一条通道启动成功 |
| `stop()` | 立即停止：关闭 socket 与 Recorder，防止旧 `onProcess` 帧写入已关闭连接 |
| `finish(timeoutMs?)` | 优雅停止：发送 stop 帧并等待后端 flush 最后的 offline/LLM 结果（默认 3 s） |
| `dispose()` | 比 `stop` 更彻底；标记 `disposed`，后续迟到的 websocket/矫正回调会被忽略 |
| `isActive()` | 当前是否处于活跃推流状态 |

## 转录状态管理

### 文本分层

| 变量 | 角色 |
|------|------|
| `recText` | 已正式落到页面的矫正后文本（来自 `segmentList`） |
| `recTextOnline` | online 通道的临时低延迟预览 |
| `offlineSentenceHandoffText` | online 通道回显 offline 句子时的交接预览 |
| `segmentList` | 已按顺序矫正完成、可持久化的正式片段列表 |
| `pendingSentenceQueue` | 等待 LLM 矫正的 offline 句子队列 |

**预览区文本** = pending 预览 + offline 交接文本 + online 临时文本  
**主转录区文本** = `pausedTranscription`（草稿/暂停内容）+ `recText`

### Epoch 与时间戳屏障

- **`liveTranscriptionEpoch`**：每次录音周期变化（暂停恢复、手动编辑等）递增。旧 epoch 的 websocket/LLM 异步结果一律丢弃。
- **`realtimeTimestampBarrierMs`**：手动编辑前记录最新 ASR 时间戳；屏障及之前的消息视为过期，防止编辑后旧 ASR 文本覆盖用户修改。

### 编辑模式

1. `enterEditMode(baseText)` — 以当前页面文本为基准，建立时间戳屏障与 `realtimeEditCarryover`（编辑尾巴，用于去重）。
2. `applyEditedText(nextText)` — 立即应用用户编辑，从 `editModeBufferStartIndex` 起继续渲染后续 ASR 片段。
3. `trimOfflineTextWithCarryover()` — 裁剪 offline 结果与编辑尾巴的重叠部分，避免重复句子。

相关方法：

| 方法 | 说明 |
|------|------|
| `resetTranscript()` | 清空内部片段与 pending 队列，不清空页面历史（草稿恢复场景） |
| `startNewEpoch(options)` | 递增 epoch，可选设置/清除时间戳屏障与编辑 carryover |
| `waitForCorrections(timeoutMs?)` | 暂停/保存草稿前等待进行中的 LLM 矫正完成 |

## 音频采集与推流

### 混流

`openMixedStream(channel)` 为 `online` 或 `offline` 通道：

1. 读取麦克风流（`getMicStream`）与系统音流（`getSystemStream`）。
2. 通过独立 `AudioContext` 混流到 `MediaStreamDestination`。
3. 打开对应通道的 `Recorder` 并开始采集。

两通道监听**同一组音源**，但各自维护独立的 `AudioContext`、`Recorder` 与 PCM 发送缓冲。

### PCM 处理流程

`processRecorderFrame(channel, buffer, …, sampleRate)`：

1. 将浏览器采样率下的 PCM 重采样为 16 kHz（`Recorder.SampleData`）。
2. 追加到通道缓冲 `sampleBufOnline` / `sampleBufOffline`。
3. 超出硬上限时丢弃最旧样本。
4. 按 960 样本切分，通过 `wsSend` 发送完整帧；不足一帧的尾部保留。

> `appendInt16` 使用显式 `Int16Array` 拼接，避免在高频 `onProcess` 回调中用展开运算符复制大数组。

## WebSocket 回包模式

后端基于 FunASR 2-pass 方案，通过 `WebSocketConnectMethod`（`wsconnecter.js`）连接 `/tool/speakr/ws/asr/live?mode=online|offline`。

### Online 通道 — `handleOnlineMessage`

| mode | 行为 |
|------|------|
| `2pass-online` | 低延迟预览，追加到 `recTextOnline`，**不进入** `segmentList`。间隔 ≥ 3 s 且句末为 `。` 时插入换行 |
| `2pass-offline` | 仅作为交接预览写入 `offlineSentenceHandoffText`；**正式路径走 offline 通道** |

### Offline 通道 — `handleOfflineMessage`

| mode | 行为 |
|------|------|
| `2pass-offline` | 正式转录路径：入 pending 队列 → 调用 LLM 矫正 API → 按序 flush 到 `segmentList` |
| `offline-speaker` | 仅回填说话人元数据，**不修改**已矫正文本 |

### LLM 矫正流程

```
2pass-offline 回包
       │
       ▼
ensurePendingSentence()  ──► 旧消息过滤 / 去重 / 编辑重叠裁剪
       │
       ▼
pendingSentenceQueue（预览区可见 raw/corrected 文本）
       │
       ▼
POST /tool/speakr/api/asr/correct  { text: rawText }
       │
       ▼
finalizePendingSentence() ──► flushProcessedPendingSentences()
       │
       ▼
segmentList ──► renderFullText() ──► onMainTextChange
```

- 同一 `sourceText + timestamp + epoch` 的重复回包不会触发第二次矫正（幂等查找）。
- 即使 LLM 请求乱序返回，也只有**队首已 processed** 的项能进入 `segmentList`，保证句子顺序。

## 页面渲染

### `renderFullText()`

从 `segmentList.slice(segmentRenderStartIndex)` 构建展示文本，拼接 `pausedTranscription` 后通过 `onMainTextChange` 回写。

### `buildSegmentText()`

- 说话人：仅在说话人变化时插入 `speaker:\n` 标题；过滤无意义的 `SPK` 占位标签（受 `getShowSpeaker()` 控制）。
- 断句：`handleWithTimestamp()` 根据字符级 timestamp 与配置关键词（`getConfigWords()`），在长停顿（默认 > 6 s）或关键词处插入换行，**不修改 ASR 原文内容**。

### `syncRealtimePreview()`

汇总 pending 预览、offline 交接文本与 online 临时文本，写入预览回调。

## 内部状态一览

| 状态 | 说明 |
|------|------|
| `wsOnline` / `wsOffline` | WebSocket 客户端实例 |
| `recOnline` / `recOffline` | PCM Recorder 实例 |
| `sampleBufOnline` / `sampleBufOffline` | 待发送 PCM 缓冲 |
| `pendingCorrectionCount` | 进行中的 LLM 矫正计数 |
| `segmentRenderStartIndex` | 编辑模式下，从哪个 segment 起渲染新内容 |
| `realtimeEditCarryover` | 编辑后文本尾巴，用于 offline 去重 |
| `disposed` | 为 `true` 时忽略所有迟到的异步回调 |

## 典型使用场景（main.js）

| 场景 | 调用 |
|------|------|
| 进入录音视图 | `createRealtimeAsrPipeline()` → `pipeline.start()` |
| 暂停录音 | `pipeline.stop()` |
| 结束录音 | `await pipeline.finish()` |
| 离开录音视图 | `disposeRealtimeAsrPipeline()` |
| 保存草稿前 | `await pipeline.waitForCorrections()` |
| 用户手动编辑转录 | `pipeline.enterEditMode()` → `pipeline.applyEditedText()` |

## 相关文件

| 文件 | 关系 |
|------|------|
| `src/asr/wsconnecter.js` | WebSocket 连接、发送队列、`wsStart` / `wsSend` / `wsFinish` |
| `src/vendor-globals.js` | 导出 `Recorder`（PCM 采集与重采样） |
| `src/main.js` | 创建管线实例、绑定 Vue 状态与 UI 交互 |

## 注意事项

1. **两套录音器**：管线内 Recorder 只做实时 PCM 推流；页面 `MediaRecorder` 用于生成可保存/上传的 webm，二者互不替代。
2. **必须先 ws 再开流**：`start()` 先 `ensureRecorders()` 再 `wsStart()`，保证 WebSocket open 后立即能推流。
3. **dispose 与 stop 的区别**：离开页面应调用 `dispose()`，否则迟到的矫正回调可能写入已销毁的 UI 状态。
4. **缓冲背压**：发送跟不上采集时会 warn；超过 5 s 硬上限会主动丢帧，优先保证实时性而非完整性。
