ogx786 commited on
Commit
8ae38d8
·
verified ·
1 Parent(s): f8f4a2a

Create data_extraction.py

Browse files
Files changed (1) hide show
  1. data_extraction.py +262 -0
data_extraction.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NADRA Urdu -> Roman Urdu: Extract & Format for Fine-Tuning
3
+ ============================================================
4
+
5
+ Extracts real Urdu/Roman-Urdu parallel pairs from HBL's NADRA transliteration
6
+ API logs and converts them into an instruction-tuned JSONL dataset ready for
7
+ fine-tuning tiny-aya-global.
8
+
9
+ Source of truth per record:
10
+ - REQUEST_BODY -> original Urdu script (input)
11
+ - data -> the transliteration service's Roman Urdu output (target)
12
+
13
+ Fields used: name, fatherName, motherName, address1, address2, placeOfBirth
14
+
15
+ Usage:
16
+ python nadra_data_prep.py --input "/path/to/*.json" --output-dir /path/to/output
17
+
18
+ Run with -h for all options.
19
+ """
20
+
21
+ import argparse
22
+ import csv
23
+ import glob
24
+ import json
25
+ import os
26
+ import random
27
+ import re
28
+ import unicodedata
29
+ from collections import Counter
30
+
31
+ FIELDS = ["name", "fatherName", "motherName", "address1", "address2", "placeOfBirth"]
32
+
33
+ INSTRUCTIONS = {
34
+ "name": "Transliterate the following Urdu name into Roman Urdu.",
35
+ "fatherName": "Transliterate the following Urdu father's name into Roman Urdu.",
36
+ "motherName": "Transliterate the following Urdu mother's name into Roman Urdu.",
37
+ "address1": "Transliterate the following Urdu address into Roman Urdu.",
38
+ "address2": "Transliterate the following Urdu address into Roman Urdu.",
39
+ "placeOfBirth": "Transliterate the following Urdu place name into Roman Urdu.",
40
+ }
41
+
42
+
43
+ # --------------------------------------------------------------------------
44
+ # Loading
45
+ # --------------------------------------------------------------------------
46
+ def load_records(filepaths):
47
+ """
48
+ Handles three shapes:
49
+ 1) {"response": [ {...}, {...}, ... ]} (paginated API wrapper)
50
+ 2) [ {...}, {...}, ... ] (plain list of records)
51
+ 3) JSON Lines: one record dict per line
52
+ Returns a flat list of record dicts.
53
+ """
54
+ records = []
55
+ for fp in filepaths:
56
+ with open(fp, "r", encoding="utf-8") as fh:
57
+ raw = fh.read().strip()
58
+ try:
59
+ obj = json.loads(raw)
60
+ if isinstance(obj, dict) and "response" in obj:
61
+ records.extend(obj["response"])
62
+ elif isinstance(obj, list):
63
+ records.extend(obj)
64
+ elif isinstance(obj, dict):
65
+ records.append(obj)
66
+ except json.JSONDecodeError:
67
+ # fall back to JSON Lines
68
+ for line in raw.splitlines():
69
+ line = line.strip()
70
+ if not line:
71
+ continue
72
+ records.append(json.loads(line))
73
+ return records
74
+
75
+
76
+ # --------------------------------------------------------------------------
77
+ # Extraction
78
+ # --------------------------------------------------------------------------
79
+ def extract_pairs(records, fields):
80
+ """
81
+ For each record, pairs record['REQUEST_BODY'][field] (Urdu)
82
+ with record['data'][field] (Roman Urdu), for each field in `fields`.
83
+ Skips records missing either block entirely.
84
+ """
85
+ pairs = []
86
+ skipped_missing_block = 0
87
+
88
+ for rec in records:
89
+ data_block = rec.get("data")
90
+ req_block = rec.get("REQUEST_BODY")
91
+ if not isinstance(data_block, dict) or not isinstance(req_block, dict):
92
+ skipped_missing_block += 1
93
+ continue
94
+
95
+ cnic = data_block.get("cnic", "")
96
+ for field in fields:
97
+ roman = data_block.get(field)
98
+ urdu = req_block.get(field)
99
+ if roman is None or urdu is None:
100
+ continue
101
+ pairs.append({"field": field, "urdu": urdu, "roman": roman, "cnic": cnic})
102
+
103
+ print(f"Extracted {len(pairs)} raw pairs "
104
+ f"({skipped_missing_block} records skipped for missing data/REQUEST_BODY blocks)")
105
+ return pairs
106
+
107
+
108
+ # --------------------------------------------------------------------------
109
+ # Cleaning
110
+ # --------------------------------------------------------------------------
111
+ def is_masked_or_junk(text):
112
+ if text is None:
113
+ return True
114
+ t = text.strip()
115
+ if t == "":
116
+ return True
117
+ if "#" in t: # redacted/placeholder PII
118
+ return True
119
+ if re.fullmatch(r"[\W_]+", t): # only punctuation/symbols, no real content
120
+ return True
121
+ return False
122
+
123
+
124
+ def normalize_text(text):
125
+ t = unicodedata.normalize("NFC", text)
126
+ t = re.sub(r"\s+", " ", t).strip()
127
+ return t
128
+
129
+
130
+ def clean_pairs(pairs):
131
+ cleaned = []
132
+ seen = set()
133
+ dropped_masked = 0
134
+ dropped_dupe = 0
135
+
136
+ for p in pairs:
137
+ urdu, roman = p["urdu"], p["roman"]
138
+
139
+ if is_masked_or_junk(urdu) or is_masked_or_junk(roman):
140
+ dropped_masked += 1
141
+ continue
142
+
143
+ urdu_n = normalize_text(urdu)
144
+ roman_n = normalize_text(roman)
145
+
146
+ key = (p["field"], urdu_n.lower(), roman_n.lower())
147
+ if key in seen:
148
+ dropped_dupe += 1
149
+ continue
150
+ seen.add(key)
151
+
152
+ cleaned.append({"field": p["field"], "urdu": urdu_n, "roman": roman_n})
153
+
154
+ print(f"Kept {len(cleaned)} pairs "
155
+ f"(dropped {dropped_masked} masked/junk, {dropped_dupe} exact duplicates)")
156
+ return cleaned
157
+
158
+
159
+ # --------------------------------------------------------------------------
160
+ # Formatting
161
+ # --------------------------------------------------------------------------
162
+ def to_instruction_example(pair):
163
+ return {
164
+ "instruction": INSTRUCTIONS[pair["field"]],
165
+ "input": pair["urdu"],
166
+ "output": pair["roman"],
167
+ "field": pair["field"], # kept for filtering/analysis; drop before training if not needed
168
+ }
169
+
170
+
171
+ def train_val_split(examples, val_fraction, seed):
172
+ random.seed(seed)
173
+ by_field = {}
174
+ for ex in examples:
175
+ by_field.setdefault(ex["field"], []).append(ex)
176
+
177
+ train_set, val_set = [], []
178
+ for field, items in by_field.items():
179
+ items = items[:]
180
+ random.shuffle(items)
181
+ n_val = max(1, int(len(items) * val_fraction)) if len(items) > 20 else 0
182
+ val_set.extend(items[:n_val])
183
+ train_set.extend(items[n_val:])
184
+
185
+ random.shuffle(train_set)
186
+ random.shuffle(val_set)
187
+ return train_set, val_set
188
+
189
+
190
+ def write_jsonl(path, rows):
191
+ with open(path, "w", encoding="utf-8") as fh:
192
+ for row in rows:
193
+ fh.write(json.dumps(row, ensure_ascii=False) + "\n")
194
+
195
+
196
+ def write_csv(path, pairs):
197
+ with open(path, "w", newline="", encoding="utf-8") as fh:
198
+ writer = csv.DictWriter(fh, fieldnames=["field", "urdu", "roman"])
199
+ writer.writeheader()
200
+ writer.writerows(pairs)
201
+
202
+
203
+ # --------------------------------------------------------------------------
204
+ # Main
205
+ # --------------------------------------------------------------------------
206
+ def main():
207
+ parser = argparse.ArgumentParser(description="Extract & format NADRA Urdu/Roman Urdu pairs for fine-tuning.")
208
+ parser.add_argument("--input", required=True,
209
+ help='Path or glob pattern to input JSON file(s), e.g. "/data/nadra_page_*.json"')
210
+ parser.add_argument("--output-dir", default="./output", help="Directory to write train.jsonl/val.jsonl/csv to")
211
+ parser.add_argument("--val-fraction", type=float, default=0.05, help="Fraction held out for validation")
212
+ parser.add_argument("--seed", type=int, default=42, help="Random seed for shuffling/splitting")
213
+ parser.add_argument("--sample", type=int, default=3, help="Number of example pairs to print per field")
214
+ args = parser.parse_args()
215
+
216
+ os.makedirs(args.output_dir, exist_ok=True)
217
+ train_path = os.path.join(args.output_dir, "train.jsonl")
218
+ val_path = os.path.join(args.output_dir, "val.jsonl")
219
+ csv_path = os.path.join(args.output_dir, "pairs_clean.csv")
220
+
221
+ files = sorted(glob.glob(args.input))
222
+ print(f"Found {len(files)} file(s) matching input pattern")
223
+ for f in files:
224
+ print(" -", f)
225
+ if not files:
226
+ print("No input files found. Check --input path/glob.")
227
+ return
228
+
229
+ records = load_records(files)
230
+ print(f"Loaded {len(records)} raw records")
231
+
232
+ raw_pairs = extract_pairs(records, FIELDS)
233
+ print("Per-field raw counts:", Counter(p["field"] for p in raw_pairs))
234
+
235
+ clean = clean_pairs(raw_pairs)
236
+ print("Per-field clean counts:", Counter(p["field"] for p in clean))
237
+
238
+ write_csv(csv_path, clean)
239
+ print(f"Wrote clean pairs CSV -> {csv_path}")
240
+
241
+ # Sanity print
242
+ by_field = {}
243
+ for p in clean:
244
+ by_field.setdefault(p["field"], []).append(p)
245
+ random.seed(args.seed)
246
+ for field, items in by_field.items():
247
+ print(f"\n--- {field} ({len(items)} pairs) ---")
248
+ for ex in random.sample(items, min(args.sample, len(items))):
249
+ print(f" UR: {ex['urdu']}")
250
+ print(f" RO: {ex['roman']}")
251
+
252
+ examples = [to_instruction_example(p) for p in clean]
253
+ train_set, val_set = train_val_split(examples, args.val_fraction, args.seed)
254
+
255
+ write_jsonl(train_path, train_set)
256
+ write_jsonl(val_path, val_set)
257
+ print(f"\nWrote {len(train_set)} rows -> {train_path}")
258
+ print(f"Wrote {len(val_set)} rows -> {val_path}")
259
+
260
+
261
+ if __name__ == "__main__":
262
+ main()