import os import gradio as gr from huggingface_hub import InferenceClient from docx import Document from PIL import Image import pytesseract import fitz import pathlib import threading import time def resolve_token(ui_token): if ui_token and ui_token.strip(): return ui_token.strip() env_token = os.getenv("hf") if env_token: return env_token.strip() return None SUPPORTED_EXT = ( ".pdf", ".docx", ".txt", ".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff" ) def extract_text_from_file(filepath): if not filepath: return "" if isinstance(filepath, dict) and "name" in filepath: filepath = filepath["name"] ext = pathlib.Path(filepath).suffix.lower() try: if ext == ".pdf": doc = fitz.open(filepath) return "\n".join(page.get_text() for page in doc) elif ext == ".docx": doc = Document(filepath) return "\n".join(p.text for p in doc.paragraphs) elif ext == ".txt": with open(filepath, "r", encoding="utf-8", errors="ignore") as f: return f.read() elif ext in (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff"): img = Image.open(filepath) return pytesseract.image_to_string(img) else: return "Unsupported file type" except Exception as e: return f"Error reading file: {str(e)}" MODELS = { "DeepSeek-Qwen 1.5B (Fastest)": "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "Qwen 3 4B (Good Speed)": "Qwen/Qwen3-4B-Thinking-2507:nscale", "Gemma 3 4B": "google/gemma-3n-E4B-it:together", "DeepSeek-Llama 8B (Recommended)": "deepseek-ai/DeepSeek-R1-Distill-Llama-8B:nscale", "Qwen 3 8B": "Qwen/Qwen3-8B:nscale", "OpenAI GPT OSS 120B": "openai/gpt-oss-120b:novita", "Gemma 4 31B (Best)": "google/gemma-4-31B-it:novita" } ALL_MODEL_NAMES = list(MODELS.keys()) SYSTEM_MSG = """You are an expert Academic Instructor and Curriculum Designer, highly familiar with SASTRA University's academic structure. - STRICTLY adhere to the provided syllabus/topic. Do not invent concepts, formulas, or fake exam papers. - Use clean, structured Markdown. Enforce exact formatting where specified. - Keep outputs practical, concise, and immediately usable. Avoid overcomplication. - SASTRA RESOURCES: Actively identify and recommend SASTRA-specific resources. Mention SASTRA Material Hub, Material Base, previous year CIA/End-Sem papers, and standard textbooks prescribed by SASTRA faculty. Frame teaching points and questions to match SASTRA's typical assessment patterns (CIA 1, CIA 2, End Semester). - GENERAL RESOURCES: If a specific standard textbook or verified platform (like NPTEL, standard authors) is widely used for this topic, cite it accurately. - NEVER hallucinate citations or paper questions. If unsure about a specific SASTRA paper, generalize based on typical university patterns while maintaining the SASTRA difficulty curve.""" def make_prompts(syllabus): base = f""" Academic Context: {syllabus} General Requirements: - Maintain strict alignment with the syllabus - Format strictly in Markdown - Keep explanations concise, practical, and exam/teaching ready - Cite SASTRA-specific resources (Material Hub, Material Base, previous papers) or widely recognized academic platforms """ return { "lesson": base + """ Generate a period-wise teaching schedule (Lesson Plan): - Assume each period is 45-50 minutes each. - Output MUST be a single Markdown table with exactly these columns: | Period | Topic/Subtopic | Key Teaching Points | - Keep descriptions brief. Focus on logical progression. - Reserve the final period for revision. - Output MUST be a valid GitHub-Flavored Markdown table. - Do NOT use ASCII separators like +--- or ---- - Table must render correctly in Markdown. - Give 'Resources' relevant SASTRA Material Hub modules, slides or materials from the interneton that topic, standard prescribed book chapters, or typical CIA question types for all topics. - Explain each concepts: Defintion (understandable way only, like points), examples (very important) """, "qa": base + """ Generate 8 exam-style questions with answers: - Match SASTRA CIA and End-Sem difficulty (include tricky and conceptual questions). - Mix: * 3 short answers (2–3 lines) * 3 medium answers (5–6 lines) * 2 long answers (8–12 lines, may include derivations or explanations or code, for diagrams verify materials/resources/text books) - Include at least 2 tricky or application-based questions. - Format strictly: **Q1.** [Question] **A:** [Answer] - For long answers: - Use stepwise explanation or bullet points - Include formulas where relevant - If aligned with SASTRA Material Base or textbook patterns, mention briefly. """, "mcq": base + """ Generate 8 multiple-choice questions: - 4 options (A-D), one clearly correct, three plausible distractors. - Format strictly: **Q1.** [Stem] A) ... B) ... C) ... D) ... **Correct:** [Letter] | **Why:** [1-line rationale] - Focus on core concepts, formula application, and common student mistakes seen in SASTRA exams. """, "summary": base + """ Produce a rapid-revision summary: - List 5 core concepts/formulas - Provide 3 high-yield takeaways for SASTRA exams - Include 1 quick self-check question based on typical CIA patterns - Keep strictly under 150 words. Use bullet points only. No paragraphs. """ } def stream_llm(model_id, prompt, hf_token): if not hf_token: yield "❌ No Hugging Face API key found." return try: client = InferenceClient(token=hf_token) stream = client.chat.completions.create( model=model_id, messages=[ {"role": "system", "content": SYSTEM_MSG}, {"role": "user", "content": prompt}, ], max_tokens=2048, temperature=0.7, top_p=0.95, stream=True, ) partial = "" for chunk in stream: if chunk.choices and chunk.choices[0].delta: token = chunk.choices[0].delta.get("content", "") if token: partial += token yield partial except Exception as e: yield f"❌ API Error:\n{str(e)}" def generate_content(text, file, model_label, token): hf_token = resolve_token(token) file_text = extract_text_from_file(file) if file else "" syllabus = (text + "\n\n" + file_text).strip() if not syllabus: yield ("Provide topic or file", "", "", "") return prompts = make_prompts(syllabus) model_id = MODELS[model_label] outputs = ["", "", "", ""] keys = ["lesson", "qa", "mcq", "summary"] lock = threading.Lock() def run_stream(i, key): for chunk in stream_llm(model_id, prompts[key], hf_token): with lock: outputs[i] = chunk threads = [] for i, key in enumerate(keys): t = threading.Thread(target=run_stream, args=(i, key)) t.start() threads.append(t) while any(t.is_alive() for t in threads): time.sleep(0.1) yield tuple(outputs) yield tuple(outputs) CSS = """ body,.gradio-container{ font-family: Inter, sans-serif !important; } """ with gr.Blocks() as demo: gr.Markdown("# 🎓 AI Study Material Generator (Streaming)") with gr.Row(): with gr.Column(): text_input = gr.Textbox(placeholder="Paste syllabus or topic", lines=6) file_input = gr.File(label="Upload syllabus file", file_types=list(SUPPORTED_EXT)) with gr.Column(): model_selector = gr.Dropdown(choices=ALL_MODEL_NAMES, value="DeepSeek-Llama 8B (Recommended)", label="Model") token_box = gr.Textbox(label="HF API Key (optional)", type="password") btn = gr.Button("Generate") with gr.Tabs(): with gr.TabItem("Lesson Plan"): lesson = gr.Markdown() with gr.TabItem("Question and Anwer"): qa = gr.Markdown() with gr.TabItem("MCQ"): mcq = gr.Markdown() with gr.TabItem("Summary"): summary = gr.Markdown() btn.click( fn=generate_content, inputs=[text_input, file_input, model_selector, token_box], outputs=[lesson, qa, mcq, summary] ) demo.launch(css=CSS)