我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

给任意一首歌做逐词卡拉OK高亮,误差控制在 5ms 级

给任意一首歌做逐词卡拉OK高亮,误差控制在 5ms 级 给任意一首歌做逐词卡拉OK高亮误差控制在 5ms 级【免费下载链接】pdoom-videoCode-rendered music video for Im Upping My P(doom)项目地址: https://gitcode.com/gh_mirrors/pd/pdoom-video卡拉OK逐词高亮看起来简单——词到了就亮、唱完就灭。但一旦把任意一首歌作为输入问题立刻变成一场信号处理与语音识别模型的联合战役唱片的伴奏混着人声、歌手会吞音和换气、副歌双轨叠唱、AI 合成的歌曲还带着不自然的延音……这恰好是 pdoom-video一个用 TypeScript three.js 代码渲染的音乐视频项目为《Im Upping My P(doom)》这首歌而生已经趟平的路。它的最终产物是精确到毫秒的词级时间戳前端用一行纯函数驱动高亮 wipe且实时预览与离线 4K 导出逐帧一致。这篇文章不聊渲染本身只拆解歌词怎么精确到每个词这件事人声分离与 CTC 强对齐如何产出词级时间戳Whisper 独立输出如何交叉验证5ms 级的信号特征精修规则长什么样以及前端wordProgress纯函数如何与渲染对接。全程对照仓库真实代码。一、人声分离与 CTC 强对齐词级时间戳怎么来第一步不是对齐是先把人声从伴奏里抠出来直接拿混音去对齐歌词伴奏的打击乐会让声学模型的发射概率一团糟。pdoom-video 的做法是双层分离用 Demucshtdemucs_ft把整首歌切成 vocals / drums / bass / other 四轨analysis/common.py再用 mel-band-roformer 的 karaoke 模型跑一版 lead vocal专门应对主唱被垫底和声埋住的场景。这里有个容易翻车的细节MP3 的 LAME 编码器延迟encoder delay会让分轨结果整体偏移。仓库里通过互相关测量得到固定偏移1015 samples 44.1kHz ≈ 23ms统一在加载时切除STEM_OFFSET_SAMPLES保证所有时间都以无间隙 MP3 解码为唯一时间基准。任何一步对齐工具的采样率、延迟不同最终时间戳就会系统性错位——这正是误差控制在毫秒级的第一道关卡。双 CTC 模型 多通道融合20ms 帧发射概率对齐的主体是字符级 CTC 强制对齐。analysis/ctc_emissions.py 用两个完全独立的声学模型分别跑torchaudioMMS_FA多语言、romanized 字符、专为对齐训练wav2vec2large-lv60k-960h英语字符。输入是 16kHz 的人声 stem按HOP 320 samples → 20ms/帧切帧输出每帧每个字符的对数概率。为了让两条独立信号尽量独立除了单声道求和还分别跑了副歌的双轨 L/R 通道——副歌是双轨叠唱的左右声道各自更接近单一声线。最终主发射集fused6把两个模型 × 三个源mono/L/R共 6 份发射概率做logaddexp概率混合def emissions(kind): if kind fused6: es [_common_logp(m s) for m in (mms, lv60k) for s in (, _vocL, _vocR)] return np.logaddexp.reduce(np.stack(es), axis0) - np.log(len(es))全曲一次 Viterbi 发音拼写适配 垃圾 token吸收杂质拿到发射概率后analysis/ctcalign.py 做的是全曲单次约束 Viterbi 对齐46 行歌词的全部字符拼成一条目标序列一次性在整首歌的时间轴上解码而不是逐行对齐。这么做有两个关键收益行与行之间插入一个 garbage star token它的帧级分数是当前帧最佳 token 概率减 margin于是 ad-lib、和声、尾奏哼唱全部被 star 吸收而真正匹配歌词的位置歌词字符依然胜出单词按发音拼写展开成子词单元。仓库的 analysis/pron.py 里有一张显式映射表PRON { AGI: ay gee i, P(doom): pee doom, ChatGPT,: chat gee pee tee, NVDA: en vee dee ay, RLHF: are el aitch eff, Neumanns: noymans, ... }没有这张表CTC 模型面对 P(doom)、NVDA、RLHF 这种歌词里的 AI 黑话几乎必然错位。展开后的子词单元顺带成为音节级时间戳的基础——这也是为什么 data/lyrics.json 里像AGI这种词会带syl数组{ w: AGI, start: 3.677, end: 4.704, conf: 0.84, syl: [[3.677, 4.06], [4.06, 4.355], [4.355, 4.704]] }最后Viterbi 支持对人工验证过的硬骨头注入锚点约束lo/hi时间窗仓库里保留了约 20 处手工校正FIX表例如副歌拾音 Im 的起音、被垫底和声埋住的终曲 P(doom) 等全部依据频谱/音高 QA 图人工确认。二、Whisper 独立输出交叉验证 信号特征 5ms 精修CTC 强对齐的精度上限受帧长20ms和声学模型制约且对长元音起音晚、擦音落点靠后这类语音学现象有系统性偏差。pdoom-video 的第二个层次是把对齐结果拉到 5ms 级。Whisper 作独立交叉验证analysis/whisper_run.py 用mlx-whisper large-v3-turbo在时间校正后的人声 stem 上跑word_timestampsTrue得到一套完全独立于 CTC 的词级时间戳。由于歌曲满是 AI doom 黑话还注入了一段初始 prompt列举 P(doom)、FOOM、shoggoth、RLHF、NVDA 等词汇压制幻觉。analysis/align.py 里的map_whisper用difflib.SequenceMatcher对 Whisper 词序列与歌词 token 做模糊序列对齐并对时间差超过 1.5s的匹配直接拒绝防止乱配对。此后 Whisper 时间戳不再直接改边界而是进入置信度公式c 0.35 0.3 * agree 0.2 * p 0.15 * wagree其中agree是各独立对齐mms、lv60k、双轨、lead对词起点的共识度±60ms 内算同意p来自 CTC 后验wagree是 Whisper 起点与最终起点的一致性±150ms。输出conf_final写进 lyrics.json渲染端可以据此把低置信词做得更保守。5ms 特征上的三条精修规则交叉验证解决对不对信号精修解决准不准。 analysis/vocal_feats.py 在人声 stem 上以5ms hopHOP110 22050Hz提取一组特征RMS 包络、pyin 基频 浊音概率、log-mel 频谱通量 onset 强度、4–10kHz 高频能量比sib_ratio专门抓擦音。analysis/align.py 的refine()对每个子词单元按序套三条规则rest-onset若词前存在 ≥50ms 静音且人声在 CTC 首个字符前 40ms 重新进入就把起点挪到人声重入点——解决 CTC 对长元音起音偏晚的问题如开场长 Ionset-snap否则在前窗内吸附到最强的频谱通量 onset元音起始词搜索窗放宽到 250ms对应连读单词的声门/元音起音fricative对 s/sh/ch/z/f/th/j/h 开头且非浊音 th 的词CTC 常把摩擦音落在摩擦结束处规则在起音前窗内沿sib_ratio回溯到 4–10kHz 噪声起始点。词尾同样有明确逻辑legato 连唱时词尾 下一词起点否则取RMS 低于本词水平 15dB 且持续 ≥60ms的时刻。全部处理完再做单调性约束相邻词起点至少间隔 20ms、杜绝重叠。特征帧是 5ms所以这套流程产出的词边界精度就是 5ms 量级——仓库 NOTES 对全曲 400 词的自我评估是词起点普遍在 30–50ms 内个别被和声埋住的词标注 ±100ms 的不确定性并给出理由。这种给出误差界的态度比任何精确到帧的宣传都诚实。三、前端 wordProgress 纯函数与 GPU 渲染的对接数据对齐完前端要做的反而简单——因为整个渲染器被设计成任意帧画面都是歌曲时间 t 的纯函数详见 docs/ENGINE.md词级高亮只是其中一个查询接口。一个纯函数吃遍所有高亮形态app/src/engine/lyrics.ts 定义了两个核心查询static wordProgress(w: Word, t: number): number { if (t w.start) return 0; if (t w.end) return 1; if (w.syl w.syl.length 1) { const n w.syl.length; for (let i 0; i n; i) { const [a, b] w.syl[i]!; if (t a) return i / n; if (t b) return (i (t - a) / Math.max(1e-3, b - a)) / n; } return 1; } return (t - w.start) / Math.max(1e-3, w.end - w.start); } static lineCharProgress(l: Line, t: number): number { let chars 0; for (const w of l.words) { const p Lyrics.wordProgress(w, t); chars p * w.w.length; if (p 1) break; chars 1; // the space } return Math.min(chars, l.text.length); }wordProgress返回 0→1 的演唱进度词前为 0、词后为 1词内线性插值带syl的复合词AGI、ChatGPT、P(doom)按音节分段推进实现字母逐个点亮。lineCharProgress再把整行的字符级进度算出来供逐字形 wipe 使用。它是纯函数给定(word, t)结果唯一确定——这正是预览与导出帧级一致的前提。从进度到画面Canvas2D、GPU uniform 与线段批次十六个场景模块app/src/scenes/把这两个函数用出了花逐字形点亮loss.ts里sparkX(t)按字符进度换算火花尖端横坐标ascent.ts中lit clamp(Lyrics.wordProgress(w, t) * w.w.length - charIdx[g.i])决定每个字形是否高亮整词色块填充room.ts用wordProgress当矩形 clip 的宽度系数把 P(DOOM) 随着演唱从左到右填充进轮廓loom.ts反向用cover 1 - p做色块擦除填条进度leftturn-gantt.ts直接拿p画甘特条填充比例p 1 ? rgba(signal, 1) : rgba(bone, 0.9)推进 GPU 着色器dense.ts把多个词的进度打包成vec4uniform 传入 GLSL——卡拉OK进度本身参与 GPU 端排布计算说明这套时间戳可以一直下沉到渲染管线的数据面机械书写同步单笔画 plotter 字体场景app/src/engine/stroke.ts提供writtenLength(st, charTimes, t)把书写进度与词级时间戳对齐火花拖着细线写歌词的视觉隐喻由此而来。时间戳还顺带驱动了剪辑app/src/timeline.ts 的cut()取目标歌词行首词所在节拍之前最近的一拍作为切镜点after()吸附到最近的重拍——切镜永远不切开正在唱的歌词且落在节拍网格上。用真实数据把闭环跑一遍仓库 data/lyrics.json 里第一行 I see sparks of AGI in your eyes 的成品时间戳I1.407–2.35、see2.35–2.739、sparks2.739–3.46、AGI3.677–4.704含三个音节分段、eyes5.263–5.88。配合 data/audio.json 的 132.007 BPM 节拍网格开场那一句从 1.4s 亮起逐词燃到 5.9s正好呼应下面这张开场场景定格TikZ 风格歌词随演唱逐词点燃整条链路是Demucs/mel-roformer 分离人声 → 双 CTC 模型 20ms 帧发射 → 全曲约束 Viterbi → Whisper 交叉验证 → 5ms 特征精修与置信度融合 → JSON 时间戳 → 前端纯函数查询 → Canvas2D/GPU/线段批次渲染。每一层都在为误差控制在 5ms 级服务分离层校正编码器延迟、对齐层用多源融合和发音映射兜住黑话、精修层用语音学规则修正声学模型的系统性偏差、置信度层公开每个词的可信区间。如果你也想给自己的任意一首歌做逐词卡拉OK可复用的部分恰好是这条流水线的前半段——analysis/ 目录下的脚本与模型选择、refine()的三条规则、置信度公式它们与具体歌曲无关而后半段的纯函数接口wordProgress则提醒你高亮效果做得再花哨其精度上限在进入前端之前就已经写死了。与其在渲染里堆帧率不如回到音频分析里把边界校到位。【免费下载链接】pdoom-videoCode-rendered music video for Im Upping My P(doom)项目地址: https://gitcode.com/gh_mirrors/pd/pdoom-video创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表