@shotstack/cli 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ # Ingesting local files (Ingest API)
2
+
3
+ The `shotstack ingest` commands host your own media on Shotstack so it can be
4
+ referenced from an Edit. This is the **Ingest API** — a separate service from
5
+ the Edit (render) API, living at `https://api.shotstack.io/ingest/{env}`. The
6
+ CLI targets it automatically; the same `SHOTSTACK_API_KEY` works for both.
7
+
8
+ Use it when your footage, audio, images, fonts, or subtitle files live on your
9
+ machine (or behind a URL) rather than already being on a public CDN. Sources
10
+ are stored until you delete them.
11
+
12
+ ## The one rule that matters
13
+
14
+ **An Edit can only reference media by URL.** A local path in an `asset.src`
15
+ will not render. So the flow is always:
16
+
17
+ ```
18
+ local file ──▶ shotstack ingest upload ──▶ hosted "source" URL ──▶ Edit asset.src ──▶ shotstack render
19
+ ```
20
+
21
+ `shotstack ingest upload <file>` performs the two-step Ingest dance for you in
22
+ one command: it asks the API for a pre-signed URL, then streams your file's
23
+ bytes straight to storage.
24
+
25
+ ## Get the hosted URL: use `--watch`
26
+
27
+ The bare command returns only a source **id** — the hosted URL does not exist
28
+ until ingestion finishes. Pass `--watch` to poll until the source is `ready`
29
+ and print its URL:
30
+
31
+ ```sh
32
+ shotstack ingest upload ./intro.mp4 --watch --output json
33
+ # → {"id":"zzytey4v-...","status":"importing"}
34
+ # → {"id":"zzytey4v-...","status":"ready","source":"https://shotstack-ingest-api-v1-sources.s3.ap-southeast-2.amazonaws.com/.../source.mp4","width":1920,"height":1080,"duration":12.4}
35
+ ```
36
+
37
+ The `source` field is the URL to drop into your Edit. In text mode the final
38
+ line is just `ready <url>`, easy to capture in a shell variable:
39
+
40
+ ```sh
41
+ SRC=$(shotstack ingest upload ./intro.mp4 --watch --output json | tail -n1 | jq -r .source)
42
+ ```
43
+
44
+ Without `--watch`, capture the id and poll it later:
45
+
46
+ ```sh
47
+ ID=$(shotstack ingest upload ./intro.mp4 --output json | jq -r .id)
48
+ shotstack ingest status "$ID" --watch --output json
49
+ ```
50
+
51
+ ## Worked example: upload then render
52
+
53
+ ```sh
54
+ # 1. Upload a local clip and a local logo, capturing the hosted URLs.
55
+ VIDEO=$(shotstack ingest upload ./clip.mp4 --watch --output json | tail -n1 | jq -r .source)
56
+ LOGO=$(shotstack ingest upload ./logo.png --watch --output json | tail -n1 | jq -r .source)
57
+
58
+ # 2. Reference them in an Edit (asset.src), then render.
59
+ cat > edit.json <<JSON
60
+ {
61
+ "timeline": {
62
+ "tracks": [
63
+ { "clips": [ { "asset": { "type": "image", "src": "$LOGO" }, "start": 0, "length": 5 } ] },
64
+ { "clips": [ { "asset": { "type": "video", "src": "$VIDEO" }, "start": 0, "length": 5 } ] }
65
+ ]
66
+ },
67
+ "output": { "format": "mp4", "size": { "width": 1920, "height": 1080 } }
68
+ }
69
+ JSON
70
+
71
+ shotstack render edit.json --watch
72
+
73
+ or
74
+
75
+ shotstack studio edit.json
76
+ ```
77
+
78
+ Remember `tracks[0]` is the **top** layer (see `shared/agent-core.md`), so the
79
+ logo track sits above the video.
80
+
81
+ ## Fetch a remote URL instead of uploading
82
+
83
+ If the media is already at a URL but you want Shotstack to host its own copy
84
+ (stable, no hot-linking), use `fetch`:
85
+
86
+ ```sh
87
+ shotstack ingest fetch https://example.com/big-video.mp4 --watch --output json
88
+ ```
89
+
90
+ This only works for fully validated urls. YouTube and similar video hosting
91
+ platforms are not supported.
92
+
93
+ ## Manage stored sources
94
+
95
+ ```sh
96
+ shotstack ingest list --output json # every source you've ingested
97
+ shotstack ingest status <id> --output json # one source's status + hosted URL
98
+ shotstack ingest delete <id> # remove a source from storage
99
+ ```
100
+
101
+ ## Status values
102
+
103
+ `queued` → `importing` → `ready` (terminal). A failed import ends in `failed`
104
+ (exit code 1 under `--watch`). `deleted` and `overwritten` are also terminal.
105
+
106
+ ## Supported files
107
+
108
+ Video, image, audio, font, and subtitle (`.srt`/`.vtt`) files. The CLI infers
109
+ a `Content-Type` from the file extension; uncommon extensions still upload
110
+ (the API sniffs the content) but naming files with a correct extension is
111
+ safest.
112
+
113
+ ## Environments
114
+
115
+ Sources are ingested into the environment you target (`--env stage` or `--env
116
+ v1`, default `v1`), mirroring `render`. Override the base URL for local testing
117
+ with `SHOTSTACK_INGEST_BASE_URL`.
118
+
119
+ ```sh
120
+ shotstack ingest upload ./clip.mp4 --watch --env stage
121
+ ```
@@ -0,0 +1,151 @@
1
+ # Motion language — the house vocabulary
2
+
3
+ A small, closed set of timing, easing and stagger values that **every** animated clip should share. Composing motion from these tokens — instead of inventing a new easing and duration per clip — is what makes a multi-clip edit feel like one production instead of eight unrelated effects. Reach for these before hardcoding a `duration`, an `ease`, or a delay.
4
+
5
+ > The values are video-paced for Shotstack's **second-based** timeline (most UI-motion guidance is frame-based). The GSAP/CSS easings below stand in for spring physics, which the render engine doesn't expose.
6
+
7
+ ## The tokens
8
+
9
+ ### Duration scale (seconds)
10
+
11
+ | Token | Seconds | Use for |
12
+ |---|---|---|
13
+ | `instant` | 0.2 | micro shifts, near-imperceptible feedback |
14
+ | `fast` | 0.33 | **exits**, small moves |
15
+ | `base` | 0.6 | **the default entrance** — ~80% of reveals |
16
+ | `slow` | 0.8 | large entrances, hero moves |
17
+ | `slower` | 1.0 | full-scene transitions |
18
+ | `hold` | 1.5 | minimum settled hold before a clip ends |
19
+
20
+ Most entrances are `base` (0.6 s). Exits are deliberately faster than entrances — get out of the way faster than you came in.
21
+
22
+ ### Easing — one house curve, no overshoot by default
23
+
24
+ | Where | Entrance / settle | Exit |
25
+ |---|---|---|
26
+ | **html5 GSAP** | `ease: 'power3.out'` | `ease: 'power2.in'` |
27
+ | **html5 CSS** | `cubic-bezier(0.16, 1, 0.3, 1)` | `cubic-bezier(0.3, 0, 0.8, 0.2)` |
28
+
29
+ `power3.out` / `cubic-bezier(0.16,1,0.3,1)` is the **house ease** — a confident "fast then settle" that reads like a heavily-damped spring with **no bounce**. Use it for anything the eye tracks (position, scale, blur). Never use raw `linear` / `'none'` for motion the eye follows; `linear` is only correct for a continuous drift (grain, marquee, a constant-speed ticker).
30
+
31
+ **Overshoot is opt-in, not default.** A gentle `back.out(1.4)` (GSAP) is reserved for **one** "hero" element per scene where a pop is intended — a logo sting, a headline punch. Calm is the default; energetic is fine when it *earns* it. Never sprinkle bounce across every element.
32
+
33
+ ### Stagger — one value
34
+
35
+ `0.13 s` (≈ 4 frames at 30 fps) between siblings in any cascade — words, list items, grouped reveals. One value everywhere; present but never frantic. In GSAP: `stagger: 0.13`. In CSS: `animation-delay` steps of `0.13s` (`0s, 0.13s, 0.26s, …`).
36
+
37
+ ### Travel & restraint
38
+
39
+ - **Travel small:** translate **12–24 px**, not 80. The eye should feel the motion, not be dragged by it. Blur reveals: `10 px → 0`. Scale reveals: `0.92 → 1` (below ~0.85 is dramatic-zoom territory — avoid unless intended).
40
+ - **One focal element per moment.** If two things compete, stagger them. Don't animate everything at once.
41
+ - **Let it settle.** After a reveal lands, hold it (a trailing `gsap.to({}, { duration: <hold> })` or enough clip `length`) before the clip ends. Never cut a reveal the instant it arrives.
42
+
43
+ ## The three motion layers
44
+
45
+ Shotstack exposes motion at three levels of control. Use the tokens at whichever level fits; reach down a level only when you need more control.
46
+
47
+ | Layer | Control | Where the tokens apply |
48
+ |---|---|---|
49
+ | 1 — **clip `transition` / `effect`** | coarse (whole-clip in/out; only `Slow`/`Fast` variants) | pick `fade`, `slide*`, `zoom`; `Slow` ≈ `slow`, default ≈ `base`, `Fast` ≈ `fast` |
50
+ | 2 — **`rich-text` `animation`** | medium (`preset` + `duration` + `style` + `direction`) | set `duration` from the scale; `style:"word"` gives the staggered cascade |
51
+ | 3 — **`html5` GSAP / CSS** | full (any property, any easing) | the full vocabulary below — durations, the house ease, `0.13` stagger |
52
+
53
+ ### Layer 2 — rich-text animation, tokenised
54
+
55
+ `animation.preset` maps onto the choreography names; set `duration` from the scale:
56
+
57
+ | Intent | `rich-text` animation |
58
+ |---|---|
59
+ | fade in | `{ "preset": "fadeIn", "duration": 0.6 }` |
60
+ | rise + fade (the workhorse) | `{ "preset": "ascend", "duration": 0.6, "direction": "up" }` |
61
+ | slide in | `{ "preset": "slideIn", "duration": 0.6, "direction": "up" }` |
62
+ | word-by-word cascade | `{ "preset": "shift", "duration": 0.6, "style": "word", "direction": "up" }` |
63
+ | typewriter | `{ "preset": "typewriter", "duration": 0.8, "style": "character" }` |
64
+
65
+ Schema constraints (`@shotstack/schemas` → `RichTextAnimation`): `preset` ∈ `fadeIn · slideIn · typewriter · ascend · shift · movingLetters`; `duration` 0.1–30 s; **`style` (`word`/`character`) applies only to `typewriter` and `shift`** (ignored on the others); `direction` is **required** for `slideIn`, `ascend`, `shift`, `movingLetters`. Rich-text `animation` is **entrance-only** — exits use the clip `transition.out` (Layer 1). Easing on the presets is engine-fixed; for full easing control, drop to html5 (Layer 3).
66
+
67
+ ## Choreography recipes (html5 / GSAP)
68
+
69
+ Pure, seekable GSAP — obey [`html5.md`](html5.md) (no `setTimeout`/`rAF`/`Date.now`/`gsap.call()`; size the clip to the content). Each recipe is a named choreography helper you compose rather than reinventing.
70
+
71
+ ```js
72
+ // entryFadeRise — THE workhorse: rise 12px + fade, house ease, base duration
73
+ gsap.from('.el', { opacity: 0, y: 12, duration: 0.6, ease: 'power3.out' });
74
+
75
+ // entryFade — presence only (overlays, avatars)
76
+ gsap.from('.el', { opacity: 0, duration: 0.6, ease: 'power3.out' });
77
+
78
+ // entryScale — calm scale-up
79
+ gsap.from('.el', { opacity: 0, scale: 0.92, duration: 0.6, ease: 'power3.out' });
80
+
81
+ // blurReveal — opacity + blur + rise off ONE tween (one progress, many channels)
82
+ gsap.from('.el', { opacity: 0, y: 16, filter: 'blur(10px)', duration: 0.6, ease: 'power3.out' });
83
+
84
+ // wordStagger — each word rises in sequence (wrap words in <span>)
85
+ gsap.from('.word', { opacity: 0, yPercent: 120, duration: 0.6, ease: 'power3.out', stagger: 0.13 });
86
+
87
+ // heroReveal — the ONE place a pop is allowed (≤1 per scene)
88
+ gsap.from('.hero', { opacity: 0, y: 16, scale: 0.97, duration: 0.8, ease: 'back.out(1.4)' });
89
+
90
+ // exitFadeFall — fade + drop, faster, ease-in (an exit doesn't settle)
91
+ gsap.to('.el', { opacity: 0, y: 8, duration: 0.33, ease: 'power2.in' });
92
+ ```
93
+
94
+ **Entrance → hold → exit, on one timeline:**
95
+
96
+ ```js
97
+ const tl = gsap.timeline();
98
+ tl.from('.el', { opacity: 0, y: 12, duration: 0.6, ease: 'power3.out' }) // in (base)
99
+ .to({}, { duration: 1.5 }) // hold
100
+ .to('.el', { opacity: 0, y: 8, duration: 0.33, ease: 'power2.in' }); // out (fast)
101
+ ```
102
+
103
+ ### CSS equivalents (no JS)
104
+
105
+ ```css
106
+ /* house ease entrance */
107
+ .el { animation: rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
108
+ @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
109
+
110
+ /* staggered siblings — 0.13s steps */
111
+ .w:nth-child(1){animation-delay:0s}.w:nth-child(2){animation-delay:.13s}.w:nth-child(3){animation-delay:.26s}
112
+ ```
113
+
114
+ ## Brand kit — set the palette once, re-skin everything
115
+
116
+ Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]` fields and reference the tokens in every clip's `css`/`html` — one edit re-skins the whole video — brand tokens applied once, via Shotstack merge:
117
+
118
+ ```json
119
+ "merge": [
120
+ { "find": "ink", "replace": "#141414" },
121
+ { "find": "accent", "replace": "#D96B82" },
122
+ { "find": "bg", "replace": "#08080A" },
123
+ { "find": "font", "replace": "Clash Display, system-ui, sans-serif" }
124
+ ]
125
+ ```
126
+
127
+ ```css
128
+ .title { color: {{ink}}; font-family: {{font}}; }
129
+ .cta { background: {{accent}}; }
130
+ ```
131
+
132
+ `merge` find/replace runs over the whole edit, including `html`/`css` strings, so `{{accent}}` resolves everywhere. Keep one accent and use it sparingly — a headline word, a number, a CTA, one glow — everything else neutral. **Colour is earned, never sprinkled.**
133
+
134
+ ## Motion checklist
135
+
136
+ Before rendering anything animated:
137
+
138
+ 1. **Durations from the scale?** (`0.6` in, `0.33` out — not `0.45`, `0.7`, `1.2`.)
139
+ 2. **House ease, not linear?** `power3.out` / `cubic-bezier(0.16,1,0.3,1)` for tracked motion.
140
+ 3. **Overshoot only on one hero element**, if at all.
141
+ 4. **Stagger is `0.13`**, applied via `stagger` / `animation-delay`.
142
+ 5. **Travel 12–24 px; scale ≥ 0.92.** Restraint over flash.
143
+ 6. **One focal element per moment;** competing elements staggered.
144
+ 7. **It holds before it cuts** (settled `hold`, not an instant exit).
145
+ 8. **Brand colours via `merge[]`**, accent used sparingly.
146
+
147
+ ## See also
148
+
149
+ - [`html5-snippets.md`](html5-snippets.md) — ready-made clips that already follow these tokens
150
+ - [`html5.md`](html5.md) — the html5 sandbox rules these obey (seekable-only, sizing, fonts)
151
+ - `shared/agent-core.md` → "Motion language" — the compact token summary (shared with the MCP server)
@@ -0,0 +1,64 @@
1
+ # Positioning & coordinates
2
+
3
+ How `position`, `offset`, `width`/`height`, `scale`, and the transform fields place a clip in the frame. `shared/agent-core.md` carries the one-paragraph summary; this is the full reference.
4
+
5
+ ## The coordinate model
6
+
7
+ `position` picks one of nine **anchor points** in the frame; `offset` then nudges the clip from that anchor. **`offset` is a fraction of the output frame — not pixels, and not a centred −1..+1 grid:**
8
+
9
+ - `offset.x` positive → **right** (a fraction of frame width); `offset.y` positive → **up** (a fraction of frame height). Note `y` is *up*-positive, the opposite of typical screen coordinates.
10
+ - `position:"center"` with `offset:{ "x":0, "y":0 }` sits dead centre. `offset:{ "x":0.5 }` moves half a frame-width right; `offset:{ "x":-1 }` moves a full frame-width left — off-screen.
11
+ - The field accepts ±10; anything past ±1 already moves the clip partly or fully out of frame (no error).
12
+
13
+ ## Anchor points
14
+
15
+ `position` pins the clip's centre to the named point; `offset` shifts from there.
16
+
17
+ | | left edge | centre | right edge |
18
+ |---|---|---|---|
19
+ | **top edge** | `topLeft` | `top` | `topRight` |
20
+ | **middle** | `left` | `center` *(default)* | `right` |
21
+ | **bottom edge** | `bottomLeft` | `bottom` | `bottomRight` |
22
+
23
+ Example: `position:"topLeft"`, `offset:{ "x":0.05, "y":-0.05 }` → a 5% margin in from the top-left corner.
24
+
25
+ ## Offset in numbers
26
+
27
+ On a 1920×1080 frame: `offset.x:0.1` → 192 px right; `offset.y:0.1` → 108 px up; `offset.x:-0.5` → 960 px left (half the frame). Because the shift scales with the frame, the same `offset` looks the same at any resolution.
28
+
29
+ ## Sizing & fit (image / video)
30
+
31
+ Give a clip a `width`/`height` (pixels) to define a box; `fit` decides how the media fills it:
32
+
33
+ | `fit` | behaviour |
34
+ |---|---|
35
+ | `crop` *(default)* | fill the box, keep aspect ratio, crop the overflow |
36
+ | `contain` | fit entirely inside the box, keep aspect ratio, letterbox the gaps |
37
+ | `cover` | stretch to the box exactly — **distorts** if the aspect ratio differs |
38
+ | `none` | no scaling; crop to the box |
39
+
40
+ `scale` then multiplies the result (`1` = the fit result, `0.5` = half). `scale` is uniform on both axes — for a deliberate non-uniform stretch, use `fit:"cover"` with a `width`/`height`.
41
+
42
+ ## Sizing text
43
+
44
+ Give a `rich-text` clip a `width`/`height` to set its text box: text wraps to that width, and `align` (`horizontal`: `left`/`center`/`right`; `vertical`: `top`/`middle`/`bottom`) positions the text inside it. **Without a `width`/`height`, the box defaults to the whole frame** — so `align` centres the text across the entire output and `position`/`offset` then move that full-frame block, which is rarely what you intend. Set `width`/`height` on any text you want placed precisely.
45
+
46
+ ## Transforms
47
+
48
+ - `scale` — uniform multiplier (above).
49
+ - `transform.rotate.angle` — degrees clockwise; rotates around the clip's own centre, so it spins in place wherever you positioned it.
50
+ - `transform.skew.x` / `.y` — shear.
51
+ - `transform.flip.horizontal` / `.vertical` — mirror.
52
+ - `opacity` — `0`–`1`.
53
+
54
+ ## Order of operations
55
+
56
+ `fit` (scale media into the box) → `position` (anchor) → `offset` (nudge) → `rotate` / `skew` → `scale`. Worth remembering when a result surprises you: scaling is applied last, around the positioned anchor.
57
+
58
+ ## Common mistakes
59
+
60
+ 1. **`offset.y` sign.** Positive `offset.y` moves **up**; use a negative value to move down.
61
+ 2. **Expecting a centred −1..+1 grid.** There isn't one — `offset` is a fraction of the frame measured from the `position` anchor. `offset.x:-1` is a whole frame-width left, not "the left edge".
62
+ 3. **`fit:"cover"` distorts.** It stretches to the box. Use `fit:"crop"` to fill while keeping aspect ratio.
63
+ 4. **Unsized text.** A `rich-text` with no `width`/`height` fills the frame; set them to place text precisely.
64
+ 5. **Offsets off-screen.** Past ±1 the clip leaves the frame silently — no error.
@@ -1,6 +1,6 @@
1
1
  # Troubleshooting
2
2
 
3
- Common errors and the fix for each. If your error isn't here, run `shotstack feedback` to file a pre-filled GitHub issue with the render UUID attached — engineers can find server-side state in seconds.
3
+ Common errors and the fix for each. **Catch most of them before rendering:** `shotstack validate <file>` checks an Edit against the schema and flags same-track overlaps, unloaded fonts and non-public URLs offline — no API key, no credits. If your error isn't here, run `shotstack feedback` to file a pre-filled GitHub issue with the render UUID attached — engineers can find server-side state in seconds.
4
4
 
5
5
  ## Contents
6
6
 
@@ -9,6 +9,7 @@ Common errors and the fix for each. If your error isn't here, run `shotstack fee
9
9
  - Video looks stretched / squished after rendering
10
10
  - "Font not found"
11
11
  - Captions cover the whole frame
12
+ - Clips flicker / "clips overlap" on one track
12
13
  - Timeline renders but layers are wrong
13
14
  - "Invalid asset URL"
14
15
  - "Render failed" with no other detail
@@ -63,6 +64,12 @@ A `rich-caption` clip without `width`, `height`, and `fit: "none"` defaults to f
63
64
 
64
65
  **Fix:** use one of the five named presets in `references/caption.md` (Nico, Kai, Kapow, Lovely Little Lychee, Rizz) which include the right dimensions, or set `width`/`height`/`fit: "none"` explicitly on your clip.
65
66
 
67
+ ## Clips flicker / "clips overlap" on one track
68
+
69
+ Two clips on the **same track** have overlapping `start`/`length` ranges, so the engine can't decide which to show — they flicker. This is the most common structural mistake when several elements appear at once (e.g. a title, a price and a button on an end card all at `start: 8`).
70
+
71
+ **Fix:** put anything visible at the same time on its **own track**. Sequential clips (one ends, the next begins — chain them with `"start": "auto"`) can share a track. `shotstack validate <file>` pinpoints the offending track and clip indices offline. See `shared/agent-core.md` → "Don't overlap clips on the same track" for a before/after.
72
+
66
73
  ## Timeline renders but layers are wrong
67
74
 
68
75
  You assumed `tracks[0]` was the bottom layer.
@@ -0,0 +1,262 @@
1
+ # Shotstack Agent Core
2
+
3
+ Universal Edit JSON authoring conventions. Read this **before composing any Edit JSON**. The conventions agents most often get wrong are listed here once. The same file ships with both the Shotstack CLI skill and the Shotstack MCP server.
4
+
5
+ ## Before composing JSON: check the schema
6
+
7
+ Don't invent property names or enum values. The Shotstack schema is published — fetch one of these before composing JSON from scratch:
8
+
9
+ - <https://shotstack.io/docs/api/api.edit.json> — single-file OpenAPI Schema. Machine-validatable; load it once and validate locally instead of round-tripping the API.
10
+ - <https://shotstack.io/docs/api/> — interactive HTML reference. Fastest for human scanning.
11
+ - <https://shotstack.io/docs/guide/llms-full.txt> — single-file LLM-friendly version of the full guide + reference.
12
+ - <https://github.com/shotstack/oas-api-definition/tree/main/schemas> — raw OpenAPI YAML, source of truth.
13
+
14
+ CSS naming conventions (`alignment`, `vertical: "center"`) **do not** apply. The spec uses precise names that often differ from web/CSS instincts:
15
+
16
+ | You'd guess (wrong) | API uses (right) |
17
+ |---|---|
18
+ | `alignment` | `align` |
19
+ | `align: "center"` (string on rich-text asset) | `align: { "horizontal": "center", "vertical": "middle" }` (object) |
20
+ | `align.vertical: "center"` | `align.vertical: "middle"` |
21
+ | `font.name` | `font.family` |
22
+ | `duration` | `length` |
23
+ | `transitions: [...]` (array) | `transition: { in, out }` (object) |
24
+ | `fit: "cover"` (CSS instinct: scale+crop maintaining aspect) | `fit: "crop"` — Shotstack's `cover` STRETCHES without maintaining aspect ratio |
25
+
26
+ When the API rejects a property, the error message names the field — fix and retry. Don't guess twice.
27
+
28
+ ## Track ordering is REVERSED
29
+
30
+ `timeline.tracks` is an array. **The first element (`tracks[0]`) is the TOP layer; the last element is the BOTTOM layer.** This is opposite to most z-index conventions.
31
+
32
+ > "Tracks are layered on top of each other in the same order they are added to the array with the top most track layered over the top of those below it." — [Shotstack docs](https://shotstack.io/docs/guide/architecting-an-application/guidelines/)
33
+
34
+ Practical rule: **put captions, overlays, and titles in early tracks; put video/image backgrounds in later tracks.**
35
+
36
+ ```json
37
+ {
38
+ "timeline": {
39
+ "tracks": [
40
+ { "clips": [/* TOP — captions */] },
41
+ { "clips": [/* MIDDLE — title overlay */] },
42
+ { "clips": [/* BOTTOM — background video */] }
43
+ ]
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Smart clip strings
49
+
50
+ These string values are accepted in addition to numbers:
51
+
52
+ | Where | Value | Meaning |
53
+ |---|---|---|
54
+ | `clip.start` | `"auto"` | Start when the previous clip on the same track finishes |
55
+ | `clip.start` | `"alias://<name>"` | Inherit start from another clip with `alias: "<name>"` |
56
+ | `clip.length` | `"auto"` | Asset's natural duration. Use for foreground (video, voiceover, scene). |
57
+ | `clip.length` | `"end"` | Until timeline ends, capped at asset duration. Use for background (music, captions, watermark). |
58
+ | `clip.length` | `"alias://<name>"` | Inherit length from another clip |
59
+
60
+ `"end"` does NOT loop short audio — use a numeric `length` if you need precise control.
61
+
62
+ The `alias://` protocol is also used in `rich-caption` `src` to auto-transcribe a referenced audio/video clip — see `references/caption.md`.
63
+
64
+ ## Public HTTPS URLs only
65
+
66
+ All asset `src` URLs must be publicly accessible HTTPS. **No local file paths, no `data:` URIs, no signed URLs that expire mid-render.** The render workers fetch assets from the public internet.
67
+
68
+ For test renders without your own assets, use the placeholder library at <https://shotstack-assets.s3.amazonaws.com/> — see `references/asset-library.md`.
69
+
70
+ ## Don't overlap clips on the same track
71
+
72
+ Clips on the same track must not have overlapping `start`/`length` ranges — overlapping clips flicker because the engine can't decide which to display. **Anything visible at the same time goes on a separate track.** This is the most common structural mistake.
73
+
74
+ ```jsonc
75
+ // WRONG — three simultaneous end-card lines on ONE track (all overlap 8.0–11.0)
76
+ { "clips": [
77
+ { "asset": { "type": "rich-text", "text": "TITLE" }, "start": 8.0, "length": 3 },
78
+ { "asset": { "type": "rich-text", "text": "$395" }, "start": 8.0, "length": 3 },
79
+ { "asset": { "type": "rich-text", "text": "SHOP NOW" }, "start": 8.0, "length": 3 }
80
+ ] }
81
+
82
+ // RIGHT — one track each
83
+ "tracks": [
84
+ { "clips": [ { "asset": { "type": "rich-text", "text": "TITLE" }, "start": 8.0, "length": 3 } ] },
85
+ { "clips": [ { "asset": { "type": "rich-text", "text": "$395" }, "start": 8.0, "length": 3 } ] },
86
+ { "clips": [ { "asset": { "type": "rich-text", "text": "SHOP NOW" }, "start": 8.0, "length": 3 } ] }
87
+ ]
88
+ ```
89
+
90
+ Sequential clips (one finishes, the next starts) **can** share a track — `"start": "auto"` chains them. A cross-fade needs two tracks with a small time overlap and a `transition` on each.
91
+
92
+ **Validate before you render:** `shotstack validate <file>` catches same-track overlaps — plus unloaded fonts, non-public `src` URLs, and wrong property names/enums — offline, no API key, no credits.
93
+
94
+ ## Clip motion & rich-text fields (cheatsheet)
95
+
96
+ Compose from this rather than round-tripping the full schema — these are the values renders actually use. (`api.edit.json` stays authoritative; `shotstack validate` checks against it.)
97
+
98
+ **Clip-level (wraps any asset):**
99
+
100
+ | Field | Values |
101
+ |---|---|
102
+ | `fit` | `crop` (fill + crop, default — CSS `object-fit: cover`) · `contain` (letterbox) · `cover` (stretch, ignores aspect) · `none` |
103
+ | `position` | `center` `top` `bottom` `left` `right` `topLeft` `topRight` `bottomLeft` `bottomRight` |
104
+ | `offset` | `{ "x": <−1..1>, "y": <−1..1> }` — fraction of the canvas; **`y` positive = up** |
105
+ | `scale` / `opacity` | number (`1` = the fit result) / `0..1` |
106
+ | `effect` | Ken-Burns drift: `zoomIn` `zoomOut` `slideLeft` `slideRight` `slideUp` `slideDown` — each also `…Slow` / `…Fast` |
107
+ | `filter` | `blur` `boost` `contrast` `darken` `greyscale` `lighten` `muted` `negative` |
108
+ | `transition` | `{ "in": …, "out": … }` ↓ |
109
+
110
+ **`transition.in` / `.out`** — each also takes a `Slow`/`Fast` suffix (e.g. `fadeSlow`, `slideUpFast`):
111
+ `none` `fade` `reveal` `wipeLeft` `wipeRight` `slideLeft` `slideRight` `slideUp` `slideDown` `carouselLeft` `carouselRight` `carouselUp` `carouselDown` `shuffle*` (eight corners, e.g. `shuffleTopRight`) `zoom`.
112
+
113
+ **`rich-text` asset** — the styled-text workhorse (use instead of `text`/`title`):
114
+
115
+ ```json
116
+ {
117
+ "type": "rich-text",
118
+ "text": "UTOPIA",
119
+ "font": { "family": "<loaded-family>", "size": 160, "weight": "700", "color": "#141414", "opacity": 1 },
120
+ "style": { "letterSpacing": 6, "lineHeight": 0.95, "textTransform": "uppercase", "textDecoration": "none" },
121
+ "stroke": { "width": 3, "color": "#000000" },
122
+ "shadow": { "offsetX": 0, "offsetY": 6, "blur": 18, "color": "#000000", "opacity": 0.4 },
123
+ "background": { "color": "#ffffff", "opacity": 1, "borderRadius": 16 },
124
+ "align": { "horizontal": "center", "vertical": "middle" },
125
+ "animation": { "preset": "fadeIn", "duration": 0.6, "style": "word", "direction": "up" }
126
+ }
127
+ ```
128
+
129
+ `align.horizontal` = `left|center|right`; `align.vertical` = `top|middle|bottom` (**not** `center`). `animation.preset` = `fadeIn` `slideIn` `typewriter` `ascend` `shift` `movingLetters`; `animation.style` = `character|word`; `animation.direction` = `left|right|up|down`. Give the clip a `width`/`height` box so text wraps and aligns where you expect.
130
+
131
+ For motion beyond this (kinetic type, count-ups, shine sweeps, grain, pulsing CTAs) reach for `html5` — see [`references/html5-snippets.md`](../references/html5-snippets.md).
132
+
133
+ ## Motion language (house tokens)
134
+
135
+ Compose motion from a **closed set of tokens** so a multi-clip edit feels like one production, not eight unrelated effects. **Don't invent a new easing or duration per clip.** Full recipes (GSAP/CSS, choreography, brand kit) in [`references/motion.md`](../references/motion.md).
136
+
137
+ - **Durations (seconds):** `instant 0.2 · fast 0.33 · base 0.6 · slow 0.8 · slower 1.0 · hold 1.5`. Entrances default to **`base` (0.6)**; **exits are faster** (`fast`).
138
+ - **One house ease, no overshoot:** html5 GSAP `power3.out` (in) / `power2.in` (out); CSS `cubic-bezier(0.16, 1, 0.3, 1)`. Never raw `linear` for tracked motion. A gentle `back.out(1.4)` is reserved for **one** hero element per scene — calm is the default.
139
+ - **One stagger:** `0.13s` between siblings (GSAP `stagger: 0.13`; CSS `animation-delay` steps).
140
+ - **Restraint:** translate 12–24 px (not 80), scale ≥ 0.92, one focal element per moment, let a reveal settle (`hold`) before it cuts.
141
+ - **rich-text `animation`** (entrance-only — exits use the clip `transition.out`): set `duration` from the scale (e.g. `0.6`); `preset:"ascend"` + `direction:"up"` is the rise-and-fade workhorse; for a word cascade use `preset:"shift"` + `style:"word"` + `direction:"up"` (`style` works only on `typewriter`/`shift`; `direction` is required for `slideIn`/`ascend`/`shift`/`movingLetters`).
142
+ - **Brand once:** put palette/font in top-level `merge[]` (`{{ink}}`, `{{accent}}`, `{{font}}`) and reference them in every clip — one edit re-skins the whole video. Accent used sparingly.
143
+
144
+ ## Positioning & coordinates
145
+
146
+ `position` picks one of nine anchor points (`center` default; `top` `bottom` `left` `right` `topLeft` `topRight` `bottomLeft` `bottomRight`); `offset` nudges from there. **`offset` is a fraction of the output frame, not a centred −1..+1 grid:** `offset.x` positive → right (× frame width), `offset.y` positive → **up** (× frame height). Range is ±10; anything past ±1 pushes the clip off-frame.
147
+
148
+ Clip-level `width`/`height` (pixels) define a bounding box. For `image`/`video`, `fit` fills it — `crop` (keep aspect, crop overflow) · `contain` (letterbox) · `cover` (**stretch, distorts**) · `none`. For `rich-text`, that same clip box sets the text-wrap width and the area `align` positions within — **size text on the clip, not the asset.** Without `width`/`height` a clip fills the frame, so unsized text centres across the whole output. `scale` then multiplies the result (uniform on both axes).
149
+
150
+ Order of operations: fit → position → offset → rotate → scale. Full reference: [`references/positioning.md`](../references/positioning.md).
151
+
152
+ ## Output resolution
153
+
154
+ Pick `output.resolution` (preset) OR `output.size.width`+`output.size.height` (custom):
155
+
156
+ | Preset | Pixels @ fps |
157
+ |---|---|
158
+ | `preview` | 512×288 @ 15 |
159
+ | `mobile` | 640×360 @ 25 |
160
+ | `sd` | 1024×576 @ 25 |
161
+ | `hd` | 1280×720 @ 25 (default) |
162
+ | `1080` | 1920×1080 @ 25 |
163
+
164
+ Custom sizes must be divisible by 2.
165
+
166
+ ## Asset types
167
+
168
+ Use only the **current** asset types; the deprecated ones still parse but should not be used in new templates.
169
+
170
+ ### Current
171
+
172
+ | Type | Purpose |
173
+ |---|---|
174
+ | `video` | Video file (mp4, mov, webm). |
175
+ | `image` | Static image — `jpg`, `png`, `webp`, `gif`, `bmp`, `tiff`. |
176
+ | `audio` | Audio clip placed at a specific time on the timeline. |
177
+ | `rich-text` | Styled text overlay with full typography control. **Use this instead of `text`/`html`/`title`.** |
178
+ | `svg` | Vector graphics from raw SVG markup. See `references/svg.md`. |
179
+ | `html5` | Self-contained HTML/CSS/JS page rendered in an iframe (motion graphics, charts, animated overlays). Preloads gsap/d3/anime/lottie. See `references/html5.md`. **Never use the deprecated `html` asset.** |
180
+ | `rich-caption` | Word-level animated captions sourced from audio, video, or subtitle files. See `references/caption.md`. |
181
+ | `luma` | Luma matte for masking effects. |
182
+ | `image-to-video` | **AI**: animate a still image into a short video clip. Billed per generation. |
183
+ | `text-to-image` | **AI**: generate an image from a text prompt. Billed per generation. |
184
+ | `text-to-speech` | **AI**: generate speech from a text prompt. Billed per generation. |
185
+
186
+ `timeline.fonts[]` is a separate field for custom font URLs (not an asset type).
187
+
188
+ For background music, **use an `audio` asset on its own track** with `length: "end"`. Do NOT use `timeline.soundtrack` — it is deprecated. The audio asset path supports keyframes, custom timing, fades, and effects; soundtrack does not.
189
+
190
+ ### Deprecated — do not use
191
+
192
+ `text`, `title`, `caption`, `html`, `shape`. They still parse but produce inferior output. Replace with:
193
+
194
+ | If you'd use… | Use instead |
195
+ |---|---|
196
+ | `text` or `title` | `rich-text` |
197
+ | `caption` | `rich-caption` |
198
+ | `html` | `html5` (for motion graphics or animated overlays) or `rich-text` (for static styled text) |
199
+ | `shape` | `svg` with `<rect>`, `<circle>`, `<polygon>` etc. |
200
+ | `timeline.soundtrack` | `audio` asset on its own track with `length: "end"` |
201
+
202
+ ### AI-generated assets
203
+
204
+ `image-to-video`, `text-to-speech`, and `text-to-image` are billed per generation **even when invoked through the sandbox stage endpoint** (which is otherwise free). They are async — the render submits the AI job and waits. Renders containing AI assets take longer.
205
+
206
+ ## The design ladder — escalate from rich-text to html5
207
+
208
+ One overlay is a `rich-text` job. **Several videos "in different styles" is not** — making them all `rich-text` ships one look eight times. Match the asset to the ambition:
209
+
210
+ | Level | Asset | Use for |
211
+ |---|---|---|
212
+ | 1 — type & layout | `rich-text` | Titles, lower-thirds, kickers, captions, price/CTA pills. Fast and reliable; the right default for static styled text. |
213
+ | 2 — shapes | `svg` | Colour panels, rules, badges, frames, geometric accents behind or around type. |
214
+ | 3 — motion graphics | `html5` | Kinetic type, count-ups / price odometers, shine sweeps, animated gradients, film grain, masked reveals, data-driven overlays — anything that should *move* beyond a `transition` or a Ken-Burns `effect`. gsap / anime / d3 / lottie are preloaded. See [`references/html5.md`](../references/html5.md) and the copy-paste clips in [`references/html5-snippets.md`](../references/html5-snippets.md). |
215
+
216
+ **When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline, price odometer, shine-swept CTA, grain-graded teaser). Reserve the elaborate `html5` treatments for the hero / hype cuts where motion sells the product. If every clip in a "variety" brief is `rich-text`, you have not delivered variety.
217
+
218
+ ## Fonts
219
+
220
+ Use **custom Google Fonts via `timeline.fonts[]`**. System fonts (`Arial`, `Helvetica`, `Times New Roman`) are NOT installed and will fail with "Font not found".
221
+
222
+ **CRITICAL: Do NOT construct or fabricate Google Fonts URLs from memory.** Google rotates them (`v26 → v31 → ...`) and the hash filenames change with each version. Any URL you reconstruct from training data is almost certainly a 404. **Use ONLY the verified entries below, copied verbatim.**
223
+
224
+ See `references/fonts.md` for how to source fonts from the Studio SDK catalogue and the built-in fallback fonts.
225
+
226
+ ### Usage example
227
+
228
+ ```json
229
+ {
230
+ "timeline": {
231
+ "fonts": [
232
+ { "src": "https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf" }
233
+ ],
234
+ "tracks": [
235
+ {
236
+ "clips": [{
237
+ "asset": {
238
+ "type": "rich-text",
239
+ "text": "Hello",
240
+ "font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 60, "weight": "700", "color": "#ffffff" }
241
+ },
242
+ "start": 0, "length": 3
243
+ }]
244
+ }
245
+ ]
246
+ }
247
+ }
248
+ ```
249
+
250
+ The `font.family` value MUST match the `family` column in the table above (it's the filename basename without `.ttf`). If `family` and the URL's basename don't match, the font will not load.
251
+
252
+ ## Top 5 mistakes
253
+
254
+ 1. **Reverse track order.** `tracks[0]` is the TOP layer, not the bottom. Captions go in early tracks; backgrounds go in late tracks.
255
+ 2. **System fonts.** `Arial`, `Helvetica`, `Times New Roman`, etc. are not installed. Use Google Fonts via `timeline.fonts[]` (preferred) or one of the built-in fonts in `references/fonts.md`.
256
+ 3. **Captions fill the whole frame.** A `rich-caption` clip without `width`, `height`, and `fit: "none"` covers the entire output. Use a named preset from `references/caption.md`.
257
+ 4. **`<text>` inside an SVG asset.** Raw `<text>` is unsupported. Use a `rich-text` asset for any text content; reserve SVG for shapes only.
258
+ 5. **Composing custom caption styles when presets exist.** The five named presets (Nico, Kai, Kapow, Lovely Little Lychee, Rizz) cover the common styles. Use one verbatim from `references/caption.md` unless the user asks for something specific.
259
+
260
+ ## Per-topic deep dives
261
+
262
+ For details beyond this core guide (rich-caption presets, SVG constraints, full font URL list, troubleshooting), see the `references/` directory in the Shotstack CLI repo or fetch the topic-specific docs from `https://shotstack.io/docs/guide/llms-full.txt`.