Skip to content

唇语的算法

背景:联邦要让模型「说话」——把音频振幅实时变成口型 morph 权重。LipSync 算法核心(lipsync.ts)。 过程findLipMorph / findAllLipMorphs 的口型 morph 候选命中、amplitudeToWeight 振幅→权重映射、LipSyncState 默认参数。与 09 卷《印章与账本》的桥接层分工。


一、沉默的城邦

外交官接到的任务,听起来比「写 VMD」温柔:让模型说话。

音频已经在放了。beatDetector 每帧从频谱里抠出人声频段的能量,交给程序化动作系统当节拍。但节拍让身体律动,嘴唇却不动——模型在跟着音乐「哼」,但嘴是闭着的。

「让它出声。」桌面壳说。

外交官查了 lipsync.ts。算法很简单:每帧从 BeatDetector.getLevel 取人声能量 → amplitudeToWeight 映射成 0..1 的权重 → 直写焦点模型的「あ」morph。

听起来直白。但外交官在 findLipMorph 这里卡住了。

二、方言的嘴

findLipMorph 要做的事是:在模型提供的 morph 名列表里,找到那张「嘴」。

但它不是猜。它列了一张候选表,按优先级降序:

typescript
const LIP_MORPH_CANDIDATES = ['あ', 'ア', 'A', 'a', '口', 'mouth', 'open'];

然后逐个查:

typescript
const set = new Set(morphNames);
for (const name of LIP_MORPH_CANDIDATES) {
    if (set.has(name)) return name;
}
return null;

外交官看着这张表,忽然觉得好笑又心酸。联邦收编的每一个模型——每一个城邦——对「嘴」的叫法都不一样:

  • 大多数说日语平假名「あ」。
  • 少数说片假名「ア」。
  • 更少的,说拉丁字母「A」或「a」,或者直接叫「mouth」「open」「口」。

没有统一的术语。没有中央命名局。每个模型的作者,在二十年前某个深夜,凭自己的习惯给那张嘴起了名。

findLipMorph 不翻译、不模糊匹配、不做语义推断。它只是按优先级,看模型的 morph 名里有没有恰好落在候选表上的那个。有,返回;没有,返回 null

返回 null 的后果很安静:角色不说话。不是报错,不是崩溃——只是那张嘴永远闭着。联邦最尴尬的失败不是「听不懂」,是「找不到那张嘴」。

三、振幅的分寸

找到了嘴,还要知道「张多大」。amplitudeToWeight 管这件事:

typescript
if (amplitude < sensitivity) return 0;
const range = 1 - sensitivity;
const t = Math.max(0, Math.min(1, (amplitude - sensitivity) / range));
return t * intensity;

两个旋钮:sensitivity(默认 0.2)是阈值——低于它,视为静音,嘴闭;intensity(默认 0.8)是最大张幅——再响也只张到八成,留两分给自然。

外交官算了一遍:一段人声,能量在 0.2 以下时,权重恒为 0——角色闭嘴;能量爬过 0.2,权重线性张开,到 1.0 时张到 0.8 满幅。不是「响了就全开」,是「按比例张」。

分寸感。和《节拍与曲线》里 Riku 调插值曲线是一个道理:匀速不够时,曲线比关键帧重要;满幅不够时,留白比全开重要。

LipSyncState 把这两个旋钮和 enabledmultiMorphEnabled 打包成一个状态对象,默认 enabled: false——联邦不替用户决定模型该不该说话,它只准备好说话的能力。

四、多嘴的扩展

multiMorphEnabled 是另一扇门。

单口型模式只驱动「あ」——所有声音都张同一张嘴。但人类说话,嘴型随元音变:あ、い、う、え。多口型模式用 findAllLipMorphs 按音素分类查找:

typescript
const MOUTH_MORPHS = {
    open:   ['あ', 'ア', 'A', 'a', '口', 'mouth', 'open'],
    close:  ['い', 'イ', 'I', 'i', 'close'],
    pucker: ['う', 'ウ', 'U', 'u', 'pucker'],
    smile:  ['え', 'エ', 'E', 'e', 'smile', 'にこり', '笑い'],
};

返回 { open, close, pucker, smile }——四张嘴,对应四种元音。如果某个模型只有「あ」没有「い」,那 close 就是 null,对应元音时安静降级。

外交官忽然明白:联邦的「说话」,本质是一场方言调和。它不创造语言,它只是把每个城邦各自的「嘴」找出来,按元音归位,再用同一段音频能量去驱动。能找到几张嘴,模型就能发几个音;一张都找不到,它就继续沉默——不是不会说,是没人告诉它嘴叫什么。

这和「读 VMD」何其相似。01 章里外交官读 Shift-JIS 骨骼名,也是在一片编码三态里,找出那个能被正确解码的名字。联邦的每一步「理解」,都是先在一堆不统一的命名里,找到那个恰好能对上的。


聚合悖论的又一则:联邦越强,收编的城邦越多;城邦越多,连「嘴」的叫法都越不统一。理解一个模型的前提,是先学会它的方言。

教训:联邦不创造语言,只在方言里找出能对上的那一个。找不到,就沉默。