steer-tts / steertts /flow.py
mrfakename's picture
Mirror WhissleAI/steer-tts
5bf57f8 verified
Raw History Blame Contribute Delete
14.2 kB
"""Flow-matching mel decoders (optional), inference subset of recipes/steer_tts/steer/flow.py.
refiner (flow.safetensors): a conditional flow (OT-CFM, as the Matcha-TTS decoder) that samples a detailed mel
given the acoustic model's mel mu and its conditioning g. Durations, pitch, energy, the coarse spectrum and
every control still come from the acoustic model; the flow adds texture (sharper harmonics / noise).
prompted flow (pflow.safetensors): the same with an audio prompt - the reference clip's own mel is given as
context before the target (Voicebox / F5-TTS-style infilling). Used only for zero-shot cloning.
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from .config import N_MELS
SIGMA = 1e-4
class TimeEmb(nn.Module):
def __init__(self, d):
super().__init__()
self.d = d
self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.SiLU(), nn.Linear(4 * d, d))
def forward(self, t): # [B] in [0, 1]
half = self.d // 2
f = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half)
a = 1000 * t[:, None] * f[None]
return self.mlp(torch.cat([a.sin(), a.cos()], -1))
class Block(nn.Module):
"""ConvNeXt block (depthwise dilated conv -> LN -> MLP) with AdaLN-zero conditioning on (t, g)."""
def __init__(self, d, c, k=7, dil=1, mult=3):
super().__init__()
self.dw = nn.Conv1d(d, d, k, padding=dil * (k // 2), dilation=dil, groups=d)
self.ln = nn.LayerNorm(d, elementwise_affine=False)
self.mod = nn.Linear(c, 3 * d)
nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
self.pw1, self.pw2 = nn.Linear(d, mult * d), nn.Linear(mult * d, d)
def forward(self, x, m, c): # x [B,d,T], m [B,1,T], c [B,c]
sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
h = self.dw(x * m).transpose(1, 2)
h = self.ln(h) * (1 + sc) + sh
h = self.pw2(F.gelu(self.pw1(h))) * gate
return x + h.transpose(1, 2)
class AttnBlock(nn.Module):
"""Full self-attention over the sequence with AdaLN-zero conditioning: lets the audio prompt (prompted flow)
reach every target frame, beyond the conv receptive field (~170 frames each side)."""
def __init__(self, d, c, heads=6):
super().__init__()
self.ln = nn.LayerNorm(d, elementwise_affine=False)
self.mod = nn.Linear(c, 3 * d)
nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
self.att = nn.MultiheadAttention(d, heads, batch_first=True)
def forward(self, x, m, c):
sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
h = self.ln(x.transpose(1, 2)) * (1 + sc) + sh
h, _ = self.att(h, h, h, key_padding_mask=~m[:, 0].bool(), need_weights=False)
return x + (h * gate).transpose(1, 2) * m
def rope(x): # x [B,H,T,dh]: rotary position embedding (Su et al. 2021) over the frame axis
T, dh = x.shape[-2], x.shape[-1]
f = torch.exp(-math.log(10000) * torch.arange(0, dh, 2, device=x.device, dtype=torch.float32) / dh)
a = torch.arange(T, device=x.device, dtype=torch.float32)[:, None] * f[None]
cos, sin = a.cos().to(x.dtype), a.sin().to(x.dtype)
x1, x2 = x[..., 0::2], x[..., 1::2]
return torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], -1).flatten(-2)
class PromptPool(nn.Module):
"""Perceiver-resampler pooling of the prompt (XTTS; SupertonicTTS's learnable timbre queries): n learnable
queries cross-attend to the encoded prompt frames -> n global voice tokens. A learned null key is always
present, so an item without a prompt still gets defined (prompt-free) tokens."""
def __init__(self, d, n, heads=6, layers=2):
super().__init__()
self.q = nn.Parameter(torch.randn(n, d) * 0.02)
self.null = nn.Parameter(torch.zeros(1, 1, d))
self.heads = heads
self.layers = nn.ModuleList([nn.ModuleDict(dict(
lq=nn.LayerNorm(d), lk=nn.LayerNorm(d), q=nn.Linear(d, d), kv=nn.Linear(d, 2 * d), o=nn.Linear(d, d),
lf=nn.LayerNorm(d), f=nn.Sequential(nn.Linear(d, 2 * d), nn.GELU(), nn.Linear(2 * d, d))))
for _ in range(layers)])
def forward(self, e, cm): # e [B,d,T] encoded prompt (zero off-prompt), cm [B,1,T]
B, d, T = e.shape
kv_in = torch.cat([self.null.expand(B, 1, d), e.transpose(1, 2)], 1)
keep = torch.cat([torch.ones(B, 1, device=e.device, dtype=torch.bool), cm[:, 0].bool()], 1)[:, None, None]
q = self.q[None].expand(B, -1, -1)
H = self.heads
for l in self.layers:
k, v = l["kv"](l["lk"](kv_in)).chunk(2, -1)
qq = l["q"](l["lq"](q))
sp = lambda z: z.view(B, -1, H, d // H).transpose(1, 2)
h = F.scaled_dot_product_attention(sp(qq), sp(k), sp(v), attn_mask=keep)
q = q + l["o"](h.transpose(1, 2).reshape(B, -1, d))
q = q + l["f"](l["lf"](q))
return q # [B,n,d]
class RAttnBlock(nn.Module):
"""Self-attention with RoPE positions (frames), plus optional cross-attention to the pooled prompt tokens; both
zero-gated (AdaLN-zero gate / zero output projection), so adding these blocks to a trained net changes nothing
at init."""
def __init__(self, d, c, heads=6, xattn=False):
super().__init__()
self.heads = heads
self.ln = nn.LayerNorm(d, elementwise_affine=False)
self.mod = nn.Linear(c, 3 * d)
nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
self.qkv, self.o = nn.Linear(d, 3 * d), nn.Linear(d, d)
self.xattn = xattn
if xattn:
self.lx = nn.LayerNorm(d)
self.xq, self.xkv, self.xo = nn.Linear(d, d), nn.Linear(d, 2 * d), nn.Linear(d, d)
nn.init.zeros_(self.xo.weight); nn.init.zeros_(self.xo.bias)
def forward(self, x, m, c, tok=None): # x [B,d,T], tok [B,n,d]
B, d, T = x.shape
H = self.heads
sp = lambda z: z.view(B, -1, H, d // H).transpose(1, 2)
sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
h = self.ln(x.transpose(1, 2)) * (1 + sc) + sh
q, k, v = (sp(z) for z in self.qkv(h).chunk(3, -1))
h = F.scaled_dot_product_attention(rope(q), rope(k), v, attn_mask=m[:, 0].bool()[:, None, None])
h = self.o(h.transpose(1, 2).reshape(B, T, d)) * gate
if self.xattn and tok is not None:
hx = self.lx(x.transpose(1, 2) + h)
k2, v2 = self.xkv(tok).chunk(2, -1)
h = h + self.xo(F.scaled_dot_product_attention(sp(self.xq(hx)), sp(k2), sp(v2)).transpose(1, 2).reshape(B, T, d))
return x + h.transpose(1, 2) * m
class FlowNet(nn.Module):
"""prompt=True (prompted flow): also takes a context mel ctx with mask cm (1 = real audio given: the reference
clip's frames), as Voicebox / F5-TTS infilling. The context enters the input stack, a masked mean of it is added
to the conditioning (global voice), and attention layers at attn_at carry it across the whole sequence.
v5 additions (all zero-initialised, so a v4 checkpoint loads into a v5 net unchanged):
rope_at layers that get an extra RoPE self-attention block (RAttnBlock)
n_ptok pooled prompt tokens (PromptPool) that the RoPE blocks cross-attend to
skips E2-TTS-style long skips, block i -> block n-1-i
align_at layers whose target-frame mean is mapped to a speaker-verification vector (TLA-SA-style feature
alignment loss; training only)"""
def __init__(self, n_mel=N_MELS, d=384, n_layers=12, g_dim=128, dils=(1, 2, 4, 1, 2, 4, 1, 2, 4, 1, 2, 4),
prompt=False, attn_at=(3, 7, 11), rope_at=(), n_ptok=0, skips=False, align_at=(), align_dim=512):
super().__init__()
self.cfg = dict(n_mel=n_mel, d=d, n_layers=n_layers, g_dim=g_dim, dils=tuple(dils), prompt=prompt,
attn_at=tuple(attn_at), rope_at=tuple(rope_at), n_ptok=n_ptok, skips=skips,
align_at=tuple(align_at), align_dim=align_dim)
self.prompt = prompt
self.inp = nn.Conv1d((3 * n_mel + 1) if prompt else 2 * n_mel, d, 3, padding=1)
self.temb = TimeEmb(d)
self.gemb = nn.Sequential(nn.Linear(g_dim, d), nn.SiLU(), nn.Linear(d, d))
self.blocks = nn.ModuleList([Block(d, d, dil=dils[i % len(dils)]) for i in range(n_layers)])
if prompt:
self.attn = nn.ModuleDict({str(i): AttnBlock(d, d) for i in attn_at})
self.ctx_enc = nn.Sequential(nn.Conv1d(n_mel, d, 3, padding=1), nn.SiLU(), nn.Conv1d(d, d, 3, padding=1))
self.ctx_out = nn.Linear(d, d)
nn.init.zeros_(self.ctx_out.weight); nn.init.zeros_(self.ctx_out.bias)
self.rattn = nn.ModuleDict({str(i): RAttnBlock(d, d, xattn=n_ptok > 0) for i in rope_at})
self.pool = PromptPool(d, n_ptok) if n_ptok else None
self.skips = skips
if skips:
self.skip = nn.ModuleDict({str(i): nn.Linear(d, d) for i in range(n_layers - n_layers // 2, n_layers)})
for l in self.skip.values():
nn.init.zeros_(l.weight); nn.init.zeros_(l.bias)
self.align_at = tuple(align_at)
if align_at:
self.align = nn.ModuleDict({str(i): nn.Sequential(nn.Linear(d, d), nn.SiLU(), nn.Linear(d, align_dim))
for i in align_at})
self.ln = nn.LayerNorm(d, elementwise_affine=False)
self.out = nn.Conv1d(d, n_mel, 1)
nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias)
def forward(self, x, t, mu, g, m, ctx=None, cm=None, feats=False): # normalised mels [B,100,T]; m, cm [B,1,T]
"""feats=True also returns {layer: speaker vector predicted from the target-frame mean} for align_at."""
c = self.temb(t) + self.gemb(g)
tok = None
if self.prompt:
if cm is None:
ctx, cm = torch.zeros_like(x), torch.zeros_like(m)
ctx = ctx * cm
e = self.ctx_enc(ctx) * cm
c = c + self.ctx_out(e.sum(-1) / cm.sum(-1).clamp_min(1))
if getattr(self, "pool", None) is not None:
tok = self.pool(e, cm)
h = self.inp(torch.cat([x, mu, ctx, cm], 1) * m)
else:
h = self.inp(torch.cat([x, mu], 1) * m)
rattn = getattr(self, "rattn", {})
stack, out = [], {}
n = len(self.blocks)
for i, b in enumerate(self.blocks):
if self.skips and str(i) in self.skip:
h = h + self.skip[str(i)](stack.pop().transpose(1, 2)).transpose(1, 2) * m
h = b(h, m, c)
if self.prompt and str(i) in self.attn:
h = self.attn[str(i)](h, m, c)
if str(i) in rattn:
h = rattn[str(i)](h, m, c, tok)
if self.skips and i < n // 2:
stack.append(h)
if feats and i in self.align_at:
w = m * (1 - cm) if cm is not None else m
out[i] = self.align[str(i)](((h * w).sum(-1) / w.sum(-1).clamp_min(1)).float())
v = self.out(self.ln(h.transpose(1, 2)).transpose(1, 2)) * m
return (v, out) if feats else v
class Flow(nn.Module):
"""FlowNet + mel normalisation; sample() maps a base mel to a refined mel (log-mel units, same frames)."""
def __init__(self, mean=-5.0, std=2.5, mu_cmn=False, **kw):
"""mu_cmn (prompted flow v2): remove the per-band time-mean of the base mel (its static spectral envelope:
the long-term voice colouring the base model guessed from a 512-d speaker vector) so the flow must take the
voice from the audio prompt (or g); timing / pitch / energy / phonetic detail of mu are kept. Without it, mu
made from the TRUE speaker vector already carries the training speaker's colouring and the prompt adds
little (v3p @20k: Seed-TTS SIM 31.2 -> 33.2 only)."""
super().__init__()
self.net = FlowNet(**kw)
self.mu_cmn = mu_cmn
self.register_buffer("mean", torch.tensor(float(mean)))
self.register_buffer("std", torch.tensor(float(std)))
def norm(self, m):
return (m - self.mean) / self.std
def _cmn(self, mu_n, w):
"""mu_n minus its per-band mean over the frames w marks (the target frames)."""
if not self.mu_cmn:
return mu_n
return mu_n - (mu_n * w).sum(-1, keepdim=True) / w.sum(-1, keepdim=True).clamp_min(1)
@torch.no_grad()
def sample(self, mu, g, m=None, steps=10, temp=0.8, method="midpoint", seed=None, ctx=None, cm=None, cfg=0.0):
"""ctx / cm (prompted flow): real mel on the context frames (log-mel units), mask 1 there; mu is replaced by
the context on those frames, as in training. Returns every frame (callers slice off the context).
cfg (prompted flow): classifier-free guidance on the prompt, v + cfg * (v - v_no_prompt); the no-prompt
velocity is the same net with the context removed (trained on p_none of the items)."""
m = torch.ones_like(mu[:, :1]) if m is None else m
gen = None if seed is None else torch.Generator(device=mu.device).manual_seed(seed)
mu_n = self._cmn(self.norm(mu), m if cm is None else m * (1 - cm))
if cm is not None:
ctx = self.norm(ctx) * cm
mu_n = torch.where(cm.bool(), ctx, mu_n)
x = temp * torch.randn(mu_n.shape, device=mu.device, generator=gen) * m
ts = torch.linspace(0, 1, steps + 1, device=mu.device)
def vel(x, t):
v = self.net(x, t.expand(x.shape[0]), mu_n, g, m, ctx, cm)
if cfg and cm is not None:
v = v + cfg * (v - self.net(x, t.expand(x.shape[0]), mu_n, g, m, None, None))
return v
for i in range(steps):
t0, dt = ts[i], ts[i + 1] - ts[i]
v = vel(x, t0)
if method == "midpoint":
v = vel(x + 0.5 * dt * v, t0 + 0.5 * dt)
x = x + dt * v
return x * self.std + self.mean
@staticmethod
def from_state(state, cfg, device="cpu"):
f = Flow(**cfg)
f.load_state_dict(state)
return f.to(device).eval()