Saturday, July 11, 2026

Native Video Summarization Pipeline: Processing Frames with SmolVLM2-2.2B


 

Introduction

 
Most video understanding instruments fall into one among 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 sort of GPU cluster most builders shouldn’t 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 sequence, 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 truly maintain up, is what this text is constructed round.

The challenge we’ll construct on this article: a neighborhood 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 objects, and a remaining 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 resolution about how pictures are tokenized.

Most vision-language fashions tokenize pictures at excessive density. Qwen2-VL, for instance, makes use of as much as 16,000 tokens to symbolize 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 price range. SmolVLM2 makes use of a pixel shuffle technique that compresses every 384×384 picture patch to 81 tokens. Fifty frames grow 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 declare however as a direct consequence of the token price range distinction.

The mannequin is available in three sizes. The 256M and 500M variants are designed for cell and edge gadgets; the 256M can run on a cellphone. The two.2B is the suitable alternative for this pipeline. It’s the solely dimension with sturdy sufficient video benchmark scores to provide dependable multi-scene summaries: Video-MME of 52.1, MLVU of 55.2, and MVBench of 46.27, towards the 500M’s 42.2, 47.3, and 39.73, respectively.

The video understanding method can be price understanding earlier than you write any code. SmolVLM2 doesn’t have a local video encoder; it treats video as a sequence of pictures. 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 method scored 27.14% on CinePile, positioning it between InternVL2 (2B) and Video-LLaVA (7B) on cinematic video understanding, a powerful end result given the mannequin’s dimension and that video was not the one factor it was educated for.

 

Setting Up the Surroundings

 
{Hardware} necessities:

 

Characteristic Minimal Advisable
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 steady 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

 

Notice: The num2words bundle 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.

 

Machine verify (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 (sluggish -- contemplate Colab T4)")
        return "cpu", torch.float32, "keen"

if __name__ == "__main__":
    system, dtype, attn = detect_device()
    print(f"Machine: {system} | 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) pictures with timestamps connected, one pair per extracted body.

Two modes matter for various use circumstances. Uniform sampling distributes frames evenly throughout the complete video period, guaranteeing protection of every thing no matter content material. That is the suitable alternative for conferences and lectures the place you can not afford to overlook a piece. Keyframe sampling extracts frames solely the place the visible content material adjustments considerably, akin to scene cuts, a brand new slide, or a brand new speaker, which reduces the body depend 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 Photographs 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: Exhausting 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) -> record[tuple[float, Image.Image]]:
        """
        Extract evenly spaced frames throughout the complete video period.
        Greatest for: assembly recordings, lectures, tutorials, course content material.

        Returns:
            Checklist 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
    ) -> record[tuple[float, Image.Image]]:
        """
        Extract frames the place visible content material adjustments considerably.
        Greatest 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 business content material.
                            Decrease = extra delicate, larger = fewer frames.

        Returns:
            Checklist 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 kind with uniform_sample. If you happen to 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 necessary particulars: AutoModelForImageTextToText is the right class (not the generic AutoModelForCausalLM), and on CUDA, you must allow Flash Consideration 2, which gives 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.
    Routinely 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
        system = "cuda"
        attn   = "flash_attention_2"
    elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
        dtype  = torch.float16
        system = "mps"
        attn   = "keen"
    else:
        dtype  = torch.float32
        system = "cpu"
        attn   = "keen"

    print(f"Loading {MODEL_ID} on {system}...")

    processor = AutoProcessor.from_pretrained(MODEL_ID)

    mannequin = AutoModelForImageTextToText.from_pretrained(
        MODEL_ID,
        torch_dtype=dtype,
        _attn_implementation=attn,
    ).to(system)

    mannequin.eval()
    print(f"Mannequin prepared on {system}")
    return mannequin, processor


def describe_frame(
    mannequin,
    processor,
    body: Picture.Picture,
    immediate: str = "Describe what is going on on this body intimately. Notice 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 information format and is necessary
    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(
        pictures=[frame],
        textual content=input_text,
        return_tensors="pt",
    ).to(mannequin.system)

    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 verify ────────────────────────────────────────────────────────

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}")

 

The right way to run:

python smolvlm2_loader.py your_video.mp4

 

The outline you get again is your sanity verify. If the mannequin appropriately identifies seen textual content, folks, objects, and actions within the first body, the pipeline is working. If you happen to get a really quick or clearly fallacious response, confirm that the transformers model is from the v4.49.0-SmolVLM-2 department; the steady Hugging Face launch doesn’t but embody SmolVLM2 help on the time of writing.

 

Constructing the Actual-World Venture (Assembly Recording Summarizer)

 
Right here is the complete pipeline. The VideoSummarizer class ties collectively the body extractor, the mannequin, and a two-pass inference technique: the primary cross generates per-frame descriptions, and the second cross synthesizes these descriptions right into a structured JSON report with a story abstract and extracted motion objects.

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 should you tried to do each in a single cross.

# 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, subject
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: record[FrameDescription]
    narrative_summary: str
    action_items: record[str] = subject(default_factory=record)
    key_moments: record[dict] = subject(default_factory=record)


# ── Per-frame immediate ──────────────────────────────────────────────────────────

FRAME_PROMPT = """You're analyzing a body from a recorded assembly.

Describe what you see concisely however utterly:
- Who or what's seen (folks, whiteboards, screens, slides)
- Any readable textual content (slide titles, whiteboard content material, display screen content material)
- The obvious exercise (presenting, discussing, writing, listening)

Preserve your response to 2-3 sentences."""

# ── Synthesis immediate ──────────────────────────────────────────────────────────

def build_synthesis_prompt(descriptions: record[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"""Beneath 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 choices or conclusions had been reached.

2. ACTION ITEMS: A bullet record of concrete duties or follow-ups talked about or implied within the assembly. Begin every with a touch (-).

3. KEY MOMENTS: A bullet record 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) -> record[str]:
    """Extract bullet-point motion objects from the synthesis output."""
    objects = []
    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:
                objects.append(clear)
    return objects

def parse_key_moments(textual content: str) -> record[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


# ── Foremost 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 objects
        """
        period = self._get_duration(video_path)
        print(f"Video: {video_path} ({period:.0f}s)")

        # ── Go 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}...")

        # ── Go 2: Describe every body ────────────────────────────────────
        descriptions: record[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,
                ))

        # ── Go 3: Synthesis ──────────────────────────────────────────────
        print("nRunning synthesis cross...")

        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 pictures on this cross
        synthesis_inputs = self.processor(
            textual content=synthesis_text_input,
            return_tensors="pt",
        ).to(self.mannequin.system)

        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", selections=["uniform", "keyframe"])
    parser.add_argument("--batch-size", kind=int, default=8)
    args = parser.parse_args()

    summarizer = VideoSummarizer(batch_size=args.batch_size)
    end result = summarizer.summarize(args.video, mode=args.mode)

    output_str = summarizer.to_json(end result)
    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: {end result.frames_analyzed}")
    print(f"Motion objects discovered: {len(end result.action_items)}")
    for merchandise in end result.action_items:
        print(f"  - {merchandise}")

 

The right way to 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

# Alter batch dimension on 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 price range. 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 on your GPU earlier than operating the pipeline

def compute_batch_size(vram_gb: float, tokens_per_frame: int = 81) -> int:
    """
    Estimate frames per inference batch for a given VRAM price range.

    Args:
        vram_gb:          Out there 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 whole 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 towards just a few frequent 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 Traces (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, document: dict):
        """Write one body document and flush instantly to disk."""
        self._fh.write(json.dumps(document, 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()
        strive:
            with open(self.path, "r", encoding="utf-8") as f:
                for line in f:
                    document = json.masses(line)
                    processed.add(document.get("frame_index", -1))
        besides FileNotFoundError:
            cross
        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 would like the output to be straight usable in a video evaluate interface:

def timestamp_label(seconds: float) -> str:
    """Convert decimal seconds to MM:SS or HH:MM:SS label."""
    whole = int(seconds)
    h, the rest = divmod(whole, 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 simply need to stream right into a downstream system (a database, a Slack notification, a doc indexer), substitute the batch buffer method with a JSONL output the place every line is one body’s document. Which means the primary body’s description is on the market 30 seconds right into a 90-minute video’s processing, moderately than ready for the complete 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 provide video summaries which might be really helpful for actual workflows. The frame-as-image method retains the implementation clear: no unique video encoders, no customized consideration implementations, simply the usual transformers API with PIL pictures as enter.

The assembly summarizer on this article is the template. Change FRAME_PROMPT with a immediate tuned to your area, change build_synthesis_prompt() to extract no matter structured fields matter on 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 cross 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 advanced ideas. You may also discover Shittu on Twitter.



Related Articles

Latest Articles