文章目录
前言
F5-TTS,一款由上海交通大学推出的高性能文本到语音(TTS)系统,凭借其创新的流匹配非自回归生成方法,并结合了先进的扩散变换器(DiT)技术,实现了在无额外监督条件下的零样本学习,能够迅速生成自然流畅且忠实于原文的语音。该系统支持多语言合成,涵盖中文和英文,并能在长文本上展现出色的语音合成效果。
F5-TTS不仅功能强大,还具备丰富的个性化设置。其情感控制功能能够根据文本内容灵活调整合成语音的情感表现,为用户带来更加生动的听觉体验。同时,系统还支持速度控制,用户可以根据实际需求自由调整语音的播放速度,满足多样化的使用场景。
得益于在10万小时大规模数据集上的训练,F5-TTS展现出了卓越的性能和强大的泛化能力。这使得它在有声读物、语音助手、语言学习、新闻播报、游戏配音等多个应用场景中都能发挥出色的作用。无论是商业用途还是非商业用途,F5-TTS都能为用户提供强大的语音合成能力,助力各种语音相关应用的开发与创新。
代码仓库:https://github.com/SWivid/F5-TTS
论文:https://arxiv.org/abs/2410.06885
demo: https://huggingface.co/spaces/mrfakename/E2-F5-TTS
权重文件:
SWivid/F5-TTS_Emilia-ZH-EN:https://www.modelscope.cn/models/SWivid/F5-TTS_Emilia-ZH-EN/files
SWivid/E2-TTS_Emilia-ZH-EN:https://www.modelscope.cn/models/SWivid/E2-TTS_Emilia-ZH-EN/files
whisper-large-v3-turbo:https://www.modelscope.cn/models/iic/Whisper-large-v3-turbo/files
(相关的权重文件可以直接在魔搭社区直接下载,关键是不需要魔法)
一、算法架构
1.创新点
-
流匹配技术(Flow Matching):
F5-TTS采用流匹配目标训练模型,该模型具备将标准正态分布等简单概率分布转换为近似数据分布的复杂概率分布的能力。这一技术涉及在整个流步骤和数据范围内对模型进行训练,确保模型能够处理从初始分布到目标分布的完整转换过程,从而生成高质量的语音。 -
扩散变换器(DiT)骨干网络:
作为F5-TTS的核心组件,扩散变换器(DiT)能够高效处理序列数据。在生成过程中,DiT通过逐步去除噪声,生成清晰的语音信号。这一特性使得F5-TTS在语音合成方面表现出色,能够生成自然流畅的语音。 -
ConvNeXtV2改进文本表示:
F5-TTS利用ConvNeXtV2对文本表示进行改进,使得文本特征更容易与语音特征对齐。这一改进不仅提高了语音合成的质量和自然度,还简化了模型的设计,降低了训练难度。 -
Sway Sampling策略:
在推理阶段,F5-TTS采用Sway Sampling策略进行流步骤采样。该策略基于非均匀采样,旨在提高模型的性能和效率。特别是在生成语音的早期阶段,Sway Sampling有助于模型更准确地捕捉目标语音的轮廓,从而生成更加逼真的语音。 -
端到端的系统设计:
F5-TTS采用简洁直接的系统设计,从文本输入到语音输出,无需经过传统的复杂设计步骤,如音素对齐和时长预测。这一设计简化了模型的训练和推理过程,降低了系统的复杂性和维护成本,使得F5-TTS在各种应用场景中都能表现出色。
网络架构图如上
F5-TTS 是一个先进的非自回归(Non-Autoregressive, NAR)文本到语音(TTS)生成系统。相较于传统的自回归 TTS 系统,F5-TTS 能够并行处理语音生成,传统自回归 TTS 系统通过顺序逐帧生成音频,依赖每个生成片段的先前状态,导致生成速度较慢,且长序列生成时容易出现累积误差,影响生成质量。F5-TTS 采用了非自回归架构(NAR),即一次性并行生成整段音频特征,从而显著提高生成速度,并减少累积误差。这种并行化生成方式使 F5-TTS 特别适用于实时语音生成和高效语音克隆任务,显著提高了生成速度,并借助一系列前沿技术(如流匹配、扩散 Transformer、Sway Sampling 和 ConvNeXt V2),实现高效且自然的语音生成。本篇博客将深入介绍 F5-TTS 系统的设计、数据预处理、特征提取以及其在声音克隆中的应用。
F5-TTS 结合了以下几项核心技术:
- 流匹配(Flow Matching):用于将模型生成的特征与目标特征匹配,提升自然性。
- 扩散 Transformer (Diffusion Transformer, DiT):作为主干模型,在并行推理中生成音频特征。
- Sway Sampling:一种创新的采样策略,用于生成流畅的语音。
- ConvNeXt V2:对文本特征进行高级表示处理,以增强文本和语音的对齐。
在 TTS 系统中,输入通常为文本,而输出为对应的语音信号。F5-TTS 使用梅尔频谱作为音频特征,以下是详细的算法流程:
2. 数据预处理
首先,将输入音频数据转换为统一的采样率并进行归一化。对于文本数据,F5-TTS 使用字符映射表(Vocab Map)将文本字符映射为离散索引,以便模型学习音素特征。此外,通过 Mel Spectrogram 模块将音频数据转换为梅尔频谱,便于后续的特征提取和匹配。
3. 特征提取
F5-TTS 使用梅尔频谱作为输入特征,具体步骤如下:
- 文本特征提取:将文本转换为音素索引,并通过 ConvNeXt V2 模块对文本特征进行细粒度处理。ConvNeXt V2 是一种改进的卷积神经网络结构,能捕捉多层次的文本特征,确保文本与语音的精确对齐。F5-TTS 系统使用 ConvNeXt V2 处理文本特征,将输入的文本表示成多层次特征,便于生成模型在不同层级上对文本信息进行理解。ConvNeXt V2 的多层次卷积结构帮助模型更准确地捕捉文本中的音素特征,使生成的语音在语音合成的音调、节奏和韵律上更加自然。
- 音频特征提取:将音频转换为梅尔频谱,使得系统能够从中提取音频的音调、频率和能量分布特征。
使用 ConvNeXt V2 处理后的文本特征为扩散 Transformer 提供了更高质量的输入,有助于生成自然的语音流。
4. 扩散 Transformer (Diffusion Transformer, DiT)
扩散 Transformer 是 F5-TTS 系统的主干模型,其结合了扩散模型的生成思想和 Transformer 的特征提取能力。在生成过程中,扩散模型从初始噪声逐渐演变为目标特征,形成最终的梅尔频谱表示。DiT 的多层特征提取能力,使其能够在并行推理中高效生成音频特征,具体步骤如下:
- 初始噪声生成:从高斯噪声开始,逐步优化特征。
- 扩散过程:通过扩散模型逐步生成目标音频特征。
- 生成梅尔频谱:最终输出为梅尔频谱,后续通过声码器将其转换为音频波形。
DiT 的并行化能力为 TTS 提供了显著的加速效果。
5. 流匹配(Flow Matching)
流匹配技术用于将生成特征与目标特征匹配。其工作原理是通过流匹配损失,使模型的输出更接近目标音频特征的分布,从而提升生成语音的自然性。在训练过程中,流匹配损失确保生成特征模拟目标特征分布,有助于减少语音生成中的不自然现象。
流匹配的优势包括:
- 自然度提升:使生成的语音更加接近人类语音特征。
- 稳定性增强:减少音频生成中的异常跳跃和累积误差。
6. Sway Sampling 策略
F5-TTS 在推理阶段引入了 Sway Sampling 策略,通过动态调整采样分布来生成流畅自然的语音。Sway Sampling 能有效降低生成中的突变情况,生成一致的音质和音调。具体而言,Sway Sampling 在生成步骤中微调采样权重,使生成过程更加接近自然音频流的分布。
在推理阶段,F5-TTS 使用梅尔频谱作为输入特征,通过以下步骤生成音频:
- 文本与音频对齐:使用 ConvNeXt V2 对输入文本特征进行多层次处理,使其与音频特征对齐。
- 扩散过程生成梅尔频谱:扩散 Transformer 将文本特征和梅尔频谱作为输入,经过逐步扩散过程生成完整的梅尔频谱。
- Sway Sampling 调整采样:通过 Sway Sampling 策略生成自然的音频流,避免突变和不自然现象。
- 声码器转换:最终的梅尔频谱传递到声码器模块,生成可播放的音频信号。
二、代码详解
model/cfm.py,F5-TTS的整体模型代码在这里面
class CFM(nn.Module):
def __init__(
self,
transformer: nn.Module,
sigma = 0.,
odeint_kwargs: dict = dict(
# atol = 1e-5,
# rtol = 1e-5,
method = 'euler' # 'midpoint'
),
audio_drop_prob = 0.3,
cond_drop_prob = 0.2,
num_channels = None,
mel_spec_module: nn.Module | None = None,
mel_spec_kwargs: dict = dict(),
frac_lengths_mask: tuple[float, float] = (0.7, 1.),
vocab_char_map: dict[str: int] | None = None
):
super().__init__()
# transformer: nn.Module: 传入一个 transformer 模型,用于处理输入数据。
# sigma: 生成过程中使用的噪声参数,用于控制条件生成的程度。
# odeint_kwargs: 定义数值积分方法,默认为 euler。
# audio_drop_prob 和 cond_drop_prob: 控制音频和条件数据丢弃的概率,用于训练时增强模型鲁棒性。
# num_channels: 用于设置模型的通道数量,默认为 None。
# mel_spec_module 和 mel_spec_kwargs: 传入一个梅尔频谱模块和它的参数,用于将音频信号转换为梅尔频谱。
# frac_lengths_mask: 用于生成掩码的比例范围。
# vocab_char_map: 用于字符到词汇映射的字典,用于处理文本输入。
self.frac_lengths_mask = frac_lengths_mask # 保存 frac_lengths_mask 作为类属性,用于生成随机掩码。
# mel spec 梅尔频谱模块
self.mel_spec = default(mel_spec_module, MelSpec(**mel_spec_kwargs))
num_channels = default(num_channels, self.mel_spec.n_mel_channels)
self.num_channels = num_channels
# classifier-free guidance 设置音频和条件数据丢弃的概率,用于控制训练时数据的可用性。
self.audio_drop_prob = audio_drop_prob
self.cond_drop_prob = cond_drop_prob
# transformer
self.transformer = transformer
dim = transformer.dim
self.dim = dim
# conditional flow related
self.sigma = sigma
# sampling related 保存积分配置 odeint_kwargs
self.odeint_kwargs = odeint_kwargs
# vocab map for tokenization 保存字符到词汇的映射。
self.vocab_char_map = vocab_char_map
@property
def device(self):
return next(self.parameters()).device
@torch.no_grad()
def sample(
self,
cond: float['b n d'] | float['b nw'],
text: int['b nt'] | list[str],
duration: int | int['b'],
*,
lens: int['b'] | None = None,
steps = 32,
cfg_strength = 1.,
sway_sampling_coef = None,
seed: int | None = None,
max_duration = 4096,
vocoder: Callable[[float['b d n']], float['b nw']] | None = None,
no_ref_audio = False,
duplicate_test = False,
t_inter = 0.1,
edit_mask = None,
):
self.eval()
# @torch.no_grad(): 禁用梯度计算,加快采样过程并节省内存。
# cond: 输入条件,梅尔频谱。
# text: 输入文本。
# duration: 生成音频的持续时间。
# 其他参数(如 cfg_strength、vocoder、no_ref_audio 等)提供额外控制,允许指定步数、随机种子、最大持续时间等。
if next(self.parameters()).dtype == torch.float16:
cond = cond.half()
# raw wave 转换为梅尔频谱并调整维度以匹配通道数
if cond.ndim == 2:
cond = self.mel_spec(cond)
cond = cond.permute(0, 2, 1)
assert cond.shape[-1] == self.num_channels
batch, cond_seq_len, device = *cond.shape[:2], cond.device
if not exists(lens):
lens = torch.full((batch,), cond_seq_len, device = device, dtype = torch.long)
# text
if isinstance(text, list):
if exists(self.vocab_char_map):
text = list_str_to_idx(text, self.vocab_char_map).to(device)
else:
text = list_str_to_tensor(text).to(device)
assert text.shape[0] == batch
if exists(text):
text_lens = (text != -1).sum(dim = -1)
lens = torch.maximum(text_lens, lens) # make sure lengths are at least those of the text characters
# duration
cond_mask = lens_to_mask(lens)
if edit_mask is not None:
cond_mask = cond_mask & edit_mask
if isinstance(duration, int):
duration = torch.full((batch,), duration, device = device, dtype = torch.long)
duration = torch.maximum(lens + 1, duration) # just add one token so something is generated
duration = duration.clamp(max = max_duration)
max_duration = duration.amax()
# duplicate test corner for inner time step oberservation
if duplicate_test:
test_cond = F.pad(cond, (0, 0, cond_seq_len, max_duration - 2*cond_seq_len), value = 0.)
cond = F.pad(cond, (0, 0, 0, max_duration - cond_seq_len), value = 0.)
cond_mask = F.pad(cond_mask, (0, max_duration - cond_mask.shape[-1]), value = False)
cond_mask = cond_mask.unsqueeze(-1)
step_cond = torch.where(cond_mask, cond, torch.zeros_like(cond)) # allow direct control (cut cond audio) with lens passed in
if batch > 1:
mask = lens_to_mask(duration)
else: # save memory and speed up, as single inference need no mask currently
mask = None
# test for no ref audio
if no_ref_audio:
cond = torch.zeros_like(cond)
# neural ode
def fn(t, x):
# at each step, conditioning is fixed
# step_cond = torch.where(cond_mask, cond, torch.zeros_like(cond))
# predict flow
pred = self.transformer(x = x, cond = step_cond, text = text, time = t, mask = mask, drop_audio_cond = False, drop_text = False)
if cfg_strength < 1e-5:
return pred
null_pred = self.transformer(x = x, cond = step_cond, text = text, time = t, mask = mask, drop_audio_cond = True, drop_text = True)
return pred + (pred - null_pred) * cfg_strength
# noise input
# to make sure batch inference result is same with different batch size, and for sure single inference
# still some difference maybe due to convolutional layers
y0 = []
for dur in duration:
if exists(seed):
torch.manual_seed(seed)
y0.append(torch.randn(dur, self.num_channels, device = self.device, dtype=step_cond.dtype))
y0 = pad_sequence(y0, padding_value = 0, batch_first = True)
t_start = 0
# duplicate test corner for inner time step oberservation
if duplicate_test:
t_start = t_inter
y0 = (1 - t_start) * y0 + t_start * test_cond
steps = int(steps * (1 - t_start))
t = torch.linspace(t_start, 1, steps, device = self.device, dtype=step_cond.dtype)
if sway_sampling_coef is not None:
t = t + sway_sampling_coef * (torch.cos(torch.pi / 2 * t) - 1 + t)
trajectory = odeint(fn, y0, t, **self.odeint_kwargs)
sampled = trajectory[-1]
out = sampled
out = torch.where(cond_mask, cond, out)
if exists(vocoder):
out = out.permute(0, 2, 1)
out = vocoder(out)
return out, trajectory
def forward(
self,
inp: float['b n d'] | float['b nw'], # mel or raw wave
text: int['b nt'] | list[str],
*,
lens: int['b'] | None = None,
noise_scheduler: str | None = None,
):
# 接收梅尔频谱或音频波形(inp)和文本输入(text),并返回损失。
# handle raw wave
if inp.ndim == 2:
inp = self.mel_spec(inp)
inp = inp.permute(0, 2, 1)
assert inp.shape[-1] == self.num_channels
batch, seq_len, dtype, device, σ1 = *inp.shape[:2], inp.dtype, self.device, self.sigma
# handle text as string
if isinstance(text, list):
if exists(self.vocab_char_map):
text = list_str_to_idx(text, self.vocab_char_map).to(device)
else:
text = list_str_to_tensor(text).to(device)
assert text.shape[0] == batch
# lens and mask
if not exists(lens):
lens = torch.full((batch,), seq_len, device = device)
mask = lens_to_mask(lens, length = seq_len) # useless here, as collate_fn will pad to max length in batch
# get a random span to mask out for training conditionally 生成随机长度掩码,用于控制模型学习不同输入片段的条件生成。
frac_lengths = torch.zeros((batch,), device = self.device).float().uniform_(*self.frac_lengths_mask)
rand_span_mask = mask_from_frac_lengths(lens, frac_lengths)
if exists(mask):
rand_span_mask &= mask
# mel is x1
x1 = inp
# x0 is gaussian noise
x0 = torch.randn_like(x1)
# time step
time = torch.rand((batch,), dtype = dtype, device = self.device)
# TODO. noise_scheduler
# sample xt (φ_t(x) in the paper)
t = time.unsqueeze(-1).unsqueeze(-1)
φ = (1 - t) * x0 + t * x1
flow = x1 - x0
# only predict what is within the random mask span for infilling
cond = torch.where(
rand_span_mask[..., None],
torch.zeros_like(x1), x1
)
# transformer and cfg training with a drop rate
drop_audio_cond = random() < self.audio_drop_prob # p_drop in voicebox paper
if random() < self.cond_drop_prob: # p_uncond in voicebox paper
drop_audio_cond = True
drop_text = True
else:
drop_text = False
# if want rigourously mask out padding, record in collate_fn in dataset.py, and pass in here
# adding mask will use more memory, thus also need to adjust batchsampler with scaled down threshold for long sequences
# 生成初始噪声 x0 和随机时间 time,并计算 φ 和 flow,分别表示用于条件生成的输入和流匹配目标。
pred = self.transformer(x = φ, cond = cond, text = text, time = time, drop_audio_cond = drop_audio_cond, drop_text = drop_text)
# flow matching loss
loss = F.mse_loss(pred, flow, reduction = 'none')
loss = loss[rand_span_mask]
return loss.mean(), cond, pred
三、总结
F5-TTS 项目在语音合成技术领域取得了显著进展,通过创新的架构设计与前沿技术的引入,大幅提升了合成语音的自然度和流畅性。项目在多个关键模块上进行了优化,包括基于深度学习的模型改进、及音频后处理等方面,为生成具有真实感的语音铺平了道路。
本文是博主自己通过查阅一下资料和动手实现后,对于F5的论文和代码产生的自己的理解,具有一定理解偏差,如果有不同理解的小伙伴可以把自己的理解发到评论区,大家一起探讨学习。