Clipify
ビデオの中から魅力的な瞬間を見つけ出し、スタンドアロンのクリップとして切り出し、必要に応じて16:9 → 9:16(顔追跡パンまたは分割画面)にリフォーマットし、時間ワープパンでよりタイトな露出を実現し、Opusスタイルの単語単位のキャプションを焼き付けます。
モード
ステップ1の前に1つ選んでください。選択によって瞬間の見つけ方とキャプションの適用有無が変わります。
- 会話モード(ポッドキャスト、インタビュー、トーキングヘッドのデフォルト):Whisperで文字起こしし、パンチライン/リアクション/気まずい間を見つけます。→ ステップ1A。キャプションはステップ5。
- アクションモード(木こり、スポーツ、パーカッション、ドラム、インパクト音が繰り返されるもの):Whisperは完全にスキップし、
detect_transients.pyを実行して各ストライクを見つけ、それぞれでカットします。→ ステップ1B。ステップ5はスキップします。ストライク音そのものがリズムです。 - ハイブリッド:トーク部分には会話モード、残りにはアクションモードを使い、インターカットします。1Aと1Bの両方を使用します。
ユーザーが「私はここで話していない」と言ったら、アクションモードです。「X(チョップ、ヒット、パンチ、ドラムビート、スイング、フットステップ)ごとにカットして」と言ったら、アクションモードです。それ以外はデフォルトで会話モードにします。
入力
- ビデオファイルパス(ユーザーが提供します。そうでなければ尋ねます)
- オプション:希望フォーマット(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 videotoolbox、レンダリングに-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からOpusスタイルのASSキャプションを生成audio_align.py— 長いソース内のサブクリップのオフセットを見つけるdetect_transients.py— 鋭いインパクト音(アクションモード)を見つける;--helpを参照
作業ディレクトリ:/tmp/clipify/(開始時にmkdirし、デバッグのためにアーティファクトを残す)
ワークフロー
ステップ1A — 会話の瞬間を見つける(会話モード)
mkdir -p /tmp/clipify
ffmpeg -y -hwaccel videotoolbox -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秒のクリップを目指します。リストを表示し、ユーザーに確認/選択してもらいます。
ステップ1B — 各ストライクを見つける(アクションモード)
Whisperをオーディオトランジェント検出に置き換えます。検出器は、1~6 kHz帯域のスペクトルフラックスを計算することで、鋭いインパクト(斧が木に当たる音、拳がパッドに当たる音、ドラムヘッド、ボールがバットに当たる音、木が山に落ちる音)を見つけます。これは、アンビエント/風では発生しない、インパクトが作り出す広帯域インパルスです。
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
python3 <skill-dir>/scripts/detect_transients.py /tmp/clipify/audio.wav \
--min-flux 30 --min-gap 0.6 > /tmp/clipify/strikes.json
チューニング:
--min-flux 30は適切なデフォルトです。実際のインパクトは50~300を記録します。アンビエント/風は5~15です。偽陽性が多すぎる → 40~50に上げます。ヒットが少なすぎる → 20に下げます。--min-gap 0.6(秒)は、2つのストライクの最短間隔です。速いドラム演奏では0.2が必要かもしれません。チョッピングは0.6~1.0で問題ありません。--band 1000:6000は木/金属/ガラスの衝撃をカバーします。重い低音の衝撃(キックドラム、ボディショット)はより低い帯域(--band 200:2000)にあります。ホイッスル、チリン、スネアクラックはより高い帯域(--band 4000:10000)にあります。- 複数クリップのソース(電話クリップのフォルダ)の場合、各クリップで検出器を実行し、さまざまな角度から多様なセットを選びます。1ショットあたり約0.9秒の45~60秒モンタージュには、50~65ストライクを目指します。
各「ショット」は、ストライクの約0.4秒前(予備動作を見せる)から始まり、約0.5秒後(ストライク+短いフォロースルー)で終了します。ビートあたり約0.9秒です。
ステップ1.5 — 各候補がフレーム内にあることを確認する
レンダリング前に必ずこれを行います(どちらのモードでも)。オーディオ検出はイベントの音を見つけます。カメラがその瞬間を捉えていない可能性があります。これをスキップすると、ユーザーから「私が見えないところでチョップをカットした」といったフィードバックを得る最大の原因になります。
# 各候補ストライク時刻Tで1フレームをサンプリング:
for T in $STRIKE_TIMES; do
ffmpeg -y -ss "$T" -i "$VIDEO" -frames:v 1 -vf scale=320:-1 \
/tmp/clipify/verify_${T}.jpg
done
各確認画像を読み、以下の場合は候補をドロップします。
- 被写体がかがんでいる、フレームから外れている、または障害物の後ろに隠れている
- レンズに親指が写っている(はい、これは起こります — そして視覚的にしかキャッチできません)
- 検出器をトリガーした明らかに異なる音源(ドアの閉まる音、道具を落とす音)である
約30の候補の場合、これは1回の高速なffmpeg呼び出しと1回のバッチ画像読み取りです。安価な保険です。
ステップ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 <middle> -i <clip> -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 -hwaccel videotoolbox -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に一致)をターゲットにする必要があります。
ステップ4c — タイムワーピングによる露出(オプション)
ユーザーが、弧を失うことなくスロースケニック/パン/露出ショットをタイトにしたい場合(「これを半分にカットして」)、トリミングの代わりにスピードランプを使用します。トリミングでは弧の一部をドロップせざるを得ません。スピードランプは、すべてのビートをより高いテンポで保持します。
# 2倍速:12秒の弧 → 6秒の出力、オーディオピッチはatempoで保持
ffmpeg -y -ss "$START" -i "$CLIP" -t "$ORIG_DUR" \
-vf "setpts=PTS/2,scale=1080:1920:flags=lanczos,fps=30" \
-af "atempo=2.0" \
-c:v libx264 -preset fast -crf 21 -pix_fmt yuv420p \
-c:a aac -b:a 128k /tmp/clipify/clip_2x.mp4
経験則:
- 1.5倍 — 少し活発に見せたいが、スピードアップしているようには見せたくない人間の動き
- 2倍 — よりパンチの効いた露出。それでも自然に見える
- 3~4倍 — タイムラプス風(4倍の場合は
atempo=2.0,atempo=2.0をチェーン。atempoはフィルターインスタンスごとに0.5~2.0のみ受け付けます) - オーディオが単なるアンビエント/風で、高音が邪魔になる場合は、
-af atempoを削除し、セグメントをミュートします
残りのカットがリズミカル(チョップモンタージュ、チャットのやりとり)で、露出がデッドスポットのように感じられる場合に便利です。
ステップ5 — 字幕を追加する
アクションモードではこのステップをスキップします — ストライク音そのものがリズムであり、キャプションは視覚的な邪魔になります。
一度だけ尋ねます(ユーザーがすでにスタイルを指定していない場合のみ):
「3つの字幕スタイルがあります:opus(大きく太字の白、黄色のアクティブワードハイライト)、karaoke(4単語のチャンク、緑のハイライト)、minimal(クリーンなHelvetica、ハイライトなし)。または、お好みの例を貼り付けてください。」
ユーザーが参照画像/例を貼り付けた場合:フォント、サイズ、太さ、色、位置、アニメーションを可能な限り一致させます — build_ass.py を手書きまたは拡張してカスタムASSを作成します。
それ以外の場合はプリセットを使用します。
# トリミングされたクリップに対して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行ずつ出力:名前、長さ、何が面白かったか、出力パス
- 最初の出力を
open <path>で開き、ユーザーが確認できるようにします - 繰り返しの提案を提供します(異なるスタイル、異なるROI、分割画面への切り替え、キャプションのタイミング再調整)
落とし穴(以前の実行からの教訓 — 繰り返さないこと)
- オーディオ検出 ≠ 可視イベント。 レンダリング前に必ずステップ1.5(各候補フレームの確認)を実行してください。検出器はチョップの音を見つけますが、チョッパーがフレーム内にいるかどうかはチェックしません。被写体がかがんでいる、カメラの外にいる、レンズに親指が写っているヒットでも、オーディオ検出器は作動します。レンダリング前にそれらをキャッチしてください。
- スペクトルフラックス帯域が重要です。 デフォルトの
--band 1000:6000は木/金属/ガラスの衝撃をカバーします。重い低音の衝撃(キックドラム、ボディショット)には--band 200:2000が必要です。ホイッスル、チリン、スネアクラックには--band 4000:10000が必要です。--min-flux 30が何も返さない場合は、最初に15に下げてみてください。何千も返す場合は、しきい値を上げる前に別の帯域を試してください。 - スピードランプオーディオはペアで。
atempoは単一のフィルターで0.5~2.0のみを受け入れます。4倍の場合はatempo=2.0,atempo=2.0をチェーンします。atempoがないと、setptsだけでチップマンクオーディオになります。 - 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を実行します。
- 計画を1行で述べてから、行動します。 すべての反復を説明しないでください。