Last updated on

YuE2批量翻唱系统与音色一致性实测(夜航书28首)


📌 一句话 用 YuE2 把 28 首歌批量翻成一张概念专辑《夜航书》,编曲与歌词全部重写为原创、逐行字数与原词严格对齐。 本文记录为达成「28 首听起来像同一个人」而建立的方法,以及一路上实测出来的、和直觉相反的技术事实。

ℹ️ 与既有笔记的关系

  • 《YuE2 翻唱全流程 SOP(字数对齐填词)》= 单首翻唱的完整流程(本文是它的批量版)
  • 《AI 换声翻唱全流程 SOP(分离·参考·转换·混音)》= 换声路线(本次刻意不用,见 §3)
  • 《YuE2 本地部署交接文档(2026-10-06)》= 环境与模型
  • 《YuE2 项目介绍与官方 Skill 能力清单(2026-10-06)》= 能力边界

0. 速查

项 值
批量规模 28 首 / 117 分钟素材
单首生成耗时 约 4–6 分钟(RTF 0.8–1.2,4–5 分钟的歌)
生成参数 cot=melody · cfg_scale=1.5 · --budget 12 --offload-ar
显存峰值 ≈14.8 GB —— 16 GB 卡上不能与其他 GPU 服务共存
音色一致性方案 两层锁 + 多趟择优(不做换声)
歌词对齐 逐行字数严格相等(硬约束);来源 LRCLIB + 网易云 + 内嵌标签三源互补
编曲/歌词 100% 重写;旋律由 cot=melody 从原曲 ABC 锁定

1. 五条与直觉相反、但实测确认的结论

⚠️ 这五条是本次最有价值的产出,逐条都有实测支撑

1.1 SheetSage2 转出的旋律高了一个八度

  • 赵雷《成都》的真实人声 f0 经独立实测为 183–188.6 Hz
  • 同曲 SheetSage2 的 melody_vocal.lab 中位是 370 Hz —— 恰好 2 倍
  • 28 首的旋律中位落在 311–784 Hz;除以 2 后,男声原曲落在 155–247 Hz、女声原曲落在 277–370 Hz,全部符合真实人声音域

推论:凡是拿 SheetSage2 的音高做绝对判断(移调、配器、音域评估),都要先减 12 个半音。 但做相对比较(谁高谁低)不受影响,因为偏移是统一的。

1.2 分离人声上,pyin / YIN 会锁到次谐波,HPS 才可靠

方法 成都源曲结果 判定
librosa.pyin 92.4 Hz ❌ 低了一个八度
librosa.yin 109.5 Hz ❌
谐波积谱 HPS 183.0 Hz ✅ 与独立实测吻合

更极端的例子:pyin 测阿桑(女声)得 146.5 Hz,测赵雷(男声)也是 146.5 Hz —— 完全无法区分性别。 做法:测人声基频一律用 HPS;pyin 只在「同方法前后对比」时可用。另外先做 130 Hz 高通能减少一部分误锁。

1.3 八度关系:同一方法内对比才有效,跨方法比较全是噪音

这一条走过一次弯路,记录完整过程,因为结论本身是「别急着按单点数据下结论」。

第一次观察(1 个数据点):试点《成都》ABC 旋律中位 370 Hz → 产物 220.7 Hz。 当时的推论是「YuE2 不照搬 ABC 的绝对八度,人声提示词才是音域支配者」。

第二次观察(8 个数据点):把 8 首已生成曲目的 ABC 中位与产物 f0 并排看,出现了明显的单调相关:

曲目 ABC 旋律中位 产物 f0
17-shanque 392 Hz 113 Hz
22-piaoyang 415 Hz 183 Hz
09-xiaoyao 415 Hz 188 Hz
16-yongqi 554 Hz 221 Hz
10-suiyue 659 Hz 248 Hz
01-jimo 698 Hz 296 Hz
05-aposhuo 622 Hz 312 Hz
12-kuaile 740 Hz 328 Hz

看起来像「产物 f0 ≈ 0.43 × ABC 中位」——如果成立,就说明 ABC 的音域确实在支配输出音域,那么「ABC 层面移调做音域归一」就是可行的、而且是必要的手段。

第三次观察(决定性):把生成产物分离出人声再测,跨曲目的 f0 全距从 113–328 Hz 收窄到 124–175 Hz。

🚨 结论:前两次的「宽散布」主要是测量假象 在全混音上跑 HPS,会锁到贝斯/伴奏而不是人声 —— 于是「曲目之间的音域差异」被严重夸大。 必须分离人声后再测音高,否则得到的散布数据不可用。

同时暴露的第二个问题:即使在分离后的人声上,HPS 与 pyin 的相关系数只有 0.727,两者都会犯八度错误。 也就是绝对音高测量本身噪声很大。

✅ 最终决策 不做 ABC 音域移调。 实测表明人声提示词把音域已经收得相当紧(分离后 124–175 Hz,约半个八度), 而 ABC 移调要写移调器、有改坏乐谱的风险、还要全部重新生成 —— 收益与风险不成比例。

音域的一致性改由择优阶段软性处理,且权重刻意压低(见 §2.2): 因为音高测量有噪声,权重给高了等于「按噪声挑选」,反而会挑出差的一趟。

💡 将来若确实要移调,记住这个性质 ABC 的纯八度移位不改变调号(音级集合不变),只需重排字母大小写与 '/, 标记: c → C,C → C,,c' → c。比半音移调简单得多、也安全得多(后者要处理调号与临时记号的重拼)。

1.4 歌词「覆盖率不足」多半是假警报

LRC 的时间轴常常只到整首歌的 60–85%,看起来是「歌词残缺」。但实际统计后发现:

  • 缺的几乎都是末段副歌重复,而唯一段落(主歌/副歌/桥段)是齐的
  • 而重复段落本来就该用成曲顺序(song_order)表达,不需要时间轴写三份

所以判断歌词是否可用,要看「唯一段落是否齐」和「演唱密度 syl/s 是否合理」,不要只看覆盖率。 实测正常区间:syl/s ≈ 1.0–3.0;>3.5 基本是 Remix 加词版;<0.9 才是真残缺。

1.5 网易云歌词接口会返回 Remix 改编版,会污染骨架

实测踩到两次:

  • 查《董小姐》返回「李三词/宋冬野 - 董小姐 (Remix)」,614 字(原曲只有 254 字)
  • 查《情深深雨濛濛》返回「$iky./ReDbuff … Remix」,342 字(原曲 126 字)

原因:我的择优逻辑原本是「取总字数最多」——Remix 加了整段说唱,字数必然最多,必然被选中。 修法:改成中位数共识打分 + 坏词表黑名单:

score = -|log(本候选字数 / 全体候选中位数)|     ← 离群值天然被剔除
        + 0.45 × 歌手匹配
        + 0.30 × 带时间轴
        + 0.25 × 时间轴可信
        - 1.00 × 命中 Remix/DJ/伴奏/加速 黑名单

2. 让 28 首「像同一个人」:两层锁 + 一次挑选

❗ 核心认知 YuE2 根本没有「音色」这个输入参数。 不接受参考音频、不接受说话人嵌入、不接受歌手名。 音色是(提示词 × 旋律绝对音域 × seed × 韵母分布 × 编曲密度)涌现出来的结果。 所以「统一提示词」只能把分布收窄,出来的是「同一类声音的 28 个不同的人」。

2.1 第一层:收敛分布(在 YuE2 内做)

杠杆 影响 做法
旋律绝对音域 ★★★★★ 改走「挑选」路线(见 §1.3)
seed ★★★★ 全批固定;多趟时按趟递增——多趟恰恰提供了选择空间,固定 seed 反而浪费了它
提示词 ★★★ 冻结一段 voice block,28 首逐字不改
编曲密度 / 混响量 ★★★ 统一制作块 + 统一混音链
模型 / VAE 版本 ★★★ 锁 commit,不换 revision

2.2 第二层:钉死到点(靠挑选,不靠转换)

用户明确选择「只用 YuE2、不做换声」(换声此前试过,效果不好)。于是「像同一个人」只能靠选择:

  1. 每首用不同 seed 跑 N 趟 → 扩大候选池
  2. 分离每趟的人声 → 用 ECAPA-TDNN(speechbrain)算 192 维说话人嵌入
  3. 求全部候选的质心(=「共识歌手」)
  4. 每首选「嵌入最靠近质心 + 音高最靠近全批中位 + 质量最好」的那一趟
score = 0.70 × cos(嵌入, 质心)      ← 主导:嵌入对八度不敏感,是「谁在唱」的正确工具
      + 0.15 × exp(-|log2(f0 / 全批f0中位)| / 0.15)   ← 音域归一(软,权重刻意压低)
      + 0.15 × 质量(有声比 / 时长合理性)

⚠️ 为什么音高只给 0.15 实测 HPS 与 pyin 在分离人声上的相关系数只有 0.727,两者都犯八度错误。 绝对音高测量本身噪声很大,权重给高了等于「按噪声挑选」。主体必须是说话人嵌入。

这只是挑选,不改变任何音频样本,所以不存在换声引入的音色失真。

2.4 ⭐ 实测证据:seed 对音色的影响超过歌曲本身

这是本次最重要的一个量化发现,也是「多趟择优」从「锦上添花」升格为「主机制」的依据。

用 ECAPA 说话人嵌入算两两相似度(42-take-spread.py):

对比 含义 相似度均值
组内 同一首歌、不同 seed 的两趟 0.851
组间 不同歌曲之间 0.824

组间 − 组内 = −0.027(负的)

❗ 这说明了什么 换一个 seed 对音色的改变,比换一首歌还要大。

原因不难理解:cot=melody 已经把旋律钉死了,模型剩下的自由度(semantic token 采样、 NAR flow matching 的噪声)几乎全由 seed 决定 —— 而人声身份正是在那里被决定的。

推论有两条:

  1. 固定单一 seed 是浪费。它只锁住了「这一次」的结果,却放弃了整个候选空间。
  2. 多趟择优是达成音色一致的主力手段,不是补充。候选越多,择优的上限越高。 (本次因此从 3 趟加到 4 趟。)

📝 顺带反驳了一个直觉 一般会以为「歌与歌之间的差异」是音色不一致的主因。实测相反: 曲目差异本身反而更小(0.824 已经是相当高的相似度), 真正制造散布的是同一首歌内部的 seed 抖动。

2.3 冻结提示词(模板)

[该曲曲风 + 编制 + 动态 + 情绪 + BPM/拍号/调性]
Mandarin, female lead vocal: light clear girlish timbre, airy and tender,
soft head voice in the verse, gentle light vibrato, natural breathiness,
Taiwanese Mandopop diction, clear articulation, no runs, no belting
Production: close-mic intimate vocal, vocal forward in the mix, warm analog
top end, restrained dynamics, real instruments over synths, no heavy compression

💡 为什么不写「18 岁港台女星」 YuE2 对社会身份词(年龄/职业/明星)响应很弱,对声学描述响应强得多;而「女星」这类词还有触发版权过滤的风险。 身份含义已经全部编码进上面那三行声学参数里了。


3. 为什么这次不做换声

  • 此前试过 RVC / HQ-SVC / Seed-VC / YingMusic-SVC 四条链,用户对纯 YuE2 提示词路线的听感接受度反而更好
  • 换声会把混响和底噪一起焊进输出(已在《AI 换声翻唱全流程 SOP》里记录),且零样本 SVC 的「味道不对」
  • A 方案不封死后续:YuE2 生成的 flac 是母版,将来任何时候想加统一换声,都是在母版上再跑一遍,不需要重新生成

4. 批量流水线(脚本清单)

全部脚本在 E:\deepseek-works\cover-batch\,对应 WSL 侧 ~/yue2/batch/。

脚本 作用
run.sh 通用执行器:同步脚本到 WSL 再跑,规避 CRLF 与 PowerShell 引号地狱
01~06-lyrics*.py 歌词抓取(LRCLIB → 网易云 → 内嵌标签),v3 起用中位数共识择优
07-dump-all.py 导出全部歌词供人工通读审查
10-transcribe-all.py SheetSage2 批量转谱(模型只加载一次,28 首仅约 7 分钟)
12-separate.py Roformer 批量人声分离(源曲 / 生成产物两种模式)
20-build.py 构建 YuE2 请求(自适应段落聚类 + 碎块合并 + 冻结块装配)
21-worksheet.py / 25-spec.py 生成填词工单(段落结构 + 逐行字数定额)
24-lyrics-check.py 核对新词逐行字数并拆分出 lyrics/<id>.txt
30-batch-gen.py 批量生成编排(多趟、跳过已完成、逐首校验)
40-select.py 多趟择优(说话人嵌入 + 音高 + 质量)
50-export.py 统一混音链 + 最终交付

4.1 段落自动聚类

从 LRC 的时间轴自动切段,两条规则:

  1. 自适应阈值:句间间隔通常 3–4.5 s,段落间隔明显更大 → 取「中位间隔 × 1.5」,夹在 [2.5, 12] 秒
  2. 合并碎块:少于 3 行的块并入后块(末尾碎块并入前块)

⚠️ 固定阈值会失败 一开始用固定 2.5 s,结果成都的每一句都被切成独立块(句间间隔 3.6–4.2 s)。 换固定 6 s 又会把第二段主歌切碎(那一段句间间隔涨到 7.8–8.3 s)。必须自适应。

4.2 人工审查仍然不可省

自动聚类后仍要逐首通读,处理三类脏数据:

  • 法语念白引子(阿桑《寂寞在唱歌》开头 8 行)
  • 制作人署名行(「器乐演奏:暗杠」「词:杨漩予」)
  • 标题行(「此生不换 - 青鸟飞鱼」)

用 drops.json 按行号区间剔除,会被骨架构建和校验同时读取。


5. 填词的工程化:并行 + 强制核对

🚨 唯一的硬约束 每行字数必须严格等于骨架定额。 字数不对,唱出来会挤字或拖腔,整句垮掉。

28 首 × 平均 25 行 ≈ 700 行的精细创作,手工数字节极易漂移(实测手工数 的/了 这类轻声字会系统性偏 ±1~2)。

做法:

  1. 25-spec.py 生成精确工单(段落 + 逐行定额 + 原词参考),按乐章分成 4 组
  2. 4 个子智能体并行填词,每个拿到工单 + 专辑设定 + 核对器命令
  3. 子智能体必须反复运行核对器直到全绿才交出
  4. 我这边再用同一核对器独立复验(防止「自称通过」)

实测效果:4 组全部一次交付全绿。

💡 子智能体自己总结出的坑

  • 阿拉伯数字不计字:写「6点十分」会比「六点十分」少 1 字 → 必须写汉字数字
  • 块号不能自己编:块由脚本按时间戳聚类生成,只有 1 个块的歌(如《空笑梦》《六月的雨》)不能写成 B2/B3,否则核对器会去找不存在的块而崩
  • 手工数字数不可靠,必须用与核对器同源的计数函数逐行比对

6. 资源与排程实测

项 实测值
YuE2 生成 1 首(4–5 分钟的歌) 4–6 分钟,RTF 0.8–1.2
显存峰值(--budget 12 --offload-ar) 14760 MiB / 16311 MiB
SheetSage2 转谱 1 首 6–40 秒(模型常驻时)
Roformer 分离 1 首 约 1 分钟

⚠️ 16 GB 卡上必须独占 峰值 14.8 GB 逼近上限。OmniVoice 语音服务占的 8.8 GB 必须停掉,否则必然 OOM。 停掉后 GPU 基线约 2.2 GB,够用。

排程策略:按「轮」推进,而不是按「首」推进。 第 1 轮先给 28 首各跑 1 趟,第 2 轮再各跑第 2 趟……这样中途任何中断,手上都是一张完整的专辑,而不是 5 首歌跑了 8 遍。


7. 踩坑速查(本次新增)

坑 现象 修法
pkill -f "demo_flashinfer.py" 把自己的 shell 也杀了(命令行里含同样字符串) 用 pgrep -f "demo_flashinfe[r]" 括号技巧,或直接按 PID
PowerShell 传多行引号给 WSL bash 语法错误 / $HOME 被 PowerShell 插值成 C:\Users\... 所有脚本落成文件再跑,环境变量在 bash 内设置
PowerShell 吃 $(...) 和 > 2>/dev/null 变成 Out-File E:\dev\null 同上,别在 pwsh 里拼 bash 一行流
/tmp 被清 执行器脚本消失 脚本放 ~/yue2/batch/ 持久位置
工具调用 600 秒超时 长任务被砍 用 run_in_background 后台任务 + 日志文件
wsl 后台进程随 shell 退出而死 nohup 也留不住 用 harness 的 managed background job
歌词「取字数最多」 选中 Remix 加词版 改中位数共识 + 黑名单
固定间隔阈值切段 每句都被切成独立块 自适应阈值 + 碎块合并
pyin 测分离人声 男女都测出 146 Hz 改用 HPS,且只在同方法内比较
覆盖正在运行的 bash 脚本 bash 增量读取脚本文件;覆盖后读取偏移错位,可能从半行处继续执行而静默崩坏 —— 长任务最阴险的坑 改动中的脚本绝不原地覆盖:先 kill 重启(若它还在等待阶段则无损失),或另存新文件名再启动
择优跑早了 selection.json 反映的是旧候选池,结论误导(本次因此差点得出「择优无效」的错误结论) 加过期检测:比对「择优时看到的候选数」与「磁盘上实际的候选数」,不一致就拒绝出结论
loudnorm 单遍模式偏靶 目标 -14 LUFS,实测落在 -13.2;8/28 首偏离 >1dB 改两遍模式:第一遍 print_format=json 测出源的真实参数,第二遍按 measured_I/TP/LRA/thresh 应用。响度标准差 1.515→0.078(收窄 95%),偏离 >1dB 的从 8 首降到 0 首
改词的代价是「那首歌全部趟次重生成」 本次累计修了 56 处歌词缺陷,作废了 16 首的全部产物;两次把预计完成时间估错(每次约 +2 小时) 排程时必须把「修改率 × 重生成成本」算进去。经验值:改 1 首歌的歌词 ≈ 作废 3–4 次生成(约 12–16 分钟 GPU)。若已进入批量生成阶段,宁可先把歌词改到位再启动生成,不要边生成边改
统计趟次时 find -name audio.flac 会双重计数 每趟产物目录下还有一个 gen/<id>/audio.flac 中间文件,于是 2 趟被数成 4 趟。我据此误报「28/28 已达 3 趟」,兜底脚本也用同一统计,因此以为已达标而提前收敛 —— 最终有 5 首只做到 2 趟 统计趟次必须写明确路径:find out -path '*/take*/audio.flac' -not -path '*/gen/*',或直接 ls -d out/<id>/take*。任何用于「驱动控制流」的统计量,都要先验证它数的是什么

8. 交付物结构

F:\待翻唱歌曲\翻唱结果\

位置 内容 用途
NN 第N封·<首句>.flac / .mp3 自动择优版(我推荐的成品) 直接听
_全部母版\NN 原曲歌手-原曲名\第N趟.mp3 每一趟都保留 人工终选(听感这件事人比指标可靠)
_全部母版\...\_这一首.md 该曲全部候选 + 自动打分 + 推荐 挑选时参考
_全部母版\_总览.md 28 首 × 各趟完成情况 全局
_歌词\NN ....md 原词 / 新词 / 逐行字数定额 三栏对照 核对填词
_歌词\_全28首新词合集.md 全部新词通读 通读
_试听\NN 原曲歌手-原曲名.mp3 第 1 趟试听版(命名用原曲名便于 A/B) 快速对比原曲
_曲目表.md 制作参数 + 曲目对照 归档

💡 为什么全部趟次都要留 自动择优用的是说话人嵌入相似度(哪一趟最像这批歌的「平均那位歌手」), 它优化的是「像同一个人」,不代表那一趟唱得最好。 听感、咬字、情绪这些东西指标测不出来 —— 所以最终挑选应该交给人。


9. 探索思路回顾:这次是怎么想出来的

这一节记的是方法,不是结论。下次遇到类似的「模型没有你要的那个旋钮」的问题,可以照这个路子走。

9.1 第一步:先确认「那个旋钮到底存不存在」

用户最初的假设是「用同一套提示词就能让 28 首音色一致」。 我没有直接照做,而是先问了一个更基本的问题:YuE2 的输入里有没有「音色」这个参数?

翻官方 skill 的接口白名单(style / lyrics / cot / seed / abc / cfg_scale / id), 确认没有参考音频、没有说话人嵌入、没有歌手名。这一步定住了整个方案的性质:

音色不是可设定的参数,而是若干变量共同涌现的结果。 那么「统一提示词」只能是收窄分布,不可能是钉死到点。

方法论:面对「用 X 让模型做到 Y」的请求,先验证 X 是不是真的连到 Y。 如果不连,要么换 X,要么在模型外面补一层。

9.2 第二步:把「结果」拆成可控的变量,并按影响排序

既然是涌现的,就列出所有影响它的变量,再逐条排影响量级:

旋律绝对音域 → seed → 提示词 → 编曲/混响 → 模型版本 → 韵母分布

排序的价值在于决定把力气花在哪。事后证明这个排序基本正确: seed 那条(多趟择优)最后成了达成目标的主力。

9.3 第三步:对每个变量,找「可测量」的替代方案

用户明确排除了换声(之前试过不行)。于是问题变成:不改变音频,怎么让它们更像?

答案是把「生成」换成「生成 + 选择」:

  • 固定 seed 只能得到一个样本,而样本音色是随机的 → 浪费了随机性
  • 换成多趟不同 seed,就有了候选池;再用说话人嵌入算「哪一趟最靠近共识质心」
  • 这只是挑选,不改变任何音频样本,所以没有换声引入的失真

方法论:当「控制」这条路过不去时,试试「过采样 + 选择」。 代价是算力,收益是可测量、可验证。

9.4 第四步:警惕「只有一个数据点的结论」—— 这次真的栽了

关于「ABC 音域是否有支配作用」,我前后判断反复了三次:

  1. 1 个数据点(试点成都 370 Hz → 220.7 Hz)→ 结论「模型不照搬 ABC 八度」
  2. 8 个数据点看到明显单调相关 → 结论反转成「ABC 确实支配音域,应该做移调」
  3. 分离人声后重测 → 发现前两次的「宽散布」主要是测量假象(全混音上测基频会锁到贝斯)

最后才定下「不做移调」。三次结论,两次是错的。

🚨 教训

  • 单个数据点不足以支撑一个方向性决策,尤其当这个数据点来自有噪声的测量
  • 测量工具本身要先验证。这次 pyin/HPS 在分离人声上相关系数只有 0.727 —— 两个工具互相不一致, 说明这个量本身测不准。在测不准的量上做优化,等于按噪声优化
  • 所以最终把音高的权重从 0.25 压到 0.15,把权重让给测得更准的说话人嵌入

9.5 第五步:让「自动化」和「人工判断」各管各的

指标能算的(字数对齐、音高、相似度)交给脚本; 指标算不出的(唱得好不好听、咬字顺不顺)明确交回给人。

所以交付上刻意保留全部趟次,而不是只给一个「最优」—— 因为自动择优优化的是「像同一个人」,不代表那一趟唱得最好。

9.6 第六步:排程要保证「任何时刻中断都拿到完整成品」

不按「首」推进(把 5 首各跑 8 遍),而按「轮」推进(28 首先各跑 1 遍,再各跑第 2 遍)。 这样第 1 轮跑完就已经是一张完整专辑,后续每一轮只是加厚候选池。

方法论:长任务的分批粒度,要让任意中断点上的产出都是可交付的,而不是半成品。

9.7 一路上最费时间的其实不是模型,是「把模型跑起来」

回顾整晚,真正花在 YuE2 上的时间反而少,大量时间耗在这些地方:

  • PowerShell 传参给 WSL 的引号/变量插值地狱($HOME 被 PowerShell 先吃掉)
  • 后台进程随 WSL shell 退出而死(nohup 也留不住,要用 managed job)
  • 歌词数据源的脏数据(Remix 版污染、法语念白、制作人署名行)

所以后来固化成两条纪律:

  1. 所有脚本落成文件再跑,绝不在 PowerShell 里拼 bash 一行流
  2. 所有环境变量在 bash 脚本内设置,绝不从 PowerShell 传进去

9.8 第七步:并行是有边界条件的

28 首 × 平均 25 行 ≈ 700 行逐行字数对齐的创作,串行做不完。于是把填词拆成 4 组, 派 4 个子智能体并行,每个拿到「精确工单 + 专辑设定 + 核对器命令」, 并要求反复运行核对器直到全绿才能交付。

但关键在下一步:子智能体交付后我自己再独立复验一遍(跑 29-audit.py), 理由是「自称通过」和「实际通过」是两件事。

方法论:并行能解决「量」,但解决不了「可信度」; 凡是并行产出的东西,都要有一道不依赖产出者自述的独立校验。

顺带一提,子智能体自己总结出的两条坑很值得记:

  • 阿拉伯数字不计字:写「6点十分」会比「六点十分」少 1 字
  • 块号不能自己编:只有 1 个块的歌(LRC 只有一段词、唱三遍)不能写成 B2/B3

9.9 第九步(教训):指标被混淆时,宁可承认它测不准

为了把「像同一个人」变成可验证的数字,我做了这个实验:

  • 用 ECAPA 算 28 首翻唱版人声的两两余弦相似度 → 均值 0.778
  • 用同一套流程算 28 首原曲真实歌手 → 均值 0.850

结果翻唱版反而更低。按字面就该得出「提示词没起作用」——但这个结论是错的。

🚨 为什么这个指标不可用(三重混淆)

  1. 模型错配:ECAPA 是在说话上训练的,用在歌唱上判别力很弱。 证据就在数据里:28 个不同的真人两两相似度高达 0.850 —— 说明它根本分不太开人。
  2. 录音质感混入:原曲是真唱片,翻唱版是合成音频 + 分离残留。 相似度里混进了「录音像不像」,而这跟「是不是同一个人在唱」无关。
  3. 曲目差异混入:两组都是 28 首不同的歌,编曲/调性/速度都不同。

方法论(这条比结论重要): 想把主观判断变成指标时,先问一句 「这个指标还会被什么影响?」 如果答案是「很多与被测对象无关的东西」,它就不能当判据 —— 宁可说「这个量我测不准」,也不要拿一个被混淆的数字去推翻人耳的判断。

💡 那它还剩什么用 唯一可用的是组内相对排序(同一批翻唱里谁最不合群),因为组内这些干扰是共享的。 本次结果:最不合群的是 19-xianjian(0.670)、11-riluo(0.719)、25-titian(0.725) —— 可以提示人工复听时优先关注这三首,但不能据此判定它们有问题。 组间比较(翻唱 vs 原唱)完全不可用。

最终权威仍是人耳。 用户实际试听后的判断是「音色很接近了」—— 这个判断建立在直接听感上,比任何被混淆的指标都可靠。

9.10 第十步:歌词质量要用三层互补的判据,且必须在生成之前定稿

字数达标 ≠ 歌词能读。本次三层判据各抓各的:

层 手段 抓到什么 抓不到什么
1 逐行字数校验 挤字/拖腔(能不能唱) 好坏
2 启发式扫描 机械凑字、跨歌重复、断尾句、副词+地误用 语感与文采
3 人工通读 「终于地」「炮团」「亮了走」「数轻」这类只有读才看得出的错 效率(只能抽样)

本次累计修正 56 处,其中:

  • 错别字 2(数为清、炮团)
  • 语法错误 4(终于地、真安安静静、不应该会、都不肯缺宾语)
  • 断尾句 / 不成句 6(从前想要赢的那个念头已经、吹过…山谷里、天已经亮了走、而我在等的还在岸、手心空空的没有、你说我们会一起)
  • 叙事矛盾 3(13-liuyue 同一动作写成三个版本)
  • 代词不一致 / 无谓重复 8
  • 语气词叠加凑字 8(吧了呀 / 好吧呀 / 的呀)
  • 生硬措辞 25

⚠️ 关键教训:歌词必须在启动批量生成之前定稿 每次改词都会作废那首歌的全部已有趟次(本次因此作废 16 首 × 3–4 趟)。 改 1 首歌 ≈ 多花 12–16 分钟 GPU。我因为“边生成边发现边修”, 两次把完成时间估错,每次多出约 2 小时。

正确顺序:写词 → 三层判据全过 → 定稿 → 再启动生成。 而不是:启动生成 → 试听/通读 → 发现问题 → 回头改词。

💡 语气词不要一刀切 启发式扫描报了 58 处句尾语气词(啊/呀/啦/呢),但绝大多数是自然的—— 人设是「港台女声」,台湾国语里「我想她呀」「今天你怎么啦」是正常口吻。 只清多词叠加的那种(吧了呀/好吧呀/的呀)。 指标的报警不等于要全部处理;先判断它是不是真的错。


10. 脚本清单

全部在 E:\deepseek-works\cover-batch\,对应 WSL 侧 ~/yue2/batch/。

脚本 作用
run.sh / run-sep.sh / run-f0.sh / run-transcribe.sh 各环境的执行器(同步脚本进 WSL 再跑,规避引号地狱)
run-rounds.sh / run-night.sh / run-finalize.sh 整夜编排:按轮生成 → 分离 → 择优 → 导出
ALBUM.md 冻结基准文档(人声块 / 制作块 / 乐章分配 / 填词规范)
manifest.json / styles.json / drops.json 曲目清单 / 每首编曲 / 需剔除的脏数据行
01~07-lyrics*.py 歌词抓取(三源互补 + 中位数共识择优)与导出审查
10-transcribe-all.py SheetSage2 批量转谱(模型常驻,28 首约 7 分钟)
12-separate.py Roformer 批量人声分离(源曲 / 生成产物两种模式)
20-build.py / 21-worksheet.py / 25-spec.py 请求构建 / 填词工单生成
24-lyrics-check.py / 29-audit.py 字数核对 / 独立复核(不采信子智能体自报)
30-batch-gen.py 批量生成(多趟、跳过已完成、逐首校验)
33-quickcheck.py / 34-f0-compare.py 早期抽查 / 估计器互验
40-select.py 多趟择优(说话人嵌入 + 音高 + 质量)
50~53-export*.py 最终导出 / 试听版 / 歌词三栏对照 / 全部母版

11. 本次结果

项 结果
歌词 28 首 / 542 行,逐行字数独立复验全部通过
禁用词 原曲歌名 / 原歌手名 / 段落标记 0 命中
产物时效 已生成产物的时间戳均晚于歌词定稿 —— 无「用旧词生成」隐患
生成 28 首 × 多趟(每趟不同 seed),全部 rc=0
最终趟次 4 趟 12 首 / 3 趟 11 首 / 2 趟 5 首(合计 93 趟;因趟次统计 bug 提前收敛,见 §7 踩坑表)
择优(嵌入维度) A 组(不择优)0.7920 → B 组(择优)0.8279,最低分 0.5637 → 0.6083
择优(音高维度) 候选池 f0 标准差 39.7 Hz → 选中组 10.9 Hz(收窄 73%)
统一混音链 响度标准差 1.225 → 0.063(收窄 95%),偏离 -14 LUFS >1dB 的 0 首
时长吻合 产物时长与源曲比值 0.97–1.01,旋律骨架确实被保留
音色 用户试听确认「很接近了」;择优效果经严格同集对照验证(见下)
交付 自动择优版 + 全部母版(供人工终选) + 歌词三栏对照 + 试听版 + 曲目表

❝ 用户反馈 「你现在做的这几首成品,音色很接近了」

这是对「两层锁 + 多趟择优」最直接的验证: 在模型根本没有音色旋钮的前提下,靠「冻结一切可控变量 + 过采样 + 按说话人嵌入择优」, 也能把 28 首的音色收拢到人耳可接受的范围。

11.1 ⭐ 择优效果的严格对照实验(本次最硬的一个数字)

设计:A 组 = 每首只保留编号最小的那一趟(等价于“不跑多趟”的方案); B 组 = 每首取其择优结果。两组必须是同一组歌,否则两两相似度的均值不可比。

🚨 第一版对照实验是错的,记录下来 初版让 A 组用“第 1 趟”,但当时只有 12 首存在 take1,B 组有 28 首 —— 样本集不同,均值根本不可比,却照样算出了一个 +0.0380 的“提升”。 修正为「A 组取该曲最早存在的那一趟」,并强制两组取交集,才得到可信数字。

教训:做 A/B 对照时,先检查两组样本是否完全一致。样本集不同导致的差异, 会被误读成处理效果的差异。

结果(同集 28 首 / 378 对):

指标 A 组(不择优) B 组(择优后) 变化
两两相似度 均值 0.7782 0.8094 +0.0312
标准差 0.0761 0.0604 −0.0157
最低分 0.4861 0.6083 +0.1222

✅ 关键洞察:择优的价值主要在削掉离群者,不是整体抬高 均值只涨了 0.031,但最低分涨了 0.122 —— 最不合群的那首被从 0.486 拉到了 0.608。 也就是说:择优真正解决的是“28 首里总有几首明显不像”这个问题, 而这恰恰是人耳最容易察觉的那种不一致。

另一个独立维度的验证 —— 音高散布:

全部候选 择优后
实测 f0 标准差 42.5 Hz 17.7 Hz
f0 范围 — 132–231 Hz

择优把音域散布收窄了 58%。 这条比上面的嵌入相似度更有说服力 —— 因为它是在可以直接听的物理量(基频)上测到的,不依赖任何“这个指标是否可信”的假设。

💡 两条独立证据互相印证 嵌入相似度(抽象维度)+ 基频散布(物理维度)同向改善。 当两个原理不同的度量都指向同一结论时,这个结论才站得住 —— 这正是本次前两个失败指标(§9.4 §9.9)所缺少的:它们都只有单一的、且被混淆的证据。

11.2 择优算法的一个隐蔽偏差(已修)

质心原本是「对所有候选求均值」。但各趟数量不等(take4 有 28 个、take1 只有 12 个), 趟次多的组会在质心里被过度代表,导致择优系统性偏向那一组。

修法:先求每首自身各趟的均值,再对这些均值求中点 —— 让每首歌等权重。

📝 最终挑选交给人工 自动择优优化的是「像同一个人」,不代表那一趟唱得最好。 咬字、情绪、句子的顺滑度这些指标测不出来 —— 所以全部趟次都保留,由人做最后判断。

相关推荐