1- import { useEffect , useMemo , useRef } from "react" ;
2- import { useReactive } from "ahooks" ;
1+ import { useEffect , useRef } from "react" ;
32import dayjs from "dayjs" ;
43import durationPlugin from "dayjs/plugin/duration" ;
54
@@ -16,77 +15,83 @@ import forwardLight from "@/assets/images/ReadAloud/forward-light.png";
1615import forwardDark from "@/assets/images/ReadAloud/forward-dark.png" ;
1716import closeLight from "@/assets/images/ReadAloud/close-light.png" ;
1817import closeDark from "@/assets/images/ReadAloud/close-dark.png" ;
18+ import { useConnectStore } from "@/stores/connectStore" ;
19+ import platformAdapter from "@/utils/platformAdapter" ;
20+ import { useStreamAudio } from "@/hooks/useStreamAudio" ;
21+ import { nanoid } from "nanoid" ;
22+ import { useChatStore } from "@/stores/chatStore" ;
1923
2024dayjs . extend ( durationPlugin ) ;
2125
22- interface State {
23- loading : boolean ;
24- playing : boolean ;
25- totalDuration : number ;
26- currentDuration : number ;
27- }
28-
29- const ReadAloud = ( ) => {
30- const isDark = useThemeStore ( ( state ) => state . isDark ) ;
31- const state = useReactive < State > ( {
32- loading : false ,
33- playing : true ,
34- totalDuration : 300 ,
35- currentDuration : 0 ,
26+ const Synthesize = ( ) => {
27+ const { isDark } = useThemeStore ( ) ;
28+ const { currentService } = useConnectStore ( ) ;
29+ const { synthesizeItem, setSynthesizeItem } = useChatStore ( ) ;
30+ const clientIdRef = useRef ( nanoid ( ) ) ;
31+
32+ const {
33+ loading,
34+ playing,
35+ currentTime,
36+ totalTime,
37+ audioRef,
38+ audioUrl,
39+ initMediaSource,
40+ toggle,
41+ seek,
42+ appendBuffer,
43+ onCanplay,
44+ onTimeupdate,
45+ onEnded,
46+ } = useStreamAudio ( {
47+ onSourceopen ( ) {
48+ return platformAdapter . invokeBackend ( "synthesize" , {
49+ clientId : clientIdRef . current ,
50+ serverId : currentService . id ,
51+ content : synthesizeItem ?. content ,
52+ voice : "longwan_v2" ,
53+ } ) ;
54+ } ,
3655 } ) ;
37- const timerRef = useRef < ReturnType < typeof setTimeout > > ( ) ;
38-
39- const formatTime = useMemo ( ( ) => {
40- return dayjs . duration ( state . currentDuration * 1000 ) . format ( "mm:ss" ) ;
41- } , [ state . currentDuration ] ) ;
4256
4357 useEffect ( ( ) => {
44- if ( state . playing && state . currentDuration >= state . totalDuration ) {
45- state . currentDuration = 0 ;
46- }
47-
48- changeCurrentDuration ( ) ;
49- } , [ state . playing ] ) ;
50-
51- const changeCurrentDuration = ( duration = state . currentDuration ) => {
52- clearTimeout ( timerRef . current ) ;
53-
54- let nextDuration = duration ;
58+ const id = nanoid ( ) ;
5559
56- if ( duration < 0 ) {
57- nextDuration = 0 ;
58- }
60+ clientIdRef . current = `synthesize-${ id } ` ;
5961
60- if ( duration >= state . totalDuration ) {
61- state . currentDuration = state . totalDuration ;
62+ initMediaSource ( ) ;
6263
63- state . playing = false ;
64- }
64+ const unlisten = platformAdapter . listenEvent (
65+ `synthesize-${ id } ` ,
66+ ( { payload } ) => {
67+ appendBuffer ( new Uint8Array ( payload ) ) ;
68+ }
69+ ) ;
6570
66- if ( ! state . playing ) return ;
67-
68- state . currentDuration = nextDuration ;
69-
70- timerRef . current = setTimeout ( ( ) => {
71- changeCurrentDuration ( duration + 1 ) ;
72- } , 1000 ) ;
73- } ;
71+ return ( ) => {
72+ unlisten . then ( ( unmount ) => unmount ( ) ) ;
73+ } ;
74+ } , [ synthesizeItem ?. id ] ) ;
7475
7576 return (
7677 < div className = "fixed top-[60px] left-1/2 z-1000 w-[200px] h-12 px-4 flex items-center justify-between -translate-x-1/2 border rounded-lg text-[#333] dark:text-[#D8D8D8] bg-white dark:bg-black dark:border-[#272828] shadow-[0_4px_8px_rgba(0,0,0,0.2)] dark:shadow-[0_4px_8px_rgba(255,255,255,0.15)]" >
78+ < audio
79+ ref = { audioRef }
80+ src = { audioUrl }
81+ onCanPlay = { onCanplay }
82+ onTimeUpdate = { onTimeupdate }
83+ onEnded = { onEnded }
84+ />
85+
7786 < div className = "flex items-center gap-2" >
78- { state . loading ? (
87+ { loading ? (
7988 < img
8089 src = { isDark ? loadingDark : loadingLight }
8190 className = "size-4 animate-spin"
8291 />
8392 ) : (
84- < div
85- onClick = { ( ) => {
86- state . playing = ! state . playing ;
87- } }
88- >
89- { state . playing ? (
93+ < div onClick = { toggle } >
94+ { playing ? (
9095 < img
9196 src = { isDark ? playDark : playLight }
9297 className = "size-4 cursor-pointer"
@@ -100,24 +105,28 @@ const ReadAloud = () => {
100105 </ div >
101106 ) }
102107
103- < span className = "text-sm" > { formatTime } </ span >
108+ { ! loading && (
109+ < span className = "text-sm" >
110+ { dayjs . duration ( currentTime * 1000 ) . format ( "mm:ss" ) }
111+ </ span >
112+ ) }
104113 </ div >
105114 < div className = "flex gap-3" >
106- { ! state . loading && (
115+ { ! loading && totalTime !== Infinity && (
107116 < >
108117 < img
109118 src = { isDark ? backDark : backLight }
110119 className = "size-4 cursor-pointer"
111120 onClick = { ( ) => {
112- changeCurrentDuration ( state . currentDuration - 15 ) ;
121+ seek ( currentTime - 15 ) ;
113122 } }
114123 />
115124
116125 < img
117126 src = { isDark ? forwardDark : forwardLight }
118127 className = "size-4 cursor-pointer"
119128 onClick = { ( ) => {
120- changeCurrentDuration ( state . currentDuration + 15 ) ;
129+ seek ( currentTime + 15 ) ;
121130 } }
122131 />
123132 </ >
@@ -126,10 +135,13 @@ const ReadAloud = () => {
126135 < img
127136 src = { isDark ? closeDark : closeLight }
128137 className = "size-4 cursor-pointer"
138+ onClick = { ( ) => {
139+ setSynthesizeItem ( void 0 ) ;
140+ } }
129141 />
130142 </ div >
131143 </ div >
132144 ) ;
133145} ;
134146
135- export default ReadAloud ;
147+ export default Synthesize ;
0 commit comments