|
|
@@ -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>
|