Spaces:
Running
Running
| import os | |
| import time | |
| import tempfile | |
| import subprocess | |
| import threading | |
| import json | |
| import base64 | |
| import io | |
| import shutil | |
| import random | |
| import logging | |
| from queue import Queue | |
| from threading import Thread | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import gradio as gr | |
| import torch | |
| import soundfile as sf | |
| import librosa | |
| import requests | |
| from transformers import pipeline, AutoTokenizer, AutoModel | |
| from scipy import signal | |
| # Cấu hình logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| # Kiểm tra và tạo thư mục cho dữ liệu | |
| os.makedirs("data", exist_ok=True) | |
| os.makedirs("data/audio", exist_ok=True) | |
| os.makedirs("data/reports", exist_ok=True) | |
| os.makedirs("data/models", exist_ok=True) | |
| class AsyncProcessor: | |
| """Xử lý các tác vụ nặng trong thread riêng để không làm đơ giao diện""" | |
| def __init__(self): | |
| self.task_queue = Queue() | |
| self.result_queue = Queue() | |
| self.running = True | |
| self.worker_thread = Thread(target=self._worker) | |
| self.worker_thread.daemon = True | |
| self.worker_thread.start() | |
| def _worker(self): | |
| while self.running: | |
| if not self.task_queue.empty(): | |
| task_id, func, args, kwargs = self.task_queue.get() | |
| try: | |
| result = func(*args, **kwargs) | |
| self.result_queue.put((task_id, result, None)) | |
| except Exception as e: | |
| logger.error(f"Lỗi trong xử lý tác vụ {task_id}: {str(e)}") | |
| self.result_queue.put((task_id, None, str(e))) | |
| self.task_queue.task_done() | |
| time.sleep(0.1) | |
| def add_task(self, task_id, func, *args, **kwargs): | |
| self.task_queue.put((task_id, func, args, kwargs)) | |
| def get_result(self): | |
| if not self.result_queue.empty(): | |
| return self.result_queue.get() | |
| return None | |
| def stop(self): | |
| self.running = False | |
| if self.worker_thread.is_alive(): | |
| self.worker_thread.join(timeout=1) | |
| class VietSpeechTrainer: | |
| def __init__(self): | |
| # Cấu hình từ biến môi trường hoặc file cấu hình | |
| self.config = self._load_config() | |
| # Khởi tạo bộ xử lý bất đồng bộ | |
| self.async_processor = AsyncProcessor() | |
| # Lưu trữ lịch sử | |
| self.session_history = [] | |
| self.current_session_id = int(time.time()) | |
| # Trạng thái hội thoại | |
| self.current_scenario = None | |
| self.current_prompt_index = 0 | |
| # Khởi tạo các mô hình | |
| logger.info("Đang tải các mô hình...") | |
| self._initialize_models() | |
| def _load_config(self): | |
| """Tải cấu hình từ file hoặc biến môi trường""" | |
| config = { | |
| # STT config | |
| "stt_model": os.environ.get("STT_MODEL", "nguyenvulebinh/wav2vec2-base-vietnamese-250h"), | |
| "use_phowhisper": os.environ.get("USE_PHOWHISPER", "false").lower() == "true", | |
| # NLP config | |
| "use_phobert": os.environ.get("USE_PHOBERT", "false").lower() == "true", | |
| "use_vncorenlp": os.environ.get("USE_VNCORENLP", "false").lower() == "true", | |
| # LLM config | |
| "llm_provider": os.environ.get("LLM_PROVIDER", "none"), # "openai", "gemini", "local", "none" | |
| "openai_api_key": os.environ.get("OPENAI_API_KEY", ""), | |
| "gemini_api_key": os.environ.get("GEMINI_API_KEY", ""), | |
| "local_llm_endpoint": os.environ.get("LOCAL_LLM_ENDPOINT", "http://localhost:8080/v1"), | |
| # TTS config | |
| "use_viettts": os.environ.get("USE_VIETTTS", "false").lower() == "true", | |
| "tts_api_url": os.environ.get("TTS_API_URL", ""), | |
| # Application settings | |
| "default_dialect": os.environ.get("DEFAULT_DIALECT", "Bắc"), | |
| "enable_pronunciation_eval": os.environ.get("ENABLE_PRONUNCIATION_EVAL", "false").lower() == "true", | |
| # Advanced settings | |
| "preprocess_audio": os.environ.get("PREPROCESS_AUDIO", "true").lower() == "true", | |
| "save_history": os.environ.get("SAVE_HISTORY", "true").lower() == "true", | |
| } | |
| # Nếu tồn tại file cấu hình, đọc thêm từ đó | |
| if os.path.exists("config.json"): | |
| try: | |
| with open("config.json", "r", encoding="utf-8") as f: | |
| file_config = json.load(f) | |
| config.update(file_config) | |
| except Exception as e: | |
| logger.error(f"Lỗi khi đọc file cấu hình: {e}") | |
| return config | |
| def _initialize_models(self): | |
| """Khởi tạo các mô hình AI cần thiết""" | |
| try: | |
| # 1. Khởi tạo mô hình STT | |
| if self.config["use_phowhisper"]: | |
| logger.info("Đang tải PhoWhisper...") | |
| self.stt_model = pipeline( | |
| "automatic-speech-recognition", | |
| model="vinai/PhoWhisper-small", | |
| device=0 if torch.cuda.is_available() else -1, | |
| ) | |
| else: | |
| logger.info(f"Đang tải mô hình STT: {self.config['stt_model']}") | |
| self.stt_model = pipeline( | |
| "automatic-speech-recognition", | |
| model=self.config["stt_model"], | |
| device=0 if torch.cuda.is_available() else -1, | |
| ) | |
| # 2. Khởi tạo PhoBERT và VnCoreNLP nếu được cấu hình | |
| self.phobert_model = None | |
| self.phobert_tokenizer = None | |
| self.rdrsegmenter = None | |
| if self.config["use_phobert"]: | |
| logger.info("Đang tải PhoBERT...") | |
| try: | |
| self.phobert_tokenizer = AutoTokenizer.from_pretrained("vinai/phobert-base") | |
| self.phobert_model = AutoModel.from_pretrained("vinai/phobert-base") | |
| except Exception as e: | |
| logger.error(f"Lỗi khi tải PhoBERT: {e}") | |
| self.config["use_phobert"] = False | |
| if self.config["use_vncorenlp"]: | |
| logger.info("Đang chuẩn bị VnCoreNLP...") | |
| try: | |
| vncorenlp_path = self._setup_vncorenlp() | |
| from py_vncorenlp import VnCoreNLP | |
| self.rdrsegmenter = VnCoreNLP(vncorenlp_path, annotators="wseg", max_heap_size="-Xmx500m") | |
| except Exception as e: | |
| logger.error(f"Lỗi khi chuẩn bị VnCoreNLP: {e}") | |
| self.config["use_vncorenlp"] = False | |
| # 3. Chuẩn bị VietTTS nếu được cấu hình | |
| self.viettts_ready = False | |
| if self.config["use_viettts"]: | |
| logger.info("Đang chuẩn bị VietTTS...") | |
| try: | |
| self.viettts_ready = self._setup_viettts() | |
| except Exception as e: | |
| logger.error(f"Lỗi khi chuẩn bị VietTTS: {e}") | |
| self.config["use_viettts"] = False | |
| logger.info("Khởi tạo mô hình hoàn tất") | |
| except Exception as e: | |
| logger.error(f"Lỗi khi khởi tạo mô hình: {e}") | |
| raise | |
| def _setup_vncorenlp(self): | |
| """Tải và cài đặt VnCoreNLP""" | |
| vncorenlp_dir = "data/models/vncorenlp" | |
| vncorenlp_jar = f"{vncorenlp_dir}/VnCoreNLP-1.1.1.jar" | |
| os.makedirs(vncorenlp_dir, exist_ok=True) | |
| if not os.path.exists(vncorenlp_jar): | |
| logger.info("Đang tải VnCoreNLP...") | |
| # Tải jar file | |
| url = "https://raw.githubusercontent.com/vncorenlp/VnCoreNLP/master/VnCoreNLP-1.1.1.jar" | |
| response = requests.get(url) | |
| with open(vncorenlp_jar, "wb") as f: | |
| f.write(response.content) | |
| # Tạo thư mục models | |
| os.makedirs(f"{vncorenlp_dir}/models/wordsegmenter", exist_ok=True) | |
| # Tải models | |
| for model_file in ["vi-vocab", "wordsegmenter.rdr"]: | |
| url = f"https://raw.githubusercontent.com/vncorenlp/VnCoreNLP/master/models/wordsegmenter/{model_file}" | |
| response = requests.get(url) | |
| with open(f"{vncorenlp_dir}/models/wordsegmenter/{model_file}", "wb") as f: | |
| f.write(response.content) | |
| return vncorenlp_jar | |
| def _setup_viettts(self): | |
| """Cài đặt và chuẩn bị VietTTS""" | |
| viettts_dir = "data/models/viettts" | |
| # Nếu đã tải VietTTS rồi | |
| if os.path.exists(f"{viettts_dir}/pretrained"): | |
| return True | |
| # Clone repo nếu chưa có | |
| os.makedirs(viettts_dir, exist_ok=True) | |
| if not os.path.exists(f"{viettts_dir}/.git"): | |
| logger.info("Đang clone VietTTS repository...") | |
| result = subprocess.run( | |
| ["git", "clone", "https://github.com/NTT123/vietTTS.git", viettts_dir], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if result.returncode != 0: | |
| logger.error(f"Lỗi khi clone VietTTS: {result.stderr}") | |
| return False | |
| # Cài đặt VietTTS | |
| logger.info("Đang cài đặt VietTTS...") | |
| os.chdir(viettts_dir) | |
| result = subprocess.run(["pip", "install", "-e", "."], capture_output=True, text=True) | |
| if result.returncode != 0: | |
| logger.error(f"Lỗi khi cài đặt VietTTS: {result.stderr}") | |
| os.chdir("..") | |
| return False | |
| # Tải mô hình pretrained | |
| if not os.path.exists("pretrained"): | |
| logger.info("Đang tải mô hình pretrained...") | |
| result = subprocess.run(["bash", "scripts/quick_start.sh"], capture_output=True, text=True) | |
| if result.returncode != 0: | |
| logger.error(f"Lỗi khi tải mô hình pretrained: {result.stderr}") | |
| os.chdir("..") | |
| return False | |
| os.chdir("..") | |
| return True | |
| def preprocess_audio(self, audio_path): | |
| """Tiền xử lý âm thanh để cải thiện chất lượng""" | |
| if not self.config["preprocess_audio"]: | |
| return audio_path | |
| try: | |
| # Đọc âm thanh | |
| y, sr = librosa.load(audio_path, sr=16000) | |
| # Chuẩn hóa âm lượng | |
| y_normalized = librosa.util.normalize(y) | |
| # Xử lý nhiễu (đơn giản) | |
| y_filtered = self._simple_noise_reduction(y_normalized) | |
| # Lưu file mới | |
| processed_path = audio_path.replace(".wav", "_processed.wav") | |
| sf.write(processed_path, y_filtered, sr) | |
| return processed_path | |
| except Exception as e: | |
| logger.error(f"Lỗi khi tiền xử lý âm thanh: {e}") | |
| return audio_path | |
| def _simple_noise_reduction(self, y): | |
| """Áp dụng lọc nhiễu đơn giản""" | |
| # Áp dụng high-pass filter để giảm nhiễu tần số thấp | |
| b, a = signal.butter(5, 80 / (16000 / 2), "highpass") | |
| y_filtered = signal.filtfilt(b, a, y) | |
| return y_filtered | |
| def transcribe_audio(self, audio_path): | |
| """Chuyển đổi âm thanh thành văn bản""" | |
| try: | |
| # Tiền xử lý audio nếu cần | |
| if self.config["preprocess_audio"]: | |
| audio_path = self.preprocess_audio(audio_path) | |
| # Thực hiện nhận dạng giọng nói | |
| result = self.stt_model(audio_path) | |
| # Kết quả có thể có cấu trúc khác nhau tùy mô hình | |
| if isinstance(result, dict) and "text" in result: | |
| text = result["text"] | |
| elif isinstance(result, list): | |
| text = " ".join([chunk.get("text", "") for chunk in result]) | |
| else: | |
| text = str(result) | |
| return text | |
| except Exception as e: | |
| logger.error(f"Lỗi khi chuyển đổi âm thanh: {e}") | |
| return f"Lỗi: {str(e)}" | |
| def segment_text(self, text): | |
| """Tách từ văn bản tiếng Việt""" | |
| if not text or not text.strip(): | |
| return text | |
| # Nếu có VnCoreNLP, sử dụng RDRSegmenter | |
| if self.config["use_vncorenlp"] and self.rdrsegmenter: | |
| try: | |
| sentences = self.rdrsegmenter.tokenize(text) | |
| segmented_text = " ".join([" ".join(sentence) for sentence in sentences]) | |
| return segmented_text | |
| except Exception as e: | |
| logger.error(f"Lỗi khi tách từ với VnCoreNLP: {e}") | |
| # Nếu không có VnCoreNLP hoặc lỗi, trả về nguyên bản | |
| return text | |
| def analyze_text(self, transcript, dialect="Bắc"): | |
| """Phân tích văn bản và đưa ra gợi ý cải thiện""" | |
| if not transcript or not transcript.strip(): | |
| return "Không nhận được văn bản để phân tích." | |
| # Tách từ | |
| segmented_text = self.segment_text(transcript) | |
| # Phân tích với LLM nếu có cấu hình | |
| llm_provider = self.config["llm_provider"] | |
| if llm_provider == "openai" and self.config["openai_api_key"]: | |
| return self._analyze_with_openai(transcript, segmented_text, dialect) | |
| elif llm_provider == "gemini" and self.config["gemini_api_key"]: | |
| return self._analyze_with_gemini(transcript, segmented_text, dialect) | |
| elif llm_provider == "local" and self.config["local_llm_endpoint"]: | |
| return self._analyze_with_local_llm(transcript, segmented_text, dialect) | |
| else: | |
| # Sử dụng phân tích dựa trên quy tắc | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| def _analyze_with_openai(self, transcript, segmented_text, dialect): | |
| """Phân tích văn bản sử dụng OpenAI API""" | |
| try: | |
| headers = { | |
| "Authorization": f"Bearer {self.config['openai_api_key']}", | |
| "Content-Type": "application/json", | |
| } | |
| # Tạo prompt | |
| prompt = self._create_analysis_prompt(transcript, segmented_text, dialect) | |
| # Gọi API | |
| response = requests.post( | |
| "https://api.openai.com/v1/chat/completions", | |
| headers=headers, | |
| json={ | |
| "model": "gpt-3.5-turbo", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "Bạn là trợ lý dạy tiếng Việt, chuyên phân tích và đưa ra gợi ý cải thiện kỹ năng nói.", | |
| }, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| "temperature": 0.5, | |
| "max_tokens": 800, | |
| }, | |
| ) | |
| if response.status_code == 200: | |
| result = response.json() | |
| analysis = result["choices"][0]["message"]["content"] | |
| return analysis | |
| else: | |
| logger.error(f"Lỗi khi gọi OpenAI API: {response.text}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| except Exception as e: | |
| logger.error(f"Lỗi khi phân tích với OpenAI: {e}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| def _analyze_with_gemini(self, transcript, segmented_text, dialect): | |
| """Phân tích văn bản sử dụng Gemini API""" | |
| try: | |
| headers = { | |
| "Content-Type": "application/json", | |
| } | |
| # Tạo prompt | |
| prompt = self._create_analysis_prompt(transcript, segmented_text, dialect) | |
| # Endpoint Gemini | |
| url = ( | |
| f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:generateContent?key={self.config['gemini_api_key']}" | |
| ) | |
| # Gọi API | |
| response = requests.post( | |
| url, | |
| headers=headers, | |
| json={ | |
| "contents": [ | |
| { | |
| "role": "user", | |
| "parts": [{"text": prompt}], | |
| } | |
| ], | |
| "generationConfig": { | |
| "temperature": 0.4, | |
| "maxOutputTokens": 800, | |
| }, | |
| }, | |
| ) | |
| if response.status_code == 200: | |
| result = response.json() | |
| if "candidates" in result and len(result["candidates"]) > 0: | |
| analysis = result["candidates"][0]["content"]["parts"][0]["text"] | |
| return analysis | |
| else: | |
| logger.error(f"Định dạng phản hồi Gemini không như mong đợi: {result}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| else: | |
| logger.error(f"Lỗi khi gọi Gemini API: {response.text}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| except Exception as e: | |
| logger.error(f"Lỗi khi phân tích với Gemini: {e}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| def _analyze_with_local_llm(self, transcript, segmented_text, dialect): | |
| """Phân tích văn bản sử dụng LLM mã nguồn mở local""" | |
| try: | |
| headers = { | |
| "Content-Type": "application/json", | |
| } | |
| # Tạo prompt | |
| prompt = self._create_analysis_prompt(transcript, segmented_text, dialect) | |
| # Endpoint local LLM | |
| url = f"{self.config['local_llm_endpoint']}/chat/completions" | |
| # Gọi API | |
| response = requests.post( | |
| url, | |
| headers=headers, | |
| json={ | |
| "model": "local-model", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "Bạn là trợ lý dạy tiếng Việt, chuyên phân tích và đưa ra gợi ý cải thiện kỹ năng nói.", | |
| }, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| "temperature": 0.5, | |
| "max_tokens": 800, | |
| }, | |
| ) | |
| if response.status_code == 200: | |
| result = response.json() | |
| analysis = result["choices"][0]["message"]["content"] | |
| return analysis | |
| else: | |
| logger.error(f"Lỗi khi gọi Local LLM API: {response.text}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| except Exception as e: | |
| logger.error(f"Lỗi khi phân tích với Local LLM: {e}") | |
| return self._rule_based_analysis(transcript, segmented_text, dialect) | |
| def _create_analysis_prompt(self, transcript, segmented_text, dialect): | |
| """Tạo prompt cho việc phân tích văn bản""" | |
| return f"""Bạn là trợ lý dạy tiếng Việt. Hãy phân tích câu nói sau và đưa ra gợi ý cải thiện: | |
| Câu nói: "{transcript}" | |
| Câu đã tách từ: "{segmented_text}" | |
| Phương ngữ: {dialect} | |
| Hãy phân tích theo các khía cạnh sau: | |
| 1. Ngữ pháp: Cấu trúc câu, thì, cách sử dụng từ nối | |
| 2. Từ vựng: Từ không phù hợp, từ dùng không đúng ngữ cảnh, từ viết tắt | |
| 3. Phong cách: Mức độ trang trọng, thân mật, văn phong | |
| 4. Tính mạch lạc: Tính rõ ràng, dễ hiểu của câu | |
| Đưa ra gợi ý cụ thể để cải thiện cách diễn đạt. | |
| Viết câu mẫu cải thiện. | |
| Định dạng phản hồi: | |
| - Sử dụng Markdown | |
| - Đặt các vấn đề vào danh sách có đánh dấu | |
| - Đưa ra câu mẫu cải thiện ở cuối""" | |
| def _rule_based_analysis(self, transcript, segmented_text, dialect): | |
| """Phân tích dựa trên quy tắc đơn giản""" | |
| # Phân tích cơ bản khi không có LLM | |
| words = transcript.split() | |
| analysis = [] | |
| # 1. Phân tích độ dài câu | |
| if len(words) < 3: | |
| analysis.append("⚠️ **Câu quá ngắn**: Thử mở rộng ý với các chi tiết hơn.") | |
| elif len(words) > 20: | |
| analysis.append("⚠️ **Câu dài**: Cân nhắc chia thành các câu ngắn hơn.") | |
| else: | |
| analysis.append("✅ **Độ dài câu**: Phù hợp.") | |
| # 2. Kiểm tra từ ngữ phổ biến | |
| common_errors = { | |
| "ko": "không", | |
| "k": "không", | |
| "bik": "biết", | |
| "j": "gì", | |
| "z": "vậy", | |
| "ntn": "như thế nào", | |
| "dc": "được", | |
| "vs": "với", | |
| "nc": "nước", | |
| "ng": "người", | |
| "trc": "trước", | |
| "sao": "sao", | |
| } | |
| errors_found = [] | |
| for word in words: | |
| word_lower = word.lower() | |
| if word_lower in common_errors: | |
| errors_found.append(f"'{word}' → '{common_errors[word_lower]}'") | |
| if errors_found: | |
| analysis.append(f"⚠️ **Từ viết tắt**: Nên dùng từ đầy đủ thay vì: {', '.join(errors_found)}") | |
| else: | |
| analysis.append("✅ **Sử dụng từ**: Không phát hiện từ viết tắt phổ biến.") | |
| # 3. Tính trùng lặp | |
| word_counts = {} | |
| for word in words: | |
| word_lower = word.lower() | |
| if len(word_lower) > 1: # Bỏ qua các từ ngắn | |
| word_counts[word_lower] = word_counts.get(word_lower, 0) + 1 | |
| duplicates = [w for w, c in word_counts.items() if c > 2] | |
| if duplicates: | |
| analysis.append( | |
| f"⚠️ **Trùng lặp từ**: Từ '{', '.join(duplicates)}' lặp lại nhiều lần. Hãy thử dùng từ đồng nghĩa." | |
| ) | |
| # 4. Gợi ý cải thiện phụ thuộc phương ngữ | |
| if dialect == "Bắc": | |
| suggestions = [ | |
| "Phát âm rõ ràng phụ âm cuối, tránh nuốt âm", | |
| "Chú ý tới thanh điệu, đặc biệt là thanh hỏi và thanh ngã", | |
| "Phát âm 'r' và 'gi' phân biệt theo phong cách Bắc Bộ", | |
| ] | |
| elif dialect == "Trung": | |
| suggestions = [ | |
| "Chú ý đến nhịp điệu đặc trưng của giọng Trung", | |
| "Phát âm rõ phụ âm đầu, đặc biệt là 'tr' và 'ch'", | |
| "Kéo dài nguyên âm một cách tự nhiên", | |
| ] | |
| else: # Nam | |
| suggestions = [ | |
| "Giữ nguyên âm ổn định, tránh biến đổi nguyên âm", | |
| "Phân biệt rõ 'v' và 'gi' theo phong cách Nam Bộ", | |
| "Tránh nhấn quá mạnh vào các phụ âm cuối", | |
| ] | |
| # 5. Câu mẫu cải thiện | |
| improved = transcript | |
| for word, replacement in common_errors.items(): | |
| improved = improved.replace(f" {word} ", f" {replacement} ") | |
| # Ghép tất cả phân tích lại | |
| full_analysis = "### Phân tích\n\n" + "\n\n".join(analysis) | |
| full_analysis += "\n\n### Gợi ý cải thiện\n\n" + "\n".join([f"- {s}" for s in suggestions]) | |
| full_analysis += f"\n\n### Câu gợi ý\n\n{improved}" | |
| return full_analysis | |
| def text_to_speech(self, text, dialect="Bắc"): | |
| """Chuyển văn bản thành giọng nói""" | |
| # Nếu có API TTS | |
| if self.config["tts_api_url"]: | |
| try: | |
| # Gọi API TTS | |
| response = requests.post( | |
| self.config["tts_api_url"], json={"text": text, "dialect": dialect.lower()} | |
| ) | |
| if response.status_code == 200: | |
| # Lưu audio vào file tạm | |
| output_file = f"data/audio/tts_{int(time.time())}.wav" | |
| with open(output_file, "wb") as f: | |
| f.write(response.content) | |
| return output_file | |
| else: | |
| logger.error(f"Lỗi khi gọi API TTS: {response.text}") | |
| return None | |
| except Exception as e: | |
| logger.error(f"Lỗi khi gọi API TTS: {e}") | |
| return None | |
| # Nếu có VietTTS | |
| elif self.config["use_viettts"] and self.viettts_ready: | |
| try: | |
| # Chuẩn bị VietTTS | |
| viettts_dir = "data/models/viettts" | |
| # Tạo file tạm thời để lưu văn bản | |
| with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt", encoding="utf-8") as f: | |
| f.write(text) | |
| text_file = f.name | |
| # Tạo tên file output | |
| output_file = f"data/audio/tts_{int(time.time())}.wav" | |
| # Lưu thư mục hiện tại | |
| current_dir = os.getcwd() | |
| try: | |
| # Đổi thư mục làm việc sang viettts_dir | |
| os.chdir(viettts_dir) | |
| # Gọi VietTTS để tạo giọng nói | |
| cmd = [ | |
| "python", | |
| "-m", | |
| "vietTTS.synthesizer", | |
| "--lexicon-file=./train_data/lexicon.txt", | |
| f"--text-file={text_file}", | |
| f"--output={os.path.join(current_dir, output_file)}", | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| # Quay lại thư mục ban đầu | |
| os.chdir(current_dir) | |
| if result.returncode != 0: | |
| logger.error(f"Lỗi khi chạy VietTTS: {result.stderr}") | |
| return None | |
| # Xóa file tạm | |
| os.unlink(text_file) | |
| return output_file | |
| except Exception as e: | |
| # Đảm bảo quay lại thư mục ban đầu | |
| os.chdir(current_dir) | |
| logger.error(f"Lỗi khi sử dụng VietTTS: {e}") | |
| os.unlink(text_file) | |
| return None | |
| except Exception as e: | |
| logger.error(f"Lỗi khi tạo file tạm: {e}") | |
| return None | |
| return None | |
| def process_recording(self, audio_path, dialect="Bắc"): | |
| """Xử lý bản ghi âm: chuyển sang văn bản và phân tích""" | |
| if audio_path is None: | |
| return "Không có âm thanh được ghi.", "", None | |
| # 1. Chuyển đổi âm thanh thành văn bản | |
| transcript = self.transcribe_audio(audio_path) | |
| # 2. Phân tích văn bản | |
| analysis = self.analyze_text(transcript, dialect) | |
| # 3. Tạo mẫu phát âm (nếu có) | |
| sample_audio = self.text_to_speech(transcript, dialect) | |
| # 4. Lưu vào lịch sử phiên | |
| entry = { | |
| "id": len(self.session_history) + 1, | |
| "time": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| "transcript": transcript, | |
| "analysis": analysis, | |
| "audio_path": audio_path, | |
| "sample_audio": sample_audio, | |
| "dialect": dialect, | |
| } | |
| self.session_history.append(entry) | |
| # 5. Lưu lịch sử nếu được cấu hình | |
| if self.config["save_history"]: | |
| self._save_session_history() | |
| return transcript, analysis, sample_audio | |
| def evaluate_pronunciation(self, original_audio, text, dialect="Bắc"): | |
| """Đánh giá chất lượng phát âm bằng cách so sánh với mẫu chuẩn""" | |
| if not self.config["enable_pronunciation_eval"]: | |
| return {"score": 0, "feedback": "Tính năng đánh giá phát âm không được bật"} | |
| try: | |
| # 1. Tạo phát âm mẫu từ text | |
| sample_audio = self.text_to_speech(text, dialect) | |
| if not sample_audio: | |
| return {"score": 0, "feedback": "Không thể tạo mẫu phát âm chuẩn"} | |
| # 2. Trích xuất đặc trưng từ cả hai file âm thanh | |
| # Trích xuất MFCCs (Mel-frequency cepstral coefficients) | |
| def extract_mfcc(audio_file): | |
| y, sr = librosa.load(audio_file, sr=16000) | |
| mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13) | |
| return mfccs | |
| original_mfccs = extract_mfcc(original_audio) | |
| sample_mfccs = extract_mfcc(sample_audio) | |
| # 3. So sánh bằng DTW (Dynamic Time Warping) | |
| # Đơn giản hóa: tính khoảng cách Euclidean giữa hai vector MFCC | |
| # Trong thực tế, nên dùng DTW hoặc thuật toán phức tạp hơn | |
| def dtw_distance(mfcc1, mfcc2): | |
| # Chỉ lấy một phần của các frames để so sánh | |
| min_len = min(mfcc1.shape[1], mfcc2.shape[1]) | |
| dist = np.linalg.norm(mfcc1[:, :min_len] - mfcc2[:, :min_len]) | |
| return dist | |
| distance = dtw_distance(original_mfccs, sample_mfccs) | |
| # 4. Tính điểm dựa trên khoảng cách | |
| max_distance = 100 # Giá trị tối đa để chuẩn hóa | |
| normalized_distance = min(distance, max_distance) / max_distance | |
| pronunciation_score = 100 * (1 - normalized_distance) | |
| # 5. Phản hồi | |
| feedback = self._get_pronunciation_feedback(pronunciation_score, dialect) | |
| evaluation = { | |
| "score": round(pronunciation_score, 2), | |
| "sample_audio": sample_audio, | |
| "feedback": feedback, | |
| } | |
| return evaluation | |
| except Exception as e: | |
| logger.error(f"Lỗi khi đánh giá phát âm: {e}") | |
| return {"score": 0, "feedback": f"Lỗi khi đánh giá: {str(e)}"} | |
| def _get_pronunciation_feedback(self, score, dialect): | |
| """Đưa ra phản hồi dựa trên điểm phát âm""" | |
| prefix = f"**Phương ngữ {dialect}**: " | |
| if score >= 90: | |
| return prefix + "Phát âm rất tốt! Gần như giống với mẫu chuẩn." | |
| elif score >= 80: | |
| return prefix + "Phát âm tốt. Có một vài điểm nhỏ cần cải thiện." | |
| elif score >= 70: | |
| return prefix + "Phát âm khá tốt. Hãy chú ý đến ngữ điệu và các phụ âm cuối." | |
| elif score >= 60: | |
| return prefix + "Phát âm trung bình. Cần luyện tập thêm về nhịp điệu và độ rõ ràng." | |
| else: | |
| return prefix + "Cần luyện tập nhiều hơn. Hãy tập trung vào từng âm tiết và chú ý các dấu." | |
| def _save_session_history(self): | |
| """Lưu lịch sử phiên hiện tại vào file""" | |
| try: | |
| history_file = f"data/reports/session_{self.current_session_id}.json" | |
| # Chuyển đổi thành JSON serializable | |
| serializable_history = [] | |
| for entry in self.session_history: | |
| # Tạo bản sao để không thay đổi bản gốc | |
| entry_copy = entry.copy() | |
| # Chỉ lưu đường dẫn, không lưu nội dung file | |
| if "audio_path" in entry_copy and entry_copy["audio_path"]: | |
| entry_copy["audio_path"] = os.path.basename(entry_copy["audio_path"]) | |
| if "sample_audio" in entry_copy and entry_copy["sample_audio"]: | |
| entry_copy["sample_audio"] = os.path.basename(entry_copy["sample_audio"]) | |
| serializable_history.append(entry_copy) | |
| with open(history_file, "w", encoding="utf-8") as f: | |
| json.dump( | |
| { | |
| "session_id": self.current_session_id, | |
| "start_time": time.strftime( | |
| "%Y-%m-%d %H:%M:%S", time.localtime(self.current_session_id) | |
| ), | |
| "entries": serializable_history, | |
| }, | |
| f, | |
| ensure_ascii=False, | |
| indent=2, | |
| ) | |
| except Exception as e: | |
| logger.error(f"Lỗi khi lưu lịch sử phiên: {e}") | |
| def export_session(self, format="markdown"): | |
| """Xuất báo cáo buổi luyện tập""" | |
| if not self.session_history: | |
| return None | |
| try: | |
| if format == "markdown": | |
| return self._export_markdown() | |
| elif format == "html": | |
| return self._export_html() | |
| else: | |
| return self._export_markdown() # Mặc định là markdown | |
| except Exception as e: | |
| logger.error(f"Lỗi khi xuất báo cáo: {e}") | |
| return None | |
| def _export_markdown(self): | |
| """Xuất báo cáo dạng Markdown""" | |
| # Tạo nội dung báo cáo | |
| content = "# BÁO CÁO LUYỆN NÓI TIẾNG VIỆT\n\n" | |
| content += f"Ngày: {time.strftime('%Y-%m-%d')}\n" | |
| content += f"Tổng số câu: {len(self.session_history)}\n\n" | |
| for entry in self.session_history: | |
| content += f"## Câu {entry['id']} ({entry['time']})\n\n" | |
| content += f"**Phương ngữ:** {entry['dialect']}\n\n" | |
| content += f"**Bạn nói:** {entry['transcript']}\n\n" | |
| content += f"**Phân tích:**\n{entry['analysis']}\n\n" | |
| content += "---\n\n" | |
| # Thêm thống kê tổng quát | |
| content += "## Thống kê tổng quát\n\n" | |
| # Tính số từ trung bình mỗi câu | |
| avg_words = sum(len(entry["transcript"].split()) for entry in self.session_history) / len( | |
| self.session_history | |
| ) | |
| content += f"- Số từ trung bình mỗi câu: {avg_words:.2f}\n" | |
| # Lưu báo cáo | |
| filename = f"data/reports/bao_cao_{time.strftime('%Y%m%d_%H%M%S')}.md" | |
| with open(filename, "w", encoding="utf-8") as f: | |
| f.write(content) | |
| return filename | |
| def _export_html(self): | |
| """Xuất báo cáo dạng HTML""" | |
| # Tạo nội dung HTML | |
| html = """<!DOCTYPE html> | |
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Báo cáo luyện nói tiếng Việt</title> | |
| <style> | |
| body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; } | |
| h1, h2 { color: #2c3e50; } | |
| .entry { margin-bottom: 30px; border-bottom: 1px solid #eee; padding-bottom: 20px; } | |
| .transcript { background-color: #f8f9fa; padding: 10px; border-left: 4px solid #4CAF50; } | |
| .analysis { margin-top: 10px; } | |
| .meta { color: #7f8c8d; font-size: 0.9em; } | |
| .dialect { display: inline-block; background-color: #e74c3c; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Báo cáo luyện nói tiếng Việt</h1> | |
| <p>Ngày: %s</p> | |
| <p>Tổng số câu: %d</p> | |
| <div class="entries"> | |
| """ % ( | |
| time.strftime("%Y-%m-%d"), | |
| len(self.session_history), | |
| ) | |
| for entry in self.session_history: | |
| html += f""" | |
| <div class="entry"> | |
| <h2>Câu {entry['id']}</h2> | |
| <div class="meta">Thời gian: {entry['time']} | <span class="dialect">{entry['dialect']}</span></div> | |
| <div class="transcript">{entry['transcript']}</div> | |
| <div class="analysis">{entry['analysis']}</div> | |
| </div> | |
| """ | |
| # Thêm thống kê | |
| avg_words = sum(len(entry["transcript"].split()) for entry in self.session_history) / len( | |
| self.session_history | |
| ) | |
| html += f""" | |
| </div> | |
| <h2>Thống kê tổng quát</h2> | |
| <ul> | |
| <li>Số từ trung bình mỗi câu: {avg_words:.2f}</li> | |
| </ul> | |
| </body> | |
| </html> | |
| """ | |
| # Lưu báo cáo | |
| filename = f"data/reports/bao_cao_{time.strftime('%Y%m%d_%H%M%S')}.html" | |
| with open(filename, "w", encoding="utf-8") as f: | |
| f.write(html) | |
| return filename | |
| def create_conversation_scenario(self): | |
| """Tạo một tình huống hội thoại thực tế cho người dùng luyện tập""" | |
| # Danh sách các tình huống | |
| scenarios = [ | |
| { | |
| "title": "Chào hỏi và giới thiệu bản thân", | |
| "description": "Bạn gặp một người mới tại một sự kiện networking.", | |
| "prompts": [ | |
| "Chào bạn, mình là người tổ chức sự kiện. Bạn tên gì và đang làm việc ở đâu?", | |
| "Bạn có thể chia sẻ một chút về công việc của mình được không?", | |
| "Bạn quan tâm đến lĩnh vực nào trong sự kiện này?", | |
| ], | |
| }, | |
| { | |
| "title": "Đặt món tại nhà hàng", | |
| "description": "Bạn đang ở một nhà hàng và muốn gọi món.", | |
| "prompts": [ | |
| "Xin chào, tôi có thể giúp gì cho bạn?", | |
| "Bạn muốn đặt món gì? Hôm nay chúng tôi có món đặc biệt là cá hồi nướng.", | |
| "Bạn muốn uống thêm gì không? Chúng tôi có nhiều loại nước và rượu vang.", | |
| ], | |
| }, | |
| { | |
| "title": "Phỏng vấn công việc", | |
| "description": "Bạn đang trong một cuộc phỏng vấn xin việc.", | |
| "prompts": [ | |
| "Chào bạn, bạn có thể giới thiệu ngắn gọn về bản thân được không?", | |
| "Tại sao bạn muốn làm việc tại công ty chúng tôi?", | |
| "Bạn có kinh nghiệm gì liên quan đến vị trí này không?", | |
| ], | |
| }, | |
| { | |
| "title": "Thuyết trình ý tưởng", | |
| "description": "Bạn đang thuyết trình một ý tưởng mới cho đồng nghiệp.", | |
| "prompts": [ | |
| "Hãy giới thiệu về ý tưởng của bạn một cách ngắn gọn.", | |
| "Ý tưởng này giải quyết vấn đề gì và đối tượng hướng đến là ai?", | |
| "Bạn cần những nguồn lực gì để thực hiện ý tưởng này?", | |
| ], | |
| }, | |
| { | |
| "title": "Hỏi đường", | |
| "description": "Bạn đang du lịch và cần hỏi đường đến một địa điểm.", | |
| "prompts": [ | |
| "Xin chào, tôi có thể giúp gì cho bạn?", | |
| "Bạn đang tìm đường đến đâu?", | |
| "Bạn muốn đi bằng phương tiện gì? Đi bộ, xe buýt hay taxi?", | |
| ], | |
| }, | |
| ] | |
| # Chọn ngẫu nhiên một tình huống | |
| scenario = random.choice(scenarios) | |
| return scenario | |
| def track_progress(self): | |
| """Theo dõi tiến độ của người dùng qua thời gian""" | |
| if not self.session_history: | |
| return { | |
| "message": "Chưa có dữ liệu để theo dõi tiến độ", | |
| "statistics": {}, | |
| "charts": {}, | |
| } | |
| # Tính toán các chỉ số tiến triển | |
| total_entries = len(self.session_history) | |
| # Phân tích độ dài câu qua thời gian | |
| sentence_lengths = [len(entry["transcript"].split()) for entry in self.session_history] | |
| avg_length = sum(sentence_lengths) / total_entries | |
| # Tính số từ độc đáo sử dụng | |
| all_words = [] | |
| for entry in self.session_history: | |
| all_words.extend(entry["transcript"].lower().split()) | |
| unique_words = set(all_words) | |
| vocabulary_size = len(unique_words) | |
| # Tạo báo cáo tiến độ | |
| progress_report = { | |
| "message": "Dữ liệu theo dõi tiến độ", | |
| "statistics": { | |
| "total_entries": total_entries, | |
| "avg_sentence_length": round(avg_length, 2), | |
| "vocabulary_size": vocabulary_size, | |
| "improvement_score": min(100, int(total_entries * 5 + vocabulary_size / 10)), | |
| }, | |
| "charts": self._generate_progress_charts(), | |
| } | |
| return progress_report | |
| def _generate_progress_charts(self): | |
| """Tạo biểu đồ trực quan hóa tiến độ""" | |
| # Dữ liệu cho biểu đồ | |
| sentence_ids = [entry["id"] for entry in self.session_history] | |
| sentence_lengths = [len(entry["transcript"].split()) for entry in self.session_history] | |
| # Tạo biểu đồ độ dài câu | |
| plt.figure(figsize=(10, 5)) | |
| plt.plot(sentence_ids, sentence_lengths, marker="o", linestyle="-") | |
| plt.title("Độ dài câu qua thời gian") | |
| plt.xlabel("Số thứ tự câu") | |
| plt.ylabel("Số từ trong câu") | |
| plt.grid(True, linestyle="--", alpha=0.7) | |
| # Lưu biểu đồ vào buffer | |
| length_chart_buf = io.BytesIO() | |
| plt.savefig(length_chart_buf, format="png", dpi=100) | |
| length_chart_buf.seek(0) | |
| length_chart_b64 = base64.b64encode(length_chart_buf.read()).decode("utf-8") | |
| plt.close() | |
| # Biểu đồ phân bố độ dài câu | |
| plt.figure(figsize=(8, 4)) | |
| plt.hist(sentence_lengths, bins=10, alpha=0.7) | |
| plt.title("Phân bố độ dài câu") | |
| plt.xlabel("Số từ trong câu") | |
| plt.ylabel("Tần suất") | |
| plt.grid(True, linestyle="--", alpha=0.7) | |
| dist_chart_buf = io.BytesIO() | |
| plt.savefig(dist_chart_buf, format="png", dpi=100) | |
| dist_chart_buf.seek(0) | |
| dist_chart_b64 = base64.b64encode(dist_chart_buf.read()).decode("utf-8") | |
| plt.close() | |
| return { | |
| "length_chart": f"data:image/png;base64,{length_chart_b64}", | |
| "distribution_chart": f"data:image/png;base64,{dist_chart_b64}", | |
| } | |
| def clean_up(self): | |
| """Dọn dẹp tài nguyên trước khi thoát""" | |
| # Lưu lịch sử phiên cuối cùng | |
| if self.config["save_history"] and self.session_history: | |
| self._save_session_history() | |
| # Dừng bộ xử lý bất đồng bộ | |
| if hasattr(self, "async_processor"): | |
| self.async_processor.stop() | |
| # Giải phóng bộ nhớ GPU nếu cần | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| logger.info("Đã dọn dẹp tài nguyên") | |
| # Tạo giao diện Gradio | |
| def create_demo(): | |
| try: | |
| trainer = VietSpeechTrainer() | |
| with gr.Blocks(title="Công cụ Luyện Nói Tiếng Việt", theme=gr.themes.Soft(primary_hue="blue")) as demo: | |
| # Header | |
| with gr.Row(variant="panel"): | |
| with gr.Column(scale=6): | |
| gr.Markdown( | |
| """ | |
| # 🎤 Công cụ Luyện Nói Tiếng Việt AI | |
| ### Nâng cao kỹ năng giao tiếp tiếng Việt với trợ lý AI thông minh | |
| """ | |
| ) | |
| with gr.Column(scale=1): | |
| dialect_selector = gr.Radio(["Bắc", "Trung", "Nam"], label="Phương ngữ tiếng Việt", value="Bắc") | |
| # Tabs for different functions | |
| with gr.Tabs() as tabs: | |
| # Tab 1: Luyện phát âm | |
| with gr.TabItem("Luyện phát âm", id=0): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # Khu vực đầu vào | |
| audio_input = gr.Audio( | |
| label="📝 Giọng nói của bạn", | |
| type="filepath", | |
| source="microphone", | |
| format="wav", | |
| ) | |
| with gr.Row(): | |
| submit_btn = gr.Button("🔍 Phân tích", variant="primary") | |
| clear_btn = gr.Button("🗑️ Xóa") | |
| gr.Markdown( | |
| """ | |
| ### Chủ đề gợi ý: | |
| - 🎯 Giới thiệu bản thân | |
| - 🎯 Kể về một trải nghiệm thú vị | |
| - 🎯 Mô tả một địa điểm yêu thích | |
| - 🎯 Trình bày quan điểm về một vấn đề | |
| """ | |
| ) | |
| with gr.Column(scale=3): | |
| # Khu vực kết quả | |
| transcript_output = gr.Textbox( | |
| label="Nội dung bạn vừa nói", | |
| placeholder="Nội dung sẽ hiển thị ở đây...", | |
| lines=3, | |
| ) | |
| analysis_output = gr.Markdown(label="Phân tích và gợi ý cải thiện") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("#### Phát âm của bạn:") | |
| playback_audio = gr.Audio(label="", type="filepath") | |
| with gr.Column(scale=1): | |
| gr.Markdown("#### Phát âm mẫu:") | |
| sample_audio = gr.Audio(label="", type="filepath") | |
| # Lịch sử phiên | |
| with gr.Accordion("Lịch sử phiên luyện tập", open=False): | |
| history_md = gr.Markdown("*Chưa có lịch sử luyện tập*") | |
| # Tab 2: Hội thoại | |
| with gr.TabItem("Hội thoại", id=1): | |
| scenario_title = gr.Markdown("## Tình huống hội thoại") | |
| scenario_desc = gr.Markdown("*Nhấn Tạo tình huống để bắt đầu*") | |
| prompt_text = gr.Markdown("*Câu hỏi/lời thoại sẽ hiển thị ở đây*") | |
| conversation_audio = gr.Audio(label="Trả lời của bạn", source="microphone", type="filepath") | |
| conversation_transcript = gr.Textbox(label="Văn bản của bạn", lines=2) | |
| conversation_feedback = gr.Markdown(label="Phản hồi") | |
| with gr.Row(): | |
| new_scenario_btn = gr.Button("🔄 Tạo tình huống mới") | |
| next_prompt_btn = gr.Button("➡️ Câu tiếp theo") | |
| analyze_response_btn = gr.Button("🔍 Phân tích câu trả lời") | |
| # Tab 3: Tiến độ | |
| with gr.TabItem("Tiến độ", id=2): | |
| refresh_stats_btn = gr.Button("🔄 Cập nhật thống kê") | |
| with gr.Row(): | |
| with gr.Column(): | |
| stats_output = gr.JSON(label="Thống kê", value={"message": "Nhấn Cập nhật thống kê để xem"}) | |
| with gr.Row(): | |
| with gr.Column(): | |
| length_chart = gr.Image(label="Độ dài câu qua thời gian", show_download_button=False) | |
| with gr.Column(): | |
| dist_chart = gr.Image(label="Phân bố độ dài câu", show_download_button=False) | |
| # Tab 4: Xuất báo cáo | |
| with gr.TabItem("Xuất báo cáo", id=3): | |
| with gr.Row(): | |
| export_md_btn = gr.Button("📝 Xuất báo cáo Markdown") | |
| export_html_btn = gr.Button("🌐 Xuất báo cáo HTML") | |
| export_output = gr.File(label="Tải báo cáo") | |
| # Tab 5: Thông tin | |
| with gr.TabItem("Thông tin", id=4): | |
| gr.Markdown( | |
| """ | |
| ## Về công cụ luyện nói tiếng Việt | |
| Công cụ này sử dụng các mô hình trí tuệ nhân tạo tiên tiến để giúp người dùng cải thiện kỹ năng nói tiếng Việt. | |
| ### Công nghệ sử dụng | |
| - **Speech-to-Text**: Chuyển đổi giọng nói thành văn bản với độ chính xác cao | |
| - PhoWhisper hoặc wav2vec2-Vietnamese | |
| - **Phân tích ngôn ngữ**: Phân tích cấu trúc câu, phát hiện lỗi | |
| - PhoBERT kết hợp với LLM (Gemini/OpenAI/Local) | |
| - **Text-to-Speech**: Tạo mẫu phát âm chuẩn | |
| - VietTTS hoặc API TTS | |
| ### Tính năng chính | |
| - Nhận dạng và phân tích giọng nói tiếng Việt | |
| - Phát hiện lỗi ngữ pháp, từ vựng và cách diễn đạt | |
| - Phát âm mẫu chuẩn với VietTTS | |
| - Lưu trữ và theo dõi tiến độ | |
| - Gợi ý cải thiện cá nhân hóa | |
| - Hỗ trợ nhiều phương ngữ (Bắc, Trung, Nam) | |
| - Luyện tập hội thoại với tình huống thực tế | |
| ### Mô hình AI sử dụng | |
| - **PhoWhisper**: Mô hình nhận dạng giọng nói tiếng Việt tiên tiến nhất (2024), được phát triển bởi VinAI Research. | |
| - **PhoBERT**: Mô hình hiểu ngôn ngữ tự nhiên tiếng Việt SOTA, cũng được phát triển bởi VinAI Research. | |
| - **VietTTS**: Mô hình chuyển văn bản tiếng Việt thành giọng nói. | |
| ### Hướng dẫn sử dụng | |
| 1. Chọn tab "Luyện phát âm" hoặc "Hội thoại" | |
| 2. Thu âm giọng nói của bạn | |
| 3. Nhận phản hồi và gợi ý cải thiện từ AI | |
| 4. Theo dõi tiến độ trong tab "Tiến độ" | |
| 5. Xuất báo cáo để lưu lại kết quả học tập | |
| """ | |
| ) | |
| # Xử lý sự kiện | |
| # 1. Tab Luyện phát âm | |
| def process_and_display(audio, dialect): | |
| if audio is None: | |
| return "Vui lòng thu âm trước khi phân tích.", "", None, None, None | |
| # Xử lý bản ghi âm | |
| transcript, analysis, sample_audio_path = trainer.process_recording(audio, dialect) | |
| # Cập nhật lịch sử | |
| history_html = update_history() | |
| return transcript, analysis, audio, sample_audio_path, history_html | |
| def update_history(): | |
| if not trainer.session_history: | |
| return "*Chưa có lịch sử luyện tập*" | |
| history = "### Lịch sử phiên\n\n" | |
| for entry in trainer.session_history[-10:]: # Chỉ hiển thị 10 mục gần nhất | |
| short_t = entry["transcript"][:50] | |
| suffix = "..." if len(entry["transcript"]) > 50 else "" | |
| history += f"{entry['id']}. **{entry['time']}**: {short_t}{suffix}\n" | |
| return history | |
| def clear_inputs(): | |
| return None, "", "", None, None | |
| submit_btn.click( | |
| fn=process_and_display, | |
| inputs=[audio_input, dialect_selector], | |
| outputs=[transcript_output, analysis_output, playback_audio, sample_audio, history_md], | |
| ) | |
| clear_btn.click(fn=clear_inputs, inputs=[], outputs=[audio_input, transcript_output, analysis_output, playback_audio, sample_audio]) | |
| # 2. Tab Hội thoại | |
| current_scenario = gr.State(None) | |
| current_prompt_index = gr.State(0) | |
| def load_new_scenario(): | |
| scenario = trainer.create_conversation_scenario() | |
| return ( | |
| f"## {scenario['title']}", | |
| f"*{scenario['description']}*", | |
| f"**Bot**: {scenario['prompts'][0]}", | |
| scenario, | |
| 0, | |
| ) | |
| def next_prompt(scenario, prompt_index): | |
| if scenario is None: | |
| return "Vui lòng tạo tình huống trước", prompt_index | |
| next_index = prompt_index + 1 | |
| if next_index >= len(scenario["prompts"]): | |
| return "Đã hết các câu hỏi trong tình huống này. Hãy tạo tình huống mới!", prompt_index | |
| return f"**Bot**: {scenario['prompts'][next_index]}", next_index | |
| def analyze_conversation_response(audio, scenario, prompt_index, dialect): | |
| if audio is None: | |
| return "Vui lòng ghi âm câu trả lời trước", "" | |
| if scenario is None or prompt_index >= len(scenario["prompts"]): | |
| return "Không có tình huống hoặc câu hỏi hợp lệ", "" | |
| # Xử lý âm thanh -> văn bản | |
| transcript = trainer.transcribe_audio(audio) | |
| # Phân tích câu trả lời trong ngữ cảnh | |
| context = scenario["prompts"][prompt_index] | |
| prompt = f"""Phân tích câu trả lời trong cuộc hội thoại: | |
| Ngữ cảnh: {context} | |
| Câu trả lời: {transcript} | |
| Phương ngữ: {dialect} | |
| Hãy đánh giá tính phù hợp của câu trả lời với ngữ cảnh, cách diễn đạt, và đưa ra gợi ý cải thiện. | |
| """ | |
| # Sử dụng hàm phân tích với LLM (nếu có) | |
| if trainer.config["llm_provider"] != "none": | |
| if trainer.config["llm_provider"] == "openai": | |
| analysis = trainer._analyze_with_openai(transcript, "", dialect) | |
| elif trainer.config["llm_provider"] == "gemini": | |
| analysis = trainer._analyze_with_gemini(transcript, "", dialect) | |
| elif trainer.config["llm_provider"] == "local": | |
| analysis = trainer._analyze_with_local_llm(transcript, "", dialect) | |
| else: | |
| analysis = trainer._rule_based_analysis(transcript, "", dialect) | |
| return transcript, analysis | |
| new_scenario_btn.click( | |
| fn=load_new_scenario, | |
| inputs=[], | |
| outputs=[scenario_title, scenario_desc, prompt_text, current_scenario, current_prompt_index], | |
| ) | |
| next_prompt_btn.click(fn=next_prompt, inputs=[current_scenario, current_prompt_index], outputs=[prompt_text, current_prompt_index]) | |
| analyze_response_btn.click( | |
| fn=analyze_conversation_response, | |
| inputs=[conversation_audio, current_scenario, current_prompt_index, dialect_selector], | |
| outputs=[conversation_transcript, conversation_feedback], | |
| ) | |
| # 3. Tab Tiến độ | |
| def update_statistics(): | |
| progress_data = trainer.track_progress() | |
| stats = progress_data["statistics"] | |
| charts = progress_data["charts"] | |
| return stats, charts.get("length_chart", ""), charts.get("distribution_chart", "") | |
| refresh_stats_btn.click(fn=update_statistics, inputs=[], outputs=[stats_output, length_chart, dist_chart]) | |
| # 4. Tab Xuất báo cáo | |
| def export_markdown(): | |
| return trainer.export_session(format="markdown") | |
| def export_html(): | |
| return trainer.export_session(format="html") | |
| export_md_btn.click(fn=export_markdown, inputs=[], outputs=[export_output]) | |
| export_html_btn.click(fn=export_html, inputs=[], outputs=[export_output]) | |
| # Xử lý khi đóng ứng dụng | |
| demo.load(lambda: None, inputs=None, outputs=None) | |
| return demo | |
| except Exception as e: | |
| logger.error(f"Lỗi khi tạo giao diện: {e}") | |
| raise | |
| def main(): | |
| try: | |
| # Kiểm tra và tạo thư mục dữ liệu | |
| os.makedirs("data", exist_ok=True) | |
| os.makedirs("data/audio", exist_ok=True) | |
| os.makedirs("data/reports", exist_ok=True) | |
| os.makedirs("data/models", exist_ok=True) | |
| # Tạo file cấu hình mẫu nếu chưa có | |
| if not os.path.exists("config.json"): | |
| sample_config = { | |
| "stt_model": "nguyenvulebinh/wav2vec2-base-vietnamese-250h", | |
| "use_phowhisper": False, | |
| "use_phobert": False, | |
| "use_vncorenlp": False, | |
| "llm_provider": "none", | |
| "use_viettts": False, | |
| "default_dialect": "Bắc", | |
| "preprocess_audio": True, | |
| "save_history": True, | |
| } | |
| with open("config.json", "w", encoding="utf-8") as f: | |
| json.dump(sample_config, f, ensure_ascii=False, indent=2) | |
| # Tạo và khởi chạy ứng dụng | |
| demo = create_demo() | |
| demo.queue() | |
| demo.launch(share=True) | |
| except Exception as e: | |
| logger.error(f"Lỗi khi khởi chạy ứng dụng: {e}") | |
| print(f"Lỗi: {e}") | |
| if __name__ == "__main__": | |
| main() | |
| # Cải tiến: | |
| # - Đánh giá ngữ điệu: Phân tích cao độ, nhịp điệu và cảm xúc trong giọng nói | |
| # - Tùy chỉnh giọng TTS: Cho phép người dùng chọn giọng đọc mẫu | |
| # - Tạo bài tập cá nhân hóa: Dựa trên lỗi thường gặp của người dùng | |