@vosjs/cli 0.14.0 → 0.15.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.
@@ -3950,8 +3950,9 @@ function resolveFace(family, weights) {
3950
3950
  url: fontFaceUrl(entry.slug, w),
3951
3951
  weight: w
3952
3952
  }));
3953
- return { stack: fontStack(entry), fonts };
3953
+ return { stack: fontStack(entry), category: entry.category, fonts };
3954
3954
  }
3955
+ var HOUSE_SERIF = "Fraunces";
3955
3956
  function posterValues(brand, words) {
3956
3957
  const b = brand ?? {};
3957
3958
  const bgA = b.bgA && HEX.test(b.bgA) ? b.bgA : null;
@@ -3981,9 +3982,10 @@ function posterValues(brand, words) {
3981
3982
  values.kicker = kicker ? kicker : [wordmark, release].filter(Boolean).join(" ").toUpperCase();
3982
3983
  const fonts = [];
3983
3984
  const display = resolveFace(b.fontDisplay, [600, 700]);
3984
- if (display) {
3985
- values.fontDisplay = display.stack;
3986
- fonts.push(...display.fonts);
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);
3987
3989
  }
3988
3990
  const body = resolveFace(b.fontBody, [700]);
3989
3991
  if (body) {
@@ -3993,6 +3995,89 @@ function posterValues(brand, words) {
3993
3995
  return { values, fonts, lightGround };
3994
3996
  }
3995
3997
 
3998
+ // src/plugin/stages.ts
3999
+ var HOUSE_SERIF2 = "Fraunces";
4000
+ var str = (v, fallback) => typeof v === "string" && v.trim() ? v.trim() : fallback;
4001
+ var firstFamily = (stack) => stack.split(",")[0].replace(/['"]/g, "").trim();
4002
+ function stageSplitCover(input) {
4003
+ const R = input.size.w / Math.max(1, input.size.h);
4004
+ const portrait = R < 0.87;
4005
+ const square = !portrait && R <= 1.15;
4006
+ const v = input.values;
4007
+ const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4008
+ const ink = str(v.ink, "#fff6ec");
4009
+ const inkSoft = str(v.inkSoft, ink);
4010
+ const serif = firstFamily(str(v.fontDisplay, HOUSE_SERIF2));
4011
+ const body = firstFamily(str(v.fontBody, "Lexend"));
4012
+ const headline = str(v.headline, "");
4013
+ const kicker = str(v.kicker, "");
4014
+ const brand = str(v.brand, "");
4015
+ const inset = portrait ? { left: 0.08, right: -0.08, top: 0.44, bottom: -0.1 } : square ? { left: 0.1, right: -0.1, top: 0.5, bottom: -0.12 } : { left: 0.44, right: -0.06, top: 0.3, bottom: -0.12 };
4016
+ const shot = {
4017
+ x: inset.left,
4018
+ y: inset.top,
4019
+ w: 1 - inset.left - inset.right,
4020
+ h: 1 - inset.top - inset.bottom
4021
+ };
4022
+ const set = [
4023
+ `frame.background=${JSON.stringify(ground)}`,
4024
+ "frame.backgroundMedia=null",
4025
+ "frame.fit=cover",
4026
+ `frame.inset=${JSON.stringify(inset)}`,
4027
+ 'frame.focus={"cx":0,"cy":0}',
4028
+ "frame.radius=16",
4029
+ "frame.shadow=0.45",
4030
+ "frame.shadowContact=0.25",
4031
+ "frame.border=0",
4032
+ "zoom=[]",
4033
+ `tilt=[{"id":"stage","in":0,"out":${input.sourceSeconds.toFixed(3)},"rx":3,"ry":${portrait || square ? 0 : 10}}]`,
4034
+ "cursor.visible=false",
4035
+ "cursor.clickFx.style=none"
4036
+ ];
4037
+ const designW = 1080 * input.size.w / input.size.h;
4038
+ const boxes = [];
4039
+ const clips = [];
4040
+ const column = portrait || square ? 0.84 : 0.36;
4041
+ const left = portrait || square ? 0.08 : 0.07;
4042
+ const size = portrait ? 60 : square ? 68 : 84;
4043
+ const word = (id, text, preset, family, px, y, color, extra, role) => {
4044
+ if (!text) return;
4045
+ const lines = text.split("\n");
4046
+ const longest = Math.max(...lines.map((l) => l.length));
4047
+ const widest = Math.min(column, longest * px * (preset === "title" ? 0.5 : 0.62) / designW);
4048
+ const lh = preset === "title" ? 1.05 : 1.2;
4049
+ const h = lines.length * px * lh / 1080;
4050
+ clips.push({
4051
+ id,
4052
+ kind: "text",
4053
+ text,
4054
+ preset,
4055
+ family,
4056
+ size: px,
4057
+ color,
4058
+ align: "left",
4059
+ maxWidth: column,
4060
+ lineHeight: lh,
4061
+ transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
4062
+ shadow: "none",
4063
+ start: 0,
4064
+ duration: +input.outputSeconds.toFixed(3),
4065
+ enter: "none",
4066
+ exit: "none",
4067
+ ...extra
4068
+ });
4069
+ boxes.push({ x: left, y: y - h / 2, w: widest, h, color, role, label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text });
4070
+ };
4071
+ const kickerY = portrait ? 0.1 : square ? 0.12 : 0.24;
4072
+ const titleY = portrait ? 0.24 : square ? 0.28 : 0.46;
4073
+ 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");
4077
+ set.push(`overlays=${JSON.stringify(clips)}`);
4078
+ return { set, text: boxes, shot };
4079
+ }
4080
+
3996
4081
  // src/plugin/motionPlan.ts
3997
4082
  import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
3998
4083
  var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
@@ -4218,6 +4303,7 @@ function templateForCard(d, opts) {
4218
4303
  }
4219
4304
  const config = templateByName(name);
4220
4305
  if (!config) return null;
4306
+ if (name === "split-cover") return { config, from: "stage split-cover", note, stage: true };
4221
4307
  return { config, from: `template ${name}`, note };
4222
4308
  }
4223
4309
  function lookOverrides(look, placement, size, video, opts) {
@@ -4453,6 +4539,53 @@ async function deliverTake(browser, dir, opts) {
4453
4539
  await writeFile6(join5(serveDir, "shot.png"), encodePng(baked));
4454
4540
  const shotAspect = raw.w / raw.h;
4455
4541
  for (const { d, plan } of cardPlans) {
4542
+ if (plan.stage) {
4543
+ const staged = stageSplitCover({
4544
+ size: d.px,
4545
+ values: fill.values,
4546
+ 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-"));
4551
+ try {
4552
+ const captured = await framesTake(browser, dir, {
4553
+ times: [heroTime],
4554
+ width: d.px.w,
4555
+ height: d.px.h,
4556
+ outDir: shotDir,
4557
+ overrides: {
4558
+ ...opts.overrides,
4559
+ set: [...staged.set, ...opts.overrides?.set ?? []]
4560
+ }
4561
+ });
4562
+ const to2 = join5(outDir, `${d.id}.png`);
4563
+ await rename4(captured.frames[0].file, to2);
4564
+ const bytes2 = (await stat(to2)).size;
4565
+ if (d.maxBytes !== void 0 && bytes2 > d.maxBytes) {
4566
+ skipped.push(`${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`);
4567
+ continue;
4568
+ }
4569
+ assets.push({
4570
+ channel: d.channel,
4571
+ asset: d.asset,
4572
+ destination: d.id,
4573
+ path: relative(outDir, to2),
4574
+ w: d.px.w,
4575
+ h: d.px.h,
4576
+ bytes: bytes2,
4577
+ seconds: null,
4578
+ frameTime: heroTime,
4579
+ source: "stage",
4580
+ template: "split-cover-stage",
4581
+ text: staged.text,
4582
+ shot: staged.shot
4583
+ });
4584
+ } finally {
4585
+ await rm3(shotDir, { recursive: true, force: true });
4586
+ }
4587
+ continue;
4588
+ }
4456
4589
  const problems = templateProblems(plan.config);
4457
4590
  if (problems.length) {
4458
4591
  skipped.push(
@@ -4677,7 +4810,8 @@ var RUBRIC = [
4677
4810
  "Name three ways this asset acknowledges THIS product (its data, its state, its brand), not a generic window."
4678
4811
  ];
4679
4812
  function rolesFor(asset) {
4680
- if (asset.source === "poster" && asset.template) return [asset.template];
4813
+ if ((asset.source === "poster" || asset.source === "stage") && asset.template)
4814
+ return [asset.template.replace(/-stage$/, "")];
4681
4815
  const spec = DESTINATIONS2.find((d) => d.id === asset.destination);
4682
4816
  if (spec?.kind === "video") {
4683
4817
  const portrait = spec.px.w < spec.px.h;
@@ -4844,9 +4978,11 @@ async function judgeKit(kitPath, manifestPath, outDir) {
4844
4978
  return { sheets, skipped, outDir: out, verdictFile };
4845
4979
  }
4846
4980
  function winRate(verdicts) {
4847
- const judged = verdicts.filter((v) => v.win !== null).length;
4848
- const wins = verdicts.filter((v) => v.win === true).length;
4849
- return { wins, judged, rate: judged ? wins / judged : null };
4981
+ const judgedList = verdicts.filter((v) => v.win !== null || (v.reasons?.length ?? 0) > 0);
4982
+ const wins = judgedList.filter((v) => v.win === true).length;
4983
+ const ties = judgedList.filter((v) => v.win === null).length;
4984
+ const judged = judgedList.length;
4985
+ return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
4850
4986
  }
4851
4987
 
4852
4988
  // src/plugin/kitPicture.ts
@@ -5069,13 +5205,16 @@ function duplicateFindings(stills) {
5069
5205
  }
5070
5206
  if (g.length > 1) groups.push(g);
5071
5207
  }
5072
- return groups.map((g) => ({
5073
- code: "duplicate",
5074
- severity: g.length >= 3 ? "error" : "warning",
5075
- asset: g.map((s) => s.destination).join(", "),
5076
- message: `${g.length} assets share one frame${g[0].time !== null ? ` (${g[0].time.toFixed(2)}s)` : ""}: ${g.map((s) => s.destination).join(", ")}`,
5077
- fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster template composes each card differently from one shot"
5078
- }));
5208
+ return groups.map((g) => {
5209
+ const composed = g.every((s) => stills.find((x) => x.destination === s.destination)?.composed);
5210
+ return {
5211
+ code: "duplicate",
5212
+ severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
5213
+ asset: g.map((s) => s.destination).join(", "),
5214
+ message: composed ? `${g.length} channels share one cover: ${g.map((s) => s.destination).join(", ")}` : `${g.length} assets share one frame${g[0].time !== null ? ` (${g[0].time.toFixed(2)}s)` : ""}: ${g.map((s) => s.destination).join(", ")}`,
5215
+ fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster template composes each card differently from one shot"
5216
+ };
5217
+ });
5079
5218
  }
5080
5219
  async function videoFrame(file, at2, ffmpeg) {
5081
5220
  try {
@@ -5124,7 +5263,8 @@ async function pictureChecks(assets) {
5124
5263
  destination: a.destination,
5125
5264
  hash: m.hash,
5126
5265
  time: null,
5127
- genre: a.spec?.genre
5266
+ genre: a.spec?.genre,
5267
+ composed: !!a.shot
5128
5268
  });
5129
5269
  continue;
5130
5270
  }
@@ -5328,7 +5468,7 @@ async function validateKit(kitPath, opts = {}) {
5328
5468
  text: a.text,
5329
5469
  seconds,
5330
5470
  composed: a.composed,
5331
- shot: a.source === "poster" ? a.shot : void 0
5471
+ shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0
5332
5472
  });
5333
5473
  if (!spec) {
5334
5474
  if (a.channel !== "demo")
@@ -7900,7 +8040,7 @@ function mostCommon(values) {
7900
8040
  }
7901
8041
  return best;
7902
8042
  }
7903
- function firstFamily(fontFamily) {
8043
+ function firstFamily2(fontFamily) {
7904
8044
  const first = fontFamily.split(",")[0]?.trim() ?? "";
7905
8045
  return first.replace(/^["']|["']$/g, "");
7906
8046
  }
@@ -8032,10 +8172,10 @@ function composeBrand(input) {
8032
8172
  const bgC = mixHex2(bgA, accent, 0.14);
8033
8173
  p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
8034
8174
  p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
8035
- const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily));
8175
+ const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily2(w.h1.fontFamily) : firstFamily2(w.body.fontFamily));
8036
8176
  p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
8037
8177
  const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
8038
- const fontBody = designBody ?? firstFamily(w.body.fontFamily);
8178
+ const fontBody = designBody ?? firstFamily2(w.body.fontFamily);
8039
8179
  p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
8040
8180
  const logoUrl = design?.logos.find((u) => /wordmark|logo/i.test(u)) ?? null;
8041
8181
  const iconUrl = w.icons.find((u) => /apple-touch/i.test(u)) ?? (w.icons.length ? w.icons[0] : null);
@@ -8847,9 +8987,10 @@ ground (a paper site is a plate, a dark site is dark), and with no brand
8847
8987
  the house gradient. Screenshot-genre stills never take a look.
8848
8988
  Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the CWS
8849
8989
  tile + marquee, GitHub social preview) COMPOSE by default: each renders
8850
- from its destination's poster TEMPLATE (split-cover carries a headline
8851
- column beside the shot; card-on-gradient is the shot alone on a mesh, the
8852
- store's tile rule), filled with BRAND.md's colours and faces and the
8990
+ from its destination's poster TEMPLATE (split-cover is a STAGE, the
8991
+ take's own card leaning in perspective with its chrome and shadow beside
8992
+ a serif headline column, on the brand's ground; card-on-gradient is the
8993
+ shot alone on a mesh, the store's tile rule), filled with BRAND.md's colours and faces and the
8853
8994
  release's words, the shot baked as an object (padded, rounded, shadowed,
8854
8995
  a hairline on a light ground), PNG at exact pixels; kit.json records the
8855
8996
  template and the text boxes. The headline is LAUNCH.md's headline role
@@ -9717,7 +9858,7 @@ async function cmdJudge(argv) {
9717
9858
  ${lines.join("\n ")}` : "") + (result.skipped.length ? `
9718
9859
  skipped: ${result.skipped.join("\n skipped: ")}` : "") + `
9719
9860
  Judge each pair both ways (the rubric is beside it) and fill ${result.verdictFile}` + (rate.judged ? `
9720
- Win rate so far: ${rate.wins}/${rate.judged} (${(rate.rate * 100).toFixed(0)}%); parity with the references is 50%, the marketability bar is 40%` : "")
9861
+ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judged (${(rate.rate * 100).toFixed(0)}%; a tie counts a half); parity with the references is 50%, the marketability bar is 40%` : "")
9721
9862
  );
9722
9863
  return EXIT_OK;
9723
9864
  }
@@ -10008,4 +10149,4 @@ export {
10008
10149
  convertAgentBrowser,
10009
10150
  run
10010
10151
  };
10011
- //# sourceMappingURL=chunk-MMKDYE57.js.map
10152
+ //# sourceMappingURL=chunk-HGLKXIXH.js.map