Clipify
動画の中で最も面白い瞬間を見つけ、それらを独立したクリップとして切り出し、必要に応じて16:9→9:16(顔追跡または分割画面)にリフォーマットし、オーパススタイルの単語ごとのキャプションを焼き付けます。
入力
- 動画ファイルのパス(ユーザーが提供します。それ以外の場合は尋ねてください)
- オプション:希望するフォーマット(9:16、16:9、1:1) — 指定がない場合は、候補を選んだ後に尋ねてください
- オプション:字幕スタイルの好み — 指定がない場合は、キャプション作成前に尋ねてください
ツール(最速のパスのみ使用)
- Whisper:
whisper --model tiny.en --word_timestamps True --output_format json(small.enの約10倍高速。英語の品質は十分)。非英語の場合:--model base(--languageは省略)。 - ffmpeg: ハードウェアデコードはオプションでプラットフォーム固有 —
-hwaccel autoを使用するか、省略します(macOS:videotoolbox、Linux:vaapi/cuda/なし)。レンダリングには-preset ultrafastを追加。最終マスターには-c:v libx264 -crf 20を使用。 - Numpy オーディオアライメント用(FFT相互相関)。scipy/cv2は不要。
- スクリプト:
<skill-dir>/scripts/(<skill-dir>はこのSKILL.mdを含むディレクトリ — 通常は~/.claude/skills/clipify/)analyze.py— 2つのROIモーションファイルから話者タイムラインを作成build_pan.py— ハードカットのあるffmpegクロップx式を作成build_ass.py— whisperのJSONからオーパススタイルのASSキャプションを作成audio_align.py— より長いソース内のサブクリップのオフセットを検出
作業ディレクトリ: /tmp/clipify/(開始時にmkdirし、デバッグ用にアーティファクトを残す)。
ワークフロー
ステップ1 — 最も面白い部分を見つける
mkdir -p /tmp/clipify
ffmpeg -y -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
whisper /tmp/clipify/audio.wav --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
結果のJSON(または .txt)を読み、3〜5個の候補クリップを選びます。面白いシグナルとしてスキャンするもの:
- オチとリアクション: 「what」「wait」「no way」、笑い声、「haha」、罵り言葉などの単語
- 逆転の瞬間: 前提となる質問 → 予想外の回答
- 気まずい沈黙: 長いギャップのあるWhisperセグメント、またはフィラー(「uh」「um」)
- 自虐ネタ / 引用可能なワンライナー: 単独で成立する短い平叙文
- オーディオピーク:
ffmpeg -af volumedetectで検出、または素早いやりとり(短いWhisperセグメントの交互)を探す
各候補について、[開始時間, 終了時間, 面白い理由, 提案タイトル] を提案します。10〜25秒のクリップを目指します。リストを表示し、ユーザーに確認/選択させます。
ステップ2 — 選択された各クリップをトリミング
ffmpeg -y -ss "$START" -t "$DURATION" -i "$VIDEO" -c copy /tmp/clipify/clip_$N.mp4
(即時トリムには -c copy を使用。カットがフレーム精度を必要とする場合のみ再エンコード。)
ステップ3 — 出力フォーマットを決定
ユーザーに尋ねます(既に指定されている場合はスキップ):「9:16(TikTok / Reels)、16:9(YouTube)、または1:1(Instaフィード)?」
ステップ4 — 16:9 → 9:16の場合:顔間パン vs 分割画面
ffprobe でソースアスペクトを検出します。ソースが16:9でターゲットが9:16の場合、尋ねます:
「2つのオプション:(a)ハードカットパン — 話している人を追従(画面には一度に一人の顔)、または(b)分割画面スタック — 両方の顔を表示。どちらにしますか?」
話者が一人だけのクリップの場合は質問をスキップします。一人の話者の場合は、中央クロップします。
ステップ4a — 顔間パン(早いカットのトークヘッドダイアログに推奨)
-
2つの顔ROIを見つける。 1フレームをサンプリング:
ffmpeg -ss <中間> -i <クリップ> -frames:v 1 /tmp/clipify/probe.jpg。それを読みます。各顔の口+あごの領域を、ソースのピクセル空間でのx,y,w,hとして目視で確認します。(cv2は不要 — クリップ内ではカメラは静的。1フレームで十分。)ボックスを描画して確認:ffmpeg -i probe.jpg -vf "drawbox=x=$LX:y=$LY:w=$LW:h=$LH:[email protected]:t=4,drawbox=x=$RX:y=$RY:w=$RW:h=$RH:[email protected]:t=4" verify.jpg最大2回繰り返します。ボックスは口+あごをカバーし、手やマイクを避ける必要があります。過度に調整しないでください — フレーム差分は寛容です。
-
各ROIのフレームごとのモーションエネルギーを抽出:
ffmpeg -y -i clip.mp4 -filter_complex " [0:v]split=2[a][b]; [a]crop=$LW:$LH:$LX:$LY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/L.txt[la]; [b]crop=$RW:$RH:$RX:$RY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/R.txt[ra] " -map "[la]" -f null - -map "[ra]" -f null - -
話者タイムラインを構築(最小滞在時間1.0秒 — 短い感嘆詞は前の話者に統合):
python3 <skill-dir>/scripts/analyze.py /tmp/clipify/L.txt /tmp/clipify/R.txt 1.0 > /tmp/clipify/segments.json -
ソースからの9:16縦ストリップ用のパンx座標を選択。 ソースW=1920、ターゲットW=1080の場合、クロップストリップ幅=608。
- LEFT_X =
face_left_center_x - 304(0以上にクランプ) - RIGHT_X =
face_right_center_x - 304(source_W - 608以下にクランプ)
- LEFT_X =
-
ハードカットx式を生成しレンダリング:
EXPR=$(python3 <skill-dir>/scripts/build_pan.py /tmp/clipify/segments.json $LEFT_X $RIGHT_X) ffmpeg -y -i clip.mp4 -filter_complex \ "[0:v]crop=608:1080:x='$EXPR':y=0,scale=1080:1920:flags=lanczos[v]" \ -map "[v]" -map 0:a -c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p \ -c:a aac -b:a 192k /tmp/clipify/clip_panned.mp4ソース1920×1080を想定。4Kソースの場合は、最初にダウンスケールするか、すべての座標を2倍にします。
ステップ4b — 分割画面(両方の顔が常に表示される)
2つの積み重ねられたタイル、それぞれ1080×960。アクティブな話者のタイルが上に来ます — 話者が変わるとオーバーレイが切り替わります。
[0:v]split=2[a0][a1];
[a0]crop=Wcrop:Hcrop:LX_tile:LY_tile,scale=1080:960,split=2[lt0][lt1];
[a1]crop=Wcrop:Hcrop:RX_tile:RY_tile,scale=1080:960,split=2[rt0][rt1];
[lt0][rt0]vstack[layoutL];
[rt1][lt1]vstack[layoutR];
[layoutL][layoutR]overlay=0:0:enable='<RIGHT_SPEAKER_ENABLE>'[v]
<RIGHT_SPEAKER_ENABLE> を segments.json から、右話者セグメントに対して between(t,a,b)+between(t,a,b)+... として構築します。タイルクロップは各顔の周りで約720×640をターゲットにします(1.125:1は1080×960に一致)。
ステップ5 — 字幕を追加
一度尋ねます(ユーザーが既にスタイルを指定していない場合のみ):
「3つの字幕スタイル:opus(大きく太字の白、黄色のアクティブワードハイライト)、カラオケ(4単語チャンク、緑のハイライト)、ミニマル(すっきりしたHelvetica、ハイライトなし)。または、お好みの例を貼り付けてください。」
ユーザーが参考画像/例を貼り付けた場合:フォント、サイズ、太さ、色、位置、アニメーションを可能な限り近づけます — 手動でカスタムASSを作成するか、build_ass.py を拡張します。
それ以外の場合はプリセットを使用:
# トリミングされたクリップでwhisperを再実行し、クリップ開始からの正確なタイムスタンプを取得
whisper /tmp/clipify/clip_panned.mp4 --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
python3 <skill-dir>/scripts/build_ass.py /tmp/clipify/clip_panned.json /tmp/clipify/captions.ass opus
キャプションを焼き付け:
ffmpeg -y -i /tmp/clipify/clip_panned.mp4 -vf "subtitles=/tmp/clipify/captions.ass" \
-c:v libx264 -preset fast -crf 20 -c:a copy "$OUTPUT.mp4"
ステップ6 — 納品
- 各出力を
<source_dir>/clipify_out/に保存(存在しない場合はmkdir) - クリップごとに1行:名前、長さ、面白かった点、出力パスを出力
- 最初の出力パスを出力(または開く — Linux
xdg-open <パス>、macOSopen <パス>) — ユーザーが確認できるように - 反復を提案(異なるスタイル、異なるROI、分割画面への切り替え、キャプションのタイミング調整)
落とし穴(以前の実行からの教訓 — 繰り返さないこと)
- ROIを過度に調整しないこと。 最大2回の反復。モーション差分は寛容です — 口+あごをカバーする広めのROIでも、完全に口の中心になくても問題なく機能します。
- クリップ内のシーンカットに注意。
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -を実行してカット数を数えます。16:9→9:16クリップに多くのカットがある場合、固定された顔ROIは支配的なシーンでのみ機能します。ユーザーに警告し、ワンテイクのクリップを選ぶか、カット中のフレームが中心からずれていることを受け入れるかを提案します。 - ソース解像度が重要。 ソースが4Kの場合、最初に1920×1080にダウンスケールするか(より高速で9:16出力には十分)、すべてのROI/パン座標を2倍にします。
- ソースに焼き付けられた字幕。 一部の「生」クリップにはまだ字幕があります。その場合は、オーディオ相互相関(
audio_align.py)で字幕なしマスターを見つけ、そこからトリミングします。 - 短いクリップで十分な場合、フル尺のソースでwhisperを実行しないこと。 ステップ2以降でトリミングされたクリップでwhisperを実行します。ステップ1では、面白い瞬間を見つけるためにトランスクリプトが必要な場合のみ、フルソースでwhisperを実行します。
- 計画を一行で述べ、その後行動すること。 すべての反復を説明しないでください。