feat(lessons): implement comprehensive lesson configurator, session runner and AI debrief (Beta)

This commit is contained in:
Antigravity committed 2026-10-07 13:03:16 +03:00
1 parent 31e105b2c2
commit 718e9797eb
12 files changed
+4093 -1

No files matched your search

+649
View File
@@ -0,0 +1,649 @@
"""
Comprehensive Lesson Builder & Runner Module (Beta).
Enables customizable, multi-step learning workflows (Words -> Grammar -> Listening -> Reading -> AI Debrief).
100% offline, zero-dependency, pure Python stdlib + SQLite.
"""
import json
import sqlite3
import random
from typing import Dict, Any, List, Optional
from datetime import datetime
# -----------------------------------------------------------------------------
# Curated High-Yield Bite-Sized Reading Articles & Facts
# -----------------------------------------------------------------------------
CURATED_ARTICLES: List[Dict[str, Any]] = [
{
"id": "art-01",
"title": "The James Webb Space Telescope: Peering into the Cosmic Dawn",
"title_ru": "Космический телескоп Джеймс Уэбб: взгляд в космический рассвет",
"topic": "Science",
"level": "B1",
"read_time_min": 2,
"paragraphs": [
"Operating in the cold vacuum of deep space, the James Webb Space Telescope observes the universe in high-resolution infrared light. Its massive gold-coated beryllium mirror captures faint photons emitted by the earliest galaxies over thirteen billion years ago.",
"Unlike optical telescopes, infrared astronomy allows scientists to pierce through dense cosmic dust clouds where stars and planetary systems are born. Recently, the observatory discovered complex organic molecules in distant stellar nurseries, challenging previous models of early cosmic chemistry."
],
"paragraphs_ru": [
"Работая в холодном вакууме глубокого космоса, космический телескоп «Джеймс Уэбб» наблюдает за Вселенной в инфракрасном диапазоне высокого разрешения. Его массивное бериллиевое зеркало с золотым покрытием улавливает слабые фотоны, испущенные самыми ранними галактиками более тринадцати миллиардов лет назад.",
"В отличие от оптических телескопов, инфракрасная астрономия позволяет учёным проникать сквозь плотные облака космической пыли, где рождаются звёзды и планетные системы. Недавно обсерватория обнаружила сложные органические молекулы в далёких звёздных колыбелях, что поставило под сомнение прежние модели ранней космической химии."
],
"vocabulary_highlights": [
{"term": "vacuum", "trans": "вакуум, пустота"},
{"term": "beryllium", "trans": "бериллий"},
{"term": "faint", "trans": "слабый, едва заметный"},
{"term": "photons", "trans": "фотоны"},
{"term": "stellar nurseries", "trans": "звёздные колыбели"},
{"term": "pierce through", "trans": "проникать сквозь, протыкать"}
]
},
{
"id": "art-02",
"title": "The Hidden Architecture of Neural Networks",
"title_ru": "Скрытая архитектура нейронных сетей",
"topic": "Technology",
"level": "B2",
"read_time_min": 2,
"paragraphs": [
"Modern artificial neural networks are loosely inspired by the biological connections in the human cortex. Each artificial neuron computes a weighted sum of its inputs and applies a non-linear activation function before propagating the signal onward.",
"During the backpropagation process, the network calculates gradient vectors to incrementally adjust billions of internal parameters. This iterative mathematical tuning enables language models to perceive subtle nuances of syntax, sentiment, and context without explicit hard-coded rules."
],
"paragraphs_ru": [
"Современные искусственные нейронные сети отдалённо вдохновлены биологическими связями в коре головного мозга человека. Каждый искусственный нейрон вычисляет взвешенную сумму своих входов и применяет нелинейную функцию активации перед передачей сигнала дальше.",
"В процессе обратного распространения ошибки сеть вычисляет векторы градиента, чтобы постепенно корректировать миллиарды внутренних параметров. Эта итеративная математическая настройка позволяет языковым моделям улавливать тонкие нюансы синтаксиса, тональности и контекста без явных жестко заданных правил."
],
"vocabulary_highlights": [
{"term": "loosely inspired", "trans": "отдалённо вдохновлённый"},
{"term": "cortex", "trans": "кора (головного мозга)"},
{"term": "weighted sum", "trans": "взвешенная сумма"},
{"term": "propagating", "trans": "распространяющий, передающий"},
{"term": "gradient vectors", "trans": "векторы градиента"},
{"term": "subtle nuances", "trans": "тонкие нюансы"}
]
},
{
"id": "art-03",
"title": "The Remarkable Intelligence of Crows and Ravens",
"title_ru": "Удивительный интеллект ворон и воронов",
"topic": "Nature",
"level": "B1",
"read_time_min": 2,
"paragraphs": [
"Corvids, the bird family that includes ravens and crows, display cognitive abilities rivaling those of primates. Laboratory experiments reveal that crows can fashion specialized hooks from wire to retrieve food containers placed out of beak reach.",
"Furthermore, field observations demonstrate that wild crows remember specific human faces for several years and communicate warnings about hostile individuals to the rest of the flock across generations."
],
"paragraphs_ru": [
"Врановые — семейство птиц, включающее воронов и ворон, демонстрируют когнитивные способности, соперничающие со способностями приматов. Лабораторные эксперименты показывают, что вороны могут изготавливать специальные крючки из проволоки, чтобы доставать контейнеры с едой, находящиеся вне досягаемости клюва.",
"Более того, полевые наблюдения доказывают, что дикие вороны помнят конкретные человеческие лица в течение нескольких лет и передают предупреждения о недружелюбных особях остальной стае через поколения."
],
"vocabulary_highlights": [
{"term": "corvids", "trans": "врановые (птицы)"},
{"term": "cognitive abilities", "trans": "когнитивные способности"},
{"term": "rivaling", "trans": "соперничающий с"},
{"term": "fashion hooks", "trans": "изготавливать крючки"},
{"term": "hostile individuals", "trans": "враждебные особи, недоброжелатели"},
{"term": "flock", "trans": "стая"}
]
},
{
"id": "art-04",
"title": "Why We Sleep: The Science of Memory Consolidation",
"title_ru": "Зачем мы спим: наука о консолидации памяти",
"topic": "Health",
"level": "B1",
"read_time_min": 2,
"paragraphs": [
"While the body rests during deep sleep, the brain is engaged in vigorous maintenance work. The hippocampus transfers newly acquired information into the neocortex for long-term storage, effectively converting fragile short-term memories into enduring knowledge.",
"Simultaneously, the glymphatic system flushes away metabolic waste and toxic proteins that accumulate between neurons during waking hours. Sleep deprivation severely impairs this cleaning cycle, making rapid vocabulary retention far more difficult."
],
"paragraphs_ru": [
"Пока тело отдыхает во время глубокого сна, мозг занят активной работой по техобслуживанию. Гиппокамп переносит вновь полученную информацию в неокортекс для долговременного хранения, эффективно преобразуя хрупкие кратковременные воспоминания в прочные знания.",
"Одновременно с этим глимфатическая система вымывает метаболические отходы и токсичные белки, которые накапливаются между нейронами в часы бодрствования. Недосыпание серьёзно нарушает этот цикл очистки, делая быстрое запоминание словарного запаса намного более трудным."
],
"vocabulary_highlights": [
{"term": "vigorous maintenance", "trans": "активное техобслуживание / поддержка"},
{"term": "fragile memories", "trans": "хрупкие воспоминания"},
{"term": "enduring knowledge", "trans": "прочные, долговечные знания"},
{"term": "flushes away", "trans": "смывает, вымывает"},
{"term": "sleep deprivation", "trans": "депривация сна, недосыпание"},
{"term": "vocabulary retention", "trans": "удержание словарного запаса"}
]
},
{
"id": "art-05",
"title": "The Antikythera Mechanism: Ancient Greek Analog Computer",
"title_ru": "Антикитерский механизм: древнегреческий аналоговый компьютер",
"topic": "History",
"level": "B2",
"read_time_min": 2,
"paragraphs": [
"Discovered by sponge divers inside a Roman shipwreck near the Greek island of Antikythera in 1901, this corroded bronze device bewildered historians for decades. Modern micro-computed tomography revealed an intricate clockwork mechanism consisting of over thirty precision gear wheels.",
"Constructed around the second century BC, the mechanism accurately tracked the astronomical positions of the Sun, Moon, and five known planets. It could even predict solar and lunar eclipses years in advance, showcasing a technological sophistication unmatched until fourteenth-century mechanical clocks."
],
"paragraphs_ru": [
"Обнаруженное ловцами губок внутри римского кораблекрушения возле греческого острова Антикитера в 1901 году, это корродированное бронзовое устройство приводило историков в замешательство на протяжении десятилетий. Современная микрокомпьютерная томография выявила сложнейший часовой механизм, состоящий из более чем тридцати прецизионных зубчатых колёс.",
"Созданный около второго века до нашей эры, механизм точно отслеживал астрономические положения Солнца, Луны и пяти известных планет. Он мог даже предсказывать солнечные и лунные затмения на годы вперёд, демонстрируя технологическую сложность, которой не было равных вплоть до механических часов XIV века."
],
"vocabulary_highlights": [
{"term": "sponge divers", "trans": "ловцы губок"},
{"term": "corroded bronze", "trans": "подвергшаяся коррозии бронза"},
{"term": "bewildered", "trans": "сбивал с толку, озадачивал"},
{"term": "gear wheels", "trans": "зубчатые колёса, шестерни"},
{"term": "lunar eclipses", "trans": "лунные затмения"},
{"term": "sophistication", "trans": "утончённость, сложность конструкции"}
]
},
{
"id": "art-06",
"title": "Underground Fungal Networks: The Wood Wide Web",
"title_ru": "Подземные грибные сети: лесная паутина",
"topic": "Nature",
"level": "B1",
"read_time_min": 2,
"paragraphs": [
"Beneath the floor of an ancient forest lies an astonishing symbiotic network of mycorrhizal fungi. These microscopic fungal threads, known as mycelium, weave through tree roots to exchange mineral nutrients and water for sugars generated through photosynthesis.",
"Remarkably, trees utilize this subterranean internet to transmit chemical distress signals when attacked by pests, allowing neighboring trees to synthesize defensive tannins before the infestation spreads."
],
"paragraphs_ru": [
"Под пологом древнего леса скрывается поразительная симбиотическая сеть микоризных грибов. Эти микроскопические грибные нити, известные как мицелий, оплетают корни деревьев, обменивая минеральные питательные вещества и воду на сахара, вырабатываемые в процессе фотосинтеза.",
"Примечательно, что деревья используют этот подземный интернет для передачи химических сигналов бедствия при нападении вредителей, позволяя соседним деревьям синтезировать защитные танины до того, как заражение распространится."
],
"vocabulary_highlights": [
{"term": "mycorrhizal fungi", "trans": "микоризные грибы"},
{"term": "mycelium", "trans": "мицелий, грибница"},
{"term": "subterranean", "trans": "подземный"},
{"term": "distress signals", "trans": "сигналы бедствия"},
{"term": "infestation", "trans": "заражение вредителями, нашествие"}
]
},
{
"id": "art-07",
"title": "Quantum Superposition and the Threshold of Reality",
"title_ru": "Квантовая суперпозиция и порог реальности",
"topic": "Science",
"level": "B2",
"read_time_min": 2,
"paragraphs": [
"In quantum mechanics, subatomic particles exist simultaneously in multiple probable states until an observation forces the wave function to collapse into a single definitive outcome. This fundamental counterintuitive principle is known as quantum superposition.",
"Quantum computers harness this phenomenon by employing qubits instead of classical binary bits. While a traditional bit can only register as a zero or a one, a qubit exists in a superposition of both states, granting exponential computational scaling for cryptographic and molecular simulation algorithms."
],
"paragraphs_ru": [
"В квантовой механике субатомные частицы существуют одновременно во множестве вероятностных состояний до тех пор, пока наблюдение не заставляет волновую функцию коллапсировать в один определённый исход. Этот фундаментальный парадоксальный принцип известен как квантовая суперпозиция.",
"Квантовые компьютеры используют этот феномен, применяя кубиты вместо классических двоичных битов. В то время как традиционный бит может принимать значение только ноль или единица, кубит существует в суперпозиции обоих состояний, обеспечивая экспоненциальное масштабирование вычислений для алгоритмов криптографии и моделирования молекул."
],
"vocabulary_highlights": [
{"term": "subatomic particles", "trans": "субатомные частицы"},
{"term": "counterintuitive", "trans": "парадоксальный, нелогичный на первый взгляд"},
{"term": "harness", "trans": "укрощать, эффективно использовать"},
{"term": "qubits", "trans": "кубиты"},
{"term": "exponential scaling", "trans": "экспоненциальное масштабирование"}
]
},
{
"id": "art-08",
"title": "The Evolution of Coffee Culture and the Age of Enlightenment",
"title_ru": "Эволюция кофейной культуры и Эпоха Просвещения",
"topic": "Culture",
"level": "A2",
"read_time_min": 2,
"paragraphs": [
"During the seventeenth century, European coffee houses emerged as lively hubs for intellectual debate, political discussion, and commercial trade. In London, these establishments were nicknamed 'penny universities' because for the price of a cup of coffee, any customer could participate in philosophical discourse.",
"Scientists like Isaac Newton and economists like Adam Smith regularly met in coffee houses to draft treatises and debate ideas that laid the foundations of the scientific revolution and modern commerce."
],
"paragraphs_ru": [
"В XVII веке европейские кофейни стали оживлёнными центрами интеллектуальных дебатов, политических дискуссий и коммерческой торговли. В Лондоне эти заведения называли «пенни-университетами», потому что по цене чашки кофе любой посетитель мог поучаствовать в философских рассуждениях.",
"Учёные вроде Исаака Ньютона и экономисты вроде Адама Смита регулярно собирались в кофейнях, чтобы составлять трактаты и обсуждать идеи, заложившие основы научной революции и современной коммерции."
],
"vocabulary_highlights": [
{"term": "lively hubs", "trans": "оживлённые центры"},
{"term": "philosophical discourse", "trans": "философские рассуждения / дискуссии"},
{"term": "draft treatises", "trans": "составлять трактаты"},
{"term": "laid the foundations", "trans": "заложили основы"}
]
}
]
# -----------------------------------------------------------------------------
# Default Lesson Presets (Ready-to-use Configurations)
# -----------------------------------------------------------------------------
DEFAULT_PRESETS: List[Dict[str, Any]] = [
{
"id": "express",
"name": "⚡ Утренний экспресс (10 мин)",
"icon": "⚡",
"description": "Быстрый разгон словарного запаса, ретро-тест грамматики и 1 аудио-ситуация.",
"estimated_min": 10,
"is_default": 1,
"steps": [
{
"id": "step_vocab",
"type": "vocab",
"title": "Вспоминаем слова (SRS)",
"enabled": True,
"count": 10,
"mode": "flashcards",
"filter": "due"
},
{
"id": "step_grammar_review",
"type": "grammar_review",
"title": "Закрепление грамматики",
"enabled": True,
"count": 5,
"topic_mode": "weakest"
},
{
"id": "step_listening",
"type": "listening",
"title": "Аудирование на слух",
"enabled": True,
"count": 1,
"topic": "all"
},
{
"id": "step_ai_debrief",
"type": "ai_debrief",
"title": "ИИ-Итог и рекомендации",
"enabled": True
}
]
},
{
"id": "balanced",
"name": "🎯 Сбалансированный урок (20 мин)",
"icon": "🎯",
"description": "Слова + закрепление прошлой темы + новая теория Мерфи + 2 ситуации на слух.",
"estimated_min": 20,
"is_default": 1,
"steps": [
{
"id": "step_vocab",
"type": "vocab",
"title": "Вспоминаем слова (SRS)",
"enabled": True,
"count": 10,
"mode": "quiz",
"filter": "due"
},
{
"id": "step_grammar_review",
"type": "grammar_review",
"title": "Закрепление грамматики",
"enabled": True,
"count": 5,
"topic_mode": "weakest"
},
{
"id": "step_grammar_new",
"type": "grammar_new",
"title": "Новая тема Мерфи",
"enabled": True,
"unit_mode": "auto_next",
"exercises_count": 5
},
{
"id": "step_listening",
"type": "listening",
"title": "Аудирование на слух",
"enabled": True,
"count": 2,
"topic": "all"
},
{
"id": "step_ai_debrief",
"type": "ai_debrief",
"title": "ИИ-Итог и рекомендации",
"enabled": True
}
]
},
{
"id": "deep",
"name": "📖 Глубокое погружение (30 мин)",
"icon": "📖",
"description": "Полный комплексный цикл: слова, закрепление, новая грамматика, аудирование и чтение.",
"estimated_min": 30,
"is_default": 1,
"steps": [
{
"id": "step_vocab",
"type": "vocab",
"title": "Вспоминаем слова (SRS)",
"enabled": True,
"count": 15,
"mode": "typing",
"filter": "due"
},
{
"id": "step_grammar_review",
"type": "grammar_review",
"title": "Закрепление грамматики",
"enabled": True,
"count": 5,
"topic_mode": "weakest"
},
{
"id": "step_grammar_new",
"type": "grammar_new",
"title": "Новая тема Мерфи",
"enabled": True,
"unit_mode": "auto_next",
"exercises_count": 5
},
{
"id": "step_listening",
"type": "listening",
"title": "Аудирование на слух",
"enabled": True,
"count": 2,
"topic": "all"
},
{
"id": "step_reading",
"type": "reading",
"title": "Чтение и словарная работа",
"enabled": True,
"source": "curated_article",
"article_id": "random"
},
{
"id": "step_ai_debrief",
"type": "ai_debrief",
"title": "ИИ-Итог и рекомендации",
"enabled": True
}
]
},
{
"id": "audio_read",
"name": "🎧 Аудио + Чтение (15 мин)",
"icon": "🎧",
"description": "Фокус на понимании на слух и чтении статьи с пополнением словаря.",
"estimated_min": 15,
"is_default": 1,
"steps": [
{
"id": "step_vocab",
"type": "vocab",
"title": "Вспоминаем слова (SRS)",
"enabled": True,
"count": 10,
"mode": "flashcards",
"filter": "due"
},
{
"id": "step_listening",
"type": "listening",
"title": "Аудирование на слух",
"enabled": True,
"count": 3,
"topic": "all"
},
{
"id": "step_reading",
"type": "reading",
"title": "Чтение и словарная работа",
"enabled": True,
"source": "curated_article",
"article_id": "random"
},
{
"id": "step_ai_debrief",
"type": "ai_debrief",
"title": "ИИ-Итог и рекомендации",
"enabled": True
}
]
}
]
# -----------------------------------------------------------------------------
# Database & Helper Functions
# -----------------------------------------------------------------------------
def get_user_presets(conn: sqlite3.Connection, user_id: int) -> List[Dict[str, Any]]:
"""Returns built-in system presets combined with user custom saved presets."""
result = []
# 1. System defaults
for p in DEFAULT_PRESETS:
item = dict(p)
item["is_custom"] = False
result.append(item)
# 2. User custom presets from SQLite
try:
rows = conn.execute(
"""
SELECT id, name, icon, description, estimated_min, config_json, created_at
FROM lesson_presets
WHERE user_id = ?
ORDER BY id ASC
""",
(user_id,),
).fetchall()
for r in rows:
cfg = json.loads(r["config_json"]) if r["config_json"] else []
result.append({
"id": f"custom_{r['id']}",
"custom_id": r["id"],
"name": r["name"],
"icon": r["icon"] or "🚀",
"description": r["description"] or "",
"estimated_min": r["estimated_min"] or 15,
"is_default": 0,
"is_custom": True,
"steps": cfg,
"created_at": r["created_at"],
})
except Exception:
pass
return result
def save_user_preset(
conn: sqlite3.Connection,
user_id: int,
name: str,
steps: List[Dict[str, Any]],
icon: str = "🚀",
description: str = "",
estimated_min: int = 15,
) -> Dict[str, Any]:
"""Saves a custom user lesson configuration to SQLite."""
config_json = json.dumps(steps, ensure_ascii=False)
cur = conn.execute(
"""
INSERT INTO lesson_presets (user_id, name, icon, description, estimated_min, config_json)
VALUES (?, ?, ?, ?, ?, ?)
""",
(user_id, name.strip(), icon, description.strip(), estimated_min, config_json),
)
new_id = cur.lastrowid
return {
"id": f"custom_{new_id}",
"custom_id": new_id,
"name": name.strip(),
"icon": icon,
"description": description.strip(),
"estimated_min": estimated_min,
"steps": steps,
"is_custom": True,
}
def delete_user_preset(conn: sqlite3.Connection, user_id: int, custom_id: int) -> bool:
"""Deletes a custom user lesson preset."""
cur = conn.execute(
"DELETE FROM lesson_presets WHERE id = ? AND user_id = ?",
(custom_id, user_id),
)
return cur.rowcount > 0
def get_grammar_recommendations(conn: sqlite3.Connection, user_id: int) -> Dict[str, Any]:
"""
Calculates intelligent grammar suggestions for the lesson:
- weakest_unit: unit with lowest score or most errors recorded in srs_logs
- previous_unit: the last unit completed by user
- next_unit: the first uncompleted unit in pedagogical sequence
- all_units: complete list of units for manual selection
"""
# 1. Fetch all grammar units
all_units_rows = conn.execute(
"""
SELECT u.id, u.unit_num, u.level, u.category, u.title, u.subtitle, u.golden_formula,
COALESCE(p.score, 0) as user_score,
COALESCE(p.is_completed, 0) as is_completed,
p.completed_at
FROM grammar_units u
LEFT JOIN grammar_progress p ON p.unit_id = u.id AND p.user_id = ?
ORDER BY u.unit_num ASC
""",
(user_id,),
).fetchall()
all_units = []
completed_units = []
uncompleted_units = []
for r in all_units_rows:
unit_info = {
"id": r["id"],
"unit_num": r["unit_num"],
"level": r["level"],
"category": r["category"],
"title": r["title"],
"subtitle": r["subtitle"] or "",
"golden_formula": r["golden_formula"] or "",
"score": r["user_score"],
"is_completed": bool(r["is_completed"]),
"completed_at": r["completed_at"],
}
all_units.append(unit_info)
if unit_info["is_completed"]:
completed_units.append(unit_info)
else:
uncompleted_units.append(unit_info)
# 2. Determine Weakest Unit (lowest score among completed units, or unit with lowest score)
weakest_unit = None
if completed_units:
weakest_unit = min(completed_units, key=lambda u: u["score"])
elif all_units:
weakest_unit = all_units[0]
# 3. Determine Previous Unit (most recently completed unit)
previous_unit = None
if completed_units:
# Sort by completed_at desc
sorted_completed = sorted(
completed_units,
key=lambda u: u["completed_at"] or "",
reverse=True,
)
previous_unit = sorted_completed[0]
elif all_units:
previous_unit = all_units[0]
# 4. Determine Next Unit (first uncompleted unit)
next_unit = uncompleted_units[0] if uncompleted_units else (all_units[0] if all_units else None)
return {
"weakest_unit": weakest_unit,
"previous_unit": previous_unit,
"next_unit": next_unit,
"all_units": all_units,
}
def get_curated_articles(topic: Optional[str] = None) -> List[Dict[str, Any]]:
"""Returns curated reading articles, optionally filtered by category."""
if not topic or topic.lower() == "all":
return CURATED_ARTICLES
return [a for a in CURATED_ARTICLES if a.get("topic", "").lower() == topic.lower()]
def record_lesson_session(
conn: sqlite3.Connection,
user_id: int,
preset_name: str,
duration_seconds: int,
total_score: int,
grade: str,
steps_summary: List[Dict[str, Any]],
errors_list: List[Dict[str, Any]],
ai_report: Dict[str, Any],
) -> int:
"""Stores the finished composite lesson session in SQLite."""
cur = conn.execute(
"""
INSERT INTO lesson_sessions (
user_id, preset_name, duration_seconds, total_score, grade,
steps_summary_json, errors_json, ai_report_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
user_id,
preset_name,
duration_seconds,
total_score,
grade,
json.dumps(steps_summary, ensure_ascii=False),
json.dumps(errors_list, ensure_ascii=False),
json.dumps(ai_report, ensure_ascii=False),
),
)
return cur.lastrowid
def get_lesson_history(conn: sqlite3.Connection, user_id: int, limit: int = 15) -> Dict[str, Any]:
"""Retrieves previous comprehensive lesson sessions for user analytics."""
rows = conn.execute(
"""
SELECT id, preset_name, duration_seconds, total_score, grade,
steps_summary_json, errors_json, ai_report_json, completed_at
FROM lesson_sessions
WHERE user_id = ?
ORDER BY completed_at DESC
LIMIT ?
""",
(user_id, limit),
).fetchall()
history = []
total_lessons = 0
total_time_sec = 0
avg_score = 0
score_sum = 0
for r in rows:
steps = json.loads(r["steps_summary_json"]) if r["steps_summary_json"] else []
errors = json.loads(r["errors_json"]) if r["errors_json"] else []
ai_rep = json.loads(r["ai_report_json"]) if r["ai_report_json"] else {}
history.append({
"id": r["id"],
"preset_name": r["preset_name"],
"duration_seconds": r["duration_seconds"],
"total_score": r["total_score"],
"grade": r["grade"],
"steps": steps,
"errors_count": len(errors),
"headline": ai_rep.get("headline", ""),
"completed_at": r["completed_at"],
})
total_lessons += 1
total_time_sec += r["duration_seconds"] or 0
score_sum += r["total_score"] or 0
if total_lessons > 0:
avg_score = round(score_sum / total_lessons)
return {
"total_lessons": total_lessons,
"total_time_minutes": round(total_time_sec / 60),
"avg_score": avg_score,
"history": history,
}