Sfoglia il codice sorgente

语音识别组件:
1、开启和关闭加载中加入加载动效
2、在结束语音识别前需要再次调取获取识别结果,保证不漏识别

liyanbo 1 mese fa
parent
commit
43fbe4492b
1 ha cambiato i file con 96 aggiunte e 12 eliminazioni
  1. 96 12
      src/components/ai/voice/VoiceInput_Api.vue

+ 96 - 12
src/components/ai/voice/VoiceInput_Api.vue

@@ -3,7 +3,8 @@
     <button
         @click="toggleSpeechInput"
         class="speech-btn"
-        :class="{ 'recording': isRecording }"
+        :class="{ 'recording': isRecording, 'loading': isLoading }"
+        :disabled="isLoading"
     >
       <div class="waveform-container" v-if="isRecording">
         <LiveWaveform
@@ -17,7 +18,8 @@
             :sensitivity="1.2"
         />
       </div>
-      <el-icon v-if="!isRecording"><Microphone /></el-icon>
+      <div class="loading-spinner" v-if="isLoading"></div>
+      <el-icon v-else-if="!isRecording"><Microphone /></el-icon>
       <!-- 显示倒计时(仅录音时显示) -->
       <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
     </button>
@@ -70,11 +72,12 @@ const mediaStreamSource = ref(null) // 媒体流源
 const scriptProcessor = ref(null) // 脚本处理器
 const audioDataBuffer = ref([]) // 音频数据缓冲区
 const MAX_BUFFER_SIZE = 1024 * 10 // 10KB
+const isLoading = ref(false) // 加载状态
 let batchSendTimer = null // 批量发送定时器
 // 声音检测相关变量
 let silenceTimer = null // 静音检测定时器
 const SILENCE_THRESHOLD = 0.01 // 静音阈值
-const SILENCE_DURATION = 1000 // 静音持续时间(毫秒)
+const SILENCE_DURATION = 2000 // 静音持续时间(毫秒)
 
 // 提取资源释放函数
 const releaseResources = () => {
@@ -90,6 +93,7 @@ const releaseResources = () => {
   }
   if (audioContext.value) {
     audioContext.value.close();
+    audioContext.value = null;
   }
   
   // 释放媒体流资源
@@ -245,18 +249,72 @@ const startBackendRecognition = async () => {
   }
 }
 
+// 重新再次调取后端结果识别结果
+const getFinalResult = async () => {
+  // 重新再次调取后端结果识别结果
+  try {
+    const finalResult = await axios({
+      url: 'admin/speech/recognition/result?sessionId=' + sessionId.value,
+      method: 'GET'
+    });
+
+    if (finalResult.code === 0 && finalResult.data) {
+      // 获取后端返回的最终结果
+      const finalRecognitionResult = finalResult.data.result || '';
+
+      // 计算处理后的文本
+      let processedText = '';
+      let cursorPos = 0;
+      if (recordingStartCursorPos.value >= recordingStartText.value.length) {
+        // 光标位置在文本末尾或找不到输入框时,追加到末尾
+        processedText = recordingStartText.value + finalRecognitionResult;
+        cursorPos = recordingStartText.value.length + finalRecognitionResult.length;
+      } else {
+        // 光标位置在文本中间时,插入到光标位置
+        processedText = recordingStartText.value.substring(0, recordingStartCursorPos.value) + finalRecognitionResult + recordingStartText.value.substring(recordingStartCursorPos.value);
+        cursorPos = recordingStartCursorPos.value + finalRecognitionResult.length;
+      }
+
+      // 发送最终识别结果
+      emit('voiceRecognized', {
+        originalText: finalRecognitionResult,
+        processedText: processedText,
+        cursorPos: cursorPos
+      });
+    }
+  } catch (error) {
+    console.error('获取最终识别结果失败:', error);
+    // 继续执行后续逻辑,不中断
+  }
+
+  // 释放资源
+  releaseResources();
+}
+
 // 停止后端识别服务
 const stopBackendRecognition = async () => {
+  // 设置加载状态
+  isLoading.value = true;
+
+  // 延迟500ms
+  await new Promise(resolve => setTimeout(resolve, 500));
+  await getFinalResult();
+  
   try {
+    // 通知后端停止识别
     const stopResult = await axios({
       url: 'admin/speech/recognition/stop?sessionId=' + sessionId.value,
       method: 'POST',
       data: {}
     });
+    
     return stopResult;
   } catch (error) {
     handleError(error, '停止后端识别服务');
     return null;
+  } finally {
+    // 无论成功失败,都设置加载状态为false
+    isLoading.value = false;
   }
 }
 
@@ -517,16 +575,19 @@ const toggleSpeechInput = async () => {
     // 通知后端停止识别
     await stopBackendRecognition()
   } else {
-    // 生成新的会话ID
-    sessionId.value = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
+    // 设置加载状态
+    isLoading.value = true;
     
-    // 记录输入框状态
-    recordInputState()
-    // 初始化倒计时前再次清除定时器(防止快速点击)
-    clearInterval(countdownTimer.value)
-    countdown.value = props.maxDuration // 设置最大录音时间
-
     try {
+      // 生成新的会话ID
+      sessionId.value = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
+      
+      // 记录输入框状态
+      recordInputState()
+      // 初始化倒计时前再次清除定时器(防止快速点击)
+      clearInterval(countdownTimer.value)
+      countdown.value = props.maxDuration // 设置最大录音时间
+
       // 启动后端识别服务
       const startResult = await startBackendRecognition()
       if (!startResult || startResult.code !== 0) {
@@ -627,7 +688,10 @@ const toggleSpeechInput = async () => {
       mediaStreamSource.value.connect(scriptProcessor.value);
       scriptProcessor.value.connect(audioContext.value.destination);
 
-      // 先设置UI状态,让用户知道系统正在准备
+      // 取消加载状态
+      isLoading.value = false;
+      
+      // 设置录音状态,让用户知道系统正在录音
       isRecording.value = true
       emit('recordingStatusChanged', true)
 
@@ -649,6 +713,8 @@ const toggleSpeechInput = async () => {
         }
       }, 1000)
     } catch (err) {
+      // 取消加载状态
+      isLoading.value = false;
       handleError(err, '麦克风权限获取');
     }
   }
@@ -728,4 +794,22 @@ defineExpose({
   font-size: rpx(6);
   color: #666;
 }
+
+.loading-spinner {
+  width: 16px;
+  height: 16px;
+  border: 2px solid rgba(0, 0, 0, 0.1);
+  border-radius: 50%;
+  border-top-color: #ffce1b;
+  animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.speech-btn.loading {
+  cursor: not-allowed;
+  opacity: 0.7;
+}
 </style>