@vosjs/cli 0.81.0 → 0.82.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
@@ -452,6 +452,8 @@ Not sure of a selector? `vos look <url>` (with the take's `--session` or `--stor
452
452
 
453
453
  **Pinned layers.** A layer ABOUT something on the page names it, and the lowering keeps the two together: `"pin": { "step": "copy", "side": "auto", "mark": "ring", "leader": true }` on any overlay kind places the layer a gap off one side of that step's element and carries it with the element as the camera moves (the layer keeps its screen size; only its place follows). One of `step` (a recorder step's `id`, else its index), `press` (SOURCE seconds; the nearest press names the element, for a human recording) or `rect` (video fractions) names the referent. `side` is `auto` (the first of right, left, below, above whose box fits inside the frame through the layer's life) or a side by name; `gap` is design px (24); `mark` is `ring` or `underline`, a standing highlight on the referent for the layer's life; `leader` draws a hairline from the layer to the referent; `color` is their ink. `transform.x/y` stay the fallback for a pin that cannot resolve, and `vos validate` says why (an unknown step, a step that touched nothing, a scroll inside the layer's window). A layer that cannot name its referent is a caption: place it in the margin, never over the app.
454
454
 
455
+ **How long a caption stays.** The captions `vos plan --motion` and `--captions` land are about their steps: each lands as its step begins and leaves as the step ends, or once its words are read when that is later (17 characters a second, the subtitle standard, 1.2 to 3 s), so it never lingers over the steps after it. A caption whose words outlast its step is named in the done line with how long it stays past the step; shorter words, or more time on the step (its `ms`, then a re-record), let it leave with it. Cutting its duration instead only takes the time its words need. It gives way to the next caption 0.1 s before it and never outlives the clip it starts in; one with too little room to be read is dropped and named in the done line, with the room it needs.
456
+
455
457
  **Captions anchored to a step.** A layer's TIME can name a step the way a pin names its place: `"anchor": { "step": "share", "media": "m1", "at": "end", "offset": 0.2 }` starts the layer where that step plays in the cut. The lowering maps the step's moment (its `tStart`, or `tEnd` with `at: "end"`, in `meta.steps` of the media `media` names; absent = the primary) through the segments, speed spans and freezes exactly as a source-anchored span is mapped, then adds `offset` in OUTPUT seconds, so a trim, a speed change or a re-record moves the caption with its step and nobody recomputes a `start`. `start` is the fallback when the step is missing, skipped or cut out, and an anchored layer may leave it out: one whose step then resolves nowhere plays nowhere, which `vos validate` refuses, naming the step. `duration` is the layer's own; on a cut that replays the step's seconds the layer starts in the pass where the step plays the longest (the first on a tie), so a caption about a step lands in the main play rather than a short cold open. A text layer's `{elapsed}` fills with the anchored step's off-camera wait when the recorder kept one (the entry of `meta.skips` that names it, else `offCameraMs` on the step; never a step's own `ms`, which is its reading time), and a step that waited for nothing itself (the beat that lands after an await) reads the nearest earlier wait on its media, which validate notes, as a rounded duration: `"{elapsed} later"` renders `12 minutes later`. `vos validate` says when an anchor cannot resolve (and which fallback plays), when `{elapsed}` has no wait at or before its step to fill from, when two captions are on screen at the same time, naming both, and when a layer anchored to one media's step starts over another media's footage (a caption landed after its gesture, then the footage after that step trimmed away). A document without an anchor lowers exactly as before. Not the re-record `anchor` on spans, which only `vos plan --reuse` reads.
456
458
 
457
459
  **Image and video overlays.** Media kinds on the same lane: `{ "id": "m0", "kind": "image" | "video", "start": 2, "duration": 4, "key": "/logo.png", "width": 0.35, "radius": 12, "opacity": 1, "loop": false, "transform": { … } }`. `key` is a file inside the take directory (`"/logo.png"`) or a URL; `width` is a fraction of the frame width, height follows the media's aspect; corners in design px. Video time is clip-local and muted by design; soundtracks belong to `doc.audio`.
@@ -4150,6 +4150,7 @@ import {
4150
4150
  migrateMotion,
4151
4151
  momentOutputs as momentOutputs2,
4152
4152
  ratedSegments as ratedSegments4,
4153
+ resolveAnchor as resolveLayerAnchor,
4153
4154
  sameMedia,
4154
4155
  spanOutputExtent as spanOutputExtent3
4155
4156
  } from "@vosjs/studio-core";
@@ -4158,6 +4159,14 @@ import {
4158
4159
  var CAPTION_MAX = 3;
4159
4160
  var CAPTION_MIN = 1.2;
4160
4161
  var CAPTION_GAP = 0.1;
4162
+ var READ_CHARS_PER_SECOND = 17;
4163
+ function readingSeconds(text) {
4164
+ const chars = text.trim().replace(/\s+/g, " ").length;
4165
+ return Math.min(
4166
+ CAPTION_MAX,
4167
+ Math.max(CAPTION_MIN, chars / READ_CHARS_PER_SECOND)
4168
+ );
4169
+ }
4161
4170
  function fitCaptions(items, slotOf, opts = {}) {
4162
4171
  const max = opts.max ?? CAPTION_MAX;
4163
4172
  const min = opts.min ?? CAPTION_MIN;
@@ -4170,7 +4179,11 @@ function fitCaptions(items, slotOf, opts = {}) {
4170
4179
  const { item, slot } = ordered[i];
4171
4180
  const clipRoom = slot.end - slot.start;
4172
4181
  const nextRoom = nextStart - gap - slot.start;
4173
- const duration = Math.min(max, clipRoom, nextRoom);
4182
+ const ownRoom = slot.until === void 0 ? Infinity : Math.max(
4183
+ slot.until - slot.start,
4184
+ slot.text === void 0 ? min : readingSeconds(slot.text)
4185
+ );
4186
+ const duration = Math.min(max, clipRoom, nextRoom, ownRoom);
4174
4187
  if (duration < min - 1e-9) {
4175
4188
  dropped.unshift({
4176
4189
  item,
@@ -4178,11 +4191,19 @@ function fitCaptions(items, slotOf, opts = {}) {
4178
4191
  });
4179
4192
  continue;
4180
4193
  }
4181
- kept.unshift({ item, duration: +duration.toFixed(3) });
4194
+ const held = slot.until === void 0 ? 0 : duration - (slot.until - slot.start);
4195
+ kept.unshift({
4196
+ item,
4197
+ duration: +duration.toFixed(3),
4198
+ ...held > 0.05 ? { held: +held.toFixed(2) } : {}
4199
+ });
4182
4200
  nextStart = slot.start;
4183
4201
  }
4184
4202
  return { kept, dropped };
4185
4203
  }
4204
+ function heldLine(label, held) {
4205
+ return `${label} stays ${held.toFixed(2)}s past its step to be read; to let it leave with the step, shorten its words (17 characters read in a second) or give its step more time (its ms, then record again). Do not cut its duration: the words need that time`;
4206
+ }
4186
4207
 
4187
4208
  // src/plugin/moments.ts
4188
4209
  import {
@@ -4343,6 +4364,7 @@ function proposeMotion(input, opts) {
4343
4364
  const { words: words2, launch, catalog } = opts;
4344
4365
  const notes = [];
4345
4366
  const skipped = [];
4367
+ const held = [];
4346
4368
  const footage = outputLength(doc);
4347
4369
  const range2 = [0, footage];
4348
4370
  const entrance = launch.entrance;
@@ -4459,15 +4481,37 @@ function proposeMotion(input, opts) {
4459
4481
  (s) => c.id !== void 0 && s.id === c.id || s.step === c.step
4460
4482
  );
4461
4483
  if (!step || step.skipped) continue;
4462
- const t = stepOutputTime(rated, step, 0.2);
4484
+ const ref = step.id ?? step.step;
4485
+ const t = resolveLayerAnchor(doc, { step: ref, at: "start" }, rated).start;
4463
4486
  if (t === null || t < 0 || t >= footage) continue;
4464
- beats.push({ caption: c, t: +t.toFixed(3) });
4487
+ const until = resolveLayerAnchor(
4488
+ doc,
4489
+ { step: ref, at: "end" },
4490
+ rated
4491
+ ).start;
4492
+ beats.push({
4493
+ caption: c,
4494
+ t: +t.toFixed(3),
4495
+ ...until !== null ? { until } : {}
4496
+ });
4465
4497
  }
4466
- const fit = fitCaptions(beats, (b) => ({ start: b.t, end: footage }), {
4467
- max: CAPTION_BEAT_MAX
4468
- });
4498
+ const fit = fitCaptions(
4499
+ beats,
4500
+ (b) => ({
4501
+ start: b.t,
4502
+ end: footage,
4503
+ ...b.until !== void 0 ? { until: b.until } : {},
4504
+ text: b.caption.caption
4505
+ }),
4506
+ { max: CAPTION_BEAT_MAX }
4507
+ );
4469
4508
  for (const { item, why } of fit.dropped)
4470
4509
  skipped.push(`caption for step ${item.caption.step}: ${why}`);
4510
+ for (const k of fit.kept)
4511
+ if (k.held)
4512
+ held.push(
4513
+ heldLine(`the caption for step ${k.item.caption.step}`, k.held)
4514
+ );
4471
4515
  for (const { item, duration } of fit.kept)
4472
4516
  captionClips.push(
4473
4517
  houseCaption(
@@ -4531,7 +4575,7 @@ function proposeMotion(input, opts) {
4531
4575
  if (times.length) notes.push(`${times.length} click sound(s)`);
4532
4576
  }
4533
4577
  doc.audio = clips;
4534
- return { doc, notes, skipped };
4578
+ return { doc, notes, skipped, held };
4535
4579
  }
4536
4580
  function destinationMechanics(d, doc) {
4537
4581
  const set = [];
@@ -6754,7 +6798,7 @@ import {
6754
6798
  import {
6755
6799
  docFreezes,
6756
6800
  ratedSegments as ratedSegments6,
6757
- resolveAnchor as resolveLayerAnchor
6801
+ resolveAnchor as resolveLayerAnchor2
6758
6802
  } from "@vosjs/studio-core";
6759
6803
  var MIN_SPAN = 0.15;
6760
6804
  var AFTER_STEP = 1;
@@ -7050,7 +7094,7 @@ function retimeOverlays(prev, next, report) {
7050
7094
  };
7051
7095
  return overlays.map((o) => {
7052
7096
  if (o.anchor && typeof o.anchor === "object") {
7053
- const r = resolveLayerAnchor(next, o.anchor, newPieces);
7097
+ const r = resolveLayerAnchor2(next, o.anchor, newPieces);
7054
7098
  if (r.start === null) {
7055
7099
  report.flagged.push(
7056
7100
  o.start === void 0 ? `overlay ${o.id}: anchored to step ${String(o.anchor.step)}, which is missing, skipped or cut in the new recording \u2014 it has no start to fall back to, so it plays nowhere; re-anchor it or give it a start` : `overlay ${o.id}: anchored to step ${String(o.anchor.step)}, which is missing, skipped or cut in the new recording \u2014 it plays at its fallback ${o.start.toFixed(2)}s; re-anchor or re-place it`
@@ -7337,7 +7381,11 @@ async function planTake(dir, opts = {}) {
7337
7381
  if (opts.motion && (fresh || opts.motion.again)) {
7338
7382
  const proposed = proposeMotion(doc, opts.motion);
7339
7383
  doc = proposed.doc;
7340
- motion = { notes: proposed.notes, skipped: proposed.skipped };
7384
+ motion = {
7385
+ notes: proposed.notes,
7386
+ skipped: proposed.skipped,
7387
+ held: proposed.held
7388
+ };
7341
7389
  }
7342
7390
  if (opts.still != null) doc.still = opts.still;
7343
7391
  await writeJson(take.paths.doc, doc, true);
@@ -14659,6 +14707,7 @@ import {
14659
14707
  docMediaList as docMediaList2,
14660
14708
  findStep as findStep3,
14661
14709
  mediaKey,
14710
+ overlayPlainText,
14662
14711
  ratedSegments as ratedSegments8,
14663
14712
  resolveAnchor as resolveAnchor5,
14664
14713
  segmentOutputExtents as segmentOutputExtents2
@@ -14744,7 +14793,12 @@ function captionOf(step, actions, script, recorded = []) {
14744
14793
  }
14745
14794
  var safeId = (v) => String(v).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "step";
14746
14795
  function landStepCaptions(doc, actions, opts = {}) {
14747
- const report = { landed: [], skipped: [], uncaptioned: [] };
14796
+ const report = {
14797
+ landed: [],
14798
+ skipped: [],
14799
+ held: [],
14800
+ uncaptioned: []
14801
+ };
14748
14802
  const mine = (o) => !opts.only || opts.only.has(mediaKey(o.anchor?.media));
14749
14803
  const kept = (doc.overlays ?? []).filter(
14750
14804
  (o) => o.from !== STEP_CAPTIONS_FROM || !mine(o)
@@ -14763,6 +14817,7 @@ function landStepCaptions(doc, actions, opts = {}) {
14763
14817
  };
14764
14818
  const ends = /* @__PURE__ */ new Map();
14765
14819
  const labels = /* @__PURE__ */ new Map();
14820
+ const untils = /* @__PURE__ */ new Map();
14766
14821
  const spoken = (media, step, steps) => kept.some((o) => {
14767
14822
  if (o.kind !== "text" || o.preset !== "caption") return false;
14768
14823
  if (o.anchor && mediaKey(o.anchor.media) === media)
@@ -14815,6 +14870,8 @@ function landStepCaptions(doc, actions, opts = {}) {
14815
14870
  from: STEP_CAPTIONS_FROM
14816
14871
  };
14817
14872
  ends.set(clip, end);
14873
+ const stepEnd = resolveAnchor5(doc, { ...anchor, at: "end" }, rated);
14874
+ if (stepEnd.start !== null) untils.set(clip, stepEnd.start);
14818
14875
  labels.set(clip, label);
14819
14876
  fresh.push(clip);
14820
14877
  report.landed.push({
@@ -14828,12 +14885,17 @@ function landStepCaptions(doc, actions, opts = {}) {
14828
14885
  }
14829
14886
  const fit = fitCaptions(fresh, (c) => ({
14830
14887
  start: c.start,
14831
- end: ends.get(c) ?? footageEnd
14888
+ end: ends.get(c) ?? footageEnd,
14889
+ ...untils.has(c) ? { until: untils.get(c) } : {},
14890
+ ...c.kind === "text" ? { text: overlayPlainText(c) } : {}
14832
14891
  }));
14833
14892
  for (const { item, why } of fit.dropped) {
14834
14893
  report.skipped.push(`${labels.get(item) ?? item.id}: ${why}`);
14835
14894
  report.landed = report.landed.filter((l) => l.id !== item.id);
14836
14895
  }
14896
+ for (const k of fit.kept)
14897
+ if (k.held)
14898
+ report.held.push(heldLine(labels.get(k.item) ?? k.item.id, k.held));
14837
14899
  const landedClips = fit.kept.map(({ item, duration }) => {
14838
14900
  item.duration = duration;
14839
14901
  return item;
@@ -16335,6 +16397,7 @@ async function cmdJoin(argv) {
16335
16397
  let captions = null;
16336
16398
  if (withCaptions) {
16337
16399
  captions = landStepCaptions(plan.doc, plan.actions, { only });
16400
+ for (const line of captions.held) plan.notes.push(line);
16338
16401
  for (const k of captions.uncaptioned)
16339
16402
  plan.notes.push(
16340
16403
  `no step in ${labelOf.get(k) ?? `media ${k}`} carries a caption`
@@ -16462,7 +16525,7 @@ import {
16462
16525
  freezeOutputExtents,
16463
16526
  mediaKey as mediaKey5,
16464
16527
  migrateMotion as migrateMotion4,
16465
- overlayPlainText,
16528
+ overlayPlainText as overlayPlainText2,
16466
16529
  longestOutputExtent as longestOutputExtent3,
16467
16530
  ratedSegments as ratedSegments11,
16468
16531
  resolveOverlayAnchors as resolveOverlayAnchors2,
@@ -16576,7 +16639,7 @@ function takeTimeline(rawDoc, actions = /* @__PURE__ */ new Map()) {
16576
16639
  const start = o.start;
16577
16640
  if (start === void 0) return [];
16578
16641
  const raw = rawById.get(o.id);
16579
- const text = o.kind === "text" ? overlayPlainText(o) : o.kind === "html" ? void 0 : o.key;
16642
+ const text = o.kind === "text" ? overlayPlainText2(o) : o.kind === "html" ? void 0 : o.key;
16580
16643
  return [
16581
16644
  {
16582
16645
  id: o.id,
@@ -19328,7 +19391,8 @@ async function cmdPlan(argvIn) {
19328
19391
  const zoomManual = s.doc.zoom.length - zoomAuto;
19329
19392
  const captionLines = captions ? `
19330
19393
  ${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} anchored to their steps` + (captions.skipped.length ? `
19331
- not landed: ${captions.skipped.join("; ")}` : "") + (captions.uncaptioned.length ? `
19394
+ not landed: ${captions.skipped.join("; ")}` : "") + (captions.held.length ? `
19395
+ ${captions.held.join("\n ")}` : "") + (captions.uncaptioned.length ? `
19332
19396
  no step in ${captions.uncaptioned.length === 1 && captions.uncaptioned[0] === "" ? "this take" : captions.uncaptioned.map((m) => m ? `media ${m}` : "the primary").join(", ")} carries a caption` : "") : "";
19333
19397
  const layoutLines = s.layout ? `
19334
19398
  layout from ${s.styleFrom}: ${[
@@ -19339,7 +19403,8 @@ async function cmdPlan(argvIn) {
19339
19403
  ${s.layout.notes.join("\n ")}` : "") : "";
19340
19404
  const motionLines = s.motion ? (s.motion.notes.length ? `
19341
19405
  proposed: ${s.motion.notes.join(", ")}` : "") + (s.motion.skipped.length ? `
19342
- not proposed: ${s.motion.skipped.join("; ")}` : "") : "";
19406
+ not proposed: ${s.motion.skipped.join("; ")}` : "") + (s.motion.held.length ? `
19407
+ ${s.motion.held.join("\n ")}` : "") : "";
19343
19408
  const reuseLines = s.reuse ? `
19344
19409
  reused ${s.reuse.from}: ${s.reuse.anchored} anchored + ${s.reuse.mapped} mapped span(s)` + (s.reuse.flagged.length ? `
19345
19410
  flagged:
@@ -20617,4 +20682,4 @@ export {
20617
20682
  verbHelp,
20618
20683
  run
20619
20684
  };
20620
- //# sourceMappingURL=chunk-S3RI2SHH.js.map
20685
+ //# sourceMappingURL=chunk-5VGIQKVT.js.map