@vosjs/cli 0.26.0 → 0.28.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
@@ -77,23 +77,23 @@ vos record --actions actions.json --out take --strict # footage replaced; the
77
77
  vos plan take --reuse # re-time that cut onto the new recording; what could not follow is named
78
78
  ```
79
79
 
80
- | Verb | Flags |
81
- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--max-duration <s>` `--background <slug\|url\|none>` |
83
- | `create` | The `record` flags plus the render flags (`--width` `--height` `--fps` `--format` `--parallel` `--draft` `--frame` `--set`), no `--range`. With `--strict` an incomplete recording exits 2 before anything is rendered |
84
- | `plan` | `--fresh` (discard the current plan) `--reuse` `--from <doc.json>` (defaults to `<take>/doc.prev.json`) `--style <doc.json\|take\|vosId>` `--with <doc.json\|take\|vosId>[@end\|@start\|@step:<id>\|@<seconds>]` (a template, repeatable) `--background` `--motion` (re-propose the motion) `--headline` `--kicker` `--launch` `--brand` `--music` `--entrance` `--end-card on\|none\|<ref>` `--captions` `--clicks` `--release` |
85
- | `digest` | `--out <take>/digest` `--full 960` `--crop 640` (image long edges, the token budget) `--no-frames` `--transcript <file>` (Whisper-shaped segments merged as `said`) `--style <ref>` (report a reference document's style fields) |
86
- | `frames` | `--times 0,25%,50%,75%,100%` (the default selector, output seconds or percent) `--frame <t>` `--at-zooms` `--at-moments` `--size WxH` `--out <take>/stills` `--background` `--set …`; writes `stills.json` |
87
- | `render` | `--width` `--height` `--fps` `--format webm\|mp4` `--parallel N` (1..16 sessions) `--range a..b` (output seconds; keeps its audio) `--draft` `--frame <kind>` `--background` `--set …`; `out` defaults to `<take>/out.<format>` |
88
- | `open` | `--studio http://localhost:6060` `--print` (print the URL, do not launch a browser) |
80
+ | Verb | Flags |
81
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--max-duration <s>` `--background <slug\|url\|none>` |
83
+ | `create` | The `record` flags plus the render flags (`--width` `--height` `--fps` `--format` `--parallel` `--draft` `--frame` `--set`), no `--range`. With `--strict` an incomplete recording exits 2 before anything is rendered |
84
+ | `plan` | `--fresh` (discard the current plan) `--reuse` `--from <doc.json>` (defaults to `<take>/doc.prev.json`) `--style <doc.json\|take\|vosId>` `--with <doc.json\|take\|vosId>[@end\|@start\|@step:<id>\|@<seconds>]` (a template, repeatable) `--background` `--motion` (re-propose the motion) `--headline` `--kicker` `--launch` `--brand` `--music` `--entrance` `--transitions slide\|fade\|scale\|none` `--end-card on\|none\|<ref>` `--captions` `--clicks` `--release` |
85
+ | `digest` | `--out <take>/digest` `--full 960` `--crop 640` (image long edges, the token budget) `--no-frames` `--transcript <file>` (Whisper-shaped segments merged as `said`) `--style <ref>` (report a reference document's style fields) |
86
+ | `frames` | `--times 0,25%,50%,75%,100%` (the default selector, output seconds or percent) `--frame <t>` `--at-zooms` `--at-moments` `--size WxH` `--out <take>/stills` `--background` `--set …`; writes `stills.json` |
87
+ | `render` | `--width` `--height` `--fps` `--format webm\|mp4` `--parallel N` (1..16 sessions) `--range a..b` (output seconds; keeps its audio) `--draft` `--frame <kind>` `--background` `--set …`; `out` defaults to `<take>/out.<format>` |
88
+ | `open` | `--studio http://localhost:6060` `--print` (print the URL, do not launch a browser) |
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. **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.
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`, `fade` or `slide` and leaving by `recede` or `fade`), a text, image or video clip, a prop, and a FOOTAGE clip at its boundaries (`segments[].anim`: `exit` is how it leaves at the boundary after it, `enter` how it arrives at the one before, by `slide`, `fade` or `scale`; a slide names its `side`, a step its `seconds`, never longer than half the shorter clip). An exit and the next clip's enter at one boundary run together as a push: the incoming clip plays live while the outgoing, frozen on its last frame, moves away, the camera rests through the window, and one cursor dot crosses from the outgoing card's last point to the incoming's. A page change inside one recording is the same primitive: two clips of one media, the load between them cut. 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, a transition at every page change the recorder marked (`meta.steps[].navigated`: the outgoing clip ends on the frame before the change, the incoming begins once the page has settled; `transitions: slide|fade|scale|none`, slide by default), from LAUNCH.md's `entrance`, `transitions`, `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
 
@@ -185,6 +185,12 @@ import {
185
185
  CAM_SIZE_MIN,
186
186
  CAM_SPAN_MIN,
187
187
  CARD_ENTER_KINDS,
188
+ SEGMENT_ENTER_KINDS,
189
+ SEGMENT_EXIT_KINDS,
190
+ enterOf,
191
+ exitOf,
192
+ segmentOutputExtents,
193
+ transitionSeconds,
188
194
  CARD_EXIT_KINDS,
189
195
  EXPORT_RESOLUTION_OPTIONS,
190
196
  FREEZE_SECONDS_MAX,
@@ -219,8 +225,11 @@ var STEP_KINDS_ALL = [
219
225
  "typewriter",
220
226
  "tilt-in",
221
227
  "pull-out",
222
- "recede"
228
+ "recede",
229
+ "slide",
230
+ "scale"
223
231
  ];
232
+ var STEP_SIDES = ["left", "right", "up", "down"];
224
233
  var STEP_UNITS = ["block", "line", "word", "char"];
225
234
  var STEP_DIRS = ["forward", "reverse", "center"];
226
235
  function lintAnim(name, anim, kinds, problems) {
@@ -270,6 +279,14 @@ function lintAnim(name, anim, kinds, problems) {
270
279
  if (key === "stagger" && (typeof obj.stagger !== "number" || obj.stagger < 0 || obj.stagger > 2))
271
280
  problems.push(`${name}.anim.${side}.stagger must be seconds in 0..2`);
272
281
  }
282
+ if (obj.side !== void 0) {
283
+ if (kind !== "slide")
284
+ problems.push(`${name}.anim.${side}.side is for a slide`);
285
+ else if (typeof obj.side !== "string" || !STEP_SIDES.includes(obj.side))
286
+ problems.push(
287
+ `${name}.anim.${side}.side must be ${STEP_SIDES.join(" | ")}`
288
+ );
289
+ }
273
290
  }
274
291
  if (a.idle !== void 0 && a.idle !== null) {
275
292
  if (!kinds.idle) problems.push(`${name}.anim.idle is for props only`);
@@ -547,6 +564,56 @@ function lintDoc(docIn) {
547
564
  problems.push("segments must be an array of {in, out} spans");
548
565
  }
549
566
  checkByMedia(entries(doc.segments), "segment");
567
+ const segs = Array.isArray(doc.segments) ? doc.segments.filter(isObj) : [];
568
+ segs.forEach((seg, i) => {
569
+ lintAnim(
570
+ `segments[${i}]`,
571
+ seg.anim,
572
+ {
573
+ enter: SEGMENT_ENTER_KINDS,
574
+ exit: SEGMENT_EXIT_KINDS,
575
+ idle: null,
576
+ words: false
577
+ },
578
+ problems
579
+ );
580
+ });
581
+ if (segs.length && segs.every((s) => isNum(s.in) && isNum(s.out))) {
582
+ const anims = segs.map(
583
+ (s) => s.anim
584
+ );
585
+ if (enterOf(anims[0]) && enterOf(anims[0]).kind !== "none")
586
+ warnings.push(
587
+ "segments[0].anim.enter is ignored: the first clip arrives with the card (frame.anim.enter)"
588
+ );
589
+ const last = segs.length - 1;
590
+ if (last > 0 && exitOf(anims[last]) && exitOf(anims[last]).kind !== "none")
591
+ warnings.push(
592
+ `segments[${last}].anim.exit is ignored: the last clip leaves with the card (frame.anim.exit)`
593
+ );
594
+ let extents = [];
595
+ try {
596
+ extents = segmentOutputExtents(doc);
597
+ } catch {
598
+ extents = [];
599
+ }
600
+ for (let i = 0; i + 1 < segs.length && extents.length === segs.length; i++) {
601
+ const cap = Math.min(
602
+ extents[i].end - extents[i].start,
603
+ extents[i + 1].end - extents[i + 1].start
604
+ ) / 2;
605
+ const exitS = transitionSeconds(exitOf(anims[i]));
606
+ const enterS = transitionSeconds(enterOf(anims[i + 1]));
607
+ if (exitS > cap + EPS)
608
+ problems.push(
609
+ `segments[${i}].anim.exit: ${exitS}s is longer than half the shorter clip at that boundary (${cap.toFixed(2)}s); the clip must be seen still before it moves`
610
+ );
611
+ if (enterS > cap + EPS)
612
+ problems.push(
613
+ `segments[${i + 1}].anim.enter: ${enterS}s is longer than half the shorter clip at that boundary (${cap.toFixed(2)}s); the clip must be seen still before it moves`
614
+ );
615
+ }
616
+ }
550
617
  }
551
618
  const speed = entries(doc.speed);
552
619
  checkByMedia(speed, "speed");
@@ -988,6 +1055,39 @@ function lintDoc(docIn) {
988
1055
  if (o.fx !== void 0) {
989
1056
  problems.push(`${name}.fx is text-only (media clips have enter/exit)`);
990
1057
  }
1058
+ if (typeof o.key === "string" && o.key.startsWith("media:")) {
1059
+ const ref = o.key.slice("media:".length);
1060
+ const ids = new Set(
1061
+ (Array.isArray(doc.media) ? doc.media : []).map((m) => typeof m.id === "string" ? m.id : "")
1062
+ );
1063
+ if (ref !== "" && !ids.has(ref))
1064
+ problems.push(
1065
+ `${name}.key names media "${ref}", which is not one of media[].id (media: alone is the primary)`
1066
+ );
1067
+ }
1068
+ const lcf = o.frame;
1069
+ if (lcf !== void 0) {
1070
+ if (!isObj(lcf)) {
1071
+ problems.push(`${name}.frame must be an object (the layer's card)`);
1072
+ } else {
1073
+ const LCF = [
1074
+ "browserBar",
1075
+ "lean",
1076
+ "shadow",
1077
+ "shadowContact",
1078
+ "shadowColor",
1079
+ "cursor"
1080
+ ];
1081
+ for (const k of Object.keys(lcf))
1082
+ if (!LCF.includes(k))
1083
+ problems.push(
1084
+ `${name}.frame.${k}: not a layer card field (${LCF.join(", ")})`
1085
+ );
1086
+ const lean = lcf.lean;
1087
+ if (lean !== void 0 && (!isObj(lean) || !isNum(lean.rx) || !isNum(lean.ry)))
1088
+ problems.push(`${name}.frame.lean must be { rx, ry } in degrees`);
1089
+ }
1090
+ }
991
1091
  if (typeof o.key !== "string" || o.key.length === 0) {
992
1092
  problems.push(
993
1093
  `${name}.key must be a non-empty media URL or take-dir file (e.g. "/logo.png")`
@@ -3531,6 +3631,43 @@ var BED_ID = "bed";
3531
3631
  var CLICK_ID_PREFIX = "click-";
3532
3632
  var CAPTION_ID_PREFIX = "caption-";
3533
3633
  var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
3634
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
3635
+ var TRANSITION_CLIP_MIN = 1;
3636
+ function proposeTransitions(input, kind) {
3637
+ const doc = structuredClone(input);
3638
+ const skipped = [];
3639
+ let count = 0;
3640
+ const steps = doc.source.meta.steps ?? [];
3641
+ for (const step of steps) {
3642
+ if (!step.navigated || step.skipped) continue;
3643
+ const name = step.id ?? String(step.step);
3644
+ const wait = step.do === "wait";
3645
+ const outEnd = round3(wait ? step.tStart : step.tEnd);
3646
+ const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
3647
+ const segs = doc.segments;
3648
+ if (segs.some(
3649
+ (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
3650
+ ))
3651
+ continue;
3652
+ const i = segs.findIndex(
3653
+ (s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
3654
+ );
3655
+ if (i < 0) {
3656
+ skipped.push(
3657
+ `no transition at step ${name}: the page change sits too close to a clip's edge`
3658
+ );
3659
+ continue;
3660
+ }
3661
+ const seg = segs[i];
3662
+ const before = { ...seg, out: outEnd, anim: { exit: kind } };
3663
+ const after = { ...seg, in: inStart, anim: { enter: kind } };
3664
+ if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
3665
+ if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
3666
+ doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
3667
+ count++;
3668
+ }
3669
+ return { doc, count, skipped };
3670
+ }
3534
3671
  function pickTrack(catalog, ask) {
3535
3672
  if (!catalog || !ask || off(ask)) return null;
3536
3673
  const want = ask.trim().toLowerCase();
@@ -3601,6 +3738,14 @@ function proposeMotion(input, opts) {
3601
3738
  if (Object.keys(rest).length) doc.frame.anim = rest;
3602
3739
  else delete doc.frame.anim;
3603
3740
  }
3741
+ const trRole = launch.transitions;
3742
+ if (!off(trRole)) {
3743
+ const trKind = trRole && /^(slide|fade|scale)$/.test(trRole.trim()) ? trRole.trim() : "slide";
3744
+ const made = proposeTransitions(doc, trKind);
3745
+ doc = made.doc;
3746
+ if (made.count) notes.push(`${made.count} transition(s) ${trKind}`);
3747
+ for (const n of made.skipped) skipped.push(n);
3748
+ }
3604
3749
  const endCardRole = launch.endCard;
3605
3750
  doc = dropTemplate(doc, END_CARD_FROM);
3606
3751
  const templates = (opts.templates ?? []).filter(
@@ -5671,6 +5816,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5671
5816
  }
5672
5817
  const stepStart = now();
5673
5818
  const skippedBefore = skipped.length;
5819
+ const urlBefore = page.url();
5674
5820
  switch (step.do) {
5675
5821
  case "wait":
5676
5822
  await sleep(clampWait(step.ms, now(), maxSeconds));
@@ -5802,7 +5948,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5802
5948
  ...stepInfo.selector ? { selector: stepInfo.selector } : {},
5803
5949
  tStart: +(stepStart / 1e3).toFixed(3),
5804
5950
  tEnd: +(now() / 1e3).toFixed(3),
5805
- ...skipped.length > skippedBefore ? { skipped: true } : {}
5951
+ ...skipped.length > skippedBefore ? { skipped: true } : {},
5952
+ ...page.url() !== urlBefore ? { navigated: true } : {}
5806
5953
  });
5807
5954
  }
5808
5955
  if (!capped) await sleep(600);
@@ -6381,7 +6528,7 @@ var KEYS = [
6381
6528
  { key: "camKey", file: CAM_NAME }
6382
6529
  ];
6383
6530
  function isTakeRelativeKey(key) {
6384
- return !!key && !/^(https?:|blob:|data:|\/\/)/.test(key) && !key.startsWith("/api/");
6531
+ return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
6385
6532
  }
6386
6533
  var takeRelativeFile = (key) => key.replace(/^\/+/, "");
6387
6534
  function docMediaRefs(doc, keep = isTakeRelativeKey) {
@@ -9001,7 +9148,7 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
9001
9148
  Take pipeline
9002
9149
  vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
9003
9150
  vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
9004
- 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]
9151
+ 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|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
9005
9152
  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]
9006
9153
  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]
9007
9154
  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]
@@ -9147,7 +9294,7 @@ from BRAND.md logoUrl and a freeze of the last frame under it, from:
9147
9294
  endcard; offline, the house clips stand in), a CAPTION per
9148
9295
  actions.json step at the step's moment, a music BED from LAUNCH.md's music
9149
9296
  role (a catalog slug or a mood) and a click sound on every press when the
9150
- take has no mic. LAUNCH.md's entrance, endCard, captions, music and clicks
9297
+ take has no mic. LAUNCH.md's entrance, transitions, endCard, captions, music and clicks
9151
9298
  roles, or the flags, change or switch each off; a deleted proposal stays
9152
9299
  deleted on a refresh. deliver applies each destination's MECHANICS and
9153
9300
  nothing more: the README loop drops the card's motion, every clip a
@@ -9576,6 +9723,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
9576
9723
  for (const [flag, role] of [
9577
9724
  ["music", "music"],
9578
9725
  ["entrance", "entrance"],
9726
+ ["transitions", "transitions"],
9579
9727
  ["end-card", "endCard"],
9580
9728
  ["captions", "captions"],
9581
9729
  ["clicks", "clicks"]
@@ -10388,4 +10536,4 @@ export {
10388
10536
  convertAgentBrowser,
10389
10537
  run
10390
10538
  };
10391
- //# sourceMappingURL=chunk-I3UUIBSH.js.map
10539
+ //# sourceMappingURL=chunk-T35XY4YW.js.map