本文档分析 ChatBotPage 中 AI 返回的音频播放与动画 GIF 播放之间的一致性问题。
触发时机:收到 audio_response 消息的第一个音频块时
代码位置:
1994:2109:frontend/src/pages/ChatBotPage.tsx2070:2109:frontend/src/pages/ChatBotPage.tsx切换逻辑:
// 1. 获取最新情绪状态
const latestEmotion = useAppStore.getState().currentEmotion;
const emotionCategory = getEmotionCategory(latestEmotion);
// 2. 根据情绪类型设置 GIF
if (emotionCategory === 'positive') {
showPositiveGif();
// 设置定时器,播放固定时长后切换到 talking GIF
const duration = getEmotionGifDuration(currentAvatarId, emotionCategory);
emotionGifTimerRef.current = window.setTimeout(() => {
showTalkingGif();
}, duration);
} else if (emotionCategory === 'negative') {
showNegativeGif();
// 同上
} else {
showTalkingGif(); // 中性情绪直接显示 talking
}
触发时机:音频播放器 onEnd 回调
代码位置:1642:1656:frontend/src/pages/ChatBotPage.tsx
切换逻辑:
player.onEnd(() => {
// 清理情绪 GIF 切换定时器
if (emotionGifTimerRef.current) {
clearTimeout(emotionGifTimerRef.current);
emotionGifTimerRef.current = null;
}
// 恢复默认头像 GIF
showDefaultGif();
setState('idle');
reset();
});
触发时机:收到 emotion 消息时(可能在音频之前或之后到达)
代码位置:1959:1991:frontend/src/pages/ChatBotPage.tsx
切换逻辑:
// 只有在 playing 或 sending 状态时才更新 GIF
if (appState.currentState === 'playing' || appState.currentState === 'sending') {
// 根据情绪类型切换 GIF(逻辑同上)
}
问题描述:
getEmotionGifDuration)具体场景:
| 场景 | 情绪 GIF 时长 | 音频播放时长 | 实际表现 |
|---|---|---|---|
| 短音频 | 2000ms | 1000ms | ❌ 音频已结束,但情绪 GIF 还在播放,然后才切换到 talking,最后才切换到 default |
| 长音频 | 2000ms | 10000ms | ✅ 情绪 GIF → talking GIF → 音频结束 → default GIF |
| 超长音频 | 2000ms | 30000ms | ⚠️ 情绪 GIF → talking GIF(持续 28 秒)→ default GIF |
代码证据:
// 获取情绪 GIF 播放时长(毫秒)
const getEmotionGifDuration = (_avatarId: string | null, emotionCategory: 'positive' | 'negative'): number => {
// 固定时长,不依赖音频播放时长
return emotionCategory === 'positive' ? 2000 : 2000;
}
问题描述:
AudioPlayer.onEnd() 回调触发代码证据:
// 音频播放结束的判断逻辑
if (this.isStreamEnded && this.currentSources.length === 0 && this.audioQueue.length === 0) {
if (this.onEndCallback) {
this.onEndCallback(); // 异步回调
}
}
潜在问题:
markStreamEnd() 可能在音频队列清空前被调用问题描述:
emotion 消息和 audio_response 消息可能以任意顺序到达emotion 消息在 audio_response 之后到达,GIF 切换逻辑可能不一致代码证据:
// emotion 消息处理:只有在 playing 或 sending 状态时才更新 GIF
if (appState.currentState === 'playing' || appState.currentState === 'sending') {
// 更新 GIF
}
潜在问题:
emotion 消息在音频播放结束后到达,GIF 不会被更新emotion 消息在音频播放开始前到达,GIF 可能先显示情绪,然后被音频处理逻辑覆盖问题描述:
emotionGifTimerRef 的清理逻辑分散在多个地方代码证据:
// 停止音频播放
playerRef.current?.stop();
// 清理情绪 GIF 切换定时器
if (emotionGifTimerRef.current) {
clearTimeout(emotionGifTimerRef.current);
emotionGifTimerRef.current = null;
}
// 立即恢复默认头像 GIF
showDefaultGif();
潜在问题:
| 问题 | 影响程度 | 用户感知 |
|---|---|---|
| 情绪 GIF 与音频时长不匹配 | 中等 | 动画与音频不同步,可能看到情绪 GIF 在音频结束后还在播放 |
| 音频结束回调延迟 | 低 | 偶尔出现 GIF 切换延迟 |
| 情绪消息顺序不确定 | 低 | 极少数情况下 GIF 状态不正确 |
| 中断播放时的状态清理 | 低 | 极少数情况下 GIF 状态残留 |
| 问题 | 影响程度 | 维护成本 |
|---|---|---|
| 多处 GIF 切换逻辑 | 高 | 修改时需要同步更新多个位置 |
| 定时器管理分散 | 中 | 容易出现定时器泄漏或重复清理 |
| 状态同步依赖异步回调 | 中 | 调试困难,时序问题难以复现 |
| 功能 | 代码位置 | 说明 |
|---|---|---|
| 情绪 GIF 时长计算 | 108:137:frontend/src/pages/ChatBotPage.tsx |
固定时长,不依赖音频 |
| 音频播放开始 GIF 切换 | 1994:2109:frontend/src/pages/ChatBotPage.tsx |
首句消息处理 |
| 音频播放开始 GIF 切换 | 2070:2109:frontend/src/pages/ChatBotPage.tsx |
普通消息处理 |
| 音频播放结束 GIF 切换 | 1642:1656:frontend/src/pages/ChatBotPage.tsx |
onEnd 回调 |
| 情绪消息 GIF 切换 | 1959:1991:frontend/src/pages/ChatBotPage.tsx |
emotion 消息处理 |
| 用户中断播放清理 | 2224:2235:frontend/src/pages/ChatBotPage.tsx |
handleStartRecord |
| 音频播放器结束判断 | 164:199:frontend/src/utils/audioPlayer.ts |
流式播放结束检测 |
emotion 消息,等待 audio_response 消息到达后再统一处理useEffect 清理函数确保定时器不会泄漏当前实现中,音频播放与 GIF 动画的同步主要依赖固定时长的定时器和异步回调,存在以下核心问题:
这些问题虽然不会导致功能错误,但会影响用户体验的一致性。建议通过统一状态管理和音频时长感知的 GIF 切换来改进。