| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- /**
- * Browser Native TTS (Text-to-Speech) Hook
- * Uses Web Speech API for client-side text-to-speech
- * Completely free, no API key required
- */
- import { useState, useCallback, useRef, useEffect } from 'react';
- // Note: Window.SpeechSynthesis declaration is already in the global scope
- export interface UseBrowserTTSOptions {
- onStart?: () => void;
- onEnd?: () => void;
- onError?: (error: string) => void;
- rate?: number; // 0.1 to 10
- pitch?: number; // 0 to 2
- volume?: number; // 0 to 1
- lang?: string; // e.g., 'zh-CN', 'en-US'
- }
- 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);
- // Load available voices
- useEffect(() => {
- if (typeof window === 'undefined' || !window.speechSynthesis) {
- return;
- }
- const loadVoices = () => {
- const voices = window.speechSynthesis.getVoices();
- setAvailableVoices(voices);
- };
- loadVoices();
- // Some browsers load voices asynchronously
- 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;
- }
- // Cancel any ongoing speech
- window.speechSynthesis.cancel();
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.rate = rate;
- utterance.pitch = pitch;
- utterance.volume = volume;
- utterance.lang = lang;
- // Set voice if specified
- 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,
- };
- }
|