Kokoro TTS v1.1 voices 文件格式逆向分析
前言
Kokoro 是一个 8200 万参数的开放权重 TTS 模型,在 GitHub 上有多个实现版本。本文记录了作者在调试 Kokoro TTS 过程中,意外发现 v1.0 和 v1.1 voices 文件格式完全不同,且官方文档语焉不详,最终通过逆向工程完整还原 v1.1 voices 文件结构的过程。
前置条件:Rust 编程语言、Tokio 异步运行时、可选 Rodio 音频播放库。
问题背景
在调试项目时,需要为 Kokoro TTS 找一个声音自然、没有电音(electronic artifact)的女声。测试中发现:
- v1.0 模型的所有 54 个女声全部存在电音问题
- v1.1 模型有 103 个 voices,理论上应该有更好的选择
- 但 v1.1 的 voices 文件(52MB)既不是标准的 NPZ(NumPy ZIP)格式,也不是任何常见二进制格式
- 官方文档只说了"下载模型资源"一句话,格式一概不提
v1.0 voices 文件格式(基线)
v1.0 的 voices 文件是标准 NPZ 格式,可以用 numpy.load() 直接读取:
python
import numpy as np
voices = np.load("voices-v1.0.bin")
# voices 是 dict: {'af_sarah': array(shape=(510,1,256), dtype=float32), ...}
print(voices['af_sarah'].shape) # (510, 1, 256)
每个 voice 是一个 (510, 1, 256) 的 float32 数组,代表 510 个 phoneme style embeddings,256 维。
这为后来分析 v1.1 提供了重要的参考基线。
v1.1 voices 文件分析
第一步:文件头分析
bash
$ xxd voices-v1.1-zh.bin | head -1
00000000: 6706 7a6d 5f30 3334 fbfe 0101 fb00 0122 g.zm_034....
文件开头是 ASCII 字符串 g\x06zm_034,意味着第一个 entry 的 voice name 叫 zm_034。但这不是 ZIP 文件(没有 PK 魔数),也不是 NumPy 的 .npy 文件。
第二步:定位文件结构
通过反复读取文件不同偏移量的数据,可以推断出每个 entry 的结构:
Entry N:
[1 byte: name_length] ← 名称长度
[name_length bytes: name] ← voice 名称,如 "zf_001"
[524280 bytes: voice_data] ← voice embedding 数据
第一个 entry:
name_length = 0x06name = "zm_034"(6 字节,从偏移 2 开始)voice_data从偏移 8 开始,占 524280 字节- Entry 1 从偏移 524291 开始(524290 = 0x80012)
第三步:解码 voice_data
最初以为 voice_data 是压缩的 float32 数组,但直接用 np.frombuffer() 解读时出现了大量 NaN 值。
尝试了多种解释方式:float32、float16、大端序、小端序......全部失败。
最终通过阅读 crates.io 上的 kokoro-tts 0.3.3 源码才找到答案:
rust
// 来源: kokoro-tts/src/lib.rs
pub struct KokoroTts {
model: Arc<Mutex<Session>>,
voices: Arc<HashMap<String, Vec<Vec<Vec<f32>>>>>,
}
voices 字段是一个 HashMap<String, Vec<Vec<Vec<f32>>>>。这不是一个简单的 3D 数组,而是嵌套的向量嵌套!
用 Rust bincode 库直接解码:
rust
use bincode::Decode;
type VoicesV11 = HashMap<String, Vec<Vec<Vec<f32>>>>;
let (voices_v11, _) = bincode::decode_from_slice(&data, bincode::config::standard()).unwrap();
解码成功! v1.1 文件是 bincode 编码 的 HashMap<String, Vec<Vec<Vec<f32>>>>。
每个 voice 的结构:
Vec<Vec<Vec<f32>>>= 510 个 style pack- 每个 pack 包含若干个 256 维向量
- 这 510 个 pack 对应 510 个 phoneme styles
v1.0 与 v1.1 的本质区别
| 维度 | v1.0 | v1.1 |
|---|---|---|
| 文件格式 | NPZ(标准 ZIP) | bincode 私有二进制 |
| voice 数量 | 54 个 | 103 个 |
| embedding 结构 | Vec<[[f32; 256]; 1]>(全局,1个向量) |
Vec<Vec<Vec<f32>>>(510个pack,每pack若乾向量) |
| 模型架构 | 全局 voice embedding | per-phoneme style |
这是两种完全不同的架构!v1.0 是全局 voice 风格,v1.1 是给每个 phoneme 分配独立的 style 向量。这就是为什么 v1.0 的女声普遍有电音------全局 embedding 在某些语言上不够细粒度。
如何用 Python 读取 v1.1 voices
python
import subprocess, struct, zipfile, io, numpy as np
# 1. 用 Rust bincode 解码(最可靠)
result = subprocess.run(
['cargo', 'run', '--release', '--example', 'synth_directly_v11'],
cwd='kokoro-tts-project'
)
# 2. 如果你只想提取 voice 数据,下面是解析思路:
def parse_v1_1_voices(filepath):
"""手动解析 v1.1 voices 文件结构"""
with open(filepath, 'rb') as f:
data = f.read()
voices = {}
pos = 0
while pos < len(data):
name_len = data[pos] # 1 byte
if not (3 <= name_len <= 15):
break
name = data[pos+1:pos+1+name_len].decode('ascii')
voice_start = pos + 1 + name_len
voice_data = data[voice_start:voice_start + 524280]
# ... 这里需要 bincode 解码,因为数据是 bincode 编码的
# 推荐直接用 Rust 解码后输出
return voices
踩过的坑
坑 1:文件格式误判
最初以为文件可能是某种自定义压缩格式,花了大量时间测试 gzip、zstd、lz4......全是白费。正确答案其实在源码里。
教训:遇到未知二进制格式,先看官方源码,不要自己瞎猜。
坑 2:Python bincode 库安装失败
尝试用 Python 的 bincode 包直接解码:
bash
pip install bincode
python -c "import bincode" # 报错!
pip 安装了但 Python 找不到。检查发现:pip 和系统 Python 版本冲突(/usr/bin/python3 vs /usr/local/bin/python3.12)。最终通过 Rust 来解码。
教训 :多版本 Python 共存时,务必确认 pip 和执行环境是同一个。
坑 3:NPZ 格式生成
成功解码 v1.1 voices 后,想把它转成 v1.0 格式(NPZ)给现有工具用。生成了 NPZ 文件,但被 ndarray-npy 报错 MissingNewline。原因是 NPY 文件头的 header 长度字段计算错误。
rust
// 错误的做法:2 字节 header 长度
npy_content.extend_from_slice(b"\x93NUMPY\x01\x00");
npy_content.extend_from_slice(&(header_len as u16).to_le_bytes());
// 正确的做法:NPY header 是 4 字节长度(含自己)
npy_content.extend_from_slice(b"\x93NUMPY\x01\x00");
npy_content.extend_from_slice(&(total_header_len as u32).to_le_bytes());
教训:文件格式细节必须严格遵守规范,差一个字节都不行。
结论
Kokoro v1.1 的 voices 文件采用 bincode 编码的私有格式,与 v1.0 的 NPZ 标准格式完全不兼容。这是 Kokoro 从全局 voice embedding 升级到 per-phoneme style 架构的副产品。由于官方文档几乎没有格式说明,开发者只能靠源码或逆向工程来理解。
如果你的项目需要使用 v1.1 voices,建议直接使用官方的 Rust 实现(artisdom/kokoro-rs),而不是尝试自己解析文件格式。
相关资源
- Kokoro TTS 官方:https://huggingface.co/hexgrad/Kokoro-82M
- Rust 实现(crates.io):https://crates.io/crates/kokoro-tts
- Python ONNX 实现:https://github.com/thewh1teagle/kokoro-onnx
- 模型下载(需自备代理):https://github.com/mzdk100/kokoro/releases/tag/V1.1