stop losing the pages that matter most to a stray backslash
مدلِ 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>
This commit is contained in:
@@ -45,12 +45,17 @@ def detect_grade_and_start_page(pdf_path: str) -> tuple[int, str | None]:
|
||||
raise RagNotConfigured('pymupdf (fitz) نصب نیست.') from exc
|
||||
|
||||
# صفحهی شروعِ محتوای واقعی؛ هرچه اینجا نباشد پیشفرضِ ۷ میگیرد.
|
||||
start_pages = {'پنجم دبستان': 11, 'ششم دبستان': 9, 'نهم': 9, 'دهم': 7}
|
||||
start_pages = {'پنجم دبستان': 11, 'ششم دبستان': 9, 'نهم': 9, 'دهم': 7,
|
||||
'یازدهم': 7, 'دوازدهم': 7}
|
||||
# ترتیب مهم است: «دهم» زیررشتهی «نهم» نیست ولی «هفتم»/«هشتم» در متنِ
|
||||
# شناسنامهی کتابِ نهم هم میآیند («در ادامهٔ پایههای هفتم و هشتم»)، پس
|
||||
# پایههای بالاتر باید زودتر بررسی شوند.
|
||||
grades = ['دهم', 'نهم', 'هشتم', 'هفتم', 'اول دبستان', 'دوم دبستان',
|
||||
'سوم دبستان', 'چهارم دبستان', 'پنجم دبستان', 'ششم دبستان']
|
||||
# ترتیب از بالا به پایین است و «دوازدهم»/«یازدهم» باید پیش از «دهم» بیایند:
|
||||
# هر دو رشتهی «دهم» را در خود دارند، پس با ترتیبِ برعکس کتابِ دوازدهم
|
||||
# «دهم» تشخیص داده میشد و صفحهی شروع و لحنِ معلمش غلط میافتاد.
|
||||
grades = ['دوازدهم', 'یازدهم', 'دهم', 'نهم', 'هشتم', 'هفتم', 'اول دبستان',
|
||||
'دوم دبستان', 'سوم دبستان', 'چهارم دبستان', 'پنجم دبستان',
|
||||
'ششم دبستان']
|
||||
|
||||
doc = fitz.open(pdf_path)
|
||||
try:
|
||||
@@ -89,6 +94,9 @@ _TEACHER_BY_GRADE = {
|
||||
'هشتم': 'a specialist middle-school math teacher for students aged 13 to 14',
|
||||
'نهم': 'a specialist middle-school math teacher for students aged 14 to 15',
|
||||
'دهم': 'a specialist high-school math teacher for students aged 15 to 16',
|
||||
'یازدهم': 'a specialist high-school math teacher for students aged 16 to 17',
|
||||
'دوازدهم': 'a specialist high-school math teacher for students aged 17 to 18, '
|
||||
'preparing them for the university entrance exam',
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +107,53 @@ def teacher_persona(grade: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
# بکاسلشهایی که همیشه escapeِ واقعیاند و نباید دست بخورند.
|
||||
_JSON_SAFE_ESCAPES = set('"\\/u')
|
||||
# اینها هم escapeِ معتبرِ JSONاند و هم آغازِ دستورهای پرکاربردِ LaTeX:
|
||||
# \frac \times \theta \beta \rightarrow \nabla. تفکیک از روی نویسهی بعدی.
|
||||
_JSON_AMBIGUOUS_ESCAPES = set('bfnrt')
|
||||
|
||||
|
||||
def repair_json_escapes(raw: str) -> str:
|
||||
"""
|
||||
بکاسلشِ نامعتبر در رشتههای JSON را دوبرابر میکند تا قابلِ تجزیه شود.
|
||||
|
||||
مدل در متنِ ریاضی LaTeX تولید میکند و `\sqrt{2}` از نظرِ JSON خطاست، پس
|
||||
json.loads کلِ صفحه را دور میریخت — دقیقاً صفحههای پرفرمول، یعنی
|
||||
باارزشترینها.
|
||||
|
||||
⚠️ نکتهای که نسخهی اولِ همین تابع را غلط کرد: `\f`، `\t`، `\b`، `\r` و
|
||||
`\n` از نظرِ JSON معتبرند، پس «سالم» رد میشدند و `\frac` به formfeed+«rac»
|
||||
و `\theta` به tab+«heta» تبدیل میشد — خرابیِ بیصدا بهجای خطای بلند.
|
||||
قاعدهی تفکیک: دستورِ LaTeX همیشه با حرفِ ASCII ادامه پیدا میکند
|
||||
(`\frac`, `\times`)، ولی escapeِ واقعی با رقم، فاصله، فارسی یا پایانِ
|
||||
رشته میآید (`\n۲.`، `\nحالت`). پس فقط وقتی control character حساب
|
||||
میشود که نویسهی بعدی حرفِ ASCII نباشد.
|
||||
"""
|
||||
out: list[str] = []
|
||||
i, n = 0, len(raw)
|
||||
while i < n:
|
||||
ch = raw[i]
|
||||
if ch != '\\':
|
||||
out.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
nxt = raw[i + 1] if i + 1 < n else ''
|
||||
after = raw[i + 2] if i + 2 < n else ''
|
||||
looks_like_latex = after.isascii() and after.isalpha()
|
||||
keep = nxt in _JSON_SAFE_ESCAPES or (
|
||||
nxt in _JSON_AMBIGUOUS_ESCAPES and not looks_like_latex
|
||||
)
|
||||
if keep:
|
||||
out.append(ch)
|
||||
out.append(nxt)
|
||||
i += 2
|
||||
else:
|
||||
out.append('\\\\')
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def extract_problem_types(image, grade: str, chapter_name: str) -> dict:
|
||||
"""
|
||||
یک عکسِ صفحهی کتاب → ساختارِ JSONِ انواعِ مسئله (فارسی)، با مدلِ vision.
|
||||
@@ -159,7 +214,11 @@ Rules:
|
||||
{'type': 'image_url', 'image_url': {'url': f'data:image/jpeg;base64,{img_b64}', 'detail': 'high'}},
|
||||
]},
|
||||
],
|
||||
max_tokens=3000,
|
||||
# ۳۰۰۰ برای صفحهی متراکم کم است: خروجی وسطِ JSON قطع میشود،
|
||||
# json.loads خطا میدهد و کلِ صفحه در failed_pages میافتد. قاعدهی
|
||||
# self-contained هم خروجی را بلندتر کرده (توصیفِ شکل داخلِ متن)، پس
|
||||
# سقف بالا رفت. هزینهی واقعی به توکنِ تولیدشده است نه به این سقف.
|
||||
max_tokens=6000,
|
||||
temperature=0.1,
|
||||
)
|
||||
raw = (response.choices[0].message.content or '').strip()
|
||||
@@ -170,7 +229,14 @@ Rules:
|
||||
if raw.endswith('```'):
|
||||
raw = raw[:-3].strip()
|
||||
|
||||
parsed = json.loads(raw)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
# مدل در متنِ ریاضی LaTeX مینویسد (\frac، \sqrt، \vec) و بکاسلشِ تنها
|
||||
# در JSON نامعتبر است، پس json.loads کلِ صفحه را دور میریخت. این دقیقاً
|
||||
# صفحههای پرفرمول — باارزشترینها — را بیسروصدا حذف میکرد؛ روی حسابان
|
||||
# یازدهم/دوازدهم چهار صفحه هرچه تلاش شد «خطادار» ماندند تا همینجا پیدا شد.
|
||||
parsed = json.loads(repair_json_escapes(raw))
|
||||
# مصرفِ واقعیِ همین فراخوانی را همراهِ نتیجه برمیگردانیم تا ingest بتواند
|
||||
# هزینهی واقعی را گزارش کند. تا پیش از این فقط تخمینِ سرانگشتی داشتیم و
|
||||
# آن تخمین ۲.۳ برابرِ واقعیت بود.
|
||||
|
||||
Reference in New Issue
Block a user