Quellcode durchsuchen

抽离语音输入组件

丸子 vor 5 Monaten
Ursprung
Commit
39c12899bc

+ 11 - 93
src/components/ai/image/ImageToImage.vue

@@ -109,16 +109,11 @@
           <!-- 参考图 -->
            <ImageUpload v-model="uploadedImage" ref="imageUploadRef"/>
           <!-- 语音输入按钮 -->
-          <button
-              @click="toggleSpeechInput"
-              class="speech-btn"
-              :class="{ 'recording': isRecording }"
-          >
-            <el-icon v-if="!isRecording"><Microphone /></el-icon>
-            <el-icon v-else><Mute /></el-icon>
-            <!-- 显示倒计时(仅录音时显示) -->
-            <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
-          </button>
+          <VoiceInput
+              @voiceRecognized="handleVoiceRecognized"
+              lang="zh-CN"
+              maxDuration="10"
+          />
 
           <!-- 终止按钮 -->
           <div
@@ -160,6 +155,7 @@ import {
   ArrowDown,
   ArrowUp
 } from '@element-plus/icons-vue'
+import VoiceInput from '../voice/VoiceInput.vue'
 
 import { saveRecord } from '@/api/personalized/index.js'
 
@@ -188,11 +184,7 @@ const imageUploadRef = ref(null);
 const selectedStyle = ref('');
 // 已移除dropdownVisible,使用el-select的v-model
 
-// 语音输入响应式变量
-const isRecording = ref(false); // 录音状态
-const recognition = ref(null); // 语音识别实例
-const countdown = ref(0); // 倒计时剩余秒数
-const countdownTimer = ref(null); // 倒计时定时器
+
 
 // 风格列表
 const styleList = ['电影写真', '中国风', '卡通', '动漫', '素描', '像素风格'];
@@ -235,86 +227,12 @@ const inputMessage = ref('')
 // 输入框引用
 const inputRef = ref(null)
 
-// 初始化语音识别
-const initSpeechRecognition = () => {
-  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
-  if (!SpeechRecognition) {
-    alert("当前浏览器不支持语音输入功能");
-    return null;
-  }
 
-  const instance = new SpeechRecognition();
-  instance.lang = 'zh-CN';
-  instance.interimResults = false;
-
-  instance.onresult = (event) => {
-    if (event.results?.[0]?.[0]) {
-      inputMessage.value += event.results[0][0].transcript;
-    }
-  };
-
-  // 识别器结束时清除定时器
-  instance.onend = () => {
-    clearInterval(countdownTimer.value);
-    isRecording.value = false;
-    countdown.value = 0;
-  };
-
-  instance.onerror = (event) => {
-    console.error('语音识别错误:', event.error);
-    clearInterval(countdownTimer.value); // 出错时清除定时器
-    isRecording.value = false;
-    Message().error('语音输入失败,请重试!', true)
-    countdown.value = 0;
-  };
-
-  return instance;
-};
 
-// 切换录音状态
-const toggleSpeechInput = () => {
-  // 清除可能存在的旧定时器
-  clearInterval(countdownTimer.value);
-  countdownTimer.value = null;
-
-  if (isRecording.value) {
-    // 手动停止时重置状态
-    countdown.value = 0;
-    recognition.value?.stop();
-    isRecording.value = false;
-  } else {
-    // 初始化倒计时前再次清除定时器(防止快速点击)
-    clearInterval(countdownTimer.value);
-    countdown.value = 10; // 重置为10秒
-
-    recognition.value = initSpeechRecognition();
-    if (!recognition.value) return;
-
-    navigator.mediaDevices.getUserMedia({ audio: true })
-      .then(() => {
-        recognition.value.start();
-        isRecording.value = true;
-
-        // 启动新的倒计时定时器
-        countdownTimer.value = setInterval(() => {
-          countdown.value--;
-          if (countdown.value <= 0) {
-            clearInterval(countdownTimer.value); // 倒计时结束清除
-            recognition.value.stop();
-            isRecording.value = false;
-            countdown.value = 0;
-          }
-        }, 1000);
-      })
-      .catch((err) => {
-        console.error("麦克风权限获取失败:", err);
-        alert("请允许麦克风权限以使用语音输入");
-        // 出错时重置状态
-        isRecording.value = false;
-        countdown.value = 0;
-      });
-  }
-};
+// 处理语音识别结果
+const handleVoiceRecognized = (transcript) => {
+  inputMessage.value += transcript;
+}
 
 // 停止操作函数
 const stopStream = async () => {

+ 11 - 93
src/components/ai/image/TextToImage.vue

@@ -112,16 +112,11 @@
             @keyup.enter="sendMessage"
           />
           <!-- 语音输入按钮 -->
-          <button
-              @click="toggleSpeechInput"
-              class="speech-btn"
-              :class="{ 'recording': isRecording }"
-          >
-            <el-icon v-if="!isRecording"><Microphone /></el-icon>
-            <el-icon v-else><Mute /></el-icon>
-            <!-- 显示倒计时(仅录音时显示) -->
-            <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
-          </button>
+          <VoiceInput
+              @voiceRecognized="handleVoiceRecognized"
+              lang="zh-CN"
+              maxDuration="10"
+          />
           <!-- 终止按钮 -->
           <div
             v-if="conversationInProgress"
@@ -168,6 +163,7 @@ import { saveRecord } from '@/api/personalized/index.js'
 import { globalState } from '@/utils/globalState.js'
 // 语音图标
 import { Microphone, Mute } from "@element-plus/icons-vue";
+import VoiceInput from '../voice/VoiceInput.vue'
 // 终止按钮
 import stopicon from "@/assets/icon/stopicon.png";
 // 消息组件
@@ -177,11 +173,7 @@ import {Message} from "@/utils/message/Message.js";
 import { getModelIdByType } from '@/api/teachers.js'
 import { ModelTypeEnum } from '@/api/teachers.js'
 
-// 语音输入响应式变量
-const isRecording = ref(false); // 录音状态
-const recognition = ref(null); // 语音识别实例
-const countdown = ref(0); // 倒计时剩余秒数
-const countdownTimer = ref(null); // 倒计时定
+
 // 对话状态变量
 const conversationInProgress = ref(false); // 对话是否正在进行中
 const conversationInAbortController = ref(); // 对话进行中 abort 控制器
@@ -246,86 +238,12 @@ onMounted(async () => {
 const messages = ref([])
 const inputMessage = ref('')
 
-// 初始化语音识别
-const initSpeechRecognition = () => {
-  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
-  if (!SpeechRecognition) {
-    alert("当前浏览器不支持语音输入功能");
-    return null;
-  }
-
-  const instance = new SpeechRecognition();
-  instance.lang = 'zh-CN';
-  instance.interimResults = false;
 
-  instance.onresult = (event) => {
-    if (event.results?.[0]?.[0]) {
-      inputMessage.value += event.results[0][0].transcript;
-    }
-  };
-
-  // 识别器结束时清除定时器
-  instance.onend = () => {
-    clearInterval(countdownTimer.value);
-    isRecording.value = false;
-    countdown.value = 0;
-  };
-
-  instance.onerror = (event) => {
-    console.error('语音识别错误:', event.error);
-    clearInterval(countdownTimer.value); // 出错时清除定时器
-    isRecording.value = false;
-    Message().error('语音输入失败,请重试!', true)
-    countdown.value = 0;
-  };
-
-  return instance;
-};
 
-// 切换录音状态
-const toggleSpeechInput = () => {
-  // 清除可能存在的旧定时器
-  clearInterval(countdownTimer.value);
-  countdownTimer.value = null;
-
-  if (isRecording.value) {
-    // 手动停止时重置状态
-    countdown.value = 0;
-    recognition.value?.stop();
-    isRecording.value = false;
-  } else {
-    // 初始化倒计时前再次清除定时器(防止快速点击)
-    clearInterval(countdownTimer.value);
-    countdown.value = 10; // 重置为10秒
-
-    recognition.value = initSpeechRecognition();
-    if (!recognition.value) return;
-
-    navigator.mediaDevices.getUserMedia({ audio: true })
-      .then(() => {
-        recognition.value.start();
-        isRecording.value = true;
-
-        // 启动新的倒计时定时器
-        countdownTimer.value = setInterval(() => {
-          countdown.value--;
-          if (countdown.value <= 0) {
-            clearInterval(countdownTimer.value); // 倒计时结束清除
-            recognition.value.stop();
-            isRecording.value = false;
-            countdown.value = 0;
-          }
-        }, 1000);
-      })
-      .catch((err) => {
-        console.error("麦克风权限获取失败:", err);
-        alert("请允许麦克风权限以使用语音输入");
-        // 出错时重置状态
-        isRecording.value = false;
-        countdown.value = 0;
-      });
-  }
-};
+// 处理语音识别结果
+const handleVoiceRecognized = (transcript) => {
+  inputMessage.value += transcript;
+}
 
 // 停止操作函数
 const stopStream = async () => {

+ 12 - 97
src/components/ai/text/TextToText.vue

@@ -41,17 +41,12 @@
                 placeholder="问我任何问题..."
                 @keyup.enter="handleSendByKeydown"
               />
-              <!-- 语音输入按钮 -->
-              <button
-                  @click="toggleSpeechInput"
-                  class="speech-btn"
-                  :class="{ 'recording': isRecording }"
-              >
-                <el-icon v-if="!isRecording"><Microphone /></el-icon>
-                <el-icon v-else><Mute /></el-icon>
-                <!-- 显示倒计时(仅录音时显示) -->
-                <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
-              </button>
+                <!-- 语音输入按钮 -->
+                <VoiceInput
+                    @voiceRecognized="handleVoiceRecognized"
+                    lang="zh-CN"
+                    maxDuration="10"
+                />
 
               <!-- 终止问答按钮 -->
               <div
@@ -84,6 +79,9 @@ import { teacherList } from '@/api/teachers.js'
 // 导入全局状态
 import { globalState } from "@/utils/globalState.js";
 
+import VoiceInput from '../voice/VoiceInput.vue'
+
+
 // 终止按钮
 import stopicon from "@/assets/icon/stopicon.png";
 
@@ -121,11 +119,6 @@ const props = defineProps({
   personIntroduce: { type: String},
 })
 
-// 语音输入响应式变量
-const isRecording = ref(false); // 录音状态
-const recognition = ref(null); // 语音识别实例
-const countdown = ref(0); // 倒计时剩余秒数
-const countdownTimer = ref(null); // 倒计时定时器
 
 // 默认消息控制
 const showDefaultMessages = ref(true);
@@ -207,87 +200,9 @@ const getConversation = async (id) => {
   activeConversationModelPath.value = personImage.value;
 };
 
-// =========== 【语音录入】相关 ===========
-// 初始化语音识别
-const initSpeechRecognition = () => {
-  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
-  if (!SpeechRecognition) {
-    alert("当前浏览器不支持语音输入功能");
-    return null;
-  }
-
-  const instance = new SpeechRecognition();
-  instance.lang = 'zh-CN';
-  instance.interimResults = false;
-
-  instance.onresult = (event) => {
-    if (event.results?.[0]?.[0]) {
-      prompt.value += event.results[0][0].transcript;
-    }
-  };
-
-  //识别器结束时清除定时器
-  instance.onend = () => {
-    clearInterval(countdownTimer.value);
-    isRecording.value = false;
-    countdown.value = 0;
-  };
-
-  instance.onerror = (event) => {
-    console.error('语音识别错误:', event.error);
-    clearInterval(countdownTimer.value); // 出错时清除定时器
-    isRecording.value = false;
-    Message().error('语音输入失败,请重试!', true)
-    countdown.value = 0;
-  };
-
-  return instance;
-};
-
-
-// 切换录音状态
-const toggleSpeechInput = () => {
-  // 无论当前状态如何,先清除可能存在的旧定时器
-  clearInterval(countdownTimer.value);
-  countdownTimer.value = null;
-
-  if (isRecording.value) {
-    // 手动停止时重置状态
-    countdown.value = 0;
-    recognition.value?.stop();
-    isRecording.value = false;
-  } else {
-    // 初始化倒计时前再次清除定时器(防止快速点击)
-    clearInterval(countdownTimer.value);
-    countdown.value = 10; // 重置为10秒
-
-    recognition.value = initSpeechRecognition();
-    if (!recognition.value) return;
-
-    navigator.mediaDevices.getUserMedia({ audio: true })
-      .then(() => {
-        recognition.value.start();
-        isRecording.value = true;
-
-        // 启动新的倒计时定时器
-        countdownTimer.value = setInterval(() => {
-          countdown.value--;
-          if (countdown.value <= 0) {
-            clearInterval(countdownTimer.value); // 倒计时结束清除
-            recognition.value.stop();
-            isRecording.value = false;
-            countdown.value = 0;
-          }
-        }, 1000);
-      })
-      .catch((err) => {
-        console.error("麦克风权限获取失败:", err);
-        alert("请允许麦克风权限以使用语音输入");
-        // 出错时重置状态
-        isRecording.value = false;
-        countdown.value = 0;
-      });
-  }
+// 语音输入识别结果处理
+const handleVoiceRecognized = (text) => {
+  prompt.value += text;
 };
 
 // =========== 【聊天对话】相关 ===========

+ 12 - 93
src/components/ai/video/ImageToVideo.vue

@@ -74,16 +74,11 @@
             />
              <ImageUpload v-model="uploadedImage" ref="imageUploadRef"/>
             <!-- 语音输入按钮 -->
-            <button
-                @click="toggleSpeechInput"
-                class="speech-btn"
-                :class="{ 'recording': isRecording }"
-            >
-              <el-icon v-if="!isRecording"><Microphone /></el-icon>
-              <el-icon v-else><Mute /></el-icon>
-              <!-- 显示倒计时(仅录音时显示) -->
-              <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
-            </button>
+                <VoiceInput
+                    @voiceRecognized="handleVoiceRecognized"
+                    lang="zh-CN"
+                    maxDuration="10"
+                />
 
             <!-- 终止按钮 -->
             <div
@@ -123,6 +118,9 @@ import {
 
 import { saveRecord } from '@/api/personalized/index.js'
 
+import VoiceInput from '../voice/VoiceInput.vue'
+
+
 // 导入全局状态
 import { globalState } from '@/utils/globalState.js'
 // 语音图标
@@ -143,10 +141,7 @@ const uploadedImage = ref('');
 const imageUploadRef = ref(null);
 
 // 语音输入响应式变量
-const isRecording = ref(false); // 录音状态
-const recognition = ref(null); // 语音识别实例
-const countdown = ref(0); // 倒计时剩余秒数
-const countdownTimer = ref(null); // 倒计时定
+
 // 对话状态变量
 const conversationInProgress = ref(false); // 对话是否正在进行中
 const conversationInAbortController = ref(); // 对话进行中 abort 控制器
@@ -208,85 +203,9 @@ onMounted(async () => {
 const messages = ref([])
 const inputMessage = ref('')
 
-// 初始化语音识别
-const initSpeechRecognition = () => {
-  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
-  if (!SpeechRecognition) {
-    alert("当前浏览器不支持语音输入功能");
-    return null;
-  }
-
-  const instance = new SpeechRecognition();
-  instance.lang = 'zh-CN';
-  instance.interimResults = false;
-
-  instance.onresult = (event) => {
-    if (event.results?.[0]?.[0]) {
-      inputMessage.value += event.results[0][0].transcript;
-    }
-  };
-
-  // 识别器结束时清除定时器
-  instance.onend = () => {
-    clearInterval(countdownTimer.value);
-    isRecording.value = false;
-    countdown.value = 0;
-  };
-
-  instance.onerror = (event) => {
-    console.error('语音识别错误:', event.error);
-    clearInterval(countdownTimer.value); // 出错时清除定时器
-    isRecording.value = false;
-    Message().error('语音输入失败,请重试!', true)
-    countdown.value = 0;
-  };
-
-  return instance;
-};
-
-// 切换录音状态
-const toggleSpeechInput = () => {
-  // 清除可能存在的旧定时器
-  clearInterval(countdownTimer.value);
-  countdownTimer.value = null;
-
-  if (isRecording.value) {
-    // 手动停止时重置状态
-    countdown.value = 0;
-    recognition.value?.stop();
-    isRecording.value = false;
-  } else {
-    // 初始化倒计时前再次清除定时器(防止快速点击)
-    clearInterval(countdownTimer.value);
-    countdown.value = 10; // 重置为10秒
-
-    recognition.value = initSpeechRecognition();
-    if (!recognition.value) return;
-
-    navigator.mediaDevices.getUserMedia({ audio: true })
-      .then(() => {
-        recognition.value.start();
-        isRecording.value = true;
-
-        // 启动新的倒计时定时器
-        countdownTimer.value = setInterval(() => {
-          countdown.value--;
-          if (countdown.value <= 0) {
-            clearInterval(countdownTimer.value); // 倒计时结束清除
-            recognition.value.stop();
-            isRecording.value = false;
-            countdown.value = 0;
-          }
-        }, 1000);
-      })
-      .catch((err) => {
-        console.error("麦克风权限获取失败:", err);
-        alert("请允许麦克风权限以使用语音输入");
-        // 出错时重置状态
-        isRecording.value = false;
-        countdown.value = 0;
-      });
-  }
+// 语音输入识别结果处理
+const handleVoiceRecognized = (text) => {
+  inputMessage.value += text;
 };
 
 // 停止操作函数

+ 166 - 0
src/components/ai/voice/VoiceInput.vue

@@ -0,0 +1,166 @@
+<template>
+  <button
+    @click="toggleSpeechInput"
+    class="speech-btn"
+    :class="{ 'recording': isRecording }"
+  >
+    <el-icon v-if="!isRecording"><Microphone /></el-icon>
+    <el-icon v-else><Mute /></el-icon>
+    <!-- 显示倒计时(仅录音时显示) -->
+    <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
+  </button>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import { Microphone, Mute } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+// 定义props
+const props = defineProps({
+  // 语音识别语言,默认为中文
+  lang: {
+    type: String,
+    default: 'zh-CN'
+  },
+  // 最大录音时间,默认为10秒 
+  maxDuration: {
+    type: Number,
+    default: 10
+  }
+})
+
+// 定义emit事件
+const emit = defineEmits(['voiceRecognized', 'recordingStatusChanged'])
+
+// 语音输入响应式变量
+const isRecording = ref(false) // 录音状态
+const recognition = ref(null) // 语音识别实例
+const countdown = ref(0) // 倒计时剩余秒数
+const countdownTimer = ref(null) // 倒计时定时器
+
+// 初始化语音识别
+const initSpeechRecognition = () => {
+  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
+  if (!SpeechRecognition) {
+    ElMessage.warning('当前浏览器不支持语音输入功能')
+    return null
+  }
+
+  const instance = new SpeechRecognition()
+  instance.lang = props.lang
+  instance.interimResults = false
+
+  instance.onresult = (event) => {
+    if (event.results?.[0]?.[0]) {
+      emit('voiceRecognized', event.results[0][0].transcript)
+    }
+  }
+
+  // 识别器结束时清除定时器
+  instance.onend = () => {
+    clearInterval(countdownTimer.value)
+    isRecording.value = false
+    countdown.value = 0
+    emit('recordingStatusChanged', false)
+  }
+
+  instance.onerror = (event) => {
+    console.error('语音识别错误:', event.error)
+    clearInterval(countdownTimer.value) // 出错时清除定时器
+    isRecording.value = false
+    emit('recordingStatusChanged', false)
+    ElMessage.error('语音输入失败,请重试!', true)
+    countdown.value = 0
+  }
+
+  return instance
+}
+
+// 切换录音状态
+const toggleSpeechInput = () => {
+  // 无论当前状态如何,先清除可能存在的旧定时器
+  clearInterval(countdownTimer.value)
+  countdownTimer.value = null
+
+  if (isRecording.value) {
+    // 手动停止时重置状态
+    countdown.value = 0
+    recognition.value?.stop()
+    isRecording.value = false
+    emit('recordingStatusChanged', false)
+  } else {
+    // 初始化倒计时前再次清除定时器(防止快速点击)
+    clearInterval(countdownTimer.value)
+    countdown.value = props.maxDuration // 设置最大录音时间
+
+    recognition.value = initSpeechRecognition()
+    if (!recognition.value) return
+
+    navigator.mediaDevices.getUserMedia({ audio: true })
+      .then(() => {
+        recognition.value.start()
+        isRecording.value = true
+        emit('recordingStatusChanged', true)
+
+        // 启动新的倒计时定时器
+        countdownTimer.value = setInterval(() => {
+          countdown.value--
+          if (countdown.value <= 0) {
+            clearInterval(countdownTimer.value) // 倒计时结束清除
+            recognition.value.stop()
+            isRecording.value = false
+            emit('recordingStatusChanged', false)
+            countdown.value = 0
+          }
+        }, 1000)
+      })
+      .catch((err) => {
+        console.error('麦克风权限获取失败:', err)
+        ElMessage.warning('请允许麦克风权限以使用语音输入')
+        // 出错时重置状态
+        isRecording.value = false
+        emit('recordingStatusChanged', false)
+        countdown.value = 0
+      })
+  }
+}
+
+// 组件卸载时清理资源
+onUnmounted(() => {
+  clearInterval(countdownTimer.value)
+  recognition.value?.stop()
+})
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+.speech-btn {
+  padding: rpx(5) rpx(10);
+  background: #fff;
+  border: 1px solid #ffce1b;
+  border-radius: rpx(5);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: rpx(4);
+
+  &.recording {
+    background: #ffeeba;
+    border-color: #ffc107;
+
+    .el-icon {
+      color: #dc3545;
+    }
+  }
+
+  .el-icon {
+    font-size: rpx(8);
+    color: #666;
+  }
+}
+</style>