Run on Cloud · serving

Video search

Ingest video, extract frames and speech, then search visually, by transcript, or by detected objects.

Scaffold locally

uvx pixeltable-new --template video-search my-video-search

Same starter-kit files Cloud uses. Local UI (static HTML in some templates) is for uvx, not for Cloud. Cloud deploys schema + insert routes via pxt serve.

Secrets

Set these on the database before calling model-backed routes: OPENAI_API_KEY

Cloud routes

  • insert/insertvideointel/videos

schema.py

"""Video Intelligence Pipeline — declarative video analysis with Pixeltable.

Ingest video → extract frames + audio → CLIP embeddings, Whisper transcription,
DETR object detection → multi-modal search (visual, spoken, objects).

    python schema.py        # create tables, views, indexes
    pxt serve videointel    # start the API (reads routes from pyproject.toml)
"""

import os

import functions
import pixeltable as pxt
from pixeltable.functions import image as pxt_image
from pixeltable.functions.audio import audio_splitter
from pixeltable.functions.huggingface import clip, detr_for_object_detection, sentence_transformer
from pixeltable.functions.string import string_splitter
from pixeltable.functions.uuid import uuid7
from pixeltable.functions.video import extract_audio, frame_iterator
from pixeltable.functions.whisper import transcribe as whisper_transcribe

pxt.create_dir("videointel", if_exists="ignore")

clip_embed = clip.using(model_id="openai/clip-vit-base-patch32")
text_embed = sentence_transformer.using(model_id="all-MiniLM-L6-v2")

# ── Videos table ─────────────────────────────────────────────────────────────

videos = pxt.create_table(
    "videointel.videos",
    {"video": pxt.Video, "title": pxt.String, "uuid": uuid7(), "timestamp": pxt.Timestamp},
    primary_key=["uuid"],
    if_exists="ignore",
)

# ── Frame extraction ─────────────────────────────────────────────────────────

frames = pxt.create_view(
    "videointel.frames",
    videos,
    iterator=frame_iterator(video=videos.video, fps=1.0),
    if_exists="ignore",
)

frames.add_computed_column(
    thumbnail=pxt_image.b64_encode(pxt_image.thumbnail(frames.frame, size=(320, 320))),
    if_exists="ignore",
)

frames.add_embedding_index(column="frame", idx_name="frames_clip_idx", embedding=clip_embed, if_exists="ignore")

# ── Object detection (DETR, via timm — a base dependency) ───────────────────
# Defined unconditionally because the `/search/objects` route and the
# `search_objects` query (compiled eagerly at decoration time) both require the
# `detections` column to exist.
frames.add_computed_column(
    detections=detr_for_object_detection(frames.frame, model_id="facebook/detr-resnet-50", threshold=0.7),
    if_exists="ignore",
)

# ── Audio extraction + transcription ─────────────────────────────────────────

videos.add_computed_column(audio=extract_audio(videos.video, format="mp3"), if_exists="ignore")

audio_chunks = pxt.create_view(
    "videointel.audio_chunks",
    videos,
    iterator=audio_splitter(audio=videos.audio, duration=30.0),
    if_exists="ignore",
)

audio_chunks.add_computed_column(
    transcription=whisper_transcribe(audio_chunks.audio_segment, model="base.en"),
    if_exists="ignore",
)

# ── Transcript sentences + text search ───────────────────────────────────────

transcript_sentences = pxt.create_view(
    "videointel.transcript_sentences",
    audio_chunks.where(audio_chunks.transcription != None),  # noqa: E711
    iterator=string_splitter(text=audio_chunks.transcription.text, separators="sentence"),
    if_exists="ignore",
)

transcript_sentences.add_embedding_index(
    column="text", idx_name="transcript_text_idx", string_embed=text_embed, if_exists="ignore"
)

# ── Scene descriptions (optional — requires OPENAI_API_KEY) ─────────────────

if os.getenv("OPENAI_API_KEY"):
    try:
        from pixeltable.functions.openai import chat_completions

        frames.add_computed_column(
            scene_description=chat_completions(
                messages=[
                    {
                        "role": "user",
                        "content": [
                            {"type": "image_url", "image_url": {"url": frames.frame}},
                            {"type": "text", "text": "Describe this video frame in one sentence."},
                        ],
                    }
                ],
                model="gpt-4o-mini",
            )
            .choices[0]
            .message.content,
            if_exists="ignore",
        )
    except Exception as exc:
        print(f"Skipping LLM scene descriptions: {exc}")

# ── Query functions ──────────────────────────────────────────────────────────


@pxt.query
def search_visual(query_text: str, limit: int = 20):
    """CLIP similarity search on video frames."""
    sim = frames.frame.similarity(string=query_text)
    return (
        frames.where(sim > 0.2)
        .order_by(sim, asc=False)
        .select(
            frames.thumbnail,
            timestamp=frames.frame_attrs.time,
            source_video=frames.video,
            score=sim,
        )
        .limit(limit)
    )


@pxt.query
def search_spoken(query_text: str, limit: int = 20):
    """Semantic search over transcribed speech."""
    sim = transcript_sentences.text.similarity(string=query_text)
    return (
        transcript_sentences.where(sim > 0.3)
        .order_by(sim, asc=False)
        .select(
            transcript_sentences.text,
            source_video=transcript_sentences.video,
            segment_start=transcript_sentences.segment_start,
            score=sim,
        )
        .limit(limit)
    )


@pxt.query
def search_objects(label: str, limit: int = 50):
    """Filter frames containing a specific detected object label."""
    return (
        frames.where(functions.has_label(frames.detections.label_text, label))
        .select(
            frames.thumbnail,
            timestamp=frames.frame_attrs.time,
            source_video=frames.video,
            labels=frames.detections.label_text,
            scores=frames.detections.scores,
        )
        .limit(limit)
    )


@pxt.query
def search_all(query_text: str, limit: int = 10):
    """Visual similarity search across all video frames (primary modality)."""
    sim = frames.frame.similarity(string=query_text)
    return (
        frames.where(sim > 0.2)
        .order_by(sim, asc=False)
        .select(
            frames.thumbnail,
            timestamp=frames.frame_attrs.time,
            source_video=frames.video,
            score=sim,
        )
        .limit(limit)
    )


if __name__ == "__main__":
    print("Schema initialized. Run: pxt serve videointel")

README

Video Intelligence Pipeline

Ingest video, automatically extract frames, transcribe audio, detect objects, and search across everything. Your own Twelve Labs, self-hosted.

What it replaces: Twelve Labs, Valossa, Ambient.ai ($10K–100K+/yr)

What Pixeltable does declaratively: the pipeline that would normally require stitching together ffmpeg + Whisper + DETR + CLIP + a vector DB + a search API — defined as tables, views, and computed columns.

Architecture

                              ┌─────────────────────┐
                              │   Video (ingested)   │
                              └──────────┬──────────┘
                         ┌───────────────┼───────────────┐
                         ▼               ▼               ▼
                ┌────────────────┐ ┌───────────┐ ┌──────────────┐
                │  Frame Extract │ │   Audio   │ │   Metadata   │
                │  (1 FPS view)  │ │ Extract   │ │   (title,    │
                │                │ │           │ │    uuid)     │
                └───┬────┬───┬──┘ └─────┬─────┘ └──────────────┘
                    │    │   │          │
                    ▼    ▼   ▼          ▼
               ┌────┐ ┌────┐ ┌────┐ ┌────────────┐
               │CLIP│ │DETR│ │LLM │ │Audio Split │
               │Emb.│ │Det.│ │Desc│ │ (30s chunks)│
               └──┬─┘ └──┬─┘ └──┬─┘ └─────┬──────┘
                  │       │      │          ▼
                  │       │      │   ┌─────────────┐
                  │       │      │   │   Whisper    │
                  │       │      │   │ Transcribe   │
                  │       │      │   └──────┬──────┘
                  │       │      │          ▼
                  │       │      │   ┌─────────────┐
                  │       │      │   │  Sentence    │
                  │       │      │   │  Splitter    │
                  │       │      │   └──────┬──────┘
                  │       │      │          ▼
                  ▼       ▼      ▼          ▼
            ┌──────────────────────────────────────┐
            │           Search API                 │
            │  /search/visual  → CLIP similarity   │
            │  /search/spoken  → text similarity   │
            │  /search/objects → label filter       │
            │  /search         → visual (default)   │
            └──────────────────────────────────────┘

Quickstart

1. Install

uv sync
# For LLM scene descriptions: uv sync --extra openai

2. Initialize & serve

uv run python schema.py           # create tables, views, indexes (idempotent)
uv run pxt serve videointel       # http://localhost:8000/docs

The server starts at http://localhost:8000. Upload a video (background job — poll until done):

RESP=$(curl -s -X POST http://localhost:8000/api/ingest \
  -F "video=@lecture.mp4" \
  -F "title=ML Lecture 1")
JOB_ID=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
curl -s "http://localhost:8000/api/jobs/$JOB_ID"
# Repeat until status is "done", then search.

Search responses return a score field (CLIP similarity), plus thumbnail (base64) — not raw image bytes.

Search across all modalities:

# Visual search — "what does X look like?"
curl -X POST http://localhost:8000/api/search/visual \
  -H "Content-Type: application/json" \
  -d '{"query_text": "person writing on whiteboard"}'

# Spoken content search — "what was said about X?"
curl -X POST http://localhost:8000/api/search/spoken \
  -H "Content-Type: application/json" \
  -d '{"query_text": "gradient descent optimization"}'

# Object detection search — "where does X appear?"
curl -X POST http://localhost:8000/api/search/objects \
  -H "Content-Type: application/json" \
  -d '{"label": "person"}'

What happens on insert

When you upload a video, Pixeltable automatically runs the full pipeline:

  1. Frame extractionframe_iterator extracts frames at 1 FPS
  2. CLIP embeddings — each frame gets a visual embedding for semantic image search
  3. Thumbnails — base64-encoded 320x320 thumbnails for API responses
  4. Object detection — DETR identifies objects in each frame
  5. Audio extractionextract_audio pulls the audio track
  6. Audio chunkingaudio_splitter creates 30-second segments
  7. Transcription — Whisper transcribes each audio chunk locally
  8. Sentence splitting — transcripts are split into sentences
  9. Text embeddings — sentence-transformer embeddings for spoken content search
  10. Scene descriptions — GPT-4o-mini describes keyframes (optional, needs OPENAI_API_KEY)

All of this is defined declaratively in schema.py. No orchestration code, no DAG, no glue.

Configuration

Environment Variable Effect
OPENAI_API_KEY Enables LLM scene descriptions on frames
PIXELTABLE_HOME Custom data directory (default ~/.pixeltable)

Project Structure

video-search/
├── schema.py          Declarative pipeline: tables, views, indexes, queries
├── functions.py       UDF: has_label object-detection filter
├── pyproject.toml     Dependencies + pxt serve route config
└── README.md          This file