受託でお預かりした一時間ほどの記録映像から、公開用に九十秒の抜粋を作る作業をしておりました。切り出す前に確かめておきたいことが一つありました。作業をされている方以外の顔が、どこかに映り込んでいないかどうかです。
その確認を Gemini に投げました。エージェント的動画理解が公開されたばかりで、processing に "agentic" と書くだけで使えると知ったところでしたので、ちょうど良い試し場だと思ったのです。
返ってきた答えは「該当する区間は見当たりません」でした。
念のため自分でも早送りしながら見返しました。終盤、通りがかった方が一瞬だけ画面の端に入っておりました。
モデルが嘘をついたわけではありません。その区間を、そもそも観ていなかったのです。
増えたのは「どこを観るかをモデルが決める」という選択肢です
2026年9月1日、Google は Gemini 3.7 Flash・3.6 Flash・3.5 Flash-Lite にエージェント的動画理解を追加しました。動画のアップロードにも YouTube の URL にも使えて、Google AI Studio 経由の Gemini API と Gemini Enterprise Agent Platform で利用できます。
これまでの取り込み方は静的処理と呼ばれます。既定では毎秒一枚、動画全体を等間隔にフレーム化して読み込みます(FPS は API で変えられます)。尺が伸びるほどトークンが素直に膨らみますので、長尺では「高いトークンを払う」か「細部を落とす」かの二択になっておりました。
エージェント的動画理解は、そこにモデル自身の判断を挟みます。フレーム・音声・トランスクリプトを横断して必要な区間を探し、必要なら FPS を上げて観直します。公式ブログ の数字では、標準的な動画解析ベンチマークで解析コストが最大66%減、トークン消費が最大88%減、精度が最大7%向上とされています。追加の機能料金はなく、通常のトークン課金のままです。
切り替えは設定一つです。
from google import genai
client = genai.Client()
interaction = client.interactions.create(
model = "gemini-3.7-flash" ,
input = [
{
"type" : "video" ,
"uri" : VIDEO_URI , # アップロード済みファイル、または YouTube の URL
"processing" : "agentic" , # ここだけ。指定しなければ従来どおりの静的処理
},
{ "type" : "text" , "text" : "ロゴが最初に画面へ出るのは何分何秒ですか" },
],
)
print (interaction.output_text)
一行足すだけで長尺の解析が軽くなるのですから、私も最初は全部これで良いと思いました。
私が取り違えたのは「数え上げ」の一語でした
公式が挙げている用途に、動作と物体の数え上げがあります。繰り返される身体の動きや個々の物体を、時間をまたいで正確に追える、と書かれています。私はこれを読んで、動画全体で何回・何個という総数の集計まで任せられると受け取りました。
読み直すと、書かれているのは別のことでした。強いのは、注目した時間窓の中を必要に応じて高い FPS で観直し、そこにあるものを取りこぼさずに数えることです。窓をどこに置くかは、モデルが問いから判断します。
ここに段差があります。問いの中に辿る足がかりがあれば、モデルは正しい窓へ降りられます。「ロゴが最初に出るのはどこか」なら、ロゴという手がかりを頼りに探せます。ところが「映ってはいけないものが無いこと」を確かめたいとき、足がかりになる手がかりは、こちらの側にも無いのです。
私の一時間の映像で起きたのは、まさにこれでした。
同じ順番の取り違えを、私は以前に画像の側でもしております。似た絵を見つける仕組みを足す前に、そもそも何をもって重複と呼ぶのかを決めていなかったという話は、画像の重複検査に埋め込み検索を足す前に、重複の定義を三つに分ける に書きました。道具を足す前に問いを分ける、という順番は動画でも同じでした。
問いを二つに仕分けます
問いの形 例 答えが確定する条件 私の投げ先
存在を見つける ロゴが最初に出るのは何分何秒か 一致が1件見つかれば確定 エージェント的処理
区間の中で数える 12:30〜13:10 で手を挙げた回数 指定した区間を覆えば確定 エージェント的処理(区間を明示)
全域で数える 映像全体で手を挙げた回数 全区間の被覆が必要 自分で区切って静的処理
不在を確かめる 作業者以外の顔が映る区間は無いか 全区間の被覆が必要 自分で区切って静的処理
上の二つは、見つかった時点で答えが決まります。見つからなかったときに「無い」と言い切る必要がありませんので、モデルが観なかった区間があっても、答えの正しさは壊れません。
下の二つは逆です。全区間を観たという前提が崩れた瞬間に、答えの意味も一緒に消えます。「該当なし」という同じ四文字が、上では有用な返答になり、下では検証されていない主張になるのです。
見つける問いは、そのまま任せています
探す側は、公式の形のまま使っております。私が足しているのは、答えと一緒に使用量を書き出す部分だけです。
import json
import time
from pathlib import Path
from google import genai
client = genai.Client()
LOG = Path( "video_probe_log.jsonl" )
def ask_agentic (video_uri: str , question: str , model: str = "gemini-3.7-flash" ) -> str :
started = time.time()
interaction = client.interactions.create(
model = model,
input = [
{ "type" : "video" , "uri" : video_uri, "processing" : "agentic" },
{ "type" : "text" , "text" : question},
],
)
# SDK の版で属性名が揺れるため、両方を見てから諦める
usage = getattr (interaction, "usage" , None ) or getattr (interaction, "usage_metadata" , None )
record = {
"ts" : time.strftime( "%Y-%m- %d T%H:%M:%S" ),
"model" : model,
"mode" : "agentic" ,
"video" : video_uri,
"question" : question,
"elapsed_sec" : round (time.time() - started, 2 ),
"usage" : usage.to_json_dict() if hasattr (usage, "to_json_dict" ) else str (usage),
"answer" : interaction.output_text,
}
with LOG .open( "a" , encoding = "utf-8" ) as f:
f.write(json.dumps(record, ensure_ascii = False ) + " \n " )
return interaction.output_text
答えだけでなく使用量を毎回書き出しているのには理由があります。エージェント的処理では、同じ動画・同じモデルでも、問いの形によってモデルが辿る量が変わります。あとで見積もりを立て直すとき、頼れるのは自分が残した実績だけなのです。
usage を二通りの属性名で取りに行っているのは、プレビュー段階の機能に共通する保険です。片方が None を返しても記録そのものは止めたくないので、文字列化してでも残す形にしております。
「無い」を確かめる問いは、手元で区切ってから投げています
区間を API のパラメータで指定する道もありますが、私は手元で切る方を選んでおります。切り出したファイルが自分のディスクに残りますので、「この区間は確かに観た」という証拠がファイルとして手に入るからです。あとで指摘を受けたときに辿れる形が残っていることは、受託の仕事では特に大事だと感じています。
音源で似たことをしたときも、Gemini へ渡す前に自分の手で端を切り分けておくと判断が速くなりました。その顛末はループ音源のプチノイズは、Gemini に聴かせる前に波形の端で切り分けられます にまとめております。
まず、窓の並びを作って被覆を確かめます。
def plan_windows (duration_sec: float , window_sec: float = 300.0 ,
overlap_sec: float = 2.0 ) -> list[tuple[ float , float ]]:
"""[0, duration] を覆う (start, end) の並びを返す。
overlap は境界で切れた出来事を落とさないための重なり。"""
if duration_sec <= 0 :
raise ValueError ( "duration_sec must be positive" )
if overlap_sec >= window_sec:
raise ValueError ( "overlap_sec must be smaller than window_sec" )
step = window_sec - overlap_sec
windows: list[tuple[ float , float ]] = []
start = 0.0
while start < duration_sec:
end = min (start + window_sec, duration_sec)
windows.append(( round (start, 3 ), round (end, 3 )))
if end >= duration_sec:
break
start += step
return windows
def assert_covers (windows, duration_sec: float , tolerance: float = 1e-6 ) -> None :
"""隙間なく [0, duration] を覆っているかを確かめる。覆えていなければ例外。"""
if not windows:
raise AssertionError ( "no windows" )
ordered = sorted (windows)
if ordered[ 0 ][ 0 ] > tolerance:
raise AssertionError ( f "head gap: 0.0 .. { ordered[ 0 ][ 0 ] } " )
reached = ordered[ 0 ][ 1 ]
for start, end in ordered[ 1 :]:
if start > reached + tolerance:
raise AssertionError ( f "gap: { reached } .. { start } " )
reached = max (reached, end)
if reached < duration_sec - tolerance:
raise AssertionError ( f "tail gap: { reached } .. { duration_sec } " )
# 一時間三分の記録映像を、5分窓・2秒重なりで覆う
windows = plan_windows( 3_780.0 , window_sec = 300.0 , overlap_sec = 2.0 )
assert_covers(windows, 3_780.0 )
print ( len (windows), windows[ 0 ], windows[ - 1 ])
# → 13 (0.0, 300.0) (3576.0, 3780.0)
assert_covers を先に通すのは、窓の作り方を変えたときに隙間へ気づくためです。window_sec を縮めたり overlap_sec を触ったりした瞬間に、うっかり穴が空くことがあります。穴が空いた状態で走らせても処理は最後まで通ってしまいますので、機械に止めてもらう場所をここへ置いております。
切り出しは ffmpeg に任せます。
import subprocess
def cut_windows (src: str , windows, out_dir: str = "windows" ) -> list[ str ]:
Path(out_dir).mkdir( exist_ok = True )
paths = []
for i, (start, end) in enumerate (windows):
out = f " { out_dir } /win_ { i :03d } .mp4"
subprocess.run(
[
"ffmpeg" , "-hide_banner" , "-loglevel" , "error" , "-y" ,
"-ss" , f " { start } " , "-to" , f " { end } " , "-i" , src,
"-c" , "copy" , out,
],
check = True ,
)
paths.append(out)
return paths
被覆を確かめてから走らせます
各窓は静的処理のまま投げます。processing を書かないだけで既定へ戻りますので、コードの上では一行少なくなります。
def sweep_absence (window_files, question, done_path = "sweep_done.jsonl" ):
"""全窓を静的処理で走査する。1窓でも欠けたら『未確定』を返す。"""
p = Path(done_path)
done: dict[ str , str ] = {}
if p.exists():
for line in p.read_text( encoding = "utf-8" ).splitlines():
rec = json.loads(line)
done[rec[ "window" ]] = rec[ "answer" ]
for path in window_files:
if path in done:
continue
interaction = client.interactions.create(
model = "gemini-3.7-flash" ,
input = [
{ "type" : "video" , "uri" : upload_file(path)}, # processing 未指定=静的
{ "type" : "text" , "text" : question},
],
)
done[path] = interaction.output_text
with p.open( "a" , encoding = "utf-8" ) as f:
f.write(json.dumps({ "window" : path, "answer" : done[path]},
ensure_ascii = False ) + " \n " )
missing = [w for w in window_files if w not in done]
hits = [(w, a) for w, a in done.items() if not a.startswith( "該当なし" )]
if missing:
return { "verdict" : "未確定" , "covered" : len (done),
"total" : len (window_files), "missing" : missing, "hits" : hits}
return { "verdict" : "該当あり" if hits else "該当なし" ,
"covered" : len (done), "total" : len (window_files), "hits" : hits}
肝心なのは戻り値の三分岐です。「該当あり」と「該当なし」の間に「未確定」を置いておくと、途中で落ちた走査を「無かった」と読み違えずに済みます。私は表示にも covered / total を必ず出すようにしました。13分の12で止まっている状態を、数字で見えるようにしておきたかったのです。
トランスクリプトを判断材料に混ぜるときは、書き起こしの側の癖も先に確かめておく価値があります。話者ラベルや単語単位の時刻が期待どおりに返らない設定があることは、gemini-3.5-transcribe の smart モードでは、話者ラベルも単語タイムスタンプも返ってきません で書きました。
探すのはモデルに任せられます。探し終えたと決めるのは、まだ私の仕事です。
見積もりの根拠が、尺から問いへ移ります
静的処理のあいだ、トークンは尺と FPS と解像度から計算できました。動画を渡す前に月々の見当がついたのです。
エージェント的処理では、その計算が成り立ちません。モデルが辿る量は問いによって変わりますので、同じ一時間の映像でも、探しやすい問いと探しにくい問いでトークンが動きます。
エージェント的処理を選ぶ場合は、問いのテンプレートごとに使用量を残す準備を先にしておくと、あとの見積もりで困りません。そこで私は、問いにテンプレートとして名前を付け、テンプレートごとに実績を貯めるようにしました。見積もりへ使うのは平均ではなく、中央値と上振れの側です。月の請求を決めるのは上振れのほうですので、そこを見ない見積もりは外れます。
公表されている最大88%という削減率も、比較の相手が静的な毎秒一枚の分割であることを押さえておく価値があります。手がかりの薄い問いではモデルが広く辿ることになりますので、差は縮みます。切り替えそのものに追加料金はありませんから、危ないのは切り替えではなく、切り替えたあとも古い見積もり式を使い続けることのほうなのです。
いま私が切り替えている境目
場面 選んでいる処理 理由
数分の画面収録から操作の瞬間を探す 静的のまま 尺が短く、切り替えの手間のほうが大きい
10分を超える映像から、言葉にできる手がかりを探す エージェント的処理 辿る足がかりがあり、削減が効きやすい
公開前の映り込み・音声混入・表記漏れの確認 自分で区切って静的処理 不在の確認には被覆の証拠が要る
判断に迷う長尺 両方 食い違った区間だけを自分の目で観る
最後の行が、いまのところ私にはいちばん実用的です。両方走らせても追加の機能料金は発生しませんし、食い違いは「モデルが観ていない場所」を教えてくれる目印になります。個人開発でも受託でも、確認へ使える時間は限られていますので、目印があるだけで見返す範囲がずいぶん狭くなるのです。
落とし穴として先にお伝えしたいこと
-ss を -i の前に置いて -c copy で切ると、開始位置がキーフレーム境界へ寄ります。秒単位の厳密さが要る確認では、-c:v libx264 などの再エンコードに切り替えています
重なりを 0 にすると、境界をまたいだ出来事が両側で半分ずつになって見落とされます。私は 2 秒から始めて、動きの速い映像では伸ばしています
「該当なし」という文字列で判定する実装は脆いので、返答の形を JSON に固定してから比べるほうが安全です。上のコードは読みやすさを優先した形で、本番運用では構造化出力へ寄せております
窓を細かくしすぎると呼び出し回数が増えて、静的処理でも高くつきます。5分前後から始めて、映像の性質で調整しています
次にやること
手元にある長い動画を一本だけ選んで、いつも投げている問いを書き出し、「見つける問い」と「無いことを確かめる問い」に分けてみていただければと思います。分け終えた時点で、どちらの処理へ投げるべきかはほとんど決まっております。
お読みいただきありがとうございました。被覆という言葉を意識するようになったのは、私の場合この一件からでしたので、同じ躓きを一つでも減らせたら嬉しく思います。