Video Moderation for Training Sets
Pre-filtering at the ingest stage, before a single frame reaches training. Reduce raw footage to the subset you can defend training on: every excluded clip carries its reason, every uncertain one goes to a person, and the thresholds are yours.
A cleared training set, filtered at ingest, with an audit trail for every clip that did not make it.
Teams preparing video corpora for model training who have to prove what was excluded and why. The problem shows up at ingest, where the corpus is largest and the cost of getting it wrong compounds through every downstream run.
What it looks like
Frame in, decisions fire, a verdict lands, a reviewer's call feeds back.
face 0.93 · est. 16 to 18 · borderlinereview- face detected 0.93
- grouped across 3 looks
- age estimate: borderline
- route: review
- source-videoYour footage, read from your own object storage. Nothing is copied out.
- brand-mark-refsLogo and mark variants you supply, one file per variant, with a hard-fail or contextual rule each.
- person-refsPublic-figure reference imagery you supply and hold the rights to. Several poses per person.
face 0.93 · est. 16 to 18 · borderlinereview
brand mark · match 0.88 · rule: contextualcleared
staged 0.81 · real 0.19 · margin +0.62cleared
onscreen_text · BREAKING NEWS · staticcleared- segment-verdictsOne row per segment: decision, the reason, and counts of people, minors, unaged and borderline faces.
- review-queueEverything routed to a human, ranked, with the decision chain attached to each item.
- cleared-set-exportThe segments you can train on, as a filterable set ready to hand to a training job.
How the namespace is wired
3 buckets, 8 collections, 2 clean views, 6 retrievers. The diagram generates the manifest below; they cannot drift apart.
Reward signals
How reviewer decisions move the thresholds
Thresholds at ingest drift as the corpus changes. The reviewers working the queue are the ones who see where a threshold is wrong first, so this template routes their decisions back into the model that set it.
A reviewer marks a verdict right or wrong and attaches what they saw. That is an annotation, written with POST /v1/annotations, versioned, and stored in the namespace's own mxp_document_annotations collection.
What a reviewer opens, skips, or acts on is an interaction, written with POST /v1/retrievers/interactions into mxp_retriever_interactions. Nobody fills in a form for these.
mxp_document_annotationsmxp_retriever_interactionsmxp_retriever_evaluationsSystem collections in your namespace, on the same vector store as the rest of the template. They are yours to query.
POST /v1/retrievers/{id}/evaluations/generate-from-interactions turns those signals into an evaluation set, so a threshold change is judged by precision and recall moving on held-out data rather than by whether signals were captured. moderation-search is configured with learned fusion in shadow mode: it learns from the signals and reports what it would have ranked, and you flip it on with a retriever PATCH when the numbers justify it.
One file spins up the namespace. Generated from the diagram above. Also served at /templates/video-moderation.namespace.yaml.
# video-moderation: 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.
version: '1.0'
metadata:
name: video-moderation
description: "Namespace template video-moderation. Generated from the flow diagram on mixpeek.com/templates/video-moderation."
namespaces:
- name: video-moderation
description: "Everything below lives in this namespace."
feature_extractors:
- name: multimodal_extractor
version: v2
- name: image_extractor
version: v1
- name: face_identity_extractor
version: v1
- name: scrolling_text_extractor
version: v1
# 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.
# source-video: s3, continuous, your footage, in your account -> bucket source-video
# brand-marks: manual, one-shot, customer-supplied logo and mark variants -> bucket brand-mark-refs
# person-references: manual, one-shot, customer-supplied public-figure reference imagery -> bucket person-refs
buckets:
- name: source-video
namespace: video-moderation
description: "Fed by source-video (s3, continuous)."
schema:
properties:
content:
type: video
- name: brand-mark-refs
namespace: video-moderation
description: "Fed by brand-marks (manual, one-shot)."
schema:
properties:
content:
type: image
- name: person-refs
namespace: video-moderation
description: "Fed by person-references (manual, one-shot)."
schema:
properties:
content:
type: image
collections:
- name: video-segments
namespace: video-moderation
description: "multimodal_extractor@v2 over bucket source-video. Feeds faces, keyframes, onscreen-text, roll-up-by-segment."
source:
type: bucket
bucket: source-video
feature_extractor:
name: multimodal_extractor
version: v2
input_mappings:
video: content
field_passthrough:
- source_path: file_location
- source_path: segment_id
- source_path: start_time
- source_path: end_time
enabled: true
- name: brand-mark-index
namespace: video-moderation
description: "image_extractor@v1 over bucket brand-mark-refs. Feeds brand-mark-match."
source:
type: bucket
bucket: brand-mark-refs
feature_extractor:
name: image_extractor
version: v1
input_mappings:
image: content
field_passthrough:
- source_path: mark_name
- source_path: rule
enabled: true
- name: person-ref-index
namespace: video-moderation
description: "face_identity_extractor@v1 over bucket person-refs. Feeds public-figure-match."
source:
type: bucket
bucket: person-refs
feature_extractor:
name: face_identity_extractor
version: v1
input_mappings:
image: content
field_passthrough:
- source_path: person_name
- source_path: rights_note
enabled: true
- name: faces
namespace: video-moderation
description: "face_identity_extractor@v1 over collection video-segments. Feeds age-estimation, person-verdicts."
source:
type: collection
collection: video-segments
feature_extractor:
name: face_identity_extractor
version: v1
input_mappings:
video: content
field_passthrough:
- source_path: segment_id
- source_path: bbox
- source_path: detection_score
- source_path: quality_score
enabled: true
- name: keyframes
namespace: video-moderation
description: "image_extractor@v1 over collection video-segments. Feeds violence-context."
source:
type: collection
collection: video-segments
feature_extractor:
name: image_extractor
version: v1
input_mappings:
image: content
field_passthrough:
- source_path: segment_id
- source_path: frame_ts
enabled: true
- name: onscreen-text
namespace: video-moderation
description: "scrolling_text_extractor@v1 over collection video-segments. Feeds roll-up-by-segment."
source:
type: collection
collection: video-segments
feature_extractor:
name: scrolling_text_extractor
version: v1
input_mappings:
video: content
field_passthrough:
- source_path: segment_id
enabled: true
- name: person-verdicts
namespace: video-moderation
description: "Derived from faces, age-estimation. One document per person_id. Writes back age_median, aged, n_looks. Declared as a passthrough collection because the manifest schema has no resource for a retriever-materialised collection."
source:
type: collection
collection: faces
feature_extractor:
name: passthrough_extractor
version: v1
field_passthrough: []
enabled: true
- name: segment-verdicts
namespace: video-moderation
description: "Materialised by retriever roll-up-by-segment (group_by segment_id) over person-verdicts, violence-context, onscreen-text, video-segments. One document per segment_id. Writes back decision, reason, n_person, n_minor, n_unaged, n_borderline. Declared as a passthrough collection because the manifest schema has no resource for a retriever-materialised collection."
source:
type: collection
collection: person-verdicts
feature_extractor:
name: passthrough_extractor
version: v1
field_passthrough: []
enabled: true
# Template-provided extractors. These collections run on age_estimator and context_scorer,
# plugins that ship with this template rather than hosted extractors, so apply would fail on
# a namespace that does not have them yet. Install the extractors, then move this block above.
# - name: age-estimation
# namespace: video-moderation
# description: "age_estimator@v1 over collection faces. Feeds person-verdicts. age_estimator is a template-provided extractor (a plugin that ships with this template, not a hosted one): install it, then apply this block."
# source:
# type: collection
# collection: faces
# feature_extractor:
# name: age_estimator
# version: v1
# input_mappings:
# face: content
# field_passthrough:
# - source_path: person_id
# - source_path: age_median
# - source_path: aged
# - source_path: n_looks
# enabled: true
# - name: violence-context
# namespace: video-moderation
# description: "context_scorer@v1 over collection keyframes. Feeds roll-up-by-segment. context_scorer is a template-provided extractor (a plugin that ships with this template, not a hosted one): install it, then apply this block."
# source:
# type: collection
# collection: keyframes
# feature_extractor:
# name: context_scorer
# version: v1
# input_mappings:
# image: content
# field_passthrough:
# - source_path: segment_id
# - source_path: staged_score
# - source_path: real_score
# - source_path: margin
# enabled: true
retrievers:
- name: brand-mark-match
namespace: video-moderation
description: "Searches brand-mark-index across 1 feature index."
collections:
- brand-mark-index
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://image_extractor@v1/google_siglip_base_v1"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 5
fusion: rrf
final_top_k: 5
tags:
- template:video-moderation
- name: public-figure-match
namespace: video-moderation
description: "Searches person-ref-index across 1 feature index."
collections:
- person-ref-index
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://face_identity_extractor@v1/insightface__arcface"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 5
fusion: rrf
final_top_k: 5
tags:
- template:video-moderation
- name: roll-up-by-segment
namespace: video-moderation
description: "Pipeline retriever over person-verdicts, onscreen-text, video-segments. Groups documents by segment_id. Materialises into segment-verdicts."
collections:
- person-verdicts
- onscreen-text
- video-segments
# - violence-context (add once context_scorer is installed and its block above is applied)
input_schema:
limit:
type: text
required: false
description: "Optional cap on documents grouped per run"
stages:
- stage_name: group
stage_id: group_by
parameters:
group_by_field: segment_id
tags:
- template:video-moderation
- name: review-queue
namespace: video-moderation
description: "Searches segment-verdicts across 1 feature index."
collections:
- segment-verdicts
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@v2/gemini-embedding-2"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 100
fusion: rrf
final_top_k: 100
tags:
- template:video-moderation
- name: moderation-search
namespace: video-moderation
description: "Searches segment-verdicts across 3 feature indexes with learned fusion."
collections:
- segment-verdicts
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
session_id:
type: text
required: false
description: "Session identifier; the learned fusion uses it as personal context"
stages:
- stage_name: search
stage_id: feature_search
parameters:
searches:
- feature_uri: "mixpeek://multimodal_extractor@v2/gemini-embedding-2"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 25
- feature_uri: "mixpeek://multimodal_extractor@v2/multilingual_e5_large_instruct_v1"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 25
- feature_uri: "mixpeek://multimodal_extractor@v2/multilingual_e5_large_instruct_ocr_v1"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 25
fusion: learned
final_top_k: 25
# Shadow first: weights are learned and logged, static fusion is served, until the
# signals justify a rollout. Flip rollout_pct through the retriever update path.
learning_config:
shadow_mode: true
rollout_pct: 0.0
context_features:
- INPUT.session_id
tags:
- template:video-moderation
- name: cleared-set-export
namespace: video-moderation
description: "Searches segment-verdicts across 1 feature index."
collections:
- segment-verdicts
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@v2/gemini-embedding-2"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 1000
fusion: rrf
final_top_k: 1000
tags:
- template:video-moderation