
# Introduction
Most video understanding instruments fall into one in all two camps. The primary camp requires a cloud API; your footage is uploaded, processed on another person’s servers, and billed per minute of video. The second camp runs domestically however calls for the type of GPU cluster most builders would not have: 70B+ fashions that want a number of A100s and take minutes per clip. Neither choice works for a developer who desires to course of a day’s price of assembly recordings, a lecture collection, or safety footage on a workstation they already personal.
SmolVLM2-2.2B-Instruct, launched by Hugging Face on February 20, 2025, adjustments the calculation. It runs on 5.2 GB of GPU RAM, an RTX 3060, a MacBook Professional M2, and the free Google Colab T4 tier. On Video-MME, the usual long-form video understanding benchmark, it outperforms each present 2B-scale mannequin. That mixture, shopper {hardware} paired with outcomes that really maintain up, is what this text is constructed round.
The undertaking we are going to construct on this article: an area pipeline that takes any video file, extracts frames at configurable intervals, analyzes them in batches with SmolVLM2-2.2B, and outputs a structured JSON abstract, together with per-frame scene descriptions, key moments with timestamps, motion gadgets, and a ultimate narrative. The identical pipeline handles assembly recordings, lectures, and surveillance footage with out altering a line of code.
# SmolVLM2-2.2B
The rationale SmolVLM2-2.2B can run on an RTX 3060 whereas outperforming bigger fashions on video duties is a design choice about how photographs are tokenized.
Most vision-language fashions tokenize photographs at excessive density. Qwen2-VL, for instance, makes use of as much as 16,000 tokens to signify a single picture. Feeding 50 frames to such a mannequin at that density would devour 800,000 tokens, far past any shopper GPU’s context funds. SmolVLM2 makes use of a pixel shuffle technique that compresses every 384×384 picture patch to 81 tokens. Fifty frames turn out to be roughly 4,050 picture tokens, manageable in a single inference name. That compression is why SmolVLM2’s prefill throughput runs 3.3 to 4.5 occasions sooner and era throughput runs 7.5 to 16 occasions sooner than Qwen2-VL-2B, not as a advertising and marketing declare however as a direct consequence of the token funds distinction.
The mannequin is available in three sizes. The 256M and 500M variants are designed for cellular and edge units; the 256M can run on a cellphone. The two.2B is the suitable alternative for this pipeline. It’s the solely dimension with robust sufficient video benchmark scores to supply dependable multi-scene summaries: Video-MME of 52.1, MLVU of 55.2, and MVBench of 46.27, in opposition to the 500M’s 42.2, 47.3, and 39.73, respectively.
The video understanding strategy can also be price understanding earlier than you write any code. SmolVLM2 doesn’t have a local video encoder; it treats video as a sequence of photographs. The official reference pipeline extracts as much as 50 evenly sampled frames per video, bypasses inner body resizing, and passes them as a multi-image sequence inside a single chat message. That strategy scored 27.14% on CinePile, positioning it between InternVL2 (2B) and Video-LLaVA (7B) on cinematic video understanding, a powerful outcome given the mannequin’s dimension and that video was not the one factor it was skilled for.
# Setting Up the Atmosphere
{Hardware} necessities:
| Characteristic | Minimal | Really helpful |
|---|---|---|
| GPU VRAM | 6 GB (RTX 3060) | 12–16 GB (RTX 4080) |
| Apple Silicon | M2 8 GB (MPS path) | M2 Professional / M3 16 GB |
| System RAM | 16 GB | 32 GB |
| Disk | 10 GB free | 20 GB+ SSD |
| Colab | T4 (free tier) | A100 (Colab Professional) |
Python packages:
# Python 3.10+ required
python --version
python -m venv smolvlm2-env
supply smolvlm2-env/bin/activate # macOS / Linux
smolvlm2-envScriptsactivate # Home windows
# Set up from the secure SmolVLM-2 department -- required for SmolVLM2 help
pip set up git+https://github.com/huggingface/transformers@v4.49.0-SmolVLM-2
# Core dependencies
pip set up torch torchvision --index-url https://obtain.pytorch.org/whl/cu121
pip set up
opencv-python
Pillow
numpy
num2words
speed up
# Flash Consideration 2 for CUDA -- considerably sooner on NVIDIA GPUs
# Skip this on Apple Silicon and CPU -- it's CUDA-only
pip set up flash-attn --no-build-isolation
# decord -- required for SmolVLM2's native video enter path (utilized in Part 5)
pip set up decord
Be aware: The
num2wordspackage deal is a non-obvious dependency. SmolVLM2’s processor makes use of it to transform numeric digits to phrase representations (e.g. 3 → “three”) for consistency with pure language coaching patterns, as defined on this walkthrough. Omitting it causes a silent import error when the processor masses.
System test (run this earlier than loading the mannequin):
# device_check.py
# Run: python device_check.py
import torch
def detect_device():
if torch.cuda.is_available():
title = torch.cuda.get_device_name(0)
vram = torch.cuda.get_device_properties(0).total_memory / 1e9
print(f"CUDA: {title} ({vram:.1f} GB VRAM)")
return "cuda", torch.bfloat16, "flash_attention_2"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
print("Apple Silicon MPS detected")
return "mps", torch.float16, "keen"
else:
print("CPU fallback (gradual -- contemplate Colab T4)")
return "cpu", torch.float32, "keen"
if __name__ == "__main__":
machine, dtype, attn = detect_device()
print(f"System: {machine} | dtype: {dtype} | attn: {attn}")
Run it with:
# Constructing the Basis of the Pipeline
Earlier than SmolVLM2 sees something, you want frames. The body extractor converts a video file into a listing of PIL (Python Imaging Library) photographs with timestamps hooked up, one pair per extracted body.
Two modes matter for various use instances. Uniform sampling distributes frames evenly throughout the total video period, guaranteeing protection of every part no matter content material. That is the suitable alternative for conferences and lectures the place you can not afford to overlook a bit. Keyframe sampling extracts frames solely the place the visible content material adjustments considerably, resembling scene cuts, a brand new slide, or a brand new speaker, which reduces the body rely and focuses consideration on distinct moments. That is higher for surveillance and spotlight extraction.
# frame_extractor.py
# Conditions: pip set up opencv-python Pillow numpy
# Utilization: from frame_extractor import FrameExtractor
import cv2
import numpy as np
from PIL import Picture
class FrameExtractor:
"""
Extracts video frames as PIL Pictures for SmolVLM2 inference.
Every extracted body is paired with its timestamp in seconds.
SmolVLM2 makes use of ~81 visible tokens per picture. At 50 frames that's
roughly 4,050 picture tokens -- the sensible higher restrict earlier than VRAM
stress impacts era high quality on shopper GPUs.
"""
MAX_FRAMES = 50
def __init__(self, max_frames: int = MAX_FRAMES):
"""
Args:
max_frames: Laborious cap on extracted frames. Default 50 matches
the SmolVLM2 reference pipeline's examined higher restrict.
"""
self.max_frames = max_frames
def uniform_sample(self, video_path: str) -> listing[tuple[float, Image.Image]]:
"""
Extract evenly spaced frames throughout the total video period.
Finest for: assembly recordings, lectures, tutorials, course content material.
Returns:
Listing of (timestamp_seconds, PIL_Image) in chronological order.
"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
increase IOError(f"Can't open video: {video_path}")
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
n_extract = min(self.max_frames, total_frames)
# Construct body indices unfold evenly from first to final body
indices = np.linspace(0, total_frames - 1, n_extract, dtype=int)
outcomes = []
for idx in indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
ret, body = cap.learn()
if not ret:
proceed
timestamp = spherical(idx / fps, 2)
rgb = cv2.cvtColor(body, cv2.COLOR_BGR2RGB)
outcomes.append((timestamp, Picture.fromarray(rgb)))
cap.launch()
return outcomes
def keyframe_sample(
self, video_path: str, diff_threshold: float = 30.0
) -> listing[tuple[float, Image.Image]]:
"""
Extract frames the place visible content material adjustments considerably.
Finest for: surveillance, occasion detection, spotlight extraction.
Makes use of imply absolute pixel distinction between consecutive grayscale frames
because the change sign. When the diff exceeds diff_threshold, a brand new
keyframe is recorded.
Args:
diff_threshold: Imply pixel distinction to deal with as a scene change.
30.0 works for many industrial content material.
Decrease = extra delicate, larger = fewer frames.
Returns:
Listing of (timestamp_seconds, PIL_Image) in chronological order,
capped at self.max_frames.
"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
increase IOError(f"Can't open video: {video_path}")
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
outcomes = []
prev_gray = None
idx = 0
whereas len(outcomes) < self.max_frames:
ret, body = cap.learn()
if not ret:
break
grey = cv2.cvtColor(body, cv2.COLOR_BGR2GRAY)
if prev_gray is None:
# All the time seize the primary body as a baseline
rgb = cv2.cvtColor(body, cv2.COLOR_BGR2RGB)
outcomes.append((spherical(idx / fps, 2), Picture.fromarray(rgb)))
else:
diff = np.imply(np.abs(grey.astype(float) - prev_gray.astype(float)))
if diff > diff_threshold:
rgb = cv2.cvtColor(body, cv2.COLOR_BGR2RGB)
outcomes.append((spherical(idx / fps, 2), Picture.fromarray(rgb)))
prev_gray = grey
idx += 1
cap.launch()
return outcomes
Begin each new video sort with uniform_sample. When you discover too many redundant frames (5 nearly-identical slides in a row), change to keyframe_sample and tune diff_threshold down from 30 to twenty till the extracted set feels consultant with out being redundant.
# Loading SmolVLM2 and Operating Single-Body Inference
With frames in hand, right here is the whole mannequin loading and first-inference sample. The essential particulars: AutoModelForImageTextToText is the right class (not the generic AutoModelForCausalLM), and on CUDA, it is best to allow Flash Consideration 2, which offers significant latency enhancements on multi-image inputs.
# smolvlm2_loader.py
# Conditions: transformers from v4.49.0-SmolVLM-2 department, torch, flash-attn (CUDA solely)
# Run: python smolvlm2_loader.py your_video.mp4
import sys
import torch
from PIL import Picture
from transformers import AutoProcessor, AutoModelForImageTextToText
MODEL_ID = "HuggingFaceTB/SmolVLM2-2.2B-Instruct"
def load_model():
"""
Load SmolVLM2-2.2B and its processor.
Mechanically selects Flash Consideration 2 on CUDA, keen mode elsewhere.
First run downloads ~4.5 GB of weights to ~/.cache/huggingface/hub.
"""
if torch.cuda.is_available():
dtype = torch.bfloat16
machine = "cuda"
attn = "flash_attention_2"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
dtype = torch.float16
machine = "mps"
attn = "keen"
else:
dtype = torch.float32
machine = "cpu"
attn = "keen"
print(f"Loading {MODEL_ID} on {machine}...")
processor = AutoProcessor.from_pretrained(MODEL_ID)
mannequin = AutoModelForImageTextToText.from_pretrained(
MODEL_ID,
torch_dtype=dtype,
_attn_implementation=attn,
).to(machine)
mannequin.eval()
print(f"Mannequin prepared on {machine}")
return mannequin, processor
def describe_frame(
mannequin,
processor,
body: Picture.Picture,
immediate: str = "Describe what is occurring on this body intimately. Be aware any textual content, folks, objects, or actions seen.",
max_new_tokens: int = 256,
) -> str:
"""
Run SmolVLM2 inference on a single PIL Picture.
The chat template expects picture content material earlier than textual content content material within the
message -- this mirrors the coaching knowledge format and is essential
for dependable output.
Args:
body: A PIL Picture (from FrameExtractor)
immediate: What to ask the mannequin about this body
max_new_tokens: Most response size in tokens
Returns:
Mannequin response as a plain string
"""
messages = [
{
"role": "user",
"content": [
# Image placed before text -- matches SmolVLM2 training format
{"type": "image"},
{"type": "text", "text": prompt},
],
}
]
# apply_chat_template codecs the message and injects visible token placeholders
input_text = processor.apply_chat_template(
messages,
add_generation_prompt=True,
)
inputs = processor(
photographs=[frame],
textual content=input_text,
return_tensors="pt",
).to(mannequin.machine)
with torch.no_grad():
output_ids = mannequin.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False, # Grasping decoding for constant structured output
)
# Decode solely the newly generated tokens -- strip the enter immediate
new_tokens = output_ids[0][inputs["input_ids"].form[-1]:]
return processor.decode(new_tokens, skip_special_tokens=True).strip()
# ── Fast sanity test ────────────────────────────────────────────────────────
if __name__ == "__main__":
from frame_extractor import FrameExtractor
if len(sys.argv) < 2:
print("Utilization: python smolvlm2_loader.py ")
sys.exit(1)
mannequin, processor = load_model()
extractor = FrameExtractor(max_frames=5)
frames = extractor.uniform_sample(sys.argv[1])
ts, first_frame = frames[0]
print(f"nDescribing body at {ts}s...")
description = describe_frame(mannequin, processor, first_frame)
print(f"n{description}")
run:
python smolvlm2_loader.py your_video.mp4
The outline you get again is your sanity test. If the mannequin appropriately identifies seen textual content, folks, objects, and actions within the first body, the pipeline is working. When you get a really brief or clearly fallacious response, confirm that the transformers model is from the v4.49.0-SmolVLM-2 department; the secure Hugging Face launch doesn’t but embrace SmolVLM2 help on the time of writing.
# Constructing the Actual-World Undertaking (Assembly Recording Summarizer)
Right here is the total pipeline. The VideoSummarizer class ties collectively the body extractor, the mannequin, and a two-pass inference technique: the primary go generates per-frame descriptions, and the second go synthesizes these descriptions right into a structured JSON report with a story abstract and extracted motion gadgets.
The 2-pass design is deliberate. Asking the mannequin to explain a single body at a time is a centered, achievable job; it produces correct, concrete descriptions. Asking it to synthesize 30 body descriptions right into a coherent narrative is a special job, and it handles that higher as a separate name with the concatenated descriptions as enter than for those who tried to do each in a single go.
# video_summarizer.py
# Conditions: frame_extractor.py and smolvlm2_loader.py in the identical listing
# Run: python video_summarizer.py meeting_recording.mp4 --output abstract.json
import re
import json
import argparse
from dataclasses import dataclass, discipline
import cv2
import torch
from frame_extractor import FrameExtractor
from smolvlm2_loader import load_model, describe_frame
# ── Knowledge fashions ───────────────────────────────────────────────────────────────
@dataclass
class FrameDescription:
timestamp: float
frame_index: int
description: str
@dataclass
class VideoSummary:
video_path: str
duration_seconds: float
frames_analyzed: int
frame_descriptions: listing[FrameDescription]
narrative_summary: str
action_items: listing[str] = discipline(default_factory=listing)
key_moments: listing[dict] = discipline(default_factory=listing)
# ── Per-frame immediate ──────────────────────────────────────────────────────────
FRAME_PROMPT = """You're analyzing a body from a recorded assembly.
Describe what you see concisely however fully:
- Who or what's seen (folks, whiteboards, screens, slides)
- Any readable textual content (slide titles, whiteboard content material, display content material)
- The obvious exercise (presenting, discussing, writing, listening)
Preserve your response to 2-3 sentences."""
# ── Synthesis immediate ──────────────────────────────────────────────────────────
def build_synthesis_prompt(descriptions: listing[FrameDescription], period: float) -> str:
"""Construct the second-pass immediate that synthesizes body descriptions right into a report."""
frames_text = "n".be part of(
f"[{int(d.timestamp // 60):02d}:{int(d.timestamp % 60):02d}] {d.description}"
for d in descriptions
)
return f"""Under are time-stamped descriptions of frames from a {period:.0f}-second assembly recording.
{frames_text}
Primarily based on these descriptions, present:
1. NARRATIVE SUMMARY: A 3-5 sentence abstract of what the assembly lined, who participated (if seen), and what selections or conclusions had been reached.
2. ACTION ITEMS: A bullet listing of concrete duties or follow-ups talked about or implied within the assembly. Begin every with a touch (-).
3. KEY MOMENTS: A bullet listing of the 3-5 most vital moments with their timestamps in [MM:SS] format.
Format your response with clear headings for every part."""
# ── Output parser ─────────────────────────────────────────────────────────────
def parse_action_items(textual content: str) -> listing[str]:
"""Extract bullet-point motion gadgets from the synthesis output."""
gadgets = []
for line in textual content.break up("n"):
stripped = line.strip()
if re.match(r"^[-*•]s+", stripped) or re.match(r"^d+.s+", stripped):
clear = re.sub(r"^[-*•d.]+s*", "", stripped).strip()
if clear and len(clear) > 5:
gadgets.append(clear)
return gadgets
def parse_key_moments(textual content: str) -> listing[dict]:
"""Extract key moments with timestamps from the synthesis output."""
moments = []
sample = re.compile(r"[(d{2}:d{2})]s*(.+)")
for match in sample.finditer(textual content):
moments.append({
"timestamp_label": match.group(1),
"description": match.group(2).strip()
})
return moments
# ── Fundamental summarizer class ─────────────────────────────────────────────────────
class VideoSummarizer:
"""
Finish-to-end native video summarizer utilizing SmolVLM2-2.2B.
Two-pass technique: per-frame descriptions + synthesis narrative.
Works on scanned, digital, and live-recorded movies alike.
"""
def __init__(self, batch_size: int = 8):
"""
Args:
batch_size: Frames to explain per inference batch.
Tune based mostly on VRAM: 8 for 8 GB, 16 for 16 GB.
Every body makes use of ~81 visible tokens; decrease batch = much less peak VRAM.
"""
self.mannequin, self.processor = load_model()
self.extractor = FrameExtractor(max_frames=50)
self.batch_size = batch_size
def _get_duration(self, video_path: str) -> float:
cap = cv2.VideoCapture(video_path)
frames = cap.get(cv2.CAP_PROP_FRAME_COUNT)
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
cap.launch()
return spherical(frames / fps, 2)
def summarize(self, video_path: str, mode: str = "uniform") -> VideoSummary:
"""
Summarize a video file.
Args:
video_path: Path to the video file (mp4, avi, mov, mkv)
mode: "uniform" for even protection, "keyframe" for scene adjustments
Returns:
VideoSummary with per-frame descriptions, narrative, and motion gadgets
"""
period = self._get_duration(video_path)
print(f"Video: {video_path} ({period:.0f}s)")
# ── Cross 1: Extract frames ─────────────────────────────────────────
if mode == "keyframe":
frames = self.extractor.keyframe_sample(video_path)
else:
frames = self.extractor.uniform_sample(video_path)
print(f"Extracted {len(frames)} frames -- describing in batches of {self.batch_size}...")
# ── Cross 2: Describe every body ────────────────────────────────────
descriptions: listing[FrameDescription] = []
for batch_start in vary(0, len(frames), self.batch_size):
batch = frames[batch_start : batch_start + self.batch_size]
for local_idx, (timestamp, img) in enumerate(batch):
global_idx = batch_start + local_idx
print(f" [{global_idx + 1}/{len(frames)}] Describing body at {timestamp}s...")
desc = describe_frame(
self.mannequin,
self.processor,
img,
immediate=FRAME_PROMPT,
max_new_tokens=128, # Preserve body descriptions concise
)
descriptions.append(FrameDescription(
timestamp=timestamp,
frame_index=global_idx,
description=desc,
))
# ── Cross 3: Synthesis ──────────────────────────────────────────────
print("nRunning synthesis go...")
synthesis_prompt = build_synthesis_prompt(descriptions, period)
synthesis_messages = [
{
"role": "user",
"content": [{"type": "text", "text": synthesis_prompt}],
}
]
synthesis_text_input = self.processor.apply_chat_template(
synthesis_messages,
add_generation_prompt=True,
)
# Synthesis is text-only -- no photographs on this go
synthesis_inputs = self.processor(
textual content=synthesis_text_input,
return_tensors="pt",
).to(self.mannequin.machine)
with torch.no_grad():
synthesis_ids = self.mannequin.generate(
**synthesis_inputs,
max_new_tokens=512,
do_sample=False,
)
synthesis_new = synthesis_ids[0][synthesis_inputs["input_ids"].form[-1]:]
synthesis_output = self.processor.decode(synthesis_new, skip_special_tokens=True).strip()
action_items = parse_action_items(synthesis_output)
key_moments = parse_key_moments(synthesis_output)
return VideoSummary(
video_path=video_path,
duration_seconds=period,
frames_analyzed=len(descriptions),
frame_descriptions=descriptions,
narrative_summary=synthesis_output,
action_items=action_items,
key_moments=key_moments,
)
def to_json(self, abstract: VideoSummary) -> str:
"""Serialize a VideoSummary to formatted JSON."""
return json.dumps({
"video": abstract.video_path,
"duration_seconds": abstract.duration_seconds,
"frames_analyzed": abstract.frames_analyzed,
"narrative": abstract.narrative_summary,
"action_items": abstract.action_items,
"key_moments": abstract.key_moments,
"frame_descriptions": [
{
"timestamp": d.timestamp,
"timestamp_label": f"{int(d.timestamp // 60):02d}:{int(d.timestamp % 60):02d}",
"description": d.description,
}
for d in summary.frame_descriptions
],
}, indent=2, ensure_ascii=False)
# ── Entry level ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Summarize a video with SmolVLM2-2.2B")
parser.add_argument("video", assist="Path to the enter video file")
parser.add_argument("--output", default="abstract.json", assist="Output JSON file path")
parser.add_argument("--mode", default="uniform", decisions=["uniform", "keyframe"])
parser.add_argument("--batch-size", sort=int, default=8)
args = parser.parse_args()
summarizer = VideoSummarizer(batch_size=args.batch_size)
outcome = summarizer.summarize(args.video, mode=args.mode)
output_str = summarizer.to_json(outcome)
with open(args.output, "w", encoding="utf-8") as f:
f.write(output_str)
print(f"nSummary saved to {args.output}")
print(f"Frames analyzed: {outcome.frames_analyzed}")
print(f"Motion gadgets discovered: {len(outcome.action_items)}")
for merchandise in outcome.action_items:
print(f" - {merchandise}")
run:
# Uniform sampling (default) -- greatest for conferences and lectures
python video_summarizer.py meeting_2026_06_14.mp4 --output meeting_summary.json
# Keyframe sampling -- greatest for occasion detection, surveillance
python video_summarizer.py security_footage.mp4 --mode keyframe --output occasions.json
# Modify batch dimension to your VRAM (8 for 8 GB VRAM, 16 for 16 GB)
python video_summarizer.py long_lecture.mp4 --batch-size 4 --output lecture.json
Pattern output (abstract.json):
{
"video": "meeting_2026_06_14.mp4",
"duration_seconds": 3247.0,
"frames_analyzed": 50,
"narrative": "The assembly centered on Q3 product planning ...",
"action_items": [
"Finalize API design document by end of June",
"Schedule testing sprint kickoff for July 1",
"Share updated Gantt chart with stakeholders"
],
"key_moments": [
{"timestamp_label": "00:00", "description": "Team introductions and agenda overview"},
{"timestamp_label": "12:30", "description": "API architecture diagram reviewed on screen"},
{"timestamp_label": "41:15", "description": "Action items summarized on whiteboard"}
]
}
# Batching Frames with VRAM Consciousness
The batch dimension in VideoSummarizer is the first knob for staying inside your VRAM funds. Too massive and also you hit out-of-memory errors. Too small and also you decelerate unnecessarily. Right here is the calculation:
SmolVLM2-2.2B weights occupy roughly 4.5 GB in bfloat16. Every body contributes roughly 81 picture tokens to the inference name, and at 2.2B scale, KV cache overhead is roughly 0.5 MB per token. Leaving 20% VRAM as headroom:
# vram_calculator.py
# Estimate a secure batch dimension to your GPU earlier than working the pipeline
def compute_batch_size(vram_gb: float, tokens_per_frame: int = 81) -> int:
"""
Estimate frames per inference batch for a given VRAM funds.
Args:
vram_gb: Obtainable GPU VRAM in gigabytes
tokens_per_frame: Visible tokens per picture (81 for SmolVLM2)
Returns:
Secure batch dimension, minimal 1, most 50
"""
MODEL_GB = 4.5 # SmolVLM2-2.2B weights in bfloat16
HEADROOM = 0.80 # Use at most 80% of complete VRAM
MB_PER_TOKEN = 0.5 / 1024 # GB per KV token at 2.2B scale (tough)
usable_gb = vram_gb * HEADROOM
inference_budget = max(0.0, usable_gb - MODEL_GB)
frames = int(inference_budget / (tokens_per_frame * MB_PER_TOKEN))
return max(1, min(frames, 50))
if __name__ == "__main__":
for vram in [6.0, 8.0, 12.0, 16.0, 24.0]:
print(f" {vram:.0f} GB VRAM → batch_size = {compute_batch_size(vram)}")
Operating this in opposition to a couple of widespread VRAM tiers offers a way of the ceiling:
6 GB VRAM → batch_size = 16
8 GB VRAM → batch_size = 30
12 GB VRAM → batch_size = 50
16 GB VRAM → batch_size = 50
24 GB VRAM → batch_size = 50
For lengthy movies the place you can not afford to restart from zero if one thing fails, add a JSON Strains (JSONL) streaming author that persists every body’s description as it’s generated:
# jsonl_writer.py -- drop-in checkpoint help for long-video processing
import json
class JSONLWriter:
"""
Writes body descriptions to a JSONL file as they're produced.
Permits resume-from-checkpoint on lengthy movies -- if inference fails at
body 30 of fifty, re-read the JSONL and skip already-processed frames.
"""
def __init__(self, path: str):
self.path = path
self._fh = open(path, "a", encoding="utf-8") # Append mode for resume
def write(self, file: dict):
"""Write one body file and flush instantly to disk."""
self._fh.write(json.dumps(file, ensure_ascii=False) + "n")
self._fh.flush()
def already_processed(self) -> set[int]:
"""Return the set of body indices already within the checkpoint file."""
processed = set()
attempt:
with open(self.path, "r", encoding="utf-8") as f:
for line in f:
file = json.masses(line)
processed.add(file.get("frame_index", -1))
besides FileNotFoundError:
go
return processed
def shut(self):
self._fh.shut()
def __enter__(self):
return self
def __exit__(self, *args):
self.shut()
# Extending the Pipeline (Timestamps and JSONL Streaming)
The output JSON from this pipeline is already timestamped on the body degree. Making it extra searchable requires one addition: a clear MM:SS label on each body description that maps on to the video participant’s scrubber.
Add this post-processing step to to_json() if you’d like the output to be immediately usable in a video assessment interface:
def timestamp_label(seconds: float) -> str:
"""Convert decimal seconds to MM:SS or HH:MM:SS label."""
complete = int(seconds)
h, the rest = divmod(complete, 3600)
m, s = divmod(the rest, 60)
if h > 0:
return f"{h:02d}:{m:02d}:{s:02d}"
return f"{m:02d}:{s:02d}"
For long-form video output that you just need to stream right into a downstream system (a database, a Slack notification, a doc indexer), substitute the batch buffer strategy with a JSONL output the place every line is one body’s file. Which means the primary body’s description is on the market 30 seconds right into a 90-minute video’s processing, somewhat than ready for the total pipeline to finish earlier than writing something.
Pair the JSONL author with JSONLWriter.already_processed() to implement resume-from-checkpoint: if the pipeline crashes at body 35 of fifty, restart it and it’ll learn the prevailing checkpoint, skip the primary 35 frames, and proceed from body 36. On lengthy movies, this protects important time over ranging from zero.
# Conclusion
SmolVLM2-2.2B sits at a genuinely helpful level on the capability-size trade-off curve. Sufficiently small to run on a single shopper GPU, succesful sufficient to supply video summaries which might be really helpful for actual workflows. The frame-as-image strategy retains the implementation clear: no unique video encoders, no customized consideration implementations, simply the usual transformers API with PIL photographs as enter.
The assembly summarizer on this article is the template. Substitute FRAME_PROMPT with a immediate tuned to your area, change build_synthesis_prompt() to extract no matter structured fields matter to your use case, and the identical pipeline works for lecture recordings, safety footage, product demo walkthroughs, or sports activities highlights. The 2-pass sample, per-frame description first and synthesis second, holds throughout all of these as a result of the mannequin describes particular person frames precisely and synthesizes throughout descriptions reliably.
The 50-frame restrict is a place to begin, not a ceiling. On higher-VRAM {hardware}, improve max_frames to 75 or 100 and experiment. High quality scales with body protection up to a degree, and your synthesis go advantages from extra materials to work with.
Shittu Olumide is a software program engineer and technical author enthusiastic about leveraging cutting-edge applied sciences to craft compelling narratives, with a eager eye for element and a knack for simplifying complicated ideas. You may as well discover Shittu on Twitter.
