rudeparis commited on
Commit
eaa16cb
Β·
verified Β·
1 Parent(s): ec3854f

Upload build_h1_corpus.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. build_h1_corpus.py +186 -0
build_h1_corpus.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build full H1 SFT corpus from all on-disk grounded sources.
2
+
3
+ Sources:
4
+ - eval_curated_v2.jsonl (4,766) β€” gold, span-dict slots
5
+ - eval_scenarios.jsonl (7,265) β€” gold, span-dict slots
6
+ - validation_hard_52.jsonl (52) β€” gold adversarial
7
+ - data/training/v40/train.jsonl (11,201) β€” Sunny silver, BIO labels
8
+ - data/training/v40/eval.jsonl (2,055) β€” Sunny silver, BIO labels
9
+
10
+ HOLD OUT (never touched during training):
11
+ - eval_set.jsonl (929)
12
+
13
+ Output: train.jsonl + valid.jsonl + test.jsonl in mlx-lm chat format with
14
+ 50-intent enum constraint in the system prompt.
15
+ """
16
+ from __future__ import annotations
17
+ import json
18
+ import random
19
+ from pathlib import Path
20
+
21
+ INTENTS = [
22
+ "acknowledgment", "altimeter_setting", "altitude_instruction", "approach_clearance",
23
+ "cleared_option", "cleared_touch_and_go", "comm_request", "correction",
24
+ "crossing_restriction", "ctaf_broadcast", "departure_instruction", "direct_to",
25
+ "disregard", "emergency_declaration", "extend_downwind", "frequency_change",
26
+ "go_around", "ground_hold", "heading_instruction", "hold_for_release",
27
+ "hold_instruction", "hold_short", "ident", "ifr_clearance", "informational",
28
+ "landing_clearance", "line_up_and_wait", "missed_approach", "other", "pattern_entry",
29
+ "position_report", "radar_identification", "radar_status", "release", "report_request",
30
+ "route_amendment", "route_clearance", "runway_crossing", "sequencing", "short_approach",
31
+ "spacing_instruction", "speed_assignment", "squawk_code_set", "takeoff_clearance",
32
+ "taxi_instruction", "traffic_advisory", "unable_response", "verification_request",
33
+ "vfr_instruction", "weather_advisory",
34
+ ]
35
+ assert len(INTENTS) == 50
36
+
37
+ SLOTS = [
38
+ "ALTIMETER_SETTING", "ALTITUDE", "APPROACH_TYPE", "CALL_SIGN", "CLOCK_POSITION",
39
+ "DIRECTION", "DISTANCE", "FACILITY", "FIX", "FREQUENCY", "HEADING", "PATTERN_LEG",
40
+ "ROUTE", "RUNWAY", "SPEED", "TAXIWAY", "TIME", "TRANSPONDER_CODE", "TURN_DIRECTION",
41
+ ]
42
+
43
+ SYSTEM_PROMPT = (
44
+ "You are an ATC parser. Parse the air traffic control transmission into a JSON object.\n"
45
+ "OUTPUT FORMAT: {\"intent\": <one-of-enum>, \"slots\": {<SLOT_TYPE>: <value>}}\n"
46
+ f"\nINTENT MUST BE ONE OF: {', '.join(INTENTS)}\n"
47
+ f"\nSLOT TYPES (UPPERCASE): {', '.join(SLOTS)}\n"
48
+ "\nOutput ONLY the JSON object. No prose, no code fences."
49
+ )
50
+
51
+ ROOT = Path("/Users/jean-patricksmith/digital/kingly/apps/production/naac")
52
+
53
+
54
+ def from_span_row(row: dict) -> dict | None:
55
+ """Parse {id,text,intent,slots:{TYPE:{value}}} β†’ flat chat row."""
56
+ text = row.get("text", "")
57
+ intent = row.get("intent", "")
58
+ if intent not in INTENTS:
59
+ return None
60
+ slots = {k: v.get("value", "") for k, v in (row.get("slots") or {}).items() if isinstance(v, dict)}
61
+ return {"text": text, "intent": intent, "slots": slots}
62
+
63
+
64
+ def from_bio_row(row: dict) -> dict | None:
65
+ """Parse {tokens[],labels[],intent,text} BIO β†’ flat chat row."""
66
+ text = row.get("text", "")
67
+ intent = row.get("intent", "")
68
+ if intent not in INTENTS:
69
+ return None
70
+ tokens = row.get("tokens") or []
71
+ labels = row.get("labels") or []
72
+ slots: dict[str, list[str]] = {}
73
+ cur_type = None
74
+ cur_buf: list[str] = []
75
+ for tok, lbl in zip(tokens, labels):
76
+ if lbl == "O" or lbl is None:
77
+ if cur_type:
78
+ slots.setdefault(cur_type, []).append(" ".join(cur_buf))
79
+ cur_type, cur_buf = None, []
80
+ continue
81
+ prefix, _, slot_type = lbl.partition("-")
82
+ if not slot_type:
83
+ continue
84
+ if prefix == "B" or slot_type != cur_type:
85
+ if cur_type:
86
+ slots.setdefault(cur_type, []).append(" ".join(cur_buf))
87
+ cur_type = slot_type
88
+ cur_buf = [tok]
89
+ else:
90
+ cur_buf.append(tok)
91
+ if cur_type:
92
+ slots.setdefault(cur_type, []).append(" ".join(cur_buf))
93
+ flat = {k: (v[0] if len(v) == 1 else v) for k, v in slots.items()}
94
+ return {"text": text, "intent": intent, "slots": flat}
95
+
96
+
97
+ def to_chat(row: dict) -> dict:
98
+ gold = json.dumps({"intent": row["intent"], "slots": row["slots"]}, ensure_ascii=False)
99
+ return {
100
+ "messages": [
101
+ {"role": "system", "content": SYSTEM_PROMPT},
102
+ {"role": "user", "content": row["text"]},
103
+ {"role": "assistant", "content": gold},
104
+ ]
105
+ }
106
+
107
+
108
+ def load_jsonl(path: Path, parser):
109
+ for line in path.read_text().splitlines():
110
+ line = line.strip()
111
+ if not line:
112
+ continue
113
+ try:
114
+ r = json.loads(line)
115
+ parsed = parser(r)
116
+ if parsed and parsed["text"]:
117
+ yield parsed
118
+ except json.JSONDecodeError:
119
+ continue
120
+
121
+
122
+ def main():
123
+ out_dir = ROOT / "poc/llm-finetune/training/data_h1"
124
+ out_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ sources = [
127
+ (ROOT / "poc/deberta_intent/data/eval/eval_curated_v2.jsonl", from_span_row, "gold"),
128
+ (ROOT / "poc/deberta_intent/data/eval_scenarios.jsonl", from_span_row, "gold"),
129
+ (ROOT / "poc/deberta_intent/data/eval/validation_hard_52.jsonl", from_span_row, "gold-adv"),
130
+ (ROOT / "data/training/v40/train.jsonl", from_bio_row, "silver-v40"),
131
+ (ROOT / "data/training/v40/eval.jsonl", from_bio_row, "silver-v40"),
132
+ ]
133
+
134
+ rows = []
135
+ counts = {}
136
+ for path, parser, tag in sources:
137
+ if not path.exists():
138
+ print(f"MISSING: {path}")
139
+ continue
140
+ loaded = list(load_jsonl(path, parser))
141
+ counts[str(path.name)] = len(loaded)
142
+ for r in loaded:
143
+ r["_source"] = tag
144
+ rows.extend(loaded)
145
+
146
+ print("source counts:", counts)
147
+ print(f"total: {len(rows)}")
148
+
149
+ # Dedupe by (text, intent) β€” keep first
150
+ seen = set()
151
+ deduped = []
152
+ for r in rows:
153
+ k = (r["text"], r["intent"])
154
+ if k in seen:
155
+ continue
156
+ seen.add(k)
157
+ deduped.append(r)
158
+ print(f"deduped: {len(deduped)}")
159
+
160
+ # HOLD OUT: eval_set.jsonl is NOT in any source above β€” confirmed safe
161
+ rng = random.Random(7)
162
+ rng.shuffle(deduped)
163
+
164
+ n = len(deduped)
165
+ n_valid = max(50, int(n * 0.02))
166
+ n_test = max(50, int(n * 0.02))
167
+ n_train = n - n_valid - n_test
168
+
169
+ splits = {
170
+ "train": deduped[:n_train],
171
+ "valid": deduped[n_train:n_train + n_valid],
172
+ "test": deduped[n_train + n_valid:],
173
+ }
174
+ for name, items in splits.items():
175
+ path = out_dir / f"{name}.jsonl"
176
+ with path.open("w") as f:
177
+ for r in items:
178
+ f.write(json.dumps(to_chat(r), ensure_ascii=False) + "\n")
179
+ print(f"{name}: {len(items)} -> {path}")
180
+
181
+ print(f"\nfinal corpus at {out_dir}")
182
+ print(f"system prompt size: {len(SYSTEM_PROMPT)} chars")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()