#!/usr/bin/env python3 """ Utility script to train a SentencePiece BPE tokenizer on the Japanese corpus. Example: python tools/tokenizer/train_bpe.py \\ --manifest JA_yodas_dataset/ja_yodas_train.jsonl \\ --output-prefix checkpoints/japanese_bpe \\ --vocab-size 12000 """ import argparse import json import os import sys import tempfile from pathlib import Path import sentencepiece as spm from indextts.utils.front import TextNormalizer def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Train a Japanese BPE tokenizer with SentencePiece.") parser.add_argument( "--manifest", nargs="+", required=True, help="One or more JSONL manifests containing a 'text' field.", ) parser.add_argument( "--output-prefix", type=Path, default=Path("checkpoints/japanese_bpe"), help="Output prefix for the tokenizer files (.model/.vocab).", ) parser.add_argument( "--vocab-size", type=int, default=12000, help="Desired vocabulary size.", ) parser.add_argument( "--character-coverage", type=float, default=0.9995, help="Character coverage for SentencePiece (keep near 1.0 for Japanese).", ) parser.add_argument( "--model-type", choices=["bpe", "unigram"], default="bpe", help="SentencePiece model type.", ) parser.add_argument( "--input-sentence-size", type=int, default=0, help="Limit the number of sentences sampled for training (0 means use all).", ) parser.add_argument( "--byte-fallback", action="store_true", default=False, help="Enable byte fallback to avoid for unseen characters (Do not enable unless you know what you're doing)", ) return parser.parse_args() def iter_texts(manifests: list[Path]) -> tuple[int, int, Path]: normalizer = TextNormalizer(preferred_language="ja") normalizer.load() num_samples = 0 num_empty = 0 tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8") try: with tmp_file as fp: for manifest in manifests: with open(manifest, "r", encoding="utf-8") as handle: for line in handle: if not line.strip(): continue payload = json.loads(line) text = payload.get("text", "") text = normalizer.normalize(text, language="ja") if not text: num_empty += 1 continue fp.write(text + "\n") num_samples += 1 except Exception: os.unlink(tmp_file.name) raise return num_samples, num_empty, Path(tmp_file.name) def train_tokenizer(args: argparse.Namespace) -> None: manifests = [Path(m).expanduser().resolve() for m in args.manifest] missing = [str(p) for p in manifests if not p.exists()] if missing: raise FileNotFoundError(f"Missing manifest(s): {', '.join(missing)}") output_prefix = args.output_prefix.expanduser().resolve() output_prefix.parent.mkdir(parents=True, exist_ok=True) num_samples, num_empty, corpus_path = iter_texts(manifests) if num_samples == 0: raise RuntimeError("No non-empty samples found. Cannot train tokenizer.") spm_kwargs = { "input": str(corpus_path), "model_prefix": str(output_prefix), "vocab_size": args.vocab_size, "character_coverage": args.character_coverage, "model_type": args.model_type, "bos_id": 0, "eos_id": 1, "unk_id": 2, "pad_id": -1, "input_sentence_size": args.input_sentence_size, "shuffle_input_sentence": True, "byte_fallback": args.byte_fallback, "train_extremely_large_corpus": True, } print(f"[Tokenizer] Training on {num_samples} samples (skipped {num_empty}).") try: spm.SentencePieceTrainer.train(**spm_kwargs) finally: corpus_path.unlink(missing_ok=True) model_path = output_prefix.with_suffix(".model") vocab_path = output_prefix.with_suffix(".vocab") print(f"[Tokenizer] Saved SentencePiece model to: {model_path}") print(f"[Tokenizer] Saved vocabulary to: {vocab_path}") def main() -> int: args = parse_args() try: train_tokenizer(args) except KeyboardInterrupt: print("Interrupted!", file=sys.stderr) return 130 return 0 if __name__ == "__main__": raise SystemExit(main())