@vosjs/cli 0.18.0 → 0.20.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 {
@@ -181,6 +180,13 @@ import {
181
180
  CAM_SIZE_MAX,
182
181
  CAM_SIZE_MIN,
183
182
  CAM_SPAN_MIN,
183
+ CARD_ENTER_KINDS,
184
+ CARD_EXIT_KINDS,
185
+ IDLE_KINDS,
186
+ MEDIA_ENTER_KINDS,
187
+ MEDIA_EXIT_KINDS,
188
+ TEXT_ENTER_KINDS,
189
+ TEXT_EXIT_KINDS,
184
190
  EXPORT_RESOLUTION_OPTIONS,
185
191
  SPEED_RATE_MAX,
186
192
  SPEED_RATE_MIN,
@@ -196,6 +202,79 @@ import {
196
202
  zoomCoversRect
197
203
  } from "@vosjs/studio-core";
198
204
  import { TYPEFACE_CATALOG, findFontFamily, findTypeface } from "@vosjs/shared";
205
+ var STEP_KINDS_ALL = [
206
+ "none",
207
+ "fade",
208
+ "rise",
209
+ "pop",
210
+ "blur",
211
+ "typewriter",
212
+ "tilt-in",
213
+ "pull-out",
214
+ "recede"
215
+ ];
216
+ var STEP_UNITS = ["block", "line", "word", "char"];
217
+ var STEP_DIRS = ["forward", "reverse", "center"];
218
+ function lintAnim(name, anim, kinds, problems) {
219
+ if (anim === void 0) return;
220
+ if (typeof anim !== "object" || anim === null || Array.isArray(anim)) {
221
+ problems.push(`${name}.anim must be { enter?, exit?, idle? }`);
222
+ return;
223
+ }
224
+ const a = anim;
225
+ for (const side of ["enter", "exit"]) {
226
+ const step = a[side];
227
+ if (step === void 0) continue;
228
+ const allowed = kinds[side];
229
+ const spelled = typeof step === "string" ? step : null;
230
+ const obj = typeof step === "object" && step !== null && !Array.isArray(step) ? step : null;
231
+ const kind = spelled ?? (obj ? obj.kind : void 0);
232
+ if (typeof kind !== "string" || !STEP_KINDS_ALL.includes(kind)) {
233
+ problems.push(
234
+ `${name}.anim.${side} must be a kind or { kind, seconds?${kinds.words ? ", unit?, direction?, stagger?" : ""} } (got ${JSON.stringify(step)})`
235
+ );
236
+ continue;
237
+ }
238
+ if (!allowed.includes(kind)) {
239
+ problems.push(
240
+ `${name}.anim.${side} cannot be "${kind}" here; one of ${allowed.join(" | ")}`
241
+ );
242
+ }
243
+ if (!obj) continue;
244
+ const secs = obj.seconds;
245
+ if (secs !== void 0 && (typeof secs !== "number" || !Number.isFinite(secs) || secs < 0.05 || secs > 3)) {
246
+ problems.push(`${name}.anim.${side}.seconds must be 0.05..3`);
247
+ }
248
+ for (const key of ["unit", "direction", "stagger"]) {
249
+ if (obj[key] === void 0) continue;
250
+ if (!kinds.words) {
251
+ problems.push(`${name}.anim.${side}.${key} is for words only`);
252
+ continue;
253
+ }
254
+ if (key === "unit" && (typeof obj.unit !== "string" || !STEP_UNITS.includes(obj.unit)))
255
+ problems.push(
256
+ `${name}.anim.${side}.unit must be ${STEP_UNITS.join("|")}`
257
+ );
258
+ if (key === "direction" && (typeof obj.direction !== "string" || !STEP_DIRS.includes(obj.direction)))
259
+ problems.push(
260
+ `${name}.anim.${side}.direction must be ${STEP_DIRS.join("|")}`
261
+ );
262
+ if (key === "stagger" && (typeof obj.stagger !== "number" || obj.stagger < 0 || obj.stagger > 2))
263
+ problems.push(`${name}.anim.${side}.stagger must be seconds in 0..2`);
264
+ }
265
+ }
266
+ if (a.idle !== void 0 && a.idle !== null) {
267
+ if (!kinds.idle) problems.push(`${name}.anim.idle is for props only`);
268
+ else if (typeof a.idle !== "string" || !kinds.idle.includes(a.idle))
269
+ problems.push(
270
+ `${name}.anim.idle must be ${kinds.idle.join(" | ")} | null`
271
+ );
272
+ }
273
+ }
274
+ function lintFrom(name, from, problems) {
275
+ if (from !== void 0 && (typeof from !== "string" || !from.length))
276
+ problems.push(`${name}.from must be the id of the template that placed it`);
277
+ }
199
278
  var EPS = 1e-3;
200
279
  var isNum = (v) => typeof v === "number" && Number.isFinite(v);
201
280
  var isObj = (v) => typeof v === "object" && v !== null;
@@ -327,20 +406,31 @@ function lintDoc(docIn) {
327
406
  }
328
407
  const endCard = doc.endCard;
329
408
  if (endCard !== void 0) {
409
+ warnings.push(
410
+ "endCard is a legacy spelling, read as clips after the footage plus a card exit (frame.anim.exit); vos plan writes that shape"
411
+ );
330
412
  if (typeof endCard !== "object" || endCard === null) {
331
- problems.push("endCard must be an object: {seconds?, headline?, sub?, wordmark?}");
413
+ problems.push(
414
+ "endCard must be an object: {seconds?, headline?, sub?, wordmark?}"
415
+ );
332
416
  } else {
333
417
  const ec = endCard;
334
418
  if (ec.seconds !== void 0 && (!isNum(ec.seconds) || ec.seconds < 1 || ec.seconds > 8)) {
335
- problems.push(`endCard.seconds must be 1..8 (got ${String(ec.seconds)}); absent = 2.5`);
419
+ problems.push(
420
+ `endCard.seconds must be 1..8 (got ${String(ec.seconds)}); absent = 2.5`
421
+ );
336
422
  }
337
423
  for (const k of ["headline", "sub", "wordmark"]) {
338
424
  if (ec[k] !== void 0 && typeof ec[k] !== "string") {
339
425
  problems.push(`endCard.${k} must be a string`);
340
426
  }
341
427
  }
342
- if (!["headline", "sub", "wordmark"].some((k) => typeof ec[k] === "string" && ec[k].trim())) {
343
- warnings.push("endCard carries no words: it holds the last frame and recedes the card over nothing");
428
+ if (!["headline", "sub", "wordmark"].some(
429
+ (k) => typeof ec[k] === "string" && ec[k].trim()
430
+ )) {
431
+ warnings.push(
432
+ "endCard carries no words: it holds the last frame and recedes the card over nothing"
433
+ );
344
434
  }
345
435
  }
346
436
  }
@@ -565,6 +655,9 @@ function lintDoc(docIn) {
565
655
  }
566
656
  const ent = frame.entrance;
567
657
  if (ent !== void 0) {
658
+ warnings.push(
659
+ "frame.entrance is a legacy spelling; write frame.anim.enter"
660
+ );
568
661
  const kinds = ["tilt-in", "pull-out", "rise", "none"];
569
662
  if (typeof ent !== "object" || ent === null || !kinds.includes(String(ent.kind))) {
570
663
  problems.push(
@@ -579,6 +672,17 @@ function lintDoc(docIn) {
579
672
  }
580
673
  }
581
674
  }
675
+ lintAnim(
676
+ "frame",
677
+ frame.anim,
678
+ {
679
+ enter: CARD_ENTER_KINDS,
680
+ exit: CARD_EXIT_KINDS,
681
+ idle: null,
682
+ words: false
683
+ },
684
+ problems
685
+ );
582
686
  if (frame.focusFollow !== void 0 && frame.focusFollow !== "camera") {
583
687
  problems.push(
584
688
  `frame.focusFollow must be "camera" (got ${String(frame.focusFollow)}); it reads under fit: cover only`
@@ -826,6 +930,27 @@ function lintDoc(docIn) {
826
930
  problems.push(`${name}.${key} must be one of ${TRANSITIONS.join("|")}`);
827
931
  }
828
932
  }
933
+ if (o.enter !== void 0 || o.exit !== void 0 || o.fx !== void 0)
934
+ warnings.push(
935
+ `${name}: enter, exit and fx are legacy spellings; write anim.enter / anim.exit`
936
+ );
937
+ lintAnim(
938
+ name,
939
+ o.anim,
940
+ o.kind === "text" ? {
941
+ enter: TEXT_ENTER_KINDS,
942
+ exit: TEXT_EXIT_KINDS,
943
+ idle: null,
944
+ words: true
945
+ } : {
946
+ enter: MEDIA_ENTER_KINDS,
947
+ exit: MEDIA_EXIT_KINDS,
948
+ idle: null,
949
+ words: false
950
+ },
951
+ problems
952
+ );
953
+ lintFrom(name, o.from, problems);
829
954
  if (o.motion !== void 0) {
830
955
  if (!Array.isArray(o.motion)) {
831
956
  problems.push(`${name}.motion must be an array of poses`);
@@ -956,6 +1081,20 @@ function lintDoc(docIn) {
956
1081
  if (o.animation !== void 0 && o.animation !== null && o.animation !== "spin" && o.animation !== "float") {
957
1082
  problems.push(`${name}.animation must be "spin" | "float" | null`);
958
1083
  }
1084
+ if (o.animation !== void 0)
1085
+ warnings.push(`${name}.animation is a legacy spelling; write anim.idle`);
1086
+ lintAnim(
1087
+ name,
1088
+ o.anim,
1089
+ {
1090
+ enter: MEDIA_ENTER_KINDS,
1091
+ exit: MEDIA_EXIT_KINDS,
1092
+ idle: IDLE_KINDS,
1093
+ words: false
1094
+ },
1095
+ problems
1096
+ );
1097
+ lintFrom(name, o.from, problems);
959
1098
  if (o.motion !== void 0) {
960
1099
  if (!Array.isArray(o.motion)) {
961
1100
  problems.push(`${name}.motion must be an array of poses`);
@@ -2631,7 +2770,6 @@ async function writeIndexJson(result) {
2631
2770
  import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
2632
2771
  import { tmpdir } from "os";
2633
2772
  import { join as join7, relative, resolve as resolve4 } from "path";
2634
- import { totalDuration } from "@vosjs/timeline";
2635
2773
  import { existsSync as existsSync6 } from "fs";
2636
2774
  import { readFile as readFile6 } from "fs/promises";
2637
2775
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
@@ -2643,7 +2781,7 @@ import {
2643
2781
  houseLook,
2644
2782
  isLookKind,
2645
2783
  lookFromBrand,
2646
- ratedSegments as ratedSegments5
2784
+ docOutputDuration as docOutputDuration2
2647
2785
  } from "@vosjs/studio-core";
2648
2786
 
2649
2787
  // src/plugin/renderTake.ts
@@ -3259,7 +3397,18 @@ function pickMoments(measured, opts = {}) {
3259
3397
  }
3260
3398
 
3261
3399
  // src/plugin/motionPlan.ts
3262
- import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
3400
+ import {
3401
+ END_CARD_FROM,
3402
+ END_CARD_RECEDE,
3403
+ END_CARD_SECONDS,
3404
+ applyTemplate,
3405
+ docOutputDuration,
3406
+ dropTemplate,
3407
+ endCardClips,
3408
+ migrateMotion,
3409
+ ratedSegments as ratedSegments4,
3410
+ spanOutputExtent as spanOutputExtent4
3411
+ } from "@vosjs/studio-core";
3263
3412
  var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
3264
3413
  "x-feed-cut",
3265
3414
  "youtube-main-demo",
@@ -3294,41 +3443,103 @@ var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
3294
3443
  const rate = s.rate && s.rate > 0 ? s.rate : 1;
3295
3444
  return acc + (s.out - s.in) / rate;
3296
3445
  }, 0);
3446
+ function templateWords(words) {
3447
+ const headline = (words.headline ?? "").trim();
3448
+ const kicker = (words.kicker ?? "").trim();
3449
+ const brand = (words.brand ?? "").trim();
3450
+ const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
3451
+ const out = {};
3452
+ if (headline) {
3453
+ out["stage-title"] = headline;
3454
+ out["endcard-title"] = headline;
3455
+ }
3456
+ if (kicker) out["stage-kicker"] = kicker;
3457
+ if (brand) {
3458
+ out["stage-brand"] = brand;
3459
+ out["endcard-mark"] = brand;
3460
+ }
3461
+ if (sub && sub !== headline) out["endcard-sub"] = sub;
3462
+ return out;
3463
+ }
3464
+ function resolveAnchor(doc, at2) {
3465
+ if (typeof at2 !== "object") return at2;
3466
+ const steps = doc.source.meta.steps ?? [];
3467
+ const step = steps.find((s) => s.id === at2.step || String(s.step) === at2.step);
3468
+ if (!step || step.skipped) return null;
3469
+ const t = stepOutputTime(ratedSegments4(doc), step, 0.2);
3470
+ return t === null ? null : +t.toFixed(3);
3471
+ }
3472
+ var anchorWord = (at2) => typeof at2 === "object" ? `step ${at2.step}` : typeof at2 === "number" ? `${at2}s` : `the ${at2}`;
3473
+ var onWord = (v) => v === void 0 || /^(on|yes|true)$/i.test(v.trim());
3297
3474
  function proposeMotion(input, opts) {
3298
- const doc = structuredClone(input);
3475
+ let doc = migrateMotion(structuredClone(input));
3299
3476
  const { words, launch, catalog } = opts;
3300
3477
  const notes = [];
3301
3478
  const skipped = [];
3302
- const length = outputLength(doc);
3303
- const range = [0, length];
3479
+ const footage = outputLength(doc);
3480
+ const range = [0, footage];
3304
3481
  const entrance = launch.entrance;
3482
+ delete doc.frame.entrance;
3305
3483
  if (!off(entrance)) {
3306
- const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
3307
- doc.frame.entrance = { kind };
3308
- notes.push(`entrance ${kind}`);
3309
- } else {
3310
- delete doc.frame.entrance;
3311
- }
3312
- if (!off(launch.endCard)) {
3484
+ const kind = entrance && /^(tilt-in|pull-out|rise|fade)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
3485
+ doc.frame.anim = { ...doc.frame.anim ?? {}, enter: kind };
3486
+ notes.push(`enter ${kind}`);
3487
+ } else if (doc.frame.anim?.enter !== void 0) {
3488
+ const { enter: _enter, ...rest } = doc.frame.anim;
3489
+ if (Object.keys(rest).length) doc.frame.anim = rest;
3490
+ else delete doc.frame.anim;
3491
+ }
3492
+ const tWords = templateWords(words);
3493
+ const keys = opts.mark ? { "stage-mark": opts.mark.key, "endcard-markimg": opts.mark.key } : void 0;
3494
+ for (const t of opts.templates ?? []) {
3495
+ const at2 = resolveAnchor(doc, t.at);
3496
+ if (at2 === null) {
3497
+ skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
3498
+ continue;
3499
+ }
3500
+ const applied = applyTemplate(t.doc, doc, {
3501
+ at: at2,
3502
+ from: t.from,
3503
+ words: tWords,
3504
+ keys
3505
+ });
3506
+ doc = applied.doc;
3507
+ notes.push(`${t.from} at ${anchorWord(t.at)}`);
3508
+ for (const n of applied.notes) skipped.push(`${t.from}: ${n}`);
3509
+ }
3510
+ const endCardRole = launch.endCard;
3511
+ doc = dropTemplate(doc, END_CARD_FROM);
3512
+ if (off(endCardRole)) {
3513
+ if (doc.frame.anim?.exit !== void 0) {
3514
+ const { exit: _exit, ...rest } = doc.frame.anim;
3515
+ if (Object.keys(rest).length) doc.frame.anim = rest;
3516
+ else delete doc.frame.anim;
3517
+ }
3518
+ } else if (onWord(endCardRole)) {
3313
3519
  const headline = (words.headline ?? "").trim();
3314
3520
  const brand = (words.brand ?? "").trim();
3315
3521
  const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
3316
3522
  if (headline || brand) {
3317
- const card = { seconds: 2.5 };
3523
+ const card = { seconds: END_CARD_SECONDS };
3318
3524
  if (opts.ink) card.ink = opts.ink;
3319
3525
  if (headline) card.headline = headline;
3320
3526
  if (sub && sub !== headline) card.sub = sub;
3321
3527
  if (brand) card.wordmark = brand;
3322
3528
  if (opts.mark) card.mark = opts.mark;
3323
- doc.endCard = card;
3529
+ doc.overlays = [
3530
+ ...doc.overlays ?? [],
3531
+ ...endCardClips(card, doc, outputLength(doc))
3532
+ ];
3533
+ doc.frame.anim = {
3534
+ ...doc.frame.anim ?? {},
3535
+ exit: { kind: "recede", seconds: END_CARD_RECEDE }
3536
+ };
3324
3537
  notes.push("end card");
3325
3538
  } else {
3326
3539
  skipped.push(
3327
3540
  "no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)"
3328
3541
  );
3329
3542
  }
3330
- } else {
3331
- delete doc.endCard;
3332
3543
  }
3333
3544
  const kept = (doc.overlays ?? []).filter(
3334
3545
  (o) => !o.id.startsWith(CAPTION_ID_PREFIX)
@@ -3343,17 +3554,16 @@ function proposeMotion(input, opts) {
3343
3554
  );
3344
3555
  if (!step || step.skipped) continue;
3345
3556
  const t = stepOutputTime(rated, step, 0.2);
3346
- if (t === null || t < 0 || t > length - 1) continue;
3557
+ if (t === null || t < 0 || t > footage - 1) continue;
3347
3558
  captionClips.push({
3348
3559
  id: `${CAPTION_ID_PREFIX}${c.step}`,
3349
3560
  kind: "text",
3350
3561
  text: c.caption,
3351
3562
  preset: "caption",
3352
3563
  start: +t.toFixed(3),
3353
- duration: Math.min(3.5, Math.max(2.5, length - t - 0.2)),
3564
+ duration: Math.min(3.5, Math.max(2.5, footage - t - 0.2)),
3354
3565
  transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3355
- enter: "rise",
3356
- exit: "fade",
3566
+ anim: { enter: "rise", exit: "fade" },
3357
3567
  align: "center",
3358
3568
  box: { color: "rgba(17,17,17,0.72)" }
3359
3569
  });
@@ -3363,6 +3573,7 @@ function proposeMotion(input, opts) {
3363
3573
  const overlays = [...kept, ...captionClips];
3364
3574
  if (overlays.length) doc.overlays = overlays;
3365
3575
  else delete doc.overlays;
3576
+ const length = docOutputDuration(doc);
3366
3577
  const clips = (doc.audio ?? []).filter(
3367
3578
  (a) => a.id !== BED_ID && !a.id.startsWith(CLICK_ID_PREFIX)
3368
3579
  );
@@ -3422,8 +3633,8 @@ function destinationMechanics(d, doc) {
3422
3633
  const sound = SOUND_DESTINATIONS.has(d.id);
3423
3634
  const portrait = d.px.w / d.px.h < 0.9;
3424
3635
  if (loop) {
3425
- unset.push("frame.entrance", "endCard");
3426
- notes.push("loop: no entrance, no end card");
3636
+ unset.push("frame.anim", "frame.entrance", "endCard");
3637
+ notes.push("loop: no card motion");
3427
3638
  }
3428
3639
  if (loop || !sound) {
3429
3640
  set.push("audio=[]");
@@ -3431,11 +3642,11 @@ function destinationMechanics(d, doc) {
3431
3642
  }
3432
3643
  if (loop || d.text === "none") {
3433
3644
  const kept = (doc.overlays ?? []).filter(
3434
- (o) => !o.id.startsWith(CAPTION_ID_PREFIX)
3645
+ (o) => !o.id.startsWith(CAPTION_ID_PREFIX) && !(loop && o.from)
3435
3646
  );
3436
3647
  if (kept.length !== (doc.overlays ?? []).length) {
3437
3648
  set.push(`overlays=${JSON.stringify(kept)}`);
3438
- notes.push("no captions");
3649
+ notes.push(loop ? "no template clips, no captions" : "no captions");
3439
3650
  }
3440
3651
  }
3441
3652
  if (portrait) {
@@ -3959,7 +4170,7 @@ async function deliverTake(browser, dir, opts) {
3959
4170
  const take = await loadTake(dir);
3960
4171
  if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
3961
4172
  const doc = take.doc;
3962
- const duration = totalDuration(ratedSegments5(doc));
4173
+ const duration = docOutputDuration2(doc);
3963
4174
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
3964
4175
  const outDir = resolve4(opts.outDir ?? join7(dir, "kit"));
3965
4176
  await mkdir4(outDir, { recursive: true });
@@ -4017,7 +4228,7 @@ async function deliverTake(browser, dir, opts) {
4017
4228
  }
4018
4229
  posterCardIds.add(d.id);
4019
4230
  const label = `${d.channel} ${d.asset}`;
4020
- const posterDuration = totalDuration(ratedSegments5(ref.doc));
4231
+ const posterDuration = docOutputDuration2(ref.doc);
4021
4232
  const time = posterStillTime(ref.doc, posterDuration);
4022
4233
  opts.onPhase?.(
4023
4234
  `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
@@ -5573,7 +5784,8 @@ import {
5573
5784
  planAutoSpeed,
5574
5785
  planAutoZoom,
5575
5786
  projectFromArtifact,
5576
- withBackdrop
5787
+ withBackdrop,
5788
+ migrateMotion as migrateMotion2
5577
5789
  } from "@vosjs/studio-core";
5578
5790
 
5579
5791
  // src/plugin/reuse.ts
@@ -5635,7 +5847,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
5635
5847
  };
5636
5848
  return { map, unmatched, matched };
5637
5849
  }
5638
- function resolveAnchor(anchor, newSteps) {
5850
+ function resolveAnchor2(anchor, newSteps) {
5639
5851
  const step = typeof anchor.step === "string" ? newSteps.find((s) => s.id === anchor.step) : newSteps.find((s) => s.step === anchor.step);
5640
5852
  if (!step || step.skipped) return null;
5641
5853
  const base = anchor.at === "end" ? step.tEnd : step.tStart;
@@ -5647,7 +5859,7 @@ function retimeSpans(kind, spans, stepMap, newSteps, newDuration, report) {
5647
5859
  const length = span.out - span.in;
5648
5860
  let nextIn = null;
5649
5861
  if (span.anchor) {
5650
- nextIn = resolveAnchor(span.anchor, newSteps);
5862
+ nextIn = resolveAnchor2(span.anchor, newSteps);
5651
5863
  if (nextIn === null) {
5652
5864
  report.flagged.push(
5653
5865
  `${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`
@@ -5796,7 +6008,7 @@ async function planTake(dir, opts = {}) {
5796
6008
  let fresh;
5797
6009
  let layout;
5798
6010
  if (opts.reuse) {
5799
- const prev = opts.reuse.doc;
6011
+ const prev = migrateMotion2(opts.reuse.doc);
5800
6012
  const artifact = {
5801
6013
  videoKey: RECORDING_NAME,
5802
6014
  cursor,
@@ -5831,7 +6043,6 @@ async function planTake(dir, opts = {}) {
5831
6043
  if (prev.objects?.length) doc.objects = prev.objects;
5832
6044
  if (prev.audio.length) doc.audio = prev.audio;
5833
6045
  if (prev.camMotion?.length) doc.camMotion = prev.camMotion;
5834
- if (prev.endCard) doc.endCard = structuredClone(prev.endCard);
5835
6046
  await writeJson(take.paths.doc, doc, true);
5836
6047
  return {
5837
6048
  doc,
@@ -8529,7 +8740,7 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
8529
8740
  Take pipeline
8530
8741
  vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
8531
8742
  vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
8532
- 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]
8743
+ 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]
8533
8744
  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]
8534
8745
  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]
8535
8746
  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]
@@ -8658,19 +8869,27 @@ made with plan --style <poster>, which copies a poster's layout onto a
8658
8869
  take (its card placement, its stage clips with the release's words
8659
8870
  patched in from LAUNCH.md's headline and kicker roles or --headline and
8660
8871
  --kicker, its rest lean, its hold), or by hand in doc.json.
8661
- The cut's MOTION is the document's too. plan proposes it on a fresh plan
8662
- (--motion re-proposes onto an existing doc.json, replacing only its own
8663
- proposals): the card's ENTRANCE (tilt-in by default: the card swings in
8664
- from a perspective pose and settles), the END CARD (the last frame holds
8665
- 2.5 s while the card recedes and the headline, the release line and the
8666
- wordmark rise; the brand's mark from BRAND.md logoUrl above them), a
8667
- CAPTION per actions.json step at the step's moment, a music BED from
8668
- LAUNCH.md's music role (a catalog slug or a mood) and a click sound on
8669
- every press when the take has no mic. LAUNCH.md's entrance, endCard,
8670
- captions, music and clicks roles, or the flags, change or switch each
8671
- off; a deleted proposal stays deleted on a refresh. deliver applies each
8672
- destination's MECHANICS and nothing more: the README loop plays no
8673
- entrance, end card or sound; a channel that autoplays muted drops the
8872
+ The cut's MOTION is the document's too, in ONE vocabulary: every visual
8873
+ thing (the card, a text, image or video clip, a prop) carries anim.enter,
8874
+ anim.exit and anim.idle; the output lasts until the last clip ends, and
8875
+ past its footage the card holds its last frame at the pose its exit
8876
+ settled into. There is no end-card field and no entrance field: a
8877
+ COMPONENT is a TEMPLATE, a plain take on a shelf whose clips carry stable
8878
+ ids, laid onto the take at an anchor (LAUNCH.md with: <ref>[@end|@start|
8879
+ @step:<id>|@<s>], comma list; --with the same, repeatable; endCard: <ref>
8880
+ names one at the end), its clips stamped with where they came from
8881
+ (from). plan proposes on a fresh plan (--motion re-proposes, replacing
8882
+ only its own work): the card's ENTER (tilt-in by default), the templates
8883
+ named, the house END CARD when endCard is on or absent (clips after the
8884
+ footage: the headline, the release line, the wordmark, the mark from
8885
+ BRAND.md logoUrl, over a card that recedes; from: endcard), a CAPTION per
8886
+ actions.json step at the step's moment, a music BED from LAUNCH.md's music
8887
+ role (a catalog slug or a mood) and a click sound on every press when the
8888
+ take has no mic. LAUNCH.md's entrance, endCard, captions, music and clicks
8889
+ roles, or the flags, change or switch each off; a deleted proposal stays
8890
+ deleted on a refresh. deliver applies each destination's MECHANICS and
8891
+ nothing more: the README loop drops the card's motion, every clip a
8892
+ template placed and every sound; a channel that autoplays muted drops the
8674
8893
  bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
8675
8894
  the camera. Screenshot-genre
8676
8895
  destinations (CWS screenshots, the PH gallery) are the real page at that
@@ -8981,11 +9200,15 @@ async function cmdCreate2(argv) {
8981
9200
  }
8982
9201
  }
8983
9202
  async function cmdPlan(argv) {
8984
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
9203
+ const { positionals, flags, multi } = parseArgs(
9204
+ argv,
9205
+ BOOLEAN_FLAGS5,
9206
+ /* @__PURE__ */ new Set(["with"])
9207
+ );
8985
9208
  const dir = positionals[0];
8986
9209
  if (!dir)
8987
9210
  throw new UsageError(
8988
- 'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
9211
+ '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]'
8989
9212
  );
8990
9213
  const r = createReporter(flags.json === true);
8991
9214
  if (flags.fresh === true) {
@@ -9008,7 +9231,7 @@ async function cmdPlan(argv) {
9008
9231
  const style = await resolveStyleRef(flags);
9009
9232
  const hasDoc = existsSync14(join17(dir, "doc.json"));
9010
9233
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
9011
- const release = await releaseInputs(dir, flags, r);
9234
+ const release = await releaseInputs(dir, flags, r, multi.with ?? []);
9012
9235
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
9013
9236
  const s = await planTake(dir, {
9014
9237
  ...style ? { style } : {},
@@ -9024,6 +9247,7 @@ async function cmdPlan(argv) {
9024
9247
  mark: release.mark,
9025
9248
  captions: release.captions,
9026
9249
  catalog: release.catalog,
9250
+ templates: release.templates,
9027
9251
  again: flags.motion === true
9028
9252
  }
9029
9253
  } : {}
@@ -9060,7 +9284,7 @@ async function cmdPlan(argv) {
9060
9284
  );
9061
9285
  return EXIT_OK;
9062
9286
  }
9063
- async function releaseInputs(dir, flags, r) {
9287
+ async function releaseInputs(dir, flags, r, withRefs = []) {
9064
9288
  let lookPick;
9065
9289
  try {
9066
9290
  lookPick = await resolveLook(dir, {
@@ -9115,13 +9339,36 @@ async function releaseInputs(dir, flags, r) {
9115
9339
  const step = st;
9116
9340
  return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
9117
9341
  });
9342
+ const refs = [];
9343
+ const parseRef = (raw, fallback) => {
9344
+ const m = /^(.*?)(?:@(end|start|step:[^@\s]+|\d+(?:\.\d+)?))?$/.exec(
9345
+ raw.trim()
9346
+ );
9347
+ const ref = m?.[1]?.trim() ?? raw.trim();
9348
+ const anchor = m?.[2];
9349
+ const at2 = !anchor ? fallback : anchor === "end" || anchor === "start" ? anchor : anchor.startsWith("step:") ? { step: anchor.slice(5) } : Number(anchor);
9350
+ if (ref) refs.push({ ref, at: at2 });
9351
+ };
9352
+ for (const raw of withRefs) parseRef(raw, "end");
9353
+ for (const raw of (launchRoles.with ?? "").split(","))
9354
+ if (raw.trim()) parseRef(raw, "end");
9355
+ const endCardRole = launchRoles.endCard;
9356
+ if (endCardRole && !/^(none|off|no|false|on|yes|true)$/i.test(endCardRole.trim()))
9357
+ parseRef(endCardRole, "end");
9358
+ const templates = [];
9359
+ for (const { ref, at: at2 } of refs) {
9360
+ const found = await resolveDocRef(ref, flags, "--with");
9361
+ templates.push({ from: found.from, doc: found.doc, at: at2 });
9362
+ r.log(`template: ${found.from}`);
9363
+ }
9118
9364
  return {
9119
9365
  words,
9120
9366
  launchRoles,
9121
9367
  ink: endCardInk(lookPick.look, lookPick.roles),
9122
9368
  mark,
9123
9369
  captions,
9124
- catalog
9370
+ catalog,
9371
+ templates
9125
9372
  };
9126
9373
  }
9127
9374
  async function cmdRender(argv) {
@@ -9216,7 +9463,7 @@ async function cmdFrames(argv) {
9216
9463
  if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
9217
9464
  size = { width: Number(m[1]), height: Number(m[2]) };
9218
9465
  }
9219
- const duration = totalDuration2(ratedSegments6(take.doc));
9466
+ const duration = docOutputDuration3(take.doc);
9220
9467
  const frameRaw = strFlag(flags, "frame");
9221
9468
  const timesRaw = strFlag(flags, "times");
9222
9469
  const atZooms = flags["at-zooms"] === true;
@@ -9303,7 +9550,7 @@ async function cmdDeliver(argv) {
9303
9550
  }
9304
9551
  const take = await loadTake(dir);
9305
9552
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
9306
- const duration = totalDuration2(ratedSegments6(take.doc));
9553
+ const duration = docOutputDuration3(take.doc);
9307
9554
  const timesRaw = strFlag(flags, "times");
9308
9555
  let times;
9309
9556
  if (timesRaw !== void 0) {
@@ -9381,6 +9628,10 @@ Store uploads stay manual: hand the human this directory and the manifest.`
9381
9628
  async function resolveStyleRef(flags) {
9382
9629
  const styleRef = strFlag(flags, "style");
9383
9630
  if (!styleRef) return null;
9631
+ return resolveDocRef(styleRef, flags, "--style");
9632
+ }
9633
+ async function resolveDocRef(ref, flags, what) {
9634
+ const styleRef = ref;
9384
9635
  const file = existsSync14(styleRef) ? resolve9(
9385
9636
  styleRef,
9386
9637
  existsSync14(join17(styleRef, "doc.json")) ? "doc.json" : ""
@@ -9388,7 +9639,9 @@ async function resolveStyleRef(flags) {
9388
9639
  if (file && existsSync14(file)) {
9389
9640
  return {
9390
9641
  from: file,
9391
- doc: JSON.parse(await readFile14(file, "utf8"))
9642
+ doc: migrateHostedDoc5(
9643
+ JSON.parse(await readFile14(file, "utf8"))
9644
+ )
9392
9645
  };
9393
9646
  }
9394
9647
  const origin = platformOrigin({
@@ -9400,7 +9653,7 @@ async function resolveStyleRef(flags) {
9400
9653
  const head = meta.body.vos?.currentVersionId;
9401
9654
  if (meta.status !== 200 || !head)
9402
9655
  throw new UsageError(
9403
- `--style: ${styleRef} is neither a doc.json nor a vos I can read`
9656
+ `${what}: ${styleRef} is neither a doc.json nor a vos I can read`
9404
9657
  );
9405
9658
  const doc = await apiJson(
9406
9659
  origin,
@@ -9408,7 +9661,7 @@ async function resolveStyleRef(flags) {
9408
9661
  { key }
9409
9662
  );
9410
9663
  if (doc.status !== 200)
9411
- throw new UsageError(`--style: ${styleRef} carries no doc (is it a take?)`);
9664
+ throw new UsageError(`${what}: ${styleRef} carries no doc (is it a take?)`);
9412
9665
  return {
9413
9666
  from: `${origin}/vos/${styleRef}`,
9414
9667
  doc: migrateHostedDoc5(doc.body)
@@ -9832,4 +10085,4 @@ export {
9832
10085
  convertAgentBrowser,
9833
10086
  run
9834
10087
  };
9835
- //# sourceMappingURL=chunk-HYUVNBEM.js.map
10088
+ //# sourceMappingURL=chunk-ZONOZZZY.js.map