@vosjs/cli 0.8.5 → 0.9.1

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,1085 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://vos.so/schemas/doc.schema.json",
4
+ "title": "vosso doc.json (the studio document \u2014 a recording document or a program document)",
5
+ "description": "The editable document a vos carries beside its config: a RECORDING document (a take: `source` plus the tracks and layers) or a PROGRAM document (`program.config` plus the shared layers). Time is SECONDS everywhere. Edit this file, then re-run `vos render` \u2014 renders are deterministic, only your edits change the output. `vos validate <dir>` checks these contracts plus semantic lints (overlap, footage bounds).",
6
+ "oneOf": [
7
+ {
8
+ "title": "Recording document",
9
+ "description": "The take: a recording plus everything the video carries. `source` is the anchor. zoom/speed/segments/tilt/camMotion are SOURCE-time spans (footage-anchored: they follow footage through trims); audio, overlays and object spans are OUTPUT-time.",
10
+ "type": "object",
11
+ "required": [
12
+ "source",
13
+ "segments",
14
+ "zoom",
15
+ "audio",
16
+ "cursor",
17
+ "cam",
18
+ "frame",
19
+ "export"
20
+ ],
21
+ "additionalProperties": true,
22
+ "properties": {
23
+ "source": {
24
+ "type": "object",
25
+ "description": "The recording this doc edits. Written by record/plan \u2014 treat as read-only except videoKey/camKey/micKey when relinking assets. micKey (extension takes recorded after the audio split) is a separately-recorded mic sidecar: the voice, gained by micGain; the recording's own audio track is then system/tab audio, gained by systemGain.",
26
+ "additionalProperties": true
27
+ },
28
+ "segments": {
29
+ "type": "array",
30
+ "description": "Kept SOURCE-time footage spans; the output timeline is their concatenation. Trim dead time by shrinking spans. Empty array = untrimmed. Never carry `rate` here (only lowered/derived segments do).",
31
+ "items": {
32
+ "type": "object",
33
+ "required": ["in", "out"],
34
+ "properties": {
35
+ "in": {
36
+ "type": "number",
37
+ "minimum": 0,
38
+ "description": "SOURCE seconds"
39
+ },
40
+ "out": {
41
+ "type": "number",
42
+ "exclusiveMinimum": 0,
43
+ "description": "SOURCE seconds, > in"
44
+ }
45
+ },
46
+ "additionalProperties": true
47
+ }
48
+ },
49
+ "speed": {
50
+ "$ref": "#/definitions/speed"
51
+ },
52
+ "zoom": {
53
+ "type": "array",
54
+ "description": "Zoom regions (SOURCE time, footage-anchored, non-overlapping). The camera ramps in around `in`, holds until `out`, ramps out \u2014 or pans straight to the next span when the gap is short. Planner-made spans have source:'auto' (replaced on re-plan); set source:'manual' on any span you add or edit so it survives.",
55
+ "items": {
56
+ "type": "object",
57
+ "required": ["id", "in", "out", "level", "cx", "cy"],
58
+ "properties": {
59
+ "id": {
60
+ "type": "string",
61
+ "description": "stable identity (`z{n}` planner, `u{n}` user)"
62
+ },
63
+ "in": {
64
+ "type": "number",
65
+ "minimum": 0,
66
+ "description": "SOURCE seconds"
67
+ },
68
+ "out": {
69
+ "type": "number",
70
+ "description": "SOURCE seconds, > in (\u2265 0.3s span reads well)"
71
+ },
72
+ "level": {
73
+ "type": "number",
74
+ "minimum": 1,
75
+ "maximum": 5,
76
+ "description": "zoom level; 1.4\u20132.8 reads well"
77
+ },
78
+ "cx": {
79
+ "type": "number",
80
+ "minimum": 0,
81
+ "maximum": 1,
82
+ "description": "focus X as a NORMALIZED [0..1] fraction of the video frame (0.5 = center). NOT pixels."
83
+ },
84
+ "cy": {
85
+ "type": "number",
86
+ "minimum": 0,
87
+ "maximum": 1,
88
+ "description": "focus Y as a NORMALIZED [0..1] fraction of the video frame (0.5 = center). NOT pixels."
89
+ },
90
+ "ease": {
91
+ "type": "string",
92
+ "description": "arrival ease (@vosjs/timeline EASINGS name). Absent = the style's default ramp."
93
+ },
94
+ "transition": {
95
+ "enum": ["instant", "fast", "smooth", "slow"],
96
+ "description": "transition SPEED for this span's ramps (in, out, and a chained pan arriving here). Absent = 'smooth' (the camera style's stock motion); 'instant' = hard cut; 'fast' \u2248 half, 'slow' \u2248 1.6\u00d7."
97
+ },
98
+ "focusMode": {
99
+ "enum": ["manual", "auto"],
100
+ "description": "'auto' = camera follows the cursor through the span (needs a cursor track); absent/'manual' = fixed cx/cy"
101
+ },
102
+ "source": {
103
+ "enum": ["auto", "manual"],
104
+ "description": "'auto' = planner suggestion, replaced by re-plan; 'manual' = user/agent work, always preserved"
105
+ },
106
+ "anchor": {
107
+ "type": "object",
108
+ "required": ["step"],
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "step": {
112
+ "type": ["string", "integer"],
113
+ "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
114
+ },
115
+ "at": {
116
+ "enum": ["start", "end"],
117
+ "description": "which edge of the step the span's `in` is measured from (default start)"
118
+ },
119
+ "offset": {
120
+ "type": "number",
121
+ "description": "seconds from that edge to the span's `in` (negative = before it)"
122
+ }
123
+ },
124
+ "description": "Re-record tie: `vos plan --reuse` re-times this span onto a NEW recording of the same script by resolving the step in the new meta.steps. Metadata only \u2014 `in`/`out` stay authoritative and lowering never reads it, so it costs nothing at render time"
125
+ }
126
+ },
127
+ "additionalProperties": true
128
+ }
129
+ },
130
+ "zoomStyle": {
131
+ "enum": [
132
+ "glide",
133
+ "keynote",
134
+ "drift",
135
+ "focus",
136
+ "cinema",
137
+ "snappy",
138
+ "cut",
139
+ "none"
140
+ ],
141
+ "description": "Camera style preset driving planner + camera motion. Absent = 'glide'. 'none' disables auto-zoom planning (manual spans keep default motion). 'keynote' and 'drift' are tilt-forward styles: they carry a tilt personality (keynote = medium lean toward each zoom's focus with tilt ramps matched to the zoom ramps; drift = subtle lean, slower 1.6s ramps, longer chains) \u2014 in the studio, picking one stamps tiltStyle and plans auto tilt spans; in doc.json, set tiltStyle + tilt spans yourself (the style still shapes tilt MOTION at render)."
142
+ },
143
+ "zoomParams": {
144
+ "type": "object",
145
+ "description": "Per-doc overrides layered on the named style (the Custom seam). Any named-style pick clears them.",
146
+ "additionalProperties": true
147
+ },
148
+ "speedParams": {
149
+ "type": "object",
150
+ "description": "Auto-speed planner overrides: idleMin/idleRate/typingMin/typingRate/scrollMin/scrollRate. Absent = defaults (idle \u22655s\u21924\u00d7, typing \u22653s\u21923\u00d7, scroll \u22652.5s\u21922\u00d7).",
151
+ "additionalProperties": true
152
+ },
153
+ "tilt": {
154
+ "type": "array",
155
+ "description": "Card tilt regions (SOURCE time, footage-anchored, non-overlapping). While a span is active the 3D card leans to its pose; between spans it returns to FLAT (there is no static card pose: a lean is a moment on the timeline). Ramps ~0.9s in / ~0.8s out; spans \u2264 ~1.35s apart swing pose-to-pose. Direction: +rx brings the TOP edge toward the camera, +ry the LEFT edge \u2014 to lean toward a right-side focus use NEGATIVE ry. Wand-made spans have source:'auto' (replaced when Dynamic tilt re-runs); set source:'manual' on spans you add or edit.",
156
+ "items": {
157
+ "type": "object",
158
+ "required": ["id", "in", "out", "rx", "ry"],
159
+ "properties": {
160
+ "id": {
161
+ "type": "string",
162
+ "description": "stable identity (`t-{zoomId}` wand, `u{n}` user)"
163
+ },
164
+ "in": {
165
+ "type": "number",
166
+ "minimum": 0,
167
+ "description": "SOURCE seconds"
168
+ },
169
+ "out": {
170
+ "type": "number",
171
+ "description": "SOURCE seconds, > in (\u2265 0.8s so the pose can settle)"
172
+ },
173
+ "rx": {
174
+ "type": "number",
175
+ "minimum": -45,
176
+ "maximum": 45,
177
+ "description": "pose DEGREES about the horizontal axis (+ = top edge toward camera). NOT radians, NOT fractions; \u00b15..18\u00b0 reads premium."
178
+ },
179
+ "ry": {
180
+ "type": "number",
181
+ "minimum": -45,
182
+ "maximum": 45,
183
+ "description": "pose DEGREES about the vertical axis (+ = left edge toward camera; lean toward a right-side focus = negative). NOT radians, NOT fractions."
184
+ },
185
+ "ease": {
186
+ "type": "string",
187
+ "description": "arrival ease (@vosjs/timeline EASINGS name). Absent = the house tilt ease."
188
+ },
189
+ "transition": {
190
+ "enum": ["instant", "fast", "smooth", "slow"],
191
+ "description": "transition SPEED for this span's ramps. Absent = 'smooth' (~0.9s in / ~0.8s out); 'instant' snaps the card to the pose."
192
+ },
193
+ "source": {
194
+ "enum": ["auto", "manual"],
195
+ "description": "'auto' = Dynamic-tilt suggestion, replaced by re-plan; 'manual' = user/agent work, always preserved"
196
+ },
197
+ "anchor": {
198
+ "type": "object",
199
+ "required": ["step"],
200
+ "additionalProperties": false,
201
+ "properties": {
202
+ "step": {
203
+ "type": ["string", "integer"],
204
+ "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
205
+ },
206
+ "at": {
207
+ "enum": ["start", "end"],
208
+ "description": "which edge of the step the span's `in` is measured from (default start)"
209
+ },
210
+ "offset": {
211
+ "type": "number",
212
+ "description": "seconds from that edge to the span's `in` (negative = before it)"
213
+ }
214
+ },
215
+ "description": "Re-record tie: `vos plan --reuse` re-times this span onto a NEW recording of the same script by resolving the step in the new meta.steps. Metadata only \u2014 `in`/`out` stay authoritative and lowering never reads it, so it costs nothing at render time"
216
+ }
217
+ },
218
+ "additionalProperties": true
219
+ }
220
+ },
221
+ "tiltStyle": {
222
+ "enum": ["off", "subtle", "medium", "strong"],
223
+ "description": "Dynamic-tilt wand intensity: derives source:'auto' tilt spans FROM the zoom spans, leaning toward each zoom's focus (max 5/9/14\u00b0 per axis). 'off'/absent = no auto tilt; manual spans work either way."
224
+ },
225
+ "audio": {
226
+ "$ref": "#/definitions/audio"
227
+ },
228
+ "micGain": {
229
+ "type": "number",
230
+ "minimum": 0,
231
+ "maximum": 1,
232
+ "description": "master gain for the VOICE: the mic sidecar when source.micKey exists, else the recording's own audio. Absent = 1. 0 = muted."
233
+ },
234
+ "systemGain": {
235
+ "type": "number",
236
+ "minimum": 0,
237
+ "maximum": 1,
238
+ "description": "master gain for the recording's own system/tab audio track \u2014 split takes (source.micKey) only; legacy takes have one track on micGain. Absent = 1. 0 = muted."
239
+ },
240
+ "cursor": {
241
+ "type": "object",
242
+ "description": "Cursor rendering (visible, hideWhenIdle, smoothing, size, click effects). See CursorStyle in @vosso/studio-core. `visible: false` hides the drawn dot only \u2014 the track still drives cursor-follow zoom, and click effects keep drawing (silence those with clickFx.style: \"none\"). `hideWhenIdle` (default true) fades the dot out after ~1s parked and back in when it moves; scrolling is not movement, and it is always back at full opacity on a click.",
243
+ "additionalProperties": true
244
+ },
245
+ "cam": {
246
+ "type": "object",
247
+ "description": "Webcam bubble style + SOURCE-time window \u2014 the bubble's REST pose. Only applies when the take has a cam track. Placement: x/y (bubble CENTER as frame fractions, the zoom cx/cy convention) when present, else the `position` corner anchor; size = diameter as a fraction of frame height. Look: shape circle|rounded; radius (rounded corner, design px, default 18); border { width design px (default 3; 0 = none), color } (default white at 0.9); shadow none|soft|strong (default soft). Animate the pose over time with camMotion spans.",
248
+ "additionalProperties": true
249
+ },
250
+ "camMotion": {
251
+ "type": "array",
252
+ "description": "Animated cam layout (SOURCE time, footage-anchored, non-overlapping): while a span is active the webcam bubble holds its pose; outside spans it rests at doc.cam; spans \u2264 ~1.2s apart in output time morph pose-to-pose. Ramps ~0.65s, settled at span start (a cam move frames what follows). Absent pose fields inherit the rest pose, so a span may move without resizing. Only renders when the take has a cam track (source.camKey).",
253
+ "items": {
254
+ "type": "object",
255
+ "required": ["id", "in", "out"],
256
+ "properties": {
257
+ "id": {
258
+ "type": "string",
259
+ "description": "stable identity (`m{n}` user-created)"
260
+ },
261
+ "in": {
262
+ "type": "number",
263
+ "minimum": 0,
264
+ "description": "SOURCE seconds"
265
+ },
266
+ "out": {
267
+ "type": "number",
268
+ "description": "SOURCE seconds, > in (\u2265 0.5s so the move can settle)"
269
+ },
270
+ "x": {
271
+ "type": "number",
272
+ "minimum": 0,
273
+ "maximum": 1,
274
+ "description": "bubble CENTER as a fraction of the frame width [0..1] (the zoom cx/cy convention). NOT pixels. Absent = the rest pose's x."
275
+ },
276
+ "y": {
277
+ "type": "number",
278
+ "minimum": 0,
279
+ "maximum": 1,
280
+ "description": "bubble CENTER as a fraction of the frame height [0..1]. Absent = the rest pose's y."
281
+ },
282
+ "size": {
283
+ "type": "number",
284
+ "minimum": 0.08,
285
+ "maximum": 0.6,
286
+ "description": "bubble diameter as a fraction of the frame height. Absent = the rest pose's size."
287
+ },
288
+ "ease": {
289
+ "type": "string",
290
+ "description": "arrival ease (@vosjs/timeline EASINGS name, e.g. 'power2.out'). Absent = the house settle ease."
291
+ },
292
+ "transition": {
293
+ "enum": ["instant", "fast", "smooth", "slow"],
294
+ "description": "transition SPEED for this span's morphs. Absent = 'smooth' (~0.65s); 'instant' jump-cuts the bubble to its pose (the layout-cut)."
295
+ },
296
+ "source": {
297
+ "type": "string",
298
+ "enum": ["auto", "manual"],
299
+ "description": "reserved for a future planner; spans you add or edit are 'manual'"
300
+ }
301
+ }
302
+ }
303
+ },
304
+ "frame": {
305
+ "type": "object",
306
+ "description": "Card framing: background (CSS), padding, radius, shadow 0..1, border 0..1, aspectRatio, browserBar.",
307
+ "additionalProperties": true,
308
+ "properties": {
309
+ "background": {
310
+ "type": "string",
311
+ "description": "CSS background (gradient/color) \u2014 the underlay/fallback, always painted under backgroundMedia"
312
+ },
313
+ "backgroundMedia": {
314
+ "type": ["object", "null"],
315
+ "description": "Optional media layer drawn OVER the CSS background and UNDER the card: a vos animation baked to a seamless loop, or a still image. Video time is OUTPUT-anchored modulo the loop (bgT = t % duration) \u2014 trims/speed never retime it. Draws outside the zoom transform. `key` may be a file inside the take dir (e.g. \"/bg.webm\"), a public https://assets.vos.so/... loop, or an /api/... URL. Fail-open: if it can't load, the CSS background still shows.",
316
+ "required": ["kind", "key", "dim"],
317
+ "properties": {
318
+ "kind": {
319
+ "enum": ["video", "image"]
320
+ },
321
+ "key": {
322
+ "type": "string",
323
+ "description": "media URL or take-dir file (e.g. \"/bg.webm\")"
324
+ },
325
+ "duration": {
326
+ "type": "number",
327
+ "exclusiveMinimum": 0,
328
+ "description": "loop length in seconds (video only \u2014 required for the modulo loop)"
329
+ },
330
+ "vosId": {
331
+ "type": "string",
332
+ "description": "provenance: the vos this loop was rendered from (powers re-bake at export resolution)"
333
+ },
334
+ "versionId": {
335
+ "type": "string"
336
+ },
337
+ "poster": {
338
+ "type": "string",
339
+ "description": "poster/thumbnail URL (picker display + reduced-motion)"
340
+ },
341
+ "dim": {
342
+ "type": "number",
343
+ "minimum": 0,
344
+ "maximum": 1,
345
+ "description": "black scrim over the media for legibility (0 = none)"
346
+ },
347
+ "blur": {
348
+ "type": "number",
349
+ "minimum": 0,
350
+ "maximum": 100,
351
+ "description": "V2: blur radius in design px \u2014 softens the media behind the card"
352
+ }
353
+ },
354
+ "additionalProperties": true
355
+ },
356
+ "padding": {
357
+ "type": "number"
358
+ },
359
+ "radius": {
360
+ "type": "number"
361
+ },
362
+ "shadow": {
363
+ "type": "number",
364
+ "minimum": 0,
365
+ "maximum": 1
366
+ },
367
+ "border": {
368
+ "type": "number",
369
+ "minimum": 0,
370
+ "maximum": 1,
371
+ "description": "stroke around the card: the switch AND its alpha (0 = off)"
372
+ },
373
+ "borderWidth": {
374
+ "type": "number",
375
+ "minimum": 0,
376
+ "maximum": 24,
377
+ "description": "border stroke width in design px (scales with the canvas, like radius), drawn OUTWARD from the card's edge so it never covers the recording. Absent = 1.5"
378
+ },
379
+ "borderColor": {
380
+ "type": "string",
381
+ "description": "border stroke colour, any CSS colour string. Absent = #ffffff; `border` is the alpha it is drawn at"
382
+ },
383
+ "aspectRatio": {
384
+ "type": "string"
385
+ },
386
+ "parallax": {
387
+ "type": "number",
388
+ "minimum": 0,
389
+ "maximum": 1,
390
+ "description": "V2: background media counter-pans subtly with the zoom camera (depth cue). 0/absent = static; 0.6 reads well"
391
+ },
392
+ "fit": {
393
+ "type": "string",
394
+ "enum": ["contain", "cover"],
395
+ "description": "How footage meets an off-ratio frame. contain (default) letterboxes the card onto the background; cover makes the padded area the card and fills it with footage, cropped around `focus` \u2014 what a 440x280 store tile or a 2.5:1 marquee demands"
396
+ },
397
+ "focus": {
398
+ "type": "object",
399
+ "additionalProperties": false,
400
+ "properties": {
401
+ "cx": {
402
+ "type": "number",
403
+ "minimum": 0,
404
+ "maximum": 1
405
+ },
406
+ "cy": {
407
+ "type": "number",
408
+ "minimum": 0,
409
+ "maximum": 1
410
+ }
411
+ },
412
+ "description": "cover-crop anchor, normalized video-frame fractions (the zoom cx/cy convention). Absent = center; ignored under contain"
413
+ },
414
+ "browserBar": {
415
+ "type": "object",
416
+ "additionalProperties": true,
417
+ "properties": {
418
+ "kind": {
419
+ "type": "string",
420
+ "description": "'mac' | 'windows' | 'minimal' | 'none' | 'original' (window takes with a chrome crop)"
421
+ }
422
+ }
423
+ }
424
+ }
425
+ },
426
+ "overlays": {
427
+ "$ref": "#/definitions/overlays"
428
+ },
429
+ "objects": {
430
+ "$ref": "#/definitions/objects"
431
+ },
432
+ "export": {
433
+ "$ref": "#/definitions/export"
434
+ }
435
+ }
436
+ },
437
+ {
438
+ "title": "Program document",
439
+ "description": "A program plus the shared layers. `program.config` is THE user's config (functions as strings), untouched; the platform stores the COMPOSED config (the config plus the studio stack entry) as what plays. In a program DIRECTORY the CLI keeps `program.config` in config.json and omits it here. Minted only when a program gains a layer, a tween edit, a speed span or its own length; a bare program has no doc.",
440
+ "type": "object",
441
+ "required": ["program"],
442
+ "additionalProperties": true,
443
+ "properties": {
444
+ "program": {
445
+ "type": "object",
446
+ "description": "The anchor.",
447
+ "required": [],
448
+ "properties": {
449
+ "config": {
450
+ "type": "object",
451
+ "description": "The user's VosConfigJson, as authored. Present on the wire; omitted on disk in a program directory (config.json is the config).",
452
+ "additionalProperties": true
453
+ },
454
+ "tweenEdits": {
455
+ "type": "object",
456
+ "description": "Retimes over the config's recorded tweens, keyed by spec index: { startTime?, duration?, ease?, to?, from? }. Baked into the composed config's createTimeline; the authored config keeps its own.",
457
+ "additionalProperties": true
458
+ },
459
+ "duration": {
460
+ "type": "number",
461
+ "exclusiveMinimum": 0,
462
+ "description": "The anchor's own output length in seconds, when the config's duration is a placeholder. Absent = the config's."
463
+ }
464
+ },
465
+ "additionalProperties": true
466
+ },
467
+ "overlays": {
468
+ "$ref": "#/definitions/overlays"
469
+ },
470
+ "objects": {
471
+ "$ref": "#/definitions/objects"
472
+ },
473
+ "audio": {
474
+ "$ref": "#/definitions/audio"
475
+ },
476
+ "speed": {
477
+ "$ref": "#/definitions/speed"
478
+ },
479
+ "export": {
480
+ "$ref": "#/definitions/export"
481
+ }
482
+ }
483
+ }
484
+ ],
485
+ "definitions": {
486
+ "overlays": {
487
+ "type": "array",
488
+ "description": "Compositor v2 (V1): screen-space text clips drawn ABOVE the card, outside the zoom transform. OUTPUT-anchored (start is final-cut seconds \u2014 trims/speed never retime a title). Optional; absent = none.",
489
+ "items": {
490
+ "type": "object",
491
+ "required": ["id", "kind", "start", "duration", "transform"],
492
+ "additionalProperties": true,
493
+ "properties": {
494
+ "id": {
495
+ "type": "string"
496
+ },
497
+ "kind": {
498
+ "enum": ["text", "image", "video"]
499
+ },
500
+ "start": {
501
+ "type": "number",
502
+ "minimum": 0,
503
+ "description": "OUTPUT seconds"
504
+ },
505
+ "duration": {
506
+ "type": "number",
507
+ "exclusiveMinimum": 0
508
+ },
509
+ "text": {
510
+ "type": "string",
511
+ "description": "content; \\n breaks lines"
512
+ },
513
+ "preset": {
514
+ "enum": ["title", "caption", "label"],
515
+ "description": "house style: title = Lexend 600 64px, caption = Lexend 400 32px, label = JetBrains Mono 22px"
516
+ },
517
+ "size": {
518
+ "type": "number",
519
+ "minimum": 12,
520
+ "maximum": 200,
521
+ "description": "font-size override, DESIGN px (H=1080 space)"
522
+ },
523
+ "color": {
524
+ "type": "string",
525
+ "description": "CSS color override"
526
+ },
527
+ "family": {
528
+ "type": "string",
529
+ "description": "font family override \u2014 a catalog family name (GET https://vos.so/api/fonts). Unknown names fail open to the preset stack on the render fleet"
530
+ },
531
+ "weight": {
532
+ "type": "number",
533
+ "minimum": 100,
534
+ "maximum": 900,
535
+ "description": "weight override \u2014 snapped to the nearest weight the catalog hosts for the family (weights are files, not synthesis)"
536
+ },
537
+ "italic": {
538
+ "type": "boolean",
539
+ "description": "synthesized oblique (no italic files are hosted)"
540
+ },
541
+ "align": {
542
+ "enum": ["left", "center", "right"],
543
+ "description": "multi-line alignment within the block (default center)"
544
+ },
545
+ "letterSpacing": {
546
+ "type": "number",
547
+ "minimum": -10,
548
+ "maximum": 60,
549
+ "description": "letter spacing, design px at the resolved size (default 0)"
550
+ },
551
+ "lineHeight": {
552
+ "type": "number",
553
+ "minimum": 0.8,
554
+ "maximum": 3,
555
+ "description": "line-height multiplier (default 1.25)"
556
+ },
557
+ "stroke": {
558
+ "type": "object",
559
+ "required": ["color", "width"],
560
+ "additionalProperties": false,
561
+ "description": "text outline drawn under the fill",
562
+ "properties": {
563
+ "color": {
564
+ "type": "string"
565
+ },
566
+ "width": {
567
+ "type": "number",
568
+ "minimum": 0.5,
569
+ "maximum": 40,
570
+ "description": "design px at the resolved size"
571
+ }
572
+ }
573
+ },
574
+ "box": {
575
+ "type": "object",
576
+ "required": ["color"],
577
+ "additionalProperties": false,
578
+ "description": "background pill behind the text block (text only; absent = none). Paddings/radius are EMs of the resolved font size",
579
+ "properties": {
580
+ "color": {
581
+ "type": "string",
582
+ "description": "CSS pill color"
583
+ },
584
+ "opacity": {
585
+ "type": "number",
586
+ "minimum": 0,
587
+ "maximum": 1
588
+ },
589
+ "paddingX": {
590
+ "type": "number",
591
+ "minimum": 0,
592
+ "maximum": 4,
593
+ "description": "EMs, default 0.6"
594
+ },
595
+ "paddingY": {
596
+ "type": "number",
597
+ "minimum": 0,
598
+ "maximum": 4,
599
+ "description": "EMs, default 0.35"
600
+ },
601
+ "radius": {
602
+ "type": "number",
603
+ "minimum": 0,
604
+ "maximum": 2,
605
+ "description": "EMs, default 0.25"
606
+ }
607
+ }
608
+ },
609
+ "transform": {
610
+ "type": "object",
611
+ "required": ["x", "y"],
612
+ "properties": {
613
+ "x": {
614
+ "type": "number",
615
+ "minimum": -0.5,
616
+ "maximum": 1.5,
617
+ "description": "anchor CENTER as a FRACTION of the frame width [0..1] (0.5 = center at ANY aspect \u2014 the zoom cx/cy convention; NOT pixels)"
618
+ },
619
+ "y": {
620
+ "type": "number",
621
+ "minimum": -0.5,
622
+ "maximum": 1.5,
623
+ "description": "anchor CENTER as a FRACTION of the frame height [0..1] (lower-third \u2248 0.82; NOT pixels)"
624
+ },
625
+ "scale": {
626
+ "type": "number",
627
+ "exclusiveMinimum": 0,
628
+ "description": "uniform multiplier on the preset size (default 1)"
629
+ },
630
+ "rotation": {
631
+ "type": "number",
632
+ "description": "degrees, screen-space (default 0)"
633
+ }
634
+ },
635
+ "additionalProperties": true
636
+ },
637
+ "maxWidth": {
638
+ "type": "number",
639
+ "minimum": 0.1,
640
+ "maximum": 1,
641
+ "description": "text kind: wrap budget as a FRACTION of the frame width (greedy word wrap at measured widths; absent = lines break only on \\n; no intra-word breaks)"
642
+ },
643
+ "shadow": {
644
+ "enum": ["none", "soft", "strong"],
645
+ "description": "media kinds: the card shadow. Absent = 'soft' (the house picture-in-picture look); 'strong' floats harder; 'none' is a flat cutout"
646
+ },
647
+ "border": {
648
+ "type": "object",
649
+ "required": ["color", "width"],
650
+ "properties": {
651
+ "color": {
652
+ "type": "string",
653
+ "description": "CSS color"
654
+ },
655
+ "width": {
656
+ "type": "number",
657
+ "minimum": 0,
658
+ "maximum": 40,
659
+ "description": "design px (scales with the canvas, like radius)"
660
+ }
661
+ },
662
+ "description": "media kinds: an outline stroke over the clipped media edge. Absent = none"
663
+ },
664
+ "enter": {
665
+ "enum": ["none", "fade", "rise"],
666
+ "description": "default rise (~0.35s); IGNORED while fx is set (fx owns the entrance)"
667
+ },
668
+ "exit": {
669
+ "enum": ["none", "fade", "rise"],
670
+ "description": "default fade (~0.35s); stays clip-level even with fx"
671
+ },
672
+ "motion": {
673
+ "type": "array",
674
+ "description": "Pose keyframes (MO): the clip's transform animated over CLIP-LOCAL time. Values interpolate across the gap between poses (~symmetric in-out ease; a hold is two identical poses); the base transform is the value before the first pose, and absent pose fields inherit it. Pure f(t) \u2014 scrub, export and chunked server renders agree.",
675
+ "items": {
676
+ "type": "object",
677
+ "required": ["at"],
678
+ "properties": {
679
+ "at": {
680
+ "type": "number",
681
+ "minimum": 0,
682
+ "description": "CLIP-LOCAL seconds (0 = the clip's start; poses ride along when the clip moves)"
683
+ },
684
+ "x": {
685
+ "type": "number",
686
+ "minimum": -0.5,
687
+ "maximum": 1.5,
688
+ "description": "anchor CENTER as a fraction of the frame width (the transform.x convention). NOT pixels. Absent = the base transform's x."
689
+ },
690
+ "y": {
691
+ "type": "number",
692
+ "minimum": -0.5,
693
+ "maximum": 1.5,
694
+ "description": "anchor CENTER as a fraction of the frame height. Absent = the base transform's y."
695
+ },
696
+ "scale": {
697
+ "type": "number",
698
+ "exclusiveMinimum": 0,
699
+ "description": "uniform scale multiplier. Absent = the base transform's scale."
700
+ },
701
+ "rotation": {
702
+ "type": "number",
703
+ "description": "degrees, screen-space. Absent = the base transform's rotation."
704
+ },
705
+ "opacity": {
706
+ "type": "number",
707
+ "minimum": 0,
708
+ "maximum": 1,
709
+ "description": "opacity MULTIPLIER on the clip's own alpha (default 1)"
710
+ },
711
+ "ease": {
712
+ "type": "string",
713
+ "description": "arrival ease (@vosjs/timeline EASINGS name). Absent = 'power2.inOut'."
714
+ }
715
+ }
716
+ }
717
+ },
718
+ "fx": {
719
+ "type": "object",
720
+ "description": "text kind only: entrance animation evaluated per unit \u2014 pure f(t), segmentation baked at lowering (scrub/seek/server chunks agree by construction)",
721
+ "properties": {
722
+ "fx": {
723
+ "enum": ["fade", "rise", "pop", "blur", "typewriter"],
724
+ "description": "the entrance; typewriter is a step reveal by unit count"
725
+ },
726
+ "unit": {
727
+ "enum": ["block", "line", "word", "char"],
728
+ "description": "what animates as one thing (default block \u2014 the whole text; char is grapheme-safe)"
729
+ },
730
+ "direction": {
731
+ "enum": ["forward", "reverse", "center"],
732
+ "description": "unit start order (default forward; center ripples outward)"
733
+ },
734
+ "stagger": {
735
+ "type": "number",
736
+ "minimum": 0,
737
+ "maximum": 2,
738
+ "description": "seconds between unit starts (defaults: typewriter 0.05, others 0.06 when unit \u2260 block; clamped so the entrance fits the clip)"
739
+ },
740
+ "duration": {
741
+ "type": "number",
742
+ "minimum": 0.05,
743
+ "maximum": 2,
744
+ "description": "per-unit seconds (default 0.35); typewriter ignores it"
745
+ }
746
+ },
747
+ "required": ["fx"],
748
+ "additionalProperties": false
749
+ },
750
+ "key": {
751
+ "type": "string",
752
+ "description": "image/video kinds: media URL or take-dir file (e.g. \"/logo.png\", \"/clip.webm\") \u2014 required for media"
753
+ },
754
+ "width": {
755
+ "type": "number",
756
+ "exclusiveMinimum": 0,
757
+ "maximum": 1,
758
+ "description": "media kinds: base width as a FRACTION of the frame width (default 0.35); height follows the media's aspect; \u00d7 transform.scale"
759
+ },
760
+ "radius": {
761
+ "type": "number",
762
+ "minimum": 0,
763
+ "description": "media kinds: corner radius, design px (default 12)"
764
+ },
765
+ "opacity": {
766
+ "type": "number",
767
+ "minimum": 0,
768
+ "maximum": 1,
769
+ "description": "media kinds: opacity (default 1)"
770
+ },
771
+ "loop": {
772
+ "type": "boolean",
773
+ "description": "video kind: loop while active (default: hold the last frame)"
774
+ }
775
+ }
776
+ }
777
+ },
778
+ "objects": {
779
+ "type": "array",
780
+ "description": "Compositor v2 (V3): world-space 3D props between the card and the overlays \u2014 the drafted V4 engine spec. Curated primitives, GLB models by key, and extruded 3D text (TX7).",
781
+ "items": {
782
+ "type": "object",
783
+ "required": ["id", "asset", "transform3d"],
784
+ "additionalProperties": true,
785
+ "properties": {
786
+ "id": {
787
+ "type": "string"
788
+ },
789
+ "asset": {
790
+ "type": "object",
791
+ "required": ["kind"],
792
+ "properties": {
793
+ "kind": {
794
+ "enum": ["primitive", "gltf", "text3d"]
795
+ },
796
+ "shape": {
797
+ "enum": ["cube", "sphere", "torus", "knot"],
798
+ "description": "primitive kind"
799
+ },
800
+ "color": {
801
+ "type": "string",
802
+ "description": "CSS color (default #e4e4e7); text3d: the material's base/emissive color"
803
+ },
804
+ "key": {
805
+ "type": "string",
806
+ "description": "gltf kind: GLB URL or take-dir file (e.g. \"/model.glb\") \u2014 bbox-normalized so scale means the same as primitives"
807
+ },
808
+ "text": {
809
+ "type": "string",
810
+ "minLength": 1,
811
+ "description": "text3d kind: the extruded string (REQUIRED there)"
812
+ },
813
+ "typeface": {
814
+ "type": "string",
815
+ "description": "text3d kind: a 3D typeface catalog slug or family name (e.g. \"bebas-neue\", \"Playfair Display\"); unknown names fall back to the house face"
816
+ },
817
+ "material": {
818
+ "enum": ["standard", "metal", "glass", "neon"],
819
+ "description": "text3d kind: fleet-audited material preset (single-sided, no dispersion; default standard)"
820
+ },
821
+ "depth": {
822
+ "type": "number",
823
+ "minimum": 0.02,
824
+ "maximum": 1,
825
+ "description": "text3d kind: extrusion as a fraction of the glyph height (default 0.25)"
826
+ },
827
+ "bevel": {
828
+ "type": "boolean",
829
+ "description": "text3d kind: beveled edges (default true)"
830
+ }
831
+ },
832
+ "additionalProperties": true
833
+ },
834
+ "span": {
835
+ "type": "object",
836
+ "description": "OUTPUT-time visibility with soft edge fades; absent = whole timeline",
837
+ "properties": {
838
+ "start": {
839
+ "type": "number",
840
+ "minimum": 0
841
+ },
842
+ "duration": {
843
+ "type": "number",
844
+ "exclusiveMinimum": 0
845
+ }
846
+ }
847
+ },
848
+ "transform3d": {
849
+ "type": "object",
850
+ "required": ["x", "y"],
851
+ "properties": {
852
+ "x": {
853
+ "type": "number",
854
+ "description": "FRACTION of the frame width [0..1] (0.5 = center; NOT pixels)"
855
+ },
856
+ "y": {
857
+ "type": "number",
858
+ "description": "FRACTION of the frame height [0..1]"
859
+ },
860
+ "z": {
861
+ "type": "number",
862
+ "minimum": -2,
863
+ "maximum": 2.5,
864
+ "description": "world units toward the camera from the card plane (0 = on it; 0.5 floats clearly in front)"
865
+ },
866
+ "rx": {
867
+ "type": "number"
868
+ },
869
+ "ry": {
870
+ "type": "number"
871
+ },
872
+ "rz": {
873
+ "type": "number"
874
+ },
875
+ "scale": {
876
+ "type": "number",
877
+ "exclusiveMinimum": 0,
878
+ "maximum": 1,
879
+ "description": "fraction of the frame height (default 0.18)"
880
+ }
881
+ },
882
+ "additionalProperties": true
883
+ },
884
+ "animation": {
885
+ "enum": ["spin", "float", null],
886
+ "description": "curated deterministic motion \u2014 composes ADDITIVELY on top of motion poses"
887
+ },
888
+ "motion": {
889
+ "type": "array",
890
+ "description": "Pose keyframes (MO) over transform3d, CLIP-LOCAL time (seconds from span.start; 0 when span-less). Values interpolate across the gap between poses; absent pose fields inherit transform3d; spin/float presets compose additively. Pure f(t).",
891
+ "items": {
892
+ "type": "object",
893
+ "required": ["at"],
894
+ "properties": {
895
+ "at": {
896
+ "type": "number",
897
+ "minimum": 0,
898
+ "description": "CLIP-LOCAL seconds from the clip's span start"
899
+ },
900
+ "x": {
901
+ "type": "number",
902
+ "description": "frame fraction (the transform3d.x convention). Absent = the base."
903
+ },
904
+ "y": {
905
+ "type": "number",
906
+ "description": "frame fraction. Absent = the base."
907
+ },
908
+ "z": {
909
+ "type": "number",
910
+ "minimum": -2,
911
+ "maximum": 2.5,
912
+ "description": "world units toward the camera. Absent = the base."
913
+ },
914
+ "rx": {
915
+ "type": "number",
916
+ "description": "Euler degrees"
917
+ },
918
+ "ry": {
919
+ "type": "number",
920
+ "description": "Euler degrees"
921
+ },
922
+ "rz": {
923
+ "type": "number",
924
+ "description": "Euler degrees"
925
+ },
926
+ "scale": {
927
+ "type": "number",
928
+ "exclusiveMinimum": 0,
929
+ "maximum": 1,
930
+ "description": "fraction of the frame height. Absent = the base."
931
+ },
932
+ "ease": {
933
+ "type": "string",
934
+ "description": "arrival ease (@vosjs/timeline EASINGS name). Absent = 'power2.inOut'."
935
+ }
936
+ }
937
+ }
938
+ }
939
+ }
940
+ }
941
+ },
942
+ "audio": {
943
+ "type": "array",
944
+ "description": "Music/SFX clips placed on the OUTPUT timeline (they do NOT follow footage through trims; mic audio does). The CLI render mixes + muxes these (Opus for webm, AAC/Opus for mp4) on full single-flight renders; --range renders stay silent and --parallel is forced to 1 when audio is present. `key` may be a file inside the take dir (e.g. \"/music.mp3\").",
945
+ "items": {
946
+ "type": "object",
947
+ "required": [
948
+ "id",
949
+ "key",
950
+ "name",
951
+ "start",
952
+ "in",
953
+ "out",
954
+ "duration",
955
+ "gain",
956
+ "fadeIn",
957
+ "fadeOut"
958
+ ],
959
+ "properties": {
960
+ "id": {
961
+ "type": "string"
962
+ },
963
+ "key": {
964
+ "type": "string",
965
+ "description": "audio file URL (blob or asset URL)"
966
+ },
967
+ "name": {
968
+ "type": "string"
969
+ },
970
+ "start": {
971
+ "type": "number",
972
+ "minimum": 0,
973
+ "description": "OUTPUT seconds"
974
+ },
975
+ "in": {
976
+ "type": "number",
977
+ "minimum": 0,
978
+ "description": "kept span start within the source file, seconds"
979
+ },
980
+ "out": {
981
+ "type": "number",
982
+ "description": "kept span end, > in"
983
+ },
984
+ "duration": {
985
+ "type": "number",
986
+ "description": "full source-file length, seconds"
987
+ },
988
+ "gain": {
989
+ "type": "number",
990
+ "minimum": 0,
991
+ "maximum": 1
992
+ },
993
+ "fadeIn": {
994
+ "type": "number",
995
+ "minimum": 0
996
+ },
997
+ "fadeOut": {
998
+ "type": "number",
999
+ "minimum": 0
1000
+ },
1001
+ "loop": {
1002
+ "type": "boolean"
1003
+ },
1004
+ "loopLen": {
1005
+ "type": "number"
1006
+ },
1007
+ "duck": {
1008
+ "type": "boolean",
1009
+ "description": "duck under the mic while speech is detected"
1010
+ }
1011
+ },
1012
+ "additionalProperties": true
1013
+ }
1014
+ },
1015
+ "export": {
1016
+ "type": "object",
1017
+ "required": ["resolution", "fps", "format"],
1018
+ "properties": {
1019
+ "resolution": {
1020
+ "enum": ["720p", "1080p", "2k", "4k"],
1021
+ "description": "names the SHORT edge. Presets above the footage's capture width upscale (validate warns)."
1022
+ },
1023
+ "fps": {
1024
+ "enum": [30, 60]
1025
+ },
1026
+ "format": {
1027
+ "enum": ["mp4"],
1028
+ "description": "doc-level default; the CLI render's --format flag (webm|mp4) overrides"
1029
+ }
1030
+ },
1031
+ "additionalProperties": true
1032
+ },
1033
+ "speed": {
1034
+ "type": "array",
1035
+ "description": "Speed-change spans (SOURCE time, footage-anchored, non-overlapping). Absent = all 1\u00d7. The planner proposes source:'auto' spans for typing passages, scroll runs and idle gaps (ids s0\u2026); set source:'manual' on spans you add or edit so they survive a re-plan (absent source also counts as manual).",
1036
+ "items": {
1037
+ "type": "object",
1038
+ "required": ["id", "in", "out", "rate"],
1039
+ "properties": {
1040
+ "id": {
1041
+ "type": "string"
1042
+ },
1043
+ "in": {
1044
+ "type": "number",
1045
+ "minimum": 0
1046
+ },
1047
+ "out": {
1048
+ "type": "number"
1049
+ },
1050
+ "rate": {
1051
+ "type": "number",
1052
+ "minimum": 0.1,
1053
+ "maximum": 16,
1054
+ "description": "playback rate: 2 = twice as fast"
1055
+ },
1056
+ "source": {
1057
+ "enum": ["auto", "manual"],
1058
+ "description": "'auto' = planner suggestion (typing/scroll/idle), replaced by re-plan; 'manual' (or absent) = user/agent work, always preserved"
1059
+ },
1060
+ "anchor": {
1061
+ "type": "object",
1062
+ "required": ["step"],
1063
+ "additionalProperties": false,
1064
+ "properties": {
1065
+ "step": {
1066
+ "type": ["string", "integer"],
1067
+ "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
1068
+ },
1069
+ "at": {
1070
+ "enum": ["start", "end"],
1071
+ "description": "which edge of the step the span's `in` is measured from (default start)"
1072
+ },
1073
+ "offset": {
1074
+ "type": "number",
1075
+ "description": "seconds from that edge to the span's `in` (negative = before it)"
1076
+ }
1077
+ },
1078
+ "description": "Re-record tie: `vos plan --reuse` re-times this span onto a NEW recording of the same script by resolving the step in the new meta.steps. Metadata only \u2014 `in`/`out` stay authoritative and lowering never reads it, so it costs nothing at render time"
1079
+ }
1080
+ },
1081
+ "additionalProperties": true
1082
+ }
1083
+ }
1084
+ }
1085
+ }