# ASR Agent
每次开发都要跟新本文件  这非常重要！！！要保证本文件能实时反应当前系统状态
## 项目架构

```
asr/
├── asr_agent/                  # 服务端（Python）
│   ├── dispatcher.py           # HTTP Dispatcher — 多房间调度
│   ├── worker.py               # VAD → ASR → LLM → TTS 完整流水线
│   ├── voiceprint.py           # 声纹识别 + 对话记忆（Zvec + JSON）
│   ├── whisper_asr/            # ASR 核心引擎
│   │   ├── audio_processor.py  # AudioBuffer 环形缓冲
│   │   ├── qwen_engine.py      # Qwen3-ASR 本地引擎
│   │   └── transcript_processor.py  # 转录去重
│   ├── silero_vad.onnx         # Silero VAD ONNX 模型
│   ├── requirements.txt        # Python 依赖
│   ├── Dockerfile              # Dispatcher + Worker 统一镜像
│   └── Dockerfile.base         # 基础依赖镜像
├── flutter_asr_client/         # Flutter 移动客户端
│   ├── lib/services/           # LiveKit / Settings 服务
│   ├── lib/models/             # 消息模型
│   └── lib/pages/recording/    # 录音页面 + 对话列表
└── livekit-server/             # LiveKit Docker Compose
    ├── docker-compose.yml
    └── livekit.yaml
```

## 端到端数据流

```mermaid
flowchart TD
    A[App 麦克风] -->|Opus| B[LiveKit Room]
    B -->|auto_subscribe| C[Worker]

    C --> D[AudioStream<br/>16kHz PCM]
    D --> E[Silero VAD<br/>ONNX 双阈值]
    D --> F[AudioBuffer<br/>环形缓冲]

    E -->|语音结束| G[ASR]
    F --> G
    F --> V[声纹识别<br/>Zvec ← MFCC]

    G -->|Mimo API / Qwen3| H[transcription]
    V -->|speaker_id| I[LLM 流式 SSE<br/>Mimo v2.5 / vLLM]
    H --> I

    V -->|加载历史| J[对话记忆<br/>JSON 持久化]
    J --> I

    I -->|TTS Segmenter<br/>分段| K[TTS 并发生成<br/>Mimo API 24kHz]
    K -->|play_q 顺序入队| L[_player 单轨播放]
    L -->|LocalAudioTrack| B
    B -->|Opus| A

    I -->|保存对话| J

    H -.->|新 utterance<br/>cur_gen++ drain_queue| L
```

## 服务部署架构

```mermaid
flowchart TD
    subgraph External[36.152.142.37 外网]
        P10005[":10005"]
        P10003[":10003"]
    end

    subgraph Nginx[nginx]
        N1[HTTP Proxy]
        N2[WebSocket Proxy]
    end

    subgraph Host[内网服务 200.200.18.11]
        D[Dispatcher<br/>:9100]
        W1[Worker 子进程 room-xxx]
        W2[Worker 子进程 room-yyy]
        LK[LiveKit Server<br/>:7880]
    end

    P10005 --> N1
    P10003 --> N2
    N1 -->|proxy_pass| D
    N2 -->|proxy_pass| LK

    A2[App] -->|POST /connect| P10005
    A2 -->|WebSocket| P10003

    D -->|spawn_worker asyncio.Task| W1
    D -->|spawn_worker asyncio.Task| W2
    W1 -->|加入房间| LK
    W2 -->|加入房间| LK
    A2 -->|同一房间| LK
```

## 多用户隔离

```mermaid
sequenceDiagram
    participant A as App A
    participant D as Dispatcher :9100
    participant LK as LiveKit
    participant W as Worker
    
    A->>D: POST /connect
    D->>D: 创建房间 room-xxx
    D->>W: spawn_worker(room-xxx)
    D-->>A: {room, url, token}
    
    A->>LK: WebSocket connect (room-xxx)
    W->>LK: connect (room-xxx)
    
    Note over A,W: A 说话 → VAD → ASR → LLM → TTS
    
    A->>LK: disconnect
    LK->>W: participant_disconnected
    W->>W: 检测房间空 → exit
    W-->>LK: disconnect
```

## Worker 流水线详解

| 组件 | 实现 | 说明 |
|------|------|------|
| **VAD** | Silero VAD (ONNX) | 双阈值 0.5/0.2，5帧滑动窗口，min_speech 0.3s / min_silence 0.4s / max_speech 8s；无 ONNX 时回退能量 VAD |
| **VP** | LiveKit WebRTC | AEC/ANS/AGC 由 WebRTC 处理 |
| **ASR** | Mimo API / Qwen3 本地 | `ASR_PROVIDER` 切换；`mimo` 为流式 SSE，`qwen` 为本地 Qwen3-ASR-0.6B |
| **LLM** | Mimo v2.5 / vLLM | `LLM_PROVIDER` 切换；流式 SSE + `<think>` 过滤；注入 speaker 上下文与历史记忆 |
| **TTS** | Mimo v2.5 API | 24kHz PCM，按句子分段，并行生成 + `play_q` 顺序入队 |
| **播放** | 单轨 `LocalAudioTrack` | 每会话复用一条 track；`_player` 协程持续 drain；新 utterance 通过 `cur_gen++` 打断旧音频 |
| **声纹** | scipy MFCC + Zvec | 自动注册 / 识别说话人，识别结果注入 LLM 上下文，不暴露名字 |
| **记忆** | Zvec + JSON | 声纹向量存 Zvec；对话历史存 JSON，按 `speaker_id` 隔离，保留最近 20 轮 |

## 声纹识别

```mermaid
flowchart LR
    A[音频输入] --> B[MFCC 特征提取<br/>120维]
    B --> C[Zvec 向量检索<br/>余弦相似度]
    C -->|匹配 >0.65| D[返回已知用户]
    C -->|无匹配| E[自动注册新用户]

    D --> F[加载对话历史<br/>JSON 记忆]
    E --> G[注入 LLM 上下文]
    F --> G

    G --> H[LLM 生成<br/>个性化回复]
    H --> I[保存对话<br/>JSON 持久化]

    I --> J[同一说话人<br/>跨会话恢复]
    J --> F
```

## 会话管理

```mermaid
sequenceDiagram
    participant A as App
    participant D as Dispatcher
    participant LK as LiveKit
    participant W as Worker

    Note over A,W: 首次连接
    A->>D: POST /connect {identity}
    D->>D: 创建 room-xxx
    D->>W: spawn_worker(room-xxx)
    D-->>A: {room: "room-xxx", url, token}
    W->>LK: connect(room-xxx)
    A->>LK: WebSocket connect(room-xxx)

    Note over A,W: 暂停 → 继续
    A->>D: POST /connect {identity, room: "room-xxx"}
    D->>D: 检测 room-xxx 存活
    D-->>A: {room: "room-xxx", url, token}
    Note over A,W: 同房间 → Worker 保持 → 上下文不丢

    Note over A,W: 全部离开
    A->>LK: disconnect
    LK->>W: participant_disconnected
    W->>W: 房间空 → 启动 60s 退出倒计时

    alt 倒计时内 App 恢复
        A->>LK: reconnect
        W->>W: 取消倒计时，上下文保留
    else 倒计时结束仍未恢复
        W->>W: 房间空 → 退出
        W->>LK: disconnect
    end
```

## 三层记忆系统

| 层级 | 存储 | 键 | 生命周期 | 用途 |
|------|------|-----|---------|------|
| **房间会话** | `self.hist` (内存) | 房间号 | APP 暂停→继续 | 当前对话上下文 |
| **声纹向量** | Zvec 嵌入式 DB | 声纹特征 | 永久 | 说话人识别 |
| **对话记忆** | JSON 文件 | `speaker_id` | 永久 | 跨会话历史 |



## 部署步骤

## SSH 连接
```bash
sshpass -p '123456' ssh -o StrictHostKeyChecking=no ubuntu@200.200.18.11
```

## 构建 & 部署

```bash
# 1. 本地编译校验
python3 -m py_compile asr_agent/worker.py
python3 -m py_compile asr_agent/dispatcher.py

# 2. 上传文件到服务器
sshpass -p '123456' scp asr_agent/Dockerfile ubuntu@200.200.18.11:/tmp/asr_build/Dockerfile
sshpass -p '123456' scp asr_agent/worker.py ubuntu@200.200.18.11:/tmp/asr_build/worker.py
sshpass -p '123456' scp asr_agent/dispatcher.py ubuntu@200.200.18.11:/tmp/asr_build/dispatcher.py
sshpass -p '123456' scp asr_agent/voiceprint.py ubuntu@200.200.18.11:/tmp/asr_build/voiceprint.py
sshpass -p '123456' scp asr_agent/requirements.txt ubuntu@200.200.18.11:/tmp/asr_build/requirements.txt
sshpass -p '123456' scp -r asr_agent/whisper_asr ubuntu@200.200.18.11:/tmp/asr_build/

# 3. 服务器构建镜像
sshpass -p '123456' ssh ubuntu@200.200.18.11 '
cd /tmp/asr_build
docker build -t asr-dispatcher:v1 .
'

# 4. 重启容器
sshpass -p '123456' ssh ubuntu@200.200.18.11 '
docker stop asr-dispatcher 2>/dev/null
docker rm asr-dispatcher 2>/dev/null
docker run -d --name asr-dispatcher --network host --restart unless-stopped \
  -e ASR_MODEL_PATH=/data/models/Qwen3-ASR \
  -e MIMO_KEY=tp-cilplawdowf6ljqlb5lrldr0c39pe55ip1riir6zqvc3z9te \
  -e LIVEKIT_URL=ws://localhost:7880 \
  -e PUBLIC_LIVEKIT_URL=ws://36.152.142.37:10003 \
  -e LLM_PROVIDER=mimo \
  -e ASR_PROVIDER=mimo \
  -v /data/models:/data/models:ro \
  -v /data/voiceprints:/data/voiceprints \
  asr-dispatcher:v1
'

# 5. 查看日志
sshpass -p '123456' ssh ubuntu@200.200.18.11 'docker logs --tail 30 asr-dispatcher'

# 6. 测试 API
sshpass -p '123456' ssh ubuntu@200.200.18.11 '
curl -s http://localhost:9100/health
curl -s -X POST http://localhost:10005/connect -H "Content-Type: application/json" -d "{\"identity\":\"test\"}"
'
```

## 环境变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `LIVEKIT_URL` | `ws://localhost:7880` | Worker 连接 LiveKit 地址 |
| `PUBLIC_LIVEKIT_URL` | 同 LIVEKIT_URL | 返回给 App 的外网地址 |
| `LLM_PROVIDER` | `vllm` | `vllm` / `mimo` |
| `ASR_PROVIDER` | `qwen` | `qwen` / `mimo` |
| `ASR_MODEL_PATH` | `Qwen/Qwen3-ASR-0.6B` | 本地 Qwen3-ASR 模型路径 |
| `LLM_MODEL` | `qwen3.6-35b-awq` | vLLM 模式下的模型名 |
| `MIMO_KEY` | — | Mimo API Key |
| `MIMO_API_BASE` | `https://token-plan-cn.xiaomimimo.com/v1` | Mimo API 地址 |
| `VAD_MODEL_PATH` | `whisper_asr/silero_vad.onnx` | Silero VAD 模型路径 |
| `VAD_THRESHOLD_HIGH` | `0.5` | VAD 触发阈值 |
| `VAD_THRESHOLD_LOW` | `0.2` | VAD 保持阈值 |
| `VAD_WINDOW_SIZE` | `5` | VAD 滑动窗口帧数 |
| `VAD_VOICE_RATIO` | `0.5` | 窗口内判定为语音的比例 |
| `MIN_SPEECH_S` | `0.3` | 最短有效语音时长 |
| `MIN_SILENCE_S` | `0.4` | 结束静音时长 |
| `MAX_SPEECH_S` | `8.0` | 最大语音切片时长 |
| `VP_DB_PATH` | `/data/voiceprints` | 声纹数据库目录 |
| `VP_SIMILARITY_THRESHOLD` | `0.65` | 声纹匹配余弦相似度阈值 |

## 端口

| 端口 | 用途 |
|------|------|
| 7880 | LiveKit 核心服务（内网） |
| 9100 | Dispatcher HTTP（内网） |
| 10003 | LiveKit nginx 代理（外网 `36.152.142.37`） |
| 10005 | Dispatcher nginx 代理（外网） |

## App 调用流程

```
1. App 启动/恢复 → 读取本地存储的 room（如有）
2. App → POST /connect {identity, room?} → Dispatcher
3. Dispatcher：room 存在则复用/重建 Worker，否则创建新 room
4. App → WebSocket (LiveKit) with token
5. App 说话 → VAD → ASR → LLM → TTS 播放
6. App 结束 → 清除本地 room，Worker 延迟退出/房间释放
```

### 暂停 → 继续

```
1. App 暂停 → disconnect，room 名称持久化到本地
2. Worker 进入 60s 退出倒计时，上下文保留
3. App 继续 → 读取本地 room → POST /connect {identity, room: "room-xxx"}
4. Dispatcher：
   - 旧 Worker 仍在 → 返回相同 room，上下文不丢
   - 旧 Worker 已退出 → 用 room-xxx 启动新 Worker，声纹记忆恢复上下文
5. App 结束 → 清除本地 room
```

## Flutter App 编译

```bash
cd flutter_asr_client
flutter build apk --debug
# 产物: build/app/outputs/flutter-apk/app-debug.apk
```

## nginx 配置

Dispatcher 代理配置（端口 10005）：
```
# /data/nginx/config/http/dispatcher-proxy.conf
server {
    listen 10005;
    location / {
        proxy_pass http://127.0.0.1:9100;
    }
}
```
重载: `docker exec nginx nginx -s reload`
