gooseworks 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +27 -0
- package/.claude-plugin/plugin.json +3 -0
- package/README.md +5 -4
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +3 -2
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +4 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/skills/master-skill.d.ts +22 -9
- package/dist/skills/master-skill.d.ts.map +1 -1
- package/dist/skills/master-skill.js +346 -19
- package/dist/skills/master-skill.js.map +1 -1
- package/dist/skills/routes.d.ts.map +1 -1
- package/dist/skills/routes.js +7 -1
- package/dist/skills/routes.js.map +1 -1
- package/package.json +1 -1
- package/skills/goose-video/SKILL.md +300 -433
- package/skills/goose-video-local/SKILL.md +440 -0
- package/skills/gooseworks/SKILL.md +2 -1
- package/skills/routes.json +8 -1
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: goose-video-local
|
|
3
|
+
slug: goose-video-local
|
|
4
|
+
description: >
|
|
5
|
+
Render an EXISTING GooseWorks video ad project or video batch on this machine (Playwright +
|
|
6
|
+
ffmpeg + GooseWorks media proxies) and save the finished MP4 back to the project over MCP. Use
|
|
7
|
+
when the app's "copy for Claude" command names goose-video-local, for "make the video for
|
|
8
|
+
project <id>" / "for video batch <id>", or to remix a video ad template locally. Needs a machine
|
|
9
|
+
with network egress and ffmpeg (local Claude Code or the desktop app), not a hosted connector.
|
|
10
|
+
To order a NEW video ad in chat, use goose-video instead.
|
|
11
|
+
category: ads
|
|
12
|
+
version: 0.3.0
|
|
13
|
+
author: GooseWorks
|
|
14
|
+
tags: [gooseworks, ads, video, remix, imessage, local-render, byoa]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# GooseWorks Video Ads — local remix runtime
|
|
18
|
+
|
|
19
|
+
You produce **video** ad creative on the user's OWN machine and sync the result back to the
|
|
20
|
+
GooseWorks app over MCP. This document is the **runtime contract** (auth, credits, the media
|
|
21
|
+
proxies, data I/O, the review gate). A separate **recipe skill** — fetched per format — tells you
|
|
22
|
+
*what to make* (the pieces, prompts, models, order of assembly).
|
|
23
|
+
|
|
24
|
+
**Division of authority — read both, but when they disagree THIS doc wins on the environment AND the
|
|
25
|
+
review/approval flow.** The recipe governs WHAT to make; this doc governs WHEN you pause, generate,
|
|
26
|
+
and spend. In particular: a recipe may spell out a **multi-phase, multi-gate** flow — "generate the
|
|
27
|
+
still [GATE] → approve → author the prompt [GATE] → approve → render [GATE] → approve", several
|
|
28
|
+
separate pauses. **Do NOT run it that way.** Collapse every one of those gates into the single
|
|
29
|
+
**review-once** flow below: one review set, one approval (Step 3). Take the recipe's pieces, prompts
|
|
30
|
+
and models; ignore its intermediate pauses. This is the exact contradiction that confused past runs
|
|
31
|
+
(GOOSE-2542) — there is no ambiguity: review-once wins.
|
|
32
|
+
|
|
33
|
+
You run inside the user's own Claude Code session (they pasted an instruction with a project
|
|
34
|
+
id). The app NEVER runs you — it is the viewer + review surface; you are the renderer.
|
|
35
|
+
|
|
36
|
+
## CLI-free environments (cowork / headless)
|
|
37
|
+
|
|
38
|
+
You may be running WITHOUT the `gooseworks` CLI binary (e.g. Anthropic cowork). The
|
|
39
|
+
`mcp__gooseworks__*` tools work over the MCP connection regardless, so wherever this skill
|
|
40
|
+
says to shell out, use the MCP equivalent:
|
|
41
|
+
|
|
42
|
+
- `gooseworks fetch <slug>` → the **`fetch_skill`** MCP tool (returns the same content/scripts/
|
|
43
|
+
files/dependencySkills). `gooseworks search <q>` → **`search_skills`**.
|
|
44
|
+
- `gooseworks credits` → the **`get_ad_credits`** MCP tool.
|
|
45
|
+
- `gooseworks doctor` → do the manual toolchain check in the preflight below.
|
|
46
|
+
|
|
47
|
+
## Report problems so we can fix them (telemetry — do this, don't skip it)
|
|
48
|
+
|
|
49
|
+
If anything blocks or degrades this run — a media/proxy call fails or errors, a required input or
|
|
50
|
+
asset is missing, a recipe instruction is ambiguous or contradictory, the render toolchain won't set
|
|
51
|
+
up, or you hit a bug you can't work around — **report it** so the team gets visibility and can fix
|
|
52
|
+
the skill. It's fire-and-forget, never counts against you, and never blocks your work.
|
|
53
|
+
|
|
54
|
+
- **First, set a stable run id** so every event (yours + the auto-logged media calls) groups together:
|
|
55
|
+
`export GW_RUN_ID="vid-<project_or_batch_id>"` (and `export GW_SKILL="<recipe-slug>"`) in the
|
|
56
|
+
shell you render from. The media proxies read `GW_RUN_ID` automatically.
|
|
57
|
+
- **CLI present →** `gooseworks log "<what happened>" --event-type <type> --level error --details '{"error":"...","step":"...","model":"..."}'`
|
|
58
|
+
- **No CLI (cowork / headless) →** the **`log_cli_event`** MCP tool with the same fields (pass `run_id`).
|
|
59
|
+
- `--event-type`: `api_failure` (a proxy/model call failed) · `missing_input` · `blocker` ·
|
|
60
|
+
`confusion` (unclear/contradictory instruction) · `error` (a bug) · `step`/`info` (progress notes).
|
|
61
|
+
- Put the **real error text + the step you were on** in `--details`. Paid FAL/ElevenLabs calls
|
|
62
|
+
ALREADY auto-log their own failures, so focus your manual logs on what the proxy can't see:
|
|
63
|
+
missing inputs, confusing/contradictory recipe instructions, toolchain/setup failures, and bugs.
|
|
64
|
+
- Logging is FOR US — it does not replace telling the user. When a problem blocks the run, still
|
|
65
|
+
explain it to the user (and ask if you need a decision); just also `log` it so we can fix the skill.
|
|
66
|
+
|
|
67
|
+
## Prerequisite — MCP + a render toolchain (Phase 0 preflight)
|
|
68
|
+
|
|
69
|
+
- The `mcp__gooseworks__*` tools are REQUIRED. If they're unavailable, stop and tell the user
|
|
70
|
+
to connect the GooseWorks MCP server (or run `gooseworks install --claude --mcp` on the CLI)
|
|
71
|
+
and restart. There is no REST fallback.
|
|
72
|
+
- **The render runs wherever THIS agent runs, and it needs a real toolchain: `ffmpeg` +
|
|
73
|
+
`ffprobe` + a Playwright **Chromium**.** Establish it in this priority order, and do NOT start
|
|
74
|
+
rendering until one is confirmed:
|
|
75
|
+
1. **CLI present →** run `gooseworks doctor` (checks login, MCP, ffmpeg/ffprobe, Playwright
|
|
76
|
+
Chromium in one shot). Fix any ✗ with the command it prints, then continue.
|
|
77
|
+
2. **No CLI →** check the toolchain yourself: `ffmpeg -version`, `ffprobe -version`, and a
|
|
78
|
+
Playwright Chromium probe (`npx playwright --version` and, if needed, `npx playwright install
|
|
79
|
+
chromium`). If all resolve, continue.
|
|
80
|
+
3. **Docker available →** this is the most reliable way to get the toolchain in a sandbox that
|
|
81
|
+
lacks it: run the render steps inside the prebuilt image
|
|
82
|
+
**`ghcr.io/gooseworks-ai/goose-video-render`** (ffmpeg + ffprobe + Playwright Chromium baked
|
|
83
|
+
in), mounting the project working directory. Use Docker whenever the host is missing ffmpeg or
|
|
84
|
+
Chromium and `docker` is on PATH. (Note: nested Docker is usually disabled inside managed
|
|
85
|
+
sandboxes like cowork — treat this as an option, not a guarantee.)
|
|
86
|
+
4. **None of the above works →** STOP and tell the user plainly, e.g.: *"Video rendering needs
|
|
87
|
+
ffmpeg + a Playwright Chromium (or Docker) on the machine running this agent. This environment
|
|
88
|
+
doesn't have them and I can't install them here. Options: (a) enable/allow Docker so I can use
|
|
89
|
+
the goose-video-render image, (b) install ffmpeg + `npx playwright install chromium`, or
|
|
90
|
+
(c) run this skill locally in your own Claude Code where the toolchain is available."* Do not
|
|
91
|
+
half-render or fake a result. Static image ads (the `goose-ads` skill) do NOT need any of this
|
|
92
|
+
and work anywhere — offer that as the fallback if they just want an ad now.
|
|
93
|
+
|
|
94
|
+
## Identity, token, credits
|
|
95
|
+
|
|
96
|
+
- Read `~/.gooseworks/credentials.json` → `api_key` (your agent token), `api_base`, `agent_id`.
|
|
97
|
+
Never print the token.
|
|
98
|
+
- **CRITICAL — target the org-default Ads agent on EVERY file op.** The app serves project files
|
|
99
|
+
(the render-file route) from the org's DEFAULT agent, but MCP file writes default to your
|
|
100
|
+
token's pinned agent — which can be a DIFFERENT agent, so a render written with the default
|
|
101
|
+
scope is **invisible in the app**. First resolve the Ads agent: `list_accessible_scopes` → the
|
|
102
|
+
scope with `is_org_default: true` (the ORG default — NOT the `is_default` / `default_agent_id`
|
|
103
|
+
fields, which are the *user's* default agent and are often a DIFFERENT agent). Its `agent_id` is
|
|
104
|
+
`ADS_AGENT` (name "Ads agent", slug `org-default`; usually also the `agent_id` in
|
|
105
|
+
credentials.json). Then pass `target: { type: "agent", agent_id: ADS_AGENT }` on EVERY
|
|
106
|
+
`get_upload_url` / `get_download_url` / `write_file` / `list_directory` / `read_file` — NEVER
|
|
107
|
+
omit `target`.
|
|
108
|
+
- **CRITICAL — publish under the PROJECT FOLDER, not the workspace root (the #1 "video renders but
|
|
109
|
+
is invisible" bug).** `get_upload_url` stores at `<ADS_AGENT>/files/<path>` verbatim, but the
|
|
110
|
+
render-file route reads from
|
|
111
|
+
`<ADS_AGENT>/files/agent-config/brands/<brand_slug>/projects/<project_id>/<path>`
|
|
112
|
+
(see backend `resolveProjectFileKey`). So EVERY publish/preview `path` MUST be prefixed with
|
|
113
|
+
`agent-config/brands/<brand_slug>/projects/<project_id>/` — e.g. upload to
|
|
114
|
+
`agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4`, NEVER bare
|
|
115
|
+
`working/final.mp4`. A bare path 404s in the app even though the render row AND a bare-path
|
|
116
|
+
`get_download_url` both "succeed" (they resolve the wrong key). The render `output_url` still
|
|
117
|
+
stays the project-relative `...render-file?path=working/final.mp4` — the route re-prepends the
|
|
118
|
+
prefix itself. Always verify with `get_download_url` on the FULL `agent-config/...` path (must
|
|
119
|
+
be non-empty; curl it for HTTP 200) BEFORE marking the render complete.
|
|
120
|
+
- Media generation (FAL / ElevenLabs) through the GooseWorks proxies is the **REAL spend** — billed
|
|
121
|
+
to the agent per call as you generate (Step 4). `submit_render { kind: "full" }` additionally
|
|
122
|
+
debits **1 nominal ad credit when the render ROW is opened** (a bookkeeping fee, NOT the render's
|
|
123
|
+
true cost) — so open it only once you actually have a rendered master (Step 4.1/4.2), and never
|
|
124
|
+
re-submit on a guess (that double-bills). The final-video QC gate (Step 4.3) then sits between
|
|
125
|
+
that master and PINNING it. Call `get_ad_credits` first; the user can check `gooseworks credits`.
|
|
126
|
+
|
|
127
|
+
## Step 0 — project id, or video BATCH id? (fan out before anything else)
|
|
128
|
+
|
|
129
|
+
The handoff you were pasted is EITHER a single `project <id>` OR a `video batch <id>`. A batch is
|
|
130
|
+
the app's "N concepts" flow: one composer submission fans out into **N independent concept projects**
|
|
131
|
+
(the user picked a concept count, default 3), and the app expects EACH to be rendered. **Handle both:**
|
|
132
|
+
|
|
133
|
+
- **`project <id>`** → you have one project. Treat it as a batch of one and continue to Step 1.
|
|
134
|
+
- **`video batch <id>`** → call `get_ad_video_batch { video_batch_id }`. It returns every child
|
|
135
|
+
concept under `projects[]` — each is a normal project with its own `id`, `variant_index`
|
|
136
|
+
(Concept 1..N), and its own `creative_brief` (the per-concept angle/hook/offer/message). **You
|
|
137
|
+
MUST process every concept, not just the first** — dropping concepts 2..N is the #1 batch bug.
|
|
138
|
+
|
|
139
|
+
**Loop shape (one agent, sequential, ONE approval for the whole batch):**
|
|
140
|
+
1. Run **Step 1 + Step 1.5 + Step 2 + Step 3-assemble** for EACH concept project (each has its own
|
|
141
|
+
`project_id`, brief, and `working/` folder — never cross-write between concepts).
|
|
142
|
+
2. Mirror EVERY concept's review set (Step 3's `update_ad_project_script` per project), then stop
|
|
143
|
+
for **ONE** approval that covers all concepts — show the per-concept credit estimate and the
|
|
144
|
+
batch total. Set the batch to `review` (`update_ad_video_batch { status: "review" }`).
|
|
145
|
+
3. On approval, set the batch to `rendering` and run **Step 4 (the expensive render)** for each
|
|
146
|
+
concept **sequentially** (finish Concept 1's master before starting Concept 2 — one machine can't
|
|
147
|
+
render them in parallel). Deliver each (Step 5). When all concepts are pinned, set the batch to
|
|
148
|
+
`complete`.
|
|
149
|
+
|
|
150
|
+
If a single concept fails, keep going with the rest, mark that concept blocked, and report which
|
|
151
|
+
ones shipped — never abort the whole batch on one bad concept. Everything below (Steps 1–5) is
|
|
152
|
+
written per-project; a batch just runs it N times with the shared approval gate above.
|
|
153
|
+
|
|
154
|
+
## Step 1 — resolve the project, source, brand
|
|
155
|
+
|
|
156
|
+
1. `get_ad_project { project_id }` → keep `brand_id`, `source_sample_id`, `name`, `status`, the
|
|
157
|
+
**top-level** `app_url` + `brand_url` (returned alongside `project`, NOT inside it — the links
|
|
158
|
+
you hand the user for the in-app review in Step 3 and delivery in Step 5), AND the user's
|
|
159
|
+
**`creative_brief`**, project **`assets`**, `character_id`, `default_voice_id` — these are the
|
|
160
|
+
authoritative inputs the user chose in the composer (see Step 1.5). Do NOT discard them.
|
|
161
|
+
|
|
162
|
+
### Step 1.5 — the project brief is AUTHORITATIVE (honor it; don't re-ask)
|
|
163
|
+
|
|
164
|
+
The composer already collected the user's creative direction onto the project. **Read it and treat
|
|
165
|
+
it as ground truth — it OVERRIDES the template recipe's defaults, and it REPLACES the clarifying
|
|
166
|
+
questions you would otherwise ask.** Only fall back to the recipe default (then, last, to asking)
|
|
167
|
+
for a field the brief leaves empty. Map the fields you WILL honor:
|
|
168
|
+
|
|
169
|
+
- `creative_brief.productName` / `.offer` / `.angle` → the product, offer/code, and angle. Do
|
|
170
|
+
**not** ask "which product / what offer / what angle" if these are set.
|
|
171
|
+
- `creative_brief.concept` (on a batch child) → this concept's **`angle` / `hook` / `offer` /
|
|
172
|
+
`message` / `note`** — the per-concept differentiator. Honor it verbatim; it's WHY the user asked
|
|
173
|
+
for N concepts. `angle: "auto"` or empty means "you choose."
|
|
174
|
+
- Project `assets` + `creative_brief.reference_image_urls` → the user's **own reference images**.
|
|
175
|
+
Use them as the product/brand refs (alongside the brand kit), don't ignore them for generic recipe
|
|
176
|
+
assets.
|
|
177
|
+
- `character_id` → the avatar/creator to use. `default_voice_id` → the voice for any VO (put its
|
|
178
|
+
NAME in the review `subtitle`). Use these instead of picking your own.
|
|
179
|
+
- `creative_brief.ratio` / `.durationSeconds` → target aspect ratio + length. Honor when the
|
|
180
|
+
format's render pipeline supports it; if the format physically can't (e.g. a fixed phone-mockup
|
|
181
|
+
aspect), keep the format's native value and note the constraint in the review rather than silently
|
|
182
|
+
ignoring the request.
|
|
183
|
+
- `polish_policy` (`standard` | `extra`) → `extra` means spend the extra pass on QC/polish.
|
|
184
|
+
2. `get_ad_template { template_id: source_sample_id }` → the source video: `media_url`,
|
|
185
|
+
`recipe`, `format` (e.g. "imessage"), `extracted_script`, `how_to`, `remix_spec`.
|
|
186
|
+
3. Brand gate: `get_brand_kit { brand_id }`. If `researchStatus` is `complete`, REUSE it —
|
|
187
|
+
never re-research. If not, run brand research first (`gooseworks fetch brand-research`,
|
|
188
|
+
follow it, then `finalize_brand_research { brand_id }`) before continuing.
|
|
189
|
+
|
|
190
|
+
## Step 2 — read the template's recipe (it carries everything; NO hardcoded format map)
|
|
191
|
+
|
|
192
|
+
The ad format is a **template (data) in the ad_sample DB**, not a per-format skill.
|
|
193
|
+
`get_ad_template(source_sample_id)` returns the template's `recipe` — a self-contained brief you
|
|
194
|
+
read and execute. **Do NOT map `format` to a hardcoded recipe slug** (there is no such table):
|
|
195
|
+
|
|
196
|
+
- `recipe.format` — the format label (e.g. `vignette`), for display only.
|
|
197
|
+
- `recipe.atoms` — the **capabilities** this template composes (e.g. `create-video-seedance-2-fal`,
|
|
198
|
+
`create-image-gpt-image-fal`, `review-ugc-render`, `watch`). `gooseworks fetch <name>` each — they
|
|
199
|
+
live in `skills/ads/capabilities/` and are reused across templates (so they cache).
|
|
200
|
+
- `recipe.instructions` — the **playbook** to follow: `instructions.inline` prose, or
|
|
201
|
+
`instructions.doc_url` (an S3 markdown doc — fetch it).
|
|
202
|
+
- `recipe.config` — every param (prompts, layout, timings, palette, model choices).
|
|
203
|
+
- `recipe.inputs` — the brand-asset contract (which product / logo / offer this template needs).
|
|
204
|
+
- `recipe.assets` — reference material as S3 links (reference render, style guide, example frames) —
|
|
205
|
+
fetch as needed.
|
|
206
|
+
|
|
207
|
+
Runtime: **read the recipe → `gooseworks fetch` each capability in `recipe.atoms` → follow
|
|
208
|
+
`recipe.instructions` with `recipe.config` + the brand's bound `inputs`.** The template IS the recipe;
|
|
209
|
+
there is no `format → recipe-slug` table and no per-format skill to fetch.
|
|
210
|
+
|
|
211
|
+
Save each fetched capability's scripts + files under `/tmp/gooseworks-scripts/<name>/`. If a capability
|
|
212
|
+
is a Node package (a phone-mockup renderer), `npm install` in its folder so its `generate.js` +
|
|
213
|
+
Playwright resolve, and point the recorder's `NODE_PATH` at it.
|
|
214
|
+
|
|
215
|
+
> **Migration note:** older phone-mockup formats (`imessage` / `chatgpt` / `apple-notes`) whose DB
|
|
216
|
+
> recipe does not yet carry `atoms` / `instructions` still hold the legacy `recipe.thread` payload;
|
|
217
|
+
> migrate them to this shape (capabilities + instructions in the DB) — do not reintroduce a CLI map.
|
|
218
|
+
|
|
219
|
+
## Step 3 — assemble the review set, then get ONE approval in the app (before the expensive render)
|
|
220
|
+
|
|
221
|
+
This is a **review-once** flow: put the whole review set in the app, get ONE approval, then run the
|
|
222
|
+
expensive render + any remaining paid work end-to-end. Never spend on the expensive render before
|
|
223
|
+
approval, and don't drip pieces out one at a time and re-pause.
|
|
224
|
+
|
|
225
|
+
**What goes in the review — show the REAL cheap pieces, PROMPT only the expensive render.** Split
|
|
226
|
+
every piece three ways by cost, NOT just "free vs paid":
|
|
227
|
+
- **FREE** (an iMessage / Apple-Notes HTML mockup, a text/CTA line — rendered locally, no proxy
|
|
228
|
+
call) → generate NOW and mirror the real asset.
|
|
229
|
+
- **CHEAP paid** — a single still/image, the creator/avatar frame, the end card, a short voiceover
|
|
230
|
+
or music bed (each costs cents → roughly **≤ 100 credits**) → **generate these NOW too** and
|
|
231
|
+
mirror the real asset. The few credits buy a real review: the user SEES the actual creator face
|
|
232
|
+
and end card and HEARS the VO, instead of judging a prompt. **This OVERRIDES any recipe rule that
|
|
233
|
+
says to gate ALL paid calls** — only the expensive render below is gated.
|
|
234
|
+
- **EXPENSIVE paid** — the video take / final AI render (hundreds of credits) → do NOT generate.
|
|
235
|
+
Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is the ONE thing approved as a
|
|
236
|
+
prompt (you can't preview a hundreds-of-credits video for free); it's generated only in Step 4.
|
|
237
|
+
|
|
238
|
+
**The expensive render's exact prompt must be in the panel BEFORE you ask for approval** — so a
|
|
239
|
+
single "go" runs it (plus any remaining paid work) without re-pausing mid-run.
|
|
240
|
+
|
|
241
|
+
**Show every cost in CREDITS, never dollars.** 1 credit = $0.01 and media generations bill at
|
|
242
|
+
provider-cost × 1.2, so **credits ≈ round-up(provider-$ × 120)** per generation, plus a flat
|
|
243
|
+
**200-credit base per video**. Convert any $ figures to credits and show ONLY credits to the user —
|
|
244
|
+
never print a "$…" amount.
|
|
245
|
+
|
|
246
|
+
**Never assemble/stitch the finished video for review.** The review is of the individual pieces (or
|
|
247
|
+
their prompts) — never a "full cascade" / "approved cut" clip. Building the whole video before
|
|
248
|
+
approval defeats the gate (the user opens the review to an already-finished video) and wastes the
|
|
249
|
+
render (GOOSE-2542). The full video is assembled ONLY in Step 4, after approval. A `video`
|
|
250
|
+
ingredient here is only a genuinely separate SOURCE clip the format needs (e.g. supplied b-roll).
|
|
251
|
+
|
|
252
|
+
1. **Assemble every piece the format needs — not just the script.** Read the recipe for the exact
|
|
253
|
+
list. For an iMessage video that's the **script** (bubble thread), the **conversation image(s)**,
|
|
254
|
+
and the **end card**; richer templates add a hook frame, background, product shots, music bed, a
|
|
255
|
+
creator/avatar, a voiceover… For each piece, decide FREE / CHEAP-paid / EXPENSIVE-paid (above):
|
|
256
|
+
- **FREE or CHEAP paid** (≤ ~100 credits — HTML mockups, a still, the creator frame, the end
|
|
257
|
+
card, a short VO/music bed) → generate it now and `get_upload_url` the asset to the project
|
|
258
|
+
folder `agent-config/brands/<brand_slug>/projects/<project_id>/working/review/<name>` (same
|
|
259
|
+
path-prefix rule as final publish — a bare `working/review/<name>` won't render in the panel);
|
|
260
|
+
set that piece's `path` in `script_drafts` to the project-relative `working/review/<name>`.
|
|
261
|
+
- **EXPENSIVE paid** (the video take / final render, hundreds of credits) → do NOT generate. Put
|
|
262
|
+
the **exact prompt/spec** (and any ref image URLs) in the tile's `text` / `subtitle` so the
|
|
263
|
+
user reviews what will be spent on. No `path` yet — it's generated in Step 4.
|
|
264
|
+
Include the **estimated cost in CREDITS** (never dollars) of the cheap pieces already generated +
|
|
265
|
+
the pending render, so the user approves knowing the total spend. **Answer clarifying questions
|
|
266
|
+
from the project brief FIRST (Step 1.5)** — only ask the user for a field (angle, which product,
|
|
267
|
+
offer/code) the `creative_brief` leaves empty AND the recipe can't default. Do not re-ask for
|
|
268
|
+
anything the composer already captured.
|
|
269
|
+
2. **Mirror the whole ingredient set for review** — `update_ad_project_script { project_id,
|
|
270
|
+
script_drafts, script }`. `script_drafts` is a structured payload of **container-tagged
|
|
271
|
+
ingredients** so the app renders each piece the right way:
|
|
272
|
+
`{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, text? }] }`. Each
|
|
273
|
+
ingredient's `container` tells the app HOW to show it:
|
|
274
|
+
- `image` (a frame shown in the video), `endcard` (the end card), `avatar` (a character
|
|
275
|
+
headshot), `background` → rendered as an image tile.
|
|
276
|
+
- `voice` (a voiceover clip — put the voice NAME in `subtitle`), `music` (the bed),
|
|
277
|
+
`audio` → rendered as an audio player.
|
|
278
|
+
- `video` (a clip) → a video player. `text` (a copy line like the CTA) → a text tile.
|
|
279
|
+
- `script` / `thread` / `note` / `conversation` → the written script (or set `scenes[]`
|
|
280
|
+
for the podcast shape, or pass the readable `script` string).
|
|
281
|
+
`path` = `working/review/<name>` (upload the preview asset first via `get_upload_url`); `url`
|
|
282
|
+
works too. **Label every ingredient** ("Hook image", "End card", "Voiceover", "Background
|
|
283
|
+
music", "HER"). The `update_ad_project_script` call itself writes no render and costs no credits
|
|
284
|
+
(the cheap pieces you already generated above have their own small cost) — it just populates the
|
|
285
|
+
review panel.
|
|
286
|
+
3. **STOP — the review happens in the APP's review panel, NOT in this chat.** You've mirrored the
|
|
287
|
+
ingredients (3.2); now hand the user the project's `app_url` (from `get_ad_project`) and tell
|
|
288
|
+
them to review the pieces there and hit **"Approve & render"**. That button gives them a short
|
|
289
|
+
message to paste back into this session — THAT is your go-ahead. Do NOT paste the
|
|
290
|
+
script/ingredients into the chat for a thumbs-up, and do NOT render until that approval comes
|
|
291
|
+
back from the app. If they want changes (via the app's comments or here), regenerate the
|
|
292
|
+
affected ingredient, call `update_ad_project_script` again, tell them it's refreshed in the
|
|
293
|
+
app, and wait for a fresh approval. Only AFTER the app approval do Step 4. A single approval
|
|
294
|
+
authorises the WHOLE remaining chain — generate every paid piece, render, self-QC, publish —
|
|
295
|
+
with NO further pauses (that is exactly why every paid prompt must already be in the panel).
|
|
296
|
+
|
|
297
|
+
## Step 4 — render locally, report stages, publish
|
|
298
|
+
|
|
299
|
+
1. Now generate every PAID piece you showed as a prompt in Step 3 — the AI stills/video, voice,
|
|
300
|
+
music, the end-card render — through the media proxies (below), each from its approved prompt.
|
|
301
|
+
Then assemble per the recipe (Playwright record where needed → ffmpeg stitch → `mix-master`
|
|
302
|
+
audio).
|
|
303
|
+
2. Open the row LAST: `submit_render { project_id, kind: "full" }` → keep `render_id`, then
|
|
304
|
+
`update_render_status { render_id, status: "running" }`. The render row tracks status only
|
|
305
|
+
(queued / running / complete / failed) — narrate fine-grained progress with
|
|
306
|
+
`append_project_message` instead.
|
|
307
|
+
3. **MANDATORY final-video QC gate — YOU review EVERY finished master before `set_final_render`,
|
|
308
|
+
whatever the format (UGC or not).** This is your own automated quality check, separate from the
|
|
309
|
+
user's Step-3 approval — it does not go back to the user. The render row is already open (its
|
|
310
|
+
nominal credit spent, `submit_render` in 4.2);
|
|
311
|
+
this gate stands between a rendered master and PINNING/publishing it, so a bad render never gets
|
|
312
|
+
set as final. A master that looks fine on a still can still have a mis-voiced word, a caption
|
|
313
|
+
drifting off its line, a beat out of order, or a deformation — review the actual VIDEO, not
|
|
314
|
+
stills. Run the passes that APPLY to this format:
|
|
315
|
+
- **Audio ↔ script** — any master with SPEECH (VO or native/Seedance voice); **skip for
|
|
316
|
+
music-only / no-speech formats.** `review-ugc-render` is format-agnostic despite the name —
|
|
317
|
+
a deterministic Whisper transcript-vs-script diff, not UGC-specific: persist the approved
|
|
318
|
+
spoken lines to `working/approved-script.txt`, then `gooseworks fetch review-ugc-render` and
|
|
319
|
+
run `review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
|
|
320
|
+
working/review-verdict.json` (exit 0 PASS / 2 FAIL / 3 ERROR). It blocks a mis-voiced word
|
|
321
|
+
(approved "human-vetted" → "human witted"), a dropped phrase, or silence. It routes Whisper
|
|
322
|
+
through the gooseworks proxy when `OPENAI_BASE_URL` is set; with no backend at all, run
|
|
323
|
+
`fal-ai/whisper` via `fal-proxy` (upload the audio, pass its `get_download_url` as `audio_url`)
|
|
324
|
+
and diff the transcript yourself.
|
|
325
|
+
- **Captions / subtitles** — ANY captioned format (the most common non-UGC defect); **skip for
|
|
326
|
+
UGC/Seedance masters, which carry no subtitle track.** Concrete check: diff the caption file
|
|
327
|
+
you burned (SRT/ASS) against the SAME Whisper transcript + word timings from the audio pass —
|
|
328
|
+
every caption line must match the heard/scripted words and sit within ~0.3s of when they're
|
|
329
|
+
spoken; then in the visual pass below, OCR-read the burned caption off 4–5 sampled frames to
|
|
330
|
+
confirm it's actually on screen at that time and not colliding with a hyperframe or the end
|
|
331
|
+
card. Mismatched text or >0.3s drift fails the gate.
|
|
332
|
+
- **Visual + structure** — always: run the `watch` skill on the master — beat/scene order + SFX,
|
|
333
|
+
the brand's product (not the source's) is shown, the end card has the real wordmark + code, no
|
|
334
|
+
deformation/artifact, duration within ~20% of the source.
|
|
335
|
+
If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, rebuild captions)
|
|
336
|
+
and re-review — only a clean pass proceeds to `set_final_render`. **This gate is universal: it
|
|
337
|
+
runs from the master skill for every format, so a recipe never has to opt in.**
|
|
338
|
+
4. Publish: `get_upload_url { target: { type: "agent", agent_id: ADS_AGENT } }` → PUT the master
|
|
339
|
+
and poster **under the project folder** (see Identity's path-prefix rule) — to
|
|
340
|
+
`agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4` and
|
|
341
|
+
`.../working/final-thumb.jpg`. **Always target ADS_AGENT AND use the full project-folder path**
|
|
342
|
+
— a bare `working/final.mp4`, even on the right agent, 404s in the app. Verify servable:
|
|
343
|
+
`get_download_url { target: ADS_AGENT, path: "agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4" }`
|
|
344
|
+
must return a non-empty URL (curl it for HTTP 200).
|
|
345
|
+
Then `update_render_status { render_id, status: "complete", output_url, thumbnail_url }` where
|
|
346
|
+
**output_url MUST be the durable render-file URL**
|
|
347
|
+
`/api/ads/projects/<project_id>/render-file?path=working/final.mp4` (the app re-presigns it on
|
|
348
|
+
every view) — NEVER a raw proxy/CDN URL (those expire). Same for `thumbnail_url`.
|
|
349
|
+
5. `set_final_render { project_id, render_id }` to pin it, then return the `app_url` +
|
|
350
|
+
`brand_url` (from the project/links) verbatim. Never end on just "done" or a file path.
|
|
351
|
+
|
|
352
|
+
Narrate each long step in one line via `append_project_message { project_id, role: "agent",
|
|
353
|
+
content }` — never sit silent on a queue > 90s.
|
|
354
|
+
|
|
355
|
+
## Media generation — the GooseWorks proxies (queue loop)
|
|
356
|
+
|
|
357
|
+
Media APIs go through GooseWorks proxies with your agent token; do NOT use an SDK's default host
|
|
358
|
+
(your token isn't a FAL/ElevenLabs token → 401). Base = `<api_base>/api/internal/<proxy>`; pass
|
|
359
|
+
`?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>` (agent_id bills the Ads agent;
|
|
360
|
+
`project_id` = the id of the project you're rendering — it attributes this generation's credits to
|
|
361
|
+
that ad project so the user sees per-project spend in the app. ALWAYS pass it). FAL = `fal-proxy`
|
|
362
|
+
(+ `fal-storage-proxy` to host a local image and get a CDN URL); ElevenLabs = `elevenlabs-proxy`
|
|
363
|
+
(VO / music bed).
|
|
364
|
+
|
|
365
|
+
**FAL queue gotcha** (#1 waste of generations): submit returns `status_url`/`response_url` on
|
|
366
|
+
`queue.fal.run` (the real host, not the proxy). Polling those 401s forever — rewrite their host
|
|
367
|
+
to the proxy base (keep the path), re-add `?token=&agent_id=`. Only the final `*.fal.media`
|
|
368
|
+
image is a real public URL. Helper:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
import json, os, pathlib, time, requests
|
|
372
|
+
from urllib.parse import urlparse
|
|
373
|
+
|
|
374
|
+
def _cfg():
|
|
375
|
+
c = json.loads(pathlib.Path(os.path.expanduser("~/.gooseworks/credentials.json")).read_text())
|
|
376
|
+
return c["api_base"].rstrip("/"), c["api_key"], c.get("agent_id")
|
|
377
|
+
|
|
378
|
+
def _params(tok, agent, project_id=None):
|
|
379
|
+
p = {"token": tok}
|
|
380
|
+
if agent: p["agent_id"] = agent
|
|
381
|
+
if project_id: p["project_id"] = project_id # attributes the spend to this ad project
|
|
382
|
+
return p
|
|
383
|
+
|
|
384
|
+
def fal_generate(model_path, payload, project_id=None, timeout_s=180, poll_s=3):
|
|
385
|
+
"""model_path e.g. 'fal-ai/nano-banana-2/edit' (the recipe names the model).
|
|
386
|
+
Pass project_id = the ad project you're rendering so credits attribute to it.
|
|
387
|
+
Returns the result image URL (a public *.fal.media CDN URL)."""
|
|
388
|
+
api_base, tok, agent = _cfg()
|
|
389
|
+
base = api_base + "/api/internal/fal-proxy"
|
|
390
|
+
sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent, project_id), json=payload).json()
|
|
391
|
+
to_proxy = lambda u: base + urlparse(u).path
|
|
392
|
+
status_url, response_url = to_proxy(sub["status_url"]), to_proxy(sub["response_url"])
|
|
393
|
+
deadline = time.time() + timeout_s
|
|
394
|
+
while time.time() < deadline:
|
|
395
|
+
st = requests.get(status_url, params=_params(tok, agent, project_id)).json()
|
|
396
|
+
if st.get("status") == "COMPLETED":
|
|
397
|
+
return requests.get(response_url, params=_params(tok, agent, project_id)).json()["images"][0]["url"]
|
|
398
|
+
if st.get("status") in ("FAILED", "ERROR"):
|
|
399
|
+
raise RuntimeError(f"FAL failed: {st}")
|
|
400
|
+
time.sleep(poll_s)
|
|
401
|
+
raise TimeoutError("FAL polling exceeded timeout")
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
ElevenLabs (VO / music) is the same shape against `<api_base>/api/internal/elevenlabs-proxy`
|
|
405
|
+
with `?token=&agent_id=&project_id=`. Feed FAL a local image by storing it (`get_upload_url`) and passing its
|
|
406
|
+
`get_download_url` presigned URL as an `image_urls` / `audio_url` entry — this is the reliable
|
|
407
|
+
path. (`fal-storage-proxy` may 404 depending on the install; don't block on it — prefer the
|
|
408
|
+
`get_download_url` presigned URL.)
|
|
409
|
+
|
|
410
|
+
## Rules
|
|
411
|
+
|
|
412
|
+
- **MCP + ffmpeg + Playwright required** — run `gooseworks doctor` in Phase 0; stop with the
|
|
413
|
+
exact fix it prints if anything is ✗.
|
|
414
|
+
- **Assemble the whole review set first**, mirror it with `update_ad_project_script`, and get the
|
|
415
|
+
user's approval **in the app's review panel** (the "Approve & render" button) BEFORE the expensive
|
|
416
|
+
render — never ask for a thumbs-up in this chat (review-once, in-app).
|
|
417
|
+
- **Show the REAL cheap pieces; PROMPT only the expensive render.** Generate the FREE + CHEAP-paid
|
|
418
|
+
pieces (≤ ~100 credits — stills, creator frame, end card, short VO/music) and mirror the real
|
|
419
|
+
assets; put ONLY the expensive video take/render in the panel as its exact prompt. That prompt
|
|
420
|
+
must be in the panel before you ask to approve, so a single "go" runs the render + any remaining
|
|
421
|
+
paid work (→ QC → publish) with no re-pausing.
|
|
422
|
+
- **Costs in CREDITS, never dollars.** credits ≈ round-up(provider-$ × 120) per generation + a flat
|
|
423
|
+
200-credit base per video; never show a "$…" figure to the user.
|
|
424
|
+
- **Never assemble the full video before approval.** The review shows the
|
|
425
|
+
individual PIECES, never the finished cut (or their prompts) — not a
|
|
426
|
+
stitched/composited cut; do not add a "full cascade" / finished-video clip
|
|
427
|
+
as a review ingredient (GOOSE-2542). The assembled video is produced only in
|
|
428
|
+
Step 4.
|
|
429
|
+
- **submit_render only after the master is rendered** (Step 4.2), never on a guess; `output_url` =
|
|
430
|
+
the durable render-file URL, never a CDN URL.
|
|
431
|
+
- **Always pass `project_id` on media-proxy calls** (fal / ElevenLabs) so the credits attribute
|
|
432
|
+
to this ad project — that's what lets the user see per-project spend in the app.
|
|
433
|
+
- **Verify a real, non-empty MP4** (watch it) before marking the render complete.
|
|
434
|
+
- **Reuse the brand** when its research is complete; never re-research.
|
|
435
|
+
- On a hard error (auth/quota/model/timeout) set the render `failed` with a short
|
|
436
|
+
`error_message` and stop — don't ship the source unchanged. **Also `log` it** (`gooseworks log`
|
|
437
|
+
/ `log_cli_event`, `--event-type api_failure|error`) so we can see + fix it (see "Report problems").
|
|
438
|
+
- **Report blockers/bugs/confusing instructions via telemetry** (`gooseworks log` or the
|
|
439
|
+
`log_cli_event` MCP tool) — not just to the user. Set `GW_RUN_ID` once so events group.
|
|
440
|
+
- Always end a successful run with `app_url` + `brand_url`, verbatim.
|
|
@@ -26,7 +26,8 @@ First apply the **Common company onboarding** gate below. Preserve the user's or
|
|
|
26
26
|
| --- | --- | --- |
|
|
27
27
|
| Remix/make an ad, research a brand for ads, OR analyze ad performance — Meta/Google ad campaigns, creative fatigue, CAC/lead quality, competitor ad intel, ad angles & hooks | **`goose-ads`** | Installed locally as an entry skill. Just use it. If unavailable, run `gooseworks install --claude`. |
|
|
28
28
|
| Charts, infographics, slides, social graphics, branded visual designs from a style/format | **`goose-graphics`** | If installed locally, use it. Otherwise `gooseworks fetch goose-graphics` (or `gooseworks install --claude --with goose-graphics`). |
|
|
29
|
-
|
|
|
29
|
+
| Order a **video** ad in chat — "make me a video ad for <brand>", a UGC / iMessage / explainer video; renders on the GooseWorks server | **`goose-video`** | Installed locally as an entry skill. Just use it. If unavailable, run `gooseworks install --claude`. |
|
|
30
|
+
| Render an EXISTING app video project or batch on this machine — the app's "copy for Claude" command names it | **`goose-video-local`** | Installed locally as an entry skill. Just use it. If unavailable, run `gooseworks install --claude`. |
|
|
30
31
|
| Make **product photos** — studio, lifestyle, marketplace, social, or on-model product photography | **`goose-product-photos`** | Installed locally as an entry skill. Just use it. If unavailable, run `gooseworks install --claude`. |
|
|
31
32
|
| Animate an approved static ad or product image | **`animate-image`** | Fetch with `gooseworks fetch animate-image` and follow its GooseWorks MCP workflow. |
|
|
32
33
|
| Anything else — scraping, research, lead gen, enrichment, any data lookup | (stay here) | Follow "How to Use" below. |
|
package/skills/routes.json
CHANGED
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"skill": "goose-video",
|
|
19
|
-
"when": "
|
|
19
|
+
"when": "Order a **video** ad in chat — \"make me a video ad for <brand>\", a UGC / iMessage / explainer video; renders on the GooseWorks server",
|
|
20
|
+
"how": "Installed locally as an entry skill. Just use it. If unavailable, run `gooseworks install --claude`.",
|
|
21
|
+
"delivery": "entry"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"skill": "goose-video-local",
|
|
25
|
+
"when": "Render an EXISTING app video project or batch on this machine — the app's \"copy for Claude\" command names it",
|
|
20
26
|
"how": "Installed locally as an entry skill. Just use it. If unavailable, run `gooseworks install --claude`.",
|
|
21
27
|
"delivery": "entry"
|
|
22
28
|
},
|
|
@@ -143,6 +149,7 @@
|
|
|
143
149
|
"goose-graphics",
|
|
144
150
|
"goose-product-photos",
|
|
145
151
|
"goose-video",
|
|
152
|
+
"goose-video-local",
|
|
146
153
|
"influencer-prospecting",
|
|
147
154
|
"meta-ad-policy-checker",
|
|
148
155
|
"meta-ads-analyzer",
|