223 lines
5.0 KiB
Markdown
223 lines
5.0 KiB
Markdown
|
|
# "idle timeout" 问题分析
|
|||
|
|
|
|||
|
|
## 问题来源
|
|||
|
|
|
|||
|
|
错误来自 **FastAPI 服务器**(`192.168.1.141:30101`)的 `lover/routers/voice_call.py` 文件。
|
|||
|
|
|
|||
|
|
## 代码分析
|
|||
|
|
|
|||
|
|
### 1. idle timeout 的触发位置
|
|||
|
|
|
|||
|
|
在 `VoiceCallSession` 类中有两个超时检测:
|
|||
|
|
|
|||
|
|
#### 1.1 空闲超时(idle timeout)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def _idle_watchdog(self):
|
|||
|
|
timeout = settings.VOICE_CALL_IDLE_TIMEOUT or 0
|
|||
|
|
if timeout <= 0:
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
while True:
|
|||
|
|
await asyncio.sleep(5)
|
|||
|
|
if time.time() - self.last_activity > timeout:
|
|||
|
|
await self.send_signal({"type": "error", "msg": "idle timeout"})
|
|||
|
|
await self.close()
|
|||
|
|
break
|
|||
|
|
except asyncio.CancelledError:
|
|||
|
|
return
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**触发条件**:
|
|||
|
|
- 每 5 秒检查一次
|
|||
|
|
- 如果 `time.time() - self.last_activity > timeout`
|
|||
|
|
- 就发送 "idle timeout" 错误并关闭连接
|
|||
|
|
|
|||
|
|
#### 1.2 静默超时(silence timeout)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def _silence_watchdog(self):
|
|||
|
|
"""长时间静默时关闭会话,ASR 常驻不再因短静音 stop。"""
|
|||
|
|
try:
|
|||
|
|
while True:
|
|||
|
|
await asyncio.sleep(1.0)
|
|||
|
|
if time.time() - self.last_voice_activity > 60:
|
|||
|
|
logger.info("Long silence, closing session")
|
|||
|
|
await self.send_signal({"type": "error", "msg": "idle timeout"})
|
|||
|
|
await self.close()
|
|||
|
|
break
|
|||
|
|
except asyncio.CancelledError:
|
|||
|
|
return
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**触发条件**:
|
|||
|
|
- 每 1 秒检查一次
|
|||
|
|
- 如果 60 秒内没有语音活动
|
|||
|
|
- 就发送 "idle timeout" 错误并关闭连接
|
|||
|
|
|
|||
|
|
### 2. last_activity 的更新
|
|||
|
|
|
|||
|
|
`last_activity` 在以下情况更新:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def _touch(self):
|
|||
|
|
self.last_activity = time.time()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
调用 `_touch()` 的地方:
|
|||
|
|
1. `feed_audio()` - 接收音频数据时
|
|||
|
|
2. `send_signal()` - 发送信号时
|
|||
|
|
3. `_synthesize_stream()` - 发送 TTS 音频时
|
|||
|
|
|
|||
|
|
### 3. 问题分析
|
|||
|
|
|
|||
|
|
从你的日志看:
|
|||
|
|
```
|
|||
|
|
✅ 录音文件发送成功
|
|||
|
|
❌ WebSocket 关闭, code: 1000
|
|||
|
|
{"type":"error","msg":"idle timeout"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**可能的原因**:
|
|||
|
|
|
|||
|
|
#### 原因1:服务器配置的超时时间太短
|
|||
|
|
|
|||
|
|
检查 `lover/config.py` 或 `.env` 文件中的配置:
|
|||
|
|
```python
|
|||
|
|
VOICE_CALL_IDLE_TIMEOUT = ? # 这个值可能太小
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 原因2:服务器处理音频时间过长
|
|||
|
|
|
|||
|
|
服务器接收音频后需要:
|
|||
|
|
1. ASR 识别(~1-2秒)
|
|||
|
|
2. LLM 生成回复(~2-5秒)
|
|||
|
|
3. TTS 合成语音(~1-2秒)
|
|||
|
|
|
|||
|
|
如果 `VOICE_CALL_IDLE_TIMEOUT` 设置为 5 秒,而处理需要 6 秒,就会超时。
|
|||
|
|
|
|||
|
|
#### 原因3:客户端发送的是完整文件,不是流式数据
|
|||
|
|
|
|||
|
|
当前客户端实现:
|
|||
|
|
- 录音完成后一次性发送整个 MP3 文件
|
|||
|
|
- 服务器期望的是 PCM 流式数据
|
|||
|
|
|
|||
|
|
服务器代码中:
|
|||
|
|
```python
|
|||
|
|
self.recognition = Recognition(
|
|||
|
|
model="paraformer-realtime-v2",
|
|||
|
|
format="pcm", # 期望 PCM 格式
|
|||
|
|
sample_rate=16000,
|
|||
|
|
...
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
但客户端发送的是:
|
|||
|
|
```javascript
|
|||
|
|
format: 'mp3' // 发送的是 MP3
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**这是主要问题!**
|
|||
|
|
|
|||
|
|
## 解决方案
|
|||
|
|
|
|||
|
|
### 方案1:修改服务器超时配置(临时方案)
|
|||
|
|
|
|||
|
|
在 `lover/.env` 或 `lover/config.py` 中增加超时时间:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
VOICE_CALL_IDLE_TIMEOUT = 30 # 从默认值增加到 30 秒
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 方案2:客户端改用 PCM 格式(推荐)
|
|||
|
|
|
|||
|
|
修改客户端录音配置:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const recorderOptions = {
|
|||
|
|
duration: 600000,
|
|||
|
|
sampleRate: 16000,
|
|||
|
|
numberOfChannels: 1,
|
|||
|
|
format: 'pcm', // 改为 PCM
|
|||
|
|
audioSource: 'auto'
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**但是**:PCM 文件很大,一次性发送会很慢。
|
|||
|
|
|
|||
|
|
### 方案3:修改服务器支持 MP3 格式(最佳方案)
|
|||
|
|
|
|||
|
|
修改 `lover/routers/voice_call.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def feed_audio(self, data: bytes):
|
|||
|
|
# 检测音频格式
|
|||
|
|
if self._is_mp3(data):
|
|||
|
|
# 转换 MP3 到 PCM
|
|||
|
|
pcm_data = self._convert_mp3_to_pcm(data)
|
|||
|
|
data = pcm_data
|
|||
|
|
|
|||
|
|
# 原有逻辑
|
|||
|
|
if self.recognition:
|
|||
|
|
self.recognition.send_audio_frame(data)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 方案4:使用流式录音(理想方案)
|
|||
|
|
|
|||
|
|
客户端使用实时音频帧:
|
|||
|
|
```javascript
|
|||
|
|
format: 'pcm',
|
|||
|
|
frameSize: 5, // 启用 onFrameRecorded
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
但这需要 App 端支持 `onFrameRecorded`。
|
|||
|
|
|
|||
|
|
## 当前最快的解决方案
|
|||
|
|
|
|||
|
|
### 步骤1:增加服务器超时时间
|
|||
|
|
|
|||
|
|
编辑 `lover/.env` 文件:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 在文件中添加或修改
|
|||
|
|
VOICE_CALL_IDLE_TIMEOUT=30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤2:重启 FastAPI 服务器
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 在服务器上
|
|||
|
|
cd /path/to/lover
|
|||
|
|
# 停止旧进程
|
|||
|
|
pkill -f "uvicorn.*main:app"
|
|||
|
|
# 启动新进程
|
|||
|
|
uvicorn main:app --host 0.0.0.0 --port 30101 --reload
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤3:测试
|
|||
|
|
|
|||
|
|
重新测试语音通话,看是否还有 "idle timeout" 错误。
|
|||
|
|
|
|||
|
|
## 长期优化建议
|
|||
|
|
|
|||
|
|
1. **服务器端支持 MP3 输入**
|
|||
|
|
- 添加音频格式检测
|
|||
|
|
- 自动转换 MP3 到 PCM
|
|||
|
|
|
|||
|
|
2. **优化处理流程**
|
|||
|
|
- 使用更快的 ASR 模型
|
|||
|
|
- 优化 LLM 调用
|
|||
|
|
- 使用流式 TTS
|
|||
|
|
|
|||
|
|
3. **客户端使用流式录音**
|
|||
|
|
- 实现 PCM 实时传输
|
|||
|
|
- 降低延迟
|
|||
|
|
|
|||
|
|
## 配置文件位置
|
|||
|
|
|
|||
|
|
需要检查的文件:
|
|||
|
|
- `lover/.env` - 环境变量配置
|
|||
|
|
- `lover/config.py` - 配置类定义
|
|||
|
|
|
|||
|
|
查找 `VOICE_CALL_IDLE_TIMEOUT` 的当前值。
|