Communitygithub.com

yojuhsu/video-cut

從 macOS 照片圖庫自動找素材、用 GPS 與時間還原行程、剪成短片並上字幕配樂的 Claude Code skill / Turn your macOS Photos library into edited short videos

¿Qué es video-cut?

video-cut is a Claude Code agent skill that 從 macOS 照片圖庫自動找素材、用 GPS 與時間還原行程、剪成短片並上字幕配樂的 Claude Code skill / Turn your macOS Photos library into edited short videos.

Compatible conClaude Code~Codex CLI~Cursor
npx skills add yojuhsu/video-cut

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

影片剪輯產線

從幾萬個檔案裡找出該剪哪些,剪成片,上字幕,配樂。

市面上的剪片工具都假設「你已經有一支片,告訴我怎麼剪」。這支 skill 補的是前面那段: 素材在哪、哪些能用、怎麼分群


鐵則

  1. 渲染完要抽格看過。 程式沒報錯不等於畫面對。CLAUDE.md 第七原則在這裡沒有例外。
  2. 每輪只修 1 到 3 個最大落差。 看整支,講出最嚴重的那個,改完重新排序,不要照舊清單往下做。
  3. 素材數字從圖庫實際查。 「大概有幾百支」這種話會害你選錯版面和長度。
  4. 合成前逐段驗證。 渲染偶爾產出截斷的 mp4,moov atom not found,重跑該段就好。

五個步驟

#做什麼產出
1掃圖庫、去重、分群、抓地名assets.json、分群表
2抽格做檢視表,用眼睛選片*_v.jpg*_p.jpg
3寫剪輯表edl.json
4渲染 → 抽格看 → 修 → 再看clips/*.mp4
5合成、配樂、輸出out/*.mp4

步驟 2 和 4 是這支 skill 的重點。選片和驗收都要靠眼睛,不能靠檔名和時長猜。


開工前的環境檢查

第一次在一台機器上跑,先確認:

for f in drawtext subtitles ass zscale overlay zoompan gblur; do
  printf "%-12s " $f; ffmpeg -hide_banner -filters 2>/dev/null | grep -qw "$f" && echo 有 || echo 沒有
done
ffmpeg -hide_banner -version | grep -c -- --enable   # 官方完整版 60+,精簡版 15 左右
python3 -c "import PIL; print('Pillow', PIL.__version__)"
command -v exiftool sips

如果沒有 drawtextsubtitles:不要重裝 ffmpeg 才動工。改用 Pillow 產字幕 PNG 再 overlay 疊上去,中文排版反而更好控制。scripts/render.py 就是這樣寫的。

如果沒有 zscale:做不了正規的 HDR→SDR tone mapping。iPhone 的 HLG 素材直接轉 8-bit 通常可以接受(實測藍天飽和、雲有層次、亮部不過曝),先做出來看,不要為這件事卡住。要更準的顏色再 brew reinstall ffmpeg


步驟 1:掃素材

python3 scripts/scan_library.py --from 2026-06-10 --to 2026-06-22

它會複製 Photos.sqlite、查出區間內的資產、去重、分群、解出地名,寫成 assets.json 並印出分群表。

四個必踩的坑

.mov 的時間戳是 UTC,照片是當地時間。 QuickTime 的 CreateDate 存 UTC,HEIC 的 DateTimeOriginal 存拍攝地當地時間。 混在一起排序,影片會全部跑到早上,順序整個亂掉。用 --tz 指定拍攝地時區。

Live Photo 佔了影片檔的九成以上。 originals 裡的 .mov 有 96% 是 Live Photo 的 2–3 秒動態部分(檔名以 _3.mov 結尾)。 按副檔名抓片會剪出一團碎片。查 ZASSET.ZKIND=1 才抓得到錄影。

重複檔案。 同行的人 AirDrop 過來的素材可能被匯入兩次。用 (拍攝時間秒, 時長×100, 原始檔案大小) 三者相同判定重複。 實測一趟 13 天的行程裡有 80 個重複檔,沙丘那組 16 支影片有 5 組是同一支存兩份。

地名不要打外部 API。 照片圖庫本機的 ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA 是 NSKeyedArchiver plist, 裡面有 Apple 已經解好的地名。用 plistlib 讀,不用把使用者的 GPS 座標送出去。 鄉下地區可能只到「某某省」,這時候要問使用者實際地名。


步驟 2:選片

做檢視表,看過再選。

python3 scripts/sheets.py videos assets.json --out camp   # 影片,長片自動抽 3 格
python3 scripts/sheets.py photos assets.json --out camp   # 照片,可 --sample 3 抽樣

每格標了編號、時間、時長,編號跟 assets.json 的索引一致,直接拿來寫 EDL。

選片時要看的

  • 主角是誰。 常常不是風景。一隻被帶著走的玩偶、一台新車、一隻站在手臂上的鷹, 這些重複出現的東西才是片子的骨架。
  • 有沒有畫中畫。 iPhone 的雙擷取會在角落放一個自拍視窗, 橫拍在左上(約寬 27%、高 38%),直拍在右上(約寬 35%、高 25%)。 這會毀掉「無人臉版」,但不必整支放棄,用 crop 裁掉即可,見下面。
  • 長鏡頭要掃時間軸。 一支 96 秒的片,主體位置會變。 掃 8 個時間點,挑主體在裁切線右邊(或下方)的那一段,就能零損失地裁掉畫中畫。
  • 照片的 EXIF 方向。 sips 轉檔不會自動套用,縮圖會出現倒立和橫躺。 一定要走 PIL.ImageOps.exif_transpose

步驟 3:寫 EDL

edl.json 是一個陣列,一個物件一格。

欄位意思
clip / photo素材編號,對應 assets.json 的索引
ss / dur從原片第幾秒開始、取幾秒(照片只要 dur
zoom放大倍率,把直拍素材在橫式版面裡放大。1.0 不放大
crop[左, 上, 右, 下] 各裁掉的比例,用來去掉畫中畫
zdir照片的 Ken Burns 方向,1 推近、-1 拉遠。相鄰兩張用相反方向
place資訊卡,一到三行 ["06.13 05:47", "地點", "天氣"]
sub動作字幕,底部置中
poem詩詞。純 list 就是詩句;要署名用 {"lines":[...], "by":"唐・王績〈野望〉"}
audiotrue 保留這格的原聲。預設全部靜音,只有需要聽現場聲音的段落才開

浮水印走環境變數,不必寫進每一格:

MARK="yoursite.com ; edited by Claude" VW=1920 VH=1080 ... python3 render.py

小字半透明,貼在畫面最下緣,跟動作字幕分層不會撞。 | title | 片頭標題 ["主標", "副標"],第三個元素給 "top" 可貼上方,避免壓到畫面正中的主體 | | end | 片尾卡 ["主標", "副標", "說明"],會加半透明黑底 |

長度怎麼抓

用途長度每格格數
Threads/IG 循環15 秒1.8–2.2 秒8
一般短片60–90 秒2.5–3.5 秒25–30
完整記錄100–130 秒2.4–6 秒35–40

15 秒循環是另一種東西,不是把長片剪短。 它沒有時間鋪陳,要靠一個重複的動作 當引擎(同一個主角、背景一直換),首尾用同一個視覺元素收口,接回去就看不出接縫。 字幕最多一行放在最後,loop 時它會變成節拍點。


步驟 4:渲染與驗收

VW=1920 VH=1080 CDIR=clips EDL=edl.json CONCAT=concat.txt CARDS=cards python3 scripts/render.py
VW=1920 VH=1080 CDIR=clips EDL=edl.json CONCAT=concat.txt CARDS=cards python3 scripts/render.py 3 12 21   # 只重跑指定段

全部的環境變數:

變數預設意思
VW / VH1080 / 1920輸出尺寸。16:9 用 VW=1920 VH=1080
CDIRclips片段輸出目錄
EDLedl.json剪輯表檔名
CONCATconcat.txtconcat 清單檔名
CARDScards照片卡片目錄
VIDEOSvideos.json影片清單。全照片的片子可以沒有這個檔
WORKDIR當前目錄工作目錄,一般不用設
MARK浮水印文字,設了就每格都疊在畫面最下緣

所有輸出都相對於「執行時的當前目錄」,不是腳本裝在哪裡。 所以可以把 skill 裝在 ~/.claude/skills/cd 到專案目錄再呼叫它。 片段落在 ./clips/,字幕圖在 ./ovl/

出 9:16 直式版:VW=1080 VH=1920。同一份 EDL,兩個版面都出得來。

合成前逐段驗證:

cd clips && for f in *.mp4; do
  ffprobe -v error -show_entries format=duration -of csv=p=0 "$f" >/dev/null 2>&1 || echo "壞: $f"
done

抽格看畫面(這步不能跳):

python3 scripts/sheets.py check out.mp4 edl.json --out chk

每格抽中點,拼成檢視表。看完講出最嚴重的 1–3 個問題,改完重新排序。

版面怎麼選

橫拍和直拍混在一起的時候,實際渲染兩版比一比,不要用猜的。判準是哪一邊的主體損失小

  • 主角特寫多半是直拍 → 9:16 滿版
  • 風景和地標多半是橫拍 → 16:9 滿版
  • 少數派用模糊背景填滿,或用 zoom 放大裁切

zoom 放太大會切掉主體。直拍在 16:9 裡放到 1.2–1.3 通常安全,超過 1.5 要看過再決定。


步驟 5:配樂與輸出

消原聲配音樂(原聲多半是風聲和雜訊,削弱效果):

MUS="/Applications/iMovie.app/Contents/Resources/iMovie Theme Music/SIMPLE2.m4a"
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy -an muted.mp4
D=$(ffprobe -v error -show_entries format=duration -of csv=p=0 muted.mp4)
FO=$(python3 -c "print(round(float('$D')-4,2))")
ffmpeg -y -i muted.mp4 -stream_loop -1 -i "$MUS" \
  -filter_complex "[1:a]atrim=0:$D,asetpts=PTS-STARTPTS,afade=t=in:d=2.5,afade=t=out:st=$FO:d=4,loudnorm=I=-19:TP=-2:LRA=11[a]" \
  -map 0:v -map "[a]" -c:v libx264 -preset slow -crf 23 -pix_fmt yuv420p \
  -c:a aac -b:a 160k -ar 48000 -shortest -movflags +faststart out.mp4

只留幾段原聲(現場演奏、有人說話)

在那幾格的 EDL 加 "audio": true,其他格自動靜音。合成時讓音樂在那幾段降下去:

DUCK="between(t,74.5,79.0)+between(t,81.5,85.5)"   # 從 EDL 累加算出來的區間
ffmpeg -y -i raw.mp4 -stream_loop -1 -i "$MUS" -filter_complex "\
[1:a]atrim=0:$D,asetpts=PTS-STARTPTS,volume=0.50,\
volume=enable='$DUCK':volume=0.22,\
afade=t=in:d=2.5,afade=t=out:st=$FO:d=4[m];\
[0:a]volume=2.0[o];\
[m][o]amix=inputs=2:duration=first:normalize=0,alimiter=limit=0.92[a]" \
  -map "[a]" -c:a aac -b:a 160k -t $D audio.m4a

實測原聲段會比周圍音樂大 4–5 dB,聽得很清楚。

分三步做,不要一次到底。 118 秒的 1080p 影片用 preset medium 壓縮要五分鐘, 一次做完常常撞到逾時而產出截斷的檔案:

  1. -an 只壓影片
  2. 單獨混音訊成 .m4a
  3. -c copy 合併

這樣任何一步失敗都能單獨重跑,不用整支重來。

保留全部原聲(現場演奏、有人說話的片子)時,各段音量落差常有 20 dB, 用 loudnorm 會把安靜的段落硬抬成噪音。改用 dynaudnorm 做動態正規化:

-af "dynaudnorm=f=250:g=7:p=0.75:m=8,afade=t=in:d=1.0,afade=t=out:st=$FO:d=2.5,alimiter=limit=0.92"

實測七段 -23 到 -43 dB 的原聲,這樣拉平到 -16 到 -17 dB。

保留原聲就不能切太碎。 現場音樂被剁成 3 秒一段會很難聽。 這種片子改成只切 2 到 3 段、每段 8 到 9 秒,接點靠淡入淡出軟化。

音樂哪裡來

/Applications/iMovie.app/Contents/Resources/iMovie Theme Music/ 有七首, Apple 授權可用於自己的作品。/Library/Audio/Apple Loops/ 有上千個 loop,同樣授權。 不要從網路抓來路不明的音樂。

沒辦法聽的時候用數據選:

ffmpeg -hide_banner -i "$f" -af "highpass=f=4000,volumedetect" -f null - 2>&1 | grep mean_volume
ffmpeg -y -i "$f" -filter_complex "showwavespic=s=900x120" -frames:v 1 wave.png

高頻能量越低音色越柔和,波形起伏越平穩越不搶戲。有規律脈衝的是舞曲,背景音樂不要選。


三種字幕,不要只有一種

只有日期地點會很乾。混著用:

類型放哪寫什麼
資訊卡 place段落開頭,左下時間、地點、天氣
動作字幕 sub有動作的鏡頭,底部置中白話、有畫面感、帶一點情緒
詩詞 poem遠景空鏡,底部偏上references/poems.md

動作字幕怎麼寫:講畫面上正在發生的事,不要抒情。 「牧民騎馬而來,一整排」「駱駝站起來的瞬間,會先往前傾」「奶茶是鹹的」。

詩詞位置:試過畫面上方三分之一,會壓在地平線和主體上。放底部, 前景多半是草地沙地,最不擋事。逗號改用全形空格,中文詩排起來才像詩。

天氣:陰晴可以從畫面確認,敢寫。溫度查得到的多半是月份氣候平均,不是當天實測, 不要把月平均寫成當天氣溫


片頭與片尾

片頭可以是一格,也可以是「一個 cut 加二到三張關鍵照片」的快切: 第一格帶標題(2 秒),後面三張各 1.2 秒無字,像預告。選能代表全片的三張。

挑主鏡頭時選最有衝擊力的那格,標題疊上去。 挑之前先渲染出來看:遠景的主體在 16:9 裡會變得很小, 直拍縮進橫式版面只剩中間一條,這種當片頭撐不住。

片尾用一個「離開」感的鏡頭(人物遠去、鏡頭上搖),疊半透明黑底加三行卡: 地點、日期、這支片有什麼。


有人臉與無人臉雙版本

需要對外發布時常要兩版。無人臉版不是把有人的鏡頭刪掉就好:

  1. 畫中畫crop 裁掉,見步驟 2
  2. 主畫面裡的人:整段排除,裁切救不回來
  3. 照片也要逐張看:縮圖上看起來是駱駝,放大才發現上面坐著人

做完無人臉版,把用到的每一張照片並排重看一次再交件。實測這步抓到兩張漏網的。

「不要人臉」通常指的是使用者自己和同行的人,當地人的臉多半是可以留的(那是內容本身)。 先問清楚,這決定可用素材是差一倍還是差一半。


純照片也能撐完一支敘事片

條件是素材本身有時間軸。實測一支 44 秒的馬拉松紀錄,一支影片都沒用到 (賽事當天的錄影全是 0 到 7 秒的碎片),十六格全是照片,但完全成立,因為:

  • 里程牌是天然的節奏器。6 MILE、21 MILE、35 KM、38 KM 一格一個, 推進感自己會出來,不需要旁白解釋「跑了很久」。
  • 表情變化是天然的弧線。天亮前的笑臉到 38 公里的自拍,中間什麼都不用說。
  • 時間戳當字幕04:21 15:15 17:51 直接寫進句子開頭, 十一個小時的跨度用兩個數字就講完了。

判斷素材夠不夠:找得出五個以上有時間標記的節點(里程牌、路標、時鐘、 日出日落、餐點)就可以,找不出來就別硬做,那會變成配樂幻燈片。

節奏要快慢交替,不是整體加快

想加快不要全片壓短,字幕格壓短會讓長句讀不完。改成兩種格分開處理:

類型每格說明
字幕格2.2–5.0 秒依字數給時間。中文約每秒 5 字,30 字就要 5 秒
快切格1.2–1.4 秒無字幕的路人、空景、連拍。眼睛掃過就好

快切段插在敘事段之間,四到八格一組。慢—快—慢—快—慢的起伏比等速好看, 而且快切段最好落在故事本身也緊湊的地方。

橫拍素材混進 9:16 的快切段要加 zoom(2.0 以上), 不然一半滿版一半細條,眼睛會一直被打斷。

結尾的順序:先回答,再留餘韻

如果片中出現過問句或懸念,結尾要先回答它,再收在餘韻上,順序不能反。

實測那支馬拉松:倒數第四格是「結束了,真的嗎?」,緊接著給終點拱門和獎牌 (回答「真的」),最後才收在曬傷加上「隔年春天都還有印記」。

如果收在獎牌,那就跟所有完賽影片一樣。收在曬傷,時間被拉到半年後, 才是這支片獨有的東西。但沒有前面的回答,餘韻會變成沒交代。

封面圖另外出一張

社群平台自己抓的縮圖多半是第一格,而第一格常常是鋪陳用的暗畫面,滑過去不會停。 從 cards/ 挑一張最有識別度的,用 Pillow 疊上大標題另存 JPG:

  • 主標放比賽或地點名,副標放日期
  • 最有力的數字放最大(成績、天數、距離),用金色
  • 底部保留浮水印

標題保持資訊型就好。如果字幕已經是自嘲或抒情的口氣,標題再跟著走會變成同一個調, 外層資訊、內層情緒,兩層才有對比。

從長片切系列短片

一支長片剪好之後,同一批素材可以再切成幾支 15–20 秒的主題短片。 一支一個主題(早晨、某個活動、某個地點),各自獨立成立,適合連續發。

每支給一張資訊卡開場,全程掛浮水印,不要片頭片尾卡(太短,撐不起來)。

配樂取同一首的不同段落,四支就 0s / 15s / 30s / 45s,淡入 1.5 秒。 這樣連著看不會覺得是同一段音樂重播。

這裡有個會咬人的坑-ss 45 之後音樂只剩 15 秒,配上 -shortest 會把 20 秒的影片砍成 15 秒,而且不報錯。實測中招過一次。 音樂起點靠後時一定要加 -stream_loop -1,並用 -t $D 明確指定長度。 輸出後逐支比對「EDL 總長」和「實際長度」,差超過 0.5 秒就是被截了。

交付

輸出兩個檔:母帶(CRF 19)和上傳版(CRF 23 + faststart)。 超過 30 MB 的檔案傳不進對話,要告訴使用者只能在桌面版看,並附上 open 指令。

留下 edl.jsonREADME.md,說明改哪個檔、怎麼重跑、這台機器有什麼坑。 下次要剪同一趟的另一支,複製整個資料夾改日期就好。

Skills relacionados