diff --git a/client/package.json b/client/package.json index 35cf53795f..bccc58196e 100644 --- a/client/package.json +++ b/client/package.json @@ -68,6 +68,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.394.0", "match-sorter": "^6.3.4", + "msedge-tts": "^1.3.4", "rc-input-number": "^7.4.2", "react": "^18.2.0", "react-avatar-editor": "^13.0.2", diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx index c52c6eb701..4bff4bc441 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx @@ -15,9 +15,13 @@ const EngineTTSDropdown: React.FC = ({ external }) => { const endpointOptions = external ? [ { value: 'browser', display: localize('com_nav_browser') }, + { value: 'edge', display: localize('com_nav_edge') }, { value: 'external', display: localize('com_nav_external') }, ] - : [{ value: 'browser', display: localize('com_nav_browser') }]; + : [ + { value: 'browser', display: localize('com_nav_browser') }, + { value: 'edge', display: localize('com_nav_edge') }, + ]; const handleSelect = (value: string) => { setEngineTTS(value); diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx index 37103b9742..340be50e8d 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx @@ -1,64 +1,31 @@ -import React, { useMemo, useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { useRecoilState } from 'recoil'; import Dropdown from '~/components/ui/DropdownNoState'; -import { useVoicesQuery } from '~/data-provider'; -import { useLocalize } from '~/hooks'; +import { useLocalize, useTextToSpeech } from '~/hooks'; import store from '~/store'; -const getLocalVoices = (): Promise => { - return new Promise((resolve) => { - const voices = speechSynthesis.getVoices(); - console.log('voices', voices); - - if (voices.length) { - resolve(voices); - } else { - speechSynthesis.onvoiceschanged = () => resolve(speechSynthesis.getVoices()); - } - }); -}; - -type VoiceOption = { - value: string; - display: string; -}; - export default function VoiceDropdown() { const localize = useLocalize(); const [voice, setVoice] = useRecoilState(store.voice); + const { voices } = useTextToSpeech(); + const [voiceOptions, setVoiceOptions] = useState([]); const [engineTTS] = useRecoilState(store.engineTTS); - const [cloudBrowserVoices] = useRecoilState(store.cloudBrowserVoices); - const externalTextToSpeech = engineTTS === 'external'; - const { data: externalVoices = [] } = useVoicesQuery(); - const [localVoices, setLocalVoices] = useState([]); useEffect(() => { - if (!externalTextToSpeech) { - getLocalVoices().then(setLocalVoices); - } - }, [externalTextToSpeech]); + async function fetchVoices() { + const options = await voices(); + setVoiceOptions(options); - useEffect(() => { - if (voice) { - return; + if (!voice && options.length > 0) { + setVoice(options[0]); + } } - if (externalTextToSpeech && externalVoices.length) { - setVoice(externalVoices[0]); - } else if (!externalTextToSpeech && localVoices.length) { - setVoice(localVoices[0].name); - } - }, [voice, setVoice, externalTextToSpeech, externalVoices, localVoices]); + fetchVoices(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [engineTTS]); - const voiceOptions: VoiceOption[] = useMemo(() => { - if (externalTextToSpeech) { - return externalVoices.map((v) => ({ value: v, display: v })); - } else { - return localVoices - .filter((v) => cloudBrowserVoices || v.localService === true) - .map((v) => ({ value: v.name, display: v.name })); - } - }, [externalTextToSpeech, externalVoices, localVoices, cloudBrowserVoices]); + const memoizedVoiceOptions = useMemo(() => voiceOptions, [voiceOptions]); return (
@@ -66,7 +33,7 @@ export default function VoiceDropdown() { { const [engineSTT] = useRecoilState(store.engineSTT); const [engineTTS] = useRecoilState(store.engineTTS); - const externalSpeechToText = engineSTT === AudioEndpoints.external; - const externalTextToSpeech = engineTTS === AudioEndpoints.external; + const speechToTextEndpoint: STTEndpoints = engineSTT as STTEndpoints; + const textToSpeechEndpoint: TTSEndpoints = engineTTS as TTSEndpoints; - return { externalSpeechToText, externalTextToSpeech }; + return { speechToTextEndpoint, textToSpeechEndpoint }; }; export default useGetAudioSettings; diff --git a/client/src/hooks/Input/useSpeechToText.ts b/client/src/hooks/Input/useSpeechToText.ts index acf0f05ed3..da09926b4e 100644 --- a/client/src/hooks/Input/useSpeechToText.ts +++ b/client/src/hooks/Input/useSpeechToText.ts @@ -4,8 +4,9 @@ import useSpeechToTextExternal from './useSpeechToTextExternal'; import useGetAudioSettings from './useGetAudioSettings'; const useSpeechToText = (handleTranscriptionComplete: (text: string) => void) => { - const { externalSpeechToText } = useGetAudioSettings(); + const { speechToTextEndpoint } = useGetAudioSettings(); const [animatedText, setAnimatedText] = useState(''); + const externalSpeechToText = speechToTextEndpoint === 'external'; const { isListening: speechIsListeningBrowser, diff --git a/client/src/hooks/Input/useSpeechToTextBrowser.ts b/client/src/hooks/Input/useSpeechToTextBrowser.ts index ce17ee54be..75393efc72 100644 --- a/client/src/hooks/Input/useSpeechToTextBrowser.ts +++ b/client/src/hooks/Input/useSpeechToTextBrowser.ts @@ -9,7 +9,8 @@ const useSpeechToTextBrowser = () => { const { showToast } = useToastContext(); const [languageSTT] = useRecoilState(store.languageSTT); const [autoTranscribeAudio] = useRecoilState(store.autoTranscribeAudio); - const { externalSpeechToText } = useGetAudioSettings(); + const { speechToTextEndpoint } = useGetAudioSettings(); + const isBrowserSTTEnabled = speechToTextEndpoint === 'browser'; const [isListening, setIsListening] = useState(false); const { @@ -51,7 +52,7 @@ const useSpeechToTextBrowser = () => { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.shiftKey && e.altKey && e.code === 'KeyL' && !externalSpeechToText) { + if (e.shiftKey && e.altKey && e.code === 'KeyL' && !isBrowserSTTEnabled) { toggleListening(); } }; diff --git a/client/src/hooks/Input/useSpeechToTextExternal.ts b/client/src/hooks/Input/useSpeechToTextExternal.ts index 01ab107817..ea96f31f51 100644 --- a/client/src/hooks/Input/useSpeechToTextExternal.ts +++ b/client/src/hooks/Input/useSpeechToTextExternal.ts @@ -7,7 +7,8 @@ import useGetAudioSettings from './useGetAudioSettings'; const useSpeechToTextExternal = (onTranscriptionComplete: (text: string) => void) => { const { showToast } = useToastContext(); - const { externalSpeechToText } = useGetAudioSettings(); + const { speechToTextEndpoint } = useGetAudioSettings(); + const isExternalSTTEnabled = speechToTextEndpoint === 'external'; const [speechToText] = useRecoilState(store.speechToText); const [autoTranscribeAudio] = useRecoilState(store.autoTranscribeAudio); const [autoSendText] = useRecoilState(store.autoSendText); @@ -194,7 +195,7 @@ const useSpeechToTextExternal = (onTranscriptionComplete: (text: string) => void }; const handleKeyDown = async (e: KeyboardEvent) => { - if (e.shiftKey && e.altKey && e.code === 'KeyL' && !externalSpeechToText) { + if (e.shiftKey && e.altKey && e.code === 'KeyL' && isExternalSTTEnabled) { if (!window.MediaRecorder) { showToast({ message: 'MediaRecorder is not supported in this browser', status: 'error' }); return; diff --git a/client/src/hooks/Input/useTextToSpeech.ts b/client/src/hooks/Input/useTextToSpeech.ts index 9722f9941b..4f915a361d 100644 --- a/client/src/hooks/Input/useTextToSpeech.ts +++ b/client/src/hooks/Input/useTextToSpeech.ts @@ -3,30 +3,67 @@ import { parseTextParts } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; import useTextToSpeechExternal from './useTextToSpeechExternal'; import useTextToSpeechBrowser from './useTextToSpeechBrowser'; -import { usePauseGlobalAudio } from '../Audio'; import useGetAudioSettings from './useGetAudioSettings'; +import useTextToSpeechEdge from './useTextToSpeechEdge'; +import { usePauseGlobalAudio } from '../Audio'; -const useTextToSpeech = (message: TMessage, isLast: boolean, index = 0) => { - const { externalTextToSpeech } = useGetAudioSettings(); +const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { + const { textToSpeechEndpoint } = useGetAudioSettings(); + const { pauseGlobalAudio } = usePauseGlobalAudio(index); + const audioRef = useRef(null); const { - generateSpeechLocal: generateSpeechLocal, - cancelSpeechLocal: cancelSpeechLocal, + generateSpeechLocal, + cancelSpeechLocal, isSpeaking: isSpeakingLocal, + voices: voicesLocal, } = useTextToSpeechBrowser(); const { - generateSpeechExternal: generateSpeechExternal, + generateSpeechEdge, + cancelSpeechEdge, + isSpeaking: isSpeakingEdge, + voices: voicesEdge, + } = useTextToSpeechEdge(); + + const { + generateSpeechExternal, cancelSpeech: cancelSpeechExternal, isSpeaking: isSpeakingExternal, - isLoading: isLoading, - audioRef, - } = useTextToSpeechExternal(message.messageId, isLast, index); - const { pauseGlobalAudio } = usePauseGlobalAudio(index); + isLoading: isLoadingExternal, + audioRef: audioRefExternal, + voices: voicesExternal, + } = useTextToSpeechExternal(message?.messageId || '', isLast, index); - const generateSpeech = externalTextToSpeech ? generateSpeechExternal : generateSpeechLocal; - const cancelSpeech = externalTextToSpeech ? cancelSpeechExternal : cancelSpeechLocal; - const isSpeaking = externalTextToSpeech ? isSpeakingExternal : isSpeakingLocal; + let generateSpeech, cancelSpeech, isSpeaking, isLoading, voices; + + switch (textToSpeechEndpoint) { + case 'external': + generateSpeech = generateSpeechExternal; + cancelSpeech = cancelSpeechExternal; + isSpeaking = isSpeakingExternal; + isLoading = isLoadingExternal; + if (audioRefExternal) { + audioRef.current = audioRefExternal.current; + } + voices = voicesExternal; + break; + case 'edge': + generateSpeech = generateSpeechEdge; + cancelSpeech = cancelSpeechEdge; + isSpeaking = isSpeakingEdge; + isLoading = false; + voices = voicesEdge; + break; + case 'browser': + default: + generateSpeech = generateSpeechLocal; + cancelSpeech = cancelSpeechLocal; + isSpeaking = isSpeakingLocal; + isLoading = false; + voices = voicesLocal; + break; + } const isMouseDownRef = useRef(false); const timerRef = useRef(undefined); @@ -52,7 +89,6 @@ const useTextToSpeech = (message: TMessage, isLast: boolean, index = 0) => { const toggleSpeech = () => { if (isSpeaking) { - console.log('canceling message audio speech'); cancelSpeech(); pauseGlobalAudio(); } else { @@ -69,6 +105,7 @@ const useTextToSpeech = (message: TMessage, isLast: boolean, index = 0) => { toggleSpeech, isSpeaking, isLoading, + voices, audioRef, }; }; diff --git a/client/src/hooks/Input/useTextToSpeechBrowser.ts b/client/src/hooks/Input/useTextToSpeechBrowser.ts index d07fb608ce..c8c8158e31 100644 --- a/client/src/hooks/Input/useTextToSpeechBrowser.ts +++ b/client/src/hooks/Input/useTextToSpeechBrowser.ts @@ -2,6 +2,11 @@ import { useRecoilState } from 'recoil'; import { useState } from 'react'; import store from '~/store'; +interface VoiceOption { + value: string; + display: string; +} + function useTextToSpeechBrowser() { const [cloudBrowserVoices] = useRecoilState(store.cloudBrowserVoices); const [isSpeaking, setIsSpeaking] = useState(false); @@ -32,7 +37,30 @@ function useTextToSpeechBrowser() { setIsSpeaking(false); }; - return { generateSpeechLocal, cancelSpeechLocal, isSpeaking }; + const voices = (): Promise => { + return new Promise((resolve) => { + const getAndMapVoices = () => { + const availableVoices = speechSynthesis + .getVoices() + .filter((v) => cloudBrowserVoices || v.localService === true); + + const voiceOptions: VoiceOption[] = availableVoices.map((v) => ({ + value: v.name, + display: v.name, + })); + + resolve(voiceOptions); + }; + + if (speechSynthesis.getVoices().length) { + getAndMapVoices(); + } else { + speechSynthesis.onvoiceschanged = getAndMapVoices; + } + }); + }; + + return { generateSpeechLocal, cancelSpeechLocal, isSpeaking, voices }; } export default useTextToSpeechBrowser; diff --git a/client/src/hooks/Input/useTextToSpeechEdge.ts b/client/src/hooks/Input/useTextToSpeechEdge.ts new file mode 100644 index 0000000000..73cbc6b794 --- /dev/null +++ b/client/src/hooks/Input/useTextToSpeechEdge.ts @@ -0,0 +1,201 @@ +import { useRecoilState } from 'recoil'; +import { useState, useCallback, useRef, useEffect } from 'react'; +import { MsEdgeTTS, OUTPUT_FORMAT } from 'msedge-tts'; +import { useToastContext } from '~/Providers'; +import useLocalize from '~/hooks/useLocalize'; +import store from '~/store'; + +interface Voice { + value: string; + display: string; +} + +interface UseTextToSpeechEdgeReturn { + generateSpeechEdge: (text: string) => Promise; + cancelSpeechEdge: () => void; + isSpeaking: boolean; + voices: () => Promise; +} + +function useTextToSpeechEdge(): UseTextToSpeechEdgeReturn { + const localize = useLocalize(); + const [isSpeaking, setIsSpeaking] = useState(false); + const [voiceName] = useRecoilState(store.voice); + const ttsRef = useRef(null); + const audioElementRef = useRef(null); + const mediaSourceRef = useRef(null); + const sourceBufferRef = useRef(null); + const pendingBuffers = useRef([]); + const { showToast } = useToastContext(); + + const initializeTTS = useCallback(async (): Promise => { + if (!ttsRef.current) { + ttsRef.current = new MsEdgeTTS(); + } + try { + await ttsRef.current.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3); + } catch (error) { + console.error('Error initializing TTS:', error); + showToast({ + message: localize('com_nav_tts_init_error', (error as Error).message), + status: 'error', + }); + } + }, [voiceName, showToast, localize]); + + const onSourceOpen = useCallback((): void => { + if (!sourceBufferRef.current && mediaSourceRef.current) { + try { + sourceBufferRef.current = mediaSourceRef.current.addSourceBuffer('audio/mpeg'); + sourceBufferRef.current.addEventListener('updateend', appendNextBuffer); + } catch (error) { + console.error('Error adding source buffer:', error); + showToast({ + message: localize('com_nav_source_buffer_error'), + status: 'error', + }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showToast, localize]); + + const initializeMediaSource = useCallback(async (): Promise => { + return new Promise((resolve) => { + if (!mediaSourceRef.current) { + mediaSourceRef.current = new MediaSource(); + audioElementRef.current = new Audio(); + audioElementRef.current.src = URL.createObjectURL(mediaSourceRef.current); + } + + const mediaSource = mediaSourceRef.current; + if (mediaSource.readyState === 'open') { + onSourceOpen(); + resolve(); + } else { + const onSourceOpenWrapper = (): void => { + onSourceOpen(); + resolve(); + mediaSource.removeEventListener('sourceopen', onSourceOpenWrapper); + }; + mediaSource.addEventListener('sourceopen', onSourceOpenWrapper); + } + }); + }, [onSourceOpen]); + + const appendNextBuffer = useCallback((): void => { + if ( + sourceBufferRef.current && + !sourceBufferRef.current.updating && + pendingBuffers.current.length > 0 + ) { + const nextBuffer = pendingBuffers.current.shift(); + if (nextBuffer) { + try { + sourceBufferRef.current.appendBuffer(nextBuffer); + } catch (error) { + console.error('Error appending buffer:', error); + showToast({ + message: localize('com_nav_buffer_append_error'), + status: 'error', + }); + pendingBuffers.current.unshift(nextBuffer); + } + } + } + }, [showToast, localize]); + + const generateSpeechEdge = useCallback( + async (text: string): Promise => { + try { + await initializeTTS(); + await initializeMediaSource(); + + if (!ttsRef.current || !audioElementRef.current) { + throw new Error('TTS or Audio element not initialized'); + } + + setIsSpeaking(true); + pendingBuffers.current = []; + + const readable = await ttsRef.current.toStream(text); + + readable.on('data', (chunk: Buffer) => { + pendingBuffers.current.push(new Uint8Array(chunk)); + appendNextBuffer(); + }); + + readable.on('end', () => { + if (mediaSourceRef.current && mediaSourceRef.current.readyState === 'open') { + mediaSourceRef.current.endOfStream(); + } + }); + + audioElementRef.current.onended = () => { + setIsSpeaking(false); + }; + + await audioElementRef.current.play(); + } catch (error) { + console.error('Error generating speech:', error); + showToast({ + message: localize('com_nav_audio_play_error', (error as Error).message), + status: 'error', + }); + setIsSpeaking(false); + } + }, + [initializeTTS, initializeMediaSource, appendNextBuffer, showToast, localize], + ); + + const cancelSpeechEdge = useCallback((): void => { + try { + if (audioElementRef.current) { + audioElementRef.current.pause(); + audioElementRef.current.currentTime = 0; + } + if (mediaSourceRef.current && mediaSourceRef.current.readyState === 'open') { + mediaSourceRef.current.endOfStream(); + } + pendingBuffers.current = []; + setIsSpeaking(false); + } catch (error) { + console.error('Error cancelling speech:', error); + showToast({ + message: localize('com_nav_speech_cancel_error'), + status: 'error', + }); + } + }, [showToast, localize]); + + const voices = useCallback(async (): Promise => { + if (!ttsRef.current) { + ttsRef.current = new MsEdgeTTS(); + } + try { + const voicesList = await ttsRef.current.getVoices(); + return voicesList.map((v) => ({ + value: v.ShortName, + display: v.FriendlyName, + })); + } catch (error) { + console.error('Error fetching voices:', error); + showToast({ + message: localize('com_nav_voices_fetch_error'), + status: 'error', + }); + return []; + } + }, [showToast, localize]); + + useEffect(() => { + return () => { + if (mediaSourceRef.current) { + URL.revokeObjectURL(audioElementRef.current?.src || ''); + } + }; + }, []); + + return { generateSpeechEdge, cancelSpeechEdge, isSpeaking, voices }; +} + +export default useTextToSpeechEdge; diff --git a/client/src/hooks/Input/useTextToSpeechExternal.ts b/client/src/hooks/Input/useTextToSpeechExternal.ts index 3a413a1505..ce78cc65a3 100644 --- a/client/src/hooks/Input/useTextToSpeechExternal.ts +++ b/client/src/hooks/Input/useTextToSpeechExternal.ts @@ -1,6 +1,6 @@ import { useRecoilValue } from 'recoil'; import { useState, useMemo, useRef, useCallback, useEffect } from 'react'; -import { useTextToSpeechMutation } from '~/data-provider'; +import { useTextToSpeechMutation, useVoicesQuery } from '~/data-provider'; import useAudioRef from '~/hooks/Audio/useAudioRef'; import useLocalize from '~/hooks/useLocalize'; import { useToastContext } from '~/Providers'; @@ -178,7 +178,18 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) return isLocalSpeaking || (isLast && globalIsPlaying); }, [isLocalSpeaking, globalIsPlaying, isLast]); - return { generateSpeechExternal, cancelSpeech, isLoading, isSpeaking, audioRef }; + const useVoices = () => { + return useVoicesQuery().data ?? []; + }; + + return { + generateSpeechExternal, + cancelSpeech, + isLoading, + isSpeaking, + audioRef, + voices: useVoices, + }; } export default useTextToSpeechExternal; diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 894b16831a..5de30b639d 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -657,8 +657,17 @@ export default { com_nav_audio_play_error: 'Error playing audio: {0}', com_nav_audio_process_error: 'Error processing audio: {0}', com_nav_long_audio_warning: 'Longer texts will take longer to process.', + com_nav_tts_init_error: 'Failed to initialize text-to-speech: {0}', + com_nav_source_buffer_error: 'Error setting up audio playback. Please refresh the page.', + com_nav_media_source_init_error: + 'Unable to prepare audio player. Please check your browser settings.', + com_nav_buffer_append_error: 'Problem with audio streaming. The playback may be interrupted.', + com_nav_speech_cancel_error: 'Unable to stop audio playback. You may need to refresh the page.', + com_nav_voices_fetch_error: + 'Could not retrieve voice options. Please check your internet connection.', com_nav_engine: 'Engine', com_nav_browser: 'Browser', + com_nav_edge: 'Edge', com_nav_external: 'External', com_nav_delete_cache_storage: 'Delete TTS cache storage', com_nav_enable_cache_tts: 'Enable cache TTS', diff --git a/package-lock.json b/package-lock.json index c23c1426ad..9ef8ea5b16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1157,6 +1157,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.394.0", "match-sorter": "^6.3.4", + "msedge-tts": "^1.3.4", "rc-input-number": "^7.4.2", "react": "^18.2.0", "react-avatar-editor": "^13.0.2", @@ -12399,7 +12400,6 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -12410,8 +12410,7 @@ "node_modules/asn1.js/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/assert": { "version": "2.1.0", @@ -12898,8 +12897,7 @@ "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { "version": "1.20.2", @@ -13031,8 +13029,7 @@ "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, "node_modules/browser-resolve": { "version": "2.0.0", @@ -13047,7 +13044,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -13061,7 +13057,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", @@ -13072,7 +13067,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", @@ -13084,7 +13078,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, "dependencies": { "bn.js": "^5.0.0", "randombytes": "^2.0.1" @@ -13094,7 +13087,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", - "dev": true, "dependencies": { "bn.js": "^5.2.1", "browserify-rsa": "^4.1.0", @@ -13114,7 +13106,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13254,8 +13245,7 @@ "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -13552,7 +13542,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -14141,7 +14130,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" @@ -14150,14 +14138,12 @@ "node_modules/create-ecdh/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -14170,7 +14156,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -14257,7 +14242,6 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, "dependencies": { "browserify-cipher": "^1.0.0", "browserify-sign": "^4.0.0", @@ -14725,7 +14709,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dev": true, "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" @@ -14807,7 +14790,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", @@ -14817,8 +14799,7 @@ "node_modules/diffie-hellman/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/digest-fetch": { "version": "1.3.0", @@ -15017,7 +14998,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -15031,8 +15011,7 @@ "node_modules/elliptic/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/emittery": { "version": "0.13.1", @@ -15929,7 +15908,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -17534,7 +17512,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -17548,7 +17525,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -17562,7 +17538,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -17771,7 +17746,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -19038,6 +19012,15 @@ "node": ">=10" } }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -21295,7 +21278,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -22152,7 +22134,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" @@ -22164,8 +22145,7 @@ "node_modules/miller-rabin/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/mime": { "version": "3.0.0", @@ -22228,14 +22208,12 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "node_modules/minimatch": { "version": "3.1.2", @@ -22506,6 +22484,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msedge-tts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/msedge-tts/-/msedge-tts-1.3.4.tgz", + "integrity": "sha512-0dj86Gg9VzdOJZVCkSSK/O5Eg0NM9W5p8LsXAEPe7qUmsvdAugPUTcPwt9tyz4GThAzAFBBu554kevH8StLEHQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.5.0", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0", + "isomorphic-ws": "^5.0.0", + "process": "^0.11.10", + "stream-browserify": "^3.0.0", + "ws": "^8.14.1" + } + }, "node_modules/multer": { "version": "1.4.5-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", @@ -23527,7 +23520,6 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, "dependencies": { "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", @@ -23795,7 +23787,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -25521,7 +25512,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", @@ -25534,8 +25524,7 @@ "node_modules/public-encrypt/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/pump": { "version": "3.0.0", @@ -25652,7 +25641,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -25661,7 +25649,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" @@ -26632,7 +26619,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -27085,7 +27071,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -27489,7 +27474,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" @@ -27499,7 +27483,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1",