ABAO77 commited on
Commit
5a412ce
·
1 Parent(s): 3fde6b6

Implement Lesson Practice 2 Agent with state management and routing logic; add practice and teaching agent functions; create prompts for conversation and teaching; establish pronunciation API with lesson data and search functionality.

Browse files
.gitattributes CHANGED
@@ -51,4 +51,5 @@ data_test
51
  **.tiff
52
  **.webp
53
  **.svg
54
- .serena
 
 
51
  **.tiff
52
  **.webp
53
  **.svg
54
+ .serena
55
+ *.wav filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -20,4 +20,4 @@ data_test
20
  **.webp
21
  **.svg
22
  .serena
23
- **.onnx
 
20
  **.webp
21
  **.svg
22
  .serena
23
+ **.onnxoutput.wav
src/agents/{lesson_practice_2 → lesson_practice}/flow.py RENAMED
@@ -45,4 +45,4 @@ class LessonPractice2Agent:
45
  return graph.compile(checkpointer=checkpointer)
46
 
47
 
48
- lesson_practice_2_agent = LessonPractice2Agent()
 
45
  return graph.compile(checkpointer=checkpointer)
46
 
47
 
48
+ lesson_practice_agent = LessonPractice2Agent()
src/agents/{lesson_practice_2 → lesson_practice}/func.py RENAMED
File without changes
src/agents/{lesson_practice_2 → lesson_practice}/prompt.py RENAMED
File without changes
src/agents/{lesson_practice_2 → lesson_practice}/tools.py RENAMED
File without changes
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/create_app.py CHANGED
@@ -4,6 +4,7 @@ from src.apis.routes.user_route import router as router_user
4
  from src.apis.routes.chat_route import router as router_chat
5
  from src.apis.routes.lesson_route import router as router_lesson
6
  from src.apis.routes.evaluation_route import router as router_evaluation
 
7
  from src.apis.routes.speaking_route import router as router_speaking
8
 
9
  api_router = APIRouter(prefix="/api")
@@ -11,6 +12,7 @@ api_router.include_router(router_user)
11
  api_router.include_router(router_chat)
12
  api_router.include_router(router_lesson)
13
  api_router.include_router(router_evaluation)
 
14
  api_router.include_router(router_speaking)
15
 
16
 
@@ -25,4 +27,4 @@ def create_app():
25
  allow_headers=["*"],
26
  )
27
 
28
- return app
 
4
  from src.apis.routes.chat_route import router as router_chat
5
  from src.apis.routes.lesson_route import router as router_lesson
6
  from src.apis.routes.evaluation_route import router as router_evaluation
7
+ from src.apis.routes.pronunciation_route import router as router_pronunciation
8
  from src.apis.routes.speaking_route import router as router_speaking
9
 
10
  api_router = APIRouter(prefix="/api")
 
12
  api_router.include_router(router_chat)
13
  api_router.include_router(router_lesson)
14
  api_router.include_router(router_evaluation)
15
+ api_router.include_router(router_pronunciation)
16
  api_router.include_router(router_speaking)
17
 
18
 
 
27
  allow_headers=["*"],
28
  )
29
 
30
+ return app
src/apis/routes/lesson_route.py CHANGED
@@ -13,7 +13,7 @@ from src.utils.logger import logger
13
  from src.services.tts_service import tts_service
14
  from pydantic import BaseModel, Field
15
  from typing import List, Dict, Any, Optional
16
- from src.agents.lesson_practice_2.flow import lesson_practice_2_agent
17
  from src.apis.models.lesson_models import Lesson, LessonResponse, LessonDetailResponse
18
  import json
19
  import os
@@ -228,7 +228,7 @@ async def chat(
228
  message = {"role": "user", "content": message_content}
229
 
230
  try:
231
- response = await lesson_practice_2_agent().ainvoke(
232
  {
233
  "messages": [message],
234
  "unit": lesson_dict.get("unit", ""),
@@ -342,7 +342,7 @@ async def chat_stream(
342
  }
343
  config = {"configurable": {"thread_id": session_id}}
344
 
345
- async for event in lesson_practice_2_agent().astream(
346
  input=input_graph,
347
  stream_mode=["messages"],
348
  config=config,
 
13
  from src.services.tts_service import tts_service
14
  from pydantic import BaseModel, Field
15
  from typing import List, Dict, Any, Optional
16
+ from src.agents.lesson_practice.flow import lesson_practice_agent
17
  from src.apis.models.lesson_models import Lesson, LessonResponse, LessonDetailResponse
18
  import json
19
  import os
 
228
  message = {"role": "user", "content": message_content}
229
 
230
  try:
231
+ response = await lesson_practice_agent().ainvoke(
232
  {
233
  "messages": [message],
234
  "unit": lesson_dict.get("unit", ""),
 
342
  }
343
  config = {"configurable": {"thread_id": session_id}}
344
 
345
+ async for event in lesson_practice_agent().astream(
346
  input=input_graph,
347
  stream_mode=["messages"],
348
  config=config,
src/apis/routes/pronunciation_route.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status
2
+ from fastapi.responses import JSONResponse
3
+ from src.utils.logger import logger
4
+ from pydantic import BaseModel
5
+ from typing import List, Dict, Any, Optional
6
+ import json
7
+ import os
8
+
9
+ router = APIRouter(prefix="/pronunciation", tags=["Pronunciation"])
10
+
11
+
12
+ class Level(BaseModel):
13
+ id: str
14
+ name: str
15
+ description: str
16
+ color: str
17
+
18
+
19
+ class Vocabulary(BaseModel):
20
+ word: str
21
+ ipa: str
22
+ vietnamese: str
23
+ audioUrl: str
24
+
25
+
26
+ class Sentence(BaseModel):
27
+ text: str
28
+ ipa: str
29
+ vietnamese: str
30
+ audioUrl: str
31
+
32
+
33
+ class PronunciationLesson(BaseModel):
34
+ id: str
35
+ title: str
36
+ description: str
37
+ level: str
38
+ vocabulary: List[Vocabulary]
39
+ sentence: Sentence
40
+
41
+
42
+ class LevelsResponse(BaseModel):
43
+ levels: List[Level]
44
+ total: int
45
+
46
+
47
+ class LessonsResponse(BaseModel):
48
+ lessons: List[PronunciationLesson]
49
+ total: int
50
+ level: str
51
+
52
+
53
+ class LessonDetailResponse(BaseModel):
54
+ lesson: PronunciationLesson
55
+
56
+
57
+ def load_pronunciation_data() -> Dict[str, Any]:
58
+ """Load pronunciation lessons data from JSON file"""
59
+ try:
60
+ data_file_path = os.path.join(
61
+ os.path.dirname(__file__), "..", "..", "data", "pronunciation_lessons.json"
62
+ )
63
+
64
+ if not os.path.exists(data_file_path):
65
+ logger.warning(f"Pronunciation lessons file not found at {data_file_path}")
66
+ return {"levels": [], "lessons": {}}
67
+
68
+ with open(data_file_path, "r", encoding="utf-8") as file:
69
+ data = json.load(file)
70
+
71
+ return data
72
+ except Exception as e:
73
+ logger.error(f"Error loading pronunciation lessons data: {str(e)}")
74
+ return {"levels": [], "lessons": {}}
75
+
76
+
77
+ @router.get("/levels", response_model=LevelsResponse)
78
+ async def get_levels():
79
+ """
80
+ Get all available levels for pronunciation practice
81
+
82
+ Returns:
83
+ LevelsResponse: Contains list of all levels and total count
84
+ """
85
+ try:
86
+ data = load_pronunciation_data()
87
+ levels_data = data.get("levels", [])
88
+
89
+ levels = [Level(**level_data) for level_data in levels_data]
90
+
91
+ return LevelsResponse(levels=levels, total=len(levels))
92
+ except Exception as e:
93
+ logger.error(f"Error retrieving levels: {str(e)}")
94
+ raise HTTPException(
95
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
96
+ detail="Failed to retrieve levels",
97
+ )
98
+
99
+
100
+ @router.get("/lessons/{level_id}", response_model=LessonsResponse)
101
+ async def get_lessons_by_level(level_id: str):
102
+ """
103
+ Get all lessons for a specific level
104
+
105
+ Args:
106
+ level_id (str): The level ID (beginner, elementary, etc.)
107
+
108
+ Returns:
109
+ LessonsResponse: Contains list of lessons for the specified level
110
+ """
111
+ try:
112
+ data = load_pronunciation_data()
113
+ lessons_data = data.get("lessons", {})
114
+
115
+ if level_id not in lessons_data:
116
+ raise HTTPException(
117
+ status_code=status.HTTP_404_NOT_FOUND,
118
+ detail=f"Level '{level_id}' not found",
119
+ )
120
+
121
+ level_lessons = lessons_data[level_id]
122
+ lessons = [PronunciationLesson(**lesson_data) for lesson_data in level_lessons]
123
+
124
+ return LessonsResponse(lessons=lessons, total=len(lessons), level=level_id)
125
+ except HTTPException:
126
+ raise
127
+ except Exception as e:
128
+ logger.error(f"Error retrieving lessons for level {level_id}: {str(e)}")
129
+ raise HTTPException(
130
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
131
+ detail="Failed to retrieve lessons",
132
+ )
133
+
134
+
135
+ @router.get("/lesson/{lesson_id}", response_model=LessonDetailResponse)
136
+ async def get_lesson_detail(lesson_id: str):
137
+ """
138
+ Get detailed information about a specific lesson
139
+
140
+ Args:
141
+ lesson_id (str): The unique identifier of the lesson
142
+
143
+ Returns:
144
+ LessonDetailResponse: Contains the lesson details
145
+ """
146
+ try:
147
+ data = load_pronunciation_data()
148
+ lessons_data = data.get("lessons", {})
149
+
150
+ # Search for the lesson across all levels
151
+ found_lesson = None
152
+ for level_id, level_lessons in lessons_data.items():
153
+ for lesson_data in level_lessons:
154
+ if lesson_data.get("id") == lesson_id:
155
+ found_lesson = lesson_data
156
+ break
157
+ if found_lesson:
158
+ break
159
+
160
+ if not found_lesson:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_404_NOT_FOUND,
163
+ detail=f"Lesson with ID '{lesson_id}' not found",
164
+ )
165
+
166
+ lesson = PronunciationLesson(**found_lesson)
167
+
168
+ return LessonDetailResponse(lesson=lesson)
169
+ except HTTPException:
170
+ raise
171
+ except Exception as e:
172
+ logger.error(f"Error retrieving lesson {lesson_id}: {str(e)}")
173
+ raise HTTPException(
174
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
175
+ detail="Failed to retrieve lesson",
176
+ )
177
+
178
+
179
+ @router.get("/search/level/{level_id}/title/{title}")
180
+ async def search_lessons_by_title(level_id: str, title: str):
181
+ """
182
+ Search lessons by title within a specific level
183
+
184
+ Args:
185
+ level_id (str): The level ID to search within
186
+ title (str): Part of the lesson title to search for
187
+
188
+ Returns:
189
+ LessonsResponse: Contains list of matching lessons
190
+ """
191
+ try:
192
+ data = load_pronunciation_data()
193
+ lessons_data = data.get("lessons", {})
194
+
195
+ if level_id not in lessons_data:
196
+ raise HTTPException(
197
+ status_code=status.HTTP_404_NOT_FOUND,
198
+ detail=f"Level '{level_id}' not found",
199
+ )
200
+
201
+ level_lessons = lessons_data[level_id]
202
+ matching_lessons = [
203
+ lesson_data for lesson_data in level_lessons
204
+ if title.lower() in lesson_data.get("title", "").lower()
205
+ ]
206
+
207
+ lessons = [PronunciationLesson(**lesson_data) for lesson_data in matching_lessons]
208
+
209
+ return LessonsResponse(lessons=lessons, total=len(lessons), level=level_id)
210
+ except HTTPException:
211
+ raise
212
+ except Exception as e:
213
+ logger.error(f"Error searching lessons by title '{title}' in level {level_id}: {str(e)}")
214
+ raise HTTPException(
215
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
216
+ detail="Failed to search lessons",
217
+ )
src/apis/routes/speaking_route.py CHANGED
@@ -1,6 +1,6 @@
1
  from fastapi import UploadFile, File, Form, HTTPException, APIRouter
2
  from pydantic import BaseModel
3
- from typing import List, Dict
4
  import tempfile
5
  import numpy as np
6
  import re
@@ -15,13 +15,16 @@ from src.utils.speaking_utils import convert_numpy_types
15
 
16
  warnings.filterwarnings("ignore")
17
 
18
- router = APIRouter(prefix="/pronunciation", tags=["Pronunciation"])
19
 
20
 
21
  class PronunciationAssessmentResult(BaseModel):
22
  transcript: str # What the user actually said (character transcript)
23
  transcript_phonemes: str # User's phonemes
24
  user_phonemes: str # Alias for transcript_phonemes for UI clarity
 
 
 
25
  character_transcript: str
26
  overall_score: float
27
  word_highlights: List[Dict]
@@ -36,7 +39,7 @@ assessor = SimplePronunciationAssessor()
36
 
37
  @router.post("/assess", response_model=PronunciationAssessmentResult)
38
  async def assess_pronunciation(
39
- audio: UploadFile = File(..., description="Audio file (.wav, .mp3, .m4a)"),
40
  reference_text: str = Form(..., description="Reference text to pronounce"),
41
  mode: str = Form(
42
  "normal",
@@ -86,13 +89,13 @@ async def assess_pronunciation(
86
  try:
87
  # Save uploaded file temporarily
88
  file_extension = ".wav"
89
- if audio.filename and "." in audio.filename:
90
- file_extension = f".{audio.filename.split('.')[-1]}"
91
 
92
  with tempfile.NamedTemporaryFile(
93
  delete=False, suffix=file_extension
94
  ) as tmp_file:
95
- content = await audio.read()
96
  tmp_file.write(content)
97
  tmp_file.flush()
98
 
@@ -101,6 +104,47 @@ async def assess_pronunciation(
101
  # Run assessment using selected mode
102
  result = assessor.assess_pronunciation(tmp_file.name, reference_text, mode)
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  # Add processing time
105
  processing_time = time.time() - start_time
106
  result["processing_info"]["processing_time"] = processing_time
@@ -128,7 +172,7 @@ async def assess_pronunciation(
128
 
129
 
130
  @router.get("/phonemes/{word}")
131
- async def get_word_phonemes(word: str):
132
  """Get phoneme breakdown for a specific word"""
133
  try:
134
  g2p = SimpleG2P()
 
1
  from fastapi import UploadFile, File, Form, HTTPException, APIRouter
2
  from pydantic import BaseModel
3
+ from typing import List, Dict, Optional, Optional
4
  import tempfile
5
  import numpy as np
6
  import re
 
15
 
16
  warnings.filterwarnings("ignore")
17
 
18
+ router = APIRouter(prefix="/speaking", tags=["Speaking"])
19
 
20
 
21
  class PronunciationAssessmentResult(BaseModel):
22
  transcript: str # What the user actually said (character transcript)
23
  transcript_phonemes: str # User's phonemes
24
  user_phonemes: str # Alias for transcript_phonemes for UI clarity
25
+ user_ipa: Optional[str] # User's IPA notation
26
+ reference_ipa: str # Reference IPA notation
27
+ reference_phonemes: str # Reference phonemes
28
  character_transcript: str
29
  overall_score: float
30
  word_highlights: List[Dict]
 
39
 
40
  @router.post("/assess", response_model=PronunciationAssessmentResult)
41
  async def assess_pronunciation(
42
+ audio_file: UploadFile = File(..., description="Audio file (.wav, .mp3, .m4a)"),
43
  reference_text: str = Form(..., description="Reference text to pronounce"),
44
  mode: str = Form(
45
  "normal",
 
89
  try:
90
  # Save uploaded file temporarily
91
  file_extension = ".wav"
92
+ if audio_file.filename and "." in audio_file.filename:
93
+ file_extension = f".{audio_file.filename.split('.')[-1]}"
94
 
95
  with tempfile.NamedTemporaryFile(
96
  delete=False, suffix=file_extension
97
  ) as tmp_file:
98
+ content = await audio_file.read()
99
  tmp_file.write(content)
100
  tmp_file.flush()
101
 
 
104
  # Run assessment using selected mode
105
  result = assessor.assess_pronunciation(tmp_file.name, reference_text, mode)
106
 
107
+ # Get reference phonemes and IPA
108
+ g2p = SimpleG2P()
109
+ reference_words = reference_text.strip().split()
110
+ reference_phonemes_list = []
111
+ reference_ipa_list = []
112
+
113
+ for word in reference_words:
114
+ word_phonemes = g2p.text_to_phonemes(word.strip('.,!?;:'))[0]
115
+ reference_phonemes_list.append(word_phonemes["phoneme_string"])
116
+ reference_ipa_list.append(word_phonemes["ipa"])
117
+
118
+ # Join phonemes and IPA for the full text
119
+ result["reference_phonemes"] = " ".join(reference_phonemes_list)
120
+ result["reference_ipa"] = " ".join(reference_ipa_list)
121
+
122
+ # Create user_ipa from transcript using G2P (same way as reference)
123
+ if "transcript" in result and result["transcript"]:
124
+ try:
125
+ user_transcript = result["transcript"].strip()
126
+ user_words = user_transcript.split()
127
+ user_ipa_list = []
128
+
129
+ for word in user_words:
130
+ clean_word = word.strip('.,!?;:').lower()
131
+ if clean_word: # Skip empty words
132
+ try:
133
+ word_phonemes = g2p.text_to_phonemes(clean_word)[0]
134
+ user_ipa_list.append(word_phonemes["ipa"])
135
+ except Exception as e:
136
+ logger.warning(f"Failed to get IPA for word '{clean_word}': {e}")
137
+ # Fallback: use the word itself
138
+ user_ipa_list.append(f"/{clean_word}/")
139
+
140
+ result["user_ipa"] = " ".join(user_ipa_list) if user_ipa_list else None
141
+ logger.info(f"Generated user IPA from transcript '{user_transcript}': '{result['user_ipa']}'")
142
+ except Exception as e:
143
+ logger.warning(f"Failed to generate user IPA from transcript: {e}")
144
+ result["user_ipa"] = None
145
+ else:
146
+ result["user_ipa"] = None
147
+
148
  # Add processing time
149
  processing_time = time.time() - start_time
150
  result["processing_info"]["processing_time"] = processing_time
 
172
 
173
 
174
  @router.get("/phonemes/{word}")
175
+ def get_word_phonemes(word: str):
176
  """Get phoneme breakdown for a specific word"""
177
  try:
178
  g2p = SimpleG2P()
src/data/pronunciation_lessons.json ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "levels": [
3
+ {
4
+ "id": "beginner",
5
+ "name": "Beginner",
6
+ "description": "Basic vocabulary and pronunciation for beginners",
7
+ "color": "#22c55e"
8
+ },
9
+ {
10
+ "id": "elementary",
11
+ "name": "Elementary",
12
+ "description": "Elementary level vocabulary building",
13
+ "color": "#3b82f6"
14
+ },
15
+ {
16
+ "id": "pre-intermediate",
17
+ "name": "Pre-Intermediate",
18
+ "description": "Pre-intermediate vocabulary and expressions",
19
+ "color": "#f59e0b"
20
+ },
21
+ {
22
+ "id": "upper-intermediate",
23
+ "name": "Upper-Intermediate",
24
+ "description": "Upper-intermediate complex vocabulary",
25
+ "color": "#f97316"
26
+ },
27
+ {
28
+ "id": "advanced",
29
+ "name": "Advanced",
30
+ "description": "Advanced vocabulary and pronunciation patterns",
31
+ "color": "#ef4444"
32
+ }
33
+ ],
34
+ "lessons": {
35
+ "beginner": [
36
+ {
37
+ "id": "beginner_1",
38
+ "title": "Basic Greetings",
39
+ "description": "Learn how to greet people in English",
40
+ "level": "beginner",
41
+ "vocabulary": [
42
+ {
43
+ "word": "hello",
44
+ "ipa": "/həˈloʊ/",
45
+ "vietnamese": "xin chào",
46
+ "audioUrl": "/api/tts/hello"
47
+ },
48
+ {
49
+ "word": "goodbye",
50
+ "ipa": "/ɡʊdˈbaɪ/",
51
+ "vietnamese": "tạm biệt",
52
+ "audioUrl": "/api/tts/goodbye"
53
+ },
54
+ {
55
+ "word": "thank",
56
+ "ipa": "/θæŋk/",
57
+ "vietnamese": "cảm ơn",
58
+ "audioUrl": "/api/tts/thank"
59
+ },
60
+ {
61
+ "word": "please",
62
+ "ipa": "/pliːz/",
63
+ "vietnamese": "xin lỗi/làm ơn",
64
+ "audioUrl": "/api/tts/please"
65
+ },
66
+ {
67
+ "word": "sorry",
68
+ "ipa": "/ˈsɔːri/",
69
+ "vietnamese": "xin lỗi",
70
+ "audioUrl": "/api/tts/sorry"
71
+ },
72
+ {
73
+ "word": "excuse",
74
+ "ipa": "/ɪkˈskjuːz/",
75
+ "vietnamese": "xin lỗi/thứ lỗi",
76
+ "audioUrl": "/api/tts/excuse"
77
+ },
78
+ {
79
+ "word": "morning",
80
+ "ipa": "/ˈmɔːrnɪŋ/",
81
+ "vietnamese": "buổi sáng",
82
+ "audioUrl": "/api/tts/morning"
83
+ },
84
+ {
85
+ "word": "evening",
86
+ "ipa": "/ˈiːvnɪŋ/",
87
+ "vietnamese": "buổi tối",
88
+ "audioUrl": "/api/tts/evening"
89
+ },
90
+ {
91
+ "word": "night",
92
+ "ipa": "/naɪt/",
93
+ "vietnamese": "đêm",
94
+ "audioUrl": "/api/tts/night"
95
+ },
96
+ {
97
+ "word": "welcome",
98
+ "ipa": "/ˈwelkəm/",
99
+ "vietnamese": "chào mừng",
100
+ "audioUrl": "/api/tts/welcome"
101
+ }
102
+ ],
103
+ "sentence": {
104
+ "text": "Hello, good morning! Thank you very much.",
105
+ "ipa": "/həˈloʊ ɡʊd ˈmɔːrnɪŋ θæŋk juː ˈveri mʌtʃ/",
106
+ "vietnamese": "Xin chào, chào buổi sáng! Cảm ơn rất nhiều.",
107
+ "audioUrl": "/api/tts/hello-good-morning-thank-you-very-much"
108
+ }
109
+ },
110
+ {
111
+ "id": "beginner_2",
112
+ "title": "Family Members",
113
+ "description": "Learn family vocabulary and pronunciation",
114
+ "level": "beginner",
115
+ "vocabulary": [
116
+ {
117
+ "word": "family",
118
+ "ipa": "/ˈfæməli/",
119
+ "vietnamese": "gia đình",
120
+ "audioUrl": "/api/tts/family"
121
+ },
122
+ {
123
+ "word": "mother",
124
+ "ipa": "/ˈmʌðər/",
125
+ "vietnamese": "mẹ",
126
+ "audioUrl": "/api/tts/mother"
127
+ },
128
+ {
129
+ "word": "father",
130
+ "ipa": "/ˈfɑːðər/",
131
+ "vietnamese": "bố",
132
+ "audioUrl": "/api/tts/father"
133
+ },
134
+ {
135
+ "word": "sister",
136
+ "ipa": "/ˈsɪstər/",
137
+ "vietnamese": "chị/em gái",
138
+ "audioUrl": "/api/tts/sister"
139
+ },
140
+ {
141
+ "word": "brother",
142
+ "ipa": "/ˈbrʌðər/",
143
+ "vietnamese": "anh/em trai",
144
+ "audioUrl": "/api/tts/brother"
145
+ },
146
+ {
147
+ "word": "child",
148
+ "ipa": "/tʃaɪld/",
149
+ "vietnamese": "đứa trẻ",
150
+ "audioUrl": "/api/tts/child"
151
+ },
152
+ {
153
+ "word": "parent",
154
+ "ipa": "/ˈperənt/",
155
+ "vietnamese": "cha mẹ",
156
+ "audioUrl": "/api/tts/parent"
157
+ },
158
+ {
159
+ "word": "baby",
160
+ "ipa": "/ˈbeɪbi/",
161
+ "vietnamese": "em bé",
162
+ "audioUrl": "/api/tts/baby"
163
+ },
164
+ {
165
+ "word": "wife",
166
+ "ipa": "/waɪf/",
167
+ "vietnamese": "vợ",
168
+ "audioUrl": "/api/tts/wife"
169
+ },
170
+ {
171
+ "word": "husband",
172
+ "ipa": "/ˈhʌzbənd/",
173
+ "vietnamese": "chồng",
174
+ "audioUrl": "/api/tts/husband"
175
+ }
176
+ ],
177
+ "sentence": {
178
+ "text": "My family has a mother, father, sister and brother.",
179
+ "ipa": "/maɪ ˈfæməli hæz ə ˈmʌðər ˈfɑːðər ˈsɪstər ænd ˈbrʌðər/",
180
+ "vietnamese": "Gia đình tôi có mẹ, bố, chị gái và anh trai.",
181
+ "audioUrl": "/api/tts/my-family-has-a-mother-father-sister-and-brother"
182
+ }
183
+ }
184
+ ],
185
+ "elementary": [
186
+ {
187
+ "id": "elementary_1",
188
+ "title": "Daily Activities",
189
+ "description": "Learn vocabulary for daily activities",
190
+ "level": "elementary",
191
+ "vocabulary": [
192
+ {
193
+ "word": "breakfast",
194
+ "ipa": "/ˈbrekfəst/",
195
+ "vietnamese": "bữa sáng",
196
+ "audioUrl": "/api/tts/breakfast"
197
+ },
198
+ {
199
+ "word": "lunch",
200
+ "ipa": "/lʌntʃ/",
201
+ "vietnamese": "bữa trưa",
202
+ "audioUrl": "/api/tts/lunch"
203
+ },
204
+ {
205
+ "word": "dinner",
206
+ "ipa": "/ˈdɪnər/",
207
+ "vietnamese": "bữa tối",
208
+ "audioUrl": "/api/tts/dinner"
209
+ },
210
+ {
211
+ "word": "school",
212
+ "ipa": "/skuːl/",
213
+ "vietnamese": "trường học",
214
+ "audioUrl": "/api/tts/school"
215
+ },
216
+ {
217
+ "word": "work",
218
+ "ipa": "/wɜːrk/",
219
+ "vietnamese": "làm việc",
220
+ "audioUrl": "/api/tts/work"
221
+ },
222
+ {
223
+ "word": "exercise",
224
+ "ipa": "/ˈeksərsaɪz/",
225
+ "vietnamese": "tập thể dục",
226
+ "audioUrl": "/api/tts/exercise"
227
+ },
228
+ {
229
+ "word": "sleep",
230
+ "ipa": "/sliːp/",
231
+ "vietnamese": "ngủ",
232
+ "audioUrl": "/api/tts/sleep"
233
+ },
234
+ {
235
+ "word": "study",
236
+ "ipa": "/ˈstʌdi/",
237
+ "vietnamese": "học tập",
238
+ "audioUrl": "/api/tts/study"
239
+ },
240
+ {
241
+ "word": "homework",
242
+ "ipa": "/ˈhoʊmwɜːrk/",
243
+ "vietnamese": "bài tập về nhà",
244
+ "audioUrl": "/api/tts/homework"
245
+ },
246
+ {
247
+ "word": "television",
248
+ "ipa": "/ˈtelɪvɪʒn/",
249
+ "vietnamese": "ti vi",
250
+ "audioUrl": "/api/tts/television"
251
+ }
252
+ ],
253
+ "sentence": {
254
+ "text": "I eat breakfast, go to school, and do my homework every day.",
255
+ "ipa": "/aɪ iːt ˈbrekfəst ɡoʊ tuː skuːl ænd duː maɪ ˈhoʊmwɜːrk ˈevri deɪ/",
256
+ "vietnamese": "Tôi ăn sáng, đi học và làm bài tập về nhà mỗi ngày.",
257
+ "audioUrl": "/api/tts/i-eat-breakfast-go-to-school-and-do-my-homework-every-day"
258
+ }
259
+ }
260
+ ],
261
+ "pre-intermediate": [
262
+ {
263
+ "id": "pre_intermediate_1",
264
+ "title": "Travel & Transportation",
265
+ "description": "Learn travel-related vocabulary",
266
+ "level": "pre-intermediate",
267
+ "vocabulary": [
268
+ {
269
+ "word": "airport",
270
+ "ipa": "/ˈerˌpɔːrt/",
271
+ "vietnamese": "sân bay",
272
+ "audioUrl": "/api/tts/airport"
273
+ },
274
+ {
275
+ "word": "passport",
276
+ "ipa": "/ˈpæspɔːrt/",
277
+ "vietnamese": "hộ chiếu",
278
+ "audioUrl": "/api/tts/passport"
279
+ },
280
+ {
281
+ "word": "luggage",
282
+ "ipa": "/ˈlʌɡɪdʒ/",
283
+ "vietnamese": "hành lý",
284
+ "audioUrl": "/api/tts/luggage"
285
+ },
286
+ {
287
+ "word": "ticket",
288
+ "ipa": "/ˈtɪkɪt/",
289
+ "vietnamese": "vé",
290
+ "audioUrl": "/api/tts/ticket"
291
+ },
292
+ {
293
+ "word": "destination",
294
+ "ipa": "/ˌdestɪˈneɪʃn/",
295
+ "vietnamese": "điểm đến",
296
+ "audioUrl": "/api/tts/destination"
297
+ },
298
+ {
299
+ "word": "journey",
300
+ "ipa": "/ˈdʒɜːrni/",
301
+ "vietnamese": "hành trình",
302
+ "audioUrl": "/api/tts/journey"
303
+ },
304
+ {
305
+ "word": "adventure",
306
+ "ipa": "/ədˈventʃər/",
307
+ "vietnamese": "phiêu lưu",
308
+ "audioUrl": "/api/tts/adventure"
309
+ },
310
+ {
311
+ "word": "reservation",
312
+ "ipa": "/ˌrezərˈveɪʃn/",
313
+ "vietnamese": "đặt chỗ",
314
+ "audioUrl": "/api/tts/reservation"
315
+ },
316
+ {
317
+ "word": "accommodation",
318
+ "ipa": "/əˌkɑːməˈdeɪʃn/",
319
+ "vietnamese": "chỗ ở",
320
+ "audioUrl": "/api/tts/accommodation"
321
+ },
322
+ {
323
+ "word": "schedule",
324
+ "ipa": "/ˈskedʒuːl/",
325
+ "vietnamese": "lịch trình",
326
+ "audioUrl": "/api/tts/schedule"
327
+ }
328
+ ],
329
+ "sentence": {
330
+ "text": "I need to check my passport and luggage before the journey to my destination.",
331
+ "ipa": "/aɪ niːd tuː tʃek maɪ ˈpæspɔːrt ænd ˈlʌɡɪdʒ bɪˈfɔːr ðə ˈdʒɜːrni tuː maɪ ˌdestɪˈneɪʃn/",
332
+ "vietnamese": "Tôi cần kiểm tra hộ chiếu và hành lý trước chuyến đi đến điểm đến của mình.",
333
+ "audioUrl": "/api/tts/i-need-to-check-my-passport-and-luggage-before-the-journey-to-my-destination"
334
+ }
335
+ }
336
+ ],
337
+ "upper-intermediate": [
338
+ {
339
+ "id": "upper_intermediate_1",
340
+ "title": "Business & Professional",
341
+ "description": "Learn business and professional vocabulary",
342
+ "level": "upper-intermediate",
343
+ "vocabulary": [
344
+ {
345
+ "word": "presentation",
346
+ "ipa": "/ˌpriːzenˈteɪʃn/",
347
+ "vietnamese": "bài thuyết trình",
348
+ "audioUrl": "/api/tts/presentation"
349
+ },
350
+ {
351
+ "word": "negotiation",
352
+ "ipa": "/nɪˌɡoʊʃiˈeɪʃn/",
353
+ "vietnamese": "đàm phán",
354
+ "audioUrl": "/api/tts/negotiation"
355
+ },
356
+ {
357
+ "word": "collaboration",
358
+ "ipa": "/kəˌlæbəˈreɪʃn/",
359
+ "vietnamese": "hợp tác",
360
+ "audioUrl": "/api/tts/collaboration"
361
+ },
362
+ {
363
+ "word": "achievement",
364
+ "ipa": "/əˈtʃiːvmənt/",
365
+ "vietnamese": "thành tựu",
366
+ "audioUrl": "/api/tts/achievement"
367
+ },
368
+ {
369
+ "word": "responsibility",
370
+ "ipa": "/rɪˌspɑːnsəˈbɪləti/",
371
+ "vietnamese": "trách nhiệm",
372
+ "audioUrl": "/api/tts/responsibility"
373
+ },
374
+ {
375
+ "word": "opportunity",
376
+ "ipa": "/ˌɑːpərˈtuːnəti/",
377
+ "vietnamese": "cơ hội",
378
+ "audioUrl": "/api/tts/opportunity"
379
+ },
380
+ {
381
+ "word": "efficiency",
382
+ "ipa": "/ɪˈfɪʃnsi/",
383
+ "vietnamese": "hiệu quả",
384
+ "audioUrl": "/api/tts/efficiency"
385
+ },
386
+ {
387
+ "word": "innovation",
388
+ "ipa": "/ˌɪnəˈveɪʃn/",
389
+ "vietnamese": "đổi mới",
390
+ "audioUrl": "/api/tts/innovation"
391
+ },
392
+ {
393
+ "word": "development",
394
+ "ipa": "/dɪˈveləpmənt/",
395
+ "vietnamese": "phát triển",
396
+ "audioUrl": "/api/tts/development"
397
+ },
398
+ {
399
+ "word": "implementation",
400
+ "ipa": "/ˌɪmplɪmenˈteɪʃn/",
401
+ "vietnamese": "thực hiện",
402
+ "audioUrl": "/api/tts/implementation"
403
+ }
404
+ ],
405
+ "sentence": {
406
+ "text": "Successful collaboration and innovation lead to efficient implementation of business development strategies.",
407
+ "ipa": "/səkˈsesfl kəˌlæbəˈreɪʃn ænd ˌɪnəˈveɪʃn liːd tuː ɪˈfɪʃnt ˌɪmplɪmenˈteɪʃn ʌv ˈbɪznəs dɪˈveləpmənt ˈstrætədʒiz/",
408
+ "vietnamese": "Hợp tác thành công và đổi mới dẫn đến việc thực hiện hiệu quả các chiến lược phát triển kinh doanh.",
409
+ "audioUrl": "/api/tts/successful-collaboration-and-innovation-lead-to-efficient-implementation-of-business-development-strategies"
410
+ }
411
+ }
412
+ ],
413
+ "advanced": [
414
+ {
415
+ "id": "advanced_1",
416
+ "title": "Academic & Scientific",
417
+ "description": "Learn advanced academic and scientific vocabulary",
418
+ "level": "advanced",
419
+ "vocabulary": [
420
+ {
421
+ "word": "hypothesis",
422
+ "ipa": "/haɪˈpɑːθəsɪs/",
423
+ "vietnamese": "giả thuyết",
424
+ "audioUrl": "/api/tts/hypothesis"
425
+ },
426
+ {
427
+ "word": "methodology",
428
+ "ipa": "/ˌmeθəˈdɑːlədʒi/",
429
+ "vietnamese": "phương pháp luận",
430
+ "audioUrl": "/api/tts/methodology"
431
+ },
432
+ {
433
+ "word": "phenomenon",
434
+ "ipa": "/fəˈnɑːmɪnən/",
435
+ "vietnamese": "hiện tượng",
436
+ "audioUrl": "/api/tts/phenomenon"
437
+ },
438
+ {
439
+ "word": "theoretical",
440
+ "ipa": "/ˌθiːəˈretɪkl/",
441
+ "vietnamese": "lý thuyết",
442
+ "audioUrl": "/api/tts/theoretical"
443
+ },
444
+ {
445
+ "word": "empirical",
446
+ "ipa": "/ɪmˈpɪrɪkl/",
447
+ "vietnamese": "thực nghiệm",
448
+ "audioUrl": "/api/tts/empirical"
449
+ },
450
+ {
451
+ "word": "comprehensive",
452
+ "ipa": "/ˌkɑːmprɪˈhensɪv/",
453
+ "vietnamese": "toàn diện",
454
+ "audioUrl": "/api/tts/comprehensive"
455
+ },
456
+ {
457
+ "word": "sophisticated",
458
+ "ipa": "/səˈfɪstɪkeɪtɪd/",
459
+ "vietnamese": "tinh vi",
460
+ "audioUrl": "/api/tts/sophisticated"
461
+ },
462
+ {
463
+ "word": "prerequisite",
464
+ "ipa": "/priːˈrekwəzɪt/",
465
+ "vietnamese": "điều kiện tiên quyết",
466
+ "audioUrl": "/api/tts/prerequisite"
467
+ },
468
+ {
469
+ "word": "substantiate",
470
+ "ipa": "/səbˈstænʃieɪt/",
471
+ "vietnamese": "chứng minh",
472
+ "audioUrl": "/api/tts/substantiate"
473
+ },
474
+ {
475
+ "word": "paradigm",
476
+ "ipa": "/ˈpærədaɪm/",
477
+ "vietnamese": "mô hình",
478
+ "audioUrl": "/api/tts/paradigm"
479
+ }
480
+ ],
481
+ "sentence": {
482
+ "text": "The comprehensive methodology requires sophisticated theoretical frameworks to substantiate empirical hypotheses about complex phenomena.",
483
+ "ipa": "/ðə ˌkɑːmprɪˈhensɪv ˌmeθəˈdɑːlədʒi rɪˈkwaɪərz səˈfɪstɪkeɪtɪd ˌθiːəˈretɪkl ˈfreɪmwɜːrks tuː səbˈstænʃieɪt ɪmˈpɪrɪkl haɪˈpɑːθəsiz əˈbaʊt kəmˈpleks fəˈnɑːmɪnə/",
484
+ "vietnamese": "Phương pháp luận toàn diện đòi hỏi các khung lý thuyết tinh vi để chứng minh các giả thuyết thực nghiệm về các hiện tượng phức tạp.",
485
+ "audioUrl": "/api/tts/the-comprehensive-methodology-requires-sophisticated-theoretical-frameworks-to-substantiate-empirical-hypotheses-about-complex-phenomena"
486
+ }
487
+ }
488
+ ]
489
+ }
490
+ }