现场版却配上录音室版的歌词?聊聊我给开源 Mac 歌词软件写的多源打分算法

我在写一个开源的 Mac 歌词软件 Lyrimuse:识别你正在放的歌,把逐字歌词显示在桌面、菜单栏、刘海下面或者一个完整的歌词窗口里。做到后来发现,显示反而是简单的部分,最花时间的是另一件事:找到的歌词,到底是不是你正在听的这一版。

这篇讲我是怎么解决这个问题的。

为什么「拿第一个不为空的」不行

大部分歌词工具的做法差不多:按顺序问几个歌词源,网易云没有就问 QQ 音乐,QQ 音乐没有就问下一家,拿到第一个不为空的就用。

问题在于,同一首歌在歌词源里通常不止一个版本:录音室版、现场版、伴奏版、剪辑版、混音版、重新灌录的英文版。按歌名和歌手去搜,搜出来的第一个往往不是你在放的那一版。

最典型的是现场专辑。你放的是演唱会版,拿到的是录音室版的词,前几句看着没问题,等到观众欢呼、歌手说几句话,后面就整段对不上了。

所以 Lyrimuse 换了个思路:十二个源一起查,每份结果都打分,选分最高的。

整体流程

markdown 复制代码
本地曲目信息(歌手 / 歌名 / 专辑 / 时长)
        │
        ▼
十二个源并发查询(网易云、QQ、酷狗、酷我、咪咕、Musixmatch、LRCLIB、
LyricFind、Deezer、AMLL、汽水音乐,连了账号的话还有 Apple Music)
        │
        ▼
资格审核:不合格的直接否决
        │
        ▼
逐项打分,再做几步需要全体候选一起比较的检查
        │
        ▼
选最高分,把整个过程记下来

引擎是 Go 写的,在后台跑;App 是 Swift 写的,只负责显示。

第一关:资格审核

有些结果连打分的资格都没有,直接否决:

  • 不是真的带时间轴的歌词:纯文本,或者带时间戳的行太少。
  • 语言明显不对:歌手和歌名都没有汉字,歌词却一大半是汉字。
  • 只有署名:整份只有作词、作曲、编曲,或者就是一行「纯音乐,请欣赏」。
  • 提不出最后一句的时间:后面的时长判断没法做。

还有一道针对逐字歌词的检查。有的源会返回一份只覆盖前面一小段的逐字时间轴,看起来是逐字歌词,后面全是空的。实测这种残片只覆盖到全曲的两成左右,正常的逐字歌词最低也有八成五,所以逐字时间轴结束得太早(不到整首歌词一半)就当它没有逐字。

第二关:打分

下面是现在的打分表。每一项分值都是拿真实曲库回放、对比结果定下来的,后面会拿几首歌举例。

看什么 分值 说明
时长吻合 +100 到 +300 歌词最后一句的时间跟歌曲时长比,越接近分越高。偏差超过 25% 不给分
歌词比歌还长 -700 最后一句比歌曲结束还晚 5 秒以上,物理上不可能是同一版
时长明显不符 -500 最后一句离歌曲结束差得太远
源自报的曲长不符 -400 有的源会告诉你它那首歌多长,跟本地差 12% 以上就扣
有逐字时间轴 +400 能做逐字高亮
跟当前播放器同一家 +250 在 QQ 音乐里放,就偏向 QQ 音乐的词,因为时间轴最可能对得上
版本标记不一致 -600 歌名、专辑名里的 Live、Remix、伴奏、Edit、English ver. 这类标记两边对不上
另一场演出 -600 两边都是现场版,但演唱会的名字完全不同
时间轴整体错开 -600 跟其它几家对齐的歌词比,整份都提前或推后了几秒
间奏里多出一段 -600 别家在这里都是十几秒的空白,它却塞了好几句词
专辑吻合 +40 到 +150 只加不减,专辑对不上不算负证据
歌名吻合 +30 到 +120 完全一致最多,去掉括号后一致、双语名一致的少一些
几家内容一致 +150 / +250 一家或两家以上给出的正文基本一样,说明这份可信
带翻译 / 读音 +50 / +30 同样对得上时,优先选能显示翻译的
行数 每行 +1,最多 200

「几家内容一致」用的是正文的 3-gram 相似度,超过 0.55 算一致。这里有个细节:有些源其实共用同一份数据,比如 Deezer 和 LyricFind 背后是同一个歌词供应商,这种只能算一家,不然它俩互相印证,平白多拿一份分。

三个例子:打分是怎么起作用的

1. 原版和单曲剪辑版

Prince 的《Diamonds and Pearls (2023 Remaster)》,本地时长 283 秒。候选里有两份看起来都很像(下表是一次实际评估的分项记录):

候选 时长 逐字 行数 专辑 歌名 几家一致 版本标记 合计
QQ 音乐《Diamonds and Pearls (2023 Remaster)》 +291 +400 +118 +150 +120 0 0 1079
酷狗《Diamonds And Pearls (Edit)》,260 秒 +299 +400 +53 0 +60 +250 -600 462

酷狗那份是单曲剪辑版。只看时长,它反而更接近,而且还有另外两家的正文印证它。决定胜负的是版本标记:本地没有「Edit」,候选有,扣 600。

这里「edit」只按整词匹配,因为它是「edition」的前缀,按子串匹配会把所有 Deluxe Edition 的专辑都当成剪辑版。另外,「Remaster」刻意不算版本标记:重新母带处理的还是同一次录音,时间轴是同一条。

2. 日文原曲和英文版

優里的《ドライフラワー》,有一个重新灌录的英文版。原曲 285.6 秒,英文版 285.7 秒,只差 0.1 秒,照着原曲编曲重录的另一语种版本,时长天然几乎一样,时长这一项完全分不开它俩。

能分开它们的是版本标记:歌名里的「English ver.」跟「粤语版」「国语版」「日语版」「韩语版」一样,算作语种版本。本地是原曲、候选标着英文版,扣 600,日文原曲那份就胜出了。

3. 同样是现场版,但不是同一场

方大同《公园 (Live版)》,候选里有两份现场版:一份是这张现场专辑的,一份是另一场演唱会的。两边都标着 Live,版本标记一致,上面那条检查不会起作用。

所以有专门的「另一场演出」检查:本地专辑名本身带现场标记,候选也是现场录音,两边去掉歌手名和「Live」「演唱会」这类通用词之后,剩下的身份词(场馆、巡演名、年份)完全没有交集,就判定为另一场,扣 600。四个条件缺一不可,只要两边有一个共同的身份词,就当作同一场,不会误伤。最后选中的是 QQ 音乐那份同一场的,1090 分。

两个设计取舍

不给任何歌词源固定加分

很容易想到的做法是给几个「质量好」的源固定加点分。我拿 250 首歌做过对照:这样的固定加分改变了 69 首歌的选择,其中没有一次是把错的改成对的,反而有 6 次把对的改成了错的。所以现在不设任何固定偏好,同分时按固定顺序决胜。

唯一保留的偏好是动态的:你在哪个播放器里放,就偏向那家的歌词(+250)。理由不是那家质量好,而是它的时间轴跟你的播放器最可能对得上。

重扣但不否决

除了第一关的否决,其它扣分再多,分数最低也只到 1。也就是说,一份存疑的歌词也比没有歌词强:只有在找不到更好的时候,它才会被用上。

第三关:选出来,并且能查

选最高分的那份,歌词、翻译、读音、逐字时间轴都跟着它走,不会这家拿正文、那家拿翻译拼在一起。

每一轮评估都会记下来:这次问了哪些源、谁应答了、每份候选每一项得了多少分、因为什么被否决,以及最后谁赢了。App 的歌词管理窗口里有个「解析决策」面板,直接把这份记录摊开给你看。

这个面板最大的用处是查错:觉得哪首歌配得不对,打开面板就能看到赢的那份赢在哪一项、对的那份输在哪一项,不用猜。

改规则之前,先过回归测试

打分规则会一直调整,最怕的是改好一首歌、改坏另外三首。所以仓库里有一套歌词搜索的回归样本:每个样本是一首真实曲目各个源的原始应答,加上已经确认正确的结论。样本覆盖了华语、英文、日文、韩文、粤语、语种版本、同场和另一场现场版、版本标记错配、多歌手合唱、各类否决、纯音乐等情况。

两个细节:

  • 正确答案不能直接信缓存。每个样本的冠军都要通过一组独立的判断:歌名对得上、版本一致、自报时长偏差在 3% 以内、至少有另一个源印证正文。缓存里那份只是旁证。
  • 样本里的歌词正文是置乱过的。真实歌词放进 git 仓库等于再分发,所以采集时把正文做了一次一一对应的字符替换,时间戳、标点、署名行原样保留,保证打分用到的特征替换前后完全一样。写入前会拿替换前后的打分结果做对比,有一项不一样就拒绝入库。

配合打分的几个设置

  • 匹配算法:默认「智能」,就是上面这套打分;也可以改成「顺序优先」,不打分,按你排的顺序用第一个有结果的源。
  • 随匹配算法更新重新选择:新版本改了打分规则以后,在后台把已经找到的歌词重新评估一遍,有更合适的就换上。
  • 锁定手选歌词:你自己从搜索结果里挑的那份,以后的自动匹配都不会换掉它。

最后

Lyrimuse 免费开源(GPL-3.0),macOS 14 及以上,支持 Apple Music、Spotify、网易云音乐、QQ音乐、酷狗音乐、汽水音乐、KKBOX、Amazon Music 和 Kaset,浏览器里的 YouTube Music、Spotify 网页版也行。打分的代码在仓库的 lyrimuse-engine 目录下。

如果你遇到配错的歌,欢迎在 GitHub 上提 issue,附上「解析决策」面板里的记录,对我来说就是下一条规则的来源。

项目地址:github.com/Yudaotor/ly...

相关推荐
对空六课1 小时前
Vue 组件曝光埋点为什么会重复触发?去重方案与实现思路
服务器·前端·算法·数据分析
David@1 小时前
Mac中cursor怎么检查更新
macos
HZY1618yzh1 小时前
下一代编程语言出炉
c++·算法
CoderYanger2 小时前
A.每日一题:856. 括号的分数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
kiracrimson2 小时前
选择排序与快速排序:一次选极值,一次分区间
算法·排序算法
All for pursuit.2 小时前
【动态规划-8】152.乘积最大子数组
数据结构·c++·算法·leetcode·动态规划
信奥卷王2 小时前
[GESP202512 六级] 路径覆盖
数据结构·算法
Lazionr2 小时前
手撕 unordered_set 和 unordered_map,剖析哈希表底层实现
c++·算法
0+1113 小时前
算法 --归并排序
数据结构·算法·排序算法