""" نقشه‌ی فیزیکیِ فایل‌های 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), ), ) TEXTBOOKS: dict[str, Textbook] = { b.key: b for b in (MATH_3, MATH_6, MATH_7, MATH_8, MATH_9, MATH_10) } # کتاب‌هایی که واقعاً فایلِ 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}