個人開発で短尺の紹介動画をいくつか扱うようになってから、動画を「理解させる」処理がいちばん重い工程になっていました。ffmpeg で毎秒フレームを切り出し、1枚ずつモデルに投げて説明を集め、最後にもう一度まとめる。動くには動くのですが、1本の動画を1回さばくのに7〜9回の API 呼び出しが走り、音声はまるごと捨てていました。
Omni Flash が公開プレビューに入り、動画をそのまま渡して理解させる経路が現実的になりました。以下では、私が実際にフレーム抽出前提の構成から1パスへ寄せたときの最小コード、相対的な計測、そして「ここから先はフレーム抽出を残したほうがよい」という境界を、判断できる形でまとめます。
フレーム抽出前提の3段パイプラインが抱えていた負債
これまで使っていた構成は、抽出・記述・要約の3段でした。コードにすると負債の場所がはっきりします。
import subprocess, os
from google import genai
client = genai.Client()
def extract_frames(video_path: str, fps: float = 1.0, out_dir: str = "frames") -> list[str]:
os.makedirs(out_dir, exist_ok=True)
subprocess.run([
"ffmpeg", "-i", video_path,
"-vf", f"fps={fps}", f"{out_dir}/f_%04d.jpg",
], check=True)
return sorted(os.path.join(out_dir, f) for f in os.listdir(out_dir))
def describe_video(video_path: str) -> str:
frames = extract_frames(video_path, fps=1.0)
notes = []
for i, path in enumerate(frames): # フレーム枚数ぶん呼び出しが増える
img = client.files.upload(file=path)
r = client.models.generate_content(
model="gemini-3.5-flash",
contents=[img, f"{i}秒付近のフレームです。写っているものを一文で。"],
)
notes.append(f"[{i}s] {r.text.strip()}")
summary = client.models.generate_content( # さらに二次要約でもう1回
model="gemini-3.5-flash",
contents=["以下は毎秒のフレーム記述です。動画全体を3行で要約してください。\n" + "\n".join(notes)],
)
return summary.text負債は3つあります。第一に、呼び出し回数が動画の長さに比例して増えます。第二に、音声トラックを一切見ていないため、ナレーションや効果音に依存する内容を取りこぼします。第三に、フレームを時系列インデックスで並べているだけなので、モデルは「動き」を推論できず、静止画の羅列として扱います。私の用途では、この3つ目が精度の頭打ちになっていました。
Omni Flash に動画をそのまま渡す最小構成
Omni Flash はネイティブに動画を扱うため、Files API でアップロードした動画を1回の generate_content に渡すだけで済みます。構造化出力と組み合わせると、後段のパースも消えます。
import time
from google import genai
from pydantic import BaseModel
client = genai.Client()
class VideoReport(BaseModel):
summary: str
spoken_language: str
has_music: bool
safe_for_all_ages: bool
key_moments: list[str]
def understand_video(video_path: str) -> VideoReport:
f = client.files.upload(file=video_path)
# アップロード直後は PROCESSING。ACTIVE になるまで待たないと 400 になる
while f.state.name == "PROCESSING":
time.sleep(2)
f = client.files.get(name=f.name)
if f.state.name != "ACTIVE":
raise RuntimeError(f"upload failed: {f.state.name}")
r = client.models.generate_content(
model="gemini-omni-flash-preview", # 公開プレビュー。実IDは changelog の表記に合わせる
contents=[f, "この動画を通しで理解し、指定スキーマで返してください。"],
config={
"response_mime_type": "application/json",
"response_schema": VideoReport,
},
)
return r.parsed呼び出しは1回です。映像・音声・時間経過をひとつのコンテキストで見るため、has_music や spoken_language のように、フレーム抽出版では取れなかった軸が同時に返ります。私は最初、フレーム版の出力に寄せてテキスト要約だけを受け取っていましたが、response_schema を渡して必要な軸を明示したほうが、後続の分岐がそのまま書けて扱いやすかったです。
Before / After の差はコード量以上に「何を捨てているか」に出ます。Before は音声と動きを捨てて呼び出し回数を払い、After はその両方を1回で拾います。