Multimodal models understand text, images, audio, and video simultaneously. They combine specialized encoders for each modality.
How Images Become Tokens
Text: "The dog jumps"
→ Tokenize → ["The", "dog", "jumps"]
Image: [Pixel Grid 224×224]
→ Vision Transformer / CNN
→ Image Tokens: [tok_1, tok_2, ..., tok_256]
Audio: [Waveform 16kHz]
→ Audio Encoder (Mel-Spectrogram)
→ Audio Tokens: [tok_1, tok_2, ..., tok_512]
Video: [30 frames]
→ Temporal Encoder
→ Video Tokens: [tok_1, tok_2, ..., tok_4096]
Then: All tokens → Transformer → LLM generates answer
2026 Model Comparison
| Model | Vision | Audio | Video | Best For | Cost |
|---|---|---|---|---|---|
| GPT-4V | ✅ Excellent | ❌ | ✅ OK | General-purpose | €0.03/K |
| GPT-4o | ✅ Excellent | ✅ | ✅ Good | General-purpose | €0.005/K |
| Claude 3.5 Vision | ✅ Best | ✅ | ✅ Okay | Image analysis | €0.003/K |
| Gemini 2.0 | ✅ Good | ✅ | ✅ Best | Video understanding | €0.0075/K |
| LLaVA 1.5 | ✅ Medium | ❌ | ❌ | Open-source, local | Free |
Vision-Language Models
Claude 3.5 Sonnet (Best for Images)
response = client.messages.create(
model="claude-3-5-sonnet",
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": base64_encoded_image,
},
},
{
"type": "text",
"text": "What do you see? Describe details."
}
],
}]
)
Strengths:
- Best detail accuracy (PDFs, graphics, code in images)
- Can OCR text
- Excellent rectangle detection
Limitations:
- 200K context (10 min max video)
- No direct audio (use Whisper first)
GPT-4o (General Purpose, Audio)
# Vision
response = client.messages.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "https://..."}},
{"type": "text", "text": "Explain this diagram"}
]
}]
)
# Audio via Whisper first
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file
)
Gemini 2.0 (Best for Video)
Better at video understanding and 1M context.
Audio Models
Whisper (Speech-to-Text)
from openai import OpenAI
client = OpenAI()
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=open("audio.mp3", "rb"),
language="en"
)
# Accuracy: 94-97%
# Cost: €0.02 per minute (expensive!)
Video Understanding
Workflow:
1. Extract keyframes (every 10 frames)
2. Patch + embed each keyframe
3. Audio: Whisper transcription separately
4. Combine: [Video tokens] + [Audio] + [Transcript] → LLM
Result: Scene understanding, object detection, summaries
Practical Use-Cases
Document Analysis
Input: 100-page scanned PDF
Process:
- PDF → Images (1 per page)
- Claude Vision: Analyze each page
- Extract: Text, tables, charts
- OCR handwriting
Cost: ~€0.03 per page
Accuracy: 95%+ for printed text
Product Detection (E-Commerce)
Input: Product photo
Output:
- Material: Cotton
- Color: Blue
- Size: Large
- Defects: None detected
Use: Automate 100+ catalogs per day
Meeting Summarization
Input: 1h recorded meeting
Process:
1. Audio extract + Whisper
2. Key frames (every 30s)
3. Vision + Transcript combined
4. Summarize
Output: 5-min summary + action items
Cost: ~€0.50 per hour
Limitations & Fixes
Issue: Hallucinations on visual details
Fix: "Describe ONLY what you see, not interpretation"
Issue: Small text in images hard to read
Fix: Enlarge text area first, or OCR separately
Issue: Video is token-expensive (many frames)
Fix: Keyframe sampling, or key-scene detection
Issue: Audio not native in tokens
Fix: Whisper first → text → LLM
Multimodal models are production-ready for image/document analysis. Claude 3.5 Vision and GPT-4o both excellent. Video understanding still developing.
Sources and Links
Advanced: Vision Transformer Architecture
How multimodal models actually work:
Image Input (224×224)
↓
Patch Embedding (16×16 patches)
↓
Position Encoding
↓
Transformer Encoder (12 layers)
↓
Visual Features: [cls_token, patch_1_feat, ..., patch_196_feat]
↓
[MERGED with Text Tokens]
↓
LLM Decoder
↓
Text Output
Key insight: Vision Transformers break images into patches (tokens), same as LLMs tokenize text. This unified token representation enables joint processing.
Cost Analysis: Image Processing at Scale
Scenario: Process 10,000 product images (1MB each)
Provider | Cost per 1000 | Total Cost | Time |
----------------|---------------|-----------|------|
Claude 3.5 | €0.30 | €3.00 | 2h |
GPT-4V | €0.60 | €6.00 | 1.5h |
Gemini 2.0 | €0.075 | €0.75 | 3h |
Local LLaVA | Free (GPU) | €0.50* | 4h |
*GPU cost amortized over 10K images
Recommendation:
- < 1000 images: Use cloud API (faster)
- 1000-100K images: Switch to local LLaVA/similar
-
100K: Custom fine-tuned model
Practical: OCR with Vision Models
Text extraction from images (documents, receipts, handwriting):
import base64
from anthropic import Anthropic
client = Anthropic()
def extract_text_from_image(image_path, include_handwriting=False):
with open(image_path, "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
prompt = """Extract all text from this image.
Format as markdown with structure preserved.
Include tables, lists, headings."""
if include_handwriting:
prompt += "\nAlso transcribe any handwritten text."
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_data,
},
},
{
"type": "text",
"text": prompt
}
],
}
],
)
return message.content[0].text
# Usage
extracted = extract_text_from_image("invoice.jpg")
print(extracted)
Accuracy benchmarks (2026):
- Printed English: 96-98%
- Printed German: 94-96%
- Handwritten: 70-85% (depends on legibility)
- Mixed (printed + handwritten): 80-90%
Batch Processing Large Document Sets
For cost efficiency with many documents:
import json
from pathlib import Path
from anthropic import Anthropic
def batch_process_documents(image_dir, output_file, batch_size=10):
"""Process multiple images, save results, resume on error."""
client = Anthropic()
results = []
processed = set()
# Load previous results to resume
if Path(output_file).exists():
with open(output_file) as f:
existing = json.load(f)
processed = {r['image'] for r in existing}
results = existing
images = sorted(Path(image_dir).glob("*.jpg"))
images = [img for img in images if img.name not in processed]
for i, image_path in enumerate(images):
if i % batch_size == 0:
print(f"Progress: {i}/{len(images)}")
try:
# Process image
text = extract_text_from_image(str(image_path))
results.append({
"image": image_path.name,
"text": text,
"status": "success"
})
except Exception as e:
results.append({
"image": image_path.name,
"error": str(e),
"status": "error"
})
# Save after each image (checkpoint for resume)
with open(output_file, 'w') as f:
json.dump(results, f)
return results
# Usage
batch_process_documents("./invoices/", "results.json")
Cost optimization tips:
- Compress images to < 500KB (same quality, 50% cost reduction)
- Batch similar documents (same system prompt → lower variation)
- Cache common instructions (context caching if supported)
- Use local models for high-volume, lower-quality tasks
Video Analysis: Frame Extraction & Summarization
import cv2
import json
from pathlib import Path
def extract_key_frames(video_path, fps_sampling=1, output_dir="frames"):
"""Extract frames at specified intervals."""
cap = cv2.VideoCapture(video_path)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
video_fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = int(video_fps / fps_sampling) # 1 frame per second
frame_num = 0
extracted = []
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_num % frame_interval == 0:
frame_path = f"{output_dir}/frame_{frame_num:06d}.jpg"
cv2.imwrite(frame_path, frame)
timestamp = frame_num / video_fps
extracted.append({
"frame": frame_path,
"timestamp": f"{int(timestamp//60)}:{int(timestamp%60):02d}",
"frame_num": frame_num
})
frame_num += 1
cap.release()
return extracted
def summarize_video(video_path, fps_sampling=1):
"""Extract frames and generate summary."""
frames = extract_key_frames(video_path, fps_sampling=fps_sampling)
summaries = []
client = Anthropic()
for frame_info in frames:
# Process frame with Claude
with open(frame_info["frame"], "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_data,
},
},
{
"type": "text",
"text": "Describe what's happening in this video frame. Be concise (1-2 sentences)."
}
],
}]
)
summaries.append({
"timestamp": frame_info["timestamp"],
"description": message.content[0].text
})
return summaries
# Usage
summaries = summarize_video("meeting.mp4", fps_sampling=0.5) # 1 frame every 2 seconds
for s in summaries:
print(f"{s['timestamp']}: {s['description']}")
Video processing strategy:
- Short videos (< 5 min): Keyframe every 5s
- Medium (5-30 min): Keyframe every 10-30s
- Long (> 30 min): Keyframe every 60s + scene detection
Multimodal RAG: Images in Knowledge Bases
from chromadb import Client
from pathlib import Path
import base64
def image_rag_pipeline(query_text, image_dir, query_image=None):
"""Retrieve similar images and documents for query."""
client = Client()
collection = client.create_collection(name="images")
# 1. Add images to collection with descriptions
for image_path in Path(image_dir).glob("*.jpg"):
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
# Generate image description
description = get_image_description(image_data)
collection.add(
ids=[image_path.stem],
documents=[description],
metadatas=[{
"image_path": str(image_path),
"image_base64": image_data
}]
)
# 2. Search by text query
text_results = collection.query(
query_texts=[query_text],
n_results=3
)
# 3. Search by image query (if provided)
if query_image:
query_description = get_image_description(query_image)
image_results = collection.query(
query_texts=[query_description],
n_results=3
)
else:
image_results = {"ids": [], "documents": []}
return text_results, image_results
def get_image_description(image_base64):
"""Get text description of image for embedding."""
client = Anthropic()
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_base64,
},
},
{
"type": "text",
"text": "Describe this image in 2-3 sentences for a search database."
}
],
}]
)
return message.content[0].text
Common Issues & Fixes
Issue: Image too small/blurry, model can't read
# Solution: Upscale before sending
from PIL import Image
import cv2
def upscale_image(image_path, scale_factor=2):
img = cv2.imread(image_path)
height, width = img.shape[:2]
new_size = (width * scale_factor, height * scale_factor)
upscaled = cv2.resize(img, new_size, interpolation=cv2.INTER_CUBIC)
return upscaled
Issue: Model hallucinates details not in image
# Solution: Use explicit instruction
prompt = """Look at this image carefully.
Describe ONLY what you can actually see.
Do NOT infer or guess about details not visible."""
Issue: Inconsistent results across modalities
# Solution: Use consistent formatting/preprocessing
def preprocess_image(image_path, target_size=1024):
img = Image.open(image_path)
# Resize maintaining aspect ratio
img.thumbnail((target_size, target_size), Image.Resampling.LANCZOS)
# Convert to RGB if RGBA
if img.mode == 'RGBA':
img = img.convert('RGB')
return img
Issue: Cost explodes with video processing
# Solution: Adaptive frame sampling based on motion
def smart_frame_extraction(video_path, motion_threshold=10):
"""Extract frames only when scene changes significantly."""
cap = cv2.VideoCapture(video_path)
prev_frame = None
frames = []
frame_num = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if prev_frame is None:
frames.append(frame_num)
else:
diff = cv2.absdiff(cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY),
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))
if diff.mean() > motion_threshold:
frames.append(frame_num)
prev_frame = frame
frame_num += 1
cap.release()
return frames # Only significant frames
Performance Benchmarks (2026)
| Task | Model | Speed | Accuracy | Cost |
|---|---|---|---|---|
| Document OCR (10 pages) | Claude 3.5 | 20s | 96% | €0.06 |
| Product Detection | GPT-4V | 5s | 94% | €0.015 |
| Video Summary (5 min) | Gemini 2.0 | 30s | 87% | €0.10 |
| Chart Reading | Claude 3.5 | 3s | 98% | €0.01 |
| Handwriting Recognition | Claude 3.5 | 8s | 78% | €0.02 |
When to Use Multimodal
✅ Excellent for:
- Document/receipt processing
- Product image analysis
- Visual Q&A
- Chart/diagram understanding
- Accessibility (describing images)
- Content moderation
❌ Poor for:
- Real-time video processing (too slow)
- High-frequency image analysis (cost prohibitive)
- Tasks solvable with traditional CV (use OpenCV instead)
- Extremely small/large images (resolution issues)
