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

7.3 KiB
Raw Permalink Blame History

官方文档分析和正确实现

📚 官方文档关键信息

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
  • 延迟 100ms0.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

🎓 经验总结

关键教训

  1. RTFMRead The Fing Manual*

    • 官方文档明确说明了参数要求
    • 必须仔细阅读文档
  2. 理解模型特性

    • Paraformer-realtime-v2 是实时流式模型
    • 必须按照流式方式喂数据
  3. 参数范围很重要

    • 1KB~16KB 不是随便说的
    • 超出范围会导致识别失败

最佳实践

  1. 遵循官方建议

    • 每包 3200 字节100ms 音频)
    • 延迟 100ms
  2. 添加结束标记

    • 告诉服务器数据发送完毕
    • 触发最终处理
  3. 完善日志

    • 记录每个步骤
    • 便于问题排查

🔗 参考文档