🧠 第八章 语音模型训练:智能声音克隆与处理
📄 一句话概括

能量包络找起点:10 个字全部检到,误差稳定在 10 毫秒(一帧的固有延迟)。 但只要两个字连读,起点就只剩 9 个——按顺序硬对齐,后面 5 个字全部错位 390 毫秒。 这就是「数起点」和「强制对齐」的差距。

📝 本节和前面几节的关系

上一节剪好了素材。这一节配歌词。 「识别歌词」听起来是一件事,实际是两件:把话听成字,和把字对上时间。

一、两条路线:识别,还是对齐

路线一:语音识别(ASR)。 从音频里听出文字。 适合「我不知道唱的是什么」。 难点是歌声的旋律会破坏语音特征, 用说话数据训的 ASR 直接拿去听歌,准确率会掉很多。 路线二:强制对齐(forced alignment)。 文字已经有了(歌词本),只要把每个字对上时间。 适合「我知道歌词,只要时间轴」。 做 MV 字幕, 绝大多数情况是路线二—— 因为歌词是现成的,你不需要机器去听写。需要的是「这一句在 1 分 23 秒出现」。 📋 实操清单:平台上的「智能识别歌词」通常给你两个产物——文本(听写结果, 要校对)和 LRC 时间轴(每句的起始时间)。校对文本一定要做,尤其是人名、英文、方言词。

二、能量包络找起点:10 个字全中,但晚一帧

最朴素的对齐思路: 把音频切成 10 毫秒一帧,算每帧的能量(RMS),能量从低跃过阈值的那一帧就算一个「字」的起点。 Demo 按这个做,10 个字全部检到,误差整齐地都是 +10 毫秒。 这个 10 毫秒不是随机误差,是系统延迟:判据是「跨过阈值的第一个帧」, 而起音有一个 50 毫秒的爬升过程,所以要爬两三帧才过阈值。 系统延迟可以整体补偿掉——减去 10 毫秒就对齐了。这跟上一章「能量检出的固有延迟」是同一类问题: 先弄清偏差的方向和来源,再谈修不修。 这条路线能用的前提是:每个字之间真的有气口。说话可以,唱歌不行。

三、连读:只漏一个,崩一片

把第 5、6 个字之间的气口去掉(模拟连读),重跑:

字序 真实起点 检测起点 误差
第 1 到 5 字 0.00 到 1.52 0.01 到 1.53 +10 ms
第 6 字(了) 1.80 2.19 +390 ms
第 7 到 9 字 2.18 到 2.94 2.57 到 3.33 +390 ms
第 10 字(了) 3.32 漏检 —
只少了一个起点,但错了五个字。原因就一句:
对齐是「第 i 个字配第 i 个起点」,丢一个,后面全部往前挪一位。
这不是理论风险。
人唱歌本来就会连字、拖长音、抢拍、加气声。所以「数起点」这条路线在歌声上基本不可能稳定。
正确的做法是动态规划式的强制对齐:把「歌词的字序列」和「音频的音素序列」放在一起,
穷举所有可能的切分,取总分最高的那一条。这样某个字没有明显起点时,它能靠上下文推断出来,
而不是让后面全错位。
但强制对齐要声学模型(把音频映射到音素的神经网络)。
沙箱里没有这种模型,这一层这里如实说测不到——能真跑的只有前面的包络检测和它暴露出来的错位问题。

四、LRC 的格式,和三条实践建议

对齐的结果最终要写成 LRC:每行一个 [分:秒.百分秒] 加一句歌词。格式极简,所以它是事实标准。

[00:00.01]月
[00:00.39]亮
[00:00.77]爬
[00:01.15]上
[00:01.53]来

注意 LRC 是「整句对时间」,不是「逐字对时间」。逐字要高亮得用 KRC、QRC 这类逐字格式, 或者自己在 LRC 里塞多个时间标签(兼容性参差不齐)。 三条实践建议。① 时间戳取句首,不取句尾——播放器是按「当前时间 ≥ 时间戳」来切句的。 ② 一句太长就拆。超过 15 个字的句子在手机屏上要换行,拆成两句时间戳,观感立刻好一截。 ③ 生成完一定要人听一遍。自动对齐的错误往往只错一两百毫秒, 而一两百毫秒在字幕上已经是「抢拍」或「慢半拍」了。

五、动手 Demo:歌词对齐:10 个字检到 10 个,连读两个字就崩一片

造一段 10 个音节的「唱句」:每字之间留 0.10 秒气口,用 10 毫秒一帧算能量包络, 能量跃过阈值的第一帧算一个字起点; 再把第 5、6 字之间的气口去掉(模拟连读),重跑一遍,按「第 i 个字配第 i 个起点」硬对齐, 看错位怎么传播。

def _env(x, sr, hop=0.01):
    h = int(sr * hop)
    n = x.size // h
    e = [np.sqrt(np.mean(x[i * h:(i + 1) * h].astype(np.float64) ** 2))
         for i in range(n)]
    return np.array(e), hop

def _onsets(e, hop, thr=0.12):
    top = float(e.max())
    out, armed = [], True
    for i, v in enumerate(e):
        if armed and v > thr * top:
            out.append(i * hop); armed = False
        elif not armed and v < thr * top * 0.6:
            armed = True                # 等能量落回去再找下一个
    return out

实跑输出:

--------------------------------------------------------------------
歌词对齐:10 个字检到 10 个,连读两个字就崩一片
--------------------------------------------------------------------
  每字后留 0.10 秒气口     检到 10 个起点(应 10 个)
  字   真实起点(s)   检测起点(s)   误差(ms)
  ----------------------------------------
  月          0.00          0.01        +10
  亮          0.38          0.39        +10
  爬          0.76          0.77        +10
  上          1.14          1.15        +10
  来          1.52          1.53        +10
  了          1.90          1.91        +10
  风          2.28          2.29        +10
  也          2.66          2.67        +10
  停          3.04          3.05        +10
  了          3.42          3.43        +10
  **10 个字全检到了**,误差 10 毫秒——这是「帧长」带来的固定延迟,不是随机误差。
  起点判定是「能量跨过阈值的第一个 10 毫秒帧」,**所以它天然会比真实起音晚一帧左右。**

  LRC 直接就能生成:
  [00:00.01]月
  [00:00.39]亮
  [00:00.77]爬
  [00:01.15]上
  [00:01.53]来
  [00:01.91]了
  [00:02.29]风
  [00:02.67]也
  [00:03.05]停
  [00:03.43]了

  第 5、6 字连读           检到 9 个起点(应 10 个)
  字   真实起点(s)   检测起点(s)   误差(ms)
  ----------------------------------------
  月          0.00          0.01        +10
  亮          0.38          0.39        +10
  爬          0.76          0.77        +10
  上          1.14          1.15        +10
  来          1.52          1.53        +10
  了          1.80          2.19       +390
  风          2.18          2.57       +390
  也          2.56          2.95       +390
  停          2.94          3.33       +390
  了          3.32          漏检          —
  **只漏了一个起点,但按顺序硬对齐之后,后面 5 个字全错位了。**
  原因很直白:对齐是「第 i 个字配第 i 个起点」,**丢一个,后面全部往前挪一位。**
  这不是理论风险——**人唱歌本来就会连字、拖长音、抢拍。**
  所以真正做歌词对齐必须用**强制对齐(forced alignment)**:
  把「这句话的字序列」和「音频的音素序列」放在一起做动态规划,**而不是一个个数起点。**
  这一节能真跑的是包络检测那一步;**强制对齐要声学模型,沙箱里没有,这里如实说测不到。**

done -> _out_39m.txt

第一版 10 个字全检到,误差整齐地都是 +10 毫秒。 这个「整齐」很关键——说明它是系统延迟,不是随机误差。系统延迟可以整体减掉,随机误差不能。 第二版只检到 9 个。 原因就在 armed 那个变量:它要求能量先落回阈值的 60% 以下, 才允许找下一个起点。两个字连读时能量根本没落下去,于是第二个字被吞掉了。 然后就是那张错位表: 第 6 到 9 字全部 +390 毫秒,第 10 字漏检。390 毫秒差不多是一个字加一个气口的长度—— 正好是「往前挪了一位」的量。 这个 Demo 的价值不在于「包络检测有多准」, 而在于它把一个抽象说法变成了可见的数:「硬对齐对漏检零容忍」。 说明:这里是合成音频(每字一个纯音加谐波),不是真人演唱。 但「能量不落就没有新起点」这件事,跟着换任何音频都成立。真实演唱里还会有滑音、气声、颤音, 情况只会更难,不会更容易。

30 秒实验

  1. 把 armed 的回落阈值从 0.6 改成 0.95,看连读那一版能检到几个。 会检到 10 个,但也会把同一句里的音量起伏误判成新起点——两个错换着来。
  2. 把帧长从 10 毫秒改成 5 毫秒,看那个 +10 毫秒的误差变成多少。 会变成 +5 毫秒左右——因为系统延迟本身就是「帧长的一到两倍」。
  3. 给连读那一版的两个字之间插 20 毫秒的极小气口(能量降到 30%),重跑。 起点就能检到了——说明「气口」是这条路线的生命线。

常见误区

  1. 「识别歌词就是把文字转出来」。做 MV 字幕要的通常是时间轴,文字是现成的。 这是强制对齐的活,不是听写的活。
  2. 「对齐差一两百毫秒没关系」。字幕上一两百毫秒就是抢拍或慢半拍,人眼看着画面就能觉出来。
  3. 「自动对齐完就能发」。连读、拖长音、抢拍会让硬对齐整段错位,必须人听一遍。
自测三问(点开对答案) 1. 识别和对齐是两件什么事? —— 识别(ASR)是把音频听成文字;对齐(forced alignment)是把已知文字对上时间。 做 MV 字幕要的是后者。 2. 为什么漏一个起点会让后面全错位? —— 因为简单对齐是「第 i 个字配第 i 个起点」,丢一个起点,后面每个字都往前挪一位。 3. 正规的强制对齐怎么解决这个问题? —— 用动态规划把「字序列」和「音素序列」一起搜,取总分最高的切分。 某个字没有明显起点时靠上下文推断。

第八章到这里结束:从平台、元信息、数据质检出到音高映射、剪辑接缝、歌词对齐, 一条完整的声音链路走完了。第九章开始进行业场景——先看政务与教育。

相关笔记