Footage Intelligence at Archive Scale
Millions of scenes you can move through instead of search. Footage, ads and editor assets land in three clean collections, and scenes are clustered one partition at a time so every run stays under the sample-size caps.
- scenes in the reference corpus
- 1.27M
- sources into clean collections
- 5 → 3
- documents per HDBSCAN run
- 100k
- documents per k-means run
- 1M
- click to first answer
- 59.2 s
How these were measured
- 1.27M scenes in the reference corpus: The scale the partitioning recipe was written against, from the blessed clustering runbook.
- 5 → 3 sources into clean collections: Three footage sources merge into one collection; ads and editor assets keep their own.
- 100k documents per HDBSCAN run: The hard cap on the quadratic methods. The pairwise distance matrix costs sample_size squared times 8 bytes, about 80 GB at that ceiling.
- 1M documents per k-means run: The hard cap on the linear methods. Above it, a partition is split again rather than the cap being raised.
- 59.2 s click to first answer: from clicking apply to the first search that answers, timed on production on 6 November 2026 by the harness that runs nightly
What’s inside
- Extractors
- passthroughmultimodal
- Retrievers
- scene-search·feature_searchad-search·feature_searchlogo-on-screen-monitor·feature_search → llm_filter
- Solution
- Media & Entertainment →
See it work
- the take where the camera pushes in on the handsscene-search
scene segments across an archive nobody has watched end to end
- night exteriors with wet streetsscene-search
returns the moment, with production_id carried through
- which cut did we use this shot inad-search
the same footage matched against the delivered ads
Try it on sample footage
This is a public sample library of stock clips from Pexels and NASA. Deploy the template and the same scene search runs over your own footage.
cleared- Ingest
- scene
- route: cleared
How it's wired
Applies 5 buckets, 4 collections and 3 retrievers today
Drag nodes to rearrange. Drag the background to pan, ctrl or cmd plus scroll to zoom. Click a node for its settings.
What it costs
Processing is priced the way a batch is charged: each bucket's minutes, images, pages or tokens times its rate card lines. The batch dry run in Studio gives the binding quote.
Models
- Gemini 2.5 FlashProvider APIGeneration
- Vertex AI multimodalembedding@001Provider APIEmbedding
Collections and their documents
| Field | Type | Required |
|---|---|---|
| document_id | string | set by Mixpeek |
| collection_id | string | set by Mixpeek |
| source_label | string | no |
| filename | string | no |
| production_id | string | no |
| job_id | string | no |
Reads video from content
| Field | Type | Required |
|---|---|---|
| document_id | string | set by Mixpeek |
| collection_id | string | set by Mixpeek |
| production_id | string | no |
| job_id | string | no |
| source_label | string | no |
mixpeek://multimodal_extractor@v1/vertex_multimodal_embeddingfrom Vertex AI multimodalembedding@001
| Field | Type | Required |
|---|---|---|
| document_id | string | set by Mixpeek |
| collection_id | string | set by Mixpeek |
| ad_id | string | yes |
| brand | string | yes |
| source_label | string | no |
mixpeek://multimodal_extractor@v1/vertex_multimodal_embeddingfrom Vertex AI multimodalembedding@001
| Field | Type | Required |
|---|---|---|
| document_id | string | set by Mixpeek |
| collection_id | string | set by Mixpeek |
| asset_type | string | yes |
| source_label | string | no |
Retrievers
- scene-searchfeature_search over footage-scenes; input query
- ad-searchfeature_search over ad-creatives; input query
- logo-on-screen-monitorfeature_search then llm_filter over footage-scenes; input query, document_id
Spend limits
- Daily spend budget in the manifest
- 1 USD a day; new batches pause once it is spent
- Acceptance run ceiling
- 0.5 USD
- Model cost
- Unmeasured for 2 of 2 models
Monthly cost at your volume is in the calculator. Served models start at zero replicas, so an idle namespace runs no model.
Reward signals
Clusters are re-cut as the corpus grows, one partition at a time.
How reviewer decisions move the thresholds
Labels stored with each cluster in the cluster's own collection, where a person can review and rename them. This version does not write labels back onto the scenes; the FAQ says why.
Which clusters get opened and searched from, which is the signal for whether the grouping is useful.
footage-scenesNew footage joins its partition's clusters on the next run of that partition, which re-cuts them when the shape has drifted.
A corpus you navigate by structure rather than by query, with every partition clustered under the limit that makes clustering possible at all.
- Five sources land in three clean collections, so the people building search never touch ingestion.
- Clustering runs one partition at a time, which is what keeps a multi-million-scene corpus inside the sample-size caps.
- A composite run groups the centroids of the partition runs you name into a layer above them, in the same collection and without disturbing what the partition runs wrote.
- Scene search works across the whole archive the entire time, and a filter keeps it inside one partition.
For: Performance-video teams whose archive outgrew search. Once a corpus passes a million scenes, the question stops being 'find me this clip' and becomes 'show me what is in here', and a result list cannot answer that.
- Footage
- Three sources: an active sync, a large static archive, and a frozen legacy load. One shared schema so the merge is homogeneous.
- Ads
- Approved creatives, carrying their own ad and brand identifiers.
- Editor assets
- Graphics, text capsules and generated assets from the edit bay.
- Clean collections
- Raw footage, ads and editor assets, landed by a passthrough extractor so downstream teams build on stable inputs.
- Per-partition clusters
- One clustering execution per partition key, written to the cluster's own collection. Each run's cluster ids start again at cl_0, so a cluster id identifies a group within its run rather than across the corpus.
- A composite roll-up
- The centroids of the partition runs you name, grouped into a layer that sits above them in the same collection.
- Filtered retrievers
- Scene and ad search that can be narrowed to one partition.
One file spins up the namespace. Generated from the workflow above. Also served at /templates/footage-intelligence.namespace.yaml.
# footage-intelligence: one manifest spins up the namespace.
# Platform manifest schema (GET /v1/discovery/schema). Validate with POST /v1/manifest/validate,
# apply with POST /v1/manifest/apply or the Deploy button. Wiring comes from the flow diagram:
# edges are bucket -> collection sources, collection -> retriever scope, retriever -> view.
# Applying a SECOND time, over a namespace this template already created: use
# POST /v1/manifest/apply?mode=create_missing, which creates what is missing and leaves
# what exists alone. The default, create_only, fails the WHOLE apply and rolls it back if
# any resource already exists, so an upgrade looks like a dead end without this. Use
# mode=upsert to also patch resources that exist but have drifted from this file.
version: '1.0'
metadata:
name: footage-intelligence
description: "Namespace template footage-intelligence. Generated from the flow diagram on mixpeek.com/templates/footage-intelligence."
namespaces:
- name: footage-intelligence
description: "Everything below lives in this namespace."
feature_extractors:
- name: passthrough_extractor
version: v1
- name: multimodal_extractor
version: v1
# Daily spend budget, in dollars of what you are charged (1,000 credits a dollar).
# New batches pause once the namespace has spent this much today; queries are not capped.
# Raise it later on the namespace: PATCH /v1/namespaces/<id> {"spend_budget": {"daily_usd": N}}.
budget:
daily_usd: 1
# Data sources. A storage connection carries credentials, so it is created in Studio
# (or POST /v1/organizations/storage-connections) and synced into the bucket named here.
# active-sync: s3, continuous -> bucket footage-active
# static-archive: s3, one-time -> bucket footage-archive
# legacy-load: s3, one-time -> bucket footage-legacy
# ad-library: s3, continuous -> bucket ads
# editor-output: manual, on upload -> bucket editor-assets
buckets:
- name: footage-active
namespace: footage-intelligence
description: "Meant to be fed by active-sync (s3, continuous). No source is connected yet: applying this manifest creates the bucket only. Connect the source to this bucket in Studio (Syncs) to start the feed."
schema:
properties:
content:
type: video
source_label:
type: string
filename:
type: string
production_id:
type: string
job_id:
type: string
- name: footage-archive
namespace: footage-intelligence
description: "Meant to be fed by static-archive (s3, one-time). No source is connected yet: applying this manifest creates the bucket only. Connect the source to this bucket in Studio (Syncs) to start the feed."
schema:
properties:
content:
type: video
source_label:
type: string
filename:
type: string
production_id:
type: string
job_id:
type: string
- name: footage-legacy
namespace: footage-intelligence
description: "Meant to be fed by legacy-load (s3, one-time). No source is connected yet: applying this manifest creates the bucket only. Connect the source to this bucket in Studio (Syncs) to start the feed."
schema:
properties:
content:
type: video
source_label:
type: string
filename:
type: string
production_id:
type: string
job_id:
type: string
- name: ads
namespace: footage-intelligence
description: "Meant to be fed by ad-library (s3, continuous). No source is connected yet: applying this manifest creates the bucket only. Connect the source to this bucket in Studio (Syncs) to start the feed."
schema:
properties:
content:
type: video
ad_id:
type: string
brand:
type: string
source_label:
type: string
- name: editor-assets
namespace: footage-intelligence
description: "Meant to be fed by editor-output (manual, on upload): upload files here or write them through the API. Nothing arrives until you do."
schema:
properties:
content:
type: video
asset_type:
type: string
source_label:
type: string
collections:
- name: raw-footage
namespace: footage-intelligence
description: "passthrough_extractor@v1 over bucket footage-active (the manifest wires one source bucket; footage-archive, footage-legacy are added after apply)."
source:
type: bucket
bucket: footage-active
feature_extractor:
name: passthrough_extractor
version: v1
field_passthrough:
- source_path: source_label
- source_path: filename
- source_path: production_id
- source_path: job_id
enabled: true
- name: footage-scenes
namespace: footage-intelligence
description: "multimodal_extractor@v1 over bucket footage-active (the manifest wires one source bucket; footage-archive, footage-legacy are added after apply). Feeds logo-on-screen-monitor, logo-on-screen, scene-search, scene-themes."
source:
type: bucket
bucket: footage-active
feature_extractor:
name: multimodal_extractor
version: v1
input_mappings:
video: content
field_passthrough:
- source_path: production_id
- source_path: job_id
- source_path: source_label
alert_applications:
- alert: logo-on-screen
input_mappings:
- input_key: query
source: {source_type: constant, value: "a brand logo on screen"}
- input_key: document_id
source: {source_type: document_field, path: _internal.document_id}
enabled: true
- name: ad-creatives
namespace: footage-intelligence
description: "multimodal_extractor@v1 over bucket ads. Feeds ad-search."
source:
type: bucket
bucket: ads
feature_extractor:
name: multimodal_extractor
version: v1
field_passthrough:
- source_path: ad_id
required: true
- source_path: brand
required: true
- source_path: source_label
enabled: true
- name: editor-assets
namespace: footage-intelligence
description: "passthrough_extractor@v1 over bucket editor-assets."
source:
type: bucket
bucket: editor-assets
feature_extractor:
name: passthrough_extractor
version: v1
field_passthrough:
- source_path: asset_type
required: true
- source_path: source_label
enabled: true
retrievers:
- name: scene-search
namespace: footage-intelligence
description: "Searches footage-scenes across 1 feature index."
collections:
- footage-scenes
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
stages:
- stage_name: search
stage_id: feature_search
parameters:
searches:
- feature_uri: "mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 50
fusion: rrf
final_top_k: 50
tags:
- template:footage-intelligence
- name: ad-search
namespace: footage-intelligence
description: "Searches ad-creatives across 1 feature index."
collections:
- ad-creatives
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
stages:
- stage_name: search
stage_id: feature_search
parameters:
searches:
- feature_uri: "mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 50
fusion: rrf
final_top_k: 50
tags:
- template:footage-intelligence
- name: logo-on-screen-monitor
namespace: footage-intelligence
description: "The alert's check: does the scene just written show a brand logo on screen. The search is scoped to that one scene, and an llm_filter watches it and keeps it only when a logo is visible. Materialises into logo-on-screen."
collections:
- footage-scenes
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
document_id:
type: text
required: true
description: "The written document this run is scoped to, supplied by the alert attachment"
stages:
- stage_name: search
stage_id: feature_search
pre_filters:
AND:
- field: _internal.document_id
operator: eq
value: "{{INPUT.document_id}}"
parameters:
searches:
- feature_uri: "mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 5
fusion: rrf
final_top_k: 5
- stage_name: judge
stage_id: llm_filter
parameters:
criteria: "Keep this footage scene only if it shows {{INPUT.query}}: a company logo or brand name mark clearly visible in the frame. Discard it otherwise."
include_media: true
tags:
- template:footage-intelligence
# Composite cluster run(s) are not emitted below, because the manifest schema cannot express them:
# cross-partition-themes: rolls up the centroids of scene-themes
# The manifest cluster spec requires source_collections to have at least one
# item, and a composite run has no source COLLECTION: it reads the centroids of prior
# EXECUTIONS. Naming it here fails validation for the entire manifest. Run it once the
# partitioned runs have completed: POST /v1/clusters/{id}/execute on the partitioned cluster
# with mode composite and source_execution_ids naming at least two of those runs.
clusters:
- name: scene-themes
namespace: footage-intelligence
description: "Groups footage-scenes by embedding similarity, one run per production_id."
source_collections:
- footage-scenes
cluster_type: vector
# Partitioned on production_id: execute once per value, passing
# filters: {AND: [{field: production_id, operator: eq, value: <one production_id>}]}
# with each execution. The manifest cannot hold that value.
# kmeans needs at least 24 documents in a production_id, since it has to
# put each one in a group. Raise or lower n_clusters to the number of groups you
# want to browse per production_id.
vector_config:
feature_uris:
- mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding
clustering_method: kmeans
algorithm_params:
n_clusters: 24
llm_labeling:
enabled: true
provider: google
model_name: gemini-2.5-flash
alerts:
- name: logo-on-screen
namespace: footage-intelligence
description: "Emails the org notifications address (Settings -> Notifications; unset means opted out) when a newly indexed footage scene shows a brand logo on screen."
source: retriever
retriever: logo-on-screen-monitor
trigger_on: results
notification_config:
channels:
- channel_type: email
include_matches: true
include_scores: true
enabled: false
# Plan 11.14: two public-domain films from the starter sample set, ingested by
# one bounded batch after apply, and the use case they must satisfy.
seed:
- bucket: footage-active
max_objects: 2
objects:
- blobs: {content: samples/starter/videos/norelco-speed.mp4}
- blobs: {content: samples/starter/videos/mexican-hat-dance.mp4}
acceptance:
max_cost_usd: 0.5
steps:
- name: footage-listed
kind: documents
collection: raw-footage
expect: {min_documents: 2}
- name: footage-scenes-indexed
kind: documents
collection: footage-scenes
expect: {min_documents: 2}
- name: scene-search-answers
kind: query
retriever: scene-search
inputs: {query: people dancing}
expect: {min_results: 1}Read from the manifest, so a part it ships commented out never shows as ready.
What each part needs
- raw-footage
- Deploys and processes documents as they arrive.
- footage-scenes
- Deploys and processes documents as they arrive.
- ad-creatives
- Deploys and processes documents as they arrive.
- editor-assets
- Deploys and processes documents as they arrive.
- scene-search
- Deploys and answers queries once its collections hold documents.
- ad-search
- Deploys and answers queries once its collections hold documents.
- scene-themes
- Deploys. Run it once per production_id by passing that filter each time you execute it; the manifest cannot hold the value.
- logo-on-screen-monitor
- Deploys and answers queries once its collections hold documents.
- active-sync
- Connect this s3 source in Studio and sync it into its bucket. The manifest creates the bucket and leaves the connection to you.
- static-archive
- Connect this s3 source in Studio and sync it into its bucket. The manifest creates the bucket and leaves the connection to you.
- legacy-load
- Connect this s3 source in Studio and sync it into its bucket. The manifest creates the bucket and leaves the connection to you.
- ad-library
- Connect this s3 source in Studio and sync it into its bucket. The manifest creates the bucket and leaves the connection to you.
- editor-output
- You upload these files yourself. There is no connection to sync.
- cross-partition-themes
- The manifest cannot describe a cluster of clusters. Once the partitioned runs have completed, run it with POST /v1/clusters/{id}/execute in composite mode, naming at least two of those runs in source_execution_ids.
| Component | Role | Source | Today |
|---|---|---|---|
| passthrough_extractor v1 | Lands objects in a clean collection with their identifiers intact, so downstream teams build on stable inputs. | Built in | Deploys ready |
| multimodal_extractor v1 | Embeds scenes and creatives so they can be searched and clustered by meaning. | Built in | Deploys ready |
| Partitioned clustering | One execution per partition key, with the algorithm chosen against the sample-size caps. | Built in | |
| Composite clustering | Clusters the centroids of prior executions, read from each execution's run record. | Built in | |
| Your footage | Three sources with one shared schema. The template creates the buckets; you supply the objects. | You supply |
The managed path. Nothing to run.
For archives that cannot leave your account. The footage stays in your object storage either way.
- Permissions
- Read on the source buckets. The template creates buckets, collections, retrievers and clusters inside one namespace and touches nothing outside it.
- Footprint
- Storage is dominated by the footage you already hold. The clustering cost scales with the partition, not the corpus, which is the point of partitioning.
A partition small enough to cluster in one run, so a change in grouping is visible without waiting on the full corpus.
| Category | Rule |
|---|---|
| Partition size | Every partition stays under the cap for its algorithm: 100,000 for HDBSCAN, 1,000,000 for k-means. Over that, split the partition rather than raising the cap. |
| Composite inputs | A composite run names only executions that completed with centroids. One missing execution makes the roll-up a different shape without failing. |
| Run completion | Every partition run completes with more than zero clusters. kmeans needs at least n_clusters documents in the partition, and it groups whatever it is given, so a partition that is too small fails rather than returning nothing. Density methods behave the other way: a test production of 68 scenes at HDBSCAN min_cluster_size 50 completed with every scene classified as noise. |
| Counting | Read the cluster documents themselves rather than the output collection's document_count, which trails one run behind on this path: 43 listed and 43 counted after the first run, 76 listed and 43 counted after the second. |
Clustering is a batch job per partition rather than a per-query cost, so search latency is unaffected by corpus size.
Why partition at all, instead of clustering everything at once?+
Because the methods have hard ceilings and they exist for memory reasons. HDBSCAN and the other quadratic methods cap at 100,000 documents, since the pairwise distance matrix costs sample_size squared times 8 bytes, about 80 GB at that ceiling. K-means caps at a million. A multi-million-scene corpus is clustered one partition at a time or not at all.
What does a composite run actually do?+
It clusters the centroids of the partition runs you name, which it reads from each run's record, and writes its groups into the same collection alongside them. Measured on prod: two partition runs left 76 documents, 8 of them centroids, and the composite over both took it to 88 with 12 centroids. Name only runs that completed with clusters, since a run that found none has no centroids to contribute.
How does an agent use this?+
Through the same retrievers a person uses. scene-search finds moments across the archive and takes a production_id filter to stay inside one partition, and each cluster run's results can be read back through the clusters API.
Do I have to re-cluster to add new footage?+
Yes, in this version. New scenes are searchable as soon as they are extracted, and they join a partition's clusters the next time that partition runs. Run it again when enough new footage has landed to change the grouping, which is a judgement rather than a schedule.
Why doesn't this template write cluster labels back onto the scenes?+
A cluster that enriches its source collection currently fails its first run on a collection that already holds documents, because a safety check reads the existing scene count as evidence of an earlier clustering run. Until that is fixed, scene-themes writes its clusters and labels to a collection of its own, and the scenes keep their production_id, job_id and search fields unchanged.
Can I filter search results by a cluster id?+
Within one run, yes. Cluster ids start again at cl_0 for every execution, so after two partition runs the same four ids hold documents from both productions. Filter on production_id to stay inside a partition, and treat a cluster id as a handle on a group within its run rather than a stable label across the corpus.
What is the partition key here?+
The template uses production_id because it is a natural grouping in the reference pipeline. The right key is whatever splits your corpus into pieces that fit under the cap and mean something to the people reading the clusters. Check the field is indexed on your documents before relying on it; a filter path that matches nothing returns an empty result rather than an error.