import { useState, useCallback, useRef, useEffect } from 'react';
// TypeScript declarations for Web Speech API
interface SpeechRecognitionEvent {
results: SpeechRecognitionResultList;
}
interface SpeechRecognitionErrorEvent {
error: string;
}
interface SpeechRecognitionInstance {
continuous: boolean;
interimResults: boolean;
lang: string;
onresult: ((event: SpeechRecognitionEvent) => void) | null;
onend: (() => void) | null;
onerror: ((event: SpeechRecognitionErrorEvent) => void) | null;
start: () => void;
stop: () => void;
}
declare global {
interface Window {
SpeechRecognition?: new () => SpeechRecognitionInstance;
webkitSpeechRecognition?: new () => SpeechRecognitionInstance;
}
}
export type VoiceState = 'idle' | 'listening' | 'speaking' | 'error';
interface UseVoiceReturn {
state: VoiceState;
error: string | null;
startListening: () => void;
stopListening: () => void;
speak: (text: string) => void;
stopSpeaking: () => void;
isSupported: boolean;
}
export function useVoice(): UseVoiceReturn {
const [state, setState] = useState<VoiceState>('idle');
const [error, setError] = useState<string | null>(null);
const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
const synthesisRef = useRef<SpeechSynthesis | null>(null);
const onResultRef = useRef<((transcript: string) => void) | null>(null);
// Check browser support
const isSupported = typeof window !== 'undefined' &&
('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) &&
'speechSynthesis' in window;
// Initialize speech recognition
useEffect(() => {
if (!isSupported) return;
const SpeechRecognitionClass = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognitionClass) return;
const recognition = new SpeechRecognitionClass();
recognition.continuous = false;
recognition.interimResults = false;
recognition.lang = 'en-US';
recognition.onresult = (event: SpeechRecognitionEvent) => {
const transcript = event.results[0][0].transcript;
if (onResultRef.current) {
onResultRef.current(transcript);
}
};
recognition.onend = () => {
setState((prev) => prev === 'listening' ? 'idle' : prev);
};
recognition.onerror = (e: SpeechRecognitionErrorEvent) => {
setError(e.error);
setState('error');
};
recognitionRef.current = recognition;
synthesisRef.current = window.speechSynthesis;
// Chrome bug workaround: keep synthesis alive
const interval = setInterval(() => {
const synth = synthesisRef.current;
if (synth && synth.speaking && !synth.paused) {
synth.pause();
synth.resume();
}
}, 5000);
return () => clearInterval(interval);
}, [isSupported]);
const startListening = useCallback(() => {
if (!recognitionRef.current) {
setError('Voice recognition not supported');
return;
}
setState('listening');
setError(null);
recognitionRef.current.start();
}, []);
const stopListening = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
setState('idle');
}, []);
const speak = useCallback((text: string) => {
const synth = synthesisRef.current;
if (!synth) return;
synth.cancel();
setTimeout(() => {
if (synth.paused) synth.resume();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 1.3;
utterance.pitch = 1.0;
utterance.volume = 1.0;
// Find a good voice
const voices = synth.getVoices();
let voice = voices.find(v =>
v.name.includes('Daniel') ||
v.name.includes('Alex') ||
v.name.includes('Google UK English Male')
);
if (!voice) voice = voices.find(v => v.lang.startsWith('en'));
if (!voice && voices.length > 0) voice = voices[0];
if (voice) utterance.voice = voice;
utterance.onstart = () => setState('speaking');
utterance.onend = () => setState('idle');
utterance.onerror = () => {
setState('error');
setError('Voice synthesis error');
};
synth.speak(utterance);
}, 100);
}, []);
const stopSpeaking = useCallback(() => {
if (synthesisRef.current) {
synthesisRef.current.cancel();
}
setState('idle');
}, []);
return {
state,
error,
startListening,
stopListening,
speak,
stopSpeaking,
isSupported,
};
}