🔀 refactor: Modularize TTS Logic for Improved Browser support (#3657)

* WIP: message audio refactor

* WIP: use MessageAudio by provider

* fix: Update MessageAudio component to use TTSEndpoints enum

* feat: Update useTextToSpeechBrowser hook to handle errors and improve error logging

* feat: Add voice dropdown components for different TTS engines

* docs: update incorrect `voices` example

changed `voice: ''` to `voices: ['alloy']`

* feat: Add brwoser support check for Edge TTS engine component with error toast if not supported

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
Danny Avila 2024-08-15 11:34:25 -04:00 committed by GitHub
parent bcde0beb47
commit dba704079c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 784 additions and 187 deletions

View file

@ -79,6 +79,7 @@ export default function HoverButtons({
messageId={message.messageId}
content={message.content ?? message.text}
isLast={isLast}
className="hover-button rounded-md p-1 pl-0 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible"
/>
)}
{isEditableEndpoint && (

View file

@ -1,104 +1,22 @@
import { useEffect } from 'react';
// client/src/components/Chat/Messages/MessageAudio.tsx
import { memo } from 'react';
import { useRecoilValue } from 'recoil';
import type { TMessageContentParts } from 'librechat-data-provider';
import { VolumeIcon, VolumeMuteIcon, Spinner } from '~/components/svg';
import { useLocalize, useTextToSpeech } from '~/hooks';
import { logger } from '~/utils';
import type { TMessageAudio } from '~/common';
import { BrowserTTS, EdgeTTS, ExternalTTS } from '~/components/Audio/TTS';
import { TTSEndpoints } from '~/common';
import store from '~/store';
type THoverButtons = {
messageId?: string;
content?: TMessageContentParts[] | string;
isLast: boolean;
index: number;
};
function MessageAudio(props: TMessageAudio) {
const engineTTS = useRecoilValue<string>(store.engineTTS);
export default function MessageAudio({ isLast, index, messageId, content }: THoverButtons) {
const localize = useLocalize();
const playbackRate = useRecoilValue(store.playbackRate);
const { toggleSpeech, isSpeaking, isLoading, audioRef } = useTextToSpeech({
isLast,
index,
messageId,
content,
});
const renderIcon = (size: string) => {
if (isLoading === true) {
return <Spinner size={size} />;
}
if (isSpeaking === true) {
return <VolumeMuteIcon size={size} />;
}
return <VolumeIcon size={size} />;
const TTSComponents = {
[TTSEndpoints.edge]: EdgeTTS,
[TTSEndpoints.browser]: BrowserTTS,
[TTSEndpoints.external]: ExternalTTS,
};
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 (
<>
<button
className="hover-button rounded-md p-1 pl-0 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible"
// onMouseDownCapture={() => {
// if (audioRef.current) {
// audioRef.current.muted = false;
// }
// handleMouseDown();
// }}
// onMouseUpCapture={() => {
// if (audioRef.current) {
// audioRef.current.muted = false;
// }
// handleMouseUp();
// }}
onClickCapture={() => {
if (audioRef.current) {
audioRef.current.muted = false;
}
toggleSpeech();
}}
type="button"
title={isSpeaking === true ? localize('com_ui_stop') : localize('com_ui_read_aloud')}
>
{renderIcon('19')}
</button>
<audio
ref={audioRef}
controls
preload="none"
controlsList="nodownload nofullscreen noremoteplayback"
style={{
position: 'absolute',
overflow: 'hidden',
display: 'none',
height: '0px',
width: '0px',
}}
src={audioRef.current?.src}
onError={(error) => {
console.error('Error fetching audio:', error);
}}
id={`audio-${messageId}`}
muted
autoPlay
/>
</>
);
const SelectedTTS = TTSComponents[engineTTS];
return <SelectedTTS {...props} />;
}
export default memo(MessageAudio);