# ai_quiz_from_pdf_gradio.py # Gradio app: AI Quiz Generation System from PDF Documents using LLM (Groq: meta-llama/llama-4-maverick-17b-128e-instruct) # # Prerequisites: # - pip install -r requirements.txt # - Set environment variable: GROQ_API_KEY= # # Run: # - python ai_quiz_from_pdf_gradio.py # # Notes: # - The app uses PyMuPDF to extract text from PDFs. # - The LLM is used to generate quizzes (MCQ/TrueFalse/ShortAnswer/Essay) and to grade Essay answers. # - Tabs are used for a clean interface: "Upload & Generate", "Take Quiz", "Grade Essays", "Export". import os import io import json import time from typing import List, Dict, Any, Tuple, Optional import pandas as pd import fitz # PyMuPDF import gradio as gr # --- LLM (Groq) --- from groq import Groq GROQ_MODEL = "meta-llama/llama-4-maverick-17b-128e-instruct" def call_groq(system_prompt: str, user_prompt: str, temperature: float = 0.7, max_tokens: int = 2048) -> str: """ Calls the Groq chat completion API and returns the full combined response text. Streams tokens and accumulates them into a final string. """ client = Groq(api_key = "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") # Uses GROQ_API_KEY env var completion = client.chat.completions.create ( model=GROQ_MODEL, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=temperature, max_completion_tokens=max_tokens, top_p=1, stream=True, stop=None ) chunks = [] for chunk in completion: piece = chunk.choices[0].delta.content or "" chunks.append(piece) return "".join(chunks) # -------------------- PDF Utilities -------------------- def extract_text_from_pdf(pdf_bytes: bytes, max_pages: int = 50) -> str: """ Extracts text from a PDF byte stream using PyMuPDF. Limits to max_pages for performance. """ text_parts = [] with fitz.open(stream=pdf_bytes, filetype="pdf") as doc: page_count = min(len(doc), max_pages) for i in range(page_count): page = doc.load_page(i) text_parts.append(page.get_text("text")) full_text = "\n".join(text_parts) # Basic cleanup lines = [ln.strip() for ln in full_text.splitlines() if ln.strip()] return "\n".join(lines) def chunk_text(text: str, max_chars: int = 4000, overlap: int = 200) -> List[str]: """ Splits text into overlapping chunks to keep prompts within context limits. """ chunks = [] start = 0 n = len(text) while start < n: end = min(start + max_chars, n) chunk = text[start:end] chunks.append(chunk) if end == n: break start = end - overlap if start < 0: start = 0 return chunks # -------------------- Quiz Generation -------------------- QUIZ_SCHEMA_EXAMPLE = { "metadata": { "source": "string (e.g., PDF title or filename)", "difficulty": "Easy|Medium|Hard" }, "questions": [ # MCQ {"id": 1, "type": "mcq", "question": "Q text", "options": ["A", "B", "C", "D"], "answer": "B", "explanation": "why"}, # True/False {"id": 2, "type": "true_false", "question": "Statement", "answer": True, "explanation": "why"}, # Short Answer (open, brief) {"id": 3, "type": "short_answer", "question": "Q text", "expected_answer": "reference answer", "rubric": "key points"}, # Essay (theory) {"id": 4, "type": "essay", "question": "Essay prompt", "rubric": "grading rubric/bullets", "max_score": 10} ] } SYSTEM_PROMPT_GENERATE = """You are an expert educational content creator. You will receive textbook or lecture content and must produce a structured quiz in strict JSON. Output ONLY valid JSON that follows the given schema. Do not add commentary. JSON schema keys: metadata{source, difficulty}, questions[list]. Question item types: "mcq", "true_false", "short_answer", "essay". For "mcq": include 4 distinct options, exactly one correct "answer", and a brief "explanation". For "true_false": "answer" must be true or false (boolean), include "explanation". For "short_answer": include "expected_answer" and a concise "rubric" with key points. For "essay": include a clear "rubric" and "max_score" as an integer. Make questions faithful to the source text (no hallucinations). Prefer concise, unambiguous wording. """ def build_generation_prompt(chunk: str, source_name: str, difficulty: str, n_mcq: int, n_tf: int, n_short: int, n_essay: int) -> str: return f""" Source name: {source_name} Desired difficulty: {difficulty} Required counts -> MCQ: {n_mcq}, True/False: {n_tf}, Short Answer: {n_short}, Essay: {n_essay} Source text: \"\"\" {chunk} \"\"\" Produce a single JSON object matching this schema (example): {json.dumps(QUIZ_SCHEMA_EXAMPLE, indent=2)} Ensure total number of questions equals the requested counts combined. Use simple language appropriate for undergraduates. """ def merge_quizzes(quizzes: List[Dict[str, Any]], source_name: str, difficulty: str) -> Dict[str, Any]: """ Merge multiple chunk-level quiz JSONs into one. Re-index question IDs. """ merged = {"metadata": {"source": source_name, "difficulty": difficulty}, "questions": []} qid = 1 for q in quizzes: for item in q.get("questions", []): item["id"] = qid merged["questions"].append(item) qid += 1 return merged def generate_quiz_from_text(text: str, source_name: str, difficulty: str, n_mcq: int, n_tf: int, n_short: int, n_essay: int, temperature: float = 0.7) -> Tuple[Dict[str, Any], str]: """ Generates a quiz by calling the LLM on one or more chunks, distributing question counts across chunks. Returns (quiz_json, raw_model_output_debug). """ chunks = chunk_text(text, max_chars=3500, overlap=200) total_required = n_mcq + n_tf + n_short + n_essay if total_required == 0: return {"metadata": {"source": source_name, "difficulty": difficulty}, "questions": []}, "" # Divide counts across chunks (simple even split) c = max(1, len(chunks)) split = lambda total: [total // c + (1 if i < (total % c) else 0) for i in range(c)] mcq_split = split(n_mcq) tf_split = split(n_tf) short_split = split(n_short) essay_split = split(n_essay) partial_quizzes = [] debug_texts = [] for i, ch in enumerate(chunks): if mcq_split[i] + tf_split[i] + short_split[i] + essay_split[i] == 0: continue user_prompt = build_generation_prompt( chunk=ch, source_name=source_name, difficulty=difficulty, n_mcq=mcq_split[i], n_tf=tf_split[i], n_short=short_split[i], n_essay=essay_split[i] ) raw = call_groq(SYSTEM_PROMPT_GENERATE, user_prompt, temperature=temperature, max_tokens=2048) debug_texts.append(raw) # attempt to parse JSON (robustly strip fences, trailing text) cleaned = raw.strip() if cleaned.startswith("```"): cleaned = cleaned.strip("`") # if it included language tag like ```json if cleaned.startswith("json"): cleaned = cleaned[len("json"):].strip() # Find first and last braces to extract JSON start = cleaned.find("{") end = cleaned.rfind("}") if start != -1 and end != -1 and end > start: cleaned = cleaned[start:end+1] try: obj = json.loads(cleaned) partial_quizzes.append(obj) except Exception as e: partial_quizzes.append({"metadata":{"source":source_name, "difficulty":difficulty},"questions":[]}) merged = merge_quizzes(partial_quizzes, source_name, difficulty) return merged, "\n\n---\n\n".join(debug_texts) # -------------------- Grading -------------------- SYSTEM_PROMPT_GRADE_ESSAY = """You are a strict but fair examiner. Grade the student's essay against the rubric and max_score provided. Return ONLY a JSON object with keys: score (integer), feedback (string). Be concise and specific in feedback. Do not add commentary outside JSON. """ def build_grading_prompt(question_text: str, rubric: str, max_score: int, student_answer: str) -> str: return f""" Question: {question_text} Rubric (key points to award marks): {rubric} Max Score: {max_score} Student Answer: \"\"\" {student_answer} \"\"\" Return JSON only: {{"score": , "feedback": ""}} """ def grade_essay_answer(question_text: str, rubric: str, max_score: int, student_answer: str, temperature: float = 0.2) -> Dict[str, Any]: raw = call_groq(SYSTEM_PROMPT_GRADE_ESSAY, build_grading_prompt(question_text, rubric, max_score, student_answer), temperature=temperature, max_tokens=512) cleaned = raw.strip() if cleaned.startswith("```"): cleaned = cleaned.strip("`") if cleaned.startswith("json"): cleaned = cleaned[len("json"):].strip() start = cleaned.find("{"); end = cleaned.rfind("}") if start != -1 and end != -1 and end > start: cleaned = cleaned[start:end+1] try: obj = json.loads(cleaned) except Exception: obj = {"score": 0, "feedback": "Grading failed to parse. Please retry."} # clamp score try: obj["score"] = int(obj.get("score", 0)) if obj["score"] < 0: obj["score"] = 0 if obj["score"] > max_score: obj["score"] = max_score except Exception: obj["score"] = 0 return obj # -------------------- Gradio App -------------------- with gr.Blocks(title="AI Quiz Generation from PDF (LLM)") as demo: gr.Markdown("# AI Quiz Generation System from PDF Documents using LLM") gr.Markdown("Upload a PDF, generate MCQ/True/Short/Essay questions, take the quiz, and grade essay answers via LLM.\n") state_pdf_text = gr.State("") state_quiz = gr.State({"metadata": {}, "questions": []}) state_source_name = gr.State("") state_debug = gr.State("") # Extra states for step-by-step quiz flow state_non_essay = gr.State([]) # holds MCQ/TF/Short questions state_take_idx = gr.State(0) # index for Take Quiz flow state_take_results = gr.State({"attempted": 0, "correct": 0, "details": []}) state_essay_list = gr.State([]) # holds Essay questions state_essay_idx = gr.State(0) # index for Essay flow state_essay_results = gr.State([]) # list of {"id","score","max","feedback"} with gr.Tabs(): with gr.Tab("1) Upload & Generate"): with gr.Row(): pdf_input = gr.File(label="Upload PDF", file_types=[".pdf"]) source_name_tb = gr.Textbox(label="Source Name (optional)", placeholder="e.g., Intro_to_AI_Notes.pdf") with gr.Accordion("Extraction & Generation Settings", open=True): with gr.Row(): difficulty_dd = gr.Dropdown(choices=["Easy","Medium","Hard"], value="Medium", label="Difficulty") temp_slider = gr.Slider(0.0, 1.5, value=0.7, step=0.1, label="Creativity (temperature)") with gr.Row(): n_mcq = gr.Number(value=5, precision=0, label="Number of MCQ") n_tf = gr.Number(value=3, precision=0, label="Number of True/False") n_short = gr.Number(value=2, precision=0, label="Number of Short Answer") n_essay = gr.Number(value=1, precision=0, label="Number of Essay") extract_btn = gr.Button("Extract Text") extracted_preview = gr.Textbox(label="Extracted Text Preview (first 2000 chars)", lines=12) generate_btn = gr.Button("Generate Quiz") quiz_json_out = gr.JSON(label="Generated Quiz (JSON)") debug_out = gr.Textbox(label="Raw Model Output (debug)", visible=False) def do_extract(pdf_file, source_name): if not pdf_file: return gr.update(value=""), "", "No file provided." with open(pdf_file.name, "rb") as f: pdf_bytes = f.read() text = extract_text_from_pdf(pdf_bytes, max_pages=50) preview = text[:2000] src = source_name or os.path.basename(pdf_file.name) return preview, text, src extract_btn.click( do_extract, inputs=[pdf_input, source_name_tb], outputs=[extracted_preview, state_pdf_text, state_source_name] ) def do_generate(text, source_name, difficulty, temp, n1, n2, n3, n4): if not text or not text.strip(): return {"metadata": {}, "questions": []}, "No text to generate from.", {"metadata": {}, "questions": []} quiz, dbg = generate_quiz_from_text( text=text, source_name=source_name or "Uploaded PDF", difficulty=difficulty, n_mcq=int(n1 or 0), n_tf=int(n2 or 0), n_short=int(n3 or 0), n_essay=int(n4 or 0), temperature=float(temp) ) return quiz, dbg, quiz generate_btn.click( do_generate, inputs=[state_pdf_text, state_source_name, difficulty_dd, temp_slider, n_mcq, n_tf, n_short, n_essay], outputs=[quiz_json_out, debug_out, state_quiz] ) with gr.Tab("2) Take Quiz"): gr.Markdown("Answer one question at a time. MCQ and True/False use radio buttons; Short Answer uses a textbox. A summary appears at the end.") take_progress = gr.Markdown("") take_q_text = gr.Textbox(label="Question", lines=4, interactive=False) take_mcq_radio = gr.Radio(choices=[], label="Choose one (MCQ)", visible=False) take_tf_radio = gr.Radio(choices=["True", "False"], label="True/False", visible=False) take_short_tb = gr.Textbox(label="Short Answer", visible=False, placeholder="Type your short answer here...") take_submit_btn = gr.Button("Submit & Next") take_feedback = gr.JSON(label="Feedback (this question)", visible=False) take_summary = gr.JSON(label="Final Summary", visible=False) def _split_non_essay(quiz: Dict[str, Any]): return [q for q in quiz.get("questions", []) if q.get("type") in ["mcq", "true_false", "short_answer"]] def _progress_str(idx: int, total: int) -> str: return f"**Question {min(idx+1, total)} of {total}**" if total else "**No questions available.**" def _show_non_essay_question(non_essay: List[Dict[str, Any]], idx: int): total = len(non_essay) if total == 0: # nothing to show return ( _progress_str(0, 0), gr.update(value="No non-essay questions in this quiz.", interactive=False), gr.update(visible=False), # mcq gr.update(visible=False), # tf gr.update(visible=False), # short gr.update(visible=False), # feedback gr.update(value={"message": "Nothing to attempt."}, visible=True) # summary ) if idx >= total: # finished return ( _progress_str(total, total), gr.update(value="You have completed all non-essay questions.", interactive=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) ) q = non_essay[idx] qtype = q.get("type") qtext = q.get("question", "") if qtype == "mcq": opts = q.get("options", []) or [] return ( _progress_str(idx, total), gr.update(value=qtext, interactive=False), gr.update(choices=opts, value=None, visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), ) elif qtype == "true_false": return ( _progress_str(idx, total), gr.update(value=qtext, interactive=False), gr.update(visible=False), gr.update(value=None, visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), ) else: # short_answer return ( _progress_str(idx, total), gr.update(value=qtext, interactive=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="", visible=True), gr.update(visible=False), gr.update(visible=False), ) def init_take_quiz(quiz: Dict[str, Any]): non_essay = _split_non_essay(quiz) idx0 = 0 results0 = {"attempted": 0, "correct": 0, "details": []} ui = _show_non_essay_question(non_essay, idx0) # return UI + states return (*ui, non_essay, idx0, results0) def _compare_short_answer(gold: str, ans: str) -> bool: gold = (gold or "").strip().lower() ans = (ans or "").strip().lower() if not gold or not ans: return False gold_tokens = set(gold.split()) ans_tokens = set(ans.split()) # require at least 25% token overlap (min 1) common = gold_tokens & ans_tokens return len(common) >= max(1, len(gold_tokens) // 4) def submit_next(mcq_choice, tf_choice, short_ans, non_essay: List[Dict[str, Any]], idx: int, results_state: Dict[str, Any]): total = len(non_essay) if total == 0 or idx >= total: # already done return ( _progress_str(total, total), gr.update(value="You have completed all non-essay questions.", interactive=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update( value={"summary": results_state}, visible=True ), idx, results_state, ) q = non_essay[idx] qid = q.get("id") qtype = q.get("type") explanation = q.get("explanation", "") feedback = {} is_correct = False your_answer = None if qtype == "mcq": your_answer = mcq_choice is_correct = (str(your_answer).strip() == str(q.get("answer", "")).strip()) elif qtype == "true_false": your_answer = tf_choice gold = str(q.get("answer", "")).strip().lower() val = str(your_answer or "").strip().lower() # normalize radio labels if val in ["t", "true", "yes", "1"]: val = "true" elif val in ["f", "false", "no", "0"]: val = "false" is_correct = (val == gold) else: # short_answer your_answer = short_ans gold = str(q.get("expected_answer", "")).strip() is_correct = _compare_short_answer(gold, your_answer) # update results results_state = dict(results_state) # copy results_state["attempted"] += 1 if is_correct: results_state["correct"] += 1 results_state["details"] = list(results_state.get("details", [])) + [{ "id": qid, "type": qtype, "your_answer": your_answer, "correct_answer": q.get("answer", q.get("expected_answer", "")), "correct": is_correct }] feedback = { "id": qid, "type": qtype, "your_answer": your_answer, "correct_answer": q.get("answer", q.get("expected_answer", "")), "correct": is_correct, } if explanation and qtype in ["mcq", "true_false"]: feedback["explanation"] = explanation # advance idx_next = idx + 1 # if finished, show summary if idx_next >= total: return ( _progress_str(total, total), gr.update(value="Completed non-essay questions.", interactive=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value=feedback, visible=True), gr.update(value={ "summary": { "attempted": results_state["attempted"], "correct": results_state["correct"], "accuracy": round(100.0 * results_state["correct"] / max(1, results_state["attempted"]), 2) }, "details": results_state["details"] }, visible=True), idx_next, results_state, ) # otherwise show next question ui_next = _show_non_essay_question(non_essay, idx_next) return ( *ui_next[:5], gr.update(value=feedback, visible=True), # feedback visible gr.update(visible=False), # summary hidden until end idx_next, results_state, ) # reinitialize when a new quiz is generated state_quiz.change( init_take_quiz, inputs=[state_quiz], outputs=[ take_progress, take_q_text, take_mcq_radio, take_tf_radio, take_short_tb, take_feedback, take_summary, state_non_essay, state_take_idx, state_take_results ] ) # submit current answer and move next take_submit_btn.click( submit_next, inputs=[ take_mcq_radio, take_tf_radio, take_short_tb, state_non_essay, state_take_idx, state_take_results ], outputs=[ take_progress, take_q_text, take_mcq_radio, take_tf_radio, take_short_tb, take_feedback, take_summary, state_take_idx, state_take_results ] ) with gr.Tab("3) Essay/Theory Grading (LLM)"): gr.Markdown("Essay questions are presented one at a time. Submit your answer to get LLM grading, then proceed to the next.") essay_progress = gr.Markdown("") essay_q_view = gr.Textbox(label="Essay Question", lines=4, interactive=False) with gr.Accordion("Rubric & Max Score", open=False): essay_rubric_view = gr.Textbox(label="Rubric", lines=6, interactive=False) essay_max_score = gr.Number(label="Max Score", value=10, precision=0, interactive=False) essay_answer_tb = gr.Textbox(label="Your Essay/Theory Answer", lines=10, placeholder="Write your answer here...") essay_submit_btn = gr.Button("Submit & Grade with LLM") essay_grade_json = gr.JSON(label="Grade (this essay)", visible=False) essay_final_summary = gr.JSON(label="Essay Summary", visible=False) def _essays_only(quiz: Dict[str, Any]): return [q for q in quiz.get("questions", []) if q.get("type") == "essay"] def _essay_progress_str(idx: int, total: int) -> str: return f"**Essay {min(idx+1, total)} of {total}**" if total else "**No essay questions available.**" def _show_essay(essays: List[Dict[str, Any]], idx: int): total = len(essays) if total == 0: return ( _essay_progress_str(0, 0), gr.update(value="No essay questions in this quiz.", interactive=False), gr.update(value="", interactive=False), gr.update(value=10, interactive=False), gr.update(value="", visible=False), gr.update(visible=False), gr.update(value={"message": "Nothing to grade."}, visible=True) ) if idx >= total: return ( _essay_progress_str(total, total), gr.update(value="All essay questions completed.", interactive=False), gr.update(value="", interactive=False), gr.update(value=10, interactive=False), gr.update(value="", visible=False), gr.update(visible=False), gr.update(visible=True) ) q = essays[idx] return ( _essay_progress_str(idx, total), gr.update(value=q.get("question", ""), interactive=False), gr.update(value=q.get("rubric", ""), interactive=False), gr.update(value=int(q.get("max_score", 10)), interactive=False), gr.update(value="", visible=True), gr.update(visible=False), gr.update(visible=False), ) def init_essay_flow(quiz: Dict[str, Any]): essays = _essays_only(quiz) idx0 = 0 results0 = [] ui = _show_essay(essays, idx0) return (*ui, essays, idx0, results0) def grade_and_next_essay(answer: str, essays: List[Dict[str, Any]], idx: int, results: List[Dict[str, Any]]): total = len(essays) if total == 0 or idx >= total: # already done return ( _essay_progress_str(total, total), gr.update(value="All essay questions completed.", interactive=False), gr.update(value="", interactive=False), gr.update(value=10, interactive=False), gr.update(value="", visible=False), gr.update(visible=False), gr.update(value={"message": "Completed."}, visible=True), essays, idx, results ) q = essays[idx] qtext = q.get("question", "") rubric = q.get("rubric", "") max_score = int(q.get("max_score", 10)) if not str(answer or "").strip(): # ask to provide an answer return ( _essay_progress_str(idx, total), gr.update(value=qtext, interactive=False), gr.update(value=rubric, interactive=False), gr.update(value=max_score, interactive=False), gr.update(value=answer or "", visible=True), gr.update(value={"error": "Provide an answer before submitting."}, visible=True), gr.update(visible=False), essays, idx, results ) # grade with LLM grade_obj = grade_essay_answer(qtext, rubric, max_score, answer) # store result results = list(results) + [{ "id": q.get("id"), "score": grade_obj.get("score", 0), "max": max_score, "feedback": grade_obj.get("feedback", "") }] idx_next = idx + 1 # if finished, compute summary if idx_next >= total: total_score = sum(r["score"] for r in results) max_total = sum(r["max"] for r in results) summary = { "completed": len(results), "total_score": total_score, "max_total": max_total, "percentage": round(100.0 * total_score / max(1, max_total), 2), "details": results } return ( _essay_progress_str(total, total), gr.update(value="All essay questions completed.", interactive=False), gr.update(value=rubric, interactive=False), gr.update(value=max_score, interactive=False), gr.update(value="", visible=False), gr.update(value=grade_obj, visible=True), gr.update(value=summary, visible=True), essays, idx_next, results ) # otherwise move to next essay ui_next = _show_essay(essays, idx_next) return ( *ui_next[:5], gr.update(value=grade_obj, visible=True), # show grade for current gr.update(visible=False), # final summary hidden essays, idx_next, results ) # initialize the essay flow on new quiz state_quiz.change( init_essay_flow, inputs=[state_quiz], outputs=[ essay_progress, essay_q_view, essay_rubric_view, essay_max_score, essay_answer_tb, essay_grade_json, essay_final_summary, state_essay_list, state_essay_idx, state_essay_results ] ) # submit current essay answer and move to next essay_submit_btn.click( grade_and_next_essay, inputs=[essay_answer_tb, state_essay_list, state_essay_idx, state_essay_results], outputs=[ essay_progress, essay_q_view, essay_rubric_view, essay_max_score, essay_answer_tb, essay_grade_json, essay_final_summary, state_essay_list, state_essay_idx, state_essay_results ] ) with gr.Tab("4) Export"): gr.Markdown("Download your quiz as JSON or CSV.") export_json_btn = gr.Button("Download Quiz (JSON)") export_csv_btn = gr.Button("Download Quiz (CSV)") file_json = gr.File(label="Quiz JSON") file_csv = gr.File(label="Quiz CSV") def export_json(quiz: Dict[str,Any]): path = "quiz_export.json" with open(path, "w", encoding="utf-8") as f: json.dump(quiz, f, ensure_ascii=False, indent=2) return path def export_csv(quiz: Dict[str,Any]): import csv path = "quiz_export.csv" fields = ["id","type","question","options","answer","expected_answer","rubric","max_score","explanation"] with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() for q in quiz.get("questions", []): row = { "id": q.get("id"), "type": q.get("type"), "question": q.get("question",""), "options": "|".join(q.get("options", [])) if isinstance(q.get("options"), list) else "", "answer": q.get("answer",""), "expected_answer": q.get("expected_answer",""), "rubric": q.get("rubric",""), "max_score": q.get("max_score",""), "explanation": q.get("explanation",""), } writer.writerow(row) return path export_json_btn.click(export_json, inputs=[state_quiz], outputs=[file_json]) export_csv_btn.click(export_csv, inputs=[state_quiz], outputs=[file_csv]) # Footer/help with gr.Accordion("Help & Notes", open=False): gr.Markdown(""" **Setup** - Install dependencies from `requirements.txt`. - Launch: `python ai_quiz_from_pdf_gradio.py` and open the local URL. **Usage** 1. Go to **Upload & Generate**: upload your PDF, tweak counts, click *Extract Text* then *Generate Quiz*. 2. In **Take Quiz**, answer MCQ/True-False/Short-Answer and click *Submit* to auto-grade. 3. In **Essay/Theory Grading**, choose an Essay question, paste your answer, and click *Grade*. 4. In **Export**, download the quiz as JSON/CSV for sharing or record-keeping. **Notes** - Essay grading uses the LLM with a rubric and max_score. Results are heuristic. - For scanned PDFs (images), add OCR (e.g., `pytesseract`) in future work. """) if __name__ == "__main__": # If the API key isn't set, warn in console. if not os.getenv("GROQ_API_KEY"): print("WARNING: GROQ_API_KEY is not set. Set it before generating or grading with the LLM.") demo.launch(debug=True)