atz21 commited on
Commit
332de5f
Β·
verified Β·
1 Parent(s): e696600

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -160
app.py CHANGED
@@ -163,55 +163,7 @@ def create_model():
163
  return model
164
  except Exception as e:
165
  print("❌ Failed to create any Gemini model:", e)
166
- raise
167
-
168
- def wait_for_file_active(uploaded_file, timeout=300):
169
- """
170
- Wait for an uploaded file to become ACTIVE before proceeding.
171
- Polls the file status every 2 seconds until ACTIVE or timeout.
172
- """
173
- import time
174
- start_time = time.time()
175
-
176
- while time.time() - start_time < timeout:
177
- try:
178
- file_info = genai.get_file(uploaded_file.name)
179
- state = file_info.state.name
180
- print(f"πŸ“Š File {uploaded_file.name} status: {state}")
181
-
182
- if state == "ACTIVE":
183
- print(f"βœ… File {uploaded_file.name} is now ACTIVE and ready to use")
184
- return True
185
- elif state == "FAILED":
186
- print(f"❌ File {uploaded_file.name} processing failed")
187
- return False
188
-
189
- # Still processing, wait a bit
190
- print(f"⏳ File still processing, waiting 2 seconds...")
191
- time.sleep(2)
192
-
193
- except Exception as e:
194
- print(f"❌ Error checking file status: {e}")
195
- time.sleep(2)
196
-
197
- print(f"⏰ Timeout waiting for file {uploaded_file.name} to become active")
198
- return False
199
-
200
- def validate_uploaded_file(uploaded_file):
201
- """
202
- Validate that an uploaded file is accessible and hasn't expired.
203
- """
204
- try:
205
- # Try to access the file info to check if it's valid
206
- file_info = genai.get_file(uploaded_file.name)
207
- if file_info.state.name == "ACTIVE":
208
- return True
209
- else:
210
- print(f"⚠️ File {uploaded_file.name} is not active (state: {file_info.state.name})")
211
- return False
212
- except Exception as e:
213
- print(f"❌ File validation failed: {e}")
214
- return False
215
 
216
  def merge_pdfs(paths, output_path):
217
  writer = PdfWriter()
@@ -386,76 +338,71 @@ def extract_marks_from_grading(grading_text):
386
  return grading_json
387
 
388
  # ---------------- MAPPING/IMPRINT HELPERS ----------------
389
- def ask_gemini_for_mapping_batch(model, image_paths, grading_json, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
390
- """
391
- Send multiple page images together to Gemini for batch mapping processing.
392
- More efficient than sending one by one.
393
- """
394
- ids_block = "{NA}"
395
- if expected_ids:
396
- ids_block = "{\n" + "\n".join(expected_ids) + "\n}"
397
-
398
- prompt = f"""You are an exam marker. Your role is to identify where each question begins on each page.
399
- The pages are divided into a {rows} x {cols} grid. Each cell has a RUNNING NUMBER label.
 
400
  For each question in the grading JSON, return the cell NUMBER where the FIRST STEP of that question begins.
401
-
402
- ⚠ IMPORTANT RULES:
 
403
  - Do not place marks inside another question's answer area.
404
- - 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.
405
- - Never place marks above or below the answer.
406
- - Each question should have unique cell number
407
  - If a question serial number is visible in the answer image, you must mandatorily identify the corresponding question using the grading JSON.
408
-
409
- IMPORTANT: For your help i have provided u questions that u can expect in the images:
410
- {ids_block}
411
-
412
  Return JSON only, like:
413
- [{{"page": 1, "question": "1(a)", "cell_number": 15}}, ...]
414
-
415
  Grading JSON:
416
- {json.dumps(grading_json, indent=2)}"""
417
-
418
- # Load all images
419
- images = [Image.open(p) for p in image_paths]
420
-
421
- print(f"πŸ“‘ Sending batch mapping request for {len(image_paths)} pages to Gemini...")
422
- response = model.generate_content([prompt, *images])
423
-
424
- raw_text = getattr(response, "text", None)
425
- if not raw_text and getattr(response, "candidates", None):
426
- raw_text = response.candidates[0].content.parts[0].text
427
- if not raw_text:
428
- raw_text = str(response)
429
-
430
- print("πŸ“₯ Batch mapping response (chars):", len(raw_text))
431
- print("πŸ”Ž Gemini raw batch output:")
432
- print(raw_text)
433
-
434
- # Try to extract JSON from response
435
- try:
436
- match = re.search(r'(\[.*\])', raw_text, re.DOTALL)
437
- if match:
438
- mapping = json.loads(match.group(1))
439
- print(f"βœ… Parsed Gemini batch mapping for {len(image_paths)} pages")
440
- return mapping
441
- else:
442
- print("❌ Failed to find JSON array in response")
443
- return []
444
- except Exception as e:
445
- print(f"❌ Failed to parse Gemini JSON mapping: {e}")
446
  return []
447
 
448
  def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expected_ids=None, rows=GRID_ROWS, cols=GRID_COLS):
449
  """
450
- Convert PDF to images, create grid-numbered images for batch sending to Gemini,
451
- then annotate and produce imprinted PDF using batch processing for better efficiency.
452
  """
453
  print("πŸ“„ Converting answer PDF to images for imprinting...")
454
  pages = convert_from_path(pdf_path, dpi=200)
455
  annotated_page_paths = []
456
  temp_grid_images = []
457
 
458
- # Create grid images for Gemini
459
  for p_index, page in enumerate(pages):
460
  img = page.convert("RGB")
461
  w, h = img.size
@@ -463,7 +410,7 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
463
 
464
  draw = ImageDraw.Draw(img)
465
  try:
466
- num_font = ImageFont.truetype("arial.ttf", 20)
467
  except Exception:
468
  num_font = ImageFont.load_default()
469
 
@@ -484,31 +431,34 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
484
  temp_grid_images.append(temp_path)
485
  print("πŸ›° Created grid image:", temp_path)
486
 
487
- # Send pages in batches to Gemini for mapping
488
- print("πŸ“‘ Sending page images to Gemini in batches for mapping...")
489
- batch_size = 10 # Process 10 pages at a time
490
- all_mappings = []
491
-
492
- for start in range(0, len(temp_grid_images), batch_size):
493
- batch_paths = temp_grid_images[start:start+batch_size]
494
- batch_mapping = ask_gemini_for_mapping_batch(model, batch_paths, grading_json, expected_ids, rows, cols)
495
- all_mappings.extend(batch_mapping)
496
- print(f"βœ… Processed batch {start//batch_size + 1}: pages {start+1}-{start+len(batch_paths)}")
 
 
 
 
 
497
 
498
  # Annotate original pages according to returned mappings
499
  print("πŸ–Š Annotating pages with marks...")
500
  for p_index, page in enumerate(pages):
501
- page_num = p_index + 1
502
  page_img = page.convert("RGB")
503
  img_cv = np.array(page_img)
504
  img_cv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2BGR)
505
  h, w, _ = img_cv.shape
506
  cell_w_px, cell_h_px = w / cols, h / rows
507
 
508
- # Filter mappings for this page
509
- page_mappings = [m for m in all_mappings if m.get("page") == page_num]
510
-
511
- for item in page_mappings:
512
  qid = item.get("question")
513
  cell_number = item.get("cell_number")
514
  if qid is None or cell_number is None:
@@ -524,24 +474,37 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
524
  row = (cell_number - 1) // cols
525
  col = (cell_number - 1) % cols
526
 
527
- # Position marks to the right of the answer, with fallback to left
528
- x_c = int((col + 1) * cell_w_px - cell_w_px / 4)
529
- y_c = int((row + 0.5) * cell_h_px)
530
-
531
- # Use larger, more visible font
532
- font_scale = max(1.0, min(2.0, cell_h_px / 40.0))
533
- thickness = max(2, int(font_scale * 2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  cv2.putText(img_cv, marks_text, (x_c, y_c), cv2.FONT_HERSHEY_SIMPLEX,
535
  font_scale, (0, 0, 255), thickness, cv2.LINE_AA)
536
- print(f"πŸ–Š Marks annotated for page {page_num}, question {qid}: {marks_text}")
537
 
538
- annotated_path = f"annotated_page_{page_num}.png"
539
  cv2.imwrite(annotated_path, img_cv)
540
  annotated_page_paths.append(annotated_path)
541
  print("βœ… Annotated page saved:", annotated_path)
542
 
543
- # Merge annotated pages into final PDF
544
- print("πŸ“‘ Merging annotated pages into final PDF...")
545
  with open(output_pdf, "wb") as f:
546
  f.write(img2pdf.convert(annotated_page_paths))
547
 
@@ -590,36 +553,11 @@ def align_and_grade_pipeline(qp_path, ms_path, ans_path, imprint=False):
590
  merge_pdfs([qp_path, ms_path], merged_qpms_path)
591
  print("πŸ“Ž Merged QP + MS ->", merged_qpms_path)
592
 
593
- # Upload files to Gemini with error handling and validation
594
  print("πŸ”Ό Uploading files to Gemini...")
595
- try:
596
- merged_uploaded = genai.upload_file(path=merged_qpms_path, display_name="QP+MS (merged)")
597
- print("πŸ“€ QP+MS file uploaded, waiting for processing...")
598
- if not wait_for_file_active(merged_uploaded):
599
- raise Exception("QP+MS file failed to become active")
600
-
601
- ans_uploaded = genai.upload_file(path=ans_path, display_name="Answer Sheet")
602
- print("πŸ“€ Answer Sheet uploaded, waiting for processing...")
603
- if not wait_for_file_active(ans_uploaded):
604
- raise Exception("Answer Sheet file failed to become active")
605
-
606
- print("βœ… All files uploaded and active.")
607
- except Exception as e:
608
- print(f"❌ File upload/processing failed: {e}")
609
- print("πŸ”„ Retrying file upload...")
610
- try:
611
- merged_uploaded = genai.upload_file(path=merged_qpms_path, display_name="QP+MS (merged)")
612
- if not wait_for_file_active(merged_uploaded):
613
- raise Exception("QP+MS retry failed to become active")
614
-
615
- ans_uploaded = genai.upload_file(path=ans_path, display_name="Answer Sheet")
616
- if not wait_for_file_active(ans_uploaded):
617
- raise Exception("Answer Sheet retry failed to become active")
618
-
619
- print("βœ… Retry upload successful.")
620
- except Exception as retry_e:
621
- print(f"❌ Retry failed: {retry_e}")
622
- return f"❌ File upload error: {retry_e}", None, None, None, None
623
 
624
  # Create model and print which selected
625
  model = create_model()
 
163
  return model
164
  except Exception as e:
165
  print("❌ Failed to create any Gemini model:", e)
166
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  def merge_pdfs(paths, output_path):
169
  writer = PdfWriter()
 
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
 
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
  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
  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
 
 
553
  merge_pdfs([qp_path, ms_path], merged_qpms_path)
554
  print("πŸ“Ž Merged QP + MS ->", merged_qpms_path)
555
 
556
+ # Upload files to Gemini
557
  print("πŸ”Ό Uploading files to Gemini...")
558
+ merged_uploaded = genai.upload_file(path=merged_qpms_path, display_name="QP+MS (merged)")
559
+ ans_uploaded = genai.upload_file(path=ans_path, display_name="Answer Sheet")
560
+ print("βœ… Upload complete.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
  # Create model and print which selected
563
  model = create_model()