- 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):
```jsonc
{
"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:
```bash
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):
```jsonc
{
"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:
```jsonc
"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
- `description`: null unless surfaced in the run's how-to
- `catalog_id`: stamped in step 3.
- **`variant_assets[]`**: emit ONLY when the source uses character-pose
stills (the `<character>-<pose>.png` filename convention). One entry
per PNG in `working/characters/` whose filename starts with the
character's lowercase name. Each entry `{file, pose_tag, kind:
real|lfs-pointer|missing, size_bytes}` — derived from the `file`
audit 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),
set `variant_assets: []` or omit the key.
- **`remix_spec.voices[]`** — one per voice in `script.json.voices`:
- `voice_id`, `voice_name` (from `name`), `provider: "elevenlabs"`
- `settings`: copy from script's `settings`, **rename to camelCase**
View on GitHub