8 min read

Frame sampling with VLM: how a local AI model understands video frame by frame

Frame sampling with VLM: how a local AI model understands video frame by frame

Part of AI Tools & Automation — automating real work with local AI models.

✅ REAL DATA — Per-frame timing measured with 08_video_frame_benchmark.py · 17 sampled frames from a real 17-minute video · Qwen3.6-35B-A3B (multimodal, mmproj-F32) served locally · AMD PRO W7900 48 GB · Windows 11. The frame descriptions below are real model output.
A local vision model analysing sampled frames from a video
A VLM does not watch video — it reads sampled frames. Pick the right interval and it reconstructs the whole story. Photo: Pixabay / CC0.

A vision-language model cannot watch a video the way you do. It reads still images. So to make a VLM understand a video, you sample frames at intervals and describe each one. String those descriptions together and you get a timeline of what happened, without ever sending the video to the cloud.

This article builds that frame-sampling pipeline and measures it on a real 17-minute recording. It answers the two questions that matter: how fast is it, and does the result actually make sense?

Key takeaways
  • Sampling frames turns video understanding into a series of cheap image queries.
  • The local VLM described each frame in 1.77 s on average, about 34 frames per minute.
  • One frame per minute reconstructed the video’s full storyline accurately.
  • The sampling interval is the key dial: more frames mean more detail and more time.

Why sample frames instead of watching video?

A minute of video at 30 fps is 1 800 frames. Feeding every frame to a VLM would be slow and pointless, because consecutive frames barely differ. Sampling solves both problems. You take one frame every few seconds, describe it, and skip the near-duplicates in between.

The interval sets the trade-off. A wide interval is fast but can miss brief events. A narrow interval catches everything but costs more time. For a talking-head or screen-recording video, one frame per minute already captures each scene change, as the benchmark below shows.

Pipeline architecture

The pipeline has three stages. ffmpeg extracts frames at a fixed rate, the VLM describes each frame, and a final step joins the descriptions into a timeline. Each stage is small and independent.

  1. Extract — ffmpeg samples one frame every N seconds and resizes it for the model.
  2. Describe — the local VLM answers “what happens in this frame?” per image.
  3. Assemble — the timestamped descriptions become a readable summary.

Implementation — frame extraction and description

ffmpeg does the sampling in one command. The fps=1/60 filter keeps one frame per minute, and the scale filter caps the width so the model spends tokens on content, not resolution.

# one frame per minute, 1024 px wide
ffmpeg -i video.mp4 -vf "fps=1/60,scale=1024:-1" frames/frame_%02d.jpg

Each frame then goes to the VLM through the OpenAI-compatible vision API. Disabling thinking keeps the answer short and fast:

Free ebook

Free AI Video, Generated Locally

Working scripts and measured benchmarks. Free.

No spam. Unsubscribe at any time.

import base64
from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:8082/v1", api_key="not-needed")

def describe(path: str) -> str:
    b64 = base64.b64encode(open(path, "rb").read()).decode()
    resp = client.chat.completions.create(
        model="local-model",
        messages=[{"role": "user", "content": [
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
            {"type": "text", "text": "Describe what happens in this video frame in one sentence."},
        ]}],
        max_tokens=160, temperature=0.1,
        extra_body={"chat_template_kwargs": {"enable_thinking": False}},
    )
    return resp.choices[0].message.content.strip()

Benchmark methodology

The test extracted one frame per minute from a 17-minute video, giving 17 frames. Each frame was described once by the VLM, and the script recorded the processing time per frame. Seventeen frames give seventeen independent timing measurements of the same task, so the mean is stable.

Speed results

VLM processing time per sampled video frame
Chart 1: Processing time per frame across the video. The dashed line marks the 1.77 s mean. Qwen3.6-35B-A3B vision on AMD PRO W7900.
MetricValue
Frames analysed17 (one per minute)
Mean time per frame1.77 ± 0.34 s
Throughput~34 frames / minute
Total for the 17-min video~30 s

The whole 17-minute video was summarised in about half a minute of compute. The first frame ran slightly slower because of the one-time warm-up, and every frame after settled near 1.7 s. At 34 frames per minute, even a dense sampling rate stays practical.

Cumulative time to analyse the sampled video frames
Chart 2: Cumulative analysis time as frames are processed. The line stays almost straight, so cost scales linearly with frame count.

The reconstructed timeline — real output

Speed means nothing if the descriptions are wrong. They were not. Here is the actual, unedited timeline the model produced from the sampled frames:

TimeModel’s description of the frame
0:00A man with a beard and headset in a circular overlay while a software settings window is shown.
2:00A user navigates the sidebar of an AI chat application, hovering over the model selector.
4:00The presenter shows a slide titled “LOCAL AI”, illustrating how a laptop processes a question locally.
6:00A user configures and loads the Gemma 4 model in a desktop application, with memory shown.

Read top to bottom, the descriptions tell the video’s story: a presenter introduces local AI, opens a chat app, explains an offline architecture, then loads a model. The model even read on-screen text like the slide title and the model name. That is a genuine, searchable summary built entirely from still frames.

Choosing the sampling rate

The interval is the one dial worth tuning. For slow content — talks, tutorials, screen recordings — one frame per minute is plenty, and it kept the storyline intact here. For fast content — sports, action, rapid cuts — drop to one frame every few seconds so brief events are not missed.

Because cost scales linearly with frame count, the maths is simple. Halving the interval doubles the frames and roughly doubles the time. Start wide, then narrow the interval only around the moments you care about, which keeps the total cheap.

Sending multiple frames at once

For tighter context, you can send several frames in a single request and ask the model to describe the sequence. It then reasons about motion and change between frames, not just each still in isolation. This works well for short clips where continuity matters, though it uses more tokens per call.

content = []
for path in ["f1.jpg", "f2.jpg", "f3.jpg"]:
    b64 = base64.b64encode(open(path, "rb").read()).decode()
    content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}})
content.append({"type": "text", "text": "These are consecutive frames. Describe what changes across them."})

Detecting scene changes automatically

Because each frame has a text description, you can spot scene changes without any video-processing library. Compare consecutive descriptions, and when they diverge sharply, a new scene has begun. A cheap way to measure that divergence is word overlap: low overlap means the content shifted.

def scene_changed(prev: str, curr: str, threshold: float = 0.4) -> bool:
    a, b = set(prev.lower().split()), set(curr.lower().split())
    overlap = len(a & b) / max(1, len(a | b))
    return overlap < threshold   # low overlap = likely a new scene

Run that across the timeline and you get chapter markers for free. In the test video, the descriptions shifted clearly when the presenter moved from a slide to a live application, exactly where a human would draw a chapter boundary.

Building a searchable video index

The real payoff is search. Store each frame’s timestamp and description in a small database, and a video becomes queryable text. Ask “when does the Gemma model get loaded?” and a keyword or embedding search returns the exact minute.

index = [{"t": r["t_seconds"], "text": r["description"]} for r in frames]
hits = [r for r in index if "gemma" in r["text"].lower()]
# -> jump straight to the frames that mention Gemma

Scale that across a whole channel and you have a search engine over video content, built with nothing but ffmpeg, a local VLM, and a text index. No frames ever leave the machine, and the running cost is only electricity.

Use cases

  • Video search: index every frame’s description so you can find the moment a topic appears.
  • Content moderation: flag frames that match a policy without a human watching the whole video.
  • Meeting and webinar notes: turn a screen recording into a timestamped outline.
  • Highlight detection: spot scene changes by comparing consecutive descriptions.

Frequently asked questions

How many frames per minute can the model handle?

About 34 on this hardware, at 1.77 s per frame. That is fast enough to analyse a long video in seconds and to sample densely when a scene demands it.

Does one frame per minute lose important moments?

For slow content, rarely. It reconstructed this video’s full storyline. For fast cuts or brief events, narrow the interval so nothing slips between samples.

Can the model read text on screen?

Yes. It read slide titles and the on-screen model name directly from the frames, which makes it strong for tutorials and screen recordings.

Is it better to send frames one by one or together?

One by one is cheaper and scales linearly. Send several together only when you need the model to reason about change and motion across a short sequence.

Does the video ever leave my machine?

No. ffmpeg and the VLM both run locally, so the frames and the video stay on your own hardware from start to finish.

Summary

Frame sampling turns video understanding into a stream of quick image queries. On an AMD W7900, a local VLM described each frame in about 1.77 seconds — roughly 34 frames per minute — and one frame per minute was enough to reconstruct a 17-minute video’s storyline accurately, on-screen text included. The whole video was summarised in about half a minute of compute, entirely offline. Reproduce every number with the script: 08_video_frame_benchmark.py.

Companion code — every script and benchmark from this series lives in one repository: github.com/bestin-it/qwen-local-ai-scripts.

Free ebook

Free AI Video, Generated Locally

Run Wan 2.1 in ComfyUI on your own GPU — the scripts I use, measured times, sample clips. No cloud, no API keys.

No spam. Unsubscribe at any time.

Artur Poniedziałek
Artur Poniedziałek
IT Expert & Project Manager
🤖 AI ⚡ PM 🐍 Python 🖥️ Local AI

IT Expert & Project Manager with 15+ years of experience. Exploring practical AI applications — from local LLMs and RAG systems to workflow automation. Writing to share knowledge and inspire others to experiment with new technologies.

Leave a Reply

Your email address will not be published. Required fields are marked *