Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
| 390 |
-
"""
|
| 391 |
-
Send
|
| 392 |
-
|
| 393 |
-
""
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
| 400 |
For each question in the grading JSON, return the cell NUMBER where the FIRST STEP of that question begins.
|
| 401 |
-
|
| 402 |
-
|
|
|
|
| 403 |
- Do not place marks inside another question's answer area.
|
| 404 |
-
-
|
| 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 |
-
|
| 410 |
-
{ids_block}
|
| 411 |
-
|
| 412 |
Return JSON only, like:
|
| 413 |
-
[{{"
|
| 414 |
-
|
| 415 |
Grading JSON:
|
| 416 |
-
{json.dumps(grading_json, indent=2)}
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
print(
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
if not raw_text
|
| 426 |
-
raw_text = response
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
match = re.search(r'(\[.*\])', raw_text, re.DOTALL)
|
| 437 |
-
if match:
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 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
|
| 451 |
-
|
| 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",
|
| 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
|
| 488 |
-
print("π‘ Sending page images to Gemini in
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 509 |
-
|
| 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 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_{
|
| 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
|
| 594 |
print("πΌ Uploading files to Gemini...")
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 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()
|