@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 +2 -0
- package/dist/{chunk-S3RI2SHH.js → chunk-5VGIQKVT.js} +83 -18
- package/dist/chunk-5VGIQKVT.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{run-MEDD2Q74.js → run-CPFCYXVL.js} +2 -2
- package/package.json +2 -2
- package/skills/VERSION +1 -1
- package/skills/launch-kit/SKILL.md +7 -0
- package/skills/product-video/SKILL.md +91 -3
- package/skills/product-video/references/how-to.md +78 -0
- package/skills/product-video/references/taste.md +4 -0
- package/skills/product-video/references/troubleshooting.md +8 -4
- package/skills/vos-cut/SKILL.md +34 -0
- package/dist/chunk-S3RI2SHH.js.map +0 -1
- /package/dist/{run-MEDD2Q74.js.map → run-CPFCYXVL.js.map} +0 -0
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
4467
|
-
|
|
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
|
|
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 =
|
|
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 = {
|
|
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 = {
|
|
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" ?
|
|
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.
|
|
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-
|
|
20685
|
+
//# sourceMappingURL=chunk-5VGIQKVT.js.map
|