Why build one
A short video can only go viral if people watch past its opening, and in a feed that decision takes a few seconds. If the opening doesn't hold the viewer, the rest of the video is never seen. Creators are the worst-placed people to judge this: they already know what the video is about, so they can't see the opening the way a stranger scrolling past would.
A virality checker gives you that stranger's first impression on demand. Be clear about what it checks. It can't predict views: topic, timing, audience and distribution all decide that, and none of them are in the clip. What it can do is grade the one thing every viral video has to get right first: whether the opening earns the next second. That's the checker you'll build here.
What you'll build
A model call on its own is not a checker. Its output format drifts, providers time out, and it will happily fill in a number it should have left blank. Most of the work is the harness: the plain code around the model that decides what goes in, what may come out, and what happens when something fails. You'll build it in nine steps:
- Cut the first 10 seconds of the video.
- Write the checklist the model grades against.
- Pin down the exact shape of the answer.
- Write the prompt.
- Call the cheapest Gemini Flash model.
- Validate the answer, and never fill in a missing value.
- Fall back to a second model, on a clock.
- Put a ceiling on spend.
- Put it together and run it.
You need Python 3.10+, ffmpeg, and a Gemini API key in GEMINI_API_KEY.
pip install google-genai pydantic
What we've tested. We ran a hosted version of this checker on our site, called Hook check. It cut the clip in the browser and called Gemini 2.5 Flash-Lite through OpenRouter; we've since retired it. The code below is the same design rewritten for Google's own Python SDK and today's cheapest Flash model. We've run it end to end against a mocked API, not against real videos. Add your key to run it for real.
Step 1: Cut the opening
Send the model only what it's grading. Re-encoding the first 10 seconds at 2.5 Mbps keeps every request around 3 MB however long or sharp the original is, and it's the only part of the video that leaves your machine. Keep the audio: the model can use it.
import subprocess
CLIP_SECONDS = 10
def cut_opening(src: str, dst: str = "opening.mp4") -> str:
"""Re-encode the first CLIP_SECONDS of a video at 2.5 Mbps."""
subprocess.run(
["ffmpeg", "-y", "-i", src, "-t", str(CLIP_SECONDS),
"-c:v", "libx264", "-b:v", "2.5M", "-vf", "scale='min(1280,iw)':-2",
"-c:a", "aac", "-movflags", "+faststart", dst],
check=True, capture_output=True,
)
return dst
If you'd rather build a web version, the browser's MediaRecorder API does the same job without a server. One catch: browsers that can't record a video element directly fall back to recording a canvas, which drops the audio.
Step 2: Write the checklist
Don't ask the model "will this go viral?" Give it five specific things to check, each with a short name it can echo back and a rule it can reason about. These are common practice for short-form openings, not laws, so change them to fit your format:
RUBRIC = [
("First 3 seconds", "The hook lands within the first three seconds of playback."),
("Earned attention", "The opening earns the next second without assuming the viewer cares yet."),
("No bait", "No fake cliffhanger or clickbait trick used just to force a watch."),
("On-screen text", "Key information is readable with the sound off."),
("Sets expectations", "The viewer knows what the video is about right away."),
]
Step 3: Pin down the shape of the answer
Decide exactly what comes back before you write the prompt: a 0–100 score, a one-sentence verdict, a pass or fail with a note for every checklist item (failures included), and an attention estimate for each second from 0 to 10. A Pydantic model states all of that in one place, and the same class does double duty: the SDK sends it to Gemini as the response schema, and you use it to check the reply.
from pydantic import BaseModel, Field
class RubricItem(BaseModel):
item: str
passed: bool
note: str = Field(description="One sentence, specific to this clip.")
class AttentionPoint(BaseModel):
t: int = Field(ge=0, le=CLIP_SECONDS)
attention: int = Field(ge=0, le=100)
class HookGrade(BaseModel):
score: int = Field(ge=0, le=100, description="Chance this opening stops a scroll.")
verdict: str = Field(description="One plain-language sentence explaining the score.")
checklist: list[RubricItem] = Field(min_length=len(RUBRIC), max_length=len(RUBRIC))
curve: list[AttentionPoint] = Field(min_length=CLIP_SECONDS + 1, max_length=CLIP_SECONDS + 1)
The attention curve is the model's guess from motion, cuts, faces, on-screen text and pacing. It is not eye-tracking or real retention data, and you shouldn't present it as either.
Step 4: Write the prompt
The prompt states the task, lists the checklist in order, and repeats the two rules models most often break: one entry per checklist item even when it fails, and one curve point per second.
def build_prompt() -> str:
rubric = "\n".join(f"{i}. {name}: {rule}" for i, (name, rule) in enumerate(RUBRIC, 1))
return (
"This is the opening of a short-form video. Judge it as a hook that must stop "
f"someone from scrolling. Judge only the first {CLIP_SECONDS} seconds.\n\n"
f"Score it against this checklist, in this order:\n{rubric}\n\n"
"Return one checklist entry per item, in order, including the ones that fail. "
f"Return one curve point per second from 0 to {CLIP_SECONDS}: your estimate of "
"viewer attention at that second, from motion, cuts, faces, on-screen text and pacing."
)
Step 5: Call the cheapest Flash model
Grading a 10-second clip against a fixed checklist needs video input and good instruction-following, not deep reasoning. That's what the bottom tier of the Flash family is for. As of this writing the cheapest one on Google's price list is gemini-3.1-flash-lite.
from google import genai
from google.genai import types
client = genai.Client() # reads GEMINI_API_KEY from the environment
def grade_once(clip_path: str, model: str = "gemini-3.1-flash-lite") -> str:
with open(clip_path, "rb") as f:
video = types.Part.from_bytes(data=f.read(), mime_type="video/mp4")
response = client.models.generate_content(
model=model,
contents=[video, build_prompt()],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=HookGrade,
max_output_tokens=1500,
thinking_config=types.ThinkingConfig(thinking_level="minimal"),
),
)
return response.text or ""
Three settings do the work:
response_schema=HookGrademakes Gemini return JSON in your shape, so you aren't scraping a score out of prose.thinking_level="minimal"keeps reasoning to a minimum. Thinking tokens are billed as output, and output is the expensive side of this call. Flash-Lite can't turn thinking fully off, so this is as low as it goes.max_output_tokens=1500caps the worst case. A complete answer is a few hundred tokens.
A 3 MB clip fits inline in the request. For longer clips, upload through the Files API first.
Step 6: Validate, and never invent a value
A schema makes a good reply likely, not certain. Check every reply against the same Pydantic model, and if anything is missing or out of range, return nothing rather than a guess. Defaulting a missing score to 0 would show you a grade the model never gave.
from pydantic import ValidationError
def parse(raw: str) -> HookGrade | None:
"""A grade, or None. Never fill in a missing value with a default."""
try:
return HookGrade.model_validate_json(raw)
except ValidationError:
return None
When parsing fails, show the model's raw reply and say it couldn't be scored. That's what our hosted version did.
Step 7: Fall back, on a clock
Providers rate-limit and time out. Walk a short list of models in order and return the first valid grade. Keep the list in one family so scores stay comparable when the fallback fires, and give the whole request a time budget so a slow provider can't hang it: 45 seconds for any one model, 75 for the request, and no new attempt with under 5 seconds left. Those are the numbers our hosted version ran with, chosen to finish before our host's request timeout.
import time
MODEL_CHAIN = ["gemini-3.1-flash-lite", "gemini-3.5-flash-lite"]
TOTAL_BUDGET_S = 75 # the whole request
PER_MODEL_S = 45 # any one model
MIN_ATTEMPT_S = 5 # don't start a call that can't finish
def grade(clip_path: str) -> dict:
deadline = time.monotonic() + TOTAL_BUDGET_S
raw = ""
for model in MODEL_CHAIN:
remaining = deadline - time.monotonic()
if remaining < MIN_ATTEMPT_S:
break
try:
raw = run_with_timeout(grade_once, clip_path, model,
timeout=min(PER_MODEL_S, remaining))
except Exception:
continue # rate limit, provider error or timeout: try the next model
result = parse(raw)
if result is not None:
return {"ok": True, "model": model, "grade": result}
return {"ok": False, "raw": raw} # show the raw reply; don't invent a score
The timeout helper:
from concurrent.futures import ThreadPoolExecutor
_pool = ThreadPoolExecutor(max_workers=4)
def run_with_timeout(fn, *args, timeout: float):
return _pool.submit(fn, *args).result(timeout=timeout)
Step 8: Put a ceiling on spend
Running the checker on your own videos costs pennies. If you put it on the web for others, it needs limits before it needs features. These are the ones our hosted version enforced on its server, which also kept the API key out of the browser:
- 10 checks per visitor per 10 minutes, and 40 per day.
- 1,000 checks per day across everyone.
- At most 4 model calls in flight at once.
- Uploads capped at 12 MB and 20 seconds.
- Requests accepted only from your own site, and an in-flight call cancelled if the visitor leaves.
With a daily cap in place, the worst-case bill is easy to state. More on cost below.
Step 9: Put it together
Save steps 1 to 7 in one file, checker.py, and add a small entry point:
import sys
if __name__ == "__main__":
result = grade(cut_opening(sys.argv[1]))
if not result["ok"]:
print("Couldn't score this clip. Raw reply:\n" + result["raw"])
sys.exit(1)
g = result["grade"]
print(f"{g.score}/100 ({result['model']}): {g.verdict}")
for item in g.checklist:
print(f" {'PASS' if item.passed else 'FAIL'} {item.item}: {item.note}")
Then point it at a video:
python checker.py my_video.mp4
You get a score, a one-line verdict and a note on each checklist item, which is usually the most useful part: it tells you what to fix in the opening before you post.
What it costs
Gemini reads video at one frame per second, about 258 tokens a frame, plus 32 tokens per second of audio. A 10-second clip and the prompt come to about 3,400 input tokens. Price that out:
PRICES = { # $ per 1M tokens: (video and text in, audio in, out). Oct 21, 2026.
"gemini-3.1-flash-lite": (0.25, 0.50, 1.50),
"gemini-3.5-flash-lite": (0.30, 0.30, 2.50),
"gemini-3.8-flash": (0.75, 0.75, 3.75), # no audio price listed; input price used
}
def cost_per_check(model, seconds=10, prompt_tokens=500, output_tokens=500):
p_in, p_audio, p_out = PRICES[model]
video_tokens = 258 * seconds # one frame per second
audio_tokens = 32 * seconds
return ((video_tokens + prompt_tokens) * p_in
+ audio_tokens * p_audio
+ output_tokens * p_out) / 1_000_000
| Model | Typical reply | Longest reply |
|---|---|---|
gemini-3.1-flash-lite | $0.0017 | $0.0032 |
gemini-3.5-flash-lite | $0.0023 | $0.0048 |
gemini-3.8-flash | $0.0044 | $0.0082 |
Typical reply: 500 output tokens. Longest: the 1,500-token cap. The full Flash model's list prices are scheduled to double on Jan 1, 2027.
On the cheapest model a check costs under a fifth of a cent, so 1,000 checks a day is a few dollars. Notice where the money goes: at the 1,500-token cap, the reply costs more than the whole video. Keeping the answer short saves more than shrinking the clip.
Optional: make grades steadier
One sample per clip is noisy: check the same video twice and the score can move. A cheap fix is to grade three times, report the median score, and pass a checklist item only if most runs pass it. The spread between runs tells you how sure to be. Three Flash-Lite calls cost about the same as one call to the full Flash model. We haven't tested this on real videos yet.
from statistics import median
def grade_with_votes(clip_path: str, n: int = 3) -> dict | None:
grades = [g["grade"] for g in (grade(clip_path) for _ in range(n)) if g["ok"]]
if not grades:
return None
scores = [g.score for g in grades]
passed = [sum(g.checklist[k].passed for g in grades) > len(grades) / 2
for k in range(len(RUBRIC))]
return {"score": median(scores), "spread": max(scores) - min(scores), "passed": passed}
Check your checker before you trust it
We haven't measured how well these grades match reality, and you shouldn't trust yours until you have. Three checks, in order:
- Does it agree with itself? Grade each video several times. If the scores swing widely, nothing else matters yet.
- Does it agree with people? Have two people apply the same checklist without seeing the model's answers. Compare the model with each of them, and the two people with each other. How often the people agree is the ceiling.
- Does it track real viewers? For your own videos, compare the score with the share of viewers still watching at three seconds, from the platform's analytics. Compare within one account, since audience and timing differ across accounts.
from scipy.stats import spearmanr
# One row per video from the same account: the checker's score, and the share
# of viewers still watching at 3 seconds from the platform's analytics.
rho, p_value = spearmanr(hook_scores, held_at_3s)
A clear positive rank correlation across enough videos is the evidence that the score means something.
Limits
- It checks the opening, not virality. A strong hook is necessary for a video to spread, not sufficient. Topic, timing, audience and distribution all matter, and none of them are in the clip.
- The checklist is a heuristic. It encodes common advice and hasn't been validated against retention. The on-screen text rule, for one, penalizes formats that work fine without it.
- Prices and models change. Swap the model name in step 5 and the prices in the cost code when they do.
Sources
- Google, Gemini Developer API pricing.
- Google, Gemini thinking, for thinking levels on Flash-Lite.
- S. Anand, How does Gemini process videos?, for tokens per frame and per second of audio.