| |
| import { useEffect } from 'react'; |
| import { useRecoilValue } from 'recoil'; |
| import type { TMessageAudio } from '~/common'; |
| import { VolumeIcon, VolumeMuteIcon, Spinner } from '@librechat/client'; |
| import { useLocalize, useTTSBrowser, useTTSExternal } from '~/hooks'; |
| import { logger } from '~/utils'; |
| import store from '~/store'; |
|
|
| export function BrowserTTS({ |
| isLast, |
| index, |
| messageId, |
| content, |
| className, |
| renderButton, |
| }: TMessageAudio) { |
| const localize = useLocalize(); |
| const playbackRate = useRecoilValue(store.playbackRate); |
|
|
| const { toggleSpeech, isSpeaking, isLoading, audioRef } = useTTSBrowser({ |
| isLast, |
| index, |
| messageId, |
| content, |
| }); |
|
|
| const renderIcon = () => { |
| if (isLoading === true) { |
| return <Spinner className="icon-md-heavy h-[18px] w-[18px]" />; |
| } |
|
|
| if (isSpeaking === true) { |
| return <VolumeMuteIcon className="icon-md-heavy h-[18px] w-[18px]" />; |
| } |
|
|
| return <VolumeIcon className="icon-md-heavy h-[18px] w-[18px]" />; |
| }; |
|
|
| 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, |
| ); |
|
|
| const handleClick = () => { |
| if (audioRef.current) { |
| audioRef.current.muted = false; |
| } |
| toggleSpeech(); |
| }; |
|
|
| const title = isSpeaking === true ? localize('com_ui_stop') : localize('com_ui_read_aloud'); |
|
|
| return ( |
| <> |
| {renderButton ? ( |
| renderButton({ |
| onClick: handleClick, |
| title: title, |
| icon: renderIcon(), |
| isActive: isSpeaking, |
| className, |
| }) |
| ) : ( |
| <button className={className} onClickCapture={handleClick} type="button" title={title}> |
| {renderIcon()} |
| </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) => { |
| logger.error('Error fetching audio:', error); |
| }} |
| id={`audio-${messageId}`} |
| autoPlay |
| /> |
| </> |
| ); |
| } |
|
|
| export function ExternalTTS({ |
| isLast, |
| index, |
| messageId, |
| content, |
| className, |
| renderButton, |
| }: TMessageAudio) { |
| const localize = useLocalize(); |
| const playbackRate = useRecoilValue(store.playbackRate); |
|
|
| const { toggleSpeech, isSpeaking, isLoading, audioRef } = useTTSExternal({ |
| isLast, |
| index, |
| messageId, |
| content, |
| }); |
|
|
| const renderIcon = () => { |
| if (isLoading === true) { |
| return <Spinner className="icon-md-heavy h-[18px] w-[18px]" />; |
| } |
|
|
| if (isSpeaking === true) { |
| return <VolumeMuteIcon className="icon-md-heavy h-[18px] w-[18px]" />; |
| } |
|
|
| return <VolumeIcon className="icon-md-heavy h-[18px] w-[18px]" />; |
| }; |
|
|
| 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 ( |
| <> |
| {renderButton ? ( |
| renderButton({ |
| onClick: () => { |
| if (audioRef.current) { |
| audioRef.current.muted = false; |
| } |
| toggleSpeech(); |
| }, |
| title: isSpeaking === true ? localize('com_ui_stop') : localize('com_ui_read_aloud'), |
| icon: renderIcon(), |
| isActive: isSpeaking, |
| className, |
| }) |
| ) : ( |
| <button |
| onClickCapture={() => { |
| if (audioRef.current) { |
| audioRef.current.muted = false; |
| } |
| toggleSpeech(); |
| }} |
| type="button" |
| title={isSpeaking === true ? localize('com_ui_stop') : localize('com_ui_read_aloud')} |
| > |
| {renderIcon()} |
| </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) => { |
| logger.error('Error fetching audio:', error); |
| }} |
| id={`audio-${messageId}`} |
| autoPlay |
| /> |
| </> |
| ); |
| } |
|
|