
Claude API はテキストも画像も扱えるマルチモーダルなインターフェースを持っています。
でも「動画を渡す」となると、公式ドキュメントを見ても直接的な方法は出てきません。
そこに目をつけたのが OSS ツール bradautomates/claude-video です。
コードを追いながら「動画→フレーム抽出→Base64→Claude API」という処理フローを実装レベルで読み解いていきます。
トークン消費の現実・コスト感・設計上の判断ポイントまで含めて整理するので、自分のプロジェクトで同様のアプローチを取るときの事前調査として使ってもらえると思います。
claude-video は何をしているツールか
リポジトリの説明文はシンプルです。
「Give Claude the ability to watch any video」——Claudeに動画を見る能力を与える、というものです。
使い方も単純で、YouTube の URL かローカルの動画ファイルパスと、質問文を渡すだけです。
あとはツールが勝手にダウンロード・フレーム抽出・文字起こしをこなして、Claude に投げてくれます。
リポジトリ構成を見ると、中心になるのは skills/watch/ 配下の watch.py というエントリポイントです。
SKILL.md はスキルコントラクトと呼ばれる仕様書的なファイルで、ツールの振る舞いを定義しています。
外部依存は3つあります。
yt-dlp が URL からの動画ダウンロードを担い、ffmpeg がフレーム抽出を担い、Whisper(mlx-whisper または openai-whisper)が音声文字起こしのフォールバックを担います。
ローカルファイルを渡した場合は yt-dlp によるダウンロードはスキップされ、ffmpeg が直接ファイルを読みます。
ここで最初に認識しておきたいのが、このツールのアーキテクチャの核心です。
Claude API には「動画を理解する」機能は存在しません。
あるのは「画像を理解する」vision 機能です。
つまり claude-video がやっていることは、動画を静止画の連続に変換して、それを順番に Claude に見せる という発想です。
「動画理解」という言葉から想像するネイティブな動画処理とは別物で、本質は 静止画の連続投入 です。
この認識を持っておくかどうかで、ツールの限界を正しく評価できるかどうかが変わってきます。
フレーム抽出からAPIリクエストまでの処理フロー
コードの中で特に面白いのが、動画の長さに応じてフレーム枚数を動的に変える「duration-aware なフレーム予算」の設計です。
動画長さ → フレーム枚数の目安
≤ 30秒 → 約30枚
1〜3分 → 約60枚
3〜10分 → 約80枚
10分超 → 最大100枚(スパース)
ハードキャップ: 2 fps・100枚
長い動画ほどフレームを間引く、という当たり前の設計ですが、これを duration に応じて自動調整している点がポイントです。
フレームレートは 2fps を上限として、指定した枚数に収まるよう ffmpeg に渡す fps 値が逆算されます。
ffmpeg でのフレーム抽出は、おおよそ次のようなコマンドになります。
ffmpeg -i input.mp4 \
-vf "fps=0.5,scale=512:-1" \
-q:v 3 \
/tmp/workdir/frame_%04d.jpg
scale=512:-1 で幅を 512px に固定し、アスペクト比を保ちながらリサイズしています。
--resolution フラグで 1024 に変更すると、画面上のテキストを読み取りたいケースに対応できます。
抽出した JPEG ファイルは Base64 エンコードされて、Claude API の content ブロックに積み込まれます。
マルチモーダルリクエストの構造はこんな形です。
import anthropic
import base64
from pathlib import Path
def build_content(frame_paths: list[Path], transcript: str, question: str) -> list:
content = []
for frame_path in frame_paths:
image_data = base64.standard_b64encode(
frame_path.read_bytes()
).decode("utf-8")
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_data,
},
})
content.append({
"type": "text",
"text": f"Transcript:\n{transcript}\n\nQuestion: {question}",
})
return content
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": build_content(frames, transcript, question)}],
)
トランスクリプトの取得には優先順位があります。
まず yt-dlp がネイティブキャプション(字幕ファイル)を取得しようとします。
YouTube の自動生成字幕などが取れる場合はこれを使い、API 呼び出しが不要なのでコストゼロです。
キャプションが取れなかった場合は、音声を 16kHz モノラルに変換して Whisper でローカル文字起こしするフォールバックが走ります。
この「フレームの連続 + テキストの文字起こし」を一つの content 配列に詰め込んで Claude に渡す、というのが処理フロー全体の骨格です。
トークン消費とコストの現実
Claude API の画像トークン計算は、ピクセル単位ではなく 28×28 ピクセルのパッチ単位 です。
計算式は ⌈width/28⌉ × ⌈height/28⌉ visual tokens になります。
512px 幅・16:9 の JPEG(高さ約 288px)で計算すると、⌈512/28⌉ × ⌈288/28⌉ = 19 × 11 = 209 トークンです。
60枚送ると画像だけで 12,540 トークン。
トランスクリプトや質問文を加えると、1リクエストで数万トークン規模になることは十分ありえます。
モデル選択がコストに直結します。
Haiku 4.5 は入力 $1/M トークン、Sonnet 系は $3/M、Opus 系は $5/M という価格帯です(公式ドキュメントより)。
同じリクエストでも Opus を選ぶと Haiku の5倍のコストになります。
コストに影響する設計判断を整理するとこうなります。
- フレーム数:--max-frames で上限を下げるのが最も直接的なコスト削減手段。30枚と60枚では画像トークンが単純に2倍違う。
- 解像度:デフォルト 512px から 1024px に上げると、パッチ数が約4倍になる。テキスト読み取りが必要な場面以外では上げない方がいい。
- モデル選択:精度が必要なシーンと不要なシーンを分けて、前者だけ Sonnet 以上を使う設計が現実的。
- 区間絞り込み:--start / --end フラグで必要な区間だけを処理すると、フレーム数が減るうえにその区間内でフレームが密になるという一石二鳥の効果がある。
レート制限(TPM: Tokens Per Minute)との衝突も見落としがちなポイントです。
数万トークンのリクエストを連続して投げると TPM の上限にすぐ当たるので、バッチ処理を組む場合は区間分割と組み合わせてリクエストを分散させる設計が必要です。
自分のプロジェクトに組み込むときの設計判断
watch.py の duration-aware なフレーム枚数計算ロジックは、自前実装にそのまま転用できる部分です。
動画の長さを ffprobe で取得して、それに応じてフレーム数と fps を決める流れをシンプルな関数として切り出すとこうなります。
import subprocess
import json
import math
def get_duration(video_path: str) -> float:
result = subprocess.run(
[
"ffprobe", "-v", "quiet",
"-print_format", "json",
"-show_format",
video_path,
],
capture_output=True, text=True, check=True,
)
return float(json.loads(result.stdout)["format"]["duration"])
def calc_frame_budget(duration: float, max_frames: int = 100) -> tuple[int, float]:
if duration <= 30:
target = 30
elif duration <= 180:
target = 60
elif duration <= 600:
target = 80
else:
target = 100
target = min(target, max_frames)
fps = min(target / duration, 2.0) # 2fps ハードキャップ
actual_frames = math.floor(duration * fps)
return actual_frames, fps
def extract_frames(video_path: str, fps: float, out_dir: str, width: int = 512):
subprocess.run(
[
"ffmpeg", "-i", video_path,
"-vf", f"fps={fps:.4f},scale={width}:-1",
"-q:v", "3",
f"{out_dir}/frame_%04d.jpg",
],
check=True,
)
ローカルファイルと URL の分岐は、yt-dlp の呼び出しを関数として分離して、URL かどうかを判定してから切り替えるのが素直な設計です。
URL 判定は単純に http:// か https:// で始まるかどうかで十分です。
一時ディレクトリの管理は tempfile.TemporaryDirectory() を with ブロックで囲むのが安全です。
処理が途中で例外を投げても一時ファイルが残り続ける、というのはよくある落とし穴なので、明示的にクリーンアップされる構造にしておきます。
content 配列の組み立ては、前節で示した build_content 関数のパターンがそのまま使えます。
ただし、フレーム数が多い場合は複数リクエストに分割するか、フレームを間引く設計が必要になる点は頭に入れておいてください。
向いているユースケースと、正直な限界
短〜中尺の動画に対しては想像以上によく機能します。
5〜10分程度の技術解説動画の要約、スライドが映っている動画からのテキスト抽出(--resolution 1024 を使う)、会議録画の特定トピックに関する質問応答といった用途では十分実用的です。
「この動画の3分あたりで説明している設定手順を教えて」のような区間指定の質問も、--start / --end と組み合わせると精度が上がります。
ただし、限界も正直に把握しておく必要があります。
長尺動画(30分超)は精度が落ちます。
100枚のフレームを30分の動画に均等に振ると、約18秒に1枚のサンプリングになります。
要約や全体把握には使えますが、特定の細かいシーンを拾う用途には向きません。
2fps のハードキャップにより、高速なアクションは追えません。
これは GitHub の issue #37 として実際に報告されている既知の問題です。
サブ秒で動く UI 操作デモやスポーツ映像のような高速アクションは、1〜2枚しかフレームが取れないケースがあり、実質的に解析不能です。
リアルタイム処理には構造的に非適合です。
Streaming Video LLM と呼ばれるアーキテクチャは、連続するフレームをオンラインで処理しながら低レイテンシで応答を返す設計になっています。
claude-video のアプローチとは根本的に異なる仕組みで、ライブ映像の解析や監視カメラへの適用は想定外の用途です。
冒頭で「静止画の連続投入」と書いたのは、こういった限界を正確に理解するためです。
Claude が動画を「見ている」わけではなく、サンプリングされたフレームと文字起こしから推論している という認識で使うことが大切です。
その認識があれば、精度が出ないケースに遭遇したときも「なぜ失敗したか」が説明できますし、ユースケースの選定も適切にできます。
動画×LLM の実装パターンとして、このアーキテクチャは「今すぐ動くもの」として十分実用的です。
ただし、それはあくまで「画像 API を動画に応用した近似解」であって、ネイティブな動画理解とは別物だという線引きを持ちながら使うのが、エンジニアとして正直なスタンスだと思います。
株式会社ホコサキは、山口県宇部を拠点に Web 制作・業務システム開発・AI 活用支援を手がけています。
Claude API を使った実装支援や、動画・ドキュメント解析の PoC 開発なども対応しています。
気になることがあれば、お気軽に お問い合わせ からどうぞ。

