datbkpro commited on
Commit
ff9d355
·
verified ·
1 Parent(s): 1129e66

Update services/streaming_voice_service.py

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