datbkpro commited on
Commit
0304fae
·
verified ·
1 Parent(s): 361b4a9

Update services/streaming_voice_service.py

Browse files
Files changed (1) hide show
  1. services/streaming_voice_service.py +121 -41
services/streaming_voice_service.py CHANGED
@@ -8,13 +8,12 @@ import threading
8
  import queue
9
  import json
10
  import os
 
 
11
  from vosk import Model, KaldiRecognizer
12
  from groq import Groq
13
  from typing import Optional, Dict, Any, Callable
14
- from config.settings import settings
15
- from core.rag_system import EnhancedRAGSystem
16
- from core.tts_service import EnhancedTTSService
17
- from core.silero_vad import SileroVAD
18
  class VoskStreamingASR:
19
  def __init__(self, model_path: str = None):
20
  self.model = None
@@ -22,10 +21,8 @@ class VoskStreamingASR:
22
  self.sample_rate = 16000
23
  self.is_streaming = False
24
 
25
- # Buffer để tích luỹ audio - QUAN TRỌNG
26
  self.audio_buffer = []
27
- self.buffer_duration = 2.0 # tích luỹ 2 giây audio
28
- self.min_samples_for_recognition = 32000 # ít nhất 2 giây audio
29
 
30
  if model_path is None:
31
  model_path = self._download_vosk_model()
@@ -40,7 +37,38 @@ class VoskStreamingASR:
40
  except Exception as e:
41
  print(f"❌ Lỗi khởi tạo VOSK model: {e}")
42
  else:
43
- print(f"❌ Không tìm thấy VOSK model tại: {model_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  def start_stream(self):
46
  """Bắt đầu stream mới"""
@@ -59,45 +87,45 @@ class VoskStreamingASR:
59
  return False
60
 
61
  def process_audio_chunk(self, audio_chunk: np.ndarray, sample_rate: int = None) -> Dict[str, Any]:
62
- """Xử lý audio chunk - SIMPLIFIED VERSION"""
63
  if self.recognizer is None or not self.is_streaming:
64
  return {"text": "", "partial": "", "is_final": False}
65
 
66
  try:
67
- # Đơn giản hoá: luôn xử lý, không check âm lượng
68
  if sample_rate and sample_rate != self.sample_rate:
69
  audio_chunk = self._resample_audio(audio_chunk, sample_rate, self.sample_rate)
70
 
71
  # Đảm bảo là int16
72
  if audio_chunk.dtype != np.int16:
73
- audio_chunk = audio_chunk.astype(np.int16)
 
 
 
74
 
75
  # THÊM VÀO BUFFER - QUAN TRỌNG
76
  self.audio_buffer.extend(audio_chunk)
77
 
78
- # Chỉ xử lý khi có đủ audio
79
- if len(self.audio_buffer) < 16000: # ít nhất 1 giây
80
  return {"text": "", "partial": "Đang nghe...", "is_final": False}
81
 
82
- # Lấy audio từ buffer để xử lý
83
- process_audio = np.array(self.audio_buffer[-32000:], dtype=np.int16) # lấy 2 giây gần nhất
84
 
85
  # Chuyển sang bytes
86
  audio_bytes = process_audio.tobytes()
87
 
88
- # Xử lý với VOSK - GỬI NHIỀU LẦN
89
- for i in range(0, len(audio_bytes), 8000): # gửi từng chunk nhỏ
90
- chunk = audio_bytes[i:i+8000]
91
- if len(chunk) > 0:
92
- if self.recognizer.AcceptWaveform(chunk):
93
- result_json = self.recognizer.Result()
94
- result = json.loads(result_json)
95
- text = result.get('text', '').strip()
96
- if text:
97
- print(f" VOSK Final: '{text}'")
98
- # Reset buffer sau khi có kết quả
99
- self.audio_buffer = []
100
- return {"text": text, "partial": "", "is_final": True}
101
 
102
  # Kiểm tra partial result
103
  partial_json = self.recognizer.PartialResult()
@@ -107,13 +135,38 @@ class VoskStreamingASR:
107
  if partial_text:
108
  print(f"🎯 VOSK Partial: '{partial_text}'")
109
  return {"text": "", "partial": partial_text, "is_final": False}
110
- else:
111
- return {"text": "", "partial": "Đang xử lý âm thanh...", "is_final": False}
112
 
113
  except Exception as e:
114
  print(f"❌ Lỗi VOSK processing: {e}")
115
 
116
- return {"text": "", "partial": "", "is_final": False}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  class StreamingVoiceService:
119
  def __init__(self, groq_client: Groq, rag_system, tts_service):
@@ -128,30 +181,39 @@ class StreamingVoiceService:
128
  self.current_callback = None
129
 
130
  def start_listening(self, speech_callback: Callable) -> bool:
131
- """Bắt đầu lắng nghe - ĐƠN GIẢN"""
132
  if self.is_listening:
133
  return False
134
 
135
  self.current_callback = speech_callback
136
 
137
  if self.vosk_asr.model is None:
 
138
  return False
139
 
140
  if not self.vosk_asr.start_stream():
 
141
  return False
142
 
143
  self.is_listening = True
144
- print("🎙️ Đã bắt đầu lắng nghe")
145
  return True
146
 
147
  def process_streaming_audio(self, audio_data: tuple) -> Dict[str, Any]:
148
- """Xử lý audio streaming - ĐƠN GIẢN HIỆU QUẢ"""
149
  if not audio_data:
150
- return {"transcription": "Không có âm thanh", "response": "", "tts_audio": None, "status": "error"}
 
 
 
 
 
151
 
152
  try:
153
  sample_rate, audio_array = audio_data
154
 
 
 
155
  # Đảm bảo VOSK stream đang chạy
156
  if not self.vosk_asr.is_streaming:
157
  self.vosk_asr.start_stream()
@@ -159,7 +221,7 @@ class StreamingVoiceService:
159
  # Xử lý với VOSK
160
  result = self.vosk_asr.process_audio_chunk(audio_array, sample_rate)
161
 
162
- # LUÔN trả về kết quả partial để hiển thị real-time
163
  if result['partial']:
164
  return {
165
  'transcription': result['partial'],
@@ -169,30 +231,36 @@ class StreamingVoiceService:
169
  }
170
  elif result['is_final'] and result['text']:
171
  # Có kết quả cuối - tạo phản hồi AI
 
172
  response = self._generate_ai_response(result['text'])
173
  return {
174
  'transcription': result['text'],
175
  'response': response,
176
- 'tts_audio': None, # có thể thêm TTS sau
177
  'status': 'completed'
178
  }
179
  else:
180
  return {
181
- 'transcription': "🎤 Đang nghe... nói tiếp đi",
182
  'response': "",
183
  'tts_audio': None,
184
  'status': 'listening'
185
  }
186
 
187
  except Exception as e:
188
- print(f"❌ Lỗi: {e}")
189
- return {"transcription": f"Lỗi: {e}", "response": "", "tts_audio": None, "status": "error"}
 
 
 
 
 
190
 
191
  def _generate_ai_response(self, transcription: str) -> str:
192
  """Tạo phản hồi AI đơn giản"""
193
  try:
194
  messages = [
195
- {"role": "system", "content": "Bạn là trợ lý AI. Trả lời ngắn gọn bằng tiếng Việt."},
196
  {"role": "user", "content": transcription}
197
  ]
198
 
@@ -206,6 +274,7 @@ class StreamingVoiceService:
206
  return response.choices[0].message.content.strip()
207
 
208
  except Exception as e:
 
209
  return "Xin lỗi, tôi không thể trả lời ngay lúc này."
210
 
211
  def stop_listening(self):
@@ -214,6 +283,17 @@ class StreamingVoiceService:
214
  if self.vosk_asr:
215
  self.vosk_asr.stop_stream()
216
  print("🛑 Đã dừng lắng nghe")
 
 
 
 
 
 
 
 
 
 
 
217
  # import io
218
  # import numpy as np
219
  # import soundfile as sf
 
8
  import queue
9
  import json
10
  import os
11
+ import urllib.request
12
+ import zipfile
13
  from vosk import Model, KaldiRecognizer
14
  from groq import Groq
15
  from typing import Optional, Dict, Any, Callable
16
+
 
 
 
17
  class VoskStreamingASR:
18
  def __init__(self, model_path: str = None):
19
  self.model = None
 
21
  self.sample_rate = 16000
22
  self.is_streaming = False
23
 
24
+ # Buffer để tích luỹ audio
25
  self.audio_buffer = []
 
 
26
 
27
  if model_path is None:
28
  model_path = self._download_vosk_model()
 
37
  except Exception as e:
38
  print(f"❌ Lỗi khởi tạo VOSK model: {e}")
39
  else:
40
+ print(f"❌ Không tìm thấy VOSK model")
41
+
42
+ def _download_vosk_model(self):
43
+ """Tải VOSK model tiếng Việt tự động"""
44
+ try:
45
+ model_url = "https://alphacephei.com/vosk/models/vosk-model-small-vn-0.4.zip"
46
+ model_dir = "models/vosk-model-small-vn-0.4"
47
+ zip_path = "models/vosk-model-small-vn-0.4.zip"
48
+
49
+ # Tạo thư mục nếu chưa có
50
+ os.makedirs("models", exist_ok=True)
51
+
52
+ if not os.path.exists(model_dir):
53
+ print("📥 Đang tải VOSK Vietnamese model...")
54
+ urllib.request.urlretrieve(model_url, zip_path)
55
+
56
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
57
+ zip_ref.extractall("models/")
58
+
59
+ # Đảm bảo thư mục tồn tại
60
+ if os.path.exists("models/vosk-model-small-vn-0.4"):
61
+ os.rename("models/vosk-model-small-vn-0.4", model_dir)
62
+
63
+ if os.path.exists(zip_path):
64
+ os.remove(zip_path)
65
+ print("✅ Đã tải VOSK model thành công")
66
+
67
+ return model_dir if os.path.exists(model_dir) else None
68
+
69
+ except Exception as e:
70
+ print(f"❌ Lỗi tải VOSK model: {e}")
71
+ return None
72
 
73
  def start_stream(self):
74
  """Bắt đầu stream mới"""
 
87
  return False
88
 
89
  def process_audio_chunk(self, audio_chunk: np.ndarray, sample_rate: int = None) -> Dict[str, Any]:
90
+ """Xử lý audio chunk - SIMPLE & EFFECTIVE"""
91
  if self.recognizer is None or not self.is_streaming:
92
  return {"text": "", "partial": "", "is_final": False}
93
 
94
  try:
95
+ # Resample nếu cần
96
  if sample_rate and sample_rate != self.sample_rate:
97
  audio_chunk = self._resample_audio(audio_chunk, sample_rate, self.sample_rate)
98
 
99
  # Đảm bảo là int16
100
  if audio_chunk.dtype != np.int16:
101
+ if audio_chunk.dtype in [np.float32, np.float64]:
102
+ audio_chunk = (audio_chunk * 32767).astype(np.int16)
103
+ else:
104
+ audio_chunk = audio_chunk.astype(np.int16)
105
 
106
  # THÊM VÀO BUFFER - QUAN TRỌNG
107
  self.audio_buffer.extend(audio_chunk)
108
 
109
+ # Chỉ xử lý khi có đủ audio (ít nhất 1 giây)
110
+ if len(self.audio_buffer) < 16000:
111
  return {"text": "", "partial": "Đang nghe...", "is_final": False}
112
 
113
+ # Lấy audio từ buffer để xử lý (2 giây gần nhất)
114
+ process_audio = np.array(self.audio_buffer[-32000:], dtype=np.int16)
115
 
116
  # Chuyển sang bytes
117
  audio_bytes = process_audio.tobytes()
118
 
119
+ # Xử lý với VOSK
120
+ if self.recognizer.AcceptWaveform(audio_bytes):
121
+ result_json = self.recognizer.Result()
122
+ result = json.loads(result_json)
123
+ text = result.get('text', '').strip()
124
+ if text:
125
+ print(f"✅ VOSK Final: '{text}'")
126
+ # Reset buffer sau khi có kết quả
127
+ self.audio_buffer = []
128
+ return {"text": text, "partial": "", "is_final": True}
 
 
 
129
 
130
  # Kiểm tra partial result
131
  partial_json = self.recognizer.PartialResult()
 
135
  if partial_text:
136
  print(f"🎯 VOSK Partial: '{partial_text}'")
137
  return {"text": "", "partial": partial_text, "is_final": False}
 
 
138
 
139
  except Exception as e:
140
  print(f"❌ Lỗi VOSK processing: {e}")
141
 
142
+ return {"text": "", "partial": "Nói tiếp đi...", "is_final": False}
143
+
144
+ def _resample_audio(self, audio: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
145
+ """Resample audio"""
146
+ if orig_sr == target_sr:
147
+ return audio
148
+ try:
149
+ from scipy import signal
150
+ num_samples = int(len(audio) * target_sr / orig_sr)
151
+ resampled_audio = signal.resample(audio, num_samples)
152
+ return resampled_audio.astype(np.int16)
153
+ except Exception as e:
154
+ print(f"❌ Lỗi resample audio: {e}")
155
+ return audio
156
+
157
+ def stop_stream(self) -> str:
158
+ """Kết thúc stream và lấy kết quả cuối"""
159
+ if self.recognizer:
160
+ try:
161
+ result_json = self.recognizer.FinalResult()
162
+ result = json.loads(result_json)
163
+ text = result.get('text', '').strip()
164
+ self.is_streaming = False
165
+ print(f"🛑 VOSK Final: '{text}'")
166
+ return text
167
+ except Exception as e:
168
+ print(f"❌ Lỗi khi dừng VOSK stream: {e}")
169
+ return ""
170
 
171
  class StreamingVoiceService:
172
  def __init__(self, groq_client: Groq, rag_system, tts_service):
 
181
  self.current_callback = None
182
 
183
  def start_listening(self, speech_callback: Callable) -> bool:
184
+ """Bắt đầu lắng nghe"""
185
  if self.is_listening:
186
  return False
187
 
188
  self.current_callback = speech_callback
189
 
190
  if self.vosk_asr.model is None:
191
+ print("❌ VOSK model không khả dụng")
192
  return False
193
 
194
  if not self.vosk_asr.start_stream():
195
+ print("❌ Không thể khởi động VOSK stream")
196
  return False
197
 
198
  self.is_listening = True
199
+ print("🎙️ Đã bắt đầu lắng nghe với VOSK")
200
  return True
201
 
202
  def process_streaming_audio(self, audio_data: tuple) -> Dict[str, Any]:
203
+ """Xử lý audio streaming - ĐƠN GIẢN & HIỆU QUẢ"""
204
  if not audio_data:
205
+ return {
206
+ 'transcription': "Không có âm thanh",
207
+ 'response': "",
208
+ 'tts_audio': None,
209
+ 'status': 'error'
210
+ }
211
 
212
  try:
213
  sample_rate, audio_array = audio_data
214
 
215
+ print(f"🎤 Nhận audio: {len(audio_array)} samples")
216
+
217
  # Đảm bảo VOSK stream đang chạy
218
  if not self.vosk_asr.is_streaming:
219
  self.vosk_asr.start_stream()
 
221
  # Xử lý với VOSK
222
  result = self.vosk_asr.process_audio_chunk(audio_array, sample_rate)
223
 
224
+ # LUÔN trả về text để hiển thị real-time
225
  if result['partial']:
226
  return {
227
  'transcription': result['partial'],
 
231
  }
232
  elif result['is_final'] and result['text']:
233
  # Có kết quả cuối - tạo phản hồi AI
234
+ print(f"📝 Final transcription: '{result['text']}'")
235
  response = self._generate_ai_response(result['text'])
236
  return {
237
  'transcription': result['text'],
238
  'response': response,
239
+ 'tts_audio': None,
240
  'status': 'completed'
241
  }
242
  else:
243
  return {
244
+ 'transcription': "🎤 Đang nghe... tiếp tục nói",
245
  'response': "",
246
  'tts_audio': None,
247
  'status': 'listening'
248
  }
249
 
250
  except Exception as e:
251
+ print(f"❌ Lỗi xử lý audio: {e}")
252
+ return {
253
+ 'transcription': f"Lỗi: {e}",
254
+ 'response': "",
255
+ 'tts_audio': None,
256
+ 'status': 'error'
257
+ }
258
 
259
  def _generate_ai_response(self, transcription: str) -> str:
260
  """Tạo phản hồi AI đơn giản"""
261
  try:
262
  messages = [
263
+ {"role": "system", "content": "Bạn là trợ lý AI thân thiện. Trả lời ngắn gọn bằng tiếng Việt."},
264
  {"role": "user", "content": transcription}
265
  ]
266
 
 
274
  return response.choices[0].message.content.strip()
275
 
276
  except Exception as e:
277
+ print(f"❌ Lỗi AI: {e}")
278
  return "Xin lỗi, tôi không thể trả lời ngay lúc này."
279
 
280
  def stop_listening(self):
 
283
  if self.vosk_asr:
284
  self.vosk_asr.stop_stream()
285
  print("🛑 Đã dừng lắng nghe")
286
+
287
+ def clear_conversation(self):
288
+ """Xóa lịch sử hội thoại"""
289
+ print("🗑️ Đã xóa lịch sử hội thoại")
290
+
291
+ def get_conversation_state(self) -> dict:
292
+ """Lấy trạng thái hội thoại"""
293
+ return {
294
+ 'is_listening': self.is_listening,
295
+ 'vosk_active': self.vosk_asr.is_streaming if self.vosk_asr else False
296
+ }
297
  # import io
298
  # import numpy as np
299
  # import soundfile as sf