wjs-burning-subtitles について
wjs-burning-subtitles は、FFmpeg で動画に字幕や吹き替え音声を合成するスキルです。字幕の焼き込みと切り替え可能な字幕トラックに対応し、字幕、吹き替え、元音声を一度のエンコードでまとめてローカライズ動画を作成します。
主な機能
- 01常時表示字幕の焼き込み
- 02切り替え可能な mov_text 字幕トラックの格納
- 03字幕・吹き替え・元音声の合成
- 04字幕のみ、吹き替えのみの動画出力
活用シーン
- 01字幕トラック非対応のプラットフォーム向け動画作成
- 02対応プレーヤー向けの切り替え字幕追加
- 03翻訳字幕と吹き替えを最終動画に統合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-burning-subtitlesインストール後、Claude Code で/wjs-burning-subtitlesを入力して読み込まれたか確認します。
同梱ファイルscripts/render.py
詳細
wjs-burning-subtitles
動画+SRT → 字幕付き動画。ローカライズパイプラインの最終エンコード段階でもあります。動画、/wjs-dubbing-video の任意の吹き替えトラック、焼き込み用の任意の SRT を受け取り、アップロード可能な MP4 を ffmpeg の1 回のパスで生成します。デコード/再エンコードを連鎖させることはありません。
使用する場合
- SRT があり、動画に字幕を常時表示したい場合(微信视频号/抖音/WeChat への焼き込み。埋め込み字幕トラックが反映されないプレーヤー向け)。
mov_textに対応した QuickTime/VLC/IINA/モバイルプレーヤーで、切り替え可能な字幕トラック(ソフトミックス)を使いたい場合。/wjs-dubbing-videoの後の最終合成:対象言語の字幕を焼き込み、オリジナル音声をベッドとして吹き替え音声とミックスし、1 回のエンコードで仕上げる場合。
使用しない場合
- SRT がまだない場合 → 先に
/wjs-transcribing-audioを実行し、続けて/wjs-translating-subtitlesを実行します。 - HyperFrames で構成したクリップに HTML/CSS 字幕(キネティック字幕、単語ごとのハイライト、カスタムフォント)を付ける場合 → 代わりに
/wjs-overlaying-videoを使用します。同じ出力に libass の焼き込みと HyperFrames の字幕を混在させないでください。 - 「字幕」が実際にはモーショングラフィックス(アニメーション付きコールアウト、ロゴ付きローワーサード、キネティックタイポグラフィ)の場合 → このスキルではなく、
/wjs-overlaying-videoを使用します。
render.py の 3 つのモード
scripts/render.py はフラグからモードを自動判定します。
- 字幕のみ —
--video + --srt→ 字幕を焼き込んで動画を再エンコードし、オリジナル音声はそのまま通します。 - 吹き替えのみ —
--video + --dub→ オリジナル動画ストリームを維持し、音声トラックを置換またはミックスします。 - ローカライズ版の完全なカット —
--video + --srt + --dub→ 字幕を焼き込み、吹き替え音声をミックスします。デフォルトでは、吹き替え音声の下に「ベッド」としてオリジナル音声を小音量で残します(オリジナル音声を削除するには--bed-volume 0または--no-original-audioを指定します)。
焼き込みには libass を含む ffmpeg が必要です。必要な場合、初回使用時にスクリプトが evermeet.cx から libass 対応の静的ビルドを /tmp/ff_bin/ に自動ダウンロードします。
ソフトミックス(切り替え可能な字幕トラック)
プレーヤーアプリで字幕の表示/非表示を切り替えられます。どの ffmpeg ビルドでも動作し、libass は不要です。
ffmpeg -i input.mp4 -i input.zh-CN.srt \
-map 0:v -map 0:a -map 1:0 \
-c:v copy -c:a copy -c:s mov_text \
-metadata:s:s:0 language=zho -metadata:s:s:0 title="中文" \
output.mp4
これは高速(ストリームコピー)で、元に戻せます。以下の場合に使用します。
- 対象プラットフォームが埋め込み字幕に対応している(YouTube は自動検出し、VLC/QuickTime は認識します)。
- 視聴者が字幕をオフにできるようにしたい場合。
- 動画を再エンコードしたくない場合。
render.py --video IN.mp4 --srt SUB.srt --soft-mux でこの処理を実行します。
ハードコードによる焼き込み(常時表示、libass)
埋め込み字幕トラックが反映されない WeChat/抖音/朋友圈 などでは必須です。
焼き込みを約束する前に、libass が利用可能か確認する
ffmpeg -filters 2>&1 | grep -E "subtitles|^.. ass "
subtitles も ass も表示されない場合、そのビルドには libass が含まれていません。Homebrew のデフォルトの ffmpeg フォーミュラは、--enable-libass、--enable-libfreetype、drawtext がなく、機能が削られていることがよくあります。force_style 内のカンマのエスケープに時間をかけても無駄です。シェルでどのように引用符を付けても、No such filter: 'subtitles' で失敗します。
macOS で最も手早く解決する方法 — システムに変更を加えず、静的ビルドを追加する
curl -fsSL -o /tmp/ff.zip https://evermeet.cx/ffmpeg/getrelease/zip
unzip -o /tmp/ff.zip -d /tmp/ff_bin >/dev/null
FF=/tmp/ff_bin/ffmpeg
$FF -version | grep -oE -- "--enable-(libass|libfreetype)"
その後、レンダリングには ffmpeg の代わりに $FF を使用します。brew のバイナリは、それ以外のすべての処理(プローブ、音声抽出、ソフトミックス)には問題ありません。render.py はデフォルトの ffmpeg に libass がない場合、この方法へ自動でフォールバックします。
スタイルを上書きして焼き込みレンダリングする
🛑 チェックポイント — 全体をレンダリングする前に確認してください。 焼き込みでは動画全体を再エンコードします(5 分のクリップでも CPU を数分間使用します)。処理を開始する前に、次の手順を行います。
- すばやくプレビューできるよう、
-t 30を指定して最初の 30 秒だけをレンダリングします。 - 最も長い行のキューからフレームを抽出し(下記のフォントサイズ調整を参照)、読み取ります。
- プレビュー画像とキューのテキストをユーザーに見せ、「字号/字体/边距 OK 吗?OK 才跑全片。」と尋ねます。明示的な承認を待ってください。
同じ会話内で、この動画をこのフォント設定で全体レンダリングすることについて、ユーザーがすでに承認している場合に限り、チェックポイントを省略できます。
$FF -i input.mp4 \
-vf "subtitles=input.zh-CN.srt:force_style='Fontname=PingFang SC\,Fontsize=12\,PrimaryColour=&H00FFFFFF\,OutlineColour=&H00000000\,BorderStyle=1\,Outline=2\,Shadow=1\,MarginL=20\,MarginR=20\,MarginV=40'" \
-c:v libx264 -crf 18 -preset medium -pix_fmt yuv420p \
-c:a copy output.mp4
force_style 内では、すべてのカンマを \, としてエスケープします(フィルターグラフパーサーがエスケープされていないカンマをチェーンの区切りとして処理するためです)。それ以外の特殊文字は問題ありません。
フォントサイズの調整 — 重要
libass は内部の PlayRes を実際の動画解像度に合わせて拡大します。指定する数値は、出力動画のピクセル数ではありません。544×960 の縦向きスマートフォン動画で調整を始めたとき、Fontsize=22 では中国語の各文字が約55px幅で描画され、画面からはみ出しました。一方、Fontsize=12 では約30〜35px幅で描画され、15文字の行がきれいに収まりました。
目安として、まずは Fontsize=12 に設定してレンダリングし、必ずフレームを抽出して確認します。
$FF -ss 30 -i output.mp4 -frames:v 1 /tmp/frame.png -y
# then Read /tmp/frame.png to verify the longest-line cue fits
1行あたりの文字数が最も多い字幕キューが表示されるタイムスタンプを選んでください。短い行では、はみ出しが起きても分かりません。安全のため MarginL=20 MarginR=20 を追加してください。左右のデフォルト余白は信用しないでください。
スタイルの早見表
重要なキー(libass の force_style):
Fontname=PingFang SC— macOS のデフォルトの CJK フォントです。代替フォント:Songti SC、Heiti SC、STHeiti、Hiragino Sans GB。Fontsize=12— 小さめの値から始め、フレームを確認してから大きくします。PrimaryColour=&H00FFFFFF— 白いテキスト(BBGGRR + アルファ値)。OutlineColour=&H00000000— 黒い縁取り。BorderStyle=1— 縁取りのみ(さまざまな背景に適しています)。背景が複雑な場合はBorderStyle=3を使い、テキストの背後に不透明なボックスを表示します。Outline=2— 縁取りの太さは2pxです。Shadow=1— 控えめなドロップシャドウです。MarginL=20 MarginR=20— テキストを画面内に収めます。MarginV=40— 下端からの垂直方向の距離です。
焼き込み用SRTの行の長さ
Fontsize が適切でも、行が長すぎると折り返されたり、画面からはみ出したりします。画面に表示する各行は、約15文字の中国語(約42文字のラテン文字)以下にしてください。SRTブロック内で明示的に \n を使って改行し、自動折り返しに頼らないでください。長い1行より、短い2行のほうが常に適しています。(これは上流工程で対応すべき内容です。/wjs-translating-subtitles では、この制限内に収まるよう、すでに字幕キューを調整しているはずです。)
音声ミキシング — 元の音声を小音量で残す
吹き替え音声だけのトラックは、吹き替えらしい音になります(実際に吹き替えだからです)。吹き替え音声の下に元の音声を小音量で重ねると、「プロによる翻訳」のような仕上がりになります。話者の息づかいや強調、笑い声は聞こえたまま、新しい音声の下に残ります。
$FF -i original.mp4 -i dub.mp4 \
-filter_complex "[0:a]volume=0.18[orig];\
[1:a]volume=1.0[dub];\
[orig][dub]amix=inputs=2:duration=longest:normalize=0[a]" \
-map 0:v -map "[a]" \
-c:v copy -c:a aac -b:a 192k mixed.mp4
音量の設定例:
- 元の音声を
0.15〜0.25(約−16〜−12dB) - 吹き替え音声を
1.0 - 両方の音声が有効なときに amix が自動で音量を下げないよう、
normalize=0を指定します。
元の音声を完全に削除するには、--no-original-audio を指定します(--bed-volume 0 と同等です)。
吹き替え+字幕の焼き込み+元の音声を組み合わせる(すべてを適用)
1回の ffmpeg 呼び出しですべてを処理できます。対象の字幕を動画ストリームに焼き込み、2つの音声トラックをミキシングします。
$FF -i original.mp4 -i dub.mp4 \
-filter_complex "[0:v]subtitles=input.zh-CN.srt:force_style='Fontname=PingFang SC\,Fontsize=12\,PrimaryColour=&H00FFFFFF\,OutlineColour=&H00000000\,BorderStyle=1\,Outline=2\,Shadow=1\,MarginL=20\,MarginR=20\,MarginV=40'[v];\
[0:a]volume=0.18[orig];[1:a]volume=1.0[dub];\
[orig][dub]amix=inputs=2:duration=longest:normalize=0[a]" \
-map "[v]" -map "[a]" \
-c:v libx264 -crf 18 -preset medium -pix_fmt yuv420p \
-c:a aac -b:a 192k final.mp4
これは「SNSに公開する」ための最終カットです。render.py --video original.mp4 --dub dub.mp4 --srt input.zh-CN.srt を実行すると、このパイプラインがそのまま実行されます。
render.py の実行
# Subtitles only (burn):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --srt SUB.srt --out OUT.mp4
# Dub only (replace audio, no subs):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --dub IN_zh_dub.mp4 --out OUT.mp4
# Full localized cut (burn + dub + original bed):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --srt IN.zh-CN.srt --dub IN_zh_dub.mp4 --out OUT.mp4
# Soft-mux (no re-encode):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --srt SUB.srt --soft-mux --out OUT.mp4
スタイルや音声に関するフラグの一覧については、render.py --help を参照してください(--font、--fontsize、--color、--outline-color、--margin-v、--bed-volume、--no-original-audio)。
出力
- 焼き込みモード:
<source>_burned.mp4(再エンコード済み、libass で字幕をレンダリング) - ソフト字幕の多重化モード:
<source>_softsub.mp4(ストリームコピー、mov_textトラック) - 完成版:
<source>_final.mp4(字幕を焼き込み、音声をミキシングした動画を再エンコード)
アンチパターン
- ❌ libass を確認せずに焼き込みができると約束する。 まず
ffmpeg -filters | grep subtitlesを確認し、見つからない場合は evermeet の静的ビルドに自動でフォールバックします。 - ❌ フレームを確認せずに焼き込み済みのレンダリング結果をコミットする。 全体のレンダリングを開始する前に、最も長い字幕のキューで必ずフレームを抽出し、内容を読み取ります。
- ❌
force_style内にエスケープしていないカンマを使う。 フィルターグラフのパーサーがカンマを処理してしまいます。内部のカンマはすべて\,としてエスケープします。 - ❌ libass の焼き込みと HyperFrames の字幕を併用する。 出力動画ごとに字幕システムはどちらか一方を選びます。
/wjs-overlaying-videoで HTML/CSS の字幕を使っている場合は、ここでは焼き込みもしないでください。 - ❌ SRT でピリオド区切りのミリ秒を使う。 Whisper local は
.mmm形式で出力します。libass では許容されますが、後続のほかのツールで問題が発生します。,mmmに正規化します。 - ❌
BorderStyle=3(不透明なボックス)をデフォルトにする。 背景が本当にごちゃついている場合を除き、BorderStyle=1(縁取りのみ)を使います。ボックスは重苦しく、古びた印象になります。
上流
/wjs-transcribing-audio+/wjs-translating-subtitles— SRT 入力を生成します。/wjs-dubbing-video— 完全ローカライズ版のカットモード用に、*_<lang>_dub.mp4入力を生成します。吹き替えのみのファイルは技術的には完成した動画ですが、このスキルで元の音声を下にミックスし、字幕を焼き込んで納品可能な状態にします。
よくある落とし穴
- ある動画で適切だったフォントサイズが、別の動画では小さすぎたり大きすぎたりする。 libass はピクセル数ではなく PlayRes の比率で拡大縮小します。動画の解像度ごとに調整し、固定値をそのまま使わないでください。
- デフォルトの余白では、縦向きのスマートフォン動画でテキストが切れる。
MarginL=20 MarginR=20とMarginV=40(またはそれ以上)を必ず明示的に設定します。 mov_textトラックが QuickTime では表示されても、一部の Android プレーヤーでは表示されない。 対象ユーザーが中国語を使うモバイルユーザーの場合、字幕のソフト多重化は信頼できないため、焼き込みます。- 背景がごちゃついている/コントラストに問題がある。
Outline=2をOutline=3にするか、BorderStyle=3に切り替えて半透明ボックスを使います(50% の黒ならBackColour=&H80000000)。