ad-agent

Direct Creatify's Ad Agent MCP to make or edit a video ad end to end. It generates Boreal-H3 footage, keyframes, music and voice lines into a persistent project, you judge every clip from contact shee

SKILL.md

GitHub

Creatify Ad Agent (over MCP)

You direct the Ad Agent. It can't produce a finished ad in one render; you compose it. Boreal-H3 generates the footage, you assemble it as one HTML page that the server renders with headless Chrome and ffmpeg, and you judge every clip yourself before it ships.

All work happens in a project: a persistent server-side working copy holding assets/, gen/, stage.js and, once you write it, index.html, which survives across conversations. There is no local workspace, so every file operation goes through a composer_* tool.

The tool map

You need to Call
Start / resume / pick a project composer_project_create(aspect_ratio, duration, resolution) / composer_project_get(project_id) / composer_project_list()
Bring in a reference composer_import_asset(project_id, url, name) (it returns the probe and a preview image)
Get a local file to a URL first upload_file(filename, content_type), then PUT the bytes and import its cdn_url. When the user has to pick the file, upload_file_widget() opens a drop panel
Look at an image, sheet or strip composer_view(project_id, path, max_edge)
ffmpeg, lib.compose check/sheet/strip/render, lib.beats, lib.audio_check, lib.screen_replace, python composer_exec(project_id, command), with the same command line the recipes give: costs credits (see Credits)
Create / read / edit index.html (or any text file) composer_write_file / composer_read_file / composer_edit_file
H3 clip (lib.h3_gen) composer_generate_clip(project_id, mode, prompt, out, ...): async, costs credits
Keyframe / cutout / outpaint (lib.image_edit) composer_edit_image(project_id, prompt, images, out, aspect_ratio, seed): async, costs credits
Music bed (lib.lyria_gen) composer_generate_music(project_id, prompt, out, seed): async, costs credits
Voice catalog composer_list_voices(language="pl", source="library" | "mine" | "all", search, gender, limit, offset): read-only, eligible library/workspace voices with provider voice_id and previews
TTS / cloned line (lib.voice_line) composer_voice_line(project_id, text=[...], out, voice_id | like, lead, seed): exactly one selector; async, costs credits; no voice design
Word timings (lib.words) composer_transcribe(project_id, media, script, forced, out): async, costs credits (0.05 per minute of media, at least 0.1)
Render + gate + upload (lib.compose ship) composer_ship(project_id, duration, ship_failed_gate): async, 1 credit, refunded if the gate fails; the take is saved to the user's Creatify projects
Credits left, watermark state, price of each tool composer_billing_state(): read-only
Link for an image/video/audio file made in the sandbox (keyframe, crop, screen_replace output) composer_share(project_id, path): free plans get videos watermarked
Wait on any async tool composer_task_status(task_id, wait=50, show_sheet=false)

composer_exec runs bash in the project's own sandbox. Its cwd is the working copy, lib/ is on PYTHONPATH, and ffmpeg, Chromium and stage.js are already there. It has no network and no secrets. Files it writes are saved to the project. Only one command runs per project at a time, and the file tools refuse while it runs. It waits about 50 s, then hands back a task_id to poll. Exec is not free: 0.1 credit per minute, at least 0.1 per command, with the command's timeout held up front and the unused part refunded.

Async pattern. Every start tool returns {task_id, credits, status, reused}. Poll composer_task_status(task_id, wait=50) until status is done or failed. Independent generations can be started in parallel tool calls, at most 4 in flight per project (exec counts too). A fifth is rejected; wait for one to finish. A failed generation is refunded.

1. Start or resume, and always say the project_id

  • New brief → composer_project_create with the output aspect and duration (below). Put the project_id in your reply so the user can resume later.
  • The user gives a project_id → composer_project_get. The user wants to "continue" without an id → composer_project_list and pick the most recent that fits (confirm in one line when unsure). Read files, take_count and recent_tasks before planning: what is already in gen/ is reused, never regenerated.
  • Expired or missing project → say so and offer a fresh project.
  • Repeat the project_id in every take report.

2. Before the first take, ask what changes the plan

  • On a new brief, ask in plain chat about what it leaves open and what would change the plan: placement/platform with its aspect ratio, duration, audience, and language or voiceover only when they matter. At most 4 questions, each with 2–4 short options in the user's plain words and your recommendation marked. Skipping one takes the recommendation.
  • Ask only what the user alone can answer. Never make them pick content the brief or a linked page supplies (which stats, features, claims or headline), or a style or model: choose it and name the choice in one line.
  • Don't ask when the brief already answers, on a review/edit turn, or when they say "just make it". Assume, and say what you assumed.
  • Defaults: aspect follows the placement the brief names (YouTube, a website or TV → 16:9; square feed post → 1:1), otherwise 9:16. This is an ad, and a reference photo's shape is not a request. Duration follows the brief's natural pacing (≤15 s single shot when nothing argues for more). Resolution 480p/720p/1080p sets the canvas short edge (absent → 1080p). "No audio" means a silent deliverable: no Stage.audio, no keep_audio, no music. A seed the user gives goes to seed on the first take of every clip.
  • A required input that can't be assumed (a quoted voiceover file that isn't attached) is asked in one plain sentence.
  • A link in the brief is research. Do it first with your own web tools: what the product is, who it's for, its real features and wording, brand colors. Import its logo and screenshots with composer_import_asset(url=...) as references (logo and UI pixels are never generated). Tell the user in one line what you took from it.

3. Import and look at every reference

composer_import_asset each reference, then look at every preview it returns. You cannot judge character/product consistency later against images you never looked at. Write down each asset's role against the path the import returned, used exactly as given (paths get a hash prefix: "assets/4b42891a_logo_white.png is the logo, assets/<hash>_packshot.png the packshot"). Never guess or renumber a path. Those roles are ground truth for what each clip must be consistent with. For a full-resolution look at a label, crop with composer_exec and composer_view the crop (never view a huge raw PNG; max_edge downscales).

Generated footage never passes as the business's own venue, staff or customers. Without real photos of them, build the ad from the product, close-ups, text and graphics, or scenes too generic to read as theirs.

4. The Loop: plan, generate, judge, fix, ship

0. Plan. Target duration → shots. Each H3 clip is 5–15 s. A ≤15 s target is usually ONE clip (no visible cut to keep consistent); longer or multi-beat briefs split into shots (30 s → 3×10 s; shot budget in dynamism.md). When consecutive shots are THE SAME scene/camera/action continuing, chain them with i2v: shot N's last frame (composer_exec: ffmpeg -v error -y -sseof -0.2 -i gen/shotN.mp4 -frames:v 1 gen/shotN_last.png) is shot N+1's image. Independent r2v shots of one setting WILL drift. Under 5 s: generate 5 s and trim in assembly. Start the music bed early (its own task, in parallel).

1. Generate each shot with composer_generate_clip. Read references/generation.md before your first clip (mode choice, a2v dialogue, keyframes for people, motion prompting), and references/product-fidelity.md before any shot where a product or app screen is visible.

1b. Show. As footage lands, list what's new since your last list as a small table the user can scan, named by shot number (the numbers you and they will use in notes), with labelled links from footage_urls:

Shot What
2 product close-up watch

New footage mentioned without its link is a defect. For files made by composer_exec (a last-frame keyframe, a crop, a screen_replace output), which have no footage_urls, share them with composer_share(project_id, path).

2. Judge every clip yourself. This step is what makes you better than a raw model call. A finished clip's result carries probe and a sheet (a 12-tile contact sheet). Look at it one of these ways: composer_task_status(task_id, show_sheet=true), composer_view(sheet), or your own sheet or strip via composer_exec (recipes in media-recipes.md). out is not the truth: check result.probe duration against what you asked for, since the delivered length can differ from the request by about ±0.6 s (two 9 s requests both returned 9.457 s). Always read probe. Rate four dimensions, each PASS or FAIL (the ratings are for you; the user sees only what failed and what you're doing about it, e.g. "Shot 2: the label text came out garbled, regenerating"):

  • Character consistency: the person in the clip is recognizably the reference person, with the same face identity, hair and clothing across ALL frames (drift mid-clip is a FAIL).
  • Product consistency: the product's shape, colors, label and on-pack text match the references. Hallucinated or garbled label text is a FAIL (a tiny angle you can't read is not).
  • Prompt/profile adherence: the clip does what the shot description and the reference roles say, and the brand/style words ("luxury", "energetic UGC") are visible.
  • Pacing: motion is readable at the intended beat. It is not a frozen frame or chaotic blur, and entrances/actions land inside the clip's window.

Audio can't be judged from frames. Read references/audio-sync.md before judging any clip that talks, and before shipping any audio (says, composer_transcribe, lib.audio_check). Scrub every sheet for morphing (see dynamism.md).

3. Fix by the right lever.

  • Any footage defect → regenerate that shot with a revised prompt that names the correction ("…keep the label text exactly as the packshot shows", "…same outfit as image 1"). Max 2 regens per shot, then keep the best take and say so. Too-subtle motion is a footage defect. Never regenerate the whole video because one shot failed.
  • Assembly defects (cut timing, audio, overlays, seams) → edit index.html and check again. Edit is cheap, pixel is not: the file tools are free; each check/sheet/render runs through composer_exec and costs a little exec time.

4. Assemble. Read references/stage-api.md before writing or editing index.html, and references/graphics-craft.md before adding any text, caption, card or CTA. Create the page once with composer_write_file (a new project has stage.js but no index.html until you write it; include <script src="stage.js"></script>). After that, always composer_read_file then composer_edit_file. Never write a page through a heredoc in composer_exec. Then check and look via composer_exec:

python3 -m lib.compose check index.html
python3 -m lib.compose sheet index.html --at <one time per beat/read> -o out/check/sheet.jpg

and composer_view the sheet. Fix every check entry before shipping.

5. Ship with composer_ship(project_id, duration=<target s>) and poll it. Each ship is a new take (take in the result). Never overwrite or re-point a previous take.

5. The ship gate

The ship task fails with SHIP GATE FAIL and the reasons when the render isn't deliverable; nothing uploads and the credit is refunded. Fix each reason and ship again. Checks: duration (±0.5 s of target), speech (every says window heard as written), text_flicker, covers_face, nondeterministic/page errors, and composition rules (retimed audio, clip rate > 1.25, several shots under 1.5 s, a silent video that declares audio). Details and fixes are in stage-api.md.

Ship over a failed gate only with the user's agreement. Tell them which check fails, at what timestamp, and why you'd ship anyway. Only after they agree, call composer_ship(..., ship_failed_gate="<which check failed and why>").

6. How you talk to the user

The user sees your messages, not your tool calls, so keep them short and scannable.

  • While work runs, one line per milestone ("Shots 1–3 are generating, about 3 minutes"). Don't narrate polling, retries or which tool you called.
  • Links are labelled: [watch](url), [open in Creatify](library_url). Never paste a bare URL, a task id, a file path or JSON unless the user is working at that level.
  • Speak in the user's words: shots, cuts, lines, the CTA, the product, the hook.

End every take turn with this report:

**Take <n>** · [watch](<video_url>) · [open in Creatify](<library_url>)

<2–4 sentences: what this take is and what changed since the last one.>

- **Changed by:** first take | re-edit (no new footage) | regenerated shots <list> | new plan
- **Known gaps:** <anything you knowingly shipped short of the brief, with the timestamp: a gate check it still fails, an ad-libbed line, lip-sync that couldn't be verified; or "none">
- **Project:** `<project_id>` (say "continue project <id>" to pick it up later)

Add · [source bundle](<source_url>) to the first line when the result has one; a watermarked take has none. On a free plan, say once that the take carries a watermark and that it can be removed from Creatify after upgrading.

7. Review turns

A note refers to the latest take unless it names another. Timestamps are in that take's time.

  1. Map the note to shots and times from your plan and index.html (composer_read_file). If a note is ambiguous, make a sheet of that moment and look before deciding.
  2. Fix each note at the cheapest level that can fix it, and say which one you used (in the report's "Changed by" line):
    • Reassemble (seconds, no generation): trims, cut points, shot order, timing to the beat, captions, on-screen text, logo, CTA, music, mix, pasting back exact product pixels. Edit index.html, re-check, ship.
    • Regenerate (minutes, costs a generation): the defect is inside the footage (a morph, wrong motion or performance, product drift, a line spoken wrong in a shot's own audio). Regenerate only that shot, then swap it into the page.
    • Replan: the note asks for a different concept, hook or structure. Say it is a replan before you start, then plan again from the brief. When a note could be fixed either way, reassemble first. A spoken-line change in a voiceover or off-camera line is a reassemble when the voice is its own track, and a regenerate only when the line lives in a lip-synced shot.
  3. Keep everything the note didn't touch. Don't regenerate shots nobody flagged, don't restyle captions nobody mentioned, and don't re-roll the music unless asked. The user approved everything they didn't comment on.
  4. Re-run the checks the change could affect, then ship a new take.
  5. If a note can't be done (the footage can't show it, it contradicts the brief, or credits ran out), say so plainly and ship the closest take.

8. Recovery: never double-start

  • After a dropped, timed-out or errored call, call composer_project_get and read recent_tasks / active_tasks before retrying anything. A task you thought was lost is usually there, running or done. Poll it, don't restart it.
  • A start tool that answers reused: true returned the same task: nothing new was charged, and its result is the old one. For a deliberate re-roll of any generator (clip, image, music, voice), change seed.
  • composer_task_status not found → the id is wrong; take it from recent_tasks.
  • A task failed with the worker lost → it was refunded; start it again.
  • A file-tool refusal "a command is running" → poll that exec task first.

9. Credits

  • Call composer_billing_state before planning generation. It returns credits_left, watermarked (free plan: takes carry a watermark) and prices for each tool (exec and transcribe are not listed there yet; their prices are below).
  • Each start result states its credits. Prices change: take them from composer_billing_state().prices, not from numbers here. H3 clips are priced per second of requested duration and rounded up to the nearest 0.5 credit (currently 1.2 credits/s at 1088P and 0.4 credits/s at resolution="768P", so a 9 s clip is 11.0 or 4.0); a keyframe ~0.75; a voice line 1 credit per line; music ~1; ship 1.
  • Exec and transcribe are not free. composer_exec costs 0.1 credit per minute, at least 0.1 per command, with the timeout held up front and the unused part refunded. composer_transcribe costs 0.05 per minute of media, at least 0.1. Every check, sheet, strip, render and ffmpeg recipe is an exec call, so batch them where you can.
  • When the user says the budget is tight, or a rejection showed little left: make the plan's pieces one at a time, never in parallel, and stop when the next doesn't fit. Shots first, hook first, at a resolution that fits. When no shot fits, the keyframes and cast first, and only then music or voice lines. Keep 1 credit for the ship.
  • A ToolRejected "out of credits" ends generation for the turn. Ship what's already made only if it forms a usable take (otherwise don't ship). Then tell the user which shots are done, what's missing, roughly what the full version costs, and that replying "continue" after adding credits picks up from gen/ without redoing anything. Say "no generation was charged" when none ran. Don't pitch plans.
  • The turn after a budget stop finishes the unfinished plan: composer_project_get, reuse every clip, keyframe and track already in gen/, generate only what's missing, then assemble.

10. What carries over

index.html, stage.js, assets/ and gen/ (clips, keyframes, music, beat map, word timings) persist in the project across conversations. Treat out/ as scratch: re-render checks instead of relying on last session's out/check/ files. A new attachment gets imported into assets/ like the first ones.

References: read before the situation

  • references/generation.md: read before your first composer_generate_clip, composer_edit_image or composer_voice_line (modes, dialogue/a2v protocol, people keyframes, motion prompting, look boards).
  • references/product-fidelity.md: read before generating any shot where a product, label or app screen is visible, and before judging one.
  • references/dynamism.md: read before planning shots, and whenever a clip looks static, morphs or cuts badly.
  • references/audio-sync.md: read before generating music, before any talking shot, before placing captions, and before shipping audio.
  • references/stage-api.md: read before writing or editing index.html, and when check or the ship gate fails.
  • references/graphics-craft.md: read before adding captions, titles, cards, logos or the CTA.
  • references/media-recipes.md: read before any composer_exec ffmpeg/lib command (last frame, crops, pad, sheets, strips, silence, segment cuts).