Documentation & FAQ

How footage is processed, how search works, and how to use the API.

Overview
What is this tool?

A semantic search engine for TubeScience's Iconik asset library. You describe a scene in plain English (or drop a reference image or video clip) and the system returns the most visually and contextually similar scenes, images, or audio clips across 8 clients and ~185K indexed embeddings.

Search is powered by Gemini embeddings — each asset is represented as a 3072-dimensional vector stored in PostgreSQL with the pgvector extension. Similarity is computed using cosine distance via an HNSW index.

Can I look up an asset by its exact name?

Yes. Paste an asset's exact name (title) into the search box and you get a straight lookup — just that asset, with no semantic search, recommendations, or relevance scoring. If several assets share the same name, all of them are returned. Because nothing is scored, these results show no score badge.

Matching is case-insensitive. A person's name that matches no talent or producer in your current folder (say Sabrina inside Il Makiage, when the only Sabrinas are other clients' talent) no longer returns an empty page: the search automatically falls back — first to an exact product/SKU match, then to the regular visual + text search — so you always get the best available results for the words you typed. A partial or near name still runs the normal "more like this" search — that is not affected by this.

A shoot id followed by words searches inside that shoot. Type the production id and then what you are looking for — HMHRS260920-T10497 talking about insurance — and the id becomes the scope while the words run the normal search, transcripts included. The id can sit anywhere in the sentence.

Ids and file names are exact, always. Paste an asset ID (UUID), a production id (FRMDG260311-R10192, ISTHD260915-0851A), a job number (J972) or a file name with its extension (C0042.MP4), and you get exactly that — the asset, the shoot, the job's cuts, the file — straight from the index, with no semantic search behind it. If nothing in your current folders matches, the page is empty: an id that is not there has no "similar" results worth showing.

File names without an extension work too. Paste a title like FCTOR251212-S10009_IMG_0660, a clip you downloaded earlier (its name carries the source asset's id), or a name with an extra word in front, and the file comes straight back. If no file matches, you get the regular search for those words instead of an empty page.

Fixed 22 Oct 2026 — a file name finds its own kind of media. Searching a file name that ends in .mp3 or .wav (or another audio format) finds that audio file, and if it isn’t in the library the answer is “audio file name is not found” and nothing else — before, a music track’s name could answer with a page of videos. The name of a still (.jpg, .png) or a video (.mp4, .mov) that isn’t in the library has the words of its name searched among images or among videos only, whichever media types are selected. A file name typed with its extension also finds its file when the library stores the name without it, for every kind of audio, still and video file; before, that only worked for the most common ones, so a file like theme.m4a or portrait.tif could be missed even though it was there.

"More like this" matches the scene you clicked. The similarity seed is the exact scene on that result card — not the beginning of the asset — so clicking it on a bjj-medal moment finds more bjj-medal shots even when the same file opens on something unrelated.

Where are the Stock folders?

Stock folders are now in the directory (2026-10-22). Each client’s LucidLink Stock folder — the licensed music, stock video and images kept beside the client’s jobs — appears under a new top-level Stock root in the folder browser, next to Footage, Client Collections and Approved Ads, whenever it has files in it. Open the root, the client and its subfolders (Music, Video, Images…) to list the files by name; clicking one opens it in Iconik.

Stock files are not analysed: they have no scenes, no thumbnails and no transcripts, so they do not come up in search results, on the home page, in “new” lists or in filter counts. Typing a file’s exact name (for example taking-a-new-step-SBA-346869127.mp3) finds it. Archived clients’ Stock folders are left out. The list is refreshed every six hours.

What is a "scene"?

When a video asset is ingested, it is automatically split into scenes using scene-change detection (PySceneDetect). Each scene has a start time, end time, and its own embedding generated from a keyframe. A single asset may contain dozens of scenes, all independently searchable.

Can I find the exact moment an action happens, not just the whole shot?

Yes. Search now recognizes action queries — "someone opens a door", "a hand places a pill on a table", "dog jumping" — and ranks them on what's happening in the footage (generic phrasings like working out or doing yoga match the activity shown, not the literal word) (the scene description and detected objects) rather than treating them like a generic visual lookup. The result lands you on the matching shot with a green in/out band marking the moment.

When footage is analyzed, Gemini describes it in short 2–5 second segments, each with its own start/end time and per-channel text (spoken words, on-screen text, objects, and a one-line description). We keep those segments (in a scene_segments table) so the in/out can be pinned to the exact few seconds of the action — to the second — wherever those fine segments exist; otherwise it falls back to the best estimate within the shot. Coverage tightens automatically as more footage is processed, at no extra analysis cost.

Each segment is searchable per channel, so the system matches the right signal for your query: spoken words (someone says X), on-screen text (a label appears), detected objects (a dog is present), or the visual description (what is happening).

Can I filter footage to studio shots vs b-roll?

Yes — and it's automatic from your query, no toggle needed. If your search implies a type — e.g. "testimonial about…", "founder talking to camera" (studio: someone on camera speaking) or "b-roll of…", "native ugc unboxing" (b-roll: product shots, demos, scenery, usually silent) — the system detects that and filters footage to it. Words like native, b-roll, or testimonial always trigger it; otherwise the query's meaning is used.

The studio/b-roll label is inferred per scene — Iconik doesn't store a reliable shot-type field, so each scene is classified from its folder name, whether anyone is speaking, and the AI scene description. Best-effort, and most useful on raw footage where shots run long and mix talking with b-roll.

Why do whole collections show up in my results, and what are they?

If your query asks for a combination the footage genuinely never shows together (say, a dog and a forklift in a beauty client's library), the search no longer returns an empty page: it automatically relaxes those must-appear requirements and shows the closest matches instead, ranked by the same words. API responses flag this with a query_relaxed field so integrations can tell a relaxed page from an exact one.

Work In Progress Jobs are being added — the jobs your team is cutting right now, as a fourth source alongside Footage, Client Collections and Approved Ads. New material appears within the hour; the existing back-catalogue (about 210,000 items) is being loaded in the background, so expect that source to keep filling in.

Some results are a collection rather than a single clip — you'll see a card with a COLLECTION tag, an accent border, the number of assets inside, and a few thumbnails from its contents. These are the curated Client Collections folders your team already organises footage into (for example 2.10. GLP1 Pill or 05_Results). Clicking one opens the folder so you can see everything in it.

Why footage sometimes lagged behind Iconik on 4 Aug 2026: the sync job spent an hour at a time re-scanning an archive folder that had been enrolled by mistake, hit its one-hour limit, was stopped, and started over — so real clients' new footage waited behind it. The archive folders can no longer be enrolled, and the job now stops itself cleanly before the limit instead of being killed mid-run, so each pass keeps the work it finished.

Fixed 4 Aug 2026 — a folder filter could be dropped: when your search words happened to match the NAME of a folder, the app switched to showing that folder's contents and stopped applying the brand you had selected — so a search under one brand could show another brand's footage. It only happened on words that collide with a folder name, which is why most searches looked fine. Every search mode now applies your folder and brand filter, and if a filter can't be applied the search returns nothing rather than something wider.

Changed 23 Oct 2026 — the Line and SKU filters show only tagged footage. Picking a line (Core, Family, Tempo) or a SKU now returns only clips tagged with it: the same line and SKU tags you see on the clip cards. Since 19 Oct the Line filter had also let through clips whose folder or production code named the line, and since 21 Oct every Home Chef clip with no line tag at all. As a result, a Line search could return a full page with not one clip tagged for that line. The number beside each line now counts the same tagged clips, so it matches the results. The filters also apply when you paste an asset ID or a file name, or type a production ID or a job number. Untagged footage is still reachable by searching without the filter. A line brand’s own footage still comes first in its searches.

Fixed 16 Oct 2026 — SKU, line and product filters on folder-name searches: the same shortcut skipped these filters. Home Chef with the SKU Tempo General picked and the search unboxing showed all 54 clips of the "Unboxing" folders, mostly Core and Family footage, because unboxing is also a folder name. The footage was labelled correctly. A folder-name search now shows only the folder's clips that match your filters, and when none do, it runs the normal search with your filters instead.

Fixed 19 Oct 2026 — a brand line's own footage comes first. Searching as a Home Chef line brand (Tempo, Core, Family) used to rank every matching Home Chef clip by relevance alone, so a Tempo search for salmon led with Home Chef partnership salmon b-roll and Tempo's own salmon meal shots sat on page two. Now the line's own footage — recognised by its facet, its folder (_TEMPO, Tempo_Footage, a shoot folder named for the line) or its production code (HCTMP) — comes first, then footage with no line, then clips tagged for a sibling line; within each group, clips whose file name carries your search words lead (a shot named BROLL_LemonHerbSalmon_MealPlateReveal outranks a creator clip that only mentions salmon). Nothing is hidden: the other clips follow in their usual order. (The same evening the Line filter was widened to accept a line’s folder and production code. That was reversed on 23 Oct; see above.)

Fixed 19 Oct 2026 — re-running a search returns the same page. The first time a search was run, the automatic studio / b-roll reading of your words often arrived a moment too late to narrow the results, while running the same search again used it straight away, so the two pages could differ (a few hundred clips the first time, under a hundred the second). The first run now applies that reading the same way, so running a search again gives you the same results.

Fixed 17 Oct 2026 — searches had been slower since 9 October. A change made on 9 October to keep one heavy database query fast also switched off the index the text-matching step relies on, for the rest of every search. Typical searches took about two seconds longer, and on the largest libraries the text-matching step gave up and its matches were missing. The setting now applies only to that one query. Results are the same clips; they arrive sooner, and text matches on large libraries are back.

17 Oct 2026 — fewer trips to the database per search. The search server and its database sit in different clouds, so every question it asks the database costs about a fortieth of a second before any work is done, and a typical search asked about fifty-five questions one after another. Several of those asked for things the server already knew or did not need an answer to. They are gone: results are the same, each search is a little quicker, and “more like this” searches no longer fetch their candidate set twice.

17 Oct 2026 — footage that search could not see. A nightly maintenance step that makes newly processed footage reachable by search had been failing quietly since 14 October. About a third of Il Makiage's footage, and a few thousand clips at other brands, were indexed but not reachable by a normal search. The step is fixed and running; those clips are being made searchable again.

They're searchable the same way everything else is. Each collection is represented by the average of the footage inside it, plus all of that footage's transcripts, on-screen text, detected objects and scene descriptions joined together — so a collection can match your query on what it contains, not just on its name. You no longer have to know a folder exists to find it: searching for the kind of footage you want can surface the whole folder that's full of it.

The card shows the first few clips inside it as a stack of cards — hover it and the stack spreads and all of them play, so you can see the actual footage inside without opening anything. The total count sits in the corner. View collection opens everything inside it in the results grid, most typical first — so the shots that define the folder come first rather than whatever was uploaded most recently. Opening a folder shows everything in it — the count on the card and the number of clips you get back are the same number. Until 4 Aug 2026 they could disagree badly (one folder showed 21 on the card and 8 when opened): clips still being processed were left out, and so was any clip filed under a slightly different spelling of the client's name — Muddy Mats where the folder said Muddy Mat. Now the folder gives you its contents, full stop; a clip that hasn't finished processing still appears, just without a preview yet. Archived footage stays hidden, as everywhere else. Opening a large folder is also about 8× faster. A collection card has no hover-preview, scrub bar or download, and its detail popup has no download, transcript or save-scene controls, because a collection isn't a file — open it and use the clips inside. While browsing a collection, More like this stays inside it — the folder's clips re-sort by similarity to the one you clicked, and nothing from outside the folder mixes in.

Because a collection is an average of many clips, it will rarely beat the single best-matching clip on a precise query — so when a collection is a genuinely close match, we reserve it a place on the first page (at most two per page). If nothing relevant qualifies, no space is used and your page looks exactly as it otherwise would.

Can I see other footage from the same production shoot?

Yes. Every result card has a "From this production" button — click it and the results grid fills with every asset shot on the same production, ranked by how visually similar each one is to the clip you started from (the similarity % on each card is measured against that clip), and paginated like any search. Raw footage that isn't part of a named shoot (client filespace mirrors, product libraries) doesn't dead-end: the button instead browses the folder the clip sits in — the header reads From folder "…" so you know the scope — still ranked by similarity to your clip. Client-collection clips that are raw footage (not ads) fall back to their folder the same way; ads without a shoot show an empty state.

A "production" is one shoot, identified from the asset's Iconik folder name (e.g. ADOBE241221-R10020_… → client ADOBE, shot on 2024-12-21, job R10020). Footage from a shoot is often split across several sub-folders (Day 0, selects, raw, …); they're all pulled together. This lets you start from one good clip and quickly see the rest of that day's material — alternate takes, b-roll, and angles from the same session.

Can I jump between raw footage and the ads built from it?

Yes — the mapping works both ways, straight from any result card:

  • On a Raw Footage card, click "Ads using this" to see every Approved Ad built from that clip. This works for every copy of the clip — footage that also lives in a Client Collections folder carries the same ad linkage as the Raw Footage original.
  • On an Approved Ads card, click "Footage used" to see the raw footage that went into that ad, listed in order — the ★ Opener (the clip the ad opens on) first, then each following clip. A Cut N badge means the order is the editor's exact cut order; a ~Scene N · % badge means it's an estimate (see below). Each card also previews the clip's actual scene used in the ad: the exact in/out of the cut where we've parsed it from the Premiere timeline, otherwise the portion that visually matches the ad. Opening the detail modal shows a Used in ad row with those in/out times — labeled exact cut or visual match (with confidence %), plus the ad's title in Top Openers.

The results fill the main grid and paginate exactly like "From this production". The set of footage is extracted from the editors' Premiere project files (.prproj): each project names the ad it builds (its J<job> number) and references the media clips it pulls in, matched to our indexed footage by filename. The order comes two ways: for ads cut directly in Premiere we parse the project's timeline and use the editor's exact cut order (Cut N badges). For ads built inside an After Effects comp the footage isn't on the Premiere timeline, so we fall back to an estimate — matching each of the ad's own scenes to the footage it most resembles (~Scene N badges, with a confidence %); a graphic/title scene matches no footage strongly and those clips fall to the end.

If a card has no button, that source isn't mapped — links exist only where we could parse a project file and match its media to assets we've indexed.

Hourly new-folder scan (2026-10-07). A folder created inside Il Makiage’s full file space could be picked up twice in one pass, which stopped the scan from recording new folders for the rest of that hour. Fixed; nothing is lost, the next pass picks them up.

Searches that stopped with "Internal server error" (2026-10-09). For about an hour, searches with a client and the default source filters timed out on both environments after the database refreshed its statistics and chose a slow plan; the search now pins the fast plan. Nothing about results changed.

Less text on the search page (2026-10-07). The hints in the bookmark tray ("Drag any result here…") and the API-log tray are gone, and the one-line explanation of how your query was read, under the weight chips, is larger so it can be read at a glance. Tooltips are unchanged.

Staff-creator production IDs (2026-10-07). Shoots by staff creators carry IDs in a slightly different shape, with four digits and a letter after the dash (for example ISTHD260915-0851A, where a second shoot the same day ends in B). Search now recognises them everywhere it recognises the usual IDs (like FRMDG260724-T10293): typing or linking one opens the whole shoot, its clips are labelled with the production ID, and the A and B shoots of the same day are kept apart.

Il Makiage's full file space (2026-10-05). Il Makiage re-shared its whole drive with us as a new top-level Iconik collection, Il Mak Full File Space, after the earlier copy under its footage folder was withdrawn in August. Search files it where its footage already lives: everything in it appears under Footage › Il Makiage_Footage, its 04_ADS folder is treated as the client's Approved Ads (so the ads filter, the ad badge and the ad analytics all see them as ads), and 03_PROJECT FILES is read only to connect ads to the footage they were cut from — its Premiere projects, auto-saves and render caches never appear as results. About 43,000 clips added or changed while the folder was withdrawn are being indexed; they arrive over the following hours as the sync walks the tree. Because the hourly sync reaches only about 150 of them an hour, the rest can also be loaded in bulk, in parallel with it: each clip is filed in exactly the same place the sync would file it, clips that are already searchable are left alone, and a clip only becomes searchable once it is fully indexed. The tree is mostly Premiere render caches (about 276,000 of its 365,000 entries), which the sync now leaves out of its listing entirely and never records, so the clips are reached in minutes rather than after a half-hour scan of files nobody will ever search for; the project files themselves are picked up once a day for the ad-to-footage links. Since 2026-10-08 the bulk loader keeps its list of the drive’s clips between runs, so restarting it (or giving it more capacity) goes straight back to loading clips instead of spending about two hours re-reading the whole drive first.

Edits in a client's own Premiere projects (2026-09-29). Some clients' footage folders also hold their editors' Premiere projects — finished edits built from footage we index (Il Makiage's creator composites, for example). We now read every sequence in those projects, not just ads, and match each clip only against that client's own footage. A clip of the same name elsewhere is rejected when its length doesn't match what the project recorded, and a clip with several different candidates is left unmatched rather than guessed. Through the API a clip lists the edits that use it, with where it sits in each and which part of the source was used. A project lists its sequences and their clips, can be found by file name, and its project file can be downloaded, so the footage app's Premiere plugin can import a chosen sequence directly. Each download says where it came from and carries a fingerprint of the exact file, so the plugin can tell when an editor has saved a newer version. When our shared drive doesn't have the file, the download tries every stored copy Iconik holds before giving up. Projects are re-read every night, so a new or re-saved edit shows up by the next morning. Premiere's automatic backup copies (auto-saves) are used only when the real project is missing, so the same edit is never listed twice. Result rows carry a has_sequences_using flag so a button only appears when there is something to show. Il Makiage's client-edit projects can't be read yet: our access to that folder was withdrawn in August, and they will be picked up once it is restored. They can already be found by their brief, because their names and folders spell it out: product line, market, video number and version, aspect ratio, cut number, test type and editor. Searching GREEN vid26 finds every cut of video 26 in the GREEN line, and vid26 never matches video 260. The same data gives a list of footage the edits use that isn't in our library. For each file it shows where the editor's project says it lives, and whether a file of that name exists here at a different length (a different take reusing a camera name) or not at all.

Following a link replaces the grid, but the result header shows a ← Back link that returns you to the exact view you came from — your search, browse view, or a previous lookup, at the same page. Chained hops (footage → ads → footage …) unwind one step at a time. Running a new search or browse clears the trail.

Can I browse openers and best-performing ads?

Yes — the Browse… pulldown next to the Search button runs analytics views that fill the results grid, and they respect whatever filters you have set (client, collection, producer, talent, media type):

  • ★ Openers — the raw-footage clip each ad opens on (the editor's first cut), ranked by the winning ad's lifetime spend; clips whose ads have no spend on record follow, newest first. Matched cards show the $ spend badge and name the winning ad. (API callers can pass sort:"thumb_stop" to rank by thumb-stop rate instead.)
  • ★ Top Openers (lifetime / 30 days) — opener clips ranked by the ad spend they opened for: each clip scored by the highest-grossing ad it opens, using lifetime or trailing-30-day spend. Cards show the ad's $ spend badge (tagged · 30d in the 30-day view), and each card previews the clip's actual scene that opened the winning ad (the exact parsed cut where available, else visually matched) and names that ad under the badges (↳ opens …).
  • Best ads · by spend (lifetime / 30d / 7d) — approved ads ranked by ad spend, highest first. Pick the window: all-time, the last 30 days, or the last 7 days (recent momentum). Each card's $ spend badge shows the spend for the selected window (e.g. sorting by 7d shows 7-day spend, tagged · 7d); the detail modal always shows lifetime, 30-day, and 7-day side by side.
  • Best ads · by thumb-stop — approved ads ranked by thumb-stop rate (3-second video plays ÷ impressions, from Meta). Cards show a % thumb-stop badge.
  • Winning Footage (lifetime / 30 days) — the raw footage used in the highest-spend ads, ranked by the spend of the ads it appears in — all-time or the trailing 30 days. Only genuine raw footage appears: assets classified as ads are excluded, so a misfiled ad export can't rank as "winning footage".
  • Footage in the thumb-stop window — the footage that played during the ad's opening ~3 seconds (the thumb-stop moment), ranked by the ad's thumb-stop rate.

Top Openers and Winning Footage load from a saved result (2026-10-23). These views (and Footage in the thumb-stop window) now open from a saved ranking instead of working it out from scratch every time. The saved ranking is refreshed only when something it depends on changes: the ads’ spend or thumb-stop figures (the daily performance sync), an ad’s cut list (when its Premiere project is re-read), or the clips themselves (approval, warnings, archiving, new footage) — and at least once a day regardless. The clips on the cards, their badges and warnings are always current. If working out a fresh ranking ever takes too long, the last saved one is shown rather than an error.

Spend and thumb-stop come from the daily Snowflake performance sync. Results paginate like any search; pick Browse… again to switch views or just run a normal search to leave the mode.

Top Openers and Winning Footage are prepared ahead of time (2026-10-23). These two rankings used to be worked out from every ad in the library each time the view opened, and at busy times that could take long enough for the view to give up. They are now kept ready in advance: rebuilt whenever an ad’s Premiere project changes (checked every 15 minutes) and again after the daily performance sync brings in new spend and thumb-stop figures. Your filters — client, collection, approval, media type — still apply each time the view opens. Since 2026-10-26 the views open from these prepared rankings; if they have not been prepared yet, the views work them out as before.

These signals also surface on regular results: any raw-footage clip that opens an ad gets a ★ Opener tag on the top-left of its preview, and opening an ad's detail modal shows its Ad spend (lifetime · 30d · 7d) and Thumb-stop rate. Approved ads with a known strategy brief also carry a 📝 script chip (see the next question).

Where can I see the script an ad was made from?

Approved-ad results show a 📝 script chip under the title (and a Script row in the detail modal) that opens the ad's strategy brief — the Google Doc the ad's script was written in. The link comes from the production job tracker: each ad's J<job>_<CLIENT> title is matched to its job, and the job's brief doc is attached automatically by a daily sync — about 98% of indexed approved ads resolve to their brief.

Opening the doc uses your own Google access — briefs restricted to specific people may still ask for permission. The sync also stores the brief's plain text for docs shared "anyone with the link", groundwork for searching ads by script copy.

Where the brief's text is available, the ad's detail modal also shows a Script match row — an automated daily comparison of the finished ad (its speech, on-screen text and visuals) against the brief's script: a 0–100 score plus a short verdict naming dropped or reordered lines, visual substitutions, and any phrases the brief itself flagged as blocked that still shipped. New approved ads are graded within a day, biggest spenders first.

Can I compare the two search engines side by side?

Yes — for admins: the Compare page (linked in the top nav on every page, visible to admin accounts) runs the same query against both engines in parallel: the production pgvector search on the left and Mixpeek on the right, with identical input parameters — set in the same Filters dialog you know from the search page (client, media type, Kind, asset source, producer, talent, SKU, page size, exclude-archived — plus keyword, duration, and a folder finder that matches any collection at any depth). The Mixpeek pane now pages like the rest of search — Prev/Next walk its ranked results instead of showing only the first page (it ranks a fixed-size pool, so paging ends when that pool does rather than when the library does). Filters Mixpeek can't honour are ignored on its side and its pane still returns results (client, media type and duration do apply; keyword and the folder finder do not, because Iconik Keywords and collection ancestry were never synced into the Mixpeek corpus — before 27 Jul 2026 a folder-scoped comparison wrongly showed an empty Mixpeek pane rather than an unscoped one). Each pane shows its own response time and result cards — previews play on hover (muted, looping, parked on the matching scene until you point at them) so the grid stays calm while you scan, and clicking a card opens a detail popup like on the search page: a full player seeked to the matched scene, the result's metadata, and clickable scene chips to jump between matches. If one engine is unavailable its pane shows the error while the other still returns. A Try: bar under the form offers refinement suggestions derived from the matching content clusters; clicking one re-runs the comparison with that query on both sides. Add ?q=your+query to the URL to share a comparison link.

What does the "Agentic" engine on the Compare page show?

Each Compare pane can switch its Engine to Agentic (admins; it only runs where agentic search is switched on). The pane searches with exactly the same query and filters as the other pane, shows the ordinary search results straight away, and then AI reviewer agents read each clip's scene description, speech, on-screen text and folder and re-rank the list. The agents first count the candidates your search has in its scope, then scan all of them in parallel — many reviewers at once, so a search with nearly two thousand candidates is scanned in well under a minute — and then keep searching with new wordings, round after round, with no limit on the number of rounds. A search ends only when every candidate has been scanned (all candidates scanned) or when its time or money limit is reached (time limit, spend limit) — never just because a few good clips were found or a round found no new ones. A visual check looks at frames of the top clips after the first scan and after every round that found new matches: a clip whose frames confirm it gains 2 points, one whose frames contradict the search loses 3. The frame check always runs on the matches, even when the search stops on its time or money limit: at the end it looks at every clip scoring 8 or higher that it has not checked yet (up to 60 at a time — any beyond that are checked by the next Dig even deeper); the strip says Verifying… while it does. That includes clips whose scores were still coming in when the search stopped: it waits a few seconds at most for the agents still reading, then counts every score that has arrived.

A strip above the results says what the agents are doing: while they work, Scanning 1,845 candidates · 93 shards · 41 done — how many candidates the search has, the batches of 20 they are split into for the reviewers, and how many batches are scored; then Done (or Stopped when a time or money limit ended it) with Scanned N assets — how many clips the agents have actually scored so far, across every round — how much video that is in all (41 h 12 m of video; clips whose length is unknown are left out of it) — M matches — how many of those score 8 or higher now, after the visual check — and, once the visual check has looked at frames, K verified — how many it confirmed; then the rounds, AI calls, the money spent and the seconds elapsed, and why the agents stopped. If the page loses touch with the server it says connection lost · retrying and keeps trying until the search finishes. The counts are for the whole search, not just the page you are on, and they match the scores on the cards. Under it, one short line shows each round: R1 60 scanned · 3 matches  R2 240 scanned · 1 match  R3 180 scanned · 0 — R1 is the first look at the ordinary results, every later R is a round of new searches; scanned is how many clips that round looked at and matches how many of them first scored 8 or higher in that round (point at a round to see how many searches it ran). It updates while the search runs. Under that, Looking for: shows how the agents understood your search, with the must-haves and nice-to-haves as chips.

When a search stopped on its time or money limit with clips it has not looked at yet, the strip ends with a Dig even deeper button (point at it: +3 min, up to +$5) — the pane's one button, in the place where It's deep enough was. Clicking it carries on the same search — it does not start over: clips already scored are never scored again, the agents try new wordings they have not tried yet, new clips join the same list, new rounds join the round line, and the strip goes back to Digging deeper… until it ends again by the same rules (every candidate scanned, or the extra time or money used up). You can press it again and again; once every candidate has been scanned the strip says all candidates scanned and the button goes away, and once a search has used 2 hours or $50 in total the strip says limit reached instead.

While the agents are scanning, an It's deep enough button sits at the end of the pane's strip. It's deep enough ends the scan; the frame check still runs on the matches found so far. Clips the agents are reading at that moment still get their scores, nothing new is started, and the button reads Stopping… (greyed out) until the search has stopped — also while the final frame check runs, since there is nothing left to interrupt then. The strip then ends with stopped by you and the K verified count, and in the button's place Dig even deeper picks the search up where it stopped.

When the re-ranked list arrives, every card carries a score out of 10 (green 8–10, amber 5–7, red below 5; unscored when no agent judged that clip by the end), how far it moved from the ordinary ranking (↑5, ↓2) and a one-line reason — point at it for the full reason (and the must-have it misses), click it to expand. On a clip the frame check looked at, click a score to see the reviewer's reasoning, what the frame check saw, and how the score was built: Reviewer 8/10 — …, Frames: confirmed — … (or contradicted, unclear, 2 of 3 must-haves seen), then one line per must-have of your search: a tick for what the frames showed, a cross for what they contradicted, a question mark for what they could not settle — a clip is confirmed only when every must-have was seen, and the list puts confirmed clips first, then partly seen, then unchecked, then contradicted — and Score 8 + 2 → 10, plus the must-have it misses when there is one. Clicking the card itself opens the usual detail popup, and Prev / Next page through the same reviewed list without starting a new search. The list reorders live as clips are scored: while the agents work, cards move into their new places as each batch of scores comes in (about once a second), new clips slide in and others move down — without reloading the page, so a clip you are previewing keeps playing, a reason you opened stays open, and you stay on the page you are on. A clip still waiting for its score shows no score yet. Raw footage comes first, because editors search for footage to build ads from: at an equal score a raw clip ranks above a finished ad, and a finished ad cut from a relevant raw clip in the list sits just below that clip, with below its source footage after its reason — unless you asked for ads (the Approved Ads filter, or words like ad, commercial or spot in your search).

If the server restarts mid-search, the search resumes on its own. When the server is updated or replaced while the agents are working, the strip reads Resuming… for a few seconds and then carries on where it left off — the same search, the same list, nothing already scored is scored again, and the time it spent waiting does not count against its time limit. Clips that were being scored at that moment are simply scored again by the resumed search. There is nothing to click; keep the page open.

The agents run on Google's Gemini 4 Argon in production (since 2026-10-13; Gemini 3.8 Flash before). An admin can switch all of them to Gemini 3.8 Flash or to Meta's Muse models (Spark 1.3 contributor, Wonder 1.0) and set the spend and time limits per model (Admin page → Agentic Search Model & Limits, per environment, with each model's list price, in effect for the next search); the frame check stays on a model that can see pictures. Since 2026-10-13 staging runs every agent on Muse Wonder 1.0 capped at $3 a search (hard stop $5).

If the agents need you to choose between two readings of your query, the question appears in the pane with buttons; pick one (or Skip to keep your query as typed) and the review carries on. Searching a production id, an exact clip name or a job code shows the plain results with no scores — there is nothing to judge. Where agentic search is off, the pane says "Agentic search is off in this environment" and the other pane works as normal.

An image or video search from the shared bar works the same way in an Agentic pane: it starts from exactly the image results the other pane shows, the agents look at your picture and at a keyframe of every candidate clip, and Looking for: shows how they described your picture.

What does the "Dig deeper" button on the search page do?

After an ordinary text search — or an image or video search — a Dig deeper button (with a figure digging a hole) sits in the results bar, right after the result count ("1–9 of 72 assets · …") and before the Prev / Next pager. Clicking it asks the same AI reviewer agents as the Compare page's Agentic engine to work on exactly the search you just ran — same words, client, folders and filters. They first count the candidates in your scope, then scan all of them in parallel — since 13 October 2026 with as many reviewers working at once as it takes to finish the scan and the frame check of its matches inside the search's time limit: a big search gets more reviewers at once, and the search keeps the time the frame check needs free for it. They read each clip's scene description, speech, on-screen text and folder and score it out of 10 against what you asked for, then keep searching with other wordings round after round — no limit on rounds — and re-rank the list. A search ends only when every candidate has been scanned (all candidates scanned) or at its time or money limit (time limit, spend limit). A visual check of the top clips' frames, after the first scan and after every round that found new matches, raises the clips it confirms and lowers the ones it contradicts — and the frame check always runs on the matches, even when the search stops on a limit: at the end it checks every clip scoring 8 or higher it has not looked at yet (up to 60; the strip reads Verifying… meanwhile), so K verified covers your matches — also the ones whose scores were still arriving when the search stopped. It only runs where agentic search is switched on.

Each search runs on its own worker, apart from the search service and from every other search, so a release never interrupts it.

Your results stay on screen while they work, under a strip that reads Digging… with Scanning N candidates · S shards · D done (the candidates found for your search, the batches of 20 the reviewers take, and how many batches are scored), the money spent so far and the seconds elapsed, then Looking for: with how the agents understood your search. As soon as the agents have scored something, the grid switches to the re-ranked list: every card shows its score out of 10, how far it moved (↑5, ↓2) and a one-line reason (point at it for the full reason, click to expand). The list reorders live as clips are scored — cards move into their new places as each batch of scores comes in, without reloading, so a clip you are previewing keeps playing and you keep your place; a clip still waiting for its score shows none yet. On a clip the frame check looked at, click a score to see the reviewer's reasoning, what the frame check saw, and how the score was built — for example Reviewer 8/10 — …, Frames: confirmed — …, Score 8 + 2 → 10. Prev / Next then page through the re-ranked list, and the page you are on stays put while it reorders. Raw footage comes first: at an equal score a raw clip ranks above a finished ad, and a finished ad cut from a relevant raw clip in the list sits just below that clip (below its source footage after its reason), unless you asked for ads — the Approved Ads filter, or ad, commercial or spot in your search. The agents now score clips as fast as the search can fetch them: in tests on 9 October 2026 a search scanned about 3,500–5,200 clips — 30 to 48 hours of video — in its three minutes for $1.20–1.90, and with the faster settings being tried on the test site 5,300–7,600 clips — 48 to 74 hours of video — in two and a half minutes for $1.80–2.50; every score was saved before the search said it was done, and the page stays as quick to refresh while it works however many clips it holds. A broad search can keep digging until its money limit. If the agents need you to choose between two readings of your search, their question appears above the results with a button per reading (and Skip).

When the search ends, the strip counts the work for the whole search: Scanned N assets (clips the agents have scored, across every round), how much video that adds up to (41 h 12 m of video, 12 m 5 s of video; clips whose length is unknown are left out of it), M matches (those scoring 8 or higher now, after the visual check) and, once the visual check has looked at frames, K verified — the same numbers whichever page you are on, and they agree with the scores on the cards. The line under it shows each round, for example R1 60 scanned · 3 matches  R2 240 scanned · 1 match  R3 180 scanned · 0: R1 is the scan of every candidate your search found, each later round a new set of searches; scanned is how many clips that round looked at and matches how many first scored 8 or higher in it (a clip the visual check later marks down stays counted in its round, but not in M matches). Rounds that find no new match do not stop the search — only scanning every candidate, or the limit, does. When the search stops on its time limit or spend limit with clips still unscanned, the Dig deeper button itself reads Dig even deeper (same figure; point at it: +3 min, up to +$5). Clicking it carries on the same search for up to 3 more minutes and $5 — it does not start a new one: nothing already scored is scored again, new clips join the same list, it stops by the same rules, and while it runs the button reads It's deep enough. Press it as often as you like. When every candidate has been scanned the strip says all candidates scanned, and once a search has used 2 hours or $50 in total it says limit reached; then the button reads Dig deeper again, and clicking it starts a new search. (There used to be a separate Need more matches? button in the strip for this; since 9 October 2026 it is the one button.) The page keeps following a search for as long as it runs; if it loses touch with the server the strip says connection lost · retrying until it is back.

It's deep enough. While the agents are working, the Dig deeper button itself turns into It's deep enough (same figure). It's deep enough ends the scan; the frame check still runs on the matches found so far. Clips the agents are reading at that moment still get their scores, nothing new is started, and the button reads Stopping… (greyed out) until the search has stopped — also during the final frame check, when there is nothing left to interrupt. The strip then reads Stopped with your matches, K verified and stopped by you, and the button reads Dig even deeper — clicking it picks the search up where it stopped. (If every candidate had already been scanned, it reads Dig deeper instead, and clicking it starts a new search.) ← Back to results works as before.

If the server restarts mid-search, the search resumes on its own. When the server is updated or replaced while the agents are working, the strip reads Resuming… for a few seconds and then carries on where it left off — the same search, the same list, nothing already scored is scored again, and the time it spent waiting does not count against its time limit. Clips that were being scored at that moment are simply scored again by the resumed search. There is nothing to click; keep the page open.

← Back to results at the right end of the strip puts your ordinary results back instantly, exactly as they were. The button is greyed out while a run is in progress; any new search, filter change or image search stops the run. It does not appear for lookups (a production id, an exact clip name, a job code or SKU), for "More like this", on Mixpeek or in the other result modes — there is nothing for the agents to judge there.

With an image or a video. On the Image tab, after you search with a picture or a short clip, Dig deeper works on exactly that search — the same file, client and filters. The agents look at your picture (for a video, a few frames spread across it), write a one-line description of what to look for (shown after Looking for:), then look at a keyframe of each candidate clip as well as its description and score how well it matches both your picture and that description. They then search further, round after round, until every candidate is scanned or the limit: more of the closest-looking footage, more like the best clip found so far, and searches in words for what the picture shows. If your picture could mean different things (the product, the setting or the style), they may ask which one you want. Dig even deeper works the same way on an image search. Your picture is only kept in the server's memory while the search runs and for half an hour after (so Dig even deeper can still look at it); it is never stored. Where agentic search is off, clicking it says "Agentic search is off in this environment" and your results stay as they are.

How do I teach the search which clip is the right one?

Thumbs up / down on a clip teaches the ranking for that query immediately; Dig deeper's verified matches do the same automatically. Every result card of a text search has two small buttons at the right of its row of actions: a thumbs-up (Good match) and a thumbs-down (Bad match). Click the thumbs-up on the clip that is the best answer — even if it sits at rank 88 — and the next time anyone runs that search (on any computer, within a few seconds), that clip comes first. A thumbs-down pushes a clip to the bottom of that search. The button you pressed stays highlighted; click it again to take your vote back. Your votes come back when you search the same words again.

A thumbs-up is the strongest single signal there is — stronger than a clip the Dig deeper agents confirmed by looking at its frames, so the clip you picked comes first even among the agents' confirmed matches. When a Dig deeper search ends, every match the frame check verified counts like a thumbs-up for that search, a match it did not get to check counts a little, and a clip the frame check contradicted counts like a thumbs-down — so the ordinary search learns from the agents' work without anyone clicking anything. Downloads, adding a clip to a collection and the reviewers' labels on the Feedback page keep counting as before; all of it adds up per search and per clip, and the clips with the most support lead.

Votes apply to the same wording, and only inside the search's own client, folders and filters. A clip the Dig deeper agents confirmed for a search is brought back for that search even when the ordinary ranking would not reach it (so are clips with a thumbs-up), and it leads the results — but never in a search scoped to another client, another source or filters it does not pass. A very close rewording borrows only the strong ones, up or down — a thumbs-up or thumbs-down, a match the frame check confirmed or contradicted, a clip added to a collection — never a weak hint. Lookups, browsing a folder and picture searches show no thumbs — there is no query to teach.

Behind the scenes a nightly job can also replay past searches that seem to have gone badly (results that scored low, a search typed again in different words a moment later, a reported bad result) through Dig deeper, so their verified matches teach the ordinary search too. It ignores the admins' own searches and is switched off until the owner turns it on.

Can I search by supervising producer?

Yes — just type a supervising producer's name in the search box. If the query contains a known producer (e.g. Dylan Quintal b-roll unboxing), results are restricted to assets that producer supervised, and the rest of your phrase (b-roll unboxing) still drives the semantic ranking within that set. Type just the name to see everything they supervised.

You can name more than one producer — results then include assets supervised by any of them (e.g. Dylan Quintal Alma Sever interviews). It's automatic — no special syntax — and works because every asset carries its Supervising Producer from Iconik metadata. You can also see the producer on the asset details.

Prefer to pick from a list? The Filters modal has a Supervising producer multi-select. Its options are scoped to your current selection (client / asset source / chosen collections), so it only offers producers that actually appear in that scope; pick one or more (empty = All) to constrain results.

For integrations, every search endpoint also accepts an explicit optional supervising_producer filter that takes one or more names (a list, a single name, or comma-separated).

Talent names work too. If a word in your query looks like a talent's name (e.g. Hannah Lee unboxing), assets tagged with that name in their Keywords are boosted to the top of the results — the rest of the query (unboxing) still ranks within and around them, and nothing is excluded. This soft prioritization fires automatically from the query text.

For a hard filter by booked talent, the Filters modal also has a Talent multi-select (alongside Supervising producer). Its options are scoped to your current selection (client / asset source / chosen collections) and pick one or more (empty = All) to restrict results to assets that booked that talent. The talent list comes from each asset's booked-talent metadata (talentname); naming a known talent in the query auto-applies the same hard filter. Talent entered in Iconik is picked up by the sync automatically — including names applied around the moment a shoot lands (the sync double-checks recent productions against Iconik directly, so a name entered as metadata always becomes searchable within the hour). For integrations, every search endpoint accepts an optional talent filter (one or more names — a list, a single name, or comma-separated).

Product SKUs work the same way — always as an exact match. Assets tagged with a product in Iconik (the SKUs field — labeled SKU Name — plus every pod's SKU Tag field, matched by name automatically; values like TurboStyle or Crossover Bra) are searchable in every mode: the Filters modal has a SKU multi-select (scoped to your current client / source / folder selection; empty = All) that restricts results to assets tagged with any chosen SKU — and it composes with your folder selection, so you get that SKU's footage within the collections you picked. Typing a SKU by itself into the search box (capitalization doesn't matter — turbostyle) skips semantic search entirely and returns every asset tagged with it, newest first, again respecting your client and folder filters. For integrations, every search endpoint accepts an optional skus filter (a list, a single value, or comma-separated). SKUs now cover far more of the library than Iconik alone does. The product field in Iconik is attached to assets far more often than anyone fills it in — Dr. Squatch had it on 20,892 clips and a value on one — so a SKU filter used to hide almost everything. The product list now comes from the post-production SKU sheet, and each client's own list is the only set of values that client's footage can be labelled with: nothing is invented, and a product an editor tagged in Iconik still wins. Where the label was missing we work it out from the client's own filing — a folder named for the product, a shoot code shared between a client's collection and its raw footage, or the product name in a clip's title or folder path. Coverage went from about 9,000 clips to over 40,000 (Home Chef about three-quarters of its library). Home Chef is a special case and is treated as one: it is the only client whose folder tree is organised by product, so its tree is read directly and translated into the products on the sheet. Some listed products can never be worked out from filing alone — Blissy's Box, Red and Blue, or Home Chef's plating variants like OvenR_Plated — so they are simply not offered: a product you can pick that returns nothing is worse than one you never saw. The full list, zeros included, is still available to integrations that want it. Products can now be filtered by their parts. For Home Chef a product is really two things — a line (Core, Family, Tempo) and a product (Oven-Ready, Express, Meal Kit, Extras, Fast & Fresh, General) — so both are filterable on their own: all Core footage whatever the meal format, or every Oven-Ready clip across both lines, or the exact pair. Picking several values inside one group widens the search; picking values in two groups narrows it. Other clients keep a single product list today, and can be given their own groups the same way. These filters apply to every kind of search — typed queries, exact titles, job numbers, product names, and "more like this" — so narrowing to a product never quietly stops applying because of how the query happened to be phrased. A product filter always needs a client selected: the same product name can belong to two different clients, and applying it across everything would show you someone else's footage. You can now search with no words at all. Set any filter — a product, a line, a folder, talent, a shoot-time range — and leave the search box empty: you get everything that matches, newest shoot first. (A client on its own doesn't count: that's the whole library, and there'd be nothing to answer.) Results carry no relevance percentages, because nothing was scored — they're ordered purely by when the footage was shot. This is how the footage app's folder browsing already works for most of its no-typing searches — those now come back in a stable, newest-first order instead of an order derived from an empty query. The folders you pick decide which products you are offered. Select folders and the product list is what is actually in them, with the counts you would get — so a list no longer offers a product that has nothing in the folders you are looking at. With no folders selected, your brand still narrows the list as before. A product's count is what you can actually open. The number beside a product used to include clips Iconik holds but we were never able to process — no preview, or a file type we cannot read — so Wegovy Pill promised 629 and the grid held 89. Counts now include only footage the search can return. You can now ask for a product that is actually ON SCREEN. A product label can mean two different things, and until now the filter could not tell them apart. For CookUnity a meal is attached both because the shoot's menu included it and because the dish's name is legible in the footage — so picking Chicken Enchiladas with Tomatillo Sauce returned 345 clips of which only 40 actually show the dish; the other 305 were other footage from a shoot that happened to feature it. Not wrong data — the wrong question. Each product-style filter can now be narrowed to the evidence you mean (“shown on screen only”), and the count beside it changes to match, so the number and the grid stay in agreement. It applies per filter rather than to the whole search, deliberately: some filters exist only as client tags (CookUnity's Studio Directed, for example) and a single global switch would have emptied them the moment you used it on a different filter. Leaving it on with nothing picked is a real request too — every clip that shows a meal on screen, whichever meal. If a request ever asks for an evidence kind that doesn't exist, the filter is ignored rather than applied — you get the wider result instead of an empty grid — and the response now says so on searches as well as on the filter counts, so a typo in an integration cannot quietly widen a search without saying it did — and it says the same thing in the same way on searches and on filter counts, including the reason, so whoever reads that log can tell "the filter was ignored, these results cover everything" from "there is nothing there". Filter counts reflect what you've already picked. Each value shows how many clips you'd get if you clicked it, under your other filters — and the group you're editing is counted as if its own selection were lifted, so you can always see what switching to a different value would give instead of watching every other option drop to zero. Counts follow your filters, not your typed query: a text search returns the best matches rather than a fixed set, so there's no honest number to show for it. HelloFresh now has its own filters. Two are live today: Footage source — whether TubeScience shot the clip (it carries a Production ID) or HelloFresh supplied it (it carries the Client Footage note) — and Market, read from the country folder the clip is filed in (US, UK, DE, FR, AU, IT, DK, NL, IE, CH, NO, ES, BE). Clips that carry neither signal, or sit in a folder that names no market, are simply not labelled rather than guessed. A Meal filter is wired up and empty: unlike CookUnity, HelloFresh meal names are almost never legible on screen, so the only source is the team's visual reference deck — once that deck is shared as a sheet (meal, Production ID, market), meals load from it, including the Nordic-specific ones that exist nowhere in the folder tree. When a SKU is really a word. If the footage uses a tag's text far more widely than the tag does (at least twice as many assets say it in descriptions, transcripts or on-screen text as carry it — glamwave at L'Ange: 32 tagged, 95 saying it), typing it no longer returns only the tagged listing: the regular visual search runs, and every result either shows the word or carries the tag, so the tagged clips stay in beside the ones that say it. Product ids like turbostyle (far more tagged than said) keep the exact listing.

Say what you DON'T want with "no". Putting no (or without) before a thing removes it from the results — no face hides shots where a face is visible, product demo no logo keeps the demo but drops shots showing a logo, and you can stack them: beach no people no cars. Articles are fine — without a person works the same as no person. Asking for no person / people excludes any visible human (a woman, a man, a face — not just the literal word), so showing perfumes without a person returns pure product shots; a specific word like no man stays specific. The rest of your query still searches normally (beach no face = search "beach", exclude faces). Exclusions are matched on what's visually in frame (detected objects, the scene description, on-screen text) — not on speech, so someone merely mentioning "face" won't hide a faceless shot. The results header shows what's being excluded.

List several things and you get shots with all of them. dog and cat returns scenes containing BOTH a dog and a cat (not just one); woman with a dog, coffee cup and laptop work the same — and the connector is optional, dog cat also requires both. The default is always AND; say or when you want alternatives — dog or cat returns shots with either. A thing you name is only required when footage in your selection is actually labelled with it (or with a known synonym): a word no clip is tagged with — blemish, flaws — still shapes the ranking but no longer removes footage. Name an emotion (frustrated customer, happy, crying) and results show a person expressing it — unless you name an animal (happy dog keeps the dog). Name a gender (woman running, man lifting weights) and the person shown matches it — and it must be the actual person, not just a hand or arm close-up (body-part-only shots are dropped when you ask for a person). Say how many people and you get that many. A plain plural doesn't count heads: women applying makeup returns any woman applying makeup, and kids returns kid footage whether one child or several is in frame. Name a pair — two kids, 2 women, both women, a pair of dancers, a couple of friends — and results need at least 2 people in frame (a lone subject won't match). Three or more, or a group — three women, group of men, crowd, team, several women — raises the bar to 3+ (a pair no longer counts). Numbers inside product names, codes or years (2-pack, ILMKG2109, summer 2024 kids) and ages (three year old kids) don't count as people. Possessive product phrasing is left alone — men's shorts or kids' toys still return the apparel, not a crowd — and non-people quantities like several dogs aren't affected.

What you can see is decided by the folders you pick, not by your brand. Every folder belongs to exactly one client by its ID, so the folders you select say whose footage this is without anyone having to translate a brand name into a client — a translation that was wrong for two brands for months. If a search arrives with no folder, no client and no asset, it returns nothing rather than everything — which is why the app asks you to pick folders when your selection is empty, instead of searching everywhere. Your brand only ever sees its own footage — and now every brand is actually recognised. When the footage app searches, it identifies you by a brand ID, and we translate that into the client library it may read. That translation used to be learned from how people used the tool, which got two brands wrong (HelloFresh US was shown CookUnity's library), and was then pinned to a hand-copied list of 84 brands — while the footage app had grown to 152. Brands missing from that list searched and got nothing, with no explanation. The list is now read from the footage app itself and re-checked every hour: 139 of 152 brands resolve to a library, including every market and product line (all 20 HelloFresh markets, the WW markets, every Home Chef menu, the SoFi and Onnit lines). A market inherits its parent brand's footage only when the parent mapping was confirmed by a person — nothing is guessed from a name that merely looks similar. The remaining 13 are brands with no footage here at all (demo and test tenants, and new brands whose material has not been filed yet); they deliberately return nothing rather than the nearest-looking client's material. If your brand shows an empty library everywhere, that is what it means — tell us and we will map it.

Approved ads were showing up under the wrong client — that is fixed. A sync entry was accidentally set up to watch the entire Approved Ads library as if it belonged to one client, and it relabelled 8,083 approved ads — The Farmer's Dog, Honeylove, Thrive Market, Nectar, Dr. Squatch and about forty others — as that client's. Those ads were reachable from the wrong client's searches and missing from their own. The watcher can no longer be pointed at a whole library, and 7,997 of those 8,083 ads have been moved back to the client whose folder they actually live in — the 86 that stayed are genuinely Uber's. The clips inside them moved too, and were re-filed into their new client's visual groupings, so an ad now appears for its own brand in every mode of search rather than just in the asset list. Every request now names exactly one client. Searches that arrive without one — there were 94 in a week that searched the entire corpus — are refused rather than answered across everybody, and a request that names two clients at once (one brand's product filter with another brand's folder) is refused too. If you see "every request must be scoped to exactly one client", pick a client or a folder and search again. Production IDs are scoped too. Typing a shoot's production code jumps straight to that shoot — but only if it is yours; another brand's code now returns nothing rather than their footage.

Restricted footage never shows up. Footage rejected in Iconik review (NOT_APPROVED) or living under a DO NOT USE / quarantine folder is excluded from all searches automatically. State words matter. kibble mixed with fresh food returns the two foods visibly combined in one shot — not one bowl of each; the same holds for topped with, poured over, coated in, drizzled on… If nothing in your scope shows the combined state, the search falls back to requiring both things present rather than returning nothing. Position words matter. dog jumping over a log returns shots where the dog is over the log — not under it. Relationship words like over, under, above, below, behind, in front of, on top of are honored (they used to be ignored by text search).

Shot language works. The full standard shot-size ladder is understood — extreme wide shot, wide shot, medium wide shot (also cowboy shot / three-quarter shot), medium shot (waist shot), medium close-up, close-up, extreme close-up — each is a distinct framing, so medium close-up returns MCUs specifically, not all close-ups. Special framings work too: POV unboxing, talking head, hero shot of the bottle, over-the-shoulder, insert shot, screen recording or drone shot. Results are restricted to that camera framing — every analyzed segment carries a shot type, so the framing is a real filter, not a lucky word match. Ambiguous single words only count with a companion (wide shot, insert shot — a "wide variety" won't trip it). Historical footage was seeded from descriptions that name the framing; coverage deepens automatically as clips are processed — and a shot phrase never dead-ends: if the framing filter would return nothing (coverage still thin for that shot type), the search automatically retries without it and flags the response (shot_types_relaxed). API callers can also pass shot_types directly — an explicit filter is honored strictly, including a hard zero. POV means you don't see the looker: a POV search (POV unboxing, first-person view of the shelf) returns shots of what is being looked at — hands can appear in frame, but scenes showing the looker's face or figure are excluded.

Name a thing and it will be in the results. Search dog and every result shows a dog (not a dog-food bag or someone saying "dog"); the same holds for any concrete object the system recognizes. Words match footage phrasing, not just the literal term — senior makeup or young talent find scenes described as elderly, older, youthful, teenage… and when a search comes back with only a handful of results, the engine learns the footage vocabulary for your words on the spot and widens the match automatically (colors and materials stay exact — red dress stays red). Some clients have their own word list: inside Little Spoon, baby eating biteables, blw or babyblends put clips described as finger food, self-feeding or puree first, and every other result stays below them; its SKU filter now covers clips filed or titled by product line (Babyblends counts as Organic Purees; Smoothies is new) — other clients' searches are unchanged. Abstract and production-value words (like quality, vibe, studio quality, cinematic) are left to normal ranking so they never empty your results — studio quality footage of dog requires the dog, and "studio quality" shapes the ranking rather than filtering. Colors and materials count too — red dress won't return a blue one, and wooden table requires the wood.

Emotions and behaviours are matched on what the clip shows. For clients whose footage has scene attributes (Little Spoon first), emotion and behaviour words such as crying, happy baby, picky eater, refusing, pushing away food or spitting out — and messy, high chair and product names — are matched on what each clip actually shows, not on whether a word appears in its description. A crying baby counts even if no one wrote "crying", and a clip that is described as crying but shows a happy baby does not. Clips that have not been analysed yet are matched the usual way, and if too few clips qualify you get the usual results instead of a short page. The cards also open on the moment the baby starts crying or smiling, and the Filters offer Expression, Feeding, Product on screen and Setting for those clients.

Visual-style queries return the visuals. Ask for visual effects, vfx, motion graphics, animation, green screen, glitch or CGI and results must actually show effects-style footage (animated overlays, transitions, renders) — a clip of someone merely talking about VFX doesn't qualify. Saying the talking is what you want (talking about visual effects) turns the filter off. Searching for an action returns the action, not talk about it. When your query names something happening — mixing the moon brew, pouring coffee, applying serum, dog jumping — the ranking leans hard on the video (what's visibly happening on screen) and all but ignores the spoken audio, so you get the shot of the action itself rather than a talking-head clip where someone merely mentions it. If you actually want the talking (founder explaining the mixing process, testimonial about…), naming the talking keeps the spoken words in play. Words that are both a thing and an action — brush, curl, spray, blend — lean toward the action (footage of brushing, not a brush sitting on a shelf), while product names built from them (dry shampoo, face mask, hair gel) still return the product.

Ambiguous queries get a "Did you mean…?" confirmation. If your search looks like it might name a known talent, producer, SKU, or client — a first name only (caroline unboxing), a near-miss spelling (hanna lee, turbo style), or a client's name inside a longer phrase (farmers dog testimonial) — a small dialog pops over the results asking which you meant. Every suggestion is checked against your current filters first — the dialog only offers interpretations that actually have results in your selected client/folders. Picking an interpretation applies it as the matching filter and re-runs the search; choosing "No — search as typed" (or pressing Esc) keeps the literal results and won't ask again for that query this session. Exact matches never ask — a full talent name or SKU applies automatically, as always.

What does the score badge mean, and why the breakdown?

In hierarchical mode the left Hierarchy panel starts collapsed — click its header to expand the cluster tree.

Results are ranked by a combined relevance score — a weighted blend of how well the query matches each signal: the visual frame, the spoken transcript, on-screen text (OCR), detected objects, and the scene description. The weights shift with the query's intent (a spoken-quote query leans on transcript; "$50 OFF banner" leans on OCR).

The badge shows the total of that blend, and the small chips under each result show how much each signal contributed — so you can see why something ranked. (The chips sum to the total.) Because the badge is the same number used for ranking, results always read high-to-low.

The badge color reflects the reliability of the visual match specifically: green ≥ 0.68, amber 0.64–0.68, red < 0.64. So a result that ranked highly on transcript/OCR but is visually weak still shows amber/red — a cue that the picture itself may not be what you asked for. These cutoffs are calibrated to this corpus's narrow ~0.60–0.73 cosine band and are interim.

What does the green NEW tag mean — and does freshness affect ranking?

Video comes first. If your query names an action — unboxing, woman applying moisturizer, leading someone — results are restricted to video, because a still can't show the action happening. For everything else, videos are simply ranked above stills rather than filtered, so photos stay available one row down. Ask for stills and you get them: use the Image/Video tab, or say photo, still, packshot or screenshot in the query — your choice always overrides this.

Photos have to show what you asked for (16 Oct 2026). When a search names something that must be in the shot, like beef, photos are now checked the same way videos are, using what the photo was described as showing. Before, photos skipped that check, so a Home Chef search for beef filled its later pages with chicken, salmon and shrimp photos. A photo that has no description yet still appears. Beef also counts the dishes footage is usually described as — steak, brisket, meatloaf, meatballs, burgers and meat patties — so real beef shots aren't lost. And clips that actually show what you asked for now come before clips that only mention it in on-screen text, such as a cooking-temperature chart that lists "ground beef".

Searching for talent by race. A phrase like black man, asian woman or latina talent is now understood as a person of that race, matched against how the scene is actually described — not as a colour, which is why it used to return men in black shirts. Colour searches still work normally (woman in a black dress is unaffected). One caveat worth knowing: scene descriptions name race far more often for some groups than others, so if nothing is described that way the search widens back to everything rather than showing you an empty page — check the description before assuming a result matches.

Takes of the same scene now sit together. When a shoot has several takes of one setup, they used to scatter through the results — the same shot at positions 2, 7 and 11, with no way to tell which was the keeper. They're now grouped as a block with the latest take first, placed where the best-matching take ranked. Result cards can also show which take they are and how many takes of that scene exist in the whole library, not just on the page you're looking at. Where the take can't be read from the filename with confidence, nothing is shown rather than a guess.

Why did a search return only a handful of clips? Until 28 Jul 2026 a filtered search (anything scoped to a client, which is most searches) could come back with far fewer clips than exist — sometimes none — because the vector index stopped looking too early once your filters were applied. That's fixed: the index now keeps searching until it has filled the page. Separately, shot-type words like POV no longer act as a hard filter unless the footage in that client is actually labelled that way in enough volume — those labels are auto-generated and can be sparse or wrong, so the wording now guides ranking instead of cutting results away. Scene descriptions, not labels, are what searches trust.

The green NEW tag marks footage that is fresh for its client. "New" adapts to how much each client shoots: an asset counts as new until its client has produced roughly 300 newer assets — so for a high-volume client that's about the last 3 days of footage, while for a client shooting a few clips a month it can be up to 45 days. Ask for it in the query: new dog footage, newest ugc, latest testimonials or recently added b-roll restrict results to NEW assets (and if your current folder has no fresh footage, the search automatically widens back rather than showing nothing). Names and offers aren't confused for freshness — new york or new customer offer search normally. The tag appears wherever results show up — search cards, “From this production”, “Ads using this” / “Footage used” lookups, the Top Openers / analytics views, the Compare page, and the feedback viewer, and API responses carry the same is_new flag on every result.

Ranking gets a matching gentle nudge: newer footage is boosted slightly (fading smoothly as it ages past its client's freshness window), and clips already used in ads — or repeatedly downloaded a while ago — are eased down a little, giving unused material a chance to surface. The nudge is deliberately small: it breaks ties between similar matches but never outranks a clearly better result, and the score-breakdown chips show its exact contribution (recency / usage rows). It never applies where it would fight your intent — "more like this", SKU, talent, and producer searches are exempt — and results an editor was already taught to prefer (reviewer OKs, downloads) keep their learned position. API callers can opt out per request with recency_bias: false.

Can I search by filename?

Yes — paste the whole name or just a piece of it. An exact title returns just that asset and nothing else (with or without the file extension); a bare J-code (J814, J814V1) returns the ad and all of its variants — searched across your whole client (the client you picked, or the client your selected folder belongs to), since a job's deliverables live in several places in the client's tree; it never crosses into another client (job numbers repeat per client), and the response notes when it looked beyond your folder; ad results are labeled — an APPROVED AD tag on the card and the header line (Ad J814 · 4 variants · approved ads); a partial name (a truncated prefix, a name without the extension) finds every title containing it, newest first. If your local filename differs from the library's (renamed on export), the search falls back to the most distinctive part — camera-file tokens like IMG_5231 or long digit runs — so Lynne-Myers_IMG_5231 still finds IMG_5231. And when the folder you have selected doesn't contain the clip, a filename-looking search automatically widens beyond the folder (the response notes it) instead of returning lookalikes.

Can I share or bookmark a search?

Yes. When you run a text search the query is written into the page URL — e.g. /?q=red wine. Copy that link to share it, or bookmark it; opening a URL with a ?q= parameter pre-fills the search box and runs the search automatically. The client you pick is in the address too (/?client=Il Makiage), so a shared link opens on that client, and it stays selected the next time you open the search page on this browser (choose All clients or Clear filters to drop it).

Why don't I see clapperboard / slate shots — and can I find them?

Production slates / clapperboards stay in the index but are hidden from normal results — each shot is checked by vision once and slate shots are dropped, so they don't crowd out real content. The verdict is cached, so this check never slows your search: the first time an asset shows up it is classified in the background, and from then on it's filtered instantly. If no frame of a shot can be fetched, it can't be checked: it stays in results and is tried again the next day.

If you actually want them, ask for them explicitly — search slate, clapperboard, or slate clap and the results return only slate shots. (Leading slates on newly-ingested footage are also trimmed at ingest; this runtime check covers everything already indexed.)

How do quotes work in a search?

Wrap text in "double quotes" to search for that exact phrase in the transcript — e.g. she says "best decision of my life". Only the quoted part is phrase-matched; surrounding words ("she says") are treated as context and ignored for the phrase match.

The Kind filter pills (Raw footage / Ads) classify assets by what they are — parsed from titles, the editors' Premiere projects, and naming grammar — rather than which folder they sit in, so they work even in mixed folders. Leave both unchecked (or both checked) for everything. Independently, the Raw Footage source now always excludes assets classified as ads — misfiled ad exports in footage folders won't appear in footage-only views. Borderline cases are additionally audited by an LLM that reads each asset's transcript and on-screen text against a formal advertisement definition (paid/sponsored disclosures, transactional calls-to-action, sponsor-controlled messaging — production quality is deliberately ignored); when it isn't sure, the asset stays raw footage. Manual reviews always outrank the automatic layers.

End the query with in the folder <NAME> to scope it to a folder — e.g. soundbite of shampoo or conditioner use in the folder PROSE260209-S10605_CURLY. The folder clause is stripped from the search itself and turned into a collection filter (name matched case-insensitively against the Iconik collection tree, with typo tolerance — a near-miss like PROOSE260209… still finds PROSE260209…; if several folders share the name, all of them are included). The result line shows 📁 name (N folders) — or tells you when no folder matched, in which case the search runs unscoped. Variants like inside/within/under folder X and quoted names work too. The Iconik Collections tree in the filters tray is client-aware: selecting a client narrows the tree to that client's folders (and quietly un-checks any folder outside the client); choosing "All clients" brings the full tree back.

If the exact phrase isn't spoken anywhere, the search doesn't come back empty — it falls back to a regular semantic search with extra emphasis on the transcript, so you still get the closest spoken matches. Quotes are no longer a strict keyword filter.

What's in the asset detail popup?

Clicking a result opens a popup with the video, its scenes, and metadata. The "Footage used" view shows the ad's Premiere project as the FIRST card (click opens it in Iconik) and links it in the header. Project-file previews show the project's poster frame at its true aspect ratio (letterboxed, never stretched), with a 🎬 tile when no poster exists. Approved-ad results also carry a 🎬 project chip (and a Project file row in the popup) linking to the Premiere .prproj the ad was cut from, next to the 📝 strategy-brief chip. It also shows the Collection path — the asset's full place in the Iconik hierarchy (root → … → its folder), each level linking back to the collection in Iconik. This is the complete ancestor tree (variable depth, walked to the leaf), the same chain used by the collection filters, so what you can filter by and what you see here always agree. The collection filter itself shows the full Iconik hierarchy — both true roots (Footage and Client Collections), every client's footage, down every sub-folder — so you can scope a search to any folder at any depth. That tree is rebuilt hourly — it had been stuck on a copy built in June, so folders created since were missing from the filter entirely — and folders with nothing in them are left out (they filter to nothing), which makes the panel load several times faster. Re-opening a page you've already loaded reuses the copy your browser already has, so it appears instantly. The footage app now gets the complete list of a brand's Client Collections from us — every folder in that brand's tree, not just the ones with indexed footage. Before, a brand could be shown 4 of its 133 folders, and folders you could open from a search result were missing from the picker entirely. Client separation is absolute, and one mapping had it wrong. The footage app identifies a client by its own brand id; ours was learned from past search reports rather than from the brand itself, and two brands pointed at the wrong company's library — HelloFresh US at CookUnity, and Home Chef - Family at Il Makiage. Both are corrected, and the mapping is now checked against the footage app's own brand names: if a mapping contradicts the brand it claims to be, the search returns nothing rather than another client's footage. Underneath that, every folder now resolves to the client that OWNS it by following the folder tree upward — by identity, not by name. Folder names repeat constantly across clients (02_Openers exists under 44 of them), so a name can never say whose footage you are looking at; the path to the top always can. That lifted the share of folders with a known owner from 29% to 92%, which is what stops a scoped search from having to guess. Mappings a person has confirmed are exempt from that check, because a brand is often a market or product line whose name looks nothing like the folder its footage lives in — WW USA (US) and Weight Watchers share no words at all. Folders that would open to a Forbidden error are left out as well — a handful hold another client's footage by mistake, so the brand they are filed under cannot open them. Folders marked DO NOT USE are left out — along with everything filed under them, since a folder is usually retired by renaming its top level — and they no longer turn up as collection cards in search either. Folders that are empty are left out — opening one used to show Forbidden, which is what a brand saw for about half the folders it was offered. One kind of Forbidden is not ours to return, and Weight Watchers hit it: the footage app runs its own ownership check on top of ours, matching the brand's name against the client name we report, and those two never agree for a brand that exists per market (WW USA (US) in the app, one folder called Weight Watchers in Iconik) — so every one of that client's collections was refused even though it listed correctly and we returned all of its assets. We already know the right answer, because the brand→client mapping is ours: an Open collection request that tells us which brand is asking now comes back stamped verified for that brand, and the footage app can trust that instead of guessing at names. Each entry says how many clips are filed in it and how many you can actually browse (those differ when some are archived), and shows its parent folder — brands reuse names like Carrot 500 in several places. Folder-scoped searches run at full speed at any depth: the engine resolves which client a folder belongs to and searches within that client's clusters directly (previously a generic query scoped to a large folder — e.g. a client's whole footage folder — could take 8-9 seconds; it's now sub-second). A search box above the tree filters it as you type (letter by letter): matching collections at any depth appear as a flat, checkable list with their parent path shown for disambiguation; clear the box to return to the full tree. The left panel is dedicated to this collection hierarchy (drag its right edge to widen it for long names); the other filters — media type (Video / Image / Audio / GFX — GFX is design graphics like logos, arrows and end cards; Image deliberately excludes them; typing gfx, clip art or vector graphics in the query applies the GFX filter automatically and lifts your selected folders/sources/kind pills for that search — graphics live outside footage folders, so a clipart request searches all of them; a client filter still applies. The GFX pill works the same way when your selected folder holds no graphics: instead of showing nothing, the search widens to that client's graphics automatically), asset source, client, page size, exclude-archived — live behind the Filters button next to the search box, which glows with a count when any is set away from its default.

Bulky values stay out of your way: nested/object metadata and long multi-line text render as collapsed, expandable blocks, and a Raw metadata (JSON) section at the bottom holds the full pretty-printed object — click to expand only what you need.

Ingestion Pipeline
How does footage get into the system?

The main path is the sync — a scheduled job polls Iconik every ~15 minutes across all three source roots (Raw Footage, Client Collections, Approved Ads), and anything new or changed is ingested and made searchable within about an hour — including brand-new clients: a new client folder in Iconik is auto-registered and enrolled within the hour it appears, and its footage flows in from the first upload — the freshest items are processed first so new footage never waits behind older backlogs. (One-off CSV bulk imports — POST /ingest/csv with an exported asset list — are still available for large historical loads.)

The sync walks each library in full — there is no cap on how many assets it pulls per run, so even a client with a large sudden influx of footage gets completely indexed, not just the most recent slice. (It walks Iconik's scroll cursor rather than paged results, which Iconik otherwise limits to ~10K.)

Running the same search again — the same words, filters and folders, shortly after — brings the results back almost at once: the page you just got is reused instead of being searched for again. Anything that changes what the search can find makes the next one run fresh: new or removed footage arriving with the sync for that client, a change of filters or folders, or a new release of the search. So newly ingested footage still appears after the next sync, exactly as before.

On ingest, each asset's client name is canonicalized so the same brand can't appear under spacing/capitalization variants (e.g. "HoneyLove" vs "Honeylove", "Cook Unity" vs "CookUnity") — every variant collapses to the one canonical client in the filters.

I filed footage into a client folder in Iconik — why isn’t it in that folder in Flawless?

Moving or filing an asset into a folder doesn’t count as an edit in Iconik, so the regular sync, which picks up changed assets, doesn’t see it. A daily pass lists every folder’s contents and catches up. Since 2026-10-28 that pass also processes footage it finds in a folder but has never indexed, so it becomes searchable as well as listed. It also retries clips that were skipped because Iconik hadn’t finished their preview yet.

New footage, or footage you edit, appears in its folder at the same time it becomes searchable. Before 2026-10-28 it could be searchable but missing from the folder until the daily pass.

2026-10-28, Courtside. Outros and other assets filed into Courtside’s Client Collections folder after they were created were missing from it. They are indexed and listed now.

I approved or edited footage in Iconik — how long until Flawless shows it?

Normally within the hour: the sync picks up any asset whose Iconik record changed — a cleared warning, an approval, new tags — and updates it in place without reprocessing the video.

2026-10-27. Edits made in Iconik between about 3:50 pm and 9:30 pm UTC waited hours instead. One sync run failed inside Google Cloud before it could start, and the sync kept treating that dead run as still in progress, so it skipped every scheduled run until a six-hour safety limit expired. The sync now recognises a run that failed before starting straight away, and the edits from that window were applied when it resumed.

Il Makiage full file space. Changes there are picked up by the same hourly sync. In addition, a separate scan checks every asset in that folder once a day (03:00 UTC), so anything the hourly sync missed is caught by the next scan. Until 2026-10-28 the hourly sync was the only pass over that folder, and it never got through the whole of it.

What happens during processing?
  1. The proxy video URL is fetched from the Iconik Files API (/API/files/v1/assets/{id}/proxies/).
  2. The video is downloaded and scene boundaries are detected using PySceneDetect's content-aware detector. (Clips under 10s skip detection and are treated as one whole-clip scene.)
  3. A leading production slate (clapperboard / colour bars / countdown card) is dropped before chunking, so it is never embedded or indexed. See "Why are clapperboard slates excluded?" below.
  4. Each scene is cut into a short video clip (always re-encoded, never stream-copied — a stream-copied cut starts on a non-keyframe and the embedding model returns an empty result for it) and the bytes are embedded directly with gemini-embedding-2 (multimodal) into a 3072-dim float32 vector, stored as halfvec in PostgreSQL.
  5. An HNSW index (halfvec_cosine_ops) enables fast approximate nearest-neighbour search. A nightly maintenance job rebuilds the index off-hours when heavy re-processing has bloated it. A rebuild never blocks live search and never takes the index offline — the existing index keeps serving every query until the replacement is finished and verified — but it does compete with search for CPU and memory while it runs, so rebuilds are scheduled into the quiet window (06:00–09:00 UTC, when search traffic is at its lowest) and you may notice slightly slower searches during one. The job can also rebuild the index with higher-quality graph settings rather than just a fresh copy of the same graph: recall (how often the true best matches are actually returned) depends on both a query-time setting and the settings the index was originally built with, and only a full rebuild-and-swap can change the latter. Scenes that don't fit neatly into any cluster are folded into the nearest one, and very small or brand-new clients whose footage hasn't formed real clusters yet get a single catch-all cluster — so cluster-based (hierarchical) search returns every client's footage.
Does background processing slow down my searches?

Search comes first (2026-10-26). Footage syncing, ad rankings and the other background jobs share one database with search. Since 26 October, search has its own share of the server’s database connections: a third of them are always kept for searches, so a burst of other work — a big batch of downloads, the Top Openers view, collection browsing, admin pages — can no longer use them all and make searches fail with "Internal server error". Searches can still borrow any connection the other work is not using, and the lookups a search runs side by side (related-search suggestions, the query classifier, availability checks) count as part of the search.

The background jobs also hold back: each of their database queries uses a single processor core instead of fanning out across several, and the Top Openers / Winning Footage rankings, rebuilt every 15 minutes when an ad changes, skip a rebuild while searches are running slow (the next one catches up, and the rankings are never left more than six hours behind). Bulk jobs already pause by themselves when searches slow down.

Before the change we checked: searches made while the rankings were being rebuilt were no slower than the rest. This is a guarantee for busier days, not a fix for a slowdown you would have noticed.

Why are clapperboard slates excluded?

Many source clips open with a production slate — a clapperboard showing scene / take / roll and a running timecode, often held up to camera by a crew member, or colour bars / a countdown card. That pre-roll has no semantic value, so indexing it just pollutes search results.

PySceneDetect already isolates the slate as the leading scene (it cuts at the clap). We then drop that scene before embedding. Detection uses object recognition — a single gemini-3.8-flash vision check on a representative frame of the leading scene — rather than brightness or freeze heuristics, because a slate can be a dark backlit silhouette or a brightly-lit board, and a moving clapper with a ticking timecode never "freezes".

The trim is deliberately conservative: it never touches a single-scene asset, never drops a leading scene longer than 45 seconds or one that makes up most of the asset, and keeps the scene whenever the check is uncertain or errors. A slate that merely flashes for under a second inside a longer real scene is kept (splitting that out is a scene-boundary-precision improvement tracked separately).

How quickly does search notice that a clip was deleted in Iconik?

Within a day. Every night a sweep checks the whole library against Iconik — all 598,433 indexed assets — and retires anything Iconik confirms is gone. It used to check about a sixth of the library per night, taking roughly six nights to come round, because the check competed with the indexer for Iconik's rate limit and had to be kept short. It now runs on its own credentials, so it can do the whole library in one pass without slowing anything down.

Nothing is retired on one opinion. A clip is only retired when two different Iconik endpoints agree it no longer exists. If they disagree — one says gone, the other says it is still there — the clip is left alone and the disagreement is recorded. That is the safeguard against the failure that hid live footage in August: an "access denied" answer is not a deletion.

An asset I could find before has disappeared from search. Why?

An asset leaves search when its status becomes inactive. Three things set that, and they are not interchangeable:

  • It is gone from Iconik. The nightly dead-asset sweeper, and the runtime check that runs when results are served, retire an asset once Iconik answers 404 — asset doesn't exist, or returns it with no usable proxy file. That is a real deletion upstream and the retirement is correct.
  • It was Work-In-Progress only. Material filed solely under Work In Progress Jobs was retired on purpose (migration 081) so half-finished cuts never surface, least of all in "Footage used". Those rows carry a retired_reason.
  • Iconik denies us access to it (fixed 2026-08-13). Iconik answers 403 Access denied for assets that exist but sit on a storage or folder tree our token cannot read. Until 2026-08-13 the runtime check read that as a deletion and retired the asset — so an asset could vanish moments after you watched it, and one did. A 403 is now counted and logged, never inactivated, and the assets retired that way are being restored after Iconik re-confirms each one individually.

A 403 asset stays searchable and usually still plays, because the preview player falls back to our own cached copy of the video. The original-file download can still fail until Iconik grants access to that folder tree — that is a permissions request, not a bug in search.

Why do some clients' clusters and taxonomy look out of date?

Clusters are rebuilt per client, one client at a time, in a nightly quiet window. A rebuild is only swapped in when it finishes, so a client whose rebuild fails keeps serving its previous clusters — never a half-built set. The trade-off is that a repeatedly failing client silently shows older groupings while search itself stays correct.

Tidying up after a rebuild — deleting the grouping it replaced — is kept separate from the rebuild itself and done in small pieces that each save their own progress. On 2026-08-19 that clean-up was attempted in one piece for a large brand, 2.6 million rows at once, which is more than the database will finish in a single step: it retried for 99 minutes, deleted nothing, and competed with live searches throughout. The new grouping was live and correct the whole time — only the housekeeping was stuck — and it now runs as roughly 140 small steps in the quiet window instead.

That happened between 2026-08-05 and 2026-08-13: 16 of the largest clients' rebuilds died on a query that had grown too expensive to finish (it aggregated across the whole library instead of the client being rebuilt). Fixed 2026-08-13 — the same step now takes under a second — and the rebuild re-armed. Scene-level search was unaffected throughout; only cluster names, bubbles and the taxonomy were stale.

Are Work-In-Progress job folders indexed?

No. WIP job folders were indexed briefly and then deliberately removed as a source: a half-finished cut appearing in results — and especially in "Footage used" — is misleading, since it is not evidence that a frame reached a delivered ad. Only Raw Footage, Client Collections and Approved Ads are indexed.

Until 2026-08-13 the hourly scan kept retrying a handful of WIP folders every hour and rejecting their assets one by one, which looked like an ingestion failure in the Admin activity log. Those folders are now recorded as out of scope and skipped outright, so the log shows real problems only. Nothing about what is searchable changed.

Some clips an ad clearly used are still missing from "Footage used". Why?

Every clip a Premiere project names is checked against Iconik. Of the 120,961 referenced clips we could not previously resolve: 75% Iconik refuses to show us at all (a permissions gap on their side — a request is open), 9% no longer exist in Iconik, and 16% are readable and are being added now.

Of that readable group, about 4 in 10 are Work-In-Progress or archived material. Those are recorded so the ad can name them and you can still download them, but they deliberately do not appear in search results — WIP is not searchable footage. A further small slice is stock music, AI voiceover and stock video that belongs to no client; those are left out entirely.

So an ad may still list fewer source clips than it used, and the honest reason is usually the first one: we cannot open the file. That number will drop when the access request is granted, not before.

I typed a word into an Advanced filter and got MORE results, not fewer. What happened?

Fixed 2026-08-19. The Objects / Actions / Subjects filters are free text, so a typo — or a word this brand's footage simply never uses — asked for something no clip could satisfy. That emptied the results, and an empty result set triggered an automatic "loosen everything and try again" step, which also discarded the parts of your query that were working. The grid came back fuller than before you added the filter, which looks like success and is the opposite.

Now a word no footage in the brand uses is not applied, and the response says so, so the app can tell you it was ignored instead of quietly returning a different search. A rare word still filters normally — if only two clips match, you get two.

I searched an action ("dog rejecting kibble") and got a handful of clips, then "no more footage". Why?

Fixed 2026-08-18. The search was requiring every word of your query — including the action — to literally appear in the footage's own labels, on-screen text or AI description. Our clips describe that moment as "sniffs and walks away", "turns away", "ignores"; the word rejecting appears in one clip out of 92,000 for that brand, so everything else was filtered out and the results ran dry after a few clips even though there were hundreds of relevant ones.

Now an action only narrows your results if the footage actually uses that word. Otherwise the meaning is matched instead — rejecting also accepts refusing, declining, turning down — and if we have no usable wording for it at all, the action stops filtering entirely and just steers the ranking. Same query, same brand: 1 clip before, 52 after.

This also quietly fixed searches nobody had reported: typos and unusual words ("apetizing meal", "baby trowing food", "cat hissing", "camping") were each able to empty a result set the same way. Searches whose wording the footage does use — "dog eating kibble", "mixing", "pouring" — behave exactly as before.

One related limitation still open: on a narrow query the first page can report a larger total than the following pages can serve, so "load more" may stop earlier than the count suggests. The count is an estimate of the candidate pool, not a promise of pages.

Searching by image failed with an upstream error when I used a full-resolution screenshot.

Fixed 2026-08-19. Analysing an image normally takes about 4 seconds, but the request was being cut off after 8 — a limit meant for text searches, where 8 seconds means something has hung. Most image searches finished well inside it; occasionally one ran slower than usual and got cut off, and the resulting error arrived fast, which is why it looked like a rejected upload rather than a timeout.

Image searches now allow up to 60 seconds, so an unusually slow one comes back slow instead of failing. Nothing about your image was the problem — size wasn't the trigger, and a search that worked before still works the same way.

CookUnity's Meal / Chef / Dietary filters disappeared for a few hours — what happened?

2026-08-19. A background job that rebuilds product filters for other brands was clearing CookUnity's filter data every ten minutes and putting nothing back, because CookUnity's filters come from a different source (the client's own menu spreadsheet) that this job doesn't know about. The filter definitions survived, so the drawer knew the six filters existed but every value counted zero.

Fixed — that job now leaves alone any brand whose filters it doesn't own — and the data has been rebuilt. No footage or metadata was ever affected; only the filter index.

2026-08-20. The hourly refresh of collection cards (the entries that let a folder appear as a result) had been failing quietly every hour for weeks: it was built to fail softly — an hour’s failure just leaves yesterday’s card serving — which also meant nothing raised an alarm. The cause was a start-up step the hourly job skipped; the connection layer now performs that step itself the first time it is needed, so no job can skip it again. Folder cards may have lagged renames and new members by up to a few weeks during that window; they are current again within the hour, as designed.

2026-08-24. Footage from a brand-new shoot folder could be missing from Production-ID search permanently, not just briefly: if clips were ingested in the first half-hour after their folder was created, the folder's name hadn't been catalogued yet, and the shoot's Production ID — which is read from that folder name — was never derived. Reported for one Factor shoot (112 clips, ingested Friday, invisible by Production ID until today). Fixed twice over: new ingests now look the folder name up directly at the source when it isn't catalogued yet, and an hourly repair pass back-fills the roughly 71,000 older clips that were affected over time — each becomes searchable by its Production ID within the hour of being repaired.

2026-08-25. The overnight bulk-processing lane (which writes transcripts and descriptions for large backlogs) stalled at the provider with zero output for seven hours, and the scheduler would have fed it the same work again the next night. It now gives up after 90 minutes of zero progress and automatically re-routes the backlog through the steady per-item path until the bulk lane proves itself healthy again — text depth keeps arriving either way, just via a different door.

2026-10-17. The database grew by about 7 GB overnight without any new footage arriving. The nightly job that refreshes the search index had been held up by another job, ran out of its time allowance a third of the way through, and left the half-built copy behind — unusable, but still taking up space. It then tried to delete that copy and was not allowed to: since mid-October the maintenance jobs run under a restricted database account that may rebuild an index but not delete one. The leftover is removed by hand once; from now on the jobs remove such leftovers themselves, a health check reports any that appear, and a rebuild is no longer started when it cannot finish in the time it has. Searches were never affected — the live index was intact throughout.

2026-10-09. About 185,000 scenes from some 10,900 Il Makiage videos received their transcripts, on-screen text, objects and scene descriptions overnight, but the last step that makes those words findable by text search ran as a few very large database writes, and one of them hit its time limit. The step stopped there. The new descriptions were saved, but the parts of search that match on words (quoted phrases, the text filter, the transcript and description boosts) could not see them yet, so those videos ranked lower or dropped out of word-matched results. That step now runs in small pieces, each with its own time limit. A piece that fails is retried, and if it fails again it is set aside and reported, while the rest finishes.

The same investigation found an older, wider gap. The everyday text pass (the one that handles new footage within the hour, and stills) built its word index differently from the overnight pass: it left the scene description out of the combined index, and never filled the separate transcript index. About 1.2 million scenes in roughly 287,000 clips across all clients were affected. Their spoken words did not count in the transcript part of the ranking, and words that appear only in a clip's description (for stills, that is most of their text) could not satisfy the text filter. Both passes now build the index from one shared definition, with a test that keeps them identical. A one-time repair rebuilds the index for the affected clips once this is deployed. Because it rewrites a lot of data, it runs only overnight (01:00–12:00 UTC) and may take two nights; searches keep working throughout, and clips improve as it goes.

Inside an ad, every clip shows (2026-10-22). Opening an approved ad’s “Footage used” (and its clip-by-clip cut list) now lists every clip the ad was built from, whatever its approval state. The ad was approved with that footage in it, and leaving half of it out made the ad impossible to explain. Clips that are restricted or carry a warning show the warning on the card instead of being left out. This applies to every role, including when the ad is opened from inside a footage library folder. Search and folder browsing are unchanged, and so is “Ads using this”: unapproved footage stays hidden there for roles that can’t see it.

“Client Footage” is a label, not a restriction (2026-09-29). Some clients’ footage carries a “Client Footage” warning so editors can tell footage the client shot from footage TubeScience shot. The same now applies to a “Client asset” note. That note no longer counts as a restriction: those clips stay visible in search and collections for every role, and the warning is still shown on the card. A note that also says “do not use” is still hidden for roles that can’t see restricted footage.

Approval-aware search (2026-08-26). Footage that isn’t approved yet is now hidden from search and folder browsing unless your account role is allowed to see it: clips explicitly marked not-approved or awaiting approval in Iconik, anything still in the Active Review staging folder, and clips carrying a non-empty warning note. Clips with no approval field at all remain visible — most of the library has never been through the approval flow, and hiding it would have emptied search. The response says when this filter was applied, so an integrator can tell a restricted view from a small library. The same rule covers shoot browsing (“From this production”) and the Openers/Best-Ads views, so restricted roles can’t route around search to see unapproved footage. Users who can see unapproved footage also get a “Hide unapproved footage” filter to switch to the reviewed-only view when they want it. Image search honors the same rules, and every result now states its approval status and whether it carries warnings. The “Ads using this” / “Footage used” lists and an ad’s clip-by-clip timeline follow the same rule — a hidden clip is left out of the timeline rather than blanked, and the cut numbers keep their original values so a gap is visible instead of a shortened timeline reading as the whole edit. (Since 2026-10-22 this applies to “Ads using this” only — inside an ad, every clip shows; see above.) Where the system can count exactly what it withheld — opening a folder, or one of those linkage lists — the response now says how many results were hidden, so the notice can read “12 hidden” rather than a vague “some are hidden”; a search cannot count what it never retrieved, so there the notice stays unquantified rather than guessing. When the filter is on and hid nothing, it reports zero, so you are never told results are missing when they are not. Since 2026-09-03 the gate reads what a warning says, not whether one exists. Only warnings that mean stale (old packaging, discontinued, outdated), do not use, restricted use (client footage, whitelist, market-only), rejected / low-performing or a technical defect hide an asset. Editorial advice ("avoid the roof at the top of frame") and one-off notes stay visible with their badge. Since 2026-10-20 the bulk stamp "Needs Review and Tagging USE WITH CAUTION" counts as restricted use and is hidden like the other restricted warnings; a plain "Needs Review and Tagging" note is still visible. That is 38,987 hidden assets instead of 125,925, and each row now says which categories its warnings fall into. (Over-exposed clips count as a technical defect since the same day’s needle fix.) Search and folder browse also accept a review-state filter by warning category (for example "unreviewed" = clips still marked Needs Review and Tagging, or "cannot use" = the five blocking categories), which the footage app uses for bookmarks; it narrows results the same way a facet does and never overrides the gate. The gate has an on/off switch per environment: it is on for staging and off for production until the footage app sends everyone’s role, so a production release cannot hide footage by accident.

2026-08-25. A HelloFresh folder (06_INTL) showed a count of 2 but opening it said “Collection filter unavailable”. The two clips inside had been attributed to a client label derived from an internal folder name (_HelloFresh_INTL) rather than to HelloFresh itself, so the safety check that stops one client’s footage appearing under another’s folder refused to open it — on footage that was HelloFresh’s all along. The 90 affected clips are re-attributed to HelloFresh and the folder opens normally; the label variant can’t be re-created.

2026-08-25 (same day). Applying the same rule more widely: five duplicate client identities were folded into their real clients — GoodChop Footage into Good Chop, Athletic Greens into AG1 (the rebrand), Factor_Canada into Factor, Tempo into Home Chef, and Truebill into Rocket Money (the company renamed). Footage that was split across two names for the same client now lives, filters and counts under one. A further set of individually misplaced clips (clips carrying one client’s production code while filed under another) has been catalogued for review before anything moves.

Can I filter CookUnity footage by meal, chef or dietary type?

Yes, as of 2026-08-18. CookUnity footage can be filtered by Meal (377 meals), Chef (118), Dietary (Gluten Free, Poultry, Dairy Free …), Region, Production type (Studio Directed, Highscale, Remote Directed …) and Market. The meal appears where a SKU appears elsewhere in the app, so the chips on a clip and the filter drawer both work the way they do for other brands.

Two kinds of meal tag, and it is worth knowing which you are looking at. Where the meal's name is on screen in a clip — CookUnity prints it on the packaging — that clip is tagged from its own footage, and the tag means "this clip shows this meal". Everywhere else the tag comes from the client's shoot list, which is authoritative about the shoot: it means "this clip is from the shoot that featured this meal", so a shoot covering eight meals tags its clips with all eight. Roughly 5,100 tags are the first kind and 121,700 the second.

Two columns in the client's list are empty at source and so are not offered as filters: cuisine type and calories/protein. A filter that lists nothing is worse than an absent one. Talent is not duplicated here either — it is already its own filter.

Why did one brand's cluster groupings look stale even after a rebuild finished?

Because the rebuild's last step was silently doing nothing. Rebuilding a brand's groupings has two halves: work out the new groups, then point every clip at them. The first half was landing correctly; the second half was being discarded before it was saved — and it reported the number of clips it had intended to move, so the logs read like success. For one large brand that meant 508,506 clips reported as regrouped and seven actually regrouped.

The visible effect: browsing that brand's clusters showed old groupings from July, including hundreds of thousands of one-clip "groups" that made the cluster view close to useless there, while the freshly computed groups sat unused. Search itself still returned correct clips — only the grouped/taxonomy view was affected.

Fixed at the root (the save now happens where it can't be skipped), the old generations are being cleared out in batches, and every clip is re-pointed at the current groups afterwards so nothing is left ungrouped. Rebuild logs now report what was saved rather than what was attempted.

Why did a clip show its SKU and product chips from search, but not when I opened it from a folder?

Because it genuinely wasn't sent. Until 2026-08-18 the folder view returned rows without the SKU and facet labels, while search returned them — the same clip, described two different ways depending on how you reached it. Nothing broke and nothing was wrong on screen; the panel was simply less complete on one route than the other, which is a hard thing to notice.

Fixed, and in more places than the folder view: shoot cards, "Ads using this" / "Footage used", and the ad performance cards had the same omission. Every one of those surfaces now carries the same labels search does, so a clip describes itself identically wherever you find it.

Why did "dog and cat" miss clips that clearly show both?

Until 2026-08-17 it could. Requiring several objects at once — and excluding one ("no face") — reads a dedicated index of the object labels, and that index was never being written even though the labels themselves were. Around 611,000 clips held labels the requirement filters could not see, so those clips were quietly left out of results whenever you asked for a combination.

Ordinary searching was unaffected, which is why this went unnoticed: object words are also folded into the main text index, so typing "dog" always worked. Only the stricter "must contain / must not contain" filters were blind. Fixed, and the existing clips are being backfilled.

The fix then broke the analysis pass for about two and a half hours (2026-08-17, 22:33–01:20 UTC), and it is worth saying plainly. The explanation of the fix was written as a note inside the database instruction, and it contained a percent sign ("48% of every video scene"). The database driver reads a percent sign as the start of a value placeholder, so the instruction became unparseable and every write of transcripts, on-screen text and object labels failed before it began — 11 consecutive runs of the hourly analysis pass, each ending in an error rather than a partial result. Nothing was lost or corrupted: the clips stayed queued and were re-analyzed on the next run. Newly ingested footage in that window simply gained its text depth late. The explanation now lives outside the instruction, and a test runs the driver's own parser over every bulk write in the codebase so a stray percent sign cannot ship again.

Can I find an image by the words printed on it?

Yes, as of 2026-08-17 — and not before. Stills were indexed by appearance only: you could find a graphic by describing what it looks like, but not by the text on it or by the objects in it, because nothing had ever read them. 67,889 image records had no object labels and no on-screen text at all.

Images now get the same treatment video already had: a list of what is visible, every character of on-screen text, and a one-line description — all folded into the same search index the spoken words and video labels use. About 50,900 images are being processed; roughly 17,000 more cannot be read because Iconik gives us no preview file for them.

Why halfvec instead of vector?

pgvector's HNSW index supports a maximum of 2000 dimensions for the vector type. Gemini embeddings are 3072-dimensional. The halfvec type (float16 storage) supports up to 4000 dimensions with HNSW and halves storage costs with negligible accuracy loss.

Webhook
How does the Iconik webhook work?

Configure Iconik to send asset.created and asset.updated events to:

POST https://semantic-search-td24dyhr7a-uc.a.run.app/v1/webhooks/iconik

The endpoint validates the X-Iconik-Signature header using the shared secret stored in Secret Manager (iconic-webhook-secret), then enqueues the asset for processing. Processing is asynchronous — the webhook returns immediately.

What events trigger re-processing?

asset.created and asset.updated both trigger ingestion. If scenes already exist for an asset, they are deleted and rebuilt from scratch so embeddings stay current with any proxy file changes.

Admin Dashboard
What is the Admin tab?

The Admin page provides a live dashboard for all background jobs and system state:

  • System Overview — asset, scene, and cluster counts; last sync and clustering timestamps.
  • Sync Jobs — per-client sync status (last synced, assets checked, new/copied/inactive, errors). Configurable poll interval and manual trigger. The list shows only jobs that ran in the last 7 days; dormant/decommissioned clients are hidden (display-only — their sync state is preserved, so a client reappears if it syncs again).
  • Clustering Jobs — history of clustering runs with scene counts, cluster/super-cluster totals, duration, and manual trigger.
  • Webhook Log — paginated event log of all Iconik webhook events with status, HTTP codes, and messages.
  • Sync Errors — expandable table of per-asset errors from sync runs.
  • Do the background jobs run the current release? They do now. A release only redeploys the jobs its build config names, and thirteen jobs were not on that list — they kept whatever build was last set by hand, and their runs still reported success, so nothing anywhere said "this is last month's code". Three of those were on timers: a daily reporting sync was pinned to a build from seven weeks earlier, and the 3-hourly catch-all that re-attempts un-embedded clips had needed four manual re-pins in a week. Nothing was wrong in what you searched — those two jobs' code had not actually changed in the meantime, so they were running old copies of identical logic; the risk was that the next real fix to one of them would ship everywhere except the job that needed it. All three now update with every release, and a test refuses a new scheduled job that has no deploy attached.
  • Environment & release — which environment you are on (staging or production), what each side is running, and what is merged but not yet released. It also watches for the production page and the production API drifting apart, since they deploy separately. That check reads a snapshot of the release taken when the page's own build was made, so once production is deployed after that snapshot the comparison is no longer answerable from here — the panel now says so and points at production's own Admin page, instead of naming a difference it cannot interpret. It used to report that difference backwards and recommend a release that had already happened. Branch names (renamed September 2026): day-to-day work merges into main, which is what the staging site runs; a release is an approved merge from main into production, which is what the production site runs. The older names were stage and main. The Admin page's Environment panel labels each side with its branch, so Staging reads main and Production reads production. Nothing about search results changed — only which branch name means which site.
What is the Search Quality page?

Search Quality (admins only) shows whether editors are finding footage, per client and per editor within a client, and where agentic search would help. Every search made from the footage app is labelled from what the editor did next:

  • Vocabulary — on the page a search is one thing an editor looked for, made of one or more attempts (related queries within 10 minutes of each other). A search is a success on the attempt that worked, or the editor gave up; Attempts to find shows the average, median, 90th and 95th percentile of attempts a success took.
  • Good (search success) — the editor added a clip to a bin, downloaded one, or rated the search helpful / great. First try means good and not rephrased afterwards; after a refinement the new query is rated as an independent search.
  • Bad — everything else: rated not helpful / bad (this beats an add), no results, an error, a related query within 10 minutes (the editor adjusted the words or re-ran it), the app recorded the results as abandoned, or nothing happened at all.
  • By attempt — on the 1st, 2nd, 3rd query of a goal one of three things happens: success, continued (bad, and the editor went on to a related query) or gave up (bad, and they did not); the three add up to 100% of that attempt. Goals split into found on the 1st / 2nd / 3rd+ attempt or given up, which also add up to 100%.

Only semantic searches count by default: a search the engine routed as a job code, an exact file or asset name or a SKU, one whose query reads like a file name or job code, a known talent's name, or an empty browse is set aside (the API can lift that filter; the page always shows semantic searches). Two charts show one bar per day for 1- and 2-week periods and one per week for longer ones: searches (search successes stacked by the attempt they came on, over bad, with the search success %) and goals (found over given up, with the found %). Periods are whole weeks of full days (4 weeks by default), so every week in them is complete, and every number is a total for the period.

Everyone who searches from the footage app is tracked, with the role the app reports for the person (editor, edit_lead, cs_lead, admin, qc, strategist, vfx; internal when the app only says "not privileged"). Leads, admins and QC search to review rather than to build, and a strategist's success is a copied link we never see, so the page defaults to Editors: everyone not seen as a lead, admin, QC, strategist or vfx (the app's editor and internal roles plus unknown); Everyone lifts the filter and the Who is searching table has each role.

Related consecutive queries by one editor form a goal (what they were trying to find); it is found when any attempt was good, and attempts counts how many queries that took. Agentic benefit ranks clients by goals given up in the period — the searches where an editor kept rephrasing and left without footage, which is exactly where an agentic dig would land. Once agentic search is used from the footage app, searches followed by a run are compared with plain searches for the same client.

The data is the footage app's own event stream (PostHog), copied every 6 hours by search-quality-job into ff_events and labelled into search_outcomes; each search is also matched to our own trace for latency and engine mode. Since 2026-10-19 the app also reports when the first result is on screen (the first card on the page with a frame, a still or a finished tile, on the same clock as the search — the search's own time stops when the answer is parsed, before anything is drawn) and when every card on the page settled; the Searches box shows the first-result p50 and p95 beside the response time, and every example attempt shows both. Rows are re-labelled for three days as late signals arrive; Re-label 3 days on the page does it now. A brand the alias table does not know shows as brand:xxxxxxxx.

How does the incremental sync work?

A Cloud Scheduler job fires every 15 minutes and calls POST /v1/admin/sync/run. It walks both the per-client folders and all three source roots directly — Raw Footage, Client Collections, and Approved Ads — so any new or changed asset in any of them is picked up (each asset is attributed to its real client automatically). For each it fetches assets modified since last_synced_at, then applies a decision tree:

  • Inactive (deleted/deactivated in Iconik) → mark asset + scenes as inactive. A separate daily pass also retires clips that have since been deleted in Iconik, so search stops offering footage that is no longer there. That pass is deliberately conservative about one case: a clip our access token cannot open is not treated as deleted, because "I can't see it" and "it's gone" are different things — checking them against Iconik directly showed the two are cleanly distinguishable, and about half of the candidates were live footage that a permissions gap merely hid from us. Retiring those would have removed searchable clips on the strength of an access error, so they are reported instead. The pass also now runs in slices with a time limit rather than sweeping the whole library in one go: it used to hold ingestion off for around nine hours a day, which meant new footage could sit unsearchable for most of a working day.
  • No proxy URL → fall back to the original master (downscaled here to the shape a proxy would have had). If there is no master either, the clip is marked skipped — its media does not exist, so re-checking it hourly cannot help. It comes back on its own if the file is ever replaced in Iconik.
  • Same proxy_path already indexed by this client → copy scenes and embeddings, no re-processing.
  • New video → scene detection + Gemini embedding + cluster assignment (each scene joins the client's nearest layer-0 cluster, so it is reachable by hierarchical search from its first sync).
  • New audio/image → direct Gemini multimodal embedding.

Results are written to sync_log and any per-asset failures go to sync_errors.

Production ids with a two-letter job type (for example CE10287) were stored one digit short until 2026-09-08; they are corrected everywhere, and the shoot search accepts the full id.

Each tick also stamps production metadata onto new assets — PRODUCTION_ID, JOB_KEY, and PRODUCTION_TYPE (Studio Directed / Location Directed / Remote Directed / Self-Taped / Highscale Production / EVP / VO Only / Creator Edited / Client Assets / Strategy Direct Ingest, inferred from the production code) derived from the shoot's collection name, plus SUPERVISING_PRODUCER from the asset's Iconik metadata — so you can see and filter by them in the asset details.

After a new video is embedded, a second pass fills its text channels — transcript, on-screen text (OCR), detected objects, and scene descriptions — plus the fine action sub-scenes that power tight in/out windows. The system sizes this pass to the workload automatically — including its parallelism: when the pending count is large, the worker pools scale up with the backlog (and back down for the trickle) so big waves drain proportionally faster. A small trickle of new assets goes through a fast online runner so everything is fully searchable within the hour, while a large backlog (e.g. after onboarding a big library) is routed to the cheaper batch pipeline that drains it in the background — overnight, deliberately: heavy backlog processing waits for off-hours so daytime searches always have priority on the database. The batch pipeline banks whatever has finished even when a few stubborn items keep a batch open — a handful of stuck files can't hold the rest of the backlog hostage; the stragglers simply re-enter the next round. Either way you don't do anything — newly ingested content becomes searchable on vectors immediately and gains its full text/action depth shortly after. A 3-hourly catch-all additionally re-attempts every asset that never got embeddings — including old clips stranded by earlier pipeline versions — pushing each one through the current pipeline once; clips that genuinely produce no scenes (blank, silent, unreadable) are marked skipped so the backlog counters reflect real work, not permanent residue. The text pass applies the same terminal rule: a clip whose Gemini analysis comes back empty (blank/silent/static — nothing to transcribe or describe) is marked complete-with-empty-text instead of staying pending, so it isn't re-analyzed on every pass and the "text pending" counter converges to real work only. The text pass no longer runs out of memory on very large recordings (2026-10-08). On 8 October about every other run of the fast online text pass was cut off partway, because several of Il Makiage's long creator recordings — 1–2 GB each — were being downloaded at the same time, and the server holds those downloads in memory. The pass now budgets how much video it holds at once, the way ingestion already does: big recordings take their turn while ordinary clips keep flowing, so fresh footage gets its transcript, on-screen text and descriptions on schedule again. A recording too large for the server to hold at all (a preview file over about 5 GB — very rare) is set aside with the reason recorded instead of being retried every hour; it stays findable by what it looks like, just without its text channels. A clip that fails now records why it failed. Failures raised while a clip was being embedded were never written down — the clip was simply left waiting, with no reason attached — so the same hopeless files were re-fetched every five minutes forever (a full-resolution master re-downloaded each time), while the only trace was in the server logs. Around 9,600 clips whose media no longer exists in Iconik had been in that loop since July. A clip's outcome and its reason are now always saved, clips whose media is genuinely gone leave the retry queue, and the ingest job stops being crowded out by work that could never succeed. Clips with no preview file are now indexed. When a clip has no preview we fall back to its full-resolution master — but that path failed on every single clip it was meant to rescue, for both audio and video, so those clips never became searchable. Both are fixed; several hundred clips are now picked up normally. A related one from 2026-08-20: the pass that gives still images their text depth (objects, on-screen text, description) had been quietly stopping early — it walked newest-first, the newest images belonged to a brand its credentials could not read, and after one full block of permission errors it stopped and reported success. It now skips past anything its credentials cannot read instead of stopping at it — a small set of images is restricted upstream to every credential we hold, and those simply wait for their permissions — and the ~80,000-image backlog is draining. Separately, the result counter above the grid could promise more than deeper pages could deliver for narrowly-filtered searches (the count came from a wider candidate set than the filters allowed through); the count now tightens as you page and always matches exactly on the last page, and paging can no longer get stuck asking for a page that will never fill. A related limit was found a day later: asking for results beyond roughly the 450th failed outright with an error, because the search asked the database for more lookahead than the database permits. Deep pages now stay within the database's limit and simply take a little longer the deeper you go, for text and image searches alike. The video half only came to light because failures started being written down: while they were discarded there was nothing to look at.

Newly added clients are brought into this hourly sync automatically: once every one of a client's assets has finished ingesting, the system enables ongoing sync for that client and runs a one-time clustering pass for it — no manual step needed.

A client now has exactly one spelling, and it is claimed the first time we see it. Different parts of the system work out which client a folder belongs to from different evidence — one reads the folder's name in Iconik, another the label already on the footage inside it — and they are supposed to agree by looking the name up in a shared registry. Nothing ever added a client to that registry, so 62 clients holding about 70,000 clips were never in it, and for those the lookup did nothing: Youfoodz and YouFoodz both survived, and the two were overwritten by each other every hour, indefinitely. Twelve clients had grown a second spelling this way. A client's spelling is now claimed on first sight and the registry refuses a second version of it, so which spelling you get no longer depends on when you look. Clips filed under a duplicate spelling were not moved — where footage genuinely exists under two names, merging it changes who owns what, so it is a decision for a person and is listed for review instead.

Footage whose folder trail we could not follow was being skipped, every time. When a clip arrives, the system works out which client it belongs to by following its folder trail upward. If that trail didn't contain a folder sitting directly beneath one of the top-level libraries, it gave up and skipped the clip — and then skipped it again on the next pass, forever. It now falls back to the folder-ownership map, which answers by folder identity rather than by name (names repeat constantly — 02_Openers exists under 44 clients); on a sample it resolved every single case. About 2,800 clips already indexed without a client are, separately, still invisible to searches that name a client — they need a deliberate repair, which is queued for review rather than done automatically. One product line had become its own client, and it made a folder look half-empty. Home Chef's Tempo line is a product line, not a customer — but its Approved-Ads folder sits at the top level, where the system reads a folder name as the client it belongs to, so 36 clips were filed under Home Chef Tempo and were invisible to anyone searching Home Chef. In one reported ad folder that was 23 of the 50 clips actually there; a Home Chef search returned 17. Those clips are now filed under Home Chef, where Tempo remains available as a product line to filter by, and that folder returns 40.

How is the library re-grouped, and why does it take a while?

Search works by grouping similar clips together, and those groupings are periodically rebuilt so they reflect everything that has been added since. Rebuilding used to be attempted for the whole library at once — which ran out of memory partway through and produced nothing, silently. Because it produced nothing, the tidy-up step that attaches loose clips never ran either, so the rebuild that was supposed to improve things left the library slightly worse.

It is now rebuilt one client at a time. That keeps each step small enough to finish, and it means a problem with one client's footage no longer costs everyone else's rebuild — that client is retried once, then recorded and skipped, and the rest continues.

It also picks up where it left off. There are over a hundred clients and the whole pass takes longer than a single run is allowed to last, so progress is saved as each client completes and the next run continues from there. Largest clients go first, because those are the ones whose groupings have drifted most.

A handful of very small clients — a few clips each — cannot be grouped meaningfully at all. Rather than failing on them repeatedly, they are given a single catch-all grouping so their footage still appears in search.

Some clips could not be found by search at all — what happened?

Search groups the library into visual clusters and looks inside the closest ones. A clip that belongs to no cluster is therefore unreachable: however well it matches what you typed, there is no route to it. About a third of the indexed clips were in that state — roughly 506,000 of 1.6 million scenes.

The uncomfortable part: the routine whose entire job is to prevent this was causing it. It attached loose clips to the nearest group, but it was allowed to attach them to the wide umbrella groupings rather than the specific ones search actually looks in, and it recorded the result in the wrong place. Then it checked its own work by re-reading that same wrong place, and reported success. Every signal said healthy.

The automated health check had the same blind spot, which is why nothing raised it — it reported about 8,000 affected clips when the real figure was over 500,000.

How we proved it rather than assumed it: we took one clip and searched for "more like this" using that clip itself as the starting point. It did not appear in its own results — not in the top 50, not in the top 300 — even though a direct database check ranked it as the single closest match to itself, a perfect 1.0. A near-identical alternate take of the same shot, which happened to be attached correctly, came back first. After the repair, the clip returns near the top of its own results.

Both the repair routine and the health check now measure what search actually uses. A clip is never attached to a group belonging to a different client. Clips whose client has no groupings at all are reported for a proper re-grouping rather than quietly attached to nothing.

If you ever shared a link to a clip and it bounced you back to the search page, this is very likely why — and it is the reason a share link should be resolved by looking the clip up directly rather than by hunting for it in a list of results.

Some ads showed no "Footage used" even though we had the data

Fixed. We recorded which clips a project references in two places: a full cut list with timings, and a simpler list of references. The panel only ever read the first one — so an ad whose project had references but no readable cut list showed nothing at all, even though the footage was on file. That was about 3,700 ads, and roughly one project in eight has references without a readable timeline (After Effects builds and some nested edits).

The panel now falls back to the simpler list, and says which one it is using: where a real cut list exists you get the clips in edit order with their exact in and out points, and where only references exist you get the clips without an implied order — because we did not read one and should not pretend otherwise.

Can I filter to a particular shoot, or the last few shoots?

Yes. Typing a shoot code such as FRMDG260724-T10293 opens that shoot directly. And there is now a list of a client's shoots — newest first, each with how many clips it holds — so a picker can offer real shoots to choose from instead of only the ones that happen to be on the page you are looking at.

That distinction caused a confusing bug worth knowing about: a shoot picker built from the current results shows "no matches" for every client when the page is empty, which is exactly what you see when you arrive. It looks like the client has no shoots. It only ever meant there was nothing on screen yet.

"The last five shoots" is also a different question from "shot in the last month" — a client with a quiet quarter still has a most-recent five. The list answers the first; the shoot-time range answers the second.

The clip counts beside each shoot only count footage search can actually return, so a shoot offered in a picker will never come back empty. Note that shoots are a raw-footage idea — ads are cut, not shot — so filtering by shoot is filtering to footage.

Why did searching a shoot code sometimes not open the shoot?

Typing a shoot code such as COOKU250619-R10055 should open that shoot. For some clients it opened a partial list instead — because those clients put the shoot code in the filenames, and the search matched the filenames before it recognised the code as a shoot. One reported case listed 24 clips out of the shoot's 64.

Because the two cases are indistinguishable from the outside, the footage app was right to refuse rather than present a partial list as the shoot — so what users actually saw was "couldn't confirm this as a production". A shoot code now opens the shoot: the same search returns all 64.

Nothing else changed. Only a search that is exactly a shoot code is affected; searching a filename, a phrase, or a job number behaves as before.

Why do the talent and producer pickers sometimes list people with no results?

They no longer should. Those lists were built from every asset on record, including clips that had been retired or deactivated, while the rest of the filters count only clips search can actually return. So a name could appear in the picker and then match nothing.

In practice the gap was tiny — one name out of 1,839, and no producers — but the lists and the search now describe the same set of footage, which is what makes a picker trustworthy.

What exactly does "Footage used" show now?

Only the clips in the Premiere project that ad was actually cut from, with the exact in and out points of each cut. Nothing else.

It used to show more than that, in three ways. It gathered clips from every project sharing the same J-number rather than only this ad's project. It then added a visual-similarity guess for anything not on the timeline, listed after the real cuts. And when no timeline could be matched at all, that guess became the entire answer — so a list that looked like "footage used" was sometimes just "footage that resembles this ad".

Resemblance is a fair answer to a different question. If a clip is in the panel now, the project file the ad was built from references it.

One nuance we got wrong first time and corrected: the panel shows the footage of the whole project, not only the one timeline named after that ad variant. Editors normally build a variant by nesting a shared sub-timeline inside it, and a nested timeline is not a media file — so reading only the variant's own timeline found almost nothing. On one reported ad it showed a single clip where twenty-four belonged, and about a fifth of all projects are built that way. The ad's own cuts are still listed first, in cut order.

Two consequences worth knowing. If an ad has no readable Premiere timeline — an After Effects build, or a project that references only graphics — the panel is now empty and says why, rather than offering a plausible-looking list that leads nowhere. And where a clip's recorded in/out can't be trusted (camera originals sometimes carry a one-hour timecode offset that makes the raw number meaningless), the clip still appears — the project did cut it — but its preview falls back to a best match instead of a wrong window.

Every clip shows, whatever its approval state (2026-10-22). The panel lists every clip the ad was cut from for every role; a clip that is restricted or not yet approved carries its warning on the card rather than being left out.

Work-in-progress material no longer appears anywhere in search. Half-finished cuts were being treated as if they were delivered work, which is misleading in this panel above all. About 36,400 work-in-progress-only clips have been retired from the index. Around 4,050 clips that sit in a work-in-progress job and in a real footage or approved-ads folder were deliberately left alone — those are genuine footage that a work-in-progress job happens to reference, and removing them would have hidden real material.

Why did "Footage used" sometimes show another client's clips?

Because a filename was being treated as an identity. When we work out which raw clips an approved ad was built from, we read the Premiere project file and match the media filenames inside it against our library. That match used the filename alone — so if two clients each had a file with the same name, the ad was linked to both copies.

The files that collide are not footage. They are the shared furniture of every edit: a "feed safe" overlay, a whoosh sound effect, a green tick graphic, a clip called 1.MOV. One overlay existed as a separate copy in sixteen different clients' libraries. Measured across 344,085 links, 30.4% pointed at another client's asset, and the file type gives it away — graphics and sound effects crossed clients 78–92% of the time, while real video (.mp4) crossed only 4.5%.

The match is now scoped to the ad's own client. Only that client's copies are linked; a genuinely shared stock library is used only when the client has no copy of its own, so real stock keeps its credit. We deliberately did not solve this by banning graphics — a client's own product photo really does appear in their ads, and 3,819 of those links were legitimate. Scoping by owner keeps those and drops the 18,343 that were not.

The same boundary is now enforced when results are served, not only when links are created. So the roughly 49,700 old cross-client links that already exist can no longer surface, and "Ads using this" is answered by the clip's own client rather than by whatever client happened to be recorded on the link — the route by which one brand's ads could appear against another brand's footage.

One related fix: a client written Naked & Thriving in one place and Naked and Thriving in another was treated as two different clients, so 1,134 links were unreachable — the ad existed, its footage existed, and nothing connected them. The two spellings now match, along with eight other variants across about 67,000 rows.

What is still not linked, and why. We only read project files from one of Iconik's project trees. A project filed under Work In Progress Jobs is never read, so its ad shows no "Footage used" — that is the case for The Farmers Dog's J1153, and for most job-numbered project files overall. Separately, static carousel ads that were never shot as a production have no shoot to resolve, so "From this production" is genuinely empty for them rather than broken.

How do we know nothing is missing from search?

Every night the system walks the Iconik libraries top down — Raw Footage, Client Collections and Approved Ads, about 54,000 folders and 656,000 folder placements — and compares what Iconik actually holds against what search can actually return. Folders named Archived, Deleted, Forbidden or Do Not Use are left out with everything beneath them, and clips Iconik marks inactive are left out too. The comparison then repairs what it can, and reports what needs a person.

The audit half never writes anything, and the repair half never invents anything: repairs are replayed from Iconik and the source files, so no label, description or AI-derived value is ever copied in from somewhere else.

What the first full run found. The largest problem was not missing footage but missing folder membership: 113,647 folder placements that Iconik has and search did not, on clips that were already fully indexed. So the clips answered searches normally while being invisible when you browsed the folder they live in. One line of code was responsible — a nightly folder refresh had been failing silently every hour for ten days, and a bug in how it recorded "last run" meant it kept telling itself it had nothing to do. Both are fixed, and the folder placements are being restored.

Footage genuinely absent from the index: 57,809 clips, and the mix matters more than the number. About 48,700 are stills — product and PDP photography in client asset packs — around 6,000 are video, roughly 1,100 are audio, and about 1,300 are not searchable media at all (Photoshop files, PDFs, an artwork file, even a font). Iconik reports no media type for these, which is exactly why they were never picked up. They are now on a tracked to-load list, video first, with the not-media tail recorded as deliberately out of scope rather than retried forever.

Three big numbers turned out to be measurement errors, and it is worth saying so. 72,871 clips looked like they had a stale folder trail; reading the data properly showed 816. 55,025 footage-to-ad links looked like they crossed between clients; almost all of them are shared production items — a "feed safe" overlay, a whoosh sound effect, a green tick graphic — that exist as a separate copy in every client's library, so a filename match linked all of them at once. Real footage (.mp4) crosses clients in only 4.5% of links, against 83–92% for graphics and sound effects. And 14,106 clips looked stuck mid-ingest; 14,102 of them had simply arrived that week and were being processed normally.

Nothing is deleted automatically. Cleaning up stale records — 48,517 cached folders that appear to be gone, and 3,820 folder placements Iconik no longer reports — is held back for explicit approval, and even then each folder is re-checked against Iconik individually and removed only if Iconik confirms it no longer exists. A folder we simply failed to reach is reported as a reachability problem, not treated as deleted.

Clean bill of health on the part that matters most: every active, finished video and image the walk encountered has scenes and embeddings. Nothing in this audit implicates the search pipeline itself.

How do we check that the AI reviewers in agentic search agree with editors?

Agentic search (switched on in staging only) has AI reviewers score each result 0–10 against what you asked for. Once a week a report compares those scores with what people actually did: the OK and Irrelevant result labels reviewers give in the feedback viewer, and the clips editors added to a collection or downloaded. A score of 7 or more counts as the AI saying “right”, 3 or less as “wrong”, anything between as no opinion. The report gives the agreement rate and a chance-corrected agreement score (Cohen’s kappa), overall and per client, and lists the 20 clips where the AI and the people disagreed most — what was searched, what the clip shows, the AI’s score and its reason — so the reviewers’ instructions can be corrected where they go wrong. Labels like Ignore or Bad search are not used, because they say nothing about whether that one clip was right.

The same job looks for rewordings the AI found useful again and again — for example searching elderly when you typed senior and finding clips it scored 8 or more, in at least three separate searches. Those are proposed as new synonyms for the search vocabulary in a spreadsheet that a person reviews; nothing is added automatically. An approved synonym is used only when a search comes back nearly empty, until someone promotes it to everyday use. Nothing in this changes how ordinary search ranks results.

How are cluster names and descriptions generated?

Each cluster's label and description are derived directly from what its member scenes have in common — the detected objects and AI scene descriptions the text pipeline already produced. The most distinctive shared terms (frequent inside the cluster, rare across others) become the label and keywords — deterministic, instant, and no extra AI pass required. Super-clusters (the larger umbrella groups) are named the same way from the union of their sub-clusters' terms.

Because naming depends on that scene text, clustering runs in two steps: the geometry step groups scenes by embedding similarity (and can run while the text pipeline is still backfilling), then the enrich step names everything from scene text and swaps the new run in. Until naming finishes, search keeps serving the previous clustering run — so you never see half-named clusters. Very large clients (over ~50K scenes) are clustered in parallel chunks whose clusters are then merged wherever their centroids coincide — same result, hours faster, and one huge client can no longer stall the whole run.

In the bubble chart, hovering a cluster shows a tooltip with its name, description, and scene count; clicking opens the full detail tray.

API Reference

Interactive API docs (Swagger UI) live at /docs (ReDoc at /redoc, OpenAPI schema at /openapi.json) — use the Authorize button with your session token or the shared X-API-Key to try any endpoint in place. The full written reference is docs/api.md in the repo.

All endpoints except /v1/config, /v1/health, /v1/auth/login, and /v1/webhooks/iconik require a session token: Authorization: Bearer <session_token>. The browser UI handles this automatically after signing in with your @tubescience.com account. Sessions last 24 hours.

Running locally: developers can run the whole app on their machine with ./run_local.sh — it talks to the same cloud Supabase, Iconik and Gemini as production (secrets pulled from GCP), but bypasses the Google login (IS_LOCAL=1): every request is treated as the local developer with admin rights, and the login page detects this via GET /v1/auth/mode and signs you straight in. This flag is never set in deployed environments.

POST /v1/auth/login
Exchange a Google OAuth2 access token for a 24-hour session token. Body: { access_token }. Returns { session_token, email }. Use Authorization: Bearer <session_token> on all subsequent calls. No auth required (this is the login endpoint).
POST /v1/auth/logout
Invalidate the current session. Send the session token in the Authorization header. The Sign-Out button in the UI calls this automatically.
GET /v1/config
Returns the Google OAuth client ID needed to initialise the Sign-In button. No authentication required. Returns { google_client_id }.
POST /v1/search

Flat semantic scene search using pgvector HNSW. Embeds query with gemini-embedding-2 and returns the closest scenes by cosine distance.

Request body:
  • query (string, required) — plain English description. Empty string when using image or asset search.
  • client_id (string, default "") — restrict to one client. Empty = all clients. When set, uses a per-client partial HNSW index for faster search.
  • limit (int, default 9, max 100) — page size: number of scenes returned per request.
  • offset (int, default 0) — pagination offset; skip the first N ranked results. Use with limit to page through deeper matches.
  • media_type (string OR list[string]) — narrow to one or more media types: video, image, audio, or gfx. GFX selects design-element graphics (logos, arrows, end cards, overlays), and image deliberately excludes them — pass both to get every image. Accepts a single string ("video"), a list (["video","audio"]), or a comma-joined string ("video,audio"). Omit / null / [] = no filter (all types). On the GET endpoint repeat the query param: ?media_type=video&media_type=audio.
  • min_duration_seconds / max_duration_seconds (float) — scene duration filter.
  • keyword (string) — STRICT filter on Iconik metadata.Keywords. Tags are read from every place Iconik keeps them (since 2026-09-29, tags on about 181,000 assets that the filter used to miss are included, such as HelloFresh’s “Client footage” tag); each term must EQUAL a whole tag — case- and space-insensitive, never a substring (Katie matches the tag Katie but not katie barker; use [katie barker] for that). Comma-separated terms OR together (dog, broll matches either tag). Enforced in both v2 modes — results never include non-matching assets. You can also type it straight into the search box: dog opening box keyword: Kona (aliases keywords: / tag: / tags:; put the directive at the end — everything after the colon is the tag list), or use brackets: [Katie] washing hair filters strictly to Katie-tagged assets while "washing hair" runs as the normal semantic query (multiple […] groups OR together). Prefix-directive and bracket-only queries suppress AI suggestion / Related chips — recommendations off an exact tag lookup rarely make sense; bracket queries with a real remainder keep them.
  • Advanced-mode strict pre-filters — talent, supervising_producer, transcript_phrase, objects, actions, subjects (each listed term must be visibly present; a gender-word subject becomes a gender requirement), and created_from/created_to (shoot time: inclusive date range on the asset's Iconik creation date, YYYY-MM-DD). All apply as hard pre-filters on the candidate pool in every engine, and work on /v2, /v3 and hierarchical search. The UI exposes shoot time under Filters → 'Shoot time (created between)'.
  • Collection-less discovered footage — production media uploaded by the Premiere panel outside any collection (discovered via .prproj references) counts as Raw Footage / TubeScience by its intrinsic identity, so it appears under the default filters like any other raw footage.
  • raw_footage_only (bool, default false) — only assets whose root ancestor collection is LucidLink.Footage (raw footage).
  • ads_only (bool, default false) — only assets whose root ancestor collection is Client Collections. Surfaced in the UI as the "Client Collections" Asset-source option. Applied as a pre-filter in every search engine (flat, v2, hierarchical).
  • approved_ads_only (bool, default false) — only assets whose ancestry contains the "Approved Ads" tree. Approved Ads is a mid-tree collection (its root is "Picture Lock Ads"), so it is matched anywhere in the ancestor chain rather than only at the root. Surfaced in the UI as the "Approved Ads" Asset-source option.
  • sources (string[], default []) — multi-select asset-source filter: any of "Raw Footage", "Client Collections", "Approved Ads", OR'd together (e.g. Raw Footage + Approved Ads in one query). Empty or all-three = no filter. Preferred over the three booleans above, which AND together and can't express a multi-source selection. Backs the UI's "Asset source" multi-select pills; applied in the flat, v2, and hierarchical engines.
  • ancestor_collection_id (string) — restrict to assets whose Iconik ancestor collection chain includes this ID. Efficiently uses the GIN index on ancestor_collections. Pass the Iconik collection UUID.
  • ancestor_collection_ids (string[]) — list of Iconik collection UUIDs. Assets matching any are returned (OR logic). Combined with ancestor_collection_id if both are sent. Each value still uses the GIN index, so multi-collection queries stay fast. All provided IDs are applied as-is — the server never silently drops any of them.
  • exclude_archived (bool, default true) — skip assets with archive_status = ARCHIVED.
Response:
{
  "query": "golden retriever on beach",
  "results": [
    {
      "id": 12345,                       // scene row ID
      "asset_id": "uuid",
      "title": "Asset title",
      "scene_index": 3,                  // 0-based scene number within asset
      "start_seconds": 12.4,
      "end_seconds": 18.1,
      "duration_seconds": 5.7,
      "similarity": 0.843,               // cosine similarity 0–1 (higher = closer)
      "media_type": "video",             // string OR ["video","audio"] OR omit for all
      "client_id": "The Farmers Dog",
      "iconik_url": "https://app.iconik.io/asset/<asset_id>",
      "metadata": { "Keywords": "...", ... },
      "date_modified": "2025-03-12T10:00:00Z",
      "selected": false,                 // true if hearted by current user
      "all_scenes": [                    // all scene boundaries for this asset
        { "start_seconds": 0.0, "end_seconds": 5.2 },
        { "start_seconds": 5.2, "end_seconds": 12.4 }
      ]
    }
  ],
  "total": 9,
  "limit": 9,
  "offset": 0,
  "page_size": 9,
  "page_number": 1,
  "returned": 9,
  "has_more": true,
  "query_time_ms": 38
}

Intent-aware hybrid ranking. A Gemini Flash classifier labels each query's intent (speech / visual / on_screen_text / objects / mixed / unknown) and emits per-channel weights for the five scoring signals — visual embedding, transcript, OCR, detected objects, and scene description. The score becomes w_vec·cosine_sim + w_t·rank(transcript) + w_o·rank(ocr) + w_obj·rank(objects) + w_sd·rank(scene_desc). "Podcast about marketing" weights transcript heavily; "$50 OFF banner" weights OCR; "red wine bottle" weights vector + objects. Low classifier confidence or mixed / unknown intents fall back to admin defaults stored in sync_config as weight_default_<channel>. The UI renders the classification + the weight pills below the search bar; the response root carries query_intent and each result row carries text_rank_<channel> for inspection. The classifier also receives the active-filter context — the client being searched, the collection/folder names the search is scoped to, and any talent/producer filters — so query tokens that match a client, folder, production code, or talent name are read as filter entities rather than content cues ("Kassandra washing hair" routes on "washing hair", not the name), and channel choice can lean on what plausibly exists in that client's catalog. Weights are also vocabulary-grounded: when your search is filtered to folders or a client and the query leans on the objects channel, the terms are checked against the objects actually detected in that footage — if none exist there, the objects weight is cut and shifted to scene-description + visual (a small "objects ↓ not in filtered footage" pill appears when this happens). The vocabulary is cached per folder/client and refreshed in the background, so this adds no search latency: a scheduled job rebuilds each folder's or client's vocabulary well before its six-hour snapshot expires, and a folder or client that is too slow to read keeps its previous snapshot instead of holding up the rest.

Strict text gate. text_required (default true) drops scenes whose transcript_tsv doesn't match the query — e.g. "podcast" no longer surfaces visually-similar talking-head ads that never say the word. When the query has zero text matches anywhere in the index the gate auto-disables, so descriptive queries like "woman in red dress" still hit pure-vector results.

v2 + hierarchical: full-pool aggregation, capped at top 50 assets. When v2 runs against the hierarchical engine, asset-level aggregation sees the full matching scene pool (capped at 5K scenes), then the asset list itself is trimmed to the top 50 assets by best per-scene score before the response is built. Previously a broad query like "dog playing" — which lit up 1000+ matching scenes spanning 700+ assets — took 15–30s end-to-end because every asset's full timeline was hydrated and JSON-encoded. The 50-asset cap covers ~5 pages at the default limit=9, which is well past the depth editors actually scroll, and trims the heavy steps (_attach_scene_boundaries + JSON encode) roughly in proportion to the asset-count reduction. For broader exploration, narrow the query or use the hierarchy facet sidebar to drill into a specific cluster.

Latency contract. Four coordinated optimizations land together: (1) _hybrid_rerank's SQL adds an @@-OR predicate so the per-channel GIN indexes serve as the planner's filter; (2) the hierarchical-search asset verifier is fire-and-forget on all pages (matching the flat-search behavior); the brief dead-preview state is handled by the frontend's Preview unavailable overlay and assets are inactivated in the background — verifier_ms 500–1,200 ms → ~10 ms; (3) fetch_full_pool_scenes no longer forces enable_indexscan = OFF; the planner uses the scenes_pkey bitmap heap scan — full_pool_fetch_ms ~900 ms → ~150 ms; (4) _hybrid_rerank caps the text-rerank input pool to the top 200 rows by vector similarity. ts_rank_cd is CPU-bound per row, so capping at _RERANK_TOP_N=200 cuts rerank work ~7.5× on broad pools (1,500+ scenes). Rows outside the cap keep pure-vector hybrid_score and merge back in the final sort — empirically rare for a low-vector row to overcome the gap via text alone.

wrapper_timings. Every /v2/search response now carries a wrapper_timings dict broken down per phase: mode_lookup_ms, inner_hierarchical_ms (or inner_flat_ms), full_pool_fetch_ms, trim_ms, attach_boundaries_ms, aggregate_ms, score_floor_ms, wrapper_total_ms. Hierarchical responses also include full_pool_count + scenes_after_trim + assets_count + inner_total_ms so you can attribute the latency budget without cross-referencing logs. Identical line is emitted at INFO level on the server as [search_v2] mode=<mode> wrapper_timings=....

Text-retrieval arm (both v2 modes). Vector retrieval can miss scenes whose transcript / on-screen text literally contains the query (a spoken quote embeds nowhere near a sentence describing it). The wrapper additionally retrieves the top text matches via the per-channel GIN indexes — same client/collection/archive/media filters — and merges them into the candidate pool before re-ranking, so verbatim quotes surface even when semantically distant. Wrap a phrase in quotes ("goo goo over dogs") to search the phrase alone; surrounding meta-words ("woman mentions … in podcast") are dropped from the text match.

Virtual sub-scenes. When a match is dominated by a temporal text channel — transcript, on-screen text (ocr), detected objects, or the per-segment visual scene_description (this is what localizes ACTION queries like "dog jumping": the embedding finds the scene, the description line pins the seconds where it happens) — each result row carries virtual_start_seconds / virtual_end_seconds / matched_fragment / matched_channel: a sub-scene spanning the match from its first to last hit inside the dominant channel's text. The UI uses it as the matching scene — chips jump to and label the sub-window, the green timeline band spans it, and hover-play starts there instead of the canonical scene start. Positioning uses ts_headline + a proportional character-to-time mapping — accurate to ±30-60s for normal text density. Short scenes (<30s) leave the virtual fields null and behave as before. Works in both flat and hierarchical v2 modes.

Pagination. Flat search is Top-K — there is no exact universe count. Use limit as page size and offset to skip earlier ranked results. has_more is true when the page is full (more ranked rows may exist downstream); page_number = floor(offset / page_size) + 1. Deeper pages widen the HNSW ef_search probe automatically to preserve recall.

GET /v1/search?q=…&client_id=…&limit=…
Same as POST /v1/search via query parameters. Useful for direct links and curl one-liners. Supports q, client_id, limit, offset, media_type, min_duration_seconds, max_duration_seconds, keyword, raw_footage_only, ads_only, exclude_archived, collection_id.
POST /v2/search
Asset-level results — one row per asset, each annotated with the scenes that matched the query. Assets are sorted by their best matched scene's score (the strongest single hit). Body is identical to POST /v1/search; limit and offset paginate at the asset level.

Internally /v2/search dispatches to either the flat or hierarchical engine. The engine is admin-tunable via sync_config.search_v2_mode ("flat" or "hierarchical", default flat) and the change applies on the next request — cache TTL is 60s, the picker invalidates it on save.

Response (extra fields vs /v1/search):
  • results[].matching_scenes[] — one entry per matched scene: { scene_id, scene_index, start_seconds, end_seconds, scene_start_seconds, scene_end_seconds, score, similarity, matched_channel, virtual_*, matched_fragment }. start_seconds/end_seconds are the matching window — for text-dominated matches (matched_channel set) that's the sub-scene spanning the match; the parent scene's canonical bounds always ride on scene_start_seconds/scene_end_seconds. Sorted in the order the engine returned them (strongest first).
  • results[].best_score — the max hybrid score across matching_scenes; what we sort assets by. Can exceed 1.0 when the transcript-text boost is large, so don't render it as a percentage directly.
  • results[].best_similarity — the max pure vector cosine across matching_scenes, bounded in [0, 1]. Use this for the per-asset percentage badge.
  • results[].metadata — full Iconik metadata. Any key containing warning with a meaningful value renders as a banner pinned to the top of the result card: the real warning field in red ("AVOID USING …" editor notes), test fields like TESTWARNING in yellow-orange (softer notes, e.g. "Old Packaging"). Placeholder values (None, null, N/A, empty) are treated as "no warning" and never banner — Iconik stores an empty warning field as the literal string None.
  • results[].all_scenes — full asset timeline (so the UI can mark non-matching scenes too). On each card's progress bar, scene boundaries render as ticks and every matching_scenes entry renders as a green band spanning the scene's full start_seconds→end_seconds range.
  • mode — which engine produced these results (flat | hierarchical).
  • result_kind — what produced this result set, on every search-shaped endpoint: search (ranked semantic search) or a deterministic lookup — talent_name / supervising_producer_name (the query was exactly a known name), job_number (bare J-code browse), asset_name (exact/partial title match), asset_id (UUID lookup), sku (product-tag match), production (production-id browse / "from this production"), keyword (keyword:/tag: directive), more_like_this (seeded by an existing asset/scene), footage_used / ads_using (footage↔ad lookups). A talent name plus content words ("sabrina cooking") is still search — the kind reports what drove the results, not which filters applied.
  • scene_pool — how many scenes were fetched from the engine before grouping by asset. The endpoint overfetches 5× so a typical asset page comes back full.
  • token_usage — per-search Gemini token accounting, on every search mode: embed_tokens_est (query embed, ~4 chars/token), intent_prompt_tokens/intent_output_tokens (exact from the classifier's usage metadata — 0 when the classification came from cache), generative modes add gen_prompt_tokens/gen_output_tokens, and total_est sums the legs. Deterministic lookups (name/J-code/SKU/production browse) report {"total_est": 0} — no model ran. The UI shows "· ~N tok" at the end of the results meta line in all modes.
POST /v3/search
The /v2/search engine behind a simplified filter model — same body and response contract except three filters (/v2/search itself is unchanged):
  • media_source — who made the material: "tubescience" (Raw Footage folders + the Approved Ads tree) or "client" (Client Collections uploads). Default: ["tubescience"].
  • media_type — what it is: "raw_footage" or "ads". Ads are identified by the intrinsic definitions (classified asset_kind='ad' or Approved-Ads-tree membership), so client ads inside Client Collections are cleanly distinguished from the raw footage next to them, and anything not affirmatively an ad counts as raw footage (not-yet-classified assets never vanish). Default: ["raw_footage"].
  • media_format — what /v2 calls media_type: video / image / audio / gfx (string, list, or comma-string). No default — all formats.
Multi-select: each filter accepts a list (or comma-string) and values OR within a filter, AND across filters — e.g. {"media_source": ["client"], "media_type": ["ads"]} = client ads only. The ads kind includes Recharm-harvested clips (segments chopped out of finished ads, *_recharm_clip) even when they live under the Raw Footage tree — they are edited client material, so a raw_footage filter correctly excludes them. Spelling is forgiving ("Raw footage", "raw-footage", "ad" all normalize); unknown values are a 422. The /v2 fields sources / asset_kinds are rejected here (422) since they overlap this model — use /v2/search if you need them. Every other /v2/search field (query, client_id, collections, talent, skus, seed_asset_id, …) works as-is, and the response is the /v2 payload plus a v3 echo of the resolved filters. Auth: session or X-API-Key, like /v2/search. Reachable both directly on Cloud Run and through the Vercel proxy alias (semantic-search-tawny.vercel.app/v3/… — rewrite in vercel.json).
GET /v1/parse
Structured query decomposition — the query understanding the search performs internally, exposed for client "advanced search" UIs. ?q=&client_id=&ancestor_collection_ids= (scope resolution like /v1/suggest) returns components — one entry per detected term with kind (talent / producer / transcript / object / sku / shot_type / exclude / freshness / footage_type), the resolved value, matched_text and span ([start, end) character offsets into your exact query string, one component per occurrence — for highlighting and edit detection), plus field + cardinality naming the real search-request field to set when the control changes (e.g. change Talent → talent: ["…"]; edit Transcript → transcript_phrase: "…"). Every control acts as a strict filter — each field hard-filters results in both engines, never just re-ranks: Transcript maps to transcript_phrase, a case-insensitive substring match on the scene's spoken transcript only (a keyword:/tag: directive component instead carries field: keyword, the strict Iconik-Keywords tag filter). A producer's lead-in is part of its component — produced by Alma Sever highlights as one Producer span (value stays the clean name), so the residual drops the connector. Also returns residual_query (your query with the detected spans removed — the free-text remainder the vector search ranks on) and intent ({label, confidence, weights} — "action" is an intent label, never a span). Example: footage of David Craig talking about wet dog food produced by Tyler Sawyer → Talent=David Craig, Transcript=wet dog food, Producer=Tyler Sawyer, each with its span. Talent/producer catalogs are scoped to your client when a scope is given. The /v1, /v2 and /v3 search responses echo the same components array, so a client can build controls off the initial search with no extra round trip — /v1/parse re-parses (e.g. after the user edits the box) without running a search. Read-only; session or X-API-Key.
POST /v1/collection-add
Add-to-collection signal (Flawless Footage app). POST when an editor adds a search result to a job collection/bin. Body: {asset_id, scene_id?, search_id?, query?, bin?, client_id?, reporter?} — pass search_id (and ideally query) from the search that produced the result, and reporter (the user's email; session callers get it defaulted). client_id may be an FF brand id (resolved via brand aliases) and falls back to the asset's client. Accepts X-API-Key. This is the strongest relevance signal in the system: it becomes a hardwired example for generative search (an exact repeat of the query then searches by the added asset's own embedding — the ⚓ note under the search bar), and counts as ×3 pin votes in the learned ranking patterns.
POST /v1/search/generative
Generative search — the ✨ Gen mode tab on the search page (UI-only: session auth, not available to API keys). The AI's system prompt is pure context — your query is never inside it. The prompt template fills as soon as you select a project (changing the client, the collections tree, or the impersonated user warms it via POST /v1/search/generative/context, cached 10 min): today's date, who is searching (email + role), the client and selected folders, the job brief for the project (resolved from folder names or shoot codes you searched → the Monday job's Google-Docs brief, fetched once and cached), digests of what the selected footage contains and of the client's produced ads, relevant clusters, your dated search history within the client, and scene descriptions of the footage the team recently selected. Your query then arrives as the final user turn — a seed the model interprets in that context, with the task framed as predicting what you are looking for next. Earlier searches whose result you took into an edit (downloads carrying their query, thumbs-up ratings) are replayed as hardwired example turns — query in, the chosen asset's scene description out — so repeated queries snap to what actually worked. The AI also tokenizes your query in context — identifying its objects, actions, subjects and setting (a bare "pen" resolves to the product pen) — shown as a 🏷 tokenized query string above the results — each word of your query as a chip tagged object / action / subject / setting (query verbs that name actions tag as actions, nouns as objects; the AI's contextual buckets win) — and returned as query_tokens + tokenized_query; hard presence gates (a named object must be visible, a named action must happen, spatial prepositions must hold) are derived from your query, not from the generated prose. The generated description is embedded and searched in hierarchical (cluster) mode. Admins get an extra dropdown right of the ✨ Gen pill to impersonate another user's context (their history, their pairs; GET /v1/search/generative/users, admin-only) — picking a user also jumps to the client and project folders they last worked in (their most recent search's client + most recent folder-scoped search there). Impersonation is session-only — it is never remembered; every page load starts back at "Context: me". The Compare page has the same dropdown next to each pane's ✨ Gen pill (one shared selection): both panes adopt that user's context and the shared search bar jumps to their last client. The line under the search bar shows "✨ searched as: …" with a view prompt expander revealing the exact system prompt, example turns and seed the AI received, the token count, and the impersonated user when one is active; if the rewrite is unavailable the mode falls back to searching your words directly and says so. Same request body as /v1/search/hierarchical (+ optional impersonate_email); response adds generated_text, generated_prompt, generative_fallback, context_cached, impersonated.
POST /v2/search/agentic
Agentic search — on in staging and production (since 2026-10-09; while off every agentic route answers 503 {"detail": "agentic search is disabled"}; switch: agentic_search.enabled.<env>, kill switch agentic_search.paused.<env>). AI agents (Gemini 3.8 Flash; an operator can move any agent to Meta Muse with sync_config agentic_search.model.<role>, not done) count every candidate in your scope, scan all of them in parallel, dig for more with new searches and check the best clips' frames — until every candidate is scanned (exhausted) or the run's time or money limit; whatever ends it, a last frame check runs on every match (score 8+) not checked yet (up to 60; allowed past the time limit, soft budget and call cap, up to the hard budget + $1; stop_reason stays what ended the search; it is built from every stored score — the same scores the live results rank — after waiting at most one call timeout for reviewers still answering, also after Stop scanning). Send the POST /v2/search body (plus optional soft_budget_usd / hard_budget_usd, which can only lower the limits, and allow_questions); the answer comes back at once with a run_id and the ordinary search's first page. Then long-poll GET /v2/search/agentic/{run_id}?wait_s=20 (it holds up to 25 s until something changes — poll back-to-back until status is done, stopped or failed; pass the last answer's ranking_version as &since= and it answers, after at least a second, as soon as new scores land): while the run works its results are live — ranked from every score so far, so they reorder as clips are scored (results_final: false) — and they are the usual /v2/search cards plus agent_score (0–10, 8+ is a match), agent_reason, agent_rank, baseline_rank, agent_verified and agent_explain (the reviewer's score and reason, what the frames showed, and how the score was built — e.g. 8 + 2 → 10), with stats (incl. unverified_matches: matches the frame check has not looked at yet, and scanned_seconds: the total length of the clips scored, with scanned_seconds_known = how many had a known length), rounds_log (per round, also scanned_seconds), a live plan, spend_usd and continuation. If the run asks a question (status: awaiting_answer), answer with POST …/{run_id}/answer ({"answers": [0]}, your own words, or {"skip": true}). Need more matches? (the search page's Dig even deeper button) — POST …/{run_id}/continue resumes a finished run that still has candidates (+3 min and +$5 a press; 409 "all candidates scanned" when nothing is left, 409 "run limits reached" after 2 h / $50). Stop scanning — POST …/{run_id}/stop (no body) while the GET's stoppable is true answers {"status": "stopping"} at once: the run starts nothing new, lets the AI calls in flight finish (their scores count), still runs the last frame check on every unchecked match, and ends stopped / stop_reason: "user_stop" — continuable with …/continue; the GET reads stopping meanwhile (stop_requested: true); a repeat answers the same; 409 (reason finished / stale / awaiting_answer / verifying) when there is nothing to interrupt. Raw footage outranks finished ads unless you ask for ads; lookups (production id, file name, job code) are returned as they are. Limits per run come from sync_config (code defaults $5 soft / $8 hard / 3 min; the response's budget shows the run's own) — a measured staging run scanned 2,749 candidates in 147 AI calls for $0.99 in 161 s. No capacity: 429 with retry_after (seconds; also the Retry-After header) — retry then. Each run executes on its own isolated worker; the GET's dispatch (worker, or fallback / in_process when it ran on the API) and worker.revision say where. With an X-API-Key every request needs exactly one client (client_id in the body, or ?client_id= on the other routes; otherwise 400 client_scope_required) and a run is visible only with its own client (404 otherwise); an identical key POST within 10 s returns the same run (replayed: true). brand_id is accepted as on /v2/search but scopes nothing on its own. No callbacks — polling is the contract. The full contract for Flawless Footage and other API callers — every field, status, stop reason, error code, recipe and curl example — is docs/agentic-search-api.md in the repo; /docs shows the typed models under the tag agentic.
POST /v2/search/agentic/media
Agentic search with an image or a short video as the query (same switch as above). Since 2026-10-12 every agentic search from the search page first shows what it is looking for — the one-line need and its must-have / nice-to-have / avoid lists, editable, the must-haves keeping every specific word of your query (since 2026-10-13) — and scans nothing until you press Start search; Cancel (before the start, or while it runs) ends it with no agentic result and the plain results stay. API callers opt in with confirm_intent; POST …/confirm and POST …/cancel in the contract doc. Multipart: file plus exactly the form fields of POST /v2/search/image, and optionally soft_budget_usd / hard_budget_usd, allow_questions, reporter and brand_id. Scans the whole pool like a text run (the query picture's visual neighbours to the end of scope plus what its one-line description finds; the first 240 candidates judged on frames, the rest by text and every match frame-checked), and keeps the picture for the run's lifetime so Dig even deeper stays visual. Returns at once with a run_id, query_media ("image" / "video") and exactly the first page /v2/search/image returns for the same upload; then poll, answer and continue with the same routes as a text run. The agents describe your picture (that becomes the run's query) and look at each candidate's keyframe as well as its description. The picture is kept only in the server's memory while the run lasts and 30 minutes after — never stored. Errors: 400 empty upload (or, with an API key, no ?client_id= in the URL), 403 when the upload names another client than the URL, 422 not an image or video, 429 busy, 503 off.
GET /v2/search/agentic
Recent agentic runs, newest first — for a history list and to reopen a run. Each entry: run_id, status, stop_reason, query, query_media, client_id, created_at, finished_at, spend_usd, calls, rounds and stats.matches. With a session: your own runs (?client_id= narrows to one client). With an X-API-Key: ?client_id= is required and lists that client's runs; &reporter=<email> narrows to the runs started for that person. limit 1–100 (default 20).
GET /v1/suggest
Contextual autocomplete — suggestions once you've typed 3+ characters, scoped to your active client/folder filters. Params: q, client_id, ancestor_collection_ids (a folder resolves to the client it belongs to), limit (default 10). Blends what people actually search (popularity-ranked history — a query whose most recent run in your scope returned no results is never suggested), talent and producer names, J-codes (when your typing looks like one), footage themes (cluster labels weighted by scene count), what's visually in the scoped footage (the per-client/folder content vocabulary), and recent asset titles. Each suggestion carries kind — the result_kind picking it will produce (talent_name, job_number, asset_name, search, …) — plus source and an optional detail ("searched 14×", "212 scenes", "in this footage"). On the search page the query box shows these in a dropdown: arrow keys + Enter to pick, Esc to dismiss; the badge on each row says what kind of match it is. Read-only; session or X-API-Key.
POST /v1/search/image
Image-to-scene search (scene-level results). Multipart form upload — file field contains the image bytes. The image is embedded directly with gemini-embedding-2 (same model and 3072-dim vector space as video scenes), so similarity scores are directly comparable to text search results. Supports the same filter fields as POST /v1/search as additional form fields, including limit (page size) and offset (pagination). Returns the same response shape with has_more / page_number / page_size.
Asset-level variant: POST /v2/search/image takes the same multipart upload — and also accepts a video file (a representative frame is auto-extracted and embedded) — but returns asset-level results (same shape as /v2/search, with each asset's matching scenes), and supports the full /v2/search filter set. The web app's Image tab uses this. media_type may be sent as repeated fields or as one comma-joined field (video,image) — before 2026-10-07 the comma-joined form matched no asset, so an Image-tab search with the default Video + Image filter showed only folder cards.
POST /v1/search/asset

Find scenes similar to an existing asset's first scene. One of asset_id (UUID) or asset_name (substring of assets.title, case-insensitive) is required.

Fast path. If the asset is already indexed in our DB, we reuse the first scene's stored embedding — no Iconik download, no Gemini call. Latency drops from ~minutes to ~ms. The response's reference.embed_source field reports which path was taken: "scene_cache" (fast) or "iconik_proxy" (slow — proxy downloaded and re-embedded on the fly).

Same filters as POST /v1/search, including limit (page size) and offset (pagination). Response adds a top-level reference object: { asset_id, resolved_from, asset_title, proxy_url, keyframe_url, embed_source } plus the standard pagination fields (has_more, page_number, page_size).

POST /v1/search/expand

AI-assisted query expansion. Given a vague seed query ("dog doing something"), returns more-specific REPLACEMENT queries ("dog eating", "dog playing fetch", "dog sleeping on couch") that the user can click to refine the search.

Request body:
  • query (string, required) — the seed query.
  • n (int, default 6) — number of candidates to return.
  • drift_threshold (float, default 0.65) — cosine-similarity floor. Each candidate is embedded with gemini-embedding-2 and dropped if its similarity to the seed embedding is below this value. Prevents drift to unrelated topics (which would produce poor scene match scores at search time).
  • exclude (string[], default []) — queries already shown to the user. Sent to the LLM with a "do not propose any of these" instruction and any leak-through is filtered out server-side (case-insensitive). Powers the UI's "More suggestions" button — each batch surfaces genuinely new candidates instead of repeats.
Pipeline: gemini-3.5-flash-lite generates ~2× the requested candidates as JSON; each is embedded and filtered by cosine similarity vs the seed; duplicates collapsed by lowercased text (highest sim kept); sorted by similarity descending.

Response:

{
  "query":           "dog doing something",
  "candidates": [
    {"query": "dog eating wet food",       "similarity": 0.8421},
    {"query": "dog playing fetch in park", "similarity": 0.8104},
    {"query": "dog sleeping on couch",     "similarity": 0.7833}
  ],
  "drift_threshold": 0.65
}

Used by the hierarchical-mode discovery chips. Failure (Gemini outage, bad JSON) returns an empty candidates list — the search itself never blocks on this.

POST /v1/search/hierarchical

Two-stage search: first matches cluster centroids (HNSW on halfvec), then retrieves scenes from the top clusters. Returns individual scenes (one result per scene, not grouped by asset) plus a hierarchical facet tree for navigation. Supabase only.

Request body:
  • query (string, required) — same plain English query as flat search.
  • limit (int, default 9) — scenes per page.
  • offset (int, default 0) — pagination offset.
  • filter_client_id (string) — narrow to a specific client (click-to-filter from facets).
  • filter_super_cluster_id (int, default -999) — narrow to a super-cluster (-999 = off).
  • filter_cluster_uuid (string) — narrow to a single cluster UUID (click-to-filter from facets).
  • ancestor_collection_id (string) — same collection filter as flat search; applied to the scene pool after the search within the matched clusters, before re-ranking (and before the v2 top-50-asset trim). When the collection doesn't overlap the query's top clusters at all, the pool falls back to the whole collection (un-clustered, embedding-ranked) instead of returning zero results. Since 2026-09-03 the same fallback also fires when the pool is merely starved — fewer than 50 scenes on a search that has a hard filter (a required object, a keyword, a folder): a single ambiguous word (blowout for L'Ange, which is a hairstyle there) used to land in the wrong 30 clusters and show 9 assets while 606 matched; now the hard filter does the narrowing and the embedding ranks within it.
  • ancestor_collection_ids (string[]) — multiple collection IDs with OR logic (same semantics as flat search; all provided IDs are applied as-is).
  • media_type (string OR list[string]) — same multi-select semantics as flat search above.
  • exclude_archived (bool, default true) — same as flat search.
Response:
{
  "query": "golden retriever on beach",
  "results": [
    {
      "id": 12345,
      "asset_id": "uuid",
      "title": "Asset title",
      "scene_index": 3,
      "start_seconds": 12.4,
      "end_seconds": 18.1,
      "duration_seconds": 5.7,
      "similarity": 0.713,               // cosine similarity as decimal (0–1)
      "media_type": "video",
      "client_id": "The Farmers Dog",
      "asset_source": "Raw Footage",     // "Raw Footage" | "Client Collections" | "Approved Ads" (derived from ancestor_collections)
      "iconik_url": "https://app.iconik.io/asset/<asset_id>",
      "scene_cluster_id": "uuid",        // cluster this scene belongs to
      "_cluster_label": "Dogs outdoors", // label of the nearest cluster
      "_super_cluster_id": 4,
      "selected": false
    }
  ],
  "total": 14823,                        // total scenes in matching clusters
  "query_time_ms": 62,
  "search_mode": "hierarchical",
  "facets": {
    "clients": [
      { "client_id": "The Farmers Dog", "cluster_count": 3, "scene_count": 4210, "top_similarity": 0.821 }
    ],
    "super_clusters": [
      { "client_id": "The Farmers Dog", "super_cluster_id": 4, "cluster_count": 2,
        "scene_count": 1840, "top_similarity": 0.821, "top_label": "Dogs outdoors" }
    ],
    "clusters": [
      { "cluster_uuid": "uuid", "cluster_id": 12, "super_cluster_id": 4,
        "client_id": "The Farmers Dog", "label": "Dogs outdoors — park",
        "description": "Scenes of dogs playing in parks...",
        "n_scenes": 920, "similarity": 0.821 }
    ]
  }
}
POST /v1/scene/download
Download just the bytes for a single scene's clip (not the whole asset). Server fetches the Iconik proxy URL, runs ffmpeg -ss <start> -i <url> -t <duration> … with HTTP-range reads on S3, and streams an mp4 back via StreamingResponse — designed for slow connections.

Body: { "asset_id": "uuid", "start_seconds": 12.5, "end_seconds": 18.0, "quality": "original" | "720p", "scene_index": 3, "title": "..." }. scene_index and title are optional and used only to build the download filename.
Container: original is a stream copy (never re-encoded), so its container is chosen from the master's own codecs — .mp4 when MP4 can hold them (most footage), QuickTime .mov when it cannot. MP4 cannot hold ProRes video or the uncompressed (PCM) audio that camera masters often carry, so an MP4 copy of such a master fails outright; files MP4 can hold keep their usual .mp4 name. 720p is re-encoded to H.264/AAC and delivered as .mp4. If a clip cannot be produced at all the endpoint returns 502 with ffmpeg's reason rather than an empty file.
Quality: "original" trims the asset's full-resolution master with -c copy — no re-encode, may snap up to ~2s to the nearest keyframe. The master is pulled from the internal LucidLink footage server (nginx), which holds the originals Iconik keeps in cold storage; if it's not there it falls back to Iconik's own /files/ master. If no full-res master is reachable from either source the request fails with a 502 — it never downgrades to a lower-resolution copy (an editor asking for full resolution is better served by a clear error than a quietly degraded file). Hover the "Original" button to see the exact source URL the clip will be pulled from. "720p" re-encodes from the proxy (already 720p H.264) with libx264 -vf scale=-2:720 -c:a aac. Hard 60-min duration cap.
Response: video/mp4 with Content-Disposition: attachment; filename=<title>_scene_<idx>_<startMs>-<endMs>_<quality>.mp4. Auth required (session or X-API-Key).
Backup source (720p only): when a 720p request finds no Iconik proxy, the clip is cut from the GCS media-cache copy (480p @ 2fps) via a signed URL — always re-encoded so the cut stays accurate despite sparse keyframes, and the filename quality tag becomes cache480p so it's obvious the clip came from the backup. (original never uses this backup — it fails instead.)
Downloads teach the ranking. Every download is recorded, and when it carries the search that produced it (optional search_id / query — the search page sends them automatically), it counts as a strong positive vote for that result under that query in the learned relevance patterns: scenes editors actually pull into their edits float up for that query (and similar phrasings) in future searches. Double-clicks don't double-vote, explicit reviewer classifications can still outvote a download, and recording never delays or fails the download itself. API callers get the same learning by passing search_id (from the /v2/search response) or a query string, plus an optional reporter email for analytics.
Watching counts too — a little. How long you actually watch a result (hover-play on a card, playback in the detail popup) is recorded as a much weaker, directional signal: only genuinely engaged views count (a mouse passing over a card doesn't; roughly 3–10 seconds of real watching depending on scene length), at most one per person per day per result-and-query, each worth a fraction of a vote. Several editors lingering on the same result for the same query will gently float it up — but a single download or reviewer classification always outweighs any amount of watching, and short abandoned views are never counted against a result.
GET /v1/scene/download?asset_id=…&start_seconds=…&end_seconds=…&quality=original|720p
Query-param variant of the POST above — semantics are identical, but it lets you grab a clip with one curl line:
curl -H "X-API-Key: $KEY" \
  "https://semantic-search-supabase-td24dyhr7a-uc.a.run.app/v1/scene/download?asset_id=ABC-123&start_seconds=12.5&end_seconds=18.0&quality=720p" \
  -o clip.mp4
Optional: scene_index, title (used only to label the download filename).
GET /v1/selected?client_id=…&date_from=YYYY-MM-DD&date_to=YYYY-MM-DD
List selected (hearted) scenes. All parameters optional. Returns up to 200 results ordered by selection date descending.
POST /v1/select/toggle
Toggle selection state for a scene. Body: { client_id, asset_id, scene_id }. Returns { selected: true|false }.
GET /v1/clients
Returns the list of distinct client IDs present in the asset library.
GET /v1/asset/{asset_id}/media
Fetch fresh proxy video URL and keyframe thumbnail URL for an asset from the Iconik API. Results are not cached — each call hits Iconik. Returns { proxy_url, keyframe_url, proxy_source }. When Iconik takes longer than ~4s (env ICONIK_MEDIA_BUDGET_SECONDS) or fails, proxy_url falls back to a 1-hour signed URL for the downscaled 480p copy in the GCS media cache (gs://semantic-fingerprinting-media-cache/<client>/<asset_id>.mp4); proxy_source is then gcs_cache instead of iconik.
GET /v1/asset/{asset_id}/transcript
Full transcript as JSON { has_transcript, text, cues } — text is the scene-ordered blocks (each prefixed with its [mm:ss–mm:ss] window); cues is a timed array [{ start, end, text }] (≤12-word windows, same chunking as the subtitle track). Backs the result modal's synced transcript panel, which highlights words karaoke-style in lock-step with playback and lets you click a word to seek. Auto-opens for audio assets (no visuals → the transcript is the content); a toggle for video. The same cues drive the grid result cards: audio cards show the scrolling transcript (instead of a waveform) and karaoke-highlight words as the muted hover-play advances (click a word to seek); video cards show a karaoke spoken-text caption over the clip (replacing the native subtitle track; falls back to it when an asset has no cues). Shown only when has_transcript is true. Auth required (session or X-API-Key).
GET /v1/asset/{asset_id}/subtitles.vtt
WebVTT track of per-scene transcripts. Bottom-anchored at line:80% so cues sit above the in-card progress bar. Returns an empty WEBVTT body if no scene carries a transcript yet (loads as a silent track).
POST /v1/feedback/judgment
Thumbs up / down on one result — teaches the ranking for that query immediately. Body: { query, verdict: "good" | "bad" | "clear", asset_id | scene_id, search_id?, client_id?, rank?, engine?, reporter? }. query is the query the results were ranked for — the query field of the /v2/search response. good is a +4 vote for that clip on that query (more than a frame-verified Dig deeper match or a collection add at +3, so the editor's pick leads), bad −3, clear withdraws the vote; one vote per (reporter, query, clip) — a second verdict replaces the first. The learned ranking is recomputed in the same request and every server picks the change up within seconds, so the next search for that query already ranks with it. Server-to-server callers forward the end user in reporter (else the session user). Returns { status: "recorded" | "withdrawn" | "unchanged", verdict, asset_id, query_norm, pattern: { action: "pin" | "demote" | null, evidence } }. GET /v1/feedback/judgment?query=… returns the caller's standing votes for that query, { query_norm, verdicts: { asset_id: "good" | "bad" } } (API-key callers may name the user in reporter). 400 on a missing query / unknown verdict / no asset; 503 until the database migration for it is applied.
POST /v1/feedback/bad-result
Records a thumbs-down on a specific result row. (Driven by API callers, e.g. the footage edge function; the search page's own thumbs use /v1/feedback/judgment below.) Since 2026-10-08 a recorded report is also that reporter's thumbs-down vote for the query — the clip sinks for that search at once, exactly as /v1/feedback/judgment with verdict: "bad". The backend stores the query, the engine that produced this row (v1 / v2 / hierarchical / image / asset / expand), the full request filters, the offending scene_id + asset_id, rank in results, scores, and an optional free-form note. Body: { query, engine, request_filters, scene_id, asset_id, rank_in_results, similarity, hybrid_score, feedback_type, notes? }. Dedup is at (email, query, feedback_type, scene_id) — re-submitting with a new note overwrites the stored note but preserves the original timestamp. Returns { id, status } where status is recorded or already_recorded. Stored in the search_feedback table for offline relevance work. Both feedback endpoints also accept an optional search_id — every /v2/search response now returns one — which joins the report to a stored trace of that exact search (query, filters, ranking internals, returned results), making every piece of feedback precisely attributable. Result tiles can be dragged into the right order (the ⠿ handle) — the order you set is saved and the moved result teaches the ranking: dragged up counts as a positive vote for that query, dragged down as a negative (tiles that merely shift to make room don't vote). Every search card offers mark all: OK / Irrelevant result / Ignore — one click classifies all listed results at once (each still editable individually afterwards). Result tiles load frozen on the returned scene's first frame with a scrub bar along the bottom: the highlighted band marks where the returned scene sits in the full asset, and dragging the bar scrolls through the video. The viewer's collapsible 📊 Classification activity tray (top of the page) graphs how many classifications of each kind were made per day — stacked bars over a 7/30/90-day range with a per-label legend and totals; each legend entry is a checkbox that shows/hides that kind (the chart rescales live) — Ignore and Bad search start hidden since they aren't result-quality signals. A second chart graphs user ratings (▲▲ great / ▲ helpful / ▼ not helpful / ▼▼ bad) per day, and a Side-by-side / Stacked toggle switches both charts between grouped daily columns and stacked bars (the default). In the admin feedback viewer, each listed result additionally carries ✓ relevant / ✗ irrelevant toggles so reviewers can grade individual results, not just whole searches — these labels build the evaluation set used to tune ranking. The viewer is full-width (previews scale with the window), result previews play the returned scene chunk on hover (uncropped — the full frame letterboxes in the tile; looping within the matched window where the feedback identifies one; leaving pauses), and ALL filtering sits on a single compact line at the top — Rating, Date, Classification, plus Search terms (matches the recorded query), a User picker, a Client picker, and a precise From/To date-and-time range, with a Clear button. Each row now also names the client the search was scoped to, shown beside the request filters control — the piece of context that tells you at a glance whether a complaint about irrelevant results was a ranking problem or a wrong-client problem. The Client picker filters on it and lists only clients that actually have feedback, with the number of reports next to each. It finds a row whether the client was recorded on the reported asset or only as the footage app's brand — so searches whose asset has since been deleted still turn up under their client instead of disappearing. Filtering and paging happen on the server, so the page loads one page at a time instead of pulling the whole log into the browser — it opens in a fraction of the time it used to, and every filter combination stays fast. Search traces also power GET /v1/search-history: your recent searches (optionally scoped to a client) with the exact query, filters, and result counts — each entry re-runnable verbatim since the original request is stored. You always see only your own history; admins may query any user's (?email=) or everyone's (?email=all). Server-to-server integrations (e.g. the footage panel) should pass the end-user's email in the optional reporter field on /v2/search — the same field they already send on feedback — so their users' searches attribute to the right person instead of a generic API identity.
GET /v1/asset/{asset_id}/ocr.vtt
WebVTT track of on-screen text captured by Gemini Vision per scene. Top-anchored at line:15%, capped at size:95% of the video width, and styled green monospace on 50% black via an inline ::cue(.ocr) block, so it travels with the VTT body (no page CSS required) and renders simultaneously with the bottom subtitle track. Repeating OCR phrases are collapsed to "text (N)" within a scene, and consecutive identical cues are merged across scenes. Returns an empty WEBVTT body if no scene has OCR text (loads as a silent track).
POST /v2/assets/lookup
Current details for up to 200 clips you already know, in one call. Built for Flawless Footage Collections, so a saved clip shows its current warning, tags, production id and talent instead of what it looked like when it was saved. Send { asset_ids, ancestor_collection_ids } (the folders are required, exactly as for search) and get back { results, missing }: each result has the same fields as a search result, without scores. Ids that are unknown, outside the folders, or hidden by the approval gate come back in missing rather than as an error. The same change made large pages (200 results at a time) of search and folder browsing load about half a second faster.
GET /v1/collections
List your collections (owned by the signed-in user). Returns [{ id, name, owner, parent_id, created_at, item_count, is_system }]. On first call for a user the three system-root folders (Video, Audio, Pictures) are auto-seeded; they are returned with is_system: true and cannot be deleted. Results are ordered with system roots first.
POST /v1/collections
Create a collection or sub-folder. Body: { name, parent_id? }. If parent_id is provided the new collection becomes a child of that folder (used by the right-side Collections tray to build nested folders under Video / Audio / Pictures). Returns the created collection object.
DEL /v1/collections/{id}
Delete a collection (and all its items). Only the owner can delete. System-root folders (Video / Audio / Pictures) return 400 and cannot be removed.
GET /v1/collections/{id}/assets
List items in a collection, enriched with full asset data. Items can be whole assets (scene_index=null) OR trimmed scenes (scene_index + custom_start_seconds + custom_end_seconds). Returns [{ collection_id, asset_id, asset_title, added_at, scene_index, custom_start_seconds, custom_end_seconds, iconik_url, proxy_url, keyframe_url, title, media_type, client_id, date_modified, folder_path }] ordered by most recently added.
proxy_url and keyframe_url: uses the cached S3 presigned URL if it has ≥6 hours remaining; fetches a fresh one from Iconik otherwise.
POST /v1/collections/{id}/assets
Add an item to a collection. Body: { asset_id, asset_title, scene_index?, custom_start_seconds?, custom_end_seconds? }.
Whole-asset bookmark: omit the three scene fields (this is what the right-side Collections-tray drag-and-drop on the search page sends). Duplicates are silently ignored.
Scene-level bookmark: set scene_index plus optional custom_start_seconds / custom_end_seconds to remember in/out points different from the scene's natural boundaries. The same asset can appear multiple times in one collection as long as the (asset_id, scene_index) pair is unique. GET /v1/scene/download against those bounds gives you the trimmed clip back.
DEL /v1/collections/{id}/assets/{asset_id}?scene_index=N
Remove an item from a collection. Without scene_index, removes the whole-asset bookmark (the row where scene_index IS NULL) — does NOT touch scene-level rows for the same asset. With ?scene_index=N, removes only the row for that scene_index.
POST /v1/clusters/run
Submit a clustering Cloud Run Job for all 8 clients. The job runs isolated from the search service (8 vCPU / 16 GB). Pipeline: wipe existing clusters → PCA→UMAP→HDBSCAN per client (largest first) → Gemini labels + 2–3 sentence descriptions. Total time ~90 min. Monitor via GET /v1/clusters/runs.
GET /v1/clusters/run/{run_id}
Poll the status of a clustering run. Returns { status, n_scenes, n_clusters, n_super, error, started_at, finished_at }.
GET /v1/clusters/data?client_id=…
Return bubble chart data for the latest completed run for a client. Returns an array of cluster objects with position, size, label, and representative assets.
POST /v1/webhooks/iconik
Iconik webhook receiver. Validates Iconik JWT token and forwards asset to Mixpeek for indexing.
POST /v1/ingest/csv
Bulk ingest from an Iconik CSV export. Multipart form with file (CSV). Submits a Cloud Batch job to process each asset.
Usage Examples
Text search via curl
curl -s -X POST https://semantic-search-td24dyhr7a-uc.a.run.app/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "person jogging outdoors at sunrise",
    "client_id": "the_farmers_dog",
    "limit": 5
  }' | jq '.results[].title'
Text search via GET (shareable link)
curl -s "https://semantic-search-td24dyhr7a-uc.a.run.app/v1/search\
?q=golden+retriever+playing+fetch&client_id=the_farmers_dog&limit=3"
Image search via curl
curl -s -X POST https://semantic-search-td24dyhr7a-uc.a.run.app/v1/search/image \
  -F "file=@/path/to/reference.jpg" \
  -F "client_id=kitsch" \
  -F "limit=6" | jq '{count: .total, first: .results[0].title}'
List selected scenes for a client
curl -s "https://semantic-search-td24dyhr7a-uc.a.run.app/v1/selected\
?client_id=home_chef&date_from=2026-05-01" | jq '.total'
Toggle selection
curl -s -X POST https://semantic-search-td24dyhr7a-uc.a.run.app/v1/select/toggle \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"kitsch","asset_id":"<uuid>","scene_id":42}'
Create a collection and add an asset
# 1. Sign in and get a session token
SESSION=$(curl -s -X POST https://semantic-search-tawny.vercel.app/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"access_token":"<google_access_token>"}' | jq -r .session_token)

# 2. Create a collection
COLL_ID=$(curl -s -X POST https://semantic-search-tawny.vercel.app/v1/collections \
  -H "Authorization: Bearer $SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Best Dog Shots"}' | jq -r .id)

# 3. Add an asset
curl -s -X POST "https://semantic-search-tawny.vercel.app/v1/collections/$COLL_ID/assets" \
  -H "Authorization: Bearer $SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"asset_id":"6f92863c-a85f-11f0-a31f-822ef301e7e9","asset_title":"FRMDG250815-S68_IMG_0209"}'

# 4. List assets in the collection
curl -s "https://semantic-search-tawny.vercel.app/v1/collections/$COLL_ID/assets" \
  -H "Authorization: Bearer $SESSION" | jq '.[].asset_title'
Filter by duration and keyword
curl -s -X POST https://semantic-search-td24dyhr7a-uc.a.run.app/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "cooking montage",
    "min_duration_seconds": 5,
    "max_duration_seconds": 30,
    "keyword": "kitchen"
  }'
Search raw footage only
curl -s -X POST https://semantic-search-td24dyhr7a-uc.a.run.app/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "dog running on beach",
    "client_id": "the_farmers_dog",
    "raw_footage_only": true,
    "media_type": "video",
    "limit": 9
  }'
Filters
What does the "Asset source: Raw Footage" filter do?

When enabled (the default), search results are restricted to assets classified as raw production footage. Assets are classified automatically at startup using signals from their title and metadata:

  • Broll / BROLL in the title or Keywords field
  • Title matches a camera roll pattern (e.g. KTSCH240123-C10187, ACAM001_09101146_C009)
  • IMG_ prefix (phone / UGC footage)
  • Versioned take suffix (e.g. _V1-0014_)
  • metadata.type is directed or self_tape
  • Folder path contains _RAWS FOR ARCHIVE

Switch to All to include graphics, end cards, and other non-footage assets.

How do I see the last few shoots?

Type "last 5 shoots" (or "latest 10 productions", "most recent 3 shoots") with a client selected. Instead of a flat page of clips, you get one card per shoot — newest first, each showing its date, how many assets it holds, and a preview deck of what was shot. Click a card to open that shoot's footage.

The wording is deliberately narrow so it never hijacks a real search: the phrase has to end in "shoots" or "productions". "Last dog shoot of the summer" is still an ordinary search, and "recent footage" still means the freshness filter. Without a number you get the last 10.

Can I see only the newest shoots?

Yes — filter by shoot. Every asset carries a production code (e.g. FRMDG260724-T10293) identifying the shoot it came from, and search accepts a list of them: pick several and you get everything from those shoots and nothing else. Combine it with any other filter — talent, media type, a folder — and the conditions all apply together.

With no search words typed, this becomes a browse: newest first, a true result count, and pages you can walk through. For example, the five most recent Farmer's Dog shoots hold 497 browsable assets, and that is the number you get.

It is not the same as a date range. A date filter matches when an asset was added to search, not when it was shot: asking for "since the fifth-newest shoot's date" returned 2,893 assets where only 501 were actually from those five shoots, because older footage added recently sweeps in. Filtering by shoot asks the question directly. A client with a quiet quarter still has a most-recent five.

What does "Media type: Video" do?

Pill row of Video / Image / Audio — click each to toggle on / off. Multiple pills can be selected at once (e.g. Video + Audio). When none are selected the filter is dropped entirely and results include every media type. Default state: Video preselected.

Current library breakdown: ~172K video scenes, ~12.6K image embeddings, ~450 audio embeddings. Audio embeddings are generated by Gemini Flash describing the first 120s of each clip, then embedding that description.

What does "Library" mean on the Admin page?

Semantic Search is moving its ingestion from Iconik to the Flawless Library, one client at a time. A client marked Library is written by the Library consumer (library_sync, polling the Library's change feed every minute); the Iconik sync, drain, sweeper and repair jobs leave that client alone. A client marked Iconik (or unmarked) is ingested exactly as before. Search does not change: both sources land in the same tables.

The pilot clients are Mosaic_Test, client1 and Courtside; everything else stays on Iconik until it is handed over.

Approvals made in the Library count. For a Library client, an approval or rejection recorded in the Flawless Library takes precedence over Iconik's approval status; with no Library decision, Iconik's applies, and footage with no decision anywhere is searchable as before. Footage that was uploaded straight into the Library (not mirrored from Iconik) is indexed from the Library's own copy only: until the Library has a preview file ready for it, it simply waits and is picked up as soon as the preview arrives, rather than being looked up in Iconik, which has never seen it. If a file is replaced with new content under the same name, it is re-indexed; a change to its details alone is not.

Which footage is whose follows the brand's own configuration in Flawless Footage: the folders chosen under Footage Sources and the brand's production codes decide which client a Library asset belongs to. Staging search follows staging Footage's configuration, production follows production's. A folder two brands of different clients both list is left unassigned and reported rather than guessed.

Why is an asset "registered, awaiting approval"?

The Library tells us about an asset as soon as it exists, but Semantic Search does not process footage that Iconik has not approved (approval_status Not Approved, Requested or Mixed). Such an asset gets a row — so it is known, counted and ready — but no scenes, no embeddings, no download, and it does not appear in search, browse or folder listings. When its approval changes, the feed carries that change and the asset is processed within minutes.

An asset with no approval status at all is processed right away; only an explicit hold waits.

How does searching inside Graphics or Jobs in Footage work?

Typing a term inside the Graphics or Jobs section in Footage searches the Flawless Library's assets in that section for the brand: job exports, motion graphics, stills, and the projects behind them. A job's export is found by what is in it (pictures, and spoken and on-screen words once the text pass has run) as well as by its name; a Premiere project, PSD or Illustrator file is found by its name and section details only, since there is nothing in it to look at.

The Library decides what is in each section, so the usual footage filters (raw footage vs. ads, TubeScience vs. client) do not narrow a section search. Section assets show up in search within the hour after they appear in the Library. An asset whose title names a brand code we cannot match to a client is left out until that code is mapped. A preview video the Library made for a graphic is not shown as a result of its own: the graphic is.