@vosjs/cli 0.20.0 → 0.21.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,9 +89,9 @@ 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
- **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 hold. What could not follow is said in words.
92
+ **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
93
 
94
- **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; past its footage the card holds its last frame at the pose its exit settled into, and a segment's `hold` is the freeze primitive. 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 house end card when `endCard` is on or absent (the headline, the release line, the wordmark and BRAND.md's mark as clips after the footage over a receding card, `from: endcard`), 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.
94
+ **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; past its footage the card holds its last frame at the pose its exit settled into. 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 house end card when `endCard` is on or absent (the headline, the release line, the wordmark and BRAND.md's mark as clips after the footage over a receding card, `from: endcard`), 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.
95
95
 
96
96
  **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.
97
97
 
@@ -186,7 +186,7 @@ vos brand https://your.app --out BRAND.md # the brand kit,
186
186
 
187
187
  [`schema/channel-specs.json`](./schema/channel-specs.json) holds per-channel launch-asset specs: dimensions, byte and duration ceilings, and a genre per image destination (`screenshot` is the real page from the take, full bleed; `card` is a composed cover). Channels: `cws`, `producthunt` (`ph`), `x`, `linkedin` (`li`), `og`, `github` (`gh`), `youtube` (`yt`), `shorts-linkedin` (`shorts`), or `all`. `vos deliver <take> --to <channels>` loops them in one pass and writes `kit.json`, the manifest the `launch-kit` skill builds the rest of the release around; an asset that misses its spec lands in `skipped[]` with the reason, and the verb exits 1 when nothing was produced. Flags: `--release <tag>`, `--out <take>/kit`, `--times`, `--range`, `--parallel`, `--launch LAUNCH.md`, `--look`, `--brand`, `--composed` (keep the cut's camera and chrome on screenshot stills instead of the full-bleed page), `--set`, `--background`. `vos validate` reads a kit back from its bytes (a `.png` that is WebP, a lying size or duration, a set under its count, a byte ceiling).
188
188
 
189
- **A poster is a document, and deliver renders it.** A card-genre destination (OG, LinkedIn, X, the YouTube thumbnail, the CWS tile and marquee, the GitHub social preview) renders from the poster document of its aspect class (`landscape`, `square`, `portrait`, `tile`), found beside the take as `poster/<class>/doc.json` (`poster/doc.json` serves every class) or named in LAUNCH.md (`poster: <path>`, `poster-<class>: <path>`, a path to a doc.json or to a pulled poster take), at the document's REST (the trailing hold's start, the composed frame before nothing moves). The poster is a plain take document: the card placed by `frame.inset`, leaned by the tilt track, the words and the mark as `stage-*` clips, a trailing `hold`; an agent writes it with `vos plan --style <poster>` from a poster on a shelf, or by hand, and pushes it like any take. `kit.json` records `source: "poster"`, the class, the file, the vos it tracks, the shot rect and the text boxes read from the document. A class with no document is the take's own frame, said once. Deliver composes nothing; it applies each video destination's mechanics (the README loop drops the card's motion, every clip a template placed, the captions and the sound; a channel that autoplays muted drops the bed; the 9:16 cut reframes and follows the camera) and verifies.
189
+ **A poster is a document, and deliver renders it.** A card-genre destination (OG, LinkedIn, X, the YouTube thumbnail, the CWS tile and marquee, the GitHub social preview) renders from the poster document of its aspect class (`landscape`, `square`, `portrait`, `tile`), found beside the take as `poster/<class>/doc.json` (`poster/doc.json` serves every class) or named in LAUNCH.md (`poster: <path>`, `poster-<class>: <path>`, a path to a doc.json or to a pulled poster take), at the document's REST (where its last freeze begins, the composed frame before nothing moves). The poster is a plain take document: the card placed by `frame.inset`, leaned by the tilt track, the words and the mark as `stage-*` clips, a trailing freeze; an agent writes it with `vos plan --style <poster>` from a poster on a shelf, or by hand, and pushes it like any take. `kit.json` records `source: "poster"`, the class, the file, the vos it tracks, the shot rect and the text boxes read from the document. A class with no document is the take's own frame, said once. Deliver composes nothing; it applies each video destination's mechanics (the README loop drops the card's motion, every clip a template placed, the captions and the sound; a channel that autoplays muted drops the bed; the 9:16 cut reframes and follows the camera) and verifies.
190
190
 
191
191
  `vos brand <url>` reads the site's `/design.md` first (the convention beside `/llms.txt`), then `/llms.txt`, then witnesses one page, and writes `BRAND.md`: the palette, faces, marks and the avoid list, with the provenance of every value, so a brand is resolved before any asset is authored.
192
192
 
@@ -188,6 +188,8 @@ import {
188
188
  TEXT_ENTER_KINDS,
189
189
  TEXT_EXIT_KINDS,
190
190
  EXPORT_RESOLUTION_OPTIONS,
191
+ FREEZE_SECONDS_MAX,
192
+ FREEZE_SECONDS_MIN,
191
193
  SPEED_RATE_MAX,
192
194
  SPEED_RATE_MIN,
193
195
  TILT_DEG_MAX,
@@ -398,9 +400,37 @@ function lintDoc(docIn) {
398
400
  }
399
401
  for (const [i, seg] of (Array.isArray(doc.segments) ? doc.segments : []).entries()) {
400
402
  const hold = seg.hold;
401
- if (hold !== void 0 && (!isNum(hold) || hold < 0 || hold > 10)) {
403
+ if (hold === void 0) continue;
404
+ if (!isNum(hold) || hold < 0 || hold > FREEZE_SECONDS_MAX) {
402
405
  problems.push(
403
- `segments[${i}].hold must be 0..10 output seconds (got ${String(hold)})`
406
+ `segments[${i}].hold must be 0..${FREEZE_SECONDS_MAX} output seconds (got ${String(hold)})`
407
+ );
408
+ } else if (hold > 0) {
409
+ warnings.push(
410
+ `segments[${i}].hold is a legacy spelling, read as a freeze at ${String(seg.out)}s (doc.freeze); vos plan writes that shape`
411
+ );
412
+ }
413
+ }
414
+ if (doc.freeze !== void 0 && !Array.isArray(doc.freeze)) {
415
+ problems.push("freeze must be an array of {id, at, seconds}");
416
+ }
417
+ const freezes = Array.isArray(doc.freeze) ? doc.freeze : [];
418
+ const freezeIds = /* @__PURE__ */ new Set();
419
+ for (const [i, raw] of freezes.entries()) {
420
+ const f = raw;
421
+ const name = `freeze[${i}]${typeof f.id === "string" ? ` (${f.id})` : ""}`;
422
+ if (typeof f.id !== "string" || !f.id)
423
+ problems.push(`${name}: id must be a string`);
424
+ else if (freezeIds.has(f.id)) problems.push(`${name}: duplicate id`);
425
+ else freezeIds.add(f.id);
426
+ if (!isNum(f.at) || f.at < 0 || f.at > duration + EPS) {
427
+ problems.push(
428
+ `${name}: at must be a SOURCE moment inside the recording (0..${duration.toFixed(2)}s, got ${String(f.at)})`
429
+ );
430
+ }
431
+ if (!isNum(f.seconds) || f.seconds < FREEZE_SECONDS_MIN - EPS || f.seconds > FREEZE_SECONDS_MAX + EPS) {
432
+ problems.push(
433
+ `${name}: seconds must be ${FREEZE_SECONDS_MIN}..${FREEZE_SECONDS_MAX} output seconds (got ${String(f.seconds)})`
404
434
  );
405
435
  }
406
436
  }
@@ -5789,6 +5819,7 @@ import {
5789
5819
  } from "@vosjs/studio-core";
5790
5820
 
5791
5821
  // src/plugin/reuse.ts
5822
+ import { docFreezes } from "@vosjs/studio-core";
5792
5823
  var MIN_SPAN = 0.15;
5793
5824
  function matchStep(old, newSteps) {
5794
5825
  const usable = (s) => !!s && s.skipped !== true;
@@ -5950,6 +5981,30 @@ function retimeCut(prev, newSteps, newDurationMs) {
5950
5981
  newDuration,
5951
5982
  report
5952
5983
  );
5984
+ const freeze = [];
5985
+ for (const f of docFreezes(prev)) {
5986
+ let at2 = null;
5987
+ if (f.anchor) {
5988
+ at2 = resolveAnchor2(f.anchor, newSteps);
5989
+ if (at2 === null) {
5990
+ report.flagged.push(
5991
+ `freeze ${f.id}: its anchored step (${String(f.anchor.step)}) is missing or skipped in the new recording \u2014 fell back to the step map`
5992
+ );
5993
+ } else report.anchored++;
5994
+ }
5995
+ if (at2 === null) {
5996
+ at2 = stepMap.map(f.at);
5997
+ report.mapped++;
5998
+ }
5999
+ if (at2 < 0 || at2 > newDuration) {
6000
+ report.flagged.push(
6001
+ `freeze ${f.id}: lands outside the new recording (${at2.toFixed(2)}s) \u2014 dropped`
6002
+ );
6003
+ continue;
6004
+ }
6005
+ freeze.push({ ...f, at: +at2.toFixed(3) });
6006
+ }
6007
+ freeze.sort((a, b) => a.at - b.at);
5953
6008
  const rejected = [];
5954
6009
  for (const lane of ["zoom", "tilt", "speed"]) {
5955
6010
  rejected.push(
@@ -5963,7 +6018,7 @@ function retimeCut(prev, newSteps, newDurationMs) {
5963
6018
  )
5964
6019
  );
5965
6020
  }
5966
- return { segments, zoom, speed, tilt, rejected, report };
6021
+ return { segments, zoom, speed, freeze, tilt, rejected, report };
5967
6022
  }
5968
6023
 
5969
6024
  // src/plugin/plan.ts
@@ -5992,7 +6047,7 @@ function patchStageWords(doc, words) {
5992
6047
  }
5993
6048
  function applyStyle(seed, doc, opts) {
5994
6049
  const parts = layoutOf(seed);
5995
- const carries = parts.clips.length > 0 || parts.lean || parts.hold;
6050
+ const carries = parts.clips.length > 0 || parts.lean || parts.freeze;
5996
6051
  if (!carries) return { doc: copyStyle(seed, doc), layout: void 0 };
5997
6052
  const keys = opts.mark ? { "stage-mark": opts.mark.key } : void 0;
5998
6053
  const { doc: next, notes } = copyLayout(seed, copyStyle(seed, doc), { keys });
@@ -6018,10 +6073,7 @@ async function planTake(dir, opts = {}) {
6018
6073
  doc = copyStyle(prev, doc);
6019
6074
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
6020
6075
  doc.segments = rt.segments;
6021
- const prevHold = prev.segments.at(-1)?.hold;
6022
- const last = doc.segments.at(-1);
6023
- if (typeof prevHold === "number" && prevHold > 0 && last)
6024
- last.hold = prevHold;
6076
+ if (rt.freeze.length) doc.freeze = rt.freeze;
6025
6077
  if (rt.rejected.length) doc.rejected = rt.rejected;
6026
6078
  const manualZoom = rt.zoom;
6027
6079
  const autoZoom = planAutoZoom(doc.source.cursor, {
@@ -8855,7 +8907,7 @@ Screenshot-genre stills never take a look.
8855
8907
  A POSTER is a document, never a template: a plain take whose card sits
8856
8908
  where the poster wants it (frame.inset), leans (the tilt track), carries
8857
8909
  the words and the mark as clips (ids stage-title, stage-kicker,
8858
- stage-brand, stage-mark) and ends on a trailing hold whose start is the
8910
+ stage-brand, stage-mark) and ends on a trailing freeze whose start is the
8859
8911
  still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
8860
8912
  CWS tile + marquee, GitHub social preview) render from the poster
8861
8913
  document of their aspect CLASS (landscape, square, portrait, tile), found
@@ -8868,7 +8920,7 @@ deliver renders and verifies; it composes nothing. The composition is
8868
8920
  made with plan --style <poster>, which copies a poster's layout onto a
8869
8921
  take (its card placement, its stage clips with the release's words
8870
8922
  patched in from LAUNCH.md's headline and kicker roles or --headline and
8871
- --kicker, its rest lean, its hold), or by hand in doc.json.
8923
+ --kicker, its rest lean, its freeze), or by hand in doc.json.
8872
8924
  The cut's MOTION is the document's too, in ONE vocabulary: every visual
8873
8925
  thing (the card, a text, image or video clip, a prop) carries anim.enter,
8874
8926
  anim.exit and anim.idle; the output lasts until the last clip ends, and
@@ -9256,7 +9308,7 @@ async function cmdPlan(argv) {
9256
9308
  layout from ${s.styleFrom}: ${[
9257
9309
  s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
9258
9310
  s.layout.lean ? "the rest lean" : "",
9259
- s.layout.hold ? "the hold" : ""
9311
+ s.layout.freeze ? "the freeze" : ""
9260
9312
  ].filter(Boolean).join(", ")}` + (s.layout.notes.length ? `
9261
9313
  ${s.layout.notes.join("\n ")}` : "") : "";
9262
9314
  const motionLines = s.motion ? (s.motion.notes.length ? `
@@ -10085,4 +10137,4 @@ export {
10085
10137
  convertAgentBrowser,
10086
10138
  run
10087
10139
  };
10088
- //# sourceMappingURL=chunk-ZONOZZZY.js.map
10140
+ //# sourceMappingURL=chunk-4IWHZBIY.js.map