SuperAPI AI Directory

remix-photo-grid-ad-from-sample

Marketing & growth

View repository

Updated 10-15 21:54

About this skill

remix-photo-grid-ad-from-sample

Purpose

Given one published Photo-Grid ad sample (from the Goose Ads library) + a researched brand, produce a finished Photo-Grid promo card for that brand. The source sample defines the shape — grid dimensions, tile rhythm (which slots are product photos vs video clips vs big-type offer tiles vs the promo-code tile), scroll pacing, headline/sub/chip structure, end offer — and this skill swaps the substance: the wordmark becomes the brand's logo, the product/clip tiles become the brand's product photos + clips harvested from its own ad corpus, the palette/fonts shift to the brand, and the headline + offer + code carry the brand's real copy.

This skill is orchestration: it derives + gets approval for the card spec, then hands rendering to create-photo-grid-ad and handles all Gooseworks I/O (render rows, stage reporting, durable upload, final pin). Craft rules (grid layout, scroll, type rhythm, ffmpeg) live in the create molecule — do not duplicate or override them here.

It runs on the user's machine inside their Claude Code session (the goose-video local-worker model). The runtime contract — MCP tools, proxies, durable URLs — is the installed ads-remix master skill; this recipe assumes it.

Inputs

Input Required Notes
project_id yes (app flow) A draft Gooseworks ad project with source_sample_id + format_key: "photo-grid". The paste-prompt carries it. Without one, create_ad_project { brand_id, name, source_sample_id, format_key: "photo-grid" } first.
source sample from project get_ad_template { template_id: <source_sample_id or slug> } → recipe.card (the source card spec: grid, tiles, palette, fonts, header copy, offer, chips), extracted_script, how_to, media_url (watch it if the structure is unclear).
brand yes Researched brand pack (research_status: "complete"); else run the brand-research recipe first. Needs: clean product photo(s), logo/wordmark, brand palette, a usable ad/video corpus (or accept Pexels tiles), CTA code or offer.
angle optional User-supplied steer ("Mother's Day", "code SUMMER20", "lead with the app not the band"). Folded into the card copy + offer.
machine yes ffmpeg + Playwright Chromium on the user's machine (preflight below).

Recipe schema (recipe.card)

A photo-grid sample stores its template in recipe.card. The remix reads it to preserve the grid + type rhythm and swaps the substance; the create molecule renders it. Canonical shape:

{
  "format": "photo-grid",
  "aspect_ratio": "9:16",            // or "4:5"
  "duration_sec": 10,                // 8–12
  "style": {
    "background": "#E6EEF3",         // page bg — MUST pass legibility vs `ink`
    "background_tile": "#F4F8FB",    // tile bg
    "background_band": "#DCE6EC",    // product-tile band bg
    "ink": "#0E1B22",                // primary text
    "ink_soft": "#4A5961",           // sub text
    "accent": "#31ABE8",             // code-tile accent
    "font_sans": "Cabinet Grotesk",  // display sans (headline/sub/chips/code) — house default
    "font_serif": "Zodiak",          // serif for the offer/price tiles — house default
    "font_sizes": { "headline": 154, "sub": 38, "offer": 156, "code_value": 104, "code_label": 26, "chip": 30 }
  },
  "header": { "wordmark": true, "headline": "For dad,\nby Hume.", "sub": "The longevity band, gift-wrapped." },
  "grid": {
    "rows": 2, "cols": 5,            // cols = tiles PER ROW (not total tiles)
    "tile_px": [360, 380], "gap_px": 24,
    "scroll": "horizontal-continuous", "edge_fade": true
  },
  "tiles": [                          // length = rows × cols, row-major (slot 0 = top-left)
    { "slot": 0, "kind": "product-photo", "label": "Band hero" },
    { "slot": 1, "kind": "video-clip",    "label": "Athletic dad", "slowdown": 3.5 },
    { "slot": 2, "kind": "type",          "text": "25%" },
    { "slot": 8, "kind": "code",          "label": "CODE", "value": "DAD25", "accent_part": "DAD" }
  ],
  "offer": { "discount": "25% OFF", "code": "DAD25" },
  "chips": ["14 DAY BATTERY", "NEW PATENTED STRAP"],
  "audio": { "treatment": "silent" }  // "silent" | "bed" (+ "bed_label" when a bed is used)
}

tile.kind ∈ product-photo | video-clip | type | code. style is the single source of truth for colors + fonts + sizes (the font_sizes here are the pick-05 reference; never author below the Phase-1 floors). recipe must be a JSON object ≤ 64 KB — reference tile media by name/label, never inline image bytes. The Phase 1.5 review script_drafts.style/grid mirror this shape.

Composed Atoms

  • create-photo-grid-ad — the renderer (sibling molecule): authors the HTML hyperframe + frame-steps it via Playwright + encodes the MP4. This skill never re-implements its steps.
  • source-brand-assets — pull the brand's product PNGs, logo SVG, and palette for the tile + header swaps.
  • atoms/review/watch — watch the rendered MP4 (frames) for the output QC pass.
  • create-music-elevenlabs — optional instrumental bed, only when the source sample carries one (route through the Gooseworks ElevenLabs proxy locally).

Workflow

Phase 0 — Fetch + gates (no render row yet)

  1. get_ad_project { project_id } → keep source_sample_id, app_url, brand_url, brand_id.
  2. get_ad_template { template_id: source_sample_id } → keep recipe.card, extracted_script, how_to, media_url. If recipe.card is missing, reconstruct the grid/tile rhythm from extracted_script (the on-screen copy lines) and /watch the media_url to read the layout.
  3. Brand gate — REUSE a complete brand, never re-research it. get_ad_brand { brand_id }. If research_status is already complete, use the pack as-is — do NOT re-run research. Only if it is not complete, run the master skill's "Set up a brand" workflow first (idempotent), then continue.
  4. Machine preflight (renders run locally — fail fast and friendly): ffmpeg -version and npx playwright --version + a Chromium launch check. If either is missing, tell the user exactly what to install (brew install ffmpeg, npx playwright install chromium) or to run goose-video doctor, and STOP — do not open a render row that can only fail.

Phase 1 — Derive the brand's card spec

Map the source card onto the brand. Rules:

  • Keep the source's structure: same grid dimensions, same tile rhythm (which slots are product photos vs video clips vs big-type offer vs the promo-code tile), same scroll pacing, same headline/sub/chip placement. The structure is why this card reads.
  • Swap the substance: wordmark → brand logo; product-photo tiles → brand's clean PDP photos; video-clip tiles → sustained-shot HD clips harvested from the brand's OWN existing-ads corpus (probe existing-ads/ for HD before Pexels); headline + sub → the brand's hook in its voice; offer tiles + promo-code tile → the brand's real discount + code from the brand pack or angle.
  • Style tokens are a CANDIDATE, never law. The brand website's palette/typography is a starting point, not a mandate — the 2026-06-12 run shipped a website-lifted background + fonts + sizes that looked bad. Run every token through the taste gate, then surface it in the Phase 1.5 review:
    • Background: must pass a legibility gate against the headline/offer ink (aim ≥ 4.5:1 contrast). If the brand color fails, default to near-white (#F4F8FB / white) with dark ink, or a dark brand-neutral with light ink — never a low-contrast muddy combo. (pick-05 ships #E6EEF3 bg on #0E1B22 ink.)
    • Fonts: use the brand font ONLY if it's an actually-loadable webface (Google Fonts or Fontshare). Otherwise use the format's house faces — Cabinet Grotesk (display sans) + Zodiak (serif, for the offer/price tiles). A brand font that can't load renders as a wrong fallback and looks broken.
    • Type sizes: never below the muted-feed legibility floors (1080-wide 9:16 — scale to canvas but not below): headline ≥ 132px, sub ≥ 32px, offer/price ≥ 140px, code value ≥ 88px, code label ≥ 22px, chips ≥ 26px. (pick-05 ships 154 / 38 / 156 / 104 / 26 / 30.)
  • Image selection — real product photos only. Photo tiles come ONLY from real product/brand photos in brand-assets/manifest.json. Read each chosen image at full res before using it to confirm subject + sharpness:
    • Hard minimum source resolution ≥ 720px on the short edge (tiles render 360–720px); never upscale a source past ~1.5× its native size (the incident's tiles were blurry upscales).
    • Never use a favicon or a logo as a photo tile — logos belong only in the wordmark slot.
    • If there aren't enough good photos, REDUCE the grid (drop a row / fewer tiles) or mark the slot a pending NB2 generation ({ kind, label, pending: true, note }, visible in review) — never pad with junk or stock-y filler.
  • Compliance: zero source-brand leakage — no source product, wordmark, code, or competitor reference may survive in any tile, the header, or the chips. Audit harvested clip captions for sensitive claims and drop any that carry them.

Phase 1.5 — Approval gate (in-session, with the in-app review mirror)

Photo-grid is image-heavy, so the review MUST show the actual tile images + the style tokens — not text. (The 2026-06-12 run mirrored text only: no images, bad website-lifted background/fonts, poor tiles — all of which the user could only catch after a wasted render.) The user vetoes background/font/photos HERE, before any render. Do all three:

  1. Persist the draft: write_file → agent-config/brands/<slug>/projects/<project_id>/working/card.json, and append_project_message { role: "agent", content: <the readable spec> }.
  2. Upload a review copy of EVERY chosen tile to working/review/<name> via get_upload_url (product photos AND each extracted video clip AND the wordmark AND any music bed). Read each image at full res first to confirm subject + sharpness.
  3. Mirror to the app's Review panel so it renders the format shape, the style, and the materials:
    update_ad_project_script {
      project_id,
      script_drafts: {
        format: "photo-grid",
        grid: { rows, cols, tiles: [ { slot, kind, label } ] },
        style: { background, font, font_sizes, accent },
        audio: { treatment: "silent" | "bed", bed_label? },
        materials: [
          { kind: "image" | "video", label, path: "working/review/<name>" },
          { kind: "image", label, pending: true, note: "<NB2 product render — the prompt>" }
        ]
      },
      script: <readable: headline, sub, tile-by-tile plan, offer + code, chips, AND a "Style:" block
               listing the background hex, the fonts, and the headline/offer/code/chip px sizes>
    }
    

Every chosen tile must appear in materials as a viewable image/clip. A tile that doesn't exist yet (an NB2 product render you'll generate) is listed pending: true with its prompt — never omitted. The approval message must spell out the style tokens (background color, fonts, type sizes) so the user can veto them before a render, not after. Do not render until the user says go. If they edit (copy, a photo, the background, a font), update card.json, re-upload changed materials, re-mirror, and re-confirm once.

Draft-only mode (free review): if the user's instruction says "draft only" / "script only" / "just the draft", STOP right after the mirror above — no render row, no paid generation, zero credits spent. Tell the user the draft (images + style) is now visible on the project page in the app, and that re-running the prompt (or replying "render it") completes the ad.

No-review escape: if the instruction says to skip review ("no review", "single-shot", "don't ask for approval"), skip the pause — still write working/card.json AND mirror via update_ad_project_script (so the project page carries the materials + style), then render immediately.

Phase 2 — Render (open the row FIRST, report stages)

Local renders are quick but still report stages so the app shows progress:

  1. submit_render { project_id, kind: "full" } → keep render_id.

  2. update_render_status { render_id, status: "running", stage: "script" } — then keep the stage current as you work:

    stage when
    script card spec approved, copy + tile plan locked
    assets product photos + logo pulled; corpus clips extracted + setpts-slowed; (bed generated if any)
    record Playwright frame-stepping the hyperframe
    mix optional music bed loudness-normalized + muxed (skip for silent cards)
    export 9:16 (or 4:5) master encode
  3. Follow create-photo-grid-ad end-to-end with the approved card spec. Local translations: write into a temp working dir (not a brand repo folder); music bed only if the source had one — generate via the Gooseworks ElevenLabs proxy (same pattern as the other recipes); this bills the user's credits, so keep the card silent when the source is silent.

Phase 3 — QC by watching

Run atoms/review/watch on the rendered master (or step its frames): the grid scrolls smoothly the full duration with the edge fade, the headline + chips are legible, every tile is the brand's (not the source's), the type/offer tiles fit, the promo-code tile reads as a voucher, no tile visibly loops, duration is within ±20% of the source. Fix-and-re-render at most once per issue; then run the create molecule's own Quality Checks list.

Phase 4 — Publish (durable URLs, then the links)

  1. get_upload_url → PUT the master MP4 to agent-config/brands/<slug>/projects/<project_id>/working/final.mp4 (content_type: video/mp4); grab a poster frame (ffmpeg -ss 1 -frames:v 1) and PUT it as working/final-thumb.jpg.
  2. update_render_status { render_id, status: "complete", output_url: "/api/ads/projects/<project_id>/render-file?path=working/final.mp4", thumbnail_url: "/api/ads/projects/<project_id>/render-file?path=working/final-thumb.jpg", stage: "export", duration_sec: <ffprobe seconds> } — the render-file path is the DURABLE form; never store a CDN or raw S3 URL.
  3. One render → done (latest shows by default). Multiple kept versions → set_final_render with the best render_id.
  4. End with BOTH links exactly as the master skill requires: app_url + brand_url, verbatim.

Decision Rules

  • This skill vs create-photo-grid-ad: remixing a library sample for a brand → this skill (it feeds the create molecule). A net-new Photo-Grid card from a brief with no source sample → the create molecule directly.
  • Card derivation source: recipe.card when present (normal); else extracted_script (on-screen copy) + watching media_url. Never invent a grid the source doesn't have.
  • REUSE a complete brand — never re-research. If get_ad_brand returns research_status: complete, use the pack as-is. Re-researching a complete brand wastes the user's credits and time.
  • Tile content: grounded brand corpus over Pexels. Harvest HD sustained-shot clips from the brand's existing-ads/ corpus before reaching for stock; only use Pexels when the brand has none.
  • Website palette/typography is a candidate, not law. Run it through the legibility + loadable- font gates (Phase 1); fall back to the house background/fonts/sizes when the brand tokens fail. A bad background/font/size combo is the #1 photo-grid failure (2026-06-12) — surface every token in the review so the user vetoes it before a render.
  • Photo tiles are real product photos only. From brand-assets/manifest.json, ≥ 720px short-edge, no favicons/logos as tiles, no > 1.5× upscales. Too few good photos → shrink the grid or mark the slot pending NB2, never pad with junk. Read each tile at full res before mirroring.
  • Review mirrors IMAGES, not just text. Every chosen tile (plus the wordmark / bed) is uploaded to working/review/ and listed in update_ad_project_script materials, so the app's Review panel shows the actual visuals + style tokens before any render. A text-only mirror is a bug.
  • Music bed: only if the source sample has one. Silent sources stay silent (feed cards autoplay muted, and silent is cheaper + faster).
  • Aspect: honor the source sample's recipe.card.aspect_ratio (a 9:16 source remixes 9:16, a 4:5 source 4:5). When the source doesn't declare one, default to 9:16.
  • Batch runs (the prompt lists several project ids): run Phases 0–1.5 for each first (collect all approvals in one message), then render sequentially — Playwright recordings fight for the display.

Output

  • working/card.json — the approved brand card spec (in the project's Gooseworks folder).
  • working/review/<name> — a review copy of every chosen tile (photos + clips), the wordmark, and any bed; mirrored into the project's script_drafts.materials via update_ad_project_script.
  • working/final.mp4 (9:16 or 4:5 master) + working/final-thumb.jpg in the project folder.
  • A complete render row carrying the render-file output_url, thumbnail_url, duration_sec, and final stage; set_final_render pin when >1 version.
  • A closing message with the project app_url + brand brand_url.

Quality Checks

  • Review mirror populated BEFORE any render: the project's script_drafts carries format, grid, style (background/font/font_sizes/accent), and a materials[] entry for every chosen tile (viewable image/clip, or pending with a note) — the app's Review panel shows images, not just text.
  • Background passes the legibility gate vs the ink; fonts are loadable faces; type sizes ≥ the floors.
  • Photo tiles are real product photos (≥ 720px short-edge, no favicons/logos, no > 1.5× upscale).
  • Grid dimensions + tile rhythm match the source (same slots are photo/clip/type/code).
  • Scroll is smooth + continuous the full duration with the edge-fade mask; no tile visibly loops.
  • Zero source-brand leakage anywhere (tiles, header, chips, code) — compliance-critical.
  • Product-photo tiles show the brand's actual product, crisp at phone scale; video tiles are HD.
  • Wordmark is the brand's real logo asset; headline/sub/offer/code carry the brand's real copy.
  • duration_sec reported matches ffprobe; render row carries render-file URLs, not CDN links.

Failure Modes

  • ffmpeg / Playwright missing → caught in Phase 0 preflight; user told the install commands (goose-video doctor); no render row opened.
  • Sample has no recipe.card and no usable extracted_script → tell the user this sample isn't remix-ready yet and stop; do not improvise a grid (the admin needs to fill the sample's recipe).
  • Brand pack lacks a clean product photo or logo → ask the user for one image (or a URL) before the approval gate; product-photo + wordmark tiles cannot be generic stock.
  • Brand has no HD corpus for video tiles → fall back to product-photo-only tiles or Pexels, and flag it; never ship upscaled-360p blur.
  • Review shows text but no images → the materials[] mirror was skipped or the tiles weren't uploaded to working/review/. Re-Read each chosen tile, get_upload_url → PUT to working/review/<name>, and re-call update_ad_project_script with the full materials[].
  • Background/font/sizes look bad → website tokens were used blindly. Apply the Phase-1 taste gate: near-white/dark-neutral background, house fonts when the brand face won't load, sizes ≥ the floors; re-surface the corrected tokens in the review.
  • Not enough good product photos → shrink the grid (fewer tiles/rows) or mark slots pending NB2 generations in the review — never pad with favicons, logos, or stock-y filler.
  • Render dies mid-run (Playwright crash, encode failure) → one retry of the failed phase; on second failure update_render_status { status: "failed", error_message } and report — never leave the row running.
  • Upload fails → retry the presigned PUT once; persistent failure → mark the render failed with the local file path in error_message so nothing is lost.

Tests

See tests/ — structural smoke, a sample project/sample input pair, the expected artifact list, a manual end-to-end script, and the verifier checklist.

Skill Location & Related