# 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`