
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、不做换声」(换声此前试过,效果不好)。于是「像同一个人」只能靠选择:
- 每首用不同 seed 跑 N 趟 → 扩大候选池
- 分离每趟的人声 → 用 ECAPA-TDNN(speechbrain)算 192 维说话人嵌入
- 求全部候选的质心(=「共识歌手」)
- 每首选「嵌入最靠近质心 + 音高最靠近全批中位 + 质量最好」的那一趟
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 决定 —— 而人声身份正是在那里被决定的。推论有两条:
- 固定单一 seed 是浪费。它只锁住了「这一次」的结果,却放弃了整个候选空间。
- 多趟择优是达成音色一致的主力手段,不是补充。候选越多,择优的上限越高。 (本次因此从 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 的时间轴自动切段,两条规则:
- 自适应阈值:句间间隔通常 3–4.5 s,段落间隔明显更大 → 取「中位间隔 × 1.5」,夹在 [2.5, 12] 秒
- 合并碎块:少于 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)。
做法:
25-spec.py生成精确工单(段落 + 逐行定额 + 原词参考),按乐章分成 4 组- 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 个数据点(试点成都 370 Hz → 220.7 Hz)→ 结论「模型不照搬 ABC 八度」
- 8 个数据点看到明显单调相关 → 结论反转成「ABC 确实支配音域,应该做移调」
- 分离人声后重测 → 发现前两次的「宽散布」主要是测量假象(全混音上测基频会锁到贝斯)
最后才定下「不做移调」。三次结论,两次是错的。
🚨 教训
- 单个数据点不足以支撑一个方向性决策,尤其当这个数据点来自有噪声的测量
- 测量工具本身要先验证。这次 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 版污染、法语念白、制作人署名行)
所以后来固化成两条纪律:
- 所有脚本落成文件再跑,绝不在 PowerShell 里拼 bash 一行流
- 所有环境变量在 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
结果翻唱版反而更低。按字面就该得出「提示词没起作用」——但这个结论是错的。
🚨 为什么这个指标不可用(三重混淆)
- 模型错配:ECAPA 是在说话上训练的,用在歌唱上判别力很弱。 证据就在数据里:28 个不同的真人两两相似度高达 0.850 —— 说明它根本分不太开人。
- 录音质感混入:原曲是真唱片,翻唱版是合成音频 + 分离残留。 相似度里混进了「录音像不像」,而这跟「是不是同一个人在唱」无关。
- 曲目差异混入:两组都是 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 个), 趟次多的组会在质心里被过度代表,导致择优系统性偏向那一组。
修法:先求每首自身各趟的均值,再对这些均值求中点 —— 让每首歌等权重。
📝 最终挑选交给人工 自动择优优化的是「像同一个人」,不代表那一趟唱得最好。 咬字、情绪、句子的顺滑度这些指标测不出来 —— 所以全部趟次都保留,由人做最后判断。
相关推荐
