PORTAL / LIBRARY / static-ad-generator

[ CREATIVE ]

/static-ad-generator

Full pipeline for generating up to 40 production-ready static ads for a ACME Agency client using Krea.ai Nano Banana 2.

Download the skill file (.md)

Placeholders like ACME Agency, <id> and you@example.com mark values that are per-agency — your install fills them with YOUR clients and accounts. If a section references a helper script you don't have yet, it ships with that workflow's install.

/static-ad-generator

Full static ad creation pipeline for ACME Agency clients: Brand DNA → Template selection → Krea.ai batch generation. Uses 40 proven ad format templates. Inspects existing client images from Drive for style continuity. Caches Brand DNA on Drive so returning clients skip research.

vs /krea-create-images: Use krea-create-images for a single one-off image. Use this skill for a full batch creative run (8–40 ads at once).

With /copywrite: For new client campaigns, run /copywrite meta "ClientName" first to generate and approve ad copy. Then run /static-ad-generator "ClientName" --phase 2 — Brand DNA is already cached, so it skips Phase 1 and goes straight to template selection using the approved copy as Mode A input.


Triggers


Preflight (run BEFORE any expensive Krea batch)

A static-ad batch is the most expensive operation in this workspace (8-40 Krea calls × ~48 compute units each). Validate everything BEFORE spending credits. If ANY check fails, abort with the specific failure — do not proceed.

  1. Client exists in ACME Agency/clients/clients.json. Resolve the canonical key (case-insensitive).
  2. CLIENT.md exists at ACME Agency/clients/<Client>/CLIENT.md. If missing → recommend /paradox-onboard first, do NOT proceed.
  3. brand-dna.md exists OR Phase 1 is set to build it. If --phase 2+ was passed but no brand-dna.md exists → abort with "run Phase 1 first".
  4. static_ads_prompts.json exists at the client folder if --phase 3 (skip generation) was passed. Otherwise it'll be built in Phase 2.
  5. drive_folder_id is present and reachable. Test with a listFiles() call before starting Phase 3 — Drive auth has expired in production before. Fail-fast beats failing 30 minutes into a batch.
  6. Krea API key set in .env. If missing → abort.
  7. Batch × templates × resolution stays under sane caps: batch * templates ≤ 80 and resolution 1K|2K|4K. If higher → require explicit confirmation in the prompt context.
  8. Reference assets resolve if reference_assets is set in clients.json: every named tag must point to a real Drive URL. If any fail → abort with the missing tag name.
  9. Slack channel resolves if Slack reporting is enabled. Skip cleanly if bot not invited (don't crash).
  10. Disk space check: confirm at least 500 MB free in the local client folder. Krea downloads + manifest can blow past quotas on small drives.

If all checks pass, log "preflight: OK (n templates × m batch = X images, ratio Y, est cost Z compute units)" and proceed.


Phase 1 — Brand DNA

Goal: Build a visual identity document that informs every prompt. Check Drive for a cached copy first to avoid redundant research.

Step 1.0 — Check Drive for existing Brand DNA

Before doing any research:

  1. Look up the client in ACME Agency/clients/clients.json — get drive_folder_id and Slack channel
  2. Run: gws drive files list --folder <drive_folder_id> and look for brand-dna.md
  3. If found and --refresh was NOT passed:
  1. If not found or --refresh was passed: Proceed with Steps 1.1–1.4 below

Step 1.1 — Load client context

Step 1.1b — Website scraping with Firecrawl

Goal: Get brand copy, colors, messaging, and visual style from the live website.

  1. Get the website URL from CLIENT.md or the notes field in clients.json
  2. Scrape with Firecrawl (handles JS-rendered pages, returns clean markdown):
node --input-type=module <<'EOF'
import { scrapeWithMeta } from './ACME Agency/scripts/lib/firecrawl.mjs';
const result = await scrapeWithMeta('<WEBSITE_URL>');
console.log('Title:', result.title);
console.log('Description:', result.description);
console.log('OG Image:', result.ogImage);
console.log('Content:\n', result.markdown.slice(0, 3000));
EOF
  1. Extract from the content:
  1. Also take a Playwright screenshot if you need exact hex colors for Brand DNA:
node --input-type=module <<'EOF'
import { chromium } from 'playwright';
const b = await chromium.launch();
const p = await b.newPage();
await p.setViewportSize({ width: 1280, height: 900 });
try { await p.goto('https://<WEBSITE_URL>', { waitUntil: 'networkidle', timeout: 15000 }); } catch {}
await p.screenshot({ path: 'ACME Agency/clients/<ClientName>/website-screenshot.png' });
await b.close();
EOF

Then Read the screenshot with Claude vision for exact hex colors, logo style, and font weight.

If the website is unreachable: fall back to web research, note "colors approximate — verify before publishing" in Brand DNA.

Step 1.1c — Logo & reference assets

The logo is auto-prepared on every Phase 3 run by static_ads_generate.mjs, which calls ensureClientLogo() from lib/logo_prepare.mjs. Resolution order: cached clients.json > reference_assets.logo → local logo.png in the client folder → Playwright screenshot from landing_page (or website URL in notes). The result is uploaded to Drive (public) and written back to reference_assets.logo automatically. The logo is then prepended to every template's reference images and the prompt is augmented with a "logo is canonical, reproduce pixel-exact" instruction.

You only need to intervene when the Phase 3 log says [Logo] No logo available. That means the client has no landing_page and no local logo.png. Drop a logo.png (or jpg/webp) into ACME Agency/clients/<ClientName>/ and re-run, or run the helper manually:

node ACME Agency/scripts/lib/logo_prepare.mjs "<ClientName>"          # idempotent (cache hit if already set)
node ACME Agency/scripts/lib/logo_prepare.mjs "<ClientName>" --force  # re-capture (use after a rebrand)

Then identify what other named reference assets this client has (interior, product, team, hero shot, etc.) and upload them from gallery-tmp/ to clients.json:

node --input-type=module <<'EOF'
import { uploadPublic } from './ACME Agency/scripts/lib/google_drive.mjs';
const folderId = '<drive_folder_id>';
const asset1 = await uploadPublic('ACME Agency/clients/<C>/gallery-tmp/<filename>', folderId);
console.log(asset1);
EOF
"reference_assets": {
  "logo":    "https://drive.google.com/uc?id=...",
  "<tag2>":  "https://drive.google.com/uc?id=...",
  "<tag3>":  "https://drive.google.com/uc?id=..."
}

Name each asset by what it shows (e.g. "exterior", "product", "team", "interior", "hero-shot") — the tag names are referenced in reference_tags in the prompts JSON. Use names that describe the content, not the client-specific subject.

Why this matters: Named assets are passed directly to Krea as reference images per-template instead of a random product-images fallback. Krea sees the real logo and places it naturally where it fits in the design — no forced overlay.

Step 1.2 — Drive Galerija inspection

Check the client's Drive folder for existing creative images — these become style inspiration for the Brand DNA.

Uses google_drive.mjs (no gws CLI needed):

// Step 1: list all files/subfolders in the client Drive folder
node --input-type=module <<'EOF'
import { listFiles, downloadFile } from './ACME Agency/scripts/lib/google_drive.mjs';
import { mkdir } from 'fs/promises';

const FOLDER_ID = '<drive_folder_id>';  // from clients.json
const CLIENT = '<ClientName>';

// Find Galerija subfolder (also matches 'Galerija', 'Gallery', 'galeria')
const all = await listFiles(FOLDER_ID);
const gallery = all.find(f =>
  f.mimeType.includes('folder') &&
  /galeri|gallery/i.test(f.name)
);

if (!gallery) { console.log('No gallery folder found'); process.exit(0); }
console.log('Gallery folder:', gallery.name, gallery.id);

// List images inside Galerija
const images = await listFiles(gallery.id, { mimeType: 'image' });
console.log(`Found ${images.length} images`);

// Download up to 5 most recent
await mkdir(`ACME Agency/clients/${CLIENT}/gallery-tmp`, { recursive: true });
for (const img of images.slice(0, 5)) {
  const ext = img.name.split('.').pop() || 'jpg';
  const dest = `ACME Agency/clients/${CLIENT}/gallery-tmp/${img.name}`;
  await downloadFile(img.id, dest);
  console.log('Downloaded:', dest);
}
EOF
  1. Read each downloaded image using the Read tool (Claude vision). For each note:
  1. Add "Existing Creative Style" section to Brand DNA with these observations — this informs the Prompt Modifier with real visual evidence, not assumptions.
  1. If no gallery folder or no images found: note it and proceed — Prompt Modifier will rely on screenshot + web research.

Step 1.3 — Web research

Use web search to fill any gaps not covered by CLIENT.md, screenshot, and gallery:

  1. "[ClientName] [city/country] branding" — design agency credits, brand guidelines
  2. "[ClientName] Facebook ads" or Meta Ad Library — current ad creative approaches
  3. 1–2 direct competitors for visual differentiation context

Skip searches where CLIENT.md + screenshot + gallery already provide sufficient detail.

Step 1.4 — Write and save Brand DNA

Compile all research into a structured document. Save locally AND upload to Drive via API.

# Brand DNA — [ClientName]
Generated: [DATE]
Drive cached: yes

## Brand Overview
- Business: [one sentence]
- Market: [Croatian / regional / other]
- Voice adjectives: [5 descriptors, e.g. "professional, warm, trustworthy, modern, local"]
- Positioning: [what makes them different]

## Visual System
- Primary font: [name or description, e.g. "geometric sans-serif, bold weight"]
- Secondary font: [body/supporting font]
- Primary color: [name + exact hex from screenshot]
- Secondary color: [name + exact hex]
- Accent color: [name + exact hex]
- Background colors: [white, off-white, dark, etc.]
- CTA color: [color + style]

## Logo
- Description: [exact visual description for Krea.ai: font style, text content, icon shape, colors]
- Example: "Bold condensed sans-serif 'ACME Agency' in sky blue (#your-channel), small gold lightning bolt icon to the left"
- Placement in ads: bottom right corner, white version on dark backgrounds

## Photography Direction
- Lighting: [e.g. "soft diffused natural light"]
- Color grading: [warm/cool/neutral, saturation]
- Composition: [e.g. "centered subjects, clean negative space"]
- Subject matter: [people, product, lifestyle, architectural, etc.]
- Props & surfaces: [typical backgrounds, objects in frame]
- Mood: [e.g. "approachable and professional"]

## Product / Service Details
- Core offering: [specific description]
- Visual appearance: [what it looks like in an ad]
- Distinctive elements: [logo marks, key visuals, brand identifiers]

## Existing Creative Style
[Observations from Galerija inspection — or "No gallery found" if empty]
- Photography: [observations]
- Copy density: [observations]
- Composition: [observations]
- Mood: [observations]

## Ad Creative Style
- Formats currently run: [what types they use]
- Text overlay: [minimal / heavy, style]
- Cultural cues: [Croatian market, language preference HR/EN]

## Image Generation Prompt Modifier
[50-75 word paragraph prepended to EVERY generated prompt. Must include: EXACT hex colors from screenshot, precise logo description, font style descriptors, photography direction, lighting, mood, cultural/market cue. End with logo placement instruction: "bottom right corner: [logo description]". This is the most critical output.]

Save locally: ACME Agency/clients/<ClientName>/brand-dna.md

Upload to Drive via API:

node --input-type=module <<'EOF'
import { uploadFile } from './ACME Agency/scripts/lib/google_drive.mjs';
const fileId = await uploadFile(
  'ACME Agency/clients/<ClientName>/brand-dna.md',
  '<drive_folder_id>',
  'brand-dna.md'
);
console.log('Uploaded brand-dna.md, fileId:', fileId);
EOF

Tell the user: "Brand DNA saved locally and uploaded to Drive. Ready for Phase 2."


Phase 2 — Prompt Generation

Goal: Select the right templates, write or adapt copy, fill all placeholders → produce static_ads_prompts.json.

Step 2.0 — Detect input mode

Before doing anything else, determine which mode the user is in:

MODE A — Copy provided: User has given specific text (headlines, body copy, offers, testimonials).

MODE B — Brief or idea only: User gives a campaign concept, goal, product focus, or just the client name.

In both modes: Before generating any prompts, show the user:

Step 2.1 — Template selection by client type

Use these rules to guide template selection:

Service businesses (dental, medical, legal, real estate, financial):

E-commerce / product brands:

B2B / agency / professional services:

Croatian market note: Lifestyle and testimonial formats convert better in the Croatian market. Heavy number/stat ads perform better in US/UK markets. When in doubt, lead with human faces and clear local context.

Step 2.2 — Fill templates

For each selected template from references/template-prompts.md:

  1. Keep the opening line "Use the attached images as brand reference." — Krea.ai supports imageUrls and the script passes up to 5 product images from the client folder automatically
  2. Prepend the Image Generation Prompt Modifier from Brand DNA to the start of the prompt
  3. Replace ALL [BRACKETED PLACEHOLDERS] with brand-specific details
  4. Ensure text overlay copy is in the correct language (Croatian for Croatian-market clients unless instructed otherwise)
  5. Do not leave any [BRACKETS] in the final prompt — every placeholder must be resolved

Copy writing guidance for text overlays:

Step 2.3 — Save prompts JSON

Save to ACME Agency/clients/<ClientName>/static_ads_prompts.json:

{
  "client": "ClientName",
  "generated_at": "ISO timestamp",
  "brand_dna_version": "YYYY-MM-DD",
  "input_mode": "A | B",
  "prompts": [
    {
      "template_number": 3,
      "template_name": "testimonials",
      "prompt": "Full completed prompt ready for Krea.ai Nano Banana 2...",
      "aspect_ratio": "9:16",
      "reference_tags": ["logo", "interior"],
      "notes": "Copy uses fictional testimonial — replace with real patient quote before publishing"
    }
  ]
}

reference_tags rules:


Show the user a summary (template name + first 80 chars of prompt) and ask for approval before Phase 3.

---

## Phase 3 — Image Generation

**Goal:** Run batch generation via Krea.ai and deliver organized output.

### Step 3.1 — Confirm count and cost

State before running:
- Number of images: `[templates selected] × [batch]`
- Estimated cost: ~48 Krea compute units per image at 2K
- Ask: "Ready to generate?" unless user has already confirmed

### Step 3.1b — Reference images

The script routes images per-template using `reference_tags` in the prompts JSON:
1. **Named assets** — if `reference_tags` is set, maps each tag to a URL from `clients.json > reference_assets`
2. **Fallback** — if no `reference_tags`, uploads images from `product-images/` (or client root) on-the-fly

Krea receives up to 5 reference images per generation call. It uses them for visual consistency (logo, brand colors, people, environments) — it places them naturally rather than being forced into a fixed position.

**Before running Phase 3:** confirm `clients.json > reference_assets` has at least `"logo"` populated. If not, run `logo_prepare.mjs` first (Step 1.1c).

### Step 3.1c — Drive output folder

Images are automatically uploaded to a structured folder inside the client's Drive:

<client drive_folder_id> └── Social media design/ └── <client.market or "Croatia">/ └── <year>/ └── <month>/


Folders are created on demand if missing. To set a non-default market, add `"market": "Germany"` (or whichever) to the client entry in `clients.json`.

### Step 3.2 — Run the script

node ACME Agency/scripts/static_ads_generate.mjs "ClientName" \ [--templates 1,3,7] \ [--batch 1] \ [--resolution 2K] \ [--no-slack] \ [--no-drive] \ [--no-images] # skip image reference, pure text-to-image


**Default settings:**
- Templates: all in `static_ads_prompts.json`
- Batch: 1 image per template
- Resolution: 2K
- Slack: enabled | Drive: enabled | Image reference: enabled (if images found)

### Step 3.3 — Output

The script saves images to `ACME Agency/clients/<ClientName>/static-ads/<NN-template-name>/`, uploads all to Drive, posts Slack report grouped by template, and appends to `static_ads_manifest.json`.

After completion: tell the user the Drive folder link and ask if they want to iterate on any template.

## Verification (run AFTER Phase 3 — prove it shipped, don't assume)

Rubric: [verify.md](verify.md). Two layers — one enforced, one you run.

**Deterministic (automatic, in `static_ads_generate.mjs`).** Every batch now runs
`validateFiles()` from `shared/verify/assert_output.mjs` BEFORE Drive upload: any 0-byte /
sub-10 KB Krea stub is dropped (never uploaded, never counted as a shipped ad), logged
`[Verify] … dropped`, and recorded as `invalid` in `static_ads_manifest.json`. The summary
line excludes them. So "size > 10 KB" and "no silent stub" are now proven in code, not
hoped. Still confirm from the run output: success count matches selected templates (failed
templates are listed, not hidden), Drive folder created, every valid image has a Drive URL,
Slack post returned a non-null `ts`.

**Vision quality (you run, before the client sees them).** Invoke the `verifier` agent on
a representative sample of the generated PNGs against [verify.md](verify.md) (threshold 8).
Regenerate any template it scores < 8 via `--templates N` (cap 2 passes); only ≥ 8 ships.
This is what catches garbled text, a wrong/redrawn logo, and off-brand color — the failures
the file-size check can't see. If the brand is consistently off, fix `brand-dna.md` and
re-run Phase 2 before regenerating.

Never report "all done" while any layer shows a gap — name the specific failure.

---

## Selective & Re-run Flows

| Goal | Command |
|------|---------|
| Re-run prompts only (new campaign, same brand) | `--phase 2` |
| Force new Brand DNA research | `--refresh` |
| Generate only specific templates | `--templates 3,11,17` |
| A/B testing — 4 variations of winners | `--templates 3,6 --batch 4` |
| Quick test run before full batch | `--templates 1,3,10 --batch 1` |

---

## Key Files

| File | Purpose |
|------|---------|
| `.claude/skills/references/template-prompts.md` | **Primary template library — 40 ad formats** |
| `.claude/skills/static-ad-generator/templates.md` | Quick 12 simplified templates (reference only) |
| `ACME Agency/scripts/static_ads_generate.mjs` | Batch generation script |
| `ACME Agency/scripts/lib/krea.mjs` | Krea.ai API wrapper |
| `ACME Agency/scripts/lib/slack.mjs` | Slack posting |
| `ACME Agency/scripts/lib/logo_prepare.mjs` | Screenshots logo from live website, uploads to Drive, returns public URL |
| `ACME Agency/clients/clients.json` | Client registry (drive_folder_id, slack_channel) |
| `ACME Agency/clients/<Name>/CLIENT.md` | Source brand context |
| `ACME Agency/clients/<Name>/brand-dna.md` | Brand DNA (local copy) |
| `ACME Agency/clients/<Name>/static_ads_prompts.json` | Filled prompts (Phase 2 output) |
| `ACME Agency/clients/<Name>/static-ads/` | Generated images (Phase 3 output) |
| `ACME Agency/clients/<Name>/static_ads_manifest.json` | Generation history |

---

## Tips for Better Output

- **Test before full batch.** Run `--templates 1,3,10` first to validate the Prompt Modifier. If the style is off, adjust Brand DNA and re-run Phase 2 before firing all 40.
- **Richer CLIENT.md = better Brand DNA = better ads.** Ask the user for the website URL and 2–3 "ads we like" examples before Phase 1 if CLIENT.md is sparse.
- **Stats and ACME Agencyws are fictional** — always note this to the user. They should substitute real testimonials and verified numbers before publishing.
- **Croatian copy:** Keep it natural and human, not corporate. Short active sentences. "Zakažite odmah" beats "Iskoristite mogućnost zakazivanja".
- **Mode B quality tip:** Ask the user one question before writing copy — "What's the single most important thing this campaign needs to communicate?" — then make every template answer that question differently.