ProjectIntent Over Keywords: From Stitched Embeddings to a Production Fashion Agent · Module 1: The problem with multimodal search · part 3 of 26
Part 3 · Module 1: The problem with multimodal search

Module 1: The problem with multimodal search

24 min read·
About 45 minutes · Needs: nothing (Module 0 gives the tour) · Code: samples/segments.json, samples/questions.jsonl, samples/build_test_video.py, originals/v1/video-rag-old/README.md

Where this fits

WHERE THIS FITS Intent Over Keywords this module PART 1: STITCHED VIDEO RAG three models, two spaces, RRF PART 2: UNIFIED VIDEO RAG one model, one space PART 3: STYLEME FastAPI, LangGraph, React QUALITY AND OPERATIONS evals, Docker, Cloud Run Frames ffmpeg, one per 5 s CLIP ViT-B/32 frames + text, 512 d Whisper base speech to text MiniLM 64-word chunks, 384 d Two collections p1_visual, p1_text Rank fusion RRF, k = 60 Gemini answer from transcript text API + Streamlit upload, ask, sources Windows ffmpeg, video + audio Embedding 2 Gemini, one vector One collection p2_unified, no fusion Notes + answer Gemini, cited times React web chat, photo, chips nginx one URL, /api proxy FastAPI /api/chat, limits Sessions in memory, cap of 3 Planner intent before retrieval Implementer batched embed, search Generator stylist reply, ids Grounding check no LLM, drops invented Gemini flash-lite, embedding 2 Qdrant styleme_products Catalogue ingest image + title vectors Langfuse optional traces Test video 26 questions, 4 kinds Video eval Part 1 vs Part 2 Agent evals intent, grounding Guards uploads, limits, secrets Containers compose profiles Cloud Run StyleMe only CI/CD GitHub Actions, WIF Original code v1, v2, fashion-agent
This module (highlighted): the test video and its questions, the two signals a video carries (frames and speech), and the original code. Blue is our system, amber a model, grey the original code.

This module is about the problem, not yet about a solution. The highlighted parts are the test video and its 26 questions, the two signals the first design pulls out of a video (frames and speech), and the author's original code, which promised to answer questions about any video. Everything later in the course is measured against what this module sets up: a video where some facts are only shown, some are only said, and some need both at once.

What it does

THE IDEA One moment, two signals, one answer c01: Which service did Tidewater remove in March? WHAT IS SHOWN WHAT IS SAID Shipment tracking: service map API Gateway Tracking Svc Route Planner Carrier Portals Label Printer Service Event Bus Analytics “Now look at the service in the red box. That's the one we removed in March.” spoken over the same slide, 0:17 to 0:36 Label Printer Service the name is shown, the month is said
Navy is the question, blue marks what each signal contributes, green the correct answer. The answer exists only when the picture and the speech of the same moment are read together.

Picture a recorded architecture review. At 0:17 a slide shows a service map with one box outlined in red: Label Printer Service. Over that slide the presenter says "Now look at the service in the red box. That's the one we removed in March". Someone later asks "Which service did Tidewater remove in March?". The answer needs the picture (the name) and the speech (the month) at the same moment. Neither one alone is enough.

That is the whole problem of multimodal search in one example. A video carries two signals that run on one clock: what is shown and what is said. A question can need one of them, the other, or both together. A search system that treats them separately can find the right moment and still give a wrong answer, and the wrong answer can look convincing. This course starts there: "Most AI projects stop when the answer looks correct. This project starts there."

At a glance

PracticeWhat it isWhy you need itHow this project does itWhen you need itWhere it lives
Treat video as two timed signalsKeep what is shown and what is said, each with its time range.An answer must point at a moment, and the two signals disagree about what that moment contains.segments.json records start_s, end_s, screen_text and narration for each of 12 segments.Any video, audio or slide search.samples/segments.json
Search by meaning, not keywordsCompare questions and passages as vectors, not as shared words.People ask in other words than the video uses ("component" vs "step").Every part embeds questions and evidence; nothing matches keywords.Whenever users phrase questions freely.Part 1 and Part 2 embedders/
Label evidence by modalityMark each fact as shown, said, or both.It tells you which part of a system must work for each question.Questions have a kind: visual 7, speech 7, cross_modal 8, distractor 4.When you build or test a multimodal system.samples/questions.jsonl
Define a good answer up frontA name or value, a time range, and the source it came from."Sounds right" is not checkable; these three parts are.answer_contains strings and gold time windows per question; answers cite times.Before you write any prompt.samples/questions.jsonl, the generators
Test before you trustUse questions that can fail, with known answers.An answer that looks right is not evidence. A designed test is.26 questions with traps; one comparison run through both APIs.Always; most of all when output reads fluently.samples/, evals/video/

Use cases and examples

Treat video as two timed signals

  • This project: segment 5 (71.8 to 92.7 s) shows a clock and three tiers with no numbers; only the narration says "forty-five days". A system that only reads slides cannot answer s02.
  • A hospital's recorded ward round: the chart on screen shows the dose, the doctor says which patient it is for. Losing either half makes the record dangerous.
  • An e-learning platform: a lecturer writes a formula on a whiteboard and explains each symbol aloud. Students search for "what does lambda stand for".

Search by meaning, not keywords

  • This project: c06 asks which "component" batches customer messages; the narration calls it "the step in the green box". The words differ, the meaning is the same.
  • An online shop: "something warm for a winter wedding" should find a wool shawl that never uses the word "warm".

Label evidence by modality

  • This project: the room name Harbor-4 is visual only, the presenter's name Priya Nair is speech only, the removed service is cross-modal.
  • An insurer's claim videos: damage is shown, the time of the accident is said. Tag each claim question so you know which pipeline to test.

Define a good answer up front

  • This project: c01's gold window is 17.333 to 36.133 s and the answer must contain "Label Printer". A reply without the name fails, however polite.
  • A bank's call-centre search: an answer is only useful with the call's timestamp, so a supervisor can check it.

Test before you trust

  • This project: Part 1 found the right moment for c01 at rank 1 and still answered "not mentioned".
  • A SaaS help centre: a support bot that cites the right article but quotes last year's price looks correct in a demo and fails customers.

When not to bother. If your videos are talking heads with nothing useful on screen (a podcast with a static logo), a transcript search is enough and the picture adds nothing. If your videos are silent (security cameras, product demos without voice), frames alone carry everything. The problem in this module only bites when the two signals carry different facts about the same moment.

How it works

We walk through the test video and its questions: what is in them, why each question kind exists, and what a correct answer must contain. Every value comes from samples/segments.json, samples/questions.jsonl and the comparison report evals/video/reports/20261027T192400Z.json (one run on 27 Oct 2026, UTC stamp 20261027T192400Z, with the free-tier key).

Step 1: one clock, two tracks

STEP 1 12 segments on one clock, 202.467 s PICTURE SPEECH 1 title 2 map 3 latency 4 whiteboard 5 retention 6 map 2 7 on-call 8 pizza 9 latency 2 10 flow 11 cost 12 next 0:00 0:30 1:00 1:30 2:00 2:30 3:00 3:22 segment 2: c01's gold window segment 8: the pizza distractor narration; gaps are silence
Drawn to scale from segments.json. Every segment is one still slide; narration starts 0.5 s in and stops at least 1.5 s before the slide changes, so slide changes fall in silence.

The test video, test_video.mp4, is a 3.4-minute fictional architecture review: "Tidewater Freight: Shipment-Tracking Architecture Review". It lasts 202.467 s and has 12 segments. Each segment is one still slide with narration over it. The slide is the picture track, the narration is the sound track, and both share the same timeline. A segment starts with 0.5 s of silence, then the narration, then at least 1.5 s of silence, so every slide change lands in a pause. That matters later: a search result that covers 15 s of video should not cut a sentence in half by accident.

Why care? Every answer in this course is judged by time. A question has a gold window, the time range where its answer lives.

Step 2: what is shown and what is said

STEP 2 Segment 2: the name is drawn, the month is spoken SCREEN_TEXT (DRAWN) 17.333 to 36.133 s NARRATION (SPOKEN) Whisper hears this Shipment tracking: service map Mobile App API Gateway Tracking Service Route Planner Carrier Portals Label Printer Service Event Bus Shipments DB Analytics Warehouse Here's the current service map. Requests come in through the gateway and fan out from there. Now look at the service in the red box. That's the one we removed in March, because the carrier portals were already doing the same job for us. Retiring it cut one deploy pipeline and a lot of late-night pages. facts.cross_modal: service removed in March = Label Printer Service
The two fields of segment 2 in segments.json. Neither contains the whole fact; the green line is the segment's own note saying the fact is cross-modal.

segments.json stores, for each segment, every string drawn on screen (screen_text) and the exact narration. Segment 2, arch_removed, shows a service map whose red box holds "Label Printer" "Service". Its narration says "the service in the red box. That's the one we removed in March". Neither field contains the whole fact. The screen has the name but not the month; the speech has the month but not the name.

The same pattern repeats on purpose. Segment 3 highlights the East bar at 640 ms while the speaker says "the bar I just highlighted ... where the geofence rollout will start". Segment 7 highlights Deepa Rao in yellow while the speaker says "the name highlighted in yellow is joining the rotation for the first time".

Step 3: why keywords are not enough

STEP 3 Keywords land in the wrong places 1 2 3 4 5 6 7 8 9 10 11 12 segment Geofence c02 gold: 6 Tracking Service c07 gold: 11 bill v03 gold: 11 March c01 gold: 2 Label Printer c01 gold: 2 on screen spoken segment that answers the question
Counted in segments.json. A keyword hit is often not the answer: 'Geofence' matches three segments, 'bill' matches the Finance target rather than the $18,400 total, and c01's two halves sit in different signals.

The simplest search is keyword search: find the passages that share words with the question. It fails here in three ways.

First, the words may not be there. "Which service did Tidewater remove in March?" shares "March" with the narration, but the answer, "Label Printer", is not spoken. A keyword search over the transcript finds the moment and cannot read the answer from it.

Second, the words may be somewhere else. The video contains keyword traps: "Geofence" is spoken in segment 3 and written in segments 6 and 12; "Tracking Service" is drawn on four slides. A question about the geofence rollout region (c03) matches three segments by keyword, and only one of them is right.

The same happens with ordinary words. v03 asks for the "total monthly cloud bill". The slide says "Monthly cloud cost by service" and "$18,400 per month". The only time "bill" is spoken is in "get the whole bill under fifteen thousand dollars by January". Part 1 answered v03 with "Based on the video, Finance has asked Tidewater to get their total monthly cloud bill under $15,000 by January [1]." The words matched; the fact did not.

Third, people use other words. c06 asks "Which component batches customer messages every thirty seconds?". The narration says "The step in the green box is where we batch messages". "Component" and "step" never meet in a keyword index.

Step 4: search by meaning

STEP 4 Meaning search: close in vector space, not in spelling an embedding space (drawn in 2 of its many dimensions) question narration, segment 10 pizza, segment 8 retention, segment 5 Question Which component batches customer messages ...? Nearest passage “The step in the green box is where we batch messages” 'component' is not 'step': a keyword index misses it, an embedding model does not
An illustration, not measured positions. An embedding model places related sentences near each other, so the question finds the narration although the key word differs. Amber marks what the model decides.

The alternative is semantic search, search by meaning. A model called an embedding model turns a piece of text (or an image, or a clip of audio) into a list of numbers, a vector, so that things with similar meaning get vectors that point in similar directions. "Which component batches customer messages" and "the step in the green box is where we batch messages" end up close together even though "component" and "step" are different words.

Meaning search fixes paraphrase. It does not fix the first two problems on its own: Part 1 already ranks by meaning, and the v03 answer above came out of it. If the name is only on screen and the month is only in speech, a meaning search over the transcript still finds a transcript chunk without the name, and a meaning search over the frames still has to guess which service map is meant. Which signals you embed, and whether you embed them together, decides what can be found. That choice is what Parts 1 and 2 disagree about.

Step 5: three kinds of evidence, and one kind of noise

STEP 5 26 questions, four kinds cross_modal 8 needs both: 'the service in the red box' visual 7 only on screen: Harbor-4, Aiko, $18,400, 5x speech 7 only spoken: Priya Nair, 45 days, Dec 9 distractor 4 the pizza workshop: 430 °C, courtyard A cross-modal failure only means something if visual and speech questions pass: they prove each path works.
Bars to scale from questions.jsonl. Blue marks the kind that carries the lesson; navy the two kinds that check each signal on its own; grey the off-topic control.

The 26 questions are split by where the answer lives. Visual questions (7) have answers drawn on screen and never spoken: the room Harbor-4, the week 45 on-call engineer Aiko Tanaka, the $18,400 bill, the "5x" load test. Speech questions (7) have answers spoken and never shown: the presenter Priya Nair, 45-day retention, the December ninth review. Cross-modal questions (8) need both: the narration points at something ("the service in the red box", "the largest slice") and the name or value it points at is only on screen. Distractor questions (4) ask about a pizza workshop placed in the middle of the review, to check that a search can leave the main topic when asked to.

The kinds are a diagnostic tool. Failing every visual question means a broken picture path. Passing visual and speech questions but failing cross-modal ones means a binding problem: each signal is read, and they are never connected.

Step 6: near-duplicates make guessing impossible

STEP 6 Near-duplicates: one detail decides Shipment tracking: service map API Gateway Tracking Svc Route Planner Carrier Portals Label Printer Service Event Bus Analytics Shipment tracking: service map API Gateway Tracking Svc Route Planner Carrier Portals Geofence Worker Event Bus Analytics segment 2, 'removed in March': c01 segment 6, 'adding next quarter': c02 Latency by region, Week 37 N S 640 E W C Latency by region, Week 42 N S 230 E W C segment 3, 'where the rollout will start': c03 segment 9, 'after the October fix': c04
Drawn from segments.json. Each pair looks the same to a quick glance; only the speech says which one a question means, so a lucky picture match scores as a miss.

Two pairs of slides are nearly identical. The two service maps (segments 2 and 6) differ only in the red-boxed service: Label Printer Service, then Geofence Worker. The two latency charts (segments 3 and 9) differ only in the East value and the week label: 640 ms in Week 37, 230 ms in Week 42. c01 and c02 can only be scored correct when the right service map is picked; c03 and c04 when the right chart is picked. A system that matches "a service map" is right half the time by luck, and the test is built so that luck does not pass.

Step 7: what a good answer needs

STEP 7 A good answer has three parts, and each can be checked A name or value Label Printer Service checked by answer_contains: any string, any case A time [0:24-0:33] checked by the source overlaps the gold window A source the retrieved moment checked by a person can open and check it
Green marks what a correct answer must contain. Each part has a mechanical check, so 'it sounds right' never has to be the judge.

A good answer to a question about a video has three parts: a name or value (Label Printer Service), a time (where in the video it is), and a source (the retrieved moment the answer came from, so a person can check it). The checks follow from that. An answer passes when it contains any of the question's answer_contains strings, case-insensitive. A source is a hit when it overlaps a gold window.

STEP 7 Scoring c01's sources against its gold window gold 17.333 to 36.133 s text 0.0 to 23.0 s: overlaps, hit at rank 1 frame 175 to 180 s (cost chart): miss text 17.9 to 54.2 s: overlaps 0 s 60 s 120 s 180 s
Three of Part 1's five sources for c01 in the 27 Oct 2026 run. Green overlaps the gold window (a hit), red does not. Retrieval scored rank 1; the answer still failed.

Here are the two observed answers to c01 from the comparison run. Part 1 (the stitched design) answered: "Based on the provided video evidence, the specific name or type of the service removed in March is not mentioned; the speaker only refers to it as the service located "in the red box" on the service map [3]. It was removed because carrier portals were already performing the same job, which helped reduce deploy pipelines and late-night pages [3]." Part 2 (the redesign) answered: "Tidewater removed the Label Printer Service in March because the carrier portals were already performing the same job [0:24-0:33]." The first has a reason and a reference and no name. The second has all three parts.

Step 8: the answer that looks right

STEP 8 Found the moment, missed the fact PART 1, CROSS-MODAL QUESTIONS (8) source in gold window, top 5 7 of 8 source in gold window, rank 1 5 of 8 answer contains the fact 0 of 8 c01 answer: “... the specific name or type of the service removed in March is not mentioned; the speaker only refers to it as the service located “in the red box” ...”
Bars to scale from the 27 Oct 2026 report. Green is retrieval doing its job, red the answers. The gap between them is what this course calls 'almost correct'.

Look again at Part 1's c01 answer. It is fluent, it cites evidence, it gives the reason for the removal, and its top source overlaps the gold window: retrieval ranked the right moment first. A reader skimming it would call it good. It fails the one check that matters: it does not name the service.

The same pattern holds across all eight cross-modal questions in that run. Part 1 had a source in the gold window in its top 5 for 88% of them (7 of 8) and a passing answer for 0 of 8. That gap between "found the right place" and "gave the right answer" is what the course calls almost correct. Module 6 diagnoses it; Part 2 removes its cause. Part 2 also used ingest-time notes written by a Gemini model, so its answer rate mixes two changes; Module 10 explains why retrieval metrics are the fair comparison.

Code tour

CODE TOUR The files behind this module samples/build_test_video.py draws 12 slides, narrates, builds the video samples/segments.json timeline: times, screen text, narration samples/questions.jsonl 26 questions: kind, gold, answer strings samples/test_video.mp4 1280x720, 15 fps, 202.467 s, about 3 MB originals/v1/video-rag-old/README.md the original stitched plan part1-.../src/api/schemas.py Part 1 Source: time, modality, scores
The build script writes the three blue sample files in one run, so the timeline, the questions and the video always agree. Grey is the author's original code, kept read-only.

This module's "code" is mostly data: the timeline, the questions, and the script that builds both. We read them in that order, then the original project's own description of its plan.

New concept: retrieval-augmented generation (RAG) A language model answers from what it learned in training, which knows nothing about your video. Retrieval-augmented generation first searches your own data for the passages most related to the question, then gives those passages to the model with the question and asks it to answer only from them. Retrieval decides what the model can see; the model only writes the sentence.
text
{
  "index": 2,
  "key": "arch_removed",
  "title": "Service map (red box: removed service)",
  "start_s": 17.333,
  "end_s": 36.133,
  "speech_start_s": 17.833,
  "speech_end_s": 34.613,
  "screen_text": ["Shipment tracking: service map", "Mobile App", "API Gateway", "Tracking", "Service",
                  "Route Planner", "Carrier Portals", "Label Printer", "Service", "Event Bus", ...],
  "narration": "Here's the current service map. ... Now look at the service in the red box. That's the one we
                removed in March, because the carrier portals were already doing the same job for us. ...",
  "facts": {
    "cross_modal": ["service removed in March = Label Printer Service (name on screen, 'removed in March' spoken)"],
    "visual_only": ["Tracking Service writes to Shipments DB"]
  }
}

This is segment 2 of samples/segments.json, shortened and reflowed (the ... marks cuts). start_s and end_s are frame-exact; speech_start_s is 0.5 s later and speech_end_s ends 1.52 s before the slide changes. The facts field is a note for humans: which fact is visual only, which is cross-modal.

python
# samples/build_test_video.py
SEGMENTS: list[Seg] = [
    # ...
    Seg(
        "arch_removed",
        "Service map (red box: removed service)",
        "Here's the current service map. Requests come in through the gateway and fan out from there. Now look at the "
        "service in the red box. That's the one we removed in March, because the carrier portals were already doing "
        "the same job for us. Retiring it cut one deploy pipeline and a lot of late-night pages.",
        lambda s: slide_arch(s, "Label Printer\nService", 2, 12),
        {
            "cross_modal": [
                "service removed in March = Label Printer Service (name on screen, 'removed in March' spoken)"
            ],
            "visual_only": ["Tracking Service writes to Shipments DB"],
        },
    ),

Code walkthrough

  • SEGMENTS is the script's single source of truth: one Seg per slide, in order. The JSON file above is generated from it.
  • The first two strings are the segment's key and title. The third is the narration, the exact text sent to the voice engine.
  • lambda s: slide_arch(s, "Label Printer\nService", 2, 12) draws the slide with Pillow. The same slide_arch function draws segment 6 with "Geofence\nWorker" in the red box, which is how the near-duplicate pair is made: one function, one changed label.
  • The dictionary at the end is the human note that becomes facts in segments.json.
  • Nothing here mentions times: they are computed from the measured length of each narration clip.
New concept: modality A modality is a kind of signal: images, speech, written text, sound. A system is multimodal when it takes in more than one. In this course "visual" means what is on screen, "speech" means what is said, and "cross-modal" means a fact that only exists when you combine the two.
text
{"id": "c01", "question": "Which service did Tidewater remove in March?", "kind": "cross_modal", "gold": [{"start_s": 17.333, "end_s": 36.133, "segment": "arch_removed"}], "answer_contains": ["Label Printer"], "notes": "near-duplicate of arch_added: a frame-only match on the service map cannot tell the two apart"}
{"id": "v01", "question": "Which meeting room is the architecture review held in?", "kind": "visual", "gold": [{"start_s": 0.0, "end_s": 17.333, "segment": "title"}], "answer_contains": ["Harbor-4", "Harbor 4"], "notes": "answer is only on screen (never spoken)"}
{"id": "s02", "question": "How long are raw GPS pings kept?", "kind": "speech", "gold": [{"start_s": 71.8, "end_s": 92.733, "segment": "retention"}], "answer_contains": ["45 days", "forty-five days", "forty five days"], "notes": "answer is only spoken (never on screen)"}

Three lines of samples/questions.jsonl, one per kind that the course keeps coming back to. Each line is one JSON object: the question, its kind, one or more gold windows copied from segments.json, and the accepted answer strings. s02 accepts three spellings of the same number because a model may write digits or words.

python
# samples/build_test_video.py
    # cross-modal: the name/value is only on screen, what it means is only in the narration
    dict(
        id="c01",
        kind="cross_modal",
        seg=["arch_removed"],
        question="Which service did Tidewater remove in March?",
        answer_contains=["Label Printer"],
        screen_terms=["Label Printer"],
        speech_terms=["March"],
        notes="near-duplicate of arch_added: a frame-only match on the service map cannot tell the two apart",
    ),

Code walkthrough

  • This is where c01 is defined. seg=["arch_removed"] names the gold segment by key; the build copies its exact start and end into the gold field, so the times cannot drift from the video.
  • screen_terms and speech_terms say which words must be drawn and which must be spoken. For c01 the name is a screen term and "March" is a speech term: that is what makes it cross-modal.
  • These two fields do not reach questions.jsonl. They feed a self-check in the build that fails if a screen-only term is ever spoken or a speech-only term is ever drawn. Module 6, on designing a test video that can fail, reads that check line by line.
  • answer_contains is deliberately short: "Label Printer" passes "Label Printer Service" and "the label printer", and fails anything without the name.
New concept: embedding (meaning search) An embedding is a fixed-length list of numbers that a model produces for a piece of content. Models are trained so that related content gets nearby vectors. To search, you embed every stored item once, embed the question at query time, and return the items whose vectors are closest. Closeness, not shared words, decides the ranking.
text
<!-- originals/v1/video-rag-old/README.md -->
1.  **Ingestion**: 
    -   Frames are extracted every 5 seconds and embedded with **CLIP**.
    -   Audio is extracted and transcribed with **Whisper**.
    -   Transcript is chunked (512 tokens) and embedded with **MiniLM**.
    -   All vectors are stored in two separate Qdrant collections.
2.  **Retrieval**:
    -   Queries are encoded with both CLIP (visual) and MiniLM (textual).
    -   Top results from both collections are fused using **Reciprocal Rank Fusion**.
3.  **Synthesis**:
    -   Fused results are passed as context to Gemini 2.5 Flash Lite to generate the final answer.

Code walkthrough

  • The author's original plan for Part 1, from originals/v1/video-rag-old (kept read-only). Line by line it is reasonable: a vision model for frames, a speech model for audio, a text model for the transcript, a vector database, a well-known way to merge rankings, a language model to write the answer.
  • What it never says is where the picture and the speech meet. They are embedded by different models into "two separate Qdrant collections", and only their rankings are merged. That gap is invisible in the plan and decisive in the results. Module 2 walks the whole pipeline.
  • The model named in step 3, Gemini 2.5 Flash Lite, now returns 404 for new users; the hardened code makes the model a setting.
python
# part1-video-rag-stitched/src/api/schemas.py
class Source(BaseModel):
    video_id: str
    start_s: float
    end_s: float
    modality: str
    score: float
    thumbnail_url: str | None = None
    preview: str | None = None
    raw_score: float | None = Field(default=None, description="Cosine score inside its own collection, before RRF")
    rank_in_modality: int | None = Field(default=None, description="Rank inside its own collection, before RRF")

Code walkthrough

  • Every answer in Parts 1 and 2 comes with a list of these: the evidence behind it. This is the "source" part of a good answer.
  • start_s and end_s are the "time" part: the eval harness checks them against the gold window.
  • modality says which signal the evidence came from (visual or text in Part 1). Part 2 returns the same shape, which is why one harness can compare both.
  • raw_score and rank_in_modality show where the item stood inside its own collection before fusion; Module 5 uses them to explain rank fusion.

What happens when someone asks c01

  1. A user types "Which service did Tidewater remove in March?" into Part 1's UI. The UI sends it to POST /search with top_k 5.
  2. Part 1 embeds the question twice (once for frames, once for transcript text) and searches two collections, 10 candidates each.
  3. In the 27 Oct 2026 run, the best transcript chunk covered 0.0 to 23.0 s. It overlaps the gold window (17.333 to 36.133 s), so retrieval scored rank 1. The best frame was from 175 to 180 s: the cost chart, not the service map.
  4. The two rankings are merged by rank. The model receives the transcript text of the text sources and only a time range for the frames (Module 6 diagnoses this failure).
  5. The transcript says "the service in the red box ... removed in March". The name is not in any text the model received. The model answers that the name "is not mentioned".
  6. The harness marks c01 as rank 1 and answer wrong: almost correct.

Design decisions

DecisionChosenAlternativesWhy
Test inputone synthetic 3.4-minute video drawn with Pillowa real recorded meeting; a public videoevery fact is placed on purpose and known exactly; no third-party media; rebuildable
Facts per modalityeach fact only on screen, only spoken, or split across bothnatural slides where speech repeats the slidea fact in both signals cannot show which path found it
Question kindsvisual 7, speech 7, cross_modal 8, distractor 4one mixed setper-kind results point at the broken component
Near-duplicatestwo service maps, two latency chartsall slides differenta lucky guess must fail
Answer checkany answer_contains string, case-insensitivean LLM judge; exact matchcheap, repeatable, no model in the grader; cross-modal answers went 0 of 8 to 8 of 8 on this check
Retrieval hita source overlapping a gold windowthe source's start must be insidesources have different lengths in Parts 1 and 2
NarrationmacOS say (Samantha) in the committed videoGemini TTS (gemini-3.8-flash-tts, voice Kore)the free tier allows 10 TTS requests per day per model and the video needs 12
Segment edges0.5 s lead silence, at least 1.5 s tail silenceback-to-back narrationslide changes land in pauses, so a window edge does not split a sentence and a slide
DESIGN Each question kind tests one path KIND PATH IT TESTS IF IT FAILS visual (7) frames the picture path is broken or blind to text speech (7) transcript the speech path is broken cross_modal (8) both, bound the system cannot connect the two signals distractor (4) leaving the topic retrieval is stuck on the main subject
The kinds turn one overall score into a diagnosis: which signal path failed, or whether the binding between them failed.

Cross-modal questions are the largest group because they are the lesson; visual and speech questions prove that each path works on its own, so a cross-modal failure cannot be blamed on a dead path.

TRADE-OFF A string check versus a model as judge ANSWER_CONTAINS (CHOSEN) LLM JUDGE (NOT BUILT) Cost per check none one model call Same result every run yes not guaranteed Fooled by fluent wording no can be Catches a false extra claim no can, if the rubric asks Accepts paraphrased values only listed spellings yes
The string check wins on cost and repeatability and cannot be talked round. Its blind spot is an answer that contains the right name and a wrong claim.

A string check is crude, and that is a feature. It cannot be talked round by a fluent answer, and it costs nothing to run. Its limit is the reverse: an answer that names "Label Printer" and then says something false about it still passes. The course states that limit wherever it quotes answer rates.

When to choose differently

If ...Switch to ...WhyIn this project
you need realistic speech for a demoGemini TTS for the narrationa natural voice; timings are re-measured, so gold windows stay rightBuilt: samples/build_test_video.py (--tts gemini)
answers are long and free-forman LLM judge with a rubric, checked against human labelsstring checks miss paraphrases and miss false statementsNot built
your real videos change picture faster than every few secondsa shorter frame interval, or more questions on fast scenes5 s frames can miss a short scene entirelySetting: FRAME_INTERVAL_S=2
you must prove behaviour on your own contenta labelled set from your own recordings, same four kindsone synthetic video says little about yoursNot built
you need confidence intervalsmany videos and repeated runsone run, one video, 26 questions has noneNot built

What can go wrong

WHAT CAN GO WRONG Four ways a video search fails quietly Keyword trap 'Geofence' in 3 segments rank by meaning, test with traps Near-duplicate wrong service map only the speech can tell them apart Distractor leak v01 top source: pizza frame distractor questions catch it Fluent but wrong c01: 'not mentioned' check against a known answer
Red is the failure, green the defence. None of them raises an error; each is caught only by a question designed to expose it.

A keyword match finds the wrong segment. "Geofence" is spoken in segment 3 and written in 6 and 12; "Tracking Service" is drawn on four slides. A system that ranks by shared words will often return the wrong one of these. The defence is to rank by meaning and to test with the traps in place: c02's note says "'Geofence Worker' is also written on the Next steps slide, but only this window says 'next quarter'". In the comparison run Part 1 missed c02 entirely (no source in the gold window); Part 2 ranked it first.

A near-duplicate wins. A frame of the wrong service map looks almost the same as the right one. The gold windows for c01 and c02 are different segments, so picking the wrong map scores as a miss. Module 3 shows a real case: for c03, CLIP's best frame came from the Week 42 chart, not the Week 37 one.

The distractor leaks into answers. The pizza segment has temperatures, timings and a "Friday". For v01, Part 1's top source was a pizza-slide frame at 125 to 130 s, and the answer was "I could not find which meeting room the architecture review is held in from the video evidence." The retrieval failed; the answer at least said so.

The answer sounds right and is not. This is c01 in Part 1, and it is the failure the course is built around. No exception is raised, no log line is written, the source is in the right place. Only a check against a known answer catches it.

Changing the voice changes the timeline. A different voice speaks at a different speed, so every segment moves. The build regenerates segments.json and questions.jsonl together from the measured timeline; re-run the evals afterwards.

Fixed: the original could not answer at all. The original v1 hard-coded gemini-2.5-flash-lite, which now returns 404 "no longer available to new users". This was found during the integration run with the author's key. The hardened code reads the model from the setting GENERATION_MODEL (default gemini-3.5-flash-lite), keeps one client with a timeout, and retries on 429s. Tests: test_health_metrics_and_errors checks that /health reports gemini-3.5-flash-lite, and test_generator_context_and_fallbacks checks that a missing key or a failed call returns the raw evidence instead of an error.

Known limits. The test video is one synthetic review with a synthetic voice, still slides and no camera motion. Its 26 questions give no confidence intervals. The answer check accepts any answer that contains the string. The video's world (Tidewater Freight, Priya Nair, the services) is fictional and exists only in segments.json.

Try it yourself

bash
cd source-code/samples
python3 -c "import json; s=json.load(open('segments.json')); [print(g['index'], g['key'], g['start_s'], g['end_s']) for g in s['segments']]"

What you should see

Twelve lines, from 1 title 0.0 17.333 to 12 next 190.0 202.467. Line 2 is 2 arch_removed 17.333 36.133, c01's gold window.

bash
cd source-code/samples
python3 -c "import json, collections; print(collections.Counter(json.loads(l)['kind'] for l in open('questions.jsonl')))"

What you should see

Counter({'cross_modal': 8, 'visual': 7, 'speech': 7, 'distractor': 4}).

bash
cd source-code/samples
uv run build_test_video.py --tts say --out-dir /tmp/iok-video

What you should see

On a Mac with ffmpeg installed: a line slides: 12 rendered, self-check passed, a narration line saying say was used, then the video, segments and question files written to /tmp/iok-video. The committed files in samples/ are not touched. If your Mac's Samantha voice differs, the timings differ slightly; that is expected.

In the Part 1 UI (make up-part1, then open http://localhost:8511): upload samples/test_video.mp4 on Upload and index, then on Ask type "Which service did Tidewater remove in March?". Read the answer, then the sources and their previews. Whatever the transcript says about that moment, it speaks of "the service in the red box" and never names it, because the name is never spoken.

Check yourself

  1. Why is c01 a cross-modal question and v01 a visual one?

v01's answer (Harbor-4) is drawn on screen and never spoken, so the picture alone answers it. c01 needs the name, which is only on screen, and "removed in March", which is only spoken. Neither signal alone contains the answer.

  1. A system scores 100% on visual and speech questions and 0% on cross-modal ones. What does that tell you?

Both paths work on their own: it can read the screen and it can read the speech. What fails is connecting a spoken pointer ("the red box") to the on-screen value at the same moment, a binding problem rather than a broken component.

  1. Why does the test video contain two almost identical service maps?

So that matching "a service map" is not enough. c01 and c02 have different gold segments; only a system that uses what was said ("removed in March" or "adding next quarter") can pick the right one. Without the pair, a lucky frame match would pass.

  1. Part 1's c01 answer cites evidence and its top source is in the gold window. Why does it fail?

It does not contain "Label Printer". A good answer needs the name or value, a time and a source; this one has a source and a reason but not the name, because the name never reached the model as text.

  1. Why does the answer check use short strings like "Label Printer" instead of a model as a judge?

It is cheap, repeatable and cannot be persuaded by fluent wording. Its limit is that it passes any answer containing the string, even one that adds a false claim.

Key terms

TermMeaning
Multimodal searchsearching content that carries more than one kind of signal, such as pictures and speech
Modalityone kind of signal: on-screen picture, speech, written text, sound
Cross-modal questiona question whose answer needs facts from two modalities at the same moment
Gold windowthe time range of the video where a question's answer lives
Keyword searchranking by shared words between the question and the content
Embeddinga list of numbers a model produces so that related content gets nearby vectors
Retrieval-augmented generation (RAG)search your data first, then have a model answer only from what was found
Near-duplicatetwo segments that look almost the same, so only the right detail can tell them apart
Almost correctan answer that finds the right moment and reads well but misses the fact asked for