The Short Answer
Your transcript only has what was SAID. Words that appear on screen and are never spoken (a title card, a lower third, a disclaimer, a price on a product shot, a scrolling ticker) never enter it, so no amount of searching the transcript will find them. They are a separate signal and they need their own extraction pass.
On Mixpeek that pass is
scrolling_text_extractor, which reads every overlay in a
video, including text that scrolls. If you also want the spoken words, that is
multimodal_extractor, which produces transcription, OCR and embeddings per
segment. Running both against the same bucket gives you one index that answers
"where does anyone say this" and another that answers "where does this appear on
screen".Why the transcript never had those words
A transcript comes from speech recognition, and speech recognition listens. A frame that shows "OFFER ENDS SUNDAY" in 90 point type produces no audio, so the transcript for that second is empty or holds whatever the voiceover happened to be saying over the top.
This is easy to miss because the failure is quiet. The search returns results, the results are relevant to the words that WERE spoken, and nothing anywhere reports that a whole class of text was never indexed. The first sign is usually a person who knows the clip exists, searches the exact phrase they can see with their own eyes, and gets nothing back.
The two kinds, and why the second one is harder
Static overlays sit still for a while: title cards, captions, disclaimers, calls to action, chyrons. Any frame that contains them contains the whole string, so sampling frames and reading them is enough.
Scrolling text never appears in full in any single frame. Tickers, banners, credits and terms-and-conditions crawls move across or up the screen, so a frame sampler gets fragments that overlap unpredictably and a naive OCR pass returns a soup of partial words.
scrolling_text_extractor handles them as two passes for exactly that reason. The
static pass samples visually distinct keyframes, stitches them into one grid
image, and makes a single VLM OCR call over the grid. The scrolling pass splits
frames into strips, phase-correlates consecutive frames to detect per-strip pixel
shift, merges strips with a consistent shift into bands, panoramic-stitches each
band into one wide or tall image, and OCRs that. It then deduplicates repeated
marquee loops and any overlap between the static and scrolling results.Indexing it
The shape is the ordinary Mixpeek flow. A bucket holds the source videos, a collection runs an extractor over them, and a batch does the work.
# 1. A bucket for the videos
POST /v1/buckets
{"bucket_name": "ad-creative",
"bucket_schema": {"properties": {"video": {"type": "video"}}}}
# 2. The videos themselves
POST /v1/buckets/{bucket_id}/objects
# 3. A collection that reads on-screen text
POST /v1/collections
{"collection_name": "onscreen-text",
"source": {"bucket_id": "{bucket_id}"},
"feature_extractors": [{"name": "scrolling_text_extractor", "version": "v1"}]}
# 4. Run it
POST /v1/buckets/{bucket_id}/batches
# 5. Read the results
GET /v1/collections/{collection_id}/documentsWhat comes back
One document per source video, carrying every string found:
| Field | What it holds |
onscreen_text | Everything found, static and scrolling, pipe separated |
static_text | Overlays only |
scrolling_text | The scrolling bands only |
scroll_bands | Per band detail: axis, direction, and that band's text |
bands_detected | How many scrolling bands were found |
The part that surprises people
The extracted text is stored as payload, not as a vector. Semantic search over it uses the
text_extractor vector, so if you want "find the ad that mentions a
money back guarantee" rather than an exact string match, you index the extracted
text with text_extractor as well.That is a deliberate split. On-screen text is often short, uppercase and punctuation free, which embeds poorly on its own, and a lot of the questions people ask about it ("which spots carry the legal line", "how many have a CTA") are filters rather than similarity queries.
Which extractor for which question
| You want | Use |
| Every word visible on screen, including tickers and credits | scrolling_text_extractor |
| The spoken words, plus per segment OCR and embeddings | multimodal_extractor |
| Text out of PDFs and scanned documents | document_graph_extractor |
| One extractor across images, video, audio and documents | universal_extractor |
scrolling_text_extractor takes video only. For spoken content reach for
multimodal_extractor with transcription enabled, and for documents reach for
text_extractor.Frequently Asked Questions
Does a transcript ever include text that is only shown on screen?
No. A transcript is produced by speech recognition, which listens to the audio track. A frame showing "OFFER ENDS SUNDAY" in large type produces no sound, so nothing about it reaches the transcript. The two are separate signals and each needs its own extraction pass.
What about burned-in subtitles or open captions?
Those are on-screen text, so they come from the OCR pass rather than the transcript. That matters when the audio is in one language and the burned-in subtitle is in another: the transcript holds the spoken language and
onscreen_text holds the written one, and searching only the transcript misses
every viewer who remembers the subtitle.Can I search the extracted on-screen text semantically?
Not from
scrolling_text_extractor alone. Its output is stored as payload, so it
supports filtering and exact matching. For similarity search over that text, index
it with text_extractor as well and query that vector.Does it handle credits and legal crawls that never sit still?
Yes, that is the scrolling pass. It detects bands moving horizontally or vertically, reconstructs each band by stitching frames like a panorama, and OCRs the reconstructed image, then removes repeated marquee loops so a ticker that cycles four times is not returned four times.
How long does extraction take?
Roughly 2 to 5 times realtime, depending on the video resolution and the frame sampling rate you configure. A one minute clip lands somewhere around twelve to thirty seconds.
My video is a screen recording of a slide deck. Is that the same problem?
Same problem, and the on-screen pass is what reads the slides. If the deck also exists as a PDF,
document_graph_extractor will give you better structure from the
source file than OCR over a recording of it, so prefer the original when you have
it.Checking whether this is your problem
Take a clip where you can see the text with your own eyes. Search the exact phrase. If the transcript search returns nothing and you can read the words in the frame, the words were never indexed, and no ranking change or embedding swap will recover them. The fix is an extraction pass, not a retrieval one.