JenishMakwana commited on
Commit
28674f1
·
1 Parent(s): 320a9c2

feat: implement real-time text-to-speech highlighting and document management within chat interface

Browse files
backend/app/api/api_v1/endpoints/chat.py CHANGED
@@ -303,20 +303,35 @@ async def speak(request: Request, speak_data: SpeakRequest):
303
 
304
  # Internal stop signal for THIS specific request
305
  import threading
 
306
  disconnect_event = threading.Event()
307
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  # Generator wrapper to monitor disconnection
309
  async def disconnect_monitor_gen():
310
  generator = stream_tts_wav_chunks(clean_text, disconnect_event)
311
  try:
312
  for chunk in generator:
313
- if await request.is_disconnected():
314
- disconnect_event.set()
315
  break
316
  yield chunk
317
  except Exception as e:
318
  disconnect_event.set()
319
  raise e
 
 
 
320
 
321
  return StreamingResponse(
322
  disconnect_monitor_gen(),
 
303
 
304
  # Internal stop signal for THIS specific request
305
  import threading
306
+ import asyncio
307
  disconnect_event = threading.Event()
308
 
309
+ async def watch_disconnect():
310
+ try:
311
+ while not disconnect_event.is_set():
312
+ if await request.is_disconnected():
313
+ disconnect_event.set()
314
+ break
315
+ await asyncio.sleep(0.1)
316
+ except asyncio.CancelledError:
317
+ pass
318
+
319
+ watch_task = asyncio.create_task(watch_disconnect())
320
+
321
  # Generator wrapper to monitor disconnection
322
  async def disconnect_monitor_gen():
323
  generator = stream_tts_wav_chunks(clean_text, disconnect_event)
324
  try:
325
  for chunk in generator:
326
+ if disconnect_event.is_set():
 
327
  break
328
  yield chunk
329
  except Exception as e:
330
  disconnect_event.set()
331
  raise e
332
+ finally:
333
+ disconnect_event.set()
334
+ watch_task.cancel()
335
 
336
  return StreamingResponse(
337
  disconnect_monitor_gen(),
backend/app/services/tts.py CHANGED
@@ -12,6 +12,7 @@ SPEECH_SPEED = 1 # Slower than default 1.0
12
 
13
  # Global event to stop any ongoing TTS generation across streams
14
  stop_tts_event = threading.Event()
 
15
 
16
  def clean_text_for_speech(text: str) -> str:
17
  """Removes citations and other non-spoken markers from text, and formats dates."""
@@ -109,7 +110,7 @@ def stream_tts_wav_chunks(text, cancel_event=None):
109
 
110
  if not raw_sentences:
111
  return
112
-
113
  import soundfile as sf
114
  import base64
115
  import json
@@ -127,12 +128,13 @@ def stream_tts_wav_chunks(text, cancel_event=None):
127
  if not s_clean:
128
  continue
129
 
130
- generator = pipeline(s_clean, voice=voice, speed=SPEECH_SPEED)
131
- sentence_audio_chunks = []
132
- for gs, ps, audio in generator:
133
- if (cancel_event and cancel_event.is_set()) or stop_tts_event.is_set() or local_stop_signal.is_set():
134
- break
135
- sentence_audio_chunks.append(audio)
 
136
 
137
  if sentence_audio_chunks:
138
  full_audio = np.concatenate(sentence_audio_chunks)
 
12
 
13
  # Global event to stop any ongoing TTS generation across streams
14
  stop_tts_event = threading.Event()
15
+ tts_generation_lock = threading.Lock()
16
 
17
  def clean_text_for_speech(text: str) -> str:
18
  """Removes citations and other non-spoken markers from text, and formats dates."""
 
110
 
111
  if not raw_sentences:
112
  return
113
+
114
  import soundfile as sf
115
  import base64
116
  import json
 
128
  if not s_clean:
129
  continue
130
 
131
+ with tts_generation_lock:
132
+ generator = pipeline(s_clean, voice=voice, speed=SPEECH_SPEED)
133
+ sentence_audio_chunks = []
134
+ for gs, ps, audio in generator:
135
+ if (cancel_event and cancel_event.is_set()) or stop_tts_event.is_set() or local_stop_signal.is_set():
136
+ break
137
+ sentence_audio_chunks.append(audio)
138
 
139
  if sentence_audio_chunks:
140
  full_audio = np.concatenate(sentence_audio_chunks)
frontend-react/src/App.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
2
  import Auth from './components/Auth';
3
  import Sidebar from './components/Sidebar';
4
  import ChatWindow from './components/ChatWindow';
@@ -27,14 +27,14 @@ function App() {
27
  const [refreshSessions, setRefreshSessions] = useState(0);
28
  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
29
 
30
- const handleLoginSuccess = (newToken, newEmail) => {
31
  setToken(newToken);
32
  setEmail(newEmail);
33
  localStorage.setItem('rag_token', newToken);
34
  localStorage.setItem('rag_email', newEmail);
35
- };
36
 
37
- const handleLogout = () => {
38
  setToken('');
39
  setEmail('');
40
  setMessages([]);
@@ -42,26 +42,30 @@ function App() {
42
  setCurrentView('chat');
43
  localStorage.removeItem('rag_token');
44
  localStorage.removeItem('rag_email');
45
- };
46
 
47
- const handleNewChat = () => {
48
  setCurrentSessionId(generateId());
49
  setMessages([]);
50
  setSessionDocuments([]);
51
  setCurrentView('chat');
52
  if (window.innerWidth <= 768) setIsSidebarCollapsed(true);
53
- };
54
 
55
- const handleSelectSession = (sessionId) => {
56
  setCurrentSessionId(sessionId);
57
  setCurrentView('chat');
58
  if (window.innerWidth <= 768) setIsSidebarCollapsed(true);
59
- };
60
 
61
- const handleViewLibrary = () => {
62
  setCurrentView('library');
63
  if (window.innerWidth <= 768) setIsSidebarCollapsed(true);
64
- };
 
 
 
 
65
 
66
  if (!token) {
67
  return <Auth onLoginSuccess={handleLoginSuccess} />;
@@ -112,9 +116,9 @@ function App() {
112
  messages={messages}
113
  setMessages={setMessages}
114
  sessionId={currentSessionId}
115
- onFirstMessage={() => setRefreshSessions(prev => prev + 1)}
116
  setIsUploading={setIsUploading}
117
- onUploadSuccess={() => setRefreshSessions(prev => prev + 1)}
118
  isSidebarCollapsed={isSidebarCollapsed}
119
  setIsSidebarCollapsed={setIsSidebarCollapsed}
120
  sessionDocuments={sessionDocuments}
 
1
+ import { useState, useEffect, useCallback } from 'react';
2
  import Auth from './components/Auth';
3
  import Sidebar from './components/Sidebar';
4
  import ChatWindow from './components/ChatWindow';
 
27
  const [refreshSessions, setRefreshSessions] = useState(0);
28
  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
29
 
30
+ const handleLoginSuccess = useCallback((newToken, newEmail) => {
31
  setToken(newToken);
32
  setEmail(newEmail);
33
  localStorage.setItem('rag_token', newToken);
34
  localStorage.setItem('rag_email', newEmail);
35
+ }, []);
36
 
37
+ const handleLogout = useCallback(() => {
38
  setToken('');
39
  setEmail('');
40
  setMessages([]);
 
42
  setCurrentView('chat');
43
  localStorage.removeItem('rag_token');
44
  localStorage.removeItem('rag_email');
45
+ }, []);
46
 
47
+ const handleNewChat = useCallback(() => {
48
  setCurrentSessionId(generateId());
49
  setMessages([]);
50
  setSessionDocuments([]);
51
  setCurrentView('chat');
52
  if (window.innerWidth <= 768) setIsSidebarCollapsed(true);
53
+ }, []);
54
 
55
+ const handleSelectSession = useCallback((sessionId) => {
56
  setCurrentSessionId(sessionId);
57
  setCurrentView('chat');
58
  if (window.innerWidth <= 768) setIsSidebarCollapsed(true);
59
+ }, []);
60
 
61
+ const handleViewLibrary = useCallback(() => {
62
  setCurrentView('library');
63
  if (window.innerWidth <= 768) setIsSidebarCollapsed(true);
64
+ }, []);
65
+
66
+ const handleRefreshSessions = useCallback(() => {
67
+ setRefreshSessions(prev => prev + 1);
68
+ }, []);
69
 
70
  if (!token) {
71
  return <Auth onLoginSuccess={handleLoginSuccess} />;
 
116
  messages={messages}
117
  setMessages={setMessages}
118
  sessionId={currentSessionId}
119
+ onFirstMessage={handleRefreshSessions}
120
  setIsUploading={setIsUploading}
121
+ onUploadSuccess={handleRefreshSessions}
122
  isSidebarCollapsed={isSidebarCollapsed}
123
  setIsSidebarCollapsed={setIsSidebarCollapsed}
124
  sessionDocuments={sessionDocuments}
frontend-react/src/components/ChatWindow.jsx CHANGED
@@ -405,8 +405,16 @@ export default function ChatWindow({
405
  const handleSpeak = async (text, index) => {
406
  // If clicking same button while playing, STOP everything
407
  if (speakingIdx === index) {
408
- if (audioRef.current) audioRef.current.pause();
409
- if (abortControllerRef.current) abortControllerRef.current.abort();
 
 
 
 
 
 
 
 
410
  if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
411
 
412
  audioQueueRef.current.forEach(item => URL.revokeObjectURL(item.url));
@@ -418,7 +426,13 @@ export default function ChatWindow({
418
  }
419
 
420
  // Stop current if any
421
- if (audioRef.current) audioRef.current.pause();
 
 
 
 
 
 
422
  if (abortControllerRef.current) abortControllerRef.current.abort();
423
  if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
424
 
@@ -478,13 +492,13 @@ export default function ChatWindow({
478
  playNextInQueue();
479
  }
480
  } catch (e) {
481
- console.error("Error parsing audio chunk", e);
482
  }
483
  }
484
  }
485
  } catch (err) {
486
  if (err.name !== 'AbortError') {
487
- console.error("TTS Stream error", err);
488
  setSpeakingIdx(null);
489
  }
490
  }
 
405
  const handleSpeak = async (text, index) => {
406
  // If clicking same button while playing, STOP everything
407
  if (speakingIdx === index) {
408
+ if (audioRef.current) {
409
+ audioRef.current.onplay = null;
410
+ audioRef.current.onended = null;
411
+ audioRef.current.pause();
412
+ audioRef.current.src = "";
413
+ audioRef.current.load();
414
+ }
415
+ if (abortControllerRef.current) {
416
+ abortControllerRef.current.abort();
417
+ }
418
  if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
419
 
420
  audioQueueRef.current.forEach(item => URL.revokeObjectURL(item.url));
 
426
  }
427
 
428
  // Stop current if any
429
+ if (audioRef.current) {
430
+ audioRef.current.onplay = null;
431
+ audioRef.current.onended = null;
432
+ audioRef.current.pause();
433
+ audioRef.current.src = "";
434
+ audioRef.current.load();
435
+ }
436
  if (abortControllerRef.current) abortControllerRef.current.abort();
437
  if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
438
 
 
492
  playNextInQueue();
493
  }
494
  } catch (e) {
495
+ console.error("Error parsing audio chunk JSON:", e);
496
  }
497
  }
498
  }
499
  } catch (err) {
500
  if (err.name !== 'AbortError') {
501
+ console.error("TTS Stream error:", err);
502
  setSpeakingIdx(null);
503
  }
504
  }