<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mixpeek</title>
    <description>The latest articles on DEV Community by Mixpeek (mixpeek).</description>
    <link>https://dev.to/mixpeek</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F10980%2Fde8557b4-a968-40d1-b4e2-70fda3d938dd.png</url>
      <title>DEV Community: Mixpeek</title>
      <link>https://dev.to/mixpeek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mixpeek"/>
    <language>en</language>
    <item>
      <title>Best Contextual Advertising Platforms for CTV in 2026</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:08:01 +0000</pubDate>
      <link>https://dev.to/mixpeek/best-contextual-advertising-platforms-for-ctv-in-2026-50of</link>
      <guid>https://dev.to/mixpeek/best-contextual-advertising-platforms-for-ctv-in-2026-50of</guid>
      <description>&lt;p&gt;The best contextual advertising platform for CTV depends on where you sit in the buy. Anoki ContextIQ and Wurl match ads to the scene playing before the break. Peer39 and Gracenote supply program-level categories (genre, rating, mood, language) that buyers apply pre-bid in their DSP. IRIS.TV, owned by Viant, tags each video with an ID that travels in the bid stream. IAS Total TV reports the show, genre and rating an ad ran against. KERV adds object-level recognition inside the frame. If you are a publisher or CTV platform that wants to label its own video, a developer API such as Mixpeek fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Program level or scene level
&lt;/h2&gt;

&lt;p&gt;CTV contextual signals come in two grains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program level&lt;/strong&gt; describes the whole show or episode: genre, rating, network, language, mood. Most streaming publishers already have it, and it is enough for broad suitability and genre buys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scene level&lt;/strong&gt; describes a moment inside the show. A film labeled "drama" can hold a dinner party, a car chase and a funeral, and each suits a different advertiser. Scene signals matter when the ad should match what the viewer has just watched.&lt;/p&gt;

&lt;p&gt;Many buyers have neither. Peer39's May 2026 market digest cites its CTV Truths report: 60% of CTV bid requests still lack usable program-level signals. So the first question is often whether any content signal reaches the auction at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortlist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Grain&lt;/th&gt;
&lt;th&gt;How it reaches the buy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.anoki.ai" rel="noopener noreferrer"&gt;Anoki ContextIQ&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Scene&lt;/td&gt;
&lt;td&gt;Index Exchange Marketplaces, through existing SSP and DSP workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.wurl.com" rel="noopener noreferrer"&gt;Wurl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Scene&lt;/td&gt;
&lt;td&gt;Content Intelligence Platform, being integrated with Yahoo DSP, StackAdapt and Viant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.peer39.com" rel="noopener noreferrer"&gt;Peer39&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Program&lt;/td&gt;
&lt;td&gt;Pre-bid contextual and suitability categories in DSPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://gracenote.com" rel="noopener noreferrer"&gt;Gracenote&lt;/a&gt; (Nielsen)&lt;/td&gt;
&lt;td&gt;Program&lt;/td&gt;
&lt;td&gt;Categories built on Gracenote metadata, offered through Peer39 partner DSPs; Magnite was the first SSP to support them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://www.iris.tv" rel="noopener noreferrer"&gt;IRIS.TV&lt;/a&gt; (Viant)&lt;/td&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;A content ID passed in the bid stream, with signals such as genre, sentiment and brand safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://integralads.com" rel="noopener noreferrer"&gt;IAS Total TV&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Program&lt;/td&gt;
&lt;td&gt;Measurement of show, genre, rating and language on Disney, NBCUniversal, Paramount and Prime Video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kerv.ai" rel="noopener noreferrer"&gt;KERV&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Object&lt;/td&gt;
&lt;td&gt;Object-level recognition and ad-break targeting, part of Wurl's platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://mixpeek.com/use-cases/iab-taxonomy-classification" rel="noopener noreferrer"&gt;Mixpeek&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Scene&lt;/td&gt;
&lt;td&gt;API that classifies your own video into IAB Content Taxonomy 3.1 for your stack to send&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few notes from the vendors' own announcements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index Exchange describes Anoki's data as scene-level understanding of "content tone, sentiment, and subject matter", activated with no added platform fees.&lt;/li&gt;
&lt;li&gt;Wurl launched its Content Intelligence Platform on September 15, 2026, combining its own scene-level targeting with KERV, Peer39 and Viant's IRIS.TV.&lt;/li&gt;
&lt;li&gt;Gracenote's categories cover genres, ratings, content types and advisories, and buyers can target or exclude metadata such as mood, theme, subject and location. Buyers do not get show or episode titles.&lt;/li&gt;
&lt;li&gt;IAS lists pre-bid optimization for CTV as a roadmap item for Total TV.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You buy CTV and want the ad to match the moment:&lt;/strong&gt; Anoki or Wurl. Ask which SSPs and DSPs you can activate them in today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need genre, rating and suitability controls across many publishers:&lt;/strong&gt; Peer39, with Gracenote metadata if you want the program descriptors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already buy through Viant:&lt;/strong&gt; IRIS.TV video-level data is the shortest path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need proof of where the ad ran:&lt;/strong&gt; IAS Total TV for post-bid reporting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You sell the inventory and want to label it yourself:&lt;/strong&gt; classify your own video and send the categories in the bid request (below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you sign, ask what grain the product returns (program, fixed time window, or scene) and how the signal reaches the auction: a deal ID, a DSP segment, or fields in the bid request.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do we target CTV contextually without viewer data?
&lt;/h2&gt;

&lt;p&gt;Contextual targeting reads the content and ignores the person watching it. No device IDs, household graphs or cookies are involved. The signal comes from three places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bid request itself.&lt;/strong&gt; OpenRTB 2.6 has a content object under &lt;code&gt;app&lt;/code&gt; (and &lt;code&gt;site&lt;/code&gt;) with fields built for this: &lt;code&gt;genre&lt;/code&gt;, &lt;code&gt;livestream&lt;/code&gt; (0 for on-demand, 1 for scheduled linear), &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;channel&lt;/code&gt;, and &lt;code&gt;cat&lt;/code&gt; with &lt;code&gt;cattax&lt;/code&gt;. &lt;code&gt;cat&lt;/code&gt; holds IAB Tech Lab category IDs, and &lt;code&gt;cattax&lt;/code&gt; names the taxonomy they come from. In the AdCOM list of category taxonomies, 9 means Content Taxonomy 3.1. Watch the default: if &lt;code&gt;cattax&lt;/code&gt; is missing, the spec says Content Category Taxonomy 1.0 is assumed, so 3.1 IDs sent without it get read against the wrong tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program metadata.&lt;/strong&gt; Vendors like Gracenote and Peer39 describe the show, and buyers apply those categories in the DSP before bidding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The video.&lt;/strong&gt; Scene-level systems classify frames, speech and on-screen text, store the result with timestamps, and look it up at the ad break. Classification happens when the content is ingested, so the auction reads stored IDs and never waits on a model.&lt;/p&gt;

&lt;p&gt;A publisher controls this last option itself. The categories describe its own inventory and travel in standard OpenRTB fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classifying your own CTV inventory with Mixpeek
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mixpeek labels video the customer already owns. It buys no media and runs no exchange; your SSAI vendor or SSP carries the categories into the auction.&lt;/p&gt;

&lt;p&gt;The pipeline has three steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cut each episode into scenes.&lt;/strong&gt; Point Mixpeek at the bucket where the video lives (S3, GCS or another object store). The &lt;a href="https://docs.mixpeek.com/processing/extractors/multimodal" rel="noopener noreferrer"&gt;multimodal extractor&lt;/a&gt; splits video with &lt;code&gt;split_method: "scene"&lt;/code&gt; and a &lt;code&gt;scene_detection_threshold&lt;/code&gt;, and each segment comes back as its own document with &lt;code&gt;start_time&lt;/code&gt;, &lt;code&gt;end_time&lt;/code&gt;, a transcript, OCR text and a description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classify each scene against IAB Content Taxonomy 3.1.&lt;/strong&gt; The taxonomy is generated from IAB Tech Lab's published 3.1 file, and a unit test diffs every ID, name, parent and tier against that file, so a renamed category or moved parent fails the test. &lt;a href="https://docs.mixpeek.com/enrichment/taxonomies" rel="noopener noreferrer"&gt;Taxonomies&lt;/a&gt; can be materialized at ingest, which stores the categories on each scene document. Each scene gets tier 1 to tier 4 category IDs, a confidence score per category, and its timestamps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Send the categories at the break.&lt;/strong&gt; When a cue marks an ad break, look up the scene that ended at the cue and put its IDs in the content object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"livestream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"cattax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"cat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"210"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"216"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the 3.1 file, 210 is Food &amp;amp; Drink and 216 is its child, Cooking. A cookware buyer can now bid on the kitchen scene inside a show whose genre is drama.&lt;/p&gt;

&lt;p&gt;Scores move from category to category, so set a cutoff for each one from labeled examples before the IDs go into an auction.&lt;/p&gt;

&lt;p&gt;To see what the classifier returns before writing code, paste a transcript or a short scene description into the free &lt;a href="https://iab-contextual.mxp.co" rel="noopener noreferrer"&gt;IAB contextual classifier&lt;/a&gt;. It ranks categories across all four tiers by score. The longer walkthrough is in &lt;a href="https://mixpeek.com/guides/how-does-scene-level-contextual-targeting-work-for-ctv" rel="noopener noreferrer"&gt;how scene-level contextual targeting works for CTV&lt;/a&gt;, and &lt;a href="https://mixpeek.com/guides/ctv-bid-request-content-categories" rel="noopener noreferrer"&gt;why CTV bid requests lack content categories&lt;/a&gt; covers the supply side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.indexexchange.com/press/press-releases/anoki-launches-with-index-marketplaces/" rel="noopener noreferrer"&gt;Index Exchange: Anoki launches with Index Marketplaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.advanced-television.com/2026/09/15/wurl-launches-content-intelligence-platform/" rel="noopener noreferrer"&gt;Advanced Television: Wurl launches Content Intelligence Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nielsen.com/news-center/2024/gracenote-optimizes-contextual-connected-tv-ad-targeting/" rel="noopener noreferrer"&gt;Nielsen: Gracenote optimizes contextual connected TV ad targeting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adexchanger.com/tv/viant-acquires-data-biz-iris-tv-to-expand-its-programmatic-ctv-reach/" rel="noopener noreferrer"&gt;AdExchanger: Viant acquires IRIS.TV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://integralads.com/insider/ias-launches-total-tv-solutions-for-connected-tv/" rel="noopener noreferrer"&gt;IAS launches Total TV solutions for connected TV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.peer39.com/market-signal-digest" rel="noopener noreferrer"&gt;Peer39 market signal digest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectcontent" rel="noopener noreferrer"&gt;OpenRTB 2.6, Object: Content&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list_categorytaxonomies" rel="noopener noreferrer"&gt;AdCOM 1.0, List: Category Taxonomies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://iabtechlab.com/standards/content-taxonomy/" rel="noopener noreferrer"&gt;IAB Tech Lab Content Taxonomy&lt;/a&gt; and the &lt;a href="https://github.com/InteractiveAdvertisingBureau/Taxonomies/blob/main/Content%20Taxonomies/Content%20Taxonomy%203.1.tsv" rel="noopener noreferrer"&gt;3.1 file&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>adtech</category>
      <category>ctv</category>
      <category>advertising</category>
      <category>api</category>
    </item>
    <item>
      <title>Tools That Analyze the Hooks in Your TikTok and Meta Video Ads</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:07:25 +0000</pubDate>
      <link>https://dev.to/mixpeek/tools-that-analyze-the-hooks-in-your-tiktok-and-meta-video-ads-2olh</link>
      <guid>https://dev.to/mixpeek/tools-that-analyze-the-hooks-in-your-tiktok-and-meta-video-ads-2olh</guid>
      <description>&lt;p&gt;The tools that analyze the hooks in your top TikTok and Meta video ads fall into three groups. Motion and Superads connect to your ad accounts and score each video's opening with a hook metric built from thumb-stop and early-view data. Segwise and Atria use AI to find and tag hooks in your own ads and tie them to results, with Segwise built for mobile apps and games. Crux tags creative elements across connected Meta, TikTok and Google accounts and adds competitor research. Foreplay surfaces top hooks in competitors' ads. If you want to break your own ad archive into scenes and query the first three seconds yourself, a developer API such as Mixpeek fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "hook analysis" means in practice
&lt;/h2&gt;

&lt;p&gt;The hook is the opening of a video ad, usually the first two or three seconds. Teams ask three different questions about it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is this hook holding attention?&lt;/strong&gt; A metric question, answered from platform data like 3-second views over impressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What kind of hook is it?&lt;/strong&gt; A tagging question: problem callout, product to camera, creator talking head, text-only open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which hook types win across hundreds of ads?&lt;/strong&gt; A comparison question, answered by joining the tags to performance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first question needs a connection to the ad account. The second needs something that watches the video. The third needs both, plus a consistent tag set so two strategists label the same hook the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortlist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;What it does with hooks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://motionapp.com" rel="noopener noreferrer"&gt;Motion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;DTC brands on Meta and TikTok&lt;/td&gt;
&lt;td&gt;Hook score from thumb stop and first frame retention; AI tags that include hook and creative angle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.superads.ai" rel="noopener noreferrer"&gt;Superads&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Brands and agencies wanting a scored view&lt;/td&gt;
&lt;td&gt;Hook Score for video ads; AI labels that include hook type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://segwise.ai" rel="noopener noreferrer"&gt;Segwise&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Mobile apps and games&lt;/td&gt;
&lt;td&gt;AI tags for hook headline, hook tactic and hook visual format, tied to ad network and MMP data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://getcrux.ai" rel="noopener noreferrer"&gt;Crux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Performance teams that also want competitor research&lt;/td&gt;
&lt;td&gt;AI tagging, creative deep-dives and custom breakdowns on connected ad accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tryatria.com" rel="noopener noreferrer"&gt;Atria&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;DTC teams on Meta and TikTok&lt;/td&gt;
&lt;td&gt;An AI analyst that finds the personas, hooks and messages that keep winning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.foreplay.co" rel="noopener noreferrer"&gt;Foreplay&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creative strategists doing research&lt;/td&gt;
&lt;td&gt;Saved ads with transcription; a competitor tracker that identifies top hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://vidmob.com" rel="noopener noreferrer"&gt;Vidmob&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Enterprise brands&lt;/td&gt;
&lt;td&gt;Element-level creative analysis and creative scoring across major ad platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which tools break an ad into hook, scenes and messaging?
&lt;/h2&gt;

&lt;p&gt;Many creative analytics tools work at the level of the whole ad: they attach tags like "hook: problem callout" or "format: UGC" to an ad ID and report on those tags. That is the right shape for weekly reporting.&lt;/p&gt;

&lt;p&gt;A per-scene breakdown is a different output. You get the ad cut at each shot change, with a start and end time on each piece, plus what was said, what text was on screen and what the shot shows. That lets you look at the hook as its own object, and also see what happens in the scenes after it: where the product first appears, when the offer lands, how long the creator stays on screen.&lt;/p&gt;

&lt;p&gt;Ask any vendor you evaluate whether their tags are per ad or per time range, and whether you can export them with timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  How creative strategists tag and compare hundreds of ads
&lt;/h2&gt;

&lt;p&gt;The workflow that holds up at a few hundred ads looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fix the tag set first.&lt;/strong&gt; Write down your hook types with a one-line definition and one example ad each. A tag set that drifts between people makes the comparison meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag the hook separately from the body.&lt;/strong&gt; An ad that opens on a creator and cuts to a product demo should carry both, at different times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join to performance by ad ID.&lt;/strong&gt; Keep spend, hook rate and conversions in the same table as the tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare within a format.&lt;/strong&gt; A UGC hook and a motion graphics hook rarely compete for the same placements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your ads live on Meta and TikTok and you want this done for you, Motion, Superads or Atria cover it. If you run an app, start with Segwise. If you want to save and study competitors' hooks, Foreplay or Crux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing hook analysis on your own ad archive
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mixpeek does not connect to your Meta or TikTok ad accounts and does not pull performance metrics. You bring the video files from your own object storage, and you join performance data yourself by ad ID. What it gives you is the per-scene breakdown described above, as an API you can query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cut every ad into scenes at ingest.&lt;/strong&gt; The &lt;a href="https://mixpeek.com/docs/processing/extractors/multimodal" rel="noopener noreferrer"&gt;multimodal extractor&lt;/a&gt; splits on visual change with &lt;code&gt;split_method: "scene"&lt;/code&gt;. Scenes longer than &lt;code&gt;max_segment_duration&lt;/code&gt; (30 seconds by default) are split further. Turn on transcription, OCR and descriptions so each scene carries what was said, the on-screen text and a description of the shot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"feature_extractor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"feature_extractor_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"multimodal_extractor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"input_mappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"video_url"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"field_passthrough"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"source_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"metadata.ad_id"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"split_method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"scene"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"scene_detection_threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"run_transcription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"run_ocr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"run_video_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each scene comes back as its own document with &lt;code&gt;start_time&lt;/code&gt;, &lt;code&gt;end_time&lt;/code&gt;, &lt;code&gt;transcription&lt;/code&gt;, &lt;code&gt;ocr_text&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; and a &lt;code&gt;thumbnail_url&lt;/code&gt;. If you would rather cut on a fixed clock, &lt;code&gt;split_method: "time"&lt;/code&gt; with &lt;code&gt;time_split_interval&lt;/code&gt; does that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pull just the hooks.&lt;/strong&gt; In a retriever, an &lt;a href="https://mixpeek.com/docs/retrieval/stages/attribute-filter" rel="noopener noreferrer"&gt;attribute filter&lt;/a&gt; on &lt;code&gt;start_time&lt;/code&gt; keeps only the opening segments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"stage_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"attribute_filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"stage_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"stage_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"attribute_filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"start_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"lt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Ask questions a tag list cannot.&lt;/strong&gt; An &lt;a href="https://mixpeek.com/docs/retrieval/stages/llm-filter" rel="noopener noreferrer"&gt;LLM filter&lt;/a&gt; judges each hook against a plain-language criterion. With &lt;code&gt;include_media: true&lt;/code&gt; the segment's media goes to the model along with its fields, so the judgment is made on what the opening shows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"stage_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"llm_filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"stage_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"stage_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"llm_filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Keep only hooks where a person holds the product up to the camera"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"include_media"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"include_reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Make your hook types stick.&lt;/strong&gt; A &lt;a href="https://mixpeek.com/docs/enrichment/taxonomies" rel="noopener noreferrer"&gt;taxonomy&lt;/a&gt; matches each segment against a reference collection, such as one example clip per hook type, and attaches the label. With &lt;code&gt;taxonomy_applications&lt;/code&gt; set to &lt;code&gt;execution_mode: "materialize"&lt;/code&gt;, every new ad gets labeled at ingest with the same definitions, so the tag set stops drifting between people.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://mixpeek.com/templates/creative-dna" rel="noopener noreferrer"&gt;creative DNA template&lt;/a&gt; sets most of this up in one step: scene-level ad documents with brand and ad ID, a separate searchable collection for ad copy, and clusters that group scenes into recurring visual patterns.&lt;/p&gt;

&lt;p&gt;On cost, the public rate for video search (scenes, speech transcript and descriptions) was 0.05 USD per minute of video when I checked on 10 November 2026, with on-screen text as a 0.10 USD per minute add-on (&lt;a href="https://mixpeek.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). A library of 1,000 thirty-second ads is 500 minutes.&lt;/p&gt;

&lt;p&gt;From there, export the labeled hooks with their &lt;code&gt;ad_id&lt;/code&gt;, join them to the spend and hook-rate numbers from your ad platform, and compare hook types in whatever reporting tool you already use.&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>video</category>
      <category>ai</category>
      <category>api</category>
    </item>
    <item>
      <title>How to fine-tune an embedding model with LoRA adapters</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Tue, 03 Nov 2026 16:05:42 +0000</pubDate>
      <link>https://dev.to/mixpeek/how-to-fine-tune-an-embedding-model-with-lora-adapters-425k</link>
      <guid>https://dev.to/mixpeek/how-to-fine-tune-an-embedding-model-with-lora-adapters-425k</guid>
      <description>&lt;p&gt;To fine-tune an embedding model with LoRA, freeze the base encoder, attach a small LoRA adapter to its linear layers, and train only the adapter with a contrastive loss on (query, relevant passage) pairs from your own domain. With Sentence Transformers this is a few lines: build a &lt;code&gt;LoraConfig&lt;/code&gt; with &lt;code&gt;task_type=FEATURE_EXTRACTION&lt;/code&gt;, call &lt;code&gt;model.add_adapter()&lt;/code&gt;, and train with &lt;code&gt;MultipleNegativesRankingLoss&lt;/code&gt;. A few hundred to a few thousand clean pairs is usually enough. The adapter comes out at a few megabytes. Afterwards you must re-embed your whole corpus, because the fine-tuned model's vectors are not comparable with the old ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LoRA fits embedding models
&lt;/h2&gt;

&lt;p&gt;Full fine-tuning updates every weight and leaves you with a second full copy of the model. LoRA freezes the weight matrix &lt;code&gt;W&lt;/code&gt; and learns a low-rank correction &lt;code&gt;B·A&lt;/code&gt; on top of it, scaled by &lt;code&gt;alpha / r&lt;/code&gt;. Only &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; train.&lt;/p&gt;

&lt;p&gt;For an encoder that matters in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small files.&lt;/strong&gt; The Sentence Transformers docs show a LoRA adapter for &lt;code&gt;bert-base-uncased&lt;/code&gt; at 9.44 MB, 2.14% of the base model's weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less forgetting.&lt;/strong&gt; The base weights never change, so the model keeps most of what it knew before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many domains, one base.&lt;/strong&gt; You can keep one encoder loaded and switch adapters per tenant, language or domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What your training data looks like
&lt;/h2&gt;

&lt;p&gt;An embedding model learns from pairs, not from next-token prediction. Each row is an anchor (usually a query) and a positive (the passage that should come back for it). A hard negative, something that looks relevant and is not, is optional and often helps.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;anchor&lt;/th&gt;
&lt;th&gt;positive&lt;/th&gt;
&lt;th&gt;negative (optional)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;torque spec for M8 flange bolt&lt;/td&gt;
&lt;td&gt;M8 flange bolts on the intake manifold are tightened to 25 Nm...&lt;/td&gt;
&lt;td&gt;M8 hex bolts on the exhaust bracket use 30 Nm...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refund window for annual plans&lt;/td&gt;
&lt;td&gt;Annual plans can be refunded in full within 30 days...&lt;/td&gt;
&lt;td&gt;Monthly plans renew automatically on...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Good sources of pairs: search logs with clicks, support tickets matched to the doc that resolved them, and headings matched to their sections. Clean pairs matter more than volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;This uses Sentence Transformers 3 or later with &lt;code&gt;peft&lt;/code&gt; installed (&lt;code&gt;pip install sentence-transformers peft datasets&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;peft&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LoraConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TaskType&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SentenceTransformerTrainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SentenceTransformerTrainingArguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers.losses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MultipleNegativesRankingLoss&lt;/span&gt;

&lt;span class="c1"&gt;# Load the base encoder in fp32 for training
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BAAI/bge-base-en-v1.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;torch_dtype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Attach a LoRA adapter; only these weights train
&lt;/span&gt;&lt;span class="n"&gt;peft_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoraConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FEATURE_EXTRACTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;inference_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lora_alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lora_dropout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peft_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Your (anchor, positive) pairs; add a "negative" column if you have hard negatives
&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_dict&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;torque spec for M8 flange bolt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund window for annual plans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M8 flange bolts on the intake manifold are tightened to 25 Nm.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Annual plans can be refunded in full within 30 days of purchase.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformerTrainingArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bge-base-lora&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_train_epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;per_device_train_batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2e-4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;warmup_ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformerTrainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;train_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MultipleNegativesRankingLoss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bge-base-lora-adapter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MultipleNegativesRankingLoss&lt;/code&gt; treats every other positive in the batch as a negative for each anchor, so larger batches give it more to learn from. LoRA trains comfortably at a higher learning rate than full fine-tuning; &lt;code&gt;2e-4&lt;/code&gt; is a common starting point.&lt;/p&gt;

&lt;p&gt;To use the result, load the adapter folder like any model: &lt;code&gt;SentenceTransformer("bge-base-lora-adapter")&lt;/code&gt;. You can also load the base model and call &lt;code&gt;model.load_adapter(...)&lt;/code&gt;. The &lt;a href="https://sbert.net/examples/sentence_transformer/training/peft/README.html" rel="noopener noreferrer"&gt;Sentence Transformers PEFT docs&lt;/a&gt; cover both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rank and alpha.&lt;/strong&gt; &lt;code&gt;r=16&lt;/code&gt; with &lt;code&gt;lora_alpha&lt;/code&gt; at &lt;code&gt;r&lt;/code&gt; or &lt;code&gt;2r&lt;/code&gt; is a sound default. Raise &lt;code&gt;r&lt;/code&gt; only when an evaluation shows the adapter is underfitting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target modules.&lt;/strong&gt; If you leave &lt;code&gt;target_modules&lt;/code&gt; out, PEFT picks defaults for known architectures (for BERT, the attention query and value projections). Adding the other linear layers gives the adapter more capacity for a little more memory. The &lt;a href="https://huggingface.co/docs/peft/en/package_reference/lora" rel="noopener noreferrer"&gt;PEFT LoRA reference&lt;/a&gt; lists the options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation before and after.&lt;/strong&gt; Hold out a few hundred of your own queries with known answers and measure recall@10 or nDCG@10 on the base model first. Sentence Transformers' &lt;code&gt;InformationRetrievalEvaluator&lt;/code&gt; does this. If the base model already scores well, a better base model, hybrid search or a reranker may help more than fine-tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting.&lt;/strong&gt; Check a general benchmark too. A large drop there means the learning rate was too high or you trained too long.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The step most tutorials skip: re-embedding
&lt;/h2&gt;

&lt;p&gt;Fine-tuning changes the geometry of the vector space. Vectors from the base model and vectors from the fine-tuned model cannot be compared, so they cannot live in the same index. After training you re-embed every document with the new model, and every query has to use the same adapter.&lt;/p&gt;

&lt;p&gt;On a large corpus, that re-embedding pass costs more than the training. Plan it like a migration: build the new index next to the old one, compare results on your evaluation set, then switch traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Mixpeek fits
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mixpeek stores and searches vectors you compute yourself, including ones from a fine-tuned encoder, next to the documents and metadata they came from. During a re-embed you can keep the old and new vectors in separate collections and compare them on the same queries before you switch. Two longer write-ups on our site go deeper: &lt;a href="https://mixpeek.com/guides/fine-tuning-with-lora-adapters" rel="noopener noreferrer"&gt;how LoRA fine-tuning works, including QLoRA and DoRA&lt;/a&gt;, and &lt;a href="https://mixpeek.com/guides/embedding-model-migration-without-reembedding" rel="noopener noreferrer"&gt;changing embedding models without breaking your index&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>nlp</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>The best creative intelligence platforms for video ads in 2026</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Tue, 03 Nov 2026 16:03:33 +0000</pubDate>
      <link>https://dev.to/mixpeek/the-best-creative-intelligence-platforms-for-video-ads-in-2026-122m</link>
      <guid>https://dev.to/mixpeek/the-best-creative-intelligence-platforms-for-video-ads-in-2026-122m</guid>
      <description>&lt;p&gt;The best creative intelligence platform for video ads depends on who you are. Motion is the usual pick for DTC brands that want creative reporting across Meta, TikTok, YouTube and LinkedIn. Vidmob and CreativeX fit enterprise brands that score creative against platform and brand guidelines. Segwise and AppsFlyer Creative Optimization fit mobile app and gaming teams that tie creative elements to installs. Atria, Foreplay and Crux add competitor ad research. If you want to search your own ad archive by what happens on screen, or build creative analysis into your own tools, a developer API such as Mixpeek fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "creative intelligence" covers
&lt;/h2&gt;

&lt;p&gt;The term gets used for four different jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reporting.&lt;/strong&gt; Which ads, hooks and formats drove spend and results, pulled from the ad platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tagging.&lt;/strong&gt; Breaking each video into elements (hook, scenes, people, product shots, on-screen text, voiceover) so results can be compared by element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring.&lt;/strong&gt; Checking creative against platform best practices or brand guidelines, before or after launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research.&lt;/strong&gt; Browsing and saving competitors' ads from public ad libraries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most tools do one or two of these well. Decide which job you have first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortlist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Main job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://motionapp.com" rel="noopener noreferrer"&gt;Motion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;DTC and consumer brands&lt;/td&gt;
&lt;td&gt;Visual creative reporting across Meta, TikTok, YouTube and LinkedIn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://vidmob.com" rel="noopener noreferrer"&gt;Vidmob&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Enterprise brands and agencies&lt;/td&gt;
&lt;td&gt;Creative scoring and measurement across many platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.creativex.com" rel="noopener noreferrer"&gt;CreativeX&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Global brands with many markets&lt;/td&gt;
&lt;td&gt;Creative quality and brand guideline compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://segwise.ai" rel="noopener noreferrer"&gt;Segwise&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Mobile apps and games&lt;/td&gt;
&lt;td&gt;Element tagging tied to ad network and MMP data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.appsflyer.com/products/creative-optimization/" rel="noopener noreferrer"&gt;AppsFlyer Creative Optimization&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Apps already on AppsFlyer&lt;/td&gt;
&lt;td&gt;Creative breakdowns tied to installs and downstream events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tryatria.com" rel="noopener noreferrer"&gt;Atria&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;DTC teams&lt;/td&gt;
&lt;td&gt;Competitor ad research plus analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.foreplay.co" rel="noopener noreferrer"&gt;Foreplay&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creative strategists&lt;/td&gt;
&lt;td&gt;Saving and organizing ads from public libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://getcrux.ai" rel="noopener noreferrer"&gt;Crux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Larger performance teams&lt;/td&gt;
&lt;td&gt;Creative analysis with competitor intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.trypencil.com" rel="noopener noreferrer"&gt;Pencil&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Teams producing many variants&lt;/td&gt;
&lt;td&gt;Generating ads and predicting which will perform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your ads run mostly on Meta and TikTok, and you need weekly answers on what is working:&lt;/strong&gt; start with Motion, or Atria if you also want competitor research in the same place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You run a mobile app or game:&lt;/strong&gt; Segwise, or AppsFlyer's module if you already use AppsFlyer. Element tags tied to installs matter more there than reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are a large brand with guidelines across many markets:&lt;/strong&gt; Vidmob or CreativeX, which score creative before it ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want ideas from what competitors run:&lt;/strong&gt; Foreplay, Atria or Crux.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you sign, check three things: which ad platforms it connects to, whether it analyzes the video itself or only the platform metrics, and whether you can export the tags it produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an off-the-shelf platform is not enough
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The platforms above work on the accounts you connect and the tags they define. Some teams need something else: search across years of ads by what is on screen ("the hook where someone holds the product to camera"), their own tag set, or creative analysis inside their own dashboards.&lt;/p&gt;

&lt;p&gt;That is what Mixpeek's &lt;a href="https://mixpeek.com/templates/creative-dna" rel="noopener noreferrer"&gt;creative DNA template&lt;/a&gt; does. It cuts every ad into scenes at ingest, makes the ad copy searchable beside them, and groups scenes into recurring visual themes per brand. A query returns the scene with its ad, brand and start and end time. Our measured run covered 271 ads, which came out at 2,306 scenes with a median length of 2.5 seconds. You query it through an API, so the results can feed whatever reporting tool you already use.&lt;/p&gt;

&lt;p&gt;For the contextual targeting and brand suitability side of ad tech, &lt;a href="https://mixpeek.com/curated-lists/best-ad-tech-ai-platforms" rel="noopener noreferrer"&gt;our ad tech platform list&lt;/a&gt; compares DoubleVerify, IAS, Zefr, Peer39 and others.&lt;/p&gt;

</description>
      <category>advertising</category>
      <category>marketing</category>
      <category>ai</category>
      <category>video</category>
    </item>
    <item>
      <title>Agentic search over a hierarchy of document clusters</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:07:57 +0000</pubDate>
      <link>https://dev.to/mixpeek/agentic-search-over-a-hierarchy-of-document-clusters-5g6d</link>
      <guid>https://dev.to/mixpeek/agentic-search-over-a-hierarchy-of-document-clusters-5g6d</guid>
      <description>&lt;p&gt;To run agentic search over a hierarchy of document clusters, first cluster the corpus: embed every document, then split the embeddings recursively into a tree, and give each cluster a label and a short summary. At query time an agent navigates the tree top-down. It reads the summaries at one level, chooses which clusters to open, descends into them, and runs vector or keyword search only inside the chosen leaf clusters. It stops when the evidence it has gathered answers the question. The agent never sees the whole corpus, only the summaries on its path and the chunks from a few leaves. The rest of this post covers why that works, how to build the tree, what the loop looks like in code, and where it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why search a tree instead of the whole index
&lt;/h2&gt;

&lt;p&gt;Flat vector search ranks every chunk against the query. On a large, varied corpus the top results often include chunks that share vocabulary with the query and come from the wrong topic. An agent that retrieves those chunks has to spend context reading them.&lt;/p&gt;

&lt;p&gt;A cluster tree changes three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded context.&lt;/strong&gt; A summary of a cluster is short. An agent can read the summaries of ten sibling clusters in a few hundred tokens and decide where to look, so the amount of text in its prompt grows with the depth of the tree. The size of the corpus barely moves it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer irrelevant chunks.&lt;/strong&gt; Once the agent has picked a branch, the final search runs against that branch only. A query about "mountain" never competes with chunks from a cooking archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An explainable path.&lt;/strong&gt; The route from root to leaf (for example &lt;code&gt;Sports &amp;gt; Skateboarding&lt;/code&gt;) is a record of why a document was returned. You can log it, show it to a user, and debug it when the answer is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building the tree
&lt;/h2&gt;

&lt;p&gt;The tree has three ingredients: embeddings, a clustering algorithm applied recursively, and a label plus summary for every node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embed.&lt;/strong&gt; Use one embedding model for the whole corpus so distances mean the same thing at every level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster recursively.&lt;/strong&gt; Run a clustering algorithm on the whole set to get top-level groups, then run it again inside each group that is big enough to split, and keep going until groups are small or you reach a depth limit. K-means works when you know roughly how many groups you want at each level. HDBSCAN works when you do not, and it marks outliers as noise instead of forcing them into a group. In both cases, reducing dimensionality first (UMAP is common) tends to make the splits cleaner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Label and summarize.&lt;/strong&gt; For each cluster, sample member documents and ask an LLM for a short label and a one or two sentence summary. These two fields are what the agent reads while navigating, so their quality sets the ceiling on the whole system. A label such as "Species" on a parent that holds both animals and landforms will send queries about mountains down the wrong branch.&lt;/p&gt;

&lt;p&gt;The best-known paper on this family of ideas is &lt;a href="https://arxiv.org/abs/2401.18059" rel="noopener noreferrer"&gt;RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval&lt;/a&gt; (Sarthi, Abdullah, Tuli, Khanna, Goldie and Manning, January 2024). Its abstract describes recursively embedding, clustering and summarizing chunks of text to build a tree with different levels of summarization, and reports that pairing RAPTOR retrieval with GPT-4 improved the best prior result on the QuALITY benchmark by 20 percentage points of absolute accuracy. RAPTOR retrieves from the tree. The loop below adds an agent that decides which branches to open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent loop
&lt;/h2&gt;

&lt;p&gt;Give the agent three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_children(cluster_id)&lt;/code&gt; returns the child clusters with their labels and summaries. With no argument it returns the top level.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read_summary(cluster_id)&lt;/code&gt; returns the full summary of one cluster.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_within(cluster_id, query)&lt;/code&gt; runs vector or keyword search over documents in that cluster and its descendants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sketch in Python, with &lt;code&gt;llm_choose&lt;/code&gt; and &lt;code&gt;enough_evidence&lt;/code&gt; standing in for whatever model calls you use to pick clusters and judge sufficiency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;agentic_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_choose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;frontier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;            &lt;span class="c1"&gt;# clusters we are about to open
&lt;/span&gt;    &lt;span class="n"&gt;leaves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;frontier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;kids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# nothing below: search here
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;kids&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="c1"&gt;# The model sees label + summary for each candidate and
&lt;/span&gt;        &lt;span class="c1"&gt;# returns the `beam` most promising ones.
&lt;/span&gt;        &lt;span class="n"&gt;frontier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm_choose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;leaves&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;frontier&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;leaf&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search_within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Stop when the evidence suffices, otherwise widen the beam and retry.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;enough_evidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;max_hits&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;max_hits&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;agentic_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_choose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;beam&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details matter in practice.&lt;/p&gt;

&lt;p&gt;First, ask the model to score every child, not to name one. Scores let you keep the top &lt;code&gt;beam&lt;/code&gt; paths and rank finished paths against each other. Averaging scores with a geometric mean keeps a deep leaf from being penalized for having more decisions behind it.&lt;/p&gt;

&lt;p&gt;Second, show a parent cluster to the model as the leaves reachable below it. A parent label is a compromise, and the leaf names tell the model what is actually inside.&lt;/p&gt;

&lt;p&gt;Third, give the agent an explicit stop rule. Without one it keeps descending or searching after the answer is already in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs and failure modes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A wrong branch early.&lt;/strong&gt; The biggest risk is the first decision. If the agent opens the wrong top-level cluster, everything below it is searching the wrong region, and the final answer can be confidently wrong. Two mitigations help. Keep a beam of k branches at each level, so a near-miss on the first choice is still alive at the second. And let the agent backtrack: if &lt;code&gt;search_within&lt;/code&gt; returns weak hits, return to the last level where a sibling scored close and try that one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model calls per level.&lt;/strong&gt; Every level costs a model call. A tree of depth three with a beam of three costs a handful of calls before any retrieval happens. Batch the candidates for all paths at one level into one request, and use a small, fast model for navigation decisions since each one is a short classification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale clusters.&lt;/strong&gt; Clusters describe the corpus as it was when you ran the clustering. New documents either land in the nearest existing cluster or sit outside the tree, and labels drift out of date as content shifts. Re-cluster on a trigger: a schedule, a document count threshold, or a quality signal such as falling cohesion or a rising share of outliers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary quality.&lt;/strong&gt; A vague summary hides a cluster from the agent. Spot-check labels and summaries when you build the tree, and keep a set of queries with known answers to run after every rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Mixpeek documents
&lt;/h2&gt;

&lt;p&gt;Mixpeek's &lt;a href="https://docs.mixpeek.com/retrieval/stages/agent-search" rel="noopener noreferrer"&gt;Agent Search&lt;/a&gt; retriever stage has a &lt;code&gt;cluster_navigation&lt;/code&gt; strategy. Per the docs, a cluster run writes one centroid document per cluster with its &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;parent_cluster_id&lt;/code&gt;. The stage starts at the top-level clusters, asks a decision model which child cluster most likely holds documents that answer the query, and repeats until it reaches leaf clusters. It then returns the member documents of the best leaves. The walk is a beam search: &lt;code&gt;beam_width&lt;/code&gt; defaults to 3, and &lt;code&gt;max_leaf_clusters&lt;/code&gt; sets how many leaves return members. Each returned document carries &lt;code&gt;cluster_path&lt;/code&gt; and &lt;code&gt;cluster_path_score&lt;/code&gt;, and the stage metadata includes a &lt;code&gt;navigation_trace&lt;/code&gt; with the candidates and probabilities at every level. The stage returns the members of the chosen clusters. It does not run a separate vector search inside them, though you can chain a search stage after it. The docs do not describe backtracking.&lt;/p&gt;

&lt;p&gt;On the build side, the &lt;a href="https://docs.mixpeek.com/enrichment/clusters" rel="noopener noreferrer"&gt;Clusters&lt;/a&gt; docs describe vector clustering with eight algorithms including HDBSCAN and K-Means, optional UMAP preprocessing, and hierarchical sub-clustering through &lt;code&gt;hierarchical: true&lt;/code&gt; and &lt;code&gt;max_hierarchy_depth&lt;/code&gt;. LLM labeling is configurable per cluster. Each execution reports quality metrics, including a &lt;code&gt;should_recluster&lt;/code&gt; recommendation, and clusters can be re-run automatically with cron, interval or event-based triggers. The &lt;a href="https://docs.mixpeek.com/tutorials/hierarchical-cluster-search" rel="noopener noreferrer"&gt;hierarchical search tutorial&lt;/a&gt; walks through the whole sequence from clustering to a retriever that navigates the tree.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt; Mixpeek builds clusters over collections, including hierarchical vector clusters with LLM-written labels (&lt;a href="https://docs.mixpeek.com/enrichment/clusters" rel="noopener noreferrer"&gt;Clusters docs&lt;/a&gt;), and its Agent Search stage can navigate those clusters top-down with the &lt;code&gt;cluster_navigation&lt;/code&gt; strategy (&lt;a href="https://docs.mixpeek.com/retrieval/stages/agent-search" rel="noopener noreferrer"&gt;Agent Search docs&lt;/a&gt;). The tutorial linked above shows the end-to-end setup.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>python</category>
      <category>search</category>
    </item>
    <item>
      <title>Is There a Reverse Video Search? How It Works, How to Build One</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:07:57 +0000</pubDate>
      <link>https://dev.to/mixpeek/is-there-a-reverse-video-search-how-it-works-how-to-build-one-4g0e</link>
      <guid>https://dev.to/mixpeek/is-there-a-reverse-video-search-how-it-works-how-to-build-one-4g0e</guid>
      <description>&lt;p&gt;Yes. Reverse video search exists, but the big search engines do not offer it as a single upload box. The usual workflow is to pull keyframes out of the video and run each one through a reverse image search. Under the hood, a dedicated system does the same thing at scale: it samples frames or detects shot boundaries, turns each frame into a perceptual hash or an embedding, stores them in an index, and matches the frames of your query clip against that index. The best-matching frames also carry timestamps, so the answer comes back as "this file, at 01:42". Consumer tools search the public web's image index. If you want to search a video library you own, you build or buy the index yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumer options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google Lens.&lt;/strong&gt; Google's &lt;a href="https://support.google.com/websearch/answer/1325808" rel="noopener noreferrer"&gt;help page&lt;/a&gt; says you can search with an image by uploading a file, dragging it in, or pasting an image URL. Take a frame from the video, save it as an image, and upload it. The page does not mention video input, so the frame step is yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bing Visual Search.&lt;/strong&gt; Microsoft's &lt;a href="https://support.microsoft.com/en-US/bing/using-bing-visual-search" rel="noopener noreferrer"&gt;support page&lt;/a&gt; describes searching the web with an image from the camera icon on Bing Images. It accepts uploads, pasted images and URLs. Same workflow: export a frame, upload it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TinEye.&lt;/strong&gt; TinEye &lt;a href="https://help.tineye.com/article/233-how-does-tineye-work" rel="noopener noreferrer"&gt;fingerprints the image you submit and compares it with its index&lt;/a&gt;, which it builds by crawling the web. Its help center says it finds &lt;a href="https://help.tineye.com/article/235-can-tineye-find-similar-images-does-tineye-do-facial-recognition" rel="noopener noreferrer"&gt;exact and altered copies, including cropped, color adjusted, resized, heavily edited or slightly rotated ones&lt;/a&gt;, and that it does not commonly return similar images of the same subject. Its help pages describe image search only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;InVID-WeVerify.&lt;/strong&gt; This browser plugin was built for journalists and fact-checkers. Its &lt;a href="https://www.invid-project.eu/tools-and-services/invid-verification-plugin/" rel="noopener noreferrer"&gt;keyframe tab&lt;/a&gt; takes a video URL or an uploaded file, segments it into keyframes, and lets you right-click a keyframe to search it on Google, Yandex, TinEye and Baidu. It automates the manual workflow above, so it is the quickest way to try it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;YouTube Content ID.&lt;/strong&gt; Content ID does match video, but only for rights holders. &lt;a href="https://support.google.com/youtube/answer/2797370" rel="noopener noreferrer"&gt;Google's help page&lt;/a&gt; says owners must hold exclusive rights to a substantial body of original material that is frequently uploaded to YouTube. They supply reference files, and every new upload is scanned against that library. A regular user cannot submit a clip and ask where it appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these tools miss
&lt;/h2&gt;

&lt;p&gt;All four web options search an index of images. None of them has your video in it. That has practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clip that circulated only as video, never as a thumbnail, screenshot or article image, may have no still in the index to match.&lt;/li&gt;
&lt;li&gt;You choose which frames to search. A frame from a title card or a black transition returns nothing useful, and a single frame says nothing about when in the clip it occurs.&lt;/li&gt;
&lt;li&gt;Copies that were cropped, mirrored, letterboxed, overlaid with captions or re-encoded at low quality produce frames that differ from the originals. TinEye's documentation covers crops and edits, but horizontal flips and heavy overlays are where fingerprint matching usually gives out, so test your own samples.&lt;/li&gt;
&lt;li&gt;Your private footage is not on the web, so no public tool can find it. Searching a library you own requires indexing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build one over your own library
&lt;/h2&gt;

&lt;p&gt;The pipeline has four steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Sample frames.&lt;/strong&gt; The simplest approach is a fixed rate such as one frame per second. A more economical one is shot-based: ffmpeg's &lt;code&gt;select&lt;/code&gt; filter exposes a &lt;code&gt;scene&lt;/code&gt; score between 0 and 1, and the &lt;a href="https://ffmpeg.org/ffmpeg-filters.html#select_002c-aselect" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; says a threshold between 0.3 and 0.5 is generally a sane choice. Extract one representative frame per shot and store its timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Choose a fingerprint.&lt;/strong&gt; Perceptual hashes (average, perceptual, difference, wavelet) from the &lt;a href="https://github.com/JohannesBuchner/imagehash" rel="noopener noreferrer"&gt;imagehash&lt;/a&gt; library produce a short code, and the distance between two codes is a Hamming distance. They are cheap and handle re-encoding, resizing and mild color shifts. They are weak against heavy crops and flips. Embeddings from a vision model such as CLIP are larger and need a vector index, but they tolerate more edits and also support searching by meaning. Many systems run a hash for near-exact copies and embeddings for everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Index.&lt;/strong&gt; Store one row per sampled frame: video ID, timestamp, hash or vector. For hashes, a linear scan over a few million rows is fine. Past that, use a vector database or a BK-tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Align in time.&lt;/strong&gt; Matching single frames gives noisy results, because unrelated frames sometimes collide. Real matches agree on an offset. If query frame at 3 s matches library frame at 63 s, and query frame at 4 s matches library frame at 64 s, the offset is 60 s in both cases. Count votes per (video, offset) and return the winner. That also gives you the start timestamp of the clip inside the source file.&lt;/p&gt;

&lt;p&gt;Here is a small version using hashes and offset voting. It needs &lt;code&gt;ffmpeg&lt;/code&gt; on your PATH, plus &lt;code&gt;pip install imagehash pillow&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;imagehash&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return [(timestamp_seconds, phash)] sampled at `fps`.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-loglevel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-vf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fps=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/f_%06d.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/f_*.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imagehash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="c1"&gt;# Build the index: one row per sampled frame.
&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# (video_id, timestamp, hash)
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;vid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_dist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;votes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hq&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hq&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_dist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# Hamming distance
&lt;/span&gt;                &lt;span class="n"&gt;votes&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tq&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;votes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;votes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_clip.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. ('a.mp4', 60, 14)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third number is the vote count, which works as a confidence signal. The distance threshold of 10 on a 64-bit hash is a starting point, so tune it on clips you know are matches and clips you know are not. The first frame of the extracted images is labeled 0 seconds, which is approximate to within one sampling interval.&lt;/p&gt;

&lt;p&gt;For production use, replace the linear scan with an index, add shot detection so static scenes do not produce hundreds of near-identical rows, and run a second embedding pass for mirrored or cropped copies.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt; Mixpeek indexes video in your own S3 or GCS bucket, splits it into scenes, and embeds frames. A retriever then returns the matching file and timestamp for an uploaded clip or image. We compare ten approaches in &lt;a href="https://mixpeek.com/curated-lists/best-reverse-video-search-tools" rel="noopener noreferrer"&gt;Best Reverse Video Search Tools&lt;/a&gt;, and the &lt;a href="https://mixpeek.com/tutorials/reverse-search" rel="noopener noreferrer"&gt;reverse search tutorial&lt;/a&gt; walks through the setup.&lt;/p&gt;

</description>
      <category>video</category>
      <category>computervision</category>
      <category>python</category>
      <category>search</category>
    </item>
    <item>
      <title>IAB Content Taxonomy 3.1 and GARM in 2026: three facts AI answers get wrong</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Fri, 23 Oct 2026 21:56:46 +0000</pubDate>
      <link>https://dev.to/mixpeek/iab-content-taxonomy-31-and-garm-in-2026-three-facts-ai-answers-get-wrong-478o</link>
      <guid>https://dev.to/mixpeek/iab-content-taxonomy-31-and-garm-in-2026-three-facts-ai-answers-get-wrong-478o</guid>
      <description>&lt;p&gt;The current IAB Content Taxonomy is version 3.1, released in December 2024, with exactly 704 categories. GARM, the advertiser coalition behind the best-known brand safety categories, shut down in August 2024. AI assistants still answer contextual advertising questions with "IAB 3.0", "700+ categories" and GARM as a live standards body. Here is what the source files say, with links, so you can check before you ship a mapping or a deal spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The current version is 3.1, and the bid request should say so
&lt;/h2&gt;

&lt;p&gt;IAB Tech Lab's &lt;a href="https://iabtechlab.com/standards/content-taxonomy/" rel="noopener noreferrer"&gt;content taxonomy page&lt;/a&gt; lists 3.1 as current and describes the change from 3.0 as a genre update that adds Thriller. In the &lt;a href="https://github.com/InteractiveAdvertisingBureau/Taxonomies/blob/main/Content%20Taxonomies/Content%20Taxonomy%203.1.tsv" rel="noopener noreferrer"&gt;3.1 file&lt;/a&gt; Thriller is ID 700 under Genres. Every 3.0 ID is also a 3.1 ID, so moving up needs only a new version label.&lt;/p&gt;

&lt;p&gt;The part that breaks things is &lt;code&gt;cattax&lt;/code&gt;. OpenRTB sends category IDs in &lt;code&gt;cat&lt;/code&gt; and says which taxonomy they come from in &lt;code&gt;cattax&lt;/code&gt;, using AdCOM's &lt;a href="https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list_categorytaxonomies" rel="noopener noreferrer"&gt;List: Category Taxonomies&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 is Content Taxonomy 2.0&lt;/li&gt;
&lt;li&gt;7 is Content Taxonomy 3.0&lt;/li&gt;
&lt;li&gt;9 is Content Taxonomy 3.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you label content with 3.1 IDs and send &lt;code&gt;cattax: 7&lt;/code&gt;, a strict receiver reads them as 3.0, which holds for every ID except the ones 3.1 added, such as Thriller. Send &lt;code&gt;cattax: 2&lt;/code&gt; and the same numbers mean different 2.0 categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. It is 704 categories, in four tiers
&lt;/h2&gt;

&lt;p&gt;"700+" is close, and it hides the shape. Counting the 3.1 file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Categories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tier 1&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 2&lt;/td&gt;
&lt;td&gt;323&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 3&lt;/td&gt;
&lt;td&gt;275&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 4&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;704&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tier matters more than the total. Some classification services return only the top two tiers: the IAB list in Amazon Bedrock Data Automation's &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/bda-ouput-video.html" rel="noopener noreferrer"&gt;video output docs&lt;/a&gt; has 24 tier 1 and 85 tier 2 categories. If a buyer, an SSP or a curated deal asks for tier 3 codes, check the depth your classifier actually returns before you promise it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. GARM shut down in 2024, and its categories live on in vendor products
&lt;/h2&gt;

&lt;p&gt;The World Federation of Advertisers &lt;a href="https://wfanet.org/leadership/garm/about-garm" rel="noopener noreferrer"&gt;discontinued GARM on 9 August 2024&lt;/a&gt;, days after X filed an antitrust suit against the WFA, GARM and its members. The WFA named no successor.&lt;/p&gt;

&lt;p&gt;What survived is the Brand Safety Floor and Suitability Framework: eleven content categories, such as arms and ammunition, hate speech and terrorism, each graded by risk from floor to low. Platforms and verification vendors still use that vocabulary in their own products, and each now maintains its own version. IAB Tech Lab's &lt;a href="https://github.com/IABTechLab/Brand-Suitability-Test-Benchmarks" rel="noopener noreferrer"&gt;human-labeled GARM benchmark&lt;/a&gt; was last updated in 2022, before the shutdown.&lt;/p&gt;

&lt;p&gt;So "GARM-aligned" today means "uses categories that GARM defined". Nobody certifies it. Ask a vendor which version of the categories its labels follow and when it last changed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checks before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your labels and your &lt;code&gt;cattax&lt;/code&gt; value name the same version.&lt;/li&gt;
&lt;li&gt;You know which tiers your classifier returns, and they match what your partners ask for.&lt;/li&gt;
&lt;li&gt;Brand suitability tiers are written down as your own policy, with the vendor's category definitions attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt; We classify video, images, audio and pages into all 704 Content Taxonomy 3.1 categories, scene by scene for long video, and return IDs ready for &lt;code&gt;cat&lt;/code&gt; with &lt;code&gt;cattax: 9&lt;/code&gt; (&lt;a href="https://mixpeek.com/use-cases/iab-taxonomy-classification" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;). Our &lt;a href="https://mixpeek.com/glossary/garm" rel="noopener noreferrer"&gt;GARM explainer&lt;/a&gt; has the longer history, and the open-source &lt;a href="https://mixpeek.com/connectors/iab-mapper" rel="noopener noreferrer"&gt;IAB mapper&lt;/a&gt; converts 2.x labels to 3.1 IDs.&lt;/p&gt;

</description>
      <category>advertising</category>
      <category>adtech</category>
      <category>machinelearning</category>
      <category>video</category>
    </item>
    <item>
      <title>The best MCP servers for advertising agents in 2026</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:21:27 +0000</pubDate>
      <link>https://dev.to/mixpeek/the-best-mcp-servers-for-advertising-agents-in-2026-g5f</link>
      <guid>https://dev.to/mixpeek/the-best-mcp-servers-for-advertising-agents-in-2026-g5f</guid>
      <description>&lt;p&gt;The MCP servers an advertising agent needs fall into four groups. The ad platforms run their own servers for buying and reporting: Meta's Ads MCP at mcp.facebook.com/ads, Amazon Ads' MCP server (open beta, for partners with API access) and Google's open-source Google Ads MCP, which is read-only. Cross-channel data servers such as Improvado's put metrics from many platforms behind one interface. AdCP, the Ad Context Protocol, is the open standard for agents that buy media, activate signals and traffic creative between companies. Content-understanding servers tell an agent what is inside a video or page before it bids.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The platforms' own servers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Meta Ads MCP.&lt;/strong&gt; A remote server hosted by Meta, &lt;a href="https://developers.facebook.com/documentation/ads-commerce/ads-ai-connectors/ads-mcp-server/ads-mcp-server-overview" rel="noopener noreferrer"&gt;documented under Ads AI Connectors&lt;/a&gt;. Its tools cover reporting, campaign and ad creation and editing, catalogs, datasets, A/B tests and lift studies. Apps need the &lt;code&gt;ads_mcp_management&lt;/code&gt; permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Ads MCP server.&lt;/strong&gt; &lt;a href="https://advertising.amazon.com/library/news/amazon-ads-mcp-server-open-beta" rel="noopener noreferrer"&gt;Open beta&lt;/a&gt; for Amazon Ads partners with active API credentials. Its prebuilt tools run multi-step jobs such as creating a campaign with its ad groups and ads from one prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Ads MCP.&lt;/strong&gt; An open-source server from the Google Ads API team, &lt;a href="https://github.com/googleads/google-ads-mcp" rel="noopener noreferrer"&gt;googleads/google-ads-mcp&lt;/a&gt;. Its tools search the account, describe API resources and list accessible customers. It reads data and changes nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use these when the agent acts inside one platform. Each needs that platform's credentials, and each returns that platform's field names.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cross-channel marketing data
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://improvado.io/docs-section-topic/improvado-mcp" rel="noopener noreferrer"&gt;Improvado's MCP server&lt;/a&gt; is a good example. It sits on Improvado's connectors and maps fields such as Google's cost and Meta's spend to one metric, so an agent can compare channels in a single query. Servers like this fit reporting and budget questions that span platforms. They need the vendor's platform underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Agent-to-agent buying: AdCP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.adcontextprotocol.org/" rel="noopener noreferrer"&gt;AdCP&lt;/a&gt; defines tasks that buyer and seller agents call over MCP or A2A. Examples are &lt;code&gt;get_products&lt;/code&gt; and &lt;code&gt;create_media_buy&lt;/code&gt; for media, &lt;code&gt;get_signals&lt;/code&gt; and &lt;code&gt;activate_signal&lt;/code&gt; for audience data, and &lt;code&gt;build_creative&lt;/code&gt; and &lt;code&gt;sync_creatives&lt;/code&gt; for creative. AgenticAdvertising.org stewards it, and the current release is 3.2.1. If your agent buys from publishers or sells inventory to other agents, implement AdCP so you do not build a custom integration for each counterparty. Some IAB Tech Lab work in this area overlaps; &lt;a href="https://mixpeek.com/guides/adcp-vs-aamp-agentic-advertising-standards" rel="noopener noreferrer"&gt;this comparison of AdCP and AAMP&lt;/a&gt; covers how they differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Content understanding
&lt;/h2&gt;

&lt;p&gt;The servers above say what an ad account holds and what inventory costs. None of them says what is inside a piece of video, so they cannot answer whether a CTV episode is safe for a brand or which IAB category a scene belongs to. For that the agent needs a server that has indexed the media.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt; Mixpeek's MCP server (&lt;code&gt;https://mcp.mixpeek.com/app/mcp&lt;/code&gt;, OAuth, read-only) lets an agent search video, images, audio and documents you have indexed from your own bucket, and run retrievers you define, for example one that classifies each scene into IAB Content Taxonomy 3.1. &lt;a href="https://docs.mixpeek.com/integrations/developer-tools/mcp-server" rel="noopener noreferrer"&gt;Setup docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reasonable starting stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One platform server for each platform where you buy.&lt;/li&gt;
&lt;li&gt;A cross-channel data server if your reporting spans platforms.&lt;/li&gt;
&lt;li&gt;AdCP if you buy from, or sell to, other agents.&lt;/li&gt;
&lt;li&gt;A content server if any decision depends on what the creative or the inventory shows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give the agent read-only tools first, and add write access once its reads have proven correct.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>advertising</category>
      <category>agents</category>
    </item>
    <item>
      <title>Twelve Labs vs AWS for contextual advertising</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:15:42 +0000</pubDate>
      <link>https://dev.to/mixpeek/twelve-labs-vs-aws-for-contextual-advertising-4kd</link>
      <guid>https://dev.to/mixpeek/twelve-labs-vs-aws-for-contextual-advertising-4kd</guid>
      <description>&lt;p&gt;For contextual advertising, AWS gives you finished labels and Twelve Labs gives you stronger video models to build on. Amazon Bedrock Data Automation (BDA) splits a video into chapters and tags each one with IAB categories, along with on-screen text, logos and seven moderation classes, so you get targeting labels with no model work. Twelve Labs sells two models: Marengo, which turns video into embeddings, and Pegasus, which turns video into text. Turning their output into IAB codes, brand-safety flags or ad breaks is a pipeline you write and run. Both Twelve Labs models are also sold inside Bedrock, so one AWS account can use both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AWS gives you
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/bda-ouput-video.html" rel="noopener noreferrer"&gt;BDA's standard video output&lt;/a&gt; has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shots and chapters, with timestamps and a summary for each chapter&lt;/li&gt;
&lt;li&gt;IAB categories per chapter&lt;/li&gt;
&lt;li&gt;a full transcript with speaker labels&lt;/li&gt;
&lt;li&gt;text that appears on screen, with bounding boxes&lt;/li&gt;
&lt;li&gt;logo detection (off by default)&lt;/li&gt;
&lt;li&gt;seven moderation categories, such as violence, alcohol and hate symbols&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The IAB list in the BDA docs has 24 tier 1 and 85 tier 2 categories. IAB Content Taxonomy 3.1, the current version, has 704 categories across four tiers, 37 of them at tier 1. If a buyer, SSP or curation deal asks for tier 3 codes or a specific taxonomy version, plan for a mapping step.&lt;/p&gt;

&lt;p&gt;AWS also publishes &lt;a href="https://aws.amazon.com/solutions/guidance/contextual-intelligence-advertising-using-generative-ai-on-aws/" rel="noopener noreferrer"&gt;guidance for contextual advertising with generative AI&lt;/a&gt;, which builds the same kind of labels from multimodal models you choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Twelve Labs gives you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marengo&lt;/strong&gt; turns video into embeddings, used for search and classification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pegasus&lt;/strong&gt; turns video into text, for summaries and structured descriptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marengo 2.7 and Pegasus 1.2 &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/07/twelvelabs-models-fully-managed-amazon-bedrock/" rel="noopener noreferrer"&gt;became fully managed models in Bedrock in July 2025&lt;/a&gt;. Twelve Labs' own API carries newer versions.&lt;/p&gt;

&lt;p&gt;Their &lt;a href="https://twelvelabs.io/blog/contextual-ad-insertion-engine" rel="noopener noreferrer"&gt;contextual ad engine tutorial&lt;/a&gt; shows what building on these models looks like. Pegasus 1.5 writes structured metadata for each scene. Marengo 3.0 embeddings feed search. The candidate labels are matched against IAB 3.1 rows, and the pipeline picks ad breaks and writes FreeWheel-style key-values. It ships as a reference build with a repo, so the pipeline is yours to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are on AWS and want labels this quarter:&lt;/strong&gt; start with BDA. Check its category depth against what your partners expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to search an archive by what happens on screen&lt;/strong&gt;, such as every scene at a beach at sunset, or define your own segments: embeddings fit better. That means Marengo, or any multimodal embedding you can run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need audited IAB 3.1 codes at full depth:&lt;/strong&gt; either route needs a classification step you can measure. Label a few hundred of your own scenes by hand and compare top-1 accuracy before you commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A third option
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mixpeek indexes video from your own S3 or GCS bucket, scene by scene. It classifies each scene into all 704 IAB Content Taxonomy 3.1 categories through a retriever you can inspect and change. On IAB Tech Lab's open 704-page benchmark, our 3.1 classifier scored 72.7% top-1 and 93.8% top-5. That benchmark is text only, and &lt;a href="https://mixpeek.com/guides/iab-content-taxonomy-classification-benchmark" rel="noopener noreferrer"&gt;the write-up&lt;/a&gt; includes where it lost. A side-by-side with Twelve Labs is at &lt;a href="https://mixpeek.com/comparisons/mixpeek-vs-twelvelabs" rel="noopener noreferrer"&gt;mixpeek.com/comparisons/mixpeek-vs-twelvelabs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>advertising</category>
      <category>video</category>
      <category>aws</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Why your video search keeps returning the wrong scene</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:35:02 +0000</pubDate>
      <link>https://dev.to/mixpeek/why-your-video-search-keeps-returning-the-wrong-scene-4c5l</link>
      <guid>https://dev.to/mixpeek/why-your-video-search-keeps-returning-the-wrong-scene-4c5l</guid>
      <description>&lt;p&gt;Your search usually returns the wrong scene because of how the video was cut up before it was indexed. If you embed fixed time windows or every Nth frame, one vector can straddle two scenes, and the match lands on the neighbour. If you embed one vector per whole video, every query returns the same file. The fix is to segment first: detect shot boundaries, group shots into scenes, then embed each scene and store its start and end timestamps. A query then matches a coherent unit and returns the moment itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the wrong scene comes from
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fixed windows.&lt;/strong&gt; Splitting every 10 seconds ignores where the cuts are. A window that holds the last three seconds of a kitchen scene and the first seven of a street scene produces an embedding that is half of each. Ask for "street at night" and you may get the kitchen timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frame sampling without boundaries.&lt;/strong&gt; Sampling one frame a second gives you lots of vectors, many nearly identical. The nearest neighbour is often a frame from a similar-looking shot elsewhere in the video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One vector per video.&lt;/strong&gt; Averaging a whole video into one embedding throws away where anything happens. Search can find the right file and still cannot tell you where in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How segmentation fixes it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shot boundary detection.&lt;/strong&gt; Compare consecutive frames. A hard cut spikes a histogram difference. Dissolves and fades spread the change over many frames, so an adaptive detector flags a boundary when the score beats a rolling local mean by two to three standard deviations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group shots into scenes.&lt;/strong&gt; A shot is one camera take. A scene can span many shots, like a conversation cutting between two angles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up.&lt;/strong&gt; Merge segments under about two seconds into a neighbour, and split long ones at audio silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed per scene, keep timestamps.&lt;/strong&gt; One embedding per scene, stored with its start and end time. Results come back as moments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This also cuts index size by 10 to 100 times, because you store one vector per scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checks on your own pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Print the boundaries of the scene your query wrongly returned. If a cut falls inside it, segmentation is the problem.&lt;/li&gt;
&lt;li&gt;Check whether dissolves are being missed. A fixed threshold catches hard cuts and misses gradual transitions.&lt;/li&gt;
&lt;li&gt;Check whether on-screen text or speech carries the meaning. If so, a visual embedding alone will keep missing, and you need OCR or transcript features on the same scene.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full walkthrough, with the algorithms and thresholds: &lt;a href="https://mixpeek.com/guides/video-scene-segmentation" rel="noopener noreferrer"&gt;https://mixpeek.com/guides/video-scene-segmentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>video</category>
      <category>search</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Vector search on S3, without a hosted vector database</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:35:01 +0000</pubDate>
      <link>https://dev.to/mixpeek/vector-search-on-s3-without-a-hosted-vector-database-d6d</link>
      <guid>https://dev.to/mixpeek/vector-search-on-s3-without-a-hosted-vector-database-d6d</guid>
      <description>&lt;p&gt;You can keep your vectors in S3 and search them there. Three ways work today. AWS S3 Vectors stores embeddings in a special bucket type and answers similarity queries through an API. LanceDB writes a columnar vector format to any S3 path and queries it from your process. A vector store built on object storage, such as Mixpeek's MVS, keeps the index and payloads in your bucket and runs dense, sparse and BM25 search over them. Pick by what you need beyond nearest neighbor: filters, hybrid keyword search, and how fast a cold query has to come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why people move vectors to object storage
&lt;/h2&gt;

&lt;p&gt;A hosted vector database keeps your whole index in memory on machines you rent all month. That is fine at a few million vectors. It gets expensive when most of your namespaces sit idle, which is the normal shape for multi-tenant apps and media archives.&lt;/p&gt;

&lt;p&gt;Object storage flips the cost model. The data sits at storage prices, around $0.023 per GB-month on S3 Standard, and compute only runs when a query arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;S3 Vectors.&lt;/strong&gt; Native to AWS. You create a vector bucket and an index, put vectors with metadata, and query by similarity. Good when you are all-in on AWS and need plain nearest-neighbor search with metadata filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LanceDB.&lt;/strong&gt; An embedded library over the Lance file format. Point it at &lt;code&gt;s3://bucket/path&lt;/code&gt; and query from your own code. Good when you want no server at all and your queries run inside one application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A vector store on object storage.&lt;/strong&gt; The index lives in your bucket, and a query layer loads what it needs, caches the hot parts, and scales idle namespaces to zero. MVS works this way: you bring your own vectors, run dense, sparse and BM25 hybrid search, and the vectors persist on your own S3-compatible storage. It also imports from Pinecone, Qdrant or Weaviate without re-embedding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before you switch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold latency.&lt;/strong&gt; The first query against an idle namespace has to fetch from storage. Measure it on your own data size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid search.&lt;/strong&gt; If your users type exact product codes or names, pure vector search misses them. You want BM25 alongside dense vectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters at query time.&lt;/strong&gt; Metadata filters should apply inside the search. A store that filters the top results afterwards hands back fewer than you asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write path.&lt;/strong&gt; Check how writes become durable and searchable, and how long that takes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full details on how MVS does this, with a cost estimator: &lt;a href="https://mixpeek.com/mvs" rel="noopener noreferrer"&gt;https://mixpeek.com/mvs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on Mixpeek.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vectordatabase</category>
      <category>aws</category>
      <category>s3</category>
      <category>search</category>
    </item>
    <item>
      <title>How to Add Adjacency-Aware Contextual Targeting to Prebid in 15 Minutes</title>
      <dc:creator>Ethan Steininger</dc:creator>
      <pubDate>Mon, 12 Jan 2026 04:11:26 +0000</pubDate>
      <link>https://dev.to/mixpeek/how-to-add-adjacency-aware-contextual-targeting-to-prebid-in-15-minutes-3342</link>
      <guid>https://dev.to/mixpeek/how-to-add-adjacency-aware-contextual-targeting-to-prebid-in-15-minutes-3342</guid>
      <description>&lt;p&gt;&lt;em&gt;Replace cookie-based targeting with privacy-first contextual signals — without blocking your auctions.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Cookie deprecation is here. Third-party targeting signals are disappearing, and publishers need alternatives that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don't rely on user identifiers&lt;/li&gt;
&lt;li&gt;Don't add latency to header bidding&lt;/li&gt;
&lt;li&gt;Work with existing SSP/DSP infrastructure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Mixpeek RTD module solves this by enriching Prebid bid requests with &lt;strong&gt;contextual + adjacency signals&lt;/strong&gt; via standard OpenRTB fields.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'll Get
&lt;/h2&gt;

&lt;p&gt;After this 15-minute setup, your bid requests will include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal Type&lt;/th&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Contextual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IAB v3.0 categories, keywords, sentiment, brand safety score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Adjacency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Previous creative ID, bidder, ad categories&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SSPs and DSPs consume these via &lt;code&gt;ortb2.site.content&lt;/code&gt; and &lt;code&gt;ortb2Imp.ext.data&lt;/code&gt; — no custom integration required on their end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Install the Module
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @mixpeek/prebid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add via CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@mixpeek/prebid@latest/dist/mixpeek-rtd.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Get Your Credentials
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign up at &lt;a href="https://mixpeek.com/start" rel="noopener noreferrer"&gt;mixpeek.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Generate an API key in your dashboard&lt;/li&gt;
&lt;li&gt;Create a collection with feature extractors&lt;/li&gt;
&lt;li&gt;Note your namespace ID (format: &lt;code&gt;ns_xxxxx&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 3: Configure Prebid
&lt;/h2&gt;

&lt;p&gt;Add the RTD provider to your Prebid config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mixpeek/prebid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;pbjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;  &lt;span class="na"&gt;realTimeData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;auctionDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Max wait time (ms)&lt;/span&gt;
    &lt;span class="na"&gt;dataProviders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mixpeek&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;waitForIt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_COLLECTION_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_NAMESPACE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Bid requests are now enriched automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Verify the Output
&lt;/h2&gt;

&lt;p&gt;Check that contextual data is being injected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pbjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mixpeekContextReady&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Category:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taxonomy&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Keywords:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Brand Safety:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brandSafety&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;ortb2&lt;/code&gt; object will now include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"site"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IAB19-11"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cattax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"genre"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Technology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai,technology,ml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"mixpeek"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"brandSafety"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"sentiment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"positive"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How DSPs Consume the Signals
&lt;/h2&gt;

&lt;p&gt;DSPs can use the enriched data for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contextual Targeting&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Target technology content&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ortb2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IAB19-11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Bid on tech inventory&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Competitive Separation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Avoid back-to-back competing ads&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prevCat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ortb2Imp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hb_mixpeek_prev_cat&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCat&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IAB18-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentAd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IAB18-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Skip this impression&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Brand Safety&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only bid on safe inventory&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ortb2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mixpeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brandSafety&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Safe to bid&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Shadow Mode: Risk-Free Testing
&lt;/h2&gt;

&lt;p&gt;Want to test without affecting live auctions? Use shadow mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pbjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;realTimeData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;auctionDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// No delay&lt;/span&gt;
    &lt;span class="na"&gt;dataProviders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mixpeek&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;waitForIt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Don't wait for response&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_COLLECTION_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_NAMESPACE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This logs contextual data without blocking auctions. Perfect for validating the integration before going &lt;/p&gt;

</description>
      <category>prebid</category>
      <category>adtech</category>
      <category>javascript</category>
      <category>contextual</category>
    </item>
  </channel>
</rss>
