tss / app /services /learning_service.py
Deploy Bot
Deploy backend to HF Spaces
77d2609
"""
Learning Service - Vocabulary, Alphabet, and SRS (Spaced Repetition System)
"""
import json
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
class LearningService:
"""Service for vocabulary and learning features"""
def __init__(self):
self.data_dir = Path(__file__).parent.parent / "data"
self._vocabulary: Dict[str, Any] = {}
self._alphabet: Dict[str, Any] = {}
self._progress: Dict[str, Any] = {} # In-memory progress (for now)
self._load_data()
def _load_data(self):
"""Load vocabulary and alphabet data"""
try:
vocab_path = self.data_dir / "vocabulary.json"
if vocab_path.exists():
with open(vocab_path, "r", encoding="utf-8") as f:
self._vocabulary = json.load(f)
else:
self._vocabulary = self._get_default_vocabulary()
alphabet_path = self.data_dir / "alphabet.json"
if alphabet_path.exists():
with open(alphabet_path, "r", encoding="utf-8") as f:
self._alphabet = json.load(f)
else:
self._alphabet = self._get_default_alphabet()
except Exception as e:
print(f"Error loading data: {e}")
self._vocabulary = self._get_default_vocabulary()
self._alphabet = self._get_default_alphabet()
def get_vocabulary_categories(self) -> List[str]:
"""Get list of vocabulary categories"""
return list(self._vocabulary.get("categories", {}).keys())
def get_vocabulary_by_category(self, category: Optional[str] = None) -> Dict[str, Any]:
"""Get vocabulary items, optionally filtered by category"""
categories = self._vocabulary.get("categories", {})
if category and category in categories:
return {
"category_id": category,
"category_name_zh": categories[category].get("name_zh", category),
"category_name_en": categories[category].get("name_en", category),
"items": categories[category].get("items", [])
}
# Return all categories
result = []
for cat_id, cat_data in categories.items():
result.append({
"category_id": cat_id,
"category_name_zh": cat_data.get("name_zh", cat_id),
"category_name_en": cat_data.get("name_en", cat_id),
"items": cat_data.get("items", [])
})
return {"categories": result}
def get_alphabet(self) -> Dict[str, Any]:
"""Get Tibetan alphabet data"""
return self._alphabet
def get_flashcards(self, category: Optional[str] = None, limit: int = 10) -> List[Dict[str, Any]]:
"""Get flashcards for practice"""
all_items = []
categories = self._vocabulary.get("categories", {})
if category and category in categories:
all_items = categories[category].get("items", [])
else:
for cat_data in categories.values():
all_items.extend(cat_data.get("items", []))
# Apply SRS ordering (items due for review first)
# For now, just return items in order
return all_items[:limit]
def update_progress(self, vocabulary_id: str, quality: int) -> Dict[str, Any]:
"""
Update learning progress using SM-2 algorithm
quality: 0-5 (0=complete blackout, 5=perfect response)
"""
progress = self._progress.get(vocabulary_id, {
"ease_factor": 2.5,
"interval_days": 1,
"repetitions": 0,
"next_review": None
})
# SM-2 Algorithm
if quality >= 3:
# Correct response
if progress["repetitions"] == 0:
progress["interval_days"] = 1
elif progress["repetitions"] == 1:
progress["interval_days"] = 6
else:
progress["interval_days"] = round(
progress["interval_days"] * progress["ease_factor"]
)
progress["repetitions"] += 1
else:
# Incorrect response
progress["repetitions"] = 0
progress["interval_days"] = 1
# Update ease factor
progress["ease_factor"] = max(
1.3,
progress["ease_factor"] + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
)
# Calculate next review date
next_review = datetime.now() + timedelta(days=progress["interval_days"])
progress["next_review"] = next_review.isoformat()
self._progress[vocabulary_id] = progress
return progress
def _get_default_vocabulary(self) -> Dict[str, Any]:
"""Default vocabulary data"""
return {
"categories": {
"greetings": {
"name_zh": "问候语",
"name_en": "Greetings",
"items": [
{
"id": "greet_001",
"tibetan": "བཀྲ་ཤིས་བདེ་ལེགས།",
"chinese": "扎西德勒(吉祥如意/你好)",
"english": "Tashi Delek (Hello/Blessings)",
"pronunciation": "tashi delek",
"category": "greetings"
},
{
"id": "greet_002",
"tibetan": "ཐུགས་རྗེ་ཆེ།",
"chinese": "谢谢",
"english": "Thank you",
"pronunciation": "thuk je che",
"category": "greetings"
},
{
"id": "greet_003",
"tibetan": "མཇལ་བར་དགའ་པོ་བྱུང་།",
"chinese": "很高兴见到你",
"english": "Nice to meet you",
"pronunciation": "jal war ga po chung",
"category": "greetings"
},
{
"id": "greet_004",
"tibetan": "བདེ་མོ།",
"chinese": "再见",
"english": "Goodbye",
"pronunciation": "de mo",
"category": "greetings"
}
]
},
"numbers": {
"name_zh": "数字",
"name_en": "Numbers",
"items": [
{"id": "num_001", "tibetan": "གཅིག", "chinese": "一", "english": "One", "pronunciation": "chik", "category": "numbers"},
{"id": "num_002", "tibetan": "གཉིས", "chinese": "二", "english": "Two", "pronunciation": "nyi", "category": "numbers"},
{"id": "num_003", "tibetan": "གསུམ", "chinese": "三", "english": "Three", "pronunciation": "sum", "category": "numbers"},
{"id": "num_004", "tibetan": "བཞི", "chinese": "四", "english": "Four", "pronunciation": "shi", "category": "numbers"},
{"id": "num_005", "tibetan": "ལྔ", "chinese": "五", "english": "Five", "pronunciation": "nga", "category": "numbers"},
{"id": "num_006", "tibetan": "དྲུག", "chinese": "六", "english": "Six", "pronunciation": "druk", "category": "numbers"},
{"id": "num_007", "tibetan": "བདུན", "chinese": "七", "english": "Seven", "pronunciation": "dün", "category": "numbers"},
{"id": "num_008", "tibetan": "བརྒྱད", "chinese": "八", "english": "Eight", "pronunciation": "gyé", "category": "numbers"},
{"id": "num_009", "tibetan": "དགུ", "chinese": "九", "english": "Nine", "pronunciation": "gu", "category": "numbers"},
{"id": "num_010", "tibetan": "བཅུ", "chinese": "十", "english": "Ten", "pronunciation": "chu", "category": "numbers"}
]
},
"colors": {
"name_zh": "颜色",
"name_en": "Colors",
"items": [
{"id": "color_001", "tibetan": "དཀར་པོ", "chinese": "白色", "english": "White", "pronunciation": "kar po", "category": "colors"},
{"id": "color_002", "tibetan": "ནག་པོ", "chinese": "黑色", "english": "Black", "pronunciation": "nak po", "category": "colors"},
{"id": "color_003", "tibetan": "དམར་པོ", "chinese": "红色", "english": "Red", "pronunciation": "mar po", "category": "colors"},
{"id": "color_004", "tibetan": "སྔོན་པོ", "chinese": "蓝色", "english": "Blue", "pronunciation": "ngön po", "category": "colors"},
{"id": "color_005", "tibetan": "སེར་པོ", "chinese": "黄色", "english": "Yellow", "pronunciation": "ser po", "category": "colors"},
{"id": "color_006", "tibetan": "ལྗང་ཁུ", "chinese": "绿色", "english": "Green", "pronunciation": "jang khu", "category": "colors"}
]
},
"daily": {
"name_zh": "日常用语",
"name_en": "Daily Phrases",
"items": [
{"id": "daily_001", "tibetan": "ང", "chinese": "我", "english": "I/Me", "pronunciation": "nga", "category": "daily"},
{"id": "daily_002", "tibetan": "ཁྱོད", "chinese": "你", "english": "You", "pronunciation": "khyö", "category": "daily"},
{"id": "daily_003", "tibetan": "ཁོ", "chinese": "他", "english": "He", "pronunciation": "kho", "category": "daily"},
{"id": "daily_004", "tibetan": "མོ", "chinese": "她", "english": "She", "pronunciation": "mo", "category": "daily"},
{"id": "daily_005", "tibetan": "ཡིན།", "chinese": "是", "english": "Yes/Is", "pronunciation": "yin", "category": "daily"},
{"id": "daily_006", "tibetan": "མིན།", "chinese": "不是", "english": "No/Is not", "pronunciation": "min", "category": "daily"},
{"id": "daily_007", "tibetan": "དགོས།", "chinese": "需要", "english": "Need", "pronunciation": "gö", "category": "daily"},
{"id": "daily_008", "tibetan": "ཆུ", "chinese": "水", "english": "Water", "pronunciation": "chu", "category": "daily"}
]
}
}
}
def _get_default_alphabet(self) -> Dict[str, Any]:
"""Default Tibetan alphabet data"""
return {
"consonants": [
{"id": "con_01", "letter": "ཀ", "unicode": "U+0F40", "pronunciation": "ka", "description_zh": "清辅音,类似汉语拼音'k'", "description_en": "Voiceless velar stop, like 'k' in 'kite'"},
{"id": "con_02", "letter": "ཁ", "unicode": "U+0F41", "pronunciation": "kha", "description_zh": "送气音,类似'k'但带强气流", "description_en": "Aspirated 'k', like 'k' in 'king' with more breath"},
{"id": "con_03", "letter": "ག", "unicode": "U+0F42", "pronunciation": "ga", "description_zh": "浊辅音,类似汉语拼音'g'", "description_en": "Voiced velar stop, like 'g' in 'go'"},
{"id": "con_04", "letter": "ང", "unicode": "U+0F44", "pronunciation": "nga", "description_zh": "鼻音,类似汉语拼音'ng'", "description_en": "Velar nasal, like 'ng' in 'sing'"},
{"id": "con_05", "letter": "ཅ", "unicode": "U+0F45", "pronunciation": "ca", "description_zh": "塞擦音,类似汉语拼音'j'", "description_en": "Voiceless palatal affricate, like 'ch' in 'church'"},
{"id": "con_06", "letter": "ཆ", "unicode": "U+0F46", "pronunciation": "cha", "description_zh": "送气塞擦音", "description_en": "Aspirated 'ch'"},
{"id": "con_07", "letter": "ཇ", "unicode": "U+0F47", "pronunciation": "ja", "description_zh": "浊塞擦音,类似汉语拼音'zh'", "description_en": "Voiced palatal affricate, like 'j' in 'judge'"},
{"id": "con_08", "letter": "ཉ", "unicode": "U+0F49", "pronunciation": "nya", "description_zh": "鼻音,类似西班牙语'ñ'", "description_en": "Palatal nasal, like 'ny' in 'canyon'"},
{"id": "con_09", "letter": "ཏ", "unicode": "U+0F4F", "pronunciation": "ta", "description_zh": "清塞音,类似汉语拼音't'", "description_en": "Voiceless dental stop, like 't' in 'top'"},
{"id": "con_10", "letter": "ཐ", "unicode": "U+0F50", "pronunciation": "tha", "description_zh": "送气塞音", "description_en": "Aspirated 't'"},
{"id": "con_11", "letter": "ད", "unicode": "U+0F51", "pronunciation": "da", "description_zh": "浊塞音,类似汉语拼音'd'", "description_en": "Voiced dental stop, like 'd' in 'dog'"},
{"id": "con_12", "letter": "ན", "unicode": "U+0F53", "pronunciation": "na", "description_zh": "鼻音,类似汉语拼音'n'", "description_en": "Dental nasal, like 'n' in 'no'"},
{"id": "con_13", "letter": "པ", "unicode": "U+0F54", "pronunciation": "pa", "description_zh": "清塞音,类似汉语拼音'p'", "description_en": "Voiceless bilabial stop, like 'p' in 'pot'"},
{"id": "con_14", "letter": "ཕ", "unicode": "U+0F55", "pronunciation": "pha", "description_zh": "送气塞音", "description_en": "Aspirated 'p'"},
{"id": "con_15", "letter": "བ", "unicode": "U+0F56", "pronunciation": "ba", "description_zh": "浊塞音,类似汉语拼音'b'", "description_en": "Voiced bilabial stop, like 'b' in 'boy'"},
{"id": "con_16", "letter": "མ", "unicode": "U+0F58", "pronunciation": "ma", "description_zh": "鼻音,类似汉语拼音'm'", "description_en": "Bilabial nasal, like 'm' in 'mom'"},
{"id": "con_17", "letter": "ཙ", "unicode": "U+0F59", "pronunciation": "tsa", "description_zh": "塞擦音,类似汉语拼音'c'", "description_en": "Voiceless alveolar affricate, like 'ts' in 'cats'"},
{"id": "con_18", "letter": "ཚ", "unicode": "U+0F5A", "pronunciation": "tsha", "description_zh": "送气塞擦音", "description_en": "Aspirated 'ts'"},
{"id": "con_19", "letter": "ཛ", "unicode": "U+0F5B", "pronunciation": "dza", "description_zh": "浊塞擦音,类似汉语拼音'z'", "description_en": "Voiced alveolar affricate, like 'dz' in 'adze'"},
{"id": "con_20", "letter": "ཝ", "unicode": "U+0F5D", "pronunciation": "wa", "description_zh": "半元音,类似汉语拼音'w'", "description_en": "Labial approximant, like 'w' in 'water'"},
{"id": "con_21", "letter": "ཞ", "unicode": "U+0F5E", "pronunciation": "zha", "description_zh": "浊擦音,类似汉语拼音'r'", "description_en": "Voiced retroflex fricative, like 'zh' in 'measure'"},
{"id": "con_22", "letter": "ཟ", "unicode": "U+0F5F", "pronunciation": "za", "description_zh": "浊擦音,类似汉语拼音'z'", "description_en": "Voiced alveolar fricative, like 'z' in 'zoo'"},
{"id": "con_23", "letter": "འ", "unicode": "U+0F60", "pronunciation": "'a", "description_zh": "声门塞音,表示元音开始", "description_en": "Glottal stop, used as a vowel carrier"},
{"id": "con_24", "letter": "ཡ", "unicode": "U+0F61", "pronunciation": "ya", "description_zh": "半元音,类似汉语拼音'y'", "description_en": "Palatal approximant, like 'y' in 'yes'"},
{"id": "con_25", "letter": "ར", "unicode": "U+0F62", "pronunciation": "ra", "description_zh": "颤音,类似西班牙语'r'", "description_en": "Alveolar trill, like rolled 'r'"},
{"id": "con_26", "letter": "ལ", "unicode": "U+0F63", "pronunciation": "la", "description_zh": "边音,类似汉语拼音'l'", "description_en": "Alveolar lateral, like 'l' in 'love'"},
{"id": "con_27", "letter": "ཤ", "unicode": "U+0F64", "pronunciation": "sha", "description_zh": "清擦音,类似汉语拼音'sh'", "description_en": "Voiceless palatal fricative, like 'sh' in 'ship'"},
{"id": "con_28", "letter": "ས", "unicode": "U+0F66", "pronunciation": "sa", "description_zh": "清擦音,类似汉语拼音's'", "description_en": "Voiceless alveolar fricative, like 's' in 'sun'"},
{"id": "con_29", "letter": "ཧ", "unicode": "U+0F67", "pronunciation": "ha", "description_zh": "清擦音,类似汉语拼音'h'", "description_en": "Voiceless glottal fricative, like 'h' in 'hat'"},
{"id": "con_30", "letter": "ཨ", "unicode": "U+0F68", "pronunciation": "a", "description_zh": "元音字母", "description_en": "Vowel letter 'a'"}
],
"vowels": [
{"id": "vow_01", "letter": "ི", "unicode": "U+0F72", "pronunciation": "i", "description_zh": "元音符号 i,加在辅音上方", "description_en": "Vowel sign 'i', placed above the consonant"},
{"id": "vow_02", "letter": "ུ", "unicode": "U+0F74", "pronunciation": "u", "description_zh": "元音符号 u,加在辅音下方", "description_en": "Vowel sign 'u', placed below the consonant"},
{"id": "vow_03", "letter": "ེ", "unicode": "U+0F7A", "pronunciation": "e", "description_zh": "元音符号 e,加在辅音上方", "description_en": "Vowel sign 'e', placed above the consonant"},
{"id": "vow_04", "letter": "ོ", "unicode": "U+0F7C", "pronunciation": "o", "description_zh": "元音符号 o,加在辅音上方", "description_en": "Vowel sign 'o', placed above the consonant"}
]
}
# Singleton instance
learning_service = LearningService()