OpenMAIC-React / src /lib /hooks /use-browser-tts.ts
muthuk1's picture
Convert OpenMAIC from Next.js to React (Vite)
f56a29b verified
/**
* 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,
};
}