atz21 commited on
Commit
954c18d
Β·
verified Β·
1 Parent(s): 3d4baa8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -15
app.py CHANGED
@@ -33,15 +33,6 @@ Each object must have exactly these keys:
33
  - Preserve math inside fenced code ```...```.
34
  - If diagram/graph missing, write "[Graph omitted]".
35
  - Do not add extra commentary outside JSON.
36
- ## Example
37
- [
38
- {
39
- "question_number": "1",
40
- "qp": "Expand (1+x)^3",
41
- "ms": "M1 for binomial expansion, A1 for coefficients, A1 for final form",
42
- "as": "```x^3 + 3x^2 + 3x + 1```"
43
- }
44
- ]
45
  """
46
  },
47
  "GRADING_PROMPT": {
@@ -70,8 +61,6 @@ Each object must have exactly these keys:
70
  - Each row = one markable step/point, in order.
71
  - For blanks: β€œ(no answer)” with marks lost.
72
  2. After the table, write ONLY one line for total marks in the form: Final Marks: X / Y
73
- ⚠️ Do NOT include summaries, error classifications, or extra commentary.
74
- Only table + final marks line.
75
  """
76
  }
77
  }
@@ -81,6 +70,7 @@ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
81
 
82
  # ---------- HELPER: Save to PDF ----------
83
  def save_as_pdf(text, filename="output.pdf"):
 
84
  pdf = MarkdownPdf()
85
  pdf.add_section(Section(text, toc=False))
86
  pdf.save(filename)
@@ -88,14 +78,17 @@ def save_as_pdf(text, filename="output.pdf"):
88
 
89
  # ---------- HELPER: Compress PDF ----------
90
  def compress_pdf(input_path, output_path=None, max_size=20*1024*1024):
 
91
  if output_path is None:
92
  base, ext = os.path.splitext(input_path)
93
  output_path = f"{base}_compressed{ext}"
94
 
95
  if os.path.getsize(input_path) <= max_size:
 
96
  return input_path
97
 
98
  try:
 
99
  gs_cmd = [
100
  "gs", "-sDEVICE=pdfwrite",
101
  "-dCompatibilityLevel=1.4",
@@ -105,17 +98,22 @@ def compress_pdf(input_path, output_path=None, max_size=20*1024*1024):
105
  ]
106
  subprocess.run(gs_cmd, check=True)
107
  if os.path.getsize(output_path) <= max_size:
 
108
  return output_path
109
  else:
 
110
  return input_path
111
- except Exception:
 
112
  return input_path
113
 
114
  # ---------- HELPER: Create Model with Fallback ----------
115
  def create_model():
116
  try:
 
117
  return genai.GenerativeModel("gemini-2.5-pro", generation_config={"temperature": 0})
118
  except Exception:
 
119
  return genai.GenerativeModel("gemini-2.5-flash", generation_config={"temperature": 0})
120
 
121
  # ---------- HELPER: Clean JSON Output ----------
@@ -131,12 +129,16 @@ def clean_json_output(raw_text: str) -> str:
131
  # ---------- PIPELINE: ALIGN + GRADE ----------
132
  def align_and_grade(qp_file, ms_file, ans_file, imprint=False):
133
  try:
 
 
134
  # Step 0: Compress
 
135
  qp_file = compress_pdf(qp_file, "qp_compressed.pdf")
136
  ms_file = compress_pdf(ms_file, "ms_compressed.pdf")
137
  ans_file = compress_pdf(ans_file, "ans_compressed.pdf")
138
 
139
  # Step 1: Uploads
 
140
  qp_uploaded = genai.upload_file(path=qp_file, display_name="Question Paper")
141
  ms_uploaded = genai.upload_file(path=ms_file, display_name="Markscheme")
142
  ans_uploaded = genai.upload_file(path=ans_file, display_name="Answer Sheet")
@@ -144,6 +146,7 @@ def align_and_grade(qp_file, ms_file, ans_file, imprint=False):
144
  model = create_model()
145
 
146
  # Step 2: Alignment
 
147
  resp = model.generate_content([
148
  PROMPTS["ALIGNMENT_PROMPT"]["content"],
149
  qp_uploaded,
@@ -156,10 +159,14 @@ def align_and_grade(qp_file, ms_file, ans_file, imprint=False):
156
 
157
  aligned_json = clean_json_output(aligned_json)
158
  questions = json.loads(aligned_json)
 
159
 
160
  # Step 3: Grading
 
 
161
  def grade_one(idx_q):
162
  idx, q = idx_q
 
163
  q_json = json.dumps(q, indent=2)
164
  response = model.generate_content([
165
  PROMPTS["GRADING_PROMPT"]["content"],
@@ -175,13 +182,13 @@ def align_and_grade(qp_file, ms_file, ans_file, imprint=False):
175
  results.sort(key=lambda x: x[0])
176
 
177
  # Step 4: Build report
 
178
  grading_sections = []
179
  grading_json = {"grading": []}
180
  for _, qnum, grading_piece in results:
181
  section = f"## Question {qnum}\n\n{grading_piece}"
182
  grading_sections.append(section)
183
 
184
- # Extract marks list
185
  marks_list = re.findall(r"(M[01]|A[0-9]|R[01])", grading_piece)
186
  grading_json["grading"].append({"question": qnum, "marks_awarded": marks_list})
187
 
@@ -191,22 +198,25 @@ def align_and_grade(qp_file, ms_file, ans_file, imprint=False):
191
 
192
  imprint_pdf_path = None
193
  if imprint:
 
194
  imprint_pdf_path = imprint_marks(ans_file, grading_json, model)
195
 
 
196
  return json.dumps(questions, indent=2), grading_report, grading_pdf_path, imprint_pdf_path
197
 
198
  except Exception as e:
 
199
  traceback.print_exc()
200
  return f"❌ Error: {e}", None, None, None
201
 
202
  # ---------- PIPELINE: IMPRINT MARKS ----------
203
  def imprint_marks(ans_pdf, grading_json, model, grid_rows=20, grid_cols=14):
 
204
  output_dir = "grid_pages"
205
  os.makedirs(output_dir, exist_ok=True)
206
  pages = convert_from_path(ans_pdf, dpi=200)
207
  page_images = []
208
 
209
- # Create grid images
210
  for i, page in enumerate(pages):
211
  img_path = os.path.join(output_dir, f"page_{i+1}_grid.png")
212
  img = page.convert("RGB")
@@ -232,10 +242,11 @@ def imprint_marks(ans_pdf, grading_json, model, grid_rows=20, grid_cols=14):
232
  cell_num += 1
233
  img.save(img_path, "PNG")
234
  page_images.append(img_path)
 
235
 
236
  annotated_pages = []
237
  for idx, page in enumerate(pages):
238
- # Ask Gemini for mapping
239
  prompt = f"""
240
  You are an exam marker. The page is divided into a {grid_rows} x {grid_cols} grid with numbered cells.
241
  Return JSON: [{{"question": "1(a)", "cell_number": 15}}, ...]
@@ -246,6 +257,7 @@ Grading JSON:
246
  mapping_text = getattr(response, "text", "")
247
  match = re.search(r'\[.*\]', mapping_text, re.DOTALL)
248
  mapping = json.loads(match.group(0)) if match else []
 
249
 
250
  # Annotate
251
  img = np.array(page.convert("RGB"))
@@ -270,10 +282,12 @@ Grading JSON:
270
  annotated_path = os.path.join(output_dir, f"annotated_{idx+1}.png")
271
  cv2.imwrite(annotated_path, img)
272
  annotated_pages.append(annotated_path)
 
273
 
274
  output_pdf = "answer_sheet_with_marks.pdf"
275
  with open(output_pdf, "wb") as f:
276
  f.write(img2pdf.convert(annotated_pages))
 
277
  return output_pdf
278
 
279
  # ---------- GRADIO APP ----------
 
33
  - Preserve math inside fenced code ```...```.
34
  - If diagram/graph missing, write "[Graph omitted]".
35
  - Do not add extra commentary outside JSON.
 
 
 
 
 
 
 
 
 
36
  """
37
  },
38
  "GRADING_PROMPT": {
 
61
  - Each row = one markable step/point, in order.
62
  - For blanks: β€œ(no answer)” with marks lost.
63
  2. After the table, write ONLY one line for total marks in the form: Final Marks: X / Y
 
 
64
  """
65
  }
66
  }
 
70
 
71
  # ---------- HELPER: Save to PDF ----------
72
  def save_as_pdf(text, filename="output.pdf"):
73
+ print(f"πŸ“„ Saving grading report to PDF β†’ {filename}")
74
  pdf = MarkdownPdf()
75
  pdf.add_section(Section(text, toc=False))
76
  pdf.save(filename)
 
78
 
79
  # ---------- HELPER: Compress PDF ----------
80
  def compress_pdf(input_path, output_path=None, max_size=20*1024*1024):
81
+ print(f"πŸ—œοΈ Checking if compression needed for {input_path}...")
82
  if output_path is None:
83
  base, ext = os.path.splitext(input_path)
84
  output_path = f"{base}_compressed{ext}"
85
 
86
  if os.path.getsize(input_path) <= max_size:
87
+ print("βœ… No compression needed")
88
  return input_path
89
 
90
  try:
91
+ print(f"⚑ Compressing {input_path} β†’ {output_path}")
92
  gs_cmd = [
93
  "gs", "-sDEVICE=pdfwrite",
94
  "-dCompatibilityLevel=1.4",
 
98
  ]
99
  subprocess.run(gs_cmd, check=True)
100
  if os.path.getsize(output_path) <= max_size:
101
+ print("βœ… Compression successful")
102
  return output_path
103
  else:
104
+ print("⚠️ Compression didn’t shrink enough, using original")
105
  return input_path
106
+ except Exception as e:
107
+ print(f"❌ Compression failed: {e}")
108
  return input_path
109
 
110
  # ---------- HELPER: Create Model with Fallback ----------
111
  def create_model():
112
  try:
113
+ print("⚑ Using gemini-2.5-pro model")
114
  return genai.GenerativeModel("gemini-2.5-pro", generation_config={"temperature": 0})
115
  except Exception:
116
+ print("⚑ Falling back to gemini-2.5-flash model")
117
  return genai.GenerativeModel("gemini-2.5-flash", generation_config={"temperature": 0})
118
 
119
  # ---------- HELPER: Clean JSON Output ----------
 
129
  # ---------- PIPELINE: ALIGN + GRADE ----------
130
  def align_and_grade(qp_file, ms_file, ans_file, imprint=False):
131
  try:
132
+ print("\nπŸš€ Starting alignment + grading pipeline")
133
+
134
  # Step 0: Compress
135
+ print("πŸ” Step 0: Compressing PDFs...")
136
  qp_file = compress_pdf(qp_file, "qp_compressed.pdf")
137
  ms_file = compress_pdf(ms_file, "ms_compressed.pdf")
138
  ans_file = compress_pdf(ans_file, "ans_compressed.pdf")
139
 
140
  # Step 1: Uploads
141
+ print("πŸ“€ Step 1: Uploading PDFs to Gemini...")
142
  qp_uploaded = genai.upload_file(path=qp_file, display_name="Question Paper")
143
  ms_uploaded = genai.upload_file(path=ms_file, display_name="Markscheme")
144
  ans_uploaded = genai.upload_file(path=ans_file, display_name="Answer Sheet")
 
146
  model = create_model()
147
 
148
  # Step 2: Alignment
149
+ print("🧩 Step 2: Aligning QP, MS, and AS...")
150
  resp = model.generate_content([
151
  PROMPTS["ALIGNMENT_PROMPT"]["content"],
152
  qp_uploaded,
 
159
 
160
  aligned_json = clean_json_output(aligned_json)
161
  questions = json.loads(aligned_json)
162
+ print(f"βœ… Parsed JSON with {len(questions)} questions")
163
 
164
  # Step 3: Grading
165
+ print("πŸ“ Step 3: Grading each question...")
166
+
167
  def grade_one(idx_q):
168
  idx, q = idx_q
169
+ print(f" ➑️ Grading Question {q['question_number']}")
170
  q_json = json.dumps(q, indent=2)
171
  response = model.generate_content([
172
  PROMPTS["GRADING_PROMPT"]["content"],
 
182
  results.sort(key=lambda x: x[0])
183
 
184
  # Step 4: Build report
185
+ print("πŸ“Š Step 4: Building grading report...")
186
  grading_sections = []
187
  grading_json = {"grading": []}
188
  for _, qnum, grading_piece in results:
189
  section = f"## Question {qnum}\n\n{grading_piece}"
190
  grading_sections.append(section)
191
 
 
192
  marks_list = re.findall(r"(M[01]|A[0-9]|R[01])", grading_piece)
193
  grading_json["grading"].append({"question": qnum, "marks_awarded": marks_list})
194
 
 
198
 
199
  imprint_pdf_path = None
200
  if imprint:
201
+ print("✍ Step 5: Imprinting marks onto answer sheet...")
202
  imprint_pdf_path = imprint_marks(ans_file, grading_json, model)
203
 
204
+ print("βœ… Pipeline completed successfully")
205
  return json.dumps(questions, indent=2), grading_report, grading_pdf_path, imprint_pdf_path
206
 
207
  except Exception as e:
208
+ print("❌ Fatal error in pipeline")
209
  traceback.print_exc()
210
  return f"❌ Error: {e}", None, None, None
211
 
212
  # ---------- PIPELINE: IMPRINT MARKS ----------
213
  def imprint_marks(ans_pdf, grading_json, model, grid_rows=20, grid_cols=14):
214
+ print("πŸ“„ Converting answer sheet to images with grid...")
215
  output_dir = "grid_pages"
216
  os.makedirs(output_dir, exist_ok=True)
217
  pages = convert_from_path(ans_pdf, dpi=200)
218
  page_images = []
219
 
 
220
  for i, page in enumerate(pages):
221
  img_path = os.path.join(output_dir, f"page_{i+1}_grid.png")
222
  img = page.convert("RGB")
 
242
  cell_num += 1
243
  img.save(img_path, "PNG")
244
  page_images.append(img_path)
245
+ print("βœ… Grid images prepared")
246
 
247
  annotated_pages = []
248
  for idx, page in enumerate(pages):
249
+ print(f"πŸ”Ž Asking Gemini for mapping on page {idx+1}...")
250
  prompt = f"""
251
  You are an exam marker. The page is divided into a {grid_rows} x {grid_cols} grid with numbered cells.
252
  Return JSON: [{{"question": "1(a)", "cell_number": 15}}, ...]
 
257
  mapping_text = getattr(response, "text", "")
258
  match = re.search(r'\[.*\]', mapping_text, re.DOTALL)
259
  mapping = json.loads(match.group(0)) if match else []
260
+ print(f" β†ͺ Gemini returned {len(mapping)} mappings")
261
 
262
  # Annotate
263
  img = np.array(page.convert("RGB"))
 
282
  annotated_path = os.path.join(output_dir, f"annotated_{idx+1}.png")
283
  cv2.imwrite(annotated_path, img)
284
  annotated_pages.append(annotated_path)
285
+ print(f"πŸ–Š Marks imprinted for page {idx+1}")
286
 
287
  output_pdf = "answer_sheet_with_marks.pdf"
288
  with open(output_pdf, "wb") as f:
289
  f.write(img2pdf.convert(annotated_pages))
290
+ print(f"βœ… Final imprinted PDF saved: {output_pdf}")
291
  return output_pdf
292
 
293
  # ---------- GRADIO APP ----------