【研究】SynthV jsm.data的解码与转换

 

原内容写于24/4/10,26/8/17取得进展后重写。

起源

打开Synthesizer V的安装目录,除了软件本体、配置文件和字体文件夹之外,就是clf-data文件夹了。打开这个文件夹,在里头可以看到许多文本文件,包括罗列音素的[语言]-[音素格式]-phones.txt、提供转换的[语言]-[转换内容]-dict.txt,以及包括cmudict-07b.txt等在内的各类字典等。

除了文本文件之外,无法直接阅读的[语言]-[音素格式]-jsm.data就是最为神秘的文件了。如果直接使用记事本打开,会发现文件是一堆乱码,无法直接阅读。但中间又夹杂着一些看上去像是音素的字母,前后两者非常相似,似乎是某种和音素有关的键值对。再加上当时SV0相比于VOCALOID在音素上的组合可谓是十分自由,甚至能够唱出“采样中并没有录出来的声音”,如中文中的fibyu等,让我开始怀疑怀疑这个文件是否为SV能够如此发音的关键。只可惜当时个人水平还不能直接通过编程或十六进制查看器来解码此类文件。

在SV1后期粤语更新后,Dreamtonics将粤语的jsm文件以文本文档的格式直接放入了目录中。打开查看,会发现它将原本的乱码以明文的形式展示了出来,中间不可见的部分实际为浮点数:

true	true	2	2
342	1468
k_i	2
kh_I	2.491175e-04
kh_i	8.996223e-08
a_k	1
6_:k_}	4.812173e-03
m_o	1
m_O	3.903579e-04
……

当时我受制作VOCALOID或UTAU相关内容的影响,认为这里可能是指的音素组合,猜测这里的数字可能是什么概率或权重,比如m_om_O的组合相似的概率等,是否意味着在没有某个组合的情况下用其他多个相似组合的加权值来代替合成缺失的衔接部分。当然,读完了这篇文章就知道这个猜想到底对不对了……

SV2中粤语的jsm已经被编码为data文件,因此可以假设被编码后的data文件与原有的明文是等价的,通过对比原有的明文与被编码成二进制文件的编码来解析jsm中的内容,由此开启破解的工作。当然由于科技的发展,这个工作肯定不是我手动完成的,而是交给WB(某国内AI Agent,此处无广告)比对并获得了一些结果。

名词解释

在破解过程中有一些专有的名词,先请WB来解释一下。

  • TTS(Text-to-Speech,语音合成)——把文字变成人声的技术。要”读对音”,必须先知道每个字怎么发音,这就引出 G2P。
  • G2P(Grapheme-to-Phoneme,字素→音素)——语音合成的核心前置模块:把书写符号(字母/汉字)转成发音符号(音素)。你这套文件就是多语言的 G2P 模型。
  • 字素(Grapheme)——书写系统的最小单位,比如英文字母 a、汉字 汉。它是”输入的文字”。
  • 音素(Phoneme)——语言中能区分意义的最小语音单位,比如汉语 b/p、英语 s/ʃ。它是”输出的发音”。
  • X-SAMPA——用纯 ASCII 字符表示国际音标(IPA)的约定。因为音标里有很多特殊符号(如 ʃ θ ŋ),机器存储和交换时用 ASCII 更省事。所以你看到的 :N(鼻化)、tsh(送气塞擦)、$/_(上下文分隔)都是 X-SAMPA 写法。各语言的 *-phones.txt 就是它认识的 X-SAMPA 音素清单。
  • n-gram——一种统计语言模型:用”前 n−1 个单元”来预测”下一个单元”。这里 n=2(阶数=2),即只看当前字素 + 它左边紧邻的 1 个字素来决定发音。
  • 联合序列模型 JSM(Joint Sequence Model)——同时给”字素序列”和”音素序列”建模的联合概率模型,用 n-gram 实现 G2P。这就是 jsm 最可能的含义:模型的”键”是字素上下文窗口,”值”是该窗口下各音素的条件概率。
  • 加权有限状态转换器 WFST(Weighted Finite-State Transducer)——一种带权重的图结构(状态 + 弧),弧上有代价/概率。G2P 本质就是”字素序列 → 音素序列”的转换器。你的二进制格式正是这种”键 → 多条带权出弧”的结构。
  • 上下文窗口(context window)——决定当前字素发音时参考的左边环境。在 key 里用 $(文本里用 _)分隔,如 h$i = 左语境 h + 当前字 i。
  • 条件概率分布——给定上下文,下一个音素取各个值的概率。CSV 里的 weight 就是它。
  • 键 key / 记录 record——模型里的一项:一个上下文窗口 + 它的候选输出列表。
  • 弧 arc——一条 (target, weight) 出边,代表”从这个上下文可以走向某个音素,且带某个权重”。
  • 权重 weight——该 target 被选中时的相对概率/代价,32 位浮点,落在 (0,1]。
  • 序列化 / 反序列化(serialization / deserialization)——把内存里的模型对象写成字节流(存盘)叫序列化;读回叫反序列化。.data 是二进制序列化,.txt 是文本序列化,两者内容等价。
  • 魔数(magic number)——文件开头固定的几个字节,用来标识格式。这里 Style A 以 ff ff ff ff 开头。
  • 头部 header——文件最前面的元信息区(版本、阶数、计数等)。
  • null 结尾字符串(cstring)——C 语言风格字符串,用字节 00(\0)标记结尾。二进制里所有符号都这样存。
  • uint16 / float32 / 小端(little-endian)——uint16=无符号 16 位整数(0~65535),float32=32 位浮点;小端指”低位字节在前”(如 01 00 表示数字 1)。
  • CSV(source, target, weight)——逗号/制表符分隔的扁平表,每行一条弧。

破解

首先要来分析一下字母简写的意思。WB预测,jsm的意思应该最可能是Joint Sequence Model(联合序列模型),而这些文件应该是G2P模型的权重。此外,它们所在的文件夹clf-data中clf的意思应该最有可能的是Cross-Lingual Frontend(跨语言前端)

jsm.data的整体结构很简单,就是一个文件头+若干条记录。通过WB对明文txt和二进制data的分析,文件头部有两种风格:

  • Style A(粤语、韩语):23 字节,以 ff ff ff ff 魔数开头,后面存版本标记、固定为 2 的 n-gram 阶、键 / 弧统计数量
  • Style B(英语、日语、普通话):18 字节,不带魔数,其余字段含义和 A 一致

手头没有SV0时代的资料,中英日三语应该是继承的SV0时期的格式,SV1之后新加入的语言则是用新的头文件格式。当然这个基本和实际的内容无关,因此无需细究。

对于每条记录而言:

[key   : cstring, \0 结尾]    例: 6a 24 6f 00 = "j$o"
[count : uint16 小端]         例: 01 00 = 1
[ 循环 count 次:
    [target : cstring, \0 结尾]  例: 6a 24 4f 00 = "j$O"
    [weight : float32 小端]      例: 73 48 71 3a = 9.204e-4
]

即一个 key + 它的出弧数 + 若干 (target, weight) 对。为了解码后阅读起来方便,导出出的CSV把上面”一个 key 多条候选弧”的嵌套结构按候选弧展开。其含义为:

  • source(源)——就是key。模型的输入键,即一个上下文窗口,形如 h_i(二进制里是 h$i)。意义:”当前要决定发音的字素 i,以及它左边紧邻的字素 h”。它代表模型这一步”看到了什么”。
  • target(目标)——该上下文下模型给出的一个候选输出音素(X-SAMPA),如 h_I、h_i。意义:”在这个上下文里,字素可能读作这个音”。
  • weight(权重)——选中该 target 的相对概率,float32,(0,1],数字越大越优先。

得到的CSV即为:

source	target	weight
j_o	j_O	9.204216e-04
k_w	kwh	2.146560e-02
h_i	h_I	2.681873e-04
h_i	h_i	4.128605e-04
……

同一个 source 在 CSV 里会出现count行,每一行是一个候选音素及其概率。解码时,沿文本逐字滑动窗口,每步查表,按 weight 选 target(可以直接挑权重最大的贪心输出,也可以 Viterbi 求全局最优路径),把所有 target 拼起来,就是完整的音素序列。

结论

最终,这个东西类似于Vocaloid里的g2pa*_XXX.dll,是SV所采用的G2P模型。与Vocaloid那种类似查表的结构不同,它采用了模型来预测字素可能对应的音素。对于难以通过列表来进行G2P转换的语言,例如英语等,这种方法似乎更好用。对于用列表就能进行G2P转换的语言,比如中文和日语,用模型来转换有点大材小用了,但这确实也能让软件单个音符内输入多个音节等情况下更灵活。很可惜,这个文件并不能让SV能够唱出来“采样中并没有录出来的声音”,这个神奇的功能应该是通过其他方法实现的,比如合成引擎泛化(WB说的)。

资源

已经解包的内容在这里:

在线转换