film-engine:用程式碼繪製電影,一幀一幀
工具包所在位置: 本技能中的所有指令都應在
reels-maker資料夾(亦即github.com/thativanng/reels-maker的 clone,內含template/與tools/)中執行。若專案中尚無此資料夾,請先依照該倉庫的 SETUP.md 進行設定。
一段話說明核心概念
在此,一部電影就是一個 HTML 頁面,但有一個鐵律:window.seek(t) 必須每次都只從時間 t 繪製出該幀畫面,且不得讀取真實時鐘。然後渲染器會以 30 fps 逐幀遞增 t,對每一幀進行 2x 螢幕截圖,並將其 pipe 到 ffmpeg。由於頁面是確定性的,重新渲染的結果會一致(平均差異小於一個色階;GPU 雜訊可能讓單一像素偏移最多約 60 個色階),在 17.3 秒處的靜態圖就是影片將顯示的幀,而且每一項檢查(film-audit)都能在不播放影片的情況下檢視任何時刻。
流程(請勿跳過任何一站)
- 先有需求與風格。 reel-intake 的產出是一份已簽署的需求摘要和分鏡表(包含格式、長度、音效、禁止出現的元素);reel-style 則確定最終的視覺風格。未完成這兩項之前,請勿開始本技能的建置步驟。
- 分鏡表。 reel-intake 會寫下分鏡表(時間 · 畫面上的文字 · 音效與字幕 · 原因;存放於 references/brief.md),並需經簽署後才能繼續。若風格包含橫幅,則需增加橫幅欄位。
- 測試片段。 以選定的風格建置約前 10 秒(步行風格的影片可能需要 15–30 秒才能展現一個章節),擷取靜態圖,取得同意後再進行下一步。在未簽署的風格上進行完整建置是浪費時間。
- 建置完整影片(風格包會說明如何建置場景)。
- 關卡(film-audit):以 40 fps 進行碰撞檢查、像素探測,然後由獨立的稽核員檢查。在此之前不得將影片交給任何人。
- 渲染:
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4(2x 幀,以 lanczos 縮小,無色彩標記)。 - 音效:
python3 template/audio.py out/film.wav(一個與電影時鐘同步的提示列表)。 - 封裝:
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4。此步驟會輸出 −14 LUFS、含限制器、無色彩標記的 H.264 檔案,以及一個與影片同比例(短邊 720 px)的手機預覽檔。 - 封面:
node tools/cover.mjs --page reels/<name>/page.html --out reels/<name>/out/cover.jpg。若頁面支援封面模式(starter 的?cover或 config 中的cover),則繪製設計好的靜態圖;否則取T=處的幀。封面會儲存為影片本身的形狀,並控制在 2 MB 以下,同時為 9:16 的影片提供中心 3:4 裁切(適用於 Instagram 網格)。LinkedIn 僅能在發佈時設定封面:發佈後只能編輯貼文文字(LinkedIn 說明,「編輯已分享的貼文」,https://www.linkedin.com/help/linkedin/answer/a522811)。其縮圖規格為 JPG 或 PNG,與影片同形狀且小於 2 MB,來源為搜尋摘要中引用的 LinkedIn 說明;由於頁面本身無法載入,因此視為未經驗證。 - 傳送手機預覽檔,然後再傳送完整檔案和封面。
頁面合約
window.seek(T):繪製時間 T 的幀。這是唯一驅動動畫的地方。window.ready:一個 Promise,在字型、紋理與第一幀繪製完成後 resolve。window.DUR:影片長度(秒)。window.FILM = { w, h, format, safe }:畫面尺寸與安全區(像素)。每個工具都會根據此值調整視窗大小,因此頁面擁有格式的主導權。- 可選:
window.cover()繪製封面而非一般幀,以及FILM.coverAt指定封面對應的時刻(tools/cover.mjs 會同時使用兩者;若未提供,則取T=處的幀)。 window.collide(T)和window._dbg:供 film-audit 使用的鉤子(請參閱該技能)。- 工具會自行透過 HTTP 提供專案資料夾(ES 模組無法從
file://載入)。若要在瀏覽器中即時預覽頁面,請執行npm run serve並開啟 http://127.0.0.1:8765/template/starter.html。
格式、起始範本與其他語言
template/formats.json包含四種尺寸(9:16 1080×1920 · 4:5 1080×1350 · 1:1 1080×1080 · 16:9 1920×1080)、平台對應格式的對照表,以及每個平台的安全區及其來源。需求摘要中的格式行會從此檔案選取。- 每個 reel 都有自己的資料夾
reels/<name>/(已加入 .gitignore):包含其需求摘要、分鏡表、page.html(從 starter 或 film.html 複製而來)、reel.json或career.json,以及out/。頁面會從工具包的根目錄載入字型和 formats.json,因此從該處複製可以正常運作。將工具指向該處:PAGE=reels/<name>/page.html。 template/starter.html+template/reel.json是除 voxel walk 之外所有風格的骨架。其文字和時間點設定在 config 中,並根據?fmt=或 config 自動調整大小,且所有文字都保持在平台的安全區內。?guides會以紅色顯示安全區,僅供檢查使用;渲染時切勿啟用。它只是一個骨架,不是風格:reel-style 會改寫其 token 和版面。template/film.html(voxel 範本)僅支援 4:5, 且不啟用安全區(safe設為 0):其橫幅和字幕刻意靠近邊緣,因此安全區探測永遠通過,改由稽核員手動檢查。其攝影機關鍵格是專為 4:5 設定的;使用其他比例需重新設定關鍵格並重新執行所有關卡。- 中文、日文、韓文: starter 在渲染時會從 Google Fonts 載入 Noto Sans TC(僅載入 config 中使用的字元),且 CJK 文字的大小約為拉丁文字的 0.72 倍(因為 CJK 字元佔一個全形 em 的寬度)。在需要斷行的地方寫入
\n:中文可以在任意兩個字元之間斷行,因此較長的行需要自行指定斷點。template/reel.zh.json是一個範例。其他語言需要自行在字型清單中加入對應字型。
時鐘與時間控制
- 一個統一的電影時鐘驅動場景、攝影機、橫幅、字幕和音效。若要在後續加入一個節拍而不重新調整整個時間軸,可以使用時間重新對應:場景時鐘在插入點暫停,但覆蓋層仍使用真實時鐘:
音效腳本也需要同樣的對照表:撰寫一個const INS = [[24.0, 5.0, 'detail']]; // 在場景時間 24.0 處插入一個 5 秒的「細節」節拍 function remap(T) { let sh = 0; for (const [a, len, id] of INS) { if (T < a + sh) return { t: T - sh, ins: null }; if (T < a + sh + len) return { t: a, ins: id, u: T - sh - a, len }; sh += len; } return { t: T - sh, ins: null }; }RT(t)函式將插入的長度加回場景時間,並將每個提示放在RT(t)處,否則插入點之後的所有提示都會偏移。(範本沒有插入節拍,因此其 audio.py 不需要此函式。) - 緩動函式: 攝影機移動使用
eio,元素進場使用eo,步行和駕駛使用trap(梯形速度),使移動在結束時不會「瞬間卡住」。 - 攝影機關鍵格是絕對位置
A(pos, target)或跟隨F(offset, targetOffset)(相對於角色的地面點)。使用eio混合關鍵格。在重要的節拍上保持靜止;一直移動的鏡頭會顯得緊張。
覆蓋層(DOM 中,位於 Canvas 上方)
- 橫幅: 年份、公司、職稱標籤,以及一個會隨當前年份呼吸的時間軸點。橫幅應該在新的場景「出現在畫面上」時才改變(也就是角色群已升起、服裝已更換、主角已登場),而不是在程式碼開始建置場景時就改變。請在每個變化點前後 ±1 秒處檢查幀畫面。
- 字幕: 每個節拍一句故事,並以粗體強調關鍵詞。使用
text-wrap: balance。提供閱讀速度約每秒 3 個詞,並在快速鏡頭移動揭開主體之前淡出字幕。 - 對話氣泡: 跟隨角色移動:將頭部點投影到畫面上,並將氣泡保持在至少有 24 px 的內邊距內。
- 結尾卡: 姓名、一句標語、擅長領域、一句引言。背景場景模糊處理。
音效
- 提示應寫在電影時鐘上,並放在其對應的時刻旁邊(例如每步行走的腳步聲、物品出現時的鈴聲、著地時的悶響)。
- 轉場不要使用呼嘯聲,這會顯得廉價。改用鈴聲、滴答聲和輕微的撞擊聲。
template/audio.py中的預設背景音樂由正弦波產生,因此可自由使用。若有人帶來 AI 生成的音軌,請檢查其授權:某些模型的權重僅允許非商業使用。- 響度:使用兩階段 loudnorm 達到 −14 LUFS(先測量,再線性套用),然後加上限制器。一次通過會超標。
渲染關鍵事實(更多內容請見 references/gotchas.md)
- 以 2x 渲染再縮小。 EffectComposer 會取消 Canvas 的反鋸齒,因此 1x 渲染會到處出現鋸齒邊緣。
- MP4 中不要加入色彩標記。 JPEG pipe 會將影片標記為全範圍,某些播放器會因此偏移色彩。請在 filter 中加入
setparams=...=unknown。 - 同一個平面上的兩個面會閃爍(z-fighting)。請為前方的面設定深度偏移。
- 字型必須先載入(
document.fonts.load),否則在 Canvas 上繪製文字時會無聲地使用後備字型。 - 開啟無頭 Chromium 的 GPU 加速(macOS 使用
--use-angle=metal)。否則渲染速度約慢 10 倍。
無需每一步都提示即可工作
使用者已同意工具包的允許規則一次(SETUP.md 步驟 4);請保持在規則範圍內,否則會一直出現要求批准的提示:
- 使用 Write 和 Edit 工具寫入和修改檔案:頁面、reel.json、需求摘要、分鏡表、AUDIT.md。不要使用 Python 或 shell 腳本、heredoc 或
sed -i產生它們;這些方式每次都會觸發批准提示。 - 使用工具包的測量工具(stills、probe、sheet、collide、render、cover),而不是使用一次性的
python3 -c片段。 - 無需
mkdir、迴圈或串聯指令: 每個工具都會自行建立輸出資料夾,並且可以在一個呼叫中接受多個時間點(stills.mjs … 1.5,6,12)。 - reel 自己的腳本(例如音效腳本、輔助腳本)放在
reels/<name>/中,並以python3 reels/<name>/audio.py或node reels/<name>/x.mjs執行,這些都在規則允許範圍內。
指令
請將每個工具以單一指令執行(使用旗標,不要有 VAR=… 前置、不要有迴圈、不要有 && 串聯)。這樣使用者的允許規則(node tools/*)就能涵蓋,而不會在每一步都要求批准。環境變數仍可作用於腳本。
npm run doctor # 先檢測此機器
npm run demo # 12 秒的證明:out/demo/
node tools/stills.mjs "2,8.5,15" # 2x 靜態圖 → out/stills(voxel 範本)
node tools/stills.mjs --page template/starter.html --format 16:9 1.6 # starter 使用其他比例
node tools/stills.mjs --page template/starter.html --cfg reel.zh.json 1.6 # 其他 config(此處為中文)
node tools/stills.mjs --dsf 1 --out out/chk 2,8.5 # 快速 1x 幀供 sheet 使用
python3 tools/sheet.py out/chk/sheet.jpg 6 300 out/chk/t*.jpg
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4
python3 template/audio.py out/film.wav
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4