import os import re import pdfplumber import pytesseract import cv2 import numpy as np import gradio as gr from PIL import Image from collections import Counter from assistant.storage import ( generate_doc_id, save_document, load_document, save_metadata, get_all_docs, ) # Configure Tesseract path (Windows only) if os.name == 'nt': pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" FALLBACK = "This information is not available in your uploaded notes." STOP_WORDS = { "what", "is", "a", "an", "the", "of", "to", "in", "and", "why", "how", "who", "when", "where", "are", "do", "does", "did", "for", "on", "it", "this", "that", "tell", "me", "about", "define", "explain", "give", "meaning", "can", "you", "please", "with", "was", "will", "has", "have", "had", "be", "been", "by", "or", "at", "from", "as", "its", "my", "your" } FACULTY_MAP = { "KKD": "Dr. Kailas Devadkar", "NR": "Dr. Nataasha Raul", "JS": "Prof. Jignesh Sisodia" } # --- TIMETABLE LOGIC --- def extract_text(file_path): if file_path is None: return "" ext = os.path.splitext(file_path)[1].lower() text = "" if ext == ".pdf": with pdfplumber.open(file_path) as pdf: for page in pdf.pages: extracted = page.extract_text() if extracted: text += extracted + "\n" elif ext in [".png", ".jpg", ".jpeg"]: img = cv2.imread(file_path) if img is None: img = Image.open(file_path) text = pytesseract.image_to_string(img) else: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) text = pytesseract.image_to_string(thresh) return text def calculate_attendance_logic(raw_list): """Internal logic to calculate attendance metrics exactly as requested.""" subject_counts = Counter() subject_to_faculty = {} for entry in raw_list: sub = entry.get("subject") fac = entry.get("faculty") subject_counts[sub] += 1 if fac and sub not in subject_to_faculty: # Resolve actual faculty name if it exists in map, else keep code subject_to_faculty[sub] = FACULTY_MAP.get(fac, fac) subjects_data = {} for subject, count in subject_counts.items(): weekly = count monthly = weekly * 4 # Calculate safe_bunks (attendance >= 75%) safe_bunks = 0 while monthly > 0 and ((monthly - (safe_bunks + 1)) / monthly) >= 0.75: safe_bunks += 1 # Calculate classes_needed (attendance >= 75%) classes_needed = 0 # If classes_needed/monthly < 0.75, it means we currently have 0% attendance # Requirement: get to 75% of (monthly + extra_classes) # However, following the user's previously working logic provided in file: current_attended = 0 # assuming starting from scratch for simplicity as per previous logic while monthly > 0 and (classes_needed / monthly) < 0.75: classes_needed += 1 subjects_data[subject] = { "faculty": subject_to_faculty.get(subject, "Unknown"), "weekly": weekly, "monthly": monthly, "safe_bunks": safe_bunks, "classes_needed": classes_needed } return subjects_data def api_process_timetable(file): if file is None: return {"error": "No file"} text = extract_text(file.name) raw_list = [] pattern = r"([A-Z]{2,})\s*/\s*(?:([A-Z]?)\s*/\s*)?([A-Z0-9]{2,})" matches = re.finditer(pattern, text) for match in matches: raw_list.append({ "subject": match.group(1).strip(), "faculty": match.group(3).strip() }) subjects_data = calculate_attendance_logic(raw_list) return { "raw": raw_list, "subjects": subjects_data } # --- ASSISTANT LOGIC (PURE RAG) --- def api_upload_notes(file): if file is None: return {"error": "No file"} text = extract_text(file.name) if not text.strip(): return {"error": "Empty text"} doc_id = generate_doc_id() save_document(doc_id, text) save_metadata(doc_id, os.path.basename(file.name)) return {"doc_id": doc_id, "message": "Saved successfully"} def _find_context(text, keywords): sentences = re.split(r'[.!?\n]+', text) matched = [] for i, sentence in enumerate(sentences): sentence = sentence.strip() if len(sentence) < 5: continue sent_lower = sentence.lower() for kw in keywords: if kw in sent_lower: matched.append({"text": sentence.replace('\n', ' '), "line": i+1}) break if len(matched) >= 3: break return matched def api_ask_question(doc_id, question): text = load_document(doc_id) if text is None: return {"answer": "Doc not found"} words = re.findall(r'\b\w+\b', question.lower()) keywords = [w for w in words if w not in STOP_WORDS and len(w) > 1] keywords = keywords if keywords else words matches = _find_context(text, keywords) if not matches: return {"answer": FALLBACK, "source": None} context = " ".join([m["text"] for m in matches]) first_match = matches[0] return { "answer": f"According to your notes: {context}", "source": { "line": first_match["line"], "column": first_match["text"].lower().find(keywords[0]) + 1 if keywords else 1 } } # --- GRADIO UI --- with gr.Blocks(title="Lumina Backend API") as demo: gr.Markdown("# 🚀 Lumina Backend (Gradio API)") with gr.Tab("Timetable Parser"): tt_file = gr.File(label="Upload Timetable") tt_btn = gr.Button("Parse") tt_out = gr.JSON(label="Parsed Output") tt_btn.click(api_process_timetable, inputs=tt_file, outputs=tt_out, api_name="process_timetable") with gr.Tab("AI Assistant"): with gr.Row(): note_file = gr.File(label="Upload Notes") note_btn = gr.Button("Upload & Process") note_out = gr.JSON(label="Doc ID Output") with gr.Row(): ask_id = gr.Textbox(label="Document ID") ask_q = gr.Textbox(label="Question") ask_btn = gr.Button("Ask AI") ask_out = gr.JSON(label="AI Answer") note_btn.click(api_upload_notes, inputs=note_file, outputs=note_out, api_name="upload_notes") ask_btn.click(api_ask_question, inputs=[ask_id, ask_q], outputs=ask_out, api_name="ask_question") if __name__ == "__main__": demo.launch(theme=gr.themes.Soft())