| import streamlit as st |
| import pandas as pd |
| import pytesseract |
| from PIL import Image |
| from rapidfuzz import fuzz, utils |
| import io |
|
|
| |
| st.set_page_config(page_title="AI Student Grader", layout="wide") |
| st.title("π AI Student Answer Grader") |
| st.markdown("Upload answer sheets and an answer key to automatically calculate marks.") |
|
|
| |
| st.sidebar.header("Grading Settings") |
| accuracy_threshold = st.sidebar.slider("Minimum Accuracy Threshold (%)", 0, 100, 70) |
| marks_per_question = st.sidebar.number_input("Marks per correct answer", value=1.0) |
|
|
| |
| def perform_ocr(image): |
| """Extracts text from an uploaded image.""" |
| img = Image.open(image) |
| |
| text = pytesseract.image_to_string(img) |
| return text.strip() |
|
|
| def compare_answers(student_text, answer_key, threshold): |
| """ |
| Compares student text with answer key using Fuzzy Matching. |
| DeepSeek-R1 style logic: We look for the presence of key concepts. |
| """ |
| |
| |
| score = 0 |
| key_lines = [line.strip() for line in answer_key.split('\n') if line.strip()] |
| student_lines = [line.strip() for line in student_text.split('\n') if line.strip()] |
| |
| details = [] |
| |
| for i, correct_ans in enumerate(key_lines): |
| match_found = False |
| highest_match = 0 |
| |
| |
| for s_line in student_lines: |
| similarity = fuzz.token_set_ratio(correct_ans, s_line) |
| if similarity > highest_match: |
| highest_match = similarity |
| |
| if highest_match >= threshold: |
| score += marks_per_question |
| match_found = True |
| |
| details.append({ |
| "Question": i + 1, |
| "Match %": round(highest_match, 2), |
| "Status": "Correct" if match_found else "Incorrect" |
| }) |
| |
| return score, details |
|
|
| |
| col1, col2 = st.columns(2) |
|
|
| with col1: |
| st.subheader("1. Reference Answer Key") |
| key_input_type = st.radio("Key Format", ["Text Input", "Upload Image"]) |
| |
| if key_input_type == "Text Input": |
| answer_key_text = st.text_area("Paste the correct answers (one per line):") |
| else: |
| key_img = st.file_uploader("Upload Answer Key Image", type=['png', 'jpg', 'jpeg']) |
| if key_img: |
| answer_key_text = perform_ocr(key_img) |
| st.text_area("Extracted Key (Edit if needed):", value=answer_key_text) |
|
|
| with col2: |
| st.subheader("2. Student Answer Sheets") |
| student_images = st.file_uploader("Upload Student Images (Max 5)", type=['png', 'jpg', 'jpeg'], accept_multiple_files=True) |
|
|
| |
| if st.button("Calculate Marks"): |
| if not answer_key_text or not student_images: |
| st.error("Please provide both the answer key and student images.") |
| else: |
| results = [] |
| |
| progress_bar = st.progress(0) |
| for idx, img_file in enumerate(student_images): |
| |
| extracted_text = perform_ocr(img_file) |
| |
| |
| score, details = compare_answers(extracted_text, answer_key_text, accuracy_threshold) |
| |
| |
| results.append({ |
| "Student Name": img_file.name.split('.')[0], |
| "Raw Score": score, |
| "Final Marks": f"{score}/{len(answer_key_text.splitlines()) * marks_per_question}", |
| "Match Percentage": f"{accuracy_threshold}%" |
| }) |
| progress_bar.progress((idx + 1) / len(student_images)) |
|
|
| |
| df = pd.DataFrame(results) |
| st.subheader("π Results Overview") |
| st.table(df) |
|
|
| |
| output = io.BytesIO() |
| with pd.ExcelWriter(output, engine='openpyxl') as writer: |
| df.to_excel(writer, index=False, sheet_name='Grades') |
| |
| st.download_button( |
| label="π₯ Download Excel Sheet", |
| data=output.getvalue(), |
| file_name="student_grades.xlsx", |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
| ) |