语音克隆、.s2voice 与对话模板
本章把”参考音频如何变成模型能吃的输入”这条链路讲完整,涉及三个文件:s2_codec.cpp 的 encode(第 08 章已分析)、s2_voice.cpp 的声音档案、s2_prompt.cpp 的 prompt 构造。
1. 语音克隆的完整链路
参考音频文件(WAV/MP3)
│ audio_read:解码 + 下混单声道 + 必要时重采样到 44.1kHz
▼
AudioData(float, 44.1kHz)
│ codec.encode:两图卷积编码 + 余弦VQ + 9级残差VQ
▼
prompt_codes(10 × T_prompt 整数码矩阵) + T_prompt
│
├─(可选)保存为 .s2voice 档案,供以后免编码复用
│
▼
build_prompt:文本 + transcript + prompt_codes → 11 行 PromptTensor
│
▼
generate → Slow-AR prefill
入口在 Pipeline 的 resolve_reference_prompt_locked():要么现场编码参考音频,要么从 .s2voice 档案载入已编码结果。”locked” 表示该方法在持有流水线互斥锁的上下文中调用。
2. build_prompt():两种模板
build_prompt() 构造第 02 章介绍的 (num_codebooks+1) × T PromptTensor。它根据是否带参考音频拼两套不同的对话模板。
2.1 带参考音频(克隆模式)
判定条件很严格(:23):必须同时有 prompt_codes、T_prompt > 0、且 prompt_text(参考音频的转写文本)非空——三者缺一就退化为无参考模式。
模板分三段拼装,sys_pre 在参考码之前、sys_post 在其后:
sys_pre:
<|im_start|>system\n
convert the provided text to speech reference to the following:\n\nText:\n
<|speaker:0|> (若转写文本中没有 <|speaker: 标签)
<prompt_text(参考音频的转写)>
\n\nSpeech:\n
[参考码段:T_prompt 帧]
sys_post:
<|im_end|>\n
<|im_start|>user\n<目标文本><|im_end|>\n
<|im_start|>assistant\n<|voice|>
字符串内容见 :34-52。设计意图:模型在训练时见过”Text + Speech”成对的转换任务,推理时先给一段已知的文本↔语音对作为示范,再让它对新文本产出同样风格的语音——这本质上是一种上下文学习(in-context learning)式的克隆,不需要单独的说话人嵌入。
<|speaker:0|> 的处理有个细节(:24、:37-39):如果转写文本本身已含说话人标签,就不再重复插入,避免多说话人参考时标签错乱。
2.2 无参考(默认音色模式)
不带参考时模板简单得多(:58-71):
<|im_start|>system\nconvert the provided text to speech<|im_end|>\n
<|im_start|>user\n<文本><|im_end|>\n
<|im_start|>assistant\n<|voice|>
此时没有参考码段,模型使用其从多说话人数据中学到的默认发声方式。
2.3 数据布局
参考码段写入矩阵时区分语义行与码本行(:86-96):
第 0 行(语义流):prompt_codes[t] + semantic_begin ← 码本索引抬升到词表中的语义区间
第 cb+1 行(cb=0..9):prompt_codes[cb*T_prompt+t] ← 原始码,不偏移
而 sys_pre、sys_post 的文本 token 只写在第 0 行(:81-83、:98-100),其他行留 0 并由语义掩码屏蔽。这保证文本段和音频段在统一的 11 行布局下各自就位。总长度在 :74 一次算出:sys_pre + (T_prompt) + sys_post。
3. .s2voice:二进制声音档案
每次合成前都现场编码参考音频会浪费时间(编码图构建 + 计算不便宜)。s2.cpp 定义了一个紧凑的二进制档案格式,由 s2_voice.cpp 实现,把编码结果连同元数据固化下来。
3.1 文件格式
文件头与字段(save()):
偏移 字段 类型
0 MAGIC 8 字节:"S2VOICE\0"
8 version uint32:1
12 num_codebooks int32
16 T_prompt int32
20 sample_rate int32
24 codebook_size int32
28 transcript_len uint64(含结尾 NUL)
36 codes_size uint64(字节数)
44 transcript UTF-8 文本 + '\0'
.. codes int32 数组(10 × T_prompt,与 codec 输出同布局)
3.2 加载时的完整性校验
load() 不是简单反序列化,而是逐层设防:
- 魔数不匹配 → “invalid voice profile magic”(:50-52);
- 版本号 != 1 → “unsupported version”(:56-58);
- transcript 长度为 0、或读出的文本不是 NUL 结尾 → 报错(:72-80),防止把脏数据当字符串;
- 读完后检查流状态,截断文件 → “truncated”(:87-89)。
3.3 兼容性检查
is_compatible() 在载入档案后比对三项:码本数、码本大小、采样率全部一致才允许使用。这防止把 A 模型(比如不同码本配置)的档案错配到 B 模型上——码布局不一致会产生无声或噪声。检查在 Pipeline 解析参考时执行,不通过即报错返回。
4. VoiceProfileManager:按 id 管理档案
VoiceProfileManager 给每个声音一个字符串 id,用 <id>.s2voice 的文件名管理:
- 存储目录可通过
--voice-dir设置,默认./voices;取路径时若目录不存在会自动创建(:105-111); - 支持 save / load / remove / list;list 靠扫描
.s2voice扩展名(:129-141); - CLI 的
--voice <id>载入、--save-voice在本次克隆后保存、--list-voices列举(main.cpp:163-166)。
典型用法:
# 第一次:用参考音频建立档案
./s2 -m model.gguf --prompt-audio ref.wav --prompt-text "参考音频转写" \
--voice alice --save-voice --text "占位" -o /tmp/x.wav
# 以后:直接用 id,无需再提供参考音频
./s2 -m model.gguf --voice alice --text "你好" -o out.wav
5. 参考音频本身的处理
进入 codec 之前,s2_audio.cpp 负责音频前处理(与输出共用一个文件):
- audio_read():先按 WAV 解,失败再按 MP3 解;多声道按下述规则下混为单声道;
- 采样率不符时用 audio_resample() 线性重采样到 44.1 kHz;
- 参考音频的长度和信噪比直接决定克隆质量——README 明确指出这是已知敏感点(README.md:565)。
6. 小结
声音克隆在 s2.cpp 里不是额外模型,而是“编一段已知码放进 prompt”的上下文技巧:codec 负责音频→码、.s2voice 负责码的持久化与兼容校验、build_prompt 负责把文本与码按 11 行布局拼成对话模板。理解了输入如何构造,第 10 章就可以看整个流水线如何把这些组件组装起来,并实现低延迟的流式合成。
关键文件
| 位置 | 职责 |
|---|---|
| s2_prompt.cpp:5 | build_prompt 模板构造 |
| s2_prompt.cpp:23 | 克隆模式判定 |
| s2_voice.cpp:15 | .s2voice 保存 |
| s2_voice.cpp:42 | .s2voice 加载与校验 |
| s2_voice.cpp:94 | 码本兼容性检查 |
| s2_voice.cpp:101 | VoiceProfileManager |
| include/s2_prompt.h | PromptTensor 定义 |