peterpanbk95 commited on
Commit
122fe8e
·
verified ·
1 Parent(s): e88cbc5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1234 -240
app.py CHANGED
@@ -6,25 +6,27 @@ import threading
6
  import json
7
  import base64
8
  import io
 
9
  import random
10
  import logging
11
  from queue import Queue
12
  from threading import Thread
13
 
 
 
14
  import gradio as gr
15
  import torch
16
- import librosa
17
  import soundfile as sf
 
18
  import requests
19
- import numpy as np
20
- from scipy import signal
21
  from transformers import pipeline, AutoTokenizer, AutoModel
 
22
 
23
- # Thiết lập logging
24
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
25
  logger = logging.getLogger(__name__)
26
 
27
- # Tạo các thư mục cần thiết
28
  os.makedirs("data", exist_ok=True)
29
  os.makedirs("data/audio", exist_ok=True)
30
  os.makedirs("data/reports", exist_ok=True)
@@ -32,7 +34,8 @@ os.makedirs("data/models", exist_ok=True)
32
 
33
 
34
  class AsyncProcessor:
35
- """Xử lý các tác vụ nặng trong thread riêng để không làm 'đơ' giao diện."""
 
36
  def __init__(self):
37
  self.task_queue = Queue()
38
  self.result_queue = Queue()
@@ -70,314 +73,1305 @@ class AsyncProcessor:
70
 
71
  class VietSpeechTrainer:
72
  def __init__(self):
73
- # Đọc cấu hình từ file config.json từ biến môi trường
74
  self.config = self._load_config()
75
 
76
  # Khởi tạo bộ xử lý bất đồng bộ
77
  self.async_processor = AsyncProcessor()
78
 
79
- # Lưu trữ lịch sử phiên làm việc
80
  self.session_history = []
81
  self.current_session_id = int(time.time())
82
 
83
- # Các biến trạng thái hội thoại
84
  self.current_scenario = None
85
  self.current_prompt_index = 0
86
 
87
- # Khởi tạo các mô hình (STT, TTS và phân tích LLM)
88
  logger.info("Đang tải các mô hình...")
89
  self._initialize_models()
90
 
91
  def _load_config(self):
92
- """Đọc file config.json cập nhật từ biến môi trường (Secrets khi deploy)"""
93
  config = {
94
- "stt_model": "nguyenvulebinh/wav2vec2-base-vietnamese-250h",
95
- "use_phowhisper": False,
96
- "use_phobert": False,
97
- "use_vncorenlp": False,
98
- "llm_provider": "none", # openai, gemini, local hoặc none
99
- "openai_api_key": "",
100
- "gemini_api_key": "",
101
- "local_llm_endpoint": "",
102
- "use_viettts": False,
103
- "default_dialect": "Bắc",
104
- "enable_pronunciation_eval": False,
105
- "preprocess_audio": True,
106
- "save_history": True,
107
- "enable_english_tts": False
 
 
 
 
 
 
108
  }
 
 
109
  if os.path.exists("config.json"):
110
  try:
111
  with open("config.json", "r", encoding="utf-8") as f:
112
  file_config = json.load(f)
113
  config.update(file_config)
114
  except Exception as e:
115
- logger.error(f"Lỗi đọc config.json: {e}")
116
- # Cập nhật từ biến môi trường
117
- if os.environ.get("LLM_PROVIDER"):
118
- config["llm_provider"] = os.environ.get("LLM_PROVIDER").lower()
119
- if os.environ.get("OPENAI_API_KEY"):
120
- config["openai_api_key"] = os.environ.get("OPENAI_API_KEY")
121
- if os.environ.get("GEMINI_API_KEY"):
122
- config["gemini_api_key"] = os.environ.get("GEMINI_API_KEY")
123
- if os.environ.get("LOCAL_LLM_ENDPOINT"):
124
- config["local_llm_endpoint"] = os.environ.get("LOCAL_LLM_ENDPOINT")
125
- if os.environ.get("ENABLE_ENGLISH_TTS") and os.environ.get("ENABLE_ENGLISH_TTS").lower() == "true":
126
- config["enable_english_tts"] = True
127
  return config
128
 
129
  def _initialize_models(self):
130
- """Khởi tạo mô hình STT thiết lập CSM cho TTS tiếng Anh nếu được bật."""
131
  try:
132
- # Khởi tạo STT
133
  if self.config["use_phowhisper"]:
134
- logger.info("Loading PhoWhisper...")
135
- self.stt_model = pipeline("automatic-speech-recognition",
136
- model="vinai/PhoWhisper-small",
137
- device=0 if torch.cuda.is_available() else -1)
 
 
138
  else:
139
- logger.info(f"Loading STT model: {self.config['stt_model']}")
140
- self.stt_model = pipeline("automatic-speech-recognition",
141
- model=self.config["stt_model"],
142
- device=0 if torch.cuda.is_available() else -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  except Exception as e:
144
- logger.error(f"Lỗi khởi tạo STT: {e}")
145
- self.stt_model = None
146
 
147
- # Các mô hình NLP (PhoBERT, VnCoreNLP) nếu cần.
148
- # ...
 
 
149
 
150
- # Nếu bật TTS tiếng Anh thì thiết lập CSM
151
- if self.config.get("enable_english_tts", False):
152
- self._setup_csm()
153
- else:
154
- self.csm_ready = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- def _setup_csm(self):
157
- """Cài đặt mô hình CSM (Conversational Speech Generation Model) cho TTS tiếng Anh."""
158
  try:
159
- csm_dir = os.path.join(os.getcwd(), "csm")
160
- if not os.path.exists(csm_dir):
161
- logger.info("Cloning CSM repo...")
162
- subprocess.run(["git", "clone", "https://github.com/SesameAILabs/csm", csm_dir], check=True)
163
- logger.info("Installing CSM requirements...")
164
- subprocess.run(["pip", "install", "-r", os.path.join(csm_dir, "requirements.txt")], check=True)
165
- self.csm_ready = True
166
- logger.info("CSM đã được thiết lập thành công!")
 
 
 
 
 
 
167
  except Exception as e:
168
- logger.error(f"Failed to set up CSM: {e}")
169
- self.csm_ready = False
170
-
171
- def text_to_speech(self, text, language="vi", dialect="Bắc"):
172
- """
173
- Chuyển văn bản thành giọng nói:
174
- - Nếu language == "en": sử dụng CSM để tạo TTS tiếng Anh.
175
- - Nếu language == "vi": sử dụng API hoặc logic TTS tiếng Việt.
176
- """
177
- if language == "en":
178
- if not self.csm_ready:
179
- logger.error("CSM chưa được thiết lập hoặc không được bật.")
180
- return None
181
- output_file = f"data/audio/csm_{int(time.time())}.wav"
182
- csm_script_path = os.path.join(os.getcwd(), "csm", "run_csm.py")
183
- cmd = [
184
- "python",
185
- csm_script_path,
186
- "--text", text,
187
- "--speaker_id", "0", # Mặc định, có thể cho phép người dùng chọn
188
- "--output", output_file
189
- ]
190
- try:
191
- subprocess.run(cmd, check=True)
192
- return output_file
193
- except subprocess.CalledProcessError as e:
194
- logger.error(f"CSM generation failed: {e}")
195
- return None
196
- else:
197
- # Ví dụ: Nếu có API TTS tiếng Việt, gọi API đó.
198
- tts_api_url = self.config.get("tts_api_url", "")
199
- if tts_api_url:
200
- try:
201
- resp = requests.post(tts_api_url, json={"text": text, "dialect": dialect.lower()})
202
- if resp.status_code == 200:
203
- output_file = f"data/audio/tts_{int(time.time())}.wav"
204
- with open(output_file, "wb") as f:
205
- f.write(resp.content)
206
- return output_file
207
- else:
208
- logger.error(f"Error calling TTS API: {resp.text}")
209
- return None
210
- except Exception as e:
211
- logger.error(f"Lỗi gọi TTS API: {e}")
212
- return None
213
- else:
214
- # Nếu không có API TTS, bạn có thể tích hợp VietTTS hoặc khác.
215
- return None
216
 
217
  def transcribe_audio(self, audio_path):
218
- """Chuyển đổi giọng nói thành văn bản (STT)."""
219
- if not self.stt_model:
220
- return "STT model not available."
221
  try:
 
 
 
 
 
222
  result = self.stt_model(audio_path)
 
 
223
  if isinstance(result, dict) and "text" in result:
224
- return result["text"]
225
  elif isinstance(result, list):
226
- return " ".join([chunk.get("text", "") for chunk in result])
227
  else:
228
- return str(result)
 
 
229
  except Exception as e:
230
- logger.error(f"Lỗi chuyển giọng nói: {e}")
231
  return f"Lỗi: {str(e)}"
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  def analyze_text(self, transcript, dialect="Bắc"):
234
- """
235
- Phân tích văn bản sử dụng LLM:
236
- - Nếu LLM_PROVIDER là "openai", "gemini" hay "local" thì gọi API tương ứng.
237
- - Nếu LLM_PROVIDER là "none", sử dụng phân tích rule-based.
238
- """
 
 
 
239
  llm_provider = self.config["llm_provider"]
 
240
  if llm_provider == "openai" and self.config["openai_api_key"]:
241
- return self._analyze_with_openai(transcript)
242
  elif llm_provider == "gemini" and self.config["gemini_api_key"]:
243
- return self._analyze_with_gemini(transcript)
244
  elif llm_provider == "local" and self.config["local_llm_endpoint"]:
245
- return self._analyze_with_local_llm(transcript)
246
  else:
247
- return self._rule_based_analysis(transcript, dialect)
 
248
 
249
- def _analyze_with_openai(self, transcript):
250
- headers = {
251
- "Authorization": f"Bearer {self.config['openai_api_key']}",
252
- "Content-Type": "application/json"
253
- }
254
- data = {
255
- "model": "gpt-3.5-turbo",
256
- "messages": [
257
- {"role": "system", "content": "Bạn là trợ lý dạy tiếng Việt."},
258
- {"role": "user", "content": transcript}
259
- ],
260
- "temperature": 0.5,
261
- "max_tokens": 150
262
- }
263
  try:
264
- response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  if response.status_code == 200:
266
  result = response.json()
267
- return result["choices"][0]["message"]["content"]
 
268
  else:
269
- return "Lỗi khi gọi OpenAI API."
 
 
270
  except Exception as e:
271
- logger.error(f"Lỗi OpenAI: {e}")
272
- return "Lỗi phân tích với OpenAI."
273
-
274
- def _analyze_with_gemini(self, transcript):
275
- # dụ minh họa: Gọi Gemini API (chi tiết phụ thuộc vào tài liệu của Gemini)
276
- return "Gemini analysis..."
277
-
278
- def _analyze_with_local_llm(self, transcript):
279
- # Giả sử gọi một endpoint local (nếu có) cho LLM cục bộ.
280
- headers = {"Content-Type": "application/json"}
281
- data = {
282
- "model": "local-model",
283
- "messages": [
284
- {"role": "system", "content": "Bạn là trợ lý dạy tiếng Việt."},
285
- {"role": "user", "content": transcript}
286
- ],
287
- "temperature": 0.5,
288
- "max_tokens": 150
289
- }
290
  try:
291
- response = requests.post(self.config["local_llm_endpoint"] + "/chat/completions", headers=headers, json=data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  if response.status_code == 200:
293
  result = response.json()
294
- return result["choices"][0]["message"]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  else:
296
- return "Lỗi khi gọi Local LLM."
297
  except Exception as e:
298
- logger.error(f"Lỗi local LLM: {e}")
299
- return "Lỗi phân tích với LLM local."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- def _rule_based_analysis(self, transcript, dialect):
302
- # Phân tích đơn giản không dùng LLM
303
- return "Phân tích rule-based: " + transcript
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
  def clean_up(self):
306
- self.async_processor.stop()
 
 
 
 
 
 
 
 
 
307
  if torch.cuda.is_available():
308
  torch.cuda.empty_cache()
309
- logger.info("Clean up done.")
310
 
 
311
 
 
 
312
  def create_demo():
313
- trainer = VietSpeechTrainer()
314
-
315
- with gr.Blocks(title="Ứng dụng Luyện Nói & TTS", theme=gr.themes.Soft(primary_hue="blue")) as demo:
316
- gr.Markdown("## Ứng dụng Luyện Nói & TTS (Tiếng Việt & Tiếng Anh)")
317
-
318
- with gr.Tabs():
319
- # Tab 1: TTS Tiếng Việt
320
- with gr.Tab("TTS Tiếng Việt"):
321
- vi_text_input = gr.Textbox(label="Nhập văn bản tiếng Việt")
322
- vi_audio_output = gr.Audio(label="Kết quả âm thanh")
323
- gen_vi_btn = gr.Button("Chuyển thành giọng nói")
324
-
325
- def gen_vi_tts(txt):
326
- return trainer.text_to_speech(txt, language="vi", dialect=trainer.config["default_dialect"])
327
-
328
- gen_vi_btn.click(fn=gen_vi_tts, inputs=vi_text_input, outputs=vi_audio_output)
329
-
330
- # Tab 2: TTS Tiếng Anh (sử dụng CSM)
331
- with gr.Tab("TTS Tiếng Anh"):
332
- en_text_input = gr.Textbox(label="Enter English text")
333
- en_audio_output = gr.Audio(label="Generated English Audio (CSM)")
334
- gen_en_btn = gr.Button("Generate English Speech")
335
-
336
- def gen_en_tts(txt):
337
- return trainer.text_to_speech(txt, language="en")
338
-
339
- gen_en_btn.click(fn=gen_en_tts, inputs=en_text_input, outputs=en_audio_output)
340
-
341
- # Tab 3: Luyện phát âm (Tiếng Việt)
342
- with gr.Tab("Luyện phát âm"):
343
- audio_input = gr.Audio("microphone", type="filepath", label="Giọng nói của bạn")
344
- transcript_output = gr.Textbox(label="Transcript")
345
- analysis_output = gr.Markdown(label="Phân tích")
346
- analyze_btn = gr.Button("Phân tích")
347
-
348
- def process_audio(audio_path):
349
- transcript = trainer.transcribe_audio(audio_path)
350
- analysis = trainer.analyze_text(transcript, dialect=trainer.config["default_dialect"])
351
- return transcript, analysis
352
-
353
- analyze_btn.click(fn=process_audio, inputs=audio_input, outputs=[transcript_output, analysis_output])
354
-
355
- # Tab 4: Thông tin & Hướng dẫn
356
- with gr.Tab("Thông tin"):
357
- gr.Markdown("""
358
- ### Hướng dẫn sử dụng:
359
- - **TTS Tiếng Việt:** Nhập văn bản tiếng Việt và nhấn "Chuyển thành giọng nói".
360
- - **TTS Tiếng Anh (CSM):** Nhập English text và nhấn "Generate English Speech".
361
- - **Luyện phát âm:** Thu âm giọng nói, sau đó nhấn "Phân tích" để xem transcript và phân tích.
362
-
363
- ### Cấu hình LLM:
364
- - **OpenAI:** Đặt biến môi trường `LLM_PROVIDER=openai` và `OPENAI_API_KEY` với key của bạn.
365
- - **Gemini:** Đặt `LLM_PROVIDER=gemini` và `GEMINI_API_KEY`.
366
- - **Local LLM:** Đặt `LLM_PROVIDER=local` và `LOCAL_LLM_ENDPOINT` với URL của server LLM nếu bạn có.
367
- - **None:** Đặt `LLM_PROVIDER=none` để sử dụng phân tích rule-based.
368
-
369
- ### Lưu ý:
370
- - Để sử dụng TTS tiếng Anh (CSM), hãy bật biến `ENABLE_ENGLISH_TTS` (hoặc đặt `"enable_english_tts": true` trong config.json).
371
- """)
372
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
 
375
  def main():
376
- demo = create_demo()
377
- # Sử dụng hàng đợi Gradio để xử lý tác vụ dài (ví dụ TTS CSM)
378
- demo.queue()
379
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
 
382
  if __name__ == "__main__":
383
  main()
 
 
 
 
 
 
6
  import json
7
  import base64
8
  import io
9
+ import shutil
10
  import random
11
  import logging
12
  from queue import Queue
13
  from threading import Thread
14
 
15
+ import numpy as np
16
+ import matplotlib.pyplot as plt
17
  import gradio as gr
18
  import torch
 
19
  import soundfile as sf
20
+ import librosa
21
  import requests
 
 
22
  from transformers import pipeline, AutoTokenizer, AutoModel
23
+ from scipy import signal
24
 
25
+ # Cấu hình logging
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
27
  logger = logging.getLogger(__name__)
28
 
29
+ # Kiểm tra và tạo thư mục cho dữ liệu
30
  os.makedirs("data", exist_ok=True)
31
  os.makedirs("data/audio", exist_ok=True)
32
  os.makedirs("data/reports", exist_ok=True)
 
34
 
35
 
36
  class AsyncProcessor:
37
+ """Xử lý các tác vụ nặng trong thread riêng để không làm đơ giao diện"""
38
+
39
  def __init__(self):
40
  self.task_queue = Queue()
41
  self.result_queue = Queue()
 
73
 
74
  class VietSpeechTrainer:
75
  def __init__(self):
76
+ # Cấu hình từ biến môi trường hoặc file cấu hình
77
  self.config = self._load_config()
78
 
79
  # Khởi tạo bộ xử lý bất đồng bộ
80
  self.async_processor = AsyncProcessor()
81
 
82
+ # Lưu trữ lịch sử
83
  self.session_history = []
84
  self.current_session_id = int(time.time())
85
 
86
+ # Trạng thái hội thoại
87
  self.current_scenario = None
88
  self.current_prompt_index = 0
89
 
90
+ # Khởi tạo các mô hình
91
  logger.info("Đang tải các mô hình...")
92
  self._initialize_models()
93
 
94
  def _load_config(self):
95
+ """Tải cấu hình từ file hoặc biến môi trường"""
96
  config = {
97
+ # STT config
98
+ "stt_model": os.environ.get("STT_MODEL", "nguyenvulebinh/wav2vec2-base-vietnamese-250h"),
99
+ "use_phowhisper": os.environ.get("USE_PHOWHISPER", "false").lower() == "true",
100
+ # NLP config
101
+ "use_phobert": os.environ.get("USE_PHOBERT", "false").lower() == "true",
102
+ "use_vncorenlp": os.environ.get("USE_VNCORENLP", "false").lower() == "true",
103
+ # LLM config
104
+ "llm_provider": os.environ.get("LLM_PROVIDER", "none"), # "openai", "gemini", "local", "none"
105
+ "openai_api_key": os.environ.get("OPENAI_API_KEY", ""),
106
+ "gemini_api_key": os.environ.get("GEMINI_API_KEY", ""),
107
+ "local_llm_endpoint": os.environ.get("LOCAL_LLM_ENDPOINT", "http://localhost:8080/v1"),
108
+ # TTS config
109
+ "use_viettts": os.environ.get("USE_VIETTTS", "false").lower() == "true",
110
+ "tts_api_url": os.environ.get("TTS_API_URL", ""),
111
+ # Application settings
112
+ "default_dialect": os.environ.get("DEFAULT_DIALECT", "Bắc"),
113
+ "enable_pronunciation_eval": os.environ.get("ENABLE_PRONUNCIATION_EVAL", "false").lower() == "true",
114
+ # Advanced settings
115
+ "preprocess_audio": os.environ.get("PREPROCESS_AUDIO", "true").lower() == "true",
116
+ "save_history": os.environ.get("SAVE_HISTORY", "true").lower() == "true",
117
  }
118
+
119
+ # Nếu tồn tại file cấu hình, đọc thêm từ đó
120
  if os.path.exists("config.json"):
121
  try:
122
  with open("config.json", "r", encoding="utf-8") as f:
123
  file_config = json.load(f)
124
  config.update(file_config)
125
  except Exception as e:
126
+ logger.error(f"Lỗi khi đọc file cấu hình: {e}")
127
+
 
 
 
 
 
 
 
 
 
 
128
  return config
129
 
130
  def _initialize_models(self):
131
+ """Khởi tạo các mô hình AI cần thiết"""
132
  try:
133
+ # 1. Khởi tạo mô hình STT
134
  if self.config["use_phowhisper"]:
135
+ logger.info("Đang tải PhoWhisper...")
136
+ self.stt_model = pipeline(
137
+ "automatic-speech-recognition",
138
+ model="vinai/PhoWhisper-small",
139
+ device=0 if torch.cuda.is_available() else -1,
140
+ )
141
  else:
142
+ logger.info(f"Đang tải mô hình STT: {self.config['stt_model']}")
143
+ self.stt_model = pipeline(
144
+ "automatic-speech-recognition",
145
+ model=self.config["stt_model"],
146
+ device=0 if torch.cuda.is_available() else -1,
147
+ )
148
+
149
+ # 2. Khởi tạo PhoBERT và VnCoreNLP nếu được cấu hình
150
+ self.phobert_model = None
151
+ self.phobert_tokenizer = None
152
+ self.rdrsegmenter = None
153
+
154
+ if self.config["use_phobert"]:
155
+ logger.info("Đang tải PhoBERT...")
156
+ try:
157
+ self.phobert_tokenizer = AutoTokenizer.from_pretrained("vinai/phobert-base")
158
+ self.phobert_model = AutoModel.from_pretrained("vinai/phobert-base")
159
+ except Exception as e:
160
+ logger.error(f"Lỗi khi tải PhoBERT: {e}")
161
+ self.config["use_phobert"] = False
162
+
163
+ if self.config["use_vncorenlp"]:
164
+ logger.info("Đang chuẩn bị VnCoreNLP...")
165
+ try:
166
+ vncorenlp_path = self._setup_vncorenlp()
167
+ from py_vncorenlp import VnCoreNLP
168
+
169
+ self.rdrsegmenter = VnCoreNLP(vncorenlp_path, annotators="wseg", max_heap_size="-Xmx500m")
170
+ except Exception as e:
171
+ logger.error(f"Lỗi khi chuẩn bị VnCoreNLP: {e}")
172
+ self.config["use_vncorenlp"] = False
173
+
174
+ # 3. Chuẩn bị VietTTS nếu được cấu hình
175
+ self.viettts_ready = False
176
+ if self.config["use_viettts"]:
177
+ logger.info("Đang chuẩn bị VietTTS...")
178
+ try:
179
+ self.viettts_ready = self._setup_viettts()
180
+ except Exception as e:
181
+ logger.error(f"Lỗi khi chuẩn bị VietTTS: {e}")
182
+ self.config["use_viettts"] = False
183
+
184
+ logger.info("Khởi tạo mô hình hoàn tất")
185
  except Exception as e:
186
+ logger.error(f"Lỗi khi khởi tạo mô hình: {e}")
187
+ raise
188
 
189
+ def _setup_vncorenlp(self):
190
+ """Tải và cài đặt VnCoreNLP"""
191
+ vncorenlp_dir = "data/models/vncorenlp"
192
+ vncorenlp_jar = f"{vncorenlp_dir}/VnCoreNLP-1.1.1.jar"
193
 
194
+ os.makedirs(vncorenlp_dir, exist_ok=True)
195
+
196
+ if not os.path.exists(vncorenlp_jar):
197
+ logger.info("Đang tải VnCoreNLP...")
198
+
199
+ # Tải jar file
200
+ url = "https://raw.githubusercontent.com/vncorenlp/VnCoreNLP/master/VnCoreNLP-1.1.1.jar"
201
+ response = requests.get(url)
202
+ with open(vncorenlp_jar, "wb") as f:
203
+ f.write(response.content)
204
+
205
+ # Tạo thư mục models
206
+ os.makedirs(f"{vncorenlp_dir}/models/wordsegmenter", exist_ok=True)
207
+
208
+ # Tải models
209
+ for model_file in ["vi-vocab", "wordsegmenter.rdr"]:
210
+ url = f"https://raw.githubusercontent.com/vncorenlp/VnCoreNLP/master/models/wordsegmenter/{model_file}"
211
+ response = requests.get(url)
212
+ with open(f"{vncorenlp_dir}/models/wordsegmenter/{model_file}", "wb") as f:
213
+ f.write(response.content)
214
+
215
+ return vncorenlp_jar
216
+
217
+ def _setup_viettts(self):
218
+ """Cài đặt và chuẩn bị VietTTS"""
219
+ viettts_dir = "data/models/viettts"
220
+
221
+ # Nếu đã tải VietTTS rồi
222
+ if os.path.exists(f"{viettts_dir}/pretrained"):
223
+ return True
224
+
225
+ # Clone repo nếu chưa có
226
+ os.makedirs(viettts_dir, exist_ok=True)
227
+ if not os.path.exists(f"{viettts_dir}/.git"):
228
+ logger.info("Đang clone VietTTS repository...")
229
+ result = subprocess.run(
230
+ ["git", "clone", "https://github.com/NTT123/vietTTS.git", viettts_dir],
231
+ capture_output=True,
232
+ text=True,
233
+ )
234
+ if result.returncode != 0:
235
+ logger.error(f"Lỗi khi clone VietTTS: {result.stderr}")
236
+ return False
237
+
238
+ # Cài đặt VietTTS
239
+ logger.info("Đang cài đặt VietTTS...")
240
+ os.chdir(viettts_dir)
241
+ result = subprocess.run(["pip", "install", "-e", "."], capture_output=True, text=True)
242
+ if result.returncode != 0:
243
+ logger.error(f"Lỗi khi cài đặt VietTTS: {result.stderr}")
244
+ os.chdir("..")
245
+ return False
246
+
247
+ # Tải mô hình pretrained
248
+ if not os.path.exists("pretrained"):
249
+ logger.info("Đang tải mô hình pretrained...")
250
+ result = subprocess.run(["bash", "scripts/quick_start.sh"], capture_output=True, text=True)
251
+ if result.returncode != 0:
252
+ logger.error(f"Lỗi khi tải mô hình pretrained: {result.stderr}")
253
+ os.chdir("..")
254
+ return False
255
+
256
+ os.chdir("..")
257
+ return True
258
+
259
+ def preprocess_audio(self, audio_path):
260
+ """Tiền xử lý âm thanh để cải thiện chất lượng"""
261
+ if not self.config["preprocess_audio"]:
262
+ return audio_path
263
 
 
 
264
  try:
265
+ # Đọc âm thanh
266
+ y, sr = librosa.load(audio_path, sr=16000)
267
+
268
+ # Chuẩn hóa âm lượng
269
+ y_normalized = librosa.util.normalize(y)
270
+
271
+ # Xử lý nhiễu (đơn giản)
272
+ y_filtered = self._simple_noise_reduction(y_normalized)
273
+
274
+ # Lưu file mới
275
+ processed_path = audio_path.replace(".wav", "_processed.wav")
276
+ sf.write(processed_path, y_filtered, sr)
277
+
278
+ return processed_path
279
  except Exception as e:
280
+ logger.error(f"Lỗi khi tiền xử lý âm thanh: {e}")
281
+ return audio_path
282
+
283
+ def _simple_noise_reduction(self, y):
284
+ """Áp dụng lọc nhiễu đơn giản"""
285
+ # Áp dụng high-pass filter để giảm nhiễu tần số thấp
286
+ b, a = signal.butter(5, 80 / (16000 / 2), "highpass")
287
+ y_filtered = signal.filtfilt(b, a, y)
288
+ return y_filtered
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  def transcribe_audio(self, audio_path):
291
+ """Chuyển đổi âm thanh thành văn bản"""
 
 
292
  try:
293
+ # Tiền xử lý audio nếu cần
294
+ if self.config["preprocess_audio"]:
295
+ audio_path = self.preprocess_audio(audio_path)
296
+
297
+ # Thực hiện nhận dạng giọng nói
298
  result = self.stt_model(audio_path)
299
+
300
+ # Kết quả có thể có cấu trúc khác nhau tùy mô hình
301
  if isinstance(result, dict) and "text" in result:
302
+ text = result["text"]
303
  elif isinstance(result, list):
304
+ text = " ".join([chunk.get("text", "") for chunk in result])
305
  else:
306
+ text = str(result)
307
+
308
+ return text
309
  except Exception as e:
310
+ logger.error(f"Lỗi khi chuyển đổi âm thanh: {e}")
311
  return f"Lỗi: {str(e)}"
312
 
313
+ def segment_text(self, text):
314
+ """Tách từ văn bản tiếng Việt"""
315
+ if not text or not text.strip():
316
+ return text
317
+
318
+ # Nếu có VnCoreNLP, sử dụng RDRSegmenter
319
+ if self.config["use_vncorenlp"] and self.rdrsegmenter:
320
+ try:
321
+ sentences = self.rdrsegmenter.tokenize(text)
322
+ segmented_text = " ".join([" ".join(sentence) for sentence in sentences])
323
+ return segmented_text
324
+ except Exception as e:
325
+ logger.error(f"Lỗi khi tách từ với VnCoreNLP: {e}")
326
+
327
+ # Nếu không có VnCoreNLP hoặc lỗi, trả về nguyên bản
328
+ return text
329
+
330
  def analyze_text(self, transcript, dialect="Bắc"):
331
+ """Phân tích văn bản và đưa ra gợi ý cải thiện"""
332
+ if not transcript or not transcript.strip():
333
+ return "Không nhận được văn bản để phân tích."
334
+
335
+ # Tách từ
336
+ segmented_text = self.segment_text(transcript)
337
+
338
+ # Phân tích với LLM nếu có cấu hình
339
  llm_provider = self.config["llm_provider"]
340
+
341
  if llm_provider == "openai" and self.config["openai_api_key"]:
342
+ return self._analyze_with_openai(transcript, segmented_text, dialect)
343
  elif llm_provider == "gemini" and self.config["gemini_api_key"]:
344
+ return self._analyze_with_gemini(transcript, segmented_text, dialect)
345
  elif llm_provider == "local" and self.config["local_llm_endpoint"]:
346
+ return self._analyze_with_local_llm(transcript, segmented_text, dialect)
347
  else:
348
+ # Sử dụng phân tích dựa trên quy tắc
349
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
350
 
351
+ def _analyze_with_openai(self, transcript, segmented_text, dialect):
352
+ """Phân tích văn bản sử dụng OpenAI API"""
 
 
 
 
 
 
 
 
 
 
 
 
353
  try:
354
+ headers = {
355
+ "Authorization": f"Bearer {self.config['openai_api_key']}",
356
+ "Content-Type": "application/json",
357
+ }
358
+
359
+ # Tạo prompt
360
+ prompt = self._create_analysis_prompt(transcript, segmented_text, dialect)
361
+
362
+ # Gọi API
363
+ response = requests.post(
364
+ "https://api.openai.com/v1/chat/completions",
365
+ headers=headers,
366
+ json={
367
+ "model": "gpt-3.5-turbo",
368
+ "messages": [
369
+ {
370
+ "role": "system",
371
+ "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.",
372
+ },
373
+ {"role": "user", "content": prompt},
374
+ ],
375
+ "temperature": 0.5,
376
+ "max_tokens": 800,
377
+ },
378
+ )
379
+
380
  if response.status_code == 200:
381
  result = response.json()
382
+ analysis = result["choices"][0]["message"]["content"]
383
+ return analysis
384
  else:
385
+ logger.error(f"Lỗi khi gọi OpenAI API: {response.text}")
386
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
387
+
388
  except Exception as e:
389
+ logger.error(f"Lỗi khi phân tích với OpenAI: {e}")
390
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
391
+
392
+ def _analyze_with_gemini(self, transcript, segmented_text, dialect):
393
+ """Phân tích văn bản sử dụng Gemini API"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  try:
395
+ headers = {
396
+ "Content-Type": "application/json",
397
+ }
398
+
399
+ # Tạo prompt
400
+ prompt = self._create_analysis_prompt(transcript, segmented_text, dialect)
401
+
402
+ # Endpoint Gemini
403
+ url = (
404
+ f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:generateContent?key={self.config['gemini_api_key']}"
405
+ )
406
+
407
+ # Gọi API
408
+ response = requests.post(
409
+ url,
410
+ headers=headers,
411
+ json={
412
+ "contents": [
413
+ {
414
+ "role": "user",
415
+ "parts": [{"text": prompt}],
416
+ }
417
+ ],
418
+ "generationConfig": {
419
+ "temperature": 0.4,
420
+ "maxOutputTokens": 800,
421
+ },
422
+ },
423
+ )
424
+
425
  if response.status_code == 200:
426
  result = response.json()
427
+ if "candidates" in result and len(result["candidates"]) > 0:
428
+ analysis = result["candidates"][0]["content"]["parts"][0]["text"]
429
+ return analysis
430
+ else:
431
+ logger.error(f"Định dạng phản hồi Gemini không như mong đợi: {result}")
432
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
433
+ else:
434
+ logger.error(f"Lỗi khi gọi Gemini API: {response.text}")
435
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
436
+
437
+ except Exception as e:
438
+ logger.error(f"Lỗi khi phân tích với Gemini: {e}")
439
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
440
+
441
+ def _analyze_with_local_llm(self, transcript, segmented_text, dialect):
442
+ """Phân tích văn bản sử dụng LLM mã nguồn mở local"""
443
+ try:
444
+ headers = {
445
+ "Content-Type": "application/json",
446
+ }
447
+
448
+ # Tạo prompt
449
+ prompt = self._create_analysis_prompt(transcript, segmented_text, dialect)
450
+
451
+ # Endpoint local LLM
452
+ url = f"{self.config['local_llm_endpoint']}/chat/completions"
453
+
454
+ # Gọi API
455
+ response = requests.post(
456
+ url,
457
+ headers=headers,
458
+ json={
459
+ "model": "local-model",
460
+ "messages": [
461
+ {
462
+ "role": "system",
463
+ "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.",
464
+ },
465
+ {"role": "user", "content": prompt},
466
+ ],
467
+ "temperature": 0.5,
468
+ "max_tokens": 800,
469
+ },
470
+ )
471
+
472
+ if response.status_code == 200:
473
+ result = response.json()
474
+ analysis = result["choices"][0]["message"]["content"]
475
+ return analysis
476
+ else:
477
+ logger.error(f"Lỗi khi gọi Local LLM API: {response.text}")
478
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
479
+
480
+ except Exception as e:
481
+ logger.error(f"Lỗi khi phân tích với Local LLM: {e}")
482
+ return self._rule_based_analysis(transcript, segmented_text, dialect)
483
+
484
+ def _create_analysis_prompt(self, transcript, segmented_text, dialect):
485
+ """Tạo prompt cho việc phân tích văn bản"""
486
+ 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:
487
+
488
+ Câu nói: "{transcript}"
489
+ Câu đã tách từ: "{segmented_text}"
490
+ Phương ngữ: {dialect}
491
+
492
+ Hãy phân tích theo các khía cạnh sau:
493
+ 1. Ngữ pháp: Cấu trúc câu, thì, cách sử dụng từ nối
494
+ 2. Từ vựng: Từ không phù hợp, từ dùng không đúng ngữ cảnh, từ viết tắt
495
+ 3. Phong cách: Mức độ trang trọng, thân mật, văn phong
496
+ 4. Tính mạch lạc: Tính rõ ràng, dễ hiểu của câu
497
+
498
+ Đưa ra gợi ý cụ thể để cải thiện cách diễn đạt.
499
+ Viết câu mẫu cải thiện.
500
+
501
+ Định dạng phản hồi:
502
+ - Sử dụng Markdown
503
+ - Đặt các vấn đề vào danh sách có đánh dấu
504
+ - Đưa ra câu mẫu cải thiện ở cuối"""
505
+
506
+ def _rule_based_analysis(self, transcript, segmented_text, dialect):
507
+ """Phân tích dựa trên quy tắc đơn giản"""
508
+ # Phân tích cơ bản khi không có LLM
509
+ words = transcript.split()
510
+ analysis = []
511
+
512
+ # 1. Phân tích độ dài câu
513
+ if len(words) < 3:
514
+ analysis.append("⚠️ **Câu quá ngắn**: Thử mở rộng ý với các chi tiết hơn.")
515
+ elif len(words) > 20:
516
+ analysis.append("⚠️ **Câu dài**: Cân nhắc chia thành các câu ngắn hơn.")
517
+ else:
518
+ analysis.append("✅ **Độ dài câu**: Phù hợp.")
519
+
520
+ # 2. Kiểm tra từ ngữ phổ biến
521
+ common_errors = {
522
+ "ko": "không",
523
+ "k": "không",
524
+ "bik": "biết",
525
+ "j": "gì",
526
+ "z": "vậy",
527
+ "ntn": "như thế nào",
528
+ "dc": "được",
529
+ "vs": "với",
530
+ "nc": "nước",
531
+ "ng": "người",
532
+ "trc": "trước",
533
+ "sao": "sao",
534
+ }
535
+
536
+ errors_found = []
537
+ for word in words:
538
+ word_lower = word.lower()
539
+ if word_lower in common_errors:
540
+ errors_found.append(f"'{word}' → '{common_errors[word_lower]}'")
541
+
542
+ if errors_found:
543
+ analysis.append(f"⚠️ **Từ viết tắt**: Nên dùng từ đầy đủ thay vì: {', '.join(errors_found)}")
544
+ else:
545
+ analysis.append("✅ **Sử dụng từ**: Không phát hiện từ viết tắt phổ biến.")
546
+
547
+ # 3. Tính trùng lặp
548
+ word_counts = {}
549
+ for word in words:
550
+ word_lower = word.lower()
551
+ if len(word_lower) > 1: # Bỏ qua các từ ngắn
552
+ word_counts[word_lower] = word_counts.get(word_lower, 0) + 1
553
+
554
+ duplicates = [w for w, c in word_counts.items() if c > 2]
555
+ if duplicates:
556
+ analysis.append(
557
+ 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."
558
+ )
559
+
560
+ # 4. Gợi ý cải thiện phụ thuộc phương ngữ
561
+ if dialect == "Bắc":
562
+ suggestions = [
563
+ "Phát âm rõ ràng phụ âm cuối, tránh nuốt âm",
564
+ "Chú ý tới thanh điệu, đặc biệt là thanh hỏi và thanh ngã",
565
+ "Phát âm 'r' và 'gi' phân biệt theo phong cách Bắc Bộ",
566
+ ]
567
+ elif dialect == "Trung":
568
+ suggestions = [
569
+ "Chú ý đến nhịp điệu đặc trưng của giọng Trung",
570
+ "Phát âm rõ phụ âm đầu, đặc biệt là 'tr' và 'ch'",
571
+ "Kéo dài nguyên âm một cách tự nhiên",
572
+ ]
573
+ else: # Nam
574
+ suggestions = [
575
+ "Giữ nguyên âm ổn định, tránh biến đổi nguyên âm",
576
+ "Phân biệt rõ 'v' và 'gi' theo phong cách Nam Bộ",
577
+ "Tránh nhấn quá mạnh vào các phụ âm cuối",
578
+ ]
579
+
580
+ # 5. Câu mẫu cải thiện
581
+ improved = transcript
582
+ for word, replacement in common_errors.items():
583
+ improved = improved.replace(f" {word} ", f" {replacement} ")
584
+
585
+ # Ghép tất cả phân tích lại
586
+ full_analysis = "### Phân tích\n\n" + "\n\n".join(analysis)
587
+ full_analysis += "\n\n### Gợi ý cải thiện\n\n" + "\n".join([f"- {s}" for s in suggestions])
588
+ full_analysis += f"\n\n### Câu gợi ý\n\n{improved}"
589
+ return full_analysis
590
+
591
+ def text_to_speech(self, text, dialect="Bắc"):
592
+ """Chuyển văn bản thành giọng nói"""
593
+ # Nếu có API TTS
594
+ if self.config["tts_api_url"]:
595
+ try:
596
+ # Gọi API TTS
597
+ response = requests.post(
598
+ self.config["tts_api_url"], json={"text": text, "dialect": dialect.lower()}
599
+ )
600
+
601
+ if response.status_code == 200:
602
+ # Lưu audio vào file tạm
603
+ output_file = f"data/audio/tts_{int(time.time())}.wav"
604
+ with open(output_file, "wb") as f:
605
+ f.write(response.content)
606
+ return output_file
607
+ else:
608
+ logger.error(f"Lỗi khi gọi API TTS: {response.text}")
609
+ return None
610
+ except Exception as e:
611
+ logger.error(f"Lỗi khi gọi API TTS: {e}")
612
+ return None
613
+
614
+ # Nếu có VietTTS
615
+ elif self.config["use_viettts"] and self.viettts_ready:
616
+ try:
617
+ # Chuẩn bị VietTTS
618
+ viettts_dir = "data/models/viettts"
619
+
620
+ # Tạo file tạm thời để lưu văn bản
621
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt", encoding="utf-8") as f:
622
+ f.write(text)
623
+ text_file = f.name
624
+
625
+ # Tạo tên file output
626
+ output_file = f"data/audio/tts_{int(time.time())}.wav"
627
+
628
+ # Lưu thư mục hiện tại
629
+ current_dir = os.getcwd()
630
+
631
+ try:
632
+ # Đổi thư mục làm việc sang viettts_dir
633
+ os.chdir(viettts_dir)
634
+
635
+ # Gọi VietTTS để tạo giọng nói
636
+ cmd = [
637
+ "python",
638
+ "-m",
639
+ "vietTTS.synthesizer",
640
+ "--lexicon-file=./train_data/lexicon.txt",
641
+ f"--text-file={text_file}",
642
+ f"--output={os.path.join(current_dir, output_file)}",
643
+ ]
644
+
645
+ result = subprocess.run(cmd, capture_output=True, text=True)
646
+
647
+ # Quay lại thư mục ban đầu
648
+ os.chdir(current_dir)
649
+
650
+ if result.returncode != 0:
651
+ logger.error(f"Lỗi khi chạy VietTTS: {result.stderr}")
652
+ return None
653
+
654
+ # Xóa file tạm
655
+ os.unlink(text_file)
656
+ return output_file
657
+
658
+ except Exception as e:
659
+ # Đảm bảo quay lại thư mục ban đầu
660
+ os.chdir(current_dir)
661
+ logger.error(f"Lỗi khi sử dụng VietTTS: {e}")
662
+ os.unlink(text_file)
663
+ return None
664
+
665
+ except Exception as e:
666
+ logger.error(f"Lỗi khi tạo file tạm: {e}")
667
+ return None
668
+
669
+ return None
670
+
671
+ def process_recording(self, audio_path, dialect="Bắc"):
672
+ """Xử lý bản ghi âm: chuyển sang văn bản và phân tích"""
673
+ if audio_path is None:
674
+ return "Không có âm thanh được ghi.", "", None
675
+
676
+ # 1. Chuyển đổi âm thanh thành văn bản
677
+ transcript = self.transcribe_audio(audio_path)
678
+
679
+ # 2. Phân tích văn bản
680
+ analysis = self.analyze_text(transcript, dialect)
681
+
682
+ # 3. Tạo mẫu phát âm (nếu có)
683
+ sample_audio = self.text_to_speech(transcript, dialect)
684
+
685
+ # 4. Lưu vào lịch sử phiên
686
+ entry = {
687
+ "id": len(self.session_history) + 1,
688
+ "time": time.strftime("%Y-%m-%d %H:%M:%S"),
689
+ "transcript": transcript,
690
+ "analysis": analysis,
691
+ "audio_path": audio_path,
692
+ "sample_audio": sample_audio,
693
+ "dialect": dialect,
694
+ }
695
+ self.session_history.append(entry)
696
+
697
+ # 5. Lưu lịch sử nếu được cấu hình
698
+ if self.config["save_history"]:
699
+ self._save_session_history()
700
+
701
+ return transcript, analysis, sample_audio
702
+
703
+ def evaluate_pronunciation(self, original_audio, text, dialect="Bắc"):
704
+ """Đánh giá chất lượng phát âm bằng cách so sánh với mẫu chuẩn"""
705
+ if not self.config["enable_pronunciation_eval"]:
706
+ return {"score": 0, "feedback": "Tính năng đánh giá phát âm không được bật"}
707
+
708
+ try:
709
+ # 1. Tạo phát âm mẫu từ text
710
+ sample_audio = self.text_to_speech(text, dialect)
711
+ if not sample_audio:
712
+ return {"score": 0, "feedback": "Không thể tạo mẫu phát âm chuẩn"}
713
+
714
+ # 2. Trích xuất đặc trưng từ cả hai file âm thanh
715
+ # Trích xuất MFCCs (Mel-frequency cepstral coefficients)
716
+ def extract_mfcc(audio_file):
717
+ y, sr = librosa.load(audio_file, sr=16000)
718
+ mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
719
+ return mfccs
720
+
721
+ original_mfccs = extract_mfcc(original_audio)
722
+ sample_mfccs = extract_mfcc(sample_audio)
723
+
724
+ # 3. So sánh bằng DTW (Dynamic Time Warping)
725
+ # Đơn giản hóa: tính khoảng cách Euclidean giữa hai vector MFCC
726
+ # Trong thực tế, nên dùng DTW hoặc thuật toán phức tạp hơn
727
+ def dtw_distance(mfcc1, mfcc2):
728
+ # Chỉ lấy một phần của các frames để so sánh
729
+ min_len = min(mfcc1.shape[1], mfcc2.shape[1])
730
+ dist = np.linalg.norm(mfcc1[:, :min_len] - mfcc2[:, :min_len])
731
+ return dist
732
+
733
+ distance = dtw_distance(original_mfccs, sample_mfccs)
734
+
735
+ # 4. Tính điểm dựa trên khoảng cách
736
+ max_distance = 100 # Giá trị tối đa để chuẩn hóa
737
+ normalized_distance = min(distance, max_distance) / max_distance
738
+ pronunciation_score = 100 * (1 - normalized_distance)
739
+
740
+ # 5. Phản hồi
741
+ feedback = self._get_pronunciation_feedback(pronunciation_score, dialect)
742
+
743
+ evaluation = {
744
+ "score": round(pronunciation_score, 2),
745
+ "sample_audio": sample_audio,
746
+ "feedback": feedback,
747
+ }
748
+ return evaluation
749
+
750
+ except Exception as e:
751
+ logger.error(f"Lỗi khi đánh giá phát âm: {e}")
752
+ return {"score": 0, "feedback": f"Lỗi khi đánh giá: {str(e)}"}
753
+
754
+ def _get_pronunciation_feedback(self, score, dialect):
755
+ """Đưa ra phản hồi dựa trên điểm phát âm"""
756
+ prefix = f"**Phương ngữ {dialect}**: "
757
+
758
+ if score >= 90:
759
+ return prefix + "Phát âm rất tốt! Gần như giống với mẫu chuẩn."
760
+ elif score >= 80:
761
+ return prefix + "Phát âm tốt. Có một vài điểm nhỏ cần cải thiện."
762
+ elif score >= 70:
763
+ return prefix + "Phát âm khá tốt. Hãy chú ý đến ngữ điệu và các phụ âm cuối."
764
+ elif score >= 60:
765
+ 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."
766
+ else:
767
+ 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."
768
+
769
+ def _save_session_history(self):
770
+ """Lưu lịch sử phiên hiện tại vào file"""
771
+ try:
772
+ history_file = f"data/reports/session_{self.current_session_id}.json"
773
+
774
+ # Chuyển đổi thành JSON serializable
775
+ serializable_history = []
776
+ for entry in self.session_history:
777
+ # Tạo bản sao để không thay đổi bản gốc
778
+ entry_copy = entry.copy()
779
+
780
+ # Chỉ lưu đường dẫn, không lưu nội dung file
781
+ if "audio_path" in entry_copy and entry_copy["audio_path"]:
782
+ entry_copy["audio_path"] = os.path.basename(entry_copy["audio_path"])
783
+
784
+ if "sample_audio" in entry_copy and entry_copy["sample_audio"]:
785
+ entry_copy["sample_audio"] = os.path.basename(entry_copy["sample_audio"])
786
+
787
+ serializable_history.append(entry_copy)
788
+
789
+ with open(history_file, "w", encoding="utf-8") as f:
790
+ json.dump(
791
+ {
792
+ "session_id": self.current_session_id,
793
+ "start_time": time.strftime(
794
+ "%Y-%m-%d %H:%M:%S", time.localtime(self.current_session_id)
795
+ ),
796
+ "entries": serializable_history,
797
+ },
798
+ f,
799
+ ensure_ascii=False,
800
+ indent=2,
801
+ )
802
+
803
+ except Exception as e:
804
+ logger.error(f"Lỗi khi lưu lịch sử phiên: {e}")
805
+
806
+ def export_session(self, format="markdown"):
807
+ """Xuất báo cáo buổi luyện tập"""
808
+ if not self.session_history:
809
+ return None
810
+
811
+ try:
812
+ if format == "markdown":
813
+ return self._export_markdown()
814
+ elif format == "html":
815
+ return self._export_html()
816
  else:
817
+ return self._export_markdown() # Mặc định markdown
818
  except Exception as e:
819
+ logger.error(f"Lỗi khi xuất báo cáo: {e}")
820
+ return None
821
+
822
+ def _export_markdown(self):
823
+ """Xuất báo cáo dạng Markdown"""
824
+ # Tạo nội dung báo cáo
825
+ content = "# BÁO CÁO LUYỆN NÓI TIẾNG VIỆT\n\n"
826
+ content += f"Ngày: {time.strftime('%Y-%m-%d')}\n"
827
+ content += f"Tổng số câu: {len(self.session_history)}\n\n"
828
+
829
+ for entry in self.session_history:
830
+ content += f"## Câu {entry['id']} ({entry['time']})\n\n"
831
+ content += f"**Phương ngữ:** {entry['dialect']}\n\n"
832
+ content += f"**Bạn nói:** {entry['transcript']}\n\n"
833
+ content += f"**Phân tích:**\n{entry['analysis']}\n\n"
834
+ content += "---\n\n"
835
+
836
+ # Thêm thống kê tổng quát
837
+ content += "## Thống kê tổng quát\n\n"
838
+
839
+ # Tính số từ trung bình mỗi câu
840
+ avg_words = sum(len(entry["transcript"].split()) for entry in self.session_history) / len(
841
+ self.session_history
842
+ )
843
+ content += f"- Số từ trung bình mỗi câu: {avg_words:.2f}\n"
844
+
845
+ # Lưu báo cáo
846
+ filename = f"data/reports/bao_cao_{time.strftime('%Y%m%d_%H%M%S')}.md"
847
+ with open(filename, "w", encoding="utf-8") as f:
848
+ f.write(content)
849
+ return filename
850
+
851
+ def _export_html(self):
852
+ """Xuất báo cáo dạng HTML"""
853
+ # Tạo nội dung HTML
854
+ html = """<!DOCTYPE html>
855
+ <html lang="vi">
856
+ <head>
857
+ <meta charset="UTF-8">
858
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
859
+ <title>Báo cáo luyện nói tiếng Việt</title>
860
+ <style>
861
+ body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
862
+ h1, h2 { color: #2c3e50; }
863
+ .entry { margin-bottom: 30px; border-bottom: 1px solid #eee; padding-bottom: 20px; }
864
+ .transcript { background-color: #f8f9fa; padding: 10px; border-left: 4px solid #4CAF50; }
865
+ .analysis { margin-top: 10px; }
866
+ .meta { color: #7f8c8d; font-size: 0.9em; }
867
+ .dialect { display: inline-block; background-color: #e74c3c; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; }
868
+ </style>
869
+ </head>
870
+ <body>
871
+ <h1>Báo cáo luyện nói tiếng Việt</h1>
872
+ <p>Ngày: %s</p>
873
+ <p>Tổng số câu: %d</p>
874
+
875
+ <div class="entries">
876
+ """ % (
877
+ time.strftime("%Y-%m-%d"),
878
+ len(self.session_history),
879
+ )
880
+
881
+ for entry in self.session_history:
882
+ html += f"""
883
+ <div class="entry">
884
+ <h2>Câu {entry['id']}</h2>
885
+ <div class="meta">Thời gian: {entry['time']} | <span class="dialect">{entry['dialect']}</span></div>
886
+ <div class="transcript">{entry['transcript']}</div>
887
+ <div class="analysis">{entry['analysis']}</div>
888
+ </div>
889
+ """
890
+
891
+ # Thêm thống kê
892
+ avg_words = sum(len(entry["transcript"].split()) for entry in self.session_history) / len(
893
+ self.session_history
894
+ )
895
+
896
+ html += f"""
897
+ </div>
898
+
899
+ <h2>Thống kê tổng quát</h2>
900
+ <ul>
901
+ <li>Số từ trung bình mỗi câu: {avg_words:.2f}</li>
902
+ </ul>
903
+ </body>
904
+ </html>
905
+ """
906
+
907
+ # Lưu báo cáo
908
+ filename = f"data/reports/bao_cao_{time.strftime('%Y%m%d_%H%M%S')}.html"
909
+ with open(filename, "w", encoding="utf-8") as f:
910
+ f.write(html)
911
+ return filename
912
+
913
+ def create_conversation_scenario(self):
914
+ """Tạo một tình huống hội thoại thực tế cho người dùng luyện tập"""
915
+ # Danh sách các tình huống
916
+ scenarios = [
917
+ {
918
+ "title": "Chào hỏi và giới thiệu bản thân",
919
+ "description": "Bạn gặp một người mới tại một sự kiện networking.",
920
+ "prompts": [
921
+ "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?",
922
+ "Bạn có thể chia sẻ một chút về công việc của mình được không?",
923
+ "Bạn quan tâm đến lĩnh vực nào trong sự kiện này?",
924
+ ],
925
+ },
926
+ {
927
+ "title": "Đặt món tại nhà hàng",
928
+ "description": "Bạn đang ở một nhà hàng và muốn gọi món.",
929
+ "prompts": [
930
+ "Xin chào, tôi có thể giúp gì cho bạn?",
931
+ "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.",
932
+ "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.",
933
+ ],
934
+ },
935
+ {
936
+ "title": "Phỏng vấn công việc",
937
+ "description": "Bạn đang trong một cuộc phỏng vấn xin việc.",
938
+ "prompts": [
939
+ "Chào bạn, bạn có thể giới thiệu ngắn gọn về bản thân được không?",
940
+ "Tại sao bạn muốn làm việc tại công ty chúng tôi?",
941
+ "Bạn có kinh nghiệm gì liên quan đến vị trí này không?",
942
+ ],
943
+ },
944
+ {
945
+ "title": "Thuyết trình ý tưởng",
946
+ "description": "Bạn đang thuyết trình một ý tưởng mới cho đồng nghiệp.",
947
+ "prompts": [
948
+ "Hãy giới thiệu về ý tưởng của bạn một cách ngắn gọn.",
949
+ "Ý tưởng này giải quyết vấn đề gì và đối tượng hướng đến là ai?",
950
+ "Bạn cần những nguồn lực gì để thực hiện ý tưởng này?",
951
+ ],
952
+ },
953
+ {
954
+ "title": "Hỏi đường",
955
+ "description": "Bạn đang du lịch và cần hỏi đường đến một địa điểm.",
956
+ "prompts": [
957
+ "Xin chào, tôi có thể giúp gì cho bạn?",
958
+ "Bạn đang tìm đường đến đâu?",
959
+ "Bạn muốn đi bằng phương tiện gì? Đi bộ, xe buýt hay taxi?",
960
+ ],
961
+ },
962
+ ]
963
+
964
+ # Chọn ngẫu nhiên một tình huống
965
+ scenario = random.choice(scenarios)
966
+ return scenario
967
+
968
+ def track_progress(self):
969
+ """Theo dõi tiến độ của người dùng qua thời gian"""
970
+ if not self.session_history:
971
+ return {
972
+ "message": "Chưa có dữ liệu để theo dõi tiến độ",
973
+ "statistics": {},
974
+ "charts": {},
975
+ }
976
+
977
+ # Tính toán các chỉ số tiến triển
978
+ total_entries = len(self.session_history)
979
+
980
+ # Phân tích độ dài câu qua thời gian
981
+ sentence_lengths = [len(entry["transcript"].split()) for entry in self.session_history]
982
+ avg_length = sum(sentence_lengths) / total_entries
983
 
984
+ # Tính số từ độc đáo sử dụng
985
+ all_words = []
986
+ for entry in self.session_history:
987
+ all_words.extend(entry["transcript"].lower().split())
988
+
989
+ unique_words = set(all_words)
990
+ vocabulary_size = len(unique_words)
991
+
992
+ # Tạo báo cáo tiến độ
993
+ progress_report = {
994
+ "message": "Dữ liệu theo dõi tiến độ",
995
+ "statistics": {
996
+ "total_entries": total_entries,
997
+ "avg_sentence_length": round(avg_length, 2),
998
+ "vocabulary_size": vocabulary_size,
999
+ "improvement_score": min(100, int(total_entries * 5 + vocabulary_size / 10)),
1000
+ },
1001
+ "charts": self._generate_progress_charts(),
1002
+ }
1003
+ return progress_report
1004
+
1005
+ def _generate_progress_charts(self):
1006
+ """Tạo biểu đồ trực quan hóa tiến độ"""
1007
+ # Dữ liệu cho biểu đồ
1008
+ sentence_ids = [entry["id"] for entry in self.session_history]
1009
+ sentence_lengths = [len(entry["transcript"].split()) for entry in self.session_history]
1010
+
1011
+ # Tạo biểu đồ độ dài câu
1012
+ plt.figure(figsize=(10, 5))
1013
+ plt.plot(sentence_ids, sentence_lengths, marker="o", linestyle="-")
1014
+ plt.title("Độ dài câu qua thời gian")
1015
+ plt.xlabel("Số thứ tự câu")
1016
+ plt.ylabel("Số từ trong câu")
1017
+ plt.grid(True, linestyle="--", alpha=0.7)
1018
+
1019
+ # Lưu biểu đồ vào buffer
1020
+ length_chart_buf = io.BytesIO()
1021
+ plt.savefig(length_chart_buf, format="png", dpi=100)
1022
+ length_chart_buf.seek(0)
1023
+ length_chart_b64 = base64.b64encode(length_chart_buf.read()).decode("utf-8")
1024
+ plt.close()
1025
+
1026
+ # Biểu đồ phân bố độ dài câu
1027
+ plt.figure(figsize=(8, 4))
1028
+ plt.hist(sentence_lengths, bins=10, alpha=0.7)
1029
+ plt.title("Phân bố độ dài câu")
1030
+ plt.xlabel("Số từ trong câu")
1031
+ plt.ylabel("Tần suất")
1032
+ plt.grid(True, linestyle="--", alpha=0.7)
1033
+
1034
+ dist_chart_buf = io.BytesIO()
1035
+ plt.savefig(dist_chart_buf, format="png", dpi=100)
1036
+ dist_chart_buf.seek(0)
1037
+ dist_chart_b64 = base64.b64encode(dist_chart_buf.read()).decode("utf-8")
1038
+ plt.close()
1039
+
1040
+ return {
1041
+ "length_chart": f"data:image/png;base64,{length_chart_b64}",
1042
+ "distribution_chart": f"data:image/png;base64,{dist_chart_b64}",
1043
+ }
1044
 
1045
  def clean_up(self):
1046
+ """Dọn dẹp tài nguyên trước khi thoát"""
1047
+ # Lưu lịch sử phiên cuối cùng
1048
+ if self.config["save_history"] and self.session_history:
1049
+ self._save_session_history()
1050
+
1051
+ # Dừng bộ xử lý bất đồng bộ
1052
+ if hasattr(self, "async_processor"):
1053
+ self.async_processor.stop()
1054
+
1055
+ # Giải phóng bộ nhớ GPU nếu cần
1056
  if torch.cuda.is_available():
1057
  torch.cuda.empty_cache()
 
1058
 
1059
+ logger.info("Đã dọn dẹp tài nguyên")
1060
 
1061
+
1062
+ # Tạo giao diện Gradio
1063
  def create_demo():
1064
+ try:
1065
+ trainer = VietSpeechTrainer()
1066
+
1067
+ with gr.Blocks(title="Công cụ Luyện Nói Tiếng Việt", theme=gr.themes.Soft(primary_hue="blue")) as demo:
1068
+ # Header
1069
+ with gr.Row(variant="panel"):
1070
+ with gr.Column(scale=6):
1071
+ gr.Markdown(
1072
+ """
1073
+ # 🎤 Công cụ Luyện Nói Tiếng Việt AI
1074
+ ### Nâng cao kỹ năng giao tiếp tiếng Việt với trợ lý AI thông minh
1075
+ """
1076
+ )
1077
+ with gr.Column(scale=1):
1078
+ dialect_selector = gr.Radio(["Bắc", "Trung", "Nam"], label="Phương ngữ tiếng Việt", value="Bắc")
1079
+
1080
+ # Tabs for different functions
1081
+ with gr.Tabs() as tabs:
1082
+ # Tab 1: Luyện phát âm
1083
+ with gr.TabItem("Luyện phát âm", id=0):
1084
+ with gr.Row():
1085
+ with gr.Column(scale=2):
1086
+ # Khu vực đầu vào
1087
+ audio_input = gr.Audio(
1088
+ label="📝 Giọng nói của bạn",
1089
+ type="filepath",
1090
+ source="microphone",
1091
+ format="wav",
1092
+ )
1093
+
1094
+ with gr.Row():
1095
+ submit_btn = gr.Button("🔍 Phân tích", variant="primary")
1096
+ clear_btn = gr.Button("🗑️ Xóa")
1097
+
1098
+ gr.Markdown(
1099
+ """
1100
+ ### Chủ đề gợi ý:
1101
+ - 🎯 Giới thiệu bản thân
1102
+ - 🎯 Kể về một trải nghiệm thú vị
1103
+ - 🎯 Mô tả một địa điểm yêu thích
1104
+ - 🎯 Trình bày quan điểm về một vấn đề
1105
+ """
1106
+ )
1107
+ with gr.Column(scale=3):
1108
+ # Khu vực kết quả
1109
+ transcript_output = gr.Textbox(
1110
+ label="Nội dung bạn vừa nói",
1111
+ placeholder="Nội dung sẽ hiển thị đây...",
1112
+ lines=3,
1113
+ )
1114
+ analysis_output = gr.Markdown(label="Phân tích và gợi ý cải thiện")
1115
+
1116
+ with gr.Row():
1117
+ with gr.Column(scale=1):
1118
+ gr.Markdown("#### Phát âm của bạn:")
1119
+ playback_audio = gr.Audio(label="", type="filepath")
1120
+
1121
+ with gr.Column(scale=1):
1122
+ gr.Markdown("#### Phát âm mẫu:")
1123
+ sample_audio = gr.Audio(label="", type="filepath")
1124
+
1125
+ # Lịch sử phiên
1126
+ with gr.Accordion("Lịch sử phiên luyện tập", open=False):
1127
+ history_md = gr.Markdown("*Chưa có lịch sử luyện tập*")
1128
+
1129
+ # Tab 2: Hội thoại
1130
+ with gr.TabItem("Hội thoại", id=1):
1131
+ scenario_title = gr.Markdown("## Tình huống hội thoại")
1132
+ scenario_desc = gr.Markdown("*Nhấn Tạo tình huống để bắt đầu*")
1133
+ prompt_text = gr.Markdown("*Câu hỏi/lời thoại sẽ hiển thị ở đây*")
1134
+
1135
+ conversation_audio = gr.Audio(label="Trả lời của bạn", source="microphone", type="filepath")
1136
+ conversation_transcript = gr.Textbox(label="Văn bản của bạn", lines=2)
1137
+ conversation_feedback = gr.Markdown(label="Phản hồi")
1138
+
1139
+ with gr.Row():
1140
+ new_scenario_btn = gr.Button("🔄 Tạo tình huống mới")
1141
+ next_prompt_btn = gr.Button("➡️ Câu tiếp theo")
1142
+ analyze_response_btn = gr.Button("🔍 Phân tích câu trả lời")
1143
+
1144
+ # Tab 3: Tiến độ
1145
+ with gr.TabItem("Tiến độ", id=2):
1146
+ refresh_stats_btn = gr.Button("🔄 Cập nhật thống kê")
1147
+
1148
+ with gr.Row():
1149
+ with gr.Column():
1150
+ stats_output = gr.JSON(label="Thống kê", value={"message": "Nhấn Cập nhật thống kê để xem"})
1151
+
1152
+ with gr.Row():
1153
+ with gr.Column():
1154
+ length_chart = gr.Image(label="Độ dài câu qua thời gian", show_download_button=False)
1155
+ with gr.Column():
1156
+ dist_chart = gr.Image(label="Phân bố độ dài câu", show_download_button=False)
1157
+
1158
+ # Tab 4: Xuất báo cáo
1159
+ with gr.TabItem("Xuất báo cáo", id=3):
1160
+ with gr.Row():
1161
+ export_md_btn = gr.Button("📝 Xuất báo cáo Markdown")
1162
+ export_html_btn = gr.Button("🌐 Xuất báo cáo HTML")
1163
+
1164
+ export_output = gr.File(label="Tải báo cáo")
1165
+
1166
+ # Tab 5: Thông tin
1167
+ with gr.TabItem("Thông tin", id=4):
1168
+ gr.Markdown(
1169
+ """
1170
+ ## Về công cụ luyện nói tiếng Việt
1171
+
1172
+ 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.
1173
+
1174
+ ### Công nghệ sử dụng
1175
+
1176
+ - **Speech-to-Text**: Chuyển đổi giọng nói thành văn bản với độ chính xác cao
1177
+ - PhoWhisper hoặc wav2vec2-Vietnamese
1178
+ - **Phân tích ngôn ngữ**: Phân tích cấu trúc câu, phát hiện lỗi
1179
+ - PhoBERT kết hợp với LLM (Gemini/OpenAI/Local)
1180
+ - **Text-to-Speech**: Tạo mẫu phát âm chuẩn
1181
+ - VietTTS hoặc API TTS
1182
+
1183
+ ### Tính năng chính
1184
+
1185
+ - Nhận dạng và phân tích giọng nói tiếng Việt
1186
+ - Phát hiện lỗi ngữ pháp, từ vựng và cách diễn đạt
1187
+ - Phát âm mẫu chuẩn với VietTTS
1188
+ - Lưu trữ và theo dõi tiến độ
1189
+ - Gợi ý cải thiện cá nhân hóa
1190
+ - Hỗ trợ nhiều phương ngữ (Bắc, Trung, Nam)
1191
+ - Luyện tập hội thoại với tình huống thực tế
1192
+
1193
+ ### Mô hình AI sử dụng
1194
+
1195
+ - **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.
1196
+ - **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.
1197
+ - **VietTTS**: Mô hình chuyển văn bản tiếng Việt thành giọng nói.
1198
+
1199
+ ### Hướng dẫn sử dụng
1200
+
1201
+ 1. Chọn tab "Luyện phát âm" hoặc "Hội thoại"
1202
+ 2. Thu âm giọng nói của bạn
1203
+ 3. Nhận phản hồi và gợi ý cải thiện từ AI
1204
+ 4. Theo dõi tiến độ trong tab "Tiến độ"
1205
+ 5. Xuất báo cáo để lưu lại kết quả học tập
1206
+ """
1207
+ )
1208
+
1209
+ # Xử lý sự kiện
1210
+ # 1. Tab Luyện phát âm
1211
+ def process_and_display(audio, dialect):
1212
+ if audio is None:
1213
+ return "Vui lòng thu âm trước khi phân tích.", "", None, None, None
1214
+
1215
+ # Xử lý bản ghi âm
1216
+ transcript, analysis, sample_audio_path = trainer.process_recording(audio, dialect)
1217
+
1218
+ # Cập nhật lịch sử
1219
+ history_html = update_history()
1220
+ return transcript, analysis, audio, sample_audio_path, history_html
1221
+
1222
+ def update_history():
1223
+ if not trainer.session_history:
1224
+ return "*Chưa có lịch sử luyện tập*"
1225
+ history = "### Lịch sử phiên\n\n"
1226
+ for entry in trainer.session_history[-10:]: # Chỉ hiển thị 10 mục gần nhất
1227
+ short_t = entry["transcript"][:50]
1228
+ suffix = "..." if len(entry["transcript"]) > 50 else ""
1229
+ history += f"{entry['id']}. **{entry['time']}**: {short_t}{suffix}\n"
1230
+ return history
1231
+
1232
+ def clear_inputs():
1233
+ return None, "", "", None, None
1234
+
1235
+ submit_btn.click(
1236
+ fn=process_and_display,
1237
+ inputs=[audio_input, dialect_selector],
1238
+ outputs=[transcript_output, analysis_output, playback_audio, sample_audio, history_md],
1239
+ )
1240
+
1241
+ clear_btn.click(fn=clear_inputs, inputs=[], outputs=[audio_input, transcript_output, analysis_output, playback_audio, sample_audio])
1242
+
1243
+ # 2. Tab Hội thoại
1244
+ current_scenario = gr.State(None)
1245
+ current_prompt_index = gr.State(0)
1246
+
1247
+ def load_new_scenario():
1248
+ scenario = trainer.create_conversation_scenario()
1249
+ return (
1250
+ f"## {scenario['title']}",
1251
+ f"*{scenario['description']}*",
1252
+ f"**Bot**: {scenario['prompts'][0]}",
1253
+ scenario,
1254
+ 0,
1255
+ )
1256
+
1257
+ def next_prompt(scenario, prompt_index):
1258
+ if scenario is None:
1259
+ return "Vui lòng tạo tình huống trước", prompt_index
1260
+ next_index = prompt_index + 1
1261
+ if next_index >= len(scenario["prompts"]):
1262
+ 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
1263
+ return f"**Bot**: {scenario['prompts'][next_index]}", next_index
1264
+
1265
+ def analyze_conversation_response(audio, scenario, prompt_index, dialect):
1266
+ if audio is None:
1267
+ return "Vui lòng ghi âm câu trả lời trước", ""
1268
+ if scenario is None or prompt_index >= len(scenario["prompts"]):
1269
+ return "Không có tình huống hoặc câu hỏi hợp lệ", ""
1270
+
1271
+ # Xử lý âm thanh -> văn bản
1272
+ transcript = trainer.transcribe_audio(audio)
1273
+
1274
+ # Phân tích câu trả lời trong ngữ cảnh
1275
+ context = scenario["prompts"][prompt_index]
1276
+ prompt = f"""Phân tích câu trả lời trong cuộc hội thoại:
1277
+
1278
+ Ngữ cảnh: {context}
1279
+ Câu trả lời: {transcript}
1280
+ Phương ngữ: {dialect}
1281
+
1282
+ 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.
1283
+ """
1284
+
1285
+ # Sử dụng hàm phân tích với LLM (nếu có)
1286
+ if trainer.config["llm_provider"] != "none":
1287
+ if trainer.config["llm_provider"] == "openai":
1288
+ analysis = trainer._analyze_with_openai(transcript, "", dialect)
1289
+ elif trainer.config["llm_provider"] == "gemini":
1290
+ analysis = trainer._analyze_with_gemini(transcript, "", dialect)
1291
+ elif trainer.config["llm_provider"] == "local":
1292
+ analysis = trainer._analyze_with_local_llm(transcript, "", dialect)
1293
+ else:
1294
+ analysis = trainer._rule_based_analysis(transcript, "", dialect)
1295
+
1296
+ return transcript, analysis
1297
+
1298
+ new_scenario_btn.click(
1299
+ fn=load_new_scenario,
1300
+ inputs=[],
1301
+ outputs=[scenario_title, scenario_desc, prompt_text, current_scenario, current_prompt_index],
1302
+ )
1303
+ next_prompt_btn.click(fn=next_prompt, inputs=[current_scenario, current_prompt_index], outputs=[prompt_text, current_prompt_index])
1304
+ analyze_response_btn.click(
1305
+ fn=analyze_conversation_response,
1306
+ inputs=[conversation_audio, current_scenario, current_prompt_index, dialect_selector],
1307
+ outputs=[conversation_transcript, conversation_feedback],
1308
+ )
1309
+
1310
+ # 3. Tab Tiến độ
1311
+ def update_statistics():
1312
+ progress_data = trainer.track_progress()
1313
+ stats = progress_data["statistics"]
1314
+ charts = progress_data["charts"]
1315
+ return stats, charts.get("length_chart", ""), charts.get("distribution_chart", "")
1316
+
1317
+ refresh_stats_btn.click(fn=update_statistics, inputs=[], outputs=[stats_output, length_chart, dist_chart])
1318
+
1319
+ # 4. Tab Xuất báo cáo
1320
+ def export_markdown():
1321
+ return trainer.export_session(format="markdown")
1322
+
1323
+ def export_html():
1324
+ return trainer.export_session(format="html")
1325
+
1326
+ export_md_btn.click(fn=export_markdown, inputs=[], outputs=[export_output])
1327
+ export_html_btn.click(fn=export_html, inputs=[], outputs=[export_output])
1328
+
1329
+ # Xử lý khi đóng ứng dụng
1330
+ demo.load(lambda: None, inputs=None, outputs=None)
1331
+
1332
+ return demo
1333
+ except Exception as e:
1334
+ logger.error(f"Lỗi khi tạo giao diện: {e}")
1335
+ raise
1336
 
1337
 
1338
  def main():
1339
+ try:
1340
+ # Kiểm tra tạo thư mục dữ liệu
1341
+ os.makedirs("data", exist_ok=True)
1342
+ os.makedirs("data/audio", exist_ok=True)
1343
+ os.makedirs("data/reports", exist_ok=True)
1344
+ os.makedirs("data/models", exist_ok=True)
1345
+
1346
+ # Tạo file cấu hình mẫu nếu chưa có
1347
+ if not os.path.exists("config.json"):
1348
+ sample_config = {
1349
+ "stt_model": "nguyenvulebinh/wav2vec2-base-vietnamese-250h",
1350
+ "use_phowhisper": False,
1351
+ "use_phobert": False,
1352
+ "use_vncorenlp": False,
1353
+ "llm_provider": "none",
1354
+ "use_viettts": False,
1355
+ "default_dialect": "Bắc",
1356
+ "preprocess_audio": True,
1357
+ "save_history": True,
1358
+ }
1359
+ with open("config.json", "w", encoding="utf-8") as f:
1360
+ json.dump(sample_config, f, ensure_ascii=False, indent=2)
1361
+
1362
+ # Tạo và khởi chạy ứng dụng
1363
+ demo = create_demo()
1364
+ demo.queue()
1365
+ demo.launch(share=True)
1366
+ except Exception as e:
1367
+ logger.error(f"Lỗi khi khởi chạy ứng dụng: {e}")
1368
+ print(f"Lỗi: {e}")
1369
 
1370
 
1371
  if __name__ == "__main__":
1372
  main()
1373
+
1374
+ # Cải tiến:
1375
+ # - Đánh giá ngữ điệu: Phân tích cao độ, nhịp điệu và cảm xúc trong giọng nói
1376
+ # - Tùy chỉnh giọng TTS: Cho phép ngư���i dùng chọn giọng đọc mẫu
1377
+ # - 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