🗣️ feat: Edge TTS engine (#3358)

* feat: MS Edge TTS

* feat: Edge TTS; fix: STT hook
This commit is contained in:
Marco Beretta 2024-08-07 20:15:41 +02:00 committed by GitHub
parent 01a88991ab
commit b390ba781f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 379 additions and 129 deletions

View file

@ -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",

View file

@ -15,9 +15,13 @@ const EngineTTSDropdown: React.FC<EngineTTSDropdownProps> = ({ 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);

View file

@ -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<SpeechSynthesisVoice[]> => {
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<SpeechSynthesisVoice[]>([]);
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 (
<div className="flex items-center justify-between">
@ -66,7 +33,7 @@ export default function VoiceDropdown() {
<Dropdown
value={voice}
onChange={setVoice}
options={voiceOptions}
options={memoizedVoiceOptions}
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
anchor="bottom start"
testId="VoiceDropdown"

View file

@ -1,4 +1,4 @@
import React, { FC, useContext, useState } from 'react';
import React, { FC } from 'react';
import {
Listbox,
ListboxButton,

View file

@ -1,19 +1,25 @@
import { useRecoilState } from 'recoil';
import store from '~/store';
export enum AudioEndpoints {
export enum STTEndpoints {
browser = 'browser',
external = 'external',
}
export enum TTSEndpoints {
browser = 'browser',
edge = 'edge',
external = 'external',
}
const useGetAudioSettings = () => {
const [engineSTT] = useRecoilState<string>(store.engineSTT);
const [engineTTS] = useRecoilState<string>(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;

View file

@ -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,

View file

@ -9,7 +9,8 @@ const useSpeechToTextBrowser = () => {
const { showToast } = useToastContext();
const [languageSTT] = useRecoilState<string>(store.languageSTT);
const [autoTranscribeAudio] = useRecoilState<boolean>(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();
}
};

View file

@ -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<boolean>(store.speechToText);
const [autoTranscribeAudio] = useRecoilState<boolean>(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;

View file

@ -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<HTMLAudioElement | null>(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<number | undefined>(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,
};
};

View file

@ -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<VoiceOption[]> => {
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;

View file

@ -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<void>;
cancelSpeechEdge: () => void;
isSpeaking: boolean;
voices: () => Promise<Voice[]>;
}
function useTextToSpeechEdge(): UseTextToSpeechEdgeReturn {
const localize = useLocalize();
const [isSpeaking, setIsSpeaking] = useState<boolean>(false);
const [voiceName] = useRecoilState<string>(store.voice);
const ttsRef = useRef<MsEdgeTTS | null>(null);
const audioElementRef = useRef<HTMLAudioElement | null>(null);
const mediaSourceRef = useRef<MediaSource | null>(null);
const sourceBufferRef = useRef<SourceBuffer | null>(null);
const pendingBuffers = useRef<Uint8Array[]>([]);
const { showToast } = useToastContext();
const initializeTTS = useCallback(async (): Promise<void> => {
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<void> => {
return new Promise<void>((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<void> => {
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<Voice[]> => {
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;

View file

@ -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;

View file

@ -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',

89
package-lock.json generated
View file

@ -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",