@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 +3 -3
- package/dist/{chunk-ZONOZZZY.js → chunk-4IWHZBIY.js} +64 -12
- package/dist/chunk-4IWHZBIY.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.js +1 -1
- package/dist/{run-ZIMJOQRN.js → run-7AOC4JUE.js} +2 -2
- package/package.json +5 -5
- package/schema/doc.schema.json +51 -2
- package/dist/chunk-ZONOZZZY.js.map +0 -1
- /package/dist/{run-ZIMJOQRN.js.map → run-7AOC4JUE.js.map} +0 -0
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
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
10140
|
+
//# sourceMappingURL=chunk-4IWHZBIY.js.map
|