建構主義解說影片 · 工作流
一條時間主軸,三個下游全部從它派生。
① 文稿 ② TTS 定軸 ③ BGM 變奏 ④ 成片
文字 ──→ 解說音軌(時間主軸)──→ LMMS 音樂(層次)──→ Remotion 畫面 + 音軌整合 + 封面
│ ▲
└──── 層次邊界時間點 ─────┘
- 畫面幀數由 TTS 的自然語速長出(不是反過來壓縮語速)
- BGM 的變奏點卡在解說的層次切換點上
- 成片以解說音軌為基準對齊,BGM 只做襯底
0. 起步:先讀路徑契約(這一步不能跳)
本技能不自己找工具,一切路徑來自環境配置技能寫下的契約:
$VW_CONTRACT (預設 ~/.config/ideaform/toolchain.json)
CONTRACT="${VW_CONTRACT:-$HOME/.config/ideaform/toolchain.json}"
source "$(dirname "$CONTRACT")/env.sh" # 匯出 VW_NODE / VW_VENV / VW_FFMPEG / …
開工前必查三件事:
- 契約檔案存在嗎?不存在 → 讓用戶先跑
ideaform-setup技能。 schema_version是本技能認識的版本嗎?不符 → 要求重跑環境配置技能。verified_at是null嗎?是 → 警告用戶「環境未通過冒煙校驗」,先修再幹。 另外tools.lmms.stkdir_ok必須是true,否則 BGM 會全程靜音且不報錯。
關鍵欄位速查:
| 用途 | 契約欄位 |
|---|---|
| 跑 TTS 腳本(edge-tts 在 venv 裡) | tools.venv.python |
| 合成音影片 | tools.ffmpeg.path / tools.ffprobe.path |
| 渲畫面 | tools.remotion.project / tools.remotion.browser.resolved |
| 渲 BGM | tools.lmms.apprun |
| 落暫存檔案(必須是大碟) | tmpdir |
⚠️ 契約裡的
tmpdir不是可選項。 本機/tmp只有 10MB,Remotion 打包 / pip 裝包 都會寫它 →ENOSPC。每條 Remotion 指令都要帶TMPDIR。
⚠️ 路徑一律不做
resolve(軟鏈解析)。 託管工具鏈路徑裡含軟鏈, 解析 venv 直譯器會丟掉 venv 上下文,套件全部 import 不到。見倉庫根docs/03-路徑契約.md。
1. 四階段總覽
| 階段 | 幹什麼 | 入口 | 出口(都落到工作空間) | 參考 |
|---|---|---|---|---|
| ① 文稿 | 把使用者的輸入變成分層的解說稿 | 使用者輸入 | script.md + 台詞.txt(分層標好) | references/stage1-script.md |
| ② TTS 定軸 | 量自然語速 → 排時間軸 → 出配音音軌 | 台詞.txt | plan.json / timeline.ts / voice.wav | references/stage2-tts.md |
| ③ BGM 變奏 | 層次邊界 → 變奏計劃 → LMMS 出音軌 | plan.json + 層次標注 | bgm-plan.json / bgm.wav | references/stage3-bgm.md |
| ④ 成片 | Remotion 排畫面 + 整合音軌 + 渲染 + 封面 | timeline.ts + 音軌 + bgm-plan.json | final.mp4 + cover.png | references/stage4-render.md · references/cover.md |
風格統一由 references/constructivist-style.md 管,
畫面與封面共用同一套調色盤 / 字型 / 柵格,所以天然一致。
2. 階段一 · 文稿(分層是硬要求)
使用者給的是輸入(一個主題 / 一段材料 / 口頭想法)。產出是:
script.md—— 完整文稿,顯式標出論述層次台詞.txt—— 一行一句的口播台詞,供 TTS 消費
分層不是為了好看,是階段三的輸入。 沒有層次標注,BGM 變奏就沒有著力點。
## 層次 1 · 引入
這是《文稿即時間軸》的第一層。
解說音軌才是整條流水線的時間主軸。
## 層次 2 · 展開
畫面幀數由它決定,不是反過來。
...
- 一句 = 一個語義單元,別把兩件事塞進一句(TTS 按句排軸,塞了會拖長槽位)
- 單句建議 8–30 字;過長拆句,過短合句
- 標點用句號/問號收尾,幫助 TTS 斷句
細節見 references/stage1-script.md。
3. 階段二 · TTS 定軸(順序鐵律)
"$VW_VENV/bin/python" skills/ideaform/scripts/make-tts-track.py \
--lines 台詞.txt --fps 30 --audio \
--plan-out out/plan.json --ts-out out/timeline.ts
先配音、後定軸。 時間軸從 TTS 的真實語速裡長出來:
① 寫台詞
② make-tts-track.py --lines 台詞.txt --fps 30 --audio ← 量自然語速,排出時間軸
③ 拿輸出的 TOTAL_FRAMES 去寫 Remotion composition ← 幀數由此確定
④ 再渲染
反例代價(實測):先按分鏡寫死 43 條時間碼,TTS 自然語速合計 212.90s, 而影片只有 174.80s → 被迫壓縮 1.218x。這妥協本來完全可以避免。
同一個腳本也能做反過來的事(已有 SRT、時間碼定死時用兩段式調速適配),
但那是補救不是首選。詳見 references/stage2-tts.md。
⚠️ 禁止向使用者索取「成片多少分鐘」。 時長 = 語速 × 台詞量,是輸出,不是輸入; 開工時既不該問、也不該給「標準版 X 分鐘」這類選項。只報「按這份文稿,實測 X 分 Y 秒」。 使用者要更長/更短 → 改的是文稿,不是時間。(2026-10-15 桂花片踩過,記 E16)
出口:plan.json 裡每條有 start/end/dur/frameStart/frameEnd;
timeline.ts 可直接 cp 進 Remotion 工程當 src/timeline.ts。
4. 階段三 · BGM 變奏(本工作流的靈魂)
要點:論述每進入一個新層次,音樂換一次型態 —— 但不換曲子。
# ① 把文稿的層次落到 cue 號上,標出每層的敘事功能
"$VW_VENV/bin/python" skills/ideaform/scripts/bgm-plan.py \
--plan out/plan.json \
--layers "1:present,4:develop,6:resolve" \
--key "A minor" --out out/bgm-plan.json
# ② 按 bgm-plan.json 生成 LMMS 工程並渲染(走 electronic-music-production 的 composer.py)
# 契約裡的 tools.lmms.apprun 渲染;bpm 與 timeline lp1pos 必須按 mmp_hints 寫
六個敘事功能(每層選一個):
| 功能 | 意圖 | 變了什麼 |
|---|---|---|
present | 呈現·引入 | 只露動機、單聲部、留白 |
develop | 展開·深入 | +bass +律動,動機模進 |
intensify | 強調·衝突 | 全聲部、升八度、借用和弦、力度抬檔 |
contrast | 對照·轉折 | 換質地不加音量:撤層、換音色、轉調式 |
resolve | 收束·回歸 | 做減法、回主調、動機增值 |
sustain | 持續·過渡 | 不動(層次未變時別亂變奏,會稀釋對比) |
鐵律:
- 身份要素不許變 —— 動機細胞、調中心、基本速度。變了就是切歌,不是變奏。
- 每段至少變 1 項型態要素;
bgm-plan.md裡會列出每段的「變奏證據」, 相鄰兩段若證據為空,說明層次標注漏了。 - 變奏點必須落在解說的層次邊界上(可用
--lead-in讓音樂略提前)。
完整方法與 LMMS 側落地見 references/stage3-bgm.md。
5. 階段四 · 成片
5.1 畫面:以解說音軌為時間主軸
cp out/timeline.ts <remotion項目>/src/timeline.ts # 幀數/幕邊界都從 CUES 推導
cd <remotion項目>
export TMPDIR="$VW_TMPDIR" # ★ 必須
npx remotion studio # 先給使用者看預覽
畫面側的幕邊界不要手寫幀號,從 CUES 推導:
export const SCENES = {
s1: { from: 0, dur: CUES[2].start },
s2: { from: CUES[2].start, dur: CUES[4].start - CUES[2].start },
} as const;
結構式 / 訊號通路 / 機理圖 → 一律確定性生成,絕不用 AI 出圖。
AI 畫化學結構必然幻構鍵,而且看不出錯——這類素材必須可複現、可校驗。
用 scripts/gen-structures.py:SMILES → RDKit 確定性描繪
→ 幀驅動的 Remotion 元件。
python3 scripts/gen-structures.py --tsv structures.tsv --out structures
# → svg/*.svg + components/*.tsx(自帶動畫) + manifest.json
# → 分子式離線校驗(RDKit CalcMolFormula),不符則非零退出,可直接串 `&&`
- 配色走 RDKit 官方 options(
useBWAtomPalette/singleColourBonds/setSymbolColour), 不要正則後處理 SVG 的顏色——那是重複造輪子且隨 RDKit 改版就碎(見錯題本 E20)。 - 生成的元件靠 CSS 變量
--mol-dash/--mol-text驅動描線動畫,外層必須給color。 - 需要網格圖 / 骨架高亮 / 3D 構象 / 蛋白-配體相互作用圖等更廣的分子視覺化, 先去找現成 skill(見 §7),別在影片流水線裡硬塞。
5.2 音軌整合(兩條路線,預設 A)
A. Remotion 整合(推薦) —— composition 裡用 <Audio>,渲出的 mp4 自帶音軌:
<Audio src={staticFile("voice.wav")} />
<Audio src={staticFile("bgm.wav")} volume={0.1694} />
volume 那個數不要拍腦袋,用實測 RMS 反推:
bash scripts/mix-av.sh --plan-only --voice voice.wav --bgm bgm.wav --drop 9
# 解說 RMS = -23.46 dBFS BGM RMS = -17.04 dBFS
# → BGM 需加 -15.42 dB → volume = 0.1694
B. 後合成(影片已渲好 / 要重配音):
bash scripts/mix-av.sh --video silent.mp4 --voice voice.wav --bgm bgm.wav --out final.mp4
⚠️ 路線 B 必須顯式
-map 0:v:0 -map 1:a:0。 Remotion 輸出的 mp4 自帶一條靜音 AAC 軌, 不加-map時 ffmpeg 會自動選中它 → 成品退出碼 0、時長正確、能播放、就是沒聲音。mix-av.sh已內建這個-map。
5.3 渲染與封面
export TMPDIR="$VW_TMPDIR"
npx remotion render <CompositionId> out/video.mp4 --concurrency=3
npx remotion still Cover out/cover.png --props='{...}' # 封面:靜態 composition,勿用動畫
- 長片分段渲染:7 分鐘片單次渲染會因記憶體被打爆而逾時。
--frames=A-B分段 →concat -c copy無損拼。 - 封面用
durationInFrames={1}的靜態 composition,元件內不要用useCurrentFrame(still 時恆為 0,動畫初值會讓元素不可見)。 - 封面版式見
references/cover.md,風格規範見references/constructivist-style.md。
6. 無目視交付:每個階段都要過校驗器
模型看不到影像、聽不到聲音。 所以「渲染成功」≠「畫面對、聲音對」。 每一步都用數值實測,不過關就拒絕交付:
# 音訊自檢(TTS 音軌、BGM):峰值/RMS/削波/時長偏差
# 畫面自檢:像素對色 / 走廊純淨度 / 文字像素計數
# --layout 必選對:cover=封面板式;scene=影片場景(預設)。
# 取樣帶按版式定,用錯了正確封面也會被判失敗(見 05-故障排查 D2)。
python3 scripts/verify-video.py cover out/cover.png --layout cover
# 成片自檢:幀數 / 時長 / 音軌碼率(防靜音軌)
# ffprobe 定位順序:VW_FFPROBE → 路徑契約 toolchain.json → 系統
VW_FFPROBE="$VW_FFPROBE" python3 scripts/verify-video.py video out/final.mp4 --frames 732 --fps 30
腳本以非零退出碼表示失敗,可直接串進 && 流水線。
常用判據速查:
| 對象 | 判據 | 閾值 |
|---|---|---|
| 音訊 | 非靜音 | 峰值 > 1000 且 RMS > 100 |
| 音訊 | 無削波 | 滿刻度樣本 < 0.01% |
| 音訊 | 時長對齊 | 與規劃偏差 < 50 ms |
| 封面 | 尺寸 | 1920×1080 |
| 封面 | 文字渲染 | 文字區墨色像素 > 300 |
| 封面 | 軸線不穿字 | 標題區內紅色像素 < 60 |
| 成片 | 幀數 | 嚴格等於規劃值 |
| 成片 | 音軌非靜音 | 碼率 > 64000 bps |
| BGM | 襯底電平 | 比旁白低 8–12 dB |
7. 與本技能配合的其它能力
| 需要什麼 | 用哪個 |
|---|---|
| 環境探測 / 補裝 / 契約 | ideaform-setup(一次性) |
| 新開源工具的現成 skill(拿來主義:GitHub 收割 → 安檢 → 落盤) | ideaform-setup 的 scripts/harvest-skills.py <工具名> |
| 化學結構式 / 通路圖的確定性圖形 | 本技能的 scripts/gen-structures.py(RDKit → SVG → Remotion 元件) |
| 更廣的分子視覺化(網格圖 / 骨架高亮 / 3D 構象 / 蛋白-配體) | 外部現成 skill:synthetic-sciences/openscience 的 molecule-visualization(Apache-2.0)、ghutchis/chem-skill(MIT) |
LMMS 工程格式、插件參數、eldata 包絡 | electronic-music-production + 其 references/05 |
| 作曲法(和聲、動機、無聽覺作曲、湧現) | electronic-music-production 的 references/06–10 |
| Remotion 語法與渲染細節 | remotion-best-practices |
| 像素級無目視校驗的完整方法 | headless-visual-verify |
| WSL 環境坑(/mnt、gh 抓取、音訊通路) | wsl-pitfalls |
8. 交付清單
一條影片做完,工作空間裡應該有:
<工作空間>/
├── script.md 分層文稿
├── 台詞.txt 口播台詞
├── out/
│ ├── plan.json 時間主軸(每條 cue 的秒/幀)
│ ├── timeline.ts 可直接進 Remotion 的時間軸
│ ├── voice.wav 解說音軌母版
│ ├── bgm-plan.json 變奏計劃 + bgm-plan.md(變奏證據)
│ ├── bgm.wav BGM 音軌
│ ├── video.mp4 無聲畫面(或已整合音軌的成片)
│ ├── final.mp4 ★ 最終成片
│ └── cover.png ★ 封面
└── .tts/ TTS 快取(保留,重跑只花幾秒)
交付前必須跑完第 6 節的全部校驗,並如實報告哪幾項通過、哪幾項沒測。
9. 自進化:踩坑就記帳
本工作流是可變的複合 skill——用著用著,它應該越來越貼合你的生產節奏:
- 踩了坑、修完那一刻 → 記進倉庫根
docs/06-錯題本.md(六欄格式,編號順延)。 ⚠️ 不要在skills/ideaform/下另建docs/——那是第二本帳,會與正本編號撞車(2026-10-15 踩過,記 E19)。 - 某條坑被腳本/文件永久修好(不是繞開)→ 把該條狀態改
已根治並註明修法。 - 同一類坑踩到第二次 → 別記第三條,直接把防線寫進腳本(校驗器 / 預設值 / 契約)。
- 引入新開源工具 → 先收割再動手:
harvest-skills.py <工具名>找社群成熟 skill (有分叉保護,不會覆蓋你本地改過的);找不到合格的才自己寫。收割回來的 skill 踩了坑,一樣記帳。 - 會話結束前掃一眼:有沒有「修了但沒記帳」的。