Download code/train_b2.py from Modularcomputing/dynamic-accent-preview: direct link, hf CLI and curl.
- Browser
- Download file 29.7 kB
-
https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/85bc6033c5db6b00b7aac86534c1ca73ceb44fef/code/train_b2.py
- Command line
-
hf download hf://Modularcomputing/dynamic-accent-preview@85bc6033c5db6b00b7aac86534c1ca73ceb44fef/code/train_b2.py
-
curl -L -o train_b2.py https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/85bc6033c5db6b00b7aac86534c1ca73ceb44fef/code/train_b2.py
29.7 kB
| """Arm B, Phase 2. CosyVoice2-0.5B (Apache-2.0) + language embeddings + speaker token + GRL speaker classifier. | |
| New in Phase 2 | |
| * data from ~/data2 (24 kHz, forced-aligned, pause-normalised; long WAXAL clips segmented; BibleTTS Hausa; CS pairs) | |
| * in-context prompts during LM training (p=0.5): [sos, prompt_text+text, spk, task, prompt_speech, speech] with | |
| loss on the target speech only -> at inference the anchor clip steers prosody and pause style | |
| * keep-best on validation CE (every --val_every steps), cosine LR, early stop | |
| * flow-matching decoder fine-tune on 24 kHz studio sources (tones / pitch / timing detail) | |
| * synthesis: anchor prompt at LM and flow, sentence-level generation for multi-sentence text with anchor-matched | |
| inter-sentence pauses, N candidates per text (seed + prompt clip vary) | |
| python train_b2.py lm --out ~/runs/p2_lm --minutes 90 | |
| python train_b2.py flow --out ~/runs/p2_flow --minutes 60 | |
| python train_b2.py synth --ckpt ~/runs/p2_lm/best.pt [--flow ~/runs/p2_flow/best.pt] --out DIR --n 4 | |
| """ | |
| import argparse, inspect, json, math, os, random, re, sys, time | |
| import numpy as np, torch, torch.nn as nn, torch.nn.functional as F, soundfile as sf, librosa | |
| HOME = os.path.expanduser("~") | |
| sys.path[:0] = [f"{HOME}/CosyVoice", f"{HOME}/CosyVoice/third_party/Matcha-TTS", f"{HOME}/da"] | |
| from train_b import load_cosy, text_tokens, DALM, LANGS, L2I, MODEL_DIR, token2wav # noqa: E402 | |
| from da.model import grad_reverse # noqa: E402 | |
| from da.text import segments # noqa: E402 | |
| D2 = os.path.expanduser(os.environ.get("P2OUT", f"{HOME}/data2")) | |
| ANCHOR = "waxal_yor_4" | |
| # ------------------------------------------------------------------ model | |
| class DALM2(DALM): | |
| def build2(self, ids, langs, spk, sp=None, pids=None, plangs=None, psp=None): | |
| """[sos, (prompt_text) text, spk, task, (prompt_speech) speech] -> embeddings, n_text_total, n_prompt_speech""" | |
| sos = self.q.llm_embedding.weight[self.q.sos][None] | |
| task = self.q.llm_embedding.weight[self.q.task_id][None] | |
| if pids is not None: | |
| ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs]) | |
| parts = [sos, self.embed_text(ids, langs), self.spk_emb(spk)[None], task] | |
| npsp = 0 | |
| if psp is not None: | |
| parts.append(self.q.speech_embedding(psp)); npsp = len(psp) | |
| if sp is not None: | |
| parts.append(self.q.speech_embedding(sp)) | |
| return torch.cat(parts, 0), len(ids), npsp | |
| def forward2(self, batch, adv_lambda=1.0): | |
| seqs, tgts, tpos, spks = [], [], [], [] | |
| for b in batch: | |
| ids, langs, spk, sp = b[:4] | |
| pr = b[4] if len(b) > 4 else None | |
| x, nt, npsp = self.build2(ids, langs, spk, sp, *(pr if pr else (None, None, None))) | |
| tgt = torch.full((x.shape[0] + 1,), -100, device=x.device, dtype=torch.long) | |
| start = 1 + nt + 2 + npsp - 1 # position whose output predicts speech[0] | |
| tgt[start: start + len(sp)] = sp | |
| tgt[start + len(sp)] = self.V | |
| seqs.append(x); tgts.append(tgt[:x.shape[0]]); tpos.append((1, 1 + nt)); spks.append(int(spk)) | |
| T = max(s.shape[0] for s in seqs) | |
| D = seqs[0].shape[1] | |
| X = seqs[0].new_zeros(len(seqs), T, D) | |
| Y = torch.full((len(seqs), T), -100, device=X.device, dtype=torch.long) | |
| M = torch.zeros(len(seqs), T, device=X.device, dtype=torch.long) | |
| for i, (s, t) in enumerate(zip(seqs, tgts)): | |
| X[i, :len(s)] = s; Y[i, :len(t)] = t; M[i, :len(s)] = 1 | |
| H = self.hidden(X, M) | |
| logits = self.q.llm_decoder(H) | |
| ce = F.cross_entropy(logits.float().reshape(-1, logits.shape[-1]), Y.reshape(-1), ignore_index=-100) | |
| valid = Y != -100 | |
| acc = (logits.argmax(-1)[valid] == Y[valid]).float().mean() | |
| hs, ys = [], [] | |
| for i, (a, b) in enumerate(tpos): | |
| hs.append(H[i, a:b]); ys.append(torch.full((b - a,), spks[i], device=X.device)) | |
| hs = torch.cat(hs); ys = torch.cat(ys) | |
| al = self.adv(grad_reverse(hs.float(), adv_lambda)) | |
| adv = F.cross_entropy(al, ys) | |
| return ce, acc, adv, (al.argmax(-1) == ys).float().mean() | |
| def generate2(self, ids, langs, spk, prompt=None, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0, | |
| min_ratio=2.0, max_ratio=20.0): | |
| g = torch.Generator(device=ids.device).manual_seed(seed) | |
| x, _, _ = self.build2(ids, langs, spk, None, *(prompt if prompt else (None, None, None))) | |
| x = x[None] | |
| n = len(ids) | |
| min_len, max_len = int(n * min_ratio), int(n * max_ratio) | |
| past, out, inp = None, [], x | |
| for i in range(max_len): | |
| o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True) | |
| past = o.past_key_values | |
| logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0] | |
| if i < min_len: | |
| logp[self.V] = -float("inf") | |
| logp[self.V + 1:] = -float("inf") | |
| p = logp.exp() | |
| sp, si = p.sort(descending=True) | |
| keep = (sp.cumsum(0) - sp) < top_p | |
| keep[top_k:] = False | |
| cp, ci = sp[keep], si[keep] | |
| tokid = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)]) | |
| if out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V: | |
| tokid = int(torch.multinomial(p, 1, generator=g)) | |
| if tokid >= self.V: | |
| break | |
| out.append(tokid) | |
| inp = self.q.speech_embedding.weight[tokid][None, None] | |
| return out | |
| # ------------------------------------------------------------------ data | |
| def load_items(datasets, split, max_tok=500, flow=False): | |
| items, mm = [], {} | |
| for ds in datasets: | |
| od = f"{D2}/{ds}" | |
| if not os.path.exists(f"{od}/s3tok.jsonl"): | |
| print("missing", ds, flush=True); continue | |
| mm[ds] = np.memmap(f"{od}/s3tok.i32", dtype=np.int32, mode="r") | |
| tk = [json.loads(l) for l in open(f"{od}/s3tok.jsonl")] | |
| for k, l in enumerate(open(f"{od}/meta.jsonl", encoding="utf-8")): | |
| m = json.loads(l) | |
| t = tk[k] | |
| assert t["id"] == m["id"] | |
| if m["split"] != split: | |
| continue | |
| if flow and not m.get("flow_ok"): | |
| continue | |
| m["toff"], m["ntok"], m["row"] = t["toff"], t["ntok"], k | |
| if 5 < m["ntok"] <= max_tok: | |
| items.append(m) | |
| return items, mm | |
| def tagged(m): | |
| return m["text"] if m["lang"] == "cs" else f"<{m['lang']}>{m['text']}</{m['lang']}>" | |
| DS_W = {"waxal": 2.0, "bible_yor": 0.5, "bible_hau": 0.6, "fleurs": 1.0, "slr86": 1.0, "slr70": 1.0, "cs_pairs": 1.0} | |
| def batches(items, budget, rng, extra_w=None, cost=None): | |
| idx = [] | |
| for i, m in enumerate(items): | |
| w = DS_W.get(m["ds"], 1.0) * (extra_w(m) if extra_w else 1.0) | |
| idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else []) | |
| cost = cost or (lambda m: m["ntok"] + len(m["text"]) // 2 + 4) | |
| idx.sort(key=lambda i: cost(items[i]) + rng.random() * 10) | |
| out, cur, mx = [], [], 0 | |
| for i in idx: | |
| L = cost(items[i]) | |
| if cur and max(mx, L) * (len(cur) + 1) > budget: | |
| out.append(cur); cur, mx = [], 0 | |
| cur.append(i); mx = max(mx, L) | |
| if cur: | |
| out.append(cur) | |
| rng.shuffle(out) | |
| return out | |
| # ------------------------------------------------------------------ LM training | |
| def train_lm(a): | |
| from transformers import AutoTokenizer | |
| tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN") | |
| cosy = load_cosy() | |
| dss = a.datasets.split(",") | |
| items, mm = load_items(dss, "train") | |
| val_all, _ = load_items(dss, "val") | |
| if a.anchor_only_w > 0: | |
| print("anchor items", sum(m["spk"] == ANCHOR for m in items), flush=True) | |
| spks = sorted({m["spk"] for m in items}) | |
| if a.init: | |
| ck = torch.load(a.init, map_location="cpu") | |
| spk2id = json.load(open(os.path.join(os.path.dirname(a.init), "spk2id.json"))) | |
| for s in spks: | |
| if s not in spk2id: | |
| spk2id[s] = len(spk2id) | |
| else: | |
| spk2id = {s: i for i, s in enumerate(spks)} | |
| n_spk = max(len(spk2id), ck["n_spk"]) if a.init else len(spk2id) | |
| # balanced val: up to 60 per language (+ anchor), never more than 15 per speaker | |
| rng = random.Random(1); rng.shuffle(val_all) | |
| val, per_l, per_s = [], {}, {} | |
| for m in val_all: | |
| if m["spk"] not in spk2id or m["lang"] == "cs": | |
| continue | |
| L = "anchor" if m["spk"] == ANCHOR else m["lang"] | |
| if per_l.get(L, 0) >= 60 or per_s.get(m["spk"], 0) >= 15: | |
| continue | |
| per_l[L] = per_l.get(L, 0) + 1; per_s[m["spk"]] = per_s.get(m["spk"], 0) + 1; val.append(m) | |
| by_spk = {} | |
| for m in items: | |
| if m["ntok"] <= 160 and m["lang"] != "cs": | |
| by_spk.setdefault(m["spk"], []).append(m) | |
| os.makedirs(a.out, exist_ok=True) | |
| json.dump(spk2id, open(f"{a.out}/spk2id.json", "w")) | |
| print("lm items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "spk", len(spk2id), | |
| "val", len(val), per_l, flush=True) | |
| model = DALM2(cosy.model.llm, n_spk).cuda().train() | |
| if a.init: | |
| sd = ck["model"] | |
| if sd["spk_emb.weight"].shape[0] < n_spk: | |
| for k in ["spk_emb.weight"]: | |
| w = model.state_dict()[k].clone(); w[:sd[k].shape[0]] = sd[k]; sd[k] = w | |
| for k in ["adv.2.weight", "adv.2.bias"]: | |
| w = model.state_dict()[k].clone(); w[:sd[k].shape[0]] = sd[k]; sd[k] = w | |
| print(model.load_state_dict(sd, strict=False), flush=True) | |
| for p in cosy.model.flow.parameters(): | |
| p.requires_grad = False | |
| for p in cosy.model.hift.parameters(): | |
| p.requires_grad = False | |
| new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters()) | |
| newids = {id(p) for p in new} | |
| old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad] | |
| opt = torch.optim.AdamW([{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}], weight_decay=0.01) | |
| base = [a.lr, a.lr_new] | |
| cache = {} | |
| def tt(text, key): | |
| if key not in cache: | |
| cache[key] = text_tokens(tok, text) | |
| return cache[key] | |
| def enc(m, prng=None, force_prompt=None): | |
| ids, langs = tt(tagged(m), m["id"]) | |
| sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64) | |
| r = [torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[m["spk"]]).cuda(), | |
| torch.from_numpy(sp).cuda()] | |
| use = force_prompt if force_prompt is not None else (prng is not None and prng.random() < a.p_prompt) | |
| if use: | |
| c = by_spk.get(m["spk"], []) | |
| c = [x for x in (prng or random).sample(c, min(6, len(c))) if x["id"] != m["id"]] if c else [] | |
| if c and c[0]["ntok"] + m["ntok"] <= 600: | |
| p = c[0] | |
| pids, plangs = tt(tagged(p), p["id"]) | |
| psp = np.asarray(mm[p["ds"]][p["toff"]: p["toff"] + p["ntok"]], dtype=np.int64) | |
| r.append((torch.tensor(pids).cuda(), torch.tensor(plangs).cuda(), torch.from_numpy(psp).cuda())) | |
| return r | |
| def validate(): | |
| model.eval(); out = {} | |
| with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16): | |
| for mode in (False, True): | |
| vr = random.Random(7); L, W = [], [] | |
| for i in range(0, len(val), 16): | |
| bt = val[i:i + 16] | |
| ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=mode) for m in bt]) | |
| L.append(float(ce) * len(bt)); W.append(len(bt)) | |
| out["p" if mode else "np"] = sum(L) / sum(W) | |
| # anchor-only val | |
| av = [m for m in val if m["spk"] == ANCHOR] | |
| if av: | |
| vr = random.Random(7) | |
| ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=True) for m in av[:32]]) | |
| out["anchor_p"] = float(ce) | |
| model.train() | |
| out["mean"] = (out["np"] + out["p"]) / 2 | |
| return out | |
| extra = (lambda m: a.anchor_only_w if m["spk"] == ANCHOR else 1.0) if a.anchor_only_w > 0 else None | |
| rng = random.Random(0); prng = random.Random(3) | |
| step, nskip, t0 = 0, 0, time.time() | |
| logf = open(f"{a.out}/log.jsonl", "a") | |
| v = validate(); best = v["mean"] if not a.select_anchor else 0.5 * (v["mean"] + v.get("anchor_p", v["mean"])); bad = 0 | |
| logf.write(json.dumps({"step": 0, "val": v}) + "\n"); print(json.dumps({"step": 0, "val": v}), flush=True) | |
| torch.save(dict(model=model.state_dict(), step=0, n_spk=n_spk, val=v), f"{a.out}/best.pt") | |
| total = a.max_steps | |
| done = False | |
| while not done: | |
| for bi in batches(items, a.tokens, rng, extra, cost=lambda m: m["ntok"] + len(m["text"]) // 2 + 4 + int(a.p_prompt * 200)): | |
| lrm = min(1.0, (step + 1) / a.warmup) * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * min(1.0, step / total)))) | |
| for g, b in zip(opt.param_groups, base): | |
| g["lr"] = b * lrm | |
| batch = [enc(items[i], prng) for i in bi] | |
| # keep the padded length inside the budget when prompts were added | |
| with torch.autocast("cuda", dtype=torch.bfloat16): | |
| ce, acc, adv, adv_acc = model.forward2(batch, adv_lambda=min(1.0, step / 1000)) | |
| loss = ce + a.adv_w * adv | |
| opt.zero_grad(set_to_none=True) | |
| if not torch.isfinite(loss): | |
| nskip += 1; print(json.dumps({"skip_nonfinite_fwd": step, "ce": float(ce), "adv": float(adv)}), flush=True) | |
| continue | |
| loss.backward() | |
| gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0) | |
| if not torch.isfinite(gn): | |
| nskip += 1; opt.zero_grad(set_to_none=True); print(json.dumps({"skip_nonfinite_grad": step}), flush=True) | |
| continue | |
| opt.step(); step += 1 | |
| if step % 25 == 0: | |
| r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), adv=round(float(adv), 3), | |
| adv_acc=round(float(adv_acc), 3), gn=round(float(gn), 2), B=len(bi), lr=round(lrm * a.lr, 9), | |
| skips=nskip, min=round((time.time() - t0) / 60, 2)) | |
| print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush() | |
| if step % a.val_every == 0: | |
| v = validate() | |
| score = v["mean"] if not a.select_anchor else 0.5 * (v["mean"] + v.get("anchor_p", v["mean"])) | |
| imp = score < best - 1e-4 | |
| rec = {"step": step, "val": v, "best": imp} | |
| print(json.dumps(rec), flush=True); logf.write(json.dumps(rec) + "\n"); logf.flush() | |
| if imp: | |
| best, bad = score, 0 | |
| torch.save(dict(model=model.state_dict(), step=step, n_spk=n_spk, val=v), f"{a.out}/best.pt") | |
| else: | |
| bad += 1 | |
| if bad >= a.patience: | |
| print("EARLY_STOP", step, flush=True); done = True; break | |
| if (time.time() - t0) / 60 > a.minutes or step >= total: | |
| done = True; break | |
| print("TRAIN_LM_DONE", step, "best", best, "skips", nskip, flush=True) | |
| # ------------------------------------------------------------------ flow training | |
| def train_flow(a): | |
| cosy = load_cosy() | |
| flow = cosy.model.flow | |
| if a.flow_init: | |
| flow.load_state_dict(torch.load(a.flow_init, map_location="cuda")["flow"]) | |
| flow.train() | |
| for p in cosy.model.llm.parameters(): | |
| p.requires_grad = False | |
| fe = cosy.frontend.feat_extractor | |
| dss = a.datasets.split(",") | |
| items, mm = load_items(dss, "train", max_tok=400, flow=True) | |
| val, _ = load_items(dss, "val", max_tok=400, flow=True) | |
| rng0 = random.Random(1); rng0.shuffle(val); val = val[:256] | |
| pcm = {ds: np.memmap(f"{D2}/{ds}/pcm24k.i16", dtype=np.int16, mode="r") for ds in dss if os.path.exists(f"{D2}/{ds}/pcm24k.i16")} | |
| emb = {ds: np.load(f"{D2}/{ds}/campplus.npy", mmap_mode="r") for ds in dss if os.path.exists(f"{D2}/{ds}/campplus.npy")} | |
| os.makedirs(a.out, exist_ok=True) | |
| print("flow items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "val", len(val), flush=True) | |
| extra = (lambda m: a.anchor_only_w if m["spk"] == ANCHOR else 1.0) if a.anchor_only_w > 0 else None | |
| def make(bt): | |
| toks, feats, es = [], [], [] | |
| for m in bt: | |
| y = torch.from_numpy(np.asarray(pcm[m["ds"]][m["off24"]: m["off24"] + m["n24"]], dtype=np.float32) / 32768.0) | |
| n = int(math.ceil(len(y) / 960)) * 960 | |
| y = F.pad(y, (0, n - len(y)))[None].cuda() | |
| ft = fe(y).squeeze(0).transpose(0, 1) # [T,80] | |
| tk = torch.from_numpy(np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)) | |
| k = min(ft.shape[0] // 2, len(tk)) | |
| toks.append(tk[:k]); feats.append(ft[:2 * k]); es.append(torch.from_numpy(np.array(emb[m["ds"]][m["row"]]))) | |
| tl = torch.tensor([len(t) for t in toks], dtype=torch.int32) | |
| fl = torch.tensor([len(f) for f in feats], dtype=torch.int32) | |
| T = int(tl.max()); Fm = int(fl.max()) | |
| tok = torch.zeros(len(bt), T, dtype=torch.long); feat = torch.zeros(len(bt), Fm, 80) | |
| for i, (t, f) in enumerate(zip(toks, feats)): | |
| tok[i, :len(t)] = t; feat[i, :len(f)] = f.cpu() | |
| return dict(speech_token=tok, speech_token_len=tl, speech_feat=feat, speech_feat_len=fl, | |
| embedding=torch.stack(es).float()) | |
| params = [p for p in flow.parameters() if p.requires_grad] | |
| opt = torch.optim.AdamW(params, lr=a.lr, weight_decay=0.0) | |
| logf = open(f"{a.out}/log.jsonl", "a") | |
| def validate(): | |
| flow.eval(); L = [] | |
| st = torch.random.get_rng_state(); cst = torch.cuda.get_rng_state(); pst = random.getstate() | |
| torch.manual_seed(0); random.seed(0) | |
| with torch.no_grad(): | |
| for i in range(0, len(val), 16): | |
| L.append(float(flow(make(val[i:i + 16]), torch.device("cuda"))["loss"])) | |
| torch.random.set_rng_state(st); torch.cuda.set_rng_state(cst); random.setstate(pst) | |
| flow.train(); return float(np.mean(L)) | |
| v = validate(); best, bad = v, 0 | |
| print(json.dumps({"step": 0, "val": v}), flush=True); logf.write(json.dumps({"step": 0, "val": v}) + "\n") | |
| torch.save(dict(flow=flow.state_dict(), step=0, val=v), f"{a.out}/best.pt") | |
| rng = random.Random(0); step, t0, done = 0, time.time(), False | |
| while not done: | |
| for bi in batches(items, a.tokens, rng, extra, cost=lambda m: m["ntok"]): | |
| lrm = min(1.0, (step + 1) / a.warmup) | |
| for g in opt.param_groups: | |
| g["lr"] = a.lr * lrm | |
| with torch.autocast("cuda", dtype=torch.bfloat16): | |
| loss = flow(make([items[i] for i in bi]), torch.device("cuda"))["loss"] | |
| opt.zero_grad(set_to_none=True) | |
| if not torch.isfinite(loss): | |
| continue | |
| loss.backward() | |
| gn = torch.nn.utils.clip_grad_norm_(params, 5.0) | |
| if not torch.isfinite(gn): | |
| continue | |
| opt.step(); step += 1 | |
| if step % 50 == 0: | |
| r = dict(step=step, loss=round(float(loss), 4), gn=round(float(gn), 2), B=len(bi), min=round((time.time() - t0) / 60, 2)) | |
| print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush() | |
| if step % a.val_every == 0: | |
| v = validate(); imp = v < best - 1e-4 | |
| print(json.dumps({"step": step, "val": v, "best": imp}), flush=True) | |
| logf.write(json.dumps({"step": step, "val": v, "best": imp}) + "\n"); logf.flush() | |
| if imp: | |
| best, bad = v, 0 | |
| torch.save(dict(flow=flow.state_dict(), step=step, val=v), f"{a.out}/best.pt") | |
| else: | |
| bad += 1 | |
| if bad >= a.patience: | |
| done = True; break | |
| if (time.time() - t0) / 60 > a.minutes or step >= a.max_steps: | |
| done = True; break | |
| print("TRAIN_FLOW_DONE", step, "best", best, flush=True) | |
| # ------------------------------------------------------------------ synthesis | |
| _SENT = re.compile(r"(?<=[.?!])\s+") | |
| def split_sentences(text): | |
| """tagged text -> list of tagged sentences. A sentence ends at . ? ! inside a span or at the end of a span; | |
| language spans are cut there and re-tagged, so a sentence keeps its in-sentence switches.""" | |
| out, cur = [], [] | |
| for l, s in segments(text): | |
| parts = _SENT.split(s.strip()) | |
| for k, p in enumerate(parts): | |
| p = p.strip() | |
| if p: | |
| cur.append(f"<{l}>{p}</{l}>") | |
| if (k < len(parts) - 1 or p[-1:] in ".?!") and cur: | |
| out.append(" ".join(cur)); cur = [] | |
| if cur: | |
| out.append(" ".join(cur)) | |
| return [o for o in out if o.strip()] | |
| def trim_edges(w, sr, keep_lead=0.12, keep_tail=0.18): | |
| from da.p2common import silence_mask | |
| sil = silence_mask(w, sr) | |
| sp = np.where(~sil)[0] | |
| if not len(sp): | |
| return w | |
| a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01 | |
| out = w[int(max(0, a - keep_lead) * sr): int(min(len(w) / sr, b + keep_tail) * sr)] | |
| lead = a - max(0, a - keep_lead) | |
| if lead < keep_lead: | |
| out = np.concatenate([np.zeros(int((keep_lead - lead) * sr), out.dtype), out]) | |
| return out | |
| def cap_internal(w, sr, cap=0.6): | |
| """shorten every internal silence longer than cap seconds to cap (the centre of the silent run is removed).""" | |
| from da.p2common import silence_mask, runs | |
| sil = silence_mask(w, sr) | |
| hop = int(0.01 * sr) | |
| R = [(a, b) for a, b in runs(sil) if a > 0 and b < len(sil) and (b - a) * 0.01 > cap] | |
| if not R: | |
| return w | |
| keep, cur = [], 0 | |
| for a, b in R: | |
| cut0 = a * hop + int(cap / 2 * sr) | |
| cut1 = b * hop - int(cap / 2 * sr) | |
| keep.append(w[cur:cut0]); cur = cut1 | |
| keep.append(w[cur:]) | |
| return np.concatenate(keep) | |
| class Synth: | |
| def __init__(self, ckpt, flow_ckpt=None, sent_pause=0.5): | |
| from transformers import AutoTokenizer | |
| self.tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN") | |
| self.cosy = load_cosy() | |
| if flow_ckpt: | |
| self.cosy.model.flow.load_state_dict(torch.load(flow_ckpt, map_location="cuda")["flow"]) | |
| ck = torch.load(ckpt, map_location="cuda") | |
| self.spk2id = json.load(open(os.path.join(os.path.dirname(ckpt), "spk2id.json"))) | |
| self.m = DALM2(self.cosy.model.llm, ck["n_spk"]).cuda().eval() | |
| self.m.load_state_dict(ck["model"]) | |
| self.sent_pause = sent_pause | |
| self.cap = 0.6 | |
| # anchor prompts: normalised anchor val clips from data2 (never in train), 3-7 s, highest SNR first | |
| meta = [json.loads(l) for l in open(f"{D2}/waxal/meta.jsonl", encoding="utf-8")] | |
| tk = [json.loads(l) for l in open(f"{D2}/waxal/s3tok.jsonl")] | |
| pcm = np.memmap(f"{D2}/waxal/pcm24k.i16", dtype=np.int16, mode="r") | |
| s3 = np.memmap(f"{D2}/waxal/s3tok.i32", dtype=np.int32, mode="r") | |
| cand = [(m, t) for m, t in zip(meta, tk) if m["spk"] == ANCHOR and m["split"] == "val" and 3.0 <= m["dur"] <= 7.0 | |
| and not m.get("longform")] | |
| cand.sort(key=lambda x: -x[0]["ascore"]) | |
| self.prompts = [] | |
| os.makedirs(f"{HOME}/eval/prompts", exist_ok=True) | |
| for m, t in cand[:4]: | |
| y = np.asarray(pcm[m["off24"]: m["off24"] + m["n24"]], dtype=np.float32) / 32768.0 | |
| p = f"{HOME}/eval/prompts/{m['id']}.wav"; sf.write(p, y, 24000) | |
| from train_b import prompt_feats | |
| ptok, pfeat, pemb = prompt_feats(self.cosy, p) | |
| ids, langs = text_tokens(self.tok, tagged(m)) | |
| lmp = (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), | |
| torch.from_numpy(np.asarray(s3[t["toff"]: t["toff"] + t["ntok"]], dtype=np.int64)).cuda()) | |
| self.prompts.append(dict(id=m["id"], text=m["text"], lm=lmp, flow=(ptok, pfeat, pemb))) | |
| print("anchor prompts", [p["id"] for p in self.prompts], flush=True) | |
| def one(self, tagged_text, seed=0, prompt_k=0, use_lm_prompt=True): | |
| ids, langs = text_tokens(self.tok, tagged_text) | |
| P = self.prompts[prompt_k % len(self.prompts)] | |
| with torch.autocast("cuda", dtype=torch.bfloat16): | |
| sp = self.m.generate2(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), | |
| torch.tensor(self.spk2id[ANCHOR]).cuda(), prompt=P["lm"] if use_lm_prompt else None, | |
| seed=seed) | |
| if len(sp) < 3: | |
| sp = sp + [0] * 3 | |
| self.last_tokens.append(sp) | |
| return token2wav(self.cosy, sp, *P["flow"]) | |
| def join(self, ws): | |
| sr = self.cosy.sample_rate | |
| if len(ws) == 1: | |
| return ws[0] | |
| out = [] | |
| for i, w in enumerate(ws): | |
| if i: | |
| out.append(np.zeros(max(0, int((self.sent_pause - 0.30) * sr)), np.float32)) | |
| out.append(w) | |
| return np.concatenate(out) | |
| def redecode(self, toks, prompt_k=0): | |
| """same LM tokens, (possibly different) flow -> identical post-processing""" | |
| sr = self.cosy.sample_rate | |
| P = self.prompts[prompt_k % len(self.prompts)] | |
| return self.join([cap_internal(trim_edges(token2wav(self.cosy, t, *P["flow"]), sr), sr, self.cap) for t in toks]) | |
| def say(self, tagged_text, seed=0, prompt_k=0, stitch=True, use_lm_prompt=True): | |
| sr = self.cosy.sample_rate | |
| self.last_tokens = [] | |
| sents = split_sentences(tagged_text) if stitch else [tagged_text] | |
| ws = [cap_internal(trim_edges(self.one(s, seed=seed + 1000 * i, prompt_k=prompt_k, use_lm_prompt=use_lm_prompt), sr), sr, self.cap) | |
| for i, s in enumerate(sents)] | |
| # trailing 0.18 + leading 0.12 already present; join() tops up to the anchor's sentence pause | |
| return self.join(ws) | |
| def redecode(a): | |
| """decode the LM tokens saved by synth (--src DIR) with another flow checkpoint -> --out""" | |
| S = Synth(a.ckpt, a.flow or None, a.sent_pause) | |
| os.makedirs(a.out, exist_ok=True) | |
| import glob | |
| for f in sorted(glob.glob(f"{a.src}/*.tok.json")): | |
| d = json.load(open(f)) | |
| i = os.path.basename(f)[:-9] | |
| sf.write(f"{a.out}/{i}.wav", S.redecode(d["tokens"], d["prompt_k"]), S.cosy.sample_rate) | |
| print("REDECODE_DONE", a.out, flush=True) | |
| def synth(a): | |
| S = Synth(a.ckpt, a.flow or None, a.sent_pause) | |
| rows = [json.loads(l) for l in open(a.eval_set, encoding="utf-8")] | |
| if a.only: | |
| keep = set(a.only.split(",")); rows = [r for r in rows if r["id"] in keep or r["lang"] in keep] | |
| if a.nper: | |
| cnt, sel = {}, [] | |
| for r in rows: | |
| if r["lang"] in ("cs", "para") or cnt.get(r["lang"], 0) < a.nper: | |
| sel.append(r); cnt[r["lang"]] = cnt.get(r["lang"], 0) + 1 | |
| rows = sel | |
| ks = [a.k] if a.k >= 0 else list(range(a.n)) | |
| for k in ks: | |
| od = a.out if len(ks) == 1 else f"{a.out}/cand{k}" | |
| os.makedirs(od, exist_ok=True) | |
| for r in rows: | |
| text = r["text"] if r["lang"] in ("cs", "para") else f"<{r['lang']}>{r['text']}</{r['lang']}>" | |
| w = S.say(text, seed=k, prompt_k=k, stitch=not a.no_stitch, use_lm_prompt=not a.no_lm_prompt) | |
| sf.write(f"{od}/{r['id']}.wav", w, S.cosy.sample_rate) | |
| json.dump(dict(tokens=S.last_tokens, prompt_k=k), open(f"{od}/{r['id']}.tok.json", "w")) | |
| print("synth cand", k, "done", od, flush=True) | |
| print("SYNTH2_DONE", a.out, flush=True) | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("cmd") | |
| ap.add_argument("--datasets", default="waxal,fleurs,slr86,slr70,bible_yor,bible_hau,cs_pairs") | |
| ap.add_argument("--out", default=f"{HOME}/runs/p2_lm") | |
| ap.add_argument("--init", default="") | |
| ap.add_argument("--flow_init", default="") | |
| ap.add_argument("--ckpt", default="") | |
| ap.add_argument("--flow", default="") | |
| ap.add_argument("--minutes", type=float, default=90) | |
| ap.add_argument("--max_steps", type=int, default=4000) | |
| ap.add_argument("--lr", type=float, default=1e-5) | |
| ap.add_argument("--lr_new", type=float, default=5e-4) | |
| ap.add_argument("--warmup", type=int, default=300) | |
| ap.add_argument("--tokens", type=int, default=16000) | |
| ap.add_argument("--adv_w", type=float, default=0.02) | |
| ap.add_argument("--p_prompt", type=float, default=0.5) | |
| ap.add_argument("--val_every", type=int, default=200) | |
| ap.add_argument("--patience", type=int, default=5) | |
| ap.add_argument("--anchor_only_w", type=float, default=0.0) | |
| ap.add_argument("--select_anchor", action="store_true") | |
| ap.add_argument("--eval_set", default=f"{HOME}/eval/eval_set2.jsonl") | |
| ap.add_argument("--n", type=int, default=1) | |
| ap.add_argument("--nper", type=int, default=0) | |
| ap.add_argument("--k", type=int, default=-1) | |
| ap.add_argument("--src", default="") | |
| ap.add_argument("--only", default="") | |
| ap.add_argument("--sent_pause", type=float, default=0.6) | |
| ap.add_argument("--no_stitch", action="store_true") | |
| ap.add_argument("--no_lm_prompt", action="store_true") | |
| a = ap.parse_args() | |
| {"lm": train_lm, "flow": train_flow, "synth": synth, "redecode": redecode}[a.cmd](a) | |