vivid-editor-core 0.1.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 ADDED
@@ -0,0 +1,19 @@
1
+ # vivid-editor-core
2
+
3
+ The core of the [VIVID](https://vividai.tv) video editor, shared between the web editor and [vivid-mcp](https://github.com/loopotv/vivid-mcp):
4
+
5
+ - **Project file v2** — Zod schema (`projectSchemaV2`), types (`ProjectFile`), v1→v2 migration.
6
+ - **Editor store** — `createEditorStore(deps)`: a vanilla zustand store holding tracks, clips, canvas objects, text overlays, undo/redo and every editing action (add, split, trim, speed, transitions, canvas presets, masks…). Browser side effects are injected and default to no-ops, so it runs in Node.
7
+ - **AI commands** — the typed `AiCommand` union, Zod validators and `executeAiCommands`, the orchestrator the VIVID Art Director uses.
8
+ - **Headless editor** — `openHeadlessProject(json, { resolveUrl })` → `apply(commands)` → `toProjectFile()`.
9
+
10
+ ```ts
11
+ import { openHeadlessProject, emptyProjectFile } from 'vivid-editor-core'
12
+
13
+ const ed = openHeadlessProject(projectJson, { resolveUrl: (assetId) => `https://vivid-api.vividoai.workers.dev/api/assets/${assetId}/download` })
14
+ ed.store.getState().addAsset({ id: 'a1', name: 'hero.mp4', url: '…', durationMs: 10000, file: null, mediaType: 'video', serverAssetId: '…' })
15
+ ed.apply([{ type: 'ADD_CLIP', payload: { assetId: 'a1' } }, { type: 'ADD_TEXT', payload: { text: 'Hello', startMs: 0, endMs: 3000 } }])
16
+ const file = ed.toProjectFile() // save via POST /api/ai/save-editor-project
17
+ ```
18
+
19
+ Peer dependencies: `zod`, `zustand`. Source lives in `vivid-app/packages/editor-core`; the app consumes it from source via alias, vivid-mcp from npm.
@@ -0,0 +1,486 @@
1
+ /**
2
+ * M6 — Zod schemas for AI Art Director command validation.
3
+ *
4
+ * Every JSON object returned by the LLM is parsed through these schemas
5
+ * before being passed to executeAiCommands(). safeParse() → readable error
6
+ * message shown in the chat UI, never a silent drop.
7
+ *
8
+ * Schema reuses the v2 type primitives from src/io/schemas.ts but is kept
9
+ * separate to avoid coupling the load/save path to the LLM path.
10
+ */
11
+ import { z } from 'zod';
12
+ /** Set keyframe animations on a canvas object or text overlay property. */
13
+ export declare const SetObjectAnimationSchema: z.ZodObject<{
14
+ type: z.ZodLiteral<"SET_OBJECT_ANIMATION">;
15
+ payload: z.ZodObject<{
16
+ objectId: z.ZodString;
17
+ objectType: z.ZodEnum<{
18
+ canvas: "canvas";
19
+ text: "text";
20
+ }>;
21
+ property: z.ZodEnum<{
22
+ x: "x";
23
+ y: "y";
24
+ scale: "scale";
25
+ rotation: "rotation";
26
+ opacity: "opacity";
27
+ blur: "blur";
28
+ brightness: "brightness";
29
+ contrast: "contrast";
30
+ saturation: "saturation";
31
+ maskProgress: "maskProgress";
32
+ }>;
33
+ animation: z.ZodObject<{
34
+ keyframes: z.ZodArray<z.ZodObject<{
35
+ t: z.ZodNumber;
36
+ v: z.ZodNumber;
37
+ easing: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
38
+ linear: "linear";
39
+ easeInQuad: "easeInQuad";
40
+ easeOutQuad: "easeOutQuad";
41
+ easeInOutQuad: "easeInOutQuad";
42
+ easeInCubic: "easeInCubic";
43
+ easeOutCubic: "easeOutCubic";
44
+ easeInOutCubic: "easeInOutCubic";
45
+ easeInBack: "easeInBack";
46
+ easeOutBack: "easeOutBack";
47
+ easeInOutBack: "easeInOutBack";
48
+ easeInElastic: "easeInElastic";
49
+ easeOutElastic: "easeOutElastic";
50
+ easeInOutElastic: "easeInOutElastic";
51
+ easeInBounce: "easeInBounce";
52
+ easeOutBounce: "easeOutBounce";
53
+ easeInOutBounce: "easeInOutBounce";
54
+ }>, z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, z.ZodObject<{
55
+ type: z.ZodLiteral<"spring">;
56
+ stiffness: z.ZodNumber;
57
+ damping: z.ZodNumber;
58
+ mass: z.ZodNumber;
59
+ }, z.core.$strip>]>>;
60
+ }, z.core.$strip>>;
61
+ }, z.core.$strip>;
62
+ }, z.core.$strip>;
63
+ }, z.core.$strip>;
64
+ /** Add or replace an effect on a canvas object or text overlay. */
65
+ export declare const AddEffectSchema: z.ZodObject<{
66
+ type: z.ZodLiteral<"ADD_EFFECT">;
67
+ payload: z.ZodObject<{
68
+ objectId: z.ZodString;
69
+ objectType: z.ZodEnum<{
70
+ canvas: "canvas";
71
+ text: "text";
72
+ }>;
73
+ effect: z.ZodDiscriminatedUnion<[z.ZodObject<{
74
+ type: z.ZodLiteral<"blur">;
75
+ amount: z.ZodNumber;
76
+ }, z.core.$strip>, z.ZodObject<{
77
+ type: z.ZodLiteral<"vignette">;
78
+ intensity: z.ZodNumber;
79
+ size: z.ZodNumber;
80
+ }, z.core.$strip>, z.ZodObject<{
81
+ type: z.ZodLiteral<"colorGrade">;
82
+ brightness: z.ZodOptional<z.ZodNumber>;
83
+ contrast: z.ZodOptional<z.ZodNumber>;
84
+ saturation: z.ZodOptional<z.ZodNumber>;
85
+ hue: z.ZodOptional<z.ZodNumber>;
86
+ }, z.core.$strip>, z.ZodObject<{
87
+ type: z.ZodLiteral<"grain">;
88
+ intensity: z.ZodNumber;
89
+ size: z.ZodNumber;
90
+ }, z.core.$strip>, z.ZodObject<{
91
+ type: z.ZodLiteral<"glitch">;
92
+ intensity: z.ZodNumber;
93
+ blockSize: z.ZodNumber;
94
+ }, z.core.$strip>, z.ZodObject<{
95
+ type: z.ZodLiteral<"chromaticAberration">;
96
+ offset: z.ZodNumber;
97
+ }, z.core.$strip>, z.ZodObject<{
98
+ type: z.ZodLiteral<"pixelate">;
99
+ blockSize: z.ZodNumber;
100
+ }, z.core.$strip>], "type">;
101
+ replace: z.ZodOptional<z.ZodBoolean>;
102
+ }, z.core.$strip>;
103
+ }, z.core.$strip>;
104
+ /** Remove an effect by type from a canvas object or text overlay. */
105
+ export declare const RemoveEffectSchema: z.ZodObject<{
106
+ type: z.ZodLiteral<"REMOVE_EFFECT">;
107
+ payload: z.ZodObject<{
108
+ objectId: z.ZodString;
109
+ objectType: z.ZodEnum<{
110
+ canvas: "canvas";
111
+ text: "text";
112
+ }>;
113
+ effectType: z.ZodEnum<{
114
+ blur: "blur";
115
+ vignette: "vignette";
116
+ colorGrade: "colorGrade";
117
+ grain: "grain";
118
+ glitch: "glitch";
119
+ chromaticAberration: "chromaticAberration";
120
+ pixelate: "pixelate";
121
+ }>;
122
+ }, z.core.$strip>;
123
+ }, z.core.$strip>;
124
+ /** Add an audio-reactive modifier to a canvas object. */
125
+ export declare const AddAudioReactiveSchema: z.ZodObject<{
126
+ type: z.ZodLiteral<"ADD_AUDIO_REACTIVE">;
127
+ payload: z.ZodObject<{
128
+ objectId: z.ZodString;
129
+ objectType: z.ZodEnum<{
130
+ canvas: "canvas";
131
+ text: "text";
132
+ }>;
133
+ modifier: z.ZodObject<{
134
+ property: z.ZodEnum<{
135
+ x: "x";
136
+ y: "y";
137
+ scale: "scale";
138
+ rotation: "rotation";
139
+ opacity: "opacity";
140
+ blur: "blur";
141
+ brightness: "brightness";
142
+ contrast: "contrast";
143
+ saturation: "saturation";
144
+ maskProgress: "maskProgress";
145
+ }>;
146
+ feature: z.ZodEnum<{
147
+ rms: "rms";
148
+ low: "low";
149
+ mid: "mid";
150
+ high: "high";
151
+ beat: "beat";
152
+ }>;
153
+ assetId: z.ZodString;
154
+ factor: z.ZodNumber;
155
+ mode: z.ZodEnum<{
156
+ add: "add";
157
+ multiply: "multiply";
158
+ }>;
159
+ smoothing: z.ZodOptional<z.ZodNumber>;
160
+ }, z.core.$strip>;
161
+ }, z.core.$strip>;
162
+ }, z.core.$strip>;
163
+ /** Set the v2 mask on a canvas object. */
164
+ export declare const SetMaskSchema: z.ZodObject<{
165
+ type: z.ZodLiteral<"SET_MASK">;
166
+ payload: z.ZodObject<{
167
+ objectId: z.ZodString;
168
+ mask: z.ZodDiscriminatedUnion<[z.ZodObject<{
169
+ type: z.ZodLiteral<"none">;
170
+ }, z.core.$strip>, z.ZodObject<{
171
+ type: z.ZodLiteral<"rect">;
172
+ x: z.ZodNumber;
173
+ y: z.ZodNumber;
174
+ w: z.ZodNumber;
175
+ h: z.ZodNumber;
176
+ feather: z.ZodOptional<z.ZodNumber>;
177
+ }, z.core.$strip>, z.ZodObject<{
178
+ type: z.ZodLiteral<"circle">;
179
+ cx: z.ZodNumber;
180
+ cy: z.ZodNumber;
181
+ r: z.ZodNumber;
182
+ feather: z.ZodOptional<z.ZodNumber>;
183
+ }, z.core.$strip>, z.ZodObject<{
184
+ type: z.ZodLiteral<"reveal">;
185
+ direction: z.ZodEnum<{
186
+ ltr: "ltr";
187
+ rtl: "rtl";
188
+ ttb: "ttb";
189
+ btt: "btt";
190
+ }>;
191
+ progress: z.ZodNumber;
192
+ }, z.core.$strip>, z.ZodObject<{
193
+ type: z.ZodLiteral<"clipPath">;
194
+ svgPath: z.ZodString;
195
+ }, z.core.$strip>], "type">;
196
+ }, z.core.$strip>;
197
+ }, z.core.$strip>;
198
+ /** Set all animations on an object at once (replaces existing). */
199
+ export declare const SetAnimationsSchema: z.ZodObject<{
200
+ type: z.ZodLiteral<"SET_ANIMATIONS">;
201
+ payload: z.ZodObject<{
202
+ objectId: z.ZodString;
203
+ objectType: z.ZodEnum<{
204
+ canvas: "canvas";
205
+ text: "text";
206
+ }>;
207
+ animations: z.ZodRecord<z.ZodEnum<{
208
+ x: "x";
209
+ y: "y";
210
+ scale: "scale";
211
+ rotation: "rotation";
212
+ opacity: "opacity";
213
+ blur: "blur";
214
+ brightness: "brightness";
215
+ contrast: "contrast";
216
+ saturation: "saturation";
217
+ maskProgress: "maskProgress";
218
+ }>, z.ZodObject<{
219
+ keyframes: z.ZodArray<z.ZodObject<{
220
+ t: z.ZodNumber;
221
+ v: z.ZodNumber;
222
+ easing: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
223
+ linear: "linear";
224
+ easeInQuad: "easeInQuad";
225
+ easeOutQuad: "easeOutQuad";
226
+ easeInOutQuad: "easeInOutQuad";
227
+ easeInCubic: "easeInCubic";
228
+ easeOutCubic: "easeOutCubic";
229
+ easeInOutCubic: "easeInOutCubic";
230
+ easeInBack: "easeInBack";
231
+ easeOutBack: "easeOutBack";
232
+ easeInOutBack: "easeInOutBack";
233
+ easeInElastic: "easeInElastic";
234
+ easeOutElastic: "easeOutElastic";
235
+ easeInOutElastic: "easeInOutElastic";
236
+ easeInBounce: "easeInBounce";
237
+ easeOutBounce: "easeOutBounce";
238
+ easeInOutBounce: "easeInOutBounce";
239
+ }>, z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, z.ZodObject<{
240
+ type: z.ZodLiteral<"spring">;
241
+ stiffness: z.ZodNumber;
242
+ damping: z.ZodNumber;
243
+ mass: z.ZodNumber;
244
+ }, z.core.$strip>]>>;
245
+ }, z.core.$strip>>;
246
+ }, z.core.$strip>>;
247
+ }, z.core.$strip>;
248
+ }, z.core.$strip>;
249
+ /** The full union of v2 AI commands — used by the LLM response validator. */
250
+ export declare const AiCommandV2Schema: z.ZodDiscriminatedUnion<[z.ZodObject<{
251
+ type: z.ZodLiteral<"SET_OBJECT_ANIMATION">;
252
+ payload: z.ZodObject<{
253
+ objectId: z.ZodString;
254
+ objectType: z.ZodEnum<{
255
+ canvas: "canvas";
256
+ text: "text";
257
+ }>;
258
+ property: z.ZodEnum<{
259
+ x: "x";
260
+ y: "y";
261
+ scale: "scale";
262
+ rotation: "rotation";
263
+ opacity: "opacity";
264
+ blur: "blur";
265
+ brightness: "brightness";
266
+ contrast: "contrast";
267
+ saturation: "saturation";
268
+ maskProgress: "maskProgress";
269
+ }>;
270
+ animation: z.ZodObject<{
271
+ keyframes: z.ZodArray<z.ZodObject<{
272
+ t: z.ZodNumber;
273
+ v: z.ZodNumber;
274
+ easing: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
275
+ linear: "linear";
276
+ easeInQuad: "easeInQuad";
277
+ easeOutQuad: "easeOutQuad";
278
+ easeInOutQuad: "easeInOutQuad";
279
+ easeInCubic: "easeInCubic";
280
+ easeOutCubic: "easeOutCubic";
281
+ easeInOutCubic: "easeInOutCubic";
282
+ easeInBack: "easeInBack";
283
+ easeOutBack: "easeOutBack";
284
+ easeInOutBack: "easeInOutBack";
285
+ easeInElastic: "easeInElastic";
286
+ easeOutElastic: "easeOutElastic";
287
+ easeInOutElastic: "easeInOutElastic";
288
+ easeInBounce: "easeInBounce";
289
+ easeOutBounce: "easeOutBounce";
290
+ easeInOutBounce: "easeInOutBounce";
291
+ }>, z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, z.ZodObject<{
292
+ type: z.ZodLiteral<"spring">;
293
+ stiffness: z.ZodNumber;
294
+ damping: z.ZodNumber;
295
+ mass: z.ZodNumber;
296
+ }, z.core.$strip>]>>;
297
+ }, z.core.$strip>>;
298
+ }, z.core.$strip>;
299
+ }, z.core.$strip>;
300
+ }, z.core.$strip>, z.ZodObject<{
301
+ type: z.ZodLiteral<"ADD_EFFECT">;
302
+ payload: z.ZodObject<{
303
+ objectId: z.ZodString;
304
+ objectType: z.ZodEnum<{
305
+ canvas: "canvas";
306
+ text: "text";
307
+ }>;
308
+ effect: z.ZodDiscriminatedUnion<[z.ZodObject<{
309
+ type: z.ZodLiteral<"blur">;
310
+ amount: z.ZodNumber;
311
+ }, z.core.$strip>, z.ZodObject<{
312
+ type: z.ZodLiteral<"vignette">;
313
+ intensity: z.ZodNumber;
314
+ size: z.ZodNumber;
315
+ }, z.core.$strip>, z.ZodObject<{
316
+ type: z.ZodLiteral<"colorGrade">;
317
+ brightness: z.ZodOptional<z.ZodNumber>;
318
+ contrast: z.ZodOptional<z.ZodNumber>;
319
+ saturation: z.ZodOptional<z.ZodNumber>;
320
+ hue: z.ZodOptional<z.ZodNumber>;
321
+ }, z.core.$strip>, z.ZodObject<{
322
+ type: z.ZodLiteral<"grain">;
323
+ intensity: z.ZodNumber;
324
+ size: z.ZodNumber;
325
+ }, z.core.$strip>, z.ZodObject<{
326
+ type: z.ZodLiteral<"glitch">;
327
+ intensity: z.ZodNumber;
328
+ blockSize: z.ZodNumber;
329
+ }, z.core.$strip>, z.ZodObject<{
330
+ type: z.ZodLiteral<"chromaticAberration">;
331
+ offset: z.ZodNumber;
332
+ }, z.core.$strip>, z.ZodObject<{
333
+ type: z.ZodLiteral<"pixelate">;
334
+ blockSize: z.ZodNumber;
335
+ }, z.core.$strip>], "type">;
336
+ replace: z.ZodOptional<z.ZodBoolean>;
337
+ }, z.core.$strip>;
338
+ }, z.core.$strip>, z.ZodObject<{
339
+ type: z.ZodLiteral<"REMOVE_EFFECT">;
340
+ payload: z.ZodObject<{
341
+ objectId: z.ZodString;
342
+ objectType: z.ZodEnum<{
343
+ canvas: "canvas";
344
+ text: "text";
345
+ }>;
346
+ effectType: z.ZodEnum<{
347
+ blur: "blur";
348
+ vignette: "vignette";
349
+ colorGrade: "colorGrade";
350
+ grain: "grain";
351
+ glitch: "glitch";
352
+ chromaticAberration: "chromaticAberration";
353
+ pixelate: "pixelate";
354
+ }>;
355
+ }, z.core.$strip>;
356
+ }, z.core.$strip>, z.ZodObject<{
357
+ type: z.ZodLiteral<"ADD_AUDIO_REACTIVE">;
358
+ payload: z.ZodObject<{
359
+ objectId: z.ZodString;
360
+ objectType: z.ZodEnum<{
361
+ canvas: "canvas";
362
+ text: "text";
363
+ }>;
364
+ modifier: z.ZodObject<{
365
+ property: z.ZodEnum<{
366
+ x: "x";
367
+ y: "y";
368
+ scale: "scale";
369
+ rotation: "rotation";
370
+ opacity: "opacity";
371
+ blur: "blur";
372
+ brightness: "brightness";
373
+ contrast: "contrast";
374
+ saturation: "saturation";
375
+ maskProgress: "maskProgress";
376
+ }>;
377
+ feature: z.ZodEnum<{
378
+ rms: "rms";
379
+ low: "low";
380
+ mid: "mid";
381
+ high: "high";
382
+ beat: "beat";
383
+ }>;
384
+ assetId: z.ZodString;
385
+ factor: z.ZodNumber;
386
+ mode: z.ZodEnum<{
387
+ add: "add";
388
+ multiply: "multiply";
389
+ }>;
390
+ smoothing: z.ZodOptional<z.ZodNumber>;
391
+ }, z.core.$strip>;
392
+ }, z.core.$strip>;
393
+ }, z.core.$strip>, z.ZodObject<{
394
+ type: z.ZodLiteral<"SET_MASK">;
395
+ payload: z.ZodObject<{
396
+ objectId: z.ZodString;
397
+ mask: z.ZodDiscriminatedUnion<[z.ZodObject<{
398
+ type: z.ZodLiteral<"none">;
399
+ }, z.core.$strip>, z.ZodObject<{
400
+ type: z.ZodLiteral<"rect">;
401
+ x: z.ZodNumber;
402
+ y: z.ZodNumber;
403
+ w: z.ZodNumber;
404
+ h: z.ZodNumber;
405
+ feather: z.ZodOptional<z.ZodNumber>;
406
+ }, z.core.$strip>, z.ZodObject<{
407
+ type: z.ZodLiteral<"circle">;
408
+ cx: z.ZodNumber;
409
+ cy: z.ZodNumber;
410
+ r: z.ZodNumber;
411
+ feather: z.ZodOptional<z.ZodNumber>;
412
+ }, z.core.$strip>, z.ZodObject<{
413
+ type: z.ZodLiteral<"reveal">;
414
+ direction: z.ZodEnum<{
415
+ ltr: "ltr";
416
+ rtl: "rtl";
417
+ ttb: "ttb";
418
+ btt: "btt";
419
+ }>;
420
+ progress: z.ZodNumber;
421
+ }, z.core.$strip>, z.ZodObject<{
422
+ type: z.ZodLiteral<"clipPath">;
423
+ svgPath: z.ZodString;
424
+ }, z.core.$strip>], "type">;
425
+ }, z.core.$strip>;
426
+ }, z.core.$strip>, z.ZodObject<{
427
+ type: z.ZodLiteral<"SET_ANIMATIONS">;
428
+ payload: z.ZodObject<{
429
+ objectId: z.ZodString;
430
+ objectType: z.ZodEnum<{
431
+ canvas: "canvas";
432
+ text: "text";
433
+ }>;
434
+ animations: z.ZodRecord<z.ZodEnum<{
435
+ x: "x";
436
+ y: "y";
437
+ scale: "scale";
438
+ rotation: "rotation";
439
+ opacity: "opacity";
440
+ blur: "blur";
441
+ brightness: "brightness";
442
+ contrast: "contrast";
443
+ saturation: "saturation";
444
+ maskProgress: "maskProgress";
445
+ }>, z.ZodObject<{
446
+ keyframes: z.ZodArray<z.ZodObject<{
447
+ t: z.ZodNumber;
448
+ v: z.ZodNumber;
449
+ easing: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
450
+ linear: "linear";
451
+ easeInQuad: "easeInQuad";
452
+ easeOutQuad: "easeOutQuad";
453
+ easeInOutQuad: "easeInOutQuad";
454
+ easeInCubic: "easeInCubic";
455
+ easeOutCubic: "easeOutCubic";
456
+ easeInOutCubic: "easeInOutCubic";
457
+ easeInBack: "easeInBack";
458
+ easeOutBack: "easeOutBack";
459
+ easeInOutBack: "easeInOutBack";
460
+ easeInElastic: "easeInElastic";
461
+ easeOutElastic: "easeOutElastic";
462
+ easeInOutElastic: "easeInOutElastic";
463
+ easeInBounce: "easeInBounce";
464
+ easeOutBounce: "easeOutBounce";
465
+ easeInOutBounce: "easeInOutBounce";
466
+ }>, z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, z.ZodObject<{
467
+ type: z.ZodLiteral<"spring">;
468
+ stiffness: z.ZodNumber;
469
+ damping: z.ZodNumber;
470
+ mass: z.ZodNumber;
471
+ }, z.core.$strip>]>>;
472
+ }, z.core.$strip>>;
473
+ }, z.core.$strip>>;
474
+ }, z.core.$strip>;
475
+ }, z.core.$strip>], "type">;
476
+ export type AiCommandV2 = z.infer<typeof AiCommandV2Schema>;
477
+ /**
478
+ * Parse and validate a raw LLM JSON response as an array of v2 AI commands.
479
+ *
480
+ * Returns { commands, errors } where errors is a human-readable list.
481
+ * Never throws — all parse errors are collected and returned.
482
+ */
483
+ export declare function parseLlmCommands(raw: unknown): {
484
+ commands: AiCommandV2[];
485
+ errors: string[];
486
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * M6 — Zod schemas for AI Art Director command validation.
3
+ *
4
+ * Every JSON object returned by the LLM is parsed through these schemas
5
+ * before being passed to executeAiCommands(). safeParse() → readable error
6
+ * message shown in the chat UI, never a silent drop.
7
+ *
8
+ * Schema reuses the v2 type primitives from src/io/schemas.ts but is kept
9
+ * separate to avoid coupling the load/save path to the LLM path.
10
+ */
11
+ import { z } from 'zod';
12
+ // ── Primitive schemas (mirrors src/io/schemas.ts, kept local for isolation) ──
13
+ const EASING_PRESET = z.enum([
14
+ 'linear',
15
+ 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
16
+ 'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
17
+ 'easeInBack', 'easeOutBack', 'easeInOutBack',
18
+ 'easeInElastic', 'easeOutElastic', 'easeInOutElastic',
19
+ 'easeInBounce', 'easeOutBounce', 'easeInOutBounce',
20
+ ]);
21
+ const BEZIER_EASING = z.tuple([z.number(), z.number(), z.number(), z.number()]);
22
+ const SPRING_CONFIG = z.object({
23
+ type: z.literal('spring'),
24
+ stiffness: z.number().positive(),
25
+ damping: z.number().min(0),
26
+ mass: z.number().positive(),
27
+ });
28
+ const EASING = z.union([EASING_PRESET, BEZIER_EASING, SPRING_CONFIG]);
29
+ const KEYFRAME = z.object({
30
+ t: z.number().min(0),
31
+ v: z.number(),
32
+ easing: EASING.optional(),
33
+ });
34
+ const ANIMATION = z.object({
35
+ keyframes: z.array(KEYFRAME).min(1),
36
+ });
37
+ const ANIMATABLE_PROP = z.enum([
38
+ 'x', 'y', 'scale', 'rotation', 'opacity',
39
+ 'blur', 'brightness', 'contrast', 'saturation', 'maskProgress',
40
+ ]);
41
+ const ANIMATIONS = z.record(ANIMATABLE_PROP, ANIMATION);
42
+ const EFFECT = z.discriminatedUnion('type', [
43
+ z.object({ type: z.literal('blur'), amount: z.number().min(0).max(40) }),
44
+ z.object({ type: z.literal('vignette'), intensity: z.number().min(0).max(1), size: z.number().min(0).max(1) }),
45
+ z.object({
46
+ type: z.literal('colorGrade'),
47
+ brightness: z.number().optional(),
48
+ contrast: z.number().optional(),
49
+ saturation: z.number().optional(),
50
+ hue: z.number().optional(),
51
+ }),
52
+ z.object({ type: z.literal('grain'), intensity: z.number().min(0).max(1), size: z.number().min(0.5).max(3) }),
53
+ z.object({ type: z.literal('glitch'), intensity: z.number().min(0).max(1), blockSize: z.number().min(4).max(64) }),
54
+ z.object({ type: z.literal('chromaticAberration'), offset: z.number().min(0).max(20) }),
55
+ z.object({ type: z.literal('pixelate'), blockSize: z.number().min(2).max(64) }),
56
+ ]);
57
+ const MASK_V2 = z.discriminatedUnion('type', [
58
+ z.object({ type: z.literal('none') }),
59
+ z.object({
60
+ type: z.literal('rect'),
61
+ x: z.number().min(0).max(1), y: z.number().min(0).max(1),
62
+ w: z.number().min(0).max(1), h: z.number().min(0).max(1),
63
+ feather: z.number().optional(),
64
+ }),
65
+ z.object({
66
+ type: z.literal('circle'),
67
+ cx: z.number().min(0).max(1), cy: z.number().min(0).max(1),
68
+ r: z.number().min(0).max(1),
69
+ feather: z.number().optional(),
70
+ }),
71
+ z.object({
72
+ type: z.literal('reveal'),
73
+ direction: z.enum(['ltr', 'rtl', 'ttb', 'btt']),
74
+ progress: z.number().min(0).max(1),
75
+ }),
76
+ z.object({ type: z.literal('clipPath'), svgPath: z.string().min(1) }),
77
+ ]);
78
+ const AUDIO_REACTIVE = z.object({
79
+ property: ANIMATABLE_PROP,
80
+ feature: z.enum(['rms', 'low', 'mid', 'high', 'beat']),
81
+ assetId: z.string(),
82
+ factor: z.number(),
83
+ mode: z.enum(['add', 'multiply']),
84
+ smoothing: z.number().min(0).max(1).optional(),
85
+ });
86
+ // ── Command schemas ───────────────────────────────────────────────────────────
87
+ /** Set keyframe animations on a canvas object or text overlay property. */
88
+ export const SetObjectAnimationSchema = z.object({
89
+ type: z.literal('SET_OBJECT_ANIMATION'),
90
+ payload: z.object({
91
+ objectId: z.string(),
92
+ /** 'canvas' for CanvasObject, 'text' for TextOverlay */
93
+ objectType: z.enum(['canvas', 'text']),
94
+ property: ANIMATABLE_PROP,
95
+ animation: ANIMATION,
96
+ }),
97
+ });
98
+ /** Add or replace an effect on a canvas object or text overlay. */
99
+ export const AddEffectSchema = z.object({
100
+ type: z.literal('ADD_EFFECT'),
101
+ payload: z.object({
102
+ objectId: z.string(),
103
+ objectType: z.enum(['canvas', 'text']),
104
+ effect: EFFECT,
105
+ /** If true, replace any existing effect of the same type */
106
+ replace: z.boolean().optional(),
107
+ }),
108
+ });
109
+ /** Remove an effect by type from a canvas object or text overlay. */
110
+ export const RemoveEffectSchema = z.object({
111
+ type: z.literal('REMOVE_EFFECT'),
112
+ payload: z.object({
113
+ objectId: z.string(),
114
+ objectType: z.enum(['canvas', 'text']),
115
+ effectType: z.enum(['blur', 'vignette', 'colorGrade', 'grain', 'glitch', 'chromaticAberration', 'pixelate']),
116
+ }),
117
+ });
118
+ /** Add an audio-reactive modifier to a canvas object. */
119
+ export const AddAudioReactiveSchema = z.object({
120
+ type: z.literal('ADD_AUDIO_REACTIVE'),
121
+ payload: z.object({
122
+ objectId: z.string(),
123
+ objectType: z.enum(['canvas', 'text']),
124
+ modifier: AUDIO_REACTIVE,
125
+ }),
126
+ });
127
+ /** Set the v2 mask on a canvas object. */
128
+ export const SetMaskSchema = z.object({
129
+ type: z.literal('SET_MASK'),
130
+ payload: z.object({
131
+ objectId: z.string(),
132
+ mask: MASK_V2,
133
+ }),
134
+ });
135
+ /** Set all animations on an object at once (replaces existing). */
136
+ export const SetAnimationsSchema = z.object({
137
+ type: z.literal('SET_ANIMATIONS'),
138
+ payload: z.object({
139
+ objectId: z.string(),
140
+ objectType: z.enum(['canvas', 'text']),
141
+ animations: ANIMATIONS,
142
+ }),
143
+ });
144
+ /** The full union of v2 AI commands — used by the LLM response validator. */
145
+ export const AiCommandV2Schema = z.discriminatedUnion('type', [
146
+ SetObjectAnimationSchema,
147
+ AddEffectSchema,
148
+ RemoveEffectSchema,
149
+ AddAudioReactiveSchema,
150
+ SetMaskSchema,
151
+ SetAnimationsSchema,
152
+ ]);
153
+ /**
154
+ * Parse and validate a raw LLM JSON response as an array of v2 AI commands.
155
+ *
156
+ * Returns { commands, errors } where errors is a human-readable list.
157
+ * Never throws — all parse errors are collected and returned.
158
+ */
159
+ export function parseLlmCommands(raw) {
160
+ if (!Array.isArray(raw)) {
161
+ return {
162
+ commands: [],
163
+ errors: ['La risposta del modello non è un array di comandi valido.'],
164
+ };
165
+ }
166
+ const commands = [];
167
+ const errors = [];
168
+ for (let i = 0; i < raw.length; i++) {
169
+ const result = AiCommandV2Schema.safeParse(raw[i]);
170
+ if (result.success) {
171
+ commands.push(result.data);
172
+ }
173
+ else {
174
+ const issues = result.error.issues
175
+ .slice(0, 3)
176
+ .map((iss) => `${iss.path.join('.')}: ${iss.message}`)
177
+ .join('; ');
178
+ errors.push(`Comando #${i + 1} non valido — ${issues}`);
179
+ }
180
+ }
181
+ return { commands, errors };
182
+ }