atz21 commited on
Commit
2d32051
·
verified ·
1 Parent(s): 305575c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +188 -176
app.py CHANGED
@@ -1,229 +1,241 @@
1
  # app.py
 
 
 
2
  import os
3
- import gradio as gr
4
- import PyPDF2
5
  import traceback
 
6
 
7
  try:
8
  import google.generativeai as genai
9
- except Exception:
10
  genai = None
11
 
12
- # ---------- Configuration ---------------------------------------------------
 
 
13
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", None)
14
- MODEL_NAME = "gemini-2.5-pro" # change if needed
15
-
16
- if genai and GEMINI_API_KEY:
17
- try:
18
  genai.configure(api_key=GEMINI_API_KEY)
19
- # instantiate model object (older SDK style)
20
- model = genai.GenerativeModel(MODEL_NAME)
21
- except Exception as e:
22
- print("Warning: could not configure genai:", e)
23
- model = None
24
  else:
25
- model = None
26
-
27
- # ---------- Utilities -------------------------------------------------------
28
- def extract_text_from_pdf(file_obj) -> str:
29
- """
30
- Extract text from a PDF file-like object using PyPDF2.
31
- file_obj is a file-like object (what Gradio File provides).
32
- """
33
- try:
34
- # PyPDF2 PdfReader can read file-like objects
35
- reader = PyPDF2.PdfReader(file_obj)
36
- pages = []
37
- for p in reader.pages:
38
- text = p.extract_text()
39
- if text:
40
- pages.append(text)
41
- return "\n\n".join(pages).strip()
42
- except Exception as e:
43
- # fallback: try to read raw bytes and decode (not ideal)
44
- try:
45
- file_obj.seek(0)
46
- raw = file_obj.read()
47
- # best-effort decode
48
- return raw.decode(errors="ignore")
49
- except Exception:
50
- return f"[Error extracting text: {e}]"
51
 
52
- # ---------- Prompt templates ------------------------------------------------
53
  TRANSCRIPTION_INSTRUCTIONS = """
54
- You are an expert transcriber. Cleanly transcribe the student's answer sheet contained below.
55
- Rules:
56
- 1. Keep section headings as Markdown headings (e.g., ## Question 1).
57
- 2. Render any mathematical notation using LaTeX between $...$ for inline or $$...$$ for display.
58
- 3. Preserve numbering and sub-numbering (a), (i), etc.
59
- 4. If handwriting or characters are illegible or missing, mark them as [???] inline.
60
- 5. Normalize spacing, remove repeated hyphens/headers from PDF conversion noise.
61
- 6. For any short answer where student left blank, write [BLANK].
62
- 7. Output ONLY the transcription in well-formatted Markdown with LaTeX where appropriate.
63
- 8. Keep the transcription faithful; do not "correct" student's conceptual errors.
 
 
 
 
 
64
  """
65
 
 
66
  GRADING_INSTRUCTIONS = """
67
- You are an experienced examiner. Use the Question Paper (QP), the Marking Scheme (MS), and the STUDENT TRANSCRIPTION to grade the student's answers.
68
- Rules:
69
- 1. Follow the MS strictly: allocate marks per the marking scheme and apply fractional marks when indicated.
70
- 2. If the student's answer is missing or [BLANK], award 0 marks for that part unless MS instructs otherwise.
71
- 3. When partial credit applies, explain what was missing and why partial marks were given.
72
- 4. If the student copied the question or gave an irrelevant answer, award 0 and add a brief reason.
73
- 5. Use negative marking only if MS explicitly instructs it.
74
- 6. Output the grading result as a JSON object ONLY (no extra commentary) with the following structure:
75
-
76
- {
77
- "total_marks": <int>,
78
- "marks_obtained": <int>,
79
- "percentage": <float>,
80
- "per_question": {
81
- "Q1": {"max_marks": <int>, "awarded": <int>, "notes": "<string>"},
82
- "Q2": {...}
83
- },
84
- "high_level_feedback": "<short summary feedback to student (1-3 sentences)>"
85
- }
86
-
87
- Make sure numeric fields are numeric (not strings). Use plain JSON (no markdown fences).
 
88
  """
89
 
90
- # ---------- Model functions -------------------------------------------------
91
- def call_gemini(prompt: str, system: str = None, max_tokens: int = 1024):
 
 
 
 
 
 
92
  """
93
- Call the Gemini model (if configured). Returns model text.
94
- If model not available, raise or return an error string.
95
  """
96
- if model is None:
97
- raise RuntimeError("Gemini model is not configured. Set GEMINI_API_KEY and install google-generativeai.")
98
- # generate_content expects a string prompt (or list). We'll call synchronously.
 
 
 
 
 
99
  try:
100
- # Compose contents: system instruction optionally and prompt
101
- contents = []
102
- if system:
103
- contents.append(system)
104
- contents.append(prompt)
105
- resp = model.generate_content(contents)
106
- # Many SDK responses have .text attribute
107
- text = getattr(resp, "text", None)
108
- if text is None:
109
- # try to string-concat chunks or .content
110
- text = str(resp)
111
- return text
 
 
 
 
 
 
 
 
 
 
 
 
112
  except Exception as e:
113
- # bubble up a helpful message
114
- raise RuntimeError(f"Error calling Gemini: {e}\n{traceback.format_exc()}")
115
 
116
- # ---------- Gradio app functions -------------------------------------------
117
- def transcribe_step(question_pdf, scheme_pdf, answer_pdf):
118
  """
119
- Extract text and run transcription prompt. Returns transcription text and a state dict.
 
120
  """
121
- # check files present
122
- if not (question_pdf and scheme_pdf and answer_pdf):
123
- return "Please upload all three PDFs (Question Paper, Marking Scheme, Answer Sheet).", None
124
-
125
- # read file-like objects (gradio provides TemporaryFile-like objects)
126
  try:
127
- question_pdf.file.seek(0)
128
- q_text = extract_text_from_pdf(question_pdf.file)
 
 
 
 
 
 
 
 
 
 
 
 
129
  except Exception as e:
130
- q_text = f"[Error reading Question Paper PDF: {e}]"
131
 
 
 
 
 
 
 
132
  try:
133
- scheme_pdf.file.seek(0)
134
- ms_text = extract_text_from_pdf(scheme_pdf.file)
135
  except Exception as e:
136
- ms_text = f"[Error reading Marking Scheme PDF: {e}]"
137
 
138
  try:
139
- answer_pdf.file.seek(0)
140
- ans_text = extract_text_from_pdf(answer_pdf.file)
 
 
 
141
  except Exception as e:
142
- ans_text = f"[Error reading Answer Sheet PDF: {e}]"
 
143
 
144
- # If model is available, run transcription prompt; else return extracted raw text
145
- if model:
146
- transcription_prompt = TRANSCRIPTION_INSTRUCTIONS + "\n\n" + "ANSWER SHEET CONTENT (begin):\n" + ans_text + "\n\n(END of answer sheet)"
147
- try:
148
- transcription = call_gemini(transcription_prompt, system="You are a precise transcription assistant.", max_tokens=2000)
149
- except Exception as e:
150
- transcription = f"[Gemini transcription failed: {e}]\n\nFalling back to raw extracted text:\n\n" + ans_text
151
- else:
152
- transcription = "[Gemini not configured — showing best-effort extracted text]\n\n" + ans_text
153
-
154
- # state to carry forward
155
- state = {
156
- "q_text": q_text,
157
- "ms_text": ms_text,
158
- "ans_text": ans_text,
159
- "transcription": transcription
160
- }
161
- return transcription, state
162
-
163
- def grade_step(state):
164
  """
165
- Use the state produced by transcribe_step to call grading prompt.
166
  """
167
- if state is None:
168
- return "No transcription state found. Run the Transcribe step first."
 
 
169
 
170
- q_text = state.get("q_text", "")
171
- ms_text = state.get("ms_text", "")
172
- transcription = state.get("transcription", "")
173
 
174
- if model:
 
 
 
 
 
 
175
  grading_prompt = (
176
- GRADING_INSTRUCTIONS
177
- + "\n\nQUESTION PAPER (begin):\n" + q_text + "\n\nQUESTION PAPER (end)\n\n"
178
- + "MARKING SCHEME (begin):\n" + ms_text + "\n\nMARKING SCHEME (end)\n\n"
179
- + "STUDENT TRANSCRIPTION (begin):\n" + transcription + "\n\nSTUDENT TRANSCRIPTION (end)\n\n"
180
- + "Produce the JSON grading result now."
 
 
 
 
181
  )
182
- try:
183
- grading_json = call_gemini(grading_prompt, system="You are an expert examiner and must respond only with the requested JSON.", max_tokens=2000)
184
- except Exception as e:
185
- grading_json = f"[Gemini grading failed: {e}]\n\n"
186
- else:
187
- grading_json = "[Gemini not configured — grading unavailable.]\n\nPlease set GEMINI_API_KEY to enable grading."
188
 
189
- return grading_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- # ---------- Gradio UI ------------------------------------------------------
192
- with gr.Blocks(title="Transcribe & Grade — Exam Papers") as demo:
193
- gr.Markdown("## Upload: Question Paper, Marking Scheme, Answer Sheet (PDFs)")
194
  with gr.Row():
195
  qp_in = gr.File(label="Question Paper (PDF)", file_count="single", type="file")
196
  ms_in = gr.File(label="Marking Scheme (PDF)", file_count="single", type="file")
197
  ans_in = gr.File(label="Answer Sheet (PDF)", file_count="single", type="file")
198
 
199
- trans_btn = gr.Button("Transcribe Answer Sheet")
200
- transcription_out = gr.Textbox(lines=20, label="Transcription (Markdown + LaTeX)", interactive=False)
201
-
202
- state_store = gr.State(value=None)
203
-
204
- def _on_transcribe(qp, ms, ans, _state):
205
- trans, new_state = transcribe_step(qp, ms, ans)
206
- return trans, new_state
207
-
208
- trans_btn.click(_on_transcribe, inputs=[qp_in, ms_in, ans_in, state_store], outputs=[transcription_out, state_store])
209
 
210
- gr.Markdown("## Grading")
211
- grade_btn = gr.Button("Grade from Transcription")
212
- grading_out = gr.Textbox(lines=20, label="Grading Result (JSON)", interactive=False)
213
 
214
- def _on_grade(_state):
215
- return grade_step(_state)
 
 
216
 
217
- grade_btn.click(_on_grade, inputs=[state_store], outputs=[grading_out])
 
 
 
 
218
 
219
- gr.Markdown("### Notes")
220
- gr.Markdown(
221
- "- First click **Transcribe Answer Sheet**. Review the transcription output.\n"
222
- "- Then click **Grade from Transcription** to produce the JSON grading result.\n"
223
- "- If you see messages about Gemini not being configured, set `GEMINI_API_KEY` in your environment and restart the app.\n"
224
- "- Adjust `MODEL_NAME` at the top of this file if you want a different Gemini model."
225
- )
226
 
227
- # ---------- Run -----------------------------------------------------------
228
  if __name__ == "__main__":
229
- demo.launch(share=False, server_name="0.0.0.0", server_port=7860)
 
1
  # app.py
2
+ # Gradio app for transcription + grading using Google Gemini
3
+ # Author: generated for your notebook logic (adapted and sanitized)
4
+
5
  import os
6
+ import tempfile
7
+ import io
8
  import traceback
9
+ import gradio as gr
10
 
11
  try:
12
  import google.generativeai as genai
13
+ except Exception as e:
14
  genai = None
15
 
16
+ # ---- Configuration ----
17
+ # IMPORTANT: Do NOT hardcode your API key here.
18
+ # Set environment variable GEMINI_API_KEY in Hugging Face Spaces Secrets.
19
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", None)
20
+ if GEMINI_API_KEY:
21
+ if genai is not None:
 
 
22
  genai.configure(api_key=GEMINI_API_KEY)
 
 
 
 
 
23
  else:
24
+ # genai may be None if package not installed; Gradio UI will show an error if user tries to run
25
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # ---- Long instructions copied-from-notebook (transcription) ----
28
  TRANSCRIPTION_INSTRUCTIONS = """
29
+ Persona:
30
+ You are an expert transcriptionist specializing in scientific and mathematical documents. Your primary goal is to convert handwritten mathematical work into a perfectly formatted, machine-readable Markdown document using LaTeX for all mathematical notation.
31
+ Core Task:
32
+ Your task is to transcribe the provided handwritten student solutions into a single, clean Markdown string.
33
+ Key Directives & Rules:
34
+ Absolute Fidelity: Transcribe exactly what is written. Do NOT correct mathematical errors, logical fallacies, or spelling mistakes. Your role is purely that of a scribe, not a grader or editor.
35
+ LaTeX for All Math: All mathematical content—including single variables, numbers in equations, fractions, exponents, roots, and symbols—must be enclosed in LaTeX delimiters. Use inline $ ... $ for math within text and block $$ ... $$ for standalone equations.
36
+ Handle Strikethroughs: Completely ignore and omit any text, numbers, or expressions that have been struck through by the student. Do not include them in the final output.
37
+ Preserve Structure:
38
+ Use Markdown bolding (e.g., **1.**, **2a.**) to clearly separate each question or sub-part.
39
+ Maintain the vertical, step-by-step flow of the student's derivations. For multi-line aligned equations, use the \\begin{align*} ... \\end{align*} environment within a $$ ... $$ block.
40
+ Handle Ambiguity: If a character or symbol is genuinely illegible or ambiguous, make your best interpretation and enclose it in square brackets. For example, if a variable could be u or v, write [u?].
41
+ Output Format:
42
+ The final output must be a single Markdown string.
43
+ Ensure all LaTeX renders correctly and the structure is clean and readable.
44
  """
45
 
46
+ # ---- Grading system instructions (as in notebook) ----
47
  GRADING_INSTRUCTIONS = """
48
+ Instructions to Examiners:
49
+ Abbreviations:
50
+ - M: Marks for correct Method.
51
+ - A: Marks for Answer or Accuracy (often depends on preceding M mark).
52
+ - R: Marks for clear Reasoning.
53
+ - AG: Answer given in the question; no marks awarded.
54
+ - FT: Follow Through; award marks for correct method/answer using incorrect earlier results.
55
+
56
+ Marking Rules:
57
+ 1. Always follow the markscheme annotations (M1, A2, etc.).
58
+ 2. M marks must be earned before dependent A marks are awarded (no M0 followed by A1 unless explicitly allowed).
59
+ 3. If M and A marks are on the same line (e.g., M1A1), M is for the method attempt, A is for correct values.
60
+ 4. Multiple A marks on the same line are awarded independently unless otherwise noted.
61
+ 5. Do not split M2, A3, etc. unless instructed.
62
+ 6. "Show that" responses do not need to restate the AG line unless noted.
63
+ 7. Once a correct answer is seen, ignore further incorrect working unless it affects a later part (then apply FT as appropriate).
64
+ 8. Do not award the final A mark if an incorrect approximation is used in the same part.
65
+
66
+ Error Avoidance:
67
+ - No incorrect mark allocation: Do not award marks unless they are explicitly justified by the markscheme.
68
+ - No misclassification of errors: Distinguish correctly between "Conceptual Errors" and "Silly Mistakes."
69
+ - Follow markscheme logic exactly: Especially regarding when to withhold accuracy marks if method marks are not earned.
70
  """
71
 
72
+ # ---- Helper functions ----
73
+ def ensure_genai_available():
74
+ if genai is None:
75
+ raise RuntimeError("google-generativeai package is not available. Make sure it's in requirements.txt.")
76
+ if not GEMINI_API_KEY:
77
+ raise RuntimeError("GEMINI_API_KEY not set. Set it in environment/secrets before running the app.")
78
+
79
+ def _save_temp_file(uploaded_file) -> str:
80
  """
81
+ uploaded_file is a file-like object provided by Gradio (temp file path).
82
+ Returns a path to a saved temp file we can pass to genai.upload_file.
83
  """
84
+ if uploaded_file is None:
85
+ raise ValueError("No file provided.")
86
+ # Gradio gives a dict with 'name' and 'data' in some modes; but usually it's a path
87
+ # Attempt to handle multiple types robustly
88
+ if isinstance(uploaded_file, str):
89
+ return uploaded_file # already a path
90
+ # Otherwise write bytes to a temp file
91
+ data = None
92
  try:
93
+ # uploaded_file may be a file-like with .read()
94
+ data = uploaded_file.read()
95
+ except Exception:
96
+ # uploaded_file may be a tuple returned by gr.File: (name, data)
97
+ try:
98
+ data = uploaded_file[0].read()
99
+ except Exception:
100
+ raise
101
+ fd, path = tempfile.mkstemp(suffix=".pdf")
102
+ os.close(fd)
103
+ with open(path, "wb") as f:
104
+ f.write(data)
105
+ return path
106
+
107
+ def upload_file_to_gemini(local_path, display_name="file"):
108
+ """
109
+ Upload a local file path to Gemini using genai.upload_file and return the file object (as returned).
110
+ """
111
+ ensure_genai_available()
112
+ # The API used in original notebook: genai.upload_file(path=...)
113
+ # We'll use the same call and return the object
114
+ try:
115
+ file_obj = genai.upload_file(path=local_path, display_name=display_name)
116
+ return file_obj
117
  except Exception as e:
118
+ # Surface the error
119
+ raise RuntimeError(f"Failed to upload file to Gemini: {e}")
120
 
121
+ def call_gemini_generate(inputs_list):
 
122
  """
123
+ Call Gemini generative model with the provided inputs list (strings and/or uploaded file objects).
124
+ Returns the textual content (tries several extraction methods).
125
  """
126
+ ensure_genai_available()
 
 
 
 
127
  try:
128
+ model = genai.GenerativeModel("gemini-2.5-pro", generation_config={"temperature": 0})
129
+ response = model.generate_content(inputs_list)
130
+ text = getattr(response, "text", None)
131
+ if not text:
132
+ # try legacy path
133
+ if hasattr(response, "candidates") and response.candidates:
134
+ # drill into candidates
135
+ try:
136
+ text = response.candidates[0].content.parts[0].text
137
+ except Exception:
138
+ text = str(response.candidates[0])
139
+ if not text:
140
+ text = str(response)
141
+ return text
142
  except Exception as e:
143
+ raise RuntimeError(f"Gemini generation failed: {e}")
144
 
145
+ # ---- Core operations ----
146
+ def transcribe_answer_sheet(answersheet_file):
147
+ """
148
+ Save the uploaded answersheet, upload to Gemini, and request transcription.
149
+ Returns the transcription string.
150
+ """
151
  try:
152
+ ensure_genai_available()
 
153
  except Exception as e:
154
+ return f"ERROR: {e}"
155
 
156
  try:
157
+ local_ans_path = _save_temp_file(answersheet_file)
158
+ uploaded_ans = upload_file_to_gemini(local_ans_path, display_name="Answer Sheet")
159
+ # Call Gemini to transcribe (instructions + uploaded file)
160
+ response_text = call_gemini_generate([TRANSCRIPTION_INSTRUCTIONS, uploaded_ans])
161
+ return response_text
162
  except Exception as e:
163
+ tb = traceback.format_exc()
164
+ return f"Transcription failed: {e}\n\n{tb}"
165
 
166
+ def grade_answer(qp_file, ms_file, transcription_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  """
168
+ Upload QP and MS, then call Gemini with grading instructions + the transcription to obtain grading output.
169
  """
170
+ try:
171
+ ensure_genai_available()
172
+ except Exception as e:
173
+ return f"ERROR: {e}"
174
 
175
+ if transcription_text is None or transcription_text.strip() == "":
176
+ return "ERROR: Empty transcription. Please run transcription first or provide transcription text."
 
177
 
178
+ try:
179
+ local_qp = _save_temp_file(qp_file)
180
+ local_ms = _save_temp_file(ms_file)
181
+ uploaded_qp = upload_file_to_gemini(local_qp, display_name="Question Paper")
182
+ uploaded_ms = upload_file_to_gemini(local_ms, display_name="Marking Scheme")
183
+
184
+ # Build the prompt combining grading instructions + strict rules (as in the notebook)
185
  grading_prompt = (
186
+ "You are an official examiner. Use the following grading system and rules to assess the answers:\n\n"
187
+ + GRADING_INSTRUCTIONS
188
+ + "\n\nYour output must:\n"
189
+ "1. Apply marks exactly as per the markscheme.\n"
190
+ "2. Justify each awarded or withheld mark with reference to the grading rules.\n"
191
+ "3. Identify and classify all errors accurately (Conceptual Error, Silly Mistake, or None).\n"
192
+ "4. Follow the dependency between M and A marks strictly.\n"
193
+ "5. Avoid giving marks that the markscheme does not allow.\n"
194
+ "6. Provide a step-by-step reasoning for each mark awarded or withheld, explaining your thought process clearly.\n"
195
  )
 
 
 
 
 
 
196
 
197
+ response_text = call_gemini_generate([grading_prompt, uploaded_qp, uploaded_ms, transcription_text])
198
+ return response_text
199
+ except Exception as e:
200
+ tb = traceback.format_exc()
201
+ return f"Grading failed: {e}\n\n{tb}"
202
+
203
+ # ---- Gradio UI ----
204
+ with gr.Blocks(title="Exam Transcription & Grading (Gemini)") as demo:
205
+ gr.Markdown(
206
+ """
207
+ # Exam Transcription & Grading
208
+ Upload three PDFs: Question Paper, Marking Scheme, and Answer Sheet.
209
+ Click **Transcribe** to get a LaTeX-friendly Markdown transcription of the student's handwritten answers.
210
+ Click **Grade** to apply the marking scheme to the transcription and get a detailed grading justification.
211
+ **Important:** set `GEMINI_API_KEY` in environment/secrets before using.
212
+ """
213
+ )
214
 
 
 
 
215
  with gr.Row():
216
  qp_in = gr.File(label="Question Paper (PDF)", file_count="single", type="file")
217
  ms_in = gr.File(label="Marking Scheme (PDF)", file_count="single", type="file")
218
  ans_in = gr.File(label="Answer Sheet (PDF)", file_count="single", type="file")
219
 
220
+ with gr.Row():
221
+ transcribe_btn = gr.Button("Transcribe Answer Sheet")
222
+ grade_btn = gr.Button("Grade (use existing transcription)")
 
 
 
 
 
 
 
223
 
224
+ transcription_out = gr.Textbox(label="Transcription (Markdown + LaTeX)", lines=20)
225
+ grading_out = gr.Textbox(label="Grading Result + Justification", lines=20)
 
226
 
227
+ # Wire buttons
228
+ transcribe_btn.click(fn=transcribe_answer_sheet, inputs=[ans_in], outputs=[transcription_out])
229
+ # Grade uses QP, MS and transcription textbox as inputs
230
+ grade_btn.click(fn=grade_answer, inputs=[qp_in, ms_in, transcription_out], outputs=[grading_out])
231
 
232
+ # Provide quick example text area for transcription override (optional)
233
+ gr.Markdown("If you already have a prepared transcription (or want to edit before grading), paste it below and click Grade.")
234
+ transcription_manual = gr.Textbox(label="Optional: Edit/Provide Transcription (overrides auto)", lines=8)
235
+ grade_with_manual_btn = gr.Button("Grade Using Provided Transcription")
236
+ grade_with_manual_btn.click(fn=grade_answer, inputs=[qp_in, ms_in, transcription_manual], outputs=[grading_out])
237
 
238
+ gr.Markdown("⚠️ Note: This app depends on Google Gemini `google-generativeai` SDK and a valid `GEMINI_API_KEY` environment variable.")
 
 
 
 
 
 
239
 
 
240
  if __name__ == "__main__":
241
+ demo.launch()