跳至主要內容
Skip to content

語音辨識 (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 核心屬性

屬性類型說明
langstring辨識語言,如 'zh-TW''en-US'
continuousboolean是否連續辨識(預設為 false,辨識一次後停止)
interimResultsboolean是否回傳中間結果(辨識中的即時文字)
maxAlternativesnumber每次辨識回傳的候選結果數量

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完整支援
Safariwebkit 前綴,支援有限
Firefoxwebkit 前綴,實驗性
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瀏覽器語音辨識介面
webkitSpeechRecognitionWebKit 前綴版本(Safari/舊版 Chrome)
continuous是否連續辨識,預設為 false
interimResults是否回傳中間結果,實現即時顯示
transcript辨識出的最終文字
isFinal判斷結果是否為最終確定

TIP

最佳實踐

  • 務必處理 onerror 事件,提供友善的錯誤訊息
  • onUnmounted 中呼叫 abort() 避免記憶體洩漏
  • 考慮使用 continuous: false 模式,減少背景運算

進階挑戰

  1. 實作一個語音筆記應用,支援中斷續錄與自動標點
  2. 建立一個支援多語言切換的語音搜尋欄位
  3. 將語音指令系統與鍵盤快捷鍵整合,提供多重輸入方式
  4. 結合 TTS 與語音辨識,打造一個完整的語音助手

延伸閱讀與資源