@vosjs/cli 0.25.1 → 0.27.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,11 +89,11 @@ 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.
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. **Many cards**: an image or video overlay may show a document media by reference (`key: "media:<id>"`, `media:` alone the primary; its cursor track and recorded page come with it) and wear a card (`frame`: `browserBar`, a `lean` `{rx, ry}` in degrees, `shadow`, `shadowContact`, `shadowColor`, `cursor`), drawn by the card painter on its own plane above the primary card with the media's cursor dot and click rings inside it; a layer without `frame` stays the flat picture. The primary card stays primary: the sequence, the camera and the cut are its.
93
93
 
94
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.
95
95
 
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.
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. **The frame that STANDS for a take** is `still` (output seconds; `vos plan --still <t>`, LAUNCH.md `still:`), else the take's own last freeze (a poster's rest; a template's freezes, stamped `from`, never count), else the hero moment after the card and the opening clips have entered: the shelf's cover, the kit's card stills and `vos frames --at-still` all read that one derivation, and a plan never moves a still the author set. 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.
97
97
 
98
98
  **A fresh take opens on a backdrop.** `create`, `record` and `plan` put the first ready loop from `GET /api/backdrops` behind the card (its ground colour as `frame.background`); `--background <slug|url|none>` overrides it, and offline the frame stays bare with a note.
99
99
 
@@ -203,6 +203,7 @@ import {
203
203
  ZOOM_LEVEL_MIN,
204
204
  ZOOM_SPAN_MIN,
205
205
  docCardLayout,
206
+ outputEnd,
206
207
  ratedSegments,
207
208
  recommendedExportResolution,
208
209
  spanOutputExtent,
@@ -456,6 +457,22 @@ function lintDoc(docIn) {
456
457
  );
457
458
  }
458
459
  }
460
+ if (recording && doc.still !== void 0) {
461
+ const st = doc.still;
462
+ if (!isNum(st) || st < 0) {
463
+ problems.push("still must be a number \u2265 0 (OUTPUT seconds)");
464
+ } else {
465
+ const rated = ratedSegments(doc);
466
+ const end = outputEnd(
467
+ doc,
468
+ rated.reduce((a, s) => a + (s.out - s.in) / (s.rate ?? 1), 0)
469
+ );
470
+ if (st > end + 1e-6)
471
+ problems.push(
472
+ `still=${String(st)} is past the output's end (${end.toFixed(2)}s)`
473
+ );
474
+ }
475
+ }
459
476
  for (const [i, seg] of (Array.isArray(doc.segments) ? doc.segments : []).entries()) {
460
477
  const hold = seg.hold;
461
478
  if (hold === void 0) continue;
@@ -971,6 +988,39 @@ function lintDoc(docIn) {
971
988
  if (o.fx !== void 0) {
972
989
  problems.push(`${name}.fx is text-only (media clips have enter/exit)`);
973
990
  }
991
+ if (typeof o.key === "string" && o.key.startsWith("media:")) {
992
+ const ref = o.key.slice("media:".length);
993
+ const ids = new Set(
994
+ (Array.isArray(doc.media) ? doc.media : []).map((m) => typeof m.id === "string" ? m.id : "")
995
+ );
996
+ if (ref !== "" && !ids.has(ref))
997
+ problems.push(
998
+ `${name}.key names media "${ref}", which is not one of media[].id (media: alone is the primary)`
999
+ );
1000
+ }
1001
+ const lcf = o.frame;
1002
+ if (lcf !== void 0) {
1003
+ if (!isObj(lcf)) {
1004
+ problems.push(`${name}.frame must be an object (the layer's card)`);
1005
+ } else {
1006
+ const LCF = [
1007
+ "browserBar",
1008
+ "lean",
1009
+ "shadow",
1010
+ "shadowContact",
1011
+ "shadowColor",
1012
+ "cursor"
1013
+ ];
1014
+ for (const k of Object.keys(lcf))
1015
+ if (!LCF.includes(k))
1016
+ problems.push(
1017
+ `${name}.frame.${k}: not a layer card field (${LCF.join(", ")})`
1018
+ );
1019
+ const lean = lcf.lean;
1020
+ if (lean !== void 0 && (!isObj(lean) || !isNum(lean.rx) || !isNum(lean.ry)))
1021
+ problems.push(`${name}.frame.lean must be { rx, ry } in degrees`);
1022
+ }
1023
+ }
974
1024
  if (typeof o.key !== "string" || o.key.length === 0) {
975
1025
  problems.push(
976
1026
  `${name}.key must be a non-empty media URL or take-dir file (e.g. "/logo.png")`
@@ -1419,6 +1469,7 @@ import { existsSync as existsSync4 } from "fs";
1419
1469
  import { join as join4, resolve as resolve2 } from "path";
1420
1470
  import { compileVosConfig } from "@vosjs/core";
1421
1471
  import {
1472
+ docStillTime,
1422
1473
  lowerToComposition,
1423
1474
  momentsFromDoc as momentsFromDoc2,
1424
1475
  planForDigest as planForDigest2,
@@ -2751,10 +2802,12 @@ async function framesTake(browser, dir, opts) {
2751
2802
  shots.push({ time: clamp(m.outputAt), kind: "moment", momentId: m.id });
2752
2803
  }
2753
2804
  }
2805
+ if (opts.atStill)
2806
+ shots.push({ time: clamp(docStillTime(doc)), kind: "still" });
2754
2807
  shots.sort((a, b) => a.time - b.time);
2755
2808
  if (!shots.length)
2756
2809
  throw new Error(
2757
- "no frame times \u2014 pass --times/--frame, --at-zooms or --at-moments"
2810
+ "no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
2758
2811
  );
2759
2812
  const outDir = resolve2(opts.outDir ?? join4(dir, "stills"));
2760
2813
  await mkdir3(outDir, { recursive: true });
@@ -3625,15 +3678,15 @@ function proposeMotion(input, opts) {
3625
3678
  const endStart = outputLength(doc);
3626
3679
  const lastSeg = doc.segments.at(-1);
3627
3680
  if (lastSeg) {
3628
- const own = (doc.freeze ?? []).filter(
3629
- (f) => Math.abs(f.at - lastSeg.out) > 1e-9 || !sameMedia(f.media, lastSeg.media)
3630
- );
3681
+ const atEnd = (f) => Math.abs(f.at - lastSeg.out) <= 1e-9 && sameMedia(f.media, lastSeg.media);
3682
+ const kept2 = (doc.freeze ?? []).find((f) => atEnd(f) && !f.from);
3683
+ const own = (doc.freeze ?? []).filter((f) => !atEnd(f));
3631
3684
  const taken = new Set(own.map((f) => f.id));
3632
3685
  let n = 0;
3633
3686
  while (taken.has(`f${n}`)) n++;
3634
3687
  doc.freeze = [
3635
3688
  ...own,
3636
- {
3689
+ kept2 ? { ...kept2, seconds: Math.max(kept2.seconds, END_CARD_SECONDS) } : {
3637
3690
  id: `f${n}`,
3638
3691
  at: lastSeg.out,
3639
3692
  seconds: END_CARD_SECONDS,
@@ -3800,13 +3853,11 @@ function isLightHexGround(hex2) {
3800
3853
  import { existsSync as existsSync5 } from "fs";
3801
3854
  import { readFile as readFile5 } from "fs/promises";
3802
3855
  import { isAbsolute, join as join6, resolve as resolve3 } from "path";
3803
- import { totalDuration } from "@vosjs/timeline";
3804
3856
  import {
3805
3857
  computeCardLayout,
3806
- docRestTime,
3858
+ docStillTime as docStillTime2,
3807
3859
  migrateHostedDoc,
3808
3860
  overlayRect,
3809
- ratedSegments as ratedSegments5,
3810
3861
  resolveOverlayStyle
3811
3862
  } from "@vosjs/studio-core";
3812
3863
 
@@ -4046,11 +4097,7 @@ async function findPosterDocs(takeDir, launchRoles) {
4046
4097
  return out;
4047
4098
  }
4048
4099
  function posterStillTime(doc, duration) {
4049
- const rest = docRestTime(doc);
4050
- if (rest != null) return rest;
4051
- const footage = totalDuration(ratedSegments5(doc));
4052
- const end = footage > 0 ? Math.min(duration, footage) : duration;
4053
- return Math.max(0, end - 1 / 30);
4100
+ return Math.max(0, Math.min(duration, docStillTime2(doc)));
4054
4101
  }
4055
4102
  var DESIGN_H = 1080;
4056
4103
  function designFrame(px) {
@@ -6245,6 +6292,8 @@ async function planTake(dir, opts = {}) {
6245
6292
  if (prev.objects?.length) doc.objects = prev.objects;
6246
6293
  if (prev.audio.length) doc.audio = prev.audio;
6247
6294
  if (prev.camMotion?.length) doc.camMotion = prev.camMotion;
6295
+ if (prev.still !== void 0) doc.still = prev.still;
6296
+ if (opts.still != null) doc.still = opts.still;
6248
6297
  await writeJson(take.paths.doc, doc, true);
6249
6298
  return {
6250
6299
  doc,
@@ -6323,6 +6372,7 @@ async function planTake(dir, opts = {}) {
6323
6372
  doc = proposed.doc;
6324
6373
  motion = { notes: proposed.notes, skipped: proposed.skipped };
6325
6374
  }
6375
+ if (opts.still != null) doc.still = opts.still;
6326
6376
  await writeJson(take.paths.doc, doc, true);
6327
6377
  return {
6328
6378
  doc,
@@ -6364,7 +6414,7 @@ var KEYS = [
6364
6414
  { key: "camKey", file: CAM_NAME }
6365
6415
  ];
6366
6416
  function isTakeRelativeKey(key) {
6367
- return !!key && !/^(https?:|blob:|data:|\/\/)/.test(key) && !key.startsWith("/api/");
6417
+ return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
6368
6418
  }
6369
6419
  var takeRelativeFile = (key) => key.replace(/^\/+/, "");
6370
6420
  function docMediaRefs(doc, keep = isTakeRelativeKey) {
@@ -8969,6 +9019,7 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
8969
9019
  "draft",
8970
9020
  "at-zooms",
8971
9021
  "at-moments",
9022
+ "at-still",
8972
9023
  "no-frames",
8973
9024
  "media",
8974
9025
  "print",
@@ -8983,9 +9034,9 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
8983
9034
  Take pipeline
8984
9035
  vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
8985
9036
  vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
8986
- vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--release v2.1] [--json]
9037
+ vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
8987
9038
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
8988
- vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
9039
+ vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
8989
9040
  vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--launch LAUNCH.md] [--look plate|gradient|dark|none] [--brand BRAND.md] [--composed] [--set path=value] [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--parallel N] [--json]
8990
9041
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
8991
9042
  vos brand <url> [--out BRAND.md] [--json]
@@ -9477,12 +9528,19 @@ async function cmdPlan(argv) {
9477
9528
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
9478
9529
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
9479
9530
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
9531
+ const stillRaw = strFlag(flags, "still") ?? release.launchRoles.still;
9532
+ const still = stillRaw === void 0 ? null : Number(stillRaw);
9533
+ if (still !== null && !(Number.isFinite(still) && still >= 0))
9534
+ throw new UsageError(
9535
+ `--still expects output seconds \u2265 0, got "${stillRaw}"`
9536
+ );
9480
9537
  const s = await planTake(dir, {
9481
9538
  ...style ? { style } : {},
9482
9539
  ...reuse ? { reuse } : {},
9483
9540
  backdrop,
9484
9541
  words: release.words,
9485
9542
  mark: release.mark,
9543
+ ...still !== null ? { still } : {},
9486
9544
  ...motionWanted ? {
9487
9545
  motion: {
9488
9546
  words: release.words,
@@ -9730,6 +9788,7 @@ async function cmdFrames(argv) {
9730
9788
  const timesRaw = strFlag(flags, "times");
9731
9789
  const atZooms = flags["at-zooms"] === true;
9732
9790
  const atMoments = flags["at-moments"] === true;
9791
+ const atStill = flags["at-still"] === true;
9733
9792
  let times;
9734
9793
  try {
9735
9794
  times = frameRaw !== void 0 ? parseTimes(frameRaw, duration) : timesRaw !== void 0 ? parseTimes(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes("0,25%,50%,75%,100%", duration);
@@ -9743,6 +9802,7 @@ async function cmdFrames(argv) {
9743
9802
  times,
9744
9803
  atZooms,
9745
9804
  atMoments,
9805
+ atStill,
9746
9806
  width: size?.width,
9747
9807
  height: size?.height,
9748
9808
  outDir: strFlag(flags, "out"),
@@ -10361,4 +10421,4 @@ export {
10361
10421
  convertAgentBrowser,
10362
10422
  run
10363
10423
  };
10364
- //# sourceMappingURL=chunk-GHJ2BFQG.js.map
10424
+ //# sourceMappingURL=chunk-IJ5A4TL3.js.map