@vosjs/cli 0.19.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.
@@ -9,8 +9,7 @@ import {
9
9
  import { mkdir as mkdir9, readFile as readFile14, rm as rm4 } from "fs/promises";
10
10
  import { existsSync as existsSync14 } from "fs";
11
11
  import { join as join17, resolve as resolve9 } from "path";
12
- import { totalDuration as totalDuration2 } from "@vosjs/timeline";
13
- import { migrateHostedDoc as migrateHostedDoc5, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
12
+ import { docOutputDuration as docOutputDuration3, migrateHostedDoc as migrateHostedDoc5 } from "@vosjs/studio-core";
14
13
 
15
14
  // src/plugin/args.ts
16
15
  var UsageError = class extends Error {
@@ -189,6 +188,8 @@ import {
189
188
  TEXT_ENTER_KINDS,
190
189
  TEXT_EXIT_KINDS,
191
190
  EXPORT_RESOLUTION_OPTIONS,
191
+ FREEZE_SECONDS_MAX,
192
+ FREEZE_SECONDS_MIN,
192
193
  SPEED_RATE_MAX,
193
194
  SPEED_RATE_MIN,
194
195
  TILT_DEG_MAX,
@@ -399,14 +400,45 @@ function lintDoc(docIn) {
399
400
  }
400
401
  for (const [i, seg] of (Array.isArray(doc.segments) ? doc.segments : []).entries()) {
401
402
  const hold = seg.hold;
402
- 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) {
403
405
  problems.push(
404
- `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)})`
405
434
  );
406
435
  }
407
436
  }
408
437
  const endCard = doc.endCard;
409
438
  if (endCard !== void 0) {
439
+ warnings.push(
440
+ "endCard is a legacy spelling, read as clips after the footage plus a card exit (frame.anim.exit); vos plan writes that shape"
441
+ );
410
442
  if (typeof endCard !== "object" || endCard === null) {
411
443
  problems.push(
412
444
  "endCard must be an object: {seconds?, headline?, sub?, wordmark?}"
@@ -653,6 +685,9 @@ function lintDoc(docIn) {
653
685
  }
654
686
  const ent = frame.entrance;
655
687
  if (ent !== void 0) {
688
+ warnings.push(
689
+ "frame.entrance is a legacy spelling; write frame.anim.enter"
690
+ );
656
691
  const kinds = ["tilt-in", "pull-out", "rise", "none"];
657
692
  if (typeof ent !== "object" || ent === null || !kinds.includes(String(ent.kind))) {
658
693
  problems.push(
@@ -925,6 +960,10 @@ function lintDoc(docIn) {
925
960
  problems.push(`${name}.${key} must be one of ${TRANSITIONS.join("|")}`);
926
961
  }
927
962
  }
963
+ if (o.enter !== void 0 || o.exit !== void 0 || o.fx !== void 0)
964
+ warnings.push(
965
+ `${name}: enter, exit and fx are legacy spellings; write anim.enter / anim.exit`
966
+ );
928
967
  lintAnim(
929
968
  name,
930
969
  o.anim,
@@ -1072,6 +1111,8 @@ function lintDoc(docIn) {
1072
1111
  if (o.animation !== void 0 && o.animation !== null && o.animation !== "spin" && o.animation !== "float") {
1073
1112
  problems.push(`${name}.animation must be "spin" | "float" | null`);
1074
1113
  }
1114
+ if (o.animation !== void 0)
1115
+ warnings.push(`${name}.animation is a legacy spelling; write anim.idle`);
1075
1116
  lintAnim(
1076
1117
  name,
1077
1118
  o.anim,
@@ -2759,7 +2800,6 @@ async function writeIndexJson(result) {
2759
2800
  import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
2760
2801
  import { tmpdir } from "os";
2761
2802
  import { join as join7, relative, resolve as resolve4 } from "path";
2762
- import { totalDuration } from "@vosjs/timeline";
2763
2803
  import { existsSync as existsSync6 } from "fs";
2764
2804
  import { readFile as readFile6 } from "fs/promises";
2765
2805
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
@@ -2771,7 +2811,7 @@ import {
2771
2811
  houseLook,
2772
2812
  isLookKind,
2773
2813
  lookFromBrand,
2774
- ratedSegments as ratedSegments5
2814
+ docOutputDuration as docOutputDuration2
2775
2815
  } from "@vosjs/studio-core";
2776
2816
 
2777
2817
  // src/plugin/renderTake.ts
@@ -3387,7 +3427,18 @@ function pickMoments(measured, opts = {}) {
3387
3427
  }
3388
3428
 
3389
3429
  // src/plugin/motionPlan.ts
3390
- import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
3430
+ import {
3431
+ END_CARD_FROM,
3432
+ END_CARD_RECEDE,
3433
+ END_CARD_SECONDS,
3434
+ applyTemplate,
3435
+ docOutputDuration,
3436
+ dropTemplate,
3437
+ endCardClips,
3438
+ migrateMotion,
3439
+ ratedSegments as ratedSegments4,
3440
+ spanOutputExtent as spanOutputExtent4
3441
+ } from "@vosjs/studio-core";
3391
3442
  var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
3392
3443
  "x-feed-cut",
3393
3444
  "youtube-main-demo",
@@ -3422,41 +3473,103 @@ var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
3422
3473
  const rate = s.rate && s.rate > 0 ? s.rate : 1;
3423
3474
  return acc + (s.out - s.in) / rate;
3424
3475
  }, 0);
3476
+ function templateWords(words) {
3477
+ const headline = (words.headline ?? "").trim();
3478
+ const kicker = (words.kicker ?? "").trim();
3479
+ const brand = (words.brand ?? "").trim();
3480
+ const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
3481
+ const out = {};
3482
+ if (headline) {
3483
+ out["stage-title"] = headline;
3484
+ out["endcard-title"] = headline;
3485
+ }
3486
+ if (kicker) out["stage-kicker"] = kicker;
3487
+ if (brand) {
3488
+ out["stage-brand"] = brand;
3489
+ out["endcard-mark"] = brand;
3490
+ }
3491
+ if (sub && sub !== headline) out["endcard-sub"] = sub;
3492
+ return out;
3493
+ }
3494
+ function resolveAnchor(doc, at2) {
3495
+ if (typeof at2 !== "object") return at2;
3496
+ const steps = doc.source.meta.steps ?? [];
3497
+ const step = steps.find((s) => s.id === at2.step || String(s.step) === at2.step);
3498
+ if (!step || step.skipped) return null;
3499
+ const t = stepOutputTime(ratedSegments4(doc), step, 0.2);
3500
+ return t === null ? null : +t.toFixed(3);
3501
+ }
3502
+ var anchorWord = (at2) => typeof at2 === "object" ? `step ${at2.step}` : typeof at2 === "number" ? `${at2}s` : `the ${at2}`;
3503
+ var onWord = (v) => v === void 0 || /^(on|yes|true)$/i.test(v.trim());
3425
3504
  function proposeMotion(input, opts) {
3426
- const doc = structuredClone(input);
3505
+ let doc = migrateMotion(structuredClone(input));
3427
3506
  const { words, launch, catalog } = opts;
3428
3507
  const notes = [];
3429
3508
  const skipped = [];
3430
- const length = outputLength(doc);
3431
- const range = [0, length];
3509
+ const footage = outputLength(doc);
3510
+ const range = [0, footage];
3432
3511
  const entrance = launch.entrance;
3512
+ delete doc.frame.entrance;
3433
3513
  if (!off(entrance)) {
3434
- const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
3435
- doc.frame.entrance = { kind };
3436
- notes.push(`entrance ${kind}`);
3437
- } else {
3438
- delete doc.frame.entrance;
3439
- }
3440
- if (!off(launch.endCard)) {
3514
+ const kind = entrance && /^(tilt-in|pull-out|rise|fade)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
3515
+ doc.frame.anim = { ...doc.frame.anim ?? {}, enter: kind };
3516
+ notes.push(`enter ${kind}`);
3517
+ } else if (doc.frame.anim?.enter !== void 0) {
3518
+ const { enter: _enter, ...rest } = doc.frame.anim;
3519
+ if (Object.keys(rest).length) doc.frame.anim = rest;
3520
+ else delete doc.frame.anim;
3521
+ }
3522
+ const tWords = templateWords(words);
3523
+ const keys = opts.mark ? { "stage-mark": opts.mark.key, "endcard-markimg": opts.mark.key } : void 0;
3524
+ for (const t of opts.templates ?? []) {
3525
+ const at2 = resolveAnchor(doc, t.at);
3526
+ if (at2 === null) {
3527
+ skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
3528
+ continue;
3529
+ }
3530
+ const applied = applyTemplate(t.doc, doc, {
3531
+ at: at2,
3532
+ from: t.from,
3533
+ words: tWords,
3534
+ keys
3535
+ });
3536
+ doc = applied.doc;
3537
+ notes.push(`${t.from} at ${anchorWord(t.at)}`);
3538
+ for (const n of applied.notes) skipped.push(`${t.from}: ${n}`);
3539
+ }
3540
+ const endCardRole = launch.endCard;
3541
+ doc = dropTemplate(doc, END_CARD_FROM);
3542
+ if (off(endCardRole)) {
3543
+ if (doc.frame.anim?.exit !== void 0) {
3544
+ const { exit: _exit, ...rest } = doc.frame.anim;
3545
+ if (Object.keys(rest).length) doc.frame.anim = rest;
3546
+ else delete doc.frame.anim;
3547
+ }
3548
+ } else if (onWord(endCardRole)) {
3441
3549
  const headline = (words.headline ?? "").trim();
3442
3550
  const brand = (words.brand ?? "").trim();
3443
3551
  const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
3444
3552
  if (headline || brand) {
3445
- const card = { seconds: 2.5 };
3553
+ const card = { seconds: END_CARD_SECONDS };
3446
3554
  if (opts.ink) card.ink = opts.ink;
3447
3555
  if (headline) card.headline = headline;
3448
3556
  if (sub && sub !== headline) card.sub = sub;
3449
3557
  if (brand) card.wordmark = brand;
3450
3558
  if (opts.mark) card.mark = opts.mark;
3451
- doc.endCard = card;
3559
+ doc.overlays = [
3560
+ ...doc.overlays ?? [],
3561
+ ...endCardClips(card, doc, outputLength(doc))
3562
+ ];
3563
+ doc.frame.anim = {
3564
+ ...doc.frame.anim ?? {},
3565
+ exit: { kind: "recede", seconds: END_CARD_RECEDE }
3566
+ };
3452
3567
  notes.push("end card");
3453
3568
  } else {
3454
3569
  skipped.push(
3455
3570
  "no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)"
3456
3571
  );
3457
3572
  }
3458
- } else {
3459
- delete doc.endCard;
3460
3573
  }
3461
3574
  const kept = (doc.overlays ?? []).filter(
3462
3575
  (o) => !o.id.startsWith(CAPTION_ID_PREFIX)
@@ -3471,17 +3584,16 @@ function proposeMotion(input, opts) {
3471
3584
  );
3472
3585
  if (!step || step.skipped) continue;
3473
3586
  const t = stepOutputTime(rated, step, 0.2);
3474
- if (t === null || t < 0 || t > length - 1) continue;
3587
+ if (t === null || t < 0 || t > footage - 1) continue;
3475
3588
  captionClips.push({
3476
3589
  id: `${CAPTION_ID_PREFIX}${c.step}`,
3477
3590
  kind: "text",
3478
3591
  text: c.caption,
3479
3592
  preset: "caption",
3480
3593
  start: +t.toFixed(3),
3481
- duration: Math.min(3.5, Math.max(2.5, length - t - 0.2)),
3594
+ duration: Math.min(3.5, Math.max(2.5, footage - t - 0.2)),
3482
3595
  transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3483
- enter: "rise",
3484
- exit: "fade",
3596
+ anim: { enter: "rise", exit: "fade" },
3485
3597
  align: "center",
3486
3598
  box: { color: "rgba(17,17,17,0.72)" }
3487
3599
  });
@@ -3491,6 +3603,7 @@ function proposeMotion(input, opts) {
3491
3603
  const overlays = [...kept, ...captionClips];
3492
3604
  if (overlays.length) doc.overlays = overlays;
3493
3605
  else delete doc.overlays;
3606
+ const length = docOutputDuration(doc);
3494
3607
  const clips = (doc.audio ?? []).filter(
3495
3608
  (a) => a.id !== BED_ID && !a.id.startsWith(CLICK_ID_PREFIX)
3496
3609
  );
@@ -3550,8 +3663,8 @@ function destinationMechanics(d, doc) {
3550
3663
  const sound = SOUND_DESTINATIONS.has(d.id);
3551
3664
  const portrait = d.px.w / d.px.h < 0.9;
3552
3665
  if (loop) {
3553
- unset.push("frame.entrance", "endCard");
3554
- notes.push("loop: no entrance, no end card");
3666
+ unset.push("frame.anim", "frame.entrance", "endCard");
3667
+ notes.push("loop: no card motion");
3555
3668
  }
3556
3669
  if (loop || !sound) {
3557
3670
  set.push("audio=[]");
@@ -3559,11 +3672,11 @@ function destinationMechanics(d, doc) {
3559
3672
  }
3560
3673
  if (loop || d.text === "none") {
3561
3674
  const kept = (doc.overlays ?? []).filter(
3562
- (o) => !o.id.startsWith(CAPTION_ID_PREFIX)
3675
+ (o) => !o.id.startsWith(CAPTION_ID_PREFIX) && !(loop && o.from)
3563
3676
  );
3564
3677
  if (kept.length !== (doc.overlays ?? []).length) {
3565
3678
  set.push(`overlays=${JSON.stringify(kept)}`);
3566
- notes.push("no captions");
3679
+ notes.push(loop ? "no template clips, no captions" : "no captions");
3567
3680
  }
3568
3681
  }
3569
3682
  if (portrait) {
@@ -4087,7 +4200,7 @@ async function deliverTake(browser, dir, opts) {
4087
4200
  const take = await loadTake(dir);
4088
4201
  if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
4089
4202
  const doc = take.doc;
4090
- const duration = totalDuration(ratedSegments5(doc));
4203
+ const duration = docOutputDuration2(doc);
4091
4204
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
4092
4205
  const outDir = resolve4(opts.outDir ?? join7(dir, "kit"));
4093
4206
  await mkdir4(outDir, { recursive: true });
@@ -4145,7 +4258,7 @@ async function deliverTake(browser, dir, opts) {
4145
4258
  }
4146
4259
  posterCardIds.add(d.id);
4147
4260
  const label = `${d.channel} ${d.asset}`;
4148
- const posterDuration = totalDuration(ratedSegments5(ref.doc));
4261
+ const posterDuration = docOutputDuration2(ref.doc);
4149
4262
  const time = posterStillTime(ref.doc, posterDuration);
4150
4263
  opts.onPhase?.(
4151
4264
  `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
@@ -5701,10 +5814,12 @@ import {
5701
5814
  planAutoSpeed,
5702
5815
  planAutoZoom,
5703
5816
  projectFromArtifact,
5704
- withBackdrop
5817
+ withBackdrop,
5818
+ migrateMotion as migrateMotion2
5705
5819
  } from "@vosjs/studio-core";
5706
5820
 
5707
5821
  // src/plugin/reuse.ts
5822
+ import { docFreezes } from "@vosjs/studio-core";
5708
5823
  var MIN_SPAN = 0.15;
5709
5824
  function matchStep(old, newSteps) {
5710
5825
  const usable = (s) => !!s && s.skipped !== true;
@@ -5763,7 +5878,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
5763
5878
  };
5764
5879
  return { map, unmatched, matched };
5765
5880
  }
5766
- function resolveAnchor(anchor, newSteps) {
5881
+ function resolveAnchor2(anchor, newSteps) {
5767
5882
  const step = typeof anchor.step === "string" ? newSteps.find((s) => s.id === anchor.step) : newSteps.find((s) => s.step === anchor.step);
5768
5883
  if (!step || step.skipped) return null;
5769
5884
  const base = anchor.at === "end" ? step.tEnd : step.tStart;
@@ -5775,7 +5890,7 @@ function retimeSpans(kind, spans, stepMap, newSteps, newDuration, report) {
5775
5890
  const length = span.out - span.in;
5776
5891
  let nextIn = null;
5777
5892
  if (span.anchor) {
5778
- nextIn = resolveAnchor(span.anchor, newSteps);
5893
+ nextIn = resolveAnchor2(span.anchor, newSteps);
5779
5894
  if (nextIn === null) {
5780
5895
  report.flagged.push(
5781
5896
  `${kind} ${span.id}: its anchored step (${String(span.anchor.step)}) is missing or skipped in the new recording \u2014 fell back to the step map`
@@ -5866,6 +5981,30 @@ function retimeCut(prev, newSteps, newDurationMs) {
5866
5981
  newDuration,
5867
5982
  report
5868
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);
5869
6008
  const rejected = [];
5870
6009
  for (const lane of ["zoom", "tilt", "speed"]) {
5871
6010
  rejected.push(
@@ -5879,7 +6018,7 @@ function retimeCut(prev, newSteps, newDurationMs) {
5879
6018
  )
5880
6019
  );
5881
6020
  }
5882
- return { segments, zoom, speed, tilt, rejected, report };
6021
+ return { segments, zoom, speed, freeze, tilt, rejected, report };
5883
6022
  }
5884
6023
 
5885
6024
  // src/plugin/plan.ts
@@ -5908,7 +6047,7 @@ function patchStageWords(doc, words) {
5908
6047
  }
5909
6048
  function applyStyle(seed, doc, opts) {
5910
6049
  const parts = layoutOf(seed);
5911
- const carries = parts.clips.length > 0 || parts.lean || parts.hold;
6050
+ const carries = parts.clips.length > 0 || parts.lean || parts.freeze;
5912
6051
  if (!carries) return { doc: copyStyle(seed, doc), layout: void 0 };
5913
6052
  const keys = opts.mark ? { "stage-mark": opts.mark.key } : void 0;
5914
6053
  const { doc: next, notes } = copyLayout(seed, copyStyle(seed, doc), { keys });
@@ -5924,7 +6063,7 @@ async function planTake(dir, opts = {}) {
5924
6063
  let fresh;
5925
6064
  let layout;
5926
6065
  if (opts.reuse) {
5927
- const prev = opts.reuse.doc;
6066
+ const prev = migrateMotion2(opts.reuse.doc);
5928
6067
  const artifact = {
5929
6068
  videoKey: RECORDING_NAME,
5930
6069
  cursor,
@@ -5934,10 +6073,7 @@ async function planTake(dir, opts = {}) {
5934
6073
  doc = copyStyle(prev, doc);
5935
6074
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
5936
6075
  doc.segments = rt.segments;
5937
- const prevHold = prev.segments.at(-1)?.hold;
5938
- const last = doc.segments.at(-1);
5939
- if (typeof prevHold === "number" && prevHold > 0 && last)
5940
- last.hold = prevHold;
6076
+ if (rt.freeze.length) doc.freeze = rt.freeze;
5941
6077
  if (rt.rejected.length) doc.rejected = rt.rejected;
5942
6078
  const manualZoom = rt.zoom;
5943
6079
  const autoZoom = planAutoZoom(doc.source.cursor, {
@@ -5959,7 +6095,6 @@ async function planTake(dir, opts = {}) {
5959
6095
  if (prev.objects?.length) doc.objects = prev.objects;
5960
6096
  if (prev.audio.length) doc.audio = prev.audio;
5961
6097
  if (prev.camMotion?.length) doc.camMotion = prev.camMotion;
5962
- if (prev.endCard) doc.endCard = structuredClone(prev.endCard);
5963
6098
  await writeJson(take.paths.doc, doc, true);
5964
6099
  return {
5965
6100
  doc,
@@ -8657,7 +8792,7 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
8657
8792
  Take pipeline
8658
8793
  vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
8659
8794
  vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
8660
- vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--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|none] [--end-card none] [--captions none] [--clicks none] [--release v2.1] [--json]
8795
+ 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] [--release v2.1] [--json]
8661
8796
  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]
8662
8797
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
8663
8798
  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]
@@ -8772,7 +8907,7 @@ Screenshot-genre stills never take a look.
8772
8907
  A POSTER is a document, never a template: a plain take whose card sits
8773
8908
  where the poster wants it (frame.inset), leans (the tilt track), carries
8774
8909
  the words and the mark as clips (ids stage-title, stage-kicker,
8775
- 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
8776
8911
  still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
8777
8912
  CWS tile + marquee, GitHub social preview) render from the poster
8778
8913
  document of their aspect CLASS (landscape, square, portrait, tile), found
@@ -8785,20 +8920,28 @@ deliver renders and verifies; it composes nothing. The composition is
8785
8920
  made with plan --style <poster>, which copies a poster's layout onto a
8786
8921
  take (its card placement, its stage clips with the release's words
8787
8922
  patched in from LAUNCH.md's headline and kicker roles or --headline and
8788
- --kicker, its rest lean, its hold), or by hand in doc.json.
8789
- The cut's MOTION is the document's too. plan proposes it on a fresh plan
8790
- (--motion re-proposes onto an existing doc.json, replacing only its own
8791
- proposals): the card's ENTRANCE (tilt-in by default: the card swings in
8792
- from a perspective pose and settles), the END CARD (the last frame holds
8793
- 2.5 s while the card recedes and the headline, the release line and the
8794
- wordmark rise; the brand's mark from BRAND.md logoUrl above them), a
8795
- CAPTION per actions.json step at the step's moment, a music BED from
8796
- LAUNCH.md's music role (a catalog slug or a mood) and a click sound on
8797
- every press when the take has no mic. LAUNCH.md's entrance, endCard,
8798
- captions, music and clicks roles, or the flags, change or switch each
8799
- off; a deleted proposal stays deleted on a refresh. deliver applies each
8800
- destination's MECHANICS and nothing more: the README loop plays no
8801
- entrance, end card or sound; a channel that autoplays muted drops the
8923
+ --kicker, its rest lean, its freeze), or by hand in doc.json.
8924
+ The cut's MOTION is the document's too, in ONE vocabulary: every visual
8925
+ thing (the card, a text, image or video clip, a prop) carries anim.enter,
8926
+ anim.exit and anim.idle; the output lasts until the last clip ends, and
8927
+ past its footage the card holds its last frame at the pose its exit
8928
+ settled into. There is no end-card field and no entrance field: a
8929
+ COMPONENT is a TEMPLATE, a plain take on a shelf whose clips carry stable
8930
+ ids, laid onto the take at an anchor (LAUNCH.md with: <ref>[@end|@start|
8931
+ @step:<id>|@<s>], comma list; --with the same, repeatable; endCard: <ref>
8932
+ names one at the end), its clips stamped with where they came from
8933
+ (from). plan proposes on a fresh plan (--motion re-proposes, replacing
8934
+ only its own work): the card's ENTER (tilt-in by default), the templates
8935
+ named, the house END CARD when endCard is on or absent (clips after the
8936
+ footage: the headline, the release line, the wordmark, the mark from
8937
+ BRAND.md logoUrl, over a card that recedes; from: endcard), a CAPTION per
8938
+ actions.json step at the step's moment, a music BED from LAUNCH.md's music
8939
+ role (a catalog slug or a mood) and a click sound on every press when the
8940
+ take has no mic. LAUNCH.md's entrance, endCard, captions, music and clicks
8941
+ roles, or the flags, change or switch each off; a deleted proposal stays
8942
+ deleted on a refresh. deliver applies each destination's MECHANICS and
8943
+ nothing more: the README loop drops the card's motion, every clip a
8944
+ template placed and every sound; a channel that autoplays muted drops the
8802
8945
  bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
8803
8946
  the camera. Screenshot-genre
8804
8947
  destinations (CWS screenshots, the PH gallery) are the real page at that
@@ -9109,11 +9252,15 @@ async function cmdCreate2(argv) {
9109
9252
  }
9110
9253
  }
9111
9254
  async function cmdPlan(argv) {
9112
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
9255
+ const { positionals, flags, multi } = parseArgs(
9256
+ argv,
9257
+ BOOLEAN_FLAGS5,
9258
+ /* @__PURE__ */ new Set(["with"])
9259
+ );
9113
9260
  const dir = positionals[0];
9114
9261
  if (!dir)
9115
9262
  throw new UsageError(
9116
- 'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
9263
+ 'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<seconds>]]... [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
9117
9264
  );
9118
9265
  const r = createReporter(flags.json === true);
9119
9266
  if (flags.fresh === true) {
@@ -9136,7 +9283,7 @@ async function cmdPlan(argv) {
9136
9283
  const style = await resolveStyleRef(flags);
9137
9284
  const hasDoc = existsSync14(join17(dir, "doc.json"));
9138
9285
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
9139
- const release = await releaseInputs(dir, flags, r);
9286
+ const release = await releaseInputs(dir, flags, r, multi.with ?? []);
9140
9287
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
9141
9288
  const s = await planTake(dir, {
9142
9289
  ...style ? { style } : {},
@@ -9152,6 +9299,7 @@ async function cmdPlan(argv) {
9152
9299
  mark: release.mark,
9153
9300
  captions: release.captions,
9154
9301
  catalog: release.catalog,
9302
+ templates: release.templates,
9155
9303
  again: flags.motion === true
9156
9304
  }
9157
9305
  } : {}
@@ -9160,7 +9308,7 @@ async function cmdPlan(argv) {
9160
9308
  layout from ${s.styleFrom}: ${[
9161
9309
  s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
9162
9310
  s.layout.lean ? "the rest lean" : "",
9163
- s.layout.hold ? "the hold" : ""
9311
+ s.layout.freeze ? "the freeze" : ""
9164
9312
  ].filter(Boolean).join(", ")}` + (s.layout.notes.length ? `
9165
9313
  ${s.layout.notes.join("\n ")}` : "") : "";
9166
9314
  const motionLines = s.motion ? (s.motion.notes.length ? `
@@ -9188,7 +9336,7 @@ async function cmdPlan(argv) {
9188
9336
  );
9189
9337
  return EXIT_OK;
9190
9338
  }
9191
- async function releaseInputs(dir, flags, r) {
9339
+ async function releaseInputs(dir, flags, r, withRefs = []) {
9192
9340
  let lookPick;
9193
9341
  try {
9194
9342
  lookPick = await resolveLook(dir, {
@@ -9243,13 +9391,36 @@ async function releaseInputs(dir, flags, r) {
9243
9391
  const step = st;
9244
9392
  return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
9245
9393
  });
9394
+ const refs = [];
9395
+ const parseRef = (raw, fallback) => {
9396
+ const m = /^(.*?)(?:@(end|start|step:[^@\s]+|\d+(?:\.\d+)?))?$/.exec(
9397
+ raw.trim()
9398
+ );
9399
+ const ref = m?.[1]?.trim() ?? raw.trim();
9400
+ const anchor = m?.[2];
9401
+ const at2 = !anchor ? fallback : anchor === "end" || anchor === "start" ? anchor : anchor.startsWith("step:") ? { step: anchor.slice(5) } : Number(anchor);
9402
+ if (ref) refs.push({ ref, at: at2 });
9403
+ };
9404
+ for (const raw of withRefs) parseRef(raw, "end");
9405
+ for (const raw of (launchRoles.with ?? "").split(","))
9406
+ if (raw.trim()) parseRef(raw, "end");
9407
+ const endCardRole = launchRoles.endCard;
9408
+ if (endCardRole && !/^(none|off|no|false|on|yes|true)$/i.test(endCardRole.trim()))
9409
+ parseRef(endCardRole, "end");
9410
+ const templates = [];
9411
+ for (const { ref, at: at2 } of refs) {
9412
+ const found = await resolveDocRef(ref, flags, "--with");
9413
+ templates.push({ from: found.from, doc: found.doc, at: at2 });
9414
+ r.log(`template: ${found.from}`);
9415
+ }
9246
9416
  return {
9247
9417
  words,
9248
9418
  launchRoles,
9249
9419
  ink: endCardInk(lookPick.look, lookPick.roles),
9250
9420
  mark,
9251
9421
  captions,
9252
- catalog
9422
+ catalog,
9423
+ templates
9253
9424
  };
9254
9425
  }
9255
9426
  async function cmdRender(argv) {
@@ -9344,7 +9515,7 @@ async function cmdFrames(argv) {
9344
9515
  if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
9345
9516
  size = { width: Number(m[1]), height: Number(m[2]) };
9346
9517
  }
9347
- const duration = totalDuration2(ratedSegments6(take.doc));
9518
+ const duration = docOutputDuration3(take.doc);
9348
9519
  const frameRaw = strFlag(flags, "frame");
9349
9520
  const timesRaw = strFlag(flags, "times");
9350
9521
  const atZooms = flags["at-zooms"] === true;
@@ -9431,7 +9602,7 @@ async function cmdDeliver(argv) {
9431
9602
  }
9432
9603
  const take = await loadTake(dir);
9433
9604
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
9434
- const duration = totalDuration2(ratedSegments6(take.doc));
9605
+ const duration = docOutputDuration3(take.doc);
9435
9606
  const timesRaw = strFlag(flags, "times");
9436
9607
  let times;
9437
9608
  if (timesRaw !== void 0) {
@@ -9509,6 +9680,10 @@ Store uploads stay manual: hand the human this directory and the manifest.`
9509
9680
  async function resolveStyleRef(flags) {
9510
9681
  const styleRef = strFlag(flags, "style");
9511
9682
  if (!styleRef) return null;
9683
+ return resolveDocRef(styleRef, flags, "--style");
9684
+ }
9685
+ async function resolveDocRef(ref, flags, what) {
9686
+ const styleRef = ref;
9512
9687
  const file = existsSync14(styleRef) ? resolve9(
9513
9688
  styleRef,
9514
9689
  existsSync14(join17(styleRef, "doc.json")) ? "doc.json" : ""
@@ -9516,7 +9691,9 @@ async function resolveStyleRef(flags) {
9516
9691
  if (file && existsSync14(file)) {
9517
9692
  return {
9518
9693
  from: file,
9519
- doc: JSON.parse(await readFile14(file, "utf8"))
9694
+ doc: migrateHostedDoc5(
9695
+ JSON.parse(await readFile14(file, "utf8"))
9696
+ )
9520
9697
  };
9521
9698
  }
9522
9699
  const origin = platformOrigin({
@@ -9528,7 +9705,7 @@ async function resolveStyleRef(flags) {
9528
9705
  const head = meta.body.vos?.currentVersionId;
9529
9706
  if (meta.status !== 200 || !head)
9530
9707
  throw new UsageError(
9531
- `--style: ${styleRef} is neither a doc.json nor a vos I can read`
9708
+ `${what}: ${styleRef} is neither a doc.json nor a vos I can read`
9532
9709
  );
9533
9710
  const doc = await apiJson(
9534
9711
  origin,
@@ -9536,7 +9713,7 @@ async function resolveStyleRef(flags) {
9536
9713
  { key }
9537
9714
  );
9538
9715
  if (doc.status !== 200)
9539
- throw new UsageError(`--style: ${styleRef} carries no doc (is it a take?)`);
9716
+ throw new UsageError(`${what}: ${styleRef} carries no doc (is it a take?)`);
9540
9717
  return {
9541
9718
  from: `${origin}/vos/${styleRef}`,
9542
9719
  doc: migrateHostedDoc5(doc.body)
@@ -9960,4 +10137,4 @@ export {
9960
10137
  convertAgentBrowser,
9961
10138
  run
9962
10139
  };
9963
- //# sourceMappingURL=chunk-4LLBPFCW.js.map
10140
+ //# sourceMappingURL=chunk-4IWHZBIY.js.map