atz21 commited on
Commit
a85f94d
Β·
verified Β·
1 Parent(s): e0bef2f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +84 -103
app.py CHANGED
@@ -338,71 +338,72 @@ def extract_marks_from_grading(grading_text):
338
  return grading_json
339
 
340
  # ---------------- MAPPING/IMPRINT HELPERS ----------------
341
- def ask_gemini_for_mapping_for_page(model, image_path, grading_json, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
342
- """
343
- Send a single page image along with the grading_json and expected_ids; LLM should return JSON mapping.
344
- """
345
- ids_block = "{NA}"
346
- if expected_ids:
347
- ids_block = "{\n" + "\n".join(expected_ids) + "\n}"
348
- prompt = f"""
349
- You are an exam marker. Your task is to locate a blank cell adjacent to the answer step and place the marks there:
350
- Primary preference: Use the blank cell immediately to the right of the answer step.
351
- Fallback: If no blank cell is available on the right, use the blank cell immediately to the left..
352
- The page is divided into a {rows} x {cols} grid. Each cell has a RUNNING NUMBER label (1..{rows*cols}).
353
  For each question in the grading JSON, return the cell NUMBER where the FIRST STEP of that question begins.
354
- IMPORTANT: For your help i have provided u questions that u can expect in the image :
355
- {ids_block}
356
- If you see a sub-question (e.g., ii) above a main question (e.g., Q4), infer it belongs to the previous question (e.g., Q3.ii).
357
  - Do not place marks inside another question's answer area.
358
- - Each question should have unique cell number
359
- - If a question serial number is visible in the answer image, you must mandatorily identify the corresponding question using the grading JSON.
360
  - Never place marks above or below the answer.
361
- - Only if there is no serial number u may omit to select cell number for mark placement
 
 
 
362
  Return JSON only, like:
363
- [{{"question": "1.a", "cell_number": 15}}, ...]
364
  Grading JSON:
365
- {json.dumps(grading_json, indent=2)}
366
- """
367
- print(f"πŸ“‘ Sending mapping request for image {image_path} to Gemini...")
368
- img = Image.open(image_path)
369
- response = model.generate_content([prompt, img])
370
- print("πŸ’¬ Gemini response:", response)
371
- raw_text = getattr(response, "text", None)
372
- if not raw_text and getattr(response, "candidates", None):
373
- raw_text = response.candidates[0].content.parts[0].text
374
- if not raw_text:
375
- raw_text = str(response)
376
- print("πŸ“₯ Mapping response (chars):", len(raw_text))
377
- try:
378
- start = raw_text.index('[')
379
- end = raw_text.rindex(']') + 1
380
- json_part = raw_text[start:end]
381
- mapping = json.loads(json_part)
382
- print("βœ… Parsed mapping JSON for", image_path, "| entries:", len(mapping))
383
- return mapping
384
- except Exception:
385
- match = re.search(r'(\[.*\])', raw_text, re.DOTALL)
386
- if match:
387
- try:
388
- mapping = json.loads(match.group(1))
389
- print("βœ… Parsed mapping JSON (alt) for", image_path, "| entries:", len(mapping))
390
- return mapping
391
- except Exception:
392
- pass
393
- print("⚠️ Failed to parse mapping JSON for", image_path)
 
394
  return []
395
 
396
  def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
397
  """
398
- Convert PDF to images, create grid-numbered images for sending to Gemini,
399
- send all page images in parallel to Gemini for mapping, then annotate and produce imprinted PDF.
400
  """
401
  print("πŸ“„ Converting answer PDF to images for imprinting...")
402
  pages = convert_from_path(pdf_path, dpi=200)
403
  annotated_page_paths = []
404
  temp_grid_images = []
405
 
 
406
  for p_index, page in enumerate(pages):
407
  img = page.convert("RGB")
408
  w, h = img.size
@@ -410,7 +411,7 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
410
 
411
  draw = ImageDraw.Draw(img)
412
  try:
413
- num_font = ImageFont.truetype("arial.ttf", 16)
414
  except Exception:
415
  num_font = ImageFont.load_default()
416
 
@@ -431,34 +432,31 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
431
  temp_grid_images.append(temp_path)
432
  print("πŸ›° Created grid image:", temp_path)
433
 
434
- # Send all grid images in parallel to Gemini to get mappings
435
- print("πŸ“‘ Sending all page images to Gemini in parallel for mapping...")
436
- mappings_per_page = {}
437
- model_local = model
438
- with ThreadPoolExecutor(max_workers=min(8, len(temp_grid_images))) as ex:
439
- futures = {ex.submit(ask_gemini_for_mapping_for_page, model_local, img_path, grading_json, expected_ids, rows, cols): idx
440
- for idx, img_path in enumerate(temp_grid_images)}
441
- for fut in as_completed(futures):
442
- idx = futures[fut]
443
- try:
444
- mapping = fut.result()
445
- except Exception as e:
446
- print("⚠️ Mapping request failed for page", idx, e)
447
- mapping = []
448
- mappings_per_page[idx] = mapping
449
 
450
  # Annotate original pages according to returned mappings
451
  print("πŸ–Š Annotating pages with marks...")
452
  for p_index, page in enumerate(pages):
 
453
  page_img = page.convert("RGB")
454
  img_cv = np.array(page_img)
455
  img_cv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2BGR)
456
  h, w, _ = img_cv.shape
457
  cell_w_px, cell_h_px = w / cols, h / rows
458
 
459
- mapping = mappings_per_page.get(p_index, [])
460
- occupied = set()
461
- for item in mapping:
 
462
  qid = item.get("question")
463
  cell_number = item.get("cell_number")
464
  if qid is None or cell_number is None:
@@ -474,37 +472,24 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
474
  row = (cell_number - 1) // cols
475
  col = (cell_number - 1) % cols
476
 
477
- candidates = []
478
- if col + 1 < cols:
479
- candidates.append((row, col + 1))
480
- candidates.append((row, col))
481
- if col - 1 >= 0:
482
- candidates.append((row, col - 1))
483
-
484
- chosen = None
485
- for (r_c, c_c) in candidates:
486
- cell_id = r_c * cols + c_c + 1
487
- if cell_id not in occupied:
488
- chosen = (r_c, c_c)
489
- occupied.add(cell_id)
490
- break
491
- if chosen is None:
492
- chosen = (row, col)
493
-
494
- r_c, c_c = chosen
495
- x_c = int((c_c + 1) * cell_w_px - cell_w_px * 0.1)
496
- y_c = int((r_c + 0.5) * cell_h_px)
497
-
498
- font_scale = max(0.6, min(1.6, cell_h_px / 60.0))
499
- thickness = max(1, int(font_scale * 2))
500
  cv2.putText(img_cv, marks_text, (x_c, y_c), cv2.FONT_HERSHEY_SIMPLEX,
501
  font_scale, (0, 0, 255), thickness, cv2.LINE_AA)
 
502
 
503
- annotated_path = f"annotated_page_{p_index+1}.png"
504
  cv2.imwrite(annotated_path, img_cv)
505
  annotated_page_paths.append(annotated_path)
506
  print("βœ… Annotated page saved:", annotated_path)
507
 
 
 
508
  with open(output_pdf, "wb") as f:
509
  f.write(img2pdf.convert(annotated_page_paths))
510
 
@@ -649,8 +634,8 @@ def align_and_grade_pipeline(qp_path, ms_path, ans_path, imprint=False):
649
  return f"❌ Error: {e}", None, None, None, None
650
 
651
  # ---------------- GRADIO UI ----------------
652
- with gr.Blocks(title="LeadIB AI Grading (Final Flow β€” Verbose)") as demo:
653
- gr.Markdown("## πŸ“˜ LeadIB AI Grading β€” Final Flow\nUpload **Question Paper**, **Markscheme**, and **Student Answer Sheet**.\nFlow: merge QP+MS -> transcribe QP+MS (questions first, full markscheme) -> extract IDs -> transcribe AS with expected IDs -> grade -> (optional) imprint. Console prints show progress.")
654
 
655
  with gr.Row():
656
  qp_file = gr.File(label="πŸ“„ Upload Question Paper (PDF)")
@@ -661,12 +646,8 @@ with gr.Blocks(title="LeadIB AI Grading (Final Flow β€” Verbose)") as demo:
661
  run_button = gr.Button("πŸš€ Run Pipeline")
662
 
663
  with gr.Row():
664
- qpms_box = gr.Textbox(label="πŸ“‘ QP+MS Transcript", lines=12)
665
- as_box = gr.Textbox(label="πŸ“ AS Transcript", lines=12)
666
-
667
- grading_output_box = gr.Textbox(label="🧾 Grading (Markdown)", lines=20)
668
- grading_pdf_file = gr.File(label="πŸ“₯ Download Grading PDF")
669
- imprint_pdf_file = gr.File(label="πŸ“₯ Download Imprinted PDF (Optional)")
670
 
671
  def run_pipeline(qp_file_obj, ms_file_obj, ans_file_obj, imprint_flag):
672
  qp_path = qp_file_obj.name
@@ -677,12 +658,12 @@ with gr.Blocks(title="LeadIB AI Grading (Final Flow β€” Verbose)") as demo:
677
  qp_path, ms_path, ans_path, imprint=imprint_flag
678
  )
679
 
680
- return qpms_text or "", as_text or "", grading_text or "", grading_pdf_path, imprinted_pdf_path
681
 
682
  run_button.click(
683
  fn=run_pipeline,
684
  inputs=[qp_file, ms_file, ans_file, imprint_toggle],
685
- outputs=[qpms_box, as_box, grading_output_box, grading_pdf_file, imprint_pdf_file]
686
  )
687
 
688
  if __name__ == "__main__":
 
338
  return grading_json
339
 
340
  # ---------------- MAPPING/IMPRINT HELPERS ----------------
341
+ def ask_gemini_for_mapping_batch(model, image_paths, grading_json, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
342
+ """
343
+ Send multiple page images together to Gemini for batch mapping processing.
344
+ More efficient than sending one by one.
345
+ """
346
+ ids_block = "{NA}"
347
+ if expected_ids:
348
+ ids_block = "{\n" + "\n".join(expected_ids) + "\n}"
349
+
350
+ prompt = f"""You are an exam marker. Your role is to identify where each question begins on each page.
351
+ The pages are divided into a {rows} x {cols} grid. Each cell has a RUNNING NUMBER label.
 
352
  For each question in the grading JSON, return the cell NUMBER where the FIRST STEP of that question begins.
353
+ ⚠ IMPORTANT RULES:
 
 
354
  - Do not place marks inside another question's answer area.
355
+ - Prefer placing the marks in a BLANK cell immediately to the RIGHT of the answer step. If no blank cell is available to the right, then place in a blank cell to the LEFT.
 
356
  - Never place marks above or below the answer.
357
+ - Each question should have unique cell number
358
+ - If a question serial number is visible in the answer image, you must mandatorily identify the corresponding question using the grading JSON.
359
+ IMPORTANT: For your help i have provided u questions that u can expect in the images:
360
+ {ids_block}
361
  Return JSON only, like:
362
+ [{{"page": 1, "question": "1(a)", "cell_number": 15}}, ...]
363
  Grading JSON:
364
+ {json.dumps(grading_json, indent=2)}"""
365
+
366
+ # Load all images
367
+ images = [Image.open(p) for p in image_paths]
368
+
369
+ print(f"πŸ“‘ Sending batch mapping request for {len(image_paths)} pages to Gemini...")
370
+ response = model.generate_content([prompt, *images])
371
+
372
+ raw_text = getattr(response, "text", None)
373
+ if not raw_text and getattr(response, "candidates", None):
374
+ raw_text = response.candidates[0].content.parts[0].text
375
+ if not raw_text:
376
+ raw_text = str(response)
377
+
378
+ print("πŸ“₯ Batch mapping response (chars):", len(raw_text))
379
+ print("πŸ”Ž Gemini raw batch output:")
380
+ print(raw_text)
381
+
382
+ # Try to extract JSON from response
383
+ try:
384
+ match = re.search(r'(\[.*\])', raw_text, re.DOTALL)
385
+ if match:
386
+ mapping = json.loads(match.group(1))
387
+ print(f"βœ… Parsed Gemini batch mapping for {len(image_paths)} pages")
388
+ return mapping
389
+ else:
390
+ print("❌ Failed to find JSON array in response")
391
+ return []
392
+ except Exception as e:
393
+ print(f"❌ Failed to parse Gemini JSON mapping: {e}")
394
  return []
395
 
396
  def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
397
  """
398
+ Convert PDF to images, create grid-numbered images for batch sending to Gemini,
399
+ then annotate and produce imprinted PDF using batch processing for better efficiency.
400
  """
401
  print("πŸ“„ Converting answer PDF to images for imprinting...")
402
  pages = convert_from_path(pdf_path, dpi=200)
403
  annotated_page_paths = []
404
  temp_grid_images = []
405
 
406
+ # Create grid images for Gemini
407
  for p_index, page in enumerate(pages):
408
  img = page.convert("RGB")
409
  w, h = img.size
 
411
 
412
  draw = ImageDraw.Draw(img)
413
  try:
414
+ num_font = ImageFont.truetype("arial.ttf", 20)
415
  except Exception:
416
  num_font = ImageFont.load_default()
417
 
 
432
  temp_grid_images.append(temp_path)
433
  print("πŸ›° Created grid image:", temp_path)
434
 
435
+ # Send pages in batches to Gemini for mapping
436
+ print("πŸ“‘ Sending page images to Gemini in batches for mapping...")
437
+ batch_size = 10 # Process 10 pages at a time
438
+ all_mappings = []
439
+
440
+ for start in range(0, len(temp_grid_images), batch_size):
441
+ batch_paths = temp_grid_images[start:start+batch_size]
442
+ batch_mapping = ask_gemini_for_mapping_batch(model, batch_paths, grading_json, expected_ids, rows, cols)
443
+ all_mappings.extend(batch_mapping)
444
+ print(f"βœ… Processed batch {start//batch_size + 1}: pages {start+1}-{start+len(batch_paths)}")
 
 
 
 
 
445
 
446
  # Annotate original pages according to returned mappings
447
  print("πŸ–Š Annotating pages with marks...")
448
  for p_index, page in enumerate(pages):
449
+ page_num = p_index + 1
450
  page_img = page.convert("RGB")
451
  img_cv = np.array(page_img)
452
  img_cv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2BGR)
453
  h, w, _ = img_cv.shape
454
  cell_w_px, cell_h_px = w / cols, h / rows
455
 
456
+ # Filter mappings for this page
457
+ page_mappings = [m for m in all_mappings if m.get("page") == page_num]
458
+
459
+ for item in page_mappings:
460
  qid = item.get("question")
461
  cell_number = item.get("cell_number")
462
  if qid is None or cell_number is None:
 
472
  row = (cell_number - 1) // cols
473
  col = (cell_number - 1) % cols
474
 
475
+ # Position marks to the right of the answer, with fallback to left
476
+ x_c = int((col + 1) * cell_w_px - cell_w_px / 4)
477
+ y_c = int((row + 0.5) * cell_h_px)
478
+
479
+ # Use larger, more visible font
480
+ font_scale = max(1.0, min(2.0, cell_h_px / 40.0))
481
+ thickness = max(2, int(font_scale * 2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  cv2.putText(img_cv, marks_text, (x_c, y_c), cv2.FONT_HERSHEY_SIMPLEX,
483
  font_scale, (0, 0, 255), thickness, cv2.LINE_AA)
484
+ print(f"πŸ–Š Marks annotated for page {page_num}, question {qid}: {marks_text}")
485
 
486
+ annotated_path = f"annotated_page_{page_num}.png"
487
  cv2.imwrite(annotated_path, img_cv)
488
  annotated_page_paths.append(annotated_path)
489
  print("βœ… Annotated page saved:", annotated_path)
490
 
491
+ # Merge annotated pages into final PDF
492
+ print("πŸ“‘ Merging annotated pages into final PDF...")
493
  with open(output_pdf, "wb") as f:
494
  f.write(img2pdf.convert(annotated_page_paths))
495
 
 
634
  return f"❌ Error: {e}", None, None, None, None
635
 
636
  # ---------------- GRADIO UI ----------------
637
+ with gr.Blocks(title=" AI Grading (Final Flow )") as demo:
638
+ gr.Markdown("## πŸ“˜ AI Grading β€” Final Flow")
639
 
640
  with gr.Row():
641
  qp_file = gr.File(label="πŸ“„ Upload Question Paper (PDF)")
 
646
  run_button = gr.Button("πŸš€ Run Pipeline")
647
 
648
  with gr.Row():
649
+ grading_pdf_file = gr.File(label="πŸ“₯ Download Grading PDF")
650
+ imprint_pdf_file = gr.File(label="πŸ“₯ Download Imprinted PDF (Optional)")
 
 
 
 
651
 
652
  def run_pipeline(qp_file_obj, ms_file_obj, ans_file_obj, imprint_flag):
653
  qp_path = qp_file_obj.name
 
658
  qp_path, ms_path, ans_path, imprint=imprint_flag
659
  )
660
 
661
+ return grading_pdf_path, imprinted_pdf_path
662
 
663
  run_button.click(
664
  fn=run_pipeline,
665
  inputs=[qp_file, ms_file, ans_file, imprint_toggle],
666
+ outputs=[grading_pdf_file, imprint_pdf_file]
667
  )
668
 
669
  if __name__ == "__main__":