以程式碼製作影片
將影片視為一個純函數 render(t) 加上一個分階段的、可斷言的管線。此任務類別中幾乎所有的失敗模式都是無聲發生的單位/符號/頻道/指標錯誤——因此以下工作流程的設計目標是讓每個錯誤可被觀測。
0. 在繪製任何東西之前凍結合約
- 單一時間來源。 一切都在
render(t)內部;影格↔時間對應 (t = frame/fps) 是唯一的公開 API。沒有 CSS 動畫、沒有計時器、沒有影格間狀態。結果:影格可平行化、可回復、可比較,且錯誤可定位到某個時間範圍。 - 確定性。 使用種子化 PRNG (
hash(i)/mulberry32),絕不使用Math.random;絕不使用Date.now;將字型和紋理嵌入為 data URI,以便沒有網路/字型後備可以在執行中途改變像素。 - 將規格寫成斷言,而非散文。 時長、fps、解析度、編解碼器、像素格式、響度、真實峰值、安全區域/邊距、禁止技術、檔案路徑、CLI 標誌、退出碼、JSON 輸出形狀。如果簡報中有一個數字,它必須最終出現在檢查中。
- 事先決定接縫策略——一次性 / 迴圈 / 拼接片段。對於迴圈:每個振盪器是
sin(2π·k·t/D)且整數k;影片收斂回自己的第一個狀態(不是「看起來相似」);事件表和螢幕上的計數器/標籤在最終轉換中重置;驗證render(0)與render(D)的渲染結果。 - 每個影格的預算。 在建立內容之前測量
render(t)(1080p 畫布繪製為 2–30 毫秒;3 個子影格 × 5400 個影格會倍數增加)。記錄預算並遵守它。 - 凍結介面,然後對其進行雜湊。 畫布大小、頁面/API 表面、worker CLI、輸出路徑。所有 QC 都綁定到該修訂雜湊——針對過期雜湊進行的渲染 QC 是沒有價值的。
- 記錄有意的偏差。 每個與簡報不符的地方(「乾淨」線條上的網格抖動、小於規定最小值的微型字型、超出規定範圍的彈簧速率)都會以簡短清單記錄下來並附上原因,以便審查者可以判斷,而不是發現。
1. 第一天進行三項探測
| 探測 | 斷言 | 原因 |
|---|---|---|
| 捕獲完整性 | 精確像素,alpha == 255,色彩範圍,存在第一/最後影格 | 半透明累積、範圍/色彩空間錯誤會無聲地毀掉整個影片 |
| 成本 | render(t) 每個場景的毫秒分佈、記憶體 | 現在架構變更很便宜,以後就很貴了 |
| 運動 | 整個時長內的每個影格差異序列 + 最長的靜止運行時間 | 立即顯示死時間範圍 |
源自真實失敗的規則:
- 白色合成陷阱。 透過在白色上合成 alpha 的檢視器審查影格會隱藏半透明;編碼器是在黑色上合成。讓審查路徑與交付路徑完全相同的合成方式,並對兩者之間的像素進行差異比對。
- 了解一個指標能看見什麼。 在 12 倍縮小後,細線的次像素運動會改變平均絕對差值約 0.03/255,而整個畫面的亮度變化則會改變約 1.5/255。如果驗收指標是縮小的平均值,最便宜的真正槓桿是全域值通道(例如,在節拍速率上加入 2 Hz ±3% 的亮度脈衝),而不是更大的擺動。
- 在資產之前驗證指標。 當檢查失敗時,先傾印原始訊號(1 毫秒的包絡、像素行輪廓、影格數列)。一半的「資產有問題」最終是檢測器有問題。
- 讓檢測器自描述。 建模並補償系統性偏差(濾波器群延遲、因果分析視窗延遲);偏愛最接近事件的候選者,而非最響亮的;永遠不要調整閾值直到它通過。
- 精確與近似的主張。 "render(0) ≡ render(D), PSNR ∞" 是一個證明;"第一個與最後一個編碼影格相差 2/255(一個影格的運動)" 是一個觀察結果。切勿在報告中混淆兩者。
已打包:scripts/av_audit.mjs 實作了容器/運動/迴圈/響度/連絡表這一半(僅 ffmpeg,無 npm 依賴)。
2. 審查迴圈是一項功能,不是開銷
- 將其網格化:數百張靜止圖片 → 十幾張連絡表,一次審查即可完成。
- 內建導引:安全區域框、中心十字線、時間碼、事件 ID。審查者不必在腦中記住版面配置。
- 按事件取樣,而非固定間隔:每個預期事件取一個影格,每個運動中間取一個影格。
- 最仔細檢查接縫:場景轉場、迴圈包圍、片段邊界、第一/最後影格。
- 隔離並放大可疑元素到空白畫布上 4–6 倍;大多數錯誤在全影格中是不可見的。
- 成品是可拋棄的:一個命令即可重新產生;切勿手動修補審查影格。
- 維護一個問題清單(雜亂/節拍不對/重疊/超出安全區域/無法閱讀/靜止),並在每次修正回合後重新審查。
3. 運動與迴圈工藝
- 環境動作 ≠ 主要動作。 每個場景需要一個與主要動作不同頻率的連續頻道(漂移、波浪、空轉旋轉、數值脈衝),其零點與節拍對齊,以便動作仍能乾淨落地。這就是滿足「沒有任何東西是靜止的」的條件。
- 一種緩動家族。 臨界阻尼彈簧
1-(1+x)e^-x(從不超調)用於移動;指數緩出/緩入用於擦除、按壓、退出。讓著陸與事件同時發生:開始 = 事件 − 持續時間,並歸一化,使得進度在事件發生時正好等於 1。 - 兩個地方的運動模糊:在影格函數內部(2–4 個快門樣本,使用運行平均值平均——
alpha = 1/(k+1)——最終完全不透明),在事件周圍更密集;加上編碼器端的時間混合。使用索引編碼的固定樣本(提供像素編碼影格編號的影格,並回讀哪些索引被平均了)證明濾波器視窗語義,切勿從文件推斷。 - 透過構造實現連續性。 在場景之間攜帶一個幾何/狀態模型並對其進行轉換(變形、材質交換、硬邊擦除泛濫、掃描線),而不是剪切或溶解。
- 保持長度的變形:透過角度/弧長參數化形狀,以便混合是增長而不是縮放。
- 預設禁止清單(除非被要求,否則刪除):粒子、發光、鏡頭光暈、攝影機晃動、RGB 分離、交叉淡入淡出、通用漸層、彈性超調。
4. 渲染與編碼
- 格式 vs 目標:首先測量吞吐量。無損 PNG 影格大小為數 MB,會給管道帶來壓力;高品質 JPEG 約快 1.5 倍。決定一次,然後保持不變。
- 用管道,不要假脫機:將影格饋送到 ffmpeg 的 stdin(
-f image2pipe -framerate <fps*sub> -vcodec png -i -)。使用tmix混合,選取組的最後一個影格,重新計時,用-frames:v <n>修剪。 - 按成本平行化,而非影格數量。 測量每個場景的毫秒數,然後分割成時間大致相等的區塊;均勻的影格計數會讓最慢的 worker 決定總時間。
- 片段必須可拼接安全:在每個端點重複 2 個邊界影格,相同的編碼器參數和時間基準,使用 ffprobe 驗證影格/時長,然後使用
-c copy拼接。 - 分階段進行管線,以便一個失敗只耗費一個階段:渲染 → 混合 → 編碼 → 音訊 → 封裝 → 測量;每個階段寫入一個檔案。
- 在各階段之間保留無損影片主檔(到處使用
-c copy),並最後處理響度。 - 在依賴之前先探測未知的工具鏈:濾波器/編碼器清單、瀏覽器可執行檔、版本;允許
$FFMPEG/$FFPROBE覆蓋;優先使用固定的本地二進位檔案,而非 PATH 的猜測。與瀏覽器一起提供的精簡「媒體」ffmpeg 通常缺少x264/tmix/loudnorm。 - 透過檔案描述符捕獲子進程輸出,而非管道。 在受限的沙箱內,使用
stdio: 'pipe'的spawn/execSync可能會因EPERM失敗,而同一個二進位檔案從 shell 執行卻正常。改為傳遞開啟的檔案處理器:spawnSync(cmd, a, { stdio: ['ignore', fdOut, fdErr] }),然後讀取檔案——對於純粹的存在性/版本探測,使用stdio: 'ignore'。將 stdout 和 stderr 路由到不同的檔案,以便二進位影格數據不會與日誌文字交錯。 - 從原始碼完成端到端重建,以便交付的檔案不是一個幸運的快取。
5. 音訊與封裝
- 與畫面相同的時間基準和相同確定性(離線渲染、種子化、無即時依賴)。
- 一個 cue 表(
{t, name})作為每個可聽/可視事件的唯一事實來源;SFX 存在自己的匯流排上,比音樂安靜。 - 使用雙通道 loudnorm 正規化(
print_format=json→ measured_* → 第二通道加上linear=true),然後使用獨立的第三方測量驗證;斷言整合響度在 ±0.5 LU 範圍內,且真實峰值 ≤ 目標。 - 迴圈:將尾端淡出(~40 毫秒)到精確零點,第一個樣本無喀嚓聲;測量打擊樂對齊到網格並報告。
- 封裝
-c:v copy -c:a aac -b:a <spec>,然後使用 ffprobe 檢查容器作為斷言,而非查看。 -b:a Nk是 ffmpeg 原生 AAC 編碼器的目標,而非下限(在安靜素材上,對 320k 請求測量約 180–250 kbps;-aac_coder twoloop有幫助)。讀回宣告的bit_rate並報告測量的平均值,而不是重複請求的數字,並在規格要求位元率時明確說明。
已打包:scripts/mux_normalize.mjs。
6. 保持繪圖層精簡且確定性
- 快取所有隨時間不變的內容:預渲染背景、文字遮罩、圖案、Path2D。快取金鑰包含每個可能改變像素的參數。
- 畫布池的大小必須來自來源畫布的尺寸,而非邏輯內容大小——否則
drawImage會無聲地拉伸或裁剪(這導致了剪裁/損壞的文字)。 - 在影格迴圈中避免使用
shadowBlur/filter;偏愛幾何圖形。 - 記憶體:1080p 畫布約 8 MB;綁定池的大小。
- 記住子影格數量會線性增加影格預算。
7. 協作
- 分離作者與驗證者。 驗證者擁有 QC 腳本以及讓交付品失敗的權限,並獨立檢查作者未檢查的維度(alpha、與頁面的保真度、響度、迴圈閉合)。這能捕捉到作者視盲的錯誤類型。
- 不重疊的寫入範圍 + 凍結的介面,以便平行工作不會衝突;對修訂進行雜湊,並在每次變更後重新執行 QC。
- 大聲失敗:非零退出、每個主張一行
PASS/FAIL、機器讀取的內容使用 JSON、無靜默後備。
8. 反模式
- 透過白色合成的審查圖像判斷顏色。
- 編輯資產以滿足你從未表徵過的指標。
- 調整檢測器的閾值,而不是修復其模型。
- 重新執行完整渲染來回答一個 20 秒頁面端探測就能回答的問題。
- 在成本差異很大的場景之間使用均勻的影格計數分塊。
- 與交付路徑不同(縮放、色彩範圍、合成)的審查路徑。
- 「看起來不錯」而不是一個斷言;以退出碼 0 失敗。
- 使用會損壞非 ASCII 的工具進行文字/資產手術(編輯後驗證編碼)。
- 用手修正審查影格,而不是修正來源。
- 用交叉淡入淡出隱藏狀態不連續性。
9. 檢查清單
飛行前 — 規格 → 斷言;render(t) 簽章已凍結;接縫策略 + 迴圈代數;資產已嵌入;三個探測存在;每個影格預算已設定;FFMPEG/FFPROBE 已解析並探測過能力。
飛行中 — 每個場景後:成本探測、運動數列、事件取樣審查網格;問題清單已關閉;已編輯來源的語法/型別檢查加上非 ASCII 檢查;對修訂進行雜湊。
交付前 — 從原始碼重建;完整渲染(成本加權、邊界影格、已驗證拼接);響度雙通道 + 第三方驗證;獨立測量腳本 → PASS/FAIL;明確報告精確與近似的主張;刪除臨時腳本/截圖並記錄每個的一鍵再生。
完成的定義 — 容器元數據精確;接縫行為已證明;靜止運行限制已滿足;響度/真實峰值已滿足;螢幕上的文字與簡報逐字匹配;一個命令從原始碼重建;所有 QC 在凍結的雜湊上為綠色。
10. 已打包的腳本
兩者都是單一檔案、零依賴(僅 ffmpeg/ffprobe),從 $FFMPEG/$FFPROBE → PATH → 常見位置解析其工具,每個主張列印一行 PASS/FAIL,使用 --json 輸出 JSON,並在失敗時以非零退出。從此技能自身的路徑解析其目錄。
# QC 一個完成的檔案:容器與期望值、靜止運行限制、迴圈接縫、
# 響度/真實峰值、可選的供人工審查的連絡表
node av_audit.mjs film.mp4 --expect-duration 30 --expect-fps 60 --expect-size 1920x1080 \
--static-limit 0.5 --sheets ./sheets # exit 0 = 所有檢查通過
# 將 WAV 正規化到目標值並將其封裝到編碼後的影片上(影片串流複製),
# 然後使用第三方獨立測量通道驗證
node mux_normalize.mjs silent.mp4 score.wav film.mp4 --lufs -14 --tp -1 --abr 320k
av_audit.mjs 故意使用低解析度(160×90)MAD 數列:它看到的是整個畫面的值變化和區塊移動,而不是細線的次像素運動——將其與 §1 中的指標敏感度規則配對使用。對於迴圈,將 first_vs_last_psnr_db 視為關於一個影格運動的觀察結果,並透過比較渲染器本身的兩個渲染結果來分別證明精確性。
一句話總結 — 成功取決於三件事,而非繪圖技巧:時間函數是純淨且可驗證的、指標在被最佳化之前已被理解、以及審查與交付路徑是相同的路徑。