@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.
@@ -0,0 +1,484 @@
1
+ import {
2
+ VideoIRSchema,
3
+ VideoIRv2AuthoringSchema,
4
+ VideoIRv2Schema,
5
+ WavesRenderError,
6
+ globalRegistry,
7
+ registerBuiltInComponents,
8
+ zodSchemaToJsonSchema
9
+ } from "./chunk-PKLHVWMD.mjs";
10
+
11
+ // src/version.ts
12
+ var __wavesVersion = "0.2.0";
13
+
14
+ // src/llm/prompt.ts
15
+ var RULES = {
16
+ timing: [
17
+ "All timing is in frames (integers).",
18
+ 'Prefer authoring using "segments" (high-level). Each segment has { durationInFrames, root } and start times are derived by order.',
19
+ 'Use "timeline" only for advanced cases (explicit timings, overlaps).',
20
+ "If you provide timing on nodes, timing is { from, durationInFrames } and children timing is relative to the parent duration.",
21
+ "Total duration must match video.durationInFrames."
22
+ ],
23
+ assets: [
24
+ 'Asset paths must be either full URLs (http:// or https://) or absolute public paths starting with "/".',
25
+ 'When using absolute public paths ("/assets/..."), the renderer resolves them relative to the --publicDir passed at render time.'
26
+ ],
27
+ ids: [
28
+ "Every component must have a stable string id.",
29
+ "Ids should be unique within the whole IR."
30
+ ],
31
+ output: [
32
+ "Return only a single JSON object (no markdown fences, no commentary).",
33
+ "The JSON must validate against the provided JSON Schemas.",
34
+ 'Only use component "type" values that exist in schemas.components.'
35
+ ]
36
+ };
37
+ function getSystemPrompt(registeredTypes) {
38
+ const maxInline = 40;
39
+ const typesLine = registeredTypes.length <= maxInline ? registeredTypes.length ? registeredTypes.join(", ") : "(none)" : `${registeredTypes.length} types (see schemas.components keys). Example: ${registeredTypes.slice(0, 20).join(", ")}, ...`;
40
+ return [
41
+ "You generate JSON for @depths/waves Video IR (version 2.0).",
42
+ "",
43
+ "Goal:",
44
+ "- Produce a single JSON object that conforms to the provided Video IR JSON Schema and uses only registered component types.",
45
+ "",
46
+ "Registered component types:",
47
+ `- ${typesLine}`,
48
+ "",
49
+ "Authoring rules:",
50
+ ...RULES.output.map((r) => `- ${r}`),
51
+ ...RULES.timing.map((r) => `- ${r}`),
52
+ ...RULES.assets.map((r) => `- ${r}`),
53
+ ...RULES.ids.map((r) => `- ${r}`),
54
+ "",
55
+ "If you are unsure about an asset, prefer a solid color Scene background and omit optional props to use defaults.",
56
+ ""
57
+ ].join("\n");
58
+ }
59
+ function getPromptPayload(options) {
60
+ registerBuiltInComponents();
61
+ const registry = options?.registry ?? globalRegistry;
62
+ const types = registry.getTypesForLLM().sort();
63
+ const categories = {};
64
+ const items = [];
65
+ for (const t of types) {
66
+ const reg = registry.get(t);
67
+ if (!reg) continue;
68
+ const cat = reg.metadata.category;
69
+ if (!categories[cat]) categories[cat] = [];
70
+ categories[cat].push(t);
71
+ const llmGuidance = reg.metadata.llmGuidance;
72
+ items.push({
73
+ type: t,
74
+ kind: reg.metadata.kind,
75
+ category: cat,
76
+ description: reg.metadata.description,
77
+ ...typeof llmGuidance === "string" ? { llmGuidance } : {}
78
+ });
79
+ }
80
+ return {
81
+ package: "@depths/waves",
82
+ version: __wavesVersion,
83
+ irVersion: "2.0",
84
+ systemPrompt: getSystemPrompt(types),
85
+ catalog: {
86
+ categories,
87
+ items
88
+ },
89
+ schemas: {
90
+ videoIR: zodSchemaToJsonSchema(VideoIRv2AuthoringSchema),
91
+ components: registry.getJSONSchemaForLLM()
92
+ },
93
+ rules: {
94
+ timing: [...RULES.timing],
95
+ assets: [...RULES.assets],
96
+ ids: [...RULES.ids],
97
+ output: [...RULES.output]
98
+ }
99
+ };
100
+ }
101
+
102
+ // src/ir/migrations.ts
103
+ function fillNodeTiming(node, parentDurationInFrames) {
104
+ const timing = node.timing ?? { from: 0, durationInFrames: parentDurationInFrames };
105
+ const children = node.children?.map((c) => fillNodeTiming(c, timing.durationInFrames));
106
+ return {
107
+ id: node.id,
108
+ type: node.type,
109
+ timing,
110
+ props: node.props,
111
+ metadata: node.metadata,
112
+ children: children && children.length ? children : void 0
113
+ };
114
+ }
115
+ function compileToRenderGraph(v2) {
116
+ const parsed = VideoIRv2Schema.parse(v2);
117
+ if (parsed.timeline) {
118
+ const timeline2 = parsed.timeline.map((n) => {
119
+ const timing = n.timing ?? { from: 0, durationInFrames: parsed.video.durationInFrames };
120
+ return fillNodeTiming({ ...n, timing }, parsed.video.durationInFrames);
121
+ });
122
+ return {
123
+ version: "2.0",
124
+ video: parsed.video,
125
+ audio: parsed.audio,
126
+ timeline: timeline2
127
+ };
128
+ }
129
+ const segments = parsed.segments ?? [];
130
+ const timeline = [];
131
+ let start = 0;
132
+ let pendingEnter;
133
+ for (let i = 0; i < segments.length; i++) {
134
+ const seg = segments[i];
135
+ const hasNext = i < segments.length - 1;
136
+ const exit = hasNext ? seg.transitionToNext : void 0;
137
+ const enter = pendingEnter;
138
+ const rootFilled = fillNodeTiming(seg.root, seg.durationInFrames);
139
+ timeline.push({
140
+ id: `segment:${seg.id}`,
141
+ type: "Segment",
142
+ timing: { from: start, durationInFrames: seg.durationInFrames },
143
+ props: {
144
+ enterTransition: enter,
145
+ exitTransition: exit
146
+ },
147
+ children: [rootFilled]
148
+ });
149
+ pendingEnter = exit;
150
+ const overlap = exit?.durationInFrames ?? 0;
151
+ start += seg.durationInFrames - overlap;
152
+ }
153
+ return {
154
+ version: "2.0",
155
+ video: parsed.video,
156
+ audio: parsed.audio,
157
+ timeline
158
+ };
159
+ }
160
+
161
+ // src/core/validator.ts
162
+ var IRValidator = class {
163
+ constructor(registry) {
164
+ this.registry = registry;
165
+ }
166
+ validateSchema(ir) {
167
+ const result = VideoIRSchema.safeParse(ir);
168
+ if (!result.success) {
169
+ return {
170
+ success: false,
171
+ errors: result.error.issues.map((issue) => ({
172
+ path: issue.path.map((p) => String(p)),
173
+ message: issue.message,
174
+ code: issue.code
175
+ }))
176
+ };
177
+ }
178
+ return { success: true, data: result.data };
179
+ }
180
+ validateSemantics(latest) {
181
+ const errors = [];
182
+ if (latest.segments) {
183
+ for (const [i, seg] of latest.segments.entries()) {
184
+ const exit = seg.transitionToNext;
185
+ const overlap = exit?.durationInFrames ?? 0;
186
+ if (exit && i === latest.segments.length - 1) {
187
+ errors.push({
188
+ path: ["segments", String(i), "transitionToNext"],
189
+ message: "transitionToNext is not allowed on the last segment",
190
+ code: "TRANSITION_ON_LAST_SEGMENT"
191
+ });
192
+ continue;
193
+ }
194
+ if (overlap > 0 && i < latest.segments.length - 1) {
195
+ const next = latest.segments[i + 1];
196
+ if (overlap > seg.durationInFrames) {
197
+ errors.push({
198
+ path: ["segments", String(i), "transitionToNext", "durationInFrames"],
199
+ message: `Transition overlap (${overlap}) exceeds segment duration (${seg.durationInFrames})`,
200
+ code: "TRANSITION_OVERLAP_TOO_LARGE"
201
+ });
202
+ }
203
+ if (overlap > next.durationInFrames) {
204
+ errors.push({
205
+ path: ["segments", String(i + 1), "durationInFrames"],
206
+ message: `Transition overlap (${overlap}) exceeds next segment duration (${next.durationInFrames})`,
207
+ code: "TRANSITION_OVERLAP_TOO_LARGE"
208
+ });
209
+ }
210
+ }
211
+ }
212
+ }
213
+ const compiled = compileToRenderGraph(latest);
214
+ const maxEnd = getMaxEnd(compiled.timeline);
215
+ if (maxEnd !== compiled.video.durationInFrames) {
216
+ errors.push({
217
+ path: ["video", "durationInFrames"],
218
+ message: `Max timeline end (${maxEnd}) does not match video duration (${compiled.video.durationInFrames})`,
219
+ code: "DURATION_MISMATCH"
220
+ });
221
+ }
222
+ for (const [i, root] of compiled.timeline.entries()) {
223
+ const end = root.timing.from + root.timing.durationInFrames;
224
+ if (root.timing.from < 0 || root.timing.durationInFrames <= 0) {
225
+ errors.push({
226
+ path: ["timeline", String(i), "timing"],
227
+ message: "Timing must have from>=0 and durationInFrames>0",
228
+ code: "INVALID_TIMING"
229
+ });
230
+ }
231
+ if (end > compiled.video.durationInFrames) {
232
+ errors.push({
233
+ path: ["timeline", String(i), "timing"],
234
+ message: `Root node exceeds video duration (${end} > ${compiled.video.durationInFrames})`,
235
+ code: "COMPONENT_EXCEEDS_VIDEO"
236
+ });
237
+ }
238
+ if (root.children?.length) {
239
+ this.validateComponentTimingRecursive(root.children, root.timing.durationInFrames, ["timeline", String(i)], errors);
240
+ }
241
+ }
242
+ if (this.registry) {
243
+ this.validateRegistryContracts(compiled, errors);
244
+ }
245
+ if (errors.length > 0) {
246
+ return { success: false, errors };
247
+ }
248
+ return { success: true, data: compiled };
249
+ }
250
+ validate(ir) {
251
+ const schemaResult = this.validateSchema(ir);
252
+ if (!schemaResult.success) {
253
+ return schemaResult;
254
+ }
255
+ const semanticsResult = this.validateSemantics(schemaResult.data);
256
+ if (!semanticsResult.success) {
257
+ return { success: false, errors: semanticsResult.errors ?? [] };
258
+ }
259
+ return semanticsResult;
260
+ }
261
+ validateComponentTimingRecursive(components, parentDuration, pathPrefix, errors) {
262
+ for (const [i, component] of components.entries()) {
263
+ const componentEnd = component.timing.from + component.timing.durationInFrames;
264
+ if (componentEnd > parentDuration) {
265
+ errors.push({
266
+ path: [...pathPrefix, "children", String(i), "timing"],
267
+ message: `Component extends beyond parent duration (${componentEnd} > ${parentDuration})`,
268
+ code: "COMPONENT_EXCEEDS_PARENT"
269
+ });
270
+ }
271
+ if (component.children && component.children.length > 0) {
272
+ this.validateComponentTimingRecursive(
273
+ component.children,
274
+ component.timing.durationInFrames,
275
+ [...pathPrefix, "children", String(i)],
276
+ errors
277
+ );
278
+ }
279
+ }
280
+ }
281
+ validateRegistryContracts(compiled, errors) {
282
+ const registry = this.registry;
283
+ const stack = compiled.timeline.map((n, i) => ({
284
+ node: n,
285
+ path: ["timeline", String(i)]
286
+ }));
287
+ while (stack.length) {
288
+ const { node, path: path2 } = stack.pop();
289
+ const reg = registry.get(node.type);
290
+ if (!reg) {
291
+ errors.push({
292
+ path: [...path2, "type"],
293
+ message: `Unknown component type: ${node.type}`,
294
+ code: "UNKNOWN_COMPONENT_TYPE"
295
+ });
296
+ continue;
297
+ }
298
+ const rawProps = node.props ?? {};
299
+ if (rawProps === null || typeof rawProps !== "object") {
300
+ errors.push({
301
+ path: [...path2, "props"],
302
+ message: "Component props must be an object",
303
+ code: "PROPS_NOT_OBJECT"
304
+ });
305
+ } else {
306
+ const propsValidation = registry.validateProps(node.type, rawProps);
307
+ if (!propsValidation.success) {
308
+ for (const issue of propsValidation.error.issues) {
309
+ errors.push({
310
+ path: [...path2, "props", ...issue.path.map((p) => String(p))],
311
+ message: issue.message,
312
+ code: "PROPS_INVALID"
313
+ });
314
+ }
315
+ } else {
316
+ node.props = propsValidation.data;
317
+ }
318
+ }
319
+ const childCount = node.children?.length ?? 0;
320
+ const meta = reg.metadata;
321
+ const acceptsChildren = Boolean(meta.acceptsChildren);
322
+ if (childCount > 0 && !acceptsChildren) {
323
+ errors.push({
324
+ path: [...path2, "children"],
325
+ message: `Component type "${node.type}" does not accept children`,
326
+ code: "CHILDREN_NOT_ALLOWED"
327
+ });
328
+ }
329
+ if (typeof meta.minChildren === "number" && childCount < meta.minChildren) {
330
+ errors.push({
331
+ path: [...path2, "children"],
332
+ message: `Component requires at least ${meta.minChildren} children`,
333
+ code: "TOO_FEW_CHILDREN"
334
+ });
335
+ }
336
+ if (typeof meta.maxChildren === "number" && childCount > meta.maxChildren) {
337
+ errors.push({
338
+ path: [...path2, "children"],
339
+ message: `Component allows at most ${meta.maxChildren} children`,
340
+ code: "TOO_MANY_CHILDREN"
341
+ });
342
+ }
343
+ if (node.children?.length) {
344
+ for (let i = 0; i < node.children.length; i++) {
345
+ stack.push({ node: node.children[i], path: [...path2, "children", String(i)] });
346
+ }
347
+ }
348
+ }
349
+ }
350
+ };
351
+ function getMaxEnd(nodes) {
352
+ let max = 0;
353
+ const stack = [...nodes];
354
+ while (stack.length) {
355
+ const n = stack.pop();
356
+ const end = n.timing.from + n.timing.durationInFrames;
357
+ if (end > max) max = end;
358
+ if (n.children?.length) stack.push(...n.children);
359
+ }
360
+ return max;
361
+ }
362
+
363
+ // src/core/engine.ts
364
+ import { bundle } from "@remotion/bundler";
365
+ import { renderMedia, selectComposition } from "@remotion/renderer";
366
+ import fs from "fs/promises";
367
+ import path from "path";
368
+ var WavesEngine = class {
369
+ constructor(registry, validator) {
370
+ this.registry = registry;
371
+ this.validator = validator;
372
+ }
373
+ async render(ir, options) {
374
+ if (this.registry !== globalRegistry) {
375
+ throw new WavesRenderError("WavesEngine currently requires using globalRegistry", {
376
+ hint: "Use `registerBuiltInComponents()` + `globalRegistry` for both validation and rendering."
377
+ });
378
+ }
379
+ const validationResult = this.validator.validate(ir);
380
+ if (!validationResult.success) {
381
+ throw new WavesRenderError("IR validation failed", { errors: validationResult.errors });
382
+ }
383
+ const validatedIR = validationResult.data;
384
+ const requiredTypes = collectComponentTypes(validatedIR);
385
+ for (const type of requiredTypes) {
386
+ if (!this.registry.has(type)) {
387
+ throw new WavesRenderError("Unknown component type", { type });
388
+ }
389
+ }
390
+ const rootDir = options.rootDir ?? process.cwd();
391
+ const tmpDir = await fs.mkdtemp(path.join(rootDir, ".waves-tmp-"));
392
+ const entryPoint = path.join(tmpDir, "entry.tsx");
393
+ try {
394
+ await fs.writeFile(entryPoint, generateEntryPoint(validatedIR, options), "utf-8");
395
+ await fs.mkdir(path.dirname(options.outputPath), { recursive: true });
396
+ const bundleLocation = await bundle({
397
+ entryPoint,
398
+ rootDir,
399
+ publicDir: options.publicDir ?? null,
400
+ onProgress: () => void 0
401
+ });
402
+ const compositionId = validatedIR.video.id ?? "main";
403
+ const composition = await selectComposition({
404
+ serveUrl: bundleLocation,
405
+ id: compositionId,
406
+ inputProps: {}
407
+ });
408
+ await renderMedia({
409
+ composition,
410
+ serveUrl: bundleLocation,
411
+ codec: options.codec ?? "h264",
412
+ outputLocation: options.outputPath,
413
+ crf: options.crf ?? null,
414
+ concurrency: options.concurrency ?? null,
415
+ overwrite: true
416
+ });
417
+ } catch (error) {
418
+ throw new WavesRenderError("Rendering failed", { originalError: error });
419
+ } finally {
420
+ await fs.rm(tmpDir, { recursive: true, force: true });
421
+ }
422
+ }
423
+ };
424
+ function collectComponentTypes(ir) {
425
+ const types = /* @__PURE__ */ new Set();
426
+ for (const node of ir.timeline) {
427
+ walkComponent(node, types);
428
+ }
429
+ return types;
430
+ }
431
+ function walkComponent(component, types) {
432
+ types.add(component.type);
433
+ const children = component.children;
434
+ if (children) {
435
+ for (const child of children) {
436
+ walkComponent(child, types);
437
+ }
438
+ }
439
+ }
440
+ function generateEntryPoint(ir, options) {
441
+ const compositionId = ir.video.id ?? "main";
442
+ const width = ir.video.width;
443
+ const height = ir.video.height;
444
+ const fps = ir.video.fps ?? 30;
445
+ const durationInFrames = ir.video.durationInFrames;
446
+ const registrationImports = (options.registrationModules ?? []).map((m) => `import ${JSON.stringify(m)};`).join("\n");
447
+ return `import React from 'react';
448
+ import { Composition, registerRoot } from 'remotion';
449
+ import { WavesComposition, globalRegistry, registerBuiltInComponents } from '@depths/waves/remotion';
450
+ ${registrationImports}
451
+
452
+ registerBuiltInComponents();
453
+
454
+ const ir = ${JSON.stringify(ir, null, 2)};
455
+ const compositionId = ${JSON.stringify(compositionId)};
456
+
457
+ const Root = () => {
458
+ return <WavesComposition ir={ir} registry={globalRegistry} />;
459
+ };
460
+
461
+ export const RemotionRoot = () => {
462
+ return (
463
+ <Composition
464
+ id={compositionId}
465
+ component={Root}
466
+ durationInFrames={${durationInFrames}}
467
+ fps={${fps}}
468
+ width={${width}}
469
+ height={${height}}
470
+ />
471
+ );
472
+ };
473
+
474
+ registerRoot(RemotionRoot);
475
+ `;
476
+ }
477
+
478
+ export {
479
+ __wavesVersion,
480
+ getSystemPrompt,
481
+ getPromptPayload,
482
+ IRValidator,
483
+ WavesEngine
484
+ };