十天前我第一次讓 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 上幾支結構很清楚的知識片,把片型固定下來:

  1. 冷開場用一個真實案例,接一張 4 秒片名卡,片名用論點短句
  2. 中段有一個元素貫穿全片,後幕一開始就把它畫好
  3. 關鍵數字做成大字 hero number,一幕最多兩個
  4. 結尾:編號回顧 → 一組對句 → 片名卡,最後 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 / check0 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 的動作手感一致。

這些檢查都交給腳本之後,剩下要人決定的是:這支片要講什麼、哪一個數字值得放大、哪一幕該拆成兩頁。片尾那組對句寫的就是這件事。