374 lines
16 KiB
Markdown
374 lines
16 KiB
Markdown
|
|
# 语音通话完整流程图
|
|||
|
|
|
|||
|
|
## 📊 整体架构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────┐ WebSocket ┌─────────────┐
|
|||
|
|
│ │ ◄─────────────────────────► │ │
|
|||
|
|
│ 客户端 │ │ 服务器端 │
|
|||
|
|
│ (uni-app) │ │ (FastAPI) │
|
|||
|
|
│ │ │ │
|
|||
|
|
└─────────────┘ └─────────────┘
|
|||
|
|
│ │
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
┌─────────────┐ ┌─────────────────┐
|
|||
|
|
│ 录音管理器 │ │ ASR (阿里云) │
|
|||
|
|
│ recorderMgr │ │ Paraformer │
|
|||
|
|
└─────────────┘ └─────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────┐
|
|||
|
|
│ LLM (通义千问) │
|
|||
|
|
│ Qwen-flash │
|
|||
|
|
└─────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────┐
|
|||
|
|
│ TTS (阿里云) │
|
|||
|
|
│ CosyVoice-v2 │
|
|||
|
|
└─────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🔄 详细流程
|
|||
|
|
|
|||
|
|
### 阶段1:连接建立
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
客户端 服务器
|
|||
|
|
│ │
|
|||
|
|
├─ 1. 打开语音通话页面 │
|
|||
|
|
│ │
|
|||
|
|
├─ 2. 建立 WebSocket 连接 ──────────────►│
|
|||
|
|
│ ws://192.168.1.141:30101/voice/call │
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 3. 验证用户身份
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 4. 创建会话 (VoiceCallSession)
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 5. 启动 ASR (paraformer-realtime-v2)
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 6. 启动后台任务
|
|||
|
|
│ │ - LLM 处理循环
|
|||
|
|
│ │ - TTS 处理循环
|
|||
|
|
│ │ - 空闲检测
|
|||
|
|
│ │ - 静默检测
|
|||
|
|
│ │
|
|||
|
|
│ ◄──────────────────────────────────── ├─ 7. 发送 ready 信号
|
|||
|
|
│ {"type":"ready"} │
|
|||
|
|
│ │
|
|||
|
|
├─ 8. 显示"已连接" │
|
|||
|
|
│ │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 阶段2:用户说话(优化后的流程)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
客户端 服务器
|
|||
|
|
│ │
|
|||
|
|
├─ 1. 用户按住"按住说话"按钮 │
|
|||
|
|
│ isTalking = true │
|
|||
|
|
│ │
|
|||
|
|
├─ 2. 启动录音 │
|
|||
|
|
│ format: 'pcm' │
|
|||
|
|
│ sampleRate: 16000 │
|
|||
|
|
│ numberOfChannels: 1 │
|
|||
|
|
│ │
|
|||
|
|
├─ 3. 用户说话 3-5 秒 │
|
|||
|
|
│ "你好,今天天气怎么样?" │
|
|||
|
|
│ │
|
|||
|
|
├─ 4. 用户松开按钮 │
|
|||
|
|
│ isTalking = false │
|
|||
|
|
│ │
|
|||
|
|
├─ 5. 停止录音 │
|
|||
|
|
│ recorderManager.stop() │
|
|||
|
|
│ │
|
|||
|
|
├─ 6. 读取录音文件 │
|
|||
|
|
│ tempFilePath → ArrayBuffer │
|
|||
|
|
│ 大小: 160000 bytes (5秒) │
|
|||
|
|
│ │
|
|||
|
|
├─ 7. 开始分片发送 ─────────────────────►│
|
|||
|
|
│ 【官方推荐参数】 │
|
|||
|
|
│ 每片: 3200 bytes │
|
|||
|
|
│ 间隔: 100ms │
|
|||
|
|
│ │
|
|||
|
|
├─ 片段 1 (3200 bytes) ─────────────────►├─ 接收片段 1
|
|||
|
|
│ ├─ feed_audio(data)
|
|||
|
|
│ ├─ recognition.send_audio_frame(data)
|
|||
|
|
│ │
|
|||
|
|
├─ 延迟 100ms │
|
|||
|
|
│ │
|
|||
|
|
├─ 片段 2 (3200 bytes) ─────────────────►├─ 接收片段 2
|
|||
|
|
│ ├─ feed_audio(data)
|
|||
|
|
│ ├─ recognition.send_audio_frame(data)
|
|||
|
|
│ │
|
|||
|
|
├─ 延迟 100ms │
|
|||
|
|
│ │
|
|||
|
|
├─ ... │
|
|||
|
|
│ │
|
|||
|
|
├─ 片段 50 (3200 bytes) ────────────────►├─ 接收片段 50
|
|||
|
|
│ ├─ feed_audio(data)
|
|||
|
|
│ ├─ recognition.send_audio_frame(data)
|
|||
|
|
│ │
|
|||
|
|
├─ 延迟 100ms │
|
|||
|
|
│ │
|
|||
|
|
├─ 发送 "end" 标记 ─────────────────────►├─ 接收 "end"
|
|||
|
|
│ ├─ finalize_asr()
|
|||
|
|
│ ├─ recognition.stop()
|
|||
|
|
│ │
|
|||
|
|
│ ├─ ASR 完成识别
|
|||
|
|
│ │ 识别结果: "你好,今天天气怎么样?"
|
|||
|
|
│ │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 阶段3:ASR 识别
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
服务器端流程
|
|||
|
|
│
|
|||
|
|
├─ 1. ASR 接收音频片段
|
|||
|
|
│ - 片段 1: 3200 bytes
|
|||
|
|
│ - 片段 2: 3200 bytes
|
|||
|
|
│ - ...
|
|||
|
|
│ - 片段 50: 3200 bytes
|
|||
|
|
│
|
|||
|
|
├─ 2. 实时识别(边收边识别)
|
|||
|
|
│ - 部分结果: "你好"
|
|||
|
|
│ - 部分结果: "你好,今天"
|
|||
|
|
│ - 部分结果: "你好,今天天气"
|
|||
|
|
│
|
|||
|
|
├─ 3. 收到 "end" 标记
|
|||
|
|
│ - 调用 recognition.stop()
|
|||
|
|
│
|
|||
|
|
├─ 4. 返回最终结果
|
|||
|
|
│ - is_sentence_end: true
|
|||
|
|
│ - text: "你好,今天天气怎么样?"
|
|||
|
|
│
|
|||
|
|
├─ 5. 触发回调
|
|||
|
|
│ - WSRecognitionCallback.on_event()
|
|||
|
|
│ - session.handle_sentence(text)
|
|||
|
|
│
|
|||
|
|
├─ 6. 放入 LLM 队列
|
|||
|
|
│ - asr_to_llm.put(text)
|
|||
|
|
│
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 阶段4:LLM 生成回复
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
服务器端流程 客户端
|
|||
|
|
│ │
|
|||
|
|
├─ 1. 从队列获取识别文本 │
|
|||
|
|
│ text = "你好,今天天气怎么样?" │
|
|||
|
|
│ │
|
|||
|
|
├─ 2. 构建对话历史 │
|
|||
|
|
│ history = [ │
|
|||
|
|
│ {role: "system", content: "..."},│
|
|||
|
|
│ {role: "user", content: text} │
|
|||
|
|
│ ] │
|
|||
|
|
│ │
|
|||
|
|
├─ 3. 调用 LLM (Qwen-flash) │
|
|||
|
|
│ stream = chat_completion_stream() │
|
|||
|
|
│ │
|
|||
|
|
├─ 4. 流式接收 LLM 输出 │
|
|||
|
|
│ chunk 1: "你" │
|
|||
|
|
│ chunk 2: "好" │
|
|||
|
|
│ chunk 3: "呀" │
|
|||
|
|
│ chunk 4: "," ◄─ 遇到标点 │
|
|||
|
|
│ │
|
|||
|
|
├─ 5. 发送文本给客户端 ─────────────────►├─ 收到 reply_text
|
|||
|
|
│ {"type":"reply_text", │ 显示文字
|
|||
|
|
│ "text":"你好呀,今天..."} │
|
|||
|
|
│ │
|
|||
|
|
├─ 6. 放入 TTS 队列 │
|
|||
|
|
│ llm_to_tts.put("你好呀,") │
|
|||
|
|
│ │
|
|||
|
|
├─ 7. 继续接收 LLM 输出 │
|
|||
|
|
│ chunk 5: "今" │
|
|||
|
|
│ chunk 6: "天" │
|
|||
|
|
│ ... │
|
|||
|
|
│ │
|
|||
|
|
├─ 8. LLM 完成 │
|
|||
|
|
│ llm_to_tts.put(END_OF_TTS) │
|
|||
|
|
│ │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 阶段5:TTS 合成语音
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
服务器端流程 客户端
|
|||
|
|
│ │
|
|||
|
|
├─ 1. 从队列获取文本片段 │
|
|||
|
|
│ text = "你好呀," │
|
|||
|
|
│ │
|
|||
|
|
├─ 2. 清理文本 │
|
|||
|
|
│ - 去除 Markdown 符号 │
|
|||
|
|
│ - 去除动作描述 │
|
|||
|
|
│ - 去除波浪线 │
|
|||
|
|
│ │
|
|||
|
|
├─ 3. 调用 TTS (CosyVoice-v2) │
|
|||
|
|
│ model: "cosyvoice-v2" │
|
|||
|
|
│ voice: "longxiaochun_v2" │
|
|||
|
|
│ format: "mp3" │
|
|||
|
|
│ │
|
|||
|
|
├─ 4. 合成音频 │
|
|||
|
|
│ audio_bytes = synthesize(text) │
|
|||
|
|
│ │
|
|||
|
|
├─ 5. 发送音频数据 ─────────────────────►├─ 收到音频流
|
|||
|
|
│ websocket.send_bytes(audio_bytes) │ audioData.push(data)
|
|||
|
|
│ │
|
|||
|
|
├─ 6. 继续处理下一个片段 │
|
|||
|
|
│ text = "今天天气不错~" │
|
|||
|
|
│ │
|
|||
|
|
├─ 7. 合成并发送 ───────────────────────►├─ 收到音频流
|
|||
|
|
│ │ audioData.push(data)
|
|||
|
|
│ │
|
|||
|
|
├─ 8. 所有片段完成 │
|
|||
|
|
│ │
|
|||
|
|
├─ 9. 发送结束信号 ─────────────────────►├─ 收到 reply_end
|
|||
|
|
│ {"type":"reply_end"} │
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 合并音频数据
|
|||
|
|
│ │ mergeAudioData()
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 播放音频
|
|||
|
|
│ │ audioContext.play()
|
|||
|
|
│ │
|
|||
|
|
│ ├─ 用户听到 AI 的声音
|
|||
|
|
│ │ "你好呀,今天天气不错~"
|
|||
|
|
│ │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## ⏱️ 时间线分析
|
|||
|
|
|
|||
|
|
### 优化前(会超时)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
时间轴:
|
|||
|
|
0s ─ 用户按住按钮
|
|||
|
|
1s ─ 用户说话
|
|||
|
|
2s ─ 用户松开按钮
|
|||
|
|
2s ─ 一次性发送 260KB ❌
|
|||
|
|
3s ─ 服务器收到大块数据
|
|||
|
|
3s ─ ASR 无法处理 ❌
|
|||
|
|
...
|
|||
|
|
62s ─ 空闲超时 ❌
|
|||
|
|
62s ─ 连接关闭
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 优化后(正常工作)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
时间轴:
|
|||
|
|
0s ─ 用户按住按钮
|
|||
|
|
5s ─ 用户松开按钮(说了 5 秒)
|
|||
|
|
5s ─ 开始分片发送
|
|||
|
|
5.1s ─ 发送片段 1 (3200 bytes)
|
|||
|
|
5.2s ─ 发送片段 2 (3200 bytes)
|
|||
|
|
...
|
|||
|
|
10s ─ 发送片段 50 (3200 bytes)
|
|||
|
|
10s ─ 发送 "end" 标记
|
|||
|
|
10s ─ ASR 完成识别 ✅
|
|||
|
|
11s ─ LLM 开始生成
|
|||
|
|
13s ─ LLM 完成,TTS 开始
|
|||
|
|
15s ─ TTS 完成,发送音频
|
|||
|
|
16s ─ 客户端播放音频 ✅
|
|||
|
|
20s ─ 播放完成 ✅
|
|||
|
|
|
|||
|
|
总耗时: 20 秒
|
|||
|
|
超时时间: 120 秒
|
|||
|
|
状态: 正常 ✅
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🔑 关键优化点
|
|||
|
|
|
|||
|
|
### 1. 分片大小
|
|||
|
|
```
|
|||
|
|
优化前: 8192 bytes (8KB)
|
|||
|
|
优化后: 3200 bytes (3.2KB) ✅
|
|||
|
|
原因: 匹配官方推荐,符合 100ms 音频时长
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 发送间隔
|
|||
|
|
```
|
|||
|
|
优化前: 50ms
|
|||
|
|
优化后: 100ms ✅
|
|||
|
|
原因: 匹配官方推荐,模拟实时音频流
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 超时时间
|
|||
|
|
```
|
|||
|
|
优化前: 60 秒
|
|||
|
|
优化后: 120 秒 ✅
|
|||
|
|
原因: 给 ASR + LLM + TTS 留出足够处理时间
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 结束标记
|
|||
|
|
```
|
|||
|
|
优化前: 无
|
|||
|
|
优化后: 发送 "end" 标记 ✅
|
|||
|
|
原因: 告诉服务器音频发送完毕,触发最终识别
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📈 性能指标
|
|||
|
|
|
|||
|
|
### 延迟分析
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────┬──────────┬─────────┐
|
|||
|
|
│ 环节 │ 耗时 │ 占比 │
|
|||
|
|
├─────────────────┼──────────┼─────────┤
|
|||
|
|
│ 用户说话 │ 5s │ 25% │
|
|||
|
|
│ 分片发送 │ 5s │ 25% │
|
|||
|
|
│ ASR 识别 │ 1s │ 5% │
|
|||
|
|
│ LLM 生成 │ 3s │ 15% │
|
|||
|
|
│ TTS 合成 │ 2s │ 10% │
|
|||
|
|
│ 音频传输 │ 1s │ 5% │
|
|||
|
|
│ 音频播放 │ 3s │ 15% │
|
|||
|
|
├─────────────────┼──────────┼─────────┤
|
|||
|
|
│ 总计 │ 20s │ 100% │
|
|||
|
|
└─────────────────┴──────────┴─────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 网络流量
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
上行(客户端 → 服务器):
|
|||
|
|
- 音频数据: 160KB (5秒录音)
|
|||
|
|
- 控制消息: < 1KB
|
|||
|
|
- 总计: ~160KB
|
|||
|
|
|
|||
|
|
下行(服务器 → 客户端):
|
|||
|
|
- 音频数据: ~50KB (MP3 格式)
|
|||
|
|
- 控制消息: < 1KB
|
|||
|
|
- 总计: ~50KB
|
|||
|
|
|
|||
|
|
总流量: ~210KB / 次对话
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🎯 成功标准
|
|||
|
|
|
|||
|
|
一次成功的语音通话应该满足:
|
|||
|
|
|
|||
|
|
1. ✅ 录音时长 ≥ 3 秒
|
|||
|
|
2. ✅ 分片发送完成(50 片左右)
|
|||
|
|
3. ✅ ASR 识别成功(收到识别文本)
|
|||
|
|
4. ✅ LLM 生成成功(收到回复文本)
|
|||
|
|
5. ✅ TTS 合成成功(收到音频数据)
|
|||
|
|
6. ✅ 音频播放成功(听到声音)
|
|||
|
|
7. ✅ 总耗时 < 30 秒
|
|||
|
|
8. ✅ 无 "idle timeout" 错误
|
|||
|
|
|
|||
|
|
## 🚀 下一步优化方向
|
|||
|
|
|
|||
|
|
### 短期优化
|
|||
|
|
1. 实现真正的实时流式录音(使用 `onFrameRecorded`)
|
|||
|
|
2. 优化 LLM 响应速度(使用更快的模型)
|
|||
|
|
3. 实现打断功能(用户可以打断 AI)
|
|||
|
|
|
|||
|
|
### 长期优化
|
|||
|
|
1. 多轮对话优化(更好的上下文管理)
|
|||
|
|
2. 情感识别(根据语气调整回复)
|
|||
|
|
3. 个性化语音(用户自定义音色)
|
|||
|
|
4. 降噪处理(提高识别准确率)
|