Update main.py
Browse files
main.py
CHANGED
@@ -183,101 +183,110 @@ def load_answer_key(pdf_bytes: bytes) -> dict:
|
|
183 |
# FastAPI Endpoints
|
184 |
##############################################################
|
185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
@app.post("/process")
|
187 |
async def process_pdfs(
|
188 |
-
student_pdf: UploadFile = File(
|
189 |
-
|
190 |
-
|
191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
):
|
193 |
try:
|
194 |
-
# Read
|
195 |
student_pdf_bytes = await student_pdf.read()
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
|
|
|
|
201 |
answer_keys = {
|
202 |
-
"A": load_answer_key(paper_a_bytes),
|
203 |
-
"B": load_answer_key(paper_b_bytes),
|
204 |
"K": load_answer_key(paper_k_bytes)
|
205 |
}
|
206 |
-
|
207 |
-
|
|
|
|
|
|
|
|
|
208 |
student_images = convert_from_bytes(student_pdf_bytes)
|
209 |
all_results = []
|
210 |
-
|
211 |
-
# Loop over all student pages
|
212 |
for idx, page in enumerate(student_images):
|
213 |
print(f"Processing student page {idx+1}...")
|
214 |
-
|
215 |
-
#
|
216 |
-
page_cv = np.array(page)
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
candidate_margin_top = int(height * 0.10)
|
225 |
-
candidate_margin_bottom = int(height * 0.75)
|
226 |
-
cv2.rectangle(candidate_mask, (0, candidate_margin_top), (width, height - candidate_margin_bottom), 255, -1)
|
227 |
-
masked_candidate = cv2.bitwise_and(page_cv, page_cv, mask=candidate_mask)
|
228 |
-
coords = cv2.findNonZero(candidate_mask)
|
229 |
if coords is None:
|
230 |
-
continue
|
231 |
-
x, y,
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
paper = ""
|
241 |
-
if candidate_info and "Candidate Info" in candidate_info:
|
242 |
-
paper = candidate_info["Candidate Info"].get("Paper", "").strip()
|
243 |
if not paper:
|
244 |
-
paper = parse_paper(
|
245 |
-
paper = paper.upper()
|
246 |
print(f"Student {idx+1} Paper: {paper}")
|
247 |
-
|
248 |
-
#
|
249 |
if paper not in answer_keys or answer_keys[paper] is None:
|
250 |
-
print(f"
|
251 |
continue
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
"Student Answers": student_answers,
|
270 |
-
"Correct Answer Key": correct_answer_key,
|
271 |
-
"Result": result
|
272 |
-
}
|
273 |
-
all_results.append(result_card)
|
274 |
-
|
275 |
-
# Write the results to a file in the temporary folder.
|
276 |
with open(RESULT_FILE, "w", encoding="utf-8") as f:
|
277 |
json.dump({"results": all_results}, f, indent=2)
|
278 |
-
|
279 |
return JSONResponse(content={"results": all_results})
|
280 |
-
|
281 |
except Exception as e:
|
282 |
raise HTTPException(status_code=500, detail=str(e))
|
283 |
|
|
|
183 |
# FastAPI Endpoints
|
184 |
##############################################################
|
185 |
|
186 |
+
from typing import Optional
|
187 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
188 |
+
from fastapi.responses import JSONResponse
|
189 |
+
import numpy as np
|
190 |
+
import cv2
|
191 |
+
import json
|
192 |
+
from PIL import Image
|
193 |
+
|
194 |
+
app = FastAPI()
|
195 |
+
|
196 |
@app.post("/process")
|
197 |
async def process_pdfs(
|
198 |
+
student_pdf: UploadFile = File(
|
199 |
+
...,
|
200 |
+
description="PDF with all student answer sheets (one page per student)"
|
201 |
+
),
|
202 |
+
paper_k_pdf: UploadFile = File(
|
203 |
+
...,
|
204 |
+
description="Answer key PDF for Paper K"
|
205 |
+
),
|
206 |
+
paper_a_pdf: Optional[UploadFile] = File(
|
207 |
+
None,
|
208 |
+
description="(Optional) Answer key PDF for Paper A"
|
209 |
+
),
|
210 |
+
paper_b_pdf: Optional[UploadFile] = File(
|
211 |
+
None,
|
212 |
+
description="(Optional) Answer key PDF for Paper B"
|
213 |
+
),
|
214 |
):
|
215 |
try:
|
216 |
+
# 1. Read the student and Paper K files (required)
|
217 |
student_pdf_bytes = await student_pdf.read()
|
218 |
+
paper_k_bytes = await paper_k_pdf.read()
|
219 |
+
|
220 |
+
# 2. Read optional answer keys if provided
|
221 |
+
paper_a_bytes = await paper_a_pdf.read() if paper_a_pdf else None
|
222 |
+
paper_b_bytes = await paper_b_pdf.read() if paper_b_pdf else None
|
223 |
+
|
224 |
+
# 3. Build the answer_keys dict dynamically
|
225 |
answer_keys = {
|
|
|
|
|
226 |
"K": load_answer_key(paper_k_bytes)
|
227 |
}
|
228 |
+
if paper_a_bytes is not None:
|
229 |
+
answer_keys["A"] = load_answer_key(paper_a_bytes)
|
230 |
+
if paper_b_bytes is not None:
|
231 |
+
answer_keys["B"] = load_answer_key(paper_b_bytes)
|
232 |
+
|
233 |
+
# 4. Convert the student PDF to images
|
234 |
student_images = convert_from_bytes(student_pdf_bytes)
|
235 |
all_results = []
|
236 |
+
|
|
|
237 |
for idx, page in enumerate(student_images):
|
238 |
print(f"Processing student page {idx+1}...")
|
239 |
+
|
240 |
+
# — Candidate Info Extraction (as before) —
|
241 |
+
page_cv = cv2.cvtColor(np.array(page), cv2.COLOR_RGB2BGR)
|
242 |
+
h, w = page_cv.shape[:2]
|
243 |
+
mask = np.zeros((h, w), dtype="uint8")
|
244 |
+
top = int(h * 0.10)
|
245 |
+
bottom = int(h * 0.75)
|
246 |
+
cv2.rectangle(mask, (0, top), (w, h - bottom), 255, -1)
|
247 |
+
masked = cv2.bitwise_and(page_cv, page_cv, mask=mask)
|
248 |
+
coords = cv2.findNonZero(mask)
|
|
|
|
|
|
|
|
|
|
|
249 |
if coords is None:
|
250 |
+
continue
|
251 |
+
x, y, mw, mh = cv2.boundingRect(coords)
|
252 |
+
cand_pil = Image.fromarray(
|
253 |
+
cv2.cvtColor(masked[y:y+mh, x:x+mw], cv2.COLOR_BGR2RGB)
|
254 |
+
)
|
255 |
+
info_resp = parse_info(cand_pil)
|
256 |
+
cand_info = extract_json_from_output(info_resp)
|
257 |
+
|
258 |
+
# Determine which paper this student sat
|
259 |
+
paper = cand_info.get("Candidate Info", {}).get("Paper", "").strip().upper()
|
|
|
|
|
|
|
260 |
if not paper:
|
261 |
+
paper = parse_paper(info_resp).upper()
|
|
|
262 |
print(f"Student {idx+1} Paper: {paper}")
|
263 |
+
|
264 |
+
# Skip if we don't have a key for that paper
|
265 |
if paper not in answer_keys or answer_keys[paper] is None:
|
266 |
+
print(f"Skipping: no answer key for paper '{paper}'")
|
267 |
continue
|
268 |
+
correct_key = answer_keys[paper]
|
269 |
+
|
270 |
+
# — Student Answers Extraction —
|
271 |
+
ans_resp = parse_all_answers(page)
|
272 |
+
stud_answers = extract_json_from_output(ans_resp)
|
273 |
+
|
274 |
+
# — Scoring —
|
275 |
+
result = calculate_result(stud_answers, correct_key)
|
276 |
+
all_results.append({
|
277 |
+
"Student Index": idx + 1,
|
278 |
+
"Candidate Info": cand_info.get("Candidate Info", {}),
|
279 |
+
"Student Answers": stud_answers,
|
280 |
+
"Correct Answer Key": correct_key,
|
281 |
+
"Result": result
|
282 |
+
})
|
283 |
+
|
284 |
+
# 5. Save & return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
285 |
with open(RESULT_FILE, "w", encoding="utf-8") as f:
|
286 |
json.dump({"results": all_results}, f, indent=2)
|
287 |
+
|
288 |
return JSONResponse(content={"results": all_results})
|
289 |
+
|
290 |
except Exception as e:
|
291 |
raise HTTPException(status_code=500, detail=str(e))
|
292 |
|