<!-- version: 3.104.0 -->
PandaStudio
🛑 Pick your interface FIRST — prefer the CLI
PandaStudio exposes the same editing surface through two transports:
-
CLI (localhost HTTP).
Prefer this. A single
bash tool covers the entire ~150-verb surface — no per-tool schema
needs to live in your context. Probe once with
(or just call
pandastudio system.status --json
). If it succeeds, you're on the CLI path; every example
in this skill is written for it directly.
-
MCP server — tools prefixed
(in-app
PandaStudio agent) or
(external hosts like
Cursor, Claude Desktop).
Use only when the CLI is not
installed — i.e.
returned empty AND
one of the MCP prefixes is visible in your tools.
Why CLI is preferred: every MCP tool definition costs context
tokens (per-tool input schema + description). With ~150 verbs that
overhead adds up quickly. The CLI is one bash tool — schema cost
stays constant regardless of which verb you call.
The verbs, argument names, and behaviors are identical across both
transports. A CLI call like
pandastudio project.add-zoom --id=… --atMs=…
maps 1:1 to the MCP tool
with the same
args (
). If you're on MCP fallback, translate the
CLI examples below by lowercasing the verb and replacing the dot
with an underscore.
Do not search the filesystem for the CLI. Don't run
,
, etc.
is the only probe you need; if it returns nothing,
immediately move to the MCP fallback without further discovery.
Version check. This skill requires
≥ 1.15.0 (or
≥ 1.15.0). On the MCP path, call
and read the returned version. On the CLI path, run
. If < 1.15.0, tell the user to update
(
npx @writepanda/mcp@latest
) and restart their agent host. Commands
like
,
, and
do not exist in older versions.
Motion graphics: use the bundled templates first
PandaStudio ships a curated set of YouTube-creator motion-graphic
templates — title cards, lower thirds, stat reveals, checklists,
comparisons, a host+panel split, animated-title overlays. They are
production-grade and the primary path when you are layering graphics OVER
existing footage (see the Mode A / Mode B note below):
- → see every template, its editable slots, and whether
it's an overlay (sits over the video with alpha). It also returns
— ~38 curated standalone compositions (effects,
overlays, flowcharts, code snippet, beat-driven cuts)
that have no slots; render a block's with .
See §"Hyperframes registry blocks".
motion_generate { templateId, slots, background }
→ render it with
your own text/colors. Returns a .
- → then
project_add_motion_graphic { fromJob }
(or
project_add_designed_segment
for ).
Everything editable — text, colors, list items, and the
background
mode (
/
/
) — is controlled through
+
. The full catalog (when to use each, what's
editable) is in the
"Motion graphics" section below.
Templates-first vs. custom depends on whether there's footage underneath:
- Mode A — graphics layered OVER existing footage (lower thirds, stat
callouts, side panels, captions on a talking-head or screen recording):
bundled templates are the default. Fast, on-brand, composite cleanly over the
host. All the "templates first" guidance applies here.
- Mode B — a video built FROM SCRATCH where the motion graphics ARE the video
(promo, explainer, intro/outro, product teaser, CTA piece — anything with NO
source clip on the main track): default to fully custom, hand-authored
scenes via , NOT bundled templates.
EXCEPTION — faceless videos are Mode B but IMAGE-driven, not HTML text
scenes. If the ask is a "faceless" video (faceless YouTube / faceless
short / narrated story or explainer with no face), do NOT author text
motion-graphic scenes — that produces a title slideshow. Generate an IMAGE
per beat + Ken-Burns + voiceover. See the dedicated "Faceless videos"
section below.
For the other Mode B pieces (promos/explainers): In from-scratch work
templates read as generic and "templated" — exactly the wrong feel for a
hero/marketing asset, which is usually the most visible, brand-defining thing
the user makes. Load
reference/promo-and-mg-videos.md
first — it is the design bar for this case: every scene a DISTINCT
composition that SHOWS its point (don't headline features — depict them), one
system never one layout, rotate layout archetypes. Then author each scene from
the canonical shell in reference/motion-philosophy.md
. One NAMED alternative
design system exists for Mode B: the whiteboard / hand-drawn explainer
(reference/whiteboard-style.md
) — paper
canvas, SVG draw-on strokes, handwriting text reveal. Reach for it when the
brief says "whiteboard / hand-drawn / sketch / doodle / handwritten", or when
an abstract CONCEPT needs to be drawn (science, process, metaphor) and the
tone wants friendly-approachable rather than premium-brand. Templates are still fine as building blocks
inside an otherwise-custom piece (e.g. a transition between custom scenes) —
just not the backbone. Reach for templates as the backbone of from-scratch
work ONLY for a deliberately quick draft or when the user explicitly asks for
speed over bespoke; if you do, SAY you're using templates for speed and offer
the custom version.
For Mode A, author custom HTML (
) only when no bundled
template fits the brief — a bespoke one-off, an unusual layout, a brand-specific
3D treatment. That path is documented under "Custom motion graphics — HTML
authoring"; load
reference/motion-philosophy.md
before authoring.
Video templates (storyboards): a whole video, not one overlay
When the user wants a finished short video (a channel intro, an
episode open, a lesson opener) rather than a single overlay, use a
storyboard — a multi-scene video template you fill in once:
- → see every storyboard: its id,
(youtube | podcaster | course), and its (the brief — text +
color fields; color fields default to the workspace brand kit).
motion_generate_storyboard { storyboardId, params }
→ the server
renders each scene and concatenates them into ONE MP4. Returns a
. Omitted color params fall back to the brand kit
(); omitted required text params fail fast,
so read the brief first.
- → then
project_add_motion_graphic { fromJob }
, same as a
single template.
Storyboards are slower than a single template (they render N scenes
sequentially), so set a generous
timeout. Transitions
between scenes are baked into each scene's own animation in v1 (the
join is a hard cut). Prefer a storyboard over hand-composing several
calls when one fits the brief — it's one call and
on-brand by default.
Quickstart
Reminder: examples below are the CLI path (preferred). If
returned empty, mentally translate each
verb — e.g.
pandastudio system.status --json
→
MCP tool,
pandastudio project.add-zoom --id=… …
→
with the same args.
bash
# 1. Confirm the server is reachable AND the user has a license.
# MCP equivalent: call `system_status` with no args.
pandastudio system.status --json
# 2. Discover what's available — never guess command names.
# MCP equivalent: call `system_list_commands`.
pandastudio commands
# 3. Render a motion graphic from a bundled template (the primary path).
# See the "Motion graphics" section for the full catalog.
JOB=$(pandastudio motion.generate \
--templateId=creator-card \
--slots='{"headline":"live demo","eyebrow":"now","brandColor":"#2563EB"}' \
--aspectRatio=16:9 \
--json | jq -r '.data.jobId')
pandastudio job.wait --id="$JOB" --json | jq '.data.job.result'
# 4. Add the rendered clip to the timeline at the playhead / a given time.
pandastudio project.add-motion-graphic --id="$PROJECT" --fromJob="$JOB" --durationMs=4500
That's the whole loop: probe → discover → call → (if async) wait. Every richer workflow is a composition of those four steps.
timeouts are not failures. Default 5 min, hard cap 30 min. If the wait returns
, the job is
still running — call
again with the same id to keep polling. NEVER treat
as a render failure; the underlying render keeps going regardless of whether anyone's waiting on it. For heavy 30s @ 1080p motion-graphic renders, expect 8–15 minutes — pass
or higher up front, or re-poll until the result lands.
Before any tool call: license check
Always run
pandastudio system.status --json
first. Read the
block:
| Field | What it means |
|---|
| Full surface available. |
| + | Active trial. Full surface available. |
| Trial expired, no license. Only and work. Stop and tell the user to activate a license in Settings → License. |
If the call fails with a connection error, the CLI auto-launches PandaStudio and waits up to 60 s. If it fails with
invalid or missing bearer token
, the on-disk credentials at
~/.config/pandastudio/{token,port}
rotated mid-flight — wait 2 s and retry once.
Workspaces (v1.19+)
PandaStudio is multi-workspace as of v1.19. Every
/
/
/
/
query operates inside the
active workspace — the one listed in
. Users (typically agencies) separate clients into their own workspaces so credentials, exports, and YouTube connections never cross-contaminate.
Right after , check the workspace context:
bash
pandastudio workspace.list --json | jq '.data | { current: .currentWorkspaceId, count: (.workspaces | length), cap: .limit }'
- — Starter plan or Trial
- — Creator plan
- — Team plan (unlimited)
Switching workspaces:
bash
# Get the id of a specific client's workspace
WS=$(pandastudio workspace.list --json | jq -r '.data.workspaces[] | select(.name == "ACME Agency — Client A") | .id')
pandastudio workspace.switch --id=$WS --json
# Every subsequent query now operates inside that workspace.
Creating a workspace:
bash
# Agencies: one workspace per client.
pandastudio workspace.create --name="ACME Agency — Client A" --switchTo=true --json
# If the plan cap is hit, the response looks like:
# { "ok": false, "error": "Your Starter plan allows 1 workspace. Upgrade to Creator to create more.",
# "details": { "code": "workspace_limit_reached", "upgradeTo": "Creator" } }
# Tell the user to upgrade at writepanda.ai/#pricing; do NOT retry.
Deleting (destructive): call
first so you can show the user what will be lost, then
. Projects' on-disk
files stay — only the library rows disappear. YouTube-published videos stay on YouTube (we can't delete those); we only drop the local connection + cache.
bash
pandastudio workspace.contents --id=$WS --json | jq '.data.counts'
# { "projectCount": 12, "exportCount": 4, "publishedVideoCount": 3 }
# Confirm with user before:
pandastudio workspace.delete --id=$WS --json
Don't quietly switch workspaces mid-task. If you need to operate in a different workspace than the one the user opened, confirm with them first. Crossing client boundaries silently is how agency relationships break.
⚠ When given a project id with no other context
If the user hands you a project id (in chat, in a CSV, in a webhook payload) and you don't know which workspace it belongs to,
always run FIRST, before any read/edit/export/publish:
bash
RES=$(pandastudio project.locate --id=$PID --json)
# { "data": { "id": ..., "filePath": ..., "workspaceId": ..., "workspaceName": "Client A",
# "isInActiveWorkspace": false } }
IN_ACTIVE=$(echo "$RES" | jq -r '.data.isInActiveWorkspace')
WS_NAME=$(echo "$RES" | jq -r '.data.workspaceName')
if [ "$IN_ACTIVE" != "true" ]; then
# STOP. Do not silently switch. Ask the user.
# "This project lives in workspace '$WS_NAME', current is '<X>' — switch?"
fi
Why this matters: happily resolves a project from any workspace, but
,
,
export.generate-thumbnail
, and
all use the
active workspace's credentials. Editing project A while workspace B is active and then publishing → the video lands on Client B's YouTube channel.
The worst kind of mistake.
Every response now also carries the workspace fields (
,
,
) — so even if you skipped
, you can still detect the mismatch from the read response and bail before mutating anything. But
is cheaper (no project body) and clearer in intent — call it first when working from a bare id.
Hard rule: never call without confirming isInActiveWorkspace === true
for the project being published. If the user asks you to publish a project in a different workspace, walk them through the explicit switch:
bash
pandastudio workspace.switch --id=$TARGET_WS --json
# Then re-run any pre-flight that depends on workspace state
# (license check, youtube account list, replicate key check)
Project-look defaults (v1.49.1+)
Save a workspace's preferred
look once so every NEW project and fresh recording starts from it — the user doesn't re-pick a background / caption style each time. Per-workspace. Covers background (
),
, and editor
(padding, shadow, corner radius, blur). The editor also exposes this as a "Save as default for new projects" button.
bash
# Read the current defaults (null = none set)
pandastudio workspace.get-project-defaults --json
# Set them — pass any subset; unknown fields are dropped.
pandastudio workspace.set-project-defaults \
--defaults='{"wallpaper":"/wallpapers/wallpaper5.jpg","captionSettings":{"enabled":true,"templateId":"editorial"},"editorDefaults":{"padding":18,"borderRadius":8}}' \
--json
# Clear them
pandastudio workspace.set-project-defaults --defaults=null --json
Use when the user says things like "use this background for all my videos" or "always start new projects with these captions". Applies to projects/recordings created AFTER it's set — it doesn't retro-edit existing projects.
Brand kit — set it, or auto-capture it from a URL (v1.84+)
The workspace brand kit (colors primary/accent/ink/background, display/body fonts, logo, voice) feeds brand-aware captions, motion graphics, lower-thirds, and thumbnails. Two ways to fill it:
bash
# Manual: set any subset.
pandastudio workspace.set-brand --brand='{"name":"Acme","colors":{"primary":"#2563EB","ink":"#111827","background":"#FFFFFF"},"typography":{"display":"Inter"}}' --json
# Auto: pull the real brand straight off a website. Runs HyperFrames capture,
# classifies the site's actual colors/fonts/logo, and MERGES them into the kit
# (your hand-set fields survive). ASYNC — poll job.wait; first run downloads the
# capture CLI so use a long timeout.
JOB=$(pandastudio workspace.capture-brand --url=https://acme.com --json | jq -r '.data.jobId')
pandastudio job.wait --id="$JOB" --timeoutMs=300000 --json | jq '.data.job.result.brand'
Reach for
whenever the user says "use my brand", "make it match my site", or you're onboarding a new client and only have their URL — it beats asking them to type six hex codes. Needs network access. After it lands, the classified brand is a starting point; if the user corrects a color, apply it with
.
Organising projects, renaming, transcription languages
Folders,
, project-look defaults, transcription-language switching (Parakeet/Whisper), and transcribing a standalone file → text/SRT/VTT. Full detail:
reference/projects-and-transcription.md
.
Recording the screen yourself (agent-driven, v1.86+)
You can START and STOP a high-quality screen recording directly — no UI, no
user in the loop. This is the full-quality alternative to a browser's built-in
capture: drive a web app (or anything on screen) yourself, record it into
PandaStudio, then edit and export. macOS/Windows only.
bash
# 1. (optional) see what you can target — displays + windows
pandastudio recording.list-sources --json
# → { displays:[{id:"screen:1:0",name:"…",primary:true}], windows:[{id:"window:123:0",name:"Google Chrome — …"}] }
# 2. start (defaults to the primary display; pass --source to pick a window/display)
pandastudio recording.start --json # whole primary display
pandastudio recording.start --source="window:123:0" --json # just that Chrome window
# → { recordingId }
# 3. …now do the thing you want to capture (click through the app, etc.)…
# 4. stop — finalizes the MP4 AND creates an editable project by default
pandastudio recording.stop --name="ACME tutorial" --json
# → { screenPath, durationMs, projectId, projectPath, projectCreated:true }
Then edit the returned project like any other:
→
transcript.remove-fillers
→
on the key clicks →
for a voiceover (
) →
.
Notes:
- Permission: screen capture needs the one-time OS Screen Recording grant.
It is already granted for anyone who has ever recorded in the app, so this
runs with zero interaction. On a brand-new install that never recorded, the
first returns a clear "grant Screen Recording and retry"
error instead of hanging — surface that to the user; you cannot grant it for
them.
- One at a time. fails if a recording is already active —
call first.
- No mic on this path. Only screen (and optional ).
Record clean, then add narration with .
recording.stop --createProject=false
just finalizes the MP4 and returns
if you want to compose project.new --withMedia=…
yourself.
Shorts: turning an exported video into vertical clips
Discover shots (
), fork the source project per shot (
), the 9:16 vertical playbook, drift detection, and batch N shorts. Full detail:
. To make a short actually RETAIN — "make it engaging/viral", "edit like Hormozi / Ali Abdaal / a podcast clip" — load
reference/shorts-styles.md
: four evidence-based recipes with the seven retention laws, quantified caption/zoom/overlay parameters, and a render-frame verification pass. Load
reference/shorts-cheatsheet.md
alongside it — exact command shapes plus a hyperframes starter shell, so you never grep schemas or other reference files mid-edit. For
edits that should RETAIN (not just play clean), load
reference/longform-styles.md
— quantified from a 9-video measured study (Ali Abdaal / MKBHD / Fireship, July 2026): three recipes (educator-pip, product-review, dev-explainer), the two-level rhythm, keyword pops instead of burned captions, in-edit segmentation, and ending liturgy.
Shorts layout: full-frame vs camera-corner-over-blur
For a
camera-only clip in a 9:16 project,
project.set-shorts-layout
is the one-click layout picker:
bash
# Camera shrinks to a draggable bottom-right tile over a blurred copy of itself
pandastudio project.set-shorts-layout --id=$PID --layout=camera-corner
# Camera fills the frame (clears the transform + backdrop)
pandastudio project.set-shorts-layout --id=$PID --layout=full
sets the main-clip transform AND a
backdrop together; reposition the tile afterward with
project.set-screen-transform
(
/
are canvas-fraction center offsets,
the tile size). The two pieces are also independently settable:
project.set-backdrop --mode=blur-self|wallpaper
controls only the fill behind a scaled-down video (invisible while the video fills the frame). For a
screen-recording (screen+camera) clip, don't use these — use
project.set-webcam-layout --preset=picture-in-picture
, which already gives screen-fills-with-camera-corner. The blurred self-fill renders identically in preview and export.
Active-speaker auto-reframe: landscape multi-person → vertical (v1.70+)
When you crop a
landscape source with more than one person (a talk show,
interview, podcast panel, any director-cut footage) into 9:16, a single static
cover-crop lands on the gap between people in wide shots and off-face in
close-ups of whoever isn't centered.
is a
tracked
virtual camera that fixes this — the same approach Opus Clip / Vizard use:
- Shot detection (ffmpeg scene cuts) segments the source.
- Dense face tracking — MediaPipe FaceLandmarker (bundled, offline)
samples ~7fps, with adaptive tiling so small/far faces in wide shots are
still found. Detections are associated into per-person tracks.
- Audio active-speaker — on multi-person shots it frames whoever is
talking (mouth-open × speech-energy), not the biggest face.
- Smoothed camera — the crop pans to follow the subject within a shot
(with a dead-band hold + a safe-zone clamp so the face never leaves frame)
and cuts at shot boundaries.
bash
# Reframe every landscape clip — tracks + follows the active speaker.
pandastudio project.auto-reframe --id=$PID --json
# → { reframed: [{clipId, shots, shotsWithFace}], skipped: [...] }
# One clip only, or tune shot sensitivity / punch-in:
pandastudio project.auto-reframe --id=$PID --clipId=clip-1 --threshold=0.3 --minShotMs=500 --zoom=1.3 --json
# Revert to the plain static cover-crop:
pandastudio project.auto-reframe --id=$PID --clear=true --json
- This is the right verb (NOT ) whenever a
landscape source with multiple/alternating speakers is cut to 9:16.
sets ONE static point for the whole clip — correct only for
a single, stationary talking-head. For director-cut / multi-person footage,
reach for .
- Async + needs a renderer (bundled offline detection). It opens a hidden
editor for the pass; the DENSE tracking + audio makes it slower than a plain
edit — allow a couple of minutes for a few-minute source. Skips clips already
matching the canvas aspect (nothing to reframe) → reported under .
- : omit for the default ADAPTIVE punch-in (each speaker's face
sized to a consistent fraction of frame). Pass a fixed value (e.g. ) to
force a uniform punch-in on every shot.
- Set the 9:16 aspect FIRST (
project.set-aspect-ratio --aspect=9:16
), then
auto-reframe — the track is computed for the canvas aspect and is ignored if
the aspect later changes (recompute after an aspect switch).
- Preview and export render the crop identically, per frame — the preview
camera pans live. Verify from the EXPORTED mp4 across a close-up, a pan, AND a
wide two-shot (render-frame is fine too, but the export is the source of truth
near shot cuts).
- Also exposed in the editor as the "Track speakers" button (Video → Layout).
- v1 limitation: one subject per shot — a held two-shot where two people
banter frames the dominant talker (no mid-shot switching yet).
Publishing (YouTube + Instagram)
Hard rules: YouTube
defaults to
— never public without explicit user say; Instagram needs a Business/Creator account; never publish in the wrong workspace (confirm
). Flows: connect → publish an export. Full detail:
.
Memory — remember preferences across chats
You have a durable, per-workspace memory that persists across every chat. Its
current contents are already injected into your context each session under
"Durable memory (this workspace)" — so honor anything there without being
re-told. Grow it with three verbs:
bash
pandastudio memory.save --note="Channel is WritePanda; energetic tone, fast cuts"
pandastudio memory.save --note="Default caption template: editorial; brand accent #2563EB"
pandastudio memory.list --json # { entries: [{id,text}], count } — get ids
pandastudio memory.forget --query="a1b2c3" # by id, or a text fragment
When to save: the user states a STANDING preference or fact worth carrying
forward — brand, default caption/zoom styles, channel name + tone, a recurring
instruction ("always 9:16 for this client", "never add background music").
When NOT to save: one-off requests about the current edit ("trim the first
10s", "make this clip louder") — those aren't memory.
Save proactively when you notice a durable preference, but keep entries concise
(one fact per note) and use
when something the user tells you
supersedes an old note. Memory is per-workspace, so an agency's clients never
share it.
Editorial decisions — what to ask, what to assume, what NEVER to ask
Video editing is a creative task with hundreds of small decisions. Asking the user about all of them kills the magic — they came to you because they wanted to type "edit this" and see something happen. Asking about none of them produces wrong-shape output. The rule:
Ask only when the answer is genuinely user-specific AND can't be inferred AND is hard to reverse. Default everything else, narrate what you did, and iterate via preview.
The default edit pipeline (vague "edit my video", no specifics)
When the user asks to edit / polish / clean up a video without naming a
specific operation, this is the intended end-to-end pipeline, in order:
- Transcribe any clip where
clipStates[i].transcribed === false
().
- Remove filler words + immediate repeats (
transcript.remove-fillers
).
- Fix transcript spelling / STT errors. Read the transcript and correct
obvious misspellings — especially product, brand, person, and technical
names the speech-to-text got wrong (e.g. "Right Panda" → "WritePanda") —
with (patches the word text in place, keeps
timing). Do this BEFORE captions, motion graphics, or title generation —
they all derive their text from the transcript, so a typo propagates.
only rewrites words that already exist. When STT DROPPED a
word entirely (the transcript is missing a spoken word), use
instead — anchor it with (or
to add at the very start) from , and pass
. It computes plausible timing (fills the gap the drop left, sized to
the local speaking rate) and is non-destructive — existing word ids, and any
trims/zooms/captions anchored to them, are untouched. It does NOT re-transcribe.
- Cut bad takes. Run (read-only — it never
edits). For each / , the default is to keep
the most recent (last, cleaner) take and delete the earlier attempt —
feed the candidate's (which point at the discarded attempt) into
. EXCEPT candidates — those
are REVIEW-class: KEEP them by default. A low-severity
means the restart diverges from the fragment, which is often deliberate
parallel structure ("one for transcription, one for outreach"), not a flub
— deleting it destroys the sentence. Only delete a low candidate when the
surrounding context clearly shows an abandoned take. If a candidate is
genuinely ambiguous (the repeat might be intentional emphasis, or you
can't tell which take is better), ask the user which take to keep
rather than guessing.
- Remove silences (
transcript.remove-silences
, 600ms default — same as
the UI Remove Silences button) — after
content cleanup so it tightens the final timing. The verb returns
+ the new (synchronous — you'll know immediately
how many it cut). Every transcript cleanup step (2, 4, 5) ADDS trims and
shifts the edited timeline, so always finish cleanup BEFORE placing
graphics/zooms/lower-thirds (steps 8–9). If you ever place a region from a
transcript word before cleanup is done, pass so it
re-anchors when the timeline shifts. (If the user already removed silences in
the UI, a fresh shows the new /
/ — treat that as "silences already done".)
- Clean audio () on clips where .
- Add captions — + (default
per profile; see the caption styles in "DO BY DEFAULT").
- Add motion graphics — follow the Motion-graphics Rules + selection
guide: first, vary templates by beat, prefer the featured
(premium) templates ( / / the Vox family),
and for camera-only / imported footage lead with or
designed segments (not the plainer ). On any
talking-head (), open with a
caption-editorial-emphasis
TOPIC card in the first 10–30s that names what the video is about (from the
speaker's opening lines) — the default hook for talking-heads. For explainer
content, author custom animated diagrams / flowcharts / charts when the
speaker explains how something works or connects and no template captures it
(see "Authored graphics") — don't flatten a real explanation into a bullet
list.
- Add emphasis zooms — punch in on the key beats for a dynamic, edited
feel (see "Emphasis zooms" just below).
- (Only when the brief is "make it engaging / cinematic / dynamic / give it
energy", NOT a plain "clean it up") — add scene transitions at the
real section boundaries (, ~1 per major section, one
consistent style). See the "Effects (FX) & transitions" section — restraint
is the rule: a transition belongs at a section change, never on every cut.
Do NOT add FX overlays here — FX is explicit-request-only (see the
"NOT part of the default pipeline" list below); "make it engaging" does
not authorize adding effects.
- Generate title / description / timestamps, then preview.
Do not skip steps, and report what actually ran. Every step the user
confirmed for the full polish must be an ACTUAL verb call this session. Three
steps are skipped far too often — none of them is optional in a full polish:
transcript.remove-fillers
— vocalised pauses (um, uh, uhm, umm, hmm, hm) AND immediate repeated words. Default behavior is the SAFE tier only — the words above are sounds, never lexical, so removing every match is unambiguously correct. Pass to additionally remove like / you know / i mean / sort of / kind of
; these are real English words too, so the aggressive mode will sometimes cut legitimate uses ("I like this template" loses "like"). Only opt in when the user explicitly asks for a thorough cleanup AND is willing to skim the result for false positives.
- → — bad takes and
repeated phrases. is read-only; you MUST then actually delete
the discarded (keep the most recent take). Running and
not deleting is the same as doing nothing — the bad take stays in the video.
( false-starts are the exception — REVIEW-class, keep by
default; see the transcript verbs table.)
transcript.remove-silences
— the single most-skipped step; silence
removal is what makes a talking-head edit feel tight.
After the pass, your summary MUST quote the real result each verb returned
(e.g. "removed 14 fillers, cut 2 bad takes + 3 repeated phrases, removed 67
silences, captions on, 4 graphics, 6 zooms"). Never claim a step happened
unless the verb actually ran and you saw its result — "edited everything" with
bad takes, repeats, or silences still in the cut is a failure the user notices
immediately. If a step legitimately returned 0, say so explicitly rather than
omitting it. Treat the numbered pipeline as a checklist: before reporting done,
confirm each item was run (with its count) or consciously skipped for a reason.
Ask-first — exactly once, for scope. Because this is a large, visible
transformation, when the request is a vague "edit my video", confirm scope in
ONE message before running it: list the pipeline above and ask "Want me to do
the full polish — remove fillers/repeats/silences, fix transcript typos, cut
bad takes (keeping the latest), clean audio, add captions, motion graphics, and
emphasis zooms? Or just some of it?" Combine this with the destination-profile
question if that's also unknown — one message, not two.
- On yes (or if they said "just go" / "do everything" / "use defaults") →
run the whole pipeline without further per-step asking, then narrate.
- If they named a specific operation ("just add captions", "only remove
silences", "add a lower third") → do exactly that and nothing else.
NOT part of the default pipeline — add ONLY on explicit request:
- Background music. Never add a music track to "edit my video" / "do the
full polish". Add it only when the user explicitly asks ("add background
music", "put a track under it"). "Full polish" does NOT include music.
- Intro / outro cards. Never open with a title card or append an outro/CTA
card unless the user explicitly asks for one ("add an intro", "add an outro").
A plain edit keeps the user's footage as the first and last frame.
- FX overlays (). Never add an FX overlay (film burn, light
leak, grain, VHS, embers, film-flash, etc.) on your own — NOT on a plain
edit, and NOT even for "make it engaging / cinematic". FX is a deliberate
creative choice the user makes; add one only when they explicitly ask for an
effect ("add a film burn between these clips", "put some grain on it", "add a
light leak"). When they do, follow the restraint rules in the "Effects (FX) &
transitions" section. (Transitions are different — those ARE part of the
engaging-tier flourish; see step 10.)
If you think music, an intro/outro, or an effect would help, you may suggest
it in your narration ("want me to add a music bed, an intro card, or a film
burn?") — but do not add it until they say yes.
Emphasis zooms — punch in on the key beats
A perfectly static frame reads as unedited. Adding
pushes on
the moments the speaker emphasizes — the payoff word, a "look at this", a key
number, a name reveal — gives the cut a dynamic, professionally-edited feel,
and is part of the default polish. Find the beats from the transcript
(emphatic phrasing, the point of a sentence, a stated result) and place a
~1.5–2.5s zoom on each. Depth: modest (2–3) for talking-head emphasis; punchier
(3–5) for a UI/detail reveal.
For recordings, prefer
cursor-telemetry-driven zooms; for /, place them on verbal
emphasis. Don't over-zoom — aim for a real beat roughly every 10–20s, not a
constant push. (Don't pre-ask about zoom positions; infer and let the user
redirect via preview.)
MUST ASK (3 things, only when context is missing)
<HARD-GATE>
Before issuing `project.new`, `project.add-*`, `motion.generate`, or `motion.render-html` calls that depend on user-specific information, you MUST have:
1. Destination profile. This single answer drives aspect ratio, pacing, zoom cadence, caption template, and whether to add lower-thirds. (Background music and intro/outro cards are NOT profile defaults — add them ONLY when the user explicitly asks; see the default edit pipeline.) Try to infer first, then fall back to asking:
Inference rules (in order — first match wins). Everything below the user's
own words comes from ONE
project.read --includeTranscript=false
you're
running in pre-flight anyway — this costs zero extra calls:
- User mentioned "Shorts" / "TikTok" / "Reels" / "Instagram story" / "vertical" / "phone"? → profile (9:16, punchy)
- User mentioned "LinkedIn" / "client pitch" / "professional" / "corporate"? → profile (16:9 or 1:1, restrained)
- User mentioned "Loom" / "internal" / "async update" / "for the team" / "quick video"? → profile (minimal editing)
- User mentioned "YouTube" / "long-form" / "tutorial" / "vlog" / "channel"? → profile (16:9, full pipeline)
- Project was created by ? → (it exists to BE a short)
- Project aspect ratio is already 9:16 or 1:1 (project setting, not just source orientation — the user chose that canvas)? →
- Workspace project defaults name a destination (
workspace.get-project-defaults
)? → use it
- Duration bands (total source duration):
· ≤ 90s → — regardless of orientation; nobody publishes a 45s "long-form video", and a landscape short gets auto-reframed to 9:16
· > 8 min → (or if it's a screen recording with cursor telemetry and no user framing — internal walkthroughs look exactly like this)
· 90s–8 min → genuinely ambiguous: could be a long-form video OR source to cut a short from. Use content type as tiebreaker (screen recording → youtube-long/loom; phone-framed talking head → shorts), else ASK.
- Source clip is portrait (height > width), no other signal? →
- Source clip is landscape, no other signal? → (safe default — most common)
- Ambiguous or no clips yet? → ASK ONE QUESTION:
"Where is this going — YouTube long-form, Shorts/TikTok/Reels, LinkedIn, or internal/async (Loom-style)?"
When the user says "just go" / "use defaults" / "I don't care" →
unless the duration band says shorts (a 60s clip with "just go" is a short).
Special case — LONG recording + short-form ask ("make a short from this",
"clip this for TikTok"): that's not one profile, it's the fork-from-shot
pipeline (reference/shorts.md) — discover shots in the long source, fork a
9:16 project per shot, then apply the
profile to the fork.
The profile is the source of truth for every default below. See the
Video editing playbook section for the full profile table.
2. Lower-third content. If the user said "add a name plate" / "introduce me" / "add a lower-third" but didn't give the actual text, ASK both fields in one message:
"What's the name and the subtitle (e.g. role / company)?"
Don't fabricate a name. Don't invent a job title. Skip this question entirely for the
and
profiles — they don't use lower thirds.
3. Brand / style direction. If the user named a style ("MrBeast" / "MKBHD" / "Vox" / "Kurzgesagt" / "Veritasium" / "Linear" / "Infinite"), first see whether a bundled template fits and set its colors via
(most accept brand / ink / accent colors). Only when no template matches the named look, author custom HTML — translate their palette, typography, and motion vocabulary into a composition built against
reference/motion-philosophy.md
§1. If they gave no style reference AND there are multiple source clips that suggest a brand context, ASK ONE QUESTION:
"Any brand colors, fonts, or visual references — or default look?"
4. Templated vs. custom for a from-scratch video. When the brief is a video
built FROM SCRATCH where the motion graphics ARE the whole video (promo,
explainer, intro/outro, product teaser, CTA piece — no source clip on the main
track), do NOT silently reach for bundled templates: they read as generic for a
hero/marketing asset. Ask once up front:
"Templated quick version, or fully custom? For a promo I'd default to fully custom scenes — they look bespoke, not templated."
If they don't care,
default to fully custom (
, scenes
authored from
reference/motion-philosophy.md
). Only use templates as the
backbone here if they explicitly choose speed — and say so. (This question does
NOT apply to Mode A — graphics layered over existing footage — where templates
stay the default and you should not ask.)
5. Voiceover & music for a from-scratch promo. Narration and a music bed are
first-class capabilities (
—
local on-device Kokoro
by default for English, no key; cloud Replicate TTS via
for more
voices/languages;
--model=elevenlabs-direct
uses the user's OWN ElevenLabs
account (their CLONED voices — pass the voice name or voice_id; needs the
ElevenLabs key in Settings → Integrations); see reference/media-generation.md;
/ bundled
). For a Mode-B piece (promo,
explainer, intro/outro, teaser), if the brief doesn't already specify,
ASK up
front whether to add them:
"Want a voiceover and/or a music bed? Both shape the timing, so I'll lock them in before building the scenes."
ALWAYS pass when placing a VOICEOVER with
. Audio overlays are never transcribed otherwise —
only walks main-track clips — so a narration-driven
video ends up with NO transcript, which means no captions and no way for you
to read back what was said. The flag transcribes the narration and merges its
words into the project transcript at the overlay's position. It returns a
;
on it before calling
or any
transcript verb. Do NOT pass it for music or ambience.
bash
NARR=$(pandastudio media.generate-narration --text="..." --json | jq -r '.data.path')
OUT=$(pandastudio project.add-audio --id=$ID --audioPath="$NARR" --startMs=0 \
--transcribe=true --json)
pandastudio job.wait --id=$(echo "$OUT" | jq -r '.data.transcribeJobId') --json
pandastudio caption.toggle --id=$ID --enabled=true # now has words to render
They materially change the build —
if there's narration, generate the VO
FIRST and time each scene to its line length (TTS runs longer than you'd
guess; visuals-first forces a re-time pass). See "Audio: decide voiceover &
music FIRST" in
reference/promo-and-mg-videos.md
.
Don't silently ship a silent promo when audio was available.
If these are clear (or already specified), proceed without asking. Combine multiple asks into a single message when possible.
</HARD-GATE>
DO BY DEFAULT, narrate transparently
For these operations, run them without asking and tell the user what you did in the same message. Every one is reversible (trims are spans, cleaned audio is a sibling file, generated text is just text — nothing is destructive).
Before running or , always call and inspect the array in the response. Each entry looks like:
json
{ "clipId": "clip-1", "mediaPath": "...", "durationMs": 62400,
"transcribed": true, "wordCount": 312,
"audioCleaned": false, "kind": "camera" }
- → skip for that clip — it already has a transcript. Running it again would overwrite any manual word edits the user made in the app.
- → skip for that clip — the already exists.
- → how the clip was captured: (talking-head — a PandaStudio camera-only recording), (screen recording, maybe with a webcam PiP), or (external import). This is the authoritative signal for your visual strategy — use it, don't guess from aspect ratio: (talking-head) OR (imported video) → there's no screen to zoom into, so lead with a premium designed segment — or (
project.add-designed-segment
) as the default for explainer beats; it's the highest-leverage way to make static footage look produced ( is the plainer fallback — see the Motion-graphics "Rules" §5). → use cursor-telemetry zooms, never clip-transform splits. On v1.28+ recordings this is stamped at capture; older projects don't have it, so infers it (paired webcam track or cursor telemetry → ; managed-dir media → ; else ) and sets .
When is inferred or absent, NEVER assume . is the costly wrong guess — it suppresses the camera enhancements (designed segments, emphasis zooms) and a talking-head ends up flat. So:
- A clip flagged is a hint, not gospel. If it reads but you have any doubt (the footage is a person talking, not a UI; no cursor), treat it as — the default when unsure is always , never .
- If it genuinely matters and you can't tell, ask the user ("Is this clip a screen recording, or you on camera?") rather than guessing .
- Once known, lock it:
project.set-clip-kind --clipId=… --kind=camera|screen|upload|podcast
stamps an authoritative value so no future agent has to re-guess (ideal for pre-v1.28 projects — stamp each clip once). = a two-speaker composite (host=mediaPath, guest=webcamPath) recorded via Record Podcast; it's set automatically at ingest and edits as one clip with the webcam layout.
- (top-level on the result, not per-clip) → a count summary
{ total, duplicateTakes, falseStarts, adjacentRepeats }
, present only when something is transcribed. If , run during the polish pass to see the actual candidates.
Only pass un-processed clips to each operation. If every clip is already transcribed, go straight to
.
| Operation | Default behaviour |
|---|
| Run only on clips where clipStates[i].transcribed === false
. Skip the rest. |
transcript.remove-fillers
| Default: auto-remove vocalised pauses (um/uh/uhm/umm/hmm/hm) + immediately-repeated words. Trim regions; fully reversible. Pass to ALSO catch lexical-word fillers (like, you know, i mean, sort of, kind of) — these can wrongly cut legitimate uses ("I like this template" loses "like"), so opt in only when the user wants a thorough cleanup. |
| Run after remove-fillers. Surfaces re-takes (), abandoned restarts (), and stutters () as candidates — each with the of the discarded attempt. Read-only — it never edits. Default: keep the most recent (last) take and delete the earlier attempt by feeding the candidate's into — but candidates are REVIEW-class: keep them by default (a low = the restart diverges from the fragment, often intentional parallel structure like "one for transcription, one for outreach"). The detector already skips comma-terminated parallel list items and lone stopword "repeats" across pause tokens. Review against context first — if a repeat looks intentional (emphasis) or you can't tell which take is cleaner, ask the user which to keep rather than blind-applying. |
transcript.remove-silences
| Run after the content cleanup. Runs the SAME two passes as the UI Remove Silences button and unions them: (1) transcript word-gaps (leading, between-word, trailing) and (2) ffmpeg audio-level on each clip's media — pass 2 catches real dead air the transcript misses when speech-to-text invents phantom words over quiet stretches, which is why this now removes the same sections a manual click would (it previously did pass 1 only and left audio-only silence behind). Default threshold 600ms; don't hand-pick a higher value "to be safe" — that leaves dead air the user expects gone. |
| Denoise only clips where clipStates[i].audioCleaned === false
. Writes a sibling ; original audio untouched. |
| (when user said "add captions" without naming a style) | Default to . Static styles: classic, modern, minimal, spotlight, boxed, neon, colored, editorial
( = magazine emphasis: the spoken word renders big + accent while the rest shrinks). Animated, transcript-driven styles (each word animates as it's spoken, identical in preview + export): (words slam in), (wipe reveal per word), (gradient text, elastic pop), (character scramble resolves), (RGB chromatic split), (auto-inverts over any footage). Reach for an animated style for Shorts/TikTok energy; keep / for long-form. |
| / / | Generate after the edit pass. Show the user; let them say "regenerate" or "use this exact title" or edit inline. |
| Specific zoom moments | Heuristically pick from the transcript ("you said 'click here' at 12.4s — adding a zoom"). Don't pre-ask. Iterate via preview. |
| FX overlays () | NOT do-by-default. Never add an effect on your own — not on a plain edit, not for "make it engaging". Only when the user explicitly asks for one ("add a film burn", "put grain on it"). When they ask, place it where they said and follow the restraint rules. |
Example of good narration after an edit pass:
"Edited.
• Transcribed both clips (136 words).
• Removed 14 fillers + 3 repeats. Trim regions are reversible — say 'undo fillers' if you want any back.
• Cut 2 bad takes (kept the cleaner second attempt each time).
• Removed 67 silences (>600ms), tightening ~48s of dead air.
• Cleaned audio with DeepFilter on both clips.
• Added a zoom at 12.4s where you said 'click here'.
• Captions enabled with the bold template (8 other styles available).
• Generated a title: 'How I Built This in 24 Hours' — say if you want a different angle.
Opening preview now."
Note how the summary names the actual count for every step that ran —
including silences. That's the bar: a summary line backed by a real verb call
and its returned result, never a vague "cleaned it up".
NEVER ASK about
These produce friction with no upside. Just decide.
- Whether to remove filler words (it's reversible — do it)
- Whether to remove repeated words (same)
- Whether to clean audio (sibling file, original untouched)
- Specific zoom positions or focus points (infer; user redirects via preview)
- Specific caption colors / font sizes (template defaults are good)
- Whether to generate a title / description / timestamps (cheap, useful)
- Whether to enable captions when user said "add captions" (yes — they said yes)
- Specific lower-third design/colour (use defaults; user can swap)
Preview, then export. Never export, then preview.
After a meaningful edit pass:
- Call (opens the editor focused on the project — same single-step UX, ~2-3s).
- Tell the user what you did (the narration block above).
- Ask: "Does this look right? Anything to tweak before I export?"
- Only after explicit user confirmation call .
Export is 30-90s and produces a multi-MB MP4. Wasting an export because you skipped the preview is the worst UX failure in this surface. The editor's preview pane shows every effect / caption / FX / lower-third / motion graphic / zoom region exactly as the export will render them.
Output modes
- Default: pretty one-line summaries to stdout. Show these to the user.
- : raw envelope. Always use when you intend to parse the response or chain commands — pipe through .
Discovery (the most important habit)
Don't memorise the command surface — the registry is the source of truth and it grows. Every PandaStudio launch self-describes:
bash
pandastudio commands --json # full schema with arg hints per command
Pattern-match
against the user's intent. If you can't find a verb that fits,
say so rather than fabricating one.
Async jobs
and any future
return a
immediately — the
block does
not carry the result. Wait server-side:
bash
pandastudio job.wait --id="$JOB" --timeoutMs=120000 --json
Terminal
is
succeeded | failed | canceled
. Read
for the rendered MP4.
Argument shape
Flags are either
scalars (
) or
JSON (
). Anything starting with
or
is parsed as JSON. Strings stay strings;
/
/ numbers auto-coerce.
Error model
Every response:
{ ok: boolean, data?: ..., error?: string, details?: ... }
.
- HTTP 4xx/5xx → transport problem. CLI exits ≥ 1 to stderr.
- HTTP 200 + → handler-level error. CLI exits 1 with to stderr. The most useful machine-readable codes:
- / — show the user the license activation flow
- — typo; run to recover
invalid or out-of-tree project path
— project paths must live under the user's recordings dir; never pass arbitrary absolute paths
Composing a real edit
HARD INVARIANT — every video the agent creates lives in a project.
Whatever the brief — promo, explainer, transcript-driven edit, PDF-to-video,
a single motion graphic, B-roll over a recording, a one-shot title card —
the
editor project is the deliverable, not the raw MP4. Loose files in
~/Library/Application Support/pandastudio/recordings/
are
intermediates
the editor consumes; they are never the agent's hand-off.
The two valid starting states:
- A project is already open in the editor ( returns
a non-null id) → use it. Add your work to that timeline. Save. Preview.
- No project is open ( returns null, or the chat opened
from the home screen) → is your FIRST tool call, before
any motion render / transcription / b-roll generation. Name the project
something the user will recognise ("PandaScribe Promo", "Q4 Recap",
"How to install — explainer"). Pick the aspect ratio from the destination
profile (16:9 YouTube, 9:16 Shorts, 1:1 LinkedIn square).
After the work lands in the project,
preview.show --id=<project-id>
to
open it in the editor for the user. THAT is the hand-off. The chat message
reports the project name/id, not a file path on disk.
Exceptions are vanishingly rare — only when the user explicitly says
"just give me the MP4, no project" (e.g. they want to upload it elsewhere
right away). When in doubt, default to project.
Flow is always:
create or open a project → add things → save (conflict-safe)
→ preview. Every verb's arg schema is discoverable at runtime — call
(MCP) or
(CLI), or see
. So this section is the
judgment the schema can't give
you: which verb, in what order, and the non-obvious gotchas.
Target the right project
project.new --withMedia='["/a.mp4","/b.mp4"]'
— create pre-loaded; clip
durations are FFmpeg-probed automatically.
- (or ) — EXACT copy of a project: every edit
(trims, zooms, speed, crops, overlays, captions, spotlights, transcript, aspect
ratio) is preserved. The copy gets a fresh id and a name and a
new file; the source is untouched (non-destructive) and media
files are shared, not copied. Returns . Use when the user
asks to duplicate / copy / clone a project — e.g. to try a variant edit without
disturbing the original.
- → the open editor project (
{id,path,name,revision,clipCount}
or ). Use it when the user says "this one" / "what's open" — don't ask
for an id. ≠ "no projects exist"; fall back to .
- → full state. Key fields the schema won't spell out:
- clips at — each carries (the
clip's own length); there is no per-clip on the raw clip.
For a normalized per-clip view use the top-level , where each
entry has , (= sourceDurationMs), , .
- motion-graphic / transition overlays at
editor.mediaOverlayRegions[]
.
- audio overlays (voiceover, music) at top-level
— NOT under . Different array from the visual overlays.
- top-level read summary fields: ,
(post-trim — use for cadence planning), , ,
, and per-clip (//,
your visual-strategy signal).
Pass
--includeTranscript=false
after the first read.
Adding things — the gotchas (call discovery for the arg schemas)
-
Clips: (
prepends),
,
,
.
is permanent — no trash. By default it only
removes the project file and KEEPS the original source recording. Pass
to ALSO delete the original recording file(s) from disk
(irreversible — only do this when the user explicitly asks to delete the source
footage, not just the project). Returns
(count removed).
-
Motion graphics — use , NOT , for render outputs. Pass
the render
(after
); the server resolves the path itself:
bash
JOB=$(pandastudio motion.render-html --htmlPath=/tmp/card.html --durationMs=4000 --json | jq -r '.data.jobId')
pandastudio job.wait --id="$JOB" --json
pandastudio project.add-motion-graphic --id=$ID --fromJob="$JOB" --durationMs=4000
is only for external uploads and
must be quoted — render outputs
live under
(a space); an unquoted path truncates and
silently produces a dead overlay (shows on the timeline, never renders).
-
Placing on a spoken word in a trimmed timeline: trims make source-time ≠
output-time. From
, each word has
(source) and
(output;
= inside a trim → skip it). Pass the
source
as the position
and so the region
re-anchors when later cleanup trims shift the timeline.
timeline.source-to-edited --sourceMs=N
returns the output time (or
if trimmed).
-
Mid-video graphic on camera / upload footage: don't cover the host — use
project.add-designed-segment
(the split-panel beat; see Motion-graphics
Rules §5). Screen recordings use
instead, never a split.
Never place a zoom inside a designed-segment / split window — the camera is
already cropped to a half-frame band, so zooming it looks wrong. Zoom the
full-frame stretches, split the rest.
-
Zoom depth → scale factor. (1–6) maps to a fixed zoom multiplier.
When the user asks for "a 1.5x zoom," translate it with this table — there is
no separate scale arg:
| depth | scale | feel |
|---|
| 1 | 1.25× | barely-there nudge |
| 2 | 1.5× | soft, modern default (talking-head, tutorials) |
| 3 | 1.8× | clear emphasis (UI clicks, callouts) |
| 4 | 2.2× | strong punch-in |
| 5 | 3.5× | dramatic detail |
| 6 | 5.0× | extreme macro |
-
Zoom / fx / SFX: (default depth 2 = 1.5× + swoosh SFX;
to silence),
(default mouse-click SFX
as of v1.36.0),
(13 bundled FX overlays — film-burn, light-leak, light-flare, lens-flare-sweep, light-streaks, bokeh-drift, prism-leak, dust-scratches, film-grain, vhs-static, embers, snow-drift, film-flash;
adjusts loop speed, default 1; see the "Effects (FX) & transitions" section for when to reach for each),
(retune/clear a placed region's
SFX). Arg values: discovery (
).
-
Spotlight / blur (v1.50.0): add-spotlight --atMs=<ms> --durationMs=<ms> [--kind=spotlight|blur]
— a focus rect over the video.
(default) DIMS everything outside the rect (draw the eye to one spot);
BLURS everything inside it (hide an email, username, or other
sensitive detail in a screen recording). Rect placement is
as 0..1 fractions of the video (default a centred half-size box,
x=y=0.25 width=height=0.5
).
(px corner radius, default 16),
(px soft edge, default 12). Spotlight:
0..1
(surround darkness, default 0.6). Blur:
px (default 12). The
rect tracks content through zooms.
is EDITED time (no anchor arg).
Edit or delete after placing (v1.85.0):
update-spotlight --regionId=<id> [--startMs --endMs --kind --x --y --width --height --roundness --feathering --maskOpacity --blurAmount]
patches only the fields you pass (move/resize,
retime, restyle, or flip spotlight<->blur);
remove-spotlight --regionId=<id>
deletes it. Get ids from
under
editor.spotlightRegions[].id
.
-
Background blur / removal / studio image + person outline (v3.69.0; outline v3.75.0; studio image v3.104.0):
add-background-effect --mode=blur|remove|image [--atMs=<ms>] [--durationMs=<ms> | --endMs=<ms>] [--strength=<px>] [--backgroundImage=<studioId|path> --backgroundFit=cover|contain] [--outline --outlineWidth=<px> --outlineColor=<hex> --outlineShadow=<bool>] [--anchorSourceMs=<srcMs>]
— AI PERSON SEGMENTATION on the camera video for
the region's span, exactly like a zoom region on the timeline (draggable,
trimmable, source-anchored, rebases on trims/speeds).
keeps the speaker sharp and gaussian-blurs everything behind
them (video-call style;
is px sigma at 1080p, default 18).
cuts the background away entirely so the project
wallpaper/background shows through behind the person — pair it with
for a branded backdrop, OR with a
background-layer media
overlay to put an IMAGE/VIDEO behind the speaker (the Shorts look): add the
media with
add-motion-graphic --file=<img/video> --layer=background
(or flip
an existing overlay with
update-region --regionType=overlay --layer=background
;
in the UI, right-click the overlay → "Send behind video"). The removed-
background speaker composites OVER that overlay in preview and export.
is the
virtual studio: it removes the real background and
composites the speaker over a STUDIO PLATE — the one-call way to put someone
with a messy room into a clean studio. Set
to a bundled
studio id:
(soft warm key),
(cool RGB rim),
(soft even),
(warm tungsten),
(natural daylight),
(magenta/teal),
(dramatic
side) — or an absolute/
/
image path for a custom background.
Defaults to
.
(default, fill+crop) or
(fit whole plate). Pick a plate lit like the footage for the most
natural composite. No outline by default (it's a real background, not a
cutout). Same matte tuning as below applies.
Matte boundary tuning: manually tightens the person
edge — >0 pulls it INWARD (kills a background fringe / cleans a loose cut),
<0 pushes it out (−60..60);
softens the edge (0..60).
Both apply to blur AND remove (they adjust the person matte the outline is
built from too).
is a colored keyline + drop shadow that hugs the person —
the "VOX magazine cutout" look.
It is ON BY DEFAULT for
(v3.80.0): a bare cutout reads as unintentional, the keyline makes it look
designed, so removal ships the VOX look out of the box (pair with a cream
for the reference look). Pass
to remove the
background with NO keyline. For
it's off unless you pass
.
px@1080p (default 36),
hex
(default
),
bool (default true). Duration defaults
to 5000ms. CAMERA / TALKING-HEAD footage only — pointless on screen recordings.
Runs on a bundled on-device model (no network); preview and export render
it identically. Regions live under
editor.backgroundEffectRegions[]
(
.outline = {enabled,width,color,shadow}
); retime/restyle with
update-region --regionType=background-effect [--startMs --endMs --mode --strength --backgroundImage --backgroundFit --outline --outlineWidth --outlineColor --outlineShadow --matteContract --matteFeather]
, delete with
remove-region --regionType=background-effect
. Also batchable inside
as
{op:'add-background-effect',atMs,durationMs,mode, strength?,backgroundImage?,backgroundFit?,outline?,outlineWidth?, outlineColor?,outlineShadow?,matteContract?,matteFeather?}
.
-
See a frame to place it (v1.85.0): render-frame --atMs=<ms> [--outPath=<png>]
composites the preview frame at that edited-time to a PNG and returns
{ path, width, height, timeMs, maskRect }
. A vision model should
the
returned
to LOCATE on-screen text/UI (e.g. the email to blur), then place
a focus region.
is the video content rect as 0..1 fractions of the
image — the SAME space as spotlight/blur x/y/width/height. Convert an image-space
box (ix,iy,iw,ih) to region coords:
x=(ix-maskRect.x)/maskRect.width
,
y=(iy-maskRect.y)/maskRect.height
,
,
height=ih/maskRect.height
. Typical privacy-blur flow:
→
read PNG → locate text →
add-spotlight --kind=blur
with converted coords →
again to verify →
. Caveats: existing focus
regions are NOT drawn (you see content clearly); the frame reflects any active
zoom at that time, so prefer an un-zoomed moment for placement.
-
Transitions (v2.98.0): add-transition --transitionId=<id> --atMs=<cutMs>
—
places a scene-change overlay CENTERED on a cut (the opaque peak masks the
join). Pass
= the cut time between two clips (from
clip
boundaries).
here is EDITED (output) time, and add-transition has NO
— unlike add-zoom / add-motion-graphic. If you only have a
source-time value (a transcript word's
), convert it first with
timeline.source-to-edited --sourceMs=N
and pass the result. Ids
(
): fade-black, fade-white, flash, light-sweep,
film-burn, glitch, scribble.
defaults to 1000 (the
hand-drawn
is authored at 2200ms — pass
to keep its scribble-on/clear beats intact). Always prefer
over this static list — it is the source of truth.
Time domains at a glance. /
/
are ALWAYS edited
(output) time.
(source/raw-recording time) is an
extra
drift-proofing arg on
add-zoom, add-motion-graphic, add-designed-segment,
add-lower-third — pass it alongside
when
came from a
transcript word so the region re-anchors across later trims. Verbs WITHOUT
an anchor (
add-transition, region edits) need a pre-converted edited time.
-
Lower thirds (v2.96.0): project.add-lower-third --name="…" --title="…" --atMs=<ms> [--templateId=lt-*]
— ONE async call that renders the nameplate
template (default
) AND places it as a transparent overlay.
Returns
;
resolves once the region is placed. Pass
when atMs comes from a transcript word. The 10
designs are in the template catalog; they also live in the editor's
Lower 3rds tab. (The pre-2.95 CSS designs +
are gone;
the verb now drives the motion-template pipeline.)
-
Reset: — one atomic call wipes every region +
audio overlays + turns captions off (keeps clips, transcript, aspect ratio;
also resets LUT/crop/webcam/wallpaper). Use it for "start over";
do NOT loop
(that's for removing ONE region by
+
).
Conflict-safe save
The editor autosaves, so two writers (you + the editor, or two agents) overwrite
each other silently unless you pass
(from
's
). On conflict you get
{ code:"revision_conflict", expected, actual, onDiskProject }
— re-read,
re-apply your change, retry. All
verbs accept it too.
Preview without exporting
pops the live WYSIWYG overlay (~1–2s boot;
,
).
moves the playhead;
closes;
inspects it.
Call after every significant edit
so the user sees the change live without leaving the chat. (
opens
the full editor — heavy, for handing off to the user.)
Motion graphics
PandaStudio ships a curated set of
YouTube-creator templates. They are
the primary way to add motion graphics — production-grade, editable, and
faster than authoring HTML. Custom HTML (
) is the
fallback for briefs no template fits (see the next section).
Rules — recommendations, not rigid law (bias toward DOING)
These are strong defaults, not handcuffs — use your judgment. The worst outcome
is a near-empty timeline because you were being cautious. A video full of
points should be full of graphics.
- FIRST, every time. Discover the catalog + each template's
editable slots at runtime; don't generate from memory or copy a templateId
out of an example.
- Add a graphic on most meaningful beats — don't be shy. Name-drops,
claims, numbers, lists, comparisons, tool/product mentions, section changes
— each is a candidate. If a 5-minute video has ten clear points, ~ten
graphics is reasonable. Under-graphicking is as much a failure as the
wrong graphic — when a beat clearly wants a visual, add one.
- Vary every scene — consistency comes from a shared SYSTEM, not a repeated
layout. Hold ONE design system across the video (same palette, type family,
motion vocabulary) so it feels cohesive — but give every scene a distinct
composition. Reusing one layout (or one template) beat after beat with only
the text swapped is the #1 way a video reads as a flat, templated slideshow —
and it is fatal for a from-scratch / promo video where the graphics ARE the
video (see
reference/promo-and-mg-videos.md
).
The ONLY repetition that belongs is a recurring functional overlay on real
footage — e.g. the same lower-third name-plate style each time a person is
introduced — which is a consistent repeated ELEMENT, not a repeated SCENE.
(This replaces the earlier "repetition is good, reuse freely" guidance, which
produced templated, monotonous output.)
- The ONE hard line: never misuse a purpose-specific template. A few
templates mean something — use them only when the content matches:
→ a real number (never a generic title) · →
exactly two things contrasted · → an actual sequence of steps ·
→ a list of points · → introducing a
person/channel. Everything else (the title family, , the
parallax reveals) is generic — usable on any beat — but still vary the
composition beat to beat; don't lean on one look for the whole video (Rule §3).
- Camera-only / imported footage → lead with a PREMIUM designed segment.
When the clip's is (talking-head) or (imported),
there's no screen to zoom into, so a static frame is what makes it look flat.
A designed segment (host one half, a panel the other, via
project.add-designed-segment
) is the highest-leverage fix and your
default workhorse for explainer beats on this footage. Default to
or — those are the featured, high-production
panels that instantly level a video up. Use only as a plainer
fallback or for variety, NOT as the go-to. Use the panels liberally (alternate
side + content). For , prefer cursor-telemetry zooms;
don't split the frame.
- Reach for the FEATURED (premium) templates first. Six templates are
flagged top-tier in () — they're the most
produced looks we ship, and on an "engaging / cinematic / level it up" brief
you should prefer them over the plainer templates (, the basic
title cards):
- Strong workhorses: , (side panels) and
(the highlighter headline reveal) — reach for these on
explainer beats over real footage, but vary the side, content, and which
one you use; don't repeat a single panel across the whole video (Rule §3).
- Reach for the moment the content gives you the hook: (a real
number / metric), (a quote or testimonial),
(circle/callout a specific subject word).
Don't force a purpose template where the content doesn't fit — Rule §4 still
holds; a stat card with no number looks worse, not better. But when the hook
IS there, take it: these elevate the video far more than a generic title card.
- Text isn't your only option. When a beat wants a visual — logos for the
tools being named, a product screenshot, an animated diagram — author a
custom graphic (see "Authored graphics" below). Don't force every beat into a
text template.
Selection guide (beat → template)
"Class" tells you when a template fits: Generic = usable on any beat ·
Purpose = only when the content matches · Semi-generic = usable, has a
natural fit. Across a video, vary your compositions — don't repeat one look
(Rule §3). "Generic" means it has no content prerequisite, NOT "repeat it
every scene."
| What's happening in the video | Reach for | Class |
|---|
| Open / chapter / section title | , , , , — vary the look across sections | Generic |
| Explainer beat, host on camera/imported footage | (torn-paper sheet + two-line title) or (graph-paper/specimen look) — the premium designed segments that level the video up. (clean brand panel + bullets) is the plainer fallback. Alternate side, content, and panel across beats. | Generic workhorse |
| Hero reveal / intro / "ways to use it" recap | , | Semi-generic |
| Introduce a person / channel / "subscribe" | | Purpose |
| A real number / metric / result | | Purpose — numbers only |
| "Here are the N things…" / key points / recap | | Purpose |
| This vs that / before vs after / old vs new | | Purpose |
| A simple linear process / N steps | | Purpose |
| How something WORKS / connects / flows — architecture, pipeline, request lifecycle, hierarchy, branching, a loop (richer than a flat step list) | Author a custom animated diagram — see "Authored graphics" below | Authored — the explainer workhorse |
| A trend / chart / data viz (bars, a line, a metric building over time) | Author a custom chart — see "Authored graphics" | Authored |
| A CONCEPT that needs to be DRAWN — from-scratch explainer of something invisible/abstract (science, process, metaphor), or the user says "whiteboard / hand-drawn / sketch / doodle" | Whiteboard hand-drawn style — load reference/whiteboard-style.md
: paper canvas, SVG draw-on strokes, handwriting reveal | Authored — named Mode-B design system |
| Talking-head OPENER — name the topic in the first 10–30s (default on every camera edit) | caption-editorial-emphasis
| Default for |
| ONE thesis sentence / pull-quote / "money line" (hook or climax) | caption-editorial-emphasis
| Purpose — at most 2–3 per video |
| Logos / tools / partners · a screenshot (a VISUAL, not text) | Author a custom graphic — see "Authored graphics" below | Authored (not a UI template) |
Authored graphics — your repertoire is bigger than the gallery
The 13 templates above are the
UI gallery: fixed-structure, slot-fill. But
your repertoire is larger — you can also
author content-specific graphics
that
can't be slot-templated because what they show depends on what's being
discussed.
This is a first-class capability, not a last resort — when a beat
needs a
visual that no template captures, authoring one is the right move, not
a fallback you apologize for. Build these as transparent overlays with
; they composite over the host exactly like an overlay
template.
Explainer videos are the prime case. The moment the speaker explains
how
something works, connects, or flows — an architecture, a pipeline, a request
lifecycle, a hierarchy, a before→after, a trend over time — a custom
animated
diagram, flowchart, or chart communicates it far better than a bullet list or
a title card. The built-in
template handles a simple linear sequence
of steps;
anything richer you author yourself. Don't flatten a real
explanation into text because the gallery has no template for it — draw it.
- Animated diagram / flowchart — a data-driven SVG that builds as the
speaker talks: boxes + connecting arrows that draw on in sequence, a
branching tree, a request flowing through services, a layered architecture
stack, a cyclic loop. For ANY "here's how it works / how the pieces fit"
explainer beat that's more than a flat list of steps. Reveal each node/edge in
time with the narration so the diagram assembles, not just appears.
- Chart / data viz — bars growing, a line plotting, a metric counting, a
donut filling — when the point is a trend or a structural comparison, not a
single headline number (use / for one number).
- Logo / brand-card row — N rounded white cards, each a logo, popped in over
the lower third. For "we use X, Y, Z", tool / partner / integration mentions
(e.g. HeyGen · Claude Code · Hyperframes).
- Image / screenshot showcase — real images via (product shots,
UI grabs) in a framed or tilted card.
- Icon / emoji concept callout — a glyph + short label punched on a concept.
- Reuse a template's shell, swap text → graphics — take the look of an
overlay template (the lower-band card, the side panel, the depth stack) and
put logos / images / animated SVG / a diagram where the text would go.
These never appear in the UI gallery (they're not in the manifest) — they're
yours to consider whenever a slot-fill template doesn't capture the beat.
Copy-able recipes (logo-card row, etc.) live in
; the authoring contract (page
shell, deterministic seek, transparent overlays) is in
reference/motion-philosophy.md
.
The workflow
bash
# 1. ALWAYS discover first — templates + editable slots (runtime source of truth).
pandastudio motion.list --json # MCP: motion_list
# 2. Pick the template that fits THIS beat (see the selection guide), then
# render it with your own text/colors + a background mode.
# Replace <TEMPLATE_ID> + slots with the chosen template's — do NOT
# hardcode one template for every insert.
JOB=$(pandastudio motion.generate \
--templateId=<TEMPLATE_ID> \
--slots='{ ...the chosen template's slots from motion.list... }' \
--aspectRatio=16:9 \
--json | jq -r '.data.jobId') # MCP: motion_generate
pandastudio job.wait --id="$JOB" --json
# 3. Add the rendered clip to the timeline (at the playhead by default).
pandastudio project.add-motion-graphic --id="$PROJECT" --fromJob="$JOB" --durationMs=4000
-
Editable everything — every template's text, colors, list items, and
images are
; pass only the ones you want to change, the rest use
defaults.
returns each template's slot keys, types (
/
/
/
), and defaults.
-
Image slots — a slot of type
takes an
absolute file path to an
image; the renderer stages the file into the render. Pass a project-media path
or a generated image (e.g. from
). Two templates take an
image:
(the dedicated one — a screenshot/photo on a
3D-tilted card; the go-to for highlighting a product page or app screen in a
demo, 16:9 + 9:16) and
(a small photo in its specimen card).
Example:
--slots='{"image":"/abs/screenshot.png","headline":"Ship faster.","eyebrow":"SEE IT IN ACTION"}'
on
.
-
not — pass the render
to the add tool; it
resolves the path internally (hand-built paths truncate at the space in
"Application Support" and silently fail).
-
Placement —
drops it at the playhead/end as an
overlay. To re-time, pass
. Anchor to a transcript word with
so it survives later transcript edits.
-
Default SFX — every primitive that places an animated callout on the
timeline now attaches a stinger by default so the agent's output sounds
the way a hand-edited timeline does. Don't pass
to "set the
default" — omit it. Override only when the user asks for a different
sound, or pass
(MCP) to make the callout silent.
| Verb | Default | Notes |
|---|
project.add-motion-graphic
| bundled:sound/mouse-click
| New in v1.36.0; previously silent. Applies to every motion graphic — generated templates, custom MP4/WebM, designed-segment panels. |
project.add-designed-segment
| bundled:sound/mouse-click
| Inherits from add-motion-graphic. |
| bundled:sound/swoosh-fast
| Pre-existing. |
| bundled:sound/mouse-click
| v2.96.0 — inherits the motion-graphic default. |
| none | FX overlays often have their own audio; left to the caller. |
Use
to discover other bundled sound ids when swapping.
Background modes, designed segments, and the template catalog
modes, the host-on-one-half designed-segment pattern, and the full bundled-template catalog (incl. podcast layouts). Full detail:
reference/motion-templates.md
.
Custom motion graphics — HTML authoring
When no template fits, author HTML against the HyperFrames contract. Render verbs (
/
/
), transparent overlays + frosted glass, add-by-jobId. Read
reference/motion-philosophy.md
+
reference/motion-recipes.md
before authoring; verbs in
.
🛑 Pass the HTML INLINE — never write a file first.
(MCP:
) accepts the whole composition as an
inline
string parameter:
motion_render_html({ html: "<!doctype html>…", durationMs, aspectRatio })
.
Author the entire HTML in your response and hand it straight to the
arg. Do
NOT try to
/save the HTML to a path and pass
—
the in-app PandaStudio agent has
no , , or tool (that's
deliberate), so a "write the file" plan fails with a tool error.
is
only for the CLI path, where a shell already wrote the file. Local assets
(images/fonts) ride along via the
param (absolute paths, referenced
by basename in the HTML) — you never write them either. If you catch yourself
reaching for a
tool to make a motion graphic, stop: pass
inline,
or use a bundled template (
) instead.
Effects (FX) & transitions
Golden rule: restraint. Scene transitions (
) and FX overlays (
). Full detail:
reference/fx-transitions.md
.
Narration (voiceover) + B-roll generation
Replicate TTS narration and gpt-image B-roll (always Ken-Burns + vignette a still, never drop a flat photo). Requires the user's Replicate key. Full detail:
reference/media-generation.md
.
Faceless videos — image-driven, voiceover-led
A "faceless" video (a.k.a. faceless YouTube / faceless short) is narration
carrying the story over AI-generated IMAGES that DEPICT each beat, with slow
Ken-Burns motion. No face, no camera. This is the format behind history/mystery/
educational channels. The user says "make a faceless video about X", "faceless
YouTube", "faceless short", or picks the home-screen "Faceless short" preset.
🛑 THE ONE RULE THAT MATTERS: a faceless video is IMAGES, not text cards.
Each scene MUST be a real image that SHOWS the beat — for "the cyclops",
generate
a one-eyed giant in a torch-lit cave, NOT a motion-graphic card
with the word "Cyclops" on it. The words belong in the
voiceover, never
on screen. A deck of animated text titles ("Departure", "The Sirens", …) is
the classic failure — it's a title slideshow, not a faceless video, and it
looks cheap. Motion-graphic templates /
text scenes are
the WRONG tool here. Reach for them only for an optional title card or a
lower-third stat, never as the scene visuals.
The pipeline — repeat per beat, then export:
- Break the topic into beats. One clear VISUAL idea per beat (~8–20s of
narration each). A 3–5 min video is ~12–20 beats.
- Write the narration line for the beat (what the voice says).
- Generate the narration → (local Kokoro by
default). Keep each call to ONE beat (~40–60 words); Kokoro caps a single
call around ~25s, so long scripts get truncated — narrate per beat, not the
whole script at once. Grab its .
- Generate the IMAGE for the beat → with a vivid,
LITERAL visual prompt of the scene (subject, setting, lighting, mood — no
on-screen words). For 16:9 generate ; for 9:16 generate . Keep one
art style across every image (state it in every prompt, e.g. "cinematic
oil-painting, warm dramatic light") so the 12+ images read as ONE film, not
random stock.
- Ken-Burns the image to a clip sized to the narration →
media.image-to-video --imagePath=<img> --durationMs=<beat narration + ~400ms> --aspectRatio=9:16 --zoom=in
(or ). This is the NATIVE one-call
path — FFmpeg pans/zooms the still into an MP4 and returns . Alternate
/ across beats so the cut breathes; a still held flat reads as a dead
slideshow. (Only reach for the heavier Ken-Burns shell when
you need a bespoke CSS treatment — grain, parallax layers, vignette animation —
that plain pan/zoom can't do.)
- Add the clip to the main track in order →
project.add-clip --media=<videoPath>
(append). The images-in-motion ARE the video.
- Lay the narration under it →
project.add-audio --audioPath=… --startMs=<beat start>
(beat start = sum of prior beats' durations).
- Polish (optional but expected): a quiet music bed ( →
at low volume, e.g. 0.15), burned captions
( / caption template) since faceless viewers often watch
muted, and maybe ONE title card at the top.
- Export → (16:9 for YouTube, 9:16 for a faceless short).
Timing rule: each scene's length = its narration length; Ken-Burns the image
over exactly that span so voice and visual stay locked. Match aspect to
destination. If the user has no Replicate key, image generation is unavailable —
say so and offer bundled templates as a (lesser) fallback, or ask them to add
the key (Settings → Integrations).
Per-beat loop (9:16 short):
bash
# For one beat — repeat, tracking the running start offset for narration.
IMG=$(pandastudio media.generate-image --prompt="a one-eyed giant in a torch-lit cave, cinematic oil-painting, warm dramatic light" --aspectRatio=2:3 --json | jq -r '.data.imagePath')
NARR=$(pandastudio media.generate-narration --text="In the cave of the cyclops, Odysseus faced a giant who ate men whole." --voice=am_michael --json)
DUR=$(echo "$NARR" | jq -r '.data.durationMs'); WAV=$(echo "$NARR" | jq -r '.data.audioPath')
CLIP=$(pandastudio media.image-to-video --imagePath="$IMG" --durationMs=$((DUR + 400)) --aspectRatio=9:16 --zoom=in --json | jq -r '.data.videoPath')
pandastudio project.add-clip --id="$PID" --media="$CLIP"
pandastudio project.add-audio --id="$PID" --audioPath="$WAV" --startMs=$OFFSET --endMs=$((OFFSET + DUR)) --volume=1
# OFFSET += clip duration (the add-clip return / project.read clip durations) for the next beat.
Place each beat's narration at that beat's CLIP start (cumulative sum of prior
clip durations), NOT at the raw narration sum — the clips carry the +400ms tail,
so read the clip durations back (
) or accumulate
to keep
audio and video locked.
Avatar (talking-head) videos — HeyGen
Generate a talking-head clip from the user's own HeyGen avatar + a script, then drop it on the timeline and edit/caption/export like any other clip. Bring-your-own key + credits: requires the user's HeyGen API key (Settings → Integrations). The HeyGen API is a paid capability, not on every plan, and renders are billed against the user's own HeyGen credits — say so if a call 401s/402s.
HeyGen is a NATIVE PandaStudio integration — use these verbs, never fetch external URLs or ask the user to paste avatar/voice IDs. The moment the user asks for a HeyGen / avatar / talking-head video, your FIRST action is
(and
) to discover their options — don't ask them for IDs, look them up. If either returns
"No HeyGen API key set", STOP and tell the user to connect HeyGen in
Settings → Integrations before continuing; do not ask anything else first.
Discover, then generate. is ASYNC (HeyGen renders server-side over minutes) — it returns
; poll
with a generous timeout, then add the returned MP4 with
.
bash
# 1. Find the avatar (the user's clone) and a voice.
AVATAR=$(pandastudio media.list-avatars --json | jq -r '.data.avatars[0].avatarId')
VOICE=$(pandastudio media.list-avatar-voices --json | jq -r '.data.voices[0].voiceId')
# 2. Kick off the render (16:9 YouTube by default; 9:16 for Shorts).
JOB=$(pandastudio media.generate-avatar-video \
--avatarId="$AVATAR" --voiceId="$VOICE" \
--script="Hey everyone, in today's video…" \
--aspectRatio=16:9 --resolution=720p \
--json | jq -r '.data.jobId')
# 3. Wait for the server-side render (minutes) — poll with a long timeout.
VIDEO=$(pandastudio job.wait --id="$JOB" --timeoutMs=900000 --json | jq -r '.data.job.result.videoPath')
# 4. Add it to the timeline like any recording; edit/caption/export as normal.
pandastudio project.add-clip --id="$PROJECT" --path="$VIDEO"
Args:
,
,
(required);
(
default |
),
(
default |
|
),
(
default |
),
(0.5–1.5),
(hex),
. If
returns
, the render is still going — poll again; never treat it as a failure.
Transcript-based editing — PandaStudio's signature feature
The reason humans pick PandaStudio over Premiere is that you edit by deleting words from the transcript, not by scrubbing the timeline. The CLI exposes the same model.
The full edit loop
bash
# 0. Check which clips still need processing (avoids clobbering in-app edits)
STATE=$(pandastudio project.read --id=$ID --json | jq '.data.clipStates')
# clipStates: [{ clipId, transcribed, wordCount, audioCleaned }, ...]
# 1. Transcribe only clips that don't already have a transcript
# (if all are transcribed, skip this step entirely)
JOB=$(pandastudio transcript.transcribe --id=$ID --json | jq -r '.data.jobId')
pandastudio job.wait --id=$JOB --timeoutMs=300000 --json
# 2. Pull the merged transcript — every word with edited-time start/end
pandastudio transcript.get --id=$ID --json | jq '.data.words[0:20]'
# 3a. AUTO: drop every "um" / "uh" / "you know" + immediate repeats
pandastudio transcript.remove-fillers --id=$ID --json
# → returns { removedCount, fillersRemoved, repeatsRemoved, trimsAdded }
# 3b. Remove silences (default ≥600ms = the UI button; covers leading/trailing/between-word)
# Omit --thresholdMs to use the 600ms default; only pass it to override.
pandastudio transcript.remove-silences --id=$ID --json
# → returns { removedCount, totalTrimmedMs }
# 3c. Fix STT errors — NEVER use project.read → JSON mutation → project.save for this.
# find-replace patches the word text in-place and preserves timing.
pandastudio transcript.find-replace --id=$ID --find="RightPanda" --replace="WritePanda" --json
# → returns { replacedCount, wordsPatched }
# Matcher caveats:
# - Matching is case-insensitive; punctuation is ignored on both sides
# (`--find="graph, crew"` matches "graph crew").
# - DIGITS are significant when the find phrase contains them:
# `--find="try30"` matches only the merged STT token "try30", never a plain
# "try". A letters-only find still ignores transcript-side digits
# (`--find="than"` also matches "than60").
# - A multi-word find ALSO matches a single merged STT token: `--find="Wispr
# Flow"` hits the one token "Wispr Flow" (STT often emits multi-word brand
# names as one token), and `--find="of $499"` hits the merged "of$499".
# - A find that exactly equals a token's raw text always matches, so
# space/digit/punctuation-bearing tokens — even pure numbers like "30%" —
# are all targetable verbatim.
# - A multi-word `--find` collapses to the FIRST word's slot: that word's text
# becomes `--replace`, the other matched words are blanked. The replacement is
# the literal `--replace` string, so include any punctuation you want kept
# (the original word's trailing comma/period is not auto-preserved).
# 3d. SURGICAL: delete specific words by ID
pandastudio transcript.delete-words --id=$ID --wordIds='["clip-1:w-42","clip-1:w-43"]' --json
# 3e. PHRASE search → bulk delete
WORDS=$(pandastudio transcript.search --id=$ID --query="this is a test" --json \
| jq -c '[.data.matches[].wordIds | .[]]')
pandastudio transcript.delete-words --id=$ID --wordIds="$WORDS" --json
# 3f. RESTORE previously deleted words (undo a delete / filler / repeat removal).
# Removes the trim region(s) covering those words; silence trims are left
# untouched. Mirrors the editor's right-click → Restore on struck-through words.
pandastudio transcript.restore-words --id=$ID --wordIds='["clip-1:w-42","clip-1:w-43"]' --json
Every deletion translates internally into a trim region the export pipeline skips. It's identical to clicking the word in the editor's transcript pane and hitting delete.
shows ALL words, including ones you've deleted. Deleted words become trim regions — they're gone from the audio export — but they still appear in the raw word list. If you need to verify a deletion happened, check
in the response rather than calling
afterwards and looking for missing words.
STT coherence with motion graphics: Fix all transcript errors with
BEFORE calling
or
. The local LLM and motion-graphic slot values are derived from the transcript text — a "RightPanda" in the transcript will propagate into the title card if you generate it first.
Audio cleanup, background audio, music, and color grading
(DeepFilter), background-audio regions, bundled + Lyria-generated music,
per-clip volume (
— balance loudness across clips, 0–2 gain, no shell/WAV workaround needed), and LUT color presets. Full detail:
reference/audio-color-music.md
.
Visual edits — zooms, trims, speed, crop, layouts
Zoom (incl. follow-cursor +
), cut/speed, crop/reframe, face centering, webcam + per-section podcast layouts, speaker-driven editing, export defaults. Full detail:
reference/visual-edits.md
.
Captions, AI metadata, thumbnails
Caption toggle/style/font, AI title/description/timestamps (local LLM), and YouTube thumbnail generation. Full detail:
reference/captions-metadata.md
.
Export — produce the final MP4
The centerpiece. Routes through the same Tier-3 PixiJS renderer the editor's Export Video button uses (v1.24+ — was a separate Skia native pipeline before that, see release notes for the convergence). When an editor window is already open on the project you're exporting, the agent reuses it. Otherwise the agent spawns a hidden editor window for the duration of the render and closes it after.
Async; poll .
bash
JOB=$(pandastudio export.start --id=$ID --quality=high --json \
| jq -r '.data.jobId')
# Watch progress (server-side block; returns when done or 5min timeout)
pandastudio job.wait --id=$JOB --timeoutMs=600000 --json | jq '.data.job'
# → status: "succeeded", result: { outputPath, durationMs, width, height, frameRate }
Quality presets:
(1280×720),
/
(1920×1080),
(3840×2160). Aspect ratio comes from the project (
). Output lands in the recordings dir by default; pass
--outputPath=/somewhere/file.mp4
to override.
The export honours everything in the project: clips, trims (incl. those from transcript word deletes), speed regions, zooms, captions, FX, lower-thirds with sound, motion graphics, annotations, cleaned audio, wallpaper, padding/shadow/radius/blur. One verb, full pipeline.
Video overlays (motion graphics) are fully composited in the export — both opaque MP4 (
,
) and transparent WebM (
motion.render-html --transparent
) are composited inline by the Tier-3 pipeline. Alpha channels from VP9/WebM sources are preserved exactly. There is nothing extra you need to call —
handles it automatically once overlays are on the timeline via
project.add-motion-graphic
.
Video editing playbook — end-to-end recipe (per destination)
When the user says "edit this" / "polish this" / "make this ready for <X>" / "YouTube-ready", follow this runbook. It turns a raw recording into a polished, destination-appropriate video using the foundational verbs above. Different destinations (YouTube long-form, Shorts/TikTok, LinkedIn, Loom) need different defaults — the table below is the source of truth.
Philosophy: good video editing is a series of pattern interrupts that match the platform's viewing context. A YouTube long-form viewer has settled in — cuts every 5–8s and a cinematic LUT feel right. A TikTok viewer is scrolling — you have 3 seconds to hook them and every second after needs a visible change. A LinkedIn viewer is at work — an aggressive soundscape is wrong. A Loom viewer doesn't want any editing at all beyond "cut the fluff". Same tools, very different dials.
Destination profiles (the source of truth)
Resolve the destination first (see
HARD-GATE step 1). Then apply every default below from the matching row — don't mix.
| Parameter | | (Shorts/TikTok/Reels) | | (internal/async) |
|---|
| Aspect | 16:9 | 9:16 | 16:9 or 1:1 | 16:9 |
| Hook deadline | 10 s | 3 s | 10 s | — (none) |
| Intro / outro card | only if the user asks (then 2–4 s) | only if the user asks | only if the user asks (then 2–3 s) | none |
| Lower thirds | yes, at first mentions | no (too small vertically) | yes | no |
| Zoom cadence | 3–6 / min | 6–12 / min | 1–2 / min | 0–1 / min |
| Default emphasis zoom duration | 7 s | 3 s | 4 s | 2 s |
| Sustained held-zoom duration (section reframe) | 15 s | — | 8 s | — |
| Agent zooms on screen-share clips | NEVER (telemetry handles it) | NEVER | NEVER | NEVER |
| Zoom SFX volume | 1.0 (swoosh-fast) | 1.0 (swoosh-fast) | 0.5 (or ) | |
| Filler/silence removal | yes | yes | yes | yes (aggressive — minSilenceMs 300) |
| Speed regions (B-roll) | 1.5–2× | 2–3× or cut entirely | 1.25–1.5× | none |
| LUT preset | by content type @ 0.5–0.8 | @ 1.0 | @ 0.3 | none |
| Background music | only if the user asks (then vol 0.15) | only if the user asks (then vol 0.30) | none | none |
| Captions enabled | no burn-in — keyword pops instead (measured: 0/9 studied long-form videos burn speech captions; see longform-styles.md LF4) | yes (required) | yes | optional |
| Caption template | — (keyword-pop overlays, not caption templates; if the user INSISTS on captions: ) | + positionY 0.85 | | (if any) |
| Export quality | | | | (faster) |
LUT by content type (only for
— other profiles use their fixed preset above):
| Content type | Preset | Intensity |
|---|
| Tech tutorial / SaaS demo | | 0.7 |
| Cinematic vlog | | 0.9 |
| Educational / neutral | | 0.5 |
| Moody storytelling | | 0.7 |
| Travel / lifestyle | | 0.7 |
Creator-style overrides (when the user names a style)
When the user says "like Ali Abdaal's videos" / "MKBHD style" /
"MrBeast-style" / etc., start from the matching base profile, then
apply the overrides below. These are on top of the profile defaults,
not instead of them. Unlisted styles → fall back to base profile.
| Style | Base profile | Pacing | LUT | Music | Caption template | Motion-graphic cadence + notes |
|---|
| Ali Abdaal (productivity / book reviews / tutorial long-form) | | 1 visual change every 3–5s; aggressive filler + silence removal | @ 0.5 | warm ambient / lofi @ 0.15–0.20 | , positionY 0.85 (below lower-third zone) | Intro title card (3s held) · host lower-third at 0:04–0:09 · 3–4 right-rail concept callouts at emphasis claims · 1 stat-reveal full-frame takeover if the video cites a number · outro card 4–6s hold with "Like & Subscribe" + shimmer on handle |
| MKBHD (tech reviews / product-focused long-form) | | 1 change every 4–6s — contemplative, product breathes on screen | @ 0.6 OR @ 0.5 | upbeat tech-review bed @ 0.20 | @ positionY 0.85 | Clean intro wordmark (2s) · minimal lower-thirds (1 total, on first product mention) · stat-reveals over product shots use chrome-gradient numbers on dark · outro: product recap card + subscribe |
| MrBeast (stunts / challenges / max-retention) | | 1 change every 2–3s — very fast, shorts-like cadence | @ 0.8 (saturated, warm) | dramatic orchestral bed @ 0.30 | , huge (fontSize ~4.5rem, near the 5.0rem max), color-coded by topic, positionY 0.85 | Big chrome-gradient kinetic-type every ~5s · frequent full-frame stat takeovers with counter tweens · countdown overlays if the video has stakes · outro: "what's next" teaser card, hold full 6s |
| Veritasium / Kurzgesagt-live (science / education long-form) | | 1 change every 5–7s — contemplative, give diagrams time to read | @ 0.4 | ambient / orchestral @ 0.12 | @ positionY 0.85 | Explanatory diagrams as motion graphics (labeled SVGs with reveals, on labels) · chapter dividers with chrome-gradient section titles · one or two hero stat-reveals with counter tweens · outro: citations card + subscribe |
| Vox / Johnny Harris (explainer / essay long-form) | | 1 change every 4–6s — narrative-driven | @ 0.7 | cinematic bed @ 0.18 | @ positionY 0.85 | Chapter cards at every act break (bold chrome-gradient section titles) · map / timeline / chart motion graphics · pull-quote callouts in right rail · outro: credits card + next video teaser |
Rule: an agent authoring any "style X" edit MUST still follow the 11
Laws from
reference/motion-philosophy.md
. The style overrides change
palette, cadence, and the
suggested music/intro/outro — they do NOT let
you ship flat-white text on a flat-black background. Grid + vignette + grain
- chrome gradient are mandatory regardless of named style.
Music + intro/outro stay opt-in even in style mode. The Music and
motion-graphic-cadence columns above describe what the style would include
— but background music and intro/outro cards are still added ONLY when the
user asked for them (see the default edit pipeline). If the user named a style
without mentioning music or an intro/outro, apply the style's palette /
pacing / captions / mid-roll motion graphics, and offer the music bed +
intro/outro rather than adding them unprompted.
Anchoring — every transcript-derived region MUST be anchored
This rule applies to FIVE region types: zoom, motion-graphic, lower-third,
annotation, and audio-overlay (when used as SFX, not background music).
The problem. Region positions are stored in
edited time —
post-trim playback time. When the user (or you) runs
transcript.remove-fillers
,
transcript.remove-silences
,
, or
,
new trim regions get added, the edited-time map shifts, and any region whose
/
was authored against the previous edited time
drifts off
the moment it was placed on. A "Like and Subscribe" lower third you placed
on the word "subscribe" silently moves 800ms early because there used to be
800ms of "um"s before it that got trimmed.
The fix is the argument. When you derive
/
from a transcript word's source time, pass that same value as
. The region records its anchor moment in raw recording
time. Every subsequent trim/speed edit auto-rebases the region's edited
positions back onto the anchor — the lower third stays glued to "subscribe"
no matter how much you trim.
v1.35.0+: every region is auto-anchored on creation, even without .
The primitive now back-computes a source-time anchor from the resolved
edited
(via
) whenever the caller doesn't supply
one explicitly. Regions placed by direct atMs survive subsequent silence
removal, filler removal, and word deletion just like transcript-anchored
ones.
Still pass when you have a transcript word in
hand — it makes the agent's intent self-documenting and avoids a
two-step round trip through edited time. But the days of "I placed an
overlay, then trimmed silences, now it's playing at the wrong moment"
are over for any project saved by v1.35.0 or later. Legacy projects
(schemaVersion < 4) are auto-migrated on the first mutation: every
anchorless region gets a source anchor back-filled at its current
position.
anchors are preserved as opt-outs.
Verbs that accept anchors (use them ALWAYS when picking from transcript):
| Verb | Anchor args | When required |
|---|
| , | Always when atMs comes from a transcript word |
project.add-motion-graphic
| , | Always when atMs comes from a transcript word |
| , | Always when atMs comes from a transcript word |
| , | Always when startMs comes from a transcript word |
| , | When the overlay is an SFX pinned to a word. NEVER for background music — those should stay free-floating (a fixed slot of the edited timeline, not anchored to content). |
Free-floating is OK — when the user explicitly placed a region by edited
time (e.g. an outro card at "the last 5 seconds of the timeline"), omit the
anchor. The region stays where you put it regardless of subsequent edits.
anchorSourceMs is global source-time (ms from recording start). In
multi-clip projects, sum the preceding clips'
to convert
an in-clip offset to global source time before passing as
.
Use
timeline.source-to-edited
to verify where a source-time position falls
on the edited timeline.
The runbook below already orders pacing FIRST, then regions. That's safe
even without anchors — regions land on the post-trim timeline. But: any
mid-flow re-edit ("actually, remove the part about X" after you've placed
motion graphics) drifts unanchored regions silently. Always anchor when the
position came from a transcript word, even if the runbook order is followed.
The cost is one extra arg per call; the benefit is correctness under iteration.
The runbook (ordered — do not rearrange)
bash
# 0. Resolve the project + destination profile
ID=$(pandastudio project.current --json | jq -r '.data.project.id // empty')
[ -z "$ID" ] && ID=$(pandastudio project.list --json | jq -r '.data.projects[0].id')
# $PROFILE is set from HARD-GATE step 1: youtube-long | shorts | linkedin | loom
# $ASPECT is derived from the profile:
# youtube-long | linkedin | loom → 16:9
# shorts → 9:16
pandastudio project.set-aspect-ratio --id=$ID --aspect=$ASPECT
# 1. PACING — the default cleanup pipeline (see "default edit pipeline" in
# Editorial decisions for the mandatory steps + report-counts rule). Run it
# in full — none of these is optional. audio.clean fires async; wait on it
# before export (step 5). First read pulls the transcript (needed for step 2);
# later reads pass --includeTranscript=false.
pandastudio project.read --id=$ID --json
pandastudio transcript.transcribe --id=$ID # skip if transcribed
AUDIO_CLEAN_JOB=$(pandastudio audio.clean --id=$ID --json | jq -r '.data.jobId // empty') # async
pandastudio transcript.remove-fillers --id=$ID # fillers + immediate repeats
# Bad takes + repeated phrases: find-issues is READ-ONLY — you MUST then delete
# the discarded wordIds (keep the most recent take). Skipping the delete cuts nothing.
ISSUES=$(pandastudio transcript.find-issues --id=$ID --json | jq -c '.data.issues')
DROP=$(echo "$ISSUES" | jq -c '[.[] | select(.type=="duplicate-take" or .type=="adjacent-repeat") | .wordIds[]]')
[ "$DROP" != "[]" ] && pandastudio transcript.delete-words --id=$ID --wordIds="$DROP"
SILENCE_MS=$([ "$PROFILE" = "loom" ] && echo 300 || echo 500)
pandastudio transcript.remove-silences --id=$ID --thresholdMs=$SILENCE_MS # NEVER skip; arg is thresholdMs (500ms = UI default)
# 2. EMPHASIS — zooms (skip for `loom`). Cadence comes from the profile table.
#
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# HARD RULE #1 — DO NOT add zooms to screen-share clips.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# PandaStudio auto-adds zooms to screen-share clips based on cursor
# telemetry captured during recording. Adding more on top = stacked
# zooms on the same moments, visual chaos.
#
# For each clip returned by project.read:
# - If clip.webcamPath is present → "both" mode (screen + PiP
# webcam). Telemetry zooms ALREADY exist on this clip. **SKIP.**
# - If the clip is pure camera (no webcamPath AND mediaPath is a
# camera recording) → safe to add zooms on emphasis.
# - If screen-only (no webcamPath, mediaPath is screen) → telemetry
# still exists, zooms are auto-added. **SKIP.**
#
# Heuristic: if project.read returns existing zoomRegions for a clip
# whose webcamPath is set, those are telemetry-based — stay OUT of
# that clip's zoom space entirely. Only author zooms on pure-camera
# clips (webcamPath absent, no pre-existing zoom regions).
#
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# HARD RULE #2 — Zoom duration floor is 6 seconds, NOT 1.5–3 seconds.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Premium YouTube zooms ride through a complete thought or cut. A 1.5s
# or 3s zoom reads as a twitch. Defaults:
# - Emphasis punch-in zoom (camera pulls in on a word/claim):
# durationMs=6000 to 8000 (was 1500 — that was wrong)
# - Sustained / held zoom (camera pulls in on a subject and
# stays for the whole sub-topic):
# durationMs=10000 to 20000
# - Reveal moment (dramatic, loud SFX): durationMs=6000 minimum
# Depth 3 (modest) for talking-head emphasis; depth 5 only for
# dramatic reveal beats. Don't stack multiple zooms inside 5s of each
# other — leaves no time to settle.
#
# Shot selection: scan the transcript for punchy claims, specific
# numbers, opinionated statements ("the best", "this changed
# everything", "most people don't know"), moments of pivot ("and
# now", "finally", "but here's the thing"). Aim for the user's
# eyes to want to lean in. NOT every UI-verb word ("click", "select")
# — that was the old rule, it's wrong for engagement-style edits.
#
# CRITICAL: ALWAYS pass --anchorSourceMs when atMs comes from a transcript
# word. Without it, the zoom drifts off the moment as soon as ANY trim is
# added — and step 1 always adds trims (remove-fillers, remove-silences).
# The anchor binds the zoom to the source recording moment so it
# re-anchors automatically on every trim/speed change.
# Emphasis punch-in — modest scale, held through the thought
pandastudio project.add-zoom --id=$ID --clipId=$CLIP_ID \
--atMs=<wordStartMs> --anchorSourceMs=<wordStartMs> \
--durationMs=7000 --depth=3
# Sustained held zoom — reframe on a topic and stay for the full section
pandastudio project.add-zoom --id=$ID --clipId=$CLIP_ID \
--atMs=<sectionStartMs> --anchorSourceMs=<sectionStartMs> \
--durationMs=15000 --depth=3
# Reveal moment (dramatic, with SFX). Use SPARINGLY — 1-2 per video max.
pandastudio project.add-zoom --id=$ID --clipId=$CLIP_ID \
--atMs=<ms> --anchorSourceMs=<ms> \
--durationMs=6000 --depth=5 \
--soundUrl=bundled:sound/dramatic-whoosh --soundVolume=0.7
# 3. POLISH — skip sections by profile:
# - `shorts`: no lower thirds (tight vertical frame)
# - `loom`: skip 3b, 3c entirely
# NOTE: 3a (intro/outro) and 3d (background music) are OPT-IN — run them
# ONLY when the user explicitly asked for an intro/outro or music. They are
# NOT part of the default "edit my video" pass for ANY profile.
# 3a. Intro / outro card — ONLY IF THE USER EXPLICITLY ASKED for one
# ("add an intro", "add an outro card", "open with a title"). Do NOT add
# one on a plain "edit this" request. When asked: fastest is the
# `creator-card` template via motion.generate; author custom HTML
# (reference/motion-philosophy.md §7) only for a bespoke intro.
if [ "$USER_ASKED_FOR_INTRO" = "1" ]; then
JOB=$(pandastudio motion.render-html \
--htmlPath=/tmp/intro-title.html \
--durationMs=3000 --json | jq -r '.data.jobId')
FILE=$(pandastudio job.wait --id=$JOB --json | jq -r '.data.job.result.outputPath')
pandastudio project.add-motion-graphic --id=$ID --file=$FILE --durationMs=3000 --atMs=0
fi
# 3b. Lower third at first mention of a person/product (NOT shorts/loom)
# One call renders the lt-* nameplate AND places it (async job).
if [ "$PROFILE" = "youtube-long" ] || [ "$PROFILE" = "linkedin" ]; then
JOB=$(pandastudio project.add-lower-third --id=$ID \
--name="<name>" --title="<role>" --atMs=<ms> --anchorSourceMs=<ms> \
--json | jq -r '.data.jobId')
pandastudio job.wait --id=$JOB
fi
# 3c. LUT (use the profile table. For youtube-long, use content-type sub-table.)
# Apply to every clip via project.set-clip-lut.
# Skip entirely for `loom`.
# 3d. Background music — ONLY IF THE USER EXPLICITLY ASKED for music
# ("add background music", "put a track under it"). Do NOT add music on a
# plain "edit this" request. When asked, use the profile volume (youtube-long
# 0.15, shorts 0.30) and let the user pick / swap the track.
if [ "$USER_ASKED_FOR_MUSIC" = "1" ]; then
VOL=$([ "$PROFILE" = "shorts" ] && echo 0.30 || echo 0.15)
pandastudio project.add-audio --id=$ID \
--path=bundled:music/corporate-underscore --volume=$VOL --fadeIn=1000 --fadeOut=2000
fi
# 4. ACCESSIBILITY — captions per profile
if [ "$PROFILE" != "loom" ]; then
pandastudio caption.toggle --id=$ID --enabled=true
TEMPLATE=$(case "$PROFILE" in
shorts) echo "neon";;
linkedin) echo "minimal";;
youtube-long) echo "bold";;
esac)
pandastudio caption.set-template --id=$ID --templateId=$TEMPLATE
# ALL CAPS look (common for shorts): force uppercase on any template.
# pandastudio caption.set-style --id=$ID --uppercase=true
# (uppercase=false turns OFF a template that ships caps, e.g. editorial)
# HIDE captions for part of the video. Captions are ON everywhere once
# enabled, so these carve out exceptions — use when subtitles would cover
# something on screen (a UI demo, on-screen text, a lower third).
# Times are EDITED-timeline ms. Overlapping regions are fine.
# pandastudio project.add-caption-region --id=$ID --atMs=12000 --durationMs=5000
# Remove one (ids come from project.read → editor.captionRegions[].id):
# pandastudio project.remove-caption-region --id=$ID --regionId=caption-hide-1
fi
# MUTE part of the video's audio. Silences the MAIN voice/screen track over a
# stretch — background music and SFX overlays keep playing (same as the editor).
# Times are EDITED-timeline ms; durationMs controls how much is silenced.
# Use for a cough, a name, or dead air you want silent WITHOUT deleting footage
# (deleting would also cut the video; mute keeps the picture, drops the sound).
# Overlapping regions are fine. To fully cut both picture and sound, use a trim.
# pandastudio project.add-mute-region --id=$ID --atMs=12000 --durationMs=3000
# Remove one (ids come from project.read → editor.muteRegions[].id):
# pandastudio project.remove-mute-region --id=$ID --regionId=mute-1
# 4.5. VERIFY FRAMES — MANDATORY. Never export without looking.
# Run motion.verify-frames on every rendered motion-graphic MP4 AND on
# a draft pass of the full composition (preview.show, then extract
# frames at hero timestamps). READ each PNG as a multimodal image and
# confirm against the motion-philosophy §4 pre-flight checklist:
# - no cropped faces / text overflow / blank frames
# - captions on the right word
# - no MG covers host face (Mode C) or screen zone (Mode B)
# - chrome-gradient text is actually rendering (not flat white)
# - grid + vignette + grain visible on every hero beat
# If any frame fails, iterate and re-verify. Do NOT skip this step
# even when "it's just a quick edit" — this is what separates
# ships-it-works-ish from ships-it-looks-good.
#
# 🛑 VERIFICATION IS A SEPARATE, BEST-EFFORT STEP — IT NEVER "FAILS" THE EDIT.
# Every project.* mutation (add-clip, add-motion-graphic, add-audio, etc.)
# COMMITS to the .pandastudio file the instant its tool returns ok. The
# verification pass (render a frame, then READ the PNG as a multimodal image)
# happens AFTER and is a QUALITY check, not part of the edit. So:
# • Order matters: make the mutating edit FIRST, confirm ok, THEN verify.
# The edit is already durable before you ever read a frame.
# • If the frame-read step fails — model times out on the image, "no output
# for N minutes", rate-limit, "tool read failed" — the EDIT STILL LANDED.
# Do NOT report the task as failed. Report: "Done — <edit> is on the
# timeline. I couldn't finish the visual check (model didn't respond);
# retry the check or switch models." Presenting a committed edit as a
# failure because a follow-up vision read hung is the wrong outcome.
# • Keep the read LIGHT so it doesn't hang the model: verify a SINGLE
# render-frame (one PNG) at the hero timestamp, NOT a big multi-frame
# render-sheet. One small image is a cheap vision call; a large contact
# sheet is the read that most often times the model out.
pandastudio preview.show --id=$ID # let the project render a full draft
# Then verify each generated motion-graphic MP4 at its hero timestamps:
# pandastudio motion.verify-frames --videoPath=/tmp/motion-intro.mp4 \
# --timestamps='[0.3,1.0,1.8,2.7]' --json
# Read every returned frame. If any fail, fix + re-render + re-verify.
# 5. EXPORT — quality per profile. Only run AFTER verify-frames passes.
# First: block on the audio.clean job that's been running in the
# background since step 1. By now it's almost certainly done (30-60s
# vs the ~90s+ the rest of the work took), so this resolves instantly.
[ -n "$AUDIO_CLEAN_JOB" ] && pandastudio job.wait --id="$AUDIO_CLEAN_JOB"
QUALITY=$([ "$PROFILE" = "loom" ] && echo "standard" || echo "high")
pandastudio export.start --id=$ID --quality=$QUALITY --json | jq -r '.data.jobId' | \
xargs -I {} pandastudio job.wait --id={}
Performance — keep wall-clock minimal
Motion-graphic renders dominate (each scene ~20–45s); everything else is
rounding error. The levers:
- renders are SERIAL — fire ONE, it, then the
next. A single-render mutex serializes them; a second concurrent render
returns
{ ok:false, error:"RENDER_BUSY" }
(parallel renders share GPU /
scratch / headless-shell state and wedge). Do NOT fire all renders at once.
- Run in the background (capture its jobId right after
transcribe; only before ). This IS safe to overlap
with a render — the serial limit is render-to-render only.
- Pre-flight HTML with before a full render — a ~2s
screenshot at beats a 20–45s wasted render. It inlines
GSAP and seeks the paused timeline, so it previews the animated frame (not
the static pre-JS DOM).
- Re-read sparingly: pass
--includeTranscript=false
after the first
, and reuse the each mutation returns instead of
re-reading.
Anti-patterns (do NOT do these — all profiles)
- 3 effects on the same moment (zoom + lower-third + motion graphic at same t) — visual noise
- Multiple LUTs per project — pick one from the profile table
- SFX on every cut — cap at 1 meaningful SFX per 15–30s (except , where 1 per 5–10s is fine)
- Speed regions over voice — always for setup / B-roll / scrolling only
- Logo intro >5s (any profile) — retention cliff
- Asking the user which filler words to remove — always-safe op, just do it
- Applying defaults to a project — wrong aspect, music too quiet, captions too subtle, pacing too slow
- Motion graphics in — kills the "this is a quick update" vibe
Entry triggers — phrases that start the playbook
These phrases all route to the edit runbook above. Don't ask the user
to expand any of them — resolve the profile + style, announce the plan,
execute.
| User says | Resolve to |
|---|
| "edit this" / "polish this" / "make it engaging" / "make it ready" | , no style override |
| "YouTube-ready" / "make a YouTube video" / "edit for YouTube" | , no style override |
| "make it a Short" / "TikTok" / "Reel" / "vertical" / "9:16" | , no style override |
| "for LinkedIn" | , no style override |
| "Loom" / "internal update" / "just cut the fluff" | , no style override |
| "edit like Ali Abdaal" / "Ali Abdaal style" / "tutorial style" / "productivity video" | + Ali Abdaal override |
| "MKBHD style" / "tech review style" / "product review" | + MKBHD override |
| "MrBeast style" / "high-retention" / "challenge video" / "maximum engagement" | + MrBeast override |
| "Veritasium style" / "educational" / "Kurzgesagt vibe" / "explainer" | + Veritasium override |
| "Vox style" / "essay" / "narrative" / "Johnny Harris style" | + Vox override |
If the user doesn't name a style and doesn't specify a destination, the
safe default is
with no style override — the most common
case by far.
Pattern: one-shot execution
After entry trigger + profile/style resolution, announce the plan in
one sentence and execute. Load
reference/motion-philosophy.md
automatically before the motion-graphics steps. Run the full runbook
including the mandatory frame-verification gate. Do NOT ask the user
to approve individual steps.
I'll edit this as a
YouTube long-form in Ali Abdaal style — aggressive
filler + silence removal, 3–5s pacing, 4 right-rail concept callouts at
emphasis claims, 1 stat-reveal takeover,
LUT at 0.5,
warm ambient music at 0.15,
captions at y=0.85, and a 5s
outro CTA card. Motion graphics authored against motion-philosophy
(chrome-gradient, grid + vignette + grain). Frame-verify before export.
~5 minutes.
I'll edit this as a
Short — aggressive pacing (hook in 3s, 6–12
zooms/min),
LUT at full intensity,
captions positioned higher, music at 30%. No intro card or lower
thirds — they don't fit the vertical frame. Frame-verify before
export. ~2 minutes.
I'll edit this as a
MrBeast-style YouTube video — 2–3s pacing
(very fast),
LUT at 0.8, dramatic orchestral bed at 0.30,
huge color-coded
captions, chrome kinetic-type every 5s,
full-frame stat takeovers, 6s outro teaser card. Motion graphics
authored against motion-philosophy. Frame-verify before export.
~6 minutes.
Don't ask the user to micro-manage step choices. The profile table +
creator overrides + motion-philosophy are the answer. If something
genuinely needs user input (missing brand reference for a named style
that has none obvious, missing subject name for the lower third),
collect ALL such questions in a single message — never one-at-a-time.
In-app agent sessions — observe and stop
The desktop app embeds its own chat agent. Two verbs (app >= 1.60) let an
external agent see and control those sessions:
(id,
title, timestamps; returns
when the embedded agent server
is down — it never starts it) and
agent.session-stop --sessionId=<id>
(or
) to abort execution while keeping the transcript. Use
when the user reports the in-app agent doing something unattended.
What this skill is NOT for
- Cloud video APIs (HeyGen, Runway, Sora). PandaStudio is local-only.
- Direct edits to project JSON. The format is owned by the editor and changes between versions. Use / and treat the JSON as opaque between reads.
- Cloud video APIs — PandaStudio is local-only; renders on the user's machine.
Reference files
- — every verb.noun with arg schema and a one-line example.
- — multi-step recipes + the long-form motion-graphics authoring recipes (SaaS promo, tilted-device shots, multi-image scenes, complex multi-overlay HTML) that used to live inline in this file. Read on demand when authoring a bespoke graphic.
- — what each motion-graphic template looks like, with the slots it accepts and which aspect ratios it supports.
reference/motion-philosophy.md
— the aesthetic contract. Laws, visual vocabulary, easing dictionary, canonical shell, pre-flight checklist. Load this BEFORE authoring any motion graphic. This is what raises output from "template-filled" to "HyperFrames-quality".
reference/video-authoring.md
— 3-mode delivery playbook. Mode A (9:16 camera-only), Mode B (9:16 screen-rec + PiP face — PandaStudio's unique mode), Mode C (16:9 YouTube side-overlay). Face choreography, caption safe zones, audio-sync protocol, frame verification. Load this for any shorts/YouTube authoring task.
reference/promo-and-mg-videos.md
— the design bar for a from-scratch promo / all-motion-graphics video (the graphics ARE the video). Show-don't-tell map, scene-variety archetypes, the templated-slideshow anti-pattern, and the motion/depiction/variety quality gate. Load this FIRST for any promo / teaser / explainer-from-scratch.
reference/motion-recipes.md
— menu of ~30 named, seek-safe motion patterns + scene transitions + the determinism guardrails (incl. the / are-in-SECONDS rule, the GSAP-must-load rule, and the motion-quality gate). Read when authoring custom motion for a specific beat.
reference/whiteboard-style.md
— the whiteboard / hand-drawn explainer design system (paper + grid canvas, SVG draw-on strokes via pathLength/dashoffset, left-to-right handwriting reveal, marker palette + fonts, scene grammar, the gotcha). Load for "whiteboard animation / hand-drawn / sketch / doodle" briefs and from-scratch concept explainers (science, process, metaphor).
reference/motion-templates.md
— background modes, designed segments, and the full bundled-template catalog (incl. podcast layouts). Read when picking/rendering a template via .
- — the render verbs (//), transparent overlays + frosted glass, add-by-jobId. Read alongside motion-philosophy when authoring custom HTML.
reference/visual-edits.md
— zooms (incl. follow-cursor + ), trims, speed, crop/reframe, face centering, webcam + per-section podcast layouts, speaker-driven editing. Read for any visual edit.
reference/audio-color-music.md
— , background-audio regions, bundled + Lyria music, and LUT color presets.
reference/captions-metadata.md
— captions (toggle/style/font), AI title/description/timestamps, YouTube thumbnails.
reference/fx-transitions.md
— scene transitions + FX overlays, with the restraint rules.
reference/media-generation.md
— Replicate narration (TTS) and B-roll image generation (always Ken-Burns a still).
- — turn an export into vertical clips: discover shots, fork per shot, the 9:16 playbook, batch.
- — connect + publish to YouTube and Instagram, with the privacy/account/workspace hard rules.
reference/projects-and-transcription.md
— folders, rename, transcription languages, transcribing a standalone file.