| |
| |
| |
| |
| |
|
|
| import { useState, useCallback, useRef, useEffect } from 'react'; |
|
|
| |
|
|
| export interface UseBrowserTTSOptions { |
| onStart?: () => void; |
| onEnd?: () => void; |
| onError?: (error: string) => void; |
| rate?: number; |
| pitch?: number; |
| volume?: number; |
| lang?: string; |
| } |
|
|
| export function useBrowserTTS(options: UseBrowserTTSOptions = {}) { |
| const { |
| onStart, |
| onEnd, |
| onError, |
| rate = 1.0, |
| pitch = 1.0, |
| volume = 1.0, |
| lang = 'zh-CN', |
| } = options; |
|
|
| const [isSpeaking, setIsSpeaking] = useState(false); |
| const [isPaused, setIsPaused] = useState(false); |
| const [availableVoices, setAvailableVoices] = useState<SpeechSynthesisVoice[]>([]); |
| const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null); |
|
|
| |
| useEffect(() => { |
| if (typeof window === 'undefined' || !window.speechSynthesis) { |
| return; |
| } |
|
|
| const loadVoices = () => { |
| const voices = window.speechSynthesis.getVoices(); |
| setAvailableVoices(voices); |
| }; |
|
|
| loadVoices(); |
|
|
| |
| if (window.speechSynthesis.onvoiceschanged !== undefined) { |
| window.speechSynthesis.onvoiceschanged = loadVoices; |
| } |
|
|
| return () => { |
| if (window.speechSynthesis.onvoiceschanged !== undefined) { |
| window.speechSynthesis.onvoiceschanged = null; |
| } |
| }; |
| }, []); |
|
|
| const speak = useCallback( |
| (text: string, voiceURI?: string) => { |
| if (typeof window === 'undefined' || !window.speechSynthesis) { |
| onError?.('浏览器不支持 Web Speech API'); |
| return; |
| } |
|
|
| |
| window.speechSynthesis.cancel(); |
|
|
| const utterance = new SpeechSynthesisUtterance(text); |
| utterance.rate = rate; |
| utterance.pitch = pitch; |
| utterance.volume = volume; |
| utterance.lang = lang; |
|
|
| |
| if (voiceURI) { |
| const voice = availableVoices.find((v) => v.voiceURI === voiceURI); |
| if (voice) { |
| utterance.voice = voice; |
| } |
| } |
|
|
| utterance.onstart = () => { |
| setIsSpeaking(true); |
| setIsPaused(false); |
| onStart?.(); |
| }; |
|
|
| utterance.onend = () => { |
| setIsSpeaking(false); |
| setIsPaused(false); |
| utteranceRef.current = null; |
| onEnd?.(); |
| }; |
|
|
| utterance.onerror = (event) => { |
| setIsSpeaking(false); |
| setIsPaused(false); |
| utteranceRef.current = null; |
| onError?.(event.error); |
| }; |
|
|
| utterance.onpause = () => { |
| setIsPaused(true); |
| }; |
|
|
| utterance.onresume = () => { |
| setIsPaused(false); |
| }; |
|
|
| utteranceRef.current = utterance; |
| window.speechSynthesis.speak(utterance); |
| }, |
| [rate, pitch, volume, lang, availableVoices, onStart, onEnd, onError], |
| ); |
|
|
| const pause = useCallback(() => { |
| if (typeof window !== 'undefined' && window.speechSynthesis) { |
| window.speechSynthesis.pause(); |
| } |
| }, []); |
|
|
| const resume = useCallback(() => { |
| if (typeof window !== 'undefined' && window.speechSynthesis) { |
| window.speechSynthesis.resume(); |
| } |
| }, []); |
|
|
| const cancel = useCallback(() => { |
| if (typeof window !== 'undefined' && window.speechSynthesis) { |
| window.speechSynthesis.cancel(); |
| setIsSpeaking(false); |
| setIsPaused(false); |
| utteranceRef.current = null; |
| } |
| }, []); |
|
|
| return { |
| speak, |
| pause, |
| resume, |
| cancel, |
| isSpeaking, |
| isPaused, |
| availableVoices, |
| }; |
| } |
|
|