@vosjs/cli 0.77.0 → 0.78.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,12 +9,14 @@ import {
9
9
  reencodeStill,
10
10
  renderStill,
11
11
  videoFormat
12
- } from "./chunk-KDIRW72G.js";
12
+ } from "./chunk-JYCW4EOA.js";
13
13
  import {
14
14
  PREV_DOC_NAME,
15
15
  RECORDING_NAME,
16
16
  docLayerRefs,
17
17
  docMediaRefs,
18
+ docObjectRefs,
19
+ docOverlayRefs,
18
20
  dropScreencastFrames,
19
21
  ensureTakeDir,
20
22
  fileSha256,
@@ -37,7 +39,7 @@ import {
37
39
  uploadDocRefs,
38
40
  uploadManifest,
39
41
  writeJson
40
- } from "./chunk-UY2N42P2.js";
42
+ } from "./chunk-ZSJEISKL.js";
41
43
  import {
42
44
  CREDENTIALS_PATH,
43
45
  MEDIA_HEAD_BYTES,
@@ -62,6 +64,7 @@ import {
62
64
  programAssets,
63
65
  promptHint,
64
66
  readSyncState,
67
+ refuseSymlink,
65
68
  requireCredential,
66
69
  resolveCredential,
67
70
  resolveMediaType,
@@ -133,12 +136,12 @@ function landedLines(l) {
133
136
  }
134
137
 
135
138
  // src/plugin/run.ts
136
- import { mkdir as mkdir8, readFile as readFile13, rm as rm2 } from "fs/promises";
137
- import { existsSync as existsSync23 } from "fs";
138
- import { join as join25, resolve as resolve14 } from "path";
139
+ import { mkdir as mkdir13, readFile as readFile15, rm as rm5 } from "fs/promises";
140
+ import { existsSync as existsSync27 } from "fs";
141
+ import { join as join29, resolve as resolve18 } from "path";
139
142
  import {
140
143
  END_CARD_FROM as END_CARD_FROM2,
141
- docOutputDuration as docOutputDuration3,
144
+ docOutputDuration as docOutputDuration4,
142
145
  migrateHostedDoc as migrateHostedDoc4
143
146
  } from "@vosjs/studio-core";
144
147
 
@@ -215,7 +218,10 @@ import {
215
218
  docCardLayout,
216
219
  docZoomTrack,
217
220
  findStep,
221
+ formatElapsed,
222
+ hasElapsed,
218
223
  outputEnd,
224
+ resolveAnchor,
219
225
  pinBox,
220
226
  pinCandidates,
221
227
  pinRectOnScreen,
@@ -451,6 +457,7 @@ function lintDoc(docIn) {
451
457
  const doc = docIn;
452
458
  const problems = [];
453
459
  const warnings = [];
460
+ const notes = [];
454
461
  const recording = isObj(doc.source);
455
462
  const source = isObj(doc.source) ? doc.source : {};
456
463
  const meta = isObj(source.meta) ? source.meta : {};
@@ -1047,6 +1054,15 @@ function lintDoc(docIn) {
1047
1054
  footageEnd = null;
1048
1055
  }
1049
1056
  }
1057
+ let anchorPieces = null;
1058
+ if (recording) {
1059
+ try {
1060
+ anchorPieces = ratedSegments(doc);
1061
+ } catch {
1062
+ anchorPieces = null;
1063
+ }
1064
+ }
1065
+ const captionTimes = [];
1050
1066
  for (let i = 0; i < (overlays?.length ?? 0); i++) {
1051
1067
  const o = overlays[i];
1052
1068
  const name = `overlays[${i}]`;
@@ -1054,6 +1070,24 @@ function lintDoc(docIn) {
1054
1070
  problems.push(`${name} must be an object`);
1055
1071
  continue;
1056
1072
  }
1073
+ const playsAt = checkOverlayAnchor(
1074
+ o,
1075
+ name,
1076
+ docIn,
1077
+ recording,
1078
+ anchorPieces,
1079
+ problems,
1080
+ warnings,
1081
+ notes
1082
+ );
1083
+ if (o.kind === "text" && o.preset === "caption" && o.pin === void 0 && isNum(playsAt) && isNum(o.duration) && o.duration > 0) {
1084
+ const label = typeof o.id === "string" ? `${name} (${o.id})` : name;
1085
+ captionTimes.push({
1086
+ name: label,
1087
+ start: playsAt,
1088
+ end: playsAt + o.duration
1089
+ });
1090
+ }
1057
1091
  if (o.kind !== "text" && o.kind !== "image" && o.kind !== "video" && o.kind !== "html") {
1058
1092
  problems.push(
1059
1093
  `${name}.kind must be "text" | "image" | "video" | "html" (got ${String(o.kind)})`
@@ -1064,14 +1098,14 @@ function lintDoc(docIn) {
1064
1098
  if (!isNum(o.duration) || o.duration <= 0) {
1065
1099
  problems.push(`${name}.duration must be > 0 (seconds)`);
1066
1100
  }
1067
- if (footageEnd !== null && footageEnd > 0 && isNum(o.start) && isNum(o.duration)) {
1068
- const over = o.start + o.duration - footageEnd;
1101
+ if (footageEnd !== null && footageEnd > 0 && isNum(playsAt) && isNum(o.duration)) {
1102
+ const over = playsAt + o.duration - footageEnd;
1069
1103
  const frameAnim = isObj(docIn) && isObj(docIn.frame) && isObj(docIn.frame.anim) ? docIn.frame.anim : null;
1070
1104
  const exit = frameAnim ? isObj(frameAnim.exit) ? frameAnim.exit.kind : frameAnim.exit : void 0;
1071
1105
  const cardLeaves = exit !== void 0 && exit !== "none";
1072
1106
  if (over > 0.05 && !cardLeaves) {
1073
1107
  warnings.push(
1074
- o.start >= footageEnd ? `${name} starts at ${o.start.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays over the card's held last frame and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the card holds its last frame under it. Shorten it, put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')`
1108
+ playsAt >= footageEnd ? `${name} starts at ${playsAt.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays over the card's held last frame and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the card holds its last frame under it. Shorten it, put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')`
1075
1109
  );
1076
1110
  }
1077
1111
  }
@@ -1616,6 +1650,17 @@ function lintDoc(docIn) {
1616
1650
  }
1617
1651
  }
1618
1652
  }
1653
+ captionTimes.sort((a, b) => a.start - b.start || a.end - b.end);
1654
+ for (let i = 0; i < captionTimes.length; i++) {
1655
+ for (let j = i + 1; j < captionTimes.length; j++) {
1656
+ const a = captionTimes[i];
1657
+ const b = captionTimes[j];
1658
+ if (b.start >= a.end - EPS) break;
1659
+ warnings.push(
1660
+ `${a.name} (${a.start.toFixed(2)}\u2013${a.end.toFixed(2)}s) and ${b.name} (${b.start.toFixed(2)}\u2013${b.end.toFixed(2)}s) are captions on screen at the same time; shorten the first, move one (an anchor's offset), or drop one`
1661
+ );
1662
+ }
1663
+ }
1619
1664
  if (isObj(doc.export)) {
1620
1665
  const resolution = doc.export.resolution;
1621
1666
  const validResolution = EXPORT_RESOLUTION_OPTIONS.includes(resolution);
@@ -1643,7 +1688,66 @@ function lintDoc(docIn) {
1643
1688
  }
1644
1689
  if (recording)
1645
1690
  framingWarnings(docIn, doc, zoom, overlays ?? [], warnings);
1646
- return { problems, warnings };
1691
+ return { problems, warnings, ...notes.length ? { notes } : {} };
1692
+ }
1693
+ function checkOverlayAnchor(o, name, docIn, recording, pieces, problems, warnings, notes) {
1694
+ const fallback = isNum(o.start) ? o.start : null;
1695
+ const words2 = o.kind === "text" ? o.text : void 0;
1696
+ const elapsed = o.kind === "text" && hasElapsed(words2);
1697
+ if (o.anchor === void 0) {
1698
+ if (elapsed)
1699
+ warnings.push(
1700
+ `${name}: its words carry {elapsed}, which fills only from an anchored step's off-camera wait; anchor it (anchor: { step }) or write the duration`
1701
+ );
1702
+ return fallback;
1703
+ }
1704
+ const a = o.anchor;
1705
+ if (!isObj(a)) {
1706
+ problems.push(`${name}.anchor must be { step, media?, at?, offset? }`);
1707
+ return fallback;
1708
+ }
1709
+ let shaped = true;
1710
+ if (typeof a.step !== "string" && !(isNum(a.step) && Number.isInteger(a.step) && a.step >= 0)) {
1711
+ problems.push(
1712
+ `${name}.anchor.step must be a step id (string) or its index (integer \u2265 0)`
1713
+ );
1714
+ shaped = false;
1715
+ }
1716
+ if (a.media !== void 0 && typeof a.media !== "string") {
1717
+ problems.push(`${name}.anchor.media must be a media[].id (string)`);
1718
+ shaped = false;
1719
+ }
1720
+ if (a.at !== void 0 && a.at !== "start" && a.at !== "end") {
1721
+ problems.push(`${name}.anchor.at must be "start" or "end"`);
1722
+ shaped = false;
1723
+ }
1724
+ if (a.offset !== void 0 && !isNum(a.offset)) {
1725
+ problems.push(`${name}.anchor.offset must be a number (OUTPUT seconds)`);
1726
+ shaped = false;
1727
+ }
1728
+ if (!shaped || !recording || !pieces) return fallback;
1729
+ const r = resolveAnchor(
1730
+ docIn,
1731
+ a,
1732
+ pieces
1733
+ );
1734
+ const step = `step ${JSON.stringify(a.step)}${typeof a.media === "string" ? ` of media ${a.media}` : ""}`;
1735
+ const at2 = fallback === null ? "nowhere" : `${fallback.toFixed(2)}s`;
1736
+ if (r.start === null) {
1737
+ const why = r.miss === "media" ? `media "${String(a.media)}" is not one of media[].id` : r.miss === "step" ? `${step} is not in its meta.steps` : r.miss === "skipped" ? `${step} was skipped by the recorder` : `the cut leaves ${step} out`;
1738
+ warnings.push(
1739
+ `${name}.anchor: ${why}, so it plays at its fallback start (${at2}); re-anchor it, or keep the footage that step plays in`
1740
+ );
1741
+ }
1742
+ if (elapsed && r.elapsedFrom && r.elapsedMs !== void 0)
1743
+ notes.push(
1744
+ `${name}: {elapsed} reads the off-camera wait of step ${JSON.stringify(r.elapsedFrom.id ?? r.elapsedFrom.step)} (${formatElapsed(r.elapsedMs)}), the nearest one before ${step}, which waited for nothing itself`
1745
+ );
1746
+ if (elapsed && r.elapsedMs === void 0 && r.miss !== "media" && r.miss !== "step")
1747
+ warnings.push(
1748
+ `${name}: {elapsed} stays as written, because neither ${step} nor any step before it recorded an off-camera wait (no meta.skips entry names one, and none has an offCameraMs)`
1749
+ );
1750
+ return r.start ?? fallback;
1647
1751
  }
1648
1752
  function clicksWithRects(source) {
1649
1753
  const cursor = Array.isArray(source.cursor) ? source.cursor : [];
@@ -1759,15 +1863,33 @@ function checkPin(o, name, docIn, recording, problems, warnings) {
1759
1863
  }
1760
1864
  function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1761
1865
  try {
1762
- const source = isObj(doc.source) ? doc.source : {};
1763
- const meta = isObj(source.meta) ? source.meta : {};
1764
- const w = isNum(meta.width) ? meta.width : 0;
1765
- const h = isNum(meta.height) ? meta.height : 0;
1766
- if (!(w > 0) || !(h > 0)) return;
1767
- const clicks = clicksWithRects(source);
1768
- if (!clicks.length) return;
1866
+ const evidence = /* @__PURE__ */ new Map();
1867
+ const others = Array.isArray(doc.media) ? doc.media : [];
1868
+ for (const [key, src] of [
1869
+ ["", doc.source],
1870
+ ...others.map((m) => [isObj(m) ? m.id : void 0, m])
1871
+ ]) {
1872
+ if (typeof key !== "string" || !isObj(src)) continue;
1873
+ const meta = isObj(src.meta) ? src.meta : {};
1874
+ const w = isNum(meta.width) ? meta.width : 0;
1875
+ const h = isNum(meta.height) ? meta.height : 0;
1876
+ if (!(w > 0) || !(h > 0)) continue;
1877
+ const clicks = clicksWithRects(src);
1878
+ if (!clicks.length) continue;
1879
+ let layout2;
1880
+ try {
1881
+ layout2 = docCardLayout(docIn, key || null);
1882
+ } catch {
1883
+ continue;
1884
+ }
1885
+ evidence.set(key, { w, h, clicks, layout: layout2 });
1886
+ }
1887
+ if (!evidence.size) return;
1769
1888
  const layout = docCardLayout(docIn);
1770
1889
  for (const z of zoom) {
1890
+ const ev = evidence.get(typeof z.media === "string" ? z.media : "");
1891
+ if (!ev) continue;
1892
+ const { w, h, clicks } = ev;
1771
1893
  if (!isNum(z.in) || !isNum(z.out) || !isNum(z.level) || !isNum(z.cx) || !isNum(z.cy) || z.level <= 1.001)
1772
1894
  continue;
1773
1895
  const zin = z.in;
@@ -1794,7 +1916,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1794
1916
  focusMode: z.focusMode
1795
1917
  },
1796
1918
  rect,
1797
- layout,
1919
+ ev.layout,
1798
1920
  0.02,
1799
1921
  cameraModel(docIn.frame)
1800
1922
  )) {
@@ -1897,19 +2019,22 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1897
2019
  } catch {
1898
2020
  return;
1899
2021
  }
1900
- for (const c of clicks) {
1901
- const out = spanOutputExtent(rated, c.t, c.t + 1e-3);
2022
+ const presses = [...evidence.entries()].flatMap(
2023
+ ([key, ev]) => ev.clicks.map((c) => ({ c, key, ev }))
2024
+ );
2025
+ for (const { c, key, ev } of presses) {
2026
+ const out = spanOutputExtent(rated, c.t, c.t + 1e-3, key || void 0);
1902
2027
  if (!out || out.start < o.start || out.start > o.start + o.duration)
1903
2028
  continue;
1904
2029
  const target = pinRectOnScreen(
1905
2030
  {
1906
- x: c.rect.x / w,
1907
- y: c.rect.y / h,
1908
- w: c.rect.w / w,
1909
- h: c.rect.h / h
2031
+ x: c.rect.x / ev.w,
2032
+ y: c.rect.y / ev.h,
2033
+ w: c.rect.w / ev.w,
2034
+ h: c.rect.h / ev.h
1910
2035
  },
1911
2036
  out.start,
1912
- layout,
2037
+ ev.layout,
1913
2038
  camera,
1914
2039
  zoomTrack
1915
2040
  );
@@ -2017,7 +2142,7 @@ function mediaProbeLints(results) {
2017
2142
  }
2018
2143
 
2019
2144
  // src/plugin/framesTake.ts
2020
- import { mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
2145
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2021
2146
  import { existsSync as existsSync3 } from "fs";
2022
2147
  import { join as join3, resolve as resolve3 } from "path";
2023
2148
  import { compileVosConfig } from "@vosjs/core";
@@ -2204,9 +2329,11 @@ import { readFile, rm } from "fs/promises";
2204
2329
  import { tweenRuntimeCode } from "@vosjs/tween/bundle";
2205
2330
  import { elementsBundleCode } from "@vosjs/elements/bundle";
2206
2331
  import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
2207
- var MEDIABUNNY_URL = "https://esm.sh/mediabunny@1.55.7?target=es2022";
2208
- var THREE_URL = "https://esm.sh/three@0.183.0?target=es2022";
2209
- var THREE_ADDONS = "https://esm.sh/three@0.183.0&target=es2022/examples/jsm/";
2332
+ import { esmShModules } from "@vosjs/core/addons";
2333
+ var MODULES = esmShModules();
2334
+ var MEDIABUNNY_URL = MODULES.mediabunny;
2335
+ var THREE_URL = MODULES.three;
2336
+ var THREE_ADDONS = MODULES.threeAddons;
2210
2337
  function renderPageHtml() {
2211
2338
  const importmap = JSON.stringify({
2212
2339
  imports: { three: THREE_URL, "three/addons/": THREE_ADDONS }
@@ -3277,6 +3404,55 @@ var VIDEO_TOKEN = "__VOS_CLI_VIDEO__";
3277
3404
  function stillSupersample(w, h) {
3278
3405
  return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
3279
3406
  }
3407
+ function zoomApexes(doc) {
3408
+ const rated = ratedSegments2(doc);
3409
+ const media = new Set((doc.media ?? []).map((m) => m.id));
3410
+ const apexes = [];
3411
+ const skipped = [];
3412
+ for (const z of doc.zoom) {
3413
+ const of = z.media ? { media: z.media } : {};
3414
+ if (z.media && !media.has(z.media)) {
3415
+ skipped.push({
3416
+ id: z.id,
3417
+ ...of,
3418
+ in: z.in,
3419
+ out: z.out,
3420
+ reason: `names media "${z.media}", which the take does not carry`
3421
+ });
3422
+ continue;
3423
+ }
3424
+ const ext = spanOutputExtent2(rated, z.in, z.out, z.media);
3425
+ if (!ext) {
3426
+ skipped.push({
3427
+ id: z.id,
3428
+ ...of,
3429
+ in: z.in,
3430
+ out: z.out,
3431
+ reason: "the cut keeps none of it, so it never plays"
3432
+ });
3433
+ continue;
3434
+ }
3435
+ apexes.push({
3436
+ id: z.id,
3437
+ ...of,
3438
+ time: (ext.start + ext.end) / 2,
3439
+ start: ext.start,
3440
+ end: ext.end
3441
+ });
3442
+ }
3443
+ return { apexes, skipped };
3444
+ }
3445
+ var ZOOM_UNRENDERED_BELOW = 4e-3;
3446
+ var SIGNATURE_LONG_SIDE = 96;
3447
+ function signatureDifference(a, b) {
3448
+ if (a.length !== b.length || a.length === 0) return 1;
3449
+ let sum = 0;
3450
+ for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
3451
+ return sum / a.length / 255;
3452
+ }
3453
+ function unrenderedZoomLine(f) {
3454
+ return `zoom ${f.zoomId ?? "?"} at ${f.time.toFixed(2)}s did not render (its frame matches the zoom-off frame${f.twin ? `: ${f.twin}` : ""})`;
3455
+ }
3280
3456
  async function stillsInPage(opts) {
3281
3457
  const w = window;
3282
3458
  const RW = opts.W * opts.ss;
@@ -3285,11 +3461,9 @@ async function stillsInPage(opts) {
3285
3461
  const vblob = await (await fetch(opts.videoUrl)).blob();
3286
3462
  const vurl = URL.createObjectURL(vblob);
3287
3463
  const code = opts.animationCode.split(opts.token).join(vurl);
3288
- const mod = await import(
3289
- /* @vite-ignore */
3290
- URL.createObjectURL(
3291
- new Blob([code], { type: "text/javascript" })
3292
- )
3464
+ const load = new Function("u", "return import(u)");
3465
+ const mod = await load(
3466
+ URL.createObjectURL(new Blob([code], { type: "text/javascript" }))
3293
3467
  );
3294
3468
  w.__vos__ = w.__vos__ || {};
3295
3469
  w.__vos__.isPaused = true;
@@ -3365,6 +3539,25 @@ async function stillsInPage(opts) {
3365
3539
  }
3366
3540
  }
3367
3541
  };
3542
+ const sigCanvas = document.createElement("canvas");
3543
+ sigCanvas.width = opts.sigW;
3544
+ sigCanvas.height = opts.sigH;
3545
+ const signature = () => {
3546
+ const g = sigCanvas.getContext("2d", { willReadFrequently: true });
3547
+ g.imageSmoothingEnabled = true;
3548
+ g.imageSmoothingQuality = "high";
3549
+ g.clearRect(0, 0, opts.sigW, opts.sigH);
3550
+ g.drawImage(canvas, 0, 0, opts.sigW, opts.sigH);
3551
+ const px = g.getImageData(0, 0, opts.sigW, opts.sigH).data;
3552
+ const out2 = new Array(opts.sigW * opts.sigH);
3553
+ for (let p = 0, q = 0; p < px.length; p += 4, q++)
3554
+ out2[q] = Math.round(
3555
+ 0.299 * px[p] + 0.587 * px[p + 1] + 0.114 * px[p + 2]
3556
+ );
3557
+ return out2;
3558
+ };
3559
+ const sigWanted = new Set(opts.sigNames);
3560
+ const sigs = {};
3368
3561
  for (let i = 0; i < opts.shots.length; i++) {
3369
3562
  const shot = opts.shots[i];
3370
3563
  timeline.seek(shot.time, false);
@@ -3376,14 +3569,56 @@ async function stillsInPage(opts) {
3376
3569
  }
3377
3570
  await waitVideosSettled(8e3);
3378
3571
  const png = await toPng();
3572
+ if (sigWanted.has(shot.name)) sigs[shot.name] = signature();
3379
3573
  await fetch("/save?name=" + shot.name, { method: "POST", body: png });
3380
3574
  w.__progress = (i + 1) / opts.shots.length;
3381
3575
  }
3382
- w.__done = { ok: true, count: opts.shots.length };
3576
+ w.__done = { ok: true, count: opts.shots.length, sigs };
3383
3577
  } catch (e) {
3384
3578
  w.__error = String(e instanceof Error && e.stack || e);
3385
3579
  }
3386
3580
  }
3581
+ async function captureShots(context2, base, opts) {
3582
+ const long = Math.max(opts.width, opts.height);
3583
+ const side = (n) => Math.max(1, Math.round(n / long * SIGNATURE_LONG_SIDE));
3584
+ const page = await context2.newPage();
3585
+ try {
3586
+ page.on("console", (m) => {
3587
+ if (m.type() === "error")
3588
+ process.stderr.write(` [stills page] ${m.text()}
3589
+ `);
3590
+ });
3591
+ await page.addInitScript(() => {
3592
+ ;
3593
+ globalThis.__name = (f) => f;
3594
+ });
3595
+ await page.goto(`${base}/render.html`);
3596
+ await page.waitForFunction("window.__pageReady__ === true");
3597
+ void page.evaluate(stillsInPage, {
3598
+ animationCode: opts.animationCode,
3599
+ token: VIDEO_TOKEN,
3600
+ videoUrl: opts.videoUrl,
3601
+ W: opts.width,
3602
+ H: opts.height,
3603
+ ss: opts.ss,
3604
+ shots: opts.shots,
3605
+ sigNames: opts.sigNames,
3606
+ sigW: side(opts.width),
3607
+ sigH: side(opts.height)
3608
+ }).catch(() => {
3609
+ });
3610
+ const done = await waitForPageDone(
3611
+ page,
3612
+ "stills",
3613
+ () => {
3614
+ },
3615
+ 12e4 + opts.shots.length * 4e3
3616
+ );
3617
+ return done.sigs ?? {};
3618
+ } finally {
3619
+ await page.close();
3620
+ }
3621
+ }
3387
3622
  async function framesTake(browser, dir, opts) {
3388
3623
  const take = await loadTake(dir);
3389
3624
  const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
@@ -3406,13 +3641,17 @@ async function framesTake(browser, dir, opts) {
3406
3641
  time: clamp(t),
3407
3642
  kind: "time"
3408
3643
  }));
3644
+ let skippedZooms;
3409
3645
  if (opts.atZooms) {
3410
- const rated = ratedSegments2(doc);
3411
- for (const z of doc.zoom) {
3412
- const ext = spanOutputExtent2(rated, z.in, z.out);
3413
- if (ext)
3414
- shots.push({ time: clamp((ext.start + ext.end) / 2), kind: "zoom" });
3415
- }
3646
+ const { apexes, skipped } = zoomApexes(doc);
3647
+ for (const a of apexes)
3648
+ shots.push({
3649
+ time: clamp(a.time),
3650
+ kind: "zoom",
3651
+ zoomId: a.id,
3652
+ ...a.media ? { media: a.media } : {}
3653
+ });
3654
+ skippedZooms = skipped;
3416
3655
  }
3417
3656
  if (opts.atMoments) {
3418
3657
  for (const m of await momentsFor(dir, doc)) {
@@ -3423,16 +3662,19 @@ async function framesTake(browser, dir, opts) {
3423
3662
  if (opts.atStill)
3424
3663
  shots.push({ time: clamp(docStillTime(doc)), kind: "still" });
3425
3664
  shots.sort((a, b) => a.time - b.time);
3426
- if (!shots.length)
3665
+ if (!shots.length && !skippedZooms?.length)
3427
3666
  throw new Error(
3428
3667
  "no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
3429
3668
  );
3430
3669
  const outDir = resolve3(opts.outDir ?? join3(dir, "stills"));
3431
3670
  await mkdir2(outDir, { recursive: true });
3671
+ const stamp = (i, t) => `${String(i).padStart(2, "0")}-${t.toFixed(2)}s.png`;
3432
3672
  const named = shots.map((s, i) => ({
3433
3673
  ...s,
3434
- name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${String(i).padStart(2, "0")}-${s.time.toFixed(2)}s.png`
3674
+ name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${stamp(i, s.time)}`,
3675
+ twinName: s.kind === "zoom" ? `zoomoff-${stamp(i, s.time)}` : void 0
3435
3676
  }));
3677
+ const zoomShots = named.filter((n) => n.kind === "zoom");
3436
3678
  const server = await startTakeServer(dir, {
3437
3679
  "/render.html": renderPageHtml()
3438
3680
  });
@@ -3440,49 +3682,73 @@ async function framesTake(browser, dir, opts) {
3440
3682
  const context2 = await browser.newContext({
3441
3683
  viewport: { width: width * ss, height: height * ss }
3442
3684
  });
3685
+ const videoUrl = `/${take.paths.recordingName}`;
3443
3686
  try {
3444
- const page = await context2.newPage();
3445
- page.on("console", (m) => {
3446
- if (m.type() === "error")
3447
- process.stderr.write(` [stills page] ${m.text()}
3448
- `);
3449
- });
3450
- await page.addInitScript(() => {
3451
- ;
3452
- globalThis.__name = (f) => f;
3453
- });
3454
- await page.goto(`${server.base}/render.html`);
3455
- await page.waitForFunction("window.__pageReady__ === true");
3456
- void page.evaluate(stillsInPage, {
3687
+ const sigs = named.length ? await captureShots(context2, server.base, {
3457
3688
  animationCode,
3458
- token: VIDEO_TOKEN,
3459
- videoUrl: `/${take.paths.recordingName}`,
3460
- W: width,
3461
- H: height,
3689
+ videoUrl,
3690
+ width,
3691
+ height,
3462
3692
  ss,
3463
- shots: named.map(({ time, name }) => ({ time, name }))
3464
- }).catch(() => {
3465
- });
3466
- await waitForPageDone(
3467
- page,
3468
- "stills",
3469
- () => {
3470
- },
3471
- 12e4 + named.length * 4e3
3472
- );
3693
+ shots: named.map(({ time, name }) => ({ time, name })),
3694
+ sigNames: zoomShots.map((n) => n.name)
3695
+ }) : {};
3696
+ let twinSigs = {};
3697
+ if (zoomShots.length) {
3698
+ const twinDoc = structuredClone(doc);
3699
+ applyDocOverrides(twinDoc, { set: ["zoom=[]"] });
3700
+ const twinLowered = lowerToComposition(twinDoc);
3701
+ twinSigs = await captureShots(context2, server.base, {
3702
+ animationCode: compileVosConfig(twinLowered.config, {
3703
+ tweenEngine: "vos"
3704
+ }),
3705
+ videoUrl,
3706
+ width,
3707
+ height,
3708
+ ss,
3709
+ shots: zoomShots.map((n) => ({ time: n.time, name: n.twinName })),
3710
+ sigNames: zoomShots.map((n) => n.twinName)
3711
+ });
3712
+ }
3473
3713
  const frames = [];
3474
3714
  for (const n of named) {
3475
3715
  const from = join3(dir, n.name);
3476
3716
  const to = join3(outDir, n.name);
3477
3717
  if (from !== to) await rename2(from, to);
3478
- frames.push({
3718
+ const frame = {
3479
3719
  file: to,
3480
3720
  time: n.time,
3481
3721
  kind: n.kind,
3482
- ...n.momentId ? { momentId: n.momentId } : {}
3483
- });
3722
+ ...n.momentId ? { momentId: n.momentId } : {},
3723
+ ...n.zoomId ? { zoomId: n.zoomId } : {},
3724
+ ...n.media ? { media: n.media } : {}
3725
+ };
3726
+ if (n.twinName) {
3727
+ const a = sigs[n.name];
3728
+ const b = twinSigs[n.twinName];
3729
+ const twinFrom = join3(dir, n.twinName);
3730
+ if (a && b) {
3731
+ const difference = signatureDifference(a, b);
3732
+ frame.rendered = difference >= ZOOM_UNRENDERED_BELOW;
3733
+ frame.difference = Math.round(difference * 1e5) / 1e5;
3734
+ if (!frame.rendered) {
3735
+ const twinTo = join3(outDir, n.twinName);
3736
+ if (twinFrom !== twinTo) await rename2(twinFrom, twinTo);
3737
+ frame.twin = twinTo;
3738
+ }
3739
+ }
3740
+ if (!frame.twin) await rm2(twinFrom, { force: true });
3741
+ }
3742
+ frames.push(frame);
3484
3743
  }
3485
- return { frames, width, height, duration, outDir };
3744
+ return {
3745
+ frames,
3746
+ width,
3747
+ height,
3748
+ duration,
3749
+ outDir,
3750
+ ...skippedZooms ? { skippedZooms } : {}
3751
+ };
3486
3752
  } finally {
3487
3753
  await context2.close();
3488
3754
  server.close();
@@ -3520,7 +3786,8 @@ async function writeIndexJson(result) {
3520
3786
  width: result.width,
3521
3787
  height: result.height,
3522
3788
  duration: result.duration,
3523
- frames: result.frames
3789
+ frames: result.frames,
3790
+ ...result.skippedZooms?.length ? { skippedZooms: result.skippedZooms } : {}
3524
3791
  },
3525
3792
  null,
3526
3793
  2
@@ -3668,8 +3935,8 @@ import {
3668
3935
  // src/plugin/moments.ts
3669
3936
  import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
3670
3937
  var STEP_SETTLE_SECONDS = 0.4;
3671
- function stepOutputTime(rated, step, settle) {
3672
- const src = step.tEnd + settle;
3938
+ function stepOutputTime(rated, step, settle2) {
3939
+ const src = step.tEnd + settle2;
3673
3940
  const ext = spanOutputExtent3(rated, src, src + 1e-3);
3674
3941
  if (!ext) return null;
3675
3942
  return ext.start;
@@ -3685,6 +3952,20 @@ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
3685
3952
  var BED_ID = "bed";
3686
3953
  var CLICK_ID_PREFIX = "click-";
3687
3954
  var CAPTION_ID_PREFIX = "caption-";
3955
+ function houseCaption(id, text, start, duration) {
3956
+ return {
3957
+ id,
3958
+ kind: "text",
3959
+ text,
3960
+ preset: "caption",
3961
+ start,
3962
+ duration,
3963
+ transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3964
+ anim: { enter: "rise", exit: "fade" },
3965
+ align: "center",
3966
+ box: { color: "rgba(17,17,17,0.72)" }
3967
+ };
3968
+ }
3688
3969
  var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
3689
3970
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
3690
3971
  var TRANSITION_CLIP_MIN = 1;
@@ -3765,7 +4046,7 @@ function templateWords(words2) {
3765
4046
  if (sub && sub !== headline) out["endcard-sub"] = sub;
3766
4047
  return out;
3767
4048
  }
3768
- function resolveAnchor(doc, at2) {
4049
+ function resolveAnchor2(doc, at2) {
3769
4050
  if (typeof at2 !== "object") return at2;
3770
4051
  const steps = doc.source.meta.steps ?? [];
3771
4052
  const step = steps.find((s) => s.id === at2.step || String(s.step) === at2.step);
@@ -3810,7 +4091,7 @@ function proposeMotion(input, opts) {
3810
4091
  const tWords = templateWords(words2);
3811
4092
  const keys = opts.mark ? { "stage-mark": opts.mark.key, "endcard-markimg": opts.mark.key } : void 0;
3812
4093
  for (const t of templates) {
3813
- const at2 = resolveAnchor(doc, t.at);
4094
+ const at2 = resolveAnchor2(doc, t.at);
3814
4095
  if (at2 === null) {
3815
4096
  skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
3816
4097
  continue;
@@ -3891,18 +4172,14 @@ function proposeMotion(input, opts) {
3891
4172
  if (!step || step.skipped) continue;
3892
4173
  const t = stepOutputTime(rated, step, 0.2);
3893
4174
  if (t === null || t < 0 || t > footage - 1) continue;
3894
- captionClips.push({
3895
- id: `${CAPTION_ID_PREFIX}${c.step}`,
3896
- kind: "text",
3897
- text: c.caption,
3898
- preset: "caption",
3899
- start: +t.toFixed(3),
3900
- duration: Math.min(3.5, Math.max(2.5, footage - t - 0.2)),
3901
- transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3902
- anim: { enter: "rise", exit: "fade" },
3903
- align: "center",
3904
- box: { color: "rgba(17,17,17,0.72)" }
3905
- });
4175
+ captionClips.push(
4176
+ houseCaption(
4177
+ `${CAPTION_ID_PREFIX}${c.step}`,
4178
+ c.caption,
4179
+ +t.toFixed(3),
4180
+ Math.min(3.5, Math.max(2.5, footage - t - 0.2))
4181
+ )
4182
+ );
3906
4183
  }
3907
4184
  if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
3908
4185
  }
@@ -5330,8 +5607,24 @@ async function validateKit(kitPath, opts = {}) {
5330
5607
 
5331
5608
  // src/plugin/recorder.ts
5332
5609
  import { writeFileSync } from "fs";
5610
+ import { mkdir as mkdir5 } from "fs/promises";
5333
5611
  import { join as join8 } from "path";
5334
- import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
5612
+ import {
5613
+ fileNameProblem,
5614
+ recordTake as recordTakeCore
5615
+ } from "@vosjs/render-core/record";
5616
+ function outputsSink(dir) {
5617
+ const outDir = join8(dir, "outputs");
5618
+ return {
5619
+ save: async (name, bytes) => {
5620
+ const problem = fileNameProblem(name);
5621
+ if (problem) throw new Error(`outputs/${name}: ${problem}`);
5622
+ await refuseSymlink(outDir);
5623
+ await mkdir5(outDir, { recursive: true });
5624
+ await writeFileSafe(join8(outDir, name), bytes);
5625
+ }
5626
+ };
5627
+ }
5335
5628
  async function recordTake(browser, url, actions, paths, log, opts = {}) {
5336
5629
  const dry = opts.dryRun === true;
5337
5630
  const result = await recordTakeCore(
@@ -5346,6 +5639,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5346
5639
  log,
5347
5640
  {
5348
5641
  ...opts,
5642
+ outputs: outputsSink(paths.dir),
5349
5643
  env: process.env,
5350
5644
  platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
5351
5645
  }
@@ -5359,6 +5653,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5359
5653
  return result;
5360
5654
  }
5361
5655
 
5656
+ // src/plugin/run.ts
5657
+ import {
5658
+ RecordRefusedError,
5659
+ guardVerdict,
5660
+ guardedSteps,
5661
+ hostedRefusals
5662
+ } from "@vosjs/render-core/record";
5663
+
5362
5664
  // src/plugin/wall.ts
5363
5665
  import {
5364
5666
  WALL_PROBE,
@@ -5453,10 +5755,10 @@ async function openSession(name, url, opts = { log: () => {
5453
5755
  opts.log(
5454
5756
  `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
5455
5757
  );
5456
- return new Promise((resolve15, reject) => {
5758
+ return new Promise((resolve19, reject) => {
5457
5759
  const child = spawn(exe, args, { stdio: "ignore" });
5458
5760
  child.on("error", reject);
5459
- child.on("exit", (code) => resolve15({ code }));
5761
+ child.on("exit", (code) => resolve19({ code }));
5460
5762
  });
5461
5763
  }
5462
5764
  async function withSession(name, fn, opts = {}) {
@@ -5605,9 +5907,14 @@ import {
5605
5907
  withBackdrop,
5606
5908
  migrateMotion as migrateMotion2
5607
5909
  } from "@vosjs/studio-core";
5910
+ import { splitSegmentsAtSkips } from "@vosjs/render-core/record";
5608
5911
 
5609
5912
  // src/plugin/reuse.ts
5610
- import { docFreezes, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
5913
+ import {
5914
+ docFreezes,
5915
+ ratedSegments as ratedSegments6,
5916
+ resolveAnchor as resolveLayerAnchor
5917
+ } from "@vosjs/studio-core";
5611
5918
  var MIN_SPAN = 0.15;
5612
5919
  var AFTER_STEP = 1;
5613
5920
  var LAYER_AFTER_STEP = 5;
@@ -5683,7 +5990,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
5683
5990
  };
5684
5991
  return { map, unmatched, matched };
5685
5992
  }
5686
- function resolveAnchor2(anchor, newSteps) {
5993
+ function resolveAnchor3(anchor, newSteps) {
5687
5994
  const step = stepByRef(newSteps, anchor.step);
5688
5995
  if (!step || step.skipped) return null;
5689
5996
  const base = anchor.at === "end" ? step.tEnd : step.tStart;
@@ -5703,7 +6010,7 @@ function framed(span) {
5703
6010
  }
5704
6011
  function placeMoment(t, anchor, ctx) {
5705
6012
  if (anchor) {
5706
- const at2 = resolveAnchor2(anchor, ctx.newSteps);
6013
+ const at2 = resolveAnchor3(anchor, ctx.newSteps);
5707
6014
  if (at2 === null) return { gone: String(anchor.step) };
5708
6015
  ctx.report.anchored++;
5709
6016
  return { at: at2, via: "anchor" };
@@ -5871,6 +6178,17 @@ function retimeOverlays(prev, next, report) {
5871
6178
  return { ...o, start: +start.toFixed(3) };
5872
6179
  };
5873
6180
  return overlays.map((o) => {
6181
+ if (o.anchor && typeof o.anchor === "object") {
6182
+ const r = resolveLayerAnchor(next, o.anchor, newPieces);
6183
+ if (r.start === null) {
6184
+ report.flagged.push(
6185
+ `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`
6186
+ );
6187
+ return o;
6188
+ }
6189
+ report.anchored++;
6190
+ return { ...o, start: r.start };
6191
+ }
5874
6192
  const pin = o.pin;
5875
6193
  if (pin?.step !== void 0) {
5876
6194
  const was = stepByRef(oldSteps, pin.step);
@@ -6001,7 +6319,7 @@ async function planTake(dir, opts = {}) {
6001
6319
  doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
6002
6320
  doc = copyStyle(prev, doc);
6003
6321
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
6004
- doc.segments = rt.segments;
6322
+ doc.segments = splitSegmentsAtSkips(rt.segments, meta.skips);
6005
6323
  if (rt.freeze.length) doc.freeze = rt.freeze;
6006
6324
  if (rt.rejected.length) doc.rejected = rt.rejected;
6007
6325
  const manualZoom = rt.zoom;
@@ -6099,6 +6417,7 @@ async function planTake(dir, opts = {}) {
6099
6417
  meta
6100
6418
  };
6101
6419
  doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
6420
+ doc.segments = splitSegmentsAtSkips(doc.segments, meta.skips);
6102
6421
  if (opts.style) {
6103
6422
  const applied = applyStyle(opts.style.doc, doc, opts);
6104
6423
  doc = applied.doc;
@@ -6210,7 +6529,7 @@ async function writeScriptBeside(dir, doc) {
6210
6529
  }
6211
6530
 
6212
6531
  // src/plugin/folder.ts
6213
- import { mkdir as mkdir5 } from "fs/promises";
6532
+ import { mkdir as mkdir6 } from "fs/promises";
6214
6533
  import { join as join12 } from "path";
6215
6534
  import { recipeHints } from "@vosjs/shared/frontmatter";
6216
6535
  import { migrateHostedDoc } from "@vosjs/studio-core";
@@ -6419,7 +6738,7 @@ async function cmdPull(argv) {
6419
6738
  const payload = res.body;
6420
6739
  const safeName = (v, id) => safePathName((v ?? "").toLowerCase(), id);
6421
6740
  const out = strFlag(flags, "out") ?? safeName(folder.slug, folder.id);
6422
- await mkdir5(join12(out, "recipes"), { recursive: true });
6741
+ await mkdir6(join12(out, "recipes"), { recursive: true });
6423
6742
  const lines = [];
6424
6743
  const recipes = [];
6425
6744
  for (const rec of payload.recipes) {
@@ -6429,7 +6748,7 @@ async function cmdPull(argv) {
6429
6748
  }
6430
6749
  for (const rec of payload.inheritedRecipes) {
6431
6750
  const from = safeName(rec.folderSlug ?? "inherited", "inherited");
6432
- await mkdir5(join12(out, "recipes", "_inherited", from), { recursive: true });
6751
+ await mkdir6(join12(out, "recipes", "_inherited", from), { recursive: true });
6433
6752
  const path = join12(
6434
6753
  out,
6435
6754
  "recipes",
@@ -6446,7 +6765,7 @@ async function cmdPull(argv) {
6446
6765
  const members = [];
6447
6766
  for (const v of payload.voses) {
6448
6767
  const dir = join12(out, "members", safeName(v.slug || v.title, v.id));
6449
- await mkdir5(dir, { recursive: true });
6768
+ await mkdir6(dir, { recursive: true });
6450
6769
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
6451
6770
  if (cfg.status !== 200) {
6452
6771
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
@@ -6996,7 +7315,7 @@ async function pushTakeClaimable(dir, flags, r) {
6996
7315
 
6997
7316
  // src/plugin/program.ts
6998
7317
  import { existsSync as existsSync13 } from "fs";
6999
- import { mkdir as mkdir6, readFile as readFile11 } from "fs/promises";
7318
+ import { mkdir as mkdir7, readFile as readFile11 } from "fs/promises";
7000
7319
  import { createInterface as createInterface2 } from "readline/promises";
7001
7320
  import { basename as basename3, dirname as dirname2, join as join14 } from "path";
7002
7321
  import {
@@ -7215,7 +7534,7 @@ async function cmdFetch(argv) {
7215
7534
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
7216
7535
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
7217
7536
  const out = strFlag(flags, "out") ?? safePathName(slug, vosId);
7218
- await mkdir6(out, { recursive: true });
7537
+ await mkdir7(out, { recursive: true });
7219
7538
  await writeFileSafe(
7220
7539
  join14(out, "config.json"),
7221
7540
  JSON.stringify(cfg.body.config, null, 2)
@@ -10869,13 +11188,13 @@ function parseLlmsTxt(text) {
10869
11188
  const h1 = /^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? null;
10870
11189
  const block = /^>\s*.+(?:\n>\s*.*)*/m.exec(text)?.[0];
10871
11190
  const joined = block ? block.split("\n").map((line) => line.replace(/^>\s*/, "").trim()).filter(Boolean).join(" ") : "";
10872
- const quote = joined ? /^(.*?[.!?])(?:\s|$)/.exec(joined)?.[1] ?? joined : null;
11191
+ const quote2 = joined ? /^(.*?[.!?])(?:\s|$)/.exec(joined)?.[1] ?? joined : null;
10873
11192
  let name = h1;
10874
11193
  if (h1) {
10875
11194
  const split = /^(.+?)\s+[—–-]\s+(.+)$/.exec(h1);
10876
11195
  if (split) name = split[1].trim();
10877
11196
  }
10878
- return { name, claim: quote ?? (h1 && h1 !== name ? h1 : null) };
11197
+ return { name, claim: quote2 ?? (h1 && h1 !== name ? h1 : null) };
10879
11198
  }
10880
11199
  var WITNESS_IN_PAGE = `(() => {
10881
11200
  const cs = (el) => el ? getComputedStyle(el) : null
@@ -11236,7 +11555,7 @@ async function recordHosted(input) {
11236
11555
  }
11237
11556
 
11238
11557
  // src/plugin/run.ts
11239
- import { urlSchemeProblem } from "@vosjs/render-core/record";
11558
+ import { urlSchemeProblem as urlSchemeProblem2 } from "@vosjs/render-core/record";
11240
11559
 
11241
11560
  // src/plugin/agentBrowser.ts
11242
11561
  import { writeFile as writeFile9 } from "fs/promises";
@@ -11286,18 +11605,18 @@ function parseAgentBrowserLog(text) {
11286
11605
  function splitCommand(command) {
11287
11606
  const out = [];
11288
11607
  let cur = "";
11289
- let quote = null;
11608
+ let quote2 = null;
11290
11609
  let has = false;
11291
11610
  for (let i = 0; i < command.length; i++) {
11292
11611
  const c = command[i];
11293
- if (quote) {
11294
- if (c === quote) quote = null;
11612
+ if (quote2) {
11613
+ if (c === quote2) quote2 = null;
11295
11614
  else if (c === "\\" && i + 1 < command.length) cur += command[++i];
11296
11615
  else cur += c;
11297
11616
  continue;
11298
11617
  }
11299
11618
  if (c === '"' || c === "'") {
11300
- quote = c;
11619
+ quote2 = c;
11301
11620
  has = true;
11302
11621
  continue;
11303
11622
  }
@@ -11341,8 +11660,8 @@ var quoteAttr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11341
11660
  function hrefSelector(href) {
11342
11661
  try {
11343
11662
  const u = new URL(href);
11344
- const relative4 = `${u.pathname}${u.search}${u.hash}`;
11345
- return `a[href=${quoteAttr(relative4)}], a[href=${quoteAttr(href)}]`;
11663
+ const relative6 = `${u.pathname}${u.search}${u.hash}`;
11664
+ return `a[href=${quoteAttr(relative6)}], a[href=${quoteAttr(href)}]`;
11346
11665
  } catch {
11347
11666
  return `a[href=${quoteAttr(href)}]`;
11348
11667
  }
@@ -11719,8 +12038,8 @@ async function cmdActions(argv) {
11719
12038
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
11720
12039
  viewport = { width: Number(m[1]), height: Number(m[2]) };
11721
12040
  }
11722
- const { readFile: readFile14 } = await import("fs/promises");
11723
- const text = await readFile14(resolve12(input), "utf8");
12041
+ const { readFile: readFile16 } = await import("fs/promises");
12042
+ const text = await readFile16(resolve12(input), "utf8");
11724
12043
  const { records, problems } = parseAgentBrowserLog(text);
11725
12044
  for (const p of problems) r.log(` ${p}`);
11726
12045
  const result = convertAgentBrowser(records, {
@@ -11757,9 +12076,9 @@ function summary2(result) {
11757
12076
  }
11758
12077
  async function cmdActionsScript(input, flags) {
11759
12078
  const r = createReporter(flags.json === true);
11760
- const { readFile: readFile14 } = await import("fs/promises");
11761
- const { shotList, formatShotList } = await import("./shotList-MAQVLZBY.js");
11762
- const raw = JSON.parse(await readFile14(resolve12(input), "utf8"));
12079
+ const { readFile: readFile16 } = await import("fs/promises");
12080
+ const { shotList, formatShotList } = await import("./shotList-SCA234OS.js");
12081
+ const raw = JSON.parse(await readFile16(resolve12(input), "utf8"));
11763
12082
  const errors = validateActions(raw);
11764
12083
  if (errors.length) throw new UsageError(`${input}:
11765
12084
  ${errors.join("\n ")}`);
@@ -11770,7 +12089,7 @@ async function cmdActionsScript(input, flags) {
11770
12089
 
11771
12090
  // src/plugin/ingest.ts
11772
12091
  import { existsSync as existsSync20 } from "fs";
11773
- import { copyFile, mkdir as mkdir7, readFile as readFile12, stat as stat2 } from "fs/promises";
12092
+ import { copyFile, mkdir as mkdir8, readFile as readFile12, stat as stat2 } from "fs/promises";
11774
12093
  import { basename as basename9, join as join22, resolve as resolve13 } from "path";
11775
12094
 
11776
12095
  // src/plugin/traceInflate.ts
@@ -11872,7 +12191,11 @@ async function copyRecording(src, dest, ext) {
11872
12191
  target
11873
12192
  });
11874
12193
  try {
11875
- const conversion = await Conversion.init({ input, output });
12194
+ const conversion = await Conversion.init({
12195
+ input,
12196
+ output,
12197
+ showWarnings: false
12198
+ });
11876
12199
  if (!conversion.isValid || conversion.discardedTracks.length > 0) {
11877
12200
  await conversion.cancel?.();
11878
12201
  await copyFile(src, dest);
@@ -11958,7 +12281,7 @@ async function cmdIngest(argv) {
11958
12281
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
11959
12282
  );
11960
12283
  }
11961
- await mkdir7(outDir, { recursive: true });
12284
+ await mkdir8(outDir, { recursive: true });
11962
12285
  const paths = await ensureTakeDir(outDir);
11963
12286
  const recording = join22(outDir, `recording.${probed.ext}`);
11964
12287
  r.log("copying the recording\u2026");
@@ -12024,115 +12347,2465 @@ async function cmdIngest(argv) {
12024
12347
  return EXIT_OK;
12025
12348
  }
12026
12349
 
12027
- // src/plugin/machineCmd.ts
12028
- import { spawnSync as spawnSync4 } from "child_process";
12029
- import {
12030
- cpSync,
12031
- existsSync as existsSync22,
12032
- mkdirSync as mkdirSync6,
12033
- readFileSync as readFileSync11,
12034
- rmSync as rmSync3,
12035
- writeFileSync as writeFileSync6
12036
- } from "fs";
12037
- import { homedir as homedir2 } from "os";
12038
- import { join as join24 } from "path";
12039
- import { chromium as chromium2 } from "playwright";
12350
+ // src/plugin/checkVideo.ts
12351
+ import { createRequire as createRequire2 } from "module";
12352
+ import { existsSync as existsSync21 } from "fs";
12353
+ import { mkdir as mkdir9, readFile as readFile13, stat as stat3, writeFile as writeFile10 } from "fs/promises";
12354
+ import { basename as basename10, dirname as dirname8, extname as extname4, join as join23, resolve as resolve14 } from "path";
12355
+ import { ALL_FORMATS as ALL_FORMATS2, FilePathSource as FilePathSource2, Input as Input2 } from "mediabunny";
12040
12356
 
12041
- // src/plugin/machine.ts
12042
- import { existsSync as existsSync21, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12043
- import { dirname as dirname8, join as join23 } from "path";
12044
- var AGENT_NAMES = [
12045
- "claude",
12046
- "cursor",
12047
- "codex",
12048
- "copilot"
12049
- ];
12050
- var PROJECT_DIRS = {
12051
- claude: [".claude"],
12052
- cursor: [".cursor"],
12053
- codex: [".codex", ".agents"],
12054
- copilot: [".github/copilot"]
12055
- };
12056
- var HOME_DIRS = {
12057
- claude: [".claude"],
12058
- cursor: [".cursor"],
12059
- codex: [".codex", ".agents"],
12060
- copilot: [".copilot"]
12061
- };
12062
- function detectAgents(root, opts = {}) {
12063
- const exists = opts.exists ?? existsSync21;
12357
+ // src/plugin/videoChecks.ts
12358
+ var SCENE_CHANGE_AT = 0.12;
12359
+ var CELL_CHANGED_AT = 0.03;
12360
+ var SCENE_WINDOW = 5;
12361
+ var UNIFORM_BELOW = 2;
12362
+ var QUIET_BELOW_LUFS = -23;
12363
+ var LOUD_ABOVE_LUFS = -9;
12364
+ var SUB_BLOCK_SECONDS = 0.1;
12365
+ var SHORT_TERM_SECONDS = 3;
12366
+ var SCENE_SPIKE = 2.5;
12367
+ var ISOLATED_CHANGE_AT = 0.03;
12368
+ var ISOLATED_SPIKE = 8;
12369
+ var ISOLATED_COVERAGE = 0.25;
12370
+ function sceneChanges2(times, changes, coverage) {
12064
12371
  const out = [];
12065
- for (const agent of AGENT_NAMES) {
12066
- for (const rel of PROJECT_DIRS[agent]) {
12067
- const dir = join23(root, rel);
12068
- if (exists(dir)) out.push({ agent, dir, scope: "project" });
12069
- }
12070
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join23(root, ".github", "copilot-instructions.md"))) {
12071
- out.push({
12072
- agent,
12073
- dir: join23(root, ".github", "copilot"),
12074
- scope: "project"
12075
- });
12076
- }
12372
+ for (let i = 1; i < changes.length; i++) {
12373
+ const c = changes[i];
12374
+ if (c < Math.min(SCENE_CHANGE_AT, ISOLATED_CHANGE_AT)) continue;
12375
+ if (i > 1 && changes[i - 1] > c) continue;
12376
+ if (i + 1 < changes.length && changes[i + 1] >= c) continue;
12377
+ const around = [i - 2, i - 1, i + 1, i + 2].filter((k) => k >= 1 && k < changes.length).map((k) => changes[k]).sort((a, b) => a - b);
12378
+ const mid = around.length ? around.length % 2 ? around[(around.length - 1) / 2] : (around[around.length / 2 - 1] + around[around.length / 2]) / 2 : 0;
12379
+ const most = around.length ? around[around.length - 1] : 0;
12380
+ const share = coverage ? coverage[i] : 1;
12381
+ const standsOut = c >= SCENE_CHANGE_AT && c >= SCENE_SPIKE * mid;
12382
+ const standsAlone = c >= ISOLATED_CHANGE_AT && share >= ISOLATED_COVERAGE && c >= ISOLATED_SPIKE * most;
12383
+ if (!standsOut && !standsAlone) continue;
12384
+ out.push({
12385
+ time: times[i],
12386
+ change: Math.round(c * 1e4) / 1e4,
12387
+ ...coverage ? { coverage: Math.round(share * 1e3) / 1e3 } : {}
12388
+ });
12077
12389
  }
12078
- if (opts.global && opts.home) {
12079
- for (const agent of AGENT_NAMES) {
12080
- for (const rel of HOME_DIRS[agent]) {
12081
- const dir = join23(opts.home, rel);
12082
- if (exists(dir)) out.push({ agent, dir, scope: "global" });
12083
- }
12084
- }
12390
+ return out;
12391
+ }
12392
+ function windowCounts(cuts, duration, window2 = SCENE_WINDOW) {
12393
+ const n = Math.max(1, Math.ceil(duration / window2 - 1e-9));
12394
+ const out = Array.from({ length: n }, (_, k) => ({
12395
+ start: k * window2,
12396
+ count: 0
12397
+ }));
12398
+ for (const c of cuts) {
12399
+ const k = Math.min(n - 1, Math.max(0, Math.floor(c.time / window2)));
12400
+ out[k].count++;
12085
12401
  }
12086
12402
  return out;
12087
12403
  }
12088
- function agentsFromFlag(value, root) {
12089
- const wanted = value.trim() === "all" ? [...AGENT_NAMES] : value.split(",").map((s) => s.trim()).filter(Boolean);
12090
- const bad = wanted.filter((w) => !AGENT_NAMES.includes(w));
12091
- if (bad.length)
12404
+ function crowdedRuns(cuts, window2 = SCENE_WINDOW) {
12405
+ const runs = [];
12406
+ let run2 = [];
12407
+ for (const c of cuts) {
12408
+ if (run2.length && c.time - run2[run2.length - 1].time < window2) run2.push(c);
12409
+ else {
12410
+ if (run2.length > 1) runs.push(run2);
12411
+ run2 = [c];
12412
+ }
12413
+ }
12414
+ if (run2.length > 1) runs.push(run2);
12415
+ return runs;
12416
+ }
12417
+ function createLoudnessMeter(sampleRate, channelCount, subBlockSeconds) {
12418
+ const shelf = (() => {
12419
+ const f0 = 1681.974450955533;
12420
+ const G = 3.999843853973347;
12421
+ const Q = 0.7071752369554196;
12422
+ const K = Math.tan(Math.PI * f0 / sampleRate);
12423
+ const Vh = Math.pow(10, G / 20);
12424
+ const Vb = Math.pow(Vh, 0.4996667741545416);
12425
+ const a0 = 1 + K / Q + K * K;
12092
12426
  return {
12093
- error: `--agent takes ${AGENT_NAMES.join(", ")} or all, not "${bad.join(", ")}"`
12427
+ b0: (Vh + Vb * K / Q + K * K) / a0,
12428
+ b1: 2 * (K * K - Vh) / a0,
12429
+ b2: (Vh - Vb * K / Q + K * K) / a0,
12430
+ a1: 2 * (K * K - 1) / a0,
12431
+ a2: (1 - K / Q + K * K) / a0
12094
12432
  };
12095
- if (!wanted.length) return { error: "--agent expects a name" };
12433
+ })();
12434
+ const highpass = (() => {
12435
+ const f0 = 38.13547087602444;
12436
+ const Q = 0.5003270373238773;
12437
+ const K = Math.tan(Math.PI * f0 / sampleRate);
12438
+ const a0 = 1 + K / Q + K * K;
12439
+ return {
12440
+ b0: 1,
12441
+ b1: -2,
12442
+ b2: 1,
12443
+ a1: 2 * (K * K - 1) / a0,
12444
+ a2: (1 - K / Q + K * K) / a0
12445
+ };
12446
+ })();
12447
+ const per = Math.max(1, Math.round(sampleRate * subBlockSeconds));
12448
+ const state = Array.from({ length: channelCount }, () => ({
12449
+ // Direct form I history for each stage.
12450
+ x1: 0,
12451
+ x2: 0,
12452
+ y1: 0,
12453
+ y2: 0,
12454
+ u1: 0,
12455
+ u2: 0,
12456
+ w1: 0,
12457
+ w2: 0,
12458
+ sum: 0,
12459
+ count: 0,
12460
+ energies: []
12461
+ }));
12462
+ let peak = 0;
12463
+ let samples = 0;
12096
12464
  return {
12097
- homes: wanted.map((agent) => ({
12098
- agent,
12099
- dir: join23(root, PROJECT_DIRS[agent][0]),
12100
- scope: "project"
12101
- }))
12465
+ push(channels) {
12466
+ for (let c = 0; c < channelCount; c++) {
12467
+ const data = channels[c];
12468
+ const s = state[c];
12469
+ if (!data) continue;
12470
+ for (let i = 0; i < data.length; i++) {
12471
+ const x = data[i];
12472
+ const ax = x < 0 ? -x : x;
12473
+ if (ax > peak) peak = ax;
12474
+ const y = shelf.b0 * x + shelf.b1 * s.x1 + shelf.b2 * s.x2 - shelf.a1 * s.y1 - shelf.a2 * s.y2;
12475
+ s.x2 = s.x1;
12476
+ s.x1 = x;
12477
+ s.y2 = s.y1;
12478
+ s.y1 = y;
12479
+ const w = highpass.b0 * y + highpass.b1 * s.u1 + highpass.b2 * s.u2 - highpass.a1 * s.w1 - highpass.a2 * s.w2;
12480
+ s.u2 = s.u1;
12481
+ s.u1 = y;
12482
+ s.w2 = s.w1;
12483
+ s.w1 = w;
12484
+ s.sum += w * w;
12485
+ s.count++;
12486
+ if (s.count === per) {
12487
+ s.energies.push(s.sum / per);
12488
+ s.sum = 0;
12489
+ s.count = 0;
12490
+ }
12491
+ }
12492
+ if (c === 0) samples += data.length;
12493
+ }
12494
+ },
12495
+ finish() {
12496
+ return {
12497
+ energies: state.map(
12498
+ (s) => s.count > 0 ? [...s.energies, s.sum / s.count] : s.energies
12499
+ ),
12500
+ peak,
12501
+ samples
12502
+ };
12503
+ }
12102
12504
  };
12103
12505
  }
12104
- function skillsDirFor(home) {
12105
- return join23(home.dir, "skills");
12106
- }
12107
- function fallbackSkillsHome(root) {
12108
- return { agent: "codex", dir: join23(root, ".agents"), scope: "project" };
12506
+ function channelWeights(count) {
12507
+ if (count === 6) return [1, 1, 1, 0, 1.41, 1.41];
12508
+ return Array.from({ length: count }, (_, i) => i < 3 ? 1 : 1.41);
12109
12509
  }
12110
- function installedSkills(dir, exists = existsSync21, list = (p) => {
12111
- try {
12112
- return readdirSync5(p);
12113
- } catch {
12114
- return [];
12510
+ var LOUDNESS_OFFSET = -0.691;
12511
+ var toLufs = (power) => power > 0 ? LOUDNESS_OFFSET + 10 * Math.log10(power) : null;
12512
+ function windowPower(energies, weights, from, count) {
12513
+ let total = 0;
12514
+ for (let c = 0; c < energies.length; c++) {
12515
+ const e = energies[c];
12516
+ let sum = 0;
12517
+ let n = 0;
12518
+ for (let k = from; k < from + count && k < e.length; k++) {
12519
+ sum += e[k];
12520
+ n++;
12521
+ }
12522
+ if (n) total += (weights[c] ?? 1) * (sum / n);
12523
+ }
12524
+ return total;
12525
+ }
12526
+ var round1 = (n) => n === null ? null : Math.round(n * 10) / 10;
12527
+ function loudnessFromEnergies(energies, peak, subBlock = SUB_BLOCK_SECONDS) {
12528
+ const weights = channelWeights(energies.length);
12529
+ const pieces = Math.max(0, ...energies.map((e) => e.length));
12530
+ const blockLen = Math.max(1, Math.round(0.4 / subBlock));
12531
+ const blocks = [];
12532
+ for (let k = 0; k + blockLen <= pieces; k++)
12533
+ blocks.push(windowPower(energies, weights, k, blockLen));
12534
+ if (!blocks.length && pieces > 0)
12535
+ blocks.push(windowPower(energies, weights, 0, pieces));
12536
+ const mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
12537
+ const loud = (p) => toLufs(p) ?? -Infinity;
12538
+ const absolute2 = blocks.filter((p) => loud(p) > -70);
12539
+ let integrated = null;
12540
+ if (absolute2.length) {
12541
+ const gate = loud(mean(absolute2)) - 10;
12542
+ const relative6 = absolute2.filter((p) => loud(p) > gate);
12543
+ integrated = toLufs(mean(relative6));
12544
+ }
12545
+ const stLen = Math.max(1, Math.round(SHORT_TERM_SECONDS / subBlock));
12546
+ const shortTerm = [];
12547
+ for (let k = 0; k < pieces; k += stLen) {
12548
+ if (pieces - k < blockLen && k > 0) break;
12549
+ shortTerm.push({
12550
+ time: Math.round(k * subBlock * 100) / 100,
12551
+ loudness: round1(toLufs(windowPower(energies, weights, k, stLen)))
12552
+ });
12115
12553
  }
12116
- }) {
12117
- if (!exists(dir)) return [];
12118
- return list(dir).filter((name) => exists(join23(dir, name, "SKILL.md"))).sort();
12554
+ const st = shortTerm.map((s) => s.loudness).filter((l) => l !== null);
12555
+ return {
12556
+ unit: "LUFS",
12557
+ integrated: round1(integrated),
12558
+ shortTerm,
12559
+ maxShortTerm: st.length ? Math.max(...st) : null,
12560
+ samplePeakDb: peak > 0 ? round1(20 * Math.log10(peak)) : null
12561
+ };
12119
12562
  }
12120
- var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
12121
- var RULES_END = "<!-- vos:end -->";
12122
- var RULES_BODY = `## Product videos (vosso)
12123
- - Skill: product-video (npx skills add vosjs/skills) records the real product from actions.json
12124
- - Look before you cut: vos frames take --at-zooms; edit take/doc.json and re-render, never re-record
12125
- - Hand back: vos open take serves the take into the studio at vos.so, doc.json intact
12126
- - Local by default: vos push only when asked for a link, the shelf or a cloud render
12127
- - Deleting: vos delete moves a vos to Trash and vos restore brings it back; if you deleted by mistake, restore it and say so. Only the person can delete forever, at vos.so/app/trash
12128
- - Credentials: VOS_API_KEY, then ~/.config/vos/credentials, then vos login; never print one
12129
- - Reference: https://vos.so/llms-full.txt`;
12130
- var RULES_BLOCK = `${RULES_BEGIN}
12131
- ${RULES_BODY}
12132
- ${RULES_END}`;
12133
- function applyRulesBlock(existing) {
12134
- const current = existing ?? "";
12135
- const begin = current.indexOf(RULES_BEGIN);
12563
+ function loudnessWarnings(l) {
12564
+ if (l.integrated === null)
12565
+ return [
12566
+ "the sound is silent: no 400 ms block reaches -70 LUFS, the floor loudness is measured from"
12567
+ ];
12568
+ if (l.integrated < QUIET_BELOW_LUFS)
12569
+ return [
12570
+ `integrated loudness ${l.integrated} LUFS is below ${QUIET_BELOW_LUFS}: too quiet for a social feed, where a viewer turns sound up once and keeps it`
12571
+ ];
12572
+ if (l.integrated > LOUD_ABOVE_LUFS)
12573
+ return [
12574
+ `integrated loudness ${l.integrated} LUFS is above ${LOUD_ABOVE_LUFS}: too loud, a platform turns it down and a peak may clip`
12575
+ ];
12576
+ return [];
12577
+ }
12578
+ function sceneWarnings(cuts) {
12579
+ return crowdedRuns(cuts).map(
12580
+ (run2) => `${run2.length} full-frame changes within ${SCENE_WINDOW} s (at ${run2.map((c) => `${c.time.toFixed(2)}s`).join(", ")}): more than one cut in ${SCENE_WINDOW} s is hard to follow`
12581
+ );
12582
+ }
12583
+ function posterWarning(which, deviation) {
12584
+ if (deviation >= UNIFORM_BELOW) return null;
12585
+ return which === "first" ? `the first frame is near-uniform (deviation ${deviation.toFixed(2)} of 255): the video opens on an empty or faded frame, which is what a feed shows before it plays` : `the last frame is near-uniform (deviation ${deviation.toFixed(2)} of 255): the video ends on an empty or faded frame, which is what a player rests on`;
12586
+ }
12587
+
12588
+ // src/plugin/checkVideo.ts
12589
+ var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set(["json", "help", "strict"]);
12590
+ var THUMB_LONG_SIDE = 64;
12591
+ async function probeVideoFacts(file) {
12592
+ const input = new Input2({
12593
+ source: new FilePathSource2(file),
12594
+ formats: ALL_FORMATS2
12595
+ });
12596
+ try {
12597
+ const video = await input.getPrimaryVideoTrack();
12598
+ if (!video) throw new UsageError(`${file} has no video track`);
12599
+ const [format, duration, stats, audio, bytes] = await Promise.all([
12600
+ input.getFormat(),
12601
+ input.computeDuration(),
12602
+ video.computePacketStats(240),
12603
+ input.getPrimaryAudioTrack(),
12604
+ stat3(file).then((s) => s.size)
12605
+ ]);
12606
+ return {
12607
+ file,
12608
+ bytes,
12609
+ duration: Math.round(duration * 1e3) / 1e3,
12610
+ width: video.displayWidth,
12611
+ height: video.displayHeight,
12612
+ fps: Math.round(stats.averagePacketRate * 100) / 100,
12613
+ container: format.mimeType,
12614
+ video: {
12615
+ codec: video.codec,
12616
+ codecString: await video.getCodecParameterString()
12617
+ },
12618
+ audio: audio ? {
12619
+ codec: audio.codec,
12620
+ codecString: await audio.getCodecParameterString(),
12621
+ sampleRate: audio.sampleRate,
12622
+ channels: audio.numberOfChannels
12623
+ } : null
12624
+ };
12625
+ } finally {
12626
+ input.dispose();
12627
+ }
12628
+ }
12629
+ async function mediabunnyBundle() {
12630
+ const entry = createRequire2(import.meta.url).resolve("mediabunny");
12631
+ const bundle = join23(dirname8(entry), "mediabunny.min.mjs");
12632
+ if (existsSync21(bundle)) return readFile13(bundle, "utf8");
12633
+ return readFile13(
12634
+ join23(entry.split("/dist/")[0], "dist/bundles/mediabunny.min.mjs"),
12635
+ "utf8"
12636
+ );
12637
+ }
12638
+ async function decodeInPage(opts) {
12639
+ const w = window;
12640
+ try {
12641
+ const load = new Function("u", "return import(u)");
12642
+ const MB = await load(
12643
+ URL.createObjectURL(new Blob([opts.mbCode], { type: "text/javascript" }))
12644
+ );
12645
+ const input = new MB.Input({
12646
+ source: new MB.UrlSource(opts.videoUrl),
12647
+ formats: MB.ALL_FORMATS
12648
+ });
12649
+ const track = await input.getPrimaryVideoTrack();
12650
+ if (!track) throw new Error("no video track");
12651
+ if (!await track.canDecode())
12652
+ throw new Error(`this browser cannot decode ${String(track.codec)}`);
12653
+ const pixels = (canvas) => {
12654
+ const g = canvas.getContext("2d", {
12655
+ willReadFrequently: true
12656
+ });
12657
+ const px = g.getImageData(0, 0, canvas.width, canvas.height).data;
12658
+ const out = new Array(px.length / 4 * 3);
12659
+ for (let p = 0, q = 0; p < px.length; p += 4, q += 3) {
12660
+ out[q] = px[p];
12661
+ out[q + 1] = px[p + 1];
12662
+ out[q + 2] = px[p + 2];
12663
+ }
12664
+ return out;
12665
+ };
12666
+ const deviation = (a) => {
12667
+ let total = 0;
12668
+ for (let c = 0; c < 3; c++) {
12669
+ let s = 0;
12670
+ let n = 0;
12671
+ for (let i = c; i < a.length; i += 3) {
12672
+ s += a[i];
12673
+ n++;
12674
+ }
12675
+ const m = s / Math.max(1, n);
12676
+ let q = 0;
12677
+ for (let i = c; i < a.length; i += 3) q += (a[i] - m) * (a[i] - m);
12678
+ total += Math.sqrt(q / Math.max(1, n));
12679
+ }
12680
+ return total / 3;
12681
+ };
12682
+ const difference = (a, b) => {
12683
+ let s = 0;
12684
+ for (let i = 0; i < a.length; i++) s += Math.abs(a[i] - b[i]);
12685
+ return s / Math.max(1, a.length) / 255;
12686
+ };
12687
+ const coverageOf = (a, b) => {
12688
+ const W = opts.thumbW;
12689
+ const H = opts.thumbH;
12690
+ let cells = 0;
12691
+ let changed = 0;
12692
+ for (let cy = 0; cy < H; cy += 4)
12693
+ for (let cx = 0; cx < W; cx += 4) {
12694
+ let s = 0;
12695
+ let n = 0;
12696
+ for (let y = cy; y < Math.min(H, cy + 4); y++)
12697
+ for (let x = cx; x < Math.min(W, cx + 4); x++) {
12698
+ const k = (y * W + x) * 3;
12699
+ s += Math.abs(a[k] - b[k]) + Math.abs(a[k + 1] - b[k + 1]) + Math.abs(a[k + 2] - b[k + 2]);
12700
+ n += 3;
12701
+ }
12702
+ cells++;
12703
+ if (s / Math.max(1, n) / 255 >= opts.cellChanged) changed++;
12704
+ }
12705
+ return changed / Math.max(1, cells);
12706
+ };
12707
+ const small = document.createElement("canvas");
12708
+ small.width = opts.thumbW;
12709
+ small.height = opts.thumbH;
12710
+ const sctx = small.getContext("2d", { willReadFrequently: true });
12711
+ sctx.imageSmoothingEnabled = true;
12712
+ sctx.imageSmoothingQuality = "high";
12713
+ const samples = new MB.VideoSampleSink(track);
12714
+ const step = 1 / opts.sampleFps;
12715
+ const times = [];
12716
+ const changes = [];
12717
+ const coverage = [];
12718
+ let next = -Infinity;
12719
+ let prev = null;
12720
+ let first = null;
12721
+ let firstTime = 0;
12722
+ let lastTime = 0;
12723
+ let lastKept = -1;
12724
+ let lastGray = null;
12725
+ let tail = null;
12726
+ const duration = await input.computeDuration();
12727
+ for await (const sample2 of samples.samples()) {
12728
+ const t = sample2.timestamp;
12729
+ sctx.clearRect(0, 0, opts.thumbW, opts.thumbH);
12730
+ sample2.draw(sctx, 0, 0, opts.thumbW, opts.thumbH);
12731
+ sample2.close();
12732
+ lastTime = t;
12733
+ if (t + step / 2 < next) {
12734
+ tail = pixels(small);
12735
+ continue;
12736
+ }
12737
+ tail = null;
12738
+ const g = pixels(small);
12739
+ if (!first) {
12740
+ first = g;
12741
+ firstTime = t;
12742
+ }
12743
+ times.push(t);
12744
+ changes.push(prev ? difference(prev, g) : 0);
12745
+ coverage.push(prev ? coverageOf(prev, g) : 0);
12746
+ prev = g;
12747
+ lastKept = t;
12748
+ lastGray = g;
12749
+ next = next === -Infinity ? t + step : next + step;
12750
+ if (next <= t) next = t + step;
12751
+ w.__progress = duration > 0 ? Math.min(0.9, t / duration * 0.9) : 0;
12752
+ }
12753
+ if (!first || !lastGray) throw new Error("no video frame decoded");
12754
+ if (tail && lastTime > lastKept) {
12755
+ times.push(lastTime);
12756
+ changes.push(prev ? difference(prev, tail) : 0);
12757
+ coverage.push(prev ? coverageOf(prev, tail) : 0);
12758
+ lastGray = tail;
12759
+ }
12760
+ const toBase64 = (bytes) => {
12761
+ let s = "";
12762
+ for (let i = 0; i < bytes.length; i += 32768)
12763
+ s += String.fromCharCode(...bytes.subarray(i, i + 32768));
12764
+ return btoa(s);
12765
+ };
12766
+ const full = new MB.CanvasSink(track);
12767
+ const png = async (t) => {
12768
+ const wc = await full.getCanvas(t);
12769
+ if (!wc) return "";
12770
+ const c = wc.canvas;
12771
+ const blob = "convertToBlob" in c ? await c.convertToBlob({ type: "image/png" }) : await new Promise(
12772
+ (res, rej) => c.toBlob(
12773
+ (b) => b ? res(b) : rej(new Error("toBlob failed")),
12774
+ "image/png"
12775
+ )
12776
+ );
12777
+ return toBase64(new Uint8Array(await blob.arrayBuffer()));
12778
+ };
12779
+ const firstPng = await png(firstTime);
12780
+ const lastPng = await png(lastTime);
12781
+ let audio = null;
12782
+ const at2 = await input.getPrimaryAudioTrack();
12783
+ if (at2 && await at2.canDecode()) {
12784
+ const make = new Function(`return (${opts.meterSrc})`)();
12785
+ const meter = make(at2.sampleRate, at2.numberOfChannels, opts.subBlock);
12786
+ const sink = new MB.AudioBufferSink(at2);
12787
+ for await (const wb of sink.buffers()) {
12788
+ const b = wb.buffer;
12789
+ const chans = [];
12790
+ for (let c = 0; c < at2.numberOfChannels; c++)
12791
+ chans.push(
12792
+ c < b.numberOfChannels ? b.getChannelData(c) : new Float32Array(0)
12793
+ );
12794
+ meter.push(chans);
12795
+ }
12796
+ audio = {
12797
+ sampleRate: at2.sampleRate,
12798
+ channels: at2.numberOfChannels,
12799
+ ...meter.finish()
12800
+ };
12801
+ }
12802
+ input.dispose();
12803
+ w.__done = {
12804
+ times,
12805
+ changes,
12806
+ coverage,
12807
+ firstDeviation: deviation(first),
12808
+ lastDeviation: deviation(lastGray),
12809
+ firstTime,
12810
+ lastTime,
12811
+ firstPng,
12812
+ lastPng,
12813
+ audio
12814
+ };
12815
+ } catch (e) {
12816
+ w.__error = String(e instanceof Error && e.stack || e);
12817
+ }
12818
+ }
12819
+ async function checkVideo(browser, file, outDir) {
12820
+ const facts = await probeVideoFacts(file);
12821
+ const ceiling = facts.duration > 120 ? 10 : 30;
12822
+ const sampleFps = Math.max(
12823
+ Math.min(10, facts.fps || 10),
12824
+ Math.min(facts.fps || 30, ceiling)
12825
+ );
12826
+ const long = Math.max(facts.width, facts.height, 1);
12827
+ const side = (n) => Math.max(1, Math.round(n / long * THUMB_LONG_SIDE));
12828
+ const server = await startTakeServer(dirname8(resolve14(file)), {
12829
+ "/check.html": '<!doctype html><html><head><meta charset="utf-8"><link rel="icon" href="data:,"></head><body></body></html>'
12830
+ });
12831
+ const page = await browser.newPage();
12832
+ let done;
12833
+ try {
12834
+ await page.addInitScript(() => {
12835
+ ;
12836
+ globalThis.__name = (f) => f;
12837
+ });
12838
+ await page.goto(`${server.base}/check.html`);
12839
+ void page.evaluate(decodeInPage, {
12840
+ mbCode: await mediabunnyBundle(),
12841
+ meterSrc: String(createLoudnessMeter),
12842
+ videoUrl: `/${encodeURIComponent(basename10(file))}`,
12843
+ sampleFps,
12844
+ thumbW: side(facts.width),
12845
+ thumbH: side(facts.height),
12846
+ subBlock: SUB_BLOCK_SECONDS,
12847
+ cellChanged: CELL_CHANGED_AT
12848
+ }).catch(() => {
12849
+ });
12850
+ done = await waitForPageDone(
12851
+ page,
12852
+ "check-video",
12853
+ () => {
12854
+ },
12855
+ 12e4 + facts.duration * 4e3
12856
+ );
12857
+ } finally {
12858
+ await page.close();
12859
+ server.close();
12860
+ }
12861
+ await mkdir9(outDir, { recursive: true });
12862
+ const firstFile = join23(outDir, "first.png");
12863
+ const lastFile = join23(outDir, "last.png");
12864
+ await writeFile10(firstFile, Buffer.from(done.firstPng, "base64"));
12865
+ await writeFile10(lastFile, Buffer.from(done.lastPng, "base64"));
12866
+ const changes = sceneChanges2(done.times, done.changes, done.coverage);
12867
+ const loudness = done.audio ? loudnessFromEnergies(done.audio.energies, done.audio.peak) : null;
12868
+ const round2 = (n) => Math.round(n * 100) / 100;
12869
+ const posters = {
12870
+ first: {
12871
+ file: firstFile,
12872
+ time: round2(done.firstTime),
12873
+ deviation: round2(done.firstDeviation),
12874
+ uniform: posterWarning("first", done.firstDeviation) !== null
12875
+ },
12876
+ last: {
12877
+ file: lastFile,
12878
+ time: round2(done.lastTime),
12879
+ deviation: round2(done.lastDeviation),
12880
+ uniform: posterWarning("last", done.lastDeviation) !== null
12881
+ }
12882
+ };
12883
+ const warnings = [
12884
+ ...sceneWarnings(changes),
12885
+ ...loudness ? loudnessWarnings(loudness) : [],
12886
+ ...[
12887
+ posterWarning("first", done.firstDeviation),
12888
+ posterWarning("last", done.lastDeviation)
12889
+ ].filter((w) => w !== null)
12890
+ ];
12891
+ const result = {
12892
+ ...facts,
12893
+ scenes: {
12894
+ sampleFps: round2(sampleFps),
12895
+ threshold: SCENE_CHANGE_AT,
12896
+ isolatedThreshold: ISOLATED_CHANGE_AT,
12897
+ window: SCENE_WINDOW,
12898
+ changes: changes.map((c) => ({ ...c, time: round2(c.time) })),
12899
+ perWindow: windowCounts(changes, facts.duration)
12900
+ },
12901
+ loudness,
12902
+ posters,
12903
+ warnings,
12904
+ outDir
12905
+ };
12906
+ await writeFile10(
12907
+ join23(outDir, "check.json"),
12908
+ `${JSON.stringify(result, null, 2)}
12909
+ `
12910
+ );
12911
+ return result;
12912
+ }
12913
+ var mb = (n) => n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
12914
+ function checkVideoLines(r) {
12915
+ const codecs = [
12916
+ r.video.codecString ?? r.video.codec ?? "unknown",
12917
+ ...r.audio ? [r.audio.codecString ?? r.audio.codec ?? "unknown"] : []
12918
+ ].join(" + ");
12919
+ const cuts = r.scenes.changes;
12920
+ const l = r.loudness;
12921
+ const st = l?.shortTerm.map(
12922
+ (s) => s.loudness === null ? "-" : String(s.loudness)
12923
+ );
12924
+ return [
12925
+ `Checked ${r.file}: ${r.duration.toFixed(2)}s, ${r.width}x${r.height} @ ${r.fps} fps, ${mb(r.bytes)}, ${codecs}`,
12926
+ ` scene changes: ${cuts.length}${cuts.length ? ` (${cuts.map((c) => `${c.time.toFixed(2)}s`).join(", ")})` : ""}; per ${r.scenes.window} s: ${r.scenes.perWindow.map((w) => w.count).join(", ")}`,
12927
+ r.audio ? ` loudness: ${l?.integrated ?? "silent"}${l?.integrated !== null ? " LUFS" : ""} integrated; short-term every 3 s: ${st?.join(", ")}; sample peak ${l?.samplePeakDb ?? "-inf"} dBFS` : " loudness: no audio track",
12928
+ ` posters: ${r.posters.first.file} (deviation ${r.posters.first.deviation}), ${r.posters.last.file} (deviation ${r.posters.last.deviation})`,
12929
+ ` ${r.warnings.length ? `${r.warnings.length} warning${r.warnings.length === 1 ? "" : "s"}` : "no warnings"}; ${join23(r.outDir, "check.json")}`
12930
+ ].join("\n");
12931
+ }
12932
+ async function cmdCheckVideo(argv) {
12933
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12934
+ const file = positionals[0];
12935
+ if (!file)
12936
+ throw new UsageError(
12937
+ "vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]"
12938
+ );
12939
+ if (!existsSync21(file)) throw new UsageError(`${file} does not exist`);
12940
+ const r = createReporter(flags.json === true);
12941
+ const strict = flags.strict === true;
12942
+ const outDir = resolve14(
12943
+ strFlag(flags, "out") ?? join23(dirname8(file), `${basename10(file, extname4(file))}-check`)
12944
+ );
12945
+ const browser = await launchBrowser();
12946
+ try {
12947
+ r.event({ event: "phase", phase: "check-video" });
12948
+ const result = await checkVideo(browser, file, outDir);
12949
+ for (const w of result.warnings) r.warn(w);
12950
+ r.done({ ...result }, checkVideoLines(result));
12951
+ return result.warnings.length && strict ? EXIT_USAGE : EXIT_OK;
12952
+ } finally {
12953
+ await browser.close();
12954
+ }
12955
+ }
12956
+
12957
+ // src/plugin/join.ts
12958
+ import { existsSync as existsSync22, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12959
+ import { copyFile as copyFile2, mkdir as mkdir10, rm as rm3 } from "fs/promises";
12960
+ import {
12961
+ basename as basename11,
12962
+ dirname as dirname9,
12963
+ isAbsolute as isAbsolute3,
12964
+ join as join24,
12965
+ relative as relative4,
12966
+ resolve as resolve15,
12967
+ sep as sep2
12968
+ } from "path";
12969
+ import {
12970
+ mediaKey as mediaKey2,
12971
+ mediaRefId,
12972
+ migrateMotion as migrateMotion3,
12973
+ projectFromArtifact as projectFromArtifact2,
12974
+ ratedSegments as ratedSegments8,
12975
+ resolveAnchor as resolveAnchor5,
12976
+ segmentOutputExtents as segmentOutputExtents3
12977
+ } from "@vosjs/studio-core";
12978
+ import { totalDuration as totalDuration2 } from "@vosjs/timeline";
12979
+
12980
+ // src/plugin/stepCaptions.ts
12981
+ import {
12982
+ docMediaList,
12983
+ findStep as findStep2,
12984
+ mediaKey,
12985
+ ratedSegments as ratedSegments7,
12986
+ resolveAnchor as resolveAnchor4,
12987
+ segmentOutputExtents as segmentOutputExtents2
12988
+ } from "@vosjs/studio-core";
12989
+ import { totalDuration } from "@vosjs/timeline";
12990
+ var STEP_CAPTIONS_FROM = "steps";
12991
+ var STEP_CAPTION_OFFSET = 0.2;
12992
+ var CAPTION_MAX = 3;
12993
+ var CAPTION_MIN = 1.2;
12994
+ var CAPTION_GAP = 0.1;
12995
+ function captionOf(step, actions, script) {
12996
+ const own = step.caption;
12997
+ if (typeof own === "string" && own.trim()) return own.trim();
12998
+ for (const list of [
12999
+ actions?.steps,
13000
+ Array.isArray(script?.steps) ? script.steps : void 0
13001
+ ]) {
13002
+ if (!Array.isArray(list)) continue;
13003
+ const byId = step.id !== void 0 ? list.find((s) => s?.id === step.id) : void 0;
13004
+ const raw = byId ?? list[step.step];
13005
+ if (typeof raw?.caption === "string" && raw.caption.trim())
13006
+ return raw.caption.trim();
13007
+ }
13008
+ return null;
13009
+ }
13010
+ var safeId = (v) => String(v).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "step";
13011
+ function landStepCaptions(doc, actions) {
13012
+ const report = { landed: [], skipped: [], uncaptioned: [] };
13013
+ const kept = (doc.overlays ?? []).filter((o) => o.from !== STEP_CAPTIONS_FROM);
13014
+ const rated = ratedSegments7(doc);
13015
+ const footageEnd = totalDuration(rated);
13016
+ const docSegments = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
13017
+ const extents = segmentOutputExtents2(doc);
13018
+ const clipEnd = (start, media) => {
13019
+ for (let i = 0; i < extents.length; i++) {
13020
+ const e = extents[i];
13021
+ if (mediaKey(docSegments[i]?.media) === media && start >= e.start - 1e-6 && start < e.end - 1e-6)
13022
+ return e.end;
13023
+ }
13024
+ return footageEnd;
13025
+ };
13026
+ const ends = /* @__PURE__ */ new Map();
13027
+ const spoken = (media, step, steps) => kept.some((o) => {
13028
+ if (o.kind !== "text" || o.preset !== "caption") return false;
13029
+ if (o.anchor && mediaKey(o.anchor.media) === media)
13030
+ return findStep2(steps, o.anchor.step) === step;
13031
+ return media === "" && o.id === `${CAPTION_ID_PREFIX}${step.step}`;
13032
+ });
13033
+ const fresh = [];
13034
+ const taken = new Set(kept.map((o) => o.id));
13035
+ for (const { id: media, source } of docMediaList(doc)) {
13036
+ const steps = source.meta.steps ?? [];
13037
+ let captioned = 0;
13038
+ for (const step of steps) {
13039
+ const caption = captionOf(step, actions.get(media), source.script);
13040
+ if (!caption) continue;
13041
+ captioned++;
13042
+ const ref = step.id ?? step.step;
13043
+ const label = `step ${JSON.stringify(ref)}${media ? ` of ${media}` : ""}`;
13044
+ if (step.skipped) {
13045
+ report.skipped.push(`${label}: the recorder skipped it`);
13046
+ continue;
13047
+ }
13048
+ if (spoken(media, step, steps)) {
13049
+ report.skipped.push(`${label}: a caption is already anchored to it`);
13050
+ continue;
13051
+ }
13052
+ const anchor = {
13053
+ step: ref,
13054
+ ...media ? { media } : {},
13055
+ at: "end",
13056
+ offset: STEP_CAPTION_OFFSET
13057
+ };
13058
+ const placed = resolveAnchor4(doc, anchor, rated);
13059
+ if (placed.start === null) {
13060
+ report.skipped.push(`${label}: the cut leaves it out`);
13061
+ continue;
13062
+ }
13063
+ const end = clipEnd(placed.start, media);
13064
+ if (placed.start >= end - CAPTION_MIN) {
13065
+ report.skipped.push(`${label}: it plays in the last second of its clip`);
13066
+ continue;
13067
+ }
13068
+ const base = `step-${media ? `${media}-` : ""}${safeId(ref)}`;
13069
+ let id = base;
13070
+ for (let n = 2; taken.has(id); n++) id = `${base}-${n}`;
13071
+ taken.add(id);
13072
+ const clip = {
13073
+ ...houseCaption(id, caption, placed.start, CAPTION_MAX),
13074
+ anchor,
13075
+ from: STEP_CAPTIONS_FROM
13076
+ };
13077
+ ends.set(clip, end);
13078
+ fresh.push(clip);
13079
+ report.landed.push({
13080
+ id,
13081
+ step: ref,
13082
+ ...media ? { media } : {},
13083
+ start: placed.start
13084
+ });
13085
+ }
13086
+ if (steps.length && !captioned) report.uncaptioned.push(media);
13087
+ }
13088
+ fresh.sort((a, b) => a.start - b.start);
13089
+ fresh.forEach((c, i) => {
13090
+ const next = fresh[i + 1];
13091
+ const clipRoom = (ends.get(c) ?? footageEnd) - c.start;
13092
+ const nextRoom = next ? next.start - CAPTION_GAP - c.start : Infinity;
13093
+ c.duration = +Math.min(
13094
+ CAPTION_MAX,
13095
+ clipRoom,
13096
+ Math.max(CAPTION_MIN, nextRoom)
13097
+ ).toFixed(3);
13098
+ });
13099
+ const overlays = [...kept, ...fresh];
13100
+ if (overlays.length) doc.overlays = overlays;
13101
+ else delete doc.overlays;
13102
+ return report;
13103
+ }
13104
+ function bareCaptionsFlag(argv) {
13105
+ return argv.map(
13106
+ (a, i) => a === "--captions" && (i + 1 >= argv.length || argv[i + 1].startsWith("--")) ? "--captions=steps" : a
13107
+ );
13108
+ }
13109
+
13110
+ // src/plugin/join.ts
13111
+ var FULL_BLEED_FRAME = {
13112
+ inset: { top: 0, right: 0, bottom: 0, left: 0 },
13113
+ radius: 0,
13114
+ shadow: 0,
13115
+ shadowContact: 0,
13116
+ border: 0,
13117
+ fit: "cover"
13118
+ };
13119
+ var round32 = (v) => Math.round(v * 1e3) / 1e3;
13120
+ var FILE_SOUND_FADE = 0.15;
13121
+ var prefixed = (id, key) => `${id}/${takeRelativeFile(key)}`;
13122
+ function fileDoc(key, probed, t0 = 0) {
13123
+ const meta = {
13124
+ dpr: 1,
13125
+ zoom: 1,
13126
+ t0,
13127
+ durationMs: probed.durationMs,
13128
+ width: probed.width,
13129
+ height: probed.height,
13130
+ captureWidth: probed.width,
13131
+ captureHeight: probed.height,
13132
+ fps: probed.fps > 0 ? probed.fps : 30,
13133
+ hasAudio: probed.hasAudio,
13134
+ producer: "ingest"
13135
+ };
13136
+ const doc = projectFromArtifact2({ videoKey: key, cursor: [], meta }, key, {
13137
+ footage: true
13138
+ }).doc;
13139
+ doc.segments = [{ in: 0, out: round32(probed.durationMs / 1e3) }];
13140
+ return doc;
13141
+ }
13142
+ function endingLayers(doc) {
13143
+ const pieces = ratedSegments8(doc);
13144
+ const end = totalDuration2(pieces);
13145
+ if (!(end > 0)) return [];
13146
+ return (doc.overlays ?? []).filter((o) => {
13147
+ const start = o.anchor ? resolveAnchor5(doc, o.anchor, pieces).start ?? o.start : o.start;
13148
+ return start >= end - 1e-3;
13149
+ });
13150
+ }
13151
+ function dropEnding(d, label, notes) {
13152
+ const ending = endingLayers(d);
13153
+ const legacy = d.endCard !== void 0;
13154
+ if (!ending.length && !legacy) return;
13155
+ const gone = new Set(ending);
13156
+ d.overlays = (d.overlays ?? []).filter((o) => !gone.has(o));
13157
+ if (!d.overlays.length) delete d.overlays;
13158
+ if (legacy) delete d.endCard;
13159
+ const named = [
13160
+ ...ending.map((o) => o.id),
13161
+ ...legacy ? ["its endCard"] : []
13162
+ ].join(", ");
13163
+ notes.push(
13164
+ `${label}: dropped ${named}, which played after its footage and would land over the next input (only the last input's ending stays, at the end of the film)`
13165
+ );
13166
+ }
13167
+ function segs0(d) {
13168
+ return d.segments.length ? d.segments[0] : { in: 0, out: round32(d.source.meta.durationMs / 1e3) };
13169
+ }
13170
+ function joinDocs(parts) {
13171
+ if (parts.length < 2)
13172
+ throw new UsageError("vos join needs two inputs or more");
13173
+ const [first, ...rest] = parts;
13174
+ const doc = structuredClone(first.doc);
13175
+ const notes = [];
13176
+ dropEnding(doc, first.label, notes);
13177
+ const copies = /* @__PURE__ */ new Map();
13178
+ const copy = (c) => {
13179
+ if (!copies.has(c.to)) copies.set(c.to, c);
13180
+ };
13181
+ const actions = /* @__PURE__ */ new Map([["", first.actions]]);
13182
+ if (first.file) {
13183
+ copy({
13184
+ from: first.file.path,
13185
+ to: takeRelativeFile(doc.source.videoKey),
13186
+ remux: first.file.ext
13187
+ });
13188
+ } else {
13189
+ const refs = [
13190
+ ...isTakeRelativeKey(doc.source.videoKey) ? [{ where: "recording", key: doc.source.videoKey, set: () => {
13191
+ } }] : [],
13192
+ ...sourceSidecarRefs(doc),
13193
+ ...docMediaRefs(doc)
13194
+ ];
13195
+ for (const ref of refs)
13196
+ copy({
13197
+ from: join24(first.dir, takeRelativeFile(ref.key)),
13198
+ to: takeRelativeFile(ref.key),
13199
+ base: first.dir
13200
+ });
13201
+ }
13202
+ if (!doc.segments.length)
13203
+ doc.segments = [{ in: 0, out: round32(doc.source.meta.durationMs / 1e3) }];
13204
+ const taken = new Set((doc.media ?? []).map((m) => m.id));
13205
+ const allocate = () => {
13206
+ let n = 1;
13207
+ while (taken.has(`m${n}`)) n++;
13208
+ taken.add(`m${n}`);
13209
+ return `m${n}`;
13210
+ };
13211
+ const media = [...doc.media ?? []];
13212
+ const joined = [];
13213
+ const placed = [];
13214
+ for (const part of rest) {
13215
+ const d = structuredClone(migrateMotion3(part.doc));
13216
+ if (part !== rest[rest.length - 1]) dropEnding(d, part.label, notes);
13217
+ const pid = allocate();
13218
+ const idMap = /* @__PURE__ */ new Map([["", pid]]);
13219
+ for (const m of d.media ?? []) idMap.set(m.id, allocate());
13220
+ const mapMedia = (k) => idMap.get(mediaKey2(k)) ?? pid;
13221
+ joined.push({ label: part.label, id: pid });
13222
+ actions.set(pid, part.actions);
13223
+ if (part.file) {
13224
+ const to = `${pid}/recording.${part.file.ext}`;
13225
+ copy({ from: part.file.path, to, remux: part.file.ext });
13226
+ d.source.videoKey = to;
13227
+ } else {
13228
+ const refs = [
13229
+ ...isTakeRelativeKey(d.source.videoKey) ? [
13230
+ {
13231
+ where: "recording",
13232
+ key: d.source.videoKey,
13233
+ set: (next) => {
13234
+ d.source.videoKey = next;
13235
+ }
13236
+ }
13237
+ ] : [],
13238
+ ...sourceSidecarRefs(d),
13239
+ ...(d.media ?? []).flatMap(
13240
+ (m) => [
13241
+ ["videoKey", m.videoKey],
13242
+ ["micKey", m.micKey],
13243
+ ["camKey", m.camKey]
13244
+ ].filter(([, k]) => isTakeRelativeKey(k)).map(([field, k]) => ({
13245
+ where: `media ${m.id}`,
13246
+ key: k,
13247
+ set: (next) => {
13248
+ ;
13249
+ m[field] = next;
13250
+ }
13251
+ }))
13252
+ ),
13253
+ ...docOverlayRefs(d),
13254
+ ...docObjectRefs(d)
13255
+ ];
13256
+ for (const ref of refs) {
13257
+ const to = prefixed(pid, ref.key);
13258
+ copy({
13259
+ from: join24(part.dir, takeRelativeFile(ref.key)),
13260
+ to,
13261
+ base: part.dir
13262
+ });
13263
+ ref.set(to);
13264
+ }
13265
+ }
13266
+ media.push({
13267
+ ...d.source,
13268
+ id: pid,
13269
+ ...part.kind === "file" ? { frame: { ...FULL_BLEED_FRAME } } : {}
13270
+ });
13271
+ for (const m of d.media ?? []) media.push({ ...m, id: idMap.get(m.id) });
13272
+ const segs = d.segments.length ? d.segments : [{ in: 0, out: round32(d.source.meta.durationMs / 1e3) }];
13273
+ const at2 = doc.segments.length;
13274
+ doc.segments.push(...segs.map((s) => ({ ...s, media: mapMedia(s.media) })));
13275
+ const ns = (id) => `${pid}-${id}`;
13276
+ const tag = (s) => ({
13277
+ ...s,
13278
+ id: ns(s.id),
13279
+ media: mapMedia(s.media)
13280
+ });
13281
+ doc.zoom = [...doc.zoom, ...d.zoom.map(tag)];
13282
+ if (d.speed?.length) doc.speed = [...doc.speed ?? [], ...d.speed.map(tag)];
13283
+ if (d.tilt?.length) doc.tilt = [...doc.tilt ?? [], ...d.tilt.map(tag)];
13284
+ if (d.camMotion?.length)
13285
+ doc.camMotion = [...doc.camMotion ?? [], ...d.camMotion.map(tag)];
13286
+ if (d.freeze?.length)
13287
+ doc.freeze = [...doc.freeze ?? [], ...d.freeze.map(tag)];
13288
+ if (d.rejected?.length)
13289
+ doc.rejected = [...doc.rejected ?? [], ...d.rejected.map(tag)];
13290
+ if (d.audio.length)
13291
+ notes.push(
13292
+ `${part.label}: its ${d.audio.length} sound clip${d.audio.length === 1 ? "" : "s"} stay behind (the film's sound is the first input's; add a score with vos score)`
13293
+ );
13294
+ placed.push({ part, d, pid, firstSegment: at2, mapMedia });
13295
+ }
13296
+ if (media.length) doc.media = media;
13297
+ const extents = segmentOutputExtents3(doc);
13298
+ for (const { part, d, pid, firstSegment, mapMedia } of placed) {
13299
+ const offset = extents[firstSegment]?.start ?? 0;
13300
+ const ownPieces = ratedSegments8(d);
13301
+ const ns = (id) => `${pid}-${id}`;
13302
+ let dropped = 0;
13303
+ for (const o of d.overlays ?? []) {
13304
+ const next = { ...o, id: ns(o.id) };
13305
+ if (next.kind !== "text" && next.kind !== "html") {
13306
+ const ref = mediaRefId(next.key);
13307
+ if (ref !== null) next.key = `media:${mapMedia(ref)}`;
13308
+ }
13309
+ if (o.anchor) {
13310
+ next.anchor = { ...o.anchor, media: mapMedia(o.anchor.media) };
13311
+ } else if (o.pin?.step !== void 0) {
13312
+ const r = resolveAnchor5(d, { step: o.pin.step }, ownPieces);
13313
+ if (r.start !== null) {
13314
+ next.anchor = {
13315
+ step: o.pin.step,
13316
+ media: pid,
13317
+ offset: round32(o.start - r.start)
13318
+ };
13319
+ } else {
13320
+ next.start = round32(o.start + offset);
13321
+ }
13322
+ delete next.pin;
13323
+ dropped++;
13324
+ } else {
13325
+ if (o.pin) {
13326
+ delete next.pin;
13327
+ dropped++;
13328
+ }
13329
+ next.start = round32(o.start + offset);
13330
+ }
13331
+ ;
13332
+ (doc.overlays ??= []).push(next);
13333
+ }
13334
+ if (dropped)
13335
+ notes.push(
13336
+ `${part.label}: ${dropped} pinned layer${dropped === 1 ? "" : "s"} kept ${dropped === 1 ? "its" : "their"} time and lost the pin (a pin places a layer on the first input's footage only)`
13337
+ );
13338
+ if (part.file?.hasAudio) {
13339
+ const seg = segs0(d);
13340
+ const at2 = extents[firstSegment];
13341
+ if (part.mute) {
13342
+ notes.push(`${part.label}: its own sound is left out (--mute)`);
13343
+ } else if (seg && at2) {
13344
+ const len = Math.max(0, seg.out - seg.in);
13345
+ const fade = round32(Math.min(FILE_SOUND_FADE, len / 4));
13346
+ const id = `${pid}-sound`;
13347
+ doc.audio = [
13348
+ ...doc.audio,
13349
+ {
13350
+ id,
13351
+ key: d.source.videoKey,
13352
+ name: basename11(part.file.path),
13353
+ start: round32(at2.start),
13354
+ in: round32(seg.in),
13355
+ out: round32(seg.out),
13356
+ duration: round32(d.source.meta.durationMs / 1e3),
13357
+ gain: 1,
13358
+ fadeIn: fade,
13359
+ fadeOut: fade
13360
+ }
13361
+ ];
13362
+ notes.push(
13363
+ `${part.label}: its own sound plays as audio clip ${id}, ${at2.start.toFixed(2)}..${(at2.start + len).toFixed(2)}s (--mute ${part.label} to leave it out)`
13364
+ );
13365
+ }
13366
+ }
13367
+ for (const ob of d.objects ?? []) {
13368
+ ;
13369
+ (doc.objects ??= []).push({
13370
+ ...ob,
13371
+ id: ns(ob.id),
13372
+ ...ob.span ? { span: { ...ob.span, start: round32(ob.span.start + offset) } } : {}
13373
+ });
13374
+ }
13375
+ }
13376
+ for (const part of rest)
13377
+ if (part.mute && !part.file?.hasAudio)
13378
+ notes.push(
13379
+ `${part.label}: --mute leaves nothing out (${part.file ? "the file carries no sound" : "a take's own footage sound is its recording's; its added sound stays behind anyway"})`
13380
+ );
13381
+ if (first.mute)
13382
+ notes.push(
13383
+ `${first.label}: --mute leaves nothing out (the first input's sound is the film's own)`
13384
+ );
13385
+ const pieces = ratedSegments8(doc);
13386
+ for (const o of doc.overlays ?? []) {
13387
+ if (!o.anchor) continue;
13388
+ const r = resolveAnchor5(doc, o.anchor, pieces);
13389
+ if (r.start !== null) o.start = r.start;
13390
+ }
13391
+ return { doc, copies: [...copies.values()], media: joined, actions, notes };
13392
+ }
13393
+ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set(["json", "help", "force", "captions"]);
13394
+ var MULTI = /* @__PURE__ */ new Set(["mute"]);
13395
+ var USAGE2 = "vos join <take> <take|file.mp4>\u2026 --out <dir> [--force] [--captions] [--mute <file>]... [--json]";
13396
+ var within = (inner, outer) => {
13397
+ const rel = relative4(outer, inner);
13398
+ return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute3(rel);
13399
+ };
13400
+ async function readPart(arg) {
13401
+ const path = resolve15(arg);
13402
+ if (!existsSync22(path)) throw new UsageError(`${arg}: no such take or file`);
13403
+ if (statSync7(path).isDirectory()) {
13404
+ if (!existsSync22(join24(path, "meta.json")))
13405
+ throw new UsageError(
13406
+ `${arg}: not a take directory (no meta.json); a video file is joined by its path`
13407
+ );
13408
+ const take = await loadTake(path);
13409
+ const doc2 = take.doc ?? projectFromArtifact2(
13410
+ {
13411
+ videoKey: take.paths.recordingName,
13412
+ cursor: take.cursor,
13413
+ meta: take.meta
13414
+ },
13415
+ take.paths.recordingName
13416
+ ).doc;
13417
+ return { label: arg, kind: "take", doc: doc2, dir: path, actions: take.actions };
13418
+ }
13419
+ const probed = await probeVideoFile(path);
13420
+ if (!(probed.durationMs > 0))
13421
+ throw new UsageError(`${arg}: the file reports no duration`);
13422
+ const doc = fileDoc(
13423
+ `recording.${probed.ext}`,
13424
+ probed,
13425
+ Math.round(statSync7(path).mtimeMs)
13426
+ );
13427
+ return {
13428
+ label: arg,
13429
+ kind: "file",
13430
+ doc,
13431
+ dir: dirname9(path),
13432
+ actions: null,
13433
+ file: { path, ext: probed.ext, hasAudio: probed.hasAudio }
13434
+ };
13435
+ }
13436
+ async function cmdJoin(argv) {
13437
+ const { positionals, flags, multi } = parseArgs(argv, BOOLEAN_FLAGS12, MULTI);
13438
+ const r = createReporter(flags.json === true);
13439
+ const outArg = strFlag(flags, "out");
13440
+ const force = flags.force === true;
13441
+ const withCaptions = flags.captions === true;
13442
+ if (positionals.length < 2 || !outArg) throw new UsageError(USAGE2);
13443
+ const out = resolve15(outArg);
13444
+ const inputs = positionals.map((p) => resolve15(p));
13445
+ const muted = /* @__PURE__ */ new Set();
13446
+ for (const m of multi.mute ?? []) {
13447
+ const i = inputs.indexOf(resolve15(m));
13448
+ if (i < 0)
13449
+ throw new UsageError(
13450
+ `--mute ${m}: not one of the inputs; name a file as it is given to vos join`
13451
+ );
13452
+ muted.add(i);
13453
+ }
13454
+ for (const [i, input] of inputs.entries()) {
13455
+ if (within(out, input) || within(input, out))
13456
+ throw new UsageError(
13457
+ `--out ${outArg} overlaps the input ${positionals[i]}; write the joined take somewhere of its own`
13458
+ );
13459
+ }
13460
+ if (existsSync22(out)) {
13461
+ if (!force)
13462
+ throw new UsageError(
13463
+ `--out ${outArg} exists; pass --force to replace it, or name a new directory`
13464
+ );
13465
+ const isTake = existsSync22(join24(out, "doc.json")) || existsSync22(join24(out, "meta.json"));
13466
+ if (!isTake && readdirSync5(out).length > 0)
13467
+ throw new UsageError(
13468
+ `--force replaces a take directory, and ${outArg} holds other files; name a new directory`
13469
+ );
13470
+ if (within(process.cwd(), out))
13471
+ throw new UsageError(
13472
+ `--out ${outArg} is the working directory or above it; name a new directory`
13473
+ );
13474
+ }
13475
+ const parts = [];
13476
+ for (const [i, p] of positionals.entries()) {
13477
+ r.log(`reading ${p}\u2026`);
13478
+ parts.push({
13479
+ ...await readPart(p),
13480
+ ...muted.has(i) ? { mute: true } : {}
13481
+ });
13482
+ }
13483
+ const plan = joinDocs(parts);
13484
+ let captions = null;
13485
+ if (withCaptions) {
13486
+ captions = landStepCaptions(plan.doc, plan.actions);
13487
+ const labelOf = new Map([
13488
+ ["", positionals[0]],
13489
+ ...plan.media.map((m) => [m.id, m.label])
13490
+ ]);
13491
+ for (const k of captions.uncaptioned)
13492
+ plan.notes.push(
13493
+ `no step in ${labelOf.get(k) ?? `media ${k}`} carries a caption`
13494
+ );
13495
+ }
13496
+ const lint = lintDoc(plan.doc);
13497
+ if (lint.problems.length) {
13498
+ r.log(
13499
+ `the joined document does not validate:
13500
+ ${lint.problems.join("\n ")}`
13501
+ );
13502
+ return EXIT_ERROR;
13503
+ }
13504
+ for (const c of plan.copies) {
13505
+ if (!c.base) continue;
13506
+ const found = localFile(relative4(c.base, c.from), c.base);
13507
+ if ("refused" in found)
13508
+ throw new Error(
13509
+ `${relative4(process.cwd(), c.from) || c.from}, which an input's doc.json names, ${found.refused}; bring its media home first (vos fetch --media) or fix the key`
13510
+ );
13511
+ c.from = found.file;
13512
+ }
13513
+ if (existsSync22(out)) await rm3(out, { recursive: true, force: true });
13514
+ await mkdir10(out, { recursive: true });
13515
+ for (const c of plan.copies) {
13516
+ const to = join24(out, c.to);
13517
+ await mkdir10(dirname9(to), { recursive: true });
13518
+ if (c.remux) {
13519
+ const remuxed = await copyRecording(c.from, to, c.remux);
13520
+ if (!remuxed && c.remux !== "mov")
13521
+ plan.notes.push(
13522
+ `could not read ${basename11(c.from)}'s audio in Node, so it was copied as it is; its length comes from the container`
13523
+ );
13524
+ } else await copyFile2(c.from, to);
13525
+ }
13526
+ const doc = plan.doc;
13527
+ await writeJson(join24(out, "meta.json"), doc.source.meta, true);
13528
+ if (doc.source.cursor.length)
13529
+ await writeJson(join24(out, "cursor.json"), doc.source.cursor);
13530
+ if (parts[0].actions)
13531
+ await writeJson(join24(out, "actions.json"), parts[0].actions, true);
13532
+ await writeJson(join24(out, "doc.json"), doc, true);
13533
+ const extents = segmentOutputExtents3(doc);
13534
+ const length = extents.length ? extents[extents.length - 1].end : 0;
13535
+ for (const n of plan.notes) r.log(`note: ${n}`);
13536
+ for (const w of lint.warnings) r.log(`warning: ${w}`);
13537
+ const captionLine = captions ? `
13538
+ ${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} landed` + (captions.skipped.length ? `; not landed: ${captions.skipped.join("; ")}` : "") : "";
13539
+ r.done(
13540
+ {
13541
+ out,
13542
+ primary: positionals[0],
13543
+ media: plan.media,
13544
+ segments: doc.segments.length,
13545
+ seconds: round32(length),
13546
+ files: plan.copies.map((c) => c.to),
13547
+ notes: plan.notes,
13548
+ warnings: lint.warnings,
13549
+ ...captions ? { captions } : {}
13550
+ },
13551
+ `Joined ${parts.length} inputs \u2192 ${out} (${length.toFixed(1)}s, ${doc.segments.length} segments)
13552
+ ${positionals[0]} is the primary; ${plan.media.map((m) => `${m.label} \u2192 ${m.id}`).join(", ")}${captionLine}
13553
+ Next: vos frames ${outArg} --times 0,25%,50%,75%,100%; vos render ${outArg}`
13554
+ );
13555
+ return EXIT_OK;
13556
+ }
13557
+
13558
+ // src/plugin/score.ts
13559
+ import { existsSync as existsSync23 } from "fs";
13560
+ import {
13561
+ copyFile as copyFile3,
13562
+ mkdir as mkdir11,
13563
+ mkdtemp,
13564
+ readFile as readFile14,
13565
+ rm as rm4,
13566
+ writeFile as writeFile11
13567
+ } from "fs/promises";
13568
+ import { tmpdir } from "os";
13569
+ import { basename as basename12, isAbsolute as isAbsolute4, join as join25, relative as relative5, resolve as resolve16, sep as sep3 } from "path";
13570
+ import {
13571
+ docMediaList as docMediaList2,
13572
+ docOutputDuration as docOutputDuration3,
13573
+ findStep as findStep3,
13574
+ ratedSegments as ratedSegments9,
13575
+ resolveAnchor as resolveAnchor6
13576
+ } from "@vosjs/studio-core";
13577
+ var SCORE_ID = "score";
13578
+ var MID_SONG_FADE_IN = 0.8;
13579
+ var BLOCK_SECONDS = 0.1;
13580
+ var WINDOW_SECONDS = 1;
13581
+ var HOP_SECONDS = 0.5;
13582
+ var FLOOR_DB = -70;
13583
+ function kWeighting(sampleRate) {
13584
+ const shelf = (() => {
13585
+ const f0 = 1681.974450955533;
13586
+ const G = 3.999843853973347;
13587
+ const Q = 0.7071752369554196;
13588
+ const K = Math.tan(Math.PI * f0 / sampleRate);
13589
+ const Vh = Math.pow(10, G / 20);
13590
+ const Vb = Math.pow(Vh, 0.4996667741545416);
13591
+ const a0 = 1 + K / Q + K * K;
13592
+ return {
13593
+ b0: (Vh + Vb * K / Q + K * K) / a0,
13594
+ b1: 2 * (K * K - Vh) / a0,
13595
+ b2: (Vh - Vb * K / Q + K * K) / a0,
13596
+ a1: 2 * (K * K - 1) / a0,
13597
+ a2: (1 - K / Q + K * K) / a0
13598
+ };
13599
+ })();
13600
+ const highPass = (() => {
13601
+ const f0 = 38.13547087602444;
13602
+ const Q = 0.5003270373238773;
13603
+ const K = Math.tan(Math.PI * f0 / sampleRate);
13604
+ const a0 = 1 + K / Q + K * K;
13605
+ return {
13606
+ b0: 1,
13607
+ b1: -2,
13608
+ b2: 1,
13609
+ a1: 2 * (K * K - 1) / a0,
13610
+ a2: (1 - K / Q + K * K) / a0
13611
+ };
13612
+ })();
13613
+ return [shelf, highPass];
13614
+ }
13615
+ var LoudnessMeter = class {
13616
+ constructor(sampleRate, channels) {
13617
+ this.sampleRate = sampleRate;
13618
+ this.channels = channels;
13619
+ this.blockLen = Math.max(1, Math.round(sampleRate * BLOCK_SECONDS));
13620
+ const weighted = channels >= 1 && channels <= 2 && sampleRate >= 8e3;
13621
+ this.unit = weighted ? "LUFS" : "dBFS";
13622
+ this.filters = weighted ? kWeighting(sampleRate) : null;
13623
+ this.state = Array.from({ length: channels }, () => [
13624
+ [0, 0, 0, 0],
13625
+ [0, 0, 0, 0]
13626
+ ]);
13627
+ this.acc = new Array(channels).fill(0);
13628
+ }
13629
+ sampleRate;
13630
+ channels;
13631
+ unit;
13632
+ blockLen;
13633
+ filters;
13634
+ /** Per channel, per stage: [x1, x2, y1, y2]. */
13635
+ state;
13636
+ acc;
13637
+ filled = 0;
13638
+ blocks = [];
13639
+ /** One chunk: a Float32Array per channel, all the same length. */
13640
+ push(planes) {
13641
+ const n = planes[0]?.length ?? 0;
13642
+ for (let i = 0; i < n; i++) {
13643
+ for (let c = 0; c < this.channels; c++) {
13644
+ let v = planes[c]?.[i] ?? 0;
13645
+ if (this.filters) {
13646
+ for (let k = 0; k < 2; k++) {
13647
+ const f = this.filters[k];
13648
+ const s = this.state[c][k];
13649
+ const y = f.b0 * v + f.b1 * s[0] + f.b2 * s[1] - f.a1 * s[2] - f.a2 * s[3];
13650
+ s[1] = s[0];
13651
+ s[0] = v;
13652
+ s[3] = s[2];
13653
+ s[2] = y;
13654
+ v = y;
13655
+ }
13656
+ }
13657
+ this.acc[c] += v * v;
13658
+ }
13659
+ if (++this.filled === this.blockLen) this.flush();
13660
+ }
13661
+ }
13662
+ flush() {
13663
+ let power = 0;
13664
+ for (let c = 0; c < this.channels; c++) {
13665
+ power += this.acc[c] / this.filled;
13666
+ this.acc[c] = 0;
13667
+ }
13668
+ if (this.unit === "dBFS") power /= Math.max(1, this.channels);
13669
+ this.blocks.push(power);
13670
+ this.filled = 0;
13671
+ }
13672
+ /** The blocks, the last partial one included. */
13673
+ finish() {
13674
+ if (this.filled > this.blockLen / 2) this.flush();
13675
+ return this.blocks;
13676
+ }
13677
+ };
13678
+ function levelCurve(blocks, unit) {
13679
+ const per = Math.round(WINDOW_SECONDS / BLOCK_SECONDS);
13680
+ const hop = Math.round(HOP_SECONDS / BLOCK_SECONDS);
13681
+ const out = [];
13682
+ for (let i = 0; i + per <= blocks.length; i += hop) {
13683
+ let sum = 0;
13684
+ for (let j = i; j < i + per; j++) sum += blocks[j];
13685
+ const mean = sum / per;
13686
+ const db = mean > 0 ? (unit === "LUFS" ? -0.691 : 0) + 10 * Math.log10(mean) : -Infinity;
13687
+ out.push({
13688
+ t: +((i + per / 2) * BLOCK_SECONDS).toFixed(3),
13689
+ level: +Math.max(FLOOR_DB, db).toFixed(2)
13690
+ });
13691
+ }
13692
+ return out;
13693
+ }
13694
+ var SWELL_LOOKBACK = 15;
13695
+ var SWELL_SUSTAIN = 1.5;
13696
+ var SWELL_HOLD_DB = 3;
13697
+ var SWELL_ARRIVE_DB = 1;
13698
+ var SWELL_MIN_RISE = 3;
13699
+ var SWELL_FLOOR_DB = 12;
13700
+ var SWELL_SETTLE = 5;
13701
+ function findSwell(curve) {
13702
+ const step = HOP_SECONDS;
13703
+ const back = Math.round(SWELL_LOOKBACK / step);
13704
+ const hold = Math.round(SWELL_SUSTAIN / step);
13705
+ const floor = median2(curve.map((p) => p.level)) - SWELL_FLOOR_DB;
13706
+ const begun = curve.findIndex((p) => p.level >= floor);
13707
+ if (begun < 0) return null;
13708
+ const settled = begun + Math.round(SWELL_SETTLE / step);
13709
+ const levelAt = (j) => Math.max(curve[j].level, floor);
13710
+ let best = null;
13711
+ for (let i = settled + 1; i < curve.length; i++) {
13712
+ const p = curve[i];
13713
+ let held = true;
13714
+ for (let k = i + 1; k <= i + hold; k++) {
13715
+ if (k >= curve.length || curve[k].level < p.level - SWELL_HOLD_DB) {
13716
+ held = false;
13717
+ break;
13718
+ }
13719
+ }
13720
+ if (!held) continue;
13721
+ let lo = i;
13722
+ for (let j = Math.max(settled, i - back); j < i; j++)
13723
+ if (levelAt(j) < levelAt(lo)) lo = j;
13724
+ const rise = p.level - levelAt(lo);
13725
+ if (rise < SWELL_MIN_RISE) continue;
13726
+ if (i + 1 < curve.length && curve[i + 1].level > p.level) continue;
13727
+ if (best && rise <= best.rise + 1e-9) continue;
13728
+ let arrive = i;
13729
+ for (let k = lo; k <= i; k++) {
13730
+ if (curve[k].level >= p.level - SWELL_ARRIVE_DB) {
13731
+ arrive = k;
13732
+ break;
13733
+ }
13734
+ }
13735
+ best = {
13736
+ at: curve[arrive].t,
13737
+ from: curve[lo].t,
13738
+ peak: p.t,
13739
+ rise: +rise.toFixed(2),
13740
+ peakLevel: p.level
13741
+ };
13742
+ }
13743
+ return best;
13744
+ }
13745
+ var median2 = (xs) => {
13746
+ if (!xs.length) return FLOOR_DB;
13747
+ const s = [...xs].sort((a, b) => a - b);
13748
+ const m = Math.floor(s.length / 2);
13749
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
13750
+ };
13751
+ function firstAboveMedian(curve) {
13752
+ const m = median2(curve.map((p) => p.level));
13753
+ const hit = curve.find((p) => p.level > m);
13754
+ return hit ? Math.max(0, +(hit.t - WINDOW_SECONDS / 2).toFixed(3)) : 0;
13755
+ }
13756
+ var r3 = (v) => Math.round(v * 1e3) / 1e3;
13757
+ function planScore(input) {
13758
+ const { trackDuration, film, swell } = input;
13759
+ const notes = [];
13760
+ let start;
13761
+ if (input.at !== null) {
13762
+ if (!swell) {
13763
+ notes.push(
13764
+ "the track has no swell (no sustained rise of 3 dB or more); it starts at its first rise instead"
13765
+ );
13766
+ start = firstAboveMedian(input.curve);
13767
+ } else {
13768
+ start = swell.at - input.at;
13769
+ if (start < 0) {
13770
+ notes.push(
13771
+ `the swell (${swell.at.toFixed(1)}s into the track) comes before ${input.at.toFixed(1)}s of film can play ahead of it; the track starts at its top and the swell lands at ${swell.at.toFixed(1)}s`
13772
+ );
13773
+ start = 0;
13774
+ }
13775
+ }
13776
+ } else {
13777
+ start = firstAboveMedian(input.curve);
13778
+ }
13779
+ if (trackDuration < film) {
13780
+ notes.push(
13781
+ `the track (${trackDuration.toFixed(1)}s) is shorter than the film (${film.toFixed(1)}s); it cannot cover it, so it starts at its top and ends early`
13782
+ );
13783
+ start = 0;
13784
+ } else if (start + film > trackDuration) {
13785
+ const fit = Math.max(0, trackDuration - film);
13786
+ notes.push(
13787
+ `from ${start.toFixed(1)}s the track ends before the film does; it starts at ${fit.toFixed(1)}s instead, so the film is covered${swell && input.at !== null ? ` and the swell lands at ${(swell.at - fit).toFixed(1)}s` : ""}`
13788
+ );
13789
+ start = fit;
13790
+ }
13791
+ const out = Math.min(trackDuration, start + film);
13792
+ const swellAt = swell && swell.at >= start && swell.at <= out ? r3(swell.at - start) : null;
13793
+ return { in: r3(start), out: r3(out), swellAt, notes };
13794
+ }
13795
+ function sparkline(curve, width = 64) {
13796
+ if (!curve.length) return "";
13797
+ const glyphs = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
13798
+ const step = Math.max(1, Math.ceil(curve.length / width));
13799
+ const pts = [];
13800
+ for (let i = 0; i < curve.length; i += step) {
13801
+ let m = -Infinity;
13802
+ for (let j = i; j < Math.min(curve.length, i + step); j++)
13803
+ m = Math.max(m, curve[j].level);
13804
+ pts.push(m);
13805
+ }
13806
+ const hi = Math.max(...pts);
13807
+ const lo = Math.max(Math.min(...pts), hi - 24);
13808
+ return pts.map(
13809
+ (v) => glyphs[hi > lo ? Math.max(
13810
+ 0,
13811
+ Math.min(
13812
+ glyphs.length - 1,
13813
+ Math.floor((v - lo) / (hi - lo) * glyphs.length)
13814
+ )
13815
+ ) : 0]
13816
+ ).join("");
13817
+ }
13818
+ async function measureInNode(path) {
13819
+ const MB = await import("mediabunny");
13820
+ const input = new MB.Input({
13821
+ formats: MB.ALL_FORMATS,
13822
+ source: new MB.FilePathSource(path)
13823
+ });
13824
+ try {
13825
+ const track = await input.getPrimaryAudioTrack();
13826
+ if (!track) throw new UsageError(`${path} has no audio track`);
13827
+ if (!await track.canDecode()) return null;
13828
+ const meter = new LoudnessMeter(track.sampleRate, track.numberOfChannels);
13829
+ const sink = new MB.AudioSampleSink(track);
13830
+ let frames = 0;
13831
+ for await (const sample2 of sink.samples()) {
13832
+ const planes = [];
13833
+ for (let c = 0; c < track.numberOfChannels; c++) {
13834
+ const a = new Float32Array(sample2.numberOfFrames);
13835
+ sample2.copyTo(a, { planeIndex: c, format: "f32-planar" });
13836
+ planes.push(a);
13837
+ }
13838
+ frames += sample2.numberOfFrames;
13839
+ sample2.close();
13840
+ meter.push(planes);
13841
+ }
13842
+ return {
13843
+ sampleRate: track.sampleRate,
13844
+ channels: track.numberOfChannels,
13845
+ duration: frames / track.sampleRate,
13846
+ unit: meter.unit,
13847
+ blocks: meter.finish(),
13848
+ via: "node"
13849
+ };
13850
+ } finally {
13851
+ input.dispose();
13852
+ }
13853
+ }
13854
+ var PAGE_CHUNK_SECONDS = 10;
13855
+ async function measureInBrowser(path) {
13856
+ const bytes = await readFile14(path);
13857
+ const browser = await launchBrowser();
13858
+ try {
13859
+ const page = await browser.newPage();
13860
+ const info = await page.evaluate(`(async () => {
13861
+ const b64 = ${JSON.stringify(bytes.toString("base64"))}
13862
+ const bin = atob(b64)
13863
+ const u8 = new Uint8Array(bin.length)
13864
+ for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i)
13865
+ const ctx = new OfflineAudioContext(1, 1, 48000)
13866
+ const buf = await ctx.decodeAudioData(u8.buffer)
13867
+ window.__vosScore = buf
13868
+ return { sampleRate: buf.sampleRate, channels: buf.numberOfChannels, length: buf.length }
13869
+ })()`);
13870
+ const meter = new LoudnessMeter(info.sampleRate, info.channels);
13871
+ const chunk = Math.round(info.sampleRate * PAGE_CHUNK_SECONDS);
13872
+ for (let from = 0; from < info.length; from += chunk) {
13873
+ const parts = await page.evaluate(`(() => {
13874
+ const buf = window.__vosScore
13875
+ const out = []
13876
+ for (let c = 0; c < buf.numberOfChannels; c++) {
13877
+ const a = buf.getChannelData(c).subarray(${from}, ${from + chunk})
13878
+ const u8 = new Uint8Array(a.buffer, a.byteOffset, a.byteLength)
13879
+ let s = ''
13880
+ for (let i = 0; i < u8.length; i += 0x8000)
13881
+ s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000))
13882
+ out.push(btoa(s))
13883
+ }
13884
+ return out
13885
+ })()`);
13886
+ meter.push(
13887
+ parts.map((p) => {
13888
+ const b = Buffer.from(p, "base64");
13889
+ return new Float32Array(b.buffer, b.byteOffset, b.byteLength / 4);
13890
+ })
13891
+ );
13892
+ }
13893
+ return {
13894
+ sampleRate: info.sampleRate,
13895
+ channels: info.channels,
13896
+ duration: info.length / info.sampleRate,
13897
+ unit: meter.unit,
13898
+ blocks: meter.finish(),
13899
+ via: "browser"
13900
+ };
13901
+ } finally {
13902
+ await browser.close();
13903
+ }
13904
+ }
13905
+ async function measureTrack(path) {
13906
+ return await measureInNode(path) ?? await measureInBrowser(path);
13907
+ }
13908
+ function resolveAt(doc, raw) {
13909
+ const trimmed = raw.trim();
13910
+ if (/^\d+(\.\d+)?s?$/.test(trimmed)) {
13911
+ const at2 = parseFloat(trimmed);
13912
+ return { at: at2, via: `${at2}s` };
13913
+ }
13914
+ const v = trimmed.startsWith("step:") ? trimmed.slice(5) : trimmed;
13915
+ const pieces = ratedSegments9(doc);
13916
+ const colon = v.indexOf(":");
13917
+ const named = colon > 0 ? { media: v.slice(0, colon), step: v.slice(colon + 1) } : null;
13918
+ const candidates = named ? [named] : docMediaList2(doc).filter(({ source }) => findStep3(source.meta.steps ?? [], v)).map(({ id }) => ({ media: id, step: v }));
13919
+ if (!candidates.length)
13920
+ throw new UsageError(
13921
+ `--at ${raw}: no step by that id or index in this take (and not a number of seconds)`
13922
+ );
13923
+ if (candidates.length > 1)
13924
+ throw new UsageError(
13925
+ `--at ${raw}: steps of several media carry it; say which, as <media>:<step> (${candidates.map((c2) => `${c2.media || "primary"}`).join(", ")})`
13926
+ );
13927
+ const c = candidates[0];
13928
+ const r = resolveAnchor6(
13929
+ doc,
13930
+ { step: c.step, ...c.media ? { media: c.media } : {} },
13931
+ pieces
13932
+ );
13933
+ if (r.start === null)
13934
+ throw new UsageError(
13935
+ `--at ${raw}: that step ${r.miss === "cut" ? "is cut out of the film" : r.miss === "skipped" ? "was skipped by the recorder" : "is not in the take"}`
13936
+ );
13937
+ return { at: r.start, via: `step ${v} (${r.start.toFixed(2)}s)` };
13938
+ }
13939
+ async function resolveTrack(dir, ref, flags, dryRun) {
13940
+ const local = resolve16(ref);
13941
+ if (existsSync23(local)) {
13942
+ const rel = relative5(resolve16(dir), local);
13943
+ const inside = !!rel && !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute4(rel);
13944
+ const key = inside ? rel.split(sep3).join("/") : `music/${basename12(local)}`;
13945
+ if (!inside && !dryRun) {
13946
+ await mkdir11(join25(dir, "music"), { recursive: true });
13947
+ await copyFile3(local, join25(dir, key));
13948
+ }
13949
+ return { path: local, key, name: basename12(local) };
13950
+ }
13951
+ const origin = platformOrigin({
13952
+ origin: strFlag(flags, "origin"),
13953
+ api: strFlag(flags, "api")
13954
+ });
13955
+ const catalog = await fetchMusicCatalog(origin);
13956
+ const track = catalog.tracks.find((t) => t.slug === ref);
13957
+ if (!track)
13958
+ throw new UsageError(
13959
+ `--track ${ref}: no such file, and no catalog track by that slug (some: ${catalog.tracks.slice(0, 6).map((t) => t.slug).join(", ")}; the list is GET ${origin}/api/music)`
13960
+ );
13961
+ const res = await fetch(track.url);
13962
+ if (!res.ok) throw new Error(`GET ${track.url} answered ${res.status}`);
13963
+ const tmp = await mkdtemp(join25(tmpdir(), "vos-score-"));
13964
+ const path = join25(tmp, basename12(new URL(track.url).pathname) || "track");
13965
+ await writeFile11(path, new Uint8Array(await res.arrayBuffer()));
13966
+ return {
13967
+ path,
13968
+ key: track.url,
13969
+ name: track.title,
13970
+ cleanup: () => rm4(tmp, { recursive: true, force: true })
13971
+ };
13972
+ }
13973
+ var BOOLEAN_FLAGS13 = /* @__PURE__ */ new Set(["json", "help", "dry-run"]);
13974
+ var USAGE3 = "vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]";
13975
+ async function cmdScore(argv) {
13976
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS13);
13977
+ const dir = positionals[0];
13978
+ const trackRef = strFlag(flags, "track");
13979
+ if (!dir || !trackRef) throw new UsageError(USAGE3);
13980
+ const r = createReporter(flags.json === true);
13981
+ const dryRun = flags["dry-run"] === true;
13982
+ const gain = numFlag(flags, "gain", 0.5);
13983
+ if (!(gain >= 0 && gain <= 1))
13984
+ throw new UsageError("--gain expects a number from 0 to 1");
13985
+ const fadeOut = numFlag(flags, "fade-out", 1.5);
13986
+ if (!(fadeOut >= 0)) throw new UsageError("--fade-out expects seconds \u2265 0");
13987
+ const fadeInFlag = strFlag(flags, "fade-in");
13988
+ const fadeInAsked = numFlag(flags, "fade-in", MID_SONG_FADE_IN);
13989
+ if (!(fadeInAsked >= 0)) throw new UsageError("--fade-in expects seconds \u2265 0");
13990
+ const take = await loadTake(dir);
13991
+ if (!take.doc)
13992
+ throw new UsageError(`${dir} has no doc.json; run vos plan first`);
13993
+ const doc = take.doc;
13994
+ const atRaw = strFlag(flags, "at");
13995
+ const at2 = atRaw !== void 0 ? resolveAt(doc, atRaw) : null;
13996
+ const track = await resolveTrack(dir, trackRef, flags, dryRun);
13997
+ let measured;
13998
+ try {
13999
+ r.log(`measuring ${track.name}\u2026`);
14000
+ measured = await measureTrack(track.path);
14001
+ } finally {
14002
+ await track.cleanup?.();
14003
+ }
14004
+ const curve = levelCurve(measured.blocks, measured.unit);
14005
+ const swell = findSwell(curve);
14006
+ const film = docOutputDuration3(doc);
14007
+ const plan = planScore({
14008
+ trackDuration: measured.duration,
14009
+ film,
14010
+ curve,
14011
+ swell,
14012
+ at: at2 ? at2.at : null
14013
+ });
14014
+ const clip = {
14015
+ id: SCORE_ID,
14016
+ key: track.key,
14017
+ name: track.name,
14018
+ start: 0,
14019
+ in: plan.in,
14020
+ out: plan.out,
14021
+ duration: r3(measured.duration),
14022
+ gain,
14023
+ // A track entered mid-song opens on a fade, never a cut into the bar;
14024
+ // one that starts at its own top needs none unless asked.
14025
+ fadeIn: Math.min(
14026
+ fadeInFlag !== void 0 || plan.in > 0 ? fadeInAsked : 0,
14027
+ Math.max(0, plan.out - plan.in)
14028
+ ),
14029
+ fadeOut: Math.min(fadeOut, Math.max(0, plan.out - plan.in)),
14030
+ ...doc.source.micKey ? { duck: true } : {}
14031
+ };
14032
+ const levels = curve.map((p) => p.level);
14033
+ const envelope = {
14034
+ unit: measured.unit,
14035
+ weighted: measured.unit === "LUFS",
14036
+ decoder: measured.via,
14037
+ sampleRate: measured.sampleRate,
14038
+ channels: measured.channels,
14039
+ min: levels.length ? Math.min(...levels) : null,
14040
+ median: levels.length ? median2(levels) : null,
14041
+ max: levels.length ? Math.max(...levels) : null,
14042
+ points: curve
14043
+ };
14044
+ const others = doc.audio.filter((a) => a.id !== SCORE_ID && a.start < 0.05);
14045
+ if (others.length)
14046
+ plan.notes.push(
14047
+ `${others.map((a) => a.id).join(", ")} also play${others.length === 1 ? "s" : ""} from the top; two tracks under one film rarely mean to`
14048
+ );
14049
+ if (!dryRun) {
14050
+ doc.audio = [...doc.audio.filter((a) => a.id !== SCORE_ID), clip];
14051
+ await writeJson(take.paths.doc, doc, true);
14052
+ }
14053
+ const label = measured.unit === "LUFS" ? "LUFS, K-weighted" : "dBFS RMS";
14054
+ const swellLine = swell ? `swell: +${swell.rise.toFixed(1)} dB from ${swell.from.toFixed(1)}s, arriving ${swell.at.toFixed(1)}s into the track (peak ${swell.peakLevel.toFixed(1)} ${measured.unit} at ${swell.peak.toFixed(1)}s)` : "swell: none (no sustained rise of 3 dB or more)";
14055
+ const placeLine = at2 && plan.swellAt !== null ? `the track starts at ${plan.in.toFixed(2)}s, so the swell lands at ${plan.swellAt.toFixed(2)}s of film (asked: ${at2.via})` : `the track starts at ${plan.in.toFixed(2)}s${at2 ? ` (asked: ${at2.via})` : ", its first rise above its median level"}`;
14056
+ r.done(
14057
+ {
14058
+ take: resolve16(dir),
14059
+ dryRun,
14060
+ track: { key: track.key, name: track.name, duration: clip.duration },
14061
+ film: r3(film),
14062
+ swell,
14063
+ at: at2?.at ?? null,
14064
+ clip,
14065
+ notes: plan.notes,
14066
+ envelope
14067
+ },
14068
+ `${dryRun ? "Would score" : "Scored"} ${dir} with ${track.name} (${measured.duration.toFixed(1)}s, ${label})
14069
+ ${sparkline(curve)}
14070
+ ${swellLine}
14071
+ ${placeLine}; it plays ${plan.in.toFixed(2)}..${plan.out.toFixed(2)} under the ${film.toFixed(1)}s film at gain ${gain}${plan.notes.length ? `
14072
+ note: ${plan.notes.join("\n note: ")}` : ""}${dryRun ? "\n (dry run: doc.json untouched)" : `
14073
+ audio clip "${SCORE_ID}" ${doc.audio.length > 1 ? "written beside the others" : "written"} in doc.json`}`
14074
+ );
14075
+ return EXIT_OK;
14076
+ }
14077
+
14078
+ // src/plugin/look.ts
14079
+ import { mkdir as mkdir12 } from "fs/promises";
14080
+ import { existsSync as existsSync24 } from "fs";
14081
+ import { join as join26, resolve as resolve17 } from "path";
14082
+ import { headersFor, urlSchemeProblem } from "@vosjs/render-core/record";
14083
+ function quote(value) {
14084
+ return `"${value.replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
14085
+ }
14086
+ var NAME_MAX = 80;
14087
+ function scopeSelector(s, kind) {
14088
+ if (s.testId) return `[${s.testId.attr}=${quote(s.testId.value)}]`;
14089
+ let base;
14090
+ if (s.role) base = `[role=${quote(s.role)}]`;
14091
+ else if (kind === "dialog" && s.tag === "dialog") base = "dialog[open]";
14092
+ else if (kind === "dialog") base = '[aria-modal="true"]';
14093
+ else base = s.tag;
14094
+ return s.label ? `${base}[aria-label=${quote(s.label)}]` : base;
14095
+ }
14096
+ function stableId(id) {
14097
+ if (!id || !/^[A-Za-z][\w-]*$/.test(id)) return false;
14098
+ if (/^(radix|headlessui|mui|react|ember|ext|rc|el|v)-/i.test(id)) return false;
14099
+ return !/\d{2,}/.test(id);
14100
+ }
14101
+ function selectorCandidates(c) {
14102
+ const out = [];
14103
+ const add = (selector, basis) => {
14104
+ if (!out.some((x) => x.selector === selector)) out.push({ selector, basis });
14105
+ };
14106
+ const dialog = c.dialog ? scopeSelector(c.dialog, "dialog") : null;
14107
+ const landmark = c.landmark ? scopeSelector(c.landmark, "landmark") : null;
14108
+ const css = (sel) => dialog ? `${dialog} ${sel}` : sel;
14109
+ const chain = (sel) => dialog ? `${dialog} >> ${sel}` : sel;
14110
+ if (c.testId) {
14111
+ const sel = `[${c.testId.attr}=${quote(c.testId.value)}]`;
14112
+ add(sel, "testid");
14113
+ if (dialog) add(`${dialog} ${sel}`, "testid");
14114
+ }
14115
+ if (c.ariaLabel && c.ariaLabel.length <= NAME_MAX)
14116
+ add(css(`[aria-label=${quote(c.ariaLabel)}]`), "aria-label");
14117
+ if (c.tag === "a" && c.hrefAttr && c.hrefAttr.length <= 200 && !/^\s*javascript:/i.test(c.hrefAttr) && c.hrefAttr.trim() !== "#")
14118
+ add(css(`a[href=${quote(c.hrefAttr)}]`), "href");
14119
+ if (c.role && c.name && c.name.length <= NAME_MAX) {
14120
+ const sel = `role=${c.role}[name=${quote(c.name)}]`;
14121
+ add(chain(sel), "role");
14122
+ if (!dialog && landmark) add(`${landmark} >> ${sel}`, "role");
14123
+ }
14124
+ const form = /^(input|textarea|select)$/.test(c.tag);
14125
+ if (form && c.nameAttr)
14126
+ add(css(`${c.tag}[name=${quote(c.nameAttr)}]`), "form");
14127
+ if (form && c.placeholder && c.placeholder.length <= NAME_MAX)
14128
+ add(css(`${c.tag}[placeholder=${quote(c.placeholder)}]`), "form");
14129
+ if (stableId(c.id)) add(`#${c.id}`, "id");
14130
+ if (!form && c.text && c.text.length <= 60) {
14131
+ const sel = `text=${quote(c.text)}`;
14132
+ if (dialog) add(`${dialog} >> ${sel}`, "text");
14133
+ else if (landmark) add(`${landmark} >> ${sel}`, "text");
14134
+ else add(sel, "text");
14135
+ }
14136
+ return out;
14137
+ }
14138
+ function judgeMatch(index, reached) {
14139
+ if (!reached.includes(String(index))) return "miss";
14140
+ return reached.length === 1 ? "unique" : "ambiguous";
14141
+ }
14142
+ function chooseProposal(index, candidates, reached) {
14143
+ let fallback = null;
14144
+ for (let i = 0; i < candidates.length; i++) {
14145
+ const verdict = judgeMatch(index, reached[i] ?? []);
14146
+ if (verdict === "unique")
14147
+ return {
14148
+ selector: candidates[i].selector,
14149
+ basis: candidates[i].basis,
14150
+ match: "unique",
14151
+ matches: 1
14152
+ };
14153
+ if (verdict === "ambiguous" && !fallback)
14154
+ fallback = { c: candidates[i], n: reached[i].length };
14155
+ }
14156
+ if (fallback)
14157
+ return {
14158
+ selector: fallback.c.selector,
14159
+ basis: fallback.c.basis,
14160
+ match: "ambiguous",
14161
+ matches: fallback.n
14162
+ };
14163
+ return { selector: null, basis: null, match: "none", matches: 0 };
14164
+ }
14165
+ var SPEND_STEMS = [
14166
+ "delet",
14167
+ "remov",
14168
+ "revok",
14169
+ "eras",
14170
+ "discard",
14171
+ "publish",
14172
+ "deploy",
14173
+ "purchas",
14174
+ "subscri",
14175
+ "unsubscri",
14176
+ "pay",
14177
+ "buy",
14178
+ "generat",
14179
+ "submit",
14180
+ "send",
14181
+ "transfer",
14182
+ "upgrad",
14183
+ "confirm",
14184
+ "leav",
14185
+ "destroy"
14186
+ ];
14187
+ var SPEND_PHRASES = [
14188
+ "move to trash",
14189
+ "empty trash",
14190
+ "cancel subscription",
14191
+ "cancel plan",
14192
+ "close account"
14193
+ ];
14194
+ var SPEND = new RegExp(
14195
+ `\\b(?:${SPEND_PHRASES.map((p) => p.replace(/ /g, "\\s+")).join("|")}|(?:${SPEND_STEMS.join("|")})[a-z]*)`,
14196
+ "i"
14197
+ );
14198
+ function spendWord(words2) {
14199
+ for (const w of words2) {
14200
+ const m = w ? SPEND.exec(w.replace(/\s+/g, " ")) : null;
14201
+ if (m) return m[0].toLowerCase();
14202
+ }
14203
+ return null;
14204
+ }
14205
+ function refusalLine(selector, label, word) {
14206
+ return `refused: --click ${selector} reaches "${label}": "${word}" spends, sends or destroys, and vos look never presses one. Look at the page before it; a flow that needs the press has it as a step in actions.json, for vos record`;
14207
+ }
14208
+ var ENGINES = /^(css|xpath|text|role|id|nth|visible|data-testid|data-test-id|data-test|internal:[a-z-]+)$/;
14209
+ function splitTypeFlag(value) {
14210
+ let depth = 0;
14211
+ let inQuote = null;
14212
+ let segment = 0;
14213
+ for (let i = 0; i < value.length; i++) {
14214
+ const ch = value[i];
14215
+ if (inQuote) {
14216
+ if (ch === "\\") i++;
14217
+ else if (ch === inQuote) inQuote = null;
14218
+ continue;
14219
+ }
14220
+ if (ch === '"' || ch === "'") inQuote = ch;
14221
+ else if (ch === "[" || ch === "(") depth++;
14222
+ else if ((ch === "]" || ch === ")") && depth > 0) depth--;
14223
+ else if (ch === ">" && value[i + 1] === ">") {
14224
+ segment = i + 2;
14225
+ i++;
14226
+ } else if (ch === "=" && depth === 0) {
14227
+ if (ENGINES.test(value.slice(segment, i).trim())) continue;
14228
+ const selector = value.slice(0, i).trim();
14229
+ if (!selector) break;
14230
+ return { selector, text: value.slice(i + 1) };
14231
+ }
14232
+ }
14233
+ throw new UsageError(
14234
+ `--type expects <selector>=<text>, got "${value}" (the text follows the first = outside the selector's brackets and quotes)`
14235
+ );
14236
+ }
14237
+ function lookSteps(argv) {
14238
+ const steps = [];
14239
+ for (let i = 0; i < argv.length; i++) {
14240
+ const arg = argv[i];
14241
+ if (arg === "--") break;
14242
+ const m = /^--(click|type)(?:=(.*))?$/s.exec(arg);
14243
+ if (!m) continue;
14244
+ let value = m[2];
14245
+ if (value === void 0) {
14246
+ value = argv[i + 1];
14247
+ if (value === void 0 || value.startsWith("--"))
14248
+ throw new UsageError(`--${m[1]} expects a value`);
14249
+ i++;
14250
+ }
14251
+ if (m[1] === "click") {
14252
+ if (!value.trim()) throw new UsageError("--click expects a selector");
14253
+ steps.push({ do: "click", selector: value });
14254
+ } else steps.push({ do: "type", ...splitTypeFlag(value) });
14255
+ }
14256
+ return steps;
14257
+ }
14258
+ function parseViewport2(value) {
14259
+ if (!value) return { width: 1280, height: 720 };
14260
+ const m = /^(\d+)x(\d+)$/i.exec(value);
14261
+ if (!m) throw new UsageError("--viewport expects WxH, like 1280x720");
14262
+ const width = Number(m[1]);
14263
+ const height = Number(m[2]);
14264
+ if (width < 200 || height < 200 || width > 3840 || height > 2160)
14265
+ throw new UsageError("--viewport wants a size from 200x200 to 3840x2160");
14266
+ return { width, height };
14267
+ }
14268
+ function displayName(c) {
14269
+ const words2 = c.name || c.text || c.placeholder || "";
14270
+ return words2.length > 60 ? `${words2.slice(0, 59).trimEnd()}\u2026` : words2;
14271
+ }
14272
+ function controlLine(c) {
14273
+ const what = c.role && c.role !== c.tag ? `${c.tag} (${c.role})` : c.tag;
14274
+ const name = c.redacted?.length ? ` [${c.redacted.join(", ")} withheld]` : c.name ? ` ${JSON.stringify(c.name)}` : "";
14275
+ const flags = [
14276
+ c.inDialog ? "in dialog" : "",
14277
+ c.disabled ? "disabled" : "",
14278
+ c.covered ? "covered" : ""
14279
+ ].filter(Boolean);
14280
+ const proposal = c.selector ? `${c.selector} ${c.match === "unique" ? "UNIQUE" : `AMBIGUOUS (${c.matches})`}` : "no stable selector (use its rect)";
14281
+ const r = c.rect;
14282
+ return ` ${what}${name}${flags.length ? ` [${flags.join(", ")}]` : ""}
14283
+ ${proposal} @ ${r.x},${r.y} ${r.w}\xD7${r.h}`;
14284
+ }
14285
+ var CONTROL_LIMIT = 150;
14286
+ var LOOK_PROBE = `(() => {
14287
+ const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
14288
+ const vw = innerWidth, vh = innerHeight
14289
+ const CONTROLS = 'button, a, [role=button], [role=tab], [role=menuitem], [role=menuitemcheckbox], [role=menuitemradio], [role=checkbox], [role=switch], [role=option], [role=radio], [role=link], input, textarea, select, [contenteditable], [data-testid], [data-test-id], [data-test], [data-cy]'
14290
+ const TEST_IDS = ['data-testid', 'data-test-id', 'data-test', 'data-cy']
14291
+ const DIALOGS = 'dialog[open], [role=dialog], [role=alertdialog], [aria-modal=true]'
14292
+ const LANDMARKS = 'nav, header, main, aside, footer, [role=navigation], [role=banner], [role=main], [role=complementary], [role=contentinfo], [role=search]'
14293
+ const squash = (s) => (s || '').replace(/\\s+/g, ' ').trim()
14294
+ const textOf = (el) => squash(typeof el.innerText === 'string' ? el.innerText : el.textContent)
14295
+ const shown = (el) => {
14296
+ const r = el.getBoundingClientRect()
14297
+ if (r.width < 2 || r.height < 2) return null
14298
+ if (el.checkVisibility && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return null
14299
+ const s = getComputedStyle(el)
14300
+ if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
14301
+ return r
14302
+ }
14303
+ const testIdOf = (el) => {
14304
+ for (const attr of TEST_IDS) { const value = el.getAttribute(attr); if (value) return { attr, value } }
14305
+ return null
14306
+ }
14307
+ const implicitRole = (el) => {
14308
+ const t = el.tagName
14309
+ if (t === 'BUTTON') return 'button'
14310
+ if (t === 'A') return el.hasAttribute('href') ? 'link' : ''
14311
+ if (t === 'TEXTAREA') return 'textbox'
14312
+ if (t === 'SELECT') return el.multiple || el.size > 1 ? 'listbox' : 'combobox'
14313
+ if (t === 'INPUT') {
14314
+ const type = (el.getAttribute('type') || 'text').toLowerCase()
14315
+ if (type === 'checkbox' || type === 'radio') return type
14316
+ if (type === 'range') return 'slider'
14317
+ if (type === 'number') return 'spinbutton'
14318
+ if (/^(button|submit|reset|image)$/.test(type)) return 'button'
14319
+ if (/^(text|email|tel|url|search)$/.test(type)) return el.hasAttribute('list') ? 'combobox' : type === 'search' ? 'searchbox' : 'textbox'
14320
+ return ''
14321
+ }
14322
+ if (el.isContentEditable && el.hasAttribute('contenteditable')) return 'textbox'
14323
+ return ''
14324
+ }
14325
+ const roleOf = (el) => (el.getAttribute('role') || '').trim().split(/\\s+/)[0] || implicitRole(el)
14326
+ const nameOf = (el) => {
14327
+ const aria = squash(el.getAttribute('aria-label'))
14328
+ if (aria) return aria
14329
+ const by = el.getAttribute('aria-labelledby')
14330
+ if (by) {
14331
+ const t = squash(by.split(/\\s+/).map((id) => document.getElementById(id)).filter(Boolean).map(textOf).join(' '))
14332
+ if (t) return t
14333
+ }
14334
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) {
14335
+ const type = (el.getAttribute('type') || '').toLowerCase()
14336
+ if (/^(button|submit|reset)$/.test(type)) return squash(el.value) || (type === 'submit' ? 'Submit' : type === 'reset' ? 'Reset' : '')
14337
+ if (type === 'image') return squash(el.getAttribute('alt'))
14338
+ const label = el.labels ? squash([...el.labels].map(textOf).join(' ')) : ''
14339
+ return label || squash(el.getAttribute('title')) || squash(el.getAttribute('placeholder'))
14340
+ }
14341
+ const text = textOf(el)
14342
+ if (text) return text
14343
+ const img = el.querySelector('img[alt]')
14344
+ return squash(img && img.getAttribute('alt')) || squash(el.getAttribute('title'))
14345
+ }
14346
+ const scopeOf = (el) => el ? {
14347
+ tag: el.tagName.toLowerCase(),
14348
+ role: (el.getAttribute('role') || '').trim().split(/\\s+/)[0] || '',
14349
+ label: squash(el.getAttribute('aria-label')) || null,
14350
+ testId: testIdOf(el),
14351
+ } : null
14352
+ const hrefOf = (el) => {
14353
+ if (el.tagName !== 'A' || !el.hasAttribute('href')) return null
14354
+ try { const u = new URL(el.href, location.href); const at = u.pathname + u.search; return u.origin === location.origin ? at : u.origin + at } catch { return null }
14355
+ }
14356
+ const out = []
14357
+ let offscreen = 0, more = 0
14358
+ for (const el of document.querySelectorAll(CONTROLS)) {
14359
+ if (el.tagName === 'INPUT' && (el.getAttribute('type') || '').toLowerCase() === 'hidden') continue
14360
+ if (el.getAttribute('contenteditable') === 'false') continue
14361
+ if (el.tagName === 'A' && !el.hasAttribute('href') && !el.getAttribute('role') && !testIdOf(el)) continue
14362
+ const r = shown(el)
14363
+ if (!r) continue
14364
+ if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) { offscreen++; continue }
14365
+ if (out.length >= ${CONTROL_LIMIT}) { more++; continue }
14366
+ const dlg = el.closest(DIALOGS)
14367
+ const lm = el.closest(LANDMARKS)
14368
+ let name = nameOf(el), text = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) ? '' : textOf(el)
14369
+ let ariaLabel = squash(el.getAttribute('aria-label')) || null
14370
+ let placeholder = squash(el.getAttribute('placeholder')) || null
14371
+ let hrefAttr = el.tagName === 'A' ? el.getAttribute('href') : null, href = hrefOf(el)
14372
+ const kinds = [...new Set([...kindsIn(name), ...kindsIn(text), ...kindsIn(ariaLabel || ''), ...kindsIn(placeholder || ''), ...kindsIn(hrefAttr || '')])]
14373
+ if (kinds.length) { name = ''; text = ''; ariaLabel = null; placeholder = null; hrefAttr = null; href = null }
14374
+ const cx = Math.min(vw - 1, Math.max(0, r.left + r.width / 2)), cy = Math.min(vh - 1, Math.max(0, r.top + r.height / 2))
14375
+ const hit = document.elementFromPoint(cx, cy)
14376
+ const covered = !!hit && !(hit === el || el.contains(hit) || (el.labels && [...el.labels].some((l) => l.contains(hit))))
14377
+ const index = out.length
14378
+ out.push({
14379
+ index,
14380
+ tag: el.tagName.toLowerCase(),
14381
+ role: roleOf(el),
14382
+ name,
14383
+ text,
14384
+ ariaLabel,
14385
+ testId: testIdOf(el),
14386
+ id: el.id || null,
14387
+ nameAttr: /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) ? el.getAttribute('name') || null : null,
14388
+ placeholder,
14389
+ href,
14390
+ hrefAttr,
14391
+ rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) },
14392
+ disabled: el.matches(':disabled') || el.getAttribute('aria-disabled') === 'true',
14393
+ covered,
14394
+ dialog: dlg && shown(dlg) ? scopeOf(dlg) : null,
14395
+ landmark: lm ? scopeOf(lm) : null,
14396
+ ...(kinds.length ? { redacted: kinds } : {}),
14397
+ })
14398
+ el.setAttribute('data-vos-look', String(index))
14399
+ }
14400
+ return { controls: out, offscreen, more }
14401
+ })()`;
14402
+ var UNSTAMP = `(() => { for (const el of document.querySelectorAll('[data-vos-look]')) el.removeAttribute('data-vos-look') })()`;
14403
+ function settleScript(quietMs, capMs) {
14404
+ return `new Promise((resolve) => {
14405
+ let last = performance.now()
14406
+ const start = last
14407
+ const mo = new MutationObserver(() => { last = performance.now() })
14408
+ mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true })
14409
+ const tick = () => {
14410
+ const now = performance.now()
14411
+ if (now - last >= ${quietMs} || now - start >= ${capMs}) {
14412
+ mo.disconnect()
14413
+ resolve(now - last >= ${quietMs})
14414
+ } else setTimeout(tick, 50)
14415
+ }
14416
+ tick()
14417
+ })`;
14418
+ }
14419
+ var QUIET_MS = 800;
14420
+ var SETTLE_CAP_MS = 6e3;
14421
+ var STEP_TIMEOUT_MS = 8e3;
14422
+ async function settle(page) {
14423
+ const started = Date.now();
14424
+ for (let tries = 0; tries < 3; tries++) {
14425
+ await page.waitForLoadState("load", { timeout: 15e3 }).catch(() => {
14426
+ });
14427
+ const left = Math.max(QUIET_MS, SETTLE_CAP_MS - (Date.now() - started));
14428
+ try {
14429
+ return await page.evaluate(settleScript(QUIET_MS, left));
14430
+ } catch {
14431
+ }
14432
+ }
14433
+ return false;
14434
+ }
14435
+ var LookRefusal = class extends Error {
14436
+ constructor(message) {
14437
+ super(message);
14438
+ this.name = "LookRefusal";
14439
+ }
14440
+ };
14441
+ var place = (u) => {
14442
+ try {
14443
+ const x = new URL(u);
14444
+ return `${x.origin}${x.pathname}`;
14445
+ } catch {
14446
+ return u;
14447
+ }
14448
+ };
14449
+ function shownUrl(u) {
14450
+ try {
14451
+ const x = new URL(u);
14452
+ if (!x.username && !x.password) return u;
14453
+ x.username = "";
14454
+ x.password = "";
14455
+ return x.href;
14456
+ } catch {
14457
+ return u;
14458
+ }
14459
+ }
14460
+ async function lookAt(page, url, opts) {
14461
+ const log = opts.log ?? (() => {
14462
+ });
14463
+ log(`goto ${shownUrl(url)}`);
14464
+ const response = await page.goto(url, { waitUntil: "load", timeout: 45e3 }).catch((e) => {
14465
+ if (e instanceof Error && /net::ERR_/.test(e.message))
14466
+ throw new Error(
14467
+ `could not open ${shownUrl(url)}: ${(e.message.split("\n")[0] ?? "").split(url).join(shownUrl(url))}`
14468
+ );
14469
+ log(" (the load event never fired; looking anyway)");
14470
+ return null;
14471
+ });
14472
+ let settled = await settle(page);
14473
+ const seen = await page.evaluate(WALL_PROBE).catch(() => null);
14474
+ const wall = wallVerdict({
14475
+ askedUrl: url,
14476
+ landedUrl: page.url(),
14477
+ ...response ? { status: response.status() } : {},
14478
+ passwordFields: seen?.passwordFields ?? 0,
14479
+ newPasswordField: seen?.newPasswordField ?? false,
14480
+ oneTimeCodeField: seen?.oneTimeCodeField ?? false
14481
+ });
14482
+ const ran = [];
14483
+ if (!wall || wall.level === "soft") {
14484
+ for (const step2 of opts.steps ?? []) {
14485
+ const all = page.locator(step2.selector);
14486
+ const target = all.first();
14487
+ try {
14488
+ await target.waitFor({ state: "visible", timeout: STEP_TIMEOUT_MS });
14489
+ } catch {
14490
+ throw new UsageError(
14491
+ `--${step2.do} ${step2.selector}: no visible element matches it after ${STEP_TIMEOUT_MS / 1e3} s. Look without the step and take a selector from the list`
14492
+ );
14493
+ }
14494
+ const count = await all.count();
14495
+ const note = count > 1 ? `the first of ${count} matches` : void 0;
14496
+ if (step2.do === "click") {
14497
+ const words2 = await target.evaluate(
14498
+ (el) => [
14499
+ el.getAttribute("aria-label"),
14500
+ el.innerText || el.textContent,
14501
+ el.value,
14502
+ el.getAttribute("title"),
14503
+ el.getAttribute("alt")
14504
+ ].map(
14505
+ (s) => typeof s === "string" ? s.replace(/\s+/g, " ").trim() : ""
14506
+ )
14507
+ ).catch(() => []);
14508
+ const word = spendWord(words2);
14509
+ if (word) {
14510
+ const label = words2.find((w) => spendWord([w])) ?? word;
14511
+ throw new LookRefusal(
14512
+ refusalLine(step2.selector, label.slice(0, 60), word)
14513
+ );
14514
+ }
14515
+ log(`click ${step2.selector}${note ? ` (${note})` : ""}`);
14516
+ await target.click({ timeout: STEP_TIMEOUT_MS });
14517
+ } else {
14518
+ log(
14519
+ `type ${step2.text.length} chars into ${step2.selector}${note ? ` (${note})` : ""}`
14520
+ );
14521
+ await target.fill(step2.text, { timeout: STEP_TIMEOUT_MS });
14522
+ }
14523
+ ran.push({
14524
+ do: step2.do,
14525
+ selector: step2.selector,
14526
+ ...note ? { note } : {}
14527
+ });
14528
+ settled = await settle(page);
14529
+ }
14530
+ }
14531
+ if (opts.waitMs && opts.waitMs > 0) await page.waitForTimeout(opts.waitMs);
14532
+ await mkdir12(opts.outDir, { recursive: true });
14533
+ const screenshot = join26(opts.outDir, "look.png");
14534
+ await page.screenshot({ path: screenshot });
14535
+ const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
14536
+ const step = ran.length ? ran.length - 1 : -1;
14537
+ const exposures = (scan?.exposures ?? []).map((e) => ({
14538
+ step,
14539
+ ...e,
14540
+ seen: 1
14541
+ }));
14542
+ const probe3 = await page.evaluate(LOOK_PROBE);
14543
+ const controls = [];
14544
+ try {
14545
+ for (const raw of probe3.controls) {
14546
+ const candidates = selectorCandidates(raw);
14547
+ const reached = [];
14548
+ for (const c of candidates) {
14549
+ const stamps = await page.locator(c.selector).evaluateAll(
14550
+ (els) => els.map((el) => el.getAttribute("data-vos-look"))
14551
+ ).catch(() => []);
14552
+ reached.push(stamps);
14553
+ if (judgeMatch(raw.index, stamps) === "unique") break;
14554
+ }
14555
+ const proposal = chooseProposal(raw.index, candidates, reached);
14556
+ controls.push({
14557
+ tag: raw.tag,
14558
+ role: raw.role,
14559
+ name: displayName(raw),
14560
+ testId: raw.testId?.value ?? null,
14561
+ href: raw.href,
14562
+ rect: raw.rect,
14563
+ disabled: raw.disabled,
14564
+ inDialog: raw.dialog !== null,
14565
+ covered: raw.covered,
14566
+ ...proposal,
14567
+ ...raw.redacted ? { redacted: raw.redacted } : {}
14568
+ });
14569
+ }
14570
+ } finally {
14571
+ await page.evaluate(UNSTAMP).catch(() => {
14572
+ });
14573
+ }
14574
+ return {
14575
+ url: shownUrl(url),
14576
+ landed: place(page.url()),
14577
+ screenshot,
14578
+ viewport: page.viewportSize() ?? { width: 0, height: 0 },
14579
+ controls,
14580
+ offscreen: probe3.offscreen,
14581
+ more: probe3.more,
14582
+ exposures,
14583
+ wall,
14584
+ steps: ran,
14585
+ settled
14586
+ };
14587
+ }
14588
+ var USAGE4 = "vos look <url> [--session <name> | --storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--out <dir>] [--json]";
14589
+ function lookReport(r) {
14590
+ const inDialog = r.controls.filter((c) => c.inDialog).length;
14591
+ const head = `Looked at ${r.landed} (${r.viewport.width}\xD7${r.viewport.height}): ${r.screenshot}${r.settled ? "" : "\n note: the page never held still for 0.8 s; looked at it as it was"}`;
14592
+ const steps = r.steps.length ? `
14593
+ after ${r.steps.map((s) => `${s.do} ${s.selector}${s.note ? ` (${s.note})` : ""}`).join(", then ")}` : "";
14594
+ const wall = r.wall ? `
14595
+ ${r.wall.level === "hard" ? "WALL" : "WARNING"}: ${r.wall.message}${r.wall.level === "hard" ? "\n The steps did not run; this is the page in front of the one asked for." : ""}` : "";
14596
+ const count = `
14597
+ ${r.controls.length} control${r.controls.length === 1 ? "" : "s"} in view${inDialog ? ` (${inDialog} in an open dialog)` : ""}${r.offscreen ? `, ${r.offscreen} more outside it (scroll, or a taller --viewport)` : ""}${r.more ? `, ${r.more} past the listing's limit` : ""}:`;
14598
+ const list = r.controls.map(controlLine).join("\n");
14599
+ const exposed = r.exposures.length ? `
14600
+ EXPOSED in the frame (${r.exposures.length}):
14601
+ ${r.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
14602
+ ${EXPOSURE_ADVICE}` : "";
14603
+ const next = `
14604
+ Take a UNIQUE selector into actions.json as it is; an AMBIGUOUS one reaches several and the recorder takes the first. Then rehearse: vos record --actions actions.json --dry-run`;
14605
+ return `${head}${steps}${wall}${count}
14606
+ ${list}${exposed}${next}`;
14607
+ }
14608
+ async function cmdLook(argv) {
14609
+ const steps = lookSteps(argv);
14610
+ const { positionals, flags, multi } = parseArgs(
14611
+ argv,
14612
+ /* @__PURE__ */ new Set(["json", "headed"]),
14613
+ /* @__PURE__ */ new Set(["click", "type", "header", "browser-arg"])
14614
+ );
14615
+ void multi.click;
14616
+ void multi.type;
14617
+ const url = positionals[0];
14618
+ if (!url) throw new UsageError(USAGE4);
14619
+ const scheme = urlSchemeProblem(url);
14620
+ if (scheme) throw new UsageError(scheme);
14621
+ const r = createReporter(flags.json === true);
14622
+ const viewport = parseViewport2(strFlag(flags, "viewport"));
14623
+ const outDir = resolve17(strFlag(flags, "out") ?? "look");
14624
+ const headed = flags.headed === true;
14625
+ const waitRaw = strFlag(flags, "wait");
14626
+ const waitMs = waitRaw === void 0 ? 0 : Number(waitRaw);
14627
+ if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 6e4)
14628
+ throw new UsageError("--wait expects milliseconds, 0 to 60000");
14629
+ const storageRaw = strFlag(flags, "storage-state");
14630
+ const storageState = storageRaw ? resolve17(storageRaw) : void 0;
14631
+ if (storageState && !existsSync24(storageState))
14632
+ throw new UsageError(`--storage-state: no such file: ${storageState}`);
14633
+ const sessionName = strFlag(flags, "session");
14634
+ if (sessionName) {
14635
+ const bad = sessionNameVerdict(sessionName);
14636
+ if (bad) throw new UsageError(bad);
14637
+ if (storageState)
14638
+ throw new UsageError(
14639
+ "--session and --storage-state are two doors to one page: a persistent profile cannot take a storage state. Pass one"
14640
+ );
14641
+ if (!existsSync24(sessionDir(sessionName)))
14642
+ throw new UsageError(
14643
+ `no session "${sessionName}". Make one: vos session open <url> --name ${sessionName} (a Chrome window opens; sign in and close it)`
14644
+ );
14645
+ }
14646
+ let headers;
14647
+ try {
14648
+ headers = parseHeaders(multi.header);
14649
+ } catch (e) {
14650
+ throw new UsageError(e instanceof Error ? e.message : String(e));
14651
+ }
14652
+ const browserArgs = (multi["browser-arg"] ?? []).filter(Boolean);
14653
+ const inContext = async (ctx) => {
14654
+ if (Object.keys(headers).length)
14655
+ await ctx.route("**/*", async (route) => {
14656
+ const req = route.request();
14657
+ const scoped = headersFor(req.url(), url, headers);
14658
+ await route.continue(
14659
+ scoped ? { headers: { ...req.headers(), ...scoped } } : void 0
14660
+ );
14661
+ });
14662
+ const page = ctx.pages()[0] ?? await ctx.newPage();
14663
+ await page.setViewportSize(viewport);
14664
+ return lookAt(page, url, { steps, waitMs, outDir, log: r.log });
14665
+ };
14666
+ let result;
14667
+ try {
14668
+ if (sessionName) {
14669
+ result = await withSession(sessionName, inContext, {
14670
+ headless: !headed,
14671
+ viewport
14672
+ });
14673
+ } else {
14674
+ const browser = await launchBrowser(browserArgs, { headless: !headed });
14675
+ try {
14676
+ const ctx = await browser.newContext({
14677
+ viewport,
14678
+ deviceScaleFactor: 1,
14679
+ ...storageState ? { storageState } : {}
14680
+ });
14681
+ result = await inContext(ctx);
14682
+ } finally {
14683
+ await browser.close();
14684
+ }
14685
+ }
14686
+ } catch (e) {
14687
+ if (e instanceof LookRefusal) {
14688
+ r.done({ ok: false, refused: true, error: e.message }, e.message);
14689
+ return EXIT_USAGE;
14690
+ }
14691
+ throw e;
14692
+ }
14693
+ r.done(
14694
+ { ok: !result.wall || result.wall.level === "soft", ...result },
14695
+ lookReport(result)
14696
+ );
14697
+ return result.wall?.level === "hard" ? EXIT_WALL : EXIT_OK;
14698
+ }
14699
+
14700
+ // src/plugin/machineCmd.ts
14701
+ import { spawnSync as spawnSync4 } from "child_process";
14702
+ import {
14703
+ cpSync,
14704
+ existsSync as existsSync26,
14705
+ mkdirSync as mkdirSync6,
14706
+ readFileSync as readFileSync11,
14707
+ rmSync as rmSync3,
14708
+ writeFileSync as writeFileSync6
14709
+ } from "fs";
14710
+ import { homedir as homedir2 } from "os";
14711
+ import { join as join28 } from "path";
14712
+ import { chromium as chromium2 } from "playwright";
14713
+
14714
+ // src/plugin/machine.ts
14715
+ import { existsSync as existsSync25, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
14716
+ import { dirname as dirname10, join as join27 } from "path";
14717
+ var AGENT_NAMES = [
14718
+ "claude",
14719
+ "cursor",
14720
+ "codex",
14721
+ "copilot"
14722
+ ];
14723
+ var PROJECT_DIRS = {
14724
+ claude: [".claude"],
14725
+ cursor: [".cursor"],
14726
+ codex: [".codex", ".agents"],
14727
+ copilot: [".github/copilot"]
14728
+ };
14729
+ var HOME_DIRS = {
14730
+ claude: [".claude"],
14731
+ cursor: [".cursor"],
14732
+ codex: [".codex", ".agents"],
14733
+ copilot: [".copilot"]
14734
+ };
14735
+ function detectAgents(root, opts = {}) {
14736
+ const exists = opts.exists ?? existsSync25;
14737
+ const out = [];
14738
+ for (const agent of AGENT_NAMES) {
14739
+ for (const rel of PROJECT_DIRS[agent]) {
14740
+ const dir = join27(root, rel);
14741
+ if (exists(dir)) out.push({ agent, dir, scope: "project" });
14742
+ }
14743
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join27(root, ".github", "copilot-instructions.md"))) {
14744
+ out.push({
14745
+ agent,
14746
+ dir: join27(root, ".github", "copilot"),
14747
+ scope: "project"
14748
+ });
14749
+ }
14750
+ }
14751
+ if (opts.global && opts.home) {
14752
+ for (const agent of AGENT_NAMES) {
14753
+ for (const rel of HOME_DIRS[agent]) {
14754
+ const dir = join27(opts.home, rel);
14755
+ if (exists(dir)) out.push({ agent, dir, scope: "global" });
14756
+ }
14757
+ }
14758
+ }
14759
+ return out;
14760
+ }
14761
+ function agentsFromFlag(value, root) {
14762
+ const wanted = value.trim() === "all" ? [...AGENT_NAMES] : value.split(",").map((s) => s.trim()).filter(Boolean);
14763
+ const bad = wanted.filter((w) => !AGENT_NAMES.includes(w));
14764
+ if (bad.length)
14765
+ return {
14766
+ error: `--agent takes ${AGENT_NAMES.join(", ")} or all, not "${bad.join(", ")}"`
14767
+ };
14768
+ if (!wanted.length) return { error: "--agent expects a name" };
14769
+ return {
14770
+ homes: wanted.map((agent) => ({
14771
+ agent,
14772
+ dir: join27(root, PROJECT_DIRS[agent][0]),
14773
+ scope: "project"
14774
+ }))
14775
+ };
14776
+ }
14777
+ function skillsDirFor(home) {
14778
+ return join27(home.dir, "skills");
14779
+ }
14780
+ function fallbackSkillsHome(root) {
14781
+ return { agent: "codex", dir: join27(root, ".agents"), scope: "project" };
14782
+ }
14783
+ function installedSkills(dir, exists = existsSync25, list = (p) => {
14784
+ try {
14785
+ return readdirSync6(p);
14786
+ } catch {
14787
+ return [];
14788
+ }
14789
+ }) {
14790
+ if (!exists(dir)) return [];
14791
+ return list(dir).filter((name) => exists(join27(dir, name, "SKILL.md"))).sort();
14792
+ }
14793
+ var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
14794
+ var RULES_END = "<!-- vos:end -->";
14795
+ var RULES_BODY = `## Product videos (vosso)
14796
+ - Skill: product-video (npx skills add vosjs/skills) records the real product from actions.json
14797
+ - Look before you cut: vos frames take --at-zooms; edit take/doc.json and re-render, never re-record
14798
+ - Hand back: vos open take serves the take into the studio at vos.so, doc.json intact
14799
+ - Local by default: vos push only when asked for a link, the shelf or a cloud render
14800
+ - Deleting: vos delete moves a vos to Trash and vos restore brings it back; if you deleted by mistake, restore it and say so. Only the person can delete forever, at vos.so/app/trash
14801
+ - Credentials: VOS_API_KEY, then ~/.config/vos/credentials, then vos login; never print one
14802
+ - Reference: https://vos.so/llms-full.txt`;
14803
+ var RULES_BLOCK = `${RULES_BEGIN}
14804
+ ${RULES_BODY}
14805
+ ${RULES_END}`;
14806
+ function applyRulesBlock(existing) {
14807
+ const current = existing ?? "";
14808
+ const begin = current.indexOf(RULES_BEGIN);
12136
14809
  const end = current.indexOf(RULES_END);
12137
14810
  let text;
12138
14811
  if (begin !== -1 && end !== -1 && end > begin) {
@@ -12148,10 +14821,10 @@ ${RULES_BLOCK}
12148
14821
  }
12149
14822
  return { text, changed: text !== current };
12150
14823
  }
12151
- function rulesFileFor(root, exists = existsSync21) {
12152
- const agents = join23(root, "AGENTS.md");
14824
+ function rulesFileFor(root, exists = existsSync25) {
14825
+ const agents = join27(root, "AGENTS.md");
12153
14826
  if (exists(agents)) return agents;
12154
- const claude = join23(root, "CLAUDE.md");
14827
+ const claude = join27(root, "CLAUDE.md");
12155
14828
  if (exists(claude)) return claude;
12156
14829
  return agents;
12157
14830
  }
@@ -12160,14 +14833,14 @@ function chromeCandidates(platform, env) {
12160
14833
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
12161
14834
  if (platform === "win32")
12162
14835
  return [
12163
- join23(
14836
+ join27(
12164
14837
  env["PROGRAMFILES"] ?? "C:\\Program Files",
12165
14838
  "Google",
12166
14839
  "Chrome",
12167
14840
  "Application",
12168
14841
  "chrome.exe"
12169
14842
  ),
12170
- join23(
14843
+ join27(
12171
14844
  env["LOCALAPPDATA"] ?? "",
12172
14845
  "Google",
12173
14846
  "Chrome",
@@ -12253,27 +14926,27 @@ function doctorReport(facts) {
12253
14926
  }
12254
14927
  function bundledSkillsVersion(dir) {
12255
14928
  try {
12256
- const v = readFileSync10(join23(dir, "VERSION"), "utf8").trim();
14929
+ const v = readFileSync10(join27(dir, "VERSION"), "utf8").trim();
12257
14930
  return /^\d+\.\d+\.\d+$/.test(v) ? v : null;
12258
14931
  } catch {
12259
14932
  return null;
12260
14933
  }
12261
14934
  }
12262
14935
  function bundledSkillsDir(from) {
12263
- let dir = dirname8(from);
14936
+ let dir = dirname10(from);
12264
14937
  for (let i = 0; i < 6; i++) {
12265
- const pkg = join23(dir, "package.json");
12266
- if (existsSync21(pkg)) {
14938
+ const pkg = join27(dir, "package.json");
14939
+ if (existsSync25(pkg)) {
12267
14940
  try {
12268
14941
  const name = JSON.parse(readFileSync10(pkg, "utf8")).name;
12269
14942
  if (name === "@vosjs/cli") {
12270
- const skills = join23(dir, "skills");
12271
- return existsSync21(skills) && statSync7(skills).isDirectory() ? skills : null;
14943
+ const skills = join27(dir, "skills");
14944
+ return existsSync25(skills) && statSync8(skills).isDirectory() ? skills : null;
12272
14945
  }
12273
14946
  } catch {
12274
14947
  }
12275
14948
  }
12276
- const up = dirname8(dir);
14949
+ const up = dirname10(dir);
12277
14950
  if (up === dir) break;
12278
14951
  dir = up;
12279
14952
  }
@@ -12281,7 +14954,7 @@ function bundledSkillsDir(from) {
12281
14954
  }
12282
14955
 
12283
14956
  // src/plugin/machineCmd.ts
12284
- var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set([
14957
+ var BOOLEAN_FLAGS14 = /* @__PURE__ */ new Set([
12285
14958
  "json",
12286
14959
  "help",
12287
14960
  "global",
@@ -12304,7 +14977,7 @@ function npx() {
12304
14977
  function bundledChromium() {
12305
14978
  try {
12306
14979
  const p = chromium2.executablePath();
12307
- return p && existsSync22(p) ? p : null;
14980
+ return p && existsSync26(p) ? p : null;
12308
14981
  } catch {
12309
14982
  return null;
12310
14983
  }
@@ -12313,7 +14986,7 @@ function probe2() {
12313
14986
  return probeBrowser({
12314
14987
  platform: process.platform,
12315
14988
  env: process.env,
12316
- exists: existsSync22,
14989
+ exists: existsSync26,
12317
14990
  bundled: bundledChromium
12318
14991
  });
12319
14992
  }
@@ -12364,7 +15037,7 @@ async function doctorFacts(opts) {
12364
15037
  };
12365
15038
  }
12366
15039
  async function cmdSetup(argv) {
12367
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15040
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12368
15041
  const r = reporterFor(flags);
12369
15042
  const root = process.cwd();
12370
15043
  const global = flags.global === true;
@@ -12398,7 +15071,7 @@ async function cmdSetup(argv) {
12398
15071
  const dest = skillsDirFor(home);
12399
15072
  mkdirSync6(dest, { recursive: true });
12400
15073
  for (const name of installedSkills(dir)) {
12401
- cpSync(join24(dir, name), join24(dest, name), { recursive: true });
15074
+ cpSync(join28(dir, name), join28(dest, name), { recursive: true });
12402
15075
  }
12403
15076
  }
12404
15077
  };
@@ -12466,7 +15139,7 @@ async function cmdSetup(argv) {
12466
15139
  let rules = null;
12467
15140
  if (!noRules) {
12468
15141
  const file = rulesFileFor(root);
12469
- const existing = existsSync22(file) ? readFileSync11(file, "utf8") : null;
15142
+ const existing = existsSync26(file) ? readFileSync11(file, "utf8") : null;
12470
15143
  const { text, changed } = applyRulesBlock(existing);
12471
15144
  if (changed) writeFileSync6(file, text);
12472
15145
  rules = { file, changed };
@@ -12497,7 +15170,7 @@ Next: ${report.next_step}`
12497
15170
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
12498
15171
  }
12499
15172
  async function cmdDoctor(argv) {
12500
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15173
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12501
15174
  const r = reporterFor(flags);
12502
15175
  const root = process.cwd();
12503
15176
  const homes = detectAgents(root, { home: homedir2(), global: true });
@@ -12516,7 +15189,7 @@ Next: ${report.next_step}`
12516
15189
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
12517
15190
  }
12518
15191
  async function cmdWhoami(argv) {
12519
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15192
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12520
15193
  const r = reporterFor(flags);
12521
15194
  const origin = platformOrigin({
12522
15195
  origin: strFlag(flags, "origin"),
@@ -12578,9 +15251,9 @@ async function cmdWhoami(argv) {
12578
15251
  return EXIT_OK;
12579
15252
  }
12580
15253
  function cmdLogout(argv) {
12581
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15254
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12582
15255
  const r = reporterFor(flags);
12583
- const had = existsSync22(CREDENTIALS_PATH);
15256
+ const had = existsSync26(CREDENTIALS_PATH);
12584
15257
  rmSync3(CREDENTIALS_PATH, { force: true });
12585
15258
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
12586
15259
  r.done(
@@ -12596,7 +15269,7 @@ function cmdLogout(argv) {
12596
15269
  }
12597
15270
 
12598
15271
  // src/plugin/run.ts
12599
- var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
15272
+ var BOOLEAN_FLAGS15 = /* @__PURE__ */ new Set([
12600
15273
  "json",
12601
15274
  "help",
12602
15275
  "wait",
@@ -12620,19 +15293,29 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
12620
15293
  "allow-wall",
12621
15294
  "hosted",
12622
15295
  "claimable",
12623
- "share"
15296
+ "share",
15297
+ "verbose"
15298
+ ]);
15299
+ var MULTI_FLAGS2 = /* @__PURE__ */ new Set([
15300
+ "set",
15301
+ "override",
15302
+ "browser-arg",
15303
+ "header",
15304
+ "confirm"
12624
15305
  ]);
12625
- var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
12626
15306
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
12627
15307
 
12628
15308
  Take pipeline
12629
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
12630
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
15309
+ vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
15310
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--verbose] [--json]
12631
15311
  vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
12632
15312
  the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
12633
- 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|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
15313
+ 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|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions|--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
12634
15314
  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]
12635
- vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
15315
+ vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--strict] [--json]
15316
+ --at-zooms stills every zoom span's apex on every media of the take and renders it again with the zooms off: a zoom whose frame matches its zoom-off twin did not render, and says so (exit 2 under --strict)
15317
+ vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]
15318
+ the rendered file itself, no ffmpeg: duration, size, codecs, resolution, frame rate; full-frame scene changes (a warning when a 5 s window holds more than one); loudness (short-term every 3 s, integrated, sample peak; a warning below -23 or above -9 LUFS); the first and last frames as PNGs (a warning when either is near-uniform). Exit 2 on any warning under --strict
12636
15319
  vos deliver <take> --to youtube,x,shorts,github (or all) [--look plate|gradient|dark|none] [--brand BRAND.md] [--set path=value] [--release v2.1] [--out dir] [--range a..b] [--parallel N] [--json]
12637
15320
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
12638
15321
  vos brand <url> [--out BRAND.md] [--json]
@@ -12642,6 +15325,10 @@ Take pipeline
12642
15325
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
12643
15326
  vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
12644
15327
  a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so. Without a trace the file opens as FINISHED footage (a render from HyperFrames or Remotion, a film): no card, no browser bar, no drawn cursor, its own frame rate; --as take gives a screen demo the card
15328
+ vos join <take> <take|file.mp4>... --out <dir> [--force] [--captions] [--mute <file>]... [--json]
15329
+ several takes (and plain video files) as ONE take, cut in argument order: the first is the primary (its frame, cursor, export and sound), each next one a media m1, m2, \u2026 with its footage copied, its spans tagged and renamed, its layers moved to where it now starts; a file opens full-bleed and its own sound plays as an audio clip (--mute <file> leaves it out). --captions lands every step's caption anchored to its step, each ending with its clip. The result must pass vos validate before anything is written
15330
+ vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]
15331
+ the film's music as one audio clip (id score): reads the track's loudness, finds its strongest swell and offsets the track so the swell lands at --at (a step, resolved through the cut, or output seconds); without --at it starts where the track first rises above its median level
12645
15332
  vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
12646
15333
  a finished piece (a HyperFrames composition, a Remotion project bundled with its own bundler, a hand-rolled page) read as a browser renders it: words, palette, faces (catalog or missing), media, scenes, gaps \u2192 port/inventory.json, with stills from its render
12647
15334
  vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
@@ -12650,6 +15337,8 @@ Take pipeline
12650
15337
  real functions \u2192 config.json, refusing a module-scope read, syntax the page cannot run and non-JSON data; config.sources.json maps each function to its line
12651
15338
  vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--threshold 0.95] [--out compare] [--json]
12652
15339
  per-frame SSIM and source | vos | difference sheets; exit 1 when ANY frame is under the threshold (needs ffmpeg)
15340
+ vos look <url> [--session <name>|--storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--header name=value]... [--browser-arg=<switch>]... [--out look] [--json]
15341
+ see a page before scripting it, signed in the way vos record would be: <out>/look.png and every control in view with ONE proposed selector (test id, aria-label, role + name, then words in its dialog or landmark), UNIQUE or AMBIGUOUS as the recorder's own engine counts it, plus what the frame exposes. --click/--type run first, in order; a click on a spend or delete word is refused. Exit 4 at a sign-in wall
12653
15342
  vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
12654
15343
  vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
12655
15344
  vos session list | rm <name> what exists and how old; delete one
@@ -12798,11 +15487,25 @@ reuse block NAMES whatever could not follow \u2014 fix by exception, never
12798
15487
  re-cut from scratch. Give steps ids in actions.json so re-timing survives
12799
15488
  script edits.
12800
15489
 
12801
- record --strict exits 2 when any selector was skipped, the page never
12802
- reached networkidle, or the take hit --max-duration (the done event lists
12803
- skipped[] and capped either way). --max-duration defaults to the hosted
12804
- recording cap, read live from GET /api/limits (30 min when the origin is
12805
- unreachable): the capture stops there and says so in one line.
15490
+ record --strict exits 2 when any selector was skipped, an expect never
15491
+ held, a click landed on a disabled control, an await timed out, or the
15492
+ take hit --max-duration (the done event lists skipped[], disabled[] and
15493
+ capped either way). A first load that never reaches networkidle (a page
15494
+ holding a socket open) is a warning, navTimeout in the done event, never
15495
+ a strict failure. --max-duration defaults to
15496
+ the hosted recording cap, read live from GET /api/limits (30 min when the
15497
+ origin is unreachable): the capture stops there and says so in one line.
15498
+ Three steps beyond the gestures: expect checks the page on camera
15499
+ (selector, text or gone; enabled; timeout 5 s); await waits OFF camera
15500
+ (until {selector|text|gone}, timeout 600 s, poll 2 s, reload, onTimeout
15501
+ fail|continue), so the footage has no gap and the cut lands in
15502
+ meta.skips[] and becomes a segment boundary; download keeps a file the page
15503
+ makes in the take's outputs/ (selector: the download a click starts; src:
15504
+ the currentSrc of a video, audio or img; as: the file name), listed in
15505
+ meta.outputs[]. A click, press or type with "guard": "spend" costs
15506
+ something real: --dry-run hovers it and never presses, and record presses
15507
+ it only with --confirm <id> (repeatable), refusing before any capture
15508
+ otherwise. --hosted refuses guarded and download steps.
12806
15509
  frames captures PNG stills at OUTPUT times (seconds or % of duration);
12807
15510
  --at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
12808
15511
  the agent's eyes (--size WxH for an exact size). render
@@ -12920,7 +15623,7 @@ lint-gated, so a bad override fails like a bad doc.json):
12920
15623
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
12921
15624
  `;
12922
15625
  async function loadActions(file) {
12923
- const raw = JSON.parse(await readFile13(file, "utf8"));
15626
+ const raw = JSON.parse(await readFile15(file, "utf8"));
12924
15627
  const errors = validateActions(raw);
12925
15628
  if (errors.length)
12926
15629
  throw new UsageError(`invalid actions file:
@@ -12949,7 +15652,10 @@ async function maxDuration(flags, r) {
12949
15652
  }
12950
15653
  function strictReason(rec) {
12951
15654
  const parts = [`${rec.skipped.length} skipped step(s)`];
12952
- if (rec.navTimeout) parts.push("networkidle timeout");
15655
+ if (rec.disabled?.length)
15656
+ parts.push(`${rec.disabled.length} click(s) on a disabled control`);
15657
+ const timedOut = timedOutAwaits(rec);
15658
+ if (timedOut) parts.push(`${timedOut} await(s) that timed out`);
12953
15659
  if (rec.capped) parts.push("stopped at --max-duration");
12954
15660
  const missed = maskMisses(rec);
12955
15661
  if (missed.length)
@@ -12958,6 +15664,15 @@ function strictReason(rec) {
12958
15664
  );
12959
15665
  return parts.join(" + ");
12960
15666
  }
15667
+ function timedOutAwaits(rec) {
15668
+ return (rec.meta?.steps ?? []).filter((s) => s.do === "await" && s.timedOut).length;
15669
+ }
15670
+ function strictFails(rec) {
15671
+ return rec.skipped.length > 0 || rec.disabled.length > 0 || timedOutAwaits(rec) > 0 || rec.capped || maskMisses(rec).length > 0;
15672
+ }
15673
+ function navTimeoutNote(rec) {
15674
+ return rec.navTimeout ? "\n note: the first load never reached networkidle (a page holding a socket open never does); recorded anyway" : "";
15675
+ }
12961
15676
  function maskMisses(rec) {
12962
15677
  return (rec.masks ?? []).filter((m) => m.hits === 0).map((m) => m.selector);
12963
15678
  }
@@ -12968,6 +15683,53 @@ function exposureNote(rec) {
12968
15683
  ${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
12969
15684
  ${EXPOSURE_ADVICE}`;
12970
15685
  }
15686
+ function takeConfirm(multi, actions, opts) {
15687
+ const given = (multi.confirm ?? []).filter(Boolean);
15688
+ if (opts.dryRun) return given;
15689
+ const verdict = guardVerdict(actions, given);
15690
+ if (!verdict.ok) {
15691
+ const guarded = guardedSteps(actions);
15692
+ for (const id of verdict.unconfirmed)
15693
+ opts.r.event({
15694
+ event: "refused",
15695
+ reason: verdict.message,
15696
+ step: guarded.find((g) => g.id === id)?.step ?? null,
15697
+ id
15698
+ });
15699
+ for (const id of verdict.unknown)
15700
+ opts.r.event({
15701
+ event: "refused",
15702
+ reason: verdict.message,
15703
+ step: null,
15704
+ id
15705
+ });
15706
+ throw new UsageError(verdict.message);
15707
+ }
15708
+ return given;
15709
+ }
15710
+ function stepReportNote(rec) {
15711
+ const lines = [];
15712
+ if (rec.disabled.length)
15713
+ lines.push(
15714
+ `DISABLED when clicked: ${rec.disabled.map((d) => `#${d.step}${d.id ? ` ${d.id}` : ""} ${d.selector}`).join(", ")} (the press likely did nothing; an expect with enabled: true before it waits for the control)`
15715
+ );
15716
+ if (rec.skips.length)
15717
+ lines.push(
15718
+ `off camera: ${rec.skips.map((s) => `#${s.step}${s.id ? ` ${s.id}` : ""} ${(s.ms / 1e3).toFixed(1)} s, cut at ${s.at.toFixed(2)}s${s.timedOut ? " (timed out)" : ""}`).join(", ")}`
15719
+ );
15720
+ if (rec.outputs.length)
15721
+ lines.push(
15722
+ `outputs/: ${rec.outputs.map((o) => `${o.name} (${o.bytes} bytes)`).join(", ")}`
15723
+ );
15724
+ if (rec.confirmed.length)
15725
+ lines.push(`pressed, as --confirm said: ${rec.confirmed.join(", ")}`);
15726
+ if (rec.stopped) lines.push(`STOPPED: ${rec.stopped.reason}`);
15727
+ return lines.map((l) => `
15728
+ ${l}`).join("");
15729
+ }
15730
+ function skippedLine(s) {
15731
+ return `#${s.step}${s.id ? ` ${s.id}` : ""} ${s.do}${s.reason ? `: ${s.reason}` : ` ${s.selector}`}`;
15732
+ }
12971
15733
  function takeHeaders(multi) {
12972
15734
  try {
12973
15735
  return parseHeaders(multi.header);
@@ -12982,8 +15744,8 @@ function takeBrowserArgs(multi) {
12982
15744
  function takeStorageState(flags) {
12983
15745
  const raw = strFlag(flags, "storage-state");
12984
15746
  if (!raw) return void 0;
12985
- const path = resolve14(raw);
12986
- if (!existsSync23(path))
15747
+ const path = resolve18(raw);
15748
+ if (!existsSync27(path))
12987
15749
  throw new UsageError(`--storage-state: no such file: ${path}`);
12988
15750
  return path;
12989
15751
  }
@@ -12996,7 +15758,7 @@ function takeSessionName(flags, storageState) {
12996
15758
  throw new UsageError(
12997
15759
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
12998
15760
  );
12999
- if (!existsSync23(sessionDir(name)))
15761
+ if (!existsSync27(sessionDir(name)))
13000
15762
  throw new UsageError(
13001
15763
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
13002
15764
  );
@@ -13052,7 +15814,7 @@ function wallGate(flags, r, opts) {
13052
15814
  async function cmdRecord(argv) {
13053
15815
  const { positionals, flags, multi } = parseArgs(
13054
15816
  argv,
13055
- BOOLEAN_FLAGS12,
15817
+ BOOLEAN_FLAGS15,
13056
15818
  MULTI_FLAGS2
13057
15819
  );
13058
15820
  const actionsPath = strFlag(flags, "actions") ?? positionals[0];
@@ -13065,14 +15827,18 @@ async function cmdRecord(argv) {
13065
15827
  const url = strFlag(flags, "url") ?? actions.url;
13066
15828
  if (!url)
13067
15829
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13068
- const scheme = urlSchemeProblem(url);
15830
+ const scheme = urlSchemeProblem2(url);
13069
15831
  if (scheme) throw new UsageError(scheme);
13070
15832
  announceEnvNames(actions, r);
13071
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
15833
+ const outDir = resolve18(strFlag(flags, "out") ?? "take");
13072
15834
  const storageState = takeStorageState(flags);
13073
15835
  const sessionName = takeSessionName(flags, storageState);
13074
15836
  const browserArgs = takeBrowserArgs(multi);
13075
15837
  const headers = takeHeaders(multi);
15838
+ const confirm = flags.hosted === true ? [] : takeConfirm(multi, actions, {
15839
+ dryRun: flags["dry-run"] === true,
15840
+ r
15841
+ });
13076
15842
  if (flags.hosted === true) {
13077
15843
  if (flags["dry-run"] === true)
13078
15844
  throw new UsageError(
@@ -13082,6 +15848,15 @@ async function cmdRecord(argv) {
13082
15848
  throw new UsageError(
13083
15849
  'a session never reaches vos.so: --hosted takes no --session or --storage-state. Put the sign-in in the setup steps with { "env": "NAME" }, or record here with vos record.'
13084
15850
  );
15851
+ const refusals = hostedRefusals(actions);
15852
+ if (refusals.length)
15853
+ throw new UsageError(
15854
+ `a hosted take cannot run this script: ${refusals.join("; ")}`
15855
+ );
15856
+ if (multi.confirm?.length)
15857
+ throw new UsageError(
15858
+ "--confirm presses a guarded step on this machine; a hosted take runs no guarded step, so drop --hosted to record it here"
15859
+ );
13085
15860
  const origin = platformOrigin({
13086
15861
  origin: strFlag(flags, "origin"),
13087
15862
  api: strFlag(flags, "api")
@@ -13181,6 +15956,8 @@ async function cmdRecord(argv) {
13181
15956
  headers,
13182
15957
  context: context2,
13183
15958
  dryRun: true,
15959
+ verbose: flags.verbose === true,
15960
+ onEvent: (e) => r.event(e),
13184
15961
  // A rehearsal is always strict, and it touches no directory.
13185
15962
  onArrival: wallGate(flags, r, { strict: true })
13186
15963
  })
@@ -13189,9 +15966,28 @@ async function cmdRecord(argv) {
13189
15966
  const lines = steps.map((s) => {
13190
15967
  const rect = s.rect ? ` rect ${s.rect.x},${s.rect.y} ${s.rect.w}\xD7${s.rect.h}` : "";
13191
15968
  const what = s.selector ? ` ${s.selector}` : "";
13192
- return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? " NOT FOUND" : rect}${s.navigated ? " \u2192 navigated" : ""}`;
15969
+ const out = rec.outputs.find((o) => o.step === s.step);
15970
+ const notes = [
15971
+ ...rec.disabled.some((d) => d.step === s.step) ? ["DISABLED, pressed anyway"] : [],
15972
+ ...s.guard && !s.skipped ? [`guarded (${s.guard}), hovered, not pressed`] : [],
15973
+ ...s.followsGuard !== void 0 ? [
15974
+ `not waited: it follows #${s.followsGuard} (guarded, not pressed)`
15975
+ ] : [],
15976
+ ...s.offCameraMs !== void 0 ? [
15977
+ `${s.timedOut ? "timed out after" : "held after"} ${(s.offCameraMs / 1e3).toFixed(1)} s off camera`
15978
+ ] : [],
15979
+ ...out ? [`\u2192 ${out.name} (${out.bytes} bytes, not kept in a rehearsal)`] : []
15980
+ ].map((n) => ` ${n}`);
15981
+ return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? ` ${s.reason ?? "NOT FOUND"}` : rect}${s.navigated ? " \u2192 navigated" : ""}${notes.join("")}`;
13193
15982
  });
13194
- const failed = rec.skipped.length > 0 || rec.navTimeout || maskMisses(rec).length > 0;
15983
+ for (const [i, step] of actions.steps.entries())
15984
+ if (!steps.some((s) => s.step === i))
15985
+ lines.push(
15986
+ ` \u2013 #${i}${step.id ? ` (${step.id})` : ""} ${step.do} not run`
15987
+ );
15988
+ const resolved = steps.filter((s) => !s.skipped).length;
15989
+ const failed = strictFails(rec) || rec.stopped !== null;
15990
+ const unconfirmed = guardVerdict(actions, confirm);
13195
15991
  const carried = [
13196
15992
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
13197
15993
  ...browserArgs.map((a) => `--browser-arg=${a}`),
@@ -13199,9 +15995,11 @@ async function cmdRecord(argv) {
13199
15995
  // printed.
13200
15996
  ...Object.keys(headers).map((k) => `--header ${k}=\u2026`),
13201
15997
  ...sessionName ? [`--session ${sessionName}`] : [],
13202
- ...flags["allow-wall"] === true ? ["--allow-wall"] : []
15998
+ ...flags["allow-wall"] === true ? ["--allow-wall"] : [],
15999
+ ...confirm.map((c) => `--confirm ${c}`)
13203
16000
  ].join(" ");
13204
16001
  const nextOut = strFlag(flags, "out") ?? "take";
16002
+ const guardNote = !unconfirmed.ok ? unconfirmed.unconfirmed.length ? ` (it refuses until you add ${unconfirmed.unconfirmed.map((id) => `--confirm ${id}`).join(" ")}: ${unconfirmed.unconfirmed.length === 1 ? "that step spends" : "those steps spend"} something real)` : ` (${unconfirmed.unknown.map((c) => `--confirm ${c}`).join(", ")} names no guarded step)` : "";
13205
16003
  r.done(
13206
16004
  {
13207
16005
  dryRun: true,
@@ -13209,28 +16007,33 @@ async function cmdRecord(argv) {
13209
16007
  seconds: +((Date.now() - started) / 1e3).toFixed(1),
13210
16008
  steps,
13211
16009
  skipped: rec.skipped,
16010
+ disabled: rec.disabled,
16011
+ skips: rec.skips,
16012
+ outputs: rec.outputs,
16013
+ pageErrors: rec.pageErrors,
16014
+ ...rec.stopped ? { stopped: rec.stopped } : {},
13212
16015
  navTimeout: rec.navTimeout,
13213
16016
  exposures: rec.exposures,
13214
16017
  masks: rec.masks
13215
16018
  },
13216
- `${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${steps.length - rec.skipped.length}/${steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
16019
+ `${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${resolved}/${actions.steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
13217
16020
  ${lines.join("\n")}
13218
- ${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict`}${exposureNote(rec)}`
16021
+ ${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${rec.stopped ? `STOPPED: ${rec.stopped.reason}. ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict${guardNote}`}${exposureNote(rec)}`
13219
16022
  );
13220
- return failed ? EXIT_USAGE : EXIT_OK;
16023
+ return rec.stopped ? EXIT_ERROR : failed ? EXIT_USAGE : EXIT_OK;
13221
16024
  } finally {
13222
16025
  await browser2.close();
13223
16026
  }
13224
16027
  }
13225
16028
  const paths = takePaths(outDir);
13226
16029
  const prepare = async () => {
13227
- if (existsSync23(join25(outDir, "meta.json"))) {
16030
+ if (existsSync27(join29(outDir, "meta.json"))) {
13228
16031
  const { prevDoc, kept } = await prepareReRecord(outDir);
13229
16032
  r.log(
13230
16033
  `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
13231
16034
  );
13232
16035
  }
13233
- await mkdir8(outDir, { recursive: true });
16036
+ await mkdir13(outDir, { recursive: true });
13234
16037
  await ensureTakeDir(outDir);
13235
16038
  };
13236
16039
  const browser = await launchBrowser(browserArgs);
@@ -13245,6 +16048,9 @@ ${lines.join("\n")}
13245
16048
  storageState,
13246
16049
  headers,
13247
16050
  context: context2,
16051
+ confirm,
16052
+ verbose: flags.verbose === true,
16053
+ onEvent: (e) => r.event(e),
13248
16054
  onArrival: wallGate(flags, r, {
13249
16055
  strict: flags.strict === true,
13250
16056
  prepare
@@ -13263,9 +16069,29 @@ ${lines.join("\n")}
13263
16069
  const plan = await planTake(outDir, { backdrop });
13264
16070
  const clicks = rec.events.filter((e) => e.type === "down").length;
13265
16071
  const strict = flags.strict === true;
13266
- const strictFail = strict && (rec.skipped.length > 0 || rec.navTimeout || rec.capped || maskMisses(rec).length > 0);
16072
+ const strictFail = strict && strictFails(rec);
13267
16073
  const skippedNote = rec.skipped.length ? `
13268
- SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map((s) => `#${s.step} ${s.do} ${s.selector}`).join(", ")}` : "";
16074
+ SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map(skippedLine).join(", ")}` : "";
16075
+ const stepNote = stepReportNote(rec);
16076
+ if (rec.stopped) {
16077
+ r.done(
16078
+ {
16079
+ take: outDir,
16080
+ ok: false,
16081
+ stopped: rec.stopped,
16082
+ durationMs: rec.meta.durationMs,
16083
+ skipped: rec.skipped,
16084
+ disabled: rec.disabled,
16085
+ confirmed: rec.confirmed,
16086
+ skips: rec.skips,
16087
+ outputs: rec.outputs,
16088
+ pageErrors: rec.pageErrors
16089
+ },
16090
+ `FAILED: ${rec.stopped.reason}.
16091
+ The footage up to it is in ${outDir}.${skippedNote}${stepNote}${navTimeoutNote(rec)}`
16092
+ );
16093
+ return EXIT_ERROR;
16094
+ }
13269
16095
  r.done(
13270
16096
  {
13271
16097
  take: outDir,
@@ -13285,11 +16111,16 @@ ${lines.join("\n")}
13285
16111
  ...rec.wall ? { wall: rec.wall } : {},
13286
16112
  exposures: rec.exposures,
13287
16113
  ...rec.masks.length ? { masks: rec.masks } : {},
16114
+ disabled: rec.disabled,
16115
+ confirmed: rec.confirmed,
16116
+ skips: rec.skips,
16117
+ outputs: rec.outputs,
16118
+ pageErrors: rec.pageErrors,
13288
16119
  ...strictFail ? { strictFailed: true } : {}
13289
16120
  },
13290
- strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
13291
- ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
13292
- Next: edit ${join25(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
16121
+ strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${stepNote}${navTimeoutNote(rec)}${exposureNote(rec)}` : `Take ready: ${outDir}
16122
+ ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}${stepNote}${navTimeoutNote(rec)}
16123
+ Next: edit ${join29(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
13293
16124
  );
13294
16125
  return strictFail ? EXIT_USAGE : EXIT_OK;
13295
16126
  } finally {
@@ -13299,7 +16130,7 @@ ${lines.join("\n")}
13299
16130
  async function cmdCreate2(argv) {
13300
16131
  const { positionals, flags, multi } = parseArgs(
13301
16132
  argv,
13302
- BOOLEAN_FLAGS12,
16133
+ BOOLEAN_FLAGS15,
13303
16134
  MULTI_FLAGS2
13304
16135
  );
13305
16136
  const actionsPath = strFlag(flags, "actions");
@@ -13313,12 +16144,12 @@ async function cmdCreate2(argv) {
13313
16144
  const url = strFlag(flags, "url") ?? actions.url;
13314
16145
  if (!url)
13315
16146
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13316
- const scheme = urlSchemeProblem(url);
16147
+ const scheme = urlSchemeProblem2(url);
13317
16148
  if (scheme) throw new UsageError(scheme);
13318
16149
  announceEnvNames(actions, r);
13319
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
16150
+ const outDir = resolve18(strFlag(flags, "out") ?? "take");
13320
16151
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
13321
- const out = positionals[0] ?? join25(outDir, `out.${format}`);
16152
+ const out = positionals[0] ?? join29(outDir, `out.${format}`);
13322
16153
  const parallel = numFlag(flags, "parallel", 1);
13323
16154
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13324
16155
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13329,15 +16160,16 @@ async function cmdCreate2(argv) {
13329
16160
  const sessionName = takeSessionName(flags, storageState);
13330
16161
  const browserArgs = takeBrowserArgs(multi);
13331
16162
  const headers = takeHeaders(multi);
16163
+ const confirm = takeConfirm(multi, actions, { dryRun: false, r });
13332
16164
  const paths = takePaths(outDir);
13333
16165
  const prepare = async () => {
13334
- if (existsSync23(join25(outDir, "meta.json"))) {
16166
+ if (existsSync27(join29(outDir, "meta.json"))) {
13335
16167
  const { prevDoc, kept } = await prepareReRecord(outDir);
13336
16168
  r.log(
13337
16169
  `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
13338
16170
  );
13339
16171
  }
13340
- await mkdir8(outDir, { recursive: true });
16172
+ await mkdir13(outDir, { recursive: true });
13341
16173
  await ensureTakeDir(outDir);
13342
16174
  };
13343
16175
  const browser = await launchBrowser(browserArgs);
@@ -13352,6 +16184,9 @@ async function cmdCreate2(argv) {
13352
16184
  storageState,
13353
16185
  headers,
13354
16186
  context: context2,
16187
+ confirm,
16188
+ verbose: flags.verbose === true,
16189
+ onEvent: (e) => r.event(e),
13355
16190
  onArrival: wallGate(flags, r, {
13356
16191
  strict: flags.strict === true,
13357
16192
  prepare
@@ -13368,11 +16203,25 @@ async function cmdCreate2(argv) {
13368
16203
  await releaseFrames(paths, flags, r.log);
13369
16204
  r.event({ event: "phase", phase: "plan" });
13370
16205
  await planTake(outDir, { backdrop });
13371
- if (flags.strict === true && (rec.skipped.length > 0 || rec.navTimeout || rec.capped || maskMisses(rec).length > 0)) {
16206
+ if (rec.stopped) {
16207
+ r.done(
16208
+ {
16209
+ take: outDir,
16210
+ ok: false,
16211
+ stopped: rec.stopped,
16212
+ skipped: rec.skipped,
16213
+ rendered: false
16214
+ },
16215
+ `FAILED: ${rec.stopped.reason}. NOT rendering; the footage up to it is in ${outDir}.`
16216
+ );
16217
+ return EXIT_ERROR;
16218
+ }
16219
+ if (flags.strict === true && strictFails(rec)) {
13372
16220
  r.done(
13373
16221
  {
13374
16222
  take: outDir,
13375
16223
  skipped: rec.skipped,
16224
+ disabled: rec.disabled,
13376
16225
  navTimeout: rec.navTimeout,
13377
16226
  capped: rec.capped,
13378
16227
  strictFailed: true,
@@ -13415,20 +16264,26 @@ async function cmdCreate2(argv) {
13415
16264
  dead: rec.dead,
13416
16265
  exposures: rec.exposures,
13417
16266
  ...rec.masks.length ? { masks: rec.masks } : {},
16267
+ disabled: rec.disabled,
16268
+ confirmed: rec.confirmed,
16269
+ skips: rec.skips,
16270
+ outputs: rec.outputs,
16271
+ pageErrors: rec.pageErrors,
13418
16272
  ...flags.draft === true ? { draft: true } : {}
13419
16273
  },
13420
16274
  `Created ${result.out} (${(result.bytes / 1024).toFixed(0)} KB, ${result.width}x${result.height}@${result.fps}, ${result.duration.toFixed(1)}s, ${result.zoomSpans} zoom spans, ${result.clicks} click effects${result.audio ? ", audio" : ""}${flags.draft === true ? ", DRAFT" : ""})
13421
- take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${exposureNote(rec)}`
16275
+ take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${stepReportNote(rec)}${navTimeoutNote(rec)}${exposureNote(rec)}`
13422
16276
  );
13423
16277
  return EXIT_OK;
13424
16278
  } finally {
13425
16279
  await browser.close();
13426
16280
  }
13427
16281
  }
13428
- async function cmdPlan(argv) {
16282
+ async function cmdPlan(argvIn) {
16283
+ const argv = bareCaptionsFlag(argvIn);
13429
16284
  const { positionals, flags, multi } = parseArgs(
13430
16285
  argv,
13431
- BOOLEAN_FLAGS12,
16286
+ BOOLEAN_FLAGS15,
13432
16287
  /* @__PURE__ */ new Set(["with"])
13433
16288
  );
13434
16289
  const dir = positionals[0];
@@ -13438,26 +16293,28 @@ async function cmdPlan(argv) {
13438
16293
  );
13439
16294
  const r = createReporter(flags.json === true);
13440
16295
  if (flags.fresh === true) {
13441
- await rm2(join25(dir, "doc.json"), { force: true });
16296
+ await rm5(join29(dir, "doc.json"), { force: true });
13442
16297
  r.log("note: --fresh discarded the existing doc.json");
13443
16298
  }
13444
16299
  let reuse;
13445
16300
  if (flags.reuse === true) {
13446
- const from = resolve14(strFlag(flags, "from") ?? join25(dir, PREV_DOC_NAME));
13447
- if (!existsSync23(from)) {
16301
+ const from = resolve18(strFlag(flags, "from") ?? join29(dir, PREV_DOC_NAME));
16302
+ if (!existsSync27(from)) {
13448
16303
  throw new UsageError(
13449
16304
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
13450
16305
  );
13451
16306
  }
13452
16307
  reuse = {
13453
16308
  from,
13454
- doc: JSON.parse(await readFile13(from, "utf8"))
16309
+ doc: JSON.parse(await readFile15(from, "utf8"))
13455
16310
  };
13456
16311
  }
13457
16312
  const style = await resolveStyleRef(flags);
13458
- const hasDoc = existsSync23(join25(dir, "doc.json"));
16313
+ const hasDoc = existsSync27(join29(dir, "doc.json"));
13459
16314
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
13460
16315
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
16316
+ const stepCaptions = strFlag(flags, "captions") === "steps";
16317
+ if (stepCaptions) release.launchRoles.captions = "none";
13461
16318
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
13462
16319
  const stillRaw = strFlag(flags, "still") ?? release.launchRoles.still;
13463
16320
  const still = stillRaw === void 0 ? null : Number(stillRaw);
@@ -13485,6 +16342,16 @@ async function cmdPlan(argv) {
13485
16342
  }
13486
16343
  } : {}
13487
16344
  });
16345
+ let captions = null;
16346
+ if (stepCaptions) {
16347
+ const take = await loadTake(dir);
16348
+ captions = landStepCaptions(s.doc, /* @__PURE__ */ new Map([["", take.actions]]));
16349
+ await writeJson(take.paths.doc, s.doc, true);
16350
+ }
16351
+ const captionLines = captions ? `
16352
+ ${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} anchored to their steps` + (captions.skipped.length ? `
16353
+ not landed: ${captions.skipped.join("; ")}` : "") + (captions.uncaptioned.length ? `
16354
+ 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` : "") : "";
13488
16355
  const layoutLines = s.layout ? `
13489
16356
  layout from ${s.styleFrom}: ${[
13490
16357
  s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
@@ -13501,7 +16368,7 @@ async function cmdPlan(argv) {
13501
16368
  ${s.reuse.flagged.join("\n ")}` : "") : "";
13502
16369
  r.done(
13503
16370
  {
13504
- take: resolve14(dir),
16371
+ take: resolve18(dir),
13505
16372
  fresh: s.fresh,
13506
16373
  cursorKept: s.cursorKept,
13507
16374
  zoomAuto: s.zoomAuto,
@@ -13511,9 +16378,10 @@ async function cmdPlan(argv) {
13511
16378
  ...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
13512
16379
  ...s.layout ? { layout: s.layout } : {},
13513
16380
  ...s.motion ? { motion: s.motion } : {},
13514
- ...s.reuse ? { reuse: s.reuse } : {}
16381
+ ...s.reuse ? { reuse: s.reuse } : {},
16382
+ ...captions ? { captions } : {}
13515
16383
  },
13516
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join25(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
16384
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join29(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}${captionLines}`
13517
16385
  );
13518
16386
  return EXIT_OK;
13519
16387
  }
@@ -13626,7 +16494,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
13626
16494
  async function cmdRender(argv) {
13627
16495
  const { positionals, flags, multi } = parseArgs(
13628
16496
  argv,
13629
- BOOLEAN_FLAGS12,
16497
+ BOOLEAN_FLAGS15,
13630
16498
  MULTI_FLAGS2
13631
16499
  );
13632
16500
  const dir = positionals[0];
@@ -13636,7 +16504,7 @@ async function cmdRender(argv) {
13636
16504
  );
13637
16505
  const r = createReporter(flags.json === true);
13638
16506
  const format = videoFormat(strFlag(flags, "format"), positionals[1]);
13639
- const out = positionals[1] ?? join25(dir, `out.${format}`);
16507
+ const out = positionals[1] ?? join29(dir, `out.${format}`);
13640
16508
  const parallel = numFlag(flags, "parallel", 1);
13641
16509
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13642
16510
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13694,7 +16562,7 @@ async function cmdRender(argv) {
13694
16562
  async function cmdFrames(argv) {
13695
16563
  const { positionals, flags, multi } = parseArgs(
13696
16564
  argv,
13697
- BOOLEAN_FLAGS12,
16565
+ BOOLEAN_FLAGS15,
13698
16566
  MULTI_FLAGS2
13699
16567
  );
13700
16568
  const dir = positionals[0];
@@ -13712,12 +16580,13 @@ async function cmdFrames(argv) {
13712
16580
  if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
13713
16581
  size = { width: Number(m[1]), height: Number(m[2]) };
13714
16582
  }
13715
- const duration = docOutputDuration3(take.doc);
16583
+ const duration = docOutputDuration4(take.doc);
13716
16584
  const frameRaw = strFlag(flags, "frame");
13717
16585
  const timesRaw = strFlag(flags, "times");
13718
16586
  const atZooms = flags["at-zooms"] === true;
13719
16587
  const atMoments = flags["at-moments"] === true;
13720
16588
  const atStill = flags["at-still"] === true;
16589
+ const strict = flags.strict === true;
13721
16590
  let times;
13722
16591
  try {
13723
16592
  times = frameRaw !== void 0 ? parseTimes2(frameRaw, duration) : timesRaw !== void 0 ? parseTimes2(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes2("0,25%,50%,75%,100%", duration);
@@ -13747,6 +16616,13 @@ async function cmdFrames(argv) {
13747
16616
  }
13748
16617
  });
13749
16618
  const index = await writeIndexJson(result);
16619
+ for (const z of result.skippedZooms ?? [])
16620
+ r.warn(
16621
+ `zoom ${z.id}${z.media ? ` (media ${z.media})` : ""} has no apex to still: ${z.reason}`
16622
+ );
16623
+ const unrendered = result.frames.filter((f) => f.rendered === false);
16624
+ for (const f of unrendered) r.warn(unrenderedZoomLine(f));
16625
+ const zoomTag = (f) => `${f.zoomId ? ` ${f.zoomId}` : ""}${f.media ? ` on ${f.media}` : ""}${f.rendered === false ? ", DID NOT RENDER" : ""}`;
13750
16626
  r.done(
13751
16627
  {
13752
16628
  outDir: result.outDir,
@@ -13754,12 +16630,14 @@ async function cmdFrames(argv) {
13754
16630
  width: result.width,
13755
16631
  height: result.height,
13756
16632
  duration: result.duration,
13757
- frames: result.frames
16633
+ frames: result.frames,
16634
+ ...result.skippedZooms ? { skippedZooms: result.skippedZooms } : {},
16635
+ ...atZooms ? { unrenderedZooms: unrendered.length } : {}
13758
16636
  },
13759
16637
  `Wrote ${result.frames.length} still(s) to ${result.outDir} (${result.width}x${result.height})
13760
- ${result.frames.map((f) => `${f.time.toFixed(2)}s${f.kind === "zoom" ? " [zoom apex]" : f.kind === "moment" ? ` [${f.momentId}]` : ""} \u2192 ${f.file}`).join("\n ")}`
16638
+ ${result.frames.map((f) => `${f.time.toFixed(2)}s${f.kind === "zoom" ? ` [zoom apex${zoomTag(f)}]` : f.kind === "moment" ? ` [${f.momentId}]` : ""} \u2192 ${f.file}`).join("\n ")}`
13761
16639
  );
13762
- return EXIT_OK;
16640
+ return unrendered.length && strict ? EXIT_USAGE : EXIT_OK;
13763
16641
  } finally {
13764
16642
  await browser.close();
13765
16643
  }
@@ -13767,7 +16645,7 @@ async function cmdFrames(argv) {
13767
16645
  async function cmdDeliver(argv) {
13768
16646
  const { positionals, flags, multi } = parseArgs(
13769
16647
  argv,
13770
- BOOLEAN_FLAGS12,
16648
+ BOOLEAN_FLAGS15,
13771
16649
  MULTI_FLAGS2
13772
16650
  );
13773
16651
  const dir = positionals[0];
@@ -13867,15 +16745,15 @@ async function resolveStyleRef(flags) {
13867
16745
  }
13868
16746
  async function resolveDocRef(ref, flags, what) {
13869
16747
  const styleRef = ref;
13870
- const file = existsSync23(styleRef) ? resolve14(
16748
+ const file = existsSync27(styleRef) ? resolve18(
13871
16749
  styleRef,
13872
- existsSync23(join25(styleRef, "doc.json")) ? "doc.json" : ""
16750
+ existsSync27(join29(styleRef, "doc.json")) ? "doc.json" : ""
13873
16751
  ) : null;
13874
- if (file && existsSync23(file)) {
16752
+ if (file && existsSync27(file)) {
13875
16753
  return {
13876
16754
  from: file,
13877
16755
  doc: migrateHostedDoc4(
13878
- JSON.parse(await readFile13(file, "utf8"))
16756
+ JSON.parse(await readFile15(file, "utf8"))
13879
16757
  )
13880
16758
  };
13881
16759
  }
@@ -13917,7 +16795,7 @@ async function officialTemplate(origin, ref) {
13917
16795
  }
13918
16796
  }
13919
16797
  async function cmdDigest(argv) {
13920
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16798
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
13921
16799
  const dir = positionals[0];
13922
16800
  if (!dir)
13923
16801
  throw new UsageError(
@@ -13927,7 +16805,7 @@ async function cmdDigest(argv) {
13927
16805
  const take = await loadTake(dir);
13928
16806
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
13929
16807
  const transcriptPath = strFlag(flags, "transcript");
13930
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile13(transcriptPath, "utf8"))) : null;
16808
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
13931
16809
  const style = await resolveStyleRef(flags);
13932
16810
  const noFrames = flags["no-frames"] === true;
13933
16811
  let browser = null;
@@ -13958,7 +16836,7 @@ async function cmdDigest(argv) {
13958
16836
  kinds,
13959
16837
  frames: d.images.full,
13960
16838
  crops: d.images.crop,
13961
- sheet: d.images.sheet ? join25(result.outDir, d.images.sheet) : null,
16839
+ sheet: d.images.sheet ? join29(result.outDir, d.images.sheet) : null,
13962
16840
  scenes: kinds.scene ?? 0,
13963
16841
  sourceDuration: d.take.sourceDuration,
13964
16842
  outputDuration: d.take.outputDuration,
@@ -13981,19 +16859,19 @@ async function cmdDigest(argv) {
13981
16859
  }
13982
16860
  }
13983
16861
  async function cmdOpen(argv) {
13984
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16862
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
13985
16863
  const dir = positionals[0];
13986
16864
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
13987
16865
  const r = createReporter(flags.json === true);
13988
16866
  const take = await loadTake(dir);
13989
- if (!existsSync23(take.paths.recording)) {
16867
+ if (!existsSync27(take.paths.recording)) {
13990
16868
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
13991
16869
  }
13992
16870
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
13993
16871
  /\/+$/,
13994
16872
  ""
13995
16873
  );
13996
- const server = await startTakeServer(resolve14(dir), {});
16874
+ const server = await startTakeServer(resolve18(dir), {});
13997
16875
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
13998
16876
  r.event({ event: "open", server: server.base, url });
13999
16877
  r.log(`take served at ${server.base}`);
@@ -14016,7 +16894,7 @@ async function cmdOpen(argv) {
14016
16894
  return EXIT_OK;
14017
16895
  }
14018
16896
  async function cmdCallout(argv) {
14019
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16897
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
14020
16898
  const dir = positionals[0];
14021
16899
  const shape = positionals[1];
14022
16900
  if (!dir || !shape || !isCalloutShape(shape))
@@ -14040,7 +16918,7 @@ async function cmdCallout(argv) {
14040
16918
  `no register: put a BRAND.md beside the take (vos brand <url> --out BRAND.md; its bgA and accent are the ground and the accent), or pass --ground #hex --accent #hex`
14041
16919
  );
14042
16920
  const codeFile = strFlag(flags, "code-file");
14043
- const code = codeFile ? await readFile13(codeFile, "utf8") : strFlag(flags, "code");
16921
+ const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
14044
16922
  const words2 = {
14045
16923
  kicker: strFlag(flags, "kicker"),
14046
16924
  title: strFlag(flags, "title"),
@@ -14115,11 +16993,11 @@ async function cmdCallout(argv) {
14115
16993
  return EXIT_OK;
14116
16994
  }
14117
16995
  async function cmdValidate(argv) {
14118
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16996
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
14119
16997
  const target = positionals[0];
14120
16998
  if (!target) throw new UsageError("vos validate <actions.json|take>");
14121
16999
  const r = createReporter(flags.json === true);
14122
- const kitTarget = target.endsWith("kit.json") ? target : existsSync23(join25(target, "kit.json")) && !isTakeDir(target) ? join25(target, "kit.json") : null;
17000
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync27(join29(target, "kit.json")) && !isTakeDir(target) ? join29(target, "kit.json") : null;
14123
17001
  if (kitTarget) {
14124
17002
  const picture = flags.picture === true;
14125
17003
  const verdict = await validateKit(kitTarget, { picture });
@@ -14151,9 +17029,9 @@ async function cmdValidate(argv) {
14151
17029
  r.done({ valid: true, target }, `${target}: valid actions file`);
14152
17030
  return EXIT_OK;
14153
17031
  }
14154
- if (!isTakeDir(target) && existsSync23(join25(target, "config.json"))) {
17032
+ if (!isTakeDir(target) && existsSync27(join29(target, "config.json"))) {
14155
17033
  const parsed = JSON.parse(
14156
- await readFile13(join25(target, "config.json"), "utf8")
17034
+ await readFile15(join29(target, "config.json"), "utf8")
14157
17035
  );
14158
17036
  const pre = preflightConfig(parsed);
14159
17037
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -14194,7 +17072,8 @@ async function cmdValidate(argv) {
14194
17072
  const take = await loadTake(target);
14195
17073
  const problems = [];
14196
17074
  const warnings = [];
14197
- if (!existsSync23(take.paths.recording)) {
17075
+ const notes = [];
17076
+ if (!existsSync27(take.paths.recording)) {
14198
17077
  problems.push("missing recording (re-run record)");
14199
17078
  } else {
14200
17079
  const mismatch = containerMismatch(
@@ -14220,6 +17099,7 @@ async function cmdValidate(argv) {
14220
17099
  const lint = lintDoc(take.doc);
14221
17100
  problems.push(...lint.problems.map((p) => `doc.json: ${p}`));
14222
17101
  warnings.push(...lint.warnings.map((w) => `doc.json: ${w}`));
17102
+ notes.push(...(lint.notes ?? []).map((n) => `doc.json: ${n}`));
14223
17103
  const probe3 = mediaProbeLints(
14224
17104
  await probeMediaUrls(collectDocMediaUrls(take.doc))
14225
17105
  );
@@ -14258,26 +17138,37 @@ async function cmdValidate(argv) {
14258
17138
  }
14259
17139
  if (problems.length) {
14260
17140
  r.done(
14261
- { valid: false, target, problems, warnings },
17141
+ {
17142
+ valid: false,
17143
+ target,
17144
+ problems,
17145
+ warnings,
17146
+ ...notes.length ? { notes } : {}
17147
+ },
14262
17148
  `${target}:
14263
17149
  ${problems.join("\n ")}${warnings.length ? `
14264
17150
  warnings:
14265
- ${warnings.join("\n ")}` : ""}`
17151
+ ${warnings.join("\n ")}` : ""}${notes.length ? `
17152
+ notes:
17153
+ ${notes.join("\n ")}` : ""}`
14266
17154
  );
14267
17155
  return EXIT_ERROR;
14268
17156
  }
17157
+ const noteLines = notes.length ? `
17158
+ notes:
17159
+ ${notes.join("\n ")}` : "";
14269
17160
  r.done(
14270
- { valid: true, target, warnings },
17161
+ { valid: true, target, warnings, ...notes.length ? { notes } : {} },
14271
17162
  `${target}: take is complete and renderable${warnings.length ? `
14272
17163
  warnings:
14273
- ${warnings.join("\n ")}` : ""}`
17164
+ ${warnings.join("\n ")}` : ""}${noteLines}`
14274
17165
  );
14275
17166
  return EXIT_OK;
14276
17167
  }
14277
17168
  async function cmdPush3(argv) {
14278
17169
  const { positionals, flags, multi } = parseArgs(
14279
17170
  argv,
14280
- BOOLEAN_FLAGS12,
17171
+ BOOLEAN_FLAGS15,
14281
17172
  MULTI_FLAGS2
14282
17173
  );
14283
17174
  const dir = positionals[0];
@@ -14287,7 +17178,7 @@ async function cmdPush3(argv) {
14287
17178
  );
14288
17179
  const refusal = takePushRefusal({
14289
17180
  vos: strFlag(flags, "vos"),
14290
- trackedVosId: readSyncState(resolve14(dir))?.vosId ?? null,
17181
+ trackedVosId: readSyncState(resolve18(dir))?.vosId ?? null,
14291
17182
  programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
14292
17183
  (name) => hasFlag(flags, name)
14293
17184
  )
@@ -14303,7 +17194,7 @@ async function cmdPush3(argv) {
14303
17194
  `--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
14304
17195
  );
14305
17196
  const claim = await pushTakeClaimable(
14306
- resolve14(dir),
17197
+ resolve18(dir),
14307
17198
  {
14308
17199
  api: strFlag(flags, "api"),
14309
17200
  origin: strFlag(flags, "origin"),
@@ -14325,7 +17216,7 @@ reference and the only credential.`
14325
17216
  }
14326
17217
  const overrides = multi.override;
14327
17218
  const result = await pushTake(
14328
- resolve14(dir),
17219
+ resolve18(dir),
14329
17220
  {
14330
17221
  key: strFlag(flags, "key"),
14331
17222
  api: strFlag(flags, "api"),
@@ -14375,11 +17266,11 @@ ${landedLines(landed)}` : "")
14375
17266
  return EXIT_OK;
14376
17267
  }
14377
17268
  async function cmdPull2(argv) {
14378
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12, MULTI_FLAGS2);
17269
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15, MULTI_FLAGS2);
14379
17270
  const dir = positionals[0] ?? ".";
14380
17271
  const r = createReporter(flags.json === true);
14381
17272
  const result = await pullTake(
14382
- resolve14(dir),
17273
+ resolve18(dir),
14383
17274
  {
14384
17275
  key: strFlag(flags, "key"),
14385
17276
  api: strFlag(flags, "api"),
@@ -14511,6 +17402,14 @@ async function run(argv) {
14511
17402
  return await cmdActions(rest);
14512
17403
  case "ingest":
14513
17404
  return await cmdIngest(rest);
17405
+ case "check-video":
17406
+ return await cmdCheckVideo(rest);
17407
+ case "join":
17408
+ return await cmdJoin(rest);
17409
+ case "score":
17410
+ return await cmdScore(rest);
17411
+ case "look":
17412
+ return await cmdLook(rest);
14514
17413
  default:
14515
17414
  throw new UsageError(`unknown command "${cmd}" \u2014 run: vos help`);
14516
17415
  }
@@ -14520,7 +17419,7 @@ async function run(argv) {
14520
17419
  `);
14521
17420
  return EXIT_USAGE;
14522
17421
  }
14523
- if (e instanceof SetupError || e instanceof SetupEnvError) {
17422
+ if (e instanceof SetupError || e instanceof SetupEnvError || e instanceof RecordRefusedError) {
14524
17423
  process.stderr.write(`${e.message}
14525
17424
  `);
14526
17425
  return EXIT_USAGE;
@@ -14543,7 +17442,7 @@ async function run(argv) {
14543
17442
  }
14544
17443
  }
14545
17444
  async function cmdSession(argv) {
14546
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
17445
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
14547
17446
  const [sub, arg] = positionals;
14548
17447
  const r = createReporter(flags.json === true);
14549
17448
  const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
@@ -14579,7 +17478,7 @@ async function cmdSession(argv) {
14579
17478
  if (!arg) throw new UsageError(usage);
14580
17479
  const bad = sessionNameVerdict(arg);
14581
17480
  if (bad) throw new UsageError(bad);
14582
- if (!existsSync23(sessionDir(arg)))
17481
+ if (!existsSync27(sessionDir(arg)))
14583
17482
  throw new UsageError(`no session "${arg}" (vos session list)`);
14584
17483
  const summary3 = await summarizeSession(arg);
14585
17484
  const url = strFlag(flags, "url");
@@ -14674,9 +17573,10 @@ export {
14674
17573
  convertAgentBrowser,
14675
17574
  MULTI_FLAGS2 as MULTI_FLAGS,
14676
17575
  HELP,
17576
+ strictFails,
14677
17577
  takeBrowserArgs,
14678
17578
  takeBackdrop,
14679
17579
  verbHelp,
14680
17580
  run
14681
17581
  };
14682
- //# sourceMappingURL=chunk-SB622PZM.js.map
17582
+ //# sourceMappingURL=chunk-INRUYQ6J.js.map