@depths/waves 0.1.0 → 0.3.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 +2139 -300
- package/dist/chunk-QP54QRAP.mjs +552 -0
- package/dist/chunk-YYS6AVTN.mjs +3346 -0
- package/dist/cli.js +3624 -250
- package/dist/cli.mjs +218 -55
- package/dist/index.d.mts +40 -12
- package/dist/index.d.ts +40 -12
- package/dist/index.js +3382 -178
- package/dist/index.mjs +3 -5
- package/dist/registry-C6H9G0df.d.mts +204 -0
- package/dist/registry-C6H9G0df.d.ts +204 -0
- package/dist/remotion/index.d.mts +8 -3
- package/dist/remotion/index.d.ts +8 -3
- package/dist/remotion/index.js +3102 -123
- package/dist/remotion/index.mjs +49 -11
- package/package.json +19 -19
- package/dist/chunk-TGAL5RQN.mjs +0 -404
- package/dist/chunk-WGQITADJ.mjs +0 -284
- package/dist/registry-hVIyqwS6.d.mts +0 -355
- package/dist/registry-hVIyqwS6.d.ts +0 -355
package/README.md
CHANGED
|
@@ -1,41 +1,147 @@
|
|
|
1
|
-
# @depths/waves
|
|
1
|
+
# @depths/waves (v0.3.0)
|
|
2
2
|
|
|
3
|
-
`@depths/waves` is a TypeScript-first library for rendering videos from a JSON
|
|
3
|
+
`@depths/waves` is a TypeScript-first library + CLI for rendering videos from a JSON "intermediate representation" (IR) using Remotion.
|
|
4
4
|
|
|
5
5
|
The intended workflow is:
|
|
6
6
|
|
|
7
7
|
1. An LLM (or a human) produces JSON that conforms to a schema (`VideoIRSchema`).
|
|
8
|
-
2. The IR is validated (schema + semantics).
|
|
8
|
+
2. The IR is validated (schema + semantics + registry contracts).
|
|
9
9
|
3. The IR is rendered to an output video file (MP4 by default) using Remotion.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
v0.2.0 introduced a shadcn-like catalog of higher-level "composite" components and a hybrid IR that supports:
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Scene timing is validated for common authoring mistakes (gaps, overlaps, duration mismatches, children exceeding parent bounds).
|
|
13
|
+
- **Segments (recommended for agents):** sequential scenes with optional overlaps ("transitions")
|
|
14
|
+
- **Timeline (escape hatch):** explicit timed nodes with overlaps allowed
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
v0.3.0 focuses on **seamless alignment + overlays**:
|
|
18
17
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- Built-in primitives: `Scene`, `Text`, `Audio`.
|
|
24
|
-
- Utilities to generate JSON Schemas for LLM prompting from registered component props.
|
|
18
|
+
- **Layout-safe primitives:** components that use normal layout flow by default (no accidental `position: absolute`)
|
|
19
|
+
- **Explicit layering primitives:** `Layers` + `Layer` for deterministic overlays (z-index, inset, opacity)
|
|
20
|
+
- **Explicit positioning primitive:** `Frame` for deliberate `x/y/width/height` placement (breaking change: `Box` no longer supports `x/y`)
|
|
21
|
+
- **Visual debugging workflow:** `waves stills` and `--debugBounds/--debugLabels` to inspect alignment issues without pixel tests
|
|
25
22
|
|
|
26
|
-
##
|
|
23
|
+
## Table of contents
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
- [Concept (end-to-end)](#concept-end-to-end)
|
|
26
|
+
- [What problem this solves](#what-problem-this-solves)
|
|
27
|
+
- [Mental model](#mental-model)
|
|
28
|
+
- [Core building blocks](#core-building-blocks)
|
|
29
|
+
- [How segments + transitions work](#how-segments--transitions-work)
|
|
30
|
+
- [Validation model](#validation-model)
|
|
31
|
+
- [Rendering model](#rendering-model)
|
|
32
|
+
- [Installation](#installation)
|
|
33
|
+
- [CLI (agent workflow)](#cli-agent-workflow)
|
|
34
|
+
- [IR v2.0 (authoring contract)](#ir-v20-authoring-contract)
|
|
35
|
+
- [Components (primitives + composites)](#components-primitives--composites)
|
|
36
|
+
- [Examples (composition recipes)](#examples-composition-recipes)
|
|
37
|
+
- [Library API (quickstart)](#library-api-quickstart)
|
|
38
|
+
- [Assets and paths (Windows/Linux)](#assets-and-paths-windowslinux)
|
|
39
|
+
- [Rendering prerequisites](#rendering-prerequisites)
|
|
40
|
+
- [Local CLI testing (before publishing)](#local-cli-testing-before-publishing)
|
|
41
|
+
- [Contributing](#contributing)
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
- The rendering system has hidden magic (implicit component inference, brittle conventions).
|
|
32
|
-
- The model lacks a precise schema + component catalog to target.
|
|
43
|
+
## Concept (end-to-end)
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
### What problem this solves
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
47
|
+
Remotion is a powerful way to render videos by writing React components. But for "LLM -> video" workflows, you need more than React:
|
|
48
|
+
|
|
49
|
+
- A strict authoring contract (so the model can reliably output something valid)
|
|
50
|
+
- Fast, deterministic validation (so failures happen before render time)
|
|
51
|
+
- A component catalog (so the model composes using known building blocks)
|
|
52
|
+
- A CLI (so an agent can drive the entire pipeline from a terminal)
|
|
53
|
+
|
|
54
|
+
Waves is a thin, explicit layer over Remotion that provides exactly those pieces.
|
|
55
|
+
|
|
56
|
+
### Mental model
|
|
57
|
+
|
|
58
|
+
Think of Waves as a compiler + runtime:
|
|
59
|
+
|
|
60
|
+
1) **Author**: you (or an agent) author a JSON IR document
|
|
61
|
+
2) **Validate**: Waves validates schema + semantics + component props
|
|
62
|
+
3) **Compile**: Waves compiles high-level authoring (segments) into a render-ready timeline
|
|
63
|
+
4) **Render**: Waves invokes Remotion bundling + rendering to produce an MP4
|
|
64
|
+
|
|
65
|
+
At render time, Waves is "just React + Remotion". The strictness lives earlier (IR + validation + registry contracts).
|
|
66
|
+
|
|
67
|
+
### Core building blocks
|
|
68
|
+
|
|
69
|
+
The codebase has a small number of core concepts:
|
|
70
|
+
|
|
71
|
+
- **Video IR (`VideoIRSchema`)**: the JSON document format you author.
|
|
72
|
+
- v0.3.0 targets `version: "2.0"` only.
|
|
73
|
+
- You author exactly one of `segments[]` (recommended) or `timeline[]` (escape hatch).
|
|
74
|
+
- **Component Registry (`ComponentRegistry`)**: a map from string `type` -> React component + Zod props schema + metadata.
|
|
75
|
+
- Only registered components can render.
|
|
76
|
+
- Props are validated with Zod and defaults are applied deterministically.
|
|
77
|
+
- **Validator (`IRValidator`)**: validates:
|
|
78
|
+
- IR schema (Zod)
|
|
79
|
+
- semantic rules (timing bounds, duration math, transition overlap constraints)
|
|
80
|
+
- registry contracts (type exists, props valid, children allowed, etc.)
|
|
81
|
+
- **Compiler (`compileToRenderGraph`)**: compiles authored IR into a normalized, timed representation used by rendering.
|
|
82
|
+
- In segments mode: inserts an internal `Segment` wrapper node for each segment and computes overlaps.
|
|
83
|
+
- In timeline mode: fills missing `timing` defaults (whole-video / whole-parent).
|
|
84
|
+
- **Renderer (`WavesEngine`)**: bundles a temporary Remotion project and calls Remotion's renderer.
|
|
85
|
+
- It writes a temporary entrypoint, bundles, selects a composition, and renders media.
|
|
86
|
+
|
|
87
|
+
### How segments + transitions work
|
|
88
|
+
|
|
89
|
+
Segments are the recommended authoring style for agents because they reduce timing math:
|
|
90
|
+
|
|
91
|
+
- Each segment has `durationInFrames` and a `root` node.
|
|
92
|
+
- Segment start times are derived by order.
|
|
93
|
+
- Optional overlaps are declared via `transitionToNext`.
|
|
94
|
+
|
|
95
|
+
Waves compiles segments into a "render timeline" (a list of timed component nodes). Each segment becomes:
|
|
96
|
+
|
|
97
|
+
- a root `Segment` node (internal; not shown in the LLM catalog)
|
|
98
|
+
- whose child is the authored `root` node
|
|
99
|
+
|
|
100
|
+
Why the internal `Segment` wrapper exists:
|
|
101
|
+
|
|
102
|
+
- Overlap transitions are easiest to implement at the segment boundary.
|
|
103
|
+
- The wrapper can apply fade/slide/zoom/clip effects for the first/last frames of a segment.
|
|
104
|
+
- The wrapper receives both the "enter transition" (inferred from the previous segment's `transitionToNext`) and the "exit transition" (the current segment's `transitionToNext`).
|
|
105
|
+
|
|
106
|
+
Overlap math:
|
|
107
|
+
|
|
108
|
+
- Let segment `i` have duration `Di`.
|
|
109
|
+
- Let overlap `Oi` be `segments[i].transitionToNext.durationInFrames` (only for segments that have a next segment).
|
|
110
|
+
- Then the compiled start time is:
|
|
111
|
+
- `start(0) = 0`
|
|
112
|
+
- `start(i+1) = start(i) + Di - Oi`
|
|
113
|
+
- The compiled video end must equal `video.durationInFrames`.
|
|
114
|
+
|
|
115
|
+
### Validation model
|
|
116
|
+
|
|
117
|
+
Waves tries to fail early with errors that are usable for an agent:
|
|
118
|
+
|
|
119
|
+
1) **Schema validation** (Zod):
|
|
120
|
+
- ensures the IR has the expected structure (types, required fields, etc.)
|
|
121
|
+
2) **Semantic validation**:
|
|
122
|
+
- validates segment overlap constraints (overlap cannot exceed either segment)
|
|
123
|
+
- validates that all timed nodes stay within their parent duration
|
|
124
|
+
- validates that the compiled timeline end equals `video.durationInFrames`
|
|
125
|
+
3) **Registry validation**:
|
|
126
|
+
- unknown component types fail
|
|
127
|
+
- props fail if they don't match the component's Zod schema
|
|
128
|
+
- children fail if the component metadata forbids children (or requires a certain number)
|
|
129
|
+
|
|
130
|
+
The validator applies Zod defaults into the IR node objects. This is intentional: it makes render-time props deterministic and reduces "undefined" cases inside components.
|
|
131
|
+
|
|
132
|
+
### Rendering model
|
|
133
|
+
|
|
134
|
+
Rendering is done by generating a tiny Remotion project on the fly:
|
|
135
|
+
|
|
136
|
+
1) Waves validates + compiles the IR to a timed timeline.
|
|
137
|
+
2) Waves writes a temporary `entry.tsx` that:
|
|
138
|
+
- registers the `WavesComposition` as the Remotion root
|
|
139
|
+
- embeds the compiled IR as JSON
|
|
140
|
+
- registers built-in components (and optional user `--register` modules)
|
|
141
|
+
3) Waves calls `@remotion/bundler` to create a bundle.
|
|
142
|
+
4) Waves calls `@remotion/renderer` to select the composition and render media.
|
|
143
|
+
|
|
144
|
+
This means you do not need to maintain a separate Remotion project just to use Waves.
|
|
39
145
|
|
|
40
146
|
## Installation
|
|
41
147
|
|
|
@@ -49,387 +155,2120 @@ Peer dependencies (must be installed by the consumer):
|
|
|
49
155
|
npm i react remotion
|
|
50
156
|
```
|
|
51
157
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- `ffmpeg` must be installed and available on the PATH.
|
|
55
|
-
- A Chromium browser must be available to `@remotion/renderer` (Remotion’s renderer runs headless Chromium).
|
|
56
|
-
|
|
57
|
-
## CLI / Agent workflow
|
|
58
|
-
|
|
59
|
-
Waves ships a `waves` CLI so a locally running AI agent (or any terminal user) can:
|
|
60
|
-
|
|
61
|
-
1) fetch a system prompt + JSON Schemas (the authoring contract)
|
|
62
|
-
2) write a starter IR file
|
|
63
|
-
3) validate the IR
|
|
64
|
-
4) render an MP4 from the IR
|
|
65
|
-
|
|
66
|
-
Install locally (recommended):
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npm i -D @depths/waves
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Run the CLI via the local bin:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
npx waves --help
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Or run without installing (one-off):
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx -y -p @depths/waves waves --help
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### 1) Get the agent prompt + schemas
|
|
85
|
-
|
|
86
|
-
Machine-readable payload (recommended for agents):
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
npx waves prompt --format json --out ./waves-prompt.json
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Human-readable system prompt:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
npx waves prompt --format text --out ./waves-system-prompt.txt
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Schemas only:
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
npx waves schema --kind all --pretty --out ./waves-schemas.json
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
If you register custom components at module import time, include them with repeatable `--register`:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
npx waves prompt --format json --register ./src/register-waves-components.ts
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### 2) Write IR JSON
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
npx waves write-ir --template basic --pretty --out ./video.ir.json
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### 3) Validate IR JSON
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
npx waves validate --in ./video.ir.json
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Structured validation result:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
npx waves validate --in ./video.ir.json --format json
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 4) Render MP4
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
npx waves render --in ./video.ir.json --out ./output.mp4 --codec h264 --crf 28 --concurrency 1
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
If your IR references `"/assets/..."` paths, pass `--publicDir` and ensure the files exist at `${publicDir}/assets/...`:
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
npx waves render --in ./video.ir.json --out ./output.mp4 --publicDir ./public
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Exit codes
|
|
141
|
-
|
|
142
|
-
- `0`: success
|
|
143
|
-
- `1`: usage error (invalid flags/command)
|
|
144
|
-
- `2`: validation failure (invalid JSON or IR validation errors)
|
|
145
|
-
- `3`: render failure
|
|
146
|
-
- `4`: I/O error (missing/unreadable/unwritable files)
|
|
147
|
-
- `5`: internal error (bug)
|
|
148
|
-
|
|
149
|
-
## Quickstart (one function)
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
import { renderVideo } from '@depths/waves';
|
|
158
|
+
Node: see `package.json` engines (this repo targets Node 22+).
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
type: 'Text',
|
|
168
|
-
timing: { from: 0, durationInFrames: 90 },
|
|
169
|
-
props: { content: 'Hello Waves', fontSize: 72, animation: 'fade' }
|
|
170
|
-
}
|
|
171
|
-
]
|
|
172
|
-
}
|
|
173
|
-
]
|
|
174
|
-
},
|
|
175
|
-
{ outputPath: './output.mp4' }
|
|
176
|
-
);
|
|
160
|
+
## CLI (agent workflow)
|
|
161
|
+
|
|
162
|
+
Waves ships a `waves` CLI so a locally running AI agent (or any terminal user) can:
|
|
163
|
+
|
|
164
|
+
1) fetch a system prompt + JSON Schemas + catalog
|
|
165
|
+
2) write a starter IR file
|
|
166
|
+
3) validate the IR
|
|
167
|
+
4) render an MP4 from the IR
|
|
168
|
+
|
|
169
|
+
Install locally (recommended):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm i -D @depths/waves
|
|
177
173
|
```
|
|
178
174
|
|
|
179
|
-
|
|
175
|
+
Run via the local bin:
|
|
180
176
|
|
|
181
|
-
|
|
177
|
+
```bash
|
|
178
|
+
npx waves --help
|
|
179
|
+
```
|
|
182
180
|
|
|
183
|
-
|
|
181
|
+
### Typical agent loop
|
|
184
182
|
|
|
185
|
-
|
|
186
|
-
- `video`: dimensions, fps, total duration
|
|
187
|
-
- `scenes`: an array of `Scene` components
|
|
183
|
+
For an agent that can execute terminal commands, the recommended flow is:
|
|
188
184
|
|
|
189
|
-
|
|
185
|
+
1. `waves prompt --format json --out waves-prompt.json`
|
|
186
|
+
2. Use `waves-prompt.json.systemPrompt` + `waves-prompt.json.schemas` + `waves-prompt.json.catalog` as the authoring contract for the model
|
|
187
|
+
3. Have the model output a single JSON object (Video IR)
|
|
188
|
+
4. Write that JSON to `video.v2.json`
|
|
189
|
+
5. `waves validate --in video.v2.json` until it passes
|
|
190
|
+
6. `waves render --in video.v2.json --out output.mp4 ...`
|
|
190
191
|
|
|
191
|
-
###
|
|
192
|
+
### 1) Get the prompt payload (system prompt + schemas + catalog)
|
|
192
193
|
|
|
193
|
-
-
|
|
194
|
-
- Scenes are expected to be sequential:
|
|
195
|
-
- Scene 0 starts at frame 0
|
|
196
|
-
- Scene N starts exactly where Scene N-1 ends
|
|
197
|
-
- Sum of scene durations must equal `video.durationInFrames`
|
|
198
|
-
- Child components (inside a `Scene`) must fit within the scene duration.
|
|
194
|
+
Machine-readable payload (recommended for agents):
|
|
199
195
|
|
|
200
|
-
|
|
196
|
+
```bash
|
|
197
|
+
npx waves prompt --format json --pretty --out ./waves-prompt.json
|
|
198
|
+
```
|
|
201
199
|
|
|
202
|
-
|
|
200
|
+
Human-readable system prompt:
|
|
203
201
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
- `metadata`: descriptions and examples useful for LLM prompting
|
|
202
|
+
```bash
|
|
203
|
+
npx waves prompt --format text --out ./waves-system-prompt.txt
|
|
204
|
+
```
|
|
208
205
|
|
|
209
|
-
|
|
206
|
+
Schemas only:
|
|
210
207
|
|
|
211
|
-
|
|
208
|
+
```bash
|
|
209
|
+
npx waves schema --kind all --pretty --out ./waves-schemas.json
|
|
210
|
+
```
|
|
212
211
|
|
|
213
|
-
|
|
214
|
-
2. Generate a temporary Remotion entry point
|
|
215
|
-
3. Bundle using `@remotion/bundler`
|
|
216
|
-
4. Select the composition using `@remotion/renderer`
|
|
217
|
-
5. Render the media to `outputPath`
|
|
212
|
+
Component catalog (grouped by category):
|
|
218
213
|
|
|
219
|
-
|
|
214
|
+
```bash
|
|
215
|
+
npx waves catalog
|
|
216
|
+
npx waves catalog --format json --pretty --out ./waves-catalog.json
|
|
217
|
+
```
|
|
220
218
|
|
|
221
|
-
|
|
219
|
+
If you register custom components at module import time, include them with repeatable `--register`:
|
|
222
220
|
|
|
223
|
-
|
|
221
|
+
```bash
|
|
222
|
+
npx waves prompt --format json --register ./src/register-waves-components.ts
|
|
223
|
+
npx waves schema --kind components --register ./src/register-waves-components.ts
|
|
224
|
+
npx waves validate --in ./video.v2.json --register ./src/register-waves-components.ts
|
|
225
|
+
```
|
|
224
226
|
|
|
225
|
-
|
|
227
|
+
Important notes on `--register`:
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
- The value must be a Node-loadable module at runtime (typically a `.js`/`.mjs` file).
|
|
230
|
+
- If you pass a path, prefer an absolute path or a relative path with `./`.
|
|
231
|
+
- A registration module usually calls `globalRegistry.register(...)` to add custom components before validation/rendering.
|
|
232
|
+
- Re-registering an existing `type` throws (registry types are unique).
|
|
228
233
|
|
|
229
|
-
|
|
230
|
-
import { VideoIRSchema } from '@depths/waves';
|
|
234
|
+
### 2) Write IR JSON
|
|
231
235
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// parsed.error.issues
|
|
235
|
-
}
|
|
236
|
+
```bash
|
|
237
|
+
npx waves write-ir --template basic --pretty --out ./video.v2.json
|
|
236
238
|
```
|
|
237
239
|
|
|
238
|
-
###
|
|
240
|
+
### 3) Validate IR JSON
|
|
239
241
|
|
|
240
|
-
|
|
242
|
+
```bash
|
|
243
|
+
npx waves validate --in ./video.v2.json
|
|
244
|
+
```
|
|
241
245
|
|
|
242
|
-
|
|
243
|
-
import { IRValidator } from '@depths/waves';
|
|
246
|
+
Structured validation result:
|
|
244
247
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (!result.success) {
|
|
248
|
-
// result.errors: { path: string[]; message: string; code: string }[]
|
|
249
|
-
}
|
|
248
|
+
```bash
|
|
249
|
+
npx waves validate --in ./video.v2.json --format json
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
### 4) Render MP4
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
```bash
|
|
255
|
+
npx waves render --in ./video.v2.json --out ./output.mp4 --codec h264 --crf 28 --concurrency 1
|
|
256
|
+
```
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
If your IR references `"/assets/..."` paths, pass `--publicDir` and ensure the files exist at `${publicDir}/assets/...`:
|
|
259
259
|
|
|
260
|
-
```
|
|
261
|
-
|
|
260
|
+
```bash
|
|
261
|
+
npx waves render --in ./video.v2.json --out ./output.mp4 --publicDir ./public
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
### Exit codes
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
- `0`: success
|
|
267
|
+
- `1`: usage error (invalid flags/command)
|
|
268
|
+
- `2`: validation failure (invalid JSON or IR validation errors)
|
|
269
|
+
- `3`: render failure
|
|
270
|
+
- `4`: I/O error (missing/unreadable/unwritable files)
|
|
271
|
+
- `5`: internal error (bug)
|
|
267
272
|
|
|
268
|
-
|
|
273
|
+
### Command reference
|
|
269
274
|
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
This section documents every command and flag supported by the current CLI implementation (`src/cli.ts`).
|
|
276
|
+
|
|
277
|
+
#### `waves --help` / `waves help`
|
|
278
|
+
|
|
279
|
+
Prints a short help text.
|
|
280
|
+
|
|
281
|
+
#### `waves --version`
|
|
282
|
+
|
|
283
|
+
Prints the package version (e.g. `0.2.0`).
|
|
284
|
+
|
|
285
|
+
#### `waves prompt`
|
|
286
|
+
|
|
287
|
+
Outputs an "agent-ready" payload that includes:
|
|
288
|
+
|
|
289
|
+
- `systemPrompt`: a full system prompt string
|
|
290
|
+
- `schemas.videoIR`: JSON Schema for authoring IR (segments mode)
|
|
291
|
+
- `schemas.components`: a JSON object keyed by component `type` containing props JSON Schemas + metadata
|
|
292
|
+
- `catalog`: categories + items (flattened list for easier prompting / UI)
|
|
293
|
+
|
|
294
|
+
Flags:
|
|
295
|
+
|
|
296
|
+
- `--format text|json` (default `text`)
|
|
297
|
+
- `--maxChars <n>` (text format only; truncates the prompt)
|
|
298
|
+
- `--pretty` (json format only)
|
|
299
|
+
- `--out <path>` (optional; writes the output to a file in addition to stdout)
|
|
300
|
+
- `--register <module>` (repeatable)
|
|
272
301
|
|
|
273
|
-
|
|
302
|
+
Examples:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
npx waves prompt --format json --pretty --out ./waves-prompt.json
|
|
306
|
+
npx waves prompt --format text --maxChars 4000 --out ./waves-system-prompt.txt
|
|
274
307
|
```
|
|
275
308
|
|
|
276
|
-
|
|
309
|
+
#### `waves schema`
|
|
277
310
|
|
|
278
|
-
|
|
311
|
+
Outputs JSON Schemas only.
|
|
279
312
|
|
|
280
|
-
|
|
313
|
+
Flags:
|
|
281
314
|
|
|
282
|
-
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
315
|
+
- `--kind video-ir|components|all` (default `all`)
|
|
316
|
+
- `--pretty`
|
|
317
|
+
- `--out <path>` (optional; writes output to a file in addition to stdout)
|
|
318
|
+
- `--register <module>` (repeatable; affects `--kind components` and `all`)
|
|
285
319
|
|
|
286
|
-
|
|
287
|
-
import { renderVideo } from '@depths/waves';
|
|
320
|
+
Examples:
|
|
288
321
|
|
|
289
|
-
|
|
322
|
+
```bash
|
|
323
|
+
npx waves schema --kind video-ir --pretty --out ./schema.video-ir.json
|
|
324
|
+
npx waves schema --kind components --pretty --out ./schema.components.json
|
|
290
325
|
```
|
|
291
326
|
|
|
292
|
-
|
|
327
|
+
#### `waves catalog`
|
|
293
328
|
|
|
294
|
-
|
|
329
|
+
Prints the built-in component catalog grouped by category.
|
|
295
330
|
|
|
296
|
-
|
|
297
|
-
|
|
331
|
+
Flags:
|
|
332
|
+
|
|
333
|
+
- `--format text|json` (default `text`)
|
|
334
|
+
- `--pretty` (json format only)
|
|
335
|
+
- `--out <path>` (optional; writes output to a file in addition to stdout)
|
|
336
|
+
- `--includeInternal` (includes internal-only types such as `Segment`)
|
|
337
|
+
- `--register <module>` (repeatable; adds additional registered types)
|
|
338
|
+
|
|
339
|
+
Examples:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
npx waves catalog
|
|
343
|
+
npx waves catalog --format json --pretty --out ./waves-catalog.json
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### `waves write-ir`
|
|
347
|
+
|
|
348
|
+
Writes a starter IR JSON file (always `version: "2.0"` segments mode).
|
|
349
|
+
|
|
350
|
+
Flags:
|
|
351
|
+
|
|
352
|
+
- `--template minimal|basic` (default `minimal`)
|
|
353
|
+
- `--pretty`
|
|
354
|
+
- `--out <path>` (required)
|
|
298
355
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
crf: 18,
|
|
305
|
-
concurrency: 4
|
|
306
|
-
});
|
|
356
|
+
Examples:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
npx waves write-ir --template minimal --pretty --out ./video.v2.json
|
|
360
|
+
npx waves write-ir --template basic --pretty --out ./examples/basic.v2.json
|
|
307
361
|
```
|
|
308
362
|
|
|
309
|
-
|
|
363
|
+
#### `waves validate`
|
|
364
|
+
|
|
365
|
+
Validates an IR JSON file.
|
|
366
|
+
|
|
367
|
+
Flags:
|
|
310
368
|
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
- `
|
|
314
|
-
-
|
|
315
|
-
- `publicDir`: optional directory for Remotion `staticFile()` assets
|
|
369
|
+
- `--in <path>` (required)
|
|
370
|
+
- `--format text|json` (default `text`)
|
|
371
|
+
- `--pretty` (json format only)
|
|
372
|
+
- `--register <module>` (repeatable)
|
|
316
373
|
|
|
317
|
-
|
|
374
|
+
Behavior:
|
|
318
375
|
|
|
319
|
-
- `
|
|
320
|
-
-
|
|
376
|
+
- text format prints `ok` to stdout on success, otherwise prints a human-readable list to stderr and exits non-zero.
|
|
377
|
+
- json format prints `{ "success": true }` or `{ "success": false, "errors": [...] }` to stdout.
|
|
378
|
+
|
|
379
|
+
Examples:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
npx waves validate --in ./video.v2.json
|
|
383
|
+
npx waves validate --in ./video.v2.json --format json --pretty
|
|
384
|
+
```
|
|
321
385
|
|
|
322
|
-
|
|
386
|
+
#### `waves render`
|
|
387
|
+
|
|
388
|
+
Renders an MP4 from an IR JSON file.
|
|
389
|
+
|
|
390
|
+
Flags:
|
|
391
|
+
|
|
392
|
+
- `--in <path>` (required)
|
|
393
|
+
- `--out <path>` (required)
|
|
394
|
+
- `--publicDir <path>` (optional; required if your IR uses `/assets/...` paths)
|
|
395
|
+
- `--codec h264|h265|vp8|vp9` (optional; default `h264`)
|
|
396
|
+
- `--crf <n>` (optional; forwarded to Remotion renderer)
|
|
397
|
+
- `--concurrency <n|string>` (optional; forwarded to Remotion renderer)
|
|
398
|
+
- `--debugBounds` (optional; draws debug outlines for every rendered node)
|
|
399
|
+
- `--debugLabels` (optional; labels debug outlines with `type#id`)
|
|
400
|
+
- `--register <module>` (repeatable)
|
|
401
|
+
- `--pretty` (only affects the formatting of error JSON when render fails)
|
|
402
|
+
|
|
403
|
+
Examples:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npx waves render --in ./video.v2.json --out ./output.mp4 --codec h264 --crf 28 --concurrency 1
|
|
407
|
+
npx waves render --in ./video.v2.json --out ./output.mp4 --publicDir ./public
|
|
408
|
+
npx waves render --in ./video.v2.json --out ./output.mp4 --publicDir ./public --debugBounds --debugLabels
|
|
409
|
+
```
|
|
323
410
|
|
|
324
|
-
|
|
411
|
+
#### `waves stills`
|
|
325
412
|
|
|
326
|
-
|
|
327
|
-
2. Absolute “public” paths: `/assets/foo.png`
|
|
413
|
+
Renders a set of still images (single frames) from an IR JSON file. This is the recommended way to iterate on alignment and overlays without re-rendering full MP4s.
|
|
328
414
|
|
|
329
|
-
|
|
415
|
+
Flags:
|
|
330
416
|
|
|
331
|
-
-
|
|
332
|
-
-
|
|
417
|
+
- `--in <path>` (required)
|
|
418
|
+
- `--outDir <path>` (required)
|
|
419
|
+
- `--frames <csv>` (required; e.g. `"0,30,60"`)
|
|
420
|
+
- `--publicDir <path>` (optional; required if your IR uses `/assets/...` paths)
|
|
421
|
+
- `--imageFormat png|jpeg|webp` (optional; default `png`)
|
|
422
|
+
- `--scale <n>` (optional; default `1`)
|
|
423
|
+
- `--jpegQuality <n>` (optional; default `90`; only used when `--imageFormat jpeg`)
|
|
424
|
+
- `--debugBounds` / `--debugLabels` (same as `waves render`)
|
|
425
|
+
- `--register <module>` (repeatable)
|
|
333
426
|
|
|
334
|
-
|
|
427
|
+
Examples:
|
|
335
428
|
|
|
429
|
+
```bash
|
|
430
|
+
npx waves stills --in ./video.v2.json --outDir ./examples/_stills --frames "0,45,90" --publicDir ./public
|
|
431
|
+
npx waves stills --in ./video.v2.json --outDir ./examples/_stills --frames "0,45,90" --publicDir ./public --debugBounds --debugLabels
|
|
336
432
|
```
|
|
337
|
-
|
|
433
|
+
|
|
434
|
+
## IR v2.0 (authoring contract)
|
|
435
|
+
|
|
436
|
+
v0.3.0 targets `version: "2.0"` only.
|
|
437
|
+
|
|
438
|
+
### Recommended: `segments[]` (high-level)
|
|
439
|
+
|
|
440
|
+
In segments mode, you provide sequential segments and Waves compiles them into an explicit timed timeline. You usually do not need to specify `timing` on nodes.
|
|
441
|
+
|
|
442
|
+
Key rules:
|
|
443
|
+
|
|
444
|
+
- Segment overlap is controlled by `transitionToNext.durationInFrames`.
|
|
445
|
+
- `transitionToNext` is only valid when there is a "next" segment (i.e. it is not allowed on the last segment).
|
|
446
|
+
- The total video duration must match the compiled timeline end:
|
|
447
|
+
- `video.durationInFrames = sum(segment.durationInFrames) - sum(overlap)`
|
|
448
|
+
- where `overlap = transitionToNext.durationInFrames` for each segment that has a next segment.
|
|
449
|
+
|
|
450
|
+
Minimal example:
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"version": "2.0",
|
|
455
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 60 },
|
|
456
|
+
"segments": [
|
|
457
|
+
{
|
|
458
|
+
"id": "scene-1",
|
|
459
|
+
"durationInFrames": 60,
|
|
460
|
+
"root": {
|
|
461
|
+
"id": "root",
|
|
462
|
+
"type": "Scene",
|
|
463
|
+
"props": { "background": { "type": "color", "value": "#000000" } },
|
|
464
|
+
"children": [{ "id": "t1", "type": "Text", "props": { "content": "Hello" } }]
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
]
|
|
468
|
+
}
|
|
338
469
|
```
|
|
339
470
|
|
|
340
|
-
|
|
471
|
+
Supported `transitionToNext.type` values (segment overlap transitions):
|
|
472
|
+
|
|
473
|
+
- `FadeTransition`
|
|
474
|
+
- `SlideTransition` (`props`: `{ direction: "left"|"right"|"up"|"down", distance?: number }`)
|
|
475
|
+
- `ZoomTransition` (`props`: `{ type: "zoomIn"|"zoomOut" }`)
|
|
476
|
+
- `WipeTransition` (`props`: `{ direction: "left"|"right"|"up"|"down"|"diagonal", softEdge?: boolean }`)
|
|
477
|
+
- `CircularReveal` (`props`: `{ direction: "open"|"close", center?: { x: 0..1, y: 0..1 } }`)
|
|
341
478
|
|
|
342
|
-
### `
|
|
479
|
+
### Escape hatch: `timeline[]` (low-level)
|
|
343
480
|
|
|
344
|
-
|
|
481
|
+
In timeline mode you provide explicit timings. Each node's `timing` is relative to its parent sequence (nested timing is "local").
|
|
482
|
+
|
|
483
|
+
Notes:
|
|
484
|
+
|
|
485
|
+
- Root `timing` is optional; if omitted, Waves treats the node as spanning the full video duration.
|
|
486
|
+
- Child `timing` is optional; if omitted, Waves treats the child as spanning the full parent duration.
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
{
|
|
490
|
+
"version": "2.0",
|
|
491
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 60 },
|
|
492
|
+
"timeline": [
|
|
493
|
+
{
|
|
494
|
+
"id": "scene",
|
|
495
|
+
"type": "Scene",
|
|
496
|
+
"timing": { "from": 0, "durationInFrames": 60 },
|
|
497
|
+
"props": { "background": { "type": "color", "value": "#000000" } }
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Component nodes
|
|
504
|
+
|
|
505
|
+
Nodes are structural (the IR does not hard-code component types):
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
type ComponentNode = {
|
|
509
|
+
id: string;
|
|
510
|
+
type: string; // must be registered at validate/render time
|
|
511
|
+
props?: Record<string, unknown>;
|
|
512
|
+
timing?: { from: number; durationInFrames: number };
|
|
513
|
+
children?: ComponentNode[];
|
|
514
|
+
};
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Validation uses the registry to enforce:
|
|
518
|
+
|
|
519
|
+
- unknown component types (error)
|
|
520
|
+
- props schemas (Zod validation, defaults applied)
|
|
521
|
+
- whether a component can have `children` (metadata contract)
|
|
522
|
+
|
|
523
|
+
## Components (primitives + composites)
|
|
524
|
+
|
|
525
|
+
Waves renders only components that have been explicitly registered in the `ComponentRegistry`. Each component is defined by:
|
|
526
|
+
|
|
527
|
+
- a **string type** (e.g. `"Scene"`, `"TypewriterText"`)
|
|
528
|
+
- a **React component** implementation (Remotion primitives + CSS)
|
|
529
|
+
- a **Zod props schema** (validation + defaults)
|
|
530
|
+
- **metadata** (kind/category/description/LLM guidance/children contract)
|
|
531
|
+
|
|
532
|
+
### Primitives vs composites
|
|
533
|
+
|
|
534
|
+
- **Primitives** are low-level building blocks (layout, text, media). They are intentionally generic.
|
|
535
|
+
- **Composites** are higher-level building blocks (shadcn-like) that encode common video patterns: titles, lower thirds, social cards, charts, transitions, etc.
|
|
536
|
+
|
|
537
|
+
In general:
|
|
538
|
+
|
|
539
|
+
- Prefer composites for agent-authored videos (less "design work" for the model).
|
|
540
|
+
- Use primitives when you need precise control or to build new composites.
|
|
541
|
+
|
|
542
|
+
### Categories and children contracts
|
|
543
|
+
|
|
544
|
+
Each component is categorized (`text`, `layout`, `media`, `transition`, etc.) to help an agent choose the right tool.
|
|
545
|
+
|
|
546
|
+
Some components can have nested `children` in the IR. This is controlled by metadata:
|
|
547
|
+
|
|
548
|
+
- `acceptsChildren: true|false`
|
|
549
|
+
- optional `minChildren` / `maxChildren` constraints
|
|
550
|
+
|
|
551
|
+
If a component does not accept children and the IR provides `children`, validation fails before rendering.
|
|
552
|
+
|
|
553
|
+
### Internal props: `__wavesDurationInFrames`
|
|
554
|
+
|
|
555
|
+
At render time, Waves injects `__wavesDurationInFrames` into every component. This is the duration of the node's `Sequence`.
|
|
556
|
+
|
|
557
|
+
This allows components to implement "in/out" animations without the author hand-computing timings.
|
|
558
|
+
|
|
559
|
+
This prop is *not* part of the author-facing props schema and does not appear in the tables below.
|
|
560
|
+
|
|
561
|
+
### Keeping docs in sync
|
|
562
|
+
|
|
563
|
+
The tables below are generated from the live registry JSON Schemas to reduce drift.
|
|
564
|
+
|
|
565
|
+
Regenerate after changing component props/metadata:
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
npm run build
|
|
569
|
+
node scripts/generate-readme-components.mjs
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
<!-- BEGIN GENERATED: COMPONENTS -->
|
|
573
|
+
|
|
574
|
+
<!-- generated by scripts/generate-readme-components.mjs; do not edit by hand -->
|
|
575
|
+
|
|
576
|
+
### Components summary
|
|
577
|
+
|
|
578
|
+
| Type | Kind | Category | Children | Internal | Description |
|
|
579
|
+
| - | - | - | - | - | - |
|
|
580
|
+
| `IntroScene` | composite | branding | no | no | Branded intro scene (logo + company name + optional tagline) |
|
|
581
|
+
| `LogoReveal` | composite | branding | no | no | Logo intro animation (fade/scale/rotate/slide), optionally with a sound effect |
|
|
582
|
+
| `OutroScene` | composite | branding | no | no | End screen with logo, message, optional CTA buttons and social handles |
|
|
583
|
+
| `Watermark` | composite | branding | no | no | Persistent logo/text watermark in a corner |
|
|
584
|
+
| `AnimatedCounter` | composite | data | no | no | Animated numeric counter (spring or linear), optionally with an icon and suffix |
|
|
585
|
+
| `BarChart` | composite | data | no | no | Animated bar chart (vertical or horizontal) |
|
|
586
|
+
| `LineGraph` | composite | data | no | no | Animated line graph (SVG) with draw/reveal modes |
|
|
587
|
+
| `ProgressBar` | composite | data | no | no | Animated progress bar that fills over the component duration |
|
|
588
|
+
| `ProgressRing` | composite | data | no | no | Circular progress indicator (SVG) that animates from 0 to percentage over duration |
|
|
589
|
+
| `Image` | primitive | image | no | no | Full-frame image with object-fit options |
|
|
590
|
+
| `ImageCollage` | composite | image | no | no | Collage of multiple images in a grid/stack/scatter layout with staggered entrances |
|
|
591
|
+
| `ImageReveal` | composite | image | no | no | Reveals an image with wipe/expand/iris entrance effects |
|
|
592
|
+
| `ImageSequence` | composite | image | no | no | Plays a numbered image sequence (frame-by-frame) |
|
|
593
|
+
| `ImageWithCaption` | composite | image | no | no | Image with a caption strip (top/bottom) or overlay caption |
|
|
594
|
+
| `KenBurnsImage` | composite | image | no | no | Slow zoom and pan (Ken Burns effect) for a still image |
|
|
595
|
+
| `Box` | primitive | layout | yes | no | Flow container for layout and backgrounds (layout-safe) |
|
|
596
|
+
| `CardStack` | composite | layout | no | no | Sequential stacked cards (2-5) with flip/slide/fade transitions |
|
|
597
|
+
| `Frame` | primitive | layout | yes | no | Absolute-positioned container (x/y placement) |
|
|
598
|
+
| `Grid` | primitive | layout | yes | no | Grid layout container with configurable rows/columns |
|
|
599
|
+
| `GridLayout` | composite | layout | yes (1..∞) | no | Simple responsive grid layout for child components |
|
|
600
|
+
| `Layer` | primitive | layout | yes (1..∞) | no | One overlay layer with explicit zIndex inside Layers |
|
|
601
|
+
| `Layers` | primitive | layout | yes (1..∞) | no | Overlay container for stacking children (use Layer for zIndex) |
|
|
602
|
+
| `Scene` | primitive | layout | yes | no | Scene container with a background and nested children |
|
|
603
|
+
| `Segment` | primitive | layout | yes (1..1) | yes | Internal segment wrapper (used by v2 segments compiler) |
|
|
604
|
+
| `Shape` | primitive | layout | no | no | Simple rect/circle shape for UI accents |
|
|
605
|
+
| `SplitScreen` | composite | layout | yes (2..2) | no | Two-panel split screen layout |
|
|
606
|
+
| `Stack` | primitive | layout | yes | no | Flexbox stack layout (row/column) with gap and alignment |
|
|
607
|
+
| `ThirdLowerBanner` | composite | layout | no | no | Broadcast-style lower-third banner with name/title and optional avatar |
|
|
608
|
+
| `Audio` | primitive | media | no | no | Plays an audio file with optional trimming and fade in/out |
|
|
609
|
+
| `Video` | primitive | media | no | no | Full-frame video with object-fit options |
|
|
610
|
+
| `VideoWithOverlay` | composite | media | no | no | Video background with an optional overlay (text/logo/gradient) |
|
|
611
|
+
| `InstagramStory` | composite | social | no | no | Instagram story-style layout with profile header, text overlay, and optional sticker |
|
|
612
|
+
| `TikTokCaption` | composite | social | no | no | TikTok-style captions with stroke and optional word highlighting |
|
|
613
|
+
| `TwitterCard` | composite | social | no | no | Twitter/X post card layout with author header and optional image |
|
|
614
|
+
| `YouTubeThumbnail` | composite | social | no | no | YouTube-style thumbnail layout (16:9) with bold title and optional face cutout |
|
|
615
|
+
| `CountUpText` | composite | text | no | no | Counts from a start value to an end value with formatting options |
|
|
616
|
+
| `GlitchText` | composite | text | no | no | Cyberpunk-style glitch text with RGB split jitter |
|
|
617
|
+
| `KineticTypography` | composite | text | no | no | Rhythmic single-word kinetic typography driven by a timing array |
|
|
618
|
+
| `OutlineText` | composite | text | no | no | Outlined title text with simple draw/fill animation |
|
|
619
|
+
| `SplitText` | composite | text | no | no | Animated text where each word or letter enters with a staggered effect |
|
|
620
|
+
| `SubtitleText` | composite | text | no | no | Caption/subtitle box with fade in/out and optional highlighted words |
|
|
621
|
+
| `Text` | primitive | text | no | no | Displays animated text with positioning and animation options |
|
|
622
|
+
| `TypewriterText` | composite | text | no | no | Character-by-character text reveal with optional blinking cursor |
|
|
623
|
+
| `CircularReveal` | composite | transition | yes (1..∞) | no | Circular iris reveal/hide transition wrapper |
|
|
624
|
+
| `FadeTransition` | composite | transition | yes (1..∞) | no | Fade in/out wrapper (used for segment transitions and overlays) |
|
|
625
|
+
| `SlideTransition` | composite | transition | yes (1..∞) | no | Slide in/out wrapper (used for segment transitions and overlays) |
|
|
626
|
+
| `WipeTransition` | composite | transition | yes (1..∞) | no | Directional wipe reveal/hide wrapper transition |
|
|
627
|
+
| `ZoomTransition` | composite | transition | yes (1..∞) | no | Zoom in/out wrapper transition |
|
|
628
|
+
|
|
629
|
+
### Components reference
|
|
630
|
+
|
|
631
|
+
#### Category: `branding`
|
|
632
|
+
|
|
633
|
+
##### `IntroScene`
|
|
634
|
+
|
|
635
|
+
- kind: `composite`
|
|
636
|
+
- category: `branding`
|
|
637
|
+
- internal: `false`
|
|
638
|
+
- children: `no`
|
|
639
|
+
- description: Branded intro scene (logo + company name + optional tagline)
|
|
640
|
+
- llmGuidance: Use as the first segment. Works best at 3-5 seconds. musicTrack can add ambience.
|
|
345
641
|
|
|
346
642
|
Props:
|
|
347
643
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
644
|
+
| Prop | Type | Required | Default | Notes |
|
|
645
|
+
| - | - | - | - | - |
|
|
646
|
+
| `backgroundColor` | string | yes | "#000000" | |
|
|
647
|
+
| `companyName` | string | yes | | minLength=1 |
|
|
648
|
+
| `logoSrc` | string | yes | | minLength=1 |
|
|
649
|
+
| `musicTrack` | string | no | | |
|
|
650
|
+
| `primaryColor` | string | yes | "#FFFFFF" | |
|
|
651
|
+
| `tagline` | string | no | | |
|
|
652
|
+
|
|
653
|
+
##### `LogoReveal`
|
|
654
|
+
|
|
655
|
+
- kind: `composite`
|
|
656
|
+
- category: `branding`
|
|
657
|
+
- internal: `false`
|
|
658
|
+
- children: `no`
|
|
659
|
+
- description: Logo intro animation (fade/scale/rotate/slide), optionally with a sound effect
|
|
660
|
+
- llmGuidance: Use for intros/outros. Keep the logo high-contrast and centered. soundEffect can be a short sting.
|
|
352
661
|
|
|
353
|
-
|
|
662
|
+
Props:
|
|
354
663
|
|
|
355
|
-
|
|
664
|
+
| Prop | Type | Required | Default | Notes |
|
|
665
|
+
| - | - | - | - | - |
|
|
666
|
+
| `backgroundColor` | string | yes | "#000000" | |
|
|
667
|
+
| `effect` | enum("fade" \| "scale" \| "rotate" \| "slide") | yes | "scale" | |
|
|
668
|
+
| `logoSrc` | string | yes | | minLength=1 |
|
|
669
|
+
| `soundEffect` | string | no | | |
|
|
356
670
|
|
|
357
|
-
|
|
671
|
+
##### `OutroScene`
|
|
358
672
|
|
|
359
|
-
|
|
673
|
+
- kind: `composite`
|
|
674
|
+
- category: `branding`
|
|
675
|
+
- internal: `false`
|
|
676
|
+
- children: `no`
|
|
677
|
+
- description: End screen with logo, message, optional CTA buttons and social handles
|
|
678
|
+
- llmGuidance: Use as the last segment. Keep CTAs <=3 for clarity.
|
|
360
679
|
|
|
361
680
|
Props:
|
|
362
681
|
|
|
363
|
-
|
|
364
|
-
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
682
|
+
| Prop | Type | Required | Default | Notes |
|
|
683
|
+
| - | - | - | - | - |
|
|
684
|
+
| `backgroundColor` | string | yes | "#000000" | |
|
|
685
|
+
| `ctaButtons` | array<object> | no | | maxItems=3 |
|
|
686
|
+
| `logoSrc` | string | yes | | minLength=1 |
|
|
687
|
+
| `message` | string | yes | "Thank You" | |
|
|
688
|
+
| `socialHandles` | array<object> | no | | maxItems=4 |
|
|
368
689
|
|
|
369
|
-
|
|
690
|
+
##### `Watermark`
|
|
370
691
|
|
|
371
|
-
|
|
692
|
+
- kind: `composite`
|
|
693
|
+
- category: `branding`
|
|
694
|
+
- internal: `false`
|
|
695
|
+
- children: `no`
|
|
696
|
+
- description: Persistent logo/text watermark in a corner
|
|
697
|
+
- llmGuidance: Use subtle opacity (0.3-0.6). bottomRight is standard.
|
|
372
698
|
|
|
373
699
|
Props:
|
|
374
700
|
|
|
375
|
-
|
|
376
|
-
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
701
|
+
| Prop | Type | Required | Default | Notes |
|
|
702
|
+
| - | - | - | - | - |
|
|
703
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
704
|
+
| `opacity` | number | yes | 0.5 | min=0.1, max=1 |
|
|
705
|
+
| `position` | enum("topLeft" \| "topRight" \| "bottomLeft" \| "bottomRight") | yes | "bottomRight" | |
|
|
706
|
+
| `size` | number | yes | 60 | min=30, max=150 |
|
|
707
|
+
| `src` | string | no | | |
|
|
708
|
+
| `text` | string | no | | |
|
|
709
|
+
| `type` | enum("logo" \| "text") | yes | "logo" | |
|
|
380
710
|
|
|
381
|
-
|
|
711
|
+
#### Category: `data`
|
|
712
|
+
|
|
713
|
+
##### `AnimatedCounter`
|
|
714
|
+
|
|
715
|
+
- kind: `composite`
|
|
716
|
+
- category: `data`
|
|
717
|
+
- internal: `false`
|
|
718
|
+
- children: `no`
|
|
719
|
+
- description: Animated numeric counter (spring or linear), optionally with an icon and suffix
|
|
720
|
+
- llmGuidance: Use for big stats. animationType="spring" feels natural. suffix for units (%, K, M).
|
|
721
|
+
|
|
722
|
+
Props:
|
|
723
|
+
|
|
724
|
+
| Prop | Type | Required | Default | Notes |
|
|
725
|
+
| - | - | - | - | - |
|
|
726
|
+
| `animationType` | enum("spring" \| "linear") | yes | "spring" | |
|
|
727
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
728
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
729
|
+
| `fontSize` | number | yes | 96 | min=8, max=300 |
|
|
730
|
+
| `fontWeight` | integer | yes | 700 | min=100, max=900 |
|
|
731
|
+
| `from` | number | yes | 0 | |
|
|
732
|
+
| `icon` | string | no | | |
|
|
733
|
+
| `suffix` | string | no | | |
|
|
734
|
+
| `to` | number | yes | 100 | |
|
|
735
|
+
|
|
736
|
+
##### `BarChart`
|
|
737
|
+
|
|
738
|
+
- kind: `composite`
|
|
739
|
+
- category: `data`
|
|
740
|
+
- internal: `false`
|
|
741
|
+
- children: `no`
|
|
742
|
+
- description: Animated bar chart (vertical or horizontal)
|
|
743
|
+
- llmGuidance: Use 2-6 bars. Provide maxValue to lock scale across multiple charts.
|
|
744
|
+
|
|
745
|
+
Props:
|
|
746
|
+
|
|
747
|
+
| Prop | Type | Required | Default | Notes |
|
|
748
|
+
| - | - | - | - | - |
|
|
749
|
+
| `data` | array<object> | yes | | minItems=2, maxItems=8 |
|
|
750
|
+
| `maxValue` | number | no | | |
|
|
751
|
+
| `orientation` | enum("horizontal" \| "vertical") | yes | "vertical" | |
|
|
752
|
+
| `showGrid` | boolean | yes | false | |
|
|
753
|
+
| `showValues` | boolean | yes | true | |
|
|
754
|
+
|
|
755
|
+
##### `LineGraph`
|
|
756
|
+
|
|
757
|
+
- kind: `composite`
|
|
758
|
+
- category: `data`
|
|
759
|
+
- internal: `false`
|
|
760
|
+
- children: `no`
|
|
761
|
+
- description: Animated line graph (SVG) with draw/reveal modes
|
|
762
|
+
- llmGuidance: Use 5-20 points. animate="draw" traces the line; animate="reveal" wipes it left-to-right.
|
|
763
|
+
|
|
764
|
+
Props:
|
|
765
|
+
|
|
766
|
+
| Prop | Type | Required | Default | Notes |
|
|
767
|
+
| - | - | - | - | - |
|
|
768
|
+
| `animate` | enum("draw" \| "reveal") | yes | "draw" | |
|
|
769
|
+
| `color` | string | yes | "#00FF00" | |
|
|
770
|
+
| `data` | array<object> | yes | | minItems=2, maxItems=50 |
|
|
771
|
+
| `fillArea` | boolean | yes | false | |
|
|
772
|
+
| `showDots` | boolean | yes | true | |
|
|
773
|
+
| `strokeWidth` | number | yes | 3 | min=1, max=10 |
|
|
774
|
+
|
|
775
|
+
##### `ProgressBar`
|
|
776
|
+
|
|
777
|
+
- kind: `composite`
|
|
778
|
+
- category: `data`
|
|
779
|
+
- internal: `false`
|
|
780
|
+
- children: `no`
|
|
781
|
+
- description: Animated progress bar that fills over the component duration
|
|
782
|
+
- llmGuidance: Use for loading/countdowns. showPercentage=true is helpful for clarity.
|
|
783
|
+
|
|
784
|
+
Props:
|
|
785
|
+
|
|
786
|
+
| Prop | Type | Required | Default | Notes |
|
|
787
|
+
| - | - | - | - | - |
|
|
788
|
+
| `backgroundColor` | string | yes | "rgba(255,255,255,0.2)" | |
|
|
789
|
+
| `color` | string | yes | "#00FF00" | |
|
|
790
|
+
| `height` | number | yes | 10 | min=5, max=50 |
|
|
791
|
+
| `label` | string | no | | |
|
|
792
|
+
| `position` | enum("top" \| "bottom") | yes | "bottom" | |
|
|
793
|
+
| `showPercentage` | boolean | yes | true | |
|
|
794
|
+
|
|
795
|
+
##### `ProgressRing`
|
|
796
|
+
|
|
797
|
+
- kind: `composite`
|
|
798
|
+
- category: `data`
|
|
799
|
+
- internal: `false`
|
|
800
|
+
- children: `no`
|
|
801
|
+
- description: Circular progress indicator (SVG) that animates from 0 to percentage over duration
|
|
802
|
+
- llmGuidance: Use for completion and goals. size 160-260 is typical. showLabel displays the percentage.
|
|
803
|
+
|
|
804
|
+
Props:
|
|
805
|
+
|
|
806
|
+
| Prop | Type | Required | Default | Notes |
|
|
807
|
+
| - | - | - | - | - |
|
|
808
|
+
| `backgroundColor` | string | yes | "rgba(255,255,255,0.2)" | |
|
|
809
|
+
| `color` | string | yes | "#00FF00" | |
|
|
810
|
+
| `percentage` | number | yes | | min=0, max=100 |
|
|
811
|
+
| `showLabel` | boolean | yes | true | |
|
|
812
|
+
| `size` | number | yes | 200 | min=100, max=500 |
|
|
813
|
+
| `strokeWidth` | number | yes | 20 | min=5, max=50 |
|
|
814
|
+
|
|
815
|
+
#### Category: `image`
|
|
816
|
+
|
|
817
|
+
##### `Image`
|
|
818
|
+
|
|
819
|
+
- kind: `primitive`
|
|
820
|
+
- category: `image`
|
|
821
|
+
- internal: `false`
|
|
822
|
+
- children: `no`
|
|
823
|
+
- description: Full-frame image with object-fit options
|
|
824
|
+
- llmGuidance: Use Image for pictures and backgrounds. Use fit="cover" for full-bleed, fit="contain" to avoid cropping.
|
|
825
|
+
|
|
826
|
+
Props:
|
|
827
|
+
|
|
828
|
+
| Prop | Type | Required | Default | Notes |
|
|
829
|
+
| - | - | - | - | - |
|
|
830
|
+
| `borderRadius` | number | yes | 0 | min=0 |
|
|
831
|
+
| `fit` | enum("cover" \| "contain") | yes | "cover" | |
|
|
832
|
+
| `opacity` | number | yes | 1 | min=0, max=1 |
|
|
833
|
+
| `src` | string | yes | | minLength=1 |
|
|
834
|
+
|
|
835
|
+
##### `ImageCollage`
|
|
836
|
+
|
|
837
|
+
- kind: `composite`
|
|
838
|
+
- category: `image`
|
|
839
|
+
- internal: `false`
|
|
840
|
+
- children: `no`
|
|
841
|
+
- description: Collage of multiple images in a grid/stack/scatter layout with staggered entrances
|
|
842
|
+
- llmGuidance: Use 2-6 images for best results. layout="grid" is clean; "scatter" is energetic.
|
|
843
|
+
|
|
844
|
+
Props:
|
|
845
|
+
|
|
846
|
+
| Prop | Type | Required | Default | Notes |
|
|
847
|
+
| - | - | - | - | - |
|
|
848
|
+
| `images` | array<object> | yes | | minItems=2, maxItems=9 |
|
|
849
|
+
| `layout` | enum("grid" \| "stack" \| "scatter") | yes | "grid" | |
|
|
850
|
+
| `stagger` | integer | yes | 5 | min=2, max=10 |
|
|
851
|
+
|
|
852
|
+
##### `ImageReveal`
|
|
853
|
+
|
|
854
|
+
- kind: `composite`
|
|
855
|
+
- category: `image`
|
|
856
|
+
- internal: `false`
|
|
857
|
+
- children: `no`
|
|
858
|
+
- description: Reveals an image with wipe/expand/iris entrance effects
|
|
859
|
+
- llmGuidance: Use wipe for directional reveals, expand for subtle pop-in, iris for circular mask openings.
|
|
860
|
+
|
|
861
|
+
Props:
|
|
862
|
+
|
|
863
|
+
| Prop | Type | Required | Default | Notes |
|
|
864
|
+
| - | - | - | - | - |
|
|
865
|
+
| `direction` | enum("left" \| "right" \| "top" \| "bottom" \| "center") | yes | "left" | |
|
|
866
|
+
| `revealType` | enum("wipe" \| "expand" \| "iris") | yes | "wipe" | |
|
|
867
|
+
| `src` | string | yes | | minLength=1 |
|
|
868
|
+
|
|
869
|
+
##### `ImageSequence`
|
|
870
|
+
|
|
871
|
+
- kind: `composite`
|
|
872
|
+
- category: `image`
|
|
873
|
+
- internal: `false`
|
|
874
|
+
- children: `no`
|
|
875
|
+
- description: Plays a numbered image sequence (frame-by-frame)
|
|
876
|
+
- llmGuidance: Use for exported sprite sequences. basePath can be /assets/seq and filePattern like img_{frame}.png.
|
|
877
|
+
|
|
878
|
+
Props:
|
|
879
|
+
|
|
880
|
+
| Prop | Type | Required | Default | Notes |
|
|
881
|
+
| - | - | - | - | - |
|
|
882
|
+
| `basePath` | string | yes | | minLength=1 |
|
|
883
|
+
| `filePattern` | string | yes | "frame_{frame}.png" | |
|
|
884
|
+
| `fps` | integer | yes | 30 | min=1, max=120 |
|
|
885
|
+
| `frameCount` | integer | yes | | max=9007199254740991 |
|
|
886
|
+
|
|
887
|
+
##### `ImageWithCaption`
|
|
888
|
+
|
|
889
|
+
- kind: `composite`
|
|
890
|
+
- category: `image`
|
|
891
|
+
- internal: `false`
|
|
892
|
+
- children: `no`
|
|
893
|
+
- description: Image with a caption strip (top/bottom) or overlay caption
|
|
894
|
+
- llmGuidance: Use overlay for quotes/testimonials over photos. Use bottom for standard captions.
|
|
895
|
+
|
|
896
|
+
Props:
|
|
897
|
+
|
|
898
|
+
| Prop | Type | Required | Default | Notes |
|
|
899
|
+
| - | - | - | - | - |
|
|
900
|
+
| `caption` | string | yes | | maxLength=200 |
|
|
901
|
+
| `captionPosition` | enum("top" \| "bottom" \| "overlay") | yes | "bottom" | |
|
|
902
|
+
| `captionStyle` | object | no | | additionalProperties=false |
|
|
903
|
+
| `src` | string | yes | | minLength=1 |
|
|
904
|
+
|
|
905
|
+
##### `KenBurnsImage`
|
|
906
|
+
|
|
907
|
+
- kind: `composite`
|
|
908
|
+
- category: `image`
|
|
909
|
+
- internal: `false`
|
|
910
|
+
- children: `no`
|
|
911
|
+
- description: Slow zoom and pan (Ken Burns effect) for a still image
|
|
912
|
+
- llmGuidance: Classic documentary-style motion. startScale 1 -> endScale 1.2 is subtle; add panDirection for extra movement.
|
|
913
|
+
|
|
914
|
+
Props:
|
|
915
|
+
|
|
916
|
+
| Prop | Type | Required | Default | Notes |
|
|
917
|
+
| - | - | - | - | - |
|
|
918
|
+
| `endScale` | number | yes | 1.2 | min=1, max=2 |
|
|
919
|
+
| `panAmount` | number | yes | 50 | min=0, max=100 |
|
|
920
|
+
| `panDirection` | enum("none" \| "left" \| "right" \| "up" \| "down") | yes | "none" | |
|
|
921
|
+
| `src` | string | yes | | minLength=1 |
|
|
922
|
+
| `startScale` | number | yes | 1 | min=1, max=2 |
|
|
923
|
+
|
|
924
|
+
#### Category: `layout`
|
|
925
|
+
|
|
926
|
+
##### `Box`
|
|
927
|
+
|
|
928
|
+
- kind: `primitive`
|
|
929
|
+
- category: `layout`
|
|
930
|
+
- internal: `false`
|
|
931
|
+
- children: `yes`
|
|
932
|
+
- description: Flow container for layout and backgrounds (layout-safe)
|
|
933
|
+
- llmGuidance: Use Box as a container inside Grid/Stack. Box participates in layout flow. For x/y positioning, use Frame.
|
|
934
|
+
|
|
935
|
+
Props:
|
|
936
|
+
|
|
937
|
+
| Prop | Type | Required | Default | Notes |
|
|
938
|
+
| - | - | - | - | - |
|
|
939
|
+
| `backgroundColor` | string | no | | |
|
|
940
|
+
| `borderRadius` | number | yes | 0 | min=0 |
|
|
941
|
+
| `height` | number | no | | |
|
|
942
|
+
| `opacity` | number | yes | 1 | min=0, max=1 |
|
|
943
|
+
| `padding` | number | yes | 0 | min=0 |
|
|
944
|
+
| `width` | number | no | | |
|
|
945
|
+
|
|
946
|
+
##### `CardStack`
|
|
947
|
+
|
|
948
|
+
- kind: `composite`
|
|
949
|
+
- category: `layout`
|
|
950
|
+
- internal: `false`
|
|
951
|
+
- children: `no`
|
|
952
|
+
- description: Sequential stacked cards (2-5) with flip/slide/fade transitions
|
|
953
|
+
- llmGuidance: Use for steps/features. displayDuration is frames per card.
|
|
954
|
+
|
|
955
|
+
Props:
|
|
956
|
+
|
|
957
|
+
| Prop | Type | Required | Default | Notes |
|
|
958
|
+
| - | - | - | - | - |
|
|
959
|
+
| `cards` | array<object> | yes | | minItems=2, maxItems=5 |
|
|
960
|
+
| `displayDuration` | integer | yes | 90 | min=30, max=150 |
|
|
961
|
+
| `transition` | enum("flip" \| "slide" \| "fade") | yes | "flip" | |
|
|
962
|
+
|
|
963
|
+
##### `Frame`
|
|
964
|
+
|
|
965
|
+
- kind: `primitive`
|
|
966
|
+
- category: `layout`
|
|
967
|
+
- internal: `false`
|
|
968
|
+
- children: `yes`
|
|
969
|
+
- description: Absolute-positioned container (x/y placement)
|
|
970
|
+
- llmGuidance: Use Frame for precise pixel placement (x/y). Use Box for normal layout flow inside Grid/Stack.
|
|
971
|
+
|
|
972
|
+
Props:
|
|
973
|
+
|
|
974
|
+
| Prop | Type | Required | Default | Notes |
|
|
975
|
+
| - | - | - | - | - |
|
|
976
|
+
| `backgroundColor` | string | no | | |
|
|
977
|
+
| `borderRadius` | number | yes | 0 | min=0 |
|
|
978
|
+
| `height` | number | no | | |
|
|
979
|
+
| `opacity` | number | yes | 1 | min=0, max=1 |
|
|
980
|
+
| `padding` | number | yes | 0 | min=0 |
|
|
981
|
+
| `width` | number | no | | |
|
|
982
|
+
| `x` | number | yes | 0 | |
|
|
983
|
+
| `y` | number | yes | 0 | |
|
|
984
|
+
|
|
985
|
+
##### `Grid`
|
|
986
|
+
|
|
987
|
+
- kind: `primitive`
|
|
988
|
+
- category: `layout`
|
|
989
|
+
- internal: `false`
|
|
990
|
+
- children: `yes`
|
|
991
|
+
- description: Grid layout container with configurable rows/columns
|
|
992
|
+
- llmGuidance: Use Grid for photo collages and dashboards. Provide exactly rows*columns children when possible.
|
|
993
|
+
|
|
994
|
+
Props:
|
|
995
|
+
|
|
996
|
+
| Prop | Type | Required | Default | Notes |
|
|
997
|
+
| - | - | - | - | - |
|
|
998
|
+
| `align` | enum("start" \| "center" \| "end" \| "stretch") | yes | "stretch" | |
|
|
999
|
+
| `columns` | integer | yes | 2 | min=1, max=12 |
|
|
1000
|
+
| `gap` | number | yes | 24 | min=0 |
|
|
1001
|
+
| `justify` | enum("start" \| "center" \| "end" \| "stretch") | yes | "stretch" | |
|
|
1002
|
+
| `padding` | number | yes | 0 | min=0 |
|
|
1003
|
+
| `rows` | integer | yes | 1 | min=1, max=12 |
|
|
1004
|
+
|
|
1005
|
+
##### `GridLayout`
|
|
1006
|
+
|
|
1007
|
+
- kind: `composite`
|
|
1008
|
+
- category: `layout`
|
|
1009
|
+
- internal: `false`
|
|
1010
|
+
- children: `yes (1..∞)`
|
|
1011
|
+
- description: Simple responsive grid layout for child components
|
|
1012
|
+
- llmGuidance: Use for dashboards and collages. 2x2 is a good default for 4 items.
|
|
1013
|
+
|
|
1014
|
+
Props:
|
|
1015
|
+
|
|
1016
|
+
| Prop | Type | Required | Default | Notes |
|
|
1017
|
+
| - | - | - | - | - |
|
|
1018
|
+
| `columns` | integer | yes | 2 | min=1, max=4 |
|
|
1019
|
+
| `gap` | number | yes | 20 | min=0, max=50 |
|
|
1020
|
+
| `padding` | number | yes | 40 | min=0, max=100 |
|
|
1021
|
+
| `rows` | integer | yes | 2 | min=1, max=4 |
|
|
1022
|
+
|
|
1023
|
+
##### `Layer`
|
|
1024
|
+
|
|
1025
|
+
- kind: `primitive`
|
|
1026
|
+
- category: `layout`
|
|
1027
|
+
- internal: `false`
|
|
1028
|
+
- children: `yes (1..∞)`
|
|
1029
|
+
- description: One overlay layer with explicit zIndex inside Layers
|
|
1030
|
+
- llmGuidance: Use Layer inside Layers to control stacking. Put exactly one child in a Layer (recommended).
|
|
1031
|
+
|
|
1032
|
+
Props:
|
|
1033
|
+
|
|
1034
|
+
| Prop | Type | Required | Default | Notes |
|
|
1035
|
+
| - | - | - | - | - |
|
|
1036
|
+
| `inset` | number | yes | 0 | min=0 |
|
|
1037
|
+
| `opacity` | number | yes | 1 | min=0, max=1 |
|
|
1038
|
+
| `pointerEvents` | enum("none" \| "auto") | yes | "none" | |
|
|
1039
|
+
| `zIndex` | integer | yes | 0 | min=-9007199254740991, max=9007199254740991 |
|
|
1040
|
+
|
|
1041
|
+
##### `Layers`
|
|
382
1042
|
|
|
383
|
-
-
|
|
384
|
-
-
|
|
1043
|
+
- kind: `primitive`
|
|
1044
|
+
- category: `layout`
|
|
1045
|
+
- internal: `false`
|
|
1046
|
+
- children: `yes (1..∞)`
|
|
1047
|
+
- description: Overlay container for stacking children (use Layer for zIndex)
|
|
1048
|
+
- llmGuidance: Use Layers to stack background/content/overlays. Prefer Layer children with explicit zIndex.
|
|
385
1049
|
|
|
386
|
-
|
|
1050
|
+
Props:
|
|
1051
|
+
|
|
1052
|
+
| Prop | Type | Required | Default | Notes |
|
|
1053
|
+
| - | - | - | - | - |
|
|
1054
|
+
| `overflow` | enum("visible" \| "hidden") | yes | "visible" | |
|
|
1055
|
+
|
|
1056
|
+
##### `Scene`
|
|
1057
|
+
|
|
1058
|
+
- kind: `primitive`
|
|
1059
|
+
- category: `layout`
|
|
1060
|
+
- internal: `false`
|
|
1061
|
+
- children: `yes`
|
|
1062
|
+
- description: Scene container with a background and nested children
|
|
1063
|
+
- llmGuidance: Use Scene to define a segment of the video. Scene timings must be sequential with no gaps. Put Text and Audio as children.
|
|
1064
|
+
|
|
1065
|
+
Props:
|
|
1066
|
+
|
|
1067
|
+
| Prop | Type | Required | Default | Notes |
|
|
1068
|
+
| - | - | - | - | - |
|
|
1069
|
+
| `background` | oneOf(object \| object \| object) | yes | | |
|
|
1070
|
+
|
|
1071
|
+
##### `Segment`
|
|
1072
|
+
|
|
1073
|
+
- kind: `primitive`
|
|
1074
|
+
- category: `layout`
|
|
1075
|
+
- internal: `true`
|
|
1076
|
+
- children: `yes (1..1)`
|
|
1077
|
+
- description: Internal segment wrapper (used by v2 segments compiler)
|
|
1078
|
+
|
|
1079
|
+
Props:
|
|
1080
|
+
|
|
1081
|
+
| Prop | Type | Required | Default | Notes |
|
|
1082
|
+
| - | - | - | - | - |
|
|
1083
|
+
| `enterTransition` | object | no | | additionalProperties=false |
|
|
1084
|
+
| `exitTransition` | object | no | | additionalProperties=false |
|
|
1085
|
+
|
|
1086
|
+
##### `Shape`
|
|
1087
|
+
|
|
1088
|
+
- kind: `primitive`
|
|
1089
|
+
- category: `layout`
|
|
1090
|
+
- internal: `false`
|
|
1091
|
+
- children: `no`
|
|
1092
|
+
- description: Simple rect/circle shape for UI accents
|
|
1093
|
+
- llmGuidance: Use Shape for lines, badges, and simple UI blocks. Use circle for dots and rings.
|
|
1094
|
+
|
|
1095
|
+
Props:
|
|
1096
|
+
|
|
1097
|
+
| Prop | Type | Required | Default | Notes |
|
|
1098
|
+
| - | - | - | - | - |
|
|
1099
|
+
| `fill` | string | yes | "#FFFFFF" | |
|
|
1100
|
+
| `height` | number | yes | 100 | |
|
|
1101
|
+
| `opacity` | number | yes | 1 | min=0, max=1 |
|
|
1102
|
+
| `shape` | enum("rect" \| "circle") | yes | "rect" | |
|
|
1103
|
+
| `strokeColor` | string | no | | |
|
|
1104
|
+
| `strokeWidth` | number | yes | 0 | min=0 |
|
|
1105
|
+
| `width` | number | yes | 100 | |
|
|
1106
|
+
| `x` | number | yes | 0 | |
|
|
1107
|
+
| `y` | number | yes | 0 | |
|
|
1108
|
+
|
|
1109
|
+
##### `SplitScreen`
|
|
1110
|
+
|
|
1111
|
+
- kind: `composite`
|
|
1112
|
+
- category: `layout`
|
|
1113
|
+
- internal: `false`
|
|
1114
|
+
- children: `yes (2..2)`
|
|
1115
|
+
- description: Two-panel split screen layout
|
|
1116
|
+
- llmGuidance: Provide exactly 2 children. Use orientation="vertical" for left/right and "horizontal" for top/bottom.
|
|
1117
|
+
|
|
1118
|
+
Props:
|
|
1119
|
+
|
|
1120
|
+
| Prop | Type | Required | Default | Notes |
|
|
1121
|
+
| - | - | - | - | - |
|
|
1122
|
+
| `dividerColor` | string | no | | |
|
|
1123
|
+
| `gap` | number | yes | 48 | min=0 |
|
|
1124
|
+
| `orientation` | enum("vertical" \| "horizontal") | yes | "vertical" | |
|
|
1125
|
+
| `padding` | number | yes | 80 | min=0 |
|
|
1126
|
+
| `split` | number | yes | 0.5 | min=0.1, max=0.9 |
|
|
1127
|
+
|
|
1128
|
+
##### `Stack`
|
|
1129
|
+
|
|
1130
|
+
- kind: `primitive`
|
|
1131
|
+
- category: `layout`
|
|
1132
|
+
- internal: `false`
|
|
1133
|
+
- children: `yes`
|
|
1134
|
+
- description: Flexbox stack layout (row/column) with gap and alignment
|
|
1135
|
+
- llmGuidance: Use Stack to arrange child components in a row or column without manual positioning.
|
|
1136
|
+
|
|
1137
|
+
Props:
|
|
1138
|
+
|
|
1139
|
+
| Prop | Type | Required | Default | Notes |
|
|
1140
|
+
| - | - | - | - | - |
|
|
1141
|
+
| `align` | enum("start" \| "center" \| "end" \| "stretch") | yes | "center" | |
|
|
1142
|
+
| `direction` | enum("row" \| "column") | yes | "column" | |
|
|
1143
|
+
| `gap` | number | yes | 24 | min=0 |
|
|
1144
|
+
| `justify` | enum("start" \| "center" \| "end" \| "between") | yes | "center" | |
|
|
1145
|
+
| `padding` | number | yes | 0 | min=0 |
|
|
1146
|
+
|
|
1147
|
+
##### `ThirdLowerBanner`
|
|
1148
|
+
|
|
1149
|
+
- kind: `composite`
|
|
1150
|
+
- category: `layout`
|
|
1151
|
+
- internal: `false`
|
|
1152
|
+
- children: `no`
|
|
1153
|
+
- description: Broadcast-style lower-third banner with name/title and optional avatar
|
|
1154
|
+
- llmGuidance: Use for speaker introductions. name = big label, title = smaller subtitle. showAvatar + avatarSrc for profile image.
|
|
1155
|
+
|
|
1156
|
+
Props:
|
|
1157
|
+
|
|
1158
|
+
| Prop | Type | Required | Default | Notes |
|
|
1159
|
+
| - | - | - | - | - |
|
|
1160
|
+
| `accentColor` | string | yes | "#FF0000" | |
|
|
1161
|
+
| `avatarSrc` | string | no | | |
|
|
1162
|
+
| `backgroundColor` | string | yes | "rgba(0,0,0,0.8)" | |
|
|
1163
|
+
| `name` | string | yes | | maxLength=50 |
|
|
1164
|
+
| `primaryColor` | string | yes | "#FFFFFF" | |
|
|
1165
|
+
| `secondaryColor` | string | yes | "#CCCCCC" | |
|
|
1166
|
+
| `showAvatar` | boolean | yes | false | |
|
|
1167
|
+
| `title` | string | yes | | maxLength=100 |
|
|
1168
|
+
|
|
1169
|
+
#### Category: `media`
|
|
1170
|
+
|
|
1171
|
+
##### `Audio`
|
|
1172
|
+
|
|
1173
|
+
- kind: `primitive`
|
|
1174
|
+
- category: `media`
|
|
1175
|
+
- internal: `false`
|
|
1176
|
+
- children: `no`
|
|
1177
|
+
- description: Plays an audio file with optional trimming and fade in/out
|
|
1178
|
+
- llmGuidance: Use for background music or sound effects. Prefer short clips for SFX. Use fadeIn/fadeOut (in frames) for smoother audio starts/ends.
|
|
1179
|
+
|
|
1180
|
+
Props:
|
|
1181
|
+
|
|
1182
|
+
| Prop | Type | Required | Default | Notes |
|
|
1183
|
+
| - | - | - | - | - |
|
|
1184
|
+
| `fadeIn` | integer | yes | 0 | min=0, max=9007199254740991 |
|
|
1185
|
+
| `fadeOut` | integer | yes | 0 | min=0, max=9007199254740991 |
|
|
1186
|
+
| `src` | string | yes | | |
|
|
1187
|
+
| `startFrom` | integer | yes | 0 | min=0, max=9007199254740991 |
|
|
1188
|
+
| `volume` | number | yes | 1 | min=0, max=1 |
|
|
1189
|
+
|
|
1190
|
+
##### `Video`
|
|
1191
|
+
|
|
1192
|
+
- kind: `primitive`
|
|
1193
|
+
- category: `media`
|
|
1194
|
+
- internal: `false`
|
|
1195
|
+
- children: `no`
|
|
1196
|
+
- description: Full-frame video with object-fit options
|
|
1197
|
+
- llmGuidance: Use Video for B-roll. Keep videos short and muted unless you intentionally want audio.
|
|
1198
|
+
|
|
1199
|
+
Props:
|
|
1200
|
+
|
|
1201
|
+
| Prop | Type | Required | Default | Notes |
|
|
1202
|
+
| - | - | - | - | - |
|
|
1203
|
+
| `borderRadius` | number | yes | 0 | min=0 |
|
|
1204
|
+
| `fit` | enum("cover" \| "contain") | yes | "cover" | |
|
|
1205
|
+
| `muted` | boolean | yes | true | |
|
|
1206
|
+
| `opacity` | number | yes | 1 | min=0, max=1 |
|
|
1207
|
+
| `src` | string | yes | | minLength=1 |
|
|
1208
|
+
|
|
1209
|
+
##### `VideoWithOverlay`
|
|
1210
|
+
|
|
1211
|
+
- kind: `composite`
|
|
1212
|
+
- category: `media`
|
|
1213
|
+
- internal: `false`
|
|
1214
|
+
- children: `no`
|
|
1215
|
+
- description: Video background with an optional overlay (text/logo/gradient)
|
|
1216
|
+
- llmGuidance: Use gradient overlay to improve text readability. Set volume=0 to mute.
|
|
1217
|
+
|
|
1218
|
+
Props:
|
|
1219
|
+
|
|
1220
|
+
| Prop | Type | Required | Default | Notes |
|
|
1221
|
+
| - | - | - | - | - |
|
|
1222
|
+
| `overlay` | object | no | | additionalProperties=false |
|
|
1223
|
+
| `playbackRate` | number | yes | 1 | min=0.5, max=2 |
|
|
1224
|
+
| `src` | string | yes | | minLength=1 |
|
|
1225
|
+
| `volume` | number | yes | 1 | min=0, max=1 |
|
|
1226
|
+
|
|
1227
|
+
#### Category: `social`
|
|
1228
|
+
|
|
1229
|
+
##### `InstagramStory`
|
|
1230
|
+
|
|
1231
|
+
- kind: `composite`
|
|
1232
|
+
- category: `social`
|
|
1233
|
+
- internal: `false`
|
|
1234
|
+
- children: `no`
|
|
1235
|
+
- description: Instagram story-style layout with profile header, text overlay, and optional sticker
|
|
1236
|
+
- llmGuidance: Best with 1080x1920 (9:16). Use backgroundImage + short text + optional sticker for mobile-style content.
|
|
1237
|
+
|
|
1238
|
+
Props:
|
|
1239
|
+
|
|
1240
|
+
| Prop | Type | Required | Default | Notes |
|
|
1241
|
+
| - | - | - | - | - |
|
|
1242
|
+
| `backgroundColor` | string | yes | "#000000" | |
|
|
1243
|
+
| `backgroundImage` | string | no | | |
|
|
1244
|
+
| `musicTrack` | string | no | | |
|
|
1245
|
+
| `profilePic` | string | no | | |
|
|
1246
|
+
| `sticker` | enum("none" \| "poll" \| "question" \| "countdown") | yes | "none" | |
|
|
1247
|
+
| `text` | string | no | | maxLength=100 |
|
|
1248
|
+
| `username` | string | no | | |
|
|
1249
|
+
|
|
1250
|
+
##### `TikTokCaption`
|
|
1251
|
+
|
|
1252
|
+
- kind: `composite`
|
|
1253
|
+
- category: `social`
|
|
1254
|
+
- internal: `false`
|
|
1255
|
+
- children: `no`
|
|
1256
|
+
- description: TikTok-style captions with stroke and optional word highlighting
|
|
1257
|
+
- llmGuidance: Always keep strokeWidth>=2 for readability. highlightStyle="word" or "bounce" makes captions feel dynamic.
|
|
1258
|
+
|
|
1259
|
+
Props:
|
|
1260
|
+
|
|
1261
|
+
| Prop | Type | Required | Default | Notes |
|
|
1262
|
+
| - | - | - | - | - |
|
|
1263
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1264
|
+
| `fontSize` | number | yes | 48 | min=12, max=120 |
|
|
1265
|
+
| `highlightStyle` | enum("word" \| "bounce" \| "none") | yes | "word" | |
|
|
1266
|
+
| `maxWidthPct` | number | yes | 0.92 | min=0.1, max=1 |
|
|
1267
|
+
| `position` | enum("center" \| "bottom") | yes | "center" | |
|
|
1268
|
+
| `safeInsetPct` | number | yes | 0.06 | min=0, max=0.25 |
|
|
1269
|
+
| `strokeColor` | string | yes | "#000000" | |
|
|
1270
|
+
| `strokeWidth` | number | yes | 3 | min=0, max=10 |
|
|
1271
|
+
| `text` | string | yes | | maxLength=150 |
|
|
1272
|
+
|
|
1273
|
+
##### `TwitterCard`
|
|
1274
|
+
|
|
1275
|
+
- kind: `composite`
|
|
1276
|
+
- category: `social`
|
|
1277
|
+
- internal: `false`
|
|
1278
|
+
- children: `no`
|
|
1279
|
+
- description: Twitter/X post card layout with author header and optional image
|
|
1280
|
+
- llmGuidance: Use for announcements/testimonials. Keep tweet short for readability.
|
|
1281
|
+
|
|
1282
|
+
Props:
|
|
1283
|
+
|
|
1284
|
+
| Prop | Type | Required | Default | Notes |
|
|
1285
|
+
| - | - | - | - | - |
|
|
1286
|
+
| `author` | string | yes | | minLength=1 |
|
|
1287
|
+
| `avatarSrc` | string | no | | |
|
|
1288
|
+
| `handle` | string | yes | | minLength=1 |
|
|
1289
|
+
| `image` | string | no | | |
|
|
1290
|
+
| `timestamp` | string | no | | |
|
|
1291
|
+
| `tweet` | string | yes | | maxLength=280 |
|
|
1292
|
+
| `verified` | boolean | yes | false | |
|
|
1293
|
+
|
|
1294
|
+
##### `YouTubeThumbnail`
|
|
1295
|
+
|
|
1296
|
+
- kind: `composite`
|
|
1297
|
+
- category: `social`
|
|
1298
|
+
- internal: `false`
|
|
1299
|
+
- children: `no`
|
|
1300
|
+
- description: YouTube-style thumbnail layout (16:9) with bold title and optional face cutout
|
|
1301
|
+
- llmGuidance: Use 1280x720 video size. Keep title short and high-contrast. style="bold" is classic thumbnail.
|
|
1302
|
+
|
|
1303
|
+
Props:
|
|
1304
|
+
|
|
1305
|
+
| Prop | Type | Required | Default | Notes |
|
|
1306
|
+
| - | - | - | - | - |
|
|
1307
|
+
| `accentColor` | string | yes | "#FF0000" | |
|
|
1308
|
+
| `backgroundImage` | string | yes | | minLength=1 |
|
|
1309
|
+
| `style` | enum("bold" \| "minimal" \| "dramatic") | yes | "bold" | |
|
|
1310
|
+
| `subtitle` | string | no | | maxLength=40 |
|
|
1311
|
+
| `thumbnailFace` | string | no | | |
|
|
1312
|
+
| `title` | string | yes | | maxLength=60 |
|
|
387
1313
|
|
|
388
|
-
|
|
1314
|
+
#### Category: `text`
|
|
1315
|
+
|
|
1316
|
+
##### `CountUpText`
|
|
1317
|
+
|
|
1318
|
+
- kind: `composite`
|
|
1319
|
+
- category: `text`
|
|
1320
|
+
- internal: `false`
|
|
1321
|
+
- children: `no`
|
|
1322
|
+
- description: Counts from a start value to an end value with formatting options
|
|
1323
|
+
- llmGuidance: Use for metrics. format="currency" adds $, format="percentage" multiplies by 100 and adds %.
|
|
1324
|
+
|
|
1325
|
+
Props:
|
|
1326
|
+
|
|
1327
|
+
| Prop | Type | Required | Default | Notes |
|
|
1328
|
+
| - | - | - | - | - |
|
|
1329
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1330
|
+
| `decimals` | integer | yes | 0 | min=0, max=4 |
|
|
1331
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1332
|
+
| `fontSize` | number | yes | 72 | min=8, max=240 |
|
|
1333
|
+
| `fontWeight` | integer | yes | 700 | min=100, max=900 |
|
|
1334
|
+
| `format` | enum("integer" \| "decimal" \| "currency" \| "percentage") | yes | "integer" | |
|
|
1335
|
+
| `from` | number | yes | 0 | |
|
|
1336
|
+
| `position` | enum("top" \| "center" \| "bottom") | yes | "center" | |
|
|
1337
|
+
| `prefix` | string | no | | |
|
|
1338
|
+
| `suffix` | string | no | | |
|
|
1339
|
+
| `to` | number | yes | 100 | |
|
|
1340
|
+
|
|
1341
|
+
##### `GlitchText`
|
|
1342
|
+
|
|
1343
|
+
- kind: `composite`
|
|
1344
|
+
- category: `text`
|
|
1345
|
+
- internal: `false`
|
|
1346
|
+
- children: `no`
|
|
1347
|
+
- description: Cyberpunk-style glitch text with RGB split jitter
|
|
1348
|
+
- llmGuidance: Use for tech/error moments. intensity 1-3 subtle, 7-10 extreme. glitchDuration is frames at start.
|
|
1349
|
+
|
|
1350
|
+
Props:
|
|
1351
|
+
|
|
1352
|
+
| Prop | Type | Required | Default | Notes |
|
|
1353
|
+
| - | - | - | - | - |
|
|
1354
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1355
|
+
| `content` | string | yes | | maxLength=100 |
|
|
1356
|
+
| `fontFamily` | string | yes | "monospace" | |
|
|
1357
|
+
| `fontSize` | number | yes | 72 | min=8, max=240 |
|
|
1358
|
+
| `glitchDuration` | integer | yes | 10 | min=5, max=30 |
|
|
1359
|
+
| `intensity` | integer | yes | 5 | min=1, max=10 |
|
|
1360
|
+
| `position` | enum("top" \| "center" \| "bottom") | yes | "center" | |
|
|
1361
|
+
|
|
1362
|
+
##### `KineticTypography`
|
|
1363
|
+
|
|
1364
|
+
- kind: `composite`
|
|
1365
|
+
- category: `text`
|
|
1366
|
+
- internal: `false`
|
|
1367
|
+
- children: `no`
|
|
1368
|
+
- description: Rhythmic single-word kinetic typography driven by a timing array
|
|
1369
|
+
- llmGuidance: Provide timing frames for when each word appears. Use emphasis="giant" sparingly for impact.
|
|
1370
|
+
|
|
1371
|
+
Props:
|
|
1372
|
+
|
|
1373
|
+
| Prop | Type | Required | Default | Notes |
|
|
1374
|
+
| - | - | - | - | - |
|
|
1375
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1376
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1377
|
+
| `fontSize` | number | yes | 48 | min=12, max=140 |
|
|
1378
|
+
| `timing` | array<integer> | yes | | minItems=1 |
|
|
1379
|
+
| `transition` | enum("fade" \| "scale" \| "slideLeft" \| "slideRight") | yes | "scale" | |
|
|
1380
|
+
| `words` | array<object> | yes | | minItems=1, maxItems=50 |
|
|
1381
|
+
|
|
1382
|
+
##### `OutlineText`
|
|
1383
|
+
|
|
1384
|
+
- kind: `composite`
|
|
1385
|
+
- category: `text`
|
|
1386
|
+
- internal: `false`
|
|
1387
|
+
- children: `no`
|
|
1388
|
+
- description: Outlined title text with simple draw/fill animation
|
|
1389
|
+
- llmGuidance: Use for bold titles. animation="draw" emphasizes the outline; animation="fill" reveals the fill color.
|
|
1390
|
+
|
|
1391
|
+
Props:
|
|
1392
|
+
|
|
1393
|
+
| Prop | Type | Required | Default | Notes |
|
|
1394
|
+
| - | - | - | - | - |
|
|
1395
|
+
| `animation` | enum("draw" \| "fill") | yes | "draw" | |
|
|
1396
|
+
| `content` | string | yes | | maxLength=50 |
|
|
1397
|
+
| `fillColor` | string | yes | "#000000" | |
|
|
1398
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1399
|
+
| `fontSize` | number | yes | 96 | min=8, max=240 |
|
|
1400
|
+
| `fontWeight` | integer | yes | 800 | min=100, max=900 |
|
|
1401
|
+
| `outlineColor` | string | yes | "#FFFFFF" | |
|
|
1402
|
+
| `position` | enum("top" \| "center" \| "bottom") | yes | "center" | |
|
|
1403
|
+
| `strokeWidth` | number | yes | 3 | min=1, max=10 |
|
|
1404
|
+
|
|
1405
|
+
##### `SplitText`
|
|
1406
|
+
|
|
1407
|
+
- kind: `composite`
|
|
1408
|
+
- category: `text`
|
|
1409
|
+
- internal: `false`
|
|
1410
|
+
- children: `no`
|
|
1411
|
+
- description: Animated text where each word or letter enters with a staggered effect
|
|
1412
|
+
- llmGuidance: Use for titles. splitBy="word" is best for phrases; splitBy="letter" is dramatic for short words.
|
|
1413
|
+
|
|
1414
|
+
Props:
|
|
1415
|
+
|
|
1416
|
+
| Prop | Type | Required | Default | Notes |
|
|
1417
|
+
| - | - | - | - | - |
|
|
1418
|
+
| `animation` | enum("fade" \| "slideUp" \| "slideDown" \| "scale" \| "rotate") | yes | "slideUp" | |
|
|
1419
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1420
|
+
| `content` | string | yes | | maxLength=200 |
|
|
1421
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1422
|
+
| `fontSize` | number | yes | 48 | min=8, max=200 |
|
|
1423
|
+
| `maxWidthPct` | number | yes | 0.9 | min=0.1, max=1 |
|
|
1424
|
+
| `position` | enum("top" \| "center" \| "bottom") | yes | "center" | |
|
|
1425
|
+
| `safeInsetPct` | number | yes | 0.06 | min=0, max=0.25 |
|
|
1426
|
+
| `splitBy` | enum("word" \| "letter") | yes | "word" | |
|
|
1427
|
+
| `stagger` | integer | yes | 3 | min=1, max=10 |
|
|
1428
|
+
|
|
1429
|
+
##### `SubtitleText`
|
|
1430
|
+
|
|
1431
|
+
- kind: `composite`
|
|
1432
|
+
- category: `text`
|
|
1433
|
+
- internal: `false`
|
|
1434
|
+
- children: `no`
|
|
1435
|
+
- description: Caption/subtitle box with fade in/out and optional highlighted words
|
|
1436
|
+
- llmGuidance: Use for narration/captions. highlightWords helps emphasize key terms.
|
|
1437
|
+
|
|
1438
|
+
Props:
|
|
1439
|
+
|
|
1440
|
+
| Prop | Type | Required | Default | Notes |
|
|
1441
|
+
| - | - | - | - | - |
|
|
1442
|
+
| `backgroundColor` | string | yes | "rgba(0,0,0,0.7)" | |
|
|
1443
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1444
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1445
|
+
| `fontSize` | number | yes | 36 | min=12, max=80 |
|
|
1446
|
+
| `highlightWords` | array<string> | no | | |
|
|
1447
|
+
| `maxWidth` | number | yes | 800 | min=200, max=1200 |
|
|
1448
|
+
| `padding` | number | yes | 20 | min=0, max=80 |
|
|
1449
|
+
| `position` | enum("top" \| "bottom") | yes | "bottom" | |
|
|
1450
|
+
| `text` | string | yes | | maxLength=200 |
|
|
1451
|
+
|
|
1452
|
+
##### `Text`
|
|
1453
|
+
|
|
1454
|
+
- kind: `primitive`
|
|
1455
|
+
- category: `text`
|
|
1456
|
+
- internal: `false`
|
|
1457
|
+
- children: `no`
|
|
1458
|
+
- description: Displays animated text with positioning and animation options
|
|
1459
|
+
- llmGuidance: Use for titles, subtitles, captions. Keep content under 100 characters for readability. Position "center" works best for titles.
|
|
1460
|
+
|
|
1461
|
+
Props:
|
|
1462
|
+
|
|
1463
|
+
| Prop | Type | Required | Default | Notes |
|
|
1464
|
+
| - | - | - | - | - |
|
|
1465
|
+
| `animation` | enum("none" \| "fade" \| "slide" \| "zoom") | yes | "fade" | |
|
|
1466
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1467
|
+
| `content` | string | yes | | |
|
|
1468
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1469
|
+
| `fontSize` | number | yes | 48 | |
|
|
1470
|
+
| `maxWidthPct` | number | yes | 0.9 | min=0.1, max=1 |
|
|
1471
|
+
| `position` | enum("top" \| "center" \| "bottom" \| "left" \| "right") | yes | "center" | |
|
|
1472
|
+
| `safeInsetPct` | number | yes | 0.06 | min=0, max=0.25 |
|
|
1473
|
+
| `textAlign` | enum("left" \| "center" \| "right") | yes | "center" | |
|
|
1474
|
+
|
|
1475
|
+
##### `TypewriterText`
|
|
1476
|
+
|
|
1477
|
+
- kind: `composite`
|
|
1478
|
+
- category: `text`
|
|
1479
|
+
- internal: `false`
|
|
1480
|
+
- children: `no`
|
|
1481
|
+
- description: Character-by-character text reveal with optional blinking cursor
|
|
1482
|
+
- llmGuidance: Use for dramatic reveals and terminal-style text. speed ~1-2 is readable; 3-5 is fast.
|
|
1483
|
+
|
|
1484
|
+
Props:
|
|
1485
|
+
|
|
1486
|
+
| Prop | Type | Required | Default | Notes |
|
|
1487
|
+
| - | - | - | - | - |
|
|
1488
|
+
| `color` | string | yes | "#FFFFFF" | |
|
|
1489
|
+
| `content` | string | yes | | maxLength=500 |
|
|
1490
|
+
| `cursorColor` | string | yes | "#FFFFFF" | |
|
|
1491
|
+
| `fontFamily` | string | yes | "Inter" | |
|
|
1492
|
+
| `fontSize` | number | yes | 48 | min=8, max=200 |
|
|
1493
|
+
| `maxWidthPct` | number | yes | 0.9 | min=0.1, max=1 |
|
|
1494
|
+
| `position` | enum("top" \| "center" \| "bottom") | yes | "center" | |
|
|
1495
|
+
| `safeInsetPct` | number | yes | 0.06 | min=0, max=0.25 |
|
|
1496
|
+
| `showCursor` | boolean | yes | true | |
|
|
1497
|
+
| `speed` | number | yes | 2 | min=0.5, max=5 |
|
|
1498
|
+
|
|
1499
|
+
#### Category: `transition`
|
|
1500
|
+
|
|
1501
|
+
##### `CircularReveal`
|
|
1502
|
+
|
|
1503
|
+
- kind: `composite`
|
|
1504
|
+
- category: `transition`
|
|
1505
|
+
- internal: `false`
|
|
1506
|
+
- children: `yes (1..∞)`
|
|
1507
|
+
- description: Circular iris reveal/hide transition wrapper
|
|
1508
|
+
- llmGuidance: direction="open" reveals from center, direction="close" hides to a point. center controls origin.
|
|
1509
|
+
|
|
1510
|
+
Props:
|
|
1511
|
+
|
|
1512
|
+
| Prop | Type | Required | Default | Notes |
|
|
1513
|
+
| - | - | - | - | - |
|
|
1514
|
+
| `center` | object | no | | additionalProperties=false |
|
|
1515
|
+
| `direction` | enum("open" \| "close") | yes | "open" | |
|
|
1516
|
+
| `durationInFrames` | integer | yes | 30 | min=10, max=60 |
|
|
1517
|
+
| `phase` | enum("in" \| "out" \| "inOut") | yes | "inOut" | |
|
|
1518
|
+
|
|
1519
|
+
##### `FadeTransition`
|
|
1520
|
+
|
|
1521
|
+
- kind: `composite`
|
|
1522
|
+
- category: `transition`
|
|
1523
|
+
- internal: `false`
|
|
1524
|
+
- children: `yes (1..∞)`
|
|
1525
|
+
- description: Fade in/out wrapper (used for segment transitions and overlays)
|
|
1526
|
+
- llmGuidance: Use for gentle transitions. durationInFrames ~30 is standard.
|
|
1527
|
+
|
|
1528
|
+
Props:
|
|
1529
|
+
|
|
1530
|
+
| Prop | Type | Required | Default | Notes |
|
|
1531
|
+
| - | - | - | - | - |
|
|
1532
|
+
| `durationInFrames` | integer | yes | 30 | min=10, max=60 |
|
|
1533
|
+
| `easing` | enum("linear" \| "easeIn" \| "easeOut" \| "easeInOut") | yes | "easeInOut" | |
|
|
1534
|
+
| `phase` | enum("in" \| "out" \| "inOut") | yes | "inOut" | |
|
|
1535
|
+
|
|
1536
|
+
##### `SlideTransition`
|
|
1537
|
+
|
|
1538
|
+
- kind: `composite`
|
|
1539
|
+
- category: `transition`
|
|
1540
|
+
- internal: `false`
|
|
1541
|
+
- children: `yes (1..∞)`
|
|
1542
|
+
- description: Slide in/out wrapper (used for segment transitions and overlays)
|
|
1543
|
+
- llmGuidance: Use for more dynamic transitions. direction controls where content enters from.
|
|
1544
|
+
|
|
1545
|
+
Props:
|
|
1546
|
+
|
|
1547
|
+
| Prop | Type | Required | Default | Notes |
|
|
1548
|
+
| - | - | - | - | - |
|
|
1549
|
+
| `direction` | enum("left" \| "right" \| "up" \| "down") | yes | "left" | |
|
|
1550
|
+
| `distance` | integer | yes | 160 | min=1, max=2000 |
|
|
1551
|
+
| `durationInFrames` | integer | yes | 30 | min=10, max=60 |
|
|
1552
|
+
| `phase` | enum("in" \| "out" \| "inOut") | yes | "inOut" | |
|
|
1553
|
+
|
|
1554
|
+
##### `WipeTransition`
|
|
1555
|
+
|
|
1556
|
+
- kind: `composite`
|
|
1557
|
+
- category: `transition`
|
|
1558
|
+
- internal: `false`
|
|
1559
|
+
- children: `yes (1..∞)`
|
|
1560
|
+
- description: Directional wipe reveal/hide wrapper transition
|
|
1561
|
+
- llmGuidance: Use as a more stylized reveal. softEdge can make it feel less harsh.
|
|
1562
|
+
|
|
1563
|
+
Props:
|
|
1564
|
+
|
|
1565
|
+
| Prop | Type | Required | Default | Notes |
|
|
1566
|
+
| - | - | - | - | - |
|
|
1567
|
+
| `direction` | enum("left" \| "right" \| "up" \| "down" \| "diagonal") | yes | "right" | |
|
|
1568
|
+
| `durationInFrames` | integer | yes | 30 | min=10, max=60 |
|
|
1569
|
+
| `phase` | enum("in" \| "out" \| "inOut") | yes | "inOut" | |
|
|
1570
|
+
| `softEdge` | boolean | yes | false | |
|
|
1571
|
+
|
|
1572
|
+
##### `ZoomTransition`
|
|
1573
|
+
|
|
1574
|
+
- kind: `composite`
|
|
1575
|
+
- category: `transition`
|
|
1576
|
+
- internal: `false`
|
|
1577
|
+
- children: `yes (1..∞)`
|
|
1578
|
+
- description: Zoom in/out wrapper transition
|
|
1579
|
+
- llmGuidance: Use for energetic cuts. type="zoomIn" feels punchy; type="zoomOut" feels calmer.
|
|
1580
|
+
|
|
1581
|
+
Props:
|
|
1582
|
+
|
|
1583
|
+
| Prop | Type | Required | Default | Notes |
|
|
1584
|
+
| - | - | - | - | - |
|
|
1585
|
+
| `durationInFrames` | integer | yes | 30 | min=10, max=60 |
|
|
1586
|
+
| `phase` | enum("in" \| "out" \| "inOut") | yes | "inOut" | |
|
|
1587
|
+
| `type` | enum("zoomIn" \| "zoomOut") | yes | "zoomIn" | |
|
|
1588
|
+
|
|
1589
|
+
<!-- END GENERATED: COMPONENTS -->
|
|
1590
|
+
|
|
1591
|
+
## Examples (composition recipes)
|
|
1592
|
+
|
|
1593
|
+
The `examples/` directory contains validated IR JSON files you can use as starting points.
|
|
1594
|
+
|
|
1595
|
+
Quick validate:
|
|
1596
|
+
|
|
1597
|
+
```bash
|
|
1598
|
+
npx waves validate --in examples/basic.v2.json
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
Validate all examples:
|
|
1602
|
+
|
|
1603
|
+
```powershell
|
|
1604
|
+
Get-ChildItem examples -Filter *.v2.json | ForEach-Object { npx waves validate --in $_.FullName }
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
Render (writes an MP4; `examples/*.mp4` are gitignored):
|
|
1608
|
+
|
|
1609
|
+
```bash
|
|
1610
|
+
npx waves render --in examples/basic.v2.json --out examples/basic.v2.mp4 --codec h264 --crf 28 --concurrency 1
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
If an example references `/assets/...`, you must provide those files and pass `--publicDir`:
|
|
1614
|
+
|
|
1615
|
+
```bash
|
|
1616
|
+
npx waves render --in examples/intro-stats-outro.v2.json --out examples/intro-stats-outro.v2.mp4 --publicDir ./public
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
### Example 0: basic starter (segments + composites)
|
|
1620
|
+
|
|
1621
|
+
File: `examples/basic.v2.json`
|
|
1622
|
+
|
|
1623
|
+
This is the default starter IR used by `waves write-ir --template basic`. It demonstrates:
|
|
1624
|
+
|
|
1625
|
+
- segments mode (recommended)
|
|
1626
|
+
- a segment-to-segment overlap via `transitionToNext`
|
|
1627
|
+
- text composites (`SplitText`, `TypewriterText`)
|
|
1628
|
+
- an overlay composite (`Watermark`)
|
|
1629
|
+
|
|
1630
|
+
```json
|
|
1631
|
+
{
|
|
1632
|
+
"version": "2.0",
|
|
1633
|
+
"video": {
|
|
1634
|
+
"id": "main",
|
|
1635
|
+
"width": 1920,
|
|
1636
|
+
"height": 1080,
|
|
1637
|
+
"fps": 30,
|
|
1638
|
+
"durationInFrames": 165
|
|
1639
|
+
},
|
|
1640
|
+
"segments": [
|
|
1641
|
+
{
|
|
1642
|
+
"id": "scene-1",
|
|
1643
|
+
"durationInFrames": 90,
|
|
1644
|
+
"transitionToNext": {
|
|
1645
|
+
"type": "FadeTransition",
|
|
1646
|
+
"durationInFrames": 15
|
|
1647
|
+
},
|
|
1648
|
+
"root": {
|
|
1649
|
+
"id": "root",
|
|
1650
|
+
"type": "Scene",
|
|
1651
|
+
"props": {
|
|
1652
|
+
"background": { "type": "color", "value": "#000000" }
|
|
1653
|
+
},
|
|
1654
|
+
"children": [
|
|
1655
|
+
{
|
|
1656
|
+
"id": "title",
|
|
1657
|
+
"type": "SplitText",
|
|
1658
|
+
"props": {
|
|
1659
|
+
"content": "Waves v0.3.0",
|
|
1660
|
+
"fontSize": 96,
|
|
1661
|
+
"splitBy": "word",
|
|
1662
|
+
"stagger": 3,
|
|
1663
|
+
"animation": "slideUp"
|
|
1664
|
+
}
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
"id": "subtitle",
|
|
1668
|
+
"type": "TypewriterText",
|
|
1669
|
+
"props": {
|
|
1670
|
+
"content": "Seamless alignment + overlays",
|
|
1671
|
+
"fontSize": 48,
|
|
1672
|
+
"position": "bottom",
|
|
1673
|
+
"speed": 1.5
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"id": "wm",
|
|
1678
|
+
"type": "Watermark",
|
|
1679
|
+
"props": {
|
|
1680
|
+
"type": "text",
|
|
1681
|
+
"text": "@depths.ai",
|
|
1682
|
+
"position": "bottomRight",
|
|
1683
|
+
"opacity": 0.4,
|
|
1684
|
+
"size": 60
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
]
|
|
1688
|
+
}
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
"id": "scene-2",
|
|
1692
|
+
"durationInFrames": 90,
|
|
1693
|
+
"root": {
|
|
1694
|
+
"id": "root-2",
|
|
1695
|
+
"type": "Scene",
|
|
1696
|
+
"props": {
|
|
1697
|
+
"background": { "type": "color", "value": "#0B1220" }
|
|
1698
|
+
},
|
|
1699
|
+
"children": [
|
|
1700
|
+
{
|
|
1701
|
+
"id": "lower-third",
|
|
1702
|
+
"type": "ThirdLowerBanner",
|
|
1703
|
+
"props": { "name": "Waves", "title": "v0.3.0 - Alignment + overlays", "accentColor": "#3B82F6" }
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
"id": "count",
|
|
1707
|
+
"type": "AnimatedCounter",
|
|
1708
|
+
"props": { "from": 0, "to": 35, "suffix": " components", "fontSize": 96, "color": "#FFFFFF" }
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
"id": "wm-2",
|
|
1712
|
+
"type": "Watermark",
|
|
1713
|
+
"props": { "type": "text", "text": "waves", "position": "topLeft", "opacity": 0.25, "size": 52 }
|
|
1714
|
+
}
|
|
1715
|
+
]
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
]
|
|
1719
|
+
}
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
### Example 1: primitives-only layout
|
|
1723
|
+
|
|
1724
|
+
File: `examples/primitives-card.v2.json`
|
|
1725
|
+
|
|
1726
|
+
This example uses only primitives: `Scene`, `Box`, `Grid`, `Shape`, `Text`.
|
|
1727
|
+
|
|
1728
|
+
```json
|
|
1729
|
+
{
|
|
1730
|
+
"version": "2.0",
|
|
1731
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 150 },
|
|
1732
|
+
"segments": [
|
|
1733
|
+
{
|
|
1734
|
+
"id": "card",
|
|
1735
|
+
"durationInFrames": 150,
|
|
1736
|
+
"root": {
|
|
1737
|
+
"id": "scene",
|
|
1738
|
+
"type": "Scene",
|
|
1739
|
+
"props": { "background": { "type": "color", "value": "#0B1220" } },
|
|
1740
|
+
"children": [
|
|
1741
|
+
{ "id": "accent", "type": "Shape", "props": { "shape": "rect", "x": 180, "y": 200, "width": 12, "height": 680, "fill": "#3B82F6", "opacity": 1 } },
|
|
1742
|
+
{
|
|
1743
|
+
"id": "panel",
|
|
1744
|
+
"type": "Frame",
|
|
1745
|
+
"props": { "x": 192, "y": 200, "width": 1548, "height": 680, "padding": 0, "backgroundColor": "rgba(255,255,255,0.06)", "borderRadius": 36, "opacity": 1 },
|
|
1746
|
+
"children": [
|
|
1747
|
+
{
|
|
1748
|
+
"id": "panel-grid",
|
|
1749
|
+
"type": "Grid",
|
|
1750
|
+
"props": { "columns": 2, "rows": 2, "gap": 28, "padding": 64, "align": "stretch", "justify": "stretch" },
|
|
1751
|
+
"children": [
|
|
1752
|
+
{
|
|
1753
|
+
"id": "tile-1",
|
|
1754
|
+
"type": "Box",
|
|
1755
|
+
"props": { "padding": 40, "backgroundColor": "rgba(255,255,255,0.07)", "borderRadius": 24 },
|
|
1756
|
+
"children": [
|
|
1757
|
+
{ "id": "t1", "type": "Text", "props": { "content": "Primitives", "fontSize": 64, "position": "center", "animation": "fade" } }
|
|
1758
|
+
]
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
"id": "tile-2",
|
|
1762
|
+
"type": "Box",
|
|
1763
|
+
"props": { "padding": 40, "backgroundColor": "rgba(255,255,255,0.07)", "borderRadius": 24 },
|
|
1764
|
+
"children": [
|
|
1765
|
+
{ "id": "t2", "type": "Text", "props": { "content": "Box + Grid", "fontSize": 54, "position": "center", "animation": "slide" } }
|
|
1766
|
+
]
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
"id": "tile-3",
|
|
1770
|
+
"type": "Box",
|
|
1771
|
+
"props": { "padding": 40, "backgroundColor": "rgba(255,255,255,0.07)", "borderRadius": 24 },
|
|
1772
|
+
"children": [
|
|
1773
|
+
{ "id": "t3", "type": "Text", "props": { "content": "Shape", "fontSize": 54, "position": "center", "animation": "zoom" } }
|
|
1774
|
+
]
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
"id": "tile-4",
|
|
1778
|
+
"type": "Box",
|
|
1779
|
+
"props": { "padding": 40, "backgroundColor": "rgba(255,255,255,0.07)", "borderRadius": 24 },
|
|
1780
|
+
"children": [
|
|
1781
|
+
{ "id": "t4", "type": "Text", "props": { "content": "Text", "fontSize": 54, "position": "center", "animation": "fade" } }
|
|
1782
|
+
]
|
|
1783
|
+
}
|
|
1784
|
+
]
|
|
1785
|
+
}
|
|
1786
|
+
]
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
"id": "footer",
|
|
1790
|
+
"type": "Text",
|
|
1791
|
+
"props": {
|
|
1792
|
+
"content": "This entire layout is built from primitives only.",
|
|
1793
|
+
"fontSize": 34,
|
|
1794
|
+
"position": "bottom",
|
|
1795
|
+
"animation": "fade",
|
|
1796
|
+
"color": "#C7D2FE"
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
]
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
]
|
|
1803
|
+
}
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
### Segment overlap math (how Example 2 works)
|
|
1807
|
+
|
|
1808
|
+
When authoring segments with overlaps, the total end time is:
|
|
1809
|
+
|
|
1810
|
+
`sum(segments[i].durationInFrames) - sum(segments[i].transitionToNext.durationInFrames)`
|
|
1811
|
+
|
|
1812
|
+
Example 2 has 4 segments:
|
|
1813
|
+
|
|
1814
|
+
- segment durations: 90 + 90 + 90 + 90 = 360
|
|
1815
|
+
- overlap durations: 15 + 15 + 15 = 45
|
|
1816
|
+
- video duration: 360 - 45 = 315 (must match `video.durationInFrames`)
|
|
1817
|
+
|
|
1818
|
+
### Example 2: segment transitions showcase
|
|
1819
|
+
|
|
1820
|
+
File: `examples/transitions-showcase.v2.json`
|
|
1821
|
+
|
|
1822
|
+
This example shows how to chain segments with overlaps using `transitionToNext`.
|
|
1823
|
+
|
|
1824
|
+
```json
|
|
1825
|
+
{
|
|
1826
|
+
"version": "2.0",
|
|
1827
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 315 },
|
|
1828
|
+
"segments": [
|
|
1829
|
+
{
|
|
1830
|
+
"id": "s1",
|
|
1831
|
+
"durationInFrames": 90,
|
|
1832
|
+
"transitionToNext": { "type": "FadeTransition", "durationInFrames": 15 },
|
|
1833
|
+
"root": {
|
|
1834
|
+
"id": "scene-1",
|
|
1835
|
+
"type": "Scene",
|
|
1836
|
+
"props": { "background": { "type": "color", "value": "#000000" } },
|
|
1837
|
+
"children": [
|
|
1838
|
+
{ "id": "title-1", "type": "SplitText", "props": { "content": "FadeTransition", "fontSize": 100, "splitBy": "word", "stagger": 3, "animation": "slideUp", "position": "center" } },
|
|
1839
|
+
{ "id": "wm-1", "type": "Watermark", "props": { "type": "text", "text": "waves", "position": "bottomRight", "opacity": 0.35, "size": 60 } }
|
|
1840
|
+
]
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
"id": "s2",
|
|
1845
|
+
"durationInFrames": 90,
|
|
1846
|
+
"transitionToNext": { "type": "SlideTransition", "durationInFrames": 15, "props": { "direction": "left", "distance": 120 } },
|
|
1847
|
+
"root": {
|
|
1848
|
+
"id": "scene-2",
|
|
1849
|
+
"type": "Scene",
|
|
1850
|
+
"props": { "background": { "type": "color", "value": "#0B1220" } },
|
|
1851
|
+
"children": [
|
|
1852
|
+
{ "id": "title-2", "type": "SplitText", "props": { "content": "SlideTransition", "fontSize": 96, "splitBy": "word", "stagger": 3, "animation": "slideUp", "position": "center" } },
|
|
1853
|
+
{ "id": "subtitle-2", "type": "TypewriterText", "props": { "content": "direction=left, distance=120", "fontSize": 44, "position": "bottom", "speed": 1.6, "showCursor": true } }
|
|
1854
|
+
]
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
"id": "s3",
|
|
1859
|
+
"durationInFrames": 90,
|
|
1860
|
+
"transitionToNext": { "type": "WipeTransition", "durationInFrames": 15, "props": { "direction": "diagonal", "softEdge": true } },
|
|
1861
|
+
"root": {
|
|
1862
|
+
"id": "scene-3",
|
|
1863
|
+
"type": "Scene",
|
|
1864
|
+
"props": { "background": { "type": "color", "value": "#111827" } },
|
|
1865
|
+
"children": [
|
|
1866
|
+
{ "id": "title-3", "type": "SplitText", "props": { "content": "WipeTransition", "fontSize": 96, "splitBy": "word", "stagger": 3, "animation": "scale", "position": "center" } },
|
|
1867
|
+
{ "id": "subtitle-3", "type": "TypewriterText", "props": { "content": "direction=diagonal, softEdge=true", "fontSize": 44, "position": "bottom", "speed": 1.6, "showCursor": false } }
|
|
1868
|
+
]
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
"id": "s4",
|
|
1873
|
+
"durationInFrames": 90,
|
|
1874
|
+
"root": {
|
|
1875
|
+
"id": "scene-4",
|
|
1876
|
+
"type": "Scene",
|
|
1877
|
+
"props": { "background": { "type": "color", "value": "#000000" } },
|
|
1878
|
+
"children": [
|
|
1879
|
+
{ "id": "title-4", "type": "SplitText", "props": { "content": "Done", "fontSize": 120, "splitBy": "letter", "stagger": 2, "animation": "rotate", "position": "center" } },
|
|
1880
|
+
{ "id": "wm-4", "type": "Watermark", "props": { "type": "text", "text": "@depths.ai", "position": "bottomRight", "opacity": 0.4, "size": 60 } }
|
|
1881
|
+
]
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
]
|
|
1885
|
+
}
|
|
1886
|
+
```
|
|
1887
|
+
|
|
1888
|
+
### Example 3: data dashboard
|
|
1889
|
+
|
|
1890
|
+
File: `examples/data-dashboard.v2.json`
|
|
1891
|
+
|
|
1892
|
+
This example uses data composites inside a `Grid`:
|
|
1893
|
+
|
|
1894
|
+
- `ProgressRing`
|
|
1895
|
+
- `BarChart`
|
|
1896
|
+
- `LineGraph`
|
|
1897
|
+
- `ProgressBar`
|
|
1898
|
+
|
|
1899
|
+
```json
|
|
1900
|
+
{
|
|
1901
|
+
"version": "2.0",
|
|
1902
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 180 },
|
|
1903
|
+
"segments": [
|
|
1904
|
+
{
|
|
1905
|
+
"id": "dashboard",
|
|
1906
|
+
"durationInFrames": 180,
|
|
1907
|
+
"root": {
|
|
1908
|
+
"id": "scene",
|
|
1909
|
+
"type": "Scene",
|
|
1910
|
+
"props": { "background": { "type": "color", "value": "#0B1220" } },
|
|
1911
|
+
"children": [
|
|
1912
|
+
{
|
|
1913
|
+
"id": "grid",
|
|
1914
|
+
"type": "Grid",
|
|
1915
|
+
"props": { "columns": 2, "rows": 2, "gap": 36, "padding": 90, "align": "stretch", "justify": "stretch" },
|
|
1916
|
+
"children": [
|
|
1917
|
+
{ "id": "ring", "type": "ProgressRing", "props": { "percentage": 72, "size": 260, "color": "#22C55E", "showLabel": true } },
|
|
1918
|
+
{
|
|
1919
|
+
"id": "bars",
|
|
1920
|
+
"type": "BarChart",
|
|
1921
|
+
"props": {
|
|
1922
|
+
"orientation": "vertical",
|
|
1923
|
+
"showValues": true,
|
|
1924
|
+
"showGrid": true,
|
|
1925
|
+
"data": [
|
|
1926
|
+
{ "label": "A", "value": 32, "color": "#3B82F6" },
|
|
1927
|
+
{ "label": "B", "value": 56, "color": "#22C55E" },
|
|
1928
|
+
{ "label": "C", "value": 18, "color": "#F59E0B" },
|
|
1929
|
+
{ "label": "D", "value": 44, "color": "#EF4444" }
|
|
1930
|
+
]
|
|
1931
|
+
}
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
"id": "line",
|
|
1935
|
+
"type": "LineGraph",
|
|
1936
|
+
"props": {
|
|
1937
|
+
"color": "#60A5FA",
|
|
1938
|
+
"strokeWidth": 4,
|
|
1939
|
+
"showDots": true,
|
|
1940
|
+
"fillArea": true,
|
|
1941
|
+
"animate": "draw",
|
|
1942
|
+
"data": [
|
|
1943
|
+
{ "x": 0, "y": 10 },
|
|
1944
|
+
{ "x": 1, "y": 16 },
|
|
1945
|
+
{ "x": 2, "y": 12 },
|
|
1946
|
+
{ "x": 3, "y": 22 },
|
|
1947
|
+
{ "x": 4, "y": 18 },
|
|
1948
|
+
{ "x": 5, "y": 28 }
|
|
1949
|
+
]
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
{ "id": "progress", "type": "ProgressBar", "props": { "label": "Rendering", "position": "bottom", "height": 16, "color": "#A855F7", "showPercentage": true } }
|
|
1953
|
+
]
|
|
1954
|
+
},
|
|
1955
|
+
{ "id": "title", "type": "SplitText", "props": { "content": "Data composites", "fontSize": 86, "splitBy": "word", "stagger": 3, "animation": "slideUp", "position": "top" } }
|
|
1956
|
+
]
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
]
|
|
1960
|
+
}
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
### Example 4: vertical social-style video (9:16)
|
|
1964
|
+
|
|
1965
|
+
File: `examples/social-vertical.v2.json`
|
|
1966
|
+
|
|
1967
|
+
This example uses:
|
|
1968
|
+
|
|
1969
|
+
- `InstagramStory` (story UI)
|
|
1970
|
+
- `TikTokCaption` (caption UI)
|
|
1971
|
+
- a `CircularReveal` segment overlap transition
|
|
1972
|
+
|
|
1973
|
+
```json
|
|
1974
|
+
{
|
|
1975
|
+
"version": "2.0",
|
|
1976
|
+
"video": { "id": "main", "width": 1080, "height": 1920, "fps": 30, "durationInFrames": 225 },
|
|
1977
|
+
"segments": [
|
|
1978
|
+
{
|
|
1979
|
+
"id": "ig",
|
|
1980
|
+
"durationInFrames": 120,
|
|
1981
|
+
"transitionToNext": { "type": "CircularReveal", "durationInFrames": 15, "props": { "direction": "open", "center": { "x": 0.5, "y": 0.45 } } },
|
|
1982
|
+
"root": {
|
|
1983
|
+
"id": "ig-root",
|
|
1984
|
+
"type": "Scene",
|
|
1985
|
+
"props": { "background": { "type": "color", "value": "#000000" } },
|
|
1986
|
+
"children": [
|
|
1987
|
+
{ "id": "ig", "type": "InstagramStory", "props": { "backgroundColor": "#0B1220", "username": "depths.ai", "text": "Waves v0.3.0 ships seamless alignment + overlays", "sticker": "poll" } }
|
|
1988
|
+
]
|
|
1989
|
+
}
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
"id": "tt",
|
|
1993
|
+
"durationInFrames": 120,
|
|
1994
|
+
"root": {
|
|
1995
|
+
"id": "tt-root",
|
|
1996
|
+
"type": "Scene",
|
|
1997
|
+
"props": { "background": { "type": "color", "value": "#0B1220" } },
|
|
1998
|
+
"children": [
|
|
1999
|
+
{ "id": "caption", "type": "TikTokCaption", "props": { "text": "Composites make LLM video authoring dramatically easier", "position": "bottom", "highlightStyle": "bounce", "fontSize": 58, "strokeWidth": 4 } },
|
|
2000
|
+
{ "id": "wm", "type": "Watermark", "props": { "type": "text", "text": "@depths.ai", "position": "topRight", "opacity": 0.35, "size": 72 } }
|
|
2001
|
+
]
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
]
|
|
2005
|
+
}
|
|
2006
|
+
```
|
|
2007
|
+
|
|
2008
|
+
### Example 5: intro -> stats -> outro (assets)
|
|
2009
|
+
|
|
2010
|
+
File: `examples/intro-stats-outro.v2.json`
|
|
2011
|
+
|
|
2012
|
+
This example uses branding + layout + data composites:
|
|
2013
|
+
|
|
2014
|
+
- `IntroScene` (requires `logoSrc` and `companyName`)
|
|
2015
|
+
- `ThirdLowerBanner`, `AnimatedCounter`, `BarChart`
|
|
2016
|
+
- `OutroScene` (requires `logoSrc`)
|
|
2017
|
+
|
|
2018
|
+
It references `/assets/logo.svg` style paths; supply those files and pass `--publicDir`.
|
|
2019
|
+
|
|
2020
|
+
```json
|
|
2021
|
+
{
|
|
2022
|
+
"version": "2.0",
|
|
2023
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 360 },
|
|
2024
|
+
"segments": [
|
|
2025
|
+
{
|
|
2026
|
+
"id": "intro",
|
|
2027
|
+
"durationInFrames": 120,
|
|
2028
|
+
"transitionToNext": { "type": "FadeTransition", "durationInFrames": 15 },
|
|
2029
|
+
"root": {
|
|
2030
|
+
"id": "intro-scene",
|
|
2031
|
+
"type": "Scene",
|
|
2032
|
+
"props": { "background": { "type": "color", "value": "#000000" } },
|
|
2033
|
+
"children": [
|
|
2034
|
+
{ "id": "intro", "type": "IntroScene", "props": { "logoSrc": "/assets/logo.svg", "companyName": "Depths AI", "tagline": "Waves v0.3.0", "backgroundColor": "#000000", "primaryColor": "#FFFFFF" } }
|
|
2035
|
+
]
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
"id": "stats",
|
|
2040
|
+
"durationInFrames": 150,
|
|
2041
|
+
"transitionToNext": { "type": "SlideTransition", "durationInFrames": 15, "props": { "direction": "up", "distance": 140 } },
|
|
2042
|
+
"root": {
|
|
2043
|
+
"id": "stats-scene",
|
|
2044
|
+
"type": "Scene",
|
|
2045
|
+
"props": { "background": { "type": "color", "value": "#0B1220" } },
|
|
2046
|
+
"children": [
|
|
2047
|
+
{ "id": "lower-third", "type": "ThirdLowerBanner", "props": { "name": "Waves", "title": "Composite components", "accentColor": "#22C55E" } },
|
|
2048
|
+
{ "id": "counter", "type": "AnimatedCounter", "props": { "from": 0, "to": 44, "suffix": " types", "fontSize": 110, "color": "#FFFFFF", "animationType": "spring" } },
|
|
2049
|
+
{ "id": "bars", "type": "BarChart", "props": { "orientation": "horizontal", "showValues": true, "showGrid": false, "data": [ { "label": "Primitives", "value": 10, "color": "#3B82F6" }, { "label": "Composites", "value": 34, "color": "#22C55E" } ] } }
|
|
2050
|
+
]
|
|
2051
|
+
}
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
"id": "outro",
|
|
2055
|
+
"durationInFrames": 120,
|
|
2056
|
+
"root": {
|
|
2057
|
+
"id": "outro-scene",
|
|
2058
|
+
"type": "Scene",
|
|
2059
|
+
"props": { "background": { "type": "color", "value": "#000000" } },
|
|
2060
|
+
"children": [
|
|
2061
|
+
{
|
|
2062
|
+
"id": "outro",
|
|
2063
|
+
"type": "OutroScene",
|
|
2064
|
+
"props": {
|
|
2065
|
+
"logoSrc": "/assets/logo.svg",
|
|
2066
|
+
"message": "Thanks for watching",
|
|
2067
|
+
"backgroundColor": "#000000",
|
|
2068
|
+
"ctaButtons": [
|
|
2069
|
+
{ "text": "Star", "icon": "*" },
|
|
2070
|
+
{ "text": "Follow", "icon": "->" }
|
|
2071
|
+
],
|
|
2072
|
+
"socialHandles": [
|
|
2073
|
+
{ "platform": "twitter", "handle": "@depths_ai" },
|
|
2074
|
+
{ "platform": "youtube", "handle": "@depths-ai" }
|
|
2075
|
+
]
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
{ "id": "wm", "type": "Watermark", "props": { "type": "text", "text": "@depths.ai", "position": "bottomRight", "opacity": 0.25, "size": 64 } }
|
|
2079
|
+
]
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
]
|
|
2083
|
+
}
|
|
2084
|
+
```
|
|
2085
|
+
|
|
2086
|
+
### Example 6: timeline mode (global audio)
|
|
2087
|
+
|
|
2088
|
+
File: `examples/timeline-global-audio.v2.json`
|
|
2089
|
+
|
|
2090
|
+
This example uses `timeline[]` to place a global `Audio` track as a root node spanning the entire video.
|
|
2091
|
+
|
|
2092
|
+
This is a common reason to choose timeline mode over segments mode: you want "global layers" (audio, watermarks, overlays) that span multiple scenes without duplicating them in each segment.
|
|
2093
|
+
|
|
2094
|
+
```json
|
|
2095
|
+
{
|
|
2096
|
+
"version": "2.0",
|
|
2097
|
+
"video": { "id": "main", "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 150 },
|
|
2098
|
+
"timeline": [
|
|
2099
|
+
{
|
|
2100
|
+
"id": "scene",
|
|
2101
|
+
"type": "Scene",
|
|
2102
|
+
"timing": { "from": 0, "durationInFrames": 150 },
|
|
2103
|
+
"props": { "background": { "type": "color", "value": "#0B1220" } },
|
|
2104
|
+
"children": [
|
|
2105
|
+
{ "id": "title", "type": "SplitText", "props": { "content": "Timeline mode", "fontSize": 96, "splitBy": "word", "stagger": 3, "animation": "slideUp", "position": "top" } },
|
|
2106
|
+
{ "id": "subtitle", "type": "TypewriterText", "props": { "content": "Audio spans the whole video", "fontSize": 44, "position": "bottom", "speed": 1.4, "showCursor": false } }
|
|
2107
|
+
]
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
"id": "music",
|
|
2111
|
+
"type": "Audio",
|
|
2112
|
+
"timing": { "from": 0, "durationInFrames": 150 },
|
|
2113
|
+
"props": { "src": "/assets/music.wav", "volume": 0.6, "fadeIn": 12, "fadeOut": 12 }
|
|
2114
|
+
}
|
|
2115
|
+
]
|
|
2116
|
+
}
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
## Library API (quickstart)
|
|
389
2120
|
|
|
390
2121
|
```ts
|
|
391
|
-
import {
|
|
2122
|
+
import { renderVideo } from '@depths/waves';
|
|
2123
|
+
|
|
2124
|
+
await renderVideo(
|
|
2125
|
+
{
|
|
2126
|
+
version: '2.0',
|
|
2127
|
+
video: { id: 'main', width: 1920, height: 1080, fps: 30, durationInFrames: 60 },
|
|
2128
|
+
segments: [
|
|
2129
|
+
{
|
|
2130
|
+
id: 'scene-1',
|
|
2131
|
+
durationInFrames: 60,
|
|
2132
|
+
root: {
|
|
2133
|
+
id: 'root',
|
|
2134
|
+
type: 'Scene',
|
|
2135
|
+
props: { background: { type: 'color', value: '#000000' } },
|
|
2136
|
+
children: [{ id: 't1', type: 'Text', props: { content: 'Hello' } }]
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
]
|
|
2140
|
+
},
|
|
2141
|
+
{ outputPath: './output.mp4', publicDir: './public' }
|
|
2142
|
+
);
|
|
2143
|
+
```
|
|
2144
|
+
|
|
2145
|
+
## Assets and paths (Windows/Linux)
|
|
2146
|
+
|
|
2147
|
+
In IR JSON, asset paths must be either:
|
|
2148
|
+
|
|
2149
|
+
- a full URL (`https://...`), or
|
|
2150
|
+
- a `/assets/...` path (leading slash) that is resolved relative to `--publicDir` / `publicDir`.
|
|
2151
|
+
|
|
2152
|
+
On Windows:
|
|
2153
|
+
|
|
2154
|
+
- Prefer forward slashes in IR (`/assets/foo.png`), not `C:\\...` paths.
|
|
2155
|
+
- If you pass Windows paths to the CLI (e.g. `--out`), quote them if they contain spaces.
|
|
2156
|
+
|
|
2157
|
+
## Rendering prerequisites
|
|
2158
|
+
|
|
2159
|
+
Waves uses `@remotion/renderer` (Remotion 4.x). Remotion runs headless Chromium; if a compatible browser isn't available, it may download one automatically (or you can configure browser paths via Remotion). Remotion 4 also ships its own ffmpeg binary, so you typically do not need `ffmpeg` installed on your PATH.
|
|
2160
|
+
|
|
2161
|
+
## Local CLI testing (before publishing)
|
|
2162
|
+
|
|
2163
|
+
From the `waves/` package directory:
|
|
2164
|
+
|
|
2165
|
+
```bash
|
|
2166
|
+
npm install
|
|
2167
|
+
npm run build
|
|
2168
|
+
node dist/cli.js --help
|
|
2169
|
+
node dist/cli.js write-ir --template basic --pretty --out examples/basic.v2.json
|
|
2170
|
+
node dist/cli.js validate --in examples/basic.v2.json
|
|
2171
|
+
node dist/cli.js render --in examples/basic.v2.json --out examples/basic.v2.mp4 --codec h264 --crf 28 --concurrency 1
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
To test the installed experience:
|
|
392
2175
|
|
|
393
|
-
|
|
394
|
-
|
|
2176
|
+
```bash
|
|
2177
|
+
npm link
|
|
2178
|
+
waves --help
|
|
395
2179
|
```
|
|
396
2180
|
|
|
397
|
-
|
|
2181
|
+
## Contributing
|
|
398
2182
|
|
|
399
|
-
|
|
400
|
-
- The component’s metadata (description, examples, etc.)
|
|
2183
|
+
This repository is intentionally "boring": most of the value lives in strict schemas, a strict registry, and a growing component catalog.
|
|
401
2184
|
|
|
402
|
-
###
|
|
2185
|
+
### Dev setup
|
|
403
2186
|
|
|
404
|
-
|
|
2187
|
+
From `internal_tools/waves/`:
|
|
405
2188
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
2189
|
+
```bash
|
|
2190
|
+
npm install
|
|
2191
|
+
npm test
|
|
2192
|
+
npm run typecheck
|
|
2193
|
+
npm run lint
|
|
2194
|
+
npm run build
|
|
2195
|
+
```
|
|
410
2196
|
|
|
411
|
-
|
|
2197
|
+
The build emits ESM into `dist/`. Most CLI tests execute the built CLI via `node dist/cli.js` (so you catch packaging issues).
|
|
2198
|
+
|
|
2199
|
+
### Repository layout (where to look)
|
|
2200
|
+
|
|
2201
|
+
- `src/cli.ts`: the CLI (`waves <command> ...`)
|
|
2202
|
+
- `src/llm/prompt.ts`: system prompt + prompt payload (schemas + catalog)
|
|
2203
|
+
- `src/ir/schema.ts`: IR v2.0 Zod schemas (authoring + full IR)
|
|
2204
|
+
- `src/ir/migrations.ts`: compiler (`compileToRenderGraph`) from authored IR -> render timeline
|
|
2205
|
+
- `src/core/registry.ts`: component registry + JSON Schema export for LLM use
|
|
2206
|
+
- `src/core/validator.ts`: schema + semantics + registry validation
|
|
2207
|
+
- `src/core/engine.ts`: "engine" that validates, compiles, bundles, renders
|
|
2208
|
+
- `src/remotion/WavesComposition.tsx`: Remotion root composition that renders the compiled IR
|
|
2209
|
+
- `src/components/primitives/*`: low-level building blocks (`Scene`, `Text`, `Box`, etc.)
|
|
2210
|
+
- `src/components/composites/*`: higher-level components built from primitives
|
|
2211
|
+
- `src/components/registry.ts`: built-in registration of all primitives + composites
|
|
2212
|
+
- `scripts/generate-readme-components.mjs`: generates the component/props tables inside this README
|
|
2213
|
+
|
|
2214
|
+
### Adding a new component (the shadcn-like flow)
|
|
2215
|
+
|
|
2216
|
+
1) Implement a React component in `src/components/primitives/` or `src/components/composites/`.
|
|
2217
|
+
2) Define:
|
|
2218
|
+
- a Zod props schema (export as `XPropsSchema`)
|
|
2219
|
+
- a metadata object (`XComponentMetadata`) describing:
|
|
2220
|
+
- `kind`: `primitive` or `composite`
|
|
2221
|
+
- `category`: one of the known categories (layout, media, text, transition, data, social, branding, etc.)
|
|
2222
|
+
- `description`: one-line description
|
|
2223
|
+
- `llmGuidance`: (optional) short "how to use" hint for agents
|
|
2224
|
+
- children contract: `acceptsChildren`, `minChildren`, `maxChildren`
|
|
2225
|
+
3) Register it in `src/components/registry.ts` via `globalRegistry.register({ type, component, propsSchema, metadata })`.
|
|
2226
|
+
4) Add tests:
|
|
2227
|
+
- registry/schema tests: `tests/unit/components-registry.test.ts` (or adjacent unit tests)
|
|
2228
|
+
- validator tests if the component introduces new timing or child-contract semantics
|
|
2229
|
+
5) Rebuild + re-generate README component docs:
|
|
412
2230
|
|
|
413
|
-
|
|
2231
|
+
```bash
|
|
2232
|
+
npm run build
|
|
2233
|
+
node scripts/generate-readme-components.mjs
|
|
2234
|
+
```
|
|
414
2235
|
|
|
415
|
-
|
|
2236
|
+
6) Run the full gates:
|
|
416
2237
|
|
|
417
|
-
|
|
2238
|
+
```bash
|
|
2239
|
+
npm test
|
|
2240
|
+
npm run typecheck
|
|
2241
|
+
npm run lint
|
|
2242
|
+
```
|
|
2243
|
+
|
|
2244
|
+
### Updating the component docs block in README
|
|
2245
|
+
|
|
2246
|
+
The "Components (primitives + composites)" section is generated from the registry so it never drifts.
|
|
2247
|
+
|
|
2248
|
+
The generator updates only the block between:
|
|
418
2249
|
|
|
419
|
-
|
|
2250
|
+
- `<!-- BEGIN GENERATED: COMPONENTS -->`
|
|
2251
|
+
- `<!-- END GENERATED: COMPONENTS -->`
|
|
420
2252
|
|
|
421
|
-
|
|
2253
|
+
Regenerate after any component/props changes:
|
|
2254
|
+
|
|
2255
|
+
```bash
|
|
2256
|
+
npm run build
|
|
2257
|
+
node scripts/generate-readme-components.mjs
|
|
2258
|
+
```
|
|
422
2259
|
|
|
423
|
-
|
|
2260
|
+
### Adding or changing CLI commands
|
|
424
2261
|
|
|
425
|
-
|
|
426
|
-
- If it’s a custom component, ensure it is registered into `globalRegistry`
|
|
2262
|
+
The CLI is implemented as a small argument parser in `src/cli.ts` (no heavy CLI framework) to keep packaging deterministic.
|
|
427
2263
|
|
|
428
|
-
|
|
2264
|
+
When adding commands:
|
|
429
2265
|
|
|
430
|
-
|
|
2266
|
+
1) update `formatHelp()`
|
|
2267
|
+
2) update `main()` command handler
|
|
2268
|
+
3) add CLI unit tests under `tests/unit/cli-*.test.ts`
|
|
2269
|
+
4) ensure the command works from the built output (`node dist/cli.js ...`)
|
|
431
2270
|
|
|
432
|
-
|
|
2271
|
+
### Notes on renders in CI/tests
|
|
433
2272
|
|
|
434
|
-
|
|
435
|
-
|
|
2273
|
+
The integration test `tests/integration/render.test.ts` is "best-effort": it tries to render a tiny video and writes to a temp folder.
|
|
2274
|
+
If Remotion rendering fails due to missing browser binaries or other environment limitations, fix the environment rather than weakening the test.
|