studio_cancelAbandon the current recording session: closes the browser and discards the tape. Nothing is written except screenshots already saved. Safe to call when no session is active (no-op).Input schema{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
} | — | | — |
studio_captionShow (or replace) the bottom-left storyline caption — an eyebrow kicker plus one sentence that narrates the current beat. Keep it under ~12 words and leave it up 2–4s (studio_hold) so it reads. Pass clear=true to fade it out before a scene change. The caption_text is ALSO recorded as the voice-over script: if you call studio_finish with voiceover:true it's spoken aloud (locally) timed to this moment — write captions as speakable sentences. Set no_voice:true for a caption you want on screen but NOT narrated.Input schema{
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"description": "Small uppercase kicker, e.g. 'STEP 1' or the feature name."
},
"caption_text": {
"type": "string",
"description": "One narrating sentence (keep it short — it's on screen and, with voiceover:true, spoken)."
},
"clear": {
"type": "boolean",
"description": "Fade the caption out instead of showing one."
},
"no_voice": {
"type": "boolean",
"description": "Show the caption but exclude it from the voice-over track (optional)."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_clickGlide the visible cursor to a target and click it, slow enough to read as a deliberate action on camera. Use for every button / link / menu interaction in the walkthrough. Precondition: the target must be on screen — studio_goto or studio_scroll to it first if it isn't. Prefer a stable CSS selector; fall back to visible text. After clicking it waits for the page to settle and re-applies the theme overlays, so the next beat is on a hydrated page. Returns 'clicked <target>', or errors 'target not visible' — then take a studio_screenshot and pick a better target.Input schema{
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "CSS selector (preferred, e.g. '[data-testid=save]' or 'nav a.pricing') or the exact visible text of the element. Must resolve to one visible element."
},
"settle_ms": {
"type": "integer",
"minimum": 0,
"description": "Pause after the cursor arrives, before the click fires, in milliseconds. Optional; default 360. Raise (e.g. 600) for a more deliberate beat."
}
},
"required": [
"target"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_end_cardShow the closing card — a centred glass card over the (still visible) product: title, optional subtitle and URL. The classic outro: what they saw + where to get it. Hold ~2.5s after this, then studio_finish.Input schema{
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Big closing line — the product name or the one-line takeaway. Required. Example: 'AIOProductOS'."
},
"subtitle": {
"type": "string",
"description": "One supporting line under the title, e.g. a tagline. Optional."
},
"url": {
"type": "string",
"description": "Call-to-action URL shown beneath the title, e.g. 'aioproductos.com'. Optional; shown as text, not a live link."
}
},
"required": [
"title"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_finishStop recording and produce the final files: closes the browser, strips residual dark frames (ffmpeg blackdetect → trim), transcodes to a share-ready MP4 (H.264, faststart), then — optionally — narrates the caption lines with a LOCAL voice (no cloud/keys), lays down a music bed, writes/burns subtitles, and exports vertical/square social crops, plus a GIF. Voice-over timing is auto-corrected for the frames the deblack step removed. Returns every produced file with size + duration. Without ffmpeg you still get the raw WebM.Input schema{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Base file name for the video (without extension), e.g. 'onboarding-walkthrough'."
},
"gif": {
"type": "boolean",
"description": "Also export a compact GIF (optional; default false)."
},
"voiceover": {
"type": "boolean",
"description": "Narrate the caption / studio_narrate lines with a local, offline voice (macOS say · piper · espeak-ng). No account, no API key. Default false."
},
"voice": {
"type": "string",
"description": "System voice name (optional), e.g. macOS 'Samantha' or 'Daniel'; falls back to the OS default voice."
},
"subtitles": {
"type": "string",
"enum": [
"none",
"srt",
"burn"
],
"description": "'srt' writes a sidecar .srt from the caption lines; 'burn' also renders them into the picture (muted-autoplay social); 'none' (default)."
},
"music": {
"type": "string",
"description": "Path to a local audio file for a background music bed, looped and ducked under the voice-over (optional)."
},
"aspects": {
"type": "array",
"items": {
"type": "string",
"enum": [
"9:16",
"1:1"
]
},
"description": "Extra social crops to export beside the 16:9 master, e.g. ['9:16','1:1'] for Reels/TikTok/feed. Padded with the theme background."
}
},
"required": [
"name"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_gotoNavigate to a URL behind a designed transition card (eyebrow + big title on the theme background) — the viewer never sees a loading flash or half-hydrated page. Use for the opening shot AND every surface change; give each a short title so the cut reads as a chapter. Waits for real content (network settle + optional wait_for selector + skeleton loaders cleared) before lifting the card.Input schema{
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "Absolute URL to navigate to, including scheme. Example: 'https://app.example.com/dashboard'."
},
"eyebrow": {
"type": "string",
"description": "Small uppercase kicker on the transition card, e.g. 'DASHBOARD'. Optional."
},
"title": {
"type": "string",
"description": "Big title on the transition card — usually the surface name. Optional but recommended so each cut reads as a chapter."
},
"wait_for": {
"type": "string",
"description": "CSS selector that must be visible before the card lifts (on top of network-settle + skeleton clearing). Optional; use it when the real content is gated behind a spinner. Example: '[data-loaded=true]'."
},
"dwell_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"description": "Minimum time the card stays up so the title reads, in milliseconds. Optional; default 1200."
}
},
"required": [
"url"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_highlightDraw an accent ring around a target (CSS selector or visible text), optionally with a label card beside it — the 'look here' callout. Side effects: scrolls the target into view, then overlays a ring (and card) in the theme accent; rings stack until cleared. Returns 'highlighted <target>' when the element is found, or errors 'target not found: <target>' when it isn't — retry with a studio_screenshot to pick a better target. Always call with clear=true before you navigate, zoom, or ring a different element, so stale rings don't linger on the next shot.Input schema{
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "CSS selector or exact visible text of the element to ring. Required unless clear=true. Example: '.metric-card' or 'Monthly revenue'."
},
"eyebrow": {
"type": "string",
"description": "Small uppercase kicker on the label card, e.g. 'RESULT'. Optional; ignored when clear=true."
},
"label": {
"type": "string",
"description": "Text of the label card shown beside the ring. Optional — omit for a ring with no card. Ignored when clear=true."
},
"clear": {
"type": "boolean",
"description": "true removes ALL current rings/labels and ignores the other fields. Default false (draw a new one)."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_holdHold the shot for a moment — glides to a target (optional) and keeps micro-drift so the recorder keeps emitting frames (a dead-static hold gets its tail frames dropped). Use after captions, highlights, and zooms: 2000–3000ms is a good beat.Input schema{
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "CSS selector or visible text to glide the cursor to and rest on. Optional — omit to hold in place. Example: '.cta-button'."
},
"ms": {
"type": "integer",
"exclusiveMinimum": 0,
"description": "Hold duration in milliseconds. Optional; default 2200. A good beat after a caption/highlight/zoom is 2000–3000."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_narrateRecord a line of voice-over WITHOUT putting a caption on screen — narration spoken over the action. The line is synthesized locally at studio_finish (needs voiceover:true). Holds the shot afterward so there's footage under the line; set hold_ms to roughly the length of the sentence when spoken (~400ms/word).Input schema{
"type": "object",
"properties": {
"line": {
"type": "string",
"description": "The sentence to narrate (spoken, not shown on screen)."
},
"hold_ms": {
"type": "number",
"description": "Hold the shot after recording the line so footage covers it (optional; default 2600)."
}
},
"required": [
"line"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_screenshotTake a high-DPI (2x) screenshot of the current frame and save it as a PNG in the output directory. Works any time during a session — element-only via selector, or full_page for the whole scroll height. Returns the file path. Clear captions/rings first unless you want them in the shot.Input schema{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "File name without extension; slugified to lowercase-dashes. e.g. 'dashboard-hero' → dashboard-hero.png in the output dir. Reused names overwrite."
},
"selector": {
"type": "string",
"description": "Capture just this element (CSS selector) instead of the viewport. Optional. When set, full_page is ignored."
},
"full_page": {
"type": "boolean",
"description": "true captures the entire scroll height, not just the visible viewport. Optional; default false. Ignored when selector is set."
}
},
"required": [
"name"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_scrollCubic-eased scroll to an absolute Y offset or until a target element is in view — never a jump cut. Use between story beats to bring the next area on camera. Side effect: changes only the page scroll position (no click, no navigation). Pass exactly ONE of to_y or to_target. Returns 'scrolled' on completion. To reveal an off-screen element before studio_click or studio_highlight, scroll to it first.Input schema{
"type": "object",
"properties": {
"to_y": {
"type": "number",
"minimum": 0,
"description": "Absolute vertical offset from the top of the page, in pixels (≥0). Use this OR to_target, not both. Example: 1200."
},
"to_target": {
"type": "string",
"description": "CSS selector to bring into view (scrolled toward center). Use this OR to_y. Example: '#pricing' or 'section.features'."
},
"duration_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"description": "Scroll animation length in milliseconds. Optional; default 1300. Larger = calmer, slower glide (e.g. 2000); smaller = snappier."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_startStart a recording session: launches a themed headless browser and begins capturing video. Returns the session config and the output directory. One session at a time — studio_finish or studio_cancel ends it. Set theme to the PRODUCT'S brand (bg = its app background — wrong bg causes visible flashes on page loads); use storage_state_path (a Playwright storageState JSON) to record logged-in areas without sharing credentials; list chat widgets / dev overlays / consent banners in hide_selectors so they never appear on tape.Input schema{
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "Viewport width in px (optional; default 1280)."
},
"height": {
"type": "number",
"description": "Viewport height in px (optional; default 720)."
},
"theme": {
"type": "object",
"properties": {
"bg": {
"type": "string",
"description": "App background color, e.g. '#0d1117' (default '#101014')."
},
"accent": {
"type": "string",
"description": "Accent for rings/captions/cursor, e.g. '#4f8cff'."
},
"text": {
"type": "string",
"description": "Overlay text color (default near-white)."
},
"font": {
"type": "string",
"description": "Overlay font stack (default system-ui)."
},
"brand": {
"type": [
"string",
"null"
],
"description": "Brand name on transition cards; null for none."
}
},
"additionalProperties": false,
"description": "Visual theme for overlays/transitions — use the recorded product's brand."
},
"hide_selectors": {
"type": "array",
"items": {
"type": "string"
},
"description": "CSS selectors to hide from frame 1 (chat bubbles, cookie banners, dev overlays)."
},
"storage_state_path": {
"type": "string",
"description": "Path to a Playwright storageState JSON for logged-in recording (optional)."
},
"cookies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"domain": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"name",
"value",
"domain"
],
"additionalProperties": false
},
"description": "Cookies to pre-set, e.g. your consent cookie so banners never mount (optional)."
},
"color_scheme": {
"type": "string",
"enum": [
"dark",
"light"
],
"description": "prefers-color-scheme for the page (default dark)."
},
"show_cursor": {
"type": "boolean",
"description": "Visible on-screen cursor that glides to targets (default true)."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_typeGlide to an input and type text at a human pace (keystroke by keystroke — it reads as real usage, not a paste). Use realistic demo content; whatever you type is on tape.Input schema{
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the input, textarea, or contenteditable to type into, e.g. 'input[name=email]'. Must be focusable and visible."
},
"input_text": {
"type": "string",
"description": "The literal text to type; it appears on tape keystroke by keystroke, so use realistic demo content (never real secrets)."
},
"delay_ms": {
"type": "integer",
"minimum": 0,
"description": "Per-keystroke delay in milliseconds. Optional; default 45. Lower = faster typing (e.g. 25); higher = more deliberate."
}
},
"required": [
"selector",
"input_text"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |
studio_zoomCamera punch-in: smoothly zoom the page toward a target (CSS selector or visible text) — the money shot for a metric, button, or result. Captions stay screen-fixed; the content scales under them. ALWAYS studio_zoom with reset=true before navigating or showing the end card, or the next scene inherits the zoom.Input schema{
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "CSS selector or visible text to punch in on (centered). Required unless reset=true. Example: '.big-number' or 'Sign up'."
},
"zoom_scale": {
"type": "number",
"minimum": 1,
"maximum": 2.2,
"description": "Zoom factor (>1). Optional; default 1.7. Keep ≤2.2 — beyond that the page pixelates. Ignored when reset=true."
},
"duration_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"description": "Zoom animation length in milliseconds. Optional; default 1100."
},
"reset": {
"type": "boolean",
"description": "true eases the camera back to 1:1 (call before navigating or showing the end card, or the next scene inherits the zoom). Ignores target/zoom_scale. Default false."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | | — |