File size: 9,962 Bytes
68a82f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Synthetic stand-in for Kodep/jev-topic-data (which is private and not yet pushed).

Same structure as the spec: 56 topics with descriptions, 279 train / 85 val / 105 test
items, skewed class counts, 14 topics with no training items, hard pairs sharing
vocabulary. The TOPICS are ours and the text is machine-generated — treat the metrics
as pipeline validation, not as a benchmark.
"""
import json, os, random

TOPICS = [
    # (name, description, keywords) — pairs listed in config hard_pairs share keywords
    ("soccer", "soccer practice, games and team news", ["soccer", "practice", "field", "game"]),
    ("sports_general", "general sports and athletic program news", ["sports", "practice", "game", "athletic"]),
    ("swim_club", "swim club sessions, meets and pool rules", ["swim", "pool", "meet", "lap"]),
    ("basketball", "basketball schedule and gym bookings", ["basketball", "gym", "court", "tournament"]),
    ("athletics_office", "athletics office announcements", ["athletic", "office", "booster"]),
    ("field_trip", "field trip itineraries, buses and permission", ["field trip", "bus", "itinerary", "chaperone"]),
    ("permission_slip", "permission slips and signature reminders", ["permission", "slip", "signature", "consent"]),
    ("lunch_menu", "weekly lunch menu and meal options", ["lunch", "menu", "meal", "cafeteria"]),
    ("cafeteria_rules", "cafeteria expectations and seating", ["cafeteria", "seating", "rules", "lunch"]),
    ("bake_sale", "bake sales and baked goods fundraisers", ["bake sale", "cookies", "sale", "fundraiser"]),
    ("fundraiser", "fundraisers, drives and donation goals", ["fundraiser", "donation", "goal", "sale"]),
    ("art_club", "art club projects, medium and shows", ["art", "paint", "project", "exhibit"]),
    ("crafts_club", "crafts club projects and supplies", ["crafts", "glue", "project", "supplies"]),
    ("drama_club", "drama club rehearsals and tickets", ["drama", "rehearsal", "play", "tickets"]),
    ("music_band", "concert band rehearsals and concerts", ["band", "concert", "performance", "instrument"]),
    ("choir", "choir practices and winter concert", ["choir", "concert", "performance", "sing"]),
    ("orchestra", "orchestra sectionals and repertoire", ["orchestra", "sectional", "violin", "concert"]),
    ("reading_week", "reading week challenges and logs", ["reading", "challenge", "log", "books"]),
    ("library", "library hours, returns and fines", ["library", "return", "books", "due"]),
    ("spelling_bee", "spelling bee rounds and registration", ["spelling", "bee", "round", "word"]),
    ("math_club", "math club contests and puzzles", ["math", "contest", "puzzle", "problem"]),
    ("math_olympiad", "math olympiad training and teams", ["math", "olympiad", "team", "contest"]),
    ("science_fair", "science fair projects and judging", ["science", "fair", "project", "judging"]),
    ("robotics", "robotics builds and competition", ["robotics", "robot", "build", "competition"]),
    ("coding_club", "coding club sessions and projects", ["coding", "app", "scratch", "project"]),
    ("history_day", "history day topics and papers", ["history", "paper", "topic", "exhibit"]),
    ("debate_club", "debate rounds and motions", ["debate", "round", "motion", "argument"]),
    ("chess_club", "chess club ladder and matches", ["chess", "match", "ladder", "tournament"]),
    ("homework", "homework policies and support", ["homework", "assignment", "due", "tutoring"]),
    ("grades", "grade reporting and conferences", ["grades", "report card", "conference", "gpa"]),
    ("attendance", "attendance windows and absences", ["attendance", "absence", "tardy", "excused"]),
    ("late_pickup", "late pickup procedures and fees", ["pickup", "late", "fee", "after school"]),
    ("early_dismissal", "early dismissal and snow schedules", ["dismissal", "early", "schedule", "snow"]),
    ("weather_closing", "weather closings and delays", ["closing", "delay", "weather", "snow"]),
    ("bus_route", "bus route changes and stops", ["bus", "route", "stop", "ride"]),
    ("carpool", "carpool coordination boards", ["carpool", "drivers", "roster", "pickup"]),
    ("flu_report", "illness, flu and nurse notes", ["flu", "nurse", "illness", "outbreak"]),
    ("allergies", "allergy alerts and food policies", ["allergy", "food", "nurse", "epipen"]),
    ("photo_day", "school photos and portrait packages", ["photo", "portrait", "package", "picture"]),
    ("dentist_note", "dental screening and vision checks", ["dental", "vision", "screening", "note"]),
    ("volunteer", "volunteer hours and signups", ["volunteer", "hours", "signup", "helper"]),
    ("pta_meeting", "PTA meetings and officers", ["pta", "meeting", "officer", "dues"]),
    ("textbook", "textbook and supply lists", ["textbook", "supply", "list", "checkout"]),
    ("locker", "locker assignments and rules", ["locker", "combination", "assignment", "hall"]),
    ("dress_code", "dress code and spirit days", ["dress code", "spirit", "attire", "jeans"]),
    ("concert_tickets", "event ticket sales and seating", ["tickets", "seating", "presale", "entry"]),
    ("yearbook", "yearbook ads, photos and delivery", ["yearbook", "ad", "delivery", "super senior"]),
    ("litter_pickup", "litter and grounds volunteer days", ["litter", "grounds", "cleanup", "volunteer"]),
    ("garden", "school garden planting and care", ["garden", "plant", "soil", "harvest"]),
    ("astronomy_night", "stargazing and planetarium trips", ["astronomy", "stars", "planetarium", "telescope"]),
    ("museums_late", "museum nights and docent tours", ["museum", "docent", "tour", "gallery"]),
    ("wellness", "wellness checks and health screenings", ["wellness", "health", "screening", "check"]),
    ("tech_help", "device help and wifi support", ["device", "wifi", "chromebook", "support"]),
    ("hall_pass", "hall pass and study hall policies", ["hall pass", "study hall", "pass", "policy"]),
    ("lunch_duty", "duty rosters and coverage", ["duty", "lunch duty", "coverage", "roster"]),
    ("staff_appreciation", "staff thank-you and Appreciation Week", ["staff", "appreciation", "thank you", "week"]),
]

HARD = {"soccer", "sports_general", "swim_club", "art_club", "crafts_club", "music_band",
        "choir", "flu_report", "allergies", "bake_sale", "fundraiser"}

FILLERS = [
    "Please reply-all if anything changed since last week.",
    "Mrs. Delgado left this note for the whole distribution list.",
    "Reminder forwarded from the office with no further edits.",
    "The link in the original message was left out of this copy.",
    "This item repeats in three separate newsletters this month.",
    "A follow-up message may or may not arrive later.",
    "Someone on the list has asked to be removed after the fact.",
    "The date appears twice and the two dates disagree.",
]
EVENTS = ["reminder", "update", "signup now", "schedule change", "final call", "quick note"]


def make_text(rng, topic):
    kws = TOPIC_KW[topic]
    bits = [rng.choice(kws), rng.choice(kws)]
    rng.shuffle(bits)
    text = f"{topic.replace('_', ' ').title()}: {rng.choice(EVENTS)} - {bits[0]} and {bits[1]} this week."
    if rng.random() < 0.35:
        text += f" Also mention {rng.choice(kws)} briefly near the end."
    if rng.random() < 0.30:
        text += " " + rng.choice(FILLERS)
    if rng.random() < 0.12:
        text += " " + " ".join([rng.choice(FILLERS), rng.choice(FILLERS)])
    return text


def main(out_dir):
    rng = random.Random(42)
    os.makedirs(out_dir, exist_ok=True)
    global TOPIC_KW
    TOPIC_KW = {t[0]: t[2] for t in TOPICS}
    names = [t[0] for t in TOPICS]
    assert len(names) == 56, len(names)

    taxonomy = {name: desc for name, desc, _ in TOPICS}
    with open(os.path.join(out_dir, "topic_taxonomy.json"), "w") as f:
        json.dump(taxonomy, f, indent=2)

    # skewed train counts: a few big topics, a long tail, 14 topics with no train items
    train_pool = [n for n in names if n not in rng.sample(names, 14)]
    counts = {}
    remaining = 279
    for i, n in enumerate(train_pool):
        w = rng.choice([1, 1, 2, 2, 3, 5]) if n in HARD else rng.choice([1, 1, 1, 2, 2, 4, 7])
        counts[n] = max(1, w)
    scale = remaining / sum(counts.values())
    counts = {n: max(1, int(c * scale + 0.5)) for n, c in counts.items()}
    # land exactly on 279, keeping every kept topic >= 1 item
    order = [n for n in train_pool if counts[n] > 1]
    i = 0
    while sum(counts.values()) != 279:
        n = order[i % len(order)]
        if sum(counts.values()) < 279:
            counts[n] += 1
        else:
            counts[n] -= 1
        i += 1

    idx = 0
    def emit(split, topic):
        nonlocal idx
        idx += 1
        return {"id": f"toy-{idx:04d}", "email_id": f"email-{idx:03d}",
                "text": make_text(rng, topic), "jev_topic": topic, "human_topic": None}

    train = [emit("train", n) for n, c in counts.items() for _ in range(c)]
    assert len(train) == 279, len(train)
    rng.shuffle(train)

    # val + test: cover all 56 topics; unseen topics appear only here
    val, test = [], []
    for n in names:
        for _ in range(1):
            val.append(emit("validation", n))
    while len(val) < 85:
        val.append(emit("validation", rng.choice(names)))
    for n in names:
        test.append(emit("test", n))
    while len(test) < 105:
        test.append(emit("test", rng.choice(names)))

    for split, rows in [("train", train), ("validation", val), ("test", test)]:
        with open(os.path.join(out_dir, f"{split}.jsonl"), "w") as f:
            for r in rows:
                f.write(json.dumps(r) + "\n")
        print(split, len(rows))
    big = sorted(counts.items(), key=lambda kv: -kv[1])[:5]
    print("busiest topics:", big, "| train topics:", len(counts), "/56")


if __name__ == "__main__":
    import argparse
    ap = argparse.ArgumentParser()
    ap.add_argument("--out", default="data")
    main(ap.parse_args().out)