|
|
| """
|
| Deep + Smart cleaning for the 1B English corpus.
|
|
|
| Two-phase pipeline:
|
| PHASE 1 β Deep Clean (text-level):
|
| - Unicode normalization (NFKC)
|
| - HTML entity & tag stripping
|
| - URL / email / file-path removal
|
| - Control character removal
|
| - Smart quote / dash / ellipsis normalization
|
| - Fix spacing, punctuation, sentence boundaries
|
| - Strip code blocks, import lines
|
| - Fix repeated words, characters, punctuation
|
|
|
| PHASE 2 β Smart Filter (document-level):
|
| - Remove docs with non-English scripts (CJK, Arabic, Cyrillic, Devanagari)
|
| - Remove docs under 50 words
|
| - Remove very repetitive docs (unique word ratio < 0.22)
|
| - Remove docs with heavy residual code (5+ code patterns)
|
| - Strip boilerplate (cookies, subscribe, social, login, nav, copyright)
|
| - Strip number-only table lines
|
| - Cross-document deduplication (blake2b fingerprints)
|
|
|
| Input: Base/data/english_1b_raw/ (parquet files from build_english_1b.py)
|
| Output: Base/data/english_1b_clean/ (cleaned parquet files + CLEANING_REPORT.txt)
|
|
|
| Usage:
|
| python Base/scripts/clean_english_1b.py
|
| python Base/scripts/clean_english_1b.py --input_dir Base/data/english_1b_raw --output_dir Base/data/english_1b_clean
|
| """
|
|
|
| import argparse
|
| import hashlib
|
| import json
|
| import multiprocessing as mp
|
| import re
|
| import time
|
| import unicodedata
|
| from collections import Counter
|
| from functools import partial
|
| from pathlib import Path
|
|
|
| import pyarrow as pa
|
| import pyarrow.parquet as pq
|
|
|
|
|
|
|
|
|
| TOKENS_PER_WORD = 1.3
|
| FLUSH_DOCS = 5_000
|
|
|
|
|
|
|
| CTRL_CHARS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\ufeff\ufffd]")
|
|
|
| HTML_ENTITIES = [
|
| ("&", "&"), ("<", "<"), (">", ">"),
|
| (""", '"'), ("'", "'"), ("'", "'"),
|
| (" ", " "), ("—", " - "), ("–", "-"),
|
| ("…", "..."), ("«", '"'), ("»", '"'),
|
| ("•", "- "), ("·", " "), ("©", "(c)"),
|
| ("®", "(R)"), ("™", "(TM)"), ("°", " degrees"),
|
| ]
|
|
|
| RE_URL = re.compile(r"https?://\S+|www\.\S+", re.I)
|
| RE_EMAIL = re.compile(r"\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b")
|
| RE_FILE_PATH = re.compile(r"(?:[A-Z]:\\|/(?:home|usr|var|etc|opt)/)\S+")
|
| RE_HTML_TAG = re.compile(r"</?[a-zA-Z][a-zA-Z0-9]*(?:\s[^>]*)?\s*/?>")
|
| RE_HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
|
| RE_CODE_BLOCK = re.compile(r"```[\s\S]*?```")
|
| RE_IMPORT_LINE = re.compile(
|
| r"^(?:import |from \S+ import |#include |using namespace |require\(|"
|
| r"const \w+ = require|module\.exports|export default).*$",
|
| re.M,
|
| )
|
| RE_REPEATED_LINE = re.compile(r"^(.{20,})\n(?:\1\n?)+", re.M)
|
| RE_REPEATED_PUNCT = re.compile(r"([!?.])\1{3,}")
|
| RE_REPEATED_CHAR = re.compile(r"(.)\1{5,}")
|
| RE_REPEATED_WORD = re.compile(r"\b(\w+)(?:\s+\1){2,}\b", re.I)
|
| RE_MULTI_NL = re.compile(r"\n{4,}")
|
| RE_MULTI_SP = re.compile(r"[ \t]{2,}")
|
| RE_TRAIL_SP = re.compile(r"[ \t]+$", re.M)
|
| RE_NO_SP_PERIOD = re.compile(r"([.!?])([A-Z])")
|
| RE_SP_PUNCT = re.compile(r"\s+([.,;:!?])")
|
| RE_DBL_PERIOD = re.compile(r"\.{2}(?!\.)")
|
| RE_ORPHAN_PAREN = re.compile(r"\(\s*\)|^\s*\)\s*$|^\s*\(\s*$", re.M)
|
|
|
|
|
|
|
| RE_CJK = re.compile(r"[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]{3,}")
|
| RE_ARABIC = re.compile(r"[\u0600-\u06ff]{5,}")
|
| RE_CYRILLIC = re.compile(r"[\u0400-\u04ff]{5,}")
|
| RE_DEVANAGARI = re.compile(r"[\u0900-\u097f]{5,}")
|
|
|
| RE_RESIDUAL_CODE = re.compile(
|
| r"(function\s*\(|var\s+\w+\s*=|console\.log|document\.get|"
|
| r"if\s*\(\s*\w+\s*[!=]==|\.addEventListener|"
|
| r"def \w+\(|class \w+:|self\.\w+|print\(|"
|
| r"\.forEach|\.map\(|\.filter\(|async\s+function)",
|
| re.I,
|
| )
|
|
|
|
|
| BOILERPLATE_PATTERNS = [
|
| re.compile(r"^.*(?:cookie|cookies)\s+(?:policy|consent|notice|preferences|settings).*$", re.I | re.M),
|
| re.compile(r"^.*(?:subscribe|sign\s*up\s+(?:for|to)\s+(?:our|the)\s+newsletter|unsubscribe|opt[\s-]*out).*$", re.I | re.M),
|
| re.compile(r"^.*(?:you\s+won'?t\s+believe|click\s+here|read\s+more\s*\.{0,3}$|share\s+this|trending\s+now|sponsored\s+content|advertisement).*$", re.I | re.M),
|
| re.compile(r"^.*(?:follow\s+us\s+on|share\s+on\s+(?:facebook|twitter|linkedin|instagram)|like\s+us\s+on|tweet\s+this).*$", re.I | re.M),
|
| re.compile(r"^.*(?:skip\s+to\s+(?:main\s+)?content|back\s+to\s+top|previous\s+article|next\s+article|related\s+(?:articles|posts)).*$", re.I | re.M),
|
| re.compile(r"^.*(?:log\s*in\s+to\s+(?:your|an)\s+account|create\s+(?:a\s+)?(?:free\s+)?account|forgot\s+(?:your\s+)?password|already\s+(?:a\s+)?member).*$", re.I | re.M),
|
| re.compile(r"^.*(?:leave\s+a\s+(?:comment|reply)|post\s+a\s+comment|\d+\s+comments?$|logged\s+in\s+as).*$", re.I | re.M),
|
| re.compile(r"^.*(?:all\s+rights\s+reserved|\(c\)\s*\d{4}|copyright\s+\d{4}).*$", re.I | re.M),
|
| ]
|
| RE_NUMBER_TABLE = re.compile(r"^[\d\s,.\-+/%$]+$", re.M)
|
|
|
|
|
|
|
|
|
| class Stats:
|
| def __init__(self):
|
| self.total_input = 0
|
| self.total_output = 0
|
| self.tokens_in = 0
|
| self.tokens_out = 0
|
|
|
| self.ctrl_removed = 0
|
| self.html_entities = 0
|
| self.html_tags = 0
|
| self.urls_removed = 0
|
| self.emails_removed = 0
|
| self.code_blocks = 0
|
| self.import_lines = 0
|
| self.repeated_lines = 0
|
| self.repeated_punct = 0
|
| self.repeated_words = 0
|
| self.punct_fixed = 0
|
| self.smart_quotes = 0
|
| self.orphan_parens = 0
|
|
|
| self.dropped_short = 0
|
| self.dropped_nonenglish = 0
|
| self.dropped_repetitive = 0
|
| self.dropped_code = 0
|
| self.dropped_lowquality = 0
|
| self.dropped_dup = 0
|
| self.boilerplate_stripped = 0
|
| self.number_tables_stripped = 0
|
|
|
|
|
|
|
|
|
| def deep_clean(text: str, st: Stats) -> str:
|
| """Thorough text-level cleaning."""
|
| if not text or len(text.strip()) < 30:
|
| return ""
|
|
|
|
|
| text = unicodedata.normalize("NFKC", text)
|
|
|
|
|
| new = CTRL_CHARS.sub("", text)
|
| if new != text:
|
| st.ctrl_removed += len(text) - len(new)
|
| text = new
|
|
|
|
|
| for old, repl in HTML_ENTITIES:
|
| if old in text:
|
| c = text.count(old)
|
| st.html_entities += c
|
| text = text.replace(old, repl)
|
|
|
|
|
| new = RE_HTML_TAG.sub("", text)
|
| if new != text:
|
| st.html_tags += 1
|
| text = new
|
|
|
|
|
| text = RE_HTML_COMMENT.sub("", text)
|
|
|
|
|
| new = RE_CODE_BLOCK.sub("", text)
|
| if new != text:
|
| st.code_blocks += 1
|
| text = new
|
|
|
|
|
| new = RE_IMPORT_LINE.sub("", text)
|
| if new != text:
|
| st.import_lines += 1
|
| text = new
|
|
|
|
|
| new = RE_URL.sub("", text)
|
| if new != text:
|
| st.urls_removed += 1
|
| text = new
|
|
|
|
|
| new = RE_EMAIL.sub("", text)
|
| if new != text:
|
| st.emails_removed += 1
|
| text = new
|
|
|
|
|
| text = RE_FILE_PATH.sub("", text)
|
|
|
|
|
| new = RE_REPEATED_LINE.sub(r"\1", text)
|
| if new != text:
|
| st.repeated_lines += 1
|
| text = new
|
|
|
|
|
| new = RE_REPEATED_PUNCT.sub(r"\1", text)
|
| if new != text:
|
| st.repeated_punct += 1
|
| text = new
|
|
|
|
|
| new = RE_REPEATED_CHAR.sub(r"\1\1", text)
|
| if new != text:
|
| text = new
|
|
|
|
|
| new = RE_REPEATED_WORD.sub(r"\1", text)
|
| if new != text:
|
| st.repeated_words += 1
|
| text = new
|
|
|
|
|
| changes = [
|
| ("\u2018", "'"), ("\u2019", "'"),
|
| ("\u201c", '"'), ("\u201d", '"'),
|
| ("\u2013", "-"), ("\u2014", " - "),
|
| ("\u2026", "..."), ("\u00a0", " "),
|
| ("\u200b", ""), ("\u200d", ""), ("\u200c", ""),
|
| ("\ufeff", ""),
|
| ]
|
| for old, repl in changes:
|
| if old in text:
|
| st.smart_quotes += 1
|
| text = text.replace(old, repl)
|
|
|
|
|
| new = RE_DBL_PERIOD.sub(".", text)
|
| if new != text:
|
| st.punct_fixed += 1
|
| text = new
|
|
|
|
|
| new = RE_NO_SP_PERIOD.sub(r"\1 \2", text)
|
| if new != text:
|
| st.punct_fixed += 1
|
| text = new
|
|
|
|
|
| new = RE_SP_PUNCT.sub(r"\1", text)
|
| if new != text:
|
| st.punct_fixed += 1
|
| text = new
|
|
|
|
|
| new = RE_ORPHAN_PAREN.sub("", text)
|
| if new != text:
|
| st.orphan_parens += 1
|
| text = new
|
|
|
|
|
| text = RE_MULTI_SP.sub(" ", text)
|
| text = RE_MULTI_NL.sub("\n\n\n", text)
|
| text = RE_TRAIL_SP.sub("", text)
|
|
|
|
|
| text = "\n".join(line.strip() for line in text.splitlines())
|
| return text.strip()
|
|
|
|
|
|
|
|
|
| def smart_filter(text: str, st: Stats) -> tuple[str | None, str | None]:
|
| """Document-level quality filter. Returns (cleaned_text, reason_if_dropped)."""
|
| words = text.split()
|
| wc = len(words)
|
|
|
| if wc < 50:
|
| st.dropped_short += 1
|
| return None, "short"
|
|
|
|
|
| scripts = []
|
| if RE_CJK.search(text):
|
| scripts.append("CJK")
|
| if RE_ARABIC.search(text):
|
| scripts.append("Arabic")
|
| if RE_CYRILLIC.search(text):
|
| scripts.append("Cyrillic")
|
| if RE_DEVANAGARI.search(text):
|
| scripts.append("Devanagari")
|
| if scripts:
|
| st.dropped_nonenglish += 1
|
| return None, f"non-english:{','.join(scripts)}"
|
|
|
|
|
| alpha = sum(c.isalpha() for c in text)
|
| if alpha / max(len(text), 1) < 0.60:
|
| st.dropped_lowquality += 1
|
| return None, "low_alpha"
|
|
|
|
|
| if wc > 50:
|
| unique_ratio = len(set(w.lower() for w in words)) / wc
|
| if unique_ratio < 0.22:
|
| st.dropped_repetitive += 1
|
| return None, f"repetitive:{unique_ratio:.3f}"
|
|
|
|
|
| code_hits = len(RE_RESIDUAL_CODE.findall(text))
|
| if code_hits >= 5:
|
| st.dropped_code += 1
|
| return None, f"code:{code_hits}"
|
|
|
|
|
| cleaned = text
|
| for pat in BOILERPLATE_PATTERNS:
|
| new = pat.sub("", cleaned)
|
| if new != cleaned:
|
| st.boilerplate_stripped += 1
|
| cleaned = new
|
|
|
|
|
| new = RE_NUMBER_TABLE.sub("", cleaned)
|
| if new != cleaned:
|
| st.number_tables_stripped += 1
|
| cleaned = new
|
|
|
|
|
| cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
|
|
|
|
|
| if len(cleaned.split()) < 40:
|
| st.dropped_short += 1
|
| return None, "short_after_strip"
|
|
|
| return cleaned, None
|
|
|
|
|
|
|
|
|
| def fingerprint(text: str) -> int:
|
| norm = " ".join(text.lower().split())[:8192]
|
| return int.from_bytes(
|
| hashlib.blake2b(norm.encode(), digest_size=8).digest(),
|
| byteorder="little", signed=False,
|
| )
|
|
|
|
|
|
|
|
|
| def _clean_one(raw_text: str) -> dict:
|
| """Clean a single document. Returns dict with result + local stats.
|
| Must be a top-level function for multiprocessing.
|
| """
|
| local = {
|
| "text": None, "fp": None, "drop_reason": None,
|
| "tokens_in": 0, "tokens_out": 0,
|
|
|
| "ctrl_removed": 0, "html_entities": 0, "html_tags": 0,
|
| "urls_removed": 0, "emails_removed": 0, "code_blocks": 0,
|
| "import_lines": 0, "repeated_lines": 0, "repeated_punct": 0,
|
| "repeated_words": 0, "punct_fixed": 0, "smart_quotes": 0,
|
| "orphan_parens": 0,
|
|
|
| "dropped_short": 0, "dropped_nonenglish": 0, "dropped_repetitive": 0,
|
| "dropped_code": 0, "dropped_lowquality": 0,
|
| "boilerplate_stripped": 0, "number_tables_stripped": 0,
|
| }
|
| est_in = max(1, int(len((raw_text or "").split()) * TOKENS_PER_WORD))
|
| local["tokens_in"] = est_in
|
|
|
|
|
| st = Stats()
|
| cleaned = deep_clean(raw_text or "", st)
|
|
|
|
|
| for k in ["ctrl_removed", "html_entities", "html_tags", "urls_removed",
|
| "emails_removed", "code_blocks", "import_lines", "repeated_lines",
|
| "repeated_punct", "repeated_words", "punct_fixed", "smart_quotes",
|
| "orphan_parens"]:
|
| local[k] = getattr(st, k)
|
|
|
| if not cleaned or len(cleaned) < 50:
|
| local["drop_reason"] = "short"
|
| local["dropped_short"] = 1
|
| return local
|
|
|
| final, reason = smart_filter(cleaned, st)
|
| for k in ["dropped_short", "dropped_nonenglish", "dropped_repetitive",
|
| "dropped_code", "dropped_lowquality", "boilerplate_stripped",
|
| "number_tables_stripped"]:
|
| local[k] = getattr(st, k)
|
|
|
| if final is None:
|
| local["drop_reason"] = reason
|
| return local
|
|
|
| local["text"] = final
|
| local["fp"] = fingerprint(final)
|
| local["tokens_out"] = max(1, int(len(final.split()) * TOKENS_PER_WORD))
|
| return local
|
|
|
|
|
|
|
|
|
| def main():
|
| parser = argparse.ArgumentParser(description="Deep + Smart clean English 1B corpus")
|
| parser.add_argument("--input_dir", default="Base/data/english_1b_raw")
|
| parser.add_argument("--output_dir", default="Base/data/english_1b_clean")
|
| parser.add_argument("--workers", type=int, default=0,
|
| help="Number of parallel workers (0 = all CPUs)")
|
| args = parser.parse_args()
|
|
|
| in_dir = Path(args.input_dir)
|
| out_dir = Path(args.output_dir)
|
| n_workers = args.workers or mp.cpu_count()
|
|
|
| if not in_dir.exists():
|
| print(f"ERROR: Input directory not found: {in_dir}")
|
| print("Run build_english_1b.py first!")
|
| return
|
|
|
| out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
| parquet_files = sorted(in_dir.glob("*.parquet"))
|
| if not parquet_files:
|
| print(f"ERROR: No parquet files in {in_dir}")
|
| return
|
|
|
| print("=" * 72)
|
| print(" DEEP + SMART CLEAN β English 1B Corpus ** PARALLEL MODE **")
|
| print("=" * 72)
|
| print(f" Input: {in_dir} ({len(parquet_files)} files)")
|
| print(f" Output: {out_dir}")
|
| print(f" Workers: {n_workers} (CPU cores: {mp.cpu_count()})")
|
| print("=" * 72)
|
|
|
| st = Stats()
|
| seen_fps: set[int] = set()
|
| out_buf: dict[str, list] = {}
|
| out_idx: Counter = Counter()
|
| t0 = time.time()
|
|
|
| def flush_buf(source: str):
|
| bucket = out_buf.get(source)
|
| if not bucket:
|
| return
|
| i = out_idx[source]
|
| path = out_dir / f"{source}_{i:04d}.parquet"
|
| pq.write_table(pa.table({"text": bucket}), str(path))
|
| out_idx[source] += 1
|
| out_buf[source] = []
|
|
|
|
|
| pool = mp.Pool(n_workers)
|
|
|
| try:
|
| for pf in parquet_files:
|
| source = pf.stem.rsplit("_", 1)[0]
|
| print(f"\n Processing: {pf.name}")
|
| table = pq.read_table(str(pf), columns=["text"])
|
| texts = table.column("text").to_pylist()
|
| del table
|
|
|
|
|
| results = pool.map(_clean_one, texts, chunksize=256)
|
|
|
| file_kept = 0
|
| for r in results:
|
| st.total_input += 1
|
| st.tokens_in += r["tokens_in"]
|
|
|
| st.ctrl_removed += r["ctrl_removed"]
|
| st.html_entities += r["html_entities"]
|
| st.html_tags += r["html_tags"]
|
| st.urls_removed += r["urls_removed"]
|
| st.emails_removed += r["emails_removed"]
|
| st.code_blocks += r["code_blocks"]
|
| st.import_lines += r["import_lines"]
|
| st.repeated_lines += r["repeated_lines"]
|
| st.repeated_punct += r["repeated_punct"]
|
| st.repeated_words += r["repeated_words"]
|
| st.punct_fixed += r["punct_fixed"]
|
| st.smart_quotes += r["smart_quotes"]
|
| st.orphan_parens += r["orphan_parens"]
|
|
|
| st.dropped_short += r["dropped_short"]
|
| st.dropped_nonenglish += r["dropped_nonenglish"]
|
| st.dropped_repetitive += r["dropped_repetitive"]
|
| st.dropped_code += r["dropped_code"]
|
| st.dropped_lowquality += r["dropped_lowquality"]
|
| st.boilerplate_stripped += r["boilerplate_stripped"]
|
| st.number_tables_stripped += r["number_tables_stripped"]
|
|
|
| if r["text"] is None:
|
| continue
|
|
|
|
|
| fpi = r["fp"]
|
| if fpi in seen_fps:
|
| st.dropped_dup += 1
|
| continue
|
| seen_fps.add(fpi)
|
|
|
|
|
| st.total_output += 1
|
| st.tokens_out += r["tokens_out"]
|
| bucket = out_buf.setdefault(source, [])
|
| bucket.append(r["text"])
|
| file_kept += 1
|
| if len(bucket) >= FLUSH_DOCS:
|
| flush_buf(source)
|
|
|
| elapsed = time.time() - t0
|
| rate = st.total_input / max(elapsed, 1)
|
| print(f" kept={file_kept:,} / {len(texts):,} "
|
| f"total_out={st.total_output:,} "
|
| f"~{st.tokens_out:,} tokens "
|
| f"elapsed={elapsed:.0f}s "
|
| f"rate={rate:,.0f} docs/s")
|
|
|
| finally:
|
| pool.close()
|
| pool.join()
|
|
|
| elapsed = time.time() - t0
|
| print(f" kept={file_kept:,} / {len(texts):,} "
|
| f"total_out={st.total_output:,} "
|
| f"~{st.tokens_out:,} tokens "
|
| f"elapsed={elapsed:.0f}s")
|
|
|
|
|
| for source in list(out_buf):
|
| flush_buf(source)
|
|
|
| elapsed = time.time() - t0
|
|
|
|
|
| kept_pct = st.total_output / max(st.total_input, 1) * 100
|
| tok_pct = st.tokens_out / max(st.tokens_in, 1) * 100
|
|
|
| lines = [
|
| "=" * 72,
|
| " CLEANING REPORT β English 1B Corpus",
|
| "=" * 72, "",
|
| f"Input: {in_dir}",
|
| f"Output: {out_dir}",
|
| f"Files in: {len(parquet_files)}",
|
| f"Files out: {sum(out_idx.values())}",
|
| f"Elapsed: {elapsed:.0f}s ({elapsed/60:.1f} min)", "",
|
| "βββ Document Stats βββ",
|
| f" Total input: {st.total_input:,}",
|
| f" Total output: {st.total_output:,}",
|
| f" Kept: {kept_pct:.1f}%",
|
| f" Unique FPs: {len(seen_fps):,}", "",
|
| "βββ Token Estimates βββ",
|
| f" Tokens in: {st.tokens_in:,}",
|
| f" Tokens out: {st.tokens_out:,}",
|
| f" Retained: {tok_pct:.1f}%", "",
|
| "βββ Deep Clean Stats βββ",
|
| f" Control chars removed: {st.ctrl_removed:,}",
|
| f" HTML entities fixed: {st.html_entities:,}",
|
| f" HTML tags stripped: {st.html_tags:,}",
|
| f" URLs removed: {st.urls_removed:,}",
|
| f" Emails removed: {st.emails_removed:,}",
|
| f" Code blocks removed: {st.code_blocks:,}",
|
| f" Import lines removed: {st.import_lines:,}",
|
| f" Repeated lines fixed: {st.repeated_lines:,}",
|
| f" Repeated punct fixed: {st.repeated_punct:,}",
|
| f" Repeated words fixed: {st.repeated_words:,}",
|
| f" Punctuation fixed: {st.punct_fixed:,}",
|
| f" Smart quotes fixed: {st.smart_quotes:,}",
|
| f" Orphan parens removed: {st.orphan_parens:,}", "",
|
| "βββ Smart Filter Stats βββ",
|
| f" Dropped (too short): {st.dropped_short:,}",
|
| f" Dropped (non-English): {st.dropped_nonenglish:,}",
|
| f" Dropped (repetitive): {st.dropped_repetitive:,}",
|
| f" Dropped (code): {st.dropped_code:,}",
|
| f" Dropped (low quality): {st.dropped_lowquality:,}",
|
| f" Dropped (duplicate): {st.dropped_dup:,}",
|
| f" Boilerplate stripped: {st.boilerplate_stripped:,}",
|
| f" Number tables stripped: {st.number_tables_stripped:,}", "",
|
| "Next: python Base/scripts/prepare_litdata.py \\",
|
| " --filtered_dir Base/data/english_1b_clean \\",
|
| " --output_dir Base/data/litdata_english_1b \\",
|
| " --label ENGLISH_1B", "",
|
| "=" * 72,
|
| ]
|
| report = "\n".join(lines)
|
| print("\n" + report)
|
| (out_dir / "CLEANING_REPORT.txt").write_text(report, encoding="utf-8")
|
| (out_dir / "clean_stats.json").write_text(json.dumps({
|
| "docs_in": st.total_input,
|
| "docs_out": st.total_output,
|
| "tokens_in": st.tokens_in,
|
| "tokens_out": st.tokens_out,
|
| "dropped": {
|
| "short": st.dropped_short,
|
| "non_english": st.dropped_nonenglish,
|
| "repetitive": st.dropped_repetitive,
|
| "code": st.dropped_code,
|
| "low_quality": st.dropped_lowquality,
|
| "duplicate": st.dropped_dup,
|
| },
|
| }, indent=2), encoding="utf-8")
|
|
|
| print(f"\nCleaning complete. {st.total_output:,} docs, ~{st.tokens_out:,} tokens retained.")
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|