read-video —— 必要なバイトだけ取得して動画を「読む」
このスキルが解決する問題
ユーザーが動画リンクを渡してきて、その内容を理解したい。素朴な方法は動画全体をダウンロードしてから処理することですが、それは遅く、ストレージを消費し、著作権リスクがあり、しかも必要なのはほんの数秒だけであることがほとんどです。このスキルのアプローチは、目的の瞬間付近のごく一部のバイトだけを取得することです。 音声トラックはローカルでオフライン文字起こしされ(「何について話しているか」を答える主力)、動画トラックからキーフレームを抽出して視覚モデルに渡し、画像情報を補完します。
実測ベンチマーク(249 MB / 9分56秒のMP4、faststart非対応):
| 動作 | 取得バイト数 | 全体に対する割合 |
|---|---|---|
| 30秒目の1フレーム取得 | 2.0 MB | 0.839% |
| 8分目の1フレーム取得 | 1.7 MB | 0.707% |
| 480秒から20秒間の音声取得 | 6.6 MB | 2.784% |
いつ使うか / いつ使わないか
使う:ユーザーが動画直リンク、または動画を含むウェブページのリンク(Bilibiliなどのプラットフォームは resolve.py が自動解析するため、手動でAPIを呼び出す必要はありません)を提供した場合。
使わない:ローカルファイル(直接読む)、または秒単位で完全に理解する必要がある長い動画(コストが依然として高いので、まずユーザーに時間を指定してもらいます)。
絶対的な原則
- 全体をダウンロードしない。 どのステップでも
wget/curl -Oでファイル全体を取得してはいけません。 - まずプローブしてから実行する。 プローブせずにシークすると、Rangeをサポートしていないサーバーで無駄に待つことになります。
-ssは必ず-iの前に配置する。 後に置くと出力側シークになり、順次デコードしてから目的の位置に進むことになり、全体を取得するのと同じになります。前に置くと入力側シークになり、デマルチプレクサが直接ジャンプします。- 測定は有効にする。
--measureはデフォルトでオン:ローカルストリーミングプロキシを使って上流のバイト数を計測します。これは「本当に必要な分だけを取得した」という唯一の確かな証拠であり、ユーザーに対してこっそり全体をダウンロードしていないことを証明する根拠となります。 - ウェブリンクはスクリプトに解析させる。 ページURL ≠ 直リンクであり、直リンクには期限付きの署名が付いています。
resolve.pyを使うか、リンクを直接grab.py/transcribe.pyに渡します(これらは自動的に解析し、リクエストヘッダーを補完します)。セッション内でサイトのAPI呼び出しを手書きしてはいけません。そのようなコードは毎回異なり、レビューもされていません。
ワークフロー
ステップ0:入力の種類を判別する — ウェブリンクか直リンクか
- ウェブリンク(Bilibili、YouTube、抖音、微博、Vimeo… 合計1700以上のサイト)→ まず直リンクに解決します。リンクを
grab.py/transcribe.pyに直接渡すこともできます。これらはリンクを認識し、自動的に解決し、必要なリクエストヘッダーを自動で付与します。 - 直リンク(
.mp4/.m3u8などのメディア拡張子)→ 解決をスキップできます。
判定ルールは resolvers.is_page_url() にあります。メディア拡張子が付いているものはすべて直リンクとみなし、それ以外のhttpリンクは解決レイヤーに送られます。
ステップ1:取得可能なストリームを入手する
python scripts/resolve.py "<ウェブリンク>"
出力:video_url(フレーム取得用)、audio_url(文字起こし用。video_urlとは異なる)、headers(CDNが要求するリクエストヘッダー)、タイトル、再生時間、および推奨フレーム取得時点のリスト。
平台(プラットフォーム)の行で使用されているバックエンドが示されます:native(手書きアダプター、現在はBilibili)またはyt-dlp(汎用、1700以上のサイト)。強制的に指定するには --backend native|yt-dlp|auto を使用します。
1つのコマンドで全てを実行(解決とデータ取得が自動的に連携、推奨):
python scripts/transcribe.py --url "<ウェブリンク>" --seconds 90
python scripts/grab.py "<ウェブリンク>" --times 14 29 44 58 73 --prefix shot --outdir ./out
ステップ2:プローブ(直リンクは先に実行することを推奨、ウェブリンクはスキップ可)
python scripts/probe.py <直リンク> --json
直リンクがCDNからのものである場合、リファラ元を指定:--header 'Referer: https://www.bilibili.com'。
以下の3つのフィールドを確認します:
seekable——falseの場合は停止。サーバーがRangeを無視しており、定点フレーム取得が逐次ストリーム取得に退化します。代わりに「ダウンロードしながらデコード」(-i <url>に-ssを付けず、ストリームの先頭のフレームを取得)するか、ユーザーに不可能であることを伝えます。container.moov_offset/faststart—— インデックスが先頭にある場合(faststart)、固定オーバーヘッドは数十KB。末尾にある場合、まずファイル末尾の一部を取得する必要があります(実測約441KB)。これは「1フレーム取得」の固定コストであり、削減できません。content_length—— 後で「全体に対する割合」を計算するための分母。
ステップ3:ユーザーの意図に応じてデータを取得する
ユーザーが具体的な時間を指定した場合 → 定点取得:
python scripts/grab.py <url> --time 183.5 --out shot.jpg
ユーザーが時間を指定しなかった場合 → まず数フレーム取得して概要を把握:
python scripts/grab.py <url> --times 5 60 180 420 --prefix shot --outdir /tmp/rv
時点選択のコツ:コンテンツの密度に応じて4〜6フレームを取得し、均等に全体に配置しないでください(それは全体をデコードするのと同じです)。迷ったら最初に3フレームを取得し、確認してから特定のセクションを詳細に見るかどうかを決定します。
何について話しているかを理解する → 直接文字起こしする(このステップを先に行うことを推奨):
python scripts/transcribe.py --url <url> --segments 0:90 320:90 630:97
1つのコマンドで「音声切り出し → ローカル認識」を実行し、タイムスタンプ付きの中国語テキストを出力します。外部APIやネットワーク推論は不要です。 初回実行時にSenseVoiceモデル(約240MB、hf-mirrorからダウンロード、1回のみ)が自動的にダウンロードされます。
長い動画の場合は、各90秒のセグメントを3〜4つ取得すれば主要な内容をカバーでき、全体を文字起こしする必要はありません。これはフレーム単位で映像を見るよりもはるかに高速で、「何について話しているか」という質問に答えるのにより適しています。まず文字起こしをし、その後フレームで視覚情報を補完する、この順序が重要です。
文字起こし後、まず生産性を確認します。 通常のナレーションは約4〜6文字/秒。スクリプト出力には chars_per_second が含まれており、1文字/秒未満の場合は自動的に注意を促します。このような場合(BGMのみ、ゲーム実況、無音デモなど)、そのわずかなテキストを内容として使わず、代わりに間隔を詰めたフレーム取得(8〜10フレーム)に切り替えて映像分析を行います。音声に情報がなく、答えは映像にしかありません。
音声ファイル自体だけが必要な場合(別の場所で文字起こしする場合など):
python scripts/grab.py <url> --start 0 --seconds 60 --audio clip.wav
埋め込み字幕トラックがある場合は、ASRよりも正確で高速なため、字幕を優先して読み取ります(ffmpeg -i <url> で subtitle ストリームがあるか確認)。
ステップ4:フレームを視覚モデルに渡す
grab.py はピクセルデータをローカルに取得するだけで、内容を理解しません。出力されたjpgのパスを視覚モデルに渡して質問します(「このフレームには何がありますか」「これは何について話していますか」)。複数フレームの場合は時間順にまとめて渡し、モデルがストーリーとして繋げられるようにします。
出力の読み方
grab.py はJSONを出力し、各アクションにバイト数の明細が含まれます:
{
"ok": true,
"actions": [{
"kind": "frame", "time": 30,
"out_bytes": 48211,
"upstream_bytes": 2097152,
"requests": 4,
"wall_seconds": 2.317
}],
"total_upstream_bytes": 2097152
}
upstream_bytes / content_lengthが「全体に対する割合」です。5%を超えたら停止して再確認する必要があります。おそらく-ssの位置が間違っているか、サーバーがRangeを認識していません。requestsが多いのは正常です:ffmpegは複数回に分けて取得する必要があります(moov、キーフレームからターゲットフレームまで、音声ヘッダー)。ok: falseの場合はerr_tailを確認します。よくあるのはタイムアウトと5XXエラーです。スクリプトにはリトライ機能が組み込まれていますが、それでも失敗する場合は別の時間を試してください。
transcribe.py の出力:
{
"ok": true,
"text": "这是一片被大河与群山环抱的土地……",
"segments": [{"start": 0, "end": 25, "text": "…"}],
"audio_seconds": 276.7,
"model": "SenseVoiceSmall-int8"
}
text はすべてのセグメントを連結した全文です。segments にはタイムスタンプが含まれており、「X秒目に何と言ったか」を調べるときに使用します。
環境
- インタプリタ:ローカルマシン上で動作するvenv内のPython。Windowsの場合、パスは
~/.workbuddy/binaries/python/envs/default/Scripts/python.exeのようになります。Scripts/であることに注意。Unixスタイルのbin/ではありません(ドキュメント通りbin/pythonと書くと「ファイルが存在しない」というエラーになります)。 - 依存関係:
httpx、imageio-ffmpeg(データ取得)、sherpa-onnx、numpy(文字起こし)、yt-dlp(汎用プラットフォーム解決)は、すべて同じvenvにインストールされています。不足している場合、スクリプトが明確にエラーを報告します。yt-dlpがインストールされていなくてもBilibiliは使用可能ですが(手書きアダプター経由)、他のサイトは解決に失敗します。 - ffmpeg:
imageio-ffmpegに同梱されている静的ビルドを使用するため、システムへのインストールは不要です。別のものを指定するには、環境変数RV_FFMPEGまたは--ffmpegを使用します。 - ASRモデルキャッシュ:
~/.workbuddy/cache/read-video/models(約240MB、初回自動ダウンロード)。ファイルが存在し、バイト数が一致すればダウンロードをスキップします。
既知の制限
- CDNはリファラ元をチェックすることが多い:Bilibiliなどのサイトの動画CDNは
Refererを要求し、付けないと403エラーになります。resolve.pyまたは--pageを使用すると自動的に付与されます。直リンクを手動で渡す場合は、必ず--header 'Referer: https://www.bilibili.com'を付けてください(3つのスクリプトすべてでサポート、繰り返し指定可)。 - 直リンクは期限切れになる:サイトの直リンクには署名と有効期限が含まれているため、同じ操作のセット内で使い切ってください。403エラーが発生したら、
resolve.pyを再実行します。 - プラットフォームのカバレッジ = yt-dlp のカバレッジ(1700以上のサイト)であり、独自に管理しているわけではありません。解決できないサイトがある場合は、まず
--backend yt-dlpを使用して元のエラーを確認してください。ログインやCookieが必要なサイトは失敗します。その場合は諦めるか、手動でCookieを提供する必要があります。yt-dlp自体は定期的にアップグレードする必要があります(サイトの変更により古いバージョンは機能しなくなります)——pip install -U yt-dlp。 - 「解決できた」≠「取得できる」:解決レイヤーはURLを提供するだけです。データ取得に失敗した場合は、まず解決の失敗なのかネットワーク到達不能なのかを切り分けてください。実測では、ローカルマシンからYouTube/Vimeoへの接続は不可(プロキシ502)でしたが、抖音/微博/西瓜視頻は到達可能でした。
- HLS / m3u8:単一ファイルではないため、
probe.pyのボックス解析は失敗します(errorフィールドが含まれます)。この場合でもgrab.pyは使用可能で、ffmpegが必要なセグメントを自分で取得します。content_lengthは空になり、割合は計算できません。 - DASHの音声・映像分離:多くのサイト(Bilibiliなど)では、動画ストリームと音声ストリームが別々のURLになっています。フレーム取得には動画ストリーム、文字起こしには音声ストリームを使用してください。間違って使用すると、静かに空の結果が返されます。
-ssの精度はGOPに制限される:特定の秒のフレームを取得するには、最も近いキーフレームからデコードを開始する必要があります。そのため、最小の取得単位は「キーフレームからターゲットフレームまで」であり、「1バイトの範囲」ではありません。- ASRの同音異義語:SenseVoiceSmallは中国語に対して良好な性能を示しますが、人名、地名、古語では同音異義語の誤りがよく見られます(実測では「阪泉之野」→「版圈之也」、「涿鹿」→「涿陸」など)。固有名詞を引用する前には、文脈に基づいて校正してください。文字起こしテキストをそのまま事実として使用しないでください。
- 依存関係:
httpx+imageio-ffmpeg(データ取得)+sherpa-onnx+numpy(文字起こし)。不足している場合、スクリプトは明確にエラーを報告し、どのパッケージをインストールすべきかを示します。
より詳細な原理、問題解決の記録、および実測データについては references/methodology.md を参照してください。
このスキルとdshプラグイン dsh-readvideo の関係については references/dsh-dual-form.md を参照してください。