""" build the piper LJSpeech dataset from the teacher-generated dataset. dataset/metadata.csv (file_path,text,mood,emotion_id) + 24kHz wavs → piper/dataset/wav/.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: - 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()