promo-video — 零成本宣傳片 / 廣告管線
把一個 web 專案變成宣傳片或社群廣告。核心 insight:如果 app 自己會電影運鏡演出,就螢幕錄它,不用「生成」影片;如果是出圖專案,就下載精選圖做 Ken Burns。 其餘(旁白 / 驗音 / 合成 / 品牌 / CTA)兩者共用。實戰驗證:battlefield-editor/tools/trailer(螢幕錄)+ catime 廣告「會讀世界的貓」(Ken Burns + 新聞→圖揭示)+《格莉奇與黑洞先生》全七章完整遊玩(螢幕錄的有聲變體,六十分鐘、七百句配音自動走完:https://youtu.be/J9OMebCjr9Y )。
決策樹:先選 capture 模式
- 會動的 app(3D/canvas/自動運鏡、有 auto-play)→ MODE A 螢幕錄(
capture-app.mjs)。 - 出圖 / 圖庫 / 相片 feed(AI 生成圖、gallery)→ MODE B Ken Burns montage(下載精選圖 +
kenburns.sh)。 - 要錄 app 自己發出的聲音(MODE A 的有聲變體;播什麼、播多久由 app 當下決定,手上沒有完整音軌)
→ 放掉
recordVideo,改 ffmpeg 同時抓畫面與音訊,見〈要「app 自己發出的聲音」 的時候:真的錄〉。手上有完整音軌就別走這條,事後 mux 音質比較好(GOTCHA 24)。 - 非視覺 repo(CLI / library / API / backend / 資料管線,沒畫面可錄)→ MODE C 終端/解說(
term-player.html+capture-term.mjs):把「指令 + 真實輸出」做成打字動畫的終端錄影。dev 工具要做「旁白教學版」不是「快切預告」(見 GOTCHA 22):結構 = 痛點 hook → 逐步驟(裝→跑→讀懂輸出→進階指令)+ edge-tts 旁白一步步講 + 步驟字幕 → CTA。旁白對時靠term-player.html的window.__STEPLOG(每步輸出出現的秒數),compose 把每句旁白 adelay 到對應步驟。實戰:wip 跨-repo 狀態 CLI 教學片(~60s,配樂壓 0.13 讓旁白為主)。 - 直播錄影的金句短影音(9:16 Reels/Shorts)→ MODE E 版型錄製(
short-9x16.html+capture-short.mjs): course-pipeline 先從逐字稿挑金句、剪出 16:9 片段,這邊把它嵌進 9:16 HTML 版型再錄。 為什麼用 HTML 不用 ffmpeg 濾鏡:裁切 ffmpeg 也做得到,但關鍵字動效在 CSS 是十行的事、 用 ASS 字幕格式做同樣的效果是地獄;版面要調也是改 CSS 看一眼,不用盲改 filtergraph。 分界:course-pipeline 剪、promo-video 包,接口是「一支剪好的 mp4 + 成品時間軸上的逐字時間戳」。 - 混合:screen-record 當主體、Ken Burns / 終端 當補充,都行。
四階段
- Capture — MODE A:
xvfb-run跑 headed Chromium(WebGL 需要),PlaywrightrecordVideo錄自動播映的高潮段;MODE E 走 headless(見 GOTCHA 47)。MODE B:撈來源清單、精選、下載。 - Narration(選)— edge-tts
zh-TW-YunJheNeural --rate=-5%生旁白。 - Verify(選)— Gemini pro 代聽逐字驗聲調;Gemini flash 海選配樂。
- Compose —
brand.py產品牌素材(字卡/角標/CTA/QR),ffmpeg 疊字幕/串接/混音,出多比例 + 濃縮版。
這裡的六個模板(改頂部 config / 傳參數就能用)
brand.py— PIL:cover/contain 守比例合成器、字卡、常駐角標、CTA 卡、mini 圖庫、QR(走 API)。kenburns.sh— 無抖動 Ken Burns clip + 文字貼圖緣 + concat + 混音(MODE B 主力)。capture-app.mjs— Playwright+xvfb 螢幕錄(MODE A);內建 http server、藏 HUD、點節點跳段 / 鍵盤驅動遊戲。term-player.html+capture-term.mjs— MODE C 終端/解說:HTML 終端播放器(打字動畫+彩色輸出)+ xvfb 錄。無需 asciinema/xterm。short-9x16.html+capture-short.mjs— MODE E:9:16 版型(投影片居中、上下大字幕、 金句字放大跳動、常駐角標、進度條)+ xvfb 錄影 + 事後 mux 原始音訊。 投影片那一格是自動運鏡:從畫面本身量出字在哪,每 2-3 秒換一次構圖,關鍵字唸到的 那一下再推近、提亮(GOTCHA 32-37)。重錄用--tag v2換檔名,不要蓋掉舊成品—— 舊的那支是比較基準。錄完會自己印「亮度事件幾次/秒」與「成品響度」把關,不用另外驗。relevel.py— 已經出貨的片要補對響度時用:只動音訊、-c:v copy,實測前後影像 串流 md5 相同,一支一秒,不必重錄。新錄的用不到(capture-short.mjsmux 時就對好了)。audition.py— Gemini 代聽:music "<用途>" *.mp3(海選配樂 fit 0-10)、tone "<文字>" v.mp3(驗旁白聲調)。
要「app 自己發出的聲音」的時候:真的錄
GOTCHA 24 講的是手上有完整音軌的情況(音樂影片、edge-tts 旁白): 用原始檔事後 mux,音質最好。可是有一種情況做不到——播什麼、播多久是 app 當下決定的。實戰:一部視覺小說的完整遊玩,七百個配音檔照劇情順序播, 沒有任何一份「完整音軌」可以拿來對齊。
那就把 Playwright 的 recordVideo 整個放掉,改成 ffmpeg 同時抓畫面與音訊:
# 一、開一個獨立的音訊接收端,瀏覽器的聲音導進去,不混桌面其他聲音
MOD=$(pactl load-module module-null-sink sink_name=cap)
# 二、自己的 X 螢幕
Xvfb :77 -screen 0 1280x808x24 -nolisten tcp &
# 三、畫面 + 聲音一起錄,分段寫檔(掛掉只損失一段)
ffmpeg -f x11grab -draw_mouse 0 -framerate 25 -video_size 1280x808 -i :77.0 \
-f pulse -i cap.monitor \
-vf crop=1280:720:0:88 \
-c:v libx264 -preset veryfast -crf 24 -pix_fmt yuv420p \
-c:a aac -b:a 160k \
-f segment -segment_time 600 -reset_timestamps 1 seg/part-%03d.mp4 &
# 四、驅動瀏覽器(DISPLAY=:77 PULSE_SINK=cap),跑完 concat 接起來
五個實測踩到的點:
--kiosk對 Playwright 沒用,它自己管視窗。分頁列加網址列固定 88 px, 螢幕開大 88 px 再用crop裁掉。游標用-draw_mouse 0藏。- 翻頁不要用固定秒數。 每句配音長短不一,等太短會蓋掉尾音、等太長整支
拖長一倍。攔
new Audio(),語音播完才點下一張(那些元素不在 DOM 裡,querySelector找不到)。 - 一定要有「內容有沒有前進」的檢查。 沒有的話,遇到點不動的卡片會一路 空點到上限,而 ffmpeg 不會報任何錯:實測錄滿五十八分鐘、檔案正常、 最後發現只走了全書的四分之一。判準是「畫面文字有沒有變」,而且 文字是空的也要算沒前進——結束畫面沒有文字,漏掉這條又多錄了三十三分鐘。
- 分段寫檔(
-f segment),中途掛掉只損失一段,最後concat -c copy無損接。 - 收工要驗四件,不能只看檔案產出來了:總長度、走了幾張卡、抽樣影格的
標準差(接近 0 = 空畫面)、
volumedetect的平均音量(靜音代表 null sink 沒接上,而且同樣不報錯)。
實戰:glitch-vn/tools/capture/(record.sh + play_through.mjs)。成品是
《格莉奇與黑洞先生》全七章完整遊玩,六十分鐘、七百句配音、一次走完沒有人在旁邊按:
https://youtu.be/J9OMebCjr9Y
GOTCHAS —— 這份是最值錢的資產,每次照它檢查
-
比例守恆:絕不對混合比例的圖用
scale=W:H(會拉伸變形)。背景用 cover(等比放大後裁切),主體用 contain(等比縮放放入)。ffmpeg 的 scale 只能是整數倍(2x)或加force_original_aspect_ratio。收工前跑「原始比例 vs 顯示比例」稽核(誤差 <0.1% 才過)。 -
Ken Burns 抖動:來源先放大 2x 再縮(整數像素跳動變次像素)+ 縮放用線性幀號
z='1+K*on/(D-1)',絕不用會累積捨入的z='min(zoom+inc,...)'。 -
zoompan 幀爆炸:別
-loop 1 -t(每輸入幀被 ×d);單張圖-frames:v D+zoompan d=D。 -
文字貼圖緣:圖先垂直置中;文字位置 = 中心 ± (圖高 × zmax)/2 ± gap,逐圖算(PIL 輸出各圖高當 sidecar)。這樣橫圖不會離很遠、也不會壓到。
-
file:// fetch 被擋:app 用
fetch(?pkg=)/載 JSON → 起python3 -m http.server走 http(或 capture-app.mjs 內建 server)。 -
headless WebGL 掉 swiftshader/全黑 → 用
xvfb-run -a --server-args="-screen 0 WxHx24"跑 headed Chromium。 -
ES-module app:
gotoPhase不在 window 上 → 用 DOM 點時間軸節點#tlNodes .node[i].click(),別evaluate(gotoPhase)。 -
QR:PEP668 裝不了 qrcode →
api.qrserver.com/v1/create-qr-code/?data=<公開連結>(只放公開連結);上線前手機掃一次確認。 -
edge-tts 破音字:先繞(「沒 mò」→改詞避開);聲調驗證用 Gemini pro 問音高走向,whisper STT 驗不了聲調(念錯照樣轉回同字)。
-
配樂:Gemini flash 海選 CC0,對味(別把戰爭配樂套在貓上);記 CC0 出處。
-
多比例:16:9=YouTube、9:16=Reels/Shorts/LINE、1:1 & 4:5=FB/IG 動態。MODE A 每比例原生重錄(直式 viewport);MODE B 每比例重合成。
-
常駐角標(台標):品牌+URL 全程在畫面角落(不只結尾卡),整支疊一張透明 PNG。
-
廣告要驅動行動:前 2 秒鉤子(靜音自動播,首幀就有畫面/大字)、燒錄字幕(FB 靜音)、CTA 給「現在就做」的理由。
-
平台細節:FB 影片進貼文、連結放第一則留言(避免觸及被壓);
+movflags +faststart;LINE 內嵌播放要用相簿/影片鈕傳,不能用「檔案」。 -
截圖要等載入完成 + 逐張驗非空:MODE A/gallery 逐項截圖別用固定秒數(慢的那張會截到「載入中」空畫面)。輪詢到 loading 指示消失才截;截完逐張量目標區 stdev(≈0 = 空/沒渲染),自動抓漏的重截。實戰:roll-formosa 苗栗固定 1.8s 截到空白,改 wait-for-load + stdev 掃描補回。
-
實際放大看,別只信縮圖:驗證務必抽全解析度單張細看(裁切/糊邊/被切在縮圖看不出)。量座標時對原圖畫 y/x 格線。實戰:地標下緣被裁 55px、下半糊影,都是縮圖看不出、放大畫格線才抓到。
-
互動遊戲沒 auto-play → 送鍵盤自己玩(MODE A 的子模式):Katamari/動作遊戲要用 Playwright
keyboard.down/up驅動(按方向鍵滾)。遊戲常在 load 後 ~10s(precache/PREPARING)才真正開始:先 wait-until-START(輪詢按鈕 ready)→ 點 START → 再送鍵。盲 bot 未必吃得到東西/長不大(roll-formosa 橫式卡在 2cm)——取「最有畫面」的窗(球夠大/物件多),或改用下條 showcase。 -
gameplay 難拍 → 用自走 showcase/gallery 當素材:很多 app 有「物件圖鑑/預覽」自走頁(如
preview.html?city=X),不用玩就渲染全部內容,是可靠 montage 來源(roll-formosa 20 城地標即此)。這些暗底物件用純暗底、不要模糊背景(不然糊出鬼影)。 -
「就緒」畫面在錄影尾段:recordVideo 從 load 起錄,PREPARING→START 的 ready 出現在後段;錄夠長(等到 START + 多停 3s),trim 從尾段取 ready 那截,別從頭(截到 loading)。
-
手機直式 viewport 常更好:遊戲 mobile layout 有時更滿版、甚至行為不同(roll-formosa 直式 gameplay 球會長大、橫式卡住)。每比例原生重錄時,順便看哪個 viewport 表現最好。
-
文案精準:主角/物件介紹用個性/怪癖不用外觀(catime 卡司);廣告的「新聞→圖」一行字要從來源資料的 idea/story 欄推,別腦補(掛錯新聞會出糗)。
-
預告 ≠ 教學,別搞錯格式:視覺專案(遊戲/app)適合 15-20s 快切預告(tease、抓注意);但 CLI/dev 工具做快切預告會很無聊、沒教學感——要做 ~60-90s 旁白教學:痛點 hook →
裝→跑→讀懂輸出→進階逐步驟 + edge-tts 一步步講(慢、每步停夠久)+ 步驟字幕 → CTA,配樂壓很低(~0.13)讓旁白為主。實戰:wip 第一版快切被嫌無聊,改旁白教學版才有「懂了、我會用」的實感。旁白對時用 term-player 的__STEPLOG。term-player 要每步清空螢幕(GOTCHA 16 的同源問題:內容累加會寫到視窗外、被講到的指令被推走)。 -
先掃完整 README/
--help做功能清單,再寫腳本:做工具教學/解說別只讀前段就開拍——會漏掉招牌功能(wip 漏了dashboard.py視覺儀表板、wip done、--root、install-skill、Codex 支援、see:規劃檔偵測)。先列全功能,再選要演的。TTS 別硬唸帳號/handle/罕見拼法(如yazelin其實是中文名「亞澤林」的拼音,逐字唸很怪)——畫面已有 URL 就讓旁白帶過(「連結在下方」);避開會頓的開頭詞(「先…」→「…很簡單」)。edge-tts 唸一般英文術語(cargo/markdown/Claude Code)反而 OK,用 Gemini flash 聽一句確認。 -
MODE D 音樂影片:整首歌 + 互動,別錄音、事後配原檔。Playwright 的
recordVideo不含音軌也不含游標(手上沒有完整音軌的時候見〈真的錄〉那一節),所以:(a) 聲音用原始 mp3 事後 mux(音質最好、不會被 xvfb 的 null sink 影響);(b) 注入一隻假游標(貼一張圖跟著 pointermove 走,錄影專用不進 repo),不然「滑鼠互動」在影片裡看起來只是特效自己在動。動作時間軸要綁audio.currentTime輪詢(waitForFunction(t => au.currentTime >= X)),不要用waitForTimeout疊加——錄影機掉幀時牆上時鐘會漂,音訊時鐘不會。 -
對時靠畫面裡的時鐘,不要靠 node 的
Date.now()。webm 的第一幀跟newContext()之間有不確定的落差,而且 Playwright 會在結尾補一大段凍結幀(實測 160 秒的內容寫成 239 秒的檔),ffprobe的 duration 不能當長度用。做法:crop 進度條那一條,逐幀量滑塊的 x(取粉紅像素最高的那一欄當峰值再取重心——飄過去的粒子也是同色,直接取平均會被拉走),對畫面時間 → 音訊時間做線性擬合,解出音訊起點。交叉驗三種估法(滑塊擬合、歌詞換行時刻、點擊當下的牆上時鐘)取中位數;歌詞那條會因為 CSS 過場慢 ~0.1 秒,記得扣掉。收工前用互相關驗成品音軌對原檔的延遲(AAC priming 會固定多 ~40ms,正常),再抽三張「離換行遠一點」的影格確認亮的是對的那句。 -
直式(9:16)不要靠瀏覽器縮放去撐解析度。
recordVideo是把「CSS 尺寸的畫面」貼進你要的影格尺寸裡:viewport 405×720 配recordVideo.size1080×1920,結果是 405×720 的內容貼在左上角、其餘填滿灰色;想用deviceScaleFactor撐出裝置像素,Playwright 的 viewport 模擬會把 DSF 打回 1;改用--force-device-scale-factor+--window-size,xvfb 沒有視窗管理員,--window-size、Browser.setWindowBounds、--kiosk全部改不動視窗(量到 innerWidth 卡在 501)。html{zoom:...}也沒用,media query 讀的還是原始寬度。可行解:viewport 直接開原生 1080×1920(DSF 1),再注入一份「放大版直式樣式」(手機版的排法 + 字級/控制項按比例放大,!important蓋掉桌面版的 grid)。錄影專用樣式不進 repo,跟HIDE同一個位置注入。字糊不糊要放大看(GOTCHA 16):405 拉到 1080 的字邊緣是軟的,原生渲染是硬的。 -
可以的話讓「畫面自己會說話」:互動類的錄影,把要展示的操作編成時間軸(點、拖、停),中間刻意留幾段完全不動讓自動隱藏的 UI 收起來——連續動反而讓人看不到重點。錄互動要記得 recordVideo 不含游標,注入一隻假游標(貼圖跟著 pointermove),否則觀眾只看到特效自己在動。
-
對時錨點要用「白閃」,不要用「全黑」。想法是「頁面先全黑、影片真的開始播才顯示, 掃第一個亮起來的幀當 t=0」——實測不成立:Playwright 的 recordVideo 不是從第一幀開始收, 整支錄影最暗的一幀是 24.4/255,根本沒有黑幀可以認。改成開播前打一段 400ms 全白, 開始播的那一刻收掉,掃「白閃結束的下一幀」。白色 250 對上內容的 24-37,認不錯。 驗過偏移全程 ±0.05 秒、沒有漂移。
-
metadata=print預設寫到 stderr。ffmpeg -vf "...,metadata=print:key=X"的輸出不在 stdout, 腳本只收 stdout 就永遠讀到空的,然後把「讀不到」誤判成「畫面是黑的」。要加:file=-。 -
內建 http server 要先讀檔再送標頭。
res.writeHead(200)寫在await readFile前面的話, 檔案不存在時 200 已經送出去了,catch 想補 404 就撞ERR_HTTP_HEADERS_SENT—— 真正的死因(路徑打錯)被這個假錯誤蓋掉。本 repo 的capture-app.mjs原本就有這個坑,已修。 -
驗 A/V 對時要用有鑑別力的量法。「抽同一時刻的幀比相關性」聽起來合理,但投影片型的內容 幾十秒不換畫面,負控制(故意差 6 秒)也是 0.996,等於沒驗。可行的做法:在版型上放一條 進度條(寬度 =
currentTime / duration),從成品畫面量條寬就把播放器的內部時間讀回來, 直接跟輸出時間相減。這條也順便當成品的視覺元素,不是只為了測試而加。 -
靜止投影片的 9:16 短影音,畫面是死的——而且量得出來。 投影片那一格的逐幀灰階差 實測只有 0.072 / 0.092(凍結對照 0.000),使用者的回饋是「太平淡、缺乏 punch」. 門檻不要用感覺定,拿自己出貨過的 9:16 作品用同一把尺量出可用區間: neko-tensei 音樂影片 0.233、k-rider 預告 1.045、battlefield-editor 1.293、 roll-formosa 1.912、catime 廣告 3.965、catime 預告 6.168(active_ratio 0.636 = 快切)。 講述型金句短片不該做成快切。
目標帶 0.30 到 1.05、active_ratio 0.10 到 0.40——這組門檻作廢:mean與active_ratio都沒對幀率正規化,而且拿自己的成品回頭 定自己的門檻是循環論證。現行判準是punch-score.py的motion.slide.per_s ≥ 5.5(每秒的灰階變化量,mean × fps,唯一不隨幀率變的那個數字),上界不設, 危險的「閃」交給 GOTCHA 35 的亮度事件擋。依據:使用者說「畫面是死的」的 v1 是 1.79 與 2.30, 而最安靜的已出貨作品 neko-tensei 是 5.82 且必須通過。 -
對靜止投影片運鏡,鏡位要從畫面本身量出來,不能寫死比例。 960×720 的投影片實測只有 250 條掃描線有字,其餘是空白;寫死的上/中/下裁切會切到頁首那行小字或整片空白。 做法:canvas 抽一幀 → 每列的墨量(跟背景中位數的差)→ 連成帶 → 丟掉墨量不到 12% 的帶 → 留下來那幾帶的左右極值就是正文範圍 SP,鏡頭再怎麼動都不准把 SP 切掉。 量的時候不要縮圖:縮 8 倍(120×90)量過,
drawImage的縮圖不是面積平均, 細筆畫直接消失(正文右端的「的路。」量不到,SP 少算 96 px),而背景漸層又被當成 橫貫全寬的東西。門檻怎麼調都顧此失彼,直接用原解析度就沒這問題,而且「墨」可以跟驗收 用同一個定義(高出背景中位數 28)。960×720 一次掃完在 headless Chromium 只要幾十毫秒。 -
取景要量「當下正在播的那一幀」,不是先 seek 過去量。 想量「金句在講的那一頁」就
V.currentTime = 金句中段再drawImage——seeked只保證解碼完成,畫面還沒換, 實測整支 quote-06 都拿第 0 秒(上一頁)的版面在取景:SP 量成 596 而不是 696, 於是「保證不切字」的框從一開始就框錯,旗艦關鍵字那 2.8 秒有一半的幀正文缺角。 可靠的做法是在播放中量:開場用第 0 秒那一幀(那正是當時螢幕上的東西), 之後每次換鏡再量一次。順帶把「一句跨兩頁」也解決了——換頁後的下一次換鏡就跟上。 -
明暗事件要有量得到的上限,不能只說「我做得很慢」。 量投影片帶的逐幀 YAVG, 門檻用相對 10% 加一條絕對地板 4/255——投影片底色 Y 大約 20,純用絕對值當門檻的話 連提亮 9% 都測不到;純用相對值又會被暗處的雜訊洗版。數「最密的一秒有幾次」,守 3 次 (光敏性癲癇的風險門檻)。實測成品 2 次/秒;負控制 5 Hz 閃爍 26 次/秒、 1 Hz ±6% 慢脈動 6 次/秒,兩個都擋得下來。這段檢查已經寫進
capture-short.mjs,錄完自己印。 -
ffmpeg的eq濾鏡,brightness 運算式預設只算一次(eval=init)。 做「故意閃爍」的 負控制時漏了:eval=frame,濾鏡等於沒作用,量出來跟成品一模一樣——差一點就把一個 好用的指標判成「無鑑別力」。負控制自己也要先驗它真的壞掉,不然它只會證明你的工具 分不出兩支一模一樣的檔案。 -
幀差對「對時」永遠無鑑別力。 GOTCHA 31 那件事換一個版型再驗一次:把 words.json 故意錯開 6 秒,投影片帶的幀差是一模一樣的 0.718(它根本不看 words.json)。 會爆的是「字幕換塊時刻 vs 字幕區最大幀差時刻」那條(0.091 → 0.567)。 動態指標只能宣稱動態,對時要另外量。
-
字幕分塊遇到「兩個相鄰的關鍵字」會把它們黏成同一塊。 分塊只在「塞不下」的時候換塊, 而換到另一個關鍵字時往往還塞得下——實測「很大的問號」「沒有被驗證過」中間沒有別的字, 黏成一塊 11 個字、整塊全金,兩個爆點縮成一個(亮塊只剩 2 塊)。 修法是加一條:
k >= 0 且 k !== 上一個字的 k就強制開新塊。修完亮塊 3 塊、 最大塊從 11 字降到 6 字。注意punch-score.py裡有一份同樣的分塊移植,要一起改。 -
執行期會偷偷把縮放乘大,取景算寬度時要先扣掉那個餘裕。 緩推(+3.5%)和關鍵字推近 (+5%)加起來是 8.7%:框得「剛剛好」的鏡位,一被推近就把正文擠出畫面,而且專挑 關鍵字那幾秒發作——最不該出問題的地方。兩件事都要做:取景時
z = 寬度容許 / 1.087, 以及每一幀重新夾一次(z ≤ 寬度容許、cx夾到 SP 兩端都在框內)。 不變量要在畫面出去的那一刻成立,不能只在「取景的時候算對了」。 -
投影片自己的頁首頁尾一旦被放進畫面中央,左右一定被切,而且會被算成切正文。 頁首那行小字(x 24 到 932)和頁尾進度條(x 0 到 759)都伸出正文範圍之外; 鏡頭一推近,它們就跨出畫面兩側。驗收把投影片帶上下各修掉一成當「頁首頁尾」, 運鏡一改,家具就落在那條線的裡面,於是被算成正文被切。 修法:凡是「墨伸出 SP 之外」的列都標成家具(判準不是「橫貫全寬」——頁尾進度條在 墨的門檻下只剩 79% 寬,用全寬去判會漏掉),再限制鏡頭中心,讓家具只能待在 畫面上下緣那一成之內、或整個出框。邊界要多讓幾個像素:剛好卡在一成上的話, 取樣的進位會讓家具的最後一兩列落到正文那一邊(實測還剩 15 幀,全在第 81、82 列)。
-
診斷取景問題,先把演算法離線重跑一次,不要靠改參數試。 一次錄影 40 秒, 盲試門檻會走進「越調越糟」:實測把緩推和關鍵字推近關掉(以為那是主因),切字反而從 102 幀變成 189 幀。改成用 ffmpeg 抽同一張偵測幀、在 Python 裡跑同一套 band/SP 邏輯, 十秒就看得出「瀏覽器量到的 SP 跟第 0 秒那一頁一模一樣」,主因當場現形。 另外把取景的前提印進 log(SP、寬度容許的 z、家具位置)——鏡位塌掉的時候, 看那三個數字就知道是量錯寬度還是走廊被夾死。
-
報成品數字要分「內容來的」和「畫面來的」,後者一定要附版型的 md5。 內容來的(鉤子、染色、亮塊、字幕塊、靜音、語速)綁在
words.json上,那份 md5 不變 數字就一個位元都不會變;畫面來的(幀差、切字)只對錄的時候那一版版型成立。 兩邊混在一起報,下一個人照文件重錄就會拿到對不上的數字:實測同一支 clip, 改版前的版型錄出來每秒變化量 14.80,改版後是 27.19——差 1.8 倍,而內容那六項一個不差。 對方因此花了一輪去查一個不存在的迴歸。capture-short.mjs現在每次錄影都會印版型 short-9x16.html md5 …,報數字的時候抄上去。 -
錄影本身有抖動,判準要留得起這個抖動。 版型和輸入都用 md5 釘死、連錄四次, 投影片帶的每秒變化量是 21.27 / 21.33 / 20.82 / 21.3
…