UncleanCode's picture
Add inference code (prosody v2), README, LICENSE, prompts, samples
256a3b6 verified
Raw History Blame
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."""
@staticmethod
def forward(ctx, x, lam, clip):
ctx.lam, ctx.clip = lam, clip
return x.view_as(x)
@staticmethod
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
@torch.no_grad()
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]
@torch.no_grad()
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())