| name | remix-photo-grid-ad-from-sample |
| description | Remix a published Photo-Grid promo card from the Goose Ads library for a new brand — keep the source's grid layout, tile rhythm, scroll, and offer structure, swap in the brand's product photos, corpus video clips, wordmark, palette, copy, and promo code, get the card approved in-session, then render with create-photo-grid-ad and publish the MP4 back to Gooseworks with live stage reporting. The photo-grid counterpart to remix-imessage-ad-from-sample; this is what the app's Photo-Grid format tab calls. |
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",
"duration_sec": 10,
"style": {
"background": "#E6EEF3",
"background_tile": "#F4F8FB",
"background_band": "#DCE6EC",
"ink": "#0E1B22",
"ink_soft": "#4A5961",
"accent": "#31ABE8",
"font_sans": "Cabinet Grotesk",
"font_serif": "Zodiak",
"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,
"tile_px": [360, 380], "gap_px": 24,
"scroll": "horizontal-continuous", "edge_fade": true
},
"tiles": [
{ "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" }
}
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)
get_ad_project { project_id } → keep source_sample_id, app_url, brand_url, brand_id.
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.
- 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.
- 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:
- 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> }.
- 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.
- 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:
-
submit_render { project_id, kind: "full" } → keep render_id.
-
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 |
-
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)
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.
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.
- One render → done (latest shows by default). Multiple kept versions →
set_final_render with the
best render_id.
- 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