@vosjs/cli 0.68.0 → 0.69.1

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,7 +9,7 @@ import {
9
9
  reencodeStill,
10
10
  renderStill,
11
11
  videoFormat
12
- } from "./chunk-5GZ3GPAK.js";
12
+ } from "./chunk-YZAM4W7B.js";
13
13
  import {
14
14
  PREV_DOC_NAME,
15
15
  RECORDING_NAME,
@@ -131,13 +131,13 @@ function landedLines(l) {
131
131
  }
132
132
 
133
133
  // src/plugin/run.ts
134
- import { mkdir as mkdir9, readFile as readFile15, rm as rm3 } from "fs/promises";
135
- import { existsSync as existsSync24 } from "fs";
136
- import { join as join26, resolve as resolve15 } from "path";
134
+ import { mkdir as mkdir8, readFile as readFile12, rm as rm2 } from "fs/promises";
135
+ import { existsSync as existsSync22 } from "fs";
136
+ import { join as join24, resolve as resolve13 } from "path";
137
137
  import {
138
138
  END_CARD_FROM as END_CARD_FROM2,
139
139
  docOutputDuration as docOutputDuration3,
140
- migrateHostedDoc as migrateHostedDoc5
140
+ migrateHostedDoc as migrateHostedDoc4
141
141
  } from "@vosjs/studio-core";
142
142
 
143
143
  // src/plugin/output.ts
@@ -2120,10 +2120,10 @@ function startTakeServer(rootDir, pages) {
2120
2120
  res.writeHead(404).end();
2121
2121
  });
2122
2122
  return new Promise(
2123
- (resolve16) => server.listen(0, () => {
2123
+ (resolve14) => server.listen(0, () => {
2124
2124
  const addr = server.address();
2125
2125
  const port = typeof addr === "object" && addr ? addr.port : 0;
2126
- resolve16({ base: `http://localhost:${port}`, close: () => server.close() });
2126
+ resolve14({ base: `http://localhost:${port}`, close: () => server.close() });
2127
2127
  })
2128
2128
  );
2129
2129
  }
@@ -2173,8 +2173,8 @@ function renderPageHtml() {
2173
2173
  ${tweenRuntimeCode}
2174
2174
  // Vos Element System (bundled IIFE) \u2014 the same injection the engine's
2175
2175
  // render template performs. Take configs never touch ctx.elements, but a
2176
- // POSTER program (the deliver --poster leg) is element text + an image
2177
- // over the ground, and without this the elements silently never draw.
2176
+ // program may draw element text and images over its ground, and without
2177
+ // this the elements silently never draw.
2178
2178
  ${elementsBundleCode}
2179
2179
  window.__vos__ = window.__vos__ || {}
2180
2180
  window.__vos__.elements = __vosElementsFactory.createVosElements(THREE)
@@ -2360,14 +2360,14 @@ async function renderInPage(opts) {
2360
2360
  w.__error = String(e instanceof Error && e.stack || e);
2361
2361
  }
2362
2362
  }
2363
- async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress) {
2364
- const outName = `render-chunk-${chunk2.index}.tmp`;
2363
+ async function renderChunk(context2, serverBase, chunk, opts, onChunkProgress) {
2364
+ const outName = `render-chunk-${chunk.index}.tmp`;
2365
2365
  const page = await context2.newPage();
2366
2366
  try {
2367
2367
  page.on("console", (m) => {
2368
2368
  if (m.type() === "error" || process.env.VOS_RENDER_DEBUG)
2369
2369
  process.stderr.write(
2370
- ` [render page ${chunk2.index} ${m.type()}] ${m.text()}
2370
+ ` [render page ${chunk.index} ${m.type()}] ${m.text()}
2371
2371
  `
2372
2372
  );
2373
2373
  });
@@ -2375,7 +2375,7 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
2375
2375
  page.on(
2376
2376
  "pageerror",
2377
2377
  (e) => process.stderr.write(
2378
- ` [render pageerror ${chunk2.index}] ${String(e)}
2378
+ ` [render pageerror ${chunk.index}] ${String(e)}
2379
2379
  `
2380
2380
  )
2381
2381
  );
@@ -2394,8 +2394,8 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
2394
2394
  W: opts.width,
2395
2395
  H: opts.height,
2396
2396
  fps: opts.fps,
2397
- startFrame: chunk2.startFrame + frameOffset,
2398
- endFrame: chunk2.endFrame + frameOffset,
2397
+ startFrame: chunk.startFrame + frameOffset,
2398
+ endFrame: chunk.endFrame + frameOffset,
2399
2399
  format: opts.format,
2400
2400
  bitrate: opts.bitrate ?? 1e7,
2401
2401
  mediabunnyUrl: MEDIABUNNY_URL,
@@ -2406,10 +2406,10 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
2406
2406
  audioDuration: audio?.duration
2407
2407
  }).catch(() => {
2408
2408
  });
2409
- const timeoutMs = 12e4 + chunk2.frameCount * 500;
2409
+ const timeoutMs = 12e4 + chunk.frameCount * 500;
2410
2410
  await waitForPageDone(
2411
2411
  page,
2412
- `render chunk ${chunk2.index}`,
2412
+ `render chunk ${chunk.index}`,
2413
2413
  onChunkProgress,
2414
2414
  timeoutMs
2415
2415
  );
@@ -2441,8 +2441,8 @@ async function renderAnimation(browser, opts) {
2441
2441
  };
2442
2442
  const outNames = await Promise.all(
2443
2443
  chunks.map(
2444
- (chunk2) => renderChunk(context2, server.base, chunk2, effOpts, (fraction) => {
2445
- progress[chunk2.index] = fraction;
2444
+ (chunk) => renderChunk(context2, server.base, chunk, effOpts, (fraction) => {
2445
+ progress[chunk.index] = fraction;
2446
2446
  reportProgress();
2447
2447
  })
2448
2448
  )
@@ -2457,9 +2457,9 @@ async function renderAnimation(browser, opts) {
2457
2457
  return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
2458
2458
  }
2459
2459
  const { bytes } = await concatEncodedVideo(
2460
- chunks.map((chunk2, i) => ({
2460
+ chunks.map((chunk, i) => ({
2461
2461
  data: new Uint8Array(files[i]),
2462
- duration: chunk2.duration
2462
+ duration: chunk.duration
2463
2463
  })),
2464
2464
  { format: opts.format, frameRate: opts.fps }
2465
2465
  );
@@ -3401,11 +3401,10 @@ async function writeIndexJson(result) {
3401
3401
  }
3402
3402
 
3403
3403
  // src/plugin/deliver.ts
3404
- import { mkdir as mkdir3, mkdtemp, rename as rename3, rm as rm2, stat, writeFile as writeFile5 } from "fs/promises";
3405
- import { tmpdir } from "os";
3406
- import { join as join5, relative, resolve as resolve4 } from "path";
3407
- import { existsSync as existsSync5 } from "fs";
3408
- import { readFile as readFile5 } from "fs/promises";
3404
+ import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
3405
+ import { join as join4, relative, resolve as resolve3 } from "path";
3406
+ import { existsSync as existsSync4 } from "fs";
3407
+ import { readFile as readFile4 } from "fs/promises";
3409
3408
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
3410
3409
  import {
3411
3410
  CHANNEL_SPECS_VERIFIED,
@@ -3523,614 +3522,119 @@ async function renderTake(browser, dir, outFile, opts) {
3523
3522
  };
3524
3523
  }
3525
3524
 
3525
+ // src/plugin/motionPlan.ts
3526
+ import {
3527
+ END_CARD_FROM,
3528
+ END_CARD_SECONDS,
3529
+ applyTemplate,
3530
+ docOutputDuration,
3531
+ dropTemplate,
3532
+ endCardClips,
3533
+ migrateMotion,
3534
+ ratedSegments as ratedSegments4,
3535
+ sameMedia,
3536
+ spanOutputExtent as spanOutputExtent4
3537
+ } from "@vosjs/studio-core";
3538
+
3526
3539
  // src/plugin/moments.ts
3527
- import { ratedSegments as ratedSegments3, spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
3540
+ import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
3541
+ var STEP_SETTLE_SECONDS = 0.4;
3542
+ function stepOutputTime(rated, step, settle) {
3543
+ const src = step.tEnd + settle;
3544
+ const ext = spanOutputExtent3(rated, src, src + 1e-3);
3545
+ if (!ext) return null;
3546
+ return ext.start;
3547
+ }
3528
3548
 
3529
- // src/plugin/picture.ts
3530
- import { inflateSync } from "zlib";
3531
- var PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
3532
- function decodePng(bytes) {
3533
- if (bytes.length < 33) return null;
3534
- for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIG[i]) return null;
3535
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3536
- let pos = 8;
3537
- let w = 0;
3538
- let h = 0;
3539
- let depth = 0;
3540
- let colorType = 0;
3541
- let interlace = 0;
3542
- const idat = [];
3543
- while (pos + 8 <= bytes.length) {
3544
- const len = view.getUint32(pos);
3545
- const type = String.fromCharCode(
3546
- bytes[pos + 4],
3547
- bytes[pos + 5],
3548
- bytes[pos + 6],
3549
- bytes[pos + 7]
3549
+ // src/plugin/motionPlan.ts
3550
+ var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
3551
+ "x-feed-cut",
3552
+ "youtube-main-demo",
3553
+ "shorts-linkedin-vertical-cut"
3554
+ ]);
3555
+ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
3556
+ var BED_ID = "bed";
3557
+ var CLICK_ID_PREFIX = "click-";
3558
+ var CAPTION_ID_PREFIX = "caption-";
3559
+ var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
3560
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
3561
+ var TRANSITION_CLIP_MIN = 1;
3562
+ function proposeTransitions(input, kind) {
3563
+ const doc = structuredClone(input);
3564
+ const skipped = [];
3565
+ let count = 0;
3566
+ const steps = doc.source.meta.steps ?? [];
3567
+ for (const step of steps) {
3568
+ if (!step.navigated || step.skipped) continue;
3569
+ const name = step.id ?? String(step.step);
3570
+ const wait = step.do === "wait";
3571
+ const outEnd = round3(wait ? step.tStart : step.tEnd);
3572
+ const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
3573
+ const segs = doc.segments;
3574
+ if (segs.some(
3575
+ (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
3576
+ ))
3577
+ continue;
3578
+ const i = segs.findIndex(
3579
+ (s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
3550
3580
  );
3551
- const start = pos + 8;
3552
- if (type === "IHDR") {
3553
- w = view.getUint32(start);
3554
- h = view.getUint32(start + 4);
3555
- depth = bytes[start + 8];
3556
- colorType = bytes[start + 9];
3557
- interlace = bytes[start + 12];
3558
- } else if (type === "IDAT") {
3559
- idat.push(bytes.subarray(start, start + len));
3560
- } else if (type === "IEND") break;
3561
- pos = start + len + 4;
3562
- }
3563
- if (!w || !h || depth !== 8 || interlace !== 0) return null;
3564
- const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
3565
- if (!channels) return null;
3566
- const total = idat.reduce((n, c) => n + c.length, 0);
3567
- const joined = new Uint8Array(total);
3568
- let off2 = 0;
3569
- for (const c of idat) {
3570
- joined.set(c, off2);
3571
- off2 += c.length;
3572
- }
3573
- let raw;
3574
- try {
3575
- raw = new Uint8Array(inflateSync(joined));
3576
- } catch {
3577
- return null;
3578
- }
3579
- const stride = w * channels;
3580
- if (raw.length < (stride + 1) * h) return null;
3581
- const out = new Uint8Array(w * h * 4);
3582
- const prev = new Uint8Array(stride);
3583
- const cur = new Uint8Array(stride);
3584
- for (let y = 0; y < h; y++) {
3585
- const filter = raw[y * (stride + 1)];
3586
- const rowStart = y * (stride + 1) + 1;
3587
- for (let i = 0; i < stride; i++) {
3588
- const x = raw[rowStart + i];
3589
- const a = i >= channels ? cur[i - channels] : 0;
3590
- const b = prev[i];
3591
- const c = i >= channels ? prev[i - channels] : 0;
3592
- let v;
3593
- switch (filter) {
3594
- case 0:
3595
- v = x;
3596
- break;
3597
- case 1:
3598
- v = x + a;
3599
- break;
3600
- case 2:
3601
- v = x + b;
3602
- break;
3603
- case 3:
3604
- v = x + (a + b >> 1);
3605
- break;
3606
- case 4: {
3607
- const p = a + b - c;
3608
- const pa = Math.abs(p - a);
3609
- const pb = Math.abs(p - b);
3610
- const pc = Math.abs(p - c);
3611
- v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
3612
- break;
3613
- }
3614
- default:
3615
- return null;
3616
- }
3617
- cur[i] = v & 255;
3618
- }
3619
- for (let px = 0; px < w; px++) {
3620
- const o = (y * w + px) * 4;
3621
- const s = px * channels;
3622
- if (channels >= 3) {
3623
- out[o] = cur[s];
3624
- out[o + 1] = cur[s + 1];
3625
- out[o + 2] = cur[s + 2];
3626
- out[o + 3] = channels === 4 ? cur[s + 3] : 255;
3627
- } else {
3628
- out[o] = out[o + 1] = out[o + 2] = cur[s];
3629
- out[o + 3] = channels === 2 ? cur[s + 1] : 255;
3630
- }
3581
+ if (i < 0) {
3582
+ skipped.push(
3583
+ `no transition at step ${name}: the page change sits too close to a clip's edge`
3584
+ );
3585
+ continue;
3631
3586
  }
3632
- prev.set(cur);
3587
+ const seg = segs[i];
3588
+ const before = { ...seg, out: outEnd, anim: { exit: kind } };
3589
+ const after = { ...seg, in: inStart, anim: { enter: kind } };
3590
+ if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
3591
+ if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
3592
+ doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
3593
+ count++;
3633
3594
  }
3634
- return { w, h, data: out };
3595
+ return { doc, count, skipped };
3635
3596
  }
3636
- var at = (img, x, y) => {
3637
- const o = (y * img.w + x) * 4;
3638
- return [img.data[o], img.data[o + 1], img.data[o + 2]];
3639
- };
3640
- var median = (a) => {
3641
- const s = [...a].sort((x, y) => x - y);
3642
- return s[s.length >> 1] ?? 0;
3643
- };
3644
- function groundColour(img) {
3645
- const ring = Math.max(1, Math.round(Math.min(img.w, img.h) * 0.01));
3646
- const r = [];
3647
- const g = [];
3648
- const b = [];
3649
- const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 400));
3650
- for (let y = 0; y < img.h; y += step) {
3651
- for (let x = 0; x < img.w; x += step) {
3652
- if (x < ring || y < ring || x >= img.w - ring || y >= img.h - ring) {
3653
- const p = at(img, x, y);
3654
- r.push(p[0]);
3655
- g.push(p[1]);
3656
- b.push(p[2]);
3657
- }
3658
- }
3597
+ function pickTrack(catalog, ask) {
3598
+ if (!catalog || !ask || off(ask)) return null;
3599
+ const want = ask.trim().toLowerCase();
3600
+ return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
3601
+ }
3602
+ function clickTimes(doc, range) {
3603
+ const rated = ratedSegments4(doc);
3604
+ const out = [];
3605
+ for (const e of doc.source.cursor) {
3606
+ if (e.type !== "down") continue;
3607
+ const src = e.t / 1e3;
3608
+ const ext = spanOutputExtent4(rated, src, src + 1e-3);
3609
+ if (!ext) continue;
3610
+ const t = ext.start;
3611
+ if (t < range[0] || t > range[1]) continue;
3612
+ if (out.length && t - out[out.length - 1] < 0.12) continue;
3613
+ out.push(+(t - range[0]).toFixed(3));
3659
3614
  }
3660
- return [median(r), median(g), median(b)];
3615
+ return out;
3661
3616
  }
3662
- var hex = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
3663
- var delta = (p, q) => Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
3664
- function cardBounds(img, ground, threshold = 60) {
3665
- let x0 = img.w;
3666
- let y0 = img.h;
3667
- let x1 = -1;
3668
- let y1 = -1;
3669
- const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 700));
3670
- for (let y = 0; y < img.h; y += step) {
3671
- for (let x = 0; x < img.w; x += step) {
3672
- if (delta(at(img, x, y), ground) > threshold) {
3673
- if (x < x0) x0 = x;
3674
- if (y < y0) y0 = y;
3675
- if (x > x1) x1 = x;
3676
- if (y > y1) y1 = y;
3677
- }
3678
- }
3617
+ var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
3618
+ const rate = s.rate && s.rate > 0 ? s.rate : 1;
3619
+ return acc + (s.out - s.in) / rate;
3620
+ }, 0);
3621
+ function templateWords(words2) {
3622
+ const headline = (words2.headline ?? "").trim();
3623
+ const kicker = (words2.kicker ?? "").trim();
3624
+ const brand = (words2.brand ?? "").trim();
3625
+ const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
3626
+ const out = {};
3627
+ if (headline) {
3628
+ out["stage-title"] = headline;
3629
+ out["endcard-title"] = headline;
3679
3630
  }
3680
- if (x1 < 0) return null;
3681
- return { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 };
3682
- }
3683
- function haloReading(img, ground, card) {
3684
- const band = Math.max(2, Math.round(Math.min(img.w, img.h) * 0.012));
3685
- const meanRect = (x0, y0, x1, y1) => {
3686
- let sum = 0;
3687
- let n = 0;
3688
- const sx = Math.max(1, Math.round((x1 - x0) / 60));
3689
- const sy = Math.max(1, Math.round((y1 - y0) / 60));
3690
- for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
3691
- for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
3692
- sum += delta(at(img, x, y), ground);
3693
- n++;
3694
- }
3695
- }
3696
- return n ? sum / n : 0;
3697
- };
3698
- const rise = (bands) => {
3699
- const [outer, mid] = bands;
3700
- return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
3701
- };
3702
- const readings = [];
3703
- const yA = card.y + card.h * 0.2;
3704
- const yB = card.y + card.h * 0.8;
3705
- if (card.x >= 1 && card.w > band * 4) {
3706
- const x0 = card.x;
3707
- readings.push(
3708
- rise([
3709
- meanRect(x0, yA, x0 + band, yB),
3710
- meanRect(x0 + band, yA, x0 + band * 2, yB),
3711
- meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
3712
- ])
3713
- );
3714
- }
3715
- if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
3716
- const x1 = card.x + card.w;
3717
- readings.push(
3718
- rise([
3719
- meanRect(x1 - band, yA, x1, yB),
3720
- meanRect(x1 - band * 2, yA, x1 - band, yB),
3721
- meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
3722
- ])
3723
- );
3724
- }
3725
- if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
3726
- const y1 = card.y + card.h;
3727
- const xA = card.x + card.w * 0.2;
3728
- const xB = card.x + card.w * 0.8;
3729
- readings.push(
3730
- rise([
3731
- meanRect(xA, y1 - band, xB, y1),
3732
- meanRect(xA, y1 - band * 2, xB, y1 - band),
3733
- meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
3734
- ])
3735
- );
3736
- }
3737
- return readings.length ? Math.max(...readings) : null;
3738
- }
3739
- function edgeContrast(img, ground, card) {
3740
- const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
3741
- const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
3742
- const col = (x) => {
3743
- if (x < 0 || x >= img.w) return 0;
3744
- const ds = [];
3745
- const step = Math.max(1, Math.round((yB - yA) / 80));
3746
- for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
3747
- return median(ds);
3748
- };
3749
- const depth = Math.min(24, Math.floor(card.w / 8));
3750
- let best = 0;
3751
- for (let d = 0; d < depth; d++) {
3752
- best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
3753
- }
3754
- return best;
3755
- }
3756
- function inkCoverage(img, rect, threshold = 48) {
3757
- const x0 = Math.max(0, Math.floor(rect.x));
3758
- const y0 = Math.max(0, Math.floor(rect.y));
3759
- const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
3760
- const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
3761
- if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
3762
- const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
3763
- const r = [];
3764
- const g = [];
3765
- const b = [];
3766
- const pts = [];
3767
- for (let y = y0; y < y1; y += step) {
3768
- for (let x = x0; x < x1; x += step) {
3769
- const p = at(img, x, y);
3770
- pts.push(p);
3771
- r.push(p[0]);
3772
- g.push(p[1]);
3773
- b.push(p[2]);
3774
- }
3775
- }
3776
- const base = [median(r), median(g), median(b)];
3777
- let ink = 0;
3778
- for (const p of pts) if (delta(p, base) > threshold) ink++;
3779
- return pts.length ? ink / pts.length : 0;
3780
- }
3781
- function differenceHash(img, rect) {
3782
- const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
3783
- const cols = 9;
3784
- const rows = 8;
3785
- const cell = [];
3786
- for (let cy = 0; cy < rows; cy++) {
3787
- for (let cx = 0; cx < cols; cx++) {
3788
- const x0 = Math.floor(r.x + cx * r.w / cols);
3789
- const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
3790
- const y0 = Math.floor(r.y + cy * r.h / rows);
3791
- const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
3792
- let sum = 0;
3793
- let n = 0;
3794
- const sx = Math.max(1, Math.floor((x1 - x0) / 8));
3795
- const sy = Math.max(1, Math.floor((y1 - y0) / 8));
3796
- for (let y = y0; y < y1 && y < img.h; y += sy) {
3797
- for (let x = x0; x < x1 && x < img.w; x += sx) {
3798
- const p = at(img, Math.max(0, x), Math.max(0, y));
3799
- sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
3800
- n++;
3801
- }
3802
- }
3803
- cell.push(n ? sum / n : 0);
3804
- }
3805
- }
3806
- let bits = "";
3807
- for (let cy = 0; cy < rows; cy++) {
3808
- let byte = 0;
3809
- for (let cx = 0; cx < cols - 1; cx++) {
3810
- const l = cell[cy * cols + cx];
3811
- const rr = cell[cy * cols + cx + 1];
3812
- byte = byte << 1 | (l > rr ? 1 : 0);
3813
- }
3814
- bits += byte.toString(16).padStart(2, "0");
3815
- }
3816
- return bits;
3817
- }
3818
- function hammingDistance(a, b) {
3819
- let d = 0;
3820
- for (let i = 0; i < Math.min(a.length, b.length); i++) {
3821
- let x = parseInt(a[i], 16) ^ parseInt(b[i], 16);
3822
- while (x) {
3823
- d += x & 1;
3824
- x >>= 1;
3825
- }
3826
- }
3827
- return d + Math.abs(a.length - b.length) * 4;
3828
- }
3829
- function luminance(c) {
3830
- const lin = (v) => {
3831
- const s = v / 255;
3832
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
3833
- };
3834
- return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
3835
- }
3836
- function lightness(c) {
3837
- const y = luminance(c);
3838
- return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
3839
- }
3840
- function medianColour(img, rect) {
3841
- const r = [];
3842
- const g = [];
3843
- const b = [];
3844
- const x0 = Math.max(0, Math.floor(rect.x));
3845
- const y0 = Math.max(0, Math.floor(rect.y));
3846
- const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
3847
- const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
3848
- const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
3849
- for (let y = y0; y < y1; y += step) {
3850
- for (let x = x0; x < x1; x += step) {
3851
- const p = at(img, x, y);
3852
- r.push(p[0]);
3853
- g.push(p[1]);
3854
- b.push(p[2]);
3855
- }
3856
- }
3857
- return [median(r), median(g), median(b)];
3858
- }
3859
- function edgeEnergy(img, width) {
3860
- const cols = Math.max(2, Math.min(width, img.w));
3861
- const rows = Math.max(2, Math.round(cols * img.h / img.w));
3862
- const lum = [];
3863
- for (let cy = 0; cy < rows; cy++) {
3864
- for (let cx = 0; cx < cols; cx++) {
3865
- const x = Math.min(img.w - 1, Math.floor((cx + 0.5) * img.w / cols));
3866
- const y = Math.min(img.h - 1, Math.floor((cy + 0.5) * img.h / rows));
3867
- const p = at(img, x, y);
3868
- lum.push(0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]);
3869
- }
3870
- }
3871
- let sum = 0;
3872
- let n = 0;
3873
- for (let cy = 0; cy < rows; cy++) {
3874
- for (let cx = 1; cx < cols; cx++) {
3875
- sum += Math.abs(lum[cy * cols + cx] - lum[cy * cols + cx - 1]);
3876
- n++;
3877
- }
3878
- }
3879
- return n ? sum / n : 0;
3880
- }
3881
- function measureStill(img) {
3882
- const g = groundColour(img);
3883
- const strong = cardBounds(img, g, 60);
3884
- const faint = cardBounds(img, g, 12) ?? strong;
3885
- const card = faint;
3886
- const bleed = [];
3887
- let pad = null;
3888
- let widthPct = null;
3889
- let shadow = null;
3890
- let edge = 0;
3891
- let separation = null;
3892
- if (card) {
3893
- pad = {
3894
- left: card.x / img.w,
3895
- right: (img.w - card.x - card.w) / img.w,
3896
- top: card.y / img.h,
3897
- bottom: (img.h - card.y - card.h) / img.h
3898
- };
3899
- for (const side of ["left", "right", "top", "bottom"])
3900
- if (pad[side] < 5e-3) bleed.push(side);
3901
- widthPct = card.w / img.w;
3902
- shadow = haloReading(img, g, card);
3903
- edge = edgeContrast(img, g, card);
3904
- const body = strong ?? card;
3905
- const inner = {
3906
- x: body.x + body.w * 0.1,
3907
- y: body.y + body.h * 0.1,
3908
- w: body.w * 0.8,
3909
- h: body.h * 0.8
3910
- };
3911
- separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
3912
- }
3913
- const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
3914
- return {
3915
- w: img.w,
3916
- h: img.h,
3917
- ground: hex(g),
3918
- card,
3919
- widthPct,
3920
- pad,
3921
- bleed,
3922
- shadow,
3923
- edge,
3924
- ink: inkCoverage(img, inkRect),
3925
- separation,
3926
- hash: differenceHash(img, inkRect)
3927
- };
3928
- }
3929
-
3930
- // src/plugin/moments.ts
3931
- var STEP_SETTLE_SECONDS = 0.4;
3932
- var NOT_A_GESTURE = /* @__PURE__ */ new Set(["wait", "goto", "sleep"]);
3933
- function momentCandidates(doc, duration) {
3934
- const rated = ratedSegments3(doc);
3935
- const out = [];
3936
- const push = (c) => {
3937
- if (c.time < 0 || c.time > duration) return;
3938
- if (out.some((o) => Math.abs(o.time - c.time) < 0.25)) return;
3939
- out.push(c);
3940
- };
3941
- const steps = doc.source.meta.steps ?? [];
3942
- for (let i = 0; i < steps.length; i++) {
3943
- const s = steps[i];
3944
- if (s.skipped || NOT_A_GESTURE.has(s.do)) continue;
3945
- const t = stepOutputTime(rated, s, settleAfter(steps, i));
3946
- if (t === null) continue;
3947
- push({ time: t, source: "step", step: s.id ?? s.step });
3948
- }
3949
- let dropped = 0;
3950
- const apexes = [];
3951
- for (const z of doc.zoom) {
3952
- const ext = spanOutputExtent3(rated, z.in, z.out);
3953
- if (ext) apexes.push((ext.start + ext.end) / 2);
3954
- else dropped++;
3955
- }
3956
- for (const t of apexes.sort((a, b) => a - b))
3957
- push({ time: t, source: "zoom" });
3958
- if (!out.length) {
3959
- for (const p of [0.1, 0.3, 0.5, 0.7, 0.9])
3960
- push({ time: p * duration, source: "spread" });
3961
- }
3962
- return { candidates: out, dropped };
3963
- }
3964
- function settleAfter(steps, i) {
3965
- const next = steps[i + 1];
3966
- if (next && next.do === "wait" && !next.skipped)
3967
- return Math.max(0, next.tEnd - steps[i].tEnd);
3968
- return STEP_SETTLE_SECONDS;
3969
- }
3970
- function stepOutputTime(rated, step, settle) {
3971
- const src = step.tEnd + settle;
3972
- const ext = spanOutputExtent3(rated, src, src + 1e-3);
3973
- if (!ext) return null;
3974
- return ext.start;
3975
- }
3976
- function resolveStepTime(doc, entry) {
3977
- const m = /^step:([^+-]+)([+-]\d+(?:\.\d+)?)?$/.exec(entry.trim());
3978
- if (!m) return null;
3979
- const key = m[1];
3980
- const offset = m[2] ? Number(m[2]) : STEP_SETTLE_SECONDS;
3981
- const steps = doc.source.meta.steps ?? [];
3982
- const step = steps.find((s) => s.id === key) ?? (/^\d+$/.test(key) ? steps.find((s) => s.step === Number(key)) : void 0);
3983
- if (!step) {
3984
- const known = steps.map((s) => s.id ?? String(s.step)).filter((s) => s.length);
3985
- throw new Error(
3986
- `--times ${entry}: no step "${key}" in the take's step timeline${known.length ? ` (steps: ${known.join(", ")})` : " (the take carries none: record it with the current CLI)"}`
3987
- );
3988
- }
3989
- const t = stepOutputTime(ratedSegments3(doc), step, offset);
3990
- if (t === null)
3991
- throw new Error(
3992
- `--times ${entry}: step "${key}" (${step.tEnd.toFixed(2)}s in the recording) falls outside the cut`
3993
- );
3994
- return t;
3995
- }
3996
- var DUPLICATE_BITS = 6;
3997
- var POPULATED_INK = 0.12;
3998
- function pickMoments(measured, opts = {}) {
3999
- const minInk = opts.minInk ?? 0.06;
4000
- const relative4 = opts.relativeInk ?? 0.4;
4001
- const bits = opts.duplicateBits ?? DUPLICATE_BITS;
4002
- const inks = measured.map((m) => m.ink).sort((a, b) => a - b);
4003
- const median2 = inks[inks.length >> 1] ?? 0;
4004
- const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative4));
4005
- const kept = [];
4006
- const dropped = [];
4007
- for (const m of measured) {
4008
- if (m.ink < floor) {
4009
- dropped.push(
4010
- `blank at ${m.time.toFixed(2)}s: ${Math.round(m.ink * 100)}% ink (the floor is ${Math.round(floor * 100)}%; the take's median is ${Math.round(median2 * 100)}%)`
4011
- );
4012
- continue;
4013
- }
4014
- const twin = kept.find((k) => hammingDistance(k.hash, m.hash) <= bits);
4015
- if (twin) {
4016
- dropped.push(
4017
- `${m.time.toFixed(2)}s is the same frame as ${twin.time.toFixed(2)}s`
4018
- );
4019
- continue;
4020
- }
4021
- kept.push(m);
4022
- }
4023
- if (!kept.length && measured.length) {
4024
- const best = [...measured].sort((a, b) => b.ink - a.ink)[0];
4025
- kept.push(best);
4026
- dropped.push(
4027
- `every candidate is under the blank floor; kept ${best.time.toFixed(2)}s (${Math.round(best.ink * 100)}% ink) as the least empty`
4028
- );
4029
- }
4030
- return { times: kept.map((k) => k.time), dropped };
4031
- }
4032
-
4033
- // src/plugin/motionPlan.ts
4034
- import {
4035
- END_CARD_FROM,
4036
- END_CARD_SECONDS,
4037
- applyTemplate,
4038
- docOutputDuration,
4039
- dropTemplate,
4040
- endCardClips,
4041
- migrateMotion,
4042
- ratedSegments as ratedSegments4,
4043
- sameMedia,
4044
- spanOutputExtent as spanOutputExtent4
4045
- } from "@vosjs/studio-core";
4046
- var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
4047
- "x-feed-cut",
4048
- "youtube-main-demo",
4049
- "shorts-linkedin-vertical-cut"
4050
- ]);
4051
- var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
4052
- var BED_ID = "bed";
4053
- var CLICK_ID_PREFIX = "click-";
4054
- var CAPTION_ID_PREFIX = "caption-";
4055
- var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
4056
- var round3 = (v) => Math.round(v * 1e3) / 1e3;
4057
- var TRANSITION_CLIP_MIN = 1;
4058
- function proposeTransitions(input, kind) {
4059
- const doc = structuredClone(input);
4060
- const skipped = [];
4061
- let count = 0;
4062
- const steps = doc.source.meta.steps ?? [];
4063
- for (const step of steps) {
4064
- if (!step.navigated || step.skipped) continue;
4065
- const name = step.id ?? String(step.step);
4066
- const wait = step.do === "wait";
4067
- const outEnd = round3(wait ? step.tStart : step.tEnd);
4068
- const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
4069
- const segs = doc.segments;
4070
- if (segs.some(
4071
- (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
4072
- ))
4073
- continue;
4074
- const i = segs.findIndex(
4075
- (s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
4076
- );
4077
- if (i < 0) {
4078
- skipped.push(
4079
- `no transition at step ${name}: the page change sits too close to a clip's edge`
4080
- );
4081
- continue;
4082
- }
4083
- const seg = segs[i];
4084
- const before = { ...seg, out: outEnd, anim: { exit: kind } };
4085
- const after = { ...seg, in: inStart, anim: { enter: kind } };
4086
- if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
4087
- if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
4088
- doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
4089
- count++;
4090
- }
4091
- return { doc, count, skipped };
4092
- }
4093
- function pickTrack(catalog, ask) {
4094
- if (!catalog || !ask || off(ask)) return null;
4095
- const want = ask.trim().toLowerCase();
4096
- return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
4097
- }
4098
- function clickTimes(doc, range) {
4099
- const rated = ratedSegments4(doc);
4100
- const out = [];
4101
- for (const e of doc.source.cursor) {
4102
- if (e.type !== "down") continue;
4103
- const src = e.t / 1e3;
4104
- const ext = spanOutputExtent4(rated, src, src + 1e-3);
4105
- if (!ext) continue;
4106
- const t = ext.start;
4107
- if (t < range[0] || t > range[1]) continue;
4108
- if (out.length && t - out[out.length - 1] < 0.12) continue;
4109
- out.push(+(t - range[0]).toFixed(3));
4110
- }
4111
- return out;
4112
- }
4113
- var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
4114
- const rate = s.rate && s.rate > 0 ? s.rate : 1;
4115
- return acc + (s.out - s.in) / rate;
4116
- }, 0);
4117
- function templateWords(words2) {
4118
- const headline = (words2.headline ?? "").trim();
4119
- const kicker = (words2.kicker ?? "").trim();
4120
- const brand = (words2.brand ?? "").trim();
4121
- const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
4122
- const out = {};
4123
- if (headline) {
4124
- out["stage-title"] = headline;
4125
- out["endcard-title"] = headline;
4126
- }
4127
- if (kicker) out["stage-kicker"] = kicker;
4128
- if (brand) {
4129
- out["stage-brand"] = brand;
4130
- out["endcard-mark"] = brand;
4131
- }
4132
- if (sub && sub !== headline) out["endcard-sub"] = sub;
4133
- return out;
3631
+ if (kicker) out["stage-kicker"] = kicker;
3632
+ if (brand) {
3633
+ out["stage-brand"] = brand;
3634
+ out["endcard-mark"] = brand;
3635
+ }
3636
+ if (sub && sub !== headline) out["endcard-sub"] = sub;
3637
+ return out;
4134
3638
  }
4135
3639
  function resolveAnchor(doc, at2) {
4136
3640
  if (typeof at2 !== "object") return at2;
@@ -4331,7 +3835,6 @@ function destinationMechanics(d, doc) {
4331
3835
  const set = [];
4332
3836
  const unset = [];
4333
3837
  const notes = [];
4334
- if (d.kind !== "video") return { set, unset, notes };
4335
3838
  const loop = LOOP_DESTINATIONS.has(d.id);
4336
3839
  const sound = SOUND_DESTINATIONS.has(d.id);
4337
3840
  const portrait = d.px.w / d.px.h < 0.9;
@@ -4383,161 +3886,12 @@ function isLightHexGround(hex2) {
4383
3886
  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
4384
3887
  }
4385
3888
 
4386
- // src/plugin/posterDoc.ts
4387
- import { existsSync as existsSync4 } from "fs";
4388
- import { readFile as readFile4 } from "fs/promises";
4389
- import { isAbsolute, join as join4, resolve as resolve3 } from "path";
4390
- import {
4391
- computeCardLayout,
4392
- docStillTime as docStillTime2,
4393
- migrateHostedDoc,
4394
- overlayPlainText,
4395
- overlayRect,
4396
- resolveOverlayStyle as resolveOverlayStyle2
4397
- } from "@vosjs/studio-core";
4398
- var POSTER_CLASSES = [
4399
- "landscape",
4400
- "square",
4401
- "portrait",
4402
- "tile"
4403
- ];
4404
- var TILE_MAX_PX = 700;
4405
- function posterClassFor(px) {
4406
- if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
4407
- const r = px.w / Math.max(1, px.h);
4408
- if (r < 0.87) return "portrait";
4409
- if (r <= 1.15) return "square";
4410
- return "landscape";
4411
- }
4412
- var ROLE_ALL = "poster";
4413
- var roleFor = (cls) => `poster-${cls}`;
4414
- async function readPosterAt(cls, path, takeDir, from) {
4415
- let file = path;
4416
- let docDir = path;
4417
- if (existsSync4(join4(path, "doc.json"))) {
4418
- file = join4(path, "doc.json");
4419
- } else if (!/\.json$/i.test(path) || !existsSync4(path)) {
4420
- return null;
4421
- } else {
4422
- docDir = resolve3(path, "..");
4423
- }
4424
- let raw;
4425
- try {
4426
- raw = JSON.parse(await readFile4(file, "utf8"));
4427
- } catch {
4428
- return null;
4429
- }
4430
- if (!raw || typeof raw !== "object") return null;
4431
- const source = raw.source;
4432
- if (!source || typeof source.videoKey !== "string") return null;
4433
- const doc = migrateHostedDoc(
4434
- raw
4435
- );
4436
- const ownFootage = existsSync4(join4(docDir, "meta.json")) && existsSync4(join4(docDir, source.videoKey));
4437
- const sync = readSyncState(docDir);
4438
- return {
4439
- cls,
4440
- file,
4441
- takeDir: ownFootage ? docDir : takeDir,
4442
- ownFootage,
4443
- doc,
4444
- ...sync?.vosId ? { vosId: sync.vosId } : {},
4445
- from
4446
- };
4447
- }
4448
- async function findPosterDocs(takeDir, launchRoles) {
4449
- const out = {};
4450
- const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
4451
- for (const cls of POSTER_CLASSES) {
4452
- const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
4453
- const candidates = [];
4454
- if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
4455
- const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
4456
- candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
4457
- }
4458
- candidates.push({
4459
- path: join4(takeDir, "poster", cls),
4460
- from: `poster/${cls}`
4461
- });
4462
- candidates.push({ path: join4(takeDir, "poster"), from: "poster" });
4463
- for (const c of candidates) {
4464
- const ref = await readPosterAt(cls, c.path, takeDir, c.from);
4465
- if (ref) {
4466
- out[cls] = ref;
4467
- break;
4468
- }
4469
- }
4470
- }
4471
- return out;
4472
- }
4473
- function posterStillTime(doc, duration) {
4474
- return Math.max(0, Math.min(duration, docStillTime2(doc)));
4475
- }
4476
- var DESIGN_H = 1080;
4477
- function designFrame(px) {
4478
- return {
4479
- W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
4480
- H: DESIGN_H
4481
- };
4482
- }
4483
- function posterShotRect(doc, px) {
4484
- const { W, H } = designFrame(px);
4485
- const meta = doc.source.meta;
4486
- const layout = computeCardLayout(
4487
- doc.frame,
4488
- {
4489
- width: meta.captureWidth ?? meta.width,
4490
- height: meta.captureHeight ?? meta.height
4491
- },
4492
- W,
4493
- H
4494
- );
4495
- const r3 = (v) => Math.round(v * 1e3) / 1e3;
4496
- return {
4497
- x: r3(layout.cardX / W),
4498
- y: r3(layout.cardY / H),
4499
- w: r3(layout.cardW / W),
4500
- h: r3(layout.cardH / H)
4501
- };
4502
- }
4503
- function estimateWidth(text, font, letterSpacingPx = 0) {
4504
- const m = /(\d+(?:\.\d+)?)px/.exec(font);
4505
- const px = m ? Number(m[1]) : 32;
4506
- return text.length * (px * 0.55 + letterSpacingPx);
4507
- }
4508
- function posterTextBoxes(doc, px, time) {
4509
- const { W, H } = designFrame(px);
4510
- const out = [];
4511
- for (const clip of doc.overlays ?? []) {
4512
- if (clip.kind !== "text") continue;
4513
- if (time < clip.start || time > clip.start + clip.duration) continue;
4514
- const rect = overlayRect(clip, estimateWidth, W, H);
4515
- const style = resolveOverlayStyle2(clip);
4516
- const r3 = (v) => Math.round(v * 1e3) / 1e3;
4517
- out.push({
4518
- x: r3((rect.cx - rect.w / 2) / W),
4519
- y: r3((rect.cy - rect.h / 2) / H),
4520
- w: r3(rect.w / W),
4521
- h: r3(rect.h / H),
4522
- role: clip.preset === "title" ? "headline" : "body",
4523
- label: overlayPlainText(clip).split("\n")[0].slice(0, 40),
4524
- .../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
4525
- });
4526
- }
4527
- return out;
4528
- }
4529
-
4530
3889
  // src/plugin/deliver.ts
4531
3890
  var CHANNEL_ALIASES = {
4532
- ph: "producthunt",
4533
3891
  yt: "youtube",
4534
3892
  gh: "github",
4535
- li: "linkedin",
4536
3893
  shorts: "shorts-linkedin"
4537
3894
  };
4538
- var NOT_FROM_FOOTAGE = {
4539
- "cws-icon": "an icon is brand art, not footage \u2014 supply it from the kit"
4540
- };
4541
3895
  function resolveChannels(raw) {
4542
3896
  const known = [...new Set(DESTINATIONS.map((d) => d.channel))];
4543
3897
  const wanted = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
@@ -4548,26 +3902,26 @@ function resolveChannels(raw) {
4548
3902
  const id = CHANNEL_ALIASES[w] ?? w;
4549
3903
  if (!known.includes(id))
4550
3904
  throw new Error(
4551
- `unknown channel "${w}" \u2014 channels: ${known.join(", ")} (or all)`
3905
+ `unknown channel "${w}" \u2014 deliver renders video cuts for: ${known.join(", ")} (or all)`
4552
3906
  );
4553
3907
  if (!out.includes(id)) out.push(id);
4554
3908
  }
4555
3909
  return out;
4556
3910
  }
4557
3911
  async function readBrandBesideTake(dir, explicit) {
4558
- const candidates = explicit ? [explicit] : [join5(dir, "BRAND.md"), join5(dir, "..", "BRAND.md")];
3912
+ const candidates = explicit ? [explicit] : [join4(dir, "BRAND.md"), join4(dir, "..", "BRAND.md")];
4559
3913
  for (const file of candidates) {
4560
- if (!existsSync5(file)) continue;
4561
- const roles = parseFrontmatter(await readFile5(file, "utf8"));
3914
+ if (!existsSync4(file)) continue;
3915
+ const roles = parseFrontmatter(await readFile4(file, "utf8"));
4562
3916
  return { file, roles };
4563
3917
  }
4564
3918
  return null;
4565
3919
  }
4566
3920
  async function readLaunchBesideTake(dir, explicit) {
4567
- const candidates = explicit ? [explicit] : [join5(dir, "LAUNCH.md"), join5(dir, "..", "LAUNCH.md")];
3921
+ const candidates = explicit ? [explicit] : [join4(dir, "LAUNCH.md"), join4(dir, "..", "LAUNCH.md")];
4568
3922
  for (const file of candidates) {
4569
- if (!existsSync5(file)) continue;
4570
- return { file, roles: parseFrontmatter(await readFile5(file, "utf8")) };
3923
+ if (!existsSync4(file)) continue;
3924
+ return { file, roles: parseFrontmatter(await readFile4(file, "utf8")) };
4571
3925
  }
4572
3926
  return null;
4573
3927
  }
@@ -4592,7 +3946,7 @@ async function resolveLook(dir, opts) {
4592
3946
  roles
4593
3947
  };
4594
3948
  }
4595
- function lookOverrides(look, placement, size, video, opts) {
3949
+ function lookOverrides(look, placement, size, video, opts = {}) {
4596
3950
  const inset = cardInset(look, size, video, placement);
4597
3951
  const set = [
4598
3952
  "frame.fit=contain",
@@ -4609,70 +3963,8 @@ function lookOverrides(look, placement, size, video, opts) {
4609
3963
  set.push(`frame.borderColor=${look.borderColor ?? "#000000"}`);
4610
3964
  }
4611
3965
  if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
4612
- if (opts.still) {
4613
- set.push(
4614
- "zoom=[]",
4615
- "tilt=[]",
4616
- "cursor.visible=false",
4617
- "cursor.clickFx.style=none"
4618
- );
4619
- }
4620
3966
  return set;
4621
3967
  }
4622
- var PROBE_WIDTH = 640;
4623
- async function pickStillTimes(browser, dir, doc, duration, opts) {
4624
- const { candidates, dropped } = momentCandidates(doc, duration);
4625
- const notes = [];
4626
- if (dropped > 0)
4627
- notes.push(
4628
- `${dropped} of ${doc.zoom.length} zoom apex(es) fall outside the cut`
4629
- );
4630
- if (!candidates.length) return { times: [], moments: [], notes };
4631
- const meta = doc.source.meta;
4632
- const vw = meta.captureWidth ?? meta.width;
4633
- const vh = meta.captureHeight ?? meta.height;
4634
- const probeDir = await mkdtemp(join5(tmpdir(), "vos-moments-"));
4635
- try {
4636
- const probe3 = await framesTake(browser, dir, {
4637
- times: candidates.map((c) => c.time),
4638
- width: PROBE_WIDTH,
4639
- height: Math.max(2, Math.round(PROBE_WIDTH * vh / vw / 2) * 2),
4640
- outDir: probeDir,
4641
- overrides: {
4642
- ...opts.overrides,
4643
- set: [...SCREENSHOT_DEFAULTS, ...opts.overrides?.set ?? []]
4644
- }
4645
- });
4646
- const byTime = /* @__PURE__ */ new Map();
4647
- for (const frame of probe3.frames) {
4648
- const img = decodePng(new Uint8Array(await readFile5(frame.file)));
4649
- if (!img) continue;
4650
- const whole = { x: 0, y: 0, w: img.w, h: img.h };
4651
- byTime.set(+frame.time.toFixed(3), {
4652
- ink: inkCoverage(img, whole),
4653
- hash: differenceHash(img, whole)
4654
- });
4655
- }
4656
- const measured = [];
4657
- for (const c of candidates) {
4658
- const m = byTime.get(+c.time.toFixed(3));
4659
- if (m) measured.push({ time: c.time, ...m });
4660
- }
4661
- const pick = pickMoments(measured);
4662
- const kept = candidates.filter((c) => pick.times.includes(c.time));
4663
- const bySource = (s) => candidates.filter((c) => c.source === s).length;
4664
- notes.push(
4665
- `${candidates.length} candidate(s): ${bySource("step")} from steps, ${bySource("zoom")} from zoom apexes, ${bySource("spread")} from the spread; ${kept.length} kept`
4666
- );
4667
- return {
4668
- times: pick.times,
4669
- moments: kept,
4670
- notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
4671
- };
4672
- } finally {
4673
- await rm2(probeDir, { recursive: true, force: true });
4674
- }
4675
- }
4676
3968
  var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
4677
3969
  var specWords = (d) => {
4678
3970
  const parts = [`${d.px.w}x${d.px.h}`];
@@ -4682,62 +3974,17 @@ var specWords = (d) => {
4682
3974
  parts.push(`\u2264${(d.maxBytes / 1024 / 1024).toFixed(0)}MB`);
4683
3975
  return parts.join(" ");
4684
3976
  };
4685
- var FULL_BLEED = [
4686
- "frame.padding=0",
4687
- "frame.radius=0",
4688
- "frame.shadow=0",
4689
- "frame.border=0",
4690
- "frame.browserBar.kind=none",
4691
- "cursor.visible=false",
4692
- "cursor.clickFx.style=none"
4693
- ];
4694
- var SCREENSHOT_DEFAULTS = [...FULL_BLEED, "zoom=[]", "tilt=[]"];
4695
- function stillOverridesFor(d, opts, video) {
4696
- const set = [];
4697
- if (d.genre === "screenshot" && !opts.composed) {
4698
- if (d.fit === "cover") set.push("frame.fit=cover");
4699
- set.push(...SCREENSHOT_DEFAULTS);
4700
- } else if (d.genre === "card" && !opts.composed && opts.look && video && d.px) {
4701
- set.push(
4702
- ...lookOverrides(opts.look, "card", d.px, video, {
4703
- still: true,
4704
- keepMedia: opts.overrides?.background !== void 0
4705
- })
4706
- );
4707
- } else {
4708
- if (d.fit === "cover") set.push("frame.fit=cover");
4709
- if (d.genre === "card" && !opts.composed) set.push(...FULL_BLEED);
4710
- }
4711
- set.push(...opts.overrides?.set ?? []);
4712
- if (!set.length) return opts.overrides;
4713
- return { ...opts.overrides, set };
4714
- }
4715
3977
  async function deliverTake(browser, dir, opts) {
4716
3978
  const take = await loadTake(dir);
4717
3979
  if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
4718
3980
  const doc = take.doc;
4719
3981
  const duration = docOutputDuration2(doc);
4720
3982
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
4721
- const outDir = resolve4(opts.outDir ?? join5(dir, "kit"));
3983
+ const outDir = resolve3(opts.outDir ?? join4(dir, "kit"));
4722
3984
  await mkdir3(outDir, { recursive: true });
4723
3985
  const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
4724
3986
  const assets = [];
4725
3987
  const skipped = [];
4726
- let stillTimes;
4727
- let moments;
4728
- if (opts.times?.length) {
4729
- stillTimes = opts.times;
4730
- moments = opts.times.map((time) => ({ time, source: "times" }));
4731
- } else {
4732
- opts.onPhase?.("moments (the step timeline, the zoom apexes, the spread)");
4733
- const picked = await pickStillTimes(browser, dir, doc, duration, opts);
4734
- stillTimes = picked.times;
4735
- moments = picked.moments;
4736
- for (const n of picked.notes) {
4737
- if (n.startsWith("moment: ")) skipped.push(n);
4738
- else opts.onPhase?.(`note: ${n}`);
4739
- }
4740
- }
4741
3988
  const meta0 = doc.source.meta;
4742
3989
  const video = {
4743
3990
  w: meta0.captureWidth ?? meta0.width,
@@ -4748,7 +3995,6 @@ async function deliverTake(browser, dir, opts) {
4748
3995
  if (opts.look) {
4749
3996
  set.push(
4750
3997
  ...lookOverrides(opts.look, "hero", d.px, video, {
4751
- still: false,
4752
3998
  keepMedia: opts.overrides?.background !== void 0
4753
3999
  })
4754
4000
  );
@@ -4760,181 +4006,59 @@ async function deliverTake(browser, dir, opts) {
4760
4006
  if (!set.length && !mech.unset.length) return opts.overrides;
4761
4007
  return { ...opts.overrides, set, unset: mech.unset };
4762
4008
  };
4763
- const posters = await findPosterDocs(dir, opts.launchRoles);
4764
- const posterCardIds = /* @__PURE__ */ new Set();
4765
- const missing = /* @__PURE__ */ new Set();
4766
- for (const d of destinations) {
4767
- if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
4768
- continue;
4769
- const cls = posterClassFor(d.px);
4770
- const ref = posters[cls];
4771
- if (!ref) {
4772
- missing.add(cls);
4773
- continue;
4774
- }
4775
- posterCardIds.add(d.id);
4776
- const label = `${d.channel} ${d.asset}`;
4777
- const posterDuration = docOutputDuration2(ref.doc);
4778
- const time = posterStillTime(ref.doc, posterDuration);
4779
- opts.onPhase?.(
4780
- `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
4781
- );
4782
- const shotDir = await mkdtemp(join5(tmpdir(), "vos-poster-"));
4783
- try {
4784
- const captured = await framesTake(browser, ref.takeDir, {
4785
- times: [time],
4786
- width: d.px.w,
4787
- height: d.px.h,
4788
- outDir: shotDir,
4789
- doc: ref.doc,
4790
- overrides: opts.overrides
4791
- });
4792
- const to = join5(outDir, `${d.id}.png`);
4793
- await rename3(captured.frames[0].file, to);
4794
- const bytes = (await stat(to)).size;
4795
- if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4796
- skipped.push(
4797
- `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
4798
- );
4799
- continue;
4800
- }
4801
- const shot = posterShotRect(ref.doc, d.px);
4802
- assets.push({
4803
- channel: d.channel,
4804
- asset: d.asset,
4805
- destination: d.id,
4806
- path: relative(outDir, to),
4807
- w: d.px.w,
4808
- h: d.px.h,
4809
- bytes,
4810
- seconds: null,
4811
- frameTime: time,
4812
- source: "poster",
4813
- poster: {
4814
- class: cls,
4815
- file: relative(dir, ref.file),
4816
- ...ref.vosId ? { vosId: ref.vosId } : {}
4817
- },
4818
- text: posterTextBoxes(ref.doc, d.px, time),
4819
- shot,
4820
- ...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
4821
- });
4822
- } finally {
4823
- await rm2(shotDir, { recursive: true, force: true });
4824
- }
4825
- }
4826
- if (missing.size) {
4827
- const classes = [...missing].sort().join(", ");
4828
- skipped.push(
4829
- `note: no poster document for the ${classes} class${missing.size > 1 ? "es" : ""} beside the take (poster/<class>/doc.json, or LAUNCH.md poster:), so those cards are the take's own frame`
4830
- );
4831
- }
4832
4009
  for (const d of destinations) {
4833
4010
  const label = `${d.channel} ${d.asset}`;
4834
- const excuse = NOT_FROM_FOOTAGE[d.id];
4835
- if (excuse) {
4836
- skipped.push(`${label}: ${excuse}`);
4011
+ if (d.minSeconds !== void 0 && videoSeconds < d.minSeconds) {
4012
+ skipped.push(
4013
+ `${label}: spec wants ${d.minSeconds}-${d.maxSeconds ?? "\u221E"}s, the ${opts.range ? "range" : "take"} is ${videoSeconds.toFixed(0)}s`
4014
+ );
4837
4015
  continue;
4838
4016
  }
4839
- if (posterCardIds.has(d.id)) continue;
4840
- if (d.kind === "video") {
4841
- if (d.minSeconds !== void 0 && videoSeconds < d.minSeconds) {
4842
- skipped.push(
4843
- `${label}: spec wants ${d.minSeconds}-${d.maxSeconds ?? "\u221E"}s, the ${opts.range ? "range" : "take"} is ${videoSeconds.toFixed(0)}s`
4844
- );
4845
- continue;
4846
- }
4847
- let range = opts.range;
4848
- let seconds = videoSeconds;
4849
- if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
4850
- if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
4851
- range = [0, d.maxSeconds];
4852
- seconds = d.maxSeconds;
4853
- opts.onPhase?.(
4854
- `note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
4855
- );
4856
- } else {
4857
- skipped.push(
4858
- `${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
4859
- );
4860
- continue;
4861
- }
4862
- }
4863
- opts.onPhase?.(`${label} (${specWords(d)})`);
4864
- const outFile = join5(outDir, `${d.id}.${d.format}`);
4865
- const bitrate = d.maxBytes !== void 0 ? Math.min(
4866
- 1e7,
4867
- Math.floor(d.maxBytes * 8 / seconds * 0.85)
4868
- ) : void 0;
4869
- const result = await renderTake(browser, dir, outFile, {
4870
- width: d.px.w,
4871
- height: d.px.h,
4872
- format: "mp4",
4873
- parallel: opts.parallel,
4874
- range,
4875
- bitrate,
4876
- overrides: videoOverrides(d),
4877
- onProgress: opts.onProgress
4878
- });
4879
- if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
4017
+ let range = opts.range;
4018
+ let seconds = videoSeconds;
4019
+ if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
4020
+ if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
4021
+ range = [0, d.maxSeconds];
4022
+ seconds = d.maxSeconds;
4023
+ opts.onPhase?.(
4024
+ `note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
4025
+ );
4026
+ } else {
4880
4027
  skipped.push(
4881
- `${label}: ${overCeiling(result.bytes, d.maxBytes)} (kept at ${outFile})`
4028
+ `${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
4882
4029
  );
4883
4030
  continue;
4884
4031
  }
4885
- assets.push({
4886
- channel: d.channel,
4887
- asset: d.asset,
4888
- destination: d.id,
4889
- path: relative(outDir, outFile),
4890
- w: result.width,
4891
- h: result.height,
4892
- bytes: result.bytes,
4893
- seconds: result.duration,
4894
- frameTime: null
4895
- });
4896
- continue;
4897
4032
  }
4898
- const wanted = d.kind === "still-set" ? stillTimes.slice(0, d.count?.max ?? stillTimes.length) : stillTimes.slice(0, 1);
4899
- opts.onPhase?.(`${label} (${specWords(d)}, ${wanted.length} still(s))`);
4900
- const overrides = stillOverridesFor(d, opts, video);
4901
- const captured = await framesTake(browser, dir, {
4902
- times: wanted,
4033
+ opts.onPhase?.(`${label} (${specWords(d)})`);
4034
+ const outFile = join4(outDir, `${d.id}.${d.format}`);
4035
+ const bitrate = d.maxBytes !== void 0 ? Math.min(1e7, Math.floor(d.maxBytes * 8 / seconds * 0.85)) : void 0;
4036
+ const result = await renderTake(browser, dir, outFile, {
4903
4037
  width: d.px.w,
4904
4038
  height: d.px.h,
4905
- outDir,
4906
- overrides
4039
+ format: "mp4",
4040
+ parallel: opts.parallel,
4041
+ range,
4042
+ bitrate,
4043
+ overrides: videoOverrides(d),
4044
+ onProgress: opts.onProgress
4907
4045
  });
4908
- for (let i = 0; i < captured.frames.length; i++) {
4909
- const frame = captured.frames[i];
4910
- const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
4911
- const to = join5(outDir, name);
4912
- await rename3(frame.file, to);
4913
- const bytes = (await stat(to)).size;
4914
- if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4915
- skipped.push(
4916
- `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
4917
- );
4918
- continue;
4919
- }
4920
- assets.push({
4921
- channel: d.channel,
4922
- asset: d.asset,
4923
- destination: d.id,
4924
- path: relative(outDir, to),
4925
- w: captured.width,
4926
- h: captured.height,
4927
- bytes,
4928
- seconds: null,
4929
- frameTime: frame.time,
4930
- ...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
4931
- });
4932
- }
4933
- if (d.count && captured.frames.length < d.count.min) {
4046
+ if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
4934
4047
  skipped.push(
4935
- `${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
4048
+ `${label}: ${overCeiling(result.bytes, d.maxBytes)} (kept at ${outFile})`
4936
4049
  );
4050
+ continue;
4937
4051
  }
4052
+ assets.push({
4053
+ channel: d.channel,
4054
+ asset: d.asset,
4055
+ destination: d.id,
4056
+ path: relative(outDir, outFile),
4057
+ w: result.width,
4058
+ h: result.height,
4059
+ bytes: result.bytes,
4060
+ seconds: result.duration
4061
+ });
4938
4062
  }
4939
4063
  const kit = {
4940
4064
  release: opts.release ?? null,
@@ -4942,19 +4066,18 @@ async function deliverTake(browser, dir, opts) {
4942
4066
  take: dir,
4943
4067
  produced: (/* @__PURE__ */ new Date()).toISOString(),
4944
4068
  specsVerified: CHANNEL_SPECS_VERIFIED,
4945
- moments,
4946
4069
  skipped,
4947
4070
  assets
4948
4071
  };
4949
- const kitFile = join5(outDir, "kit.json");
4072
+ const kitFile = join4(outDir, "kit.json");
4950
4073
  await writeFile5(kitFile, JSON.stringify(kit, null, 2));
4951
4074
  return { kit, kitFile, outDir };
4952
4075
  }
4953
4076
 
4954
4077
  // src/plugin/markAsset.ts
4955
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
4956
- import { existsSync as existsSync6 } from "fs";
4957
- import { isAbsolute as isAbsolute2, join as join6, resolve as resolve5 } from "path";
4078
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
4079
+ import { existsSync as existsSync5 } from "fs";
4080
+ import { isAbsolute, join as join5, resolve as resolve4 } from "path";
4958
4081
  function markExtension(url) {
4959
4082
  const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4960
4083
  if (!m) return null;
@@ -5006,418 +4129,493 @@ function imageAspect(bytes, ext) {
5006
4129
  if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
5007
4130
  return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
5008
4131
  }
5009
- i += 2 + len;
4132
+ i += 2 + len;
4133
+ }
4134
+ return null;
4135
+ }
4136
+ return null;
4137
+ }
4138
+ function ratio(w, h) {
4139
+ return w > 0 && h > 0 ? w / h : null;
4140
+ }
4141
+ async function fetchBrandMarks(takeDir, roles) {
4142
+ const out = { light: null, dark: null, notes: [] };
4143
+ if (!roles) return out;
4144
+ const one = async (role, name) => {
4145
+ const src = (roles[role] ?? "").trim();
4146
+ if (!src) return null;
4147
+ const ext = markExtension(src);
4148
+ if (!ext) {
4149
+ out.notes.push(
4150
+ `${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
4151
+ );
4152
+ return null;
4153
+ }
4154
+ let bytes;
4155
+ try {
4156
+ if (/^https?:/i.test(src)) {
4157
+ const res = await fetch(src);
4158
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
4159
+ bytes = new Uint8Array(await res.arrayBuffer());
4160
+ } else {
4161
+ const file = isAbsolute(src) ? src : resolve4(takeDir, src);
4162
+ if (!existsSync5(file)) throw new Error("no such file");
4163
+ bytes = new Uint8Array(await readFile5(file));
4164
+ }
4165
+ } catch (e) {
4166
+ out.notes.push(
4167
+ `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
4168
+ );
4169
+ return null;
4170
+ }
4171
+ const aspect = imageAspect(bytes, ext);
4172
+ if (!aspect) {
4173
+ out.notes.push(
4174
+ `${role}: ${src} has no readable size; the wordmark stands in`
4175
+ );
4176
+ return null;
4177
+ }
4178
+ await mkdir4(join5(takeDir, "brand"), { recursive: true });
4179
+ await writeFile6(join5(takeDir, "brand", `${name}.${ext}`), bytes);
4180
+ return { key: `/brand/${name}.${ext}`, aspect };
4181
+ };
4182
+ out.light = await one("logoUrl", "mark");
4183
+ out.dark = await one("logoOnDarkUrl", "mark-on-dark");
4184
+ return out;
4185
+ }
4186
+
4187
+ // src/plugin/officialTemplates.ts
4188
+ var OFFICIAL_END_CARD_TITLE = "End card";
4189
+ function templateKey(s) {
4190
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "");
4191
+ }
4192
+ function findOfficialByRef(rows, ref) {
4193
+ const key = templateKey(ref);
4194
+ if (!key) return null;
4195
+ for (const row of rows) {
4196
+ if (templateKey(row.title) === key) return row;
4197
+ if (row.slug && templateKey(row.slug) === key) return row;
4198
+ }
4199
+ return null;
4200
+ }
4201
+
4202
+ // src/plugin/music.ts
4203
+ async function fetchMusicCatalog(origin) {
4204
+ const base = origin.replace(/\/+$/, "");
4205
+ const res = await fetch(`${base}/api/music`);
4206
+ if (!res.ok) throw new Error(`GET ${base}/api/music answered ${res.status}`);
4207
+ const body = await res.json();
4208
+ return {
4209
+ tracks: Array.isArray(body.tracks) ? body.tracks : [],
4210
+ sfx: Array.isArray(body.sfx) ? body.sfx : []
4211
+ };
4212
+ }
4213
+
4214
+ // src/plugin/fontName.ts
4215
+ import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
4216
+ function catalogFamily(family) {
4217
+ const name = family.trim();
4218
+ if (findFontFamily2(name)) return name;
4219
+ const base = name.replace(/\s+Variable$/i, "");
4220
+ if (base !== name) {
4221
+ const hit = findFontFamily2(base);
4222
+ if (hit) return hit.family;
4223
+ }
4224
+ return family;
4225
+ }
4226
+
4227
+ // src/plugin/callout.ts
4228
+ import { existsSync as existsSync6 } from "fs";
4229
+ import { readFile as readFile6 } from "fs/promises";
4230
+ import { join as join6 } from "path";
4231
+ import { lerpArray, sample } from "@vosjs/timeline";
4232
+ import {
4233
+ CALLOUT_DEFAULT_BODY_PX,
4234
+ CALLOUT_SHAPES,
4235
+ calloutClip,
4236
+ calloutLook,
4237
+ calloutGroundOf,
4238
+ cameraModel as cameraModel2,
4239
+ deltaE,
4240
+ docCardLayout as docCardLayout2,
4241
+ docZoomTrack as docZoomTrack2,
4242
+ hexToRgb,
4243
+ parseCssColour,
4244
+ pinBox as pinBox2,
4245
+ ratedSegments as ratedSegments5,
4246
+ resolvePins as resolvePins2,
4247
+ spanOutputExtent as spanOutputExtent5
4248
+ } from "@vosjs/studio-core";
4249
+
4250
+ // src/plugin/picture.ts
4251
+ import { inflateSync } from "zlib";
4252
+ var PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
4253
+ function decodePng(bytes) {
4254
+ if (bytes.length < 33) return null;
4255
+ for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIG[i]) return null;
4256
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4257
+ let pos = 8;
4258
+ let w = 0;
4259
+ let h = 0;
4260
+ let depth = 0;
4261
+ let colorType = 0;
4262
+ let interlace = 0;
4263
+ const idat = [];
4264
+ while (pos + 8 <= bytes.length) {
4265
+ const len = view.getUint32(pos);
4266
+ const type = String.fromCharCode(
4267
+ bytes[pos + 4],
4268
+ bytes[pos + 5],
4269
+ bytes[pos + 6],
4270
+ bytes[pos + 7]
4271
+ );
4272
+ const start = pos + 8;
4273
+ if (type === "IHDR") {
4274
+ w = view.getUint32(start);
4275
+ h = view.getUint32(start + 4);
4276
+ depth = bytes[start + 8];
4277
+ colorType = bytes[start + 9];
4278
+ interlace = bytes[start + 12];
4279
+ } else if (type === "IDAT") {
4280
+ idat.push(bytes.subarray(start, start + len));
4281
+ } else if (type === "IEND") break;
4282
+ pos = start + len + 4;
4283
+ }
4284
+ if (!w || !h || depth !== 8 || interlace !== 0) return null;
4285
+ const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
4286
+ if (!channels) return null;
4287
+ const total = idat.reduce((n, c) => n + c.length, 0);
4288
+ const joined = new Uint8Array(total);
4289
+ let off2 = 0;
4290
+ for (const c of idat) {
4291
+ joined.set(c, off2);
4292
+ off2 += c.length;
4293
+ }
4294
+ let raw;
4295
+ try {
4296
+ raw = new Uint8Array(inflateSync(joined));
4297
+ } catch {
4298
+ return null;
4299
+ }
4300
+ const stride = w * channels;
4301
+ if (raw.length < (stride + 1) * h) return null;
4302
+ const out = new Uint8Array(w * h * 4);
4303
+ const prev = new Uint8Array(stride);
4304
+ const cur = new Uint8Array(stride);
4305
+ for (let y = 0; y < h; y++) {
4306
+ const filter = raw[y * (stride + 1)];
4307
+ const rowStart = y * (stride + 1) + 1;
4308
+ for (let i = 0; i < stride; i++) {
4309
+ const x = raw[rowStart + i];
4310
+ const a = i >= channels ? cur[i - channels] : 0;
4311
+ const b = prev[i];
4312
+ const c = i >= channels ? prev[i - channels] : 0;
4313
+ let v;
4314
+ switch (filter) {
4315
+ case 0:
4316
+ v = x;
4317
+ break;
4318
+ case 1:
4319
+ v = x + a;
4320
+ break;
4321
+ case 2:
4322
+ v = x + b;
4323
+ break;
4324
+ case 3:
4325
+ v = x + (a + b >> 1);
4326
+ break;
4327
+ case 4: {
4328
+ const p = a + b - c;
4329
+ const pa = Math.abs(p - a);
4330
+ const pb = Math.abs(p - b);
4331
+ const pc = Math.abs(p - c);
4332
+ v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
4333
+ break;
4334
+ }
4335
+ default:
4336
+ return null;
4337
+ }
4338
+ cur[i] = v & 255;
4339
+ }
4340
+ for (let px = 0; px < w; px++) {
4341
+ const o = (y * w + px) * 4;
4342
+ const s = px * channels;
4343
+ if (channels >= 3) {
4344
+ out[o] = cur[s];
4345
+ out[o + 1] = cur[s + 1];
4346
+ out[o + 2] = cur[s + 2];
4347
+ out[o + 3] = channels === 4 ? cur[s + 3] : 255;
4348
+ } else {
4349
+ out[o] = out[o + 1] = out[o + 2] = cur[s];
4350
+ out[o + 3] = channels === 2 ? cur[s + 1] : 255;
4351
+ }
4352
+ }
4353
+ prev.set(cur);
4354
+ }
4355
+ return { w, h, data: out };
4356
+ }
4357
+ var at = (img, x, y) => {
4358
+ const o = (y * img.w + x) * 4;
4359
+ return [img.data[o], img.data[o + 1], img.data[o + 2]];
4360
+ };
4361
+ var median = (a) => {
4362
+ const s = [...a].sort((x, y) => x - y);
4363
+ return s[s.length >> 1] ?? 0;
4364
+ };
4365
+ function groundColour(img) {
4366
+ const ring = Math.max(1, Math.round(Math.min(img.w, img.h) * 0.01));
4367
+ const r = [];
4368
+ const g = [];
4369
+ const b = [];
4370
+ const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 400));
4371
+ for (let y = 0; y < img.h; y += step) {
4372
+ for (let x = 0; x < img.w; x += step) {
4373
+ if (x < ring || y < ring || x >= img.w - ring || y >= img.h - ring) {
4374
+ const p = at(img, x, y);
4375
+ r.push(p[0]);
4376
+ g.push(p[1]);
4377
+ b.push(p[2]);
4378
+ }
5010
4379
  }
5011
- return null;
5012
4380
  }
5013
- return null;
5014
- }
5015
- function ratio(w, h) {
5016
- return w > 0 && h > 0 ? w / h : null;
4381
+ return [median(r), median(g), median(b)];
5017
4382
  }
5018
- async function fetchBrandMarks(takeDir, roles) {
5019
- const out = { light: null, dark: null, notes: [] };
5020
- if (!roles) return out;
5021
- const one = async (role, name) => {
5022
- const src = (roles[role] ?? "").trim();
5023
- if (!src) return null;
5024
- const ext = markExtension(src);
5025
- if (!ext) {
5026
- out.notes.push(
5027
- `${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
5028
- );
5029
- return null;
5030
- }
5031
- let bytes;
5032
- try {
5033
- if (/^https?:/i.test(src)) {
5034
- const res = await fetch(src);
5035
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
5036
- bytes = new Uint8Array(await res.arrayBuffer());
5037
- } else {
5038
- const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
5039
- if (!existsSync6(file)) throw new Error("no such file");
5040
- bytes = new Uint8Array(await readFile6(file));
4383
+ var hex = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
4384
+ var delta = (p, q) => Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
4385
+ function cardBounds(img, ground, threshold = 60) {
4386
+ let x0 = img.w;
4387
+ let y0 = img.h;
4388
+ let x1 = -1;
4389
+ let y1 = -1;
4390
+ const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 700));
4391
+ for (let y = 0; y < img.h; y += step) {
4392
+ for (let x = 0; x < img.w; x += step) {
4393
+ if (delta(at(img, x, y), ground) > threshold) {
4394
+ if (x < x0) x0 = x;
4395
+ if (y < y0) y0 = y;
4396
+ if (x > x1) x1 = x;
4397
+ if (y > y1) y1 = y;
5041
4398
  }
5042
- } catch (e) {
5043
- out.notes.push(
5044
- `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
5045
- );
5046
- return null;
5047
4399
  }
5048
- const aspect = imageAspect(bytes, ext);
5049
- if (!aspect) {
5050
- out.notes.push(
5051
- `${role}: ${src} has no readable size; the wordmark stands in`
5052
- );
5053
- return null;
4400
+ }
4401
+ if (x1 < 0) return null;
4402
+ return { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 };
4403
+ }
4404
+ function haloReading(img, ground, card) {
4405
+ const band = Math.max(2, Math.round(Math.min(img.w, img.h) * 0.012));
4406
+ const meanRect = (x0, y0, x1, y1) => {
4407
+ let sum = 0;
4408
+ let n = 0;
4409
+ const sx = Math.max(1, Math.round((x1 - x0) / 60));
4410
+ const sy = Math.max(1, Math.round((y1 - y0) / 60));
4411
+ for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
4412
+ for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
4413
+ sum += delta(at(img, x, y), ground);
4414
+ n++;
4415
+ }
5054
4416
  }
5055
- await mkdir4(join6(takeDir, "brand"), { recursive: true });
5056
- await writeFile6(join6(takeDir, "brand", `${name}.${ext}`), bytes);
5057
- return { key: `/brand/${name}.${ext}`, aspect };
4417
+ return n ? sum / n : 0;
5058
4418
  };
5059
- out.light = await one("logoUrl", "mark");
5060
- out.dark = await one("logoOnDarkUrl", "mark-on-dark");
5061
- return out;
5062
- }
5063
-
5064
- // src/plugin/officialTemplates.ts
5065
- var OFFICIAL_END_CARD_TITLE = "End card";
5066
- function templateKey(s) {
5067
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "");
5068
- }
5069
- function findOfficialByRef(rows, ref) {
5070
- const key = templateKey(ref);
5071
- if (!key) return null;
5072
- for (const row of rows) {
5073
- if (templateKey(row.title) === key) return row;
5074
- if (row.slug && templateKey(row.slug) === key) return row;
4419
+ const rise = (bands) => {
4420
+ const [outer, mid] = bands;
4421
+ return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
4422
+ };
4423
+ const readings = [];
4424
+ const yA = card.y + card.h * 0.2;
4425
+ const yB = card.y + card.h * 0.8;
4426
+ if (card.x >= 1 && card.w > band * 4) {
4427
+ const x0 = card.x;
4428
+ readings.push(
4429
+ rise([
4430
+ meanRect(x0, yA, x0 + band, yB),
4431
+ meanRect(x0 + band, yA, x0 + band * 2, yB),
4432
+ meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
4433
+ ])
4434
+ );
5075
4435
  }
5076
- return null;
4436
+ if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
4437
+ const x1 = card.x + card.w;
4438
+ readings.push(
4439
+ rise([
4440
+ meanRect(x1 - band, yA, x1, yB),
4441
+ meanRect(x1 - band * 2, yA, x1 - band, yB),
4442
+ meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
4443
+ ])
4444
+ );
4445
+ }
4446
+ if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
4447
+ const y1 = card.y + card.h;
4448
+ const xA = card.x + card.w * 0.2;
4449
+ const xB = card.x + card.w * 0.8;
4450
+ readings.push(
4451
+ rise([
4452
+ meanRect(xA, y1 - band, xB, y1),
4453
+ meanRect(xA, y1 - band * 2, xB, y1 - band),
4454
+ meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
4455
+ ])
4456
+ );
4457
+ }
4458
+ return readings.length ? Math.max(...readings) : null;
5077
4459
  }
5078
-
5079
- // src/plugin/music.ts
5080
- async function fetchMusicCatalog(origin) {
5081
- const base = origin.replace(/\/+$/, "");
5082
- const res = await fetch(`${base}/api/music`);
5083
- if (!res.ok) throw new Error(`GET ${base}/api/music answered ${res.status}`);
5084
- const body = await res.json();
5085
- return {
5086
- tracks: Array.isArray(body.tracks) ? body.tracks : [],
5087
- sfx: Array.isArray(body.sfx) ? body.sfx : []
4460
+ function edgeContrast(img, ground, card) {
4461
+ const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
4462
+ const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
4463
+ const col = (x) => {
4464
+ if (x < 0 || x >= img.w) return 0;
4465
+ const ds = [];
4466
+ const step = Math.max(1, Math.round((yB - yA) / 80));
4467
+ for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
4468
+ return median(ds);
5088
4469
  };
4470
+ const depth = Math.min(24, Math.floor(card.w / 8));
4471
+ let best = 0;
4472
+ for (let d = 0; d < depth; d++) {
4473
+ best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
4474
+ }
4475
+ return best;
5089
4476
  }
5090
-
5091
- // src/plugin/judge.ts
5092
- import { existsSync as existsSync7 } from "fs";
5093
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
5094
- import { dirname, isAbsolute as isAbsolute3, join as join7, resolve as resolve6 } from "path";
5095
- import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
5096
-
5097
- // src/plugin/shotBake.ts
5098
- import { deflateSync } from "zlib";
5099
- var crcTable = (() => {
5100
- const t = new Uint32Array(256);
5101
- for (let n = 0; n < 256; n++) {
5102
- let c = n;
5103
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
5104
- t[n] = c >>> 0;
5105
- }
5106
- return t;
5107
- })();
5108
- function crc32(buf) {
5109
- let c = 4294967295;
5110
- for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
5111
- return (c ^ 4294967295) >>> 0;
5112
- }
5113
- function chunk(type, body) {
5114
- const out = new Uint8Array(12 + body.length);
5115
- const dv = new DataView(out.buffer);
5116
- dv.setUint32(0, body.length);
5117
- for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
5118
- out.set(body, 8);
5119
- dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
5120
- return out;
5121
- }
5122
- function encodePng(img) {
5123
- const stride = img.w * 4;
5124
- const raw = new Uint8Array((stride + 1) * img.h);
5125
- for (let y = 0; y < img.h; y++) {
5126
- raw[y * (stride + 1)] = 2;
5127
- for (let i = 0; i < stride; i++) {
5128
- const cur = img.data[y * stride + i];
5129
- const up = y ? img.data[(y - 1) * stride + i] : 0;
5130
- raw[y * (stride + 1) + 1 + i] = cur - up & 255;
5131
- }
5132
- }
5133
- const ihdr = new Uint8Array(13);
5134
- const dv = new DataView(ihdr.buffer);
5135
- dv.setUint32(0, img.w);
5136
- dv.setUint32(4, img.h);
5137
- ihdr[8] = 8;
5138
- ihdr[9] = 6;
5139
- const parts = [
5140
- new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
5141
- chunk("IHDR", ihdr),
5142
- chunk("IDAT", new Uint8Array(deflateSync(raw))),
5143
- chunk("IEND", new Uint8Array(0))
5144
- ];
5145
- const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
5146
- let o = 0;
5147
- for (const p of parts) {
5148
- out.set(p, o);
5149
- o += p.length;
4477
+ function inkCoverage(img, rect, threshold = 48) {
4478
+ const x0 = Math.max(0, Math.floor(rect.x));
4479
+ const y0 = Math.max(0, Math.floor(rect.y));
4480
+ const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
4481
+ const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
4482
+ if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
4483
+ const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
4484
+ const r = [];
4485
+ const g = [];
4486
+ const b = [];
4487
+ const pts = [];
4488
+ for (let y = y0; y < y1; y += step) {
4489
+ for (let x = x0; x < x1; x += step) {
4490
+ const p = at(img, x, y);
4491
+ pts.push(p);
4492
+ r.push(p[0]);
4493
+ g.push(p[1]);
4494
+ b.push(p[2]);
4495
+ }
5150
4496
  }
5151
- return out;
4497
+ const base = [median(r), median(g), median(b)];
4498
+ let ink = 0;
4499
+ for (const p of pts) if (delta(p, base) > threshold) ink++;
4500
+ return pts.length ? ink / pts.length : 0;
5152
4501
  }
5153
-
5154
- // src/plugin/judge.ts
5155
- var RUBRIC = [
5156
- "A plate, and room on it: the subject sits at 74 to 89% of the width with headroom; nobody bleeds a window on all four sides.",
5157
- "The window is an object: rounded corners and a shadow with weight (a soft wide one on a light plate, a tight glow on a dark one).",
5158
- "It enters: the first frame is not a static, centred, flat window (a tilt-in, a pull-out, a rise).",
5159
- "The camera zooms to the affordance, holds through the response, and pulls out to show the consequence.",
5160
- "Beats are cut at stillness; no wipes.",
5161
- "Content is staged: real data, a populated state, never an empty canvas or a wallpaper.",
5162
- "The cursor is a character: visible, smoothed, a press on click, never drifting through dead space.",
5163
- "The last frame is a poster: a resolved state you could ship as the still.",
5164
- "The message has its own column or its own frame; the headline is two to six words over up to three lines at about 40% of the height; the wordmark is anchored.",
5165
- "The poster test: would you post this frame as a still, alone?",
5166
- "The thumbnail test: does it read at half size?",
5167
- "Name three ways this asset acknowledges THIS product (its data, its state, its brand), not a generic window."
5168
- ];
5169
- function rolesFor(asset) {
5170
- if ((asset.source === "poster" || asset.source === "stage") && asset.template)
5171
- return [asset.template.replace(/-stage$/, "")];
5172
- const spec = DESTINATIONS2.find((d) => d.id === asset.destination);
5173
- if (spec?.kind === "video") {
5174
- const portrait = spec.px.w < spec.px.h;
5175
- return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
5176
- }
5177
- if (spec?.genre === "card")
5178
- return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
5179
- if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
5180
- return [];
5181
- }
5182
- function resample(img, w, h) {
5183
- const out = new Uint8Array(w * h * 4);
5184
- const sx = img.w / w;
5185
- const sy = img.h / h;
5186
- for (let y = 0; y < h; y++) {
5187
- const y0 = Math.floor(y * sy);
5188
- const y1 = Math.max(y0 + 1, Math.floor((y + 1) * sy));
5189
- for (let x = 0; x < w; x++) {
5190
- const x0 = Math.floor(x * sx);
5191
- const x1 = Math.max(x0 + 1, Math.floor((x + 1) * sx));
5192
- let r = 0;
5193
- let g = 0;
5194
- let b = 0;
5195
- let a = 0;
4502
+ function differenceHash(img, rect) {
4503
+ const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
4504
+ const cols = 9;
4505
+ const rows = 8;
4506
+ const cell = [];
4507
+ for (let cy = 0; cy < rows; cy++) {
4508
+ for (let cx = 0; cx < cols; cx++) {
4509
+ const x0 = Math.floor(r.x + cx * r.w / cols);
4510
+ const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
4511
+ const y0 = Math.floor(r.y + cy * r.h / rows);
4512
+ const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
4513
+ let sum = 0;
5196
4514
  let n = 0;
5197
- for (let yy = y0; yy < y1 && yy < img.h; yy++) {
5198
- for (let xx = x0; xx < x1 && xx < img.w; xx++) {
5199
- const o2 = (yy * img.w + xx) * 4;
5200
- r += img.data[o2];
5201
- g += img.data[o2 + 1];
5202
- b += img.data[o2 + 2];
5203
- a += img.data[o2 + 3];
4515
+ const sx = Math.max(1, Math.floor((x1 - x0) / 8));
4516
+ const sy = Math.max(1, Math.floor((y1 - y0) / 8));
4517
+ for (let y = y0; y < y1 && y < img.h; y += sy) {
4518
+ for (let x = x0; x < x1 && x < img.w; x += sx) {
4519
+ const p = at(img, Math.max(0, x), Math.max(0, y));
4520
+ sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
5204
4521
  n++;
5205
4522
  }
5206
4523
  }
5207
- const o = (y * w + x) * 4;
5208
- out[o] = n ? r / n : 0;
5209
- out[o + 1] = n ? g / n : 0;
5210
- out[o + 2] = n ? b / n : 0;
5211
- out[o + 3] = n ? a / n : 0;
4524
+ cell.push(n ? sum / n : 0);
5212
4525
  }
5213
4526
  }
5214
- return { w, h, data: out };
5215
- }
5216
- function composeSheet(left, right, height = 540, gutter = 32) {
5217
- const lw = Math.max(1, Math.round(left.w * height / left.h));
5218
- const rw = Math.max(1, Math.round(right.w * height / right.h));
5219
- const L = resample(left, lw, height);
5220
- const R = resample(right, rw, height);
5221
- const margin = 24;
5222
- const band = 8;
5223
- const w = margin * 2 + lw + gutter + rw;
5224
- const h = margin * 2 + height + band + 6;
5225
- const out = new Uint8Array(w * h * 4);
5226
- for (let i = 0; i < w * h; i++) {
5227
- out[i * 4] = 240;
5228
- out[i * 4 + 1] = 242;
5229
- out[i * 4 + 2] = 244;
5230
- out[i * 4 + 3] = 255;
5231
- }
5232
- const blit = (src, x0, y0) => {
5233
- for (let y = 0; y < src.h; y++) {
5234
- for (let x = 0; x < src.w; x++) {
5235
- const si = (y * src.w + x) * 4;
5236
- const a = src.data[si + 3] / 255;
5237
- const o = ((y0 + y) * w + x0 + x) * 4;
5238
- for (let c = 0; c < 3; c++)
5239
- out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
5240
- }
4527
+ let bits = "";
4528
+ for (let cy = 0; cy < rows; cy++) {
4529
+ let byte = 0;
4530
+ for (let cx = 0; cx < cols - 1; cx++) {
4531
+ const l = cell[cy * cols + cx];
4532
+ const rr = cell[cy * cols + cx + 1];
4533
+ byte = byte << 1 | (l > rr ? 1 : 0);
5241
4534
  }
5242
- };
5243
- blit(L, margin, margin);
5244
- blit(R, margin + lw + gutter, margin);
5245
- const fill = (x0, x1, y0, y1, v) => {
5246
- for (let y = y0; y < y1; y++)
5247
- for (let x = x0; x < x1; x++) {
5248
- const o = (y * w + x) * 4;
5249
- out[o] = v;
5250
- out[o + 1] = v;
5251
- out[o + 2] = v;
5252
- }
5253
- };
5254
- fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
5255
- fill(
5256
- margin + lw + gutter,
5257
- margin + lw + gutter + rw,
5258
- margin + height + 6,
5259
- margin + height + 6 + band,
5260
- 200
5261
- );
5262
- return { w, h, data: out };
4535
+ bits += byte.toString(16).padStart(2, "0");
4536
+ }
4537
+ return bits;
5263
4538
  }
5264
- async function judgeKit(kitPath, manifestPath, outDir) {
5265
- const kit = JSON.parse(await readFile7(kitPath, "utf8"));
5266
- const manifest = JSON.parse(
5267
- await readFile7(manifestPath, "utf8")
5268
- );
5269
- const manifestDir = dirname(resolve6(manifestPath));
5270
- const kitDir = dirname(resolve6(kitPath));
5271
- const out = resolve6(outDir ?? join7(kitDir, "judge"));
5272
- await mkdir5(out, { recursive: true });
5273
- const sheets = [];
5274
- const skipped = [];
5275
- const refCache = /* @__PURE__ */ new Map();
5276
- const loadRef = async (file) => {
5277
- if (refCache.has(file)) return refCache.get(file) ?? null;
5278
- const p = join7(manifestDir, file);
5279
- const img = existsSync7(p) ? decodePng(new Uint8Array(await readFile7(p))) : null;
5280
- refCache.set(file, img);
5281
- return img;
4539
+ function luminance(c) {
4540
+ const lin = (v) => {
4541
+ const s = v / 255;
4542
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
5282
4543
  };
5283
- for (const a of kit.assets) {
5284
- if (!/\.png$/i.test(a.path)) continue;
5285
- const file = isAbsolute3(a.path) && existsSync7(a.path) ? a.path : join7(kitDir, a.path.split("/").pop() ?? a.path);
5286
- const img = existsSync7(file) ? decodePng(new Uint8Array(await readFile7(file))) : null;
5287
- if (!img) {
5288
- skipped.push(`${a.destination ?? a.path}: unreadable`);
5289
- continue;
5290
- }
5291
- const roles = rolesFor({
5292
- destination: a.destination,
5293
- source: a.source,
5294
- template: a.template,
5295
- path: a.path
5296
- });
5297
- const ref = manifest.assets.find(
5298
- (r) => roles.includes(r.role) && r.genre !== "context"
5299
- );
5300
- if (!ref) {
5301
- skipped.push(
5302
- `${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`
5303
- );
5304
- continue;
5305
- }
5306
- const refImg = await loadRef(ref.file);
5307
- if (!refImg) {
5308
- skipped.push(
5309
- `${a.destination ?? a.path}: reference ${ref.file} unreadable`
5310
- );
5311
- continue;
4544
+ return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
4545
+ }
4546
+ function lightness(c) {
4547
+ const y = luminance(c);
4548
+ return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
4549
+ }
4550
+ function medianColour(img, rect) {
4551
+ const r = [];
4552
+ const g = [];
4553
+ const b = [];
4554
+ const x0 = Math.max(0, Math.floor(rect.x));
4555
+ const y0 = Math.max(0, Math.floor(rect.y));
4556
+ const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
4557
+ const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
4558
+ const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
4559
+ for (let y = y0; y < y1; y += step) {
4560
+ for (let x = x0; x < x1; x += step) {
4561
+ const p = at(img, x, y);
4562
+ r.push(p[0]);
4563
+ g.push(p[1]);
4564
+ b.push(p[2]);
5312
4565
  }
5313
- const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
5314
- const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
5315
- const nameA = `${id}--A.png`;
5316
- const nameB = `${id}--B.png`;
5317
- await writeFile7(join7(out, nameA), encodePng(composeSheet(img, refImg)));
5318
- await writeFile7(join7(out, nameB), encodePng(composeSheet(refImg, img)));
5319
- const rubric = [
5320
- `# ${id} against ${ref.id} (${ref.file})`,
5321
- "",
5322
- `Sheet A: the kit asset LEFT (dark band), the reference RIGHT (light band). Sheet B: the reverse.`,
5323
- `Judge each sheet on its own, both orders, and write the verdict where the two agree; a disagreement is a tie.`,
5324
- "",
5325
- `## The reference`,
5326
- ref.layout,
5327
- ...ref.facts ? ["", "```json", JSON.stringify(ref.facts, null, 2), "```"] : [],
5328
- "",
5329
- `Rule: ${ref.rule}`,
5330
- "",
5331
- "## The rubric",
5332
- ...RUBRIC.map((r, i) => `${i + 1}. ${r}`),
5333
- "",
5334
- "## Verdict",
5335
- "In judge.json: `win` true when the kit asset is at least as good as the reference on the rules that apply to its genre, false when it is worse, null for a tie; `reasons` names the rules by number.",
5336
- ""
5337
- ].join("\n");
5338
- await writeFile7(join7(out, `${id}.md`), rubric);
5339
- sheets.push({
5340
- asset: id,
5341
- reference: ref.id,
5342
- sheetA: nameA,
5343
- sheetB: nameB,
5344
- rubric: `${id}.md`
5345
- });
5346
- }
5347
- const verdictFile = join7(out, "judge.json");
5348
- if (!existsSync7(verdictFile)) {
5349
- await writeFile7(
5350
- verdictFile,
5351
- JSON.stringify(
5352
- {
5353
- kit: kitPath,
5354
- manifest: manifestPath,
5355
- verdicts: sheets.map((s) => ({
5356
- asset: s.asset,
5357
- reference: s.reference,
5358
- A: null,
5359
- B: null,
5360
- win: null,
5361
- reasons: []
5362
- }))
5363
- },
5364
- null,
5365
- 2
5366
- )
5367
- );
5368
4566
  }
5369
- return { sheets, skipped, outDir: out, verdictFile };
4567
+ return [median(r), median(g), median(b)];
5370
4568
  }
5371
- function winRate(verdicts) {
5372
- const judgedList = verdicts.filter(
5373
- (v) => v.win !== null || (v.reasons?.length ?? 0) > 0
5374
- );
5375
- const wins = judgedList.filter((v) => v.win === true).length;
5376
- const ties = judgedList.filter((v) => v.win === null).length;
5377
- const judged = judgedList.length;
4569
+ function measureStill(img) {
4570
+ const g = groundColour(img);
4571
+ const strong = cardBounds(img, g, 60);
4572
+ const faint = cardBounds(img, g, 12) ?? strong;
4573
+ const card = faint;
4574
+ const bleed = [];
4575
+ let pad = null;
4576
+ let widthPct = null;
4577
+ let shadow = null;
4578
+ let edge = 0;
4579
+ let separation = null;
4580
+ if (card) {
4581
+ pad = {
4582
+ left: card.x / img.w,
4583
+ right: (img.w - card.x - card.w) / img.w,
4584
+ top: card.y / img.h,
4585
+ bottom: (img.h - card.y - card.h) / img.h
4586
+ };
4587
+ for (const side of ["left", "right", "top", "bottom"])
4588
+ if (pad[side] < 5e-3) bleed.push(side);
4589
+ widthPct = card.w / img.w;
4590
+ shadow = haloReading(img, g, card);
4591
+ edge = edgeContrast(img, g, card);
4592
+ const body = strong ?? card;
4593
+ const inner = {
4594
+ x: body.x + body.w * 0.1,
4595
+ y: body.y + body.h * 0.1,
4596
+ w: body.w * 0.8,
4597
+ h: body.h * 0.8
4598
+ };
4599
+ separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
4600
+ }
4601
+ const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
5378
4602
  return {
5379
- wins,
5380
- ties,
5381
- judged,
5382
- rate: judged ? (wins + ties / 2) / judged : null
4603
+ w: img.w,
4604
+ h: img.h,
4605
+ ground: hex(g),
4606
+ card,
4607
+ widthPct,
4608
+ pad,
4609
+ bleed,
4610
+ shadow,
4611
+ edge,
4612
+ ink: inkCoverage(img, inkRect),
4613
+ separation,
4614
+ hash: differenceHash(img, inkRect)
5383
4615
  };
5384
4616
  }
5385
4617
 
5386
- // src/plugin/fontName.ts
5387
- import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
5388
- function catalogFamily(family) {
5389
- const name = family.trim();
5390
- if (findFontFamily2(name)) return name;
5391
- const base = name.replace(/\s+Variable$/i, "");
5392
- if (base !== name) {
5393
- const hit = findFontFamily2(base);
5394
- if (hit) return hit.family;
5395
- }
5396
- return family;
5397
- }
5398
-
5399
4618
  // src/plugin/callout.ts
5400
- import { existsSync as existsSync8 } from "fs";
5401
- import { readFile as readFile8 } from "fs/promises";
5402
- import { join as join8 } from "path";
5403
- import { lerpArray, sample } from "@vosjs/timeline";
5404
- import {
5405
- CALLOUT_DEFAULT_BODY_PX,
5406
- CALLOUT_SHAPES,
5407
- calloutClip,
5408
- calloutLook,
5409
- calloutGroundOf,
5410
- cameraModel as cameraModel2,
5411
- deltaE,
5412
- docCardLayout as docCardLayout2,
5413
- docZoomTrack as docZoomTrack2,
5414
- hexToRgb,
5415
- parseCssColour,
5416
- pinBox as pinBox2,
5417
- ratedSegments as ratedSegments5,
5418
- resolvePins as resolvePins2,
5419
- spanOutputExtent as spanOutputExtent5
5420
- } from "@vosjs/studio-core";
5421
4619
  var CALLOUT_DEFAULT_SECONDS = 3;
5422
4620
  var CALLOUT_LEAD_SECONDS = 0.2;
5423
4621
  var CALLOUT_DELTA_E_PROBLEM = 8;
@@ -5596,13 +4794,13 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
5596
4794
  const out = /* @__PURE__ */ new Map();
5597
4795
  if (!times.length) return out;
5598
4796
  const bare = { ...doc, overlays: [] };
5599
- const outDir = join8(dir, ".ground-check");
4797
+ const outDir = join6(dir, ".ground-check");
5600
4798
  const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
5601
4799
  for (const f of res.frames) {
5602
- if (!existsSync8(f.file)) continue;
4800
+ if (!existsSync6(f.file)) continue;
5603
4801
  out.set(
5604
4802
  +f.time.toFixed(3),
5605
- decodePng(new Uint8Array(await readFile8(f.file)))
4803
+ decodePng(new Uint8Array(await readFile6(f.file)))
5606
4804
  );
5607
4805
  }
5608
4806
  return out;
@@ -5611,241 +4809,9 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
5611
4809
  // src/plugin/kitPicture.ts
5612
4810
  import { execFile } from "child_process";
5613
4811
  import { promisify } from "util";
5614
- import { readFile as readFile9 } from "fs/promises";
5615
4812
  var execFileP = promisify(execFile);
5616
- var SUBJECT_BAND = { min: 0.6, max: 0.92 };
5617
4813
  var BLANK_INK = 0.12;
5618
- var SEPARATION_L = 8;
5619
- var SHADOW_PRESENT = 6;
5620
- var EDGE_PRESENT = 40;
5621
- var DUPLICATE_BITS2 = 6;
5622
- var HALFSIZE_KEEP = 0.45;
5623
- function apcaContrast(text, bg) {
5624
- const lum = (c) => {
5625
- const ch = (v) => Math.pow(v / 255, 2.4);
5626
- let y = 0.2126729 * ch(c[0]) + 0.7151522 * ch(c[1]) + 0.072175 * ch(c[2]);
5627
- if (y < 0.022) y += Math.pow(0.022 - y, 1.414);
5628
- return y;
5629
- };
5630
- const yt = lum(text);
5631
- const yb = lum(bg);
5632
- if (Math.abs(yb - yt) < 5e-4) return 0;
5633
- let sapc;
5634
- if (yb > yt) {
5635
- sapc = (Math.pow(yb, 0.56) - Math.pow(yt, 0.57)) * 1.14;
5636
- return sapc < 0.1 ? 0 : (sapc - 0.027) * 100;
5637
- }
5638
- sapc = (Math.pow(yb, 0.65) - Math.pow(yt, 0.62)) * 1.14;
5639
- return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
5640
- }
5641
4814
  var pct = (v) => `${Math.round(v * 100)}%`;
5642
- var hexToRgb2 = (h) => {
5643
- const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
5644
- if (!m) return null;
5645
- const n = parseInt(m[1], 16);
5646
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
5647
- };
5648
- function stillFindings(a, img, m) {
5649
- const out = [];
5650
- const genre = a.spec?.genre;
5651
- const bledAll = m.bleed.length === 4;
5652
- if (a.shot) {
5653
- const sx = Math.max(0, a.shot.x) * img.w;
5654
- const sy = Math.max(0, a.shot.y) * img.h;
5655
- const sw = Math.min(1, a.shot.x + a.shot.w) * img.w - sx;
5656
- const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
5657
- const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
5658
- const ink = inkCoverage(img, rect);
5659
- if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
5660
- out.push({
5661
- code: "blank",
5662
- severity: "error",
5663
- asset: a.destination,
5664
- message: `${pct(ink)} ink inside the shot: the moment shows a wallpaper, an empty canvas or a flat panel`,
5665
- fixHint: "pick a moment after a gesture landed (--shot-time, or --times step:<id>) and stage the set before recording",
5666
- bbox: rect
5667
- });
5668
- }
5669
- if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
5670
- out.push({
5671
- code: "subject",
5672
- severity: "error",
5673
- asset: a.destination,
5674
- message: `the shot is ${pct(a.shot.w)} of the width; a card sits at 60 to 92%, a bleed a little past the edge`,
5675
- fixHint: "the template layout places the slot; fix its place for this aspect",
5676
- bbox: rect
5677
- });
5678
- }
5679
- for (const f of textFindings(a, img)) out.push(f);
5680
- return out;
5681
- }
5682
- const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
5683
- if (m.ink < BLANK_INK) {
5684
- out.push({
5685
- code: "blank",
5686
- severity: "error",
5687
- asset: a.destination,
5688
- message: `${pct(m.ink)} ink inside the ${m.card ? "card" : "frame"}: a wallpaper, an empty canvas or a flat panel, not the product doing something`,
5689
- fixHint: "pick a moment after a gesture landed (--times step:<id>) and stage the set before recording: real data, a populated state",
5690
- bbox: subject
5691
- });
5692
- }
5693
- if (genre === "card") {
5694
- if (bledAll || m.widthPct === null) {
5695
- out.push({
5696
- code: "subject",
5697
- severity: "error",
5698
- asset: a.destination,
5699
- message: bledAll ? "the picture fills the frame on all four sides: a crop, not a card on a ground" : "no card found: the frame is one flat tone",
5700
- fixHint: "present the card in a look (vos deliver reads BRAND.md or --look) or render this destination from a poster template"
5701
- });
5702
- } else if (m.widthPct < SUBJECT_BAND.min || m.widthPct > SUBJECT_BAND.max) {
5703
- out.push({
5704
- code: "subject",
5705
- severity: "error",
5706
- asset: a.destination,
5707
- message: `the card is ${pct(m.widthPct)} of the width; the references sit at ${pct(SUBJECT_BAND.min)} to ${pct(SUBJECT_BAND.max)}`,
5708
- fixHint: "the look places the card at 84% (frame.inset); a poster template sets its own placement",
5709
- bbox: m.card ?? void 0
5710
- });
5711
- }
5712
- if (m.card && !bledAll) {
5713
- const sep = m.separation ?? 0;
5714
- const shadow = m.shadow ?? 0;
5715
- if (sep < SEPARATION_L && shadow < SHADOW_PRESENT && m.edge < EDGE_PRESENT) {
5716
- out.push({
5717
- code: "separation",
5718
- severity: "error",
5719
- asset: a.destination,
5720
- message: `the card and the ground are ${sep.toFixed(1)} L* apart with no shadow halo (${shadow.toFixed(1)}) and no drawn edge (${m.edge.toFixed(0)}): the card dissolves into the plate`,
5721
- fixHint: "a contact shadow (frame.shadowContact) plus a hairline (frame.border with borderColor) makes a light card sit on a light ground; the plate look sets both",
5722
- bbox: m.card
5723
- });
5724
- }
5725
- }
5726
- }
5727
- if (genre === "screenshot" && a.composed && m.card && (m.widthPct ?? 0) < 0.98) {
5728
- out.push({
5729
- code: "subject",
5730
- severity: "error",
5731
- asset: a.destination,
5732
- message: `a store screenshot must be the real page full bleed; this one shows a ${pct(m.widthPct ?? 0)} card on a ground`,
5733
- fixHint: "drop --composed: the screenshot genre renders the page with no chrome and no padding",
5734
- bbox: m.card
5735
- });
5736
- }
5737
- if (img.w <= 500 && (a.spec?.text === "none" || genre === "card")) {
5738
- const full = edgeEnergy(img, Math.min(img.w, 400));
5739
- const half = edgeEnergy(img, Math.min(img.w, 400) >> 1);
5740
- const keep = full > 0 ? half / full : 1;
5741
- if (keep < HALFSIZE_KEEP) {
5742
- out.push({
5743
- code: "halfsize",
5744
- severity: "warning",
5745
- asset: a.destination,
5746
- message: `at half size the tile keeps ${pct(keep)} of its edges: fine text and thin lines vanish where the store shows it small`,
5747
- fixHint: "a tile is the subject large and saturated with no text (the store rule); render it from the card-on-gradient template"
5748
- });
5749
- }
5750
- }
5751
- for (const f of textFindings(a, img)) out.push(f);
5752
- return out;
5753
- }
5754
- function textFindings(a, img) {
5755
- const out = [];
5756
- for (const t of a.text ?? []) {
5757
- const box = {
5758
- x: t.x * img.w,
5759
- y: t.y * img.h,
5760
- w: t.w * img.w,
5761
- h: t.h * img.h
5762
- };
5763
- const name = t.label ? `"${t.label}"` : "a text box";
5764
- if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
5765
- out.push({
5766
- code: "sliced",
5767
- severity: "error",
5768
- asset: a.destination,
5769
- message: `${name} crosses the frame edge: cut mid-word`,
5770
- fixHint: "shorten the line, or let the template recompose (headline lines at 12 to 14% of the height, inside the safe rect)",
5771
- bbox: box
5772
- });
5773
- }
5774
- const s = a.spec?.safe;
5775
- if (s) {
5776
- const sx = s.x * img.w;
5777
- const sy = s.y * img.h;
5778
- const sw = s.w * img.w;
5779
- const sh = s.h * img.h;
5780
- if (box.x < sx - 0.5 || box.y < sy - 0.5 || box.x + box.w > sx + sw + 0.5 || box.y + box.h > sy + sh + 0.5) {
5781
- out.push({
5782
- code: "safe",
5783
- severity: "warning",
5784
- asset: a.destination,
5785
- message: `${name} sits outside the destination's safe rect (${pct(s.w)}x${pct(s.h)} at ${pct(s.x)},${pct(s.y)}): the platform's chrome or crop covers it`,
5786
- fixHint: "move the text inside the safe rect; the template reads channel-specs safe",
5787
- bbox: box
5788
- });
5789
- }
5790
- }
5791
- const rgb = t.color ? hexToRgb2(t.color) : null;
5792
- if (rgb) {
5793
- const ground = medianColour(img, box);
5794
- const lc = apcaContrast(rgb, ground);
5795
- const floor = t.role === "body" ? 75 : 60;
5796
- if (lc < floor) {
5797
- out.push({
5798
- code: "contrast",
5799
- severity: "warning",
5800
- asset: a.destination,
5801
- message: `${name} reads APCA Lc ${lc.toFixed(0)} on its ground; ${t.role === "body" ? "body" : "a headline"} wants ${floor}`,
5802
- fixHint: "darken the ink or lighten the ground under the text (BRAND.md ink on bgA/bgB, never on the shot)",
5803
- bbox: box
5804
- });
5805
- }
5806
- }
5807
- }
5808
- if (a.spec?.text === "none" && (a.text?.length ?? 0) > 0) {
5809
- out.push({
5810
- code: "safe",
5811
- severity: "warning",
5812
- asset: a.destination,
5813
- message: "this destination wants no text (the picture carries it alone) and the kit put words on it",
5814
- fixHint: "render the tile from the card-on-gradient template with no headline"
5815
- });
5816
- }
5817
- return out;
5818
- }
5819
- function duplicateFindings(stills) {
5820
- const groups = [];
5821
- const seen = /* @__PURE__ */ new Set();
5822
- const comparable = (a, b) => a.genre === "screenshot" || b.genre === "screenshot" ? a.destination === b.destination : true;
5823
- for (let i = 0; i < stills.length; i++) {
5824
- if (seen.has(i)) continue;
5825
- const g = [stills[i]];
5826
- for (let j = i + 1; j < stills.length; j++) {
5827
- if (seen.has(j)) continue;
5828
- if (!comparable(stills[i], stills[j])) continue;
5829
- if (hammingDistance(stills[i].hash, stills[j].hash) <= DUPLICATE_BITS2) {
5830
- g.push(stills[j]);
5831
- seen.add(j);
5832
- }
5833
- }
5834
- if (g.length > 1) groups.push(g);
5835
- }
5836
- return groups.map((g) => {
5837
- const composed = g.every(
5838
- (s) => stills.find((x) => x.destination === s.destination)?.composed
5839
- );
5840
- return {
5841
- code: "duplicate",
5842
- severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
5843
- asset: g.map((s) => s.destination).join(", "),
5844
- message: composed ? `${g.length} channels share one cover: ${g.map((s) => s.destination).join(", ")}` : `${g.length} assets share one frame${g[0].time !== null ? ` (${g[0].time.toFixed(2)}s)` : ""}: ${g.map((s) => s.destination).join(", ")}`,
5845
- fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster document per class (poster/<class>/doc.json) or per destination (LAUNCH.md poster roles) gives each channel its own cover"
5846
- };
5847
- });
5848
- }
5849
4815
  async function videoFrame(file, at2, ffmpeg2) {
5850
4816
  try {
5851
4817
  const { stdout } = await execFileP(
@@ -5880,91 +4846,62 @@ async function onPath(bin) {
5880
4846
  return false;
5881
4847
  }
5882
4848
  }
4849
+ function frameFindings(asset, name, img, m) {
4850
+ const out = [];
4851
+ const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
4852
+ const ink = inkCoverage(img, subject);
4853
+ if (ink < BLANK_INK) {
4854
+ out.push({
4855
+ code: "firstlast",
4856
+ severity: "error",
4857
+ asset,
4858
+ message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
4859
+ fixHint: name === "first" ? "open on the product (trim the head, or an entrance over a populated frame)" : "end on a resolved state or an end card"
4860
+ });
4861
+ }
4862
+ if (m.bleed.length === 4) {
4863
+ out.push({
4864
+ code: "firstlast",
4865
+ severity: "warning",
4866
+ asset,
4867
+ message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
4868
+ fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
4869
+ });
4870
+ }
4871
+ return out;
4872
+ }
5883
4873
  async function pictureChecks(assets) {
5884
4874
  const findings = [];
5885
4875
  const measured = [];
5886
- const stills = [];
5887
4876
  let ffmpeg2 = null;
5888
4877
  for (const a of assets) {
5889
- if (/\.png$/i.test(a.file)) {
5890
- const img = decodePng(new Uint8Array(await readFile9(a.file)));
5891
- if (!img) {
5892
- findings.push({
5893
- code: "unreadable",
5894
- severity: "info",
5895
- asset: a.destination,
5896
- message: `${a.path} is not an 8-bit non-interlaced PNG; the picture checks cannot read it`,
5897
- fixHint: "render stills through vos deliver or vos frames, which write readable PNGs"
5898
- });
5899
- measured.push({ destination: a.destination, measure: null });
5900
- continue;
5901
- }
5902
- const m = measureStill(img);
5903
- measured.push({ destination: a.destination, measure: m });
5904
- findings.push(...stillFindings(a, img, m));
5905
- if (a.spec?.kind !== "video")
5906
- stills.push({
5907
- destination: a.destination,
5908
- hash: m.hash,
5909
- time: null,
5910
- genre: a.spec?.genre,
5911
- composed: !!a.shot
5912
- });
4878
+ if (!/\.(mp4|webm|mov)$/i.test(a.file)) continue;
4879
+ if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
4880
+ if (!ffmpeg2) {
4881
+ findings.push({
4882
+ code: "firstlast",
4883
+ severity: "info",
4884
+ asset: a.destination,
4885
+ message: "ffmpeg is not on PATH, so the first and last frames were not read",
4886
+ fixHint: "install ffmpeg to have the video checks run"
4887
+ });
4888
+ measured.push({ destination: a.destination, measure: null });
5913
4889
  continue;
5914
4890
  }
5915
- if (/\.(mp4|webm|mov)$/i.test(a.file)) {
5916
- if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
5917
- if (!ffmpeg2) {
5918
- findings.push({
5919
- code: "firstlast",
5920
- severity: "info",
5921
- asset: a.destination,
5922
- message: "ffmpeg is not on PATH, so the first and last frames were not read",
5923
- fixHint: "install ffmpeg to have the video checks run"
5924
- });
5925
- measured.push({ destination: a.destination, measure: null });
5926
- continue;
5927
- }
5928
- const seconds = a.seconds ?? 0;
5929
- const first = await videoFrame(a.file, 0, "ffmpeg");
5930
- const last = await videoFrame(
5931
- a.file,
5932
- Math.max(0, seconds - 0.1),
5933
- "ffmpeg"
5934
- );
5935
- for (const [name, img] of [
5936
- ["first", first],
5937
- ["last", last]
5938
- ]) {
5939
- if (!img) continue;
5940
- const m = measureStill(img);
5941
- if (name === "first")
5942
- measured.push({ destination: a.destination, measure: m });
5943
- const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
5944
- const ink = inkCoverage(img, subject);
5945
- if (ink < BLANK_INK) {
5946
- findings.push({
5947
- code: "firstlast",
5948
- severity: "error",
5949
- asset: a.destination,
5950
- message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
5951
- fixHint: name === "first" ? "open on the product (trim the head, or an entrance over a populated frame)" : "end on a resolved state or an end card (the last frame is the poster)"
5952
- });
5953
- }
5954
- if (m.bleed.length === 4 && a.spec?.genre !== "screenshot") {
5955
- findings.push({
5956
- code: "firstlast",
5957
- severity: "warning",
5958
- asset: a.destination,
5959
- message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
5960
- fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
5961
- });
5962
- }
5963
- }
5964
- continue;
4891
+ const seconds = a.seconds ?? 0;
4892
+ const first = await videoFrame(a.file, 0, "ffmpeg");
4893
+ const last = await videoFrame(a.file, Math.max(0, seconds - 0.1), "ffmpeg");
4894
+ for (const [name, img] of [
4895
+ ["first", first],
4896
+ ["last", last]
4897
+ ]) {
4898
+ if (!img) continue;
4899
+ const m = measureStill(img);
4900
+ if (name === "first")
4901
+ measured.push({ destination: a.destination, measure: m });
4902
+ findings.push(...frameFindings(a.destination, name, img, m));
5965
4903
  }
5966
4904
  }
5967
- findings.push(...duplicateFindings(stills));
5968
4905
  const order = {
5969
4906
  error: 0,
5970
4907
  warning: 1,
@@ -5980,28 +4917,15 @@ function formatFinding(f) {
5980
4917
  }
5981
4918
 
5982
4919
  // src/plugin/validateKit.ts
5983
- import { existsSync as existsSync9 } from "fs";
5984
- import { readFile as readFile10, stat as stat2 } from "fs/promises";
5985
- import { dirname as dirname2, isAbsolute as isAbsolute4, join as join9 } from "path";
5986
- import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5987
- var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5988
- function pngDimensions(bytes) {
5989
- if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIG2)) return null;
5990
- if (bytes.toString("ascii", 12, 16) !== "IHDR") return null;
5991
- return { w: bytes.readUInt32BE(16), h: bytes.readUInt32BE(20) };
5992
- }
5993
- function sniffImage(bytes) {
5994
- if (bytes.subarray(0, 8).equals(PNG_SIG2)) return "png";
5995
- if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
5996
- return "webp";
5997
- if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216) return "jpeg";
5998
- return "unknown";
5999
- }
4920
+ import { existsSync as existsSync7 } from "fs";
4921
+ import { readFile as readFile7, stat } from "fs/promises";
4922
+ import { dirname, isAbsolute as isAbsolute2, join as join7 } from "path";
4923
+ import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
6000
4924
  async function probeVideo(path) {
6001
4925
  const MB = await import("mediabunny");
6002
4926
  const input = new MB.Input({
6003
4927
  formats: MB.ALL_FORMATS,
6004
- source: new MB.BufferSource(new Uint8Array(await readFile10(path)))
4928
+ source: new MB.BufferSource(new Uint8Array(await readFile7(path)))
6005
4929
  });
6006
4930
  try {
6007
4931
  const track = await input.getPrimaryVideoTrack();
@@ -6013,7 +4937,7 @@ async function probeVideo(path) {
6013
4937
  }
6014
4938
  }
6015
4939
  var specById = new Map(
6016
- DESTINATIONS3.map((d) => [d.id, d])
4940
+ DESTINATIONS2.map((d) => [d.id, d])
6017
4941
  );
6018
4942
  var near = (a, b, tol) => Math.abs(a - b) <= tol;
6019
4943
  async function validateKit(kitPath, opts = {}) {
@@ -6022,7 +4946,7 @@ async function validateKit(kitPath, opts = {}) {
6022
4946
  const measured = [];
6023
4947
  let kit;
6024
4948
  try {
6025
- kit = JSON.parse(await readFile10(kitPath, "utf8"));
4949
+ kit = JSON.parse(await readFile7(kitPath, "utf8"));
6026
4950
  } catch (e) {
6027
4951
  return {
6028
4952
  valid: false,
@@ -6039,13 +4963,12 @@ async function validateKit(kitPath, opts = {}) {
6039
4963
  measured
6040
4964
  };
6041
4965
  }
6042
- const base = dirname2(kitPath);
4966
+ const base = dirname(kitPath);
6043
4967
  const resolvePath = (p) => {
6044
- if (isAbsolute4(p) && existsSync9(p)) return p;
6045
- const beside = join9(base, p.split("/").pop() ?? p);
4968
+ if (isAbsolute2(p) && existsSync7(p)) return p;
4969
+ const beside = join7(base, p.split("/").pop() ?? p);
6046
4970
  return beside;
6047
4971
  };
6048
- const perDestination = /* @__PURE__ */ new Map();
6049
4972
  const pictureAssets = [];
6050
4973
  for (const a of kit.assets) {
6051
4974
  const id = a.destination ?? `${a.channel}-${a.asset}`;
@@ -6053,12 +4976,11 @@ async function validateKit(kitPath, opts = {}) {
6053
4976
  const file = resolvePath(a.path);
6054
4977
  let bytes;
6055
4978
  try {
6056
- bytes = (await stat2(file)).size;
4979
+ bytes = (await stat(file)).size;
6057
4980
  } catch {
6058
4981
  problems.push(`${label}: ${a.path} is missing`);
6059
4982
  continue;
6060
4983
  }
6061
- perDestination.set(id, (perDestination.get(id) ?? 0) + 1);
6062
4984
  if (bytes !== a.bytes)
6063
4985
  problems.push(
6064
4986
  `${label}: manifest says ${a.bytes} bytes, the file is ${bytes}`
@@ -6067,25 +4989,7 @@ async function validateKit(kitPath, opts = {}) {
6067
4989
  let w = null;
6068
4990
  let h = null;
6069
4991
  let seconds = null;
6070
- if (/\.(png|jpe?g|webp)$/i.test(file)) {
6071
- const head = Buffer.from(await readFile10(file));
6072
- const kind = sniffImage(head);
6073
- if (file.toLowerCase().endsWith(".png") && kind !== "png")
6074
- problems.push(
6075
- `${label}: ${a.path} is named .png but its bytes are ${kind} \u2014 stores refuse a mislabelled image`
6076
- );
6077
- const dims = kind === "png" ? pngDimensions(head) : null;
6078
- if (dims) {
6079
- w = dims.w;
6080
- h = dims.h;
6081
- if (w !== a.w || h !== a.h)
6082
- problems.push(
6083
- `${label}: manifest says ${a.w}x${a.h}, the file is ${w}x${h}`
6084
- );
6085
- } else if (kind === "png") {
6086
- problems.push(`${label}: ${a.path} has no readable IHDR`);
6087
- }
6088
- } else if (/\.(mp4|webm|mov)$/i.test(file)) {
4992
+ if (/\.(mp4|webm|mov)$/i.test(file)) {
6089
4993
  try {
6090
4994
  const v = await probeVideo(file);
6091
4995
  if (!v) problems.push(`${label}: ${a.path} has no video track`);
@@ -6108,21 +5012,14 @@ async function validateKit(kitPath, opts = {}) {
6108
5012
  );
6109
5013
  }
6110
5014
  } else {
6111
- warnings.push(
6112
- `${label}: ${a.path} is not an image or video the kit verifies`
6113
- );
5015
+ warnings.push(`${label}: ${a.path} is not a video the kit verifies`);
6114
5016
  }
6115
5017
  measured.push({ destination: id, path: a.path, w, h, bytes, seconds });
6116
5018
  pictureAssets.push({
6117
5019
  destination: id,
6118
5020
  path: a.path,
6119
5021
  file,
6120
- spec,
6121
- text: a.text,
6122
- seconds,
6123
- composed: a.composed,
6124
- shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
6125
- crop: a.crop
5022
+ seconds
6126
5023
  });
6127
5024
  if (!spec) {
6128
5025
  if (a.channel !== "demo")
@@ -6149,17 +5046,6 @@ async function validateKit(kitPath, opts = {}) {
6149
5046
  `${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
6150
5047
  );
6151
5048
  }
6152
- if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
6153
- warnings.push(
6154
- `${label}: spec renders png, the file is ${file.split(".").pop()}`
6155
- );
6156
- }
6157
- for (const [id, n] of perDestination) {
6158
- const spec = specById.get(id);
6159
- if (spec?.count && (n < spec.count.min || n > spec.count.max))
6160
- problems.push(
6161
- `${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
6162
- );
6163
5049
  }
6164
5050
  if (!opts.picture) {
6165
5051
  return { valid: problems.length === 0, problems, warnings, measured };
@@ -6178,7 +5064,7 @@ async function validateKit(kitPath, opts = {}) {
6178
5064
 
6179
5065
  // src/plugin/recorder.ts
6180
5066
  import { writeFileSync } from "fs";
6181
- import { join as join10 } from "path";
5067
+ import { join as join8 } from "path";
6182
5068
  import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
6183
5069
  async function recordTake(browser, url, actions, paths, log, opts = {}) {
6184
5070
  const dry = opts.dryRun === true;
@@ -6188,7 +5074,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6188
5074
  actions,
6189
5075
  {
6190
5076
  frame: (file, bytes) => {
6191
- writeFileSync(join10(paths.framesDir, file), bytes);
5077
+ writeFileSync(join8(paths.framesDir, file), bytes);
6192
5078
  }
6193
5079
  },
6194
5080
  log,
@@ -6240,11 +5126,11 @@ import {
6240
5126
 
6241
5127
  // src/plugin/session.ts
6242
5128
  import { spawn } from "child_process";
6243
- import { existsSync as existsSync10, mkdirSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
5129
+ import { existsSync as existsSync8, mkdirSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
6244
5130
  import { homedir } from "os";
6245
- import { join as join11 } from "path";
5131
+ import { join as join9 } from "path";
6246
5132
  import { chromium } from "playwright";
6247
- var SESSIONS_DIR = join11(homedir(), ".config", "vos", "sessions");
5133
+ var SESSIONS_DIR = join9(homedir(), ".config", "vos", "sessions");
6248
5134
  var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
6249
5135
  function sessionNameVerdict(name) {
6250
5136
  if (!name) return "a session needs a name: --name <app>";
@@ -6253,20 +5139,20 @@ function sessionNameVerdict(name) {
6253
5139
  return null;
6254
5140
  }
6255
5141
  function sessionDir(name) {
6256
- return join11(SESSIONS_DIR, name);
5142
+ return join9(SESSIONS_DIR, name);
6257
5143
  }
6258
5144
  function systemChromePath() {
6259
5145
  const explicit = process.env.VOS_BROWSER_PATH;
6260
5146
  if (explicit) return explicit;
6261
5147
  const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
6262
- join11(
5148
+ join9(
6263
5149
  process.env["PROGRAMFILES"] ?? "C:\\Program Files",
6264
5150
  "Google",
6265
5151
  "Chrome",
6266
5152
  "Application",
6267
5153
  "chrome.exe"
6268
5154
  ),
6269
- join11(
5155
+ join9(
6270
5156
  process.env["LOCALAPPDATA"] ?? "",
6271
5157
  "Google",
6272
5158
  "Chrome",
@@ -6278,7 +5164,7 @@ function systemChromePath() {
6278
5164
  "/usr/bin/google-chrome-stable",
6279
5165
  "/opt/google/chrome/chrome"
6280
5166
  ];
6281
- const found = candidates.find((c) => existsSync10(c));
5167
+ const found = candidates.find((c) => existsSync8(c));
6282
5168
  if (!found)
6283
5169
  throw new Error(
6284
5170
  "no Google Chrome found: a session needs the system Chrome (the bundled Chromium cannot read a profile Chrome wrote). Install Chrome, or set VOS_BROWSER_PATH"
@@ -6301,15 +5187,15 @@ async function openSession(name, url, opts = { log: () => {
6301
5187
  opts.log(
6302
5188
  `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
6303
5189
  );
6304
- return new Promise((resolve16, reject) => {
5190
+ return new Promise((resolve14, reject) => {
6305
5191
  const child = spawn(exe, args, { stdio: "ignore" });
6306
5192
  child.on("error", reject);
6307
- child.on("exit", (code) => resolve16({ code }));
5193
+ child.on("exit", (code) => resolve14({ code }));
6308
5194
  });
6309
5195
  }
6310
5196
  async function withSession(name, fn, opts = {}) {
6311
5197
  const dir = sessionDir(name);
6312
- if (!existsSync10(dir))
5198
+ if (!existsSync8(dir))
6313
5199
  throw new Error(`no session "${name}" (vos session list)`);
6314
5200
  const explicit = process.env.VOS_BROWSER_PATH;
6315
5201
  const ctx = await chromium.launchPersistentContext(dir, {
@@ -6358,9 +5244,9 @@ function summaryLine(s) {
6358
5244
  return `session "${s.name}" holds cookies for ${hosts}${more}; ${exp}`;
6359
5245
  }
6360
5246
  function listSessions() {
6361
- if (!existsSync10(SESSIONS_DIR)) return [];
5247
+ if (!existsSync8(SESSIONS_DIR)) return [];
6362
5248
  return readdirSync(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
6363
- const st = statSync2(join11(SESSIONS_DIR, d.name));
5249
+ const st = statSync2(join9(SESSIONS_DIR, d.name));
6364
5250
  return {
6365
5251
  name: d.name,
6366
5252
  ageDays: Math.floor(
@@ -6371,7 +5257,7 @@ function listSessions() {
6371
5257
  }
6372
5258
  function removeSession(name) {
6373
5259
  const dir = sessionDir(name);
6374
- if (!existsSync10(dir)) return false;
5260
+ if (!existsSync8(dir)) return false;
6375
5261
  rmSync(dir, { recursive: true, force: true });
6376
5262
  return true;
6377
5263
  }
@@ -6436,9 +5322,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
6436
5322
  }
6437
5323
 
6438
5324
  // src/plugin/plan.ts
6439
- import { existsSync as existsSync11 } from "fs";
6440
- import { readFile as readFile11 } from "fs/promises";
6441
- import { join as join12 } from "path";
5325
+ import { existsSync as existsSync9 } from "fs";
5326
+ import { readFile as readFile8 } from "fs/promises";
5327
+ import { join as join10 } from "path";
6442
5328
  import {
6443
5329
  DEFAULT_FRAME_STYLE,
6444
5330
  STYLE_FIELDS,
@@ -6660,10 +5546,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
6660
5546
  // src/plugin/plan.ts
6661
5547
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6662
5548
  async function readDigestActivity(dir) {
6663
- const file = join12(dir, "digest", "digest.json");
6664
- if (!existsSync11(file)) return null;
5549
+ const file = join10(dir, "digest", "digest.json");
5550
+ if (!existsSync9(file)) return null;
6665
5551
  try {
6666
- const d = JSON.parse(await readFile11(file, "utf8"));
5552
+ const d = JSON.parse(await readFile8(file, "utf8"));
6667
5553
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
6668
5554
  } catch {
6669
5555
  return null;
@@ -6877,17 +5763,17 @@ async function planTake(dir, opts = {}) {
6877
5763
 
6878
5764
  // src/plugin/sync.ts
6879
5765
  import { createHash } from "crypto";
6880
- import { existsSync as existsSync12, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
6881
- import { readFile as readFile12 } from "fs/promises";
5766
+ import { existsSync as existsSync10, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
5767
+ import { readFile as readFile9 } from "fs/promises";
6882
5768
  import { createInterface } from "readline/promises";
6883
- import { basename, join as join14 } from "path";
6884
- import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
5769
+ import { basename, join as join12 } from "path";
5770
+ import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
6885
5771
 
6886
5772
  // src/plugin/folder.ts
6887
- import { mkdir as mkdir6, writeFile as writeFile8 } from "fs/promises";
6888
- import { join as join13 } from "path";
5773
+ import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
5774
+ import { join as join11 } from "path";
6889
5775
  import { recipeHints } from "@vosjs/shared/frontmatter";
6890
- import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
5776
+ import { migrateHostedDoc } from "@vosjs/studio-core";
6891
5777
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
6892
5778
  async function listFolders(origin, key) {
6893
5779
  const r = await apiJson(origin, "/api/folders", { key });
@@ -7061,25 +5947,25 @@ async function cmdPull(argv) {
7061
5947
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
7062
5948
  const payload = res.body;
7063
5949
  const out = strFlag(flags, "out") ?? folder.slug;
7064
- await mkdir6(join13(out, "recipes"), { recursive: true });
5950
+ await mkdir5(join11(out, "recipes"), { recursive: true });
7065
5951
  const lines = [];
7066
5952
  const recipes = [];
7067
5953
  for (const rec of payload.recipes) {
7068
- const path = join13(out, "recipes", safeName(rec.filename));
7069
- await writeFile8(path, rec.body ?? "");
5954
+ const path = join11(out, "recipes", safeName(rec.filename));
5955
+ await writeFile7(path, rec.body ?? "");
7070
5956
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
7071
5957
  }
7072
5958
  for (const rec of payload.inheritedRecipes) {
7073
5959
  const from = safeName(rec.folderSlug ?? "inherited");
7074
- await mkdir6(join13(out, "recipes", "_inherited", from), { recursive: true });
7075
- const path = join13(
5960
+ await mkdir5(join11(out, "recipes", "_inherited", from), { recursive: true });
5961
+ const path = join11(
7076
5962
  out,
7077
5963
  "recipes",
7078
5964
  "_inherited",
7079
5965
  from,
7080
5966
  safeName(rec.filename)
7081
5967
  );
7082
- await writeFile8(path, rec.body ?? "");
5968
+ await writeFile7(path, rec.body ?? "");
7083
5969
  recipes.push({
7084
5970
  path,
7085
5971
  line: `${recipeLine(rec.filename, rec.body ?? "")} [inherited from ${rec.folderName ?? from}]`
@@ -7087,15 +5973,15 @@ async function cmdPull(argv) {
7087
5973
  }
7088
5974
  const members = [];
7089
5975
  for (const v of payload.voses) {
7090
- const dir = join13(out, "members", safeName(v.slug || v.title || v.id));
7091
- await mkdir6(dir, { recursive: true });
5976
+ const dir = join11(out, "members", safeName(v.slug || v.title || v.id));
5977
+ await mkdir5(dir, { recursive: true });
7092
5978
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
7093
5979
  if (cfg.status !== 200) {
7094
5980
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
7095
5981
  continue;
7096
5982
  }
7097
- await writeFile8(
7098
- join13(dir, "config.json"),
5983
+ await writeFile7(
5984
+ join11(dir, "config.json"),
7099
5985
  JSON.stringify(cfg.body.config, null, 2)
7100
5986
  );
7101
5987
  let take = false;
@@ -7106,13 +5992,13 @@ async function cmdPull(argv) {
7106
5992
  { key }
7107
5993
  );
7108
5994
  if (doc.status === 200) {
7109
- await writeFile8(
7110
- join13(dir, "doc.json"),
5995
+ await writeFile7(
5996
+ join11(dir, "doc.json"),
7111
5997
  JSON.stringify(doc.body, null, 2)
7112
5998
  );
7113
5999
  take = true;
7114
6000
  if (flags.media === true) {
7115
- const hosted = migrateHostedDoc2(doc.body);
6001
+ const hosted = migrateHostedDoc(doc.body);
7116
6002
  await pullMedia({ origin, key }, dir, hosted, r.log);
7117
6003
  }
7118
6004
  }
@@ -7233,8 +6119,8 @@ function storageStateInDir(dir) {
7233
6119
  if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
7234
6120
  continue;
7235
6121
  try {
7236
- const v = JSON.parse(readFileSync2(join14(dir, f), "utf8"));
7237
- if (looksLikeStorageState(v)) return join14(dir, f);
6122
+ const v = JSON.parse(readFileSync2(join12(dir, f), "utf8"));
6123
+ if (looksLikeStorageState(v)) return join12(dir, f);
7238
6124
  } catch {
7239
6125
  }
7240
6126
  }
@@ -7255,7 +6141,7 @@ async function pushTake(dir, flags, r) {
7255
6141
  throw new Error(`doc.json fails lint:
7256
6142
  ${lint.problems.join("\n ")}`);
7257
6143
  }
7258
- if (!existsSync12(take.paths.recording)) {
6144
+ if (!existsSync10(take.paths.recording)) {
7259
6145
  throw new Error(
7260
6146
  "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
7261
6147
  );
@@ -7304,7 +6190,7 @@ async function pushTake(dir, flags, r) {
7304
6190
  throw new Error("push cancelled");
7305
6191
  }
7306
6192
  }
7307
- const bytes = await readFile12(take.paths.recording);
6193
+ const bytes = await readFile9(take.paths.recording);
7308
6194
  const hash = createHash("sha256").update(bytes).digest("hex");
7309
6195
  const { type: contentType } = resolveMediaType({
7310
6196
  head: bytes.subarray(0, MEDIA_HEAD_BYTES),
@@ -7476,9 +6362,9 @@ async function pullTake(dir, flags, r) {
7476
6362
  };
7477
6363
  }
7478
6364
  let media2;
7479
- if (flags.media && existsSync12(join14(dir, "doc.json"))) {
6365
+ if (flags.media && existsSync10(join12(dir, "doc.json"))) {
7480
6366
  const local = JSON.parse(
7481
- await readFile12(join14(dir, "doc.json"), "utf8")
6367
+ await readFile9(join12(dir, "doc.json"), "utf8")
7482
6368
  );
7483
6369
  media2 = await pullMedia(ctx, dir, local, r.log);
7484
6370
  }
@@ -7536,13 +6422,13 @@ async function pullTake(dir, flags, r) {
7536
6422
  docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
7537
6423
  );
7538
6424
  }
7539
- const hostedDoc = migrateHostedDoc3(docRes.json);
6425
+ const hostedDoc = migrateHostedDoc2(docRes.json);
7540
6426
  const doc = hostedDoc;
7541
6427
  const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
7542
- if (existsSync12(localRecording)) {
6428
+ if (existsSync10(localRecording)) {
7543
6429
  doc.source.videoKey = basename(localRecording);
7544
6430
  }
7545
- await writeJson(join14(dir, "doc.json"), doc, true);
6431
+ await writeJson(join12(dir, "doc.json"), doc, true);
7546
6432
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
7547
6433
  const versions = await api(ctx, `/vos/${vosId}/versions`);
7548
6434
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -7598,8 +6484,8 @@ async function pushTakeClaimable(dir, flags, r) {
7598
6484
  `${ref.where} (${file}) is not a file this take fetched from vos.so, and a claimable push never uploads footage. Push the take with an account (vos login, then vos push ${dir}), or remix a public take fetched with \`vos fetch <url> --media\``
7599
6485
  );
7600
6486
  }
7601
- const path = join14(dir, file);
7602
- const now = existsSync12(path) ? await fileSha256(path) : null;
6487
+ const path = join12(dir, file);
6488
+ const now = existsSync10(path) ? await fileSha256(path) : null;
7603
6489
  if (now !== origin2.sha256) {
7604
6490
  throw new Error(
7605
6491
  `${ref.where} (${file}) changed since it was fetched, so it is no longer the hosted file. A claimable push names only the original; push with an account to upload the new one`
@@ -7633,16 +6519,16 @@ async function pushTakeClaimable(dir, flags, r) {
7633
6519
  }
7634
6520
 
7635
6521
  // src/plugin/program.ts
7636
- import { existsSync as existsSync14 } from "fs";
7637
- import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile9 } from "fs/promises";
6522
+ import { existsSync as existsSync12 } from "fs";
6523
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
7638
6524
  import { createInterface as createInterface2 } from "readline/promises";
7639
- import { basename as basename3, dirname as dirname3, join as join15 } from "path";
6525
+ import { basename as basename3, dirname as dirname2, join as join13 } from "path";
7640
6526
  import {
7641
6527
  CURRENT_CONFIG_VERSION,
7642
6528
  migrateConfig,
7643
6529
  vosConfigJsonSchema
7644
6530
  } from "@vosjs/core";
7645
- import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc4 } from "@vosjs/studio-core";
6531
+ import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
7646
6532
 
7647
6533
  // src/plugin/login.ts
7648
6534
  import { hostname } from "os";
@@ -7770,12 +6656,12 @@ async function handoffLogin(url, r, opts = {}) {
7770
6656
 
7771
6657
  // src/plugin/programFiles.ts
7772
6658
  import { basename as basename2, extname as extname2, relative as relative2 } from "path";
7773
- import { existsSync as existsSync13, realpathSync } from "fs";
6659
+ import { existsSync as existsSync11, realpathSync } from "fs";
7774
6660
  function isLocalName(value) {
7775
6661
  if (/^(https?:|data:|blob:|asset:|media:)/.test(value)) return false;
7776
6662
  if (value.startsWith("$")) return false;
7777
6663
  if (value.startsWith("<svg") || value.startsWith("<?xml")) return false;
7778
- if (value.startsWith("/")) return existsSync13(value);
6664
+ if (value.startsWith("/")) return existsSync11(value);
7779
6665
  return true;
7780
6666
  }
7781
6667
  var FILE_EXT = /\.(png|jpe?g|webp|gif|avif|svg|mp4|webm|mov|mp3|m4a|ogg|wav|flac|aac|glb|gltf|woff2?|ttf|otf|hdr|exr)$/i;
@@ -7950,8 +6836,8 @@ function preflightConfig(parsed) {
7950
6836
  }
7951
6837
  function resolveConfigPath(target) {
7952
6838
  if (target.endsWith(".json")) return target;
7953
- const inDir = join15(target, "config.json");
7954
- if (existsSync14(inDir)) return inDir;
6839
+ const inDir = join13(target, "config.json");
6840
+ if (existsSync12(inDir)) return inDir;
7955
6841
  throw new UsageError(
7956
6842
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
7957
6843
  );
@@ -7977,9 +6863,9 @@ async function cmdFetch(argv) {
7977
6863
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
7978
6864
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
7979
6865
  const out = strFlag(flags, "out") ?? slug;
7980
- await mkdir7(out, { recursive: true });
7981
- await writeFile9(
7982
- join15(out, "config.json"),
6866
+ await mkdir6(out, { recursive: true });
6867
+ await writeFile8(
6868
+ join13(out, "config.json"),
7983
6869
  JSON.stringify(cfg.body.config, null, 2)
7984
6870
  );
7985
6871
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -8001,15 +6887,15 @@ async function cmdFetch(argv) {
8001
6887
  }
8002
6888
  );
8003
6889
  if (doc.status === 200 && !("source" in doc.body)) {
8004
- const hosted = migrateHostedDoc4(doc.body);
6890
+ const hosted = migrateHostedDoc3(doc.body);
8005
6891
  const { config: own } = await writeProgramDoc(out, hosted);
8006
6892
  if (own) {
8007
- await writeFile9(join15(out, "config.json"), JSON.stringify(own, null, 2));
6893
+ await writeFile8(join13(out, "config.json"), JSON.stringify(own, null, 2));
8008
6894
  }
8009
6895
  } else if (doc.status === 200) {
8010
6896
  take = true;
8011
- const hosted = migrateHostedDoc4(doc.body);
8012
- await writeFile9(join15(out, "doc.json"), JSON.stringify(hosted, null, 2));
6897
+ const hosted = migrateHostedDoc3(doc.body);
6898
+ await writeFile8(join13(out, "doc.json"), JSON.stringify(hosted, null, 2));
8013
6899
  if (flags.media === true) {
8014
6900
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
8015
6901
  await recordHostedMedia(out, vosId, media);
@@ -8019,15 +6905,15 @@ async function cmdFetch(argv) {
8019
6905
  }
8020
6906
  let assetsLine = "";
8021
6907
  if (!take) {
8022
- const configPath = join15(out, "config.json");
8023
- const written = JSON.parse(await readFile13(configPath, "utf8"));
6908
+ const configPath = join13(out, "config.json");
6909
+ const written = JSON.parse(await readFile10(configPath, "utf8"));
8024
6910
  const hostedCount = manifestRefs(written).filter(
8025
6911
  ({ ref }) => hostedAssetId(ref, [origin, "https://vos.so", "https://www.vos.so"])
8026
6912
  ).length;
8027
6913
  if (hostedCount && flags.media === true) {
8028
6914
  const pulled = await pullManifest({ origin, key }, out, written, r.log);
8029
6915
  if (pulled) {
8030
- await writeFile9(
6916
+ await writeFile8(
8031
6917
  configPath,
8032
6918
  JSON.stringify({ ...written, assets: pulled.assets }, null, 2)
8033
6919
  );
@@ -8146,8 +7032,8 @@ async function cmdPushProgram(argv) {
8146
7032
  origin: strFlag(flags, "origin"),
8147
7033
  api: strFlag(flags, "api")
8148
7034
  });
8149
- const dir = dirname3(source);
8150
- const parsed = JSON.parse(await readFile13(source, "utf8"));
7035
+ const dir = dirname2(source);
7036
+ const parsed = JSON.parse(await readFile10(source, "utf8"));
8151
7037
  const pre = preflightConfig(parsed);
8152
7038
  if (!pre.ok || !pre.config) {
8153
7039
  for (const issue of pre.issues) r.log(`error ${issue}`);
@@ -8430,7 +7316,7 @@ ${landedLines(landed2)}` : "")
8430
7316
  async function cmdPullProgram(argv) {
8431
7317
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
8432
7318
  const target = positionals[0] ?? ".";
8433
- const dir = target.endsWith(".json") ? dirname3(target) : target;
7319
+ const dir = target.endsWith(".json") ? dirname2(target) : target;
8434
7320
  const r = createReporter(flags.json === true);
8435
7321
  const origin = platformOrigin({
8436
7322
  origin: strFlag(flags, "origin"),
@@ -8495,10 +7381,10 @@ async function cmdPullProgram(argv) {
8495
7381
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
8496
7382
  if (cfg.status !== 200)
8497
7383
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
8498
- const configPath = join15(dir, "config.json");
7384
+ const configPath = join13(dir, "config.json");
8499
7385
  let backedUp = false;
8500
- if (existsSync14(configPath)) {
8501
- await writeFile9(join15(dir, "config.backup.json"), await readFile13(configPath));
7386
+ if (existsSync12(configPath)) {
7387
+ await writeFile8(join13(dir, "config.backup.json"), await readFile10(configPath));
8502
7388
  backedUp = true;
8503
7389
  }
8504
7390
  let headConfig = cfg.body.config;
@@ -8512,12 +7398,12 @@ async function cmdPullProgram(argv) {
8512
7398
  if (docRes.status === 200 && !("source" in docRes.body)) {
8513
7399
  const { config: own } = await writeProgramDoc(
8514
7400
  dir,
8515
- migrateHostedDoc4(docRes.body)
7401
+ migrateHostedDoc3(docRes.body)
8516
7402
  );
8517
7403
  if (own) headConfig = own;
8518
7404
  }
8519
7405
  }
8520
- await writeFile9(configPath, JSON.stringify(headConfig, null, 2));
7406
+ await writeFile8(configPath, JSON.stringify(headConfig, null, 2));
8521
7407
  writeSyncState(dir, {
8522
7408
  vosId,
8523
7409
  versionId: typeof head.id === "string" ? head.id : since
@@ -8531,7 +7417,7 @@ async function cmdPullProgram(argv) {
8531
7417
  protected: protectedIds,
8532
7418
  changes,
8533
7419
  out: configPath,
8534
- backup: backedUp ? join15(dir, "config.backup.json") : null
7420
+ backup: backedUp ? join13(dir, "config.backup.json") : null
8535
7421
  },
8536
7422
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
8537
7423
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -8639,9 +7525,9 @@ function storedProgramConfig(config, programDoc) {
8639
7525
  return lowerProgramDoc(doc).config;
8640
7526
  }
8641
7527
  async function readProgramDoc(dir, config) {
8642
- const docPath = join15(dir, "doc.json");
8643
- if (!existsSync14(docPath)) return null;
8644
- const parsed = JSON.parse(await readFile13(docPath, "utf8"));
7528
+ const docPath = join13(dir, "doc.json");
7529
+ if (!existsSync12(docPath)) return null;
7530
+ const parsed = JSON.parse(await readFile10(docPath, "utf8"));
8645
7531
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
8646
7532
  return null;
8647
7533
  const raw = parsed;
@@ -8649,23 +7535,23 @@ async function readProgramDoc(dir, config) {
8649
7535
  return { ...raw, program: { ...program, config } };
8650
7536
  }
8651
7537
  async function setProgramStill(dir, t) {
8652
- const docPath = join15(dir, "doc.json");
7538
+ const docPath = join13(dir, "doc.json");
8653
7539
  let doc = { program: {}, audio: [] };
8654
- if (existsSync14(docPath)) {
8655
- const parsed = JSON.parse(await readFile13(docPath, "utf8"));
7540
+ if (existsSync12(docPath)) {
7541
+ const parsed = JSON.parse(await readFile10(docPath, "utf8"));
8656
7542
  if (!parsed || typeof parsed !== "object" || "source" in parsed)
8657
7543
  throw new UsageError(
8658
7544
  `${docPath} is a take's document; a take's cover is its own "still" field`
8659
7545
  );
8660
7546
  doc = parsed;
8661
7547
  }
8662
- await writeFile9(docPath, JSON.stringify({ ...doc, still: t }, null, 2) + "\n");
7548
+ await writeFile8(docPath, JSON.stringify({ ...doc, still: t }, null, 2) + "\n");
8663
7549
  }
8664
7550
  async function writeProgramDoc(dir, hosted) {
8665
7551
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
8666
7552
  const { config, ...rest } = program;
8667
7553
  const onDisk = { ...hosted, program: rest };
8668
- await writeFile9(join15(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
7554
+ await writeFile8(join13(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
8669
7555
  return {
8670
7556
  config: config && typeof config === "object" ? config : null
8671
7557
  };
@@ -9077,8 +7963,8 @@ async function cmdAsset(argv) {
9077
7963
  }
9078
7964
 
9079
7965
  // src/plugin/remove.ts
9080
- import { existsSync as existsSync15, rmSync as rmSync2, statSync as statSync3 } from "fs";
9081
- import { join as join16 } from "path";
7966
+ import { existsSync as existsSync13, rmSync as rmSync2, statSync as statSync3 } from "fs";
7967
+ import { join as join14 } from "path";
9082
7968
  import { createInterface as createInterface3 } from "readline/promises";
9083
7969
 
9084
7970
  // src/plugin/trash.ts
@@ -9257,7 +8143,7 @@ async function cmdDelete(argv) {
9257
8143
  api: strFlag(flags, "api")
9258
8144
  }).replace(/\/+$/, "");
9259
8145
  const key = requireCredential(strFlag(flags, "key"));
9260
- const dir = existsSync15(target) && statSync3(target).isDirectory() ? target : null;
8146
+ const dir = existsSync13(target) && statSync3(target).isDirectory() ? target : null;
9261
8147
  const vosId = dir ? readSyncState(dir)?.vosId : parseVosId(target);
9262
8148
  if (!vosId)
9263
8149
  throw new UsageError(
@@ -9300,7 +8186,7 @@ async function cmdDelete(argv) {
9300
8186
  const tracking = dir ?? trackingCwd(vosId);
9301
8187
  let unlinked = false;
9302
8188
  if (tracking) {
9303
- rmSync2(join16(tracking, SYNC_STATE_NAME), { force: true });
8189
+ rmSync2(join14(tracking, SYNC_STATE_NAME), { force: true });
9304
8190
  unlinked = true;
9305
8191
  }
9306
8192
  const trashed = res.body.trashed === true;
@@ -9325,13 +8211,13 @@ async function cmdDelete(argv) {
9325
8211
  }
9326
8212
  function trackingCwd(vosId) {
9327
8213
  const cwd = process.cwd();
9328
- if (!existsSync15(join16(cwd, SYNC_STATE_NAME))) return null;
8214
+ if (!existsSync13(join14(cwd, SYNC_STATE_NAME))) return null;
9329
8215
  return readSyncState(cwd)?.vosId === vosId ? cwd : null;
9330
8216
  }
9331
8217
 
9332
8218
  // src/plugin/buildProgram.ts
9333
8219
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
9334
- import { basename as basename5, dirname as dirname4, join as join17, resolve as resolve7 } from "path";
8220
+ import { basename as basename5, dirname as dirname3, join as join15, resolve as resolve5 } from "path";
9335
8221
  import { pathToFileURL } from "url";
9336
8222
  import { parse } from "acorn";
9337
8223
  import { fullAncestor } from "acorn-walk";
@@ -9531,7 +8417,7 @@ async function cmdBuild(argv) {
9531
8417
  if (!source)
9532
8418
  throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
9533
8419
  const r = createReporter(flags.json === true);
9534
- const file = resolve7(source);
8420
+ const file = resolve5(source);
9535
8421
  const text = readFileSync4(file, "utf8");
9536
8422
  const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
9537
8423
  const program = mod.default ?? mod.program;
@@ -9548,8 +8434,8 @@ async function cmdBuild(argv) {
9548
8434
  );
9549
8435
  return EXIT_ERROR;
9550
8436
  }
9551
- const out = resolve7(
9552
- strFlag(flags, "out") ?? join17(dirname4(file), "config.json")
8437
+ const out = resolve5(
8438
+ strFlag(flags, "out") ?? join15(dirname3(file), "config.json")
9553
8439
  );
9554
8440
  writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
9555
8441
  const sidecar = out.replace(/\.json$/, "") + ".sources.json";
@@ -9566,8 +8452,8 @@ async function cmdBuild(argv) {
9566
8452
 
9567
8453
  // src/plugin/compare.ts
9568
8454
  import { spawnSync } from "child_process";
9569
- import { existsSync as existsSync16, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
9570
- import { dirname as dirname5, join as join18, resolve as resolve8 } from "path";
8455
+ import { existsSync as existsSync14, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
8456
+ import { dirname as dirname4, join as join16, resolve as resolve6 } from "path";
9571
8457
  var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
9572
8458
  function sampleTimes(duration, every) {
9573
8459
  const step = every > 0 ? every : 1;
@@ -9628,13 +8514,13 @@ async function cmdCompare(argv) {
9628
8514
  );
9629
8515
  requireFfmpeg();
9630
8516
  const r = createReporter(flags.json === true);
9631
- const ref = resolve8(against);
8517
+ const ref = resolve6(against);
9632
8518
  const loaded = await loadVosConfig(source);
9633
8519
  for (const w of loaded.warnings) r.log(`note: ${w}`);
9634
8520
  const config = loaded.config;
9635
8521
  const duration = configDuration(config) ?? 5;
9636
8522
  const assets = await programAssets(config, {
9637
- baseDir: /^https?:\/\//.test(source) ? null : existsSync16(source) && statSync4(source).isDirectory() ? source : dirname5(source),
8523
+ baseDir: /^https?:\/\//.test(source) ? null : existsSync14(source) && statSync4(source).isDirectory() ? source : dirname4(source),
9638
8524
  origin: platformOrigin({}),
9639
8525
  key: resolveCredential(),
9640
8526
  log: (line) => r.log(line)
@@ -9650,16 +8536,16 @@ async function cmdCompare(argv) {
9650
8536
  const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
9651
8537
  const timesRaw = strFlag(flags, "times");
9652
8538
  const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
9653
- const outDir = resolve8(strFlag(flags, "out") ?? "compare");
8539
+ const outDir = resolve6(strFlag(flags, "out") ?? "compare");
9654
8540
  mkdirSync2(outDir, { recursive: true });
9655
8541
  const browser = await launchBrowser();
9656
8542
  const scores = [];
9657
8543
  try {
9658
8544
  for (const t of times) {
9659
8545
  const tag = t.toFixed(2);
9660
- const refPng = join18(outDir, `src-${tag}.png`);
9661
- const vosPng = join18(outDir, `vos-${tag}.png`);
9662
- const sheet = join18(outDir, `sheet-${tag}.png`);
8546
+ const refPng = join16(outDir, `src-${tag}.png`);
8547
+ const vosPng = join16(outDir, `vos-${tag}.png`);
8548
+ const sheet = join16(outDir, `sheet-${tag}.png`);
9663
8549
  const grab = ffmpeg([
9664
8550
  "-v",
9665
8551
  "error",
@@ -9716,7 +8602,7 @@ async function cmdCompare(argv) {
9716
8602
  } finally {
9717
8603
  await browser.close();
9718
8604
  }
9719
- const overview = join18(outDir, "overview.png");
8605
+ const overview = join16(outDir, "overview.png");
9720
8606
  if (scores.length) {
9721
8607
  const inputs = scores.flatMap((s) => ["-i", s.sheet]);
9722
8608
  const scaled = scores.map((_, i) => `[${i}:v]scale=iw/3:-2[s${i}]`).join(";");
@@ -9742,7 +8628,7 @@ async function cmdCompare(argv) {
9742
8628
  overview
9743
8629
  };
9744
8630
  writeFileSync3(
9745
- join18(outDir, "report.json"),
8631
+ join16(outDir, "report.json"),
9746
8632
  JSON.stringify(report, null, 2) + "\n"
9747
8633
  );
9748
8634
  r.done(
@@ -9753,28 +8639,28 @@ async function cmdCompare(argv) {
9753
8639
  }
9754
8640
 
9755
8641
  // src/plugin/port.ts
9756
- import { existsSync as existsSync20, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9757
- import { dirname as dirname8, join as join22, resolve as resolve11 } from "path";
8642
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
8643
+ import { dirname as dirname7, join as join20, resolve as resolve9 } from "path";
9758
8644
  import { pathToFileURL as pathToFileURL3 } from "url";
9759
8645
 
9760
8646
  // src/plugin/portInventory.ts
9761
8647
  import { spawnSync as spawnSync2 } from "child_process";
9762
8648
  import {
9763
- existsSync as existsSync18,
8649
+ existsSync as existsSync16,
9764
8650
  mkdirSync as mkdirSync3,
9765
8651
  readFileSync as readFileSync6,
9766
8652
  readdirSync as readdirSync4,
9767
8653
  statSync as statSync6
9768
8654
  } from "fs";
9769
- import { dirname as dirname6, join as join20, relative as relative3, resolve as resolve9 } from "path";
8655
+ import { dirname as dirname5, join as join18, relative as relative3, resolve as resolve7 } from "path";
9770
8656
  import { pathToFileURL as pathToFileURL2 } from "url";
9771
8657
  import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
9772
8658
 
9773
8659
  // src/plugin/portRemotion.ts
9774
- import { existsSync as existsSync17, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
8660
+ import { existsSync as existsSync15, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
9775
8661
  import { createServer as createServer2 } from "http";
9776
8662
  import { createRequire } from "module";
9777
- import { basename as basename6, extname as extname3, join as join19 } from "path";
8663
+ import { basename as basename6, extname as extname3, join as join17 } from "path";
9778
8664
  function themeColors(sources) {
9779
8665
  const out = {};
9780
8666
  const re = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*['"](#[0-9a-fA-F]{3,8})['"]/g;
@@ -9840,14 +8726,14 @@ function sourceTexts(dir) {
9840
8726
  if (depth > 4 || out.length > 200) return;
9841
8727
  for (const name of readdirSync3(d)) {
9842
8728
  if (name === "node_modules" || name.startsWith(".")) continue;
9843
- const p = join19(d, name);
8729
+ const p = join17(d, name);
9844
8730
  if (statSync5(p).isDirectory()) walk(p, depth + 1);
9845
8731
  else if (/\.(tsx?|jsx?|mjs)$/.test(name))
9846
8732
  out.push(readFileSync5(p, "utf8"));
9847
8733
  }
9848
8734
  };
9849
- const src = join19(dir, "src");
9850
- walk(existsSync17(src) ? src : dir, 0);
8735
+ const src = join17(dir, "src");
8736
+ walk(existsSync15(src) ? src : dir, 0);
9851
8737
  return out;
9852
8738
  }
9853
8739
  function entryPoint(dir) {
@@ -9859,7 +8745,7 @@ function entryPoint(dir) {
9859
8745
  "remotion/index.ts",
9860
8746
  "remotion/index.tsx"
9861
8747
  ])
9862
- if (existsSync17(join19(dir, p))) return join19(dir, p);
8748
+ if (existsSync15(join17(dir, p))) return join17(dir, p);
9863
8749
  return null;
9864
8750
  }
9865
8751
  var TYPES = {
@@ -9934,7 +8820,7 @@ async function readRemotion(dir, log) {
9934
8820
  const entry = entryPoint(dir);
9935
8821
  let bundler;
9936
8822
  try {
9937
- bundler = createRequire(join19(dir, "package.json"))("@remotion/bundler");
8823
+ bundler = createRequire(join17(dir, "package.json"))("@remotion/bundler");
9938
8824
  } catch {
9939
8825
  log?.(
9940
8826
  "a Remotion project whose dependencies are not installed: run `npm install` in it so vos can read its compositions (words, scenes, faces); reading only the render"
@@ -9951,9 +8837,9 @@ async function readRemotion(dir, log) {
9951
8837
  } });
9952
8838
  const server = createServer2((rq, rs) => {
9953
8839
  const path = decodeURIComponent(new URL(rq.url ?? "/", "http://x").pathname);
9954
- let f = join19(out, path);
9955
- if (!f.startsWith(out) || !existsSync17(f) || statSync5(f).isDirectory())
9956
- f = join19(out, "index.html");
8840
+ let f = join17(out, path);
8841
+ if (!f.startsWith(out) || !existsSync15(f) || statSync5(f).isDirectory())
8842
+ f = join17(out, "index.html");
9957
8843
  rs.writeHead(200, {
9958
8844
  "content-type": TYPES[extname3(f)] ?? "application/octet-stream"
9959
8845
  });
@@ -10052,10 +8938,10 @@ async function readRemotion(dir, log) {
10052
8938
  perScene.push({ scene: s.name, texts: read.texts ?? [] });
10053
8939
  }
10054
8940
  const sources = sourceTexts(dir);
10055
- const publicDir = join19(dir, "public");
8941
+ const publicDir = join17(dir, "public");
10056
8942
  const media = tree.audio.map((a) => {
10057
8943
  const name = basename6(a.src.split("?")[0]);
10058
- const local = existsSync17(join19(publicDir, name)) ? `public/${name}` : a.src;
8944
+ const local = existsSync15(join17(publicDir, name)) ? `public/${name}` : a.src;
10059
8945
  return {
10060
8946
  kind: "audio",
10061
8947
  src: local,
@@ -10096,9 +8982,9 @@ async function readRemotion(dir, log) {
10096
8982
 
10097
8983
  // src/plugin/portInventory.ts
10098
8984
  function detectEngine(dir) {
10099
- if (existsSync18(join20(dir, "hyperframes.json"))) return "hyperframes";
10100
- const pkg = join20(dir, "package.json");
10101
- if (existsSync18(pkg)) {
8985
+ if (existsSync16(join18(dir, "hyperframes.json"))) return "hyperframes";
8986
+ const pkg = join18(dir, "package.json");
8987
+ if (existsSync16(pkg)) {
10102
8988
  try {
10103
8989
  const p = JSON.parse(readFileSync6(pkg, "utf8"));
10104
8990
  const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
@@ -10112,14 +8998,14 @@ function detectEngine(dir) {
10112
8998
  return "page";
10113
8999
  }
10114
9000
  function sourcePage(source) {
10115
- if (!existsSync18(source)) return null;
9001
+ if (!existsSync16(source)) return null;
10116
9002
  if (statSync6(source).isFile()) return /\.html?$/i.test(source) ? source : null;
10117
- const index = join20(source, "index.html");
10118
- return existsSync18(index) ? index : null;
9003
+ const index = join18(source, "index.html");
9004
+ return existsSync16(index) ? index : null;
10119
9005
  }
10120
9006
  function findRender(dir) {
10121
- const videos = (d) => existsSync18(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join20(d, f)) : [];
10122
- for (const d of [join20(dir, "renders"), join20(dir, "out"), dir]) {
9007
+ const videos = (d) => existsSync16(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join18(d, f)) : [];
9008
+ for (const d of [join18(dir, "renders"), join18(dir, "out"), dir]) {
10123
9009
  const found = videos(d);
10124
9010
  if (found.length) return found[0];
10125
9011
  }
@@ -10389,8 +9275,8 @@ var SEEK_AND_READ = `((args) => {
10389
9275
  return out
10390
9276
  })`;
10391
9277
  async function readInventory(source, opts) {
10392
- const abs = resolve9(source);
10393
- const dir = existsSync18(abs) && statSync6(abs).isDirectory() ? abs : dirname6(abs);
9278
+ const abs = resolve7(source);
9279
+ const dir = existsSync16(abs) && statSync6(abs).isDirectory() ? abs : dirname5(abs);
10394
9280
  const engine = detectEngine(dir);
10395
9281
  const page = sourcePage(abs);
10396
9282
  const render = opts.render === void 0 ? findRender(dir) : opts.render;
@@ -10493,7 +9379,7 @@ async function readInventory(source, opts) {
10493
9379
  if (render && duration) {
10494
9380
  const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
10495
9381
  if (has) {
10496
- const refDir = join20(opts.outDir, "ref");
9382
+ const refDir = join18(opts.outDir, "ref");
10497
9383
  mkdirSync3(refDir, { recursive: true });
10498
9384
  const times = read.scenes.length >= 3 ? read.scenes.map(
10499
9385
  (s) => s.start + (s.duration ?? 1) / 2
@@ -10502,7 +9388,7 @@ async function readInventory(source, opts) {
10502
9388
  (_, i) => i + 0.5
10503
9389
  );
10504
9390
  for (const t of times.filter((t2) => t2 < duration)) {
10505
- const file = join20(refDir, `src-${t.toFixed(2)}.png`);
9391
+ const file = join18(refDir, `src-${t.toFixed(2)}.png`);
10506
9392
  const ok = spawnSync2("ffmpeg", [
10507
9393
  "-v",
10508
9394
  "error",
@@ -10532,7 +9418,7 @@ async function readInventory(source, opts) {
10532
9418
  const scriptStrings = scriptStringsOf(read.scriptText);
10533
9419
  return {
10534
9420
  source: relative3(process.cwd(), abs) || ".",
10535
- root: abs.endsWith(".html") ? dirname6(abs) : abs,
9421
+ root: abs.endsWith(".html") ? dirname5(abs) : abs,
10536
9422
  engine,
10537
9423
  width: read.width,
10538
9424
  height: read.height,
@@ -10556,8 +9442,8 @@ async function readInventory(source, opts) {
10556
9442
 
10557
9443
  // src/plugin/portScaffold.ts
10558
9444
  import { spawnSync as spawnSync3 } from "child_process";
10559
- import { copyFileSync, existsSync as existsSync19, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
10560
- import { basename as basename7, dirname as dirname7, join as join21, resolve as resolve10 } from "path";
9445
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
9446
+ import { basename as basename7, dirname as dirname6, join as join19, resolve as resolve8 } from "path";
10561
9447
  import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
10562
9448
 
10563
9449
  // src/plugin/painterLib.ts
@@ -10966,9 +9852,9 @@ function planScaffold(inv, opts) {
10966
9852
  const audio = [];
10967
9853
  const copies = [];
10968
9854
  inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
10969
- const from = resolve10(opts.sourceDir, m.src);
9855
+ const from = resolve8(opts.sourceDir, m.src);
10970
9856
  const name = basename7(from);
10971
- if (!existsSync19(from)) {
9857
+ if (!existsSync17(from)) {
10972
9858
  report.push(
10973
9859
  `- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
10974
9860
  );
@@ -11166,13 +10052,13 @@ function reportText(inv, r) {
11166
10052
  }
11167
10053
  function writeScaffold(plan, outDir) {
11168
10054
  mkdirSync4(outDir, { recursive: true });
11169
- writeFileSync4(join21(outDir, "program.mjs"), plan.program);
10055
+ writeFileSync4(join19(outDir, "program.mjs"), plan.program);
11170
10056
  writeFileSync4(
11171
- join21(outDir, "doc.json"),
10057
+ join19(outDir, "doc.json"),
11172
10058
  JSON.stringify(plan.doc, null, 2) + "\n"
11173
10059
  );
11174
- writeFileSync4(join21(dirname7(outDir), "REPORT.md"), plan.report);
11175
- for (const [from, name] of plan.copies) copyFileSync(from, join21(outDir, name));
10060
+ writeFileSync4(join19(dirname6(outDir), "REPORT.md"), plan.report);
10061
+ for (const [from, name] of plan.copies) copyFileSync(from, join19(outDir, name));
11176
10062
  }
11177
10063
  function probeAudioDuration(file) {
11178
10064
  const res = spawnSync3(
@@ -11198,15 +10084,15 @@ async function inventory(argv) {
11198
10084
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
11199
10085
  const source = positionals[0] ?? ".";
11200
10086
  const r = createReporter(flags.json === true);
11201
- const outDir = resolve11(strFlag(flags, "out") ?? "port");
10087
+ const outDir = resolve9(strFlag(flags, "out") ?? "port");
11202
10088
  mkdirSync5(outDir, { recursive: true });
11203
10089
  const renderFlag = strFlag(flags, "render");
11204
10090
  const inv = await readInventory(source, {
11205
- render: renderFlag === void 0 ? void 0 : resolve11(renderFlag),
10091
+ render: renderFlag === void 0 ? void 0 : resolve9(renderFlag),
11206
10092
  outDir,
11207
10093
  log: (l) => r.log(`note: ${l}`)
11208
10094
  });
11209
- const file = join22(outDir, "inventory.json");
10095
+ const file = join20(outDir, "inventory.json");
11210
10096
  writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
11211
10097
  const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
11212
10098
  r.log(
@@ -11238,38 +10124,38 @@ function summary(inv) {
11238
10124
  async function scaffold(argv) {
11239
10125
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS8);
11240
10126
  const r = createReporter(flags.json === true);
11241
- const from = resolve11(strFlag(flags, "from") ?? join22("port", "inventory.json"));
11242
- if (!existsSync20(from))
10127
+ const from = resolve9(strFlag(flags, "from") ?? join20("port", "inventory.json"));
10128
+ if (!existsSync18(from))
11243
10129
  throw new UsageError(
11244
10130
  `no inventory at ${from}: run npx vos port inventory <source> first`
11245
10131
  );
11246
10132
  const inv = JSON.parse(readFileSync7(from, "utf8"));
11247
- const outDir = resolve11(
11248
- strFlag(flags, "out") ?? join22(dirname8(from), "program")
10133
+ const outDir = resolve9(
10134
+ strFlag(flags, "out") ?? join20(dirname7(from), "program")
11249
10135
  );
11250
10136
  const plan = planScaffold(inv, {
11251
10137
  sourceDir: scaffoldSourceDir(inv),
11252
10138
  probeDuration: probeAudioDuration
11253
10139
  });
11254
10140
  writeScaffold(plan, outDir);
11255
- const programFile = join22(outDir, "program.mjs");
10141
+ const programFile = join20(outDir, "program.mjs");
11256
10142
  const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
11257
10143
  const built = buildProgram(mod.default, "program.mjs", plan.program);
11258
10144
  for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
11259
10145
  if (built.problems.length) return EXIT_ERROR;
11260
10146
  writeFileSync5(
11261
- join22(outDir, "config.json"),
10147
+ join20(outDir, "config.json"),
11262
10148
  JSON.stringify(built.config, null, 2) + "\n"
11263
10149
  );
11264
10150
  r.done(
11265
- { program: outDir, report: join22(dirname8(outDir), "REPORT.md") },
11266
- `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join22(dirname8(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
10151
+ { program: outDir, report: join20(dirname7(outDir), "REPORT.md") },
10152
+ `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join20(dirname7(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
11267
10153
  );
11268
10154
  return EXIT_OK;
11269
10155
  }
11270
10156
  function scaffoldSourceDir(inv) {
11271
10157
  if (inv.root) return inv.root;
11272
- return inv.source.endsWith(".html") ? dirname8(resolve11(inv.source)) : resolve11(inv.source);
10158
+ return inv.source.endsWith(".html") ? dirname7(resolve9(inv.source)) : resolve9(inv.source);
11273
10159
  }
11274
10160
 
11275
10161
  // src/plugin/recipe.ts
@@ -11360,8 +10246,8 @@ async function cmdRecipe(argv) {
11360
10246
  }
11361
10247
 
11362
10248
  // src/plugin/brand.ts
11363
- import { writeFile as writeFile10 } from "fs/promises";
11364
- import { resolve as resolve12 } from "path";
10249
+ import { writeFile as writeFile9 } from "fs/promises";
10250
+ import { resolve as resolve10 } from "path";
11365
10251
  import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
11366
10252
  import { lookKindForGround } from "@vosjs/studio-core";
11367
10253
  var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
@@ -11649,7 +10535,7 @@ function renderBrandMd(c, claim) {
11649
10535
  "",
11650
10536
  "## Use",
11651
10537
  "",
11652
- `The poster family binds these roles: \`bgA/bgB/bgC\` for the ground, \`ink\` for type, \`fontDisplay\` for the headline (if it is not in the hosted catalog, \`vos check\` warns and the nearest hosted serif or sans stands in), \`logoUrl\` for the mark (placed beside the wordmark on the cards and the end card; \`logoOnDarkUrl\` stands in on a dark ground). A site that publishes only a favicon or an app icon gets the icon here, tile and all: publish a bare mark and name it in design.md. A take's frame ground is a gradient of \`accent\` toward \`bgC\` unless the brand is paper, in which case \`bgB\` toward \`bgC\`.`
10538
+ `The look, the titles and the end card bind these roles: \`bgA/bgB/bgC\` for the ground, \`ink\` for type, \`fontDisplay\` for the headline (if it is not in the hosted catalog, \`vos check\` warns and the nearest hosted serif or sans stands in), \`logoUrl\` for the mark (placed beside the wordmark on the end card; \`logoOnDarkUrl\` stands in on a dark ground). A site that publishes only a favicon or an app icon gets the icon here, tile and all: publish a bare mark and name it in design.md. A take's frame ground is a gradient of \`accent\` toward \`bgC\` unless the brand is paper, in which case \`bgB\` toward \`bgC\`.`
11653
10539
  ];
11654
10540
  if (c.avoid.length) {
11655
10541
  lines.push("", "## Avoid (the site says)", "");
@@ -11680,7 +10566,7 @@ async function cmdBrand(argv) {
11680
10566
  throw new UsageError(`not a URL: ${raw}`);
11681
10567
  }
11682
10568
  const r = createReporter(flags.json === true);
11683
- const out = resolve12(strFlag(flags, "out") ?? "BRAND.md");
10569
+ const out = resolve10(strFlag(flags, "out") ?? "BRAND.md");
11684
10570
  r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
11685
10571
  const [designText, llmsText] = await Promise.all([
11686
10572
  fetchText(`${origin}/design.md`),
@@ -11714,7 +10600,7 @@ async function cmdBrand(argv) {
11714
10600
  composition,
11715
10601
  llms?.claim ?? design?.description ?? null
11716
10602
  );
11717
- await writeFile10(out, md);
10603
+ await writeFile9(out, md);
11718
10604
  const k = composition.kit;
11719
10605
  r.done(
11720
10606
  {
@@ -11829,8 +10715,8 @@ async function recordHosted(input) {
11829
10715
  }
11830
10716
 
11831
10717
  // src/plugin/agentBrowser.ts
11832
- import { writeFile as writeFile11 } from "fs/promises";
11833
- import { resolve as resolve13 } from "path";
10718
+ import { writeFile as writeFile10 } from "fs/promises";
10719
+ import { resolve as resolve11 } from "path";
11834
10720
  function parseAgentBrowserLog(text) {
11835
10721
  const records = [];
11836
10722
  const problems = [];
@@ -12301,7 +11187,7 @@ async function cmdActions(argv) {
12301
11187
  );
12302
11188
  }
12303
11189
  const r = createReporter(flags.json === true);
12304
- const out = resolve13(strFlag(flags, "out") ?? "actions.json");
11190
+ const out = resolve11(strFlag(flags, "out") ?? "actions.json");
12305
11191
  const viewportFlag = strFlag(flags, "viewport");
12306
11192
  let viewport;
12307
11193
  if (viewportFlag) {
@@ -12309,8 +11195,8 @@ async function cmdActions(argv) {
12309
11195
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
12310
11196
  viewport = { width: Number(m[1]), height: Number(m[2]) };
12311
11197
  }
12312
- const { readFile: readFile16 } = await import("fs/promises");
12313
- const text = await readFile16(resolve13(input), "utf8");
11198
+ const { readFile: readFile13 } = await import("fs/promises");
11199
+ const text = await readFile13(resolve11(input), "utf8");
12314
11200
  const { records, problems } = parseAgentBrowserLog(text);
12315
11201
  for (const p of problems) r.log(` ${p}`);
12316
11202
  const result = convertAgentBrowser(records, {
@@ -12327,7 +11213,7 @@ async function cmdActions(argv) {
12327
11213
  );
12328
11214
  return EXIT_ERROR;
12329
11215
  }
12330
- await writeFile11(out, `${JSON.stringify(result.actions, null, 2)}
11216
+ await writeFile10(out, `${JSON.stringify(result.actions, null, 2)}
12331
11217
  `);
12332
11218
  r.done(
12333
11219
  { ok: true, out, url: result.actions.url ?? null, ...summary2(result) },
@@ -12347,9 +11233,9 @@ function summary2(result) {
12347
11233
  }
12348
11234
  async function cmdActionsScript(input, flags) {
12349
11235
  const r = createReporter(flags.json === true);
12350
- const { readFile: readFile16 } = await import("fs/promises");
11236
+ const { readFile: readFile13 } = await import("fs/promises");
12351
11237
  const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
12352
- const raw = JSON.parse(await readFile16(resolve13(input), "utf8"));
11238
+ const raw = JSON.parse(await readFile13(resolve11(input), "utf8"));
12353
11239
  const errors = validateActions(raw);
12354
11240
  if (errors.length) throw new UsageError(`${input}:
12355
11241
  ${errors.join("\n ")}`);
@@ -12359,9 +11245,9 @@ async function cmdActionsScript(input, flags) {
12359
11245
  }
12360
11246
 
12361
11247
  // src/plugin/ingest.ts
12362
- import { existsSync as existsSync21 } from "fs";
12363
- import { copyFile, mkdir as mkdir8, readFile as readFile14, stat as stat3 } from "fs/promises";
12364
- import { basename as basename9, join as join23, resolve as resolve14 } from "path";
11248
+ import { existsSync as existsSync19 } from "fs";
11249
+ import { copyFile, mkdir as mkdir7, readFile as readFile11, stat as stat2 } from "fs/promises";
11250
+ import { basename as basename9, join as join21, resolve as resolve12 } from "path";
12365
11251
  import { inflateRawSync } from "zlib";
12366
11252
  import {
12367
11253
  cursorFromPlaywrightTrace,
@@ -12449,7 +11335,7 @@ async function readTraceFile(path, offsetMs) {
12449
11335
  );
12450
11336
  }
12451
11337
  if (kind === "zip") {
12452
- const bytes = new Uint8Array(await readFile14(path));
11338
+ const bytes = new Uint8Array(await readFile11(path));
12453
11339
  const entry = playwrightTraceEntry(zipEntries(bytes));
12454
11340
  if (!entry) {
12455
11341
  throw new UsageError(
@@ -12462,7 +11348,7 @@ async function readTraceFile(path, offsetMs) {
12462
11348
  );
12463
11349
  return cursorFromPlaywrightTrace(text2, { offsetMs });
12464
11350
  }
12465
- const text = await readFile14(path, "utf8");
11351
+ const text = await readFile11(path, "utf8");
12466
11352
  return cursorFromTrace(basename9(path), text, { offsetMs });
12467
11353
  }
12468
11354
  function parseViewport(v) {
@@ -12478,10 +11364,10 @@ async function cmdIngest(argv) {
12478
11364
  throw new UsageError(
12479
11365
  "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]"
12480
11366
  );
12481
- const videoPath = resolve14(source);
12482
- if (!existsSync21(videoPath)) throw new UsageError(`${source}: no such file`);
11367
+ const videoPath = resolve12(source);
11368
+ if (!existsSync19(videoPath)) throw new UsageError(`${source}: no such file`);
12483
11369
  const r = createReporter(flags.json === true);
12484
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
11370
+ const outDir = resolve12(strFlag(flags, "out") ?? "take");
12485
11371
  const offsetMs = numFlag(flags, "offset", 0);
12486
11372
  const viewportFlag = parseViewport(strFlag(flags, "viewport"));
12487
11373
  const cursorPath = strFlag(flags, "cursor");
@@ -12503,18 +11389,18 @@ async function cmdIngest(argv) {
12503
11389
  }
12504
11390
  let trace = null;
12505
11391
  if (cursorPath) {
12506
- trace = await readTraceFile(resolve14(cursorPath), offsetMs);
11392
+ trace = await readTraceFile(resolve12(cursorPath), offsetMs);
12507
11393
  for (const n of trace.notes) r.log(`note: ${n}`);
12508
11394
  }
12509
- if (existsSync21(join23(outDir, "meta.json"))) {
11395
+ if (existsSync19(join21(outDir, "meta.json"))) {
12510
11396
  const { prevDoc, kept } = await prepareReRecord(outDir);
12511
11397
  r.log(
12512
11398
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
12513
11399
  );
12514
11400
  }
12515
- await mkdir8(outDir, { recursive: true });
11401
+ await mkdir7(outDir, { recursive: true });
12516
11402
  const paths = await ensureTakeDir(outDir);
12517
- const recording = join23(outDir, `recording.${probed.ext}`);
11403
+ const recording = join21(outDir, `recording.${probed.ext}`);
12518
11404
  r.log("copying the recording\u2026");
12519
11405
  const remuxed = await copyRecording(videoPath, recording, probed.ext);
12520
11406
  const viewport = viewportFlag ?? trace?.viewport ?? {
@@ -12522,7 +11408,7 @@ async function cmdIngest(argv) {
12522
11408
  height: probed.height
12523
11409
  };
12524
11410
  const events = trace?.events ?? [];
12525
- const fileStat = await stat3(videoPath);
11411
+ const fileStat = await stat2(videoPath);
12526
11412
  const meta = {
12527
11413
  dpr: 1,
12528
11414
  zoom: 1,
@@ -12582,19 +11468,19 @@ async function cmdIngest(argv) {
12582
11468
  import { spawnSync as spawnSync4 } from "child_process";
12583
11469
  import {
12584
11470
  cpSync,
12585
- existsSync as existsSync23,
11471
+ existsSync as existsSync21,
12586
11472
  mkdirSync as mkdirSync6,
12587
11473
  readFileSync as readFileSync10,
12588
11474
  rmSync as rmSync3,
12589
11475
  writeFileSync as writeFileSync6
12590
11476
  } from "fs";
12591
11477
  import { homedir as homedir2 } from "os";
12592
- import { join as join25 } from "path";
11478
+ import { join as join23 } from "path";
12593
11479
  import { chromium as chromium2 } from "playwright";
12594
11480
 
12595
11481
  // src/plugin/machine.ts
12596
- import { existsSync as existsSync22, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12597
- import { dirname as dirname9, join as join24 } from "path";
11482
+ import { existsSync as existsSync20, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
11483
+ import { dirname as dirname8, join as join22 } from "path";
12598
11484
  var AGENT_NAMES = [
12599
11485
  "claude",
12600
11486
  "cursor",
@@ -12614,17 +11500,17 @@ var HOME_DIRS = {
12614
11500
  copilot: [".copilot"]
12615
11501
  };
12616
11502
  function detectAgents(root, opts = {}) {
12617
- const exists = opts.exists ?? existsSync22;
11503
+ const exists = opts.exists ?? existsSync20;
12618
11504
  const out = [];
12619
11505
  for (const agent of AGENT_NAMES) {
12620
11506
  for (const rel of PROJECT_DIRS[agent]) {
12621
- const dir = join24(root, rel);
11507
+ const dir = join22(root, rel);
12622
11508
  if (exists(dir)) out.push({ agent, dir, scope: "project" });
12623
11509
  }
12624
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join24(root, ".github", "copilot-instructions.md"))) {
11510
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join22(root, ".github", "copilot-instructions.md"))) {
12625
11511
  out.push({
12626
11512
  agent,
12627
- dir: join24(root, ".github", "copilot"),
11513
+ dir: join22(root, ".github", "copilot"),
12628
11514
  scope: "project"
12629
11515
  });
12630
11516
  }
@@ -12632,7 +11518,7 @@ function detectAgents(root, opts = {}) {
12632
11518
  if (opts.global && opts.home) {
12633
11519
  for (const agent of AGENT_NAMES) {
12634
11520
  for (const rel of HOME_DIRS[agent]) {
12635
- const dir = join24(opts.home, rel);
11521
+ const dir = join22(opts.home, rel);
12636
11522
  if (exists(dir)) out.push({ agent, dir, scope: "global" });
12637
11523
  }
12638
11524
  }
@@ -12650,18 +11536,18 @@ function agentsFromFlag(value, root) {
12650
11536
  return {
12651
11537
  homes: wanted.map((agent) => ({
12652
11538
  agent,
12653
- dir: join24(root, PROJECT_DIRS[agent][0]),
11539
+ dir: join22(root, PROJECT_DIRS[agent][0]),
12654
11540
  scope: "project"
12655
11541
  }))
12656
11542
  };
12657
11543
  }
12658
11544
  function skillsDirFor(home) {
12659
- return join24(home.dir, "skills");
11545
+ return join22(home.dir, "skills");
12660
11546
  }
12661
11547
  function fallbackSkillsHome(root) {
12662
- return { agent: "codex", dir: join24(root, ".agents"), scope: "project" };
11548
+ return { agent: "codex", dir: join22(root, ".agents"), scope: "project" };
12663
11549
  }
12664
- function installedSkills(dir, exists = existsSync22, list = (p) => {
11550
+ function installedSkills(dir, exists = existsSync20, list = (p) => {
12665
11551
  try {
12666
11552
  return readdirSync5(p);
12667
11553
  } catch {
@@ -12669,7 +11555,7 @@ function installedSkills(dir, exists = existsSync22, list = (p) => {
12669
11555
  }
12670
11556
  }) {
12671
11557
  if (!exists(dir)) return [];
12672
- return list(dir).filter((name) => exists(join24(dir, name, "SKILL.md"))).sort();
11558
+ return list(dir).filter((name) => exists(join22(dir, name, "SKILL.md"))).sort();
12673
11559
  }
12674
11560
  var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
12675
11561
  var RULES_END = "<!-- vos:end -->";
@@ -12702,10 +11588,10 @@ ${RULES_BLOCK}
12702
11588
  }
12703
11589
  return { text, changed: text !== current };
12704
11590
  }
12705
- function rulesFileFor(root, exists = existsSync22) {
12706
- const agents = join24(root, "AGENTS.md");
11591
+ function rulesFileFor(root, exists = existsSync20) {
11592
+ const agents = join22(root, "AGENTS.md");
12707
11593
  if (exists(agents)) return agents;
12708
- const claude = join24(root, "CLAUDE.md");
11594
+ const claude = join22(root, "CLAUDE.md");
12709
11595
  if (exists(claude)) return claude;
12710
11596
  return agents;
12711
11597
  }
@@ -12714,14 +11600,14 @@ function chromeCandidates(platform, env) {
12714
11600
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
12715
11601
  if (platform === "win32")
12716
11602
  return [
12717
- join24(
11603
+ join22(
12718
11604
  env["PROGRAMFILES"] ?? "C:\\Program Files",
12719
11605
  "Google",
12720
11606
  "Chrome",
12721
11607
  "Application",
12722
11608
  "chrome.exe"
12723
11609
  ),
12724
- join24(
11610
+ join22(
12725
11611
  env["LOCALAPPDATA"] ?? "",
12726
11612
  "Google",
12727
11613
  "Chrome",
@@ -12806,20 +11692,20 @@ function doctorReport(facts) {
12806
11692
  return { ok: nodeOk && browserOk, lines, next_step, facts };
12807
11693
  }
12808
11694
  function bundledSkillsDir(from) {
12809
- let dir = dirname9(from);
11695
+ let dir = dirname8(from);
12810
11696
  for (let i = 0; i < 6; i++) {
12811
- const pkg = join24(dir, "package.json");
12812
- if (existsSync22(pkg)) {
11697
+ const pkg = join22(dir, "package.json");
11698
+ if (existsSync20(pkg)) {
12813
11699
  try {
12814
11700
  const name = JSON.parse(readFileSync9(pkg, "utf8")).name;
12815
11701
  if (name === "@vosjs/cli") {
12816
- const skills = join24(dir, "skills");
12817
- return existsSync22(skills) && statSync7(skills).isDirectory() ? skills : null;
11702
+ const skills = join22(dir, "skills");
11703
+ return existsSync20(skills) && statSync7(skills).isDirectory() ? skills : null;
12818
11704
  }
12819
11705
  } catch {
12820
11706
  }
12821
11707
  }
12822
- const up = dirname9(dir);
11708
+ const up = dirname8(dir);
12823
11709
  if (up === dir) break;
12824
11710
  dir = up;
12825
11711
  }
@@ -12845,7 +11731,7 @@ function npx() {
12845
11731
  function bundledChromium() {
12846
11732
  try {
12847
11733
  const p = chromium2.executablePath();
12848
- return p && existsSync23(p) ? p : null;
11734
+ return p && existsSync21(p) ? p : null;
12849
11735
  } catch {
12850
11736
  return null;
12851
11737
  }
@@ -12854,7 +11740,7 @@ function probe2() {
12854
11740
  return probeBrowser({
12855
11741
  platform: process.platform,
12856
11742
  env: process.env,
12857
- exists: existsSync23,
11743
+ exists: existsSync21,
12858
11744
  bundled: bundledChromium
12859
11745
  });
12860
11746
  }
@@ -12958,7 +11844,7 @@ async function cmdSetup(argv) {
12958
11844
  const dest = skillsDirFor(home);
12959
11845
  mkdirSync6(dest, { recursive: true });
12960
11846
  for (const name of installedSkills(bundled)) {
12961
- cpSync(join25(bundled, name), join25(dest, name), { recursive: true });
11847
+ cpSync(join23(bundled, name), join23(dest, name), { recursive: true });
12962
11848
  }
12963
11849
  }
12964
11850
  skillsVia = "bundled";
@@ -12993,7 +11879,7 @@ async function cmdSetup(argv) {
12993
11879
  let rules = null;
12994
11880
  if (!noRules) {
12995
11881
  const file = rulesFileFor(root);
12996
- const existing = existsSync23(file) ? readFileSync10(file, "utf8") : null;
11882
+ const existing = existsSync21(file) ? readFileSync10(file, "utf8") : null;
12997
11883
  const { text, changed } = applyRulesBlock(existing);
12998
11884
  if (changed) writeFileSync6(file, text);
12999
11885
  rules = { file, changed };
@@ -13107,7 +11993,7 @@ async function cmdWhoami(argv) {
13107
11993
  function cmdLogout(argv) {
13108
11994
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
13109
11995
  const r = reporterFor(flags);
13110
- const had = existsSync23(CREDENTIALS_PATH);
11996
+ const had = existsSync21(CREDENTIALS_PATH);
13111
11997
  rmSync3(CREDENTIALS_PATH, { force: true });
13112
11998
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
13113
11999
  r.done(
@@ -13139,7 +12025,6 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
13139
12025
  "media",
13140
12026
  "print",
13141
12027
  "yes",
13142
- "composed",
13143
12028
  "check",
13144
12029
  "motion",
13145
12030
  "leader",
@@ -13161,13 +12046,12 @@ Take pipeline
13161
12046
  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]
13162
12047
  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]
13163
12048
  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]
13164
- vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--launch LAUNCH.md] [--look plate|gradient|dark|none] [--brand BRAND.md] [--composed] [--set path=value] [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--parallel N] [--json]
12049
+ 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]
13165
12050
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
13166
12051
  vos brand <url> [--out BRAND.md] [--json]
13167
12052
  vos open <take> [--studio <url>] [--print]
13168
12053
  vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex (the mark and leader; the kicker is --accent)] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px <n> (the body size on the delivered frame)] [--id x] [--print] [--json]
13169
12054
  vos validate <actions.json|take|kit.json> [--picture] [--json]
13170
- vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
13171
12055
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
13172
12056
  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]
13173
12057
  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
@@ -13329,47 +12213,28 @@ recording cap, read live from GET /api/limits (30 min when the origin is
13329
12213
  unreachable): the capture stops there and says so in one line.
13330
12214
  frames captures PNG stills at OUTPUT times (seconds or % of duration);
13331
12215
  --at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
13332
- the agent's eyes AND exact-size posters/screenshots (--size WxH). render
12216
+ the agent's eyes (--size WxH for an exact size). render
13333
12217
  --range a..b --draft spot-checks a doc edit in seconds (half res, low
13334
12218
  bitrate; range in OUTPUT seconds); a --range render keeps its audio.
13335
- deliver renders the take to a release's destinations in one pass \u2014 the
13336
- channel specs (schema/channel-specs.json, verified sizes for the Chrome
13337
- Web Store, Product Hunt, X, LinkedIn, OG, GitHub, YouTube) drive stills at
13338
- exact pixels and video cuts, every artifact is VERIFIED against its spec
13339
- (px, bytes, duration; misses land in skipped[] with the reason), and
13340
- kit.json beside the assets is the manifest. Still times come from the
13341
- STORY: every step's end plus a 0.4 s settle (the response, not the
13342
- travel), then the zoom apexes, then an even spread; each candidate is
13343
- read once as the real page, blank ones (a wallpaper, an empty canvas) are
13344
- dropped and two of one frame collapse to one, with every drop said in
13345
- skipped[]. --times overrides with seconds, percents or step:<id>[+offset]
13346
- (the id from actions.json); --range cuts every video destination.
13347
- The LOOK presents the card: card-genre stills with no poster document
13348
- and every video cut sit on a ground (a cream plate, the house gradient, a
13349
- dark plate with a light streak) at ~84% of the width with headroom, a
13350
- soft ambient shadow plus a tight contact shadow, and a hairline when card
13351
- and ground are both light; a wide frame runs the card off the bottom.
13352
- --look picks a house look (or none for the pre-look crops); with no flag
13353
- the BRAND.md beside the take (or --brand <file>) decides from its look
13354
- role or its own ground, and with no brand the house gradient.
13355
- Screenshot-genre stills never take a look.
13356
- A POSTER is a document, never a template: a plain take whose card sits
13357
- where the poster wants it (frame.inset), leans (the tilt track), carries
13358
- the words and the mark as clips (ids stage-title, stage-kicker,
13359
- stage-brand, stage-mark) and ends on a trailing freeze whose start is the
13360
- still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
13361
- CWS tile + marquee, GitHub social preview) render from the poster
13362
- document of their aspect CLASS (landscape, square, portrait, tile), found
13363
- beside the take as poster/<class>/doc.json (poster/doc.json serves every
13364
- class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
13365
- the document's rest; kit.json records source poster, the class, the file,
13366
- the vos it tracks, the shot rect and the text boxes read FROM the
13367
- document. A class with no document is the take's own frame, said once.
13368
- deliver renders and verifies; it composes nothing. The composition is
13369
- made with plan --style <poster>, which copies a poster's layout onto a
13370
- take (its card placement, its stage clips with the release's words
13371
- patched in from LAUNCH.md's headline and kicker roles or --headline and
13372
- --kicker, its rest lean, its freeze), or by hand in doc.json.
12219
+ deliver renders the take to a release's video destinations in one pass \u2014
12220
+ the channel specs (schema/channel-specs.json, verified sizes for a YouTube
12221
+ demo, an X feed cut, a 9:16 cut for Shorts and LinkedIn, a GitHub README
12222
+ loop) drive each cut at exact pixels, every artifact is VERIFIED against
12223
+ its spec (px, bytes, duration; misses land in skipped[] with the reason),
12224
+ and kit.json beside the assets is the manifest. --range cuts every
12225
+ destination. A channel with no video destination is refused in words.
12226
+ The LOOK presents the card: every cut sits on a ground (a cream plate,
12227
+ the house gradient, a dark plate with a light streak) at ~84% of the width
12228
+ with headroom, a soft ambient shadow plus a tight contact shadow, and a
12229
+ hairline when card and ground are both light; a wide frame runs the card
12230
+ off the bottom. --look picks a house look (or none for the doc's own
12231
+ frame); with no flag the BRAND.md beside the take (or --brand <file>)
12232
+ decides from its look role or its own ground, and with no brand the house
12233
+ gradient.
12234
+ plan --style <doc.json|vosId> copies a signed-off take's layout onto this
12235
+ one (its card placement, its stage clips with the release's words patched
12236
+ in from LAUNCH.md's headline and kicker roles or --headline and --kicker,
12237
+ its rest lean, its freeze), or edit doc.json by hand.
13373
12238
  The cut's MOTION is the document's too, in ONE vocabulary: every visual
13374
12239
  thing (the card, a text, image or video clip, a prop) carries anim.enter,
13375
12240
  anim.exit and anim.idle; the output lasts until the last clip ends, and
@@ -13393,40 +12258,21 @@ deleted on a refresh. deliver applies each destination's MECHANICS and
13393
12258
  nothing more: the README loop drops the card's motion, every clip a
13394
12259
  template placed and every sound; a channel that autoplays muted drops the
13395
12260
  bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
13396
- the camera. Screenshot-genre
13397
- destinations (CWS screenshots, the PH gallery) are the real page at that
13398
- moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
13399
- policy); --composed keeps the cut's camera and chrome instead. --set
12261
+ the camera. --set
13400
12262
  path=value overrides the doc in memory for every render here (the user's
13401
- sets apply last). Store uploads stay manual: hand the human the kit
13402
- directory, then vos validate <kit.json> re-measures every asset from its
13403
- bytes against the channel specs; --picture adds what each asset LOOKS
13404
- like, read from its pixels: blank (a wallpaper or an empty canvas where
13405
- the product should be), duplicate (two stills of one frame), subject (the
13406
- card off the 60 to 92% band, or a crop where a card was asked for),
13407
- separation (a light card on a light ground with no shadow), halfsize (a
13408
- tile that loses its edges when the store shrinks it), and, where the kit
13409
- records its text boxes, sliced, safe and contrast (APCA Lc 60/75); a
13410
- video's first and last frames are read through ffmpeg. Every finding
13411
- carries a code, a severity, a fix hint and a box; an error fails the
13412
- verdict beside the spec problems.
13413
- judge puts the kit beside its REFERENCES: for every still that has a
13414
- reference of its role in the manifest (a template family, a card genre),
13415
- two sheets at a common height (the asset left, then right, so order
13416
- cannot bias the call) and the rubric in words, plus judge.json, a slot
13417
- per pair the judge fills (win true, false or null for a tie, with the
13418
- rule numbers). No model runs inside the verb: the skill judges the
13419
- sheets pairwise, both orders, and the verb reports the win rate beside
13420
- the spec and picture counts. The reference set is the maker's own
13421
- fixture folder with a MANIFEST.json (id, file, role, layout, facts,
13422
- rule per asset).
12263
+ sets apply last). Uploads stay manual: hand the human the kit
12264
+ directory, then vos validate <kit.json> re-measures every cut from its
12265
+ bytes against the channel specs; --picture adds what each cut LOOKS like:
12266
+ its first and last frames, read through ffmpeg, must not be blank or bled
12267
+ on all four sides. Every finding carries a code, a severity and a fix
12268
+ hint; an error fails the verdict beside the spec problems.
13423
12269
  brand writes the product's BRAND.md, witnessed: it reads /design.md when
13424
12270
  the site publishes one (the convention beside /llms.txt: fonts, logo assets,
13425
12271
  an avoid list), /llms.txt for the name and the claim, then the page itself
13426
12272
  in a browser (the body ground, the surfaces, the h1 and body faces and inks,
13427
12273
  the accent buttons and links agree on, theme-color, the icons, og:image).
13428
- The frontmatter carries the roles the poster family binds (bgA/bgB/bgC,
13429
- ink, accent, fontDisplay, logoUrl); the prose says where every value came
12274
+ The frontmatter carries the brand's roles (bgA/bgB/bgC, ink, accent,
12275
+ fontDisplay, logoUrl); the prose says where every value came
13430
12276
  from and quotes the site's own avoid list. Resolve the brand BEFORE any
13431
12277
  asset is authored; file it with vos recipe push BRAND.md --folder <slug>.
13432
12278
  digest is how an agent SEES a recording before it cuts: writes digest/ with
@@ -13482,7 +12328,7 @@ lint-gated, so a bad override fails like a bad doc.json):
13482
12328
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
13483
12329
  `;
13484
12330
  async function loadActions(file) {
13485
- const raw = JSON.parse(await readFile15(file, "utf8"));
12331
+ const raw = JSON.parse(await readFile12(file, "utf8"));
13486
12332
  const errors = validateActions(raw);
13487
12333
  if (errors.length)
13488
12334
  throw new UsageError(`invalid actions file:
@@ -13544,8 +12390,8 @@ function takeBrowserArgs(multi) {
13544
12390
  function takeStorageState(flags) {
13545
12391
  const raw = strFlag(flags, "storage-state");
13546
12392
  if (!raw) return void 0;
13547
- const path = resolve15(raw);
13548
- if (!existsSync24(path))
12393
+ const path = resolve13(raw);
12394
+ if (!existsSync22(path))
13549
12395
  throw new UsageError(`--storage-state: no such file: ${path}`);
13550
12396
  return path;
13551
12397
  }
@@ -13558,7 +12404,7 @@ function takeSessionName(flags, storageState) {
13558
12404
  throw new UsageError(
13559
12405
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
13560
12406
  );
13561
- if (!existsSync24(sessionDir(name)))
12407
+ if (!existsSync22(sessionDir(name)))
13562
12408
  throw new UsageError(
13563
12409
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
13564
12410
  );
@@ -13627,7 +12473,7 @@ async function cmdRecord(argv) {
13627
12473
  const url = strFlag(flags, "url") ?? actions.url;
13628
12474
  if (!url)
13629
12475
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13630
- const outDir = resolve15(strFlag(flags, "out") ?? "take");
12476
+ const outDir = resolve13(strFlag(flags, "out") ?? "take");
13631
12477
  const storageState = takeStorageState(flags);
13632
12478
  const sessionName = takeSessionName(flags, storageState);
13633
12479
  const browserArgs = takeBrowserArgs(multi);
@@ -13781,13 +12627,13 @@ ${lines.join("\n")}
13781
12627
  }
13782
12628
  const paths = takePaths(outDir);
13783
12629
  const prepare = async () => {
13784
- if (existsSync24(join26(outDir, "meta.json"))) {
12630
+ if (existsSync22(join24(outDir, "meta.json"))) {
13785
12631
  const { prevDoc, kept } = await prepareReRecord(outDir);
13786
12632
  r.log(
13787
12633
  `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" : "")
13788
12634
  );
13789
12635
  }
13790
- await mkdir9(outDir, { recursive: true });
12636
+ await mkdir8(outDir, { recursive: true });
13791
12637
  await ensureTakeDir(outDir);
13792
12638
  };
13793
12639
  const browser = await launchBrowser(browserArgs);
@@ -13846,7 +12692,7 @@ ${lines.join("\n")}
13846
12692
  },
13847
12693
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
13848
12694
  ${(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}
13849
- Next: edit ${join26(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
12695
+ Next: edit ${join24(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
13850
12696
  );
13851
12697
  return strictFail ? EXIT_USAGE : EXIT_OK;
13852
12698
  } finally {
@@ -13870,9 +12716,9 @@ async function cmdCreate2(argv) {
13870
12716
  const url = strFlag(flags, "url") ?? actions.url;
13871
12717
  if (!url)
13872
12718
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13873
- const outDir = resolve15(strFlag(flags, "out") ?? "take");
12719
+ const outDir = resolve13(strFlag(flags, "out") ?? "take");
13874
12720
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
13875
- const out = positionals[0] ?? join26(outDir, `out.${format}`);
12721
+ const out = positionals[0] ?? join24(outDir, `out.${format}`);
13876
12722
  const parallel = numFlag(flags, "parallel", 1);
13877
12723
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13878
12724
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13885,13 +12731,13 @@ async function cmdCreate2(argv) {
13885
12731
  const headers = takeHeaders(multi);
13886
12732
  const paths = takePaths(outDir);
13887
12733
  const prepare = async () => {
13888
- if (existsSync24(join26(outDir, "meta.json"))) {
12734
+ if (existsSync22(join24(outDir, "meta.json"))) {
13889
12735
  const { prevDoc, kept } = await prepareReRecord(outDir);
13890
12736
  r.log(
13891
12737
  `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" : "")
13892
12738
  );
13893
12739
  }
13894
- await mkdir9(outDir, { recursive: true });
12740
+ await mkdir8(outDir, { recursive: true });
13895
12741
  await ensureTakeDir(outDir);
13896
12742
  };
13897
12743
  const browser = await launchBrowser(browserArgs);
@@ -13992,24 +12838,24 @@ async function cmdPlan(argv) {
13992
12838
  );
13993
12839
  const r = createReporter(flags.json === true);
13994
12840
  if (flags.fresh === true) {
13995
- await rm3(join26(dir, "doc.json"), { force: true });
12841
+ await rm2(join24(dir, "doc.json"), { force: true });
13996
12842
  r.log("note: --fresh discarded the existing doc.json");
13997
12843
  }
13998
12844
  let reuse;
13999
12845
  if (flags.reuse === true) {
14000
- const from = resolve15(strFlag(flags, "from") ?? join26(dir, PREV_DOC_NAME));
14001
- if (!existsSync24(from)) {
12846
+ const from = resolve13(strFlag(flags, "from") ?? join24(dir, PREV_DOC_NAME));
12847
+ if (!existsSync22(from)) {
14002
12848
  throw new UsageError(
14003
12849
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
14004
12850
  );
14005
12851
  }
14006
12852
  reuse = {
14007
12853
  from,
14008
- doc: JSON.parse(await readFile15(from, "utf8"))
12854
+ doc: JSON.parse(await readFile12(from, "utf8"))
14009
12855
  };
14010
12856
  }
14011
12857
  const style = await resolveStyleRef(flags);
14012
- const hasDoc = existsSync24(join26(dir, "doc.json"));
12858
+ const hasDoc = existsSync22(join24(dir, "doc.json"));
14013
12859
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
14014
12860
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
14015
12861
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -14055,7 +12901,7 @@ async function cmdPlan(argv) {
14055
12901
  ${s.reuse.flagged.join("\n ")}` : "") : "";
14056
12902
  r.done(
14057
12903
  {
14058
- take: resolve15(dir),
12904
+ take: resolve13(dir),
14059
12905
  fresh: s.fresh,
14060
12906
  cursorKept: s.cursorKept,
14061
12907
  zoomAuto: s.zoomAuto,
@@ -14067,7 +12913,7 @@ async function cmdPlan(argv) {
14067
12913
  ...s.motion ? { motion: s.motion } : {},
14068
12914
  ...s.reuse ? { reuse: s.reuse } : {}
14069
12915
  },
14070
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join26(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
12916
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join24(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
14071
12917
  );
14072
12918
  return EXIT_OK;
14073
12919
  }
@@ -14190,7 +13036,7 @@ async function cmdRender(argv) {
14190
13036
  );
14191
13037
  const r = createReporter(flags.json === true);
14192
13038
  const format = videoFormat(strFlag(flags, "format"), positionals[1]);
14193
- const out = positionals[1] ?? join26(dir, `out.${format}`);
13039
+ const out = positionals[1] ?? join24(dir, `out.${format}`);
14194
13040
  const parallel = numFlag(flags, "parallel", 1);
14195
13041
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
14196
13042
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -14328,7 +13174,7 @@ async function cmdDeliver(argv) {
14328
13174
  const toRaw = strFlag(flags, "to");
14329
13175
  if (!dir || !toRaw)
14330
13176
  throw new UsageError(
14331
- "vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--json]\n(hosted work: vos pull <id> --media first \u2014 deliver runs on a local take)"
13177
+ "vos deliver <take> --to youtube,x,shorts,github (or all) [--release v2.1] [--out dir] [--range a..b] [--json]\n(hosted work: vos pull <id> --media first \u2014 deliver runs on a local take)"
14332
13178
  );
14333
13179
  const r = createReporter(flags.json === true);
14334
13180
  if (!isTakeDir(dir))
@@ -14355,17 +13201,6 @@ async function cmdDeliver(argv) {
14355
13201
  }
14356
13202
  const take = await loadTake(dir);
14357
13203
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
14358
- const duration = docOutputDuration3(take.doc);
14359
- const timesRaw = strFlag(flags, "times");
14360
- let times;
14361
- if (timesRaw !== void 0) {
14362
- try {
14363
- const doc = take.doc;
14364
- times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes2(s, duration)[0]);
14365
- } catch (e) {
14366
- throw new UsageError(e instanceof Error ? e.message : String(e));
14367
- }
14368
- }
14369
13204
  const parallel = numFlag(flags, "parallel", 1);
14370
13205
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
14371
13206
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -14380,20 +13215,15 @@ async function cmdDeliver(argv) {
14380
13215
  throw new UsageError(e instanceof Error ? e.message : String(e));
14381
13216
  }
14382
13217
  r.log(`look: ${lookPick.from}`);
14383
- const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
14384
- if (launch) r.log(`launch: ${launch.file}`);
14385
13218
  const browser = await launchBrowser();
14386
13219
  try {
14387
13220
  const result = await deliverTake(browser, dir, {
14388
13221
  look: lookPick.look,
14389
- launchRoles: launch?.roles ?? null,
14390
13222
  channels,
14391
13223
  outDir: strFlag(flags, "out"),
14392
13224
  release: strFlag(flags, "release"),
14393
- times,
14394
13225
  range,
14395
13226
  parallel,
14396
- composed: hasFlag(flags, "composed"),
14397
13227
  overrides: {
14398
13228
  set: multi.set,
14399
13229
  background: strFlag(flags, "background"),
@@ -14410,7 +13240,7 @@ async function cmdDeliver(argv) {
14410
13240
  });
14411
13241
  const { kit } = result;
14412
13242
  const assetLines = kit.assets.map(
14413
- (a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB${a.seconds !== null ? `, ${a.seconds.toFixed(1)}s` : ""}${a.frameTime !== null ? `, frame ${a.frameTime.toFixed(2)}s` : ""}${a.poster ? `, poster ${a.poster.class}` : ""})`
13243
+ (a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB, ${a.seconds.toFixed(1)}s)`
14414
13244
  );
14415
13245
  const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
14416
13246
  r.done(
@@ -14423,7 +13253,7 @@ async function cmdDeliver(argv) {
14423
13253
  `Wrote ${kit.assets.length} asset(s) + kit.json to ${result.outDir}` + (assetLines.length ? `
14424
13254
  ${assetLines.join("\n ")}` : "") + (skippedLines.length ? `
14425
13255
  ${skippedLines.join("\n ")}` : "") + `
14426
- Store uploads stay manual: hand the human this directory and the manifest.`
13256
+ Uploads stay manual: hand the human this directory and the manifest.`
14427
13257
  );
14428
13258
  return kit.assets.length > 0 ? EXIT_OK : EXIT_ERROR;
14429
13259
  } finally {
@@ -14437,15 +13267,15 @@ async function resolveStyleRef(flags) {
14437
13267
  }
14438
13268
  async function resolveDocRef(ref, flags, what) {
14439
13269
  const styleRef = ref;
14440
- const file = existsSync24(styleRef) ? resolve15(
13270
+ const file = existsSync22(styleRef) ? resolve13(
14441
13271
  styleRef,
14442
- existsSync24(join26(styleRef, "doc.json")) ? "doc.json" : ""
13272
+ existsSync22(join24(styleRef, "doc.json")) ? "doc.json" : ""
14443
13273
  ) : null;
14444
- if (file && existsSync24(file)) {
13274
+ if (file && existsSync22(file)) {
14445
13275
  return {
14446
13276
  from: file,
14447
- doc: migrateHostedDoc5(
14448
- JSON.parse(await readFile15(file, "utf8"))
13277
+ doc: migrateHostedDoc4(
13278
+ JSON.parse(await readFile12(file, "utf8"))
14449
13279
  )
14450
13280
  };
14451
13281
  }
@@ -14473,7 +13303,7 @@ async function resolveDocRef(ref, flags, what) {
14473
13303
  throw new UsageError(`${what}: ${styleRef} carries no doc (is it a take?)`);
14474
13304
  return {
14475
13305
  from: `${origin}/vos/${styleRef}`,
14476
- doc: migrateHostedDoc5(doc.body)
13306
+ doc: migrateHostedDoc4(doc.body)
14477
13307
  };
14478
13308
  }
14479
13309
  async function officialTemplate(origin, ref) {
@@ -14497,7 +13327,7 @@ async function cmdDigest(argv) {
14497
13327
  const take = await loadTake(dir);
14498
13328
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
14499
13329
  const transcriptPath = strFlag(flags, "transcript");
14500
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
13330
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile12(transcriptPath, "utf8"))) : null;
14501
13331
  const style = await resolveStyleRef(flags);
14502
13332
  const noFrames = flags["no-frames"] === true;
14503
13333
  let browser = null;
@@ -14528,7 +13358,7 @@ async function cmdDigest(argv) {
14528
13358
  kinds,
14529
13359
  frames: d.images.full,
14530
13360
  crops: d.images.crop,
14531
- sheet: d.images.sheet ? join26(result.outDir, d.images.sheet) : null,
13361
+ sheet: d.images.sheet ? join24(result.outDir, d.images.sheet) : null,
14532
13362
  scenes: kinds.scene ?? 0,
14533
13363
  sourceDuration: d.take.sourceDuration,
14534
13364
  outputDuration: d.take.outputDuration,
@@ -14556,14 +13386,14 @@ async function cmdOpen(argv) {
14556
13386
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
14557
13387
  const r = createReporter(flags.json === true);
14558
13388
  const take = await loadTake(dir);
14559
- if (!existsSync24(take.paths.recording)) {
13389
+ if (!existsSync22(take.paths.recording)) {
14560
13390
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
14561
13391
  }
14562
13392
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
14563
13393
  /\/+$/,
14564
13394
  ""
14565
13395
  );
14566
- const server = await startTakeServer(resolve15(dir), {});
13396
+ const server = await startTakeServer(resolve13(dir), {});
14567
13397
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
14568
13398
  r.event({ event: "open", server: server.base, url });
14569
13399
  r.log(`take served at ${server.base}`);
@@ -14588,35 +13418,6 @@ async function cmdOpen(argv) {
14588
13418
  });
14589
13419
  return EXIT_OK;
14590
13420
  }
14591
- async function cmdJudge(argv) {
14592
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
14593
- const target = positionals[0];
14594
- const against = strFlag(flags, "against");
14595
- if (!target || !against)
14596
- throw new UsageError(
14597
- "vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
14598
- );
14599
- const r = createReporter(flags.json === true);
14600
- const kitFile = target.endsWith("kit.json") ? target : join26(target, "kit.json");
14601
- if (!existsSync24(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
14602
- if (!existsSync24(against))
14603
- throw new UsageError(`${against}: no reference manifest`);
14604
- const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
14605
- const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
14606
- const rate = winRate(verdicts);
14607
- const lines = result.sheets.map(
14608
- (s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
14609
- );
14610
- r.done(
14611
- { ...result, winRate: rate },
14612
- `Wrote ${result.sheets.length} sheet pair(s) to ${result.outDir}` + (lines.length ? `
14613
- ${lines.join("\n ")}` : "") + (result.skipped.length ? `
14614
- skipped: ${result.skipped.join("\n skipped: ")}` : "") + `
14615
- Judge each pair both ways (the rubric is beside it) and fill ${result.verdictFile}` + (rate.judged ? `
14616
- Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judged (${(rate.rate * 100).toFixed(0)}%; a tie counts a half); parity with the references is 50%, the marketability bar is 40%` : "")
14617
- );
14618
- return EXIT_OK;
14619
- }
14620
13421
  async function cmdCallout(argv) {
14621
13422
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
14622
13423
  const dir = positionals[0];
@@ -14642,7 +13443,7 @@ async function cmdCallout(argv) {
14642
13443
  `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`
14643
13444
  );
14644
13445
  const codeFile = strFlag(flags, "code-file");
14645
- const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
13446
+ const code = codeFile ? await readFile12(codeFile, "utf8") : strFlag(flags, "code");
14646
13447
  const words2 = {
14647
13448
  kicker: strFlag(flags, "kicker"),
14648
13449
  title: strFlag(flags, "title"),
@@ -14721,7 +13522,7 @@ async function cmdValidate(argv) {
14721
13522
  const target = positionals[0];
14722
13523
  if (!target) throw new UsageError("vos validate <actions.json|take>");
14723
13524
  const r = createReporter(flags.json === true);
14724
- const kitTarget = target.endsWith("kit.json") ? target : existsSync24(join26(target, "kit.json")) && !isTakeDir(target) ? join26(target, "kit.json") : null;
13525
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync22(join24(target, "kit.json")) && !isTakeDir(target) ? join24(target, "kit.json") : null;
14725
13526
  if (kitTarget) {
14726
13527
  const picture = flags.picture === true;
14727
13528
  const verdict = await validateKit(kitTarget, { picture });
@@ -14753,9 +13554,9 @@ async function cmdValidate(argv) {
14753
13554
  r.done({ valid: true, target }, `${target}: valid actions file`);
14754
13555
  return EXIT_OK;
14755
13556
  }
14756
- if (!isTakeDir(target) && existsSync24(join26(target, "config.json"))) {
13557
+ if (!isTakeDir(target) && existsSync22(join24(target, "config.json"))) {
14757
13558
  const parsed = JSON.parse(
14758
- await readFile15(join26(target, "config.json"), "utf8")
13559
+ await readFile12(join24(target, "config.json"), "utf8")
14759
13560
  );
14760
13561
  const pre = preflightConfig(parsed);
14761
13562
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -14796,7 +13597,7 @@ async function cmdValidate(argv) {
14796
13597
  const take = await loadTake(target);
14797
13598
  const problems = [];
14798
13599
  const warnings = [];
14799
- if (!existsSync24(take.paths.recording)) {
13600
+ if (!existsSync22(take.paths.recording)) {
14800
13601
  problems.push("missing recording (re-run record)");
14801
13602
  } else {
14802
13603
  const mismatch = containerMismatch(
@@ -14889,7 +13690,7 @@ async function cmdPush3(argv) {
14889
13690
  );
14890
13691
  const refusal = takePushRefusal({
14891
13692
  vos: strFlag(flags, "vos"),
14892
- trackedVosId: readSyncState(resolve15(dir))?.vosId ?? null,
13693
+ trackedVosId: readSyncState(resolve13(dir))?.vosId ?? null,
14893
13694
  programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
14894
13695
  (name) => hasFlag(flags, name)
14895
13696
  )
@@ -14905,7 +13706,7 @@ async function cmdPush3(argv) {
14905
13706
  `--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
14906
13707
  );
14907
13708
  const claim = await pushTakeClaimable(
14908
- resolve15(dir),
13709
+ resolve13(dir),
14909
13710
  {
14910
13711
  api: strFlag(flags, "api"),
14911
13712
  origin: strFlag(flags, "origin"),
@@ -14927,7 +13728,7 @@ reference and the only credential.`
14927
13728
  }
14928
13729
  const overrides = multi.override;
14929
13730
  const result = await pushTake(
14930
- resolve15(dir),
13731
+ resolve13(dir),
14931
13732
  {
14932
13733
  key: strFlag(flags, "key"),
14933
13734
  api: strFlag(flags, "api"),
@@ -14981,7 +13782,7 @@ async function cmdPull2(argv) {
14981
13782
  const dir = positionals[0] ?? ".";
14982
13783
  const r = createReporter(flags.json === true);
14983
13784
  const result = await pullTake(
14984
- resolve15(dir),
13785
+ resolve13(dir),
14985
13786
  {
14986
13787
  key: strFlag(flags, "key"),
14987
13788
  api: strFlag(flags, "api"),
@@ -15056,8 +13857,6 @@ async function run(argv) {
15056
13857
  return await cmdValidate(rest);
15057
13858
  case "callout":
15058
13859
  return await cmdCallout(rest);
15059
- case "judge":
15060
- return await cmdJudge(rest);
15061
13860
  case "fetch":
15062
13861
  return await cmdFetch(rest);
15063
13862
  case "duplicate":
@@ -15183,7 +13982,7 @@ async function cmdSession(argv) {
15183
13982
  if (!arg) throw new UsageError(usage);
15184
13983
  const bad = sessionNameVerdict(arg);
15185
13984
  if (bad) throw new UsageError(bad);
15186
- if (!existsSync24(sessionDir(arg)))
13985
+ if (!existsSync22(sessionDir(arg)))
15187
13986
  throw new UsageError(`no session "${arg}" (vos session list)`);
15188
13987
  const summary3 = await summarizeSession(arg);
15189
13988
  const url = strFlag(flags, "url");
@@ -15283,4 +14082,4 @@ export {
15283
14082
  verbHelp,
15284
14083
  run
15285
14084
  };
15286
- //# sourceMappingURL=chunk-ZS3L7CEN.js.map
14085
+ //# sourceMappingURL=chunk-NHJRD2XC.js.map