# 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
16kHz PCM]
D --> E[Silero VAD
ONNX 双阈值]
D --> F[AudioBuffer
环形缓冲]
E -->|语音结束| G[ASR]
F --> G
F --> V[声纹识别
Zvec ← MFCC]
G -->|Mimo API / Qwen3| H[transcription]
V -->|speaker_id| I[LLM 流式 SSE
Mimo v2.5 / vLLM]
H --> I
V -->|加载历史| J[对话记忆
JSON 持久化]
J --> I
I -->|TTS Segmenter
分段| K[TTS 并发生成
Mimo API 24kHz]
K -->|play_q 顺序入队| L[_player 单轨播放]
L -->|LocalAudioTrack| B
B -->|Opus| A
I -->|保存对话| J
H -.->|新 utterance
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
:9100]
W1[Worker 子进程 room-xxx]
W2[Worker 子进程 room-yyy]
LK[LiveKit Server
: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 + `` 过滤;注入 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 特征提取
120维]
B --> C[Zvec 向量检索
余弦相似度]
C -->|匹配 >0.65| D[返回已知用户]
C -->|无匹配| E[自动注册新用户]
D --> F[加载对话历史
JSON 记忆]
E --> G[注入 LLM 上下文]
F --> G
G --> H[LLM 生成
个性化回复]
H --> I[保存对话
JSON 持久化]
I --> J[同一说话人
跨会话恢复]
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`