Subjects per grade (math, calculus, geometry, stats, discrete), 5 new textbooks, 202 new islands, long islands split, hand-written figures and lesson texts for the new books

- content.Subject + Unit.subject, /api/home?subject=, tutor grounded in the chosen book
- 5 new books ingested; ~1150 hand-written questions, islands named by hand
- node_split: islands over 7 lessons split into parts of <=6 (lesson ids and progress kept)
- Lesson.figures hand-authored and visually checked for 653 lessons of the new books;
  gen_lesson_figures got --subject and survives network errors
- seed files re-exported

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:35:18 +03:30
parent 3d51b3b83a
commit 8cb1ecb85e
32 changed files with 81218 additions and 4486 deletions

View File

@@ -124,7 +124,7 @@ class Command(BaseCommand):
continue
finder._book = book
collection = build_collection_name(book.subject, book.grade)
lessons = (Lesson.objects.filter(node__unit__grade=grade)
lessons = (Lesson.objects.filter(node__unit__grade=grade, node__unit__subject__title=book.subject)
.select_related('node__unit').order_by('node__unit__order', 'node__order', 'id'))
for lesson in lessons:
if lesson.example.strip() or lesson.steps:

View File

@@ -24,6 +24,7 @@ import threading
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import openai
from django.core.management.base import BaseCommand, CommandError
from content.models import Lesson
@@ -109,6 +110,7 @@ class Command(BaseCommand):
def add_arguments(self, p):
p.add_argument('--grade', type=int, help='بدونِ آن همه‌ی پایه‌ها')
p.add_argument('--subject', help='عنوانِ کتاب (مثلاً هندسه)؛ بدونِ آن همه‌ی کتاب‌ها')
p.add_argument('--limit', type=int, default=4)
p.add_argument('--lesson', type=int, help='فقط همین شناسه')
p.add_argument('--redo', action='store_true', help='درس‌هایی که شکل دارند هم')
@@ -128,6 +130,8 @@ class Command(BaseCommand):
'node__unit__grade__number', 'node__unit__order', 'node__order', 'id')
if o['grade']:
qs = qs.filter(node__unit__grade__number=o['grade'])
if o['subject']:
qs = qs.filter(node__unit__subject__title=o['subject'])
if o['lesson']:
qs = qs.filter(pk=o['lesson'])
if not o['redo']:
@@ -179,7 +183,8 @@ class Command(BaseCommand):
return
try:
items = _json_list(ask(PROMPT + spec, text))
except (ValueError, KeyError) as e:
# خطای شبکه/درگاه یک درس را رد می‌کند، نه کلِ اجرا را؛ در ledger ثبت نمی‌شود تا دفعه‌ی بعد دوباره برود.
except (ValueError, KeyError, openai.OpenAIError) as e:
with lock:
stats['invalid'] += 1
self.stderr.write(f'درس {lesson.id}: خروجیِ نامعتبر ({e}); ذخیره نشد.')
@@ -218,7 +223,7 @@ class Command(BaseCommand):
ok = {v.get('i'): v for v in verdicts if isinstance(v, dict)}
kept = [f for i, f in enumerate(figures) if ok.get(i, {}).get('ok') is True]
notes = [f"{i}: {v.get('why', '')}" for i, v in ok.items() if v.get('ok') is not True]
except (ValueError, KeyError, AttributeError) as e:
except (ValueError, KeyError, AttributeError, openai.OpenAIError) as e:
kept, notes = [], [f'وارسی ناموفق: {e}']
with lock:
stats['rejected'] += len(figures) - len(kept)

View File

@@ -298,7 +298,7 @@ class Command(BaseCommand):
"""[(lesson, chunk), ...] — فقط درس‌هایی که هنوز سوال کم دارند."""
lessons = (
Lesson.objects
.filter(node__unit__grade=grade)
.filter(node__unit__grade=grade, node__unit__subject__title=self._book.subject)
.select_related('node__unit')
.order_by('node__unit__order', 'node__order', 'id')
)

View File

@@ -21,7 +21,7 @@
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from content.models import Grade, Lesson, Node, Unit
from content.models import Grade, Lesson, Node, Subject, Unit
from rag.models import LessonChunk
from rag.services.config import build_collection_name
from rag.services.textbooks import TEXTBOOKS, get_textbook
@@ -179,11 +179,17 @@ class Command(BaseCommand):
title=grade_title, defaults={'icon': icon, 'color': color},
)
# کتاب باید در جدولِ Subject باشد (migrationِ 0015 پنج‌تایی را می‌سازد؛ کتابِ تازه از ادمین).
subject = Subject.objects.filter(title=book.subject).first()
if subject is None:
raise CommandError(
f'کتابِ «{book.subject}» در جدولِ Subject نیست؛ از ادمین (محتوا ← کتاب‌ها) بسازید.'
)
counts = {'units': 0, 'nodes': 0, 'lessons': 0}
for chapter, groups in plan:
unit, _ = Unit.objects.update_or_create(
grade=grade, title=chapter.title,
defaults={'order': chapter.number},
defaults={'order': chapter.number, 'subject': subject},
)
counts['units'] += 1

View File

@@ -0,0 +1,36 @@
"""
جزیره‌های بلند را به جزیره‌های حداکثر ۶ درسی می‌شکند (منطق در `content/node_split.py`).
python manage.py split_long_nodes --grade 8 --dry-run
python manage.py split_long_nodes --grade 4 --grade 5
پایه‌های ۳ تا ۱۲ را migrationِ 0018 یک بار شکسته است؛ این دستور برای پایه‌ی تازه یا آستانه‌ی دیگر است و اجرای
دوباره‌اش بی‌اثر. بعد از اجرا فایل‌های seed را دوباره بسازید (`seed_content --export`)، وگرنه
`seed_content` جزیره‌های بلندِ قدیمی را کنارِ بخش‌های تازه دوباره می‌سازد.
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from content.models import Lesson, Node, NodeProgress, Unit, UserNodeCompletion
from content.node_split import split_long_nodes
class Command(BaseCommand):
help = 'شکستنِ جزیره‌های بلند به جزیره‌های کوتاه‌تر، با حفظِ پیشرفتِ کاربران.'
def add_arguments(self, parser):
parser.add_argument('--grade', type=int, action='append', required=True,
help='شماره‌ی پایه؛ چند بار قابلِ تکرار.')
parser.add_argument('--max-lessons', type=int, default=7)
parser.add_argument('--part-cap', type=int, default=6)
parser.add_argument('--dry-run', action='store_true')
def handle(self, *args, **opts):
units = Unit.objects.filter(grade__number__in=opts['grade']).order_by('id')
with transaction.atomic():
counts = split_long_nodes(
Node=Node, Lesson=Lesson, UserNodeCompletion=UserNodeCompletion,
NodeProgress=NodeProgress, units=units,
max_lessons=opts['max_lessons'], part_cap=opts['part_cap'], dry_run=opts['dry_run'],
)
self.stdout.write(f"{'(dry-run) ' if opts['dry_run'] else ''}{counts}")