""" استخراج از PDF کتابِ درسی + تحلیلِ عکسِ صفحه با مدلِ vision. برداشتِ پاک‌سازی‌شده از mathapp/services/read_pdf.py و info_question.py مخزنِ Rag_backend. به‌جای نوشتنِ نتیجه در یک فایلِ JSON روی دیسک، دیکشنری برمی‌گردونه تا لایه‌ی ingest تصمیم بگیره. pymupdf (fitz) و pdf2image اختیاری‌ان؛ نبودشون فقط این ماژول رو از کار می‌ندازه (RagNotConfigured)، نه بقیه‌ی پروژه رو. """ import base64 import json from io import BytesIO from django.conf import settings from . import clients from .clients import RagNotConfigured def extract_page_images(pdf_path: str, first_page: int = 1, dpi: int = 200): """لیستی از تصاویرِ PIL برای صفحاتِ PDF از first_page به بعد.""" try: from pdf2image import convert_from_path except ModuleNotFoundError as exc: raise RagNotConfigured('pdf2image نصب نیست یا poppler موجود نیست.') from exc return convert_from_path(pdf_path, dpi=dpi, first_page=first_page) def detect_grade_and_start_page(pdf_path: str) -> tuple[int, str | None]: """ از متنِ صفحه‌ی اولِ PDF، پایه‌ی تحصیلی و صفحه‌ای که محتوای واقعی از اونجا شروع می‌شه رو حدس می‌زنه (مثلِ نسخه‌ی Rag_backend). """ try: import fitz # PyMuPDF except ModuleNotFoundError as exc: raise RagNotConfigured('pymupdf (fitz) نصب نیست.') from exc start_pages = {'پنجم دبستان': 11, 'ششم دبستان': 9} grades = ['هفتم', 'اول دبستان', 'دوم دبستان', 'سوم دبستان', 'چهارم دبستان', 'پنجم دبستان', 'ششم دبستان'] doc = fitz.open(pdf_path) try: text = doc[0].get_text() finally: doc.close() grade = next((g for g in grades if g in text), None) return start_pages.get(grade, 7), grade _VISION_JSON_TEMPLATE = """ { "grade": "", "chapter": "", "problemTypes": [ {"type": "", "keywords": [], "generalSolution": "", "exampleFromImage": {"problem": "", "solution": ""}} ] } """ def extract_problem_types(image, grade: str, chapter_name: str) -> dict: """ یک عکسِ صفحه‌ی کتاب → ساختارِ JSONِ انواعِ مسئله (فارسی)، با مدلِ vision. خروجی همون شکلی‌ست که uploadِ ریاضیِ تیمِ دیگه هم تولید می‌کرد. """ prompt = f""" You are a specialist elementary math teacher for children aged 8 to 12. Analyze the uploaded image and extract every unique math problem type for the given chapter. For each type produce: type, keywords (3-7, Persian), generalSolution (step-by-step), exampleFromImage (one real problem from the image + full solution). Output must follow EXACTLY this JSON structure: {_VISION_JSON_TEMPLATE} Rules: - Everything in Persian (Farsi). - grade = "{grade}" - chapter = "{chapter_name}" - exampleFromImage must be a real problem from the image (not invented). - Output ONLY the JSON object. No markdown, no backticks. """ buffered = BytesIO() image.save(buffered, format='JPEG') img_b64 = base64.b64encode(buffered.getvalue()).decode() client = clients.get_openai_client() response = client.chat.completions.create( model=settings.OPENAI_VISION_MODEL, messages=[ {'role': 'system', 'content': 'You process educational visual content and respond ONLY with valid JSON.'}, {'role': 'user', 'content': [ {'type': 'text', 'text': prompt}, {'type': 'image_url', 'image_url': {'url': f'data:image/jpeg;base64,{img_b64}', 'detail': 'high'}}, ]}, ], max_tokens=3000, temperature=0.1, ) raw = (response.choices[0].message.content or '').strip() if raw.startswith('```json'): raw = raw[len('```json'):].strip() if raw.startswith('```'): raw = raw[3:].strip() if raw.endswith('```'): raw = raw[:-3].strip() return json.loads(raw) def problem_type_to_text(problem_type: dict) -> str: """یک problemType → متنِ قابلِ embedding (مثلِ doc_text در Rag_backend).""" example = problem_type.get('exampleFromImage', {}) or {} keywords = ' '.join(problem_type.get('keywords', []) or []) return ( f"Type: {problem_type.get('type', '')}\n" f"Keywords: {keywords}\n" f"General solution: {problem_type.get('generalSolution', '')}\n" f"Example problem: {example.get('problem', '')}\n" f"Example solution: {example.get('solution', '')}" ).strip()