Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files
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
|