saptak21's picture
Update app.py
bcbf113 verified
Raw
History Blame Contribute Delete
45.2 kB
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
)