File size: 12,192 Bytes
ba633b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | """
Core tutor components: curriculum, ASR, scoring, knowledge tracing, storage.
"""
import json
import sqlite3
from datetime import datetime
from typing import Dict, Optional
import numpy as np
# ============================================================================
# PART 1: CURRICULUM LOADER
# ============================================================================
class CurriculumLoader:
"""Load and manage math curriculum items."""
def __init__(self, curriculum_path: str = "tutor/data/curriculum.json"):
"""Load curriculum from JSON file."""
try:
with open(curriculum_path, 'r') as f:
self.items = json.load(f)
except FileNotFoundError:
print(f"⚠️ Curriculum file not found: {curriculum_path}")
self.items = []
# Index by skill
self.by_skill = {}
for item in self.items:
skill = item.get('skill', 'unknown')
if skill not in self.by_skill:
self.by_skill[skill] = []
self.by_skill[skill].append(item)
# Sort by difficulty
for skill in self.by_skill:
self.by_skill[skill].sort(key=lambda x: x.get('difficulty', 0))
def get_item_by_id(self, item_id: str) -> Optional[Dict]:
"""Fetch a single item by ID."""
for item in self.items:
if item.get('id') == item_id:
return item
return None
def get_initial_item(self) -> Optional[Dict]:
"""Return the easiest item (difficulty 1)."""
for item in self.items:
if item.get('difficulty') == 1:
return item
return self.items[0] if self.items else None
def get_next_item(self, skill: str, current_difficulty: int) -> Optional[Dict]:
"""Get next item above current difficulty."""
candidates = [
item for item in self.by_skill.get(skill, [])
if item.get('difficulty', 0) > current_difficulty
]
return candidates[0] if candidates else None
# ============================================================================
# PART 2: ASR + LANGUAGE DETECTION
# ============================================================================
class ChildASRAdapter:
"""Transcribe and detect language."""
def __init__(self):
"""Initialize ASR adapter."""
try:
import whisper
self.model = whisper.load_model("tiny")
self.asr_available = True
except (ImportError, Exception) as e:
print(f"⚠️ Whisper not available: {e}")
self.model = None
self.asr_available = False
self.language_keywords = {
'en': ['one', 'two', 'three', 'four', 'five', 'apple', 'goat', 'yes'],
'fr': ['un', 'deux', 'trois', 'quatre', 'cinq', 'pomme', 'chèvre', 'oui'],
'kin': ['rimwe', 'kabiri', 'gatatu', 'ine', 'itanu', 'pome', 'ihene', 'yego']
}
def transcribe(self, audio_path: str, language: Optional[str] = None) -> str:
"""Transcribe audio using Whisper."""
if not self.asr_available or self.model is None:
return "[ASR disabled]"
try:
result = self.model.transcribe(audio_path, language=language)
return result['text'].strip().lower()
except Exception as e:
print(f"⚠️ ASR error: {e}")
return ""
def detect_language(self, transcript: str) -> str:
"""Detect language from transcript."""
if not transcript:
return 'en'
scores = {lang: 0 for lang in ['en', 'fr', 'kin']}
transcript_lower = transcript.lower()
for lang, keywords in self.language_keywords.items():
scores[lang] = sum(1 for kw in keywords if kw in transcript_lower)
detected = [lang for lang, score in scores.items() if score > 0]
if len(detected) > 1:
return 'mixed'
elif detected:
return detected[0]
else:
return 'en'
# ============================================================================
# PART 3: SCORING
# ============================================================================
class ResponseScorer:
"""Score child responses."""
@staticmethod
def score_response(expected_answer: int, transcript: str, item: Dict = None) -> bool:
"""Determine if response is correct."""
number_words = {
'en': {1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five'},
'fr': {1: 'un', 2: 'deux', 3: 'trois', 4: 'quatre', 5: 'cinq'},
'kin': {1: 'rimwe', 2: 'kabiri', 3: 'gatatu', 4: 'ine', 5: 'itanu'}
}
transcript_lower = transcript.lower()
for lang, words in number_words.items():
if expected_answer in words:
if words[expected_answer] in transcript_lower:
return True
if str(expected_answer) in transcript_lower:
return True
return False
# ============================================================================
# PART 4: KNOWLEDGE TRACING
# ============================================================================
class BayesianKnowledgeTracing:
"""Bayesian Knowledge Tracing model."""
def __init__(self, skill: str, p_init: float = 0.1, p_g: float = 0.25,
p_s: float = 0.05, p_t: float = 0.1):
"""Initialize BKT for a skill."""
self.skill = skill
self.p_init = p_init
self.p_g = p_g
self.p_s = p_s
self.p_t = p_t
self.p_learned = p_init
self.history = []
def update(self, correct: bool) -> float:
"""Update P(learned) based on response."""
if correct:
p_correct = (self.p_learned * (1 - self.p_s)) + \
((1 - self.p_learned) * self.p_g)
else:
p_correct = (self.p_learned * self.p_s) + \
((1 - self.p_learned) * (1 - self.p_g))
if p_correct < 1e-10:
return self.p_learned
if correct:
posterior = (self.p_learned * (1 - self.p_s)) / p_correct
else:
posterior = (self.p_learned * self.p_s) / p_correct
self.p_learned = posterior + ((1 - posterior) * self.p_t)
self.p_learned = np.clip(self.p_learned, 0, 1)
self.history.append({
'correct': correct,
'p_learned': self.p_learned
})
return self.p_learned
def predict_next_response(self) -> float:
"""Predict P(next response is correct)."""
return (self.p_learned * (1 - self.p_s)) + \
((1 - self.p_learned) * self.p_g)
def next_item_difficulty(self) -> int:
"""Recommend difficulty change: -1, 0, or +1."""
if self.p_learned > 0.85:
return +1
elif self.p_learned < 0.3:
return -1
else:
return 0
class LearnerState:
"""Track learner's knowledge across all skills."""
def __init__(self, learner_id: str):
"""Initialize learner."""
self.learner_id = learner_id
self.skills = {
'counting': BayesianKnowledgeTracing('counting'),
'addition': BayesianKnowledgeTracing('addition'),
'subtraction': BayesianKnowledgeTracing('subtraction'),
'number_sense': BayesianKnowledgeTracing('number_sense'),
'word_problem': BayesianKnowledgeTracing('word_problem'),
}
self.current_skill = 'counting'
self.response_count = 0
def record_response(self, skill: str, correct: bool):
"""Update BKT model."""
if skill in self.skills:
self.skills[skill].update(correct)
self.response_count += 1
def get_next_item_difficulty(self, skill: str) -> int:
"""Get recommended difficulty change."""
if skill in self.skills:
return self.skills[skill].next_item_difficulty()
return 0
# ============================================================================
# PART 5: FEEDBACK GENERATION
# ============================================================================
class FeedbackGenerator:
"""Generate multilingual feedback."""
@staticmethod
def generate_feedback(correct: bool, language: str, expected_answer: int) -> str:
"""Generate feedback."""
templates = {
('en', True): "Correct! Very good!",
('en', False): f"Not quite. The answer is {expected_answer}.",
('fr', True): "Correct! Très bien!",
('fr', False): f"Non. La réponse est {expected_answer}.",
('kin', True): "Wembe! Neza cyane!",
('kin', False): f"Ntabwo. Igisubizo ni {expected_answer}.",
}
key = (language if language in ['en', 'fr', 'kin'] else 'en', correct)
return templates.get(key, templates[('en', correct)])
# ============================================================================
# PART 6: LOCAL STORAGE
# ============================================================================
class LocalProgressStore:
"""Store learner progress in SQLite."""
def __init__(self, db_path: str = "tutor/data/progress.db"):
"""Initialize SQLite database."""
self.db_path = db_path
self._init_db()
def _init_db(self):
"""Create tables if they don't exist."""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('''
CREATE TABLE IF NOT EXISTS learners (
learner_id TEXT PRIMARY KEY,
name TEXT,
language TEXT DEFAULT 'en',
created_at TIMESTAMP
)
''')
c.execute('''
CREATE TABLE IF NOT EXISTS responses (
response_id INTEGER PRIMARY KEY,
learner_id TEXT,
skill TEXT,
item_id TEXT,
correct BOOLEAN,
transcript TEXT,
timestamp TIMESTAMP,
FOREIGN KEY(learner_id) REFERENCES learners(learner_id)
)
''')
conn.commit()
conn.close()
def add_learner(self, learner_id: str, name: str, language: str = 'en'):
"""Register a learner."""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute(
'INSERT OR IGNORE INTO learners VALUES (?, ?, ?, ?)',
(learner_id, name, language, datetime.now())
)
conn.commit()
conn.close()
def add_response(self, learner_id: str, skill: str, item_id: str,
correct: bool, transcript: str):
"""Log a response."""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute(
'INSERT INTO responses VALUES (NULL, ?, ?, ?, ?, ?, ?)',
(learner_id, skill, item_id, correct, transcript, datetime.now())
)
conn.commit()
conn.close()
def get_stats(self, learner_id: str) -> Dict:
"""Get learner stats."""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('''
SELECT skill, COUNT(*) as attempts, SUM(correct) as correct_count
FROM responses
WHERE learner_id = ?
GROUP BY skill
''', (learner_id,))
rows = c.fetchall()
conn.close()
stats = {}
for skill, attempts, correct_count in rows:
accuracy = correct_count / attempts if attempts > 0 else 0
stats[skill] = {
'accuracy': accuracy,
'attempts': attempts,
'correct': correct_count
}
return stats
|