Init: 导入开发日志和项目文档

This commit is contained in:
Kevin Wong
2025-12-31 16:18:28 +08:00
commit bcebc7e316
32 changed files with 9208 additions and 0 deletions

219
DevLogs/Day13.md Normal file
View File

@@ -0,0 +1,219 @@
# Day 13: TTS 播放优化与事件循环阻塞修复
**日期**2025-12-11
**目标**:解决 TTS 音频播放断续问题、修复服务器退出问题
---
## 📅 工作摘要
### 1. 核心问题发现 ✅
**症状**TTS 音频播放断断续续,每隔几秒才播放几个字
**根因**`omni_client.py` 中使用**同步迭代器**处理 Omni API 响应,阻塞了整个 asyncio 事件循环
```python
# 问题代码(已修复)
async def stream_chat(...):
completion = client.chat.completions.create(stream=True, ...)
for chunk in completion: # ← 同步迭代,阻塞事件循环!
yield OmniStreamPiece(...)
```
### 2. Omni 客户端异步化 ✅
**修复**:使用 `threading.Thread` + `asyncio.Queue` 解耦同步 API 调用
```python
# 修复后的代码
async def stream_chat(...):
queue = asyncio.Queue()
def _sync_stream(): # 在独立线程中运行
for chunk in completion:
loop.call_soon_threadsafe(queue.put_nowait, OmniStreamPiece(...))
thread = threading.Thread(target=_sync_stream, daemon=True)
thread.start()
while True:
item = await queue.get() # 非阻塞等待
if item is None: break
yield item
```
### 3. 客户端 TTS 预缓冲机制 ✅
**问题**即使服务器端修复后TTS 仍断断续续Underrun
**原因**Omni API 每 ~2 秒返回一个音频块,客户端播放速度快于接收速度
**解决方案**:在 `main.cpp` 中实现预缓冲机制
| 参数 | 值 | 说明 |
|------|---|------|
| PRE_BUFFER_FRAMES | 16000 | 预缓冲 1 秒音频再播放 |
| MIN_PLAY_FRAMES | 8000 | 最小播放阈值 0.5 秒 |
| 批次大小 | 1600 帧 | 每次播放 100ms |
```cpp
// 客户端预缓冲逻辑
static std::vector<int16_t> tts_buffer;
static bool is_buffering = true;
// 接收时:追加到缓冲区
tts_buffer.insert(tts_buffer.end(), samples, samples + frames);
// 播放时:积累足够再开始
if (tts_buffer.size() >= PRE_BUFFER_FRAMES) {
is_buffering = false; // 开始播放
}
```
### 4. 服务器退出修复 ✅
**问题**Ctrl+C 后进程挂起,不返回 shell
**修复**:在 lifespan 中捕获 CancelledError并启动强制退出线程
```python
@asynccontextmanager
async def lifespan(app):
# ... 启动逻辑 ...
try:
yield
except asyncio.CancelledError:
pass # Ctrl+C 正常行为
finally:
print("[LIFESPAN] 应用关闭完成")
# 强制退出线程
def _force_exit():
time.sleep(0.5)
os._exit(0)
threading.Thread(target=_force_exit, daemon=True).start()
```
### 5. 其他修复 ✅
- **TTS 上采样**8kHz → 16kHz`audio_stream.py`
- **TTS 缓存机制**WebSocket 断开时缓存音频,重连后发送
- **WebSocket 引用保护**:防止模型加载期间 ws 被清空
---
## 📝 代码变更汇总
| 文件 | 变更内容 |
|------|---------|
| `omni_client.py` | 使用线程+队列解耦同步 API 调用 |
| `main.cpp` | 添加 TTS 预缓冲机制 |
| `app_main.py` | lifespan 捕获 CancelledError + 强制退出线程 |
| `audio_stream.py` | TTS 上采样、缓存机制、WebSocket 引用保护 |
---
## 🔴 遗留问题
### 1. TTS 音频仍有轻微断续
**现象**
- 预缓冲后首次播放流畅
- 后续仍偶发 `Underrun occurred`
**根因**
- Omni API 流式响应速度慢(每 ~2 秒一个块)
- 客户端播放速度(实时)> 服务器推送速度
**客户端日志示例**
```
[AUD-PLAY] Pre-buffer full, starting playback
[AUD-PLAY] Played 4000 frames, remaining: 14399
[AudioPlayer] Underrun occurred, recovering...
```
### 2. WebSocket 仍偶发断开
**现象**:长时间对话后出现 `websocket.send after websocket.close`
**原因**模型加载或其他长时间操作期间WebSocket 超时断开
---
## 🚀 改进方向
### 方案 A切换到 HTTP `/stream.wav` 模式(推荐)
服务器**已支持** `/stream.wav` 端点,参考 ESP32S3 的成功实现:
```cpp
// ESP32 的 HTTP 流式播放(已验证可用)
WiFiClient cli;
cli.connect(SERVER_HOST, SERVER_PORT);
cli.print("GET /stream.wav HTTP/1.1\r\n...");
// 持续读取 WAV 数据并播放
```
**优势**
- HTTP 流比 WebSocket 更稳定
- 服务器已实现,只需修改客户端
- ESP32 方案成熟,可直接参考
### 方案 B增大 ALSA 缓冲区
当前客户端 ALSA 缓冲区较小1280 帧 = 80ms可尝试增大
```cpp
// 增大 ALSA 缓冲区
snd_pcm_hw_params_set_buffer_size(handle, params, 16000); // 1秒
snd_pcm_hw_params_set_period_size(handle, params, 4000); // 250ms
```
### 方案 C服务器端预生成完整响应
在 Omni API 调用前等待完整响应再发送,牺牲首次延迟换取流畅播放:
```python
# 收集完整响应再发送
full_audio = b""
async for piece in stream_chat(...):
full_audio += piece.audio_bytes
# 一次性发送
await send_tts(full_audio)
```
---
## 🔜 下一步开发
1. **实现 HTTP `/stream.wav` 客户端**~150 行 C++ 代码)
- 添加 HTTP 客户端线程
- 解析 WAV 头
- 读取 chunked 数据并播放
2. **测试验证**
- 验证 HTTP 模式播放流畅度
- 对比 WebSocket 模式性能
3. **摄像头 VBV 缓冲区优化**
- 继续调整编码参数
- 减少 `VBV buffer full` 警告
---
## 📊 测试结果
| 测试项 | 修复前 | 修复后 |
|--------|--------|--------|
| TTS 块间隔 | 5-15 秒 | ~2 秒 |
| 首次播放延迟 | 立即 | ~1 秒(预缓冲) |
| Underrun 频率 | 每块一次 | 偶发 |
| 服务器退出 | 挂起 | 正常返回 |
---
## 📂 参考文件
- ESP32S3 固件:`AvaotaF1/ESP32S3/compile.ino`HTTP `/stream.wav` 播放实现)
- 服务器端点:`audio_stream.py``/stream.wav`
- 客户端代码:`avaota_app_demo/src/main.cpp`