使用F5-tts复刻音色
最近第一人称视角的视频很火,想试试看复刻一下电视剧中某个角色的音色。看了下字节的API,嗯。。。138元一个音色,还不包括合成语音的费用,算了还是看看开源项目吧。
随便搜了搜,发现了两个项目一个是openvoice,另一个是F5-tts。openvoice看介绍感觉比较麻烦,所以直接试了F5-tts。
安装步骤
# 安装conda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh# 安装 Miniconda - Linux 版本安装脚本
bash Miniconda3-latest-Linux-x86_64.sh# 添加清华大学的 Anaconda 镜像源(free 频道)以加速下载
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/# 添加清华大学的 Anaconda 镜像源(main 频道)以加速下载
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/# 设置显示频道地址,便于查看包的来源
conda config --set show_channel_urls yes# 创建名为 f5-tts 的 conda 环境,指定 Python 版本为 3.10
conda create -n f5-tts python=3.10# 激活刚创建的 conda 环境
conda activate f5-tts# 从 GitHub 克隆 F5-TTS 代码库
git clone https://github.com/SWivid/F5-TTS.git# 进入 F5-TTS 项目目录
cd F5-TTS# 以可编辑模式安装项目,会自动安装 pyproject.toml 中定义的所有依赖
pip install -e .# 安装ffmpeg
sudo apt update
sudo apt install ffmpeg -y
复刻并生成指定的音频
# 指定端口
f5-tts_infer-gradio --port 7860 --host 0.0.0.0
打开网站后,操作界面如下,步骤很简单
第一次运行的时候会很慢,因为后台需要先下载对应的模型,生成音频后听了不满意的话,可以反复生成,直到满意为止,然后点击右上角的下载下来
项目源码阅读
Gradio框架
可以看到这些命令对应的代码入口,f5-tts_infer-gradio
命令启动的就是一个Gradio web。
Gradio 是一个非常适合快速构建机器学习模型 Web 界面的 Python 库,学习它可以帮助你:
- 快速搭建模型的交互式演示页面
- 在本地或远程分享模型的 UI 界面
- 用于调试和展示深度学习模型
通过简单的代码如下, 即可构建一个简单的交互页面
import gradio as grdef greet(name):return "Hello " + name + "!"with gr.Blocks() as demo:name = gr.Textbox(label="Name")output = gr.Textbox(label="Output Box")greet_btn = gr.Button("Greet")greet_btn.click(fn=greet, inputs=name, outputs=output, api_name="greet")demo.launch()
页面如下,简单的几行代码就完成了输入、调用函数返回结果、输出返回结果
回到项目代码中,可以看到web是这样组织得到的
找到对应button绑定click函数的代码
在 Gradio 中,函数返回值传递给 UI 组件的过程是通过事件处理系统完成的。以 F5-TTS 应用中的 basic_tts
函数为例:
@gpu_decorator
def basic_tts(ref_audio_input,ref_text_input,gen_text_input,remove_silence,randomize_seed,seed_input,cross_fade_duration_slider,nfe_slider,speed_slider,
):if randomize_seed:seed_input = np.random.randint(0, 2**31 - 1)audio_out, spectrogram_path, ref_text_out, used_seed = infer(ref_audio_input,ref_text_input,gen_text_input,tts_model_choice,remove_silence,seed=seed_input,cross_fade_duration=cross_fade_duration_slider,nfe_step=nfe_slider,speed=speed_slider,)return audio_out, spectrogram_path, ref_text_out, used_seed
事件绑定
generate_btn.click(basic_tts,inputs=[ref_audio_input,ref_text_input,gen_text_input,remove_silence,randomize_seed,seed_input,cross_fade_duration_slider,nfe_slider,speed_slider,],outputs=[audio_output, spectrogram_output, ref_text_input, seed_input],
)
值映射过程
- 当点击
generate_btn
时,Gradio 调用basic_tts
函数 - 函数参数来自
inputs
列表中指定的 UI 组件的当前值 - 函数返回一个元组,其中每个元素按顺序映射到
outputs
列表中的组件 - 第一个返回值
audio_out
映射到audio_output
- 第二个返回值
spectrogram_path
映射到spectrogram_output
- 第三个返回值
ref_text_out
映射到ref_text_input
- 第四个返回值
used_seed
映射到seed_input
语音模型
不是搞AI的,下面只记录些搜索来的基本概念,不保证是对的,仅个人观点
在 F5-TTS 项目中,主要提供了两个文本到语音 (TTS) 模型
- F5-TTS
- E2-TTS
F5-TTS
F5-TTS
全称为"A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching"(使用流匹配技术生成流畅且忠实语音的讲故事者)这个模型采用了近年来在图像和语音生成中非常成功的 扩散模型(Diffusion Model) 框架。
扩散模型的本质是"先加噪声,再去噪声"
- 训练阶段:把真实数据(如图像、语音)逐步添加噪声,直到变成接近纯噪声的形式(像模糊图、沙沙声等);
- 生成阶段:从随机噪声出发,通过一个训练好的模型一步步“去噪”还原出真实数据。
为什么叫“扩散”?
它借用了物理学中**扩散过程(Diffusion Process)**的概念。你可以把“加噪声”看作是一种扩散(从结构明确的数据变成无序的噪声),然后再反过来学习“反扩散”过程(从混乱中恢复秩序)。
E2-TTS
这里的E2 TTS 是一种 零样本语音合成(Zero-Shot TTS)模型。 这个搜出来的东西不多,有需要可以自己去看看论文
https://arxiv.org/abs/2406.18009
加载模型
web启动的时候,默认就会加载f5-tts模型。
"hf://..."
是一种 Hugging Face 模型资源路径的简写形式。它等价于标准 Hugging Face 文件地址,例如:
"hf://SWivid/F5-TTS/F5TTS_v1_Base/model_1250000.safetensors"
等价于
https://huggingface.co/SWivid/F5-TTS/resolve/main/F5TTS_v1_Base/model_1250000.safetensors
hugging face上找对应的模型
路径的结构是:
hf://<用户名>/<仓库名>/<子文件夹>/<文件名>
所以你访问的实际 URL 是:
https://huggingface.co/SWivid/F5-TTS/tree/main/F5TTS_v1_Base/
safetensors文件格式
.safetensors
是由 Hugging Face 推出的安全模型格式,主要用于替代 .pt
格式以防止 pickle 反序列化带来的安全风险。虽然不是 PyTorch 原生格式,但可以很好地与 PyTorch 一起使用,只需安装一个轻量库即可。
pip install safetensors
加载代码大致如下
from safetensors.torch import load_file
import torch
import torch.nn as nn# 定义模型
model = MyModel() # 你自己的模型定义# 加载 safetensors 格式的参数
state_dict = load_file("model.safetensors")# 加载到模型中
model.load_state_dict(state_dict)
model.eval()
音频处理代码
这段代码实现了对参考音频的预处理操作,是 F5-TTS 系统中批量推理过程的重要组成部分。具体d代码如下:
audio, sr = ref_audio
if audio.shape[0] > 1:audio = torch.mean(audio, dim=0, keepdim=True)rms = torch.sqrt(torch.mean(torch.square(audio)))
if rms < target_rms:audio = audio * target_rms / rms
if sr != target_sample_rate:resampler = torchaudio.transforms.Resample(sr, target_sample_rate)audio = resampler(audio)
audio = audio.to(device)
这段代码确保了无论输入参考音频的格式、音量或采样率如何,都能被标准化为模型所期望的格式,从而提高语音合成的质量和一致性。
立体声转单声道处理
if audio.shape[0] > 1:audio = torch.mean(audio, dim=0, keepdim=True)
- 检查音频是否为立体声(多个通道)
- 如果是,通过对所有通道取均值将其转换为单声道
keepdim=True
保持张量维度结构一致
为什么转换?
- 语音识别或模型训练时,大多数模型只支持单声道输入
- 转换可以减少计算资源、统一数据形态
音量标准化
rms = torch.sqrt(torch.mean(torch.square(audio)))
if rms < target_rms:audio = audio * target_rms / rms
- 计算音频的均方根(RMS)值,这是音频音量的度量
- 如果音频 RMS 低于目标值 (
target_rms
,默认 0.1),则进行音量提升 - 通过简单的比例缩放使音频达到标准音量水平
- 注意:这里只对音量过低的音频进行提升,而不会降低过响的音频
采样率调整
if sr != target_sample_rate:resampler = torchaudio.transforms.Resample(sr, target_sample_rate)audio = resampler(audio)
- 检查音频的采样率是否与目标采样率匹配(默认 24000Hz)
- 如果不匹配,使用 torchaudio 的 Resample 转换器调整采样率
- 这确保无论输入音频格式如何,模型始终处理相同采样率的音频
为什么要采样?
现实中声音是“连续”的,但计算机只能处理“离散数据”。
- 采样就是将连续的声音波形,以一定频率转换成数字序列
- 每秒采的点越多,声音保真度越高,但数据量越大
奈奎斯特定理(Nyquist Theorem)
为了完整还原频率为 f
的信号,采样率应至少为 2f
.比如:人类听力最高大约 20kHz,采样率应不低于 40kHz → CD 采样率为 44.1kHz。
设备迁移
audio = audio.to(device)
- 将音频张量移动到指定计算设备(GPU/CPU)
- 确保后续计算在正确的设备上执行
音频张量是什么?
在深度学习中,“张量”就是多维数组(n 维矩阵)的统称。音频本质是:一段声音的数字采样值的序列。这些数字构成了张量结构,就称为“音频张量”。张量是深度学习的通用数据结构.
- 可以直接送入神经网络模型
- 支持 GPU 加速(
.to('cuda')
) - 易于做各种运算(卷积、傅里叶变换、归一化、拼接等)
音频张量具体是什么样子的数据
(channels, samples)
—— torchaudio 默认加载格式
channels
: 有多少个声道?(1 = 单声道,2 = 立体声)samples
: 每个声道的采样点数量 = 时长 × 采样率
举个例子: (2, 16000) 可以理解为“两个数组,每个数组里有 16000 个浮点数字,每个数字表示一小段声音的振幅。”
[
[左声道数据0, 左声道数据1, …,左声道数据16000],
[右声道数据0, 右声道数据1, …,右声道数据16000]
]
为什么要移动?
- PyTorch 的模型和数据必须在同一计算设备(CPU 或 GPU),否则会报错
- GPU 运算速度远快于 CPU,适合大批量数据或深度模型
计算音频时长
动态时长计算(默认模式)
# Calculate duration
ref_text_len = len(ref_text.encode("utf-8"))
gen_text_len = len(gen_text.encode("utf-8"))
duration = ref_audio_len + int(ref_audio_len / ref_text_len * gen_text_len / local_speed)
使用比例方法计算所需时长:ref_audio_len / ref_text_len * gen_text_len / local_speed
ref_audio_len
是参考音频时长,ref_text_len
是参考文本长度,gen_text_len
是要生成文本的长度,local_speed
是语速
ref_audio_len / ref_text_len
计算参考音频的讲话速度(每个字节多少帧)- 乘以
gen_text_len
得到生成文本按相同速度需要的帧数 - 除以
local_speed
调整语速(大于1加速,小于1减速)
这种计算方法确保了生成的语音保持与参考音频相似的讲话风格和速度,同时允许用户通过 speed
参数灵活调整。
TTS 模型推理核心代码
这段代码是 F5-TTS 系统中语音生成的核心部分,实现了从文本到语音的转换过程。以下是详细解析:
# inference
with torch.inference_mode():generated, _ = model_obj.sample(cond=audio,text=final_text_list,duration=duration,steps=nfe_step,cfg_strength=cfg_strength,sway_sampling_coef=sway_sampling_coef,)del _generated = generated.to(torch.float32) # generated mel spectrogramgenerated = generated[:, ref_audio_len:, :]generated = generated.permute(0, 2, 1)if mel_spec_type == "vocos":generated_wave = vocoder.decode(generated)elif mel_spec_type == "bigvgan":generated_wave = vocoder(generated)if rms < target_rms:generated_wave = generated_wave * rms / target_rms# wav -> numpygenerated_wave = generated_wave.squeeze().cpu().numpy()if streaming:for j in range(0, len(generated_wave), chunk_size):yield generated_wave[j : j + chunk_size], target_sample_rateelse:generated_cpu = generated[0].cpu().numpy()del generatedyield generated_wave, generated_cpu
生成梅尔频谱图
语音合成系统的后半部分流程:文本 → 梅尔频谱图 → 音频。 梅尔频谱图已经包含了要说的内容,语调、音色、速度等。梅尔频谱图可视化后长这个样子
下面的代码就是生成梅尔频谱图的代码,generated
是梅尔频谱图的张量表示
with torch.inference_mode():generated, _ = model_obj.sample(cond=audio,text=final_text_list,duration=duration,steps=nfe_step,cfg_strength=cfg_strength,sway_sampling_coef=sway_sampling_coef,)
torch.inference_mode()
: 启用推理模式,禁用梯度计算以节省内存和提高速度model_obj.sample()
: 调用模型的采样方法,生成梅尔频谱图cond=audio
: 条件输入,即参考音频text=final_text_list
: 要生成的文本(已预处理,包含拼音转换)duration
: 计算得到的输出音频持续时间(帧数)steps=nfe_step
: 流匹配扩散模型的推理步数(默认32)cfg_strength
: 分类器引导强度,控制生成内容对条件的依赖程度sway_sampling_coef
: 摇摆采样系数,影响语音多样性
梅尔频谱图处理
generated = generated.to(torch.float32) # generated mel spectrogram
generated = generated[:, ref_audio_len:, :]
generated = generated.permute(0, 2, 1)
有些模型输出是 float16
或其他精度,为了兼容 vocoder (声码器)等组件,这里转换为 float32
generated.to(torch.float32)
: 确保频谱图为浮点32位格式generated[:, ref_audio_len:, :]
: 裁剪掉参考音频部分,只保留生成的语音generated.permute(0, 2, 1)
: 重新排列张量维度以匹配声码器的输入要求
声码器解码
Vocoder(声码器) 是一种把语音的“特征表示”还原成可听波形的工具或模型。它是语音合成系统(如 TTS、语音克隆、语音转换)中的最后一个步骤。它接受类似 梅尔频谱图 的特征图,输出“听得见的声音”。
if mel_spec_type == "vocos":generated_wave = vocoder.decode(generated)
elif mel_spec_type == "bigvgan":generated_wave = vocoder(generated)# wav -> numpy
generated_wave = generated_wave.squeeze().cpu().numpy()
两种声码器
vocos
: 较新的神经声码器bigvgan
: NVIDIA的高质量声码器
这里声码器(vocoder)将梅尔频谱图转换为音频波形,得到的 generated_wave
是一个 PyTorch
张量(tensor)。张量转numpy的原因是后续的音频拼接、交叉淡入淡出等操作使用 NumPy 函数更方便
音量调整
if rms < target_rms:generated_wave = generated_wave * rms / target_rms
- 只有当参考音频音量低于目标音量时才调整生成音频
- 确保生成的音频与参考音频音量水平相匹配
转换为 NumPy 数组
generated_wave = generated_wave.squeeze().cpu().numpy()
squeeze()
: 移除大小为1的维度cpu()
: 将张量从 GPU 移至 CPUnumpy()
: 转换为 NumPy 数组以便后续处理
音频交叉淡入淡出拼接
在语音合成中,每句可能是分段生成的波形,例如一段文字生成了多个音频段
"你好,今天" → 波形A
"过得怎么样?" → 波形B
这个时候就要使用交叉淡入淡出拼接多个音频,让合成的音频听起来更自然。
交叉淡入淡出(Crossfade) 是一种常用的音频拼接技术,用于平滑地连接多个音频片段,避免听起来突兀或出现明显断点。
可以理解为将两段音频在头尾处重叠在一起,例如,A音频5秒,B音频3秒直接拼接是一个8秒的C音频,交叉淡入淡出是将A最后一秒和B音频的头一秒重叠在一起,此时得到的是一个7秒的音频。
A音频最后一秒的音量是逐渐减弱的,B音频是从弱变强的。
# 使用交叉淡入淡出技术拼接所有生成的波形final_wave = generated_waves[0]for i in range(1, len(generated_waves)):prev_wave = final_wavenext_wave = generated_waves[i]# 计算交叉淡入淡出的样本数,确保不超过波形长度cross_fade_samples = int(cross_fade_duration * target_sample_rate)cross_fade_samples = min(cross_fade_samples, len(prev_wave), len(next_wave))if cross_fade_samples <= 0:# 无法重叠,直接拼接final_wave = np.concatenate([prev_wave, next_wave])continue# 重叠部分prev_overlap = prev_wave[-cross_fade_samples:]next_overlap = next_wave[:cross_fade_samples]# 淡出和淡入权重fade_out = np.linspace(1, 0, cross_fade_samples)fade_in = np.linspace(0, 1, cross_fade_samples)# 创建交叉淡变的重叠部分cross_faded_overlap = prev_overlap * fade_out + next_overlap * fade_in# 组合最终波形new_wave = np.concatenate([prev_wave[:-cross_fade_samples], cross_faded_overlap, next_wave[cross_fade_samples:]])final_wave = new_wave# 创建合并的频谱图combined_spectrogram = np.concatenate(spectrograms, axis=1)