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 # python-dotenv (fetch/plan are stdlib)
cp .env.example .env # set GOOSEWORKS_API_TOKEN (+ GOOSEWORKS_API_BASE_URL for local)
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:
- Local-chain (the common case in content-goose). The
extract-source-sampleskill has already written<source-run-dir>/remix/source-sample.jsonto disk. Read it directly. Skipfetch_sample.py— no library round-trip needed. - Library-only. When the source is a sample id (no local run on
disk), fetch from the library:
python scripts/fetch_sample.py --id <sample-id> --out ./remix/source-sample.json # or: --title "hemp serum"
Review the source's printed summary or the JSON itself: recipe shots,
formatProfile, atom-skills, voices/worlds/characters,variant_assets[]audit results,production_scripts[]. - Local-chain (the common case in content-goose). The
-
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.mdexists, 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/andclients/hume-health/are the same brand), run thebrand-researchskill first and pass itsbrand_dir.Brand-pack-current-SKU wins over the user's one-liner. If the user says "can on nightstand" but
01-company-overview.mdflags 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.jsonThe 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_scriptandrecipeas 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_templateskill, 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
/watchthe 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-sampleskill. Keepskills_used/remix_spec.skillsatom-level so the next remix stays clean.
Decision rules
-
Keep the format + beat arc; honor the source's
sceneCountproperty. Readsource-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.nullor 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 whenroute_to_template = (confirm-with-user): port the source run'sworking/*.pyscripts and adapt brand-specific paths. The molecule SKILL.md is a recipe, not executable; the source's driver scripts are the closest runnable template. Readsource-sample.json.production_scripts[]to find them.Common adaptations when porting:
REPO_ROOTpath depth. Older drivers usedPath(__file__).resolve().parents[3]; the currentclients/<brand>/ad-runs/<run>/working/layout needsparents[4]. Better: anchor on.git/CLAUDE.mdpresence. 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-styleflag (see below). pythonvspython3shebangs — many machines only havepython3.
-
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 everykind: "lfs-pointer"orkind: "missing":- Try
git lfs pull --include=<path>andgit lfs checkout. - If
git lfs ls-filesreturns the pointer but the LFS endpoint returns 404 (objects committed as pointers but never pushed), regenerate via the source's image atom (generate-character-imagevia Higgsfield nano_banana_pro; spillover tocreate-image-nano-banana-2-falat $0.08/image onnot_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) —
fileevery one before the end-card build. If LFS-pointer-only ANDgit lfs pulldoesn't materialize, fetch from the brand's public CDN (URLs inclients/<brand>/brand-research/asset-urls.md). Stage replacements under<run-dir>/working/end-card-assets/so the build script doesn't depend onbrand-assets/.Surface the count of regenerated variants and replaced brand assets in the plan summary.
- Try
-
Caption pipeline is audio-type-dependent. Read
source-sample.json.formatProfileProperties.audioType:"sung-music"→ drive captions fromscript.jsonscene windows (voper 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-styleflag — standardize the three known-good options to avoid the per-run AskUserQuestion. Pass onstitch.py/burn_captions.py:brand-accent(default): the new brand's accent color fromvisual-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'sgenerate.pyto see the full chainedFalClientHTTPError. See [[feedback_fal_subscribe_error_envelope]]. The common chained causes: image > 10MB (fal cap, fix withsips -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.pyfilters the list. If--titlematches >1, it asks for--id. - Sample has no remix payload (
recipe/remix_specempty) → it was a bare showcase upload; you can still mimic frommedia_urlby watching it, but there's no structured recipe to keep — tell the user. - Routed template guess is wrong → override it in
remix-plan.jsonbefore generating. - Source voice doesn't fit the new brand → re-run the plan without
--keep-voicesand recast. - Auth 401 →
GOOSEWORKS_API_TOKENmissing/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>(perhiggsfield generate create --help's media-flag list).higgsfield model get <model>listsmedias arrayas a param but the CLI translates per-model —--helpis authoritative. See [[feedback_higgsfield_cli_media_flag]]. -
Higgsfield:
not_enough_creditsmid-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 tocreate-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 failedwith truncated stderr → re-run ONE clip via the atom'sgenerate.pydirectly to surface the chainedFalClientHTTPError. 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 (orffmpeg -vf scale=1536:-2). See [[feedback_fal_10mb_upload_cap]]. -
Groq Whisper
403 / error code: 1010→ Cloudflare blocks default Python urllib User-Agent. AddUser-Agent: <app>/<version>andAccept: application/jsonheaders on every Groq request. See [[feedback_groq_whisper_cloudflare_ua]]. -
Whisper transcribes sung music as
🎵 Music Playing 🎵→ expected wheneveraudioType === "sung-music". Drive captions fromscript.jsonscene windows instead. See [[feedback_subtitles_from_whisper_word_level]] Case B. -
ElevenLabs Music 422 on
music_length_ms+composition_plantogether → dropmusic_length_ms. Composition plan section durations sum to the total. See [[reference_elevenlabs_music_api_quirks]]. -
ElevenLabs Music
words_timestampscome back empty / null on sung mixes → expected. ThewithTimestamps: trueflag is unreliable for atmospheric music. Drive captions fromscript.jsonscene windows. -
Ported driver script's
REPO_ROOTresolves wrong → bumpPath(__file__).resolve().parents[N]by 1 under theclients/<brand>/ad-runs/<run>/working/layout (or anchor on.git/CLAUDE.mdpresence). See [[feedback_repo_root_parents4_clients_layout]]. -
pythonnot found, onlypython3→ many macOS / Linux machines only havepython3on PATH. Invoke scripts aspython3 …or set a shebang#!/usr/bin/env python3.