@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.
- package/README.md +44 -1
- package/SKILL.md +59 -16
- package/dist/shotstack.js +44 -41
- package/package.json +3 -2
- package/references/fonts.md +25 -23
- package/references/html5-snippets.md +206 -0
- package/references/html5.md +252 -0
- package/references/ingest.md +121 -0
- package/references/motion.md +151 -0
- package/references/positioning.md +64 -0
- package/references/troubleshooting.md +8 -1
- package/shared/agent-core.md +262 -0
|
@@ -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`.
|