Spaces:
Running
Running
| """Curated /r/ exercise bank following the SLP clinical hierarchy. | |
| Curriculum levels (maps to `difficulty` field): | |
| 0 Syllable sounds β ra, re, ri, ro, ru, ar, er, or | |
| 1 Initial /r/ words β common, high-frequency | |
| 2 Medial and final /r/ words | |
| 3 Vocalic /r/ and /r/ blends | |
| 4 Short phrases with /r/ | |
| The `type` field labels the exercise category: | |
| "syllable" | "word" | "phrase" | |
| IPA notes: | |
| β’ target phonemes always use "r" (not "ΙΉ"); scoring._normalize_phonemes | |
| collapses ΙΉ β r before comparison. | |
| β’ Syllable targets are minimal β just the critical phonemes. | |
| """ | |
| import random | |
| EXERCISES = [ | |
| # ββ Level 0: Syllable sounds βββββββββββββββββββββββββββββββββββββββββ | |
| # Pre-word practice: isolate the /r/ in CV and VC syllables. | |
| {"word": "ra", "phonemes": "r Γ¦", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "re", "phonemes": "r Ι", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "ri", "phonemes": "r iΛ", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "ro", "phonemes": "r oΚ", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "ru", "phonemes": "r uΛ", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "ar", "phonemes": "Ι r", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "er", "phonemes": "Ι r", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "or", "phonemes": "Ι r", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "ir", "phonemes": "Ιͺ r", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| {"word": "ur", "phonemes": "Κ r", "position": "syllable", "difficulty": 0, "type": "syllable"}, | |
| # ββ Level 1: Initial /r/ words ββββββββββββββββββββββββββββββββββββββββ | |
| # /r/ is easiest to practice at the start of a word. | |
| {"word": "red", "phonemes": "r Ι d", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "run", "phonemes": "r Κ n", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "rain", "phonemes": "r eΙͺ n", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "rope", "phonemes": "r oΚ p", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "right", "phonemes": "r aΙͺ t", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "read", "phonemes": "r iΛ d", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "real", "phonemes": "r iΛ l", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "ride", "phonemes": "r aΙͺ d", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "race", "phonemes": "r eΙͺ s", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "roof", "phonemes": "r uΛ f", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "ring", "phonemes": "r Ιͺ Ε", "position": "initial", "difficulty": 1, "type": "word"}, | |
| {"word": "road", "phonemes": "r oΚ d", "position": "initial", "difficulty": 1, "type": "word"}, | |
| # ββ Level 2: Medial and final /r/ words ββββββββββββββββββββββββββββββ | |
| {"word": "very", "phonemes": "v Ι r i", "position": "medial", "difficulty": 2, "type": "word"}, | |
| {"word": "carrot", "phonemes": "k Γ¦ r Ι t", "position": "medial", "difficulty": 2, "type": "word"}, | |
| {"word": "around", "phonemes": "Ι r aΚ n d", "position": "medial", "difficulty": 2, "type": "word"}, | |
| {"word": "story", "phonemes": "s t Ι r i", "position": "medial", "difficulty": 2, "type": "word"}, | |
| {"word": "error", "phonemes": "Ι r Ι r", "position": "medial", "difficulty": 2, "type": "word"}, | |
| {"word": "correct","phonemes": "k Ι r Ι k t", "position": "medial", "difficulty": 2, "type": "word"}, | |
| {"word": "car", "phonemes": "k Ι r", "position": "final", "difficulty": 2, "type": "word"}, | |
| {"word": "four", "phonemes": "f Ι r", "position": "final", "difficulty": 2, "type": "word"}, | |
| {"word": "star", "phonemes": "s t Ι r", "position": "final", "difficulty": 2, "type": "word"}, | |
| {"word": "more", "phonemes": "m Ι r", "position": "final", "difficulty": 2, "type": "word"}, | |
| {"word": "door", "phonemes": "d Ι r", "position": "final", "difficulty": 2, "type": "word"}, | |
| {"word": "floor", "phonemes": "f l Ι r", "position": "final", "difficulty": 2, "type": "word"}, | |
| # ββ Level 3: Vocalic /r/ and blends ββββββββββββββββββββββββββββββββββ | |
| # Vocalic /r/ (r-colored vowels) and consonant clusters are the hardest. | |
| {"word": "bird", "phonemes": "b Ι r d", "position": "vocalic", "difficulty": 3, "type": "word"}, | |
| {"word": "her", "phonemes": "h Ι r", "position": "vocalic", "difficulty": 3, "type": "word"}, | |
| {"word": "butter", "phonemes": "b Κ t Ι r", "position": "vocalic", "difficulty": 3, "type": "word"}, | |
| {"word": "better", "phonemes": "b Ι t Ι r", "position": "vocalic", "difficulty": 3, "type": "word"}, | |
| {"word": "teacher","phonemes": "t iΛ tΚ Ι r","position": "vocalic", "difficulty": 3, "type": "word"}, | |
| {"word": "early", "phonemes": "Ι r l i", "position": "vocalic", "difficulty": 3, "type": "word"}, | |
| {"word": "tree", "phonemes": "t r i", "position": "blend", "difficulty": 3, "type": "word"}, | |
| {"word": "dream", "phonemes": "d r i m", "position": "blend", "difficulty": 3, "type": "word"}, | |
| {"word": "frog", "phonemes": "f r Ι g", "position": "blend", "difficulty": 3, "type": "word"}, | |
| {"word": "green", "phonemes": "g r i n", "position": "blend", "difficulty": 3, "type": "word"}, | |
| {"word": "bread", "phonemes": "b r Ι d", "position": "blend", "difficulty": 3, "type": "word"}, | |
| {"word": "price", "phonemes": "p r aΙͺ s", "position": "blend", "difficulty": 3, "type": "word"}, | |
| # ββ Level 4: Short phrases with /r/ ββββββββββββββββββββββββββββββββββ | |
| # Carryover: the hardest setting. Produces /r/ in natural connected speech. | |
| {"word": "red rose", "phonemes": "r Ι d r oΚ z", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "run fast", "phonemes": "r Κ n f Γ¦ s t", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "really great", "phonemes": "r iΛ l i g r eΙͺ t", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "right here", "phonemes": "r aΙͺ t h Ιͺ r", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "green rabbit", "phonemes": "g r i n r Γ¦ b Ιͺ t", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "every year", "phonemes": "Ι v r i j Ιͺ r", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "read a book", "phonemes": "r iΛ d Ι b Κ k", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "rainy morning", "phonemes": "r eΙͺ n i m Ι r n Ιͺ Ε", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "far from here", "phonemes": "f Ι r f r Κ m h Ιͺ r", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| {"word": "wrong road", "phonemes": "r Ι Ε r oΚ d", "position": "phrase", "difficulty": 4, "type": "phrase"}, | |
| ] | |
| # Backward-compatible alias | |
| WORDS = EXERCISES | |
| _EXERCISES_BY_NAME = {e["word"]: e for e in EXERCISES} | |
| # Levels as human-readable labels (for UI display) | |
| LEVEL_LABELS = { | |
| 0: "Level 1 Β· Syllables", | |
| 1: "Level 2 Β· Starting Words", | |
| 2: "Level 3 Β· Middle & End", | |
| 3: "Level 4 Β· Blends & Vocalic", | |
| 4: "Level 5 Β· Phrases", | |
| } | |
| LEVEL_DESCRIPTIONS = { | |
| 0: "Build the /r/ sound in simple syllables", | |
| 1: "Practice /r/ at the start of words", | |
| 2: "Tackle /r/ in the middle and end of words", | |
| 3: "Master /r/ blends and vowel combinations", | |
| 4: "Carry your /r/ into real phrases", | |
| } | |
| XP_PER_CORRECT = 10 | |
| XP_FIRST_TRY_BONUS = 5 # extra XP if correct on the first attempt | |
| ADVANCE_AFTER_CORRECT = 5 # consecutive correct to auto-advance level | |
| # XP required to unlock each level on the home path. Level 0 is always open. | |
| # Streak-based auto-advance during practice still works β these thresholds only | |
| # gate the level picker on the home view. | |
| LEVEL_UNLOCK_XP = { | |
| 0: 0, | |
| 1: 50, | |
| 2: 150, | |
| 3: 300, | |
| 4: 500, | |
| } | |
| def level_unlocked(level: int, xp: int) -> bool: | |
| return xp >= LEVEL_UNLOCK_XP.get(level, 0) | |
| def find_word(name: str | None) -> dict | None: | |
| if not name: | |
| return None | |
| return _EXERCISES_BY_NAME.get(name.strip().lower()) | |
| def get_next_word( | |
| history: list, | |
| suggestion: str = "", | |
| explicit_level: int | None = None, | |
| ) -> dict: | |
| """Pick the next exercise. | |
| Args: | |
| history: Full session history list. | |
| suggestion: "easier" | "harder" | "same" | "" β hint for difficulty. | |
| explicit_level: If provided, override the automatic progression and | |
| pick from this curriculum level (0β4). | |
| """ | |
| if explicit_level == 0 and not history: | |
| for e in EXERCISES: | |
| if e["word"] == "re": | |
| return e | |
| if explicit_level is not None: | |
| target_diff = max(0, min(4, explicit_level)) | |
| else: | |
| n = len(history) if history else 0 | |
| base_diff = min(3, 1 + n // 3) | |
| if suggestion == "easier": | |
| target_diff = max(0, base_diff - 1) | |
| elif suggestion == "harder": | |
| target_diff = min(4, base_diff + 1) | |
| else: | |
| target_diff = base_diff | |
| recent_words = {h.get("target_word") for h in (history[-5:] if history else [])} | |
| last_word = history[-1].get("target_word") if history else None | |
| pool = [e for e in EXERCISES if e["difficulty"] == target_diff and e["word"] not in recent_words] | |
| if not pool: | |
| pool = [e for e in EXERCISES if e["difficulty"] == target_diff and e["word"] != last_word] | |
| if not pool: | |
| pool = [e for e in EXERCISES if e["difficulty"] == target_diff] | |
| if not pool: | |
| pool = EXERCISES | |
| return random.choice(pool) | |