Hammad712 commited on
Commit
0eecb97
·
verified ·
1 Parent(s): 55d36f2

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +189 -229
main.py CHANGED
@@ -1,190 +1,58 @@
1
- from fastapi import FastAPI, UploadFile, File, HTTPException, Query
2
- from fastapi.responses import JSONResponse, StreamingResponse
3
- import uvicorn
4
- import io
5
- import json
6
  import os
7
  import tempfile
 
 
8
  import numpy as np
9
  import cv2
10
  from PIL import Image
11
  from pdf2image import convert_from_bytes
 
 
 
12
 
 
13
  GENAI_API_KEY = os.getenv("GENAI_API_KEY")
14
  if not GENAI_API_KEY:
15
- raise Exception("GENAI_API_KEY not set in .env file.")
16
 
17
  # Import the Google GenAI client libraries.
18
  from google import genai
19
  from google.genai import types
20
 
21
- # Initialize the GenAI client with the API key from .env.
22
  client = genai.Client(api_key=GENAI_API_KEY)
23
 
24
  app = FastAPI(title="Student Result Card API")
25
 
26
- # Use the system temporary directory
27
  TEMP_FOLDER = tempfile.gettempdir()
 
28
 
29
- # -----------------------------
30
- # Preprocessing Methods
31
- # -----------------------------
32
- def preprocess_candidate_info(image_cv):
33
- """
34
- Preprocess the image to extract the candidate information region.
35
- Region is defined by a mask covering the top-left portion.
36
- """
37
- height, width = image_cv.shape[:2]
38
- mask = np.zeros((height, width), dtype="uint8")
39
- margin_top = int(height * 0.10)
40
- margin_bottom = int(height * 0.25)
41
- cv2.rectangle(mask, (0, margin_top), (width, height - margin_bottom), 255, -1)
42
- masked = cv2.bitwise_and(image_cv, image_cv, mask=mask)
43
- coords = cv2.findNonZero(mask)
44
- x, y, w, h = cv2.boundingRect(coords)
45
- cropped = masked[y:y+h, x:x+w]
46
- return Image.fromarray(cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB))
47
-
48
- def preprocess_mcq(image_cv):
49
- """
50
- Preprocess the image to extract the MCQ answers region (questions 1 to 10).
51
- Region is defined by a mask on the left side of the page.
52
- """
53
- height, width = image_cv.shape[:2]
54
- mask = np.zeros((height, width), dtype="uint8")
55
- margin_top = int(height * 0.27)
56
- margin_bottom = int(height * 0.23)
57
- right_boundary = int(width * 0.35)
58
- cv2.rectangle(mask, (0, margin_top), (right_boundary, height - margin_bottom), 255, -1)
59
- masked = cv2.bitwise_and(image_cv, image_cv, mask=mask)
60
- coords = cv2.findNonZero(mask)
61
- x, y, w, h = cv2.boundingRect(coords)
62
- cropped = masked[y:y+h, x:x+w]
63
- return Image.fromarray(cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB))
64
 
65
- def preprocess_free_response(image_cv):
66
- """
67
- Preprocess the image to extract the free-response answers region (questions 11 to 15).
68
- Region is defined by a mask on the middle-right part of the page.
69
- """
70
- height, width = image_cv.shape[:2]
71
- mask = np.zeros((height, width), dtype="uint8")
72
- margin_top = int(height * 0.27)
73
- margin_bottom = int(height * 0.38)
74
- left_boundary = int(width * 0.35)
75
- right_boundary = int(width * 0.68)
76
- cv2.rectangle(mask, (left_boundary, margin_top), (right_boundary, height - margin_bottom), 255, -1)
77
- masked = cv2.bitwise_and(image_cv, image_cv, mask=mask)
78
- coords = cv2.findNonZero(mask)
79
- x, y, w, h = cv2.boundingRect(coords)
80
- cropped = masked[y:y+h, x:x+w]
81
- return Image.fromarray(cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB))
82
-
83
- def preprocess_full_answers(image_cv):
84
- """
85
- For extracting the correct answer key, we assume the entire page contains the answers.
86
- """
87
- return Image.fromarray(cv2.cvtColor(image_cv, cv2.COLOR_BGR2RGB))
88
-
89
- # -----------------------------
90
- # Extraction Methods using Gemini
91
- # -----------------------------
92
- def extract_json_from_output(output_str):
93
  """
94
  Extracts a JSON object from a string containing extra text.
95
  """
96
  start = output_str.find('{')
97
  end = output_str.rfind('}')
98
  if start == -1 or end == -1:
 
99
  return None
100
  json_str = output_str[start:end+1]
101
  try:
102
- return json.loads(json_str)
103
- except json.JSONDecodeError:
 
 
104
  return None
105
 
106
- def get_student_info(image_input):
107
  """
108
- Extracts candidate information from an image.
109
- """
110
- output_format = """
111
- Answer in the following JSON format. Do not write anything else:
112
- {
113
- "Candidate Info": {
114
- "Name": "<name>",
115
- "Number": "<number>",
116
- "Country": "<country>",
117
- "Level": "<level>"
118
- }
119
- }
120
- """
121
- prompt = f"""
122
- You are an assistant that extracts candidate information from an image.
123
- The image contains details including name, candidate number, country, and level.
124
- Extract the information accurately and provide the result in JSON using the format below:
125
- {output_format}
126
- """
127
- response = client.models.generate_content(model="gemini-2.0-flash", contents=[prompt, image_input])
128
- return extract_json_from_output(response.text)
129
-
130
- def get_mcq_answers(image_input):
131
- """
132
- Extracts multiple-choice answers (questions 1 to 10) from an image.
133
- """
134
- output_format = """
135
- Answer in the following JSON format do not write anything else:
136
- {
137
- "Answers": {
138
- "1": "<option>",
139
- "2": "<option>",
140
- "3": "<option>",
141
- "4": "<option>",
142
- "5": "<option>",
143
- "6": "<option>",
144
- "7": "<option>",
145
- "8": "<option>",
146
- "9": "<option>",
147
- "10": "<option>"
148
- }
149
- }
150
- """
151
- prompt = f"""
152
- You are an assistant that extracts MCQ answers from an image.
153
- The image is a screenshot of a 10-question multiple-choice answer sheet.
154
- Extract which option is marked for each question (1 to 10) and provide the answers in JSON using the format below:
155
- {output_format}
156
- """
157
- response = client.models.generate_content(model="gemini-2.0-flash", contents=[prompt, image_input])
158
- return extract_json_from_output(response.text)
159
-
160
- def get_free_response_answers(image_input):
161
- """
162
- Extracts free-text answers (questions 11 to 15) from an image.
163
- """
164
- output_format = """
165
- Answer in the following JSON format. Do not write anything else:
166
- {
167
- "Free Answers": {
168
- "11": "<answer for question 11>",
169
- "12": "<answer for question 12>",
170
- "13": "<answer for question 13>",
171
- "14": "<answer for question 14>",
172
- "15": "<answer for question 15>"
173
- }
174
- }
175
- """
176
- prompt = f"""
177
- You are an assistant that extracts free-text answers from an image.
178
- The image contains responses for questions 11 to 15.
179
- Extract the answers accurately and provide the result in JSON using the format below:
180
- {output_format}
181
- """
182
- response = client.models.generate_content(model="gemini-2.0-flash", contents=[prompt, image_input])
183
- return extract_json_from_output(response.text)
184
-
185
- def get_all_answers(image_input):
186
- """
187
- Extracts all answers (questions 1 to 15) from an image of the correct answer key.
188
  """
189
  output_format = """
190
  Answer in the following JSON format. Do not write anything else:
@@ -213,43 +81,83 @@ You are an assistant that extracts answers from an image.
213
  The image is a screenshot of an answer sheet containing 15 questions.
214
  For questions 1 to 10, the answers are multiple-choice selections.
215
  For questions 11 to 15, the answers are free-text responses.
216
- Extract the answer for each question and provide the result in JSON using the format below:
217
  {output_format}
218
  """
219
- response = client.models.generate_content(model="gemini-2.0-flash", contents=[prompt, image_input])
220
- return extract_json_from_output(response.text)
 
 
 
221
 
222
- # -----------------------------
223
- # Method to calculate result card
224
- # -----------------------------
225
- def calculate_result(student_info, student_mcq, student_free, correct_answers):
226
  """
227
- Compares student's answers with the correct answers, calculates marks and percentage,
228
- and returns a result card in JSON.
229
  """
230
- student_all = {}
231
- if student_mcq and "Answers" in student_mcq:
232
- student_all.update(student_mcq["Answers"])
233
- if student_free and "Free Answers" in student_free:
234
- student_all.update(student_free["Free Answers"])
235
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  correct_all = correct_answers.get("Answers", {})
237
  total_questions = 15
238
  marks = 0
239
  detailed = {}
240
 
241
  for q in map(str, range(1, total_questions + 1)):
242
- student_ans = student_all.get(q, "").strip()
243
- correct_ans = correct_all.get(q, "").strip()
244
- if student_ans == correct_ans:
245
  marks += 1
246
- detailed[q] = {"Student": student_ans, "Correct": correct_ans, "Result": "Correct"}
247
  else:
248
- detailed[q] = {"Student": student_ans, "Correct": correct_ans, "Result": "Incorrect"}
249
 
250
  percentage = (marks / total_questions) * 100
251
  result_card = {
252
- "Candidate Info": student_info.get("Candidate Info", {}),
253
  "Total Marks": marks,
254
  "Total Questions": total_questions,
255
  "Percentage": percentage,
@@ -257,88 +165,140 @@ def calculate_result(student_info, student_mcq, student_free, correct_answers):
257
  }
258
  return result_card
259
 
260
- # -----------------------------
261
- # API Endpoint to process PDFs and return student result cards
262
- # -----------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  @app.post("/process")
264
  async def process_pdfs(
265
- student_pdf: UploadFile = File(...),
266
- answer_key_pdf: UploadFile = File(...),
267
- download: bool = Query(True, description="Set to true to download result card list as a JSON file")
 
268
  ):
269
  try:
270
- # Read student PDF bytes and convert to images
271
- student_bytes = await student_pdf.read()
272
- student_images = convert_from_bytes(student_bytes)
 
 
273
 
274
- # Read answer key PDF bytes and convert to images; assume correct key is in the last page.
275
- answer_key_bytes = await answer_key_pdf.read()
276
- answer_key_images = convert_from_bytes(answer_key_bytes)
277
- last_page = answer_key_images[-1]
278
- last_page_cv = np.array(last_page)
279
- last_page_cv = cv2.cvtColor(last_page_cv, cv2.COLOR_RGB2BGR)
280
- correct_image = preprocess_full_answers(last_page_cv)
281
- correct_answers = get_all_answers(correct_image)
282
 
283
- student_result_cards = []
 
 
284
 
285
- # Process each student page.
286
  for idx, page in enumerate(student_images):
 
 
 
287
  page_cv = np.array(page)
288
  page_cv = cv2.cvtColor(page_cv, cv2.COLOR_RGB2BGR)
289
- student_info_image = preprocess_candidate_info(page_cv)
290
- mcq_image = preprocess_mcq(page_cv)
291
- free_image = preprocess_free_response(page_cv)
292
 
293
- student_info = get_student_info(student_info_image)
294
- student_mcq = get_mcq_answers(mcq_image)
295
- student_free = get_free_response_answers(free_image)
 
 
 
 
 
 
 
 
 
 
 
296
 
297
- result_card = calculate_result(student_info, student_mcq, student_free, correct_answers)
298
- result_card["Student Index"] = idx + 1
299
- student_result_cards.append(result_card)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- response_data = {"result_cards": student_result_cards}
 
 
302
 
303
- if download:
304
- # Create downloadable JSON file and save to system temp folder
305
- json_bytes = json.dumps(response_data, indent=2).encode("utf-8")
306
- file_path = os.path.join(TEMP_FOLDER, "result_cards.json")
307
- with open(file_path, "wb") as f:
308
- f.write(json_bytes)
309
- return StreamingResponse(
310
- io.BytesIO(json_bytes),
311
- media_type="application/json",
312
- headers={"Content-Disposition": "attachment; filename=result_cards.json"}
313
- )
314
- else:
315
- return JSONResponse(content=response_data)
316
 
317
  except Exception as e:
318
  raise HTTPException(status_code=500, detail=str(e))
319
 
320
- # -----------------------------
321
- # New Download Endpoint
322
- # -----------------------------
323
  @app.get("/download")
324
- async def download_result_cards():
325
  """
326
- Returns the previously generated result_cards.json file from the system temporary folder.
327
  """
328
- file_path = os.path.join(TEMP_FOLDER, "result_cards.json")
329
- if not os.path.exists(file_path):
330
- raise HTTPException(status_code=404, detail="File not found")
331
  return StreamingResponse(
332
- open(file_path, "rb"),
333
- media_type="application/json",
334
- headers={"Content-Disposition": "attachment; filename=result_cards.json"}
335
  )
336
 
337
  @app.get("/")
338
  async def root():
339
  return {
340
  "message": "Welcome to the Student Result Card API.",
341
- "usage": "POST PDFs to /process with 'student_pdf' and 'answer_key_pdf' fields. Use ?download=true for file download or GET /download to re-download the JSON file."
342
  }
343
 
344
  if __name__ == "__main__":
 
 
 
 
 
 
1
  import os
2
  import tempfile
3
+ import io
4
+ import json
5
  import numpy as np
6
  import cv2
7
  from PIL import Image
8
  from pdf2image import convert_from_bytes
9
+ from fastapi import FastAPI, UploadFile, File, HTTPException
10
+ from fastapi.responses import JSONResponse, StreamingResponse
11
+ import uvicorn
12
 
13
+ # Get API key from environment
14
  GENAI_API_KEY = os.getenv("GENAI_API_KEY")
15
  if not GENAI_API_KEY:
16
+ raise Exception("GENAI_API_KEY not set in environment")
17
 
18
  # Import the Google GenAI client libraries.
19
  from google import genai
20
  from google.genai import types
21
 
22
+ # Initialize the GenAI client with the API key.
23
  client = genai.Client(api_key=GENAI_API_KEY)
24
 
25
  app = FastAPI(title="Student Result Card API")
26
 
27
+ # Use system temporary directory to store the results file.
28
  TEMP_FOLDER = tempfile.gettempdir()
29
+ RESULT_FILE = os.path.join(TEMP_FOLDER, "result_cards.json")
30
 
31
+ ##############################################################
32
+ # Preprocessing & Extraction Functions
33
+ ##############################################################
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ def extract_json_from_output(output_str: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  """
37
  Extracts a JSON object from a string containing extra text.
38
  """
39
  start = output_str.find('{')
40
  end = output_str.rfind('}')
41
  if start == -1 or end == -1:
42
+ print("No JSON block found in the output.")
43
  return None
44
  json_str = output_str[start:end+1]
45
  try:
46
+ result = json.loads(json_str)
47
+ return result
48
+ except json.JSONDecodeError as e:
49
+ print("Error decoding JSON:", e)
50
  return None
51
 
52
+ def parse_all_answers(image_input: Image.Image) -> str:
53
  """
54
+ Extracts answers from an image of a 15-question answer sheet.
55
+ Returns the response text (JSON string).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  """
57
  output_format = """
58
  Answer in the following JSON format. Do not write anything else:
 
81
  The image is a screenshot of an answer sheet containing 15 questions.
82
  For questions 1 to 10, the answers are multiple-choice selections.
83
  For questions 11 to 15, the answers are free-text responses.
84
+ Extract the answer for each question (1 to 15) and provide the result in JSON using the format below:
85
  {output_format}
86
  """
87
+ response = client.models.generate_content(
88
+ model="gemini-2.0-flash",
89
+ contents=[prompt, image_input]
90
+ )
91
+ return response.text
92
 
93
+ def parse_info(image_input: Image.Image) -> str:
 
 
 
94
  """
95
+ Extracts candidate information including name, number, country, level and paper from an image.
96
+ Returns the response text (JSON string).
97
  """
98
+ output_format = """
99
+ Answer in the following JSON format. Do not write anything else:
100
+ {
101
+ "Candidate Info": {
102
+ "Name": "<name>",
103
+ "Number": "<number>",
104
+ "Country": "<country>",
105
+ "Level": "<level>",
106
+ "Paper": "<paper>"
107
+ }
108
+ }
109
+ """
110
+ prompt = f"""
111
+ You are an assistant that extracts candidate information from an image.
112
+ The image contains candidate details including name, candidate number, country, level and paper.
113
+ Extract the information accurately and provide the result in JSON using the following format:
114
+ {output_format}
115
+ """
116
+ response = client.models.generate_content(
117
+ model="gemini-2.0-flash",
118
+ contents=[prompt, image_input]
119
+ )
120
+ return response.text
121
+
122
+ def parse_paper(student_info_text: str) -> str:
123
+ """
124
+ Extracts the Paper field from candidate information.
125
+ Returns the paper letter (e.g. "A", "B", or "K") as a string.
126
+ """
127
+ prompt = f"""
128
+ You are an assistant that extracts the Paper from candidate information.
129
+ The candidate information contains details including their paper designation.
130
+ Extract the Paper value (one alphabet only) from the following:
131
+ {student_info_text}
132
+ """
133
+ response = client.models.generate_content(
134
+ model="gemini-2.0-flash",
135
+ contents=[prompt, student_info_text]
136
+ )
137
+ return response.text.strip()
138
+
139
+ def calculate_result(student_answers: dict, correct_answers: dict) -> dict:
140
+ """
141
+ Compares student's answers with the correct answers and calculates the score.
142
+ Assumes JSON structures with a top-level "Answers" key containing Q1 to Q15.
143
+ """
144
+ student_all = student_answers.get("Answers", {})
145
  correct_all = correct_answers.get("Answers", {})
146
  total_questions = 15
147
  marks = 0
148
  detailed = {}
149
 
150
  for q in map(str, range(1, total_questions + 1)):
151
+ stud_ans = student_all.get(q, "").strip()
152
+ corr_ans = correct_all.get(q, "").strip()
153
+ if stud_ans == corr_ans:
154
  marks += 1
155
+ detailed[q] = {"Student": stud_ans, "Correct": corr_ans, "Result": "Correct"}
156
  else:
157
+ detailed[q] = {"Student": stud_ans, "Correct": corr_ans, "Result": "Incorrect"}
158
 
159
  percentage = (marks / total_questions) * 100
160
  result_card = {
 
161
  "Total Marks": marks,
162
  "Total Questions": total_questions,
163
  "Percentage": percentage,
 
165
  }
166
  return result_card
167
 
168
+ ##############################################################
169
+ # Helper: Load and Process an Answer Key PDF (from bytes)
170
+ ##############################################################
171
+ def load_answer_key(pdf_bytes: bytes) -> dict:
172
+ """
173
+ Converts a PDF (as bytes) to images, extracts the last page, and parses the answers.
174
+ Returns the parsed JSON answer key.
175
+ """
176
+ images = convert_from_bytes(pdf_bytes)
177
+ last_page_image = images[-1]
178
+ answer_key_response = parse_all_answers(last_page_image)
179
+ answer_key = extract_json_from_output(answer_key_response)
180
+ return answer_key
181
+
182
+ ##############################################################
183
+ # FastAPI Endpoints
184
+ ##############################################################
185
+
186
  @app.post("/process")
187
  async def process_pdfs(
188
+ student_pdf: UploadFile = File(..., description="PDF with all student answer sheets (one page per student)"),
189
+ paper_a_pdf: UploadFile = File(..., description="Answer key PDF for Paper A"),
190
+ paper_b_pdf: UploadFile = File(..., description="Answer key PDF for Paper B"),
191
+ paper_k_pdf: UploadFile = File(..., description="Answer key PDF for Paper K")
192
  ):
193
  try:
194
+ # Read file bytes
195
+ student_pdf_bytes = await student_pdf.read()
196
+ paper_a_bytes = await paper_a_pdf.read()
197
+ paper_b_bytes = await paper_b_pdf.read()
198
+ paper_k_bytes = await paper_k_pdf.read()
199
 
200
+ # Preload answer keys from the three PDFs
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
+ # Convert the student answer PDF to images (each page = one student)
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
+ # Convert the PIL image to OpenCV format for masking
216
  page_cv = np.array(page)
217
  page_cv = cv2.cvtColor(page_cv, cv2.COLOR_RGB2BGR)
218
+ height, width = page_cv.shape[:2]
 
 
219
 
220
+ ###########################################################
221
+ # 1. Extract Candidate Information Region
222
+ ###########################################################
223
+ candidate_mask = np.zeros((height, width), dtype="uint8")
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 # Skip page if no candidate region is found.
231
+ x, y, w, h = cv2.boundingRect(coords)
232
+ cropped_candidate = masked_candidate[y:y+h, x:x+w]
233
+ candidate_pil = Image.fromarray(cv2.cvtColor(cropped_candidate, cv2.COLOR_BGR2RGB))
234
 
235
+ # Extract candidate info using GenAI.
236
+ candidate_info_response = parse_info(candidate_pil)
237
+ candidate_info = extract_json_from_output(candidate_info_response)
238
+
239
+ # Determine the candidate's paper.
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(candidate_info_response)
245
+ paper = paper.upper()
246
+ print(f"Student {idx+1} Paper: {paper}")
247
+
248
+ # Retrieve the appropriate answer key.
249
+ if paper not in answer_keys or answer_keys[paper] is None:
250
+ print(f"Error: Invalid or missing answer key for paper '{paper}' for student {idx+1}. Skipping.")
251
+ continue
252
+ correct_answer_key = answer_keys[paper]
253
+
254
+ ###########################################################
255
+ # 2. Extract Student Answers from the Entire Page
256
+ ###########################################################
257
+ student_answers_response = parse_all_answers(page)
258
+ student_answers = extract_json_from_output(student_answers_response)
259
+
260
+ ###########################################################
261
+ # 3. Calculate the Result for this Student
262
+ ###########################################################
263
+ result = calculate_result(student_answers, correct_answer_key)
264
+
265
+ # Compile the result for this student.
266
+ result_card = {
267
+ "Student Index": idx + 1,
268
+ "Candidate Info": candidate_info.get("Candidate Info", {}) if candidate_info else {},
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
 
 
 
 
284
  @app.get("/download")
285
+ async def download_results():
286
  """
287
+ Returns the result JSON file stored in the temporary folder.
288
  """
289
+ if not os.path.exists(RESULT_FILE):
290
+ raise HTTPException(status_code=404, detail="Result file not found. Please run /process first.")
 
291
  return StreamingResponse(
292
+ open(RESULT_FILE, "rb"),
293
+ media_type="application/json",
294
+ headers={"Content-Disposition": f"attachment; filename=result_cards.json"}
295
  )
296
 
297
  @app.get("/")
298
  async def root():
299
  return {
300
  "message": "Welcome to the Student Result Card API.",
301
+ "usage": "POST PDFs to /process (student answer sheet, paper A, paper B, paper K). Then use /download to retrieve the results."
302
  }
303
 
304
  if __name__ == "__main__":