十天前我第一次讓 Claude Code 做影片,目標很單純:把一個程式 repo 做成一支一分多鐘的介紹片,拿去給同事看。
十天後回頭數,影片工作區裡有 21 支片(含同一支片的改版),加起來大約 50 分鐘。中間做出來的東西整理成一個叫 repo-video 的 skill,前後改了 6 次 commit,scripts/ 底下長到 20 支腳本。
這篇記錄它怎麼一路長成現在這樣。文章最後那支影片,是用 /repo-video 介紹它自己。
影片是寫出來的
先講底層。「讓 LLM 做影片」聽起來像是模型直接生成畫面,實際上主流做法都是同一條路:
寫動畫程式碼(HTML / React / p5.js)
→ headless Chrome 逐格截圖(一秒 30 張)
→ ffmpeg 把截圖接成 MP4
我比較過三個選項:手繪卡通風的 ClaudeAnimationBase、React 系的 Remotion,以及 HeyGen 開源的 HyperFrames(HTML + GSAP)。介紹片的內容多半是文字、程式碼、圖表,用 HTML 最順手,就選了 HyperFrames。本體和本機 render 都免費,只有旁白、配樂這類素材走 HeyGen 才會扣額度。
這個做法有個好處後來越來越明顯:影片既然是程式碼,就能 lint、能 check、能 diff。後面大部分的演進,都是在這個基礎上加檢查。
HyperFrames 的 plugin 有一個要注意的地方:它的入口 skill 自稱是影片、動畫「和簡報 deck」的強制入口。裝在 user scope 會搶走我原本做簡報的 skill,所以我把它關掉,只在影片工作區用 project scope 開啟。
repo-video 只固定三件事
HyperFrames 本身已經有完整的流程(brief → storyboard → 每幕一個 frame → 組裝 → render)。repo-video 沒有重寫這些,只固定三件事:
| 項目 | 做法 |
|---|---|
| 影片放哪 | 一律放在獨立的影片工作區,程式 repo 只當唯讀資料來源,不放任何影片檔 |
| 中文字型 | 只打包文案實際用到的字(Noto TC 子集),render 前重算 |
| 派 worker | 分鏡寫好後,每幕派一個 subagent,全部同時開工 |
第一支片 9 幕、74 秒、無旁白無配樂。9 個 subagent 平行做,本機 render 約 45 秒。
這裡第一個坑:HyperFrames 把每幕的規格切成 frame packet 交給 worker,但 packet 不包含分鏡最上面的整體方向(配色、字型、共用圖表的座標)。worker 被規定不能讀整份分鏡,所以 9 幕各做各的。解法是把整體方向那一段附加到每個 worker 都會讀的 frame.md 尾端。
演進時間線
| 日期 | 加了什麼 | 為什麼 |
|---|---|---|
| 10/04 | 中文字型子集、句子級資料片、片型骨架 | 第一支片的字型坑;第二支片定下風格 |
| 10/05 | 卡拍一鍵化、Frame 0 片名卡 | 換句沒對到拍點;聊天軟體預覽圖是黑的 |
| 10/10 | 驗收三件套;拿掉音效 | 片子變多,靠肉眼抽格不夠 |
| 10/12 | 旁白版 | 有些片要配音 |
| 10/13 | 動作規格 | 9 個 worker 各挑各的 easing |
10/04:中文字會悄悄換字型
render 用的是乾淨的 headless Chrome,沒有任何系統 CJK 字型。所以要用 Google Fonts 的 text= 參數,只下載文案用到的字。第一支片是 266 個字,每個字型檔 45–58KB。
問題在之後改文案。新出現的字不在子集裡,瀏覽器會改用別的字型,畫面看得出來不一致,但 lint、check、render 都沒有任何錯誤訊息。規則因此寫成:改完中文、render 之前,一律重跑 fonts.py。✓、→、≈ 這類符號也不在字型裡,要畫成 inline SVG。
10/04:句子級資料片
第二支片定下了後來所有片的風格,參考的是小紅書上一系列動態圖表知識片:
- 畫面上方一次只有一句話,三到四秒換一句,關鍵詞上金色或紅色
- 下方是一直存在的資料動畫,跟著每一句改變狀態
- 數字一律來自真的跑出來的資料
最後一條影響最大。那支片介紹的是一個回測平台,我讓 agent 在本機實際跑了四組回測(理想化、加交易成本、加滑價、加成交量上限),畫面上的權益曲線就是那四條。理想化的報酬加完摩擦之後幾乎歸零,這句話本身就是整支片的故事,不需要另外編。
做到第五支片時另一個 agent 提醒了一件事:「repo 裡有多少個排程設定檔」不等於「每天有多少排程在跑」。之後的規則是設定面的數字在句子裡寫「設定」,講「每天在跑」就去查實際執行紀錄。
10/04:片型骨架與字在抖
做一支 SRE agent 的介紹片時,參考 YouTube 上幾支結構很清楚的知識片,把片型固定下來:
- 冷開場用一個真實案例,接一張 4 秒片名卡,片名用論點短句
- 中段有一個元素貫穿全片,後幕一開始就把它畫好
- 關鍵數字做成大字 hero number,一幕最多兩個
- 結尾:編號回顧 → 一組對句 → 片名卡,最後 3 秒完全靜止
同一支片還抓到一個怪問題:字看起來在抖。原因是有幾幕做了整個畫面的慢速推鏡(scale 1 → 1.03,拉 8–14 秒),字被持續縮放,每一格都重新貼齊像素。量了一下,推鏡的幕有 40–78% 的格數在變,沒推鏡的幕約 10%。拿掉之後就穩了。
10/05:卡拍
配樂用 Pixabay 上一首 88 BPM 的輕電音(Pixabay Content License,可商用、免標註),之後每支片都用同一首,不再每次選曲。
卡拍的做法是:用 spectral flux 算出拍點和小節線的相位,再把動畫時間做分段重映射——幕首吸到小節線、每次換句吸到拍點,動畫碼本身不動。
第一版量出來換句中位數晚了 0.10 秒。原因是換句是淡出再淡入,視覺上的「打點」比錨點晚大約 3 格。把拍點相位提前 0.1 秒後,中位數歸零;整個流程收成一個指令 beat-sync.sh 之後,實測換句偏差中位數 0.006 秒。
一鍵化的過程也有坑:每一幕各自吸最近的小節線時,相位差 0.01 秒就讓 4 秒的片名卡縮成 2.7 秒。改成把長度誤差往後擴散到整小節之後才穩定。
同一天加了 Frame 0:影片第 0 格就是完整的片名卡。原本片子丟到聊天軟體,預覽圖是一片黑。
10/10:驗收三件套
到這時已經做了十幾支片,每次靠抽幾格截圖檢查不夠用了。加了三樣東西:
| 工具 | 檢查什麼 |
|---|---|
check --at-transitions | 不只在每幕中點取樣,也在每個 tween 的邊界取樣,抓文字被遮住、對比不足 |
index.motion.json 動作斷言 | 每一句是否準時出現、順序對不對、有沒有跑出畫面 |
| beat sheet | 每次換句後 0.6 秒抽一格,拼成總表,交付前逐張看 |
--at-transitions 第一次就在一支已經交付過的片抓到 17 處文字被遮住:長條圖升起時,後面淡化的圖表軸標從縫隙透出來。預設的 check 是 0 error。修完後同一支片 86 條動作斷言全過。
beat sheet 抓到過 check 和 lint 都抓不到的問題:有個 worker 寫句子的 class 時漏了 position:absolute,五句照文件流一句比一句低。這種東西只有用眼睛看總表才看得出來。
試過、又拿掉的
不是每個想法都留下來了。
- 音效:在配樂上疊程式合成的音效。第一版跟配樂打架,查下來是有 5/8 的音效落在兩拍中間。改成上拍、調進配樂的 D 大調、加 sidechain 讓配樂讓位之後,聽起來只剩「配樂變小聲」。結論:有配樂就不加音效。
- 擬真布景:用生圖做一張拉絲金屬背景,被評為不適合;改成全片只用一種胡桃木桌布景。
- 偵探片:試了「提出假設 → 推翻 → 找線索 → 破案」的片型,方向不錯,但每個嫌疑的證據太少,暫停了。
這些都寫進 skill,下一支片不用再試一次。
10/12:旁白版
有些片需要配音。設計上句子帶就是字幕也就是旁白:念的就是畫面上那句話,一句一個音檔,句子的時間直接由音檔長度決定,不需要語音轉文字對齊。
- 預設是自己錄。
record_lines.sh會逐句提詞,Enter 開始、Enter 停止,可以重錄單句。 - TTS 是備選(edge-tts 的台灣口音),但稿子會送到外部服務,公司內容要先確認。
scan_heteronyms.py掃多音字(「調倉」「行情」「重跑」這類),發音只能靠人耳確認。
寫稿也要改:念起來大約每秒 3.5 個字,原本三到四秒一句的片子加旁白通常會變長 1.5 倍,所以要先砍稿,再談拉長。
10/13:動作規格
9 個 worker 平行做,沒有共用規格就各挑各的動畫曲線。數了一下,一支 9 幕的片用了 12 種 easing,單幕最多 9 種,看起來每一幕的「手感」都不一樣。
參考 LottieFiles 的 motion-design-skill,分鏡最上面固定寫四行:
- motion: premium
- motion-eases: power3.out, power2.in, power2.inOut, sine.inOut
- motion-durations: 0.3 / 0.4 / 0.9
- motion-entrance: opacity 0→1 + y 12→0, 0.4s
再加一支 motion_audit.py 檢查各幕用的 easing 是否在白名單內。重派之後,白名單外的 easing 從 57 處降到 0,單幕不超過 4 種。
沒寫進流程圖的幾件事
有些教訓不屬於哪個功能,但每一條都花過時間:
- 一頁只講一件事:有一幕把兩個不同的改版前後對照放在同一頁,同事看不懂,以為是同一個比較。拆成兩頁就好了。
- 數字要用上線的版本:有一次影片裡的對照數字用了一個從未上線的中間版本,是另一個 agent 抓出來的。
- GSAP 會把空白字串當成 0:打字機效果逐字 set
textContent,遇到空格時 GSAP 把" "解析成數字 0,成片的空格全變成「0」。只有從 MP4 抽格才看得到。 - 切換顯示用 opacity:用
visibility時,子元素 inline 的visible會蓋過外層隱藏,在別幕的轉場期間閃一下。 - 讀別人的 repo 用 worktree:有一次為了取資料,在來源 repo 的主目錄切了分支。之後一律開 detached worktree。
這支片怎麼做的
下面這支片是用 /repo-video 做的,來源是 skill 自己的 SKILL.md、git log 和這十天的工作紀錄。11 幕、10 個 worker 平行製作,配樂同一首,無旁白。
| 項目 | 結果 |
|---|---|
| 長度 | 191.7 秒,1920×1080,15.4MB |
| lint / check | 0 error;check --at-transitions 通過,88 條動作斷言 |
| 動作規格 | 白名單外 easing 0 處 |
| 卡拍 | 29 次換句, |
做這支片的過程也踩到新的坑,照例寫回 skill:
- 畫面上的程式碼被當成真的:第 2 幕的程式碼卡片秀出
window.__timelines["02-html"] = tl;這行字,卡拍腳本用 regex 找 timeline 註冊,結果找到兩處而中止。畫面上的引號改成"就好。 - 示範「字型被換掉」反而會出事:第 3 幕要示範中文 fallback,worker 原本寫
font-family: serif。可是 render 機根本沒有系統中文字型,那一格會變成空框。worker 自己發現並提出來,改用打包好的另一套 Noto 字型示範。 - 第 1 次 check 沒過:大數字跟說明文字重疊、清單被暗角圖層蓋住、片尾靜止 5.1 秒超過上限。三幕各自交回原本的 worker 修,第 2 次通過。
- 數字差一:10/13 那天原本的 3 支片不包含這支,第 7 幕卻把其中一格當成「這支片」。改成多疊出第 4 格。
結語
回頭看,這十天大部分的工作都在補「驗收」。一開始寫出一支能看的片很快,難的是第十支片還能保持同樣的水準:字型不悄悄換掉、換句對到拍上、文字不被遮住、9 個 worker 的動作手感一致。
這些檢查都交給腳本之後,剩下要人決定的是:這支片要講什麼、哪一個數字值得放大、哪一幕該拆成兩頁。片尾那組對句寫的就是這件事。
