基礎影片剪輯(將原始拍攝裁切以符合腳本)
為何有此技能
單次連續拍攝充滿重拍,單靠轉錄稿來正確裁切實際上非常困難。首次手動執行此工作流程時,若信任 Whisper 對單一完整檔案進行的段落時間戳記,會產生一個剪輯計畫,其中某個句子看起來花了 22 秒才說完。實際上只花了 5.75 秒;Whisper 將 16 秒的靜音和兩次假開頭合併到了該段落。信任那份轉錄稿進行裁切,會送出充滿空白的影片。
請記住這個教訓:此工作流程的目標是精確的裁切點,而僅靠 Whisper 無法可靠地為含有停頓、重拍或重複台詞的素材提供這些點。 以下所有內容都是為了彌補這一點而存在。
工作流程
1. 分析來源
在執行任何其他操作之前,先探查影片的基本資訊:
ffprobe -v error -show_entries format=duration,size -show_entries stream=width,height,codec_name,codec_type,avg_frame_rate -of default=noprint_wrappers=0 "<video>"
提取單聲道 16kHz 音訊用於轉錄(Whisper 需要這個格式,且處理起來比起完整影片小得多/快得多):
ffmpeg -y -i "<video>" -vn -ac 1 -ar 16000 audio.wav -loglevel error
如果使用者提供了腳本/劇本檔案(txt、rtf、docx 等),請立即讀取,以便將預期內容與轉錄結果進行比對。.rtf 檔案通常需要 textutil -convert txt -stdout file.rtf(macOS)才能取得純文字而非原始 RTF 標記。
2. 透過首次轉錄取得粗略概覽
whisper audio.wav --language <lang> --model medium --output_format txt --output_format srt --output_dir .
僅限使用此結果來了解整體內容以及不同台詞在錄製中的大致時間位置。不要將其段落開始/結束時間作為真正的裁切點——請參閱上方「為何有此技能」。如果錄製時間較長,此步驟可以與步驟 3 並行執行(或直接跳過,改用步驟 3)。
3. 透過靜音偵測找到真實的語音邊界
這才是真正重要的步驟。執行:
python3 scripts/detect_bursts.py audio.wav > bursts.txt
這會提供樣本級精確的語音「爆發段」(靜音偵測的補集),並過濾掉小於 0.5 秒的短暫爆音(這些幾乎總是呼吸聲而非話語)。請信任這些時間戳記,而非 Whisper 的。
合理性檢查:自然語速大約每秒 2-3 個單詞。如果某個爆發段的持續時間看起來與其中應該包含的文字量不成比例,則表示可能有問題。
4. 獨立轉錄每個爆發段
python3 scripts/transcribe_bursts.py audio.wav bursts.txt --language <lang> --model medium
這會載入 Whisper 模型一次,然後將每個爆發段作為獨立的短片段進行轉錄,使用 condition_on_previous_text=False 和 temperature=0。這個組合才是真正揭露隱藏重拍的關鍵:完整檔案處理時標記為一個乾淨句子的爆發段,在獨立轉錄後可能發現它包含一個假開頭緊接著真正的表達——這些在首次轉錄中是完全不可見的,因為它悄悄地合併或壓抑了重複內容。
預計有明顯重拍的錄音會產生比腳本台詞更多的爆發段——通常對每個台詞,尤其是說話者最卡關的那一句,會產生數個假開頭片段。這是正常的;這正是此步驟的目的。
5. 建立剪輯計畫——在實際剪輯前務必取得確認
將轉錄後的爆發段依其歸屬於腳本的哪個台詞/想法進行分組。對於每個台詞,通常會看到以下情況之一:
- 一個乾淨、完整的爆發段——簡單,就是這個版本。
- 一個假開頭後接一個完整爆發段——使用完整的那個,捨棄片段。
- 多個措辭相同的完整、良好版本——這些是真正的表演重複;無法僅從轉錄稿判斷表演/情感,因此先以合理的預設值(例如較晚的嘗試,因為人們通常會逐漸進入狀態)進行剪輯,並讓使用者在 Dublovi 模式(見下方)中選擇。
- 多個措辭確實不同的完整版本(說話者每次即興方式不同)——這是一個真正的內容選擇,而不僅僅是表演選擇。明確地向使用者呈現這些選項,而不是默默選擇——他們可能更喜歡自己的即興措辭而非腳本,反之亦然。
對於這兩種重複版本的情況,使用者會與您一起透過觀看來決定:這些版本會進入剪輯計畫編輯器的 Dublovi 模式(兩個版本並排,左/右,「Ovaj je bolji」),每個影片都如此。不要在聊天中描述版本,也不要在那裡問「dubl 1 還是 2」。先以您的預設選擇進行剪輯,將每個重複版本列在 takes_src.json(剪輯計畫技能,「Dublovi」)中並開啟編輯器;如果使用者選擇了另一個版本,則在剪輯中替換。
然後將完整的計畫作為訊息寫給使用者:每個台詞保留的具體文字、其精確時間戳記範圍,以及被裁切掉的內容及其原因(空白、假開頭、被捨棄的替代版本)。在執行任何裁切指令之前,等待使用者明確確認。這不是形式——剪輯影片具有破壞性,且不觀看輸出很難檢查,因此使用者需要在渲染時間花費之前看到計畫以發現問題(選錯版本、拼接在一起的台詞聽起來不自然),而不是之後。
如果使用者詢問總時長,只需加總保留片段的時長——但提醒最終實際時長會在加入剪輯間距(步驟 6)後稍長一些。
6. 裁切與組裝
將已確認的選擇(每行一個 start<TAB>end 對,按輸出順序)寫入檔案,然後:
scripts/cut_and_concat.sh "<source_video>" selections.txt final_output.mp4
在自行手動執行或改編 cut_and_concat.sh 之前,請先閱讀其頂部的註解區塊——它記錄了常見陷阱(ffmpeg 從 bash 迴圈中盜用 stdin、淡出會吃掉一個詞的最後一個音節、為何每個片段在拼接前需要各自重新編碼),如果從頭重寫而不是直接使用腳本,這些問題很容易無聲地再次出現。
預設值:每個裁切點前 0.15 秒填充,每個裁切點後 0.35 秒填充,僅 5 毫秒淡入(無淡出),每個片段以 h264 (crf 18) + aac 192k 重新編碼,每個片段精確裁切到指定幀數並將其音訊修剪至相同長度,使用 ffmpeg concat 濾鏡(絕不使用 concat demuxer——請參閱下方的「檢查每個渲染」)進行拼接。如果需要為特定裁切做不同處理(例如,一個帶有較多尾音呼吸聲的台詞可能需要更大的 POST_PAD),可透過 PRE_PAD、POST_PAD、CRF、FADE_IN 環境變數覆蓋。
如果某個句子的最後一個字母在第一次輸出後聽起來仍然被截斷,這幾乎總是意味著該特定裁切的 POST_PAD 太緊(說話者的尾輔音持續時間比靜音偵測器的閾值更長)——增加該片段的填充量,而不是使用淡出,後者只會讓緊湊的裁切聽起來更糟。
緊湊節奏——使用者的標準
預設填充(0.15 秒 / 0.35 秒)在每個裁切處留下約 0.5 秒的停頓。使用者發現這破壞了節奏(「mora biti sve zbijenije i tečnije」),因此對於他們的影片,需在組裝前收緊每個裁切:
python3 scripts/tighten_cuts.py audio.wav selections.txt tight.txt
PRE_PAD=0.04 POST_PAD=0.08 scripts/cut_and_concat.sh "<source_video>" tight.txt final_output.mp4 <abs workdir>
它會將每個邊緣移動到語音實際開始/結束的位置(-42 dB),並分割任何超過 0.30 秒的內部停頓。在一個真實影片上,這將時長從 1:48 縮短到 1:41,且每個單詞完整無缺。之後,透過轉錄結果來檢查:保留文字中的每個單詞都必須仍然存在。放置於舊裁切點上的任何內容(特寫、字幕、標題)的時間都必須透過來源時間重新對映,而非簡單地進行常數偏移。
當兩個分別裁切的部分拼接在一起時(例如主要部分 + 結尾),拼接點應取自第一部分真實的幀數(nb_read_frames / fps),而非其容器持續時間:在一個真實影片上,0.04 秒的差異導致在特寫與結尾之間遺留了一幀廣角鏡頭,且結尾的第一個字幕提前一幀出現。在拼接點結束的疊加層必須執行到該幀。
檢查每個渲染——在送出任何內容之前
在一個真實影片中,使用者在 1:05 處發現了一幀遺留的廣角鏡頭(「to ne sme da nam se dešava」)。真正的原因更大:舊的 cut_and_concat.sh 使用 concat demuxer 拼接片段,該方式根據容器持續時間和 AAC 初始化/填充來放置片段,而不是根據其實際幀數。這在 10 個裁切處留下了 80 毫秒的間隙(2 幀)——重新編碼後的螢幕上出現凍結幀——並且音訊逐漸領先畫面,導致基於它的每個疊加層和字幕時間都偏移了一兩幀。修復後的腳本(其標頭中的陷阱 #4)將每個片段精確裁切到指定幀數,將其音訊填充/修剪到完全相同的長度,並使用 concat 濾鏡進行拼接。
由此衍生的規則:
-
在將每個渲染(裁切、縮放、加字幕、最終版)傳送給使用者之前,對其執行檢查器:
python3 scripts/check_video.py final.mp4 --ass captions.ass --edges edges.txt --cuts joins.txtjoins.txt= 最終時間軸上每個裁切的拼接點(每個部分的durs.txt的累加總和,加上部分邊界)。始終傳遞它:相同畫面內的跳接得分遠低於場景閾值,因此在一個真實影片 (1:15) 中,沒有它,檢查器錯過了一個在拼接點前 3 幀處發生的縮放變化——使用者看到了一個 3 幀的閃爍。它會報告時間戳記間隙、孤立的凍結幀、音訊與影片長度差異、僅持續數幀的鏡頭、短於 0.4 秒或開始/結束於畫面裁切點前後 1-3 幀的字幕,以及離裁切點 2-5 幀的疊加層邊緣(
edges.txt:每行一個特寫 / B-roll 開始或結束時間,單位為秒)。Exit 1 = 發現問題:修復並重新渲染;僅「OK - nothing found」才能送出。 -
每個片段的位置都來自幀數(
nb_frames / fps或腳本寫入的durs.txt),永不來自format=duration。 -
當裁切變更時,透過來源時間(舊時間軸 -> 來源 -> 新時間軸)移動特寫、字幕和標題,而非透過常數偏移。
-
在裁切點結束的疊加層(例如結尾前的特寫)精確執行到該裁切的幀;字幕隨其片段結束。
-
將距離拼接點 6 幀以內的每個縮放變化、特寫結束和字幕邊緣吸附到該拼接點上(僅當拼接點正好在特寫開始之前時,特寫才開始)。相距幾幀的兩個畫面變化總是看起來像故障;而在同一幀上,它們看起來像一個裁切。
-
為最後一個片段提供一個小的尾部(約 0.5 秒),以免最後一個字幕和單詞被截斷。
7. 傳遞
來自 cut_and_concat.sh 的主要輸出為全品質(以 crf 18 重新編碼)。如果需要透過具有檔案大小限制的工具傳遞,則為該目的製作一個次要的壓縮副本,而不是壓縮主要輸出:
ffmpeg -nostdin -y -i final_output.mp4 -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 128k final_output_share.mp4 -loglevel error
如果使用者還想要動態推入/縮放裁切以增加動感,請參閱 zoom 技能——它可以將此技能的輸出作為輸入並重新裁切,或者(更高效地)您可以將 zoom 技能中每個片段的濾鏡鏈直接寫入 selections.txt 的第三欄,然後執行一次 cut_and_concat.sh。如果需要旋轉為垂直畫面,則分別處理(不屬於任一技能)——詢問使用者,因為並非總是需要。
平台注意事項
- macOS 預設沒有 GNU
timeout——不要在 ffmpeg 呼叫外面包一層;對於短片片段它們很快,而且呼叫工具自身的後台/超時處理涵蓋了步驟 2 中長時間運行的完整檔案處理。 - 任何時候您自行撰寫
while read ...; done < file迴圈(而非使用捆綁腳本)並在其中呼叫ffmpeg時,請在 ffmpeg 呼叫中加入-nostdin並將其標準輸入重新導向至/dev/null——否則 ffmpeg 會消費掉原本屬於迴圈read的行,在沒有明顯錯誤的情況下破壞哪個時間戳記屬於哪個迭代。
裁切後:縮放和字幕透過剪輯計畫編輯器進行
當裁切後的影片也將套用縮放/推入(zoom 技能)和/或字幕(subtitle 技能)時,使用者的標準是使用 剪輯計畫編輯器 技能:一個互動式點擊單詞工具,他們在其中與您一起決定縮放,並在同一句子中修正字幕文字(「Izmeni tekst」)。保留此技能計畫中每個片段的保留文字——這將成為該工具的 sentences.txt(每行對應一個裁切片段,順序與 selections.txt 相同)。