docs(review): apply batch-04 consultant fixes and approve 5 tutorial/web3 posts
Deploy / build (push) Successful in 22s

- performUpkeep: external callable, re-check is mandatory security (not stale-data hygiene)
- BasicNft.tokenURI: ownerOf -> _ownerOf, dead custom error explained
- storage (SSTORE) vs EIP-170 runtime bytecode separated, derived ~1.3M gas for the two SVGs
- subs-15 retimed from a fresh faster-whisper word alignment (10.84s first line, was 28.22s) + SRT regenerated
- RDAP needs curl -L (bootstrap 302), ASN/Spamhaus blocklist check added
- learning-project framing -> Sepolia reference implementation / testnet validation build
- dynamicCta on the two posts that ended with no conversion block
- cross-links between the two NFT posts; tracker 91/91 approved
This commit is contained in:
hoelee committed 2026-10-27 06:13:00 +08:00
1 parent 01c55805fb
commit cd6511a0b3
14 files changed
+322 -166

No files matched your search

@@ -9,6 +9,7 @@ stream: creative-lab
tags: ["subtitles", "lrc", "srt", "whisper", "forced-alignment", "ai-music", "python"]
ogImage: "/og/zh-generating-subtitles-for-songs-without-timestamps.jpg"
banner: "/banners/generating-subtitles-for-songs-without-timestamps.jpg"
dynamicCta: true
draft: false
---
我试过的每一个 AI 音乐工具,交出来的东西形状都一样:一个音频文件。没有一个会告诉你**每一句是什么时候唱的**。只想听的话没问题,但只要你想把歌词放到屏幕上 —— 卡拉 OK 的那一行、视频字幕、播放器下面的滚动框 —— 就完全不够用。
@@ -84,6 +85,8 @@ for i, ch in enumerate(hanzi_in_word):
**故障 B —— 四行挤在 0.6 秒里。** 有一首歌的尾奏被识别器整段漏掉,剩下的行被压到相隔 0.18 秒。12 个字不可能在 0.18 秒里唱完。
**故障 C —— 六行被死死压在硬下限上。** 有一首的整个第一段主歌完全没有可用锚点,于是第 3 条规则把连续六行扫到了最小间隔:每行正好 1.20 秒,大约相当于每秒十个词。输出看起来依然「合理」—— 单调递增、在音频时长范围内 —— 直到我去听那一首,整段主歌六秒就唱完了。只对这一个文件重跑一次对齐,第一行就落到了 10.8 秒而不是 28.2 秒:锚点不是缺失,而是被匹配到了歌里错误的位置。
所以对齐之后,跑三条规则:
1. **估计这首歌自己的速率** —— 用相邻两行都已锚定的样本(单位数 ÷ 秒数,取中位数)。每行的期望时长 = `单位数 ÷ 速率`,下限约 1.8 秒。同一批里英文是 0.96 到 2.33 词/秒,中文是 2.3 到 4.1 字/秒,**不能用全局常数**。
@@ -141,7 +144,7 @@ a.dispatchEvent(new Event('timeupdate'));
## 结果
18 首歌,没有一首自带时间戳。现在每一首都有逐行的 LRC 和 SRT —— 只靠歌词加音频生成。
18 首歌,没有一首自带时间戳。现在每一首都有逐行的 LRC 和 SRT —— 只靠歌词加音频生成。上面覆盖率表里的中文那一行,来自另一批单独跑的 11 首中文验证集,不是这一页的播放器。
| | |
|---|---:|
@@ -149,8 +152,12 @@ a.dispatchEvent(new Event('timeupdate'));
| 字幕行数 | 342 |
| 平均锚定覆盖率 | 0.96 |
| 全行锚定的歌 | 9 / 18(每行都有时间) |
| 处理耗时 | 每首 63–90 秒,纯 CPU |
| 占用显卡 | 无 |
| 处理耗时 | 每首 63–90 秒,纯 CPU 跑 `faster-whisper` |
跑在 CPU 上是刻意的,不是我绕不过去的限制:这 18 首全部在 Core i9-13900K 上
(跑这套流程的那台 Windows 虚拟机)完成对齐,因为 GPU 要留给生成这些歌曲的
生产管线,而 `faster-whisper` int8 每首 63–90 秒已经足够便宜 —— 把对齐任务排在
一个扩散生成任务后面等,省下来的那点 CPU 时间根本不值。
下面每个播放器都会滚动歌词、把当前句保持在中间,旁边还有 **.srt 下载** —— 想烧字进视频或在 VLC 里打开都行。
@@ -189,7 +196,7 @@ a.dispatchEvent(new Event('timeupdate'));
[00:11.56]Underneath the floorboards where the shadows play
[00:14.60]I tied a little ribbon at the break of day
[00:18.32]A button for a table, a thimble for a cup
[00:22.02]I climbs the ivy ladder when the sun comes up
[00:22.02]I climb the ivy ladder when the sun comes up
[00:27.76]Sugar on the windowsill, a feather in my hand
[00:31.42]The garden is a jungle and a wonderland
[00:35.22]Careful not to wake the cat sleeping by the door
@@ -509,7 +516,7 @@ a.dispatchEvent(new Event('timeupdate'));
[00:38.16]Counting every falling tear that drops into the tide
[00:44.50]Waiting for the heavy gates of heaven to divide
[00:51.16]Do not fear the rising storm or shadows in the deep
[00:57.24]There is still a ancient promise that the ocean keeps
[00:57.24]There is still an ancient promise that the ocean keeps
[01:06.34]Spread your wings and rise above the sea of clouds and rain
[01:13.18]Never let the mortal world bind you with a chain
[01:19.74]Fly across the mountain peaks where immortal rivers flow
@@ -625,23 +632,23 @@ a.dispatchEvent(new Event('timeupdate'));
<audio controls preload="metadata" oncontextmenu="return false;" controlslist="nodownload" data-lrc="subs-15" src="https://content.hoelee.com/file/hoelee/music/Green-Train-to-the-Sea-Mr-Hoelee.mp3"></audio>
<pre id="subs-15" hidden>[ti:Green Train to the Sea]
[ar:Mr Hoelee]
[00:28.22]Clickety-clack upon the track, the green train starts to roll
[00:29.42]Leaving all the city gray behind the tunnel hole
[00:30.62]Forehead pressed against the glass, I watch the fields go by
[00:31.82]Golden wheat and windmills waving underneath the sky
[00:33.02]Take me where the spring wind blows across the open blue
[00:34.22]Ten miles of wildflowers blooming just for me and you
[00:38.36]Every station has a name I've never heard before
[00:42.68]Soon the scent of salty air will meet us at the shore
[00:57.62]Strangers sharing oranges and stories in the seat
[01:01.92]Sunlight dancing in the aisle around my dangling feet
[01:06.68]Postcards in my backpack waiting for a stamp and pen
[00:10.84]Clickety-clack upon the track, the green train starts to roll
[00:14.28]Leaving all the city gray behind the tunnel hole
[00:19.42]Forehead pressed against the glass, I watch the fields go by
[00:24.20]Golden wheat and windmills waving underneath the sky
[00:28.38]Take me where the spring wind blows across the open blue
[00:33.78]Ten miles of wildflowers blooming just for me and you
[00:38.82]Every station has a name I've never heard before
[00:43.46]Soon the scent of salty air will meet us at the shore
[00:57.42]Strangers sharing oranges and stories in the seat
[01:02.46]Sunlight dancing in the aisle around my dangling feet
[01:07.48]Postcards in my backpack waiting for a stamp and pen
[01:11.50]To tell the people back at home I'm coming back again
[01:16.24]Take me where the spring wind blows across the open blue
[01:21.14]Ten miles of wildflowers blooming just for me and you
[01:26.34]Every station has a name I've never heard before
[01:30.66]Soon the scent of salty air will meet us at the shore
[01:46.82]Look, there is the sea...</pre>
[01:16.22]Take me where the spring wind blows across the open blue
[01:21.52]Ten miles of wildflowers blooming just for me and you
[01:26.80]Every station has a name I've never heard before
[01:31.40]Soon the scent of salty air will meet us at the shore
[01:46.20]Look, there is the sea...</pre>
</div>
</figure>