7.3 KiB
7.3 KiB
官方文档分析和正确实现
📚 官方文档关键信息
1. send_audio_frame 的正确用法
根据官方文档:
每次推送的音频流不宜过大或过小,建议每包音频时长为100ms左右,大小在1KB~16KB之间。
2. 官方示例代码
识别本地文件的正确方式
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)
- ✅ 延迟 100ms(0.1秒)
- ✅ 循环发送,模拟实时流
识别麦克风的正确方式
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 字节
- ✅ 实时发送,无需延迟(因为是实时流)
🔍 我们的问题
当前实现(错误)
# 服务器端 lover/routers/voice_call.py
async def feed_audio(self, data: bytes):
if self.recognition:
self.recognition.send_audio_frame(data) # 直接发送整个文件
// 客户端
fs.readFile({
filePath: res.tempFilePath,
success: (fileRes) => {
// 一次性发送 260KB ❌
socketTask.send({ data: fileRes.data })
}
})
问题:
- ❌ 客户端一次性发送 260KB
- ❌ 服务器直接喂给 ASR
- ❌ 不符合官方要求(1KB~16KB)
正确实现(已修复)
// 客户端分片发送
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:使用官方推荐的参数(推荐)
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:保持当前实现
const chunkSize = 8192 // 8KB
const delay = 50 // 50ms
优点:
- 发送更快
- 减少网络请求次数
- 仍在官方范围内(1KB~16KB)
🎯 服务器端需要的改动
当前代码
async def feed_audio(self, data: bytes):
if self.recognition:
self.recognition.send_audio_frame(data)
问题:没有处理 "end" 标记
建议改动
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 消息处理中:
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
🎓 经验总结
关键教训
-
RTFM(Read The Fing Manual)*
- 官方文档明确说明了参数要求
- 必须仔细阅读文档
-
理解模型特性
- Paraformer-realtime-v2 是实时流式模型
- 必须按照流式方式喂数据
-
参数范围很重要
- 1KB~16KB 不是随便说的
- 超出范围会导致识别失败
最佳实践
-
遵循官方建议
- 每包 3200 字节(100ms 音频)
- 延迟 100ms
-
添加结束标记
- 告诉服务器数据发送完毕
- 触发最终处理
-
完善日志
- 记录每个步骤
- 便于问题排查