Spaces:
Running
Running
| 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 |