●MODEL — Gemini 3.8 Flash reached general availability on September 2, the third Flash release in six weeks. Pricing holds at 3.7 Flash levels: $0.75 input and $3.75 output per MTok●PRICE — That introductory rate runs through December 31. From January 1, 2027 it becomes $1.50 and $7.50 per MTok, which is worth folding into next year's estimates now●BENCH — Google reports 54.9% on HLE-Verified and, on DeepSWE v1.1, results ahead of most larger frontier models, along with gains on the Vals Finance Agent V2 and Harvey Legal Agent benchmarks●EFFORT — 3.8 Flash works harder on hard problems, taking extra reasoning steps and calling tools iteratively, so token counts can rise. Where efficiency comes first, 3.7 Flash remains fully supported●CYBER — Gemini 3.8 Flash Cyber launched alongside it, tuned for vulnerability discovery and automated patching, and offered only to trusted defenders through the Fairwind Program●AUDIO — Lyria 3.5 entered public preview on September 3. It takes text and images as input and generates full-length tracks in 44.1 kHz stereo●MODEL — Gemini 3.8 Flash reached general availability on September 2, the third Flash release in six weeks. Pricing holds at 3.7 Flash levels: $0.75 input and $3.75 output per MTok●PRICE — That introductory rate runs through December 31. From January 1, 2027 it becomes $1.50 and $7.50 per MTok, which is worth folding into next year's estimates now●BENCH — Google reports 54.9% on HLE-Verified and, on DeepSWE v1.1, results ahead of most larger frontier models, along with gains on the Vals Finance Agent V2 and Harvey Legal Agent benchmarks●EFFORT — 3.8 Flash works harder on hard problems, taking extra reasoning steps and calling tools iteratively, so token counts can rise. Where efficiency comes first, 3.7 Flash remains fully supported●CYBER — Gemini 3.8 Flash Cyber launched alongside it, tuned for vulnerability discovery and automated patching, and offered only to trusted defenders through the Fairwind Program●AUDIO — Lyria 3.5 entered public preview on September 3. It takes text and images as input and generates full-length tracks in 44.1 kHz stereo
Two Kinds of Video Questions: Why I Send "Find It" and "Prove It Isn't There" Down Separate Paths
Agentic video understanding lets the model decide which parts of a video to watch. That works beautifully for finding things, and it quietly breaks when you need to prove something never appears. Here is how I split my questions, and the coverage check I now run first.
I was pulling a ninety-second excerpt out of an hour-long process recording for a client. Before cutting anything, I wanted to confirm one thing: whether anyone other than the person working had wandered into frame.
So I asked Gemini. Agentic video understanding had just shipped, and switching it on is a single field — processing set to "agentic" — so this felt like a good place to try it.
The answer came back: no matching segments found.
I scrubbed through the footage myself anyway. Near the end, someone walking past appeared at the edge of the frame for a moment.
The model had not lied to me. It had simply never looked at that stretch of video.
What actually shipped is a choice about who decides where to look
On September 1, 2026, Google added agentic video understanding to Gemini 3.7 Flash, 3.6 Flash, and 3.5 Flash-Lite. It works for uploaded files and YouTube URLs, through the Gemini API in Google AI Studio and the Gemini Enterprise Agent Platform.
The older path is called static processing. By default it samples the whole video at one frame per second, evenly, end to end — and the FPS is adjustable through the API. Tokens grow with duration, so on long footage you were choosing between paying for the tokens or dropping detail.
Agentic processing inserts the model's own judgment into that step. It searches across frames, audio, and transcript, opens only the segments it decides it needs, and re-watches at a higher frame rate when it wants to. Google's announcement post reports up to 66% lower analysis cost, up to 88% fewer tokens, and up to 7% better accuracy on standard video analysis benchmarks. There is no extra feature fee — it bills at ordinary token rates.
The switch is one line.
from google import genaiclient = genai.Client()interaction = client.interactions.create( model="gemini-3.7-flash", input=[ { "type": "video", "uri": VIDEO_URI, # an uploaded file, or a YouTube URL "processing": "agentic", # omit this and you get the old static path }, {"type": "text", "text": "At what timestamp does the logo first appear on screen?"}, ],)print(interaction.output_text)
One line for a dramatically lighter long-video pipeline. My first instinct was to move everything over.
The word I misread was "counting"
Among the listed capabilities is counting actions and objects — accurately tracking repeated physical movements and distinct objects over time. I read that as permission to ask for totals across a whole video.
Reading it again, it says something narrower. The strength is counting what is inside a time window it has chosen to inspect, resampling at a higher FPS when the motion is fast. Where to put that window is a decision the model makes from your question.
That is where the step is. If the question contains a handhold, the model can climb down to the right window. "Where does the logo first appear" gives it the logo. But when you want to confirm that something you don't want is absent, neither of you has a handhold — that is the whole problem.
Which is exactly what happened to my hour of footage.
I have made this same ordering mistake on the image side too. Before adding a similarity search, I had never written down what counted as a duplicate in the first place, which I wrote up in splitting the definition of duplicate into three before adding embedding search. Define the question before you add the tool — video turned out to be no different.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll be able to sort any video question into find-it or prove-absence, and pick agentic or static processing from your own requirements instead of guessing
✦You'll be able to stop trusting a no matches found answer that came from segments the model never actually opened, by adding one coverage check before you publish
✦You'll be able to estimate what the up-to-88% token reduction really looks like on your footage, based on the shape of your question rather than the length of the video
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The top two resolve the moment something is found. They never need to claim "there is none," so segments the model skipped cannot corrupt the answer.
The bottom two are the opposite. The instant "we watched all of it" stops being true, the answer stops meaning anything. The same three words — no matches found — are a useful reply above the line and an unverified claim below it.
Find-it questions go straight through
For searching, I use the official shape as-is. The only thing I add is writing the usage figures down next to the answer.
import jsonimport timefrom pathlib import Pathfrom google import genaiclient = 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}, ], ) # attribute naming shifts between SDK releases, so try both before giving up usage = getattr(interaction, "usage", None) or getattr(interaction, "usage_metadata", None) record = { "ts": time.strftime("%Y-%m-%dT%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
There is a reason the usage goes in every time. Under agentic processing, the same video and the same model consume different amounts depending on how the question is shaped. When you later need to rebuild a cost estimate, your own logged history is the only thing you can lean on.
Reaching for the attribute under two different names is the kind of insurance preview features tend to need. If one returns None I would still rather keep the record, even as a string, than lose the row.
Prove-absence questions get cut up on my machine first
You can name a time range through API parameters, but I cut the file locally instead. The clips stay on my disk, which means "this stretch was definitely reviewed" exists as an artifact I can hand back. On client work especially, being able to retrace a check months later matters more than saving a step.
The windows come first, and the coverage check comes before anything is uploaded.
def plan_windows(duration_sec: float, window_sec: float = 300.0, overlap_sec: float = 2.0) -> list[tuple[float, float]]: """Return (start, end) pairs covering [0, duration]. The overlap keeps events that straddle a boundary from being halved.""" 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 windowsdef assert_covers(windows, duration_sec: float, tolerance: float = 1e-6) -> None: """Raise unless the windows cover [0, duration] without gaps.""" 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}")# A 63-minute recording, five-minute windows, two seconds of overlapwindows = 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 runs first so that changing the window math surfaces a hole immediately. Shrink window_sec or nudge overlap_sec and you can open a gap without noticing — and a sweep with a hole in it still runs to completion and still prints an answer. This is the spot where I want a machine to stop me.
Sweeping, with coverage reported alongside the verdict
Each window goes through static processing. Leaving processing out returns you to the default, so the sweep is actually one line shorter than the search.
def sweep_absence(window_files, question, done_path="sweep_done.jsonl"): """Scan every window with static processing. A single missing window yields 'undetermined' rather than 'none found'.""" 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)}, # no processing key = static {"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("NONE")] if missing: return {"verdict": "undetermined", "covered": len(done), "total": len(window_files), "missing": missing, "hits": hits} return {"verdict": "found" if hits else "none", "covered": len(done), "total": len(window_files), "hits": hits}
The three-way return value is the point. Putting undetermined between found and none means an interrupted sweep can never be misread as a clean result. I print covered / total in the output too — I wanted twelve-of-thirteen to look like twelve of thirteen.
Searching is something I can delegate. Deciding that the search is finished is still my job.
Your cost estimate stops being a function of duration
Under static processing, tokens fell out of duration, FPS, and resolution. You could work out the monthly figure before uploading anything.
Agentic processing breaks that arithmetic. How much the model traverses depends on the question, so the same hour of footage costs differently depending on how easy it is to find what you asked for.
So I name my questions as templates and accumulate real usage per template. What I estimate from is not the mean but the median and the upper tail — the tail is what decides the invoice, and an estimate that ignores it will be wrong.
It also helps to remember what the up-to-88% figure is measured against: static sampling at one frame per second. On questions with thin handholds the model ranges more widely and the gap narrows. Since switching costs nothing extra, the risk is not the switch — it is continuing to use the old estimating formula afterwards.
Where I currently draw the line
Situation
Processing I pick
Why
Finding a moment in a few minutes of screen capture
Static, unchanged
Too short for the switch to earn back the effort
Searching 10+ minutes with a describable handhold
Agentic
There is something to climb down to, so the savings land
Pre-publication checks for stray faces, bleed-through audio, missing credits
My own windows, static
Proving absence needs evidence of coverage
Long footage where I am genuinely unsure
Both
Only the disagreements get my own eyes
That last row has turned out to be the most useful one. Running both costs no extra feature fee, and the disagreements are a map of where the model chose not to look. As an indie developer splitting time between my own apps and client work, I have a fixed and small budget for reviewing footage — a map that narrows the rewatch is worth a lot.
Things worth knowing before you build this
Putting -ss before -i with -c copy snaps the start to a keyframe boundary. When the check needs second-level precision, I re-encode with -c:v libx264 instead
With zero overlap, an event straddling a boundary shows up as two halves and gets missed by both. I start at two seconds and lengthen it for fast-moving footage
Matching on a literal phrase to detect "nothing found" is brittle. Pin the response to a JSON shape and compare fields — the code above favours readability, and in production I use structured output
Very small windows drive up call counts until static processing gets expensive too. I start around five minutes and adjust to the material
What to do next
Pick one long video you already have, write out the questions you habitually ask it, and sort them into find-it and prove-absence. By the time the sorting is done, which processing mode each one belongs to has mostly answered itself.
Thank you for reading. Coverage was not a word I thought about until this particular hour of footage taught me to, and if it saves you one version of that afternoon, I will be glad.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.