File size: 13,612 Bytes
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
bdef2d7
b82077c
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bdef2d7
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7bdd0b5
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bd2916
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bd2916
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bd2916
1e2a4ce
 
 
 
 
 
 
6bd2916
1e2a4ce
 
 
6bd2916
1e2a4ce
 
6bd2916
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bd2916
f2d5943
1e2a4ce
 
 
 
 
 
 
 
 
 
6bd2916
1e2a4ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bd2916
1e2a4ce
 
 
 
 
bdef2d7
1e2a4ce
 
 
 
 
bdef2d7
1e2a4ce
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
"""
Automatic Time Table Generator (Improved)
- Gradio app for Hugging Face Spaces
- Exports: Word (.docx) and Text (.txt)
- Sunday = Holiday
- Languages: English, Sindhi, Urdu
"""

import io
import tempfile
import random
from typing import List, Dict
import gradio as gr
from docx import Document
from datetime import time, timedelta, datetime

# -------------------
# Localization labels
# -------------------
LANG = {
    "English": {
        "app_title": "Automatic Time Table Generator",
        "subtitle": "Generate timetables with subjects, teachers, rooms and period times.",
        "days_label": "Working days (comma-separated, omit Sunday; Sunday will be shown as Holiday)",
        "subjects_label": "Subjects (comma-separated, e.g. Math, English, Science)",
        "teachers_label": "Teachers (comma-separated, will cycle if fewer than subjects)",
        "rooms_label": "Rooms (comma-separated or single numbers, e.g. 1,2,3 ... 10)",
        "classes_label": "Class list (comma-separated, e.g. 1,2,3,4,5)",
        "periods_label": "Periods per day (integer)",
        "duration_label": "Period duration (hours, integer)",
        "start_label": "Start time (24h, e.g. 8 for 08:00)",
        "generate_btn": "Generate & Export (Word / Text)",
        "preview_label": "Generated Timetable Preview",
        "holiday_text": "Holiday",
        "word_label": "Download Word (.docx)",
        "text_label": "Download Text (.txt)",
        "doc_title": "Timetable"
    },
    "Sindhi": {
        "app_title": "خودڪار ٽائيم ٽيبل جنريٽر",
        "subtitle": "مضمون، استادن، ڪمري ۽ دورانيه سان ٽائيم ٽيبل ٺاهيو.",
        "days_label": "ڪم وارا ڏينهن (ڪاما سان الڳ، آچر شامل نه ڪريو؛ آچر موڪل ڏيکاريو ويندو)",
        "subjects_label": "مضمون (ڪاما سان الڳ ڪريو، مثال: Math, English, Science)",
        "teachers_label": "استاد (ڪاما سان الڳ ڪريو، جيڪڏهن گهٽ هجن ته ورجايا ويندا)",
        "rooms_label": "ڪمره (ڪاما سان يا انگ، مثال: 1,2,3 ... 10)",
        "classes_label": "ڪلاس لسٽ (ڪاما سان، مثال: 1,2,3,4,5)",
        "periods_label": "في ڏينهن دورا (عدد)",
        "duration_label": "هر دورو (گهنٽا)",
        "start_label": "شروعات جو وقت (24h، مثال: 8 لاءِ 08:00)",
        "generate_btn": "جنريٽ ۽ ٻاھ ڪڍو (Word / Text)",
        "preview_label": "پريوئو",
        "holiday_text": "موڪل",
        "word_label": "Word (.docx) ڊائونلوڊ ڪريو",
        "text_label": "Text (.txt) ڊائونلوڊ ڪريو",
        "doc_title": "ٽائيم ٽيبل"
    },
    "Urdu": {
        "app_title": "خودکار ٹائم ٹیبل جنریٹر",
        "subtitle": "مضامین، اساتذہ، کمروں اور دورانیوں کے ساتھ جدول بنائیں۔",
        "days_label": "کام کے دن (کاما سے جدا کریں، اتوار شامل نہ کریں؛ اتوار تعطیل دکھایا جائے گا)",
        "subjects_label": "مضامین (کاما سے جدا کریں، مثال: Math, English, Science)",
        "teachers_label": "اساتذہ (کاما سے جدا کریں، اگر کم ہوں گے تو دوبارہ آئیں گے)",
        "rooms_label": "کمروں کے نمبر (کاما سے یا نمبر، مثال: 1,2,3 ... 10)",
        "classes_label": "کلاس فہرست (کاما سے، مثال: 1,2,3,4,5)",
        "periods_label": "روزانہ دورانیے (عدد)",
        "duration_label": "ہر دورانیہ (گھنٹے)",
        "start_label": "شروع وقت (24h، مثال: 8 = 08:00)",
        "generate_btn": "جنریٹ کریں اور ایکسپورٹ کریں (Word / Text)",
        "preview_label": "پریویو",
        "holiday_text": "چھٹی",
        "word_label": "Word (.docx) ڈاؤن لوڈ کریں",
        "text_label": "Text (.txt) ڈاؤن لوڈ کریں",
        "doc_title": "ٹائم ٹیبل"
    }
}

HOLIDAY = "Sunday"

# -------------------
# Utilities
# -------------------
def parse_csv(s: str) -> List[str]:
    return [p.strip() for p in s.split(",") if p.strip()]

def ensure_days(days: List[str]) -> List[str]:
    # Normalize and ensure Sunday at end
    cleaned = []
    for d in days:
        d2 = d.strip().capitalize()
        if d2 and d2.lower() != HOLIDAY.lower():
            if d2 not in cleaned:
                cleaned.append(d2)
    cleaned.append(HOLIDAY)
    return cleaned

def compute_period_times(start_hour: int, duration_hours: int, periods: int) -> List[str]:
    times = []
    current = datetime(2000,1,1, start_hour, 0)
    delta = timedelta(hours=duration_hours)
    for _ in range(periods):
        end = current + delta
        times.append(f"{current.strftime('%H:%M')} - {end.strftime('%H:%M')}")
        current = end
    return times

# -------------------
# Timetable generation
# -------------------
def generate_timetable_for_class(days, subjects, teachers, rooms, period_times):
    """
    returns: dict day -> list of {time, subject, teacher, room}
    """
    mapping = {}
    # ensure rooms list has at least 1 item
    if not rooms:
        rooms = ["Room1"]
    if not teachers:
        teachers = ["TBD"]
    if not subjects:
        subjects = ["Free"]

    for day in days:
        if day.lower() == HOLIDAY.lower():
            mapping[day] = []  # Holiday marker
            continue

        # shuffle subjects for uniqueness
        order = subjects.copy()
        random.shuffle(order)

        day_slots = []
        for idx, t in enumerate(period_times):
            subj = order[idx % len(order)]
            teacher = teachers[idx % len(teachers)]
            room = rooms[idx % len(rooms)]
            day_slots.append({
                "time": t,
                "subject": subj,
                "teacher": teacher,
                "room": room
            })
        mapping[day] = day_slots
    return mapping

def generate_full_timetable(class_list, days, subjects, teachers, rooms, period_times):
    full = {}
    for cls in class_list:
        full[cls] = generate_timetable_for_class(days, subjects, teachers, rooms, period_times)
    return full

# -------------------
# Export helpers
# -------------------
def export_docx_bytes(full_tt: Dict[str, Dict], labels) -> str:
    """
    Create a temporary .docx file and return its path
    """
    doc = Document()
    doc.add_heading(labels["doc_title"], level=0)

    for cls, tt in full_tt.items():
        doc.add_heading(f"Class {cls}", level=1)
        for day, slots in tt.items():
            doc.add_heading(day, level=2)
            if not slots:
                doc.add_paragraph(labels["holiday_text"])
            else:
                table = doc.add_table(rows=1, cols=4)
                hdr = table.rows[0].cells
                hdr[0].text = "Time"
                hdr[1].text = "Subject"
                hdr[2].text = "Teacher"
                hdr[3].text = "Room"
                for s in slots:
                    row = table.add_row().cells
                    row[0].text = s["time"]
                    row[1].text = s["subject"]
                    row[2].text = s["teacher"]
                    row[3].text = s["room"]
                doc.add_paragraph("")  # small space

    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".docx")
    doc.save(tmp.name)
    tmp.close()
    return tmp.name

def export_text_bytes(full_tt: Dict[str, Dict], labels) -> str:
    lines = []
    for cls, tt in full_tt.items():
        lines.append(f"Class {cls}")
        lines.append("-" * (8 + len(str(cls))))
        for day, slots in tt.items():
            lines.append(f"{day}:")
            if not slots:
                lines.append(f"  {labels['holiday_text']}")
            else:
                for s in slots:
                    lines.append(f"  {s['time']} | {s['subject']} | {s['teacher']} | Room: {s['room']}")
        lines.append("")
    txt = "\n".join(lines)
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8")
    tmp.write(txt)
    tmp.close()
    return tmp.name

# -------------------
# Main Gradio function
# -------------------
def build_and_export(
    language: str,
    days_input: str,
    subjects_input: str,
    teachers_input: str,
    rooms_input: str,
    classes_input: str,
    periods_per_day: int,
    duration_hours: int,
    start_hour: int,
    export_choice: str
):
    labels = LANG.get(language, LANG["English"])

    # parse inputs
    days = parse_csv(days_input)
    days = ensure_days(days)
    subjects = [s.strip() for s in parse_csv(subjects_input)]
    teachers = [t.strip() for t in parse_csv(teachers_input)]
    rooms = [r.strip() for r in parse_csv(rooms_input)] or [f"Room{i}" for i in range(1, 11)]
    class_list = [c.strip() for c in parse_csv(classes_input)] or ["1","2","3","4","5"]

    # validation
    if periods_per_day <= 0:
        return "⚠️ Periods per day must be a positive integer.", None, None

    if duration_hours <= 0:
        return "⚠️ Period duration must be a positive integer (hours).", None, None

    if start_hour < 0 or start_hour > 23:
        return "⚠️ Start hour must be between 0 and 23.", None, None

    period_times = compute_period_times(start_hour, duration_hours, periods_per_day)

    full_tt = generate_full_timetable(class_list, days, subjects, teachers, rooms, period_times)

    # Build preview text
    preview_lines = []
    for cls, tt in full_tt.items():
        preview_lines.append(f"Class {cls}")
        preview_lines.append("-" * (8 + len(str(cls))))
        for day, slots in tt.items():
            preview_lines.append(f"{day}:")
            if not slots:
                preview_lines.append(f"  {labels['holiday_text']}")
            else:
                for s in slots:
                    preview_lines.append(f"  {s['time']} | {s['subject']} | {s['teacher']} | Room: {s['room']}")
        preview_lines.append("")
    preview_text = "\n".join(preview_lines)

    # Exports
    docx_path = None
    txt_path = None
    if export_choice in ("Word", "Both"):
        docx_path = export_docx_bytes(full_tt, labels)
    if export_choice in ("Text", "Both"):
        txt_path = export_text_bytes(full_tt, labels)

    # return preview and file paths (Gradio File component accepts a filepath)
    # If a particular export wasn't requested, return None for its file output
    return preview_text, docx_path, txt_path

# -------------------
# Gradio UI
# -------------------
with gr.Blocks(title="Automatic Time Table Generator (Improved)") as demo:
    # Header
    with gr.Row():
        lang = gr.Dropdown(list(LANG.keys()), value="English", label="Language")
        gr.Markdown("<h2 style='margin-left:12px'>Automatic Time Table Generator (Improved)</h2>")

    with gr.Row():
        days_in = gr.Textbox(value="Monday, Tuesday, Wednesday, Thursday, Friday, Saturday", label=LANG["English"]["days_label"])
        classes_in = gr.Textbox(value="1,2,3,4,5", label=LANG["English"]["classes_label"])

    subjects_in = gr.Textbox(value="Math, English, Science, History, Art", label=LANG["English"]["subjects_label"])
    teachers_in = gr.Textbox(value="Mr. Ali, Ms. Fatima, Mr. Khan, Ms. Sara", label=LANG["English"]["teachers_label"])
    rooms_in = gr.Textbox(value="1,2,3,4,5,6,7,8,9,10", label=LANG["English"]["rooms_label"])

    with gr.Row():
        periods_in = gr.Number(value=6, label=LANG["English"]["periods_label"])
        duration_in = gr.Number(value=1, label=LANG["English"]["duration_label"])
        start_in = gr.Number(value=8, label=LANG["English"]["start_label"])

    export_choice = gr.Dropdown(choices=["Word", "Text", "Both"], value="Both", label="Export option")
    generate_btn = gr.Button(LANG["English"]["generate_btn"], variant="primary")

    preview = gr.Textbox(label=LANG["English"]["preview_label"], lines=20, interactive=False)
    download_docx = gr.File(label=LANG["English"]["word_label"])
    download_txt = gr.File(label=LANG["English"]["text_label"])

    # When language changes, update labels (client-side)
    def update_ui_labels(selected_lang):
        labels = LANG.get(selected_lang, LANG["English"])
        return (
            gr.update(label=labels["days_label"]),
            gr.update(label=labels["subjects_label"]),
            gr.update(label=labels["teachers_label"]),
            gr.update(label=labels["rooms_label"]),
            gr.update(label=labels["classes_label"]),
            gr.update(label=labels["periods_label"]),
            gr.update(label=labels["duration_label"]),
            gr.update(label=labels["start_label"]),
            gr.update(value=labels["generate_btn"]),
            gr.update(label=labels["preview_label"]),
            gr.update(label=labels["word_label"]),
            gr.update(label=labels["text_label"])
        )

    lang.change(
        fn=update_ui_labels,
        inputs=[lang],
        outputs=[days_in, subjects_in, teachers_in, rooms_in, classes_in, periods_in, duration_in, start_in, generate_btn, preview, download_docx, download_txt]
    )

    generate_btn.click(
        fn=build_and_export,
        inputs=[lang, days_in, subjects_in, teachers_in, rooms_in, classes_in, periods_in, duration_in, start_in, export_choice],
        outputs=[preview, download_docx, download_txt]
    )

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", share=False)