LibreChat/client/src/hooks/Input/useTextToSpeechBrowser.ts

120 lines
3.2 KiB
TypeScript

import { useRecoilValue } from 'recoil';
import { useState, useEffect, useCallback } from 'react';
import type { VoiceOption } from '~/common';
import store from '~/store';
function useTextToSpeechBrowser({
setIsSpeaking,
}: {
setIsSpeaking: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const voiceName = useRecoilValue(store.voice);
const [voices, setVoices] = useState<VoiceOption[]>([]);
const cloudBrowserVoices = useRecoilValue(store.cloudBrowserVoices);
const [isSpeechSynthesisSupported, setIsSpeechSynthesisSupported] = useState(true);
const updateVoices = useCallback(() => {
const synth = window.speechSynthesis as SpeechSynthesis | undefined;
if (!synth) {
setIsSpeechSynthesisSupported(false);
return;
}
try {
const availableVoices = synth.getVoices();
if (!Array.isArray(availableVoices)) {
console.error('getVoices() did not return an array');
return;
}
const filteredVoices = availableVoices.filter(
(v) => cloudBrowserVoices || v.localService === true,
);
const voiceOptions: VoiceOption[] = filteredVoices.map((v) => ({
value: v.name,
label: v.name,
}));
setVoices(voiceOptions);
} catch (error) {
console.error('Error updating voices:', error);
setIsSpeechSynthesisSupported(false);
}
}, [cloudBrowserVoices]);
useEffect(() => {
const synth = window.speechSynthesis as SpeechSynthesis | undefined;
if (!synth) {
setIsSpeechSynthesisSupported(false);
return;
}
try {
if (synth.getVoices().length) {
updateVoices();
} else {
synth.onvoiceschanged = updateVoices;
}
} catch (error) {
console.error('Error in useEffect:', error);
setIsSpeechSynthesisSupported(false);
}
return () => {
if (synth.onvoiceschanged) {
synth.onvoiceschanged = null;
}
};
}, [updateVoices]);
const generateSpeechLocal = (text: string) => {
if (!isSpeechSynthesisSupported) {
console.warn('Speech synthesis is not supported');
return;
}
const synth = window.speechSynthesis;
const voice = voices.find((v) => v.value === voiceName);
if (!voice) {
console.warn('Selected voice not found');
return;
}
try {
synth.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = synth.getVoices().find((v) => v.name === voice.value) || null;
utterance.onend = () => {
setIsSpeaking(false);
};
utterance.onerror = (event) => {
console.error('Speech synthesis error:', event);
setIsSpeaking(false);
};
setIsSpeaking(true);
synth.speak(utterance);
} catch (error) {
console.error('Error generating speech:', error);
setIsSpeaking(false);
}
};
const cancelSpeechLocal = () => {
if (!isSpeechSynthesisSupported) {
return;
}
try {
window.speechSynthesis.cancel();
} catch (error) {
console.error('Error cancelling speech:', error);
} finally {
setIsSpeaking(false);
}
};
return { generateSpeechLocal, cancelSpeechLocal, voices, isSpeechSynthesisSupported };
}
export default useTextToSpeechBrowser;