Skip to main content

Cinematlas

Ask a question. Get the second in the video that answers it.

Cinematlas turns videos into searchable moments in MongoDB Atlas. It listens to what's said and looks at what's shown, and for each question it decides which to trust.

pip install "cinematlas[whisper]"
cinematlas doctor
cinematlas ingest "www.b.com/keynote.mp4"
cinematlas search "when do they announce pricing?"
 1. vid_3f2a…#12 @    7:11  Pricing starts at ten dollars a seat.
    https://www.b.com/keynote.mp4#t=431

The story behind it: blog.md · the numbers: bench/RESULTS.md · the limits: honest.md · running it in production: considerations.md


How it works

 video (URL, YouTube, upload)
   │
   ├─ scenes ────── PySceneDetect cuts, ≤30s each
   ├─ said ──────── faster-whisper → timestamped sentences, aligned to scenes word by word
   └─ shown ─────── a keyframe per scene
                      │
   Voyage AI ─────── keyframe vector · transcript vector · joint keyframe+speech vector
                      │
   MongoDB Atlas ─── one document per scene: vectors, sentences, deep links

 search(question)
   ├─ $rankFusion over keyframe, scene, transcript and full-text retrieval   (one query)
   ├─ $rerank over candidate sentences                                        (finds the moment)
   └─ route: is this about what was said or what was shown?                   (reranker confidence)

Questions about video come in two kinds. "How many medals has his beer won?" is about what was said; "the one with the girl on hay bales" is about what was shown. Each needs a different specialist:

Retrieval Said Shown
transcript vectors + sentence reranker 0.90 0.43
joint keyframe + speech vectors 0.73 0.93
blending both with fixed weights 0.80 0.50
routing by reranker confidence (default) 0.83 0.80

Hit@1 on a 60-question benchmark (details and caveats).

The reranker scores how well any transcript sentence answers the question. High means "about what was said", low means "about what was shown". search() turns that into a speech confidence and weights the two specialists accordingly.


Quickstart

export MONGODB_URI="mongodb+srv://…"     # MDB_URI also works
export VOYAGE_API_KEY="pa-…"
from cinematlas import Cinematlas

engine = Cinematlas()
engine.ensure_indexes()                                  # once; idempotent

engine.ingest("https://www.youtube.com/watch?v=5NhYvbMdbBU")
engine.ingest("www.b.com/talk.mp4")                      # any file URL, scheme optional
engine.ingest("lecture.mov")                             # or a path, bytes, file object, web upload

results = engine.search("how loud is a sonic boom?")
print(results)                  # a readable table (markdown in Jupyter)
results.top.link                # 'https://…#t=34': the second it's said
results.top.text                # 'Sonic booms are about 110 decibels.'
results.top.explain()           # why it ranked: rank per source, relevance, score
results.speech_confidence       # 1.0: routed as a question about what was said

Results are plain dicts underneath (json.dumps works) with attribute shortcuts on top.

Build your own answer layer

Cinematlas doesn't choose an LLM for you. Results become numbered, citable context in one call:

hits = engine.search("How loud is a sonic boom, and why is it banned over land?")
answer = my_llm(f"Answer from these excerpts, citing [n]:\n\n{hits.to_context()}")
sources = {f"[{i}]": h.link for i, h in enumerate(hits, 1)}

From the shell: cinematlas search "…" --format context | your-llm-cli.


Source Matches Group
scene joint keyframe + speech vectors shown
visual keyframe vectors shown
transcript speech vectors (Atlas autoEmbed, or client-side voyage-4) said
text Atlas Search BM25 on transcripts (names, numbers, IDs) said
rerank Voyage reranker over candidate sentences (picks the moment) said

Every hit carries moment (the best sentence, {start, end, text}), moment_link (YouTube ?t=431s, direct files #t=431), ranks (position in each source), relevance, score, plus the scene's fields (video_id, scene_id, timestamp_start/end, transcript, segments, video_url, …). results.speech_confidence and results.weights show how the question was routed.

Need Call
Best overall (default) search(q)
Fast, visual-first (~300 ms, scenes not moments) search(q, sources=("scene",), rerank=False)
Speech only search(q, sources=("transcript", "text"))
Your own blend search(q, weights={"scene": 2, "transcript": 1, "rerank": 1})
One source search_transcript · search_text · search_visual_vector · search_scene_vector

All of them accept video_id= and return SearchResults.


Ingest

engine.ingest(source) accepts a URL (YouTube or any file link, scheme optional), a local path, bytes, a binary file object, or a FastAPI UploadFile / Flask FileStorage. It returns an IngestResult (video id, scene counts, per-stage timings); pass progress=lambda stage, info: … to follow along.

@app.post("/videos")                     # FastAPI
def upload(file: UploadFile):
    return {"scenes": engine.ingest(file).scenes}
  • Uploads stream to disk in 1 MiB chunks. Without a video_id, the ID is a content hash, so a re-upload replaces instead of duplicating.
  • Remote URLs are treated as untrusted: private and metadata addresses are refused (allow_private_urls=True for trusted hosts), only http(s) is allowed, downloads are capped at max_download_mb, credentials in signed URLs are stripped before storage, and deep links use #t= fragments so signatures stay valid.
  • Re-ingesting a video replaces it without a gap: the new version is written before the old one is removed.

CLI

cinematlas doctor                               # health check with fixes; --json, exit 1 on failure
cinematlas setup [--update]                     # create indexes; --update upgrades outdated ones in place
cinematlas ingest <url|path|->                  # progress on stderr, IngestResult JSON on stdout
cinematlas search "<question>" [-k 5]           # table in a terminal, JSON lines when piped
    [--by hybrid|transcript|visual|text] [--format table|json|context] [--video-id ID]

Global options: --uri, --db, --collection, --transcript-mode, -v.


Doctor

$ cinematlas doctor
 ✓ MongoDB                        connected · server 9.0.2 · cinematlas.scenes
 ✓ Transcript search              Atlas autoEmbed (voyage-4, embedded server-side)
 ! Index cinematlas_vector_index  outdated: visual_embedding.quantization: None -> 'scalar'
                                  → engine.ensure_indexes(update=True)  ·  cinematlas setup --update  (in place, no downtime)
 ✓ $rankFusion                    hybrid search runs as one native query
 ✓ $rerank                        sentence reranking runs inside Atlas (rerank-2.5)
 ✓ Voyage AI                      API key works (voyage-multimodal-3.5, voyage-4)
 ✓ Data                           65 scenes across 6 videos
 ✓ ffmpeg                         /opt/homebrew/bin/ffmpeg
 ✓ Speech-to-text                 faster-whisper (small), runs locally

Also available as engine.doctor() (.ok, .problems, .to_dict()). When a native stage isn't available, Cinematlas says why once per process (disabled, too old, or not offered on this deployment) and keeps working on an equivalent path.


MongoDB Atlas features

Feature Use When unavailable
$rankFusion (8.0+) All retrieval in one query, with per-source ranks Client-side fusion, same ranking
$rerank (8.3+, Atlas; enable in project settings) Sentence reranking in the database Voyage rerank API, same model
Automated Embedding (Preview) Atlas embeds transcripts and queries with voyage-4 Client-side voyage-4 vectors
Atlas Search BM25 on transcripts —
Scalar quantization ~75% less vector-index memory quantization=None
BSON float32 vectors 3.2× smaller stored vectors bson_vectors=False
updateSearchIndex Upgrades outdated indexes in place —

Configuration

Parameter Default
voyage_model voyage-multimodal-3.5 keyframe and scene vectors
text_model voyage-4 transcripts (-lite / -large share the space)
rerank_model rerank-2.5 None disables reranking and routing
transcript_mode auto autoembed, client, or detect
whisper_model small local faster-whisper model
scene_embeddings True joint keyframe + speech vectors
max_scene_seconds 30 None to disable
native_fusion / native_rerank auto force the Atlas-native path on or off
bson_vectors True BSON float32 storage
allow_private_urls False SSRF guard
max_download_mb 2048 download cap
progress None default progress(stage, info) callback

Every external client can be injected (mongo_client=, voyage_client=, s3_client=, openai_client=).


Development

uv sync
uv run pytest -m "not integration and not media"   # unit, offline (~9s)
uv run pytest -m media                              # real ffmpeg / Whisper / downloads, on a committed fixture
uv run pytest -m integration                        # live Atlas + Docker Atlas Local (reads .env)
uv run python bench/ingest.py && uv run python bench/run.py   # the benchmark

CI runs the unit and media tiers. No test contacts YouTube: end-to-end runs use an 847 KiB public-domain NASA clip with known cuts and known speech (build script), served over loopback HTTP. The integration tier runs locally from .env.

MIT license. Test and benchmark media: NASA, public domain.

Release files for cinematlas 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cinematlas 0.4.0
File Size Uploaded
cinematlas-0.4.0.tar.gz 929.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cinematlas 0.4.0
File Interpreter ABI Platform
cinematlas-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 972.5 kB

Release files / cinematlas-0.4.0.tar.gz

Download URL cinematlas-0.4.0.tar.gz
Size 929.7 kB
Tags Source
SHA-256 checksum
How to use checksums
4eec20f8682bdd54e0d3ddfdc8b8361449a8e1b6273a5df0210b5435ed6a8299
BLAKE2b-256 checksum
How to use checksums
93513cc4a3176b3e5ae753cf380966de3b46e9edc73c5e4df254fb2611cf6fd6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / cinematlas-0.4.0-py3-none-any.whl

Download URL cinematlas-0.4.0-py3-none-any.whl
Size 42.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9510005765e36e15aef988665bb5dabde323532c93ebc565b160b6df29af2c72
BLAKE2b-256 checksum
How to use checksums
51567f86fc5e05c73897be2e665b01d8a8076e438eec354aea88094d8b053b05
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

This release

0.4.0 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page