Management commands
seed_creative_test_data — local seeding
Creates synthetic AdCreativeFields rows pointing at stable Picsum image URLs plus matching Ads_AdTracker rows shaped to actually trigger interesting signals (some archetypes decline so exhaustion lights up, others have flat ROAS).
The seeded perf is shaped so:
- Spend is Pareto-skewed (a handful of ads carry most of the budget).
- 60% of ads convert; the rest are duds.
before_afterandurgencyarchetypes have a 4-week declining ROAS curve so hook exhaustion fires.- Other archetypes hold roughly steady or improve.
run_creative_pipeline — end-to-end orchestrator
Runs all five pipeline stages for one tenant or all tenants. Prints a per-tenant cost preview up front and prompts for confirmation if the bill exceeds $5.
Stages 1+2 enqueue Celery tasks (you need a worker on
automations_standard and automations_low). Stages 3+4+5 run synchronously, so the brief is materialized when the command exits.
creative_pipeline_status — read-only status
backfill_creative_embeddings — embedding-only backfill
Used during the v1 → v2 cutover (or any time you need to retroactively embed old CreativeAnalysis rows without running vision again).
analysis_text AND image rows exist. Pre-v2 single-kind rows are picked up so the missing kind lands on the next run.
analyze_creatives — vision-only backfill
Standalone wrapper for the vision-only stage of the pipeline. run_creative_pipeline calls this internally; you can invoke directly when you only want vision.
Celery beat schedule
The hourly cadence + idempotency means a worker outage of up to ~60 minutes recovers automatically on the next tick. Don’t run multiple beat workers — django-celery-beat’s
DatabaseScheduler is the single source of truth.
Task queues
Pertask_routes in layerfive/celery.py:
automations_standard (latency-sensitive ingest path) and automations_low (batch sweeps + briefs).
Deployment checklist
When deploying the v2 pipeline to a new environment:1
Postgres has pgvector
CREATE EXTENSION IF NOT EXISTS vector; is included as a defensive RunSQL in migration 0012_creative_v2_schema. If you’re on a managed Postgres that doesn’t allow extensions by default, enable pgvector at the DB level first (Supabase / RDS / Cloud SQL all support it).Verify with: python manage.py check — agent_surface.W002 warns if the extension isn’t loaded.2
ffmpeg is on the path
Required for video keyframe extraction. The
Dockerfile should apt-get install ffmpeg. Without ffmpeg, video assets degrade gracefully to thumbnail-only analysis (you lose frame-by-frame observations and the spoken_hook signal).Verify: which ffmpeg && which ffprobe.3
OPENAI_API_KEY is configured
Required for vision, Whisper, embeddings, brief generation, and concept naming.Verify:
python manage.py check — agent_surface.W001 warns if the key is missing.4
Migrations applied
python manage.py migrate marketing_resources (also picks up the knowledge_base migration adding cost / transcript columns).The migrations are idempotent — re-running on an already-migrated DB is a no-op.5
Beat scheduler enabled
Run
celery -A layerfive beat --loglevel=info exactly once per cluster. The schedule in layerfive/celery.py registers itself; user-created automations also write PeriodicTask rows that the same scheduler picks up.6
Workers running on both queues
celery -A layerfive worker -Q automations_standard,automations_low --concurrency=4Adjust concurrency based on OpenAI rate limits.Backfilling production
For a fresh tenant or a v1 → v2 migration:Cost monitoring
Cost is captured on every artifact:CreativeAnalysis.cost_cents— vision + Whisper combined, per creative.CreativeEmbedding.cost_cents— embedding call, per kind per creative.CreativeBrief.cost_cents— daily brief, per tenant per day.
creative_pipeline_status --json and aggregate the cost_30d_usd field across rows.
Troubleshooting
No creatives are being analyzed automatically
No creatives are being analyzed automatically
Check, in order:
- Is a Celery worker running on
automations_standard? (The signal enqueues there.) - Is the
interaction_insightapp loaded?apps.py:ready()raises on missing imports — if startup logs show a stack trace there, fix it before relying on the signal. - Does the
AdCreativeFieldsrow actually haves3_image_urlors3_video_url? The signal short-circuits without media. - Does the row have a
client_id? The signal also short-circuits without it. - If saves come from a code path passing
update_fields, the v2 signal short-circuits unless one of the media URL fields is in the list. Audit the call site.
kb-sweep-unanalyzed-creatives runs hourly and picks up missed creatives. If even that’s not running, check the beat scheduler.Vision call returns _unparseable
Vision call returns _unparseable
The model occasionally returns markdown-fenced JSON or invalid JSON despite
response_format='json_object'. v2 catches that and returns {'_unparseable': '<first 5000 chars>'}. The analysis is not persisted in this case — re-run with --analyze-force to retry.Recurring unparseable responses on the same asset usually mean the model couldn’t fetch the image (private S3 URL, dead link). Test the URL with curl -I from the worker host.Hook exhaustion never fires
Hook exhaustion never fires
All three gates must hold simultaneously:
composite ≥ 0.6— high enough composite score.spend_share ≥ 0.20— enough budget on this archetype.roas_slope < 0— actually declining.
Brief shows status='failed'
Brief shows status='failed'
The generator persists a row with
status='failed' if the LLM call errored. The headline includes the error class. Common causes:OPENAI_API_KEYnot set (system check W001 should have warned at boot).- Rate limit (
429). Retry; the existing token bucket usually recovers within seconds. - Network timeout. Re-running with the Regenerate button typically succeeds on retry.
POST /daily-brief/regenerate/ with body { "client_id": "..." }.Concept clusters are missing names
Concept clusters are missing names
The naming pass is best-effort. If the GPT-4o-mini call fails for a cluster, the row is persisted with
name="Cluster N" and summary="". The page still renders it. The next weekly refresh tries again.Force an immediate retry: POST /concept-clusters/refresh/.Frontend shows 'permission denied'
Frontend shows 'permission denied'
The page module gate uses
validateAccessPermission (not the strict validateBlockAccessPermission). Super admins always pass. For other users, permissions['creative-reporting']?.view must be true — same gate as the existing Creative Library.See Permissions for the full agent-surface gate..png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)