mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-12 16:38:59 +08:00
### What problem does this PR solve? feat: Play audio #2088 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
97e4eccf03
commit
1a1888ed22
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -28,6 +28,7 @@
|
|||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mammoth": "^1.7.2",
|
"mammoth": "^1.7.2",
|
||||||
|
"openai-speech-stream-player": "^1.0.8",
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-force-graph": "^1.44.4",
|
"react-force-graph": "^1.44.4",
|
||||||
@ -20565,6 +20566,11 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openai-speech-stream-player": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/openai-speech-stream-player/-/openai-speech-stream-player-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-0SUybbhStl65s66ezh2QaoZE5k1kNb2t5M8tDOqJFILdHpwHaBqnYy4uHl3Hk/8F5VFWxxHaLamjKOnfNDKgbw=="
|
||||||
|
},
|
||||||
"node_modules/option": {
|
"node_modules/option": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mammoth": "^1.7.2",
|
"mammoth": "^1.7.2",
|
||||||
|
"openai-speech-stream-player": "^1.0.8",
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-force-graph": "^1.44.4",
|
"react-force-graph": "^1.44.4",
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DislikeOutlined,
|
DislikeOutlined,
|
||||||
LikeOutlined,
|
LikeOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
SoundOutlined,
|
SoundOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@ -13,7 +14,7 @@ import { useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SvgIcon from '../svg-icon';
|
import SvgIcon from '../svg-icon';
|
||||||
import FeedbackModal from './feedback-modal';
|
import FeedbackModal from './feedback-modal';
|
||||||
import { useRemoveMessage, useSendFeedback } from './hooks';
|
import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
|
||||||
import PromptModal from './prompt-modal';
|
import PromptModal from './prompt-modal';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@ -37,6 +38,7 @@ export const AssistantGroupButton = ({
|
|||||||
showModal: showPromptModal,
|
showModal: showPromptModal,
|
||||||
} = useSetModalState();
|
} = useSetModalState();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { handleRead, ref, isPlaying } = useSpeech(content);
|
||||||
|
|
||||||
const handleLike = useCallback(() => {
|
const handleLike = useCallback(() => {
|
||||||
onFeedbackOk({ thumbup: true });
|
onFeedbackOk({ thumbup: true });
|
||||||
@ -48,10 +50,11 @@ export const AssistantGroupButton = ({
|
|||||||
<Radio.Button value="a">
|
<Radio.Button value="a">
|
||||||
<CopyToClipboard text={content}></CopyToClipboard>
|
<CopyToClipboard text={content}></CopyToClipboard>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
<Radio.Button value="b">
|
<Radio.Button value="b" onClick={handleRead}>
|
||||||
<Tooltip title={t('chat.read')}>
|
<Tooltip title={t('chat.read')}>
|
||||||
<SoundOutlined />
|
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<audio src="" ref={ref}></audio>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
{showLikeButton && (
|
{showLikeButton && (
|
||||||
<>
|
<>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useDeleteMessage, useFeedback } from '@/hooks/chat-hooks';
|
import { useDeleteMessage, useFeedback } from '@/hooks/chat-hooks';
|
||||||
import { useSetModalState } from '@/hooks/common-hooks';
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { IRemoveMessageById } from '@/hooks/logic-hooks';
|
import { IRemoveMessageById, useSpeechWithSse } from '@/hooks/logic-hooks';
|
||||||
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
|
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
|
||||||
import { getMessagePureId } from '@/utils/chat';
|
import { getMessagePureId } from '@/utils/chat';
|
||||||
import { useCallback } from 'react';
|
import { SpeechPlayer } from 'openai-speech-stream-player';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
export const useSendFeedback = (messageId: string) => {
|
export const useSendFeedback = (messageId: string) => {
|
||||||
const { visible, hideModal, showModal } = useSetModalState();
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
@ -50,3 +51,52 @@ export const useRemoveMessage = (
|
|||||||
|
|
||||||
return { onRemoveMessage, loading };
|
return { onRemoveMessage, loading };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSpeech = (content: string) => {
|
||||||
|
const ref = useRef<HTMLAudioElement>(null);
|
||||||
|
const { read } = useSpeechWithSse();
|
||||||
|
const player = useRef<SpeechPlayer>();
|
||||||
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const initialize = useCallback(async () => {
|
||||||
|
player.current = new SpeechPlayer({
|
||||||
|
audio: ref.current!,
|
||||||
|
onPlaying: () => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
},
|
||||||
|
onPause: () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
},
|
||||||
|
onChunkEnd: () => {},
|
||||||
|
mimeType: 'audio/mpeg',
|
||||||
|
});
|
||||||
|
await player.current.init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const pause = useCallback(() => {
|
||||||
|
player.current?.pause();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const speech = useCallback(async () => {
|
||||||
|
const response = await read({ text: content });
|
||||||
|
if (response) {
|
||||||
|
player?.current?.feedWithResponse(response);
|
||||||
|
}
|
||||||
|
}, [read, content]);
|
||||||
|
|
||||||
|
const handleRead = useCallback(async () => {
|
||||||
|
if (isPlaying) {
|
||||||
|
setIsPlaying(false);
|
||||||
|
pause();
|
||||||
|
} else {
|
||||||
|
setIsPlaying(true);
|
||||||
|
speech();
|
||||||
|
}
|
||||||
|
}, [setIsPlaying, speech, isPlaying, pause]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initialize();
|
||||||
|
}, [initialize]);
|
||||||
|
|
||||||
|
return { ref, handleRead, isPlaying };
|
||||||
|
};
|
||||||
|
@ -278,6 +278,88 @@ export const useSendMessageWithSse = (
|
|||||||
return { send, answer, done, setDone };
|
return { send, answer, done, setDone };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSpeechWithSse = (url: string = api.tts) => {
|
||||||
|
const read = useCallback(
|
||||||
|
(body: any) => {
|
||||||
|
const response = fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
[Authorization]: getAuthorization(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
[url],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { read };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFetchAudioWithSse = (url: string = api.tts) => {
|
||||||
|
// const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
|
||||||
|
const [done, setDone] = useState(true);
|
||||||
|
|
||||||
|
const read = useCallback(
|
||||||
|
async (
|
||||||
|
body: any,
|
||||||
|
): Promise<{ response: Response; data: ResponseType } | undefined> => {
|
||||||
|
try {
|
||||||
|
setDone(false);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
[Authorization]: getAuthorization(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = response.clone().json();
|
||||||
|
|
||||||
|
const reader = response?.body?.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const x = await reader?.read();
|
||||||
|
if (x) {
|
||||||
|
const { done, value } = x;
|
||||||
|
try {
|
||||||
|
// const val = JSON.parse(value || '');
|
||||||
|
const val = value;
|
||||||
|
// const d = val?.data;
|
||||||
|
// if (typeof d !== 'boolean') {
|
||||||
|
// console.info('data:', d);
|
||||||
|
// setAnswer({
|
||||||
|
// ...d,
|
||||||
|
// conversationId: body?.conversation_id,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
console.info('done');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info('done?');
|
||||||
|
setDone(true);
|
||||||
|
// setAnswer({} as IAnswer);
|
||||||
|
return { data: await res, response };
|
||||||
|
} catch (e) {
|
||||||
|
setDone(true);
|
||||||
|
// setAnswer({} as IAnswer);
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[url],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { read, done, setDone };
|
||||||
|
};
|
||||||
|
|
||||||
//#region chat hooks
|
//#region chat hooks
|
||||||
|
|
||||||
export const useScrollToBottom = (messages?: unknown) => {
|
export const useScrollToBottom = (messages?: unknown) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user