Download train/train_decoder.py from VantoraLabs/Vocetta2-147k: direct link, hf CLI and curl.
- Browser
- Download file 8.86 kB
-
https://huggingface.co/VantoraLabs/Vocetta2-147k/resolve/main/train/train_decoder.py
- Command line
-
hf download hf://VantoraLabs/Vocetta2-147k/train/train_decoder.py
-
curl -L -o train_decoder.py https://huggingface.co/VantoraLabs/Vocetta2-147k/resolve/main/train/train_decoder.py
8.86 kB
| """Z-mix decoder training: recovery on teacher mels, then a 50/50 student mix. | |
| This is the load-bearing stage for the acoustic -> decoder handoff. A decoder | |
| trained only on teacher mels is brittle to the acoustic student's (smoother) | |
| output at synthesis time; mixing student mels into the training data with | |
| --mix-prob 0.5 fixes it. The schedule that works: | |
| 1. recovery --mix-prob 0.0 (teacher mels only) | |
| 2. z-mix --mix-prob 0.5 (half student mels) | |
| Use the same --init checkpoint for both stages and pass the acoustic and | |
| duration checkpoints with --ac-ckpt / --dur-ckpt (they are frozen and only | |
| serve to synthesize training mels). | |
| """ | |
| import argparse | |
| import glob | |
| import os | |
| import time | |
| import numpy as np | |
| import torch | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| import torch.nn.functional as F | |
| from microtts.models import DurationStudent, AcousticStudent, DecoderStudent | |
| def stft_loss(y, yh, device): | |
| loss = 0.0 | |
| n = min(y.shape[-1], yh.shape[-1]) | |
| for n_fft, hop in [(512, 128), (1024, 256), (2048, 512)]: | |
| if n < n_fft: | |
| continue # skip FFTs larger than the input | |
| Y = torch.stft(y[..., :n], n_fft, hop, window=torch.hann_window(n_fft, device=device), | |
| return_complex=True, center=False) | |
| Yh = torch.stft(yh[..., :n], n_fft, hop, window=torch.hann_window(n_fft, device=device), | |
| return_complex=True, center=False) | |
| T = min(Y.shape[-1], Yh.shape[-1]) | |
| mag, mag_r = Y[..., :T].abs(), Yh[..., :T].abs() | |
| sc = (mag - mag_r).norm(dim=-2) / (mag.norm(dim=-2) + mag_r.norm(dim=-2) + 1e-6) | |
| lm = (mag.clamp_min(1e-6).log10() - mag_r.clamp_min(1e-6).log10()).abs().mean() | |
| loss = loss + sc.mean() + lm | |
| return loss / 3.0 | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--pack", default="packs/train") | |
| ap.add_argument("--steps", type=int, default=10000) | |
| ap.add_argument("--batch", type=int, default=4) | |
| ap.add_argument("--lr", type=float, default=1e-4) | |
| ap.add_argument("--device", default="cuda") | |
| ap.add_argument("--tag", default="dec_zmix") | |
| ap.add_argument("--mix-prob", type=float, default=0.5) | |
| ap.add_argument("--phone-aligned", action="store_true", | |
| help="crop student-mel windows phone-aligned to the audio " | |
| "(fixes train/inference timeline desync for the mix stage)") | |
| ap.add_argument("--init", default="decoder_init.pt", | |
| help="teacher-initialized decoder checkpoint") | |
| ap.add_argument("--dec-dim", type=int, default=40) | |
| ap.add_argument("--dec-pw", type=int, default=120) | |
| ap.add_argument("--dec-blocks", type=int, default=3) | |
| ap.add_argument("--ac-hidden", type=int, default=28) | |
| ap.add_argument("--ac-ckpt", default="acoustic.pt") | |
| ap.add_argument("--dur-hidden", type=int, default=12) | |
| ap.add_argument("--dur-depth", type=int, default=3) | |
| ap.add_argument("--dur-ckpt", default="duration.pt") | |
| args = ap.parse_args() | |
| device = torch.device(args.device if torch.cuda.is_available() else "cpu") | |
| files = sorted(glob.glob(os.path.join(args.pack, "*.npz"))) | |
| print(f"pack: {len(files)} rows, device {device}") | |
| # load acoustic student (frozen) for student-mel generation | |
| ac = AcousticStudent(hidden=args.ac_hidden).to(device) | |
| ac.load_state_dict(torch.load(args.ac_ckpt, map_location=device)) | |
| ac.eval() | |
| dur = DurationStudent(hidden=args.dur_hidden, depth=args.dur_depth).to(device) | |
| dur.load_state_dict(torch.load(args.dur_ckpt, map_location=device)) | |
| dur.eval() | |
| model = DecoderStudent(dim=args.dec_dim, pw_hidden=args.dec_pw, blocks=args.dec_blocks).to(device) | |
| model.load_state_dict(torch.load(args.init, map_location=device)) | |
| n = sum(p.numel() for p in model.parameters()) | |
| print(f"decoder params: {n:,} (init from {args.init})") | |
| opt = torch.optim.AdamW(model.parameters(), lr=args.lr, betas=(0.8, 0.99)) | |
| sched = torch.optim.lr_scheduler.ExponentialLR(opt, gamma=0.9998) | |
| RUN = os.path.join("runs", args.tag) | |
| os.makedirs(RUN, exist_ok=True) | |
| t0 = time.time() | |
| for step in range(1, args.steps + 1): | |
| idx = np.random.choice(len(files), min(args.batch, len(files)), replace=False) | |
| total = 0.0 | |
| cnt = 0 | |
| opt.zero_grad() | |
| for i in idx: | |
| d = np.load(files[i]) | |
| mel_t = torch.from_numpy(d["mel"]).to(device) # teacher mel [100, T] | |
| audio = torch.from_numpy(d["audio"]).to(device) | |
| T = mel_t.shape[1] | |
| seg = 64 | |
| if T > seg: | |
| s = np.random.randint(0, T - seg) | |
| mel_t = mel_t[:, s:s + seg] | |
| audio = audio[s * 256:(s + seg) * 256] | |
| # 50/50: teacher mel or acoustic-student mel | |
| if np.random.rand() < args.mix_prob: | |
| ids = torch.from_numpy(d["phone_ids"]).long().to(device) | |
| with torch.no_grad(): | |
| dd = dur(ids) | |
| mel_s = ac(ids, dd) # student mel [100, T_full] | |
| if args.phone_aligned: | |
| # Phone-aligned crop: choose a phone range, take student mel | |
| # frames from the STUDENT timeline and audio from the TEACHER | |
| # timeline for the SAME phones (student/teacher frame counts | |
| # differ; same-offset cropping desynchronises them). | |
| w = np.rint(d["durations"].astype(np.float64)).astype(np.int64) # teacher per-phone | |
| dd_np = np.rint(dd.detach().cpu().numpy().reshape(-1)).astype(np.int64) | |
| cum_s = np.concatenate([[0], np.cumsum(np.maximum(dd_np, 1))]) | |
| cum_t = np.concatenate([[0], np.cumsum(np.maximum(w, 1))]) | |
| P = len(dd_np) | |
| if P < 2: | |
| continue | |
| # find a phone window [j1, j2) with >= 16 student frames | |
| j1 = np.random.randint(0, max(1, P - 1)) | |
| j2 = j1 + 1 | |
| while j2 < P and cum_s[j2] - cum_s[j1] < seg: | |
| j2 += 1 | |
| if cum_s[j2] - cum_s[j1] < 16: | |
| continue | |
| x_s, y_s = int(cum_s[j1]), int(cum_s[j2]) | |
| x_t, y_t = int(cum_t[j1]), int(cum_t[j2]) | |
| mel_s = mel_s[:, x_s:y_s] | |
| a0, a1 = x_t * 256, y_t * 256 | |
| if a1 > audio.shape[0]: | |
| a1 = audio.shape[0] | |
| if a1 - a0 < 1024: | |
| continue | |
| audio = audio[a0:a1] | |
| mel = mel_s | |
| else: | |
| seg_s = min(seg, mel_s.shape[1]) | |
| if seg_s < 16: # need >= 16 frames for the 2048-FFT loss | |
| continue | |
| s_s = np.random.randint(0, max(1, mel_s.shape[1] - seg_s + 1)) | |
| mel_s = mel_s[:, s_s:s_s + seg_s] | |
| a0, a1 = s_s * 256, (s_s + seg_s) * 256 | |
| if a1 > audio.shape[0]: | |
| a1 = audio.shape[0] | |
| if a1 - a0 < 1024: | |
| continue | |
| audio = audio[a0:a1] | |
| mel = mel_s | |
| else: | |
| mel = mel_t | |
| noise = torch.randn(4, mel.shape[1], device=device) | |
| wav = model.synthesize(mel, noise) | |
| n_w = min(wav.shape[-1], audio.shape[-1]) | |
| l_wav = F.l1_loss(wav[:n_w], audio[:n_w]) | |
| l_stft = stft_loss(wav[:n_w].unsqueeze(0), audio[:n_w].unsqueeze(0), device) | |
| rms_p = wav[:n_w].square().mean().clamp_min(1e-8).sqrt() | |
| rms_t = audio[:n_w].square().mean().clamp_min(1e-8).sqrt() | |
| l_loud = F.l1_loss(rms_p, rms_t) | |
| total = total + 5.0 * l_wav + 1.0 * l_stft + 3.0 * l_loud | |
| cnt += 1 | |
| if cnt == 0: | |
| continue | |
| loss = total / cnt | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step() | |
| sched.step() | |
| if step % 100 == 0 or step == 1: | |
| dt = time.time() - t0 | |
| print(f"step {step}/{args.steps} | wav {l_wav.item():.4f} stft {l_stft.item():.3f} " | |
| f"loud {l_loud.item():.4f} | {step/dt:.1f} sps", flush=True) | |
| if step % 1000 == 0: | |
| torch.save(model.state_dict(), os.path.join(RUN, "last.pt")) | |
| torch.save(model.state_dict(), os.path.join(RUN, "final.pt")) | |
| print("Z-MIX DECODER DONE") | |
| if __name__ == "__main__": | |
| main() | |