localizing-video は何をしますか?
外国語の動画を英語に変換します — 同期した吹き替え + 英語字幕 + 翻訳された画面上のテキスト — Apple Silicon上で100%ローカル・無料。熟練したエージェントであれば(yt-dlp、whisper、ffmpeg、シーン検出、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"を試す。HLS形式はDASHがDRM保護されている場合でも動作することが多い)。 - ASR — ただし
yt-dlp --list-subsを先に実行(フットガン参照):手動/作成者トラックはWhisperより無料で優れている場合があり、-origタグは真のソース言語を明らかにし(16分間Whisperする前に「既にターゲット言語」であることを検出)、-orig自動キャプションは無料の第2のASRとして相互検証に使えます。その後 openai-whisperwhisper source.mp4 --model large-v3-turbo --language <zh/ja> --word_timestamps True --output_format all(JSONを保持)。言語を固定してください。(torchにはpy3.14用のwheelがありません → py3.12のvenvからwhisperを実行)。 - 翻訳は自分で行う(エージェントがインループで)— 無料、トーンを考慮、専門用語も一貫。1:1のセグメント↔タイムスタンプマッピングを維持。固有名詞は知識で修正(ASRは誤認識します)。言葉だけでなくジョークを保存 — 駄洒落は直訳では生き残りにくい。同じメカニズムを備えた英語の駄洒落にトランスクリエイトし、字幕で落とし込む(ジョークの主要な伝達手段 — 吹き替えは元の音声をダッキングした上に乗る)。同音異義語の駄洒落はASRでは見えないため(フットガン参照)、OCRでキャプションを取得して見つけます。
- 吹き替え — Kokoro-onnx TTS(フットガン参照)。文チャンクで合成、各チャンクを真のタイムスタンプにアンカー(セグメントごとのドリフト追跡ではありません — 遅延が蓄積します)、タイムフィット、ミックス。吹き替えに合わせてビジュアルのタイミングを再調整 — 単に吹き替えを時計に合わせるだけではありません — 時間密度を参照。
- 字幕 — レターボックスバーに英語を焼き付け、フレーム上には重ねない(フットガン参照)。
- ビジュアルOCR —
ocrmac(Apple Vision)、高密度1fps、重複を時間スパンに統合。 - ビジュアルの文字列は自分で翻訳;中国語のみのボックスに英語をオーバーレイ。
- Mux —
ffmpeg-full(libass)、オーバーレイ → レターボックス → 字幕焼き付け → 吹き替え音声。
フットガン — このスキルが実際に存在する理由
| 落とし穴 | 現実 / 修正方法 |
|---|---|
ffmpegに subtitles フィルターがない | デフォルトのbrew ffmpegはlibassを欠いています。/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg を使用。(スキル: hardsubbing-video) |
TTS = say は平坦/ロボット的 | デフォルトは Kokoro-onnx(無料、ニューラル、py3.14もonnxruntimeでOK)。男性 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。 |
| 英語はZH/JAよりも密度が高く、吹き替えが遅れる | atempo は使わない(>1.25× = ボコーダーアーティファクト)— Kokoroのネイティブspeedで再合成。実行中のカーソルでドリフトを追跡しない(cursor-start 追い上げ):経験的に、密度の高い連続発話では動画途中で 約+5秒蓄積。代わりに:(a) 各発話を真の開始位置にアンカー(=字幕/動画タイムライン)してドリフトが蓄積できないようにする;(b) 隣接するASRセグメントを〜文単位にチャンク化し、長い英語の行が短い隣接行から時間を借用する — セグメントごとのフィッティングは1.9倍の制限で59% の行を切り捨てたが、文チャンク化でそれは約0に減少。アンカーはドリフトを殺すが、オーバーランは殺さない — スロットを超える音声チャンクは次のアンカーと衝突する。オーバーフローのフォールバック順序: (1) 簡潔な翻訳、(2) 〜1.9倍の制限内で高速再合成(「圧縮」 = ネイティブのspeedでの再合成または短いテキスト — atempo ではありません)、(3) その下のスライドをフリーズ延長(時間密度を参照)、(4) 低密度の隣接チャンクから余裕を借用。最終手段:ハードキャップ + 末尾を30msフェード(字幕はそれでも全文を保持)。 |
| 英語字幕が動画の上に巨大にレンダリングされる | SRT→libassのデフォルトは PlayResY=288 → フォント/マージンが約4.4倍にスケール。明示的に PlayResY を設定したASS(パディング後の高さ)を生成。そしてレターボックス(下部バーをパディング、Alignment=2)にすることで、英語がソース自身の焼き込み字幕と衝突するのを防ぐ(それらは除去できない)。 |
| Vision OCRに「カスタムスクリプトが必要」 | いいえ — pip install ocrmac; ocrmac.OCR(f, language_preference=["zh-Hans","en-US"]).recognize() で (text, conf, (x,y,w,h)) が正規化されて返り、原点は左下。 |
| ASRと画面上のキャプションの調整 | 下部のキャプションはナレーションの信頼できる正解 — ASRの相互検証に最適。ただし作成者はギャグキャプションを仕込むことがある(無意味な文字列 ≠ 発話)。ASRをOCRで無闇に置き換えない。差異をフラグし、個別に判断。 |
| ASRが同音異義語の駄洒落を黙って平坦化する | ジョークは同音異義語そのものの場合がある:人工智能 (AI) が 人工痔能 と発音されたり、 搁置(プロジェクトを棚上げ)が 割痔疮(痔の除去)と発音されたり。Whisperは珍しい駄洒落を一般的な単語に「修正」するため、ギャグがトランスクリプトに届かない — そして固有名詞の修正では回復できない。作成者は通常、駄洒落を焼き込みキャプションで綴るため、ここではOCRは相互検証ではなく唯一の信頼できる回復経路であることが多い(綴られた同音異義語の場合、キャプションが優先、上記のギャグキャプションとは逆)。兆候:行が妙に文字通りに読める、または反応/ビジュアルが言葉と合わない → 翻訳前にそのキャプションをOCR。 |
| YouTubeの字幕を確認する前にWhisperを実行する | yt-dlp --list-subs は5秒で完了し、3つの利益がある。(1) <lang>-orig タグ = 動画の真のソース言語。既にターゲット言語(例:en-orig)の場合、パイプライン全体がノーオペレーション — ダウンロード+Whisperの前に停止。(2) 手動/作成者トラック(ソース言語)はしばしばWhisperより優れている:正しい句読点、正しい固有名詞、挿入された[音楽]なし。(3) -orig 自動キャプションは無料の第2のASR — Whisperと差分を取ることで、人間の修正が必要な行を正確に浮き彫りにする。16分の英語トークでの測定:98.2% の単語一致率だったが、2つのエンジンは Arthur Andersen(YT、正解)と 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つのレバーであり、約1.9倍が上限(それ以上は急ぎすぎに聞こえる)。より大きなレバーは:ビジュアルタイムラインは伸縮自在 — 吹き替えに合わせて再調整する。 3つのレバーを、最も安価な順に:
- まず簡潔な翻訳(密度が高い→短く)。 最も密度の高い行を短く言い換える。無料で、視聴者が見るものを失わず、焼き付けられた字幕は完全な意味を保持する(吹き替えが少し削っても)。トーキングヘッドショットの主要なレバー(動く顔をフリーズするとジャダーが出る。Bロールでの再カットは通常、現地化-in-placeの仕事の範囲外)。
- 静的なスライドをフリーズ延長(圧力がかかる場所で時間を稼ぐ)。 吹き替えが最も速くなるのは、ナレーターがスクリーンショット/スライドを読み上げているまさにその瞬間 — そしてそのフレームは静的なので、自然な間で少し長く保持して時間を稼ぐ。3つの条件を尊重すればほぼ見えない:(i) タイムライン編集である — 後続のすべてがホールド分シフトするため、フリーズを挿入し、保持されたタイムラインに対して字幕/吹き替え/オーバーレイのタイミングを再生成する(または全てのトラックを同時に保持してmuxする)、元の時間にアンカーしない;(ii) オーディオベッドも一時停止している場合のみフリーズ — 連続する音楽/部屋の音/元の音声の上でのホールドはベッドを非同期にする(拡張/クロスフェードするか、ベッドがダッキング/無音になっている吹き替え下でのみフリーズ);(iii) 「静的」は真に静的でなければならない — カーソル、点滅するキャレット、レーザーポインター、ウェブカメラのインセットは錯覚を壊す。多数の小さなホールドを行い、1つの長い引き伸ばしは決してしない(数秒のホールドはストール/バッファリングのように読まれる)。
- 低密度の部分から余裕を回収(逆 — 低密度→高密度)。 イントロ、サインオフ、スローパンには余裕がある。そこで吹き替えを休ませるか、その余裕を密度の高い部分に前方借用する。
なぜこれがうまくいくか:密度とフリーズ可能性は相関する。 ナレーションが最も密度が高いのはスライドを説明しているから — そしてスライドこそがフリーズ可能なものだ。したがって、時間的プレッシャーの大部分は吸収できる場所に集中する傾向がある。測定し、仮定しない: 各秒をフレーム間の動き(ダウンスケールグレースケールでの mean|frame − prev|)で静的とマーク — しかし閾値は動画ごとのキャリブレーションが必要(コーデックノイズ、ビットレート、fps、グレインがすべて動かす。ある動画では約4/255)。そして全フレーム平均は局所的な動きを逃す(カーソル/キャレット/ポインター)→ キャプションバンドをマスクし、全フレームではなく静的な領域をチェック。次に、静的マスクを高速吹き替えチャンクと交差させて、どれだけのプレッシャーがフリーズ可能かを確認する。ある10分の解説動画の測定では、穏やかな1.4倍の上限では約14秒の回収が必要で、その74%がフリーズ可能な静的スライド上にあった(26%はフリーズ不可能なトーキングヘッド上 → より簡潔な表現)— しかしこの分割は動画ごとに異なる。ソースのランタイムを超えることは、スタンドアロンファイルでは問題ない。何も外部と同期していない。
オペレーターに提起すべき判断ポイント
- ビジュアル範囲: ターゲット(中国語のみ、推奨) vs フル(記事段落も含む — スクロールで劣化)。
- 吹き替え音声: どの無料Kokoro音声。有料(ElevenLabs)は、彼らが課金禁止制約を緩和した場合のみ提供。
- 音声: 完全置換 vs 吹き替え下で元の音声をダッキングしたまま。
- ペーシングの労力: 速い吹き替えを受け入れる(字幕が完全な意味を運ぶ) vs. 穏やかな上限のためにリタイミングする(簡潔な表現 + スライドフリーズ延長 — 測定 + 再muxのコストがかかる)。測定値を提供して、彼らが決定できるようにする。
確認(信頼しないで — 確認する)
最終的なmp4をフレームサンプル(JPGを読む)で次の箇所で確認:ナレーションの瞬間、スライド/オーバーレイの瞬間、英語クリップのギャップ、最後の行 — 字幕同期、オーバーレイ配置、レターボックス、A/Vの長さ一致を確認。ピクセルは永久に焼き付く。特に密度の高いスライドの瞬間をサンプリング(吹き替えが速くなる場所)— そこが同期/ドリフトが最初に壊れる場所。そのタイムスタンプでの焼き付けられた中国語キャプションが同期のオラクルとなる。(自分で音声を audition することはできない — 同期はアンカータイムラインを介して構造的 — そう言って、最終マイルのリスニングをオペレーターに依頼する。)
組み合わせ可能なスキル
hardsubbing-video(ffmpeg/libassの焼き付けフットガン — このスキルのステージ8)。