Spaces:
Sleeping
Sleeping
import gradio as gr | |
import os | |
from agent import ReadingCoachAgent | |
# Create a single instance of the agent | |
reading_coach = ReadingCoachAgent() | |
session = {"story": "", "name": "", "grade": "", "progress": 0, "last_feedback": "", "practice_count": 0} | |
# Define theme colors (Duolingo-inspired) | |
PRIMARY_COLOR = "#58CC02" # Green | |
SECONDARY_COLOR = "#FFC800" # Yellow | |
ACCENT_COLOR = "#FF4B4B" # Red | |
BG_COLOR = "#F7F7F7" # Light gray | |
# Custom CSS for more professional styling | |
custom_css = """ | |
:root { | |
--primary-color: #58CC02; | |
--secondary-color: #FFC800; | |
--accent-color: #FF4B4B; | |
--neutral-color: #4B4B4B; | |
--light-bg: #F7F7F7; | |
--white: #FFFFFF; | |
--border-radius: 16px; | |
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
.container { | |
font-family: 'Nunito', sans-serif; | |
max-width: 900px; | |
margin: 0 auto; | |
font-size: 0.95rem; | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 2rem; | |
color: var(--neutral-color); | |
} | |
.app-title { | |
color: var(--primary-color); | |
font-size: 2.2rem; | |
font-weight: 800; | |
margin: 0; | |
} | |
.card { | |
background: var(--white); | |
border-radius: var(--border-radius); | |
padding: 1.5rem; | |
box-shadow: var(--shadow); | |
margin-bottom: 1.5rem; | |
} | |
.btn-primary { | |
background: var(--primary-color) !important; | |
color: var(--white) !important; | |
font-weight: bold !important; | |
border: none !important; | |
padding: 0.75rem 1.5rem !important; | |
border-radius: 50px !important; | |
cursor: pointer !important; | |
transition: transform 0.1s, box-shadow 0.1s, background-color 0.2s !important; | |
box-shadow: 0 4px 0 #48a700 !important; | |
} | |
.btn-primary:hover { | |
background: #62d40a !important; | |
} | |
.btn-primary:active { | |
background: #4ab000 !important; | |
transform: translateY(2px) !important; | |
box-shadow: 0 2px 0 #3e9500 !important; | |
} | |
.btn-secondary { | |
background: var(--secondary-color) !important; | |
color: var(--neutral-color) !important; | |
font-weight: bold !important; | |
border: none !important; | |
padding: 0.75rem 1.5rem !important; | |
border-radius: 50px !important; | |
cursor: pointer !important; | |
transition: transform 0.1s, box-shadow 0.1s, background-color 0.2s !important; | |
box-shadow: 0 4px 0 #e0b000 !important; | |
} | |
.btn-secondary:hover { | |
background: #ffd119 !important; | |
} | |
.btn-secondary:active { | |
background: #e0b000 !important; | |
transform: translateY(2px) !important; | |
box-shadow: 0 2px 0 #c69e00 !important; | |
} | |
.btn-practice { | |
background: #9333ea !important; | |
color: var(--white) !important; | |
font-weight: bold !important; | |
border: none !important; | |
padding: 0.75rem 1.5rem !important; | |
border-radius: 50px !important; | |
cursor: pointer !important; | |
transition: transform 0.1s, box-shadow 0.1s, background-color 0.2s !important; | |
box-shadow: 0 4px 0 #7c2d9c !important; | |
} | |
.btn-practice:hover { | |
background: #a346f5 !important; | |
} | |
.btn-practice:active { | |
background: #7e2bd0 !important; | |
transform: translateY(2px) !important; | |
box-shadow: 0 2px 0 #6b228b !important; | |
} | |
.btn-clear { | |
background: var(--accent-color) !important; | |
color: var(--white) !important; | |
font-weight: bold !important; | |
border: none !important; | |
padding: 0.5rem 1rem !important; | |
border-radius: 25px !important; | |
cursor: pointer !important; | |
transition: transform 0.1s, box-shadow 0.1s, background-color 0.2s !important; | |
box-shadow: 0 2px 0 #d93636 !important; | |
} | |
.btn-clear:hover { | |
background: #ff5f5f !important; | |
} | |
.btn-clear:active { | |
background: #e43a3a !important; | |
transform: translateY(1px) !important; | |
box-shadow: 0 1px 0 #c52e2e !important; | |
} | |
.progress-container { | |
width: 100%; | |
background-color: #e0e0e0; | |
border-radius: 50px; | |
margin: 1rem 0; | |
height: 10px; | |
} | |
.progress-bar { | |
background-color: var(--primary-color); | |
height: 100%; | |
border-radius: 50px; | |
transition: width 0.3s ease; | |
} | |
.feedback-area { | |
background: #f8f9fa; | |
border: 2px solid #e9ecef; | |
border-radius: 12px; | |
padding: 1rem; | |
margin: 1rem 0; | |
font-family: 'Consolas', 'Monaco', monospace; | |
font-size: 0.9rem; | |
line-height: 1.6; | |
white-space: pre-wrap; | |
} | |
.practice-info { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
padding: 1rem; | |
border-radius: 12px; | |
margin: 1rem 0; | |
text-align: center; | |
} | |
input, textarea { | |
border: 2px solid #e0e0e0 !important; | |
border-radius: var(--border-radius) !important; | |
padding: 10px !important; | |
font-size: 14px !important; | |
} | |
input:focus, textarea:focus { | |
border-color: var(--primary-color) !important; | |
outline: none !important; | |
} | |
@media (max-width: 768px) { | |
.card { | |
padding: 1rem; | |
} | |
} | |
""" | |
def start_session(name, grade, topic): | |
"""Generate a new story based on name, grade, and topic""" | |
if not name.strip() or not grade.strip() or not topic.strip(): | |
return "Please fill in all fields", gr.update(visible=False), gr.update(visible=False) | |
try: | |
# Store session data | |
session["name"] = name | |
session["grade"] = grade | |
session["practice_count"] = 0 | |
# Clear previous session | |
reading_coach.clear_session() | |
# Generate the story using the correct method | |
generated_story = reading_coach.generate_story_for_student(name, grade, topic) | |
session["story"] = generated_story | |
session["progress"] = 33 | |
# Print for debugging | |
print(f"Generated story: {session['story'][:50]}...") | |
# Return the story and make practice card visible | |
return session["story"], gr.update(visible=True), gr.update(visible=False) | |
except Exception as e: | |
print(f"Error in start_session: {e}") | |
# Provide a fallback story if generation fails | |
fallback = f"Once upon a time, {name} went on an adventure to learn about {topic}..." | |
session["story"] = fallback | |
return fallback, gr.update(visible=True), gr.update(visible=False) | |
def generate_audio(): | |
"""Generate audio for the current story""" | |
try: | |
if not session.get("story"): | |
return None | |
print("Generating audio for story...") | |
audio_path = reading_coach.create_audio_from_story(session["story"]) | |
if audio_path: | |
session["progress"] = 66 | |
print(f"Audio generated successfully: {audio_path}") | |
print(f"Audio path type: {type(audio_path)}") | |
# Ensure the path exists and is accessible | |
if os.path.exists(audio_path): | |
print(f"Audio file exists at: {audio_path}") | |
return audio_path | |
else: | |
print(f"Audio file does not exist at: {audio_path}") | |
return None | |
else: | |
print("No audio path returned from TTS") | |
# Instead of returning None, we could return a message or skip audio | |
return None | |
except Exception as e: | |
print(f"Error in generate_audio: {e}") | |
import traceback | |
traceback.print_exc() | |
return None | |
def submit_reading(audio): | |
"""Process the student's reading and provide comprehensive agentic feedback""" | |
try: | |
# Handle various audio input formats | |
if audio is None: | |
return "Please record your reading first.", gr.update(visible=False) | |
# Debug: Print what we received | |
print(f"Received audio input: {type(audio)} - {str(audio)[:100]}...") | |
# Pass audio directly to the agent - let STT handle the format conversion | |
transcribed, feedback, accuracy = reading_coach.analyze_student_reading(audio) | |
# Store feedback for potential story generation | |
session["last_feedback"] = feedback | |
session["progress"] = 100 | |
session["practice_count"] += 1 | |
# Always show practice story section so students can get more challenging stories | |
show_practice = True | |
return feedback, gr.update(visible=show_practice) | |
except Exception as e: | |
print(f"Error in submit_reading: {e}") | |
import traceback | |
traceback.print_exc() | |
return "There was an error processing your reading. Please try again.", gr.update(visible=False) | |
def generate_practice_story(): | |
"""Generate a new targeted story based on previous feedback""" | |
try: | |
if not session.get("name") or not session.get("grade"): | |
return "Please complete a reading session first to get a personalized practice story.", "" | |
# Generate targeted story using the correct method | |
new_story = reading_coach.generate_practice_story(session["name"], session["grade"]) | |
# Update session with new story | |
session["story"] = new_story | |
session["progress"] = 33 | |
session["practice_count"] += 1 | |
# Clear previous feedback | |
session["last_feedback"] = "" | |
# Create adaptive message based on practice count and performance | |
if session["practice_count"] == 1: | |
practice_msg = f"π New Challenge Story Generated!\nThis story is tailored to help you continue growing as a reader." | |
else: | |
practice_msg = f"π Challenge Story #{session['practice_count']} Ready!\nKeep pushing yourself - you're doing amazing!" | |
return new_story, practice_msg | |
except Exception as e: | |
print(f"Error generating practice story: {e}") | |
return "There was an error generating a new practice story. Please try again.", "" | |
def clear_all_audio(): | |
"""Clear all audio components and reset audio session""" | |
return None, None, "π All audio cleared!" | |
def reset_session(): | |
"""Reset the session to start over""" | |
session.clear() | |
session.update({"story": "", "name": "", "grade": "", "progress": 0, "last_feedback": "", "practice_count": 0}) | |
reading_coach.reset_all_data() | |
return ( | |
gr.update(value=""), | |
gr.update(value=""), | |
gr.update(value=""), | |
gr.update(value=""), | |
gr.update(value=None), | |
gr.update(value=""), | |
gr.update(value=""), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
0 | |
) | |
def clear_recording(): | |
"""Clear the recorded audio""" | |
return None | |
def record_again(): | |
"""Reset recording for a new attempt""" | |
return None | |
def update_progress_bar(progress): | |
"""Update the progress bar width based on progress percentage""" | |
return f"<div class='progress-container'><div class='progress-bar' style='width: {progress}%'></div></div>" | |
def launch_ui(): | |
with gr.Blocks(css=custom_css, title="ReadRight - AI Reading Coach") as demo: | |
with gr.Column(elem_classes="container"): | |
# Header | |
gr.HTML(""" | |
<div class="header"> | |
<h1 class="app-title">π¦ ReadRight</h1> | |
<p>AI-powered reading coach for kids</p> | |
</div> | |
""") | |
# Progress tracker | |
progress_bar = gr.HTML(update_progress_bar(0), elem_id="progress-bar") | |
# Step 1: Story Setup | |
with gr.Column(elem_classes="card") as setup_card: | |
gr.HTML("<h2>π Let's Create Your Personalized Reading Adventure!</h2>") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
with gr.Row(): | |
name = gr.Text(label="π€ Your Name", placeholder="Enter your name...") | |
with gr.Row(): | |
grade = gr.Dropdown( | |
label="π Grade Level", | |
choices=["Grade 1", "Grade 2", "Grade 3", "Grade 4", "Grade 5", "Grade 6", "Grade 7", "Grade 8", "Grade 9", "Grade 10"], | |
value="Grade 1", | |
allow_custom_value=True | |
) | |
with gr.Row(): | |
topic = gr.Text(label="π Story Topic", placeholder="e.g., space adventure, friendly dinosaurs, ocean exploration...") | |
with gr.Row(): | |
btn_start = gr.Button("π Create My Story", elem_classes="btn-primary") | |
with gr.Column(scale=1): | |
gr.HTML(""" | |
<div style="background: linear-gradient(135deg, var(--primary-color) 0%, #48a700 100%); | |
color: white; padding: 1.5rem; border-radius: var(--border-radius); | |
margin-left: 1rem; height: fit-content; box-shadow: var(--shadow);"> | |
<h3 style="margin-top: 0; color: white; font-size: 1.1rem; font-weight: 700;">π How to Use ReadRight</h3> | |
<div style="font-size: 0.85rem; line-height: 1.5;"> | |
<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> | |
<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> | |
<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> | |
<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> | |
<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> | |
<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> | |
</div> | |
</div> | |
""") | |
# Step 2: Reading Practice | |
with gr.Column(elem_classes="card", visible=False) as practice_card: | |
gr.HTML("<h2>π Time to Practice Reading!</h2>") | |
# Story display | |
story = gr.Markdown(label="π Your Personalized Story") | |
with gr.Row(): | |
btn_play = gr.Button("π Listen to Story", elem_classes="btn-secondary") | |
# Audio playback | |
audio_out = gr.Audio(label="π΅ Story Audio - Listen and Follow Along", visible=True) | |
gr.HTML("<h3>π€ Now Read the Story Aloud!</h3>") | |
# Recording | |
record = gr.Audio(label="π€ Record Your Reading", sources=["microphone"],type="filepath") | |
with gr.Row(): | |
btn_record_again = gr.Button("π€ Record Again", elem_classes="btn-secondary") | |
with gr.Row(): | |
btn_submit = gr.Button("β¨ Get AI Feedback", elem_classes="btn-primary") | |
# Agentic Feedback area | |
feedback = gr.TextArea( | |
label="π€ Your Personalized AI Reading Coach Feedback", | |
interactive=False, | |
elem_classes="feedback-area", | |
lines=12 | |
) | |
# Step 3: Continue Learning (appears after reading feedback) | |
with gr.Column(elem_classes="card", visible=False) as practice_story_card: | |
gr.HTML(""" | |
<div class="practice-info"> | |
<h2>π Keep Learning & Growing!</h2> | |
<p>Ready for your next challenge? Get a new story that's perfect for your reading level!</p> | |
</div> | |
""") | |
practice_info = gr.Text(label="Practice Information", interactive=False) | |
with gr.Row(): | |
btn_generate_practice = gr.Button("π Get Next Challenge Story", elem_classes="btn-practice") | |
btn_reset = gr.Button("π Start Fresh Session", elem_classes="btn-clear") | |
# Event handlers | |
btn_start.click( | |
start_session, | |
inputs=[name, grade, topic], | |
outputs=[story, practice_card, practice_story_card] | |
) | |
btn_play.click( | |
generate_audio, | |
inputs=[], | |
outputs=[audio_out] | |
) | |
btn_submit.click( | |
submit_reading, | |
inputs=[record], | |
outputs=[feedback, practice_story_card] | |
) | |
btn_generate_practice.click( | |
generate_practice_story, | |
inputs=[], | |
outputs=[story, practice_info] | |
) | |
btn_record_again.click( | |
record_again, | |
inputs=[], | |
outputs=[record] | |
) | |
btn_reset.click( | |
reset_session, | |
inputs=[], | |
outputs=[name, grade, topic, story, record, feedback, practice_info, practice_card, practice_story_card] | |
) | |
return demo |