# 官方文档分析和正确实现 ## 📚 官方文档关键信息 ### 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) - ✅ 延迟 100ms(0.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. **RTFM(Read 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)