語音辨識 (Speech Recognition)
在上一篇文章中,我們實作了 useTTS 讓網頁能夠「開口說話」。但真正的語音互動是雙向的——網頁不只要會說,還要會「聽」。
本篇將探討 Web Speech API 的另一半:SpeechRecognition(語音辨識)。我們將它封裝成一個功能完整、類型安全的 Vue 3 組合函式,實現語音轉文字與語音指令控制。
一、 語音辨識 API 簡介
Web Speech API 提供的 SpeechRecognition 介面讓瀏覽器能夠辨識使用者的語音,將其轉換為文字。這項技術廣泛應用於:
- 語音搜尋:讓使用者用說的進行搜尋
- 語音指令:「開啟選單」、「捲到頂部」等操作
- 輔助功能:協助行動不便的使用者操作介面
- 語言學習:練習口語發音
WARNING
瀏覽器支援度:SpeechRecognition 在 Firefox 和 Safari 上需加上 webkit 前綴,且 Safari 的支援仍處於實驗階段。生產環境使用前請務必進行相容性檢測。
二、 SpeechRecognition 核心概念
2.1 取得 SpeechRecognition
由於瀏覽器相容性問題,我們需要處理前綴:
typescript
// 取得 SpeechRecognition 建構函式(跨瀏覽器)
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
// 建立實例
const recognition = new SpeechRecognition();2.2 核心屬性
| 屬性 | 類型 | 說明 |
|---|---|---|
lang | string | 辨識語言,如 'zh-TW'、'en-US' |
continuous | boolean | 是否連續辨識(預設為 false,辨識一次後停止) |
interimResults | boolean | 是否回傳中間結果(辨識中的即時文字) |
maxAlternatives | number | 每次辨識回傳的候選結果數量 |
2.3 核心方法與事件
typescript
const recognition = new SpeechRecognition();
// 設定選項
recognition.lang = "zh-TW";
recognition.continuous = true;
recognition.interimResults = true;
// 開始/停止
recognition.start(); // 開始監聽
recognition.stop(); // 停止監聽
recognition.abort(); // 立即中止
// 主要事件
recognition.onresult = (event) => {
// 辨識結果
const transcript = event.results[0][0].transcript;
const confidence = event.results[0][0].confidence;
};
recognition.onstart = () => {
/* 開始監聽 */
};
recognition.onend = () => {
/* 停止監聽 */
};
recognition.onerror = (event) => {
/* 錯誤處理 */
};2.4 辨識結果結構
results:辨識結果列表,可能包含多個片段isFinal:該結果是否為最終確定結果transcript:辨識出的文字confidence:信心指數(0 到 1)
三、 設計 Composable 介面
在封裝 Composable 前,我們先定義清楚的 TypeScript 介面:
typescript
// types/useSpeechRecognition.ts
import type { Ref, ComputedRef } from "vue";
export interface SpeechRecognitionOptions {
lang?: string; // 辨識語言,預設 'zh-TW'
continuous?: boolean; // 是否連續辨識
interimResults?: boolean; // 是否回傳中間結果
}
export interface UseSpeechRecognitionReturn {
// 狀態
isSupported: ComputedRef<boolean>;
isListening: Ref<boolean>;
transcript: Ref<string>; // 當前辨識文字
interimTranscript: Ref<string>; // 中間結果(即時)
error: Ref<string | null>;
// 方法
start: () => void;
stop: () => void;
toggle: () => void;
}四、 實作 useSpeechRecognition Composable
4.1 完整實作
typescript
// composables/useSpeechRecognition.ts
import { ref, computed, onUnmounted } from "vue";
import type {
SpeechRecognitionOptions,
UseSpeechRecognitionReturn,
} from "@/types/useSpeechRecognition";
export function useSpeechRecognition(
options: SpeechRecognitionOptions = {},
): UseSpeechRecognitionReturn {
// 取得 SpeechRecognition 建構函式(跨瀏覽器)
const SpeechRecognition =
typeof window !== "undefined"
? window.SpeechRecognition || window.webkitSpeechRecognition
: null;
const isSupported = computed(() => SpeechRecognition !== null);
// 響應式狀態
const isListening = ref(false);
const transcript = ref("");
const interimTranscript = ref("");
const error = ref<string | null>(null);
// SpeechRecognition 實例
let recognition: SpeechRecognition | null = null;
// 初始化
function init() {
if (!SpeechRecognition) return;
recognition = new SpeechRecognition();
// 設定選項
recognition.lang = options.lang ?? "zh-TW";
recognition.continuous = options.continuous ?? false;
recognition.interimResults = options.interimResults ?? true;
// 事件處理
recognition.onstart = () => {
isListening.value = true;
error.value = null;
};
recognition.onend = () => {
isListening.value = false;
};
recognition.onresult = (event) => {
let final = "";
let interim = "";
// 遍歷所有結果
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
final += result[0].transcript;
} else {
interim += result[0].transcript;
}
}
// 更新狀態
if (final) {
transcript.value += final;
}
interimTranscript.value = interim;
};
recognition.onerror = (event) => {
error.value = event.error;
isListening.value = false;
// 處理特定錯誤
switch (event.error) {
case "no-speech":
console.warn("未偵測到語音");
break;
case "audio-capture":
console.error("無法存取麥克風");
break;
case "not-allowed":
console.error("麥克風權限被拒絕");
break;
case "network":
console.error("網路錯誤");
break;
default:
console.error("語音辨識錯誤:", event.error);
}
};
}
// 開始辨識
function start() {
if (!recognition) init();
if (!recognition) return;
// 重置臨時結果
interimTranscript.value = "";
error.value = null;
try {
recognition.start();
} catch (e) {
// 如果已經在監聽中,忽略錯誤
console.warn("Recognition already started");
}
}
// 停止辨識
function stop() {
if (!recognition) return;
recognition.stop();
}
// 切換辨識狀態
function toggle() {
if (isListening.value) {
stop();
} else {
start();
}
}
// 清理
onUnmounted(() => {
if (recognition) {
recognition.abort();
recognition = null;
}
});
return {
isSupported,
isListening,
transcript,
interimTranscript,
error,
start,
stop,
toggle,
};
}4.2 狀態流程圖
五、 元件中使用
5.1 基本用法:語音轉文字
vue
<script setup lang="ts">
import { useSpeechRecognition } from "@/composables/useSpeechRecognition";
const {
isSupported,
isListening,
transcript,
interimTranscript,
error,
toggle,
} = useSpeechRecognition({
lang: "zh-TW",
continuous: true,
interimResults: true,
});
</script>
<template>
<div v-if="isSupported" class="speech-recognition">
<button @click="toggle" :class="{ listening: isListening }">
{{ isListening ? "🔴 停止" : "🎤 開始" }}
</button>
<div class="result">
<p class="final">{{ transcript }}</p>
<p class="interim">{{ interimTranscript }}</p>
</div>
<p v-if="error" class="error">錯誤:{{ error }}</p>
</div>
<div v-else>您的瀏覽器不支援語音辨識功能</div>
</template>
<style scoped>
.listening {
background: #ef4444;
animation: pulse 1.5s infinite;
}
.interim {
color: #888;
font-style: italic;
}
.error {
color: #ef4444;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
</style>5.2 進階用法:語音指令系統
typescript
// composables/useVoiceCommands.ts
import { watch } from "vue";
import { useSpeechRecognition } from "./useSpeechRecognition";
interface VoiceCommand {
patterns: string[]; // 觸發詞
action: () => void; // 執行動作
}
export function useVoiceCommands(commands: VoiceCommand[]) {
const { transcript, isListening, start, stop } = useSpeechRecognition({
lang: "zh-TW",
continuous: true,
});
// 監聽辨識結果,比對指令
watch(transcript, (text) => {
const lowerText = text.toLowerCase().trim();
for (const command of commands) {
const matched = command.patterns.some((pattern) =>
lowerText.includes(pattern.toLowerCase()),
);
if (matched) {
command.action();
break;
}
}
});
return {
isListening,
transcript,
start,
stop,
};
}使用範例:
vue
<script setup lang="ts">
import { useVoiceCommands } from "@/composables/useVoiceCommands";
import { useRouter } from "vue-router";
const router = useRouter();
const { isListening, start, stop } = useVoiceCommands([
{
patterns: ["回首頁", "回到首頁", "首頁"],
action: () => router.push("/"),
},
{
patterns: ["捲到頂部", "回到頂部", "頂部"],
action: () => window.scrollTo({ top: 0, behavior: "smooth" }),
},
{
patterns: ["捲到底部", "底部"],
action: () =>
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }),
},
{
patterns: ["開啟深色模式", "深色模式"],
action: () => document.documentElement.classList.add("dark"),
},
]);
</script>
<template>
<button @click="isListening ? stop() : start()">
{{ isListening ? "🔴 停止語音指令" : "🎤 啟用語音指令" }}
</button>
</template>六、 技術實務與相容性
6.1 瀏覽器支援狀況
| 瀏覽器 | 支援狀況 |
|---|---|
| Chrome | 完整支援 |
| Edge | 完整支援 |
| Safari | 需 webkit 前綴,支援有限 |
| Firefox | 需 webkit 前綴,實驗性 |
| iOS Safari | 需使用者互動觸發 |
TIP
跨瀏覽器支援:務必使用 window.SpeechRecognition || window.webkitSpeechRecognition 來取得建構函式。
6.2 TypeScript 型別定義
為了讓 TypeScript 正確辨識 webkitSpeechRecognition,需要擴充全域型別:
typescript
// types/global.d.ts
interface Window {
webkitSpeechRecognition: typeof SpeechRecognition;
}6.3 權限處理
語音辨識需要麥克風權限,建議在開始前檢查:
typescript
async function checkMicrophonePermission(): Promise<boolean> {
try {
const result = await navigator.permissions.query({
name: "microphone" as PermissionName,
});
return result.state === "granted";
} catch {
// 某些瀏覽器不支援 permissions API
// 嘗試直接請求權限
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach((track) => track.stop());
return true;
} catch {
return false;
}
}
}6.4 常見錯誤處理
| 錯誤代碼 | 說明 | 解決方案 |
|---|---|---|
no-speech | 未偵測到語音 | 提示使用者說話 |
audio-capture | 無法存取麥克風 | 檢查麥克風連接 |
not-allowed | 權限被拒絕 | 引導使用者授權 |
network | 網路錯誤 | 檢查網路連線 |
aborted | 辨識被中止 | 正常流程,無需處理 |
service-not-allowed | 服務不可用 | 瀏覽器或協定不支援 |
WARNING
HTTPS 要求:語音辨識 API 在大多數瀏覽器上需要 HTTPS 環境(localhost 除外)。確保生產環境使用 HTTPS。
七、 與 TTS 結合:打造語音對話
將語音辨識與上一篇的 TTS 結合,可以打造完整的語音對話體驗:
vue
<script setup lang="ts">
import { ref, watch } from "vue";
import { useSpeechRecognition } from "@/composables/useSpeechRecognition";
import { useTTS } from "@/composables/useTTS";
const { transcript, isListening, toggle } = useSpeechRecognition({
lang: "zh-TW",
continuous: false,
});
const { speak, isSpeaking } = useTTS({ lang: "zh-TW" });
const conversation = ref<{ role: "user" | "bot"; text: string }[]>([]);
// 監聽辨識結果,產生回應
watch(transcript, (text) => {
if (!text.trim()) return;
// 加入使用者訊息
conversation.value.push({ role: "user", text });
// 簡單的回應邏輯(實際可接入 AI API)
const response = generateResponse(text);
conversation.value.push({ role: "bot", text: response });
// 語音回覆
speak(response);
});
function generateResponse(input: string): string {
if (input.includes("你好")) return "你好!有什麼我可以幫助你的嗎?";
if (input.includes("時間"))
return `現在時間是 ${new Date().toLocaleTimeString()}`;
if (input.includes("天氣")) return "抱歉,我還沒有接入天氣 API";
return "我聽到你說:" + input;
}
</script>
<template>
<div class="voice-chat">
<div class="messages">
<div
v-for="(msg, i) in conversation"
:key="i"
:class="['message', msg.role]"
>
{{ msg.text }}
</div>
</div>
<button
@click="toggle"
:disabled="isSpeaking"
:class="{ listening: isListening }"
>
{{ isListening ? "🔴 聆聽中..." : "🎤 按住說話" }}
</button>
</div>
</template>總結
| 概念 | 說明 |
|---|---|
SpeechRecognition | 瀏覽器語音辨識介面 |
webkitSpeechRecognition | WebKit 前綴版本(Safari/舊版 Chrome) |
continuous | 是否連續辨識,預設為 false |
interimResults | 是否回傳中間結果,實現即時顯示 |
transcript | 辨識出的最終文字 |
isFinal | 判斷結果是否為最終確定 |
TIP
最佳實踐:
- 務必處理
onerror事件,提供友善的錯誤訊息 - 在
onUnmounted中呼叫abort()避免記憶體洩漏 - 考慮使用
continuous: false模式,減少背景運算
進階挑戰
- 實作一個語音筆記應用,支援中斷續錄與自動標點
- 建立一個支援多語言切換的語音搜尋欄位
- 將語音指令系統與鍵盤快捷鍵整合,提供多重輸入方式
- 結合 TTS 與語音辨識,打造一個完整的語音助手