BHARGAV REDDY commited on
Commit
20bb6fd
Β·
verified Β·
1 Parent(s): 23b3bd1

Upload Base/scripts/build_english_corpus.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. Base/scripts/build_english_corpus.py +349 -349
Base/scripts/build_english_corpus.py CHANGED
@@ -1,349 +1,349 @@
1
- """
2
- Build an ultra-clean English text corpus for continued pretraining.
3
-
4
- Downloads from high-quality, professionally curated sources via HuggingFace
5
- streaming (constant memory, no full dataset download needed):
6
-
7
- 1. Wikipedia English β€” encyclopedic, multi-editor-reviewed text
8
- (the single cleanest large-scale English corpus in existence)
9
- 2. FineWeb-Edu β€” top-scored educational web content (score >= 4.0)
10
- (adds diversity: textbooks, tutorials, explanations, articles)
11
-
12
- Quality filters applied to every text:
13
- - Minimum length and word count
14
- - High alphabetic ratio (rejects tables, code, symbol dumps)
15
- - Sentence structure check (real prose, not lists/fragments)
16
- - Repetition detection (rejects copy-paste / boilerplate)
17
- - URL density filter (rejects link farms)
18
- - Unicode normalization and whitespace cleanup
19
-
20
- Outputs parquet files to Base/data/filtered_english/ with a single 'text'
21
- column, compatible with the existing prepare_litdata.py pipeline.
22
-
23
- After running this script:
24
- 1. python Base/scripts/prepare_litdata.py --mode english
25
- 2. litgpt pretrain --config Base/configs/pretrain_100m_english.yaml
26
-
27
- Usage:
28
- python Base/scripts/build_english_corpus.py
29
- python Base/scripts/build_english_corpus.py --target_tokens 100000000
30
- python Base/scripts/build_english_corpus.py --sources wiki
31
- python Base/scripts/build_english_corpus.py --sources fineweb
32
- python Base/scripts/build_english_corpus.py --fineweb_min_score 4.5
33
- """
34
-
35
- import argparse
36
- import os
37
- import re
38
- import time
39
- import unicodedata
40
- from pathlib import Path
41
-
42
- import pyarrow as pa
43
- import pyarrow.parquet as pq
44
-
45
-
46
- # ─── Text Quality Filters ──────────────────────────────────────────────────
47
-
48
- def clean_text(text: str) -> str:
49
- """Normalize Unicode, strip control chars, fix whitespace."""
50
- # Normalize unicode (NFKC merges compatibility chars)
51
- text = unicodedata.normalize("NFKC", text)
52
- # Remove control characters (keep newlines and tabs)
53
- text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text)
54
- # Collapse 3+ blank lines into 2
55
- text = re.sub(r'\n{3,}', '\n\n', text)
56
- # Collapse multiple spaces/tabs into single space
57
- text = re.sub(r'[ \t]+', ' ', text)
58
- # Strip each line
59
- text = '\n'.join(line.strip() for line in text.split('\n'))
60
- return text.strip()
61
-
62
-
63
- def is_high_quality(text: str, min_chars: int = 500, min_words: int = 80) -> bool:
64
- """Strict quality gate β€” only passes clean, well-formed English prose."""
65
- if len(text) < min_chars:
66
- return False
67
-
68
- words = text.split()
69
- num_words = len(words)
70
- if num_words < min_words:
71
- return False
72
-
73
- # Must be mostly alphabetic (not tables, code, numbers)
74
- alpha = sum(c.isalpha() for c in text)
75
- if alpha / max(len(text), 1) < 0.65:
76
- return False
77
-
78
- # Average word length sanity check (2.5–15 chars for English)
79
- avg_word_len = sum(len(w) for w in words) / num_words
80
- if avg_word_len < 2.5 or avg_word_len > 15:
81
- return False
82
-
83
- # Not too many URLs (< 3% of words)
84
- url_hits = text.count('http://') + text.count('https://')
85
- if url_hits > num_words * 0.03:
86
- return False
87
-
88
- # Must contain real sentences (at least 3 sentences > 10 chars)
89
- sentences = re.split(r'[.!?]+', text)
90
- real_sentences = [s.strip() for s in sentences if len(s.strip()) > 10]
91
- if len(real_sentences) < 3:
92
- return False
93
-
94
- # Repetition filter β€” unique lines should be > 50%
95
- lines = [ln.strip() for ln in text.split('\n') if ln.strip()]
96
- if len(lines) > 5:
97
- unique_ratio = len(set(lines)) / len(lines)
98
- if unique_ratio < 0.5:
99
- return False
100
-
101
- return True
102
-
103
-
104
- # ─── Wikipedia Source ───────────────────────────────────────────────────────
105
-
106
- # Wikipedia articles with these title patterns are low-value for language model
107
- _WIKI_SKIP_PATTERNS = re.compile(
108
- r'(disambiguation|list of|lists of|index of|outline of|'
109
- r'wikipedia:|template:|category:|portal:|module:|mediawiki:)',
110
- re.IGNORECASE
111
- )
112
-
113
-
114
- def fetch_wikipedia(target_tokens: int, output_dir: str, tokens_per_word: float = 1.3):
115
- """Stream Wikipedia English, apply strict quality filters, output parquet."""
116
- from datasets import load_dataset
117
-
118
- print(f"\n{'='*60}")
119
- print(f"[Wikipedia] Streaming English articles")
120
- print(f" Target: {target_tokens:,} tokens")
121
- print(f" Output: {output_dir}")
122
- print(f"{'='*60}\n")
123
-
124
- os.makedirs(output_dir, exist_ok=True)
125
-
126
- ds = load_dataset(
127
- "wikimedia/wikipedia", "20231101.en",
128
- split="train", streaming=True,
129
- trust_remote_code=False
130
- )
131
-
132
- buf = []
133
- total_tokens = 0
134
- kept = 0
135
- skipped = 0
136
- file_idx = 0
137
- BATCH = 5000
138
- t0 = time.time()
139
-
140
- for article in ds:
141
- title = (article.get("title") or "").strip()
142
- raw = article.get("text") or ""
143
-
144
- # Skip meta / navigation articles
145
- if _WIKI_SKIP_PATTERNS.search(title):
146
- skipped += 1
147
- continue
148
-
149
- cleaned = clean_text(raw)
150
-
151
- # Strict filter: min 800 chars, 120 words for Wikipedia
152
- if not is_high_quality(cleaned, min_chars=800, min_words=120):
153
- skipped += 1
154
- continue
155
-
156
- # Prepend title as natural heading
157
- full_text = f"{title}\n\n{cleaned}"
158
- est_tok = int(len(full_text.split()) * tokens_per_word)
159
-
160
- buf.append(full_text)
161
- total_tokens += est_tok
162
- kept += 1
163
-
164
- # Flush batch to parquet
165
- if len(buf) >= BATCH:
166
- fp = Path(output_dir) / f"wiki_{file_idx:04d}.parquet"
167
- pq.write_table(pa.table({"text": buf}), str(fp))
168
- elapsed = time.time() - t0
169
- rate = total_tokens / max(elapsed, 1)
170
- print(f" wiki_{file_idx:04d}.parquet | {kept:,} articles | "
171
- f"~{total_tokens:,} tok | {rate:,.0f} tok/s | {elapsed:.0f}s")
172
- buf = []
173
- file_idx += 1
174
-
175
- if total_tokens >= target_tokens:
176
- break
177
-
178
- # Write remaining
179
- if buf:
180
- fp = Path(output_dir) / f"wiki_{file_idx:04d}.parquet"
181
- pq.write_table(pa.table({"text": buf}), str(fp))
182
- file_idx += 1
183
-
184
- elapsed = time.time() - t0
185
- print(f"\n--- Wikipedia complete ---")
186
- print(f" Kept: {kept:,} articles | Skipped: {skipped:,}")
187
- print(f" Est. tokens: {total_tokens:,}")
188
- print(f" Files: {file_idx} | Time: {elapsed:.0f}s")
189
- return total_tokens
190
-
191
-
192
- # ─── FineWeb-Edu Source ─────────────────────────────────────────────────────
193
-
194
- def fetch_fineweb_edu(target_tokens: int, output_dir: str,
195
- min_score: float = 4.0, tokens_per_word: float = 1.3):
196
- """Stream FineWeb-Edu (top educational web content), output parquet."""
197
- from datasets import load_dataset
198
-
199
- print(f"\n{'='*60}")
200
- print(f"[FineWeb-Edu] Streaming (score >= {min_score})")
201
- print(f" Target: {target_tokens:,} tokens")
202
- print(f" Output: {output_dir}")
203
- print(f"{'='*60}\n")
204
-
205
- os.makedirs(output_dir, exist_ok=True)
206
-
207
- ds = load_dataset(
208
- "HuggingFaceFW/fineweb-edu", "sample-10BT",
209
- split="train", streaming=True,
210
- trust_remote_code=False
211
- )
212
-
213
- buf = []
214
- total_tokens = 0
215
- kept = 0
216
- skipped = 0
217
- file_idx = 0
218
- BATCH = 5000
219
- t0 = time.time()
220
-
221
- for doc in ds:
222
- # FineWeb-Edu has 'score' (float) β€” educational quality 0-5
223
- score = doc.get("score", 0)
224
- if not isinstance(score, (int, float)):
225
- try:
226
- score = float(score)
227
- except (ValueError, TypeError):
228
- skipped += 1
229
- continue
230
-
231
- if score < min_score:
232
- skipped += 1
233
- continue
234
-
235
- raw = doc.get("text") or ""
236
- cleaned = clean_text(raw)
237
-
238
- if not is_high_quality(cleaned, min_chars=500, min_words=80):
239
- skipped += 1
240
- continue
241
-
242
- est_tok = int(len(cleaned.split()) * tokens_per_word)
243
-
244
- buf.append(cleaned)
245
- total_tokens += est_tok
246
- kept += 1
247
-
248
- if len(buf) >= BATCH:
249
- fp = Path(output_dir) / f"fineweb_{file_idx:04d}.parquet"
250
- pq.write_table(pa.table({"text": buf}), str(fp))
251
- elapsed = time.time() - t0
252
- rate = total_tokens / max(elapsed, 1)
253
- print(f" fineweb_{file_idx:04d}.parquet | {kept:,} docs | "
254
- f"~{total_tokens:,} tok | {rate:,.0f} tok/s | {elapsed:.0f}s")
255
- buf = []
256
- file_idx += 1
257
-
258
- if total_tokens >= target_tokens:
259
- break
260
-
261
- if buf:
262
- fp = Path(output_dir) / f"fineweb_{file_idx:04d}.parquet"
263
- pq.write_table(pa.table({"text": buf}), str(fp))
264
- file_idx += 1
265
-
266
- elapsed = time.time() - t0
267
- print(f"\n--- FineWeb-Edu complete ---")
268
- print(f" Kept: {kept:,} docs | Skipped: {skipped:,}")
269
- print(f" Est. tokens: {total_tokens:,}")
270
- print(f" Files: {file_idx} | Time: {elapsed:.0f}s")
271
- return total_tokens
272
-
273
-
274
- # ─── Main ───────────────────────────────────────────────────────────────────
275
-
276
- def main():
277
- parser = argparse.ArgumentParser(
278
- description="Build ultra-clean English corpus for continued pretraining"
279
- )
280
- parser.add_argument(
281
- "--target_tokens", type=int, default=50_000_000,
282
- help="Total target token count (default: 50,000,000 = 50M)"
283
- )
284
- parser.add_argument(
285
- "--sources", type=str, default="wiki,fineweb",
286
- help="Comma-separated sources: wiki, fineweb (default: wiki,fineweb)"
287
- )
288
- parser.add_argument(
289
- "--wiki_share", type=float, default=0.6,
290
- help="Wikipedia share when both sources are used (default: 0.6 = 60%%)"
291
- )
292
- parser.add_argument(
293
- "--fineweb_min_score", type=float, default=4.0,
294
- help="Minimum FineWeb-Edu educational score 0-5 (default: 4.0)"
295
- )
296
- parser.add_argument(
297
- "--output_dir", type=str, default="Base/data/filtered_english",
298
- help="Output directory for parquet files"
299
- )
300
- args = parser.parse_args()
301
-
302
- sources = [s.strip().lower() for s in args.sources.split(",")]
303
-
304
- print(f"\n{'#'*60}")
305
- print(f" ULTRA-CLEAN ENGLISH CORPUS BUILDER")
306
- print(f" Target: {args.target_tokens:,} tokens")
307
- print(f" Sources: {', '.join(sources)}")
308
- print(f" Output: {args.output_dir}")
309
- print(f"{'#'*60}")
310
-
311
- # Check datasets library
312
- try:
313
- import datasets
314
- print(f" datasets v{datasets.__version__}")
315
- except ImportError:
316
- print("\n ERROR: 'datasets' library is required for streaming.")
317
- print(" Install it: pip install datasets")
318
- return
319
-
320
- total = 0
321
-
322
- if "wiki" in sources:
323
- if "fineweb" in sources:
324
- wiki_target = int(args.target_tokens * args.wiki_share)
325
- else:
326
- wiki_target = args.target_tokens
327
- total += fetch_wikipedia(wiki_target, args.output_dir)
328
-
329
- if "fineweb" in sources:
330
- fw_target = args.target_tokens - total if total > 0 else args.target_tokens
331
- if fw_target > 0:
332
- total += fetch_fineweb_edu(
333
- fw_target, args.output_dir,
334
- min_score=args.fineweb_min_score
335
- )
336
-
337
- print(f"\n{'#'*60}")
338
- print(f" CORPUS BUILD COMPLETE")
339
- print(f" Total estimated tokens: {total:,}")
340
- print(f" Output directory: {args.output_dir}")
341
- print(f"{'#'*60}")
342
- print(f"\nNext steps:")
343
- print(f" 1. python Base/scripts/prepare_litdata.py --mode english")
344
- print(f" 2. $env:TORCHDYNAMO_DISABLE = '1'")
345
- print(f" 3. litgpt pretrain --config Base/configs/pretrain_100m_english.yaml")
346
-
347
-
348
- if __name__ == "__main__":
349
- main()
 
1
+ """
2
+ Build an ultra-clean English text corpus for continued pretraining.
3
+
4
+ Downloads from high-quality, professionally curated sources via HuggingFace
5
+ streaming (constant memory, no full dataset download needed):
6
+
7
+ 1. Wikipedia English β€” encyclopedic, multi-editor-reviewed text
8
+ (the single cleanest large-scale English corpus in existence)
9
+ 2. FineWeb-Edu β€” top-scored educational web content (score >= 4.0)
10
+ (adds diversity: textbooks, tutorials, explanations, articles)
11
+
12
+ Quality filters applied to every text:
13
+ - Minimum length and word count
14
+ - High alphabetic ratio (rejects tables, code, symbol dumps)
15
+ - Sentence structure check (real prose, not lists/fragments)
16
+ - Repetition detection (rejects copy-paste / boilerplate)
17
+ - URL density filter (rejects link farms)
18
+ - Unicode normalization and whitespace cleanup
19
+
20
+ Outputs parquet files to Base/data/filtered_english/ with a single 'text'
21
+ column, compatible with the existing prepare_litdata.py pipeline.
22
+
23
+ After running this script:
24
+ 1. python Base/scripts/prepare_litdata.py --mode english
25
+ 2. litgpt pretrain --config Base/configs/pretrain_100m_english.yaml
26
+
27
+ Usage:
28
+ python Base/scripts/build_english_corpus.py
29
+ python Base/scripts/build_english_corpus.py --target_tokens 100000000
30
+ python Base/scripts/build_english_corpus.py --sources wiki
31
+ python Base/scripts/build_english_corpus.py --sources fineweb
32
+ python Base/scripts/build_english_corpus.py --fineweb_min_score 4.5
33
+ """
34
+
35
+ import argparse
36
+ import os
37
+ import re
38
+ import time
39
+ import unicodedata
40
+ from pathlib import Path
41
+
42
+ import pyarrow as pa
43
+ import pyarrow.parquet as pq
44
+
45
+
46
+ # ─── Text Quality Filters ──────────────────────────────────────────────────
47
+
48
+ def clean_text(text: str) -> str:
49
+ """Normalize Unicode, strip control chars, fix whitespace."""
50
+ # Normalize unicode (NFKC merges compatibility chars)
51
+ text = unicodedata.normalize("NFKC", text)
52
+ # Remove control characters (keep newlines and tabs)
53
+ text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text)
54
+ # Collapse 3+ blank lines into 2
55
+ text = re.sub(r'\n{3,}', '\n\n', text)
56
+ # Collapse multiple spaces/tabs into single space
57
+ text = re.sub(r'[ \t]+', ' ', text)
58
+ # Strip each line
59
+ text = '\n'.join(line.strip() for line in text.split('\n'))
60
+ return text.strip()
61
+
62
+
63
+ def is_high_quality(text: str, min_chars: int = 500, min_words: int = 80) -> bool:
64
+ """Strict quality gate β€” only passes clean, well-formed English prose."""
65
+ if len(text) < min_chars:
66
+ return False
67
+
68
+ words = text.split()
69
+ num_words = len(words)
70
+ if num_words < min_words:
71
+ return False
72
+
73
+ # Must be mostly alphabetic (not tables, code, numbers)
74
+ alpha = sum(c.isalpha() for c in text)
75
+ if alpha / max(len(text), 1) < 0.65:
76
+ return False
77
+
78
+ # Average word length sanity check (2.5–15 chars for English)
79
+ avg_word_len = sum(len(w) for w in words) / num_words
80
+ if avg_word_len < 2.5 or avg_word_len > 15:
81
+ return False
82
+
83
+ # Not too many URLs (< 3% of words)
84
+ url_hits = text.count('http://') + text.count('https://')
85
+ if url_hits > num_words * 0.03:
86
+ return False
87
+
88
+ # Must contain real sentences (at least 3 sentences > 10 chars)
89
+ sentences = re.split(r'[.!?]+', text)
90
+ real_sentences = [s.strip() for s in sentences if len(s.strip()) > 10]
91
+ if len(real_sentences) < 3:
92
+ return False
93
+
94
+ # Repetition filter β€” unique lines should be > 50%
95
+ lines = [ln.strip() for ln in text.split('\n') if ln.strip()]
96
+ if len(lines) > 5:
97
+ unique_ratio = len(set(lines)) / len(lines)
98
+ if unique_ratio < 0.5:
99
+ return False
100
+
101
+ return True
102
+
103
+
104
+ # ─── Wikipedia Source ───────────────────────────────────────────────────────
105
+
106
+ # Wikipedia articles with these title patterns are low-value for language model
107
+ _WIKI_SKIP_PATTERNS = re.compile(
108
+ r'(disambiguation|list of|lists of|index of|outline of|'
109
+ r'wikipedia:|template:|category:|portal:|module:|mediawiki:)',
110
+ re.IGNORECASE
111
+ )
112
+
113
+
114
+ def fetch_wikipedia(target_tokens: int, output_dir: str, tokens_per_word: float = 1.3):
115
+ """Stream Wikipedia English, apply strict quality filters, output parquet."""
116
+ from datasets import load_dataset
117
+
118
+ print(f"\n{'='*60}")
119
+ print(f"[Wikipedia] Streaming English articles")
120
+ print(f" Target: {target_tokens:,} tokens")
121
+ print(f" Output: {output_dir}")
122
+ print(f"{'='*60}\n")
123
+
124
+ os.makedirs(output_dir, exist_ok=True)
125
+
126
+ ds = load_dataset(
127
+ "wikimedia/wikipedia", "20231101.en",
128
+ split="train", streaming=True,
129
+ trust_remote_code=False
130
+ )
131
+
132
+ buf = []
133
+ total_tokens = 0
134
+ kept = 0
135
+ skipped = 0
136
+ file_idx = 0
137
+ BATCH = 5000
138
+ t0 = time.time()
139
+
140
+ for article in ds:
141
+ title = (article.get("title") or "").strip()
142
+ raw = article.get("text") or ""
143
+
144
+ # Skip meta / navigation articles
145
+ if _WIKI_SKIP_PATTERNS.search(title):
146
+ skipped += 1
147
+ continue
148
+
149
+ cleaned = clean_text(raw)
150
+
151
+ # Strict filter: min 800 chars, 120 words for Wikipedia
152
+ if not is_high_quality(cleaned, min_chars=800, min_words=120):
153
+ skipped += 1
154
+ continue
155
+
156
+ # Prepend title as natural heading
157
+ full_text = f"{title}\n\n{cleaned}"
158
+ est_tok = int(len(full_text.split()) * tokens_per_word)
159
+
160
+ buf.append(full_text)
161
+ total_tokens += est_tok
162
+ kept += 1
163
+
164
+ # Flush batch to parquet
165
+ if len(buf) >= BATCH:
166
+ fp = Path(output_dir) / f"wiki_{file_idx:04d}.parquet"
167
+ pq.write_table(pa.table({"text": buf}), str(fp))
168
+ elapsed = time.time() - t0
169
+ rate = total_tokens / max(elapsed, 1)
170
+ print(f" wiki_{file_idx:04d}.parquet | {kept:,} articles | "
171
+ f"~{total_tokens:,} tok | {rate:,.0f} tok/s | {elapsed:.0f}s")
172
+ buf = []
173
+ file_idx += 1
174
+
175
+ if total_tokens >= target_tokens:
176
+ break
177
+
178
+ # Write remaining
179
+ if buf:
180
+ fp = Path(output_dir) / f"wiki_{file_idx:04d}.parquet"
181
+ pq.write_table(pa.table({"text": buf}), str(fp))
182
+ file_idx += 1
183
+
184
+ elapsed = time.time() - t0
185
+ print(f"\n--- Wikipedia complete ---")
186
+ print(f" Kept: {kept:,} articles | Skipped: {skipped:,}")
187
+ print(f" Est. tokens: {total_tokens:,}")
188
+ print(f" Files: {file_idx} | Time: {elapsed:.0f}s")
189
+ return total_tokens
190
+
191
+
192
+ # ─── FineWeb-Edu Source ─────────────────────────────────────────────────────
193
+
194
+ def fetch_fineweb_edu(target_tokens: int, output_dir: str,
195
+ min_score: float = 4.0, tokens_per_word: float = 1.3):
196
+ """Stream FineWeb-Edu (top educational web content), output parquet."""
197
+ from datasets import load_dataset
198
+
199
+ print(f"\n{'='*60}")
200
+ print(f"[FineWeb-Edu] Streaming (score >= {min_score})")
201
+ print(f" Target: {target_tokens:,} tokens")
202
+ print(f" Output: {output_dir}")
203
+ print(f"{'='*60}\n")
204
+
205
+ os.makedirs(output_dir, exist_ok=True)
206
+
207
+ ds = load_dataset(
208
+ "HuggingFaceFW/fineweb-edu", "sample-10BT",
209
+ split="train", streaming=True,
210
+ trust_remote_code=False
211
+ )
212
+
213
+ buf = []
214
+ total_tokens = 0
215
+ kept = 0
216
+ skipped = 0
217
+ file_idx = 0
218
+ BATCH = 5000
219
+ t0 = time.time()
220
+
221
+ for doc in ds:
222
+ # FineWeb-Edu has 'score' (float) β€” educational quality 0-5
223
+ score = doc.get("score", 0)
224
+ if not isinstance(score, (int, float)):
225
+ try:
226
+ score = float(score)
227
+ except (ValueError, TypeError):
228
+ skipped += 1
229
+ continue
230
+
231
+ if score < min_score:
232
+ skipped += 1
233
+ continue
234
+
235
+ raw = doc.get("text") or ""
236
+ cleaned = clean_text(raw)
237
+
238
+ if not is_high_quality(cleaned, min_chars=500, min_words=80):
239
+ skipped += 1
240
+ continue
241
+
242
+ est_tok = int(len(cleaned.split()) * tokens_per_word)
243
+
244
+ buf.append(cleaned)
245
+ total_tokens += est_tok
246
+ kept += 1
247
+
248
+ if len(buf) >= BATCH:
249
+ fp = Path(output_dir) / f"fineweb_{file_idx:04d}.parquet"
250
+ pq.write_table(pa.table({"text": buf}), str(fp))
251
+ elapsed = time.time() - t0
252
+ rate = total_tokens / max(elapsed, 1)
253
+ print(f" fineweb_{file_idx:04d}.parquet | {kept:,} docs | "
254
+ f"~{total_tokens:,} tok | {rate:,.0f} tok/s | {elapsed:.0f}s")
255
+ buf = []
256
+ file_idx += 1
257
+
258
+ if total_tokens >= target_tokens:
259
+ break
260
+
261
+ if buf:
262
+ fp = Path(output_dir) / f"fineweb_{file_idx:04d}.parquet"
263
+ pq.write_table(pa.table({"text": buf}), str(fp))
264
+ file_idx += 1
265
+
266
+ elapsed = time.time() - t0
267
+ print(f"\n--- FineWeb-Edu complete ---")
268
+ print(f" Kept: {kept:,} docs | Skipped: {skipped:,}")
269
+ print(f" Est. tokens: {total_tokens:,}")
270
+ print(f" Files: {file_idx} | Time: {elapsed:.0f}s")
271
+ return total_tokens
272
+
273
+
274
+ # ─── Main ───────────────────────────────────────────────────────────────────
275
+
276
+ def main():
277
+ parser = argparse.ArgumentParser(
278
+ description="Build ultra-clean English corpus for continued pretraining"
279
+ )
280
+ parser.add_argument(
281
+ "--target_tokens", type=int, default=50_000_000,
282
+ help="Total target token count (default: 50,000,000 = 50M)"
283
+ )
284
+ parser.add_argument(
285
+ "--sources", type=str, default="wiki,fineweb",
286
+ help="Comma-separated sources: wiki, fineweb (default: wiki,fineweb)"
287
+ )
288
+ parser.add_argument(
289
+ "--wiki_share", type=float, default=0.6,
290
+ help="Wikipedia share when both sources are used (default: 0.6 = 60%%)"
291
+ )
292
+ parser.add_argument(
293
+ "--fineweb_min_score", type=float, default=4.0,
294
+ help="Minimum FineWeb-Edu educational score 0-5 (default: 4.0)"
295
+ )
296
+ parser.add_argument(
297
+ "--output_dir", type=str, default="Base/data/filtered_english",
298
+ help="Output directory for parquet files"
299
+ )
300
+ args = parser.parse_args()
301
+
302
+ sources = [s.strip().lower() for s in args.sources.split(",")]
303
+
304
+ print(f"\n{'#'*60}")
305
+ print(f" ULTRA-CLEAN ENGLISH CORPUS BUILDER")
306
+ print(f" Target: {args.target_tokens:,} tokens")
307
+ print(f" Sources: {', '.join(sources)}")
308
+ print(f" Output: {args.output_dir}")
309
+ print(f"{'#'*60}")
310
+
311
+ # Check datasets library
312
+ try:
313
+ import datasets
314
+ print(f" datasets v{datasets.__version__}")
315
+ except ImportError:
316
+ print("\n ERROR: 'datasets' library is required for streaming.")
317
+ print(" Install it: pip install datasets")
318
+ return
319
+
320
+ total = 0
321
+
322
+ if "wiki" in sources:
323
+ if "fineweb" in sources:
324
+ wiki_target = int(args.target_tokens * args.wiki_share)
325
+ else:
326
+ wiki_target = args.target_tokens
327
+ total += fetch_wikipedia(wiki_target, args.output_dir)
328
+
329
+ if "fineweb" in sources:
330
+ fw_target = args.target_tokens - total if total > 0 else args.target_tokens
331
+ if fw_target > 0:
332
+ total += fetch_fineweb_edu(
333
+ fw_target, args.output_dir,
334
+ min_score=args.fineweb_min_score
335
+ )
336
+
337
+ print(f"\n{'#'*60}")
338
+ print(f" CORPUS BUILD COMPLETE")
339
+ print(f" Total estimated tokens: {total:,}")
340
+ print(f" Output directory: {args.output_dir}")
341
+ print(f"{'#'*60}")
342
+ print(f"\nNext steps:")
343
+ print(f" 1. python Base/scripts/prepare_litdata.py --mode english")
344
+ print(f" 2. $env:TORCHDYNAMO_DISABLE = '1'")
345
+ print(f" 3. litgpt pretrain --config Base/configs/pretrain_100m_english.yaml")
346
+
347
+
348
+ if __name__ == "__main__":
349
+ main()