File size: 4,446 Bytes
e4c97d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | """Synthesize code-switched ASR training audio with Dynamic Accent Flash-DPO (public-safe, licensed-data TTS).
~/venv/bin/python synth_cs.py <worker> <n_workers> -> /scratch/asr/synth/wav/*.wav + parts/part_<k>.jsonl
Voices: 1 train clip per WAXAL speaker (ibo/yor/hau 1-8, pcm F1-4/M1-4; CC-BY-SA-4.0 Media Trust) as zero-shot prompt + LM speaker id.
Pace: random LM rate token (slow/normal/fast) + random speed perturbation 0.9-1.1. Channel/noise/reverb augmentation is applied on the fly in training."""
import os, sys, json, random, re, unicodedata, time
os.environ.setdefault("CV3_DIR", "/home/dev/models/Fun-CosyVoice3-0.5B-2512")
REL = "/home/dev/rel_flash"; sys.path[:0] = [REL]
import numpy as np, soundfile as sf, librosa, torch
K, NW = int(sys.argv[1]), int(sys.argv[2])
S_DIR = "/scratch/asr/synth"; os.makedirs(f"{S_DIR}/wav", exist_ok=True); os.makedirs(f"{S_DIR}/voices", exist_ok=True); os.makedirs(f"{S_DIR}/parts", exist_ok=True)
import flash_lm
TONE = {"\u0300", "\u0301", "\u0304", "\u0302", "\u030c"}
def norm(s, lang):
s = unicodedata.normalize("NFC", s.lower())
if lang == "ibo": s = unicodedata.normalize("NFC", "".join(c for c in unicodedata.normalize("NFD", s) if c not in TONE))
s = s.replace("\u2019", "'"); s = re.sub(r"[^\w\s']", " ", s); s = re.sub(r"(^|\s)'+|'+(\s|$)", " ", s)
return re.sub(r"\s+", " ", s).strip()
TAG = re.compile(r"<(eng|ibo|yor|hau|pcm)>(.*?)</\1>", re.S)
# voice pool (deterministic, built by every worker identically; worker 0 writes the wavs)
W = "/home/dev/data3/waxal"; meta = [json.loads(l) for l in open(f"{W}/meta.jsonl")]
pcm = np.memmap(f"{W}/pcm24k.i16", dtype=np.int16, mode="r")
rng0 = random.Random(3); by = {}
for r in meta:
if r["split"] == "train" and 4.0 <= r["dur"] <= 9.0 and r.get("ascore", 0) > 0.4: by.setdefault(r["spk"], []).append(r)
voices = {}
for spk in sorted(by):
r = rng0.choice(by[spk]); p = f"{S_DIR}/voices/{spk}.wav"
if not os.path.exists(p):
tmp = p + f".{K}.tmp.wav"; sf.write(tmp, pcm[r["off24"]: r["off24"] + r["n24"]].astype(np.float32) / 32768, 24000); os.replace(tmp, p)
voices[spk] = dict(wav=p, spk=spk, lang=r["lang"], text=f"<{r['lang']}>{r['text']}</{r['lang']}>")
for v, d in voices.items(): flash_lm.VOICES[v] = dict(wav=d["wav"], spk=d["spk"])
S = flash_lm.Synth(f"{REL}/checkpoints/flash/flash_dpo/best.pt")
for v, d in voices.items(): S.vtext[v] = d["text"]
VL = sorted(voices); print("voices", len(VL), VL, flush=True)
lines = [json.loads(l) for l in open(f"{S_DIR}/cs_text.jsonl")]
mine = [l for i, l in enumerate(lines) if i % NW == K]
out = open(f"{S_DIR}/parts/part_{K}.jsonl", "a"); done = {json.loads(l)["id"] for l in open(f"{S_DIR}/parts/part_{K}.jsonl")}
rng = random.Random(100 + K); t0 = time.time(); n = 0
for L in mine:
if L["id"] in done: continue
langs = L["pair"].split("+")
# prefer a voice whose native language is in the pair (natural accent), sometimes any voice (accent variety)
pool = [v for v in VL if voices[v]["lang"] in langs] if rng.random() < 0.75 else VL
v = rng.choice(pool or VL); rate = rng.choice(["slow", "normal", "normal", "normal", "fast"]); seed = rng.randrange(10 ** 6)
try:
toks = S.tokens(L["tagged"], v, rate, seed)[0]
if len(toks) < 10 or len(toks) > 25 * 30: continue
w = S.wav(toks, v); w = w.squeeze().detach().cpu().numpy() if torch.is_tensor(w) else np.asarray(w).squeeze()
except Exception as e:
print("ERR", L["id"], repr(e)[:200], flush=True); continue
y = librosa.resample(w.astype(np.float32), orig_sr=S.cosy.sample_rate, target_sr=16000)
sp = rng.uniform(0.9, 1.1)
if abs(sp - 1) > 0.02: y = librosa.resample(y, orig_sr=16000, target_sr=int(16000 / sp)) # speed perturb (played at 16 kHz)
y = y / (np.abs(y).max() + 1e-6) * rng.uniform(0.5, 0.95)
p = f"{S_DIR}/wav/{L['id']}.wav"; sf.write(p, y, 16000, subtype="PCM_16")
text = " ".join(norm(t, l) for l, t in TAG.findall(L["tagged"]))
out.write(json.dumps(dict(id=L["id"], wav=p, dur=round(len(y) / 16000, 2), lang="cs", src="synth_flash_dpo", text=text, tagged=L["tagged"],
pair=L["pair"], voice=v, rate=rate, speed=round(sp, 3), licence="synthetic (Flash-DPO, licensed-data TTS)"), ensure_ascii=False) + "\n")
out.flush(); n += 1
if n % 20 == 0: print(f"w{K} {n}/{len(mine)} {(time.time() - t0) / n:.2f}s/clip", flush=True)
print("SYNTH_DONE", K, n, flush=True)
|