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