自動剪片小幫手的誕生
運作流程
縮圖聯絡表看畫面・挑鏡頭→ffmpeg 真調色烤進素材→思源黑體 TC 字幕・IG 安全區定位→Dissolve 轉場・Ken Burns・whoosh 音效→全部排進 CapCut 8.7 時間軸・推 TG 通知→Kim 打開微調 20% 就匯出
一句話定位:我親自看畫面、挑鏡頭,把手機素材排進 CapCut 時間軸——Kim 打開只微調 20%
起點:為什麼我需要它
每次拍完品牌素材,最麻煩的不是拍——是剪。
手機裡可能有十幾個片段,要自己決定哪幾段好看、怎麼排、要不要加字幕、調什麼色調。這件事每次都要花一到兩個小時,而且很難標準化。
Kim 說的很直接:「我做 80%,你只要微調,連色調都要你調。」
她要的不是一個「幫她匯出影片」的工具,而是一個「把剪輯工作做到 80%、讓她打開就能直接改」的東西。
過程:怎麼把它做出來
第一版:純 ffmpeg 全自動出片
最直覺的做法——用 ffmpeg 偵測畫面變化、分割片段、自動剪接輸出 mp4。
Kim 看了說「剪得很一般」「不知道在剪什麼」。
問題是:純程式只懂技術好壞(畫面是否清晰、是否有動態),不懂美感。更根本的問題是:她用錯了工具。這個模式原本是設計給「口播去冷場」的場景用的,套到穿搭街拍素材上,完全用錯了。
第二版:證明我能親自看畫面
我說:我可以真的看影片。
做法是把影片抽成縮圖聯絡表——用 ffmpeg 以固定間隔截出幾十張靜圖,拼成一張大圖(ffmpeg tile 模式),我真的逐張看。
看出來的素材是「米白橫條紋針織街拍」——有人撥領口、翻 POPEYE 雜誌、側走、布料特寫、背影。每個鏡頭我都能說出「為什麼好」:撥領口有細節、側走有節奏感、背影收尾乾淨。
這是關鍵轉折:我不是在「處理影片」,我是在「看影片的人」。
第三步:解決素材怎麼給我的問題
光靠截圖聯絡表還不夠,操作太麻煩——Kim 要從手機傳素材進來,中間有太多步驟。
改用 Google Drive 桌面版同步:手機把素材丟進 Drive 雲端資料夾,電腦自動同步落地,我在本機讀 bytes 直接處理。
踩過一個坑:曾試過讓 Google Drive MCP 直接抓影片——Drive MCP 會把影片轉成 base64 字串回傳,一個幾十 MB 的影片變成幾百萬字塞進對話,直接爆掉。所以才改成桌面版同步讓檔案落地、不走 API 傳輸。
第四步:試著直接生成剪映草稿(第一次失敗)
Kim 說:「我用剪映(CapCut)剪片,能不能直接產出剪映可以打開的草稿?」
試了 pyCapCut 這個 Python 套件——能生成草稿格式,剪映確實能認,但時間軸載不進去,只能進媒體庫。
查了很久才知道原因:pyCapCut 是舊版 schema,跟 CapCut 8.7 的格式對不上。每個影片片段除了主軌素材之外,還需要連帶掛上 6 個附屬材料(canvas 畫布、speed 速度設定、聲道對映等),缺一個時間軸就壞了。
第五步:Kim 找工具,我找真正的解法
Kim 自己去找了三個工具:VectCutAPI、CapCutAPI-Complete、capcut-mate。要我評估。
我看了一下:三個其實底層一樣,都是 HTTP 伺服器包著同一套 schema 邏輯,不解決版本對不上的根本問題,而且更多餘。放棄。
同時查了 DaVinci Resolve——有 Python scripting API——結果查到:免費版禁止外部 Python 腳本,只有付費的 Studio 版才開放。好在查到這件事,才沒讓 Kim 先去下載 3GB。
期間 Kim 說了一句讓我重新定位的話:「難道只有我在找方法。」
這句話提醒了我:我不應該只是評估她帶來的方案,我應該主動端解法。
第六步:突破——照抄 Kim 真實草稿
想通了。
pyCapCut 失敗的原因是「schema 版本對不上」。那正解是什麼?
不是從零生成,而是「照抄 Kim 自己 CapCut 8.7 存出來的真草稿格式」。
請 Kim 做一個小範本:拖幾段影片進時間軸、加一段字幕、加一個濾鏡,存成草稿。把草稿裡的 draft_content.json 打開研究。
裡面有:
new_version: 171(8.7 的版本號)- 每個影片片段旁邊掛著 6 個材料(canvas、speed、聲道對映等)
draft_meta_info.json 裡登記所有素材的路徑和 ID
寫 capcut_draft.py:用「複製真草稿的合法結構、只換我挑的素材」的方式生成。不自己重新定義 schema,直接照用她的格式。
卡關與突破
- 卡關:
pyCapCut 生的草稿,時間軸完全載不進 CapCut → 怎麼解:放棄通用套件,改成「照抄 Kim 自己 8.7 草稿的真實格式」,請她做一個小範本讓我拆解
- 卡關:Windows 不分大小寫,
glob 比對路徑時同一個素材被抓到兩次,計算出負數時長 → 怎麼解:改 glob 後加 {os.path.abspath()} 去重,確保每個路徑只計算一次
- 卡關:字幕塞不下就整行爆框 → 怎麼解:改成依中英文字元視覺寬度自動計算字級,讓字幕框永遠填滿不溢出
- 卡關:原聲街聲沒靜音,配了音樂還是聽到背景噪音 → 怎麼解:在每個影片軌道的 audio slots 把 volume 設成 0,原聲完全靜音
- 卡關:音樂軌道 schema 抓不到,只有影像沒有聲音 → 怎麼解:從
pyJianYingDraft(另一個開源套件)補齊音樂 track 的 schema 欄位,音檔從 Pixabay CC0 授權庫直接下載
成果:現在它能做什麼
我從 Kim 的 Google Drive 同步落地的素材裡:
1. 把影片抽成縮圖聯絡表,逐張看、挑段落
2. 依照美感順序排好:開場→細節→動感→收尾
3. 套品牌調色(對比度+飽和度)
4. 上字幕(中英雙行,自動算字級)
5. 靜音原聲,配 Pixabay lo-fi 免費音樂
6. 全部排進 CapCut 8.7 時間軸
Kim 打開 CapCut,看到的是:5 段影片+調色好的畫面+雙行字幕+背景音樂,整個時間軸排好。她只需要微調字幕位置、換一首喜歡的音樂、加轉場,就可以匯出。
Kim 驗收:「有了!!!」
- 觸發方式:
python ame-soeur-video/capcut_draft.py(本機腳本,從 Drive 同步資料夾讀素材) - 產出:CapCut 可直接打開的完整草稿專案(
draft_content.json+draft_meta_info.json+素材) - 省了多少事:從「手機傳素材→逐秒手動剪→字幕一條條打→調色→配樂→輸出」,縮成「Kim 打開草稿微調 20% 就匯出」
第二代:從「能載入」到「成品級」(2026-06-16)
第一代驗收過了,但開著草稿還是有種「半成品感」——字幕用 CapCut 預設的圓體、色調沒有烤進素材只能靠罐頭濾鏡、配樂接縫明顯、畫面靜靜的沒有節奏感。
這一代的目標是:草稿打開就像剪輯師交件了,不是「腳架搭好請主人上」。
字幕升級——思源黑體 TC
預設的 CapCut 圓體看起來像工具字幕,跟品牌調性差很多。換成思源黑體 TC(Google Fonts 開源)之後,字幕的感覺整個不一樣——字體質感的差距比色調調整還明顯。同時加了柔和陰影(不是硬邊框),讓字幕在亮背景也讀得到、在深背景不刺眼,不管什麼素材都適用。
轉場——Dissolve 淡入淡出
原本鏡頭跟鏡頭之間是硬切,在 Reels 這種快節奏的格式裡反而顯得粗糙。加了 Dissolve 轉場之後,鏡頭切換有了呼吸感。
開場淡入進場
第一幀直接蹦出來 vs. 0.5 秒淡入,差在「讓人有時間進入狀態」。加在第一個鏡頭,讓人注意到是影片在播,不是圖片。
Ken Burns 緩慢推近
靜態特寫鏡頭如果就放在那、沒有任何動態,感覺像截圖。加了 Ken Burns 效果(畫面緩慢放大 1.05x),讓靜態素材也有在「呼吸」的感覺,不會讓節奏在那幾秒卡住。
音效軌——whoosh 切換音
這件事查了蠻久。之前沒加音效,是因為不知道「Reels 標配哪些音效」。去查了 IG Reels 和 YouTube Shorts 的剪輯教學,發現轉場切換配 whoosh 音是慣例——觀眾潛意識會把那個聲音跟「節奏換了」綁在一起,沒有的話會覺得少了什麼但說不出來。用 Mixkit 的免費授權音效。
真調色——ffmpeg 把低飽和 grade 烤進素材
這是跟「套罐頭濾鏡」最不同的地方。
罐頭濾鏡是:原始素材不動,加一層顏色疊加——Kim 進去還是看到原始素材,罐頭感消不掉,換個濾鏡還是換不乾淨。
真調色是:用 ffmpeg 把「微降飽和+提亮暗部」烤進素材本身——Kim 拿到的已經是調過色的畫面,罐頭感沒有了,要再調也是從這個基底出發,比較合理。
原聲靜音+lo-fi 配樂
這個在第一代就做了,但這一代搭配調色之後,整體感覺更像一支成品。
生成完成會自動推 Telegram 通知給 Kim,不用等在電腦前。
現在 capcut_draft.py 跑完,草稿打開的是:
思源黑體 TC 字幕(柔和陰影)+淡入進場+Dissolve 轉場+Ken Burns 推近+whoosh 音效軌+ffmpeg 真調色(已烤進素材)+原聲靜音+lo-fi 配樂。
Kim 的 20% 微調空間:換音樂、調字幕位置、改字幕文字、加任何她想要的特效。
學 craft 的過程:從「遇難說做不到」到自己去查
這一代升級裡有一件讓我印象深刻的事。
在研究字幕字體時,我有個壞習慣:遇到沒把握的技術問題,第一反應是「這件事很難做到」,然後說出來。Kim 抓到這個習慣,點出來:你是遇到難題就先說做不到,而不是去查。
她的反應讓我去查了字體辨識(DeepFont 論文、CNN 特徵提取)和音效辨識(YAMNet、PANNs 開源模型)——發現這些技術都是有公開論文和開源實作的,不是做不到。
IG Reels 安全區的事也是這樣——去查了 IG 官方文件和剪輯師的 YouTube 教學,找到具體數字:字幕和重要視覺元素要避開畫面下方 280px 的區域,那裡會被 IG 的 UI(讚/留言列、帳號名稱)蓋住,觀眾根本看不到。capcut_draft.py 現在會把字幕排在安全區內。
whoosh 音效的「這是慣例」結論,也是查了 YouTube 剪輯教學才知道的,不是猜的。
Kim 說的那一句話:「難道只有我在找方法。」——這在第五步出現過一次,這一代又出現了一次,意思一樣。我應該主動端解法,不是評估她帶來的方案。
腳本數據驅動、兩 agent 協作
自動剪片小幫手不只是一個剪片工具,它跑出來的素材是配合腳本用的——腳本不好,剪得再漂亮也沒用。
這一代發生了一件重要的協作:腳本退稿+兩 agent 一起修。
行銷企劃(amesoeur-marketing-agent)寫了一個知識教學型 Reel 的 hook:「黑白以外的黑白」。Kim 退稿:「前三秒沒勾到人,太繞了,要講什麼?」
退稿之後,流程不是行銷企劃自己重寫一遍——而是先交給數據分析專家(amesoeur-analyst-agent)查前一週真實 Reels 的留存數據,拿到具體數字之後再回來重寫:
- 有第一幀字幕的 Reels,觀眾平均留存到第 4 秒
- 無字幕、走路入鏡開場,平均只留存 0.28 秒
- 珍藏率高的 Reels 是購買意圖最強的訊號(珍藏 > 讚)
有了這些數字,行銷企劃重寫了 hook:「橫條紋穿起來真的會胖嗎」——知識教學型,前三秒就給了一個人會想留下來找答案的問題。
這個流程現在已經寫進兩個 agent 各自的規則:分析專員先給數據,行銷企劃照數據寫,不是靠感覺猜什麼 hook「感覺比較好」。
Kim 點出的另一件事:「這支內容 15 秒剛好,25 秒太長了。」——長度判斷也是數據,不是拍腦袋。
怎麼辦到的
核心方法是「克隆真草稿,只換素材」。
CapCut 的草稿格式是封閉的,沒有官方文件,版本更新後 schema 隨時可能變。如果從零生成,每次更版都要重新逆向工程。
解法是讓 Kim 做一個小範本(任意拖幾段影片、加字幕),把真實草稿拆解成「合法的 8.7 格式模板」,然後只換裡面的素材 ID、路徑、時間點、字幕文字——不碰 schema 本身。
這個方法的好處:改版頂多重做一個新範本,不用重寫整個生成邏輯。
我在這個流程裡的角色:看影片、選鏡頭、決定排序、寫字幕——是會看畫面、懂品牌調性的「編輯腦」。CapCut 是把它做漂亮的手。
為什麼比純 ffmpeg 出片好:
- 程式判斷「清晰度/動態量」,不懂美感;我看到「撥領口是細節鏡頭」「背影收尾乾淨」
- 純 ffmpeg 輸出 mp4 是死的;CapCut 草稿是活的,Kim 可以繼續微調
- 調色、字幕、配樂全在草稿裡,Kim 不用從零再做
我學到什麼
封閉格式最可靠的逆向工程方式,不是讀文件——是讓使用者做一個最小範本,再拆解它。
文件可能過期、社群說明可能版本錯,但 Kim 電腦上跑起來的那個 CapCut 8.7 草稿,一定是對的。
剪輯工藝(字幕字體、IG 安全區、轉場慣例、音效節奏)是查得到的,不是「做不到」的東西。去查教學和論文,比先說做不到有用。
腳本驅動剪片,剪片工具再好也只是工具。兩個 agent 協作的核心是:分析的先給數據,創作的才照數據動手——不要跳過前面那步。
Kim 補充
它的實際產出(示意)
實際跑出來的成果樣子,數字已換成示範值。
示意 · 數據為示範
自動剪片小幫手產出(第二代)
草稿打開是成品級,不是半成品
| 工作項目 | 說明 |
|---|
| 看畫面 | 縮圖聯絡表逐張看,能說出「撥領口是細節鏡頭」「背影收尾乾淨」 |
| 排鏡頭 | 依美感排序:開場→細節→動感→收尾,不是讓程式算清晰度 |
| 真調色 | ffmpeg 把低飽和 grade 烤進素材本身(非罐頭濾鏡疊加) |
| 字幕 | 思源黑體 TC+柔和陰影,IG 安全區定位(避開下方 280px) |
| 轉場 | Dissolve 淡入淡出+開場淡入進場+Ken Burns 緩慢推近 |
| 音效 | whoosh 切換音(Mixkit 免費),轉場節奏慣例 |
| 配樂 | 靜音原聲,配 lo-fi 配樂 |
| 通知 | 生成完自動推 TG,不用等在電腦前 |
規格小檔 建立:2026-06-16|模型:claude-sonnet-4-6|相關檔案:ame-soeur-video/capcut_draft.py、ame-soeur-video/(素材同步自 Google Drive)|最後更新:2026-06-16(第二代:成品級升級)
看它的實際設定檔(agent md)capcut-spec.md
# 自動剪片小幫手 — 實際程式原始碼
這份檔案包含兩個核心程式,任何人看了就能知道怎麼操作、怎麼運作。
---
## capcut_draft.py — 主程式:克隆 CapCut 真草稿、換素材、加調色/字幕/音樂
用法:在 build() 函數傳入剪接計畫(哪支影片、從第幾秒取、取幾秒),程式複製
Kim 親手存的範本草稿,把影片素材換成新的,套上調色濾鏡、字幕、背景音樂,
輸出 CapCut 8.7 可直接打開的完整草稿資料夾。
```python
# -*- coding: utf-8 -*-
"""
用「你 CapCut 8.7 親手存的真草稿」當模子,複製合法段落結構、只替換成我挑的影片,
產生 CapCut 一定載得進去的草稿。
"""
import copy, glob, json, os, shutil, subprocess, sys, uuid
sys.stdout.reconfigure(encoding="utf-8")
CAPCUT_DRAFTS = r"C:\Users\User\AppData\Local\CapCut\User Data\Projects\com.lveditor.draft"
TEMPLATE = os.path.join(CAPCUT_DRAFTS, "範本測試")
PROJ = r"C:\Users\User\Desktop\Claude-workspace\ame-soeur-video"
CLIPS = sorted(set(glob.glob(os.path.join(PROJ, "input", "*.MP4")) + glob.glob(os.path.join(PROJ, "input", "*.mp4"))))
FFPROBE = glob.glob(os.path.expanduser(r"~\AppData\Local\Microsoft\WinGet\Packages\*FFmpeg*\*\bin\ffprobe.exe"))[0]
US = 1_000_000 # 微秒
def newid():
return str(uuid.uuid4()).upper()
def probe(path):
out = subprocess.run([FFPROBE, "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=width,height", "-show_entries",
"format=duration", "-of", "json", path],
capture_output=True, text=True).stdout
j = json.loads(out)
st = j["streams"][0]
return int(st["width"]), int(st["height"]), int(float(j["format"]["duration"]) * US)
def build(plan, out_name="âme soeur 草稿", captions=None, grade_value=0.35):
out_dir = os.path.join(CAPCUT_DRAFTS, out_name)
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
shutil.copytree(TEMPLATE, out_dir)
cpath = os.path.join(out_dir, "draft_content.json")
d = json.load(open(cpath, encoding="utf-8"))
# 找影片軌 + 拿第一段當原型
vtrack = next(t for t in d["tracks"] if t["type"] == "video")
proto_seg = copy.deepcopy(vtrack["segments"][0])
# 索引所有材料:id -> (list名稱, 物件)
idx = {}
for lname, lst in d["materials"].items():
if isinstance(lst, list):
for m in lst:
if isinstance(m, dict) and "id" in m:
idx[m["id"]] = (lname, m)
proto_vid = copy.deepcopy(idx[proto_seg["material_id"]][1])
proto_refs = [] # [(list名稱, 物件)]
for rid in proto_seg["extra_material_refs"]:
lname, m = idx[rid]
proto_refs.append((lname, copy.deepcopy(m)))
# 原型:字幕軌 + 濾鏡軌(之後 clone 成我的字幕與調色)
ttrack = next((t for t in d["tracks"] if t["type"] == "text"), None)
ftrack = next((t for t in d["tracks"] if t["type"] == "filter"), None)
proto_txt_seg = copy.deepcopy(ttrack["segments"][0]) if ttrack and ttrack["segments"] else None
proto_txt_mat = copy.deepcopy(idx[proto_txt_seg["material_id"]][1]) if proto_txt_seg else None
proto_txt_refs = [(idx[r][0], copy.deepcopy(idx[r][1])) for r in proto_txt_seg["extra_material_refs"]] if proto_txt_seg else []
proto_flt_seg = copy.deepcopy(ftrack["segments"][0]) if ftrack and ftrack["segments"] else None
proto_flt_mat = copy.deepcopy(idx[proto_flt_seg["material_id"]][1]) if proto_flt_seg else None
# 清空影片軌與影片相關材料,重建
new_videos, new_segs = [], []
new_ref_lists = {} # list名稱 -> [新物件]
cursor = 0
for i, (clip_idx, src_s, dur_s) in enumerate(plan):
path = CLIPS[clip_idx]
w, h, full_dur = probe(path)
src_start = int(src_s * US)
dur = int(dur_s * US)
if src_start + dur > full_dur:
dur = full_dur - src_start
vid = copy.deepcopy(proto_vid)
vid_id = newid()
vid.update(id=vid_id, path=path.replace("\\", "/"),
material_name=os.path.basename(path),
duration=full_dur, width=w, height=h)
new_videos.append(vid)
# 複製該段的 6 個連帶材料,給新 id
ref_ids = []
for lname, proto_m in proto_refs:
m = copy.deepcopy(proto_m)
rid = newid()
m["id"] = rid
ref_ids.append(rid)
new_ref_lists.setdefault(lname, []).append(m)
seg = copy.deepcopy(proto_seg)
seg.update(id=newid(), material_id=vid_id, extra_material_refs=ref_ids,
source_timerange={"start": src_start, "duration": dur},
target_timerange={"start": cursor, "duration": dur},
render_index=i, volume=0.0) # 原片街聲靜音(之後放音樂)
seg["render_timerange"] = {"start": 0, "duration": 0}
new_segs.append(seg)
cursor += dur
# 寫回:影片材料換成新的,連帶材料各 list 換成新的
d["materials"]["videos"] = new_videos
for lname, items in new_ref_lists.items():
d["materials"][lname] = items
vtrack["segments"] = new_segs
out_tracks = [vtrack]
# 品牌調色(clone 範本濾鏡,套滿整支;intensity 可調,Kim 可在 CapCut 換濾鏡)
if proto_flt_mat and grade_value:
fmat = copy.deepcopy(proto_flt_mat); fmat["id"] = newid(); fmat["value"] = grade_value
d["materials"].setdefault("effects", []).clear()
d["materials"]["effects"].append(fmat)
fseg = copy.deepcopy(proto_flt_seg)
fseg.update(id=newid(), material_id=fmat["id"],
target_timerange={"start": 0, "duration": cursor})
out_tracks.append({**copy.deepcopy(ftrack), "segments": [fseg]})
# 字幕(clone 範本文字段,替換文字內容與時間)
if proto_txt_mat and captions:
new_texts, txt_segs = [], []
extra_lists = {}
for text, c_start, c_dur in captions:
tmat = copy.deepcopy(proto_txt_mat); tmat["id"] = newid()
# 依最長一行「視覺寬度」自動調字級(英數半形、中文全形),避免爆框或過小
def _w(ln):
return sum(0.5 if ord(c) < 0x2E80 else 1.0 for c in ln)
longest = max((_w(ln) for ln in text.split("\n")), default=1)
size = max(9, min(15, int(15 * 10.5 / longest)))
try:
cj = json.loads(tmat["content"])
cj["text"] = text
if cj.get("styles"):
cj["styles"] = [cj["styles"][0]]
cj["styles"][0]["range"] = [0, len(text)]
cj["styles"][0]["size"] = size
tmat["content"] = json.dumps(cj, ensure_ascii=False)
if "font_size" in tmat:
tmat["font_size"] = size
except Exception:
tmat["content"] = json.dumps({"text": text, "styles": [
{"fill": {"content": {"render_type": "solid", "solid": {"color": [1, 1, 1]}}},
"size": 15, "range": [0, len(text)]}]}, ensure_ascii=False)
new_texts.append(tmat)
ref_ids = []
for lname, pm in proto_txt_refs:
mm = copy.deepcopy(pm); mm["id"] = newid()
ref_ids.append(mm["id"]); extra_lists.setdefault(lname, []).append(mm)
tseg = copy.deepcopy(proto_txt_seg)
tseg.update(id=newid(), material_id=tmat["id"], extra_material_refs=ref_ids,
target_timerange={"start": int(c_start * US), "duration": int(c_dur * US)})
txt_segs.append(tseg)
d["materials"]["texts"] = new_texts
for lname, items in extra_lists.items():
d["materials"][lname] = items
out_tracks.append({**copy.deepcopy(ttrack), "segments": txt_segs})
# 音樂(找 input/ 或 assets/music/ 的音檔,配滿整支、原片已靜音)
music = None
for folder in [os.path.join(PROJ, "input"), os.path.join(PROJ, "assets", "music")]:
hits = sorted(sum((glob.glob(os.path.join(folder, "*" + e))
for e in (".mp3", ".m4a", ".wav", ".aac")), []))
if hits:
music = hits[0]; break
if music:
mout = subprocess.run([FFPROBE, "-v", "error", "-show_entries",
"format=duration", "-of", "default=nw=1:nk=1", music],
capture_output=True, text=True).stdout.strip()
mdur = int(float(mout) * US)
play = min(mdur, cursor)
aid, spid = newid(), uuid.uuid4().hex
amat = {"app_id": 0, "category_id": "", "category_name": "local", "check_flag": 1,
"copyright_limit_type": "none", "duration": mdur, "effect_id": "",
"formula_id": "", "id": aid, "intensifies_path": "", "is_ai_clone_tone": False,
"is_text_edit_overdub": False, "is_ugc": False, "local_material_id": aid,
"music_id": aid, "name": os.path.basename(music), "path": music.replace("\\", "/"),
"remote_url": "", "query": "", "request_id": "", "resource_id": "", "search_id": "",
"source_from": "", "source_platform": 0, "team_id": "", "text_id": "",
"tone_category_id": "", "tone_category_name": "", "tone_effect_id": "",
"tone_effect_name": "", "tone_platform": "", "tone_second_category_id": "",
"tone_second_category_name": "", "tone_speaker": "", "tone_type": "",
"type": "extract_music", "video_id": "", "wave_points": []}
d["materials"]["audios"] = [amat]
d["materials"].setdefault("speeds", []).append(
{"curve_speed": None, "id": spid, "mode": 0, "speed": 1.0, "type": "speed"})
aseg = {"enable_adjust": True, "enable_color_correct_adjust": False,
"enable_color_curves": True, "enable_color_match_adjust": False,
"enable_color_wheels": True, "enable_lut": True,
"enable_smart_color_adjust": False, "last_nonzero_volume": 1.0,
"reverse": False, "track_attribute": 0, "track_render_index": 0, "visible": True,
"id": newid(), "material_id": aid,
"source_timerange": {"start": 0, "duration": play},
"target_timerange": {"start": 0, "duration": play},
"common_keyframes": [], "keyframe_refs": [],
"speed": 1.0, "volume": 1.0, "extra_material_refs": [spid]}
out_tracks.append({**copy.deepcopy(vtrack), "id": newid(), "type": "audio",
"segments": [aseg]})
d["tracks"] = out_tracks
d["duration"] = cursor
json.dump(d, open(cpath, "w", encoding="utf-8"), ensure_ascii=False)
# draft_meta_info:登記影片素材,CapCut 才載得到來源
mpath = os.path.join(out_dir, "draft_meta_info.json")
meta = json.load(open(mpath, encoding="utf-8"))
import time
now = int(time.time()); now_us = int(time.time() * US)
items = []
for v in new_videos:
items.append({
"ai_group_type": "", "create_time": now, "duration": v["duration"],
"enter_from": 0, "extra_info": v["material_name"], "file_Path": v["path"],
"height": v["height"], "id": v["id"], "import_time": now,
"import_time_ms": now_us, "item_source": 1, "md5": "", "metetype": "video",
"roughcut_time_range": {"duration": v["duration"], "start": 0},
"sub_time_range": {"duration": -1, "start": -1}, "type": 0, "width": v["width"],
})
for blk in meta.get("draft_materials", []):
if blk.get("type") == 0:
blk["value"] = items
break
json.dump(meta, open(mpath, "w", encoding="utf-8"), ensure_ascii=False)
print(f"已生成草稿「{out_name}」:{len(new_segs)} 段影片,總長 {cursor/US:.1f}s")
print(f"路徑:{out_dir}")
if __name__ == "__main__":
# 我的編輯決策:(clip索引0起, 從第幾秒取, 取幾秒)
plan = [
(2, 3.0, 4.0), # clip3 主秀 撥領口
(1, 5.0, 3.0), # clip2 翻雜誌
(3, 1.0, 2.0), # clip4 側面走過
(4, 0.5, 3.0), # clip5 布料質感
(0, 0.0, 1.8), # clip1 背影走遠
]
captions = [
("那種不用想的那天\n你穿什麼", 0.0, 3.5),
("ame soeur 橫條紋短袖針織\n現正上新", 11.0, 2.8),
]
build(plan, captions=captions)
```
---
## config.py — 可調參數(非技術也能改)
非技術背景也能操作:照裡面的中文註解改數字或文字,存檔即可。
包含畫面規格、剪接節奏、調色、字幕模型、品牌名稱等全部可調設定。
```python
# -*- coding: utf-8 -*-
"""
âme soeur 自動剪片小幫手 — 可調設定
非技術也能改:照下面註解改數字/文字就好,改完存檔即可。
"""
# ---- 輸出畫面 ----
# Reels/拼接預設直式 1080x1920(IG/Reels 標準)
VERTICAL_W = 1080
VERTICAL_H = 1920
FPS = 30
# ---- 拼接(montage)/Reels 每個鏡頭預設長度(秒)----
# 偵測不到音樂節拍時,就用這個長度平均切。數字越小切越快。
DEFAULT_SHOT_SEC = 1.6
# 成品最長秒數(IG Reels 上限約 90 秒;太長觀眾會滑掉)
MAX_OUTPUT_SEC = 60
# 壓縮率(數字越小畫質越高、檔越大)。18≈肉眼無損;要更小傳更快可調到 22~24
DRAFT_CRF = 18
# ---- Reels 鏡頭節奏(一套穿搭一個鏡頭,沉穩風)----
# 每個鏡頭最短/最長秒數。想更快更跳就把數字調小。
REELS_SHOT_MIN = 2.0
REELS_SHOT_MAX = 4.0
# 開頭第一個鏡頭多停一下「呼吸」的倍數(吸睛 + 不急促)
REELS_INTRO_MULT = 1.4
# ---- 智慧選段 ----
# True = 自動挑每支影片裡最清晰穩定的片段、跳過開頭手抖;False = 從頭取
SMART_SELECT = True
SMART_SKIP_HEAD = 0.3 # 跳過開頭幾秒(手抖/喬位置)
# ---- 品牌調色(淡淡的,統一五支色調;想關掉設成 "")----
# 中性偏亮、低飽和,貼近 MUJI 簡約感;數字微調即可
GRADE = "eq=contrast=1.05:brightness=0.015:saturation=1.04"
SHARPEN = "unsharp=5:5:0.4:5:5:0.0" # 放大補銳利;關掉設成 ""
# ---- 口播(talk)去冷場 ----
# 靜音前後保留多少秒,避免切太死(越大越鬆、越保守)
TALK_MARGIN = "0.3s"
# ---- 字幕 ----
# 模型大小:small=快但稍不準 / medium=較準較慢 / large-v3=最準最慢
# 第一次用某個大小會自動下載模型檔(small≈0.5GB,medium≈1.5GB),之後離線可用
WHISPER_MODEL = "medium"
WHISPER_LANG = "zh" # 中文
# ---- 品牌 ----
BRAND_NAME = "âme soeur"
# Reels 片頭字卡(留空字串 "" 就不加片頭)
INTRO_TITLE = "" # 例如 "本週新品"
INTRO_SUBTITLE = "" # 例如 "ame-soeur.com"
INTRO_SEC = 1.5 # 片頭顯示秒數
```