diff --git a/client/src/common/types.ts b/client/src/common/types.ts index e78cc5afe52..e79bf2bddfd 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -19,6 +19,7 @@ import type { TStartupConfig, EModelEndpoint, AssistantsEndpoint, + TMessageContentParts, AuthorizationTypeEnum, TSetOption as SetOption, TokenExchangeMethodEnum, @@ -31,6 +32,17 @@ export enum PromptsEditorMode { ADVANCED = 'advanced', } +export enum STTEndpoints { + browser = 'browser', + external = 'external', +} + +export enum TTSEndpoints { + browser = 'browser', + edge = 'edge', + external = 'external', +} + export type AudioChunk = { audio: string; isFinal: boolean; @@ -374,6 +386,19 @@ export type Option = Record & { value: string | number | null; }; +export type VoiceOption = { + value: string; + label: string; +}; + +export type TMessageAudio = { + messageId?: string; + content?: TMessageContentParts[] | string; + className?: string; + isLast: boolean; + index: number; +}; + export type OptionWithIcon = Option & { icon?: React.ReactNode }; export type MentionOption = OptionWithIcon & { type: string; diff --git a/client/src/components/Audio/TTS.tsx b/client/src/components/Audio/TTS.tsx new file mode 100644 index 00000000000..0ccad8a0517 --- /dev/null +++ b/client/src/components/Audio/TTS.tsx @@ -0,0 +1,256 @@ +import { useEffect, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; +import type { TMessageAudio } from '~/common'; +import { useLocalize, useTTSBrowser, useTTSEdge, useTTSExternal } from '~/hooks'; +import { VolumeIcon, VolumeMuteIcon, Spinner } from '~/components/svg'; +import { useToastContext } from '~/Providers/ToastContext'; +import { logger } from '~/utils'; +import store from '~/store'; + +export function BrowserTTS({ isLast, index, messageId, content, className }: TMessageAudio) { + const localize = useLocalize(); + const playbackRate = useRecoilValue(store.playbackRate); + + const { toggleSpeech, isSpeaking, isLoading, audioRef } = useTTSBrowser({ + isLast, + index, + messageId, + content, + }); + + const renderIcon = (size: string) => { + if (isLoading === true) { + return ; + } + + if (isSpeaking === true) { + return ; + } + + return ; + }; + + useEffect(() => { + const messageAudio = document.getElementById(`audio-${messageId}`) as HTMLAudioElement | null; + if (!messageAudio) { + return; + } + if (playbackRate != null && playbackRate > 0 && messageAudio.playbackRate !== playbackRate) { + messageAudio.playbackRate = playbackRate; + } + }, [audioRef, isSpeaking, playbackRate, messageId]); + + logger.log( + 'MessageAudio: audioRef.current?.src, audioRef.current', + audioRef.current?.src, + audioRef.current, + ); + + return ( + <> + +