これは何
「Claudeに動画URLを渡す or 動画ファイルを渡すと、Claudeが映像+音声から中身を理解して答えてくれる」MCPサーバを自宅NAS上に立てた話。
文字起こし・話者分離・場面(フレーム)認識までやる。実装はほぼ全部Claude Codeにやらせた。
完成イメージ(実際のやり取り):
私: この動画の内容を教えて
https://www.youtube.com/watch?v=mjA3LC8zpeI
Claude: (裏でDL → 文字起こし → 場面フレーム取得)
2026年の宝塚記念のレース実況です。メイショウタバルが差し切って優勝し、史上初の親子連覇を達成。クロワデュノールは春古馬三冠ならず……という内容でした。
ちゃんと喋っている実況を文字起こしで拾い、画面に映っているレース展開はフレーム画像で読み取り、両方を突き合わせて着順や勝ち馬まで要約してくれる。
動機
ふとClaudeに動画を渡して「この動画さ〜」みたいな会話がしたくなった。それだけです。
Claudeは画像は見られる。動画は直接は理解しない。じゃあ動画を「Claudeが理解できる形」に変換してやればいいのでは、というのが出発点でした。
最初に考えた構成
最初の発想は天才的閃きを得たと思ったけど誰でも思いつくもので、
- 画像認識はできるんだから、動画をフレームに分解して、各フレーム画像を順番にClaudeに読ませればいい
- フレームの前後の変化を見れば、動画の中で何が起きたか把握できそう
- 全フレームis多すぎるから間引けばよさそう
- 音声は別途、音声認識AIで文字起こしすればいい
寝てても思いつく
今のClaudeでも「動画ファイルをClaudeに送りつける」ところまではできるので、問題はどうやって動画を理解させるかの部分。
てかClaude内でシェル動作させられるからffmpeg動かせるみたいで、割とフレーム分割して画像として読み込んで~~とか指示与えれば動画の内容を把握できるっていうね・・・
SKILLで良かったんじゃね??
否、フレームには音声がない
あと動画DLのたびに毎回セッションでffmpeg落としてyt-dlp使って動画落として・・・とかやってたらアホみたいにトークンを食う
少なくともそれらはMCPとして切り出すべきだし、見出しの通り音声も認識させたいのでこれもWhisperMCPを立てる。
その他は以下
- 動画はNASに置く
- ffmpegもwhisperもNAS側で動く
- Claudeに渡すのは結果だけ=「場面の画像」と「文字起こしテキスト」
- URL入力とファイル入力の合流点は「Claude」ではなく「NASに動画が置かれた状態(video_id)」
まあつまりこういうこと(このへんから殆どClaudeに言われるがまま)↓
全体図
ポイントは、往復で動くのは小さいデータだけ(行き=URL/ファイル、帰り=画像+テキスト)。動画そのものはNASから出ていかない。これによってトークンを多少減らせるだろうしらんけど
処理の流れ
NAS?
ネットワークアクセスストレージ。
自宅においてるサーバ。
家に転がってたPCパーツの寄せ集めにTrueNAS入れた。
おもちゃ。
過去20年くらいの携帯やスマホの動画像とか、家族がクラウド代わりに使ってたり
簡単にスペック
CPU:ThreadRipper1950X
RAM:32GB
GPU:RTX3080Ti
HDD:32TB
電気代は気にしたら負け。
主要な設計判断
| 論点 | 決定 | 理由 |
|---|---|---|
| 動画の受け渡し | NASに置いたまま、結果だけ返す | トークン節約・速い |
| フレーム間引き | シーン検出+768pxに縮小、上限キャップ | 似たコマを自動で間引ける |
| 追加掘り下げ |
get_frames(start, end) で区間指定 |
Claudeが能動的に「ここをもっと」と取りに行ける |
| 重い処理 | job_idを即返して後でポーリング | クライアント側タイムアウト回避 |
| 利用範囲 | まずPCのClaude Codeのみ | スマホ対応はOAuthが必要で工数大 |
| 認証 | 固定Bearerトークン1個 | PCのClaude Code/.mcp.jsonで完結 |
ツール一覧
| ツール | 役割 |
|---|---|
download_video(url) |
URLから動画をDL(yt-dlp) |
POST /upload |
ローカル動画をcurlで投入 |
get_job(job_id) |
DL/文字起こしの進捗 |
transcribe(video_id, diarize, num_speakers) |
文字起こし+話者分離 |
get_keyframes(video_id) |
シーン検出した代表フレーム(画像) |
get_frames(video_id, start, end) |
指定区間を細かく(画像) |
probe_video / list_videos / get_transcript
|
メタ情報・一覧・キャッシュ取得 |
認証とアップロード
最初はスマホからも使いたかったんだけど、Claudeスマホアプリ(claude.aiのカスタムコネクタ)は
- 固定ヘッダ(
Authorization: Bearer ...)を受け付けず、OAuth 2.1が必要 - シェルが無いのでローカルファイルを
curlでアップロードできない
ってことで、まずはPCのClaude Code限定に割り切った。これで認証は固定トークン1個で済む。複数PCでも同じトークンを使い回すだけ。
ファイルアップロードの抜け道
MCPプロトコル自体は「クライアント→サーバへファイル送信」が苦手。でもClaude Codeは手元でシェルを叩けるので、/uploadエンドポイントへcurlでPOSTさせればいい。
curl --data-binary @movie.mp4 \
-H "X-Filename: movie.mp4" \
-H "Authorization: Bearer <TOKEN>" \
https://video-mcp.example.com/upload
URL入力もファイル入力も、結局「NASにvideo_idがつく」状態に合流して、そこから先は共通処理。
実装
実装はもろちんClaude様
- 言語/FW: Python + FastMCP(Streamable HTTP)
- DL: yt-dlp(以前作りかけた動画DLサービスの資産を流用)
-
フレーム: ffmpegのシーン検出(
select='gt(scene,0.3)')+768pxにJPEG縮小。長尺は全デコードを避けて高速サンプリングに自動切替 - 文字起こし: faster-whisper(large-v3, GPU)
- 公開: 既存のCloudflare Tunnelにingress追加+DNS(自宅NASをグローバル公開せずに済む)
-
コンテナ: CUDA cudnn ランタイムベース、
runtime: nvidia
.mcp.jsonに下記を足せば、PCのClaude Codeからvideoとして見える。
{
"mcpServers": {
"video": {
"type": "http",
"url": "https://video-mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <TOKEN>" }
}
}
}
話者分離を追加(HFトークン不要でやる)
「誰が喋ったか」も欲しくなって追加した。定番はpyannote / whisperXだけど、これはゲート付きHugging Faceモデル+トークン+利用承諾が必要で面倒。
そこで sherpa-onnx を使った。
- pyannoteセグメンテーションをONNX再配布した非ゲートモデル+話者埋め込み(NeMo TitaNet)
- HFトークン不要、CPUで動く(GPUを取り合わない)
-
transcribeが既定で話者分離して、各セグメントにSPEAKER_00みたいなラベルを付与
自動の人数推定は完璧じゃなくて、短い単一話者クリップでは多めに数えることがある(57秒4話者は正確、19秒単一話者は3と過剰)。なので人数が分かるときはnum_speakersを渡すと正確(対談なら2、とか)。実際num_speakers=1→1, 2→2, 4→4ときれいに制御できた。
動作確認
例として約3分20秒の競馬レース実況動画(2026年 宝塚記念)を渡したときの実ログ(抜粋)。
私: この動画の内容を教えて https://www.youtube.com/watch?v=mjA3LC8zpeI
[download_video] → {"job_id":"...","video_id":"vid_3aaf16db3cba"}
[get_job] → status:done,
title:"【宝塚記念・GⅠ】宝塚記念 阪神 芝 2200m 2026 レース【みんなのKEIBA】",
uploader:"フジテレビ みんなのKEIBA",
duration:201.7s, 1920x1080, 45MB, has_audio:true
[get_keyframes] → 4 keyframes([t=136.6s][t=148.8s][t=176.3s][t=181.6s] 768x432)
※連続した中継映像なのでシーン変化が少なく、検出は4枚
[transcribe] → job → done(約70秒で完了)
{
"language": "ja", "diarized": true, "num_speakers": 1, "model": "large-v3",
"segments": [
{"start":0.08,"end":7.42,"text":"さあ嵐の今年のグランプリです 親子連覇...","speaker":"SPEAKER_00"},
... (レース全編の実況が time-stamp 付きで30数セグメント) ...
{"start":148.64,"end":156.1,"text":"史上初親子連覇だ名勝たばる 今年もたばるだまたたばる","speaker":"SPEAKER_00"},
{"start":176.98,"end":182.48,"text":"それを止めたのは今年もまたまた名勝たばる","speaker":"SPEAKER_00"}
]
}
これを受けてClaudeがまとめた要約:
2026年・宝塚記念(阪神 芝2200m GⅠ)のレース実況。コスモキュランダが逃げる展開を、外から メイショウタバル が差し切って優勝し 史上初の親子連覇を達成。クロワデュノールは春古馬三冠ならず、ダノンデサイルはダービー馬の復活ならず、で上位人気を退けた。「恵みの雨」で渋った馬場が味方した、と締めくくり。
ここが面白いとこで、whisperの文字起こしは固有名詞をかなり聞き間違えてる(「名勝たばる」→メイショウタバル、「黒ワディノール」→クロワデュノール)。でもClaudeが文字起こし+フレーム画像+世間知識を突き合わせて、正しい馬名とレース結果に補正して要約してる。
つまり「ローカルの文字起こしが多少粗くても、Claude側が映像と文脈で補正できる」っていう、音声認識+映像認識+LLMの合わせ技の旨みがモロに出た例。3分超・実況の早口でもちゃんと展開と着順を把握できてる。
武豊ほんとすごすぎんのよ。。
ハマりどころ
-
画像が返せない(
Unable to serialize unknown type: Image): 画像を返すツールは戻り型注釈からoutput schemaが生成されると死ぬ。@mcp.tool(structured_output=False)で解決。 -
sherpa-onnxのモデル配布タグがスペルミス: 埋め込みモデルのリリースタグが
speaker-recongition-models(recognitionの綴り間違い)。正しい綴りだと404。
らしい。Claudeでもハマったって自覚あるんだね
まとめ
自分専用の「動画を理解するMCP」がいい感じにできた。
使い所はわからん
自宅のNASに動画アップしちゃうから仕事で使うにはセキュリティ的にアレだし、ほんとどうしようかこれ