# Einführung
Die meisten Instruments zum Verstehen von Movies fallen in eines von zwei Lagern. Das erste Camp erfordert eine Cloud-API; Ihr Filmmaterial wird hochgeladen, auf den Servern einer anderen Particular person verarbeitet und professional Videominute abgerechnet. Das zweite Lager läuft lokal, erfordert aber die Artwork von GPU-Cluster, über die die meisten Entwickler nicht verfügen: 70B+-Modelle, die mehrere A100 benötigen und Minuten professional Clip benötigen. Keine der beiden Optionen funktioniert für einen Entwickler, der Besprechungsaufzeichnungen, eine Vortragsreihe oder Sicherheitsaufnahmen eines Tages auf einer Workstation verarbeiten möchte, die er bereits besitzt.
SmolVLM2-2.2B-Anleitungveröffentlicht von Umarmendes Gesicht am 20. Februar 2025 ändert die Berechnung. Es läuft auf 5,2 GB GPU-RAM, einer RTX 3060, einem MacBook Professional M2 und dem kostenlosen Google Colab T4-Stufe. An Video-MMEdem Commonplace-Benchmark für das Videoverständnis im Langformat, übertrifft es jedes vorhandene Modell im 2B-Maßstab. Diese Kombination aus Client-{Hardware} gepaart mit Ergebnissen, die tatsächlich Bestand haben, ist das, worum es in diesem Artikel geht.
Das Projekt, das wir in diesem Artikel erstellen werden: eine lokale Pipeline, die jede Videodatei aufnimmt, Frames in konfigurierbaren Intervallen extrahiert, sie stapelweise mit SmolVLM2-2.2B analysiert und eine strukturierte JSON-Zusammenfassung ausgibt, einschließlich Szenenbeschreibungen professional Body, Schlüsselmomenten mit Zeitstempeln, Aktionselementen und einer abschließenden Erzählung. Die gleiche Pipeline verarbeitet Besprechungsaufzeichnungen, Vorträge und Überwachungsaufnahmen, ohne eine Codezeile zu ändern.
# SmolVLM2-2.2B
Der Grund, warum SmolVLM2-2.2B auf einer RTX 3060 laufen kann und dabei größere Modelle bei Videoaufgaben übertrifft, ist eine Designentscheidung darüber, wie Bilder tokenisiert werden.
Die meisten Imaginative and prescient-Language-Modelle tokenisieren Bilder mit hoher Dichte. Qwen2-VLverwendet beispielsweise bis zu 16.000 Token, um ein einzelnes Bild darzustellen. Die Einspeisung von 50 Frames in ein solches Modell mit dieser Dichte würde 800.000 Token verbrauchen, was weit über das Kontextbudget einer Client-GPU hinausgeht. SmolVLM2 verwendet a Pixel-Shuffle-Strategie Dadurch wird jeder 384×384-Bild-Patch auf 81 Token komprimiert. Aus fünfzig Bildern werden ungefähr 4.050 Bildtokens, die in einem einzigen Inferenzaufruf verwaltet werden können. Diese Komprimierung ist der Grund Der Vorfülldurchsatz von SmolVLM2 ist 3,3- bis 4,5-mal schneller und der Generierungsdurchsatz 7,5- bis 16-mal schneller als bei Qwen2-VL-2Bnicht als Marketingaussage, sondern als direkte Folge der symbolischen Budgetdifferenz.
Das Modell ist in drei Größen erhältlich. Die Varianten 256M und 500M sind für Mobil- und Edge-Geräte konzipiert; Der 256M kann auf einem Telefon ausgeführt werden. Für diese Pipeline ist der 2.2B die richtige Wahl. Es ist die einzige Größe mit ausreichend starken Video-Benchmark-Ergebnissen, um zuverlässige Multiszenen-Zusammenfassungen zu erstellen: Video-MME von 52,1, MLVU von 55,2 und MVBench von 46,27 im Vergleich zu 42,2, 47,3 bzw. 39,73 des 500M.
Es lohnt sich auch, den Videoverständnisansatz zu verstehen, bevor Sie Code schreiben. SmolVLM2 verfügt nicht über einen nativen Video-Encoder; Es behandelt Movies als eine Folge von Bildern. Der offizielle Referenzpipeline Extrahiert bis zu 50 gleichmäßig abgetastete Bilder professional Video, umgeht die interne Größenänderung der Bilder und übergibt sie als Mehrbildsequenz in einer einzelnen Chat-Nachricht. Dieser Ansatz erreichte 27,14 % CinePileund positionierte es zwischen InternVL2 (2B) und Video-LLaVA (7B) hinsichtlich des filmischen Videoverständnisses. Angesichts der Größe des Modells battle dies ein starkes Ergebnis, und Video battle nicht das Einzige, wofür es trainiert wurde.
# Einrichten der Umgebung
Hardwareanforderungen:
| Besonderheit | Minimal | Empfohlen |
|---|---|---|
| GPU-VRAM | 6 GB (RTX 3060) | 12–16 GB (RTX 4080) |
| Apple Silicon | M2 8 GB (MPS-Pfad) | M2 Professional / M3 16 GB |
| System-RAM | 16 GB | 32 GB |
| Scheibe | 10 GB kostenlos | 20 GB+ SSD |
| Colab | T4 (kostenloses Kontingent) | A100 (Colab Professional) |
Python-Pakete:
# 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 assist
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 quicker 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
Notiz: Der
num2wordsPaket ist eine nicht offensichtliche Abhängigkeit. Der Prozessor von SmolVLM2 verwendet es, um numerische Ziffern in Wortdarstellungen (z. B. 3 → „drei“) umzuwandeln, um die Konsistenz mit Trainingsmustern natürlicher Sprache zu gewährleisten, z wird in dieser Komplettlösung erklärt. Wenn Sie es weglassen, kommt es beim Laden des Prozessors zu einem Fehler beim stillen Import.
Geräteprüfung (führen Sie diese vor dem Laden des Modells durch):
# 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 -- take into account Colab T4)")
return "cpu", torch.float32, "keen"
if __name__ == "__main__":
system, dtype, attn = detect_device()
print(f"Machine: {system} | dtype: {dtype} | attn: {attn}")
Führen Sie es aus mit:
# Aufbau des Fundaments der Pipeline
Bevor SmolVLM2 etwas sieht, benötigen Sie Frames. Der Body-Extraktor konvertiert eine Videodatei in eine Liste von PIL (Python Imaging Library) Bilder mit angehängten Zeitstempeln, ein Paar professional extrahiertem Body.
Für unterschiedliche Anwendungsfälle sind zwei Modi von Bedeutung. Durch das einheitliche Sampling werden die Bilder gleichmäßig über die gesamte Videodauer verteilt, sodass unabhängig vom Inhalt alles abgedeckt wird. Dies ist die richtige Wahl für Besprechungen und Vorträge, bei denen Sie es sich nicht leisten können, einen Abschnitt zu verpassen. Beim Keyframe-Sampling werden nur Bilder extrahiert, bei denen sich der visuelle Inhalt erheblich ändert, z. B. bei Szenenschnitten, einer neuen Folie oder einem neuen Sprecher. Dadurch wird die Anzahl der Bilder reduziert und die Aufmerksamkeit auf bestimmte Momente gelenkt. Dies ist besser für die Überwachung und Spotlight-Extraktion.
# 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
strain impacts era high quality on client 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) -> listing(tuple(float, Picture.Picture)):
"""
Extract evenly spaced frames throughout the complete 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():
elevate 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, Picture.Picture)):
"""
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, greater = 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():
elevate 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
Beginnen Sie jeden neuen Videotyp mit uniform_sample. Wenn Sie zu viele redundante Bilder finden (fünf nahezu identische Folien hintereinander), wechseln Sie zu keyframe_sample und stimmen diff_threshold von 30 auf 20 herunter, bis sich die extrahierte Menge repräsentativ anfühlt, ohne überflüssig zu sein.
# Laden von SmolVLM2 und Ausführen von Single-Body-Inferenz
Mit den vorliegenden Frames finden Sie hier das vollständige Modelllade- und Erstinferenzmuster. Die wichtigen Particulars: AutoModelForImageTextToText ist die richtige Klasse (nicht die generische). AutoModelForCausalLM), und auf CUDA sollten Sie es aktivieren Blitzaufmerksamkeit 2was bedeutende Latenzverbesserungen bei Eingaben mit mehreren Bildern bietet.
# 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.
Robotically 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. 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 = (
{
"position": "person",
"content material": (
# Picture positioned earlier than textual content -- matches SmolVLM2 coaching format
{"kind": "picture"},
{"kind": "textual content", "textual content": immediate},
),
}
)
# apply_chat_template codecs the message and injects visible token placeholders
input_text = processor.apply_chat_template(
messages,
add_generation_prompt=True,
)
inputs = processor(
photos=(body),
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 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}")
So führen Sie aus:
python smolvlm2_loader.py your_video.mp4
Die Beschreibung, die Sie zurückerhalten, ist Ihr Gesundheitscheck. Wenn das Modell sichtbaren Textual content, Personen, Objekte und Aktionen im ersten Body korrekt identifiziert, funktioniert die Pipeline. Wenn Sie eine sehr kurze oder offensichtlich falsche Antwort erhalten, überprüfen Sie, ob die transformers Model ist von der v4.49.0-SmolVLM-2 Zweig; Die stabile Hugging Face-Model beinhaltet zum Zeitpunkt des Schreibens noch keine SmolVLM2-Unterstützung.
# Aufbau des Actual-World-Projekts (Assembly Recording Summarizer)
Hier ist die vollständige Pipeline. Der VideoSummarizer Die Klasse verknüpft den Body-Extraktor, das Modell und eine Inferenzstrategie mit zwei Durchgängen: Der erste Durchgang generiert Beschreibungen professional Body und der zweite Durchgang synthetisiert diese Beschreibungen in einem strukturierten JSON-Bericht mit einer narrativen Zusammenfassung und extrahierten Aktionselementen.
Das Zwei-Move-Design ist bewusst. Das Modell zu bitten, jeweils nur einen einzelnen Body zu beschreiben, ist eine gezielte, erreichbare Aufgabe. es liefert genaue, konkrete Beschreibungen. Die Aufgabe, 30 Body-Beschreibungen zu einer kohärenten Erzählung zusammenzufassen, ist eine andere Aufgabe, und das kann besser als separater Aufruf mit den verketteten Beschreibungen als Eingabe erledigt werden, als wenn man versucht hätte, beides in einem Durchgang zu erledigen.
# 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}
Based mostly on these descriptions, present:
1. NARRATIVE SUMMARY: A 3-5 sentence abstract of what the assembly coated, 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 important 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 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) -> 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
# ── Major 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)")
# ── Move 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}...")
# ── Move 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,
))
# ── Move 3: Synthesis ──────────────────────────────────────────────
print("nRunning synthesis cross...")
synthesis_prompt = build_synthesis_prompt(descriptions, period)
synthesis_messages = (
{
"position": "person",
"content material": ({"kind": "textual content", "textual content": synthesis_prompt}),
}
)
synthesis_text_input = self.processor.apply_chat_template(
synthesis_messages,
add_generation_prompt=True,
)
# Synthesis is text-only -- no photos 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 abstract.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}")
So führen Sie aus:
# Uniform sampling (default) -- finest for conferences and lectures
python video_summarizer.py meeting_2026_06_14.mp4 --output meeting_summary.json
# Keyframe sampling -- finest 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
Beispielausgabe (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 doc by finish of June",
"Schedule testing dash kickoff for July 1",
"Share up to date Gantt chart with stakeholders"
),
"key_moments": (
{"timestamp_label": "00:00", "description": "Workforce introductions and agenda overview"},
{"timestamp_label": "12:30", "description": "API structure diagram reviewed on display"},
{"timestamp_label": "41:15", "description": "Motion objects summarized on whiteboard"}
)
}
# Batch-Frames mit VRAM-Bewusstsein
Die Chargengröße in VideoSummarizer ist der wichtigste Knopf, um innerhalb Ihres VRAM-Budgets zu bleiben. Wenn es zu groß ist, treten Fehler aufgrund von nicht genügend Arbeitsspeicher auf. Zu klein und man wird unnötig langsamer. Hier ist die Berechnung:
SmolVLM2-2.2B-Gewichte belegen in bfloat16 etwa 4,5 GB. Jeder Body trägt etwa 81 Bild-Tokens zum Inferenzaufruf bei, und im 2,2-B-Maßstab beträgt der KV-Cache-Overhead etwa 0,5 MB professional Token. 20 % VRAM als Spielraum belassen:
# 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 price range.
Args:
vram_gb: Accessible GPU VRAM in gigabytes
tokens_per_frame: Visible tokens per picture (81 for SmolVLM2)
Returns:
Protected 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)}")
Vergleicht man dies mit einigen gängigen VRAM-Stufen, bekommt man einen Eindruck von der Obergrenze:
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
Für lange Movies, bei denen Sie es sich nicht leisten können, bei einem Fehler von Null neu zu starten, fügen Sie einen JSON Traces (JSONL)-Streaming-Author hinzu, der die Beschreibung jedes Frames bei der Generierung beibehält:
# jsonl_writer.py -- drop-in checkpoint assist for long-video processing
import json
class JSONLWriter:
"""
Writes body descriptions to a JSONL file as they're produced.
Allows 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()
attempt:
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()
# Erweiterung der Pipeline (Zeitstempel und JSONL-Streaming)
Der Ausgabe-JSON dieser Pipeline ist bereits auf Body-Ebene mit einem Zeitstempel versehen. Um die Durchsuchbarkeit zu verbessern, ist ein Zusatz erforderlich: eine Bereinigung MM:SS Beschriftung auf jeder Bildbeschreibung, die direkt dem Scrubber des Videoplayers zugeordnet ist.
Fügen Sie diesen Nachbearbeitungsschritt hinzu to_json() Wenn Sie möchten, dass die Ausgabe direkt in einer Videoüberprüfungsoberfläche verwendet werden kann:
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}"
Für lange Videoausgaben, die Sie in ein Downstream-System (eine Datenbank, eine Slack-Benachrichtigung, einen Dokumentindexer) streamen möchten, ersetzen Sie den Batch-Puffer-Ansatz durch eine JSONL-Ausgabe, bei der jede Zeile der Datensatz eines Frames ist. Das bedeutet, dass die Beschreibung des ersten Frames 30 Sekunden nach Beginn der Verarbeitung eines 90-minütigen Movies verfügbar ist, anstatt darauf zu warten, dass die gesamte Pipeline abgeschlossen ist, bevor etwas geschrieben wird.
Koppeln Sie den JSONL-Author mit JSONLWriter.already_processed() So implementieren Sie „Resume-from-Checkpoint“: Wenn die Pipeline bei Bild 35 von 50 abstürzt, starten Sie sie neu. Sie liest dann den vorhandenen Prüfpunkt, überspringt die ersten 35 Bilder und fährt mit Bild 36 fort. Bei langen Movies spart dies erheblich Zeit gegenüber dem Begin bei Null.
# Abschluss
SmolVLM2-2.2B befindet sich an einem wirklich nützlichen Punkt auf der Kompromisskurve zwischen Kapazität und Größe. Klein genug, um auf einer einzelnen Verbraucher-GPU ausgeführt zu werden, und leistungsfähig genug, um Videozusammenfassungen zu erstellen, die für echte Arbeitsabläufe tatsächlich nützlich sind. Der Body-as-Picture-Ansatz hält die Implementierung sauber: keine exotischen Video-Encoder, keine benutzerdefinierten Aufmerksamkeitsimplementierungen, nur der Commonplace Transformatoren API mit PIL-Bildern als Eingabe.
Die Besprechungszusammenfassung in diesem Artikel ist die Vorlage. Ersetzen FRAME_PROMPT mit einer Aufforderung, die auf Ihre Area abgestimmt ist, ändern build_synthesis_prompt() um alle strukturierten Felder zu extrahieren, die für Ihren Anwendungsfall wichtig sind, und die gleiche Pipeline eignet sich für Vorlesungsaufzeichnungen, Sicherheitsaufnahmen, exemplarische Vorgehensweisen zu Produktdemos oder Sporthighlights. Das Zwei-Durchlauf-Muster, zuerst die Beschreibung professional Body und dann die Synthese, gilt für alle, da das Modell einzelne Frames genau beschreibt und Beschreibungen zuverlässig synthetisiert.
Die Grenze von 50 Bildern ist ein Ausgangspunkt, keine Obergrenze. Bei {Hardware} mit höherem VRAM erhöhen max_frames auf 75 oder 100 und experimentieren Sie. Die Qualität skaliert bis zu einem gewissen Punkt mit der Bildabdeckung, und Ihr Synthesedurchlauf profitiert von mehr Materials, mit dem Sie arbeiten können.
Shittu Olumid ist ein Software program-Ingenieur und technischer Autor, der sich leidenschaftlich dafür einsetzt, modernste Technologien zu nutzen, um fesselnde Erzählungen zu erschaffen, mit einem scharfen Blick fürs Element und einem Gespür für die Vereinfachung komplexer Konzepte. Sie können Shittu auch auf finden Twitter.
