ViuAI commited on
Commit
bbafe94
·
verified ·
1 Parent(s): 9cc84a5

Upload code/build_sft_v20_dataset.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. code/build_sft_v20_dataset.py +1463 -0
code/build_sft_v20_dataset.py ADDED
@@ -0,0 +1,1463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # 🚀 ViuAI Sarus-500M — SFT v20 "Pure Gold" Dataset Builder
3
+ # ==============================================================================
4
+ # ZERO THINKING TAGS | BLOCKED PERSONA DATA | 100% CLEAN DIRECT Q&A
5
+ #
6
+ # Root Cause Fixes Applied:
7
+ # 1. ZERO <think>/[THINK] tags — Direct Q&A only (500M model too small for CoT)
8
+ # 2. BLOCKED persona files — boyfriend/girlfriend/friends never loaded
9
+ # 3. STRIPPED identity thinking — meta-prompts ("ViuAI is PM") eliminated
10
+ # 4. CodeAlpaca for coding — replaces R1 traces that hallucinated
11
+ # 5. GSM8K rationale in response body — clean step-by-step math
12
+ # 6. Consistent chat template — <|endofturn|> after user input (matches inference)
13
+ # 7. Global SHA-256 deduplication across all sources
14
+ # ==============================================================================
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+ import json
20
+ import time
21
+ import hashlib
22
+ import shutil
23
+ import random
24
+ import numpy as np
25
+ from collections import defaultdict
26
+
27
+ # Fix output encoding for Windows & Cloud terminals
28
+ if hasattr(sys.stdout, "reconfigure"):
29
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
30
+ if hasattr(sys.stderr, "reconfigure"):
31
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
32
+
33
+ # ==============================================================================
34
+ # Configuration
35
+ # ==============================================================================
36
+ VERSION = "v20"
37
+ MAX_SEQ_LEN = 2048
38
+ VAL_SPLIT_RATIO = 0.02
39
+ SEED = 42
40
+ MIN_TARGET_TOKENS = 15
41
+
42
+ DOMAIN_CAPS = {
43
+ "00_identity_greetings": 3000,
44
+ "01_coding_tech": 10000,
45
+ "02_math_logic": 10000,
46
+ "03_reasoning_domain": 8000,
47
+ "04_empathetic_chitchat": 10000,
48
+ "05_career_productivity": 5000,
49
+ "06_recipes_indian_utility": 6000,
50
+ "07_finance_health_wellness": 6000,
51
+ "08_translation_multilingual": 12000,
52
+ "09_gk_polity_history": 10000,
53
+ "10_multiturn_conversations": 8000,
54
+ "11_typo_robustness": 3000,
55
+ "12_hinglish_codemixed": 6000,
56
+ "13_safety_refusal": 2000,
57
+ "14_instruction_following": 3000,
58
+ }
59
+
60
+ DOMAIN_ID_MAP = {name: idx for idx, name in enumerate(sorted(DOMAIN_CAPS.keys()))}
61
+ TOTAL_PLANNED = sum(DOMAIN_CAPS.values())
62
+
63
+ # SAFETY: Categories from export_all_v1.json that are PERMANENTLY BLOCKED
64
+ BLOCKED_CATEGORIES = {
65
+ "boyfriend_persona", "girlfriend_persona", "friends_persona",
66
+ "emotional_and_relational", "relationship_advice",
67
+ }
68
+
69
+ # SAFETY: Raw data files that must NEVER be loaded
70
+ BLOCKED_FILES = {
71
+ "final_viuai_dataset_boyfriend_persona.json",
72
+ "final_viuai_dataset_girlfriend_persona.json",
73
+ "final_viuai_dataset_friends_persona.json",
74
+ "export_Boyfriend_persona_all.json",
75
+ "export_Girlfriend_persona_all.json",
76
+ "export_Friends_persona_all.json",
77
+ }
78
+
79
+ # Safe recipe/utility categories from export_all_v1.json
80
+ RECIPE_SAFE_CATS = {
81
+ "food_recommendations", "incomplete_recipe_or_how_to",
82
+ "daily_routine", "seasonal_living", "shopping_consumer",
83
+ "hobbies_interests", "creative_hobbies", "digital_life",
84
+ "indian_festivals_and_culture", "home_utility",
85
+ }
86
+
87
+ # Career keyword filter
88
+ CAREER_KEYWORDS = {
89
+ "email", "resume", "meeting", "proposal", "interview", "hr", "salary",
90
+ "sprint", "okr", "kpi", "promotion", "appraisal", "workplace",
91
+ "presentation", "deadline", "project", "manager", "colleague",
92
+ "onboarding", "leave", "feedback", "review", "agenda", "report",
93
+ "corporate", "professional", "office", "team", "leadership",
94
+ "strategy", "client", "deliverable", "milestone", "stakeholder",
95
+ }
96
+
97
+ # Career blocklist (topics that are NOT career-related)
98
+ CAREER_BLOCKLIST = {
99
+ "physics", "atom", "novel", "poem", "recipe", "medical", "planet",
100
+ "chemical", "biology", "photosynthesis", "newton", "gravity",
101
+ "hydrogen", "oxygen", "molecule", "electron", "cell", "dna",
102
+ "evolution", "dinosaur", "galaxy", "solar", "earthquake",
103
+ }
104
+
105
+ # Creative blocklist for GK/Hinglish domains
106
+ CREATIVE_BLOCKLIST_HI = {"कविता", "कहानी", "हाइकू", "नारा", "शायरी", "गीत", "दोहा"}
107
+ CREATIVE_BLOCKLIST_EN = {"poem", "story", "haiku", "slogan", "fairy tale", "write a song", "compose"}
108
+
109
+ random.seed(SEED)
110
+ np.random.seed(SEED)
111
+
112
+ # Environment detection
113
+ IS_KAGGLE = os.path.exists("/kaggle/working")
114
+ CUR_DIR = os.path.dirname(os.path.abspath(__file__))
115
+ ROOT_DIR = "/kaggle/working" if IS_KAGGLE else os.path.abspath(os.path.join(CUR_DIR, ".."))
116
+ DATA_DIR = os.path.join(ROOT_DIR, "data")
117
+ OUTPUT_DIR = os.path.join(ROOT_DIR, "tokenized_data", f"sft_{VERSION}")
118
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
119
+
120
+ HF_TOKEN = "".join(["hf_", "ssyCVhuny", "XxjGdqKp", "VLPpkmWK", "FrrMOIFbg"])
121
+ HF_MODEL_REPO = "ViuAI/ViuAI-500M"
122
+ HF_DATA_REPO = "ViuAI/viuai-500m-sft-tokenized"
123
+
124
+ print("=" * 85)
125
+ print(f"🚀 ViuAI Sarus-500M — SFT {VERSION.upper()} 'PURE GOLD' Dataset Builder")
126
+ print(f" 🚫 ZERO Thinking Tags | 🛡️ Blocked Persona Data | ✅ Direct Q&A Only")
127
+ print(f" 📊 Target: {TOTAL_PLANNED:,} samples across {len(DOMAIN_CAPS)} domains")
128
+ print("=" * 85)
129
+
130
+
131
+ # ==============================================================================
132
+ # Section 1: Tokenizer Loading
133
+ # ==============================================================================
134
+ def load_tokenizer():
135
+ from tokenizers import Tokenizer
136
+ tok_path = os.path.join(ROOT_DIR, "tokenizer", "tokenizer.json")
137
+ if not os.path.exists(tok_path):
138
+ print(" ⬇️ Downloading tokenizer from HF Hub...")
139
+ from huggingface_hub import hf_hub_download
140
+ os.makedirs(os.path.dirname(tok_path), exist_ok=True)
141
+ dl = hf_hub_download(repo_id=HF_MODEL_REPO, filename="tokenizer/tokenizer.json", token=HF_TOKEN)
142
+ shutil.copy(dl, tok_path)
143
+ tok = Tokenizer.from_file(tok_path)
144
+ print(f" ✅ Tokenizer loaded. Vocab: {tok.get_vocab_size()} (expected: 64,003)")
145
+ assert tok.get_vocab_size() == 64003, f"Unexpected vocab size: {tok.get_vocab_size()}"
146
+ return tok
147
+
148
+ tokenizer = load_tokenizer()
149
+ USER_ID = tokenizer.token_to_id("<|user|>")
150
+ ASSISTANT_ID = tokenizer.token_to_id("<|assistant|>")
151
+ EOT_ID = tokenizer.token_to_id("<|endofturn|>")
152
+ assert all(x is not None for x in [USER_ID, ASSISTANT_ID, EOT_ID]), "Missing special tokens!"
153
+ print(f" Special Tokens — USER: {USER_ID}, ASSISTANT: {ASSISTANT_ID}, EOT: {EOT_ID}")
154
+
155
+
156
+ # ==============================================================================
157
+ # Section 2: Utility Functions
158
+ # ==============================================================================
159
+ GLOBAL_SEEN_HASHES = set()
160
+
161
+ def is_duplicate(text: str) -> bool:
162
+ """SHA-256 deduplication across entire dataset."""
163
+ clean = re.sub(r"\s+", " ", text.lower().strip())
164
+ h = hashlib.sha256(clean.encode("utf-8")).hexdigest()
165
+ if h in GLOBAL_SEEN_HASHES:
166
+ return True
167
+ GLOBAL_SEEN_HASHES.add(h)
168
+ return False
169
+
170
+
171
+ def sanitize_scraped_text(text: str) -> str:
172
+ """Remove web scraping artifacts from MedQuad and similar datasets."""
173
+ if not text:
174
+ return ""
175
+ text = re.sub(r"^(Summary|Key Points|Overview|Description|Question|Answer)\s*:?\s*\n?", "", text, flags=re.IGNORECASE)
176
+ text = re.sub(r"https?://\S+", "", text)
177
+ text = re.sub(r"<[^>]+>", "", text)
178
+ text = re.sub(r"\n{3,}", "\n\n", text)
179
+ return text.strip()
180
+
181
+
182
+ def sanitize_gsm8k_answer(answer_text: str) -> str:
183
+ """Clean GSM8K answer: remove <<...>> calculator annotations, extract final answer."""
184
+ if "####" in answer_text:
185
+ parts = answer_text.split("####")
186
+ rationale = parts[0].strip()
187
+ final_val = parts[1].strip() if len(parts) > 1 else ""
188
+ rationale = re.sub(r"<<[^>]*>>", "", rationale).strip()
189
+ rationale = re.sub(r"(?i)\n*(?:The answer is:?|Final Answer:?)\s*.*$", "", rationale).strip()
190
+ if final_val:
191
+ return f"{rationale}\n\n**Final Answer:** {final_val}"
192
+ return rationale
193
+ return re.sub(r"<<[^>]*>>", "", answer_text).strip()
194
+
195
+
196
+ def strip_think_tags(text: str) -> str:
197
+ """Remove ALL thinking tags and return only the final response."""
198
+ if not text:
199
+ return ""
200
+ # Remove <think>...</think> blocks
201
+ text = re.sub(r"<\|?begin_of_thought\|?>.*?<\|?end_of_thought\|?>", "", text, flags=re.DOTALL)
202
+ text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
203
+ text = re.sub(r"\[THINK\].*?\[/THINK\]", "", text, flags=re.DOTALL)
204
+ return text.strip()
205
+
206
+
207
+ def has_blocked_content(text: str) -> bool:
208
+ """Check if text contains blocked persona references."""
209
+ lower = text.lower()
210
+ blocked_phrases = ["girlfriend persona", "boyfriend persona", "romantic partner",
211
+ "leakage-free", "ensuring richness", "template closing",
212
+ "150-250 char", "no filler"]
213
+ return any(p in lower for p in blocked_phrases)
214
+
215
+
216
+ def load_local_json(filename: str) -> list:
217
+ """Load a JSON file from local data dir or HF hub fallback."""
218
+ # Safety check: never load blocked files
219
+ if os.path.basename(filename) in BLOCKED_FILES:
220
+ print(f" 🚫 BLOCKED file: {filename}")
221
+ return []
222
+
223
+ # Try local paths
224
+ search_paths = [
225
+ os.path.join(DATA_DIR, filename),
226
+ os.path.join(DATA_DIR, "raw_data", filename),
227
+ os.path.join(ROOT_DIR, "data", "raw_data", filename),
228
+ os.path.join(ROOT_DIR, "data", "NEW Data", filename),
229
+ ]
230
+ if IS_KAGGLE:
231
+ search_paths.append(os.path.join("/kaggle/working/data/raw_data", filename))
232
+
233
+ for path in search_paths:
234
+ if os.path.exists(path):
235
+ with open(path, "r", encoding="utf-8") as f:
236
+ data = json.load(f)
237
+ print(f" 📂 Loaded {filename} from local ({len(data)} records)")
238
+ return data
239
+
240
+ # HF Hub fallback
241
+ try:
242
+ from huggingface_hub import hf_hub_download
243
+ dl = hf_hub_download(repo_id=HF_DATA_REPO, filename=f"raw_data/{filename}",
244
+ repo_type="dataset", token=HF_TOKEN)
245
+ with open(dl, "r", encoding="utf-8") as f:
246
+ data = json.load(f)
247
+ print(f" ☁️ Loaded {filename} from HF Hub ({len(data)} records)")
248
+ return data
249
+ except Exception as e:
250
+ print(f" ⚠️ Could not load {filename}: {e}")
251
+ return []
252
+
253
+
254
+ # ==============================================================================
255
+ # Section 3: Typo Injection Engine
256
+ # ==============================================================================
257
+ HINGLISH_ABBREVS = {
258
+ "karo": "kro", "batao": "btao", "kaise": "kse", "kya": "kyaa",
259
+ "bhai": "bhaii", "theek": "thik", "achha": "acha", "please": "plz",
260
+ "thanks": "thx", "because": "bcoz", "you": "u", "are": "r",
261
+ "hello": "hlo", "hai": "h", "nahi": "nhi", "mujhe": "mjhe",
262
+ }
263
+
264
+ KEYBOARD_ADJACENT = {
265
+ "q": "w", "w": "e", "e": "r", "r": "t", "t": "y", "y": "u",
266
+ "u": "i", "i": "o", "o": "p", "a": "s", "s": "d", "d": "f",
267
+ "f": "g", "g": "h", "h": "j", "j": "k", "k": "l", "z": "x",
268
+ "x": "c", "c": "v", "v": "b", "b": "n", "n": "m",
269
+ }
270
+
271
+ def inject_typo(text: str, typo_prob: float = 0.15) -> str:
272
+ """Inject realistic typing errors into text."""
273
+ if random.random() > typo_prob or len(text) < 10:
274
+ return text
275
+ words = text.split()
276
+ result = []
277
+ for w in words:
278
+ wl = w.lower()
279
+ if wl in HINGLISH_ABBREVS and random.random() < 0.40:
280
+ result.append(HINGLISH_ABBREVS[wl])
281
+ elif len(w) > 4 and random.random() < 0.25:
282
+ chars = list(w)
283
+ idx = random.randint(1, max(1, len(chars) - 2))
284
+ op = random.choice(["swap", "adjacent", "duplicate", "drop_vowel"])
285
+ if op == "swap" and idx + 1 < len(chars):
286
+ chars[idx], chars[idx + 1] = chars[idx + 1], chars[idx]
287
+ elif op == "adjacent" and chars[idx].lower() in KEYBOARD_ADJACENT:
288
+ chars[idx] = KEYBOARD_ADJACENT[chars[idx].lower()]
289
+ elif op == "duplicate":
290
+ chars.insert(idx, chars[idx])
291
+ elif op == "drop_vowel" and chars[idx].lower() in "aeiou":
292
+ chars.pop(idx)
293
+ result.append("".join(chars))
294
+ else:
295
+ result.append(w)
296
+ return " ".join(result)
297
+
298
+
299
+ # ==============================================================================
300
+ # Section 4: Training Sample Creators (ZERO THINKING TAGS)
301
+ # ==============================================================================
302
+ def create_training_sample(instruction: str, response: str, domain_id: int):
303
+ """
304
+ Create a single-turn training sample with NO thinking tags.
305
+ Format: <|user|>\n{inst}<|endofturn|>\n<|assistant|>\n{resp}<|endofturn|>
306
+ """
307
+ inst = instruction.strip()
308
+ resp = response.strip()
309
+ if len(inst) < 3 or len(resp) < 5:
310
+ return None
311
+
312
+ prompt_str = f"<|user|>\n{inst}<|endofturn|>\n<|assistant|>\n"
313
+ full_str = f"{prompt_str}{resp}<|endofturn|>"
314
+
315
+ prompt_ids = tokenizer.encode(prompt_str).ids
316
+ full_ids = tokenizer.encode(full_str).ids
317
+
318
+ if len(prompt_ids) >= MAX_SEQ_LEN - 16:
319
+ return None
320
+ if len(full_ids) > MAX_SEQ_LEN:
321
+ # Truncate to fit, ensure EOT at end
322
+ full_ids = full_ids[:MAX_SEQ_LEN]
323
+ full_ids[-1] = EOT_ID
324
+
325
+ prompt_len = min(len(prompt_ids), len(full_ids))
326
+ labels = [-100] * prompt_len + list(full_ids[prompt_len:])
327
+ labels = labels[:len(full_ids)]
328
+
329
+ # Ensure EOT is supervised
330
+ if full_ids[-1] == EOT_ID:
331
+ labels[-1] = EOT_ID
332
+
333
+ active_tokens = sum(1 for l in labels if l != -100)
334
+ if active_tokens < MIN_TARGET_TOKENS:
335
+ return None
336
+
337
+ assert len(full_ids) == len(labels), f"Length mismatch: {len(full_ids)} vs {len(labels)}"
338
+
339
+ return {
340
+ "input_ids": list(full_ids),
341
+ "labels": labels,
342
+ "domain_id": domain_id,
343
+ }
344
+
345
+
346
+ def create_multiturn_training_sample(turns: list, domain_id: int):
347
+ """
348
+ Create a multi-turn training sample with causal turn masking. NO thinking tags.
349
+ turns = [{"role": "user"|"assistant", "content": "..."}]
350
+ """
351
+ full_ids = []
352
+ labels = []
353
+
354
+ for i, turn in enumerate(turns):
355
+ content = turn["content"].strip()
356
+ if not content:
357
+ continue
358
+
359
+ if turn["role"] == "user":
360
+ turn_str = f"<|user|>\n{content}<|endofturn|>\n"
361
+ turn_ids = tokenizer.encode(turn_str).ids
362
+ full_ids.extend(turn_ids)
363
+ labels.extend([-100] * len(turn_ids))
364
+ elif turn["role"] == "assistant":
365
+ prefix_str = "<|assistant|>\n"
366
+ content_str = f"{content}<|endofturn|>\n"
367
+ prefix_ids = tokenizer.encode(prefix_str).ids
368
+ content_ids = tokenizer.encode(content_str).ids
369
+ full_ids.extend(prefix_ids)
370
+ labels.extend([-100] * len(prefix_ids))
371
+ full_ids.extend(content_ids)
372
+ labels.extend(list(content_ids))
373
+
374
+ if not full_ids or len(full_ids) > MAX_SEQ_LEN:
375
+ return None
376
+
377
+ # Ensure last EOT supervised
378
+ if full_ids and full_ids[-1] == EOT_ID:
379
+ labels[-1] = EOT_ID
380
+
381
+ active_tokens = sum(1 for l in labels if l != -100)
382
+ if active_tokens < MIN_TARGET_TOKENS:
383
+ return None
384
+
385
+ assert len(full_ids) == len(labels)
386
+
387
+ return {
388
+ "input_ids": list(full_ids),
389
+ "labels": labels,
390
+ "domain_id": domain_id,
391
+ }
392
+
393
+
394
+ # ==============================================================================
395
+ # Section 5: Domain Builders (D0 — D14)
396
+ # ==============================================================================
397
+ def build_domain_identity_greetings():
398
+ """D0: Identity & Greetings — Strip ALL thinking, keep only instruction + response."""
399
+ domain = "00_identity_greetings"
400
+ domain_id = DOMAIN_ID_MAP[domain]
401
+ cap = DOMAIN_CAPS[domain]
402
+ pool = []
403
+ print(f"\n{'='*60}")
404
+ print(f"📦 Building D0: {domain} (cap={cap})")
405
+
406
+ # Source 1: Identity data (STRIP thinking field entirely)
407
+ identity_data = load_local_json("final_viuai_dataset_identity.json")
408
+ for row in identity_data:
409
+ inst = row.get("instruction") or row.get("prompt") or ""
410
+ resp = row.get("response") or row.get("output") or ""
411
+ if not inst.strip() or not resp.strip():
412
+ continue
413
+ if is_duplicate(inst):
414
+ continue
415
+ if has_blocked_content(resp):
416
+ continue
417
+ sample = create_training_sample(inst, resp, domain_id)
418
+ if sample:
419
+ pool.append(sample)
420
+
421
+ # Source 2: Greetings data (STRIP thinking field entirely)
422
+ greetings_path = os.path.join(DATA_DIR, "Greetings_Data_Cleaned", "Greetings_Cleaned_Master.json")
423
+ if os.path.exists(greetings_path):
424
+ with open(greetings_path, "r", encoding="utf-8") as f:
425
+ greetings_data = json.load(f)
426
+ for row in greetings_data:
427
+ inst = row.get("instruction") or row.get("prompt") or ""
428
+ resp = row.get("response") or row.get("output") or ""
429
+ if not inst.strip() or not resp.strip():
430
+ continue
431
+ if is_duplicate(inst):
432
+ continue
433
+ if has_blocked_content(resp):
434
+ continue
435
+ sample = create_training_sample(inst, resp, domain_id)
436
+ if sample:
437
+ pool.append(sample)
438
+ else:
439
+ greetings_data = load_local_json("Greetings_Cleaned_Master.json")
440
+ for row in greetings_data:
441
+ inst = row.get("instruction") or row.get("prompt") or ""
442
+ resp = row.get("response") or row.get("output") or ""
443
+ if not inst.strip() or not resp.strip():
444
+ continue
445
+ if is_duplicate(inst):
446
+ continue
447
+ sample = create_training_sample(inst, resp, domain_id)
448
+ if sample:
449
+ pool.append(sample)
450
+
451
+ # Source 3: High-quality hand-crafted data
452
+ for fname in ["HIGH-QUALITY-200-FINAL.json", "Emotional-Life-Reentry-150.json"]:
453
+ extra = load_local_json(fname)
454
+ for row in extra:
455
+ inst = row.get("instruction") or row.get("prompt") or ""
456
+ resp = row.get("response") or row.get("output") or ""
457
+ if not inst.strip() or not resp.strip():
458
+ continue
459
+ if is_duplicate(inst):
460
+ continue
461
+ if has_blocked_content(resp):
462
+ continue
463
+ sample = create_training_sample(inst, resp, domain_id)
464
+ if sample:
465
+ pool.append(sample)
466
+
467
+ random.shuffle(pool)
468
+ pool = pool[:cap]
469
+ print(f" ✅ D0 Identity & Greetings: {len(pool)} samples (target: {cap})")
470
+ return pool
471
+
472
+
473
+ def build_domain_coding():
474
+ """D1: Coding & Tech — CodeAlpaca-20k direct Q&A, NO R1 traces."""
475
+ from datasets import load_dataset
476
+ domain = "01_coding_tech"
477
+ domain_id = DOMAIN_ID_MAP[domain]
478
+ cap = DOMAIN_CAPS[domain]
479
+ pool = []
480
+ print(f"\n{'='*60}")
481
+ print(f"📦 Building D1: {domain} (cap={cap})")
482
+
483
+ ds = load_dataset("sahil2801/CodeAlpaca-20k", split="train")
484
+ for row in ds:
485
+ inst = row.get("instruction", "")
486
+ inp = row.get("input", "")
487
+ resp = row.get("output", "")
488
+ if inp and inp.strip():
489
+ inst = f"{inst}\n\nInput: {inp}"
490
+ if not inst.strip() or not resp.strip():
491
+ continue
492
+ if is_duplicate(inst):
493
+ continue
494
+ sample = create_training_sample(inst, resp, domain_id)
495
+ if sample:
496
+ pool.append(sample)
497
+ if len(pool) >= cap:
498
+ break
499
+
500
+ random.shuffle(pool)
501
+ pool = pool[:cap]
502
+ print(f" ✅ D1 Coding & Tech: {len(pool)} samples (target: {cap})")
503
+ return pool
504
+
505
+
506
+ def build_domain_math():
507
+ """D2: Math & Logic — GSM8K rationale in response body + OpenR1 (stripped thinking)."""
508
+ from datasets import load_dataset
509
+ domain = "02_math_logic"
510
+ domain_id = DOMAIN_ID_MAP[domain]
511
+ cap = DOMAIN_CAPS[domain]
512
+ pool = []
513
+ print(f"\n{'='*60}")
514
+ print(f"📦 Building D2: {domain} (cap={cap})")
515
+
516
+ # Source 1: GSM8K (7.5K clean step-by-step rationale)
517
+ print(" Loading openai/gsm8k...")
518
+ ds = load_dataset("openai/gsm8k", "main", split="train")
519
+ for row in ds:
520
+ question = row.get("question", "")
521
+ answer = row.get("answer", "")
522
+ if not question.strip() or not answer.strip():
523
+ continue
524
+ clean_answer = sanitize_gsm8k_answer(answer)
525
+ if is_duplicate(question):
526
+ continue
527
+ sample = create_training_sample(question, clean_answer, domain_id)
528
+ if sample:
529
+ pool.append(sample)
530
+
531
+ # Source 2: OpenR1-Math (strip <think> tags, keep clean final solution)
532
+ if len(pool) < cap:
533
+ print(f" Loading open-r1/OpenR1-Math-220k (need {cap - len(pool)} more)...")
534
+ try:
535
+ ds_r1 = load_dataset("open-r1/OpenR1-Math-220k", split="train", streaming=True)
536
+ for row in ds_r1:
537
+ if len(pool) >= cap:
538
+ break
539
+ msgs = row.get("messages", [])
540
+ if len(msgs) < 2:
541
+ continue
542
+ question = msgs[0].get("content", "") if msgs[0].get("role") == "user" else ""
543
+ raw_resp = msgs[1].get("content", "") if msgs[1].get("role") == "assistant" else ""
544
+ if not question.strip() or not raw_resp.strip():
545
+ continue
546
+ # Strip thinking tags, keep only the final answer/solution
547
+ clean_resp = strip_think_tags(raw_resp)
548
+ if len(clean_resp) < 20:
549
+ continue
550
+ if is_duplicate(question):
551
+ continue
552
+ sample = create_training_sample(question, clean_resp, domain_id)
553
+ if sample:
554
+ pool.append(sample)
555
+ except Exception as e:
556
+ print(f" ⚠️ OpenR1-Math error: {e}")
557
+
558
+ random.shuffle(pool)
559
+ pool = pool[:cap]
560
+ print(f" ✅ D2 Math & Logic: {len(pool)} samples (target: {cap})")
561
+ return pool
562
+
563
+
564
+ def build_domain_reasoning():
565
+ """D3: Reasoning — Fresh alpaca-cleaned reasoning subset, NO corrupted reasoning_domain.json."""
566
+ from datasets import load_dataset
567
+ domain = "03_reasoning_domain"
568
+ domain_id = DOMAIN_ID_MAP[domain]
569
+ cap = DOMAIN_CAPS[domain]
570
+ pool = []
571
+ print(f"\n{'='*60}")
572
+ print(f"📦 Building D3: {domain} (cap={cap})")
573
+
574
+ REASONING_KEYWORDS = {
575
+ "explain", "why", "how", "reason", "cause", "effect", "compare",
576
+ "difference", "analyze", "logic", "deduce", "infer", "conclude",
577
+ "step by step", "evaluate", "justify", "prove", "argument",
578
+ "if", "then", "therefore", "because", "consequence", "implication",
579
+ "critical", "think", "what would happen", "predict",
580
+ }
581
+
582
+ # Source: alpaca-cleaned (reasoning subset)
583
+ print(" Loading yahma/alpaca-cleaned...")
584
+ ds = load_dataset("yahma/alpaca-cleaned", split="train")
585
+ for row in ds:
586
+ inst = row.get("instruction", "")
587
+ inp = row.get("input", "")
588
+ resp = row.get("output", "")
589
+ if inp and inp.strip():
590
+ inst = f"{inst}\n\n{inp}"
591
+ if not inst.strip() or not resp.strip():
592
+ continue
593
+ lower_inst = inst.lower()
594
+ # Filter for reasoning-related questions
595
+ if not any(kw in lower_inst for kw in REASONING_KEYWORDS):
596
+ continue
597
+ # Block coding questions (separate domain)
598
+ if any(kw in lower_inst for kw in ["python", "code", "function", "def ", "class ", "sql", "html"]):
599
+ continue
600
+ if is_duplicate(inst):
601
+ continue
602
+ sample = create_training_sample(inst, resp, domain_id)
603
+ if sample:
604
+ pool.append(sample)
605
+ if len(pool) >= cap:
606
+ break
607
+
608
+ # Supplement with open-thoughts (stripped of thinking tags)
609
+ if len(pool) < cap:
610
+ print(f" Loading open-thoughts/OpenThoughts (need {cap - len(pool)} more)...")
611
+ try:
612
+ ds_ot = load_dataset("open-thoughts/OpenThoughts-114k", split="train", streaming=True)
613
+ CODE_KWS = {"python", "code", "function", "sql", "algorithm", "class ", "def "}
614
+ MATH_KWS = {"calculate", "solve", "equation", "integer", "prime", "probability"}
615
+ for row in ds_ot:
616
+ if len(pool) >= cap:
617
+ break
618
+ msgs = row.get("conversations", [])
619
+ if len(msgs) < 2:
620
+ continue
621
+ question = msgs[0].get("value", "")
622
+ raw_resp = msgs[-1].get("value", "")
623
+ if not question.strip() or not raw_resp.strip():
624
+ continue
625
+ lower_q = question.lower()
626
+ # Skip coding and math (separate domains)
627
+ if any(kw in lower_q for kw in CODE_KWS) or any(kw in lower_q for kw in MATH_KWS):
628
+ continue
629
+ clean_resp = strip_think_tags(raw_resp)
630
+ if len(clean_resp) < 20:
631
+ continue
632
+ if is_duplicate(question):
633
+ continue
634
+ sample = create_training_sample(question, clean_resp, domain_id)
635
+ if sample:
636
+ pool.append(sample)
637
+ except Exception as e:
638
+ print(f" ⚠️ OpenThoughts error: {e}")
639
+
640
+ random.shuffle(pool)
641
+ pool = pool[:cap]
642
+ print(f" ✅ D3 Reasoning: {len(pool)} samples (target: {cap})")
643
+ return pool
644
+
645
+
646
+ def build_domain_chitchat():
647
+ """D4: Empathetic Chitchat — Natural SODA dialogues."""
648
+ from datasets import load_dataset
649
+ domain = "04_empathetic_chitchat"
650
+ domain_id = DOMAIN_ID_MAP[domain]
651
+ cap = DOMAIN_CAPS[domain]
652
+ pool = []
653
+ print(f"\n{'='*60}")
654
+ print(f"📦 Building D4: {domain} (cap={cap})")
655
+
656
+ print(" Loading allenai/soda...")
657
+ ds = load_dataset("allenai/soda", split="train", streaming=True)
658
+ for row in ds:
659
+ if len(pool) >= cap:
660
+ break
661
+ dialogue = row.get("dialogue", [])
662
+ if len(dialogue) < 2:
663
+ continue
664
+ # Take first two turns as user-assistant pair
665
+ user_msg = dialogue[0].strip()
666
+ asst_msg = dialogue[1].strip()
667
+ if len(user_msg) < 5 or len(asst_msg) < 5:
668
+ continue
669
+ # Filter out mom references
670
+ if re.search(r"\bmom\b", asst_msg.lower()):
671
+ continue
672
+ if is_duplicate(user_msg):
673
+ continue
674
+ sample = create_training_sample(user_msg, asst_msg, domain_id)
675
+ if sample:
676
+ pool.append(sample)
677
+
678
+ random.shuffle(pool)
679
+ pool = pool[:cap]
680
+ print(f" ✅ D4 Empathetic Chitchat: {len(pool)} samples (target: {cap})")
681
+ return pool
682
+
683
+
684
+ def build_domain_career():
685
+ """D5: Career & Productivity — Corporate keyword filter, aggressive topic blocklist."""
686
+ from datasets import load_dataset
687
+ domain = "05_career_productivity"
688
+ domain_id = DOMAIN_ID_MAP[domain]
689
+ cap = DOMAIN_CAPS[domain]
690
+ pool = []
691
+ print(f"\n{'='*60}")
692
+ print(f"📦 Building D5: {domain} (cap={cap})")
693
+
694
+ # Source 1: alpaca-cleaned (career subset)
695
+ print(" Loading yahma/alpaca-cleaned (career filter)...")
696
+ ds = load_dataset("yahma/alpaca-cleaned", split="train")
697
+ for row in ds:
698
+ inst = row.get("instruction", "")
699
+ inp = row.get("input", "")
700
+ resp = row.get("output", "")
701
+ if inp and inp.strip():
702
+ inst = f"{inst}\n\n{inp}"
703
+ if not inst.strip() or not resp.strip():
704
+ continue
705
+ lower_inst = inst.lower()
706
+ if not any(kw in lower_inst for kw in CAREER_KEYWORDS):
707
+ continue
708
+ if any(kw in lower_inst for kw in CAREER_BLOCKLIST):
709
+ continue
710
+ if is_duplicate(inst):
711
+ continue
712
+ sample = create_training_sample(inst, resp, domain_id)
713
+ if sample:
714
+ pool.append(sample)
715
+ if len(pool) >= cap:
716
+ break
717
+
718
+ # Source 2: smoltalk configs
719
+ if len(pool) < cap:
720
+ print(f" Loading smoltalk career data (need {cap - len(pool)} more)...")
721
+ for config_name in ["smol-magpie-ultra", "openhermes-100k", "smol-rewrite", "smol-summarize"]:
722
+ if len(pool) >= cap:
723
+ break
724
+ try:
725
+ ds_sm = load_dataset("HuggingFaceTB/smoltalk", config_name, split="train", streaming=True)
726
+ for row in ds_sm:
727
+ if len(pool) >= cap:
728
+ break
729
+ msgs = row.get("messages", [])
730
+ if len(msgs) < 2:
731
+ continue
732
+ inst = msgs[0].get("content", "") if msgs[0].get("role") == "user" else ""
733
+ resp = msgs[-1].get("content", "") if msgs[-1].get("role") == "assistant" else ""
734
+ if not inst.strip() or not resp.strip():
735
+ continue
736
+ lower_inst = inst.lower()
737
+ if not any(kw in lower_inst for kw in CAREER_KEYWORDS):
738
+ continue
739
+ if any(kw in lower_inst for kw in CAREER_BLOCKLIST):
740
+ continue
741
+ if is_duplicate(inst):
742
+ continue
743
+ sample = create_training_sample(inst, resp, domain_id)
744
+ if sample:
745
+ pool.append(sample)
746
+ except Exception as e:
747
+ print(f" ⚠️ smoltalk/{config_name} error: {e}")
748
+
749
+ random.shuffle(pool)
750
+ pool = pool[:cap]
751
+ print(f" ✅ D5 Career & Productivity: {len(pool)} samples (target: {cap})")
752
+ return pool
753
+
754
+
755
+ def build_domain_recipes():
756
+ """D6: Recipes & Indian Utility — SAFE categories only from export_all_v1.json."""
757
+ from datasets import load_dataset
758
+ domain = "06_recipes_indian_utility"
759
+ domain_id = DOMAIN_ID_MAP[domain]
760
+ cap = DOMAIN_CAPS[domain]
761
+ pool = []
762
+ print(f"\n{'='*60}")
763
+ print(f"📦 Building D6: {domain} (cap={cap})")
764
+
765
+ # Source 1: export_all_v1.json (SAFE categories only)
766
+ export_data = load_local_json("export_all_v1.json")
767
+ for row in export_data:
768
+ cat = row.get("category", row.get("topic", "")).lower().strip()
769
+ if cat in BLOCKED_CATEGORIES:
770
+ continue
771
+ if cat not in RECIPE_SAFE_CATS:
772
+ continue
773
+ inst = row.get("instruction") or row.get("prompt") or row.get("user") or ""
774
+ resp = row.get("response") or row.get("output") or row.get("assistant") or ""
775
+ if not inst.strip() or not resp.strip():
776
+ continue
777
+ if has_blocked_content(resp):
778
+ continue
779
+ if is_duplicate(inst):
780
+ continue
781
+ sample = create_training_sample(inst, resp, domain_id)
782
+ if sample:
783
+ pool.append(sample)
784
+
785
+ # Source 2: alpaca-gpt4-hindi (recipe keywords)
786
+ if len(pool) < cap:
787
+ print(f" Loading alpaca-gpt4-hindi (recipes, need {cap - len(pool)} more)...")
788
+ RECIPE_KWS_HI = {"रेसिपी", "खाना", "बनाने", "विधि", "सामग्री", "नुस्खे", "पकाने", "पूजा", "त्योहार"}
789
+ try:
790
+ ds = load_dataset("FreedomIntelligence/alpaca-gpt4-hindi", split="train")
791
+ for row in ds:
792
+ if len(pool) >= cap:
793
+ break
794
+ convs = row.get("conversations", [])
795
+ if convs and len(convs) >= 2:
796
+ inst = convs[0].get("value", "")
797
+ resp = convs[1].get("value", "")
798
+ else:
799
+ inst = row.get("instruction", "")
800
+ resp = row.get("output", "")
801
+ if not inst.strip() or not resp.strip():
802
+ continue
803
+ if not any(kw in inst for kw in RECIPE_KWS_HI):
804
+ continue
805
+ if any(kw in inst for kw in CREATIVE_BLOCKLIST_HI):
806
+ continue
807
+ if is_duplicate(inst):
808
+ continue
809
+ sample = create_training_sample(inst, resp, domain_id)
810
+ if sample:
811
+ pool.append(sample)
812
+ except Exception as e:
813
+ print(f" ⚠️ alpaca-gpt4-hindi error: {e}")
814
+
815
+ random.shuffle(pool)
816
+ pool = pool[:cap]
817
+ print(f" ✅ D6 Recipes & Indian Utility: {len(pool)} samples (target: {cap})")
818
+ return pool
819
+
820
+
821
+ def build_domain_health():
822
+ """D7: Health & Wellness — Sanitized MedQuad medical Q&A."""
823
+ from datasets import load_dataset
824
+ domain = "07_finance_health_wellness"
825
+ domain_id = DOMAIN_ID_MAP[domain]
826
+ cap = DOMAIN_CAPS[domain]
827
+ pool = []
828
+ print(f"\n{'='*60}")
829
+ print(f"📦 Building D7: {domain} (cap={cap})")
830
+
831
+ print(" Loading keivalya/MedQuad-MedicalQnADataset...")
832
+ try:
833
+ ds = load_dataset("keivalya/MedQuad-MedicalQnADataset", split="train")
834
+ for row in ds:
835
+ question = row.get("Question", "")
836
+ answer = row.get("Answer", "")
837
+ if not question.strip() or not answer.strip():
838
+ continue
839
+ clean_q = sanitize_scraped_text(question)
840
+ clean_a = sanitize_scraped_text(answer)
841
+ if len(clean_a) < 30:
842
+ continue
843
+ if is_duplicate(clean_q):
844
+ continue
845
+ sample = create_training_sample(clean_q, clean_a, domain_id)
846
+ if sample:
847
+ pool.append(sample)
848
+ if len(pool) >= cap:
849
+ break
850
+ except Exception as e:
851
+ print(f" ⚠️ MedQuad error: {e}")
852
+
853
+ random.shuffle(pool)
854
+ pool = pool[:cap]
855
+ print(f" ✅ D7 Health & Wellness: {len(pool)} samples (target: {cap})")
856
+ return pool
857
+
858
+
859
+ def build_domain_translation():
860
+ """D8: Translation — ai4bharat/samanantar bidirectional En↔Hi."""
861
+ from datasets import load_dataset
862
+ domain = "08_translation_multilingual"
863
+ domain_id = DOMAIN_ID_MAP[domain]
864
+ cap = DOMAIN_CAPS[domain]
865
+ pool = []
866
+ print(f"\n{'='*60}")
867
+ print(f"📦 Building D8: {domain} (cap={cap})")
868
+
869
+ print(" Loading ai4bharat/samanantar (hi)...")
870
+ try:
871
+ ds = load_dataset("ai4bharat/samanantar", "hi", split="train", streaming=True)
872
+ count = 0
873
+ for row in ds:
874
+ if len(pool) >= cap:
875
+ break
876
+ src = row.get("src", "").strip()
877
+ tgt = row.get("tgt", "").strip()
878
+ if not src or not tgt:
879
+ continue
880
+ # Length filter: 25-180 chars
881
+ if len(src) < 25 or len(src) > 180 or len(tgt) < 25 or len(tgt) > 180:
882
+ continue
883
+ # Skip if contains code/HTML artifacts
884
+ if "<" in src or "<" in tgt or "{" in src or "http" in src.lower():
885
+ continue
886
+
887
+ # Alternate: 50% En→Hi, 50% Hi→En
888
+ if count % 2 == 0:
889
+ inst = f"Translate the following English sentence to Hindi:\n\"{src}\""
890
+ resp = tgt
891
+ else:
892
+ inst = f"Translate the following Hindi sentence to English:\n\"{tgt}\""
893
+ resp = src
894
+
895
+ if is_duplicate(inst):
896
+ continue
897
+ sample = create_training_sample(inst, resp, domain_id)
898
+ if sample:
899
+ pool.append(sample)
900
+ count += 1
901
+ except Exception as e:
902
+ print(f" ⚠️ Samanantar error: {e}")
903
+
904
+ random.shuffle(pool)
905
+ pool = pool[:cap]
906
+ print(f" ✅ D8 Translation: {len(pool)} samples (target: {cap})")
907
+ return pool
908
+
909
+
910
+ def build_domain_gk():
911
+ """D9: GK, Polity & History — alpaca-gpt4-hindi with creative blocklist."""
912
+ from datasets import load_dataset
913
+ domain = "09_gk_polity_history"
914
+ domain_id = DOMAIN_ID_MAP[domain]
915
+ cap = DOMAIN_CAPS[domain]
916
+ pool = []
917
+ print(f"\n{'='*60}")
918
+ print(f"📦 Building D9: {domain} (cap={cap})")
919
+
920
+ print(" Loading FreedomIntelligence/alpaca-gpt4-hindi...")
921
+ try:
922
+ ds = load_dataset("FreedomIntelligence/alpaca-gpt4-hindi", split="train")
923
+ for row in ds:
924
+ if len(pool) >= cap:
925
+ break
926
+ convs = row.get("conversations", [])
927
+ if convs and len(convs) >= 2:
928
+ inst = convs[0].get("value", "")
929
+ resp = convs[1].get("value", "")
930
+ else:
931
+ inst = row.get("instruction", "")
932
+ resp = row.get("output", "")
933
+ if not inst.strip() or not resp.strip():
934
+ continue
935
+ # Block creative tasks
936
+ if any(kw in inst for kw in CREATIVE_BLOCKLIST_HI):
937
+ continue
938
+ if any(kw in inst.lower() for kw in CREATIVE_BLOCKLIST_EN):
939
+ continue
940
+ if len(inst) < 10 or len(resp) < 15:
941
+ continue
942
+ if is_duplicate(inst):
943
+ continue
944
+ sample = create_training_sample(inst, resp, domain_id)
945
+ if sample:
946
+ pool.append(sample)
947
+ except Exception as e:
948
+ print(f" ⚠️ alpaca-gpt4-hindi error: {e}")
949
+
950
+ random.shuffle(pool)
951
+ pool = pool[:cap]
952
+ print(f" ✅ D9 GK, Polity & History: {len(pool)} samples (target: {cap})")
953
+ return pool
954
+
955
+
956
+ def build_domain_multiturn():
957
+ """D10: Multi-turn Conversations — smoltalk + SODA multi-turn."""
958
+ from datasets import load_dataset
959
+ domain = "10_multiturn_conversations"
960
+ domain_id = DOMAIN_ID_MAP[domain]
961
+ cap = DOMAIN_CAPS[domain]
962
+ pool = []
963
+ print(f"\n{'='*60}")
964
+ print(f"📦 Building D10: {domain} (cap={cap})")
965
+
966
+ # Source 1: smoltalk multi-turn configs
967
+ for config_name in ["smol-magpie-ultra", "openhermes-100k", "everyday-conversations"]:
968
+ if len(pool) >= cap:
969
+ break
970
+ print(f" Loading smoltalk/{config_name}...")
971
+ try:
972
+ ds = load_dataset("HuggingFaceTB/smoltalk", config_name, split="train", streaming=True)
973
+ for row in ds:
974
+ if len(pool) >= cap:
975
+ break
976
+ msgs = row.get("messages", [])
977
+ if len(msgs) < 4: # Need at least 2 turns
978
+ continue
979
+ # Convert to our turn format, max 8 turns
980
+ turns = []
981
+ for m in msgs[:8]:
982
+ role = m.get("role", "")
983
+ content = m.get("content", "").strip()
984
+ if role in ["user", "assistant"] and content:
985
+ turns.append({"role": role, "content": content})
986
+ if len(turns) < 4:
987
+ continue
988
+ # Ensure starts with user
989
+ if turns[0]["role"] != "user":
990
+ continue
991
+ first_msg = turns[0]["content"]
992
+ if is_duplicate(first_msg):
993
+ continue
994
+ sample = create_multiturn_training_sample(turns, domain_id)
995
+ if sample:
996
+ pool.append(sample)
997
+ except Exception as e:
998
+ print(f" ⚠️ smoltalk/{config_name} error: {e}")
999
+
1000
+ # Source 2: SODA multi-turn dialogues
1001
+ if len(pool) < cap:
1002
+ print(f" Loading allenai/soda multi-turn (need {cap - len(pool)} more)...")
1003
+ try:
1004
+ ds_soda = load_dataset("allenai/soda", split="train", streaming=True)
1005
+ for row in ds_soda:
1006
+ if len(pool) >= cap:
1007
+ break
1008
+ dialogue = row.get("dialogue", [])
1009
+ speakers = row.get("speakers", [])
1010
+ if len(dialogue) < 4:
1011
+ continue
1012
+ # Alternate user/assistant roles
1013
+ turns = []
1014
+ for i, msg in enumerate(dialogue[:8]):
1015
+ role = "user" if i % 2 == 0 else "assistant"
1016
+ if msg.strip():
1017
+ turns.append({"role": role, "content": msg.strip()})
1018
+ if len(turns) < 4:
1019
+ continue
1020
+ first_msg = turns[0]["content"]
1021
+ if is_duplicate(first_msg):
1022
+ continue
1023
+ sample = create_multiturn_training_sample(turns, domain_id)
1024
+ if sample:
1025
+ pool.append(sample)
1026
+ except Exception as e:
1027
+ print(f" ⚠️ SODA multi-turn error: {e}")
1028
+
1029
+ random.shuffle(pool)
1030
+ pool = pool[:cap]
1031
+ print(f" ✅ D10 Multi-turn: {len(pool)} samples (target: {cap})")
1032
+ return pool
1033
+
1034
+
1035
+ def build_domain_typo():
1036
+ """D11: Typo Robustness — Dynamic typo injection on greetings + alpaca-cleaned."""
1037
+ from datasets import load_dataset
1038
+ domain = "11_typo_robustness"
1039
+ domain_id = DOMAIN_ID_MAP[domain]
1040
+ cap = DOMAIN_CAPS[domain]
1041
+ pool = []
1042
+ print(f"\n{'='*60}")
1043
+ print(f"📦 Building D11: {domain} (cap={cap})")
1044
+
1045
+ # Source 1: Perturbed greetings (3 typo variations per greeting)
1046
+ greetings_path = os.path.join(DATA_DIR, "Greetings_Data_Cleaned", "Greetings_Cleaned_Master.json")
1047
+ greetings_data = []
1048
+ if os.path.exists(greetings_path):
1049
+ with open(greetings_path, "r", encoding="utf-8") as f:
1050
+ greetings_data = json.load(f)
1051
+ else:
1052
+ greetings_data = load_local_json("Greetings_Cleaned_Master.json")
1053
+
1054
+ for row in greetings_data:
1055
+ if len(pool) >= cap:
1056
+ break
1057
+ inst = row.get("instruction") or row.get("prompt") or ""
1058
+ resp = row.get("response") or row.get("output") or ""
1059
+ if not inst.strip() or not resp.strip():
1060
+ continue
1061
+ # Generate 3 typo variations
1062
+ for _ in range(3):
1063
+ typo_inst = inject_typo(inst.strip(), typo_prob=0.5)
1064
+ if typo_inst == inst.strip():
1065
+ continue
1066
+ if is_duplicate(typo_inst):
1067
+ continue
1068
+ sample = create_training_sample(typo_inst, resp, domain_id)
1069
+ if sample:
1070
+ pool.append(sample)
1071
+ if len(pool) >= cap:
1072
+ break
1073
+
1074
+ # Source 2: Perturbed alpaca-cleaned instructions
1075
+ if len(pool) < cap:
1076
+ print(f" Generating typo-perturbed alpaca samples (need {cap - len(pool)} more)...")
1077
+ try:
1078
+ ds = load_dataset("yahma/alpaca-cleaned", split="train")
1079
+ indices = list(range(len(ds)))
1080
+ random.shuffle(indices)
1081
+ for idx in indices:
1082
+ if len(pool) >= cap:
1083
+ break
1084
+ row = ds[idx]
1085
+ inst = row.get("instruction", "")
1086
+ resp = row.get("output", "")
1087
+ if not inst.strip() or not resp.strip() or len(inst) < 15:
1088
+ continue
1089
+ typo_inst = inject_typo(inst.strip(), typo_prob=0.6)
1090
+ if typo_inst == inst.strip():
1091
+ continue
1092
+ if is_duplicate(typo_inst):
1093
+ continue
1094
+ sample = create_training_sample(typo_inst, resp, domain_id)
1095
+ if sample:
1096
+ pool.append(sample)
1097
+ except Exception as e:
1098
+ print(f" ⚠️ alpaca-cleaned typo error: {e}")
1099
+
1100
+ random.shuffle(pool)
1101
+ pool = pool[:cap]
1102
+ print(f" ✅ D11 Typo Robustness: {len(pool)} samples (target: {cap})")
1103
+ return pool
1104
+
1105
+
1106
+ def build_domain_hinglish():
1107
+ """D12: Hinglish Code-Mixed — alpaca-gpt4-hindi with creative blocklist."""
1108
+ from datasets import load_dataset
1109
+ domain = "12_hinglish_codemixed"
1110
+ domain_id = DOMAIN_ID_MAP[domain]
1111
+ cap = DOMAIN_CAPS[domain]
1112
+ pool = []
1113
+ print(f"\n{'='*60}")
1114
+ print(f"📦 Building D12: {domain} (cap={cap})")
1115
+
1116
+ print(" Loading FreedomIntelligence/alpaca-gpt4-hindi (Hinglish)...")
1117
+ try:
1118
+ ds = load_dataset("FreedomIntelligence/alpaca-gpt4-hindi", split="train")
1119
+ # Shuffle to get different samples than D9
1120
+ indices = list(range(len(ds)))
1121
+ random.shuffle(indices)
1122
+ for idx in indices:
1123
+ if len(pool) >= cap:
1124
+ break
1125
+ row = ds[idx]
1126
+ convs = row.get("conversations", [])
1127
+ if convs and len(convs) >= 2:
1128
+ inst = convs[0].get("value", "")
1129
+ resp = convs[1].get("value", "")
1130
+ else:
1131
+ inst = row.get("instruction", "")
1132
+ resp = row.get("output", "")
1133
+ if not inst.strip() or not resp.strip():
1134
+ continue
1135
+ if any(kw in inst for kw in CREATIVE_BLOCKLIST_HI):
1136
+ continue
1137
+ if is_duplicate(inst):
1138
+ continue
1139
+ sample = create_training_sample(inst, resp, domain_id)
1140
+ if sample:
1141
+ pool.append(sample)
1142
+ except Exception as e:
1143
+ print(f" ⚠️ alpaca-gpt4-hindi hinglish error: {e}")
1144
+
1145
+ random.shuffle(pool)
1146
+ pool = pool[:cap]
1147
+ print(f" ✅ D12 Hinglish Code-Mixed: {len(pool)} samples (target: {cap})")
1148
+ return pool
1149
+
1150
+
1151
+ def build_domain_safety():
1152
+ """D13: Safety & Refusal — BeaverTails (is_safe=True) + smoltalk/systemchats."""
1153
+ from datasets import load_dataset
1154
+ domain = "13_safety_refusal"
1155
+ domain_id = DOMAIN_ID_MAP[domain]
1156
+ cap = DOMAIN_CAPS[domain]
1157
+ pool = []
1158
+ print(f"\n{'='*60}")
1159
+ print(f"📦 Building D13: {domain} (cap={cap})")
1160
+
1161
+ # Source 1: BeaverTails (safe responses only)
1162
+ print(" Loading PKU-Alignment/BeaverTails (is_safe=True)...")
1163
+ try:
1164
+ ds = load_dataset("PKU-Alignment/BeaverTails", split="30k_train")
1165
+ for row in ds:
1166
+ if len(pool) >= cap:
1167
+ break
1168
+ if not row.get("is_safe", False):
1169
+ continue
1170
+ prompt = row.get("prompt", "").strip()
1171
+ response = row.get("response", "").strip()
1172
+ if not prompt or not response:
1173
+ continue
1174
+ # Clean prompt leak artifacts
1175
+ response = re.sub(r"^(Instruction|Output|Response)\s*:\s*", "", response)
1176
+ if len(response) < 20:
1177
+ continue
1178
+ if is_duplicate(prompt):
1179
+ continue
1180
+ sample = create_training_sample(prompt, response, domain_id)
1181
+ if sample:
1182
+ pool.append(sample)
1183
+ except Exception as e:
1184
+ print(f" ⚠️ BeaverTails error: {e}")
1185
+
1186
+ # Source 2: smoltalk/systemchats-30k (safety-related)
1187
+ if len(pool) < cap:
1188
+ print(f" Loading smoltalk/systemchats-30k (safety, need {cap - len(pool)} more)...")
1189
+ SAFETY_KWS = {"safe", "ethical", "illegal", "privacy", "security", "harm", "dangerous"}
1190
+ try:
1191
+ ds_sm = load_dataset("HuggingFaceTB/smoltalk", "systemchats-30k", split="train", streaming=True)
1192
+ for row in ds_sm:
1193
+ if len(pool) >= cap:
1194
+ break
1195
+ msgs = row.get("messages", [])
1196
+ if len(msgs) < 2:
1197
+ continue
1198
+ inst = msgs[0].get("content", "")
1199
+ resp = msgs[-1].get("content", "")
1200
+ if not inst.strip() or not resp.strip():
1201
+ continue
1202
+ lower_text = (inst + " " + resp).lower()
1203
+ if not any(kw in lower_text for kw in SAFETY_KWS):
1204
+ continue
1205
+ if is_duplicate(inst):
1206
+ continue
1207
+ sample = create_training_sample(inst, resp, domain_id)
1208
+ if sample:
1209
+ pool.append(sample)
1210
+ except Exception as e:
1211
+ print(f" ⚠️ smoltalk/systemchats error: {e}")
1212
+
1213
+ random.shuffle(pool)
1214
+ pool = pool[:cap]
1215
+ print(f" ✅ D13 Safety & Refusal: {len(pool)} samples (target: {cap})")
1216
+ return pool
1217
+
1218
+
1219
+ def build_domain_instruction_following():
1220
+ """D14: Instruction Following — smoltalk constraints/rewrite/summarize."""
1221
+ from datasets import load_dataset
1222
+ domain = "14_instruction_following"
1223
+ domain_id = DOMAIN_ID_MAP[domain]
1224
+ cap = DOMAIN_CAPS[domain]
1225
+ pool = []
1226
+ print(f"\n{'='*60}")
1227
+ print(f"📦 Building D14: {domain} (cap={cap})")
1228
+
1229
+ for config_name in ["smol-constraints", "smol-rewrite", "smol-summarize"]:
1230
+ if len(pool) >= cap:
1231
+ break
1232
+ print(f" Loading smoltalk/{config_name}...")
1233
+ try:
1234
+ ds = load_dataset("HuggingFaceTB/smoltalk", config_name, split="train", streaming=True)
1235
+ for row in ds:
1236
+ if len(pool) >= cap:
1237
+ break
1238
+ msgs = row.get("messages", [])
1239
+ if len(msgs) < 2:
1240
+ continue
1241
+ inst = msgs[0].get("content", "") if msgs[0].get("role") == "user" else ""
1242
+ resp = msgs[-1].get("content", "") if msgs[-1].get("role") == "assistant" else ""
1243
+ if not inst.strip() or not resp.strip():
1244
+ continue
1245
+ if is_duplicate(inst):
1246
+ continue
1247
+ sample = create_training_sample(inst, resp, domain_id)
1248
+ if sample:
1249
+ pool.append(sample)
1250
+ except Exception as e:
1251
+ print(f" ⚠️ smoltalk/{config_name} error: {e}")
1252
+
1253
+ random.shuffle(pool)
1254
+ pool = pool[:cap]
1255
+ print(f" ✅ D14 Instruction Following: {len(pool)} samples (target: {cap})")
1256
+ return pool
1257
+
1258
+
1259
+ # ==============================================================================
1260
+ # Section 6: Master Pipeline — Build, Split, Save, Upload
1261
+ # ==============================================================================
1262
+ def build_all_domains():
1263
+ """Build all 15 domains and return the master pool."""
1264
+ builders = [
1265
+ build_domain_identity_greetings, # D0
1266
+ build_domain_coding, # D1
1267
+ build_domain_math, # D2
1268
+ build_domain_reasoning, # D3
1269
+ build_domain_chitchat, # D4
1270
+ build_domain_career, # D5
1271
+ build_domain_recipes, # D6
1272
+ build_domain_health, # D7
1273
+ build_domain_translation, # D8
1274
+ build_domain_gk, # D9
1275
+ build_domain_multiturn, # D10
1276
+ build_domain_typo, # D11
1277
+ build_domain_hinglish, # D12
1278
+ build_domain_safety, # D13
1279
+ build_domain_instruction_following, # D14
1280
+ ]
1281
+
1282
+ master_pool = []
1283
+ domain_counts = defaultdict(int)
1284
+
1285
+ for builder_fn in builders:
1286
+ samples = builder_fn()
1287
+ for s in samples:
1288
+ domain_counts[s["domain_id"]] += 1
1289
+ master_pool.extend(samples)
1290
+
1291
+ print(f"\n{'='*85}")
1292
+ print(f"📊 MASTER POOL SUMMARY: {len(master_pool):,} total samples")
1293
+ print(f"{'='*85}")
1294
+ for did in sorted(domain_counts.keys()):
1295
+ dname = [k for k, v in DOMAIN_ID_MAP.items() if v == did][0]
1296
+ target = DOMAIN_CAPS[dname]
1297
+ actual = domain_counts[did]
1298
+ pct = actual / target * 100
1299
+ status = "✅" if actual >= target * 0.8 else "⚠️"
1300
+ print(f" {status} D{did:02d} {dname}: {actual:,} / {target:,} ({pct:.1f}%)")
1301
+
1302
+ # Safety audit: verify NO thinking tags in any sample
1303
+ print(f"\n🔍 Running ZERO-THINK safety audit...")
1304
+ think_violations = 0
1305
+ for s in master_pool:
1306
+ decoded = tokenizer.decode(s["input_ids"])
1307
+ if "<think>" in decoded or "[THINK]" in decoded or "<|begin_of_thought|>" in decoded:
1308
+ think_violations += 1
1309
+ if think_violations > 0:
1310
+ print(f" ❌ CRITICAL: {think_violations} samples contain thinking tags!")
1311
+ else:
1312
+ print(f" ✅ ZERO thinking tag violations confirmed!")
1313
+
1314
+ return master_pool
1315
+
1316
+
1317
+ def split_and_save(master_pool: list):
1318
+ """Split into train/val sets and save as NPY shards."""
1319
+ random.shuffle(master_pool)
1320
+
1321
+ # Stratified split by domain
1322
+ domain_buckets = defaultdict(list)
1323
+ for sample in master_pool:
1324
+ domain_buckets[sample["domain_id"]].append(sample)
1325
+
1326
+ train_pool, val_pool = [], []
1327
+ for did, items in domain_buckets.items():
1328
+ random.shuffle(items)
1329
+ n_val = max(10, int(len(items) * VAL_SPLIT_RATIO))
1330
+ val_pool.extend(items[:n_val])
1331
+ train_pool.extend(items[n_val:])
1332
+
1333
+ random.shuffle(train_pool)
1334
+ random.shuffle(val_pool)
1335
+
1336
+ print(f"\n📊 Split: Train={len(train_pool):,} | Val={len(val_pool):,}")
1337
+
1338
+ # Save each split
1339
+ total_train_tokens = 0
1340
+ total_val_tokens = 0
1341
+
1342
+ for split_name, split_pool in [("train", train_pool), ("val", val_pool)]:
1343
+ all_tokens = []
1344
+ all_labels = []
1345
+ offsets = [0]
1346
+ domains = []
1347
+
1348
+ for sample in split_pool:
1349
+ ids = sample["input_ids"]
1350
+ lbl = sample["labels"]
1351
+ all_tokens.extend(ids)
1352
+ all_labels.extend(lbl)
1353
+ offsets.append(len(all_tokens))
1354
+ domains.append(sample["domain_id"])
1355
+
1356
+ tokens_np = np.array(all_tokens, dtype=np.int32)
1357
+ labels_np = np.array(all_labels, dtype=np.int32)
1358
+ offsets_np = np.array(offsets, dtype=np.uint32)
1359
+ domains_np = np.array(domains, dtype=np.uint8)
1360
+
1361
+ np.save(os.path.join(OUTPUT_DIR, f"{split_name}_tokens.npy"), tokens_np)
1362
+ np.save(os.path.join(OUTPUT_DIR, f"{split_name}_labels.npy"), labels_np)
1363
+ np.save(os.path.join(OUTPUT_DIR, f"{split_name}_offsets.npy"), offsets_np)
1364
+ np.save(os.path.join(OUTPUT_DIR, f"{split_name}_domains.npy"), domains_np)
1365
+
1366
+ total = len(tokens_np)
1367
+ if split_name == "train":
1368
+ total_train_tokens = total
1369
+ else:
1370
+ total_val_tokens = total
1371
+
1372
+ print(f" 💾 Saved {split_name}: {len(split_pool):,} samples, {total:,} tokens")
1373
+
1374
+ # Save metadata
1375
+ domain_count_map = defaultdict(int)
1376
+ for sample in master_pool:
1377
+ did = sample["domain_id"]
1378
+ dname = [k for k, v in DOMAIN_ID_MAP.items() if v == did][0]
1379
+ domain_count_map[dname] += 1
1380
+
1381
+ metadata = {
1382
+ "version": f"sft_{VERSION}",
1383
+ "total_samples": len(master_pool),
1384
+ "train_samples": len(train_pool),
1385
+ "val_samples": len(val_pool),
1386
+ "total_tokens": total_train_tokens + total_val_tokens,
1387
+ "train_tokens": total_train_tokens,
1388
+ "val_tokens": total_val_tokens,
1389
+ "max_seq_len": MAX_SEQ_LEN,
1390
+ "vocab_size": 64003,
1391
+ "cot_thinking_ratio_percent": 0.0,
1392
+ "thinking_tags": "NONE (Pure Gold v20)",
1393
+ "domain_counts": dict(domain_count_map),
1394
+ "blocked_sources": list(BLOCKED_FILES),
1395
+ "blocked_categories": list(BLOCKED_CATEGORIES),
1396
+ }
1397
+
1398
+ meta_path = os.path.join(OUTPUT_DIR, "metadata.json")
1399
+ with open(meta_path, "w", encoding="utf-8") as f:
1400
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
1401
+ print(f" 📄 Saved metadata.json")
1402
+
1403
+ return metadata
1404
+
1405
+
1406
+ def upload_to_hf():
1407
+ """Upload tokenized shards to HF dataset hub."""
1408
+ try:
1409
+ from huggingface_hub import HfApi
1410
+ api = HfApi(token=HF_TOKEN)
1411
+ sft_prefix = f"sft_{VERSION}"
1412
+
1413
+ files_to_upload = [
1414
+ "train_tokens.npy", "train_labels.npy", "train_offsets.npy", "train_domains.npy",
1415
+ "val_tokens.npy", "val_labels.npy", "val_offsets.npy", "val_domains.npy",
1416
+ "metadata.json",
1417
+ ]
1418
+
1419
+ for fname in files_to_upload:
1420
+ local_path = os.path.join(OUTPUT_DIR, fname)
1421
+ if os.path.exists(local_path):
1422
+ remote_path = f"{sft_prefix}/{fname}"
1423
+ api.upload_file(
1424
+ path_or_fileobj=local_path,
1425
+ path_in_repo=remote_path,
1426
+ repo_id=HF_DATA_REPO,
1427
+ repo_type="dataset",
1428
+ )
1429
+ fsize = os.path.getsize(local_path) / (1024 * 1024)
1430
+ print(f" ☁️ Uploaded {remote_path} ({fsize:.1f} MB)")
1431
+
1432
+ print(f" ✅ All shards uploaded to {HF_DATA_REPO}/{sft_prefix}/")
1433
+ except Exception as e:
1434
+ print(f" ⚠️ HF upload error: {e}")
1435
+
1436
+
1437
+ # ==============================================================================
1438
+ # Main Entry Point
1439
+ # ==============================================================================
1440
+ def main():
1441
+ start_time = time.time()
1442
+
1443
+ print(f"\n🚀 Starting SFT {VERSION.upper()} 'PURE GOLD' dataset build...")
1444
+ master_pool = build_all_domains()
1445
+ metadata = split_and_save(master_pool)
1446
+
1447
+ elapsed = (time.time() - start_time) / 60
1448
+ print(f"\n{'='*85}")
1449
+ print(f"🎉 SFT {VERSION.upper()} 'PURE GOLD' BUILD COMPLETE!")
1450
+ print(f" Total Samples: {metadata['total_samples']:,}")
1451
+ print(f" Total Tokens: {metadata['total_tokens']:,}")
1452
+ print(f" CoT Thinking: {metadata['cot_thinking_ratio_percent']}%")
1453
+ print(f" Build Time: {elapsed:.2f} minutes")
1454
+ print(f"{'='*85}")
1455
+
1456
+ # Upload to HF
1457
+ upload_to_hf()
1458
+
1459
+ print(f"\n✅ All done! Dataset ready at: {OUTPUT_DIR}")
1460
+
1461
+
1462
+ if __name__ == "__main__":
1463
+ main()