| name | extract-source-sample |
| description | Given the path to a finished content-goose ad-run folder, extract everything that defines that ad — recipe shot list, VO script, characters, voices, world, atom-skills, master mp4 — and emit a `source-sample.json` in the exact shape the `upload-ad-sample` skill writes to the Goose Ads library. Also links every character and voice to the central character library at `<repo-root>/assets/character-library/` (repo-root derived from the run-dir, not a hardcoded path), and if a character isn't in the library yet, adds it first then links. Use when the user wants to remix one of their existing ads — this skill produces the source JSON that the script-rewriting step and `remix-ad` consume. |
extract-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-ad skill).
- 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 role active_master
points at the master mp4; per-asset provider + metadata.model produce
the atom-skill rows.
HOW_TO_MAKE_THIS_VIDEO.md — gets dumped verbatim into how_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 in
production_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",
"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",
"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":
| | |
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"
}
audioType drives the caption pipeline (spoken-vo → Whisper word-level;
sung-music → script.json scene windows, because Whisper returns 🎵 Music Playing 🎵; mixed → split per segment).
sceneCount drives 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 in script.json.scenes[]:
{ "id": "s01", "shot": "<still filename>", "type": "<shot type>", "speaker": "HER|HIM|null", "duration_sec": <parsed from time field e.g. "0:02-0:05"→3> }.
Sum durations into total_duration_sec.
Add pose_tag ONLY 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), omit pose_tag from the shot.
-
extracted_script — concatenate <who>: <text> per scene, newlines
between.
-
remix_spec.worlds[0] — derive from script.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, …) in
script.json.voices:
key: "her" / "him" (lowercase role)
name: voices.<role>.name
gender: HER→"f", HIM→"m", NB→"nb"
soul_id: null unless the run has a Higgsfield Soul anchor
anchor_asset_id: "asset-char-<key>-base-01" if an anchor PNG exists, else null
anchor_image_url: file:// URL to working/characters/<name>-base.png
(or the first png matching the lowercase name), else null
method: "anchor-ref" if anchor PNG present, else null
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" }
Matching rules (per character in the source):
- Primary key —
voice_id. Search index.json for a row whose
voice_id equals the source character's voice_id. If exactly one match,
that row's key is the catalog id. Done.
- Fallback — name (case-insensitive). If voice_id didn't match, search
for a row whose
name equals the source character's name (case-insensitive).
If exactly one match, that row's key is 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 in index.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.json with the schema
above. Fill in what you can confidently derive — leave the rest null
rather than guessing:
key, name, gender, default_voice — from the source.
ethnicity, age_band, archetype, description — leave null
unless the run's HOW_TO.md or 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.json matching that
character.json's outer fields. Bump total by 1. Keep characters[] 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 edit INDEX.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: true voice. First voice in script order
unless the user passes a different selection.
- camelCase voice settings. Never emit
similarity_boost / use_speaker_boost.
catalog_id is null only when matching genuinely fails AND the
library-extension step also failed (e.g. no anchor PNG to seed
shots/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.json missing → 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 + method stay null, AND library
extension can't proceed (no shots/front.png to copy). Stamp the row's
catalog_id: null and 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.json empty (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 lfs isn'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
updated assets/character-library/index.json if 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.