影片疊加圖層
將原始剪輯轉變為一組動畫疊加圖層,每個圖層都精準落在講者說出該內容的影格上,並產出 Resolve 匯入檔案。
在此設定你的品牌
只需編輯一次,在 scripts/scene.html 中(頂端的 :root 區塊)。其他所有設定都會從這裡讀取。
| 設定 | CSS 變數 | 預設值 |
|---|---|---|
| 強調色 | --brand-accent (+ --brand-accent-rgb 作為 r,g,b) | #4a90e2 |
| 淺色文字顏色 | --brand-light (+ --brand-light-rgb) | #f2f2f2 |
| 深色墨水顏色 | --brand-ink | #0d131a |
| 顯示字型(標題、數字) | --font-display | 'Anton','Bebas Neue',Impact,sans-serif |
| 主要字型 | --font-body | 'Inter',-apple-system,sans-serif |
顯示字型必須在本機安裝(例如在 ~/Library/Fonts),因為影片片段會在無頭 Chrome 瀏覽器中離線渲染。Anton 和 Bebas Neue 可在 Google Fonts 免費取得。程式碼中的名稱 BLUE 只是代表「強調色」。
風格原則:場景內文字。 不使用卡片。文字與講者的頭部位於同一空間,大型標題環繞頭部兩側,提問內容隨著講者說出而逐字顯示。始終使用你的品牌顏色和顯示字型。
最重要的規則: 讓圖形保持在講者頭部後方或側邊。不要在臉部前方放置任何東西,也不要使用背景顏色。圖表同樣位於講者身旁的牆面上。所有內容都經過遮罩處理,因此講者的手可以穿過圖形前方。
一次執行會產出: N 個透明的 .mov 影片片段、PLACEMENT.txt、兩個 .fcpxml 檔案,以及一個簡短的 PREVIEW.mp4。
預設沒有聲音。 除非被要求,否則不要加入音樂或音效。
風格預設值(可依喜好調整)
- 沒有任何內容會在說出前出現。 將每個片段錨定到該短語的逐字時間碼,再加上約 0.12 秒,使其在文字說出後立即出現。章節層級的時間設定會被視為較早。
- 簡短。 2.5 到 6.5 秒。大部分約 3 秒。它應該像社群媒體影片一樣快速切換,而不是長時間停留。
- 位於頭部後方或側邊,絕不覆蓋講者。 不使用卡片、背景或變暗效果。這表示不再使用角落標籤、所有
full-*卡片以及新影片的define類型。 - 仍然顯示機制。 在寫出句子之前,先使用圖表。使用場景內圖表類型(
dots、flow、bars、compare、math),它們會位於講者身旁的牆面上。 - 將任何有兩部分內容的項目拆分開來。 舊方法和新方法是兩個片段。10% 和 20% 是兩個片段。三個選項就是三個片段,每個片段都會在它被提到時出現。
- 每一行都精準落在該詞上。 列表、側邊文字和逐字輸入的文字,每行都有各自的偏移量(
at、t),這些偏移量來自於單字的時間碼,而不是平均分配。scripts/plan.example.py示範了做法:引用短語,命名提示詞,它就會進行查詢。 - 良好的混合(約 48 個片段,持續 11 分鐘):大約四分之一是場景內圖表(主要是
flow),然後是side、pair、type、list2、num。保留pair給重點結論。 - 大型標題要環繞頭部,而不是放在頭部後方。
pair會將每一行在單詞邊界處分成兩半,分別位於頭部兩側,間距為臉部框寬度的 1.9 倍(包含頭髮和耳朵),並縮放以確保兩半都顯示在畫面內。因此每個pair行都需要 2 個或以上的單詞(例如「ONE HOUR」,而不是「HOUR」):單一單詞無法分割,最終會被隱藏。在data中設定"around": false可以恢復置中顯示。外框線條為 150px,帶有 5px 的筆觸和 35% 透明度的淺色強調填充,以確保可讀性。
開始之前:剪輯是否已鎖定?
疊加圖層是錨定在特定詞語上的,因此之後的任何修剪都會導致所有片段偏移。如果使用者提供的是原始素材,或者剪輯尚未鎖定,請先執行 paper-edit 技能,並將疊加圖層放置在其產生的時間軸上。
流程
1. 取得剪輯並進行探測
要求使用者從 Resolve 匯出(例如到 ~/Movies/),或者如果先進行紙本編輯,則要求原始素材在 <YOUR_FOOTAGE_DRIVE> 上。提供影片檔案即可,無需單獨匯出音訊。當 Resolve 開啟時,透過 MCP 讀取時間軸,而不是要求匯出。
ffprobe -v error -show_entries format=duration -show_entries stream=codec_type,r_frame_rate -of default=nw=1 "<file>"
注意影格率(渲染器預設為 23.976 = 24000/1001;如果使用其他影格率,請在 render.mjs 中更改 FPSN/FPSD),並詢問時間軸是從 00:00:00:00 還是 01:00:00:00 開始(Resolve 的預設值是 01)。
2. 使用逐字時間碼進行轉錄
ffmpeg -y -i "<file>" -ac 1 -ar 16000 -c:a pcm_s16le cut.wav
whisper-cli -m ~/.cache/whisper-models/ggml-small.en.bin -f cut.wav -oj -ml 1 -sow -of cut
-ml 1 -sow 參數為每個片段提供一個標記,這是實現精確錨定的關鍵。然後,透過比對 cut.json 中的標記序列(transcription[].offsets.from 單位為毫秒),找出每個短語的起始時間。
3. 挑選時刻
先閱讀完整的轉錄稿。目標是每 15 秒影片大約一個片段。針對每個片段,決定:錨點短語、類型,以及能讓動畫展現並有足夠呼吸空間的最短持續時間。
要根據實際說出的內容來決定,而不是書面筆記。如果筆記寫著「30 分鐘」,但鏡頭前說的是「大約半小時」,則以錄影內容為準,並向使用者標記此差異。
4. 撰寫 clips.json
複製 scripts/clips.example.json 作為模型。每個片段一個物件:
{"id":"06-math-10","start":52.96,"dur":3.6,"type":"dots","cue":"\"if 10 out of 100 meals are home cooked\"",
"data":{"lit":10,"pct":"10%","count":"10 meals"}}
將它放在技能資料夾之外(臨時路徑即可),並使用 CLIPS= 傳遞。start 是短語的原始時間(秒);渲染器會自行加上 0.12 秒的提前量。id 的順序決定了檔案順序,因此請對它們進行編號。
5. 渲染乾淨的剪輯(場景內類型需要)
場景內類型會讀取頭部位置,並從一份沒有疊加圖層的剪輯副本中將講者挖出。從 Resolve 渲染已鎖定的時間軸,停用所有疊加圖層片段,使用 H.264 1080p,輸出到臨時路徑。在 API 中,SetTrackEnable 在 Resolve 19 中對 V2 軌道無效;應對軌道中的每個片段使用 item.SetClipEnabled(False)。原始素材必須在線,因此素材硬碟必須已掛載;如果未掛載,請要求使用者插入並監控掛載狀態,而不是渲染一個已完成的母版(舊的疊加圖層會被燒錄進去)。
6. 渲染
cd <skill>/scripts && npm i # 僅首次執行,安裝 puppeteer-core
swiftc -O person.swift -o person # 僅首次執行,遮罩 + 頭部尋找器
TITLE="<video name>" CLIPS=/path/to/clips.json FOOTAGE=/path/to/clean.mp4 OUT_DIR="$HOME/Movies/<video-name>-overlays" node render.mjs
# 僅一個或少數幾個片段:
ONLY=06-math-10,07-math-30 OUT_DIR=... node render.mjs
每渲染一秒的疊加圖層,大約需要 10 到 15 秒的真實時間(pair 較慢,因為它需要對每個影格進行遮罩)。讓它在背景執行並定期檢查進度是正確的做法。它會在結束時寫入 PLACEMENT.txt 和兩個 .fcpxml 檔案。
在完整渲染之前,務必在真實素材上進行預覽。 node proof.mjs <clean.mp4> <clips.json> out.jpg [email protected] [email protected] ... 會為每個片段渲染一個影格(前綴@進入片段的秒數),並疊加在實際畫面上(包含遮罩),然後拼貼成一張圖片。檢查它。文字跑出畫面、標題穿過眼睛、文字疊在複雜背景上:這些在程式碼中看不見,但在圖片中一目了然。對於圖表,疊加在灰色背景上仍然可以接受。
預覽影格:先提取為 PNG。 使用 -ss 將 qtrle .mov 直接疊加到 lavfi 顏色來源上會得到空白影格。先將影格提取出來(ffmpeg -ss 2.8 -i clip.mov -frames:v 1 f.png),然後合成 PNG。
在 Resolve 中放置: 複製鎖定的時間軸並命名為 YouTube Final,將 mov 檔案匯入到 Overlays 素材箱中,然後對每個檔案使用 AppendToTimeline,設定 trackIndex: 2 以及來自 PLACEMENT.txt 中 IN 時間碼的 recordFrame(((h*3600+m*60+s)*24+ff),因此 01:00:00:00 = 86400)。一次解析 PLACEMENT.txt 中的一行(line.split()[0] 是 id,[1] 是時間碼)。使用整個檔案的正規表達式可能會將像「60-second」這樣的提示文字誤認為片段 id,並竊取下一行的時間碼,導致一個片段被靜默地遺漏。請確認已放置的片段數量等於片段總數。
7. 預覽,然後交付
在真實素材上建立一個 10-15 秒的預覽,以便使用者在將其加入之前進行判斷:
ffmpeg -y -ss <t> -t 11 -i "<cut>" -itsoffset <a> -i "<clip1>.mov" -itsoffset <b> -i "<clip2>.mov" \
-filter_complex "[0:v][1:v]overlay=0:0:eof_action=pass[v1];[v1][2:v]overlay=0:0:eof_action=pass[vo];[vo]scale=1280:-2[v]" \
-map "[v]" -map 0:a -c:v libx264 -crf 20 -preset veryfast -c:a aac PREVIEW.mp4
原始音訊會隨素材一起包含進來,這正是預覽所需的全部內容。
然後提供匯入步驟(檔案 > 匯入 > 時間軸 > 匯入 AAF、EDL、XML,選擇 starts-01 檔案,將片段複製到素材上方的一個軌道,起始時間為 01:00:00:00)。
直接在 Resolve 內部工作(當 Resolve 開啟時為首選)
如果已安裝 resolve MCP 伺服器(例如第三方的 barckley75/resolve-claude-mcp),它可以與 Resolve Studio 的腳本 API 通訊。如果 paper-edit 技能提供了 ensure_resolve.sh,請先執行它,以便在 Resolve 關閉時將其啟動。在 Preferences > System > General > External scripting using: Local 啟用的情況下,優先使用此方法而非匯出再匯入的繁瑣流程。
它會改變什麼:
- 不需要匯出的剪輯。 讀取開啟時間軸的真實時間碼、影格率、起始時間碼和片段佈局,然後將疊加圖層錨定到這些資訊上。這解決了「每次重新修剪時間碼就會偏移」的問題。
- 不需要手動匯入。 直接將渲染好的
.mov片段放置到影片軌道上。 - 更好的時刻選擇。 看到剪輯點的位置,意味著疊加圖層可以與剪輯協同而非對抗。
- 它還可以在疊加圖層就緒後啟動渲染。
使用規則:
- 先讀取再寫入。 首先確認專案名稱、時間軸名稱、起始時間碼和影格率,並向使用者複述確認。
- 絕不寫入主時間軸。 先複製它,然後在副本上工作,這樣即使犯錯也毫無損失。
- 避免使用
execute_resolve_code,除非一般工具無法勝任。 它會針對使用者的專案執行任意 Python 程式碼。如果這是唯一的方法,請先顯示程式碼。 - 僅限本機工作階段。 Resolve 必須在這台 Mac 上執行,因此這在雲端或排程執行中永遠無法運作。在這種情況下,請回退到 FCPXML 路徑。
- FCPXML 路徑仍然有效且仍然是備案。 繼續產生兩個檔案;這只需要幾秒鐘。
音訊轉錄: 某些 Resolve MCP 伺服器附帶自己的 mlx-whisper 轉錄功能,但上述的 whisper.cpp 步驟已在此流程中得到驗證。
片段類型
以下所有內容在新影片中已不再使用(保留以便重新剪輯舊影片)。請使用下一節中的場景內類型。全螢幕類型會繪製一個純色墨水背景,帶有柔和的強調色光暈,因此它們會完全遮蓋講者。角落標籤位於左下角。
| 類型 | 用途 | data |
|---|---|---|
full-dots | 已廢棄,改用 dots | lit、pct、count |
full-flow | 已廢棄,改用 flow | label、steps[]、good |
full-compare | 已廢棄,改用 compare | title、left{label,value,sub}、right{...,hi}、vs |
full-bars | 已廢棄,改用 bars | title、bars[{label,value,value_label,hi}] |
full-math | 已廢棄,改用 math | parts[](運算子 x = + → 以強調色渲染)、sub |
full-pack | 已廢棄,改用 bars 或 side | name、note、size 0-1、hi |
full-stat | 已廢棄,改用 pair | label、value、sub、hi |
full-title | 已廢棄,改用 pair 或 type | title(允許 <br>)、size、sub |
full-list | 已廢棄,改用 list2 | title、items[]、step 秒 |
chip | 已廢棄,改用 side 或 num | num、title、sub、hi |
chip-stack | 已廢棄,改用 list2 | items[] |
已廢棄的類型仍然可以渲染,用於重新剪輯舊影片。
場景內類型(預設)
不使用卡片,文字位於講者頭部旁的空間中,大型標題環繞頭部。
| 類型 | 用途 | data |
|---|---|---|
pair | 重點結論標題:頭部上方為外框線條,前額高度為實心線條 | top、bottom、outline(top/bottom) |
side | 頭部旁邊的 2-3 行簡短文字,每行在其對應詞語出現時顯示 | lines[]、accent(索引,大號 + 強調色)、at[] |
type | 隨著講者說出而逐詞出現的引言或問題 | words[{w,t,hi,br}] |
define | 已廢棄:會使畫面變暗。改用 side 或 pair | word、sub |
num | 外框數字逐漸出現,旁邊有文字(例如「01 plan the week」) | num、lines[] |
list2 | 在頭部左側和/或右側逐步建立的項目 | left[]、right[]、at[](按順序,先左後右) |
場景內圖表(相同牆面位置和遮罩):
| 類型 | 用途 | data |
|---|---|---|
dots | 每 100 個中的比率,網格填充在講者旁邊 | lit、pct、count |
flow | 流程,步驟由上而下串聯,最後一個使用強調色 | label、steps[]、at[](可選) |
bars | 依大小排列的兩到三個數字 | title、bars[{label,value,value_label,hi}] |
compare | 兩個選項,頭部兩側各一個 | left{label,value,sub}、right{...,hi} |
math | 沿牆面閱讀的方程式,每行由運算子引導 | parts[]、sub |
at 和 t 是從片段起始算起的秒數;根據單字時間碼建立它們,以便每一行都精準落在對應的詞語上。
這些需要 FOOTAGE=,即沒有疊加圖層的剪輯(從 Resolve 渲染時間軸,並停用疊加圖層片段)。渲染器會讀取每個片段的臉部框和背景亮度網格,以便文字找到空曠的牆面,如果文字超出畫面則翻轉側邊或縮小,並在淺色背景上使用深色文字,在深色背景上使用淺色文字。它會將講者從每個場景內片段(Apple Vision 人物分割、person.swift)的每個影格中遮罩出來,因此標題位於頭部後方,手部可以在圖形前方穿過。編譯一次:swiftc -O person.swift -o person。如果不提供 FOOTAGE,渲染器會回退到帶有黑色文字的全螢幕類型。
生命週期、資料格式和常見問題
重新渲染
再次使用完全相同的 clips.json 運行 render.mjs 是安全的——它會覆蓋輸出目錄中的 .mov 檔案,並重新產生 PLACEMENT.txt 和 .fcpxml 檔案。
要修剪或更改一個片段:在 clips.json 中更改物件,然後使用 ONLY= 僅運行那個片段。要刪除一個片段:從陣列中移除其物件並重新執行完整渲染。要重新排序:更改 id 字首的數字。id 的字母數字排序順序就是 FCPXML 中的匯入順序。
半途添加新類型:需要編輯 render.mjs 和相應的 .hbs 範本,然後重新運行。
data[].around 與經典 pair
"around": false 使 pair 回退到早期的置中雙行佈局。在 pair 上設定 "around": false 會使兩行置中,而不是環繞頭部分散。這對於非常寬的頭部或當頭部太靠近畫面邊緣時很有用,但提詞器文字同時佔據兩側仍然更好。
多個 pair 頭部
對於廣角鏡頭,person.swift 輸出第一個偵測到的臉部。如果講者身體右側有第二個人,他們的臉也會被偵測到但被忽略。對於兩個人的對話鏡頭,你無法同時環繞兩者——請求一個特寫鏡頭來補充,或者使用 full-* 類型。
遮罩故障
人物分割使用 VNGeneratePersonSegmentationRequest,它預測離散的遮罩。如果 person.swift 渲染出全白或全黑的遮罩,這通常是分割失敗——可能是因為鏡頭中長時間沒有人臉。如果你看到分割錯誤,請檢查原始影格:使用者可能正在螢幕分享、展示戴手套的手,或者鏡頭完全沒有人。
對於頭部側面或極度旋轉的頭部,遮罩有時會抖動。在哪些片段需要遮罩的材料處理中,這通常可以忽略,但如果你看到遮罩在語義上不正確,請輸出一個非 person 的對應版本,並手動疊加它。
對抗時間碼偏移
如果使用者說他們「裁剪了開頭一秒」,不要手動調整 clips.json 時間碼。所有時間碼都相對於檔案開始處。正確的做法是:重新渲染乾淨的剪輯,然後在 clips.json 上運行 render.mjs。裁剪會改變 ffprobe 的持續時間、轉錄時間戳以及乾淨剪輯的每個影格,因此需要重新渲染。唯一的捷徑:如果你有原始未裁剪剪輯的轉錄結果,你可以將其裁剪後的音訊與原始音訊進行交叉比對,然後重新對齊並更新 clips.json 中的 start 時間,但這僅在 .mov 仍然完美匹配且你願意手動重新對齊時才可行。對於一個被使用者多次裁剪的影片,這種做法會失敗得很慘。只要有可能,直接透過 MCP 在 Resolve 時間軸上操作。