Connect via CLI

Open ExpoCut → Settings → enable the connector. Copy the address and token it shows, then run one of these in your terminal:

claude mcp add --transport http expocut \
  http://<device-ip>:<port>/mcp \
  --header "Authorization: Bearer <token>"
The address and token are shown in ExpoCut → Settings once the connector is on. Replace <device-ip>, <port>, and <token> with those values.
Model Context Protocol · 263 tools · on-device

Drive ExpoCut with your AI agent.

A Model Context Protocol reference for ExpoCut's editing surface — 263 typed tools an AI assistant can call to plan, draft, and build videos with you, right on your phone.

263TYPED TOOLS
35FAMILIES
On-deviceNO CLOUD EDIT
Off by defaultPRIVATE LAN ONLY
Overview

A vocabulary, not an API.

ExpoCut is a mobile-first video editor. This page documents the conceptual tools, resources, and prompts an AI assistant can reference when guiding a creator. It mirrors the shape of the Model Context Protocol so agents can index it and reason about what ExpoCut can do.

Mobile-native

Hardware-accelerated rendering on iOS and Android with native, device-level video encoders.

Multi-track timeline

Unlimited video, audio, image, and text layers with frame-accurate trim, split, and ripple edit.

4K export

Up to 4K UHD at 60fps with H.264 / HEVC and configurable bitrate.

How it works

Three views of the same idea.

An AI assistant on your laptop talks to ExpoCut on your phone. The connector is a thin wrapper; the editor does the work. Here is the whole picture.

01 · Constellation
Every desk has an agent. Every pocket has ExpoCut.

Connect from any MCP-compatible client. The connector lives inside the App, accepts requests only from private or loopback network addresses, and exposes a typed tool surface the agent can call. Seven popular clients shown — bring your own.

MCP · LOCAL PREVIEW ExpoCut Claude ChatGPT Cursor Continue { } Cline Windsurf Z Zed App runs on your device
02 · Sequence
One sentence in. A render out.

A natural-language request flows through the agent, into the connector, through the editor's command bus, and back as an undoable, auditable result.

You creator "Slow-mo my Bali clips, add captions, duck music" AI Agent plans the edit Connector tool surface · local Editor runs the commands Result undoable · auditable prompt tool calls commands render
03 · Toolbelt
Eight families. One engine.

Every tool the agent can call maps to a real user action — composed by the same scene graph that powers the touch UI. Reach any family in one hop from the engine.

EXPOCUT Engine Project create · export Media import · stock Timeline addLayer · trim · speed Effect apply · transition Export render · share Audio stems · mix · duck AI captions · cutout Color grade · LUT
Quickstart

From zero to a rendered clip.

Turn the connector on, point your AI client at it, and let the assistant call the tools. Here is the shortest path to a finished video.

1

Enable the connector

In ExpoCut → Settings, switch on the in-app MCP server. It is off by default and accepts requests only from private or loopback network addresses on the device.

2

Connect your AI client

Point any MCP-compatible client at the local address ExpoCut shows. For Claude CLI:

claude mcp add --transport http expocut \
  http://<device-ip>:<port>/mcp \
  --header "Authorization: Bearer <token>"

Address and token appear in Settings once the connector is on.

3

Ask, then let it build

Describe the video. The assistant plans a sequence of tool calls — like the one below — and you watch it take shape on the canvas, every step undoable.

// 1 · start a project and open it in the editor
create_project { name: "Beach Reel", aspectRatio: "9:16", fps: 30 }
open_project   { id }

// 2 · add a stock clip, then a titled intro over it
add_stock_video_layer { query: "ocean sunrise", duration: 5 }
add_text_layer        { text: "SUMMER", fontSize: 96, verticalAnchor: "center" }
set_text_animation    { layerId, inId: "in-fade", inDurationSec: 0.6 }

// 3 · look at a frame, then render
describe_canvas { timeSec: 1.5 }   // cheap, image-free layout readout
capture_canvas  { timeSec: 1.5 }   // a real frame the model can see
export_project  {}                 // → file://…mp4

iOS: how to connect

ExpoCut and your AI client must be on the same Wi‑Fi network. iPhone and iPad also require Local Network access.

  1. In ExpoCut, open Settings → AI Agent (MCP Server) and turn on Enable.
  2. When iOS asks, choose Allow for Local Network. If the prompt was dismissed, enable ExpoCut under Settings → Privacy & Security → Local Network.
  3. Copy the displayed Server URL and Bearer Token into your MCP client, then send it a test request.
Concepts

Eight rules every tool follows.

Assume these conventions — they hold across the whole surface, so each tool's docs don't repeat them.

Time is secondsAll startTime / duration / *Sec arguments are in seconds.
Position is top-left %x / y are 0–100 % of the canvas; full-canvas layers pass stretchToCanvas: true.
Open a project firstLayer & setter tools need an open project — call create_project then open_project (it auto-navigates to the editor).
Editor must be mountedcapture_canvas, preview_filmstrip and export_project drive the live editor; open_project makes this just work.
Catalog before setterTools that take an id reject unknown ids with a hint pointing back to the matching list_* / get_effect_schema.
Z-order: 0 is frontNew layers land at trackIndex 0 (front); everything else shifts back. Use reorder_layer to restack.
Discover at runtimeCatalogs evolve — call list_effects, list_shaders, list_transitions, list_fonts, list_shapes… for the live set.
Every call is undoableEach tool call is recorded as a reversible step in the on-device edit history.
Tools

What an assistant can suggest.

Each tool maps to a real action a user performs in the ExpoCut UI. An AI assistant can compose them into a full editing plan.

create_project

Start a new project

Create a new edit at a target resolution and aspect ratio. Templates available for Reels, TikTok, YouTube, and Shorts.

namestring
aspectRatio"9:16" | "16:9" | "1:1" | "4:5"
resolution"1080p" | "2K" | "4K"
frameRate24 | 30 | 60
add_video_layer

Bring in clips

Import video, audio, or image media from the device gallery, Pexels stock, Freesound audio, or a remote URL.

source"gallery" | "pexels" | "freesound" | "url"
type"video" | "image" | "audio"
querystring (for stock sources)
add_text_layer

Place a layer on the timeline

Add a clip, photo, sticker, text, lower third, or animation overlay onto a track at a specific in-point.

layerType"video" | "audio" | "image" | "text" | "lowerThird" | "lottie" | "shape"
startseconds
durationseconds
trim_layer

Trim & split

Adjust in/out points or split a clip at the playhead. Ripple edits keep the rest of the timeline aligned.

clipIdstring
inseconds
outseconds
rippleboolean
set_layer_speed

Speed — slow-mo & fast-forward

Retime a video or audio clip. 2× plays twice as fast and shortens the clip; 0.5× is slow motion. Keep-pitch holds the audio natural, or let it varispeed.

layerIdstring
speed0.25–4 (1 = normal)
keepPitchboolean
set_layer_effect

Apply visual effects

Choose from 220+ effects and 220+ preset filters & looks — 7 parametric engines including Cinematic Grade (skin-tone protection, film halation, split-tone wheels), Warp & Distort, Retro Display, Edge & Sketch, Light FX, Cartoon and Light Flicker, plus blur, glitch, chroma key, vignette and cinematic LUTs. See them live →

clipIdstring
effectstring (effect id)
intensity0.0 – 1.0
set_layer_audio_transitions

Add transitions

Place one of 99 transitions between adjacent clips: fade, dip-to-black, slide, push, zoom, glitch, whip pan, shader wipes. See them live →

edge{ leftClipId, rightClipId }
stylestring
durationseconds (0.2 – 2.0)
add_lower_third_layer

Titles & captions

Add styled text or one of 180 animated lower thirds with custom font, color, position, and entrance.

preset"title" | "caption" | "lowerThird:"
contentstring
position{ x, y } (0 – 1)
set_layer_audio_effects

Mix & sweeten audio

Adjust volume, ducking, fade in/out, and apply 47 audio effects across 35 audio transitions.

trackIdstring
volume0.0 – 1.5
duckboolean
effectstring (effect id)
apply_global_color_grade

Color grade

Exposure, contrast, saturation, temperature, tint, shadows, highlights — applied per-clip or globally.

scope"clip" | "global"
params{ exposure, contrast, saturation, temp, tint, shadows, highlights }
export_project

Render & export

Encode the final timeline using the on-device hardware encoder. Saves to camera roll or shares via system sheet.

resolution"720p" | "1080p" | "2K" | "4K"
codec"h264" | "hevc"
bitratenumber (Mbps)
capture_canvas

Capture a frame · with debug views

Render the current canvas at a given playhead position and return a self-describing screenshot — project size, total length, and the layers actually visible at that frame (with their computed bounding boxes in canvas %). Toggle xray to dim the composition under labeled layer boxes, outlinesOnly for a borders-only view, and grid for a 10%-step coordinate overlay with % labels — pin-pointing layout bugs in one tool call.

timeSecnumber (optional, defaults to playhead)
maxWidthnumber (64–1024, default 512)
format"png" | "jpg"
xrayboolean · dim + labeled boxes
outlinesOnlyboolean · borders only
gridboolean · 10% coordinate grid
analyze_pixels

Read pixels at a point

Sample the rendered canvas — useful for AI vision tools that need to check what's actually on-screen.

region{ x, y, w, h } (0 – 1 normalized)
timeseconds
tts_validate_script

Validate a TTS script

Run a script through the TTS pre-flight: catches unsupported characters, unpronounceable tokens, and estimated duration.

scriptstring
voicestring (voice id)
get_effect_schema

Discover effect parameters

Look up the typed parameter schema for any effect, filter, or transition so an assistant can compose valid calls.

idstring (effect id)
set_layer_background_remover

Remove background

On-device segmentation. Produces an alpha mask on a clip or image without uploading frames.

layerIdstring
feather0.0 – 1.0
add_caption_layer_from_audio

Generate captions

Transcribe spoken audio on-device and place time-aligned caption layers on the timeline, styled with a chosen preset. See transcript styles live →

trackIdstring
stylePresetstring
transcribe_audio

Transcribe audio to text

Run on-device speech-to-text against an audio layer and get back time-aligned segments — input to captions, chapters, or summaries.

layerIdstring
language"auto" | locale
reorder_layer

Reorder / hide layers

Change z-order, bring-to-front, send-to-back, or temporarily hide a layer for A/B compare.

layerIdstring
op"front" | "back" | "up" | "down" | "hide" | "show"
add_widget_layer

Add a dynamic widget

Place a countdown timer or stopwatch as a first-class timeline layer with editable target time and styling.

kind"countdown" | "stopwatch"
startseconds
durationseconds
style{ font, color, size }
import_foreign_file

Import a third-party project

Ingest a motion-graphics template, an editing-project XML, an animated-graphic JSON, or a color file — parsed entirely on-device into a native ExpoCut project.

payloadbase64 | { uri }
hintformat hint (optional)
list_shapes · add_shape_layer

Add a shape

Browse the full library of 91 shape presets — basic geometry, arrows, stars & bursts, callouts & speech bubbles, badges, ribbons, lines and rulers — and drop any one on the timeline with fill, gradient, or stretch-to-canvas.

shapeshape preset id (91 options)
fillColorhex
gradientColors[hex, …]
list_brushes · add_brush_layer

Add a brush stroke

Browse 134 hand-drawn brush strokes across 14 categories — arrows, encircles & frames, underlines & highlights, sparkles, neon one-liners, comic speedlines, lightning, splats, ink, marker, paint, dry brush, spray and scribbles — and drop any one on the timeline. Brushes are vector shapes, so fills, gradients, outlines and glow all apply.

brushbrush id (134 options)
fillColorhex
gradientColors[hex, …]
set_layer_fade_mask

Fade on edge

Apply a spatial gradient alpha fade to any image, video, or shape — a directional linear gradient, a radial vignette, or a four-edge inset feather. Distinct from the time-based fade-in / fade-out ramps.

layerIdstring
mode"linear" | "radial" | "inset"
softness0.0 – 1.0
list_light_leaks · add_light_leak_overlay

Light leak overlay

Browse the light-leak catalog — warm sunset, cool window, prism, neon, sun flare and more — and lay a real-footage leak over the current content on its own track, with adjustable blend intensity. See light leaks live →

presetIdstring (from list_light_leaks)
intensity0.0 – 1.0
durationseconds
Sequence automation

Assemble, tighten, clean, and light a cut.

These editing primitives are callable directly from an agent: build a multi-scene sequence, remove dead air, reduce a long video to its strongest passages, clean dialogue, and apply localized shader or lighting treatments without leaving the project.

add_clip_sequence

Build a sequence

Compose a multi-scene edit from local, stock, or remote clips with per-clip duration, transitions, and optional audio.

auto_remove_silence · long_video_to_short

Auto-edit the pacing

Detect and ripple-delete silent gaps, or select the most energetic passages up to a target duration while preserving the source layer.

clean_audio

Clean dialogue

Run on-device denoise and speech enhancement on a video or audio layer, with the result available to the same export pipeline.

set_layer_shader_filter · remove_layer_shader_filter · clear_layer_shader_filters

Layer shader filters

Apply the 7 parametric filter engines by named Look preset (e.g. "Teal & Orange") or raw params, stack up to 4 different engines per layer with append, and remove one or all — same render path as the canvas and export.

add_light_region · list_light_regions · clear_light_regions

Localized lighting

Add, inspect, or clear area, gradient, radial, object, and trident lighting regions for a layer without changing the base grade.

Recently shipped

Brushes, junction transitions & frame-accurate render tools.

The newest additions: a 134-stroke brush library (list_brushes · add_brush_layer), cut-level junction transitions (set_junction_transition), a virtual camera (set_camera), and off-screen render tools for export-parity checks — on top of the importer, codec, color & audio batch below. Each is callable from any MCP-compatible client.

list_brushes · add_brush_layer

Brush strokes

134 hand-drawn vector strokes in 14 categories — arrows, encircles, underlines, sparkles, neon one-liners, comic speedlines, lightning — added as layers with the full fill / gradient / outline / glow system.

render_composited_frame · capture_export_frame · verify_export_parity

Frame-accurate verification

Render a fully-composited frame off-screen, grab the exact encoder frame, and diff it against the editor canvas — agents catch preview↔export drift before delivery.

import_foreign_file · import_from_file_system · detect_format · migrate_template

Importer

Ingest a motion-graphics template, an editing-project XML, an animated-graphic JSON, or a color file — from an inline payload or from the local filesystem. Sniff the format, migrate the data model, and route to the right parser.

query_codec_support · default_device_class · resolve_codec_fallback

Codec compatibility

Ask whether a target codec, profile, and container will play on a given device class — and degrade-and-warn when it won't, so the agent's export plan never produces a file the device can't open.

measure_loudness · gain_to_loudness_target · list_loudness_targets · build_loudness_plan

Loudness pipeline

Measure integrated loudness, compute the gain needed to hit a broadcast or streaming target (EBU R128, ATSC A/85, –14 LUFS), and produce a normalization plan ready to apply to the timeline.

parse_cube_lut · write_cdl · bake_cdl_to_cube

Color science

Parse a .cube LUT, write an ASC CDL/CCC color-decision file, and bake a CDL into a 3D LUT — round-trip color from grading session to final render.

resolve_font_fallback · audit_font_coverage · list_font_aliases

Font fallback

Given a font name and a glyph set, resolve to the best available family on the current device and report coverage gaps — keeps imported templates legible everywhere.

export_template_to_lottie · export_template_to_fcpxml

Cross-format export

Round-trip an ExpoCut template out to Lottie JSON (with animated keyframes) or to FCPXML (with embedded CDL) — let an agent hand a finished project back to the user's NLE.

Just shipped

Keyframes — animate any property on image, text, shape & video.

The keyframe substrate now accepts image, text, shape, and video layers for the shared transform / opacity / fx / border / mask / color surface. Nine new MCP commands expose every authoring path the in-app keyframe editor uses — scalar tracks, held-step discrete tracks, snapshot columns, easing curves, and bulk plans.

keyframe_add

Add a scalar keyframe

Place a value on any animatable scalar property at a given time. The optional interp chooses the easing curve leaving the keyframe.

layerIdstring
propertyPropertyPath (40 options)
timeMsnumber (≥ 0)
valuenumber
interp{ type, … } optional
keyframe_add_discrete

Add a held-step keyframe

For properties that don't interpolate — font weight, italic, mask shape, transition id, fill style. Value type follows the property (string / number / boolean).

propertyDiscretePropertyPath (12 options)
timeMsnumber
valuestring | number | boolean
keyframe_remove_at

Delete a column

Remove every keyframe at a given time across all tracks — mirrors the editor's "delete column" UX. Times match with ~1 ms tolerance.

layerIdstring
timeMsnumber
keyframe_clear

Reset animation

Clear all keyframes on a layer, or just one property. The "Reset" button as a tool call.

layerIdstring
propertyPropertyPath (optional)
keyframe_list

Inspect a layer's animation

Return the full LayerAnimation JSON — every scalar track, position track, and discrete track. Use before authoring to avoid double-keyframing.

layerIdstring
keyframe_set_interp

Refine an easing curve

Replace the easing leaving an existing keyframe without touching the value. Use to retune motion without re-adding keyframes.

propertyPropertyPath
timeMsnumber
interp{ type: "hold" | "linear" | "bezier" | "preset", … }
keyframe_snapshot_at

Capture current state

Take a snapshot of the layer's current transform + opacity + colour values and write a column of keyframes at the given time — useful as the "before" state before authoring a change.

layerIdstring
timeMsnumber
keyframe_set_animation

Bulk-replace the whole animation

Submit a full LayerAnimation JSON to replace every track on a layer in one call. Validates each track's property against the target layer type — returns PropertyMismatch on a bad pairing.

layerIdstring
animation{ tracks, positionTrack?, discreteTracks? }
list_keyframe_properties

Catalog animatable properties

Catalog every animatable property — scalar and discrete — with the layer types each applies to. Optional layerType filter narrows to image / text / shape / video / audio.

layerType"image" | "text" | "shape" | "video" | "audio" (optional)

Which properties apply where

Call list_keyframe_properties { layerType } for the exact list. Authoring an invalid pair returns PropertyMismatch.

Property family image text shape video audio
transform.* · opacity
color.* · fx.* · filter.*
border.* · mask.*
transition.*
text.* (color/stroke/font/align)
shape.fillStyle
audio.volume

Audio layers keyframe audio.volume directly — but most audio shaping is generated, not hand-keyed. The on-device stem-separation suite below writes ducking envelopes onto the music layer, drives any layer property from a drum/bass envelope, and returns beat-cut times — no manual keyframes required.

Easing curves

Every interp argument accepts one of four shapes. Times are integer microseconds internally — pass milliseconds via the MCP boundary.

// hold — value snaps; useful for discrete-like behaviour on a scalar track
{ "type": "hold" }

// linear — straight-line interpolation
{ "type": "linear" }

// CSS-compatible cubic-bezier (P1.x & P2.x clamped to [0,1]; P1.y / P2.y may overshoot)
{ "type": "bezier", "x1": 0.42, "y1": 0, "x2": 0.58, "y2": 1 }

// Named preset (allow-listed)
{ "type": "preset", "name": "easeInOut" }
// presets: "ease" | "easeIn" | "easeOut" | "easeInOut" | "bounce" | "elastic" | "spring"

Example — fade in + scale-up on a text layer

Pop the layer in over 600 ms with spring easing on scale and a linear opacity ramp.

keyframe_add  { layerId: "text_abc", property: "opacity",         timeMs:   0, value: 0 }
keyframe_add  { layerId: "text_abc", property: "opacity",         timeMs: 600, value: 1 }
keyframe_add  { layerId: "text_abc", property: "transform.scale", timeMs:   0, value: 0.4,
                interp: { type: "preset", name: "spring" } }
keyframe_add  { layerId: "text_abc", property: "transform.scale", timeMs: 600, value: 1 }

Or use keyframe_set_animation with the full LayerAnimation JSON to author the same plan in one call. Call list_keyframe_properties { layerType: "text" } first to see every property a text layer accepts.

Mask authoring

Masks are geometry, timing, and finish.

Use one mask contract for canvas preview, the demo player, and the native MP4 encoder. Set the shape first, then animate only the properties that should change. The renderer reads the same normalized values and keyframe interpolation on every surface.

set_layer_mask

Shape + invert + mask finish

Supports rectangle, ellipse, triangle, star, heart, cross, xShape, linear, mirror, radial, angular, diamond, path, and text. Pass rect:{x,y,width,height} in normalized layer-local 0..1 space; rotation is degrees, bandWidth is 0..1 for Mirror, feather is pixels at 1080p, and invert:true shows the outside of the shape.

effectzoomIn / zoomOut / pulse / breathe / pan / kenBurns / spin / sway / shake + intensity, speed, curve
morphwater / wave / sand / air / stone / particles / marbles / wires between shape keyframes
mask keyframes

Animate the reveal without losing state

Use keyframe_add for numeric mask.rect.x/y/width/height, mask.rotation, mask.bandWidth, mask.feather, and mask.expansion. Use keyframe_add_discrete for mask.shape and mask.invert. Each keyframe owns its complete mask snapshot; do not reuse the last frame's object when creating the next one.

A curve is meaningful only between two keyframes (1 → 2, or 1 → 2 → 3). Select a mask curve after the second keyframe; the chosen easing is stored on the outgoing segment and is evaluated identically by canvas, demo, and export. A single keyframe has no transition and must stay linear/inactive.

create_mask_animation

Author a mask pattern or custom graph

One call creates the selected mask shape, generates a shape-aware sample pattern, and writes real mask keyframes. Choose a named curve or cubic-bezier curve. For original work, send keyframes nodes with mask geometry plus opacity, layer rotation, filter look, invert, feather, and expansion.

patternreveal / pulse / sweep / elastic or shape:sample:name
keyframescustom graph: timeMs + mask + opacity + rotation + filter
receiptpattern, curve, keyframe count, written tracks

Verification loop: call keyframe_list, inspect with capture_canvas at the transition times, then call verify_export_parity before delivery. For a static mask, omit keyframes. Removing the final keyframe should leave the layer at its default unmasked state rather than a stale mask snapshot.

Templates & community

Save locally, or share a reusable edit.

These are two different operations. Keep a working copy in My Templates for reuse on the device; submit a full-fidelity snapshot only when the author explicitly wants a community share.

save_project_as_template

Save to My Templates

mode:"template" (default) turns text/media into editable slots and keeps the reusable recipe. mode:"snapshot" preserves masks, invert/mirror geometry, effects, morphs, keyframes, looks, styles, media references, and export settings exactly as authored. This writes to the on-device template library and does not publish anything.

submit_template

Share to Community

Snapshots the open project, validates it, packages safe local assets into an .ectpl, and shares it with the ExpoCut Community. An optional thumbnail and rendered demo MP4 can be included. Private video/image/audio URIs are stripped by default; masks, effects, styles, looks, and keyframes remain.

Retention is once, 1d, 7d, 21d, or 30d. Save the returned ownerKey; then use list_my_submissions and delete_my_submission to manage only uploads from this device.

Recommended agent flow: save_project_as_template(mode:"snapshot") → render/capture a demo and thumbnail → ask for explicit permission to share source media → submit_template → return the share URL and expiry.

Just shipped

On-device audio intelligence — split a song into stems.

Most editors keyframe one thing on audio: volume. ExpoCut splits any clip into vocals · drums · bass · other stems on the device — nothing uploaded — then composes that into karaoke tracks, dialogue boosts, sidechain ducking, beat-synced cuts, and audio-reactive motion. Eight tools, all non-destructive (the source clip is never touched; stems are added as their own layers).

separate_audio

Split into stems

Separate a clip into vocals / drums / bass / other as four new audio layers. tier:"fast" is a quick, model-free split; tier:"studio" is a high-quality neural split (falls back to fast if it can't run). Runs entirely on-device.

layerIdstring
tier"fast" | "studio"
stemsstring[] (optional subset)
remove_vocals

Karaoke / instrumental

One call: split and mute the vocal stem, leaving a clean instrumental bed. Non-destructive.

layerIdstring
tier"fast" | "studio"
isolate_voice

Dialogue boost

Raise the vocal stem and duck the rest — clean up talking-head and interview clips where the voice sits under the music.

layerIdstring
boostDbnumber (default 6)
set_stem_mix

Per-stem faders

Set volume (0–1) and/or mute on already-separated stems — push the drums, pull the vocal back. Faders write to the stem layers, so it stays non-destructive.

sourceLayerIdstring
mix{ stem, volume?, muted? }[]
beat_cut_from_drums

Beat-synced cut times

Detect onsets on the isolated drum stem and return cut times in ms — feed them straight into split_layer or set_layer_transition for an edit that lands on the beat.

sourceLayerIdstring
maxCutsnumber (keep strongest N)
set_audio_reactive

Sound-reactive motion

Bind a stem's loudness envelope to a layer property — zoom-pulse on the kick, flash on the snare, wobble on the bass. The motion follows the music automatically.

targetLayerIdstring
stem"drums" | "bass" | …
presetId"bass-zoom-pulse" | …
auto_duck

Sidechain ducking

Detect where the voice is active and write volume keyframes that dip the music underneath it — broadcast-style ducking without drawing a single envelope.

musicLayerIdstring
vocalSourceLayerIdstring
duckDbnumber (e.g. -12)
swap_music

Copyright-safe strip

Mute the instrumental stems so you can re-layer a licensed track — keep the original vocal/performance, replace the bed.

sourceLayerIdstring
includeDrumsboolean

Example — beat-synced, voice-forward edit

Split the track once, then duck the music under the voiceover and pull cut times off the drums — three calls.

separate_audio       { layerId: "music_0", tier: "studio" }
auto_duck            { musicLayerId: "music_0", vocalSourceLayerId: "vo_0", duckDb: -12 }
beat_cut_from_drums  { sourceLayerId: "music_0", maxCuts: 8 }   // → cutTimesMs: [508, 1016, …]

All eight live under the Audio separation · stems family. Separation, detection and mixing all run on-device — audio never leaves the phone.

Just shipped

Let agents invent new effects, LUTs, borders & masks.

When no built-in preset matches the brief, the agent submits a JSON spec and a brand-new asset is admitted to the registry — gated behind a permission setting (Off · Safe · Standard · Full), a static validator, an automated shader probe-pass, per-session quota, and an on-device audit log. All 14 authoring commands are live; flip the tier in Settings → "AI may author assets" to enable.

fx_register_spec

Author a new FX / filter / transition

Submit a full FxSpec — declarative timeline or shader fragment. Validates shape, checks id format, runs the shader probe, then admits to the registry. The id can immediately be used by set_layer_effect.

specFxSpec v1.0
persistboolean (default false)
overwriteIfExistsboolean (default false)
idempotencyKeyuuid (optional)
fx_compose_from_template

Build from a curated family

Lower-risk path: pick a template (e.g. transition.distortion.twirl) and override its variant params. Server materializes the spec — agent never writes raw shader code.

templateIdstring
namestring ≤ 64
params{ [key]: number }
durationMs100–8000
fx_dry_run

Validate without persisting

Validate + sample-evaluate the declarative timeline at t ∈ {0, 0.25, 0.5, 0.75, 1}. Returns the per-track output. Use as the first call when authoring — never writes to the registry.

specFxSpec v1.0
sampleTimesnumber[] (≤ 33)
fx_list_custom · fx_delete_custom

Audit + clean up

List or remove AI-authored specs. Built-in specs are immutable — delete refuses with PermissionDenied. Pass alsoFromDisk: true to delete the persisted file.

author"ai" | "user" | "any"
idstring
alsoFromDiskboolean
lut_register_custom · lut_list_custom · lut_delete_custom

Author a custom LUT

Submit a .cube text payload or a declarative grade descriptor (white balance + tone curve + lift/gamma/gain). LUT_3D_SIZE capped at 33 for AI submissions.

cubeTextstring (≤ 1 MB) — variant A
grade{ whiteBalance, toneCurve, lift } — variant B
border_register_preset · border_list_presets · border_delete_preset

Save a named border preset

Persist a full BorderConfig under an id so the agent can reuse the same look across layers. set_layer_border learns a new presetId argument.

id^(ai|user)\.[a-z0-9-]{1,48}$
configBorderConfig
mask_register_shape · mask_list_custom · mask_delete_custom

Add a new mask shape

Accepts rect, ellipse, polygon (≤ 256 vertices), or path (limited SVG d — M / L / Q / C / Z only). JS bakes geometry to a coverage bitmap; no new native opcodes needed.

geometry{ kind, …shape-specific }
feathernumber px @ 1080p
invertboolean

Permission tiers

A single App setting controls which authoring paths are available. The default is Off. Shader admission is opt-in via the Full tier and a per-session in-app confirmation.

Tier FX declarative FX shader LUT .cube LUT grade Border Mask
Off (default)
Safe rect · ellipse
Standard all geometry
Full ✓ + confirm all geometry

Guard ladder — five layers

Every registration request passes through all five before the asset is admitted. Failure at any layer aborts the request and writes an audit-log entry.

01 · STATIC
Schema validator

Allow-listed kinds & categories. Shader ≤ 32 KB. Expr ≤ 1024 chars. Polygon ≤ 256 vertices. Path commands restricted to M / L / Q / C / Z.

02 · PROBE
Runtime sanity

Shader compiles on a 64×64 off-screen surface with a 250 ms timeout. LUT sample-tested for NaN. Mask rasterized to verify non-empty coverage.

03 · QUOTA
Resource caps

50 registrations per session. 256 FX · 64 LUT · 128 border · 32 mask per install. 10 calls/minute rate limit. LRU eviction when caps hit.

04 · TIER
Permission gate

App setting (Off / Safe / Standard / Full) decides which surfaces the call may touch. persist: true always requires Standard or Full + a user confirmation.

05 · AUDIT
Reversibility

Every admit / reject / delete logged to the on-device audit log. AI Operations screen exposes Disable + Remove for any AI-authored asset.

Failure modes the agent will see

Every tool returns { ok: false, error, message } with one of these stable codes — never a bare exception. Build retries against the code, not the message.

Disabled InvalidSpec InvalidId AlreadyExists NotFound EvalFailed ProbeFailed PersistFailed DeleteFailed PermissionDenied OverQuota RateLimited NotImplemented

Known limitations

Example — register a declarative transition

Lightweight Y-axis whip-pan transition, authored entirely as keyframes — no shader code.

{
  "FXVSN": "1.0",
  "id": "ai.whip-pan-y-soft",
  "name": "Whip Pan Y · soft",
  "author": "ai",
  "kind": "transition",
  "category": "Transition",
  "renderer": "declarative",
  "timeline": {
    "duration": "auto",
    "tracks": [
      { "param": "translateY",
        "keyframes": [[0, 0], [0.4, -120], [0.6, 120], [1, 0]],
        "easing": "easeInOutCubic" },
      { "param": "blur",
        "keyframes": [[0, 0], [0.5, 8], [1, 0]] }
    ]
  }
}

Dry-run first via fx_dry_run, then register with fx_register_spec { spec, persist: false }, then apply via set_layer_effect { layerId, effectId: "ai.whip-pan-y-soft" }. Each authoring surface is admitted through a typed validation + guard pass before it can touch a project.

Full surface

All 263 tools, by family.

Every command the connector registers. Names are the canonical tool IDs — copy-paste into your agent's tool catalog. Green chips are recently shipped.

Projects

8 TOOLS
list_projects create_project open_project get_active_project save_project rename_project duplicate_project delete_project

Layers · basic

8 TOOLS
list_layers add_text_layer add_shape_layer add_brush_layer add_shape_widget add_layout update_layer remove_layer

Media layers

5 TOOLS
add_video_layer add_image_layer add_audio_layer add_svg_layer add_device_mockup

Stock & music

9 TOOLS
stock_search_photos stock_search_videos stock_curated_photos stock_popular_videos add_stock_image_layer add_stock_video_layer stock_search_music stock_trending_music add_stock_music_layer

Cuts & order

5 TOOLS
trim_layer set_layer_speed split_layer reorder_layer set_layer_visibility

Effects & polish

23 TOOLS
set_layer_effect set_layer_filter set_layer_video_effects set_layer_audio_effects set_layer_audio_transitions set_layer_audio_offset set_layer_border set_layer_border_glow set_shape_glow set_shape set_shape_fill set_shape_outline set_shape_style set_layer_color_adjust set_layer_fade set_layer_crop set_layer_chroma_key set_layer_lut set_layer_shader_filter remove_layer_shader_filter clear_layer_shader_filters set_layer_volume_keyframes set_layer_overlay_color

Masks & procedural

13 TOOLS
set_layer_mask create_mask_animation set_layer_background_remover add_generative_bg_layer add_procedural_filter_layer set_layer_fade_mask clear_layer_fade_mask add_light_leak_overlay set_shader_source clear_shader_source add_light_region list_light_regions clear_light_regions

Transitions

6 TOOLS
set_layer_transition clear_layer_transition list_junctions set_junction_transition clear_junction_transition

Two kinds of transition. Layer tools set a clip's own entrance / exit. Junction tools set the blend on the cut between two adjacent clips — a different thing, addressed by the pair of layers that meet there rather than by one layer.

Color

1 TOOL
apply_global_color_grade

Text & animation

8 TOOLS
add_lottie_layer add_lower_third_layer set_text_animation set_typewriter set_layer_scale_xy list_lower_thirds list_text_animations add_widget_layer

TTS & Speech-to-Text

5 TOOLS
tts_add_audio_layer tts_validate_script tts_list_voices transcribe_audio add_caption_layer_from_audio

Audio separation · stems

8 TOOLS
separate_audio remove_vocals isolate_voice set_stem_mix beat_cut_from_drums set_audio_reactive auto_duck swap_music

Sequences & auto-edit · NEW

4 TOOLS
add_clip_sequence auto_remove_silence long_video_to_short clean_audio

Catalogs

20 TOOLS
list_effects get_effect_schema list_transitions list_widgets list_video_effects list_audio_effects list_audio_transitions list_shapes list_brushes list_light_leaks list_fonts list_rtl_fonts list_text_styles list_text_effects list_shaders list_layouts list_shape_styles list_shape_textures list_mesh_gradients list_border_presets

Canvas, preview & render

14 TOOLS
get_canvas_info describe_canvas capture_canvas preview_filmstrip render_still get_render_status render_composited_frame capture_project_base_frame compose_single_frame capture_export_frame analyze_pixels set_export_settings export_project seek

Importer · NEW

5 TOOLS
import_foreign_file import_from_file_system detect_format migrate_template parse_cube_lut

Codec · NEW

3 TOOLS
query_codec_support default_device_class resolve_codec_fallback

Loudness · NEW

4 TOOLS
measure_loudness gain_to_loudness_target list_loudness_targets build_loudness_plan

Color writers · NEW

2 TOOLS
write_cdl bake_cdl_to_cube

Cross-format export · NEW

2 TOOLS
export_template_to_lottie export_template_to_fcpxml

Font fallback · NEW

3 TOOLS
resolve_font_fallback audit_font_coverage list_font_aliases

FX Authoring · NEW

5 TOOLS
fx_register_spec fx_compose_from_template fx_dry_run fx_list_custom fx_delete_custom

LUT Authoring · NEW

3 TOOLS
lut_register_custom lut_list_custom lut_delete_custom

Border Authoring · NEW

3 TOOLS
border_register_preset border_list_presets border_delete_preset

Mask Authoring · NEW

3 TOOLS
mask_register_shape mask_list_custom mask_delete_custom

Keyframes · NEW

9 TOOLS
keyframe_add keyframe_add_discrete keyframe_remove_at keyframe_clear keyframe_list keyframe_set_interp keyframe_snapshot_at keyframe_set_animation list_keyframe_properties

Motion path & text-2 · NEW

19 TOOLS
set_layer_motion_path clear_motion_path list_motion_path_presets update_text set_text_font set_text_style set_text_effect set_text_style_runs clear_text_style_runs set_text_shade set_text_wrap_box set_text_writing_direction set_text_path clear_text_path set_text_animation_range set_text_font_variations set_text_font_features set_text_media_fill clear_text_media_fill

Audio companion & layout · NEW

11 TOOLS
mute_video_audio unlink_video_audio relink_video_audio set_stretch_pan set_layer_fit_mode set_layer_anchor set_aspect_lock set_blur_fill set_shape_canvas_relative_size get_resize_review_queue clear_resize_review

Advanced compositing · NEW

12 TOOLS
set_layer_parent set_layer_is_null set_track_matte clear_track_matte set_layer_is_adjustment set_time_remap clear_time_remap set_layer_blend_mode set_layer_cdl set_working_color_space set_layer_alpha_mode set_layer_alpha_packing

Widget config · NEW

3 TOOLS
update_widget_config get_widget_config bar_chart_race.set_data

Templates & Reels · NEW

11 TOOLS
list_templates apply_template import_template_json save_project_as_template submit_template list_my_submissions delete_my_submission set_template_theme list_reels apply_reel swap_reel_theme

Brand profiles · NEW

6 TOOLS
list_brand_profiles get_brand_profile create_brand_profile update_brand_profile delete_brand_profile apply_brand_profile_to_project

Session control · NEW

16 TOOLS
select_layer get_selection play pause playback_status list_tracks set_track_visibility set_track_mute set_track_volume reorder_track save_history_checkpoint undo redo undo_to_checkpoint list_checkpoints seek_seconds

Introspection & verify · NEW

5 TOOLS
get_layer get_layer_schema get_project set_camera verify_export_parity

Builtins

2 TOOLS
ping echo
No tools match that term. Try a feature word like shape, caption, glow, or export.

Param schemas live in llms-full.txt. Call get_effect_schema at runtime for the canonical signature of any effect.

Creative recipes

After Effects-class motion. CapCut-class speed.

ExpoCut's tool surface composes into the same moves agents already know from desktop NLEs and social editors. Below: ten copy-paste plans an AI assistant can drop on the timeline to produce highly creative output — images and shapes animate as first-class citizens via the keyframe substrate.

After Effects · Scale Pop

Logo / image scale-in with bounce

Animate any image or shape from 0 → 1.08 → 1.0 with an overshoot — the AE classic in three keyframes. interp presets do the easing math.

keyframe_add { layerId, property: "transform.scale", timeMs: 0,    value: 0.0, interp: { type: "preset", name: "easeOut" } }
keyframe_add { layerId, property: "transform.scale", timeMs: 420,  value: 1.08, interp: { type: "preset", name: "easeIn" } }
keyframe_add { layerId, property: "transform.scale", timeMs: 560,  value: 1.0 }
After Effects · Position Travel

Shape slides across with motion blur fake

Move a shape left → right while ramping fx.blur for a streak feel — both tracks are scalar keyframe properties on the same layer.

keyframe_add { layerId, property: "transform.x", timeMs: 0,    value: -40 }
keyframe_add { layerId, property: "transform.x", timeMs: 600,  value: 50, interp: { type: "preset", name: "easeInOut" } }
keyframe_add { layerId, property: "fx.blur",     timeMs: 0,    value: 0.6 }
keyframe_add { layerId, property: "fx.blur",     timeMs: 600,  value: 0.0 }
After Effects · Spin Loop

Continuous rotation on a shape badge

Two keyframes, linear interp — drop in any seal / record-button / loading element. Replay keyframe_set_interp to ease.

keyframe_add { layerId, property: "transform.rotation", timeMs: 0,    value: 0 }
keyframe_add { layerId, property: "transform.rotation", timeMs: 2000, value: 360 }
After Effects · Mask Wipe

Image revealed by an expanding rectangle

Animate the mask rect from 0% → 100% width — the classic AE box-reveal. Combine with mask.feather for a soft edge.

set_layer_mask  { layerId, shape: "rectangle", rect: { x: 0, y: 0, width: 0, height: 1 }, feather: 4 }
keyframe_add    { layerId, property: "mask.rect.width", timeMs: 0,   value: 0 }
keyframe_add    { layerId, property: "mask.rect.width", timeMs: 700, value: 1, interp: { type: "preset", name: "easeOut" } }
On-device Stems · Beat Sync

Cut on every real onset of the drum stem

No BPM guesswork — separate the track, run onset detection on the isolated DRUM stem, then split the visuals at the actual hits.

separate_audio         { layerId: "music", tier: "fast" }
beat_cut_from_drums    { sourceLayerId: "music", sensitivity: 1.2, maxCuts: 6 }
# → { cutTimesMs: [508, 1016, 1524, 2031, 2554, 3070] }
split_layer            { layerId: "v0", atSec: 0.508 }  # atSec is relative to the layer's own startTime
split_layer            { layerId: "v1", atSec: 0.508 }
set_layer_transition   { layerId: "v1", out: { id: "slicewipe", durationSec: 0.18 } }
Cuts · Junction transitions

Put the blend on the cut, not on the clip

A junction is the seam where two clips touch. It is derived from clip adjacency, so it has no permanent id — list_junctions resolves the current cuts, and the setter addresses one by the pair of layers that meet there. This is also the only route to the reveal's origin and its rim refraction.

list_junctions {}
# → { count: 1, junctions: [{ fromLayerId: "v0", toLayerId: "v1", atSec: 4,
#       maxDurationSec: { center: 4, start: 4, end: 4 }, transition: null }] }

# A droplet reveal that opens from where the action is, not from dead centre.
set_junction_transition {
  fromLayerId: "v0", toLayerId: "v1",
  effectId: "wipe.iris",
  durationSec: 0.6,
  params: { originX: 0.82, originY: 0.71, bulge: 0.8 }
}
# → resolvedOptions echoes the post-clamp values the shaders actually run,
#   and durationClamped tells you if the cut could not hold what you asked for.

# Directional motion smear instead of an even blur.
set_junction_transition {
  fromLayerId: "v1", toLayerId: "v2",
  effectId: "distort.warp",
  params: { blur: 0.6, blurAniso: 1, blurAngle: 45 }
}

Params are merged onto whatever the cut already had, so adjusting one option never resets the rest of the look. clear_junction_transition puts the straight cut back.

CapCut · Auto Captions

On-device captions, time-aligned to the source audio

Whisper transcribes the spoken language and the segments land straight into a positioned transcript layer — one call, bottom-safe by default.

add_caption_layer_from_audio {
  uri: "voiceover.wav",
  language: "auto",
  startTime: 0,
  x: 50, y: 82
}
CapCut · Zoom Punch

Snap-zoom into a clip on the drop

Hold scale at 1.0, then snap to 1.18 at the drop with a hard easeIn — the CapCut "punch" trick. Reset at the next cut.

keyframe_add { layerId, property: "transform.scale", timeMs: 0,    value: 1.0 }
keyframe_add { layerId, property: "transform.scale", timeMs: 980,  value: 1.0, interp: { type: "preset", name: "easeIn" } }
keyframe_add { layerId, property: "transform.scale", timeMs: 1020, value: 1.18 }
keyframe_add { layerId, property: "transform.scale", timeMs: 1480, value: 1.0 }
DaVinci-style · Color

Teal-orange cinematic grade with LUT bake

Bake an ASC-CDL (slope/offset/power/saturation) to a .cube LUT, register it as a custom LUT, then apply it to every video/image clip in one pass — the round-trip a colorist would use.

bake_cdl_to_cube    {
  cdl: { slope:[1.05,1.02,0.96], offset:[0.01,0.00,-0.02], power:[1.0,1.0,1.05], saturation: 1.12 },
  size: 33
}
# → { cube: "TITLE \"...\" LUT_3D_SIZE 33 ...", size: 33 }
lut_register_custom { id: "cinematic-teal-orange", name: "Cinematic Teal-Orange", cubeText: "<cube text from above>" }
apply_global_color_grade { lutId: "cinematic-teal-orange", lutIntensity: 0.65 }
Agent-authored FX

Compose a custom whip-pan from a curated template

No raw GLSL — pick a curated distortion family, override its typed params, and the spec is immediately usable as an effectId.

fx_compose_from_template {
  templateId: "transition.distortion.warp",
  name: "Whip Pan Soft",
  params: { strength: 1.4, angle: 90 },
  durationMs: 450
}
# → { ok: true, id: "ai.whip-pan-soft-..." }
set_layer_effect  { layerId, effectId: "ai.whip-pan-soft-..." }
On-device Segmentation · Bg Replace

Cut subject out, slot onto a generative backdrop

ML person segmentation removes the original background; an animated shader gradient sits underneath. Zero uploads, zero green screen.

set_layer_background_remover { layerId: "subject", quality: "balanced", featherPx: 3 }
add_generative_bg_layer      { effectId: "auroragradient", preset: "Soft Violet", startTime: 0, duration: 30 }
reorder_layer                { layerId: "subject", position: "front" }

Every recipe is a pure tool composition. Agents can mix them: e.g. recipe 1 (scale pop) + recipe 4 (mask wipe) + recipe 8 (teal-orange grade) is a complete title-card.

Catalog discovery

What an assistant can reference.

Catalogs are discovered at runtime through typed list_* tools — the live set, never a stale snapshot. (MCP resources/* and prompts/* endpoints are not part of the v1 server.)

See them move — live, in-browser previews in the Showcase:

Worked examples

Eight productions. Eight plans.

Each plan below is a current production pattern — intake, structure, tool composition, visual verification, and export — exactly as a senior assistant should respond. Copy one into a system prompt, then adapt catalog ids at runtime. These examples use the current 251-tool surface, including shape-aware mask graphs, curve interpolation, filmstrip preview, export-parity checks, and reusable template sharing; every call is a real MCP command.

01 / 08 — TRAVEL REEL
01 · Travel Reel · 30s · 9:16 · verified

"Build a 30-second travel reel with beat cuts, a soft mask reveal, and captions."

Use real drum-stem onsets for the cut points, a shape-aware mask animation for the opening shot, and a filmstrip review before export. The handoff includes a canvas-versus-MP4 parity check and an optional reusable template snapshot.

# Intake (assistant asks three short questions)
Q1: Platform & length?  → Reels, 30s, 9:16, 1080p, 30fps
Q2: Vibe?             → Upbeat travel, "I want it to feel alive"
Q3: Music?            → "Surprise me — something cinematic-pop"

# Structure (3-part)
Hook   (0–2.5s):  strongest beach drone push-in.
Middle (2.5–26s): cuts on the REAL drum hits — temple, food, scooter, sunset, waterfall, friend.
Payoff (26–30s): wide drone with location title + soft fade-to-grade.

# Plan — 24 calls
create_project           { name: "Bali Reel", aspectRatio: "9:16", fps: 30 }
add_video_layer × 8        # import 8 gallery clips → v0..v7
stock_search_music        { query: "cinematic pop travel", category: "cinematic" }
add_stock_music_layer     { soundId: 4421, startTime: 0, duration: 30, volume: 0.85 }

# Hook — drone push-in
trim_layer      { layerId:"v0", mediaOffsetSec: 0.4, durationSec: 2.5 }
keyframe_add    { layerId:"v0", property:"transform.scale", timeMs:0,    value:1.0 }
keyframe_add    { layerId:"v0", property:"transform.scale", timeMs:2500, value:1.06, interp:{type:"preset",name:"easeOut"} }

# Beat-detect the music's drum stem — real onsets, not BPM guesswork
separate_audio         { layerId:"music", tier:"fast" }
beat_cut_from_drums    { sourceLayerId:"music", sensitivity: 1.3, maxCuts: 6 }
# → cutTimesMs: [2480, 6610, 10720, 14790, 18930, 22980] — split each clip at its own onset
split_layer   { layerId:"v1", atSec: 2.48 }  # temple
split_layer   { layerId:"v2", atSec: 1.30 }  # food
split_layer   { layerId:"v3", atSec: 1.40 }  # scooter
split_layer   { layerId:"v4", atSec: 1.45 }  # sunset
split_layer   { layerId:"v5", atSec: 1.50 }  # waterfall
set_layer_transition { layerId:"v3", out:{ id:"slicewipe", durationSec:0.18 } }
set_layer_transition { layerId:"v5", out:{ id:"zoomBlur", durationSec:0.20 } }

# Look — global cinematic grade, then signature LUT (1=neutral on hue/sat/contrast)
apply_global_color_grade { hue: 6, contrast: 1.10, saturation: 1.18, temperature: -0.2 }
set_layer_lut             { layerId:"v3", id:"teal-orange", intensity: 0.6 }

# Titles & lower thirds
add_text_layer            { text:"BALI", startTime: 0.2, duration: 2.2, fontSize: 96, verticalAnchor:"center", fontWeight:"900" }
set_text_animation        { layerId:"text0", inId:"in-scale-up", inDurationSec: 0.45, outId:"out-fade-up", outDurationSec: 0.35 }
add_lower_third_layer     { presetId:"bold-pop", lines:["Day 2 — Ubud"], startTime: 18.0, duration: 3.0 }

# On-device captions for any future VO
add_caption_layer_from_audio { uri:"vo.wav", language:"auto", x: 50, y: 84 }

# Shape-aware opening mask — real keyframes, not a preview-only effect
create_mask_animation { layerId:"v0", shape:"ellipse", pattern:"ellipse:sample:reveal", curve:"easeOut" }

# Mix — fade music out on the last beat
set_layer_volume_keyframes { layerId:"music", keyframes:[
    {timeMs:0,    volume:0.85},
    {timeMs:27000,volume:0.85},
    {timeMs:30000,volume:0.0}
]}

# Review the motion and compare canvas with the encoded result
preview_filmstrip      { fromSec:0, toSec:30, frames:6, maxWidth:512 }
capture_canvas          { timeSec:1.5, grid:true }
verify_export_parity   { timesSec:[1.5,15,28] }
save_project_as_template { id:"bali-reel", name:"Bali Reel", mode:"template" }

# Export
set_export_settings { resolution:"1080x1920", fps: 30 }
export_project      {}
02 · Cinematic Brand Spot · 60s · 16:9 · branded

"Launch a premium product film with a masked title card and a portable brand look."

Animate the title mask with an explicit curve, keep the logo card's mesh fill and shader backdrop in the same scene graph, then bake the brand CDL/LUT. Verify the title at its transition times before exporting a snapshot template.

# Intake
Q1: Platform?          → YouTube pre-roll, 16:9 1080p 24fps (film cadence)
Q2: Brand palette?     → Espresso brown, cream, gold
Q3: Length cap?        → 60s hard, with 15s and 30s cutdowns later

# Structure
Act I  (0–12s):  origin — slow drone over coffee terraces, brand title card.
Act II (12–42s): craft — close-ups of hands, pour, steam, roasted bean macro.
Act III(42–60s): payoff — finished cup in hand, logo reveal, tagline, URL.

# Plan — 26 calls, film-style 24fps
create_project     { name:"Origin — 60", aspectRatio:"16:9", fps:24 }
add_video_layer × 6  # 6 hand-picked hero shots → video0..video5
add_stock_music_layer{ soundId: 8842, startTime:0, duration:60, volume:0.4 }

# Letterbox bars — flat black rectangles, canvas-relative so they hold at any export size
add_shape_layer    { shape:"rectangle", fillColor:"#000000", x:0, y:0,  canvasRelativeWidth:100, canvasRelativeHeight:12, startTime:0, duration:60 }
add_shape_layer    { shape:"rectangle", fillColor:"#000000", x:0, y:88, canvasRelativeWidth:100, canvasRelativeHeight:12, startTime:0, duration:60 }

# Slow push-ins on every hero shot (24fps cinematic cadence, ~7.8s clips)
for clipId in [video0..video5]:
  keyframe_add { layerId:clipId, property:"transform.scale", timeMs:0,    value:1.00 }
  keyframe_add { layerId:clipId, property:"transform.scale", timeMs:7800, value:1.07, interp:{type:"preset",name:"easeInOut"} }

# Title card — a shape with an animated MESH gradient fill, not a flat color
add_shape_layer      { shape:"rectangle", x:0, y:0, stretchToCanvas:true, startTime:8.0, duration:4.0 }
set_shape_fill       { layerId:"shape2", style:"mesh", meshPresetId:"sunset" }
add_text_layer       { text:"ORIGIN", startTime:8.0, duration:4.0, fontSize:120, verticalAnchor:"center", fontFamily:"Serif Display" }
# One-call mask graph — geometry, opacity, and curve stay in one auditable animation
create_mask_animation { layerId:"text0", shape:"rectangle", curve:"easeInOut", keyframes:[
  {timeMs:0,    mask:{rect:{x:0,y:0,width:0,height:1}, feather:2}, opacity:0},
  {timeMs:900,  mask:{rect:{x:0,y:0,width:1,height:1}, feather:2}, opacity:1},
  {timeMs:3200, mask:{rect:{x:0,y:0,width:1,height:1}, feather:2}, opacity:1},
  {timeMs:4000, mask:{rect:{x:0,y:0,width:1,height:1}, feather:2}, opacity:0}
] }

# Brand LUT — bake a CDL grade from the reference still, register, apply globally
bake_cdl_to_cube      { cdl:{ slope:[1.02,0.98,0.93], offset:[0.02,0.01,-0.02], power:[1.05,1.0,1.08], saturation:0.92 }, size:33 }
lut_register_custom   { id:"origin-warm", name:"Origin Warm", cubeText:"<cube text>" }
apply_global_color_grade { lutId:"origin-warm", lutIntensity:0.78 }

# VO + manual music duck under the line
tts_validate_script  { text:"Some things take time. Patience. Heat. A hand that knows.", defaultVoice:"am_michael" }
tts_add_audio_layer  { text:"Some things take time. Patience. Heat. A hand that knows.", voiceId:"am_michael", startTime:14.0 }
set_layer_volume_keyframes { layerId:"music", keyframes:[
    {timeMs:14000,volume:0.40},{timeMs:14400,volume:0.18},
    {timeMs:30000,volume:0.18},{timeMs:30400,volume:0.40}
]}

# Logo reveal end card — scale-pop on a generative shader backdrop
add_generative_bg_layer { effectId:"silk", preset:"Mercury", startTime:54.0, duration:6.0, opacity:0.6 }
add_image_layer      { uri:"asset://logo.png", startTime:54.0, duration:6.0, x:30, y:30, stretchToCanvas:false }
keyframe_add          { layerId:"image0", property:"transform.scale", timeMs:0,   value:0.0, interp:{type:"preset",name:"easeOut"} }
keyframe_add          { layerId:"image0", property:"transform.scale", timeMs:480, value:1.06, interp:{type:"preset",name:"easeIn"} }
keyframe_add          { layerId:"image0", property:"transform.scale", timeMs:620, value:1.0 }
set_layer_border_glow { layerId:"image0", color:"#d4a056", intensity:60, radius:14 }
# Check the title transition and full end card, then save the portable brand recipe
capture_canvas          { timeSec:8.6, xray:true }
preview_filmstrip      { fromSec:7.5, toSec:60, frames:6 }
verify_export_parity   { timesSec:[8.6,56] }
save_project_as_template { id:"origin-brand", name:"Origin Brand Spot", mode:"snapshot" }

# Export — film cadence
set_export_settings { resolution:"1920x1080", fps:24, quality:"high" }
export_project      {}
03 · Documentary Short · 3:00 · 16:9 · captions

"Cut a three-minute documentary with on-device captions, dialogue cleanup, and loudness delivery."

Keep interview language intact, clean the dialogue on-device, generate time-aligned captions, and measure the final mix against a named loudness target. Use canvas inspection to catch lower-thirds that drift outside the safe area.

# Intake
Q1: Aspect & length? → YouTube 16:9 1080p 30fps, 3:00 hard cap
Q2: Subject lang?    → Hindi interview — on-device Whisper transcribes Hindi (no translation pass; captions stay in the spoken language)
Q3: Tone?            → Warm, observational, no music under speech

# Structure
00:00–00:18  Cold open — wide shot of stall + ambient sound, no VO
00:18–00:32  Lower-third intro: "Ramesh · Vada Pav · Dadar"
00:32–02:20  Interview A-roll with B-roll over the long answers
02:20–02:48  Cooking montage with ambient music swell
02:48–03:00  Tail card + URL

# Plan
create_project          { name:"Ramesh", aspectRatio:"16:9", fps:30 }
add_video_layer          { uri:"interview.mp4", startTime:0, duration:180 }

# On-device transcript + captions — keep the interview language intact
transcribe_audio             { uri:"interview.mp4", language:"hi", modelSize:"base" }
add_caption_layer_from_audio { uri:"interview.mp4", language:"hi", x:50, y:86 }
clean_audio                  { layerId:"video0", strength:0.7 }
add_audio_layer          { uri:"ambient_market.wav", startTime:0, duration:180, volume:0.25 }
add_stock_music_layer    { soundId: 5510, startTime:140, duration:40, volume:0.0 }

# Music swell under cooking montage
set_layer_volume_keyframes { layerId:"audio1", keyframes:[
    {timeMs:0,    volume:0.0},{timeMs:4000,volume:0.45},
    {timeMs:28000,volume:0.45},{timeMs:32000,volume:0.0}
]}

# B-roll cutaways over the long answers
add_video_layer  { uri:"b-roll-pour.mp4",  startTime:45,  duration:8  }
add_video_layer  { uri:"b-roll-spice.mp4", startTime:72,  duration:7 }
add_video_layer  { uri:"b-roll-hands.mp4", startTime:110, duration:9 }
add_video_layer  { uri:"b-roll-crowd.mp4", startTime:135, duration:5 }
# A-roll audio stays under each B-roll cutaway — that's the rule.

# Lower third intro
add_lower_third_layer { presetId:"documentary-thin", lines:["Ramesh · Vada Pav · Dadar"], startTime:18.0, duration:5.0, x:18, y:82 }

# Chapter breaks — dip to black is a classic VISUAL transition on the cut, not an audio edge
set_layer_transition { layerId:"video0", out:{ id:"dipToBlack", durationSec:0.6 } }

# On-device captions — transcribed straight off the interview audio (Hindi stays Hindi)
add_caption_layer_from_audio { uri:"interview.mp4", language:"hi", modelSize:"base", x:50, y:86 }

# Vintage doc grade — slightly faded, warm (multipliers around 1.0 = neutral)
apply_global_color_grade { contrast:1.18, saturation:0.88, temperature:0.05 }
set_layer_lut             { layerId:"video0", id:"bleach-bypass", intensity:0.5 }

# Tail card
add_text_layer { text:"Find Ramesh — Dadar West, 6am – 10am",
                 startTime:170, duration:10, fontSize:36, verticalAnchor:"center", fontWeight:"500" }
set_text_animation { layerId:"text1", inId:"in-fade-up", inDurationSec:0.8 }

# Loudness — broadcast spec (per-clip measure, then a normalization plan)
measure_loudness { samplesBase64:"<float32 mono PCM>", sampleRate:48000 }
build_loudness_plan { rows:[{ clipId:"a-roll", name:"Interview", measurement:{...} }], target:"broadcastEbu" }

# Inspect lower-third bounds, caption timing, and export parity before delivery
describe_canvas       { timeSec:18.0 }
capture_canvas         { timeSec:18.0, xray:true, grid:true }
verify_export_parity  { timesSec:[18,96,176] }

set_export_settings { resolution:"1920x1080", fps:30 }
export_project      {}
04 · Music Video · 2:30 · 9:16 · reactive

"Make a vertical music video where the mask, grade, and typography follow the track."

Separate bass and vocal stems, drive the chorus from the audio envelope, and author a shape-aware mask graph with named curve presets. Review motion as a filmstrip rather than trusting a single frame, then parity-check the MP4.

# Intake
Q1: Aspect & length? → 9:16 1080p 30fps, full single = 2:30
Q2: Sections?        → Intro 0:00, Verse 0:18, Chorus 0:48, Verse 0:90, Chorus 0:120, Outro 2:18
Q3: Lyrics on screen? → Hook line only, on every chorus

# Plan
create_project          { name:"Indie Single", aspectRatio:"9:16", fps:30 }
add_audio_layer          { uri:"single_master.wav", startTime:0, duration:150, volume:1.0 }
add_video_layer × 12      # performance shots video0..video11

# Intro (0–18s) — mask reveal of the artist name
add_text_layer     { text:"AYA", startTime:6.0, duration:6.0, fontSize:200, verticalAnchor:"center", fontWeight:"900" }
create_mask_animation { layerId:"text0", shape:"rectangle", pattern:"rectangle:sample:pulse", curve:"flow" }

# Real beat-detection — separate the master, find the actual drum + bass hits
separate_audio      { layerId:"audio0", tier:"studio" }
beat_cut_from_drums { sourceLayerId:"audio0", sensitivity:1.0, maxCuts:24 }
# → cutTimesMs over the verse + chorus; split each performance clip atSec relative to its own startTime
split_layer { layerId:"video1", atSec:0.97 }   # verse cut
split_layer { layerId:"video6", atSec:0.48 }   # chorus cut, punchier

# Chorus drop — bass stem drives a zoom-pulse on the performance clip, no manual keyframes
set_audio_reactive { targetLayerId:"video6", stemLayerId:"bass_audio0", stem:"bass", presetId:"bass-zoom-pulse", intensity:0.8 }
set_audio_reactive { targetLayerId:"video6", stemLayerId:"drums_audio0", stem:"drums", presetId:"drum-wobble-rotate", intensity:0.4 }

# Kinetic-typography lyric drop — hook line, one word per beat, vocal stem flashes its opacity
words = ["I", "DON'T", "REMEMBER", "ANYMORE"]
for i, w in enumerate(words):
    add_text_layer { text:w, startTime: 48.0 + i*0.484, duration:0.6, fontSize:140, verticalAnchor:"top", y:38, fontWeight:"900" }
    set_text_animation { layerId:"text"+(i+1), inId:"in-scale-up", inDurationSec:0.18 }
    set_audio_reactive { targetLayerId:"text"+(i+1), stemLayerId:"vocals_audio0", stem:"vocals", presetId:"vocal-flash-opacity", intensity:0.9 }

# Mask-wipe section break at the bridge (96s) — an off-screen shape slides across
add_shape_layer   { shape:"rectangle", fillColor:"#0a0a0f", stretchToCanvas:true, startTime:96.0, duration:0.6 }
keyframe_add      { layerId:"shape0", property:"transform.x", timeMs:0,   value:-100 }
keyframe_add      { layerId:"shape0", property:"transform.x", timeMs:600, value:100, interp:{type:"preset",name:"easeInOut"} }

# Time-ranged split grade — verses cooler, choruses warmer (apply_global_color_grade scopes by startSec/endSec)
apply_global_color_grade { startSec:18, endSec:48,  temperature:-0.12, saturation:0.92, contrast:1.10 }
apply_global_color_grade { startSec:48, endSec:72,  temperature:0.10,  saturation:1.18, contrast:1.18 }
set_layer_lut             { layerId:"video6", id:"teal-orange", intensity:0.55 }

# Vignette to focus the eye on the performer
set_layer_video_effects { layerId:"video6", effects:[{ effectId:"vignette", intensity:35 }] }

# Filmstrip + parity check catch a mask/beat mismatch before the final render
preview_filmstrip     { fromSec:44, toSec:56, frames:6 }
capture_canvas         { timeSec:48.75, outlinesOnly:true }
verify_export_parity  { timesSec:[6,48.75,96] }

set_export_settings { resolution:"1080x1920", fps:30 }
export_project      {}
05 · Tutorial / How-to · 90s · 9:16 · guided

"Build a 90-second how-to with inspectable steps, a countdown widget, and safe captions."

Use a widget for the live countdown, keyframe the step badge, and let the agent describe and capture the canvas at each chapter. The verification pass checks text bounds, timing, and the final vertical export.

# Intake — fast for a tutorial
Q1: Steps?       → 6 steps, ~12-15s each
Q2: Voice?       → No VO — text-on-screen with ambient kitchen sounds

# Plan
create_project          { name:"Pour-Over 90s", aspectRatio:"9:16", fps:30 }
add_video_layer × 6      # video0..video5 — one shot per step
add_audio_layer          { uri:"kitchen_amb.wav", duration:90, volume:0.5 }
add_stock_music_layer    { soundId: 6190, duration:90, volume:0.35 }

# Step counter — animated number badge, top-right safe
for i in 1..6:
    t0 = (i-1) * 15  # every 15s
    add_text_layer     { text: i+"/6", startTime:t0, duration:14.5, fontSize:42, x:80, y:8, fullWidth:false, fontWeight:"800" }
    set_layer_border   { layerId:"text"+i, width:3, color:"#ffffff", cornerRadius:24 }
    set_text_animation { layerId:"text"+i, inId:"in-scale-up", inDurationSec:0.3, outId:"out-fade", outDurationSec:0.25 }

# Instruction kinetic text — pop on each step start, typewriter reveal
instructions = ["RINSE THE FILTER", "ADD 18g COFFEE", "BLOOM 30s · 50g WATER",
                "POUR TO 150g", "POUR TO 300g", "WAIT · DRINK · ENJOY"]
for i, line in enumerate(instructions):
    t0 = i * 15
    add_text_layer     { text:line, startTime:t0+0.3, duration:3.5, fontSize:56, verticalAnchor:"top", y:18, fontWeight:"700" }
    set_typewriter     { layerId:"text"+(i+7), charDelayMs:42 }
    set_text_animation { layerId:"text"+(i+7), outId:"out-slide-up", outDurationSec:0.4 }

# Countdown widget for the 30s bloom step (starts at 32s)
add_widget_layer { widgetId:"countdown", startTime:32.0, duration:30,
                   config:{ countdownFromMs:30000, fontSize:120, textColor:"#fbbf24", fontWeight:"bold" } }

# Clean daylight grade — bright, accurate, slight warmth (multipliers around 1.0 = neutral)
apply_global_color_grade { brightness:1.10, contrast:1.05, saturation:1.10, temperature:-0.05 }

# Light music swell on the last step
set_layer_volume_keyframes { layerId:"audio1", keyframes:[
    {timeMs:0,    volume:0.35},
    {timeMs:75000,volume:0.55},
    {timeMs:90000,volume:0.0}
]}

# Inspect each chapter before export — bounds and timing are part of the plan
describe_canvas      { timeSec:15 }
capture_canvas        { timeSec:45, grid:true }
preview_filmstrip    { fromSec:0, toSec:90, frames:6 }
verify_export_parity { timesSec:[15,45,75] }

set_export_settings { resolution:"1080x1920", fps:30 }
export_project      {}
06 · Wedding Highlight · 75s · 16:9 · keepsake

"Shape a 75-second wedding highlight with clean vows, gentle motion, and a keepsake template."

Isolate and boost the vows, auto-duck the music, apply a warm look, and use a soft heart or ellipse mask for the rings detail. Save a full-fidelity snapshot locally; only submit it to Community after explicit approval.

# Intake
Q1: Aspect & length? → 16:9 1080p 24fps (cinematic), 75s
Q2: Music?           → "Something soft, piano, no lyrics"
Q3: VO?              → Use a 25-second slice of the bride's vows, recorded on a phone mic
Q4: Names & date?    → "Sara & Adam · June 14, 2026"

# Structure (emotional arc, not beats)
00:00–00:12  Morning prep — soft details, dress, rings
00:12–00:30  Ceremony walk + kiss (slow-mo on the kiss)
00:30–00:55  Vows VO over wide reception shots
00:55–01:15  Dance + party + tail card

# Plan
create_project     { name:"Sara & Adam", aspectRatio:"16:9", fps:24 }
add_video_layer × 18 # curated highlights video0..video17
add_audio_layer     { uri:"bride_vows.wav", startTime:30, duration:25 }
add_stock_music_layer{ soundId: 9021, startTime:0, duration:75, volume:0.45 }

# Dialogue boost — separate the phone-mic vows, raise vocals, duck room noise
isolate_voice { layerId:"audio0", boostDb:7, tier:"studio" }

# Sparkle Lottie on the rings detail (around 6s)
add_lottie_layer    { presetId:"sparkles", startTime:5.5, duration:2.5, x:50, y:50 }

# Speed-ramp into the kiss
set_layer_speed { layerId:"video5", speed:0.5, keepPitch:true }
keyframe_add     { layerId:"video5", property:"transform.scale", timeMs:0,    value:1.0 }
keyframe_add     { layerId:"video5", property:"transform.scale", timeMs:1200, value:1.12, interp:{type:"preset",name:"easeInOut"} }
keyframe_add     { layerId:"video5", property:"transform.scale", timeMs:2400, value:1.06 }

# Cross-fade transitions between clips in the slow sections
set_layer_transition { layerId:"video1", out:{ id:"dissolve", durationSec:0.6 } }
set_layer_transition { layerId:"video2", out:{ id:"dissolve", durationSec:0.6 } }

# Custom warm-cream LUT (round-trip from a reference still)
bake_cdl_to_cube    { cdl:{ slope:[1.04,1.02,0.95], offset:[0.02,0.01,-0.01], power:[1.0,1.0,1.04], saturation:0.96 }, size:33 }
lut_register_custom { id:"sara-adam-warm", name:"Sara & Adam Warm", cubeText:"<cube text>" }
apply_global_color_grade { lutId:"sara-adam-warm", lutIntensity:0.72, brightness:1.10, saturation:1.05, temperature:0.04 }

# Auto-duck — music drops automatically wherever the isolated vocal stem is active
auto_duck { musicLayerId:"audio1", vocalSourceLayerId:"audio0", duckDb:-14 }

# Tail card — names and date with elegant fade
add_text_layer     { text:"Sara & Adam", startTime:68.0, duration:7.0, fontSize:90, verticalAnchor:"center", fontFamily:"Serif Display", fontWeight:"400" }
add_text_layer     { text:"June 14, 2026", startTime:68.0, duration:7.0, fontSize:36, y:56, fontFamily:"Serif Display" }
set_text_animation { layerId:"text0", inId:"in-fade-up", inDurationSec:1.0 }
set_text_animation { layerId:"text1", inId:"in-fade-up", inDurationSec:1.0 }
keyframe_add       { layerId:"text0", property:"opacity", timeMs:6000, value:1.0 }
keyframe_add       { layerId:"text0", property:"opacity", timeMs:7000, value:0.0, interp:{type:"preset",name:"easeIn"} }

# Rings detail — a soft ellipse sample with a stored easing curve
create_mask_animation { layerId:"video2", shape:"ellipse", pattern:"ellipse:sample:drift", curve:"easeInOut" }
preview_filmstrip     { fromSec:4.5, toSec:8, frames:4 }
verify_export_parity  { timesSec:[6,35,70] }

set_export_settings { resolution:"1920x1080", fps:24, quality:"high" }
export_project      {}
07 · Sports Hype Reel · 45s · 9:16 · punchy

"Cut a 45-second sports hype reel with drum-locked edits and a mask punch on the drop."

Use onset detection for the cut rhythm, then combine a glitch burst with a mask animation using bounce or elastic easing. Inspect the score card in x-ray mode, confirm the curve has two keyframes, and verify the rendered parity.

# Intake
Q1: Platform?      → IG Reel + TikTok cross-post, 9:16 1080p 30fps
Q2: Energy?        → "Maximum. Like a hype train hit a building."
Q3: Music?         → trap-style instrumental, no vocals

create_project          { name:"Wildcats Playoff", aspectRatio:"9:16", fps:30 }
add_video_layer × 16     # video0..video15 — dunks, blocks, threes, crowd
add_stock_music_layer    { soundId: 7734, startTime:0, duration:45 }

# Real onset detection on the drum stem — punchier than fixed BPM math
separate_audio      { layerId:"audio0", tier:"fast" }
beat_cut_from_drums { sourceLayerId:"audio0", sensitivity:0.8, maxCuts:30 }
# → cutTimesMs drives every split below (atSec relative to each clip's own startTime)
split_layer { layerId:"video0", atSec:0.43 }
split_layer { layerId:"video1", atSec:0.43 }
# ...repeat split_layer per returned cut through the verse and the double-time chorus

# Drop hits — glitch burst + zoom punch on the dunk clip
set_layer_effect      { layerId:"video6", effectId:"glitch", params:{ amount:0.6 } }
keyframe_add          { layerId:"video6", property:"fx.intensity", timeMs:0,   value:0.6 }
keyframe_add          { layerId:"video6", property:"fx.intensity", timeMs:200, value:0.0 }
keyframe_add          { layerId:"video6", property:"transform.scale", timeMs:0,   value:1.0, interp:{type:"preset",name:"easeIn"} }
keyframe_add          { layerId:"video6", property:"transform.scale", timeMs:120, value:1.25 }
keyframe_add          { layerId:"video6", property:"transform.scale", timeMs:430, value:1.0 }
set_layer_transition { layerId:"video6", in:{ id:"flash", durationSec:0.08 } }

# Chorus whip-pans on the punchiest hits
set_layer_transition { layerId:"video8",  out:{ id:"wipeLeft", durationSec:0.12 } }
set_layer_transition { layerId:"video11", out:{ id:"zoomBlur", durationSec:0.14 } }

# Big text — "WILDCATS" punch on drop
add_text_layer     { text:"WILDCATS", startTime:6.7, duration:1.4, fontSize:180, verticalAnchor:"center", fontWeight:"900" }
set_layer_border_glow{ layerId:"text0", color:"#ef4444", intensity:85, radius:24, mode:"pulse" }
keyframe_add       { layerId:"text0", property:"transform.scale", timeMs:0,   value:0.0 }
keyframe_add       { layerId:"text0", property:"transform.scale", timeMs:120, value:1.3, interp:{type:"preset",name:"easeOut"} }
keyframe_add       { layerId:"text0", property:"transform.scale", timeMs:240, value:1.0 }
keyframe_add       { layerId:"text0", property:"transform.rotation", timeMs:0,   value:-5 }
keyframe_add       { layerId:"text0", property:"transform.rotation", timeMs:240, value:0 }

# Moody high-contrast grade
apply_global_color_grade { contrast:1.30, saturation:0.90, temperature:-0.15, brightness:0.95 }
set_layer_lut             { layerId:"video6", id:"bleach-bypass", intensity:0.55 }

# Sponsor lower-third (last 5s)
add_lower_third_layer { presetId:"sport-aggressive", lines:["@ HighSchool Gym", "Fri 7pm"], startTime:40, duration:5 }

# Final score card — one-tap neon shape style instead of hand-tuned glow values
add_shape_layer      { shape:"rounded_rect", fillColor:"#000000", startTime:42, duration:3, canvasRelativeWidth:70, canvasRelativeHeight:22, x:15, y:39, cornerRadius:18 }
set_shape_style      { layerId:"shape0", styleId:"neon-pink" }
add_text_layer       { text:"WILDCATS 78 — TIGERS 64", startTime:42, duration:3, fontSize:50, verticalAnchor:"center", fontWeight:"900" }

# Mask punch — Mirror H sample with bounce easing on the drop
create_mask_animation { layerId:"video6", shape:"mirror", pattern:"mirror:sample:pulse", curve:"bounce", mirrorAxis:"horizontal" }
capture_canvas         { timeSec:7.0, xray:true }
preview_filmstrip     { fromSec:6.5, toSec:8.5, frames:5 }
verify_export_parity  { timesSec:[7,24,42] }

set_export_settings { resolution:"1080x1920", fps:30 }
export_project      {}
08 · Luxury Real-Estate Walkthrough · 60s · 16:9 · listing

"Create a 60-second property walkthrough with room-safe reveals and a shareable listing template."

Use a diagonal or mirror mask per room, soft edge fades for the footage, animated labels, and a measured warm grade. Check every reveal with a filmstrip, export the listing, then offer a sanitized Community template with its thumbnail and demo.

# Intake
Q1: Aspect?        → 16:9 1080p 30fps for MLS + Instagram
Q2: Tone?          → "Quiet luxury — let the house speak"
Q3: CTA?           → Agent name + phone number + open-house date

# Structure — 7 rooms × ~8s each + 4s opening + 4s closing
00:00–00:04  Aerial drone — front of house with title overlay
00:04–00:12  Entry / great room
00:12–00:20  Kitchen
00:20–00:28  Primary suite
00:28–00:36  Pool deck
00:36–00:44  Wine cellar
00:44–00:52  Sunset terrace
00:52–01:00  Price card + agent CTA

# Plan
create_project          { name:"Malibu Listing", aspectRatio:"16:9", fps:30 }
add_video_layer × 7      # video0..video6, one per room
add_stock_music_layer    { soundId: 3387, duration:60, volume:0.5 }

# Slow push-ins everywhere — the room should feel like it's revealing itself
for clipId in [video0..video6]:
  keyframe_add { layerId:clipId, property:"transform.scale", timeMs:0,    value:1.00 }
  keyframe_add { layerId:clipId, property:"transform.scale", timeMs:7800, value:1.05, interp:{type:"preset",name:"easeInOut"} }

# Shader-driven wipe between rooms — clean diagonal sweep on every cut
for clipId in [video0..video5]:
    set_layer_transition { layerId:clipId, out:{ id:"slicewipe", durationSec:0.55 } }

# Animated room labels — clean serif, slide-in from left, hold, fade
labels = ["GREAT ROOM","KITCHEN","PRIMARY SUITE","POOL DECK","WINE CELLAR","SUNSET TERRACE"]
for i, l in enumerate(labels):
    t0 = 4 + i*8
    add_text_layer     { text:l, startTime:t0+0.4, duration:6.0, fontSize:48, x:8, y:86, fontFamily:"Serif Display", fontWeight:"300", textAlign:"left", fullWidth:false }
    keyframe_add       { layerId:"text"+i, property:"transform.x", timeMs:0,    value:-30 }
    keyframe_add       { layerId:"text"+i, property:"transform.x", timeMs:600,  value:8, interp:{type:"preset",name:"easeOut"} }
    keyframe_add       { layerId:"text"+i, property:"opacity",    timeMs:5400, value:1.0 }
    keyframe_add       { layerId:"text"+i, property:"opacity",    timeMs:6000, value:0.0, interp:{type:"preset",name:"easeIn"} }

# Price card — Pattern-fill (gold foil texture) border instead of a flat stroke
add_shape_layer      { shape:"rounded_rect", fillColor:"#1a1a1a", startTime:52, duration:8, canvasRelativeWidth:60, canvasRelativeHeight:40, x:20, y:30, cornerRadius:16 }
set_shape_outline    { layerId:"shape0", color:"#d4a056", width:2, align:"outside" }
set_shape_glow       { layerId:"shape0", color:"#d4a056", mode:"breathe", intensity:40, radius:10, placement:"outer" }
add_text_layer       { text:"$12,450,000", startTime:52.5, duration:7.5, fontSize:72, verticalAnchor:"center", y:42, fontFamily:"Serif Display", fontWeight:"300" }
add_text_layer       { text:"5 BD · 7 BA · 8,400 SF", startTime:52.5, duration:7.5, fontSize:30, y:52, fontWeight:"400" }
add_lower_third_layer { presetId:"elegant-thin", lines:["Maya Linden · Coldwell Banker", "(310) 555-0182 · Open Sat 1–4pm"], startTime:54, duration:6 }

# Soft warm grade (multipliers around 1.0 = neutral)
apply_global_color_grade { brightness:1.10, contrast:1.08, saturation:1.08, temperature:0.08 }

# Opening title — mask-wipe reveal
add_text_layer     { text:"22810 Pacific Coast Highway", startTime:0.5, duration:3.5, fontSize:52, verticalAnchor:"center", fontFamily:"Serif Display", fontWeight:"300" }
create_mask_animation { layerId:"text6", shape:"rectangle", pattern:"rectangle:sample:reveal", curve:"easeOut" }

# Room reveal review + community handoff (private media stripped by default)
set_layer_fade_mask  { layerId:"video0", mode:"inset", softness:0.24 }
preview_filmstrip     { fromSec:0, toSec:60, frames:8 }
verify_export_parity  { timesSec:[1,28,56] }
save_project_as_template { id:"malibu-listing", name:"Malibu Listing", mode:"snapshot" }

set_export_settings { resolution:"1920x1080", fps:30 }
export_project      {} # use the returned MP4 path for the Community demo
submit_template     { id:"malibu-listing", name:"Malibu Listing", category:"real-estate", stripPrivateMedia:true, thumbnailUri:"file:///tmp/malibu-thumb.jpg", previewVideoUri:"file:///tmp/malibu-demo.mp4" }
			

Each plan is a complete tool composition — an agent could send these calls to a connected ExpoCut MCP server and the listed render would appear on the device. Mix and match: a wedding cut can borrow the music-video lyric overlay; a tutorial can borrow the brand-spot end-card reveal. Tip: use ← / → arrow keys to flip between examples.

Prompts

Conversation starters for assistants.

"Help me cut a Reel"

A guided flow: pick clips, choose a vibe, set duration, auto-suggest transitions and music.

"Make this look cinematic"

Recommend a LUT, set 2.39:1 letterbox, apply teal/orange grade, slow the timeline 12%.

"Add captions"

Suggest a caption style, font, position, and timing for the spoken audio in the clip.

"Trim to 30 seconds"

Identify the strongest beats and propose cuts that fit the target duration.

Console

Run commands from your browser.

Connect to the MCP server running on your phone and execute tool calls directly — no CLI needed. Paste the address and token from ExpoCut → Settings → AI Integration.

🛠️
Open the full MCP Console — a developer studio
Postman-style: browse the live tool catalog with schemas, send calls and inspect detailed responses, run multi-step recipes, and see the canvas the agent built — real frame and wireframe.
Open Console →
Not connected
FAQ

Questions, answered.

The essentials about the connector, privacy, and how agents stay current.

Does ExpoCut have a Model Context Protocol (MCP) server?
Yes. ExpoCut ships an in-app MCP server that exposes 263 typed tools. It is off by default and accepts requests only from private or loopback network addresses — the user turns it on in Settings.
What can an AI agent actually do with it?
Create and open projects; add and trim video, image, audio, text, shape and widget layers; assemble clip sequences and auto-edit silence or long footage; clean dialogue; apply 220+ effects, 380+ preset filters and looks and 99 transitions; add masks, gradient fade masks, shader filters, light regions and light-leak overlays; generate on-device captions and text-to-speech voiceover; author keyframes, motion paths and advanced compositing; apply templates and brand profiles; inspect the canvas (describe_canvas, capture_canvas, preview_filmstrip); and export up to 4K — every step undoable.
How does an AI assistant connect to ExpoCut?
The user turns on the connector in ExpoCut's Settings. An MCP-compatible client (such as Claude) then connects over the private local network and calls the typed tool surface. No editing traffic is sent to ExpoTechIn servers.
How do agents discover the current capabilities?
Read llms.txt and llms-full.txt for a machine-readable map, or call the runtime introspection tools — list_effects, list_shaders, list_transitions, list_shapes, list_fonts, get_effect_schema, get_layer_schema — for the live catalogs.
Is it free, and where does the work run?
ExpoCut offers a free plan; MCP availability depends on the plan shown in the app. The server runs entirely on-device — the rendering and editing happen on your phone, not in the cloud.
Status

Live, on the device.

ExpoCut ships an in-app MCP server. When the user enables it, an MCP-compatible client like Claude Desktop connects to a local port on the device and can call the tools above. The server is off by default, accepts requests only from private or loopback network addresses, and every call is recorded as an undoable command in the on-device edit-history log. This page is the public reference for that surface so agents can index it and reason about the app.

Build with ExpoCut.

Download the app, or read the matching Skill for AI assistants.

Join the beta View Skill →