90 lines
3.0 KiB
Python
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()
|