The 3D kinetic short
A ninety second vertical short where the words are the motion and the frame has real depth. Your agent writes the spine, casts the narration from voices already in your library, pulls footage from your library or the public domain (or uses a still when there is none), stages each shot in 3D with gradients and glass, adds a lens and a film stock, syncs the type word by word to the voice, and ends on a thought the viewer has to finish.
Use /groundslate-3d-kinetic-short: make me a ninety second vertical short about <<YOUR SUBJECT>>, in GroundSlate profile <<PROFILE ID OR NAME>>. Teaser, then the beats, then a landing that leaves me thinking. Cast the narration from the voices already in my library and ask me before minting any new ones. A different visual idea per beat, a different move on every seam, real 3D depth, gradients, overlays and glass in every frame, and kinetic typography that highlights word by word against the voice. Seam the beats with page moves where you can and keep the rendered joins in the light tier. Put a lens on every beat and hold one film stock across the film. Nothing on screen holds still and nothing is smaller than 30pt. Prefer real footage over stills, treated, with the licence recorded off screen and never on it. Three beds, written from a brief or taken from my music library, ducked under the voice. Show me the beat plan and the style frames before you render anything, and tell me what did not resolve.
Works with
- Your agent's own skills
- Research, analysis and writing. GroundSlate renders web pages itself now, capture_page for stills and record_page for footage, so the pictures come from the app; the reading, the judgement and any account that needs a sign-in still arrive through your agent.
Shorts like this usually fail the same way. A template gets built for the first point and runs nine more times with the words swapped. The background sits still while a voice talks over it. A bar creeps along the bottom to show the end is coming. By the fourth screen the viewer knows what every screen after it will look like, and leaves.
This skill has your agent design every scene as its own visual idea. The frame has real depth, not a drop shadow standing in for it, and the words are the motion rather than a caption underneath.
The subject can be anything: a myth-busting run, a product story, a history, a set of numbers that deserve better than a chart. The structure is a teaser, a run of beats, and a landing that leaves a thought behind.
GroundSlate handles the parts that usually cost money and a week. Voices are cast from your library or made new, and spoken on your own Mac, so a recast costs a re-render, not an invoice. Footage comes from your own library first and the public domain second, with each clip's licence recorded off screen. A catalog of named looks adds lenses that bend the field and film stocks with live grain and halation, so a beat looks photographed rather than composited. There are two transition libraries, one rendered by ffmpeg and one that runs inside the page. The two transition libraries explains which to use and what each costs. Word timings come from the narration itself, so the type highlights in step with the voice. Music beds are written from a brief on the same Mac, or taken from your own library, and ducked under the voice. Everything renders locally and deterministically, whenever you start it.
The skill does not let the agent put production text on screen: no labels, source notes, counters, voice names, image credits or commentary. Text appears only when it is what the viewer should be reading at that moment.
Everything in <<double brackets>> is yours. Fill it in before you paste. If you have no music indexed yet, say so in the same message, and the skill will write the beds itself or tell you what to point it at.
Install it
Copy or download SKILL.md above.
For Claude Code, save it as ~/.claude/skills/groundslate-3d-kinetic-short/SKILL.md.
For claude.ai and the Claude desktop app, put it in a folder named groundslate-3d-kinetic-short, zip the folder, and upload it under Settings, Capabilities, Skills.
The skill
---
name: groundslate-3d-kinetic-short
description: Build a 90 second vertical short in GroundSlate: every beat its own 3D-rigged composition over footage where footage exists, treated with a lens and a film stock, type highlighted word by word against local narration, seamed with the in-page transition kit. Art directed, not templated.
---
# The 3D kinetic short
Ninety seconds, vertical, no template. The words are the motion, the frame has
depth, nothing holds still. The code blocks are literal.
## The shape
Teaser of about eight seconds, one idea landed hard. Then the beats, ten is
common. Then a landing of about seven leaving a thought behind: a reversal, a
consequence, a question the viewer finishes. A recap is not a landing. No
furniture: no chapter cards, labels or counters, **no progress bar**.
## Before you start
```
list_profiles(); get_context()
composition_doctor(); voice_status()
search_catalog(query="", media_type="audio", visual_kind="music", profile=...)
list_fonts()
```
- `get_context` is machine scoped and refuses `profile=`; it names the ACTIVE
profile. Confirm it, then pass `profile=` on every dataset call.
- `composition_doctor` wants Playwright, headless Chromium and ffmpeg. Relay its
install line and stop.
- On `voice_status` act only on `weights_present`: false is a download only the
**user** can start, and `fix` names the screen. Relay it verbatim and stop. The
tier and model are theirs; never plan around changing them.
- Three music beds. `generate_music` writes them from a brief on your Mac; the
user's own tracks work too. If it reports the model is not downloaded, relay
that sentence and stop, and offer to work from an indexed folder instead.
- Three type families that resolve locally, or the render silently falls back.
Run `check_fonts` every render pass.
## Three questions, then work
1. The subject, and the claim it leaves behind. Not a topic, a claim.
2. Existing voices or new. Show `list_voices(profile=...)` and recommend casting
from it; propose an anchor for the spine, guests for a beat each.
3. Anything that must appear, or must not.
Never ask about transitions, fonts, footage or timing. Those are the job. Then
write the script: 210 to 240 spoken words, and use fewer. Hold one **callback
rail** throughout: an object that exits changed, a colour that migrates, a word
recurring at a new scale. Show the beat plan first: what is said, who says it,
the idea, the seam.
## Cast
```
preview_voice(voice_id="v1a2b3c4", profile=...)
create_voice(name="Anchor", gender="female", age="adult", pitch="low",
tone="dry, unhurried, close to the mic", profile=...)
synthesize_speech(text="<one line>", voice_id="v1a2b3c4", out_path="...", profile=...)
```
`preview_voice` speaks a fixed house line, not your script. Prefer those composed
axes; adjectives alone land near one default. One audio file per line, never one
for the film. The anchor returns; guests arrive on a change of footing.
## The law of the type
```
transcribe(path="<the line's audio>", profile=...)
```
`segments[].words[]` gives `{w, start, end}`. Words **enter** on their own start
times, and the active word **highlights** off its own start and end, lifting
through colour, weight, scale or a passing light. Match by word, never a timer at
an average rate. Then:
- **Nothing below 30pt** at 1080 by 1920. Too long to read at 30pt means cutting
words, never the size.
- **No widows**: no word alone on a last line, no orphan on any line. Rebreak,
tighten the measure, or cut. Going under 30pt is not a fix.
- **No word cut off** by frame, container or mask, unless it is deliberately
travelling and its exit is the move. Type that holds position stays in the
title safe area: 10 percent in, more at the bottom.
- **Fifteen words on screen is a ceiling**, three to six the target.
- **Nothing on screen that is not being said or shown**: no labels, source notes,
chapter numbers, voice names, credits or commentary.
- **Type is material**: `background-clip: text` with a moving gradient or the
beat's own picture through the letterforms; type on its own tilted 3D plane
catching a moving light; a blurred lit-edge glass plate under a line on a busy
ground.
## Every beat is its own composition
If beat six's page could render beat seven unchanged, you built the thing this
skill prevents. Vary where the eye lands, what leads the move, how a beat opens.
```
write_composition(name="beat-04", html="<the page>",
files=[{"name": "gs-transitions.js", "text": "..."},
{"name": "moves.js", "text": "..."}], profile=...)
```
Every renderer needs an `html_path` that already exists; this makes one, siblings
vendored beside it, same `name` overwriting.
- **Rig it in 3D**: `perspective` on the root, elements at different
`translateZ` and different rates, so words pass behind the subject.
- **Everything moves at once**: the ground pushing, the type travelling on the
grid and in Z, the light or gradient moving over it, the depth relation
changing. The type never parks; motion that stops before a beat ends is a bug.
- **Overlays and gradients in every beat**, blur as distance, shadow cast by the
geometry rather than a uniform drop shadow, and translucent edge-lit glass the
type sits on or passes behind.
## The ground: footage first
**Prefer moving footage; fall back to a still.** Look inside the profile first.
```
search_catalog(query="<the beat>", media_type="video", visual_kind="footage", profile=...)
extract_clip(src="<the clip>", start=12.5, duration=4.0, out_path="...", profile=...)
reframe_vertical(src="...", start=0, duration=4.0, out_path="...", profile=...)
fetch_asset(url="<the url>", source={"credit": "...", "license": "Public domain"}, profile=...)
```
Library footage, then public domain footage, then a still. Reframe landscape to
1080 by 1920 **before the join**, because `render_sequence` fits rather than
crops and unreframed footage arrives letterboxed; `make_reframe` plans a
subject-anchored crop and returns an argv for `render_ffmpeg`.
**`render_html(background_video=...)`** composites the page over a cut, keeps its
audio, and puts no `<video>` in the page, so capture stays cheap;
`window.__duration` must equal the cut's length or it truncates. A `<video>` in
the page, or `author_film` media scenes, is the expensive path: see the seams.
**Formats, because a Commons file is often not one of ours.** GroundSlate ingests
`.mov .mp4 .m4v .mpg .mpeg .avi .mkv .webm .mts .m2ts .3gp`. A `.ogv` is not on
that list and **does not refuse**: it lands renamed `.ogg`, filed as
`media_type: "audio"` and queued for transcription, picture gone, no error
anywhere. Read `media_type` on every fetch and take the WebM rendition, which
ingests as video. In a page it is narrower still, since `author_film` checks the
extension and Chromium then has to decode it: H.264 `.mp4` or VP8/VP9 `.webm`.
Put anything else through `extract_clip`, which re-encodes to H.264 at 29.97.
`remove_background` is stills only, so the cutout-in-front trick belongs to a
still beat. **The receipt lives in the project note, never in the frame**: no
credit line, no source URL, no end card, and leave `recognize` off. Nothing
locked off holds past five seconds. Treat every one: grade, blur a plane, crop
oddly, duotone, mask it to the type.
## The look
```
list_effects(category="fam_2cc52492")
list_effects(category="fam_f3e2b453")
apply_effect(src="<the beat's clip>", effect="petzval", out_path="...",
intensity=0.6, profile=...)
```
Ninety-six looks, all `cost: light`. **Lenses are depth**, optics that agree with
the geometry you rigged: `diorama` runs a plane of focus through the frame,
`petzval` swirls the edges soft, `spectra` splits the channels. **Stocks are
movement**: emulsion grain is temporal, regenerated every frame, so it lives even
over a still.
Hold **one stock** across the film, change the **lens** per beat: one material, a
shifting eye. Both moving every beat is a slideshow of filters. It bakes a whole
clip at one `intensity`, so a look is a per-beat decision taken after the beat
renders and before the join.
## The seams
Twelve moves, none twice in a row.
```
list_transitions(engine="all")
get_kit("transition kit", include_notes=True)
```
**Prefer the page**, a hundred moves in eight families. `page-fluid` derives its
blur from its own easing, so the smear is the speed; `page-glass` agrees with the
glass in your beats; `page-rig` turns assemblies in depth. Vendor the kit through
`write_composition`. Every move is a pure function of `t`: no `transition:`, no
`@keyframes`, no `animation:`, no state between seeks, and a seam overlaps rather
than adds time. `seek_not_pure` at a 3D seam is a known false positive.
**In ffmpeg, stay in the light thirteen.** The other eighteen are per-pixel
warps at five to thirty seconds a join, paid again every draft pass. Page rows
carry `sec_per_frame` as `{still, video}` rather than `cost`, and you read the
column your scene is: over stills a wipe is about 30ms a seam frame and a turning
rig 200, on a 25ms baseline; with a `<video>` in the page the baseline is 177 and
those rigs are 600 to 750, which is the argument for `background_video=` on a
footage beat you want a rig on. Never imitate `pull_back`, `dolly_zoom`,
`lens_warp`, `warp_morph`, `swirl_morph`, `ripple_bloom` or `liquid_swipe` in CSS.
```
render_sequence(sources=[...], transitions=[...], out_path="...",
quality="draft", audio_path="...", plan_only=True, profile=...)
```
N-1 transitions or it refuses; `"cut"` is a hard one; the move is `name` here,
`transition=` on `render_transition`; `duration` in seconds. `plan_only` returns
the length the mix must match; drop it to render.
## The score
```
music_map(path="<track>", profile=...)
mix_audio(voice="<the narration>", music="<the bed>", duration=<the plan total>,
out_path="<the mix>", profile=...)
render_ffmpeg(argv=<the ffmpeg_argv it returned>, out_path="<the same path>")
```
Cut on `cut_points`, not raw beats, changing track at a beat boundary on a cut
point of the outgoing one. One continuous mix, never a bed per scene. `mix_audio`
emits an argv and runs nothing; both calls need one `out_path`, and the mix goes
to the join as `audio_path=`.
## GroundSlate renders the web itself
This is the art department for anything that lives on a site, and it beats
sending a human for screenshots.
- `capture_page` is a still, rendered by WebKit on your Mac. `device` is
desktop (1440x900) or mobile (390x844 with a phone user agent, so the site
serves its real mobile layout). `full_page` for the whole scroll, `scale`
for retina. For a 9:16 still use `device="mobile", width=360, height=640,
scale=3`, a true phone viewport upscaled rather than a squeezed desktop.
- `record_page` is VIDEO of a page, and `scroll=True` walks it top to bottom
across the recording. That is what turns a static site into footage. Record
at 24fps: a scrolling page repaints continuously and holds 7 percent of its
frames at 24 against 16 to 26 percent at 30, measured.
- `consent="reject"` dismisses a cookie banner by REFUSING it, at capture
time, so the banner is never in your plate. There is no accept option and
there should not be one.
- `session="shared"` uses the durable signed in session from a window the user
opened, so a page behind a login can be captured. `open_browser_window` puts
a real window on their screen with your `reason` written above it, for a
login or a verification step. Nothing here solves a challenge for you and
nothing should try.
- Capture is off until the user turns it on, and then answers to their site
policy. `browser_policy` reads it. `request_browser_site` ASKS, with your
reason, and the user decides in GroundSlate. You cannot grant it, and asking
twice increments a counter rather than creating a second request.
Use it for the real artefact rather than a recreation. A film about a website
should contain that website, moving, with its own type and its own
photography, not your drawing of it.
## Show them, every round
A round of work ends when the human has seen it, not when you believe it is
good. `request_review(source, ask, agent=, profile=)` puts the file in front
of them with a timeline they can comment on, regions they can draw and a
verdict bar. Do this at the end of every round, unprompted. Describing a
render in prose is the thing the tool exists to replace.
- ALWAYS pass an `ask`. With one, the window carries Approve and Request
changes. WITHOUT one it is a SHOW: it still accepts notes and offers no way
to submit them, so a human can write thirty notes into a window that cannot
send them. Use a show only to put an asset in front of somebody with nothing
to decide.
- ALWAYS pass `agent=`. Unnamed, you are recorded as the person: nothing is
reused, `withdraw_review` refuses everything including your own, and the
app's raise skips your review entirely.
- READ `placed` BACK AND SAY WHAT IT SAYS. `opened` means the window is coming
up, `waiting` means the app is running but not frontmost so its Dock icon is
bouncing and the review is on the Dashboard, `saved` means the app is shut
and it will be there next launch. Telling somebody to take a look when their
app is closed is exactly what that field exists to prevent.
- Poll `review_status` for the verdict and the notes. Park on `review_wait`
only when the verdict genuinely blocks the next step; it is a blocking wait
on a human.
- One live ask per file. `withdraw_review` takes back a superseded question of
your own. It refuses a review that has been decided, one carrying comments,
and anybody else's, which is correct: never delete somebody's writing.
Every note carries an anchor: `t`, sometimes `t_end`, and a `region` with a
normalised box plus a `where` in words. Work from the anchor rather than your
reading of the sentence. Map each note to the scene that owns that moment and
write the map into the project with `write_project_file` before you start, so
the round survives you.
Then report per note, one block each, DONE or NOT DONE or PARTIAL, with the
evidence. A claim is not evidence. "Added kinetic text" is a claim; "probe
returned clean, rendered 562 frames, the three lines land at 2.94, 4.10 and
6.84 inside the safe area" is evidence. If a note cannot be done, say NOT DONE
and why, and never report a note done that you did not verify. Ask for the
next round the same way.
## Everything happens inside GroundSlate
The tools run on the user's Mac, in the background, on a queue. They are
faster than you and they cost none of your context. Every minute you spend
hand rolling a filtergraph is a minute the machine was idle and your context
was being spent on something nobody can read afterwards. Reach for the tool
first, every time.
- Authored pages: `write_composition` writes your HTML to a real path the
renderers can open, and vendors sibling files beside it. Before it existed
an agent authoring scenes had to reach for a filesystem bridge outside
GroundSlate. Do not go back to that. `preview_html` to check it,
`render_html` to bake it.
- Assembly: `render_sequence` takes N clips and N-1 seams in one call and
encodes every frame exactly once. `author_film` expresses scenes and seams
as one page composition. Both enforce the identity below; a hand rolled
concat does not.
- Ducking: `mix_audio` IS the broadcast sidechain. Pass voice and music, get
`ffmpeg_argv` back, hand it to `render_ffmpeg`. Note the one parameter that
is not in decibels: `threshold` is a linear amplitude because that is what
the compressor takes, and a dB value is refused rather than misread.
- Effects: `list_effects` and `apply_effect` for the graded looks,
`list_ai_effects` with `preview_ai_effect` before `apply_ai_effect`. The AI
effects run on models the user installs in the app, Models then Effects;
until they are there `apply_ai_effect` answers `effect_not_ready`, so say so
and stay with the graded looks.
- Transitions: `list_transitions` catalogs both engines, the ffmpeg one and
the page one (`engine="html"`), and `transition_motion` gives you a move's
actual curve and what it drives. Choose on the MEASURED `seam_seconds`, not
the `cost` tier, which is wrong in both directions at the edges.
- The web: see the capture section. GroundSlate renders pages itself now.
And do not bake early. A film that exists only as a finished mp4 has to be
rebuilt in full for a one word change. Keep the edit as data for as long as
you can, and bake the deliverable last.
## The project is the source of truth, and the test is a second agent
Everything this production learns lives in the project, not in the
conversation and not in a scratch directory. The test is concrete: a
DIFFERENT agent, tomorrow, given only the profile and the project id, must be
able to pick the work up and continue it. If they cannot, the production
failed, however good the film is.
- `create_project` early and hold the `prj_` id. Pass it to everything that
binds, not only the final render.
- Media from the web binds as it arrives: `fetch_asset` with `project_id`.
Media you made binds in two steps, `index_image` then
`add_media_to_project`, whose result carries `unresolved` for references
that matched nothing. Read it rather than assuming.
- Text is working material and it has its own door. The brief, the script,
the per scene cue tables, the note map from each review round, the voice
ids, the measurements you took and the ones that surprised you: all of it
goes in with `write_project_file` under an area prefix. `add_project_note`
records a decision AND the reason for it, which is the part a later agent
cannot reconstruct.
- `project_manifest`, `list_project_files` and `read_project_file` are how
that agent resumes. Write for them.
- `commit_to_library` the finals only, with the project. Renders live in
Scratch Outputs, which may be emptied.
Anything you leave only in the conversation is gone when the conversation
ends. That includes the reason you chose a voice, the cue table you measured,
and which of the client's notes you decided not to act on and why.
## Timing is frames, and frames are the currency
- A transition OVERLAPS its two shots, it never adds time. A cut runs
`sum(clip frames) - sum(seam frames)`. Do the arithmetic in frames.
- Declare durations that land on whole frames at your fps. 18.15 seconds at
30fps is 544.5 frames and floors to 544, one short of what you meant, and
nothing tells you.
- ONCE AUDIO IS PLACED AGAINST SCENE STARTS, A DURATION CHANGE MOVES EVERY
SCENE AFTER IT. Freeze scene durations before you place a single cue, or
accept that you will re-derive every cue downstream by hand.
- Verify frame counts with `ffprobe` on the delivered file, not from the job
record. Jobs can vanish from a shared queue without writing a file and
without a terminal state.
- Anything that passes through `minterpolate` needs an explicit constant frame
rate normalisation afterwards. Its timestamps do not land on the frame grid,
and the next step drops frames without saying so.
- Keep the whole film one pixel format and one colour range. A single
full range clip among limited range ones is a black level jump at that
splice, and a frame count will never show it.
## Motion has a floor, and a still is not a scene
Both of these are measured, not aesthetic.
- A drift below about 0.3 px per frame emits IDENTICAL frames. A ground
crushed to near black cannot register motion whatever it travels. Clear
8 px per second of visible travel on a ground you can actually see, and
check the render for repeated frames rather than assuming.
- Text over a moving background is not a moving scene. The plate drifts, so a
freeze check passes, and it still reads as a slide with video wallpaper.
The test: IF YOU PAUSED THE PLATE, WOULD THE SCENE STILL BE MOVING? Measure
the type and graphic layers on their own, separately from the background,
and know which of your scenes have grounds that are not tagged as plates,
because those flatter the number.
- Generated video has its own failure modes worth knowing before you spend an
hour of GPU on them. A ping pong loop built by reverse and concat returns to
its first frame on a fixed period and reads as a loop however slow it is.
Bright emissive elements, neon, filaments, blown highlights, smear and plume
frame to frame and look like smoke. Naming camera equipment in a prompt gets
the equipment rendered into the shot. And a generated plate almost always
needs a blur before it stops reading as generated.
- Prefer authoring the camera move yourself over asking the model for one. A
scale driven from `t` is exact, adjustable without regenerating, and cannot
smear. A model's camera move is whatever it decided.
## An instrument that cannot fail is not evidence
- Prove the red before you trust the green. Run your check against something
you know is broken and confirm it reports the fault. A checker that skips
elements with children will pass every headline in the film.
- Whisper is a reconstructive model. Its language model restores the plural it
expects, so a transcript will tell you a word was spoken when the audio does
not contain it. Never use a transcript as evidence for an inflection, a word
ending, or anything acoustic. Measure the acoustics.
- Beware a default that is indistinguishable from a real answer. It passes
every test and is wrong exactly where it matters.
- When two instruments disagree, the one that can be wrong in the direction of
the common case is the suspect. Look at the thing itself; looking settles
cases that measurement cannot.
- Measure the population the claim is about, and confirm on a different axis
than the one that produced the claim.
## Look at it, then deliver
```
preview_html(html_path="<the scene>", at=[0.4, 1.2, 2.6], profile=...)
sample_frames(path="<the film>", at=[2, 9, 21, 38, 55, 72, 88], profile=...)
create_project(name="<the subject>", kind="personal|business", profile=...)
commit_to_library(path="<the final render>", project="prj_1a2b3c4d5e", profile=...)
add_project_note(project_id="prj_1a2b3c4d5e", title="Build", body="<beat plan, cast,
sources with credits and licences, the stock and lenses, the moves>", profile=...)
```
Both sampling calls return pictures: check every law above against the frames,
plus two beats that look alike or a move repeated at consecutive seams. The note
is where the licences live now. Then report the claim and the thought it lands
on, the cast, stock and lens per beat, the moves in order, every source with its
credit and licence, and what did not resolve.