Dynamic Accent Flash: add Flash-DPO (CosyVoice3) as --version flash (default), diacritic front-end, Flash results, samples
e4ae2bb verified Download flash_lm.py from Modularcomputing/dynamic-accent-preview: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/main/flash_lm.py
- Command line
-
hf download hf://Modularcomputing/dynamic-accent-preview/flash_lm.py
-
curl -L -o flash_lm.py https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/main/flash_lm.py
26.1 kB
| """Dynamic Accent Flash: CosyVoice3 (Fun-CosyVoice3-0.5B-2512, Apache-2.0) LM fine-tune + synthesis. | |
| Same additions as Phase 2/3 (per-token language embedding, learned speaker token after the text, rate embedding added to | |
| the speaker vector, GRL speaker classifier on text positions, in-context same-speaker prompt p=0.5), ported to the CV3 LM: | |
| seq = [sos, "You are a helpful assistant.<|endofprompt|>", (prompt text) text, spk(+rate), task, (prompt speech) speech, eos] | |
| sos/task live in speech_embedding (CV3), eos = speech_token_size+1. Targets are CV3 speech tokens (s3v3.*). | |
| Flow (DiT) + HiFT = CV3 base, zero-shot prompted with the voice clip (no flow fine-tune). | |
| python flash_lm.py lm --out ~/flash/runs/flash_lm --max_steps 12000 --minutes 75 | |
| python flash_lm.py gen --lm CKPT --out DIR [--voice anchor|waxal_ibo_5|waxal_yor_5] [--rate normal] [--rows ROWS] | |
| Release note: in this repo use `infer.py --version flash` (it wraps Synth below). Training needs the Phase 3 data | |
| manifest, which is not shipped; the training code is included for reference. Paths come from COSYVOICE_REPO (CosyVoice | |
| clone with CosyVoice3 support) and CV3_DIR (Fun-CosyVoice3-0.5B-2512 snapshot). | |
| """ | |
| import argparse, json, math, os, random, sys, time, inspect | |
| import numpy as np, torch, torch.nn as nn, torch.nn.functional as F, soundfile as sf, librosa | |
| HOME = os.path.expanduser("~") | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| _CV = os.environ.get("COSYVOICE_REPO") or (f"{_HERE}/CosyVoice" if os.path.isdir(f"{_HERE}/CosyVoice") else f"{HOME}/CosyVoice") | |
| sys.path[:0] = [_CV, f"{_CV}/third_party/Matcha-TTS", f"{_HERE}/code", f"{HOME}/da"] | |
| from da.text import segments # noqa: E402 | |
| from da.model import grad_reverse # noqa: E402 | |
| CV3 = os.environ.get("CV3_DIR") or (f"{_HERE}/pretrained/Fun-CosyVoice3-0.5B-2512" if os.path.isdir(f"{_HERE}/pretrained/Fun-CosyVoice3-0.5B-2512") else f"{HOME}/models/Fun-CosyVoice3-0.5B-2512") | |
| LANGS = ["eng", "ibo", "hau", "yor", "pcm"] | |
| L2I = {l: i for i, l in enumerate(LANGS)} | |
| RATES = [None, "very slow", "slow", "normal", "fast", "very fast"] | |
| R2I = {r: i for i, r in enumerate(RATES)} | |
| TAGS = ("[pause]", "[breath]", "[quick_breath]", "[laughter]") | |
| SIL = {1, 2, 28, 29, 55, 248, 494, 2241, 2242, 2322, 2323} # CV3 FSQ silent/breath tokens (cosyvoice/cli/model.py) | |
| ANCHOR = "waxal_yor_4" | |
| NEW = ["waxal", "igboapi", "open_bible_igbo", "naijas2st_igbo", "igbosyncorp"] | |
| MAN = f"{HOME}/p3t/manifest_p3.jsonl" | |
| PDIR = os.environ.get("DA_PROMPTS") or (f"{_HERE}/prompts" if os.path.isdir(f"{_HERE}/prompts") else f"{HOME}/hf_test/prompts") | |
| VOICES = { | |
| "anchor": dict(wav=f"{PDIR}/clip0_waxal_yor_yor_1258.wav" if os.path.exists(f"{PDIR}/clip0_waxal_yor_yor_1258.wav") | |
| else f"{PDIR}/waxal_yor_yor_1258.wav", spk=ANCHOR), | |
| "waxal_ibo_5": dict(wav=f"{PDIR}/voice_waxal_ibo_5.wav", spk="waxal_ibo_5"), | |
| "waxal_yor_5": dict(wav=f"{PDIR}/voice_waxal_yor_5.wav", spk="waxal_yor_5"), | |
| } | |
| # Flash source re-weighting on top of the Phase 3 manifest weight: less Bible narration and slow phone/dictionary reads | |
| FLASH_W = {"open_bible_igbo": 0.4, "bible_hau": 0.5, "bible_yor": 0.5, "igboapi": 0.6, "naijas2st_igbo": 0.6, | |
| "igbosyn_pseudo": 1.0} | |
| def strip_tags(t): | |
| for g in TAGS: | |
| t = t.replace(g, " ") | |
| return " ".join(t.split()).replace(" ,", ",").replace(" .", ".") | |
| def _sf_load_wav(wav, target_sr, min_sr=16000): | |
| y, sr = sf.read(wav, dtype="float32", always_2d=True) | |
| y = y.mean(1) | |
| if sr != target_sr: | |
| y = librosa.resample(y, orig_sr=sr, target_sr=target_sr) | |
| return torch.from_numpy(y)[None] | |
| def load_cv3(): | |
| import cosyvoice.utils.file_utils as fu | |
| fu.load_wav = _sf_load_wav | |
| import cosyvoice.cli.frontend as cf | |
| cf.load_wav = _sf_load_wav | |
| from cosyvoice.cli.cosyvoice import CosyVoice3 | |
| kw = dict(load_jit=False, load_trt=False, fp16=False) | |
| sig = inspect.signature(CosyVoice3.__init__).parameters | |
| return CosyVoice3(CV3, **{k: v for k, v in kw.items() if k in sig}) | |
| class Tok: | |
| def __init__(self, cosy): | |
| self.t = cosy.frontend.tokenizer | |
| self.prefix = self.t.encode("You are a helpful assistant.") + [151646] | |
| self.cache = {} | |
| def __call__(self, text, default="eng"): | |
| if text in self.cache: | |
| return self.cache[text] | |
| ids, langs = [], [] | |
| for l, s in segments(text, default): | |
| t = self.t.encode(s) | |
| ids += t; langs += [L2I[l]] * len(t) | |
| self.cache[text] = (ids, langs) | |
| return ids, langs | |
| class FLM(nn.Module): | |
| def __init__(self, q, n_spk, prefix, d=896): | |
| super().__init__() | |
| self.q = q | |
| self.lang_emb = nn.Embedding(len(LANGS), d); nn.init.zeros_(self.lang_emb.weight) | |
| self.spk_emb = nn.Embedding(n_spk, d); nn.init.normal_(self.spk_emb.weight, std=0.02) | |
| self.rate_emb = nn.Embedding(len(RATES), d, padding_idx=0); nn.init.zeros_(self.rate_emb.weight) | |
| self.adv = nn.Sequential(nn.Linear(d, 256), nn.ReLU(), nn.Linear(256, n_spk)) | |
| self.V = q.speech_token_size | |
| self.EOS = q.eos_token | |
| self.register_buffer("prefix", torch.tensor(prefix, dtype=torch.long), persistent=False) | |
| def embed_text(self, ids, langs): | |
| return self.q.llm.model.model.embed_tokens(ids) + self.lang_emb(langs) | |
| def spkvec(self, spk): | |
| return self.spk_emb(spk[0]) + self.rate_emb(spk[1]) | |
| def build(self, ids, langs, spk, sp=None, pids=None, plangs=None, psp=None): | |
| E = self.q.speech_embedding.weight | |
| if pids is not None: | |
| ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs]) | |
| pre = self.q.llm.model.model.embed_tokens(self.prefix.to(ids.device)) | |
| parts = [E[self.q.sos][None], pre, self.embed_text(ids, langs), self.spkvec(spk)[None], E[self.q.task_id][None]] | |
| 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(pre), len(ids), npsp | |
| def hidden(self, x, mask): | |
| return self.q.llm.model.model(inputs_embeds=x, attention_mask=mask, return_dict=True).last_hidden_state | |
| def pack(self, batch): | |
| seqs, tgts, tpos, spks = [], [], [], [] | |
| for b in batch: | |
| ids, langs, spk, sp = b[:4] | |
| pr = b[4] if len(b) > 4 else None | |
| x, P, nt, npsp = self.build(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 = P + nt + 2 + npsp | |
| tgt[start: start + len(sp)] = sp | |
| tgt[start + len(sp)] = self.EOS | |
| seqs.append(x); tgts.append(tgt[:x.shape[0]]); tpos.append((1 + P, 1 + P + nt)); spks.append(int(spk[0])) | |
| Tm = max(s.shape[0] for s in seqs); D = seqs[0].shape[1] | |
| X = seqs[0].new_zeros(len(seqs), Tm, D) | |
| Y = torch.full((len(seqs), Tm), -100, device=X.device, dtype=torch.long) | |
| M = torch.zeros(len(seqs), Tm, 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 | |
| return X, Y, M, tpos, spks | |
| def forward2(self, batch, adv_lambda=1.0): | |
| X, Y, M, tpos, spks = self.pack(batch) | |
| 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 = torch.cat([H[i, a:b] for i, (a, b) in enumerate(tpos)]) | |
| ys = torch.cat([torch.full((b - a,), spks[i], device=X.device) for i, (a, b) in enumerate(tpos)]) | |
| al = self.adv(grad_reverse(hs.float(), adv_lambda)) | |
| return ce, acc, F.cross_entropy(al, ys), (al.argmax(-1) == ys).float().mean() | |
| def seq_logp(self, batch): | |
| """sum log p(speech tokens + eos) per item (for DPO).""" | |
| X, Y, M, _, _ = self.pack(batch) | |
| logits = self.q.llm_decoder(self.hidden(X, M)).float() | |
| lp = logits.log_softmax(-1) | |
| valid = Y != -100 | |
| g = lp.gather(-1, Y.clamp(min=0)[..., None])[..., 0] * valid | |
| return g.sum(-1), valid.sum(-1) | |
| def generate(self, ids, langs, spk, prompt=None, B=1, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0, | |
| min_ratio=2.0, max_ratio=30.0, sampler="silexcap", max_tok=900): | |
| g = torch.Generator(device=ids.device).manual_seed(seed) | |
| x, _, _, _ = self.build(ids, langs, spk, None, *(prompt if prompt else (None, None, None))) | |
| inp = x[None].repeat(B, 1, 1) | |
| n = len(ids) | |
| min_len, max_len = int(n * min_ratio), min(int(n * max_ratio), max_tok) | |
| past, outs, done = None, [[] for _ in range(B)], [False] * B | |
| E = self.q.speech_embedding.weight | |
| 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) | |
| eos_lp = logp[:, self.EOS].clone() | |
| logp[:, self.V:] = -float("inf") | |
| if i >= min_len: | |
| logp[:, self.EOS] = eos_lp | |
| P = logp.exp() | |
| nxt = [] | |
| for b in range(B): | |
| if done[b]: | |
| nxt.append(0); continue | |
| p = P[b]; out = outs[b] | |
| sp_, si = p.sort(descending=True) | |
| keep = (sp_.cumsum(0) - sp_) < top_p | |
| keep[top_k:] = False | |
| cp, ci = sp_[keep], si[keep] | |
| t = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)]) | |
| exempt = False | |
| if sampler == "silexcap" and t in SIL: | |
| run = 0 | |
| for u in reversed(out): | |
| if u not in SIL: | |
| break | |
| run += 1 | |
| exempt = run < 25 | |
| if not exempt and out and t != self.EOS and sum(1 for u in out[-win:] if u == t) >= win * tau_r: | |
| t = int(torch.multinomial(p, 1, generator=g)) | |
| if t >= self.V: | |
| done[b] = True; nxt.append(0); continue | |
| out.append(t); nxt.append(t) | |
| if all(done): | |
| break | |
| inp = E[torch.tensor(nxt, device=ids.device)][:, None] | |
| return outs | |
| def load_model(cosy, ckpt, tok): | |
| ck = torch.load(ckpt, map_location="cuda", weights_only=False) | |
| m = FLM(cosy.model.llm, ck["n_spk"], tok.prefix).cuda().eval() | |
| print("load", ckpt, m.load_state_dict(ck["model"], strict=False), flush=True) | |
| return m, json.load(open(os.path.join(os.path.dirname(ckpt), "spk2id.json"))) | |
| # ------------------------------------------------------------------ data | |
| def load_data(splits, max_tok=750, extra_man=None, qual=None, qual_drop=0.1): | |
| rows = [json.loads(l) for l in open(MAN, encoding="utf-8")] | |
| if extra_man and os.path.exists(extra_man): | |
| rows += [json.loads(l) for l in open(extra_man, encoding="utf-8")] | |
| want = {r["id"] for r in rows if r["split"] in splits} | |
| drop = set() | |
| if qual and os.path.exists(qual) and "train" in splits: # drop the bottom qual_drop of UTMOS per source | |
| Q = [json.loads(l) for l in open(qual)] | |
| by = {} | |
| for q in Q: | |
| by.setdefault(q["src"], []).append(q) | |
| for src, L in by.items(): | |
| if src in ("cs_pairs",): | |
| continue | |
| thr = float(np.quantile([q["u22"] for q in L], qual_drop)) | |
| drop |= {q["id"] for q in L if q["u22"] < thr} | |
| print("quality filter drops", len(drop), flush=True) | |
| src_idx, mm = {}, {} | |
| for src in sorted({r["src"] for r in rows}): | |
| if src in NEW: | |
| d = f"{HOME}/p3t/{src}" | |
| elif src == "igbosyn_pseudo": | |
| d = f"{HOME}/flash/data/igbosyn_pseudo" | |
| else: | |
| d = f"{HOME}/data3/{src}" | |
| if not os.path.exists(f"{d}/s3v3.done"): | |
| print("missing v3 tokens", d, flush=True); continue | |
| meta = [json.loads(l) for l in open(f"{d}/meta.jsonl", encoding="utf-8")] | |
| tk = [json.loads(l) for l in open(f"{d}/s3v3.jsonl")] | |
| ix = {} | |
| for k, (m, t) in enumerate(zip(meta, tk)): | |
| assert m["id"] == t["id"] | |
| if m["id"] in want: | |
| ix[m["id"]] = (t["toff"], t["ntok"]) | |
| src_idx[src] = ix | |
| mm[src] = np.memmap(f"{d}/s3v3.i32", dtype=np.int32, mode="r") | |
| items = [] | |
| for r in rows: | |
| if r["split"] not in splits or r["src"] not in src_idx or r["id"] in drop and r["speaker"] != ANCHOR: | |
| continue | |
| ix = src_idx[r["src"]] | |
| if r["id"] not in ix: | |
| continue | |
| toff, ntok = ix[r["id"]] | |
| if not (5 < ntok <= max_tok): | |
| continue | |
| w = float(r.get("weight", 1.0)) * FLASH_W.get(r["src"], 1.0) | |
| items.append(dict(id=r["id"], ds=r["src"], src=r["src"], lang=r["lang"], spk=r["speaker"], split=r["split"], | |
| dur=r["dur"], text=r["text_plain"], tags=r.get("text_tags"), rate=R2I.get(r.get("rate_bin"), 0), | |
| w=w, toff=toff, ntok=ntok)) | |
| return items, mm | |
| def tagged(text, lang): | |
| return text if lang == "cs" else f"<{lang}>{text}</{lang}>" | |
| def batches(items, budget, rng, wfn, cost): | |
| idx = [] | |
| for i, m in enumerate(items): | |
| w = wfn(m) | |
| idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else []) | |
| 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 | |
| VAL_SPLITS = {"val", "validation", "dev"} | |
| def train_lm(a): | |
| cosy = load_cv3(); tok = Tok(cosy) | |
| items, mm = load_data({"train"}, a.max_tok, a.extra_man, a.qual, a.qual_drop) | |
| val_all, mmv = load_data(VAL_SPLITS, a.max_tok) | |
| mm.update(mmv) | |
| spk2id = {} | |
| for s in sorted({m["spk"] for m in items}): | |
| spk2id[s] = len(spk2id) | |
| n_spk = len(spk2id) | |
| 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"] <= 200 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")); json.dump(vars(a), open(f"{a.out}/args.json", "w")) | |
| hrs = {} | |
| for m in items: | |
| hrs[m["src"]] = hrs.get(m["src"], 0) + m["dur"] / 3600 | |
| print("lm items", len(items), "hours", round(sum(hrs.values()), 1), {k: round(v, 1) for k, v in hrs.items()}, | |
| "spk", n_spk, "val", len(val), per_l, flush=True) | |
| model = FLM(cosy.model.llm, n_spk, tok.prefix).cuda().train() | |
| for p in list(cosy.model.flow.parameters()) + list(cosy.model.hift.parameters()): | |
| p.requires_grad = False | |
| new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters()) + list(model.rate_emb.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] | |
| def text_of(m, r): | |
| if m["tags"] and r is not None and r.random() >= a.tag_drop: | |
| return m["tags"] | |
| return strip_tags(m["text"]) | |
| def spk_t(m, r): | |
| rate = m["rate"] if (r is not None and r.random() >= a.rate_drop) else 0 | |
| return torch.tensor([spk2id[m["spk"]], rate]).cuda() | |
| def enc(m, prng=None, force_prompt=None, val_mode=False): | |
| r = None if val_mode else prng | |
| ids, langs = tok(tagged(text_of(m, r), m["lang"])) | |
| sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64) | |
| out = [torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), spk_t(m, r), 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"] <= a.max_tok + 150: | |
| p = c[0] | |
| pids, plangs = tok(tagged(strip_tags(p["text"]), p["lang"])) | |
| psp = np.asarray(mm[p["ds"]][p["toff"]: p["toff"] + p["ntok"]], dtype=np.int64) | |
| out.append((torch.tensor(pids).cuda(), torch.tensor(plangs).cuda(), torch.from_numpy(psp).cuda())) | |
| return out | |
| def validate(): | |
| model.eval(); res = {} | |
| 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, val_mode=True) for m in bt]) | |
| L.append(float(ce) * len(bt)); W.append(len(bt)) | |
| res["p" if mode else "np"] = sum(L) / sum(W) | |
| model.train() | |
| res["mean"] = (res["np"] + res["p"]) / 2 | |
| return res | |
| wfn = lambda m: m["w"] * (a.anchor_w if m["spk"] == ANCHOR else 1.0) # noqa | |
| cost = lambda m: (lambda L: int(L * (1 + L / a.quad)))(m["ntok"] + len(m["text"]) // 2 + 20 + int(a.p_prompt * 200)) # noqa | |
| rng = random.Random(0); prng = random.Random(3) | |
| step, nskip, noom, t0 = 0, 0, 0, time.time() | |
| logf = open(f"{a.out}/log.jsonl", "a") | |
| v = validate(); best = v["mean"]; bad = 0 | |
| print(json.dumps({"step": 0, "val": v}), flush=True); logf.write(json.dumps({"step": 0, "val": v}) + "\n") | |
| total, done = a.max_steps, False | |
| while not done: | |
| for bi in batches(items, a.tokens, rng, wfn, cost): | |
| 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 gp, b in zip(opt.param_groups, base): | |
| gp["lr"] = b * lrm | |
| batch = [enc(items[i], prng) for i in bi] | |
| opt.zero_grad(set_to_none=True) | |
| oom = False | |
| try: | |
| 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 | |
| if not torch.isfinite(loss): | |
| nskip += 1; continue | |
| loss.backward() | |
| except torch.cuda.OutOfMemoryError: | |
| oom = True | |
| if oom: | |
| batch = ce = acc = adv = adv_acc = loss = None | |
| opt.zero_grad(set_to_none=True); torch.cuda.empty_cache(); noom += 1; continue | |
| 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); continue | |
| opt.step(); step += 1 | |
| if step % 50 == 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, oom=noom, 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["mean"] < 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 = v["mean"], 0 | |
| torch.save(dict(model={k: t for k, t in model.state_dict().items()}, 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, "oom", noom, flush=True) | |
| # ------------------------------------------------------------------ synthesis | |
| def prompt_feats(cosy, wav): | |
| fe = cosy.frontend | |
| tokn, _ = fe._extract_speech_token(wav) | |
| feat, _ = fe._extract_speech_feat(wav) | |
| n = min(feat.shape[1] // 2, tokn.shape[1]) | |
| return tokn[:, :n].cuda(), feat[:, :2 * n].cuda(), fe._extract_spk_embedding(wav).cuda() | |
| def token2wav(cosy, tokens, ptok, pfeat, emb): | |
| flow = cosy.model.flow | |
| t = torch.tensor([tokens], dtype=torch.int32).cuda() | |
| mel, _ = flow.inference(token=t, token_len=torch.tensor([t.shape[1]], dtype=torch.int32).cuda(), | |
| prompt_token=ptok.int(), prompt_token_len=torch.tensor([ptok.shape[1]], dtype=torch.int32).cuda(), | |
| prompt_feat=pfeat, prompt_feat_len=torch.tensor([pfeat.shape[1]], dtype=torch.int32).cuda(), | |
| embedding=emb, streaming=False, finalize=True) | |
| w, _ = cosy.model.hift.inference(speech_feat=mel, finalize=True) | |
| return w.squeeze().float().cpu().numpy() | |
| def item_text(r): | |
| return r["text"] if r["lang"] in ("cs", "para") else f"<{r['lang']}>{r['text']}</{r['lang']}>" | |
| class Synth: | |
| def __init__(self, lm_ckpt): | |
| self.cosy = load_cv3(); self.tok = Tok(self.cosy) | |
| self.m, self.spk2id = load_model(self.cosy, lm_ckpt, self.tok) | |
| self.vcache = {} | |
| vt = f"{PDIR}/flash_voices_text.json" | |
| self.vtext = json.load(open(vt if os.path.exists(vt) else f"{_HERE}/voices_text.json", encoding="utf-8")) | |
| def voice(self, name): | |
| if name not in self.vcache: | |
| v = VOICES[name] | |
| ptok, pfeat, emb = prompt_feats(self.cosy, v["wav"]) | |
| ids, langs = self.tok(self.vtext[name]) | |
| lmp = (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), ptok[0].long()) | |
| self.vcache[name] = (ptok, pfeat, emb, lmp, self.spk2id.get(v["spk"], self.spk2id.get(ANCHOR))) | |
| return self.vcache[name] | |
| def tokens(self, text, voice="anchor", rate="normal", seed=0, B=1, sampler="silexcap"): | |
| ptok, pfeat, emb, lmp, sid = self.voice(voice) | |
| ids, langs = self.tok(text) | |
| spk = torch.tensor([sid, R2I.get(None if rate == "none" else rate, 0)]).cuda() | |
| with torch.autocast("cuda", dtype=torch.bfloat16): | |
| return self.m.generate(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), spk, prompt=lmp, B=B, seed=seed, | |
| sampler=sampler) | |
| def wav(self, toks, voice="anchor"): | |
| ptok, pfeat, emb, _, _ = self.voice(voice) | |
| if len(toks) < 3: | |
| toks = list(toks) + [1, 1, 1] | |
| return token2wav(self.cosy, toks, ptok, pfeat, emb) | |
| def gen(a): | |
| S = Synth(a.lm) | |
| rows = [json.loads(l) for l in open(a.rows, encoding="utf-8")] | |
| if a.shard: | |
| i, n = map(int, a.shard.split("/")); rows = rows[i::n] | |
| os.makedirs(a.out, exist_ok=True) | |
| json.dump(vars(a), open(f"{a.out}/variant.json", "w")) | |
| for r in rows: | |
| if os.path.exists(f"{a.out}/{r['id']}.wav"): | |
| continue | |
| text = r.get("tts_text") or item_text(r) | |
| toks = S.tokens(text, a.voice, a.rate, a.seed)[0] | |
| w = S.wav(toks, a.voice) | |
| sf.write(f"{a.out}/{r['id']}.wav", w, S.cosy.sample_rate) | |
| json.dump(dict(tokens=[toks], ntok=[len(toks)]), open(f"{a.out}/{r['id']}.tok.json", "w")) | |
| print("GEN_DONE", a.out, len(rows), flush=True) | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("cmd") | |
| ap.add_argument("--out", required=True) | |
| ap.add_argument("--lm") | |
| ap.add_argument("--rows", default=f"{HOME}/flash/score/rows_flash.jsonl") | |
| ap.add_argument("--voice", default="anchor") | |
| ap.add_argument("--rate", default="normal") | |
| ap.add_argument("--seed", type=int, default=0) | |
| ap.add_argument("--shard") | |
| ap.add_argument("--minutes", type=float, default=75) | |
| ap.add_argument("--max_steps", type=int, default=12000) | |
| ap.add_argument("--lr", type=float, default=2e-5) | |
| ap.add_argument("--lr_new", type=float, default=1e-3) | |
| ap.add_argument("--warmup", type=int, default=300) | |
| ap.add_argument("--tokens", type=int, default=16000) | |
| ap.add_argument("--max_tok", type=int, default=750) | |
| ap.add_argument("--adv_w", type=float, default=0.02) | |
| ap.add_argument("--p_prompt", type=float, default=0.5) | |
| ap.add_argument("--tag_drop", type=float, default=0.5) | |
| ap.add_argument("--rate_drop", type=float, default=0.5) | |
| ap.add_argument("--anchor_w", type=float, default=1.0) | |
| ap.add_argument("--val_every", type=int, default=1000) | |
| ap.add_argument("--patience", type=int, default=4) | |
| ap.add_argument("--quad", type=float, default=600.0) | |
| ap.add_argument("--extra_man", default=f"{HOME}/flash/data/igbosyn_pseudo/manifest.jsonl") | |
| ap.add_argument("--qual", default=f"{HOME}/flash/qual/utmos_train.jsonl") | |
| ap.add_argument("--qual_drop", type=float, default=0.1) | |
| a = ap.parse_args() | |
| {"lm": train_lm, "gen": gen}[a.cmd](a) | |