ViuAI commited on
Commit
8a7fbdc
·
verified ·
1 Parent(s): 322b3c6

Delete code/build_sft_v2_chat_dataset.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. code/build_sft_v2_chat_dataset.py +0 -861
code/build_sft_v2_chat_dataset.py DELETED
@@ -1,861 +0,0 @@
1
- # ==============================================================================
2
- # 💬 ViuAI Sarus-500M — Master SFT v2 Pure Chat Dataset Builder & Tokenizer
3
- # ==============================================================================
4
- # 100% Real Conversational & Knowledge Data | 0% Math | 0% Coding
5
- # Specially Designed for Sarus-500M Parameter Capacity:
6
- # • D0: Identity & Persona (Sarus / ViuAI) ~3,000
7
- # • D1: Bilingual Greetings & Openers ~4,000
8
- # • D2: Conversational Dialogue & Multi-turn ~35,000
9
- # • D3: General Knowledge & Science (English) ~40,000
10
- # • D4: Hindi Factual QA & Knowledge (Breaks Greeting Loop) ~40,000
11
- # • D5: Practical Writing & Everyday Tasks ~30,000
12
- # • D6: Culture, Recipes & Lifestyle ~25,000
13
- # • D7: Bilingual Translation (EN <-> HI) ~25,000
14
- # Total: ~202,000 High-Purity Conversational Samples
15
- # ==============================================================================
16
-
17
- import os
18
- import sys
19
- import json
20
- import re
21
- import random
22
- import hashlib
23
- import shutil
24
- import argparse
25
- import numpy as np
26
- from datasets import load_dataset
27
- from huggingface_hub import HfApi, hf_hub_download
28
- from tokenizers import Tokenizer
29
-
30
- # Fix console encoding
31
- if hasattr(sys.stdout, "reconfigure"):
32
- sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
33
- if hasattr(sys.stderr, "reconfigure"):
34
- sys.stderr.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
35
-
36
- BASE_DIR = os.getcwd()
37
- RAW_EXPORT_DIR = os.path.join(BASE_DIR, "sarus_sft_raw", "sft_v2")
38
- TOKENIZED_DIR = os.path.join(BASE_DIR, "sarus_sft_tokenized", "sft_v2")
39
- os.makedirs(RAW_EXPORT_DIR, exist_ok=True)
40
- os.makedirs(TOKENIZED_DIR, exist_ok=True)
41
-
42
- # SECURITY: HF token from env only. Never hardcode. Export HF_TOKEN before running.
43
- HF_TOKEN = os.environ.get("HF_TOKEN")
44
- RAW_REPO_ID = "ViuAI/sarus-sft-data-raw"
45
- TOKENIZED_REPO_ID = "ViuAI/viuai-500m-sft-tokenized"
46
- MODEL_REPO_ID = "ViuAI/ViuAI-500M"
47
-
48
- MAX_SEQ_LEN = 1024
49
- VAL_SPLIT_RATIO = 0.02
50
- MIN_TOKENS = 6
51
-
52
- USER_TOKEN = "<|user|>"
53
- ASSISTANT_TOKEN = "<|assistant|>"
54
- EOT_TOKEN = "<|endofturn|>"
55
- EOT_ID = 64002
56
- # EOT-as-pad. Never use <|user|> (64000) as pad.
57
- PAD_TOKEN_ID = EOT_ID
58
-
59
- DOMAIN_NAMES = {
60
- 0: "identity_sarus",
61
- 1: "greetings_cultural",
62
- 2: "conversational_dialogue",
63
- 3: "general_knowledge_science",
64
- 4: "hindi_knowledge_and_qa",
65
- 5: "practical_writing_tasks",
66
- 6: "culture_recipes_lifestyle",
67
- 7: "translation_bilingual"
68
- }
69
-
70
- DOMAIN_TARGETS = {
71
- 0: 3000, # Identity & Attribution
72
- 1: 4000, # Greetings & Icebreakers
73
- 2: 35000, # Everyday Multi-Turn Chat
74
- 3: 40000, # English Science & World Facts
75
- 4: 40000, # Hindi Factual QA (NCERT Hindi + Indian GK)
76
- 5: 30000, # Emails, Summaries & Everyday Writing
77
- 6: 25000, # Indian Recipes, Culture & Wellness
78
- 7: 25000 # English <-> Hindi Translation
79
- }
80
-
81
- # ------------------------------------------------------------------------------
82
- # 1. Tokenizer Setup
83
- # ------------------------------------------------------------------------------
84
- def load_fast_tokenizer():
85
- tok_candidates = [
86
- os.path.join(BASE_DIR, "tokenizer", "tokenizer.json"),
87
- os.path.join(BASE_DIR, "tokenizer.json")
88
- ]
89
- for p in tok_candidates:
90
- if os.path.exists(p):
91
- tok = Tokenizer.from_file(p)
92
- print(f"✅ Tokenizer loaded from {p} ({tok.get_vocab_size():,} vocab)")
93
- return tok
94
-
95
- print("🌐 Downloading tokenizer.json from Hugging Face Model Hub...")
96
- dl = hf_hub_download(repo_id=MODEL_REPO_ID, filename="tokenizer/tokenizer.json", token=HF_TOKEN)
97
- tok = Tokenizer.from_file(dl)
98
- os.makedirs(os.path.join(BASE_DIR, "tokenizer"), exist_ok=True)
99
- shutil.copy(dl, os.path.join(BASE_DIR, "tokenizer", "tokenizer.json"))
100
- print(f"✅ Tokenizer loaded ({tok.get_vocab_size():,} vocab)")
101
- return tok
102
-
103
- TOKENIZER = None
104
-
105
- # ------------------------------------------------------------------------------
106
- # 2. Text Cleaner & Math/Code Filter (Guarantees Pure Chat)
107
- # ------------------------------------------------------------------------------
108
- GLOBAL_SEEN_HASHES = set()
109
-
110
- def is_duplicate(text: str) -> bool:
111
- clean = re.sub(r"\s+", " ", text.lower().strip())
112
- h = hashlib.sha256(clean.encode("utf-8")).hexdigest()
113
- if h in GLOBAL_SEEN_HASHES:
114
- return True
115
- GLOBAL_SEEN_HASHES.add(h)
116
- return False
117
-
118
- def clean_text(text: str) -> str:
119
- if not text:
120
- return ""
121
- text = re.sub(r"(?:संदर्भ|सन्दर्भ|बाहरी कड़ियाँ|श्रेणियाँ|सम्पादन|References|External links|See also)[\s:]*$", "", text, flags=re.IGNORECASE)
122
- text = re.sub(r"<[^>]+>", " ", text)
123
- text = re.sub(r"[ \t]+", " ", text)
124
- text = re.sub(r"\n{3,}", "\n\n", text)
125
- return text.strip()
126
-
127
- def contains_math_or_code(text: str) -> bool:
128
- """Strict filter: rejects any programming code or complex math formula."""
129
- lower = text.lower()
130
- # Code patterns
131
- if "```python" in lower or "```java" in lower or "```cpp" in lower or "```c" in lower:
132
- return True
133
- if "def " in text and "):" in text:
134
- return True
135
- if "class " in text and ":" in text and "def __init__" in text:
136
- return True
137
- if "#include <" in text or "public static void main" in text:
138
- return True
139
- # Math / equation patterns
140
- if "\\frac" in text or "\\sqrt" in text or "\\begin{equation}" in text:
141
- return True
142
- if re.search(r"\b(?:calculate|solve|evaluate)\s*:\s*\d+\s*[\+\-\*\/]\s*\d+", lower):
143
- return True
144
- return False
145
-
146
- # ------------------------------------------------------------------------------
147
- # 3. Domain Streamers & Builders
148
- # ------------------------------------------------------------------------------
149
-
150
- def load_json_or_jsonl(path):
151
- if not path or not os.path.exists(path):
152
- return []
153
- try:
154
- with open(path, "r", encoding="utf-8") as f:
155
- c = f.read().strip()
156
- if c.startswith("["):
157
- return json.loads(c)
158
- return [json.loads(line) for line in c.splitlines() if line.strip()]
159
- except Exception as e:
160
- print(f" ⚠️ Could not parse {path}: {e}")
161
- return []
162
-
163
- # --- D0: Identity (Sarus / ViuAI) ---
164
- def build_domain_identity(target=3000):
165
- print(f"\n[D0: Identity] Sourcing authentic Sarus identity dataset (Target: {target:,})...")
166
- samples = []
167
- candidates = [
168
- os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d0_identity_sarus.jsonl"),
169
- os.path.join(BASE_DIR, "data", "sft_identity.json"),
170
- os.path.join(BASE_DIR, "sft_identity.json")
171
- ]
172
- p_to_use = None
173
- for p in candidates:
174
- if os.path.exists(p):
175
- p_to_use = p
176
- break
177
-
178
- if p_to_use is None:
179
- try:
180
- print(f" 📥 Downloading d0_identity_sarus.jsonl from HF ({RAW_REPO_ID})...")
181
- p_dl = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d0_identity_sarus.jsonl")
182
- os.makedirs(os.path.dirname(p_dl), exist_ok=True)
183
- dl = hf_hub_download(repo_id=RAW_REPO_ID, filename="d0_identity_sarus.jsonl", repo_type="dataset", token=HF_TOKEN)
184
- shutil.copy(dl, p_dl)
185
- p_to_use = p_dl
186
- except Exception as e:
187
- print(f" ℹ️ HF fetch identity notice: {e}")
188
- try:
189
- dl2 = hf_hub_download(repo_id=MODEL_REPO_ID, filename="data/sft_identity.json", token=HF_TOKEN)
190
- p_to_use = dl2
191
- except Exception as e2:
192
- print(f" ℹ️ Fallback notice: {e2}")
193
-
194
- raw_items = load_json_or_jsonl(p_to_use)
195
- for item in raw_items:
196
- q = clean_text((item.get("instruction") or "").replace("ViuAi", "ViuAI"))
197
- r = clean_text((item.get("response") or "").replace("ViuAi", "ViuAI"))
198
- if q and r and not is_duplicate(q):
199
- samples.append({"instruction": q, "response": r, "domain_id": 0})
200
- if len(samples) >= target:
201
- break
202
- print(f" ✅ [D0: Identity] Loaded {len(samples):,} authentic Sarus identity samples.")
203
- return samples[:target]
204
-
205
- # --- D1: Greetings & Cultural Openers ---
206
- def build_domain_greetings(target=4000):
207
- print(f"\n[D1: Greetings] Sourcing authentic greetings dataset (Target: {target:,})...")
208
- samples = []
209
- candidates = [
210
- os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d1_greetings_cultural.jsonl"),
211
- os.path.join(BASE_DIR, "data", "sft_greetings.json"),
212
- os.path.join(BASE_DIR, "sft_greetings.json")
213
- ]
214
- p_to_use = None
215
- for p in candidates:
216
- if os.path.exists(p):
217
- p_to_use = p
218
- break
219
-
220
- if p_to_use is None:
221
- try:
222
- print(f" 📥 Downloading d1_greetings_cultural.jsonl from HF ({RAW_REPO_ID})...")
223
- p_dl = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d1_greetings_cultural.jsonl")
224
- os.makedirs(os.path.dirname(p_dl), exist_ok=True)
225
- dl = hf_hub_download(repo_id=RAW_REPO_ID, filename="d1_greetings_cultural.jsonl", repo_type="dataset", token=HF_TOKEN)
226
- shutil.copy(dl, p_dl)
227
- p_to_use = p_dl
228
- except Exception as e:
229
- print(f" ℹ️ HF fetch greetings notice: {e}")
230
- try:
231
- dl2 = hf_hub_download(repo_id=MODEL_REPO_ID, filename="data/sft_greetings.json", token=HF_TOKEN)
232
- p_to_use = dl2
233
- except Exception as e2:
234
- print(f" ℹ️ Fallback notice: {e2}")
235
-
236
- raw_items = load_json_or_jsonl(p_to_use)
237
- for item in raw_items:
238
- q = clean_text(item.get("instruction") or "")
239
- r = clean_text(item.get("response") or "")
240
- if q and r and not is_duplicate(q):
241
- samples.append({"instruction": q, "response": r, "domain_id": 1})
242
- if len(samples) >= target:
243
- break
244
- print(f" ✅ [D1: Greetings] Loaded {len(samples):,} authentic greetings.")
245
- return samples[:target]
246
-
247
- # --- D2: Conversational Dialogue & Multi-turn ---
248
- def build_domain_dialogue(target=35000):
249
- print(f"\n[D2: Dialogue] Sourcing everyday conversational dialogue (Target: {target:,})...")
250
- samples = []
251
-
252
- # 1. Check local pre-extracted multi-turn
253
- local_p = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d8_multiturn_dialogue.jsonl")
254
- if os.path.exists(local_p):
255
- with open(local_p, "r", encoding="utf-8") as f:
256
- for line in f:
257
- try:
258
- item = json.loads(line)
259
- except Exception:
260
- continue
261
- q = clean_text(item.get("instruction") or "")
262
- r = clean_text(item.get("response") or "")
263
- if q and r and not is_duplicate(q) and not contains_math_or_code(r):
264
- samples.append({"instruction": q, "response": r, "domain_id": 2})
265
- if len(samples) >= target:
266
- break
267
-
268
- # 2. UltraChat 200k fallback/stream
269
- if len(samples) < target:
270
- try:
271
- print(" 🌐 Streaming everyday conversations from HuggingFaceH4/ultrachat_200k...")
272
- ds = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft", streaming=True)
273
- for row in ds:
274
- msgs = row.get("messages", [])
275
- if len(msgs) >= 2 and msgs[0].get("role") == "user" and msgs[1].get("role") == "assistant":
276
- q = clean_text(msgs[0].get("content", ""))
277
- r = clean_text(msgs[1].get("content", ""))
278
- if q and r and len(q) < 600 and len(r) < 1200 and not is_duplicate(q) and not contains_math_or_code(r):
279
- samples.append({"instruction": q, "response": r, "domain_id": 2})
280
- if len(samples) >= target:
281
- break
282
- except Exception as e:
283
- print(f" ℹ️ UltraChat stream warning: {e}")
284
-
285
- print(f" ✅ [D2: Dialogue] Loaded {len(samples):,} conversational dialogue samples.")
286
- return samples[:target]
287
-
288
- # --- D3: General Knowledge & Science (English) ---
289
- def build_domain_gk_science(target=40000):
290
- print(f"\n[D3: GK & Science] Sourcing English Science & World Facts (Target: {target:,})...")
291
- samples = []
292
-
293
- # 1. NCERT Science Class 9-12 (Biology, Chemistry, Physics)
294
- try:
295
- print(" 🌐 Streaming NCERT Science from ParthKadam2003/NCERT_Dataset...")
296
- ds_ncert = load_dataset("ParthKadam2003/NCERT_Dataset", split="train", streaming=True)
297
- for row in ds_ncert:
298
- q = (row.get("Question") or "").strip()
299
- ans = (row.get("Answer") or "").strip()
300
- exp = (row.get("Explanation") or "").strip()
301
- if q and ans and not is_duplicate(q) and not contains_math_or_code(ans):
302
- resp = ans
303
- if exp and len(exp) > 20 and exp != ans and len(exp) < 600:
304
- resp = f"{ans}\n\nExplanation: {exp}"
305
- samples.append({"instruction": clean_text(q), "response": clean_text(resp), "domain_id": 3})
306
- if len(samples) >= 20000:
307
- break
308
- except Exception as e:
309
- print(f" ℹ️ NCERT dataset stream warning: {e}")
310
-
311
- # 2. Local D5 General STEM (Filtered for clean science concepts, no math/code)
312
- local_stem = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d5_general_stem.jsonl")
313
- if os.path.exists(local_stem) and len(samples) < target:
314
- with open(local_stem, "r", encoding="utf-8") as f:
315
- for line in f:
316
- try:
317
- item = json.loads(line)
318
- except Exception:
319
- continue
320
- q = clean_text(item.get("instruction") or "")
321
- r = clean_text(item.get("response") or "")
322
- if q and r and not is_duplicate(q) and not contains_math_or_code(r) and not contains_math_or_code(q):
323
- samples.append({"instruction": q, "response": r, "domain_id": 3})
324
- if len(samples) >= target:
325
- break
326
-
327
- # 3. Stanford Alpaca (General Science/World knowledge)
328
- if len(samples) < target:
329
- try:
330
- print(" 🌐 Streaming Stanford Alpaca for World Knowledge...")
331
- ds_alpaca = load_dataset("tatsu-lab/alpaca", split="train", streaming=True)
332
- for row in ds_alpaca:
333
- q = (row.get("instruction") or "").strip()
334
- inp = (row.get("input") or "").strip()
335
- r = (row.get("output") or "").strip()
336
- if inp:
337
- q = f"{q}\n\n{inp}"
338
- if q and r and not is_duplicate(q) and not contains_math_or_code(q) and not contains_math_or_code(r):
339
- samples.append({"instruction": clean_text(q), "response": clean_text(r), "domain_id": 3})
340
- if len(samples) >= target:
341
- break
342
- except Exception as e:
343
- print(f" ℹ️ Alpaca stream warning: {e}")
344
-
345
- print(f" ✅ [D3: GK & Science] Loaded {len(samples):,} Science and World Knowledge samples.")
346
- return samples[:target]
347
-
348
- # --- D4: Hindi Factual QA & Knowledge (BREAKS THE GREETING LOOP!) ---
349
- def build_domain_hindi_qa(target=40000):
350
- print(f"\n[D4: Hindi Factual QA] Sourcing pure Hindi QA & Knowledge (Target: {target:,})...")
351
- samples = []
352
-
353
- # 1. Curated Ground-Truth Hindi Factual QA (Directly fixes observed failures)
354
- ground_truth_hindi = [
355
- ("भारत के प्रथम प्रधानमंत्री कौन थे?", "भारत के प्रथम प्रधानमंत्री पंडित जवाहरलाल नेहरू थे।"),
356
- ("भारत के प्रथम राष्ट्रपति कौन थे?", "भारत के प्रथम राष्ट्रपति डॉ. राजेंद्र प्रसाद थे।"),
357
- ("भारतीय संविधान के निर्माता कौन थे?", "भारतीय संविधान के मुख्य निर्माता और प्रारूप समिति (Drafting Committee) के अध्यक्ष डॉ. भीमराव अंबेडकर थे।"),
358
- ("भारत की राजधानी क्या है?", "भारत की राजधानी नई दिल्ली है।"),
359
- ("सूर्य किस दिशा से निकलता है?", "सूर्य पूर्व दिशा से निकलता है और पश्चिम दिशा में अस्त होता है।"),
360
- ("पृथ्वी के सबसे नजदीक कौन सा तारा है?", "पृथ्वी के सबसे निकटतम तारा सूर्य है।"),
361
- ("सौरमंडल का सबसे बड़ा ग्रह कौन सा है?", "सौरमंडल का सबसे बड़ा ग्रह बृहस्पति (Jupiter) है।"),
362
- ("लाल ग्रह (Red Planet) किसे कहा जाता है?", "मंगल ग्रह (Mars) को लाल ग्रह कहा जाता है क्योंकि इसकी सतह पर आयरन ऑक्साइड प्रचुर मात्रा में है।"),
363
- ("मानव शरीर में कुल कितनी हड्डियां होती हैं?", "एक वयस्क मानव शरीर में कुल 206 हड्डियां होती हैं।"),
364
- ("पौधे प्रकाश संश्लेषण में कौन सी गैस लेते हैं?", "पौधे प्रकाश संश्लेषण (Photosynthesis) की प्रक्रिया में कार्बन डाइऑक्साइड (CO2) ग्रहण करते हैं और ऑक्सीजन (O2) छोड़ते हैं।"),
365
- ("मनुष्य सांस लेने में कौन सी गैस उपयोग करते हैं?", "मनुष्य जीवित रहने के लिए सांस के रूप में ऑक्सीजन (O2) गैस ग्रहण करते हैं।"),
366
- ("भारत का राष्ट्रीय पशु कौन सा है?", "भारत का राष्ट्रीय पशु बाघ (Bengal Tiger) है।"),
367
- ("भारत का राष्ट्रीय पक्षी कौन सा है?", "भारत का राष्ट्रीय पक्षी मोर (Peacock) है।"),
368
- ("जल का रासायनिक सूत्र क्या है?", "जल (पानी) का रासायनिक सूत्र H2O है।"),
369
- ("दुनिया की सबसे ऊंची चोटी कौन सी है?", "दुनिया की सबसे ऊंची पर्वत चोटी माउंट एवरेस्ट है, जो नेपाल में स्थित है।"),
370
- ("स्वतंत्र भारत का संविधान कब लागू हुआ था?", "भारतीय संविधान 26 जनवरी 1950 को पूर्ण रूप से लागू हुआ था।"),
371
- ("राष्ट्रगान 'जन गण मन' किसने लिखा था?", "भारत का राष्ट्रगान 'जन गण मन' गुरुदेव रवींद्रनाथ टैगोर ने लिखा था।"),
372
- ("राष्ट्रगीत 'वंदे मातरम' के रचयिता कौन हैं?", "राष्ट्रगीत 'वंदे मातरम' के रचयिता बंकिम चंद्र ��ट्टोपाध्याय हैं।"),
373
- ("ताजमहल कहाँ स्थित है और किसने बनवाया था?", "ताजमहल उत्तर प्रदेश के आगरा शहर में यमुना नदी के किनारे स्थित है, जिसे मुगल सम्राट शाहजहाँ ने बनवाया था।"),
374
- ("गाय के दूध का हल्का पीला रंग किस कारण होता है?", "गाय के दूध का हल्का पीला रंग कैरोटीन (Carotene) की उपस्थिति के कारण होता है।")
375
- ]
376
- for q, a in ground_truth_hindi:
377
- if not is_duplicate(q):
378
- samples.append({"instruction": q, "response": a, "domain_id": 4})
379
-
380
- # 2. Real Indian History in Hindi (kaifahmad/indian-history-hindi-QA-3.4k)
381
- try:
382
- print(" 🌐 Streaming Indian History QA in Hindi (kaifahmad/indian-history-hindi-QA-3.4k)...")
383
- ds_hist = load_dataset("kaifahmad/indian-history-hindi-QA-3.4k", split="train", streaming=True)
384
- for row in ds_hist:
385
- q = (row.get("Question") or "").strip()
386
- ans = (row.get("Answer") or "").strip()
387
- if q and ans and len(q) > 8 and len(ans) > 10 and not is_duplicate(q) and not contains_math_or_code(ans):
388
- samples.append({"instruction": clean_text(q), "response": clean_text(ans), "domain_id": 4})
389
- if len(samples) >= 5000:
390
- break
391
- except Exception as e:
392
- print(f" ℹ️ Indian history QA stream warning: {e}")
393
-
394
- # 3. Clean Hindi Alpaca (Filtered for Knowledge, strictly avoiding math/code)
395
- try:
396
- print(" 🌐 Streaming clean Hindi knowledge from iamshnoo/alpaca-cleaned-hindi...")
397
- ds_hi = load_dataset("iamshnoo/alpaca-cleaned-hindi", split="train", streaming=True)
398
- for row in ds_hi:
399
- q = (row.get("instruction") or "").strip()
400
- inp = (row.get("input") or "").strip()
401
- ans = (row.get("output") or "").strip()
402
- if inp:
403
- q = f"{q}\n{inp}"
404
- if q and ans and not is_duplicate(q) and not contains_math_or_code(q) and not contains_math_or_code(ans):
405
- samples.append({"instruction": clean_text(q), "response": clean_text(ans), "domain_id": 4})
406
- if len(samples) >= target:
407
- break
408
- except Exception as e:
409
- print(f" ℹ️ Hindi Alpaca stream warning: {e}")
410
-
411
- # 4. Fallback from local d6_hindi_culture if needed
412
- local_hi = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d6_hindi_culture.jsonl")
413
- if os.path.exists(local_hi) and len(samples) < target:
414
- with open(local_hi, "r", encoding="utf-8") as f:
415
- for line in f:
416
- try:
417
- item = json.loads(line)
418
- except Exception:
419
- continue
420
- q = clean_text(item.get("instruction") or "")
421
- r = clean_text(item.get("response") or "")
422
- if q and r and not is_duplicate(q) and not contains_math_or_code(q) and not contains_math_or_code(r):
423
- samples.append({"instruction": q, "response": r, "domain_id": 4})
424
- if len(samples) >= target:
425
- break
426
-
427
- print(f" ✅ [D4: Hindi Factual QA] Loaded {len(samples):,} real Hindi QA & Knowledge samples.")
428
- return samples[:target]
429
-
430
- # --- D5: Practical Writing & Everyday Tasks ---
431
- def build_domain_practical_writing(target=30000):
432
- print(f"\n[D5: Writing] Sourcing practical writing, emails & summaries (Target: {target:,})...")
433
- samples = []
434
-
435
- # 1. Databricks Dolly 15k (human-written emails, summaries, brainstorming)
436
- try:
437
- print(" 🌐 Streaming Databricks Dolly-15k writing tasks...")
438
- ds_dolly = load_dataset("databricks/databricks-dolly-15k", split="train", streaming=True)
439
- for row in ds_dolly:
440
- cat = (row.get("category") or "").lower()
441
- if cat in ["creative_writing", "summarization", "open_qa", "brainstorming", "general_qa"]:
442
- q = (row.get("instruction") or "").strip()
443
- ctx = (row.get("context") or "").strip()
444
- ans = (row.get("response") or "").strip()
445
- if ctx:
446
- q = f"{q}\n\nContext:\n{ctx}"
447
- if q and ans and not is_duplicate(q) and not contains_math_or_code(ans):
448
- samples.append({"instruction": clean_text(q), "response": clean_text(ans), "domain_id": 5})
449
- if len(samples) >= 12000:
450
- break
451
- except Exception as e:
452
- print(f" ℹ️ Dolly-15k stream warning: {e}")
453
-
454
- # 2. Local D7 Summarization
455
- local_d7 = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d7_summarization_translation.jsonl")
456
- if os.path.exists(local_d7) and len(samples) < target:
457
- with open(local_d7, "r", encoding="utf-8") as f:
458
- for line in f:
459
- try:
460
- item = json.loads(line)
461
- except Exception:
462
- continue
463
- q = clean_text(item.get("instruction") or "")
464
- r = clean_text(item.get("response") or "")
465
- if "summar" in q.lower() or "dialogue" in q.lower():
466
- if q and r and not is_duplicate(q) and not contains_math_or_code(r):
467
- samples.append({"instruction": q, "response": r, "domain_id": 5})
468
- if len(samples) >= target:
469
- break
470
-
471
- # 3. UltraChat everyday assistance
472
- if len(samples) < target:
473
- try:
474
- ds = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft", streaming=True)
475
- for row in ds:
476
- msgs = row.get("messages", [])
477
- if len(msgs) >= 2:
478
- q = clean_text(msgs[0].get("content", ""))
479
- r = clean_text(msgs[1].get("content", ""))
480
- if ("email" in q.lower() or "letter" in q.lower() or "suggest" in q.lower() or "tips" in q.lower()):
481
- if q and r and not is_duplicate(q) and not contains_math_or_code(r):
482
- samples.append({"instruction": q, "response": r, "domain_id": 5})
483
- if len(samples) >= target:
484
- break
485
- except Exception as e:
486
- print(f" ℹ️ UltraChat writing stream warning: {e}")
487
-
488
- print(f" ✅ [D5: Writing] Loaded {len(samples):,} practical writing and summary samples.")
489
- return samples[:target]
490
-
491
- # --- D6: Culture, Recipes & Lifestyle ---
492
- def build_domain_recipes_lifestyle(target=25000):
493
- print(f"\n[D6: Recipes & Lifestyle] Sourcing Indian cooking, tea recipes & lifestyle tips (Target: {target:,})...")
494
- samples = []
495
-
496
- # Hand-curated authentic Chai & Indian food guides
497
- indian_culinary = [
498
- ("Masala Chai banane ki sabse best recipe step by step batao.",
499
- "**स्वादिष्ट कड़क मसाला चाय की प्रामाणिक रेसिपी:**\n\n"
500
- "**सामग्री (2 कप के लिए):**\n"
501
- "• पानी: 1.5 कप\n"
502
- "• ताजा दूध: 1 कप\n"
503
- "• चाय पत्ती: 2 छोटे चम्मच\n"
504
- "• चीनी: 2 छोटे चम्मच (स्वादानुसार)\n"
505
- "• ताजा अदरक: 1 इंच टुकड़ा (कुटा हुआ)\n"
506
- "• हरी इलायची: 2 (कुटी हुई)\n"
507
- "• लौंग: 1-2 (वैकल्पिक)\n"
508
- "• दालचीनी: छोटा सा टुकड़ा\n\n"
509
- "**बनाने की विधि:**\n"
510
- "1. एक पैन में 1.5 कप पानी गर्म करें। इसमें कुटा हुआ अदरक, इलायची और दालचीनी डालकर 2 मिनट तक उबालें ताकि मसालों का अर्क पानी में आ जाए।\n"
511
- "2. अब इसमें चाय पत्ती डालें और मध्यम आंच पर 1-2 मिनट खौलाएं।\n"
512
- "3. इसके बाद दूध और चीनी डालें। आंच तेज करें और चाय में पहला उबाल आने दें।\n"
513
- "4. उबाल आने के बाद आंच धीमी करें और चाय को चम्मच से चलाते हुए 2-3 मिनट तक अच्छे से पकने दें।\n"
514
- "5. जब सुंदर सुनहरा रंग आ जाए, तो गैस बंद करें और गरमा-गरम मसाला चाय छानकर परोसें।"),
515
- ("How to make traditional Indian Masala Chai?",
516
- "**Authentic Indian Masala Chai Recipe (Serves 2):**\n\n"
517
- "**Ingredients:**\n"
518
- "• Water: 1.5 cups\n"
519
- "• Whole milk: 1 cup\n"
520
- "• Strong black tea leaves: 2 tsp\n"
521
- "• Sugar: 2 tsp (to taste)\n"
522
- "• Fresh ginger: 1 inch (crushed)\n"
523
- "• Green cardamom pods: 2 (crushed)\n"
524
- "• Cinnamon stick: 1 small piece\n\n"
525
- "**Instructions:**\n"
526
- "1. Bring water to a simmer in a saucepan. Add crushed ginger, cardamom, and cinnamon. Boil for 2 minutes to extract flavors.\n"
527
- "2. Add black tea leaves and simmer for another minute.\n"
528
- "3. Pour in the milk and add sugar. Bring the mixture to a rolling boil.\n"
529
- "4. Reduce heat and let it simmer gently for 2-3 minutes until a rich caramel-brown color forms.\n"
530
- "5. Strain into cups and serve piping hot with biscuits or snacks."),
531
- ("What are healthy morning habits for high energy?",
532
- "**5 Simple Habits for High Morning Energy:**\n\n"
533
- "1. **Hydrate Immediately:** Drink 1-2 glasses of warm water right after waking up to activate metabolism.\n"
534
- "2. **Natural Sunlight Exposure:** Spend 10-15 minutes in morning sunlight to regulate your circadian rhythm and boost alertness.\n"
535
- "3. **Light Stretching or Yoga:** 10 minutes of Surya Namaskar or gentle stretching improves blood circulation.\n"
536
- "4. **Protein-Rich Breakfast:** Avoid high-sugar cereals; choose eggs, oats, sprouts, or paneer to avoid energy crashes.\n"
537
- "5. **Delay Screen Time:** Avoid checking emails or social media for the first 30 minutes to reduce morning stress.")
538
- ]
539
- for q, a in indian_culinary:
540
- if not is_duplicate(q):
541
- samples.append({"instruction": q, "response": a, "domain_id": 6})
542
-
543
- # Stream from Dolly & Alpaca (cooking, food, lifestyle categories)
544
- try:
545
- ds_dolly = load_dataset("databricks/databricks-dolly-15k", split="train", streaming=True)
546
- for row in ds_dolly:
547
- q = (row.get("instruction") or "").strip()
548
- ans = (row.get("response") or "").strip()
549
- lower = q.lower()
550
- if any(k in lower for k in ["recipe", "cook", "food", "health", "exercise", "habit", "tea", "coffee", "fruit"]):
551
- if q and ans and not is_duplicate(q) and not contains_math_or_code(ans):
552
- samples.append({"instruction": clean_text(q), "response": clean_text(ans), "domain_id": 6})
553
- if len(samples) >= 10000:
554
- break
555
- except Exception as e:
556
- print(f" ℹ️ Dolly lifestyle stream warning: {e}")
557
-
558
- # Fallback from d6_hindi_culture
559
- local_d6 = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d6_hindi_culture.jsonl")
560
- if not os.path.exists(local_d6) and len(samples) < target:
561
- try:
562
- print(f" 📥 Downloading d6_hindi_culture.jsonl from HF ({RAW_REPO_ID})...")
563
- os.makedirs(os.path.dirname(local_d6), exist_ok=True)
564
- dl = hf_hub_download(repo_id=RAW_REPO_ID, filename="d6_hindi_culture.jsonl", repo_type="dataset", token=HF_TOKEN)
565
- shutil.copy(dl, local_d6)
566
- except Exception as e:
567
- print(f" ℹ️ HF fetch d6 notice: {e}")
568
-
569
- raw_items = load_json_or_jsonl(local_d6)
570
- for item in raw_items:
571
- q = clean_text(item.get("instruction") or "")
572
- r = clean_text(item.get("response") or "")
573
- if q and r and not is_duplicate(q) and not contains_math_or_code(q) and not contains_math_or_code(r):
574
- samples.append({"instruction": q, "response": r, "domain_id": 6})
575
- if len(samples) >= target:
576
- break
577
-
578
- print(f" ✅ [D6: Recipes & Lifestyle] Loaded {len(samples):,} lifestyle and recipe samples.")
579
- return samples[:target]
580
-
581
- # --- D7: Bilingual Translation (EN <-> HI) ---
582
- def build_domain_translation(target=25000):
583
- print(f"\n[D7: Translation] Sourcing clean English <-> Hindi sentence translation (Target: {target:,})...")
584
- samples = []
585
-
586
- # 1. Local pre-filtered translation
587
- local_d7 = os.path.join(BASE_DIR, "data", "sarus_sft_raw", "d7_summarization_translation.jsonl")
588
- if os.path.exists(local_d7):
589
- with open(local_d7, "r", encoding="utf-8") as f:
590
- for line in f:
591
- try:
592
- item = json.loads(line)
593
- except Exception:
594
- continue
595
- q = clean_text(item.get("instruction") or "")
596
- r = clean_text(item.get("response") or "")
597
- if "translate" in q.lower() or "अनुवाद" in q:
598
- if q and r and not is_duplicate(q) and not contains_math_or_code(r):
599
- samples.append({"instruction": q, "response": r, "domain_id": 7})
600
- if len(samples) >= target:
601
- break
602
-
603
- # 2. IITB English-Hindi stream
604
- if len(samples) < target:
605
- try:
606
- print(" 🌐 Streaming authentic translation pairs from cfilt/iitb-english-hindi...")
607
- ds = load_dataset("cfilt/iitb-english-hindi", split="train", streaming=True)
608
- for row in ds:
609
- trans = row.get("translation", {})
610
- en = (trans.get("en") or "").strip()
611
- hi = (trans.get("hi") or "").strip()
612
- if en and hi and 15 < len(en) < 200 and 15 < len(hi) < 200:
613
- if not contains_math_or_code(en) and not contains_math_or_code(hi):
614
- # Add EN -> HI
615
- q_en = f"Translate to Hindi: '{en}'"
616
- if not is_duplicate(q_en):
617
- samples.append({"instruction": q_en, "response": hi, "domain_id": 7})
618
- # Add HI -> EN
619
- q_hi = f"Translate to English: '{hi}'"
620
- if not is_duplicate(q_hi):
621
- samples.append({"instruction": q_hi, "response": en, "domain_id": 7})
622
- if len(samples) >= target:
623
- break
624
- except Exception as e:
625
- print(f" ℹ️ IITB stream warning: {e}")
626
-
627
- print(f" ✅ [D7: Translation] Loaded {len(samples):,} translation pairs.")
628
- return samples[:target]
629
-
630
-
631
- # ------------------------------------------------------------------------------
632
- # 4. Phase 1: Build & Export Raw JSONL
633
- # ------------------------------------------------------------------------------
634
- def run_phase_raw(upload_hf=False):
635
- print("\n" + "=" * 85)
636
- print("🚀 Commencing SFT v2 Pure Chat Dataset Creation across all 8 Domains...")
637
- print("=" * 85)
638
-
639
- domain_builders = [
640
- (0, build_domain_identity),
641
- (1, build_domain_greetings),
642
- (2, build_domain_dialogue),
643
- (3, build_domain_gk_science),
644
- (4, build_domain_hindi_qa),
645
- (5, build_domain_practical_writing),
646
- (6, build_domain_recipes_lifestyle),
647
- (7, build_domain_translation),
648
- ]
649
-
650
- all_samples = []
651
- domain_counts = {}
652
-
653
- for did, builder in domain_builders:
654
- tgt = DOMAIN_TARGETS[did]
655
- d_name = DOMAIN_NAMES[did]
656
- d_samples = builder(tgt)
657
- domain_counts[d_name] = len(d_samples)
658
- all_samples.extend(d_samples)
659
-
660
- raw_file = os.path.join(RAW_EXPORT_DIR, f"d{did}_{d_name}.jsonl")
661
- with open(raw_file, "w", encoding="utf-8") as f:
662
- for item in d_samples:
663
- f.write(json.dumps(item, ensure_ascii=False) + "\n")
664
- print(f" 📁 Saved raw JSONL -> {raw_file} ({len(d_samples):,} rows)")
665
-
666
- total_samples = len(all_samples)
667
- print("\n" + "=" * 85)
668
- print(f"🏆 SFT v2 Raw Dataset Assembly Completed: {total_samples:,} High-Purity Chat Samples!")
669
- print("=" * 85)
670
-
671
- # Fail-fast validation: identity + greetings are non-negotiable (persona grounding),
672
- # and streaming failures must never silently produce a tiny/empty dataset.
673
- for _did, _need in [(0, 500), (1, 500)]:
674
- _got = domain_counts.get(DOMAIN_NAMES[_did], 0)
675
- if _got < _need:
676
- raise RuntimeError(
677
- f"[D{_did}: {DOMAIN_NAMES[_did]}] only {_got:,} samples (need >={_need:,}). "
678
- f"Check local file data/sarus_sft_raw/d{_did}_*.jsonl and HF_TOKEN/network access."
679
- )
680
- if total_samples < 50000:
681
- raise RuntimeError(
682
- f"Total {total_samples:,} samples is far below the ~202k target. "
683
- f"Counts: {domain_counts}. A streaming source probably failed — inspect warnings above."
684
- )
685
-
686
- # Optional HF Upload
687
- if upload_hf:
688
- if not HF_TOKEN:
689
- raise ValueError("HF_TOKEN env var is not set! Export HF_TOKEN before --upload_hf.")
690
- print(f"\n🌐 Uploading raw data to Hugging Face ({RAW_REPO_ID}/sft_v2)...")
691
- api = HfApi(token=HF_TOKEN)
692
- try:
693
- api.upload_folder(
694
- folder_path=RAW_EXPORT_DIR,
695
- repo_id=RAW_REPO_ID,
696
- path_in_repo="sft_v2_chat",
697
- repo_type="dataset",
698
- commit_message=f"Upload Sarus SFT v2 Pure Chat Raw Corpus ({total_samples:,} samples)"
699
- )
700
- print("✅ Successfully uploaded raw JSONL to Hugging Face!")
701
- except Exception as e:
702
- print(f"⚠️ Upload warning: {e}")
703
-
704
- return all_samples
705
-
706
- # ------------------------------------------------------------------------------
707
- # 5. Phase 2: Tokenize & Pack NumPy Shards
708
- # ------------------------------------------------------------------------------
709
- def run_phase_tokenize(all_samples=None, upload_hf=False):
710
- global TOKENIZER
711
- if TOKENIZER is None:
712
- TOKENIZER = load_fast_tokenizer()
713
-
714
- if all_samples is None:
715
- print("\n📥 Loading raw JSONL files from disk for tokenization...")
716
- all_samples = []
717
- for did in sorted(DOMAIN_NAMES.keys()):
718
- d_name = DOMAIN_NAMES[did]
719
- fpath = os.path.join(RAW_EXPORT_DIR, f"d{did}_{d_name}.jsonl")
720
- if os.path.exists(fpath):
721
- with open(fpath, "r", encoding="utf-8") as f:
722
- for line in f:
723
- if line.strip():
724
- all_samples.append(json.loads(line))
725
- print(f" ✅ Loaded {len(all_samples):,} raw samples.")
726
-
727
- total_samples = len(all_samples)
728
- if total_samples == 0:
729
- print("❌ Error: No samples to tokenize!")
730
- return
731
-
732
- print("\n⚡ [PHASE 2] Tokenizing into Memory-Mapped NumPy Shards for GPU Training...")
733
- random.seed(42)
734
- random.shuffle(all_samples)
735
-
736
- train_tokens = []
737
- train_labels = []
738
- train_offsets = [0]
739
- train_domains = []
740
-
741
- val_tokens = []
742
- val_labels = []
743
- val_offsets = [0]
744
- val_domains = []
745
-
746
- val_split_count = int(total_samples * VAL_SPLIT_RATIO)
747
- print(f" • Train Split: {total_samples - val_split_count:,} samples | Val Split: {val_split_count:,} samples")
748
-
749
- domain_counts = {}
750
- for did in DOMAIN_NAMES.keys():
751
- domain_counts[DOMAIN_NAMES[did]] = sum(1 for x in all_samples if x.get("domain_id") == did)
752
-
753
- for i, item in enumerate(all_samples):
754
- inst = item["instruction"]
755
- resp = item["response"]
756
- did = item["domain_id"]
757
-
758
- prompt_str = f"{USER_TOKEN}\n{inst}{EOT_TOKEN}\n{ASSISTANT_TOKEN}\n"
759
- resp_str = f"{resp}{EOT_TOKEN}"
760
-
761
- p_ids = TOKENIZER.encode(prompt_str).ids
762
- r_ids = TOKENIZER.encode(resp_str).ids
763
-
764
- if len(r_ids) < MIN_TOKENS:
765
- continue
766
-
767
- sample_tokens = p_ids + r_ids
768
- sample_labels = [-100] * len(p_ids) + r_ids
769
-
770
- if len(sample_tokens) > MAX_SEQ_LEN:
771
- sample_tokens = sample_tokens[:MAX_SEQ_LEN]
772
- sample_labels = sample_labels[:MAX_SEQ_LEN]
773
- sample_tokens[-1] = EOT_ID
774
- sample_labels[-1] = EOT_ID
775
-
776
- is_val = (i < val_split_count)
777
- if is_val:
778
- val_tokens.extend(sample_tokens)
779
- val_labels.extend(sample_labels)
780
- val_offsets.append(len(val_tokens))
781
- val_domains.append(did)
782
- else:
783
- train_tokens.extend(sample_tokens)
784
- train_labels.extend(sample_labels)
785
- train_offsets.append(len(train_tokens))
786
- train_domains.append(did)
787
-
788
- if (i + 1) % 50000 == 0 or (i + 1) == total_samples:
789
- print(f" • Tokenized {i + 1:,} / {total_samples:,} samples...")
790
-
791
- # Export NumPy Arrays
792
- print("\n📦 Writing memory-mapped NumPy shards...")
793
- np.save(os.path.join(TOKENIZED_DIR, "train_tokens.npy"), np.array(train_tokens, dtype=np.int32))
794
- np.save(os.path.join(TOKENIZED_DIR, "train_labels.npy"), np.array(train_labels, dtype=np.int32))
795
- np.save(os.path.join(TOKENIZED_DIR, "train_offsets.npy"), np.array(train_offsets, dtype=np.int64))
796
- np.save(os.path.join(TOKENIZED_DIR, "train_domains.npy"), np.array(train_domains, dtype=np.uint8))
797
-
798
- np.save(os.path.join(TOKENIZED_DIR, "val_tokens.npy"), np.array(val_tokens, dtype=np.int32))
799
- np.save(os.path.join(TOKENIZED_DIR, "val_labels.npy"), np.array(val_labels, dtype=np.int32))
800
- np.save(os.path.join(TOKENIZED_DIR, "val_offsets.npy"), np.array(val_offsets, dtype=np.int64))
801
- np.save(os.path.join(TOKENIZED_DIR, "val_domains.npy"), np.array(val_domains, dtype=np.uint8))
802
-
803
- meta_payload = {
804
- "version": "v2",
805
- "model_name": "Sarus",
806
- "developer": "ViuAI",
807
- "dataset_type": "pure_chat",
808
- "total_samples": total_samples,
809
- "train_samples": len(train_offsets) - 1,
810
- "val_samples": len(val_offsets) - 1,
811
- "train_tokens_total": len(train_tokens),
812
- "val_tokens_total": len(val_tokens),
813
- "max_seq_len": MAX_SEQ_LEN,
814
- "domain_names": DOMAIN_NAMES,
815
- "domain_sample_counts": domain_counts
816
- }
817
-
818
- with open(os.path.join(TOKENIZED_DIR, "metadata.json"), "w", encoding="utf-8") as f:
819
- json.dump(meta_payload, f, indent=2, ensure_ascii=False)
820
-
821
- print(f"✅ Saved metadata.json with {len(DOMAIN_NAMES)} domains and stats!")
822
-
823
- if upload_hf:
824
- if not HF_TOKEN:
825
- raise ValueError("HF_TOKEN env var is not set! Export HF_TOKEN before --upload_hf.")
826
- print(f"\n🌐 Uploading tokenized shards to Hugging Face ({TOKENIZED_REPO_ID}/sft_v2)...")
827
- api = HfApi(token=HF_TOKEN)
828
- try:
829
- api.upload_folder(
830
- folder_path=TOKENIZED_DIR,
831
- repo_id=TOKENIZED_REPO_ID,
832
- path_in_repo="sft_v2",
833
- repo_type="dataset",
834
- commit_message=f"Upload Sarus SFT v2 Pure Chat Tokenized Shards ({total_samples:,} samples)"
835
- )
836
- print("✅ Successfully uploaded tokenized dataset to Hugging Face!")
837
- except Exception as e:
838
- print(f"⚠️ Upload warning: {e}")
839
-
840
- print("\n" + "=" * 85)
841
- print("🎉 [PHASE 2 COMPLETE] TOKENIZED SHARDS READY FOR HIGH-SPEED TRAINING!")
842
- print(f" • Train Tokens: {len(train_tokens):,}")
843
- print(f" • Val Tokens: {len(val_tokens):,}")
844
- print(f" • Shard Directory: {TOKENIZED_DIR}")
845
- print("=" * 85)
846
-
847
- # ------------------------------------------------------------------------------
848
- # Main CLI
849
- # ------------------------------------------------------------------------------
850
- if __name__ == "__main__":
851
- parser = argparse.ArgumentParser(description="ViuAI Sarus-500M SFT v2 Pure Chat Dataset Builder")
852
- parser.add_argument("--phase", type=str, default="all", choices=["raw", "tokenize", "all"], help="Execution phase")
853
- parser.add_argument("--upload_hf", action="store_true", default=False, help="Upload outputs to Hugging Face")
854
- args = parser.parse_args()
855
-
856
- if args.phase in ["raw", "all"]:
857
- samples = run_phase_raw(upload_hf=args.upload_hf)
858
- if args.phase == "all":
859
- run_phase_tokenize(all_samples=samples, upload_hf=args.upload_hf)
860
- elif args.phase == "tokenize":
861
- run_phase_tokenize(all_samples=None, upload_hf=args.upload_hf)