Vue3集成浏览器API实时语音识别

效果示例

用法

javascript 复制代码
<!-- 浏览器语音识别 -->
<BrowserSpeechRecognitionModal v-if="showModal" :isOpen="showModal" @close="showModal = false" @confirm="handleRecognitionResult" />

const showModal = ref(false);
const inputText= ref(false);
const handleRecognitionResult = (text: any) => {
  inputText.value = text;
};

BrowserSpeechRecognitionModal.vue

javascript 复制代码
<template>
  <transition name="modal-fade">
    <div v-if="isOpen" class="modal-overlay" @click.self="handleOverlayClick">
      <div class="modal-container">
        <div class="modal-header">
          <h2>语音输入</h2>
          <button class="close-button" @click="closeModal">&times;</button>
        </div>

        <div class="modal-body">
          <div class="status-indicator">
            <div v-if="isListening" class="mic-animation">
              <div class="mic-icon">
                <svg viewBox="0 0 24 24">
                  <path
                    d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" />
                </svg>
              </div>
              <div class="sound-wave">
                <div class="wave"></div>
                <div class="wave"></div>
                <div class="wave"></div>
              </div>
            </div>
            <div v-else class="mic-ready">
              <svg viewBox="0 0 24 24">
                <path
                  d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" />
              </svg>
            </div>
            <p class="status-text">{{ statusText }}</p>
            <div v-if="recognitionError" class="error-message">
              <svg viewBox="0 0 24 24" class="error-icon">
                <path
                  d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z" />
              </svg>
              <span>{{ friendlyErrorMessage }}</span>
            </div>
          </div>

          <div class="result-container">
            <div class="result-content" :class="{ 'has-result': transcript }">
              {{ transcript || '请点击"开始录音"按钮并说话...' }}
            </div>
          </div>
        </div>

        <div class="modal-footer">
          <button @click="toggleRecognition" class="control-button" :class="{ 'listening': isListening }"
            :disabled="!isBrowserSupported">
            {{ isListening ? '停止录音' : '开始录音' }}
          </button>
          <button @click="confirmResult" class="confirm-button" :disabled="!transcript">
            使用内容
          </button>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';

const props = defineProps({
  isOpen: {
    type: Boolean,
    required: true
  },
  lang: {
    type: String,
    default: 'zh-CN'
  }
});

const emit = defineEmits(['close', 'confirm']);

const recognition = ref(null);
const isListening = ref(false);
const transcript = ref('');
const recognitionError = ref(null);
const isBrowserSupported = ref(true);

// 错误代码到友好提示的映射
const errorMessageMap = {
  'no-speech': '未检测到语音,请靠近麦克风说话',
  'audio-capture': '无法访问麦克风,请检查麦克风是否已连接',
  'not-allowed': '麦克风权限被拒绝,请允许浏览器使用麦克风',
  'aborted': '语音识别已中止',
  'network': '网络连接问题,请检查网络后重试',
  'not-supported': '您的浏览器不支持语音识别功能',
  'service-not-allowed': '语音识别服务不可用',
  'bad-grammar': '识别过程中出现语法错误',
  'language-not-supported': '不支持当前语言设置',
  'default': '语音识别出现问题,请刷新页面后重试'
};

// 计算属性:生成友好的错误提示
const friendlyErrorMessage = computed(() => {
  if (!recognitionError.value) return '';
  return errorMessageMap[recognitionError.value] || errorMessageMap['default'];
});

const statusText = computed(() => {
  if (recognitionError.value) {
    return '语音识别遇到问题';
  }
  return isListening.value ? '正在聆听中...请说话' : '点击"开始录音"按钮开始说话';
});

// 检查浏览器支持情况
const checkBrowserSupport = () => {
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  isBrowserSupported.value = !!SpeechRecognition;
  if (!isBrowserSupported.value) {
    recognitionError.value = 'not-supported';
  }
  return isBrowserSupported.value;
};

// 初始化语音识别
const initRecognition = () => {
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  recognition.value = new SpeechRecognition();

  recognition.value.continuous = true;
  recognition.value.interimResults = true;
  recognition.value.lang = props.lang;

  recognition.value.onstart = () => {
    isListening.value = true;
    recognitionError.value = null;
    console.log('语音识别已启动');
  };

  recognition.value.onend = () => {
    isListening.value = false;
    console.log('语音识别已结束');
  };

  recognition.value.onresult = (event) => {
    let interimTranscript = '';
    let finalTranscript = '';

    for (let i = event.resultIndex; i < event.results.length; i++) {
      const result = event.results[i];
      const transcript = result[0].transcript;
      if (result.isFinal) {
        finalTranscript += transcript;
      } else {
        interimTranscript += transcript;
      }
    }

    transcript.value = finalTranscript || interimTranscript;
    console.log('识别结果:', transcript.value);
  };

  recognition.value.onerror = (event) => {
    recognitionError.value = event.error;
    isListening.value = false;
    console.error('语音识别错误:', event.error);

    // 特定错误自动重新尝试
    if (['network', 'service-not-allowed'].includes(event.error)) {
      setTimeout(() => {
        if (props.isOpen && !isListening.value) {
          startRecognition();
        }
      }, 1500);
    }
  };
};

// 检查麦克风权限
const checkMicrophonePermission = async () => {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    stream.getTracks().forEach(track => track.stop());
    return true;
  } catch (error) {
    console.error('麦克风权限被拒绝:', error);
    recognitionError.value = 'not-allowed';
    return false;
  }
};

// 开始语音识别
const startRecognition = async () => {
  if (!recognition.value) {
    initRecognition();
  }

  const hasPermission = await checkMicrophonePermission();
  if (!hasPermission) return;

  transcript.value = '';
  recognitionError.value = null;

  try {
    recognition.value.start();
  } catch (error) {
    console.error('启动语音识别失败:', error);
    recognitionError.value = 'service-not-allowed';
  }
};

// 停止语音识别
const stopRecognition = () => {
  if (!recognition.value) return;

  try {
    recognition.value.stop();
  } catch (error) {
    console.error('停止语音识别失败:', error);
  }
};

// 切换录音状态
const toggleRecognition = () => {
  if (!isBrowserSupported.value) return;

  if (isListening.value) {
    stopRecognition();
  } else {
    startRecognition();
  }
};

// 关闭模态框
const closeModal = () => {
  if (isListening.value) {
    stopRecognition();
  }
  emit('close');
};

// 确认使用结果
const confirmResult = () => {
  emit('confirm', transcript.value);
  closeModal();
};

// 点击遮罩层
const handleOverlayClick = (event) => {
  if (event.target === event.currentTarget) {
    closeModal();
  }
};

// 监听语言变化
watch(() => props.lang, (newLang) => {
  if (recognition.value) {
    recognition.value.lang = newLang;
  }
});

// 组件挂载时初始化
onMounted(() => {
  checkBrowserSupport();
  if (isBrowserSupported.value) {
    initRecognition();
  }
});

// 组件卸载前清理
onBeforeUnmount(() => {
  if (recognition.value) {
    recognition.value.onend = null;
    recognition.value.onresult = null;
    recognition.value.onerror = null;
    if (isListening.value) {
      recognition.value.abort();
    }
  }
});
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-container {
  background-color: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  width: 90%;
  max-width: 500px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.modal-header {
  padding: 16px 24px;
  border-bottom: 1px solid #eee;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-header h2 {
  margin: 0;
  font-size: 1.25rem;
  color: #333;
}

.close-button {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #666;
  padding: 0;
  line-height: 1;
  outline: none;
}

.modal-body {
  padding: 24px;
  flex: 1;
  overflow-y: auto;
}

.status-indicator {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 24px;
}

.mic-animation {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.mic-icon svg,
.mic-ready svg {
  width: 36px;
  height: 36px;
  fill: #4a6cf7;
}

.sound-wave {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 36px;
}

.wave {
  width: 6px;
  height: 16px;
  background-color: #4a6cf7;
  border-radius: 3px;
  animation: wave 1.2s infinite ease-in-out;
}

.wave:nth-child(1) {
  animation-delay: -0.6s;
}

.wave:nth-child(2) {
  animation-delay: -0.3s;
}

.wave:nth-child(3) {
  animation-delay: 0s;
}

@keyframes wave {

  0%,
  60%,
  100% {
    transform: scaleY(0.4);
  }

  30% {
    transform: scaleY(1);
  }
}

.mic-ready svg {
  opacity: 0.7;
}

.status-text {
  margin: 0;
  color: #666;
  font-size: 0.9rem;
  text-align: center;
}

.result-container {
  background-color: #f8f9fa;
  border-radius: 8px;
  padding: 16px;
  min-height: 120px;
}

.result-content {
  color: #666;
  font-size: 0.95rem;
  line-height: 1.5;
}

.result-content.has-result {
  color: #333;
}

.modal-footer {
  padding: 16px 24px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.control-button {
  padding: 8px 16px;
  background-color: #f0f2f5;
  border: none;
  outline: none;
  border-radius: 6px;
  color: #333;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s;
}

.control-button.listening {
  background-color: #ffebee;
  color: #f44336;
}

.control-button:hover {
  background-color: #e4e6eb;
}

.confirm-button {
  padding: 8px 16px;
  background-color: #4a6cf7;
  border: none;
  outline: none;
  border-radius: 6px;
  color: white;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.2s;
}

.confirm-button:hover {
  background-color: #3a5bd9;
}

.confirm-button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.modal-fade-enter-active,
.modal-fade-leave-active {
  transition: opacity 0.3s ease;
}

.modal-fade-enter-from,
.modal-fade-leave-to {
  opacity: 0;
}

.error-message {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 12px;
  padding: 8px 12px;
  background-color: #ffebee;
  border-radius: 6px;
  color: #d32f2f;
  font-size: 0.9rem;
}

.error-icon {
  width: 18px;
  height: 18px;
  fill: #d32f2f;
}

.control-button:disabled {
  background-color: #e0e0e0;
  color: #9e9e9e;
  cursor: not-allowed;
}

.status-text {
  font-weight: 500;
  color: #333;
  margin-bottom: 4px;
}
</style>
相关推荐
硅谷秋水8 分钟前
CoT-Drive:利用 LLM 和思维链提示实现自动驾驶的高效运动预测
人工智能·机器学习·语言模型·自动驾驶
界面开发小八哥14 分钟前
Java开发工具IntelliJ IDEA v2025.1——全面支持Java 24、整合AI
java·ide·人工智能·intellij-idea·idea
IT古董34 分钟前
【漫话机器学习系列】214.停用词(Stop Words)
人工智能·机器学习
zz93842 分钟前
Trae 04.22重磅更新:AI 编程领域的革新者
人工智能
爱编程的鱼1 小时前
C# 结构(Struct)
开发语言·人工智能·算法·c#
2301_769624401 小时前
基于Pytorch的深度学习-第二章
人工智能·pytorch·深度学习
咨询187150651271 小时前
高企复审奖补!2025年合肥市高新技术企业重新认定奖励补贴政策及申报条件
大数据·人工智能·区块链
Guheyunyi1 小时前
智能照明系统:照亮智慧生活的多重价值
大数据·前端·人工智能·物联网·信息可视化·生活
云天徽上2 小时前
【数据可视化-27】全球网络安全威胁数据可视化分析(2015-2024)
人工智能·安全·web安全·机器学习·信息可视化·数据分析
ONEYAC唯样2 小时前
“在中国,为中国” 英飞凌汽车业务正式发布中国本土化战略
大数据·人工智能