zlwu 1 vuosi sitten
vanhempi
commit
21bbb4dd4a

+ 7 - 5
backend/doubao_api/realtime_dialog_client.py

@@ -107,7 +107,7 @@ class RealtimeDialogClient:
         session_config = {
             "asr": {
                 "extra": {
-                    "end_smooth_window_ms": 600,  # 缩短到600ms,让VAD更快检测结束
+                    "end_smooth_window_ms": 1200,  # 增加到1.2秒,避免中间停顿误判
                 },
             },
             "tts": {
@@ -120,8 +120,8 @@ class RealtimeDialogClient:
             },
             "dialog": {
                 "bot_name": "豆包",
-                "system_role": "你是一位英语口语陪练老师,帮助用户提高英语口语水平。请用简洁自然的英语回复,语速适中。",
-                "speaking_style": "你的说话风格友好亲切,鼓励用户多说英语。",
+                "system_role": "你是一位英语口语陪练老师,帮助用户提高英语口语水平。请用简洁自然的英语回复,每次回复不超过2-3句话。",
+                "speaking_style": "你的说话风格友好亲切,回复简短有力,鼓励用户多说英语。",
                 "location": {
                     "city": "北京",
                 },
@@ -252,7 +252,8 @@ class RealtimeDialogClient:
                     asr_text = results[0].get('text', '')
                 is_final = results[0].get('is_final', False) if results else False
                 logger.info(f"📥 [ASR识别] {'[最终]' if is_final else '[中间]'} Text: {asr_text}")
-                return None
+                # 返回ASR文本给websocket_handler
+                return ('asr_text', asr_text)
             
             # Event 459: ASR最终结果(用户说话结束)
             elif event == 459 and isinstance(payload_msg, dict):
@@ -268,7 +269,8 @@ class RealtimeDialogClient:
             elif event == 550 and isinstance(payload_msg, dict):
                 content = payload_msg.get('content', '')
                 logger.debug(f"💬 [LLM文本片段] {content}")
-                return None
+                # 返回一个特殊标记,让websocket_handler知道这是LLM文本
+                return ('llm_text', content)
             
             # Event 359: 无内容
             elif event == 359:

+ 33 - 6
backend/websocket_handler.py

@@ -36,6 +36,7 @@ class StreamingSession:
         
         # 文本收集(用于日志)
         self.llm_text_chunks = []  # 收集LLM流式文本
+        self.asr_text = ''  # 最后一次ASR识别结果
         
         # 对话轮次控制
         self.response_ended = False  # 标记当前轮次是否已结束(避免重复发送结束消息)
@@ -162,7 +163,12 @@ class StreamingSession:
                         if self.audio_sent_bytes > 0:
                             # 计算本轮对话的延迟
                             response_latency = int((time.time() - self.start_time) * 1000) if self.start_time > 0 else 0
-                            logger.info(f"🔊 [AI回复完成] 音频大小: {self.audio_sent_bytes} bytes ({self.audio_sent_bytes/1024:.1f}KB), 延迟: {response_latency}ms")
+                            
+                            # 打印完整的LLM回复文本
+                            full_llm_text = ''.join(self.llm_text_chunks)
+                            logger.info(f"🔊 [AI回复完成] 音频: {self.audio_sent_bytes / 1024:.1f}KB, 延迟: {response_latency}ms")
+                            if full_llm_text:
+                                logger.info(f"💬 [完整回复] {full_llm_text}")
                             
                             await self.send_control_message({
                                 'type': 'ai_response_end',
@@ -208,6 +214,15 @@ class StreamingSession:
                         self.audio_sent_bytes = 0
                         # 不break,继续外层循环等待下一次录音
                         continue
+                    elif isinstance(audio_data, tuple):
+                        # 处理文本数据
+                        if audio_data[0] == 'llm_text':
+                            # LLM文本片段
+                            self.llm_text_chunks.append(audio_data[1])
+                        elif audio_data[0] == 'asr_text':
+                            # ASR识别文本(保存最新的)
+                            self.asr_text = audio_data[1]
+                        continue
                     elif audio_data:
                         # bytes表示音频数据
                         # 发送AI响应开始消息(仅第一次)
@@ -235,6 +250,10 @@ class StreamingSession:
                         self.audio_sent_bytes += len(audio_data)
                         performance_monitor.record_audio_sent(len(audio_data))
                         logger.debug(f"收到豆包音频: {len(audio_data)} bytes, 总计: {self.audio_sent_bytes} bytes")
+                        
+                        # 监控输出是否过长(超过100KB警告)
+                        if self.audio_sent_bytes > 100 * 1024:
+                            logger.warning(f"⚠️ AI回复过长: {self.audio_sent_bytes / 1024:.1f}KB,可能需要优化prompt")
                     else:
                         # None表示其他事件(文本、状态等),继续等待
                         logger.debug("收到豆包事件,继续等待音频")
@@ -372,6 +391,7 @@ class StreamingSession:
         self.audio_received_bytes = 0
         self.audio_sent_bytes = 0
         self.llm_text_chunks = []  # 重置文本收集
+        self.asr_text = ''  # 重置ASR文本
         self.response_ended = False  # 重置结束标志
         
         logger.info(f"🎙️ [开始录音] 新一轮对话")
@@ -400,17 +420,24 @@ class StreamingSession:
         duration = int((time.time() - self.start_time) * 1000) if self.start_time > 0 else 0
         
         # 记录录音完成信息
-        logger.info(f"🎤 [录音完成] 时长: {duration}ms, 音频大小: {self.audio_received_bytes} bytes ({self.audio_received_bytes/1024:.1f}KB)")
+        logger.info(f"🎤 [录音完成] 时长: {duration}ms, 音频: {self.audio_received_bytes / 1024:.1f}KB")
+        
+        # 等待一下让ASR完成识别
+        await asyncio.sleep(0.3)
+        
+        # 打印ASR识别的完整文本
+        if self.asr_text:
+            logger.info(f"📝 [用户说] {self.asr_text}")
         
         # 等待队列中的音频数据全部发送完成
         await asyncio.sleep(0.15)
         
-        # 发送较短的静音帧让豆包VAD检测到结束
-        # 豆包配置了 end_smooth_window_ms: 600,需要600ms静音
-        # 发送700ms静音确保触发(16kHz 16bit = 32000 bytes/s)
+        # 发送静音帧让豆包VAD检测到结束
+        # 豆包配置了 end_smooth_window_ms: 1200,需要1.2秒静音
+        # 发送1300ms静音确保触发(16kHz 16bit = 32000 bytes/s)
         if self.doubao_client:
             try:
-                silence_duration_ms = 700
+                silence_duration_ms = 1300
                 silence_bytes = int(16000 * 2 * silence_duration_ms / 1000)
                 silence_data = b'\x00' * silence_bytes
                 

+ 32 - 3
frontend/src/components/MainPage.tsx

@@ -95,13 +95,18 @@ export default function MainPage() {
 
         const playerConfig = {
           onPlay: () => {
-            console.log('开始播放')
+            console.log('🔊 开始播放')
             startPlaying()
+            // 确保动画状态是SPEAKING
+            setAnimationState('SPEAKING')
           },
           onEnded: () => {
-            console.log('播放结束')
+            console.log('✅ 播放结束')
             stopPlaying()
             
+            // 播放完成后切换到IDLE状态
+            setAnimationState('IDLE')
+            
             // 计算延迟并记录
             if (latencyStartTime.current > 0) {
               const latency = Date.now() - latencyStartTime.current
@@ -181,6 +186,15 @@ export default function MainPage() {
         break
 
       case 'ai_response_start':
+        // 检查是否还在录音状态
+        const currentState = useAppStore.getState()
+        if (currentState.isRecording) {
+          console.log('⚠️ 录音中收到AI回复,忽略(用户可能在继续说话)')
+          // 清理播放器,避免音频块堆积
+          playerRef.current?.clear()
+          return
+        }
+        
         console.log('🎬 AI开始回复 - 清理播放器状态')
         // 清理播放器状态,准备接收新的音频
         playerRef.current?.clear()
@@ -204,7 +218,15 @@ export default function MainPage() {
         } else if (newState === 'speaking') {
           setAnimationState('SPEAKING')
         } else if (newState === 'idle') {
-          setAnimationState('IDLE')
+          // 如果是从speaking切换到idle,不要立即切换
+          // 让播放器的onEnded回调来控制最终状态
+          const previousState = message.payload?.previousState
+          if (previousState === 'speaking') {
+            console.log('忽略过早的idle状态,等待播放完成')
+            // 不切换状态,保持SPEAKING直到播放完成
+          } else {
+            setAnimationState('IDLE')
+          }
         }
         break
 
@@ -233,6 +255,13 @@ export default function MainPage() {
 
   // 处理音频数据
   const handleAudioData = (audioData: ArrayBuffer) => {
+    // 检查是否还在录音状态
+    const currentState = useAppStore.getState()
+    if (currentState.isRecording) {
+      console.log('⚠️ 录音中收到音频数据,忽略(豆包中间回复)')
+      return
+    }
+    
     console.log('收到音频数据:', audioData.byteLength, 'bytes')
     performanceMonitor.recordAudioChunkReceived()
     playerRef.current?.appendAudioChunk(audioData)

+ 6 - 5
frontend/src/utils/audioPlayerFallback.ts

@@ -208,8 +208,9 @@ export class FallbackAudioPlayer {
     // 创建Audio元素
     this.audio = new Audio(url)
     
-    this.audio.onplay = () => {
-      console.log('音频开始播放')
+    // 监听播放开始事件(使用onplaying而不是onplay,因为onplaying是真正开始播放)
+    this.audio.onplaying = () => {
+      console.log('🎵 音频开始播放')
       this.config.onPlay?.()
     }
     
@@ -264,8 +265,8 @@ export class FallbackAudioPlayer {
         const audio = new Audio(url)
         
         // 设置事件处理器
-        audio.onplay = () => {
-          console.log(`降级播放成功: ${mimeType}`)
+        audio.onplaying = () => {
+          console.log(`🎵 降级播放开始: ${mimeType}`)
           this.config.onPlay?.()
         }
         
@@ -283,7 +284,7 @@ export class FallbackAudioPlayer {
         
         await audio.play()
         this.audio = audio
-        console.log(`降级播放成功: ${mimeType}`)
+        console.log(`降级播放尝试: ${mimeType}`)
         return // 成功播放,退出
         
       } catch (error) {