@vosjs/cli 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,9 @@ import {
6
6
  } from "./chunk-NHKHTIDR.js";
7
7
 
8
8
  // src/plugin/run.ts
9
- import { mkdir as mkdir8, readFile as readFile12, rm as rm4 } from "fs/promises";
10
- import { existsSync as existsSync12 } from "fs";
11
- import { join as join15, resolve as resolve7 } from "path";
9
+ import { mkdir as mkdir9, readFile as readFile13, rm as rm4 } from "fs/promises";
10
+ import { existsSync as existsSync13 } from "fs";
11
+ import { join as join16, resolve as resolve8 } from "path";
12
12
  import { totalDuration as totalDuration2 } from "@vosjs/timeline";
13
13
  import { migrateHostedDoc as migrateHostedDoc4, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
14
14
 
@@ -1279,6 +1279,11 @@ var MIME = {
1279
1279
  ".json": "application/json",
1280
1280
  ".jpg": "image/jpeg",
1281
1281
  ".png": "image/png",
1282
+ // A brand mark or an image overlay: an <img> refuses an SVG served as an
1283
+ // octet stream, so the kinds a doc can key are named.
1284
+ ".svg": "image/svg+xml",
1285
+ ".webp": "image/webp",
1286
+ ".gif": "image/gif",
1282
1287
  ".webm": "video/webm",
1283
1288
  ".mp4": "video/mp4",
1284
1289
  // Take-dir audio (doc.audio keys like "/music.mp3") — decode goes through
@@ -1347,10 +1352,10 @@ function startTakeServer(rootDir, pages) {
1347
1352
  res.writeHead(404).end();
1348
1353
  });
1349
1354
  return new Promise(
1350
- (resolve8) => server.listen(0, () => {
1355
+ (resolve9) => server.listen(0, () => {
1351
1356
  const addr = server.address();
1352
1357
  const port = typeof addr === "object" && addr ? addr.port : 0;
1353
- resolve8({ base: `http://localhost:${port}`, close: () => server.close() });
1358
+ resolve9({ base: `http://localhost:${port}`, close: () => server.close() });
1354
1359
  })
1355
1360
  );
1356
1361
  }
@@ -2359,8 +2364,13 @@ function parseTranscript(raw) {
2359
2364
 
2360
2365
  // src/plugin/framesTake.ts
2361
2366
  var VIDEO_TOKEN = "__VOILA_CLI_VIDEO__";
2367
+ function stillSupersample(w, h) {
2368
+ return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
2369
+ }
2362
2370
  async function stillsInPage(opts) {
2363
2371
  const w = window;
2372
+ const RW = opts.W * opts.ss;
2373
+ const RH = opts.H * opts.ss;
2364
2374
  try {
2365
2375
  const vblob = await (await fetch(opts.videoUrl)).blob();
2366
2376
  const vurl = URL.createObjectURL(vblob);
@@ -2377,11 +2387,11 @@ async function stillsInPage(opts) {
2377
2387
  THREE: w.__THREE__,
2378
2388
  gsap: w.__gsap__,
2379
2389
  resolution: {
2380
- width: opts.W,
2381
- height: opts.H,
2390
+ width: RW,
2391
+ height: RH,
2382
2392
  pixelRatio: 1,
2383
- drawingBufferWidth: opts.W,
2384
- drawingBufferHeight: opts.H
2393
+ drawingBufferWidth: RW,
2394
+ drawingBufferHeight: RH
2385
2395
  },
2386
2396
  preserveDrawingBuffer: true
2387
2397
  };
@@ -2391,32 +2401,44 @@ async function stillsInPage(opts) {
2391
2401
  timeline.pause();
2392
2402
  timeline.seek(0, false);
2393
2403
  const canvas = document.querySelector("canvas");
2394
- canvas.width = opts.W;
2395
- canvas.height = opts.H;
2396
- canvas.style.width = opts.W + "px";
2397
- canvas.style.height = opts.H + "px";
2404
+ canvas.width = RW;
2405
+ canvas.height = RH;
2406
+ canvas.style.width = RW + "px";
2407
+ canvas.style.height = RH + "px";
2408
+ const out = document.createElement("canvas");
2409
+ out.width = opts.W;
2410
+ out.height = opts.H;
2398
2411
  const raf = () => new Promise((r) => requestAnimationFrame(r));
2399
2412
  const wvr = () => w.__vos__?.waitForVideosReady ? w.__vos__.waitForVideosReady() : null;
2400
2413
  const pending = () => w.__vos__?.pendingDecodes?.size ?? 0;
2401
- const toPng = () => new Promise(
2402
- (res, rej) => canvas.toBlob(
2414
+ const toPng = () => new Promise((res, rej) => {
2415
+ let src = canvas;
2416
+ if (opts.ss > 1) {
2417
+ const g = out.getContext("2d");
2418
+ g.imageSmoothingEnabled = true;
2419
+ g.imageSmoothingQuality = "high";
2420
+ g.clearRect(0, 0, opts.W, opts.H);
2421
+ g.drawImage(canvas, 0, 0, opts.W, opts.H);
2422
+ src = out;
2423
+ }
2424
+ src.toBlob(
2403
2425
  (b) => b ? res(b) : rej(new Error("toBlob failed")),
2404
2426
  "image/png"
2405
- )
2406
- );
2427
+ );
2428
+ });
2407
2429
  const collectVideos = () => {
2408
- const out = /* @__PURE__ */ new Set();
2430
+ const out2 = /* @__PURE__ */ new Set();
2409
2431
  const cache = w.__vos__?.videoCache;
2410
2432
  if (cache) {
2411
2433
  const vals = cache instanceof Map ? [...cache.values()] : Object.values(cache);
2412
2434
  for (const v of vals) {
2413
- if (v?.tagName === "VIDEO") out.add(v);
2414
- else if (v?.el?.tagName === "VIDEO") out.add(v.el);
2415
- else if (v?.video?.tagName === "VIDEO") out.add(v.video);
2435
+ if (v?.tagName === "VIDEO") out2.add(v);
2436
+ else if (v?.el?.tagName === "VIDEO") out2.add(v.el);
2437
+ else if (v?.video?.tagName === "VIDEO") out2.add(v.video);
2416
2438
  }
2417
2439
  }
2418
- document.querySelectorAll("video").forEach((v) => out.add(v));
2419
- return [...out];
2440
+ document.querySelectorAll("video").forEach((v) => out2.add(v));
2441
+ return [...out2];
2420
2442
  };
2421
2443
  const videosSettled = () => collectVideos().every((v) => !v.seeking && v.readyState >= 2);
2422
2444
  const waitVideosSettled = async (maxMs) => {
@@ -2502,7 +2524,10 @@ async function framesTake(browser, dir, opts) {
2502
2524
  const server = await startTakeServer(dir, {
2503
2525
  "/render.html": renderPageHtml()
2504
2526
  });
2505
- const context = await browser.newContext({ viewport: { width, height } });
2527
+ const ss = stillSupersample(width, height);
2528
+ const context = await browser.newContext({
2529
+ viewport: { width: width * ss, height: height * ss }
2530
+ });
2506
2531
  try {
2507
2532
  const page = await context.newPage();
2508
2533
  page.on("console", (m) => {
@@ -2522,6 +2547,7 @@ async function framesTake(browser, dir, opts) {
2522
2547
  videoUrl: `/${RECORDING_NAME}`,
2523
2548
  W: width,
2524
2549
  H: height,
2550
+ ss,
2525
2551
  shots: named.map(({ time, name }) => ({ time, name }))
2526
2552
  }).catch(() => {
2527
2553
  });
@@ -2592,19 +2618,12 @@ async function writeIndexJson(result) {
2592
2618
  }
2593
2619
 
2594
2620
  // src/plugin/deliver.ts
2595
- import {
2596
- mkdir as mkdir4,
2597
- mkdtemp,
2598
- rename as rename4,
2599
- rm as rm3,
2600
- stat,
2601
- writeFile as writeFile6
2602
- } from "fs/promises";
2621
+ import { mkdir as mkdir5, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile7 } from "fs/promises";
2603
2622
  import { tmpdir } from "os";
2604
- import { join as join5, relative, resolve as resolve3 } from "path";
2623
+ import { join as join6, relative, resolve as resolve4 } from "path";
2605
2624
  import { totalDuration } from "@vosjs/timeline";
2606
- import { existsSync as existsSync5 } from "fs";
2607
- import { readFile as readFile5 } from "fs/promises";
2625
+ import { existsSync as existsSync6 } from "fs";
2626
+ import { readFile as readFile6 } from "fs/promises";
2608
2627
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
2609
2628
  import {
2610
2629
  CHANNEL_SPECS_VERIFIED,
@@ -2720,6 +2739,8 @@ async function renderTake(browser, dir, outFile, opts) {
2720
2739
  import { compileVosConfig as compileVosConfig3 } from "@vosjs/core";
2721
2740
  async function posterStillInPage(opts) {
2722
2741
  const w = window;
2742
+ const RW = opts.W * opts.ss;
2743
+ const RH = opts.H * opts.ss;
2723
2744
  try {
2724
2745
  const mod = await import(
2725
2746
  /* @vite-ignore */
@@ -2733,11 +2754,11 @@ async function posterStillInPage(opts) {
2733
2754
  THREE: w.__THREE__,
2734
2755
  gsap: w.__gsap__,
2735
2756
  resolution: {
2736
- width: opts.W,
2737
- height: opts.H,
2757
+ width: RW,
2758
+ height: RH,
2738
2759
  pixelRatio: 1,
2739
- drawingBufferWidth: opts.W,
2740
- drawingBufferHeight: opts.H
2760
+ drawingBufferWidth: RW,
2761
+ drawingBufferHeight: RH
2741
2762
  },
2742
2763
  preserveDrawingBuffer: true
2743
2764
  };
@@ -2747,16 +2768,27 @@ async function posterStillInPage(opts) {
2747
2768
  timeline.pause();
2748
2769
  timeline.seek(opts.time, false);
2749
2770
  const canvas = document.querySelector("canvas");
2750
- canvas.width = opts.W;
2751
- canvas.height = opts.H;
2752
- canvas.style.width = opts.W + "px";
2753
- canvas.style.height = opts.H + "px";
2771
+ canvas.width = RW;
2772
+ canvas.height = RH;
2773
+ canvas.style.width = RW + "px";
2774
+ canvas.style.height = RH + "px";
2754
2775
  const raf = () => new Promise((r) => requestAnimationFrame(r));
2755
2776
  await raf();
2756
2777
  await raf();
2757
2778
  await raf();
2779
+ let src = canvas;
2780
+ if (opts.ss > 1) {
2781
+ const out = document.createElement("canvas");
2782
+ out.width = opts.W;
2783
+ out.height = opts.H;
2784
+ const g = out.getContext("2d");
2785
+ g.imageSmoothingEnabled = true;
2786
+ g.imageSmoothingQuality = "high";
2787
+ g.drawImage(canvas, 0, 0, opts.W, opts.H);
2788
+ src = out;
2789
+ }
2758
2790
  const png = await new Promise(
2759
- (res) => canvas.toBlob((b) => res(b), "image/png")
2791
+ (res) => src.toBlob((b) => res(b), "image/png")
2760
2792
  );
2761
2793
  if (!png) throw new Error("toBlob returned null");
2762
2794
  await fetch("/save?name=" + opts.outName, { method: "POST", body: png });
@@ -2774,8 +2806,9 @@ async function renderPosterStills(browser, config, serveDir, shots, time) {
2774
2806
  });
2775
2807
  try {
2776
2808
  for (const shot of shots) {
2809
+ const ss = stillSupersample(shot.width, shot.height);
2777
2810
  const context = await browser.newContext({
2778
- viewport: { width: shot.width, height: shot.height }
2811
+ viewport: { width: shot.width * ss, height: shot.height * ss }
2779
2812
  });
2780
2813
  try {
2781
2814
  const page = await context.newPage();
@@ -2794,6 +2827,7 @@ async function renderPosterStills(browser, config, serveDir, shots, time) {
2794
2827
  animationCode,
2795
2828
  W: shot.width,
2796
2829
  H: shot.height,
2830
+ ss,
2797
2831
  time,
2798
2832
  outName: shot.name
2799
2833
  }).catch(() => {
@@ -3325,7 +3359,8 @@ function blurPlane(src, w, h, r, passes = 3) {
3325
3359
  for (let y = 0; y < h; y++) {
3326
3360
  let acc = 0;
3327
3361
  const row = y * w;
3328
- for (let x = -r; x <= r; x++) acc += src[row + Math.min(w - 1, Math.max(0, x))];
3362
+ for (let x = -r; x <= r; x++)
3363
+ acc += src[row + Math.min(w - 1, Math.max(0, x))];
3329
3364
  for (let x = 0; x < w; x++) {
3330
3365
  tmp[row + x] = acc / (2 * r + 1);
3331
3366
  const add = src[row + Math.min(w - 1, x + r + 1)];
@@ -3335,7 +3370,8 @@ function blurPlane(src, w, h, r, passes = 3) {
3335
3370
  }
3336
3371
  for (let x = 0; x < w; x++) {
3337
3372
  let acc = 0;
3338
- for (let y = -r; y <= r; y++) acc += tmp[Math.min(h - 1, Math.max(0, y)) * w + x];
3373
+ for (let y = -r; y <= r; y++)
3374
+ acc += tmp[Math.min(h - 1, Math.max(0, y)) * w + x];
3339
3375
  for (let y = 0; y < h; y++) {
3340
3376
  src[y * w + x] = acc / (2 * r + 1);
3341
3377
  const add = tmp[Math.min(h - 1, y + r + 1) * w + x];
@@ -3351,7 +3387,8 @@ function roundedCoverage(x, y, rect, r) {
3351
3387
  for (const dx of [0.25, 0.75]) {
3352
3388
  const px = x + dx;
3353
3389
  const py = y + dy;
3354
- if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h) continue;
3390
+ if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h)
3391
+ continue;
3355
3392
  const cx = Math.min(Math.max(px, rect.x + r), rect.x + rect.w - r);
3356
3393
  const cy = Math.min(Math.max(py, rect.y + r), rect.y + rect.h - r);
3357
3394
  if ((px - cx) ** 2 + (py - cy) ** 2 <= r * r) inside++;
@@ -3362,24 +3399,34 @@ function roundedCoverage(x, y, rect, r) {
3362
3399
  function bakeShot(shot, opts = {}) {
3363
3400
  const margin = Math.round(shot.w * (opts.margin ?? 0.06));
3364
3401
  const radius = shot.w * (opts.radius ?? 0.014);
3365
- const shadowA = opts.shadow ?? 0.38;
3366
- const blur = Math.round(shot.w * (opts.blur ?? 0.03));
3367
- const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.012));
3402
+ const shadowA = opts.shadow ?? 0.3;
3403
+ const blur = Math.round(shot.w * (opts.blur ?? 0.05));
3404
+ const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.02));
3368
3405
  const hair = opts.hairline ?? 0;
3369
3406
  const w = shot.w + margin * 2;
3370
3407
  const h = shot.h + margin * 2;
3371
3408
  const out = new Uint8Array(w * h * 4);
3372
3409
  const rect = { x: margin, y: margin, w: shot.w, h: shot.h };
3373
3410
  if (shadowA > 0) {
3374
- const mask = new Float32Array(w * h);
3375
- for (let y = 0; y < h; y++)
3376
- for (let x = 0; x < w; x++) {
3377
- const c = roundedCoverage(x, y - offsetY, rect, radius);
3378
- if (c > 0) mask[y * w + x] = c;
3379
- }
3380
- blurPlane(mask, w, h, Math.max(1, Math.round(blur / 2)));
3411
+ const layers = [
3412
+ [Math.max(1, Math.round(blur / 4)), Math.round(offsetY / 5), 0.4],
3413
+ [blur, offsetY, 0.6]
3414
+ ];
3415
+ const acc = new Float32Array(w * h);
3416
+ for (const [lb, lo, share] of layers) {
3417
+ const mask = new Float32Array(w * h);
3418
+ for (let y = 0; y < h; y++)
3419
+ for (let x = 0; x < w; x++) {
3420
+ const c = roundedCoverage(x, y - lo, rect, radius);
3421
+ if (c > 0) mask[y * w + x] = c;
3422
+ }
3423
+ blurPlane(mask, w, h, Math.max(1, Math.round(lb / 2)));
3424
+ const a = shadowA * share;
3425
+ for (let i = 0; i < w * h; i++)
3426
+ acc[i] = 1 - (1 - acc[i]) * (1 - mask[i] * a);
3427
+ }
3381
3428
  for (let i = 0; i < w * h; i++) {
3382
- const a = mask[i] * shadowA;
3429
+ const a = acc[i];
3383
3430
  if (a <= 2e-3) continue;
3384
3431
  out[i * 4] = 0;
3385
3432
  out[i * 4 + 1] = 0;
@@ -3399,7 +3446,12 @@ function bakeShot(shot, opts = {}) {
3399
3446
  let g = shot.data[si + 1];
3400
3447
  let b = shot.data[si + 2];
3401
3448
  if (hair > 0) {
3402
- const edge = Math.min(x - rect.x, rect.x + rect.w - x, y - rect.y, rect.y + rect.h - y);
3449
+ const edge = Math.min(
3450
+ x - rect.x,
3451
+ rect.x + rect.w - x,
3452
+ y - rect.y,
3453
+ rect.y + rect.h - y
3454
+ );
3403
3455
  if (edge < 1.5) {
3404
3456
  r = Math.round(r * (1 - hair));
3405
3457
  g = Math.round(g * (1 - hair));
@@ -3924,12 +3976,18 @@ var rgb = (hex2) => {
3924
3976
  const n = parseInt(m[1], 16);
3925
3977
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
3926
3978
  };
3927
- var toHex = (c) => "#" + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
3979
+ var toHex = (c) => "#" + c.map(
3980
+ (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")
3981
+ ).join("");
3928
3982
  function mixHex(a, b, t) {
3929
3983
  const pa = rgb(a);
3930
3984
  const pb = rgb(b);
3931
3985
  if (!pa || !pb) return a;
3932
- return toHex([pa[0] + (pb[0] - pa[0]) * t, pa[1] + (pb[1] - pa[1]) * t, pa[2] + (pb[2] - pa[2]) * t]);
3986
+ return toHex([
3987
+ pa[0] + (pb[0] - pa[0]) * t,
3988
+ pa[1] + (pb[1] - pa[1]) * t,
3989
+ pa[2] + (pb[2] - pa[2]) * t
3990
+ ]);
3933
3991
  }
3934
3992
  function rgba(hex2, alpha) {
3935
3993
  const c = rgb(hex2) ?? [255, 255, 255];
@@ -3945,14 +4003,15 @@ function resolveFace(family, weights) {
3945
4003
  const first = family.split(",")[0].replace(/['"]/g, "").replace(/\s+variable$/i, "").trim();
3946
4004
  const entry = findFontFamily2(first);
3947
4005
  if (!entry) return null;
3948
- const fonts = [...new Set(weights.map((w) => nearestFontWeight(entry, w)))].map((w) => ({
4006
+ const fonts = [
4007
+ ...new Set(weights.map((w) => nearestFontWeight(entry, w)))
4008
+ ].map((w) => ({
3949
4009
  family: entry.family,
3950
4010
  url: fontFaceUrl(entry.slug, w),
3951
4011
  weight: w
3952
4012
  }));
3953
4013
  return { stack: fontStack(entry), category: entry.category, fonts };
3954
4014
  }
3955
- var HOUSE_SERIF = "Fraunces";
3956
4015
  function posterValues(brand, words) {
3957
4016
  const b = brand ?? {};
3958
4017
  const bgA = b.bgA && HEX.test(b.bgA) ? b.bgA : null;
@@ -3974,7 +4033,11 @@ function posterValues(brand, words) {
3974
4033
  if (bgA) values.grain = lightGround ? 10 : 22;
3975
4034
  if (accent) values.blobA = rgba(accent, lightGround ? 0.28 : 0.4);
3976
4035
  if (bgC) values.blobB = rgba(bgC, 0.75);
3977
- if (accent) values.blobC = rgba(mixHex(accent, "#ffffff", 0.55), lightGround ? 0.35 : 0.25);
4036
+ if (accent)
4037
+ values.blobC = rgba(
4038
+ mixHex(accent, "#ffffff", 0.55),
4039
+ lightGround ? 0.35 : 0.25
4040
+ );
3978
4041
  if (wordmark) values.brand = wordmark;
3979
4042
  const headline = (words.headline ?? "").trim();
3980
4043
  if (headline) values.headline = headline;
@@ -3982,10 +4045,9 @@ function posterValues(brand, words) {
3982
4045
  values.kicker = kicker ? kicker : [wordmark, release].filter(Boolean).join(" ").toUpperCase();
3983
4046
  const fonts = [];
3984
4047
  const display = resolveFace(b.fontDisplay, [600, 700]);
3985
- const serif = display && display.category === "serif" ? display : resolveFace(HOUSE_SERIF, [600, 700]);
3986
- if (serif) {
3987
- values.fontDisplay = serif.stack;
3988
- fonts.push(...serif.fonts);
4048
+ if (display) {
4049
+ values.fontDisplay = display.stack;
4050
+ fonts.push(...display.fonts);
3989
4051
  }
3990
4052
  const body = resolveFace(b.fontBody, [700]);
3991
4053
  if (body) {
@@ -3996,7 +4058,6 @@ function posterValues(brand, words) {
3996
4058
  }
3997
4059
 
3998
4060
  // src/plugin/stages.ts
3999
- var HOUSE_SERIF2 = "Fraunces";
4000
4061
  var str = (v, fallback) => typeof v === "string" && v.trim() ? v.trim() : fallback;
4001
4062
  var firstFamily = (stack) => stack.split(",")[0].replace(/['"]/g, "").trim();
4002
4063
  function stageSplitCover(input) {
@@ -4007,7 +4068,7 @@ function stageSplitCover(input) {
4007
4068
  const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4008
4069
  const ink = str(v.ink, "#fff6ec");
4009
4070
  const inkSoft = str(v.inkSoft, ink);
4010
- const serif = firstFamily(str(v.fontDisplay, HOUSE_SERIF2));
4071
+ const display = firstFamily(str(v.fontDisplay, "Lexend"));
4011
4072
  const body = firstFamily(str(v.fontBody, "Lexend"));
4012
4073
  const headline = str(v.headline, "");
4013
4074
  const kicker = str(v.kicker, "");
@@ -4026,8 +4087,8 @@ function stageSplitCover(input) {
4026
4087
  `frame.inset=${JSON.stringify(inset)}`,
4027
4088
  'frame.focus={"cx":0,"cy":0}',
4028
4089
  "frame.radius=16",
4029
- "frame.shadow=0.45",
4030
- "frame.shadowContact=0.25",
4090
+ "frame.shadow=0.5",
4091
+ "frame.shadowContact=0",
4031
4092
  "frame.border=0",
4032
4093
  "zoom=[]",
4033
4094
  `tilt=[{"id":"stage","in":0,"out":${input.sourceSeconds.toFixed(3)},"rx":3,"ry":${portrait || square ? 0 : 10}}]`,
@@ -4040,11 +4101,14 @@ function stageSplitCover(input) {
4040
4101
  const column = portrait || square ? 0.84 : 0.36;
4041
4102
  const left = portrait || square ? 0.08 : 0.07;
4042
4103
  const size = portrait ? 60 : square ? 68 : 84;
4043
- const word = (id, text, preset, family, px, y, color, extra, role) => {
4104
+ const word = (id, text, preset, family, px, y, color, extra, role, x0 = left) => {
4044
4105
  if (!text) return;
4045
4106
  const lines = text.split("\n");
4046
4107
  const longest = Math.max(...lines.map((l) => l.length));
4047
- const widest = Math.min(column, longest * px * (preset === "title" ? 0.5 : 0.62) / designW);
4108
+ const widest = Math.min(
4109
+ column,
4110
+ longest * px * (preset === "title" ? 0.5 : 0.62) / designW
4111
+ );
4048
4112
  const lh = preset === "title" ? 1.05 : 1.2;
4049
4113
  const h = lines.length * px * lh / 1080;
4050
4114
  clips.push({
@@ -4058,25 +4122,318 @@ function stageSplitCover(input) {
4058
4122
  align: "left",
4059
4123
  maxWidth: column,
4060
4124
  lineHeight: lh,
4061
- transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
4062
- shadow: "none",
4125
+ transform: { x: x0 + widest / 2, y, scale: 1, rotation: 0 },
4126
+ // Words on the ground carry no footage shadow: a drop shadow under a
4127
+ // headline on a plate is the old-web tell.
4128
+ shadow: 0,
4063
4129
  start: 0,
4064
4130
  duration: +input.outputSeconds.toFixed(3),
4065
4131
  enter: "none",
4066
4132
  exit: "none",
4067
4133
  ...extra
4068
4134
  });
4069
- boxes.push({ x: left, y: y - h / 2, w: widest, h, color, role, label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text });
4135
+ boxes.push({
4136
+ x: x0,
4137
+ y: y - h / 2,
4138
+ w: widest,
4139
+ h,
4140
+ color,
4141
+ role,
4142
+ label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text
4143
+ });
4144
+ };
4145
+ const markKey = str(v.logoKey, "");
4146
+ const markAspect = typeof v.logoAspect === "number" && v.logoAspect > 0 ? v.logoAspect : 1;
4147
+ const wideMark = !!markKey && markAspect > 2.2;
4148
+ const lockup = (px, y) => {
4149
+ if (!markKey) return left;
4150
+ const hPx = wideMark ? px * 1.6 : px * 1.3;
4151
+ const w = Math.min(column, hPx * markAspect / designW);
4152
+ clips.push({
4153
+ id: "stage-mark",
4154
+ kind: "image",
4155
+ key: markKey,
4156
+ width: +w.toFixed(4),
4157
+ radius: 0,
4158
+ shadow: "none",
4159
+ transform: { x: left + w / 2, y, scale: 1, rotation: 0 },
4160
+ start: 0,
4161
+ duration: +input.outputSeconds.toFixed(3),
4162
+ enter: "none",
4163
+ exit: "none"
4164
+ });
4165
+ return left + w + px * 0.5 / designW;
4070
4166
  };
4071
4167
  const kickerY = portrait ? 0.1 : square ? 0.12 : 0.24;
4072
4168
  const titleY = portrait ? 0.24 : square ? 0.28 : 0.46;
4073
4169
  const brandY = portrait ? 0.94 : square ? 0.94 : 0.86;
4074
- word("stage-kicker", kicker, "label", "JetBrains Mono", 19, kickerY, inkSoft, { letterSpacing: 6, weight: 400 }, "body");
4075
- word("stage-title", headline, "title", serif, size, titleY, ink, { weight: 600 }, "headline");
4076
- word("stage-brand", brand, "label", body, 25, brandY, ink, { weight: 700, letterSpacing: 1 }, "body");
4170
+ word(
4171
+ "stage-kicker",
4172
+ kicker,
4173
+ "label",
4174
+ "JetBrains Mono",
4175
+ 19,
4176
+ kickerY,
4177
+ inkSoft,
4178
+ { letterSpacing: 6, weight: 400 },
4179
+ "body"
4180
+ );
4181
+ word(
4182
+ "stage-title",
4183
+ headline,
4184
+ "title",
4185
+ display,
4186
+ size,
4187
+ titleY,
4188
+ ink,
4189
+ { weight: 600, letterSpacing: -Math.round(size * 0.02) },
4190
+ "headline"
4191
+ );
4192
+ const brandX = lockup(25, brandY);
4193
+ if (!wideMark)
4194
+ word(
4195
+ "stage-brand",
4196
+ brand,
4197
+ "label",
4198
+ body,
4199
+ 25,
4200
+ brandY,
4201
+ ink,
4202
+ { weight: 700, letterSpacing: 1 },
4203
+ "body",
4204
+ brandX
4205
+ );
4077
4206
  set.push(`overlays=${JSON.stringify(clips)}`);
4078
4207
  return { set, text: boxes, shot };
4079
4208
  }
4209
+ var TILE_MAX_PX = 700;
4210
+ function isTileSize(size) {
4211
+ return Math.max(size.w, size.h) < TILE_MAX_PX;
4212
+ }
4213
+ function stageTile(input) {
4214
+ const R = input.size.w / Math.max(1, input.size.h);
4215
+ const square = R <= 1.15;
4216
+ const v = input.values;
4217
+ const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4218
+ const ink = str(v.ink, "#fff6ec");
4219
+ const display = firstFamily(str(v.fontDisplay, "Lexend"));
4220
+ const headline = str(v.headline, "") || str(v.brand, "Release");
4221
+ const wordless = input.text === "none";
4222
+ const aspect = input.footageAspect && input.footageAspect > 0 ? input.footageAspect : 16 / 9;
4223
+ const left = square ? 0.08 : 0.07;
4224
+ const top = wordless ? square ? 0.1 : 0.12 : square ? 0.42 : 0.47;
4225
+ const visible = wordless ? square ? 0.66 : 0.8 : square ? 0.56 : 0.62;
4226
+ const cardW = (1 - left) / visible;
4227
+ const cardH = cardW * R / aspect;
4228
+ const inset = {
4229
+ left,
4230
+ right: +(1 - left - cardW).toFixed(4),
4231
+ top,
4232
+ bottom: +(1 - top - cardH).toFixed(4)
4233
+ };
4234
+ const shot = {
4235
+ x: inset.left,
4236
+ y: inset.top,
4237
+ w: 1 - inset.left - inset.right,
4238
+ h: 1 - inset.top - inset.bottom
4239
+ };
4240
+ const set = [
4241
+ `frame.background=${JSON.stringify(ground)}`,
4242
+ "frame.backgroundMedia=null",
4243
+ "frame.fit=cover",
4244
+ `frame.inset=${JSON.stringify(inset)}`,
4245
+ 'frame.focus={"cx":0,"cy":0}',
4246
+ "frame.radius=24",
4247
+ "frame.shadow=0.5",
4248
+ "frame.shadowContact=0",
4249
+ "frame.border=0",
4250
+ "zoom=[]",
4251
+ "tilt=[]",
4252
+ "cursor.visible=false",
4253
+ "cursor.clickFx.style=none"
4254
+ ];
4255
+ const designW = 1080 * input.size.w / input.size.h;
4256
+ const column = square ? 0.84 : 0.86;
4257
+ const px = square ? 96 : 108;
4258
+ const lines = headline.split("\n");
4259
+ const longest = Math.max(...lines.map((l) => l.length));
4260
+ const widest = Math.min(column, longest * px * 0.5 / designW);
4261
+ const lh = 1.05;
4262
+ const h = lines.length * px * lh / 1080;
4263
+ const y = square ? 0.2 : 0.24;
4264
+ const clip = {
4265
+ id: "stage-title",
4266
+ kind: "text",
4267
+ text: headline,
4268
+ preset: "title",
4269
+ family: display,
4270
+ size: px,
4271
+ color: ink,
4272
+ align: "left",
4273
+ maxWidth: column,
4274
+ lineHeight: lh,
4275
+ weight: 600,
4276
+ letterSpacing: -Math.round(px * 0.02),
4277
+ transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
4278
+ shadow: 0,
4279
+ start: 0,
4280
+ duration: +input.outputSeconds.toFixed(3),
4281
+ enter: "none",
4282
+ exit: "none"
4283
+ };
4284
+ if (wordless) {
4285
+ const markKey = str(v.logoKey, "");
4286
+ const markAspect = typeof v.logoAspect === "number" && v.logoAspect > 0 ? v.logoAspect : 1;
4287
+ if (markKey) {
4288
+ const hFrac = top * 0.5;
4289
+ const w = Math.min(
4290
+ 0.8,
4291
+ hFrac * input.size.h * markAspect / input.size.w
4292
+ );
4293
+ set.push(
4294
+ `overlays=${JSON.stringify([
4295
+ {
4296
+ id: "stage-mark",
4297
+ kind: "image",
4298
+ key: markKey,
4299
+ width: +w.toFixed(4),
4300
+ radius: 0,
4301
+ shadow: "none",
4302
+ transform: { x: left + w / 2, y: top / 2, scale: 1, rotation: 0 },
4303
+ start: 0,
4304
+ duration: +input.outputSeconds.toFixed(3),
4305
+ enter: "none",
4306
+ exit: "none"
4307
+ }
4308
+ ])}`
4309
+ );
4310
+ } else set.push("overlays=[]");
4311
+ return { set, text: [], shot };
4312
+ }
4313
+ set.push(`overlays=${JSON.stringify([clip])}`);
4314
+ const text = [
4315
+ {
4316
+ x: left,
4317
+ y: y - h / 2,
4318
+ w: widest,
4319
+ h,
4320
+ color: ink,
4321
+ role: "headline",
4322
+ label: headline.length > 24 ? `${headline.slice(0, 24)}\u2026` : headline
4323
+ }
4324
+ ];
4325
+ return { set, text, shot };
4326
+ }
4327
+
4328
+ // src/plugin/markAsset.ts
4329
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
4330
+ import { existsSync as existsSync5 } from "fs";
4331
+ import { isAbsolute, join as join5, resolve as resolve3 } from "path";
4332
+ function markExtension(url) {
4333
+ const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4334
+ if (!m) return null;
4335
+ const e = m[1].toLowerCase();
4336
+ return e === "jpeg" ? "jpg" : e;
4337
+ }
4338
+ function imageAspect(bytes, ext) {
4339
+ if (ext === "svg") {
4340
+ const head = new TextDecoder().decode(bytes.subarray(0, 4096));
4341
+ const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(
4342
+ head
4343
+ );
4344
+ if (vb) return ratio(+vb[1], +vb[2]);
4345
+ const w = /<svg[^>]*\swidth\s*=\s*["']([\d.]+)/i.exec(head);
4346
+ const h = /<svg[^>]*\sheight\s*=\s*["']([\d.]+)/i.exec(head);
4347
+ if (w && h) return ratio(+w[1], +h[1]);
4348
+ return null;
4349
+ }
4350
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4351
+ if (ext === "png") {
4352
+ if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
4353
+ return ratio(dv.getUint32(16), dv.getUint32(20));
4354
+ }
4355
+ if (ext === "webp") {
4356
+ if (bytes.length < 30) return null;
4357
+ const tag = String.fromCharCode(...bytes.subarray(12, 16));
4358
+ if (tag === "VP8X")
4359
+ return ratio(
4360
+ 1 + (dv.getUint32(24, true) & 16777215),
4361
+ 1 + (dv.getUint32(27, true) & 16777215)
4362
+ );
4363
+ if (tag === "VP8L") {
4364
+ const b = dv.getUint32(21, true);
4365
+ return ratio(1 + (b & 16383), 1 + (b >> 14 & 16383));
4366
+ }
4367
+ if (tag === "VP8 ")
4368
+ return ratio(
4369
+ dv.getUint16(26, true) & 16383,
4370
+ dv.getUint16(28, true) & 16383
4371
+ );
4372
+ return null;
4373
+ }
4374
+ if (ext === "jpg") {
4375
+ let i = 2;
4376
+ while (i + 9 < bytes.length) {
4377
+ if (bytes[i] !== 255) return null;
4378
+ const marker = bytes[i + 1];
4379
+ const len = dv.getUint16(i + 2);
4380
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
4381
+ return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
4382
+ }
4383
+ i += 2 + len;
4384
+ }
4385
+ return null;
4386
+ }
4387
+ return null;
4388
+ }
4389
+ function ratio(w, h) {
4390
+ return w > 0 && h > 0 ? w / h : null;
4391
+ }
4392
+ async function fetchBrandMarks(takeDir, roles) {
4393
+ const out = { light: null, dark: null, notes: [] };
4394
+ if (!roles) return out;
4395
+ const one = async (role, name) => {
4396
+ const src = (roles[role] ?? "").trim();
4397
+ if (!src) return null;
4398
+ const ext = markExtension(src);
4399
+ if (!ext) {
4400
+ out.notes.push(
4401
+ `${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
4402
+ );
4403
+ return null;
4404
+ }
4405
+ let bytes;
4406
+ try {
4407
+ if (/^https?:/i.test(src)) {
4408
+ const res = await fetch(src);
4409
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
4410
+ bytes = new Uint8Array(await res.arrayBuffer());
4411
+ } else {
4412
+ const file = isAbsolute(src) ? src : resolve3(takeDir, src);
4413
+ if (!existsSync5(file)) throw new Error("no such file");
4414
+ bytes = new Uint8Array(await readFile5(file));
4415
+ }
4416
+ } catch (e) {
4417
+ out.notes.push(
4418
+ `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
4419
+ );
4420
+ return null;
4421
+ }
4422
+ const aspect = imageAspect(bytes, ext);
4423
+ if (!aspect) {
4424
+ out.notes.push(
4425
+ `${role}: ${src} has no readable size; the wordmark stands in`
4426
+ );
4427
+ return null;
4428
+ }
4429
+ await mkdir4(join5(takeDir, "brand"), { recursive: true });
4430
+ await writeFile6(join5(takeDir, "brand", `${name}.${ext}`), bytes);
4431
+ return { key: `/brand/${name}.${ext}`, aspect };
4432
+ };
4433
+ out.light = await one("logoUrl", "mark");
4434
+ out.dark = await one("logoOnDarkUrl", "mark-on-dark");
4435
+ return out;
4436
+ }
4080
4437
 
4081
4438
  // src/plugin/motionPlan.ts
4082
4439
  import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
@@ -4133,10 +4490,13 @@ function planMotion(input) {
4133
4490
  if (headline) card.headline = headline;
4134
4491
  if (sub && sub !== headline) card.sub = sub;
4135
4492
  if (brand) card.wordmark = brand;
4493
+ if (input.mark) card.mark = input.mark;
4136
4494
  set.push(`endCard=${JSON.stringify(card)}`);
4137
4495
  notes.push("end card");
4138
4496
  } else {
4139
- skipped.push(`${d.id}: no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)`);
4497
+ skipped.push(
4498
+ `${d.id}: no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)`
4499
+ );
4140
4500
  }
4141
4501
  }
4142
4502
  if (!loop && d.text !== "none" && input.captions.length && !off(launch.captions)) {
@@ -4144,7 +4504,9 @@ function planMotion(input) {
4144
4504
  const steps = doc.source.meta.steps ?? [];
4145
4505
  const clips = [];
4146
4506
  for (const c of input.captions) {
4147
- const step = steps.find((s) => c.id !== void 0 && s.id === c.id || s.step === c.step);
4507
+ const step = steps.find(
4508
+ (s) => c.id !== void 0 && s.id === c.id || s.step === c.step
4509
+ );
4148
4510
  if (!step || step.skipped) continue;
4149
4511
  const t = stepOutputTime(rated, step, 0.2);
4150
4512
  if (t === null || t < range[0] || t > range[1] - 1) continue;
@@ -4192,7 +4554,9 @@ function planMotion(input) {
4192
4554
  });
4193
4555
  notes.push(`bed ${track.slug}`);
4194
4556
  } else if (launch.music && !off(launch.music)) {
4195
- skipped.push(`${d.id}: music "${launch.music}" is not a catalog track or mood${catalog ? "" : " (the catalog could not be read)"}`);
4557
+ skipped.push(
4558
+ `${d.id}: music "${launch.music}" is not a catalog track or mood${catalog ? "" : " (the catalog could not be read)"}`
4559
+ );
4196
4560
  }
4197
4561
  const click = catalog?.sfx.find((s) => s.slug === "sfx-click");
4198
4562
  if (click && !doc.source.micKey && !off(launch.clicks)) {
@@ -4252,19 +4616,19 @@ function resolveChannels(raw) {
4252
4616
  return out;
4253
4617
  }
4254
4618
  async function readBrandBesideTake(dir, explicit) {
4255
- const candidates = explicit ? [explicit] : [join5(dir, "BRAND.md"), join5(dir, "..", "BRAND.md")];
4619
+ const candidates = explicit ? [explicit] : [join6(dir, "BRAND.md"), join6(dir, "..", "BRAND.md")];
4256
4620
  for (const file of candidates) {
4257
- if (!existsSync5(file)) continue;
4258
- const roles = parseFrontmatter(await readFile5(file, "utf8"));
4621
+ if (!existsSync6(file)) continue;
4622
+ const roles = parseFrontmatter(await readFile6(file, "utf8"));
4259
4623
  return { file, roles };
4260
4624
  }
4261
4625
  return null;
4262
4626
  }
4263
4627
  async function readLaunchBesideTake(dir, explicit) {
4264
- const candidates = explicit ? [explicit] : [join5(dir, "LAUNCH.md"), join5(dir, "..", "LAUNCH.md")];
4628
+ const candidates = explicit ? [explicit] : [join6(dir, "LAUNCH.md"), join6(dir, "..", "LAUNCH.md")];
4265
4629
  for (const file of candidates) {
4266
- if (!existsSync5(file)) continue;
4267
- return { file, roles: parseFrontmatter(await readFile5(file, "utf8")) };
4630
+ if (!existsSync6(file)) continue;
4631
+ return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
4268
4632
  }
4269
4633
  return null;
4270
4634
  }
@@ -4293,6 +4657,10 @@ function templateForCard(d, opts) {
4293
4657
  if (opts.poster === null) return null;
4294
4658
  if (opts.poster) return { config: opts.poster.config, from: opts.poster.from };
4295
4659
  if (!d.template) return null;
4660
+ if (isTileSize(d.px)) {
4661
+ const config2 = templateByName(d.template) ?? templateByName("card-on-gradient");
4662
+ if (config2) return { config: config2, from: "stage tile", stage: "tile" };
4663
+ }
4296
4664
  let name = d.template;
4297
4665
  let note;
4298
4666
  const wants = templateOf(templateByName(name) ?? {});
@@ -4303,7 +4671,8 @@ function templateForCard(d, opts) {
4303
4671
  }
4304
4672
  const config = templateByName(name);
4305
4673
  if (!config) return null;
4306
- if (name === "split-cover") return { config, from: "stage split-cover", note, stage: true };
4674
+ if (name === "split-cover")
4675
+ return { config, from: "stage split-cover", note, stage: "split-cover" };
4307
4676
  return { config, from: `template ${name}`, note };
4308
4677
  }
4309
4678
  function lookOverrides(look, placement, size, video, opts) {
@@ -4324,7 +4693,12 @@ function lookOverrides(look, placement, size, video, opts) {
4324
4693
  }
4325
4694
  if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
4326
4695
  if (opts.still) {
4327
- set.push("zoom=[]", "tilt=[]", "cursor.visible=false", "cursor.clickFx.style=none");
4696
+ set.push(
4697
+ "zoom=[]",
4698
+ "tilt=[]",
4699
+ "cursor.visible=false",
4700
+ "cursor.clickFx.style=none"
4701
+ );
4328
4702
  }
4329
4703
  return set;
4330
4704
  }
@@ -4357,7 +4731,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4357
4731
  const meta = doc.source.meta;
4358
4732
  const vw = meta.captureWidth ?? meta.width;
4359
4733
  const vh = meta.captureHeight ?? meta.height;
4360
- const probeDir = await mkdtemp(join5(tmpdir(), "vos-moments-"));
4734
+ const probeDir = await mkdtemp(join6(tmpdir(), "vos-moments-"));
4361
4735
  try {
4362
4736
  const probe = await framesTake(browser, dir, {
4363
4737
  times: candidates.map((c) => c.time),
@@ -4371,7 +4745,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4371
4745
  });
4372
4746
  const byTime = /* @__PURE__ */ new Map();
4373
4747
  for (const frame of probe.frames) {
4374
- const img = decodePng(new Uint8Array(await readFile5(frame.file)));
4748
+ const img = decodePng(new Uint8Array(await readFile6(frame.file)));
4375
4749
  if (!img) continue;
4376
4750
  const whole = { x: 0, y: 0, w: img.w, h: img.h };
4377
4751
  byTime.set(+frame.time.toFixed(3), {
@@ -4442,10 +4816,16 @@ async function deliverTake(browser, dir, opts) {
4442
4816
  const take = await loadTake(dir);
4443
4817
  if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
4444
4818
  const doc = take.doc;
4819
+ const marks = await fetchBrandMarks(dir, opts.brandRoles);
4820
+ for (const n of marks.notes) opts.onPhase?.(`note: ${n}`);
4821
+ const darkGround = opts.look?.kind === "dark";
4822
+ const mark = darkGround ? marks.dark : marks.light;
4823
+ if (mark)
4824
+ opts.onPhase?.(`brand mark: ${mark.key} (${mark.aspect.toFixed(2)}:1)`);
4445
4825
  const duration = totalDuration(ratedSegments5(doc));
4446
4826
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
4447
- const outDir = resolve3(opts.outDir ?? join5(dir, "kit"));
4448
- await mkdir4(outDir, { recursive: true });
4827
+ const outDir = resolve4(opts.outDir ?? join6(dir, "kit"));
4828
+ await mkdir5(outDir, { recursive: true });
4449
4829
  const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
4450
4830
  const assets = [];
4451
4831
  const skipped = [];
@@ -4487,7 +4867,8 @@ async function deliverTake(browser, dir, opts) {
4487
4867
  launch: opts.launchRoles ?? {},
4488
4868
  captions: opts.captions ?? [],
4489
4869
  catalog: opts.catalog ?? null,
4490
- ink: endCardInk(opts.look, opts.brandRoles)
4870
+ ink: endCardInk(opts.look, opts.brandRoles),
4871
+ mark
4491
4872
  });
4492
4873
  set.push(...plan.set);
4493
4874
  if (plan.notes.length) opts.onPhase?.(`${d.id}: ${plan.notes.join(", ")}`);
@@ -4496,17 +4877,26 @@ async function deliverTake(browser, dir, opts) {
4496
4877
  if (!set.length) return opts.overrides;
4497
4878
  return { ...opts.overrides, set };
4498
4879
  };
4499
- const cardPlans = destinations.filter((d) => d.kind !== "video" && d.genre === "card" && !NOT_FROM_FOOTAGE[d.id]).map((d) => ({ d, plan: templateForCard(d, opts) })).filter((p) => p.plan !== null);
4880
+ const cardPlans = destinations.filter(
4881
+ (d) => d.kind !== "video" && d.genre === "card" && !NOT_FROM_FOOTAGE[d.id]
4882
+ ).map((d) => ({ d, plan: templateForCard(d, opts) })).filter(
4883
+ (p) => p.plan !== null
4884
+ );
4500
4885
  const posterCardIds = new Set(cardPlans.map((p) => p.d.id));
4501
- for (const p of cardPlans) if (p.plan.note) skipped.push(`note: ${p.plan.note}`);
4886
+ for (const p of cardPlans)
4887
+ if (p.plan.note) skipped.push(`note: ${p.plan.note}`);
4502
4888
  if (cardPlans.length) {
4503
4889
  const meta = doc.source.meta;
4504
4890
  const heroTime = opts.shotTime ?? (stillTimes.length ? stillTimes[0] : duration / 2);
4505
4891
  const fill = posterValues(opts.brandRoles, opts.words ?? {});
4892
+ if (mark) {
4893
+ fill.values.logoKey = mark.key;
4894
+ fill.values.logoAspect = mark.aspect;
4895
+ }
4506
4896
  opts.onPhase?.(
4507
4897
  `poster shot (full bleed at ${heroTime.toFixed(2)}s), baked as an object`
4508
4898
  );
4509
- const serveDir = await mkdtemp(join5(tmpdir(), "vos-poster-"));
4899
+ const serveDir = await mkdtemp(join6(tmpdir(), "vos-poster-"));
4510
4900
  try {
4511
4901
  const shot = await framesTake(browser, dir, {
4512
4902
  times: [heroTime],
@@ -4528,26 +4918,31 @@ async function deliverTake(browser, dir, opts) {
4528
4918
  ]
4529
4919
  }
4530
4920
  });
4531
- const raw = decodePng(new Uint8Array(await readFile5(shot.frames[0].file)));
4921
+ const raw = decodePng(new Uint8Array(await readFile6(shot.frames[0].file)));
4532
4922
  if (!raw) throw new Error("the poster shot could not be decoded");
4533
4923
  const PAD = 0.06;
4534
4924
  const baked = bakeShot(raw, {
4535
4925
  margin: PAD,
4536
4926
  hairline: fill.lightGround ? 0.14 : 0,
4537
- shadow: fill.lightGround ? 0.32 : 0.45
4927
+ shadow: fill.lightGround ? 0.28 : 0.4
4538
4928
  });
4539
- await writeFile6(join5(serveDir, "shot.png"), encodePng(baked));
4929
+ await writeFile7(join6(serveDir, "shot.png"), encodePng(baked));
4540
4930
  const shotAspect = raw.w / raw.h;
4541
4931
  for (const { d, plan } of cardPlans) {
4542
4932
  if (plan.stage) {
4543
- const staged = stageSplitCover({
4933
+ const stageInput = {
4544
4934
  size: d.px,
4545
4935
  values: fill.values,
4546
4936
  sourceSeconds: meta.durationMs / 1e3,
4547
- outputSeconds: duration
4548
- });
4549
- opts.onPhase?.(`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`);
4550
- const shotDir = await mkdtemp(join5(tmpdir(), "vos-stage-"));
4937
+ outputSeconds: duration,
4938
+ text: d.text,
4939
+ footageAspect: (meta.captureWidth ?? meta.width) / (meta.captureHeight ?? meta.height)
4940
+ };
4941
+ const staged = plan.stage === "tile" ? stageTile(stageInput) : stageSplitCover(stageInput);
4942
+ opts.onPhase?.(
4943
+ `${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`
4944
+ );
4945
+ const shotDir = await mkdtemp(join6(tmpdir(), "vos-stage-"));
4551
4946
  try {
4552
4947
  const captured = await framesTake(browser, dir, {
4553
4948
  times: [heroTime],
@@ -4559,11 +4954,13 @@ async function deliverTake(browser, dir, opts) {
4559
4954
  set: [...staged.set, ...opts.overrides?.set ?? []]
4560
4955
  }
4561
4956
  });
4562
- const to2 = join5(outDir, `${d.id}.png`);
4957
+ const to2 = join6(outDir, `${d.id}.png`);
4563
4958
  await rename4(captured.frames[0].file, to2);
4564
4959
  const bytes2 = (await stat(to2)).size;
4565
4960
  if (d.maxBytes !== void 0 && bytes2 > d.maxBytes) {
4566
- skipped.push(`${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`);
4961
+ skipped.push(
4962
+ `${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`
4963
+ );
4567
4964
  continue;
4568
4965
  }
4569
4966
  assets.push({
@@ -4577,9 +4974,10 @@ async function deliverTake(browser, dir, opts) {
4577
4974
  seconds: null,
4578
4975
  frameTime: heroTime,
4579
4976
  source: "stage",
4580
- template: "split-cover-stage",
4977
+ template: `${plan.stage}-stage`,
4581
4978
  text: staged.text,
4582
- shot: staged.shot
4979
+ shot: staged.shot,
4980
+ ...plan.stage === "tile" ? { crop: true } : {}
4583
4981
  });
4584
4982
  } finally {
4585
4983
  await rm3(shotDir, { recursive: true, force: true });
@@ -4618,7 +5016,9 @@ async function deliverTake(browser, dir, opts) {
4618
5016
  opts.posterTime ?? posterDuration * 0.9,
4619
5017
  Math.max(0, posterDuration - 0.05)
4620
5018
  );
4621
- opts.onPhase?.(`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`);
5019
+ opts.onPhase?.(
5020
+ `${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`
5021
+ );
4622
5022
  await renderPosterStills(
4623
5023
  browser,
4624
5024
  config,
@@ -4626,8 +5026,8 @@ async function deliverTake(browser, dir, opts) {
4626
5026
  [{ name: `${d.id}.png`, width: d.px.w, height: d.px.h }],
4627
5027
  time
4628
5028
  );
4629
- const from = join5(serveDir, `${d.id}.png`);
4630
- const to = join5(outDir, `${d.id}.png`);
5029
+ const from = join6(serveDir, `${d.id}.png`);
5030
+ const to = join6(outDir, `${d.id}.png`);
4631
5031
  await rename4(from, to);
4632
5032
  const bytes = (await stat(to)).size;
4633
5033
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
@@ -4688,7 +5088,7 @@ async function deliverTake(browser, dir, opts) {
4688
5088
  }
4689
5089
  }
4690
5090
  opts.onPhase?.(`${label} (${specWords(d)})`);
4691
- const outFile = join5(outDir, `${d.id}.${d.format}`);
5091
+ const outFile = join6(outDir, `${d.id}.${d.format}`);
4692
5092
  const bitrate = d.maxBytes !== void 0 ? Math.min(
4693
5093
  1e7,
4694
5094
  Math.floor(d.maxBytes * 8 / seconds * 0.85)
@@ -4735,7 +5135,7 @@ async function deliverTake(browser, dir, opts) {
4735
5135
  for (let i = 0; i < captured.frames.length; i++) {
4736
5136
  const frame = captured.frames[i];
4737
5137
  const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
4738
- const to = join5(outDir, name);
5138
+ const to = join6(outDir, name);
4739
5139
  await rename4(frame.file, to);
4740
5140
  const bytes = (await stat(to)).size;
4741
5141
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
@@ -4773,8 +5173,8 @@ async function deliverTake(browser, dir, opts) {
4773
5173
  skipped,
4774
5174
  assets
4775
5175
  };
4776
- const kitFile = join5(outDir, "kit.json");
4777
- await writeFile6(kitFile, JSON.stringify(kit, null, 2));
5176
+ const kitFile = join6(outDir, "kit.json");
5177
+ await writeFile7(kitFile, JSON.stringify(kit, null, 2));
4778
5178
  return { kit, kitFile, outDir };
4779
5179
  }
4780
5180
 
@@ -4791,9 +5191,9 @@ async function fetchMusicCatalog(origin) {
4791
5191
  }
4792
5192
 
4793
5193
  // src/plugin/judge.ts
4794
- import { existsSync as existsSync6 } from "fs";
4795
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
4796
- import { dirname, isAbsolute, join as join6, resolve as resolve4 } from "path";
5194
+ import { existsSync as existsSync7 } from "fs";
5195
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
5196
+ import { dirname, isAbsolute as isAbsolute2, join as join7, resolve as resolve5 } from "path";
4797
5197
  import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
4798
5198
  var RUBRIC = [
4799
5199
  "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.",
@@ -4897,26 +5297,26 @@ function composeSheet(left, right, height = 540, gutter = 32) {
4897
5297
  return { w, h, data: out };
4898
5298
  }
4899
5299
  async function judgeKit(kitPath, manifestPath, outDir) {
4900
- const kit = JSON.parse(await readFile6(kitPath, "utf8"));
4901
- const manifest = JSON.parse(await readFile6(manifestPath, "utf8"));
4902
- const manifestDir = dirname(resolve4(manifestPath));
4903
- const kitDir = dirname(resolve4(kitPath));
4904
- const out = resolve4(outDir ?? join6(kitDir, "judge"));
4905
- await mkdir5(out, { recursive: true });
5300
+ const kit = JSON.parse(await readFile7(kitPath, "utf8"));
5301
+ const manifest = JSON.parse(await readFile7(manifestPath, "utf8"));
5302
+ const manifestDir = dirname(resolve5(manifestPath));
5303
+ const kitDir = dirname(resolve5(kitPath));
5304
+ const out = resolve5(outDir ?? join7(kitDir, "judge"));
5305
+ await mkdir6(out, { recursive: true });
4906
5306
  const sheets = [];
4907
5307
  const skipped = [];
4908
5308
  const refCache = /* @__PURE__ */ new Map();
4909
5309
  const loadRef = async (file) => {
4910
5310
  if (refCache.has(file)) return refCache.get(file) ?? null;
4911
- const p = join6(manifestDir, file);
4912
- const img = existsSync6(p) ? decodePng(new Uint8Array(await readFile6(p))) : null;
5311
+ const p = join7(manifestDir, file);
5312
+ const img = existsSync7(p) ? decodePng(new Uint8Array(await readFile7(p))) : null;
4913
5313
  refCache.set(file, img);
4914
5314
  return img;
4915
5315
  };
4916
5316
  for (const a of kit.assets) {
4917
5317
  if (!/\.png$/i.test(a.path)) continue;
4918
- const file = isAbsolute(a.path) && existsSync6(a.path) ? a.path : join6(kitDir, a.path.split("/").pop() ?? a.path);
4919
- const img = existsSync6(file) ? decodePng(new Uint8Array(await readFile6(file))) : null;
5318
+ const file = isAbsolute2(a.path) && existsSync7(a.path) ? a.path : join7(kitDir, a.path.split("/").pop() ?? a.path);
5319
+ const img = existsSync7(file) ? decodePng(new Uint8Array(await readFile7(file))) : null;
4920
5320
  if (!img) {
4921
5321
  skipped.push(`${a.destination ?? a.path}: unreadable`);
4922
5322
  continue;
@@ -4936,8 +5336,8 @@ async function judgeKit(kitPath, manifestPath, outDir) {
4936
5336
  const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
4937
5337
  const nameA = `${id}--A.png`;
4938
5338
  const nameB = `${id}--B.png`;
4939
- await writeFile7(join6(out, nameA), encodePng(composeSheet(img, refImg)));
4940
- await writeFile7(join6(out, nameB), encodePng(composeSheet(refImg, img)));
5339
+ await writeFile8(join7(out, nameA), encodePng(composeSheet(img, refImg)));
5340
+ await writeFile8(join7(out, nameB), encodePng(composeSheet(refImg, img)));
4941
5341
  const rubric = [
4942
5342
  `# ${id} against ${ref.id} (${ref.file})`,
4943
5343
  "",
@@ -4957,12 +5357,12 @@ async function judgeKit(kitPath, manifestPath, outDir) {
4957
5357
  "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.",
4958
5358
  ""
4959
5359
  ].join("\n");
4960
- await writeFile7(join6(out, `${id}.md`), rubric);
5360
+ await writeFile8(join7(out, `${id}.md`), rubric);
4961
5361
  sheets.push({ asset: id, reference: ref.id, sheetA: nameA, sheetB: nameB, rubric: `${id}.md` });
4962
5362
  }
4963
- const verdictFile = join6(out, "judge.json");
4964
- if (!existsSync6(verdictFile)) {
4965
- await writeFile7(
5363
+ const verdictFile = join7(out, "judge.json");
5364
+ if (!existsSync7(verdictFile)) {
5365
+ await writeFile8(
4966
5366
  verdictFile,
4967
5367
  JSON.stringify(
4968
5368
  {
@@ -4988,7 +5388,7 @@ function winRate(verdicts) {
4988
5388
  // src/plugin/kitPicture.ts
4989
5389
  import { execFile } from "child_process";
4990
5390
  import { promisify } from "util";
4991
- import { readFile as readFile7 } from "fs/promises";
5391
+ import { readFile as readFile8 } from "fs/promises";
4992
5392
  var execFileP = promisify(execFile);
4993
5393
  var SUBJECT_BAND = { min: 0.6, max: 0.92 };
4994
5394
  var BLANK_INK = 0.12;
@@ -5033,7 +5433,7 @@ function stillFindings(a, img, m) {
5033
5433
  const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
5034
5434
  const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
5035
5435
  const ink = inkCoverage(img, rect);
5036
- if (ink < BLANK_INK) {
5436
+ if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
5037
5437
  out.push({
5038
5438
  code: "blank",
5039
5439
  severity: "error",
@@ -5043,7 +5443,7 @@ function stillFindings(a, img, m) {
5043
5443
  bbox: rect
5044
5444
  });
5045
5445
  }
5046
- if (a.shot.w < 0.5 || a.shot.w > 1.2) {
5446
+ if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
5047
5447
  out.push({
5048
5448
  code: "subject",
5049
5449
  severity: "error",
@@ -5131,7 +5531,12 @@ function stillFindings(a, img, m) {
5131
5531
  function textFindings(a, img) {
5132
5532
  const out = [];
5133
5533
  for (const t of a.text ?? []) {
5134
- const box = { x: t.x * img.w, y: t.y * img.h, w: t.w * img.w, h: t.h * img.h };
5534
+ const box = {
5535
+ x: t.x * img.w,
5536
+ y: t.y * img.h,
5537
+ w: t.w * img.w,
5538
+ h: t.h * img.h
5539
+ };
5135
5540
  const name = t.label ? `"${t.label}"` : "a text box";
5136
5541
  if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
5137
5542
  out.push({
@@ -5206,7 +5611,9 @@ function duplicateFindings(stills) {
5206
5611
  if (g.length > 1) groups.push(g);
5207
5612
  }
5208
5613
  return groups.map((g) => {
5209
- const composed = g.every((s) => stills.find((x) => x.destination === s.destination)?.composed);
5614
+ const composed = g.every(
5615
+ (s) => stills.find((x) => x.destination === s.destination)?.composed
5616
+ );
5210
5617
  return {
5211
5618
  code: "duplicate",
5212
5619
  severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
@@ -5220,7 +5627,21 @@ async function videoFrame(file, at2, ffmpeg) {
5220
5627
  try {
5221
5628
  const { stdout } = await execFileP(
5222
5629
  ffmpeg,
5223
- ["-v", "error", "-ss", at2.toFixed(3), "-i", file, "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "-"],
5630
+ [
5631
+ "-v",
5632
+ "error",
5633
+ "-ss",
5634
+ at2.toFixed(3),
5635
+ "-i",
5636
+ file,
5637
+ "-frames:v",
5638
+ "1",
5639
+ "-f",
5640
+ "image2pipe",
5641
+ "-vcodec",
5642
+ "png",
5643
+ "-"
5644
+ ],
5224
5645
  { encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
5225
5646
  );
5226
5647
  return decodePng(new Uint8Array(stdout));
@@ -5243,7 +5664,7 @@ async function pictureChecks(assets) {
5243
5664
  let ffmpeg = null;
5244
5665
  for (const a of assets) {
5245
5666
  if (/\.png$/i.test(a.file)) {
5246
- const img = decodePng(new Uint8Array(await readFile7(a.file)));
5667
+ const img = decodePng(new Uint8Array(await readFile8(a.file)));
5247
5668
  if (!img) {
5248
5669
  findings.push({
5249
5670
  code: "unreadable",
@@ -5283,14 +5704,19 @@ async function pictureChecks(assets) {
5283
5704
  }
5284
5705
  const seconds = a.seconds ?? 0;
5285
5706
  const first = await videoFrame(a.file, 0, "ffmpeg");
5286
- const last = await videoFrame(a.file, Math.max(0, seconds - 0.1), "ffmpeg");
5707
+ const last = await videoFrame(
5708
+ a.file,
5709
+ Math.max(0, seconds - 0.1),
5710
+ "ffmpeg"
5711
+ );
5287
5712
  for (const [name, img] of [
5288
5713
  ["first", first],
5289
5714
  ["last", last]
5290
5715
  ]) {
5291
5716
  if (!img) continue;
5292
5717
  const m = measureStill(img);
5293
- if (name === "first") measured.push({ destination: a.destination, measure: m });
5718
+ if (name === "first")
5719
+ measured.push({ destination: a.destination, measure: m });
5294
5720
  const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
5295
5721
  const ink = inkCoverage(img, subject);
5296
5722
  if (ink < BLANK_INK) {
@@ -5316,7 +5742,11 @@ async function pictureChecks(assets) {
5316
5742
  }
5317
5743
  }
5318
5744
  findings.push(...duplicateFindings(stills));
5319
- const order = { error: 0, warning: 1, info: 2 };
5745
+ const order = {
5746
+ error: 0,
5747
+ warning: 1,
5748
+ info: 2
5749
+ };
5320
5750
  findings.sort((a, b) => order[a.severity] - order[b.severity]);
5321
5751
  return { findings, measured };
5322
5752
  }
@@ -5327,9 +5757,9 @@ function formatFinding(f) {
5327
5757
  }
5328
5758
 
5329
5759
  // src/plugin/validateKit.ts
5330
- import { existsSync as existsSync7 } from "fs";
5331
- import { readFile as readFile8, stat as stat2 } from "fs/promises";
5332
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join7 } from "path";
5760
+ import { existsSync as existsSync8 } from "fs";
5761
+ import { readFile as readFile9, stat as stat2 } from "fs/promises";
5762
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join8 } from "path";
5333
5763
  import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5334
5764
  var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5335
5765
  function pngDimensions(bytes) {
@@ -5348,7 +5778,7 @@ async function probeVideo(path) {
5348
5778
  const MB = await import("mediabunny");
5349
5779
  const input = new MB.Input({
5350
5780
  formats: MB.ALL_FORMATS,
5351
- source: new MB.BufferSource(new Uint8Array(await readFile8(path)))
5781
+ source: new MB.BufferSource(new Uint8Array(await readFile9(path)))
5352
5782
  });
5353
5783
  try {
5354
5784
  const track = await input.getPrimaryVideoTrack();
@@ -5369,7 +5799,7 @@ async function validateKit(kitPath, opts = {}) {
5369
5799
  const measured = [];
5370
5800
  let kit;
5371
5801
  try {
5372
- kit = JSON.parse(await readFile8(kitPath, "utf8"));
5802
+ kit = JSON.parse(await readFile9(kitPath, "utf8"));
5373
5803
  } catch (e) {
5374
5804
  return {
5375
5805
  valid: false,
@@ -5388,8 +5818,8 @@ async function validateKit(kitPath, opts = {}) {
5388
5818
  }
5389
5819
  const base = dirname2(kitPath);
5390
5820
  const resolvePath = (p) => {
5391
- if (isAbsolute2(p) && existsSync7(p)) return p;
5392
- const beside = join7(base, p.split("/").pop() ?? p);
5821
+ if (isAbsolute3(p) && existsSync8(p)) return p;
5822
+ const beside = join8(base, p.split("/").pop() ?? p);
5393
5823
  return beside;
5394
5824
  };
5395
5825
  const perDestination = /* @__PURE__ */ new Map();
@@ -5415,7 +5845,7 @@ async function validateKit(kitPath, opts = {}) {
5415
5845
  let h = null;
5416
5846
  let seconds = null;
5417
5847
  if (/\.(png|jpe?g|webp)$/i.test(file)) {
5418
- const head = Buffer.from(await readFile8(file));
5848
+ const head = Buffer.from(await readFile9(file));
5419
5849
  const kind = sniffImage(head);
5420
5850
  if (file.toLowerCase().endsWith(".png") && kind !== "png")
5421
5851
  problems.push(
@@ -5468,7 +5898,8 @@ async function validateKit(kitPath, opts = {}) {
5468
5898
  text: a.text,
5469
5899
  seconds,
5470
5900
  composed: a.composed,
5471
- shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0
5901
+ shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
5902
+ crop: a.crop
5472
5903
  });
5473
5904
  if (!spec) {
5474
5905
  if (a.channel !== "demo")
@@ -5525,7 +5956,7 @@ async function validateKit(kitPath, opts = {}) {
5525
5956
  // src/plugin/platform.ts
5526
5957
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5527
5958
  import { homedir } from "os";
5528
- import { join as join8 } from "path";
5959
+ import { join as join9 } from "path";
5529
5960
  function platformOrigin(flags = {}) {
5530
5961
  const legacyEnv = process.env.VOS_API_BASE?.trim();
5531
5962
  const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
@@ -5558,7 +5989,7 @@ function parseVosId(input) {
5558
5989
  function deriveSlug(title) {
5559
5990
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
5560
5991
  }
5561
- var CREDENTIALS_PATH = join8(homedir(), ".config", "vos", "credentials");
5992
+ var CREDENTIALS_PATH = join9(homedir(), ".config", "vos", "credentials");
5562
5993
  function resolveCredential(explicit) {
5563
5994
  const flag = explicit?.trim();
5564
5995
  if (flag) return flag;
@@ -5581,7 +6012,7 @@ function requireCredential(explicit) {
5581
6012
  return key;
5582
6013
  }
5583
6014
  function writeCredential(key) {
5584
- mkdirSync(join8(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
6015
+ mkdirSync(join9(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
5585
6016
  writeFileSync(CREDENTIALS_PATH, `${key.trim()}
5586
6017
  `, { mode: 384 });
5587
6018
  return CREDENTIALS_PATH;
@@ -5625,7 +6056,7 @@ function readJsonFile(file) {
5625
6056
  }
5626
6057
  }
5627
6058
  function readSyncState(dir) {
5628
- const own = readJsonFile(join8(dir, SYNC_STATE_NAME));
6059
+ const own = readJsonFile(join9(dir, SYNC_STATE_NAME));
5629
6060
  if (own && typeof own.vosId === "string") {
5630
6061
  return {
5631
6062
  vosId: own.vosId,
@@ -5636,7 +6067,7 @@ function readSyncState(dir) {
5636
6067
  ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
5637
6068
  };
5638
6069
  }
5639
- const push = readJsonFile(join8(dir, "push.json"));
6070
+ const push = readJsonFile(join9(dir, "push.json"));
5640
6071
  if (push && typeof push.vosId === "string") {
5641
6072
  return {
5642
6073
  vosId: push.vosId,
@@ -5644,7 +6075,7 @@ function readSyncState(dir) {
5644
6075
  ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
5645
6076
  };
5646
6077
  }
5647
- const meta = readJsonFile(join8(dir, "meta.json"));
6078
+ const meta = readJsonFile(join9(dir, "meta.json"));
5648
6079
  if (meta && typeof meta.id === "string") {
5649
6080
  return {
5650
6081
  vosId: meta.id,
@@ -5662,7 +6093,7 @@ function writeSyncState(dir, patch) {
5662
6093
  ...patch
5663
6094
  };
5664
6095
  writeFileSync(
5665
- join8(dir, SYNC_STATE_NAME),
6096
+ join9(dir, SYNC_STATE_NAME),
5666
6097
  `${JSON.stringify(merged, null, 2)}
5667
6098
  `
5668
6099
  );
@@ -5683,7 +6114,7 @@ function formatChanges(changes) {
5683
6114
 
5684
6115
  // src/plugin/recorder.ts
5685
6116
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5686
- import { join as join9 } from "path";
6117
+ import { join as join10 } from "path";
5687
6118
 
5688
6119
  // src/plugin/recordingCap.ts
5689
6120
  var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
@@ -5771,7 +6202,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5771
6202
  cdp.on("Page.screencastFrame", (ev) => {
5772
6203
  const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
5773
6204
  const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
5774
- writeFileSync2(join9(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6205
+ writeFileSync2(join10(paths.framesDir, file), Buffer.from(ev.data, "base64"));
5775
6206
  frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
5776
6207
  cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
5777
6208
  });
@@ -5994,7 +6425,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5994
6425
  await sleep(200);
5995
6426
  const pageTitle = await page.title().catch(() => "");
5996
6427
  await context.close();
5997
- const firstFrame = frames[0] ? jpegDims(readFileSync3(join9(paths.framesDir, frames[0].file))) : null;
6428
+ const firstFrame = frames[0] ? jpegDims(readFileSync3(join10(paths.framesDir, frames[0].file))) : null;
5998
6429
  const meta = {
5999
6430
  dpr: 1,
6000
6431
  zoom: 1,
@@ -6102,9 +6533,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
6102
6533
  }
6103
6534
 
6104
6535
  // src/plugin/plan.ts
6105
- import { existsSync as existsSync8 } from "fs";
6106
- import { readFile as readFile9 } from "fs/promises";
6107
- import { join as join10 } from "path";
6536
+ import { existsSync as existsSync9 } from "fs";
6537
+ import { readFile as readFile10 } from "fs/promises";
6538
+ import { join as join11 } from "path";
6108
6539
  import {
6109
6540
  DEFAULT_FRAME_STYLE,
6110
6541
  STYLE_FIELDS,
@@ -6297,10 +6728,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
6297
6728
  // src/plugin/plan.ts
6298
6729
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6299
6730
  async function readDigestActivity(dir) {
6300
- const file = join10(dir, "digest", "digest.json");
6301
- if (!existsSync8(file)) return null;
6731
+ const file = join11(dir, "digest", "digest.json");
6732
+ if (!existsSync9(file)) return null;
6302
6733
  try {
6303
- const d = JSON.parse(await readFile9(file, "utf8"));
6734
+ const d = JSON.parse(await readFile10(file, "utf8"));
6304
6735
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
6305
6736
  } catch {
6306
6737
  return null;
@@ -6413,16 +6844,16 @@ async function planTake(dir, opts = {}) {
6413
6844
 
6414
6845
  // src/plugin/sync.ts
6415
6846
  import { createHash } from "crypto";
6416
- import { existsSync as existsSync10 } from "fs";
6417
- import { readFile as readFile10 } from "fs/promises";
6847
+ import { existsSync as existsSync11 } from "fs";
6848
+ import { readFile as readFile11 } from "fs/promises";
6418
6849
  import { createInterface } from "readline/promises";
6419
- import { basename, join as join13 } from "path";
6850
+ import { basename, join as join14 } from "path";
6420
6851
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
6421
6852
 
6422
6853
  // src/plugin/media.ts
6423
- import { createWriteStream, existsSync as existsSync9 } from "fs";
6424
- import { writeFile as writeFile8 } from "fs/promises";
6425
- import { join as join11 } from "path";
6854
+ import { createWriteStream, existsSync as existsSync10 } from "fs";
6855
+ import { writeFile as writeFile9 } from "fs/promises";
6856
+ import { join as join12 } from "path";
6426
6857
  import { Readable } from "stream";
6427
6858
  import { pipeline } from "stream/promises";
6428
6859
  var MIC_NAME = "mic.webm";
@@ -6443,8 +6874,8 @@ async function pullMedia(ctx, dir, doc, log) {
6443
6874
  const url = doc.source[key];
6444
6875
  const assetId = assetIdOf(url);
6445
6876
  if (!assetId) continue;
6446
- const target = join11(dir, file);
6447
- if (existsSync9(target)) {
6877
+ const target = join12(dir, file);
6878
+ if (existsSync10(target)) {
6448
6879
  result.kept.push(file);
6449
6880
  } else {
6450
6881
  const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
@@ -6466,17 +6897,17 @@ async function pullMedia(ctx, dir, doc, log) {
6466
6897
  }
6467
6898
  doc.source[key] = file;
6468
6899
  }
6469
- const metaPath = join11(dir, "meta.json");
6470
- if (!existsSync9(metaPath)) await writeJson(metaPath, doc.source.meta, true);
6471
- const cursorPath = join11(dir, "cursor.json");
6472
- if (!existsSync9(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
6473
- await writeFile8(join11(dir, "doc.json"), JSON.stringify(doc, null, 2));
6900
+ const metaPath = join12(dir, "meta.json");
6901
+ if (!existsSync10(metaPath)) await writeJson(metaPath, doc.source.meta, true);
6902
+ const cursorPath = join12(dir, "cursor.json");
6903
+ if (!existsSync10(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
6904
+ await writeFile9(join12(dir, "doc.json"), JSON.stringify(doc, null, 2));
6474
6905
  return result;
6475
6906
  }
6476
6907
 
6477
6908
  // src/plugin/folder.ts
6478
- import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
6479
- import { join as join12 } from "path";
6909
+ import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
6910
+ import { join as join13 } from "path";
6480
6911
  import { recipeHints } from "@vosjs/shared/frontmatter";
6481
6912
  import { migrateHostedDoc } from "@vosjs/studio-core";
6482
6913
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
@@ -6652,25 +7083,25 @@ async function cmdPull(argv) {
6652
7083
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
6653
7084
  const payload = res.body;
6654
7085
  const out = strFlag(flags, "out") ?? folder.slug;
6655
- await mkdir6(join12(out, "recipes"), { recursive: true });
7086
+ await mkdir7(join13(out, "recipes"), { recursive: true });
6656
7087
  const lines = [];
6657
7088
  const recipes = [];
6658
7089
  for (const rec of payload.recipes) {
6659
- const path = join12(out, "recipes", safeName(rec.filename));
6660
- await writeFile9(path, rec.body ?? "");
7090
+ const path = join13(out, "recipes", safeName(rec.filename));
7091
+ await writeFile10(path, rec.body ?? "");
6661
7092
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
6662
7093
  }
6663
7094
  for (const rec of payload.inheritedRecipes) {
6664
7095
  const from = safeName(rec.folderSlug ?? "inherited");
6665
- await mkdir6(join12(out, "recipes", "_inherited", from), { recursive: true });
6666
- const path = join12(
7096
+ await mkdir7(join13(out, "recipes", "_inherited", from), { recursive: true });
7097
+ const path = join13(
6667
7098
  out,
6668
7099
  "recipes",
6669
7100
  "_inherited",
6670
7101
  from,
6671
7102
  safeName(rec.filename)
6672
7103
  );
6673
- await writeFile9(path, rec.body ?? "");
7104
+ await writeFile10(path, rec.body ?? "");
6674
7105
  recipes.push({
6675
7106
  path,
6676
7107
  line: `${recipeLine(rec.filename, rec.body ?? "")} [inherited from ${rec.folderName ?? from}]`
@@ -6678,15 +7109,15 @@ async function cmdPull(argv) {
6678
7109
  }
6679
7110
  const members = [];
6680
7111
  for (const v of payload.voses) {
6681
- const dir = join12(out, "members", safeName(v.slug || v.title || v.id));
6682
- await mkdir6(dir, { recursive: true });
7112
+ const dir = join13(out, "members", safeName(v.slug || v.title || v.id));
7113
+ await mkdir7(dir, { recursive: true });
6683
7114
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
6684
7115
  if (cfg.status !== 200) {
6685
7116
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
6686
7117
  continue;
6687
7118
  }
6688
- await writeFile9(
6689
- join12(dir, "config.json"),
7119
+ await writeFile10(
7120
+ join13(dir, "config.json"),
6690
7121
  JSON.stringify(cfg.body.config, null, 2)
6691
7122
  );
6692
7123
  let take = false;
@@ -6697,8 +7128,8 @@ async function cmdPull(argv) {
6697
7128
  { key }
6698
7129
  );
6699
7130
  if (doc.status === 200) {
6700
- await writeFile9(
6701
- join12(dir, "doc.json"),
7131
+ await writeFile10(
7132
+ join13(dir, "doc.json"),
6702
7133
  JSON.stringify(doc.body, null, 2)
6703
7134
  );
6704
7135
  take = true;
@@ -6815,7 +7246,7 @@ async function pushTake(dir, flags, r) {
6815
7246
  throw new Error(`doc.json fails lint:
6816
7247
  ${lint.problems.join("\n ")}`);
6817
7248
  }
6818
- if (!existsSync10(take.paths.recording)) {
7249
+ if (!existsSync11(take.paths.recording)) {
6819
7250
  throw new Error("no recording.webm in this take");
6820
7251
  }
6821
7252
  const ctx = apiContext(flags);
@@ -6847,7 +7278,7 @@ async function pushTake(dir, flags, r) {
6847
7278
  throw new Error("push cancelled");
6848
7279
  }
6849
7280
  }
6850
- const bytes = await readFile10(take.paths.recording);
7281
+ const bytes = await readFile11(take.paths.recording);
6851
7282
  const hash = createHash("sha256").update(bytes).digest("hex");
6852
7283
  r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
6853
7284
  const upload = await api(ctx, "/assets/recording", {
@@ -6998,9 +7429,9 @@ async function pullTake(dir, flags, r) {
6998
7429
  };
6999
7430
  }
7000
7431
  let media2;
7001
- if (flags.media && existsSync10(join13(dir, "doc.json"))) {
7432
+ if (flags.media && existsSync11(join14(dir, "doc.json"))) {
7002
7433
  const local = JSON.parse(
7003
- await readFile10(join13(dir, "doc.json"), "utf8")
7434
+ await readFile11(join14(dir, "doc.json"), "utf8")
7004
7435
  );
7005
7436
  media2 = await pullMedia(ctx, dir, local, r.log);
7006
7437
  }
@@ -7060,10 +7491,10 @@ async function pullTake(dir, flags, r) {
7060
7491
  }
7061
7492
  const hostedDoc = migrateHostedDoc2(docRes.json);
7062
7493
  const doc = hostedDoc;
7063
- if (existsSync10(join13(dir, RECORDING_NAME))) {
7494
+ if (existsSync11(join14(dir, RECORDING_NAME))) {
7064
7495
  doc.source.videoKey = RECORDING_NAME;
7065
7496
  }
7066
- await writeJson(join13(dir, "doc.json"), doc, true);
7497
+ await writeJson(join14(dir, "doc.json"), doc, true);
7067
7498
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
7068
7499
  const versions = await api(ctx, `/vos/${vosId}/versions`);
7069
7500
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -7082,10 +7513,10 @@ async function pullTake(dir, flags, r) {
7082
7513
  }
7083
7514
 
7084
7515
  // src/plugin/program.ts
7085
- import { existsSync as existsSync11 } from "fs";
7086
- import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
7516
+ import { existsSync as existsSync12 } from "fs";
7517
+ import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
7087
7518
  import { createInterface as createInterface2 } from "readline/promises";
7088
- import { basename as basename2, dirname as dirname3, join as join14 } from "path";
7519
+ import { basename as basename2, dirname as dirname3, join as join15 } from "path";
7089
7520
  import {
7090
7521
  CURRENT_CONFIG_VERSION,
7091
7522
  migrateConfig,
@@ -7255,8 +7686,8 @@ function preflightConfig(parsed) {
7255
7686
  }
7256
7687
  function resolveConfigPath(target) {
7257
7688
  if (target.endsWith(".json")) return target;
7258
- const inDir = join14(target, "config.json");
7259
- if (existsSync11(inDir)) return inDir;
7689
+ const inDir = join15(target, "config.json");
7690
+ if (existsSync12(inDir)) return inDir;
7260
7691
  throw new UsageError(
7261
7692
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
7262
7693
  );
@@ -7282,9 +7713,9 @@ async function cmdFetch(argv) {
7282
7713
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
7283
7714
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
7284
7715
  const out = strFlag(flags, "out") ?? slug;
7285
- await mkdir7(out, { recursive: true });
7286
- await writeFile10(
7287
- join14(out, "config.json"),
7716
+ await mkdir8(out, { recursive: true });
7717
+ await writeFile11(
7718
+ join15(out, "config.json"),
7288
7719
  JSON.stringify(cfg.body.config, null, 2)
7289
7720
  );
7290
7721
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -7309,12 +7740,12 @@ async function cmdFetch(argv) {
7309
7740
  const hosted = migrateHostedDoc3(doc.body);
7310
7741
  const { config: own } = await writeProgramDoc(out, hosted);
7311
7742
  if (own) {
7312
- await writeFile10(join14(out, "config.json"), JSON.stringify(own, null, 2));
7743
+ await writeFile11(join15(out, "config.json"), JSON.stringify(own, null, 2));
7313
7744
  }
7314
7745
  } else if (doc.status === 200) {
7315
7746
  take = true;
7316
7747
  const hosted = migrateHostedDoc3(doc.body);
7317
- await writeFile10(join14(out, "doc.json"), JSON.stringify(hosted, null, 2));
7748
+ await writeFile11(join15(out, "doc.json"), JSON.stringify(hosted, null, 2));
7318
7749
  if (flags.media === true) {
7319
7750
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
7320
7751
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
@@ -7426,7 +7857,7 @@ async function cmdPushProgram(argv) {
7426
7857
  api: strFlag(flags, "api")
7427
7858
  });
7428
7859
  const dir = dirname3(source);
7429
- const parsed = JSON.parse(await readFile11(source, "utf8"));
7860
+ const parsed = JSON.parse(await readFile12(source, "utf8"));
7430
7861
  const pre = preflightConfig(parsed);
7431
7862
  if (!pre.ok || !pre.config) {
7432
7863
  for (const issue of pre.issues) r.log(`error ${issue}`);
@@ -7659,10 +8090,10 @@ async function cmdPullProgram(argv) {
7659
8090
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
7660
8091
  if (cfg.status !== 200)
7661
8092
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
7662
- const configPath = join14(dir, "config.json");
8093
+ const configPath = join15(dir, "config.json");
7663
8094
  let backedUp = false;
7664
- if (existsSync11(configPath)) {
7665
- await writeFile10(join14(dir, "config.backup.json"), await readFile11(configPath));
8095
+ if (existsSync12(configPath)) {
8096
+ await writeFile11(join15(dir, "config.backup.json"), await readFile12(configPath));
7666
8097
  backedUp = true;
7667
8098
  }
7668
8099
  let headConfig = cfg.body.config;
@@ -7681,7 +8112,7 @@ async function cmdPullProgram(argv) {
7681
8112
  if (own) headConfig = own;
7682
8113
  }
7683
8114
  }
7684
- await writeFile10(configPath, JSON.stringify(headConfig, null, 2));
8115
+ await writeFile11(configPath, JSON.stringify(headConfig, null, 2));
7685
8116
  writeSyncState(dir, {
7686
8117
  vosId,
7687
8118
  versionId: typeof head.id === "string" ? head.id : since
@@ -7695,7 +8126,7 @@ async function cmdPullProgram(argv) {
7695
8126
  protected: protectedIds,
7696
8127
  changes,
7697
8128
  out: configPath,
7698
- backup: backedUp ? join14(dir, "config.backup.json") : null
8129
+ backup: backedUp ? join15(dir, "config.backup.json") : null
7699
8130
  },
7700
8131
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
7701
8132
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -7752,9 +8183,9 @@ function isTakeDir(target) {
7752
8183
  return directoryKind(target) === "take";
7753
8184
  }
7754
8185
  async function readProgramDoc(dir, config) {
7755
- const docPath = join14(dir, "doc.json");
7756
- if (!existsSync11(docPath)) return null;
7757
- const parsed = JSON.parse(await readFile11(docPath, "utf8"));
8186
+ const docPath = join15(dir, "doc.json");
8187
+ if (!existsSync12(docPath)) return null;
8188
+ const parsed = JSON.parse(await readFile12(docPath, "utf8"));
7758
8189
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
7759
8190
  return null;
7760
8191
  const raw = parsed;
@@ -7765,7 +8196,7 @@ async function writeProgramDoc(dir, hosted) {
7765
8196
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
7766
8197
  const { config, ...rest } = program;
7767
8198
  const onDisk = { ...hosted, program: rest };
7768
- await writeFile10(join14(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
8199
+ await writeFile11(join15(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
7769
8200
  return {
7770
8201
  config: config && typeof config === "object" ? config : null
7771
8202
  };
@@ -7972,8 +8403,8 @@ async function cmdRecipe(argv) {
7972
8403
  }
7973
8404
 
7974
8405
  // src/plugin/brand.ts
7975
- import { writeFile as writeFile11 } from "fs/promises";
7976
- import { resolve as resolve5 } from "path";
8406
+ import { writeFile as writeFile12 } from "fs/promises";
8407
+ import { resolve as resolve6 } from "path";
7977
8408
  import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
7978
8409
  import { lookKindForGround } from "@vosjs/studio-core";
7979
8410
  var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
@@ -8177,7 +8608,14 @@ function composeBrand(input) {
8177
8608
  const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
8178
8609
  const fontBody = designBody ?? firstFamily2(w.body.fontFamily);
8179
8610
  p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
8180
- const logoUrl = design?.logos.find((u) => /wordmark|logo/i.test(u)) ?? null;
8611
+ const isIcon = (u) => /favicon|apple-touch|icon/i.test(u);
8612
+ const onDark = (u) => /on-dark|white/i.test(u);
8613
+ const logoUrl = design?.logos.find(
8614
+ (u) => /wordmark|logo|mark/i.test(u) && !isIcon(u) && !onDark(u)
8615
+ ) ?? null;
8616
+ const logoOnDarkUrl = design?.logos.find(
8617
+ (u) => /wordmark|logo|mark/i.test(u) && !isIcon(u) && onDark(u)
8618
+ ) ?? null;
8181
8619
  const iconUrl = w.icons.find((u) => /apple-touch/i.test(u)) ?? (w.icons.length ? w.icons[0] : null);
8182
8620
  p.logoUrl = logoUrl ? `linked from design.md` : `none linked; the icon stands in`;
8183
8621
  p.iconUrl = iconUrl ? `the page's icon link` : `no icon link`;
@@ -8196,6 +8634,7 @@ function composeBrand(input) {
8196
8634
  fontDisplay,
8197
8635
  fontBody,
8198
8636
  logoUrl: logoUrl ?? iconUrl,
8637
+ logoOnDarkUrl,
8199
8638
  iconUrl,
8200
8639
  ogImage: w.ogImage,
8201
8640
  wordmark,
@@ -8225,6 +8664,7 @@ function renderBrandMd(c, claim) {
8225
8664
  `fontDisplay: ${q(k.fontDisplay)}`,
8226
8665
  `fontBody: ${q(k.fontBody)}`,
8227
8666
  `logoUrl: ${q(k.logoUrl)}`,
8667
+ `logoOnDarkUrl: ${q(k.logoOnDarkUrl)}`,
8228
8668
  `iconUrl: ${q(k.iconUrl)}`,
8229
8669
  `ogImage: ${q(k.ogImage)}`,
8230
8670
  `wordmark: ${q(k.wordmark)}`,
@@ -8250,7 +8690,7 @@ function renderBrandMd(c, claim) {
8250
8690
  "",
8251
8691
  "## Use",
8252
8692
  "",
8253
- `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. A take's frame ground is a gradient of \`accent\` toward \`bgC\` unless the brand is paper, in which case \`bgB\` toward \`bgC\`.`
8693
+ `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\`.`
8254
8694
  ];
8255
8695
  if (c.avoid.length) {
8256
8696
  lines.push("", "## Avoid (the site says)", "");
@@ -8281,7 +8721,7 @@ async function cmdBrand(argv) {
8281
8721
  throw new UsageError(`not a URL: ${raw}`);
8282
8722
  }
8283
8723
  const r = createReporter(flags.json === true);
8284
- const out = resolve5(strFlag(flags, "out") ?? "BRAND.md");
8724
+ const out = resolve6(strFlag(flags, "out") ?? "BRAND.md");
8285
8725
  r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
8286
8726
  const [designText, llmsText] = await Promise.all([
8287
8727
  fetchText(`${origin}/design.md`),
@@ -8315,7 +8755,7 @@ async function cmdBrand(argv) {
8315
8755
  composition,
8316
8756
  llms?.claim ?? design?.description ?? null
8317
8757
  );
8318
- await writeFile11(out, md);
8758
+ await writeFile12(out, md);
8319
8759
  const k = composition.kit;
8320
8760
  r.done(
8321
8761
  {
@@ -8333,8 +8773,8 @@ async function cmdBrand(argv) {
8333
8773
  }
8334
8774
 
8335
8775
  // src/plugin/agentBrowser.ts
8336
- import { writeFile as writeFile12 } from "fs/promises";
8337
- import { resolve as resolve6 } from "path";
8776
+ import { writeFile as writeFile13 } from "fs/promises";
8777
+ import { resolve as resolve7 } from "path";
8338
8778
  function parseAgentBrowserLog(text) {
8339
8779
  const records = [];
8340
8780
  const problems = [];
@@ -8804,7 +9244,7 @@ async function cmdActions(argv) {
8804
9244
  );
8805
9245
  }
8806
9246
  const r = createReporter(flags.json === true);
8807
- const out = resolve6(strFlag(flags, "out") ?? "actions.json");
9247
+ const out = resolve7(strFlag(flags, "out") ?? "actions.json");
8808
9248
  const viewportFlag = strFlag(flags, "viewport");
8809
9249
  let viewport;
8810
9250
  if (viewportFlag) {
@@ -8812,8 +9252,8 @@ async function cmdActions(argv) {
8812
9252
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
8813
9253
  viewport = { width: Number(m[1]), height: Number(m[2]) };
8814
9254
  }
8815
- const { readFile: readFile13 } = await import("fs/promises");
8816
- const text = await readFile13(resolve6(input), "utf8");
9255
+ const { readFile: readFile14 } = await import("fs/promises");
9256
+ const text = await readFile14(resolve7(input), "utf8");
8817
9257
  const { records, problems } = parseAgentBrowserLog(text);
8818
9258
  for (const p of problems) r.log(` ${p}`);
8819
9259
  const result = convertAgentBrowser(records, {
@@ -8830,7 +9270,7 @@ async function cmdActions(argv) {
8830
9270
  );
8831
9271
  return EXIT_ERROR;
8832
9272
  }
8833
- await writeFile12(out, `${JSON.stringify(result.actions, null, 2)}
9273
+ await writeFile13(out, `${JSON.stringify(result.actions, null, 2)}
8834
9274
  `);
8835
9275
  r.done(
8836
9276
  { ok: true, out, url: result.actions.url ?? null, ...summary(result) },
@@ -9101,7 +9541,7 @@ lint-gated, so a bad override fails like a bad doc.json):
9101
9541
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
9102
9542
  `;
9103
9543
  async function loadActions(file) {
9104
- const raw = JSON.parse(await readFile12(file, "utf8"));
9544
+ const raw = JSON.parse(await readFile13(file, "utf8"));
9105
9545
  const errors = validateActions(raw);
9106
9546
  if (errors.length)
9107
9547
  throw new UsageError(`invalid actions file:
@@ -9157,16 +9597,16 @@ async function cmdRecord(argv) {
9157
9597
  const url = strFlag(flags, "url") ?? actions.url;
9158
9598
  if (!url)
9159
9599
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
9160
- const outDir = resolve7(strFlag(flags, "out") ?? "take");
9600
+ const outDir = resolve8(strFlag(flags, "out") ?? "take");
9161
9601
  const backdrop = await takeBackdrop(flags, r);
9162
9602
  const maxDurationSeconds = await maxDuration(flags, r);
9163
- if (existsSync12(join15(outDir, "meta.json"))) {
9603
+ if (existsSync13(join16(outDir, "meta.json"))) {
9164
9604
  const { prevDoc, kept } = await prepareReRecord(outDir);
9165
9605
  r.log(
9166
9606
  `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" : "")
9167
9607
  );
9168
9608
  }
9169
- await mkdir8(outDir, { recursive: true });
9609
+ await mkdir9(outDir, { recursive: true });
9170
9610
  const paths = await ensureTakeDir(outDir);
9171
9611
  const browser = await launchBrowser();
9172
9612
  try {
@@ -9207,7 +9647,7 @@ async function cmdRecord(argv) {
9207
9647
  },
9208
9648
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
9209
9649
  ${(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${rec.freezePct >= 25 ? " \u26A0 keep motion in frame or trim" : ""}${skippedNote}
9210
- Next: edit ${join15(outDir, "doc.json")} (optional), then: vos render ${outDir}`
9650
+ Next: edit ${join16(outDir, "doc.json")} (optional), then: vos render ${outDir}`
9211
9651
  );
9212
9652
  return strictFail ? EXIT_USAGE : EXIT_OK;
9213
9653
  } finally {
@@ -9231,25 +9671,25 @@ async function cmdCreate2(argv) {
9231
9671
  const url = strFlag(flags, "url") ?? actions.url;
9232
9672
  if (!url)
9233
9673
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
9234
- const outDir = resolve7(strFlag(flags, "out") ?? "take");
9674
+ const outDir = resolve8(strFlag(flags, "out") ?? "take");
9235
9675
  const fmtRaw = strFlag(flags, "format") ?? "webm";
9236
9676
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
9237
9677
  throw new UsageError("--format must be webm or mp4");
9238
9678
  const format = fmtRaw;
9239
- const out = positionals[0] ?? join15(outDir, `out.${format}`);
9679
+ const out = positionals[0] ?? join16(outDir, `out.${format}`);
9240
9680
  const parallel = numFlag(flags, "parallel", 1);
9241
9681
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
9242
9682
  throw new UsageError("--parallel expects an integer between 1 and 16");
9243
9683
  }
9244
9684
  const backdrop = await takeBackdrop(flags, r);
9245
9685
  const maxDurationSeconds = await maxDuration(flags, r);
9246
- if (existsSync12(join15(outDir, "meta.json"))) {
9686
+ if (existsSync13(join16(outDir, "meta.json"))) {
9247
9687
  const { prevDoc, kept } = await prepareReRecord(outDir);
9248
9688
  r.log(
9249
9689
  `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" : "")
9250
9690
  );
9251
9691
  }
9252
- await mkdir8(outDir, { recursive: true });
9692
+ await mkdir9(outDir, { recursive: true });
9253
9693
  const paths = await ensureTakeDir(outDir);
9254
9694
  const browser = await launchBrowser();
9255
9695
  try {
@@ -9330,24 +9770,24 @@ async function cmdPlan(argv) {
9330
9770
  );
9331
9771
  const r = createReporter(flags.json === true);
9332
9772
  if (flags.fresh === true) {
9333
- await rm4(join15(dir, "doc.json"), { force: true });
9773
+ await rm4(join16(dir, "doc.json"), { force: true });
9334
9774
  r.log("note: --fresh discarded the existing doc.json");
9335
9775
  }
9336
9776
  let reuse;
9337
9777
  if (flags.reuse === true) {
9338
- const from = resolve7(strFlag(flags, "from") ?? join15(dir, PREV_DOC_NAME));
9339
- if (!existsSync12(from)) {
9778
+ const from = resolve8(strFlag(flags, "from") ?? join16(dir, PREV_DOC_NAME));
9779
+ if (!existsSync13(from)) {
9340
9780
  throw new UsageError(
9341
9781
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
9342
9782
  );
9343
9783
  }
9344
9784
  reuse = {
9345
9785
  from,
9346
- doc: JSON.parse(await readFile12(from, "utf8"))
9786
+ doc: JSON.parse(await readFile13(from, "utf8"))
9347
9787
  };
9348
9788
  }
9349
9789
  const style = await resolveStyleRef(flags);
9350
- const backdrop = existsSync12(join15(dir, "doc.json")) ? null : await takeBackdrop(flags, r);
9790
+ const backdrop = existsSync13(join16(dir, "doc.json")) ? null : await takeBackdrop(flags, r);
9351
9791
  const s = await planTake(dir, {
9352
9792
  ...style ? { style } : {},
9353
9793
  ...reuse ? { reuse } : {},
@@ -9359,7 +9799,7 @@ async function cmdPlan(argv) {
9359
9799
  ${s.reuse.flagged.join("\n ")}` : "") : "";
9360
9800
  r.done(
9361
9801
  {
9362
- take: resolve7(dir),
9802
+ take: resolve8(dir),
9363
9803
  fresh: s.fresh,
9364
9804
  cursorKept: s.cursorKept,
9365
9805
  zoomAuto: s.zoomAuto,
@@ -9369,7 +9809,7 @@ async function cmdPlan(argv) {
9369
9809
  ...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
9370
9810
  ...s.reuse ? { reuse: s.reuse } : {}
9371
9811
  },
9372
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join15(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${reuseLines}`
9812
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join16(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${reuseLines}`
9373
9813
  );
9374
9814
  return EXIT_OK;
9375
9815
  }
@@ -9389,7 +9829,7 @@ async function cmdRender(argv) {
9389
9829
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
9390
9830
  throw new UsageError("--format must be webm or mp4");
9391
9831
  const format = fmtRaw;
9392
- const out = positionals[1] ?? join15(dir, `out.${format}`);
9832
+ const out = positionals[1] ?? join16(dir, `out.${format}`);
9393
9833
  const parallel = numFlag(flags, "parallel", 1);
9394
9834
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
9395
9835
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -9574,10 +10014,10 @@ async function cmdDeliver(argv) {
9574
10014
  } else if (posterRef !== void 0 && templateByName(posterRef)) {
9575
10015
  poster = { from: `template ${posterRef}`, config: templateByName(posterRef) };
9576
10016
  } else if (posterRef !== void 0) {
9577
- if (existsSync12(posterRef)) {
10017
+ if (existsSync13(posterRef)) {
9578
10018
  poster = {
9579
- from: resolve7(posterRef),
9580
- config: JSON.parse(await readFile12(posterRef, "utf8"))
10019
+ from: resolve8(posterRef),
10020
+ config: JSON.parse(await readFile13(posterRef, "utf8"))
9581
10021
  };
9582
10022
  } else {
9583
10023
  const origin = platformOrigin({
@@ -9703,14 +10143,14 @@ Store uploads stay manual: hand the human this directory and the manifest.`
9703
10143
  async function resolveStyleRef(flags) {
9704
10144
  const styleRef = strFlag(flags, "style");
9705
10145
  if (!styleRef) return null;
9706
- const file = existsSync12(styleRef) ? resolve7(
10146
+ const file = existsSync13(styleRef) ? resolve8(
9707
10147
  styleRef,
9708
- existsSync12(join15(styleRef, "doc.json")) ? "doc.json" : ""
10148
+ existsSync13(join16(styleRef, "doc.json")) ? "doc.json" : ""
9709
10149
  ) : null;
9710
- if (file && existsSync12(file)) {
10150
+ if (file && existsSync13(file)) {
9711
10151
  return {
9712
10152
  from: file,
9713
- doc: JSON.parse(await readFile12(file, "utf8"))
10153
+ doc: JSON.parse(await readFile13(file, "utf8"))
9714
10154
  };
9715
10155
  }
9716
10156
  const origin = platformOrigin({
@@ -9747,7 +10187,7 @@ async function cmdDigest(argv) {
9747
10187
  const take = await loadTake(dir);
9748
10188
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
9749
10189
  const transcriptPath = strFlag(flags, "transcript");
9750
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile12(transcriptPath, "utf8"))) : null;
10190
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile13(transcriptPath, "utf8"))) : null;
9751
10191
  const style = await resolveStyleRef(flags);
9752
10192
  const noFrames = flags["no-frames"] === true;
9753
10193
  let browser = null;
@@ -9778,7 +10218,7 @@ async function cmdDigest(argv) {
9778
10218
  kinds,
9779
10219
  frames: d.images.full,
9780
10220
  crops: d.images.crop,
9781
- sheet: d.images.sheet ? join15(result.outDir, d.images.sheet) : null,
10221
+ sheet: d.images.sheet ? join16(result.outDir, d.images.sheet) : null,
9782
10222
  scenes: kinds.scene ?? 0,
9783
10223
  sourceDuration: d.take.sourceDuration,
9784
10224
  outputDuration: d.take.outputDuration,
@@ -9802,14 +10242,14 @@ async function cmdOpen(argv) {
9802
10242
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
9803
10243
  const r = createReporter(flags.json === true);
9804
10244
  const take = await loadTake(dir);
9805
- if (!existsSync12(take.paths.recording)) {
10245
+ if (!existsSync13(take.paths.recording)) {
9806
10246
  throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
9807
10247
  }
9808
10248
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
9809
10249
  /\/+$/,
9810
10250
  ""
9811
10251
  );
9812
- const server = await startTakeServer(resolve7(dir), {});
10252
+ const server = await startTakeServer(resolve8(dir), {});
9813
10253
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
9814
10254
  r.event({ event: "open", server: server.base, url });
9815
10255
  r.log(`take served at ${server.base}`);
@@ -9843,11 +10283,11 @@ async function cmdJudge(argv) {
9843
10283
  "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)"
9844
10284
  );
9845
10285
  const r = createReporter(flags.json === true);
9846
- const kitFile = target.endsWith("kit.json") ? target : join15(target, "kit.json");
9847
- if (!existsSync12(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
9848
- if (!existsSync12(against)) throw new UsageError(`${against}: no reference manifest`);
10286
+ const kitFile = target.endsWith("kit.json") ? target : join16(target, "kit.json");
10287
+ if (!existsSync13(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
10288
+ if (!existsSync13(against)) throw new UsageError(`${against}: no reference manifest`);
9849
10289
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
9850
- const verdicts = JSON.parse(await readFile12(result.verdictFile, "utf8")).verdicts;
10290
+ const verdicts = JSON.parse(await readFile13(result.verdictFile, "utf8")).verdicts;
9851
10291
  const rate = winRate(verdicts);
9852
10292
  const lines = result.sheets.map(
9853
10293
  (s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
@@ -9867,7 +10307,7 @@ async function cmdValidate(argv) {
9867
10307
  const target = positionals[0];
9868
10308
  if (!target) throw new UsageError("vos validate <actions.json|take>");
9869
10309
  const r = createReporter(flags.json === true);
9870
- const kitTarget = target.endsWith("kit.json") ? target : existsSync12(join15(target, "kit.json")) && !isTakeDir(target) ? join15(target, "kit.json") : null;
10310
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync13(join16(target, "kit.json")) && !isTakeDir(target) ? join16(target, "kit.json") : null;
9871
10311
  if (kitTarget) {
9872
10312
  const picture = flags.picture === true;
9873
10313
  const verdict = await validateKit(kitTarget, { picture });
@@ -9899,9 +10339,9 @@ async function cmdValidate(argv) {
9899
10339
  r.done({ valid: true, target }, `${target}: valid actions file`);
9900
10340
  return EXIT_OK;
9901
10341
  }
9902
- if (!isTakeDir(target) && existsSync12(join15(target, "config.json"))) {
10342
+ if (!isTakeDir(target) && existsSync13(join16(target, "config.json"))) {
9903
10343
  const parsed = JSON.parse(
9904
- await readFile12(join15(target, "config.json"), "utf8")
10344
+ await readFile13(join16(target, "config.json"), "utf8")
9905
10345
  );
9906
10346
  const pre = preflightConfig(parsed);
9907
10347
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -9942,7 +10382,7 @@ async function cmdValidate(argv) {
9942
10382
  const take = await loadTake(target);
9943
10383
  const problems = [];
9944
10384
  const warnings = [];
9945
- if (!existsSync12(take.paths.recording))
10385
+ if (!existsSync13(take.paths.recording))
9946
10386
  problems.push("missing recording.webm (re-run record)");
9947
10387
  if (!take.doc) problems.push("missing doc.json (run plan)");
9948
10388
  if (take.doc) {
@@ -9987,7 +10427,7 @@ async function cmdPush3(argv) {
9987
10427
  const r = createReporter(flags.json === true);
9988
10428
  const overrides = multi.override;
9989
10429
  const result = await pushTake(
9990
- resolve7(dir),
10430
+ resolve8(dir),
9991
10431
  {
9992
10432
  key: strFlag(flags, "key"),
9993
10433
  api: strFlag(flags, "api"),
@@ -10014,7 +10454,7 @@ async function cmdPull2(argv) {
10014
10454
  const dir = positionals[0] ?? ".";
10015
10455
  const r = createReporter(flags.json === true);
10016
10456
  const result = await pullTake(
10017
- resolve7(dir),
10457
+ resolve8(dir),
10018
10458
  {
10019
10459
  key: strFlag(flags, "key"),
10020
10460
  api: strFlag(flags, "api"),
@@ -10149,4 +10589,4 @@ export {
10149
10589
  convertAgentBrowser,
10150
10590
  run
10151
10591
  };
10152
- //# sourceMappingURL=chunk-HGLKXIXH.js.map
10592
+ //# sourceMappingURL=chunk-Q5NF5EN2.js.map