feat: add expressive offline Russian book TTS

This commit is contained in:
Курнат Андрей
2026-07-21 21:28:48 +03:00
parent 28312e3fbc
commit cf82ab61eb
47 changed files with 127843 additions and 124 deletions
+87
View File
@@ -0,0 +1,87 @@
#!/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 = ("{{", "}}", "[[", "]]", "<ref", "</", "{|", "|}", "http://", "https://")
RUSSIAN = re.compile(r"[А-Яа-яЁё]")
LETTERS = re.compile(r"[^\W\d_]", re.UNICODE)
def percentile(sorted_values: list[int], fraction: float) -> 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)