@vosjs/cli 0.13.1 → 0.14.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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
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).",
4
+ "title": "vosso doc.json (the studio document 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` renders are deterministic, only your edits change the output. `vos validate <dir>` checks these contracts plus semantic lints (overlap, footage bounds).",
6
6
  "oneOf": [
7
7
  {
8
8
  "title": "Recording document",
@@ -22,7 +22,7 @@
22
22
  "properties": {
23
23
  "source": {
24
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.",
25
+ "description": "The recording this doc edits. Written by record/plan 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
26
  "additionalProperties": true
27
27
  },
28
28
  "segments": {
@@ -30,7 +30,10 @@
30
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
31
  "items": {
32
32
  "type": "object",
33
- "required": ["in", "out"],
33
+ "required": [
34
+ "in",
35
+ "out"
36
+ ],
34
37
  "properties": {
35
38
  "in": {
36
39
  "type": "number",
@@ -41,6 +44,12 @@
41
44
  "type": "number",
42
45
  "exclusiveMinimum": 0,
43
46
  "description": "SOURCE seconds, > in"
47
+ },
48
+ "hold": {
49
+ "type": "number",
50
+ "minimum": 0,
51
+ "maximum": 10,
52
+ "description": "freeze on this segment's last frame for these OUTPUT seconds (the beat before a cut, the ground for the end card)"
44
53
  }
45
54
  },
46
55
  "additionalProperties": true
@@ -51,10 +60,17 @@
51
60
  },
52
61
  "zoom": {
53
62
  "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.",
63
+ "description": "Zoom regions (SOURCE time, footage-anchored, non-overlapping). The camera ramps in around `in`, holds until `out`, ramps out 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
64
  "items": {
56
65
  "type": "object",
57
- "required": ["id", "in", "out", "level", "cx", "cy"],
66
+ "required": [
67
+ "id",
68
+ "in",
69
+ "out",
70
+ "level",
71
+ "cx",
72
+ "cy"
73
+ ],
58
74
  "properties": {
59
75
  "id": {
60
76
  "type": "string",
@@ -67,13 +83,13 @@
67
83
  },
68
84
  "out": {
69
85
  "type": "number",
70
- "description": "SOURCE seconds, > in (\u2265 0.3s span reads well)"
86
+ "description": "SOURCE seconds, > in ( 0.3s span reads well)"
71
87
  },
72
88
  "level": {
73
89
  "type": "number",
74
90
  "minimum": 1,
75
91
  "maximum": 5,
76
- "description": "zoom level; 1.4\u20132.8 reads well"
92
+ "description": "zoom level; 1.4–2.8 reads well"
77
93
  },
78
94
  "cx": {
79
95
  "type": "number",
@@ -92,28 +108,47 @@
92
108
  "description": "arrival ease (@vosjs/timeline EASINGS name). Absent = the style's default ramp."
93
109
  },
94
110
  "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."
111
+ "enum": [
112
+ "instant",
113
+ "fast",
114
+ "smooth",
115
+ "slow"
116
+ ],
117
+ "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' ≈ half, 'slow' ≈ 1.6×."
97
118
  },
98
119
  "focusMode": {
99
- "enum": ["manual", "auto"],
120
+ "enum": [
121
+ "manual",
122
+ "auto"
123
+ ],
100
124
  "description": "'auto' = camera follows the cursor through the span (needs a cursor track); absent/'manual' = fixed cx/cy"
101
125
  },
102
126
  "source": {
103
- "enum": ["auto", "manual"],
127
+ "enum": [
128
+ "auto",
129
+ "manual"
130
+ ],
104
131
  "description": "'auto' = planner suggestion, replaced by re-plan; 'manual' = user/agent work, always preserved"
105
132
  },
106
133
  "anchor": {
107
134
  "type": "object",
108
- "required": ["step"],
135
+ "required": [
136
+ "step"
137
+ ],
109
138
  "additionalProperties": false,
110
139
  "properties": {
111
140
  "step": {
112
- "type": ["string", "integer"],
141
+ "type": [
142
+ "string",
143
+ "integer"
144
+ ],
113
145
  "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
114
146
  },
115
147
  "at": {
116
- "enum": ["start", "end"],
148
+ "enum": [
149
+ "start",
150
+ "end"
151
+ ],
117
152
  "description": "which edge of the step the span's `in` is measured from (default start)"
118
153
  },
119
154
  "offset": {
@@ -121,7 +156,7 @@
121
156
  "description": "seconds from that edge to the span's `in` (negative = before it)"
122
157
  }
123
158
  },
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"
159
+ "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 `in`/`out` stay authoritative and lowering never reads it, so it costs nothing at render time"
125
160
  }
126
161
  },
127
162
  "additionalProperties": true
@@ -138,7 +173,7 @@
138
173
  "cut",
139
174
  "none"
140
175
  ],
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)."
176
+ "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) 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
177
  },
143
178
  "zoomParams": {
144
179
  "type": "object",
@@ -147,15 +182,21 @@
147
182
  },
148
183
  "speedParams": {
149
184
  "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).",
185
+ "description": "Auto-speed planner overrides: idleMin/idleRate/typingMin/typingRate/scrollMin/scrollRate. Absent = defaults (idle ≥5s→4×, typing ≥3s→3×, scroll ≥2.5s→2×).",
151
186
  "additionalProperties": true
152
187
  },
153
188
  "tilt": {
154
189
  "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.",
190
+ "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 ~1.35s apart swing pose-to-pose. Direction: +rx brings the TOP edge toward the camera, +ry the LEFT edge 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
191
  "items": {
157
192
  "type": "object",
158
- "required": ["id", "in", "out", "rx", "ry"],
193
+ "required": [
194
+ "id",
195
+ "in",
196
+ "out",
197
+ "rx",
198
+ "ry"
199
+ ],
159
200
  "properties": {
160
201
  "id": {
161
202
  "type": "string",
@@ -168,13 +209,13 @@
168
209
  },
169
210
  "out": {
170
211
  "type": "number",
171
- "description": "SOURCE seconds, > in (\u2265 0.8s so the pose can settle)"
212
+ "description": "SOURCE seconds, > in ( 0.8s so the pose can settle)"
172
213
  },
173
214
  "rx": {
174
215
  "type": "number",
175
216
  "minimum": -45,
176
217
  "maximum": 45,
177
- "description": "pose DEGREES about the horizontal axis (+ = top edge toward camera). NOT radians, NOT fractions; \u00b15..18\u00b0 reads premium."
218
+ "description": "pose DEGREES about the horizontal axis (+ = top edge toward camera). NOT radians, NOT fractions; ±5..18° reads premium."
178
219
  },
179
220
  "ry": {
180
221
  "type": "number",
@@ -187,24 +228,40 @@
187
228
  "description": "arrival ease (@vosjs/timeline EASINGS name). Absent = the house tilt ease."
188
229
  },
189
230
  "transition": {
190
- "enum": ["instant", "fast", "smooth", "slow"],
231
+ "enum": [
232
+ "instant",
233
+ "fast",
234
+ "smooth",
235
+ "slow"
236
+ ],
191
237
  "description": "transition SPEED for this span's ramps. Absent = 'smooth' (~0.9s in / ~0.8s out); 'instant' snaps the card to the pose."
192
238
  },
193
239
  "source": {
194
- "enum": ["auto", "manual"],
240
+ "enum": [
241
+ "auto",
242
+ "manual"
243
+ ],
195
244
  "description": "'auto' = Dynamic-tilt suggestion, replaced by re-plan; 'manual' = user/agent work, always preserved"
196
245
  },
197
246
  "anchor": {
198
247
  "type": "object",
199
- "required": ["step"],
248
+ "required": [
249
+ "step"
250
+ ],
200
251
  "additionalProperties": false,
201
252
  "properties": {
202
253
  "step": {
203
- "type": ["string", "integer"],
254
+ "type": [
255
+ "string",
256
+ "integer"
257
+ ],
204
258
  "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
205
259
  },
206
260
  "at": {
207
- "enum": ["start", "end"],
261
+ "enum": [
262
+ "start",
263
+ "end"
264
+ ],
208
265
  "description": "which edge of the step the span's `in` is measured from (default start)"
209
266
  },
210
267
  "offset": {
@@ -212,29 +269,43 @@
212
269
  "description": "seconds from that edge to the span's `in` (negative = before it)"
213
270
  }
214
271
  },
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"
272
+ "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 `in`/`out` stay authoritative and lowering never reads it, so it costs nothing at render time"
216
273
  }
217
274
  },
218
275
  "additionalProperties": true
219
276
  }
220
277
  },
221
278
  "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."
279
+ "enum": [
280
+ "off",
281
+ "subtle",
282
+ "medium",
283
+ "strong"
284
+ ],
285
+ "description": "Dynamic-tilt wand intensity: derives source:'auto' tilt spans FROM the zoom spans, leaning toward each zoom's focus (max 5/9/14° per axis). 'off'/absent = no auto tilt; manual spans work either way."
224
286
  },
225
287
  "rejected": {
226
288
  "type": "array",
227
289
  "description": "Deleted planner proposals, kept so no re-plan proposes them again (\"not this one\"). Each entry is the lane and the SOURCE extent of an auto span you deleted; a fresh proposal on that lane overlapping the extent by half of the shorter span is dropped by vos plan, by plan --reuse (which re-times these like manual spans) and by the studio's re-plans. Manual spans never need this: a re-plan keeps them by contract. The renderer never reads it.",
228
290
  "items": {
229
291
  "type": "object",
230
- "required": ["id", "lane", "in", "out"],
292
+ "required": [
293
+ "id",
294
+ "lane",
295
+ "in",
296
+ "out"
297
+ ],
231
298
  "properties": {
232
299
  "id": {
233
300
  "type": "string",
234
301
  "description": "stable identity (`r{n}`)"
235
302
  },
236
303
  "lane": {
237
- "enum": ["zoom", "tilt", "speed"],
304
+ "enum": [
305
+ "zoom",
306
+ "tilt",
307
+ "speed"
308
+ ],
238
309
  "description": "which planner's proposals this rejects"
239
310
  },
240
311
  "in": {
@@ -249,15 +320,23 @@
249
320
  "anchor": {
250
321
  "type": "object",
251
322
  "description": "the deleted span's step anchor, when it had one, so a re-record re-times the rejection the same way",
252
- "required": ["step"],
323
+ "required": [
324
+ "step"
325
+ ],
253
326
  "additionalProperties": false,
254
327
  "properties": {
255
328
  "step": {
256
- "type": ["string", "integer"],
329
+ "type": [
330
+ "string",
331
+ "integer"
332
+ ],
257
333
  "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
258
334
  },
259
335
  "at": {
260
- "enum": ["start", "end"],
336
+ "enum": [
337
+ "start",
338
+ "end"
339
+ ],
261
340
  "description": "which edge of the step the span's `in` is measured from (default start)"
262
341
  },
263
342
  "offset": {
@@ -287,24 +366,28 @@
287
366
  "type": "number",
288
367
  "minimum": 0,
289
368
  "maximum": 1,
290
- "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."
369
+ "description": "master gain for the recording's own system/tab audio track split takes (source.micKey) only; legacy takes have one track on micGain. Absent = 1. 0 = muted."
291
370
  },
292
371
  "cursor": {
293
372
  "type": "object",
294
- "description": "Cursor rendering (visible, hideWhenIdle, smoothing, size, click effects). See CursorStyle in @vosjs/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.",
373
+ "description": "Cursor rendering (visible, hideWhenIdle, smoothing, size, click effects). See CursorStyle in @vosjs/studio-core. `visible: false` hides the drawn dot only 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.",
295
374
  "additionalProperties": true
296
375
  },
297
376
  "cam": {
298
377
  "type": "object",
299
- "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.",
378
+ "description": "Webcam bubble style + SOURCE-time window 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.",
300
379
  "additionalProperties": true
301
380
  },
302
381
  "camMotion": {
303
382
  "type": "array",
304
- "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).",
383
+ "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 ~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).",
305
384
  "items": {
306
385
  "type": "object",
307
- "required": ["id", "in", "out"],
386
+ "required": [
387
+ "id",
388
+ "in",
389
+ "out"
390
+ ],
308
391
  "properties": {
309
392
  "id": {
310
393
  "type": "string",
@@ -317,7 +400,7 @@
317
400
  },
318
401
  "out": {
319
402
  "type": "number",
320
- "description": "SOURCE seconds, > in (\u2265 0.5s so the move can settle)"
403
+ "description": "SOURCE seconds, > in ( 0.5s so the move can settle)"
321
404
  },
322
405
  "x": {
323
406
  "type": "number",
@@ -342,12 +425,20 @@
342
425
  "description": "arrival ease (@vosjs/timeline EASINGS name, e.g. 'power2.out'). Absent = the house settle ease."
343
426
  },
344
427
  "transition": {
345
- "enum": ["instant", "fast", "smooth", "slow"],
428
+ "enum": [
429
+ "instant",
430
+ "fast",
431
+ "smooth",
432
+ "slow"
433
+ ],
346
434
  "description": "transition SPEED for this span's morphs. Absent = 'smooth' (~0.65s); 'instant' jump-cuts the bubble to its pose (the layout-cut)."
347
435
  },
348
436
  "source": {
349
437
  "type": "string",
350
- "enum": ["auto", "manual"],
438
+ "enum": [
439
+ "auto",
440
+ "manual"
441
+ ],
351
442
  "description": "reserved for a future planner; spans you add or edit are 'manual'"
352
443
  }
353
444
  }
@@ -360,15 +451,25 @@
360
451
  "properties": {
361
452
  "background": {
362
453
  "type": "string",
363
- "description": "CSS background (gradient/color) \u2014 the underlay/fallback, always painted under backgroundMedia"
454
+ "description": "CSS background (gradient/color) the underlay/fallback, always painted under backgroundMedia"
364
455
  },
365
456
  "backgroundMedia": {
366
- "type": ["object", "null"],
367
- "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.",
368
- "required": ["kind", "key", "dim"],
457
+ "type": [
458
+ "object",
459
+ "null"
460
+ ],
461
+ "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) — 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.",
462
+ "required": [
463
+ "kind",
464
+ "key",
465
+ "dim"
466
+ ],
369
467
  "properties": {
370
468
  "kind": {
371
- "enum": ["video", "image"]
469
+ "enum": [
470
+ "video",
471
+ "image"
472
+ ]
372
473
  },
373
474
  "key": {
374
475
  "type": "string",
@@ -377,7 +478,7 @@
377
478
  "duration": {
378
479
  "type": "number",
379
480
  "exclusiveMinimum": 0,
380
- "description": "loop length in seconds (video only \u2014 required for the modulo loop)"
481
+ "description": "loop length in seconds (video only required for the modulo loop)"
381
482
  },
382
483
  "vosId": {
383
484
  "type": "string",
@@ -400,7 +501,7 @@
400
501
  "type": "number",
401
502
  "minimum": 0,
402
503
  "maximum": 100,
403
- "description": "V2: blur radius in design px \u2014 softens the media behind the card"
504
+ "description": "V2: blur radius in design px softens the media behind the card"
404
505
  }
405
506
  },
406
507
  "additionalProperties": true
@@ -414,7 +515,46 @@
414
515
  "shadow": {
415
516
  "type": "number",
416
517
  "minimum": 0,
417
- "maximum": 1
518
+ "maximum": 1,
519
+ "description": "ambient shadow strength: the wide soft layer under the card (blur 60, offset 24 design px)"
520
+ },
521
+ "shadowContact": {
522
+ "type": "number",
523
+ "minimum": 0,
524
+ "maximum": 1,
525
+ "description": "contact shadow strength: a second tight layer (blur 10, offset 3 design px) over the ambient one, what makes a light card sit on a light ground. Absent = 0"
526
+ },
527
+ "shadowColor": {
528
+ "type": "string",
529
+ "pattern": "^#[0-9a-fA-F]{6}$",
530
+ "description": "shadow colour as #rrggbb; the strengths are its alpha. Absent = black"
531
+ },
532
+ "inset": {
533
+ "type": "object",
534
+ "additionalProperties": false,
535
+ "description": "per-side card placement as FRACTIONS of the frame (left/right of its width, top/bottom of its height), each overriding `padding` on its side; a NEGATIVE side bleeds the card past the frame edge (headroom above, the window running off the bottom). Absent sides keep padding. Under contain the card centres inside the inset area; under cover the inset area is the card",
536
+ "properties": {
537
+ "top": {
538
+ "type": "number",
539
+ "minimum": -2,
540
+ "maximum": 0.9
541
+ },
542
+ "right": {
543
+ "type": "number",
544
+ "minimum": -2,
545
+ "maximum": 0.9
546
+ },
547
+ "bottom": {
548
+ "type": "number",
549
+ "minimum": -2,
550
+ "maximum": 0.9
551
+ },
552
+ "left": {
553
+ "type": "number",
554
+ "minimum": -2,
555
+ "maximum": 0.9
556
+ }
557
+ }
418
558
  },
419
559
  "border": {
420
560
  "type": "number",
@@ -443,8 +583,11 @@
443
583
  },
444
584
  "fit": {
445
585
  "type": "string",
446
- "enum": ["contain", "cover"],
447
- "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"
586
+ "enum": [
587
+ "contain",
588
+ "cover"
589
+ ],
590
+ "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` — what a 440x280 store tile or a 2.5:1 marquee demands"
448
591
  },
449
592
  "focus": {
450
593
  "type": "object",
@@ -472,6 +615,36 @@
472
615
  "description": "'mac' | 'windows' | 'minimal' | 'none' | 'original' (window takes with a chrome crop)"
473
616
  }
474
617
  }
618
+ },
619
+ "entrance": {
620
+ "type": "object",
621
+ "additionalProperties": false,
622
+ "required": [
623
+ "kind"
624
+ ],
625
+ "properties": {
626
+ "kind": {
627
+ "enum": [
628
+ "tilt-in",
629
+ "pull-out",
630
+ "rise",
631
+ "none"
632
+ ]
633
+ },
634
+ "seconds": {
635
+ "type": "number",
636
+ "minimum": 0.2,
637
+ "maximum": 3
638
+ }
639
+ },
640
+ "description": "how the card ENTERS at t = 0: tilt-in swings in from a perspective pose while the card settles up and in; pull-out opens zoomed in and pulls out; rise settles up and in flat. Lowered into the tilt or zoom track's head plus a card-pose track. Absent = none"
641
+ },
642
+ "focusFollow": {
643
+ "type": "string",
644
+ "enum": [
645
+ "camera"
646
+ ],
647
+ "description": "under fit: cover, the crop follows the zoom track's focus (a 9:16 cut of a 16:9 take keeps the affordance in frame). Absent = focus, or the centre"
475
648
  }
476
649
  }
477
650
  },
@@ -483,6 +656,27 @@
483
656
  },
484
657
  "export": {
485
658
  "$ref": "#/definitions/export"
659
+ },
660
+ "endCard": {
661
+ "type": "object",
662
+ "additionalProperties": false,
663
+ "properties": {
664
+ "seconds": {
665
+ "type": "number",
666
+ "minimum": 1,
667
+ "maximum": 8
668
+ },
669
+ "headline": {
670
+ "type": "string"
671
+ },
672
+ "sub": {
673
+ "type": "string"
674
+ },
675
+ "wordmark": {
676
+ "type": "string"
677
+ }
678
+ },
679
+ "description": "the end card the clip closes on: a hold on the last frame (default 2.5 s) while the card recedes and the words rise as the house title, caption and label overlays. The clip's last frame is its poster"
486
680
  }
487
681
  }
488
682
  },
@@ -490,7 +684,9 @@
490
684
  "title": "Program document",
491
685
  "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.",
492
686
  "type": "object",
493
- "required": ["program"],
687
+ "required": [
688
+ "program"
689
+ ],
494
690
  "additionalProperties": true,
495
691
  "properties": {
496
692
  "program": {
@@ -537,17 +733,27 @@
537
733
  "definitions": {
538
734
  "overlays": {
539
735
  "type": "array",
540
- "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.",
736
+ "description": "Compositor v2 (V1): screen-space text clips drawn ABOVE the card, outside the zoom transform. OUTPUT-anchored (start is final-cut seconds trims/speed never retime a title). Optional; absent = none.",
541
737
  "items": {
542
738
  "type": "object",
543
- "required": ["id", "kind", "start", "duration", "transform"],
739
+ "required": [
740
+ "id",
741
+ "kind",
742
+ "start",
743
+ "duration",
744
+ "transform"
745
+ ],
544
746
  "additionalProperties": true,
545
747
  "properties": {
546
748
  "id": {
547
749
  "type": "string"
548
750
  },
549
751
  "kind": {
550
- "enum": ["text", "image", "video"]
752
+ "enum": [
753
+ "text",
754
+ "image",
755
+ "video"
756
+ ]
551
757
  },
552
758
  "start": {
553
759
  "type": "number",
@@ -563,7 +769,11 @@
563
769
  "description": "content; \\n breaks lines"
564
770
  },
565
771
  "preset": {
566
- "enum": ["title", "caption", "label"],
772
+ "enum": [
773
+ "title",
774
+ "caption",
775
+ "label"
776
+ ],
567
777
  "description": "house style: title = Lexend 600 64px, caption = Lexend 400 32px, label = JetBrains Mono 22px"
568
778
  },
569
779
  "size": {
@@ -578,20 +788,24 @@
578
788
  },
579
789
  "family": {
580
790
  "type": "string",
581
- "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"
791
+ "description": "font family override a catalog family name (GET https://vos.so/api/fonts). Unknown names fail open to the preset stack on the render fleet"
582
792
  },
583
793
  "weight": {
584
794
  "type": "number",
585
795
  "minimum": 100,
586
796
  "maximum": 900,
587
- "description": "weight override \u2014 snapped to the nearest weight the catalog hosts for the family (weights are files, not synthesis)"
797
+ "description": "weight override snapped to the nearest weight the catalog hosts for the family (weights are files, not synthesis)"
588
798
  },
589
799
  "italic": {
590
800
  "type": "boolean",
591
801
  "description": "synthesized oblique (no italic files are hosted)"
592
802
  },
593
803
  "align": {
594
- "enum": ["left", "center", "right"],
804
+ "enum": [
805
+ "left",
806
+ "center",
807
+ "right"
808
+ ],
595
809
  "description": "multi-line alignment within the block (default center)"
596
810
  },
597
811
  "letterSpacing": {
@@ -608,7 +822,10 @@
608
822
  },
609
823
  "stroke": {
610
824
  "type": "object",
611
- "required": ["color", "width"],
825
+ "required": [
826
+ "color",
827
+ "width"
828
+ ],
612
829
  "additionalProperties": false,
613
830
  "description": "text outline drawn under the fill",
614
831
  "properties": {
@@ -625,7 +842,9 @@
625
842
  },
626
843
  "box": {
627
844
  "type": "object",
628
- "required": ["color"],
845
+ "required": [
846
+ "color"
847
+ ],
629
848
  "additionalProperties": false,
630
849
  "description": "background pill behind the text block (text only; absent = none). Paddings/radius are EMs of the resolved font size",
631
850
  "properties": {
@@ -660,19 +879,22 @@
660
879
  },
661
880
  "transform": {
662
881
  "type": "object",
663
- "required": ["x", "y"],
882
+ "required": [
883
+ "x",
884
+ "y"
885
+ ],
664
886
  "properties": {
665
887
  "x": {
666
888
  "type": "number",
667
889
  "minimum": -0.5,
668
890
  "maximum": 1.5,
669
- "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)"
891
+ "description": "anchor CENTER as a FRACTION of the frame width [0..1] (0.5 = center at ANY aspect the zoom cx/cy convention; NOT pixels)"
670
892
  },
671
893
  "y": {
672
894
  "type": "number",
673
895
  "minimum": -0.5,
674
896
  "maximum": 1.5,
675
- "description": "anchor CENTER as a FRACTION of the frame height [0..1] (lower-third \u2248 0.82; NOT pixels)"
897
+ "description": "anchor CENTER as a FRACTION of the frame height [0..1] (lower-third 0.82; NOT pixels)"
676
898
  },
677
899
  "scale": {
678
900
  "type": "number",
@@ -693,12 +915,19 @@
693
915
  "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)"
694
916
  },
695
917
  "shadow": {
696
- "enum": ["none", "soft", "strong"],
918
+ "enum": [
919
+ "none",
920
+ "soft",
921
+ "strong"
922
+ ],
697
923
  "description": "media kinds: the card shadow. Absent = 'soft' (the house picture-in-picture look); 'strong' floats harder; 'none' is a flat cutout"
698
924
  },
699
925
  "border": {
700
926
  "type": "object",
701
- "required": ["color", "width"],
927
+ "required": [
928
+ "color",
929
+ "width"
930
+ ],
702
931
  "properties": {
703
932
  "color": {
704
933
  "type": "string",
@@ -714,19 +943,29 @@
714
943
  "description": "media kinds: an outline stroke over the clipped media edge. Absent = none"
715
944
  },
716
945
  "enter": {
717
- "enum": ["none", "fade", "rise"],
946
+ "enum": [
947
+ "none",
948
+ "fade",
949
+ "rise"
950
+ ],
718
951
  "description": "default rise (~0.35s); IGNORED while fx is set (fx owns the entrance)"
719
952
  },
720
953
  "exit": {
721
- "enum": ["none", "fade", "rise"],
954
+ "enum": [
955
+ "none",
956
+ "fade",
957
+ "rise"
958
+ ],
722
959
  "description": "default fade (~0.35s); stays clip-level even with fx"
723
960
  },
724
961
  "motion": {
725
962
  "type": "array",
726
- "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.",
963
+ "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) scrub, export and chunked server renders agree.",
727
964
  "items": {
728
965
  "type": "object",
729
- "required": ["at"],
966
+ "required": [
967
+ "at"
968
+ ],
730
969
  "properties": {
731
970
  "at": {
732
971
  "type": "number",
@@ -769,25 +1008,40 @@
769
1008
  },
770
1009
  "fx": {
771
1010
  "type": "object",
772
- "description": "text kind only: entrance animation evaluated per unit \u2014 pure f(t), segmentation baked at lowering (scrub/seek/server chunks agree by construction)",
1011
+ "description": "text kind only: entrance animation evaluated per unit pure f(t), segmentation baked at lowering (scrub/seek/server chunks agree by construction)",
773
1012
  "properties": {
774
1013
  "fx": {
775
- "enum": ["fade", "rise", "pop", "blur", "typewriter"],
1014
+ "enum": [
1015
+ "fade",
1016
+ "rise",
1017
+ "pop",
1018
+ "blur",
1019
+ "typewriter"
1020
+ ],
776
1021
  "description": "the entrance; typewriter is a step reveal by unit count"
777
1022
  },
778
1023
  "unit": {
779
- "enum": ["block", "line", "word", "char"],
780
- "description": "what animates as one thing (default block \u2014 the whole text; char is grapheme-safe)"
1024
+ "enum": [
1025
+ "block",
1026
+ "line",
1027
+ "word",
1028
+ "char"
1029
+ ],
1030
+ "description": "what animates as one thing (default block — the whole text; char is grapheme-safe)"
781
1031
  },
782
1032
  "direction": {
783
- "enum": ["forward", "reverse", "center"],
1033
+ "enum": [
1034
+ "forward",
1035
+ "reverse",
1036
+ "center"
1037
+ ],
784
1038
  "description": "unit start order (default forward; center ripples outward)"
785
1039
  },
786
1040
  "stagger": {
787
1041
  "type": "number",
788
1042
  "minimum": 0,
789
1043
  "maximum": 2,
790
- "description": "seconds between unit starts (defaults: typewriter 0.05, others 0.06 when unit \u2260 block; clamped so the entrance fits the clip)"
1044
+ "description": "seconds between unit starts (defaults: typewriter 0.05, others 0.06 when unit block; clamped so the entrance fits the clip)"
791
1045
  },
792
1046
  "duration": {
793
1047
  "type": "number",
@@ -796,18 +1050,20 @@
796
1050
  "description": "per-unit seconds (default 0.35); typewriter ignores it"
797
1051
  }
798
1052
  },
799
- "required": ["fx"],
1053
+ "required": [
1054
+ "fx"
1055
+ ],
800
1056
  "additionalProperties": false
801
1057
  },
802
1058
  "key": {
803
1059
  "type": "string",
804
- "description": "image/video kinds: media URL or take-dir file (e.g. \"/logo.png\", \"/clip.webm\") \u2014 required for media"
1060
+ "description": "image/video kinds: media URL or take-dir file (e.g. \"/logo.png\", \"/clip.webm\") required for media"
805
1061
  },
806
1062
  "width": {
807
1063
  "type": "number",
808
1064
  "exclusiveMinimum": 0,
809
1065
  "maximum": 1,
810
- "description": "media kinds: base width as a FRACTION of the frame width (default 0.35); height follows the media's aspect; \u00d7 transform.scale"
1066
+ "description": "media kinds: base width as a FRACTION of the frame width (default 0.35); height follows the media's aspect; × transform.scale"
811
1067
  },
812
1068
  "radius": {
813
1069
  "type": "number",
@@ -829,10 +1085,14 @@
829
1085
  },
830
1086
  "objects": {
831
1087
  "type": "array",
832
- "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).",
1088
+ "description": "Compositor v2 (V3): world-space 3D props between the card and the overlays the drafted V4 engine spec. Curated primitives, GLB models by key, and extruded 3D text (TX7).",
833
1089
  "items": {
834
1090
  "type": "object",
835
- "required": ["id", "asset", "transform3d"],
1091
+ "required": [
1092
+ "id",
1093
+ "asset",
1094
+ "transform3d"
1095
+ ],
836
1096
  "additionalProperties": true,
837
1097
  "properties": {
838
1098
  "id": {
@@ -840,13 +1100,24 @@
840
1100
  },
841
1101
  "asset": {
842
1102
  "type": "object",
843
- "required": ["kind"],
1103
+ "required": [
1104
+ "kind"
1105
+ ],
844
1106
  "properties": {
845
1107
  "kind": {
846
- "enum": ["primitive", "gltf", "text3d"]
1108
+ "enum": [
1109
+ "primitive",
1110
+ "gltf",
1111
+ "text3d"
1112
+ ]
847
1113
  },
848
1114
  "shape": {
849
- "enum": ["cube", "sphere", "torus", "knot"],
1115
+ "enum": [
1116
+ "cube",
1117
+ "sphere",
1118
+ "torus",
1119
+ "knot"
1120
+ ],
850
1121
  "description": "primitive kind"
851
1122
  },
852
1123
  "color": {
@@ -855,7 +1126,7 @@
855
1126
  },
856
1127
  "key": {
857
1128
  "type": "string",
858
- "description": "gltf kind: GLB URL or take-dir file (e.g. \"/model.glb\") \u2014 bbox-normalized so scale means the same as primitives"
1129
+ "description": "gltf kind: GLB URL or take-dir file (e.g. \"/model.glb\") bbox-normalized so scale means the same as primitives"
859
1130
  },
860
1131
  "text": {
861
1132
  "type": "string",
@@ -867,7 +1138,12 @@
867
1138
  "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"
868
1139
  },
869
1140
  "material": {
870
- "enum": ["standard", "metal", "glass", "neon"],
1141
+ "enum": [
1142
+ "standard",
1143
+ "metal",
1144
+ "glass",
1145
+ "neon"
1146
+ ],
871
1147
  "description": "text3d kind: fleet-audited material preset (single-sided, no dispersion; default standard)"
872
1148
  },
873
1149
  "depth": {
@@ -899,7 +1175,10 @@
899
1175
  },
900
1176
  "transform3d": {
901
1177
  "type": "object",
902
- "required": ["x", "y"],
1178
+ "required": [
1179
+ "x",
1180
+ "y"
1181
+ ],
903
1182
  "properties": {
904
1183
  "x": {
905
1184
  "type": "number",
@@ -934,15 +1213,21 @@
934
1213
  "additionalProperties": true
935
1214
  },
936
1215
  "animation": {
937
- "enum": ["spin", "float", null],
938
- "description": "curated deterministic motion \u2014 composes ADDITIVELY on top of motion poses"
1216
+ "enum": [
1217
+ "spin",
1218
+ "float",
1219
+ null
1220
+ ],
1221
+ "description": "curated deterministic motion — composes ADDITIVELY on top of motion poses"
939
1222
  },
940
1223
  "motion": {
941
1224
  "type": "array",
942
1225
  "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).",
943
1226
  "items": {
944
1227
  "type": "object",
945
- "required": ["at"],
1228
+ "required": [
1229
+ "at"
1230
+ ],
946
1231
  "properties": {
947
1232
  "at": {
948
1233
  "type": "number",
@@ -1066,17 +1351,31 @@
1066
1351
  },
1067
1352
  "export": {
1068
1353
  "type": "object",
1069
- "required": ["resolution", "fps", "format"],
1354
+ "required": [
1355
+ "resolution",
1356
+ "fps",
1357
+ "format"
1358
+ ],
1070
1359
  "properties": {
1071
1360
  "resolution": {
1072
- "enum": ["720p", "1080p", "2k", "4k"],
1361
+ "enum": [
1362
+ "720p",
1363
+ "1080p",
1364
+ "2k",
1365
+ "4k"
1366
+ ],
1073
1367
  "description": "names the SHORT edge. Presets above the footage's capture width upscale (validate warns)."
1074
1368
  },
1075
1369
  "fps": {
1076
- "enum": [30, 60]
1370
+ "enum": [
1371
+ 30,
1372
+ 60
1373
+ ]
1077
1374
  },
1078
1375
  "format": {
1079
- "enum": ["mp4"],
1376
+ "enum": [
1377
+ "mp4"
1378
+ ],
1080
1379
  "description": "doc-level default; the CLI render's --format flag (webm|mp4) overrides"
1081
1380
  }
1082
1381
  },
@@ -1084,10 +1383,15 @@
1084
1383
  },
1085
1384
  "speed": {
1086
1385
  "type": "array",
1087
- "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).",
1386
+ "description": "Speed-change spans (SOURCE time, footage-anchored, non-overlapping). Absent = all 1×. The planner proposes source:'auto' spans for typing passages, scroll runs and idle gaps (ids s0); set source:'manual' on spans you add or edit so they survive a re-plan (absent source also counts as manual).",
1088
1387
  "items": {
1089
1388
  "type": "object",
1090
- "required": ["id", "in", "out", "rate"],
1389
+ "required": [
1390
+ "id",
1391
+ "in",
1392
+ "out",
1393
+ "rate"
1394
+ ],
1091
1395
  "properties": {
1092
1396
  "id": {
1093
1397
  "type": "string"
@@ -1106,20 +1410,31 @@
1106
1410
  "description": "playback rate: 2 = twice as fast"
1107
1411
  },
1108
1412
  "source": {
1109
- "enum": ["auto", "manual"],
1413
+ "enum": [
1414
+ "auto",
1415
+ "manual"
1416
+ ],
1110
1417
  "description": "'auto' = planner suggestion (typing/scroll/idle), replaced by re-plan; 'manual' (or absent) = user/agent work, always preserved"
1111
1418
  },
1112
1419
  "anchor": {
1113
1420
  "type": "object",
1114
- "required": ["step"],
1421
+ "required": [
1422
+ "step"
1423
+ ],
1115
1424
  "additionalProperties": false,
1116
1425
  "properties": {
1117
1426
  "step": {
1118
- "type": ["string", "integer"],
1427
+ "type": [
1428
+ "string",
1429
+ "integer"
1430
+ ],
1119
1431
  "description": "the actions.json step: its `id` when it has one, else its record-time index (meta.steps)"
1120
1432
  },
1121
1433
  "at": {
1122
- "enum": ["start", "end"],
1434
+ "enum": [
1435
+ "start",
1436
+ "end"
1437
+ ],
1123
1438
  "description": "which edge of the step the span's `in` is measured from (default start)"
1124
1439
  },
1125
1440
  "offset": {
@@ -1127,7 +1442,7 @@
1127
1442
  "description": "seconds from that edge to the span's `in` (negative = before it)"
1128
1443
  }
1129
1444
  },
1130
- "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"
1445
+ "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 `in`/`out` stay authoritative and lowering never reads it, so it costs nothing at render time"
1131
1446
  }
1132
1447
  },
1133
1448
  "additionalProperties": true