Files
model-training/tts/piper/build_dataset.py
T
2026-07-19 23:52:25 +04:00

90 lines
3.0 KiB
Python

"""
build the piper LJSpeech dataset from the teacher-generated dataset.
dataset/metadata.csv (file_path,text,mood,emotion_id) + 24kHz wavs
→ piper/dataset/wav/<id>.wav (22050 Hz mono)
→ piper/dataset/metadata.csv (id|stressed_text)
stress convention: the mood lists / teacher text use "+VOWEL" (plus before the
stressed vowel). espeak-ng — piper's phonemizer — wants the COMBINING ACUTE
(U+0301) AFTER the vowel. we convert here. (the uppercase-vowel form in
generate_synthetic_voice.py's plus_to_acute is for the qwen/OmniVoice teacher,
NOT espeak — don't reuse it.)
run: python piper/build_dataset.py # build
python piper/build_dataset.py --check # self-check only
requires: pip install soundfile librosa
"""
import csv
import sys
import argparse
from pathlib import Path
ACUTE = "́" # combining acute accent
VOWELS = set("аеёиоуыэюяАЕЁИОУЫЭЮЯ")
SRC_META = Path("tts/dataset/metadata.csv")
OUT_DIR = Path("tts/piper/dataset")
TARGET_SR = 22050
def plus_to_espeak(text: str) -> str:
"""'+VOWEL' -> 'VOWEL' + U+0301 (espeak-ng stress). Leaves other '+' intact."""
out = []
i = 0
while i < len(text):
if text[i] == "+" and i + 1 < len(text) and text[i + 1] in VOWELS:
out.append(text[i + 1])
out.append(ACUTE)
i += 2
else:
out.append(text[i])
i += 1
return "".join(out)
def build():
import soundfile as sf
import librosa
wav_out = OUT_DIR / "wav"
wav_out.mkdir(parents=True, exist_ok=True)
rows = list(csv.DictReader(SRC_META.open(encoding="utf-8")))
meta_lines, n = [], 0
for r in rows:
src = Path(r["file_path"])
if not src.exists():
print(f" [!] missing wav, skipping: {src}")
continue
# id: <mood>-<stem> so ids stay unique across per-mood NNNN.wav collisions
wid = f"{r['mood']}-{src.stem}"
audio, sr = sf.read(str(src))
if audio.ndim > 1:
audio = audio.mean(axis=1)
if sr != TARGET_SR:
audio = librosa.resample(audio, orig_sr=sr, target_sr=TARGET_SR)
sf.write(str(wav_out / f"{wid}.wav"), audio, TARGET_SR)
meta_lines.append(f"{wid}|{plus_to_espeak(r['text'])}")
n += 1
(OUT_DIR / "metadata.csv").write_text("\n".join(meta_lines) + "\n", encoding="utf-8")
print(f"[✓] {n} samples → {OUT_DIR} (wav {TARGET_SR}Hz, metadata id|text)")
def check():
assert plus_to_espeak("з+амок") == "за" + ACUTE + "мок"
assert plus_to_espeak("сто+ят") == "стоя" + ACUTE + "т"
assert plus_to_espeak("нет плюса") == "нет плюса" # no stress → untouched
assert plus_to_espeak("1 + 2") == "1 + 2" # '+' not before vowel → kept
print("[✓] plus_to_espeak self-check passed")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--check", action="store_true", help="run self-check only")
args = ap.parse_args()
check()
if not args.check:
build()