import gradio as gr import pandas as pd import zipfile import os from collections import defaultdict from openpyxl import Workbook from openpyxl.styles import ( Font, Alignment, Border, Side ) from openpyxl.utils import get_column_letter # ========================================================= # SUBJECT GROUPS # ========================================================= SCIENCE_SUBJECTS = { 'Physics', 'Chemistry', 'Mathematics', 'Biology', 'Statistics', 'Computer Science', 'Bengali', 'English', 'I.C.' } ARTS_SUBJECTS = { 'History', 'Geography', 'Education', 'Political Science', 'Pol. Sc.', 'Philosophy', 'Sanskrit', 'Computer Application', 'Bengali', 'English', 'I.C.' } COMMON_XI_XII = {'Bengali', 'English', 'I.C.'} # ========================================================= # SUBJECT ABBREVIATIONS # ========================================================= SUBJECT_ABBR = { 'Bengali': 'Beng', 'English': 'Eng', 'History': 'Hist', 'Geography': 'Geo', 'Mathematics': 'Math', 'Life Science': 'L. Sc.', 'Sanskrit': 'Sans', 'Physical Science': 'P. Sc.', 'Computer Science': 'COMS', 'Physics': 'Phy', 'Chemistry': 'Chem', 'Statistics': 'Stat', 'Computer': 'Comp', 'Biology': 'Bio', 'Political Science': 'Pol Sc.', 'Pol. Sc.': 'Pol Sc.', 'Philosophy': 'Phil', 'Computer Application': 'COMA', 'Hindi': 'Hindi', 'I.C.': 'I.C.', 'Education': 'Edu' } # ========================================================= # SIMULTANEOUS GROUPS # ========================================================= SIMULTANEOUS_GROUPS = [ {'Biology', 'Computer Science', 'Statistics'}, {'Philosophy', 'Geography'}, {'Education', 'Sanskrit'}, {'Political Science', 'Pol. Sc.', 'Computer Application'} ] # ========================================================= # SCHOOL CONFIG # ========================================================= WORKING_DAYS = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ] PERIODS_WEEKDAYS = [ 'Period 1', 'Period 2', 'Period 3', 'Period 4', 'Period 5', 'Period 6', 'Period 7' ] PERIODS_SATURDAY = [ 'Period 1', 'Period 2', 'Period 3', 'Period 4' ] XI_NOPERIOD_6_7_DAYS = ['Monday', 'Wednesday'] XII_NOPERIOD_6_7_DAYS = ['Monday', 'Tuesday'] V_TO_VII_PRIORITY = ['VII', 'VI', 'V'] ROMAN_CLASS_MAP = { 'V': 5, 'VI': 6, 'VII': 7, 'VIII': 8, 'IX': 9, 'X': 10, 'XI': 11, 'XII': 12 } # ========================================================= # MAIN CLASS # ========================================================= class TimetableGenerator: def __init__(self): self.timetable = defaultdict(lambda: defaultdict(dict)) self.teacher_weekly_load = defaultdict(int) self.unassigned = [] # ===================================================== # LOAD FILES # ===================================================== def load_files(self, teacher_file, class_dist_file, section_file): self.teachers_df = pd.read_excel(teacher_file) self.class_dist_df = pd.read_excel(class_dist_file) self.section_df = pd.read_excel(section_file) self.class_dist_df['Class'] = ( self.class_dist_df['Class'] .astype(str) .str.strip() .str.upper() ) self.section_df['Class'] = ( self.section_df['Class'] .astype(str) .str.strip() .str.upper() ) self.all_teachers = ( self.teachers_df['Teachers Name'] .dropna() .unique() .tolist() ) for t in self.all_teachers: self.teacher_weekly_load[t] = 0 # ===================================================== # CLASS NUMBER # ===================================================== def class_to_num(self, cls): return ROMAN_CLASS_MAP.get( str(cls).strip().upper(), 0 ) # ===================================================== # ELIGIBLE TEACHERS # ===================================================== def get_eligible_teachers(self, subject, cls=None): eligibles = [] cls_num = self.class_to_num(cls) if cls else 0 # ===================================================== # SPECIAL RULE FOR PHYSICAL SCIENCE # ===================================================== if subject == 'Physical Science': if 8 <= cls_num <= 10: physics_teachers = [] chemistry_teachers = [] for _, row in self.teachers_df.iterrows(): teacher = row['Teachers Name'] main_sub = str(row.get('Main Subject', '')).strip() if main_sub == 'Physics': if teacher not in physics_teachers: physics_teachers.append(teacher) elif main_sub == 'Chemistry': if teacher not in chemistry_teachers: chemistry_teachers.append(teacher) eligibles.extend(physics_teachers) for t in chemistry_teachers: if t not in eligibles: eligibles.append(t) elif 5 <= cls_num <= 7: for _, row in self.teachers_df.iterrows(): teacher = row['Teachers Name'] for col in ['Main Subject', 'Second Subject', 'Third Subject']: sub = str(row.get(col, '')).strip() if sub in ['Physics', 'Chemistry']: if teacher not in eligibles: eligibles.append(teacher) return eligibles for col in ['Main Subject', 'Second Subject', 'Third Subject']: if col in self.teachers_df.columns: matches = self.teachers_df[self.teachers_df[col] == subject]['Teachers Name'].tolist() for t in matches: if t not in eligibles: eligibles.append(t) return eligibles # =============================== # Backfilling / Repair Scheduling # ============================== def repair_unassigned_classes( self, teacher_occupied, teacher_daily_load, class_daily_subjects, max_period_lookup ): remaining_unassigned = [] for item in self.unassigned: cs = item['Class-Section'] sub = item['Subject'] remaining = item['Unassigned Periods'] cls = cs.split('-')[0] eligible_teachers = self.get_eligible_teachers(sub, cls) assigned_count = 0 for day in WORKING_DAYS: if assigned_count >= remaining: break periods = PERIODS_SATURDAY if day == 'Saturday' else PERIODS_WEEKDAYS if cls == 'XI' and 'A' in cs and day in XI_NOPERIOD_6_7_DAYS: periods = periods[:5] if cls == 'XII' and 'A' in cs and day in XII_NOPERIOD_6_7_DAYS: periods = periods[:5] for p in periods: if assigned_count >= remaining: break if self.timetable[cs][day].get(p): continue for teacher in eligible_teachers: if p in teacher_occupied[day][teacher]: continue max_p = max_period_lookup.get(teacher, 0) if teacher_daily_load[teacher][day] >= max_p: continue display_sub = SUBJECT_ABBR.get(sub, sub) self.timetable[cs][day][p] = {'subject': f"{display_sub} - {teacher}",'teacher': teacher,'raw_sub': sub} teacher_occupied[day][teacher].add(p) teacher_daily_load[teacher][day] += 1 self.teacher_weekly_load[teacher] += 1 assigned_count += 1 break still_left = remaining - assigned_count if still_left > 0: item['Unassigned Periods'] = still_left remaining_unassigned.append(item) self.unassigned = remaining_unassigned # ===================================================== # SIMULTANEOUS CHECK # ===================================================== def can_overlap(self, sub1, sub2): for group in SIMULTANEOUS_GROUPS: if sub1 in group and sub2 in group: return True return False # ===================================================== # UNASSIGNED REPORT # ===================================================== def add_unassigned( self, cls_sec, subject, remaining, reason ): if remaining > 0: self.unassigned.append({ "Class-Section": cls_sec, "Subject": subject, "Unassigned Periods": remaining, "Reason": reason }) def get_failure_reason( self, eligible_teachers, teacher_occupied, teacher_daily_load, periods_by_day, max_period_lookup ): for teacher in eligible_teachers: max_p = max_period_lookup.get(teacher, 0) for day, periods in periods_by_day.items(): if teacher_daily_load[teacher][day] < max_p: for p in periods: if p not in teacher_occupied[day][teacher]: return "No free slot" return "Teacher unavailable" # ===================================================== # EXCEL FORMATTING # ===================================================== def format_excel(self, ws): thin = Side( border_style="thin", color="000000" ) border = Border( left=thin, right=thin, top=thin, bottom=thin ) for row in ws.iter_rows(): for cell in row: cell.alignment = Alignment( horizontal='center', vertical='center', wrap_text=True ) cell.border = border for col in ws.columns: max_len = 0 col_letter = get_column_letter( col[0].column ) for cell in col: try: val = str(cell.value) if len(val) > max_len: max_len = len(val) except: pass ws.column_dimensions[ col_letter ].width = max_len + 4 # ========================================================= # FULL GENERATE ROUTINE FUNCTION # ========================================================= def generate_routine(self): self.timetable = defaultdict(lambda: defaultdict(dict)) self.unassigned = [] # Reset weekly load every time routine is generated self.teacher_weekly_load = defaultdict(int) for teacher in self.all_teachers: self.teacher_weekly_load[teacher] = 0 teacher_daily_load = defaultdict(lambda: defaultdict(int)) teacher_occupied = defaultdict(lambda: defaultdict(set)) class_daily_subjects = defaultdict(lambda: defaultdict(set)) class_req = defaultdict(dict) max_period_lookup = {} # ===================================================== # TEACHER DAILY MAX PERIOD LOOKUP # ===================================================== for _, row in self.teachers_df.iterrows(): max_period_lookup[row['Teachers Name']] = row['Max Periods Per Day'] # ===================================================== # CLASS SUBJECT REQUIREMENTS # ===================================================== for _, r in self.class_dist_df.iterrows(): cls = str(r['Class']).strip().upper() sub = str(r['Subject']).strip() ppw = int(r['Periods per Week']) class_req[cls][sub] = ppw sorted_classes = sorted( class_req.keys(), key=lambda x: ( x not in V_TO_VII_PRIORITY, self.class_to_num(x) ) ) # ===================================================== # STEP 1: PRIORITY COMMON XI/XII SUBJECTS # Bengali, English, I.C. common between Science and Arts # ===================================================== for priority_cls in ['XI', 'XII']: if priority_cls not in class_req: continue sections = self.section_df[ self.section_df['Class'] == priority_cls ]['Section'].tolist() sci_sec = next( ( s for s in sections if str(s).upper() in ['A', 'SCIENCE'] ), None ) art_sec = next( ( s for s in sections if str(s).upper() in ['B', 'ARTS'] ), None ) sci_cs = f"{priority_cls}-{sci_sec}" if sci_sec else None art_cs = f"{priority_cls}-{art_sec}" if art_sec else None for sub in ['Bengali', 'English', 'I.C.']: if sub not in class_req[priority_cls]: continue eligible = self.get_eligible_teachers( sub, priority_cls ) if not eligible: if sci_cs: self.add_unassigned( sci_cs, sub, class_req[priority_cls][sub], "Teacher unavailable" ) if art_cs: self.add_unassigned( art_cs, sub, class_req[priority_cls][sub], "Teacher unavailable" ) continue remaining = class_req[priority_cls][sub] for day in WORKING_DAYS: if remaining <= 0: break periods = ( PERIODS_SATURDAY if day == 'Saturday' else PERIODS_WEEKDAYS ) if priority_cls == 'XI' and day in XI_NOPERIOD_6_7_DAYS: periods = periods[:5] if priority_cls == 'XII' and day in XII_NOPERIOD_6_7_DAYS: periods = periods[:5] for p in periods: if remaining <= 0: break if ( sci_cs and p in self.timetable[sci_cs][day] ) or ( art_cs and p in self.timetable[art_cs][day] ): continue assigned = False for t in eligible: if ( p not in teacher_occupied[day][t] and teacher_daily_load[t][day] < max_period_lookup.get(t, 0) ): disp = SUBJECT_ABBR.get( sub, sub ) if sci_cs: self.timetable[sci_cs][day][p] = { 'subject': f"{disp} - {t}", 'teacher': t, 'raw_sub': sub } if art_cs: self.timetable[art_cs][day][p] = { 'subject': f"{disp} - {t}", 'teacher': t, 'raw_sub': sub } teacher_occupied[day][t].add(p) teacher_daily_load[t][day] += 1 self.teacher_weekly_load[t] += 1 remaining -= 1 assigned = True break if assigned: break if remaining > 0: if sci_cs: self.add_unassigned( sci_cs, sub, remaining, "Common slot unavailable" ) if art_cs: self.add_unassigned( art_cs, sub, remaining, "Common slot unavailable" ) # ===================================================== # STEP 2: NORMAL SCHEDULING # ===================================================== for cls in sorted_classes: sections = self.section_df[ self.section_df['Class'] == cls ]['Section'].tolist() # ================================================= # CLASSES V-X # Uniform multiplicity without same-teacher same-slot clash # ================================================= if self.class_to_num(cls) <= 10: sections = sorted(sections) num_sections = len(sections) # If no section exists, skip safely if num_sections == 0: continue def get_teacher_weekly_limit(teacher): """ Uses optional 'Max Periods Per Week' column. If missing or blank, old behavior is preserved with 999. """ if 'Max Periods Per Week' not in self.teachers_df.columns: return 999 row = self.teachers_df[ self.teachers_df['Teachers Name'] == teacher ] if row.empty: return 999 value = row.iloc[0].get( 'Max Periods Per Week', 999 ) try: if pd.isna(value): return 999 return int(value) except (TypeError, ValueError): return 999 def find_sectionwise_plan( teacher, subject, count_per_section ): """ Finds a complete section-wise plan. Conditions: - Teacher teaches count_per_section periods in EACH section. - Same teacher is never placed in multiple sections in the same day-period slot. - Each section receives same multiplicity. - Same subject is not repeated on the same day in the same section. - Teacher daily load is respected. - Existing occupied slots are respected. """ temp_teacher_occupied = defaultdict(set) temp_teacher_daily_load = defaultdict(int) temp_class_subject_days = defaultdict(set) for d in WORKING_DAYS: temp_teacher_occupied[d] = set( teacher_occupied[d][teacher] ) temp_teacher_daily_load[d] = teacher_daily_load[ teacher ][d] for sec in sections: cs = f"{cls}-{sec}" for d in WORKING_DAYS: temp_class_subject_days[cs].update( class_daily_subjects[cs][d] ) plan = { sec: [] for sec in sections } for sec in sections: cs = f"{cls}-{sec}" assigned_for_section = 0 for day in WORKING_DAYS: if assigned_for_section >= count_per_section: break # Existing same-subject-on-same-day restriction if subject in class_daily_subjects[cs][day]: continue # Temporary same-subject-on-same-day restriction if subject in temp_class_subject_days[f"{cs}-{day}"]: continue periods = ( PERIODS_SATURDAY if day == 'Saturday' else PERIODS_WEEKDAYS ) for p in periods: if assigned_for_section >= count_per_section: break # Section slot must be free if p in self.timetable[cs][day]: continue # Teacher must not already be occupied # in this period if p in temp_teacher_occupied[day]: continue # Daily load must allow this period if ( temp_teacher_daily_load[day] >= max_period_lookup.get(teacher, 0) ): continue plan[sec].append( ( day, p ) ) temp_teacher_occupied[day].add(p) temp_teacher_daily_load[day] += 1 temp_class_subject_days[f"{cs}-{day}"].add( subject ) assigned_for_section += 1 # Only one period of the same subject # per day for the same section break if assigned_for_section < count_per_section: return None return plan # ================================================= # SUBJECT-WISE DISTRIBUTION FOR V-X # ================================================= for sub, req in sorted( class_req[cls].items(), key=lambda x: x[1], reverse=True ): eligible = self.get_eligible_teachers( sub, cls ) if not eligible: for sec in sections: cs = f"{cls}-{sec}" self.add_unassigned( cs, sub, req, "Teacher unavailable" ) continue remaining_per_section = req # Prefer teachers with lower current weekly load eligible = sorted( eligible, key=lambda t: self.teacher_weekly_load[t] ) for teacher in eligible: if remaining_per_section <= 0: break weekly_limit = get_teacher_weekly_limit( teacher ) weekly_remaining_capacity = ( weekly_limit - self.teacher_weekly_load[teacher] ) if weekly_remaining_capacity <= 0: continue # Since the same teacher has to teach each section # for this share, total required load is: # share_per_section * number_of_sections max_possible_per_section = ( weekly_remaining_capacity // num_sections ) if max_possible_per_section <= 0: continue target_per_section = min( remaining_per_section, max_possible_per_section ) selected_plan = None selected_count = 0 # Try largest possible equal section-wise share first for trial_count in range( target_per_section, 0, -1 ): plan = find_sectionwise_plan( teacher, sub, trial_count ) if plan: selected_plan = plan selected_count = trial_count break if not selected_plan: continue disp = SUBJECT_ABBR.get( sub, sub ) # ============================================= # COMMIT PLAN # ============================================= for sec in sections: cs = f"{cls}-{sec}" for day, p in selected_plan[sec]: self.timetable[cs][day][p] = { 'subject': f"{disp} - {teacher}", 'teacher': teacher, 'raw_sub': sub } teacher_occupied[day][teacher].add(p) teacher_daily_load[teacher][day] += 1 class_daily_subjects[cs][day].add(sub) self.teacher_weekly_load[teacher] += 1 remaining_per_section -= selected_count if self.teacher_weekly_load[teacher] >= weekly_limit: continue # ============================================= # UNASSIGNED REPORT FOR V-X # ============================================= if remaining_per_section > 0: p_map = {} for d in WORKING_DAYS: p_map[d] = ( PERIODS_SATURDAY if d == 'Saturday' else PERIODS_WEEKDAYS ) reason = self.get_failure_reason( eligible, teacher_occupied, teacher_daily_load, p_map, max_period_lookup ) if reason == "No free slot": reason = "Section-wise slot unavailable" for sec in sections: cs = f"{cls}-{sec}" self.add_unassigned( cs, sub, remaining_per_section, reason ) # ================================================= # CLASSES XI-XII # Existing stream logic unchanged # ================================================= else: for sec in sections: cs = f"{cls}-{sec}" stream_subs = ( SCIENCE_SUBJECTS if str(sec).upper() in ['A', 'SCIENCE'] else ARTS_SUBJECTS ) for sub, req in class_req[cls].items(): if sub not in stream_subs or sub in COMMON_XI_XII: continue eligible = self.get_eligible_teachers( sub, cls ) if not eligible: self.add_unassigned( cs, sub, req, "Teacher unavailable" ) continue rem = req for day in WORKING_DAYS: if rem <= 0: break if sub in class_daily_subjects[cs][day]: continue periods = ( PERIODS_SATURDAY if day == 'Saturday' else PERIODS_WEEKDAYS ) if ( cls == 'XI' and str(sec).upper() in ['A', 'SCIENCE'] and day in XI_NOPERIOD_6_7_DAYS ): periods = periods[:5] if ( cls == 'XII' and str(sec).upper() in ['A', 'SCIENCE'] and day in XII_NOPERIOD_6_7_DAYS ): periods = periods[:5] for p in periods: if rem <= 0: break existing = self.timetable[cs][day].get(p) if existing and not self.can_overlap( sub, existing['raw_sub'] ): continue for t in eligible: if ( p not in teacher_occupied[day][t] and teacher_daily_load[t][day] < max_period_lookup.get(t, 0) ): disp = SUBJECT_ABBR.get( sub, sub ) if existing: self.timetable[cs][day][p]['subject'] = ( f"{self.timetable[cs][day][p]['subject']} " f"/ {disp} - {t}" ) self.timetable[cs][day][p]['teacher'] = ( f"{self.timetable[cs][day][p].get('teacher', '')} / {t}" if self.timetable[cs][day][p].get('teacher', '') else t ) else: self.timetable[cs][day][p] = { 'subject': f"{disp} - {t}", 'teacher': t, 'raw_sub': sub } teacher_occupied[day][t].add(p) teacher_daily_load[t][day] += 1 class_daily_subjects[cs][day].add( sub ) self.teacher_weekly_load[t] += 1 rem -= 1 break if sub in class_daily_subjects[cs][day]: break if rem > 0: p_map = {} for d in WORKING_DAYS: d_p = ( PERIODS_SATURDAY if d == 'Saturday' else PERIODS_WEEKDAYS ) if str(sec).upper() in ['A', 'SCIENCE']: if cls == 'XI' and d in XI_NOPERIOD_6_7_DAYS: d_p = d_p[:5] if cls == 'XII' and d in XII_NOPERIOD_6_7_DAYS: d_p = d_p[:5] p_map[d] = d_p reason = self.get_failure_reason( eligible, teacher_occupied, teacher_daily_load, p_map, max_period_lookup ) self.add_unassigned( cs, sub, rem, reason ) # ===================================================== # FINAL REPAIR STEP # ===================================================== # Important: # repair_unassigned_classes() repairs one class-section # at a time. # # For V-X, this can break equal multiplicity across sections. # Therefore: # - V-X unassigned records are preserved. # - Only XI-XII unassigned records are repaired. # ===================================================== vx_unassigned = [] repairable_unassigned = [] for item in self.unassigned: cls_name = str( item["Class-Section"] ).split("-", 1)[0] if self.class_to_num(cls_name) <= 10: vx_unassigned.append(item) else: repairable_unassigned.append(item) self.unassigned = repairable_unassigned self.repair_unassigned_classes( teacher_occupied, teacher_daily_load, class_daily_subjects, max_period_lookup ) # Restore V-X unassigned records after XI-XII repair self.unassigned = vx_unassigned + self.unassigned def create_central_routine(self): wb = Workbook() ws = wb.active ws.append(['Day', 'Class', 'Section'] + PERIODS_WEEKDAYS) for cs in sorted(self.timetable.keys()): cls, sec = cs.split('-', 1) for day in WORKING_DAYS: row = [day, cls, sec] for p in PERIODS_WEEKDAYS: entry = self.timetable[cs][day].get(p, {}) row.append(f"{entry.get('subject','')} - {entry.get('teacher','')}" if entry else "") ws.append(row) self.format_excel(ws); path = "/tmp/Central_Routine.xlsx"; wb.save(path); return path def create_zip(self, filter_func, name): path = f"/tmp/{name}.zip" with zipfile.ZipFile(path, 'w') as z: for cs in sorted(self.timetable.keys()): if filter_func(cs.split('-')[0]): wb = Workbook() ws = wb.active ws.append( ['Day'] + PERIODS_WEEKDAYS ) for day in WORKING_DAYS: row = [day] for p in PERIODS_WEEKDAYS: entry = self.timetable[ cs ][day].get(p, {}) row.append( entry.get('subject', '') ) ws.append(row) temp = f"/tmp/Routine_{cs}.xlsx" self.format_excel(ws) wb.save(temp) z.write( temp, os.path.basename(temp) ) return path def create_teacher_routine(self): wb = Workbook() for i, day in enumerate(WORKING_DAYS): ws = ( wb.active if i == 0 else wb.create_sheet(day) ) ws.title = day periods = ( PERIODS_SATURDAY if day == 'Saturday' else PERIODS_WEEKDAYS ) ws.append( ['Teacher'] + periods ) teacher_map = defaultdict( lambda: {p: "" for p in periods} ) for cs, days in self.timetable.items(): for p, data in days.get(day, {}).items(): teacher = data.get( 'teacher', '' ) if not teacher: continue current = teacher_map[ teacher ][p] new_text = ( f"{cs}" f"({data['subject']})" ) if current: teacher_map[ teacher ][p] = ( current + "\n" + new_text ) else: teacher_map[ teacher ][p] = new_text for t in sorted(self.all_teachers): row = [t] for p in periods: row.append( teacher_map[t][p] ) ws.append(row) self.format_excel(ws) path = "/tmp/Teacher_Routine.xlsx" wb.save(path) return path def create_load(self): wb = Workbook(); ws = wb.active; ws.append(['Teacher', 'Weekly Load']) for t in sorted(self.all_teachers): ws.append([t, self.teacher_weekly_load[t]]) self.format_excel(ws); path = "/tmp/Teacher_Load.xlsx"; wb.save(path); return path def create_unassigned_report(self): wb = Workbook(); ws = wb.active; ws.append(["Class-Section", "Subject", "Unassigned Periods", "Reason"]) for row in self.unassigned: ws.append([row["Class-Section"], row["Subject"], row["Unassigned Periods"], row["Reason"]]) self.format_excel(ws); path = "/tmp/Unassigned_Classes.xlsx"; wb.save(path); return path def ui_fn(t, d, s): if not all([t, d, s]): return [None]*6 + ["Error: Upload all files"] try: g = TimetableGenerator(); g.load_files(t.name, d.name, s.name); g.generate_routine() return g.create_central_routine(), g.create_zip(lambda x: g.class_to_num(x)<=10, "V_X"), g.create_zip(lambda x: g.class_to_num(x)>10, "XI_XII"), g.create_teacher_routine(), g.create_load(), g.create_unassigned_report(), "Success" except Exception as e: return [None]*6 + [f"Error: {str(e)}"] import gradio as gr custom_css = """ body { background: linear-gradient(to right, #dbeafe, #fce7f3); } .gradio-container { font-family: 'Segoe UI', sans-serif; } #title { text-align: center; font-size: 38px; font-weight: bold; color: #1e3a8a; margin-bottom: 10px; } .upload-box { border-radius: 18px !important; border: 2px solid #60a5fa !important; background: #ffffffdd !important; } .output-box { border-radius: 18px !important; border: 2px solid #34d399 !important; background: #f0fdf4 !important; } .generate-btn { background: linear-gradient(to right, #2563eb, #7c3aed) !important; color: white !important; border-radius: 14px !important; font-size: 20px !important; height: 55px !important; border: none !important; } .generate-btn:hover { background: linear-gradient(to right, #1d4ed8, #6d28d9) !important; } .status-box textarea { background: #fefce8 !important; color: #92400e !important; font-weight: bold !important; } """ with gr.Blocks( title="School Timetable Generator", css=custom_css, theme=gr.themes.Soft( primary_hue="blue", secondary_hue="purple", neutral_hue="slate" ) ) as demo: gr.Image( value="https://drive.google.com/uc?id=1GrjJOlrbi7rtJksQaidl2ZjugEWzqEFa", height=500, width=6000, show_label=False, container=False ) gr.Markdown( "# ★ School Timetable Generator", elem_id="title" ) with gr.Row(): t_in = gr.File( label="📘 Teacher File", elem_classes="upload-box" ) d_in = gr.File( label="📗 Distribution File", elem_classes="upload-box" ) s_in = gr.File( label="📙 Section File", elem_classes="upload-box" ) btn = gr.Button( "🚀 Generate Timetable", elem_classes="generate-btn" ) with gr.Row(): out1 = gr.File( label="📄 Central Routine", elem_classes="output-box" ) out2 = gr.File( label="🏫 V-X Routine", elem_classes="output-box" ) out3 = gr.File( label="🎓 XI-XII Routine", elem_classes="output-box" ) out4 = gr.File( label="👨‍🏫 Teacher Routine", elem_classes="output-box" ) out5 = gr.File( label="📊 Teacher Load", elem_classes="output-box" ) out6 = gr.File( label="⚠️ Unassigned Classes", elem_classes="output-box" ) status = gr.Textbox( label="📢 Status", lines=3, elem_classes="status-box" ) btn.click( ui_fn, [t_in, d_in, s_in], [out1, out2, out3, out4, out5, out6, status] ) demo.launch( debug=True, share=True )