balibabu commited on
Commit
a7e962d
·
1 Parent(s): b4fdc97

feat: Play audio #2088 (#2200)

Browse files

### What problem does this PR solve?
feat: Play audio #2088


### Type of change


- [x] New Feature (non-breaking change which adds functionality)

web/package-lock.json CHANGED
@@ -28,6 +28,7 @@
28
  "jsencrypt": "^3.3.2",
29
  "lodash": "^4.17.21",
30
  "mammoth": "^1.7.2",
 
31
  "rc-tween-one": "^3.0.6",
32
  "react-copy-to-clipboard": "^5.1.0",
33
  "react-force-graph": "^1.44.4",
@@ -20565,6 +20566,11 @@
20565
  "node": ">=12"
20566
  }
20567
  },
 
 
 
 
 
20568
  "node_modules/option": {
20569
  "version": "0.2.4",
20570
  "resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
 
28
  "jsencrypt": "^3.3.2",
29
  "lodash": "^4.17.21",
30
  "mammoth": "^1.7.2",
31
+ "openai-speech-stream-player": "^1.0.8",
32
  "rc-tween-one": "^3.0.6",
33
  "react-copy-to-clipboard": "^5.1.0",
34
  "react-force-graph": "^1.44.4",
 
20566
  "node": ">=12"
20567
  }
20568
  },
20569
+ "node_modules/openai-speech-stream-player": {
20570
+ "version": "1.0.8",
20571
+ "resolved": "https://registry.npmmirror.com/openai-speech-stream-player/-/openai-speech-stream-player-1.0.8.tgz",
20572
+ "integrity": "sha512-0SUybbhStl65s66ezh2QaoZE5k1kNb2t5M8tDOqJFILdHpwHaBqnYy4uHl3Hk/8F5VFWxxHaLamjKOnfNDKgbw=="
20573
+ },
20574
  "node_modules/option": {
20575
  "version": "0.2.4",
20576
  "resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
web/package.json CHANGED
@@ -39,6 +39,7 @@
39
  "jsencrypt": "^3.3.2",
40
  "lodash": "^4.17.21",
41
  "mammoth": "^1.7.2",
 
42
  "rc-tween-one": "^3.0.6",
43
  "react-copy-to-clipboard": "^5.1.0",
44
  "react-force-graph": "^1.44.4",
 
39
  "jsencrypt": "^3.3.2",
40
  "lodash": "^4.17.21",
41
  "mammoth": "^1.7.2",
42
+ "openai-speech-stream-player": "^1.0.8",
43
  "rc-tween-one": "^3.0.6",
44
  "react-copy-to-clipboard": "^5.1.0",
45
  "react-force-graph": "^1.44.4",
web/src/components/message-item/group-button.tsx CHANGED
@@ -5,6 +5,7 @@ import {
5
  DeleteOutlined,
6
  DislikeOutlined,
7
  LikeOutlined,
 
8
  SoundOutlined,
9
  SyncOutlined,
10
  } from '@ant-design/icons';
@@ -13,7 +14,7 @@ import { useCallback } from 'react';
13
  import { useTranslation } from 'react-i18next';
14
  import SvgIcon from '../svg-icon';
15
  import FeedbackModal from './feedback-modal';
16
- import { useRemoveMessage, useSendFeedback } from './hooks';
17
  import PromptModal from './prompt-modal';
18
 
19
  interface IProps {
@@ -37,6 +38,7 @@ export const AssistantGroupButton = ({
37
  showModal: showPromptModal,
38
  } = useSetModalState();
39
  const { t } = useTranslation();
 
40
 
41
  const handleLike = useCallback(() => {
42
  onFeedbackOk({ thumbup: true });
@@ -48,10 +50,11 @@ export const AssistantGroupButton = ({
48
  <Radio.Button value="a">
49
  <CopyToClipboard text={content}></CopyToClipboard>
50
  </Radio.Button>
51
- <Radio.Button value="b">
52
  <Tooltip title={t('chat.read')}>
53
- <SoundOutlined />
54
  </Tooltip>
 
55
  </Radio.Button>
56
  {showLikeButton && (
57
  <>
 
5
  DeleteOutlined,
6
  DislikeOutlined,
7
  LikeOutlined,
8
+ PauseCircleOutlined,
9
  SoundOutlined,
10
  SyncOutlined,
11
  } from '@ant-design/icons';
 
14
  import { useTranslation } from 'react-i18next';
15
  import SvgIcon from '../svg-icon';
16
  import FeedbackModal from './feedback-modal';
17
+ import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
18
  import PromptModal from './prompt-modal';
19
 
20
  interface IProps {
 
38
  showModal: showPromptModal,
39
  } = useSetModalState();
40
  const { t } = useTranslation();
41
+ const { handleRead, ref, isPlaying } = useSpeech(content);
42
 
43
  const handleLike = useCallback(() => {
44
  onFeedbackOk({ thumbup: true });
 
50
  <Radio.Button value="a">
51
  <CopyToClipboard text={content}></CopyToClipboard>
52
  </Radio.Button>
53
+ <Radio.Button value="b" onClick={handleRead}>
54
  <Tooltip title={t('chat.read')}>
55
+ {isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
56
  </Tooltip>
57
+ <audio src="" ref={ref}></audio>
58
  </Radio.Button>
59
  {showLikeButton && (
60
  <>
web/src/components/message-item/hooks.ts CHANGED
@@ -1,9 +1,10 @@
1
  import { useDeleteMessage, useFeedback } from '@/hooks/chat-hooks';
2
  import { useSetModalState } from '@/hooks/common-hooks';
3
- import { IRemoveMessageById } from '@/hooks/logic-hooks';
4
  import { IFeedbackRequestBody } from '@/interfaces/request/chat';
5
  import { getMessagePureId } from '@/utils/chat';
6
- import { useCallback } from 'react';
 
7
 
8
  export const useSendFeedback = (messageId: string) => {
9
  const { visible, hideModal, showModal } = useSetModalState();
@@ -50,3 +51,52 @@ export const useRemoveMessage = (
50
 
51
  return { onRemoveMessage, loading };
52
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { useDeleteMessage, useFeedback } from '@/hooks/chat-hooks';
2
  import { useSetModalState } from '@/hooks/common-hooks';
3
+ import { IRemoveMessageById, useSpeechWithSse } from '@/hooks/logic-hooks';
4
  import { IFeedbackRequestBody } from '@/interfaces/request/chat';
5
  import { getMessagePureId } from '@/utils/chat';
6
+ import { SpeechPlayer } from 'openai-speech-stream-player';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
 
9
  export const useSendFeedback = (messageId: string) => {
10
  const { visible, hideModal, showModal } = useSetModalState();
 
51
 
52
  return { onRemoveMessage, loading };
53
  };
54
+
55
+ export const useSpeech = (content: string) => {
56
+ const ref = useRef<HTMLAudioElement>(null);
57
+ const { read } = useSpeechWithSse();
58
+ const player = useRef<SpeechPlayer>();
59
+ const [isPlaying, setIsPlaying] = useState<boolean>(false);
60
+
61
+ const initialize = useCallback(async () => {
62
+ player.current = new SpeechPlayer({
63
+ audio: ref.current!,
64
+ onPlaying: () => {
65
+ setIsPlaying(true);
66
+ },
67
+ onPause: () => {
68
+ setIsPlaying(false);
69
+ },
70
+ onChunkEnd: () => {},
71
+ mimeType: 'audio/mpeg',
72
+ });
73
+ await player.current.init();
74
+ }, []);
75
+
76
+ const pause = useCallback(() => {
77
+ player.current?.pause();
78
+ }, []);
79
+
80
+ const speech = useCallback(async () => {
81
+ const response = await read({ text: content });
82
+ if (response) {
83
+ player?.current?.feedWithResponse(response);
84
+ }
85
+ }, [read, content]);
86
+
87
+ const handleRead = useCallback(async () => {
88
+ if (isPlaying) {
89
+ setIsPlaying(false);
90
+ pause();
91
+ } else {
92
+ setIsPlaying(true);
93
+ speech();
94
+ }
95
+ }, [setIsPlaying, speech, isPlaying, pause]);
96
+
97
+ useEffect(() => {
98
+ initialize();
99
+ }, [initialize]);
100
+
101
+ return { ref, handleRead, isPlaying };
102
+ };
web/src/hooks/logic-hooks.ts CHANGED
@@ -278,6 +278,88 @@ export const useSendMessageWithSse = (
278
  return { send, answer, done, setDone };
279
  };
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  //#region chat hooks
282
 
283
  export const useScrollToBottom = (messages?: unknown) => {
 
278
  return { send, answer, done, setDone };
279
  };
280
 
281
+ export const useSpeechWithSse = (url: string = api.tts) => {
282
+ const read = useCallback(
283
+ (body: any) => {
284
+ const response = fetch(url, {
285
+ method: 'POST',
286
+ headers: {
287
+ [Authorization]: getAuthorization(),
288
+ 'Content-Type': 'application/json',
289
+ },
290
+ body: JSON.stringify(body),
291
+ });
292
+ return response;
293
+ },
294
+ [url],
295
+ );
296
+
297
+ return { read };
298
+ };
299
+
300
+ export const useFetchAudioWithSse = (url: string = api.tts) => {
301
+ // const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
302
+ const [done, setDone] = useState(true);
303
+
304
+ const read = useCallback(
305
+ async (
306
+ body: any,
307
+ ): Promise<{ response: Response; data: ResponseType } | undefined> => {
308
+ try {
309
+ setDone(false);
310
+ const response = await fetch(url, {
311
+ method: 'POST',
312
+ headers: {
313
+ [Authorization]: getAuthorization(),
314
+ 'Content-Type': 'application/json',
315
+ },
316
+ body: JSON.stringify(body),
317
+ });
318
+
319
+ const res = response.clone().json();
320
+
321
+ const reader = response?.body?.getReader();
322
+
323
+ while (true) {
324
+ const x = await reader?.read();
325
+ if (x) {
326
+ const { done, value } = x;
327
+ try {
328
+ // const val = JSON.parse(value || '');
329
+ const val = value;
330
+ // const d = val?.data;
331
+ // if (typeof d !== 'boolean') {
332
+ // console.info('data:', d);
333
+ // setAnswer({
334
+ // ...d,
335
+ // conversationId: body?.conversation_id,
336
+ // });
337
+ // }
338
+ } catch (e) {
339
+ console.warn(e);
340
+ }
341
+ if (done) {
342
+ console.info('done');
343
+ break;
344
+ }
345
+ }
346
+ }
347
+ console.info('done?');
348
+ setDone(true);
349
+ // setAnswer({} as IAnswer);
350
+ return { data: await res, response };
351
+ } catch (e) {
352
+ setDone(true);
353
+ // setAnswer({} as IAnswer);
354
+ console.warn(e);
355
+ }
356
+ },
357
+ [url],
358
+ );
359
+
360
+ return { read, done, setDone };
361
+ };
362
+
363
  //#region chat hooks
364
 
365
  export const useScrollToBottom = (messages?: unknown) => {