ParulPandey commited on
Commit
d88f855
·
verified ·
1 Parent(s): 2607e55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +451 -273
app.py CHANGED
@@ -39,30 +39,23 @@ try:
39
  except Exception: whisper_stt_client = None
40
 
41
  # --- Helper Functions ---
42
- def generate_story_from_llm(name, grade_str, topic, progress=gr.Progress(track_tqdm=True)):
43
- progress(0.0, desc="Starting story creation...")
44
  default_passage_val = ""
45
- default_audio_gen_update = gr.update(interactive=False, visible=False)
46
- default_audio_player_update = gr.update(value=None, visible=False)
47
  if not LLM_API_CONFIGURED:
48
- progress(1.0, desc="Complete")
49
- return "LLM API key not configured...", default_audio_gen_update, default_audio_player_update
50
  try:
51
  if grade_str.startswith("Grade "):
52
  grade = int(grade_str.replace("Grade ", ""))
53
  else:
54
  grade = int(grade_str)
55
  except ValueError:
56
- progress(1.0, desc="Complete")
57
- return "Invalid grade level selected.", default_audio_gen_update, default_audio_player_update
58
  if grade <= 2: word_target, max_llm_tokens = "around 40-60 words", 100
59
  elif grade <= 5: word_target, max_llm_tokens = "around 80-100 words", 200
60
  elif grade <= 8: word_target, max_llm_tokens = "around 100-120 words", 250
61
  else: word_target, max_llm_tokens = "around 120-150 words", 300
62
 
63
- progress(0.1, desc="Setting up AI story generator...")
64
  story_text_result = default_passage_val
65
- audio_gen_btn_update = default_audio_gen_update
66
  try:
67
  model = genai.GenerativeModel(model_name="gemini-1.5-flash-latest")
68
  prompt = (
@@ -78,9 +71,7 @@ def generate_story_from_llm(name, grade_str, topic, progress=gr.Progress(track_t
78
  "HARM_CATEGORY_DANGEROUS_CONTENT"
79
  ]]
80
  generation_config = genai.types.GenerationConfig(candidate_count=1, max_output_tokens=max_llm_tokens, temperature=0.7)
81
- progress(0.3, desc="AI is writing your story...")
82
  response = model.generate_content(prompt, generation_config=generation_config, safety_settings=safety_settings)
83
- progress(0.8, desc="Polishing your story...")
84
  if response.candidates and response.candidates[0].content.parts:
85
  story = response.text
86
  if response.prompt_feedback and response.prompt_feedback.block_reason:
@@ -89,51 +80,40 @@ def generate_story_from_llm(name, grade_str, topic, progress=gr.Progress(track_t
89
  story_text_result = f"The LLM couldn't generate a story for '{topic}'. Try another topic or rephrase. ✨"
90
  else:
91
  story_text_result = story.strip()
92
- audio_gen_btn_update = gr.update(interactive=True, visible=True)
93
  else:
94
  if response.prompt_feedback and response.prompt_feedback.block_reason:
95
  story_text_result = f"Story idea for '{topic}' got blocked (Reason: {response.prompt_feedback.block_reason}). Try a different topic. 😊"
96
  else:
97
  story_text_result = "Hmm, LLM had trouble with that topic. Maybe try another one? 🤔"
98
- progress(1.0, desc="Story complete!")
99
- return story_text_result, audio_gen_btn_update, default_audio_player_update
100
  except Exception as e:
101
- progress(1.0, desc="Complete")
102
- return f"Oh no! 😟 Error generating story. Details: {e}", default_audio_gen_update, default_audio_player_update
103
 
104
- def text_to_speech_using_space(text_to_speak, progress=gr.Progress(track_tqdm=True)):
 
105
  global tts_client
106
- progress(0.0, desc="🔊 Preparing voice synthesis...")
107
 
108
- if not text_to_speak or not text_to_speak.strip():
109
- progress(1.0, desc="Complete")
110
  return None
111
 
112
- progress(0.1, desc="🔊 Initializing audio generation...")
113
-
114
  # Reconnect to TTS client if needed
115
  if not tts_client:
116
- progress(0.2, desc="🔗 Connecting to advanced voice service...")
117
  try:
118
  tts_client = Client("NihalGazi/Text-To-Speech-Unlimited")
119
- progress(0.3, desc="🔗 Connected to voice service...")
120
  except Exception as e:
121
  print(f"Failed to connect to TTS service: {e}")
122
- progress(1.0, desc="Complete")
123
  return None
124
-
125
  if not tts_client:
126
- progress(1.0, desc="Complete")
127
  return None
128
 
129
- progress(0.4, desc="🎙️ AI is reading your story aloud...")
130
  try:
131
  # Try the correct API configuration with emotion parameter
132
  api_methods = [
133
- {"params": [text_to_speak, "alloy", "happy"], "api_name": "/text_to_speech_app"},
134
- {"params": [text_to_speak, "alloy", "neutral"], "api_name": "/text_to_speech_app"},
135
- {"params": [text_to_speak, "nova", "neutral"], "api_name": "/text_to_speech_app"},
136
- {"params": [text_to_speak], "api_name": "/predict"}
137
  ]
138
 
139
  audio_filepath = None
@@ -163,8 +143,6 @@ def text_to_speech_using_space(text_to_speak, progress=gr.Progress(track_tqdm=Tr
163
  continue
164
 
165
  if audio_filepath:
166
- progress(0.9, desc="🎵 Voice generation complete!")
167
- progress(1.0, desc="🔊 Audio ready!")
168
  print(f"FINAL: Returning audio file path: {audio_filepath}")
169
  return audio_filepath
170
  else:
@@ -175,13 +153,11 @@ def text_to_speech_using_space(text_to_speak, progress=gr.Progress(track_tqdm=Tr
175
  print(f"TTS error: {e}")
176
  # Try to reconnect on error
177
  try:
178
- progress(0.6, desc="🔄 Reconnecting to voice service...")
179
  tts_client = Client("NihalGazi/Text-To-Speech-Unlimited")
180
  if tts_client:
181
- progress(0.8, desc="🎙️ Retrying voice generation...")
182
  # Try the most basic approach with emotion parameter
183
  audio_result = tts_client.predict(
184
- text_to_speak,
185
  "alloy", # voice
186
  "neutral", # emotion
187
  api_name="/text_to_speech_app"
@@ -197,7 +173,6 @@ def text_to_speech_using_space(text_to_speak, progress=gr.Progress(track_tqdm=Tr
197
  audio_filepath = audio_result[0]
198
 
199
  if audio_filepath:
200
- progress(1.0, desc="🔊 Audio ready!")
201
  print(f"RETRY SUCCESS: Returning audio file path: {audio_filepath}")
202
  return audio_filepath
203
 
@@ -205,23 +180,17 @@ def text_to_speech_using_space(text_to_speak, progress=gr.Progress(track_tqdm=Tr
205
  print(f"TTS retry failed: {retry_error}")
206
  pass
207
 
208
- progress(1.0, desc="Audio generation failed")
209
  print("TTS failed completely - returning None")
210
  return None
211
 
212
- def speech_to_text_whisper_space(audio_filepath, progress=gr.Progress(track_tqdm=True), max_retries=3):
213
- progress(0.1, desc="Sending your reading for transcription...")
214
  if not whisper_stt_client:
215
- progress(1.0, desc="Complete")
216
  return "Speech-to-text service is not available. 🛠️"
217
  if not audio_filepath:
218
- progress(1.0, desc="Complete")
219
  return "No recording received for transcription. 🎤"
220
  for attempt in range(max_retries):
221
  try:
222
- progress(0.2 + (attempt * 0.1), desc=f"Transcribing your voice (Whisper) - Attempt {attempt + 1}...")
223
  result = whisper_stt_client.predict(audio_filepath, api_name="/predict")
224
- progress(0.9, desc="Transcription complete.")
225
  if isinstance(result, tuple) and len(result) > 0:
226
  transcribed_text = result[0] if result[0] else ""
227
  elif isinstance(result, list) and len(result) > 0:
@@ -229,13 +198,10 @@ def speech_to_text_whisper_space(audio_filepath, progress=gr.Progress(track_tqdm
229
  elif isinstance(result, str):
230
  transcribed_text = result
231
  else:
232
- progress(1.0, desc="Complete")
233
  return "Hmm, STT service returned unexpected format. 🤔"
234
- progress(1.0, desc="Transcription complete!")
235
  return transcribed_text if transcribed_text else "No speech detected in the recording. 🤫"
236
  except Exception:
237
  continue
238
- progress(1.0, desc="Complete")
239
  return "Unexpected error during transcription. Please try again! 🔄"
240
 
241
  def clean_text_for_comparison(text):
@@ -246,25 +212,64 @@ def clean_text_for_comparison(text):
246
  def compare_texts_for_feedback(original_text, student_text):
247
  original_words, student_words = clean_text_for_comparison(original_text), clean_text_for_comparison(student_text)
248
  if not student_words: return "It sounds like you didn't record or it was very quiet! 🤫 Try recording again nice and clear!", ""
 
 
 
 
 
249
  matcher = difflib.SequenceMatcher(None, original_words, student_words, autojunk=False)
250
- feedback_lines, highlighted_parts = [], []
251
- word_diff_count = 0
252
- pronunciation_tips = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  pronunciation_guide = {
254
  'the': 'thuh or thee', 'through': 'threw', 'though': 'thoh', 'thought': 'thawt',
255
  'knight': 'night', 'know': 'noh', 'write': 'right', 'wrong': 'rawng', 'what': 'wot',
256
  'where': 'wair', 'when': 'wen', 'why': 'wy', 'who': 'hoo', 'laugh': 'laff',
257
  'enough': 'ee-nuff', 'cough': 'koff', 'rough': 'ruff', 'tough': 'tuff', 'magic': 'maj-ik',
258
  'school': 'skool', 'friend': 'frend', 'said': 'sed', 'says': 'sez', 'once': 'wunts',
259
- 'was': 'wuz', 'were': 'wur', 'you': 'yoo', 'your': 'yor', 'there': 'thair', 'their': 'thair', 'they': 'thay'
 
 
 
260
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  def get_pronunciation_tip(word):
262
  word_lower = word.lower()
263
  if word_lower in pronunciation_guide:
264
- return f"🗣️ Try saying: \"{pronunciation_guide[word_lower]}\""
265
  elif len(word) > 6:
266
- syllables = []
267
  vowels = 'aeiou'
 
268
  current_syllable = ''
269
  for i, char in enumerate(word_lower):
270
  current_syllable += char
@@ -273,83 +278,264 @@ def compare_texts_for_feedback(original_text, student_text):
273
  syllables.append(current_syllable)
274
  current_syllable = ''
275
  if current_syllable: syllables.append(current_syllable)
276
- if len(syllables) > 1: return f"🔤 Break it down: \"{'-'.join(syllables)}\""
277
- if word_lower.endswith('tion'): return "🗣️ Words ending in '-tion' sound like 'shun'"
278
- elif word_lower.endswith('ough'): return "🗣️ '-ough' can be tricky! Listen to the audio again"
279
- elif 'gh' in word_lower: return "🗣️ 'gh' is often silent or sounds like 'f'"
280
- elif word_lower.startswith('wr'): return "🗣️ In 'wr-' words, the 'w' is silent"
281
- elif word_lower.startswith('kn'): return "🗣️ In 'kn-' words, the 'k' is silent"
282
- return f"🎯 Focus on each sound in \"{word}\""
283
  for tag, i1, i2, j1, j2 in matcher.get_opcodes():
284
  orig_seg_words, stud_seg_words = original_words[i1:i2], student_words[j1:j2]
285
  orig_seg_text, stud_seg_text = " ".join(orig_seg_words), " ".join(stud_seg_words)
 
286
  if tag == 'equal':
287
- highlighted_parts.append(f'<span style="background: #90EE90; padding: 2px 4px; border-radius: 4px; margin: 1px;">{orig_seg_text}</span>')
288
- else:
289
- word_diff_count += max(len(orig_seg_words), len(stud_seg_words))
290
- if tag == 'replace':
291
- for orig_word, stud_word in zip(orig_seg_words, stud_seg_words):
292
- if orig_word != stud_word:
293
- tip = get_pronunciation_tip(orig_word)
294
- pronunciation_tips.append(f"**{orig_word.upper()}**: {tip}")
295
- feedback_lines.append(f"🔄 Instead of: \"{orig_seg_text}\", you said: \"{stud_seg_text}\"")
296
- highlighted_parts.append(f'<span style="background: #FFE4B5; padding: 2px 4px; border-radius: 4px; margin: 1px; text-decoration: line-through;">{orig_seg_text}</span> <span style="background: #FFB6C1; padding: 2px 4px; border-radius: 4px; margin: 1px; font-weight: bold;">{stud_seg_text}</span>')
297
- elif tag == 'delete':
298
- for missed_word in orig_seg_words:
299
- tip = get_pronunciation_tip(missed_word)
300
- pronunciation_tips.append(f"**{missed_word.upper()}** (missed): {tip}")
301
- feedback_lines.append(f"⏭️ You missed: \"{orig_seg_text}\"")
302
- highlighted_parts.append(f'<span style="background: #FFA0B4; padding: 2px 4px; border-radius: 4px; margin: 1px; text-decoration: line-through;">{orig_seg_text}</span> <span style="font-style: italic; color: #666;">(*skipped*)</span>')
303
- elif tag == 'insert':
304
- feedback_lines.append(f"➕ You added: \"{stud_seg_text}\" (not in original)")
305
- highlighted_parts.append(f'<span style="background: #DDA0DD; padding: 2px 4px; border-radius: 4px; margin: 1px; font-style: italic;">(*added:* {stud_seg_text})</span>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  final_text = " ".join(highlighted_parts)
307
- if not feedback_lines:
308
- feedback_html = """
309
- 🎉🥳 **PERFECT READING!** 🥳🎉
310
- Amazing! You read every single word correctly! 🌟
311
- 🏆 **Reading Champion!** 🏆
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  """
313
  return feedback_html, final_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  else:
315
- feedback_parts = [
316
- f"📈 **Reading Progress Report**",
317
- f"📊 **Words to practice:** {word_diff_count}",
318
- f"💪 **Keep improving!** Practice makes perfect!",
319
- "",
320
- "🔍 **What to work on:**"
321
  ]
322
- for line in feedback_lines: feedback_parts.append(f"• {line}")
323
- if pronunciation_tips:
324
- feedback_parts.extend([
325
- "",
326
- "🎤 **Pronunciation Helper**",
327
- "Here's how to say the tricky words:"
328
- ])
329
- for tip in pronunciation_tips[:5]: feedback_parts.append(f"• {tip}")
330
- feedback_parts.extend([
331
- "",
332
- "💡 **Pro tip:** Listen to the story audio again and pay special attention to these words!"
333
- ])
334
- feedback_parts.extend([
335
- "",
336
- "🎯 **Practice Suggestions**",
337
- "• 🎧 Listen to the AI reading first",
338
- "• 🔤 Practice saying difficult words slowly",
339
- "• 📖 Read the story again at your own pace",
340
- "• 🔄 Try recording again when you're ready!"
341
- ])
342
- feedback_html = "\n".join(feedback_parts)
343
- return feedback_html, final_text
344
 
345
- def assess_student_reading_ui(original_passage_state, student_audio_path, progress=gr.Progress(track_tqdm=True)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  if not student_audio_path: return "🎤 Please record your reading first!", ""
347
  if not original_passage_state: return "Hmm, the original story is missing. 😟 Please generate a story first.", ""
348
- transcribed_text = speech_to_text_whisper_space(student_audio_path, progress=progress)
349
  stt_errors = ["couldn't understand", "had trouble", "service isn't working", "service is busy", "didn't get any recording", "filepath type issue"]
350
  if any(err in (transcribed_text or "").lower() for err in stt_errors): return transcribed_text, ""
351
- progress(0.6, desc="Analyzing your reading accuracy..."); feedback, highlighted_passage = compare_texts_for_feedback(original_passage_state, transcribed_text)
352
- progress(1.0, desc="Assessment complete!"); return feedback, highlighted_passage
353
 
354
  css = """
355
  body, .gradio-container {
@@ -357,17 +543,54 @@ body, .gradio-container {
357
  font-family: -apple-system, BlinkMacSystemFont, 'San Francisco', 'Segoe UI', 'Roboto', Arial, sans-serif !important;
358
  }
359
  .main-header {
360
- background: white !important;
361
- border-radius: 20px !important;
362
- box-shadow: 0 8px 32px 0 rgba(60,60,90,0.06) !important;
363
- padding: 36px 20px 24px 20px !important;
364
- margin-bottom: 28px !important;
 
365
  text-align: center;
366
  border: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
368
- .main-header h1 {font-size: 2.2rem !important; font-weight: 700 !important; color: #23232b !important;}
369
- .main-header p {color: #6b7280 !important; font-size: 1.08rem !important; margin-bottom: 8px !important;}
370
- .tech-badge {background: #e0e7ef !important; color: #4f8fff !important; border-radius: 12px !important; padding: 4px 12px !important; font-size: 12px !important; font-weight: 600 !important;}
371
  .gr-block, .gr-panel {background: white !important; border-radius: 18px !important; box-shadow: 0 2px 8px 0 rgba(60,60,90,0.07) !important; border: none !important; padding: 28px 22px !important;}
372
  .section-header {background: transparent !important; border: none !important; padding: 0 !important; margin-bottom: 16px !important;}
373
  .section-header h3 {color: #1e293b !important; font-size: 1.14rem !important; font-weight: 600 !important;}
@@ -404,7 +627,7 @@ body, .gradio-container {
404
  background: linear-gradient(90deg, #e0e7ef, #dde5f2) !important;
405
  color: #2a3140 !important;
406
  transition: all 0.15s cubic-bezier(0.4,0.0,0.2,1) !important;
407
- transform: translateY(0) !important;
408
  }
409
 
410
  .gr-button[variant="secondary"]:hover {
@@ -480,7 +703,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
480
  audio_out = gr.Audio(label="🎵 Story Audio", type="filepath", visible=True, autoplay=False)
481
  gr.Markdown("""
482
  <div style="margin: 20px 0 0 0; padding: 10px 20px; background: #f4f7fa; border-radius: 16px;">
483
- <b>➡️ Next:</b> Record yourself reading below for feedback.
484
  </div>
485
  """)
486
  stud_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="🎤 Your Recording")
@@ -489,53 +712,56 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
489
  assess_btn = gr.Button("🔍 Analyze Reading", variant="primary", size="lg", interactive=False)
490
  recording_status = gr.Markdown("", elem_id="recording_status")
491
  analysis_status = gr.Markdown("", elem_id="analysis_status")
492
- with gr.TabItem("📊 Analysis & Feedback", elem_id="analysis_tab"):
493
- gr.Markdown("""
494
- <div class="section-header">
495
- <h3>📈 Analysis</h3>
496
- <p>See your performance and areas to improve</p>
497
- </div>
498
- """)
499
- feedback_out = gr.Markdown(
500
- value="""
501
- <div style="text-align: center; color: #6b7280;">
502
- <h4>Analysis Results</h4>
503
- <p>Your feedback will appear here.</p>
504
- </div>
505
- """,
506
- elem_id="feedback_output"
507
- )
508
- highlighted_out = gr.Markdown(
509
- value="""
510
- <div style="text-align: center; color: #6b7280;">
511
- <h4>Word-by-Word Analysis</h4>
512
- <p>Get color-coded feedback below.</p>
513
- </div>
514
- """,
515
- elem_id="highlighted_passage_output"
516
- )
517
- gr.Markdown("""
518
- <div style="background: #f7fafc; border-radius: 16px; padding: 16px 12px 10px 12px; margin: 22px 0 18px 0; box-shadow: 0 2px 6px 0 rgba(60,60,90,0.04);">
519
- <b>Color code:</b>
520
- <div style="display: flex; gap: 14px; flex-wrap: wrap; margin-top: 8px;">
521
- <span style="background: #90EE90; padding: 5px 14px; border-radius: 12px; font-size: 13px; font-weight: 500; color: #155724;">Perfect Match</span>
522
- <span style="background: #FFE4B5; padding: 5px 14px; border-radius: 12px; font-size: 13px; font-weight: 500; color: #856404;">Substitution</span>
523
- <span style="background: #FFA0B4; padding: 5px 14px; border-radius: 12px; font-size: 13px; font-weight: 500; color: #721c24;">Skipped Word</span>
524
- <span style="background: #DDA0DD; padding: 5px 14px; border-radius: 12px; font-size: 13px; font-weight: 500; color: #5f006a;">Extra Word</span>
525
- </div>
526
- </div>
527
- """)
528
- gr.Markdown("""
529
- <div style="margin: 14px 0; padding: 14px 22px; background: #f8fafc; border-radius: 14px;">
530
- <span style="color: #0a58ca; font-weight: 500;">Goals:</span>
531
- <ul style="margin: 7px 0 0 18px; color: #6b7280;">
532
- <li>Word accuracy above 90%</li>
533
- <li>Speak clearly and with confidence</li>
534
- <li>Practice as much as you like</li>
535
- </ul>
536
- </div>
537
- """)
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  with gr.TabItem("ℹ️ About & How It Works", elem_id="about_tab"):
540
  gr.Markdown("""
541
  <div class="section-header">
@@ -544,23 +770,10 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
544
  </div>
545
  """)
546
 
547
- gr.Markdown("""
548
- ## 🎯 What This Platform Does
549
-
550
- ReadRight is an AI-powered tool designed to help students improve their reading skills through:
551
-
552
- - **✨ Personalized Story Generation**: Creates age-appropriate reading passages tailored to your grade level and interests
553
- - **🔊 Audio Pronunciation Models**: Provides clear audio examples of proper pronunciation
554
- - **⚡ Real-time Speech Analysis**: Analyzes your reading accuracy and identifies areas for improvement
555
- - **🎯 Detailed Feedback**: Offers specific pronunciation tips and practice suggestions
556
-
557
- ## 🏗️ Reading Practice Application Workflow
558
- """)
559
-
560
- # Use HTML component for the SVG
561
  gr.HTML("""
562
- <div style="width: 100%; overflow-x: auto; padding: 20px 0;">
563
- <svg width="1400" height="700" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto;">
 
564
  <!-- Background -->
565
  <rect width="1400" height="600" fill="#fafafa"/>
566
 
@@ -656,6 +869,15 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
656
  """)
657
 
658
  gr.Markdown("""
 
 
 
 
 
 
 
 
 
659
  ---
660
 
661
  ## 🔧 Key Components
@@ -685,7 +907,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
685
  passage_state = story_text
686
  return story_text, audio_btn_update, audio_player_update, passage_state
687
 
688
- def assess_reading_with_analysis(original_passage_state, student_audio_path, progress=gr.Progress(track_tqdm=True)):
689
  if not student_audio_path:
690
  return (
691
  """
@@ -707,12 +929,8 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
707
  ""
708
  )
709
 
710
- # Immediate feedback that analysis is starting
711
- progress(0.05, desc="Analysis starting...")
712
-
713
  # Start transcription
714
- progress(0.1, desc="Starting transcription...")
715
- transcribed_text = speech_to_text_whisper_space(student_audio_path, progress=progress)
716
 
717
  stt_errors = ["couldn't understand", "had trouble", "service isn't working", "service is busy", "didn't get any recording", "filepath type issue"]
718
  if any(err in (transcribed_text or "").lower() for err in stt_errors):
@@ -727,9 +945,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
727
  ""
728
  )
729
 
730
- progress(0.6, desc="Analyzing your reading accuracy...")
731
  feedback, highlighted_passage = compare_texts_for_feedback(original_passage_state, transcribed_text)
732
- progress(1.0, desc="Assessment complete!")
733
 
734
  analysis_msg = """
735
  <div class="status-indicator status-success">
@@ -739,6 +955,28 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
739
  """
740
  return (analysis_msg, feedback, highlighted_passage)
741
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  def update_recording_status(audio_file):
743
  if audio_file is not None:
744
  return (
@@ -810,89 +1048,29 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="ReadRight") as app:
810
  gr.update(visible=False),
811
  gr.update(interactive=False)
812
  )
813
-
814
- def handle_audio_generation(story_text, progress=gr.Progress(track_tqdm=True)):
815
- """Handle audio generation with visual progress indicator"""
816
- if not story_text or not story_text.strip():
817
- return (
818
- gr.update(value=None, visible=True),
819
- gr.update(value="", visible=False)
820
- )
821
-
822
- # Generate the audio file
823
- audio_filepath = text_to_speech_using_space(story_text, progress)
824
- print(f"AUDIO HANDLER: Received audio file path: {audio_filepath}")
825
-
826
- if audio_filepath:
827
- print(f"AUDIO HANDLER: Updating audio component with file: {audio_filepath}")
828
- success_msg = """
829
- <div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 12px; padding: 12px 20px; margin: 8px 0; text-align: center;">
830
- <span style="color: #15803d; font-weight: 500;">✅ Audio ready! You can now listen to your story.</span>
831
- </div>
832
- """
833
- return (
834
- gr.update(value=audio_filepath, visible=True),
835
- gr.update(value=success_msg, visible=True)
836
- )
837
- else:
838
- print("AUDIO HANDLER: No audio file received, returning None")
839
- error_msg = """
840
- <div style="background: #fef2f2; border: 1px solid #ef4444; border-radius: 12px; padding: 12px 20px; margin: 8px 0; text-align: center;">
841
- <span style="color: #dc2626; font-weight: 500;">❌ Audio generation failed. Please try again.</span>
842
- </div>
843
- """
844
- return (
845
- gr.update(value=None, visible=True),
846
- gr.update(value=error_msg, visible=True)
847
- )
848
 
849
- def generate_story_and_audio_automatically(name, grade, topic, progress=gr.Progress(track_tqdm=True)):
850
- """Generate story and automatically create audio in one seamless flow"""
851
- progress(0.0, desc="Starting story creation...")
852
-
853
- # First generate the story using the existing function
854
- story_result = generate_story_from_llm(name, grade, topic, progress)
855
- if not story_result:
856
- return "", gr.update(value=None, visible=True), ""
857
-
858
- # Extract story text from the result tuple
859
- story_text = story_result[0] if isinstance(story_result, tuple) else story_result
860
-
861
- # Check if story generation was successful
862
- if not story_text or any(err in story_text.lower() for err in ["error", "blocked", "couldn't", "api key not configured"]):
863
- return story_text, gr.update(value=None, visible=True), ""
864
-
865
- # Story generated successfully, now automatically generate audio
866
- progress(0.5, desc="Story complete! Now generating audio...")
867
-
868
- try:
869
- # Generate audio automatically
870
- audio_filepath = text_to_speech_using_space(story_text, progress)
871
-
872
- if audio_filepath:
873
- print(f"AUTO AUDIO: Successfully generated audio: {audio_filepath}")
874
- return story_text, gr.update(value=audio_filepath, visible=True), story_text
875
- else:
876
- print("AUTO AUDIO: Audio generation failed, but story is still available")
877
- return story_text, gr.update(value=None, visible=True), story_text
878
-
879
- except Exception as e:
880
- print(f"AUTO AUDIO ERROR: {e}")
881
- return story_text, gr.update(value=None, visible=True), story_text
882
-
883
- # Event handlers with automatic audio generation
884
  gen_btn.click(
885
- fn=generate_story_and_audio_automatically,
886
  inputs=[s_name, s_grade, s_topic],
887
- outputs=[passage_out, audio_out, original_passage_state],
888
- show_progress=True
 
 
 
 
 
 
 
 
 
 
889
  )
890
 
891
  assess_btn.click(
892
  fn=assess_reading_with_analysis,
893
  inputs=[original_passage_state, stud_audio_in],
894
- outputs=[analysis_status, feedback_out, highlighted_out],
895
- show_progress=True
896
  )
897
 
898
  stud_audio_in.change(
 
39
  except Exception: whisper_stt_client = None
40
 
41
  # --- Helper Functions ---
42
+ def generate_story_from_llm(name, grade_str, topic):
 
43
  default_passage_val = ""
 
 
44
  if not LLM_API_CONFIGURED:
45
+ return "LLM API key not configured..."
 
46
  try:
47
  if grade_str.startswith("Grade "):
48
  grade = int(grade_str.replace("Grade ", ""))
49
  else:
50
  grade = int(grade_str)
51
  except ValueError:
52
+ return "Invalid grade level selected."
 
53
  if grade <= 2: word_target, max_llm_tokens = "around 40-60 words", 100
54
  elif grade <= 5: word_target, max_llm_tokens = "around 80-100 words", 200
55
  elif grade <= 8: word_target, max_llm_tokens = "around 100-120 words", 250
56
  else: word_target, max_llm_tokens = "around 120-150 words", 300
57
 
 
58
  story_text_result = default_passage_val
 
59
  try:
60
  model = genai.GenerativeModel(model_name="gemini-1.5-flash-latest")
61
  prompt = (
 
71
  "HARM_CATEGORY_DANGEROUS_CONTENT"
72
  ]]
73
  generation_config = genai.types.GenerationConfig(candidate_count=1, max_output_tokens=max_llm_tokens, temperature=0.7)
 
74
  response = model.generate_content(prompt, generation_config=generation_config, safety_settings=safety_settings)
 
75
  if response.candidates and response.candidates[0].content.parts:
76
  story = response.text
77
  if response.prompt_feedback and response.prompt_feedback.block_reason:
 
80
  story_text_result = f"The LLM couldn't generate a story for '{topic}'. Try another topic or rephrase. ✨"
81
  else:
82
  story_text_result = story.strip()
 
83
  else:
84
  if response.prompt_feedback and response.prompt_feedback.block_reason:
85
  story_text_result = f"Story idea for '{topic}' got blocked (Reason: {response.prompt_feedback.block_reason}). Try a different topic. 😊"
86
  else:
87
  story_text_result = "Hmm, LLM had trouble with that topic. Maybe try another one? 🤔"
88
+ return story_text_result
 
89
  except Exception as e:
90
+ return f"Oh no! 😟 Error generating story. Details: {e}"
 
91
 
92
+ def text_to_speech_using_space_simple(text):
93
+ """Simplified TTS function - Gradio will show its default loading indicator"""
94
  global tts_client
 
95
 
96
+ if not text or not text.strip():
 
97
  return None
98
 
 
 
99
  # Reconnect to TTS client if needed
100
  if not tts_client:
 
101
  try:
102
  tts_client = Client("NihalGazi/Text-To-Speech-Unlimited")
 
103
  except Exception as e:
104
  print(f"Failed to connect to TTS service: {e}")
 
105
  return None
106
+
107
  if not tts_client:
 
108
  return None
109
 
 
110
  try:
111
  # Try the correct API configuration with emotion parameter
112
  api_methods = [
113
+ {"params": [text, "alloy", "happy"], "api_name": "/text_to_speech_app"},
114
+ {"params": [text, "alloy", "neutral"], "api_name": "/text_to_speech_app"},
115
+ {"params": [text, "nova", "neutral"], "api_name": "/text_to_speech_app"},
116
+ {"params": [text], "api_name": "/predict"}
117
  ]
118
 
119
  audio_filepath = None
 
143
  continue
144
 
145
  if audio_filepath:
 
 
146
  print(f"FINAL: Returning audio file path: {audio_filepath}")
147
  return audio_filepath
148
  else:
 
153
  print(f"TTS error: {e}")
154
  # Try to reconnect on error
155
  try:
 
156
  tts_client = Client("NihalGazi/Text-To-Speech-Unlimited")
157
  if tts_client:
 
158
  # Try the most basic approach with emotion parameter
159
  audio_result = tts_client.predict(
160
+ text,
161
  "alloy", # voice
162
  "neutral", # emotion
163
  api_name="/text_to_speech_app"
 
173
  audio_filepath = audio_result[0]
174
 
175
  if audio_filepath:
 
176
  print(f"RETRY SUCCESS: Returning audio file path: {audio_filepath}")
177
  return audio_filepath
178
 
 
180
  print(f"TTS retry failed: {retry_error}")
181
  pass
182
 
 
183
  print("TTS failed completely - returning None")
184
  return None
185
 
186
+ def speech_to_text_whisper_space(audio_filepath, max_retries=3):
 
187
  if not whisper_stt_client:
 
188
  return "Speech-to-text service is not available. 🛠️"
189
  if not audio_filepath:
 
190
  return "No recording received for transcription. 🎤"
191
  for attempt in range(max_retries):
192
  try:
 
193
  result = whisper_stt_client.predict(audio_filepath, api_name="/predict")
 
194
  if isinstance(result, tuple) and len(result) > 0:
195
  transcribed_text = result[0] if result[0] else ""
196
  elif isinstance(result, list) and len(result) > 0:
 
198
  elif isinstance(result, str):
199
  transcribed_text = result
200
  else:
 
201
  return "Hmm, STT service returned unexpected format. 🤔"
 
202
  return transcribed_text if transcribed_text else "No speech detected in the recording. 🤫"
203
  except Exception:
204
  continue
 
205
  return "Unexpected error during transcription. Please try again! 🔄"
206
 
207
  def clean_text_for_comparison(text):
 
212
  def compare_texts_for_feedback(original_text, student_text):
213
  original_words, student_words = clean_text_for_comparison(original_text), clean_text_for_comparison(student_text)
214
  if not student_words: return "It sounds like you didn't record or it was very quiet! 🤫 Try recording again nice and clear!", ""
215
+
216
+ # Enhanced analysis metrics
217
+ total_original_words = len(original_words)
218
+ total_student_words = len(student_words)
219
+
220
  matcher = difflib.SequenceMatcher(None, original_words, student_words, autojunk=False)
221
+ highlighted_parts = []
222
+
223
+ # Detailed tracking
224
+ correct_words = 0
225
+ substituted_words = 0
226
+ missed_words = 0
227
+ extra_words = 0
228
+
229
+ # New improved tracking
230
+ challenging_words = []
231
+ skill_areas = {
232
+ 'accuracy': {'score': 0, 'tips': []},
233
+ 'fluency': {'score': 0, 'tips': []},
234
+ 'pronunciation': {'score': 0, 'tips': []}
235
+ }
236
+
237
+ # Enhanced pronunciation guide with more words
238
  pronunciation_guide = {
239
  'the': 'thuh or thee', 'through': 'threw', 'though': 'thoh', 'thought': 'thawt',
240
  'knight': 'night', 'know': 'noh', 'write': 'right', 'wrong': 'rawng', 'what': 'wot',
241
  'where': 'wair', 'when': 'wen', 'why': 'wy', 'who': 'hoo', 'laugh': 'laff',
242
  'enough': 'ee-nuff', 'cough': 'koff', 'rough': 'ruff', 'tough': 'tuff', 'magic': 'maj-ik',
243
  'school': 'skool', 'friend': 'frend', 'said': 'sed', 'says': 'sez', 'once': 'wunts',
244
+ 'was': 'wuz', 'were': 'wur', 'you': 'yoo', 'your': 'yor', 'there': 'thair', 'their': 'thair', 'they': 'thay',
245
+ 'because': 'bee-koz', 'beautiful': 'byoo-ti-ful', 'different': 'dif-er-ent', 'important': 'im-por-tant',
246
+ 'people': 'pee-pul', 'together': 'too-geth-er', 'water': 'waw-ter', 'favorite': 'fay-vor-it',
247
+ 'journey': 'jur-nee', 'treasure': 'trezh-er', 'adventure': 'ad-ven-cher', 'mysterious': 'mis-teer-ee-us'
248
  }
249
+
250
+ def identify_challenging_words(text_words):
251
+ """Identify potentially difficult words from the story for proactive help"""
252
+ challenging = []
253
+ for word in text_words:
254
+ word_lower = word.lower()
255
+ # Add words that are commonly mispronounced or complex
256
+ if (len(word) > 6 or # Long words
257
+ word_lower in pronunciation_guide or # Known difficult words
258
+ 'tion' in word_lower or 'ough' in word_lower or # Tricky endings
259
+ word_lower.startswith(('wr', 'kn', 'ph')) or # Silent letters
260
+ 'gh' in word_lower or 'th' in word_lower): # Difficult sounds
261
+ if word_lower not in challenging:
262
+ challenging.append(word_lower)
263
+ return challenging[:5] # Limit to 5 most relevant words
264
+
265
  def get_pronunciation_tip(word):
266
  word_lower = word.lower()
267
  if word_lower in pronunciation_guide:
268
+ return pronunciation_guide[word_lower]
269
  elif len(word) > 6:
270
+ # Simple syllable breakdown
271
  vowels = 'aeiou'
272
+ syllables = []
273
  current_syllable = ''
274
  for i, char in enumerate(word_lower):
275
  current_syllable += char
 
278
  syllables.append(current_syllable)
279
  current_syllable = ''
280
  if current_syllable: syllables.append(current_syllable)
281
+ if len(syllables) > 1: return '-'.join(syllables)
282
+ return word_lower
283
+
284
+ # Process each operation in the diff for highlighting
 
 
 
285
  for tag, i1, i2, j1, j2 in matcher.get_opcodes():
286
  orig_seg_words, stud_seg_words = original_words[i1:i2], student_words[j1:j2]
287
  orig_seg_text, stud_seg_text = " ".join(orig_seg_words), " ".join(stud_seg_words)
288
+
289
  if tag == 'equal':
290
+ correct_words += len(orig_seg_words)
291
+ highlighted_parts.append(f'<span style="background: #22c55e; color: white; padding: 3px 6px; border-radius: 6px; margin: 2px; font-weight: 500;">{orig_seg_text}</span>')
292
+ elif tag == 'replace':
293
+ substituted_words += len(orig_seg_words)
294
+ highlighted_parts.append(f'<span style="background: #f59e0b; color: white; padding: 3px 6px; border-radius: 6px; margin: 2px; font-weight: 500; text-decoration: line-through;">{orig_seg_text}</span> <span style="background: #ef4444; color: white; padding: 3px 6px; border-radius: 6px; margin: 2px; font-weight: 500;">→{stud_seg_text}</span>')
295
+ elif tag == 'delete':
296
+ missed_words += len(orig_seg_words)
297
+ highlighted_parts.append(f'<span style="background: #ef4444; color: white; padding: 3px 6px; border-radius: 6px; margin: 2px; font-weight: 500; text-decoration: line-through;">{orig_seg_text}</span> <span style="font-style: italic; color: #9ca3af; font-size: 0.9em;">(skipped)</span>')
298
+ elif tag == 'insert':
299
+ extra_words += len(stud_seg_words)
300
+ highlighted_parts.append(f'<span style="background: #8b5cf6; color: white; padding: 3px 6px; border-radius: 6px; margin: 2px; font-weight: 500; font-style: italic;">+{stud_seg_text}</span>')
301
+
302
+ # Calculate comprehensive metrics
303
+ accuracy_percentage = round((correct_words / total_original_words) * 100, 1) if total_original_words > 0 else 0
304
+
305
+ # Determine performance level
306
+ if accuracy_percentage >= 95:
307
+ performance_level = "🏆 Excellent"
308
+ performance_color = "#10b981"
309
+ performance_message = "Outstanding reading! You're reading like a champion!"
310
+ elif accuracy_percentage >= 85:
311
+ performance_level = "🌟 Very Good"
312
+ performance_color = "#3b82f6"
313
+ performance_message = "Great job! You're doing really well with your reading."
314
+ elif accuracy_percentage >= 70:
315
+ performance_level = "💪 Good Progress"
316
+ performance_color = "#f59e0b"
317
+ performance_message = "Nice work! Keep practicing to improve even more."
318
+ elif accuracy_percentage >= 50:
319
+ performance_level = "📚 Keep Practicing"
320
+ performance_color = "#ef4444"
321
+ performance_message = "You're learning! More practice will help you improve."
322
+ else:
323
+ performance_level = "🚀 Just Getting Started"
324
+ performance_color = "#8b5cf6"
325
+ performance_message = "Every reader starts somewhere! Keep trying and you'll get better."
326
+
327
+ # Generate challenging words for proactive help
328
+ challenging_words = identify_challenging_words(original_words)
329
+
330
+ # Assess skill areas
331
+ skill_areas['accuracy']['score'] = accuracy_percentage
332
+ if accuracy_percentage < 90:
333
+ skill_areas['accuracy']['tips'] = ['Practice reading slowly and clearly', 'Follow along with the text while listening']
334
+
335
+ skill_areas['fluency']['score'] = max(0, 100 - (missed_words * 10))
336
+ if missed_words > 2:
337
+ skill_areas['fluency']['tips'] = ['Try reading the story multiple times', 'Practice difficult words separately first']
338
+
339
+ skill_areas['pronunciation']['score'] = max(0, 100 - (substituted_words * 15))
340
+ if substituted_words > 1:
341
+ skill_areas['pronunciation']['tips'] = ['Listen carefully to each word sound', 'Break long words into smaller parts']
342
+
343
  final_text = " ".join(highlighted_parts)
344
+
345
+ # Perfect reading case
346
+ if accuracy_percentage == 100:
347
+ feedback_html = f"""
348
+ <div style="background: linear-gradient(135deg, #10b981, #059669); padding: 24px; border-radius: 16px; color: white; text-align: center; margin-bottom: 20px;">
349
+ <h2 style="margin: 0 0 8px 0; font-size: 1.8rem;">🎉 PERFECT READING! 🎉</h2>
350
+ <p style="margin: 0; font-size: 1.1rem; opacity: 0.9;">Amazing! You read every single word correctly!</p>
351
+ </div>
352
+
353
+ <div style="background: #f0fdf4; border: 2px solid #22c55e; border-radius: 12px; padding: 20px; margin-bottom: 16px;">
354
+ <h3 style="color: #15803d; margin: 0 0 12px 0;">📊 Your Reading Score</h3>
355
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
356
+ <div style="text-align: center;">
357
+ <div style="font-size: 2rem; font-weight: bold; color: #15803d;">100%</div>
358
+ <div style="font-size: 0.9rem; color: #166534;">Word Accuracy</div>
359
+ </div>
360
+ <div style="text-align: center;">
361
+ <div style="font-size: 2rem; font-weight: bold, color: #15803d;">{total_original_words}/{total_original_words}</div>
362
+ <div style="font-size: 0.9rem, color: #166534;">Words Correct</div>
363
+ </div>
364
+ </div>
365
+ <div style="text-align: center; padding: 12px; background: #dcfce7; border-radius: 8px;">
366
+ <strong style="color: #15803d;">🏆 Reading Champion Level!</strong>
367
+ </div>
368
+ </div>
369
+
370
+ <div style="background: #fffbeb; border-radius: 12px; padding: 16px;">
371
+ <h4 style="color: #92400e; margin: 0 0 8px 0;">🎯 What's Next?</h4>
372
+ <ul style="margin: 8px 0; padding-left: 20px; color: #78350f;">
373
+ <li>Try a more challenging story topic</li>
374
+ <li>Practice reading faster while staying accurate</li>
375
+ <li>Help a friend or family member practice reading</li>
376
+ <li>Celebrate your excellent reading skills! 🎊</li>
377
+ </ul>
378
+ </div>
379
  """
380
  return feedback_html, final_text
381
+
382
+ # Improved analysis with non-repetitive, skill-focused feedback
383
+ feedback_html = f"""
384
+ <div style="background: linear-gradient(135deg, {performance_color}, {performance_color}dd); padding: 20px; border-radius: 16px; color: white; text-align: center; margin-bottom: 20px;">
385
+ <h2 style="margin: 0 0 8px 0; font-size: 1.6rem;">{performance_level}</h2>
386
+ <p style="margin: 0; font-size: 1rem; opacity: 0.9;">{performance_message}</p>
387
+ </div>
388
+
389
+ <div style="background: #f8fafc; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
390
+ <h3 style="color: #1e293b; margin: 0 0 16px 0;">📊 Reading Dashboard</h3>
391
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; margin-bottom: 16px;">
392
+ <div style="text-align: center; background: white; padding: 12px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
393
+ <div style="font-size: 1.8rem; font-weight: bold; color: {performance_color};">{accuracy_percentage}%</div>
394
+ <div style="font-size: 0.85rem; color: #64748b;">Accuracy</div>
395
+ </div>
396
+ <div style="text-align: center; background: white; padding: 12px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
397
+ <div style="font-size: 1.8rem; font-weight: bold; color: #22c55e;">{correct_words}</div>
398
+ <div style="font-size: 0.85rem; color: #64748b;">Words Correct</div>
399
+ </div>
400
+ <div style="text-align: center; background: white; padding: 12px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
401
+ <div style="font-size: 1.8rem; font-weight: bold; color: #ef4444;">{missed_words}</div>
402
+ <div style="font-size: 0.85rem; color: #64748b;">Missed</div>
403
+ </div>
404
+ <div style="text-align: center; background: white; padding: 12px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
405
+ <div style="font-size: 1.8rem; font-weight: bold; color: #f59e0b;">{substituted_words}</div>
406
+ <div style="font-size: 0.85rem; color: #64748b;">Changed</div>
407
+ </div>
408
+ </div>
409
+
410
+ <!-- Color Legend -->
411
+ <div style="background: #ffffff; border-radius: 8px; padding: 12px; margin-top: 16px;">
412
+ <h4 style="color: #374151; margin: 0 0 8px 0; font-size: 0.9rem;">📖 Word Color Guide:</h4>
413
+ <div style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 0.8rem;">
414
+ <span style="background: #22c55e; color: white; padding: 2px 8px; border-radius: 4px;">✓ Correct</span>
415
+ <span style="background: #ef4444; color: white; padding: 2px 8px; border-radius: 4px;">✗ Missed</span>
416
+ <span style="background: #f59e0b; color: white; padding: 2px 8px; border-radius: 4px;">~ Changed</span>
417
+ <span style="background: #8b5cf6; color: white; padding: 2px 8px; border-radius: 4px;">+ Added</span>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ """
422
+
423
+ # Smart Focus Areas - skill-based instead of error repetition
424
+ improvement_areas = []
425
+ if accuracy_percentage < 85:
426
+ improvement_areas.append("🎯 **Reading Accuracy**: Focus on reading each word carefully")
427
+ if missed_words > 2:
428
+ improvement_areas.append("📖 **Reading Fluency**: Practice reading without skipping words")
429
+ if substituted_words > 2:
430
+ improvement_areas.append("🗣️ **Pronunciation**: Work on saying words clearly")
431
+ if extra_words > 1:
432
+ improvement_areas.append("👁️ **Focus & Attention**: Follow the text closely while reading")
433
+
434
+ if improvement_areas:
435
+ feedback_html += f"""
436
+ <div style="background: #fef9c3; border-left: 4px solid #eab308; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
437
+ <h4 style="color: #a16207; margin: 0 0 12px 0;">🎯 Smart Focus Areas</h4>
438
+ <div style="color: #a16207;">
439
+ """
440
+ for area in improvement_areas[:3]: # Limit to 3 most important areas
441
+ feedback_html += f" • {area}<br>"
442
+
443
+ feedback_html += """
444
+ </div>
445
+ </div>
446
+ """
447
+
448
+ # Proactive Pronunciation Helper - based on story words, not just errors
449
+ if challenging_words:
450
+ feedback_html += f"""
451
+ <div style="background: #e0f2fe; border-radius: 12px; padding: 16px; margin-bottom: 16px;">
452
+ <h4 style="color: #0277bd; margin: 0 0 12px 0;">🗣️ Story Word Pronunciation Guide</h4>
453
+ <p style="color: #0277bd; font-size: 0.9rem; margin: 0 0 12px 0;">Here are some words from your story that might be tricky:</p>
454
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
455
+ """
456
+ for word in challenging_words:
457
+ pronunciation = get_pronunciation_tip(word)
458
+ feedback_html += f"""
459
+ <div style="background: #b3e5fc; padding: 8px 12px; border-radius: 6px;">
460
+ <strong style="color: #01579b;">{word.upper()}</strong><br>
461
+ <span style="color: #0277bd; font-size: 0.85rem;">"say: {pronunciation}"</span>
462
+ </div>"""
463
+
464
+ feedback_html += """
465
+ </div>
466
+ <div style="margin-top: 12px; padding: 8px; background: #b3e5fc; border-radius: 6px; font-size: 0.9rem;">
467
+ 💡 <strong>Practice tip:</strong> Listen to the AI reading these words and repeat them slowly!
468
+ </div>
469
+ </div>
470
+ """
471
+
472
+ # Progress Insights for Parents
473
+ feedback_html += f"""
474
+ <div style="background: #f0f9ff; border-radius: 12px; padding: 16px; margin-bottom: 16px;">
475
+ <h4 style="color: #0369a1; margin: 0 0 12px 0;">📈 Reading Skills Progress</h4>
476
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;">
477
+ <div style="background: white; padding: 10px; border-radius: 6px; text-align: center;">
478
+ <div style="font-size: 1.2rem; font-weight: bold; color: #0369a1;">{skill_areas['accuracy']['score']:.0f}%</div>
479
+ <div style="font-size: 0.8rem; color: #64748b;">Word Accuracy</div>
480
+ </div>
481
+ <div style="background: white; padding: 10px; border-radius: 6px; text-align: center;">
482
+ <div style="font-size: 1.2rem; font-weight: bold; color: #0369a1;">{skill_areas['fluency']['score']:.0f}%</div>
483
+ <div style="font-size: 0.8rem; color: #64748b;">Reading Flow</div>
484
+ </div>
485
+ <div style="background: white; padding: 10px; border-radius: 6px; text-align: center;">
486
+ <div style="font-size: 1.2rem; font-weight: bold; color: #0369a1;">{skill_areas['pronunciation']['score']:.0f}%</div>
487
+ <div style="font-size: 0.8rem; color: #64748b;">Pronunciation</div>
488
+ </div>
489
+ </div>
490
+ </div>
491
+ """
492
+
493
+ # Personalized Next Steps
494
+ if accuracy_percentage >= 85:
495
+ next_steps = [
496
+ "🎧 Practice reading along with the audio for better timing",
497
+ "📚 Try a slightly more challenging story topic",
498
+ "🗣️ Focus on reading with expression and emotion"
499
+ ]
500
+ elif accuracy_percentage >= 70:
501
+ next_steps = [
502
+ "🎧 Listen to the AI reading first, then read yourself",
503
+ "🔤 Practice the tricky words from above separately",
504
+ "📱 Record yourself multiple times and compare"
505
+ ]
506
  else:
507
+ next_steps = [
508
+ "🎧 Listen to the audio several times before recording",
509
+ "👁️ Follow along with the text while listening",
510
+ " Take your time - read slowly and clearly"
 
 
511
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
+ feedback_html += f"""
514
+ <div style="background: #f0f9ff; border-radius: 12px; padding: 16px;">
515
+ <h4 style="color: #0369a1; margin: 0 0 12px 0;">🎮 Your Reading Quest - Next Steps!</h4>
516
+ <div style="color: #0369a1;">
517
+ """
518
+ for step in next_steps:
519
+ feedback_html += f" • {step}<br>"
520
+
521
+ feedback_html += f"""
522
+ </div>
523
+ <div style="margin-top: 16px; padding: 12px; background: #dbeafe; border-radius: 8px; text-align: center;">
524
+ <strong style="color: #1e40af;">🎯 Next Goal: Reach {min(accuracy_percentage + 15, 100)}% accuracy!</strong>
525
+ </div>
526
+ </div>
527
+ """
528
+
529
+ return feedback_html, final_text
530
+
531
+ def assess_student_reading_ui(original_passage_state, student_audio_path):
532
  if not student_audio_path: return "🎤 Please record your reading first!", ""
533
  if not original_passage_state: return "Hmm, the original story is missing. 😟 Please generate a story first.", ""
534
+ transcribed_text = speech_to_text_whisper_space(student_audio_path)
535
  stt_errors = ["couldn't understand", "had trouble", "service isn't working", "service is busy", "didn't get any recording", "filepath type issue"]
536
  if any(err in (transcribed_text or "").lower() for err in stt_errors): return transcribed_text, ""
537
+ feedback, highlighted_passage = compare_texts_for_feedback(original_passage_state, transcribed_text)
538
+ return feedback, highlighted_passage
539
 
540
  css = """
541
  body, .gradio-container {
 
543
  font-family: -apple-system, BlinkMacSystemFont, 'San Francisco', 'Segoe UI', 'Roboto', Arial, sans-serif !important;
544
  }
545
  .main-header {
546
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
547
+ border-radius: 0 !important;
548
+ box-shadow: 0 8px 32px 0 rgba(102, 126, 234, 0.3) !important;
549
+ padding: 32px 20px 28px 20px !important;
550
+ margin: -20px -20px 28px -20px !important;
551
+ width: calc(100% + 40px) !important;
552
  text-align: center;
553
  border: none !important;
554
+ position: relative;
555
+ overflow: hidden;
556
+ }
557
+ .main-header::before {
558
+ content: '';
559
+ position: absolute;
560
+ top: 0;
561
+ left: 0;
562
+ right: 0;
563
+ bottom: 0;
564
+ background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
565
+ pointer-events: none;
566
+ }
567
+ .main-header h1 {
568
+ font-size: 2.4rem !important;
569
+ font-weight: 800 !important;
570
+ color: white !important;
571
+ margin: 0 0 8px 0 !important;
572
+ text-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
573
+ letter-spacing: -0.5px !important;
574
+ position: relative;
575
+ z-index: 1;
576
+ }
577
+ .main-header p {
578
+ color: rgba(255,255,255,0.9) !important;
579
+ font-size: 1.1rem !important;
580
+ margin: 0 !important;
581
+ font-weight: 400 !important;
582
+ position: relative;
583
+ z-index: 1;
584
+ }
585
+ .tech-badge {
586
+ background: rgba(255,255,255,0.2) !important;
587
+ color: white !important;
588
+ border-radius: 12px !important;
589
+ padding: 4px 12px !important;
590
+ font-size: 12px !important;
591
+ font-weight: 600 !important;
592
+ backdrop-filter: blur(10px) !important;
593
  }
 
 
 
594
  .gr-block, .gr-panel {background: white !important; border-radius: 18px !important; box-shadow: 0 2px 8px 0 rgba(60,60,90,0.07) !important; border: none !important; padding: 28px 22px !important;}
595
  .section-header {background: transparent !important; border: none !important; padding: 0 !important; margin-bottom: 16px !important;}
596
  .section-header h3 {color: #1e293b !important; font-size: 1.14rem !important; font-weight: 600 !important;}
 
627
  background: linear-gradient(90deg, #e0e7ef, #dde5f2) !important;
628
  color: #2a3140 !important;
629
  transition: all 0.15s cubic-bezier(0.4,0.0,0.2,1) !important;
630
+ transform: translateY(0) !important
631
  }
632
 
633
  .gr-button[variant="secondary"]:hover {
 
703
  audio_out = gr.Audio(label="🎵 Story Audio", type="filepath", visible=True, autoplay=False)
704
  gr.Markdown("""
705
  <div style="margin: 20px 0 0 0; padding: 10px 20px; background: #f4f7fa; border-radius: 16px;">
706
+ <b>➡️ Next:</b> Record yourself reading below, then check the "Analysis & Feedback" tab for results.
707
  </div>
708
  """)
709
  stud_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="🎤 Your Recording")
 
712
  assess_btn = gr.Button("🔍 Analyze Reading", variant="primary", size="lg", interactive=False)
713
  recording_status = gr.Markdown("", elem_id="recording_status")
714
  analysis_status = gr.Markdown("", elem_id="analysis_status")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
715
 
716
+ with gr.TabItem("📊 Analysis & Feedback", elem_id="analysis_tab"):
717
+ with gr.Row():
718
+ with gr.Column(scale=1, variant="panel"):
719
+ gr.Markdown("""
720
+ <div class="section-header">
721
+ <h3>🔍 Word-by-Word Analysis</h3>
722
+ <p>See exactly which words you read correctly</p>
723
+ </div>
724
+ """)
725
+ highlighted_out = gr.HTML(
726
+ value="""
727
+ <div style="text-align: center; color: #6b7280; padding: 20px;">
728
+ <h4>🎯 Detailed Word Analysis</h4>
729
+ <p>Color-coded word analysis will appear here.</p>
730
+ <div style="margin-top: 15px; padding: 15px; background: #f8fafc; border-radius: 12px;">
731
+ <div style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; font-size: 0.8rem; margin-bottom: 10px;">
732
+ <span style="background: #22c55e; color: white; padding: 2px 8px; border-radius: 4px;">✓ Correct</span>
733
+ <span style="background: #ef4444; color: white; padding: 2px 8px; border-radius: 4px;">✗ Missed</span>
734
+ <span style="background: #f59e0b; color: white; padding: 2px 8px; border-radius: 4px;">~ Changed</span>
735
+ <span style="background: #8b5cf6; color: white; padding: 2px 8px; border-radius: 4px;">+ Added</span>
736
+ </div>
737
+ <p style="margin: 0; font-size: 14px;">🎤 Complete a reading practice session to see your word analysis!</p>
738
+ </div>
739
+ </div>
740
+ """,
741
+ elem_id="highlighted_passage_output"
742
+ )
743
+
744
+ with gr.Row():
745
+ with gr.Column(scale=1, variant="panel"):
746
+ gr.Markdown("""
747
+ <div class="section-header">
748
+ <h3>📊 Reading Performance</h3>
749
+ <p>Your detailed feedback and scores</p>
750
+ </div>
751
+ """)
752
+ feedback_out = gr.HTML(
753
+ value="""
754
+ <div style="text-align: center; color: #6b7280; padding: 20px;">
755
+ <h4>📈 Performance Analysis</h4>
756
+ <p>Your detailed feedback will appear here after recording.</p>
757
+ <div style="margin-top: 15px; padding: 15px; background: #f8fafc; border-radius: 12px;">
758
+ <p style="margin: 0; font-size: 14px;">💡 <strong>Tip:</strong> Go to the "Practice & Generate" tab to record yourself reading!</p>
759
+ </div>
760
+ </div>
761
+ """,
762
+ elem_id="feedback_output"
763
+ )
764
+
765
  with gr.TabItem("ℹ️ About & How It Works", elem_id="about_tab"):
766
  gr.Markdown("""
767
  <div class="section-header">
 
770
  </div>
771
  """)
772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  gr.HTML("""
774
+ <div style="text-align: center; margin: 20px 0;">
775
+ <h3 style="color: #1e293b; margin-bottom: 20px;">📊 Application Workflow</h3>
776
+ <svg width="1400" height="700" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto; border: 2px solid #e5e7eb; border-radius: 12px; background: white;">
777
  <!-- Background -->
778
  <rect width="1400" height="600" fill="#fafafa"/>
779
 
 
869
  """)
870
 
871
  gr.Markdown("""
872
+ ## 🚀 How to Use the App
873
+
874
+ • **Enter details** → Name, grade, and story topic
875
+ • **Generate story** → Click "✨ Generate Story" button
876
+ • **Listen to AI** → Play the audio to hear correct pronunciation
877
+ • **Record yourself** → Use microphone to read the story aloud
878
+ • **Get feedback** → Click "🔍 Analyze Reading" for results
879
+ • **Practice more** → Try new topics and improve your reading!
880
+
881
  ---
882
 
883
  ## 🔧 Key Components
 
907
  passage_state = story_text
908
  return story_text, audio_btn_update, audio_player_update, passage_state
909
 
910
+ def assess_reading_with_analysis(original_passage_state, student_audio_path):
911
  if not student_audio_path:
912
  return (
913
  """
 
929
  ""
930
  )
931
 
 
 
 
932
  # Start transcription
933
+ transcribed_text = speech_to_text_whisper_space(student_audio_path)
 
934
 
935
  stt_errors = ["couldn't understand", "had trouble", "service isn't working", "service is busy", "didn't get any recording", "filepath type issue"]
936
  if any(err in (transcribed_text or "").lower() for err in stt_errors):
 
945
  ""
946
  )
947
 
 
948
  feedback, highlighted_passage = compare_texts_for_feedback(original_passage_state, transcribed_text)
 
949
 
950
  analysis_msg = """
951
  <div class="status-indicator status-success">
 
955
  """
956
  return (analysis_msg, feedback, highlighted_passage)
957
 
958
+ def generate_story_and_audio_complete(name, grade, topic):
959
+ """Generate story and audio in one function - Gradio will show default loading indicators"""
960
+
961
+ # Generate story text first
962
+ story_text = generate_story_from_llm(name, grade, topic)
963
+ if not story_text:
964
+ return "", None, ""
965
+
966
+ # Check if story generation was successful
967
+ if not story_text or any(err in story_text.lower() for err in ["error", "blocked", "couldn't", "api key not configured"]):
968
+ return story_text, None, story_text
969
+
970
+ # Generate audio (Gradio will show its loading indicator)
971
+ audio_filepath = text_to_speech_using_space_simple(story_text)
972
+
973
+ if audio_filepath:
974
+ print(f"COMPLETE: Story and audio generated successfully")
975
+ return story_text, audio_filepath, story_text
976
+ else:
977
+ print("COMPLETE: Story generated, but audio failed")
978
+ return story_text, None, story_text
979
+
980
  def update_recording_status(audio_file):
981
  if audio_file is not None:
982
  return (
 
1048
  gr.update(visible=False),
1049
  gr.update(interactive=False)
1050
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
 
1052
+ # Event handlers - Generate story first, then audio separately
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1053
  gen_btn.click(
1054
+ fn=generate_story_from_llm,
1055
  inputs=[s_name, s_grade, s_topic],
1056
+ outputs=[passage_out]
1057
+ ).then(
1058
+ fn=lambda story_text: story_text, # Store story in state immediately
1059
+ inputs=[passage_out],
1060
+ outputs=[original_passage_state]
1061
+ )
1062
+
1063
+ # Separate audio generation triggered by story output change
1064
+ passage_out.change(
1065
+ fn=lambda story_text: text_to_speech_using_space_simple(story_text) if story_text and not any(err in story_text.lower() for err in ["error", "blocked", "couldn't", "api key not configured"]) else None,
1066
+ inputs=[passage_out],
1067
+ outputs=[audio_out]
1068
  )
1069
 
1070
  assess_btn.click(
1071
  fn=assess_reading_with_analysis,
1072
  inputs=[original_passage_state, stud_audio_in],
1073
+ outputs=[analysis_status, feedback_out, highlighted_out]
 
1074
  )
1075
 
1076
  stud_audio_in.change(