mlx-audio Echo TTS:基于扩散模型的快速语音克隆 TTS 实现全解
mlx-audio Echo TTS:基于扩散模型的快速语音克隆 TTS 实现全解
本文以 Echo TTS 模块文档 为主体,系统讲解 mlx-audio 中 Echo TTS 的两种使用方式(Python API 与 CLI)、完整配置参数、扩散生成架构与推理管线。读完本文,你将掌握如何加载 mlx-community/echo-tts-base 完成带参考音频的零样本语音克隆,并理解其文本/说话人双编码器、双重 CFG 采样器、Fish S1 DAC 编解码与 PCA 潜空间压缩在源码中的具体实现。
Echo TTS 是什么
Echo TTS 是一个基于扩散模型(Diffusion)的文本转语音模型,特点是快速、高保真的语音克隆:只需一段参考音频,即可让合成语音复刻该说话人的音色。在 TTS 模型列表 中,Echo TTS 被标注为 "Diffusion-based, fast voice cloning",支持语言为英文(EN)。
在 mlx-audio 中,它的实现位于 mlx_audio/tts/models/echo_tts/ 目录,由以下源码文件构成:
| 文件 | 职责 |
|---|---|
| echo_tts.py | 对外 Model 类:加载、generate() 推理入口 |
| model.py | 核心 EchoDiT 扩散主干(文本/说话人编码器、联合注意力) |
| sampling.py | Euler ODE 采样器(一次性生成 + 块状生成) |
| audio.py | Fish S1 DAC 编解码、PCA 潜空间压缩、静音裁剪 |
| text.py | 文本规范化与字节级分词 |
| config.py | ModelConfig / EchoDiTConfig / SamplerConfig 三层配置 |
模块通过 mlx_audio/tts/utils.py 中的 "echo_tts": "echo_tts" 映射注册进统一加载器,因此可以像其他 TTS 模型一样用 load() 一行加载。
快速上手:Python API 与 CLI
原始文档给出的两种用法如下,均可直接复制运行。
Python API
from mlx_audio.tts import load
model = load("mlx-community/echo-tts-base")
result = next(model.generate("Hello from Echo TTS.", ref_audio="speaker.wav"))
audio = result.audio
要点说明(对照 echo_tts.py 中 Model.generate 的签名):
ref_audio支持文件路径字符串或mx.array波形数组;传路径时会按模型采样率 44100 Hz 重新加载(内部调用load_audio)。- 若参考音频是立体声,源码会自动取两声道均值降为单声道(
audio.ndim == 2 and audio.shape[0] > 1分支)。 generate()是一个生成器,next(...)取出首个GenerationResult,其中result.audio是mx.array波形;同时result还携带samples、sample_rate、audio_duration、real_time_factor(RTF)、peak_memory_usage等指标,方便你做性能评估。- 其余
**kwargs会透传给采样器,凡与SamplerConfig字段同名的参数(如num_steps、cfg_scale_text)都会被采样器识别,无关参数(如speed)会被静默忽略。 generate(stream=True)目前会抛出NotImplementedError——Echo TTS 尚不支持流式输出,这是文档之外的一个明确限制。rng_seed控制随机种子(默认 0),保证同参数可复现。
CLI
python -m mlx_audio.tts.generate --model mlx-community/echo-tts-base \
--text "Hello from Echo TTS." --ref-audio speaker.wav
需要提醒的是,原始 README 中该行写成了 mmlx-community/echo-tts-base,多了一个 m,属于笔误,实际模型 ID 应为 mlx-community/echo-tts-base(与 Python API 示例一致)。CLI 入口 mlx_audio/tts/generate.py 的 --ref-audio 参数同样支持路径或数组形式,并在文件不存在时给出明确报错(Reference audio file not found: ...)。
配置体系:三层 Dataclass 与全部默认值
Echo TTS 的配置集中在 config.py,由 ModelConfig 聚合 EchoDiTConfig(模型结构)和 SamplerConfig(采样行为)两个子配置。完整默认值如下,均可在模型仓库的 config.json 中按字段覆盖(ModelConfig.from_dict 支持 dit、sampler 两个嵌套字典)。
ModelConfig(顶层)
| 参数 | 默认值 | 含义 |
|---|---|---|
model_type |
"echo_tts" |
模型类型标识 |
sample_rate |
44100 |
输出音频采样率(CD 级,高于常见 16k/24k TTS) |
max_text_length |
768 |
文本 token 最大长度,超过部分截断 |
max_speaker_latent_length |
6400 |
参考音频潜序列上限(帧),对应 6400 × 2048 ≈ 13.1 秒×640 的音频上限,见下文 |
audio_downsample_factor |
2048 |
每帧潜向量对应的音频采样数,即 44100/2048 ≈ 21.5 fps 潜帧率 |
normalize_text |
True |
是否做文本规范化(见下文) |
delete_blockwise_modules |
False |
是否删除块状生成专用模块以省内存(见下文) |
pca_filename |
"pca_state.safetensors" |
模型目录下 PCA 状态文件名 |
fish_codec_repo |
"jordand/fish-s1-dac-min" |
Fish S1 DAC 编解码器仓库 |
model_path |
None |
可选本地权重路径 |
EchoDiTConfig(扩散主干结构)
| 参数 | 默认值 | 说明 |
|---|---|---|
latent_size |
80 |
潜向量维度(PCA 压缩后的维度) |
model_size / num_layers / num_heads |
2048 / 24 / 16 |
DiT 主干宽度、层数、注意力头数 |
intermediate_size |
5888 |
MLP 中间维度 |
norm_eps |
1e-5 |
归一化 epsilon |
text_vocab_size |
256 |
文本词表大小(UTF-8 字节 + BOS) |
text_model_size / text_num_layers / text_num_heads / text_intermediate_size |
1280 / 14 / 10 / 3328 |
文本编码器规模 |
speaker_patch_size |
4 |
说话人潜帧 patch 大小(4 帧合并为 1 个 token) |
speaker_model_size / speaker_num_layers / speaker_num_heads / speaker_intermediate_size |
1280 / 14 / 10 / 3328 |
说话人编码器规模 |
timestep_embed_size |
512 |
扩散时间步正弦嵌入维度 |
adaln_rank |
256 |
AdaLN 低秩条件投影秩 |
SamplerConfig(采样行为)
| 参数 | 默认值 | 说明 |
|---|---|---|
num_steps |
40 |
Euler ODE 步数,质量/速度权衡主旋钮 |
cfg_scale_text |
3.0 |
文本条件 CFG 强度 |
cfg_scale_speaker |
8.0 |
说话人条件 CFG 强度(克隆相似度主旋钮) |
cfg_min_t / cfg_max_t |
0.5 / 1.0 |
只在 t ∈ [0.5, 1.0] 的高噪声区间启用 CFG,低噪声段只做单前向,节省约一半条件推理 |
truncation_factor |
None → 实际取 0.96 |
初始噪声截断系数,防止初始 x_t 过大 |
rescale_k / rescale_sigma |
None |
启用后对速度场做时间相关的 score rescale |
speaker_kv_scale / speaker_kv_max_layers / speaker_kv_min_t |
None / None / None |
说话人 KV 缓存动态缩放三件套:speaker_kv_scale 放大克隆强度,speaker_kv_max_layers 限制作用的层数,speaker_kv_min_t 指定在某时间步之后将缩放还原(见采样器实现) |
sequence_length |
640 |
一次性生成时的潜序列长度,640 × 2048 / 44100 ≈ 29.5 秒音频 |
模型架构:文本、说话人、潜变量三路联合注意力
核心主干是 model.py 中的 EchoDiT,它是一个把文本条件、说话人条件、自注意力统一在同一个 Transformer 里的扩散模型。从源码结构看,其组成如下:
1. 文本编码器(TextEncoder)
一个 14 层、宽度 1280 的双向(非因果)Transformer,输入是 text_vocab_size=256 上的嵌入——即 UTF-8 字节级 token。文本分词逻辑在 text.py 中:
normalize_text_prompt会先把省略号、弯引号、冒号/分号统一替换,并给文本加上[S1]说话人标签前缀(若文本未以[、(开头且不含S1/S2标记);tokenizer_encode直接对 UTF-8 字节序列编码并在开头插入 BOS token(id=0)。
文本状态会被逐层投影成 KV 缓存(get_kv_cache_text),之后每个扩散步都复用这份缓存而非重算文本编码——这是把条件计算从 O(步数 × 文本长度) 降为 O(文本长度) 的关键优化。
2. 说话人编码器(SpeakerEncoder)与 latent 前缀编码器
SpeakerEncoder 与文本编码器同构(14 层、1280),但注意两点:
- 输入不是音频,而是 Fish S1 DAC 编码再经 PCA 压缩后的说话人潜向量,每
speaker_patch_size=4帧合并为一个 token(in_proj将latent_size × patch_size = 320维投到 1280 维,且除以 6.0 做尺度校正); - 它使用因果注意力,与文本编码器的双向注意力形成对比。
当 delete_blockwise_modules=False 时,模型还会额外构建一个同构的 latent_encoder + latent_norm,用于把已生成的潜序列也编码成 KV 缓存(get_kv_cache_latent)。这正是块状/续写生成的基础:新生成块可以"看到"之前所有块的潜变量。
3. 联合注意力(JointAttention)
DiT 的 24 个 TransformerBlock 中,注意力不再是单一自注意力,而是把四类 K/V 拼接在一起做单次 SDPA:
xk = [ 自身潜变量 K | 已生成潜前缀 K | 文本 K | 说话人 K ]
- 自身注意力部分应用 RoPE 位置编码(
_apply_rotary_half只对序列前半施加旋转,与 latent 位置约定配合); - 输出乘以一个 sigmoid 门控(
output * mx.sigmoid(gate)),属于 gated attention 设计; - Q/K 均带 per-head RMSNorm(类似 QK-Norm),增强长序列训练稳定性。
时间步 t 经正弦嵌入后通过 cond_module(两层 MLP + 3 倍投影)得到条件向量,再经两个 LowRankAdaLN 模块分别调制注意力和 MLP 子层:低秩(rank=256)下投影 + SiLU + 上投影,生成 shift/scale/gate,实现 DiT 经典的 AdaLN-Zero 条件注入,且 gate 经 tanh 约束在 [-1, 1]。
推理管线:从文本与参考音频到波形
Model.generate() 的完整数据流(echo_tts.py)可以拆解为五步:
- 文本准备:
_prepare_text走规范化 → 字节分词 → 打包出text_input_ids与text_mask(pad_to_max=False,即不填充到 768,实际多短多长)。 - 说话人潜向量提取:若提供了
ref_audio,调用get_speaker_latent_and_mask(audio.py):- 参考音频先截断到
max_speaker_latent_length × 2048采样,即 6400 × 2048 ≈ 13.1 秒(44.1kHz 下); - 按 640 × 2048 的块切分,逐块用 Fish S1 DAC
encode_zq得到 1024 维潜帧,再经 PCA 白化降维:z_q = (z_q - pca_mean) @ pca_components.T × latent_scale,把 1024 维压到 80 维——latent_size=80由此而来; - 生成逐帧
speaker_mask,并将总长裁剪到 4 的倍数以匹配 patch size。 若不提供参考音频,则退化为全零潜向量 + 全零 mask,即无克隆条件。
- 参考音频先截断到
- 扩散采样:
generate_latents先算好文本/说话人 KV 缓存,再调用sample_euler_cfg_independent_guidances(sampling.py),按sequence_length(默认 640 帧 ≈ 29.5 秒)一次性采样;若传入block_sizes(如[160, 160, ...])则走块状采样器。 - DAC 解码:
ae_decode先把 80 维潜向量逆 PCA 还原为 1024 维,交给 Fish S1 DACdecode_zq还原为 44.1kHz 波形。Fish S1 DAC 本身在 mlx_audio/codec/models/fish_s1_dac/ 中实现;注意它是在post_load_hook里通过FishS1DAC.from_pretrained(config.fish_codec_repo)从jordand/fish-s1-dac-min仓库拉取的,且加载失败时fish_ae会被置空并延迟到generate时报错——因此必须经由mlx_audio.tts.load()完整加载管线才能正常工作。 - 尾部裁剪:
crop_audio_to_flattening_point用一个滑窗(20 帧)检测潜序列中方差小于 0.05 且均值接近 0 的第一个"平坦点"(audio.py),把该点之后的波形全部裁掉。由于固定长度采样会产生尾部静音/塌缩,这一步保证了输出时长贴合实际内容。
采样器深解:双重 CFG 与时间步门控
Echo TTS 的采样器是理解其"克隆强、生成快"的关键。sample_euler_cfg_independent_guidances 的每个时间步逻辑(sampling.py):
t_schedule = np.linspace(1.0 * 0.999, 0.0, num_steps + 1) # 从 t=0.999 走到 0
x_t = mx.random.normal((B, sequence_length, latent_size)) * trunc # trunc 默认 0.96
for i in range(num_steps):
has_cfg = cfg_min_t <= t <= cfg_max_t # 默认只有 t ∈ [0.5, 1.0] 才开 CFG
if has_cfg:
# 一次前向同时算 3 份:条件 / 去文本 / 去说话人(batch 拼接)
v_pred = (v_cond
+ cfg_scale_text * (v_cond - v_uncond_text)
+ cfg_scale_speaker* (v_cond - v_uncond_speaker))
else:
v_pred = model(仅条件分支)
x_t = x_t + v_pred * (t_next - t) # Euler 步进(v-prediction 形式)
几个值得注意的设计:
- 独立双 CFG:文本与说话人各有一个 CFG 强度,且"无条件"分支是通过把对应 mask 置零实现的(
text_mask_uncond/speaker_mask_uncond),不依赖额外权重; - CFG 时间窗:默认
cfg_min_t=0.5,意味着 40 步里有大约一半步只做 1 次前向而非 3 次,把 CFG 的算力开销压到约 2/3 批处理规模; - 初始噪声截断:
truncation_factor=0.96(DEFAULT_TRUNCATION_FACTOR)缩放初始高斯噪声,避免极端初始值导致波形爆音; - Score rescale:同时提供
rescale_k与rescale_sigma时,按 SNR = ((1-t)/t)² 对速度场做_temporal_score_rescale,用于抑制 CFG 过冲带来的能量漂移; - 说话人 KV 动态缩放:设置
speaker_kv_scale后,采样开始前把说话人 KV 缓存整体乘上该系数(可限制到前speaker_kv_max_layers层);当时间步跨过speaker_kv_min_t时再乘回1/scale还原。从源码结构看,这是一种"前段强克隆、后段保自然度"的软调度,用来在克隆相似度与音色伪影之间调参。
块状生成:latent 前缀 KV 与流式潜力
sample_blockwise_euler_cfg_independent_guidances(sampling.py)实现了按块递进生成:
- 通过
generate(text=..., block_sizes=[160, 160, 160], ...)指定每块潜帧数(块之间拼接); - 每块开始前,把已生成潜序列经
latent_encoder编码为kv_latent缓存,新块注意力时可看到全部历史,保证跨块韵律连贯; - 支持
continuation_latent参数续写:以已有潜序列为起点继续生成,是长文本/连续对话场景的潜在入口; - 块状路径依赖
latent_encoder、latent_norm及wk_latent/wv_latent投影。若你在配置里设了delete_blockwise_modules=True省内存,调用块状生成会抛出ValueError("Blockwise generation requires latent-prefix modules...");sanitize权重时也会把latent_encoder.、latent_norm.、.wk_latent.、.wv_latent.开头的键直接丢弃(echo_tts.py)。
文本处理的细节
text.py 的规范化策略反映了模型的训练分布约定:
[S1]前缀:默认给文本加上说话人 1 标记,除非文本已显式以[、(开头或包含S1/S2(即模型原生支持多说话人标记文本,你可以直接写[S1] Hello. [S2] Hi.之类的内容);- 标点替换:
…→...、’→'、”→"、:;→,、—→", "、换行→空格,减少未见过字符对字节嵌入的扰动; - 该流程受
normalize_text配置控制,设False可传入完全原始文本(对应测试 mlx_audio/tts/tests/test_echo_tts.py 中对规范化与分词的覆盖)。
性能与限制须知
- 采样率 44100Hz:明显高于多数 16k/24k 的开源 TTS,但相应地 Fish S1 DAC 解码与 24 层 DiT 的显存/算力开销也更高;
GenerationResult.peak_memory_usage会报告 GB 级峰值内存,方便你按机型评估。 - 单次最长约 30 秒:
sequence_length=640帧 × 2048 / 44100 ≈ 29.5 秒。更长的文本应使用block_sizes分块生成,而不是盲目调大sequence_length。 - 参考音频最长约 13 秒:超出
max_speaker_latent_length=6400帧的部分会被直接截断。 - 不支持流式:
stream=True明确抛NotImplementedError;块状生成目前仍是整批产出。 - 许可证:README 明确说明 Echo-TTS 与 Fish S1 权重均以
CC-BY-NC-SA-4.0发布,非商用用途之外需另行获得模型作者授权。商用集成前务必确认这一点。
小结
mlx-audio 中的 Echo TTS 把"扩散生成 + 强克隆"落到了一套相当完整的工程实现上:条件 KV 缓存复用避免了每步重算文本/说话人编码,CFG 时间窗与独立双 CFG 强度提供了质量/速度/相似度的三向调节,PCA 压缩 + Fish S1 DAC 兼顾了潜空间紧凑与波形保真,块状生成与 latent 前缀 KV 则为长音频铺了路。上手只需两行代码(load("mlx-community/echo-tts-base") + generate(text, ref_audio=...)),而调优空间则集中在 SamplerConfig 的 num_steps、cfg_scale_speaker 与 speaker_kv_* 三组参数上。
相关路径索引
- 模块文档:mlx_audio/tts/models/echo_tts/README.md
- 模型实现:mlx_audio/tts/models/echo_tts/echo_tts.py、model.py、sampling.py
- 音频/文本前端:audio.py、text.py、config.py
- Fish S1 DAC 编解码器:mlx_audio/codec/models/fish_s1_dac/
- 统一加载与 CLI:mlx_audio/tts/utils.py、mlx_audio/tts/generate.py
- 单元测试:mlx_audio/tts/tests/test_echo_tts.py
- 模型列表(含语言与克隆能力标注):docs/models/tts/index.md