| """ |
| Streamlit demo for the Math Tutor with multilingual & voice support. |
| Run: streamlit run demo.py |
| """ |
|
|
| import streamlit as st |
| import io |
| import wave |
|
|
| from gtts import gTTS |
| import tempfile |
| from tutor.core import ( |
| CurriculumLoader, |
| ChildASRAdapter, |
| ResponseScorer, |
| LearnerState, |
| FeedbackGenerator, |
| LocalProgressStore, |
| ) |
|
|
|
|
| |
| |
| |
| def speak(text, lang="en"): |
| lang_map = { |
| "en": "en", |
| "fr": "fr", |
| "kin": "en" |
| } |
|
|
| tts = gTTS(text=text, lang=lang_map.get(lang, "en")) |
|
|
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f: |
| tts.save(f.name) |
| return f.name |
|
|
|
|
| |
| |
| |
| st.set_page_config( |
| page_title="Math Tutor", |
| page_icon="🧮", |
| layout="centered" |
| ) |
|
|
| |
| |
| |
| if 'curriculum' not in st.session_state: |
| st.session_state.curriculum = CurriculumLoader() |
| if 'learner_state' not in st.session_state: |
| st.session_state.learner_state = LearnerState('demo_learner') |
| if 'item_index' not in st.session_state: |
| st.session_state.item_index = 0 |
| if 'store' not in st.session_state: |
| st.session_state.store = LocalProgressStore() |
| st.session_state.store.add_learner('demo_learner', 'Demo Child', 'en') |
| if 'asr' not in st.session_state: |
| st.session_state.asr = ChildASRAdapter() |
| if 'scorer' not in st.session_state: |
| st.session_state.scorer = ResponseScorer() |
| if 'language' not in st.session_state: |
| st.session_state.language = 'en' |
|
|
| |
| |
| |
| st.title("🧮 AI Math Tutor for Early Learners") |
|
|
| curriculum = st.session_state.curriculum |
| learner_state = st.session_state.learner_state |
| store = st.session_state.store |
| asr = st.session_state.asr |
| scorer = st.session_state.scorer |
|
|
| |
| |
| |
| with st.sidebar: |
| st.markdown("### ⚙️ Settings") |
|
|
| language_map = { |
| 'English 🇬🇧': 'en', |
| 'Français 🇫🇷': 'fr', |
| 'Kinyarwanda 🇷🇼': 'kin' |
| } |
|
|
| lang_display = st.radio( |
| "Language:", |
| list(language_map.keys()) |
| ) |
|
|
| st.session_state.language = language_map[lang_display] |
|
|
| |
| |
| |
| if st.session_state.item_index < len(curriculum.items): |
|
|
| current_item = curriculum.items[st.session_state.item_index] |
|
|
| st.markdown("### 👂 Listen and Answer") |
|
|
| lang_key = f"stem_{st.session_state.language}" |
| question = current_item.get(lang_key, current_item.get('stem_en')) |
|
|
| st.markdown(f"**Question:** {question}") |
|
|
| |
| |
| |
| if st.button("🔊 Listen Question"): |
| audio_q = speak(question, st.session_state.language) |
| st.audio(audio_q) |
|
|
| |
| |
| |
| visual = current_item.get('visual') |
|
|
| if visual: |
| parts = visual.split('_') |
| name = parts[0] |
|
|
| nums = [int(s) for s in parts if s.isdigit()] |
| count = nums[0] if nums else None |
|
|
| emoji_map = { |
| 'apples': '🍎', |
| 'goats': '🐐', |
| 'beads': '🔵', |
| 'default': '🔢' |
| } |
|
|
| symbol = emoji_map.get(name.lower(), '🔢') |
|
|
| if count: |
| st.markdown("### 🧠 Visual Learning") |
| st.markdown(symbol * min(count, 20)) |
|
|
| |
| |
| |
| transcript = st.text_input( |
| "Your Answer:" |
| ) |
|
|
| |
| |
| |
| if st.button("Submit Answer", type="primary"): |
|
|
| if transcript.strip(): |
|
|
| |
| correct = scorer.score_response( |
| current_item.get('answer_int', 0), |
| transcript, |
| current_item |
| ) |
|
|
| |
| detected_lang = asr.detect_language(transcript) |
|
|
| |
| store.add_response( |
| 'demo_learner', |
| current_item.get('skill'), |
| current_item.get('id'), |
| correct, |
| transcript |
| ) |
|
|
| learner_state.record_response( |
| current_item.get('skill'), |
| correct |
| ) |
|
|
| |
| |
| |
| feedback = FeedbackGenerator.generate_feedback( |
| correct, |
| st.session_state.language, |
| current_item.get('answer_int') |
| ) |
|
|
| |
| |
| |
| if detected_lang != st.session_state.language: |
| feedback += " (I noticed mixed language input 👍)" |
|
|
| |
| |
| |
| if correct: |
| st.success(f"✅ {feedback}") |
| else: |
| st.error(f"❌ {feedback}") |
|
|
| |
| |
| |
| audio = speak(feedback, st.session_state.language) |
| st.audio(audio, autoplay=True) |
|
|
| |
| st.session_state.item_index += 1 |
| st.rerun() |
|
|
| else: |
| st.warning("Please enter an answer") |
|
|
| |
| |
| |
| else: |
| st.success("🎉 Great job! You finished!") |
|
|
| if st.button("Restart"): |
| st.session_state.item_index = 0 |
| st.rerun() |