@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 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 intermediate representation (IR).
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
- This project is designed around explicitness:
11
+ v0.2.0 introduced a shadcn-like catalog of higher-level "composite" components and a hybrid IR that supports:
12
12
 
13
- - You only render components that have been explicitly registered in a component registry.
14
- - Component props are validated with Zod before rendering.
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
- ## What you get (v0.1.0)
16
+ v0.3.0 focuses on **seamless alignment + overlays**:
18
17
 
19
- - A stable IR format (currently `version: "1.0"`) for videos composed of scenes and components.
20
- - A component registry (`ComponentRegistry`) that maps `type` strings to React components + Zod props schemas + metadata.
21
- - A validator (`IRValidator`) with schema + semantic checks.
22
- - A rendering engine (`WavesEngine`) that bundles a Remotion project on the fly and renders to a media file.
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
- ## Why this exists
23
+ ## Table of contents
27
24
 
28
- Many “LLM → video” pipelines fail for one of these reasons:
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
- - The LLM output is not reliably validated and fails at render time.
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
- Waves solves this by:
45
+ ### What problem this solves
35
46
 
36
- - Giving the model a JSON schema to follow.
37
- - Enforcing strict component registration and props validation.
38
- - Providing deterministic, testable semantic rules around timing.
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
- Rendering prerequisites:
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
- await renderVideo(
155
- {
156
- version: '1.0',
157
- video: { id: 'my-video', width: 1920, height: 1080, fps: 30, durationInFrames: 90 },
158
- scenes: [
159
- {
160
- id: 'scene-1',
161
- type: 'Scene',
162
- timing: { from: 0, durationInFrames: 90 },
163
- props: { background: { type: 'color', value: '#000000' } },
164
- children: [
165
- {
166
- id: 'title',
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
- ## Core concepts
175
+ Run via the local bin:
180
176
 
181
- ### 1) IR (Intermediate Representation)
177
+ ```bash
178
+ npx waves --help
179
+ ```
182
180
 
183
- The IR is plain JSON. At minimum, it includes:
181
+ ### Typical agent loop
184
182
 
185
- - `version`: currently `"1.0"`
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
- All timing is expressed in frames.
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
- ### 2) Timing model
192
+ ### 1) Get the prompt payload (system prompt + schemas + catalog)
192
193
 
193
- - Every component has a `timing` object: `{ from, durationInFrames }`
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
- ### 3) Component registry
196
+ ```bash
197
+ npx waves prompt --format json --pretty --out ./waves-prompt.json
198
+ ```
201
199
 
202
- Waves never renders arbitrary `type` strings. A component `type` must be registered:
200
+ Human-readable system prompt:
203
201
 
204
- - `type`: a string identifier, e.g. `"Text"`
205
- - `component`: a React component to render
206
- - `propsSchema`: a Zod schema used to validate and coerce defaults
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
- ### 4) Rendering engine
206
+ Schemas only:
210
207
 
211
- Rendering is done through Remotion:
208
+ ```bash
209
+ npx waves schema --kind all --pretty --out ./waves-schemas.json
210
+ ```
212
211
 
213
- 1. Validate IR
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
- Important limitation (v0.1.0):
214
+ ```bash
215
+ npx waves catalog
216
+ npx waves catalog --format json --pretty --out ./waves-catalog.json
217
+ ```
220
218
 
221
- - `WavesEngine` currently requires using `globalRegistry` for rendering, because the generated Remotion bundle needs to register the same components at bundle-time.
219
+ If you register custom components at module import time, include them with repeatable `--register`:
222
220
 
223
- ## API reference
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
- ### `VideoIRSchema`
227
+ Important notes on `--register`:
226
228
 
227
- Use this to validate IR generated by an LLM:
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
- ```ts
230
- import { VideoIRSchema } from '@depths/waves';
234
+ ### 2) Write IR JSON
231
235
 
232
- const parsed = VideoIRSchema.safeParse(maybeJson);
233
- if (!parsed.success) {
234
- // parsed.error.issues
235
- }
236
+ ```bash
237
+ npx waves write-ir --template basic --pretty --out ./video.v2.json
236
238
  ```
237
239
 
238
- ### `IRValidator`
240
+ ### 3) Validate IR JSON
239
241
 
240
- Runs both schema validation and semantic checks:
242
+ ```bash
243
+ npx waves validate --in ./video.v2.json
244
+ ```
241
245
 
242
- ```ts
243
- import { IRValidator } from '@depths/waves';
246
+ Structured validation result:
244
247
 
245
- const validator = new IRValidator();
246
- const result = validator.validate(maybeJson);
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
- Semantic checks include:
252
+ ### 4) Render MP4
253
253
 
254
- - Scene duration sum matches `video.durationInFrames` (`DURATION_MISMATCH`)
255
- - Scene `from` frames are sequential (`TIMING_GAP_OR_OVERLAP`)
256
- - Child components do not exceed their parent duration (`COMPONENT_EXCEEDS_PARENT`)
254
+ ```bash
255
+ npx waves render --in ./video.v2.json --out ./output.mp4 --codec h264 --crf 28 --concurrency 1
256
+ ```
257
257
 
258
- ### `ComponentRegistry` / `globalRegistry`
258
+ If your IR references `"/assets/..."` paths, pass `--publicDir` and ensure the files exist at `${publicDir}/assets/...`:
259
259
 
260
- ```ts
261
- import { ComponentRegistry, globalRegistry } from '@depths/waves';
260
+ ```bash
261
+ npx waves render --in ./video.v2.json --out ./output.mp4 --publicDir ./public
262
262
  ```
263
263
 
264
- Typically you will use `globalRegistry` so that the rendering bundle can register and render the same types.
264
+ ### Exit codes
265
265
 
266
- ### `registerBuiltInComponents()`
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
- Registers built-in primitives (`Scene`, `Text`, `Audio`) into `globalRegistry`.
273
+ ### Command reference
269
274
 
270
- ```ts
271
- import { registerBuiltInComponents } from '@depths/waves';
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
- registerBuiltInComponents();
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
- This function is idempotent: calling it multiple times does not double-register types.
309
+ #### `waves schema`
277
310
 
278
- ### `renderVideo()`
311
+ Outputs JSON Schemas only.
279
312
 
280
- Convenience wrapper:
313
+ Flags:
281
314
 
282
- - Registers built-ins
283
- - Constructs `WavesEngine(globalRegistry, new IRValidator())`
284
- - Calls `engine.render()`
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
- ```ts
287
- import { renderVideo } from '@depths/waves';
320
+ Examples:
288
321
 
289
- await renderVideo(ir, { outputPath: './out.mp4' });
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
- ### `WavesEngine`
327
+ #### `waves catalog`
293
328
 
294
- Lower-level control:
329
+ Prints the built-in component catalog grouped by category.
295
330
 
296
- ```ts
297
- import { WavesEngine, globalRegistry, IRValidator, registerBuiltInComponents } from '@depths/waves';
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
- registerBuiltInComponents();
300
- const engine = new WavesEngine(globalRegistry, new IRValidator());
301
- await engine.render(ir, {
302
- outputPath: './out.mp4',
303
- codec: 'h264',
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
- Options:
363
+ #### `waves validate`
364
+
365
+ Validates an IR JSON file.
366
+
367
+ Flags:
310
368
 
311
- - `outputPath` (required): output file path
312
- - `codec`: one of `'h264' | 'h265' | 'vp8' | 'vp9'` (default: `h264`)
313
- - `crf`: quality parameter (lower is higher quality)
314
- - `concurrency`: number or percentage string supported by Remotion renderer
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
- Advanced / internal:
374
+ Behavior:
318
375
 
319
- - `rootDir`: temp directory root (defaults to `process.cwd()`)
320
- - `registrationModules`: module specifiers to import into the Remotion bundle before rendering (useful if you register custom components at module import time)
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
- ## Asset handling
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
- Waves supports two kinds of asset references in the IR:
411
+ #### `waves stills`
325
412
 
326
- 1. Remote URLs: `https://...` or `http://...`
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
- For absolute paths:
415
+ Flags:
330
416
 
331
- - `Scene`/`Audio` resolve them using Remotion’s `staticFile()` which expects a path relative to the `publicDir`.
332
- - Example: `src: "/assets/a.png"` becomes `staticFile("assets/a.png")`.
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
- If you reference `"/assets/..."`, pass `publicDir` to `renderVideo()` / `engine.render()` and make sure the file exists at:
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
- ${publicDir}/assets/...
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
- ## Built-in components (v0.1.0)
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
- ### `Scene`
479
+ ### Escape hatch: `timeline[]` (low-level)
343
480
 
344
- Container component for a segment of the video.
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
- - `background` (required):
349
- - `{ type: "color", value: "#RRGGBB" }`
350
- - `{ type: "image", value: "/assets/bg.png" | "https://..." }`
351
- - `{ type: "video", value: "/assets/bg.mp4" | "https://..." }`
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
- Children:
662
+ Props:
354
663
 
355
- - Any registered components; typically `Text` and `Audio`.
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
- ### `Text`
671
+ ##### `OutroScene`
358
672
 
359
- Animated text overlay.
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
- - `content` (required)
364
- - `fontSize` (default `48`)
365
- - `color` (default `#FFFFFF`)
366
- - `position` (default `"center"`) — `"top" | "center" | "bottom" | "left" | "right"`
367
- - `animation` (default `"fade"`) — `"none" | "fade" | "slide" | "zoom"`
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
- ### `Audio`
690
+ ##### `Watermark`
370
691
 
371
- Audio playback. Supports remote URLs or `staticFile()` assets.
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
- - `src` (required)
376
- - `volume` (default `1`)
377
- - `startFrom` (default `0`) trims from the beginning in frames
378
- - `fadeIn` (default `0`) fade-in duration in frames
379
- - `fadeOut` (default `0`) fade-out duration in frames
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
- Notes:
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
- - `fadeIn`/`fadeOut` are implemented via a `volume(frame)` curve.
384
- - The engine passes an internal prop `__wavesDurationInFrames` so `fadeOut` can compute its end.
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
- ## LLM integration
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
- ### Generating a prompt schema for the model
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 { globalRegistry, registerBuiltInComponents } from '@depths/waves';
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
- registerBuiltInComponents();
394
- const componentCatalog = globalRegistry.getJSONSchemaForLLM();
2176
+ ```bash
2177
+ npm link
2178
+ waves --help
395
2179
  ```
396
2180
 
397
- This returns an object keyed by component type, including:
2181
+ ## Contributing
398
2182
 
399
- - A JSON Schema describing the component’s props
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
- ### Typical prompt rules (recommended)
2185
+ ### Dev setup
403
2186
 
404
- When you prompt a model, include rules like:
2187
+ From `internal_tools/waves/`:
405
2188
 
406
- - All timing is in frames (`fps` defaults to 30)
407
- - Total scene durations must equal `video.durationInFrames`
408
- - Scene start frames must be sequential with no gaps/overlaps
409
- - Asset paths are either full URLs or absolute paths like `/assets/foo.png`
2189
+ ```bash
2190
+ npm install
2191
+ npm test
2192
+ npm run typecheck
2193
+ npm run lint
2194
+ npm run build
2195
+ ```
410
2196
 
411
- ## Troubleshooting
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
- ### Rendering fails with browser/Chromium errors
2231
+ ```bash
2232
+ npm run build
2233
+ node scripts/generate-readme-components.mjs
2234
+ ```
414
2235
 
415
- Remotion renders using headless Chromium. Ensure a compatible Chromium is available to `@remotion/renderer`.
2236
+ 6) Run the full gates:
416
2237
 
417
- ### Rendering fails with ffmpeg errors
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
- Install `ffmpeg` and ensure it is on the PATH.
2250
+ - `<!-- BEGIN GENERATED: COMPONENTS -->`
2251
+ - `<!-- END GENERATED: COMPONENTS -->`
420
2252
 
421
- ### Unknown component type
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
- If the engine throws “Unknown component type”:
2260
+ ### Adding or changing CLI commands
424
2261
 
425
- - Ensure you called `registerBuiltInComponents()`
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
- ### Props validation errors
2264
+ When adding commands:
429
2265
 
430
- Props are validated using the registered Zod schema. Defaults are applied by Zod on parse.
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
- ## License notes
2271
+ ### Notes on renders in CI/tests
433
2272
 
434
- - This package is MIT licensed (see `LICENSE`).
435
- - Remotion has separate licensing terms; ensure you comply with Remotion’s license for your use case.
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.