UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

101 lines 4.6 kB
import { useState, useEffect, useCallback, useRef } from 'react'; const isBrowser = typeof window !== 'undefined'; const synth = isBrowser ? window.speechSynthesis : undefined; /** * Hook to utilize the browser's Speech Synthesis API (Text-to-Speech). * Provides controls to speak text, cancel speech, list available voices, and track status. * * @returns An object with speech synthesis state and control functions. */ export function useSpeechSynthesis() { const [voices, setVoices] = useState([]); const [speaking, setSpeaking] = useState(false); const [error, setError] = useState(null); const isSupported = !!synth; // Ref to hold the utterance instance to prevent stale closures in callbacks const utteranceRef = useRef(null); const updateVoices = useCallback(() => { if (isSupported) { try { const availableVoices = (synth === null || synth === void 0 ? void 0 : synth.getVoices()) || []; setVoices(availableVoices); setError(null); // Clear previous error if voices load now } catch (err) { console.error('Error getting voices:', err); setError(err instanceof Error ? err : new Error('Failed to get voices')); } } }, [isSupported]); // Load voices initially and update when the voices list changes useEffect(() => { if (!isSupported) return; updateVoices(); // Initial attempt // Voices might load asynchronously, listen for changes synth === null || synth === void 0 ? void 0 : synth.addEventListener('voiceschanged', updateVoices); return () => { synth === null || synth === void 0 ? void 0 : synth.removeEventListener('voiceschanged', updateVoices); // Cancel any ongoing speech on unmount if (utteranceRef.current) { synth === null || synth === void 0 ? void 0 : synth.cancel(); } }; }, [isSupported, updateVoices]); const speak = useCallback((text, options = {}) => { var _a, _b, _c, _d; if (!isSupported || speaking) return; // Not supported or already speaking // Cancel previous utterance if any (shouldn't be needed if speaking state is accurate, but safe) synth === null || synth === void 0 ? void 0 : synth.cancel(); const utterance = new SpeechSynthesisUtterance(text); utteranceRef.current = utterance; // Store ref // Apply options utterance.voice = options.voice || null; utterance.lang = options.lang || ((_a = voices[0]) === null || _a === void 0 ? void 0 : _a.lang) || 'en-US'; // Default lang utterance.pitch = (_b = options.pitch) !== null && _b !== void 0 ? _b : 1; utterance.rate = (_c = options.rate) !== null && _c !== void 0 ? _c : 1; utterance.volume = (_d = options.volume) !== null && _d !== void 0 ? _d : 1; utterance.onstart = () => { setSpeaking(true); setError(null); }; utterance.onend = () => { setSpeaking(false); utteranceRef.current = null; }; utterance.onerror = (event) => { console.error('Speech synthesis error:', event.error); setError(new Error(`Speech error: ${event.error}`)); setSpeaking(false); utteranceRef.current = null; }; try { synth === null || synth === void 0 ? void 0 : synth.speak(utterance); } catch (err) { console.error('Error calling synth.speak:', err); setError(err instanceof Error ? err : new Error('Failed to initiate speech')); setSpeaking(false); // Ensure speaking is false if speak fails immediately utteranceRef.current = null; } }, [isSupported, speaking, voices]); // voices needed for default lang const cancel = useCallback(() => { if (!isSupported || !speaking) return; synth === null || synth === void 0 ? void 0 : synth.cancel(); // Note: onend will fire after cancel, which sets speaking to false. // Explicitly setting here might cause a race condition if onend hasn't fired yet. // Relying on the onend handler is generally safer. // setSpeaking(false); // Let onend handle this }, [isSupported, speaking]); return { isSupported, voices, speaking, speak, cancel, error, }; } //# sourceMappingURL=useSpeechSynthesis.js.map