html-to-video · 程式碼逐幀做影片
這套東西是什麼
不是 AI 生成影片,是用程式碼逐幀畫影片:
寫 HTML/Canvas,把畫面寫成 frame(t) 純函數
→ 無頭瀏覽器逐幀 seek 到 t,截圖
→ ffmpeg 編成 H.264 MP4
字幕 / 運鏡 / 轉場全在程式碼裡,改程式碼重渲即可
優點:確定性(同輸入同輸出)、可控到幀、中文文字完美、零費用。 最適合做:解說動畫、動態圖解、產品演示、數據視覺化動畫、宣傳片片頭。 不適合:實拍感/氛圍鏡頭(那種走 VideoGen)。
能拿它做什麼(assets/templates/ 裡 11 個模板直接改字就能出片):
| 模板 | 用途 | 出片 |
|---|---|---|
explainer.html | 多鏡頭解說片:片頭 / 人物+氣泡 / 要點 / 條形對比 / 片尾,鏡頭帶轉場,可餵 JSON 鏡頭表;橫屏恰好三條要點時自動並排成三欄,豎屏(1080×1920)另有上下版式 | --duration=17.6 |
title-card.html | 影片片頭:主標題逐字彈入 + 副標題 | --duration=6 |
bar-chart.html | 條形圖/排名:條形依次生長,數值同步滾動 | --duration=6 |
line-chart.html | 折線趨勢:折線生長 + 數據點亮起 + 末點浮標 | --duration=8 |
number-counter.html | 大數字/KPI:從 0 滾到目標值 + 漲跌標籤 | --duration=7 |
timeline.html | 時間線/流程:中軸生長,節點逐個點亮 | --duration=7 |
vertical-card.html | 豎屏 9:16 卡片:大字標題 + 大數字,抖音/視訊號/Reels 用 | --duration=6 --width=1080 --height=1920 |
compare.html | 前後對比:左右兩欄對照,結論條收口 | --duration=7 |
flow-steps.html | 流程圖動畫:步驟依次點亮,目前步高亮 | --duration=7 |
subtitle-card.html | 逐句解說卡:一句話一句地出,底部帶進度條 | --duration=8 + --srt |
vertical-subtitle.html | 豎屏字幕卡:關鍵詞高亮,9:16 原生版式 | --duration=8 --width=1080 --height=1920 + --srt |
每個模板單檔案自包含,頂部有一個顯眼的 CFG 配置塊,改文案/數據即可 ——
不用懂 canvas 也能出片。11 個模板全部支援換主題(見「換配色」一節)。
想做別的,再看 assets/animation-skeleton.html 從零寫。
做「一支片子」而不是「一段動畫」:用 assets/lib/scenes.js 寫多鏡頭 + 轉場,
鏡頭表還能用 --story=shots.json 外置 —— LLM 寫 JSON 比寫幾百行 canvas 程式碼可靠得多。
詳見下面「多鏡頭與轉場」一節。
尺寸:模板跟視口走,不用手改
11 個模板都按 window.innerWidth/innerHeight 設畫布,所以換任何尺寸都不裁切:
# 橫屏 1080p(預設)
bash assets/render.sh title-card.html --duration=6 --fps=60
# 豎屏 9:16
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1080 --height=1920
# 720p / 4K 同理
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1280 --height=720
模板內部用三檔縮放把設計稿落到實際畫布:
| 變數 | 含義 | 1920×1080 | 1080×1920 |
|---|---|---|---|
SX = W/1920 | 橫向尺寸、左右邊距 | 1 | 0.5625 |
SY = H/1080 | 縱向尺寸、行距 | 1 | 1.7778 |
SF = √(SX·SY) | 字號、線寬、圓角、模糊 | 1 | 1 |
字號用面積等比(SF)而不是跟寬度走 —— 換成豎屏時字不會變小,只是版式鋪開。
1920×1080 下三者都等於 1,所以預設輸出與舊版逐像素一致(實測 PSNR = inf)。
自己寫的動畫也照這個來,否則改 --width/--height 會靜默裁掉右下角:
const cv = document.getElementById('stage');
cv.width = window.innerWidth; // ← 不要寫死 1920
cv.height = window.innerHeight; // ← 不要寫死 1080
渲染器現在會主動量一次畫布尺寸,比視口小就明確告警(不再靜默出半張圖)。
先裝依賴(只需一次)
渲染器需要 Node 工作區(內含 playwright + 完整版 ffmpeg)。一條命令搞定:
bash scripts/install-deps.sh # 工作區預設 ~/.workbuddy/binaries/node/workspace
bash scripts/install-deps.sh /path/to/workspace # 或自己指定
它會自動探測本機瀏覽器快取、挑匹配的 playwright 版本、裝完整版 ffmpeg,
並把工作區路徑寫進 ~/.workbuddy/.html2video-env,之後 render.sh / smoke.sh / qa.sh 開箱即用。
手動裝也行(工作區需含 playwright + @ffmpeg-installer/ffmpeg),然後自己寫配置:
# ~/.workbuddy/.html2video-env
WB_NODE_WORKSPACE="<你的 node 工作區絕對路徑>"
也可以用環境變數臨時指定:
export WB_NODE_WORKSPACE="<路徑>"。 不要把路徑寫進${VAR:-預設值}——使用者名稱若含撇號(如O'Brien), bash 會把撇號當引號開始,報bad substitution。
四條硬約束(踩過,別重犯)
-
Playwright 版本必須和瀏覽器快取匹配。
playwright-core@X只認自己在browsers.json裡宣告的 chromium build 號, 不匹配就報 "Executable doesn't exist"。→ 別猜版本,用
scripts/install-deps.sh,它會跑scripts/detect-env.js探測快取後自動挑版本。 單獨探測:node scripts/detect-env.js(輸出RECOMMEND_PW=...)。 已知映射:build 1148→1.49.1、1155→1.50.1、1169→1.52.0、1187→1.55.0、1208→1.58.0。注意:某些網路環境下 chromium 下載會反覆中斷(表現為快取目錄裡躺著半截的殘缺目錄)。 此時優先復用已有快取而非升級——這正是探測腳本存在的理由。
-
Playwright 自帶的那個 ffmpeg 沒用。
ms-playwright/ffmpeg-*/ffmpeg-win64.exe是--disable-everything編譯的, 只有 VP8 編碼 + webm 封裝,沒有 H.264、沒有 GIF、連 PNG 解碼都沒開。 必須用@ffmpeg-installer/ffmpeg的完整版。 -
ESM 不認
NODE_PATH。.mjs裡import 'playwright'必然ERR_MODULE_NOT_FOUND。 → 輔助腳本一律寫 CJS(.cjs/require)。 -
PATH 不可靠。 尤其在 Git Bash / Windows 上,塞進 PATH 的 Windows 風格路徑會被轉換搞壞。 → 別靠 PATH 傳 ffmpeg,用
FFMPEG環境變數(渲染器已支援)。
出片流程
第 0 步(最省事的路):挑一個模板改字
cp assets/templates/bar-chart.html my-video.html
# 打開 my-video.html,只改頂部 CFG 裡的 title / data / note
bash assets/render.sh my-video.html --duration=6 --fps=60
模板有字幕安全區意識:文字不會落在畫面底部 15%(燒錄字幕後會被蓋住)。 自己做版式時也照這個來。
第 1 步:寫動畫 HTML(模板不夠用時)
用 assets/animation-skeleton.html 當骨架。核心契約(渲染器靠它工作):
// 必須暴露這兩個
window.__seek = (t) => { /* 把 t 時刻的畫面完整重繪一幀 */ };
window.__ready = true; // 就緒訊號
// 渲染器會注入 window.__seekRender = true,
// 此時必須【凍結自驅時鐘】,完全由外部 __seek 推幀(保證確定性)
if (window.__seekRender) { renderAt(0); window.__ready = true; }
else { /* 瀏覽器預覽:requestAnimationFrame 自驅循環 */ }
鐵律:每一幀畫面 = 時間 t 的純函數。 不要依賴「上一幀」的狀態,
不要用 Math.random() / Date.now() / performance.now()——那會讓同輸入不同輸出,
QA 裡的「確定性」會亮紅燈,而且多 worker 並行渲染會各渲各的。
畫布就是唯一輸出:<canvas width=1920 height=1080>,CSS 定位在 left:0; top:0,
body margin:0。渲染器用 clip:{x:0,y:0,width:W,height:H} 截左上角,所以畫布必須正好鋪滿視口、不能置中。
(渲染器也支援 DOM/CSS/SVG 動畫,同樣只需暴露 __seek——但注意:DOM 動畫每幀重排較慢,
純 canvas 通常更穩更快。)
禁止使用的 class 名(渲染器注入的 CSS 會無條件隱藏它們,元素會憑空消失):
.no-record .progress .progress-bar .counter .tCur .phases .phase-label .phase .replay .masthead .kicker .title .footer
另外別給 position:fixed/sticky 的元素加 <button>,會被啟發式規則當成播放控制項隱藏。
(想讓某塊出現在成片裡,給它加 data-record-keep="true" 就能豁免啟發式隱藏。)
中文字型:'"Microsoft YaHei", "PingFang SC", system-ui, sans-serif';等寬用 'ui-monospace, Consolas, monospace'。
第 2 步:冒煙測試(別跳)
先只截幾幀,確認契約就緒、沒控制檯報錯、畫面沒糊,再跑全量:
bash assets/smoke.sh animation.html
# SMOKE_T="0,1,2" SMOKE_W=1280 SMOKE_H=720 bash assets/smoke.sh animation.html
更快的迭代方式:只渲一小段,幾秒就能看到動態效果。
bash assets/render.sh animation.html --from=3 --to=5 --fps=30 # 只渲 3s–5s
bash assets/render.sh animation.html --stills=1,3,5 # 只導出三張靜幀
第 3 步:全量渲染
bash assets/render.sh animation.html --duration=12 --fps=60 --width=1920 --height=1080 --concurrency=4
常用參數:
| 參數 | 作用 |
|---|---|
--duration / --from / --to | 時長 / 只渲某段(秒)。給了範圍時輸出名變成 名.起-止s.mp4,不會覆蓋成品 |
--stills=0.5,2.0 | 只導出這些時刻的 PNG,不編碼 |
--fps / --width / --height | 幀率與解析度 |
--preset=<名> | 按平台預設定尺寸:douyin / tiktok / vertical(1080×1920)、xiaohongshu(1080×1440)、instagram(1080×1350)、square(1080×1080)、bilibili / youtube / landscape(1920×1080) |
--presets=a,b,c | 一條命令出全套比例,每個比例一個成片(檔名自動加 -比例名 後綴)。跨平台釋出用這個 |
--theme=<名> | 換主題(midnight / paper / neon),覆蓋模板裡的 CFG.theme,不用改檔案。名字打錯會明確告警並回退 midnight |
--out=<檔案> | 指定成片路徑(預設同目錄同名)。一份 HTML 出多套配色時用它,否則後渲的會蓋掉前一個;上層目錄不存在會自動建 |
--json | stdout 只輸出一行結果 JSON(output / duration / frames / warnings 等),人讀日誌全部走 stderr。給 Agent 解析用 |
--concurrency | 並行 worker 數(每個 worker 一個 Chromium 頁面,注意記憶體)。給 auto 按 CPU 與記憶體自己挑 |
--audio=<file> | 編碼後自動混音(影片流直接 copy,不重編碼)。沒給 --duration 時自動按音訊長度出片(向上取整到整幀,尾音不被削) |
--audio-fit=trim|pad | 音畫長度不一致時怎麼辦:trim 按音軌收尾(預設)/pad 給音訊補靜音、保住影片長度 |
--gop=<秒> | 關鍵幀間隔,如 --gop=1 每秒一個關鍵幀,方便後期按秒 seek |
--keep-frames | 成功也保留逐幀 PNG(換 CRF 重編碼時不必重截) |
--tmpdir=<目錄> | 逐幀 PNG 放哪塊盤(預設挨著 HTML)。1080p 一分鐘約 2.4 GB,系統盤沒餘量時指到別的盤 |
--resume | 上次渲到一半被打斷?接著渲,已渲好的幀直接復用(參數和上次不一致會拒絕,見下) |
--alpha | 透明背景輸出(ProRes 4444 .mov),疊到實拍 / AI 生成畫面上用,見下 |
--srt=<file> | 外部字幕時間軸(SRT / VTT / JSON),注入 window.__cues,見下。同義引數 --cues |
--cue-log=<file> | 匯出 frame,t,cue_index,cue_start,cue_end CSV,用來核對字幕到底對不對軸 |
--story=<file> | 鏡頭表 JSON,注入 window.__story,整份換掉劇本(見「多鏡頭與轉場」) |
--scene-log=<file> | 匯出鏡頭時間軸 CSV;鏡頭表總時長與 --duration 不一致會告警 |
--gif[=fps,寬] | 出片後順手產 GIF(預設 15fps / 寬 960),兩階段 palette 最佳化,不重渲 |
--poster=<秒> | 出片後順手抽一張封面靜幀 .jpg,發站 / 社媒用 |
--batch=<file.json> | 一串任務按順序渲,最後打彙總表;單個失敗不連坐已渲好的,但整體非零退出 |
--headed | 開著瀏覽器視窗渲(除錯用;併發自動降到 1,免得彈一排視窗) |
--allow-errors | 頁面報錯不判失敗(預設:有報錯 → 非零退出) |
--keep-chrome | 不隱藏播放控制項(除錯用) |
--readytimeout=8 | 等 __seek 就緒的秒數(啟動慢的大字型/大資料可調大) |
--no-serve | 用 file:// 載入頁面(預設走本地 http server,見下) |
ESM / fetch 專案直接能渲。渲染器預設起一個只綁 127.0.0.1 的本地 http server 來載入頁面 ——
file:// 下 <script type="module"> 和 fetch() 會被 CORS 攔死,用 Vite/esbuild 組織的動畫、
讀本地 JSON 的專案根本起不來;走 http 就沒這些事。渲染開頭會列印這個地址,排障時直接貼到瀏覽器裡看控制檯最方便。
伺服器起不來會自動退回 file:// 並提示(只警告,不判失敗)。
- 輸出與 HTML 同名的
.mp4 - 幀數 =
fps × duration。12s@60fps = 720 幀,實測約 6~7 分鐘(concurrency=4) - 編碼約 1 分鐘(crf 18 / preset medium)
渲染結束有頁面報錯會非零退出(但成片照樣產出,方便你核對)。
渲到一半斷了,別從零開始
長片子(60fps × 30s = 1800 幀)跑到 80% 崩了,從零重來最傷。加 --resume 就接著渲:
bash assets/render.sh animation.html --fps=60 --duration=30 --resume
# ▸ resume: 已復用 1432 幀,還需渲 368 幀
規則三條,都是為了「續出來的片子和一次渲完的片子一模一樣」:
- 幀先寫
.part再改名。被 kill 的那一幀不會留下半個 PNG 冒充「已完成」,所以復用的幀都是真畫完的。 - 參數變了直接拒絕。上次 30fps、這次 60fps 會把兩批幀混著編碼,片子前後節奏不一致還不報錯 ——
所以
fps / 尺寸 / 範圍 / 渲染模式任一不同就退出並列印「上次 vs 這次」,讓你自己決定刪不刪。 - 幀號必須連續。缺一張 ffmpeg 會靜默提前收尾、成片莫名變短,所以編碼前按
[起,止)逐個核號, 有洞就報出來並提示加--resume補。
臨時目錄固定在 HTML 旁邊的 .seek-tmp-resume/(不加 --resume 時仍是一次性目錄,併發渲互不干擾)。
渲完自動清掉;中途想留著就配 --keep-frames。
這是有意的:頁面報錯常常意味著「整片黑屏 / 缺了半屏內容」,以前的版本會一路說成功,
直到你回抽幀才發現——現在它會在編碼前就把報錯打出來,並在最後判失敗。
透明輸出:把文字層疊到別人拍的畫面上
AI 生成的背景 + 程式碼生成的文字層,是現在最省錢的組合(模型畫文字必糊,程式碼畫文字精確到畫素)。
--alpha 就是為這條路準備的:
bash assets/render.sh animation.html --duration=6 --fps=30 --alpha
# → animation.mov(ProRes 4444 / yuva444p10le,背景透明)
模板側要配合一件事:讀到 window.__transparent 為真時跳過背景層繪製。
目前 title-card、compare、flow-steps、subtitle-card、vertical-subtitle、
explainer 這 6 個已經這樣寫,直接加 --alpha 就有透明效果;
其餘 5 個是滿版圖表(條形 / 折線 / 大數字 / 時間線 / 豎屏卡),本來就要底色,加了也只會得到不透明的片子。
自己寫的動畫照這個來:
const TRANSPARENT = !!window.__transparent;
function drawFrame(t) {
g.clearRect(0, 0, W, H);
if (!TRANSPARENT) {
// 底色 / 全屏漸變 / 光斑 —— 這些都要跳過
g.fillStyle = '#080A10';
g.fillRect(0, 0, W, H);
}
// 文字、圖形照常畫
}
全屏的半透明光暈也要一起跳過。哪怕它只有 5% 不透明度,疊到實拍上會讓整片區域泛白, 剪輯師第一反應是「這素材髒」。實測:只跳底色、留著光斑時,四角 alpha=12(不是 0); 全跳之後四角 alpha=0、只有 3.7% 的畫素不透明(全是文字)。
體積代價(960×540 / 2s / 20 幀實測):
| 輸出 | 體積 | 用途 |
|---|---|---|
H.264 .mp4 | 118 KB | 成片直接發 |
ProRes 4444 .mov | 760 KB(6.5×) | 拖進剪輯軟體疊層 |
只有 ProRes,沒有 VP9。試過 --pix_fmt yuva420p + -auto-alt-ref 0,檔案確實從 7.8KB
漲到 16.8KB(alpha 資料寫進去了),但 Chromium 播出來整幀 alpha 全 0 —— 連文字都透明;
換 lossless、固定碼率、-lag-in-frames 0 都一樣。既然沒法自證正確,就不把它作為選項釋出。
要小體積透明素材,用 ProRes 匯入剪輯軟體後再導 webm,那是那邊的活。
配音對齊:先有聲音,再定畫面長度
真實工作流是「寫文案 → 配音 → 配畫面」,而不是先定 6 秒再想辦法把配音塞進去。
所以給了 --audio 又沒給 --duration 時,影片按音訊長度出片:
bash assets/render.sh title-card.html --audio=voice.m4a --story=shots.json
# ▸ audio: 音訊 12.48s,未指定 --duration → 影片按音訊定長(12.5s,向上取整到整幀)
- 向上取整到整幀,不是四捨五入:少半幀就會把最後一個字的尾音削掉。
- 顯式給了
--duration且與音訊差超過 0.35s 會明確告警,說清差多少、傷在哪邊。 舊版本這裡會靜默出一支短片 —— 畫面後半段整段消失,日誌卻只有一句✓ Done。 --audio-fit=pad:音訊比影片短時補靜音,保住整段畫面(預設trim按音軌收尾)。
注意:trim 下成片不會精確等於音訊秒數。影片流是 -c:v copy 直接複製的,
ffmpeg 只能按幀包邊界收尾,實測會多留 0.3–0.5s。想要毫秒級對齊得重編碼,
那是拿畫質換精度,不值得 —— 片尾多留幾幀通常看不出來。
一稿多發:一條命令出全套平台比例
同一條片子要發抖音、小紅書、B 站,剪輯軟體裡得一個比例一個序列、導一次盯一次。 這裡版式本來就是引數化的,多渲一遍只是多一次迴圈:
bash assets/render.sh bar-chart.html --duration=8 \
--presets=douyin,xiaohongshu,square --out=dist/chart.mp4
# → dist/chart-douyin.mp4 1080×1920
# dist/chart-xiaohongshu.mp4 1080×1440
# dist/chart-square.mp4 1080×1080
| 預設 | 尺寸 | 給誰用 |
|---|---|---|
douyin / tiktok / vertical | 1080×1920 | 抖音、快手、視訊號、Reels、Shorts |
xiaohongshu / xhs | 1080×1440 | 小紅書 3:4 |
instagram / ig | 1080×1350 | IG 資訊流 4:5 |
square | 1080×1080 | 方版 1:1 |
bilibili / youtube / landscape | 1920×1080 | 橫屏 16:9 |
- 單個比例用
--preset=xiaohongshu;多個用--presets=a,b(至少兩個,只想要一個就用--preset)。 - 預設名拼錯會直接失敗並列出可用名字 —— 不會悄悄按 1920×1080 渲一版比例錯誤的片子。
- 版式能不能撐住是另一回事:模板跟視口自適應,但 9:16 與 16:9 的資訊密度不同,
3:4 上字號偏大 / 內容偏擠要自己看一眼。
--presets只保證尺寸對,不保證構圖好看。 - 豎屏底部那塊留白是有意的,不是排版出錯:平台進度條和按鈕壓在那裡, 模板統一給燒錄字幕留了 15% 安全區。
- 與
--stills互斥(一個是出多片、一個是隻截圖);與--batch不能巢狀。
給 Agent 用:--json
這個工具的主力呼叫方是 AI Agent。人讀 ✓ Done: xxx.mp4 (1.2 MB · 30fps · 12s) 沒問題,
Agent 只能拿正則去猜 —— 路徑裡帶空格、中文、括號就崩。--json 把結果結構化:
bash assets/render.sh title-card.html --duration=6 --json
{"ok":true,"exitCode":0,"input":"…/title-card.html","output":"…/title-card.mp4",
"width":1920,"height":1080,"fps":60,"duration":6,"frames":360,"bytes":2411724,
"gif":null,"poster":null,"warnings":[],"mode":"video","theme":null,"preset":null}
- stdout 只有這一行,進度與人讀日誌全部改道 stderr —— 直接
JSON.parse(stdout)即可。 warnings是陣列:引數衝突、音畫長度不符、主題名不認識都進這裡。空陣列 = 乾淨。duration是成片實際時長(ffmpeg 讀回來的),不是計劃值。--presets時output變成陣列,另附presets: [{name,width,height,output,ok,bytes}]。
字幕掛外部時間軸:配音改了,字幕跟著走
字幕文案寫在模板的 CFG.lines 裡最省事,但一旦要配音,時間點就得跟著音訊走 ——
總不能每改一句都回去手數秒數。所以支援直接餵一坨字幕檔案:
bash assets/render.sh subtitle-card.html --duration=8 --srt=voice.srt --cue-log=cues.csv
- 支援 SRT(
HH:MM:SS,mmm)、WebVTT(MM:SS.mmm也行)、JSON([{start,end,text}])。 - 解析一次,注入
window.__cues,模板按當前幀的t自己去查該顯示哪句。 模板裡用--srt時優先讀__cues,沒有才回退CFG.lines,所以同一個模板兩種用法都成立。 --cue-log輸出frame,t,cue_index,cue_start,cue_end的 CSV。把「字幕時間對不對」 變成一個能算的數:拿每句的首幀時間減 SRT 的起點,就是誤差。 實測 4 句 / 80 幀,每句誤差 0 ms,幀覆蓋數與理論值完全一致。
模板側只要兩行就能支援:
const cues = (window.__cues && window.__cues.length) ? window.__cues : CFG.lines;
const cur = cues.find(c => t >= c.start && t < c.end) || cues[cues.length - 1];
字幕別踩進底部 15%。那是各平台的進度條 / 點讚按鈕區,寫進去等於白寫。
qa.js 會提示「進字幕帶」,四個新模板的豎屏版式都留出了這條安全帶。
換配色:改一個欄位,不用碰畫布程式碼
assets/templates/themes.js 裡有三套調色盤,11 個模板全部接入。兩種換法:
# ① 命令列換(推薦):不改任何檔案,一次渲染一個主題
bash assets/render.sh bar-chart.html --duration=6 --theme=paper --out=bar-paper.mp4
# ② 改檔案:在模板頂部 CFG 裡寫死
const CFG = {
theme: 'paper', // midnight(預設,暗色)| paper(淺色)| neon(黑紫霓虹)
...
};
命令列優先於 CFG.theme(渲染器注入 window.__h2vTheme,模板讀
h2vTheme(window.__h2vTheme || CFG.theme))。所以同一份 HTML 可以批次出三套配色:
[
{ "html": "bar-chart.html", "duration": 6, "fps": 30, "theme": "midnight" },
{ "html": "bar-chart.html", "duration": 6, "fps": 30, "theme": "paper" },
{ "html": "bar-chart.html", "duration": 6, "fps": 30, "theme": "neon" }
]
若 Theme 名不認識,渲染器會印警告並回退 midnight,不會直接掛掉。
多鏡頭與轉場:從「一段動畫」變成「一支片子」
assets/explainer.html 已整合進場景系統,但從零開始或用模板做自己的多鏡頭片,
核心概念一樣:
// 鏡頭 = 一個函數,接收 (t, ctx, storyItem)
const shots = {
intro : (t) => { /* 0–2s */ },
point1 : (t) => { /* 2–5s */ },
point2 : (t) => { /* 5–8s */ },
outro : (t) => { /* 8–10s */ }
};
// 轉場 = timeline 協調:shots 物件 + 時間表
// assets/lib/scenes.js 已有跨鏡頭過渡的實作
assets/lib/scenes.js 提供:
- 鏡頭時間軸:定義每個鏡頭的起止時間。
- 轉場函數:內建簡單的淡入淡出與滑動轉場。
- 鏡頭表 JSON:可用
--story=shots.json完全由外部 JSON 驅動, 讓 LLM 產 JSON 比產 canvas 程式碼可靠。
explainer 模板會讀 window.__story,所以餵給它不同的 JSON 就能做出不同的解說片,
不用改模板本身。
大規模渲染技巧
批次(--batch)
把多個渲染任務寫在一個 JSON 裡,按順序執行:
bash assets/render.sh --batch=batch.json
[
{ "html": "title-card.html", "duration": 6, "fps": 30 },
{ "html": "bar-chart.html", "duration": 8, "fps": 60, "width": 1080, "height": 1920, "theme": "paper" },
{ "html": "explainer.html", "duration": 20, "fps": 30, "story": "shots.json", "audio": "voice.m4a" }
]
- 每個任務可含 render.sh 的引數(
html/duration/fps/width/height/theme/out/srt/story/audio/gif/poster等)。 - 順序執行,任務間不並行,避免記憶體競爭。
- 結束時列印彙總表。部分失敗不影響已完成的任務,但整體 exit code 非零。
覆蓋變數(實驗室用)
若是 CI 或 Lab 環境跑大量渲染,可以透過程式碼動態生成 batch JSON。
例如 gen_batch.py 列舉所有模板 + 所有主題 + 所有平台比例,
幾十個任務一次排程。渲染器對純方塊圖形(如條形圖)的渲染效能約 1.2 幀/秒/worker,
動畫複雜度上去會掉到 0.3–0.4。
環境要求與依賴
| 依賴 | 版本 | 說明 |
|---|---|---|
| Node.js | ≥ 18 | 執行 playwright 與 ffmpeg |
| playwright-core | 與瀏覽器快取匹配 | 見安裝步驟 |
| ffmpeg(完整版) | 透過 @ffmpeg-installer/ffmpeg 安裝 | 必須包含 H.264 / PNG / GIF 解碼器 |
| 瀏覽器 | Chromium | 由 playwright 管理 |
支援的作業系統
- macOS(Intel / Apple Silicon)
- Linux(x64,需要系統庫:
libnss3libatk-bridge2.0-0libdrm2libxkbcommon0libgbm1等) - Windows(Git Bash / WSL)
常見問題(FAQ)
渲染出來的影片畫面是黑的
可能是:
__seek未暴露或__ready未設為true- Canvas 尺寸與視口不匹配
file://下使用 ESM 被 CORS 攔截(用--no-serve停用 http server 試試,但 ESM 專案不行)- 播放控制項被隱藏(確保沒有使用禁止的 class 名)
渲染很慢
- 降低解析度(720p 比 1080p 快 2x)
- 降低幀率(30fps 比 60fps 快 2x)
- 減少並行數(記憶體不足時反而互相拖慢)
- 改純 canvas 渲染,避免 DOM/CSS 動畫
- 用
--from=... --to=...只渲關鍵段落
字幕時間對不上
使用 --cue-log 可以匯出每句字幕在成片中的精確時間,與 SRT 原文比對。
換了 --width/--height 後畫面被裁切
模板內 Canvas 必須使用 window.innerWidth/innerHeight 設定尺寸,不要寫死 1920×1080。
參考資訊
- 問題回報:GitHub Issues
- 許可證:MIT