@@ -25,20 +25,13 @@ let globalAudio: HTMLAudioElement | null = null
2525
2626/**
2727 * Clean text for TTS - removes emojis, markdown, HTML tags, etc.
28- * Based on Nora's clean_text_for_tts function
2928 */
3029function cleanTextForTTS ( text : string ) : string {
3130 if ( ! text ) return ''
3231
3332 let cleaned = text
3433
3534 // Remove ALL emojis (Unicode ranges)
36- // Basic Emoji: 1F600-1F64F (emoticons)
37- // Misc Symbols: 2600-26FF
38- // Dingbats: 2700-27BF
39- // Misc Symbols Extended: 1F900-1F9FF
40- // Supplemental Symbols: 1F300-1F5FF
41- // Transport & Map: 1F680-1F6FF
4235 cleaned = cleaned . replace ( / [ \u{1F300} - \u{1F9FF} ] | [ \u{2600} - \u{26FF} ] | [ \u{2700} - \u{27BF} ] / gu, '' )
4336
4437 // Remove emoji shortcodes like :smile: :thumbsup:
@@ -48,12 +41,12 @@ function cleanTextForTTS(text: string): string {
4841 cleaned = cleaned . replace ( / < [ ^ > ] * > / g, '' )
4942
5043 // Remove Markdown bold/italic markers
51- cleaned = cleaned . replace ( / \* \* \* / g, '' ) // Bold + Italic (***text***)
52- cleaned = cleaned . replace ( / \* \* / g, '' ) // Bold (**text**)
53- cleaned = cleaned . replace ( / \* / g, '' ) // Italic (*text*)
54- cleaned = cleaned . replace ( / _ _ _ / g, '' ) // Bold + Italic (___text___)
55- cleaned = cleaned . replace ( / _ _ / g, '' ) // Bold (__text__)
56- cleaned = cleaned . replace ( / _ / g, ' ' ) // Italic (_text_) - replace with space to avoid joined words
44+ cleaned = cleaned . replace ( / \* \* \* / g, '' )
45+ cleaned = cleaned . replace ( / \* \* / g, '' )
46+ cleaned = cleaned . replace ( / \* / g, '' )
47+ cleaned = cleaned . replace ( / _ _ _ / g, '' )
48+ cleaned = cleaned . replace ( / _ _ / g, '' )
49+ cleaned = cleaned . replace ( / _ / g, ' ' )
5750
5851 // Remove Markdown links but keep the text: [text](url) -> text
5952 cleaned = cleaned . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] + \) / g, '$1' )
@@ -99,73 +92,42 @@ export function useTTSAutoPlay(messages: MessageOrDateBlock[] | undefined, isBot
9992 const initialMessageCountRef = useRef < number | null > ( null )
10093
10194 // Track when we started waiting for messages (when isBot became true with empty messages)
102- // This allows us to play TTS for messages that arrive after we opened the thread
10395 const waitingForMessagesSinceRef = useRef < number | null > ( null )
10496
10597 const { call } = useFrappePostCall < TTSResponse > ( "nora.api.tts.generate_audio" )
10698
10799 useEffect ( ( ) => {
108- // Debug logging - include first message name to identify which channel/thread
109- const firstMsgName = messages ?. [ 0 ] ?. name ?? 'none'
110- console . log ( '[TTS AutoPlay] Hook triggered' , { ttsEnabled, isBot, messageCount : messages ?. length , firstMsg : firstMsgName } )
111-
112- // Show debug indicator in dev/debug mode
113- if ( typeof window !== 'undefined' && ( window as any ) . __TTS_DEBUG__ ) {
114- const debugDiv = document . createElement ( 'div' )
115- debugDiv . style . cssText = 'position:fixed;top:10px;right:10px;background:#333;color:#fff;padding:8px;border-radius:4px;z-index:99999;font-size:12px;'
116- debugDiv . textContent = `TTS: enabled=${ ttsEnabled } , isBot=${ isBot } , msgs=${ messages ?. length } `
117- document . body . appendChild ( debugDiv )
118- setTimeout ( ( ) => debugDiv . remove ( ) , 2000 )
119- }
120-
121- // Only process if TTS is enabled and this is a bot channel
122100 if ( ! ttsEnabled || ! isBot ) {
123- console . log ( '[TTS AutoPlay] Skipping - ttsEnabled:' , ttsEnabled , 'isBot:' , isBot )
124101 return
125102 }
126103
127- // Filter out date blocks to get only actual messages
128104 const actualMessages = messages ?. filter (
129105 ( m ) : m is Message => m . message_type !== 'date' && m . message_type !== 'System'
130106 )
131107
132108 if ( ! actualMessages || actualMessages . length === 0 ) {
133- console . log ( '[TTS AutoPlay] No actual messages after filtering, raw count:' , messages ?. length ?? 0 )
134109 return
135110 }
136111
137- // On first run with messages, store the initial count
138- // BUT check if there's a recent bot message (within 30 seconds) that should be played
139- // This handles the case where a new thread opens and the bot has just responded
112+ // On first run with messages, store the initial count and check for recent bot message
140113 if ( initialMessageCountRef . current === null ) {
141114 initialMessageCountRef . current = actualMessages . length
142115
143- // Find the most recent bot message (not just the latest message, which could be user's)
144116 const latestBotMessage = [ ...actualMessages ] . reverse ( ) . find ( m => m . is_bot_message === 1 && m . text )
145117 const latestMessage = actualMessages [ actualMessages . length - 1 ]
146118
147- console . log ( '[TTS AutoPlay] Initial load, storing count:' , actualMessages . length , 'latestBotMessage:' , latestBotMessage ?. name ?? 'none' )
148-
149- // Check if there's a recent bot message that should be played
150119 if ( latestBotMessage && globalLastProcessedMessage !== latestBotMessage . name && ! globalIsPlaying ) {
151120 const messageTime = new Date ( latestBotMessage . creation ) . getTime ( )
152121 const now = Date . now ( )
153122 const ageMs = now - messageTime
154123
155- // Message is "recent" if either:
156- // 1. It was created in the last 30 seconds, OR
157- // 2. It was created after we started waiting for messages (thread just opened)
158124 const wasWaitingForMessages = waitingForMessagesSinceRef . current !== null
159125 const arrivedAfterWaiting = wasWaitingForMessages && messageTime >= waitingForMessagesSinceRef . current !
160126 const isRecent = ageMs < 30000 || arrivedAfterWaiting
161127
162- console . log ( '[TTS AutoPlay] Initial load with bot message, isRecent:' , isRecent , 'age:' , ageMs , 'ms, wasWaiting:' , wasWaitingForMessages , 'arrivedAfter:' , arrivedAfterWaiting )
163-
164- // Clear waiting state
165128 waitingForMessagesSinceRef . current = null
166129
167130 if ( isRecent ) {
168- // Play TTS for this recent bot message
169131 globalLastProcessedMessage = latestBotMessage . name
170132 globalIsPlaying = true
171133
@@ -175,116 +137,85 @@ export function useTTSAutoPlay(messages: MessageOrDateBlock[] | undefined, isBot
175137 const cleanedText = cleanTextForTTS ( rawText )
176138
177139 if ( cleanedText . trim ( ) ) {
178- console . log ( '[TTS AutoPlay] Playing initial bot message:' , cleanedText . substring ( 0 , 50 ) + '...' )
179140 call ( { text : cleanedText , voice : ttsVoice } )
180141 . then ( ( response ) => {
181- console . log ( '[TTS AutoPlay] TTS API response:' , response )
182142 const data = ( response as any ) ?. message ?? response
183143 if ( data ?. success && data ?. audio_url ) {
184- console . log ( '[TTS AutoPlay] Got audio URL:' , data . audio_url )
185144 if ( globalAudio ) {
186145 globalAudio . pause ( )
187146 globalAudio = null
188147 }
189148 const audio = new Audio ( data . audio_url )
190149 globalAudio = audio
191150 audio . onended = ( ) => {
192- console . log ( '[TTS AutoPlay] Audio playback ended' )
193151 globalIsPlaying = false
194152 globalAudio = null
195153 }
196- audio . onerror = ( e ) => {
197- console . error ( '[TTS AutoPlay] Audio error:' , e )
154+ audio . onerror = ( ) => {
198155 globalIsPlaying = false
199156 globalAudio = null
200157 }
201- audio . play ( )
202- . then ( ( ) => console . log ( '[TTS AutoPlay] Audio playing successfully' ) )
203- . catch ( ( err ) => {
204- console . error ( '[TTS AutoPlay] Failed to play:' , err . name , err . message )
205- globalIsPlaying = false
206- globalAudio = null
207- } )
158+ audio . play ( ) . catch ( ( ) => {
159+ globalIsPlaying = false
160+ globalAudio = null
161+ } )
208162 } else {
209- console . log ( '[TTS AutoPlay] TTS API returned no audio:' , data )
210163 globalIsPlaying = false
211164 }
212165 } )
213- . catch ( ( err ) => {
214- console . error ( '[TTS AutoPlay] TTS API call failed:' , err )
166+ . catch ( ( ) => {
215167 globalIsPlaying = false
216168 } )
217169 } else {
218170 globalIsPlaying = false
219171 }
220- // Mark the latest message as processed to avoid processing it again
221172 globalLastProcessedMessage = latestMessage . name
222173 return
223174 }
224175 }
225176
226- // No recent bot message, just mark the latest as processed
227177 globalLastProcessedMessage = latestMessage . name
228- // Clear any waiting state
229178 waitingForMessagesSinceRef . current = null
230179 return
231180 }
232181
233182 // Only process if we have more messages than initial (new message arrived)
234183 if ( actualMessages . length <= initialMessageCountRef . current ) {
235- console . log ( '[TTS AutoPlay] No new messages, current:' , actualMessages . length , 'initial:' , initialMessageCountRef . current )
236184 return
237185 }
238186
239- console . log ( '[TTS AutoPlay] New message detected! Count:' , actualMessages . length , 'vs initial:' , initialMessageCountRef . current )
240-
241- // Get the most recent message (messages are sorted oldest-to-newest, so newest is at the end)
242187 const latestMessage = actualMessages [ actualMessages . length - 1 ]
243188
244- // Skip if we've already processed this message (global check across all instances)
245189 if ( globalLastProcessedMessage === latestMessage . name ) {
246190 return
247191 }
248192
249- // Only process bot messages with text content
250- // is_bot_message is 1 or 0, not boolean
251193 if ( latestMessage . is_bot_message !== 1 || ! latestMessage . text ) {
252194 globalLastProcessedMessage = latestMessage . name
253195 return
254196 }
255197
256- // Skip if audio is already playing globally (across all instances)
257198 if ( globalIsPlaying ) {
258199 return
259200 }
260201
261- // Mark as processed globally
262202 globalLastProcessedMessage = latestMessage . name
263203
264- // Extract plain text from HTML content and clean for TTS
265204 const tempDiv = document . createElement ( "div" )
266205 tempDiv . innerHTML = latestMessage . text
267206 const rawText = tempDiv . textContent || tempDiv . innerText || ""
268-
269- // Clean text: remove emojis, markdown, etc.
270207 const cleanedText = cleanTextForTTS ( rawText )
271208
272209 if ( ! cleanedText . trim ( ) ) {
273210 return
274211 }
275212
276- // Generate and play TTS
277213 globalIsPlaying = true
278- console . log ( '[TTS AutoPlay] Calling TTS API with text:' , cleanedText . substring ( 0 , 50 ) + '...' )
279214
280215 call ( { text : cleanedText , voice : ttsVoice } )
281216 . then ( ( response ) => {
282- console . log ( '[TTS AutoPlay] TTS API response (new msg):' , response )
283- // Frappe API wraps response in 'message' key
284217 const data = ( response as any ) ?. message ?? response
285218 if ( data ?. success && data ?. audio_url ) {
286- console . log ( '[TTS AutoPlay] Got audio URL (new msg):' , data . audio_url )
287- // Stop any currently playing audio
288219 if ( globalAudio ) {
289220 globalAudio . pause ( )
290221 globalAudio = null
@@ -294,81 +225,55 @@ export function useTTSAutoPlay(messages: MessageOrDateBlock[] | undefined, isBot
294225 globalAudio = audio
295226
296227 audio . onended = ( ) => {
297- console . log ( '[TTS AutoPlay] Audio playback ended' )
298228 globalIsPlaying = false
299229 globalAudio = null
300230 }
301231
302- audio . onerror = ( e ) => {
303- console . error ( '[TTS AutoPlay] Audio error:' , e )
232+ audio . onerror = ( ) => {
304233 globalIsPlaying = false
305234 globalAudio = null
306235 }
307236
308- audio . play ( )
309- . then ( ( ) => console . log ( '[TTS AutoPlay] Audio playing successfully' ) )
310- . catch ( ( error ) => {
311- console . error ( '[TTS AutoPlay] Failed to play audio:' , error . name , error . message )
312- globalIsPlaying = false
313- globalAudio = null
314- } )
237+ audio . play ( ) . catch ( ( ) => {
238+ globalIsPlaying = false
239+ globalAudio = null
240+ } )
315241 } else {
316- console . log ( '[TTS AutoPlay] TTS API returned no audio (new msg):' , data )
317242 globalIsPlaying = false
318243 }
319244 } )
320- . catch ( ( err ) => {
321- console . error ( '[TTS AutoPlay] TTS API call failed (new msg):' , err )
245+ . catch ( ( ) => {
322246 globalIsPlaying = false
323247 } )
324248 } , [ messages , isBot , ttsEnabled , ttsVoice , call ] )
325249
326- // Track previous isBot value to detect transitions
327250 const previousIsBotRef = useRef < boolean > ( isBot )
328251
329- // Reset initial count when channel changes (messages becomes undefined or empty)
330- // OR when isBot transitions from false to true (e.g., when channelMembers loads)
331252 useEffect ( ( ) => {
332253 if ( ! messages || messages . length === 0 ) {
333254 initialMessageCountRef . current = null
334255 }
335256 } , [ messages ] )
336257
337258 // Handle when isBot changes from false to true
338- // This handles the case where channelMembers loads AFTER messages (e.g., new thread creation)
339259 useEffect ( ( ) => {
340260 if ( isBot && ! previousIsBotRef . current && ttsEnabled ) {
341- console . log ( '[TTS AutoPlay] isBot transitioned to true, checking for recent bot message' )
342-
343- // Filter to actual messages
344261 const actualMessages = messages ?. filter (
345262 ( m ) : m is Message => m . message_type !== 'date' && m . message_type !== 'System'
346263 )
347264
348- console . log ( '[TTS AutoPlay] Messages available:' , actualMessages ?. length ?? 0 , 'messages:' , actualMessages ?. map ( m => ( { name : m . name , is_bot : m . is_bot_message , owner : m . owner } ) ) )
349-
350- // If messages are empty, start waiting for them
351- // This timestamp will be used to determine if incoming messages should trigger TTS
352265 if ( ! actualMessages || actualMessages . length === 0 ) {
353- console . log ( '[TTS AutoPlay] No messages yet, starting to wait...' )
354266 waitingForMessagesSinceRef . current = Date . now ( )
355267 } else if ( actualMessages && actualMessages . length > 0 ) {
356- // Clear any waiting state since we already have messages
357268 waitingForMessagesSinceRef . current = null
358- // Find the most recent bot message
359269 const latestBotMessage = [ ...actualMessages ] . reverse ( ) . find ( m => m . is_bot_message === 1 && m . text )
360- console . log ( '[TTS AutoPlay] Latest bot message found:' , latestBotMessage ? latestBotMessage . name : 'none' )
361270
362271 if ( latestBotMessage && globalLastProcessedMessage !== latestBotMessage . name && ! globalIsPlaying ) {
363- // Check if this message is recent (within last 30 seconds)
364272 const messageTime = new Date ( latestBotMessage . creation ) . getTime ( )
365273 const now = Date . now ( )
366274 const isRecent = ( now - messageTime ) < 30000
367275
368- console . log ( '[TTS AutoPlay] Found bot message, isRecent:' , isRecent , 'age:' , now - messageTime , 'ms' )
369-
370276 if ( isRecent ) {
371- // Play TTS for this message
372277 globalLastProcessedMessage = latestBotMessage . name
373278 globalIsPlaying = true
374279
@@ -378,7 +283,6 @@ export function useTTSAutoPlay(messages: MessageOrDateBlock[] | undefined, isBot
378283 const cleanedText = cleanTextForTTS ( rawText )
379284
380285 if ( cleanedText . trim ( ) ) {
381- console . log ( '[TTS AutoPlay] Playing missed bot message:' , cleanedText . substring ( 0 , 50 ) + '...' )
382286 call ( { text : cleanedText , voice : ttsVoice } )
383287 . then ( ( response ) => {
384288 const data = ( response as any ) ?. message ?? response
@@ -414,7 +318,6 @@ export function useTTSAutoPlay(messages: MessageOrDateBlock[] | undefined, isBot
414318 }
415319 }
416320
417- // Set initial count to current so future messages trigger normally
418321 initialMessageCountRef . current = actualMessages . length
419322 }
420323 }
0 commit comments