
Extract Source Sample
FreeEasily extract ad run data for remixing.
Free · Opens the source repo
What Extract Source Sample does
The Extract Source Sample skill is designed for users working with content-goose ad-run folders. It automates the process of extracting essential components of an ad, including the recipe shot list, voice-over scripts, character details, and media files, and compiles them into a structured JSON format. This skill is particularly useful for those looking to remix existing ads, as it prepares the necessary data that can be further manipulated in subsequent steps of the ad creation process.
Upon execution, the skill reads the specified ad-run folder and gathers information from various files, such as script.json, asset-manifest.json, and any character images present. It intelligently handles inconsistencies in folder structures, ensuring that even if some files are missing or empty, the extraction process continues without interruption. The output is a source-sample.json file that mirrors the structure required by the upload-ad-sample skill, facilitating seamless integration into the Goose Ads library.
This skill is particularly suited for developers and designers who frequently work with ad runs and need a reliable way to prepare their projects for remixing. By automating the extraction process, users can save time and reduce errors associated with manual data handling. The skill also ensures that all characters and voices are linked to a central character library, enhancing the organization and accessibility of assets across projects.
In summary, Extract Source Sample is an essential tool for anyone involved in the ad creation process who requires a streamlined method for extracting and preparing ad data for remixing. Its adaptability to varied folder structures and focus on essential components make it a valuable addition to any content-goose workflow.
When to use it
Use this skill when you need to extract the source data from a finished content-goose ad run for remixing purposes.
When not to use it
This skill is not suitable for rewriting scripts or rendering remixed ads; those tasks require separate steps.
What you can build with it
Preparing for Ad Remixing
Use this skill to extract the necessary data from an existing ad run to prepare for a remix.
Organizing Ad Assets
Automate the extraction and organization of ad components to streamline your workflow.
Handling Inconsistent Folder Structures
Utilize this skill to effectively manage and extract data from ad runs with varying folder setups.
How to install Extract Source Sample
View source1. Install with the skills CLI
npx skills add gooseworks-ai/goose-skills/extract-source-sample --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by gooseworks-aiextract-source-sample
This is an agent-executed skill. There are no Python scripts. The agent reads the run folder, builds the JSON, and stamps catalog links by hand. The content-goose run folders aren't always cleanly structured (some have empty production/ JSON, some carry everything in working/) — an agent adapts, a script would brittle out.
When to use
- "Extract the source-sample.json for
<run>." - "Get the upload-sample JSON for this ad so I can remix it."
- "Prep
<run>for remix."
Do NOT use to:
- Rewrite the script for a new brand (that's a separate agent step that consumes this skill's output).
- Render the remix (that's the existing
remix-adskill). - Upload an ad to the library (that's
upload-ad-sample).
Inputs
| Input | Required | Notes |
|---|---|---|
run-dir | yes | Absolute path to a content-goose ad-run folder (e.g. clients/ladder/ad-runs/run-02-podcast-skit). |
out | no | Where to write the JSON. Default: <run-dir>/remix/source-sample.json. |
That's the entire interface.
What the agent must do
1. Read the run
Open each file if it exists; tolerate missing files (most production/*.json
in older runs are empty stubs — fall back to working/):
working/script.json— primary source of truth for scenes, voices, set.production/asset-manifest.json—assets[]with roleactive_masterpoints at the master mp4; per-assetprovider+metadata.modelproduce the atom-skill rows.HOW_TO_MAKE_THIS_VIDEO.md— gets dumped verbatim intohow_to.video-project.json— fallback for title / format when script.json doesn't carry them.finals/*.mp4— fallback for the master mp4 if asset-manifest is empty.working/characters/*.png— anchor portraits per character.working/*.py— driver scripts (render_vo.py,render_variants.py,render_clips.py,stitch.py,build_end_card.py, etc.). These are the source's runnable code; the remix consumer ports them. Capture inproduction_scripts[](step 2 below).
For sources with character-pose stills (any run with a
working/characters/ folder of <character>-<pose>.png files —
podcast-skit, founder-led, testimonial, recreate-ugc, etc.), audit every
PNG with file. Do NOT stop at the base portraits. The recipe shot list
references variant expression PNGs (e.g. brittney-eyebrow-up.png,
brad-phone-up.png) by filename; the consumer assumes they exist on disk
and will spend real money on lipsync calls before discovering they don't.
For sources without character-pose stills (music-video b-roll, abstract
animated, product-only) — skip this audit; variant_assets[] stays empty.
Record per file (when auditing):
{
"file": "brittney-eyebrow-up.png",
"pose_tag": "skeptical-eyebrow", // slug from filename stem
"kind": "real" | "lfs-pointer" | "missing",
"size_bytes": 142336
}
file <path> says PNG image data, … for real binaries and ASCII text
for LFS pointers. A real binary is >10KB in practice; an LFS pointer is
<200 bytes.
Materialize LFS pointers before reading any binary. An LFS pointer is a
tiny (<200 byte) ASCII file beginning with version https://git-lfs.github.com.
If a PNG or mp4 looks like one, run:
cd <run-dir-or-repo-root>
git lfs fetch --include=<relative path>
git lfs checkout <relative path>
before referencing it. If git lfs pull no-ops and the LFS endpoint
returns 404 (objects committed as pointers but never pushed — common on
content-goose), leave the entry as kind: "lfs-pointer" in
variant_assets[]. The consumer will regenerate or scrape; this skill
does NOT fabricate. See [[feedback_lfs_pointer_audit_before_paid_calls]]
and [[feedback_fal_subscribe_error_envelope]] for the downstream cost when
this audit is skipped — Hume run-03 lost ~$3 + 25 min to it.
2. Build source-sample.json
Shape (every key always present, arrays may be empty):
{
"title": "<from script.json or video-project.json>",
"format": "video",
"ratio": "<aspect_ratio from script.json — e.g. 9:16>",
"formatProfile": "podcast-skit-fabricated", // enum, see below
"media_url": "file://<abs path to master mp4>",
"thumbnail_url": null,
"brand": "<derive from path: clients/<brand>/ad-runs/...>",
"tags": [],
"recipe": { "shots": [...], "total_duration_sec": <int> },
"extracted_script": "HER: …\nHIM: …\n…",
"skills_used": ["generate-voiceover", "..."], // atoms only
"skills_source": "measured" | "derived-from-production-scripts" | "inferred-canonical" | "guessed",
"how_to": "<contents of HOW_TO_MAKE_THIS_VIDEO.md or null>",
"production_scripts": [
{ "path": "working/render_vo.py", "role": "voiceover" },
{ "path": "working/render_variants.py", "role": "stills" },
{ "path": "working/render_clips.py", "role": "lipsync" },
{ "path": "working/stitch.py", "role": "stitch" },
{ "path": "working/build_end_card.py", "role": "end_card" }
],
"remix_spec": {
"version": 1,
"skills": [{"slug": "...", "provider": "...", "model": "..."}],
"worlds": [{"key": "...", "name": "...", "set": "...", "lighting": null, "color_grade": null, "reference_image_url": null, "catalog_id": null}],
"characters": [
{
"key": "her",
"name": "Brittney",
"gender": "f",
"soul_id": null,
"anchor_asset_id": "asset-char-her-base-01",
"anchor_image_url": "file://...png",
"method": "anchor-ref",
"description": null,
"catalog_id": "brittney",
"variant_assets": [
{ "file": "brittney-base.png", "pose_tag": "base", "kind": "real", "size_bytes": 1842336 },
{ "file": "brittney-eyebrow-up.png", "pose_tag": "skeptical-eyebrow", "kind": "lfs-pointer", "size_bytes": 132 },
{ "file": "brittney-shrug.png", "pose_tag": "shrug", "kind": "missing", "size_bytes": 0 }
]
}
],
"voices": [{"voice_id": "kPzsL2i3teMYv0FxEYQ6", "voice_name": "Brittney", "provider": "elevenlabs", "settings": {"stability": 0.45, "similarityBoost": 0.78, "style": 0.45, "useSpeakerBoost": true}, "selected": true, "catalog_id": "brittney"}]
}
}
formatProfile — open string, drives downstream pipeline choices
A short slug naming the source's ad format. Open vocabulary — the
content-goose molecule library has ~40 distinct ad formats and growing;
don't try to fit a closed enum. Pick a slug that matches the source's
molecule name (e.g. create-podcast-skit-ad → podcast-skit-fabricated,
create-cinematic-music-video → music-video-sung), or invent a new
short slug when none fits.
The consumer (remix-script, remix-ad) routes on the slug. Two routing
properties downstream cares about — record them alongside the profile so
the consumer doesn't have to re-derive:
"formatProfile": "podcast-skit-fabricated",
"formatProfileProperties": {
"audioType": "spoken-vo" | "sung-music" | "mixed",
"sceneCount": "flexible" | "locked-to-source"
}
audioTypedrives the caption pipeline (spoken-vo → Whisper word-level; sung-music → script.json scene windows, because Whisper returns🎵 Music Playing 🎵; mixed → split per segment).sceneCountdrives whether the remix can flex (most spoken formats) or must lock 1:1 (sung-music, anywhere lyric meter sets timing).
If you can't confidently assign either property, leave it null — the
consumer surfaces to the user rather than guessing.
Known slugs from past runs (extend as new formats appear):
| Slug | audioType | sceneCount | Origin |
|---|---|---|---|
podcast-skit-fabricated | spoken-vo | flexible | Ladder run-02 (HER/HIM, 22 scenes, talking-head + broll) |
music-video-sung | sung-music | locked-to-source | Loóna run-01 (single VOCAL track, 14 lyric-locked beats) |
The two known slugs are what real retros produced. Add new rows here when you extract a source that fits a new molecule (single-host-ugc, animated- explainer-villain, stop-motion-tabletop, goose-vs-tool, hook-variant, etc.). Don't pre-invent slugs that haven't shipped yet.
Per-section derivation:
-
recipe.shots[]— one shot per scene inscript.json.scenes[]:{ "id": "s01", "shot": "<still filename>", "type": "<shot type>", "speaker": "HER|HIM|null", "duration_sec": <parsed fromtimefield e.g. "0:02-0:05"→3> }. Sum durations intototal_duration_sec.Add
pose_tagONLY when the filename matches<character>-<pose>.png(the character-pose convention used by podcast-skit and other character-led formats). Derive it by stripping the character prefix from the filename stem (e.g.brittney-eyebrow-up.png→pose_tag: "eyebrow-up",brad-phone-up.png→pose_tag: "phone-up"). For formats whose shots aren't keyed to character poses (music-video b-roll, product hyperframes, abstract animated scenes), omitpose_tagfrom the shot. -
extracted_script— concatenate<who>: <text>per scene, newlines between. -
remix_spec.worlds[0]— derive fromscript.json.set_description:{ key: <slug of the run's setting label>, name: <human label>, set: <full set_description>, lighting: null, color_grade: null, reference_image_url: null, catalog_id: null }. -
remix_spec.characters[]— one per voice role (HER, HIM, …) inscript.json.voices:key:"her"/"him"(lowercase role)name:voices.<role>.namegender: HER→"f", HIM→"m", NB→"nb"soul_id: null unless the run has a Higgsfield Soul anchoranchor_asset_id:"asset-char-<key>-base-01"if an anchor PNG exists, else nullanchor_image_url:file://URL toworking/characters/<name>-base.png(or the first png matching the lowercase name), else nullmethod:"anchor-ref"if anchor PNG present, else nulldescription: null unless surfaced in the run's how-tocatalog_id: stamped in step 3.variant_assets[]: emit ONLY when the source uses character-pose stills (the<character>-<pose>.pngfilename convention). One entry per PNG inworking/characters/whose filename starts with the character's lowercase name. Each entry{file, pose_tag, kind: real|lfs-pointer|missing, size_bytes}— derived from thefileaudit in step 1. This is the canary the consumer needs to decide whether to regenerate variants before paid lipsync calls. Include the base entry too (pose_tag: "base"). For formats without per-character pose stills (music-video, animated, product-only), setvariant_assets: []or omit the key.
-
remix_spec.voices[]— one per voice inscript.json.voices:voice_id,voice_name(fromname),provider: "elevenlabs"settings: copy from script'ssettings, rename to camelCase (similarity_boost→similarityBoost,use_speaker_boost→useSpeakerBoost)selected:truefor the first voice in script order,falseotherwise — exactly oneselected: true.catalog_id: stamped in step 3.
-
remix_spec.skills[]— derive atom rows fromproduction/asset-manifest.json.assets[]: each asset'sprovider+metadata.model(+skillormetadata.skillfor the slug) produces one row, deduped. Drop molecule slugs — only atoms allowed. The canonical atom inventory is:generate-voiceover, generate-character-image, generate-broll-shot, generate-lipsync, generate-music, compose-master, burn-captions, add-captions-klap, render-hyperframe, stitch-clips, build-end-cardWhen the asset-manifest is empty (common in older runs), derive atoms generically — don't hard-code per-format recipes. The repo has ~40 ad-format molecules and growing; canonical recipes drift fast. Use this cascade:
-
Read
production_scripts[](next section) — each driver script's actual provider calls are authoritative. Openrender_vo.py, grep forelevenlabs/fal/higgsfieldimports + endpoint URLs, and derive one atom row per provider × model the script actually invokes. This is more reliable than any guessed recipe because it reflects what the source ACTUALLY did, not what the format usually does. -
If
production_scripts[]is also empty, fall back to thepodcast-skit-fabricatedcanonical recipe ONLY whenformatProfile === "podcast-skit-fabricated"— the one format with enough run data to canonicalize:[ {"slug": "generate-voiceover", "provider": "elevenlabs", "model": null}, {"slug": "generate-character-image", "provider": "higgsfield", "model": null}, {"slug": "generate-lipsync", "provider": "fal", "model": null}, {"slug": "compose-master", "provider": "ffmpeg", "model": null}, {"slug": "burn-captions", "provider": "ffmpeg", "model": "libass"} ]Note
model: null— model ids drift (eleven_multilingual_v2 → eleven_multilingual_v3, veed/fabric-1.0 → veed/fabric-2.0); don't freeze them in the SKILL. -
For any other formatProfile with neither asset-manifest nor production_scripts, surface to the user and ask which atoms ran. Don't invent —
skills_source: "guessed"is worse thannull.
Mirror
skills_usedas the flat slug list. -
-
skills_source— top-level field recording how the atom list was obtained, so the consumer knows how much to trust it:"measured"— derived from a populatedasset-manifest.json(cascade step would have used the real provider/model fields)."derived-from-production-scripts"— grepped from the source'sworking/*.pydriver scripts (cascade step 1). Reliable: reflects actual API calls."inferred-canonical"— fell back to the canonical podcast-skit recipe (cascade step 2). Only valid whenformatProfile === "podcast-skit-fabricated"."guessed"— none of the above worked and the user supplied the list. Should be rare; surface in the summary.
The consumer reads this field. Without it, a guessed atom list propagates downstream as if it were measured (Ladder extract retro flagged this — skills_source=inferred quietly made it into Hume's remix-plan as fact). When
skills_source !== "measured", the consumer should cross-check againstproduction_scripts[]before trusting any individual row. -
production_scripts[]— list everyworking/*.pyfile in the run with{path, role}. Roles:voiceover | stills | variants | lipsync | stitch | end_card | music | composites | other. Match by filename:Filename Role render_vo.py,gen_vo.pyvoiceover render_keyframes.py,gen_keyframes.pystills render_variants.pyvariants render_clips.pylipsync stitch.py,compose.py,compose_master.pystitch build_end_card.pyend_card gen_music.pymusic burn_captions.py,make_subtitles.py(none — these are atom-level scripts) build_composites.pycomposites anything else other These are the source's runnable code. The consumer ports them as the starting template — molecule SKILL.mds are recipes, not executables.
3. Link characters + voices to the character library — and add any that are missing
Library location: <repo-root>/assets/character-library/, where <repo-root>
is the content-goose checkout that contains the run-dir (derive it by walking up
from run-dir to the directory that holds assets/ — do not hardcode an
absolute machine path, and never write outside this repo).
Layout:
character-library/
├── index.json ← machine-readable catalog
├── INDEX.md ← human-readable table (hand-curated)
└── <key>/
├── character.json
└── shots/
└── front.png
index.json schema (one row per character):
{
"key": "brittney",
"name": "Brittney",
"gender": "f",
"ethnicity": "white",
"age_band": "20s",
"archetype": "podcast-skeptic-host",
"voice": "Brittney",
"voice_id": "kPzsL2i3teMYv0FxEYQ6",
"default_shot": "brittney/shots/front.png",
"shots": ["front"],
"source": "reuse"
}
character.json schema (per character folder — match an existing one
verbatim; e.g. brittney/character.json):
{
"key": "brittney",
"name": "Brittney",
"gender": "f",
"ethnicity": "white",
"age_band": "20s",
"archetype": "podcast-skeptic-host",
"description": "...",
"source": "reuse",
"origin_anchor_path": "clients/ladder/ad-runs/run-02-podcast-skit/working/characters/brittney-base.png",
"generation_prompt": null,
"default_voice": { "name": "Brittney", "voice_id": "kPzsL2i3teMYv0FxEYQ6", "provider": "elevenlabs" },
"note": null,
"shots": [{"angle": "front", "path": "shots/front.png", "is_default": true}],
"default_shot": "shots/front.png"
}
Matching rules (per character in the source):
- Primary key —
voice_id. Searchindex.jsonfor a row whosevoice_idequals the source character's voice_id. If exactly one match, that row'skeyis the catalog id. Done. - Fallback — name (case-insensitive). If voice_id didn't match, search
for a row whose
nameequals the source character's name (case-insensitive). If exactly one match, that row'skeyis the catalog id. - No match → extend the library, then link. See below.
When a match is found, stamp catalog_id on BOTH the character row AND the
voices[] row that shares the same voice_id.
Extending the library (no-match path):
- Pick a key: lowercase the name, replace non-alphanumeric with
-, strip. If the key already exists inindex.json, append-2,-3, etc. - Create
assets/character-library/<key>/shots/. - Copy the source anchor PNG to
assets/character-library/<key>/shots/front.png. Materialize the source PNG first if it's an LFS pointer (see step 1). - Write
assets/character-library/<key>/character.jsonwith the schema above. Fill in what you can confidently derive — leave the restnullrather than guessing:key,name,gender,default_voice— from the source.ethnicity,age_band,archetype,description— leavenullunless the run'sHOW_TO.mdor character description explicitly states them.source:"reuse"(we're pulling from an existing run, not generating fresh).origin_anchor_path: the source PNG's path relative to the content-goose repo root (e.g.clients/ladder/ad-runs/run-02-podcast-skit/working/characters/brittney-base.png).generation_prompt:null.note:null.shots:[{"angle": "front", "path": "shots/front.png", "is_default": true}].default_shot:"shots/front.png".
- Append a row to
assets/character-library/index.jsonmatching that character.json's outer fields. Bumptotalby 1. Keepcharacters[]in the existing order — append at the end. - Tell the user:
INDEX.md is hand-curated; refresh it manually or run the library indexer if there is one.Do NOT editINDEX.md. - Now stamp
catalog_id: "<new-key>"on the source-sample.json's character row + the voice row sharing that voice_id.
Refuse to fabricate library fields. If you don't know a character's ethnicity / age band / archetype, write
null. A wrong guess pollutes future remixes — the user prefers a null they can fill in over a confident wrong value.
4. Write the output + summary
Default output path: <run-dir>/remix/source-sample.json (create the
remix/ folder if it doesn't exist; do not touch anything else in the run
folder).
Print a summary:
extracted source-sample at: <out path>
title: <title>
brand: <brand>
formatProfile: <profile>
recipe: <N> shots, <total_duration_sec>s
remix_spec: <S> skills (<measured|inferred>), <W> worlds, <C> chars, <V> voices
variant audit: <X> real / <Y> lfs-pointer / <Z> missing across <C> chars
production_scripts: <K> scripts ({voiceover, lipsync, stitch, end_card, …})
catalog links:
characters: her=brittney, him=brad
voices: kPzsL2i3teMYv0FxEYQ6=brittney, T4x5CtnhOiichhcqFzgg=brad
library extensions: <none | <key> (new)>
If any catalog link is null, surface that too — the user wants to know
what didn't link. If variant audit shows any LFS pointers or missing
PNGs, lead with that in the summary — it's the single biggest cost
multiplier for the downstream remix if missed.
Decision rules
- Agent-executed; no scripts. The run folders aren't perfectly structured — adapt to what's actually present rather than imposing a rigid extractor.
- Atoms only in
remix_spec.skills+skills_used. Drop molecule slugs silently; surface a note if you couldn't recover at least one atom. - Exactly one
selected: truevoice. First voice in script order unless the user passes a different selection. - camelCase voice settings. Never emit
similarity_boost/use_speaker_boost. catalog_idis null only when matching genuinely fails AND the library-extension step also failed (e.g. no anchor PNG to seedshots/front.png). Otherwise every character + voice should end up linked.- Materialize LFS pointers before reading binaries or copying them into the library.
- Don't touch
INDEX.md. It's hand-curated; tell the user to refresh it. - Don't touch anything else in the run folder. This skill is read-only
on the source run, write-only on
<run-dir>/remix/source-sample.json+ the character library.
Failure modes
working/script.jsonmissing → can't extract; ask the user where the script is or refuse.- No anchor PNG for a character → the character row's
anchor_image_url+anchor_asset_id+methodstay null, AND library extension can't proceed (noshots/front.pngto copy). Stamp the row'scatalog_id: nulland tell the user in the summary. - Multiple library matches on voice_id → very rare; surface both keys and ask which one to link to.
production/asset-manifest.jsonempty (common in older runs) → fall back to the canonical podcast-skit atom list; flag in the summary that skills were inferred rather than read.- LFS pointer for an anchor PNG and
git lfsisn't installed or the repo isn't an LFS clone → surface the error; don't copy the pointer bytes into the library.
Output
<run-dir>/remix/source-sample.json— the upload-sample-shape JSON.- Optionally, new folder(s) under
assets/character-library/<key>/and updatedassets/character-library/index.jsonif any source character wasn't in the library yet.
The output JSON is what the next agent step (script rewrite / character
swap) and remix-ad consume.
Frequently asked questions about Extract Source Sample
Similar skills
Spring Boot Testing
Master testing techniques for Spring Boot 4 applications.
GitHub Issues
Manage GitHub issues efficiently with MCP tools.
Geofeed Tuner
Optimize your IP geolocation feeds in CSV format.
Batch Files
Master Windows batch scripting for automation and task management.
Adobe Illustrator Scripting
Automate your Illustrator workflows with ExtendScript.
Plugin Structure
Create and organize Claude Code plugins effectively.
