AdAnvil
An MCP server that lets an AI agent edit vertical 9:16 video ads.
AdAnvil works with Claude Code, Codex, Cursor or any MCP client. It builds ads from a brand's own footage, from a reference ad, or from an idea. The agent judges (which clips, what the text says, whether the result looks right). AdAnvil renders deterministically and checks the result: the same storyboard always gives the same video.
It is not tied to a niche. The recipes and the editing guide speak in general terms (product, benefit, problem, target customer) with examples from several niches: coffee, fashion, fitness, pets, software, home, beauty.
The demo above was made from the CC0 demo pack in examples/demo/ (four Wikimedia Commons clips, a product still and a fictional brand called Ember & Oak). No footage is stored in this repository: the pack is fetched on demand.
What it does
- Reference decoding: shots, pacing, burnt-in text and speech, plus a shot-by-shot breakdown the agent writes and compares to your edit.
- Footage library: indexing, contact sheets and mosaics so the agent can look at every clip before using it, and the speech of every video and audio file (word timestamps, searchable as soon as each file is done) so a talking creator can be cut on the pauses.
- 9 recipes (YAML) for common ad shapes: text hook, hook plus product, before and after, customer diary, comment reply, meme text, voice-over ad, long remake, talking-head UGC.
- 7 caption styles (
pill,meme,outline,outline_soft,box,label,comment) with emoji support. - Removal of burnt-in text from reference footage.
- HDR (HLG and PQ) to SDR tone-mapping.
- Music matching and loudness control.
- Optional open-source voice-over (Chatterbox) with captions synced to the spoken words.
- Talking-head UGC: best take of each line across several recordings, cuts on the pauses without hesitations, the creator's own sound kept (with B-roll laid over the voice), captions built from the speech.
- Visual devices: a before/after reveal (a divider sweeps one picture away to show the other), split screens, picture cards (a photo or a screenshot that comes in over the video), video insets (picture-in-picture), stickers and arrows, a progress bar and punch-in zooms.
- Sound design: a pack of about 18 sound effects (CC0 or synthesised, never stored in the repository),
sfxoverlays andaudio.sfx_autoat the cuts and captions, and cuts that snap to the beat of the music. - End cards composed from a real product photo.
- Quality control with images: automatic checks, contact sheet of the result, full-resolution frames.
- Detached background jobs for indexing, voice and rendering, so long work survives a dropped connection.
Quick start
Requirements:
- Python 3.11 or newer, and uv (it provides
uvx). - ffmpeg 6.1 or newer, built with
libzimg, on yourPATH. Check withffmpeg -version. If ffmpeg lives elsewhere, setADANVIL_FFMPEGandADANVIL_FFPROBEto the full paths.
Three commands:
# 1. Check the install (the first run downloads the package)
uvx --from git+https://github.com/maxbuildog/adanvil adanvil --version
# 2. Register the server in Claude Code
claude mcp add adanvil -- uvx --from git+https://github.com/maxbuildog/adanvil adanvil-mcp
# 3. Then ask your agent:
# "Make a 9:16 ad from the footage in ./my-project"
The package is named adanvil. It installs two commands: adanvil (command line) and adanvil-mcp (the MCP server over stdio). uvx adanvil-mcp alone does not work, because uvx looks for a package with that name: always add --from. Once the package is on PyPI you can use --from adanvil instead of the Git URL.
The agent creates the project folder and tells you where to drop your files: footage in footage/, the reference ad in reference/, music in music/, logo and end card in brand/. It learns the method from the editing_guide tool and from AGENTS.md.
Other clients
Codex, in ~/.codex/config.toml:
[mcp_servers.adanvil]
command = "uvx"
args = ["--from", "git+https://github.com/maxbuildog/adanvil", "adanvil-mcp"]
tool_timeout_sec = 900 # indexing long footage can take minutes
default_tools_approval_mode = "approve" # needed with approval_policy = "never" (codex exec)
Without default_tools_approval_mode, a non-interactive codex exec run rejects every AdAnvil tool call with "MCP tool call requires approval".
Cursor, in .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"adanvil": {
"command": "uvx",
"args": ["--from", "git+https://github.com/maxbuildog/adanvil", "adanvil-mcp"]
}
}
}
Any other MCP client (generic stdio entry):
{
"mcpServers": {
"adanvil": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "git+https://github.com/maxbuildog/adanvil", "adanvil-mcp"],
"env": {}
}
}
}
Restart the client after editing its config, then ask the agent to call recipe_list. Nine recipes means the server is connected.
Optional: reference transcription needs the transcribe extra (faster-whisper). Use --from "adanvil[transcribe] @ git+https://github.com/maxbuildog/adanvil" in place of the plain --from value above.
Try it with the demo pack
The demo needs a checkout, because the fetch script lives in the repository:
git clone https://github.com/maxbuildog/adanvil
cd adanvil
uv sync
uv run python examples/demo/fetch_demo.py ./demo-project
uv run adanvil index ./demo-project --source footage
Then register the MCP server and ask your agent to make an ad from ./demo-project (give it the absolute path), or follow the command line walkthrough in examples/demo/README.md.
How it works
Everything lives in one project folder: footage/, brand/, music/, reference/, renders/, cache/, plus adanvil.json, brand.yaml and catalog.json. Every tool takes the absolute project_dir; every other path is relative to it and cannot escape it.
- The agent indexes the footage, looks at the contact sheets, and optionally decodes a reference ad.
- It picks a recipe and writes a storyboard (JSON): shots, captions, audio.
storyboard_validatechecks the storyboard against the recipe rules, thenrender_startrenders it in the background.qc_runchecks the file and returns control images that the agent looks at before delivering;ad_scorescores its creative quality (aim for 80 or more).
Details: docs/ARCHITECTURE.md for the pipeline, docs/EDITING_GUIDE.md for the editing doctrine, and AGENTS.md for the step-by-step guide an agent follows (same content as skill/SKILL.md, the Claude Code skill version).
Choosing a model
The result depends on the model that drives AdAnvil. Reference analysis and shot choice are judgement work: use the strongest model you can (for example Claude Opus) for them. Smaller models execute the steps well, but they tend to copy the surface of a reference (same words, same shots) instead of rebuilding its logic for your brand.
Tools (42)
| Tool | What it does |
|---|---|
project_init |
Create an AdAnvil project skeleton. |
project_read_text |
Read a small project text file (licence notes, JSON, YAML). |
brand_onboard |
Read a brand's site, then write the brand profile from the answers. |
brand_get |
Read the brand profile and brief. |
library_index |
Index or refresh footage (long runs become detached jobs). |
library_view |
One catalogue entry with its contact sheet. |
library_annotate |
Save what the agent saw in a clip (description, people, emotion, quality, exclusion). |
library_search |
Search the indexed footage by text (including what the clips say), tags, people, emotion, duration, or by sentence (query, with the vision environment). |
speech_view |
Compact phrases (with flags), fillers, restarts and clean cut segments of one clip's speech. |
speech_takes |
The best take of each line across several recordings of a script. |
speech_refine |
Transcribe chosen clips again with a stronger model and the brand vocabulary (detached job). |
library_overview |
Overview of the footage folders without indexing, plus a mosaic. |
frame_view |
One full-resolution frame from a video or image. |
reference_decode |
Decode a reference video into shots, pacing, text, speech and sheets. |
reference_breakdown |
Save the agent's shot-by-shot breakdown of the reference. |
recipe_list |
List the ad recipes with purpose and duration. |
recipe_get |
One full recipe with its rules and an example. |
editing_guide |
The editing doctrine for footage, copy, rendering and QC. |
storyboard_schema |
The JSON Schema of the storyboard. |
storyboard_save |
Save a storyboard as storyboards/<name>.json once; later tools take that path instead of the object. |
storyboard_patch |
Edit a saved storyboard with a list of {path, value} changes. |
caption_preview |
A caption or comment drawn over a real 9:16 frame. |
end_card |
An end card image composed from a real product photo. |
storyboard_validate |
Validate a storyboard against its schema and recipe rules. |
render_start |
Validate and queue a background render (preview or final). |
voice_generate |
Queue a checked voice-over generation job. |
voice_view |
Read a voice manifest and suggested shot boundaries. |
music_profile |
Tempo, loudness and energy of the music tracks. |
music_search |
Find music whose licence allows an ad (CC0, PDM, CC BY, CC BY-SA) on Openverse and Freesound; fit_to="storyboard" ranks for an ad. |
music_fetch |
Download a found track into music/ with its licence record (.licence.json, LICENCES.txt), then profile it. |
music_generate |
Make a track locally (optional ACE-Step environment, adanvil music-setup), as a job. |
music_beats |
Beat times of a music track as it will play in the ad. |
sfx_list |
List the project's sound effects; fetch=true builds the CC0 sound pack in sfx/. |
storyboard_snap |
Move the cuts of a storyboard onto the beats of its music. |
job_status |
Status of a render, batch, voice or index job, with optional waiting. |
qc_run |
Technical QC on a rendered video, with control images (overlay crops follow the re-laid captions of a format; caption_on_face). Optional storyboard: the effective storyboard of that render. |
ad_score |
Creative score out of 100 of a rendered ad (hook, pacing, reading, story, platform safe zones, audio) with findings to fix and a safe zones preview. |
variants_build |
The matrix of variants of a storyboard (hooks x bodies x calls to action, up to 24): patches or slot replacements, validated and saved, with a contact sheet of the first and last 3 s of each. |
render_batch |
One detached job that renders many storyboards (and formats) with a shared segment cache and writes renders/<batch>/manifest.csv. |
render_formats |
One storyboard in 9:16, 4:5 and 1:1 (re-cropped footage, captions re-laid out by bands, safe zones of the placement). |
compare_pair |
How different a before and an after look (0 to 100), on the face or the whole frame, with a side-by-side preview, to pick the pair. |
vision_setup |
Install the optional vision environment (search by sentence, automatic tags, product versions, faces). |
CLI commands (adanvil <command> --help): mcp, voice-setup, vision-setup, init, cat, onboard, index, decode, recipes, validate, render, voice, qc, preview-caption, end-card, sfx-fetch. Add --json for machine-readable output.
Smart library (optional)
Finding the right shot is the slowest part of an edit. adanvil vision-setup (or the vision_setup tool) installs, once, a separate environment (about 2.5 GB, CPU only, no GPU needed) with a small multilingual image and text model (SigLIP 2, Apache-2.0) and a face detector (YuNet, MIT); the licences are in THIRD_PARTY.md. Nothing else changes without it, and library_index then says vision: unavailable.
- Index with vision.
library_indexalso embeds the frames of each contact sheet and looks for faces (cache/vision/<file key>.npz), and gives every entryauto_tags(a fixed label list such asperson,bathroom,hands,smiling,text on screen), kept apart from your owntags. - Search by sentence.
library_search(query="a woman with a sad face touching her jaw"), in any language, ranks shots across the whole library, with the usual filters applied first, and returns the best ones with one numbered mosaic: no sheet to open. - Product versions from photos.
brand.yamllistsproduct_versions: [{name: current, photos: [...]}, {name: old, photos: [...]}]; indexing guesses which version each shot shows, andlibrary_search(product_version="current")uses the guess unless a human annotation exists (a human annotation always wins). Give photos, not words. - Face-aware framing.
focus: "face"keeps the main face centred atface_y(0.38 of the frame by default) on clips, images, split panels and reveal sides; arevealalso puts both faces at the same place and size by default (align_faces).compare_pairuses the face detector too.
Voice-over (optional)
AdAnvil can add a generated voice-over with Chatterbox, an open-source text-to-speech model, with captions synced to the spoken words.
- Run
adanvil voice-setuponce. It creates a separate Python environment (in the AdAnvil cache folder) and downloads about 3 GB of models.adanvil voice-setup --engine kokoroadds the fast engine (below) to the same environment. - Generation runs on the CPU. One worker process per job loads each model once and serves every step (speech, alignment, check). Measured on an idle 8-core server (French, 3 to 9 lines of about 5 seconds): the Chatterbox voice takes about 25 to 35 seconds per line, so 9 lines take about 5.5 minutes (it took 10.5 before V2.6); a busy machine is slower, count up to 5 minutes per sentence.
- Two engines.
chatterboxis the default and the recommended voice for ads (the owner listened to both and kept it): it clones thereferencevoice, so it also fits a brand that has a voice to match.kokoro(Apache-2.0) has fixed voices and no cloning ({"engine": "kokoro", "voice": "ff_siwis"}, Frenchff_siwis, Englishaf_heartby default,bf_emmaand others for British English): a fast option for drafts and previews (check the script, the timing and the captions before paying for the real voice), about 25 seconds for 9 lines plus 50 seconds of alignment and check on the same server. It speaks French and English only. - The pronunciation check reuses the alignment transcript when every word is confident (
check_confidence, default 0.6) and the line reads as written; only doubtful lines are transcribed again by thecheck_model. Setcheck_confidencetonull(orADANVIL_VOICE_CHECK_CONFIDENCE=off) to check every line with thecheck_model. - The voice speaks for the brand, as a narrator. It must not pose as a fake customer.
- The voice language is the
languageof the voice spec. When the spec does not set it, AdAnvil uses thelanguageof the project'sbrand.yaml(fr-FRcounts asfr), andenwhen the brand has none. - Chatterbox embeds a Perth watermark in its audio. AdAnvil keeps it. Do not remove it.
- Use only a reference voice you have the right to use.
Talking-head UGC (optional)
A creator talking to camera is cut from the speech in the footage. library_index transcribes the clips and audio files that have sound (word timestamps, in the same job, once per file, each file stored as soon as it is done), with faster-whisper when it is installed (pip install 'adanvil[transcribe]') or else the voice environment (adanvil voice-setup); without either, indexing still succeeds and the entries say so. Indexing uses a fast model to make the speech searchable; speech_refine transcribes the clips you keep again with a stronger one, and the brand name, the product name and speech.vocabulary of brand.yaml are given to the transcriber. A voice recorded in a separate audio file is found by name (separate_audio_candidates on a video with silent sound) and laid with audio_from; syncing is up to the agent, on a preview. Then speech_takes picks the best take of each line, speech_view gives the cuts that keep the speech and drop pauses and hesitations, and a storyboard with audio.clips, audio_from (B-roll over the voice) and a speech_captions overlay renders the ad. The ugc_talking recipe puts it together; the guides explain each step.
ADANVIL_SPEECH_MODELsets the indexing model (defaultbase: fast, good enough to search).speech_refineusesmediumunless told otherwise. The language is thelanguageofbrand.yaml, else auto-detected.ADANVIL_SPEECH=offstopslibrary_indexfrom transcribing on its own (transcribe="auto");transcribe=truestill does.- The creator's words are claims: the validator warns
banned_word_spokenwhen a kept word is on the brand's banned list, andcut_mid_wordwhen a cut falls inside a word (not when the voice goes on across the cut). The fillers that are cut (euh, um, uh, er, genre...) can be replaced inbrand.yamlunderspeech, which also takes avocabularylist of words to spell right. A misheard word can be fixed in the captions withspeech_captions.corrections, close spellings only, and every correction is listed for review (speech_corrections).
Visual devices (optional)
A reveal segment (a divider line sweeps a before picture away to show the after picture, from two real pictures or two clips, with labels that follow their side) for a before/after the viewer watches happen, a split segment (side by side or top and bottom, with labels such as FROM THIS and TO THIS) for comparisons, an image overlay with animation, frame and rotate (a photo or a review screenshot that comes in over the video as a card), a video overlay (picture-in-picture, with a rect, rounded or circle shape, border and shadow), sticker overlays (arrow, hand-drawn circle and underline, badge, emoji, image, with a pop or draw animation), a progress bar and a punch (instant zoom) on a clip. Use one or two per ad. Fields and rules: AGENTS.md and the editing guide.
Sound design (optional)
Sounds go over the audio bed of an ad: a whoosh on a cut, a pop on a caption, a ding on the price, a riser before the reveal.
adanvil sfx-fetch <project_dir>(orsfx_listwithfetch=true) builds a pack of about 18 short sounds in the project'ssfx/folder. Each one is downloaded from a pinned CC0 source (checked by SHA-1) or synthesised in code, then trimmed and set to one level. Nothing is stored in this repository; sources and licences are listed inexamples/sfx/README.mdand written tosfx/LICENCES.txt. Your own files insfx/work too.{"type": "sfx", "source": "sfx/pop.wav", "at": 3.2, "volume": 0.8}places a sound;audio.sfx_autoplaces them at the cuts and at the captions. Sounds never clip and never run past the end.music_beatsgives the beats of a track as it will play, andstoryboard_snapmoves the cuts onto them (the validator reports how many cuts are already on a beat).
Performance settings
ADANVIL_WORKERS: how many files, thumbnails or segments are processed at once. Defaultmax(1, min(4, cpus // 2)), and at most 2 when the machine has under 12 GB of RAM.ADANVIL_FFMPEG_THREADS: threads of each ffmpeg decoder or encoder. Defaultmax(1, cpus // workers).- Voice:
ADANVIL_VOICE_CHECK_MODELsets the Whisper model of the pronunciation check (unless the voice spec setscheck_model).ADANVIL_VOICE_THREADSsets the torch and Whisper threads (default: usable cores).ADANVIL_VOICE_CHECK_CONFIDENCEis the word confidence from 0 to 1 above which the alignment transcript replaces the check model (offto always run the check model; the spec'scheck_confidencewins).ADANVIL_VOICE_PERSISTENT=0goes back to one process per step instead of one worker per job. - Speech:
ADANVIL_SPEECH_MODEL(defaultbase),ADANVIL_SPEECH=off(no automatic transcription),ADANVIL_SPEECH_PROMPT(an optional text that biases the transcriber, for example towards keeping hesitations). - Other:
ADANVIL_CACHEmoves the cache folder;ADANVIL_FFMPEGandADANVIL_FFPROBEpoint to the binaries. - Benchmark:
uv run python scripts/bench.py --files 12 --seconds 4.
Responsible use
AdAnvil edits ads; you answer for what they say and show.
- Use real comments and real customer words only. The tool never invents an offer, a price, a guarantee or a claim: the human confirms them.
- Get the consent of the people who appear in your footage.
- Check the licences of the music and the footage you use (the demo pack is CC0, your own files may not be).
- Make sure every claim in the ad is true and allowed in your market.
- Do not clone a voice without the right to do so, and do not present a generated voice as a real customer.
Contributing
See CONTRIBUTING.md.
Licence and status
MIT, see LICENSE. Fonts, emoji and other third-party material are listed in THIRD_PARTY.md.
Status: alpha, version 0.1.0. Interfaces can still change. See CHANGELOG.md.
Metadata
Release files for adanvil 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| adanvil-0.2.0.tar.gz | 7.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| adanvil-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 8.8 MB
Release files / adanvil-0.2.0.tar.gz
| Download URL | adanvil-0.2.0.tar.gz |
|---|---|
| Size | 7.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d957121f1df222073cb6d853b2d6c87f0bb26dae4907c22be5d1e9274d07767
|
|
BLAKE2b-256 checksum How to use checksums |
a68344076761885a744726fb93fe786ba3038cb9a5050eb792b27ee816dcbe5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / adanvil-0.2.0-py3-none-any.whl
| Download URL | adanvil-0.2.0-py3-none-any.whl |
|---|---|
| Size | 1.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e9681623ea36a44ebd79b56cc980afaebe79d42e7d7a72290c429b6ac0e53cb1
|
|
BLAKE2b-256 checksum How to use checksums |
8d748bb6c7dc0ad2b4f65bc6f481a64e26c0f26e930b05865edd4e9fcdf33db7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|