Leen172 commited on
Commit
254fb45
·
verified ·
1 Parent(s): 739131d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -34
app.py CHANGED
@@ -21,8 +21,6 @@ import regex as re2
21
  import yake
22
  from tqdm import tqdm
23
 
24
- # ملاحظة: سنستورد torch/transformers داخل الدوال (تحميل كسول) لسرعة الإقلاع.
25
-
26
  # =========================
27
  # إعدادات عامة
28
  # =========================
@@ -32,7 +30,7 @@ DEFAULT_NUM_QUESTIONS = 8
32
  DEFAULT_TROCR_MODEL = "microsoft/trocr-base-printed" # أسرع من large
33
  DEFAULT_TROCR_ZOOM = 2.8
34
 
35
- # كاش بسيط للـ OCR pipeline
36
  _OCR_PIPE = {}
37
  def _get_ocr_pipeline(model_id: str):
38
  """تحميل كسول + كاش لنموذج TrOCR."""
@@ -144,13 +142,15 @@ def normalize_arabic(text: str) -> str:
144
  text = re2.sub(r"[إأآا]", "ا", text)
145
  text = re2.sub(r"[يى]", "ي", text)
146
  text = re2.sub(r"\s+", " ", text)
 
 
 
147
  return text.strip()
148
 
149
  def arabic_ocr_fixes(text: str) -> str:
150
  fixes = {
151
  " الصطناعي": " الاصطناعي",
152
  "صطناعي": "اصطناعي",
153
- "التعل م": "التعلم",
154
  "الذكاء الاصطناعيي": "الذكاء الاصطناعي",
155
  "ذكاء صطناعي": "ذكاء اصطناعي",
156
  "الذكاء الاصطناعي.": "الذكاء الاصطناعي.",
@@ -158,6 +158,8 @@ def arabic_ocr_fixes(text: str) -> str:
158
  " مع غني": " غني",
159
  "مع غني ": " غني ",
160
  " غير المشبعة": " غيرُ المشبعة",
 
 
161
  }
162
  for wrong, right in fixes.items():
163
  text = text.replace(wrong, right)
@@ -218,7 +220,15 @@ def split_sentences(text: str) -> List[str]:
218
  sents = [s.strip() for s in SENT_SPLIT.split(text) if s.strip()]
219
  return [s for s in sents if len(s) >= 25]
220
 
 
 
 
 
 
 
221
  def build_distractors(correct: str, pool: List[str], k: int = 3) -> List[str]:
 
 
222
  cand = []
223
  for w in pool:
224
  if not w:
@@ -226,11 +236,13 @@ def build_distractors(correct: str, pool: List[str], k: int = 3) -> List[str]:
226
  w2 = w.strip()
227
  if w2 == correct.strip():
228
  continue
229
- if len(w2) < 3:
230
  continue
231
- if w2 in AR_STOP:
232
  continue
233
- cand.append(w2)
 
 
234
 
235
  random.shuffle(cand)
236
  out = []
@@ -239,7 +251,7 @@ def build_distractors(correct: str, pool: List[str], k: int = 3) -> List[str]:
239
  if len(out) == k:
240
  break
241
 
242
- fillers = ["—", "-", "—-"]
243
  while len(out) < k:
244
  out.append(random.choice(fillers))
245
  return out
@@ -261,6 +273,8 @@ def make_mcqs_from_text(text: str, n: int = 8, lang: str = 'ar') -> List[MCQ]:
261
  sent_for_kw = {}
262
  for s in sentences:
263
  for kw in keywords:
 
 
264
  if re2.search(rf"(?<!\p{{L}}){re2.escape(kw)}(?!\p{{L}})", s) and kw not in sent_for_kw:
265
  sent_for_kw[kw] = s
266
 
@@ -271,6 +285,8 @@ def make_mcqs_from_text(text: str, n: int = 8, lang: str = 'ar') -> List[MCQ]:
271
  for kw in pool_iter:
272
  if len(items) >= n:
273
  break
 
 
274
  s = sent_for_kw[kw]
275
  if s in used_sents:
276
  continue
@@ -323,7 +339,7 @@ def is_bad_choice(txt: str) -> bool:
323
  return True
324
  return False
325
 
326
- def build_json_records(items: List[MCQ], lang: str, source_pdf: str, method: str):
327
  json_data = []
328
  letters = ["A", "B", "C", "D"]
329
  for it in items:
@@ -353,14 +369,15 @@ def build_json_records(items: List[MCQ], lang: str, source_pdf: str, method: str
353
  "lang": lang,
354
  "normalized": True,
355
  "source_pdf": source_pdf,
356
- "extraction_method": method
 
357
  }
358
  }
359
  json_data.append(record)
360
  return json_data
361
 
362
  # =========================
363
- # 7) الدالة الرئيسية (تتعامل مع Filepath من Gradio)
364
  # =========================
365
  def process_pdf(pdf_file_path,
366
  num_questions=DEFAULT_NUM_QUESTIONS,
@@ -370,27 +387,37 @@ def process_pdf(pdf_file_path,
370
  logs = []
371
  try:
372
  if not pdf_file_path:
373
- return {}, None, "يرجى رفع ملف PDF أولاً."
374
 
375
  # pdf_file_path قد يكون str أو NamedString -> خذه كمسار
376
  src_path = str(pdf_file_path)
377
- # اسم ملف مناسب
378
  name_guess = getattr(pdf_file_path, "name", "") if hasattr(pdf_file_path, "name") else ""
379
- filename = Path(name_guess).name or Path(src_path).name or "input.pdf"
 
 
 
 
 
 
 
380
  if not Path(filename).suffix:
381
- filename += ".pdf"
382
 
383
- workdir = tempfile.mkdtemp(prefix="mcq_")
384
- pdf_path = os.path.join(workdir, filename)
385
- shutil.copy(src_path, pdf_path)
386
- logs.append(f"تم نسخ الملف إلى: {pdf_path}")
387
-
388
- # 1) استخراج النص
389
- raw_text, out_txt_path, method = pdf_to_txt(
390
- pdf_path=pdf_path,
391
- ocr_model=trocr_model,
392
- ocr_zoom=float(trocr_zoom)
393
- )
 
 
 
 
394
  logs.append(f"طريقة الاستخراج: {method}")
395
 
396
  # 2) تنظيف/تطبيع
@@ -403,7 +430,9 @@ def process_pdf(pdf_file_path,
403
  logs.append(f"تم توليد {len(items)} سؤالاً.")
404
 
405
  # 4) بناء JSON
406
- json_records = build_json_records(items, lang=lang, source_pdf=Path(filename).name, method=method)
 
 
407
  json_str = json.dumps(json_records, ensure_ascii=False, indent=2)
408
 
409
  # 5) حفظ ملف JSON للتنزيل
@@ -423,15 +452,15 @@ def process_pdf(pdf_file_path,
423
  # =========================
424
  import gradio as gr
425
 
426
- with gr.Blocks(title="PDF → MCQ JSON (Arabic YAKE / TrOCR)") as demo:
427
- gr.Markdown("## تحويل PDF إلى أسئلة اختيار من متعدد وإرجاع JSON جاهز للواجهة")
428
 
429
  with gr.Row():
430
  inp_pdf = gr.File(
431
- label="ارفع PDF",
432
  file_count="single",
433
- file_types=[".pdf"],
434
- type="filepath", # مهم: يُعيد مسار الملف
435
  )
436
  with gr.Column():
437
  num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
@@ -444,7 +473,7 @@ with gr.Blocks(title="PDF → MCQ JSON (Arabic YAKE / TrOCR)") as demo:
444
  "microsoft/trocr-large-handwritten",
445
  ],
446
  value=DEFAULT_TROCR_MODEL,
447
- label="موديل TrOCR"
448
  )
449
 
450
  btn = gr.Button("تشغيل المعالجة", variant="primary")
@@ -459,6 +488,5 @@ with gr.Blocks(title="PDF → MCQ JSON (Arabic YAKE / TrOCR)") as demo:
459
  )
460
 
461
  # ملاحظة: Spaces تتعرف تلقائياً على المتغير "demo".
462
- # لو شغّلت محلياً:
463
  if __name__ == "__main__":
464
  demo.queue().launch()
 
21
  import yake
22
  from tqdm import tqdm
23
 
 
 
24
  # =========================
25
  # إعدادات عامة
26
  # =========================
 
30
  DEFAULT_TROCR_MODEL = "microsoft/trocr-base-printed" # أسرع من large
31
  DEFAULT_TROCR_ZOOM = 2.8
32
 
33
+ # كاش بسيط للـ OCR pipeline (تحميل كسول)
34
  _OCR_PIPE = {}
35
  def _get_ocr_pipeline(model_id: str):
36
  """تحميل كسول + كاش لنموذج TrOCR."""
 
142
  text = re2.sub(r"[إأآا]", "ا", text)
143
  text = re2.sub(r"[يى]", "ي", text)
144
  text = re2.sub(r"\s+", " ", text)
145
+ # إزالة التكرار الزائد للحروف (مثل جذرياا -> جذريا)
146
+ text = re2.sub(r'(\p{L})\1{2,}', r'\1', text) # أكثر من مرتين
147
+ text = re2.sub(r'(\p{L})\1', r'\1', text) # التكرار المتبقي
148
  return text.strip()
149
 
150
  def arabic_ocr_fixes(text: str) -> str:
151
  fixes = {
152
  " الصطناعي": " الاصطناعي",
153
  "صطناعي": "اصطناعي",
 
154
  "الذكاء الاصطناعيي": "الذكاء الاصطناعي",
155
  "ذكاء صطناعي": "ذكاء اصطناعي",
156
  "الذكاء الاصطناعي.": "الذكاء الاصطناعي.",
 
158
  " مع غني": " غني",
159
  "مع غني ": " غني ",
160
  " غير المشبعة": " غيرُ المشبعة",
161
+ "الااصطناعي": "الاصطناعي",
162
+ "وشخصياا": "وشخصياً",
163
  }
164
  for wrong, right in fixes.items():
165
  text = text.replace(wrong, right)
 
220
  sents = [s.strip() for s in SENT_SPLIT.split(text) if s.strip()]
221
  return [s for s in sents if len(s) >= 25]
222
 
223
+ def _is_good_kw(kw: str) -> bool:
224
+ if not kw or len(kw) < 3: return False
225
+ if kw in AR_STOP: return False
226
+ if re2.match(r"^[\p{P}\p{S}\d_]+$", kw): return False
227
+ return True
228
+
229
  def build_distractors(correct: str, pool: List[str], k: int = 3) -> List[str]:
230
+ """ملهيات أقرب طولياً للسياق."""
231
+ target_len = len(correct.strip())
232
  cand = []
233
  for w in pool:
234
  if not w:
 
236
  w2 = w.strip()
237
  if w2 == correct.strip():
238
  continue
239
+ if len(w2) < 3 or w2 in AR_STOP:
240
  continue
241
+ if re2.match(r"^[\p{P}\p{S}\d_]+$", w2):
242
  continue
243
+ # تقارب طولي
244
+ if abs(len(w2) - target_len) <= 3:
245
+ cand.append(w2)
246
 
247
  random.shuffle(cand)
248
  out = []
 
251
  if len(out) == k:
252
  break
253
 
254
+ fillers = ["—", "— —", "—-"]
255
  while len(out) < k:
256
  out.append(random.choice(fillers))
257
  return out
 
273
  sent_for_kw = {}
274
  for s in sentences:
275
  for kw in keywords:
276
+ if not _is_good_kw(kw):
277
+ continue
278
  if re2.search(rf"(?<!\p{{L}}){re2.escape(kw)}(?!\p{{L}})", s) and kw not in sent_for_kw:
279
  sent_for_kw[kw] = s
280
 
 
285
  for kw in pool_iter:
286
  if len(items) >= n:
287
  break
288
+ if not _is_good_kw(kw):
289
+ continue
290
  s = sent_for_kw[kw]
291
  if s in used_sents:
292
  continue
 
339
  return True
340
  return False
341
 
342
+ def build_json_records(items: List[MCQ], lang: str, source_pdf: str, method: str, num_questions: int):
343
  json_data = []
344
  letters = ["A", "B", "C", "D"]
345
  for it in items:
 
369
  "lang": lang,
370
  "normalized": True,
371
  "source_pdf": source_pdf,
372
+ "extraction_method": method,
373
+ "num_questions": int(num_questions),
374
  }
375
  }
376
  json_data.append(record)
377
  return json_data
378
 
379
  # =========================
380
+ # 7) الدالة الرئيسية (دعم PDF و TXT)
381
  # =========================
382
  def process_pdf(pdf_file_path,
383
  num_questions=DEFAULT_NUM_QUESTIONS,
 
387
  logs = []
388
  try:
389
  if not pdf_file_path:
390
+ return {}, None, "يرجى رفع ملف PDF/TXT أولاً."
391
 
392
  # pdf_file_path قد يكون str أو NamedString -> خذه كمسار
393
  src_path = str(pdf_file_path)
 
394
  name_guess = getattr(pdf_file_path, "name", "") if hasattr(pdf_file_path, "name") else ""
395
+ filename = Path(name_guess).name or Path(src_path).name or "input"
396
+ workdir = tempfile.mkdtemp(prefix="mcq_")
397
+
398
+ # تأكد من الامتداد
399
+ ext = Path(filename).suffix.lower()
400
+ if ext not in [".pdf", ".txt"]:
401
+ # حاول تخمين نوعه، افتراض PDF
402
+ ext = ".pdf"
403
  if not Path(filename).suffix:
404
+ filename += ext
405
 
406
+ local_path = os.path.join(workdir, filename)
407
+ shutil.copy(src_path, local_path)
408
+ logs.append(f"تم نسخ الملف إلى: {local_path}")
409
+
410
+ # 1) استخراج النص بحسب النوع
411
+ if ext == ".txt":
412
+ with open(local_path, "r", encoding="utf-8", errors="ignore") as f:
413
+ raw_text = f.read()
414
+ method = "plain text (no PDF)"
415
+ else:
416
+ raw_text, out_txt_path, method = pdf_to_txt(
417
+ pdf_path=local_path,
418
+ ocr_model=trocr_model,
419
+ ocr_zoom=float(trocr_zoom)
420
+ )
421
  logs.append(f"طريقة الاستخراج: {method}")
422
 
423
  # 2) تنظيف/تطبيع
 
430
  logs.append(f"تم توليد {len(items)} سؤالاً.")
431
 
432
  # 4) بناء JSON
433
+ json_records = build_json_records(
434
+ items, lang=lang, source_pdf=Path(filename).name, method=method, num_questions=num_questions
435
+ )
436
  json_str = json.dumps(json_records, ensure_ascii=False, indent=2)
437
 
438
  # 5) حفظ ملف JSON للتنزيل
 
452
  # =========================
453
  import gradio as gr
454
 
455
+ with gr.Blocks(title="PDF/TXT → MCQ JSON (Arabic YAKE / TrOCR)") as demo:
456
+ gr.Markdown("## تحويل PDF/TXT إلى أسئلة اختيار من متعدد وإرجاع JSON جاهز للواجهة")
457
 
458
  with gr.Row():
459
  inp_pdf = gr.File(
460
+ label="ارفع PDF أو TXT",
461
  file_count="single",
462
+ file_types=[".pdf", ".txt"],
463
+ type="filepath", # يُعيد مسار الملف
464
  )
465
  with gr.Column():
466
  num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
 
473
  "microsoft/trocr-large-handwritten",
474
  ],
475
  value=DEFAULT_TROCR_MODEL,
476
+ label="موديل TrOCR (للـ PDF المصوّر)"
477
  )
478
 
479
  btn = gr.Button("تشغيل المعالجة", variant="primary")
 
488
  )
489
 
490
  # ملاحظة: Spaces تتعرف تلقائياً على المتغير "demo".
 
491
  if __name__ == "__main__":
492
  demo.queue().launch()