leadib / app.py
atz21's picture
Update app.py
74c1aa8 verified
import os
import gradio as gr
import google.generativeai as genai
from markdown_pdf import MarkdownPdf, Section
import subprocess
# ---------- PROMPTS ----------
PROMPTS = {
"ALIGNMENT_PROMPT": {
"role": "system",
"content": """Developer: Role: Expert examiner and transcription specialist.
Your objective is to align three sources per question/sub-question:
- Question Paper (QP)
- Markscheme (MS)
- Student Answer Sheet (AS)
## Instructions
1. Carefully parse all documents and align content per question and sub-question.
2. For each question/sub-question, create a structured Markdown block as follows:
---
## Question X [and sub-question if applicable, e.g., ### (b)(ii)]
*QP:* [Exact question text or [Not found]]
*MS:* [Relevant markscheme section or [Not found]]
*AS:* [Final cleaned student answer; use fenced code for mathematics; insert [illegible] or [No response] as required]
---
3. Formatting requirements:
- Use '##' for main questions, '###' for sub-questions.
- Maintain section order: QP | MS | AS (always in that sequence).
- Enclose all mathematical expressions in Markdown fenced code blocks (``` triple backticks).
- If a diagram/graph is omitted, write [Graph omitted] in its place.
- For unreadable portions of the student's answer, insert [illegible]; if the answer is wholly unreadable, set AS to [illegible].
- If a question is skipped or unanswered, AS must be exactly [No response].
- Keep MS annotations (e.g., M1, A1, R1) verbatim.
- Diagrams/graphs are not to be recreated.
- If any QP, MS, or AS content is missing, specify [Not found] for that section.
- Ensure consistency and determinism in formatting so subsequent models can grade directly from this aligned format.
- List all main questions and sub-questions in their original order, clearly denoting sub-questions (e.g., '### (b)(i)', '### (b)(ii)').
After each alignment action, briefly validate that the content for QP, MS, and AS matches expectations and alignments are correct. If validation fails, self-correct or flag the issue.
## Example
---
## Question 1
*QP:* Expand (1+x)^3
*MS:* M1 for binomial expansion, A1 for coefficients, A1 for final form
*AS:* x^3 + 3x^2 + 3x + 1
---
"""
},
"GRADING_PROMPT": {
"role": "system",
"content": """Developer: You are an official examiner. Apply the following grading rules precisely.
## Grading Checklist
- Assess each question part against the provided markscheme.
- Award marks for correct methods (M), accurate answers (A), and clear reasoning (R).
- Use Follow Through (FT) for correctly applied subsequent working using a previous error.
- Always state BOTH:
1. What was wrong (the error).
2. What is right (the correct method/answer from markscheme).
- Summarize total marks and classify error types.
- End with an Examiner’s Report table.
### Abbreviations:
- **M**: Method
- **A**: Accuracy/Answer
- **R**: Reasoning
- **AG**: Answer given (no marks)
- **FT**: Follow Through
---
## Grading Instructions
1. Award marks using official annotations (M1, A1, etc.).
2. A marks generally require valid M marks.
3. Allow FT unless result is nonsensical.
4. Accept valid alternative forms.
5. Apply accuracy requirements (default 3 s.f. if not stated).
6. Ignore crossed-out work unless requested otherwise.
7. Mark only the first full solution unless otherwise indicated.
8. Assume graphs/diagrams are correct if required.
---
## Output Format
Produce a GitHub-flavored Markdown table:
| Student wrote | Marks Awarded | Reason |
|---------------|---------------|--------|
Rules:
- Each row matches a markable step.
- For blanks, write “(no answer)” and indicate lost mark(s).
- Lost marks: wrap in red with `<span style="color:red">A0</span>` (or M0, R0) and make Reason column red. Always also show the correct method/answer.
- Awarded marks remain plain text.
- For partial awards (M1A0A1), highlight only lost marks.
- After each question, show total in square brackets: `[2/4]`.
---
### Examiner’s Report
At the very end, provide a summary table:
Codes:
- A : All Good
- B : Silly Mistake
- C : Conceptual Error
- D : Hard Question
- E : Not Applicable
| Question Number | Marks | Remark |
|-----------------|-------|--------|
| 1 | 6/9 | C |
| 2 | 7/7 | A |
| 3 | 8/14 | D |
| … | … | … |
Then show total clearly:
`Total: 40/61`
Optionally, if reasons are available, extend with:
| Question Number | Marks | Remark | Reason |
|-----------------|-------|--------|--------|
⚠️ Do NOT add any "Validation" or meta commentary. End the output after Examiner’s Report.
"""
}
}
# -------------------- CONFIG --------------------
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
# ---------- HELPER: Save to PDF ----------
def save_as_pdf(text, filename="output.pdf"):
pdf = MarkdownPdf()
pdf.add_section(Section(text, toc=False))
pdf.save(filename)
return filename
# ---------- HELPER: Compress PDF ----------
def compress_pdf(input_path, output_path=None, max_size=20*1024*1024):
if output_path is None:
base, ext = os.path.splitext(input_path)
output_path = f"{base}_compressed{ext}"
if os.path.getsize(input_path) <= max_size:
return input_path
try:
gs_cmd = [
"gs", "-sDEVICE=pdfwrite",
"-dCompatibilityLevel=1.4",
"-dPDFSETTINGS=/ebook",
"-dNOPAUSE", "-dQUIET", "-dBATCH",
f"-sOutputFile={output_path}", input_path
]
subprocess.run(gs_cmd, check=True)
if os.path.getsize(output_path) <= max_size:
print(f"✅ Compressed {input_path}{output_path}")
return output_path
else:
print(f"⚠️ Compression failed to reduce below {max_size/1024/1024} MB")
return input_path
except Exception as e:
print(f"⚠️ Compression error: {e}")
return input_path
# ---------- HELPER: Create Model with Fallback ----------
def create_model():
try:
print("⚡ Using gemini-2.5-pro model")
return genai.GenerativeModel("gemini-2.5-pro", generation_config={"temperature": 0})
except Exception:
print("⚡ Falling back to gemini-2.5-flash model")
return genai.GenerativeModel("gemini-2.5-flash", generation_config={"temperature": 0})
# ---------- PIPELINE: ALIGN + GRADE ----------
def align_and_grade(qp_file, ms_file, ans_file):
try:
qp_file = compress_pdf(qp_file, "qp_compressed.pdf")
ms_file = compress_pdf(ms_file, "ms_compressed.pdf")
ans_file = compress_pdf(ans_file, "ans_compressed.pdf")
qp_uploaded = genai.upload_file(path=qp_file, display_name="Question Paper")
ms_uploaded = genai.upload_file(path=ms_file, display_name="Markscheme")
ans_uploaded = genai.upload_file(path=ans_file, display_name="Answer Sheet")
model = create_model()
resp = model.generate_content([
PROMPTS["ALIGNMENT_PROMPT"]["content"],
qp_uploaded,
ms_uploaded,
ans_uploaded
])
aligned_text = getattr(resp, "text", None)
if not aligned_text and resp.candidates:
aligned_text = resp.candidates[0].content.parts[0].text
aligned_pdf_path = save_as_pdf(aligned_text, "aligned_qp_ms_as.pdf")
response = model.generate_content([
PROMPTS["GRADING_PROMPT"]["content"],
aligned_text
])
grading = getattr(response, "text", None)
if not grading and response.candidates:
grading = response.candidates[0].content.parts[0].text
base_name = os.path.splitext(os.path.basename(ans_file))[0]
grading_pdf_path = save_as_pdf(grading, f"{base_name}_graded.pdf")
return aligned_text, aligned_pdf_path, grading, grading_pdf_path
except Exception as e:
return f"❌ Error: {e}", None, None, None
# ---------- GRADIO APP ----------
with gr.Blocks(title="LeadIB AI Grading (Alignment + Auto-Grading)") as demo:
gr.Markdown("## LeadIB AI Grading\nUpload Question Paper, Markscheme, and Student Answer Sheet.\nThe system will align and grade automatically.")
with gr.Row():
qp_file = gr.File(label="Upload Question Paper (PDF)", type="filepath")
ms_file = gr.File(label="Upload Markscheme (PDF)", type="filepath")
ans_file = gr.File(label="Upload Student Answer Sheet (PDF)", type="filepath")
run_btn = gr.Button("Start Alignment + Auto-Grading")
with gr.Row():
aligned_out = gr.Textbox(label="📄 Aligned QP | MS | AS", lines=20)
aligned_pdf = gr.File(label="⬇️ Download Aligned (PDF)")
with gr.Row():
grading_out = gr.Textbox(label="✅ Grading Report", lines=20)
grading_pdf = gr.File(label="⬇️ Download Grading Report (PDF)")
run_btn.click(
fn=align_and_grade,
inputs=[qp_file, ms_file, ans_file],
outputs=[aligned_out, aligned_pdf, grading_out, grading_pdf],
show_progress=True
)
if __name__ == "__main__":
demo.launch()