localizing-video
將外語影片轉換為英語版本 — 同步配音 + 英語字幕 + 翻譯後的螢幕文字 — 在 Apple Silicon 上 100% 離線/免費。一個有能力的代理可以自行推導整個流程(yt-dlp、whisper、ffmpeg、scene-detect、mux);這個技能存在的意義在於工具選擇和那些推導容易踩錯的陷阱,以及經過實證的參考實作。
參考實作(從這裡開始)
此儲存庫提供了可運行的完整管道:engine/ 是通用的可執行引擎(build_dub.py — 以區塊為錨點、無漂移(請參見配音陷阱)、make_ass.py、dense_ocr.py、dedup_spans.py、render_overlay.py、run.sh);example/ 是一個完整的範例資料集。請閱讀 engine/SETUP.md + example/README.md,然後執行引擎 — 不要從頭開始撰寫。每個影片唯一需要重新處理的工作是產生 en.srt + translations.json(翻譯步驟)。引擎預設輸入為 1920×1080 解析度。
管道及各階段的決策
- 下載
yt-dlp(遇到 403 錯誤時嘗試--extractor-args "youtube:player_client=web_safari,ios,tv";當 DASH 受 DRM 保護時,HLS 格式通常有效)。 - 語音辨識 (ASR) — 但先執行
yt-dlp --list-subs(參見陷阱):一個手動/創作者提供的字幕軌可能比 Whisper 更好且免費,-orig標記會揭露影片的真實原始語言(避免在對已是目標語言的影片執行 Whisper 浪費時間),而一條-orig自動字幕則是一個免費的第二 ASR 用於交叉驗證。接著使用 openai-whisper:whisper source.mp4 --model large-v3-turbo --language <zh/ja> --word_timestamps True --output_format all(保留 JSON)。務必鎖定語言。(torch 沒有 py3.14 的 wheel → 請在 py3.12 的虛擬環境中執行 whisper。) - 自行翻譯字幕(由代理以循環方式處理)— 免費、貼近語境、術語一致。保持 1:1 的片段↔時間戳映射。利用知識修正專有名詞(ASR 常聽錯)。保留笑點,而不只是字面意思 — 雙關語通常無法經由字面翻譯存活;重新創作一個具有相同機制的英文雙關語,並在字幕中呈現笑點(字幕是笑話的主要載體 — 配音會壓低原始音量)。同音雙關語在 ASR 中是不可見的(參見陷阱),因此請透過 OCR 辨識字幕來找到它們。
- 配音 — Kokoro-onnx TTS(參見陷阱)。以句子為單位合成,將每個區塊錨定到其真實時間戳(而不是逐段追逐漂移 — 這會累積延遲),進行時間調整,混音。調整視覺內容以配合配音,而不僅僅是讓配音去配合時鐘 — 請參見時間密度。
- 字幕 — 將英文字幕燒錄在信箱式黑色區域中,而非畫面上方(參見陷阱)。
- 視覺 OCR —
ocrmac(Apple Vision),密集掃描(1fps),去重後得到時間區間。 - 自行翻譯螢幕文字;將英文覆蓋在僅含中文的區域上。
- 封裝 —
ffmpeg-full(含 libass),依序進行:覆蓋層 → 信箱式黑邊 → 字幕燒入 → 配音音訊。
陷阱 — 這個技能存在的真正原因
| 陷阱 | 現實 / 修正方式 |
|---|---|
ffmpeg 沒有 subtitles 濾鏡 | 預設的 brew ffmpeg 缺少 libass。請使用 /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg。(技能:hardsubbing-video) |
TTS = say 聽起來平淡/機械 | 預設使用 Kokoro-onnx(免費、神經網路、透過 onnxruntime 支援 py3.14)。男性聲 am_michael 等。 |
Kokoro 靜默失敗:phontab ... No such file | 捆綁的 espeak 資料路徑錯誤。brew install espeak-ng,然後在腳本中覆寫 espeakng_loader.get_data_path/get_library_path 指向 /opt/homebrew/share/espeak-ng-data + /opt/homebrew/lib/libespeak-ng.dylib。 |
| 英語發音資訊密度高於中文/日語 → 配音會延遲 | 不要使用 atempo(>1.25× 會產生聲碼器偽影)— 改用 Kokoro 原生 speed 參數重新合成。不要使用遊標追逐漂移(cursor-start 追趕法):實證上在密集的連續語音中,它會在中段累積約 +5 秒的延遲。解決方案:(a) 將每個語句錨定到其真實起始時間(= 字幕/影片時間軸),這樣漂移就無法累積;(b) 將相鄰的 ASR 片段合併成約等於句子的語句,這樣較長的英文句子可以從較短的鄰居那裡「借用」時間 — 逐片段的調整在 1.9× 上限下截斷了 59% 的句子;而合併成句子後這個數字降至約 0%。錨定消滅了漂移,但非超時 — 一個語句的音訊長度超過其時間槽時,仍會與下一個錨點衝突。超時容錯順序: (1) 更簡潔的翻譯、(2) 在約 1.9× 上限內以更快的速度重新合成(「壓縮」= 原生 speed 重新合成或更短的文字 — 不是 atempo)、(3) 凍結延伸其下的投影片(參見時間密度)、(4) 借用相鄰低密度區塊的空閒時間;最後手段,設定硬上限並將尾部淡出 30 毫秒(字幕仍承載完整句子)。 |
| 英文字幕渲染在畫面上方,字體超大 | SRT→libass 預設使用 PlayResY=288 → 字型和邊距被放大了約 4.4 倍。請輸出含明確 PlayResY 的 ASS = 加上黑邊後的高度。並且使用信箱式黑邊(在底部填充一個黑條,Alignment=2),這樣英文字幕就不會與來源影片本身已燒錄的字幕衝突(你無法移除那些)。 |
| 視覺 OCR "需要自訂腳本" | 不需要 — pip install ocrmac;使用 ocrmac.OCR(f, language_preference=["zh-Hans","en-US"]).recognize() 即可回傳 (text, conf, (x,y,w,h)) 標準化座標,原點在左下角。 |
| 比對 ASR 與螢幕字幕 | 底部字幕是旁白權威性的真實文字 — 絕佳的 ASR 交叉驗證。但創作者會埋入搞笑字幕(亂碼 ≠ 台詞);永遠不要盲目地用 OCR 取代 ASR。標記出差異處,逐一判斷。 |
| ASR 靜默地抹平了同音雙關 | 笑點就是這個同音字:例如 人工智能(AI)被說成 人工痔能("hemorrhoid-tech"),或 擱置("shelve the project")被說成 割痔瘡("hemorrhoid removal")。Whisper 會將罕見的雙關「校正」為常見詞,所以笑點永遠無法進入轉錄稿 — 而且沒有任何專有名詞修正能挽回。創作者通常會在內嵌字幕中拼寫出這個雙關,所以在這裡 OCR 不是交叉驗證 — 它通常是唯一可靠的恢復途徑(對於拼寫出的同音雙關,字幕是正確的,這與上面搞笑字幕的情況相反)。徵兆:某一行讀起來特別直白,或者觀眾的反應/畫面與台詞不符 → 在翻譯前先對那行字幕進行 OCR。 |
| 在檢查 YouTube 自己的字幕之前就執行 Whisper | yt-dlp --list-subs 只需 5 秒,卻能帶來三種好處。(1) <lang>-orig 標記 = 影片的真實原始語言;如果它已經是你的目標語言(例如 en-orig),那麼整個管道就是無操作 — 在下載和執行 Whisper 之前就停止。(2) 手動/創作者提供的字幕軌(原始語言)通常比 Whisper 更好:有正確的標點、正確的專有名詞、沒有插入的 [music]。(3) 一條 -orig 自動字幕是免費的第二 ASR — 將其與 Whisper 的輸出進行比對,可以精確找出需要人工修正的臺詞。在一個 16 分鐘的英文演講中測量:單字一致性高達 98.2%,然而兩個引擎在 Arthur Andersen(YouTube 正確)vs Anderson(Whisper 錯誤)上產生分歧,並且兩者都搞錯了一個品牌名稱(Saily → "Saylite"/"Saley")— 交叉驗證可以標記這些錯誤;單獨任何一個引擎都無法發現。陷阱: --list-subs 中列出的其他 100 多種語言是 YouTube 的自動翻譯,不是創作者提供的字幕 — 不要將那裡的 zh-Hant 行誤認為是真正的中文來源。 |
| 覆蓋所有中文區域 | 在評論影片中,螢幕上的許多「內容」(文章、嵌入的演講、推文)已經是英文,只是疊加了中文 — 再覆蓋一次是多餘且雜亂的。請將範圍限縮在僅中文元素。 |
| 在滾動文字上使用靜態覆蓋 | 基於每個區塊(平均位置)的覆蓋無法追蹤連續滾動 — 中文部分會保持未覆蓋狀態。需要逐幀 OCR 加上框體追蹤;通常不值得(那些內容通常是已經處理過的英文內容)。 |
| 保留原始英文片段的可聽性 | 當來源影片播放真實的英文片段(如訪談/示範)時,請在間隔處以完整音量傳遞原始音訊;僅在配音下方將其降低(約 0.1)。 |
| 在新影片上重複使用腳本 | 請調整腳本,但檢查是否有硬編碼的長度或路徑。render_overlay.py 曾從一個硬編碼的幀數推導覆蓋層持續時間 — 在較長的影片上,覆蓋層會在中途靜默停止。請從 len(glob('f1/f_*.png')) 推導持續時間,而不是使用常數。 每個影片需要重新翻譯 en.srt + translations.json;索引配對的斷言檢查加上並排比對抽查可以捕捉翻譯/按鍵錯位。 |
時間密度:重新調整時間軸,而不只是調整配音
英語的資訊承載速度大約是普通話/日語的 1.5~2 倍,因此忠實的配音在密集的來源影片上總是會爭搶播放時間。壓縮配音(使用原生 speed 參數)是一個手段,但它在大約 1.9× 之前聽起來會很匆忙。更大的槓桿是:視覺時間軸是有彈性的 — 重新調整它以配合配音。 三個槓桿,按成本從低到高排列:
- 首先採用更簡潔的翻譯(密集→更短)。 將最密集的句子改寫得更短。免費,不會損失觀眾看到的任何資訊,而且如果配音稍微修剪了一點,燒入的字幕仍然承載完整含義。這是 talking-head 鏡頭的主要槓桿(你無法在不產生抖動的情況下凍結移動的臉部;使用 B-roll 重新剪輯通常超出本地化任務的範圍)。
- 凍結延伸靜態投影片(在壓力大的地方爭取時間)。 配音最急促的時候,正是 narrator 在朗讀螢幕截圖/投影片的時候 — 而那個畫面是靜態的,所以在一個自然的停頓處將它延長一小段時間來爭取空檔。這幾乎是隱形的,前提是你遵守三個條件:(i) 這是一個時間軸編輯 — 所有後續內容都會因這個停頓而延後,所以請插入凍結並根據調整後的時間軸重新產生字幕/配音/覆蓋層的時序(或在封裝時一併調整所有軌道),而不僅僅是將它錨定到原始時間;(ii) 僅在音訊背景也暫停時才進行凍結 — 在連續的音樂/環境音/原始音訊上進行凍結會使背景音訊不同步(請延伸/交叉淡入處理它,或僅在配音下方進行凍結,此時背景音訊已被壓低或靜音);(iii) 「靜態」必須是真正的靜態 — 畫面中的游標、閃爍的插入符號、雷射筆或網路攝影機畫面都會破壞這個幻覺。請進行多次小凍結,永遠不要一次長時間拖曳(幾秒鐘的凍結會被視為停頓/緩衝)。
- 從低密度區域回收空閒時間(反向操作 — 低密度→高密度)。 開場、片尾、慢速平移都有多餘空間;讓配音在這些地方呼吸,或將空閒時間往前借給密集的段落。
為什麼這很有效:密度和可凍結性相關。 Narrator 的旁白最密集,正是因為他在解釋投影片 — 而投影片正是可以被凍結的。所以大部分的時間壓力往往會落在你可以吸收它的地方。測量它,不要假設它: 透過逐幀的畫面運動(在縮小灰階圖上計算 mean|frame − prev|)來標記每一秒是否為靜態 — 但閾值需要針對每個影片校準(編碼器噪點、位元率、幀率、顆粒感都會影響它;在某一個影片中約為 4/255),而且整幀平均值會錯過局部運動(游標/插入符號/滑鼠指標)→ 請遮罩掉字幕區域並檢查一個靜態區域,而不是整個畫面。然後將靜態遮罩與快速配音的語句區塊進行比對,以了解有多少壓力是可以透過凍結來解決的。在一個測量過的 10 分鐘解說影片中,在 1.4× 的冷靜上限下需要回收約 14 秒,而其中 74% 落在可凍結的靜態投影片上(26% 落在無法凍結的 talking-head 上 → 需要使用更簡潔的措辭)— 但這個比例因影片而異。對於一個獨立的檔案而言,超過來源影片的執行時間是可以接受的;沒有外部時鐘需要與它同步。
需要與操作者討論的決策點
- 視覺範圍: 目標性(僅中文,建議)vs 全面性(也包含文章段落 — 在滾動時會降級)。
- 配音聲音: 選擇哪個免費的 Kokoro 聲音;僅在操作者放寬禁止付費的限制時提議付費選項(ElevenLabs)。
- 音訊: 完全取代 vs 保留原始音訊並在配音下方降低音量。
- 節奏處理: 接受快速的配音(字幕承載完整含義)vs 重新調整時間軸以達到冷靜的上限(更簡潔的措辭 + 凍結延伸投影片 — 需要測量一次並重新封裝)。提供測量數字供操作者決定。
驗證(不要相信 — 親眼查看)
對最終的 mp4 檔案(讀取 JPG)進行幀取樣,時間點包括:一個旁白時刻、一個投影片/覆蓋層時刻、一個英文片段間隔,以及最後一行 — 確認字幕同步、覆蓋層位置、信箱式黑邊,以及音/影片長度匹配。像素是永久燒錄進去的。特別要對一個 DENSE-投影片時刻進行取樣(即配音最急促的地方)— 那是同步/漂移首先崩潰的地方;該時間戳上已燒入的中文字幕就是你的同步判準。(你無法自己試聽音訊 — 同步是結構性的,透過錨定的時間軸來保證;請直接這樣說,並要求操作者做最後的聽覺檢查。)
與以下技能搭配使用
hardsubbing-video(關於 ffmpeg/libass 燒錄陷阱的處理 — 本技能的第 8 階段)。