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
|