مدلِ vision در متنِ ریاضی LaTeX مینویسد (\frac، \sqrt، \vec) و بکاسلشِ تنها در JSON نامعتبر است، پس json.loads کلِ صفحه را دور میریخت — دقیقاً صفحههای پرفرمول، یعنی باارزشترینها. روی حسابان یازدهم/دوازدهم چهار صفحه هرچه تلاش شد «خطادار» ماندند تا همینجا پیدا شد. ⚠️ نسخهی اولِ تعمیر خودش تله داشت: \f، \t، \b، \r و \n از نظرِ JSON معتبرند، پس «سالم» رد میشدند و \frac به formfeed+«rac» و \theta به tab+«heta» تبدیل میشد — خرابیِ بیصدا بهجای خطای بلند. قاعدهی تفکیک: دستورِ LaTeX همیشه با حرفِ ASCII ادامه مییابد، ولی escapeِ واقعی با رقم، فارسی یا پایانِ رشته. کنارش: پایههای یازدهم و دوازدهم به persona و تشخیصِ پایه اضافه شدند («دوازدهم» پیش از «دهم» بررسی میشود چون زیررشتهاش را دارد)، سقفِ max_tokens از ۳۰۰۰ به ۶۰۰۰ رفت چون قاعدهی self-contained خروجی را بلندتر کرده، و حسابان یازدهم و دوازدهم با نگاشتِ فصلبهصفحه ثبت شدند. «حسابان» به MATH_SUBJECTS اضافه شد: بدونِ آن retrieval این کتابها را «درسِ متنی» میدید و بهجای chapter روی lesson_number فیلتر میکرد — بیهیچ خطایی همیشه صفر نتیجه. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
382 lines
18 KiB
Python
382 lines
18 KiB
Python
"""
|
||
نقشهی فیزیکیِ فایلهای PDF کتابهای درسی در data/textbooks/.
|
||
|
||
چرا این فایل لازم است: `ingest_pdf` یک PDF را یکتکه و با یک نامِ فصل ingest
|
||
میکند. کتابِ کاملِ درسی چند فصل دارد، پس بدونِ نگاشتِ «فصل → بازهی صفحه»
|
||
همهی نقاطِ کتاب زیرِ یک فصلِ اشتباه ذخیره میشدند و فیلترِ `chapter` در
|
||
retrieval هیچوقت درست جواب نمیداد.
|
||
|
||
دو نکته که موقعِ افزودنِ کتابِ جدید باید حواستان باشد:
|
||
|
||
- `page_offset`: شمارهی صفحهای که روی کاغذ چاپ شده با شمارهی صفحه در PDF
|
||
یکی نیست (جلد، شناسنامه، فهرست و سخنی با معلم جلوترند). فهرستِ کتاب شمارهی
|
||
کاغذی میدهد، pdf2image شمارهی PDF میخواهد: pdf_page = book_page + offset.
|
||
- بازهها بر اساسِ فهرستِ خودِ کتاباند و شاملِ صفحهی جداکنندهی فصل هم
|
||
میشوند؛ صفحهی آخرِ هر فصل = یکی قبل از شروعِ فصلِ بعد.
|
||
|
||
مقادیرِ زیر با خواندنِ فهرست و بازبینیِ چشمیِ صفحههای مرزی از همان فایلها
|
||
درآمدهاند (۱۴۰۵/۰۶/۲۱).
|
||
"""
|
||
from dataclasses import dataclass
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Section:
|
||
"""یک «درس» داخلِ فصل، همانطور که در فهرستِ کتاب آمده."""
|
||
|
||
number: int
|
||
title: str
|
||
first_page: int # شمارهی چاپشده روی کاغذ
|
||
last_page: int = 0 # از روی درسِ بعدی پر میشود؛ صفر یعنی «تا آخرِ فصل»
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Chapter:
|
||
number: int
|
||
title: str # عنوانِ کاملِ فصل، دقیقاً همان رشتهای که در payload مینشیند
|
||
first_page: int # شمارهی چاپشده روی کاغذ
|
||
last_page: int
|
||
sections: tuple[Section, ...] = ()
|
||
|
||
def resolved_sections(self) -> tuple[Section, ...]:
|
||
"""درسها با last_page پرشده (هر درس تا یکی قبل از درسِ بعدی)."""
|
||
out = []
|
||
for index, section in enumerate(self.sections):
|
||
end = (
|
||
self.sections[index + 1].first_page - 1
|
||
if index + 1 < len(self.sections) else self.last_page
|
||
)
|
||
out.append(Section(section.number, section.title, section.first_page, end))
|
||
return tuple(out)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Textbook:
|
||
subject: str
|
||
grade: str # همان رشتهای که در نامِ collection میآید («نهم»)
|
||
grade_label: str # همان رشتهای که در payloadِ `grade` مینشیند
|
||
filename: str
|
||
page_offset: int
|
||
chapters: tuple[Chapter, ...]
|
||
|
||
@property
|
||
def key(self) -> str:
|
||
return f'{self.subject} {self.grade}'.strip()
|
||
|
||
def pdf_pages(self, chapter: Chapter) -> tuple[int, int]:
|
||
return chapter.first_page + self.page_offset, chapter.last_page + self.page_offset
|
||
|
||
def chapter(self, number: int) -> Chapter | None:
|
||
return next((c for c in self.chapters if c.number == number), None)
|
||
|
||
|
||
def _ch(*rows) -> tuple[Chapter, ...]:
|
||
"""rows: (number, title, first_page, last_page, [(sec_no, sec_title, sec_page), ...])"""
|
||
chapters = []
|
||
for row in rows:
|
||
number, title, first, last = row[:4]
|
||
sections = tuple(Section(*s) for s in (row[4] if len(row) > 4 else ()))
|
||
chapters.append(Chapter(number, title, first, last, sections))
|
||
return tuple(chapters)
|
||
|
||
|
||
MATH_3 = Textbook(
|
||
subject='ریاضی', grade='سوم', grade_label='سوم دبستان',
|
||
filename='', page_offset=0,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: الگوها', 0, 0),
|
||
(2, 'فصل ۲: عددهای چهار رقمی', 0, 0),
|
||
(3, 'فصل ۳: عددهای کسری', 0, 0),
|
||
(4, 'فصل ۴: ضرب و تقسیم', 0, 0),
|
||
(5, 'فصل ۵: محیط و مساحت', 0, 0),
|
||
(6, 'فصل ۶: جمع و تفریق', 0, 0),
|
||
(7, 'فصل ۷: آمار و احتمال', 0, 0),
|
||
(8, 'فصل ۸: ضرب عددها', 0, 0),
|
||
),
|
||
)
|
||
|
||
# هشتم تنها کتابیست که هم PDF دارد و هم روی Qdrant نیست — یعنی فصلبندی و
|
||
# درسهایش کاملاً از فهرستِ خودِ کتاب میآید و ingest دقیقاً همین رشتهها را
|
||
# مینویسد. «مرور فصل» جزء آخرین درسِ هر فصل حساب شده است.
|
||
MATH_8 = Textbook(
|
||
subject='ریاضی', grade='هشتم', grade_label='هشتم',
|
||
filename='ریاضی هشتم.pdf', page_offset=8,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: عددهای صحیح و گویا', 1, 18, (
|
||
(1, 'یادآوری عددهای صحیح', 2),
|
||
(2, 'معرفی عددهای گویا', 6),
|
||
(3, 'جمع و تفریق عددهای گویا', 10),
|
||
(4, 'ضرب و تقسیم عددهای گویا', 14),
|
||
)),
|
||
(2, 'فصل ۲: عددهای اول', 19, 28, (
|
||
(1, 'یادآوری عددهای اول', 20),
|
||
(2, 'تعیین عددهای اول', 24),
|
||
)),
|
||
(3, 'فصل ۳: چندضلعیها', 29, 50, (
|
||
(1, 'چندضلعیها و تقارن', 30),
|
||
(2, 'توازی و تعامد', 34),
|
||
(3, 'چهارضلعیها', 38),
|
||
(4, 'زاویههای داخلی', 42),
|
||
(5, 'زاویههای خارجی', 46),
|
||
)),
|
||
(4, 'فصل ۴: جبر و معادله', 51, 68, (
|
||
(1, 'ساده کردن عبارتهای جبری', 52),
|
||
(2, 'پیدا کردن مقدار یک عبارت جبری', 56),
|
||
(3, 'تجزیه عبارتهای جبری', 60),
|
||
(4, 'معادله', 64),
|
||
)),
|
||
(5, 'فصل ۵: بردار و مختصات', 69, 82, (
|
||
(1, 'جمع بردارها', 70),
|
||
(2, 'ضرب عدد در بردار', 74),
|
||
(3, 'بردارهای واحد مختصات', 78),
|
||
)),
|
||
(6, 'فصل ۶: مثلث', 83, 100, (
|
||
(1, 'رابطهٔ فیثاغورس', 84),
|
||
(2, 'شکلهای همنهشت', 88),
|
||
(3, 'مثلثهای همنهشت', 92),
|
||
(4, 'همنهشتی مثلثهای قائمالزاویه', 96),
|
||
)),
|
||
(7, 'فصل ۷: توان و جذر', 101, 118, (
|
||
(1, 'توان', 102),
|
||
(2, 'تقسیم اعداد تواندار', 106),
|
||
(3, 'جذر تقریبی', 110),
|
||
(4, 'نمایش اعداد رادیکالی روی محور اعداد', 114),
|
||
(5, 'خواص ضرب و تقسیم رادیکالها', 115),
|
||
)),
|
||
(8, 'فصل ۸: آمار و احتمال', 119, 136, (
|
||
(1, 'دستهبندی دادهها', 120),
|
||
(2, 'میانگین دادهها', 124),
|
||
(3, 'احتمال یا اندازهگیری شانس', 128),
|
||
(4, 'بررسی حالتهای ممکن', 132),
|
||
)),
|
||
(9, 'فصل ۹: دایره', 137, 150, (
|
||
(1, 'خط و دایره', 138),
|
||
(2, 'زاویههای مرکزی', 142),
|
||
(3, 'زاویههای محاطی', 146),
|
||
)),
|
||
),
|
||
)
|
||
|
||
MATH_9 = Textbook(
|
||
subject='ریاضی', grade='نهم', grade_label='نهم',
|
||
filename='ریاضی نهم.pdf', page_offset=8,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: مجموعهها', 1, 17, (
|
||
(1, 'معرفی مجموعه', 2),
|
||
(2, 'مجموعههای برابر و نمایش مجموعهها', 6),
|
||
(3, 'اجتماع، اشتراک و تفاضلِ مجموعهها', 11),
|
||
(4, 'مجموعهها و احتمال', 15),
|
||
)),
|
||
(2, 'فصل ۲: عددهای حقیقی', 18, 31, (
|
||
(1, 'عددهای گویا', 19),
|
||
(2, 'عددهای حقیقی', 23),
|
||
(3, 'قدر مطلق و محاسبهٔ تقریبی', 28),
|
||
)),
|
||
(3, 'فصل ۳: استدلال و اثبات در هندسه', 32, 58, (
|
||
(1, 'استدلال', 33),
|
||
(2, 'آشنایی با اثبات در هندسه', 37),
|
||
(3, 'همنهشتی مثلثها', 44),
|
||
(4, 'حل مسئله در هندسه', 49),
|
||
(5, 'شکلهای متشابه', 53),
|
||
)),
|
||
(4, 'فصل ۴: توان و ریشه', 59, 77, (
|
||
(1, 'توان صحیح', 60),
|
||
(2, 'نماد علمی', 65),
|
||
(3, 'ریشهگیری', 68),
|
||
(4, 'جمع و تفریق رادیکالها', 73),
|
||
)),
|
||
(5, 'فصل ۵: عبارتهای جبری', 78, 94, (
|
||
(1, 'عبارتهای جبری و مفهوم اتحاد', 79),
|
||
(2, 'چند اتحاد دیگر، تجزیه و کاربردها', 86),
|
||
(3, 'نابرابریها و نامعادلهها', 90),
|
||
)),
|
||
(6, 'فصل ۶: خط و معادلههای خطی', 95, 112, (
|
||
(1, 'معادلهٔ خط', 96),
|
||
(2, 'شیب خط و عرض از مبدأ', 102),
|
||
(3, 'دستگاه معادلههای خطی', 108),
|
||
)),
|
||
(7, 'فصل ۷: عبارتهای گویا', 113, 129, (
|
||
(1, 'معرفی و ساده کردن عبارتهای گویا', 114),
|
||
(2, 'محاسبات عبارتهای گویا', 119),
|
||
(3, 'تقسیم چندجملهایها', 126),
|
||
)),
|
||
(8, 'فصل ۸: حجم و مساحت', 130, 143, (
|
||
(1, 'حجم و مساحت کره', 131),
|
||
(2, 'حجم هرم و مخروط', 135),
|
||
(3, 'سطح و حجم', 140),
|
||
)),
|
||
),
|
||
)
|
||
|
||
MATH_10 = Textbook(
|
||
subject='ریاضی', grade='دهم', grade_label='دهم',
|
||
filename='ریاضی دهم.pdf', page_offset=6,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: مجموعه، الگو و دنباله', 1, 27, (
|
||
(1, 'مجموعههای متناهی و نامتناهی', 2),
|
||
(2, 'متمم یک مجموعه', 8),
|
||
(3, 'الگو و دنباله', 14),
|
||
(4, 'دنبالههای حسابی و هندسی', 21),
|
||
)),
|
||
(2, 'فصل ۲: مثلثات', 28, 46, (
|
||
(1, 'نسبتهای مثلثاتی', 29),
|
||
(2, 'دایرهٔ مثلثاتی', 36),
|
||
(3, 'روابط بین نسبتهای مثلثاتی', 42),
|
||
)),
|
||
(3, 'فصل ۳: توانهای گویا و عبارتهای جبری', 47, 68, (
|
||
(1, 'ریشه و توان', 48),
|
||
(2, 'ریشهٔ nام', 54),
|
||
(3, 'توانهای گویا', 59),
|
||
(4, 'عبارتهای جبری', 62),
|
||
)),
|
||
(4, 'فصل ۴: معادلهها و نامعادلهها', 69, 93, (
|
||
(1, 'معادلهٔ درجه دوم و روشهای مختلف حل آن', 70),
|
||
(2, 'سهمی', 78),
|
||
(3, 'تعیین علامت', 83),
|
||
)),
|
||
(5, 'فصل ۵: تابع', 94, 117, (
|
||
(1, 'مفهوم تابع و بازنماییهای آن', 95),
|
||
(2, 'دامنه و برد توابع', 101),
|
||
(3, 'انواع تابع', 109),
|
||
)),
|
||
(6, 'فصل ۶: شمارش، بدون شمردن', 118, 140, (
|
||
(1, 'شمارش', 119),
|
||
(2, 'جایگشت', 127),
|
||
(3, 'ترکیب', 133),
|
||
)),
|
||
(7, 'فصل ۷: آمار و احتمال', 141, 170, (
|
||
(1, 'احتمال یا اندازهگیری شانس', 142),
|
||
(2, 'مقدمهای بر علم آمار، جامعه و نمونه', 152),
|
||
(3, 'متغیر و انواع آن', 159),
|
||
)),
|
||
),
|
||
)
|
||
|
||
# پایههای ششم و هفتم را تیمِ دیگر ingest کرده و روی Qdrant هستند؛ فایلِ PDF
|
||
# برایشان لازم نیست و بازهی صفحه هم نداریم (نقاطشان شمارهی صفحه ندارند). فقط
|
||
# فصلبندی ثبت میشود تا `seed_curriculum_from_textbook` بتواند برایشان برنامهی
|
||
# درسی بسازد؛ `filename` خالی یعنی «این کتاب از مسیرِ PDF ingest نمیشود».
|
||
MATH_6 = Textbook(
|
||
subject='ریاضی', grade='ششم', grade_label='ششم دبستان',
|
||
filename='', page_offset=0,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: عدد و الگوهای عددی', 0, 0),
|
||
(2, 'فصل ۲: کسر', 0, 0),
|
||
(3, 'فصل ۳: اعداد اعشاری', 0, 0),
|
||
(4, 'فصل ۴: تقارن و مختصات', 0, 0),
|
||
(5, 'فصل ۵: اندازه گیری', 0, 0),
|
||
(6, 'فصل ۶: تناسب و درصد', 0, 0),
|
||
(7, 'فصل ۷: تقریب', 0, 0),
|
||
),
|
||
)
|
||
|
||
MATH_7 = Textbook(
|
||
subject='ریاضی', grade='هفتم', grade_label='هفتم',
|
||
filename='', page_offset=0,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: راهبردهای حل مسئله', 0, 0),
|
||
(2, 'فصل ۲: عددهای صحیح', 0, 0),
|
||
(3, 'فصل ۳: جبر و معادله', 0, 0),
|
||
(4, 'فصل ۴: هندسه و استدلال', 0, 0),
|
||
(5, 'فصل ۵: شمارندهها و اعداد اول', 0, 0),
|
||
(6, 'فصل ۶: سطح و حجم', 0, 0),
|
||
(7, 'فصل ۷: توان و جذر', 0, 0),
|
||
(8, 'فصل ۸: بردار و مختصات', 0, 0),
|
||
(9, 'فصل ۹: آمار و احتمال', 0, 0),
|
||
),
|
||
)
|
||
|
||
# ── حسابان ─────────────────────────────────────────────────────────────
|
||
# «حسابان» کتابِ رشتهی ریاضیفیزیک است، نه «ریاضی» عمومی. subject عمداً
|
||
# «حسابان» است تا collection جدا بماند: همنامیِ آن با «ریاضی یازدهم»
|
||
# (کتابِ تجربی) دو کتابِ متفاوت را سرِ یک collection میانداخت.
|
||
# ⚠️ هر subjectِ تازهی ریاضیوار باید به known_collections.MATH_SUBJECTS هم
|
||
# اضافه شود، وگرنه retrieval آن را «درسِ متنی» میبیند و بهجای `chapter`
|
||
# روی `lesson_number` فیلتر میکند — یعنی بیسروصدا هیچوقت چیزی برنمیگرداند.
|
||
#
|
||
# بازهها از فهرستِ خودِ کتاب (PDF صفحهی ۵) درآمدهاند و page_offset با
|
||
# تطبیقِ صفحهی ۱ کتاب روی صفحهی ۷ PDF تأیید شده است.
|
||
CALCULUS_11 = Textbook(
|
||
subject='حسابان', grade='یازدهم', grade_label='یازدهم',
|
||
filename='حسابان یازدهم.pdf', page_offset=6,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: جبر و معادله', 1, 36, (
|
||
(1, 'مجموع جملههای دنبالههای حسابی و هندسی', 2),
|
||
(2, 'معادلههای درجه دوم', 7),
|
||
(3, 'معادلههای گویا و گنگ', 17),
|
||
(4, 'قدر مطلق و ویژگیهای آن', 23),
|
||
(5, 'آشنایی با هندسه تحلیلی', 29),
|
||
)),
|
||
(2, 'فصل ۲: تابع', 37, 70, (
|
||
(1, 'آشنایی بیشتر با تابع', 38),
|
||
(2, 'انواع توابع', 44),
|
||
(3, 'وارون تابع', 54),
|
||
(4, 'اعمال روی توابع', 63),
|
||
)),
|
||
(3, 'فصل ۳: توابع نمایی و لگاریتمی', 71, 90, (
|
||
(1, 'تابع نمایی', 72),
|
||
(2, 'تابع لگاریتمی و لگاریتم', 80),
|
||
(3, 'ویژگیهای لگاریتم و حل معادلههای لگاریتمی', 86),
|
||
)),
|
||
(4, 'فصل ۴: مثلثات', 91, 112, (
|
||
(1, 'رادیان', 92),
|
||
(2, 'نسبتهای مثلثاتی برخی زوایا', 98),
|
||
(3, 'توابع مثلثاتی', 105),
|
||
(4, 'روابط مثلثاتی مجموع و تفاضل زوایا', 110),
|
||
)),
|
||
(5, 'فصل ۵: حد و پیوستگی', 113, 151, (
|
||
(1, 'مفهوم حد و فرایندهای حدی', 114),
|
||
(2, 'حدهای یکطرفه (حد چپ و حد راست)', 123),
|
||
(3, 'قضایای حد', 130),
|
||
(4, 'محاسبه حد توابع کسری', 141),
|
||
(5, 'پیوستگی', 145),
|
||
)),
|
||
),
|
||
)
|
||
|
||
CALCULUS_12 = Textbook(
|
||
subject='حسابان', grade='دوازدهم', grade_label='دوازدهم',
|
||
filename='حسابان دوازدهم.pdf', page_offset=6,
|
||
chapters=_ch(
|
||
(1, 'فصل ۱: تابع', 1, 22, (
|
||
(1, 'تبدیل نمودار توابع', 2),
|
||
(2, 'تابع درجه سوم، توابع یکنوا و بخشپذیری و تقسیم', 13),
|
||
)),
|
||
(2, 'فصل ۲: مثلثات', 23, 44, (
|
||
(1, 'تناوب و تانژانت', 24),
|
||
(2, 'معادلههای مثلثاتی', 35),
|
||
)),
|
||
(3, 'فصل ۳: حدهای نامتناهی ـ حد در بینهایت', 45, 70, (
|
||
(1, 'حدهای نامتناهی', 46),
|
||
(2, 'حد در بینهایت', 59),
|
||
)),
|
||
(4, 'فصل ۴: مشتق', 71, 110, (
|
||
(1, 'آشنایی با مفهوم مشتق', 72),
|
||
(2, 'مشتقپذیری و پیوستگی', 84),
|
||
(3, 'آهنگ متوسط تغییر و آهنگ لحظهای تغییر', 102),
|
||
)),
|
||
(5, 'فصل ۵: کاربردهای مشتق', 111, 146, (
|
||
(1, 'اکسترممهای یک تابع و توابع صعودی و نزولی', 112),
|
||
(2, 'جهت تقعر نمودار یک تابع و نقطهی عطف آن', 127),
|
||
(3, 'رسم نمودار تابع', 137),
|
||
)),
|
||
),
|
||
)
|
||
|
||
|
||
TEXTBOOKS: dict[str, Textbook] = {
|
||
b.key: b for b in (MATH_3, MATH_6, MATH_7, MATH_8, MATH_9, MATH_10,
|
||
CALCULUS_11, CALCULUS_12)
|
||
}
|
||
|
||
# کتابهایی که واقعاً فایلِ PDF دارند و `rag_ingest_book` رویشان کار میکند.
|
||
INGESTABLE = {b.key: b for b in TEXTBOOKS.values() if b.filename}
|
||
|
||
|
||
def get_textbook(subject: str, grade: str) -> Textbook | None:
|
||
return TEXTBOOKS.get(f'{(subject or "").strip()} {(grade or "").strip()}')
|
||
|
||
|
||
def chapter_titles(book: Textbook) -> dict[int, str]:
|
||
return {c.number: c.title for c in book.chapters}
|