Skip to main content
Creative Analytics is Mission Control’s end-to-end pipeline for understanding ad creatives. Every creative ingested from Meta, TikTok, or another connected ad platform is automatically analyzed by GPT-4o vision (frame-by-frame for video), transcribed (Whisper for video), tagged across an 8-category taxonomy, embedded into a pgvector index, and then fed into a daily Creative Brief — a persisted artifact recommending the next creative bet, with rationale, risks, and inspirations.
The pipeline is fully idempotent end-to-end. Each stage is SHA-keyed on its input so re-runs against unchanged content cost nothing. Newly ingested creatives flow through automatically via a post_save signal on AdCreativeFields.

What it does

Sees the creative

Multi-frame vision (5 keyframes per video, 1 frame per still) + Whisper transcript for audio. Output: structured analysis JSON with hooks, themes, on-screen copy, CTA, color palette, frame-by-frame observations, and a diagnostics block with strengths / weaknesses / recommended fixes.

Tags the creative

Auto-tags drawn from a closed 8-category taxonomy (hook type, format, visual style, pace, emotion, CTA style, audience signal, messaging theme). Per-category confidence floors mean precision-critical categories like hook type need ≥0.75 confidence; fuzzy categories like emotional tone are fine at 0.5. Manual tags are never overwritten.

Embeds the creative

Two pgvector embeddings per creative: analysis_text (concept similarity) and image (visual-twin / near-duplicate detection). Powers cosine kNN, free-text semantic search, k-means clustering, and the net-new-ratio signal that drives exhaustion.

Detects fatigue

Hook exhaustion is computed per (client × archetype) from spend share, ROAS slope, CTR slope, and net-new ratio. A composite score crosses a threshold to mark an archetype as exhausted, which fires the creative.hook_exhausted automation trigger.

Names the concepts

Weekly k-means over the analysis-text embedding space produces concept clusters (e.g. “UGC founder testimonial — vertical, fast-paced”). Each cluster is named by GPT-4o-mini from the cluster’s tag distribution + 3 sample analyses. Stored as CreativeConcept rows.

Writes the brief

Once a day per tenant, a single source-of-truth Creative Brief is generated from the full library state. It is persisted as CreativeBrief, surfaced on the Ad Creative Analytics page, posted to Slack via the daily digest, and returned by Bob’s recommend_next_creative_brief tool.

Pipeline architecture

┌──────────────────────────────────────────────────────────────────────┐
│   Ad platform ingestion (Meta / TikTok / Google)                     │
│   ──> AdCreativeFields rows materialized in interaction_insight      │
│       with s3_image_url and/or s3_video_url populated                │
└──────────────────────────────────────────────────────────────────────┘

                              ▼   post_save signal
┌──────────────────────────────────────────────────────────────────────┐
│   knowledge_base.creative_analysis.analyze_creative                  │
│                                                                      │
│   Image asset:  fetch bytes → 1 frame                                │
│   Video asset:  ffmpeg keyframes (5)  +  Whisper transcript          │
│                                                                      │
│   GPT-4o vision  →  structured JSON (auto_tags, diagnostics, ...)    │
│                                                                      │
│   Persists CreativeAnalysis (kb_creative_analysis)                   │
│   Promotes auto_tags above per-category floors                       │
│   Mirrors prose synthesis into a "Creative Insights" KB              │
└──────────────────────────────────────────────────────────────────────┘

                              ▼   .delay()
┌──────────────────────────────────────────────────────────────────────┐
│   marketing_resources.tasks.embed_creative                           │
│                                                                      │
│   Two embeddings per creative:                                       │
│     kind='analysis_text'  ←  prose synthesis of the analysis         │
│     kind='image'          ←  format + frame_observations + OCR copy  │
│                                                                      │
│   text-embedding-3-small (1536-dim, HNSW pgvector index)             │
└──────────────────────────────────────────────────────────────────────┘

        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ refresh_hook_    │  │ refresh_creative_│  │ generate_daily_  │
│ exhaustion_cache │  │ concepts         │  │ brief (fan-out)  │
│ (hourly)         │  │ (weekly Mon)     │  │ (hourly poller)  │
│                  │  │                  │  │                  │
│ Composite signal │  │ k-means + GPT-4o-│  │ Per-tenant LLM   │
│ per archetype.   │  │ mini naming pass.│  │ call grounded in │
│ Emits trigger on │  │ Names concepts   │  │ exhaustion +     │
│ False→True.      │  │ from tag distrib │  │ diversity + top  │
│                  │  │ + sample summary.│  │ performers + ... │
│                  │  │                  │  │                  │
│ HookExhaustion-  │  │ CreativeConcept  │  │ CreativeBrief    │
│ Snapshot rows.   │  │ rows.            │  │ rows.            │
└──────────────────┘  └──────────────────┘  └──────────────────┘
        │                     │                     │
        └─────────────────────┼─────────────────────┘

┌──────────────────────────────────────────────────────────────────────┐
│   Surfaces                                                           │
│                                                                      │
│   Frontend  ▶  /ad-creative-analytics page (KPIs, brief, exhaustion  │
│                  chart, concept bubbles, semantic search, explain)   │
│   Slack     ▶  Daily Mission Briefing — Creative Brief block         │
│   Bob       ▶  8 deep-dive tools (recommend_next_creative_brief is   │
│                  a thin fetch wrapper over CreativeBrief)            │
│   API       ▶  /api/marketing_resources/creatives/{daily-brief,      │
│                  hook-exhaustion, concept-clusters, search,          │
│                  similar, explain, cost}                             │
│   Automations ▶ creative.hook_exhausted trigger fires user-defined   │
│                  Slack alerts + Note creation when an archetype      │
│                  transitions from healthy to exhausted               │
└──────────────────────────────────────────────────────────────────────┘

Data model

ModelStoresKey fields
interaction_insight.AdCreativeFieldsIngested creatives from ad platformsclient_id, ad_id, s3_image_url, s3_video_url, ad_name
knowledge_base.CreativeAnalysisVision-pass output (one row per creative)analysis (JSON), transcript, frames_analyzed, cost_cents, asset_kind
marketing_resources.CreativeTagCategory8 system categoriesslug, name, confidence_floor
marketing_resources.CreativeTagSystem tags + per-client custom tagsslug, category, is_system
marketing_resources.CreativeTagAssignmentTag assignments to creativestag, creative_id, source (manual/auto), confidence
marketing_resources.CreativeEmbeddingpgvector embeddings (2 kinds per creative)embedding (1536-dim), kind, cost_cents
marketing_resources.HookExhaustionSnapshotPer-(client × archetype) exhaustion signalexhaustion_score, is_exhausted, recommended_alternatives
marketing_resources.CreativeConceptNamed concept clustersname, summary, centroid, member_creative_ids, spend_30d, roas_30d
marketing_resources.CreativeBriefDaily Creative Brief (one per (client, date))headline, hook_concept, recommended_*, rationale, risks, inspirations

Key design decisions

The first version of recommend_next_creative_brief re-aggregated everything every call (and returned hardcoded format='ugc', audience='gen_z' defaults). v2 promotes the brief to a CreativeBrief row generated once per day per tenant by a Celery task. The page, Slack, and Bob all read the same row — single source of truth, cacheable, auditable, comparable day-over-day.
For video creatives s3_image_url is just a thumbnail — one frame. Short-form social ads ARE the first 3 seconds; one frame loses the hook. The pipeline runs ffmpeg to extract 5 keyframes (frame 0 + 4 evenly spaced) and passes all of them to GPT-4o vision in a single multimodal call.
Voiceover and on-screen captions are half the storytelling. Video assets are transcribed via whisper-1; both the raw transcript and the model’s distilled spoken_hook land on the row and fold into the embedding doc.
kind='analysis_text' embeds a prose synthesis of the analysis JSON — used for concept similarity and free-text search. kind='image' embeds a visually-flavored doc (format notes, frame observations, palette, OCR copy) — used for visual-twin / near-duplicate detection. The two are queryable independently from the API and Bob tools.
Different categories deserve different confidence thresholds. hook_type benefits from precision (≥0.75) so wrong tags don’t poison the exhaustion signal; emotion is inherently fuzzy and is fine at 0.5. Floors are stored on CreativeTagCategory.confidence_floor and applied at auto-tag promotion time.
CreativeTagAssignment.source distinguishes manual from auto. The auto-tagger’s _upsert_assignment function only refreshes rows where source='auto'. A user’s deliberate tag is permanent unless they delete it themselves.
The first version of the embedding doc looked like **Hooks:** ...\n**Themes:** .... Those literal labels dominated the embedding vector and made semantic search return matches on the word Hooks. v2 emits a clean prose synthesis (3–6 sentences in plain English, transcript appended for video).

Surfaces

The same artifacts power four surfaces:
1

Ad Creative Analytics page (frontend)

/ad-creative-analytics — KPI tiles, full Daily Brief panel, hook-exhaustion chart, concept-clusters bubble chart, top performers, semantic search with concept↔visual-twin toggle, and a per-creative explain drawer with diagnostics, transcript, tags, benchmarks, and similar creatives.See Analytics page.
2

Daily Slack digest

The existing daily Mission Briefing Slack message now includes a Creative Brief block: headline, recommended bet badges, hook concept, three rationale bullets, and a button linking to the analytics page. Honors the existing daily_digest_enabled / daily_digest_time per-workspace settings.
3

Bob deep-dive tools

8 LangGraph tools for ad-hoc creative analysis in the chat: similarity (concept + visual), free-text semantic search, hook exhaustion, diversity score, concept clusters, side-by-side compare, top performers per archetype, and recommend_next_creative_brief (which fetches today’s persisted brief or generates one synchronously).See Space Bob.
4

Automation triggers

creative.hook_exhausted fires on the False→True transition when an archetype’s composite score crosses 0.6 with spend share ≥ 0.2 and a negative ROAS slope. Wire it to an automation rule to send Slack alerts, create Notes, or hand off to another agent.

Cost profile

Costs are tracked on every row (tokens_in, tokens_out, cost_cents) so a per-tenant 30-day spend rollup is just creative_cost_for_client(client_id). Defaults at May 2026 OpenAI rates:
OperationPer-call cost
Vision (single image)~$0.005
Vision (5-frame video) + Whisper transcript~$0.025
Embedding (both kinds combined)~$0.0002
Daily brief generation~0.020.02–0.05 per tenant per day
Concept naming (per cluster, weekly)~$0.003
The analytics page surfaces 30-day spend in a KPI tile. The creative_pipeline_status management command prints it for every tenant. Backfill cost previews are available via run_creative_pipeline --dry-run.

Where the code lives

LayerPath
Ingestion signalinteraction_insight/signals_creative.py
Vision + auto-taggingknowledge_base/creative_analysis.py
Asset pipeline (frames + transcript)knowledge_base/asset_pipeline.py
Embeddingsmarketing_resources/tasks/embed_creative.py
Hook exhaustionmarketing_resources/tasks/refresh_hook_exhaustion.py
Concept clustersmarketing_resources/tasks/refresh_creative_concepts.py
Daily brief generatormarketing_resources/services/brief_generator.py
Daily brief Celery surfacemarketing_resources/tasks/generate_daily_brief.py
API endpointsmarketing_resources/views/creative_*.py
Bob deep-dive toolsai_chat/tools_creative.py
Frontend pagel5ui/src/app/components/ad-creative-analytics/
Beat schedulelayerfive/celery.py

What’s next

Daily Creative Brief

The brief generator deep dive — what’s in the LLM context, the persistence model, and how the same brief flows to UI / Slack / Bob.

Hook Exhaustion

The composite signal math, the False→True transition trigger, and how to wire automations off it.

Concept Clusters

k-means over embeddings, GPT-4o-mini naming, and the bubble chart UI.

Taxonomy & Tagging

The 8 categories, ~70 system tags, per-category confidence floors, and how to extend the taxonomy.

Embeddings & Similarity

pgvector setup, the two embedding kinds, and how kNN / semantic-search endpoints work.

Operations & Backfill

The four management commands (seed / run / status / embed-backfill), Celery beat, deployment, and cost monitoring.