atz21 commited on
Commit
9dcc575
Β·
verified Β·
1 Parent(s): 0f3e3eb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -208
app.py CHANGED
@@ -3,41 +3,65 @@ import gradio as gr
3
  import google.generativeai as genai
4
  from markdown_pdf import MarkdownPdf, Section
5
  import subprocess
6
- import json
7
- import traceback
8
- import re
9
- import concurrent.futures
10
- from pdf2image import convert_from_path
11
- from PIL import Image, ImageDraw, ImageFont
12
- import cv2
13
- import numpy as np
14
- import img2pdf
15
 
16
  # ---------- PROMPTS ----------
17
  PROMPTS = {
18
  "ALIGNMENT_PROMPT": {
19
  "role": "system",
20
- "content": """Developer: Extract and align QP, MS, and AS into JSON.
21
- ## Instructions
22
- Output must be a valid JSON array, with one object per (sub-)question, in order.
23
- Each object must have exactly these keys:
24
- - "question_number": string (e.g., "1", "1(a)", "2(b)(ii)")
25
- - "qp": string (exact question text or "[Not found]")
26
- - "ms": string (relevant markscheme text or "[Not found]")
27
- - "as": string (final cleaned student answer; "[No response]" or "[illegible]" if needed)
28
- ### Numbering Rules
29
- - Always use **logical order of questions** (1, 2, 3, …) regardless of how they are labeled in the PDF.
30
- - If the QP shows a mismatch (e.g., under "Question 1" the serial number says "12"), **still treat it as Q1**.
31
- - Subparts must be written in standard form (e.g., "1(a)", "1(b)(ii)").
32
- ### Formatting Rules
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": {
39
  "role": "system",
40
  "content": """Developer: You are an official examiner. Apply the following grading rules precisely.
 
41
  ### Abbreviations:
42
  - **M**: Marks for Method
43
  - **A**: Marks for Accuracy/Answer
@@ -45,6 +69,7 @@ Each object must have exactly these keys:
45
  - **AG**: Answer given in questionβ€”no marks
46
  - **FT**: Follow Through marks (if error carried forward correctly)
47
  - **MR**: Deduct for misread (once only)
 
48
  ---
49
  ## Grading Instructions
50
  1. Award marks using official annotations (e.g., M1, A2).
@@ -54,14 +79,36 @@ Each object must have exactly these keys:
54
  5. Apply FT where appropriate.
55
  6. Use proper notation: M1A0, A1, etc.
56
  7. Any lost mark: use red `<span style="color:red">M0</span>` and make Reason red.
 
57
  ---
58
  ## Output Format
59
- 1. Produce a GitHub-flavored Markdown table with columns:
60
- | Student wrote | Marks Awarded | Reason |
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
  }
67
 
@@ -70,7 +117,6 @@ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
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,17 +124,14 @@ def save_as_pdf(text, filename="output.pdf"):
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,13 +141,13 @@ def compress_pdf(input_path, output_path=None, max_size=20*1024*1024):
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 ----------
@@ -116,207 +159,69 @@ def create_model():
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 ----------
120
- def clean_json_output(raw_text: str) -> str:
121
- if not raw_text:
122
- return ""
123
- cleaned = re.sub(r"```(?:json)?", "", raw_text, flags=re.IGNORECASE)
124
- cleaned = re.sub(r"(Raw output|json|JSON)[:\s]*", "", cleaned, flags=re.IGNORECASE)
125
- cleaned = cleaned.strip()
126
- cleaned = re.sub(r"```$", "", cleaned)
127
- return cleaned
128
-
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")
145
 
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,
153
  ms_uploaded,
154
  ans_uploaded
155
  ])
156
- aligned_json = getattr(resp, "text", None)
157
- if not aligned_json and resp.candidates:
158
- aligned_json = resp.candidates[0].content.parts[0].text
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"],
173
- q_json
174
- ])
175
- grading_piece = getattr(response, "text", None)
176
- if not grading_piece and response.candidates:
177
- grading_piece = response.candidates[0].content.parts[0].text
178
- return idx, q["question_number"], grading_piece
179
-
180
- with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
181
- results = list(executor.map(grade_one, enumerate(questions)))
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
-
195
- grading_report = "\n\n".join(grading_sections)
196
- base_name = os.path.splitext(os.path.basename(ans_file))[0]
197
- grading_pdf_path = save_as_pdf(grading_report, f"{base_name}_graded.pdf")
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")
223
- draw = ImageDraw.Draw(img)
224
- w, h = img.size
225
- cell_w, cell_h = w / grid_cols, h / grid_rows
226
-
227
- try:
228
- num_font = ImageFont.truetype("arial.ttf", 20)
229
- except IOError:
230
- num_font = ImageFont.load_default()
231
-
232
- cell_num = 1
233
- for r in range(grid_rows):
234
- for c in range(grid_cols):
235
- x = int(c * cell_w + cell_w / 2)
236
- y = int(r * cell_h + cell_h / 2)
237
- text = str(cell_num)
238
- bbox = draw.textbbox((0, 0), text, font=num_font)
239
- tw = bbox[2] - bbox[0]
240
- th = bbox[3] - bbox[1]
241
- draw.text((x - tw/2, y - th/2), text, fill="black", font=num_font)
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}}, ...]
253
- Grading JSON:
254
- {json.dumps(grading_json, indent=2)}
255
- """
256
- response = model.generate_content([prompt, Image.open(page_images[idx])])
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"))
264
- img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
265
- h, w, _ = img.shape
266
- cell_w, cell_h = w / grid_cols, h / grid_rows
267
-
268
- for item in mapping:
269
- q = item["question"]
270
- cell_number = item["cell_number"]
271
- row = (cell_number - 1) // grid_cols
272
- col = (cell_number - 1) % grid_cols
273
-
274
- marks_list = next((g["marks_awarded"] for g in grading_json["grading"] if g["question"] == q), [])
275
- marks_text = ",".join(marks_list)
276
-
277
- x_c = int((col+1) * cell_w - cell_w/4)
278
- y_c = int((row+0.5) * cell_h)
279
- cv2.putText(img, marks_text, (x_c, y_c), cv2.FONT_HERSHEY_SIMPLEX,
280
- 1.5, (0, 0, 255), 3, cv2.LINE_AA)
281
-
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 ----------
294
- with gr.Blocks(title="LeadIB AI Grading with Optional Imprinting") as demo:
295
- gr.Markdown("## LeadIB AI Grading\nUpload QP, MS, and AS. Get aligned JSON, grading report, and optionally imprint marks on the answer sheet.")
296
 
297
  with gr.Row():
298
- qp_file = gr.File(label="Upload Question Paper (PDF)", type="filepath")
299
- ms_file = gr.File(label="Upload Markscheme (PDF)", type="filepath")
300
- ans_file = gr.File(label="Upload Student Answer Sheet (PDF)", type="filepath")
301
 
302
- imprint_opt = gr.Checkbox(label="Imprint Marks on Answer Sheet?", value=False)
303
- run_btn = gr.Button("Start Alignment + Auto-Grading")
304
 
305
  with gr.Row():
306
- aligned_out = gr.Textbox(label="πŸ“„ Aligned QP | MS | AS (JSON)", lines=20)
 
307
 
308
- with gr.Row():
309
- grading_out = gr.Textbox(label="βœ… Grading Report (Markdown)", lines=20)
310
-
311
- with gr.Row():
312
- grading_pdf = gr.File(label="⬇️ Download Grading Report (PDF)")
313
- imprint_pdf = gr.File(label="⬇️ Download Answer Sheet with Imprinted Marks (PDF)")
314
 
315
- run_btn.click(
316
  fn=align_and_grade,
317
- inputs=[qp_file, ms_file, ans_file, imprint_opt],
318
- outputs=[aligned_out, grading_out, grading_pdf, imprint_pdf],
319
- show_progress=True
320
  )
321
 
322
  if __name__ == "__main__":
 
3
  import google.generativeai as genai
4
  from markdown_pdf import MarkdownPdf, Section
5
  import subprocess
 
 
 
 
 
 
 
 
 
6
 
7
  # ---------- PROMPTS ----------
8
  PROMPTS = {
9
  "ALIGNMENT_PROMPT": {
10
  "role": "system",
11
+ "content": """Developer: Align QP, MS, and AS into structured JSON format.
12
+
13
+ ## Instructions:
14
+ - Each question must include:
15
+ - `id` (question/sub-question number, e.g., "1", "2.a")
16
+ - `qp` (exact question wording)
17
+ - `total_marks` (integer)
18
+ - `ms` (markscheme with mark IDs and descriptions)
19
+ - `as` (student’s steps, numerical values, and notes)
20
+ - Include `total_verification` in MS showing explicit mark breakdown.
21
+ - The structure must be **valid JSON only**.
22
+
23
+ ## Example JSON:
24
+ {
25
+ "questions": [
26
+ {
27
+ "id": "1",
28
+ "qp": "Ramiro walks to work each morning. During the first minute he walks 80 metres. In each subsequent minute he walks 90% of the distance walked during the previous minute.\\nThe distance between his house and work is 660 metres. Ramiro leaves his house at 08:00 and has to be at work by 08:15.\\nExplain why he will not be at work on time.",
29
+ "total_marks": 7,
30
+ "ms": {
31
+ "marks": [
32
+ { "id": "M1_1", "desc": "Recognise that the distance each minute forms a geometric sequence; show r = 0.9 (method mark)." },
33
+ { "id": "M1_2", "desc": "Recognise that total distance is the sum of a geometric sequence and give the sum formula (method mark)." },
34
+ { "id": "M1_3", "desc": "List at least 5 correct terms of the GP (method mark)." },
35
+ { "id": "A1_list", "desc": "List all 15 correct terms (accuracy mark)." },
36
+ { "id": "M1_4", "desc": "Attempt to find S_15 (method mark)." },
37
+ { "id": "A1_sum", "desc": "Correct numerical value for S_15 β‰ˆ 635.287 (accuracy mark)." },
38
+ { "id": "R1", "desc": "Conclude: since S < 660, he will not be there on time (requires preceding A mark)." }
39
+ ],
40
+ "total_verification": "M1 + M1 + M1 + A1 + M1 + A1 + R1 = 7"
41
+ },
42
+ "as": {
43
+ "steps": [
44
+ "90% of 80 = 72 (2nd minute).",
45
+ "90% of 72 = 64.8 (3rd minute).",
46
+ "Sequence shown: 80, 72, 64.8, 58.32.",
47
+ "r = 72/80 = 0.9 ; also 64.8/72 = 0.9.",
48
+ "u_n = u_1 * r^(n-1).",
49
+ "S_n = u_1 * (r^n - 1)/(r - 1).",
50
+ "S_15 = 80 * (0.9^15 - 1)/(0.9 - 1).",
51
+ "S_15 = 635.29 (approx)."
52
+ ],
53
+ "numeric_S15": 635.29,
54
+ "notes": "Student found r and used the sum formula correctly, listed only 4 terms, got S15 β‰ˆ 635.29 but did not explicitly state the final conclusion."
55
+ }
56
+ }
57
+ ]
58
+ }"""
59
  },
60
+
61
  "GRADING_PROMPT": {
62
  "role": "system",
63
  "content": """Developer: You are an official examiner. Apply the following grading rules precisely.
64
+
65
  ### Abbreviations:
66
  - **M**: Marks for Method
67
  - **A**: Marks for Accuracy/Answer
 
69
  - **AG**: Answer given in questionβ€”no marks
70
  - **FT**: Follow Through marks (if error carried forward correctly)
71
  - **MR**: Deduct for misread (once only)
72
+
73
  ---
74
  ## Grading Instructions
75
  1. Award marks using official annotations (e.g., M1, A2).
 
79
  5. Apply FT where appropriate.
80
  6. Use proper notation: M1A0, A1, etc.
81
  7. Any lost mark: use red `<span style="color:red">M0</span>` and make Reason red.
82
+
83
  ---
84
  ## Output Format
85
+ Produce two sections per question/sub-question:
86
+
87
+ ---
88
+ ## Question X (and sub-question if applicable)
89
+
90
+ ### Markscheme vs Student Answer
91
+ | Mark ID | Markscheme Expectation | Student’s Response | Awarded |
92
+ |---------|------------------------|--------------------|---------|
93
+ | M1_1 | Recognise GP, r=0.9 | "r = 72/80 = 0.9" | M1 |
94
+ | M1_2 | Sum formula for GP | "S_n = u1(r^n-1)/(r-1)" | M1 |
95
+ | A1_list | 15 terms listed | Only 4 terms shown | <span style="color:red">A0</span> |
96
+ | … | … | … | … |
97
+
98
+ ➑️ **Total: 6/7**
99
+
100
+ ---
101
+
102
+ ### Examiner’s Report
103
+ At the very end, provide a summary table:
104
+
105
+ | Question Number | Marks | Remark |
106
+ |-----------------|-------|--------|
107
+ | 1 | 6/7 | C |
108
+ | 2.a | 9/9 | A |
109
+
110
+ Then show total clearly:
111
+ `Total: 15/16`"""
112
  }
113
  }
114
 
 
117
 
118
  # ---------- HELPER: Save to PDF ----------
119
  def save_as_pdf(text, filename="output.pdf"):
 
120
  pdf = MarkdownPdf()
121
  pdf.add_section(Section(text, toc=False))
122
  pdf.save(filename)
 
124
 
125
  # ---------- HELPER: Compress PDF ----------
126
  def compress_pdf(input_path, output_path=None, max_size=20*1024*1024):
 
127
  if output_path is None:
128
  base, ext = os.path.splitext(input_path)
129
  output_path = f"{base}_compressed{ext}"
130
 
131
  if os.path.getsize(input_path) <= max_size:
 
132
  return input_path
133
 
134
  try:
 
135
  gs_cmd = [
136
  "gs", "-sDEVICE=pdfwrite",
137
  "-dCompatibilityLevel=1.4",
 
141
  ]
142
  subprocess.run(gs_cmd, check=True)
143
  if os.path.getsize(output_path) <= max_size:
144
+ print(f"βœ… Compressed {input_path} β†’ {output_path}")
145
  return output_path
146
  else:
147
+ print(f"⚠️ Compression failed to reduce below {max_size/1024/1024} MB")
148
  return input_path
149
  except Exception as e:
150
+ print(f"⚠️ Compression error: {e}")
151
  return input_path
152
 
153
  # ---------- HELPER: Create Model with Fallback ----------
 
159
  print("⚑ Falling back to gemini-2.5-flash model")
160
  return genai.GenerativeModel("gemini-2.5-flash", generation_config={"temperature": 0})
161
 
 
 
 
 
 
 
 
 
 
 
162
  # ---------- PIPELINE: ALIGN + GRADE ----------
163
+ def align_and_grade(qp_file, ms_file, ans_file):
164
  try:
 
 
 
 
165
  qp_file = compress_pdf(qp_file, "qp_compressed.pdf")
166
  ms_file = compress_pdf(ms_file, "ms_compressed.pdf")
167
  ans_file = compress_pdf(ans_file, "ans_compressed.pdf")
168
 
 
 
169
  qp_uploaded = genai.upload_file(path=qp_file, display_name="Question Paper")
170
  ms_uploaded = genai.upload_file(path=ms_file, display_name="Markscheme")
171
  ans_uploaded = genai.upload_file(path=ans_file, display_name="Answer Sheet")
172
 
173
  model = create_model()
174
 
175
+ # ---------------- STEP 1: ALIGN (JSON only) ----------------
 
176
  resp = model.generate_content([
177
  PROMPTS["ALIGNMENT_PROMPT"]["content"],
178
  qp_uploaded,
179
  ms_uploaded,
180
  ans_uploaded
181
  ])
182
+ json_output = getattr(resp, "text", None)
183
+ if not json_output and resp.candidates:
184
+ json_output = resp.candidates[0].content.parts[0].text
185
+
186
+ # ---------------- STEP 2: GRADING (Markdown + PDF) ----------------
187
+ response = model.generate_content([
188
+ PROMPTS["GRADING_PROMPT"]["content"],
189
+ json_output
190
+ ])
191
+ grading = getattr(response, "text", None)
192
+ if not grading and response.candidates:
193
+ grading = response.candidates[0].content.parts[0].text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ base_name = os.path.splitext(os.path.basename(ans_file))[0]
196
+ grading_pdf_path = save_as_pdf(grading, f"{base_name}_graded.pdf")
 
 
197
 
198
+ # Return JSON (alignment), Markdown grading, and PDF
199
+ return json_output, grading, grading_pdf_path
200
 
201
  except Exception as e:
202
+ return f"❌ Error: {e}", None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  # ---------- GRADIO APP ----------
205
+ with gr.Blocks(title="LeadIB AI Grading (Alignment + Auto-Grading)") as demo:
206
+ gr.Markdown("## πŸ“˜ LeadIB AI Grading\nUpload **Question Paper**, **Markscheme**, and **Student Answer Sheet**.\nThe system will first align into JSON, then auto-grade with detailed feedback.")
207
 
208
  with gr.Row():
209
+ qp_file = gr.File(label="πŸ“„ Upload Question Paper (PDF)")
210
+ ms_file = gr.File(label="πŸ“„ Upload Markscheme (PDF)")
211
+ ans_file = gr.File(label="πŸ“ Upload Student Answer Sheet (PDF)")
212
 
213
+ run_button = gr.Button("πŸš€ Run Alignment + Grading")
 
214
 
215
  with gr.Row():
216
+ json_output = gr.Textbox(label="πŸ“‘ Step 1: Alignment (JSON)", lines=20)
217
+ grading_output = gr.Textbox(label="πŸ“ Step 2: Grading (Markdown)", lines=20)
218
 
219
+ grading_pdf = gr.File(label="πŸ“₯ Download Grading PDF")
 
 
 
 
 
220
 
221
+ run_button.click(
222
  fn=align_and_grade,
223
+ inputs=[qp_file, ms_file, ans_file],
224
+ outputs=[json_output, grading_output, grading_pdf]
 
225
  )
226
 
227
  if __name__ == "__main__":