import { useRecoilValue } from 'recoil'; import { useState, useEffect, useCallback } from 'react'; import type { VoiceOption } from '~/common'; import store from '~/store'; function useTextToSpeechBrowser({ setIsSpeaking, }: { setIsSpeaking: React.Dispatch>; }) { const voiceName = useRecoilValue(store.voice); const [voices, setVoices] = useState([]); const cloudBrowserVoices = useRecoilValue(store.cloudBrowserVoices); const [isSpeechSynthesisSupported, setIsSpeechSynthesisSupported] = useState(true); const updateVoices = useCallback(() => { const synth = window.speechSynthesis as SpeechSynthesis | undefined; if (!synth) { setIsSpeechSynthesisSupported(false); return; } try { const availableVoices = synth.getVoices(); if (!Array.isArray(availableVoices)) { console.error('getVoices() did not return an array'); return; } const filteredVoices = availableVoices.filter( (v) => cloudBrowserVoices || v.localService === true, ); const voiceOptions: VoiceOption[] = filteredVoices.map((v) => ({ value: v.name, label: v.name, })); setVoices(voiceOptions); } catch (error) { console.error('Error updating voices:', error); setIsSpeechSynthesisSupported(false); } }, [cloudBrowserVoices]); useEffect(() => { const synth = window.speechSynthesis as SpeechSynthesis | undefined; if (!synth) { setIsSpeechSynthesisSupported(false); return; } try { if (synth.getVoices().length) { updateVoices(); } else { synth.onvoiceschanged = updateVoices; } } catch (error) { console.error('Error in useEffect:', error); setIsSpeechSynthesisSupported(false); } return () => { if (synth.onvoiceschanged) { synth.onvoiceschanged = null; } }; }, [updateVoices]); const generateSpeechLocal = (text: string) => { if (!isSpeechSynthesisSupported) { console.warn('Speech synthesis is not supported'); return; } const synth = window.speechSynthesis; const voice = voices.find((v) => v.value === voiceName); if (!voice) { console.warn('Selected voice not found'); return; } try { synth.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.voice = synth.getVoices().find((v) => v.name === voice.value) || null; utterance.onend = () => { setIsSpeaking(false); }; utterance.onerror = (event) => { console.error('Speech synthesis error:', event); setIsSpeaking(false); }; setIsSpeaking(true); synth.speak(utterance); } catch (error) { console.error('Error generating speech:', error); setIsSpeaking(false); } }; const cancelSpeechLocal = () => { if (!isSpeechSynthesisSupported) { return; } try { window.speechSynthesis.cancel(); } catch (error) { console.error('Error cancelling speech:', error); } finally { setIsSpeaking(false); } }; return { generateSpeechLocal, cancelSpeechLocal, voices, isSpeechSynthesisSupported }; } export default useTextToSpeechBrowser;