Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import pandas as pd | |
| import json | |
| import os | |
| import re | |
| from PyPDF2 import PdfReader | |
| from collections import defaultdict | |
| from typing import Dict, List, Optional, Tuple, Union | |
| import html | |
| from pathlib import Path | |
| import fitz # PyMuPDF | |
| import pytesseract | |
| from PIL import Image | |
| import io | |
| import secrets | |
| import string | |
| from huggingface_hub import HfApi, HfFolder | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| import time | |
| import logging | |
| import asyncio | |
| from functools import lru_cache | |
| import hashlib | |
| from concurrent.futures import ThreadPoolExecutor | |
| from pydantic import BaseModel | |
| import plotly.express as px | |
| import pdfplumber | |
| from io import BytesIO | |
| import base64 | |
| import matplotlib.pyplot as plt | |
| # ========== CONFIGURATION ========== | |
| PROFILES_DIR = "student_profiles" | |
| ALLOWED_FILE_TYPES = [".pdf", ".png", ".jpg", ".jpeg"] | |
| MAX_FILE_SIZE_MB = 5 | |
| MIN_AGE = 5 | |
| MAX_AGE = 120 | |
| SESSION_TOKEN_LENGTH = 32 | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| SESSION_TIMEOUT = 3600 # 1 hour session timeout | |
| # Initialize logging | |
| logging.basicConfig( | |
| level=logging.DEBUG, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
| filename='transcript_parser.log' | |
| ) | |
| # Model configuration - Using smaller model | |
| MODEL_NAME = "deepseek-ai/deepseek-llm-1.3b" | |
| # Initialize Hugging Face API | |
| if HF_TOKEN: | |
| try: | |
| hf_api = HfApi(token=HF_TOKEN) | |
| HfFolder.save_token(HF_TOKEN) | |
| except Exception as e: | |
| logging.error(f"Failed to initialize Hugging Face API: {str(e)}") | |
| # ========== MODEL LOADER ========== | |
| class ModelLoader: | |
| def __init__(self): | |
| self.model = None | |
| self.tokenizer = None | |
| self.loaded = False | |
| self.loading = False | |
| self.error = None | |
| self.device = "cuda" if torch.cuda.is_available() else "cpu" | |
| def load_model(self, progress: gr.Progress = None) -> Tuple[Optional[AutoModelForCausalLM], Optional[AutoTokenizer]]: | |
| """Lazy load the model with progress feedback""" | |
| if self.loaded: | |
| return self.model, self.tokenizer | |
| if self.loading: | |
| while self.loading: | |
| time.sleep(0.1) | |
| return self.model, self.tokenizer | |
| self.loading = True | |
| try: | |
| if progress: | |
| progress(0.1, desc="Checking GPU availability...") | |
| torch.cuda.empty_cache() | |
| if progress: | |
| progress(0.2, desc="Loading tokenizer...") | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| MODEL_NAME, | |
| trust_remote_code=True | |
| ) | |
| if progress: | |
| progress(0.5, desc="Loading model (this may take a few minutes)...") | |
| model_kwargs = { | |
| "trust_remote_code": True, | |
| "torch_dtype": torch.float16 if self.device == "cuda" else torch.float32, | |
| "device_map": "auto" if self.device == "cuda" else None, | |
| "low_cpu_mem_usage": True, | |
| "offload_folder": "offload" | |
| } | |
| try: | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_NAME, | |
| **model_kwargs | |
| ) | |
| except torch.cuda.OutOfMemoryError: | |
| model_kwargs["device_map"] = None | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_NAME, | |
| **model_kwargs | |
| ).to('cpu') | |
| self.device = 'cpu' | |
| test_input = tokenizer("Test", return_tensors="pt").to(self.device) | |
| _ = model.generate(**test_input, max_new_tokens=1) | |
| self.model = model.eval() | |
| self.tokenizer = tokenizer | |
| self.loaded = True | |
| return model, tokenizer | |
| except Exception as e: | |
| self.error = f"Model loading failed: {str(e)}" | |
| logging.error(self.error) | |
| return None, None | |
| finally: | |
| self.loading = False | |
| # Initialize model loader | |
| model_loader = ModelLoader() | |
| def get_model_and_tokenizer(): | |
| return model_loader.load_model() | |
| # ========== UTILITY FUNCTIONS ========== | |
| def generate_session_token() -> str: | |
| alphabet = string.ascii_letters + string.digits | |
| return ''.join(secrets.choice(alphabet) for _ in range(SESSION_TOKEN_LENGTH)) | |
| def sanitize_input(text: str) -> str: | |
| if not text: | |
| return "" | |
| text = html.escape(text.strip()) | |
| text = re.sub(r'<[^>]*>', '', text) | |
| text = re.sub(r'[^\w\s\-.,!?@#\$%^&*()+=]', '', text) | |
| return text | |
| def validate_name(name: str) -> str: | |
| name = name.strip() | |
| if not name: | |
| raise ValueError("Name cannot be empty.") | |
| if len(name) > 100: | |
| raise ValueError("Name is too long (maximum 100 characters).") | |
| if any(c.isdigit() for c in name): | |
| raise ValueError("Name cannot contain numbers.") | |
| return name | |
| def validate_age(age: Union[int, float, str]) -> int: | |
| try: | |
| age_int = int(age) | |
| if not MIN_AGE <= age_int <= MAX_AGE: | |
| raise ValueError(f"Age must be between {MIN_AGE} and {MAX_AGE}.") | |
| return age_int | |
| except (ValueError, TypeError): | |
| raise ValueError("Please enter a valid age number.") | |
| def validate_file(file_obj) -> None: | |
| if not file_obj: | |
| raise ValueError("Please upload a file first") | |
| file_ext = os.path.splitext(file_obj.name)[1].lower() | |
| if file_ext not in ALLOWED_FILE_TYPES: | |
| raise ValueError(f"Invalid file type. Allowed types: {', '.join(ALLOWED_FILE_TYPES)}") | |
| file_size = os.path.getsize(file_obj.name) / (1024 * 1024) | |
| if file_size > MAX_FILE_SIZE_MB: | |
| raise ValueError(f"File too large. Maximum size is {MAX_FILE_SIZE_MB}MB.") | |
| # ========== ENHANCED PDF PARSING ========== | |
| def parse_transcript_pdf(file_path: str): | |
| """Parse the PDF transcript and extract structured data using pdfplumber""" | |
| student_info = {} | |
| requirements = [] | |
| courses = [] | |
| with pdfplumber.open(file_path) as pdf: | |
| for page in pdf.pages: | |
| text = page.extract_text() | |
| tables = page.extract_tables() | |
| # Parse student information from the first table | |
| if not student_info and len(tables) > 0: | |
| header_row = tables[0][0] | |
| if "Graduation Progress Summary" in header_row[0]: | |
| student_info = { | |
| 'name': tables[0][1][0].split('-')[-1].strip(), | |
| 'id': tables[0][1][0].split('-')[0].strip(), | |
| 'school': tables[0][0][0].split('|')[1].strip(), | |
| 'cohort': tables[0][0][1].replace('Cohort', '').strip(), | |
| 'grade': tables[0][2][0].replace('Current Grade:', '').strip(), | |
| 'grad_year': tables[0][2][1].replace('YOG', '').strip(), | |
| 'gpa_weighted': tables[0][2][2].replace('Weighted GPA', '').strip(), | |
| 'gpa_unweighted': tables[0][0][2].replace('Un-weighted GPA', '').strip(), | |
| 'service_hours': tables[0][0][3].replace('Comm Serv Hours', '').strip(), | |
| 'service_date': tables[0][2][3].replace('Comm Serv Date', '').strip(), | |
| 'total_credits': tables[0][2][4].replace('Total Credits Earned', '').strip(), | |
| 'virtual_grade': tables[0][0][4].replace('Virtual Grade', '').strip() | |
| } | |
| # Parse requirements table | |
| if len(tables) > 1 and "Code" in tables[1][0][0]: | |
| for row in tables[1][1:]: | |
| if len(row) >= 6 and row[0] and row[0] != 'Total': | |
| requirements.append({ | |
| 'code': row[0], | |
| 'desc': row[1], | |
| 'required': float(row[2]) if row[2] else 0, | |
| 'waived': float(row[3]) if row[3] else 0, | |
| 'completed': float(row[4]) if row[4] else 0, | |
| 'status': float(row[5].replace('%', '')) if row[5] and '%' in row[5] else 0 | |
| }) | |
| # Parse course history table | |
| if len(tables) > 2 and "Requirement" in tables[2][0][0]: | |
| for row in tables[2][1:]: | |
| if len(row) >= 10 and row[0]: | |
| courses.append({ | |
| 'requirement': row[0], | |
| 'year': row[1], | |
| 'grade': row[2], | |
| 'course_code': row[3], | |
| 'course_name': row[4], | |
| 'term': row[5], | |
| 'district_num': row[6], | |
| 'grade_earned': row[7], | |
| 'included': row[8], | |
| 'credits': float(row[9]) if row[9] and row[9] not in ['inProgress', ''] else 0, | |
| 'status': 'Completed' if row[9] and row[9] != 'inProgress' else 'In Progress' | |
| }) | |
| return student_info, requirements, courses | |
| def analyze_college_readiness(student_info, requirements, courses): | |
| """Analyze the student's profile for college readiness""" | |
| analysis = { | |
| 'gpa_rating': '', | |
| 'rigor_rating': '', | |
| 'service_rating': '', | |
| 'recommendations': [] | |
| } | |
| # GPA Analysis | |
| weighted_gpa = float(student_info.get('gpa_weighted', 0)) | |
| if weighted_gpa >= 4.5: | |
| analysis['gpa_rating'] = 'Excellent (Highly Competitive)' | |
| elif weighted_gpa >= 3.8: | |
| analysis['gpa_rating'] = 'Strong (Competitive)' | |
| elif weighted_gpa >= 3.0: | |
| analysis['gpa_rating'] = 'Good' | |
| else: | |
| analysis['gpa_rating'] = 'Below Average' | |
| # Course Rigor Analysis | |
| ap_count = sum(1 for course in courses if 'AP' in course['course_name']) | |
| de_count = sum(1 for course in courses if 'DE' in course['course_name']) | |
| honors_count = sum(1 for course in courses if 'Honors' in course['course_name']) | |
| total_rigorous = ap_count + de_count + honors_count | |
| if total_rigorous >= 10: | |
| analysis['rigor_rating'] = 'Very High' | |
| elif total_rigorous >= 6: | |
| analysis['rigor_rating'] = 'High' | |
| elif total_rigorous >= 3: | |
| analysis['rigor_rating'] = 'Moderate' | |
| else: | |
| analysis['rigor_rating'] = 'Low' | |
| # Community Service Analysis | |
| service_hours = int(student_info.get('service_hours', 0)) | |
| if service_hours >= 100: | |
| analysis['service_rating'] = 'Exceptional' | |
| elif service_hours >= 50: | |
| analysis['service_rating'] = 'Strong' | |
| elif service_hours >= 30: | |
| analysis['service_rating'] = 'Adequate' | |
| else: | |
| analysis['service_rating'] = 'Limited' | |
| # Generate recommendations | |
| if weighted_gpa < 3.5 and ap_count < 3: | |
| analysis['recommendations'].append("Consider taking more advanced courses (AP/DE) to strengthen your academic profile") | |
| if service_hours < 50: | |
| analysis['recommendations'].append("Additional community service hours could enhance your college applications") | |
| return analysis | |
| def create_requirements_visualization_matplotlib(requirements): | |
| """Create matplotlib visualization for requirements completion""" | |
| fig, ax = plt.subplots(figsize=(10, 6)) | |
| req_names = [req['code'] for req in requirements] | |
| req_completion = [min(req['status'], 100) for req in requirements] | |
| colors = ['#4CAF50' if x >= 100 else '#FFC107' if x > 0 else '#F44336' for x in req_completion] | |
| bars = ax.barh(req_names, req_completion, color=colors) | |
| ax.set_xlabel('Completion (%)') | |
| ax.set_title('Requirement Completion Status') | |
| ax.set_xlim(0, 100) | |
| # Add value labels | |
| for bar in bars: | |
| width = bar.get_width() | |
| ax.text(width + 1, bar.get_y() + bar.get_height()/2, | |
| f'{width:.1f}%', | |
| ha='left', va='center') | |
| plt.tight_layout() | |
| return fig | |
| def create_credits_distribution_visualization(requirements): | |
| """Create pie chart for credits distribution""" | |
| fig, ax = plt.subplots(figsize=(8, 8)) | |
| core_credits = sum(req['completed'] for req in requirements if req['code'] in ['A-English', 'B-Math', 'C-Science', 'D-Social']) | |
| elective_credits = sum(req['completed'] for req in requirements if req['code'] in ['G-Electives']) | |
| other_credits = sum(req['completed'] for req in requirements if req['code'] in ['E-Arts', 'F-PE']) | |
| credit_values = [core_credits, elective_credits, other_credits] | |
| credit_labels = ['Core Subjects', 'Electives', 'Arts/PE'] | |
| colors = ['#3498db', '#2ecc71', '#9b59b6'] | |
| ax.pie(credit_values, labels=credit_labels, autopct='%1.1f%%', | |
| colors=colors, startangle=90) | |
| ax.set_title('Credit Distribution') | |
| plt.tight_layout() | |
| return fig | |
| # ========== TEXT EXTRACTION FUNCTIONS ========== | |
| def preprocess_text(text: str) -> str: | |
| """Normalize text for more reliable parsing""" | |
| text = re.sub(r'\s+', ' ', text) # Normalize whitespace | |
| text = text.upper() # Standardize case for certain fields | |
| return text | |
| def extract_text_from_file(file_path: str, file_ext: str) -> str: | |
| text = "" | |
| try: | |
| if file_ext == '.pdf': | |
| try: | |
| # First try pdfplumber for better table extraction | |
| student_info, requirements, courses = parse_transcript_pdf(file_path) | |
| if student_info: | |
| # Convert parsed data to text format for compatibility | |
| text += f"STUDENT INFORMATION:\n" | |
| text += f"Name: {student_info.get('name', '')}\n" | |
| text += f"ID: {student_info.get('id', '')}\n" | |
| text += f"School: {student_info.get('school', '')}\n" | |
| text += f"Grade: {student_info.get('grade', '')}\n" | |
| text += f"Graduation Year: {student_info.get('grad_year', '')}\n" | |
| text += f"Weighted GPA: {student_info.get('gpa_weighted', '')}\n" | |
| text += f"Unweighted GPA: {student_info.get('gpa_unweighted', '')}\n" | |
| text += f"Service Hours: {student_info.get('service_hours', '')}\n" | |
| text += f"Total Credits: {student_info.get('total_credits', '')}\n\n" | |
| text += "GRADUATION REQUIREMENTS:\n" | |
| for req in requirements: | |
| text += f"{req['code']} | {req['desc']} | Required: {req['required']} | Completed: {req['completed']} | Status: {req['status']}%\n" | |
| text += "\nCOURSE HISTORY:\n" | |
| for course in courses: | |
| text += f"{course['course_code']} | {course['course_name']} | Grade: {course['grade_earned']} | Credits: {course['credits']} | Status: {course['status']}\n" | |
| return text | |
| # Fall back to regular text extraction if specialized parsing fails | |
| import pdfplumber | |
| with pdfplumber.open(file_path) as pdf: | |
| for page in pdf.pages: | |
| # Try to extract tables first | |
| tables = page.extract_tables({ | |
| "vertical_strategy": "text", | |
| "horizontal_strategy": "text", | |
| "intersection_y_tolerance": 10 | |
| }) | |
| if tables: | |
| for table in tables: | |
| for row in table: | |
| text += " | ".join(str(cell).strip() for cell in row if cell) + "\n" | |
| # Fall back to text extraction if tables are empty | |
| page_text = page.extract_text() | |
| if page_text: | |
| text += page_text + "\n" | |
| if not text.strip(): | |
| raise ValueError("PDFPlumber returned empty text") | |
| except Exception as e: | |
| logging.warning(f"PDFPlumber failed: {str(e)}. Trying PyMuPDF...") | |
| doc = fitz.open(file_path) | |
| for page in doc: | |
| text += page.get_text("text") + '\n' | |
| elif file_ext in ['.png', '.jpg', '.jpeg']: | |
| text = extract_text_with_ocr(file_path) | |
| text = clean_extracted_text(text) | |
| if not text.strip(): | |
| raise ValueError("No text could be extracted.") | |
| return text | |
| except Exception as e: | |
| logging.error(f"Text extraction error: {str(e)}") | |
| raise ValueError(f"Failed to extract text: {str(e)}") | |
| def extract_text_with_ocr(file_path: str) -> str: | |
| try: | |
| image = Image.open(file_path) | |
| image = image.convert('L') | |
| image = image.point(lambda x: 0 if x < 128 else 255, '1') | |
| custom_config = r'--oem 3 --psm 6' | |
| text = pytesseract.image_to_string(image, config=custom_config) | |
| return text | |
| except Exception as e: | |
| raise ValueError(f"OCR processing failed: {str(e)}") | |
| def clean_extracted_text(text: str) -> str: | |
| """Special cleaning for Miami-Dade transcripts""" | |
| # Normalize whitespace | |
| text = re.sub(r'\s+', ' ', text).strip() | |
| # Fix common OCR errors | |
| replacements = { | |
| 'GradeLv1': 'GradeLvl', | |
| 'CrsNu m': 'CrsNum', | |
| 'YOG': 'Year of Graduation', | |
| 'Comm Serv': 'Community Service', | |
| r'\bA\s*-\s*': 'A-', # Fix requirement codes | |
| r'\bB\s*-\s*': 'B-', | |
| r'\bC\s*-\s*': 'C-', | |
| r'\bD\s*-\s*': 'D-', | |
| r'\bE\s*-\s*': 'E-', | |
| r'\bF\s*-\s*': 'F-', | |
| r'\bG\s*-\s*': 'G-', | |
| r'\bZ\s*-\s*': 'Z-' | |
| } | |
| for pattern, replacement in replacements.items(): | |
| text = re.sub(pattern, replacement, text) | |
| # Fix course codes with spaces | |
| text = re.sub(r'(\b[A-Z]{2,4})\s(\d{3}[A-Z]?\b)', r'\1\2', text) | |
| # Fix common OCR errors in credits | |
| text = re.sub(r'in\s*Progress', 'inProgress', text, flags=re.IGNORECASE) | |
| return text | |
| def remove_sensitive_info(text: str) -> str: | |
| text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[REDACTED]', text) | |
| text = re.sub(r'\b\d{6,9}\b', '[ID]', text) | |
| text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', text) | |
| return text | |
| # ========== TRANSCRIPT PARSING ========== | |
| class Course(BaseModel): | |
| requirement: str | |
| school_year: str | |
| grade_level: str | |
| course_code: str | |
| description: str | |
| term: str | |
| district_number: str | |
| fg: str | |
| included: str | |
| credits: str | |
| class GraduationProgress(BaseModel): | |
| student_name: str | |
| student_id: str | |
| current_grade: str | |
| year_of_graduation: str | |
| unweighted_gpa: float | |
| weighted_gpa: float | |
| community_service_hours: int | |
| community_service_date: str | |
| total_credits_earned: float | |
| virtual_grade: str | |
| requirements: Dict[str, Dict[str, float]] | |
| courses: List[Course] | |
| assessments: Dict[str, str] | |
| def validate_parsed_data(parsed_data: Dict) -> bool: | |
| """Ensure all critical fields exist""" | |
| required_fields = [ | |
| ('student_info', 'name'), | |
| ('student_info', 'weighted_gpa'), | |
| ('requirements', 'A-English'), # Sample requirement | |
| ('course_history', 0) # At least one course | |
| ] | |
| for path in required_fields: | |
| current = parsed_data | |
| for key in path: | |
| if key not in current: | |
| raise ValueError(f"Missing critical field: {'.'.join(path)}") | |
| current = current[key] | |
| return True | |
| class TranscriptParser: | |
| def __init__(self): | |
| self.student_data = {} | |
| self.requirements = {} | |
| self.current_courses = [] | |
| self.course_history = [] | |
| self.graduation_status = {} | |
| def parse_transcript(self, text: str) -> Dict: | |
| """Parse transcript text and return structured data""" | |
| try: | |
| text = preprocess_text(text) | |
| # First try the specialized Miami-Dade parser | |
| parsed_data = self._parse_miami_dade_transcript(text) | |
| if parsed_data: | |
| validate_parsed_data(parsed_data) | |
| return parsed_data | |
| # Fall back to simplified parser if detailed parsing fails | |
| parsed_data = self._parse_simplified_transcript(text) | |
| if parsed_data: | |
| return parsed_data | |
| raise ValueError("No data could be parsed from the transcript") | |
| except Exception as e: | |
| logging.error(f"Error parsing transcript: {str(e)}") | |
| raise ValueError(f"Couldn't parse transcript content. Error: {str(e)}") | |
| def _parse_miami_dade_transcript(self, text: str) -> Optional[Dict]: | |
| """Specialized parser for Miami-Dade County Public Schools transcripts""" | |
| try: | |
| parsed_data = { | |
| 'student_info': {}, | |
| 'requirements': {}, | |
| 'course_history': [], | |
| 'assessments': {} | |
| } | |
| # Extract student info with more robust pattern | |
| student_info_match = re.search( | |
| r"(\d{7})\s*-\s*(.*?)\s*\n.*?Current Grade:\s*(\d+).*?YOG\s*(\d{4})", | |
| text, | |
| re.DOTALL | |
| ) | |
| if student_info_match: | |
| parsed_data['student_info']['id'] = student_info_match.group(1) | |
| parsed_data['student_info']['name'] = student_info_match.group(2).strip() | |
| parsed_data['student_info']['grade'] = student_info_match.group(3) | |
| parsed_data['student_info']['year_of_graduation'] = student_info_match.group(4) | |
| # Extract GPA information | |
| gpa_matches = re.findall( | |
| r"(?:Un.?weighted|Weighted)\s*GPA\s*([\d.]+)", | |
| text, | |
| re.IGNORECASE | |
| ) | |
| if len(gpa_matches) >= 1: | |
| parsed_data['student_info']['unweighted_gpa'] = float(gpa_matches[0]) | |
| if len(gpa_matches) >= 2: | |
| parsed_data['student_info']['weighted_gpa'] = float(gpa_matches[1]) | |
| # Extract community service info | |
| service_hours_match = re.search(r"Comm\s*Serv\s*Hours\s*(\d+)", text, re.IGNORECASE) | |
| if service_hours_match: | |
| parsed_data['student_info']['community_service_hours'] = int(service_hours_match.group(1)) | |
| service_date_match = re.search(r"Comm\s*Serv\s*Date\s*(\d{2}/\d{2}/\d{4})", text, re.IGNORECASE) | |
| if service_date_match: | |
| parsed_data['student_info']['community_service_date'] = service_date_match.group(1) | |
| # Extract credits info | |
| credits_match = re.search(r"Total\s*Credits\s*Earned\s*([\d.]+)", text, re.IGNORECASE) | |
| if credits_match: | |
| parsed_data['student_info']['total_credits'] = float(credits_match.group(1)) | |
| # Extract virtual grade | |
| virtual_grade_match = re.search(r"Virtual\s*Grade\s*([A-Z])", text, re.IGNORECASE) | |
| if virtual_grade_match: | |
| parsed_data['student_info']['virtual_grade'] = virtual_grade_match.group(1) | |
| # Extract requirements section - more robust table parsing | |
| req_section = re.search( | |
| r"Code\s*Description\s*Required\s*Waived\s*Completed\s*Status(.*?)(?:\n\s*\n|$)", | |
| text, | |
| re.DOTALL | re.IGNORECASE | |
| ) | |
| if req_section: | |
| req_lines = [line.strip() for line in req_section.group(1).split('\n') if line.strip()] | |
| for line in req_lines: | |
| if '|' in line: # Table format | |
| parts = [part.strip() for part in line.split('|') if part.strip()] | |
| if len(parts) >= 5: # More lenient check for number of columns | |
| try: | |
| code = parts[0] if len(parts) > 0 else "" | |
| description = parts[1] if len(parts) > 1 else "" | |
| required = float(parts[2]) if len(parts) > 2 and parts[2].replace('.','').isdigit() else 0.0 | |
| waived = float(parts[3]) if len(parts) > 3 and parts[3].replace('.','').isdigit() else 0.0 | |
| completed = float(parts[4]) if len(parts) > 4 and parts[4].replace('.','').isdigit() else 0.0 | |
| status = parts[5] if len(parts) > 5 else "" | |
| # Extract percentage if available | |
| percent = 0.0 | |
| if status: | |
| percent_match = re.search(r"(\d+)%", status) | |
| if percent_match: | |
| percent = float(percent_match.group(1)) | |
| parsed_data['requirements'][code] = { | |
| "description": description, | |
| "required": required, | |
| "waived": waived, | |
| "completed": completed, | |
| "percent_complete": percent, | |
| "status": status | |
| } | |
| except (IndexError, ValueError) as e: | |
| logging.warning(f"Skipping malformed requirement line: {line}. Error: {str(e)}") | |
| continue | |
| # Extract assessments section | |
| assess_section = re.search(r"Z-Assessment.*?\n(.*?)(?:\n\s*\n|$)", text, re.DOTALL | re.IGNORECASE) | |
| if assess_section: | |
| assess_lines = [line.strip() for line in assess_section.group(1).split('\n') if line.strip()] | |
| for line in assess_lines: | |
| if '|' in line: | |
| parts = [part.strip() for part in line.split('|') if part.strip()] | |
| if len(parts) >= 5 and parts[0].startswith('Z-'): | |
| name = parts[0].replace('Z-', '').strip() | |
| status = parts[4] if len(parts) > 4 else "" | |
| parsed_data['assessments'][name] = status | |
| # Extract course history with more fault-tolerant parsing | |
| course_section = re.search( | |
| r"Requirement.*?School Year.*?GradeLv1.*?CrsNum.*?Description.*?Term.*?DstNumber.*?FG.*?Incl.*?Credits(.*?)(?:Legend|\Z)", | |
| text, | |
| re.DOTALL | re.IGNORECASE | |
| ) | |
| if course_section: | |
| course_lines = [ | |
| line.strip() for line in course_section.group(1).split('\n') | |
| if line.strip() and '|' in line | |
| ] | |
| for line in course_lines: | |
| parts = [part.strip() for part in line.split('|') if part.strip()] | |
| # More robust handling of course data | |
| try: | |
| course = { | |
| 'requirement': parts[0] if len(parts) > 0 else "", | |
| 'school_year': parts[1] if len(parts) > 1 else "", | |
| 'grade_level': parts[2] if len(parts) > 2 else "", | |
| 'course_code': parts[3] if len(parts) > 3 else "", | |
| 'description': parts[4] if len(parts) > 4 else "", | |
| 'term': parts[5] if len(parts) > 5 else "", | |
| 'district_number': parts[6] if len(parts) > 6 else "", | |
| 'fg': parts[7] if len(parts) > 7 else "", | |
| 'included': parts[8] if len(parts) > 8 else "", | |
| 'credits': parts[9] if len(parts) > 9 else "0" | |
| } | |
| # Handle "inProgress" and empty credits | |
| if "inprogress" in course['credits'].lower() or not course['credits']: | |
| course['credits'] = "0" | |
| elif not course['credits'].replace('.','').isdigit(): | |
| course['credits'] = "0" | |
| parsed_data['course_history'].append(course) | |
| except (IndexError, ValueError) as e: | |
| logging.warning(f"Skipping malformed course line: {line}. Error: {str(e)}") | |
| continue | |
| return parsed_data | |
| except Exception as e: | |
| logging.warning(f"Miami-Dade transcript parsing failed: {str(e)}") | |
| return None | |
| def _parse_simplified_transcript(self, text: str) -> Dict: | |
| """Fallback simplified transcript parser with multiple pattern attempts""" | |
| patterns = [ | |
| (r'(?:COURSE|SUBJECT)\s*CODE.*?GRADE.*?CREDITS(.*?)(?:\n\s*\n|\Z)', 'table'), | |
| (r'([A-Z]{2,4}\s?\d{3}[A-Z]?)\s+(.*?)\s+([A-F][+-]?)\s+(\d+\.?\d*)', 'line'), | |
| (r'(.*?)\s+([A-F][+-]?)\s+(\d+\.?\d*)', 'minimal') | |
| ] | |
| for pattern, pattern_type in patterns: | |
| try: | |
| if pattern_type == 'table': | |
| # Parse tabular data | |
| table_section = re.search(pattern, text, re.DOTALL | re.IGNORECASE) | |
| if table_section: | |
| courses = re.findall(r'([A-Z]{2,4}\s?\d{3}[A-Z]?)\s+(.*?)\s+([A-F][+-]?)\s+(\d+\.?\d*)', | |
| table_section.group(1)) | |
| elif pattern_type == 'line': | |
| courses = re.findall(pattern, text) | |
| else: | |
| courses = re.findall(pattern, text) | |
| if courses: | |
| parsed_data = {'course_history': []} | |
| for course in courses: | |
| if len(course) >= 4: | |
| parsed_data['course_history'].append({ | |
| 'course_code': course[0].strip(), | |
| 'description': course[1].strip(), | |
| 'grade': course[2].strip(), | |
| 'credits': float(course[3]) if course[3] else 0.0 | |
| }) | |
| elif len(course) == 3: | |
| parsed_data['course_history'].append({ | |
| 'description': course[0].strip(), | |
| 'grade': course[1].strip(), | |
| 'credits': float(course[2]) if course[2] else 0.0 | |
| }) | |
| return parsed_data | |
| except Exception as e: | |
| logging.warning(f"Pattern {pattern} failed: {str(e)}") | |
| continue | |
| return None | |
| # ========== ENHANCED ANALYSIS FUNCTIONS ========== | |
| def analyze_gpa(parsed_data: Dict) -> str: | |
| try: | |
| gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0)) | |
| if gpa >= 4.5: | |
| return "🌟 Excellent GPA! You're in the top tier of students." | |
| elif gpa >= 3.5: | |
| return "👍 Good GPA! You're performing above average." | |
| elif gpa >= 2.5: | |
| return "ℹ️ Average GPA. Consider focusing on improvement in weaker areas." | |
| else: | |
| return "⚠️ Below average GPA. Please consult with your academic advisor." | |
| except (TypeError, ValueError, KeyError, AttributeError): | |
| return "❌ Could not analyze GPA." | |
| def analyze_graduation_status(parsed_data: Dict) -> str: | |
| try: | |
| total_required = sum( | |
| float(req.get('required', 0)) | |
| for req in parsed_data.get('requirements', {}).values() | |
| if req and str(req.get('required', '0')).replace('.', '').isdigit() | |
| ) | |
| total_completed = sum( | |
| float(req.get('completed', 0)) | |
| for req in parsed_data.get('requirements', {}).values() | |
| if req and str(req.get('completed', '0')).replace('.', '').isdigit() | |
| ) | |
| completion_percentage = (total_completed / total_required) * 100 if total_required > 0 else 0 | |
| if completion_percentage >= 100: | |
| return "🎉 You've met all graduation requirements!" | |
| elif completion_percentage >= 80: | |
| return f"✅ You've completed {completion_percentage:.1f}% of requirements. Almost there!" | |
| elif completion_percentage >= 50: | |
| return f"🔄 You've completed {completion_percentage:.1f}% of requirements. Keep working!" | |
| else: | |
| return f"⚠️ You've only completed {completion_percentage:.1f}% of requirements. Please meet with your counselor." | |
| except (ZeroDivisionError, TypeError, KeyError, AttributeError): | |
| return "❌ Could not analyze graduation status." | |
| def generate_advice(parsed_data: Dict) -> str: | |
| advice = [] | |
| # GPA advice | |
| try: | |
| gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0)) | |
| if gpa < 3.0: | |
| advice.append("📚 Your GPA could improve. Consider:\n- Seeking tutoring for challenging subjects\n- Meeting with teachers during office hours\n- Developing better study habits") | |
| except (TypeError, ValueError, KeyError, AttributeError): | |
| pass | |
| # Community service advice | |
| try: | |
| service_hours = int(parsed_data.get('student_info', {}).get('community_service_hours', 0)) | |
| if service_hours < 100: | |
| advice.append("🤝 Consider more community service:\n- Many colleges value 100+ hours\n- Look for opportunities that align with your interests") | |
| except (TypeError, ValueError, KeyError, AttributeError): | |
| pass | |
| # Missing requirements advice | |
| try: | |
| missing_reqs = [ | |
| req for code, req in parsed_data.get('requirements', {}).items() | |
| if req and float(req.get('percent_complete', 0)) < 100 and not code.startswith("Z-Assessment") | |
| ] | |
| if missing_reqs: | |
| req_list = "\n- ".join([f"{code}: {req.get('description', '')}" for code, req in missing_reqs]) | |
| advice.append(f"🎓 Focus on completing these requirements:\n- {req_list}") | |
| except (TypeError, ValueError, KeyError, AttributeError): | |
| pass | |
| # Course rigor advice | |
| try: | |
| ap_count = sum(1 for course in parsed_data.get('course_history', []) | |
| if course and "ADVANCED PLACEMENT" in course.get('description', '').upper()) | |
| if ap_count < 3: | |
| advice.append("🧠 Consider taking more challenging courses:\n- AP/IB courses can strengthen college applications\n- Shows academic rigor to admissions officers") | |
| except (TypeError, KeyError, AttributeError): | |
| pass | |
| return "\n\n".join(advice) if advice else "🎯 You're on track! Keep up the good work." | |
| def generate_college_recommendations(parsed_data: Dict) -> str: | |
| try: | |
| gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0)) | |
| ap_count = sum(1 for course in parsed_data.get('course_history', []) | |
| if course and "ADVANCED PLACEMENT" in course.get('description', '').upper()) | |
| service_hours = int(parsed_data.get('student_info', {}).get('community_service_hours', 0)) | |
| recommendations = [] | |
| if gpa >= 4.0 and ap_count >= 5: | |
| recommendations.append("🏛️ Reach Schools: Ivy League, Stanford, MIT, etc.") | |
| if gpa >= 3.7: | |
| recommendations.append("🎓 Competitive Schools: Top public universities, selective private colleges") | |
| if gpa >= 3.0: | |
| recommendations.append("📚 Good Match Schools: State flagship universities, many private colleges") | |
| if gpa >= 2.0: | |
| recommendations.append("🏫 Safety Schools: Community colleges, open admission universities") | |
| # Add scholarship opportunities | |
| if gpa >= 3.5: | |
| recommendations.append("\n💰 Scholarship Opportunities:\n- Bright Futures (Florida)\n- National Merit Scholarship\n- College-specific merit scholarships") | |
| elif gpa >= 3.0: | |
| recommendations.append("\n💰 Scholarship Opportunities:\n- Local community scholarships\n- Special interest scholarships\n- First-generation student programs") | |
| # Add extracurricular advice | |
| if service_hours < 50: | |
| recommendations.append("\n🎭 Extracurricular Advice:\n- Colleges value depth over breadth in activities\n- Consider leadership roles in 1-2 organizations") | |
| if not recommendations: | |
| return "❌ Not enough data to generate college recommendations" | |
| return "Based on your academic profile:\n\n" + "\n\n".join(recommendations) | |
| except: | |
| return "❌ Could not generate college recommendations" | |
| def create_gpa_visualization(parsed_data: Dict): | |
| try: | |
| gpa_data = { | |
| "Type": ["Weighted GPA", "Unweighted GPA"], | |
| "Value": [ | |
| float(parsed_data.get('student_info', {}).get('weighted_gpa', 0)), | |
| float(parsed_data.get('student_info', {}).get('unweighted_gpa', 0)) | |
| ] | |
| } | |
| df = pd.DataFrame(gpa_data) | |
| fig = px.bar(df, x="Type", y="Value", title="GPA Comparison", | |
| color="Type", text="Value", | |
| color_discrete_sequence=["#4C78A8", "#F58518"]) | |
| fig.update_traces(texttemplate='%{text:.2f}', textposition='outside') | |
| fig.update_layout(yaxis_range=[0,5], uniformtext_minsize=8, uniformtext_mode='hide') | |
| return fig | |
| except: | |
| return None | |
| def create_requirements_visualization(parsed_data: Dict): | |
| try: | |
| req_data = [] | |
| for code, req in parsed_data.get('requirements', {}).items(): | |
| if req and req.get('percent_complete'): | |
| completion = float(req['percent_complete']) | |
| req_data.append({ | |
| "Requirement": code, | |
| "Completion (%)": completion, | |
| "Status": "Complete" if completion >= 100 else "Incomplete" | |
| }) | |
| if not req_data: | |
| return None | |
| df = pd.DataFrame(req_data) | |
| fig = px.bar(df, x="Requirement", y="Completion (%)", | |
| title="Graduation Requirements Completion", | |
| color="Status", | |
| color_discrete_map={"Complete": "#2CA02C", "Incomplete": "#D62728"}, | |
| hover_data=["Requirement"]) | |
| fig.update_layout(xaxis={'categoryorder':'total descending'}) | |
| return fig | |
| except: | |
| return None | |
| def parse_transcript(file_obj, progress=gr.Progress()) -> Tuple[str, Optional[Dict]]: | |
| """Process transcript file and return analysis results""" | |
| try: | |
| if not file_obj: | |
| raise gr.Error("Please upload a transcript file first (PDF or image)") | |
| validate_file(file_obj) | |
| file_ext = os.path.splitext(file_obj.name)[1].lower() | |
| # Additional PDF validation | |
| if file_ext == '.pdf': | |
| try: | |
| with open(file_obj.name, 'rb') as f: | |
| PdfReader(f) # Test if PDF is readable | |
| except Exception as e: | |
| raise gr.Error(f"Invalid PDF file: {str(e)}. Please upload a non-corrupted PDF.") | |
| if progress: | |
| progress(0.2, desc="Extracting text from file...") | |
| try: | |
| text = extract_text_from_file(file_obj.name, file_ext) | |
| except Exception as e: | |
| raise ValueError(f"Failed to extract text: {str(e)}. The file may be corrupted or in an unsupported format.") | |
| if not text.strip(): | |
| raise ValueError("The file appears to be empty or contains no readable text.") | |
| if progress: | |
| progress(0.5, desc="Parsing transcript...") | |
| parser = TranscriptParser() | |
| try: | |
| parsed_data = parser.parse_transcript(text) | |
| if not parsed_data: | |
| raise ValueError("No data could be parsed from the transcript.") | |
| except Exception as e: | |
| raise ValueError(f"Couldn't parse transcript content. Error: {str(e)}") | |
| # Perform enhanced analyses | |
| gpa_analysis = analyze_gpa(parsed_data) | |
| grad_status = analyze_graduation_status(parsed_data) | |
| advice = generate_advice(parsed_data) | |
| college_recs = generate_college_recommendations(parsed_data) | |
| gpa_viz = create_gpa_visualization(parsed_data) | |
| req_viz = create_requirements_visualization(parsed_data) | |
| # Format results for display | |
| results = [ | |
| f"📊 GPA Analysis: {gpa_analysis}", | |
| f"🎓 Graduation Status: {grad_status}", | |
| f"💡 Recommendations:\n{advice}", | |
| f"🏫 College Recommendations:\n{college_recs}" | |
| ] | |
| # Store all analysis results in the parsed_data | |
| parsed_data['analysis'] = { | |
| 'gpa_analysis': gpa_analysis, | |
| 'grad_status': grad_status, | |
| 'advice': advice, | |
| 'college_recs': college_recs, | |
| 'visualizations': { | |
| 'gpa_viz': gpa_viz, | |
| 'req_viz': req_viz | |
| } | |
| } | |
| return "\n\n".join(results), parsed_data | |
| except Exception as e: | |
| error_msg = f"Error processing transcript: {str(e)}" | |
| logging.error(error_msg) | |
| raise gr.Error(f"{error_msg}\n\nPossible solutions:\n1. Try a different file format\n2. Ensure text is clear and not handwritten\n3. Check file size (<5MB)") | |
| # ========== LEARNING STYLE QUIZ ========== | |
| class LearningStyleQuiz: | |
| def __init__(self): | |
| self.questions = [ | |
| "When you study for a test, you prefer to:", | |
| "When you need directions to a new place, you prefer:", | |
| "When you learn a new skill, you prefer to:", | |
| "When you're trying to concentrate, you:", | |
| "When you meet new people, you remember them by:", | |
| "When you're assembling furniture or a gadget, you:", | |
| "When choosing a restaurant, you rely most on:", | |
| "When you're in a waiting room, you typically:", | |
| "When giving someone instructions, you tend to:", | |
| "When you're trying to recall information, you:", | |
| "When you're at a museum or exhibit, you:", | |
| "When you're learning a new language, you prefer:", | |
| "When you're taking notes in class, you:", | |
| "When you're explaining something complex, you:", | |
| "When you're at a party, you enjoy:", | |
| "When you're trying to remember a phone number, you:", | |
| "When you're relaxing, you prefer to:", | |
| "When you're learning to use new software, you:", | |
| "When you're giving a presentation, you rely on:", | |
| "When you're solving a difficult problem, you:" | |
| ] | |
| self.options = [ | |
| ["Read the textbook (Reading/Writing)", "Listen to lectures (Auditory)", "Use diagrams/charts (Visual)", "Practice problems (Kinesthetic)"], | |
| ["Look at a map (Visual)", "Have someone tell you (Auditory)", "Write down directions (Reading/Writing)", "Try walking/driving there (Kinesthetic)"], | |
| ["Read instructions (Reading/Writing)", "Have someone show you (Visual)", "Listen to explanations (Auditory)", "Try it yourself (Kinesthetic)"], | |
| ["Need quiet (Reading/Writing)", "Need background noise (Auditory)", "Need to move around (Kinesthetic)", "Need visual stimulation (Visual)"], | |
| ["Their face (Visual)", "Their name (Auditory)", "What you talked about (Reading/Writing)", "What you did together (Kinesthetic)"], | |
| ["Read the instructions carefully (Reading/Writing)", "Look at the diagrams (Visual)", "Ask someone to explain (Auditory)", "Start putting pieces together (Kinesthetic)"], | |
| ["Online photos of the food (Visual)", "Recommendations from friends (Auditory)", "Reading the menu online (Reading/Writing)", "Remembering how it felt to eat there (Kinesthetic)"], | |
| ["Read magazines (Reading/Writing)", "Listen to music (Auditory)", "Watch TV (Visual)", "Fidget or move around (Kinesthetic)"], | |
| ["Write them down (Reading/Writing)", "Explain verbally (Auditory)", "Demonstrate (Visual)", "Guide them physically (Kinesthetic)"], | |
| ["See written words in your mind (Visual)", "Hear the information in your head (Auditory)", "Write it down to remember (Reading/Writing)", "Associate it with physical actions (Kinesthetic)"], | |
| ["Read all the descriptions (Reading/Writing)", "Listen to audio guides (Auditory)", "Look at the displays (Visual)", "Touch interactive exhibits (Kinesthetic)"], | |
| ["Study grammar rules (Reading/Writing)", "Listen to native speakers (Auditory)", "Use flashcards with images (Visual)", "Practice conversations (Kinesthetic)"], | |
| ["Write detailed paragraphs (Reading/Writing)", "Record the lecture (Auditory)", "Draw diagrams and charts (Visual)", "Doodle while listening (Kinesthetic)"], | |
| ["Write detailed steps (Reading/Writing)", "Explain verbally with examples (Auditory)", "Draw diagrams (Visual)", "Use physical objects to demonstrate (Kinesthetic)"], | |
| ["Conversations with people (Auditory)", "Watching others or the environment (Visual)", "Writing notes or texting (Reading/Writing)", "Dancing or physical activities (Kinesthetic)"], | |
| ["See the numbers in your head (Visual)", "Say them aloud (Auditory)", "Write them down (Reading/Writing)", "Dial them on a keypad (Kinesthetic)"], | |
| ["Read a book (Reading/Writing)", "Listen to music (Auditory)", "Watch TV/movies (Visual)", "Do something physical (Kinesthetic)"], | |
| ["Read the manual (Reading/Writing)", "Ask someone to show you (Visual)", "Call tech support (Auditory)", "Experiment with the software (Kinesthetic)"], | |
| ["Detailed notes (Reading/Writing)", "Verbal explanations (Auditory)", "Visual slides (Visual)", "Physical demonstrations (Kinesthetic)"], | |
| ["Write out possible solutions (Reading/Writing)", "Talk through it with someone (Auditory)", "Draw diagrams (Visual)", "Build a model or prototype (Kinesthetic)"] | |
| ] | |
| self.learning_styles = { | |
| "Visual": { | |
| "description": "Visual learners prefer using images, diagrams, and spatial understanding.", | |
| "tips": [ | |
| "Use color coding in your notes", | |
| "Create mind maps and diagrams", | |
| "Watch educational videos", | |
| "Use flashcards with images", | |
| "Highlight important information in different colors" | |
| ], | |
| "careers": [ | |
| "Graphic Designer", "Architect", "Photographer", | |
| "Engineer", "Surgeon", "Pilot" | |
| ] | |
| }, | |
| "Auditory": { | |
| "description": "Auditory learners learn best through listening and speaking.", | |
| "tips": [ | |
| "Record lectures and listen to them", | |
| "Participate in study groups", | |
| "Explain concepts out loud to yourself", | |
| "Use rhymes or songs to remember information", | |
| "Listen to educational podcasts" | |
| ], | |
| "careers": [ | |
| "Musician", "Journalist", "Lawyer", | |
| "Psychologist", "Teacher", "Customer Service" | |
| ] | |
| }, | |
| "Reading/Writing": { | |
| "description": "These learners prefer information displayed as words.", | |
| "tips": [ | |
| "Write detailed notes", | |
| "Create summaries in your own words", | |
| "Read textbooks and articles", | |
| "Make lists to organize information", | |
| "Rewrite your notes to reinforce learning" | |
| ], | |
| "careers": [ | |
| "Writer", "Researcher", "Editor", | |
| "Accountant", "Programmer", "Historian" | |
| ] | |
| }, | |
| "Kinesthetic": { | |
| "description": "Kinesthetic learners learn through movement and hands-on activities.", | |
| "tips": [ | |
| "Use hands-on activities", | |
| "Take frequent movement breaks", | |
| "Create physical models", | |
| "Associate information with physical actions", | |
| "Study while walking or pacing" | |
| ], | |
| "careers": [ | |
| "Athlete", "Chef", "Mechanic", | |
| "Dancer", "Physical Therapist", "Carpenter" | |
| ] | |
| } | |
| } | |
| def evaluate_quiz(self, *answers) -> str: | |
| """Evaluate quiz answers and return learning style results""" | |
| answers = list(answers) | |
| if len(answers) != len(self.questions): | |
| raise gr.Error("Please answer all questions before submitting") | |
| scores = {style: 0 for style in self.learning_styles} | |
| for i, answer in enumerate(answers): | |
| if not answer: | |
| continue | |
| for j, style in enumerate(self.learning_styles): | |
| if answer == self.options[i][j]: | |
| scores[style] += 1 | |
| break | |
| total_answered = sum(1 for ans in answers if ans) | |
| if total_answered == 0: | |
| raise gr.Error("No answers provided") | |
| percentages = {style: (score/total_answered)*100 for style, score in scores.items()} | |
| sorted_styles = sorted(scores.items(), key=lambda x: x[1], reverse=True) | |
| result = "## Your Learning Style Results\n\n" | |
| result += "### Scores:\n" | |
| for style, score in sorted_styles: | |
| result += f"- **{style}**: {score}/{total_answered} ({percentages[style]:.1f}%)\n" | |
| max_score = max(scores.values()) | |
| primary_styles = [style for style, score in scores.items() if score == max_score] | |
| result += "\n### Analysis:\n" | |
| if len(primary_styles) == 1: | |
| primary_style = primary_styles[0] | |
| style_info = self.learning_styles[primary_style] | |
| result += f"Your primary learning style is **{primary_style}**\n\n" | |
| result += f"**{primary_style} Characteristics**:\n" | |
| result += f"{style_info['description']}\n\n" | |
| result += "**Recommended Study Strategies**:\n" | |
| for tip in style_info['tips']: | |
| result += f"- {tip}\n" | |
| result += "\n**Potential Career Paths**:\n" | |
| for career in style_info['careers'][:6]: | |
| result += f"- {career}\n" | |
| complementary = [s for s in sorted_styles if s[0] != primary_style][0][0] | |
| result += f"\nYou might also benefit from some **{complementary}** strategies:\n" | |
| for tip in self.learning_styles[complementary]['tips'][:3]: | |
| result += f"- {tip}\n" | |
| else: | |
| result += "You have multiple strong learning styles:\n" | |
| for style in primary_styles: | |
| result += f"- **{style}**\n" | |
| result += "\n**Combined Learning Strategies**:\n" | |
| result += "You may benefit from combining different learning approaches:\n" | |
| for style in primary_styles: | |
| result += f"\n**{style}** techniques:\n" | |
| for tip in style_info['tips'][:2]: | |
| result += f"- {tip}\n" | |
| result += f"\n**{style}** career suggestions:\n" | |
| for career in style_info['careers'][:3]: | |
| result += f"- {career}\n" | |
| return result | |
| learning_style_quiz = LearningStyleQuiz() | |
| # ========== PROFILE MANAGEMENT ========== | |
| class ProfileManager: | |
| def __init__(self): | |
| self.profiles_dir = Path(PROFILES_DIR) | |
| self.profiles_dir.mkdir(exist_ok=True, parents=True) | |
| self.current_session = None | |
| def set_session(self, session_token: str) -> None: | |
| self.current_session = session_token | |
| def get_profile_path(self, name: str) -> Path: | |
| if self.current_session: | |
| name_hash = hashlib.sha256(name.encode()).hexdigest()[:16] | |
| return self.profiles_dir / f"{name_hash}_{self.current_session}_profile.json" | |
| return self.profiles_dir / f"{name.replace(' ', '_')}_profile.json" | |
| def save_profile(self, name: str, age: Union[int, str], interests: str, | |
| transcript: Dict, learning_style: str, | |
| movie: str, movie_reason: str, show: str, show_reason: str, | |
| book: str, book_reason: str, character: str, character_reason: str, | |
| blog: str) -> str: | |
| try: | |
| name = validate_name(name) | |
| age = validate_age(age) | |
| if not interests.strip(): | |
| raise ValueError("Please describe at least one interest or hobby.") | |
| if not transcript: | |
| raise ValueError("Please complete the transcript analysis first.") | |
| if not learning_style or "Your primary learning style is:" not in learning_style: | |
| raise ValueError("Please complete the learning style quiz first.") | |
| favorites = { | |
| "movie": sanitize_input(movie), | |
| "movie_reason": sanitize_input(movie_reason), | |
| "show": sanitize_input(show), | |
| "show_reason": sanitize_input(show_reason), | |
| "book": sanitize_input(book), | |
| "book_reason": sanitize_input(book_reason), | |
| "character": sanitize_input(character), | |
| "character_reason": sanitize_input(character_reason) | |
| } | |
| data = { | |
| "name": name, | |
| "age": age, | |
| "interests": sanitize_input(interests), | |
| "transcript": transcript, | |
| "learning_style": learning_style, | |
| "favorites": favorites, | |
| "blog": sanitize_input(blog) if blog else "", | |
| "session_token": self.current_session, | |
| "last_updated": time.time() | |
| } | |
| filepath = self.get_profile_path(name) | |
| with open(filepath, "w", encoding='utf-8') as f: | |
| json.dump(data, f, indent=2, ensure_ascii=False) | |
| if HF_TOKEN and 'hf_api' in globals(): | |
| try: | |
| hf_api.upload_file( | |
| path_or_fileobj=filepath, | |
| path_in_repo=f"profiles/{filepath.name}", | |
| repo_id="your-username/student-learning-assistant", | |
| repo_type="dataset" | |
| ) | |
| except Exception as e: | |
| logging.error(f"Failed to upload to HF Hub: {str(e)}") | |
| # Return simple confirmation with GPA if available | |
| confirmation = f"Profile saved successfully for {name}." | |
| if 'gpa' in data.get('transcript', {}).get('student_info', {}): | |
| confirmation += f"\nGPA: {data['transcript']['student_info']['gpa']}" | |
| return confirmation | |
| except Exception as e: | |
| logging.error(f"Profile validation error: {str(e)}") | |
| raise gr.Error(f"Couldn't save profile: {str(e)}") | |
| def load_profile(self, name: str = None, session_token: str = None) -> Dict: | |
| try: | |
| if session_token: | |
| profile_pattern = f"*{session_token}_profile.json" | |
| else: | |
| profile_pattern = "*.json" | |
| profiles = list(self.profiles_dir.glob(profile_pattern)) | |
| if not profiles: | |
| return {} | |
| if name: | |
| name_hash = hashlib.sha256(name.encode()).hexdigest()[:16] | |
| if session_token: | |
| profile_file = self.profiles_dir / f"{name_hash}_{session_token}_profile.json" | |
| else: | |
| profile_file = self.profiles_dir / f"{name_hash}_profile.json" | |
| if not profile_file.exists(): | |
| if HF_TOKEN and 'hf_api' in globals(): | |
| try: | |
| hf_api.download_file( | |
| path_in_repo=f"profiles/{profile_file.name}", | |
| repo_id="your-username/student-learning-assistant", | |
| repo_type="dataset", | |
| local_dir=self.profiles_dir | |
| ) | |
| except: | |
| raise gr.Error(f"No profile found for {name}") | |
| else: | |
| raise gr.Error(f"No profile found for {name}") | |
| else: | |
| profile_file = profiles[0] | |
| with open(profile_file, "r", encoding='utf-8') as f: | |
| profile_data = json.load(f) | |
| if time.time() - profile_data.get('last_updated', 0) > SESSION_TIMEOUT: | |
| raise gr.Error("Session expired. Please start a new session.") | |
| return profile_data | |
| except Exception as e: | |
| logging.error(f"Error loading profile: {str(e)}") | |
| return {} | |
| def list_profiles(self, session_token: str = None) -> List[str]: | |
| if session_token: | |
| profiles = list(self.profiles_dir.glob(f"*{session_token}_profile.json")) | |
| else: | |
| profiles = list(self.profiles_dir.glob("*.json")) | |
| profile_names = [] | |
| for p in profiles: | |
| with open(p, "r", encoding='utf-8') as f: | |
| try: | |
| data = json.load(f) | |
| profile_names.append(data.get('name', p.stem)) | |
| except json.JSONDecodeError: | |
| continue | |
| return profile_names | |
| profile_manager = ProfileManager() | |
| # ========== AI TEACHING ASSISTANT ========== | |
| class TeachingAssistant: | |
| def __init__(self): | |
| self.context_history = [] | |
| self.max_context_length = 5 | |
| async def generate_response(self, message: str, history: List[List[Union[str, None]]], session_token: str) -> str: | |
| try: | |
| profile = profile_manager.load_profile(session_token=session_token) | |
| if not profile: | |
| return "Please complete and save your profile first." | |
| self._update_context(message, history) | |
| # Focus on GPA if mentioned | |
| if "gpa" in message.lower(): | |
| gpa = profile.get("transcript", {}).get("student_info", {}).get("gpa", "unknown") | |
| return f"Your GPA is {gpa}. Would you like advice on improving it?" | |
| # Generic response otherwise | |
| return "I'm your learning assistant. Ask me about your GPA, courses, or study tips." | |
| except Exception as e: | |
| logging.error(f"Error generating response: {str(e)}") | |
| return "I encountered an error. Please try again." | |
| def _update_context(self, message: str, history: List[List[Union[str, None]]]) -> None: | |
| self.context_history.append({"role": "user", "content": message}) | |
| if history: | |
| for h in history[-self.max_context_length:]: | |
| if h[0]: | |
| self.context_history.append({"role": "user", "content": h[0]}) | |
| if h[1]: | |
| self.context_history.append({"role": "assistant", "content": h[1]}) | |
| self.context_history = self.context_history[-(self.max_context_length*2):] | |
| teaching_assistant = TeachingAssistant() | |
| # ========== GRADIO INTERFACE ========== | |
| def create_interface(): | |
| with gr.Blocks(theme=gr.themes.Soft(), title="Student Learning Assistant") as app: | |
| session_token = gr.State(value=generate_session_token()) | |
| profile_manager.set_session(session_token.value) | |
| tab_completed = gr.State({ | |
| 0: False, # Transcript Upload | |
| 1: False, # Learning Style Quiz | |
| 2: False, # Personal Questions | |
| 3: False, # Save & Review | |
| 4: False # AI Assistant | |
| }) | |
| # Custom CSS | |
| app.css = """ | |
| .gradio-container { max-width: 1200px !important; margin: 0 auto !important; } | |
| .tab-content { padding: 20px !important; border: 1px solid #e0e0e0 !important; border-radius: 8px !important; margin-top: 10px !important; } | |
| .completed-tab { background: #4CAF50 !important; color: white !important; } | |
| .incomplete-tab { background: #E0E0E0 !important; } | |
| .nav-message { padding: 10px; margin: 10px 0; border-radius: 4px; background-color: #ffebee; color: #c62828; } | |
| .file-upload { border: 2px dashed #4CAF50 !important; padding: 20px !important; border-radius: 8px !important; text-align: center; } | |
| .file-upload:hover { background: #f5f5f5; } | |
| .progress-bar { height: 5px; background: linear-gradient(to right, #4CAF50, #8BC34A); margin-bottom: 15px; border-radius: 3px; } | |
| .quiz-question { margin-bottom: 15px; padding: 15px; background: #f5f5f5; border-radius: 5px; } | |
| .quiz-results { margin-top: 20px; padding: 20px; background: #e8f5e9; border-radius: 8px; } | |
| .error-message { color: #d32f2f; background-color: #ffebee; padding: 10px; border-radius: 4px; margin: 10px 0; } | |
| .transcript-results { border-left: 4px solid #4CAF50 !important; padding: 15px !important; background: #f8f8f8 !important; } | |
| .error-box { border: 1px solid #ff4444 !important; background: #fff8f8 !important; } | |
| .metric-box { background-color: white; border-radius: 10px; padding: 15px; margin: 10px 0; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } | |
| .recommendation { background-color: #fff8e1; padding: 10px; border-left: 4px solid #ffc107; margin: 5px 0; } | |
| .dark .tab-content { background-color: #2d2d2d !important; border-color: #444 !important; } | |
| .dark .quiz-question { background-color: #3d3d3d !important; } | |
| .dark .quiz-results { background-color: #2e3d2e !important; } | |
| .dark textarea, .dark input { background-color: #333 !important; color: #eee !important; } | |
| .dark .output-markdown { color: #eee !important; } | |
| .dark .chatbot { background-color: #333 !important; } | |
| .dark .chatbot .user, .dark .chatbot .assistant { color: #eee !important; } | |
| .dark .metric-box { background-color: #333 !important; } | |
| """ | |
| # Header | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| gr.Markdown(""" | |
| # Student Learning Assistant | |
| **Your personalized education companion** | |
| Complete each step to get customized learning recommendations. | |
| """) | |
| with gr.Column(scale=1): | |
| dark_mode = gr.Checkbox(label="Dark Mode", value=False) | |
| # Navigation buttons | |
| with gr.Row(): | |
| with gr.Column(scale=1, min_width=100): | |
| step1 = gr.Button("1. Transcript", elem_classes="incomplete-tab") | |
| with gr.Column(scale=1, min_width=100): | |
| step2 = gr.Button("2. Quiz", elem_classes="incomplete-tab", interactive=False) | |
| with gr.Column(scale=1, min_width=100): | |
| step3 = gr.Button("3. Profile", elem_classes="incomplete-tab", interactive=False) | |
| with gr.Column(scale=1, min_width=100): | |
| step4 = gr.Button("4. Review", elem_classes="incomplete-tab", interactive=False) | |
| with gr.Column(scale=1, min_width=100): | |
| step5 = gr.Button("5. Assistant", elem_classes="incomplete-tab", interactive=False) | |
| nav_message = gr.HTML(visible=False) | |
| # Main tabs | |
| with gr.Tabs(visible=True) as tabs: | |
| # ===== TAB 1: TRANSCRIPT UPLOAD ===== | |
| with gr.Tab("Transcript", id=0): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Step 1: Upload Your Transcript") | |
| with gr.Group(elem_classes="file-upload"): | |
| file_input = gr.File( | |
| label="Drag and drop your transcript here (PDF or Image)", | |
| file_types=ALLOWED_FILE_TYPES, | |
| type="filepath" | |
| ) | |
| upload_btn = gr.Button("Analyze Transcript", variant="primary") | |
| file_error = gr.HTML(visible=False) | |
| with gr.Column(scale=2): | |
| transcript_output = gr.Textbox( | |
| label="Analysis Results", | |
| lines=10, | |
| interactive=False, | |
| elem_classes="transcript-results" | |
| ) | |
| with gr.Row(): | |
| gpa_viz = gr.Plot(label="GPA Visualization", visible=False) | |
| req_viz = gr.Plot(label="Requirements Visualization", visible=False) | |
| transcript_data = gr.State() | |
| file_input.change( | |
| fn=lambda f: ( | |
| gr.update(visible=False), | |
| gr.update(value="File ready for analysis!", visible=True) if f | |
| else gr.update(value="Please upload a file", visible=False) | |
| ), | |
| inputs=file_input, | |
| outputs=[file_error, transcript_output] | |
| ) | |
| def process_and_visualize(file_obj, tab_status): | |
| results, data = parse_transcript(file_obj) | |
| # Update visualizations | |
| gpa_viz_update = gr.update(visible=data.get('analysis', {}).get('visualizations', {}).get('gpa_viz') is not None) | |
| req_viz_update = gr.update(visible=data.get('analysis', {}).get('visualizations', {}).get('req_viz') is not None) | |
| # Update tab completion status | |
| tab_status[0] = True | |
| return results, data, gpa_viz_update, req_viz_update, tab_status | |
| upload_btn.click( | |
| fn=process_and_visualize, | |
| inputs=[file_input, tab_completed], | |
| outputs=[transcript_output, transcript_data, gpa_viz, req_viz, tab_completed] | |
| ).then( | |
| fn=lambda: gr.update(elem_classes="completed-tab"), | |
| outputs=step1 | |
| ).then( | |
| fn=lambda: gr.update(interactive=True), | |
| outputs=step2 | |
| ) | |
| # ===== TAB 2: LEARNING STYLE QUIZ ===== | |
| with gr.Tab("Learning Style Quiz", id=1): | |
| with gr.Column(): | |
| gr.Markdown("### Step 2: Discover Your Learning Style") | |
| progress = gr.HTML("<div class='progress-bar' style='width: 0%'></div>") | |
| quiz_components = [] | |
| with gr.Accordion("Quiz Questions", open=True): | |
| for i, (question, options) in enumerate(zip(learning_style_quiz.questions, learning_style_quiz.options)): | |
| with gr.Group(elem_classes="quiz-question"): | |
| q = gr.Radio( | |
| options, | |
| label=f"{i+1}. {question}", | |
| show_label=True | |
| ) | |
| quiz_components.append(q) | |
| with gr.Row(): | |
| quiz_submit = gr.Button("Submit Quiz", variant="primary") | |
| quiz_clear = gr.Button("Clear Answers") | |
| quiz_alert = gr.HTML(visible=False) | |
| learning_output = gr.Markdown( | |
| label="Your Learning Style Results", | |
| visible=False, | |
| elem_classes="quiz-results" | |
| ) | |
| for component in quiz_components: | |
| component.change( | |
| fn=lambda *answers: { | |
| progress: gr.HTML( | |
| f"<div class='progress-bar' style='width: {sum(1 for a in answers if a)/len(answers)*100}%'></div>" | |
| ) | |
| }, | |
| inputs=quiz_components, | |
| outputs=progress | |
| ) | |
| quiz_submit.click( | |
| fn=lambda *answers: learning_style_quiz.evaluate_quiz(*answers), | |
| inputs=quiz_components, | |
| outputs=learning_output | |
| ).then( | |
| fn=lambda: gr.update(visible=True), | |
| outputs=learning_output | |
| ).then( | |
| fn=lambda: {1: True}, | |
| inputs=None, | |
| outputs=tab_completed | |
| ).then( | |
| fn=lambda: gr.update(elem_classes="completed-tab"), | |
| outputs=step2 | |
| ).then( | |
| fn=lambda: gr.update(interactive=True), | |
| outputs=step3 | |
| ) | |
| quiz_clear.click( | |
| fn=lambda: [None] * len(quiz_components), | |
| outputs=quiz_components | |
| ).then( | |
| fn=lambda: gr.HTML("<div class='progress-bar' style='width: 0%'></div>"), | |
| outputs=progress | |
| ) | |
| # ===== TAB 3: PERSONAL QUESTIONS ===== | |
| with gr.Tab("Personal Profile", id=2): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Step 3: Tell Us About Yourself") | |
| with gr.Group(): | |
| name = gr.Textbox(label="Full Name", placeholder="Your name") | |
| age = gr.Number(label="Age", minimum=MIN_AGE, maximum=MAX_AGE, precision=0) | |
| interests = gr.Textbox( | |
| label="Your Interests/Hobbies", | |
| placeholder="e.g., Science, Music, Sports, Art..." | |
| ) | |
| save_personal_btn = gr.Button("Save Information", variant="primary") | |
| save_confirmation = gr.HTML(visible=False) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Favorites") | |
| with gr.Group(): | |
| movie = gr.Textbox(label="Favorite Movie") | |
| movie_reason = gr.Textbox(label="Why do you like it?", lines=2) | |
| show = gr.Textbox(label="Favorite TV Show") | |
| show_reason = gr.Textbox(label="Why do you like it?", lines=2) | |
| book = gr.Textbox(label="Favorite Book") | |
| book_reason = gr.Textbox(label="Why do you like it?", lines=2) | |
| character = gr.Textbox(label="Favorite Character (from any story)") | |
| character_reason = gr.Textbox(label="Why do you like them?", lines=2) | |
| with gr.Accordion("Personal Blog (Optional)", open=False): | |
| blog = gr.Textbox( | |
| label="Share your thoughts", | |
| placeholder="Write something about yourself...", | |
| lines=5 | |
| ) | |
| save_personal_btn.click( | |
| fn=lambda n, a, i, ts: ( | |
| {2: True}, | |
| gr.update(elem_classes="completed-tab"), | |
| gr.update(interactive=True), | |
| gr.update(value="<div class='alert-box'>Information saved!</div>", visible=True) | |
| ), | |
| inputs=[name, age, interests, tab_completed], | |
| outputs=[tab_completed, step3, step4, save_confirmation] | |
| ) | |
| # ===== TAB 4: SAVE & REVIEW ===== | |
| with gr.Tab("Save Profile", id=3): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Step 4: Review & Save Your Profile") | |
| with gr.Group(): | |
| load_profile_dropdown = gr.Dropdown( | |
| label="Load Existing Profile", | |
| choices=profile_manager.list_profiles(session_token.value), | |
| visible=False | |
| ) | |
| with gr.Row(): | |
| load_btn = gr.Button("Load", visible=False) | |
| delete_btn = gr.Button("Delete", variant="stop", visible=False) | |
| save_btn = gr.Button("Save Profile", variant="primary") | |
| clear_btn = gr.Button("Clear Form") | |
| with gr.Column(scale=2): | |
| output_summary = gr.Markdown( | |
| "Your profile summary will appear here after saving.", | |
| label="Profile Summary" | |
| ) | |
| with gr.Row(): | |
| req_viz_matplotlib = gr.Plot(label="Requirements Progress", visible=False) | |
| credits_viz = gr.Plot(label="Credits Distribution", visible=False) | |
| save_btn.click( | |
| fn=profile_manager.save_profile, | |
| inputs=[ | |
| name, age, interests, transcript_data, learning_output, | |
| movie, movie_reason, show, show_reason, | |
| book, book_reason, character, character_reason, blog | |
| ], | |
| outputs=output_summary | |
| ).then( | |
| fn=lambda td: ( | |
| gr.update(visible=True), | |
| gr.update(visible=True) | |
| ) if td and 'requirements' in td else (gr.update(visible=False), gr.update(visible=False)), | |
| inputs=transcript_data, | |
| outputs=[req_viz_matplotlib, credits_viz] | |
| ).then( | |
| fn=lambda: {3: True}, | |
| inputs=None, | |
| outputs=tab_completed | |
| ).then( | |
| fn=lambda: gr.update(elem_classes="completed-tab"), | |
| outputs=step4 | |
| ).then( | |
| fn=lambda: gr.update(interactive=True), | |
| outputs=step5 | |
| ).then( | |
| fn=lambda: profile_manager.list_profiles(session_token.value), | |
| outputs=load_profile_dropdown | |
| ).then( | |
| fn=lambda: gr.update(visible=bool(profile_manager.list_profiles(session_token.value))), | |
| outputs=load_btn | |
| ).then( | |
| fn=lambda: gr.update(visible=bool(profile_manager.list_profiles(session_token.value))), | |
| outputs=delete_btn | |
| ) | |
| # Create visualizations when profile is loaded | |
| load_btn.click( | |
| fn=lambda name: profile_manager.load_profile(name, session_token.value), | |
| inputs=load_profile_dropdown, | |
| outputs=None | |
| ).then( | |
| fn=lambda profile: ( | |
| profile.get('name', ''), | |
| profile.get('age', ''), | |
| profile.get('interests', ''), | |
| profile.get('learning_style', ''), | |
| profile.get('favorites', {}).get('movie', ''), | |
| profile.get('favorites', {}).get('movie_reason', ''), | |
| profile.get('favorites', {}).get('show', ''), | |
| profile.get('favorites', {}).get('show_reason', ''), | |
| profile.get('favorites', {}).get('book', ''), | |
| profile.get('favorites', {}).get('book_reason', ''), | |
| profile.get('favorites', {}).get('character', ''), | |
| profile.get('favorites', {}).get('character_reason', ''), | |
| profile.get('blog', ''), | |
| profile.get('transcript', {}), | |
| gr.update(value="Profile loaded successfully!"), | |
| create_requirements_visualization_matplotlib(profile.get('transcript', {}).get('requirements', [])), | |
| create_credits_distribution_visualization(profile.get('transcript', {}).get('requirements', [])) | |
| ), | |
| inputs=None, | |
| outputs=[ | |
| name, age, interests, learning_output, | |
| movie, movie_reason, show, show_reason, | |
| book, book_reason, character, character_reason, | |
| blog, transcript_data, output_summary, | |
| req_viz_matplotlib, credits_viz | |
| ] | |
| ) | |
| # ===== TAB 5: AI ASSISTANT ===== | |
| with gr.Tab("AI Assistant", id=4): | |
| gr.Markdown("## Your Personalized Learning Assistant") | |
| gr.Markdown("Ask me anything about studying, your courses, grades, or learning strategies.") | |
| async def chat_wrapper(message: str, history: List[List[str]]): | |
| response = await teaching_assistant.generate_response( | |
| message, | |
| history, | |
| session_token.value | |
| ) | |
| return response | |
| chatbot = gr.ChatInterface( | |
| fn=chat_wrapper, | |
| examples=[ | |
| "What's my GPA?", | |
| "How should I study for math?", | |
| "What courses am I taking?", | |
| "Study tips for my learning style" | |
| ], | |
| title="" | |
| ) | |
| # Navigation logic | |
| def navigate_to_tab(tab_index: int, tab_completed_status): | |
| current_tab = tabs.selected | |
| if tab_index <= current_tab: | |
| return gr.Tabs(selected=tab_index), gr.update(visible=False) | |
| # Check all previous tabs are completed | |
| for i in range(tab_index): | |
| if not tab_completed_status.get(i, False): | |
| messages = [ | |
| "Please complete the transcript analysis first", | |
| "Please complete the learning style quiz first", | |
| "Please fill out your personal information first", | |
| "Please save your profile first" | |
| ] | |
| return ( | |
| gr.Tabs(selected=i), | |
| gr.update( | |
| value=f"<div class='error-message'>⛔ {messages[i]}</div>", | |
| visible=True | |
| ) | |
| ) | |
| return gr.Tabs(selected=tab_index), gr.update(visible=False) | |
| step1.click( | |
| lambda idx, status: navigate_to_tab(idx, status), | |
| inputs=[gr.State(0), tab_completed], | |
| outputs=[tabs, nav_message] | |
| ) | |
| step2.click( | |
| lambda idx, status: navigate_to_tab(idx, status), | |
| inputs=[gr.State(1), tab_completed], | |
| outputs=[tabs, nav_message] | |
| ) | |
| step3.click( | |
| lambda idx, status: navigate_to_tab(idx, status), | |
| inputs=[gr.State(2), tab_completed], | |
| outputs=[tabs, nav_message] | |
| ) | |
| step4.click( | |
| lambda idx, status: navigate_to_tab(idx, status), | |
| inputs=[gr.State(3), tab_completed], | |
| outputs=[tabs, nav_message] | |
| ) | |
| step5.click( | |
| lambda idx, status: navigate_to_tab(idx, status), | |
| inputs=[gr.State(4), tab_completed], | |
| outputs=[tabs, nav_message] | |
| ) | |
| # Dark mode toggle | |
| def toggle_dark_mode(dark): | |
| return gr.themes.Soft(primary_hue="blue", secondary_hue="gray") if not dark else gr.themes.Soft(primary_hue="blue", secondary_hue="gray", neutral_hue="slate") | |
| dark_mode.change( | |
| fn=toggle_dark_mode, | |
| inputs=dark_mode, | |
| outputs=None | |
| ) | |
| # Load model on startup | |
| app.load(fn=lambda: model_loader.load_model(), outputs=[]) | |
| return app | |
| app = create_interface() | |
| if __name__ == "__main__": | |
| app.launch() | |