Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
| 342 |
-
"""
|
| 343 |
-
Send
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 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
|
| 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 |
-
-
|
| 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 |
-
-
|
|
|
|
|
|
|
|
|
|
| 362 |
Return JSON only, like:
|
| 363 |
-
[{{"question": "1
|
| 364 |
Grading JSON:
|
| 365 |
-
{json.dumps(grading_json, indent=2)}
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
print("
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
if not raw_text
|
| 375 |
-
raw_text =
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
match = re.search(r'(\[.*\])', raw_text, re.DOTALL)
|
| 386 |
-
if match:
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
| 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 |
-
|
| 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",
|
| 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
|
| 435 |
-
print("π‘ Sending
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 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 |
-
|
| 460 |
-
|
| 461 |
-
|
|
|
|
| 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 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 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_{
|
| 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="
|
| 653 |
-
gr.Markdown("## π
|
| 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 |
-
|
| 665 |
-
|
| 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
|
| 681 |
|
| 682 |
run_button.click(
|
| 683 |
fn=run_pipeline,
|
| 684 |
inputs=[qp_file, ms_file, ans_file, imprint_toggle],
|
| 685 |
-
outputs=[
|
| 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__":
|