Vue 3 与 Web Speech API

概述

Web Speech API 是一项用于语音识别和语音合成的 Web 标准 API,允许 Web 应用程序实现语音交互功能。在 Vue 3 应用中集成 Web Speech API 可以为用户提供更自然、更便捷的交互方式,增强应用的可访问性和用户体验。

核心知识

1. Web Speech API 基本概念

  • 作用:提供语音识别和语音合成功能
  • 组成部分
    • SpeechRecognition:语音识别,将语音转换为文本
    • SpeechSynthesis:语音合成,将文本转换为语音
  • 浏览器支持:主流现代浏览器(Chrome、Firefox、Safari、Edge)
  • 安全限制:语音识别需要用户授权,只能在 HTTPS 环境下使用

2. 语音合成(SpeechSynthesis)

语音合成允许应用程序将文本转换为语音输出,支持多种语言、声音和语速设置。

2.1 语音合成核心接口

  • SpeechSynthesis:语音合成控制器,管理语音合成任务
  • SpeechSynthesisUtterance:表示一个语音合成请求,包含要朗读的文本和各种语音参数
  • SpeechSynthesisVoice:表示可用的语音,包含语言、名称、性别等信息

2.2 语音合成工作流程

  1. 创建 SpeechSynthesisUtterance 对象
  2. 设置要朗读的文本和语音参数
  3. 调用 speechSynthesis.speak() 方法开始朗读

3. 语音识别(SpeechRecognition)

语音识别允许应用程序将用户的语音转换为文本,支持多种语言和实时识别。

3.1 语音识别核心接口

  • SpeechRecognition:语音识别控制器,管理语音识别任务
  • SpeechRecognitionEvent:包含语音识别结果的事件对象
  • SpeechRecognitionResult:表示一次语音识别的结果
  • SpeechRecognitionAlternative:表示一个可能的识别结果

3.2 语音识别工作流程

  1. 创建 SpeechRecognition 对象
  2. 设置识别语言和其他参数
  3. 监听 result 事件获取识别结果
  4. 调用 start() 方法开始识别
  5. 调用 stop()abort() 方法停止识别

4. 前端实现(Vue 3)

4.1 语音合成功能实现

<template>
  <div>
    <h2>语音合成</h2>
    <textarea v-model="textToSpeak" placeholder="输入要朗读的文本" rows="4"></textarea>
    
    <div class="controls">
      <select v-model="selectedVoice" @change="updateVoice">
        <option v-for="voice in availableVoices" :key="voice.voiceURI" :value="voice.voiceURI">
          {{ voice.name }} ({{ voice.lang }})
        </option>
      </select>
      
      <div>
        <label>语速: {{ rate }}</label>
        <input type="range" v-model.number="rate" min="0.1" max="3" step="0.1" @change="updateRate" />
      </div>
      
      <div>
        <label>音量: {{ volume }}</label>
        <input type="range" v-model.number="volume" min="0" max="1" step="0.1" @change="updateVolume" />
      </div>
      
      <div>
        <label>音调: {{ pitch }}</label>
        <input type="range" v-model.number="pitch" min="0" max="2" step="0.1" @change="updatePitch" />
      </div>
    </div>
    
    <div class="buttons">
      <button @click="speak" :disabled="!textToSpeak">朗读</button>
      <button @click="pause" :disabled="!isSpeaking">暂停</button>
      <button @click="resume" :disabled="!isPaused">继续</button>
      <button @click="stop">停止</button>
    </div>
    
    <div v-if="isSpeaking" class="status">正在朗读...</div>
    <div v-if="isPaused" class="status">已暂停</div>
  </div>
</template>

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

const textToSpeak = ref('欢迎使用 Vue 3 语音合成功能!');
const availableVoices = ref([]);
const selectedVoice = ref('');
const rate = ref(1);
const volume = ref(1);
const pitch = ref(1);
const isSpeaking = ref(false);
const isPaused = ref(false);

let utterance = null;

// 初始化语音合成
onMounted(() => {
  utterance = new SpeechSynthesisUtterance();
  utterance.text = textToSpeak.value;
  utterance.rate = rate.value;
  utterance.volume = volume.value;
  utterance.pitch = pitch.value;
  
  // 监听语音合成事件
  utterance.onstart = () => {
    isSpeaking.value = true;
    isPaused.value = false;
  };
  
  utterance.onend = utterance.onpause = () => {
    isSpeaking.value = false;
    isPaused.value = utterance.onpause ? true : false;
  };
  
  utterance.onresume = () => {
    isSpeaking.value = true;
    isPaused.value = false;
  };
  
  // 获取可用语音列表
  const loadVoices = () => {
    availableVoices.value = window.speechSynthesis.getVoices();
    if (availableVoices.value.length > 0) {
      // 默认选择中文语音
      const chineseVoice = availableVoices.value.find(voice => voice.lang.includes('zh'));
      selectedVoice.value = chineseVoice ? chineseVoice.voiceURI : availableVoices.value[0].voiceURI;
      updateVoice();
    }
  };
  
  loadVoices();
  
  // 监听语音列表变化
  window.speechSynthesis.onvoiceschanged = loadVoices;
});

// 更新要朗读的文本
watch(textToSpeak, (newText) => {
  if (utterance) {
    utterance.text = newText;
  }
});

// 更新语音
const updateVoice = () => {
  const voice = availableVoices.value.find(v => v.voiceURI === selectedVoice.value);
  if (voice && utterance) {
    utterance.voice = voice;
  }
};

// 更新语速
const updateRate = () => {
  if (utterance) {
    utterance.rate = rate.value;
  }
};

// 更新音量
const updateVolume = () => {
  if (utterance) {
    utterance.volume = volume.value;
  }
};

// 更新音调
const updatePitch = () => {
  if (utterance) {
    utterance.pitch = pitch.value;
  }
};

// 开始朗读
const speak = () => {
  if (utterance) {
    window.speechSynthesis.speak(utterance);
  }
};

// 暂停朗读
const pause = () => {
  window.speechSynthesis.pause();
};

// 继续朗读
const resume = () => {
  window.speechSynthesis.resume();
};

// 停止朗读
const stop = () => {
  window.speechSynthesis.cancel();
  isSpeaking.value = false;
  isPaused.value = false;
};
</script>

<style scoped>
.controls {
  margin: 1rem 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.buttons {
  margin: 1rem 0;
  display: flex;
  gap: 0.5rem;
}

button {
  padding: 0.5rem 1rem;
  cursor: pointer;
}

.status {
  margin: 1rem 0;
  color: #666;
  font-style: italic;
}

textarea {
  width: 100%;
  padding: 0.5rem;
  font-size: 1rem;
}
</style>

4.2 语音识别功能实现

<template>
  <div>
    <h2>语音识别</h2>
    
    <div class="controls">
      <select v-model="selectedLanguage">
        <option value="zh-CN">中文(简体)</option>
        <option value="en-US">英语(美国)</option>
        <option value="ja-JP">日语</option>
        <option value="ko-KR">韩语</option>
        <option value="fr-FR">法语</option>
        <option value="de-DE">德语</option>
      </select>
      
      <label>
        <input type="checkbox" v-model="continuous" />
        连续识别
      </label>
      
      <label>
        <input type="checkbox" v-model="interimResults" />
        显示 interim 结果
      </label>
    </div>
    
    <div class="buttons">
      <button @click="startRecognition" :disabled="isRecognizing">开始识别</button>
      <button @click="stopRecognition" :disabled="!isRecognizing">停止识别</button>
    </div>
    
    <div v-if="isRecognizing" class="status">正在聆听...</div>
    
    <div class="results">
      <h3>识别结果:</h3>
      <div v-if="finalTranscript" class="final">{{ finalTranscript }}</div>
      <div v-if="interimTranscript" class="interim">{{ interimTranscript }}</div>
      <div v-if="!finalTranscript && !interimTranscript" class="placeholder">点击"开始识别"按钮开始说话</div>
    </div>
    
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

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

const selectedLanguage = ref('zh-CN');
const continuous = ref(false);
const interimResults = ref(true);
const isRecognizing = ref(false);
const finalTranscript = ref('');
const interimTranscript = ref('');
const error = ref('');

let recognition = null;

// 初始化语音识别
onMounted(() => {
  // 检查浏览器支持
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (!SpeechRecognition) {
    error.value = '您的浏览器不支持语音识别功能';
    return;
  }
  
  // 创建语音识别对象
  recognition = new SpeechRecognition();
  recognition.lang = selectedLanguage.value;
  recognition.continuous = continuous.value;
  recognition.interimResults = interimResults.value;
  
  // 监听语音识别事件
  recognition.onresult = (event) => {
    interimTranscript.value = '';
    finalTranscript.value = '';
    
    for (let i = event.resultIndex; i < event.results.length; i++) {
      const result = event.results[i];
      if (result.isFinal) {
        finalTranscript.value += result[0].transcript;
      } else if (interimResults.value) {
        interimTranscript.value += result[0].transcript;
      }
    }
  };
  
  recognition.onstart = () => {
    isRecognizing.value = true;
    error.value = '';
  };
  
  recognition.onend = () => {
    isRecognizing.value = false;
  };
  
  recognition.onerror = (event) => {
    isRecognizing.value = false;
    error.value = `识别错误: ${event.error}`;
  };
});

// 组件销毁前停止识别
onBeforeUnmount(() => {
  if (recognition) {
    recognition.stop();
  }
});

// 开始识别
const startRecognition = () => {
  if (!recognition) return;
  
  recognition.lang = selectedLanguage.value;
  recognition.continuous = continuous.value;
  recognition.interimResults = interimResults.value;
  
  try {
    recognition.start();
  } catch (e) {
    error.value = `启动识别失败: ${e.message}`;
  }
};

// 停止识别
const stopRecognition = () => {
  if (recognition) {
    recognition.stop();
  }
};
</script>

<style scoped>
.controls {
  margin: 1rem 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.buttons {
  margin: 1rem 0;
  display: flex;
  gap: 0.5rem;
}

button {
  padding: 0.5rem 1rem;
  cursor: pointer;
}

.status {
  margin: 1rem 0;
  color: #666;
  font-style: italic;
}

.results {
  margin: 1rem 0;
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  min-height: 100px;
}

.final {
  font-size: 1.2rem;
  margin: 0.5rem 0;
}

.interim {
  color: #999;
  font-style: italic;
  margin: 0.5rem 0;
}

.placeholder {
  color: #999;
  font-style: italic;
  text-align: center;
  margin: 2rem 0;
}

.error {
  color: red;
  margin: 1rem 0;
}
</style>

4.3 创建可复用的 Web Speech Composable

// composables/useWebSpeech.js
import { ref, onMounted, onBeforeUnmount } from 'vue';

export function useWebSpeech() {
  // 语音合成相关
  const isSpeaking = ref(false);
  const isPaused = ref(false);
  const availableVoices = ref([]);
  let utterance = null;
  
  // 语音识别相关
  const isRecognizing = ref(false);
  const finalTranscript = ref('');
  const interimTranscript = ref('');
  const recognitionError = ref('');
  let recognition = null;
  
  // 初始化语音合成
  const initSpeechSynthesis = () => {
    utterance = new SpeechSynthesisUtterance();
    
    utterance.onstart = () => {
      isSpeaking.value = true;
      isPaused.value = false;
    };
    
    utterance.onend = () => {
      isSpeaking.value = false;
      isPaused.value = false;
    };
    
    utterance.onpause = () => {
      isSpeaking.value = false;
      isPaused.value = true;
    };
    
    utterance.onresume = () => {
      isSpeaking.value = true;
      isPaused.value = false;
    };
    
    // 获取可用语音
    const loadVoices = () => {
      availableVoices.value = window.speechSynthesis.getVoices();
    };
    
    loadVoices();
    window.speechSynthesis.onvoiceschanged = loadVoices;
  };
  
  // 初始化语音识别
  const initSpeechRecognition = () => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SpeechRecognition) {
      recognitionError.value = '您的浏览器不支持语音识别功能';
      return false;
    }
    
    recognition = new SpeechRecognition();
    recognition.interimResults = true;
    recognition.continuous = false;
    
    recognition.onresult = (event) => {
      interimTranscript.value = '';
      finalTranscript.value = '';
      
      for (let i = event.resultIndex; i < event.results.length; i++) {
        const result = event.results[i];
        if (result.isFinal) {
          finalTranscript.value += result[0].transcript;
        } else {
          interimTranscript.value += result[0].transcript;
        }
      }
    };
    
    recognition.onstart = () => {
      isRecognizing.value = true;
      recognitionError.value = '';
    };
    
    recognition.onend = () => {
      isRecognizing.value = false;
    };
    
    recognition.onerror = (event) => {
      isRecognizing.value = false;
      recognitionError.value = `识别错误: ${event.error}`;
    };
    
    return true;
  };
  
  // 语音合成方法
  const speak = (text, options = {}) => {
    if (!utterance) {
      initSpeechSynthesis();
    }
    
    utterance.text = text;
    utterance.rate = options.rate || 1;
    utterance.volume = options.volume || 1;
    utterance.pitch = options.pitch || 1;
    
    if (options.voice) {
      const voice = availableVoices.value.find(v => v.voiceURI === options.voice);
      if (voice) {
        utterance.voice = voice;
      }
    }
    
    window.speechSynthesis.speak(utterance);
  };
  
  const pause = () => {
    window.speechSynthesis.pause();
  };
  
  const resume = () => {
    window.speechSynthesis.resume();
  };
  
  const stopSpeaking = () => {
    window.speechSynthesis.cancel();
    isSpeaking.value = false;
    isPaused.value = false;
  };
  
  // 语音识别方法
  const startRecognition = (options = {}) => {
    if (!recognition && !initSpeechRecognition()) {
      return;
    }
    
    recognition.lang = options.lang || 'zh-CN';
    recognition.continuous = options.continuous || false;
    recognition.interimResults = options.interimResults || true;
    
    try {
      recognition.start();
    } catch (e) {
      recognitionError.value = `启动识别失败: ${e.message}`;
    }
  };
  
  const stopRecognition = () => {
    if (recognition) {
      recognition.stop();
    }
  };
  
  const abortRecognition = () => {
    if (recognition) {
      recognition.abort();
    }
  };
  
  const clearTranscript = () => {
    finalTranscript.value = '';
    interimTranscript.value = '';
  };
  
  // 组件挂载时初始化
  onMounted(() => {
    initSpeechSynthesis();
    initSpeechRecognition();
  });
  
  // 组件销毁前清理
  onBeforeUnmount(() => {
    if (recognition) {
      recognition.stop();
    }
    stopSpeaking();
  });
  
  return {
    // 语音合成
    isSpeaking,
    isPaused,
    availableVoices,
    speak,
    pause,
    resume,
    stopSpeaking,
    
    // 语音识别
    isRecognizing,
    finalTranscript,
    interimTranscript,
    recognitionError,
    startRecognition,
    stopRecognition,
    abortRecognition,
    clearTranscript
  };
}

4.4 使用 Composable 的示例

<template>
  <div>
    <h2>使用 Web Speech Composable</h2>
    
    <div class="synthesis-section">
      <h3>语音合成</h3>
      <input v-model="textToSpeak" placeholder="输入要朗读的文本" />
      <button @click="handleSpeak" :disabled="isSpeaking">朗读</button>
      <button @click="handleStop" :disabled="!isSpeaking">停止</button>
      <div v-if="isSpeaking" class="status">正在朗读...</div>
    </div>
    
    <div class="recognition-section">
      <h3>语音识别</h3>
      <button @click="handleStartRecognition" :disabled="isRecognizing">开始识别</button>
      <button @click="handleStopRecognition" :disabled="!isRecognizing">停止识别</button>
      <div v-if="isRecognizing" class="status">正在聆听...</div>
      <div class="result">
        <h4>识别结果:</h4>
        <div>{{ finalTranscript }}</div>
        <div v-if="interimTranscript" class="interim">{{ interimTranscript }}</div>
      </div>
      <div v-if="recognitionError" class="error">{{ recognitionError }}</div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useWebSpeech } from './composables/useWebSpeech';

const textToSpeak = ref('欢迎使用 Vue 3 Web Speech Composable!');

const {
  isSpeaking,
  speak,
  stopSpeaking,
  isRecognizing,
  finalTranscript,
  interimTranscript,
  recognitionError,
  startRecognition,
  stopRecognition
} = useWebSpeech();

const handleSpeak = () => {
  speak(textToSpeak.value);
};

const handleStop = () => {
  stopSpeaking();
};

const handleStartRecognition = () => {
  startRecognition({ lang: 'zh-CN' });
};

const handleStopRecognition = () => {
  stopRecognition();
};
</script>

最佳实践

1. 用户体验优化

  • 提供清晰的反馈:在语音合成和语音识别过程中提供明确的状态指示
  • 允许用户控制:提供开始、停止、暂停等控制按钮
  • 支持多种语言:根据用户的语言偏好提供相应的语音选项
  • 优化语音参数:提供语速、音量、音调等调整选项
  • 处理错误情况:向用户显示友好的错误信息

2. 性能优化

  • 合理使用连续识别:连续识别会消耗更多资源,只在必要时使用
  • 限制识别时长:对于长时间识别,考虑设置合理的超时时间
  • 优化语音合成队列:避免同时提交过多的语音合成请求
  • 预加载语音资源:在应用启动时预加载可用语音列表

3. 可访问性考虑

  • 支持键盘操作:为所有控制按钮添加键盘支持
  • 提供替代输入方式:对于不支持语音功能的浏览器,提供文本输入选项
  • 使用语义化 HTML:确保界面元素具有正确的语义和 ARIA 属性
  • 支持屏幕阅读器:确保应用可以被屏幕阅读器正确读取

4. 安全和隐私

  • 尊重用户隐私:明确告知用户语音数据的使用方式
  • 获取用户授权:在使用语音识别前,确保获得用户的明确授权
  • 保护语音数据:如果需要传输语音数据,使用安全的 HTTPS 连接
  • 遵守隐私法规:确保应用符合 GDPR、CCPA 等隐私法规

5. 跨浏览器兼容性

  • 检查浏览器支持:在使用前检查 Web Speech API 是否可用
  • 使用前缀:对于某些浏览器,需要使用带前缀的 API(如 webkitSpeechRecognition
  • 提供降级方案:为不支持的浏览器提供替代功能
  • 测试多种浏览器:在不同浏览器上测试语音功能

常见问题与解决方案

1. 语音合成没有声音

  • 原因
    • 设备音量过低或静音
    • 浏览器不支持语音合成
    • 没有可用的语音
  • 解决方案
    • 检查设备音量设置
    • 检查浏览器支持情况
    • 等待语音资源加载完成

2. 语音识别无法启动

  • 原因
    • 没有获得用户授权
    • 浏览器不支持语音识别
    • 非 HTTPS 环境
  • 解决方案
    • 请求用户授权
    • 检查浏览器支持情况
    • 确保在 HTTPS 环境下使用

3. 语音识别结果不准确

  • 原因
    • 背景噪音过大
    • 说话语速过快或过慢
    • 选择了错误的语言
    • 网络连接不稳定
  • 解决方案
    • 减少背景噪音
    • 调整说话语速
    • 选择正确的识别语言
    • 确保网络连接稳定

4. 语音识别自动停止

  • 原因
    • 静默时间过长
    • 连续识别未开启
    • 网络中断
  • 解决方案
    • 开启连续识别模式
    • 保持正常语速和节奏
    • 检查网络连接

5. 语音合成发音不准确

  • 原因
    • 选择了不适合的语音
    • 文本包含特殊字符或生僻词
    • 语音引擎限制
  • 解决方案
    • 选择合适的语音
    • 优化文本内容
    • 使用 phoneme 标记(如果支持)

高级学习资源

1. 官方文档

2. 深度教程

3. 相关库和工具

4. 视频教程

实践练习

1. 基础练习:简单的语音合成应用

  • 创建 Vue 3 应用,实现基本的语音合成功能
  • 支持文本输入和朗读控制
  • 允许用户选择不同的语音和调整语速

2. 进阶练习:语音识别应用

  • 实现语音识别功能,将语音转换为文本
  • 支持多种语言和连续识别
  • 显示实时识别结果

3. 高级练习:创建语音交互助手

  • 结合语音合成和语音识别功能
  • 实现简单的命令识别和响应
  • 添加对话历史记录

4. 综合练习:语音笔记应用

  • 创建一个语音笔记应用,支持语音输入和文本输入
  • 允许用户保存、编辑和删除笔记
  • 支持语音朗读笔记内容

5. 挑战练习:多语言语音翻译应用

  • 实现一个实时语音翻译应用
  • 支持多种语言之间的翻译
  • 结合语音识别、机器翻译和语音合成
  • 实现实时翻译和语音输出

总结

Web Speech API 为 Vue 3 应用提供了强大的语音交互能力,可以显著增强应用的可访问性和用户体验。通过合理使用语音合成和语音识别功能,可以为用户提供更自然、更便捷的交互方式。掌握 Web Speech API 的实现原理和最佳实践,对于构建现代化、用户友好的 Vue 3 应用至关重要。

« 上一篇 Vue 3与Web Crypto API - 实现前端加密安全的核心技术 下一篇 » Vue 3与WebXR API - 实现虚拟现实和增强现实体验的核心技术