Ai_GirlFriend/xuniYou/官方文档分析和正确实现.md
2026-02-28 18:04:34 +08:00

330 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 官方文档分析和正确实现
## 📚 官方文档关键信息
### 1. send_audio_frame 的正确用法
根据官方文档:
> **每次推送的音频流不宜过大或过小建议每包音频时长为100ms左右大小在1KB~16KB之间。**
### 2. 官方示例代码
#### 识别本地文件的正确方式
```python
recognition.start()
try:
f = open("asr_example.wav", 'rb')
while True:
audio_data = f.read(3200) # 每次读取 3200 字节(约 3KB
if not audio_data:
break
else:
recognition.send_audio_frame(audio_data) # 发送小块数据
time.sleep(0.1) # 延迟 100ms
f.close()
except Exception as e:
raise e
recognition.stop()
```
**关键点**
- ✅ 每次读取 3200 字节(约 3KB
- ✅ 延迟 100ms0.1秒)
- ✅ 循环发送,模拟实时流
#### 识别麦克风的正确方式
```python
recognition.start()
while True:
if stream:
data = stream.read(3200, exception_on_overflow=False) # 每次 3200 字节
recognition.send_audio_frame(data) # 立即发送
else:
break
recognition.stop()
```
**关键点**
- ✅ 每次读取 3200 字节
- ✅ 实时发送,无需延迟(因为是实时流)
## 🔍 我们的问题
### 当前实现(错误)
```python
# 服务器端 lover/routers/voice_call.py
async def feed_audio(self, data: bytes):
if self.recognition:
self.recognition.send_audio_frame(data) # 直接发送整个文件
```
```javascript
// 客户端
fs.readFile({
filePath: res.tempFilePath,
success: (fileRes) => {
// 一次性发送 260KB ❌
socketTask.send({ data: fileRes.data })
}
})
```
**问题**
- ❌ 客户端一次性发送 260KB
- ❌ 服务器直接喂给 ASR
- ❌ 不符合官方要求1KB~16KB
### 正确实现(已修复)
```javascript
// 客户端分片发送
sendAudioInChunks(audioData) {
const chunkSize = 8192 // 8KB符合官方要求
for (let offset = 0; offset < totalSize; offset += chunkSize) {
const chunk = audioData.slice(offset, offset + chunkSize)
socketTask.send({ data: chunk })
await sleep(50) // 延迟 50ms每秒发送 20 片)
}
socketTask.send({ data: 'end' }) // 发送结束标记
}
```
**改进**
- ✅ 每次发送 8KB符合 1KB~16KB 要求)
- ✅ 延迟 50ms比官方建议的 100ms 更快)
- ✅ 发送结束标记
## 📊 数据大小计算
### PCM 音频数据大小
```
采样率16000 Hz
位深度16 bit = 2 bytes
声道数1单声道
每秒数据量 = 16000 × 2 × 1 = 32000 bytes = 31.25 KB/s
```
### 官方建议
```
每包时长100ms
每包大小31.25 KB/s × 0.1s = 3.125 KB ≈ 3200 bytes
```
**这就是为什么官方示例用 3200 字节!**
### 我们的实现
```
每包大小8192 bytes = 8 KB
每包时长8192 / 32000 = 0.256 秒 = 256ms
发送间隔50ms
实际传输速率8192 / 0.05 = 163840 bytes/s = 160 KB/s
实际音频速率32000 bytes/s = 31.25 KB/s
速率比160 / 31.25 = 5.12 倍
```
**结论**:我们的发送速度是实际音频速度的 5 倍,完全够用。
## 🔧 优化建议
### 方案1使用官方推荐的参数推荐
```javascript
sendAudioInChunks(audioData) {
const chunkSize = 3200 // 3.2KB(官方推荐)
const delay = 100 // 100ms官方推荐
for (let offset = 0; offset < totalSize; offset += chunkSize) {
const chunk = audioData.slice(offset, offset + chunkSize)
socketTask.send({ data: chunk })
await sleep(delay)
}
socketTask.send({ data: 'end' })
}
```
**优点**
- 完全符合官方建议
- 更接近实时音频流
- 延迟更低
### 方案2保持当前实现
```javascript
const chunkSize = 8192 // 8KB
const delay = 50 // 50ms
```
**优点**
- 发送更快
- 减少网络请求次数
- 仍在官方范围内1KB~16KB
## 🎯 服务器端需要的改动
### 当前代码
```python
async def feed_audio(self, data: bytes):
if self.recognition:
self.recognition.send_audio_frame(data)
```
**问题**:没有处理 "end" 标记
### 建议改动
```python
async def feed_audio(self, data: bytes):
# 检查是否为结束标记
if isinstance(data, str) and data == 'end':
# 停止 ASR触发最终识别
self.finalize_asr()
return
# 正常音频数据
if self.recognition:
self.recognition.send_audio_frame(data)
```
或者在 WebSocket 消息处理中:
```python
async def voice_call(websocket: WebSocket):
# ...
while True:
msg = await websocket.receive()
if "bytes" in msg and msg["bytes"] is not None:
await session.feed_audio(msg["bytes"])
elif "text" in msg and msg["text"]:
text = msg["text"].strip()
if text == "end":
session.finalize_asr() # 触发最终识别
# ...
```
## 📋 完整的工作流程
### 正确的流程
```
1. 客户端录音完成
2. 读取 PCM 文件260KB
3. 分片发送(每片 8KB间隔 50ms
├─ 发送片段 1 (8KB)
├─ 延迟 50ms
├─ 发送片段 2 (8KB)
├─ 延迟 50ms
├─ ...
└─ 发送片段 32 (6KB)
4. 发送 "end" 标记
5. 服务器接收每个片段
├─ 片段 1 → recognition.send_audio_frame()
├─ 片段 2 → recognition.send_audio_frame()
├─ ...
└─ 片段 32 → recognition.send_audio_frame()
6. 服务器收到 "end" 标记
7. 调用 recognition.stop()
8. ASR 完成识别,触发回调
9. LLM 生成回复
10. TTS 合成语音
11. 返回音频给客户端
```
## ✅ 验证清单
测试时检查以下日志:
### 客户端日志
```
✅ 📦 开始分片发送,总大小: 260000 bytes每片: 8192 bytes
✅ 📤 发送第 1 片,范围: 0-8192大小: 8192 bytes
✅ ✅ 第 1 片发送成功
✅ 📤 发送第 2 片,范围: 8192-16384大小: 8192 bytes
✅ ✅ 第 2 片发送成功
...
✅ ✅ 所有音频片段发送完成,共 32 片
✅ ✅ 发送结束标记
```
### 服务器日志
```
✅ ASR connection opened
✅ ASR event end=False sentence=...
✅ ASR event end=True sentence=...
✅ ASR complete
✅ LLM 生成回复
✅ TTS 合成语音
```
### 客户端收到响应
```
✅ 📋 收到控制消息, type: reply_text
✅ 🎵 收到音频数据流
✅ 📋 收到控制消息, type: reply_end
```
## 🎓 经验总结
### 关键教训
1. **RTFMRead The F***ing Manual**
- 官方文档明确说明了参数要求
- 必须仔细阅读文档
2. **理解模型特性**
- Paraformer-realtime-v2 是实时流式模型
- 必须按照流式方式喂数据
3. **参数范围很重要**
- 1KB~16KB 不是随便说的
- 超出范围会导致识别失败
### 最佳实践
1. **遵循官方建议**
- 每包 3200 字节100ms 音频)
- 延迟 100ms
2. **添加结束标记**
- 告诉服务器数据发送完毕
- 触发最终处理
3. **完善日志**
- 记录每个步骤
- 便于问题排查
## 🔗 参考文档
- [Paraformer 实时语音识别 Python SDK](https://help.aliyun.com/zh/model-studio/paraformer-real-time-speech-recognition-python-sdk)
- [实时语音识别](https://help.aliyun.com/zh/model-studio/real-time-speech-recognition)