datbkpro commited on
Commit
2c94679
·
verified ·
1 Parent(s): a00cee2

Update services/streaming_voice_service.py

Browse files
Files changed (1) hide show
  1. services/streaming_voice_service.py +549 -240
services/streaming_voice_service.py CHANGED
@@ -14,40 +14,497 @@ from core.speechbrain_vad import SpeechBrainVAD
14
  from core.silero_vad import SileroVAD
15
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  class StreamingVoiceService:
18
  def __init__(self, groq_client: Groq, rag_system: EnhancedRAGSystem, tts_service: EnhancedTTSService):
19
  self.client = groq_client
20
  self.rag_system = rag_system
21
  self.tts_service = tts_service
22
 
23
- # Khởi tạo VAD
24
- self.vad_processor = SileroVAD()
25
  self.is_listening = False
26
  self.speech_callback = None
27
- self.is_processing = False # Tránh xử lý chồng chéo
28
- self.last_speech_time = 0
29
- self.silence_timeout = 2.0 # 2 giây im lặng thì dừng
30
 
31
  # Conversation context
32
  self.conversation_history = []
33
  self.current_transcription = ""
34
 
35
- # Audio buffer for VAD
36
  self.audio_buffer = []
37
  self.buffer_lock = threading.Lock()
 
 
 
 
38
 
39
  def start_listening(self, speech_callback: Callable) -> bool:
40
- """Bắt đầu lắng nghe với VAD"""
41
  if self.is_listening:
42
  return False
43
 
44
  self.speech_callback = speech_callback
45
- self.last_speech_time = time.time()
46
  success = self.vad_processor.start_stream(self._on_speech_detected)
47
  if success:
48
  self.is_listening = True
49
  self.is_processing = False
50
- print("🎙️ Đã bắt đầu lắng nghe với VAD")
 
 
51
  return success
52
 
53
  def stop_listening(self):
@@ -60,73 +517,51 @@ class StreamingVoiceService:
60
  self.audio_buffer = []
61
  print("🛑 Đã dừng lắng nghe")
62
 
63
- def process_audio_chunk(self, audio_data: tuple) -> Dict[str, Any]:
64
- """Xử lý audio chunk với VAD (dùng cho real-time streaming)"""
65
- if not audio_data or not self.is_listening or self.is_processing:
66
- return {
67
- 'transcription': "Đang lắng nghe...",
68
- 'response': "",
69
- 'tts_audio': None,
70
- 'status': 'listening'
71
- }
72
-
73
- try:
74
- sample_rate, audio_array = audio_data
75
-
76
- # Thêm vào buffer và xử lý với VAD
77
- with self.buffer_lock:
78
- self.audio_buffer.extend(audio_array)
79
- # Giới hạn buffer để tránh tràn bộ nhớ
80
- max_buffer_samples = sample_rate * 10 # 10 giây
81
- if len(self.audio_buffer) > max_buffer_samples:
82
- self.audio_buffer = self.audio_buffer[-max_buffer_samples:]
83
-
84
- # Xử lý với VAD
85
- self.vad_processor.process_stream(audio_array, sample_rate)
86
-
87
- # Kiểm tra timeout im lặng
88
- current_time = time.time()
89
- if current_time - self.last_speech_time > self.silence_timeout and len(self.audio_buffer) > 0:
90
- self._process_final_audio()
91
-
92
- return {
93
- 'transcription': "Đang lắng nghe...",
94
- 'response': "",
95
- 'tts_audio': None,
96
- 'status': 'listening'
97
- }
98
-
99
- except Exception as e:
100
- print(f"❌ Lỗi xử lý audio chunk: {e}")
101
- return {
102
- 'transcription': "",
103
- 'response': "",
104
- 'tts_audio': None,
105
- 'status': 'error'
106
- }
107
-
108
  def _on_speech_detected(self, speech_audio: np.ndarray, sample_rate: int):
109
- """Callback khi VAD phát hiện speech"""
110
- print(f"🎯 VAD phát hiện speech segment: {len(speech_audio)/sample_rate:.2f}s")
111
- self.last_speech_time = time.time()
112
 
113
- # Chỉ xử nếu không đang xử lý cái khác
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  if self.is_processing:
115
- print("⚠️ Đang xử lý request trước đó, bỏ qua...")
116
  return
117
-
118
- self.is_processing = True
119
-
 
120
  try:
121
  # Chuyển đổi speech thành text
122
  transcription = self._transcribe_audio(speech_audio, sample_rate)
123
 
124
  if not transcription or len(transcription.strip()) < 2:
125
  print("⚠️ Transcription quá ngắn hoặc trống")
126
- self.is_processing = False
127
  return
128
 
129
- print(f"📝 VAD Transcription: {transcription}")
130
  self.current_transcription = transcription
131
 
132
  # Tạo phản hồi AI
@@ -145,152 +580,56 @@ class StreamingVoiceService:
145
  })
146
 
147
  except Exception as e:
148
- print(f"❌ Lỗi trong _on_speech_detected: {e}")
 
149
  finally:
150
- # Cho phép xử lý tiếp sau khi TTS kết thúc
151
- threading.Timer(1.0, self._reset_processing).start()
152
-
153
- def _reset_processing(self):
154
- """Reset trạng thái xử lý sau khi hoàn thành"""
155
- self.is_processing = False
156
- with self.buffer_lock:
157
- self.audio_buffer = []
158
-
159
- def _process_final_audio(self):
160
- """Xử lý audio cuối cùng khi hết thời gian im lặng"""
161
- if self.is_processing or not self.audio_buffer:
162
- return
163
-
164
- try:
165
- with self.buffer_lock:
166
- if not self.audio_buffer:
167
- return
168
-
169
- final_audio = np.array(self.audio_buffer)
170
- self.audio_buffer = []
171
-
172
- # Chỉ xử lý nếu audio đủ dài
173
- if len(final_audio) > 16000 * 0.5: # Ít nhất 0.5 giây
174
- print("🔄 Xử lý audio cuối cùng do im lặng timeout")
175
- self._on_speech_detected(final_audio, 16000)
176
-
177
- except Exception as e:
178
- print(f"❌ Lỗi xử lý final audio: {e}")
179
 
180
- def process_streaming_audio(self, audio_data: tuple) -> Dict[str, Any]:
181
- """Xử lý audio streaming (phương thức cũ cho compatibility)"""
182
- if not audio_data:
183
- return {
184
- 'transcription': "❌ Không có dữ liệu âm thanh",
185
- 'response': "Vui lòng nói lại",
186
- 'tts_audio': None,
187
- 'status': 'error'
188
- }
189
-
190
- # Nếu đang xử lý VAD, trả về trạng thái listening
191
- if self.is_processing:
192
  return {
193
- 'transcription': "Đang xử lý...",
194
  'response': "",
195
  'tts_audio': None,
196
- 'status': 'processing'
197
  }
198
 
199
  try:
200
- # Lấy dữ liệu audio từ Gradio
201
  sample_rate, audio_array = audio_data
202
 
203
- print(f"🎯 Nhận audio: {len(audio_array)} samples, SR: {sample_rate}")
204
-
205
- # Kiểm tra kiểu dữ liệu và chuyển đổi nếu cần
206
- if isinstance(audio_array, np.ndarray):
207
- if audio_array.dtype == np.float32 or audio_array.dtype == np.float64:
208
- # Chuyển từ float sang int16
209
- audio_array = (audio_array * 32767).astype(np.int16)
210
-
211
- # Kiểm tra audio có dữ liệu không
212
- if len(audio_array) == 0:
213
- return {
214
- 'transcription': "❌ Âm thanh trống",
215
- 'response': "Vui lòng nói lại",
216
- 'tts_audio': None,
217
- 'status': 'error'
218
- }
219
-
220
- # Tính toán âm lượng
221
- audio_abs = np.abs(audio_array.astype(np.float32))
222
- audio_rms = np.sqrt(np.mean(audio_abs**2)) / 32767.0
223
- print(f"📊 Âm lượng RMS: {audio_rms:.4f}")
224
-
225
- if audio_rms < 0.005:
226
- return {
227
- 'transcription': "❌ Âm thanh quá yếu",
228
- 'response': "Xin vui lòng nói to hơn",
229
- 'tts_audio': None,
230
- 'status': 'error'
231
- }
232
-
233
- # Sử dụng VAD để kiểm tra speech
234
- if not self.vad_processor.is_speech(audio_array, sample_rate):
235
- return {
236
- 'transcription': "❌ Không phát hiện giọng nói",
237
- 'response': "Vui lòng nói rõ hơn",
238
- 'tts_audio': None,
239
- 'status': 'error'
240
- }
241
-
242
- # Chuyển đổi thành văn bản
243
- transcription = self._transcribe_audio(audio_array, sample_rate)
244
-
245
- if not transcription or len(transcription.strip()) == 0:
246
- return {
247
- 'transcription': "❌ Không nghe rõ",
248
- 'response': "Xin vui lòng nói lại rõ hơn",
249
- 'tts_audio': None,
250
- 'status': 'error'
251
- }
252
-
253
- # Kiểm tra nếu transcription quá ngắn
254
- if len(transcription.strip()) < 2:
255
- return {
256
- 'transcription': "❌ Câu nói quá ngắn",
257
- 'response': "Xin vui lòng nói câu dài hơn",
258
- 'tts_audio': None,
259
- 'status': 'error'
260
- }
261
-
262
- print(f"📝 Đã chuyển đổi: {transcription}")
263
-
264
- # Cập nhật transcription hiện tại
265
- self.current_transcription = transcription
266
-
267
- # Tạo phản hồi AI
268
- response = self._generate_ai_response(transcription)
269
 
270
- # Tạo TTS
271
- tts_audio_path = self._text_to_speech(response)
272
 
273
  return {
274
- 'transcription': transcription,
275
- 'response': response,
276
- 'tts_audio': tts_audio_path,
277
- 'status': 'completed'
278
  }
279
 
280
  except Exception as e:
281
- print(f"❌ Lỗi xử lý streaming audio: {e}")
282
- print(f"Chi tiết lỗi: {traceback.format_exc()}")
283
  return {
284
- 'transcription': f"❌ Lỗi: {str(e)}",
285
- 'response': "Xin lỗi, có lỗi xảy ra trong quá trình xử lý",
286
  'tts_audio': None,
287
  'status': 'error'
288
  }
289
 
290
  def _transcribe_audio(self, audio_data: np.ndarray, sample_rate: int) -> Optional[str]:
291
- """Chuyển audio -> text với xử lý sample rate cải tiến"""
292
  try:
293
- # Đảm bảo kiểu dữ liệu int16
294
  if audio_data.dtype != np.int16:
295
  if audio_data.dtype in [np.float32, np.float64]:
296
  audio_data = (audio_data * 32767).astype(np.int16)
@@ -299,30 +638,26 @@ class StreamingVoiceService:
299
 
300
  # Chuẩn hóa audio data
301
  if audio_data.ndim > 1:
302
- audio_data = np.mean(audio_data, axis=1).astype(np.int16) # Chuyển sang mono
303
 
304
- # Resample nếu sample rate không phải 16000Hz (Whisper yêu cầu)
305
  target_sample_rate = 16000
306
  if sample_rate != target_sample_rate:
307
  audio_data = self._resample_audio(audio_data, sample_rate, target_sample_rate)
308
  sample_rate = target_sample_rate
309
- print(f"🔄 Đã resample từ {sample_rate}Hz xuống {target_sample_rate}Hz")
310
 
311
  # Giới hạn độ dài audio
312
- max_duration = 10 # giây
313
  max_samples = sample_rate * max_duration
314
  if len(audio_data) > max_samples:
315
  audio_data = audio_data[:max_samples]
316
- print(f"⚠️ Cắt audio xuống còn {max_duration} giây")
317
 
318
  # Đảm bảo audio đủ dài
319
- min_duration = 0.5 # giây
320
  min_samples = int(sample_rate * min_duration)
321
  if len(audio_data) < min_samples:
322
- # Pad audio nếu quá ngắn
323
  padding = np.zeros(min_samples - len(audio_data), dtype=np.int16)
324
  audio_data = np.concatenate([audio_data, padding])
325
- print(f"⚠️ Đã pad audio lên {min_duration} giây")
326
 
327
  print(f"🔊 Gửi audio đến Whisper: {len(audio_data)} samples, {sample_rate}Hz")
328
 
@@ -331,8 +666,7 @@ class StreamingVoiceService:
331
  sf.write(buffer, audio_data, sample_rate, format='wav', subtype='PCM_16')
332
  buffer.seek(0)
333
 
334
- # Gọi API Whisper với timeout
335
- import requests
336
  try:
337
  transcription = self.client.audio.transcriptions.create(
338
  model=settings.WHISPER_MODEL,
@@ -341,9 +675,6 @@ class StreamingVoiceService:
341
  language="vi",
342
  temperature=0.0,
343
  )
344
- except requests.exceptions.Timeout:
345
- print("❌ Whisper API timeout")
346
- return None
347
  except Exception as e:
348
  print(f"❌ Lỗi Whisper API: {e}")
349
  return None
@@ -356,47 +687,13 @@ class StreamingVoiceService:
356
  else:
357
  result = str(transcription).strip()
358
 
359
- print(f"✅ Transcription thành công: '{result}'")
360
  return result
361
 
362
  except Exception as e:
363
  print(f"❌ Lỗi transcription: {e}")
364
- print(f"Audio details: dtype={audio_data.dtype}, shape={audio_data.shape}, sr={sample_rate}")
365
  return None
366
 
367
- def _resample_audio(self, audio_data: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
368
- """Resample audio sử dụng scipy - cải tiến độ chính xác"""
369
- try:
370
- from scipy import signal
371
-
372
- # Tính số samples mới
373
- duration = len(audio_data) / orig_sr
374
- new_length = int(duration * target_sr)
375
-
376
- # Resample sử dụng scipy.signal.resample với windowing
377
- resampled_audio = signal.resample(audio_data, new_length)
378
-
379
- # Chuyển lại về int16
380
- resampled_audio = np.clip(resampled_audio, -32768, 32767).astype(np.int16)
381
-
382
- return resampled_audio
383
-
384
- except ImportError:
385
- print("⚠️ Không có scipy, sử dụng simple resampling")
386
- # Simple resampling bằng interpolation
387
- orig_length = len(audio_data)
388
- new_length = int(orig_length * target_sr / orig_sr)
389
-
390
- # Linear interpolation
391
- x_old = np.linspace(0, 1, orig_length)
392
- x_new = np.linspace(0, 1, new_length)
393
- resampled_audio = np.interp(x_new, x_old, audio_data).astype(np.int16)
394
-
395
- return resampled_audio
396
- except Exception as e:
397
- print(f"❌ Lỗi resample: {e}")
398
- return audio_data
399
-
400
  def _generate_ai_response(self, user_input: str) -> str:
401
  """Sinh phản hồi AI với xử lý lỗi"""
402
  try:
@@ -414,8 +711,8 @@ Thông tin tham khảo:
414
  """
415
 
416
  messages = [{"role": "system", "content": system_prompt}]
417
- # Giữ lại 4 tin nhắn gần nhất
418
- messages.extend(self.conversation_history[-4:])
419
 
420
  completion = self.client.chat.completions.create(
421
  model="llama-3.1-8b-instant",
@@ -428,8 +725,8 @@ Thông tin tham khảo:
428
  self.conversation_history.append({"role": "assistant", "content": response})
429
 
430
  # Giới hạn lịch sử
431
- if len(self.conversation_history) > 8:
432
- self.conversation_history = self.conversation_history[-8:]
433
 
434
  return response
435
 
@@ -438,7 +735,7 @@ Thông tin tham khảo:
438
  return "Xin lỗi, tôi gặp lỗi khi tạo phản hồi. Vui lòng thử lại."
439
 
440
  def _text_to_speech(self, text: str) -> Optional[str]:
441
- """Chuyển văn bản thành giọng nói với xử lý lỗi"""
442
  try:
443
  if not text or text.startswith("❌") or text.startswith("Xin lỗi"):
444
  return None
@@ -452,6 +749,17 @@ Thông tin tham khảo:
452
  print(f"❌ Lỗi TTS: {e}")
453
  return None
454
 
 
 
 
 
 
 
 
 
 
 
 
455
  def clear_conversation(self):
456
  """Xóa lịch sử hội thoại"""
457
  self.conversation_history = []
@@ -465,5 +773,6 @@ Thông tin tham khảo:
465
  'is_processing': self.is_processing,
466
  'history_length': len(self.conversation_history),
467
  'current_transcription': self.current_transcription,
 
468
  'last_update': time.strftime("%H:%M:%S")
469
  }
 
14
  from core.silero_vad import SileroVAD
15
 
16
 
17
+ # class StreamingVoiceService:
18
+ # def __init__(self, groq_client: Groq, rag_system: EnhancedRAGSystem, tts_service: EnhancedTTSService):
19
+ # self.client = groq_client
20
+ # self.rag_system = rag_system
21
+ # self.tts_service = tts_service
22
+
23
+ # # Khởi tạo VAD
24
+ # self.vad_processor = SileroVAD()
25
+ # self.is_listening = False
26
+ # self.speech_callback = None
27
+ # self.is_processing = False # Tránh xử lý chồng chéo
28
+ # self.last_speech_time = 0
29
+ # self.silence_timeout = 2.0 # 2 giây im lặng thì dừng
30
+
31
+ # # Conversation context
32
+ # self.conversation_history = []
33
+ # self.current_transcription = ""
34
+
35
+ # # Audio buffer for VAD
36
+ # self.audio_buffer = []
37
+ # self.buffer_lock = threading.Lock()
38
+
39
+ # def start_listening(self, speech_callback: Callable) -> bool:
40
+ # """Bắt đầu lắng nghe với VAD"""
41
+ # if self.is_listening:
42
+ # return False
43
+
44
+ # self.speech_callback = speech_callback
45
+ # self.last_speech_time = time.time()
46
+ # success = self.vad_processor.start_stream(self._on_speech_detected)
47
+ # if success:
48
+ # self.is_listening = True
49
+ # self.is_processing = False
50
+ # print("🎙️ Đã bắt đầu lắng nghe với VAD")
51
+ # return success
52
+
53
+ # def stop_listening(self):
54
+ # """Dừng lắng nghe"""
55
+ # self.vad_processor.stop_stream()
56
+ # self.is_listening = False
57
+ # self.is_processing = False
58
+ # self.speech_callback = None
59
+ # with self.buffer_lock:
60
+ # self.audio_buffer = []
61
+ # print("🛑 Đã dừng lắng nghe")
62
+
63
+ # def process_audio_chunk(self, audio_data: tuple) -> Dict[str, Any]:
64
+ # """Xử lý audio chunk với VAD (dùng cho real-time streaming)"""
65
+ # if not audio_data or not self.is_listening or self.is_processing:
66
+ # return {
67
+ # 'transcription': "Đang lắng nghe...",
68
+ # 'response': "",
69
+ # 'tts_audio': None,
70
+ # 'status': 'listening'
71
+ # }
72
+
73
+ # try:
74
+ # sample_rate, audio_array = audio_data
75
+
76
+ # # Thêm vào buffer và xử lý với VAD
77
+ # with self.buffer_lock:
78
+ # self.audio_buffer.extend(audio_array)
79
+ # # Giới hạn buffer để tránh tràn bộ nhớ
80
+ # max_buffer_samples = sample_rate * 10 # 10 giây
81
+ # if len(self.audio_buffer) > max_buffer_samples:
82
+ # self.audio_buffer = self.audio_buffer[-max_buffer_samples:]
83
+
84
+ # # Xử lý với VAD
85
+ # self.vad_processor.process_stream(audio_array, sample_rate)
86
+
87
+ # # Kiểm tra timeout im lặng
88
+ # current_time = time.time()
89
+ # if current_time - self.last_speech_time > self.silence_timeout and len(self.audio_buffer) > 0:
90
+ # self._process_final_audio()
91
+
92
+ # return {
93
+ # 'transcription': "Đang lắng nghe...",
94
+ # 'response': "",
95
+ # 'tts_audio': None,
96
+ # 'status': 'listening'
97
+ # }
98
+
99
+ # except Exception as e:
100
+ # print(f"❌ Lỗi xử lý audio chunk: {e}")
101
+ # return {
102
+ # 'transcription': "",
103
+ # 'response': "",
104
+ # 'tts_audio': None,
105
+ # 'status': 'error'
106
+ # }
107
+
108
+ # def _on_speech_detected(self, speech_audio: np.ndarray, sample_rate: int):
109
+ # """Callback khi VAD phát hiện speech"""
110
+ # print(f"🎯 VAD phát hiện speech segment: {len(speech_audio)/sample_rate:.2f}s")
111
+ # self.last_speech_time = time.time()
112
+
113
+ # # Chỉ xử lý nếu không đang xử lý cái khác
114
+ # if self.is_processing:
115
+ # print("⚠️ Đang xử lý request trước đó, bỏ qua...")
116
+ # return
117
+
118
+ # self.is_processing = True
119
+
120
+ # try:
121
+ # # Chuyển đổi speech thành text
122
+ # transcription = self._transcribe_audio(speech_audio, sample_rate)
123
+
124
+ # if not transcription or len(transcription.strip()) < 2:
125
+ # print("⚠️ Transcription quá ngắn hoặc trống")
126
+ # self.is_processing = False
127
+ # return
128
+
129
+ # print(f"📝 VAD Transcription: {transcription}")
130
+ # self.current_transcription = transcription
131
+
132
+ # # Tạo phản hồi AI
133
+ # response = self._generate_ai_response(transcription)
134
+
135
+ # # Tạo TTS
136
+ # tts_audio_path = self._text_to_speech(response)
137
+
138
+ # # G���i kết quả đến callback
139
+ # if self.speech_callback:
140
+ # self.speech_callback({
141
+ # 'transcription': transcription,
142
+ # 'response': response,
143
+ # 'tts_audio': tts_audio_path,
144
+ # 'status': 'completed'
145
+ # })
146
+
147
+ # except Exception as e:
148
+ # print(f"❌ Lỗi trong _on_speech_detected: {e}")
149
+ # finally:
150
+ # # Cho phép xử lý tiếp sau khi TTS kết thúc
151
+ # threading.Timer(1.0, self._reset_processing).start()
152
+
153
+ # def _reset_processing(self):
154
+ # """Reset trạng thái xử lý sau khi hoàn thành"""
155
+ # self.is_processing = False
156
+ # with self.buffer_lock:
157
+ # self.audio_buffer = []
158
+
159
+ # def _process_final_audio(self):
160
+ # """Xử lý audio cuối cùng khi hết thời gian im lặng"""
161
+ # if self.is_processing or not self.audio_buffer:
162
+ # return
163
+
164
+ # try:
165
+ # with self.buffer_lock:
166
+ # if not self.audio_buffer:
167
+ # return
168
+
169
+ # final_audio = np.array(self.audio_buffer)
170
+ # self.audio_buffer = []
171
+
172
+ # # Chỉ xử lý nếu audio đủ dài
173
+ # if len(final_audio) > 16000 * 0.5: # Ít nhất 0.5 giây
174
+ # print("🔄 Xử lý audio cuối cùng do im lặng timeout")
175
+ # self._on_speech_detected(final_audio, 16000)
176
+
177
+ # except Exception as e:
178
+ # print(f"❌ Lỗi xử lý final audio: {e}")
179
+
180
+ # def process_streaming_audio(self, audio_data: tuple) -> Dict[str, Any]:
181
+ # """Xử lý audio streaming (phương thức cũ cho compatibility)"""
182
+ # if not audio_data:
183
+ # return {
184
+ # 'transcription': "❌ Không có dữ liệu âm thanh",
185
+ # 'response': "Vui lòng nói lại",
186
+ # 'tts_audio': None,
187
+ # 'status': 'error'
188
+ # }
189
+
190
+ # # Nếu đang xử lý VAD, trả về trạng thái listening
191
+ # if self.is_processing:
192
+ # return {
193
+ # 'transcription': "Đang xử lý...",
194
+ # 'response': "",
195
+ # 'tts_audio': None,
196
+ # 'status': 'processing'
197
+ # }
198
+
199
+ # try:
200
+ # # Lấy dữ liệu audio từ Gradio
201
+ # sample_rate, audio_array = audio_data
202
+
203
+ # print(f"🎯 Nhận audio: {len(audio_array)} samples, SR: {sample_rate}")
204
+
205
+ # # Kiểm tra kiểu dữ liệu và chuyển đổi nếu cần
206
+ # if isinstance(audio_array, np.ndarray):
207
+ # if audio_array.dtype == np.float32 or audio_array.dtype == np.float64:
208
+ # # Chuyển từ float sang int16
209
+ # audio_array = (audio_array * 32767).astype(np.int16)
210
+
211
+ # # Kiểm tra audio có dữ liệu không
212
+ # if len(audio_array) == 0:
213
+ # return {
214
+ # 'transcription': "❌ Âm thanh trống",
215
+ # 'response': "Vui lòng nói lại",
216
+ # 'tts_audio': None,
217
+ # 'status': 'error'
218
+ # }
219
+
220
+ # # Tính toán âm lượng
221
+ # audio_abs = np.abs(audio_array.astype(np.float32))
222
+ # audio_rms = np.sqrt(np.mean(audio_abs**2)) / 32767.0
223
+ # print(f"📊 Âm lượng RMS: {audio_rms:.4f}")
224
+
225
+ # if audio_rms < 0.005:
226
+ # return {
227
+ # 'transcription': "❌ Âm thanh quá yếu",
228
+ # 'response': "Xin vui lòng nói to hơn",
229
+ # 'tts_audio': None,
230
+ # 'status': 'error'
231
+ # }
232
+
233
+ # # Sử dụng VAD để kiểm tra speech
234
+ # if not self.vad_processor.is_speech(audio_array, sample_rate):
235
+ # return {
236
+ # 'transcription': "❌ Không phát hiện giọng nói",
237
+ # 'response': "Vui lòng nói rõ hơn",
238
+ # 'tts_audio': None,
239
+ # 'status': 'error'
240
+ # }
241
+
242
+ # # Chuyển đổi thành văn bản
243
+ # transcription = self._transcribe_audio(audio_array, sample_rate)
244
+
245
+ # if not transcription or len(transcription.strip()) == 0:
246
+ # return {
247
+ # 'transcription': "❌ Không nghe rõ",
248
+ # 'response': "Xin vui lòng nói lại rõ hơn",
249
+ # 'tts_audio': None,
250
+ # 'status': 'error'
251
+ # }
252
+
253
+ # # Kiểm tra nếu transcription quá ngắn
254
+ # if len(transcription.strip()) < 2:
255
+ # return {
256
+ # 'transcription': "��� Câu nói quá ngắn",
257
+ # 'response': "Xin vui lòng nói câu dài hơn",
258
+ # 'tts_audio': None,
259
+ # 'status': 'error'
260
+ # }
261
+
262
+ # print(f"📝 Đã chuyển đổi: {transcription}")
263
+
264
+ # # Cập nhật transcription hiện tại
265
+ # self.current_transcription = transcription
266
+
267
+ # # Tạo phản hồi AI
268
+ # response = self._generate_ai_response(transcription)
269
+
270
+ # # Tạo TTS
271
+ # tts_audio_path = self._text_to_speech(response)
272
+
273
+ # return {
274
+ # 'transcription': transcription,
275
+ # 'response': response,
276
+ # 'tts_audio': tts_audio_path,
277
+ # 'status': 'completed'
278
+ # }
279
+
280
+ # except Exception as e:
281
+ # print(f"❌ Lỗi xử lý streaming audio: {e}")
282
+ # print(f"Chi tiết lỗi: {traceback.format_exc()}")
283
+ # return {
284
+ # 'transcription': f"❌ Lỗi: {str(e)}",
285
+ # 'response': "Xin lỗi, có lỗi xảy ra trong quá trình xử lý",
286
+ # 'tts_audio': None,
287
+ # 'status': 'error'
288
+ # }
289
+
290
+ # def _transcribe_audio(self, audio_data: np.ndarray, sample_rate: int) -> Optional[str]:
291
+ # """Chuyển audio -> text với xử lý sample rate cải tiến"""
292
+ # try:
293
+ # # Đảm bảo kiểu dữ liệu là int16
294
+ # if audio_data.dtype != np.int16:
295
+ # if audio_data.dtype in [np.float32, np.float64]:
296
+ # audio_data = (audio_data * 32767).astype(np.int16)
297
+ # else:
298
+ # audio_data = audio_data.astype(np.int16)
299
+
300
+ # # Chuẩn hóa audio data
301
+ # if audio_data.ndim > 1:
302
+ # audio_data = np.mean(audio_data, axis=1).astype(np.int16) # Chuyển sang mono
303
+
304
+ # # Resample nếu sample rate không phải 16000Hz (Whisper yêu cầu)
305
+ # target_sample_rate = 16000
306
+ # if sample_rate != target_sample_rate:
307
+ # audio_data = self._resample_audio(audio_data, sample_rate, target_sample_rate)
308
+ # sample_rate = target_sample_rate
309
+ # print(f"🔄 Đã resample từ {sample_rate}Hz xuống {target_sample_rate}Hz")
310
+
311
+ # # Giới hạn độ dài audio
312
+ # max_duration = 10 # giây
313
+ # max_samples = sample_rate * max_duration
314
+ # if len(audio_data) > max_samples:
315
+ # audio_data = audio_data[:max_samples]
316
+ # print(f"⚠️ Cắt audio xuống còn {max_duration} giây")
317
+
318
+ # # Đảm bảo audio đủ dài
319
+ # min_duration = 0.5 # giây
320
+ # min_samples = int(sample_rate * min_duration)
321
+ # if len(audio_data) < min_samples:
322
+ # # Pad audio nếu quá ngắn
323
+ # padding = np.zeros(min_samples - len(audio_data), dtype=np.int16)
324
+ # audio_data = np.concatenate([audio_data, padding])
325
+ # print(f"⚠️ Đã pad audio lên {min_duration} giây")
326
+
327
+ # print(f"🔊 Gửi audio đến Whisper: {len(audio_data)} samples, {sample_rate}Hz")
328
+
329
+ # # Tạo temporary file trong memory
330
+ # buffer = io.BytesIO()
331
+ # sf.write(buffer, audio_data, sample_rate, format='wav', subtype='PCM_16')
332
+ # buffer.seek(0)
333
+
334
+ # # Gọi API Whisper với timeout
335
+ # import requests
336
+ # try:
337
+ # transcription = self.client.audio.transcriptions.create(
338
+ # model=settings.WHISPER_MODEL,
339
+ # file=("speech.wav", buffer.read(), "audio/wav"),
340
+ # response_format="text",
341
+ # language="vi",
342
+ # temperature=0.0,
343
+ # )
344
+ # except requests.exceptions.Timeout:
345
+ # print("❌ Whisper API timeout")
346
+ # return None
347
+ # except Exception as e:
348
+ # print(f"❌ Lỗi Whisper API: {e}")
349
+ # return None
350
+
351
+ # # Xử lý response
352
+ # if hasattr(transcription, 'text'):
353
+ # result = transcription.text.strip()
354
+ # elif isinstance(transcription, str):
355
+ # result = transcription.strip()
356
+ # else:
357
+ # result = str(transcription).strip()
358
+
359
+ # print(f"✅ Transcription thành công: '{result}'")
360
+ # return result
361
+
362
+ # except Exception as e:
363
+ # print(f"❌ Lỗi transcription: {e}")
364
+ # print(f"Audio details: dtype={audio_data.dtype}, shape={audio_data.shape}, sr={sample_rate}")
365
+ # return None
366
+
367
+ # def _resample_audio(self, audio_data: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
368
+ # """Resample audio sử dụng scipy - cải tiến độ chính xác"""
369
+ # try:
370
+ # from scipy import signal
371
+
372
+ # # Tính số samples mới
373
+ # duration = len(audio_data) / orig_sr
374
+ # new_length = int(duration * target_sr)
375
+
376
+ # # Resample sử dụng scipy.signal.resample với windowing
377
+ # resampled_audio = signal.resample(audio_data, new_length)
378
+
379
+ # # Chuyển lại về int16
380
+ # resampled_audio = np.clip(resampled_audio, -32768, 32767).astype(np.int16)
381
+
382
+ # return resampled_audio
383
+
384
+ # except ImportError:
385
+ # print("⚠️ Không có scipy, sử dụng simple resampling")
386
+ # # Simple resampling bằng interpolation
387
+ # orig_length = len(audio_data)
388
+ # new_length = int(orig_length * target_sr / orig_sr)
389
+
390
+ # # Linear interpolation
391
+ # x_old = np.linspace(0, 1, orig_length)
392
+ # x_new = np.linspace(0, 1, new_length)
393
+ # resampled_audio = np.interp(x_new, x_old, audio_data).astype(np.int16)
394
+
395
+ # return resampled_audio
396
+ # except Exception as e:
397
+ # print(f"❌ Lỗi resample: {e}")
398
+ # return audio_data
399
+
400
+ # def _generate_ai_response(self, user_input: str) -> str:
401
+ # """Sinh phản hồi AI với xử lý lỗi"""
402
+ # try:
403
+ # # Thêm vào lịch sử
404
+ # self.conversation_history.append({"role": "user", "content": user_input})
405
+
406
+ # # Tìm kiếm RAG
407
+ # rag_results = self.rag_system.semantic_search(user_input, top_k=2)
408
+ # context_text = "\n".join([f"- {result.get('text', str(result))}" for result in rag_results]) if rag_results else ""
409
+
410
+ # system_prompt = f"""Bạn là trợ lý AI thông minh chuyên về tiếng Việt.
411
+ # Hãy trả lời ngắn gọn, tự nhiên và hữu ích (dưới 100 từ).
412
+ # Thông tin tham khảo:
413
+ # {context_text}
414
+ # """
415
+
416
+ # messages = [{"role": "system", "content": system_prompt}]
417
+ # # Giữ lại 4 tin nhắn gần nhất
418
+ # messages.extend(self.conversation_history[-4:])
419
+
420
+ # completion = self.client.chat.completions.create(
421
+ # model="llama-3.1-8b-instant",
422
+ # messages=messages,
423
+ # max_tokens=150,
424
+ # temperature=0.7
425
+ # )
426
+
427
+ # response = completion.choices[0].message.content
428
+ # self.conversation_history.append({"role": "assistant", "content": response})
429
+
430
+ # # Giới hạn lịch sử
431
+ # if len(self.conversation_history) > 8:
432
+ # self.conversation_history = self.conversation_history[-8:]
433
+
434
+ # return response
435
+
436
+ # except Exception as e:
437
+ # print(f"❌ Lỗi tạo AI response: {e}")
438
+ # return "Xin lỗi, tôi gặp lỗi khi tạo phản hồi. Vui lòng thử lại."
439
+
440
+ # def _text_to_speech(self, text: str) -> Optional[str]:
441
+ # """Chuyển văn bản thành giọng nói với xử lý lỗi"""
442
+ # try:
443
+ # if not text or text.startswith("❌") or text.startswith("Xin lỗi"):
444
+ # return None
445
+
446
+ # tts_bytes = self.tts_service.text_to_speech(text, 'vi')
447
+ # if tts_bytes:
448
+ # audio_path = self.tts_service.save_audio_to_file(tts_bytes)
449
+ # print(f"✅ Đã tạo TTS: {audio_path}")
450
+ # return audio_path
451
+ # except Exception as e:
452
+ # print(f"❌ Lỗi TTS: {e}")
453
+ # return None
454
+
455
+ # def clear_conversation(self):
456
+ # """Xóa lịch sử hội thoại"""
457
+ # self.conversation_history = []
458
+ # self.current_transcription = ""
459
+ # print("🗑️ Đã xóa lịch sử hội thoại")
460
+
461
+ # def get_conversation_state(self) -> dict:
462
+ # """Lấy trạng thái hội thoại"""
463
+ # return {
464
+ # 'is_listening': self.is_listening,
465
+ # 'is_processing': self.is_processing,
466
+ # 'history_length': len(self.conversation_history),
467
+ # 'current_transcription': self.current_transcription,
468
+ # 'last_update': time.strftime("%H:%M:%S")
469
+ # }
470
  class StreamingVoiceService:
471
  def __init__(self, groq_client: Groq, rag_system: EnhancedRAGSystem, tts_service: EnhancedTTSService):
472
  self.client = groq_client
473
  self.rag_system = rag_system
474
  self.tts_service = tts_service
475
 
476
+ # Khởi tạo VAD tối ưu
477
+ self.vad_processor = OptimizedSileroVAD()
478
  self.is_listening = False
479
  self.speech_callback = None
480
+ self.is_processing = False
481
+ self.processing_lock = threading.Lock()
 
482
 
483
  # Conversation context
484
  self.conversation_history = []
485
  self.current_transcription = ""
486
 
487
+ # Audio buffer
488
  self.audio_buffer = []
489
  self.buffer_lock = threading.Lock()
490
+
491
+ # Response queue để quản lý thứ tự xử lý
492
+ self.response_queue = queue.Queue()
493
+ self.current_task = None
494
 
495
  def start_listening(self, speech_callback: Callable) -> bool:
496
+ """Bắt đầu lắng nghe với VAD tối ưu"""
497
  if self.is_listening:
498
  return False
499
 
500
  self.speech_callback = speech_callback
 
501
  success = self.vad_processor.start_stream(self._on_speech_detected)
502
  if success:
503
  self.is_listening = True
504
  self.is_processing = False
505
+ # Bắt đầu thread xử response
506
+ threading.Thread(target=self._process_response_queue, daemon=True).start()
507
+ print("🎙️ Đã bắt đầu lắng nghe với VAD tối ưu")
508
  return success
509
 
510
  def stop_listening(self):
 
517
  self.audio_buffer = []
518
  print("🛑 Đã dừng lắng nghe")
519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  def _on_speech_detected(self, speech_audio: np.ndarray, sample_rate: int):
521
+ """Callback khi VAD phát hiện speech - TỐI ƯU HÓA"""
522
+ print(f"🎯 VAD phát hiện speech: {len(speech_audio)/sample_rate:.2f}s")
 
523
 
524
+ # Thêm vào queue thay xử lý trực tiếp
525
+ self.response_queue.put((speech_audio, sample_rate))
526
+
527
+ def _process_response_queue(self):
528
+ """Xử lý tuần tự các request từ queue"""
529
+ while self.is_listening:
530
+ try:
531
+ # Chờ item từ queue với timeout
532
+ speech_audio, sample_rate = self.response_queue.get(timeout=1.0)
533
+
534
+ # Xử lý speech
535
+ self._process_speech_segment(speech_audio, sample_rate)
536
+
537
+ # Đánh dấu task hoàn thành
538
+ self.response_queue.task_done()
539
+
540
+ except queue.Empty:
541
+ continue
542
+ except Exception as e:
543
+ print(f"❌ Lỗi trong response queue: {e}")
544
+ continue
545
+
546
+ def _process_speech_segment(self, speech_audio: np.ndarray, sample_rate: int):
547
+ """Xử lý speech segment - TỐI ƯU HÓA"""
548
+ # Kiểm tra nếu đang xử lý
549
  if self.is_processing:
550
+ print("⚠️ Bỏ qua speech segment - đang xử lý request trước")
551
  return
552
+
553
+ with self.processing_lock:
554
+ self.is_processing = True
555
+
556
  try:
557
  # Chuyển đổi speech thành text
558
  transcription = self._transcribe_audio(speech_audio, sample_rate)
559
 
560
  if not transcription or len(transcription.strip()) < 2:
561
  print("⚠️ Transcription quá ngắn hoặc trống")
 
562
  return
563
 
564
+ print(f"📝 Transcription: {transcription}")
565
  self.current_transcription = transcription
566
 
567
  # Tạo phản hồi AI
 
580
  })
581
 
582
  except Exception as e:
583
+ print(f"❌ Lỗi xử lý speech segment: {e}")
584
+ traceback.print_exc()
585
  finally:
586
+ with self.processing_lock:
587
+ self.is_processing = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
 
589
+ def process_audio_chunk(self, audio_data: tuple) -> Dict[str, Any]:
590
+ """Xử lý audio chunk với VAD"""
591
+ if not audio_data or not self.is_listening:
 
 
 
 
 
 
 
 
 
592
  return {
593
+ 'transcription': "Đang lắng nghe...",
594
  'response': "",
595
  'tts_audio': None,
596
+ 'status': 'listening'
597
  }
598
 
599
  try:
 
600
  sample_rate, audio_array = audio_data
601
 
602
+ # Thêm vào buffer xử lý với VAD
603
+ with self.buffer_lock:
604
+ self.audio_buffer.extend(audio_array)
605
+ # Giới hạn buffer
606
+ max_buffer_samples = sample_rate * 15 # 15 giây
607
+ if len(self.audio_buffer) > max_buffer_samples:
608
+ self.audio_buffer = self.audio_buffer[-max_buffer_samples:]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
610
+ # Xử lý với VAD
611
+ self.vad_processor.process_stream(audio_array, sample_rate)
612
 
613
  return {
614
+ 'transcription': "Đang lắng nghe...",
615
+ 'response': "",
616
+ 'tts_audio': None,
617
+ 'status': 'listening'
618
  }
619
 
620
  except Exception as e:
621
+ print(f"❌ Lỗi xử lý audio chunk: {e}")
 
622
  return {
623
+ 'transcription': "",
624
+ 'response': "",
625
  'tts_audio': None,
626
  'status': 'error'
627
  }
628
 
629
  def _transcribe_audio(self, audio_data: np.ndarray, sample_rate: int) -> Optional[str]:
630
+ """Chuyển audio -> text với xử lý cải tiến"""
631
  try:
632
+ # Đảm bảo kiểu dữ liệu chuẩn hóa
633
  if audio_data.dtype != np.int16:
634
  if audio_data.dtype in [np.float32, np.float64]:
635
  audio_data = (audio_data * 32767).astype(np.int16)
 
638
 
639
  # Chuẩn hóa audio data
640
  if audio_data.ndim > 1:
641
+ audio_data = np.mean(audio_data, axis=1).astype(np.int16)
642
 
643
+ # Resample nếu cần
644
  target_sample_rate = 16000
645
  if sample_rate != target_sample_rate:
646
  audio_data = self._resample_audio(audio_data, sample_rate, target_sample_rate)
647
  sample_rate = target_sample_rate
 
648
 
649
  # Giới hạn độ dài audio
650
+ max_duration = 15 # giây
651
  max_samples = sample_rate * max_duration
652
  if len(audio_data) > max_samples:
653
  audio_data = audio_data[:max_samples]
 
654
 
655
  # Đảm bảo audio đủ dài
656
+ min_duration = 0.8 # giây
657
  min_samples = int(sample_rate * min_duration)
658
  if len(audio_data) < min_samples:
 
659
  padding = np.zeros(min_samples - len(audio_data), dtype=np.int16)
660
  audio_data = np.concatenate([audio_data, padding])
 
661
 
662
  print(f"🔊 Gửi audio đến Whisper: {len(audio_data)} samples, {sample_rate}Hz")
663
 
 
666
  sf.write(buffer, audio_data, sample_rate, format='wav', subtype='PCM_16')
667
  buffer.seek(0)
668
 
669
+ # Gọi API Whisper
 
670
  try:
671
  transcription = self.client.audio.transcriptions.create(
672
  model=settings.WHISPER_MODEL,
 
675
  language="vi",
676
  temperature=0.0,
677
  )
 
 
 
678
  except Exception as e:
679
  print(f"❌ Lỗi Whisper API: {e}")
680
  return None
 
687
  else:
688
  result = str(transcription).strip()
689
 
690
+ print(f"✅ Transcription: '{result}'")
691
  return result
692
 
693
  except Exception as e:
694
  print(f"❌ Lỗi transcription: {e}")
 
695
  return None
696
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  def _generate_ai_response(self, user_input: str) -> str:
698
  """Sinh phản hồi AI với xử lý lỗi"""
699
  try:
 
711
  """
712
 
713
  messages = [{"role": "system", "content": system_prompt}]
714
+ # Giữ lại 6 tin nhắn gần nhất
715
+ messages.extend(self.conversation_history[-6:])
716
 
717
  completion = self.client.chat.completions.create(
718
  model="llama-3.1-8b-instant",
 
725
  self.conversation_history.append({"role": "assistant", "content": response})
726
 
727
  # Giới hạn lịch sử
728
+ if len(self.conversation_history) > 12:
729
+ self.conversation_history = self.conversation_history[-12:]
730
 
731
  return response
732
 
 
735
  return "Xin lỗi, tôi gặp lỗi khi tạo phản hồi. Vui lòng thử lại."
736
 
737
  def _text_to_speech(self, text: str) -> Optional[str]:
738
+ """Chuyển văn bản thành giọng nói"""
739
  try:
740
  if not text or text.startswith("❌") or text.startswith("Xin lỗi"):
741
  return None
 
749
  print(f"❌ Lỗi TTS: {e}")
750
  return None
751
 
752
+ def _resample_audio(self, audio_data: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
753
+ """Resample audio"""
754
+ try:
755
+ from scipy import signal
756
+ duration = len(audio_data) / orig_sr
757
+ new_length = int(duration * target_sr)
758
+ resampled_audio = signal.resample(audio_data, new_length)
759
+ return np.clip(resampled_audio, -32768, 32767).astype(np.int16)
760
+ except Exception:
761
+ return audio_data
762
+
763
  def clear_conversation(self):
764
  """Xóa lịch sử hội thoại"""
765
  self.conversation_history = []
 
773
  'is_processing': self.is_processing,
774
  'history_length': len(self.conversation_history),
775
  'current_transcription': self.current_transcription,
776
+ 'queue_size': self.response_queue.qsize(),
777
  'last_update': time.strftime("%H:%M:%S")
778
  }