// 音声認識カスタムフック // onFinalTranscript: 確定した音声テキストが得られたときのコールバック // onError: エラー発生時のコールバック window.useVoiceRecognition = ({ onFinalTranscript, onError }) => { const { useState, useEffect, useRef } = React; const [isVoiceRecognitionEnabled, setIsVoiceRecognitionEnabled] = useState(false); const [interimTranscript, setInterimTranscript] = useState(''); const speechRecognitionRef = useRef(null); const isVoiceRecognitionEnabledRef = useRef(false); const onFinalTranscriptRef = useRef(onFinalTranscript); const onErrorRef = useRef(onError); // refを最新の値に同期 useEffect(() => { isVoiceRecognitionEnabledRef.current = isVoiceRecognitionEnabled; onFinalTranscriptRef.current = onFinalTranscript; onErrorRef.current = onError; }, [isVoiceRecognitionEnabled, onFinalTranscript, onError]); // 音声認識のセットアップ const setupSpeechRecognition = () => { if (!speechRecognitionRef.current && (window.webkitSpeechRecognition || window.SpeechRecognition)) { const SpeechRecognitionAPI = window.webkitSpeechRecognition || window.SpeechRecognition; const recognition = new SpeechRecognitionAPI(); recognition.lang = 'ja-JP'; recognition.interimResults = true; recognition.maxAlternatives = 1; recognition.continuous = true; recognition.onstart = () => { // 音声認識開始 }; recognition.onend = () => { // トグルがONの場合のみ自動的に再開 if (isVoiceRecognitionEnabledRef.current) { // 少し遅延を入れて再開(連続エラーを防ぐ) setTimeout(() => { if (isVoiceRecognitionEnabledRef.current) { try { recognition.start(); } catch (error) { console.error('音声認識の再開に失敗:', error); } } }, 300); } }; recognition.onerror = (event) => { // 致命的なエラーの場合のみトグルをOFFにする if (event.error === 'not-allowed' || event.error === 'service-not-allowed') { if (onErrorRef.current) { onErrorRef.current('マイクへのアクセスが許可されていません'); } setIsVoiceRecognitionEnabled(false); } else if (event.error === 'audio-capture') { if (onErrorRef.current) { onErrorRef.current('マイクが見つかりません。マイクが接続されているか確認してください。'); } setIsVoiceRecognitionEnabled(false); } // その他のエラー(network, no-speech, abortedなど)は、onendで自動再開される }; recognition.onresult = (event) => { let finalTranscript = ''; let interim = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interim += transcript; } } // 途中経過を表示 setInterimTranscript(interim); // 確定した音声があれば、コールバックを呼ぶ if (finalTranscript && onFinalTranscriptRef.current) { onFinalTranscriptRef.current(finalTranscript); setInterimTranscript(''); } }; speechRecognitionRef.current = recognition; } return speechRecognitionRef.current; }; // 音声認識のトグル処理 useEffect(() => { if (isVoiceRecognitionEnabled) { const recognition = setupSpeechRecognition(); if (recognition) { try { recognition.start(); } catch (error) { console.error('音声認識の開始に失敗:', error); if (onErrorRef.current) { onErrorRef.current(`音声認識の開始に失敗しました: ${error.message}`); } setIsVoiceRecognitionEnabled(false); } } else { if (onErrorRef.current) { onErrorRef.current('お使いのブラウザは音声認識に対応していません'); } setIsVoiceRecognitionEnabled(false); } } else { // トグルOFF時は音声認識を停止 if (speechRecognitionRef.current) { speechRecognitionRef.current.stop(); } setInterimTranscript(''); } }, [isVoiceRecognitionEnabled]); return { isVoiceRecognitionEnabled, setIsVoiceRecognitionEnabled, interimTranscript }; };