---
title: "AdRadar: Creative Fatigue and Copycat Search Without a Tagging Farm"
date: "2026-09-17"
dateModified: "2026-09-17"
author: "Pierre Brunelle"
tags:
  - Advertising
  - CLIP
  - Visual Search
  - TableModel
  - Multimodal AI
  - Pixeltable Cloud
  - Creative
description: "Ingest ad images (and optional video), read the copy, transcribe the voiceover, score fatigue from spend and days. CLIP finds lookalikes. Not a trained fatigue model."
url: "https://www.pixeltable.com/blog/adradar-creative-fatigue-visual-search"
---

# AdRadar: Creative Fatigue and Copycat Search Without a Tagging Farm

**Summary:** AdRadar is the media-buying loop: a creative image (and optional video) becomes readable copy, a voiceover transcript, a fatigue flag from spend and age, and a CLIP index for lookalikes. You do not run OCR, Whisper, a scoring spreadsheet, and Pinecone as four products. You declare `Ads`. `pip install pixeltable`.

 
## The Product

 

 - Ingest `creative_image` with optional `creative_video`

 - Store spend, impressions, conversions, `active_days`

 - Read on-image copy with Gemini — not a fake OCR product

 - Flag creatives that have been live too long with a weak conversion rate

 - Drop a competitor screenshot; return nearest ads

 

 
SKU shop-the-look is [SnapCatalog](/blog/snapcatalog-product-visual-search). This post is the same CLIP-on-image index with campaign telemetry on the row.

 
## The Stitch You Delete

 
A folder of exports from the ad account. A VLM batch for copy. Whisper for video ads. A sheet that divides conversions by impressions. Embeddings in a sidecar keyed by `ad_id`. Pixeltable is the creative row those joints were reconciling in ops.

 
## The Receipt

 
```python
import pixeltable as pxt
from pixeltable.catalog.model import Column, EmbeddingIndex
from pixeltable.functions import gemini, whisper
from pixeltable.functions.huggingface import clip
from pixeltable.functions.video import extract_audio
from pixeltable.serving import FastAPIRouter
from pixeltable.type_system import VideoType

TableModel = pxt.model_base()
clip_embed = clip.using(model_id='openai/clip-vit-base-patch32')

COPY_PROMPT = 'Read all visible ad copy. Return plain text only. No OCR vendor name.'

@pxt.udf
def fatigue_risk(active_days: int, conversions: int, impressions: int) -> str:
 if impressions = 14 and cvr < 0.01:
 return 'HIGH'
 if active_days >= 7 and cvr < 0.02:
 return 'WATCH'
 return 'OK'

class Ads(TableModel, name='ads'):
 ad_id = Column(pxt.String, primary_key=True)
 brand: pxt.String
 campaign: pxt.String
 creative_image: pxt.Image
 creative_video = Column(VideoType(nullable=True))
 spend: pxt.Float
 impressions: pxt.Int
 conversions: pxt.Int
 active_days: pxt.Int

 ocr_text = gemini.generate_content(
 [creative_image, COPY_PROMPT],
 model='gemini-2.5-flash',
 )
 audio = extract_audio(creative_video)
 voiceover = whisper.transcribe(audio, model='base')
 risk = fatigue_risk(active_days, conversions, impressions)

 __indexes__ = [
 EmbeddingIndex(column=creative_image, embedding=clip_embed, name='ar_visual_clip_idx'),
 ]

@pxt.query
def fatigued_ads():
 return (
 Ads.where(Ads.risk == 'HIGH')
 .select(Ads.ad_id, Ads.brand, Ads.campaign, Ads.spend, Ads.risk, Ads.ocr_text)
 )

@pxt.query
def find_copycats(ref_image: pxt.Image, top_k: int = 8):
 sim = Ads.creative_image.similarity(ref_image)
 return (
 Ads.order_by(sim, asc=False)
 .limit(top_k)
 .select(Ads.ad_id, Ads.brand, Ads.ocr_text, Ads.creative_image)
 )

api = FastAPIRouter(name='adradar')
api.add_insert_route(
 Ads,
 path='/ads',
 inputs=[
 Ads.creative_image,
 Ads.creative_video,
 Ads.ad_id,
 Ads.brand,
 Ads.campaign,
 Ads.spend,
 Ads.impressions,
 Ads.conversions,
 Ads.active_days,
 ],
)
api.add_query_route(path='/fatigued', query=fatigued_ads)
api.add_query_route(path='/copycats', query=find_copycats)
```

 
`fatigue_risk` is arithmetic on columns you already store — days live and conversion rate — not a trained fatigue model we pretend to ship. Gemini reads the pixels for copy. CLIP on the image is the copycat path, same `embedding=` form as SnapCatalog. Same file on Cloud: db → schema → service.

 
## What This Is Not

 
Not Meta Ads Manager. Not a bid optimizer. Spend truth stays in the ad account. Not an OCR vendor. Not PixelSearch as a hosted consumer UI.

 
## People Also Ask

 
**Image-only ads?** Leave `creative_video` null. Voiceover is empty; the image path still runs.

 
**Why not a second MiniLM index on copy?** You can add one. This receipt keeps CLIP on the pixels so lookalikes do not depend on OCR quality.

 
**How do I go to Cloud?** Same `app.py`. `PIXELTABLE_API_KEY`, `pxt://org:db`, then `pxt db update` → `pxt schema update` → `pxt service update`.

 
**Is this an AI automation workflow?** Yes. Insert the creative; copy, fatigue arithmetic, and CLIP run as columns. The category is [AI automation workflow](/blog/ai-automation-workflow).

 
## Score the Row, Search the Pixels

 
Declare the creative. Apply the file. Insert the JPEG. Query `HIGH` and lookalikes. Keep the catalog. Delete the tagging farm.

 

 - **[Pixeltable on GitHub](https://github.com/pixeltable/pixeltable)**: `pip install pixeltable`

 - **[ReelForge](/blog/reelforge-podcast-chaptering-viral-clips)** — podcast chapters

 - **[MedDossier](/blog/meddossier-clinical-intake-triage)** — clinical intake row

 - **[LectureSync](/blog/lecturesync-slide-lecture-qa)** — slides + lecture video

 - **[SnapCatalog](/blog/snapcatalog-product-visual-search)** — shop-the-look

 - **[Pixeltable documentation](https://docs.pixeltable.com)**