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

@@ -125,7 +125,7 @@ class Command(BaseCommand):
parser.add_argument('--judge', default=DEFAULT_JUDGE)
parser.add_argument('--per-grade', type=int, default=6)
parser.add_argument('--collections', nargs='*', default=None,
help='پیش‌فرض: همه‌ی کتاب‌های ریاضی/حسابان')
help='پیش‌فرض: همه‌ی کتاب‌های ریاضی/حسابان/هندسه/آمار/گسسته')
parser.add_argument('--seed', default='bench-v1')
parser.add_argument('--workers', type=int, default=4)
parser.add_argument('--no-context', action='store_true',
@@ -149,7 +149,7 @@ class Command(BaseCommand):
if collections:
qs = qs.filter(collection__in=collections)
else:
qs = qs.filter(collection__regex=r'^(ریاضی|حسابان) ')
qs = qs.filter(collection__regex=r'^(ریاضی|حسابان|هندسه|آمار و احتمال|گسسته) ')
by_collection = defaultdict(list)
for chunk in qs.only('id', 'problem', 'checked_answer', 'collection', 'grade_title',
'subject', 'chapter'):

View File

@@ -24,7 +24,7 @@ MATH_LESSON_FILTER_KEY = 'chapter'
# همان شکلِ payload را دارد. اگر اینجا نباشد، retrieval آن را درسِ متنی حساب
# می‌کند و به‌جای `chapter` روی `lesson_number` فیلتر می‌زند — بدونِ هیچ خطایی
# همیشه صفر نتیجه.
MATH_SUBJECTS = {'ریاضی', 'حسابان'}
MATH_SUBJECTS = {'ریاضی', 'حسابان', 'هندسه', 'آمار و احتمال', 'گسسته'}
# فصول ریاضی به تفکیکِ (subject, grade) — کلیدِ داخلی هم شماره‌ی فصل و هم
# رشته‌ی کامل رو می‌پذیره.

View File

@@ -51,7 +51,7 @@ def resolve_from_lesson(lesson_id) -> dict:
lesson = (
Lesson.objects
.select_related('node__unit__grade')
.select_related('node__unit__grade', 'node__unit__subject')
.filter(pk=lesson_id)
.first()
)
@@ -67,16 +67,15 @@ def resolve_from_lesson(lesson_id) -> dict:
if not grade_title:
return {}
# عنوانِ Unit همان رشته‌ی دقیقِ فصل است؛ در کاتالوگ دنبالش می‌گردیم تا هم
# subject و هم شماره‌ی فصل دربیاید. برابریِ رشته‌ای عمدی‌ست: فیلترِ Qdrant
# هم روی همین رشته‌ی دقیق است، پس اگر اینجا نخورد آنجا هم نمی‌خورد.
# کتاب از خودِ فصل می‌آید (`Unit.subject`)، نه از حدسِ روی عنوان: یک پایه چند کتاب دارد و
# عنوانِ فصل بینِ کتاب‌ها می‌تواند تکرار شود. عنوانِ Unit همان رشته‌ی دقیقِ فصل است؛ در جدولِ
# فصل‌های همان کتاب دنبالش می‌گردیم تا شماره‌ی فصل دربیاید. برابریِ رشته‌ای عمدی‌ست: فیلترِ
# Qdrant هم روی همین رشته‌ی دقیق است، پس اگر اینجا نخورد آنجا هم نمی‌خورد.
unit_title = (unit.title or '').strip()
for (subject, grade), chapters in MATH_CHAPTERS.items():
if grade != grade_title:
continue
for number, title in chapters.items():
if title == unit_title:
return {'subject': subject, 'gradeTitle': grade_title, 'chapter': number}
subject = unit.subject.title
for number, title in MATH_CHAPTERS.get((subject, grade_title), {}).items():
if title == unit_title:
return {'subject': subject, 'gradeTitle': grade_title, 'chapter': number}
logger.info(
'rag.lesson_context: عنوانِ فصل «%s» (پایه %s) در کاتالوگ نبود',
@@ -90,10 +89,13 @@ def resolve_from_grade(grade_id) -> dict:
{'subject', 'gradeTitle'} برای وقتی که هیچ درسی در کار نیست.
صفحه‌ی «از پی بپرس» از نوارِ پایینِ اپ باز می‌شود، نه از داخلِ یک درس؛
پس نه lessonId دارد نه subject. بدونِ این تابع retrieval کلاً رد می‌شد و
همه‌ی جواب‌های آن صفحه عمومی بودند. هر پایه دقیقاً یک کتابِ ریاضی دارد،
پس پایه‌ی خودِ کاربر برای انتخابِ کتاب کافی‌ست؛ فصل معلوم نیست و جستجو
روی کلِ همان کتاب انجام می‌شود.
پس نه lessonId دارد نه (اگر فرانت نفرستد) subject. بدونِ این تابع retrieval
کلاً رد می‌شد و همه‌ی جواب‌های آن صفحه عمومی بودند.
⚠️ از ۲۰۲۶-۱۰-۱۱ یک پایه چند کتاب دارد (دوازدهم: حسابان، هندسه، گسسته). این تابع فقط
«اولین کتابِ پایه» را برمی‌گرداند (ترتیبِ ثبتِ کتاب‌ها در `TEXTBOOKS`: ریاضی/حسابان پیش از
هندسه…)؛ دانش‌آموزی که در اپ «هندسه» را انتخاب کرده باید `subject` را صریح بفرستد تا از
کتابِ خودش جواب بگیرد. فصل معلوم نیست و جستجو روی کلِ همان کتاب انجام می‌شود.
"""
from .config import resolve_grade_title

View File

@@ -470,9 +470,131 @@ CALCULUS_12 = Textbook(
)
# ── هندسه، آمار و احتمال، گسسته (۲۰۲۶-۱۰-۱۱؛ درخواستِ سینیور) ─────────────
# کتاب‌های رشته‌ی ریاضی‌فیزیک که کنارِ ریاضی/حسابانِ همان پایه می‌آیند، پس هر
# کدام subject و collectionِ خودش را دارد (همان دلیلِ «حسابان»).
# فهرست و آفست از خودِ PDFها خوانده شد (فهرست چشمی، آفست با تطبیقِ صفحه‌ی بازکننده‌ی
# فصل با متنِ همان صفحه‌ی PDF): هندسه‌ها و گسسته چاپی=PDF (۰) و (۸)؛ آمار صفحه‌ی ۱
# چاپی روی PDF ۷ است (آفست ۶، مثلِ حسابان). ⚠️ آخرین صفحه‌ی هر فصل «یکی قبل از فصل
# بعد» است و آخرین فصل تا قبل از «منابع»؛ مرزها با ingest و شمارشِ chunk وارسی شوند.
GEOMETRY_10 = Textbook(
subject='هندسه', grade='دهم', grade_label='دهم',
filename='هندسه دهم.pdf', page_offset=0,
chapters=_ch(
(1, 'فصل ۱: ترسیم‌های هندسی و استدلال', 9, 28, (
(1, 'ترسیم‌های هندسی', 10),
(2, 'استدلال', 17),
)),
(2, 'فصل ۲: قضیه‌ی تالس، تشابه و کاربردهای آن', 29, 52, (
(1, 'نسبت و تناسب در هندسه', 30),
(2, 'قضیه‌ی تالس', 34),
(3, 'تشابه مثلث‌ها', 38),
(4, 'کاربردهایی از قضیه‌ی تالس و تشابه مثلث‌ها', 45),
)),
(3, 'فصل ۳: چندضلعی‌ها', 53, 76, (
(1, 'چندضلعی‌ها و ویژگی‌هایی از آن‌ها', 54),
(2, 'مساحت و کاربردهای آن', 65),
)),
(4, 'فصل ۴: تجسم فضایی', 77, 96, (
(1, 'خط، نقطه و صفحه', 78),
(2, 'تفکر تجسمی', 87),
)),
),
)
GEOMETRY_11 = Textbook(
subject='هندسه', grade='یازدهم', grade_label='یازدهم',
filename='هندسه یازدهم.pdf', page_offset=0,
chapters=_ch(
(1, 'فصل ۱: دایره', 9, 30, (
(1, 'مفاهیم اولیه و زاویه‌ها در دایره', 10),
(2, 'رابطه‌های طولی در دایره', 18),
(3, 'چندضلعی‌های محاطی و محیطی', 24),
)),
(2, 'فصل ۲: تبدیل‌های هندسی و کاربردها', 31, 58, (
(1, 'تبدیل‌های هندسی', 32),
(2, 'کاربرد تبدیل‌ها', 50),
)),
(3, 'فصل ۳: روابط طولی در مثلث', 59, 80, (
(1, 'قضیه‌ی سینوس‌ها', 60),
(2, 'قضیه‌ی کسینوس‌ها', 64),
(3, 'قضیه‌ی نیمسازهای زوایای داخلی و محاسبه‌ی طول نیمسازها', 68),
(4, 'قضیه‌ی هرون (محاسبه‌ی ارتفاع‌ها و مساحت مثلث)', 71),
)),
),
)
GEOMETRY_12 = Textbook(
subject='هندسه', grade='دوازدهم', grade_label='دوازدهم',
filename='هندسه دوازدهم.pdf', page_offset=0,
chapters=_ch(
(1, 'فصل ۱: ماتریس و کاربردها', 9, 32, (
(1, 'ماتریس و اعمال روی ماتریس‌ها', 10),
(2, 'وارون ماتریس و دترمینان', 22),
)),
(2, 'فصل ۲: آشنایی با مقاطع مخروطی', 33, 60, (
(1, 'آشنایی با مقاطع مخروطی و مکان هندسی', 34),
(2, 'دایره', 40),
(3, 'بیضی و سهمی', 47),
)),
(3, 'فصل ۳: بردارها', 61, 86, (
(1, 'معرفی فضای R³', 62),
(2, 'ضرب داخلی و ضرب خارجی بردارها', 77),
)),
),
)
STATS_11 = Textbook(
subject='آمار و احتمال', grade='یازدهم', grade_label='یازدهم',
filename='آمار و احتمال یازدهم.pdf', page_offset=6,
chapters=_ch(
(1, 'فصل ۱: آشنایی با مبانی ریاضیات', 1, 34, (
(1, 'آشنایی با منطق ریاضی', 2),
(2, 'جبر مجموعه‌ها', 16),
)),
(2, 'فصل ۲: احتمال', 35, 68, (
(1, 'مبانی احتمال', 36),
(2, 'احتمال غیرهم‌شانس', 44),
(3, 'احتمال شرطی', 48),
(4, 'پیشامدهای مستقل و وابسته', 63),
)),
(3, 'فصل ۳: آمار توصیفی', 69, 96, (
(1, 'توصیف و نمایش داده‌ها', 70),
(2, 'معیارهای گرایش به مرکز', 80),
(3, 'معیارهای پراکندگی', 87),
)),
(4, 'فصل ۴: آمار استنباطی', 97, 121, (
(1, 'گردآوری داده‌ها', 98),
(2, 'برآورد', 112),
)),
),
)
DISCRETE_12 = Textbook(
subject='گسسته', grade='دوازدهم', grade_label='دوازدهم',
filename='گسسته دوازدهم.pdf', page_offset=8,
chapters=_ch(
(1, 'فصل ۱: آشنایی با نظریه‌ی اعداد', 1, 30, (
(1, 'استدلال ریاضی', 2),
(2, 'بخش‌پذیری در اعداد صحیح', 9),
(3, 'هم‌نهشتی در اعداد صحیح و کاربردها', 18),
)),
(2, 'فصل ۲: گراف و مدل‌سازی', 31, 54, (
(1, 'معرفی گراف', 32),
(2, 'مدل‌سازی با گراف', 43),
)),
(3, 'فصل ۳: ترکیبیات (شمارش)', 55, 84, (
(1, 'مباحثی در ترکیبیات', 56),
(2, 'روش‌هایی برای شمارش', 73),
)),
),
)
TEXTBOOKS: dict[str, Textbook] = {
b.key: b for b in (MATH_3, MATH_4, MATH_5, MATH_6, MATH_7, MATH_8, MATH_9, MATH_10,
CALCULUS_11, CALCULUS_12)
CALCULUS_11, CALCULUS_12, GEOMETRY_10, GEOMETRY_11, GEOMETRY_12,
STATS_11, DISCRETE_12)
}
# کتاب‌هایی که واقعاً فایلِ PDF دارند و `rag_ingest_book` رویشان کار می‌کند.