"""Clean pinned V3 inputs, sample documents and write existing V2 token/window formats.""" import argparse import hashlib import json import re import sqlite3 import sys import unicodedata from collections import Counter from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "src")) import numpy as np import pyarrow.parquet as pq import sentencepiece as spm from vimeml.training.data import prepare_indexes, write_json from vimeml.tokenizer.encode_parallel import output_lock SPLITS = ("train", "validation", "test") CONTROLS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") JAPANESE = re.compile(r"[\u3040-\u30ff\u3400-\u9fff]") NOISE = re.compile(r"^(?:https?://\S+|ログイン|ログアウト|前のページ|次のページ|広告|Cookie設定)$") MASK = re.compile(r"(?:\[PERSON\]|\[NAME\]|\[MASK\]||)", re.I) def normalized(text): return CONTROLS.sub(" ", unicodedata.normalize("NFC", text)).strip().lstrip("\ufeff") def digest(text): # Content identity for deduplication, not repeated file-integrity SHA256 scans. return hashlib.blake2b(text.encode("utf-8"), digest_size=16).hexdigest() def chunks(text, chat): text = normalized(text) if not text or "\ufffd" in text or MASK.search(text): return [] if chat: return [text] # Preserve short turns, punctuation, spaces and internal newlines. lines = [line.strip() for line in text.splitlines() if line.strip()] text = " ".join(line for line in lines if not NOISE.fullmatch(line)) if len(text) < 24 or len(JAPANESE.findall(text)) / len(text) < .20: return [] pieces, pending = [], "" for sentence in re.findall(r"[^。!?!?]+[。!?!?]*|[。!?!?]+", text): if pending and len(pending) + len(sentence) > 1024: pieces.append(pending) pending = "" while len(sentence) > 1024: pieces.append(sentence[:1024]) sentence = sentence[1024:] pending += sentence if pending: pieces.append(pending) return [piece.strip() for piece in pieces if piece.strip()] def ingest(db, sp, texts, identity, source, pool, seed, encoded=None): doc_id = digest(identity) if db.execute("SELECT 1 FROM docs WHERE id=?", (doc_id,)).fetchone(): return number = int(digest(f"split:{seed}:{doc_id}"), 16) % 10000 split = "train" if number < 9800 else "validation" if number < 9900 else "test" priority = int(digest(f"sample:{seed}:{doc_id}")[:15], 16) if encoded is None: encoded = sp.encode(texts, out_type=int, num_threads=1) pairs = 0 reserved = {sp.bos_id(), sp.eos_id(), sp.pad_id(), sp.unk_id()} for ordinal, (text, ids) in enumerate(zip(texts, encoded, strict=True)): if not ids or any(token in reserved for token in ids): continue key = digest(text) seen = db.execute("SELECT split,pool FROM seen WHERE sig=?", (key,)).fetchone() if seen and (seen[0] != split or pool == "web"): continue # No exact sequence crosses splits; preserve repeated chat within a split. if db.execute("SELECT 1 FROM blocked WHERE sig=?", (key,)).fetchone(): continue db.execute("INSERT OR IGNORE INTO seen VALUES (?,?,?)", (key, split, pool)) tokens = np.asarray([sp.bos_id(), *ids, sp.eos_id()], dtype="= 8: db.execute("INSERT OR IGNORE INTO blocked VALUES (?)", (digest(normalized(text)),)) db.execute("INSERT INTO done VALUES ('exclusions')") db.commit() jobs = [(name, folder) for name, folder in chats] + [(str(path.relative_to(ROOT)), path) for path in inputs] for name, path in jobs: if db.execute("SELECT 1 FROM done WHERE input=?", (name,)).fetchone(): continue print(f"Cleaning/encoding {name}", flush=True) write_json(output / "progress.json", {"status": "cleaning_encoding", "input": name}) if path.is_dir(): for position, file in enumerate(sorted(path.rglob("*.json")), 1): doc = json.loads(file.read_text(encoding="utf-8")) texts = [part for item in doc["utterances"] for part in chunks(item["text"], True)] ingest(db, sp, texts, f"{name}:{file.stem}", name, "chat", args.seed) if position % 2000 == 0: print(f" {position:,} dialogues", flush=True) else: position = 0 for batch in pq.ParquetFile(path).iter_batches(batch_size=256, columns=["text", "source"]): documents = [] for row in batch.to_pylist(): texts = chunks(row["text"], False) if texts: documents.append((texts, row["source"])) encoded = sp.encode([text for texts, _ in documents for text in texts], out_type=int, num_threads=8) offset = 0 for texts, source in documents: ingest(db, sp, texts, "\n".join(texts), "jpnmix:" + source, "web", args.seed, encoded[offset:offset + len(texts)]) offset += len(texts) position += batch.num_rows if position % 10240 == 0: print(f" {position:,} documents", flush=True) write_json(output / "progress.json", {"status": "cleaning_encoding", "input": name, "processed_documents": position}) db.execute("INSERT INTO done VALUES (?)", (name,)) db.commit() # Interrupted current input rolls back; completed inputs are reused. db.execute("DELETE FROM selected") db.execute("INSERT INTO selected SELECT id FROM docs WHERE pool='chat' OR split!='train'") used = 0 for doc, pairs in db.execute("SELECT id,pairs FROM docs WHERE pool='web' AND split='train' ORDER BY priority,id").fetchall(): if pairs and used + pairs <= args.web_train_tokens: db.execute("INSERT INTO selected VALUES (?)", (doc,)) used += pairs db.commit() sources = {name: index for index, (name,) in enumerate(db.execute("SELECT DISTINCT source FROM seq ORDER BY source"))} selection = {"parquets": [entry["path"] for entry in files], "seed": args.seed, "web_train_prediction_pairs": used, "budget": args.web_train_tokens, "split": "98/1/1 by document or complete dialogue", "source_versions": download} for pool in ("mixed", "chat"): print(f"Exporting {pool}", flush=True) write_json(output / "progress.json", {"status": "exporting", "pool": pool}) build_store(db, output / pool, tokenizer, sources, selection, pool) prepare_indexes(output / pool, output / "indexes" / pool, 128, verification="metadata") db.close() write_json(output / "progress.json", {"status": "complete", "web_train_prediction_pairs": used}) print(f"Complete: {output}", flush=True) if __name__ == "__main__": main()