🎯 What You'll Learn

  • Understand what multimodal models are and why they're essential for real-world AI
  • Understand CLIP's contrastive learning objective and its joint text-image embedding space
  • Use CLIP for zero-shot image classification and semantic image search
  • Understand the architecture of Vision-Language Models (VLMs) like LLaVA and GPT-4V
  • Use BLIP-2 for image captioning and Visual Question Answering (VQA)
  • Use OpenAI Whisper for speech recognition and transcription
  • Build a multimodal application pipeline combining vision, language, and audio
💡
The Big Intuition

Humans experience the world through multiple senses simultaneously — we see, hear, read, and integrate all of this naturally. "The car screeched to a halt" means something different if you see a wet road vs a dry one. The cutting edge of AI is now multimodal: models that can see images, read text, listen to audio, and reason across all of these modalities together. GPT-4V can read a handwritten recipe and suggest substitutions. Gemini can watch a video and summarize it. Whisper can transcribe any language. This lesson is a map of where AI is right now and where it's going.

1 What Are Multimodal Models?

A unimodal model processes exactly one type of data: a text-only LLM, an image classifier CNN, or a speech recognition model. Each is excellent within its domain but blind to others. Multimodal models accept, process, and reason about multiple data types simultaneously or in combination.

Why Multimodality Matters

Consider a doctor reviewing a patient case. They have: (1) a chest X-ray image, (2) the patient's written history, (3) previous lab reports, (4) audio recordings of lung sounds. A unimodal image model can read the X-ray but ignores the history. A text-only LLM can read the history but can't interpret the image. A truly helpful clinical AI must reason over all of these together, understanding how the visual findings relate to the reported symptoms and history.

This multimodal reasoning is the norm, not the exception, in real-world applications:

  • E-commerce: Product search using an image you photographed + a text description ("blue running shoes, lightweight, similar to this")
  • Accessibility: Reading an image-heavy document aloud by understanding both layout and content
  • Code debugging: Looking at a screenshot of an error alongside the source code
  • Education: Explaining a diagram by understanding what is drawn and what the student asks

The Modality Zoo

Modality Representation Key Models
Text Token embeddings GPT-4, LLaMA, Mistral
Image Patch embeddings (ViT) CLIP, DINOv2, SigLIP
Audio Mel spectrogram Whisper, EnCodec, AudioLM
Video Temporal + spatial patches Sora, VideoLLaMA, InternVideo
3D / Point Cloud PointNet features PointBERT, Uni3D
Molecule / Protein Graph embeddings AlphaFold3, MolBERT, GNNs

2 CLIP: Connecting Vision and Language

CLIP (Contrastive Language-Image Pre-training, OpenAI 2021) was a landmark model that created a shared embedding space for text and images. Understanding CLIP is essential for understanding most modern vision-language systems — it underpins Stable Diffusion, LLaVA, DALL·E, and many other systems.

The Training Objective

CLIP was trained on 400 million (image, caption) pairs scraped from the internet. The training objective is contrastive: given a batch of N (image, text) pairs, the image encoder and text encoder should produce similar vectors for matching pairs and dissimilar vectors for non-matching pairs. For a batch of N=256 pairs, there are 256 correct pairs and 256×255 = 65,280 incorrect pairs. The loss pushes the 256 correct similarities up and the 65,280 incorrect ones down.

Batch of N Images I₁, I₂, I₃ … Iₙ Image Encoder I_emb (N × 512) Batch of N Captions "a photo of a dog", "a red car" … Batch of N Texts T₁, T₂, T₃ … Tₙ Text Encoder T_emb (N × 512) Similarity Matrix (N × N) I_emb · T_embᵀ diagonal (matches) ↑ pushed high · off-diagonal ↓ pushed low

The CLIP contrastive training setup: N images and N texts are embedded independently, then every image embedding is dotted with every text embedding to form an N×N similarity matrix. The loss (InfoNCE) increases the diagonal entries (each image with its own caption) and decreases every off-diagonal entry (each image with every other caption in the batch).

To see what a "well-trained" similarity matrix looks like numerically, here is an illustrative 6×6 example using the six candidate captions from the code below, paired with six matching images — cosine similarities scaled by CLIP's learned temperature. Notice the diagonal (correct pairs) is visibly brighter than every off-diagonal cell:

Illustrative similarity matrix for a batch of 6 (image, caption) pairs. The bright diagonal shows each image scoring highest against its own caption; off-diagonal cells (wrong pairings) are darker, as the contrastive loss pushes them down during training.

The Result: A Joint Semantic Space

After training, CLIP's embedding space has a remarkable property: the vector for the text "a dog playing fetch" is geometrically close to the vector for an image showing that scene — even if that exact image was never paired with that exact caption in training. CLIP has learned the semantic mapping between visual and linguistic concepts.

This is the single idea to hold onto: images and text are not two separate spaces that get compared at the end — they are embedded into one shared space, so a photo and a sentence about that photo can sit right next to each other, and a photo and an unrelated sentence sit far apart. The chart below places a few illustrative image and caption embeddings (projected to 2D) to make this concrete — notice the dog photo and "a photo of a dog" land in the same neighborhood, as do the two car examples, while the dog and car clusters sit far apart from each other.

Illustrative 2D projection of CLIP's joint embedding space — not real computed embeddings. Real CLIP embeddings are 512 dimensions; these coordinates were hand-placed to show the pattern that training produces: each image (■) sits close to its matching caption (●), and unrelated (image, caption) pairs sit far apart.

In [1]:
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import torch
import requests

# Load CLIP
model     = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
model.eval()

# Load a real image
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/43/Cute_dog.jpg/1200px-Cute_dog.jpg"
image = Image.open(requests.get(url, stream=True).raw)

# Candidate text descriptions
texts = [
    "a photo of a dog",
    "a photo of a cat",
    "a photo of a car",
    "a photo of a bird",
    "a photo of a dog running in a park",
]

# Process inputs
inputs = processor(
    text=texts,
    images=image,
    return_tensors="pt",
    padding=True
)

# Get embeddings and compute similarity
with torch.no_grad():
    outputs          = model(**inputs)
    image_embedding  = outputs.image_embeds    # (1, 512)
    text_embeddings  = outputs.text_embeds     # (5, 512)

    # Cosine similarity between image and each text
    image_norm  = image_embedding  / image_embedding.norm(dim=-1, keepdim=True)
    text_norms  = text_embeddings  / text_embeddings.norm(dim=-1, keepdim=True)
    similarities = (image_norm @ text_norms.T).squeeze()   # (5,)

    # Convert to probabilities (softmax over texts)
    probs = similarities.softmax(dim=0)

print("CLIP zero-shot classification results:")
for text, prob, sim in zip(texts, probs, similarities):
    print(f"  {prob*100:5.1f}%  (sim={sim:.3f})  |  '{text}'")
Out[1]:
CLIP zero-shot classification results: 58.3% (sim=0.312) | 'a photo of a dog' 2.1% (sim=0.178) | 'a photo of a cat' 1.4% (sim=0.164) | 'a photo of a car' 3.2% (sim=0.192) | 'a photo of a bird' 35.0% (sim=0.296) | 'a photo of a dog running in a park'

CLIP Architecture Details

In [2]:
from transformers import CLIPModel
import torch

model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")

# Inspect the architecture
total_params = sum(p.numel() for p in model.parameters()) / 1e6
print(f"Total CLIP parameters: {total_params:.1f}M")

# Text encoder: Transformer
text_params   = sum(p.numel() for p in model.text_model.parameters()) / 1e6
print(f"Text encoder:  {text_params:.1f}M parameters")
print(f"  Architecture: 12-layer Transformer, 512-dim hidden, 512-dim output")

# Vision encoder: ViT (Vision Transformer)
vision_params = sum(p.numel() for p in model.vision_model.parameters()) / 1e6
print(f"Vision encoder: {vision_params:.1f}M parameters")
print(f"  Architecture: ViT-B/32 — 12-layer Transformer on 32×32 image patches")

# The embeddings are projected to a shared 512-dim space
print(f"\nShared embedding dimension: 512")
print("Both text and image embeddings are L2-normalized before similarity computation")

# Test with random inputs
dummy_text  = torch.randint(0, 1000, (2, 77))  # 2 text sequences, max 77 tokens
dummy_image = torch.randn(2, 3, 224, 224)       # 2 RGB images, 224x224

with torch.no_grad():
    t_emb = model.get_text_features(dummy_text)
    i_emb = model.get_image_features(dummy_image)

print(f"\nText embedding shape:  {t_emb.shape}")   # (2, 512)
print(f"Image embedding shape: {i_emb.shape}")   # (2, 512)
🔑
Why CLIP Embeddings Are So Powerful

CLIP's joint space enables zero-shot transfer: if you train a model on 1000 classes with CLIP embeddings, it often generalises to new unseen classes — because the embedding space is learned from 400M diverse examples and encodes general visual concepts. CLIP embeddings are also surprisingly useful for tasks beyond classification: semantic search (find the image matching a text query), anomaly detection (flag images far from any known class description), and as visual features for downstream models (LLaVA, Stable Diffusion). CLIP's impact on the field has been compared to ImageNet's impact — a universally useful pretrained representation.

3 CLIP Applications: Zero-Shot Classification and Image Search

The joint embedding space opens up capabilities that didn't require any labeled training data for new tasks.

Zero-Shot Image Classification

Instead of training a separate linear classifier head, you define classes as text descriptions and find which description has the highest cosine similarity with the image embedding. No labeled examples needed — just describe what you're looking for in natural language.

In [3]:
from transformers import CLIPModel, CLIPProcessor
from datasets import load_dataset
import torch

model     = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
model.eval()

def clip_zero_shot_classify(image, class_names, template="a photo of a {}"):
    """
    Zero-shot image classification with CLIP.
    class_names: list of class name strings
    template: how to turn a class name into a text prompt
    """
    # Create text prompts from class names
    text_prompts = [template.format(cn) for cn in class_names]

    inputs = processor(
        text=text_prompts,
        images=image,
        return_tensors="pt",
        padding=True,
    )

    with torch.no_grad():
        outputs = model(**inputs)
        image_emb = outputs.image_embeds / outputs.image_embeds.norm(dim=-1, keepdim=True)
        text_embs  = outputs.text_embeds  / outputs.text_embeds.norm(dim=-1, keepdim=True)
        logits     = (image_emb @ text_embs.T).squeeze() * model.logit_scale.exp()
        probs      = logits.softmax(dim=-1)

    results = {cn: prob.item() for cn, prob in zip(class_names, probs)}
    return dict(sorted(results.items(), key=lambda x: x[1], reverse=True))


# Compare zero-shot vs supervised on CIFAR-10
# CIFAR-10 classes
cifar10_classes = [
    "airplane", "automobile", "bird", "cat", "deer",
    "dog", "frog", "horse", "ship", "truck"
]

# Load a few CIFAR-10 test images
dataset = load_dataset("cifar10", split="test[:10]")

correct = 0
for sample in dataset:
    image = sample['img']
    true_label = cifar10_classes[sample['label']]
    predictions = clip_zero_shot_classify(image, cifar10_classes)
    predicted = max(predictions, key=predictions.get)
    is_correct = predicted == true_label
    correct += is_correct
    print(f"  True: {true_label:12s} | Predicted: {predicted:12s} | {'✓' if is_correct else '✗'}")

print(f"\nZero-shot accuracy: {correct}/10 = {correct*10}%")
print("CLIP ViT-B/32 on full CIFAR-10: ~65% zero-shot vs ~95% supervised ResNet-50")

Semantic Image Search

In [4]:
import torch
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import numpy as np

def build_image_index(image_paths, model, processor):
    """Pre-compute and store embeddings for all images in a collection."""
    embeddings = []
    for path in image_paths:
        img    = Image.open(path).convert("RGB")
        inputs = processor(images=img, return_tensors="pt")
        with torch.no_grad():
            emb = model.get_image_features(**inputs)
        emb = emb / emb.norm(dim=-1, keepdim=True)
        embeddings.append(emb.squeeze().numpy())
    return np.array(embeddings)


def text_to_image_search(query, image_paths, image_embeddings, model, processor, top_k=3):
    """Find images that best match a text query."""
    # Embed the text query
    inputs = processor(text=[query], return_tensors="pt", padding=True)
    with torch.no_grad():
        text_emb = model.get_text_features(**inputs)
    text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True)
    text_emb = text_emb.squeeze().numpy()

    # Cosine similarity
    sims = image_embeddings @ text_emb
    top_indices = sims.argsort()[-top_k:][::-1]

    return [(image_paths[i], float(sims[i])) for i in top_indices]


# Usage example (assuming you have a collection of images)
# image_paths = list(Path("./images").glob("*.jpg"))
# embeddings = build_image_index(image_paths, model, processor)
# results = text_to_image_search(
#     "a red car on a mountain road",
#     image_paths, embeddings, model, processor
# )
# for path, score in results:
#     print(f"  Score: {score:.3f}  |  {path}")

print("CLIP semantic search: find images matching natural language descriptions")
print("Applications: Google Photos (find 'beach sunset'), Unsplash (stock photo search)")
🌍
CLIP in Production: Open-Vocabulary Detection

Standard object detectors (YOLO, Faster R-CNN) detect only the categories they were trained on (COCO's 80 classes). CLIP enables open-vocabulary detection (GLIP, OWL-ViT, Grounding DINO): describe any object in natural language and the model detects it — no retraining needed. OWL-ViT can detect "a partially eaten apple on a wooden table" or "a scratch on the car bumper" without ever being specifically trained on those categories. This is currently one of the most practically impactful applications of CLIP's joint embedding space.

4 Vision-Language Models (VLMs): LLaVA and GPT-4V

CLIP gives us a joint embedding space but can't have a conversation about an image. Vision-Language Models (VLMs) combine a powerful visual encoder with a generative language model, enabling free-form visual question answering, description, and reasoning.

The VLM Architecture

The standard VLM architecture has three components: (1) a visual encoder (typically CLIP/ViT) that extracts image features; (2) a projection layer (typically a linear layer or small MLP) that maps image features into the LLM's token embedding space; (3) a language model (LLaMA, Mistral, Vicuna) that generates text, treating the projected image features as "visual tokens" prepended to the text tokens. The visual tokens occupy a portion of the context window (typically 256 or 576 tokens for a 336×336 image).

🖼 Input Image 336×336 px Visual Encoder CLIP ViT-L/14 576 patch tokens · 1024-dim Projection Layer 2-layer MLP 1024-dim → 4096-dim 576 "visual tokens" Text Prompt "What's happening here?" Language Model (LLM) LLaMA / Vicuna / Mistral [visual tokens] + [text tokens] → self-attention Generated Answer "Two people hiking near a lake…"

The VLM pipeline: a frozen visual encoder turns the image into patch features, a small trainable projection layer maps those features into the LLM's token embedding space as "visual tokens," and the LLM attends over visual tokens + text tokens together to generate a free-form answer.

LLaVA: Open-Source VLM

LLaVA (Large Language and Vision Assistant) is the most widely used open-source VLM. LLaVA-1.5 uses CLIP ViT-L/14@336 as the visual encoder, a two-layer MLP as the projection layer, and Vicuna-13B (a fine-tuned LLaMA) as the language model. Training: two stages — first train the projection layer alone with image captioning data; then train projection + LLM with visual instruction following data. The result: a model that can answer detailed questions about images, describe scene contents, read text in images (OCR), count objects, and reason about spatial relationships.

In [5]:
from transformers import LlavaForConditionalGeneration, AutoProcessor
from PIL import Image
import torch
import requests

# Load LLaVA-1.5 (7B model — needs ~16GB VRAM for fp16)
model_id = "llava-hf/llava-1.5-7b-hf"

# For memory efficiency on smaller GPUs, load in 4-bit
from transformers import BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(load_in_4bit=True)

processor = AutoProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
    model_id,
    quantization_config=quantization_config,
    device_map="auto",
)

# Load an image
url   = "https://llava-vl.github.io/static/images/view.jpg"
image = Image.open(requests.get(url, stream=True).raw)

# Ask questions about the image
questions = [
    "What is happening in this image?",
    "How many people are in the image?",
    "What time of day does it appear to be?",
    "Describe the weather conditions.",
]

for question in questions:
    # LLaVA conversation format
    conversation = [
        {
            "role": "user",
            "content": [
                {"type": "image"},
                {"type": "text", "text": question},
            ],
        },
    ]

    prompt = processor.apply_chat_template(conversation, add_generation_prompt=True)
    inputs = processor(images=image, text=prompt, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=128,
            do_sample=False,
        )

    # Decode only the generated tokens (not the prompt)
    generated = outputs[0][inputs["input_ids"].shape[1]:]
    answer    = processor.decode(generated, skip_special_tokens=True)
    print(f"Q: {question}")
    print(f"A: {answer}\n")

GPT-4V and Commercial VLMs

In [6]:
from openai import OpenAI
import base64
from pathlib import Path

client = OpenAI()  # requires OPENAI_API_KEY

def encode_image_base64(image_path):
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode('utf-8')


def gpt4v_query(image_path, question, detail="high"):
    """
    Query GPT-4V with an image and text question.
    detail: "low" (512 tokens, faster/cheaper) or "high" (up to 4096 tokens for resolution)
    """
    base64_image = encode_image_base64(image_path)

    response = client.chat.completions.create(
        model="gpt-4o",   # GPT-4o has best vision capabilities
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}",
                            "detail": detail,
                        }
                    },
                    {
                        "type": "text",
                        "text": question,
                    },
                ],
            }
        ],
        max_tokens=500,
    )
    return response.choices[0].message.content


# GPT-4V capabilities beyond LLaVA:
# 1. Read and interpret complex charts/graphs
# 2. Understand math equations written on whiteboards
# 3. Interpret X-ray or medical images
# 4. Read handwritten text
# 5. Understand diagrams and technical schematics
# 6. Code completion from a screenshot of code

print("GPT-4V: most capable commercial VLM")
print("LLaVA-1.5-13B: best open-source alternative, competitive for many tasks")
🔑
The Projection Layer: Bridging Vision and Language

The visual encoder produces features in its own embedding space (e.g., 1024-dim for ViT-L). The language model operates in a different token embedding space (e.g., 4096-dim for LLaMA-7B). The projection layer — typically just a two-layer MLP — maps from visual space to language space. This tiny bridge (a few million parameters) is what enables the LLM to "read" images. LLaVA showed that this simple architecture, combined with good training data, was sufficient for impressive visual reasoning — you don't need a complex fusion architecture.

5 Image Captioning and Visual Question Answering

Image captioning and VQA are two foundational vision-language tasks that have driven substantial progress in multimodal AI.

BLIP-2: Bootstrap Language-Image Pretraining

BLIP-2 (Li et al., 2023) uses a lightweight Querying Transformer (Q-Former) to bridge a frozen image encoder and a frozen LLM. The Q-Former learns to extract visual features that are most informative for the language model through a set of learnable query vectors. Because both the image encoder and LLM are frozen, only the lightweight Q-Former (~188M parameters) is trained — making BLIP-2 much cheaper to train than LLaVA while achieving competitive performance.

In [7]:
from transformers import Blip2ForConditionalGeneration, Blip2Processor
from PIL import Image
import requests
import torch

# Load BLIP-2 (OPT-2.7B backbone — smaller than 7B LLaVA)
processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b")
model = Blip2ForConditionalGeneration.from_pretrained(
    "Salesforce/blip2-opt-2.7b",
    torch_dtype=torch.float16,
    device_map="auto",
)
model.eval()

# Load a real image
url   = "https://storage.googleapis.com/sfr-vision-language-research/BLIP/demo.jpg"
image = Image.open(requests.get(url, stream=True).raw).convert('RGB')


# ── Task 1: Image Captioning (no question) ──
inputs = processor(images=image, return_tensors="pt").to(model.device, torch.float16)
with torch.no_grad():
    generated_ids = model.generate(**inputs, max_new_tokens=50)
caption = processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
print(f"Caption: {caption}")


# ── Task 2: Visual Question Answering ──
vqa_questions = [
    "What animals are in this photo?",
    "What is the weather like?",
    "How many people are visible?",
    "What is in the background?",
]

for question in vqa_questions:
    # Prepend question to guide generation
    inputs = processor(
        images=image,
        text=f"Question: {question} Answer:",
        return_tensors="pt"
    ).to(model.device, torch.float16)

    with torch.no_grad():
        generated_ids = model.generate(**inputs, max_new_tokens=30)
    answer = processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
    print(f"Q: {question}")
    print(f"A: {answer}\n")
💡
VQA Benchmark Performance

VQAv2 is the standard benchmark for visual question answering: 443k (image, question, answer) triplets. Human accuracy: ~87%. Best supervised models: ~85–86%. GPT-4V zero-shot: ~78%. LLaVA-1.5-13B: ~72%. BLIP-2 (OPT-2.7B): ~65%. The gap between human performance and models narrows with model scale and fine-tuning. VQAv2 questions include spatial reasoning ("What is to the left of the car?"), counting ("How many birds are visible?"), and attribute recognition ("What color is the umbrella?") — each type challenges different aspects of visual understanding.

6 Speech and Audio: Whisper

Audio is the other critical modality beyond vision. Whisper (OpenAI, 2022) demonstrated that large-scale training on diverse audio data could produce a single model that handles speech recognition across languages, accents, and recording conditions.

Whisper Architecture

Whisper uses a standard encoder-decoder transformer. The encoder processes audio: the raw audio waveform is first converted to log-Mel spectrogram features (a visual representation of frequency content over time — the "image" of the sound). These 2D features are then processed by a CNN stem followed by transformer encoder layers. The decoder autoregressively generates text tokens conditioned on the encoder's audio representation.

Scale Is Everything

Whisper was trained on 680,000 hours of multilingual speech — vastly more than any previous model. The data came from the internet and covers 99+ languages, diverse accents, technical jargon, background noise, and multiple recording conditions (phone calls, podcasts, lectures). This scale makes Whisper robust to real-world conditions that previous models failed on.

In [8]:
import whisper
import torch

# Load Whisper (pip install openai-whisper)
# Model sizes: tiny (39M), base (74M), small (244M), medium (769M), large (1550M)
model = whisper.load_model("base")   # good balance of speed and accuracy

# Transcribe an audio file
def transcribe_audio(audio_path, language=None, task="transcribe"):
    """
    Transcribe or translate an audio file with Whisper.
    task: "transcribe" (keep original language) or "translate" (to English)
    language: force a specific language (e.g., "French") or None for auto-detection
    """
    options = {}
    if language:
        options['language'] = language
    if task == "translate":
        options['task'] = 'translate'

    result = model.transcribe(audio_path, **options)

    return {
        "text":     result["text"],
        "language": result.get("language", "detected"),
        "segments": result.get("segments", []),
    }

# Example transcription
result = transcribe_audio("meeting_recording.mp3")
print(f"Detected language: {result['language']}")
print(f"Transcript: {result['text']}")

# Word-level timestamps (with verbose_json)
result_detailed = model.transcribe(
    "meeting_recording.mp3",
    word_timestamps=True,
    verbose=False,
)
print("\nWord-level timestamps (first 5 words):")
if result_detailed.get("segments"):
    for segment in result_detailed["segments"][:2]:
        for word in segment.get("words", [])[:5]:
            print(f"  [{word['start']:.2f}s – {word['end']:.2f}s]: {word['word']}")
In [9]:
# Whisper via Hugging Face transformers (alternative API)
from transformers import WhisperForConditionalGeneration, WhisperProcessor
import torchaudio

processor = WhisperProcessor.from_pretrained("openai/whisper-small")
model_hf  = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small")
model_hf.config.forced_decoder_ids = None

def transcribe_hf(audio_path):
    """Transcribe using Hugging Face transformers API."""
    # Load audio (resample to 16kHz — Whisper's required sample rate)
    waveform, sample_rate = torchaudio.load(audio_path)
    if sample_rate != 16000:
        resampler = torchaudio.transforms.Resample(sample_rate, 16000)
        waveform  = resampler(waveform)
    waveform = waveform.mean(dim=0)  # convert to mono

    # Extract log-Mel features
    inputs = processor(
        waveform.numpy(),
        sampling_rate=16000,
        return_tensors="pt",
    )

    # Generate transcription
    with torch.no_grad():
        predicted_ids = model_hf.generate(inputs.input_features)

    transcript = processor.batch_decode(predicted_ids, skip_special_tokens=True)
    return transcript[0]

# Whisper performance benchmarks:
print("\nWhisper Word Error Rate (WER) on LibriSpeech test-clean:")
print("  tiny:   WER = 9.8%   (fastest, use on-device)")
print("  base:   WER = 7.4%   (good for general use)")
print("  small:  WER = 5.5%   (recommended for most tasks)")
print("  medium: WER = 4.2%   (near-human on clean speech)")
print("  large:  WER = 3.0%   (human-level, ~5% on spontaneous speech)")
🌍
Whisper in Production

Otter.ai, Otter.ai, and Fireflies.ai use Whisper-based models for automated meeting transcription. YouTube's automatic captions use Whisper or Whisper-derived models, covering 80+ languages. Deaf and hard-of-hearing users benefit enormously from Whisper's accuracy on diverse accents and background noise — conditions that failed previous generation models. In emergency services, Whisper automates dispatch recording transcription. In medical settings, it transcribes clinical notes dictated by doctors, though domain-specific fine-tuning is needed for technical medical vocabulary.

7 Building a Multimodal Application

Let's combine the building blocks we've studied into a concrete end-to-end multimodal pipeline: an automated product catalog system that takes a product photo and an audio description and generates a complete catalog entry with visual search capability.

In [10]:
from transformers import (
    CLIPModel, CLIPProcessor,
    Blip2ForConditionalGeneration, Blip2Processor,
    WhisperForConditionalGeneration, WhisperProcessor,
)
from PIL import Image
import torch, numpy as np


class MultimodalProductCatalogue:
    """
    Automated product catalog pipeline:
    Input:  product photo + audio description
    Output: structured catalog entry + visual search embedding
    """

    def __init__(self):
        device = "cuda" if torch.cuda.is_available() else "cpu"
        self.device = device

        # Visual encoder for search (CLIP)
        self.clip_model = CLIPModel.from_pretrained(
            "openai/clip-vit-base-patch32"
        ).to(device).eval()
        self.clip_proc = CLIPProcessor.from_pretrained(
            "openai/clip-vit-base-patch32"
        )

        # Image captioning (BLIP-2)
        # Note: in production, use a 7B VLM for richer descriptions
        print("Loading BLIP-2 for image analysis...")

        # Speech recognition (Whisper)
        self.whisper = WhisperForConditionalGeneration.from_pretrained(
            "openai/whisper-base"
        ).to(device).eval()
        self.whisper_proc = WhisperProcessor.from_pretrained("openai/whisper-base")

        print("Multimodal pipeline ready")

    def process_product(self, image_path: str, audio_path: str) -> dict:
        """
        Complete pipeline: photo + audio → catalog entry + search embedding.
        """
        # Step 1: Transcribe audio description
        import torchaudio
        waveform, sr = torchaudio.load(audio_path)
        if sr != 16000:
            waveform = torchaudio.transforms.Resample(sr, 16000)(waveform)
        waveform = waveform.mean(dim=0)
        w_inputs = self.whisper_proc(
            waveform.numpy(), sampling_rate=16000, return_tensors="pt"
        ).to(self.device)
        with torch.no_grad():
            w_ids = self.whisper.generate(w_inputs.input_features)
        spoken_description = self.whisper_proc.batch_decode(
            w_ids, skip_special_tokens=True
        )[0]

        # Step 2: Analyze image with CLIP for attribute extraction
        image = Image.open(image_path).convert("RGB")
        attribute_queries = {
            "color":   ["red product", "blue product", "black product",
                          "white product", "green product", "yellow product"],
            "material": ["plastic item", "metal item", "wooden item",
                          "fabric item", "glass item", "leather item"],
            "size":     ["small compact item", "medium-sized item", "large bulky item"],
        }

        detected_attributes = {}
        for attr_name, options in attribute_queries.items():
            inputs = self.clip_proc(text=options, images=image,
                                    return_tensors="pt", padding=True).to(self.device)
            with torch.no_grad():
                logits = self.clip_model(**inputs).logits_per_image.squeeze()
            best_idx = logits.argmax().item()
            detected_attributes[attr_name] = options[best_idx].split()[0]  # first word

        # Step 3: Generate visual search embedding (CLIP image embedding)
        img_inputs = self.clip_proc(images=image, return_tensors="pt").to(self.device)
        with torch.no_grad():
            search_embedding = self.clip_model.get_image_features(**img_inputs)
        search_embedding = (
            search_embedding / search_embedding.norm(dim=-1, keepdim=True)
        ).squeeze().cpu().numpy()

        # Step 4: Assemble catalog entry
        catalogue_entry = {
            "audio_description": spoken_description,
            "visual_attributes": detected_attributes,
            "search_embedding":  search_embedding.tolist(),  # 512-dim CLIP embedding
            "embedding_dim":     len(search_embedding),
        }

        return catalogue_entry


# Illustrative demo output (simulating what the pipeline produces)
demo_output = {
    "audio_description": "This is a compact blue wireless speaker with a fabric cover.",
    "visual_attributes": {
        "color":   "blue",
        "material": "fabric",
        "size":     "small",
    },
    "embedding_dim": 512,
    "search_embedding": "[...512-dimensional CLIP embedding for visual search...]",
}

print("Multimodal catalog entry:")
for key, val in demo_output.items():
    if key != "search_embedding":
        print(f"  {key}: {val}")

8 Foundation Models and the Multimodal Frontier

The term foundation model describes a large model trained on broad, diverse data that can be adapted to many downstream tasks. GPT-4, LLaMA, and CLIP are all foundation models. The trend in 2024–2026 is unification: single models that natively handle multiple modalities without explicit modality-specific pipelines.

Current State-of-the-Art

GPT-4o ("o" for "omni"): accepts and generates text, images, and audio in a unified architecture. Can engage in real-time voice conversation with emotional prosody, describe images, read charts. Gemini 1.5 Pro: 1 million token context window handling text + images + audio + video. Can analyze an entire 1-hour movie or 1,000-page document in a single pass. Claude 3.5: strong vision capabilities, can read and reason about screenshots, diagrams, charts with high accuracy. LLaMA 3.2 Vision: best open-source multimodal LLM, competitive with GPT-4V on many benchmarks.

Video Understanding

Video is the most demanding multimodal challenge: it combines temporal reasoning (what happened before/after?) with spatial reasoning (where in the frame?) and language. Sora (OpenAI, 2024) generates high-quality videos from text descriptions using video diffusion. Gemini 1.5 Pro can watch a 1-hour video and answer questions about specific moments. VideoLLaMA and InternVideo process video frames as a sequence of visual tokens for temporal question answering.

In [11]:
# Example: multimodal reasoning with GPT-4o (API)
from openai import OpenAI
import base64

client = OpenAI()

def multimodal_reason(text_question, image_path=None, audio_path=None):
    """
    Query GPT-4o with text, and optionally image and audio.
    """
    messages = [{"role": "user", "content": []}]

    # Add image if provided
    if image_path:
        with open(image_path, "rb") as f:
            img_b64 = base64.b64encode(f.read()).decode()
        messages[0]["content"].append({
            "type": "image_url",
            "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"},
        })

    # Add audio if provided (GPT-4o audio preview)
    if audio_path:
        with open(audio_path, "rb") as f:
            audio_b64 = base64.b64encode(f.read()).decode()
        messages[0]["content"].append({
            "type": "input_audio",
            "input_audio": {
                "data": audio_b64,
                "format": "mp3",
            }
        })

    # Add text question
    messages[0]["content"].append({"type": "text", "text": text_question})

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        max_tokens=500,
    )
    return response.choices[0].message.content


# Example use cases:
use_cases = [
    "Describe the chart and identify the trend",         # chart image
    "What medication is shown in this prescription?",   # medical image
    "Transcribe and summarize this meeting clip",        # audio
    "What bug causes the error in this screenshot?",     # code/UI screenshot
    "What's the issue with this architectural diagram?", # diagram
]
for case in use_cases:
    print(f"  '{case}'")

Emerging: Audio Generation and Unified Models

In [12]:
# MusicGen: generate music from text description
# pip install audiocraft
from audiocraft.models import MusicGen
from audiocraft.data.audio import audio_write

model = MusicGen.get_pretrained('facebook/musicgen-small')
model.set_generation_params(duration=8)  # 8 seconds

# Generate music from description
descriptions = [
    "upbeat jazz piano with light drumming",
    "ambient electronic music with deep bass",
    "classical violin solo with reverb",
]

wav = model.generate(descriptions)
for i, one_wav in enumerate(wav):
    audio_write(f'music_{i}', one_wav.cpu(), model.sample_rate, strategy="loudness")

print("MusicGen: text → music generation")
print("AudioCraft: broader suite including sound effects")
print("Bark: text → speech with emotion, different voices, non-verbal sounds")
🔑
The Convergence Trend

In 2020, you needed separate models for each modality. In 2025, a single model (GPT-4o, Gemini 1.5) handles text, images, audio, and video in one system. This convergence is driven by the observation that the same Transformer architecture works for all modalities when inputs are tokenized appropriately: text → word tokens, images → patch tokens, audio → spectrogram tokens. The future is likely a single unified model trained on all modalities simultaneously, developing a shared representation of the world — similar to how humans integrate sensory information into a unified understanding.

🌍

Real-World Spotlight: Medical Imaging and Autonomous Vehicles

Medical Multimodal AI: Radiology Report Generation

CheXagent (Stanford 2024) is a VLM specialized for chest X-ray interpretation. It takes a chest X-ray image plus the patient's clinical history text and generates a structured radiology report. The model integrates: (1) visual findings from the X-ray (bilateral infiltrates in lower lobes), (2) clinical context from the patient history (fever, productive cough for 3 days), (3) prior reports if available. Output: "Findings consistent with community-acquired pneumonia. Recommend follow-up chest X-ray in 6 weeks to confirm resolution."

In [13]:
# Conceptual: multimodal medical AI pipeline
def radiology_report_pipeline(xray_image, clinical_history, prior_reports=None):
    """
    Multimodal medical AI for radiology report generation.
    In practice: uses a specialized VLM fine-tuned on radiology data (like CheXagent).
    """
    # 1. Extract visual features from X-ray
    # visual_features = chest_xray_encoder(xray_image)  # specialized ViT

    # 2. Encode clinical history
    # text_features = clinical_bert(clinical_history)

    # 3. Multimodal fusion
    # combined = cross_attention(visual_features, text_features)

    # 4. Generate structured report
    # report = report_generator(combined)

    # Simulated output:
    simulated_report = {
        "findings": "Bilateral airspace opacities in the right lower lobe. "
                    "No pleural effusion. Cardiac silhouette within normal limits.",
        "impression": "Findings consistent with right lower lobe pneumonia.",
        "recommendation": "Clinical correlation with symptoms. "
                          "Consider antibiotics. Recommend follow-up in 6 weeks.",
        "confidence": 0.87,
        "regions_flagged": ["right_lower_lobe"],
    }

    return simulated_report

# Performance metrics:
print("CheXagent agreement with radiologists:")
print("  NLG metrics (BLEU-4): 0.182 vs radiologist reference reports")
print("  Pathology F1 score:   0.73 (detection of 14 conditions)")
print("  Time savings: ~45 min manual → ~5 min review+edit with AI")
print("  Caveat: FDA clearance required for clinical deployment (Class II device)")

Autonomous Vehicles: Multimodal Perception Fusion

Modern self-driving systems fuse multiple sensor modalities: cameras (rich visual detail), LiDAR (precise 3D point clouds), radar (works in all weather), and HD maps (static environment knowledge). Tesla's full-self-driving uses camera-only with transformer-based models that explicitly reason across all 8 cameras simultaneously. Waymo uses camera + LiDAR fusion. The multimodal fusion is not just concatenation — models learn to query different sensors for different aspects: "is this a real object or a reflection?" (needs radar), "how far away is it exactly?" (needs LiDAR), "what class is it?" (needs camera).

🌍
The 1M Token Context Window: A Game Changer

Gemini 1.5 Pro's 1 million token context window changes what's possible with multimodal AI. 1M tokens ≈ 700 PDF pages of text, or 10 hours of audio, or an entire codebase of 1,000 files, or 1 hour of video (at 1 frame/second). This means: ask about something that happened 40 minutes into a long lecture, query across an entire technical manual, understand a complete software project in one context. Previous models needed complex retrieval pipelines (like RAG) to handle long documents — with 1M context, you can just include the whole thing. The bottleneck shifts from context length to inference cost: a 1M-token context costs ~$10–20 per API call.

✍️ Practice Exercises

  1. Use CLIP to build a semantic image search engine over a local collection of 100+ images. Given a text query like "dog playing in water" or "city skyline at night", return the top 5 matching images with similarity scores. Verify that the semantic search finds relevant images even when their filenames don't match the query.
  2. Implement CLIP zero-shot classification on 50 ImageNet validation images. Compare accuracy using: (a) simple template "a photo of a {classname}", (b) ensemble of 7 templates (e.g., "a photo of a {}", "a {} in the wild", "a close-up of a {}", "a rendering of a {}"). Does the ensemble improve accuracy?
  3. Use Whisper to transcribe 3 audio recordings: (a) a clear podcast clip, (b) a recording with background noise, (c) a non-English recording. Compare WER for different model sizes (tiny, base, small). At what point does the quality improvement justify the speed/size cost?
  4. Build a simple image QA system using BLIP-2. For a set of 10 images with known facts (e.g., "this is a photo of the Eiffel Tower"), ask 5 questions each and score the answers. What types of questions does BLIP-2 answer correctly vs incorrectly?
▶ Show Solution (Exercise 1 — CLIP Image Search)
In [14]:
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
from pathlib import Path
import torch, numpy as np

model     = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
model.eval()

# Build index
def build_index(image_dir):
    paths, embeddings = [], []
    for p in Path(image_dir).glob("*.jpg"):
        try:
            img    = Image.open(p).convert("RGB")
            inputs = processor(images=img, return_tensors="pt")
            with torch.no_grad():
                emb = model.get_image_features(**inputs)
            emb = (emb / emb.norm(dim=-1, keepdim=True)).squeeze().numpy()
            paths.append(str(p))
            embeddings.append(emb)
        except Exception:
            pass
    return paths, np.array(embeddings)

# Search
def text_search(query, paths, embeddings, k=5):
    inputs = processor(text=[query], return_tensors="pt", padding=True)
    with torch.no_grad():
        t_emb = model.get_text_features(**inputs)
    t_emb = (t_emb / t_emb.norm(dim=-1, keepdim=True)).squeeze().numpy()
    sims  = embeddings @ t_emb
    top_k = sims.argsort()[-k:][::-1]
    return [(paths[i], float(sims[i])) for i in top_k]

# Usage:
# paths, embs = build_index("./images/")
# results = text_search("dog playing in water", paths, embs)
# for path, score in results:
#     print(f"  {score:.3f}  {path}")

print("CLIP image search: semantic text → image retrieval without any training")

📚 Primary Source for This Lesson

Radford et al. (2021) — "Learning Transferable Visual Models From Natural Language Supervision" (CLIP)
The paper that established joint vision-language embedding spaces via contrastive pretraining, the foundation this lesson's zero-shot classification and image search sections build on.

💬 Confused about how CLIP's contrastive loss actually pulls matching image/text pairs together? Your AI tutor can walk through the training objective with a small worked batch.