File size: 9,167 Bytes
fa2ac16
 
 
02d8496
0b422e6
fa2ac16
 
40d9691
 
 
3b4b98e
 
40d9691
 
 
3b4b98e
 
 
 
 
 
 
 
74c1aa8
3b4b98e
 
 
74c1aa8
3b4b98e
74c1aa8
 
 
3b4b98e
74c1aa8
 
 
 
 
 
3b4b98e
 
 
 
74c1aa8
3b4b98e
74c1aa8
3b4b98e
02d8496
40d9691
1091f22
 
 
3b4b98e
d5a9c21
 
1091f22
 
 
 
 
 
 
d5a9c21
3b4b98e
1091f22
 
 
 
 
3b4b98e
 
 
d5a9c21
 
1091f22
d5a9c21
1091f22
d5a9c21
 
 
3b4b98e
d5a9c21
 
 
3b4b98e
d5a9c21
 
3b4b98e
d5a9c21
 
1091f22
 
d5a9c21
1091f22
 
3b4b98e
d5a9c21
 
1091f22
3b4b98e
1091f22
d5a9c21
 
 
 
 
3b4b98e
d5a9c21
 
 
 
 
1091f22
3b4b98e
1091f22
d5a9c21
3b4b98e
1091f22
3b4b98e
d5a9c21
 
3b4b98e
1091f22
4350cad
1091f22
f44aff8
 
40d9691
 
fa2ac16
40d9691
c755c74
02d8496
 
 
c755c74
fa2ac16
0b422e6
 
 
 
 
 
 
1091f22
0b422e6
fa2ac16
0b422e6
 
 
d5a9c21
0b422e6
 
 
 
 
1091f22
0b422e6
 
1091f22
0b422e6
40d9691
0b422e6
40d9691
 
 
 
 
1091f22
40d9691
 
1091f22
40d9691
 
1091f22
40d9691
 
0b422e6
 
 
c755c74
40d9691
 
 
c755c74
40d9691
7aaa8eb
40d9691
 
 
 
 
 
1091f22
 
 
 
02d8496
 
40d9691
1091f22
02d8496
1091f22
 
 
7aaa8eb
40d9691
1091f22
40d9691
1091f22
40d9691
fa2ac16
40d9691
fa2ac16
 
40d9691
1091f22
fa2ac16
 
 
40d9691
fa2ac16
 
40d9691
 
c755c74
40d9691
 
fa2ac16
c755c74
40d9691
 
 
 
 
 
 
c755c74
 
fa2ac16
 
 
d5a9c21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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()