ParulPandey commited on
Commit
2253319
Β·
verified Β·
1 Parent(s): 839431d

Upload 4 files

Browse files
Files changed (4) hide show
  1. agent.py +950 -0
  2. main.py +5 -0
  3. requirements.txt +13 -0
  4. ui.py +453 -0
agent.py ADDED
@@ -0,0 +1,950 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from smolagents.tools import tool
4
+ from difflib import SequenceMatcher
5
+
6
+ try:
7
+ from gradio_client import Client
8
+ except ImportError:
9
+ # Fallback import for older versions
10
+ import gradio_client
11
+ Client = gradio_client.Client
12
+ from google import genai
13
+ from google.genai import types
14
+ import json
15
+ import time
16
+ import numpy as np
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional, Tuple
19
+ from dotenv import load_dotenv
20
+
21
+ # Load environment variables
22
+ load_dotenv()
23
+
24
+ # Configure API keys
25
+ TTS_API = os.getenv("TTS_API")
26
+ STT_API = os.getenv("STT_API")
27
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
28
+
29
+ # Configure Google Gemini client
30
+ if GOOGLE_API_KEY:
31
+ gemini_client = genai.Client(api_key=GOOGLE_API_KEY)
32
+
33
+ @tool
34
+ def generate_story(name: str, grade: str, topic: str) -> str:
35
+ """
36
+ Generate a short, age-appropriate story for reading practice using LLM.
37
+
38
+ Args:
39
+ name (str): The child's name.
40
+ grade (str): The student's grade level, e.g., "Grade 3".
41
+ topic (str): The story topic, e.g., "space", "animals".
42
+
43
+ Returns:
44
+ str: Generated story text.
45
+ """
46
+ # Extract grade number and determine age/reading level
47
+ grade_num = int(''.join(filter(str.isdigit, grade)) or "3")
48
+ age = grade_num + 5 # Grade 1 = ~6 years old, Grade 6 = ~11 years old
49
+
50
+ # Dynamically determine story parameters based on grade
51
+ if grade_num <= 2:
52
+ # Grades 1-2: Very simple stories
53
+ story_length = "2-3 short sentences"
54
+ vocabulary_level = "very simple words (mostly 1-2 syllables)"
55
+ sentence_structure = "short, simple sentences"
56
+ complexity = "basic concepts"
57
+ reading_level = "beginner"
58
+ elif grade_num <= 4:
59
+ # Grades 3-4: Intermediate stories
60
+ story_length = "1-2 short paragraphs"
61
+ vocabulary_level = "age-appropriate words with some longer words"
62
+ sentence_structure = "mix of simple and compound sentences"
63
+ complexity = "intermediate concepts with some detail"
64
+ reading_level = "intermediate"
65
+ else:
66
+ # Grades 5-6: More advanced stories
67
+ story_length = "2-3 paragraphs"
68
+ vocabulary_level = "varied vocabulary including descriptive words"
69
+ sentence_structure = "complex sentences with descriptive language"
70
+ complexity = "detailed concepts and explanations"
71
+ reading_level = "advanced elementary"
72
+
73
+ # Create dynamic, grade-adaptive prompt
74
+ prompt = f"""
75
+ You are an expert children's reading coach. Create an engaging, educational story for a {age}-year-old child named {name} about {topic}.
76
+
77
+ GRADE LEVEL: {grade} ({reading_level} level)
78
+
79
+ Story Requirements:
80
+ - Length: {story_length}
81
+ - Vocabulary: Use {vocabulary_level}
82
+ - Sentence structure: {sentence_structure}
83
+ - Complexity: {complexity}
84
+ - Include {name} as the main character
85
+ - Teach something interesting about {topic}
86
+ - End with a positive, encouraging message
87
+ - Make it engaging and fun to read aloud
88
+
89
+ Additional Guidelines:
90
+ - For younger students (Grades 1-2): Focus on simple actions, basic emotions, and clear cause-and-effect
91
+ - For middle students (Grades 3-4): Include some problem-solving, friendship themes, and basic science/nature facts
92
+ - For older students (Grades 5-6): Add character development, more detailed explanations, and encourage curiosity
93
+
94
+ The story should be perfectly suited for a {grade} student's reading ability and attention span.
95
+
96
+ Story:
97
+ """
98
+
99
+ # Use Google Gemini
100
+ # Adjust generation parameters based on grade level
101
+ max_tokens = 300 if grade_num <= 2 else 600 if grade_num <= 4 else 1000
102
+
103
+ generation_config = types.GenerateContentConfig(
104
+ temperature=0.8,
105
+ max_output_tokens=max_tokens,
106
+ top_p=0.9,
107
+ )
108
+
109
+ response = gemini_client.models.generate_content(
110
+ model="gemini-2.0-flash",
111
+ contents=[prompt],
112
+ config=generation_config
113
+ )
114
+
115
+ return response.text.strip()
116
+
117
+ @tool
118
+ def text_to_speech(text: str) -> str:
119
+ """
120
+ Convert story text into an audio URL via TTS service using the gradio_client.
121
+
122
+ Args:
123
+ text (str): The story to convert to speech.
124
+
125
+ Returns:
126
+ str: URL or file path of the generated audio.
127
+ """
128
+ try:
129
+ # Use the gradio_client to interact with the TTS API with correct parameters based on API docs
130
+ client = Client("NihalGazi/Text-To-Speech-Unlimited")
131
+
132
+ # Call the API with proper keyword arguments as per documentation
133
+ result = client.predict(
134
+ prompt=text, # Required: The text to convert to speech
135
+ voice="nova", # Voice selection from available options
136
+ emotion="neutral", # Required: Emotion style
137
+ use_random_seed=True, # Use random seed for variety
138
+ specific_seed=12345, # Specific seed value
139
+ api_name="/text_to_speech_app"
140
+ )
141
+
142
+ print(f"TTS result: {result}")
143
+ print(f"TTS result type: {type(result)}")
144
+
145
+ # According to API docs, returns tuple of (filepath, status_str)
146
+ if isinstance(result, tuple) and len(result) >= 2:
147
+ audio_path, status = result[0], result[1]
148
+ print(f"TTS Status: {status}")
149
+
150
+ # Return the audio file path
151
+ if audio_path and isinstance(audio_path, str):
152
+ print(f"TTS generated audio at: {audio_path}")
153
+ return audio_path
154
+ else:
155
+ print(f"Invalid audio path: {audio_path}")
156
+ return None
157
+ else:
158
+ print(f"Unexpected TTS result format: {result}")
159
+ return None
160
+
161
+ except Exception as e:
162
+ print(f"TTS Error: {e}")
163
+ import traceback
164
+ traceback.print_exc()
165
+ return None
166
+
167
+ @tool
168
+
169
+
170
+ def transcribe_audio(audio_input: str) -> str:
171
+ """
172
+ Transcribe the student's audio into text via Whisper STT service.
173
+ Using abidlabs/whisper-large-v2 Hugging Face Space API.
174
+
175
+ Args:
176
+ audio_input: Either a file path (str) or tuple (sample_rate, numpy_array) from Gradio
177
+
178
+ Returns:
179
+ str: Transcribed speech text.
180
+ """
181
+ try:
182
+ print(f"Received audio input: {type(audio_input)}")
183
+
184
+ # Handle different input formats
185
+ if isinstance(audio_input, tuple) and len(audio_input) == 2:
186
+ # Gradio microphone format: (sample_rate, numpy_array)
187
+ sample_rate, audio_data = audio_input
188
+ print(f"Audio tuple: sample_rate={sample_rate}, data_shape={audio_data.shape}")
189
+ # Pass the tuple directly to the STT service
190
+ audio_for_stt = audio_input
191
+ elif isinstance(audio_input, (str, Path)):
192
+ audio_for_stt = str(audio_input)
193
+ else:
194
+ print(f"Unsupported audio input type: {type(audio_input)}")
195
+ return "Error: Unsupported audio format. Please try recording again."
196
+
197
+ if isinstance(audio_for_stt, Path):
198
+ audio_for_stt = str(audio_for_stt)
199
+
200
+ # Initialize client with error handling
201
+ print("Initializing Gradio client for STT...")
202
+ try:
203
+ client = Client("abidlabs/whisper-large-v2")
204
+ except Exception as client_error:
205
+ print(f"Failed to initialize client: {client_error}")
206
+ # Try alternative approach
207
+ try:
208
+ print("Trying direct API approach...")
209
+ return "Error: STT service initialization failed. Please try again."
210
+ except Exception as fallback_error:
211
+ print(f"Fallback also failed: {fallback_error}")
212
+ return "Error: Speech recognition service unavailable. Please try again later."
213
+
214
+ print("Sending audio for transcription...")
215
+
216
+ # Make the API call with timeout and error handling
217
+ try:
218
+ if isinstance(audio_for_stt, tuple):
219
+ result = client.predict(audio_for_stt, api_name="/predict")
220
+ else:
221
+ result = client.predict(audio_for_stt, api_name="/predict")
222
+ except Exception as api_error:
223
+ print(f"API call failed: {api_error}")
224
+ if "extra_headers" in str(api_error):
225
+ return "Error: Connection protocol mismatch. Please try recording again."
226
+ elif "connection" in str(api_error).lower():
227
+ return "Error: Network connection issue. Please check your internet and try again."
228
+ else:
229
+ return "Error: Transcription service temporarily unavailable. Please try again."
230
+
231
+ print(f"Raw transcription result: {result}")
232
+ print(f"Result type: {type(result)}")
233
+
234
+ # Handle different result types more robustly
235
+ if result is None:
236
+ return "Error: No transcription result. Please try speaking more clearly and loudly."
237
+
238
+ # Extract text from result
239
+ transcribed_text = ""
240
+
241
+ if isinstance(result, str):
242
+ transcribed_text = result.strip()
243
+ elif isinstance(result, (list, tuple)):
244
+ if len(result) > 0:
245
+ # Try to find the text in the result structure
246
+ transcribed_text = str(result[0]).strip()
247
+ print(f"Extracted from list/tuple: {transcribed_text}")
248
+ else:
249
+ return "Error: Empty transcription result. Please try again."
250
+ elif isinstance(result, dict):
251
+ # Handle dictionary results - try common keys
252
+ transcribed_text = result.get('text', result.get('transcription', str(result))).strip()
253
+ print(f"Extracted from dict: {transcribed_text}")
254
+ else:
255
+ transcribed_text = str(result).strip()
256
+ print(f"Converted to string: {transcribed_text}")
257
+
258
+ # Clean up common API artifacts
259
+ transcribed_text = transcribed_text.replace('```', '').replace('json', '').replace('{', '').replace('}', '')
260
+
261
+ # Validate the transcription
262
+ if not transcribed_text or (isinstance(transcribed_text, str) and transcribed_text.lower() in ['', 'none', 'null', 'error', 'undefined']):
263
+ return "I couldn't hear any speech clearly. Please try recording again and speak more loudly."
264
+
265
+ # Ensure transcribed_text is a string before further processing
266
+ if not isinstance(transcribed_text, str):
267
+ return "I couldn't hear any speech clearly. Please try recording again and speak more loudly."
268
+
269
+ # Check for common error messages from the API
270
+ error_indicators = ['error', 'failed', 'could not', 'unable to', 'timeout']
271
+ if any(indicator in transcribed_text.lower() for indicator in error_indicators):
272
+ return "Transcription service had an issue. Please try recording again."
273
+
274
+ # Clean up the transcribed text
275
+ transcribed_text = transcribed_text.replace('\n', ' ').replace('\t', ' ')
276
+ # Remove extra whitespace
277
+ transcribed_text = ' '.join(transcribed_text.split())
278
+
279
+ if len(transcribed_text) < 3:
280
+ return "The recording was too short or unclear. Please try reading more slowly and clearly."
281
+
282
+ print(f"Final transcribed text: {transcribed_text}")
283
+ return transcribed_text
284
+
285
+ except ImportError as e:
286
+ print(f"Import error: {str(e)}")
287
+ return "Error: Missing required libraries. Please check your installation."
288
+
289
+ except ConnectionError as e:
290
+ print(f"Connection error: {str(e)}")
291
+ return "Network connection error. Please check your internet connection and try again."
292
+
293
+ except TimeoutError as e:
294
+ print(f"Timeout error: {str(e)}")
295
+ return "Transcription service is taking too long. Please try again with a shorter recording."
296
+
297
+ except Exception as e:
298
+ print(f"Unexpected transcription error: {str(e)}")
299
+ error_msg = str(e).lower()
300
+
301
+ # Provide helpful error messages based on the error type
302
+ if "timeout" in error_msg or "connection" in error_msg:
303
+ return "Network timeout. Please check your internet connection and try again."
304
+ elif "file" in error_msg or "path" in error_msg:
305
+ return "Audio file error. Please try recording again."
306
+ elif "api" in error_msg or "client" in error_msg or "gradio" in error_msg:
307
+ return "Transcription service temporarily unavailable. Please try again in a moment."
308
+ elif "memory" in error_msg or "size" in error_msg:
309
+ return "Audio file is too large or complex. Please try with a shorter recording."
310
+ else:
311
+ return f"Transcription failed. Please try recording again. If the problem persists, try speaking more clearly."
312
+
313
+ def compare_texts_for_feedback(original: str, spoken: str) -> str:
314
+ """
315
+ Compare the original and spoken text, provide age-appropriate feedback with pronunciation help.
316
+ Agentic feedback system that adapts to student needs.
317
+
318
+ Args:
319
+ original (str): The original story text.
320
+ spoken (str): The student's transcribed reading.
321
+
322
+ Returns:
323
+ str: Comprehensive, age-appropriate feedback with learning suggestions.
324
+ """
325
+ # Check if the spoken text is too short to be meaningful
326
+ if not spoken or len(spoken.split()) < 3:
327
+ return "⚠️ Your reading was too short. Please try reading the complete story."
328
+
329
+ # Clean and process text
330
+ orig_words = [w.strip(".,!?;:\"'").lower() for w in original.split() if w.strip()]
331
+ spoken_words = [w.strip(".,!?;:\"'").lower() for w in spoken.split() if w.strip()]
332
+
333
+ # Set minimum threshold for overall matching - if nothing matches at all,
334
+ # it's likely the student read something completely different
335
+ common_words = set(orig_words).intersection(set(spoken_words))
336
+ if len(common_words) < max(2, len(orig_words) * 0.1): # At least 2 words or 10% must match
337
+ return "⚠️ I couldn't recognize enough words from the story. Please try reading the story text shown on the screen.\n\nReading accuracy: 0.0%"
338
+
339
+ # Calculate accuracy using sequence matching
340
+ matcher = SequenceMatcher(None, orig_words, spoken_words, autojunk=False)
341
+ accuracy = matcher.ratio() * 100
342
+
343
+ # Identify different types of errors using context-aware approach
344
+ # Use difflib to get a more accurate understanding of missed words in context
345
+ import difflib
346
+ d = difflib.Differ()
347
+ diff = list(d.compare([w.lower() for w in original.split() if w.strip()],
348
+ [w.lower() for w in spoken.split() if w.strip()]))
349
+
350
+ missed_words = []
351
+ for word in diff:
352
+ if word.startswith('- '): # Words in original but not in spoken
353
+ clean_word = word[2:].strip(".,!?;:\"'").lower()
354
+ if clean_word and len(clean_word) > 1: # Avoid punctuation
355
+ missed_words.append(clean_word)
356
+
357
+ # Convert to set to remove duplicates but preserve order for important words
358
+ missed_words_set = set(missed_words)
359
+
360
+ # Extra words (might be mispronunciations or additions)
361
+ extra_words = set(spoken_words) - set(orig_words)
362
+
363
+ # Find mispronounced words (words that sound similar but are different)
364
+ mispronounced = find_similar_words(orig_words, spoken_words)
365
+
366
+ # Prioritize important words (like nouns, longer words) if available
367
+ important_missed = [w for w in missed_words if len(w) > 4]
368
+ if important_missed:
369
+ missed_words_set = set(important_missed) | set([w for w in missed_words if w not in important_missed][:3])
370
+
371
+ # Generate age-appropriate feedback
372
+ return generate_adaptive_feedback(accuracy, missed_words_set, extra_words, mispronounced, len(orig_words))
373
+
374
+ def find_similar_words(original_words: list, spoken_words: list) -> list:
375
+ """
376
+ Find words that might be mispronounced (similar but not exact matches).
377
+
378
+ Args:
379
+ original_words (list): Original story words
380
+ spoken_words (list): Transcribed words
381
+
382
+ Returns:
383
+ list: Tuples of (original_word, spoken_word) for potential mispronunciations
384
+ """
385
+ from difflib import get_close_matches
386
+
387
+ mispronounced = []
388
+ for orig_word in original_words:
389
+ if orig_word not in spoken_words and len(orig_word) > 2:
390
+ close_matches = get_close_matches(orig_word, spoken_words, n=1, cutoff=0.6)
391
+ if close_matches:
392
+ mispronounced.append((orig_word, close_matches[0]))
393
+
394
+ return mispronounced[:5]
395
+
396
+ def generate_adaptive_feedback(accuracy: float, missed_words: set, extra_words: set,
397
+ mispronounced: list, total_words: int) -> str:
398
+ """
399
+ Generate age-appropriate, encouraging feedback with specific learning guidance.
400
+
401
+ Args:
402
+ accuracy (float): Reading accuracy percentage
403
+ missed_words (set): Words that were skipped
404
+ extra_words (set): Words that were added
405
+ mispronounced (list): Potential mispronunciations
406
+ total_words (int): Total words in story
407
+
408
+ Returns:
409
+ str: Comprehensive feedback message
410
+ """
411
+ feedback_parts = []
412
+
413
+ # Start with encouraging accuracy feedback
414
+ if accuracy >= 95:
415
+ feedback_parts.append("🌟 AMAZING! You read almost perfectly!")
416
+ elif accuracy >= 85:
417
+ feedback_parts.append("πŸŽ‰ GREAT JOB! You're doing wonderful!")
418
+ elif accuracy >= 70:
419
+ feedback_parts.append("πŸ‘ GOOD WORK! You're getting better!")
420
+ elif accuracy >= 50:
421
+ feedback_parts.append("😊 NICE TRY! Keep practicing!")
422
+ else:
423
+ feedback_parts.append("πŸš€ GREAT START! Every practice makes you better!")
424
+
425
+ feedback_parts.append(f"Reading accuracy: {accuracy:.1f}%")
426
+
427
+ # Provide specific help for missed words
428
+ if missed_words:
429
+ missed_list = sorted(list(missed_words))[:8] # Limit to 8 words
430
+ feedback_parts.append("\nπŸ“š PRACTICE THESE WORDS:")
431
+
432
+ for word in missed_list:
433
+ pronunciation_tip = get_pronunciation_tip(word)
434
+ feedback_parts.append(f"β€’ {word.upper()} - {pronunciation_tip}")
435
+
436
+ # Help with mispronounced words
437
+ if mispronounced:
438
+ feedback_parts.append("\n🎯 PRONUNCIATION PRACTICE:")
439
+ for orig, spoken in mispronounced:
440
+ tip = get_pronunciation_correction(orig, spoken)
441
+ feedback_parts.append(f"β€’ {orig.upper()} (you said '{spoken}') - {tip}")
442
+
443
+ # Positive reinforcement and next steps
444
+ if accuracy >= 80:
445
+ feedback_parts.append("\nπŸ† You're ready for more challenging stories!")
446
+ elif accuracy >= 60:
447
+ feedback_parts.append("\nπŸ’ͺ Try reading this story again to improve your score!")
448
+ else:
449
+ feedback_parts.append("\n🌱 Let's practice with shorter, simpler stories first!")
450
+
451
+ return "\n".join(feedback_parts)
452
+
453
+ def get_pronunciation_tip(word: str) -> str:
454
+ """
455
+ Generate pronunciation tips for difficult words.
456
+
457
+ Args:
458
+ word (str): Word to provide pronunciation help for
459
+
460
+ Returns:
461
+ str: Pronunciation tip
462
+ """
463
+ word = word.lower()
464
+
465
+ # Common pronunciation patterns and tips
466
+ if len(word) <= 3:
467
+ return f"Sound it out: {'-'.join(word)}"
468
+ elif word.endswith('tion'):
469
+ return "Ends with 'shun' sound"
470
+ elif word.endswith('sion'):
471
+ return "Ends with 'zhun' or 'shun' sound"
472
+ elif word.endswith('ed'):
473
+ if word[-3] in 'td':
474
+ return "Past tense - ends with 'ed' sound"
475
+ else:
476
+ return "Past tense - ends with 'd' sound"
477
+ elif 'th' in word:
478
+ return "Put your tongue between your teeth for 'th'"
479
+ elif 'ch' in word:
480
+ return "Make the 'ch' sound like in 'cheese'"
481
+ elif 'sh' in word:
482
+ return "Make the 'sh' sound like in 'ship'"
483
+ elif word.startswith('kn'):
484
+ return "The 'k' is silent, start with the 'n' sound"
485
+ elif word.startswith('ph'):
486
+ return "The 'ph' makes an 'f' sound"
487
+ elif word.startswith('wh'):
488
+ return "Starts with 'w' sound (like 'when')"
489
+ elif word.endswith('igh'):
490
+ return "The 'igh' makes a long 'i' sound like in 'night'"
491
+ elif 'ou' in word:
492
+ return "The 'ou' often sounds like 'ow' in 'cow'"
493
+ elif 'ai' in word:
494
+ return "The 'ai' makes the long 'a' sound"
495
+ elif 'ea' in word:
496
+ return "The 'ea' usually makes the long 'e' sound"
497
+ elif len(word) >= 6:
498
+ # Break longer words into syllables
499
+ return f"Break it down: {break_into_syllables(word)}"
500
+ else:
501
+ return f"Sound it out slowly: {'-'.join(word[:len(word)//2])}-{'-'.join(word[len(word)//2:])}"
502
+
503
+ def get_pronunciation_correction(original: str, spoken: str) -> str:
504
+ """
505
+ Provide specific correction for mispronounced words.
506
+
507
+ Args:
508
+ original (str): Correct word
509
+ spoken (str): How it was pronounced
510
+
511
+ Returns:
512
+ str: Correction tip
513
+ """
514
+ orig = original.lower()
515
+ spok = spoken.lower()
516
+
517
+ # Common mispronunciation patterns
518
+ if len(orig) > len(spok):
519
+ return f"Don't skip letters! Say all sounds in '{orig}'"
520
+ elif len(spok) > len(orig):
521
+ return f"Not too fast! The word is just '{orig}'"
522
+ elif orig[0] != spok[0]:
523
+ return f"Starts with '{orig[0]}' sound, not '{spok[0]}'"
524
+ elif orig[-1] != spok[-1]:
525
+ return f"Ends with '{orig[-1]}' sound"
526
+
527
+ # Check for vowel confusion
528
+ orig_vowels = [c for c in orig if c in 'aeiou']
529
+ spok_vowels = [c for c in spok if c in 'aeiou']
530
+
531
+ if orig_vowels != spok_vowels:
532
+ # Find the first different vowel
533
+ for i in range(min(len(orig_vowels), len(spok_vowels))):
534
+ if orig_vowels[i] != spok_vowels[i]:
535
+ vowel_map = {
536
+ 'a': "ah (like in 'cat')",
537
+ 'e': "eh (like in 'bed')",
538
+ 'i': "ih (like in 'sit')",
539
+ 'o': "oh (like in 'hot')",
540
+ 'u': "uh (like in 'cup')"
541
+ }
542
+ correct_sound = vowel_map.get(orig_vowels[i], f"'{orig_vowels[i]}'")
543
+ wrong_sound = vowel_map.get(spok_vowels[i], f"'{spok_vowels[i]}'")
544
+ return f"Say the vowel sound as {correct_sound}, not {wrong_sound}"
545
+
546
+ # Default case
547
+ return f"Listen carefully: '{orig}' - try saying it slower"
548
+
549
+ def break_into_syllables(word: str) -> str:
550
+ """
551
+ Improved syllable breaking for pronunciation help.
552
+
553
+ Args:
554
+ word (str): Word to break into syllables
555
+
556
+ Returns:
557
+ str: Word broken into syllables
558
+ """
559
+ vowels = 'aeiouy'
560
+ word = word.lower()
561
+ syllables = []
562
+ current_syllable = ''
563
+ consonant_cluster = ''
564
+
565
+ # Handle common prefixes
566
+ common_prefixes = ['re', 'pre', 'un', 'in', 'im', 'dis', 'mis', 'non', 'sub', 'inter', 'ex']
567
+ for prefix in common_prefixes:
568
+ if word.startswith(prefix) and len(word) > len(prefix) + 1:
569
+ syllables.append(prefix)
570
+ word = word[len(prefix):]
571
+ break
572
+
573
+ # Handle common suffixes
574
+ common_suffixes = ['ing', 'ed', 'er', 'est', 'ly', 'ful', 'ness', 'less', 'ment', 'able', 'ible']
575
+ for suffix in common_suffixes:
576
+ if word.endswith(suffix) and len(word) > len(suffix) + 1:
577
+ suffix_syllable = suffix
578
+ word = word[:-len(suffix)]
579
+ syllables.append(word)
580
+ syllables.append(suffix_syllable)
581
+ return '-'.join(syllables)
582
+
583
+ # Process the word character by character
584
+ i = 0
585
+ while i < len(word):
586
+ char = word[i]
587
+
588
+ # If we encounter a vowel
589
+ if char in vowels:
590
+ # Start or add to a syllable
591
+ if consonant_cluster:
592
+ # For consonant clusters, we generally add one consonant to the current syllable
593
+ # and move the rest to the next syllable
594
+ if len(consonant_cluster) > 1:
595
+ if current_syllable: # If we already have a syllable started
596
+ current_syllable += consonant_cluster[0]
597
+ syllables.append(current_syllable)
598
+ current_syllable = consonant_cluster[1:] + char
599
+ else: # For starting consonant clusters
600
+ current_syllable = consonant_cluster + char
601
+ else: # Single consonant
602
+ current_syllable += consonant_cluster + char
603
+ consonant_cluster = ''
604
+ else:
605
+ current_syllable += char
606
+
607
+ # Check for vowel pairs that should stay together
608
+ if i < len(word) - 1 and word[i+1] in vowels:
609
+ vowel_pairs = ['ea', 'ee', 'oo', 'ou', 'ie', 'ai', 'oa']
610
+ if word[i:i+2] in vowel_pairs:
611
+ current_syllable += word[i+1]
612
+ i += 1 # Skip the next vowel since we've added it
613
+ else: # Consonant
614
+ if current_syllable: # If we have an open syllable
615
+ if i < len(word) - 1 and word[i+1] not in vowels: # Consonant cluster
616
+ consonant_cluster += char
617
+ else: # Single consonant followed by vowel
618
+ current_syllable += char
619
+ else: # Starting with consonant or building consonant cluster
620
+ consonant_cluster += char
621
+
622
+ # Handle end of word or ready to break syllable
623
+ if i == len(word) - 1 or (char in vowels and i < len(word) - 1 and word[i+1] not in vowels):
624
+ if current_syllable:
625
+ syllables.append(current_syllable)
626
+ current_syllable = ''
627
+
628
+ i += 1
629
+
630
+ # Add any remaining parts
631
+ if consonant_cluster:
632
+ if syllables:
633
+ syllables[-1] += consonant_cluster
634
+ else:
635
+ syllables.append(consonant_cluster)
636
+
637
+ if current_syllable:
638
+ syllables.append(current_syllable)
639
+
640
+ # Special case handling
641
+ result = '-'.join(syllables) if syllables else word
642
+
643
+ # If we ended up with no breaks, provide a simpler approach
644
+ if result == word and len(word) > 3:
645
+ # Simple fallback: break after every other letter
646
+ syllables = [word[i:i+2] for i in range(0, len(word), 2)]
647
+ result = '-'.join(syllables)
648
+
649
+ return result
650
+
651
+ @tool
652
+ def generate_targeted_story(previous_feedback: str, name: str, grade: str, missed_words: list = None) -> str:
653
+ """
654
+ Generate a new story that specifically targets words the student struggled with.
655
+ Agentic story generation based on learning gaps.
656
+
657
+ Args:
658
+ previous_feedback (str): Previous reading feedback
659
+ name (str): Student's name
660
+ grade (str): Student's grade level
661
+ missed_words (list): Words the student had trouble with
662
+
663
+ Returns:
664
+ str: New targeted story for practice
665
+ """
666
+ grade_num = int(''.join(filter(str.isdigit, grade)) or "3")
667
+ age = grade_num + 5
668
+
669
+ # Dynamically determine story parameters based on grade - match the same criteria as main story generation
670
+ if grade_num <= 2:
671
+ # Grades 1-2: Very simple stories
672
+ story_length = "2-3 short sentences"
673
+ vocabulary_level = "very simple words (mostly 1-2 syllables)"
674
+ sentence_structure = "short, simple sentences"
675
+ complexity = "basic concepts"
676
+ reading_level = "beginner"
677
+ elif grade_num <= 4:
678
+ # Grades 3-4: Intermediate stories
679
+ story_length = "1-2 short paragraphs"
680
+ vocabulary_level = "age-appropriate words with some longer words"
681
+ sentence_structure = "mix of simple and compound sentences"
682
+ complexity = "intermediate concepts with some detail"
683
+ reading_level = "intermediate"
684
+ else:
685
+ # Grades 5-6: More advanced stories
686
+ story_length = "2-3 paragraphs"
687
+ vocabulary_level = "varied vocabulary including descriptive words"
688
+ sentence_structure = "complex sentences with descriptive language"
689
+ complexity = "detailed concepts and explanations"
690
+ reading_level = "advanced elementary"
691
+
692
+ # Extract difficulty level from previous feedback
693
+ if "AMAZING" in previous_feedback or "accuracy: 9" in previous_feedback:
694
+ difficulty_adjustment = "slightly more challenging but still within grade level"
695
+ focus_area = "new vocabulary and longer sentences"
696
+ elif "GOOD" in previous_feedback or "accuracy: 8" in previous_feedback:
697
+ difficulty_adjustment = "similar level with some new words"
698
+ focus_area = "reinforcing current skills"
699
+ else:
700
+ difficulty_adjustment = "slightly simpler but still grade-appropriate"
701
+ focus_area = "basic vocabulary and simple sentences"
702
+
703
+ # Create targeted practice words
704
+ if missed_words:
705
+ practice_words = missed_words[:5] # Focus on top 5 missed words
706
+ word_focus = f"Include and repeat these practice words: {', '.join(practice_words)}"
707
+ else:
708
+ word_focus = "Focus on common sight words for this grade level"
709
+
710
+ # Generate adaptive prompt
711
+ prompt = f"""
712
+ You are an expert reading coach creating a personalized story for {name}, a {age}-year-old in {grade}.
713
+
714
+ GRADE LEVEL: {grade} ({reading_level} level)
715
+
716
+ STORY SPECIFICATIONS:
717
+ - Length: {story_length}
718
+ - Vocabulary: {vocabulary_level}
719
+ - Sentence structure: {sentence_structure}
720
+ - Complexity: {complexity}
721
+
722
+ LEARNING ADAPTATION:
723
+ - Make this story {difficulty_adjustment}
724
+ - Focus on: {focus_area}
725
+ - {word_focus}
726
+
727
+ STORY REQUIREMENTS:
728
+ - Feature {name} as the main character
729
+ - Include an engaging adventure or discovery theme
730
+ - Naturally incorporate the practice words multiple times
731
+ - Make it fun and encouraging
732
+ - End with {name} feeling proud and accomplished
733
+
734
+ Create a story that helps {name} practice the words they found challenging while building confidence.
735
+
736
+ Story:
737
+ """
738
+
739
+ # Generate targeted story
740
+ max_tokens = 300 if grade_num <= 2 else 600 if grade_num <= 4 else 1000
741
+
742
+ generation_config = genai.GenerationConfig(
743
+ temperature=0.7,
744
+ max_output_tokens=max_tokens,
745
+ top_p=0.9,
746
+ )
747
+
748
+ response = gemini_client.models.generate_content(
749
+ model="gemini-2.5-flash",
750
+ contents=[prompt],
751
+ generation_config=generation_config
752
+ )
753
+
754
+ return response.text.strip()
755
+
756
+ class SessionManager:
757
+ """Manages student sessions and progress tracking"""
758
+
759
+ def __init__(self):
760
+ self.sessions = {}
761
+ self.student_progress = {}
762
+
763
+ def start_session(self, student_name: str, grade: str) -> str:
764
+ """Start a new reading session for a student"""
765
+ session_id = f"{student_name}_{int(time.time())}"
766
+ self.sessions[session_id] = {
767
+ "student_name": student_name,
768
+ "grade": grade,
769
+ "start_time": time.time(),
770
+ "stories_read": 0,
771
+ "total_accuracy": 0,
772
+ "feedback_history": []
773
+ }
774
+ return session_id
775
+
776
+ def get_session(self, session_id: str) -> dict:
777
+ """Get session data"""
778
+ return self.sessions.get(session_id, {})
779
+
780
+ def update_session(self, session_id: str, accuracy: float, feedback: str):
781
+ """Update session with reading results"""
782
+ if session_id in self.sessions:
783
+ session = self.sessions[session_id]
784
+ session["stories_read"] += 1
785
+ session["total_accuracy"] += accuracy
786
+ session["feedback_history"].append({
787
+ "timestamp": time.time(),
788
+ "accuracy": accuracy,
789
+ "feedback": feedback
790
+ })
791
+
792
+
793
+ class ReadingCoachAgent:
794
+ """
795
+ Main agent class that provides the interface for the reading coach system.
796
+ Wraps the individual tool functions and manages student sessions.
797
+ """
798
+
799
+ def __init__(self):
800
+ self.session_manager = SessionManager()
801
+ self.current_session = None
802
+ self.current_story = ""
803
+ self.student_info = {"name": "", "grade": ""}
804
+
805
+ def generate_story_for_student(self, name: str, grade: str, topic: str) -> str:
806
+ """Generate a story for a student and start/update session"""
807
+ # Store student info
808
+ self.student_info = {"name": name, "grade": grade}
809
+
810
+ # Start or update session
811
+ session_id = self.session_manager.start_session(name, grade)
812
+ self.current_session = session_id
813
+
814
+ # Generate story using the tool function
815
+ story = generate_story(name, grade, topic)
816
+ self.current_story = story
817
+
818
+ return story
819
+
820
+ def create_audio_from_story(self, story: str) -> str:
821
+ """Convert story to audio using TTS"""
822
+ return text_to_speech(story)
823
+
824
+ def analyze_student_reading(self, audio_path: str) -> tuple:
825
+ """Analyze student's reading and provide feedback"""
826
+ # Transcribe the audio
827
+ transcribed_text = transcribe_audio(audio_path)
828
+
829
+ # Check if the transcribed text is an error message or empty
830
+ if transcribed_text.startswith("Error:") or transcribed_text.startswith("I couldn't hear") or len(transcribed_text.strip()) < 3:
831
+ # Return a helpful message instead of giving feedback with accuracy points
832
+ error_feedback = "⚠️ I couldn't hear your reading clearly. Please try again and make sure to:\n"
833
+ error_feedback += "β€’ Speak clearly and at a normal pace\n"
834
+ error_feedback += "β€’ Make sure your microphone is working properly\n"
835
+ error_feedback += "β€’ Try reading in a quieter environment\n"
836
+ error_feedback += "β€’ Read the complete story from beginning to end\n\n"
837
+ error_feedback += "Reading accuracy: 0.0%"
838
+
839
+ return transcribed_text, error_feedback, 0.0
840
+
841
+ # Compare with original story and get feedback
842
+ feedback = compare_texts_for_feedback(self.current_story, transcribed_text)
843
+
844
+ # Extract accuracy from feedback
845
+ accuracy = self._extract_accuracy_from_feedback(feedback)
846
+
847
+ # Update session if we have one
848
+ if self.current_session:
849
+ self.session_manager.update_session(self.current_session, accuracy, feedback)
850
+
851
+ return transcribed_text, feedback, accuracy
852
+
853
+ def generate_new_passage(self, topic: str) -> str:
854
+ """Generate a new passage with the current student info"""
855
+ if not self.student_info["name"] or not self.student_info["grade"]:
856
+ raise ValueError("No active session. Please start a new session first.")
857
+
858
+ # Generate new story
859
+ story = generate_story(self.student_info["name"], self.student_info["grade"], topic)
860
+ self.current_story = story
861
+
862
+ return story
863
+
864
+ def generate_practice_story(self, name: str, grade: str) -> str:
865
+ """Generate a new targeted practice story based on previous feedback"""
866
+ if not self.student_info.get("name") or not self.student_info.get("grade"):
867
+ # Use provided parameters if student info is not available
868
+ name = name or "Student"
869
+ grade = grade or "Grade 3"
870
+ else:
871
+ name = self.student_info["name"]
872
+ grade = self.student_info["grade"]
873
+
874
+ # Get the last feedback to personalize the practice story
875
+ last_feedback = ""
876
+ missed_words_list = []
877
+
878
+ # Extract missed words from feedback if available
879
+ if self.current_session:
880
+ session_data = self.session_manager.get_session(self.current_session)
881
+ if session_data and "feedback_history" in session_data and session_data["feedback_history"]:
882
+ last_feedback = session_data["feedback_history"][-1]["feedback"]
883
+
884
+ # Extract missed words from the feedback
885
+ import re
886
+ if "PRACTICE THESE WORDS:" in last_feedback:
887
+ # Find all words that appear after bullet points
888
+ matches = re.findall(r'β€’ ([A-Z]+)', last_feedback)
889
+ missed_words_list = [word.lower() for word in matches]
890
+
891
+ # Generate a new practice story using the targeted story function
892
+ practice_story = generate_targeted_story(last_feedback, name, grade, missed_words_list)
893
+ self.current_story = practice_story
894
+
895
+ return practice_story
896
+
897
+ def clear_session(self):
898
+ """Clear current session"""
899
+ self.current_session = None
900
+ self.current_story = ""
901
+ self.student_info = {"name": "", "grade": ""}
902
+
903
+ def reset_all_data(self):
904
+ """Reset all current session state but keep tracked sessions."""
905
+ self.clear_session()
906
+
907
+ def _extract_accuracy_from_feedback(self, feedback: str) -> float:
908
+ """Extract accuracy percentage from feedback text"""
909
+ import re
910
+ # Look for "Reading accuracy: XX.X%" pattern in feedback
911
+ match = re.search(r'Reading accuracy:\s*(\d+\.?\d*)%', feedback)
912
+ if match:
913
+ return float(match.group(1))
914
+ return 0.0
915
+
916
+ def _extract_missed_words_from_feedback(feedback: str) -> list:
917
+ """
918
+ Extract missed words from feedback text.
919
+
920
+ Args:
921
+ feedback (str): Feedback text containing missed words
922
+
923
+ Returns:
924
+ list: List of missed words
925
+ """
926
+ import re
927
+ missed_words = []
928
+
929
+ # Check if feedback contains practice words section
930
+ if "PRACTICE THESE WORDS:" in feedback:
931
+ # Extract the section with practice words
932
+ practice_section = feedback.split("PRACTICE THESE WORDS:")[1].split("\n")[1:]
933
+ # Extract words that appear after bullet points
934
+ for line in practice_section:
935
+ if "β€’" in line and "-" in line:
936
+ # Extract word before the dash
937
+ match = re.search(r'β€’ ([A-Z]+) -', line)
938
+ if match:
939
+ missed_words.append(match.group(1).lower())
940
+
941
+ # If we also have mispronounced words, add them too
942
+ if "PRONUNCIATION PRACTICE:" in feedback:
943
+ pronun_section = feedback.split("PRONUNCIATION PRACTICE:")[1].split("\n")[1:]
944
+ for line in pronun_section:
945
+ if "β€’" in line and "(you said" in line:
946
+ match = re.search(r'β€’ ([A-Z]+) \(you said', line)
947
+ if match:
948
+ missed_words.append(match.group(1).lower())
949
+
950
+ return missed_words
main.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from ui import launch_ui
2
+
3
+ if __name__ == "__main__":
4
+ demo = launch_ui()
5
+ demo.launch(debug=True, share=True)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ google-genai>=0.1.0
3
+ gradio_client>=0.15.0
4
+ python-dotenv>=1.0.0
5
+ soundfile>=0.12.1
6
+ numpy>=1.24.0
7
+ tqdm>=4.65.0
8
+ smolagents>=0.2.0
9
+ # Add these for better HTTP compatibility
10
+ aiohttp>=3.8.0
11
+ httpx>=0.24.0
12
+ # SSL/Certificate handling
13
+ certifi>=2023.0.0
ui.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ from agent import ReadingCoachAgent
4
+
5
+ # Create a single instance of the agent
6
+ reading_coach = ReadingCoachAgent()
7
+ session = {"story": "", "name": "", "grade": "", "progress": 0, "last_feedback": "", "practice_count": 0}
8
+
9
+ # Define theme colors (Duolingo-inspired)
10
+ PRIMARY_COLOR = "#58CC02" # Green
11
+ SECONDARY_COLOR = "#FFC800" # Yellow
12
+ ACCENT_COLOR = "#FF4B4B" # Red
13
+ BG_COLOR = "#F7F7F7" # Light gray
14
+
15
+ # Custom CSS for more professional styling
16
+ custom_css = """
17
+ :root {
18
+ --primary-color: #58CC02;
19
+ --secondary-color: #FFC800;
20
+ --accent-color: #FF4B4B;
21
+ --neutral-color: #4B4B4B;
22
+ --light-bg: #F7F7F7;
23
+ --white: #FFFFFF;
24
+ --border-radius: 16px;
25
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
26
+ }
27
+
28
+ .container {
29
+ font-family: 'Nunito', sans-serif;
30
+ max-width: 900px;
31
+ margin: 0 auto;
32
+ font-size: 0.95rem;
33
+ }
34
+
35
+ .header {
36
+ text-align: center;
37
+ margin-bottom: 2rem;
38
+ color: var(--neutral-color);
39
+ }
40
+
41
+ .app-title {
42
+ color: var(--primary-color);
43
+ font-size: 2.2rem;
44
+ font-weight: 800;
45
+ margin: 0;
46
+ }
47
+
48
+ .card {
49
+ background: var(--white);
50
+ border-radius: var(--border-radius);
51
+ padding: 1.5rem;
52
+ box-shadow: var(--shadow);
53
+ margin-bottom: 1.5rem;
54
+ }
55
+
56
+ .btn-primary {
57
+ background: var(--primary-color) !important;
58
+ color: var(--white) !important;
59
+ font-weight: bold !important;
60
+ border: none !important;
61
+ padding: 0.75rem 1.5rem !important;
62
+ border-radius: 50px !important;
63
+ cursor: pointer !important;
64
+ transition: transform 0.1s, box-shadow 0.1s !important;
65
+ box-shadow: 0 4px 0 #48a700 !important;
66
+ }
67
+
68
+ .btn-secondary {
69
+ background: var(--secondary-color) !important;
70
+ color: var(--neutral-color) !important;
71
+ font-weight: bold !important;
72
+ border: none !important;
73
+ padding: 0.75rem 1.5rem !important;
74
+ border-radius: 50px !important;
75
+ cursor: pointer !important;
76
+ box-shadow: 0 4px 0 #e0b000 !important;
77
+ }
78
+
79
+ .btn-practice {
80
+ background: var(--primary-color) !important;
81
+ color: var(--white) !important;
82
+ font-weight: bold !important;
83
+ border: none !important;
84
+ padding: 0.75rem 1.5rem !important;
85
+ border-radius: 50px !important;
86
+ cursor: pointer !important;
87
+ box-shadow: 0 4px 0 #48a700 !important;
88
+ }
89
+
90
+ .btn-clear {
91
+ background: var(--accent-color) !important;
92
+ color: var(--white) !important;
93
+ font-weight: bold !important;
94
+ border: none !important;
95
+ padding: 0.5rem 1rem !important;
96
+ border-radius: 25px !important;
97
+ cursor: pointer !important;
98
+ box-shadow: 0 2px 0 #d93636 !important;
99
+ }
100
+
101
+ .progress-container {
102
+ width: 100%;
103
+ background-color: #e0e0e0;
104
+ border-radius: 50px;
105
+ margin: 1rem 0;
106
+ height: 10px;
107
+ }
108
+
109
+ .progress-bar {
110
+ background-color: var(--primary-color);
111
+ height: 100%;
112
+ border-radius: 50px;
113
+ transition: width 0.3s ease;
114
+ }
115
+
116
+ .feedback-area {
117
+ background: #f8f9fa;
118
+ border: 2px solid #e9ecef;
119
+ border-radius: 12px;
120
+ padding: 1rem;
121
+ margin: 1rem 0;
122
+ font-family: 'Consolas', 'Monaco', monospace;
123
+ font-size: 0.9rem;
124
+ line-height: 1.6;
125
+ white-space: pre-wrap;
126
+ }
127
+
128
+ .practice-info {
129
+ background: linear-gradient(135deg, var(--primary-color) 0%, #48a700 100%);
130
+ color: white;
131
+ padding: 1rem;
132
+ border-radius: 12px;
133
+ margin: 1rem 0;
134
+ text-align: center;
135
+ }
136
+
137
+ input, textarea {
138
+ border: 2px solid #e0e0e0 !important;
139
+ border-radius: var(--border-radius) !important;
140
+ padding: 10px !important;
141
+ font-size: 14px !important;
142
+ }
143
+
144
+ input:focus, textarea:focus {
145
+ border-color: var(--primary-color) !important;
146
+ outline: none !important;
147
+ }
148
+
149
+ @media (max-width: 768px) {
150
+ .card {
151
+ padding: 1rem;
152
+ }
153
+ }
154
+ """
155
+
156
+ def start_session(name, grade, topic):
157
+ """Generate a new story based on name, grade, and topic"""
158
+ if not name.strip() or not grade.strip() or not topic.strip():
159
+ return "Please fill in all fields", gr.update(visible=False), gr.update(visible=False)
160
+
161
+ try:
162
+ # Store session data
163
+ session["name"] = name
164
+ session["grade"] = grade
165
+ session["practice_count"] = 0
166
+
167
+ # Clear previous session
168
+ reading_coach.clear_session()
169
+
170
+ # Generate the story using the correct method
171
+ generated_story = reading_coach.generate_story_for_student(name, grade, topic)
172
+ session["story"] = generated_story
173
+ session["progress"] = 33
174
+
175
+ # Print for debugging
176
+ print(f"Generated story: {session['story'][:50]}...")
177
+
178
+ # Return the story and make practice card visible
179
+ return session["story"], gr.update(visible=True), gr.update(visible=False)
180
+
181
+ except Exception as e:
182
+ print(f"Error in start_session: {e}")
183
+ # Provide a fallback story if generation fails
184
+ fallback = f"Once upon a time, {name} went on an adventure to learn about {topic}..."
185
+ session["story"] = fallback
186
+ return fallback, gr.update(visible=True), gr.update(visible=False)
187
+
188
+ def generate_audio():
189
+ """Generate audio for the current story"""
190
+ try:
191
+ if not session.get("story"):
192
+ return None
193
+
194
+ print("Generating audio for story...")
195
+ audio_path = reading_coach.create_audio_from_story(session["story"])
196
+
197
+ if audio_path:
198
+ session["progress"] = 66
199
+ print(f"Audio generated successfully: {audio_path}")
200
+ print(f"Audio path type: {type(audio_path)}")
201
+
202
+ # Ensure the path exists and is accessible
203
+ if os.path.exists(audio_path):
204
+ print(f"Audio file exists at: {audio_path}")
205
+ return audio_path
206
+ else:
207
+ print(f"Audio file does not exist at: {audio_path}")
208
+ return None
209
+ else:
210
+ print("No audio path returned from TTS")
211
+ # Instead of returning None, we could return a message or skip audio
212
+ return None
213
+
214
+ except Exception as e:
215
+ print(f"Error in generate_audio: {e}")
216
+ import traceback
217
+ traceback.print_exc()
218
+ return None
219
+
220
+ def submit_reading(audio):
221
+ """Process the student's reading and provide comprehensive agentic feedback"""
222
+ try:
223
+ # Handle various audio input formats
224
+ if audio is None:
225
+ return "Please record your reading first.", gr.update(visible=False)
226
+
227
+ # Debug: Print what we received
228
+ print(f"Received audio input: {type(audio)} - {str(audio)[:100]}...")
229
+
230
+ # Pass audio directly to the agent - let STT handle the format conversion
231
+ transcribed, feedback, accuracy = reading_coach.analyze_student_reading(audio)
232
+
233
+ # Store feedback for potential story generation
234
+ session["last_feedback"] = feedback
235
+ session["progress"] = 100
236
+
237
+ # Only increment practice count for valid reading attempts
238
+ if not feedback.startswith("⚠️"):
239
+ session["practice_count"] += 1
240
+
241
+ # Show practice story section if feedback indicates areas for improvement
242
+ # But don't show it for error messages or warnings
243
+ show_practice = (not feedback.startswith("⚠️")) and ("PRACTICE THESE WORDS:" in feedback or "PRONUNCIATION PRACTICE:" in feedback)
244
+
245
+ return feedback, gr.update(visible=show_practice)
246
+
247
+ except Exception as e:
248
+ print(f"Error in submit_reading: {e}")
249
+ import traceback
250
+ traceback.print_exc()
251
+ return "There was an error processing your reading. Please try again.", gr.update(visible=False)
252
+
253
+ def generate_practice_story():
254
+ """Generate a new targeted story based on previous feedback"""
255
+ try:
256
+ if not session.get("name") or not session.get("grade"):
257
+ return "Please complete a reading session first to get a personalized practice story.", ""
258
+
259
+ # Generate targeted story using the correct method
260
+ new_story = reading_coach.generate_practice_story(session["name"], session["grade"])
261
+
262
+ # Update session with new story
263
+ session["story"] = new_story
264
+ session["progress"] = 33
265
+ session["practice_count"] += 1
266
+
267
+ # Clear previous feedback
268
+ session["last_feedback"] = ""
269
+
270
+ practice_msg = f"🎯 Practice Story #{session['practice_count']} Generated!\nThis story focuses on words you found challenging."
271
+
272
+ return new_story, practice_msg
273
+
274
+ except Exception as e:
275
+ print(f"Error generating practice story: {e}")
276
+ return "There was an error generating a new practice story. Please try again.", ""
277
+
278
+
279
+
280
+ def clear_all_audio():
281
+ """Clear all audio components and reset audio session"""
282
+ return None, None, "πŸ”„ All audio cleared!"
283
+
284
+ def reset_session():
285
+ """Reset the session to start over"""
286
+ session.clear()
287
+ session.update({"story": "", "name": "", "grade": "", "progress": 0, "last_feedback": "", "practice_count": 0})
288
+ reading_coach.reset_all_data()
289
+
290
+ return (
291
+ gr.update(value=""),
292
+ gr.update(value=""),
293
+ gr.update(value=""),
294
+ gr.update(value=""),
295
+ gr.update(value=None),
296
+ gr.update(value=""),
297
+ gr.update(value=""),
298
+ gr.update(visible=False),
299
+ gr.update(visible=False),
300
+ 0
301
+ )
302
+
303
+ def clear_recording():
304
+ """Clear the recorded audio"""
305
+ return None
306
+
307
+ def record_again():
308
+ """Reset recording for a new attempt"""
309
+ return None
310
+
311
+ def update_progress_bar(progress):
312
+ """Update the progress bar width based on progress percentage"""
313
+ return f"<div class='progress-container'><div class='progress-bar' style='width: {progress}%'></div></div>"
314
+
315
+ def launch_ui():
316
+ with gr.Blocks(css=custom_css, title="ReadRight - AI Reading Coach") as demo:
317
+ with gr.Column(elem_classes="container"):
318
+ # Header
319
+ gr.HTML("""
320
+ <div class="header">
321
+ <h1 class="app-title">πŸ¦‰ ReadRight</h1>
322
+ <p>AI-powered reading coach that adapts to help you learn!</p>
323
+ <p style="font-size:0.9rem; margin-top:0.5rem; opacity:0.8;">For students, parents, and educators</p>
324
+ </div>
325
+ """)
326
+
327
+ # Progress tracker
328
+ progress_bar = gr.HTML(update_progress_bar(0), elem_id="progress-bar")
329
+
330
+ # Step 1: Story Setup
331
+ with gr.Column(elem_classes="card") as setup_card:
332
+ gr.HTML("<h2>🌟 Let's Create Your Personalized Reading Adventure!</h2>")
333
+
334
+ with gr.Row():
335
+ with gr.Column(scale=2):
336
+ with gr.Row():
337
+ name = gr.Text(label="πŸ‘€ Your Name", placeholder="Enter your name...")
338
+
339
+ with gr.Row():
340
+ grade = gr.Dropdown(
341
+ label="πŸ“š Grade Level",
342
+ choices=["Grade 1", "Grade 2", "Grade 3", "Grade 4", "Grade 5", "Grade 6"," Grade 7", "Grade 8", "Grade 9", "Grade 10"],
343
+ value="Grade 1"
344
+ )
345
+
346
+ with gr.Row():
347
+ topic = gr.Text(label="🌟 Story Topic", placeholder="e.g., space adventure, friendly dinosaurs, ocean exploration...")
348
+
349
+ with gr.Row():
350
+ btn_start = gr.Button("πŸš€ Create My Story", elem_classes="btn-primary")
351
+
352
+ with gr.Column(scale=1):
353
+ gr.HTML("""
354
+ <div style="background: linear-gradient(135deg, var(--primary-color) 0%, #48a700 100%);
355
+ color: white; padding: 1.5rem; border-radius: var(--border-radius);
356
+ margin-left: 1rem; height: fit-content; box-shadow: var(--shadow);">
357
+ <h3 style="margin-top: 0; color: white; font-size: 1.1rem; font-weight: 700;">πŸ“š How to Use ReadRight</h3>
358
+ <div style="font-size: 0.85rem; line-height: 1.5;">
359
+ <p style="margin: 0.5rem 0;"><span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-weight: 600;">Step 1:</span> Enter your name, grade, and choose a fun story topic</p>
360
+ <p style="margin: 0.5rem 0;"><span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-weight: 600;">Step 2:</span> Click "Create My Story" to generate your personalized reading adventure</p>
361
+ <p style="margin: 0.5rem 0;"><span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-weight: 600;">Step 3:</span> Listen to the story first by clicking "Listen to Story"</p>
362
+ <p style="margin: 0.5rem 0;"><span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-weight: 600;">Step 4:</span> Record yourself reading the story aloud</p>
363
+ <p style="margin: 0.5rem 0;"><span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-weight: 600;">Step 5:</span> Get personalized feedback from your AI reading coach</p>
364
+ <p style="margin: 0.5rem 0;"><span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-weight: 600;">Step 6:</span> Practice with targeted stories if needed!</p>
365
+ </div>
366
+ </div>
367
+ """)
368
+
369
+ # Step 2: Reading Practice
370
+ with gr.Column(elem_classes="card", visible=False) as practice_card:
371
+ gr.HTML("<h2>πŸ“– Time to Practice Reading!</h2>")
372
+
373
+ # Story display
374
+ story = gr.Markdown(label="πŸ“– Your Personalized Story")
375
+
376
+ with gr.Row():
377
+ btn_play = gr.Button("πŸ”Š Listen to Story", elem_classes="btn-secondary")
378
+
379
+ # Audio playback
380
+ audio_out = gr.Audio(label="🎡 Story Audio - Listen and Follow Along", visible=True)
381
+
382
+ gr.HTML("<h3>🎀 Now Read the Story Aloud!</h3>")
383
+
384
+ # Recording
385
+ record = gr.Audio(label="🎀 Record Your Reading", sources=["microphone"],type="filepath")
386
+
387
+ with gr.Row():
388
+ btn_record_again = gr.Button("🎀 Record Again", elem_classes="btn-secondary")
389
+
390
+ with gr.Row():
391
+ btn_submit = gr.Button("✨ Get AI Feedback", elem_classes="btn-primary")
392
+
393
+ # Agentic Feedback area
394
+ feedback = gr.TextArea(
395
+ label="πŸ€– Your Personalized AI Reading Coach Feedback",
396
+ interactive=False,
397
+ elem_classes="feedback-area",
398
+ lines=12
399
+ )
400
+
401
+ # Step 3: Targeted Practice (appears when student needs more practice)
402
+ with gr.Column(elem_classes="card", visible=False) as practice_story_card:
403
+ gr.HTML("""
404
+ <div class="practice-info" style="background: linear-gradient(135deg, var(--primary-color) 0%, #48a700 100%);">
405
+ <h2>🎯 Targeted Practice Zone</h2>
406
+ <p>Your AI coach has created a special story to help you practice the words you're learning!</p>
407
+ </div>
408
+ """)
409
+
410
+ practice_info = gr.Text(label="Practice Information", interactive=False)
411
+
412
+ with gr.Row():
413
+ btn_generate_practice = gr.Button("πŸ“š Generate Practice Story", elem_classes="btn-practice")
414
+ btn_reset = gr.Button("πŸ”„ Start Fresh Session", elem_classes="btn-clear")
415
+
416
+ # Event handlers
417
+ btn_start.click(
418
+ start_session,
419
+ inputs=[name, grade, topic],
420
+ outputs=[story, practice_card, practice_story_card]
421
+ )
422
+
423
+ btn_play.click(
424
+ generate_audio,
425
+ inputs=[],
426
+ outputs=[audio_out]
427
+ )
428
+
429
+ btn_submit.click(
430
+ submit_reading,
431
+ inputs=[record],
432
+ outputs=[feedback, practice_story_card]
433
+ )
434
+
435
+ btn_generate_practice.click(
436
+ generate_practice_story,
437
+ inputs=[],
438
+ outputs=[story, practice_info]
439
+ )
440
+
441
+ btn_record_again.click(
442
+ record_again,
443
+ inputs=[],
444
+ outputs=[record]
445
+ )
446
+
447
+ btn_reset.click(
448
+ reset_session,
449
+ inputs=[],
450
+ outputs=[name, grade, topic, story, record, feedback, practice_info, practice_card, practice_story_card]
451
+ )
452
+
453
+ return demo