wjs-reframing-video について
wjs-reframing-videoは、動画を反対のアスペクト方向に変換する動画処理スキルです。口の動きから話者を追跡し、区間ごとに固定クロップを配置します。元のファイルは変更しません。
主な機能
- 01口の動きによる話者検出
- 02サンプルフレーム間の顔追跡
- 03話者に沿ったクロップ区間
- 04ハードカットまたは滑らかな移動
- 05音声ストリームのコピー
- 06クロップ計画のJSONサイドカー
活用シーン
- 01横長インタビューの縦型化
- 02縦型録画の横長動画化
- 03単独話者の動画のリフレーミング
- 04サイドカーでクロップ判断を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-reframing-videoインストール後、Claude Code で/wjs-reframing-videoを入力して読み込まれたか確認します。
同梱ファイルmodels/blaze_face_short_range.tflitescripts/crop.py
詳細
wjs-reframing-video
元動画の狭い帯状の範囲をクロップして、動画の向きを変換します。物理的に回転させるわけではありません。クロップ範囲は、最も大きい顔や信頼度の最も高い顔ではなく、話している人物(口が動いている顔)に追従します。.crop.json サイドカーファイルには、クロップ計画、セグメントごとの話者判定、使用したパラメーターが記録されます。元の入力ファイルは変更されません。
使用する場面
- 16:9 のポッドキャスト/インタビュー/トーク動画を、縦型のショート動画プラットフォーム(WeChat Channels 视频号、Douyin 抖音、Xiaohongshu 小红书、YouTube Shorts、TikTok、Reels)向けに再利用する場合。
- 9:16 のスマートフォン録画を、横型プレーヤー(YouTube の長尺動画、ブログへの埋め込み)向けに再利用する場合。
- 4:3 のアーカイブ映像を 3:4 のモバイル向けに変換する場合、またはその逆の場合。
出力のアスペクト比は、幅と高さを入れ替えた元動画のアスペクト比です。つまり、16:9 → 9:16 であり、「9:16 のフレームにレターボックスで収めた 16:9」ではありません。
使用しない場面
- 複数人の Q&Aで、顔ごとに個別のクロップが必要な場合。このスキルは動画ごとに 1 つのクロップトラックを選択します。話者ごとに分割してレンダリングするには、代わりに wjs-editing-multicam を使用してください。
- 顔のないアニメーションコンテンツ/B ロール。中央クロップにフォールバックしますが、意図に合わないことがほとんどです。
- 元動画に大きなカメラ移動(手持ちでのパン/ズーム)がある場合。顔トラッカーがカメラの揺れを増幅します。先に手ぶれ補正を行ってください。
- 元動画がすでに目標のアスペクト比である場合。処理は不要です。
このスキルでできること/できないこと
| できること | できないこと |
|---|---|
| MAR(口のアスペクト比)の変動による視覚的な話者検出 | 音声と映像の融合(音声エネルギーと口の動きを相互相関) |
| 中心間距離の照合による、フレーム間で安定した顔追跡 | 長い途切れ/オクルージョンをまたいだ再識別 |
| 切り替わりを防ぐヒステリシスを用いた、話者に合わせたセグメント | 一瞬の変化ごとにフレーム単位で切り替え |
--face-pick speaker(デフォルト):口が動いている人物を選択 | --face-pick largest(オプションのレガシー動作):最も大きい顔を選択 |
セグメント間はハードカット、各セグメント内のクロップは固定(--motion cut、デフォルト) | 話者の発話中に位置がずれていく滑らかなパン(オプションの --motion smooth) |
| 音声ストリームのコピー(ビット単位で正確) | 音声の再処理/再エンコード |
ffmpeg でサンプリングした 5 fps の MediaPipe Tasks FaceLandmarker(478 点メッシュ) | フレームごとのニューラル・インペインティング/アウトペインティング |
ffmpeg crop + scale の 1 回の処理 | フレームごとの Python コンポジター |
誰も話していない場合(無音や音楽のみの区間)は、自動的に「最も大きい顔」にフォールバックします。
依存関係
pip install mediapipe opencv-python numpy
(MediaPipe は標準の Python ディストリビューションには含まれていません。ffmpeg と ffprobe が PATH 上にある必要があります。)
初回実行時のモデルのダウンロード:MediaPipe 0.10 以降では Tasks API が使用され、face_landmarker.task モデルファイル(約 4 MB)が必要です。初回の呼び出し時に、crop.py がモデルを ~/.claude/skills/wjs-reframing-video/models/ にダウンロードし、以降の実行のためにキャッシュします。初回実行時にオフラインの場合、スクリプトは失敗します。
検出範囲の制限:同梱のランドマーカーは、カメラから約 2 m 以内の顔(自撮り/ポッドキャスト/インタビューの距離)向けに調整されています。顔が小さく映るワイドなイベント映像では検出できない場合があるため、先にフレームをサンプリングして確認してください。
クロップの計算
元動画のアスペクト比 = W / H。目標のアスペクト比 = H / W(反転)。クロップ範囲は次のように計算します。
| 元動画の向き | クロップ範囲 |
|---|---|
| 横向き(W > H)→ 縦向き | W_crop = H × H / W、H_crop = H(狭い縦帯) |
| 縦向き(W < H)→ 横向き | W_crop = W、H_crop = W × W / H(狭い横帯) |
1920×1080 → 縦向きの場合、W_crop = 608、H_crop = 1080 です。最終的に 1080×1920 にスケーリングします(約 1.78 倍に拡大)。
1080×1920 → 横向きの場合、W_crop = 1080、H_crop = 608 です。最終的に 1920×1080 にスケーリングします。
拡大せずにクロップ後のネイティブサイズを使用する場合は、--output-size 1080x1920 で最終サイズを指定してください。
パイプライン
- プローブ — ffprobe で入力の寸法、fps、再生時間を調べます。
- 向きを決定 — アスペクト比から自動判定します(上書きするには
--target portrait|landscapeを指定します)。 --sample-fpsでフレームをサンプリング(デフォルトは 5。口の動きを捉えるには十分な値です。音声のナイキスト周波数は約 10 Hz なので、少なくとも 4~5 fps が必要です)。- サンプリングした各フレームで顔のランドマークを検出 — MediaPipe Tasks の
FaceLandmarker(478 個のランドマーク)を使用します。検出した顔ごとに、中心、サイズの目安、MAR(口のアスペクト比=内側の唇の垂直距離/口角間の水平距離)を記録します。 - フレーム間で顔を追跡 — 中心間距離で照合し、各顔に安定した
face_idを割り当てます。 - サンプルごとのアクティブスピーカー — 各顔トラックについて、スライディングウィンドウ内の MAR の分散(
--mar-var-window-sec、デフォルトは 1 秒)を計算します。分散が最も高い顔を「話している」と判定します。--mar-var-thresholdを下回る場合は誰も話していないと判定し、最も大きい顔を対象とします。 - ヒステリシス — 新しい話者が
--min-segment-sec(デフォルトは 1.5 秒)以上安定している場合にのみ、切り替え候補を確定します。それより短い揺らぎは無視されるため、1 フレームの誤検出でクロップ位置が行ったり来たりするのを防げます。 - 話者に合わせたセグメント — 各セグメントで、その話者の顔の中心(cx、cy)の平均をクロップの中心とし、セグメント全体で固定します。
- ffmpeg のステップ関数式を構築(
--motion cut、デフォルト) — 各セグメントのクロップ位置を一定に保ち、セグメント境界で瞬時にジャンプします。カメラアングルが切り替わる実際のカットのような見た目です。(--motion smoothを指定すると、セグメント中間点間を区分線形でパンします。文の途中でカメラが動いて見えるため、トークヘッドの映像では適さないことがほとんどです。) - レンダリング — ffmpeg を 1 回実行し、
crop=W:H:x='expr':y='expr', scale=OUT_W:OUT_Hを適用します。クロップフィルターはxとyを各フレームでネイティブに評価します。音声ストリームは再エンコードせずにコピーします。
実装は scripts/crop.py です。出力される副産物:
<input>.crop.json— クロップ計画を記録したサイドカーファイル<input>_cropped.mp4— クロップとスケーリングを適用した最終動画
サイドカースキーマ(<input>.crop.json)
{
"_about": "wjs-reframing-video crop plan for cam_a.MOV. Active-speaker detected via MAR variance.",
"_help": {
"source_size": "[width, height] in pixels.",
"target_size": "[width, height] of the final rendered output.",
"crop_window": "[width, height] of the moving crop in source coords.",
"chunks": "Speaker-aligned segments: {t0, t1, cx, cy, speaker_id}.",
"face_pick_mode": "speaker = MAR-variance active-speaker; largest = old behavior.",
"speaker_id": "Stable face track id. null means no face / silence fallback."
},
"schema_version": 2,
"source": "cam_a.MOV",
"source_size": [1920, 1080],
"target": "portrait",
"target_size": [1080, 1920],
"crop_window": [608, 1080],
"face_pick_mode": "speaker",
"sample_fps": 5.0,
"mar_var_window_sec": 1.0,
"mar_var_threshold": 1.5e-4,
"min_segment_sec": 1.5,
"chunks": [
{"t0": 0.0, "t1": 4.2, "cx": 808, "cy": 540, "speaker_id": 0},
{"t0": 4.2, "t1": 11.6, "cx": 1182, "cy": 540, "speaker_id": 1},
{"t0": 11.6, "t1": 14.0, "cx": 808, "cy": 540, "speaker_id": 0}
],
"face_sample_count": 1234,
"track_count": 2
}
パフォーマンス
- 検出に最も時間がかかります。Apple Silicon で 2 fps のサンプリングを行う場合、リアルタイムの約 10~20 倍の速度が目安です(30 分のソースなら、検出に約 1~2 分かかります)。
--sample-fpsを上げると検出は遅くなりますが、追跡の応答性は高まります。 - レンダリングは高速です。ハードウェアエンコード(macOS では
hevc_videotoolbox)を使い、ffmpeg を 1 回実行します。1080p のソースなら、リアルタイムより速いことがよくあります。 - 非常に長いソース(200 チャンク超)では ffmpeg の式が複雑になります。式を約 200 個の制御点以内に保つため、スクリプトはチャンクの中間点を自動的に間引きます。
よくある落とし穴
- 口の動きは発話とは限りません — あくび、笑い、食事、息を吸い込む動作はいずれも MAR の分散を高めます。検出器は、これらを一時的に発話と誤認することがあります。重要度の高いコンテンツでは、サイドカーの話者タイムラインを目視で確認し(スクリプトは
face#N: Xs on screen (Y%)というサマリーを出力します)、必要に応じて--mar-var-thresholdを変えて再実行してください。 - 横顔、または下向きに傾いた顔 — 顔がカメラに対して 60° を超えて傾いていると、MediaPipe が口のランドマークを安定して検出できず、MAR の分散が横ばいになることがあります。話者の判定は「最大の顔」へのフォールバックに切り替わります。横顔のショットが長く続く場合は、
--face-pick largestを検討してください。 - 2 人の発話が重なる場合(割り込み/かぶせて話す) — 両方の顔で MAR の分散が発生しますが、選ばれるのは一方だけです。選ばれなかった顔は聞き手として扱われます。クロストーク時に話者ごとの追跡精度を高めるには、wjs-editing-multicam を使い、カメラを分けてください。
- 無音が長く続く場合(B ロール、音楽) — 最大の顔にフォールバックします。最大の顔が適切でない場合(例:話者のマイクから音楽が流れている間、聞き手が動かずにいる場合)、追跡のずれが発生します。音楽のみの区間の前後であらかじめ分割してください。
- ソースに焼き込み済みのローワーサード/字幕がある場合 — H→V では下部の帯がクロップされ、V→H では残りますが、引き伸ばされます。実行前に焼き込みを除去してください。
- 広角レンズ/魚眼レンズ — 画面端付近の顔ではランドマークを検出できないことがあります。まず
ffmpeg lenscorrectionで歪みを補正してください。 - アップスケーリングによるアーティファクト —
608×1080 → 1080×1920は 1.78 倍のアップスケーリングで、シャープなテキストでは劣化が目立ちます。鮮明さを保ちたいオーバーレイがある場合は、ネイティブのクロップサイズ(--output-size 608x1080)でレンダリングし、プラットフォーム側でアップスケーリングしてください。 - 出力ビットレートがプラットフォームの上限を超える場合 — デフォルトは
--bitrate 12Mです。WeChat Channels(视频号)の上限は 10 Mbps なので、このプラットフォーム向けには--bitrate 8Mを指定してください。
顔を検出できない場合のフォールバック:決定論的な固定クロップ
「検出できない可能性がある」範囲の制限は、単なる警告ではありません。MediaPipe が顔を 0 個検出した場合(広角レンズで撮影した、遠くにいる/動かない 2 人のインタビューで、距離が約 2 m 以上)、クロップログには 0 face observations across N sampled frames と表示され、スクリプトはフレームを中央でクロップします。インタビューの撮影現場では、これにより 2 人の間の背景(暖炉、植物、ロゴなど)が画面に入り、人物は映りません。出力が壊れたように見え、--mar-var-threshold を調整しても改善しません。ランドマークがまったく検出されていないためです。
出力を信用する前に、必ずクロップログを確認してください。 0 face track(s) identified / (no face / fallback): … 100% と表示された場合は、MediaPipe のクロップをやめて、代わりに決定論的な固定クロップを使用してください。こうした撮影ではカメラが固定され、話者の画面内の位置も一定なので、手動で設定する X オフセットは非常に安定します。
# 1920×1080 → 9:16 ⇒ crop window 608×1080. X = speaker's screen position:
# left speaker → x=0 right speaker → x=1920-608=1312 centred → x=656
# Do crop + (HLG→SDR tone-map) + 30fps + dense keyframes in ONE pass so the
# body clip is final and HyperFrames can seek it (see /wjs-overlaying-video).
ZF=~/Library/Python/3.9/.../imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1
"$ZF" -i clip.mp4 -vf \
"crop=608:1080:0:0,zscale=t=linear:npl=203,format=gbrpf32le,\
tonemap=tonemap=hable:desat=0,\
zscale=w=1080:h=1920:t=bt709:m=bt709:p=bt709:r=tv,format=yuv420p,fps=30" \
-c:v libx264 -crf 19 -preset medium -g 30 -keyint_min 30 \
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
-c:a aac -b:a 192k -movflags +faststart clip_vert.mp4
フレームを切り出し(-ss 20 -frames:v 1)、コミットする前に話者が中央にいることを確認してください。実際に両方の話者を追う必要があるクリップ(頻繁に会話が行き来するもの)では、トランスクリプトをもとに話者ごとのウィンドウを手動でラベル付けし、固定クロップを連結してください。1 人のモノローグであれば、X の固定値を 1 つ指定するだけで十分です。ソースがすでに SDR(bt709)の場合はトーンマッピングのフィルターを外してください。SDR に HLG 用の設定を適用すると、色が正しく表示されません。