mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 02:10:15 +01:00
120 lines
3.2 KiB
TypeScript
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;
|