mlx-audio 中的 Irodori-TTS:48kHz 日语 Flow Matching 语音合成,从声音克隆到自动时长预测

原创2026-09-15 10:00:18417 阅读
文章标签:语音音频人工智能本地部署模型推理服务

mlx-audio 中的 Irodori-TTS:48kHz 日语 Flow Matching 语音合成,从声音克隆到自动时长预测

Irodori-TTS 是 mlx-audio 内置的一款日语 TTS 模型,基于 Rectified Flow DiT 在 48kHz 连续 DACVAE 潜空间上做扩散采样,架构与训练方式沿袭 Echo-TTS。本文以仓库内 Irodori-TTS 模块文档 为主线,结合 irodori_tts.py 等源码实现,完整讲解 v1 至 v4.1 各版本模型的选型、声音克隆与 VoiceDesign 的调用方式、自动时长预测与 Sway Sampling 的参数原理,以及 16GB 统一内存机器上的低显存调优方案。读完本文,你能够在 Apple Silicon 上跑通日语语音合成、按需求挑选模型档位,并理解每个采样参数在底层代码中究竟如何生效。

一、模型总览:Rectified Flow DiT + DACVAE 的日语 TTS

Irodori-TTS 在 MLX 上的移植核心思路是:文本(以及可选的说话人参考音频、风格描述文本)作为条件,在一个 Rectified Flow(直线化流)DiT 中从噪声逐步去噪出连续 DACVAE 潜变量,最后由 DACVAE 解码器把潜变量还原为 48kHz 波形。从 config.py 的 ModelConfig 可以看到关键常量:采样率 48000Hz,DACVAE 下采样因子 audio_downsample_factor=1920(即每 1920 个采样点压缩为 1 个 latent 帧,约 25 帧/秒),参考音频潜变量硬上限 max_speaker_latent_length=6400。

模型各版本的核心差异在于条件能力与捆绑的组件,README 给出的完整模型矩阵如下:

v4.1(推荐)

v4.1-Small 是单一统一模型:声音克隆、VoiceDesign 与自动时长预测合并在一个 checkpoint 中。

模型 权重来源 条件能力
mlx-community/Irodori-TTS-v4.1-Small-fp16 HuggingFace mlx-community 组织 声音克隆 + VoiceDesign + 自动时长
mlx-community/Irodori-TTS-v4.1-Small-8bit HuggingFace mlx-community 组织 声音克隆 + VoiceDesign + 自动时长

v4

模型 权重来源 条件能力
mlx-community/Irodori-TTS-v4-Small-fp16 HuggingFace mlx-community 组织 声音克隆 + VoiceDesign + 自动时长
mlx-community/Irodori-TTS-v4-Small-8bit HuggingFace mlx-community 组织 声音克隆 + VoiceDesign + 自动时长

v3

模型 权重来源 条件能力
mlx-community/Irodori-TTS-500M-v3-fp16 HuggingFace mlx-community 组织 声音克隆 + 自动时长
mlx-community/Irodori-TTS-500M-v3-8bit HuggingFace mlx-community 组织 声音克隆 + 自动时长
mlx-community/Irodori-TTS-600M-v3-VoiceDesign-fp16 HuggingFace mlx-community 组织 声音克隆 + VoiceDesign(双条件)
mlx-community/Irodori-TTS-600M-v3-VoiceDesign-8bit HuggingFace mlx-community 组织 声音克隆 + VoiceDesign(双条件)

v2

模型 权重来源 条件能力
mlx-community/Irodori-TTS-500M-v2-fp16 / -8bit / -4bit HuggingFace mlx-community 组织 声音克隆(参考音频)
mlx-community/Irodori-TTS-500M-v2-VoiceDesign-fp16 / -8bit / -4bit HuggingFace mlx-community 组织 语音设计(文本描述)

v1

模型 权重来源
mlx-community/Irodori-TTS-500M-fp16 HuggingFace mlx-community 组织

从源码结构看,这些版本共用同一套 IrodoriDiTConfig(config.py),差异由 checkpoint 内的 config.json 控制:use_caption_condition 决定是否存在 VoiceDesign 分支,use_duration_predictor 决定是否启用时长预测头,text_encoder_type(scratch/pretrained)区分 v2/v3 的自训练文本编码器与 v4 的预训练编码器。DACVAE 方面,v2/v3/v4 系列使用 32 维 Semantic-DACVAE-Japanese-32dim(已捆绑在转换后的权重中,v4 还会随 checkpoint 捆绑 ModernBERT-ja-310m 文本编码器,同精度下体积比 v3 大约 1GB);v1 使用 facebook/dacvae-watermarked,首次使用时自动下载——这一逻辑实现在 irodori_tts.py 的 post_load_hook 中:优先加载模型目录下的本地 dacvae/ 子目录,找不到才回退到从 HuggingFace 仓库下载。

二、快速上手:声音克隆

最直接的用法是通过 mlx-audio 统一的 generate_audio 入口传入参考音频,generate.py 会将 ref_audio 列表逐项加载为与模型同采样率的波形后交给模型:

from mlx_audio.tts.generate import generate_audio

generate_audio(
    model="mlx-community/Irodori-TTS-v4.1-Small-fp16",
    text="今日はいい天気ですね。",
    ref_audio="speaker.wav",
    file_prefix="output",
)

也可以走 CLI,--ref_audio 支持重复传参以提供多条参考(对应 v4 的多片段克隆):

python -m mlx_audio.tts.generate \
  --model mlx-community/Irodori-TTS-v4.1-Small-fp16 \
  --text "今日はいい天気ですね。" \
  --ref_audio speaker.wav

在 irodori_tts.py 的 generate() 中可以看到完整的数据流:文本先经 normalize_text 归一化再 tokenize(max_text_length=256);参考音频经 _load_ref_waveform 转为单声道 48kHz 波形后由 DACVAE 编码为 latent;随后进入 generate_latents 决定输出长度,再由 sample_euler_cfg 采样出 latent,最后 dacvae.decode 还原波形。

文本归一化:日语专用预处理

进入编码器之前,输入文本会先经过 text.py 中 normalize_text 的归一化,该函数逐条对应上游 Irodori-TTS 的 text_normalization.py:

  • 简单替换:删除制表符、全角空格,?/! 归一为半角,〇/○/◯/● 统一为 ○;
  • 正则清理:移除分号、▼、♂/♀、书名号、带圈数字及各类连字符/减号;全角波浪线 〜/~ 统一为长音符号 ー;三个及以上省略号折叠为 ……;
  • strip_outer_brackets 以深度追踪方式剥离包裹整串的括号对(「」『』()【】),例如 「前半」と「後半」 中开头的「在结尾前就闭合,因此不会被误删;
  • 最后做 NFKC 归一化,把全角字母数字、半角假名、㈱/Ⅲ 等折半,并将 .../.. 替换为 …。

仓库在 test_models.py 中保留了与上游 normalize_text 的差分测试(TestIrodoriTextNormalization),确保两步归一化逻辑保持一致。encode_text 则复刻了上游 PretrainedTextTokenizer 的行为:不依赖 tokenizer 的 special token,手动前置 BOS,右侧补 pad 到 max_length,并显式设置 padding_side="right"。

三、VoiceDesign:用文字描述塑造声音

generate_audio 的 instruct 参数在 irodori_tts.py 的 generate() 中是 caption 的别名(caption = caption or kwargs.pop("instruct", None)),对应 DiT 中的 caption 条件分支,最大长度 max_caption_length=512。注意 v2/v3/v4 各版本对 caption 的支持方式不同,以下示例全部来自 README。

v4 / v4.1 VoiceDesign

仅凭描述词(caption only):

generate_audio(
    model="mlx-community/Irodori-TTS-v4.1-Small-fp16",
    text="今日はいい天気ですね。",
    instruct="落ち着いた女性の声で、近い距離感でやわらかく自然に読み上げてください。",
    file_prefix="output",
)

参考语音 + 描述词的风格受控克隆(双条件同时生效):

generate_audio(
    model="mlx-community/Irodori-TTS-v4.1-Small-fp16",
    text="今日はいい天気ですね。",
    ref_audio="speaker.wav",
    instruct="深く傷つき、今にも泣き出しそうな様子。声が震えており、悲痛なトーンで弱々しく話す。",
    file_prefix="output",
)

v3 VoiceDesign

Caption only:

generate_audio(
    model="mlx-community/Irodori-TTS-600M-v3-VoiceDesign-fp16",
    text="今日はいい天気ですね。",
    instruct="落ち着いた女性の声で、近い距離感でやわらかく自然に読み上げてください。",
    file_prefix="output",
)
python -m mlx_audio.tts.generate \
  --model mlx-community/Irodori-TTS-600M-v3-VoiceDesign-fp16 \
  --text "今日はいい天気ですね。" \
  --instruct "落ち着いた女性の声で、近い距離感でやわらかく自然に読み上げてください。"

参考语音 + caption 的风格受控克隆:

generate_audio(
    model="mlx-community/Irodori-TTS-600M-v3-VoiceDesign-fp16",
    text="今日はいい天気ですね。",
    ref_audio="speaker.wav",
    instruct="深く傷つき、今にも泣き出しそうな様子。声が震えており、悲痛なトーンで弱々しく話す。",
    file_prefix="output",
)
python -m mlx_audio.tts.generate \
  --model mlx-community/Irodori-TTS-600M-v3-VoiceDesign-fp16 \
  --text "今日はいい天気ですね。" \
  --ref_audio speaker.wav \
  --instruct "深く傷つき、今にも泣き出しそうな様子。声が震えており、悲痛なトーンで弱々しく話す。"

v2 VoiceDesign

v2 的 VoiceDesign 只接受描述词,不支持参考音频:

generate_audio(
    model="mlx-community/Irodori-TTS-500M-v2-VoiceDesign-fp16",
    text="今日はいい天気ですね。",
    instruct="落ち着いた、近い距離感の女性話者",
    file_prefix="output",
)
python -m mlx_audio.tts.generate \
  --model mlx-community/Irodori-TTS-500M-v2-VoiceDesign-fp16 \
  --text "今日はいい天気ですね。" \
  --instruct "落ち着いた、近い距離感の女性話者"

双条件如何在 DiT 内部生效

从 model.py 的结构看,JointAttention 把 latent 自注意力、文本上下文、说话人(参考音频)上下文、caption 上下文的 K/V 拼接在一条 key 序列上做联合注意力,RoPE 只作用于 head 维度的前半(half-RoPE),输出端还有 sigmoid 门控。v3 VoiceDesign 的双条件模式即同时提供 wk_speaker/wv_speaker 与 wk_caption/wv_caption 两组投影。空 caption 有专门的掩码处理:generate_latents 中若 caption 为空串,caption_mask 会整体置零,避免一个孤立的 BOS token 被当成真实描述喂给 DiT 和时长预测器。

四、v4 / v4.1 特性:共享预训练文本编码器与多片段参考音频

共享预训练文本编码器

v4 将 v2/v3 中两个从零训练的文本/caption 编码器替换为单个预训练 ModernBERT-ja-310m 骨干,分别经独立的 projector 投射到 DiT 条件空间。modernbert.py 实现了 ModernBertEncoder;model.py 中的 PretrainedConditionProjector 支持 linear(纯线性投影)与 residual_mlp(在投影上叠加 SiLU 残差分支)两种类型,由 pretrained_projector_type 配置。骨干权重与 tokenizer 都捆绑在转换后的模型里,因此推理时不会触发额外下载——irodori_tts.py 的 _load_tokenizer 优先读取 checkpoint 下的 tokenizer/ 目录,只有不存在时才回退到从上游仓库拉取。

多片段参考音频(最长 120 秒)

v4 训练时使用了最长 120 秒的参考音频。从源码注释看,训练数据是同一说话人的多段较短录音而非单条不间断长录音,因此传入列表逐段编码再拼接更贴近训练分布。ref_audio 传列表即可:

generate_audio(
    model="mlx-community/Irodori-TTS-v4.1-Small-fp16",
    text="今日はいい天気ですね。",
    ref_audio=["speaker_1.wav", "speaker_2.wav", "speaker_3.wav"],
    file_prefix="output",
)

_encode_ref_audios(irodori_tts.py)的执行逻辑值得注意:每一段先用 DACVAE 独立编码,按输入顺序拼接,累计达到预算即停止;拼接后统一截断到 max_ref_seconds 预算(默认取 checkpoint 的 ref_max_seconds,v4 为 120s),再向 speaker_patch_size(v4 为 4)对齐截断,避免出现半个 patch 被静默丢弃;若对齐后不足一个完整 patch 则直接报错并提示最短参考时长。max_ref_seconds 参数可覆盖 checkpoint 的 120s 预算,截断发生在拼接之后。

五、v3 特性(一):自动时长预测

v3 基座模型内置时长预测器:省略 seconds 时,输出长度由文本与参考音频自动估计。

generate_audio(
    model="mlx-community/Irodori-TTS-500M-v3-fp16",
    text="今日はいい天気ですね。",
    ref_audio="speaker.wav",
    file_prefix="output",
    # seconds 自动预测;用 duration_scale 微调
    duration_scale=1.0,  # >1 更长,<1 更短
)

预测器的输入特征与裁剪逻辑

generate_latents(irodori_tts.py)在 seconds is None 且 use_duration_predictor=True 时调用 model.predict_duration_log_frames,其输入之一是由 duration.py 的 build_duration_features 构造的 14 维手工特征向量,对日语语料高度定制:

维度 特征
0 文本 token 数 / max_text_length(归一化)
1 字符数,log1p 缩放(上限 512)
2 token/字符 比
3 句号 。/. 计数(log1p,上限 8)
4 逗号 、/, 计数(log1p,上限 16)
5 长音 ー 计数(log1p,上限 8)
6 省略号 … 计数(log1p,上限 8)
7 感叹号 !/! 计数
8 问号 ?/? 计数
9 表演标注 emoji 计数(⏩、😭 等 50 余个白名单表情)
10 平假名占比
11 汉字占比
12 ASCII 字母数字占比
13 是否有说话人参考(0/1)

预测结果以 log1p 帧数输出,经 expm1 还原后乘以 duration_scale,最终钳制在 <a href="https://link.gitcode.com/i/db1abb2a049d672a67237fadbcf19e38" target="_blank">min_seconds, max_seconds] = [0.5, 30] 秒之间([SamplerConfig 默认值)。test_models.py 中的 TestIrodoriDurationFeatures 与 TestIrodoriDurationPredictor 分别验证了 14 维特征的构造和预测器的前向形状。若模型既无时长预测器又未指定 seconds,则回退到 sampler.sequence_length(默认 750 帧,约 30 秒);手动指定 seconds 时同样被钳制在 0.5–30s 内。

六、v3 特性(二):Sway Sampling 与采样器参数

Sway Sampling:更少步数的 Euler 采样

为了加速推理,可以把 Sway Sampling 与更少的 Euler 步数组合使用:

generate_audio(
    model="mlx-community/Irodori-TTS-500M-v3-fp16",
    text="今日はいい天気ですね。",
    ref_audio="speaker.wav",
    file_prefix="output",
    num_steps=6,
    t_schedule_mode="sway",
    sway_coeff=-1.0,
)

在 sampling.py 的 sample_euler_cfg 中,时间步网格的构造为:默认(t_schedule_mode="linear")取 linspace(0.999, 0, num_steps+1);sway 模式先取 u = linspace(0, 1, num_steps+1),再套用 u = u + sway_coeff * (cos(0.5πu) + u - 1) 并裁剪到 [0, 1],最后 t = (1-u) * 0.999。sway_coeff=-1.0 使轨迹在中段更密,从而 6 步即可逼近 40 步线性调度的收敛效果。

采样器默认参数全解

SamplerConfig 的完整默认值如下,均可通过 generate_audio 的 kwargs 透传覆盖:

参数 默认值 作用
num_steps 40 Euler 步数,越大质量越好、越慢
cfg_scale_text 3.0 文本条件的 CFG 强度
cfg_scale_speaker 5.0 说话人参考条件的 CFG 强度
cfg_scale_caption 3.0 caption 条件的 CFG 强度
cfg_guidance_mode "independent" CFG 计算方式(见下)
cfg_min_t / cfg_max_t 0.5 / 1.0 仅在 t ∈ [min, max] 区间启用 CFG,t 更低时退化为无条件前向
sequence_length 750 输出 latent 帧数上限(约 30s 音频),实际会被时长逻辑动态覆盖
t_schedule_mode "linear" 时间步调度,"sway" 启用 Sway Sampling
sway_coeff -1.0 sway 强度系数
duration_scale 1.0 自动时长预测结果的缩放系数
min_seconds / max_seconds 0.5 / 30.0 输出时长钳制范围
truncation_factor / rescale_k / rescale_sigma None 初始噪声截断、temporal score rescale(可选后处理)
context_kv_cache True 复用条件 KV cache
speaker_kv_scale / speaker_kv_min_t / speaker_kv_max_layers None / 0.9 / None 参考音频 KV 的增益缩放及其在 t < 0.9 后的回退

cfg_guidance_mode 有三种取值,实现均在 sample_euler_cfg 中:

  • independent(默认):把 [条件, 文本无条件, 说话人无条件, caption 无条件] 打包成一个最多 4 倍 batch 的前向(双条件模型),单条件模型则是 3 倍 batch,各条件各自插值,引导最精细但最耗内存;
  • joint:一次完全无条件前向(文本与上下文同时清零)+ 一次条件前向,两个 scale 需一致,否则直接抛错;
  • alternating:每个 Euler 步只在"文本无条件"与"上下文无条件"之间交替取用,每步仅两次 1×batch 前向,是低内存场景的推荐档位。

七、内存需求与 16GB 机器调优

默认 sequence_length=750 大约需要 24GB 统一内存。16GB 机器可使用缩减配置:

generate_audio(
    model="mlx-community/Irodori-TTS-500M-v3-fp16",
    text="こんにちは。",
    ref_audio="speaker.wav",
    sequence_length=300,
    cfg_guidance_mode="alternating",
    file_prefix="output",
)

cfg_guidance_mode="alternating" 下的近似内存占用:

sequence_length 内存 音频时长
100 ~2GB ~4s
300 ~2GB ~12s
400 ~3GB ~16s

若使用默认的 cfg_guidance_mode="independent",内存占用约为上表的 3 倍——这与源码中 independent 模式 3×batch 前向、alternating 模式每步两次 1×batch 前向的结构完全对应(见第六节的分析)。

另一个对 16GB 机器友好的细节藏在 irodori_tts.py 的解码阶段:DACVAE 解码采用 chunk_size=50 的分块反卷积,源码注释说明单步 stride-8 反卷积在 T=750 时会瞬间产生约 17GB 的中间张量,按 50 帧分块后峰值压到约 1.2GB。生成完成后还有一个后处理:_find_silence_point 以滑动窗口(窗口 20 帧,std < 0.05 且 |mean| < 0.1)检测 latent 尾部的静音点,把输出裁剪到"预测时长"与"静音点"的较短一侧,同时防御了静音检测误判首窗口导致输出被清空为零的情况。

八、工程细节与限制

结合源码,还有几点使用时的注意点:

  1. 流式生成暂不可用:generate() 对 stream=True 直接抛出 NotImplementedError,Irodori-TTS 目前只支持整段生成后落盘。
  2. 无参考音频的兜底:启用说话人条件的模型若未提供 ref_latent,generate_latents 会构造一个 speaker_patch_size × latent_dim 的全零 latent 作为占位,保证 patch 后序列非空(v4 的 speaker_patch_size=4)。
  3. 权重键重映射:上游 PyTorch 的 Sequential 用整数下标,MLX 的 nn.Sequential 用 "layers.N",Model.sanitize(irodori_tts.py)负责把 cond_module.0.weight 之类的键改写为 cond_module.layers.0.weight,并统一挂到 model. 命名空间下。
  4. 测试覆盖:test_models.py 中 TestIrodoriGenerateSmoke、TestIrodoriV3GenerateSmoke 用小尺寸配置对 v2 克隆与 v3 全链路做冒烟测试,TestIrodoriVoiceDesignGenerate 覆盖 caption 分支,TestIrodoriEncodeText 覆盖 tokenize 的 BOS/右填充行为,可作为自改配置后的回归依据。
  5. 许可证:模型为 MIT License(详见上游 Irodori-TTS 仓库的说明),v4/v4.1 因捆绑 Semantic-DACVAE-Japanese-32dim 与现代 BERT 编码器,同精度下比 v3 大约 1GB。

综合来看,mlx-audio 的 Irodori-TTS 提供了从 v1 单音色到 v4.1"克隆 + 设计 + 自动时长"一体化的清晰版本梯度:追求统一体验选 v4.1-Small,需要更细粒度的双条件控制可选 v3-600M VoiceDesign,而低内存环境下通过 sequence_length 与 cfg_guidance_mode="alternating" 的组合仍可在 2–3GB 内存内稳定产出约 12 秒的 48kHz 日语语音。

登录后查看全文
mlx-audio