Spaces:
Sleeping
Sleeping
Enhance ASR and feedback mechanisms with Vietnamese pronunciation tips
Browse files- Added VietnameseTipsProvider for detailed pronunciation guidance.
- Implemented audio quality assessment and error handling in EnhancedWav2Vec2CharacterASR.
- Improved transcription process with retry mechanism and audio validation.
- Enhanced feedback generation with specific tips for common phoneme errors.
- Updated error handling in ProductionPronunciationAssessor to provide more helpful suggestions.
- Refactored existing methods to utilize new Vietnamese tips for better user experience.
- src/agents/role_play/__pycache__/func.cpython-311.pyc +0 -0
- src/agents/role_play/__pycache__/prompt.cpython-311.pyc +0 -0
- src/agents/role_play/__pycache__/scenarios.cpython-311.pyc +0 -0
- src/apis/__pycache__/create_app.cpython-311.pyc +0 -0
- src/apis/controllers/speaking_controller.py +321 -50
- src/apis/routes/__pycache__/chat_route.cpython-311.pyc +0 -0
- src/apis/routes/__pycache__/user_route.cpython-311.pyc +0 -0
- src/config/__pycache__/llm.cpython-311.pyc +0 -0
- src/utils/__pycache__/logger.cpython-311.pyc +0 -0
- src/utils/vietnamese_tips.py +528 -0
src/agents/role_play/__pycache__/func.cpython-311.pyc
CHANGED
|
Binary files a/src/agents/role_play/__pycache__/func.cpython-311.pyc and b/src/agents/role_play/__pycache__/func.cpython-311.pyc differ
|
|
|
src/agents/role_play/__pycache__/prompt.cpython-311.pyc
CHANGED
|
Binary files a/src/agents/role_play/__pycache__/prompt.cpython-311.pyc and b/src/agents/role_play/__pycache__/prompt.cpython-311.pyc differ
|
|
|
src/agents/role_play/__pycache__/scenarios.cpython-311.pyc
CHANGED
|
Binary files a/src/agents/role_play/__pycache__/scenarios.cpython-311.pyc and b/src/agents/role_play/__pycache__/scenarios.cpython-311.pyc differ
|
|
|
src/apis/__pycache__/create_app.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/__pycache__/create_app.cpython-311.pyc and b/src/apis/__pycache__/create_app.cpython-311.pyc differ
|
|
|
src/apis/controllers/speaking_controller.py
CHANGED
|
@@ -13,10 +13,12 @@ from loguru import logger
|
|
| 13 |
import Levenshtein
|
| 14 |
from dataclasses import dataclass
|
| 15 |
from enum import Enum
|
|
|
|
| 16 |
from src.AI_Models.wave2vec_inference import (
|
| 17 |
create_inference,
|
| 18 |
export_to_onnx,
|
| 19 |
)
|
|
|
|
| 20 |
|
| 21 |
# Download required NLTK data
|
| 22 |
try:
|
|
@@ -70,9 +72,7 @@ class EnhancedWav2Vec2CharacterASR:
|
|
| 70 |
if onnx:
|
| 71 |
import os
|
| 72 |
|
| 73 |
-
model_path =
|
| 74 |
-
f"wav2vec2-large-960h-lv60-self{'.quant' if quantized else ''}.onnx"
|
| 75 |
-
)
|
| 76 |
if not os.path.exists(model_path):
|
| 77 |
export_to_onnx(model_name, quantize=quantized)
|
| 78 |
|
|
@@ -81,17 +81,71 @@ class EnhancedWav2Vec2CharacterASR:
|
|
| 81 |
model_name=model_name, use_onnx=onnx, use_onnx_quantize=quantized
|
| 82 |
)
|
| 83 |
|
| 84 |
-
def transcribe_with_features(self, audio_path: str) -> Dict:
|
| 85 |
-
"""Enhanced transcription with audio features for prosody analysis - Optimized"""
|
|
|
|
|
|
|
| 86 |
try:
|
| 87 |
start_time = time.time()
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
# Basic transcription (already fast - 0.3s)
|
| 90 |
character_transcript = self.model.file_to_text(audio_path)
|
| 91 |
character_transcript = self._clean_character_transcript(
|
| 92 |
character_transcript
|
| 93 |
)
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
# Fast phoneme conversion
|
| 96 |
phoneme_representation = self._characters_to_phoneme_representation(
|
| 97 |
character_transcript
|
|
@@ -109,11 +163,19 @@ class EnhancedWav2Vec2CharacterASR:
|
|
| 109 |
"phoneme_representation": phoneme_representation,
|
| 110 |
"audio_features": audio_features,
|
| 111 |
"confidence": self._estimate_confidence(character_transcript),
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
except Exception as e:
|
| 115 |
logger.error(f"Enhanced ASR error: {e}")
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
def _extract_basic_audio_features(self, audio_path: str) -> Dict:
|
| 119 |
"""Extract basic audio features for prosody analysis - Optimized"""
|
|
@@ -231,6 +293,119 @@ class EnhancedWav2Vec2CharacterASR:
|
|
| 231 |
if letter in letter_to_phoneme
|
| 232 |
]
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
def _estimate_confidence(self, transcript: str) -> float:
|
| 235 |
"""Estimate transcription confidence"""
|
| 236 |
if not transcript or len(transcript.strip()) < 2:
|
|
@@ -240,13 +415,8 @@ class EnhancedWav2Vec2CharacterASR:
|
|
| 240 |
return max(0.0, 1.0 - (repeated_chars * 0.2))
|
| 241 |
|
| 242 |
def _empty_result(self) -> Dict:
|
| 243 |
-
"""Empty result for error cases"""
|
| 244 |
-
return
|
| 245 |
-
"character_transcript": "",
|
| 246 |
-
"phoneme_representation": "",
|
| 247 |
-
"audio_features": {"duration": 0},
|
| 248 |
-
"confidence": 0.0,
|
| 249 |
-
}
|
| 250 |
|
| 251 |
|
| 252 |
class EnhancedG2P:
|
|
@@ -1164,7 +1334,7 @@ class EnhancedProsodyAnalyzer:
|
|
| 1164 |
return max(1, syllable_count)
|
| 1165 |
|
| 1166 |
def _empty_prosody_result(self) -> Dict:
|
| 1167 |
-
"""Return
|
| 1168 |
return {
|
| 1169 |
"pace_score": 0.5,
|
| 1170 |
"intonation_score": 0.5,
|
|
@@ -1172,7 +1342,12 @@ class EnhancedProsodyAnalyzer:
|
|
| 1172 |
"stress_score": 0.5,
|
| 1173 |
"overall_prosody": 0.5,
|
| 1174 |
"details": {},
|
| 1175 |
-
"feedback": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
}
|
| 1177 |
|
| 1178 |
|
|
@@ -1187,43 +1362,68 @@ class EnhancedFeedbackGenerator:
|
|
| 1187 |
mode: AssessmentMode,
|
| 1188 |
prosody_analysis: Dict = None,
|
| 1189 |
) -> List[str]:
|
| 1190 |
-
"""Generate comprehensive feedback based on assessment mode"""
|
| 1191 |
|
| 1192 |
feedback = []
|
| 1193 |
|
| 1194 |
-
# Overall score feedback
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
|
| 1206 |
# Mode-specific feedback
|
| 1207 |
if mode == AssessmentMode.WORD:
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
)
|
|
|
|
| 1211 |
elif mode == AssessmentMode.SENTENCE:
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
)
|
|
|
|
| 1215 |
|
| 1216 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
error_patterns = self._analyze_error_patterns(phoneme_comparisons)
|
| 1218 |
if error_patterns:
|
| 1219 |
-
feedback.extend(error_patterns)
|
| 1220 |
|
| 1221 |
return feedback
|
| 1222 |
|
| 1223 |
def _generate_word_mode_feedback(
|
| 1224 |
self, wrong_words: List[Dict], phoneme_comparisons: List[Dict]
|
| 1225 |
) -> List[str]:
|
| 1226 |
-
"""Generate feedback specific to word mode"""
|
| 1227 |
feedback = []
|
| 1228 |
|
| 1229 |
if wrong_words:
|
|
@@ -1231,11 +1431,18 @@ class EnhancedFeedbackGenerator:
|
|
| 1231 |
word = wrong_words[0]["word"]
|
| 1232 |
feedback.append(f"Từ '{word}' cần luyện tập thêm")
|
| 1233 |
|
| 1234 |
-
# Character-level feedback
|
| 1235 |
char_errors = wrong_words[0].get("character_errors", [])
|
| 1236 |
if char_errors:
|
| 1237 |
error_chars = [err.character for err in char_errors[:3]]
|
| 1238 |
feedback.append(f"Chú ý các âm: {', '.join(error_chars)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1239 |
else:
|
| 1240 |
word_list = [w["word"] for w in wrong_words[:3]]
|
| 1241 |
feedback.append(f"Các từ cần luyện: {', '.join(word_list)}")
|
|
@@ -1263,7 +1470,7 @@ class EnhancedFeedbackGenerator:
|
|
| 1263 |
return feedback
|
| 1264 |
|
| 1265 |
def _analyze_error_patterns(self, phoneme_comparisons: List[Dict]) -> List[str]:
|
| 1266 |
-
"""Analyze common error patterns across phonemes"""
|
| 1267 |
feedback = []
|
| 1268 |
|
| 1269 |
# Count error types
|
|
@@ -1276,21 +1483,18 @@ class EnhancedFeedbackGenerator:
|
|
| 1276 |
difficult_phonemes[phoneme] += 1
|
| 1277 |
error_counts[comparison["status"]] += 1
|
| 1278 |
|
| 1279 |
-
# Most problematic phoneme
|
| 1280 |
if difficult_phonemes:
|
| 1281 |
most_difficult = max(difficult_phonemes.items(), key=lambda x: x[1])
|
| 1282 |
if most_difficult[1] >= 2:
|
| 1283 |
phoneme = most_difficult[0]
|
| 1284 |
-
|
| 1285 |
-
"θ": "Lưỡi giữa răng, thổi nhẹ",
|
| 1286 |
-
"ð": "Lưỡi giữa răng, rung dây thanh",
|
| 1287 |
-
"v": "Môi dưới chạm răng trên",
|
| 1288 |
-
"r": "Cuộn lưỡi nhẹ",
|
| 1289 |
-
"z": "Như 's' nhưng rung dây thanh",
|
| 1290 |
-
}
|
| 1291 |
|
| 1292 |
-
if
|
| 1293 |
-
feedback.append(f"Âm khó nhất /{phoneme}
|
|
|
|
|
|
|
|
|
|
| 1294 |
|
| 1295 |
return feedback
|
| 1296 |
|
|
@@ -1353,8 +1557,36 @@ class ProductionPronunciationAssessor:
|
|
| 1353 |
# Step 1: Enhanced ASR transcription with features (0.3s)
|
| 1354 |
asr_result = self.asr.transcribe_with_features(audio_path)
|
| 1355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1356 |
if not asr_result["character_transcript"]:
|
| 1357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1358 |
|
| 1359 |
# Step 2: Parallel analysis processing
|
| 1360 |
future_word_analysis = self.executor.submit(
|
|
@@ -1436,7 +1668,13 @@ class ProductionPronunciationAssessor:
|
|
| 1436 |
|
| 1437 |
except Exception as e:
|
| 1438 |
logger.error(f"Production assessment error: {e}")
|
| 1439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1440 |
|
| 1441 |
def _normalize_mode(self, mode: str, reference_text: str) -> AssessmentMode:
|
| 1442 |
"""Normalize mode parameter with backward compatibility"""
|
|
@@ -1581,7 +1819,30 @@ class ProductionPronunciationAssessor:
|
|
| 1581 |
return result
|
| 1582 |
|
| 1583 |
def _create_error_result(self, error_message: str) -> Dict:
|
| 1584 |
-
"""Create error result structure"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1585 |
return {
|
| 1586 |
"transcript": "",
|
| 1587 |
"transcript_phonemes": "",
|
|
@@ -1591,8 +1852,17 @@ class ProductionPronunciationAssessor:
|
|
| 1591 |
"word_highlights": [],
|
| 1592 |
"phoneme_differences": [],
|
| 1593 |
"wrong_words": [],
|
| 1594 |
-
"feedback": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1595 |
"error": error_message,
|
|
|
|
|
|
|
|
|
|
| 1596 |
"assessment_mode": "error",
|
| 1597 |
"processing_info": {
|
| 1598 |
"processing_time": 0,
|
|
@@ -1601,6 +1871,7 @@ class ProductionPronunciationAssessor:
|
|
| 1601 |
"confidence": 0.0,
|
| 1602 |
"enhanced_features": False,
|
| 1603 |
"optimized": True,
|
|
|
|
| 1604 |
},
|
| 1605 |
}
|
| 1606 |
|
|
|
|
| 13 |
import Levenshtein
|
| 14 |
from dataclasses import dataclass
|
| 15 |
from enum import Enum
|
| 16 |
+
import os
|
| 17 |
from src.AI_Models.wave2vec_inference import (
|
| 18 |
create_inference,
|
| 19 |
export_to_onnx,
|
| 20 |
)
|
| 21 |
+
from src.utils.vietnamese_tips import vietnamese_tips
|
| 22 |
|
| 23 |
# Download required NLTK data
|
| 24 |
try:
|
|
|
|
| 72 |
if onnx:
|
| 73 |
import os
|
| 74 |
|
| 75 |
+
model_path = f"{model_name}{'.quant' if quantized else ''}.onnx"
|
|
|
|
|
|
|
| 76 |
if not os.path.exists(model_path):
|
| 77 |
export_to_onnx(model_name, quantize=quantized)
|
| 78 |
|
|
|
|
| 81 |
model_name=model_name, use_onnx=onnx, use_onnx_quantize=quantized
|
| 82 |
)
|
| 83 |
|
| 84 |
+
def transcribe_with_features(self, audio_path: str, retry_count: int = 0) -> Dict:
|
| 85 |
+
"""Enhanced transcription with audio features for prosody analysis - Optimized with retry mechanism"""
|
| 86 |
+
max_retries = 2
|
| 87 |
+
|
| 88 |
try:
|
| 89 |
start_time = time.time()
|
| 90 |
|
| 91 |
+
# Validate audio file
|
| 92 |
+
if not os.path.exists(audio_path):
|
| 93 |
+
logger.error(f"Audio file not found: {audio_path}")
|
| 94 |
+
return self._create_detailed_error_result(
|
| 95 |
+
"Không tìm thấy file âm thanh"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# Check audio file size and duration
|
| 99 |
+
try:
|
| 100 |
+
y, sr = librosa.load(audio_path, sr=self.sample_rate)
|
| 101 |
+
duration = len(y) / sr
|
| 102 |
+
|
| 103 |
+
if duration < 0.5:
|
| 104 |
+
logger.warning(f"Audio too short: {duration}s")
|
| 105 |
+
return self._create_detailed_error_result(
|
| 106 |
+
"Âm thanh quá ngắn. Hãy nói dài hơn và rõ ràng hơn."
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
if duration > 30:
|
| 110 |
+
logger.warning(f"Audio too long: {duration}s")
|
| 111 |
+
return self._create_detailed_error_result(
|
| 112 |
+
"Âm thanh quá dài. Hãy nói ngắn gọn hơn."
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Check audio volume/energy
|
| 116 |
+
rms_energy = np.sqrt(np.mean(y**2))
|
| 117 |
+
if rms_energy < 0.01: # Very quiet audio
|
| 118 |
+
logger.warning(f"Audio too quiet: RMS={rms_energy}")
|
| 119 |
+
return self._create_detailed_error_result(
|
| 120 |
+
"Âm thanh quá nhỏ. Hãy nói to hơn hoặc di chuyển gần microphone hơn."
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
except Exception as e:
|
| 124 |
+
logger.error(f"Audio preprocessing error: {e}")
|
| 125 |
+
return self._create_detailed_error_result(
|
| 126 |
+
"Lỗi xử lý âm thanh. Hãy kiểm tra định dạng file âm thanh."
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
# Basic transcription (already fast - 0.3s)
|
| 130 |
character_transcript = self.model.file_to_text(audio_path)
|
| 131 |
character_transcript = self._clean_character_transcript(
|
| 132 |
character_transcript
|
| 133 |
)
|
| 134 |
|
| 135 |
+
# Check if transcription is empty or too short
|
| 136 |
+
if not character_transcript or len(character_transcript.strip()) < 2:
|
| 137 |
+
if retry_count < max_retries:
|
| 138 |
+
logger.warning(
|
| 139 |
+
f"Empty transcription, retry {retry_count + 1}/{max_retries}"
|
| 140 |
+
)
|
| 141 |
+
time.sleep(0.1) # Brief pause before retry
|
| 142 |
+
return self.transcribe_with_features(audio_path, retry_count + 1)
|
| 143 |
+
else:
|
| 144 |
+
logger.error("No speech detected after retries")
|
| 145 |
+
return self._create_detailed_error_result(
|
| 146 |
+
"Không phát hiện được giọng nói. Hãy nói to và rõ ràng hơn, hoặc kiểm tra microphone."
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
# Fast phoneme conversion
|
| 150 |
phoneme_representation = self._characters_to_phoneme_representation(
|
| 151 |
character_transcript
|
|
|
|
| 163 |
"phoneme_representation": phoneme_representation,
|
| 164 |
"audio_features": audio_features,
|
| 165 |
"confidence": self._estimate_confidence(character_transcript),
|
| 166 |
+
"audio_quality": self._assess_audio_quality(y, sr),
|
| 167 |
}
|
| 168 |
|
| 169 |
except Exception as e:
|
| 170 |
logger.error(f"Enhanced ASR error: {e}")
|
| 171 |
+
if retry_count < max_retries:
|
| 172 |
+
logger.warning(f"ASR failed, retry {retry_count + 1}/{max_retries}")
|
| 173 |
+
time.sleep(0.2)
|
| 174 |
+
return self.transcribe_with_features(audio_path, retry_count + 1)
|
| 175 |
+
else:
|
| 176 |
+
return self._create_detailed_error_result(
|
| 177 |
+
f"Lỗi xử lý âm thanh sau {max_retries} lần thử. Hãy kiểm tra file âm thanh và thử lại."
|
| 178 |
+
)
|
| 179 |
|
| 180 |
def _extract_basic_audio_features(self, audio_path: str) -> Dict:
|
| 181 |
"""Extract basic audio features for prosody analysis - Optimized"""
|
|
|
|
| 293 |
if letter in letter_to_phoneme
|
| 294 |
]
|
| 295 |
|
| 296 |
+
def _assess_audio_quality(self, y: np.ndarray, sr: int) -> Dict:
|
| 297 |
+
"""Assess audio quality and provide feedback"""
|
| 298 |
+
try:
|
| 299 |
+
# RMS energy
|
| 300 |
+
rms_energy = np.sqrt(np.mean(y**2))
|
| 301 |
+
|
| 302 |
+
# Signal-to-noise ratio estimation
|
| 303 |
+
# Calculate energy in different frequency bands
|
| 304 |
+
stft = librosa.stft(y)
|
| 305 |
+
mag_spectrum = np.abs(stft)
|
| 306 |
+
|
| 307 |
+
# Estimate noise floor (lowest 10% of energy)
|
| 308 |
+
noise_floor = np.percentile(mag_spectrum, 10)
|
| 309 |
+
signal_energy = np.mean(mag_spectrum)
|
| 310 |
+
snr_estimate = 20 * np.log10(signal_energy / (noise_floor + 1e-10))
|
| 311 |
+
|
| 312 |
+
# Assess clipping
|
| 313 |
+
clipping_ratio = np.sum(np.abs(y) > 0.95) / len(y)
|
| 314 |
+
|
| 315 |
+
quality_score = 1.0
|
| 316 |
+
issues = []
|
| 317 |
+
|
| 318 |
+
if rms_energy < 0.01:
|
| 319 |
+
quality_score -= 0.3
|
| 320 |
+
issues.append("volume_too_low")
|
| 321 |
+
elif rms_energy > 0.8:
|
| 322 |
+
quality_score -= 0.2
|
| 323 |
+
issues.append("volume_too_high")
|
| 324 |
+
|
| 325 |
+
if snr_estimate < 10:
|
| 326 |
+
quality_score -= 0.3
|
| 327 |
+
issues.append("noisy_background")
|
| 328 |
+
|
| 329 |
+
if clipping_ratio > 0.01:
|
| 330 |
+
quality_score -= 0.4
|
| 331 |
+
issues.append("audio_clipping")
|
| 332 |
+
|
| 333 |
+
return {
|
| 334 |
+
"quality_score": max(0.0, quality_score),
|
| 335 |
+
"rms_energy": float(rms_energy),
|
| 336 |
+
"snr_estimate": float(snr_estimate),
|
| 337 |
+
"clipping_ratio": float(clipping_ratio),
|
| 338 |
+
"issues": issues,
|
| 339 |
+
"recommendations": self._get_audio_quality_recommendations(issues),
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
logger.error(f"Audio quality assessment error: {e}")
|
| 344 |
+
return {
|
| 345 |
+
"quality_score": 0.5,
|
| 346 |
+
"issues": ["assessment_failed"],
|
| 347 |
+
"recommendations": ["Thử ghi âm lại với chất lượng tốt hơn"],
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
def _get_audio_quality_recommendations(self, issues: List[str]) -> List[str]:
|
| 351 |
+
"""Get specific recommendations based on audio quality issues"""
|
| 352 |
+
recommendations = []
|
| 353 |
+
|
| 354 |
+
if "volume_too_low" in issues:
|
| 355 |
+
recommendations.extend(
|
| 356 |
+
[
|
| 357 |
+
"Nói to hơn hoặc di chuyển gần microphone hơn",
|
| 358 |
+
"Kiểm tra cài đặt âm lượng microphone",
|
| 359 |
+
]
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
if "volume_too_high" in issues:
|
| 363 |
+
recommendations.extend(
|
| 364 |
+
[
|
| 365 |
+
"Nói nhỏ hơn hoặc di chuyển xa microphone",
|
| 366 |
+
"Giảm âm lượng microphone trong cài đặt",
|
| 367 |
+
]
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
if "noisy_background" in issues:
|
| 371 |
+
recommendations.extend(
|
| 372 |
+
[
|
| 373 |
+
"Tìm nơi yên tĩnh hơn để ghi âm",
|
| 374 |
+
"Tắt các thiết bị gây tiếng ồn xung quanh",
|
| 375 |
+
]
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
if "audio_clipping" in issues:
|
| 379 |
+
recommendations.extend(
|
| 380 |
+
["Giảm âm lượng microphone", "Nói nhỏ hơn hoặc di chuyển xa microphone"]
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
return recommendations
|
| 384 |
+
|
| 385 |
+
def _create_detailed_error_result(self, error_message: str) -> Dict:
|
| 386 |
+
"""Create detailed error result with helpful tips"""
|
| 387 |
+
return {
|
| 388 |
+
"character_transcript": "",
|
| 389 |
+
"phoneme_representation": "",
|
| 390 |
+
"audio_features": {"duration": 0},
|
| 391 |
+
"confidence": 0.0,
|
| 392 |
+
"error": True,
|
| 393 |
+
"error_message": error_message,
|
| 394 |
+
"audio_quality": {
|
| 395 |
+
"quality_score": 0.0,
|
| 396 |
+
"issues": ["detection_failed"],
|
| 397 |
+
"recommendations": vietnamese_tips.get_audio_quality_tips(),
|
| 398 |
+
},
|
| 399 |
+
"retry_suggestions": [
|
| 400 |
+
vietnamese_tips.get_retry_encouragement(),
|
| 401 |
+
"Thử các bước sau:",
|
| 402 |
+
"1. Kiểm tra microphone hoạt động tốt",
|
| 403 |
+
"2. Nói to và rõ ràng hơn",
|
| 404 |
+
"3. Tìm nơi yên tĩnh để ghi âm",
|
| 405 |
+
"4. Thử ghi âm lại",
|
| 406 |
+
],
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
def _estimate_confidence(self, transcript: str) -> float:
|
| 410 |
"""Estimate transcription confidence"""
|
| 411 |
if not transcript or len(transcript.strip()) < 2:
|
|
|
|
| 415 |
return max(0.0, 1.0 - (repeated_chars * 0.2))
|
| 416 |
|
| 417 |
def _empty_result(self) -> Dict:
|
| 418 |
+
"""Empty result for error cases - deprecated, use _create_detailed_error_result instead"""
|
| 419 |
+
return self._create_detailed_error_result("Không thể xử lý âm thanh")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
|
| 422 |
class EnhancedG2P:
|
|
|
|
| 1334 |
return max(1, syllable_count)
|
| 1335 |
|
| 1336 |
def _empty_prosody_result(self) -> Dict:
|
| 1337 |
+
"""Return enhanced prosody result for error cases"""
|
| 1338 |
return {
|
| 1339 |
"pace_score": 0.5,
|
| 1340 |
"intonation_score": 0.5,
|
|
|
|
| 1342 |
"stress_score": 0.5,
|
| 1343 |
"overall_prosody": 0.5,
|
| 1344 |
"details": {},
|
| 1345 |
+
"feedback": [
|
| 1346 |
+
"Không thể phân tích ngữ điệu chi tiết.",
|
| 1347 |
+
"Hãy thử nói rõ ràng và tự nhiên hơn.",
|
| 1348 |
+
vietnamese_tips.get_retry_encouragement(),
|
| 1349 |
+
],
|
| 1350 |
+
"suggestions": vietnamese_tips.get_audio_quality_tips()[:3],
|
| 1351 |
}
|
| 1352 |
|
| 1353 |
|
|
|
|
| 1362 |
mode: AssessmentMode,
|
| 1363 |
prosody_analysis: Dict = None,
|
| 1364 |
) -> List[str]:
|
| 1365 |
+
"""Generate comprehensive feedback based on assessment mode with Vietnamese tips"""
|
| 1366 |
|
| 1367 |
feedback = []
|
| 1368 |
|
| 1369 |
+
# Overall score feedback with encouragement
|
| 1370 |
+
encouragement = vietnamese_tips.get_encouragement_message(overall_score)
|
| 1371 |
+
feedback.append(encouragement)
|
| 1372 |
+
|
| 1373 |
+
# Collect problematic phonemes for detailed tips
|
| 1374 |
+
problematic_phonemes = []
|
| 1375 |
+
for comparison in phoneme_comparisons:
|
| 1376 |
+
if comparison.get("status") in ["wrong", "substitution"]:
|
| 1377 |
+
ref_phoneme = comparison.get("reference_phoneme")
|
| 1378 |
+
if ref_phoneme and ref_phoneme not in problematic_phonemes:
|
| 1379 |
+
problematic_phonemes.append(ref_phoneme)
|
| 1380 |
|
| 1381 |
# Mode-specific feedback
|
| 1382 |
if mode == AssessmentMode.WORD:
|
| 1383 |
+
mode_feedback = self._generate_word_mode_feedback(
|
| 1384 |
+
wrong_words, phoneme_comparisons
|
| 1385 |
)
|
| 1386 |
+
feedback.extend(mode_feedback)
|
| 1387 |
elif mode == AssessmentMode.SENTENCE:
|
| 1388 |
+
mode_feedback = self._generate_sentence_mode_feedback(
|
| 1389 |
+
wrong_words, prosody_analysis
|
| 1390 |
)
|
| 1391 |
+
feedback.extend(mode_feedback)
|
| 1392 |
|
| 1393 |
+
# Add detailed Vietnamese tips for problematic phonemes
|
| 1394 |
+
if problematic_phonemes and overall_score < 0.8:
|
| 1395 |
+
vietnamese_feedback = vietnamese_tips.get_comprehensive_feedback(
|
| 1396 |
+
problematic_phonemes[:3], overall_score # Limit to top 3
|
| 1397 |
+
)
|
| 1398 |
+
|
| 1399 |
+
if vietnamese_feedback["detailed_tips"]:
|
| 1400 |
+
feedback.append("🎯 Hướng dẫn cải thiện:")
|
| 1401 |
+
for tip in vietnamese_feedback["detailed_tips"][
|
| 1402 |
+
:2
|
| 1403 |
+
]: # Limit to 2 most important
|
| 1404 |
+
tip_lines = tip["instruction"].strip().split("\n")
|
| 1405 |
+
# Add first few lines of instruction
|
| 1406 |
+
for line in tip_lines[
|
| 1407 |
+
1:4
|
| 1408 |
+
]: # Skip title, take first 3 instruction lines
|
| 1409 |
+
if line.strip():
|
| 1410 |
+
feedback.append(f" • {line.strip()}")
|
| 1411 |
+
|
| 1412 |
+
# Add pattern-based tips
|
| 1413 |
+
if vietnamese_feedback["pattern_tips"]:
|
| 1414 |
+
feedback.extend(vietnamese_feedback["pattern_tips"][:2])
|
| 1415 |
+
|
| 1416 |
+
# Common error patterns analysis
|
| 1417 |
error_patterns = self._analyze_error_patterns(phoneme_comparisons)
|
| 1418 |
if error_patterns:
|
| 1419 |
+
feedback.extend(error_patterns[:2]) # Limit to avoid overwhelming
|
| 1420 |
|
| 1421 |
return feedback
|
| 1422 |
|
| 1423 |
def _generate_word_mode_feedback(
|
| 1424 |
self, wrong_words: List[Dict], phoneme_comparisons: List[Dict]
|
| 1425 |
) -> List[str]:
|
| 1426 |
+
"""Generate feedback specific to word mode with detailed phoneme guidance"""
|
| 1427 |
feedback = []
|
| 1428 |
|
| 1429 |
if wrong_words:
|
|
|
|
| 1431 |
word = wrong_words[0]["word"]
|
| 1432 |
feedback.append(f"Từ '{word}' cần luyện tập thêm")
|
| 1433 |
|
| 1434 |
+
# Character-level feedback with phoneme guidance
|
| 1435 |
char_errors = wrong_words[0].get("character_errors", [])
|
| 1436 |
if char_errors:
|
| 1437 |
error_chars = [err.character for err in char_errors[:3]]
|
| 1438 |
feedback.append(f"Chú ý các âm: {', '.join(error_chars)}")
|
| 1439 |
+
|
| 1440 |
+
# Add specific tips for each problematic character/phoneme
|
| 1441 |
+
for char_error in char_errors[:2]: # Limit to 2 most important
|
| 1442 |
+
phoneme = char_error.expected_sound
|
| 1443 |
+
tip = vietnamese_tips.get_detailed_tip(phoneme)
|
| 1444 |
+
if tip:
|
| 1445 |
+
feedback.append(f"Âm /{phoneme}/: {tip.mouth_position}")
|
| 1446 |
else:
|
| 1447 |
word_list = [w["word"] for w in wrong_words[:3]]
|
| 1448 |
feedback.append(f"Các từ cần luyện: {', '.join(word_list)}")
|
|
|
|
| 1470 |
return feedback
|
| 1471 |
|
| 1472 |
def _analyze_error_patterns(self, phoneme_comparisons: List[Dict]) -> List[str]:
|
| 1473 |
+
"""Analyze common error patterns across phonemes with detailed Vietnamese guidance"""
|
| 1474 |
feedback = []
|
| 1475 |
|
| 1476 |
# Count error types
|
|
|
|
| 1483 |
difficult_phonemes[phoneme] += 1
|
| 1484 |
error_counts[comparison["status"]] += 1
|
| 1485 |
|
| 1486 |
+
# Most problematic phoneme with detailed Vietnamese tips
|
| 1487 |
if difficult_phonemes:
|
| 1488 |
most_difficult = max(difficult_phonemes.items(), key=lambda x: x[1])
|
| 1489 |
if most_difficult[1] >= 2:
|
| 1490 |
phoneme = most_difficult[0]
|
| 1491 |
+
tip = vietnamese_tips.get_detailed_tip(phoneme)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1492 |
|
| 1493 |
+
if tip:
|
| 1494 |
+
feedback.append(f"Âm khó nhất /{phoneme}/ ({tip.vietnamese_name})")
|
| 1495 |
+
feedback.append(f"💡 Mẹo: {tip.mouth_position}")
|
| 1496 |
+
if tip.common_mistakes:
|
| 1497 |
+
feedback.append(f"Tránh lỗi: {tip.common_mistakes[0]}")
|
| 1498 |
|
| 1499 |
return feedback
|
| 1500 |
|
|
|
|
| 1557 |
# Step 1: Enhanced ASR transcription with features (0.3s)
|
| 1558 |
asr_result = self.asr.transcribe_with_features(audio_path)
|
| 1559 |
|
| 1560 |
+
# Enhanced error handling for ASR results
|
| 1561 |
+
if asr_result.get("error", False):
|
| 1562 |
+
error_message = asr_result.get(
|
| 1563 |
+
"error_message", "Không thể xử lý âm thanh"
|
| 1564 |
+
)
|
| 1565 |
+
return self._create_enhanced_error_result(
|
| 1566 |
+
error_message,
|
| 1567 |
+
asr_result.get("retry_suggestions", []),
|
| 1568 |
+
asr_result.get("audio_quality", {}),
|
| 1569 |
+
)
|
| 1570 |
+
|
| 1571 |
if not asr_result["character_transcript"]:
|
| 1572 |
+
# Provide more helpful error message based on audio quality
|
| 1573 |
+
audio_quality = asr_result.get("audio_quality", {})
|
| 1574 |
+
quality_score = audio_quality.get("quality_score", 0.0)
|
| 1575 |
+
|
| 1576 |
+
if quality_score < 0.3:
|
| 1577 |
+
error_msg = "Chất lượng âm thanh không đủ tốt để nhận diện."
|
| 1578 |
+
suggestions = audio_quality.get("recommendations", [])
|
| 1579 |
+
else:
|
| 1580 |
+
error_msg = "Không phát hiện được giọng nói rõ ràng."
|
| 1581 |
+
suggestions = [
|
| 1582 |
+
"Hãy nói to và rõ ràng hơn",
|
| 1583 |
+
"Kiểm tra microphone có hoạt động tốt không",
|
| 1584 |
+
"Thử ghi âm trong môi trường yên tĩnh hơn",
|
| 1585 |
+
]
|
| 1586 |
+
|
| 1587 |
+
return self._create_enhanced_error_result(
|
| 1588 |
+
error_msg, suggestions, audio_quality
|
| 1589 |
+
)
|
| 1590 |
|
| 1591 |
# Step 2: Parallel analysis processing
|
| 1592 |
future_word_analysis = self.executor.submit(
|
|
|
|
| 1668 |
|
| 1669 |
except Exception as e:
|
| 1670 |
logger.error(f"Production assessment error: {e}")
|
| 1671 |
+
error_msg = f"Lỗi xử lý: {str(e)}"
|
| 1672 |
+
suggestions = [
|
| 1673 |
+
"Thử ghi âm lại với chất lượng tốt hơn",
|
| 1674 |
+
"Kiểm tra định dạng file âm thanh",
|
| 1675 |
+
"Đảm bảo file âm thanh không bị hỏng",
|
| 1676 |
+
]
|
| 1677 |
+
return self._create_enhanced_error_result(error_msg, suggestions)
|
| 1678 |
|
| 1679 |
def _normalize_mode(self, mode: str, reference_text: str) -> AssessmentMode:
|
| 1680 |
"""Normalize mode parameter with backward compatibility"""
|
|
|
|
| 1819 |
return result
|
| 1820 |
|
| 1821 |
def _create_error_result(self, error_message: str) -> Dict:
|
| 1822 |
+
"""Create error result structure - deprecated, use _create_enhanced_error_result"""
|
| 1823 |
+
return self._create_enhanced_error_result(error_message, [], {})
|
| 1824 |
+
|
| 1825 |
+
def _create_enhanced_error_result(
|
| 1826 |
+
self,
|
| 1827 |
+
error_message: str,
|
| 1828 |
+
suggestions: List[str] = None,
|
| 1829 |
+
audio_quality: Dict = None,
|
| 1830 |
+
) -> Dict:
|
| 1831 |
+
"""Create enhanced error result with helpful tips and suggestions"""
|
| 1832 |
+
|
| 1833 |
+
if suggestions is None:
|
| 1834 |
+
suggestions = vietnamese_tips.get_audio_quality_tips()
|
| 1835 |
+
|
| 1836 |
+
if audio_quality is None:
|
| 1837 |
+
audio_quality = {
|
| 1838 |
+
"quality_score": 0.0,
|
| 1839 |
+
"issues": ["unknown"],
|
| 1840 |
+
"recommendations": suggestions,
|
| 1841 |
+
}
|
| 1842 |
+
|
| 1843 |
+
# Add encouraging retry message
|
| 1844 |
+
retry_encouragement = vietnamese_tips.get_retry_encouragement()
|
| 1845 |
+
|
| 1846 |
return {
|
| 1847 |
"transcript": "",
|
| 1848 |
"transcript_phonemes": "",
|
|
|
|
| 1852 |
"word_highlights": [],
|
| 1853 |
"phoneme_differences": [],
|
| 1854 |
"wrong_words": [],
|
| 1855 |
+
"feedback": [
|
| 1856 |
+
f"❌ {error_message}",
|
| 1857 |
+
f"💪 {retry_encouragement}",
|
| 1858 |
+
"",
|
| 1859 |
+
"🔧 Gợi ý cải thiện:",
|
| 1860 |
+
*[f" • {suggestion}" for suggestion in suggestions[:4]],
|
| 1861 |
+
],
|
| 1862 |
"error": error_message,
|
| 1863 |
+
"error_type": "audio_processing",
|
| 1864 |
+
"audio_quality": audio_quality,
|
| 1865 |
+
"retry_suggestions": suggestions,
|
| 1866 |
"assessment_mode": "error",
|
| 1867 |
"processing_info": {
|
| 1868 |
"processing_time": 0,
|
|
|
|
| 1871 |
"confidence": 0.0,
|
| 1872 |
"enhanced_features": False,
|
| 1873 |
"optimized": True,
|
| 1874 |
+
"error_handled": True,
|
| 1875 |
},
|
| 1876 |
}
|
| 1877 |
|
src/apis/routes/__pycache__/chat_route.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/routes/__pycache__/chat_route.cpython-311.pyc and b/src/apis/routes/__pycache__/chat_route.cpython-311.pyc differ
|
|
|
src/apis/routes/__pycache__/user_route.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/routes/__pycache__/user_route.cpython-311.pyc and b/src/apis/routes/__pycache__/user_route.cpython-311.pyc differ
|
|
|
src/config/__pycache__/llm.cpython-311.pyc
CHANGED
|
Binary files a/src/config/__pycache__/llm.cpython-311.pyc and b/src/config/__pycache__/llm.cpython-311.pyc differ
|
|
|
src/utils/__pycache__/logger.cpython-311.pyc
CHANGED
|
Binary files a/src/utils/__pycache__/logger.cpython-311.pyc and b/src/utils/__pycache__/logger.cpython-311.pyc differ
|
|
|
src/utils/vietnamese_tips.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vietnamese Pronunciation Tips for English Learners
|
| 3 |
+
Mẹo phát âm tiếng Anh dành cho người Việt Nam
|
| 4 |
+
|
| 5 |
+
This module provides detailed pronunciation guidance in Vietnamese
|
| 6 |
+
for common English phonemes that are challenging for Vietnamese speakers.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Dict, List, Optional, Tuple
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
import random
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class PronunciationTip:
|
| 16 |
+
"""Structured pronunciation tip with detailed guidance"""
|
| 17 |
+
|
| 18 |
+
phoneme: str
|
| 19 |
+
vietnamese_name: str
|
| 20 |
+
mouth_position: str
|
| 21 |
+
tongue_position: str
|
| 22 |
+
breathing: str
|
| 23 |
+
common_mistakes: List[str]
|
| 24 |
+
practice_words: List[str]
|
| 25 |
+
detailed_instruction: str
|
| 26 |
+
difficulty_level: int # 1-5, where 5 is most difficult
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class VietnameseTipsProvider:
|
| 30 |
+
"""Provides detailed pronunciation tips in Vietnamese for English learners"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self.tips_database = self._initialize_tips_database()
|
| 34 |
+
self.error_patterns = self._initialize_error_patterns()
|
| 35 |
+
|
| 36 |
+
def _initialize_tips_database(self) -> Dict[str, PronunciationTip]:
|
| 37 |
+
"""Initialize comprehensive tips database"""
|
| 38 |
+
|
| 39 |
+
tips = {
|
| 40 |
+
# Consonants that are difficult for Vietnamese speakers
|
| 41 |
+
"θ": PronunciationTip(
|
| 42 |
+
phoneme="θ",
|
| 43 |
+
vietnamese_name="âm th không thanh (như trong 'think')",
|
| 44 |
+
mouth_position="Miệng mở vừa phải, môi tự nhiên",
|
| 45 |
+
tongue_position="Đưa lưỡi ra ngoài, đặt giữa răng trên và răng dưới",
|
| 46 |
+
breathing="Thổi khí nhẹ qua khe giữa lưỡi và răng",
|
| 47 |
+
common_mistakes=[
|
| 48 |
+
"Phát âm thành /f/ (như 'think' thành 'fink')",
|
| 49 |
+
"Phát âm thành /s/ (như 'think' thành 'sink')",
|
| 50 |
+
"Phát âm thành /t/ (như 'think' thành 'tink')",
|
| 51 |
+
],
|
| 52 |
+
practice_words=["think", "three", "thank", "thing", "throw"],
|
| 53 |
+
detailed_instruction="""
|
| 54 |
+
Cách luyện tập chi tiết:
|
| 55 |
+
1. Đưa lưỡi ra ngoài sao cho đầu lưỡi chạm nhẹ vào răng trên
|
| 56 |
+
2. Thổi khí nhẹ qua khe giữa lưỡi và răng - KHÔNG rung dây thanh
|
| 57 |
+
3. Luyện tập từ 'think' - bắt đầu chậm, sau đó tăng tốc
|
| 58 |
+
4. Đặt gương trước mặt để kiểm tra vị trí lưỡi
|
| 59 |
+
5. Luyện tập 10-15 phút mỗi ngày
|
| 60 |
+
""",
|
| 61 |
+
difficulty_level=5,
|
| 62 |
+
),
|
| 63 |
+
"ð": PronunciationTip(
|
| 64 |
+
phoneme="ð",
|
| 65 |
+
vietnamese_name="âm th có thanh (như trong 'this')",
|
| 66 |
+
mouth_position="Miệng mở vừa phải, môi tự nhiên",
|
| 67 |
+
tongue_position="Đưa lưỡi ra ngoài, đặt giữa răng trên và răng dưới",
|
| 68 |
+
breathing="Rung dây thanh và thổi khí nhẹ",
|
| 69 |
+
common_mistakes=[
|
| 70 |
+
"Phát âm thành /d/ (như 'this' thành 'dis')",
|
| 71 |
+
"Phát âm thành /z/ (như 'this' thành 'zis')",
|
| 72 |
+
"Phát âm thành /v/ (như 'this' thành 'vis')",
|
| 73 |
+
],
|
| 74 |
+
practice_words=["this", "that", "they", "there", "mother"],
|
| 75 |
+
detailed_instruction="""
|
| 76 |
+
Cách luyện tập chi tiết:
|
| 77 |
+
1. Giống như âm /θ/ nhưng phải rung dây thanh
|
| 78 |
+
2. Đặt tay lên cổ họng để cảm nhận rung động
|
| 79 |
+
3. Bắt đầu với âm /z/, sau đó đưa lưỡi ra giữa răng
|
| 80 |
+
4. Luyện cặp từ: 'sink/think' và 'zis/this'
|
| 81 |
+
5. Tập đọc câu: 'This is the thing they threw'
|
| 82 |
+
""",
|
| 83 |
+
difficulty_level=5,
|
| 84 |
+
),
|
| 85 |
+
"v": PronunciationTip(
|
| 86 |
+
phoneme="v",
|
| 87 |
+
vietnamese_name="âm v (như trong 'very')",
|
| 88 |
+
mouth_position="Môi dưới chạm nhẹ vào răng trên",
|
| 89 |
+
tongue_position="Lưỡi ở vị trí tự nhiên, không chạm răng",
|
| 90 |
+
breathing="Rung dây thanh, khí thổi qua khe môi-răng",
|
| 91 |
+
common_mistakes=[
|
| 92 |
+
"Phát âm thành /w/ (như 'very' thành 'wery')",
|
| 93 |
+
"Phát âm thành /f/ (như 'very' thành 'fery')",
|
| 94 |
+
"Dùng cả hai môi như âm /w/ trong tiếng Việt",
|
| 95 |
+
],
|
| 96 |
+
practice_words=["very", "voice", "video", "love", "have"],
|
| 97 |
+
detailed_instruction="""
|
| 98 |
+
Cách luyện tập chi tiết:
|
| 99 |
+
1. CHỈ dùng môi dưới chạm răng trên - đây là điểm khác biệt chính
|
| 100 |
+
2. Đừng dùng cả hai môi như âm 'u' trong tiếng Việt
|
| 101 |
+
3. Luyện phân biệt: 'west/vest', 'wine/vine'
|
| 102 |
+
4. Đặt ngón tay lên môi trên để tránh cử động
|
| 103 |
+
5. Tập đọc: 'Very valuable video'
|
| 104 |
+
""",
|
| 105 |
+
difficulty_level=4,
|
| 106 |
+
),
|
| 107 |
+
"w": PronunciationTip(
|
| 108 |
+
phoneme="w",
|
| 109 |
+
vietnamese_name="âm w (như trong 'water')",
|
| 110 |
+
mouth_position="Cả hai môi chu tròn như âm 'u'",
|
| 111 |
+
tongue_position="Lưỡi co lại ở phía sau",
|
| 112 |
+
breathing="Rung dây thanh, thổi qua môi tròn",
|
| 113 |
+
common_mistakes=[
|
| 114 |
+
"Phát âm thành /v/ (như 'water' thành 'vater')",
|
| 115 |
+
"Không chu môi đủ tròn",
|
| 116 |
+
"Lưỡi đặt sai vị trí",
|
| 117 |
+
],
|
| 118 |
+
practice_words=["water", "wind", "wall", "woman", "sweet"],
|
| 119 |
+
detailed_instruction="""
|
| 120 |
+
Cách luyện tập chi tiết:
|
| 121 |
+
1. Bắt đầu với âm 'u' trong tiếng Việt, sau đó chuyển sang âm tiếp theo
|
| 122 |
+
2. Cả hai môi phải chu tròn - không chỉ môi dưới
|
| 123 |
+
3. Luyện phân biệt: 'vest/west', 'vine/wine'
|
| 124 |
+
4. Tập nói chậm: 'W-A-T-E-R' rồi tăng tốc
|
| 125 |
+
5. Luyện câu: 'We went to the west wall'
|
| 126 |
+
""",
|
| 127 |
+
difficulty_level=3,
|
| 128 |
+
),
|
| 129 |
+
"r": PronunciationTip(
|
| 130 |
+
phoneme="r",
|
| 131 |
+
vietnamese_name="âm r tiếng Anh (như trong 'red')",
|
| 132 |
+
mouth_position="Miệng mở vừa, không mỉm cười",
|
| 133 |
+
tongue_position="Cuộn lưỡi lên không chạm vòm miệng",
|
| 134 |
+
breathing="Rung dây thanh, khí đi qua lưỡi cuộn",
|
| 135 |
+
common_mistakes=[
|
| 136 |
+
"Cuộn lưỡi quá mạnh như tiếng Việt",
|
| 137 |
+
"Lưỡi chạm vào vòm miệng",
|
| 138 |
+
"Phát âm thành /l/",
|
| 139 |
+
"Rung lưỡi như âm 'r' tiếng Việt",
|
| 140 |
+
],
|
| 141 |
+
practice_words=["red", "run", "right", "car", "very"],
|
| 142 |
+
detailed_instruction="""
|
| 143 |
+
Cách luyện tập chi tiết:
|
| 144 |
+
1. Cuộn lưỡi lên NHƯNG KHÔNG chạm vào vòm miệng
|
| 145 |
+
2. Âm /r/ tiếng Anh mềm hơn nhiều so với tiếng Việt
|
| 146 |
+
3. Luyện phân biệt: 'light/right', 'long/wrong'
|
| 147 |
+
4. Bắt đầu với âm 'ơ' rồi cuộn lưỡi nhẹ
|
| 148 |
+
5. Tập từ cuối: 'car', 'far', 'star' - đây dễ hơn
|
| 149 |
+
""",
|
| 150 |
+
difficulty_level=4,
|
| 151 |
+
),
|
| 152 |
+
"l": PronunciationTip(
|
| 153 |
+
phoneme="l",
|
| 154 |
+
vietnamese_name="âm l tiếng Anh (như trong 'love')",
|
| 155 |
+
mouth_position="Miệng mở tự nhiên",
|
| 156 |
+
tongue_position="Đầu lưỡi chạm vào nướu răng trên",
|
| 157 |
+
breathing="Rung dây thanh, khí đi qua hai bên lưỡi",
|
| 158 |
+
common_mistakes=[
|
| 159 |
+
"Phát âm thành /r/",
|
| 160 |
+
"Đầu lưỡi không chạm đúng vị trí",
|
| 161 |
+
"Phát âm quá nặng như tiếng Việt",
|
| 162 |
+
],
|
| 163 |
+
practice_words=["love", "light", "blue", "apple", "wall"],
|
| 164 |
+
detailed_instruction="""
|
| 165 |
+
Cách luyện tập chi tiết:
|
| 166 |
+
1. Đầu lưỡi phải chạm vào nướu răng trên, không phải răng
|
| 167 |
+
2. Âm /l/ tiếng Anh nhẹ hơn tiếng Việt
|
| 168 |
+
3. Luyện phân biệt: 'right/light', 'wrong/long'
|
| 169 |
+
4. Tập âm /l/ cuối từ: 'wall', 'ball', 'call'
|
| 170 |
+
5. Luyện câu: 'Lucy loves blue apples'
|
| 171 |
+
""",
|
| 172 |
+
difficulty_level=3,
|
| 173 |
+
),
|
| 174 |
+
"z": PronunciationTip(
|
| 175 |
+
phoneme="z",
|
| 176 |
+
vietnamese_name="âm z (như trong 'zero')",
|
| 177 |
+
mouth_position="Miệng như âm /s/ nhưng rung dây thanh",
|
| 178 |
+
tongue_position="Đầu lưỡi gần răng trên nhưng không chạm",
|
| 179 |
+
breathing="Rung dây thanh, khí đi qua khe nhỏ",
|
| 180 |
+
common_mistakes=[
|
| 181 |
+
"Phát âm thành /s/ (không rung dây thanh)",
|
| 182 |
+
"Phát âm thành /dʒ/ (như 'gi' trong tiếng Việt)",
|
| 183 |
+
"Quá to, quá rõ",
|
| 184 |
+
],
|
| 185 |
+
practice_words=["zero", "zoo", "easy", "buzz", "maze"],
|
| 186 |
+
detailed_instruction="""
|
| 187 |
+
Cách luyện tập chi tiết:
|
| 188 |
+
1. Giống hệt âm /s/ nhưng phải rung dây thanh
|
| 189 |
+
2. Đặt tay lên cổ họng để kiểm tra rung động
|
| 190 |
+
3. Luyện cặp: 'sip/zip', 'seal/zeal'
|
| 191 |
+
4. Bắt đầu với âm 's' rồi thêm rung dây thanh
|
| 192 |
+
5. Tập câu: 'Zero zebras in the zoo'
|
| 193 |
+
""",
|
| 194 |
+
difficulty_level=3,
|
| 195 |
+
),
|
| 196 |
+
"ʒ": PronunciationTip(
|
| 197 |
+
phoneme="ʒ",
|
| 198 |
+
vietnamese_name="âm zh (như trong 'measure')",
|
| 199 |
+
mouth_position="Môi chu tròn nhẹ",
|
| 200 |
+
tongue_position="Đầu lưỡi gần vòm miệng nhưng không chạm",
|
| 201 |
+
breathing="Rung dây thanh, khí đi qua khe rộng hơn /z/",
|
| 202 |
+
common_mistakes=[
|
| 203 |
+
"Phát âm thành /ʃ/ (như 'sh' nhưng không thanh)",
|
| 204 |
+
"Phát âm thành /z/",
|
| 205 |
+
"Quá giống âm 'gi' tiếng Việt",
|
| 206 |
+
],
|
| 207 |
+
practice_words=["measure", "pleasure", "vision", "usual", "garage"],
|
| 208 |
+
detailed_instruction="""
|
| 209 |
+
Cách luyện tập chi tiết:
|
| 210 |
+
1. Giống âm /ʃ/ (sh) nhưng có rung dây thanh
|
| 211 |
+
2. Luyện cặp: 'ship/vision', 'cash/casual'
|
| 212 |
+
3. Bắt đầu với 'sh' rồi thêm thanh âm
|
| 213 |
+
4. Âm này ít gặp ở đầu từ, nhiều ở giữa và cuối
|
| 214 |
+
5. Tập câu: 'It's my pleasure to measure'
|
| 215 |
+
""",
|
| 216 |
+
difficulty_level=4,
|
| 217 |
+
),
|
| 218 |
+
"ʃ": PronunciationTip(
|
| 219 |
+
phoneme="ʃ",
|
| 220 |
+
vietnamese_name="âm sh (như trong 'ship')",
|
| 221 |
+
mouth_position="Môi chu tròn nhẹ, hơi thụt vào",
|
| 222 |
+
tongue_position="Lưỡi cong lên gần vòm miệng",
|
| 223 |
+
breathing="Không rung dây thanh, thổi khí mạnh",
|
| 224 |
+
common_mistakes=[
|
| 225 |
+
"Phát âm thành /s/",
|
| 226 |
+
"Không chu môi",
|
| 227 |
+
"Lưỡi đặt sai vị trí",
|
| 228 |
+
],
|
| 229 |
+
practice_words=["ship", "shoe", "wash", "fish", "shell"],
|
| 230 |
+
detailed_instruction="""
|
| 231 |
+
Cách luyện tập chi tiết:
|
| 232 |
+
1. Chu môi như chuẩn bị huýt sáo nhưng nhẹ hơn
|
| 233 |
+
2. Lưỡi cong lên, tạo khoảng rộng hơn âm /s/
|
| 234 |
+
3. Luyện phân biệt: 'sip/ship', 'sass/shash'
|
| 235 |
+
4. Âm /ʃ/ dài hơn và êm hơn âm /s/
|
| 236 |
+
5. Tập câu: 'She sells seashells'
|
| 237 |
+
""",
|
| 238 |
+
difficulty_level=3,
|
| 239 |
+
),
|
| 240 |
+
# Vowels challenging for Vietnamese speakers
|
| 241 |
+
"æ": PronunciationTip(
|
| 242 |
+
phoneme="æ",
|
| 243 |
+
vietnamese_name="âm ae (như trong 'cat')",
|
| 244 |
+
mouth_position="Miệng mở rộng, góc miệng kéo ra",
|
| 245 |
+
tongue_position="Lưỡi thấp, phẳng ở đáy miệng",
|
| 246 |
+
breathing="Rung dây thanh",
|
| 247 |
+
common_mistakes=[
|
| 248 |
+
"Phát âm thành /ɛ/ (như 'bet')",
|
| 249 |
+
"Phát âm thành /a/ (như 'car')",
|
| 250 |
+
"Không mở miệng đủ rộng",
|
| 251 |
+
],
|
| 252 |
+
practice_words=["cat", "hat", "bad", "man", "apple"],
|
| 253 |
+
detailed_instruction="""
|
| 254 |
+
Cách luyện tập chi tiết:
|
| 255 |
+
1. Mở miệng rộng như đang cười nhưng kéo dài
|
| 256 |
+
2. Lưỡi phải thấp xuống đáy miệng
|
| 257 |
+
3. Luyện phân biệt: 'bet/bat', 'pen/pan'
|
| 258 |
+
4. Âm này không có trong tiếng Việt
|
| 259 |
+
5. Tập câu: 'The fat cat sat on the mat'
|
| 260 |
+
""",
|
| 261 |
+
difficulty_level=4,
|
| 262 |
+
),
|
| 263 |
+
"ɪ": PronunciationTip(
|
| 264 |
+
phoneme="ɪ",
|
| 265 |
+
vietnamese_name="âm i ngắn (như trong 'bit')",
|
| 266 |
+
mouth_position="Miệng mở hẹp hơn âm /i/",
|
| 267 |
+
tongue_position="Lưỡi hơi thấp hơn âm /i/ dài",
|
| 268 |
+
breathing="Rung dây thanh, âm ngắn",
|
| 269 |
+
common_mistakes=[
|
| 270 |
+
"Phát âm thành /i/ dài (như 'bee')",
|
| 271 |
+
"Kéo dài âm quá mức",
|
| 272 |
+
"Lưỡi đặt quá cao",
|
| 273 |
+
],
|
| 274 |
+
practice_words=["bit", "sit", "big", "fish", "list"],
|
| 275 |
+
detailed_instruction="""
|
| 276 |
+
Cách luyện tập chi tiết:
|
| 277 |
+
1. Ngắn và thư giãn hơn âm /i/ dài
|
| 278 |
+
2. Miệng mở hơi rộng hơn âm /i/
|
| 279 |
+
3. Luyện phân biệt: 'bit/beat', 'sit/seat'
|
| 280 |
+
4. Đừng kéo dài như âm 'i' tiếng Việt
|
| 281 |
+
5. Tập câu: 'The big fish sits in the dish'
|
| 282 |
+
""",
|
| 283 |
+
difficulty_level=3,
|
| 284 |
+
),
|
| 285 |
+
"ʊ": PronunciationTip(
|
| 286 |
+
phoneme="ʊ",
|
| 287 |
+
vietnamese_name="âm u ngắn (như trong 'book')",
|
| 288 |
+
mouth_position="Môi chu tròn nhẹ, không căng",
|
| 289 |
+
tongue_position="Lưỡi co lại ở phía sau, thấp hơn /u/",
|
| 290 |
+
breathing="Rung dây thanh, âm ngắn",
|
| 291 |
+
common_mistakes=[
|
| 292 |
+
"Phát âm thành /u/ dài (như 'boot')",
|
| 293 |
+
"Chu môi quá mạnh",
|
| 294 |
+
"Kéo dài âm",
|
| 295 |
+
],
|
| 296 |
+
practice_words=["book", "look", "good", "put", "could"],
|
| 297 |
+
detailed_instruction="""
|
| 298 |
+
Cách luyện tập chi tiết:
|
| 299 |
+
1. Thư giãn hơn âm /u/ dài
|
| 300 |
+
2. Môi chu tròn nhẹ, không căng
|
| 301 |
+
3. Luyện phân biệt: 'book/boot', 'look/Luke'
|
| 302 |
+
4. Âm ngắn và êm dịu
|
| 303 |
+
5. Tập câu: 'Look at the good book'
|
| 304 |
+
""",
|
| 305 |
+
difficulty_level=3,
|
| 306 |
+
),
|
| 307 |
+
"ŋ": PronunciationTip(
|
| 308 |
+
phoneme="ŋ",
|
| 309 |
+
vietnamese_name="âm ng (như trong 'sing')",
|
| 310 |
+
mouth_position="Miệng mở tự nhiên",
|
| 311 |
+
tongue_position="Phần sau lưỡi chạm vào vòm miệng mềm",
|
| 312 |
+
breathing="Rung dây thanh, khí đi qua mũi",
|
| 313 |
+
common_mistakes=[
|
| 314 |
+
"Thêm âm /g/ ở cuối (sing thành 'singg')",
|
| 315 |
+
"Phát âm thành /n/",
|
| 316 |
+
"Không dùng mũi",
|
| 317 |
+
],
|
| 318 |
+
practice_words=["sing", "ring", "long", "thing", "walking"],
|
| 319 |
+
detailed_instruction="""
|
| 320 |
+
Cách luyện tập chi tiết:
|
| 321 |
+
1. Phần sau lưỡi chạm vòm miệng mềm - như chuẩn bị nuốt
|
| 322 |
+
2. KHÔNG phát âm /g/ ở cuối
|
| 323 |
+
3. Khí ra qua mũi, không qua miệng
|
| 324 |
+
4. Luyện: 'sin' vs 'sing' - cảm nhận khác biệt
|
| 325 |
+
5. Tập câu: 'The king is singing a long song'
|
| 326 |
+
""",
|
| 327 |
+
difficulty_level=2,
|
| 328 |
+
),
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
return tips
|
| 332 |
+
|
| 333 |
+
def _initialize_error_patterns(self) -> Dict[str, List[str]]:
|
| 334 |
+
"""Initialize common error patterns for Vietnamese speakers"""
|
| 335 |
+
return {
|
| 336 |
+
"th_sounds": [
|
| 337 |
+
"Âm /θ/ và /ð/ là khó nhất với người Việt - hãy kiên nhẫn luyện tập",
|
| 338 |
+
"Đặt gương trước mặt để kiểm tra vị trí lưỡi khi phát âm /θ/ và /ð/",
|
| 339 |
+
"Luyện tập /θ/ và /ð/ ít nhất 10 phút mỗi ngày",
|
| 340 |
+
],
|
| 341 |
+
"v_w_confusion": [
|
| 342 |
+
"Nhớ: âm /v/ chỉ dùng môi dưới, âm /w/ dùng cả hai môi",
|
| 343 |
+
"Luyện cặp từ: 'very/wary', 'vest/west' để phân biệt /v/ và /w/",
|
| 344 |
+
"Đặt ngón tay lên môi trên khi tập âm /v/ để tránh cử động",
|
| 345 |
+
],
|
| 346 |
+
"r_l_confusion": [
|
| 347 |
+
"Âm /r/ tiếng Anh không rung lưỡi như tiếng Việt",
|
| 348 |
+
"Âm /l/ cần đầu lưỡi chạm nướu răng trên",
|
| 349 |
+
"Luyện từ có cả /r/ và /l/: 'really', 'library', 'world'",
|
| 350 |
+
],
|
| 351 |
+
"final_consonants": [
|
| 352 |
+
"Phát âm âm cuối rõ ràng nhưng không thêm nguyên âm",
|
| 353 |
+
"Tập nhóm âm cuối: -st, -nd, -th, -ng",
|
| 354 |
+
"Đừng nuốt âm cuối như trong tiếng Việt",
|
| 355 |
+
],
|
| 356 |
+
"vowel_length": [
|
| 357 |
+
"Phân biệt nguyên âm dài/ngắn: /i/ vs /ɪ/, /u/ vs /ʊ/",
|
| 358 |
+
"Nguyên âm tiếng Anh có nhiều biến thể hơn tiếng Việt",
|
| 359 |
+
"Luyện các cặp tối thiểu: 'bit/beat', 'book/boot'",
|
| 360 |
+
],
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
def get_detailed_tip(self, phoneme: str) -> Optional[PronunciationTip]:
|
| 364 |
+
"""Get detailed pronunciation tip for a specific phoneme"""
|
| 365 |
+
return self.tips_database.get(phoneme)
|
| 366 |
+
|
| 367 |
+
def get_error_pattern_tips(self, error_phonemes: List[str]) -> List[str]:
|
| 368 |
+
"""Get tips based on common error patterns"""
|
| 369 |
+
tips = []
|
| 370 |
+
|
| 371 |
+
# Analyze error patterns
|
| 372 |
+
has_th_sounds = any(p in ["θ", "ð"] for p in error_phonemes)
|
| 373 |
+
has_v_w = any(p in ["v", "w"] for p in error_phonemes)
|
| 374 |
+
has_r_l = any(p in ["r", "l"] for p in error_phonemes)
|
| 375 |
+
|
| 376 |
+
if has_th_sounds:
|
| 377 |
+
tips.extend(random.sample(self.error_patterns["th_sounds"], 2))
|
| 378 |
+
|
| 379 |
+
if has_v_w:
|
| 380 |
+
tips.extend(random.sample(self.error_patterns["v_w_confusion"], 1))
|
| 381 |
+
|
| 382 |
+
if has_r_l:
|
| 383 |
+
tips.extend(random.sample(self.error_patterns["r_l_confusion"], 1))
|
| 384 |
+
|
| 385 |
+
return tips
|
| 386 |
+
|
| 387 |
+
def get_comprehensive_feedback(
|
| 388 |
+
self, incorrect_phonemes: List[str], difficulty_threshold: float = 0.7
|
| 389 |
+
) -> Dict[str, any]:
|
| 390 |
+
"""
|
| 391 |
+
Generate comprehensive feedback with detailed tips
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
incorrect_phonemes: List of phonemes that were pronounced incorrectly
|
| 395 |
+
difficulty_threshold: Score threshold below which to provide detailed tips
|
| 396 |
+
|
| 397 |
+
Returns:
|
| 398 |
+
Dictionary with comprehensive feedback and tips
|
| 399 |
+
"""
|
| 400 |
+
|
| 401 |
+
if not incorrect_phonemes:
|
| 402 |
+
return {
|
| 403 |
+
"status": "excellent",
|
| 404 |
+
"message": "Phát âm xuất sắc! Bạn đã phát âm chính xác.",
|
| 405 |
+
"tips": [],
|
| 406 |
+
"practice_suggestions": [
|
| 407 |
+
"Tiếp tục luyện tập để duy trì độ chính xác",
|
| 408 |
+
"Thử các từ khó hơn để thách thức bản thân",
|
| 409 |
+
],
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
# Get detailed tips for each problematic phoneme
|
| 413 |
+
detailed_tips = []
|
| 414 |
+
practice_words = set()
|
| 415 |
+
|
| 416 |
+
for phoneme in incorrect_phonemes[:3]: # Limit to top 3 problematic phonemes
|
| 417 |
+
tip = self.get_detailed_tip(phoneme)
|
| 418 |
+
if tip:
|
| 419 |
+
detailed_tips.append(
|
| 420 |
+
{
|
| 421 |
+
"phoneme": phoneme,
|
| 422 |
+
"vietnamese_name": tip.vietnamese_name,
|
| 423 |
+
"instruction": tip.detailed_instruction,
|
| 424 |
+
"common_mistakes": tip.common_mistakes,
|
| 425 |
+
"difficulty": tip.difficulty_level,
|
| 426 |
+
}
|
| 427 |
+
)
|
| 428 |
+
practice_words.update(tip.practice_words[:3])
|
| 429 |
+
|
| 430 |
+
# Get pattern-based tips
|
| 431 |
+
pattern_tips = self.get_error_pattern_tips(incorrect_phonemes)
|
| 432 |
+
|
| 433 |
+
# Generate practice suggestions
|
| 434 |
+
practice_suggestions = [
|
| 435 |
+
f"Luyện tập các từ này: {', '.join(list(practice_words)[:5])}",
|
| 436 |
+
"Tập phát âm trước gương để kiểm tra vị trí môi và lưỡi",
|
| 437 |
+
"Thu âm và nghe lại để so sánh với bản gốc",
|
| 438 |
+
"Luyện tập 10-15 phút mỗi ngày cho mỗi âm kh��",
|
| 439 |
+
]
|
| 440 |
+
|
| 441 |
+
return {
|
| 442 |
+
"status": "needs_improvement",
|
| 443 |
+
"message": f"Cần cải thiện {len(incorrect_phonemes)} âm. Đừng nản lòng, cứ luyện tập từ từ!",
|
| 444 |
+
"detailed_tips": detailed_tips,
|
| 445 |
+
"pattern_tips": pattern_tips,
|
| 446 |
+
"practice_suggestions": practice_suggestions,
|
| 447 |
+
"priority_phonemes": incorrect_phonemes[
|
| 448 |
+
:2
|
| 449 |
+
], # Focus on top 2 most problematic
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
def get_encouragement_message(self, score: float) -> str:
|
| 453 |
+
"""Get encouraging message based on score"""
|
| 454 |
+
|
| 455 |
+
if score >= 0.9:
|
| 456 |
+
messages = [
|
| 457 |
+
"Tuyệt vời! Phát âm của bạn rất chuẩn!",
|
| 458 |
+
"Xuất sắc! Bạn đã tiến bộ rất nhiều!",
|
| 459 |
+
"Hoàn hảo! Tiếp tục duy trì nhé!",
|
| 460 |
+
]
|
| 461 |
+
elif score >= 0.8:
|
| 462 |
+
messages = [
|
| 463 |
+
"Rất tốt! Chỉ còn một vài điểm nhỏ cần cải thiện.",
|
| 464 |
+
"Tuyệt! Bạn đang trên đúng con đường.",
|
| 465 |
+
"Giỏi lắm! Tiếp tục cố gắng nhé!",
|
| 466 |
+
]
|
| 467 |
+
elif score >= 0.6:
|
| 468 |
+
messages = [
|
| 469 |
+
"Khá tốt! Hãy tập trung vào những âm được gạch chân.",
|
| 470 |
+
"Bạn đang tiến bộ! Cứ luyện tập đều đặn.",
|
| 471 |
+
"Tốt đấy! Từ từ sẽ chuẩn thôi.",
|
| 472 |
+
]
|
| 473 |
+
elif score >= 0.4:
|
| 474 |
+
messages = [
|
| 475 |
+
"Đừng nản lòng! Mọi người đều phải trải qua giai đoạn này.",
|
| 476 |
+
"Tiếp tục cố gắng! Luyện tập là chìa khóa thành công.",
|
| 477 |
+
"Bạn đang học! Mỗi lần luyện là một bước tiến.",
|
| 478 |
+
]
|
| 479 |
+
else:
|
| 480 |
+
messages = [
|
| 481 |
+
"Bạn đang bắt đầu hành trình! Hãy luyện chậm và rõ ràng.",
|
| 482 |
+
"Đừng vội! Phát âm cần thời gian để hoàn thiện.",
|
| 483 |
+
"Cứ bình tĩnh luyện tập, bạn sẽ tiến bộ nhanh thôi!",
|
| 484 |
+
]
|
| 485 |
+
|
| 486 |
+
return random.choice(messages)
|
| 487 |
+
|
| 488 |
+
def get_audio_quality_tips(self) -> List[str]:
|
| 489 |
+
"""Get tips for better audio recording quality"""
|
| 490 |
+
return [
|
| 491 |
+
"Nói to và rõ ràng hơn - microphone cần âm thanh đủ mạnh",
|
| 492 |
+
"Tìm nơi yên tĩnh để thu âm, tránh tiếng ồn xung quanh",
|
| 493 |
+
"Giữ khoảng cách 15-20cm từ miệng đến microphone",
|
| 494 |
+
"Nói chậm rãi và phát âm từng âm một cách rõ ràng",
|
| 495 |
+
"Kiểm tra microphone có hoạt động tốt không",
|
| 496 |
+
"Thử thu âm lại nếu âm thanh không rõ ràng",
|
| 497 |
+
]
|
| 498 |
+
|
| 499 |
+
def get_retry_encouragement(self) -> str:
|
| 500 |
+
"""Get encouraging message for retry attempts"""
|
| 501 |
+
messages = [
|
| 502 |
+
"Không sao! Hãy thử lại và nói to hơn một chút.",
|
| 503 |
+
"Microphone không nghe rõ. Bạn có thể nói rõ ràng hơn không?",
|
| 504 |
+
"Thử nói chậm và to hơn để hệ thống có thể nhận diện tốt hơn.",
|
| 505 |
+
"Đừng lo! Hãy thở sâu và thử lại một lần nữa.",
|
| 506 |
+
"Có thể do tiếng ồn xung quanh. Hãy tìm nơi yên tĩnh hơn và thử lại.",
|
| 507 |
+
]
|
| 508 |
+
return random.choice(messages)
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# Global instance for easy access
|
| 512 |
+
vietnamese_tips = VietnameseTipsProvider()
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def get_phoneme_tip(phoneme: str) -> Optional[str]:
|
| 516 |
+
"""Quick access function to get tip for a phoneme"""
|
| 517 |
+
tip = vietnamese_tips.get_detailed_tip(phoneme)
|
| 518 |
+
return tip.detailed_instruction if tip else None
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def get_quick_tips(phonemes: List[str]) -> List[str]:
|
| 522 |
+
"""Get quick tips for a list of phonemes"""
|
| 523 |
+
tips = []
|
| 524 |
+
for phoneme in phonemes[:3]: # Limit to 3 most important
|
| 525 |
+
tip = vietnamese_tips.get_detailed_tip(phoneme)
|
| 526 |
+
if tip:
|
| 527 |
+
tips.append(f"Âm /{phoneme}/: {tip.vietnamese_name} - {tip.mouth_position}")
|
| 528 |
+
return tips
|