# Music Disc Video

> 把一首歌做成旋轉碟片配同步滾動歌詞的視覺：輸出可雙擊播放的單一 HTML （圖與音樂內嵌）和／或 MP4 影片。六套版面：16:9、9:16、1:1， 各有含歌詞與純音樂兩種。素材需要封面圖（正方形）+ mp3 + 歌詞 （.srt / .lrc / 純文字皆可，沒有時間軸會產生對時工具）。 當使用者想把歌曲做成可分享的視覺、歌詞影片或碟片動畫時使用。 觸發語：把這首歌做成有歌詞的影片、音樂碟片、旋轉唱片 歌詞、歌詞影片、 做一個音樂視覺化、把 mp3 做成影片、IG 限動音樂動畫、短影音 音樂 歌詞、 lyric video、music disc video、music visualizer。

- Skill: `unbias38/music-disc-video` (Agent Skill, multi-file: 28 files)
- Install (CLI): `npx skillmds@latest add unbias38/music-disc-video`
- Raw SKILL.md: https://api.skillmd.com/api/skills/unbias38/music-disc-video/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: unbias38 (https://skillmd.com/u/unbias38)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/unbias38/music-disc-video

---


# music-disc-video：一首歌 → 旋轉碟片 + 滾動歌詞

把「封面圖 + 音樂 + 歌詞」做成：

- **單一 HTML 檔** —— 雙擊就能播，圖跟音樂都內嵌，複製到任何電腦都不會破
- **MP4 影片** —— 1920×1080 / 1080×1920 / 1080×1080，可直接上傳社群

---

## 第一次使用先做這個

```bash
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`：

```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](https://aistudio.google.com/) 上傳這首歌的音檔或影片，送出這段 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 不能用時的備案）

```bash
python3 <SKILL_DIR>/scripts/timetap.py <歌曲資料夾>
```

會產生「敲拍對時.html」。**主動幫使用者打開它**，並說明：
播放後每唱到一句新歌詞就敲一下空白鍵，敲錯按 Backspace，
標完按「匯出歌詞檔」。然後把下載到的檔案放回資料夾、更新 `project.json` 的 `lyrics`。

這條路要使用者從頭聽完整首歌，4 分鐘的歌大約花 5 分鐘，但時間點是他自己敲的、最準。

### 步驟 3：準備（自動調校 + 挑主色）

```bash
python3 <SKILL_DIR>/scripts/prepare.py <歌曲資料夾>
```

它會自動算好三件事並寫回 `project.json`：

- **字級** —— 保證最長那句放大後也不會被切掉
- **背景遮罩濃度** —— 依封面亮度反推，讓白色歌詞有足夠對比
- **主色** —— 抽 3 個候選色，畫成對照圖 `_主色候選.png`

**一定要把 `_主色候選.png` 打開給使用者看，讓他挑一個**（用 AskUserQuestion，把三個顏色和說明列出來）。
選好之後：

```bash
python3 <SKILL_DIR>/scripts/prepare.py <歌曲資料夾> --accent 2
```

### 步驟 4：先出網頁版給使用者確認

```bash
python3 <SKILL_DIR>/scripts/build_html.py <歌曲資料夾>
```

**產出後主動打開給使用者看**（`explorer.exe` / `open` / `xdg-open`），並請他確認：

1. 按播放有沒有聲音？碟片有沒有轉？暫停時碟片會停嗎？
2. 拖進度條，歌詞和頻譜會不會一起跟著跳？
3. 金色高亮跟唱的時間對不對得準？
4. 轉速、字級、顏色、背景亮度喜不喜歡？

要改的話改 `project.json` 的 `overrides`（見下方「常見調整」），重跑 build_html.py。

預設會直接覆蓋舊檔。**使用者已經點頭的版本要留著，就用 `--out` 另存**，
免得後面越調越糟卻回不去：

```bash
python3 <SKILL_DIR>/scripts/build_html.py <歌曲資料夾> --out musicdisk_v2.html
```

`render_video.py` 也吃同一個參數。兩支都是「只給檔名就寫進歌曲資料夾」。

### 步驟 5：使用者滿意後才出影片

**先出 30 秒試片**，挑一段換句多的：

```bash
python3 <SKILL_DIR>/scripts/render_video.py <歌曲資料夾> --start 88 --end 118
```

試片確認後才跑全長。**全長要用背景執行**（4 分鐘的歌約 13 分鐘）：

```bash
python3 <SKILL_DIR>/scripts/render_video.py <歌曲資料夾>
```

完成後自我檢查：時長對不對、有沒有聲軌、抽幾格看歌詞高亮正不正確，
然後打開給使用者看。

---

## 常見調整

改 `project.json` 的 `overrides`，重跑 build_html.py / render_video.py 即可。
**網頁和影片讀同一份設定，改一次兩邊都會變。**

```json
"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 裡不附，見下方說明）：

```bash
python3 <SKILL_DIR>/tests/regress.py --bless
```

之後每次改完程式跑這個比對：

```bash
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` 會讓次像素對齊飄動，拿掉 |

