atz21 commited on
Commit
01025e4
Β·
verified Β·
1 Parent(s): 27ea33f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +84 -95
app.py CHANGED
@@ -338,71 +338,76 @@ 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 +415,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 +436,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 +476,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 +638,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)")
 
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
+
354
+ ⚠ IMPORTANT RULES:
 
355
  - Do not place marks inside another question's answer area.
356
+ - 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.
 
357
  - Never place marks above or below the answer.
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
+
361
+ IMPORTANT: For your help i have provided u questions that u can expect in the images:
362
+ {ids_block}
363
+
364
  Return JSON only, like:
365
+ [{{"page": 1, "question": "1(a)", "cell_number": 15}}, ...]
366
+
367
  Grading JSON:
368
+ {json.dumps(grading_json, indent=2)}"""
369
+
370
+ # Load all images
371
+ images = [Image.open(p) for p in image_paths]
372
+
373
+ print(f"πŸ“‘ Sending batch mapping request for {len(image_paths)} pages to Gemini...")
374
+ response = model.generate_content([prompt, *images])
375
+
376
+ raw_text = getattr(response, "text", None)
377
+ if not raw_text and getattr(response, "candidates", None):
378
+ raw_text = response.candidates[0].content.parts[0].text
379
+ if not raw_text:
380
+ raw_text = str(response)
381
+
382
+ print("πŸ“₯ Batch mapping response (chars):", len(raw_text))
383
+ print("πŸ”Ž Gemini raw batch output:")
384
+ print(raw_text)
385
+
386
+ # Try to extract JSON from response
387
+ try:
388
+ match = re.search(r'(\[.*\])', raw_text, re.DOTALL)
389
+ if match:
390
+ mapping = json.loads(match.group(1))
391
+ print(f"βœ… Parsed Gemini batch mapping for {len(image_paths)} pages")
392
+ return mapping
393
+ else:
394
+ print("❌ Failed to find JSON array in response")
395
+ return []
396
+ except Exception as e:
397
+ print(f"❌ Failed to parse Gemini JSON mapping: {e}")
398
  return []
399
 
400
  def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
401
  """
402
+ Convert PDF to images, create grid-numbered images for batch sending to Gemini,
403
+ then annotate and produce imprinted PDF using batch processing for better efficiency.
404
  """
405
  print("πŸ“„ Converting answer PDF to images for imprinting...")
406
  pages = convert_from_path(pdf_path, dpi=200)
407
  annotated_page_paths = []
408
  temp_grid_images = []
409
 
410
+ # Create grid images for Gemini
411
  for p_index, page in enumerate(pages):
412
  img = page.convert("RGB")
413
  w, h = img.size
 
415
 
416
  draw = ImageDraw.Draw(img)
417
  try:
418
+ num_font = ImageFont.truetype("arial.ttf", 20)
419
  except Exception:
420
  num_font = ImageFont.load_default()
421
 
 
436
  temp_grid_images.append(temp_path)
437
  print("πŸ›° Created grid image:", temp_path)
438
 
439
+ # Send pages in batches to Gemini for mapping
440
+ print("πŸ“‘ Sending page images to Gemini in batches for mapping...")
441
+ batch_size = 10 # Process 10 pages at a time
442
+ all_mappings = []
443
+
444
+ for start in range(0, len(temp_grid_images), batch_size):
445
+ batch_paths = temp_grid_images[start:start+batch_size]
446
+ batch_mapping = ask_gemini_for_mapping_batch(model, batch_paths, grading_json, expected_ids, rows, cols)
447
+ all_mappings.extend(batch_mapping)
448
+ print(f"βœ… Processed batch {start//batch_size + 1}: pages {start+1}-{start+len(batch_paths)}")
 
 
 
 
 
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_num = p_index + 1
454
  page_img = page.convert("RGB")
455
  img_cv = np.array(page_img)
456
  img_cv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2BGR)
457
  h, w, _ = img_cv.shape
458
  cell_w_px, cell_h_px = w / cols, h / rows
459
 
460
+ # Filter mappings for this page
461
+ page_mappings = [m for m in all_mappings if m.get("page") == page_num]
462
+
463
+ for item in page_mappings:
464
  qid = item.get("question")
465
  cell_number = item.get("cell_number")
466
  if qid is None or cell_number is None:
 
476
  row = (cell_number - 1) // cols
477
  col = (cell_number - 1) % cols
478
 
479
+ # Position marks to the right of the answer, with fallback to left
480
+ x_c = int((col + 1) * cell_w_px - cell_w_px / 4)
481
+ y_c = int((row + 0.5) * cell_h_px)
482
+
483
+ # Use larger, more visible font
484
+ font_scale = max(1.0, min(2.0, cell_h_px / 40.0))
485
+ thickness = max(2, int(font_scale * 2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  cv2.putText(img_cv, marks_text, (x_c, y_c), cv2.FONT_HERSHEY_SIMPLEX,
487
  font_scale, (0, 0, 255), thickness, cv2.LINE_AA)
488
+ print(f"πŸ–Š Marks annotated for page {page_num}, question {qid}: {marks_text}")
489
 
490
+ annotated_path = f"annotated_page_{page_num}.png"
491
  cv2.imwrite(annotated_path, img_cv)
492
  annotated_page_paths.append(annotated_path)
493
  print("βœ… Annotated page saved:", annotated_path)
494
 
495
+ # Merge annotated pages into final PDF
496
+ print("πŸ“‘ Merging annotated pages into final PDF...")
497
  with open(output_pdf, "wb") as f:
498
  f.write(img2pdf.convert(annotated_page_paths))
499
 
 
638
  return f"❌ Error: {e}", None, None, None, None
639
 
640
  # ---------------- GRADIO UI ----------------
641
+ with gr.Blocks(title=" AI Grading (Final Flow )") as demo:
642
+ gr.Markdown("## πŸ“˜ AI Grading β€” Final Flow")
643
 
644
  with gr.Row():
645
  qp_file = gr.File(label="πŸ“„ Upload Question Paper (PDF)")