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
Data model
| Model | Stores | Key fields |
|---|---|---|
interaction_insight.AdCreativeFields | Ingested creatives from ad platforms | client_id, ad_id, s3_image_url, s3_video_url, ad_name |
knowledge_base.CreativeAnalysis | Vision-pass output (one row per creative) | analysis (JSON), transcript, frames_analyzed, cost_cents, asset_kind |
marketing_resources.CreativeTagCategory | 8 system categories | slug, name, confidence_floor |
marketing_resources.CreativeTag | System tags + per-client custom tags | slug, category, is_system |
marketing_resources.CreativeTagAssignment | Tag assignments to creatives | tag, creative_id, source (manual/auto), confidence |
marketing_resources.CreativeEmbedding | pgvector embeddings (2 kinds per creative) | embedding (1536-dim), kind, cost_cents |
marketing_resources.HookExhaustionSnapshot | Per-(client × archetype) exhaustion signal | exhaustion_score, is_exhausted, recommended_alternatives |
marketing_resources.CreativeConcept | Named concept clusters | name, summary, centroid, member_creative_ids, spend_30d, roas_30d |
marketing_resources.CreativeBrief | Daily Creative Brief (one per (client, date)) | headline, hook_concept, recommended_*, rationale, risks, inspirations |
Key design decisions
Brief is a persisted artifact, not a re-aggregated tool output
Brief is a persisted artifact, not a re-aggregated tool output
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.Multi-frame video, not just a thumbnail
Multi-frame video, not just a thumbnail
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.Whisper transcript for video
Whisper transcript for video
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.Two embedding kinds, not one
Two embedding kinds, not one
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.Per-category confidence floors
Per-category confidence floors
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.Prose embedding doc, not labeled JSON
Prose embedding doc, not labeled JSON
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: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.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.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.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:
| Operation | Per-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.05 per tenant per day |
| Concept naming (per cluster, weekly) | ~$0.003 |
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
| Layer | Path |
|---|---|
| Ingestion signal | interaction_insight/signals_creative.py |
| Vision + auto-tagging | knowledge_base/creative_analysis.py |
| Asset pipeline (frames + transcript) | knowledge_base/asset_pipeline.py |
| Embeddings | marketing_resources/tasks/embed_creative.py |
| Hook exhaustion | marketing_resources/tasks/refresh_hook_exhaustion.py |
| Concept clusters | marketing_resources/tasks/refresh_creative_concepts.py |
| Daily brief generator | marketing_resources/services/brief_generator.py |
| Daily brief Celery surface | marketing_resources/tasks/generate_daily_brief.py |
| API endpoints | marketing_resources/views/creative_*.py |
| Bob deep-dive tools | ai_chat/tools_creative.py |
| Frontend page | l5ui/src/app/components/ad-creative-analytics/ |
| Beat schedule | layerfive/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.
.png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)