AGENTS.md 11 KB

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

端到端数据流

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

服务部署架构

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

多用户隔离

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 轮

声纹识别

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

会话管理

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 连接

sshpass -p '123456' ssh -o StrictHostKeyChecking=no ubuntu@200.200.18.11

构建 & 部署

# 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 编译

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