@vosjs/cli 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,6 +89,8 @@ vos plan take --reuse # re-time that cut onto
89
89
 
90
90
  **Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video. It writes `digest/digest.json`: one moment per thing the cursor track says mattered (click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail, and frame-diff scene changes), each with source and output extents, a normalized `focus` and `rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids; plus one footage frame and a crop around the target per moment, and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
91
91
 
92
+ **Many media in one take (concat).** `doc.json` may carry `media: [{ id, videoKey, cursor, meta, … }]`, the take's OTHER recordings or uploads, each the `source` shape with an `id`; a segment on one (`{ in, out, media: "m1" }`) plays it, and a zoom, tilt, speed, freeze or cam-move span names the media its source seconds belong to (absent = the primary, `source`). The Video row shows the clips in order, a span is drawn where its media plays, `vos plan` proposes zoom and speed spans on every media from its own cursor track, `vos validate` measures each span against its own media's length, and `vos push` and `vos pull --media` carry every media through the recording door. A media wears its OWN card: `media[].frame` carries the card-owned fields (its placement and size as `inset`, the browser bar, the corner, the shadow, the border, the cover fit and its focus) over the take's frame while it plays; the bar names that media's recorded page, an upload with no page wears none, and the frame-wide fields (the aspect, the padding, the ground, the backdrop, the card's animation) stay the take's. Sound: the primary's tracks play at the primary's moments; another media's own audio is not spliced into the cut yet.
93
+
92
94
  **A series shares its look by data.** `vos plan <take> --style <seed doc.json | vosId>` copies the seed's `zoomStyle`, `zoomParams`, `speedParams`, `tiltStyle`, `frame`, `cursor`, `cam` and `export` onto a new take (never its spans, overlays or audio) and re-plans the automatic spans under them. A seed that is a POSTER carries its layout too: its card placement, its `stage-*` clips by id (with the release's words from LAUNCH.md's `headline` and `kicker` roles, BRAND.md's `wordmark`, or `--headline` and `--kicker` patched in; the brand's mark from BRAND.md `logoUrl` fetched into `<take>/brand/` for `stage-mark`), its rest lean, its trailing freeze. What could not follow is said in words.
93
95
 
94
96
  **The cut's motion is the document's, in one vocabulary.** Every visual thing carries `anim` (`enter`, `exit`, `idle`; a kind, or a step with its seconds and, for words, `unit`, `direction`, `stagger`): the card (`frame.anim`, entering by `tilt-in`, `pull-out`, `rise` or `fade` and leaving by `recede` or `fade`), a text, image or video clip, a prop. The output lasts until the last clip ends; the card is on screen exactly while its clip runs (the footage, freezes included) and leaves at its end, so its `exit` plays over the clip's last seconds like every clip's; past the footage the clips play over the ground alone. A FREEZE (`freeze: [{id, at, seconds}]`, a source moment held for output seconds, beside the speed spans on the retime lane) freezes the footage anywhere, the middle of the take included; a segment's older `hold` is read as a freeze at its end. There is no end-card field and no entrance field. A COMPONENT is a TEMPLATE: a plain take on a shelf whose clips carry stable ids, laid onto a take at an anchor and stamped `from` with where its clips came from. A fresh `vos plan` proposes the card's enter, the templates the recipe names (`LAUNCH.md` `with: <ref>[@end|@start|@step:<id>|@<seconds>], …` or `--with`, repeatable; `endCard: <ref>` names one at the end), the END CARD when `endCard` is on or absent: the official `End card` template on vos.so (the platform's `Templates` project, promoted; any official template resolves by its title, `--with "End card"`, `--style "Split cover, landscape"`), laid at the end with the release's words and BRAND.md's mark, stamped `from: endcard`; offline, the house clips stand in (a freeze of the last frame for the card's seconds, the card receding over it, the headline, the release line, the wordmark and the mark as clips over the freeze), a caption per `actions.json` step, a music bed and click sounds, from LAUNCH.md's `entrance`, `endCard`, `with`, `captions`, `music` and `clicks` roles (or the flags), as data in doc.json with stable ids (`bed`, `click-<n>`, `caption-<step>`), so the studio shows what the kit will render; a refresh never re-proposes (a deleted clip stays deleted) and `--motion` re-proposes on purpose, replacing only its own work. A document written in the older spellings (`frame.entrance`, `endCard`, a clip's `enter`/`exit`/`fx`, a prop's `animation`) is read into the vocabulary and `vos validate` says so.
@@ -186,16 +186,17 @@ import {
186
186
  CAM_SPAN_MIN,
187
187
  CARD_ENTER_KINDS,
188
188
  CARD_EXIT_KINDS,
189
- IDLE_KINDS,
190
- MEDIA_ENTER_KINDS,
191
- MEDIA_EXIT_KINDS,
192
- TEXT_ENTER_KINDS,
193
- TEXT_EXIT_KINDS,
194
189
  EXPORT_RESOLUTION_OPTIONS,
195
190
  FREEZE_SECONDS_MAX,
196
191
  FREEZE_SECONDS_MIN,
192
+ IDLE_KINDS,
193
+ MEDIA_ENTER_KINDS,
194
+ MEDIA_EXIT_KINDS,
195
+ MEDIA_FRAME_KEYS,
197
196
  SPEED_RATE_MAX,
198
197
  SPEED_RATE_MIN,
198
+ TEXT_ENTER_KINDS,
199
+ TEXT_EXIT_KINDS,
199
200
  TILT_DEG_MAX,
200
201
  TILT_SPAN_MIN,
201
202
  ZOOM_LEVEL_MAX,
@@ -357,6 +358,59 @@ function lintDoc(docIn) {
357
358
  const program = isObj(doc.program) ? doc.program : void 0;
358
359
  const programConfig = program && isObj(program.config) ? program.config : void 0;
359
360
  const duration = recording ? (isNum(meta.durationMs) ? meta.durationMs : 0) / 1e3 : program && isNum(program.duration) ? program.duration : programConfig && isNum(programConfig.duration) ? programConfig.duration : 0;
361
+ const mediaLen = /* @__PURE__ */ new Map();
362
+ if (recording && doc.media !== void 0) {
363
+ if (!Array.isArray(doc.media)) problems.push("media must be an array");
364
+ for (const [i, raw] of (Array.isArray(doc.media) ? doc.media : []).entries()) {
365
+ const m = isObj(raw) ? raw : {};
366
+ const id = typeof m.id === "string" ? m.id : "";
367
+ if (!id) problems.push(`media[${i}]: id must be a string`);
368
+ else if (mediaLen.has(id))
369
+ problems.push(`media[${i}] (${id}): duplicate id`);
370
+ const mf = m.frame;
371
+ if (mf !== void 0) {
372
+ if (!isObj(mf)) {
373
+ problems.push(
374
+ `media[${i}]${id ? ` (${id})` : ""}: frame must be an object`
375
+ );
376
+ } else {
377
+ for (const k of Object.keys(mf)) {
378
+ if (!MEDIA_FRAME_KEYS.includes(k))
379
+ problems.push(
380
+ `media[${i}]${id ? ` (${id})` : ""}.frame.${k}: not a card field (a media's frame carries ${MEDIA_FRAME_KEYS.join(", ")}; the rest is the take's frame)`
381
+ );
382
+ }
383
+ }
384
+ }
385
+ if (typeof m.videoKey !== "string")
386
+ problems.push(
387
+ `media[${i}]${id ? ` (${id})` : ""}: videoKey must be a string`
388
+ );
389
+ const mm = isObj(m.meta) ? m.meta : {};
390
+ const ms = isNum(mm.durationMs) ? mm.durationMs : 0;
391
+ if (id) mediaLen.set(id, ms / 1e3);
392
+ }
393
+ }
394
+ const checkByMedia = (list, label) => {
395
+ const groups = /* @__PURE__ */ new Map();
396
+ for (const s of list) {
397
+ const m = typeof s.media === "string" ? s.media : "";
398
+ if (m && !mediaLen.has(m)) {
399
+ problems.push(
400
+ `${spanName(label, s)}: media "${m}" is not one of media[].id`
401
+ );
402
+ continue;
403
+ }
404
+ groups.set(m, [...groups.get(m) ?? [], s]);
405
+ }
406
+ for (const [m, group] of groups)
407
+ checkSpanList(
408
+ group,
409
+ label,
410
+ m ? mediaLen.get(m) ?? 0 : duration,
411
+ problems
412
+ );
413
+ };
360
414
  if (!recording) {
361
415
  if (!program)
362
416
  problems.push(
@@ -373,7 +427,7 @@ function lintDoc(docIn) {
373
427
  problems.push("zoom must be an array of spans");
374
428
  const zoom = recording ? entries(doc.zoom) : [];
375
429
  if (recording) {
376
- checkSpanList(zoom, "zoom", duration, problems);
430
+ checkByMedia(zoom, "zoom");
377
431
  for (const z of zoom) {
378
432
  const name = spanName("zoom", z);
379
433
  if (!isNum(z.level) || z.level < ZOOM_LEVEL_MIN - EPS || z.level > ZOOM_LEVEL_MAX + EPS) {
@@ -427,9 +481,13 @@ function lintDoc(docIn) {
427
481
  problems.push(`${name}: id must be a string`);
428
482
  else if (freezeIds.has(f.id)) problems.push(`${name}: duplicate id`);
429
483
  else freezeIds.add(f.id);
430
- if (!isNum(f.at) || f.at < 0 || f.at > duration + EPS) {
484
+ const fm = typeof f.media === "string" ? f.media : "";
485
+ const fLen = fm ? mediaLen.get(fm) : duration;
486
+ if (fm && fLen === void 0) {
487
+ problems.push(`${name}: media "${fm}" is not one of media[].id`);
488
+ } else if (!isNum(f.at) || f.at < 0 || f.at > (fLen ?? duration) + EPS) {
431
489
  problems.push(
432
- `${name}: at must be a SOURCE moment inside the recording (0..${duration.toFixed(2)}s, got ${String(f.at)})`
490
+ `${name}: at must be a SOURCE moment inside the recording (0..${(fLen ?? duration).toFixed(2)}s, got ${String(f.at)})`
433
491
  );
434
492
  }
435
493
  if (!isNum(f.seconds) || f.seconds < FREEZE_SECONDS_MIN - EPS || f.seconds > FREEZE_SECONDS_MAX + EPS) {
@@ -471,10 +529,10 @@ function lintDoc(docIn) {
471
529
  if (doc.segments !== void 0 && !Array.isArray(doc.segments)) {
472
530
  problems.push("segments must be an array of {in, out} spans");
473
531
  }
474
- checkSpanList(entries(doc.segments), "segment", duration, problems);
532
+ checkByMedia(entries(doc.segments), "segment");
475
533
  }
476
534
  const speed = entries(doc.speed);
477
- checkSpanList(speed, "speed", duration, problems);
535
+ checkByMedia(speed, "speed");
478
536
  for (const s of speed) {
479
537
  if (!isNum(s.rate) || s.rate < SPEED_RATE_MIN - EPS || s.rate > SPEED_RATE_MAX + EPS) {
480
538
  problems.push(
@@ -487,7 +545,7 @@ function lintDoc(docIn) {
487
545
  problems.push("tilt must be an array of spans");
488
546
  }
489
547
  const tilt = entries(doc.tilt);
490
- checkSpanList(tilt, "tilt", duration, problems);
548
+ checkByMedia(tilt, "tilt");
491
549
  for (const t of tilt) {
492
550
  const name = spanName("tilt", t);
493
551
  for (const ax of ["rx", "ry"]) {
@@ -540,7 +598,7 @@ function lintDoc(docIn) {
540
598
  problems.push("camMotion must be an array of spans");
541
599
  }
542
600
  const camMotion = entries(doc.camMotion);
543
- checkSpanList(camMotion, "camMotion", duration, problems);
601
+ checkByMedia(camMotion, "camMotion");
544
602
  if (camMotion.length && typeof source.camKey !== "string") {
545
603
  warnings.push(
546
604
  "camMotion has spans but the take has no cam track (source.camKey) \u2014 nothing renders them"
@@ -6104,6 +6162,35 @@ function applyStyle(seed, doc, opts) {
6104
6162
  if (opts.words) patchStageWords(next, opts.words);
6105
6163
  return { doc: next, layout: { ...parts, notes } };
6106
6164
  }
6165
+ function planOtherMedia(doc, activity) {
6166
+ const zoom = [];
6167
+ const speed = [];
6168
+ for (const m of doc.media ?? []) {
6169
+ const manualZoom = doc.zoom.filter(
6170
+ (z) => z.media === m.id && z.source === "manual"
6171
+ );
6172
+ const rejected = (doc.rejected ?? []).filter((r) => r.media === m.id);
6173
+ zoom.push(
6174
+ ...planAutoZoom(m.cursor, {
6175
+ width: m.meta.width,
6176
+ height: m.meta.height,
6177
+ style: doc.zoomStyle,
6178
+ params: doc.zoomParams
6179
+ }).filter((z) => !manualZoom.some((x) => overlaps(z, x))).filter((z) => !isRejected("zoom", z, rejected)).map((z) => ({ ...z, id: `${z.id}-${m.id}`, media: m.id }))
6180
+ );
6181
+ const manualSpeed = (doc.speed ?? []).filter(
6182
+ (x) => x.media === m.id && x.source !== "auto"
6183
+ );
6184
+ speed.push(
6185
+ ...planAutoSpeed(m.cursor, {
6186
+ durationMs: m.meta.durationMs,
6187
+ params: doc.speedParams,
6188
+ activity
6189
+ }).filter((x) => !manualSpeed.some((y) => x.in < y.out && x.out > y.in)).filter((x) => !isRejected("speed", x, rejected)).map((x) => ({ ...x, id: `${x.id}-${m.id}`, media: m.id }))
6190
+ );
6191
+ }
6192
+ return { zoom, speed };
6193
+ }
6107
6194
  async function planTake(dir, opts = {}) {
6108
6195
  const take = await loadTake(dir);
6109
6196
  const { meta, cursor } = take;
@@ -6140,6 +6227,17 @@ async function planTake(dir, opts = {}) {
6140
6227
  activity
6141
6228
  }).filter((s) => !manualSpeed.some((m) => s.in < m.out && s.out > m.in)).filter((s) => !isRejected("speed", s, doc.rejected));
6142
6229
  doc.speed = [...manualSpeed, ...autoSpeed].sort((a, b) => a.in - b.in);
6230
+ {
6231
+ const others = planOtherMedia(doc, activity);
6232
+ doc.zoom = [
6233
+ ...doc.zoom.filter((z) => !z.media || z.source === "manual"),
6234
+ ...others.zoom
6235
+ ].sort((a, b) => a.in - b.in);
6236
+ doc.speed = [
6237
+ ...(doc.speed ?? []).filter((x) => !x.media || x.source !== "auto"),
6238
+ ...others.speed
6239
+ ].sort((a, b) => a.in - b.in);
6240
+ }
6143
6241
  if (rt.tilt.length) doc.tilt = rt.tilt;
6144
6242
  if (prev.overlays?.length) doc.overlays = prev.overlays;
6145
6243
  if (prev.objects?.length) doc.objects = prev.objects;
@@ -6164,7 +6262,7 @@ async function planTake(dir, opts = {}) {
6164
6262
  doc = take.doc;
6165
6263
  }
6166
6264
  fresh = false;
6167
- const manual = doc.zoom.filter((z) => z.source === "manual");
6265
+ const manual = doc.zoom.filter((z) => z.source === "manual" && !z.media);
6168
6266
  const auto = planAutoZoom(doc.source.cursor, {
6169
6267
  width: doc.source.meta.width,
6170
6268
  height: doc.source.meta.height,
@@ -6172,13 +6270,26 @@ async function planTake(dir, opts = {}) {
6172
6270
  params: doc.zoomParams
6173
6271
  }).filter((z) => !manual.some((m) => overlaps(z, m))).filter((z) => !isRejected("zoom", z, doc.rejected));
6174
6272
  doc.zoom = [...manual, ...auto].sort((a, b) => a.in - b.in);
6175
- const manualSpeed = (doc.speed ?? []).filter((s) => s.source !== "auto");
6273
+ const manualSpeed = (doc.speed ?? []).filter(
6274
+ (s) => s.source !== "auto" && !s.media
6275
+ );
6176
6276
  const autoSpeed = planAutoSpeed(doc.source.cursor, {
6177
6277
  durationMs: doc.source.meta.durationMs,
6178
6278
  params: doc.speedParams,
6179
6279
  activity
6180
6280
  }).filter((s) => !manualSpeed.some((m) => s.in < m.out && s.out > m.in)).filter((s) => !isRejected("speed", s, doc.rejected));
6181
6281
  doc.speed = [...manualSpeed, ...autoSpeed].sort((a, b) => a.in - b.in);
6282
+ {
6283
+ const others = planOtherMedia(doc, activity);
6284
+ doc.zoom = [
6285
+ ...doc.zoom.filter((z) => !z.media || z.source === "manual"),
6286
+ ...others.zoom
6287
+ ].sort((a, b) => a.in - b.in);
6288
+ doc.speed = [
6289
+ ...(doc.speed ?? []).filter((x) => !x.media || x.source !== "auto"),
6290
+ ...others.speed
6291
+ ].sort((a, b) => a.in - b.in);
6292
+ }
6182
6293
  } else {
6183
6294
  const artifact = {
6184
6295
  videoKey: RECORDING_NAME,
@@ -6256,6 +6367,34 @@ function isTakeRelativeKey(key) {
6256
6367
  var takeRelativeFile = (key) => key.replace(/^\/+/, "");
6257
6368
  function docMediaRefs(doc, keep = isTakeRelativeKey) {
6258
6369
  const out = [];
6370
+ for (const m of doc.media ?? []) {
6371
+ if (keep(m.videoKey))
6372
+ out.push({
6373
+ where: `media ${m.id}`,
6374
+ key: m.videoKey,
6375
+ set: (next) => {
6376
+ m.videoKey = next;
6377
+ }
6378
+ });
6379
+ const mic = m.micKey;
6380
+ if (mic && keep(mic))
6381
+ out.push({
6382
+ where: `media ${m.id} mic`,
6383
+ key: mic,
6384
+ set: (next) => {
6385
+ m.micKey = next;
6386
+ }
6387
+ });
6388
+ const cam = m.camKey;
6389
+ if (cam && keep(cam))
6390
+ out.push({
6391
+ where: `media ${m.id} cam`,
6392
+ key: cam,
6393
+ set: (next) => {
6394
+ m.camKey = next;
6395
+ }
6396
+ });
6397
+ }
6259
6398
  for (const clip of doc.overlays ?? []) {
6260
6399
  if (clip.kind === "text" || !keep(clip.key)) continue;
6261
6400
  out.push({
@@ -10220,4 +10359,4 @@ export {
10220
10359
  convertAgentBrowser,
10221
10360
  run
10222
10361
  };
10223
- //# sourceMappingURL=chunk-SLCQCRNI.js.map
10362
+ //# sourceMappingURL=chunk-TYTUA5L5.js.map