Download code/da/model.py from Modularcomputing/dynamic-accent-preview: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/e0ac11827613b2568bf90b4116aead84fe6b8939/code/da/model.py
- Command line
-
hf download hf://Modularcomputing/dynamic-accent-preview@e0ac11827613b2568bf90b4116aead84fe6b8939/code/da/model.py
-
curl -L -o model.py https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/e0ac11827613b2568bf90b4116aead84fe6b8939/code/da/model.py
15.7 kB
| """Dynamic Accent - Arm A, written from scratch in PyTorch. | |
| Backbone: Parallel Tacotron 2 (Elias et al. 2021, arXiv 2103.14574): non-autoregressive, token durations, | |
| differentiable learned upsampling (MLP over start/end offsets -> token->frame weights W), iterative mel loss. | |
| Multilingual recipe: Zhang et al. 2019 (arXiv 1907.04448): separate speaker + language embeddings, | |
| domain-adversarial speaker classifier (gradient reversal) on text-encoder outputs, residual VAE. | |
| Documented deviation (alignment): PT2 trains durations end-to-end with Soft-DTW. Soft-DTW is O(T*N)-sequential and | |
| too slow for a 2 h smoke budget, so we use the internal unsupervised alignment-learning objective of | |
| Badlani et al. 2022 ("One TTS Alignment To Rule Them All", arXiv 2108.10447): a small text/mel attention trained with | |
| a CTC forward-sum loss + beta-binomial prior, hardened with monotonic alignment search (MAS). The hard durations | |
| supervise the duration predictor and drive the SAME differentiable PT2 upsampler at train time; predicted durations | |
| drive it at inference. No external aligner is used. | |
| """ | |
| import math | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from .text import VOCAB, LANGS | |
| N_MELS = 100 | |
| # ---------------------------------------------------------------- utils | |
| def seq_mask(lens, maxlen=None): | |
| maxlen = maxlen or int(lens.max()) | |
| return torch.arange(maxlen, device=lens.device)[None] < lens[:, None] # True = valid | |
| def sinusoid(T, d, device): | |
| pos = torch.arange(T, device=device).float()[:, None] | |
| i = torch.arange(0, d, 2, device=device).float()[None] | |
| ang = pos / (10000 ** (i / d)) | |
| pe = torch.zeros(T, d, device=device) | |
| pe[:, 0::2] = torch.sin(ang) | |
| pe[:, 1::2] = torch.cos(ang) | |
| return pe | |
| class GradReverse(torch.autograd.Function): | |
| """Gradient reversal. As in Zhang et al. 2019 the reversed gradient sent into the text encoder is clipped | |
| (here: its L2 norm is capped at `clip`), which keeps the min-max game from blowing up the encoder.""" | |
| def forward(ctx, x, lam, clip): | |
| ctx.lam, ctx.clip = lam, clip | |
| return x.view_as(x) | |
| def backward(ctx, g): | |
| g = -ctx.lam * g | |
| if ctx.clip is not None: | |
| n = g.norm() | |
| if n > ctx.clip: | |
| g = g * (ctx.clip / (n + 1e-12)) | |
| return g, None, None | |
| def grad_reverse(x, lam=1.0, clip=0.5): | |
| return GradReverse.apply(x, lam, clip) | |
| class ConvNorm(nn.Module): | |
| def __init__(self, cin, cout, k=5, drop=0.1): | |
| super().__init__() | |
| self.conv = nn.Conv1d(cin, cout, k, padding=k // 2) | |
| self.ln = nn.LayerNorm(cout) | |
| self.drop = nn.Dropout(drop) | |
| def forward(self, x, mask): # x [B,T,C], mask [B,T] | |
| x = x.masked_fill(~mask[..., None], 0) | |
| y = self.conv(x.transpose(1, 2)).transpose(1, 2) | |
| return self.drop(F.relu(self.ln(y))) | |
| class FFTBlock(nn.Module): | |
| """Pre-LN self-attention + convolutional feed-forward (FastSpeech/PT2-style lightweight block).""" | |
| def __init__(self, d, heads=4, ff=4, k=3, drop=0.1): | |
| super().__init__() | |
| self.ln1 = nn.LayerNorm(d) | |
| self.att = nn.MultiheadAttention(d, heads, dropout=drop, batch_first=True) | |
| self.ln2 = nn.LayerNorm(d) | |
| self.c1 = nn.Conv1d(d, d * ff, k, padding=k // 2) | |
| self.c2 = nn.Conv1d(d * ff, d, 1) | |
| self.drop = nn.Dropout(drop) | |
| def forward(self, x, mask): | |
| h = self.ln1(x) | |
| a, _ = self.att(h, h, h, key_padding_mask=~mask, need_weights=False) | |
| x = x + self.drop(a) | |
| h = self.ln2(x).masked_fill(~mask[..., None], 0).transpose(1, 2) | |
| h = self.c2(F.gelu(self.c1(h))).transpose(1, 2) | |
| x = x + self.drop(h) | |
| return x.masked_fill(~mask[..., None], 0) | |
| # ---------------------------------------------------------------- modules | |
| class TextEncoder(nn.Module): | |
| def __init__(self, d, n_lang, layers=6): | |
| super().__init__() | |
| self.emb = nn.Embedding(VOCAB, d, padding_idx=0) | |
| self.lang_in = nn.Embedding(n_lang, d) # per-token language id at the input | |
| self.convs = nn.ModuleList([ConvNorm(d, d, 5) for _ in range(3)]) | |
| self.blocks = nn.ModuleList([FFTBlock(d) for _ in range(layers)]) | |
| def forward(self, tok, lang, mask): | |
| x = self.emb(tok) + self.lang_in(lang) | |
| for c in self.convs: | |
| x = x + c(x, mask) | |
| x = x + sinusoid(x.shape[1], x.shape[2], x.device)[None] | |
| for b in self.blocks: | |
| x = b(x, mask) | |
| return x | |
| class ResidualVAE(nn.Module): | |
| """Global residual (prosody) encoder: mel -> q(z|x); z broadcast to every token (Zhang 2019 / Hsu 2019).""" | |
| def __init__(self, zdim=16, h=256): | |
| super().__init__() | |
| self.convs = nn.Sequential( | |
| nn.Conv1d(N_MELS, h, 3, padding=1), nn.ReLU(), | |
| nn.Conv1d(h, h, 3, stride=2, padding=1), nn.ReLU(), | |
| nn.Conv1d(h, h, 3, stride=2, padding=1), nn.ReLU()) | |
| self.gru = nn.GRU(h, h // 2, batch_first=True, bidirectional=True) | |
| self.mu = nn.Linear(h, zdim) | |
| self.logvar = nn.Linear(h, zdim) | |
| def forward(self, mel, mel_lens): | |
| h = self.convs(mel.transpose(1, 2)).transpose(1, 2) | |
| lens = torch.clamp((mel_lens + 3) // 4, min=1) | |
| h, _ = self.gru(h) | |
| m = seq_mask(lens, h.shape[1]).float()[..., None] | |
| h = (h * m).sum(1) / m.sum(1) | |
| mu, lv = self.mu(h), self.logvar(h).clamp(-8, 8) | |
| z = mu + torch.randn_like(mu) * torch.exp(0.5 * lv) if self.training else mu | |
| kl = 0.5 * (mu.pow(2) + lv.exp() - 1 - lv).sum(-1).mean() | |
| return z, kl | |
| class Aligner(nn.Module): | |
| """Unsupervised text-mel alignment (Badlani et al. 2022).""" | |
| def __init__(self, d, h=256, temp=0.0005): | |
| super().__init__() | |
| self.temp = temp | |
| self.key = nn.Sequential(nn.Conv1d(d, h * 2, 3, padding=1), nn.ReLU(), nn.Conv1d(h * 2, 80, 1)) | |
| self.query = nn.Sequential(nn.Conv1d(N_MELS, h * 2, 3, padding=1), nn.ReLU(), | |
| nn.Conv1d(h * 2, h, 1), nn.ReLU(), nn.Conv1d(h, 80, 1)) | |
| def forward(self, txt, mel, tmask, mmask, prior_log): | |
| k = self.key(txt.transpose(1, 2)) # B,80,N | |
| q = self.query(mel.transpose(1, 2)) # B,80,T | |
| q, k = q.float(), k.float() | |
| dist = (q.pow(2).sum(1)[:, :, None] + k.pow(2).sum(1)[:, None, :] | |
| - 2 * torch.bmm(q.transpose(1, 2), k)).clamp_min(0) # B,T,N (memory-light ||q-k||^2) | |
| logits = -self.temp * dist | |
| logits = logits.masked_fill(~tmask[:, None, :], -1e4) | |
| logp = F.log_softmax(logits, -1) + prior_log # prior in log domain | |
| logp = logp.masked_fill(~tmask[:, None, :], -1e4) | |
| soft = F.softmax(logp, -1) * mmask[..., None] | |
| return logits, logp, soft | |
| def forward_sum_loss(logits, tlens, mlens, blank_logprob=-1.0): | |
| """CTC forward-sum: every token must be visited monotonically. logits [B,T,N] (pre-softmax, masked).""" | |
| B, T, N = logits.shape | |
| lp = F.pad(logits, (1, 0), value=blank_logprob) | |
| lp = F.log_softmax(lp, -1).transpose(0, 1) # T,B,N+1 | |
| tgt = torch.arange(1, N + 1, device=logits.device)[None].expand(B, N) | |
| return F.ctc_loss(lp.float(), tgt, mlens, tlens, blank=0, reduction="mean", zero_infinity=True) | |
| def _mas_np(logp, N, T): | |
| """Monotonic alignment search on one [T,N] log-prob matrix -> hard [T,N].""" | |
| Q = np.full((T, N), -np.inf, dtype=np.float32) | |
| Q[0, 0] = logp[0, 0] | |
| for t in range(1, T): | |
| for n in range(min(N, t + 1)): | |
| stay = Q[t - 1, n] | |
| move = Q[t - 1, n - 1] if n > 0 else -np.inf | |
| Q[t, n] = max(stay, move) + logp[t, n] | |
| path = np.zeros((T, N), dtype=np.float32) | |
| n = N - 1 | |
| for t in range(T - 1, -1, -1): | |
| path[t, n] = 1 | |
| if t > 0 and n > 0 and (n == t or Q[t - 1, n - 1] > Q[t - 1, n]): | |
| n -= 1 | |
| return path | |
| try: | |
| import numba | |
| _mas_np = numba.njit(cache=True)(_mas_np) # noqa: F811 | |
| except Exception: # numba optional (CPU unit tests) | |
| pass | |
| def mas(logp, tlens, mlens): | |
| out = torch.zeros_like(logp) | |
| lp = logp.float().cpu().numpy() | |
| for b in range(lp.shape[0]): | |
| T, N = int(mlens[b]), int(tlens[b]) | |
| out[b, :T, :N] = torch.from_numpy(_mas_np(np.ascontiguousarray(lp[b, :T, :N]), N, T)) | |
| return out | |
| _prior_cache = {} | |
| def beta_binomial_prior(N, T, scale=1.0): | |
| from scipy.stats import betabinom | |
| key = (N, T) | |
| if key not in _prior_cache: | |
| x = np.arange(N)[None] | |
| t = np.arange(1, T + 1)[:, None] | |
| pmf = betabinom.pmf(x, N - 1, scale * t, scale * (T + 1 - t)) | |
| _prior_cache[key] = torch.from_numpy(np.log(pmf + 1e-8)).float() | |
| if len(_prior_cache) > 20000: | |
| _prior_cache.clear() | |
| return _prior_cache[key] | |
| def batch_prior(tlens, mlens, N, T, device): | |
| p = torch.full((len(tlens), T, N), -1e4) | |
| for b in range(len(tlens)): | |
| n, t = int(tlens[b]), int(mlens[b]) | |
| p[b, :t, :n] = beta_binomial_prior(n, t) | |
| return p.to(device) | |
| class DurationPredictor(nn.Module): | |
| def __init__(self, d, h=256): | |
| super().__init__() | |
| self.convs = nn.ModuleList([ConvNorm(d, h, 3), ConvNorm(h, h, 3), ConvNorm(h, h, 3)]) | |
| self.out = nn.Linear(h, 1) | |
| def forward(self, x, mask): | |
| for c in self.convs: | |
| x = c(x, mask) | |
| return self.out(x).squeeze(-1).masked_fill(~mask, 0) # log(1 + d) | |
| class LearnedUpsampler(nn.Module): | |
| """PT2 differentiable upsampling: W[t,k] = softmax_k MLP(S_tk, E_tk, h_k); O = W H + proj(sum_k W C).""" | |
| def __init__(self, d, hw=16, hc=8): | |
| super().__init__() | |
| self.se_w = nn.Linear(2, hw) | |
| self.h_w = nn.Linear(d, hw) | |
| self.w_out = nn.Linear(hw, 1) | |
| self.se_c = nn.Linear(2, hc) | |
| self.h_c = nn.Linear(d, hc) | |
| self.c_out = nn.Linear(hc, d) | |
| def forward(self, H, dur, tmask, T): | |
| dur = dur.masked_fill(~tmask, 0) | |
| e = torch.cumsum(dur, 1) | |
| s = e - dur | |
| t = torch.arange(T, device=H.device).float()[None, :, None] + 0.5 | |
| S = (t - s[:, None, :]) / 10.0 | |
| E = (e[:, None, :] - t) / 10.0 | |
| se = torch.stack([S, E], -1) # B,T,N,2 | |
| lw = self.w_out(F.silu(self.se_w(se) + self.h_w(H)[:, None])).squeeze(-1) | |
| lw = lw.masked_fill(~tmask[:, None, :], -1e4) | |
| W = torch.softmax(lw, -1) # B,T,N | |
| C = F.silu(self.se_c(se) + self.h_c(H)[:, None]) # B,T,N,hc | |
| out = torch.bmm(W, H) + self.c_out(torch.einsum("btn,btnc->btc", W, C)) | |
| return out, W | |
| class MelDecoder(nn.Module): | |
| def __init__(self, d, layers=6, n_iter=3): | |
| super().__init__() | |
| self.blocks = nn.ModuleList([FFTBlock(d) for _ in range(layers)]) | |
| self.n_iter = n_iter | |
| self.outs = nn.ModuleList([nn.Linear(d, N_MELS) for _ in range(n_iter)]) | |
| def forward(self, x, mask): | |
| x = x + sinusoid(x.shape[1], x.shape[2], x.device)[None] | |
| mels = [] | |
| L = len(self.blocks) | |
| for i, b in enumerate(self.blocks): | |
| x = b(x, mask) | |
| j = i - (L - self.n_iter) | |
| if j >= 0: | |
| mels.append(self.outs[j](x)) | |
| return mels | |
| # ---------------------------------------------------------------- full model | |
| class DynamicAccentTTS(nn.Module): | |
| def __init__(self, n_spk, n_lang=len(LANGS), d=384, enc_layers=6, dec_layers=6, spk_dim=128, lang_dim=64, | |
| zdim=16, adv_hidden=256, mel_mean=None, mel_std=None, align_temp=0.0005): | |
| super().__init__() | |
| self.n_spk = n_spk | |
| self.encoder = TextEncoder(d, n_lang, enc_layers) | |
| self.spk_emb = nn.Embedding(n_spk, spk_dim) | |
| self.lang_out = nn.Embedding(n_lang, lang_dim) # per-token, concatenated to encoder outputs | |
| self.vae = ResidualVAE(zdim) | |
| self.cond = nn.Linear(d + spk_dim + lang_dim + zdim, d) | |
| self.adv = nn.Sequential(nn.LayerNorm(d), nn.Linear(d, adv_hidden), nn.ReLU(), nn.Linear(adv_hidden, n_spk)) | |
| self.aligner = Aligner(d, temp=align_temp) | |
| self.dur = DurationPredictor(d) | |
| self.up = LearnedUpsampler(d) | |
| self.dec_spk = nn.Linear(spk_dim, d) | |
| self.decoder = MelDecoder(d, dec_layers) | |
| self.register_buffer("mel_mean", torch.tensor(mel_mean if mel_mean is not None else -2.5)) | |
| self.register_buffer("mel_std", torch.tensor(mel_std if mel_std is not None else 2.2)) | |
| def _cond(self, Ht, spk, lang, z): | |
| B, N, _ = Ht.shape | |
| s = self.spk_emb(spk)[:, None].expand(B, N, -1) | |
| zz = z[:, None].expand(B, N, -1) | |
| return self.cond(torch.cat([Ht, s, self.lang_out(lang), zz], -1)), s | |
| def forward(self, tok, lang, tlens, mel, mlens, spk, step=0, adv_lambda=1.0, bin_start=4000, prior=None): | |
| tmask, mmask = seq_mask(tlens, tok.shape[1]), seq_mask(mlens, mel.shape[1]) | |
| meln = (mel - self.mel_mean) / self.mel_std | |
| Ht = self.encoder(tok, lang, tmask) | |
| # adversarial speaker classifier on text encodings (Zhang 2019): GRL removes speaker info from text path | |
| adv_logits = self.adv(grad_reverse(Ht, adv_lambda)) | |
| adv_ce = F.cross_entropy(adv_logits[tmask], spk[:, None].expand_as(tok)[tmask]) | |
| adv_acc = (adv_logits[tmask].argmax(-1) == spk[:, None].expand_as(tok)[tmask]).float().mean() | |
| z, kl = self.vae(meln, mlens) | |
| H, _ = self._cond(Ht, spk, lang, z) | |
| # alignment (internal, unsupervised) | |
| if prior is None: | |
| prior = batch_prior(tlens, mlens, tok.shape[1], mel.shape[1], tok.device) | |
| logits, logp, soft = self.aligner(self.encoder.emb(tok) + Ht.detach(), meln, tmask, mmask, prior) | |
| l_fs = forward_sum_loss(logits.masked_fill(~mmask[..., None], -1e4) + prior, tlens, mlens) | |
| hard = mas(logp, tlens, mlens) | |
| l_bin = -(hard * torch.log(soft.clamp_min(1e-8))).sum() / hard.sum() if step >= bin_start else soft.new_zeros(()) | |
| dur_t = hard.sum(1) # B,N frames | |
| # durations | |
| logd = self.dur(H.detach() if step < 1000 else H, tmask) | |
| l_dur = F.mse_loss(logd[tmask], torch.log1p(dur_t[tmask])) | |
| # upsample with target durations (train) -> decoder | |
| up, W = self.up(H, dur_t, tmask, mel.shape[1]) | |
| x = up + self.dec_spk(self.spk_emb(spk))[:, None] | |
| mels = self.decoder(x, mmask) | |
| m = mmask[..., None].float() | |
| l_mel = sum((F.l1_loss(o, meln, reduction="none") * m).sum() / (m.sum() * N_MELS) for o in mels) / len(mels) | |
| return dict(mel=l_mel, dur=l_dur, fs=l_fs, bin=l_bin, kl=kl, adv=adv_ce, adv_acc=adv_acc.detach()), mels[-1] | |
| def infer(self, tok, lang, spk, pace=1.0, z=None): | |
| tlens = torch.full((tok.shape[0],), tok.shape[1], device=tok.device) | |
| tmask = seq_mask(tlens, tok.shape[1]) | |
| Ht = self.encoder(tok, lang, tmask) | |
| if z is None: | |
| z = torch.zeros(tok.shape[0], self.vae.mu.out_features, device=tok.device) | |
| H, _ = self._cond(Ht, spk, lang, z) | |
| dur = (torch.expm1(self.dur(H, tmask)).clamp(min=0) * pace).masked_fill(~tmask, 0) | |
| T = max(int(dur.sum(1).max().round().item()), 1) | |
| up, W = self.up(H, dur, tmask, T) | |
| x = up + self.dec_spk(self.spk_emb(spk))[:, None] | |
| mel = self.decoder(x, torch.ones(tok.shape[0], T, dtype=torch.bool, device=tok.device))[-1] | |
| return mel * self.mel_std + self.mel_mean, dur | |
| def count_params(m): | |
| return sum(p.numel() for p in m.parameters()) | |