atz21 commited on
Commit
b8e8d08
Β·
verified Β·
1 Parent(s): 20351b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -116
app.py CHANGED
@@ -29,6 +29,7 @@ TASK:
29
  1. Transcribe EXACTLY all the questions FIRST (with their total marks).
30
  2. After ALL questions, transcribe the Markscheme exactly, preserving M/A/R notation in brackets.
31
  3. Always number the questions sequentially (Question 1, Question 2, Question 3, …) **in the order they appear in the PDF**, even if the PDF shows a different number or leaves it blank. Do NOT skip or leave Question: blank. Never start a question other than question 1 ( even if it is labelled in pdf as 8 name it 1)
 
32
  FORMAT:
33
  ==== PAPER TOTAL MARKS ====
34
  <total marks>
@@ -52,7 +53,7 @@ Answer 2 :
52
  <exact MS for Q2 with notations>
53
  (repeat for all answers)
54
  ==== MARKSCHEME END ====
55
- """
56
  }
57
  ,
58
 
@@ -94,8 +95,9 @@ At the very end, provide a summary table:
94
  Then show total clearly as a final line:
95
  `Total: <obtained_marks>/<max_marks>`
96
  NOTES:
97
- - The assistant will receive two transcripts: (1) QP+MS transcription (questions then markscheme) and (2) AS transcription (student answers). Use the QP+MS transcript as the authoritative source of question wording, total marks, and verbatim markscheme entries (M/A/R mark IDs).
98
  - Match student answers to question IDs and grade according to the provided verbatim markscheme.
 
99
  - Produce full markdown as above. Ensure mark IDs used in the grading are present and consistent with the markscheme.
100
  """
101
  }
@@ -221,32 +223,64 @@ def extract_question_ids_from_qpms(text):
221
  print("⚠️ No question IDs extracted; will send NA placeholder.")
222
  return ids
223
 
224
- def build_as_prompt_with_expected_ids(expected_ids):
225
- """
226
- Construct the AS transcription prompt injecting the expected IDs block.
227
- """
228
- if not expected_ids:
229
- ids_block = "{NA}"
230
- else:
231
- ids_block = "{\n" + "\n".join(expected_ids) + "\n}"
232
- prompt = f"""You are a high-quality handwritten transcription assistant.
233
- INPUT: This PDF contains a student's handwritten answer sheet.
234
- TASK: Transcribe the student's answers exactly (as text). Preserve step order and line breaks. Attempt to assign each answer to a question ID if the student has labelled it (e.g., "1", "1a", "2(b)", "3"). If the student hasn't labelled answers, segment contiguous answer blocks and attempt to infer question IDs from context β€” but mark inferred IDs clearly as "INFERRED: <id>"
235
- Enclose all mathematical expressions in Markdown fenced code blocks (``` triple backticks).
236
- If a diagram/graph is omitted, write [Graph omitted].
237
- Unreadable parts: [illegible].
238
- Unanswered: [No response].
239
- Do NOT recreate diagrams.
240
- Ensure consistency and determinism in formatting so subsequent models can grade directly from this aligned format.
241
- Expected questions (if missing, write NA):
242
- {ids_block}
243
- -----------------------
244
- OUTPUT FORMAT:
245
- Question <id>
246
- AS:
247
- <transcribed answer or placeholder>
248
- """
249
- return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  def extract_marks_from_grading(grading_text):
252
  """
@@ -286,7 +320,6 @@ def ask_gemini_for_mapping_for_page(model, image_path, grading_json, expected_id
286
  You are an exam marker. Your role is to identify where each question begins on the page.
287
  The page is divided into a {rows} x {cols} grid. Each cell has a RUNNING NUMBER label (1..{rows*cols}).
288
  For each question in the grading JSON, return the cell NUMBER where the FIRST STEP of that question begins.
289
-
290
  IMPORTANT: Only spot and return cell numbers for the following question IDs (one per line):
291
  {ids_block}
292
  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).
@@ -450,93 +483,124 @@ def imprint_marks_using_mapping(pdf_path, grading_json, output_pdf, model, expec
450
  print("πŸ“‘ Imprinted PDF saved to:", compressed)
451
  return compressed
452
 
453
- # ---------------- MAIN PIPELINE ----------------
454
- def align_and_grade_pipeline(qp_path, ms_path, ans_path, imprint=False):
455
- """
456
- Final pipeline implementing requested flow and verbose console logging.
457
- """
458
- try:
459
- print("πŸ” Starting pipeline...")
460
- # Step 0: compress as needed
461
- qp_path = compress_pdf(qp_path)
462
- ms_path = compress_pdf(ms_path)
463
- ans_path = compress_pdf(ans_path)
464
-
465
- # Merge QP + MS
466
- merged_qpms_path = os.path.splitext(qp_path)[0] + "_merged_qp_ms.pdf"
467
- merge_pdfs([qp_path, ms_path], merged_qpms_path)
468
- print("πŸ“Ž Merged QP + MS ->", merged_qpms_path)
469
-
470
- # Upload files to Gemini
471
- print("πŸ”Ό Uploading files to Gemini...")
472
- merged_uploaded = genai.upload_file(path=merged_qpms_path, display_name="QP+MS (merged)")
473
- ans_uploaded = genai.upload_file(path=ans_path, display_name="Answer Sheet")
474
- print("βœ… Upload complete.")
475
-
476
- # Create model and print which selected
477
- model = create_model()
478
-
479
- # Step 1.i: QP+MS transcription (first)
480
- print("1.i) Transcribing QP+MS (questions first, then full markscheme)...")
481
- qpms_prompt = PROMPTS["QP_MS_TRANSCRIPTION"]["content"]
482
- qpms_text = gemini_generate_content(model, qpms_prompt, file_upload_obj=merged_uploaded)
483
- print("πŸ“„ QP+MS transcription received. Saving debug file: debug_qpms_transcript.txt")
484
- with open("debug_qpms_transcript.txt", "w", encoding="utf-8") as f:
485
- f.write(qpms_text)
486
-
487
- # Step 2: extract serial numbers (question IDs) using regex from qpms_text
488
- extracted_ids = extract_question_ids_from_qpms(qpms_text)
489
- if not extracted_ids:
490
- extracted_ids = ["NA"]
491
-
492
- # Step 1.ii: Build AS prompt injecting extracted IDs and transcribe AS
493
- print("1.ii) Building AS transcription prompt with expected question IDs and sending to Gemini...")
494
- as_prompt = build_as_prompt_with_expected_ids(extracted_ids)
495
- as_text = gemini_generate_content(model, as_prompt, file_upload_obj=ans_uploaded)
496
- print("πŸ“ AS transcription received. Saving debug file: debug_as_transcript.txt")
497
- with open("debug_as_transcript.txt", "w", encoding="utf-8") as f:
498
- f.write(as_text)
499
-
500
- # Step 3: Grading - send both transcripts to grading model
501
- print("2) Preparing grading input and sending to Gemini for grading...")
502
- grading_input = (
503
- "=== QP+MS TRANSCRIPT BEGIN ===\n"
504
- + qpms_text
505
- + "\n=== QP+MS TRANSCRIPT END ===\n\n"
506
- + "=== ANSWER SHEET TRANSCRIPT BEGIN ===\n"
507
- + as_text
508
- + "\n=== ANSWER SHEET TRANSCRIPT END ===\n"
509
- )
510
- grading_prompt_system = PROMPTS["GRADING_PROMPT"]["content"]
511
- grading_text = gemini_generate_content(model, grading_prompt_system + "\n\nPlease grade the following transcripts:\n" + grading_input)
512
- print("🧾 Grading output received. Saving debug file: debug_grading.md")
513
- with open("debug_grading.md", "w", encoding="utf-8") as f:
514
- f.write(grading_text)
515
-
516
- # Save grading PDF
517
- base_name = os.path.splitext(os.path.basename(ans_path))[0]
518
- grading_pdf_path = save_as_pdf(grading_text, f"{base_name}_graded.pdf")
519
- print("πŸ“„ Grading PDF saved:", grading_pdf_path)
520
-
521
- # Step 4: Extract marks for imprinting
522
- grading_json = extract_marks_from_grading(grading_text)
523
- with open("debug_grading_json.json", "w", encoding="utf-8") as f:
524
- json.dump(grading_json, f, indent=2, ensure_ascii=False)
525
- print("πŸ”§ Grading marks extraction complete.")
526
-
527
- imprinted_pdf_path = None
528
- if imprint:
529
- print("✍ Imprint option enabled. Starting imprinting process (parallel mapping requests)...")
530
- imprinted_pdf_path = f"{base_name}_imprinted.pdf"
531
- imprinted_pdf_path = imprint_marks_using_mapping(ans_path, grading_json, imprinted_pdf_path, model, extracted_ids)
532
- print("βœ… Imprinting finished. Imprinted PDF at:", imprinted_pdf_path)
533
-
534
- print("🏁 Pipeline finished successfully.")
535
- return qpms_text, as_text, grading_text, grading_pdf_path, imprinted_pdf_path
536
-
537
- except Exception as e:
538
- print("❌ Pipeline error:", e)
539
- return f"❌ Error: {e}", None, None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
  # ---------------- GRADIO UI ----------------
542
  with gr.Blocks(title="LeadIB AI Grading (Final Flow β€” Verbose)") as demo:
 
29
  1. Transcribe EXACTLY all the questions FIRST (with their total marks).
30
  2. After ALL questions, transcribe the Markscheme exactly, preserving M/A/R notation in brackets.
31
  3. Always number the questions sequentially (Question 1, Question 2, Question 3, …) **in the order they appear in the PDF**, even if the PDF shows a different number or leaves it blank. Do NOT skip or leave Question: blank. Never start a question other than question 1 ( even if it is labelled in pdf as 8 name it 1)
32
+ 4. After the markscheme, DETECT and FLAG all questions in the markscheme where a graph/diagram is expected. For each, output the question number and the page number in the format below.
33
  FORMAT:
34
  ==== PAPER TOTAL MARKS ====
35
  <total marks>
 
53
  <exact MS for Q2 with notations>
54
  (repeat for all answers)
55
  ==== MARKSCHEME END ====
56
+ ==== GRAPH EXPECTED QUESTIONS ====\nGraph expected in:\n- Question <number> β†’ Page <number>\n(one per line)\n==== END GRAPH EXPECTED ====\n"""
57
  }
58
  ,
59
 
 
95
  Then show total clearly as a final line:
96
  `Total: <obtained_marks>/<max_marks>`
97
  NOTES:
98
+ - The assistant will receive two transcripts: (1) QP+MS transcript (questions then markscheme) and (2) AS transcript (student answers). Use the QP+MS transcript as the authoritative source of question wording, total marks, and verbatim markscheme entries (M/A/R mark IDs).
99
  - Match student answers to question IDs and grade according to the provided verbatim markscheme.
100
+ - For questions where a graph is expected and the student attempted a graph, you will be provided with the relevant markscheme and answer sheet graph images/pages. Use these for grading those questions with visual context. For all other questions, proceed as usual.
101
  - Produce full markdown as above. Ensure mark IDs used in the grading are present and consistent with the markscheme.
102
  """
103
  }
 
223
  print("⚠️ No question IDs extracted; will send NA placeholder.")
224
  return ids
225
 
226
+ # Update AS prompt builder to include graph detection
227
+
228
+ def build_as_prompt_with_expected_ids(expected_ids):
229
+ """
230
+ Construct the AS transcription prompt injecting the expected IDs block and graph detection instructions.
231
+ """
232
+ if not expected_ids:
233
+ ids_block = "{NA}"
234
+ else:
235
+ ids_block = "{\n" + "\n".join(expected_ids) + "\n}"
236
+ prompt = f"""You are a high-quality handwritten transcription assistant.
237
+ INPUT: This PDF contains a student's handwritten answer sheet.
238
+ TASK: Transcribe the student's answers exactly (as text). Preserve step order and line breaks. Attempt to assign each answer to a question ID if the student has labelled it (e.g., "1", "1a", "2(b)", "3"). If the student hasn't labelled answers, segment contiguous answer blocks and attempt to infer question IDs from context β€” but mark inferred IDs clearly as "INFERRED: <id>"
239
+ Enclose all mathematical expressions in Markdown fenced code blocks (``` triple backticks).
240
+ If a diagram/graph is omitted, write [Graph omitted].
241
+ Unreadable parts: [illegible].
242
+ Unanswered: [No response].
243
+ Do NOT recreate diagrams.
244
+ Ensure consistency and determinism in formatting so subsequent models can grade directly from this aligned format.
245
+ Expected questions (if missing, write NA):
246
+ {ids_block}
247
+ -----------------------
248
+ OUTPUT FORMAT:
249
+ Question <id>
250
+ AS:
251
+ <transcribed answer or placeholder>
252
+ ==== GRAPH FOUND ANSWERS ====\nGraph found in:\n- Answer <number> β†’ Page <number>\n(one per line)\n==== END GRAPH FOUND ====\n"""
253
+ return prompt
254
+
255
+ # Robust parsing functions for graph detection
256
+
257
+ def extract_graph_questions_from_ms(ms_text):
258
+ """
259
+ Parse LLM output for Markscheme to extract questions/pages where a graph is expected.
260
+ Returns dict: {question_number: ms_page_number}
261
+ """
262
+ matches = re.findall(r"==== GRAPH EXPECTED QUESTIONS ====\\s*Graph expected in:(.*?)==== END GRAPH EXPECTED ====" , ms_text, re.DOTALL)
263
+ mapping = {}
264
+ if matches:
265
+ for line in matches[0].splitlines():
266
+ m = re.match(r"-\s*Question\s*(\d+)\s*[\u2192\-\:]\s*Page\s*(\d+)", line.strip())
267
+ if m:
268
+ mapping[int(m.group(1))] = int(m.group(2))
269
+ return mapping
270
+
271
+ def extract_graph_answers_from_as(as_text):
272
+ """
273
+ Parse LLM output for Answer Sheet to extract answers/pages where a graph was found.
274
+ Returns dict: {answer_number: as_page_number}
275
+ """
276
+ matches = re.findall(r"==== GRAPH FOUND ANSWERS ====\\s*Graph found in:(.*?)==== END GRAPH FOUND ====" , as_text, re.DOTALL)
277
+ mapping = {}
278
+ if matches:
279
+ for line in matches[0].splitlines():
280
+ m = re.match(r"-\s*Answer\s*(\d+)\s*[\u2192\-\:]\s*Page\s*(\d+)", line.strip())
281
+ if m:
282
+ mapping[int(m.group(1))] = int(m.group(2))
283
+ return mapping
284
 
285
  def extract_marks_from_grading(grading_text):
286
  """
 
320
  You are an exam marker. Your role is to identify where each question begins on the page.
321
  The page is divided into a {rows} x {cols} grid. Each cell has a RUNNING NUMBER label (1..{rows*cols}).
322
  For each question in the grading JSON, return the cell NUMBER where the FIRST STEP of that question begins.
 
323
  IMPORTANT: Only spot and return cell numbers for the following question IDs (one per line):
324
  {ids_block}
325
  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).
 
483
  print("πŸ“‘ Imprinted PDF saved to:", compressed)
484
  return compressed
485
 
486
+ # ---------------- GRAPH DETECTION HELPERS ----------------
487
+ # These functions are now robustly handled by the new_code, so they are no longer needed here.
488
+
489
+ # ---------------- PIPELINE UPDATE FOR GRAPH-AWARE GRADING ----------------
490
+ def align_and_grade_pipeline(qp_path, ms_path, ans_path, imprint=False):
491
+ """
492
+ Final pipeline implementing requested flow and verbose console logging.
493
+ Now includes Graph-Aware Grading logic.
494
+ """
495
+ try:
496
+ print("πŸ” Starting pipeline...")
497
+ # Step 0: compress as needed
498
+ qp_path = compress_pdf(qp_path)
499
+ ms_path = compress_pdf(ms_path)
500
+ ans_path = compress_pdf(ans_path)
501
+
502
+ # Merge QP + MS
503
+ merged_qpms_path = os.path.splitext(qp_path)[0] + "_merged_qp_ms.pdf"
504
+ merge_pdfs([qp_path, ms_path], merged_qpms_path)
505
+ print("πŸ“Ž Merged QP + MS ->", merged_qpms_path)
506
+
507
+ # Upload files to Gemini
508
+ print("πŸ”Ό Uploading files to Gemini...")
509
+ merged_uploaded = genai.upload_file(path=merged_qpms_path, display_name="QP+MS (merged)")
510
+ ans_uploaded = genai.upload_file(path=ans_path, display_name="Answer Sheet")
511
+ print("βœ… Upload complete.")
512
+
513
+ # Create model and print which selected
514
+ model = create_model()
515
+
516
+ # Step 1.i: QP+MS transcription (first)
517
+ print("1.i) Transcribing QP+MS (questions first, then full markscheme, with graph detection)...")
518
+ qpms_prompt = PROMPTS["QP_MS_TRANSCRIPTION"]["content"] + "\nAt the end, also list all questions in the markscheme where a graph is expected, in the format:\nGraph expected in:\n- Question <number> β†’ Page <number>\n(One per line, after ==== MARKSCHEME END ====)"
519
+ qpms_text = gemini_generate_content(model, qpms_prompt, file_upload_obj=merged_uploaded)
520
+ print("πŸ“„ QP+MS transcription received. Saving debug file: debug_qpms_transcript.txt")
521
+ with open("debug_qpms_transcript.txt", "w", encoding="utf-8") as f:
522
+ f.write(qpms_text)
523
+
524
+ # Step 1.i.a: Extract graph-expected questions from MS
525
+ ms_graph_mapping = extract_graph_questions_from_ms(qpms_text)
526
+ print("πŸ–ΌοΈ Graph-expected questions in MS:", ms_graph_mapping)
527
+
528
+ # Step 2: extract serial numbers (question IDs) using regex from qpms_text
529
+ extracted_ids = extract_question_ids_from_qpms(qpms_text)
530
+ if not extracted_ids:
531
+ extracted_ids = ["NA"]
532
+
533
+ # Step 1.ii: Build AS prompt injecting extracted IDs and transcribe AS
534
+ print("1.ii) Building AS transcription prompt with expected question IDs and graph detection, sending to Gemini...")
535
+ as_prompt = build_as_prompt_with_expected_ids(extracted_ids) + "\nAt the end, also list all answers where a graph is found, in the format:\nGraph found in:\n- Answer <number> β†’ Page <number>\n(One per line, after all answers)"
536
+ as_text = gemini_generate_content(model, as_prompt, file_upload_obj=ans_uploaded)
537
+ print("πŸ“ AS transcription received. Saving debug file: debug_as_transcript.txt")
538
+ with open("debug_as_transcript.txt", "w", encoding="utf-8") as f:
539
+ f.write(as_text)
540
+
541
+ # Step 2.a: Extract graph-attempted answers from AS
542
+ as_graph_mapping = extract_graph_answers_from_as(as_text)
543
+ print("πŸ–ΌοΈ Graph-attempted answers in AS:", as_graph_mapping)
544
+
545
+ # Step 3: Graph Matching
546
+ graph_bundles = []
547
+ for ans_num, as_page in as_graph_mapping.items():
548
+ if ans_num in ms_graph_mapping:
549
+ graph_bundles.append({
550
+ "question": ans_num,
551
+ "ms_page": ms_graph_mapping[ans_num],
552
+ "as_page": as_page
553
+ })
554
+ print("πŸ”— Graph bundles for grading:", graph_bundles)
555
+
556
+ # Step 4: Grading - send both transcripts to grading model, inject graph bundle info
557
+ print("2) Preparing grading input and sending to Gemini for grading...")
558
+ grading_input = (
559
+ "=== QP+MS TRANSCRIPT BEGIN ===\n"
560
+ + qpms_text
561
+ + "\n=== QP+MS TRANSCRIPT END ===\n\n"
562
+ + "=== ANSWER SHEET TRANSCRIPT BEGIN ===\n"
563
+ + as_text
564
+ + "\n=== ANSWER SHEET TRANSCRIPT END ===\n"
565
+ )
566
+ # Inject graph bundle note
567
+ if graph_bundles:
568
+ graph_note = "\n\n---\nFor the following questions, a graph was expected and the student attempted it. Please use the provided images for grading these questions:\n"
569
+ for bundle in graph_bundles:
570
+ graph_note += f"- Question {bundle['question']}:\n - Markscheme graph (Page {bundle['ms_page']})\n - Student’s graph (Page {bundle['as_page']})\n"
571
+ graph_note += "\nGrade these with visual context. For all other questions, proceed as usual.\n---\n"
572
+ grading_input += graph_note
573
+
574
+ grading_prompt_system = PROMPTS["GRADING_PROMPT"]["content"]
575
+ grading_text = gemini_generate_content(model, grading_prompt_system + "\n\nPlease grade the following transcripts:\n" + grading_input)
576
+ print("🧾 Grading output received. Saving debug file: debug_grading.md")
577
+ with open("debug_grading.md", "w", encoding="utf-8") as f:
578
+ f.write(grading_text)
579
+
580
+ # Save grading PDF
581
+ base_name = os.path.splitext(os.path.basename(ans_path))[0]
582
+ grading_pdf_path = save_as_pdf(grading_text, f"{base_name}_graded.pdf")
583
+ print("πŸ“„ Grading PDF saved:", grading_pdf_path)
584
+
585
+ # Step 4: Extract marks for imprinting
586
+ grading_json = extract_marks_from_grading(grading_text)
587
+ with open("debug_grading_json.json", "w", encoding="utf-8") as f:
588
+ json.dump(grading_json, f, indent=2, ensure_ascii=False)
589
+ print("πŸ”§ Grading marks extraction complete.")
590
+
591
+ imprinted_pdf_path = None
592
+ if imprint:
593
+ print("✍ Imprint option enabled. Starting imprinting process (parallel mapping requests)...")
594
+ imprinted_pdf_path = f"{base_name}_imprinted.pdf"
595
+ imprinted_pdf_path = imprint_marks_using_mapping(ans_path, grading_json, imprinted_pdf_path, model, extracted_ids)
596
+ print("βœ… Imprinting finished. Imprinted PDF at:", imprinted_pdf_path)
597
+
598
+ print("🏁 Pipeline finished successfully.")
599
+ return qpms_text, as_text, grading_text, grading_pdf_path, imprinted_pdf_path
600
+
601
+ except Exception as e:
602
+ print("❌ Pipeline error:", e)
603
+ return f"❌ Error: {e}", None, None, None, None
604
 
605
  # ---------------- GRADIO UI ----------------
606
  with gr.Blocks(title="LeadIB AI Grading (Final Flow β€” Verbose)") as demo: