| import { useState, useCallback, useRef, useEffect } from "react"; |
| import { VoiceState } from "@/types/chat"; |
|
|
| |
| interface SpeechRecognitionEvent extends Event { |
| results: SpeechRecognitionResultList; |
| } |
|
|
| interface SpeechRecognitionErrorEvent extends Event { |
| error: string; |
| } |
|
|
| interface SpeechRecognitionInstance extends EventTarget { |
| continuous: boolean; |
| interimResults: boolean; |
| lang: string; |
| start: () => void; |
| stop: () => void; |
| abort: () => void; |
| onresult: ((event: SpeechRecognitionEvent) => void) | null; |
| onerror: ((event: SpeechRecognitionErrorEvent) => void) | null; |
| onend: (() => void) | null; |
| } |
|
|
| declare global { |
| interface Window { |
| SpeechRecognition: new () => SpeechRecognitionInstance; |
| webkitSpeechRecognition: new () => SpeechRecognitionInstance; |
| } |
| } |
|
|
| export function useVoice() { |
| const [state, setState] = useState<VoiceState>({ |
| isListening: false, |
| isSpeaking: false, |
| transcript: "", |
| error: null, |
| }); |
|
|
| const recognitionRef = useRef<SpeechRecognitionInstance | null>(null); |
| const synthesisRef = useRef<SpeechSynthesisUtterance | null>(null); |
|
|
| |
| useEffect(() => { |
| if (typeof window !== "undefined") { |
| const SpeechRecognition = |
| window.SpeechRecognition || (window as any).webkitSpeechRecognition; |
|
|
| if (SpeechRecognition) { |
| const recognition = new SpeechRecognition(); |
| recognition.continuous = false; |
| recognition.interimResults = true; |
| recognition.lang = "en-US"; |
|
|
| recognition.onresult = (event) => { |
| const transcript = Array.from(event.results) |
| .map((result) => result[0].transcript) |
| .join(""); |
|
|
| setState((prev) => ({ ...prev, transcript })); |
| }; |
|
|
| recognition.onerror = (event) => { |
| setState((prev) => ({ |
| ...prev, |
| isListening: false, |
| error: event.error, |
| })); |
| }; |
|
|
| recognition.onend = () => { |
| setState((prev) => ({ ...prev, isListening: false })); |
| }; |
|
|
| recognitionRef.current = recognition; |
| } |
| } |
|
|
| return () => { |
| if (recognitionRef.current) { |
| recognitionRef.current.abort(); |
| } |
| }; |
| }, []); |
|
|
| const startListening = useCallback(() => { |
| if (!recognitionRef.current) { |
| setState((prev) => ({ |
| ...prev, |
| error: "Speech recognition not supported", |
| })); |
| return; |
| } |
|
|
| setState((prev) => ({ |
| ...prev, |
| isListening: true, |
| transcript: "", |
| error: null, |
| })); |
|
|
| try { |
| recognitionRef.current.start(); |
| } catch (error) { |
| console.error("Failed to start recognition:", error); |
| } |
| }, []); |
|
|
| const stopListening = useCallback(() => { |
| if (recognitionRef.current) { |
| recognitionRef.current.stop(); |
| } |
| setState((prev) => ({ ...prev, isListening: false })); |
| }, []); |
|
|
| const speak = useCallback((text: string) => { |
| if (!window.speechSynthesis) { |
| setState((prev) => ({ ...prev, error: "Speech synthesis not supported" })); |
| return; |
| } |
|
|
| |
| window.speechSynthesis.cancel(); |
|
|
| const utterance = new SpeechSynthesisUtterance(text); |
| utterance.rate = 1; |
| utterance.pitch = 1; |
| utterance.volume = 1; |
|
|
| utterance.onstart = () => { |
| setState((prev) => ({ ...prev, isSpeaking: true })); |
| }; |
|
|
| utterance.onend = () => { |
| setState((prev) => ({ ...prev, isSpeaking: false })); |
| }; |
|
|
| utterance.onerror = () => { |
| setState((prev) => ({ ...prev, isSpeaking: false })); |
| }; |
|
|
| synthesisRef.current = utterance; |
| window.speechSynthesis.speak(utterance); |
| }, []); |
|
|
| const stopSpeaking = useCallback(() => { |
| window.speechSynthesis.cancel(); |
| setState((prev) => ({ ...prev, isSpeaking: false })); |
| }, []); |
|
|
| return { |
| ...state, |
| startListening, |
| stopListening, |
| speak, |
| stopSpeaking, |
| }; |
| } |
|
|