voicebot offical
Browse files- app.py +0 -1211
- config/__pycache__/settings.cpython-310.pyc +0 -0
- config/settings.py +45 -0
- core/__pycache__/multilingual_manager.cpython-310.pyc +0 -0
- core/__pycache__/rag_system.cpython-310.pyc +0 -0
- core/__pycache__/speechbrain_vad.cpython-310.pyc +0 -0
- core/__pycache__/tts_service.cpython-310.pyc +0 -0
- core/__pycache__/wikipedia_processor.cpython-310.pyc +0 -0
- core/multilingual_manager.py +146 -0
- core/rag_system.py +262 -0
- core/speechbrain_vad.py +154 -0
- core/tts_service.py +189 -0
- core/wikipedia_processor.py +74 -0
- main.py +50 -0
- models/__pycache__/schemas.cpython-310.pyc +0 -0
- models/schemas.py +22 -0
- requirements.txt +1 -1
- services/__pycache__/audio_service.cpython-310.pyc +0 -0
- services/__pycache__/chat_service.cpython-310.pyc +0 -0
- services/__pycache__/image_service.cpython-310.pyc +0 -0
- services/__pycache__/streaming_voice_service.cpython-310.pyc +0 -0
- services/audio_service.py +91 -0
- services/chat_service.py +69 -0
- services/image_service.py +33 -0
- services/streaming_voice_service.py +205 -0
- ui/__pycache__/components.cpython-310.pyc +0 -0
- ui/__pycache__/tabs.cpython-310.pyc +0 -0
- ui/components.py +174 -0
- ui/tabs.py +315 -0
- utils/helpers.py +14 -0
app.py
DELETED
|
@@ -1,1211 +0,0 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
-
import groq
|
| 3 |
-
import os
|
| 4 |
-
import io
|
| 5 |
-
import numpy as np
|
| 6 |
-
import soundfile as sf
|
| 7 |
-
from PIL import Image
|
| 8 |
-
from dotenv import load_dotenv
|
| 9 |
-
import pandas as pd
|
| 10 |
-
import json
|
| 11 |
-
from typing import List, Dict
|
| 12 |
-
from sentence_transformers import SentenceTransformer
|
| 13 |
-
import faiss
|
| 14 |
-
import time
|
| 15 |
-
import re
|
| 16 |
-
from gtts import gTTS
|
| 17 |
-
import edge_tts
|
| 18 |
-
import asyncio
|
| 19 |
-
|
| 20 |
-
# //load_dotenv()
|
| 21 |
-
api_key = os.getenv("GROQ_API_KEY")
|
| 22 |
-
# Get the GROQ_API_KEY from environment variables
|
| 23 |
-
# api_key = os.environ.get("GROQ_API_KEY")
|
| 24 |
-
if not api_key:
|
| 25 |
-
raise ValueError("Please set the GROQ_API_KEY environment variable.")
|
| 26 |
-
|
| 27 |
-
# Initialize the Groq client
|
| 28 |
-
client = groq.Client(api_key=api_key)
|
| 29 |
-
|
| 30 |
-
# Initialize Vietnamese embedding model
|
| 31 |
-
print("🔄 Đang tải mô hình embedding tiếng Việt...")
|
| 32 |
-
try:
|
| 33 |
-
vietnamese_embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
|
| 34 |
-
print("✅ Đã tải mô hình embedding tiếng Việt")
|
| 35 |
-
except Exception as e:
|
| 36 |
-
print(f"❌ Lỗi tải mô hình embedding: {e}")
|
| 37 |
-
vietnamese_embedder = None
|
| 38 |
-
|
| 39 |
-
# Enhanced RAG system with Vietnamese embeddings
|
| 40 |
-
class EnhancedRAGSystem:
|
| 41 |
-
def __init__(self):
|
| 42 |
-
self.documents = []
|
| 43 |
-
self.metadatas = []
|
| 44 |
-
self.embeddings = None
|
| 45 |
-
self.index = None
|
| 46 |
-
self.dimension = 384 # Dimension for Vietnamese SBERT
|
| 47 |
-
|
| 48 |
-
print("✅ Đã khởi tạo Enhanced RAG system với embedding tiếng Việt")
|
| 49 |
-
|
| 50 |
-
# Initialize sample nutrition data in Vietnamese
|
| 51 |
-
self._initialize_sample_data()
|
| 52 |
-
|
| 53 |
-
def _initialize_sample_data(self):
|
| 54 |
-
"""Khởi tạo dữ liệu dinh dưỡng mẫu bằng tiếng Việt"""
|
| 55 |
-
nutrition_data = [
|
| 56 |
-
"Chế độ ăn Địa Trung Hải giàu rau củ, trái cây, ngũ cốc nguyên hạt và dầu olive tốt cho tim mạch",
|
| 57 |
-
"Protein từ thịt gà, cá hồi và đậu phụ giúp xây dựng cơ bắp và duy trì sức khỏe",
|
| 58 |
-
"Trái cây họ cam quýt như cam, bưởi cung cấp vitamin C tăng cường hệ miễn dịch",
|
| 59 |
-
"Rau xanh như cải bó xôi, bông cải xanh chứa nhiều chất xơ và vitamin K",
|
| 60 |
-
"Cá hồi giàu omega-3 tốt cho não bộ và sức khỏe tim mạch",
|
| 61 |
-
"Các loại hạt như hạnh nhân, óc chó cung cấp chất béo lành mạnh và protein",
|
| 62 |
-
"Sữa chua Hy Lạp chứa probiotic tốt cho hệ tiêu hóa và giàu protein",
|
| 63 |
-
"Gạo lứt và yến mạch là nguồn carbohydrate phức tạp cung cấp năng lượng lâu dài"
|
| 64 |
-
]
|
| 65 |
-
|
| 66 |
-
self.add_documents(nutrition_data, [{"type": "nutrition", "source": "sample", "language": "vi"}] * len(nutrition_data))
|
| 67 |
-
|
| 68 |
-
def add_documents(self, documents: List[str], metadatas: List[Dict] = None):
|
| 69 |
-
"""Thêm documents vào database với embedding"""
|
| 70 |
-
if not documents:
|
| 71 |
-
return
|
| 72 |
-
|
| 73 |
-
# Generate embeddings for new documents
|
| 74 |
-
if vietnamese_embedder is not None:
|
| 75 |
-
try:
|
| 76 |
-
new_embeddings = vietnamese_embedder.encode(documents)
|
| 77 |
-
|
| 78 |
-
if self.embeddings is None:
|
| 79 |
-
self.embeddings = new_embeddings
|
| 80 |
-
else:
|
| 81 |
-
self.embeddings = np.vstack([self.embeddings, new_embeddings])
|
| 82 |
-
|
| 83 |
-
# Update FAISS index
|
| 84 |
-
self._update_faiss_index()
|
| 85 |
-
|
| 86 |
-
except Exception as e:
|
| 87 |
-
print(f"❌ Lỗi tạo embedding: {e}")
|
| 88 |
-
|
| 89 |
-
self.documents.extend(documents)
|
| 90 |
-
self.metadatas.extend(metadatas or [{}] * len(documents))
|
| 91 |
-
print(f"✅ Đã thêm {len(documents)} documents vào RAG database với embedding")
|
| 92 |
-
|
| 93 |
-
def _update_faiss_index(self):
|
| 94 |
-
"""Cập nhật FAISS index với embeddings hiện tại"""
|
| 95 |
-
if self.embeddings is None or len(self.embeddings) == 0:
|
| 96 |
-
return
|
| 97 |
-
|
| 98 |
-
try:
|
| 99 |
-
dimension = self.embeddings.shape[1]
|
| 100 |
-
self.index = faiss.IndexFlatIP(dimension) # Inner product for cosine similarity
|
| 101 |
-
self.index.add(self.embeddings.astype(np.float32))
|
| 102 |
-
except Exception as e:
|
| 103 |
-
print(f"❌ Lỗi cập nhật FAISS index: {e}")
|
| 104 |
-
|
| 105 |
-
def semantic_search(self, query: str, top_k: int = 3) -> List[Dict]:
|
| 106 |
-
"""Tìm kiếm ngữ nghĩa sử dụng embedding tiếng Việt"""
|
| 107 |
-
if not self.documents or self.index is None:
|
| 108 |
-
return self._fallback_keyword_search(query, top_k)
|
| 109 |
-
|
| 110 |
-
try:
|
| 111 |
-
# Encode query using Vietnamese embedder
|
| 112 |
-
query_embedding = vietnamese_embedder.encode([query])
|
| 113 |
-
|
| 114 |
-
# Search in FAISS index
|
| 115 |
-
similarities, indices = self.index.search(query_embedding.astype(np.float32), min(top_k, len(self.documents)))
|
| 116 |
-
|
| 117 |
-
results = []
|
| 118 |
-
for i, (similarity, idx) in enumerate(zip(similarities[0], indices[0])):
|
| 119 |
-
if idx < len(self.documents):
|
| 120 |
-
results.append({
|
| 121 |
-
'id': str(idx),
|
| 122 |
-
'text': self.documents[idx],
|
| 123 |
-
'similarity': float(similarity),
|
| 124 |
-
'metadata': self.metadatas[idx] if idx < len(self.metadatas) else {}
|
| 125 |
-
})
|
| 126 |
-
|
| 127 |
-
return results
|
| 128 |
-
|
| 129 |
-
except Exception as e:
|
| 130 |
-
print(f"❌ Lỗi tìm kiếm ngữ nghĩa: {e}")
|
| 131 |
-
return self._fallback_keyword_search(query, top_k)
|
| 132 |
-
|
| 133 |
-
def _fallback_keyword_search(self, query: str, top_k: int = 3) -> List[Dict]:
|
| 134 |
-
"""Tìm kiếm dự phòng dựa trên từ khóa"""
|
| 135 |
-
query_lower = query.lower()
|
| 136 |
-
results = []
|
| 137 |
-
|
| 138 |
-
for i, doc in enumerate(self.documents):
|
| 139 |
-
score = 0
|
| 140 |
-
for word in query_lower.split():
|
| 141 |
-
if word in doc.lower():
|
| 142 |
-
score += 1
|
| 143 |
-
|
| 144 |
-
if score > 0:
|
| 145 |
-
results.append({
|
| 146 |
-
'id': str(i),
|
| 147 |
-
'text': doc,
|
| 148 |
-
'similarity': min(score / 5, 1.0),
|
| 149 |
-
'metadata': self.metadatas[i] if i < len(self.metadatas) else {}
|
| 150 |
-
})
|
| 151 |
-
|
| 152 |
-
results.sort(key=lambda x: x['similarity'], reverse=True)
|
| 153 |
-
return results[:top_k]
|
| 154 |
-
|
| 155 |
-
def get_collection_stats(self) -> Dict:
|
| 156 |
-
"""Lấy thống kê collection"""
|
| 157 |
-
return {
|
| 158 |
-
'count': len(self.documents),
|
| 159 |
-
'embedding_count': len(self.embeddings) if self.embeddings is not None else 0,
|
| 160 |
-
'name': 'enhanced_rag_vi',
|
| 161 |
-
'status': 'active',
|
| 162 |
-
'has_embeddings': self.embeddings is not None
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
class WikipediaProcessor:
|
| 166 |
-
def __init__(self):
|
| 167 |
-
self.supported_formats = ['.txt', '.csv', '.json']
|
| 168 |
-
|
| 169 |
-
def process_uploaded_file(self, file_path: str) -> List[str]:
|
| 170 |
-
"""Xử lý file Wikipedia uploaded"""
|
| 171 |
-
file_ext = os.path.splitext(file_path)[1].lower()
|
| 172 |
-
|
| 173 |
-
try:
|
| 174 |
-
if file_ext == '.txt':
|
| 175 |
-
return self._process_txt_file(file_path)
|
| 176 |
-
elif file_ext == '.csv':
|
| 177 |
-
return self._process_csv_file(file_path)
|
| 178 |
-
elif file_ext == '.json':
|
| 179 |
-
return self._process_json_file(file_path)
|
| 180 |
-
else:
|
| 181 |
-
raise ValueError(f"Định dạng file không được hỗ trợ: {file_ext}")
|
| 182 |
-
except Exception as e:
|
| 183 |
-
raise Exception(f"Lỗi xử lý file: {str(e)}")
|
| 184 |
-
|
| 185 |
-
def _process_txt_file(self, file_path: str) -> List[str]:
|
| 186 |
-
"""Xử lý file text"""
|
| 187 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 188 |
-
content = f.read()
|
| 189 |
-
|
| 190 |
-
paragraphs = [p.strip() for p in content.split('\n\n') if p.strip() and len(p.strip()) > 20]
|
| 191 |
-
return paragraphs
|
| 192 |
-
|
| 193 |
-
def _process_csv_file(self, file_path: str) -> List[str]:
|
| 194 |
-
"""Xử lý file CSV"""
|
| 195 |
-
try:
|
| 196 |
-
df = pd.read_csv(file_path)
|
| 197 |
-
documents = []
|
| 198 |
-
|
| 199 |
-
for _, row in df.iterrows():
|
| 200 |
-
doc_parts = []
|
| 201 |
-
for col in df.columns:
|
| 202 |
-
if pd.notna(row[col]) and str(row[col]).strip():
|
| 203 |
-
doc_parts.append(f"{col}: {row[col]}")
|
| 204 |
-
if doc_parts:
|
| 205 |
-
documents.append(" | ".join(doc_parts))
|
| 206 |
-
|
| 207 |
-
return documents
|
| 208 |
-
except Exception as e:
|
| 209 |
-
raise Exception(f"Lỗi đọc CSV: {str(e)}")
|
| 210 |
-
|
| 211 |
-
def _process_json_file(self, file_path: str) -> List[str]:
|
| 212 |
-
"""Xử lý file JSON"""
|
| 213 |
-
try:
|
| 214 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 215 |
-
data = json.load(f)
|
| 216 |
-
|
| 217 |
-
documents = []
|
| 218 |
-
|
| 219 |
-
def extract_text(obj, current_path=""):
|
| 220 |
-
if isinstance(obj, dict):
|
| 221 |
-
for key, value in obj.items():
|
| 222 |
-
extract_text(value, f"{current_path}.{key}" if current_path else key)
|
| 223 |
-
elif isinstance(obj, list):
|
| 224 |
-
for item in obj:
|
| 225 |
-
extract_text(item, current_path)
|
| 226 |
-
elif isinstance(obj, str) and len(obj.strip()) > 10:
|
| 227 |
-
documents.append(f"{current_path}: {obj.strip()}")
|
| 228 |
-
|
| 229 |
-
extract_text(data)
|
| 230 |
-
return documents
|
| 231 |
-
except Exception as e:
|
| 232 |
-
raise Exception(f"Lỗi đọc JSON: {str(e)}")
|
| 233 |
-
|
| 234 |
-
# Enhanced TTS Service with multiple providers and chunking
|
| 235 |
-
class EnhancedTTSService:
|
| 236 |
-
def __init__(self):
|
| 237 |
-
self.supported_languages = {
|
| 238 |
-
'vi': 'vi', # Vietnamese
|
| 239 |
-
'en': 'en', # English
|
| 240 |
-
'fr': 'fr', # French
|
| 241 |
-
'es': 'es', # Spanish
|
| 242 |
-
'de': 'de', # German
|
| 243 |
-
'ja': 'ja', # Japanese
|
| 244 |
-
'ko': 'ko', # Korean
|
| 245 |
-
'zh': 'zh' # Chinese
|
| 246 |
-
}
|
| 247 |
-
self.max_chunk_length = 200 # Maximum characters per TTS request
|
| 248 |
-
|
| 249 |
-
def detect_language(self, text: str) -> str:
|
| 250 |
-
"""Đơn giản phát hiện ngôn ngữ dựa trên ký tự"""
|
| 251 |
-
vietnamese_chars = set('àáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ')
|
| 252 |
-
if any(char in vietnamese_chars for char in text.lower()):
|
| 253 |
-
return 'vi'
|
| 254 |
-
# Simple detection for other languages
|
| 255 |
-
elif any(char in text for char in 'あいうえお'): # Japanese
|
| 256 |
-
return 'ja'
|
| 257 |
-
elif any(char in text for char in '你好'): # Chinese
|
| 258 |
-
return 'zh'
|
| 259 |
-
elif any(char in text for char in '안녕'): # Korean
|
| 260 |
-
return 'ko'
|
| 261 |
-
else:
|
| 262 |
-
return 'en' # Default to English
|
| 263 |
-
|
| 264 |
-
def split_text_into_chunks(self, text: str, max_length: int = None) -> List[str]:
|
| 265 |
-
"""Chia văn bản thành các đoạn nhỏ cho TTS"""
|
| 266 |
-
if max_length is None:
|
| 267 |
-
max_length = self.max_chunk_length
|
| 268 |
-
|
| 269 |
-
# Split by sentences first
|
| 270 |
-
sentences = re.split(r'[.!?]+', text)
|
| 271 |
-
chunks = []
|
| 272 |
-
current_chunk = ""
|
| 273 |
-
|
| 274 |
-
for sentence in sentences:
|
| 275 |
-
sentence = sentence.strip()
|
| 276 |
-
if not sentence:
|
| 277 |
-
continue
|
| 278 |
-
|
| 279 |
-
# If sentence is too long, split by commas
|
| 280 |
-
if len(sentence) > max_length:
|
| 281 |
-
parts = re.split(r'[,;:]', sentence)
|
| 282 |
-
for part in parts:
|
| 283 |
-
part = part.strip()
|
| 284 |
-
if not part:
|
| 285 |
-
continue
|
| 286 |
-
if len(current_chunk) + len(part) + 2 <= max_length:
|
| 287 |
-
if current_chunk:
|
| 288 |
-
current_chunk += ". " + part
|
| 289 |
-
else:
|
| 290 |
-
current_chunk = part
|
| 291 |
-
else:
|
| 292 |
-
if current_chunk:
|
| 293 |
-
chunks.append(current_chunk)
|
| 294 |
-
current_chunk = part
|
| 295 |
-
else:
|
| 296 |
-
if len(current_chunk) + len(sentence) + 2 <= max_length:
|
| 297 |
-
if current_chunk:
|
| 298 |
-
current_chunk += ". " + sentence
|
| 299 |
-
else:
|
| 300 |
-
current_chunk = sentence
|
| 301 |
-
else:
|
| 302 |
-
if current_chunk:
|
| 303 |
-
chunks.append(current_chunk)
|
| 304 |
-
current_chunk = sentence
|
| 305 |
-
|
| 306 |
-
if current_chunk:
|
| 307 |
-
chunks.append(current_chunk)
|
| 308 |
-
|
| 309 |
-
return chunks
|
| 310 |
-
|
| 311 |
-
def text_to_speech_gtts(self, text: str, language: str = 'vi') -> bytes:
|
| 312 |
-
"""Sử dụng gTTS (Google Text-to-Speech) library"""
|
| 313 |
-
try:
|
| 314 |
-
# Split long text into chunks
|
| 315 |
-
chunks = self.split_text_into_chunks(text)
|
| 316 |
-
audio_chunks = []
|
| 317 |
-
|
| 318 |
-
for chunk in chunks:
|
| 319 |
-
if not chunk.strip():
|
| 320 |
-
continue
|
| 321 |
-
|
| 322 |
-
tts = gTTS(text=chunk, lang=language, slow=False)
|
| 323 |
-
audio_buffer = io.BytesIO()
|
| 324 |
-
tts.write_to_fp(audio_buffer)
|
| 325 |
-
audio_buffer.seek(0)
|
| 326 |
-
audio_chunks.append(audio_buffer.read())
|
| 327 |
-
|
| 328 |
-
# Small delay between requests
|
| 329 |
-
time.sleep(0.1)
|
| 330 |
-
|
| 331 |
-
# Combine all audio chunks
|
| 332 |
-
if audio_chunks:
|
| 333 |
-
return b''.join(audio_chunks)
|
| 334 |
-
return None
|
| 335 |
-
|
| 336 |
-
except Exception as e:
|
| 337 |
-
print(f"❌ Lỗi gTTS: {e}")
|
| 338 |
-
return None
|
| 339 |
-
|
| 340 |
-
async def text_to_speech_edgetts(self, text: str, voice: str = 'vi-VN-NamMinhNeural') -> bytes:
|
| 341 |
-
"""Sử dụng Edge-TTS (Microsoft Edge) - async version"""
|
| 342 |
-
try:
|
| 343 |
-
communicate = edge_tts.Communicate(text, voice)
|
| 344 |
-
audio_buffer = io.BytesIO()
|
| 345 |
-
|
| 346 |
-
async for chunk in communicate.stream():
|
| 347 |
-
if chunk["type"] == "audio":
|
| 348 |
-
audio_buffer.write(chunk["data"])
|
| 349 |
-
|
| 350 |
-
audio_buffer.seek(0)
|
| 351 |
-
return audio_buffer.read()
|
| 352 |
-
|
| 353 |
-
except Exception as e:
|
| 354 |
-
print(f"❌ Lỗi Edge-TTS: {e}")
|
| 355 |
-
return None
|
| 356 |
-
|
| 357 |
-
def text_to_speech_edgetts_sync(self, text: str, voice: str = 'vi-VN-NamMinhNeural') -> bytes:
|
| 358 |
-
"""Sync wrapper for Edge-TTS"""
|
| 359 |
-
try:
|
| 360 |
-
return asyncio.run(self.text_to_speech_edgetts(text, voice))
|
| 361 |
-
except Exception as e:
|
| 362 |
-
print(f"❌ Lỗi Edge-TTS sync: {e}")
|
| 363 |
-
return None
|
| 364 |
-
|
| 365 |
-
def text_to_speech_fallback(self, text: str, language: str = 'vi') -> bytes:
|
| 366 |
-
"""Fallback TTS using simple method"""
|
| 367 |
-
try:
|
| 368 |
-
# Use gTTS as fallback
|
| 369 |
-
return self.text_to_speech_gtts(text, language)
|
| 370 |
-
except Exception as e:
|
| 371 |
-
print(f"❌ Lỗi fallback TTS: {e}")
|
| 372 |
-
return None
|
| 373 |
-
|
| 374 |
-
def text_to_speech(self, text: str, language: str = None, provider: str = "auto") -> bytes:
|
| 375 |
-
"""Chuyển văn b���n thành giọng nói với nhiều nhà cung cấp"""
|
| 376 |
-
if not text or len(text.strip()) == 0:
|
| 377 |
-
return None
|
| 378 |
-
|
| 379 |
-
if language is None:
|
| 380 |
-
language = self.detect_language(text)
|
| 381 |
-
|
| 382 |
-
# Clean and prepare text
|
| 383 |
-
text = self.clean_text(text)
|
| 384 |
-
|
| 385 |
-
try:
|
| 386 |
-
if provider == "auto" or provider == "gtts":
|
| 387 |
-
print(f"🔊 Đang sử dụng gTTS cho văn bản {len(text)} ký tự...")
|
| 388 |
-
audio_bytes = self.text_to_speech_gtts(text, language)
|
| 389 |
-
if audio_bytes:
|
| 390 |
-
return audio_bytes
|
| 391 |
-
|
| 392 |
-
if provider == "auto" or provider == "edgetts":
|
| 393 |
-
print(f"🔊 Đang thử Edge-TTS cho văn bản {len(text)} ký tự...")
|
| 394 |
-
voice_map = {
|
| 395 |
-
'vi': 'vi-VN-NamMinhNeural',
|
| 396 |
-
'en': 'en-US-AriaNeural',
|
| 397 |
-
'fr': 'fr-FR-DeniseNeural',
|
| 398 |
-
'es': 'es-ES-ElviraNeural',
|
| 399 |
-
'de': 'de-DE-KatjaNeural',
|
| 400 |
-
'ja': 'ja-JP-NanamiNeural',
|
| 401 |
-
'ko': 'ko-KR-SunHiNeural',
|
| 402 |
-
'zh': 'zh-CN-XiaoxiaoNeural'
|
| 403 |
-
}
|
| 404 |
-
voice = voice_map.get(language, 'vi-VN-NamMinhNeural')
|
| 405 |
-
audio_bytes = self.text_to_speech_edgetts_sync(text, voice)
|
| 406 |
-
if audio_bytes:
|
| 407 |
-
return audio_bytes
|
| 408 |
-
|
| 409 |
-
# Final fallback
|
| 410 |
-
print(f"🔊 Đang sử dụng fallback TTS...")
|
| 411 |
-
return self.text_to_speech_fallback(text, language)
|
| 412 |
-
|
| 413 |
-
except Exception as e:
|
| 414 |
-
print(f"❌ Lỗi TTS tổng hợp: {e}")
|
| 415 |
-
return None
|
| 416 |
-
|
| 417 |
-
def clean_text(self, text: str) -> str:
|
| 418 |
-
"""Làm sạch văn bản trước khi chuyển thành giọng nói"""
|
| 419 |
-
# Remove URLs
|
| 420 |
-
text = re.sub(r'http\S+', '', text)
|
| 421 |
-
# Remove special characters but keep Vietnamese diacritics
|
| 422 |
-
text = re.sub(r'[^\w\sàáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ.,!?;:()-]', '', text)
|
| 423 |
-
# Replace multiple spaces with single space
|
| 424 |
-
text = re.sub(r'\s+', ' ', text)
|
| 425 |
-
# Remove extra whitespace
|
| 426 |
-
text = text.strip()
|
| 427 |
-
return text
|
| 428 |
-
|
| 429 |
-
def save_audio_to_file(self, audio_bytes: bytes, filename: str = None) -> str:
|
| 430 |
-
"""Lưu audio bytes thành file tạm thời"""
|
| 431 |
-
if audio_bytes is None:
|
| 432 |
-
return None
|
| 433 |
-
|
| 434 |
-
if filename is None:
|
| 435 |
-
filename = f"tts_output_{int(time.time())}.mp3"
|
| 436 |
-
|
| 437 |
-
temp_dir = "temp_audio"
|
| 438 |
-
os.makedirs(temp_dir, exist_ok=True)
|
| 439 |
-
|
| 440 |
-
filepath = os.path.join(temp_dir, filename)
|
| 441 |
-
with open(filepath, 'wb') as f:
|
| 442 |
-
f.write(audio_bytes)
|
| 443 |
-
|
| 444 |
-
return filepath
|
| 445 |
-
|
| 446 |
-
# Initialize systems
|
| 447 |
-
rag_system = EnhancedRAGSystem()
|
| 448 |
-
wikipedia_processor = WikipediaProcessor()
|
| 449 |
-
tts_service = EnhancedTTSService()
|
| 450 |
-
|
| 451 |
-
# Audio utility functions
|
| 452 |
-
def numpy_to_mp3(audio_array: np.ndarray, sampling_rate: int = 24000) -> bytes:
|
| 453 |
-
"""Convert numpy array to MP3 bytes"""
|
| 454 |
-
buffer = io.BytesIO()
|
| 455 |
-
sf.write(buffer, audio_array, sampling_rate, format='mp3')
|
| 456 |
-
buffer.seek(0)
|
| 457 |
-
return buffer.read()
|
| 458 |
-
|
| 459 |
-
def transcribe_audio(audio):
|
| 460 |
-
"""Chuyển đổi audio thành văn bản và tạo phản hồi với TTS"""
|
| 461 |
-
if audio is None:
|
| 462 |
-
return "No audio provided.", "", None
|
| 463 |
-
|
| 464 |
-
sr, y = audio
|
| 465 |
-
|
| 466 |
-
# Convert to mono if stereo
|
| 467 |
-
if y.ndim > 1:
|
| 468 |
-
y = y.mean(axis=1)
|
| 469 |
-
|
| 470 |
-
# Normalize audio
|
| 471 |
-
y = y.astype(np.float32)
|
| 472 |
-
y /= np.max(np.abs(y))
|
| 473 |
-
|
| 474 |
-
# Write audio to buffer
|
| 475 |
-
buffer = io.BytesIO()
|
| 476 |
-
sf.write(buffer, y, sr, format='wav')
|
| 477 |
-
buffer.seek(0)
|
| 478 |
-
|
| 479 |
-
try:
|
| 480 |
-
# Use Whisper model for transcription
|
| 481 |
-
completion = client.audio.transcriptions.create(
|
| 482 |
-
model="whisper-large-v3-turbo",
|
| 483 |
-
file=("audio.wav", buffer),
|
| 484 |
-
response_format="text"
|
| 485 |
-
)
|
| 486 |
-
transcription = completion
|
| 487 |
-
except Exception as e:
|
| 488 |
-
transcription = f"Error in transcription: {str(e)}"
|
| 489 |
-
|
| 490 |
-
response = generate_response_with_rag(transcription)
|
| 491 |
-
|
| 492 |
-
# Generate TTS audio for response
|
| 493 |
-
tts_audio = None
|
| 494 |
-
if response and not response.startswith("Error"):
|
| 495 |
-
tts_bytes = tts_service.text_to_speech(response, 'vi')
|
| 496 |
-
if tts_bytes:
|
| 497 |
-
tts_audio_path = tts_service.save_audio_to_file(tts_bytes)
|
| 498 |
-
tts_audio = tts_audio_path
|
| 499 |
-
|
| 500 |
-
return transcription, response, tts_audio
|
| 501 |
-
|
| 502 |
-
def generate_response_with_rag(user_input):
|
| 503 |
-
"""Tạo phản hồi sử dụng RAG với embedding tiếng Việt"""
|
| 504 |
-
if not user_input or user_input.startswith("Error"):
|
| 505 |
-
return "No valid input available. Please try again."
|
| 506 |
-
|
| 507 |
-
try:
|
| 508 |
-
# Tìm kiếm thông tin liên quan từ RAG với embedding tiếng Việt
|
| 509 |
-
rag_results = rag_system.semantic_search(user_input, top_k=3)
|
| 510 |
-
|
| 511 |
-
# Tạo context từ RAG results
|
| 512 |
-
context_text = ""
|
| 513 |
-
if rag_results:
|
| 514 |
-
context_text = "\n".join([f"- {doc['text']}" for doc in rag_results])
|
| 515 |
-
|
| 516 |
-
# System prompt với RAG context
|
| 517 |
-
system_prompt = """Bạn là trợ lý AI thông minh chuyên về tiếng Việt. Hãy sử dụng thông tin từ cơ sở kiến thức được cung cấp để trả lời câu hỏi một cách chính xác và hữu ích bằng tiếng Việt.
|
| 518 |
-
|
| 519 |
-
Thông tin tham khảo từ cơ sở kiến thức:
|
| 520 |
-
{context}
|
| 521 |
-
|
| 522 |
-
Nếu thông tin từ cơ sở kiến thức không đủ để trả lời, hãy dựa vào kiến thức chung của bạn. Luôn trả lời bằng tiếng Việt tự nhiên và dễ hiểu."""
|
| 523 |
-
|
| 524 |
-
messages = [
|
| 525 |
-
{"role": "system", "content": system_prompt.format(context=context_text)},
|
| 526 |
-
{"role": "user", "content": user_input}
|
| 527 |
-
]
|
| 528 |
-
|
| 529 |
-
# Use Llama 3.3 70B model for text generation với RAG context
|
| 530 |
-
completion = client.chat.completions.create(
|
| 531 |
-
model="llama-3.3-70b-versatile",
|
| 532 |
-
messages=messages,
|
| 533 |
-
)
|
| 534 |
-
return completion.choices[0].message.content
|
| 535 |
-
except Exception as e:
|
| 536 |
-
return f"Error in response generation: {str(e)}"
|
| 537 |
-
|
| 538 |
-
def analyze_image_with_description(image, user_description):
|
| 539 |
-
"""Phân tích hình ảnh kết hợp với mô tả từ người dùng"""
|
| 540 |
-
if image is None:
|
| 541 |
-
return "No image uploaded."
|
| 542 |
-
|
| 543 |
-
try:
|
| 544 |
-
if user_description:
|
| 545 |
-
prompt = f"""Người dùng tải lên một hình ảnh và mô tả: "{user_description}"
|
| 546 |
-
|
| 547 |
-
Dựa trên mô tả này, hãy phân tích chi tiết bằng tiếng Việt:
|
| 548 |
-
1. Mô tả những gì có trong hình ảnh
|
| 549 |
-
2. Nếu là thức ăn: ước tính dinh dưỡng (calo, protein, carbs, chất béo, chất xơ)
|
| 550 |
-
3. Nếu là cảnh quan/con người: mô tả chi tiết và ý nghĩa
|
| 551 |
-
4. Đưa ra nhận xét hoặc lời khuyên liên quan"""
|
| 552 |
-
else:
|
| 553 |
-
prompt = """Hãy mô tả chi tiết bằng tiếng Việt những gì bạn nghĩ có thể có trong hình ảnh này. Nếu là thức ăn, hãy ước tính giá trị dinh dưỡng. Nếu là cảnh quan hoặc con người, hãy mô tả chi tiết."""
|
| 554 |
-
|
| 555 |
-
chat_completion = client.chat.completions.create(
|
| 556 |
-
messages=[
|
| 557 |
-
{
|
| 558 |
-
"role": "user",
|
| 559 |
-
"content": prompt
|
| 560 |
-
}
|
| 561 |
-
],
|
| 562 |
-
model="llama-3.3-70b-versatile",
|
| 563 |
-
)
|
| 564 |
-
description = chat_completion.choices[0].message.content
|
| 565 |
-
except Exception as e:
|
| 566 |
-
description = f"Error in image analysis: {str(e)}"
|
| 567 |
-
|
| 568 |
-
return description
|
| 569 |
-
|
| 570 |
-
def respond(message, chat_history):
|
| 571 |
-
"""Xử lý chat với TTS output"""
|
| 572 |
-
if chat_history is None:
|
| 573 |
-
chat_history = []
|
| 574 |
-
|
| 575 |
-
# Prepare the message history for the API
|
| 576 |
-
messages = []
|
| 577 |
-
for user_msg, assistant_msg in chat_history:
|
| 578 |
-
messages.append({"role": "user", "content": user_msg})
|
| 579 |
-
messages.append({"role": "assistant", "content": assistant_msg})
|
| 580 |
-
|
| 581 |
-
messages.append({"role": "user", "content": message})
|
| 582 |
-
|
| 583 |
-
try:
|
| 584 |
-
# Sử dụng RAG để tìm kiếm thông tin liên quan
|
| 585 |
-
rag_results = rag_system.semantic_search(message, top_k=2)
|
| 586 |
-
|
| 587 |
-
# Thêm context từ RAG vào system prompt
|
| 588 |
-
context_text = ""
|
| 589 |
-
if rag_results:
|
| 590 |
-
context_text = "\nThông tin tham khảo:\n" + "\n".join([f"- {doc['text']}" for doc in rag_results])
|
| 591 |
-
|
| 592 |
-
system_message = {
|
| 593 |
-
"role": "system",
|
| 594 |
-
"content": f"Bạn là trợ lý AI hữu ích chuyên về tiếng Việt. Sử dụng thông tin từ cơ sở kiến thức khi có liên quan. Luôn trả lời bằng tiếng Việt tự nhiên.{context_text}"
|
| 595 |
-
}
|
| 596 |
-
|
| 597 |
-
# Chèn system message vào đầu
|
| 598 |
-
messages_with_context = [system_message] + messages
|
| 599 |
-
|
| 600 |
-
# Use Llama 3.3 70B model for generating assistant response
|
| 601 |
-
completion = client.chat.completions.create(
|
| 602 |
-
model="llama-3.3-70b-versatile",
|
| 603 |
-
messages=messages_with_context,
|
| 604 |
-
)
|
| 605 |
-
assistant_message = completion.choices[0].message.content
|
| 606 |
-
chat_history.append((message, assistant_message))
|
| 607 |
-
|
| 608 |
-
# Generate TTS audio for the response
|
| 609 |
-
tts_audio_path = None
|
| 610 |
-
if assistant_message and not assistant_message.startswith("Error"):
|
| 611 |
-
tts_bytes = tts_service.text_to_speech(assistant_message, 'vi')
|
| 612 |
-
if tts_bytes:
|
| 613 |
-
tts_audio_path = tts_service.save_audio_to_file(tts_bytes)
|
| 614 |
-
|
| 615 |
-
except Exception as e:
|
| 616 |
-
assistant_message = f"Error: {str(e)}"
|
| 617 |
-
chat_history.append((message, assistant_message))
|
| 618 |
-
tts_audio_path = None
|
| 619 |
-
|
| 620 |
-
return "", chat_history, chat_history, tts_audio_path
|
| 621 |
-
|
| 622 |
-
def upload_wikipedia_file(file):
|
| 623 |
-
"""Xử lý upload file Wikipedia"""
|
| 624 |
-
if file is None:
|
| 625 |
-
return "Vui lòng chọn file để upload"
|
| 626 |
-
|
| 627 |
-
try:
|
| 628 |
-
documents = wikipedia_processor.process_uploaded_file(file.name)
|
| 629 |
-
|
| 630 |
-
if not documents:
|
| 631 |
-
return "Không tìm thấy dữ liệu nào trong file."
|
| 632 |
-
|
| 633 |
-
# Thêm metadata
|
| 634 |
-
metadatas = [{"source": "wikipedia", "type": "knowledge", "file": os.path.basename(file.name), "language": "vi"} for _ in documents]
|
| 635 |
-
|
| 636 |
-
rag_system.add_documents(documents, metadatas)
|
| 637 |
-
|
| 638 |
-
stats = rag_system.get_collection_stats()
|
| 639 |
-
return f"✅ Đã thêm {len(documents)} documents Wikipedia vào RAG database. Tổng số documents: {stats['count']}, Embeddings: {stats['embedding_count']}"
|
| 640 |
-
|
| 641 |
-
except Exception as e:
|
| 642 |
-
return f"❌ Lỗi xử lý file Wikipedia: {str(e)}"
|
| 643 |
-
|
| 644 |
-
def get_rag_stats():
|
| 645 |
-
"""Lấy thống kê RAG database"""
|
| 646 |
-
stats = rag_system.get_collection_stats()
|
| 647 |
-
return f"📊 Thống kê RAG Database:\n- Tổng documents: {stats['count']}\n- Embeddings: {stats['embedding_count']}\n- Trạng thái: {stats['status']}\n- Hỗ trợ embedding: {stats['has_embeddings']}"
|
| 648 |
-
|
| 649 |
-
def search_rag_database(query):
|
| 650 |
-
"""Tìm kiếm trong RAG database để debug"""
|
| 651 |
-
if not query.strip():
|
| 652 |
-
return []
|
| 653 |
-
|
| 654 |
-
results = rag_system.semantic_search(query, top_k=5)
|
| 655 |
-
return results
|
| 656 |
-
|
| 657 |
-
def clear_chat_history(chat_history):
|
| 658 |
-
"""Xóa lịch sử chat"""
|
| 659 |
-
return [], []
|
| 660 |
-
|
| 661 |
-
# Enhanced Streaming Voice AI Functions with TTS
|
| 662 |
-
def generate_streaming_response(audio_file):
|
| 663 |
-
"""Generate response for streaming voice AI với TTS"""
|
| 664 |
-
if audio_file is None:
|
| 665 |
-
return None, "No audio provided", None
|
| 666 |
-
|
| 667 |
-
try:
|
| 668 |
-
# Transcribe audio using Whisper
|
| 669 |
-
with open(audio_file, "rb") as f:
|
| 670 |
-
transcription = client.audio.transcriptions.create(
|
| 671 |
-
model="whisper-large-v3-turbo",
|
| 672 |
-
file=f,
|
| 673 |
-
response_format="text"
|
| 674 |
-
)
|
| 675 |
-
|
| 676 |
-
# Generate response using RAG với embedding tiếng Việt
|
| 677 |
-
rag_results = rag_system.semantic_search(transcription, top_k=2)
|
| 678 |
-
context_text = ""
|
| 679 |
-
if rag_results:
|
| 680 |
-
context_text = "\nThông tin tham khảo:\n" + "\n".join([f"- {doc['text']}" for doc in rag_results])
|
| 681 |
-
|
| 682 |
-
system_prompt = f"""Bạn là trợ lý AI thông minh và thân thiện chuyên về tiếng Việt. Hãy trả lời câu hỏi một cách tự nhiên và hữu ích bằng tiếng Việt. Sử dụng thông tin từ cơ sở kiến thức khi có liên quan.{context_text}"""
|
| 683 |
-
|
| 684 |
-
messages = [
|
| 685 |
-
{"role": "system", "content": system_prompt},
|
| 686 |
-
{"role": "user", "content": transcription}
|
| 687 |
-
]
|
| 688 |
-
|
| 689 |
-
completion = client.chat.completions.create(
|
| 690 |
-
model="llama-3.3-70b-versatile",
|
| 691 |
-
messages=messages,
|
| 692 |
-
max_tokens=150
|
| 693 |
-
)
|
| 694 |
-
|
| 695 |
-
response = completion.choices[0].message.content
|
| 696 |
-
|
| 697 |
-
# Generate TTS audio
|
| 698 |
-
tts_audio_bytes = tts_service.text_to_speech(response, 'vi')
|
| 699 |
-
if tts_audio_bytes:
|
| 700 |
-
# Save to temporary file for audio output
|
| 701 |
-
temp_audio_file = tts_service.save_audio_to_file(tts_audio_bytes)
|
| 702 |
-
return response, response, temp_audio_file
|
| 703 |
-
|
| 704 |
-
return response, response, None
|
| 705 |
-
|
| 706 |
-
except Exception as e:
|
| 707 |
-
error_msg = f"Error in streaming response: {str(e)}"
|
| 708 |
-
return None, error_msg, None
|
| 709 |
-
|
| 710 |
-
def read_streaming_response(answer):
|
| 711 |
-
"""Read response aloud using TTS"""
|
| 712 |
-
if not answer:
|
| 713 |
-
return answer, None
|
| 714 |
-
|
| 715 |
-
try:
|
| 716 |
-
tts_audio_bytes = tts_service.text_to_speech(answer, 'vi')
|
| 717 |
-
if tts_audio_bytes:
|
| 718 |
-
temp_audio_file = tts_service.save_audio_to_file(tts_audio_bytes)
|
| 719 |
-
return answer, temp_audio_file
|
| 720 |
-
except Exception as e:
|
| 721 |
-
print(f"❌ Lỗi TTS: {e}")
|
| 722 |
-
|
| 723 |
-
return answer, None
|
| 724 |
-
|
| 725 |
-
# Enhanced Magic 8 Ball Functions with Vietnamese responses
|
| 726 |
-
def generate_magic_8_ball_response(audio_file):
|
| 727 |
-
"""Generate Magic 8 Ball response for audio input với tiếng Việt"""
|
| 728 |
-
if audio_file is None:
|
| 729 |
-
return None, "No audio provided", None
|
| 730 |
-
|
| 731 |
-
try:
|
| 732 |
-
# Transcribe audio using Whisper
|
| 733 |
-
with open(audio_file, "rb") as f:
|
| 734 |
-
transcription = client.audio.transcriptions.create(
|
| 735 |
-
model="whisper-large-v3-turbo",
|
| 736 |
-
file=f,
|
| 737 |
-
response_format="text"
|
| 738 |
-
)
|
| 739 |
-
|
| 740 |
-
# Magic 8 Ball system prompt in Vietnamese
|
| 741 |
-
messages = [
|
| 742 |
-
{
|
| 743 |
-
"role": "system",
|
| 744 |
-
"content": (
|
| 745 |
-
)
|
| 746 |
-
},
|
| 747 |
-
{
|
| 748 |
-
"role": "user",
|
| 749 |
-
"content": f"Quả cầu pha lê xin hãy trả lời câu hỏi này - {transcription}"
|
| 750 |
-
}
|
| 751 |
-
]
|
| 752 |
-
|
| 753 |
-
completion = client.chat.completions.create(
|
| 754 |
-
model="llama-3.3-70b-versatile",
|
| 755 |
-
messages=messages,
|
| 756 |
-
max_tokens=64,
|
| 757 |
-
temperature=0.8
|
| 758 |
-
)
|
| 759 |
-
|
| 760 |
-
response = completion.choices[0].message.content
|
| 761 |
-
# Clean up response
|
| 762 |
-
response = response.replace("Magic 8 Ball", "").replace("Quả cầu pha lê", "").replace(":", "").strip()
|
| 763 |
-
|
| 764 |
-
# Generate TTS audio
|
| 765 |
-
tts_audio_bytes = tts_service.text_to_speech(response, 'vi')
|
| 766 |
-
if tts_audio_bytes:
|
| 767 |
-
temp_audio_file = tts_service.save_audio_to_file(tts_audio_bytes)
|
| 768 |
-
return response, response, temp_audio_file
|
| 769 |
-
|
| 770 |
-
return response, response, None
|
| 771 |
-
|
| 772 |
-
except Exception as e:
|
| 773 |
-
error_msg = f"Error in Magic 8 Ball response: {str(e)}"
|
| 774 |
-
return None, error_msg, None
|
| 775 |
-
|
| 776 |
-
def read_magic_8_ball_response(answer):
|
| 777 |
-
"""Read Magic 8 Ball response aloud using TTS"""
|
| 778 |
-
if not answer:
|
| 779 |
-
return answer, None
|
| 780 |
-
|
| 781 |
-
try:
|
| 782 |
-
tts_audio_bytes = tts_service.text_to_speech(answer, 'vi')
|
| 783 |
-
if tts_audio_bytes:
|
| 784 |
-
temp_audio_file = tts_service.save_audio_to_file(tts_audio_bytes)
|
| 785 |
-
return answer, temp_audio_file
|
| 786 |
-
except Exception as e:
|
| 787 |
-
print(f"❌ Lỗi TTS: {e}")
|
| 788 |
-
|
| 789 |
-
return answer, None
|
| 790 |
-
|
| 791 |
-
# Text-to-Speech standalone function
|
| 792 |
-
def text_to_speech_standalone(text, language, tts_provider):
|
| 793 |
-
"""Chức năng TTS độc lập"""
|
| 794 |
-
if not text:
|
| 795 |
-
return None
|
| 796 |
-
|
| 797 |
-
try:
|
| 798 |
-
tts_audio_bytes = tts_service.text_to_speech(text, language, tts_provider)
|
| 799 |
-
if tts_audio_bytes:
|
| 800 |
-
temp_audio_file = tts_service.save_audio_to_file(tts_audio_bytes)
|
| 801 |
-
return temp_audio_file
|
| 802 |
-
except Exception as e:
|
| 803 |
-
print(f"❌ Lỗi TTS: {e}")
|
| 804 |
-
|
| 805 |
-
return None
|
| 806 |
-
|
| 807 |
-
# Custom CSS (giữ nguyên)
|
| 808 |
-
custom_css = """
|
| 809 |
-
.gradio-container {
|
| 810 |
-
background-color: #1f1f1f;
|
| 811 |
-
}
|
| 812 |
-
|
| 813 |
-
.gr-markdown, .gr-markdown * {
|
| 814 |
-
color: #ffffff !important;
|
| 815 |
-
}
|
| 816 |
-
|
| 817 |
-
.gr-textbox, .gr-textbox * {
|
| 818 |
-
color: #ffffff !important;
|
| 819 |
-
}
|
| 820 |
-
|
| 821 |
-
.gr-label, .gr-label * {
|
| 822 |
-
color: #ffffff !important;
|
| 823 |
-
}
|
| 824 |
-
|
| 825 |
-
.gr-chatbot, .gr-chatbot * {
|
| 826 |
-
color: #ffffff !important;
|
| 827 |
-
}
|
| 828 |
-
|
| 829 |
-
.gr-json, .gr-json * {
|
| 830 |
-
color: #ffffff !important;
|
| 831 |
-
}
|
| 832 |
-
|
| 833 |
-
.gr-box, .gr-block, .panel, .tab-item {
|
| 834 |
-
background-color: #2d2d2d !important;
|
| 835 |
-
border-color: #444444 !important;
|
| 836 |
-
}
|
| 837 |
-
|
| 838 |
-
input, textarea, select {
|
| 839 |
-
color: #ffffff !important;
|
| 840 |
-
background-color: #2d2d2d !important;
|
| 841 |
-
border-color: #444444 !important;
|
| 842 |
-
}
|
| 843 |
-
|
| 844 |
-
::placeholder {
|
| 845 |
-
color: #aaaaaa !important;
|
| 846 |
-
}
|
| 847 |
-
|
| 848 |
-
.message {
|
| 849 |
-
color: #ffffff !important;
|
| 850 |
-
}
|
| 851 |
-
|
| 852 |
-
.user-message, .bot-message {
|
| 853 |
-
color: #ffffff !important;
|
| 854 |
-
}
|
| 855 |
-
|
| 856 |
-
h1, h2, h3, h4, h5, h6 {
|
| 857 |
-
color: #ffffff !important;
|
| 858 |
-
}
|
| 859 |
-
|
| 860 |
-
.tab-nav {
|
| 861 |
-
color: #ffffff !important;
|
| 862 |
-
}
|
| 863 |
-
|
| 864 |
-
.tab-item {
|
| 865 |
-
color: #ffffff !important;
|
| 866 |
-
}
|
| 867 |
-
|
| 868 |
-
.gr-button {
|
| 869 |
-
color: #ffffff !important;
|
| 870 |
-
background-color: #f55036 !important;
|
| 871 |
-
border-color: #f55036 !important;
|
| 872 |
-
}
|
| 873 |
-
|
| 874 |
-
.gr-button-secondary {
|
| 875 |
-
color: #ffffff !important;
|
| 876 |
-
background-color: #666666 !important;
|
| 877 |
-
border-color: #666666 !important;
|
| 878 |
-
}
|
| 879 |
-
|
| 880 |
-
.gr-file, .gr-file * {
|
| 881 |
-
color: #ffffff !important;
|
| 882 |
-
}
|
| 883 |
-
|
| 884 |
-
.gr-json {
|
| 885 |
-
background-color: #2d2d2d !important;
|
| 886 |
-
}
|
| 887 |
-
|
| 888 |
-
.gr-audio, .gr-audio * {
|
| 889 |
-
color: #ffffff !important;
|
| 890 |
-
}
|
| 891 |
-
|
| 892 |
-
.gr-image, .gr-image * {
|
| 893 |
-
color: #ffffff !important;
|
| 894 |
-
}
|
| 895 |
-
|
| 896 |
-
.form, .form * {
|
| 897 |
-
color: #ffffff !important;
|
| 898 |
-
}
|
| 899 |
-
|
| 900 |
-
.block, .block * {
|
| 901 |
-
color: #ffffff !important;
|
| 902 |
-
}
|
| 903 |
-
|
| 904 |
-
div[data-testid="block"] {
|
| 905 |
-
background-color: #2d2d2d !important;
|
| 906 |
-
color: #ffffff !important;
|
| 907 |
-
}
|
| 908 |
-
|
| 909 |
-
.gr-component, .gr-component * {
|
| 910 |
-
color: #ffffff !important;
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
#groq-badge {
|
| 914 |
-
position: fixed;
|
| 915 |
-
bottom: 20px;
|
| 916 |
-
right: 20px;
|
| 917 |
-
z-index: 1000;
|
| 918 |
-
color: #ffffff !important;
|
| 919 |
-
}
|
| 920 |
-
|
| 921 |
-
.streaming-voice-container {
|
| 922 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 923 |
-
padding: 20px;
|
| 924 |
-
border-radius: 10px;
|
| 925 |
-
margin: 10px 0;
|
| 926 |
-
}
|
| 927 |
-
|
| 928 |
-
.magic-8-ball-container {
|
| 929 |
-
background: linear-gradient(135deg, #1a2a6c 0%, #b21f1f 50%, #fdbb2d 100%);
|
| 930 |
-
padding: 20px;
|
| 931 |
-
border-radius: 15px;
|
| 932 |
-
margin: 10px 0;
|
| 933 |
-
text-align: center;
|
| 934 |
-
border: 3px solid #ffffff;
|
| 935 |
-
}
|
| 936 |
-
|
| 937 |
-
.magic-8-ball-title {
|
| 938 |
-
font-size: 2.5em !important;
|
| 939 |
-
font-weight: bold !important;
|
| 940 |
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 941 |
-
margin-bottom: 10px !important;
|
| 942 |
-
}
|
| 943 |
-
|
| 944 |
-
.magic-8-ball-subtitle {
|
| 945 |
-
font-size: 1.2em !important;
|
| 946 |
-
opacity: 0.9;
|
| 947 |
-
margin-bottom: 20px !important;
|
| 948 |
-
}
|
| 949 |
-
|
| 950 |
-
.tts-container {
|
| 951 |
-
background: linear-gradient(135deg, #00b09b 0%, #96c93d 100%);
|
| 952 |
-
padding: 20px;
|
| 953 |
-
border-radius: 10px;
|
| 954 |
-
margin: 10px 0;
|
| 955 |
-
}
|
| 956 |
-
"""
|
| 957 |
-
|
| 958 |
-
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="orange", neutral_hue="slate")) as demo:
|
| 959 |
-
gr.Markdown("# 🎙️ Groq x Gradio Multi-Modal với RAG Wikipedia & TTS Tiếng Việt")
|
| 960 |
-
gr.Markdown("**Ứng dụng đa chức năng với Llama 3.3, Whisper, RAG embedding tiếng Việt**")
|
| 961 |
-
|
| 962 |
-
with gr.Tab("🎙️ Audio"):
|
| 963 |
-
gr.Markdown("## Nói chuyện với AI (có TTS)")
|
| 964 |
-
with gr.Row():
|
| 965 |
-
audio_input = gr.Audio(type="numpy", label="Nói hoặc tải lên file âm thanh")
|
| 966 |
-
with gr.Row():
|
| 967 |
-
transcription_output = gr.Textbox(
|
| 968 |
-
label="Bản ghi âm",
|
| 969 |
-
lines=5,
|
| 970 |
-
interactive=True,
|
| 971 |
-
placeholder="Bản ghi âm sẽ hiển thị ở đây..."
|
| 972 |
-
)
|
| 973 |
-
response_output = gr.Textbox(
|
| 974 |
-
label="Phản hồi AI",
|
| 975 |
-
lines=5,
|
| 976 |
-
interactive=True,
|
| 977 |
-
placeholder="Phản hồi của AI sẽ hiển thị ở đây..."
|
| 978 |
-
)
|
| 979 |
-
with gr.Row():
|
| 980 |
-
tts_audio_output = gr.Audio(
|
| 981 |
-
label="Phản hồi bằng giọng nói",
|
| 982 |
-
interactive=False
|
| 983 |
-
)
|
| 984 |
-
process_button = gr.Button("Xử lý", variant="primary")
|
| 985 |
-
process_button.click(
|
| 986 |
-
transcribe_audio,
|
| 987 |
-
inputs=audio_input,
|
| 988 |
-
outputs=[transcription_output, response_output, tts_audio_output]
|
| 989 |
-
)
|
| 990 |
-
|
| 991 |
-
with gr.Tab("🔊 Streaming Voice"):
|
| 992 |
-
gr.Markdown("## 🎤 Trò chuyện giọng nói thời gian thực với TTS")
|
| 993 |
-
gr.Markdown("Nói chuyện tự nhiên với AI - Câu hỏi của bạn sẽ được chuyển thành văn bản và AI sẽ trả lời bằng giọng nói tiếng Việt")
|
| 994 |
-
|
| 995 |
-
with gr.Group():
|
| 996 |
-
with gr.Row():
|
| 997 |
-
audio_out = gr.Audio(
|
| 998 |
-
label="Câu trả lời bằng giọng nói",
|
| 999 |
-
autoplay=True,
|
| 1000 |
-
format="mp3"
|
| 1001 |
-
)
|
| 1002 |
-
answer_text = gr.Textbox(
|
| 1003 |
-
label="Câu trả lời văn bản",
|
| 1004 |
-
lines=5,
|
| 1005 |
-
placeholder="Câu trả lời văn bản sẽ hiển thị ở đây..."
|
| 1006 |
-
)
|
| 1007 |
-
streaming_state = gr.State()
|
| 1008 |
-
|
| 1009 |
-
with gr.Row():
|
| 1010 |
-
audio_in = gr.Audio(
|
| 1011 |
-
label="Nói câu hỏi của bạn",
|
| 1012 |
-
sources="microphone",
|
| 1013 |
-
type="filepath",
|
| 1014 |
-
format="wav"
|
| 1015 |
-
)
|
| 1016 |
-
|
| 1017 |
-
audio_in.stop_recording(
|
| 1018 |
-
generate_streaming_response,
|
| 1019 |
-
inputs=[audio_in],
|
| 1020 |
-
outputs=[streaming_state, answer_text, audio_out]
|
| 1021 |
-
).then(
|
| 1022 |
-
fn=read_streaming_response,
|
| 1023 |
-
inputs=[streaming_state],
|
| 1024 |
-
outputs=[answer_text, audio_out]
|
| 1025 |
-
)
|
| 1026 |
-
|
| 1027 |
-
with gr.Tab("🎱 Magic 8 Ball"):
|
| 1028 |
-
gr.HTML(
|
| 1029 |
-
"""
|
| 1030 |
-
<div class="magic-8-ball-container">
|
| 1031 |
-
<h1 class="magic-8-ball-title">Magic 8 Ball 🎱</h1>
|
| 1032 |
-
<h3 class="magic-8-ball-subtitle">Hỏi một câu hỏi và nhận trí tuệ từ quả cầu thần kỳ bằng tiếng Việt</h3>
|
| 1033 |
-
<p class="magic-8-ball-subtitle">Powered by Groq & Whisper & TTS</p>
|
| 1034 |
-
</div>
|
| 1035 |
-
"""
|
| 1036 |
-
)
|
| 1037 |
-
|
| 1038 |
-
with gr.Group():
|
| 1039 |
-
with gr.Row():
|
| 1040 |
-
magic_audio_out = gr.Audio(
|
| 1041 |
-
label="Câu trả lời bằng giọng nói",
|
| 1042 |
-
autoplay=True,
|
| 1043 |
-
format="mp3"
|
| 1044 |
-
)
|
| 1045 |
-
magic_answer = gr.Textbox(
|
| 1046 |
-
label="Câu trả lời",
|
| 1047 |
-
lines=3,
|
| 1048 |
-
placeholder="Câu trả lời thần kỳ sẽ hiển thị ở đây..."
|
| 1049 |
-
)
|
| 1050 |
-
magic_state = gr.State()
|
| 1051 |
-
|
| 1052 |
-
with gr.Row():
|
| 1053 |
-
magic_audio_in = gr.Audio(
|
| 1054 |
-
label="Nói câu hỏi của bạn",
|
| 1055 |
-
sources="microphone",
|
| 1056 |
-
type="filepath",
|
| 1057 |
-
format="wav"
|
| 1058 |
-
)
|
| 1059 |
-
|
| 1060 |
-
magic_audio_in.stop_recording(
|
| 1061 |
-
generate_magic_8_ball_response,
|
| 1062 |
-
inputs=[magic_audio_in],
|
| 1063 |
-
outputs=[magic_state, magic_answer, magic_audio_out]
|
| 1064 |
-
).then(
|
| 1065 |
-
fn=read_magic_8_ball_response,
|
| 1066 |
-
inputs=[magic_state],
|
| 1067 |
-
outputs=[magic_answer, magic_audio_out]
|
| 1068 |
-
)
|
| 1069 |
-
|
| 1070 |
-
with gr.Tab("🔊 Text-to-Speech"):
|
| 1071 |
-
gr.Markdown("## 🎵 Chuyển văn bản thành giọng nói nâng cao")
|
| 1072 |
-
gr.Markdown("Nhập văn bản và chọn ngôn ngữ để chuyển thành giọng nói")
|
| 1073 |
-
|
| 1074 |
-
with gr.Group():
|
| 1075 |
-
with gr.Row():
|
| 1076 |
-
tts_text_input = gr.Textbox(
|
| 1077 |
-
label="Văn bản cần chuyển thành giọng nói",
|
| 1078 |
-
lines=4,
|
| 1079 |
-
placeholder="Nhập văn bản tại đây..."
|
| 1080 |
-
)
|
| 1081 |
-
with gr.Row():
|
| 1082 |
-
tts_language = gr.Dropdown(
|
| 1083 |
-
choices=["vi", "en", "fr", "es", "de", "ja", "ko", "zh"],
|
| 1084 |
-
value="vi",
|
| 1085 |
-
label="Ngôn ngữ"
|
| 1086 |
-
)
|
| 1087 |
-
tts_provider = gr.Dropdown(
|
| 1088 |
-
choices=["auto", "gtts", "edgetts"],
|
| 1089 |
-
value="auto",
|
| 1090 |
-
label="Nhà cung cấp TTS"
|
| 1091 |
-
)
|
| 1092 |
-
with gr.Row():
|
| 1093 |
-
tts_output_audio = gr.Audio(
|
| 1094 |
-
label="Kết quả giọng nói",
|
| 1095 |
-
interactive=False
|
| 1096 |
-
)
|
| 1097 |
-
tts_button = gr.Button("🔊 Chuyển thành giọng nói", variant="primary")
|
| 1098 |
-
|
| 1099 |
-
tts_button.click(
|
| 1100 |
-
text_to_speech_standalone,
|
| 1101 |
-
inputs=[tts_text_input, tts_language, tts_provider],
|
| 1102 |
-
outputs=[tts_output_audio]
|
| 1103 |
-
)
|
| 1104 |
-
|
| 1105 |
-
with gr.Tab("🖼️ Image"):
|
| 1106 |
-
gr.Markdown("## Phân tích hình ảnh")
|
| 1107 |
-
with gr.Row():
|
| 1108 |
-
image_input = gr.Image(type="numpy", label="Tải lên hình ảnh")
|
| 1109 |
-
with gr.Row():
|
| 1110 |
-
image_description = gr.Textbox(
|
| 1111 |
-
label="Mô tả hình ảnh của bạn (tùy chọn)",
|
| 1112 |
-
placeholder="Mô tả ngắn về hình ảnh để AI phân tích chính xác hơn..."
|
| 1113 |
-
)
|
| 1114 |
-
with gr.Row():
|
| 1115 |
-
image_output = gr.Textbox(label="Kết quả phân tích")
|
| 1116 |
-
analyze_button = gr.Button("Phân tích hình ảnh", variant="primary")
|
| 1117 |
-
analyze_button.click(
|
| 1118 |
-
analyze_image_with_description,
|
| 1119 |
-
inputs=[image_input, image_description],
|
| 1120 |
-
outputs=[image_output]
|
| 1121 |
-
)
|
| 1122 |
-
|
| 1123 |
-
with gr.Tab("💬 Chat"):
|
| 1124 |
-
gr.Markdown("## Trò chuyện với AI Assistant (có TTS)")
|
| 1125 |
-
chatbot = gr.Chatbot()
|
| 1126 |
-
state = gr.State([])
|
| 1127 |
-
with gr.Row():
|
| 1128 |
-
user_input = gr.Textbox(
|
| 1129 |
-
show_label=False,
|
| 1130 |
-
placeholder="Nhập tin nhắn của bạn ở đây...",
|
| 1131 |
-
container=False,
|
| 1132 |
-
scale=4
|
| 1133 |
-
)
|
| 1134 |
-
send_button = gr.Button("Gửi", variant="primary", scale=1)
|
| 1135 |
-
clear_button = gr.Button("Xóa Chat", variant="secondary", scale=1)
|
| 1136 |
-
with gr.Row():
|
| 1137 |
-
chat_tts_output = gr.Audio(
|
| 1138 |
-
label="Phản hồi bằng giọng nói",
|
| 1139 |
-
interactive=False
|
| 1140 |
-
)
|
| 1141 |
-
|
| 1142 |
-
send_button.click(
|
| 1143 |
-
respond,
|
| 1144 |
-
inputs=[user_input, state],
|
| 1145 |
-
outputs=[user_input, chatbot, state, chat_tts_output],
|
| 1146 |
-
)
|
| 1147 |
-
clear_button.click(
|
| 1148 |
-
clear_chat_history,
|
| 1149 |
-
inputs=[state],
|
| 1150 |
-
outputs=[chatbot, state]
|
| 1151 |
-
)
|
| 1152 |
-
|
| 1153 |
-
with gr.Tab("📚 RAG Wikipedia"):
|
| 1154 |
-
gr.Markdown("## Quản lý kiến thức với Wikipedia và Embedding Tiếng Việt")
|
| 1155 |
-
|
| 1156 |
-
with gr.Row():
|
| 1157 |
-
with gr.Column(scale=1):
|
| 1158 |
-
gr.Markdown("### 📤 Upload dữ liệu Wikipedia")
|
| 1159 |
-
file_upload = gr.File(
|
| 1160 |
-
label="Tải lên file Wikipedia",
|
| 1161 |
-
file_types=['.txt', '.csv', '.json'],
|
| 1162 |
-
file_count="single"
|
| 1163 |
-
)
|
| 1164 |
-
upload_btn = gr.Button("📤 Upload Data", variant="primary")
|
| 1165 |
-
upload_status = gr.Textbox(label="Trạng thái Upload", interactive=False)
|
| 1166 |
-
|
| 1167 |
-
gr.Markdown("### 📊 Thống kê Database")
|
| 1168 |
-
stats_btn = gr.Button("📊 Database Stats", variant="secondary")
|
| 1169 |
-
stats_display = gr.Textbox(label="Thống kê", interactive=False)
|
| 1170 |
-
|
| 1171 |
-
gr.Markdown("### 🔍 Tìm kiếm Database")
|
| 1172 |
-
search_query = gr.Textbox(
|
| 1173 |
-
label="Tìm kiếm trong database",
|
| 1174 |
-
placeholder="Nhập từ khóa để tìm kiếm..."
|
| 1175 |
-
)
|
| 1176 |
-
search_btn = gr.Button("🔍 Tìm kiếm", variant="secondary")
|
| 1177 |
-
|
| 1178 |
-
with gr.Column(scale=2):
|
| 1179 |
-
gr.Markdown("### 📋 Kết quả tìm kiếm RAG")
|
| 1180 |
-
rag_results = gr.JSON(
|
| 1181 |
-
label="Tài liệu tham khảo tìm được"
|
| 1182 |
-
)
|
| 1183 |
-
|
| 1184 |
-
upload_btn.click(
|
| 1185 |
-
upload_wikipedia_file,
|
| 1186 |
-
inputs=[file_upload],
|
| 1187 |
-
outputs=[upload_status]
|
| 1188 |
-
)
|
| 1189 |
-
|
| 1190 |
-
stats_btn.click(
|
| 1191 |
-
get_rag_stats,
|
| 1192 |
-
inputs=[],
|
| 1193 |
-
outputs=[stats_display]
|
| 1194 |
-
)
|
| 1195 |
-
|
| 1196 |
-
search_btn.click(
|
| 1197 |
-
search_rag_database,
|
| 1198 |
-
inputs=[search_query],
|
| 1199 |
-
outputs=[rag_results]
|
| 1200 |
-
)
|
| 1201 |
-
|
| 1202 |
-
gr.HTML("""
|
| 1203 |
-
<div id="groq-badge">
|
| 1204 |
-
<div style="color: #ffffff !important; font-weight: bold; background-color: #f55036; padding: 8px 12px; border-radius: 5px;">
|
| 1205 |
-
POWERED BY DAT | VIETNAMESE EMBEDDING & ENHANCED TTS
|
| 1206 |
-
</div>
|
| 1207 |
-
</div>
|
| 1208 |
-
""")
|
| 1209 |
-
|
| 1210 |
-
if __name__ == "__main__":
|
| 1211 |
-
demo.launch(share=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config/__pycache__/settings.cpython-310.pyc
ADDED
|
Binary file (1.33 kB). View file
|
|
|
config/settings.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
class Settings:
|
| 7 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 8 |
+
|
| 9 |
+
# Multilingual Model Settings
|
| 10 |
+
VIETNAMESE_EMBEDDING_MODEL = 'dangvantuan/vietnamese-embedding'
|
| 11 |
+
VIETNAMESE_LLM_MODEL = "Vietnamese_LLaMA2_13B_8K_SFT_General_Domain_Knowledge"
|
| 12 |
+
|
| 13 |
+
MULTILINGUAL_EMBEDDING_MODEL = 'Qwen/Qwen3-Embedding-4B'
|
| 14 |
+
MULTILINGUAL_LLM_MODEL = "meta-llama/Llama-3.1-8B-Instruct"
|
| 15 |
+
|
| 16 |
+
# Fallback models in case primary models fail
|
| 17 |
+
FALLBACK_MULTILINGUAL_EMBEDDING_MODEL = 'sentence-transformers/all-MiniLM-L6-v2'
|
| 18 |
+
|
| 19 |
+
# Default models (fallback)
|
| 20 |
+
DEFAULT_EMBEDDING_MODEL = 'dangvantuan/vietnamese-embedding'
|
| 21 |
+
DEFAULT_LLM_MODEL = "Vietnamese_LLaMA2_13B_8K_SFT_General_Domain_Knowledge"
|
| 22 |
+
|
| 23 |
+
WHISPER_MODEL = "whisper-large-v3-turbo"
|
| 24 |
+
|
| 25 |
+
# TTS Settings
|
| 26 |
+
MAX_CHUNK_LENGTH = 200
|
| 27 |
+
SUPPORTED_LANGUAGES = {
|
| 28 |
+
'vi': 'vi', 'en': 'en', 'fr': 'fr', 'es': 'es',
|
| 29 |
+
'de': 'de', 'ja': 'ja', 'ko': 'ko', 'zh': 'zh'
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# RAG Settings
|
| 33 |
+
EMBEDDING_DIMENSION = 768 # For Vietnamese model
|
| 34 |
+
MULTILINGUAL_EMBEDDING_DIMENSION = 4096 # For Nemotron model
|
| 35 |
+
|
| 36 |
+
TOP_K_RESULTS = 3
|
| 37 |
+
|
| 38 |
+
# SpeechBrain VAD Settings
|
| 39 |
+
VAD_MODEL = "speechbrain/vad-crdnn-libriparty"
|
| 40 |
+
VAD_THRESHOLD = 0.5
|
| 41 |
+
VAD_MIN_SILENCE_DURATION = 0.5
|
| 42 |
+
VAD_SPEECH_PAD_DURATION = 0.1
|
| 43 |
+
SAMPLE_RATE = 16000
|
| 44 |
+
|
| 45 |
+
settings = Settings()
|
core/__pycache__/multilingual_manager.cpython-310.pyc
ADDED
|
Binary file (5.64 kB). View file
|
|
|
core/__pycache__/rag_system.cpython-310.pyc
ADDED
|
Binary file (7.83 kB). View file
|
|
|
core/__pycache__/speechbrain_vad.cpython-310.pyc
ADDED
|
Binary file (4.99 kB). View file
|
|
|
core/__pycache__/tts_service.cpython-310.pyc
ADDED
|
Binary file (6.24 kB). View file
|
|
|
core/__pycache__/wikipedia_processor.cpython-310.pyc
ADDED
|
Binary file (3.14 kB). View file
|
|
|
core/multilingual_manager.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from typing import Dict, Tuple, Optional
|
| 3 |
+
from sentence_transformers import SentenceTransformer
|
| 4 |
+
from config.settings import settings
|
| 5 |
+
|
| 6 |
+
class MultilingualManager:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
self.vietnamese_model = None
|
| 9 |
+
self.multilingual_model = None
|
| 10 |
+
self.current_language = 'vi'
|
| 11 |
+
|
| 12 |
+
# Phát hiện thuộc ngôn ngữ dựa trên các mẫu ký tự và từ phổ biến
|
| 13 |
+
self.language_patterns = {
|
| 14 |
+
'vi': {
|
| 15 |
+
'chars': set('àáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ'),
|
| 16 |
+
'common_words': ['của', 'và', 'là', 'có', 'được', 'trong', 'cho', 'với', 'như', 'tôi']
|
| 17 |
+
},
|
| 18 |
+
'en': {
|
| 19 |
+
'chars': set('abcdefghijklmnopqrstuvwxyz'),
|
| 20 |
+
'common_words': ['the', 'and', 'is', 'are', 'for', 'with', 'this', 'that', 'you', 'they']
|
| 21 |
+
},
|
| 22 |
+
'fr': {
|
| 23 |
+
'chars': set('àâæçèéêëîïôœùûüÿ'),
|
| 24 |
+
'common_words': ['le', 'la', 'et', 'est', 'dans', 'pour', 'avec', 'vous', 'nous', 'ils']
|
| 25 |
+
},
|
| 26 |
+
'es': {
|
| 27 |
+
'chars': set('áéíóúñü'),
|
| 28 |
+
'common_words': ['el', 'la', 'y', 'es', 'en', 'por', 'con', 'los', 'las', 'del']
|
| 29 |
+
},
|
| 30 |
+
'de': {
|
| 31 |
+
'chars': set('äöüß'),
|
| 32 |
+
'common_words': ['der', 'die', 'das', 'und', 'ist', 'in', 'für', 'mit', 'sich', 'nicht']
|
| 33 |
+
},
|
| 34 |
+
'ja': {
|
| 35 |
+
'chars': set('ぁ-んァ-ン一-龯'),
|
| 36 |
+
'common_words': ['の', 'に', 'は', 'を', 'た', 'で', 'し', 'が', 'ます', 'です']
|
| 37 |
+
},
|
| 38 |
+
'ko': {
|
| 39 |
+
'chars': set('가-힣'),
|
| 40 |
+
'common_words': ['이', '그', '에', '를', '의', '에', '에서', '으로', '하다', '이다']
|
| 41 |
+
},
|
| 42 |
+
'zh': {
|
| 43 |
+
'chars': set('一-鿌'),
|
| 44 |
+
'common_words': ['的', '是', '在', '有', '和', '了', '人', '我', '他', '这']
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
self._initialize_models()
|
| 48 |
+
def _initialize_models(self):
|
| 49 |
+
"""Khởi tạo các mô hình đa ngôn ngữ"""
|
| 50 |
+
try:
|
| 51 |
+
print("🔄 Đang tải mô hình embedding tiếng Việt...")
|
| 52 |
+
self.vietnamese_model = SentenceTransformer(settings.VIETNAMESE_EMBEDDING_MODEL)
|
| 53 |
+
print("✅ Đã tải mô hình embedding tiếng Việt")
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"❌ Lỗi tải mô hình embedding tiếng Việt: {e}")
|
| 56 |
+
self.vietnamese_model = None
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
print("🔄 Đang tải mô hình embedding đa ngôn ngữ...")
|
| 60 |
+
self.multilingual_model = SentenceTransformer(settings.MULTILINGUAL_EMBEDDING_MODEL,trust_remote_code=True )
|
| 61 |
+
print("✅ Đã tải mô hình embedding đa ngôn ngữ")
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print(f"❌ Lỗi tải mô hình embedding đa ngôn ngữ: {e}")
|
| 64 |
+
self.multilingual_model = None
|
| 65 |
+
|
| 66 |
+
def detect_language(self, text: str) -> str:
|
| 67 |
+
"""Phát hiện ngôn ngữ với độ chính xác cao"""
|
| 68 |
+
if not text or len(text.strip()) == 0:
|
| 69 |
+
return 'vi' # Default to Vietnamese
|
| 70 |
+
|
| 71 |
+
text_lower = text.lower()
|
| 72 |
+
scores = {}
|
| 73 |
+
|
| 74 |
+
for lang, patterns in self.language_patterns.items():
|
| 75 |
+
score = 0
|
| 76 |
+
|
| 77 |
+
# Score based on special characters
|
| 78 |
+
char_score = sum(1 for char in text if char in patterns['chars'])
|
| 79 |
+
score += char_score * 2
|
| 80 |
+
|
| 81 |
+
# Score based on common words
|
| 82 |
+
word_score = sum(1 for word in patterns['common_words'] if word in text_lower)
|
| 83 |
+
score += word_score
|
| 84 |
+
|
| 85 |
+
scores[lang] = score
|
| 86 |
+
|
| 87 |
+
# Return language with highest score
|
| 88 |
+
detected_lang = max(scores.items(), key=lambda x: x[1])[0]
|
| 89 |
+
|
| 90 |
+
# If no strong detection, use character-based fallback
|
| 91 |
+
if max(scores.values()) < 3:
|
| 92 |
+
vietnamese_chars = set('àáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ')
|
| 93 |
+
if any(char in vietnamese_chars for char in text):
|
| 94 |
+
return 'vi'
|
| 95 |
+
elif any(char in text for char in 'あいうえおぁ-んァ-ン'):
|
| 96 |
+
return 'ja'
|
| 97 |
+
elif any(char in text for char in '你好'):
|
| 98 |
+
return 'zh'
|
| 99 |
+
elif any(char in text for char in '안녕'):
|
| 100 |
+
return 'ko'
|
| 101 |
+
else:
|
| 102 |
+
return 'en' # Default to English for other cases
|
| 103 |
+
|
| 104 |
+
return detected_lang
|
| 105 |
+
def get_embedding_model(self, language: str = None) -> Optional[SentenceTransformer]:
|
| 106 |
+
"""Lấy mô hình embedding dựa trên ngôn ngữ đã phát hiện"""
|
| 107 |
+
lang = language if language in settings.SUPPORTED_LANGUAGES else self.current_language
|
| 108 |
+
|
| 109 |
+
if lang == 'vi':
|
| 110 |
+
return self.vietnamese_model
|
| 111 |
+
else:
|
| 112 |
+
return self.multilingual_model
|
| 113 |
+
|
| 114 |
+
def get_llm_model_name(self, language: str = None) -> str:
|
| 115 |
+
"""Lấy tên mô hình LLM dựa trên ngôn ngữ đã phát hiện"""
|
| 116 |
+
lang = language if language in settings.SUPPORTED_LANGUAGES else self.current_language
|
| 117 |
+
|
| 118 |
+
if lang == 'vi':
|
| 119 |
+
return settings.VIETNAMESE_LLM_MODEL
|
| 120 |
+
else:
|
| 121 |
+
return settings.MULTILINGUAL_LLM_MODEL
|
| 122 |
+
|
| 123 |
+
def get_language_info(self, language: str = None) -> Dict:
|
| 124 |
+
"""Lấy thông tin ngôn ngữ bao gồm mã và tên đầy đủ"""
|
| 125 |
+
lang = language if language in settings.SUPPORTED_LANGUAGES else self.current_language
|
| 126 |
+
|
| 127 |
+
model_info = {
|
| 128 |
+
'vi': {
|
| 129 |
+
'name': 'Tiếng Việt',
|
| 130 |
+
'embedding_model': settings.VIETNAMESE_EMBEDDING_MODEL,
|
| 131 |
+
'llm_model': settings.VIETNAMESE_LLM_MODEL,
|
| 132 |
+
'status': 'active' if self.vietnamese_model else 'inactive'
|
| 133 |
+
},
|
| 134 |
+
'other': {
|
| 135 |
+
'name': 'Multilingual',
|
| 136 |
+
'embedding_model': settings.MULTILINGUAL_EMBEDDING_MODEL,
|
| 137 |
+
'llm_model': settings.MULTILINGUAL_LLM_MODEL,
|
| 138 |
+
'status': 'active' if self.multilingual_model else 'inactive'
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
if lang == 'vi':
|
| 143 |
+
return model_info['vi']
|
| 144 |
+
else:
|
| 145 |
+
return model_info['other']
|
| 146 |
+
|
core/rag_system.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import faiss
|
| 3 |
+
from typing import List, Dict, Optional
|
| 4 |
+
from sentence_transformers import SentenceTransformer
|
| 5 |
+
from models.schemas import RAGSearchResult
|
| 6 |
+
from config.settings import settings
|
| 7 |
+
from core.multilingual_manager import MultilingualManager
|
| 8 |
+
|
| 9 |
+
class EnhancedRAGSystem:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.documents: List[str] = []
|
| 12 |
+
self.metadatas: List[Dict] = []
|
| 13 |
+
self.embeddings: Optional[np.ndarray] = None
|
| 14 |
+
self.index: Optional[faiss.Index] = None
|
| 15 |
+
|
| 16 |
+
# Multilingual support
|
| 17 |
+
self.multilingual_manager = MultilingualManager()
|
| 18 |
+
self.current_dimension = settings.EMBEDDING_DIMENSION
|
| 19 |
+
|
| 20 |
+
self._initialize_sample_data() # SỬA TÊN HÀM
|
| 21 |
+
|
| 22 |
+
def _initialize_sample_data(self): # SỬA TÊN HÀM
|
| 23 |
+
"""Khởi tạo dữ liệu mẫu"""
|
| 24 |
+
# Vietnamese sample data
|
| 25 |
+
vietnamese_data = [
|
| 26 |
+
"Rau xanh cung cấp nhiều vitamin và chất xơ tốt cho sức khỏe",
|
| 27 |
+
"Trái cây tươi chứa nhiều vitamin C và chất chống oxy hóa",
|
| 28 |
+
"Cá hồi giàu omega-3 tốt cho tim mạch và trí não",
|
| 29 |
+
"Nước rất quan trọng cho cơ thể, nên uống ít nhất 2 lít mỗi ngày",
|
| 30 |
+
"Hà Nội là thủ đô của Việt Nam, nằm ở miền Bắc",
|
| 31 |
+
"Thành phố Hồ Chí Minh là thành phố lớn nhất Việt Nam",
|
| 32 |
+
"Việt Nam có khí hậu nhiệt đới gió mùa với 4 mùa rõ rệt"
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
# English sample data
|
| 36 |
+
english_data = [
|
| 37 |
+
"Green vegetables provide many vitamins and fiber that are good for health",
|
| 38 |
+
"Fresh fruits contain lots of vitamin C and antioxidants",
|
| 39 |
+
"Salmon is rich in omega-3 which is good for heart and brain",
|
| 40 |
+
"Water is very important for the body, should drink at least 2 liters per day",
|
| 41 |
+
"London is the capital of England and the United Kingdom",
|
| 42 |
+
"New York City is the most populous city in the United States",
|
| 43 |
+
"The United States has diverse climate zones from tropical to arctic"
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# Vietnamese metadata - SỬA LỖI SYNTAX
|
| 47 |
+
vietnamese_metadatas = [
|
| 48 |
+
{"type": "nutrition", "source": "sample", "language": "vi"},
|
| 49 |
+
{"type": "nutrition", "source": "sample", "language": "vi"},
|
| 50 |
+
{"type": "nutrition", "source": "sample", "language": "vi"},
|
| 51 |
+
{"type": "health", "source": "sample", "language": "vi"},
|
| 52 |
+
{"type": "geography", "source": "sample", "language": "vi"},
|
| 53 |
+
{"type": "geography", "source": "sample", "language": "vi"},
|
| 54 |
+
{"type": "geography", "source": "sample", "language": "vi"}
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
# English metadata - SỬA LỖI SYNTAX
|
| 58 |
+
english_metadatas = [
|
| 59 |
+
{"type": "nutrition", "source": "sample", "language": "en"},
|
| 60 |
+
{"type": "nutrition", "source": "sample", "language": "en"},
|
| 61 |
+
{"type": "nutrition", "source": "sample", "language": "en"},
|
| 62 |
+
{"type": "health", "source": "sample", "language": "en"},
|
| 63 |
+
{"type": "geography", "source": "sample", "language": "en"},
|
| 64 |
+
{"type": "geography", "source": "sample", "language": "en"},
|
| 65 |
+
{"type": "geography", "source": "sample", "language": "en"}
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
# Add documents with language metadata
|
| 69 |
+
self.add_documents(vietnamese_data, vietnamese_metadatas)
|
| 70 |
+
self.add_documents(english_data, english_metadatas)
|
| 71 |
+
|
| 72 |
+
def add_documents(self, documents: List[str], metadatas: List[Dict] = None):
|
| 73 |
+
"""Thêm documents vào database với embedding phù hợp"""
|
| 74 |
+
if not documents:
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
# Ensure metadatas has the same length as documents
|
| 78 |
+
if metadatas is None:
|
| 79 |
+
metadatas = [{} for _ in documents]
|
| 80 |
+
elif len(metadatas) != len(documents):
|
| 81 |
+
# Extend or truncate metadatas to match documents length
|
| 82 |
+
if len(metadatas) < len(documents):
|
| 83 |
+
metadatas = metadatas + [{} for _ in range(len(documents) - len(metadatas))]
|
| 84 |
+
else:
|
| 85 |
+
metadatas = metadatas[:len(documents)]
|
| 86 |
+
|
| 87 |
+
# Detect language for each document and create embeddings accordingly
|
| 88 |
+
new_embeddings_list = []
|
| 89 |
+
valid_documents = []
|
| 90 |
+
valid_metadatas = []
|
| 91 |
+
|
| 92 |
+
for i, doc in enumerate(documents):
|
| 93 |
+
if not doc or len(doc.strip()) == 0:
|
| 94 |
+
continue
|
| 95 |
+
|
| 96 |
+
language = metadatas[i].get('language', 'vi')
|
| 97 |
+
embedding_model = self.multilingual_manager.get_embedding_model(language)
|
| 98 |
+
|
| 99 |
+
if embedding_model is not None:
|
| 100 |
+
try:
|
| 101 |
+
# Create embedding for this document
|
| 102 |
+
doc_embedding = embedding_model.encode([doc])
|
| 103 |
+
new_embeddings_list.append(doc_embedding[0])
|
| 104 |
+
valid_documents.append(doc)
|
| 105 |
+
valid_metadatas.append(metadatas[i])
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"❌ Lỗi tạo embedding cho document {i}: {e}")
|
| 109 |
+
|
| 110 |
+
if not valid_documents:
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
# Convert list of embeddings to numpy array
|
| 114 |
+
new_embeddings = np.array(new_embeddings_list)
|
| 115 |
+
|
| 116 |
+
# Handle dimension mismatch
|
| 117 |
+
if self.embeddings is not None and self.embeddings.shape[1] != new_embeddings.shape[1]:
|
| 118 |
+
print(f"⚠️ Phát hiện dimension mismatch ({self.embeddings.shape[1]} vs {new_embeddings.shape[1]}), tạo index mới...")
|
| 119 |
+
self.embeddings = None
|
| 120 |
+
self.index = None
|
| 121 |
+
|
| 122 |
+
# Update embeddings
|
| 123 |
+
if self.embeddings is None:
|
| 124 |
+
self.embeddings = new_embeddings
|
| 125 |
+
self.current_dimension = new_embeddings.shape[1]
|
| 126 |
+
else:
|
| 127 |
+
self.embeddings = np.vstack([self.embeddings, new_embeddings])
|
| 128 |
+
|
| 129 |
+
# Update FAISS index
|
| 130 |
+
self._update_faiss_index()
|
| 131 |
+
|
| 132 |
+
self.documents.extend(valid_documents)
|
| 133 |
+
self.metadatas.extend(valid_metadatas)
|
| 134 |
+
print(f"✅ Đã thêm {len(valid_documents)} documents vào RAG database")
|
| 135 |
+
|
| 136 |
+
def _update_faiss_index(self):
|
| 137 |
+
"""Cập nhật FAISS index với embeddings hiện tại"""
|
| 138 |
+
if self.embeddings is None or len(self.embeddings) == 0:
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
dimension = self.embeddings.shape[1]
|
| 143 |
+
self.index = faiss.IndexFlatIP(dimension)
|
| 144 |
+
|
| 145 |
+
# Normalize embeddings for cosine similarity
|
| 146 |
+
faiss.normalize_L2(self.embeddings)
|
| 147 |
+
self.index.add(self.embeddings.astype(np.float32))
|
| 148 |
+
|
| 149 |
+
print(f"✅ Đã cập nhật FAISS index với dimension {dimension}")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"❌ Lỗi cập nhật FAISS index: {e}")
|
| 152 |
+
|
| 153 |
+
def semantic_search(self, query: str, top_k: int = None) -> List[RAGSearchResult]:
|
| 154 |
+
"""Tìm kiếm ngữ nghĩa với model phù hợp theo ngôn ngữ"""
|
| 155 |
+
if top_k is None:
|
| 156 |
+
top_k = settings.TOP_K_RESULTS
|
| 157 |
+
|
| 158 |
+
if not self.documents or self.index is None:
|
| 159 |
+
return self._fallback_keyword_search(query, top_k)
|
| 160 |
+
|
| 161 |
+
# Detect query language and get appropriate model
|
| 162 |
+
query_language = self.multilingual_manager.detect_language(query)
|
| 163 |
+
embedding_model = self.multilingual_manager.get_embedding_model(query_language)
|
| 164 |
+
|
| 165 |
+
if embedding_model is None:
|
| 166 |
+
return self._fallback_keyword_search(query, top_k)
|
| 167 |
+
|
| 168 |
+
try:
|
| 169 |
+
# Encode query with appropriate model
|
| 170 |
+
query_embedding = embedding_model.encode([query])
|
| 171 |
+
|
| 172 |
+
# Normalize query embedding for cosine similarity
|
| 173 |
+
faiss.normalize_L2(query_embedding)
|
| 174 |
+
|
| 175 |
+
# Search in FAISS index
|
| 176 |
+
similarities, indices = self.index.search(
|
| 177 |
+
query_embedding.astype(np.float32),
|
| 178 |
+
min(top_k, len(self.documents))
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
results = []
|
| 182 |
+
for i, (similarity, idx) in enumerate(zip(similarities[0], indices[0])):
|
| 183 |
+
if idx < len(self.documents):
|
| 184 |
+
results.append(RAGSearchResult(
|
| 185 |
+
id=str(idx),
|
| 186 |
+
text=self.documents[idx],
|
| 187 |
+
similarity=float(similarity),
|
| 188 |
+
metadata=self.metadatas[idx] if idx < len(self.metadatas) else {}
|
| 189 |
+
))
|
| 190 |
+
|
| 191 |
+
# Filter results by language relevance
|
| 192 |
+
filtered_results = self._filter_by_language_relevance(results, query_language)
|
| 193 |
+
|
| 194 |
+
print(f"🔍 Tìm kiếm '{query[:50]}...' (ngôn ngữ: {query_language}) - Tìm thấy {len(filtered_results)} kết quả")
|
| 195 |
+
return filtered_results
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
print(f"❌ Lỗi tìm kiếm ngữ nghĩa: {e}")
|
| 199 |
+
return self._fallback_keyword_search(query, top_k)
|
| 200 |
+
|
| 201 |
+
def _filter_by_language_relevance(self, results: List[RAGSearchResult], query_language: str) -> List[RAGSearchResult]:
|
| 202 |
+
"""Lọc kết quả theo độ liên quan ngôn ngữ"""
|
| 203 |
+
if not results:
|
| 204 |
+
return results
|
| 205 |
+
|
| 206 |
+
# Boost scores for documents in the same language
|
| 207 |
+
for result in results:
|
| 208 |
+
doc_language = result.metadata.get('language', 'vi')
|
| 209 |
+
if doc_language == query_language:
|
| 210 |
+
# Boost similarity score for same language documents
|
| 211 |
+
result.similarity = min(result.similarity * 1.2, 1.0)
|
| 212 |
+
|
| 213 |
+
# Re-sort by updated similarity scores
|
| 214 |
+
results.sort(key=lambda x: x.similarity, reverse=True)
|
| 215 |
+
return results
|
| 216 |
+
|
| 217 |
+
def _fallback_keyword_search(self, query: str, top_k: int) -> List[RAGSearchResult]:
|
| 218 |
+
"""Tìm kiếm dự phòng dựa trên từ khóa"""
|
| 219 |
+
query_lower = query.lower()
|
| 220 |
+
results = []
|
| 221 |
+
|
| 222 |
+
for i, doc in enumerate(self.documents):
|
| 223 |
+
score = 0
|
| 224 |
+
doc_language = self.metadatas[i].get('language', 'vi') if i < len(self.metadatas) else 'vi'
|
| 225 |
+
query_language = self.multilingual_manager.detect_language(query)
|
| 226 |
+
|
| 227 |
+
# Language matching bonus
|
| 228 |
+
if doc_language == query_language:
|
| 229 |
+
score += 0.5
|
| 230 |
+
|
| 231 |
+
# Keyword matching
|
| 232 |
+
for word in query_lower.split():
|
| 233 |
+
if len(word) > 2 and word in doc.lower():
|
| 234 |
+
score += 1
|
| 235 |
+
|
| 236 |
+
if score > 0:
|
| 237 |
+
results.append(RAGSearchResult(
|
| 238 |
+
id=str(i),
|
| 239 |
+
text=doc,
|
| 240 |
+
similarity=min(score / 5, 1.0),
|
| 241 |
+
metadata=self.metadatas[i] if i < len(self.metadatas) else {}
|
| 242 |
+
))
|
| 243 |
+
|
| 244 |
+
results.sort(key=lambda x: x.similarity, reverse=True)
|
| 245 |
+
return results[:top_k]
|
| 246 |
+
|
| 247 |
+
def get_collection_stats(self) -> Dict:
|
| 248 |
+
"""Lấy thống kê collection với thông tin đa ngôn ngữ"""
|
| 249 |
+
language_stats = {}
|
| 250 |
+
for metadata in self.metadatas:
|
| 251 |
+
lang = metadata.get('language', 'unknown')
|
| 252 |
+
language_stats[lang] = language_stats.get(lang, 0) + 1
|
| 253 |
+
|
| 254 |
+
return {
|
| 255 |
+
'total_documents': len(self.documents),
|
| 256 |
+
'embedding_count': len(self.embeddings) if self.embeddings is not None else 0,
|
| 257 |
+
'embedding_dimension': self.current_dimension,
|
| 258 |
+
'language_distribution': language_stats,
|
| 259 |
+
'name': 'multilingual_rag_system',
|
| 260 |
+
'status': 'active',
|
| 261 |
+
'has_embeddings': self.embeddings is not None
|
| 262 |
+
}
|
core/speechbrain_vad.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torchaudio
|
| 3 |
+
import numpy as np
|
| 4 |
+
from speechbrain.inference import VAD
|
| 5 |
+
from typing import List, Tuple, Optional
|
| 6 |
+
import queue
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
from config.settings import settings
|
| 10 |
+
|
| 11 |
+
class SpeechBrainVAD:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.vad_model = None
|
| 14 |
+
self.sample_rate = settings.SAMPLE_RATE
|
| 15 |
+
self.threshold = settings.VAD_THRESHOLD
|
| 16 |
+
self.min_silence_duration = settings.VAD_MIN_SILENCE_DURATION
|
| 17 |
+
self.speech_pad_duration = settings.VAD_SPEECH_PAD_DURATION
|
| 18 |
+
self.is_running = False
|
| 19 |
+
self.audio_queue = queue.Queue()
|
| 20 |
+
self.speech_buffer = []
|
| 21 |
+
self.silence_start_time = None
|
| 22 |
+
self.callback = None
|
| 23 |
+
|
| 24 |
+
self._initialize_model()
|
| 25 |
+
|
| 26 |
+
def _initialize_model(self):
|
| 27 |
+
"""Khởi tạo mô hình VAD từ SpeechBrain"""
|
| 28 |
+
try:
|
| 29 |
+
print("🔄 Đang tải mô hình SpeechBrain VAD...")
|
| 30 |
+
self.vad_model = VAD.from_hparams(
|
| 31 |
+
source=settings.VAD_MODEL,
|
| 32 |
+
savedir=f"pretrained_models/{settings.VAD_MODEL}"
|
| 33 |
+
)
|
| 34 |
+
print("✅ Đã tải mô hình VAD thành công")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"❌ Lỗi tải mô hình VAD: {e}")
|
| 37 |
+
self.vad_model = None
|
| 38 |
+
|
| 39 |
+
def preprocess_audio(self, audio_data: np.ndarray, original_sr: int) -> np.ndarray:
|
| 40 |
+
"""Tiền xử lý audio cho VAD"""
|
| 41 |
+
if original_sr != self.sample_rate:
|
| 42 |
+
# Resample audio to VAD sample rate
|
| 43 |
+
audio_tensor = torch.from_numpy(audio_data).float()
|
| 44 |
+
if len(audio_tensor.shape) > 1:
|
| 45 |
+
audio_tensor = audio_tensor.mean(dim=0) # Convert to mono
|
| 46 |
+
|
| 47 |
+
resampler = torchaudio.transforms.Resample(
|
| 48 |
+
orig_freq=original_sr,
|
| 49 |
+
new_freq=self.sample_rate
|
| 50 |
+
)
|
| 51 |
+
audio_tensor = resampler(audio_tensor)
|
| 52 |
+
audio_data = audio_tensor.numpy()
|
| 53 |
+
|
| 54 |
+
# Normalize audio
|
| 55 |
+
if np.max(np.abs(audio_data)) > 0:
|
| 56 |
+
audio_data = audio_data / np.max(np.abs(audio_data))
|
| 57 |
+
|
| 58 |
+
return audio_data
|
| 59 |
+
|
| 60 |
+
def detect_voice_activity(self, audio_chunk: np.ndarray) -> bool:
|
| 61 |
+
"""Phát hiện hoạt động giọng nói trong audio chunk"""
|
| 62 |
+
if self.vad_model is None:
|
| 63 |
+
# Fallback: simple energy-based VAD
|
| 64 |
+
return self._energy_based_vad(audio_chunk)
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
# Convert to tensor and add batch dimension
|
| 68 |
+
audio_tensor = torch.from_numpy(audio_chunk).float().unsqueeze(0)
|
| 69 |
+
|
| 70 |
+
# Get VAD probabilities
|
| 71 |
+
with torch.no_grad():
|
| 72 |
+
prob = self.vad_model.get_speech_prob_chunk(audio_tensor)
|
| 73 |
+
|
| 74 |
+
return prob.item() > self.threshold
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"❌ Lỗi VAD detection: {e}")
|
| 78 |
+
return self._energy_based_vad(audio_chunk)
|
| 79 |
+
|
| 80 |
+
def _energy_based_vad(self, audio_chunk: np.ndarray) -> bool:
|
| 81 |
+
"""Fallback VAD dựa trên năng lượng âm thanh"""
|
| 82 |
+
energy = np.mean(audio_chunk ** 2)
|
| 83 |
+
return energy > 0.01 # Simple threshold
|
| 84 |
+
|
| 85 |
+
def process_stream(self, audio_chunk: np.ndarray, original_sr: int):
|
| 86 |
+
"""Xử lý audio stream real-time"""
|
| 87 |
+
if not self.is_running:
|
| 88 |
+
return
|
| 89 |
+
|
| 90 |
+
# Preprocess audio
|
| 91 |
+
processed_audio = self.preprocess_audio(audio_chunk, original_sr)
|
| 92 |
+
|
| 93 |
+
# Detect voice activity
|
| 94 |
+
is_speech = self.detect_voice_activity(processed_audio)
|
| 95 |
+
|
| 96 |
+
if is_speech:
|
| 97 |
+
self.silence_start_time = None
|
| 98 |
+
self.speech_buffer.extend(processed_audio)
|
| 99 |
+
print("🎤 Đang nói...")
|
| 100 |
+
else:
|
| 101 |
+
# Silence detected
|
| 102 |
+
if self.silence_start_time is None:
|
| 103 |
+
self.silence_start_time = time.time()
|
| 104 |
+
elif len(self.speech_buffer) > 0:
|
| 105 |
+
silence_duration = time.time() - self.silence_start_time
|
| 106 |
+
if silence_duration >= self.min_silence_duration:
|
| 107 |
+
# End of speech segment
|
| 108 |
+
self._process_speech_segment()
|
| 109 |
+
|
| 110 |
+
return is_speech
|
| 111 |
+
|
| 112 |
+
def _process_speech_segment(self):
|
| 113 |
+
"""Xử lý segment giọng nói khi kết thúc"""
|
| 114 |
+
if len(self.speech_buffer) == 0:
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
# Convert buffer to numpy array
|
| 118 |
+
speech_audio = np.array(self.speech_buffer)
|
| 119 |
+
|
| 120 |
+
# Call callback with speech segment
|
| 121 |
+
if self.callback and callable(self.callback):
|
| 122 |
+
self.callback(speech_audio, self.sample_rate)
|
| 123 |
+
|
| 124 |
+
# Clear buffer
|
| 125 |
+
self.speech_buffer = []
|
| 126 |
+
self.silence_start_time = None
|
| 127 |
+
|
| 128 |
+
print("✅ Đã xử lý segment giọng nói")
|
| 129 |
+
|
| 130 |
+
def start_stream(self, callback: callable):
|
| 131 |
+
"""Bắt đầu xử lý stream"""
|
| 132 |
+
self.is_running = True
|
| 133 |
+
self.callback = callback
|
| 134 |
+
self.speech_buffer = []
|
| 135 |
+
self.silence_start_time = None
|
| 136 |
+
print("🎙️ Bắt đầu stream VAD...")
|
| 137 |
+
|
| 138 |
+
def stop_stream(self):
|
| 139 |
+
"""Dừng xử lý stream"""
|
| 140 |
+
self.is_running = False
|
| 141 |
+
# Process any remaining speech
|
| 142 |
+
if len(self.speech_buffer) > 0:
|
| 143 |
+
self._process_speech_segment()
|
| 144 |
+
print("🛑 Đã dừng stream VAD")
|
| 145 |
+
|
| 146 |
+
def get_audio_chunk_from_stream(self, stream, chunk_size: int = 1024):
|
| 147 |
+
"""Lấy audio chunk từ stream (for microphone input)"""
|
| 148 |
+
try:
|
| 149 |
+
data = stream.read(chunk_size, exception_on_overflow=False)
|
| 150 |
+
audio_data = np.frombuffer(data, dtype=np.int16)
|
| 151 |
+
return audio_data.astype(np.float32) / 32768.0 # Normalize to [-1, 1]
|
| 152 |
+
except Exception as e:
|
| 153 |
+
print(f"❌ Lỗi đọc audio stream: {e}")
|
| 154 |
+
return None
|
core/tts_service.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import re
|
| 3 |
+
import time
|
| 4 |
+
import asyncio
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
from gtts import gTTS
|
| 7 |
+
import edge_tts
|
| 8 |
+
from config.settings import settings
|
| 9 |
+
from models.schemas import TTSRequest
|
| 10 |
+
|
| 11 |
+
class EnhancedTTSService:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.supported_languages = settings.SUPPORTED_LANGUAGES
|
| 14 |
+
self.max_chunk_length = settings.MAX_CHUNK_LENGTH
|
| 15 |
+
|
| 16 |
+
def detect_language(self, text: str) -> str:
|
| 17 |
+
"""Đơn giản phát hiện ngôn ngữ dựa trên ký tự"""
|
| 18 |
+
vietnamese_chars = set('àáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ')
|
| 19 |
+
if any(char in vietnamese_chars for char in text.lower()):
|
| 20 |
+
return 'vi'
|
| 21 |
+
elif any(char in text for char in 'あいうえお'):
|
| 22 |
+
return 'ja'
|
| 23 |
+
elif any(char in text for char in '你好'):
|
| 24 |
+
return 'zh'
|
| 25 |
+
elif any(char in text for char in '안녕'):
|
| 26 |
+
return 'ko'
|
| 27 |
+
else:
|
| 28 |
+
return 'en'
|
| 29 |
+
|
| 30 |
+
def split_text_into_chunks(self, text: str, max_length: int = None) -> List[str]:
|
| 31 |
+
"""Chia văn bản thành các đoạn nhỏ cho TTS"""
|
| 32 |
+
if max_length is None:
|
| 33 |
+
max_length = self.max_chunk_length
|
| 34 |
+
|
| 35 |
+
sentences = re.split(r'[.!?]+', text)
|
| 36 |
+
chunks = []
|
| 37 |
+
current_chunk = ""
|
| 38 |
+
|
| 39 |
+
for sentence in sentences:
|
| 40 |
+
sentence = sentence.strip()
|
| 41 |
+
if not sentence:
|
| 42 |
+
continue
|
| 43 |
+
|
| 44 |
+
if len(sentence) > max_length:
|
| 45 |
+
parts = re.split(r'[,;:]', sentence)
|
| 46 |
+
for part in parts:
|
| 47 |
+
part = part.strip()
|
| 48 |
+
if not part:
|
| 49 |
+
continue
|
| 50 |
+
if len(current_chunk) + len(part) + 2 <= max_length:
|
| 51 |
+
if current_chunk:
|
| 52 |
+
current_chunk += ". " + part
|
| 53 |
+
else:
|
| 54 |
+
current_chunk = part
|
| 55 |
+
else:
|
| 56 |
+
if current_chunk:
|
| 57 |
+
chunks.append(current_chunk)
|
| 58 |
+
current_chunk = part
|
| 59 |
+
else:
|
| 60 |
+
if len(current_chunk) + len(sentence) + 2 <= max_length:
|
| 61 |
+
if current_chunk:
|
| 62 |
+
current_chunk += ". " + sentence
|
| 63 |
+
else:
|
| 64 |
+
current_chunk = sentence
|
| 65 |
+
else:
|
| 66 |
+
if current_chunk:
|
| 67 |
+
chunks.append(current_chunk)
|
| 68 |
+
current_chunk = sentence
|
| 69 |
+
|
| 70 |
+
if current_chunk:
|
| 71 |
+
chunks.append(current_chunk)
|
| 72 |
+
|
| 73 |
+
return chunks
|
| 74 |
+
|
| 75 |
+
def text_to_speech_gtts(self, text: str, language: str = 'vi') -> Optional[bytes]:
|
| 76 |
+
"""Sử dụng gTTS (Google Text-to-Speech) library"""
|
| 77 |
+
try:
|
| 78 |
+
chunks = self.split_text_into_chunks(text)
|
| 79 |
+
audio_chunks = []
|
| 80 |
+
|
| 81 |
+
for chunk in chunks:
|
| 82 |
+
if not chunk.strip():
|
| 83 |
+
continue
|
| 84 |
+
|
| 85 |
+
tts = gTTS(text=chunk, lang=language, slow=False)
|
| 86 |
+
audio_buffer = io.BytesIO()
|
| 87 |
+
tts.write_to_fp(audio_buffer)
|
| 88 |
+
audio_buffer.seek(0)
|
| 89 |
+
audio_chunks.append(audio_buffer.read())
|
| 90 |
+
|
| 91 |
+
time.sleep(0.1)
|
| 92 |
+
|
| 93 |
+
if audio_chunks:
|
| 94 |
+
return b''.join(audio_chunks)
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f"❌ Lỗi gTTS: {e}")
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
async def text_to_speech_edgetts(self, text: str, voice: str = 'vi-VN-NamMinhNeural') -> Optional[bytes]:
|
| 102 |
+
"""Sử dụng Edge-TTS (Microsoft Edge) - async version"""
|
| 103 |
+
try:
|
| 104 |
+
communicate = edge_tts.Communicate(text, voice)
|
| 105 |
+
audio_buffer = io.BytesIO()
|
| 106 |
+
|
| 107 |
+
async for chunk in communicate.stream():
|
| 108 |
+
if chunk["type"] == "audio":
|
| 109 |
+
audio_buffer.write(chunk["data"])
|
| 110 |
+
|
| 111 |
+
audio_buffer.seek(0)
|
| 112 |
+
return audio_buffer.read()
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"❌ Lỗi Edge-TTS: {e}")
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
def text_to_speech_edgetts_sync(self, text: str, voice: str = 'vi-VN-NamMinhNeural') -> Optional[bytes]:
|
| 119 |
+
"""Sync wrapper for Edge-TTS"""
|
| 120 |
+
try:
|
| 121 |
+
return asyncio.run(self.text_to_speech_edgetts(text, voice))
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"❌ Lỗi Edge-TTS sync: {e}")
|
| 124 |
+
return None
|
| 125 |
+
|
| 126 |
+
def text_to_speech(self, text: str, language: str = None, provider: str = "auto") -> Optional[bytes]:
|
| 127 |
+
"""Chuyển văn bản thành giọng nói với nhiều nhà cung cấp"""
|
| 128 |
+
if not text or len(text.strip()) == 0:
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
if language is None:
|
| 132 |
+
language = self.detect_language(text)
|
| 133 |
+
|
| 134 |
+
text = self.clean_text(text)
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
if provider == "auto" or provider == "gtts":
|
| 138 |
+
print(f"🔊 Đang sử dụng gTTS cho văn bản {len(text)} ký tự...")
|
| 139 |
+
audio_bytes = self.text_to_speech_gtts(text, language)
|
| 140 |
+
if audio_bytes:
|
| 141 |
+
return audio_bytes
|
| 142 |
+
|
| 143 |
+
if provider == "auto" or provider == "edgetts":
|
| 144 |
+
print(f"🔊 Đang thử Edge-TTS cho văn bản {len(text)} ký tự...")
|
| 145 |
+
voice_map = {
|
| 146 |
+
'vi': 'vi-VN-NamMinhNeural',
|
| 147 |
+
'en': 'en-US-AriaNeural',
|
| 148 |
+
'fr': 'fr-FR-DeniseNeural',
|
| 149 |
+
'es': 'es-ES-ElviraNeural',
|
| 150 |
+
'de': 'de-DE-KatjaNeural',
|
| 151 |
+
'ja': 'ja-JP-NanamiNeural',
|
| 152 |
+
'ko': 'ko-KR-SunHiNeural',
|
| 153 |
+
'zh': 'zh-CN-XiaoxiaoNeural'
|
| 154 |
+
}
|
| 155 |
+
voice = voice_map.get(language, 'vi-VN-NamMinhNeural')
|
| 156 |
+
audio_bytes = self.text_to_speech_edgetts_sync(text, voice)
|
| 157 |
+
if audio_bytes:
|
| 158 |
+
return audio_bytes
|
| 159 |
+
|
| 160 |
+
return self.text_to_speech_gtts(text, language)
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
print(f"❌ Lỗi TTS tổng hợp: {e}")
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
def clean_text(self, text: str) -> str:
|
| 167 |
+
"""Làm sạch văn bản trước khi chuyển thành giọng nói"""
|
| 168 |
+
text = re.sub(r'http\S+', '', text)
|
| 169 |
+
text = re.sub(r'[^\w\sàáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ.,!?;:()-]', '', text)
|
| 170 |
+
text = re.sub(r'\s+', ' ', text)
|
| 171 |
+
return text.strip()
|
| 172 |
+
|
| 173 |
+
def save_audio_to_file(self, audio_bytes: bytes, filename: str = None) -> str:
|
| 174 |
+
"""Lưu audio bytes thành file tạm thời"""
|
| 175 |
+
if audio_bytes is None:
|
| 176 |
+
return None
|
| 177 |
+
|
| 178 |
+
if filename is None:
|
| 179 |
+
filename = f"tts_output_{int(time.time())}.mp3"
|
| 180 |
+
|
| 181 |
+
import os
|
| 182 |
+
temp_dir = "temp_audio"
|
| 183 |
+
os.makedirs(temp_dir, exist_ok=True)
|
| 184 |
+
|
| 185 |
+
filepath = os.path.join(temp_dir, filename)
|
| 186 |
+
with open(filepath, 'wb') as f:
|
| 187 |
+
f.write(audio_bytes)
|
| 188 |
+
|
| 189 |
+
return filepath
|
core/wikipedia_processor.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from typing import List
|
| 5 |
+
from models.schemas import RAGDocument
|
| 6 |
+
|
| 7 |
+
class WikipediaProcessor:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.supported_formats = ['.txt', '.csv', '.json']
|
| 10 |
+
|
| 11 |
+
def process_uploaded_file(self, file_path: str) -> List[str]:
|
| 12 |
+
"""Xử lý file Wikipedia uploaded"""
|
| 13 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
if file_ext == '.txt':
|
| 17 |
+
return self._process_txt_file(file_path)
|
| 18 |
+
elif file_ext == '.csv':
|
| 19 |
+
return self._process_csv_file(file_path)
|
| 20 |
+
elif file_ext == '.json':
|
| 21 |
+
return self._process_json_file(file_path)
|
| 22 |
+
else:
|
| 23 |
+
raise ValueError(f"Định dạng file không được hỗ trợ: {file_ext}")
|
| 24 |
+
except Exception as e:
|
| 25 |
+
raise Exception(f"Lỗi xử lý file: {str(e)}")
|
| 26 |
+
|
| 27 |
+
def _process_txt_file(self, file_path: str) -> List[str]:
|
| 28 |
+
"""Xử lý file text"""
|
| 29 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 30 |
+
content = f.read()
|
| 31 |
+
|
| 32 |
+
paragraphs = [p.strip() for p in content.split('\n\n') if p.strip() and len(p.strip()) > 20]
|
| 33 |
+
return paragraphs
|
| 34 |
+
|
| 35 |
+
def _process_csv_file(self, file_path: str) -> List[str]:
|
| 36 |
+
"""Xử lý file CSV"""
|
| 37 |
+
try:
|
| 38 |
+
df = pd.read_csv(file_path)
|
| 39 |
+
documents = []
|
| 40 |
+
|
| 41 |
+
for _, row in df.iterrows():
|
| 42 |
+
doc_parts = []
|
| 43 |
+
for col in df.columns:
|
| 44 |
+
if pd.notna(row[col]) and str(row[col]).strip():
|
| 45 |
+
doc_parts.append(f"{col}: {row[col]}")
|
| 46 |
+
if doc_parts:
|
| 47 |
+
documents.append(" | ".join(doc_parts))
|
| 48 |
+
|
| 49 |
+
return documents
|
| 50 |
+
except Exception as e:
|
| 51 |
+
raise Exception(f"Lỗi đọc CSV: {str(e)}")
|
| 52 |
+
|
| 53 |
+
def _process_json_file(self, file_path: str) -> List[str]:
|
| 54 |
+
"""Xử lý file JSON"""
|
| 55 |
+
try:
|
| 56 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 57 |
+
data = json.load(f)
|
| 58 |
+
|
| 59 |
+
documents = []
|
| 60 |
+
|
| 61 |
+
def extract_text(obj, current_path=""):
|
| 62 |
+
if isinstance(obj, dict):
|
| 63 |
+
for key, value in obj.items():
|
| 64 |
+
extract_text(value, f"{current_path}.{key}" if current_path else key)
|
| 65 |
+
elif isinstance(obj, list):
|
| 66 |
+
for item in obj:
|
| 67 |
+
extract_text(item, current_path)
|
| 68 |
+
elif isinstance(obj, str) and len(obj.strip()) > 10:
|
| 69 |
+
documents.append(f"{current_path}: {obj.strip()}")
|
| 70 |
+
|
| 71 |
+
extract_text(data)
|
| 72 |
+
return documents
|
| 73 |
+
except Exception as e:
|
| 74 |
+
raise Exception(f"Lỗi đọc JSON: {str(e)}")
|
main.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import groq
|
| 3 |
+
from config.settings import settings
|
| 4 |
+
from core.rag_system import EnhancedRAGSystem
|
| 5 |
+
from core.tts_service import EnhancedTTSService
|
| 6 |
+
from core.wikipedia_processor import WikipediaProcessor
|
| 7 |
+
from services.audio_service import AudioService
|
| 8 |
+
from services.chat_service import ChatService
|
| 9 |
+
from services.image_service import ImageService
|
| 10 |
+
from services.streaming_voice_service import StreamingVoiceService
|
| 11 |
+
from ui.components import create_custom_css, create_header
|
| 12 |
+
from ui.tabs import create_all_tabs
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
# Initialize clients and services
|
| 16 |
+
if not settings.GROQ_API_KEY:
|
| 17 |
+
raise ValueError("Please set the GROQ_API_KEY environment variable.")
|
| 18 |
+
|
| 19 |
+
client = groq.Client(api_key=settings.GROQ_API_KEY)
|
| 20 |
+
|
| 21 |
+
# Initialize core systems
|
| 22 |
+
rag_system = EnhancedRAGSystem()
|
| 23 |
+
tts_service = EnhancedTTSService()
|
| 24 |
+
wikipedia_processor = WikipediaProcessor()
|
| 25 |
+
|
| 26 |
+
# Initialize services
|
| 27 |
+
audio_service = AudioService(client, rag_system, tts_service)
|
| 28 |
+
chat_service = ChatService(client, rag_system, tts_service)
|
| 29 |
+
image_service = ImageService(client)
|
| 30 |
+
streaming_voice_service = StreamingVoiceService(client, rag_system, tts_service)
|
| 31 |
+
|
| 32 |
+
# Create Gradio interface
|
| 33 |
+
with gr.Blocks(css=create_custom_css(), theme=gr.themes.Soft(primary_hue="orange", neutral_hue="slate")) as demo:
|
| 34 |
+
create_header()
|
| 35 |
+
gr.Markdown("### 🌐 Hệ thống Đa ngôn ngữ - Tự động chuyển đổi model theo ngôn ngữ")
|
| 36 |
+
create_all_tabs(
|
| 37 |
+
audio_service=audio_service,
|
| 38 |
+
chat_service=chat_service,
|
| 39 |
+
image_service=image_service,
|
| 40 |
+
rag_system=rag_system,
|
| 41 |
+
tts_service=tts_service,
|
| 42 |
+
wikipedia_processor=wikipedia_processor,
|
| 43 |
+
streaming_voice_service=streaming_voice_service
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return demo
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
demo = main()
|
| 50 |
+
demo.launch(share=True)
|
models/__pycache__/schemas.cpython-310.pyc
ADDED
|
Binary file (1.22 kB). View file
|
|
|
models/schemas.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any, Optional
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
|
| 4 |
+
class RAGDocument(BaseModel):
|
| 5 |
+
text: str
|
| 6 |
+
metadata: Dict[str, Any] = {}
|
| 7 |
+
similarity: Optional[float] = None
|
| 8 |
+
|
| 9 |
+
class RAGSearchResult(BaseModel):
|
| 10 |
+
id: str
|
| 11 |
+
text: str
|
| 12 |
+
similarity: float
|
| 13 |
+
metadata: Dict[str, Any]
|
| 14 |
+
|
| 15 |
+
class ChatMessage(BaseModel):
|
| 16 |
+
role: str
|
| 17 |
+
content: str
|
| 18 |
+
|
| 19 |
+
class TTSRequest(BaseModel):
|
| 20 |
+
text: str
|
| 21 |
+
language: str = 'vi'
|
| 22 |
+
provider: str = 'auto'
|
requirements.txt
CHANGED
|
@@ -8,4 +8,4 @@ faiss-cpu
|
|
| 8 |
edge-tts
|
| 9 |
gtts
|
| 10 |
groq
|
| 11 |
-
|
|
|
|
| 8 |
edge-tts
|
| 9 |
gtts
|
| 10 |
groq
|
| 11 |
+
speechbrain
|
services/__pycache__/audio_service.cpython-310.pyc
ADDED
|
Binary file (4.1 kB). View file
|
|
|
services/__pycache__/chat_service.cpython-310.pyc
ADDED
|
Binary file (3.03 kB). View file
|
|
|
services/__pycache__/image_service.cpython-310.pyc
ADDED
|
Binary file (1.81 kB). View file
|
|
|
services/__pycache__/streaming_voice_service.cpython-310.pyc
ADDED
|
Binary file (6.17 kB). View file
|
|
|
services/audio_service.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import soundfile as sf
|
| 3 |
+
import io
|
| 4 |
+
from groq import Groq
|
| 5 |
+
from config.settings import settings
|
| 6 |
+
from core.rag_system import EnhancedRAGSystem
|
| 7 |
+
from core.tts_service import EnhancedTTSService
|
| 8 |
+
from core.multilingual_manager import MultilingualManager # NEW
|
| 9 |
+
|
| 10 |
+
class AudioService:
|
| 11 |
+
|
| 12 |
+
def __init__(self, groq_client: Groq, rag_system: EnhancedRAGSystem, tts_service: EnhancedTTSService):
|
| 13 |
+
self.groq_client = groq_client
|
| 14 |
+
self.rag_system = EnhancedRAGSystem()
|
| 15 |
+
self.tts_service = EnhancedTTSService()
|
| 16 |
+
self.multilingual_manager = MultilingualManager() # NEW
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def transcribe_audio(self, audio: str) -> str:
|
| 20 |
+
"""Chuyển đổi giọng nói thành văn bản sử dụng mô hình Whisper."""
|
| 21 |
+
if not audio:
|
| 22 |
+
raise ValueError("Audio input is empty.")
|
| 23 |
+
sr, y =audio
|
| 24 |
+
|
| 25 |
+
if y.ndim > 1:
|
| 26 |
+
y = np.mean(y, axis=1) # Chuyển đổi sang mono nếu cần
|
| 27 |
+
y = y.astype(np.float32)
|
| 28 |
+
y /= np.max(np.abs(y)) # Chuẩn hóa âm thanh
|
| 29 |
+
|
| 30 |
+
buffer = io.BytesIO()
|
| 31 |
+
sf.write(buffer, y, sr, format='WAV')
|
| 32 |
+
buffer.seek(0)
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
completion = self.groq_client.audio.transcribe(
|
| 36 |
+
model=settings.WHISPER_MODEL,
|
| 37 |
+
audio=buffer,
|
| 38 |
+
response_format="text"
|
| 39 |
+
)
|
| 40 |
+
transcription = completion
|
| 41 |
+
except Exception as e:
|
| 42 |
+
transcription = f"Error trong quá trình chuyển đổi giọng nói thành văn bản: {e}"
|
| 43 |
+
|
| 44 |
+
language = self.multilingual_manager.detect_language(transcription)
|
| 45 |
+
respone = self._generate_response_with_rag(transcription, language)
|
| 46 |
+
|
| 47 |
+
tts_audio = None
|
| 48 |
+
if respone and respone.startswith("Error") is False:
|
| 49 |
+
tts_bytes = self.tts_service.text_to_speech(respone, language)
|
| 50 |
+
if tts_bytes:
|
| 51 |
+
tts_audio_path = self.tts_service.save_tts_audio(tts_bytes)
|
| 52 |
+
tts_audio = tts_audio_path
|
| 53 |
+
return transcription, respone, tts_audio, language
|
| 54 |
+
|
| 55 |
+
def _generate_response_with_rag(self, query: str, language: str) -> str:
|
| 56 |
+
"""Tạo phản hồi sử dụng hệ thống RAG dựa trên truy vấn và ngôn ngữ."""
|
| 57 |
+
if not query or query.startswith("Error"):
|
| 58 |
+
return "Error: Truy vấn không hợp lệ để tạo phản hồi."
|
| 59 |
+
try:
|
| 60 |
+
rag_results = self.rag_system.semantic_search(query, top_k=3)
|
| 61 |
+
context_text = ""
|
| 62 |
+
if rag_results:
|
| 63 |
+
for result in rag_results:
|
| 64 |
+
context_text += result.document + "\n"
|
| 65 |
+
llm_model = self.multilingual_manager.get_llm_model(language)
|
| 66 |
+
if language == "vi":
|
| 67 |
+
system_prompt = """Bạn là trợ lý AI thông minh chuyên về tiếng Việt. Hãy sử dụng thông tin từ cơ sở kiến thức được cung cấp để trả lời câu hỏi một cách chính xác và hữu ích bằng tiếng Việt.
|
| 68 |
+
Thông tin tham khảo từ cơ sở kiến thức:
|
| 69 |
+
{context}
|
| 70 |
+
Nếu thông tin từ cơ sở kiến thức không đủ để trả lời, hãy dựa vào kiến thức chung của bạn. Luôn trả lời bằng tiếng Việt tự nhiên và dễ hiểu."""
|
| 71 |
+
else:
|
| 72 |
+
system_prompt = """You are a smart AI assistant. Please use the information from the provided knowledge base to answer questions accurately and helpfully in the same language as the user's question.
|
| 73 |
+
|
| 74 |
+
Reference information from knowledge base:
|
| 75 |
+
{context}
|
| 76 |
+
|
| 77 |
+
If the information from the knowledge base is not sufficient to answer, rely on your general knowledge. Always respond in natural and easy-to-understand language matching the user's language."""
|
| 78 |
+
message = [
|
| 79 |
+
{"role": "system", "content": system_prompt.format(context=context_text)},
|
| 80 |
+
{"role": "user", "content": query}
|
| 81 |
+
]
|
| 82 |
+
completion = self.groq_client.chat.completions.create(
|
| 83 |
+
model=llm_model,
|
| 84 |
+
messages=message,
|
| 85 |
+
max_tokens=512,
|
| 86 |
+
temperature=0.7,
|
| 87 |
+
)
|
| 88 |
+
return completion.choices[0].message['content'].strip()
|
| 89 |
+
except Exception as e:
|
| 90 |
+
return f"Error trong quá trình tạo phản hồi với RAG: {e}"
|
| 91 |
+
|
services/chat_service.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Tuple, Optional
|
| 2 |
+
from groq import Groq
|
| 3 |
+
from config.settings import settings
|
| 4 |
+
from core.rag_system import EnhancedRAGSystem
|
| 5 |
+
from core.tts_service import EnhancedTTSService
|
| 6 |
+
from models.schemas import ChatMessage
|
| 7 |
+
|
| 8 |
+
class ChatService:
|
| 9 |
+
def __init__(self, groq_client: Groq, rag_system: EnhancedRAGSystem, tts_service: EnhancedTTSService):
|
| 10 |
+
self.groq_client = groq_client
|
| 11 |
+
self.rag_system = rag_system
|
| 12 |
+
self.tts_service = tts_service
|
| 13 |
+
self.multilingual_manager = rag_system.multilingual_manager
|
| 14 |
+
|
| 15 |
+
def respond(self, message: str,chat_history: List[Tuple[str, str]]) -> tuple:
|
| 16 |
+
"""Tạo phản hồi cho tin nhắn đầu vào dựa trên lịch sử trò chuyện và ngôn ngữ."""
|
| 17 |
+
if chat_history is None:
|
| 18 |
+
chat_history = []
|
| 19 |
+
|
| 20 |
+
language = self.multilingual_manager.detect_language(message)
|
| 21 |
+
llm_model = self.multilingual_manager.get_llm_model(language)
|
| 22 |
+
|
| 23 |
+
messages = []
|
| 24 |
+
for user_msg, assistant_msg in chat_history:
|
| 25 |
+
messages.append({"role": "user", "content": user_msg})
|
| 26 |
+
messages.append({"role": "assistant", "content": assistant_msg})
|
| 27 |
+
messages.append({"role": "user", "content": message})
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
rag_results = self.rag_system.semantic_search(message, top_k=3)
|
| 31 |
+
context_text = ""
|
| 32 |
+
if rag_results:
|
| 33 |
+
context_text = "\n Thông tin tham khảo:\n +".join([result.document for result in rag_results])
|
| 34 |
+
|
| 35 |
+
if language == 'vi':
|
| 36 |
+
system_message = {
|
| 37 |
+
"role": "system",
|
| 38 |
+
"content": f"Bạn là trợ lý AI hữu ích chuyên về tiếng Việt. Sử dụng thông tin từ cơ sở kiến thức khi có liên quan. Luôn trả lời bằng tiếng Việt tự nhiên.{context_text}"
|
| 39 |
+
}
|
| 40 |
+
else:
|
| 41 |
+
system_message = {
|
| 42 |
+
"role": "system",
|
| 43 |
+
"content": f"You are a helpful AI assistant. Use information from the knowledge base when relevant. Always respond in natural language matching the user's language.{context_text}"
|
| 44 |
+
}
|
| 45 |
+
messages_with_context = [system_message] + messages
|
| 46 |
+
|
| 47 |
+
completion = self.groq_client.chat.completions.create(
|
| 48 |
+
model=llm_model,
|
| 49 |
+
messages=messages_with_context,
|
| 50 |
+
max_tokens=512,
|
| 51 |
+
temperature=0.7,
|
| 52 |
+
)
|
| 53 |
+
assistant_message = completion.choices[0].message['content'].strip()
|
| 54 |
+
|
| 55 |
+
chat_history.append((message, assistant_message))
|
| 56 |
+
|
| 57 |
+
tts_audio_path = None
|
| 58 |
+
if assistant_message and not assistant_message.startswith("Error"):
|
| 59 |
+
tts_bytes = self.tts_service.text_to_speech(assistant_message, language)
|
| 60 |
+
if tts_bytes:
|
| 61 |
+
tts_audio_path = self.tts_service.save_tts_audio(tts_bytes)
|
| 62 |
+
except Exception as e:
|
| 63 |
+
assistant_message = f"Error trong quá trình tạo phản hồi: {e}"
|
| 64 |
+
chat_history.append((message, assistant_message))
|
| 65 |
+
tts_audio_path = None
|
| 66 |
+
return "", chat_history, tts_audio_path, language
|
| 67 |
+
def clear_chat_history(self, chat_history: List[Tuple[str,str]]) -> tuple:
|
| 68 |
+
"""Xóa lịch sử trò chuyện (nếu lưu trữ lịch sử trong phiên làm việc)."""
|
| 69 |
+
return [],[]
|
services/image_service.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from groq import Groq
|
| 2 |
+
from config.settings import settings
|
| 3 |
+
|
| 4 |
+
class ImageService:
|
| 5 |
+
def __init__(self, groq_client: Groq):
|
| 6 |
+
self.client = groq_client
|
| 7 |
+
|
| 8 |
+
def analyze_image_with_description(self, image, user_description: str) -> str:
|
| 9 |
+
"""Phân tích hình ảnh kết hợp với mô tả từ người dùng"""
|
| 10 |
+
if image is None:
|
| 11 |
+
return "No image uploaded."
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
if user_description:
|
| 15 |
+
prompt = f"""Người dùng tải lên một hình ảnh và mô tả: "{user_description}"
|
| 16 |
+
|
| 17 |
+
Dựa trên mô tả này, hãy phân tích chi tiết bằng tiếng Việt:
|
| 18 |
+
1. Mô tả chi tiết những gì có trong hình ảnh
|
| 19 |
+
2. Phân tích các yếu tố liên quan đến mô tả của người dùng
|
| 20 |
+
3. Đưa ra nhận xét và thông tin hữu ích"""
|
| 21 |
+
else:
|
| 22 |
+
prompt = """Hãy mô tả chi tiết bằng tiếng Việt những gì bạn nghĩ có thể có trong hình ảnh này.
|
| 23 |
+
Mô tả các đối tượng, màu sắc, bố cục và ngữ cảnh có thể có của hình ảnh."""
|
| 24 |
+
|
| 25 |
+
chat_completion = self.client.chat.completions.create(
|
| 26 |
+
messages=[{"role": "user", "content": prompt}],
|
| 27 |
+
model=settings.LLM_MODEL,
|
| 28 |
+
)
|
| 29 |
+
description = chat_completion.choices[0].message.content
|
| 30 |
+
except Exception as e:
|
| 31 |
+
description = f"Error in image analysis: {str(e)}"
|
| 32 |
+
|
| 33 |
+
return description
|
services/streaming_voice_service.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import numpy as np
|
| 3 |
+
import soundfile as sf
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
import pyaudio
|
| 7 |
+
from groq import Groq
|
| 8 |
+
from typing import Optional, Callable
|
| 9 |
+
from config.settings import settings
|
| 10 |
+
from core.speechbrain_vad import SpeechBrainVAD
|
| 11 |
+
from core.rag_system import EnhancedRAGSystem
|
| 12 |
+
from core.tts_service import EnhancedTTSService
|
| 13 |
+
|
| 14 |
+
class StreamingVoiceService:
|
| 15 |
+
def __init__(self, groq_client: Groq, rag_system: EnhancedRAGSystem, tts_service: EnhancedTTSService):
|
| 16 |
+
self.client = groq_client
|
| 17 |
+
self.rag_system = rag_system
|
| 18 |
+
self.tts_service = tts_service
|
| 19 |
+
self.vad_processor = SpeechBrainVAD()
|
| 20 |
+
|
| 21 |
+
# Streaming state
|
| 22 |
+
self.is_listening = False
|
| 23 |
+
self.audio_stream = None
|
| 24 |
+
self.pyaudio_instance = None
|
| 25 |
+
self.callback_handler = None
|
| 26 |
+
|
| 27 |
+
# Conversation context
|
| 28 |
+
self.conversation_history = []
|
| 29 |
+
self.current_transcription = ""
|
| 30 |
+
|
| 31 |
+
def start_listening(self, callback_handler: Callable):
|
| 32 |
+
"""Bắt đầu lắng nghe với VAD"""
|
| 33 |
+
if self.is_listening:
|
| 34 |
+
return False
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
self.callback_handler = callback_handler
|
| 38 |
+
self.is_listening = True
|
| 39 |
+
self.conversation_history = []
|
| 40 |
+
|
| 41 |
+
# Initialize PyAudio
|
| 42 |
+
self.pyaudio_instance = pyaudio.PyAudio()
|
| 43 |
+
|
| 44 |
+
# Start audio stream
|
| 45 |
+
self.audio_stream = self.pyaudio_instance.open(
|
| 46 |
+
format=pyaudio.paInt16,
|
| 47 |
+
channels=1,
|
| 48 |
+
rate=settings.SAMPLE_RATE,
|
| 49 |
+
input=True,
|
| 50 |
+
frames_per_buffer=1024,
|
| 51 |
+
stream_callback=self._audio_callback
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Start VAD processing
|
| 55 |
+
self.vad_processor.start_stream(self._process_speech_segment)
|
| 56 |
+
|
| 57 |
+
print("🎙️ Bắt đầu lắng nghe...")
|
| 58 |
+
return True
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"❌ Lỗi khởi động stream: {e}")
|
| 62 |
+
self.stop_listening()
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
def stop_listening(self):
|
| 66 |
+
"""Dừng lắng nghe"""
|
| 67 |
+
self.is_listening = False
|
| 68 |
+
|
| 69 |
+
if self.audio_stream:
|
| 70 |
+
self.audio_stream.stop_stream()
|
| 71 |
+
self.audio_stream.close()
|
| 72 |
+
|
| 73 |
+
if self.pyaudio_instance:
|
| 74 |
+
self.pyaudio_instance.terminate()
|
| 75 |
+
|
| 76 |
+
self.vad_processor.stop_stream()
|
| 77 |
+
print("🛑 Đã dừng lắng nghe")
|
| 78 |
+
|
| 79 |
+
def _audio_callback(self, in_data, frame_count, time_info, status):
|
| 80 |
+
"""Callback xử lý audio input real-time"""
|
| 81 |
+
if status:
|
| 82 |
+
print(f"Audio stream status: {status}")
|
| 83 |
+
|
| 84 |
+
if self.is_listening:
|
| 85 |
+
# Convert audio data to numpy array
|
| 86 |
+
audio_data = np.frombuffer(in_data, dtype=np.int16)
|
| 87 |
+
audio_float = audio_data.astype(np.float32) / 32768.0
|
| 88 |
+
|
| 89 |
+
# Process with VAD
|
| 90 |
+
self.vad_processor.process_stream(audio_float, settings.SAMPLE_RATE)
|
| 91 |
+
|
| 92 |
+
return (in_data, pyaudio.paContinue)
|
| 93 |
+
|
| 94 |
+
def _process_speech_segment(self, speech_audio: np.ndarray, sample_rate: int):
|
| 95 |
+
"""Xử lý segment giọng nói đã được VAD phát hiện"""
|
| 96 |
+
if not self.is_listening or len(speech_audio) == 0:
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
print(f"🎯 Đang xử lý segment giọng nói ({len(speech_audio)} samples)...")
|
| 100 |
+
|
| 101 |
+
# Transcribe speech segment
|
| 102 |
+
transcription = self._transcribe_audio(speech_audio, sample_rate)
|
| 103 |
+
if transcription and len(transcription.strip()) > 0:
|
| 104 |
+
self.current_transcription = transcription
|
| 105 |
+
print(f"📝 Transcription: {transcription}")
|
| 106 |
+
|
| 107 |
+
# Generate AI response
|
| 108 |
+
response = self._generate_ai_response(transcription)
|
| 109 |
+
|
| 110 |
+
# Convert response to speech
|
| 111 |
+
tts_audio = self._text_to_speech(response)
|
| 112 |
+
|
| 113 |
+
# Call callback with results
|
| 114 |
+
if self.callback_handler:
|
| 115 |
+
self.callback_handler({
|
| 116 |
+
'transcription': transcription,
|
| 117 |
+
'response': response,
|
| 118 |
+
'tts_audio': tts_audio,
|
| 119 |
+
'speech_audio': speech_audio
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
def _transcribe_audio(self, audio_data: np.ndarray, sample_rate: int) -> Optional[str]:
|
| 123 |
+
"""Chuyển đổi audio thành văn bản sử dụng Whisper"""
|
| 124 |
+
try:
|
| 125 |
+
# Convert numpy array to bytes buffer
|
| 126 |
+
buffer = io.BytesIO()
|
| 127 |
+
sf.write(buffer, audio_data, sample_rate, format='wav')
|
| 128 |
+
buffer.seek(0)
|
| 129 |
+
|
| 130 |
+
# Transcribe with Whisper
|
| 131 |
+
transcription = self.client.audio.transcriptions.create(
|
| 132 |
+
model=settings.WHISPER_MODEL,
|
| 133 |
+
file=("speech.wav", buffer.read()),
|
| 134 |
+
response_format="text",
|
| 135 |
+
language="vi" # Focus on Vietnamese
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
return transcription.strip()
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f"❌ Lỗi transcription: {e}")
|
| 142 |
+
return None
|
| 143 |
+
|
| 144 |
+
def _generate_ai_response(self, user_input: str) -> str:
|
| 145 |
+
"""Tạo phản hồi AI với RAG context"""
|
| 146 |
+
try:
|
| 147 |
+
# Add to conversation history
|
| 148 |
+
self.conversation_history.append({"role": "user", "content": user_input})
|
| 149 |
+
|
| 150 |
+
# Semantic search với RAG
|
| 151 |
+
rag_results = self.rag_system.semantic_search(user_input, top_k=2)
|
| 152 |
+
context_text = "\n".join([f"- {doc.text}" for doc in rag_results]) if rag_results else ""
|
| 153 |
+
|
| 154 |
+
# Prepare messages với conversation history
|
| 155 |
+
system_prompt = f"""Bạn là trợ lý AI thông minh chuyên về tiếng Việt. Hãy trả lời ngắn gọn, tự nhiên và hữu ích. Sử dụng thông tin từ cơ sở kiến thức khi có liên quan.
|
| 156 |
+
|
| 157 |
+
Thông tin tham khảo:
|
| 158 |
+
{context_text}
|
| 159 |
+
|
| 160 |
+
Hãy giữ câu trả lời ngắn gọn và tự nhiên như đang trò chuyện."""
|
| 161 |
+
|
| 162 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 163 |
+
|
| 164 |
+
# Add last 3 exchanges for context (to keep it manageable)
|
| 165 |
+
recent_history = self.conversation_history[-6:] # Last 3 user-assistant pairs
|
| 166 |
+
messages.extend(recent_history)
|
| 167 |
+
|
| 168 |
+
# Generate response
|
| 169 |
+
completion = self.client.chat.completions.create(
|
| 170 |
+
model=settings.LLM_MODEL,
|
| 171 |
+
messages=messages,
|
| 172 |
+
max_tokens=150, # Keep responses concise for streaming
|
| 173 |
+
temperature=0.7
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
response = completion.choices[0].message.content
|
| 177 |
+
self.conversation_history.append({"role": "assistant", "content": response})
|
| 178 |
+
|
| 179 |
+
# Keep conversation history manageable
|
| 180 |
+
if len(self.conversation_history) > 10:
|
| 181 |
+
self.conversation_history = self.conversation_history[-10:]
|
| 182 |
+
|
| 183 |
+
return response
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
return f"Xin lỗi, tôi gặp lỗi: {str(e)}"
|
| 187 |
+
|
| 188 |
+
def _text_to_speech(self, text: str) -> Optional[str]:
|
| 189 |
+
"""Chuyển văn bản thành giọng nói"""
|
| 190 |
+
try:
|
| 191 |
+
tts_bytes = self.tts_service.text_to_speech(text, 'vi')
|
| 192 |
+
if tts_bytes:
|
| 193 |
+
return self.tts_service.save_audio_to_file(tts_bytes)
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"❌ Lỗi TTS: {e}")
|
| 196 |
+
|
| 197 |
+
return None
|
| 198 |
+
|
| 199 |
+
def get_conversation_state(self) -> dict:
|
| 200 |
+
"""Lấy trạng thái cuộc hội thoại hiện tại"""
|
| 201 |
+
return {
|
| 202 |
+
'is_listening': self.is_listening,
|
| 203 |
+
'history_length': len(self.conversation_history),
|
| 204 |
+
'current_transcription': self.current_transcription
|
| 205 |
+
}
|
ui/__pycache__/components.cpython-310.pyc
ADDED
|
Binary file (4.65 kB). View file
|
|
|
ui/__pycache__/tabs.cpython-310.pyc
ADDED
|
Binary file (11.6 kB). View file
|
|
|
ui/components.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
|
| 3 |
+
def create_custom_css() -> str:
|
| 4 |
+
return """
|
| 5 |
+
.gradio-container {
|
| 6 |
+
background-color: #1f1f1f;
|
| 7 |
+
}
|
| 8 |
+
.gr-markdown, .gr-markdown * {
|
| 9 |
+
color: #ffffff !important;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.streaming-active {
|
| 13 |
+
border: 3px solid #00ff00 !important;
|
| 14 |
+
animation: pulse 2s infinite;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.streaming-inactive {
|
| 18 |
+
border: 2px solid #ff4444 !important;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@keyframes pulse {
|
| 22 |
+
0% { border-color: #00ff00; }
|
| 23 |
+
50% { border-color: #00cc00; }
|
| 24 |
+
100% { border-color: #00ff00; }
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.vad-visualizer {
|
| 28 |
+
background: linear-gradient(90deg, #00ff00, #ffff00, #ff0000);
|
| 29 |
+
height: 20px;
|
| 30 |
+
border-radius: 10px;
|
| 31 |
+
margin: 10px 0;
|
| 32 |
+
transition: all 0.1s ease;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.conversation-bubble {
|
| 36 |
+
background: #2d2d2d;
|
| 37 |
+
border-radius: 15px;
|
| 38 |
+
padding: 10px 15px;
|
| 39 |
+
margin: 5px 0;
|
| 40 |
+
border-left: 4px solid #f55036;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.user-bubble {
|
| 44 |
+
border-left-color: #36a2f5;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.assistant-bubble {
|
| 48 |
+
border-left-color: #f55036;
|
| 49 |
+
}
|
| 50 |
+
"""
|
| 51 |
+
def create_header() -> gr.Markdown:
|
| 52 |
+
return gr.Markdown("# 🎙️ Groq x Gradio Multi-Modal với RAG Wikipedia")
|
| 53 |
+
def create_audio_components() -> tuple:
|
| 54 |
+
with gr.Row():
|
| 55 |
+
audio_input = gr.Audio(type="numpy", label="Nói hoặc tải lên file âm thanh")
|
| 56 |
+
|
| 57 |
+
with gr.Row():
|
| 58 |
+
transcription_output = gr.Textbox(
|
| 59 |
+
label="Bản ghi âm",
|
| 60 |
+
lines=5,
|
| 61 |
+
interactive=True,
|
| 62 |
+
placeholder="Bản ghi âm sẽ hiển thị ở đây..."
|
| 63 |
+
)
|
| 64 |
+
response_output = gr.Textbox(
|
| 65 |
+
label="Phản hồi AI",
|
| 66 |
+
lines=5,
|
| 67 |
+
interactive=True,
|
| 68 |
+
placeholder="Phản hồi của AI sẽ hiển thị ở đây..."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
with gr.Row():
|
| 72 |
+
tts_audio_output = gr.Audio(
|
| 73 |
+
label="Phản hồi bằng giọng nói",
|
| 74 |
+
interactive=False
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
process_button = gr.Button("Xử lý", variant="primary")
|
| 78 |
+
|
| 79 |
+
return audio_input, transcription_output, response_output, tts_audio_output, process_button
|
| 80 |
+
|
| 81 |
+
def create_chat_components() -> tuple:
|
| 82 |
+
chatbot = gr.Chatbot()
|
| 83 |
+
state = gr.State([])
|
| 84 |
+
|
| 85 |
+
with gr.Row():
|
| 86 |
+
user_input = gr.Textbox(
|
| 87 |
+
show_label=False,
|
| 88 |
+
placeholder="Nhập tin nhắn của bạn ở đây...",
|
| 89 |
+
container=False,
|
| 90 |
+
scale=4
|
| 91 |
+
)
|
| 92 |
+
send_button = gr.Button("Gửi", variant="primary", scale=1)
|
| 93 |
+
clear_button = gr.Button("Xóa Chat", variant="secondary", scale=1)
|
| 94 |
+
|
| 95 |
+
with gr.Row():
|
| 96 |
+
chat_tts_output = gr.Audio(
|
| 97 |
+
label="Phản hồi bằng giọng nói",
|
| 98 |
+
interactive=False
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return chatbot, state, user_input, send_button, clear_button, chat_tts_output
|
| 102 |
+
def create_streaming_voice_components() -> tuple:
|
| 103 |
+
"""Tạo components cho streaming voice với VAD"""
|
| 104 |
+
with gr.Group():
|
| 105 |
+
gr.Markdown("## 🎤 Trò chuyện giọng nói thời gian thực với VAD")
|
| 106 |
+
gr.Markdown("Hệ thống sẽ tự động phát hiện khi bạn nói và dừng, tạo cuộc hội thoại tự nhiên")
|
| 107 |
+
|
| 108 |
+
with gr.Row():
|
| 109 |
+
with gr.Column(scale=1):
|
| 110 |
+
# Voice activity visualization
|
| 111 |
+
vad_visualizer = gr.HTML(
|
| 112 |
+
value="<div class='vad-visualizer' style='width: 100%;'></div>",
|
| 113 |
+
label="Voice Activity"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# Controls
|
| 117 |
+
with gr.Row():
|
| 118 |
+
start_listening_btn = gr.Button(
|
| 119 |
+
"🎙️ Bắt đầu lắng nghe",
|
| 120 |
+
variant="primary",
|
| 121 |
+
size="sm"
|
| 122 |
+
)
|
| 123 |
+
stop_listening_btn = gr.Button(
|
| 124 |
+
"🛑 Dừng lắng nghe",
|
| 125 |
+
variant="secondary",
|
| 126 |
+
size="sm"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Status
|
| 130 |
+
status_display = gr.Textbox(
|
| 131 |
+
label="Trạng thái",
|
| 132 |
+
value="Chưa lắng nghe",
|
| 133 |
+
interactive=False
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Conversation state
|
| 137 |
+
state_display = gr.JSON(
|
| 138 |
+
label="Thông tin hội thoại",
|
| 139 |
+
value={}
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
with gr.Column(scale=2):
|
| 143 |
+
# Real-time transcription
|
| 144 |
+
realtime_transcription = gr.Textbox(
|
| 145 |
+
label="🎯 Đang nói...",
|
| 146 |
+
lines=2,
|
| 147 |
+
interactive=False,
|
| 148 |
+
placeholder="Văn bản được chuyển đổi sẽ xuất hiện ở đây..."
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# AI Response
|
| 152 |
+
ai_response = gr.Textbox(
|
| 153 |
+
label="🤖 Phản hồi AI",
|
| 154 |
+
lines=3,
|
| 155 |
+
interactive=False,
|
| 156 |
+
placeholder="Phản hồi của AI sẽ xuất hiện ở đây..."
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
# TTS Audio output
|
| 160 |
+
tts_output = gr.Audio(
|
| 161 |
+
label="🔊 Phản hồi bằng giọng nói",
|
| 162 |
+
interactive=False,
|
| 163 |
+
autoplay=True
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# Hidden state components
|
| 167 |
+
streaming_state = gr.State(value=False)
|
| 168 |
+
conversation_history = gr.State(value=[])
|
| 169 |
+
|
| 170 |
+
return (
|
| 171 |
+
start_listening_btn, stop_listening_btn, status_display, state_display,
|
| 172 |
+
realtime_transcription, ai_response, tts_output, streaming_state,
|
| 173 |
+
conversation_history, vad_visualizer
|
| 174 |
+
)
|
ui/tabs.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import threading
|
| 3 |
+
import time
|
| 4 |
+
from services.audio_service import AudioService
|
| 5 |
+
from services.chat_service import ChatService
|
| 6 |
+
from services.image_service import ImageService
|
| 7 |
+
from services.streaming_voice_service import StreamingVoiceService
|
| 8 |
+
from core.rag_system import EnhancedRAGSystem
|
| 9 |
+
from core.tts_service import EnhancedTTSService
|
| 10 |
+
from core.wikipedia_processor import WikipediaProcessor
|
| 11 |
+
from ui.components import create_audio_components, create_chat_components, create_streaming_voice_components
|
| 12 |
+
|
| 13 |
+
def create_all_tabs(audio_service: AudioService, chat_service: ChatService,
|
| 14 |
+
image_service: ImageService, rag_system: EnhancedRAGSystem,
|
| 15 |
+
tts_service: EnhancedTTSService, wikipedia_processor: WikipediaProcessor,
|
| 16 |
+
streaming_voice_service: StreamingVoiceService):
|
| 17 |
+
|
| 18 |
+
with gr.Tab("🎙️ Streaming Voice (VAD)"):
|
| 19 |
+
create_streaming_voice_tab(streaming_voice_service)
|
| 20 |
+
|
| 21 |
+
with gr.Tab("🎙️ Audio"):
|
| 22 |
+
create_audio_tab(audio_service)
|
| 23 |
+
|
| 24 |
+
with gr.Tab("💬 Chat"):
|
| 25 |
+
create_chat_tab(chat_service)
|
| 26 |
+
|
| 27 |
+
with gr.Tab("🖼️ Image"):
|
| 28 |
+
create_image_tab(image_service)
|
| 29 |
+
|
| 30 |
+
with gr.Tab("📚 RAG Wikipedia"):
|
| 31 |
+
create_rag_tab(rag_system, wikipedia_processor)
|
| 32 |
+
|
| 33 |
+
with gr.Tab("🔊 Text-to-Speech"):
|
| 34 |
+
create_tts_tab(tts_service)
|
| 35 |
+
|
| 36 |
+
with gr.Tab("🌐 Language Info"): # NEW TAB
|
| 37 |
+
create_language_info_tab(rag_system.multilingual_manager)
|
| 38 |
+
def create_rag_tab(rag_system: EnhancedRAGSystem, wikipedia_processor: WikipediaProcessor):
|
| 39 |
+
gr.Markdown("## Upload data có sẵn")
|
| 40 |
+
|
| 41 |
+
with gr.Row():
|
| 42 |
+
with gr.Column(scale=1):
|
| 43 |
+
gr.Markdown("### 📤 Upload dữ liệu")
|
| 44 |
+
file_upload = gr.File(
|
| 45 |
+
label="Tải lên file",
|
| 46 |
+
file_types=['.txt', '.csv', '.json'],
|
| 47 |
+
file_count="single"
|
| 48 |
+
)
|
| 49 |
+
upload_btn = gr.Button("📤 Upload Data", variant="primary")
|
| 50 |
+
upload_status = gr.Textbox(label="Trạng thái Upload", interactive=False)
|
| 51 |
+
|
| 52 |
+
gr.Markdown("### 📊 Thống kê Database")
|
| 53 |
+
stats_btn = gr.Button("📊 Database Stats", variant="secondary")
|
| 54 |
+
stats_display = gr.Textbox(label="Thống kê", interactive=False)
|
| 55 |
+
|
| 56 |
+
gr.Markdown("### 🔍 Tìm kiếm Database")
|
| 57 |
+
search_query = gr.Textbox(
|
| 58 |
+
label="Tìm kiếm trong database",
|
| 59 |
+
placeholder="Nhập từ khóa để tìm kiếm..."
|
| 60 |
+
)
|
| 61 |
+
search_btn = gr.Button("🔍 Tìm kiếm", variant="secondary")
|
| 62 |
+
|
| 63 |
+
with gr.Column(scale=2):
|
| 64 |
+
gr.Markdown("### 📋 Kết quả tìm kiếm RAG")
|
| 65 |
+
rag_results = gr.JSON(label="Tài liệu tham khảo tìm được")
|
| 66 |
+
|
| 67 |
+
def upload_wikipedia_file(file):
|
| 68 |
+
if file is None:
|
| 69 |
+
return "Vui lòng chọn file để upload"
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
documents = wikipedia_processor.process_uploaded_file(file.name)
|
| 73 |
+
|
| 74 |
+
if not documents:
|
| 75 |
+
return "Không tìm thấy dữ liệu nào trong file."
|
| 76 |
+
|
| 77 |
+
metadatas = [{"source": "wikipedia", "type": "knowledge", "file": file.name, "language": "vi"} for _ in documents]
|
| 78 |
+
rag_system.add_documents(documents, metadatas)
|
| 79 |
+
|
| 80 |
+
stats = rag_system.get_collection_stats()
|
| 81 |
+
return f"✅ Đã thêm {len(documents)} documents Wikipedia vào RAG database. Tổng số documents: {stats['count']}"
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
return f"❌ Lỗi xử lý file Wikipedia: {str(e)}"
|
| 85 |
+
|
| 86 |
+
upload_btn.click(upload_wikipedia_file, inputs=[file_upload], outputs=[upload_status])
|
| 87 |
+
stats_btn.click(rag_system.get_collection_stats, inputs=[], outputs=[stats_display])
|
| 88 |
+
search_btn.click(rag_system.semantic_search, inputs=[search_query], outputs=[rag_results])
|
| 89 |
+
def create_audio_tab(audio_service: AudioService):
|
| 90 |
+
gr.Markdown("## Nói chuyện với AI (Đa ngôn ngữ)")
|
| 91 |
+
audio_input, transcription_output, response_output, tts_audio_output, process_button = create_audio_components()
|
| 92 |
+
|
| 93 |
+
# NEW: Language display
|
| 94 |
+
language_display = gr.Textbox(
|
| 95 |
+
label="🌐 Ngôn ngữ phát hiện",
|
| 96 |
+
interactive=False,
|
| 97 |
+
placeholder="Ngôn ngữ sẽ hiển thị ở đây..."
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
process_button.click(
|
| 101 |
+
audio_service.transcribe_audio,
|
| 102 |
+
inputs=audio_input,
|
| 103 |
+
outputs=[transcription_output, response_output, tts_audio_output, language_display] # UPDATED
|
| 104 |
+
)
|
| 105 |
+
def create_image_tab(image_service: ImageService):
|
| 106 |
+
gr.Markdown("## Phân tích hình ảnh")
|
| 107 |
+
with gr.Row():
|
| 108 |
+
image_input = gr.Image(type="numpy", label="Tải lên hình ảnh")
|
| 109 |
+
with gr.Row():
|
| 110 |
+
image_description = gr.Textbox(
|
| 111 |
+
label="Mô tả hình ảnh của bạn (tùy chọn)",
|
| 112 |
+
placeholder="Mô tả ngắn về hình ảnh để AI phân tích chính xác hơn..."
|
| 113 |
+
)
|
| 114 |
+
with gr.Row():
|
| 115 |
+
image_output = gr.Textbox(label="Kết quả phân tích")
|
| 116 |
+
analyze_button = gr.Button("Phân tích hình ảnh", variant="primary")
|
| 117 |
+
analyze_button.click(
|
| 118 |
+
image_service.analyze_image_with_description,
|
| 119 |
+
inputs=[image_input, image_description],
|
| 120 |
+
outputs=[image_output]
|
| 121 |
+
)
|
| 122 |
+
def create_chat_tab(chat_service: ChatService):
|
| 123 |
+
gr.Markdown("## Trò chuyện với AI Assistant (Đa ngôn ngữ)")
|
| 124 |
+
chatbot, state, user_input, send_button, clear_button, chat_tts_output = create_chat_components()
|
| 125 |
+
|
| 126 |
+
# NEW: Language display
|
| 127 |
+
chat_language_display = gr.Textbox(
|
| 128 |
+
label="🌐 Ngôn ngữ phát hiện",
|
| 129 |
+
interactive=False,
|
| 130 |
+
placeholder="Ngôn ngữ sẽ hiển thị ở đây..."
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
send_button.click(
|
| 134 |
+
chat_service.respond,
|
| 135 |
+
inputs=[user_input, state],
|
| 136 |
+
outputs=[user_input, chatbot, state, chat_tts_output, chat_language_display] # UPDATED
|
| 137 |
+
)
|
| 138 |
+
clear_button.click(
|
| 139 |
+
chat_service.clear_chat_history,
|
| 140 |
+
inputs=[state],
|
| 141 |
+
outputs=[chatbot, state]
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
def create_language_info_tab(multilingual_manager): # NEW FUNCTION
|
| 145 |
+
"""Tab hiển thị thông tin về hệ thống đa ngôn ngữ"""
|
| 146 |
+
gr.Markdown("## 🌐 Thông tin Hệ thống Đa ngôn ngữ")
|
| 147 |
+
|
| 148 |
+
with gr.Row():
|
| 149 |
+
with gr.Column():
|
| 150 |
+
gr.Markdown("### 🔧 Cấu hình Model")
|
| 151 |
+
|
| 152 |
+
vietnamese_info = multilingual_manager.get_language_info('vi')
|
| 153 |
+
multilingual_info = multilingual_manager.get_language_info('en')
|
| 154 |
+
|
| 155 |
+
gr.Markdown(f"""
|
| 156 |
+
**Tiếng Việt:**
|
| 157 |
+
- Embedding Model: `{vietnamese_info['embedding_model']}`
|
| 158 |
+
- LLM Model: `{vietnamese_info['llm_model']}`
|
| 159 |
+
- Trạng thái: {vietnamese_info['status']}
|
| 160 |
+
|
| 161 |
+
**Đa ngôn ngữ:**
|
| 162 |
+
- Embedding Model: `{multilingual_info['embedding_model']}`
|
| 163 |
+
- LLM Model: `{multilingual_info['llm_model']}`
|
| 164 |
+
- Trạng thái: {multilingual_info['status']}
|
| 165 |
+
""")
|
| 166 |
+
|
| 167 |
+
with gr.Column():
|
| 168 |
+
gr.Markdown("### 🎯 Ngôn ngữ được hỗ trợ")
|
| 169 |
+
|
| 170 |
+
supported_languages = """
|
| 171 |
+
- 🇻🇳 **Tiếng Việt**: Sử dụng model chuyên biệt
|
| 172 |
+
- 🇺🇸 **English**: Sử dụng model đa ngôn ngữ
|
| 173 |
+
- 🇫🇷 **French**: Sử dụng model đa ngôn ngữ
|
| 174 |
+
- 🇪🇸 **Spanish**: Sử dụng model đa ngôn ngữ
|
| 175 |
+
- 🇩🇪 **German**: Sử dụng model đa ngôn ngữ
|
| 176 |
+
- 🇯🇵 **Japanese**: Sử dụng model đa ngôn ngữ
|
| 177 |
+
- 🇰🇷 **Korean**: Sử dụng model đa ngôn ngữ
|
| 178 |
+
- 🇨🇳 **Chinese**: Sử dụng model đa ngôn ngữ
|
| 179 |
+
"""
|
| 180 |
+
gr.Markdown(supported_languages)
|
| 181 |
+
|
| 182 |
+
with gr.Row():
|
| 183 |
+
with gr.Column():
|
| 184 |
+
gr.Markdown("### 🔍 Kiểm tra Ngôn ngữ")
|
| 185 |
+
test_text = gr.Textbox(
|
| 186 |
+
label="Nhập văn bản để kiểm tra ngôn ngữ",
|
| 187 |
+
placeholder="Nhập văn bản bằng bất kỳ ngôn ngữ nào..."
|
| 188 |
+
)
|
| 189 |
+
test_button = gr.Button("🔍 Kiểm tra", variant="primary")
|
| 190 |
+
|
| 191 |
+
test_result = gr.JSON(label="Kết quả phát hiện ngôn ngữ")
|
| 192 |
+
|
| 193 |
+
test_button.click(
|
| 194 |
+
lambda text: {
|
| 195 |
+
'detected_language': multilingual_manager.detect_language(text),
|
| 196 |
+
'language_info': multilingual_manager.get_language_info(multilingual_manager.detect_language(text)),
|
| 197 |
+
'embedding_model': multilingual_manager.get_embedding_model(multilingual_manager.detect_language(text)) is not None,
|
| 198 |
+
'llm_model': multilingual_manager.get_llm_model(multilingual_manager.detect_language(text))
|
| 199 |
+
},
|
| 200 |
+
inputs=[test_text],
|
| 201 |
+
outputs=[test_result]
|
| 202 |
+
)
|
| 203 |
+
def create_tts_tab(tts_service: EnhancedTTSService):
|
| 204 |
+
gr.Markdown("## 🎵 Chuyển văn bản thành giọng nói nâng cao")
|
| 205 |
+
gr.Markdown("Nhập văn bản và chọn ngôn ngữ để chuyển thành giọng nói")
|
| 206 |
+
|
| 207 |
+
with gr.Group():
|
| 208 |
+
with gr.Row():
|
| 209 |
+
tts_text_input = gr.Textbox(
|
| 210 |
+
label="Văn bản cần chuyển thành giọng nói",
|
| 211 |
+
lines=4,
|
| 212 |
+
placeholder="Nhập văn bản tại đây..."
|
| 213 |
+
)
|
| 214 |
+
with gr.Row():
|
| 215 |
+
tts_language = gr.Dropdown(
|
| 216 |
+
choices=["vi", "en", "fr", "es", "de", "ja", "ko", "zh"],
|
| 217 |
+
value="vi",
|
| 218 |
+
label="Ngôn ngữ"
|
| 219 |
+
)
|
| 220 |
+
tts_provider = gr.Dropdown(
|
| 221 |
+
choices=["auto", "gtts", "edgetts"],
|
| 222 |
+
value="auto",
|
| 223 |
+
label="Nhà cung cấp TTS"
|
| 224 |
+
)
|
| 225 |
+
with gr.Row():
|
| 226 |
+
tts_output_audio = gr.Audio(
|
| 227 |
+
label="Kết quả giọng nói",
|
| 228 |
+
interactive=False
|
| 229 |
+
)
|
| 230 |
+
tts_button = gr.Button("🔊 Chuyển thành giọng nói", variant="primary")
|
| 231 |
+
|
| 232 |
+
def text_to_speech_standalone(text, language, tts_provider):
|
| 233 |
+
if not text:
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
try:
|
| 237 |
+
tts_audio_bytes = tts_service.text_to_speech(text, language, tts_provider)
|
| 238 |
+
if tts_audio_bytes:
|
| 239 |
+
temp_audio_file = tts_service.save_audio_to_file(tts_audio_bytes)
|
| 240 |
+
return temp_audio_file
|
| 241 |
+
except Exception as e:
|
| 242 |
+
print(f"❌ Lỗi TTS: {e}")
|
| 243 |
+
|
| 244 |
+
return None
|
| 245 |
+
|
| 246 |
+
tts_button.click(
|
| 247 |
+
text_to_speech_standalone,
|
| 248 |
+
inputs=[tts_text_input, tts_language, tts_provider],
|
| 249 |
+
outputs=[tts_output_audio]
|
| 250 |
+
)
|
| 251 |
+
def create_streaming_voice_tab(streaming_service: StreamingVoiceService):
|
| 252 |
+
"""Tạo tab streaming voice với VAD"""
|
| 253 |
+
|
| 254 |
+
# Create components
|
| 255 |
+
(start_btn, stop_btn, status_display, state_display,
|
| 256 |
+
transcription, ai_response, tts_output, streaming_state,
|
| 257 |
+
conversation_history, vad_visualizer) = create_streaming_voice_components()
|
| 258 |
+
|
| 259 |
+
def start_streaming():
|
| 260 |
+
"""Bắt đầu streaming với VAD"""
|
| 261 |
+
def callback_handler(result):
|
| 262 |
+
"""Xử lý kết quả real-time"""
|
| 263 |
+
# Cập nhật UI với kết quả mới
|
| 264 |
+
# Note: Trong thực tế, cần sử dụng gr.update() và queue
|
| 265 |
+
print(f"🎯 Kết quả: {result['transcription']}")
|
| 266 |
+
print(f"🤖 Phản hồi: {result['response']}")
|
| 267 |
+
|
| 268 |
+
success = streaming_service.start_listening(callback_handler)
|
| 269 |
+
status = "✅ Đang lắng nghe..." if success else "❌ Lỗi khởi động"
|
| 270 |
+
|
| 271 |
+
# Start background thread for UI updates
|
| 272 |
+
if success:
|
| 273 |
+
threading.Thread(target=update_ui_loop, daemon=True).start()
|
| 274 |
+
|
| 275 |
+
return status, streaming_service.get_conversation_state()
|
| 276 |
+
|
| 277 |
+
def stop_streaming():
|
| 278 |
+
"""Dừng streaming"""
|
| 279 |
+
streaming_service.stop_listening()
|
| 280 |
+
return "🛑 Đã dừng lắng nghe", streaming_service.get_conversation_state()
|
| 281 |
+
|
| 282 |
+
def update_ui_loop():
|
| 283 |
+
"""Vòng lặp cập nhật UI real-time"""
|
| 284 |
+
while streaming_service.is_listening:
|
| 285 |
+
# Cập nhật trạng thái
|
| 286 |
+
state = streaming_service.get_conversation_state()
|
| 287 |
+
|
| 288 |
+
# Ở đây cần sử dụng gr.update() và queue để cập nhật UI
|
| 289 |
+
# Đây là phiên bản đơn giản, trong thực tế cần tích hợp với Gradio Queue
|
| 290 |
+
|
| 291 |
+
time.sleep(0.1)
|
| 292 |
+
|
| 293 |
+
# Event handlers
|
| 294 |
+
start_btn.click(
|
| 295 |
+
start_streaming,
|
| 296 |
+
outputs=[status_display, state_display]
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
stop_btn.click(
|
| 300 |
+
stop_streaming,
|
| 301 |
+
outputs=[status_display, state_display]
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Demo real-time updates (simplified)
|
| 305 |
+
def demo_update():
|
| 306 |
+
"""Demo cập nhật real-time"""
|
| 307 |
+
if streaming_service.is_listening:
|
| 308 |
+
state = streaming_service.get_conversation_state()
|
| 309 |
+
return (
|
| 310 |
+
state['current_transcription'] or "Đang lắng nghe...",
|
| 311 |
+
"Phản hồi sẽ xuất hiện ở đây...",
|
| 312 |
+
state
|
| 313 |
+
)
|
| 314 |
+
return "Chưa lắng nghe", "Chưa có phản hồi", {}
|
| 315 |
+
|
utils/helpers.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import io
|
| 3 |
+
import soundfile as sf
|
| 4 |
+
|
| 5 |
+
def numpy_to_mp3(audio_array: np.ndarray, sampling_rate: int = 24000) -> bytes:
|
| 6 |
+
"""Convert numpy array to MP3 bytes"""
|
| 7 |
+
buffer = io.BytesIO()
|
| 8 |
+
sf.write(buffer, audio_array, sampling_rate, format='mp3')
|
| 9 |
+
buffer.seek(0)
|
| 10 |
+
return buffer.read()
|
| 11 |
+
|
| 12 |
+
def validate_api_key(api_key: str) -> bool:
|
| 13 |
+
"""Validate Groq API key"""
|
| 14 |
+
return api_key is not None and len(api_key.strip()) > 0
|