#!/usr/bin/env python3 """Audit a BookTTS text corpus and emit reproducible statistics and samples.""" from __future__ import annotations import argparse import hashlib import json import random import re import sys from pathlib import Path MARKUP_PATTERNS = ("{{", "}}", "[[", "]]", " int: if not sorted_values: return 0 return sorted_values[round((len(sorted_values) - 1) * fraction)] def audit(args: argparse.Namespace) -> None: rng = random.Random(args.seed) lengths: list[int] = [] words: list[int] = [] samples: list[str] = [] suspicious = {pattern: 0 for pattern in MARKUP_PATTERNS} low_russian_ratio = 0 with_digits = 0 with_dialogue_dash = 0 digest = hashlib.sha256() with args.corpus.open("rb") as binary: for raw in binary: digest.update(raw) line = raw.decode("utf-8").rstrip("\r\n") index = len(lengths) lengths.append(len(line)); words.append(len(line.split())) if any(char.isdigit() for char in line): with_digits += 1 if line.startswith(("—", "–", "-")): with_dialogue_dash += 1 letters = LETTERS.findall(line) russian = RUSSIAN.findall(line) if letters and len(russian) / len(letters) < 0.75: low_russian_ratio += 1 for pattern in MARKUP_PATTERNS: if pattern in line: suspicious[pattern] += 1 if len(samples) < args.samples: samples.append(line) else: replacement = rng.randint(0, index) if replacement < args.samples: samples[replacement] = line ordered_lengths = sorted(lengths); ordered_words = sorted(words) report = { "corpus": str(args.corpus), "sha256": digest.hexdigest().upper(), "lines": len(lengths), "bytes": args.corpus.stat().st_size, "characters": {"min": min(lengths, default=0), "mean": sum(lengths) / len(lengths) if lengths else 0, "p50": percentile(ordered_lengths, 0.50), "p95": percentile(ordered_lengths, 0.95), "max": max(lengths, default=0)}, "words": {"min": min(words, default=0), "mean": sum(words) / len(words) if words else 0, "p50": percentile(ordered_words, 0.50), "p95": percentile(ordered_words, 0.95), "max": max(words, default=0)}, "with_digits": with_digits, "with_dialogue_dash": with_dialogue_dash, "below_75_percent_russian_letters": low_russian_ratio, "suspicious_markup": suspicious, "sample_seed": args.seed, "samples": samples, } rendered = json.dumps(report, ensure_ascii=False, indent=2) + "\n" if args.output: if args.output.exists(): raise FileExistsError(f"Refusing to overwrite {args.output}") args.output.write_text(rendered, encoding="utf-8") print(rendered, end="") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--corpus", type=Path, required=True) parser.add_argument("--output", type=Path) parser.add_argument("--samples", type=int, default=20) parser.add_argument("--seed", type=int, default=20260721) return parser.parse_args() if __name__ == "__main__": sys.exit(audit(parse_args()) or 0)