Imported from sdkv2/kino (
skills/video-production/SKILL.md). Install upstream withnpx skills add sdkv2/kino --skill video-production. Copyright stays with the author.
Producing videos with kino
kino turns a JSON spec you author into a finished 9:16 (and optional 3:4) video.
You supply the creative; the CLI handles VO (ElevenLabs) → avatar (optional) → composite (kino's frame engine).
Colour scheme (every spec declares one)
Declare one — an unset scheme still builds (falls back to kino's midnight) but validate warns. Cheapest form is one line:
"colors": "midnight" // or "noir" | "paper"
"colors": { "bg": "#0a0a0c", "accent": "#e6b34a" } // roles: bg fg accent accent2 deep
"colors": { "preset": "noir", "accent": "#ff0055" } // preset with one role deviating
kino colors lists the presets + what each role paints. A named preset replaces all five roles; role
keys layer on top. Read the real palette off the product (app stills in projects/<name>/assets/,
the logo, the store listing) before reaching for a preset — a preset is the honest default when you
have no brand colours yet, not a substitute for ones you could have looked up.
Light scheme (paper, or any light bg) → also set "film": 0; the cinematic vignette reads as a
dirty border on a light base.
Brand discovery (only when a spec needs more than colours)
A brand is not required, and colours alone do not justify one. spec.colors covers the palette;
a brand is for what several specs must share — Tone / Voice guidelines, fonts, disclosures,
voice/look aliases, banned phrases. Don't scaffold an empty brand and invent a personality. When a
spec genuinely needs one that doesn't exist yet:
- Reuse first.
kino brandlists existing brands — if one fits, use it (spec.brand/project.json) or copy the nearestbrands/<name>/brand.mdas a starting point. A new brand is for a genuinely new product, not a variation you can express per spec. - Ask the owner for what only they hold: product truth (what it does, who it's for), real palette + fonts, logo file, platform, tone, and any legal/disclosure constraints or banned phrases. Draft from product truth and get approval — never fabricate a look or voice.
- Gather from what's already here (don't invent what the repo can show you):
brands/*/brand.md— the nearest brand as the pattern for frontmatter + guidelines shapeprojects/<name>/assets/{screens,recordings}— real app stills: read the actual palette + type off the product, don't guess hex codes- the logo / brand kit the user drops in →
brands/<name>/assets/ kino fonts·kino voices(always search the catalog before settingvoice/defaultVoice) ·kino backgrounds— what's settable (font/labelFont, defaultVoice, background)assets-lib/— shared motion/backgrounds the brand can lean on (music/sfx ship empty)- or more (with permission): the app's App Store listing / site / press kit for public brand assets;
kino pexels/kino photos/ image-gen for stills the brand lacks
- Then scaffold
kino init <brand>and fillbrands/<brand>/brand.mdfrom what you discovered — frontmatter (palette/font/voice/disclosure) + a real Tone / Voice body (readad-voice), not the placeholder scaffold. Confirm palette + Tone/Voice with the owner before mass-producing specs.
Workflow
kino doctor— confirm ffmpeg and the keys for your chosen provider are present. (kino fontslists fonts settable asbrand.font/brand.labelFont— downloaded on demand.) Brands are optional markdown —brands/<name>/brand.md(YAML frontmatter for palette/font/voice/ disclosure + a free-form guidelines body with a Tone / Voice section). Runkino brand <name>to read a brand's styling + tone rules; with no brand, the spec's owncolorscarries the look. (Set the brand viaspec.brandor a project'sproject.json.) New brand? Do Brand discovery (above) first.- Author a spec (schema below). Opener: prefer a cold open on your strongest footage (see Trailer
shape) before a mesh caption card. Copy: read
ad-voiceskill before writing segmenttext/caption— follow the brand's Tone / Voice dial, then the anti-slop rules. Keep captions short. Typed UI / caption-free montage / spoof chat window: readspeech-synced-ui— captions are optional; stylised speech-locked typing lives in motion graphics (env.words), not the caption engine. Look / hierarchy / anti-generic craft for those graphics:motion-design. - Iterate (fast, free):
kino inspect specs/foo.jsonto map the beats, then look at pixels — never trust the JSON alone for motion/Lottie. Defaults are mock (zero spend). If mockdurationSec≤ ~20 on a 20–30s brief, pad VO lines before storyboard (both mock promos landed short on first inspect).kino still specs/foo.json --segment N— layout / composition of one beat (~1–2s)kino still specs/foo.json --around <sec>— required for any animated beat (typewriter, counters, Lottie, camera push): sheets N frames around a moment so progression is visible in one image (default 5 frames / 1s window; tune--span/--count)kino storyboard specs/foo.json— every beat twice (composition + ·full); check ·full for caption overflow /textscollisions Edit → still/--aroundagain → repeat. Before shipping a storyboard as "done": runadversarial-critique(subagent frame QA) — include--aroundsheets for motion/Lottie beats.
kino build specs/foo.json— real render →out/<title>/. For a spoken piece the timing pass needs real VO, so build it once with--tts(the only spending flag); that fills the VO cache and every--realread afterwards is free. Post-build:kino inspect --realfor word times, thenkino frames <mp4> --around <sec>(orkino still … --around <sec> --real) on every motion/Lottie/typed beat — mock timing lies; retune triggers / KEY_MS / camera from the sheet. (On a silent build there is nothing to reconcile: the estimate is what renders, and--realwill just tell you the cache is empty.) Re-runadversarial-critiquewhen layout could have shifted with real VO.
Projects keep campaigns tidy: projects/<name>/{specs,assets,out} + a project.json that assigns a
shared brand and default overrides. Run any command on a spec inside a project (kino infers it from the
path) or pass --project <name>. kino projects --new <name> [--brand <brand>] scaffolds one (brand
optional — omitted = kino house defaults). Specs must
live under a project — there is no flat layout.
Presenter provider (cost lever — pick deliberately)
A presenter is a video source on a beat, not a beat type: give a scene "source": "avatar:" for the
configured provider, or pin one with "heygen:look-id" / "hedra:portraits/x.png" / "replicate:".
Which provider avatar: means is set per spec with "provider", per brand with defaultProvider, or
via --provider:
none— no presenter; footage + VO + captions over the background. $0 presenter cost, and the strongest format for app installs because it shows the product. Default for most videos.heygen— Avatar-IV hosted look. Highest quality, most expensive (~20 credits/min). Needs a look id.hedra— Character-3. Cheap API + free monthly tier. Needsbrand.avatarImage(a portrait).replicate— open-source lip-sync (default SadTalker). Pennies/clip. Needsbrand.avatarImage.
Two automatic savings when a presenter IS used: it is trimmed to the on-camera beats only (video cut-ins aren't billed), and VO + presenter are content-hash cached so caption/motion edits don't re-bill. One presenter clip is generated per build, so every presenter beat must agree on provider and look.
Spec schema
{ "title": "kebab-case", "format": ["9:16"], "voice": "<alias>",
"colors": "midnight", // recommended unless a brand declares colors — `kino colors`
"brand": "<brand>", // optional — tone/voice, fonts, disclosures, aliases
"provider": "none", // none | heygen | hedra | replicate (else brand.defaultProvider)
"background": "custom", // prefer custom+backgroundComponent over mesh for brand identity
"backgroundComponent": "brand-wash", // bare id → assets-lib/backgrounds/ (or path / brand field)
"segments": [
{ "text": "spoken over the background", "caption": "on-screen text (optional — omit on any kind for a caption-free beat)" },
{ "source": "avatar:", "text": "spoken + lip-synced by the configured presenter provider" },
{ "kind": "video", "source": "screens/x.png", "text": "spoken (presenter hidden)", "caption": "...",
"captionMode": "words", "emphasis": ["claim"], // optional: spoken text, word-synced + highlighted
"kicker": { "text": "86% match", "color": "mint" } } ] }
Caption-free beats: omit caption → no caption node. Under a words-mode brand, also set
"captionMode": "phrase" on that beat or synced spoken words still paint. Stylised typed prompts
(terminal/chat) → speech-synced-ui, not a caption with fancy captionStyle.
⚠ Words-mode vs short lower-thirds (field trap): brand captionMode: words paints the spoken
VO on screen — the short caption string is not what appears. Want a punchy scroller line over
busy UI/plates? Override that beat with "captionMode": "phrase" + short caption. Want silence?
phrase + omit caption. Typed motion: same (phrase + omit). Both Driftlog + VoltStack mock
promos burned a pass on this.
Trailer shape — adapt, don't stamp. A ~20–30s / 7–9-beat trailer runs OPENER → a MIDDLE that shows the product → PAYOFF + CTA.
Opener = scroll-stop first. TikTok/Reels thumbs and the first ~1s decide whether anyone hears the VO. Prefer a
cold open on your strongest footage (video: product screen, real-world b-roll, or the most kinetic clip you have)
with a short hook caption over it. Mesh/glow/aurora caption cards (scene) and motion title cards stay valid —
use them when the brand is deliberately quiet/editorial, when you truly have no usable footage yet, or when a typographic
cold open is the brand move — but do not default to a soft blurred mesh + centered line just because presenter-less
trailers used to start that way. If the first still could be any SaaS ad after you squint, the opener is too weak.
Opener menu (pick one; bias toward #1 for consumer/app ads):
- Cold open (
video) — default lean. Strongest clip + one hook line (lower-third + backplate). Optional kicker.shot: "push-in"+transition: "cut"reads as a thumb-stop. Caption stays short (which hold?, not the full VO). - Motion title (
motion) — brand-forward graphic cold open when the product truth is a number/diagram. - Caption card (
scene) — hero text onbackgroundmesh/glow/aurora. Fine for quiet/luxury brands or copy-led hooks; still compose it (big line, hot palette intensity, not a muddy mid-grey blur). Never open on the brand name (ad-voice rule).
One proven layout for a footage-driven trailer (cold-open first):
0 video cold open — strongest footage + hook caption ← scroll-stop; not a mesh card by default
1 video footage — establish the world / product surface
2 video footage — show the product truth ← consecutive video beats auto-crossfade = montage
3 video footage — the payoff moment
4 motion a data/feature beat (counter, timer…) ← media ≈ half the runtime
5 scene payoff — the emotional turn (caption card OK here)
6 scene CTA — brand name + action; a presenter-less scene beat's caption is **automatically the centered end card** (hero), not a lower-third subtitle
Footage-cut rules: match each clip's length to its beat's VO; vary the shot per cut-in to the action (push-in / pan / pull-out); keep related shots back-to-back for the auto-crossfade; set the brand's captionStyle.background backplate so captions stay legible over uncontrolled footage. Plan the opener clip before writing beat 0 copy — pick the thumb-stopping frame, then write the one-line caption that rides it.
Source recordings (long captures): when slicing a screen recording / imported clip into multiple
beats, or seating footage in custom chrome, follow the importing-footage skill (clipFrom /
clipTo / speed / pauseAt / frame) — don't guess timestamps without reading stills.
Framed beats: shot: "static" only (no push-in / pan) — renderer enforces this.
Short-form layout defaults (TikTok / Reels / Shorts)
Default to this composition unless the brand guidelines explicitly ask otherwise. Agents over-tween layout and crowd the top chrome — don't.
| Layer | Default | Don't |
|---|---|---|
Hook / cold open (video) |
Strongest footage first; short lower-third caption + backplate; optional kicker | Soft mesh card as the default opener; brand-name first line |
Hook (scene, no presenter) |
Centered hero caption — big, calm, no captionKeyframes (use when caption-card opener is intentional) |
Pin to top edge; y: -16 "for variety"; muddy low-contrast mesh behind a weak line |
| Video / footage captions | Lower-third (engine default) + brand backplate | Per-beat y/scale jitters |
CTA (presenter-less scene, end beat) |
Centered end card (hero, automatic) — short brand + action; captionReveal: "all" or captionMode: "phrase" |
Park the CTA in the lower-third caption gutter; word-by-word drip on a long App Store line; empty mesh with no brand mark |
| Kickers | Top pill — fine when the still has empty top chrome | Treat kicker as the end card; kicker on a feed/chip still that already labels the moment |
texts[] labels |
Small, position: "top" (or clear of caption band) |
Second headline fighting the CTA end card |
| Motion / counters | Stack mid-frame: CSS .wrap { top: 38%–42%; } (no tiny translateY(20vw)), clear of caption band + top UI |
Park the graphic in the top ~20% (Following/For You chrome) |
| Platform chrome | still --platform overlay is a guide — protect hooks/CTAs/captions; tab bars & nav docks may sit in the shaded bands |
Treat safe-zone overlay as a hard keep-out for every element |
--platform safe zones are a guide, not a mandate. The overlay shows where feed UI tends to sit.
Hooks, CTAs, hero captions, and kickers should stay readable. Non-critical chrome (bottom nav, tab
bars, decorative docks, secondary labels) is allowed in those bands — don't lift them just to clear
the tint.
| Music | Quiet bed "volume": 0.10–0.14, "duck": 0.04, short fadeOutSec | Loud beds fighting VO/captions |
| Brand mark | A declared layer at z: 1000, rect near the top on talking runs; centred on a CTA only if it clears hero type | Omit the mark when the CTA caption already names the brand (double mark); tiny lower-third as the whole ending |
Caption stability is the default. Omit captionKeyframes on a first pass. Add one only when a
single beat must dodge a bright subject (check that still) — never a different y on every beat.
CTA = end card, not a subtitle. There is no cta field in the schema — the centered-hero treatment
is automatic: any presenter-less scene beat (no source) gets it, the same as every other
presenter-less talking beat (isHeroCaption — no avatar + kind: "scene", nothing beat-specific to a
CTA). Two valid CTA shapes: (a) a plain scene beat (no source) as the last beat — its caption
renders as the centered hero for free; add a motionOverlay for a wordmark if you want a graphic, or
(b) a pure kind:"motion" end-card with the CTA copy baked into the graphic/texts. A scene has no
face unless its source asks for one. Write a short caption (Cadence · free to try, not a
full spoken sentence). Prefer captionMode: "phrase" or captionReveal: "all" so the line lands as
one poster, not a word drip. A brand mark is optional: if the caption already carries the brand name,
leave the mark layer out — a centered mark + centered wordmark fights. Otherwise put it mid/top so the
frame isn't empty mesh + type. Stronger: end on an app still of the product, then a short hero CTA
beat — never a lone lower-third pill on blank mesh.
Screen PNG margins: bake empty top/bottom bands into product stills when you use push-in /
captions — chrome baked into the image (12 CAPTURES, status rows) gets cropped by camera zoom.
Prefer padding in the asset over fighting layout in the spec. Assets taller than 9:16 (standard
19.5:9 iPhone screenshots / marketing composites, e.g. 1320×2868) are cover-cropped top+bottom even
when static — not only under zoom. A headline baked at the extreme top/bottom (marketing plates)
gets clipped; keep essential text inside the centre ~80%, or use a raw screen / a plate authored at
9:16 for a text-carrying beat. (ffprobe <asset> to check the aspect before you seat it.) There is no
in-spec letterbox/contain fit — so if your only plate is taller than 9:16 (the bundled
marketing-*.png / presets.png are 19.5:9), either accept the top/bottom crop or don't hang a VO
line that names the text the crop eats.
Motion-beat recipe (counters, timers, big numbers): layout mid-frame, then animate — see Make motion graphics move (≥3 layers: entrance + life + speech/camera).
/* assets/motion/*.html — short-form safe */
.wrap {
position: absolute; left: 0; right: 0;
top: 40%; /* below platform chrome; above lower-third caption */
display: flex; flex-direction: column; align-items: center;
}
/* Keep the stack ≤ ~50vw tall so it doesn't collide with the caption band (~CAPTION_BOTTOM). */
Preview with kino still --segment <n> (layout) and kino still --around <t> (motion richness)
before the real build. If the graphic kisses the caption or sits under the top UI, nudge top —
don't reintroduce per-caption y offsets to compensate.
scenesegments are the hook/payoff beats (on camera only ifsourcenames a presenter);videosegments show the screenshot/recording while the VO continues. With no presenter, scene beats are branded caption cards.- Emphasis is a spice, not a sauce —
emphasisadds a glow to the marked word while it's spoken. Cap it at one word (max two) per beat, on the single word carrying the claim; several emphasised words per beat reads as noise and devalues all of them. Beats can (and often should) have none. - Key the highlight colour for contrast — the active (spoken) word and the brand name render in
colors.mint. Choose it to pop against the background luminance: a bright accent over a dark ground, a deep one over a light ground. A dark saturated accent (deep red, navy) on a near-black background reads dimmer than the white body text, so the one word meant to pop recedes — if the brand's signature colour is dark, keep the active word bright and carry that colour in the background ortextsoverlays instead. - Media density: caption cards are connective tissue, not the show — viewers stay for footage,
screenshots, and motion. Target roughly half the runtime on media (
appcut-ins,motionbeats, motionOverlays): in a ~20s spec that's 2-3videobeats + at least onemotion/Lottie moment. Never run more than two plain caption-card beats back-to-back; break the pattern with a cut-in or overlay. Compose each caption card for what its beat says, not to a template — the monotony that reads as a slideshow comes from framing every beat as the same centered line. Fit the frame to the content: a short hook can go big (centered, or only slightly raised — never pinned to the top edge), a full sentence sits calmer and centered, a two-part contrast can split, the CTA is a centered end card with the wordmark/action; add atextslabel only where the beat earns one. Variety is the result of composing per beat, not the goal — two beats that genuinely want the same frame may share it. The failure is every beat defaulting to dead-center because none was composed for what it says (don't jitter position just to make cards differ — that reads as noise, not design). B-roll sources: project assets,kino pexels/kino photos. Consecutivevideobeats crossfade shot-to-shot automatically (the first holds under the next's fade-in — no background flash between them), so sequencing related footage back-to-back is encouraged: it reads as edited film, not a slideshow. Consecutivemotionbeats dissolve the same way (~0.5s hold-through-gap + fade-in; first motion stays opaque for loop seams — seespeech-synced-ui→ Motion→motion handoffs). - Camera/transitions auto-vary — omit and
kinopicks a varied shot + transition per cut-in. Override per segment with"shot"(push-in/pull-out/pan-left/pan-right/tilt-up/static, plusscroll/scroll-upto pan vertically through a tall app still — a simulated scroll that reveals content below the frame; opt-in, so it's never auto-picked) and"transition"(fade/dissolve/fly-left/fly-up/pop/cut, plus thewipefamily) onappandmotionsegments — set it on the incoming beat. Motion beats are not in the auto-vary rotation: they default todissolve. The wipe family (wipe-down/-up/-left/-right, or barewipe+transitionParams.anglefor a diagonal) travels a lit edge across the frame and uncovers the incoming beat — that's the one that reads as motion graphics rather than a cross-fade, especially when consecutive beats share a layout skeleton. Tune it withtransitionParams(angle/softness/edgeWidth/edgeColor/edgeGain;edgeWidth: 0= unlit). Don't hand-roll a sweeping band inside the graphic to fake this: a CSS band can only draw over the outgoing beat — it cannot reveal the incoming one, which is a separate page. Carry a camera through the cut with"transitionCamera": { "move": "push" }(alsopull,pan-*,tilt-*,whip-*, or a raw zoom/pan vector) — the outgoing beat keeps moving as it leaves and the incoming arrives in motion, so the cut reads as one shot. Stacks on any transition. Reverse any of them with"transitionInvert": true(a reveal becomes a conceal) — works on built-ins and custom shaders alike. Need a shape the built-ins don't cover?transition: "custom"+transitionSource(bare id fromassets-lib/transitions/, or anassets/path) runs your own.frag— author a ShaderToymainImage()withkinoFrom(uv)/kinoTo(uv)/uPin scope. Runkino transitionsfor the full contract and the library list; copyiris.fragas a starting point. Auto-vary is asset-aware: video b-roll defaults to the soft pair (dissolve/fade) and UI stills to the punchy rotation — match that instinct when overriding (footage wants a natural fade, not a spring fly-in). Plan shot variety before writing the spec, not after seeing the storyboard: for 3+ consecutivevideobeats, jot the camera move and transition per beat first (CONCEPT.md) — auto-vary picks per-cut, not across the whole run, so an unlucky repeat (three push-ins) slips through unless you skim your own list. Tall stills (~2200px+) earnscroll; 9:16 plates do not. - Faceless backgrounds (
kino backgrounds): do not default tomesh— but a brand whosebrand.mddeliberately setsmesh+ tunedbackgroundColorsis brand-correct; keep it (the CLI nag is generic and can't tell a brand-set mesh from a lazy default). Set mesh colours via brand frontmatterbackgroundColors, or per-spec viabackgroundKeyframesparamscolorA/colorB/colorC(a single keyframe sets a constant; two animate) — there is no top-levelbackgroundColorsspec key (strict schema rejects it). Stock mesh/aurora with no colour work is the generic tell. Prefer:"background": "custom"+"backgroundComponent": "brand-wash"(or your draw fn) for authored identity"solid"whenseamlessLoop/ settle (no global-frame drift)"image"+backdropfor photo stages- a full-bleed
.bginside motion graphics (occludes the background layer entirely) SpecbackgroundComponentoverrides brand. Tween withbackgroundKeyframes/backgroundTriggers; sync to VO viakino inspect. Seedocs/backgrounds-and-overlays.md.
- Overlay elements tween (
kino elements): captions + kickers tween via per-segmentcaptionKeyframes/kickerKeyframes, and anything you declare inspec.layers[]takes the samekeyframestrack — all x/y/scale/opacity over time, same keyframe system. - Camera push on app footage (
zoomKeyframes, perappsegment): scales/pans the footage + frame chrome as one group about centre — the "canvas zoom" for inset iPhone footage. The phone grows/pushes in; captions, kicker, logo and the background stay anchored. Beat-relative track (at= seconds from the beat's start,0= beat start — it rides the beat, so re-timing the video never desyncs it), paramsscale/x/y/opacity; one keyframe = static hold, two = animated push. Aframedisables the innershot, soshot:"static"+zoomKeyframesis the way to move the camera on device footage. Seeimporting-footage. Motion graphics do not usezoomKeyframes— zoom/pan there is a CSStransformon a wrapper driven by--progress/ a keyframed param / typed fraction (speech-synced-ui). A zoomingmotionOverlayon a static PNG frame desyncs text from chrome; put chrome+text in one motion beat. - Stylised captions:
captionStyle(stroke/highlight/gradient/minimal, defaultstroke) andcaptionAnimation(pop/rise/typewriter/wave/blur-in/none, default = the surface's native entrance) set top-level or per-segment (segment overrides spec overrides brand).captionModesets the same way — brand < spec < segment: a top-levelcaptionModeis the spec-wide default, a segment overrides it (verify withkino inspect, which reports each beat's resolved mode). The spec schema is.strict(), so a misspelled or misplaced top-level key now errors at parse instead of being silently ignored — a typo surfaces immediately rather than being swallowed.captionReveal(words mode, defaultword) sets how the line arrives:wordpops each word in at its VO time;alllays the whole caption out and fades it in together, the active word still highlighting as the VO reaches it — useall(orphrasemode) for a CTA or any long line, since a word-by-word reveal of a long line strands its first word at a wrapped corner during a VO pause. Honored in the native raster (opacity/layout, not motion).captionAnimationentrance presets paint into the caption raster too — per-frame while the entrance is in flight, keyed once it settles — seereference.md. Per-segmenttexts: [{ text, at, dur?, position?, size?, style?, animation? }]drops standalone headline overlays anywhere on the frame (slot + small/medium/big, independent of the segment's own caption) — keep them clear of the caption's band so the two can't collide (the ·full storyboard tile shows collisions). Details + the style/animation tables inreference.md. - Motion graphics (
kino motion): for a fully custom animated beat or overlay, author a self-contained HTML/CSS file inassets/motion/and reference it from the spec — a full-screen beat ({ "kind": "motion", "source": "motion/x.html", "text": "spoken VO" }) or an overlay on an app/avatar beat ("motionOverlay": { "source": "motion/x.html" }). You write the HTML/CSS; the JSON owns timing. Layout first (short-form): put the stack mid-frame (.wrap { top: 40%; }) — see Short-form layout defaults. AtranslateY(20–28vw)from the top lands under TikTok/Reels chrome; don't ship that. Animate by reading kino-set CSS variables —--progress(0→1 over the beat),--t,--frame,--pulse, yourparams(e.g.--pct, tweened bykeyframes), the brand palette (--kino-mintetc.), and--kino-words-shown/--kino-word-count(VO-locked typed UI). Tier-2.jsalso getsenv.words— beat-relative{ word, start, end }[]from the same TTS timings the caption engine uses. Use that for terminal/chat typing the caption presets can't style (speech-synced-ui). Two font vars are available too:--kino-font(the caption font) and--kino-label-font(brand.labelFont, falls back to--kino-fontif the brand sets none) — pair a display face on the hero number with a mono/label face on a supporting chip instead of reusing one font everywhere. You can also use real@keyframes— addclass="kino-anim"and kino force-pauses- scrubs them across the beat deterministically (sub-timing in the
%stops, stagger via--kino-delay). No CSStransition/JS and don't setanimation-play-state— motion is always frame-driven (CSS variables or scrubbed@keyframes). For gradient-filled text (background-clip:text) with tight/negativeletter-spacing, addclass="kino-cliptext"so the last glyph's edge keeps its gradient instead of being clipped. For loops/computed geometry, pointsourceat a.jsfile whose body isrender(env)returning an HTML string (evaluated per frame, determinism-linted) instead of a.htmlfile. Stagger reveals so things don't all land at once — auto-stagger a list withsibling-index()(--d: calc((sibling-index() - 1) * .08)), give each element its own slice of--progress, or use a param-per-element with offset keyframeattimes for spring/ease control. Runkino motionfor the full contract, the stagger recipes, and a copyable example. Preview is not optional — see Make motion graphics move and Motion / Lottie visual loop below. Tier-3 Lottie (.json): pointsourceat a designer-authored Bodymovin/LottieFiles.jsonfile to embed organic illustrated motion or AE-produced animations that an agent can't hand-author. kino plays it deterministically with a frame-seeked Lottie player. Key rule: for amotionOverlay, the asset must have a transparent background — an opaque export occludes the avatar or app screenshot. Add"loop": true(sibling ofsource) to loop at native speed; default plays once stretched across the beat. Word-fire: give the Lottietriggersat VO word times (fromkino inspect) and each fires a fresh one-shot burst in sync with the words (use a short, transparent burst asset; triggers override stretch/loop). Assets must embed images (base64data:URIs) and outline/embed fonts (no system fonts, no AE expressions). Works in all three motion slots (kind:"motion",motionOverlayonavatarorapp). Rebrand a template's logo/image slot by replacing the image asset's base64ppayload. When adapting LottieFiles downloads: strip thefh/fs/fbblock creator exports stamp on text animators (renders all text red in lottie-web), delete the near-universal opaqueBackgroundlayer, and don't rewrite template text (glyphs are baked; only exported characters render). After any Lottie adapt/rebrand:kino still --segment N(layout + transparency) andkino still --around <mid>(stretch/loop/word-fire actually moves) before calling it done.
- scrubs them across the beat deterministically (sub-timing in the
Make motion graphics move
Agents under-animate. Default failure mode: a static card that only fades opacity with
--progress, then holds dead for half the beat. A motion beat should feel like edited film, not a
poster with a dissolve. Prefer too much intentional motion (then dial back) over a freeze.
Minimum motion budget (every kind:"motion" / rich overlay)
Ship at least 3 simultaneous layers of motion on the beat (pick from the menu). Opacity-only entrance counts as one — not enough by itself.
| Layer | Examples (use the toolkit) |
|---|---|
| Entrance | Staggered kino-rise / kino-pop / kino-blur-rise; scrubbed @keyframes; param tween with overshoot/spring |
| Continuous life | Slow drift/rotate/breathe off --t (rotate(calc(var(--t)*12deg)), subtle scale pulse); looping Lottie ornament; blinking caret |
| Speech lock | env.words / --kino-words-shown typing; Lottie triggers / kino-pulse on word times; counter params keyed to VO |
| Camera / settle | CSS .cam push-in or pan across the beat; end settle (scale back / opacity hold) so the last third isn't frozen |
Design rules
- Stagger is mandatory when ≥2 elements share the frame —
sibling-index()+--kino-delay, or offset--progressslices. Everything landing on the same frame = slideshow. - Something must keep moving after the entrance — idle life (
--t), looping Lottie, caret, shimmer, or a slow camera. A beat that finishes its reveal at--progress:0.3and sits still until the end is unfinished. - Drive numbers/bars with
params+keyframes, not a static label. Preferease: "overshoot"or"spring"on the money moment; linear only for clocks/meters that should feel mechanical. Baseparamsare an implicit t=0 keyframe, so"params": {"pct": 0}+ one keyframe tweens 0→86. Anchor to spoken words:{"atWord": "match", ...}(word text or index) instead ofatseconds wherever the moment belongs to a word — it resolves against the build's real VO, so nothing desyncs between mock and real.atstays beat-relative seconds for word-less moments. - Punch the VO — at least one visual accent on a spoken word (
triggers→kino-pulse, word-fire Lottie, or a param jump). Silent motion + talking VO = disconnected. - Multi-step UIs (pipelines, tile triptychs) light off
env.wordswhen the VO names those steps — not a fixedt0/perclock. Fixed clocks finish early under real VO → dead tail. See Real VO retune. - Prefer Lottie for organic loops/bursts (dots, confetti, sparkle) over reinventing them in CSS — but don't bake headline copy into Lottie glyphs.
- Brand calm ≠ motionless. Quiet brands still get soft continuous life + one clear entrance; loud brands get harder pops and word-fire. Match Tone/Voice amplitude, don't delete motion.
Anti-patterns (reject on --around sheet)
- Only
opacity: var(--progress)on one block - All chips/words appear on the same tile of an
--aroundsheet - Counter/label never changes across the sheet
- End card wordmark static for the whole beat (no scale/fade/pan)
- Lottie stretched once with no loop and no triggers when the beat is >1.5s of “thinking”/ornament
- Pipeline/tiles all land in the first third of a long beat, then hold dead while VO continues
- On-screen step nouns ≠ spoken nouns
Quick stack (copyable instincts)
<!-- entrance + stagger -->
<div class="chip kino-pop" style="--kino-delay:calc((sibling-index()-1)*.08)">…</div>
<!-- continuous life -->
.orb { transform: rotate(calc(var(--t) * 25deg)) scale(calc(1 + 0.03*sin(var(--t)))); }
<!-- camera -->
.cam { transform: scale(calc(1 + 0.06*var(--progress))); transform-origin:50% 45%; }
"params": { "pct": 0 },
"keyframes": [{ "at": 0.15, "params": { "pct": 86 }, "ease": "overshoot" }],
"triggers": [{ "at": 0.4, "action": "pulse" }]
Verify richness with --around — tiles should look different in a way that reads as craft, not
noise. See kino motion + docs/motion-graphics.md for the full contract.
Motion / Lottie visual loop (use still + --around)
Agents under-preview motion. Treat every HTML/CSS/JS graphic and every Lottie as unfinished until
you have Read pixel stills at multiple stages — not just inspect JSON or one midpoint frame.
A still sheet is cheap. Measure it once and stop rationing. On an M4, eight stills of a motion-only spec is ~1.7s end to end, of which ~0.9s is fixed (Electron boot + page load) and the marginal cost is ~15ms per extra frame. So: ask for MORE frames, not fewer, and re-render after every change instead of batching edits. Two corollaries that matter more than the rule itself:
- Never infer render cost from how long a tool call felt. A call that also ran a montage, edited three files and carried your own reasoning is not a measurement of the renderer. If you find yourself rationing previews, time one run first — the belief that stills are expensive causes far more under-previewing than any amount of "repeat often" guidance cures.
- A rendered frame pulls this loop in, whatever the task was. Engine work, a new effect, a library spike, a demo for a feature — if the deliverable is something a person will look at, it is authoring output and gets the authoring loop. Classify by what came OUT, not by what it was for.
| Stage | Command | What you're checking |
|---|---|---|
| Scaffold / first paint | kino still <spec> --segment N |
Layout, palette, caption clearance, opaque Lottie bg, chrome geometry |
| While tuning animation | kino still <spec> --around <t> (repeat often) |
Progression and richness: typewriter, counter, camera, Lottie phase, stagger, idle life |
| Dense / short beats | --around <t> --span 0.6 --count 7 |
Sub-second motion that a 1s/5-frame sheet smears |
| Whole-cut layout | kino storyboard <spec> |
Beat-to-beat jumps; ·full overflow/collisions |
After a --tts build |
kino still … --around <t> --real or kino frames <mp4> --around <t> |
Speech lock (mock word times ≠ real); retune triggers / KEY_MS / params |
| Critique | adversarial-critique on stills plus --around sheets for motion/Lottie beats |
Overlap + frozen + under-animated |
Hard rules:
- After every non-trivial edit to a motion file, Lottie JSON,
keyframes/triggers/params, or typed-UI proc → run--aroundon that beat (picktnear the interesting moment fromkino inspect) and Read the sheet image before the next edit. - Do not ship a motion/Lottie beat that you have only seen as a single
still --segmentor storyboard midpoint — that frame can look fine while the animation is wrong or missing. - Prefer
--aroundover hand-listing--at a,b,cunless you need uneven sample times; use--montagewhen you already have an--atlist and want one sheet. - Typed UI / speech-locked surfaces → also follow
speech-synced-ui(same still loop, stricter). - If the
--aroundsheet barely changes → add layers from Make motion graphics move, don't declare victory. --segment Nis the beat midpoint, not t=0. Loop posters / seam frames needkino still … --at 0(and--at <beatEnd>for the last frame). Midpoint stills lie about empty ready-states and end-of-beat clears.- Preview before the expensive rebuild. A full encode of a motion-heavy cut can take
many minutes (~tens of minutes for ~20s @ 30fps of Tier-2 graphics). Keep per-beat harness
specs (
specs/_b0.json…) that render one motion source so you cankino still --aroundin seconds. Onlykino buildthe assembled spec after harness sheets pass. - Copy edits move the clock.
--aroundtakes global seconds, and editing any beat'stextre-paces every beat after it — sheet times derived before the edit now straddle beat edges (classic case: padding VO to hit runtime, then sheeting with pre-pad times). Preferkino still --segment N --word <w>(always resolves against the current VO); for raw--aroundtimes, re-runkino inspectand re-derive everytafter any copy change.
Real VO retune (mandatory before ship)
Mock word times are evenly faked. Real ElevenLabs timestamps differ — fixed schedules
(t0/per, hardcoded triggers, progress-only reveals keyed to mock length) desync and leave
dead tails (animation finishes, VO still talking).
After the first --tts build (which is what puts real VO in the cache for --real to read):
kino inspect <spec> --real— note per-beatstart/endand each word's timeskino frames <mp4> --around <t>on every speech-locked beat — Read the sheet- Drive UI off
env.words, not fixed clocks — pipeline steps, capability tiles, counters that name spoken nouns should light when that word starts (fallback schedule only for mock/empty words) - Retune
triggers/ KEY_MS / clear thresholds from real times; rebuild (VO is content-hash cached — re-render is the cost, not re-TTS).atWord-anchored triggers/keyframes need no retune — they re-resolve against each build's VO; only hand-placedatseconds drift. - Re-check the loop seam on the encoded mp4 (see below)
Copy/VO lock: on-screen labels that enumerate steps must use the same nouns the VO speaks
(e.g. chip compose + VO "motion" = 🟠). Align chip text to VO or VO to chips before ship.
Seamless loops (hero reels)
When the brief is a looping site/hero video (first frame ≡ last frame):
- Own the background in every motion graphic — paint a full-bleed
.bgas the first layer. Brand presets likemesh/auroraanimate off the global frame counter; occlude them. Animated grounds are OK if life is gated byedge = sin(progress·π)(0 at beat start/end) so seam frames match — seespeech-synced-ui→ Seam-safe animated grounds. - Set
"seamlessLoop": true(+ prefer"film": 0). Validate warns on structure; post-build compares first/last RGB (warn only). Build also holds the last video frame to cover AAC audio pad — without that, players flash black for ~1–2 frames after the video track ends even when the true last picture is the ready poster. - Motion→motion dissolves automatically (~0.5s): outgoing beat holds through the VO gap,
incoming fades in. First beat stays opaque (no fade-from-black).
transitionon amotionbeat defaults to"dissolve"(this handoff) — leave it unset for a seamless loop's crossfade. Setting"cut"explicitly abuts with no crossfade instead, which is right for anappopener so nothing fades the cold open, but would break the seam here. - Design beat 0 t=0 and the final beat's end as the same ready poster (empty field, solid caret,
native scale S=1, no CTA). Cameras: soft mid-beat breath only — native at every beat edge
so cuts don't zoom-pop (
speech-synced-ui→ Camera). Prove withkino still --at 0vs settle end on harnesses (lossless PNG AE=0), then trust post-build seam / PSNR on the mp4. - H.264 lies about AE. Encoded first/last can differ by millions of AE from compression noise while looking identical. Gate with PSNR / RMSE / fuzz, not raw AE: PSNR ≳ 40 dB ≈ seamless.
env.progressnever reaches exactly1.0(max ≈(frames-1)/frames). End-of-beat clears / seam caret solid → use thresholds likeprogress > 0.95, neverprogress === 1.
Worked example: projects/kino-meta/ (prompt → spec → build → settle loop). Typed-UI / camera /
handoff detail: speech-synced-ui.
- Check copy for cross-beat redundancy before the first preview: a
motionbeat's on-screen label/dial text and the VO caption for that beat (or the one next to it) can end up saying the same thing twice (a timer graphic labelled "START TO FINISH" under a caption reading "start to finish, about twenty minutes"). Read the full beat list — spoken lines + anytexts/motion labels — start to finish, script only, before building the storyboard. Also check VO nouns vs on-screen chips. Same-frame counts too: on a typed beat the surface already paints the VO (env.words), so a foot/kicker label repeating that sentence duplicates it in one frame — give the label artifact meta instead (filename, line count), not the spoken claim. - Target the middle of your runtime range, not the floor: if
kino inspect's mock estimate lands at or below your minimum, don't assume the real VO will pace it out to a comfortable length — pad now (a beat, a slightly longer line, more breathing room on the hook/payoff) rather than shipping the edge and calling it a known weakness after the real build.
Stock stills (Pexels photos)
When a beat needs a photograph (lifestyle plate, texture, environment) and the brand has no
asset — same key as video, separate command:
kino photos "coffee desk morning light" lists portrait stills (size, author, alt + local thumb),
then kino photos "coffee desk morning light" --get 2 --project <name> → assets/pexels/<id>.jpg.
Screen the local thumb before --get (thumb: $TMPDIR/kino-pexels-photo-thumbs/<id>.jpg — Read
it). Reference like any still: "source": "pexels/<id>.jpg". Prefer real product screenshots when
they exist. Needs PEXELS_API_KEY.
Generated stills (image gen)
When stock photos still won't fit (hero illustration, product mock, logo variant) — use image gen
if the session permits it (image-gen skill / host image tool available and the user hasn't
forbidden generated assets). Save into assets/gen/…. Don't invent UI chrome that misrepresents
the app. Skip when the tool isn't available or the brand bans AI art.
Stock b-roll (Pexels video)
When a beat needs real-world footage the brand assets can't provide — lifestyle shots,
environments, hands-on-phone, city texture — pull licensed stock video:
kino pexels "city commute at night" lists portrait clips (duration, size, author, thumbnail URL),
then kino pexels "city commute at night" --get 2 --project <name> downloads into assets/pexels/<id>.mp4.
Screen the local thumb before downloading: search prints thumb: /tmp/kino-pexels-thumbs/<id>.jpg
— Read that file and reject on composition/mood there. Don't curl the remote URL by hand.
Downloading full clips just to preview burns bandwidth for candidates you were never going to use.
Only --get the ones you'd plausibly cut in.
Reference it from a video segment like any asset ("source": "pexels/<id>.mp4" — .mp4 assets play
with the same shots/transitions as stills). Prefer real product footage when it exists; match the
clip's duration to the beat's VO length (durations are listed). Needs PEXELS_API_KEY (free — pexels.com/api).
Caption legibility over footage is not optional: stock/photographic clips have uncontrolled
luminance, so before shipping a spec with video (or busy screenshot) cut-ins, make sure the brand
sets captionStyle.background (the translucent lower-third backplate, appOnly by default) — ink
captions straight on dark footage disappear. Same check for kickers: pick a kicker color whose
brand chip contrasts with the footage (preview with kino still --segment <n> before a real build).
The backplate helps average luminance, not a bright subject sitting right under the text — a white
caption over a white shirt/wall/sky can still fail even with the plate on, if the plate's opacity is
low or the caption sits squarely on the brightest part of the frame. Check the specific region behind
the caption, not just "is there a backplate": if it's still weak, reposition the caption off the bright
area (captionKeyframes) rather than only raising opacity.
No repo example currently sequences real Pexels footage end-to-end across a full spec — don't burn
time searching projects/*/specs/ for one; build the beat sequencing straight from this schema.
Search caches local thumbs under $TMPDIR/kino-pexels-thumbs/<id>.jpg — Read those paths to
screen composition/mood (don't curl the remote URL by hand).
Sound (music + SFX)
Every production-ready trailer needs a ducked music bed. Do not scrape Mixkit / Pixabay / Bensound / random CDNs — they 403, return empty bodies, or waste the run.
kino music # library beds (ships empty) + short-form Freesound query ideas
kino music "soft ambient pad loop" # Freesound CC0 search (needs FREESOUND_API_KEY)
kino music "soft ambient pad loop" --get 2 --project <name>
# in the spec (short-form: quiet bed, hard duck — VO wins on TikTok/Reels/Shorts):
"music": { "src": "music/bed.mp3", "volume": 0.12, "duck": 0.04, "fadeOutSec": 2 }
# SFX optional — omit by default, and assets-lib/sfx/ ships empty (add your own CC0 clip first).
# Soft pop/click only when a beat earns it (not every cut), referenced by project asset path:
# "sfx": [ { "src": "sfx/click.mp3", "at": 10.1, "volume": 0.25 } ]
- Neither library ships clips: music and SFX bare ids resolve from CC0 files you drop into
assets-lib/music//assets-lib/sfx/. Default route is a project asset path (music/bed.mp3,sfx/<name>.mp3) — sourced viakino musicFreesound search or your own. - Freesound search is CC0 + 15–90s by default (fits a 15–30s cut). Catalog skews ambient/SFX — good beds, not chart songs. Platform trending audio is not pullable (copyright).
- Short-form taste: sparse bed under VO; no default cut whoosh — silent cuts + ducked music
are enough. Skip
sfxunless a reveal/CTA earns a soft pop/click. Avoid loud drums fighting captions. - Ride the bed by hand only where ducking can't hear:
music.keyframes([{ at, params: { volume }, ease? }], absolute timeline seconds) sets the bed's level over time — a drop on a hard cut, a swell into the CTA.volumeis the implicit t=0 key, and auto-ducking still applies on top, so a keyframe to0is a gate the VO cannot reopen.musicalso takes an array for stacked beds (drone + pulse); they SUM, so split the budget. - One sample, several sounds:
sfx[].pan(-1…1, constant-power, +3 dB at a hard pan — dropvolumeto match) andsfx[].rate(varispeed — pitch and length move together; transients only).voVolume(top level) trims the whole VO when it sits too far in front of the bed. Full field reference + traps: docs/audio.md. - Put an effect on its BEAT, not on the timeline:
segments[].sfxtakesat(seconds from that beat's start) oratWord(a spoken word, same anchor motion keyframes use), plusoffset(seconds,atWordonly) to trim the landing — a hit a few frames before a word reads as having caused it, a few after as dubbed. Both forms ride real TTS with no retune, which top-levelsfx[].atdoes not:kino retunerewrites motiontriggers/backgroundTriggersand neversfx. Keep top-levelsfxfor hits that belong to the timeline — a music downbeat, a bookend, a tail. - Place timeline SFX after the real VO exists (when used):
kino build --tts→kino inspect --realand/orkino audio-markers→ setsfx[].at→ rebuild (VO cached). Guessingatmid-word is not shipping — or anchor withatWordon the beat and skip this loop entirely. - Music-driven pieces cut on the beat:
kino sync <spec> --offset autodetects the bed's beat grid, writesmusic.startSec(loudest on-grid stretch, opens on a hit) and retimes the visual beats'durs so every cut lands on a bar (--grain beatfor faster cutting;--dry-runfirst). VO beats keep their spoken length. See docs/audio.md § Beat-syncing cuts.
Hard rules (the CLI enforces these — don't fight them)
- HeyGen looks must be Avatar-IV photo-avatars — list valid ones with
kino avatars(add--gender male|femaleto narrow). BrandlookAliasesmap a friendly name → look id. Forhedra/replicate, setbrand.avatarImage(a portrait file) instead — those engines lip-sync a source image, not a hosted id. - Voices (mandatory search): Always run
kino voicesbefore setting"voice"orbrand.defaultVoice— search the catalog for the most appropriate match to the brand's Tone/Voice (register, energy, age, accent) and, when a presenter is used, the avatar's gender/age. Add--gender male|femaleto narrow. Never reuse the same default voice across brands or pick from memory without searching. Set the chosen id per spec ("voice") or per brand (defaultVoice+voiceAliasesfor friendly names). Ifkino voices401s (a scoped key withoutvoices_read), these premade ElevenLabs voices work on every account — still pick the most appropriate by character:21m00Tcm4TlvDq8ikWAMRachel (calm narrative F) ·AZnzlk1XvdvUeBnXmlldDomi (confident, punchy F) ·EXAVITQu4vr4xnSDxMaLSarah (soft, warm F) ·ErXwobaYiN019PkySvjVAntoni (warm, easy M) ·TxGEqnHWrfWFTfGW9XjXJosh (deep, serious M) ·pNInz6obpgDQGcFmaJgBAdam (broadcast M). - Expressive VO (audio tags): default TTS is
"voiceModel": "eleven_v3". Direct the read inline in segment text with bracketed tags —[excited],[whispers],[sighs],[laughs],[curious],[short pause]. Tags are stripped from word-synced captions automatically. Use like emphasis: 1-2 tags per spec where the copy earns them (a hook, a reveal), not on every beat. ⚠ Tags only work oneleven_v3. Oneleven_multilingual_v2the model reads them aloud ("short pause", "softly", …). If the spec pins v2 for metronome-critical / speech-synced timing, use punctuation for pauses (…/.) and drop bracket tags entirely. Faceless only for now: with an avatar provider the tagged text also reaches lip-sync, untested. - Timing comes from the generated VO, not your guesses — don't put timestamps in the spec.
- Recorded VO instead of TTS: set segment
voFile(project audio asset) — the file is the beat verbatim; word timings come from STT (Scribe with the ElevenLabs key, else local whisper.cpp). Keeptextmatching the recording; STT normalizes tokens ("thirty"→"30"), soatWordanchors bind to the transcribed words (a miss fails the build listing them). See docs/audio.md § Imported real voiceover. --ttsis the only flag that spends. A plainkino build <spec>is silent, FULL quality and $0 — so a silent cut is a real deliverable, not just a preview. Add--draftfor a faster 720p pass while iterating. Reach for--ttsonly when the piece actually needs a voiceover, or to lock speech-synced timing against real words; VO+avatar are content-cached, so caption edits don't re-bill, and--tts --no-avatarspends ElevenLabs only (no avatar credits).- Suspect a rendering bug (not a spec mistake)? Stop and say so before patching render source.
The render pipeline (
src/render/**) is shared across every brand and spec — a fix there is correct for everyone or wrong for everyone, unlike a spec/brand tweak scoped to the one video you're making. Report what you saw (still/frame + what's wrong) and the suspected file/line, and confirm before editing it. Don't reason your way to "this falls within scope" solo on a task that was to produce a video, not patch the renderer.
Adversarial visual critique
Layout QA is a separate skill — read and follow adversarial-critique after storyboard (and after
real-build frames when timing can shift layout). Do not inline a self-check of storyboard.png instead.
Analysing reference videos (research only)
Use kino transcribe <video> / kino scan <video> ONLY to study external reference clips
(competitors, trending videos, downloaded reference footage). They transcribe speech to timestamped
text and pull frames so you can see what's said and shown.
Never use them on our own rendered output (we already have word timings from TTS — use kino inspect), and never inside the build pipeline. See reference.md for flags.
Related
The Spec schema above is the common-case subset — for a field not covered here (masks/effects,
layers, keyframes/triggers, transitions, post FX, brand.md frontmatter, project.json), check
docs/spec-reference.md before guessing at a shape or inventing a field name. docs/cli-reference.md
has every command and flag, including ones this skill doesn't call out by name.
docs/spec-reference.md— full JSON spec schema,brand.md,project.jsondocs/cli-reference.md— everykinocommand + flagreference.md— command flags, brand config, troubleshooting (this skill's own companion)skills/ad-voice— segmenttext/caption/CTA copyskills/motion-design— visual craft for motion graphicsskills/speech-synced-ui— VO-locked typed/typing UIskills/importing-footage— long recordings → beat map + clip windowsskills/adversarial-critique— layout QA gate before shipping