数分のアプリ操作を画面録画して、後から「設定を変えた瞬間って何分何秒だっけ」と探す。私自身、個人開発で不具合の再現動画を撮るたびに、このシーク作業に時間を溶かしてきました。動画は情報が濃いぶん、目で追って探すのが一番遅い媒体です。
Gemini API の動画理解は、ここを「時刻を答えてくれる検索」に変えてくれます。動画をまるごと渡し、「この操作が起きる箇所を分秒で挙げて」と頼めば、該当シーンのタイムスタンプを返してきます。ただし素朴に長尺動画を投げると、トークンが膨らんで応答も遅く、費用も読めません。以下では、タイムスタンプ付きで必要なシーンだけを引き出す実装と、FPS・解像度で消費を抑える設計を、動く形で組み立てます。
まず動画を Files API に預ける
20MB を超える動画はインラインで送れないため、Files API にアップロードしてから参照します。アップロード直後はサーバ側で処理中(PROCESSING)なので、ACTIVE になるまで待ってから使います。ここを待たずに投げると failed precondition 系のエラーになります。
import time
from google import genai
client = genai.Client() # GEMINI_API_KEY を環境変数から読む
def upload_and_wait(path: str, timeout: float = 300.0):
f = client.files.upload(file=path)
start = time.time()
while f.state.name == "PROCESSING":
if time.time() - start > timeout:
raise TimeoutError(f"processing timed out: {f.name}")
time.sleep(3)
f = client.files.get(name=f.name)
if f.state.name != "ACTIVE":
raise RuntimeError(f"upload not active: {f.state.name}")
return f
video = upload_and_wait("app_demo.mp4")
print("ready:", video.name, video.state.name)client.files.get で状態を取り直すのを忘れると、最初に取った PROCESSING のままループが回り続けます。アップロードしたファイルは既定で数日後に自動削除されるため、使い終わったら client.files.delete(name=...) で明示的に消しておくと、保存容量と取り違えの両方を防げます。
タイムスタンプで答えさせる
本題です。プロンプトで「MM:SS 形式の時刻つきで答えて」と明示し、出力をそのまま処理に使えるよう構造化を指示します。Gemini は動画の時間軸を理解しているので、「○○が画面に出る最初の瞬間」のような時刻依存の問いに答えられます。
from google.genai import types
prompt = """この画面録画から、次のイベントが最初に起きた時刻を MM:SS で挙げてください。
- 設定画面を開いた瞬間
- 保存ボタンを押した瞬間
- エラーダイアログが表示された瞬間
各イベントについて time(MM:SS) と what(40字以内の説明) を返してください。
該当が無いイベントは time を null にしてください。"""
schema = {
"type": "array",
"items": {
"type": "object",
"properties": {
"event": {"type": "string"},
"time": {"type": "string", "nullable": True},
"what": {"type": "string"},
},
"required": ["event", "what"],
},
}
resp = client.models.generate_content(
model="gemini-flash-latest",
contents=[video, prompt],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=schema,
),
)
for row in resp.parsed:
print(f'{row.get("time") or "--:--"} {row["event"]}: {row["what"]}')ポイントは、時刻の表現をプロンプトで MM:SS に固定し、さらに response_schema で配列の形まで縛っていることです。こうしておくと、返ってきた time をそのまま ffmpeg -ss のシーク開始点に渡したり、該当フレームのサムネイルを切り出したりと、後段の自動処理につなげられます。モデルは速くて安い gemini-flash-latest(3.5 Flash 系の本体)で十分実用になります。動画理解のような前処理こそ、速度と費用の効く Flash 系に回すのが、Dolice Labs の自動運用での私の定番です。
FPS と解像度でトークンを抑える
長尺になるほど効いてくるのが、サンプリング設定です。Gemini は動画を既定で毎秒1フレーム前後で取り込みますが、video_metadata の fps を下げれば取り込むフレーム数が減り、消費トークンと処理時間がまとめて下がります。スライド主体の録画や、ゆっくりした操作の動画なら、FPS を落としても精度はほとんど落ちません。
from google.genai import types
# 低FPS + 低解像度で、長尺のざっくり把握を安く済ませる
part = types.Part(
file_data=types.FileData(file_uri=video.uri, mime_type="video/mp4"),
video_metadata=types.VideoMetadata(
fps=0.5, # 2秒に1フレーム。動きの少ない録画に有効
start_offset="30s", # 冒頭30秒を飛ばして本編から
end_offset="5m0s", # 5分まで。範囲を切ると更に軽くなる
),
)
resp = client.models.generate_content(
model="gemini-flash-latest",
contents=[part, "この区間の主要な操作を時系列で5点、MM:SS つきで。"],
config=types.GenerateContentConfig(
media_resolution=types.MediaResolution.MEDIA_RESOLUTION_LOW,
),
)
print(resp.text)
print("tokens:", resp.usage_metadata.total_token_count)私の使い分けは単純です。長い動画から「だいたいどこに何があるか」を地図化したい一次パスは、fps=0.5 と MEDIA_RESOLUTION_LOW で安く広く。そこで当たりをつけた区間だけ、start_offset/end_offset で 30 秒ほどに切り、FPS と解像度を上げて精読する二次パス。こうすると、最初から全編を高解像度で見るより、トークンを数分の一に抑えつつ、肝心な箇所の精度は確保できます。usage_metadata.total_token_count を毎回ログに残しておくと、どの設定がどれだけ効いたかを実測で比べられます。
注意点として、fps を上げるほど短い瞬間の検出力は上がりますが、トークンは線形に増えます。1フレームで消える通知バナーのような対象を確実に捉えたいときだけ、その区間に限って高 FPS にするのが費用対効果の良い進め方です。
二段で読む発想に切り替える
動画理解で遠回りに見えて速いのは、最初から精読しないことでした。低 FPS・低解像度で全体の地図を作り、当たりのついた短い区間だけを高解像度で読み直す。タイムスタンプを構造化して返させておけば、その地図はそのまま後段の自動処理の入力になります。
次の一歩として、手元の画面録画を1本、まず fps=0.5 で「主要イベントを MM:SS つきで」と読ませてみてください。返ってきた時刻を ffmpeg -ss に渡せば、該当フレームだけを瞬時に取り出せます。シークに溶かしていた時間が、そのまま開発に戻ってくるはずです。同じように動画から情報を拾う作業に追われている方の、最初の足がかりになれば嬉しいです。