Update services/streaming_voice_service.py
Browse files
services/streaming_voice_service.py
CHANGED
|
@@ -39,6 +39,15 @@ class StreamingVoiceService:
|
|
| 39 |
self.response_queue = queue.Queue()
|
| 40 |
self.current_task = None
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def start_listening(self, speech_callback: Callable) -> bool:
|
| 43 |
"""Bắt đầu lắng nghe với VAD tối ưu"""
|
| 44 |
if self.is_listening:
|
|
@@ -99,11 +108,13 @@ class StreamingVoiceService:
|
|
| 99 |
|
| 100 |
with self.processing_lock:
|
| 101 |
self.is_processing = True
|
| 102 |
-
|
| 103 |
try:
|
| 104 |
# Chuyển đổi speech thành text
|
|
|
|
|
|
|
| 105 |
transcription = self._transcribe_audio(speech_audio, sample_rate)
|
| 106 |
-
|
| 107 |
if not transcription or len(transcription.strip()) < 2:
|
| 108 |
print("⚠️ Transcription quá ngắn hoặc trống")
|
| 109 |
return
|
|
@@ -112,11 +123,21 @@ class StreamingVoiceService:
|
|
| 112 |
self.current_transcription = transcription
|
| 113 |
|
| 114 |
# Tạo phản hồi AI
|
|
|
|
| 115 |
response = self._generate_ai_response(transcription)
|
| 116 |
-
|
| 117 |
# Tạo TTS
|
|
|
|
| 118 |
tts_audio_path = self._text_to_speech(response)
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
# Gửi kết quả đến callback
|
| 121 |
if self.speech_callback:
|
| 122 |
self.speech_callback({
|
|
@@ -132,7 +153,7 @@ class StreamingVoiceService:
|
|
| 132 |
finally:
|
| 133 |
with self.processing_lock:
|
| 134 |
self.is_processing = False
|
| 135 |
-
|
| 136 |
def process_streaming_audio(self, audio_data: tuple) -> Dict[str, Any]:
|
| 137 |
"""Xử lý audio streaming (phương thức cũ cho compatibility với Gradio)"""
|
| 138 |
if not audio_data:
|
|
@@ -284,6 +305,7 @@ class StreamingVoiceService:
|
|
| 284 |
|
| 285 |
def _transcribe_audio(self, audio_data: np.ndarray, sample_rate: int) -> Optional[str]:
|
| 286 |
"""Chuyển audio -> text với xử lý cải tiến"""
|
|
|
|
| 287 |
try:
|
| 288 |
# Đảm bảo kiểu dữ liệu và chuẩn hóa
|
| 289 |
if audio_data.dtype != np.int16:
|
|
@@ -303,13 +325,13 @@ class StreamingVoiceService:
|
|
| 303 |
sample_rate = target_sample_rate
|
| 304 |
|
| 305 |
# Giới hạn độ dài audio
|
| 306 |
-
max_duration =
|
| 307 |
max_samples = sample_rate * max_duration
|
| 308 |
if len(audio_data) > max_samples:
|
| 309 |
audio_data = audio_data[:max_samples]
|
| 310 |
|
| 311 |
# Đảm bảo audio đủ dài
|
| 312 |
-
min_duration =
|
| 313 |
min_samples = int(sample_rate * min_duration)
|
| 314 |
if len(audio_data) < min_samples:
|
| 315 |
padding = np.zeros(min_samples - len(audio_data), dtype=np.int16)
|
|
@@ -322,7 +344,9 @@ class StreamingVoiceService:
|
|
| 322 |
sf.write(buffer, audio_data, sample_rate, format='wav', subtype='PCM_16')
|
| 323 |
buffer.seek(0)
|
| 324 |
|
|
|
|
| 325 |
# Gọi API Whisper
|
|
|
|
| 326 |
try:
|
| 327 |
transcription = self.client.audio.transcriptions.create(
|
| 328 |
model=settings.WHISPER_MODEL,
|
|
@@ -334,7 +358,7 @@ class StreamingVoiceService:
|
|
| 334 |
except Exception as e:
|
| 335 |
print(f"❌ Lỗi Whisper API: {e}")
|
| 336 |
return None
|
| 337 |
-
|
| 338 |
# Xử lý response
|
| 339 |
if hasattr(transcription, 'text'):
|
| 340 |
result = transcription.text.strip()
|
|
@@ -342,7 +366,8 @@ class StreamingVoiceService:
|
|
| 342 |
result = transcription.strip()
|
| 343 |
else:
|
| 344 |
result = str(transcription).strip()
|
| 345 |
-
|
|
|
|
| 346 |
print(f"✅ Transcription: '{result}'")
|
| 347 |
return result
|
| 348 |
|
|
@@ -352,12 +377,15 @@ class StreamingVoiceService:
|
|
| 352 |
|
| 353 |
def _generate_ai_response(self, user_input: str) -> str:
|
| 354 |
"""Sinh phản hồi AI với xử lý lỗi"""
|
|
|
|
| 355 |
try:
|
| 356 |
# Thêm vào lịch sử
|
| 357 |
self.conversation_history.append({"role": "user", "content": user_input})
|
| 358 |
|
| 359 |
# Tìm ki���m RAG
|
|
|
|
| 360 |
rag_results = self.rag_system.semantic_search(user_input, top_k=2)
|
|
|
|
| 361 |
context_text = "\n".join([f"- {result.get('text', str(result))}" for result in rag_results]) if rag_results else ""
|
| 362 |
|
| 363 |
system_prompt = f"""Bạn là trợ lý AI thông minh chuyên về tiếng Việt.
|
|
@@ -371,19 +399,21 @@ Thông tin tham khảo:
|
|
| 371 |
messages.extend(self.conversation_history[-6:])
|
| 372 |
|
| 373 |
completion = self.client.chat.completions.create(
|
| 374 |
-
model=
|
| 375 |
messages=messages,
|
| 376 |
-
max_tokens=
|
| 377 |
temperature=0.7
|
| 378 |
)
|
| 379 |
-
|
| 380 |
response = completion.choices[0].message.content
|
| 381 |
self.conversation_history.append({"role": "assistant", "content": response})
|
| 382 |
-
|
| 383 |
# Giới hạn lịch sử
|
| 384 |
if len(self.conversation_history) > 12:
|
| 385 |
self.conversation_history = self.conversation_history[-12:]
|
| 386 |
-
|
|
|
|
|
|
|
| 387 |
return response
|
| 388 |
|
| 389 |
except Exception as e:
|
|
@@ -391,14 +421,18 @@ Thông tin tham khảo:
|
|
| 391 |
return "Xin lỗi, tôi gặp lỗi khi tạo phản hồi. Vui lòng thử lại."
|
| 392 |
|
| 393 |
def _text_to_speech(self, text: str) -> Optional[str]:
|
| 394 |
-
"""Chuyển văn bản thành giọng nói"""
|
|
|
|
| 395 |
try:
|
| 396 |
if not text or text.startswith("❌") or text.startswith("Xin lỗi"):
|
| 397 |
return None
|
| 398 |
|
| 399 |
tts_bytes = self.tts_service.text_to_speech(text, 'vi')
|
|
|
|
|
|
|
| 400 |
if tts_bytes:
|
| 401 |
audio_path = self.tts_service.save_audio_to_file(tts_bytes)
|
|
|
|
| 402 |
print(f"✅ Đã tạo TTS: {audio_path}")
|
| 403 |
return audio_path
|
| 404 |
except Exception as e:
|
|
@@ -421,7 +455,51 @@ Thông tin tham khảo:
|
|
| 421 |
self.conversation_history = []
|
| 422 |
self.current_transcription = ""
|
| 423 |
print("🗑️ Đã xóa lịch sử hội thoại")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
def get_conversation_state(self) -> dict:
|
| 426 |
"""Lấy trạng thái hội thoại"""
|
| 427 |
return {
|
|
|
|
| 39 |
self.response_queue = queue.Queue()
|
| 40 |
self.current_task = None
|
| 41 |
|
| 42 |
+
#Latency
|
| 43 |
+
self.latency_metrics = {
|
| 44 |
+
'asr': [],
|
| 45 |
+
'rag' : [],
|
| 46 |
+
'llm' : [],
|
| 47 |
+
'tts' : [],
|
| 48 |
+
'total' : []
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
def start_listening(self, speech_callback: Callable) -> bool:
|
| 52 |
"""Bắt đầu lắng nghe với VAD tối ưu"""
|
| 53 |
if self.is_listening:
|
|
|
|
| 108 |
|
| 109 |
with self.processing_lock:
|
| 110 |
self.is_processing = True
|
| 111 |
+
total_start_time = time.time()
|
| 112 |
try:
|
| 113 |
# Chuyển đổi speech thành text
|
| 114 |
+
# 1. ASR
|
| 115 |
+
asr_start = time.time()
|
| 116 |
transcription = self._transcribe_audio(speech_audio, sample_rate)
|
| 117 |
+
asr_latency = time.time() - asr_start
|
| 118 |
if not transcription or len(transcription.strip()) < 2:
|
| 119 |
print("⚠️ Transcription quá ngắn hoặc trống")
|
| 120 |
return
|
|
|
|
| 123 |
self.current_transcription = transcription
|
| 124 |
|
| 125 |
# Tạo phản hồi AI
|
| 126 |
+
rag_start = time.time()
|
| 127 |
response = self._generate_ai_response(transcription)
|
| 128 |
+
rag_latency = time.time() - rag_start
|
| 129 |
# Tạo TTS
|
| 130 |
+
tts_start = time.time()
|
| 131 |
tts_audio_path = self._text_to_speech(response)
|
| 132 |
+
tts_latency = time.time() - tts_start
|
| 133 |
+
total_latency = time.time() - total_start_time
|
| 134 |
+
# Log latency metrics
|
| 135 |
+
self._log_latency_metrics({
|
| 136 |
+
'asr': asr_latency,
|
| 137 |
+
'rag': rag_latency,
|
| 138 |
+
'tts': tts_latency,
|
| 139 |
+
'total': total_latency
|
| 140 |
+
})
|
| 141 |
# Gửi kết quả đến callback
|
| 142 |
if self.speech_callback:
|
| 143 |
self.speech_callback({
|
|
|
|
| 153 |
finally:
|
| 154 |
with self.processing_lock:
|
| 155 |
self.is_processing = False
|
| 156 |
+
|
| 157 |
def process_streaming_audio(self, audio_data: tuple) -> Dict[str, Any]:
|
| 158 |
"""Xử lý audio streaming (phương thức cũ cho compatibility với Gradio)"""
|
| 159 |
if not audio_data:
|
|
|
|
| 305 |
|
| 306 |
def _transcribe_audio(self, audio_data: np.ndarray, sample_rate: int) -> Optional[str]:
|
| 307 |
"""Chuyển audio -> text với xử lý cải tiến"""
|
| 308 |
+
asr_start = time.time()
|
| 309 |
try:
|
| 310 |
# Đảm bảo kiểu dữ liệu và chuẩn hóa
|
| 311 |
if audio_data.dtype != np.int16:
|
|
|
|
| 325 |
sample_rate = target_sample_rate
|
| 326 |
|
| 327 |
# Giới hạn độ dài audio
|
| 328 |
+
max_duration = 30 # giây
|
| 329 |
max_samples = sample_rate * max_duration
|
| 330 |
if len(audio_data) > max_samples:
|
| 331 |
audio_data = audio_data[:max_samples]
|
| 332 |
|
| 333 |
# Đảm bảo audio đủ dài
|
| 334 |
+
min_duration = 2 # giây
|
| 335 |
min_samples = int(sample_rate * min_duration)
|
| 336 |
if len(audio_data) < min_samples:
|
| 337 |
padding = np.zeros(min_samples - len(audio_data), dtype=np.int16)
|
|
|
|
| 344 |
sf.write(buffer, audio_data, sample_rate, format='wav', subtype='PCM_16')
|
| 345 |
buffer.seek(0)
|
| 346 |
|
| 347 |
+
|
| 348 |
# Gọi API Whisper
|
| 349 |
+
api_start = time.time()
|
| 350 |
try:
|
| 351 |
transcription = self.client.audio.transcriptions.create(
|
| 352 |
model=settings.WHISPER_MODEL,
|
|
|
|
| 358 |
except Exception as e:
|
| 359 |
print(f"❌ Lỗi Whisper API: {e}")
|
| 360 |
return None
|
| 361 |
+
api_latency = time.time() - api_start
|
| 362 |
# Xử lý response
|
| 363 |
if hasattr(transcription, 'text'):
|
| 364 |
result = transcription.text.strip()
|
|
|
|
| 366 |
result = transcription.strip()
|
| 367 |
else:
|
| 368 |
result = str(transcription).strip()
|
| 369 |
+
total_asr_latency = time.time() - asr_start
|
| 370 |
+
print(f"✅ ASR Latency: {total_asr_latency:.2f}s (API: {api_latency:.2f}s)")
|
| 371 |
print(f"✅ Transcription: '{result}'")
|
| 372 |
return result
|
| 373 |
|
|
|
|
| 377 |
|
| 378 |
def _generate_ai_response(self, user_input: str) -> str:
|
| 379 |
"""Sinh phản hồi AI với xử lý lỗi"""
|
| 380 |
+
llm_start = time.time()
|
| 381 |
try:
|
| 382 |
# Thêm vào lịch sử
|
| 383 |
self.conversation_history.append({"role": "user", "content": user_input})
|
| 384 |
|
| 385 |
# Tìm ki���m RAG
|
| 386 |
+
rag_start = time.time()
|
| 387 |
rag_results = self.rag_system.semantic_search(user_input, top_k=2)
|
| 388 |
+
rag_latency = time.time() - rag_start
|
| 389 |
context_text = "\n".join([f"- {result.get('text', str(result))}" for result in rag_results]) if rag_results else ""
|
| 390 |
|
| 391 |
system_prompt = f"""Bạn là trợ lý AI thông minh chuyên về tiếng Việt.
|
|
|
|
| 399 |
messages.extend(self.conversation_history[-6:])
|
| 400 |
|
| 401 |
completion = self.client.chat.completions.create(
|
| 402 |
+
model=settings.MULTILINGUAL_LLM_MODEL,
|
| 403 |
messages=messages,
|
| 404 |
+
max_tokens=300,
|
| 405 |
temperature=0.7
|
| 406 |
)
|
| 407 |
+
ttft = time.time() - llm_inference_start # Time To First Token
|
| 408 |
response = completion.choices[0].message.content
|
| 409 |
self.conversation_history.append({"role": "assistant", "content": response})
|
| 410 |
+
total_llm_latency = time.time() - llm_start
|
| 411 |
# Giới hạn lịch sử
|
| 412 |
if len(self.conversation_history) > 12:
|
| 413 |
self.conversation_history = self.conversation_history[-12:]
|
| 414 |
+
print(f"✅ RAG Latency: {rag_latency:.2f}s")
|
| 415 |
+
print(f"✅ LLM TTFT: {ttft:.2f}s")
|
| 416 |
+
print(f"✅ Total LLM Latency: {total_llm_latency:.2f}s")
|
| 417 |
return response
|
| 418 |
|
| 419 |
except Exception as e:
|
|
|
|
| 421 |
return "Xin lỗi, tôi gặp lỗi khi tạo phản hồi. Vui lòng thử lại."
|
| 422 |
|
| 423 |
def _text_to_speech(self, text: str) -> Optional[str]:
|
| 424 |
+
"""Chuyển văn bản thành giọng nói với latency tracking"""
|
| 425 |
+
tts_start = time.time()
|
| 426 |
try:
|
| 427 |
if not text or text.startswith("❌") or text.startswith("Xin lỗi"):
|
| 428 |
return None
|
| 429 |
|
| 430 |
tts_bytes = self.tts_service.text_to_speech(text, 'vi')
|
| 431 |
+
tts_latency = time.time() - tts_start
|
| 432 |
+
|
| 433 |
if tts_bytes:
|
| 434 |
audio_path = self.tts_service.save_audio_to_file(tts_bytes)
|
| 435 |
+
print(f"✅ TTS Latency: {tts_latency:.2f}s")
|
| 436 |
print(f"✅ Đã tạo TTS: {audio_path}")
|
| 437 |
return audio_path
|
| 438 |
except Exception as e:
|
|
|
|
| 455 |
self.conversation_history = []
|
| 456 |
self.current_transcription = ""
|
| 457 |
print("🗑️ Đã xóa lịch sử hội thoại")
|
| 458 |
+
def _log_latency_metrics(self, latencies: dict):
|
| 459 |
+
"""Log và theo dõi latency metrics"""
|
| 460 |
+
for key, value in latencies.items():
|
| 461 |
+
if key in self.latency_metrics:
|
| 462 |
+
self.latency_metrics[key].append(value)
|
| 463 |
+
|
| 464 |
+
# Giữ chỉ 100 mẫu gần nhất
|
| 465 |
+
if len(self.latency_metrics[key]) > 100:
|
| 466 |
+
self.latency_metrics[key] = self.latency_metrics[key][-100:]
|
| 467 |
+
|
| 468 |
+
# Log tổng hợp
|
| 469 |
+
print("📊 LATENCY REPORT:")
|
| 470 |
+
print(f" ASR: {latencies['asr']:.2f}s")
|
| 471 |
+
print(f" RAG: {latencies['rag']:.2f}s")
|
| 472 |
+
print(f" TTS: {latencies['tts']:.2f}s")
|
| 473 |
+
print(f" TOTAL: {latencies['total']:.2f}s")
|
| 474 |
+
|
| 475 |
+
# Tính toán và hiển thị latency trung bình
|
| 476 |
+
self._print_average_latencies()
|
| 477 |
+
|
| 478 |
+
def _print_average_latencies(self):
|
| 479 |
+
"""In ra latency trung bình"""
|
| 480 |
+
if len(self.latency_metrics['total']) > 0:
|
| 481 |
+
print("📈 AVERAGE LATENCIES (last 10 requests):")
|
| 482 |
+
for component in ['asr', 'rag', 'tts', 'total']:
|
| 483 |
+
recent_latencies = self.latency_metrics[component][-10:]
|
| 484 |
+
if recent_latencies:
|
| 485 |
+
avg = sum(recent_latencies) / len(recent_latencies)
|
| 486 |
+
print(f" {component.upper()}: {avg:.2f}s")
|
| 487 |
|
| 488 |
+
def get_latency_stats(self) -> dict:
|
| 489 |
+
"""Lấy thống kê latency"""
|
| 490 |
+
stats = {}
|
| 491 |
+
for component, latencies in self.latency_metrics.items():
|
| 492 |
+
if latencies:
|
| 493 |
+
stats[component] = {
|
| 494 |
+
'avg': sum(latencies) / len(latencies),
|
| 495 |
+
'min': min(latencies),
|
| 496 |
+
'max': max(latencies),
|
| 497 |
+
'count': len(latencies),
|
| 498 |
+
'recent_avg': sum(latencies[-10:]) / min(10, len(latencies)) if latencies else 0
|
| 499 |
+
}
|
| 500 |
+
else:
|
| 501 |
+
stats[component] = {'avg': 0, 'min': 0, 'max': 0, 'count': 0, 'recent_avg': 0}
|
| 502 |
+
return stats
|
| 503 |
def get_conversation_state(self) -> dict:
|
| 504 |
"""Lấy trạng thái hội thoại"""
|
| 505 |
return {
|