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 语音合成工作流程
- 创建
SpeechSynthesisUtterance对象 - 设置要朗读的文本和语音参数
- 调用
speechSynthesis.speak()方法开始朗读
3. 语音识别(SpeechRecognition)
语音识别允许应用程序将用户的语音转换为文本,支持多种语言和实时识别。
3.1 语音识别核心接口
- SpeechRecognition:语音识别控制器,管理语音识别任务
- SpeechRecognitionEvent:包含语音识别结果的事件对象
- SpeechRecognitionResult:表示一次语音识别的结果
- SpeechRecognitionAlternative:表示一个可能的识别结果
3.2 语音识别工作流程
- 创建
SpeechRecognition对象 - 设置识别语言和其他参数
- 监听
result事件获取识别结果 - 调用
start()方法开始识别 - 调用
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. 相关库和工具
- annyang:轻量级语音识别库
- Artyom.js:语音识别和合成库
- Speechly:语音识别 API 服务
- AssemblyAI:高精度语音识别 API
4. 视频教程
实践练习
1. 基础练习:简单的语音合成应用
- 创建 Vue 3 应用,实现基本的语音合成功能
- 支持文本输入和朗读控制
- 允许用户选择不同的语音和调整语速
2. 进阶练习:语音识别应用
- 实现语音识别功能,将语音转换为文本
- 支持多种语言和连续识别
- 显示实时识别结果
3. 高级练习:创建语音交互助手
- 结合语音合成和语音识别功能
- 实现简单的命令识别和响应
- 添加对话历史记录
4. 综合练习:语音笔记应用
- 创建一个语音笔记应用,支持语音输入和文本输入
- 允许用户保存、编辑和删除笔记
- 支持语音朗读笔记内容
5. 挑战练习:多语言语音翻译应用
- 实现一个实时语音翻译应用
- 支持多种语言之间的翻译
- 结合语音识别、机器翻译和语音合成
- 实现实时翻译和语音输出
总结
Web Speech API 为 Vue 3 应用提供了强大的语音交互能力,可以显著增强应用的可访问性和用户体验。通过合理使用语音合成和语音识别功能,可以为用户提供更自然、更便捷的交互方式。掌握 Web Speech API 的实现原理和最佳实践,对于构建现代化、用户友好的 Vue 3 应用至关重要。