Files
Docs/DevLogs/Day14.md
2025-12-31 16:18:28 +08:00

9.8 KiB
Raw Blame History

Day 14: WebSocket TTS 调试与项目清理

日期2025-12-12
目标:解决 TTS 断续问题,清理项目冗余文件


📅 工作摘要

1. 项目清理

删除的文件(共 19 个):

类型 文件
HTTP TTS 相关 http_tts_stream.cpp, http_tts_stream.h, http_client.cpp, http_client.h
测试脚本 build_test.sh, build_test_camera.sh, build_test_imu.sh
调试脚本 debug_network_libs.sh, find_libs_v3.sh, setup_mic.sh, fix_speaker.sh
测试源码 main_test.cpp, test_audio.cpp, test_camera.cpp, test_gpio.cpp, test_imu.cpp, test_network.cpp, test_udp_only.cpp
其他 Makefile_test, build_custom.sh, build_phase2.sh, build_phase3.sh, test_mic.sh, test/ 目录

清理后目录结构

avaota_app_demo/
├── build_main.sh       # 主编译脚本
├── README.md
├── MUSL_COMPILE.md
└── src/
    ├── Makefile        # 主 Makefile
    ├── main.cpp        # 主程序
    ├── audio/          # 音频模块
    ├── camera/         # 摄像头模块
    ├── imu/            # IMU 模块
    ├── network/        # 网络模块 (ws_client, udp_sender)
    └── utils/          # 日志工具

2. 恢复 WebSocket TTS

原因HTTP TTS (/stream.wav) 方案虽然存在,但之前 WebSocket TTS 已验证可以播放声音,只是存在断续问题。重新切换回 WebSocket TTS 进行优化。

代码变更

  • main.cpp: 移除 http_tts_player_thread(),恢复 audio_capture_thread() 中的 WebSocket TTS 播放逻辑
  • Makefile: 移除 http_tts_stream.cpphttp_client.cpp

3. 增大 TTS 预缓冲区

问题Omni API 响应慢,每 1-2 秒才返回一批音频数据导致播放缓冲区频繁欠载underrun

修改 (main.cpp):

// 原设置0.5 秒预缓冲)
const size_t PRE_BUFFER_FRAMES = 8000;   // 0.5s
const size_t MIN_PLAY_FRAMES = 3200;     // 0.2s

// 新设置2 秒预缓冲)
const size_t PRE_BUFFER_FRAMES = 32000;  // 2s
const size_t MIN_PLAY_FRAMES = 4800;     // 0.3s

4. 日志优化

更新日志中的设备名称:

  • ESP32设备 (更通用)

5. 服务器端导航器预初始化

问题:客户端连接后才开始初始化导航器,导致首次连接时服务器卡顿一段时间。

修改 (app_main.py):在服务器启动时预初始化所有导航组件:

# 在服务器启动时预初始化(避免客户端连接后延迟)
print("[NAVIGATION] 预初始化导航器...")
blind_path_navigator = BlindPathNavigator(yolo_seg_model, obstacle_detector)
cross_street_navigator = CrossStreetNavigator(yolo_seg_model, obstacle_detector)
orchestrator = NavigationMaster(blind_path_navigator, cross_street_navigator)
print("[NAV MASTER] 统领状态机已预初始化")

效果:客户端连接时无需等待导航器加载,响应更快。


📊 测试结果

2 秒预缓冲测试

[AUD-PLAY] Pre-buffer full (33920 frames), starting playback
[WARN] [AudioPlayer] Underrun occurred, recovering...
[WARN] [AudioPlayer] Underrun occurred, recovering...
...(仍有断续)

结论2 秒预缓冲仍不足以解决问题Omni API 发送间隔过长(~2 秒/批),播放速度远快于接收速度。


🔴 遗留问题

TTS 语音断断续续

根本原因

  • Omni API 生成音频速度 << 播放速度
  • 每 1-2 秒发送一批 10KB 音频
  • 即使 2 秒预缓冲,也会在长句子后半段出现 underrun

可能的解决方案

方案 描述 工作量 效果
继续增大预缓冲 3-4 秒预缓冲 5 分钟 可能有效
服务器预缓冲 服务端收集完整 TTS 后再发送 2 小时 最佳
静音填充 缓冲区空时插入静音,防止卡顿 1 小时 中等
变速播放 缓冲区低时降速播放0.95x 3 小时 高级

推荐下一步

  1. 先尝试增大预缓冲到 3-4 秒
  2. 如仍不行,考虑服务器端预缓冲方案

5. 服务器端 TTS 发送优化

发现问题

通过代码分析发现,audio_stream.py 中的 broadcast_pcm16_realtime 函数存在设计问题:

# 原来的代码HTTP 20ms 节拍循环会阻塞整个函数
async def broadcast_pcm16_realtime(pcm16: bytes):
    # ... WebSocket 发送 ...
    
    # HTTP 节拍广播(阻塞!)
    while off < len(pcm16):
        # 每 20ms 发送一小块
        await asyncio.sleep(next_tick - now)  # 这会阻塞整个函数

问题:虽然 WebSocket 发送在前面,但 HTTP 节拍循环会阻塞函数返回。每 1KB 数据需要约 62ms 才能完成,导致下一个 Omni 音频块被延迟处理。

修复:将 HTTP 节拍广播改为后台任务

# 修改后WebSocket 立即发送HTTP 在后台执行
async def broadcast_pcm16_realtime(pcm16: bytes):
    # ... WebSocket 发送 ...
    
    # Day 14 优化HTTP 广播放到后台任务
    if stream_clients:
        asyncio.create_task(_http_pacing_broadcast(pcm16))
    # 函数立即返回,不阻塞

async def _http_pacing_broadcast(pcm16: bytes):
    """独立后台任务处理 HTTP 节拍广播"""
    # 原来的 20ms 节拍循环代码

预期效果

  • WebSocket 发送后立即返回处理下一个 Omni 音频块
  • TTS 传输间隔完全由 Omni API 生成速度决定
  • 无额外延迟

📝 代码变更汇总

文件 变更
main.cpp 移除 HTTP TTS 代码,增大预缓冲到 2 秒
Makefile 移除 HTTP 相关源文件
network/http_* 删除
测试文件 删除 19 个文件
audio_stream.py (服务器) HTTP 节拍广播改为后台任务WebSocket 立即返回

🔧 技术研究:实时语音传输最佳方案

协议对比

协议 适用场景 延迟
WebRTC P2P 实时通话 <100ms
WebSocket 服务器中转、AI 语音 100-500ms
HTTP Chunked 简单流式传输 >500ms

关键技术Jitter Buffer

专业音频系统使用动态抖动缓冲

  1. 预缓冲启动(等待足够数据)
  2. 动态调整缓冲区大小
  3. 静音填充(缓冲区空时)


🎉 下午进展TTS 流畅 + 导航卡顿问题

6. TTS 播放成功

服务器端修复生效重启服务器后测试TTS 播放已流畅!

修复方法总结

问题 原因 修复
TTS 断续 broadcast_pcm16_realtime 中 HTTP 20ms 节拍循环阻塞 WebSocket 发送 将 HTTP 广播改为后台任务 asyncio.create_task(_http_pacing_broadcast())

关键代码变更 (audio_stream.py):

# 修改前HTTP 节拍循环阻塞函数
async def broadcast_pcm16_realtime(pcm16: bytes):
    await ws.send_bytes(pcm16k)  # WebSocket 发送
    while off < len(pcm16):      # HTTP 节拍循环(阻塞!)
        await asyncio.sleep(...)

# 修改后WebSocket 发送后立即返回
async def broadcast_pcm16_realtime(pcm16: bytes):
    await ws.send_bytes(pcm16k)  # WebSocket 发送
    if stream_clients:
        asyncio.create_task(_http_pacing_broadcast(pcm16))  # 后台执行
    # 函数立即返回,不阻塞

7. 导航模式卡顿问题 🔴 (新发现)

现象:发出"开始导航"指令后,服务器响应"盲道导航已启动"可视化界面立即卡住FPS 从 10.0 暴跌到 0.2。

根因分析

服务器端 workflow_blindpath.py 存在 YOLO 检测间隔参数未使用 的 bug

# 第253-256行定义了间隔参数
self.BLINDPATH_DETECTION_INTERVAL = 8  # 每8帧检测一次
self.last_blindpath_detection_frame = 0  # 但这个变量从未被使用!

# 第424行实际上每帧都执行 YOLO 推理
blind_path_mask, crosswalk_mask = self._detect_path_and_crosswalk(image)  # 无间隔!

对比障碍物检测(正确实现):

# 第454行障碍物检测正确使用了间隔
if self.frame_counter % self.OBSTACLE_DETECTION_INTERVAL == 0:
    detected_obstacles = self._detect_obstacles(image, blind_path_mask)

推荐修复方案

修改 workflow_blindpath.py 第 424 行,添加帧间隔检查:

# 修改后:每 8 帧执行一次 YOLO 推理
if self.frame_counter % self.BLINDPATH_DETECTION_INTERVAL == 0:
    blind_path_mask, crosswalk_mask = self._detect_path_and_crosswalk(image)
    self.last_blindpath_mask = blind_path_mask
    self.last_crosswalk_mask = crosswalk_mask
else:
    blind_path_mask = self.last_blindpath_mask
    crosswalk_mask = self.last_crosswalk_mask

预期效果YOLO 推理从每帧 1 次降为每 8 帧 1 次,处理负载降低 8 倍。


📝 Day 14 完整代码变更汇总

文件 变更
main.cpp 移除 HTTP TTS 代码,增大预缓冲到 2 秒
Makefile 移除 HTTP 相关源文件
network/http_* 删除
测试文件 删除 19 个文件
audio_stream.py (服务器) HTTP 节拍广播改为后台任务WebSocket 立即返回

📂 待下次会话处理

必须修复

  1. 导航卡顿问题
    • 修改 workflow_blindpath.py 第 424 行
    • 添加 BLINDPATH_DETECTION_INTERVAL 帧间隔检查
    • 测试验证导航模式 FPS 恢复正常

已完成验证

  • 摄像头 WebSocket
  • IMU UDP
  • 麦克风采集
  • TTS 播放(已流畅)
  • ⚠️ 导航模式(待修复 YOLO 间隔问题)