music-disc-video:一首歌 → 旋轉碟片 + 滾動歌詞
把「封面圖 + 音樂 + 歌詞」做成:
- 單一 HTML 檔 —— 雙擊就能播,圖跟音樂都內嵌,複製到任何電腦都不會破
- MP4 影片 —— 1920×1080 / 1080×1920 / 1080×1080,可直接上傳社群
第一次使用先做這個
python3 <SKILL_DIR>/scripts/env.py
會檢查 numpy、Pillow、中文字型、ffmpeg 有沒有齊。有 ✗ 的照著它給的指令裝。
Windows 上通常沒有
python3這個指令,本文所有python3都改成python。 跑起來說「找不到 python3」就是這個原因,不是環境壞了。
工作流(給 Claude 跟著做)
💡
<SKILL_DIR>是本 SKILL.md 所在的資料夾。 ⚠️ 所有產出都寫進使用者的歌曲資料夾,不要寫進 skill 資料夾。
步驟 1:確認素材,建立歌曲資料夾
跟使用者要三樣東西,放進同一個資料夾:
| 素材 | 說明 |
|---|---|
| 封面圖 | .png / .jpg,正方形最好(會同時當背景、封面、碟片) |
| 音樂 | .mp3 |
| 歌詞 | .srt / .lrc / 純文字都可以,沒有時間軸也沒關係(見步驟 2) |
然後在該資料夾建立 project.json:
{
"title": "歌名",
"subtitle": "英文副標或留空",
"art": "cover.png",
"audio": "song.mp3",
"lyrics": "lyrics.txt",
"layout": "16x9",
"out_prefix": "musicdisk"
}
layout 要問使用者想要哪一種(用 AskUserQuestion):
| 值 | 尺寸 | 適合 |
|---|---|---|
16x9 |
1920×1080 | YouTube、電腦螢幕、上課投影 |
9x16 |
1080×1920 | IG Reels、抖音、YouTube Shorts |
1x1 |
1080×1080 | IG 貼文 |
16x9-solo 9x16-solo 1x1-solo |
同上 | 純音樂版(無歌詞,碟片放大置中) |
純音樂版不需要 lyrics 欄位。
步驟 2:歌詞沒有時間軸的話,先對時
有兩條路。先建議方法 A,使用者做不到才走方法 B。
方法 A:請 Gemini 直接聽出時間軸(快,優先推薦)
叫使用者到 Google AI Studio 上傳這首歌的音檔或影片,送出這段 prompt:
請提取這部影片的完整字幕,格式要求如下:
1. 使用 SRT 字幕格式
2. 每一句話(以句號、問號、感嘆號為斷句點)獨立一條,不要把多句話合併
3. 時間軸精確到秒,格式為 HH:MM:SS,毫秒 --> HH:MM:SS,毫秒
4. 不要用時間區間概括一整段,要逐句對應
輸出範例:
1
00:00:01,000 --> 00:00:03,500
第一句話。
2
00:00:03,500 --> 00:00:05,200
第二句話。
把輸出存成 .srt 放進歌曲資料夾、更新 project.json 的 lyrics 就好 ——
標準 SRT 是 lyrics.py 原生支援的格式,不用再轉檔。
第 2 點(逐句斷開)和第 4 點(不要用區間概括)是關鍵: 少了它們,模型很容易回一整段配一個大時間區間,那樣歌詞會整段整段跳、不會逐句走。
拿到之後一定要在步驟 4 的網頁版驗收。 唱歌的咬字和拖拍跟講話不一樣, 模型抓的時間點常常偏早或偏晚一點,尤其是前奏後的第一句和轉折句。 偏移不大的話直接在歌詞檔上手改幾句最快。
方法 B:手動敲拍對時(方法 A 不能用時的備案)
python3 <SKILL_DIR>/scripts/timetap.py <歌曲資料夾>
會產生「敲拍對時.html」。主動幫使用者打開它,並說明:
播放後每唱到一句新歌詞就敲一下空白鍵,敲錯按 Backspace,
標完按「匯出歌詞檔」。然後把下載到的檔案放回資料夾、更新 project.json 的 lyrics。
這條路要使用者從頭聽完整首歌,4 分鐘的歌大約花 5 分鐘,但時間點是他自己敲的、最準。
步驟 3:準備(自動調校 + 挑主色)
python3 <SKILL_DIR>/scripts/prepare.py <歌曲資料夾>
它會自動算好三件事並寫回 project.json:
- 字級 —— 保證最長那句放大後也不會被切掉
- 背景遮罩濃度 —— 依封面亮度反推,讓白色歌詞有足夠對比
- 主色 —— 抽 3 個候選色,畫成對照圖
_主色候選.png
一定要把 _主色候選.png 打開給使用者看,讓他挑一個(用 AskUserQuestion,把三個顏色和說明列出來)。
選好之後:
python3 <SKILL_DIR>/scripts/prepare.py <歌曲資料夾> --accent 2
步驟 4:先出網頁版給使用者確認
python3 <SKILL_DIR>/scripts/build_html.py <歌曲資料夾>
產出後主動打開給使用者看(explorer.exe / open / xdg-open),並請他確認:
- 按播放有沒有聲音?碟片有沒有轉?暫停時碟片會停嗎?
- 拖進度條,歌詞和頻譜會不會一起跟著跳?
- 金色高亮跟唱的時間對不對得準?
- 轉速、字級、顏色、背景亮度喜不喜歡?
要改的話改 project.json 的 overrides(見下方「常見調整」),重跑 build_html.py。
預設會直接覆蓋舊檔。使用者已經點頭的版本要留著,就用 --out 另存,
免得後面越調越糟卻回不去:
python3 <SKILL_DIR>/scripts/build_html.py <歌曲資料夾> --out musicdisk_v2.html
render_video.py 也吃同一個參數。兩支都是「只給檔名就寫進歌曲資料夾」。
步驟 5:使用者滿意後才出影片
先出 30 秒試片,挑一段換句多的:
python3 <SKILL_DIR>/scripts/render_video.py <歌曲資料夾> --start 88 --end 118
試片確認後才跑全長。全長要用背景執行(4 分鐘的歌約 13 分鐘):
python3 <SKILL_DIR>/scripts/render_video.py <歌曲資料夾>
完成後自我檢查:時長對不對、有沒有聲軌、抽幾格看歌詞高亮正不正確, 然後打開給使用者看。
常見調整
改 project.json 的 overrides,重跑 build_html.py / render_video.py 即可。
網頁和影片讀同一份設定,改一次兩邊都會變。
"overrides": {
"disc": { "period": 16.0 }, 比 12 大 = 轉得慢
"colors": { "accent": "#7FD4FF" }, 主色
"lyrics": { "size": 34, "zoom": 1.24 }, 字級、當前句放大倍率
"background": { "mask_base": 0.62, "mask_side": 0.5 } 背景壓暗程度(0~1)
"video_progress": null 影片不要底部那條進度線
}
想動版面座標(碟片位置、歌詞欄大小)就改 layouts/*.json。
16x9.json 是手寫的,其餘五套由 layouts/_generate.py 產生 ——
要改那五套請改 _generate.py 再重跑它,不要直接改產生出來的 JSON。
改完程式一定要跑回歸測試
第一次用要先建立自己的基準(repo 裡不附,見下方說明):
python3 <SKILL_DIR>/tests/regress.py --bless
之後每次改完程式跑這個比對:
python3 <SKILL_DIR>/tests/regress.py
用合成的測試素材把六套版面各畫 12 格,跟基準逐像素比對(比的是整張圖的 SHA)。
刻意改了外觀而且確認新的比較好,才用 --bless 重新定基準。
為什麼基準不附在 repo 裡? 因為比對方式是「整張圖的 SHA 完全相等」,只要 Pillow 版本、系統字型、 Chrome 版本任一不同,算出來的像素就會有微小差異,別台機器的基準一定全部對不上。 基準的用途是「抓自己這次改壞了什麼」,本來就該在自己的環境產生。
設計原則(改東西之前先讀這段)
1. 所有動畫都是「時間 t 的函數」 碟片角度、歌詞位置、哪句金色、柱子多高,全部只依賴 t,不依賴「上一格畫了什麼」。 這是拖進度條、暫停、跳轉不會亂掉的原因,也是影片能單獨渲染任何一格的原因。 不要引進任何會累加的狀態。
2. 版面數字只有一個來源
全部在 layouts/*.json。網頁的 CSS 和影片的 Python 都從那裡讀。
以前兩邊各寫一份,改一邊忘另一邊就會不一致 —— 不要走回頭路。
3. 碟片中心不能蓋白圓 要用遮罩挖出「真正透明」的孔(看得到底下的背景),外面一圈只留 14% 模擬透明塑膠。 蓋白圓中間會死白一片,那是最明顯的廉價感來源。
4. 背景保持清晰,不要模糊 讀不清楚就把遮罩壓深,不要用模糊解決 —— 模糊會讓整體質感掉一階。
踩過的坑(別再踩一次)
| 症狀 | 原因 |
|---|---|
| 圖片全黑 | 大圖不能放進 CSS 變數再引用,要直接寫在規則裡 |
| 長句右半邊被切掉 | 忘了「放大倍率」也會增加寬度。autotune 已自動處理 |
| 影片顏色比網頁暗 | 兩層都半透明時,合成後要除以總透明度,否則等於乘兩遍 |
| 碟片蓋在封面前面 | 網頁靠 DOM 順序,Python 要自己排:背景→碟片→封面→歌詞→頻譜 |
| 渲染到最後卡住不動 | ffmpeg 的訊息不能導到管線(只有 64KB,塞爆會死結),要導到檔案 |
| 歌詞位置偶爾整批偏掉 | 不要去量 DOM 高度(字型晚載入會量錯),用公式算 + 禁止折行 |
| 同一份檔案截圖每次不同 | will-change:transform 會讓次像素對齊飄動,拿掉 |