Open3

音声入力について

まさぴょん🐱まさぴょん🐱

SpeechRecognition API を活用した音声入力と会話が終わったタイミングの検知について

「最後に音声が認識された時点から一定時間(例: 5秒)無音が続いたら自動で音声認識をストップする」Logicのポイント

  1. 無音タイマー用の state(または ref) を用意し、最後に音声が更新されたタイミングでタイマーを再セットする。
  2. タイマーが発火したら recognition.stop() を呼び出し、音声入力を停止する。
  3. ユーザーが手動でトグルや停止をした場合にも、タイマーのクリアが必要。
import { useState, useEffect, useCallback } from "react";

/**
 * 音声入力 Input Hook
 *
 * - 音声入力の状態を管理する。
 */
export const useVoiceInput = () => {
  // 音声入力中かどうか
  const [isListening, setIsListening] = useState(false);
  // 音声入力のテキスト
  const [transcript, setTranscript] = useState("");

  // 無音タイマーの ID を格納するための state
  const [silenceTimer, setSilenceTimer] = useState<number | null>(null);

  // @ts-ignore
  const SpeechRecognition =
    window.SpeechRecognition || window.webkitSpeechRecognition;
  type SpeechRecognition = typeof SpeechRecognition;

  // 音声認識インスタンス
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);

  useEffect(() => {
    if (
      (typeof window !== "undefined" && "SpeechRecognition" in window) ||
      "webkitSpeechRecognition" in window
    ) {
      const SpeechRecognition =
        window.SpeechRecognition || window.webkitSpeechRecognition;
      const recognitionInstance = new SpeechRecognition();

      recognitionInstance.continuous = true;
      recognitionInstance.interimResults = true;
      recognitionInstance.lang = "ja-JP";

      // 音声認識結果が返ってきたときの処理
      recognitionInstance.onresult = (event: SpeechRecognitionEvent) => {
        const current = event.resultIndex;
        const text = event.results[current][0].transcript;
        setTranscript(text);

        // 音声が認識されたので、無音タイマーをリセット
        resetSilenceTimer(recognitionInstance);
      };

      // 音声認識を開始したときにも無音タイマーをセット
      recognitionInstance.onstart = () => {
        resetSilenceTimer(recognitionInstance);
      };

      setRecognition(recognitionInstance);
    }
  }, []);

  /**
   * 無音タイマーをリセットし、一定時間後に音声認識を停止するようセットする
   */
  const resetSilenceTimer = useCallback(
    (recognitionInstance: SpeechRecognition) => {
      // 既存のタイマーがあればクリア
      if (silenceTimer) {
        clearTimeout(silenceTimer);
      }

      // 5秒後に自動終了するタイマーをセット(時間は適宜変更)
      const timerId = window.setTimeout(() => {
        recognitionInstance.stop();
        setIsListening(false);
        // 状況に応じて transcript のリセットは好みで
        // setTranscript("");
      }, 5000);

      setSilenceTimer(timerId);
    },
    [silenceTimer]
  );

  /**
   * 音声認識の ON/OFF をトグル
   */
  const toggleListening = useCallback(() => {
    if (!recognition) return;

    if (isListening) {
      recognition.stop();
      setIsListening(false);
      // タイマーもクリアしておく
      if (silenceTimer) {
        clearTimeout(silenceTimer);
        setSilenceTimer(null);
      }
    } else {
      recognition.start();
      setIsListening(true);
    }
  }, [isListening, recognition, silenceTimer]);

  /**
   * 明示的に会話を終了させる
   */
  const stopConversation = useCallback(() => {
    if (recognition) {
      recognition.stop();
    }
    setIsListening(false);
    setTranscript("");

    // タイマーもクリア
    if (silenceTimer) {
      clearTimeout(silenceTimer);
      setSilenceTimer(null);
    }
  }, [recognition, silenceTimer]);

  return {
    isListening,
    transcript,
    toggleListening,
    stopConversation,
  };
};