| name | remix-ad |
| description | Remix an ad already uploaded to the Goose Ads library into a NEW ad — same proven structure, retargeted to a different brand/product (or the same brand, new angle). Reads the sample's remix payload (recipe shot-list, extracted_script, how_to, and remix_spec of atom-skills / voices / worlds / characters), decides what to KEEP vs SWAP, rewrites the script for the new brand while preserving the beat structure and timing, then routes to the matching production template (animated-explainer-ad, create-podcast-skit-ad, …) and runs the kept atom-skills in recipe order. The consumer counterpart to the upload-ad-sample skill. Use when the user says "remix this ad", "make this for [other brand]", "same format, new brand", or references a library sample to riff on. Needs a Goose Ads API token; regeneration needs the routed template's keys. |
remix-ad
Take a winning ad that's already in the Goose Ads library and make another one
from it — usually for a different brand, sometimes the same brand with a new
angle. The library sample carries a remix payload (written by the
upload-ad-sample skill); this skill reads it, keeps the proven skeleton, swaps
the brand-specific layer, and regenerates. It's the consumer end of the
upload↔remix loop.
This skill is a planner + router: it fetches the payload, makes explicit
keep-vs-swap decisions, rewrites the script, and hands the actual generation to
the production template that made the original (e.g. create-podcast-skit-ad).
When to use
- "Remix this ad for [other brand]" / "make the laundromat podcast ad but for [X]."
- "Same format, new brand" / "riff on sample
<id>" / "give me a new cut of this."
- Any time a library sample's structure is worth reusing on fresh brand content.
Do NOT use to: research a brand from scratch (use brand-research), or make an
ad with no source to remix (use a production template directly).
What it reads — the remix payload
Full contract in references/remix-payload.md. The library has no GET-by-id
route, so fetch_sample.py lists samples and filters by id/title. Each sample
carries: recipe (shot list + timing), extracted_script, skills_used,
how_to, and remix_spec (skills, voices, worlds, characters).
The skills are atoms, never a molecule. remix_spec.skills / skills_used
list atomic capability slugs (generate-voiceover, generate-broll-shot,
compose-master). Run exactly those atoms in recipe order — never substitute a
molecule, which would over-specify and re-pull steps the source didn't use.
Keep that discipline when you re-upload the remix.
Keep vs swap (default policy)
| Layer | Default | Notes |
|---|
recipe (format + beat arc) | KEEP | The proven format (e.g. two-host skit) and beat arc (hook → mechanism → payoff). The recipe is a reference; the new ad's scene count flexes with the new brand's copy density. |
remix_spec.skills (atoms) | KEEP | Run in recipe order to regenerate. |
how_to gotchas | KEEP | Carry forward. |
| script copy | SWAP | Rewrite freely for the new brand within the source's format + beat arc. Do NOT preserve the source's literal scene count — the new brand's mechanism complexity / copy density determines how many scenes it needs. |
| brand assets (wordmark, product, end card) | SWAP | New brand's real assets. Never AI-render brand text. |
| voices | keep (swap on request) | Reuse the selected voice unless the new brand needs a different read. |
| world/set | keep (swap on request) | Reuse the set, or pick a new themed one. |
| characters | swap (keep on request) | Usually recast for the new brand; reuse a soul_id only if the persona transfers. |
Setup
pip install -r requirements.txt
cp .env.example .env
The regeneration runs a production template — install ITS deps/keys when you reach the route step.
Workflow
-
Get the source payload — local-chain OR library. Two cases:
Review the source's printed summary or the JSON itself: recipe shots,
formatProfile, atom-skills, voices/worlds/characters, variant_assets[]
audit results, production_scripts[].
-
Intake the target — ASK explicitly for the four swap dimensions.
Use AskUserQuestion (or equivalent in the harness) to get:
- World: keep the source's set, or swap to a new themed set?
- Angle: one-line creative direction. If
clients/<slug>/concept-brief.md
exists, surface its top concepts as the option list rather than
inventing.
- Voices: keep / swap which voice_ids? (One row per source voice.)
- Characters: keep / swap / recast per source character (by sourceKey).
Do NOT infer these from a one-line product description. The user's
brief is usually skeletal — every Som and Hume run hit this and asked
the same four questions; standardize.
Get the new brand + product (+ guardrails). If the new brand has no
context yet (ls clients/<slug>/brand-research/ returns nothing — check
case-insensitively: clients/Hume-health/ and clients/hume-health/
are the same brand), run the brand-research skill first and pass its
brand_dir.
Brand-pack-current-SKU wins over the user's one-liner. If the user
says "can on nightstand" but 01-company-overview.md flags the can line
as EOL, use the current SKU and flag the swap. No question unless they
explicitly override.
-
Build the remix plan.
python scripts/build_remix_plan.py --source ./remix/source-sample.json \
--brand "<NewBrand>" --product "<New product>" \
[--keep-voices] [--keep-world] [--recast-characters] \
--out ./remix/remix-plan.json
The plan records keep/swap per layer, the ordered atom-skills, and the route_to_template. Confirm the routed template with the user before generating.
-
Rewrite the script. Using extracted_script and recipe as a reference, write the new brand's script that follows the same format + beat arc (hook → mechanism → payoff in the source's archetype, e.g. two-host skit). Do NOT force the same scene count — different brands have different copy density and mechanism complexity, so the new script flexes longer or shorter. What's proven and must carry: the format and the arc. Honor the new brand's guardrails (no competitor names, claim limits).
-
Route + regenerate. Hand the plan to the route_to_template skill, passing the kept layers (recipe beats, voices/world if kept, the atom-skill list) and the new brand assets + rewritten script. Run the kept atoms in recipe order. Lock the new timeline to the freshly rendered VO, not the source duration.
-
Assemble + QC. The routed template stitches, captions, builds the new brand's end card, and you /watch the master — confirm pacing matches the source's energy and every line lands.
-
Re-upload (optional). Push the remix back to the library via the upload-ad-sample skill. Keep skills_used / remix_spec.skills atom-level so the next remix stays clean.
Decision rules
-
Keep the format + beat arc; honor the source's sceneCount property.
Read source-sample.json.formatProfileProperties.sceneCount:
"flexible" (most formats) — scene count flexes with the new brand's
copy density. The archetype + arc are what's proven, not the literal
count.
"locked-to-source" (any format where audio meter sets timing — sung
music, scored cinematic, etc.) — mirror the source 1:1. Flexing would
desync the audio.
null or missing — surface to the user.
-
Atoms, not molecules. Run the exact atoms in remix_spec.skills, in recipe order. Never expand a molecule.
-
Lock timeline to the new VO, never the source duration — new copy runs to a different length. (Exception: locked-meter sung music.)
-
New brand's guardrails win. Re-apply claim/competitor limits for the target brand, not the source's.
-
Brand text composited, never AI-rendered — new wordmark + end card.
-
Confirm the routed template with the user before paid generation — unless the operator's prompt already names a format that uniquely matches the inferred template (e.g. "Ladder podcast skit" → create-podcast-skit-ad). Then skip the confirm.
-
For molecule routes without an executable driver (e.g.
create-podcast-skit-ad) OR when route_to_template = (confirm-with-user):
port the source run's working/*.py scripts and adapt brand-specific
paths. The molecule SKILL.md is a recipe, not executable; the source's
driver scripts are the closest runnable template. Read
source-sample.json.production_scripts[] to find them.
Common adaptations when porting:
REPO_ROOT path depth. Older drivers used
Path(__file__).resolve().parents[3]; the current
clients/<brand>/ad-runs/<run>/working/ layout needs parents[4].
Better: anchor on .git / CLAUDE.md presence. See
[[feedback_repo_root_parents4_clients_layout]].
- End-card brand assets (logo, hero PNG) → swap to the new brand's
assets after the audit below.
- Caption color → wire through the
--caption-style flag (see below).
python vs python3 shebangs — many machines only have python3.
-
Verify source variant + brand-asset availability BEFORE any paid call.
Audit results live in source-sample.json.remix_spec.characters[].variant_assets[]
— read them. For every kind: "lfs-pointer" or kind: "missing":
- Try
git lfs pull --include=<path> and git lfs checkout.
- If
git lfs ls-files returns the pointer but the LFS endpoint
returns 404 (objects committed as pointers but never pushed), regenerate
via the source's image atom (generate-character-image via
Higgsfield nano_banana_pro; spillover to create-image-nano-banana-2-fal
at $0.08/image on not_enough_credits). Don't try to lipsync against
a pointer text file — fal will return "Failed to load image" wrapped
in a generic RuntimeError that's hard to debug.
Same protocol for brand-asset PNGs / SVGs (logo, wordmark, hero
product shot) — file every one before the end-card build. If LFS-pointer-only
AND git lfs pull doesn't materialize, fetch from the brand's public
CDN (URLs in clients/<brand>/brand-research/asset-urls.md). Stage
replacements under <run-dir>/working/end-card-assets/ so the build
script doesn't depend on brand-assets/.
Surface the count of regenerated variants and replaced brand assets in
the plan summary.
-
Caption pipeline is audio-type-dependent. Read
source-sample.json.formatProfileProperties.audioType:
"sung-music" → drive captions from script.json scene windows
(vo per scene, tStartSec/tEndSec). Whisper-large-v3 returns
🎵 Music Playing 🎵 on atmospheric mixes — don't even try.
"spoken-vo" → Whisper word-level on the rendered audio, per
[[feedback_subtitles_from_whisper_word_level]] Case A.
"mixed" → split per segment by audio type and apply the matching
pipeline to each.
null / missing → surface to the user.
Pick the pipeline FROM the source-sample's audioType, not by trial
and error.
-
--caption-style flag — standardize the three known-good options
to avoid the per-run AskUserQuestion. Pass on stitch.py /
burn_captions.py:
brand-accent (default): the new brand's accent color from
visual-identity.md.
outline-white: white text + black outline only; safest for
brand-bright paid-social (Hume / Ladder lime). See
[[feedback_caption_outline_only_for_paid_social]].
source-color: keep the source's caption color verbatim.
-
Lipsync failure debugging. When a fal lipsync batch fails with
truncated RuntimeError: fal subscribe failed for ..., don't iterate
on the orchestrator — run ONE clip directly via the atom's
generate.py to see the full chained FalClientHTTPError. See
[[feedback_fal_subscribe_error_envelope]]. The common chained causes:
image > 10MB (fal cap, fix with sips -Z 1536), LFS pointer not
materialized, wallet exhausted, rate limit at high concurrency.
Output
./remix/source-sample.json — the fetched payload.
./remix/remix-plan.json — keep/swap decisions + atom-skill order + routed template.
- A rewritten script for the new brand.
- The new ad master (produced by the routed production template), optionally re-uploaded to the library.
Failure modes
- No GET-by-id → expected;
fetch_sample.py filters the list. If --title matches >1, it asks for --id.
- Sample has no remix payload (
recipe/remix_spec empty) → it was a bare showcase upload; you can still mimic from media_url by watching it, but there's no structured recipe to keep — tell the user.
- Routed template guess is wrong → override it in
remix-plan.json before generating.
- Source voice doesn't fit the new brand → re-run the plan without
--keep-voices and recast.
- Auth 401 →
GOOSEWORKS_API_TOKEN missing/invalid.
Provider-specific gotchas (each costs ~20-30 min to debug from scratch)
-
Higgsfield CLI: --medias <path> rejected as "should be array, got
string" → the right flag is --start-image <path> (per
higgsfield generate create --help's media-flag list).
higgsfield model get <model> lists medias array as a param but the
CLI translates per-model — --help is authoritative. See
[[feedback_higgsfield_cli_media_flag]].
-
Higgsfield: not_enough_credits mid-batch → switch workspace
first (higgsfield workspace list → set <id> to one with credits;
Gooseworks is usually the right one but verify, balances drift). On
full exhaustion, spillover to create-image-nano-banana-2-fal
($0.08/image, same quality for character variants). See
[[feedback_higgsfield_burst_credit_reserve]] and
[[reference_higgsfield_gooseworks_workspace]].
-
fal subscribe failed with truncated stderr → re-run ONE clip via
the atom's generate.py directly to surface the chained
FalClientHTTPError. See [[feedback_fal_subscribe_error_envelope]].
-
fal upload returns generic error past 10MB → fal caps uploads at
10MB. sips -Z 1536 <file> downscales the longest side to 1536px
(or ffmpeg -vf scale=1536:-2). See [[feedback_fal_10mb_upload_cap]].
-
Groq Whisper 403 / error code: 1010 → Cloudflare blocks default
Python urllib User-Agent. Add User-Agent: <app>/<version> and
Accept: application/json headers on every Groq request. See
[[feedback_groq_whisper_cloudflare_ua]].
-
Whisper transcribes sung music as 🎵 Music Playing 🎵 →
expected whenever audioType === "sung-music". Drive captions from
script.json scene windows instead. See
[[feedback_subtitles_from_whisper_word_level]] Case B.
-
ElevenLabs Music 422 on music_length_ms + composition_plan together
→ drop music_length_ms. Composition plan section durations sum to the
total. See [[reference_elevenlabs_music_api_quirks]].
-
ElevenLabs Music words_timestamps come back empty / null on sung
mixes → expected. The withTimestamps: true flag is unreliable for
atmospheric music. Drive captions from script.json scene windows.
-
Ported driver script's REPO_ROOT resolves wrong → bump
Path(__file__).resolve().parents[N] by 1 under the
clients/<brand>/ad-runs/<run>/working/ layout (or anchor on
.git/CLAUDE.md presence). See
[[feedback_repo_root_parents4_clients_layout]].
-
python not found, only python3 → many macOS / Linux machines
only have python3 on PATH. Invoke scripts as python3 … or set a
shebang #!/usr/bin/env python3.