@vosjs/cli 0.14.0 → 0.16.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.
- package/dist/{chunk-MMKDYE57.js → chunk-PKHORUJ2.js} +479 -90
- package/dist/chunk-PKHORUJ2.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-R5P44XNR.js → run-5TIKH2IB.js} +2 -2
- package/package.json +5 -5
- package/schema/doc.schema.json +15 -6
- package/dist/chunk-MMKDYE57.js.map +0 -1
- /package/dist/{run-R5P44XNR.js.map → run-5TIKH2IB.js.map} +0 -0
|
@@ -2359,8 +2359,13 @@ function parseTranscript(raw) {
|
|
|
2359
2359
|
|
|
2360
2360
|
// src/plugin/framesTake.ts
|
|
2361
2361
|
var VIDEO_TOKEN = "__VOILA_CLI_VIDEO__";
|
|
2362
|
+
function stillSupersample(w, h) {
|
|
2363
|
+
return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
|
|
2364
|
+
}
|
|
2362
2365
|
async function stillsInPage(opts) {
|
|
2363
2366
|
const w = window;
|
|
2367
|
+
const RW = opts.W * opts.ss;
|
|
2368
|
+
const RH = opts.H * opts.ss;
|
|
2364
2369
|
try {
|
|
2365
2370
|
const vblob = await (await fetch(opts.videoUrl)).blob();
|
|
2366
2371
|
const vurl = URL.createObjectURL(vblob);
|
|
@@ -2377,11 +2382,11 @@ async function stillsInPage(opts) {
|
|
|
2377
2382
|
THREE: w.__THREE__,
|
|
2378
2383
|
gsap: w.__gsap__,
|
|
2379
2384
|
resolution: {
|
|
2380
|
-
width:
|
|
2381
|
-
height:
|
|
2385
|
+
width: RW,
|
|
2386
|
+
height: RH,
|
|
2382
2387
|
pixelRatio: 1,
|
|
2383
|
-
drawingBufferWidth:
|
|
2384
|
-
drawingBufferHeight:
|
|
2388
|
+
drawingBufferWidth: RW,
|
|
2389
|
+
drawingBufferHeight: RH
|
|
2385
2390
|
},
|
|
2386
2391
|
preserveDrawingBuffer: true
|
|
2387
2392
|
};
|
|
@@ -2391,32 +2396,44 @@ async function stillsInPage(opts) {
|
|
|
2391
2396
|
timeline.pause();
|
|
2392
2397
|
timeline.seek(0, false);
|
|
2393
2398
|
const canvas = document.querySelector("canvas");
|
|
2394
|
-
canvas.width =
|
|
2395
|
-
canvas.height =
|
|
2396
|
-
canvas.style.width =
|
|
2397
|
-
canvas.style.height =
|
|
2399
|
+
canvas.width = RW;
|
|
2400
|
+
canvas.height = RH;
|
|
2401
|
+
canvas.style.width = RW + "px";
|
|
2402
|
+
canvas.style.height = RH + "px";
|
|
2403
|
+
const out = document.createElement("canvas");
|
|
2404
|
+
out.width = opts.W;
|
|
2405
|
+
out.height = opts.H;
|
|
2398
2406
|
const raf = () => new Promise((r) => requestAnimationFrame(r));
|
|
2399
2407
|
const wvr = () => w.__vos__?.waitForVideosReady ? w.__vos__.waitForVideosReady() : null;
|
|
2400
2408
|
const pending = () => w.__vos__?.pendingDecodes?.size ?? 0;
|
|
2401
|
-
const toPng = () => new Promise(
|
|
2402
|
-
|
|
2409
|
+
const toPng = () => new Promise((res, rej) => {
|
|
2410
|
+
let src = canvas;
|
|
2411
|
+
if (opts.ss > 1) {
|
|
2412
|
+
const g = out.getContext("2d");
|
|
2413
|
+
g.imageSmoothingEnabled = true;
|
|
2414
|
+
g.imageSmoothingQuality = "high";
|
|
2415
|
+
g.clearRect(0, 0, opts.W, opts.H);
|
|
2416
|
+
g.drawImage(canvas, 0, 0, opts.W, opts.H);
|
|
2417
|
+
src = out;
|
|
2418
|
+
}
|
|
2419
|
+
src.toBlob(
|
|
2403
2420
|
(b) => b ? res(b) : rej(new Error("toBlob failed")),
|
|
2404
2421
|
"image/png"
|
|
2405
|
-
)
|
|
2406
|
-
);
|
|
2422
|
+
);
|
|
2423
|
+
});
|
|
2407
2424
|
const collectVideos = () => {
|
|
2408
|
-
const
|
|
2425
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
2409
2426
|
const cache = w.__vos__?.videoCache;
|
|
2410
2427
|
if (cache) {
|
|
2411
2428
|
const vals = cache instanceof Map ? [...cache.values()] : Object.values(cache);
|
|
2412
2429
|
for (const v of vals) {
|
|
2413
|
-
if (v?.tagName === "VIDEO")
|
|
2414
|
-
else if (v?.el?.tagName === "VIDEO")
|
|
2415
|
-
else if (v?.video?.tagName === "VIDEO")
|
|
2430
|
+
if (v?.tagName === "VIDEO") out2.add(v);
|
|
2431
|
+
else if (v?.el?.tagName === "VIDEO") out2.add(v.el);
|
|
2432
|
+
else if (v?.video?.tagName === "VIDEO") out2.add(v.video);
|
|
2416
2433
|
}
|
|
2417
2434
|
}
|
|
2418
|
-
document.querySelectorAll("video").forEach((v) =>
|
|
2419
|
-
return [...
|
|
2435
|
+
document.querySelectorAll("video").forEach((v) => out2.add(v));
|
|
2436
|
+
return [...out2];
|
|
2420
2437
|
};
|
|
2421
2438
|
const videosSettled = () => collectVideos().every((v) => !v.seeking && v.readyState >= 2);
|
|
2422
2439
|
const waitVideosSettled = async (maxMs) => {
|
|
@@ -2502,7 +2519,10 @@ async function framesTake(browser, dir, opts) {
|
|
|
2502
2519
|
const server = await startTakeServer(dir, {
|
|
2503
2520
|
"/render.html": renderPageHtml()
|
|
2504
2521
|
});
|
|
2505
|
-
const
|
|
2522
|
+
const ss = stillSupersample(width, height);
|
|
2523
|
+
const context = await browser.newContext({
|
|
2524
|
+
viewport: { width: width * ss, height: height * ss }
|
|
2525
|
+
});
|
|
2506
2526
|
try {
|
|
2507
2527
|
const page = await context.newPage();
|
|
2508
2528
|
page.on("console", (m) => {
|
|
@@ -2522,6 +2542,7 @@ async function framesTake(browser, dir, opts) {
|
|
|
2522
2542
|
videoUrl: `/${RECORDING_NAME}`,
|
|
2523
2543
|
W: width,
|
|
2524
2544
|
H: height,
|
|
2545
|
+
ss,
|
|
2525
2546
|
shots: named.map(({ time, name }) => ({ time, name }))
|
|
2526
2547
|
}).catch(() => {
|
|
2527
2548
|
});
|
|
@@ -2592,14 +2613,7 @@ async function writeIndexJson(result) {
|
|
|
2592
2613
|
}
|
|
2593
2614
|
|
|
2594
2615
|
// 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";
|
|
2616
|
+
import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
|
|
2603
2617
|
import { tmpdir } from "os";
|
|
2604
2618
|
import { join as join5, relative, resolve as resolve3 } from "path";
|
|
2605
2619
|
import { totalDuration } from "@vosjs/timeline";
|
|
@@ -2720,6 +2734,8 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
2720
2734
|
import { compileVosConfig as compileVosConfig3 } from "@vosjs/core";
|
|
2721
2735
|
async function posterStillInPage(opts) {
|
|
2722
2736
|
const w = window;
|
|
2737
|
+
const RW = opts.W * opts.ss;
|
|
2738
|
+
const RH = opts.H * opts.ss;
|
|
2723
2739
|
try {
|
|
2724
2740
|
const mod = await import(
|
|
2725
2741
|
/* @vite-ignore */
|
|
@@ -2733,11 +2749,11 @@ async function posterStillInPage(opts) {
|
|
|
2733
2749
|
THREE: w.__THREE__,
|
|
2734
2750
|
gsap: w.__gsap__,
|
|
2735
2751
|
resolution: {
|
|
2736
|
-
width:
|
|
2737
|
-
height:
|
|
2752
|
+
width: RW,
|
|
2753
|
+
height: RH,
|
|
2738
2754
|
pixelRatio: 1,
|
|
2739
|
-
drawingBufferWidth:
|
|
2740
|
-
drawingBufferHeight:
|
|
2755
|
+
drawingBufferWidth: RW,
|
|
2756
|
+
drawingBufferHeight: RH
|
|
2741
2757
|
},
|
|
2742
2758
|
preserveDrawingBuffer: true
|
|
2743
2759
|
};
|
|
@@ -2747,16 +2763,27 @@ async function posterStillInPage(opts) {
|
|
|
2747
2763
|
timeline.pause();
|
|
2748
2764
|
timeline.seek(opts.time, false);
|
|
2749
2765
|
const canvas = document.querySelector("canvas");
|
|
2750
|
-
canvas.width =
|
|
2751
|
-
canvas.height =
|
|
2752
|
-
canvas.style.width =
|
|
2753
|
-
canvas.style.height =
|
|
2766
|
+
canvas.width = RW;
|
|
2767
|
+
canvas.height = RH;
|
|
2768
|
+
canvas.style.width = RW + "px";
|
|
2769
|
+
canvas.style.height = RH + "px";
|
|
2754
2770
|
const raf = () => new Promise((r) => requestAnimationFrame(r));
|
|
2755
2771
|
await raf();
|
|
2756
2772
|
await raf();
|
|
2757
2773
|
await raf();
|
|
2774
|
+
let src = canvas;
|
|
2775
|
+
if (opts.ss > 1) {
|
|
2776
|
+
const out = document.createElement("canvas");
|
|
2777
|
+
out.width = opts.W;
|
|
2778
|
+
out.height = opts.H;
|
|
2779
|
+
const g = out.getContext("2d");
|
|
2780
|
+
g.imageSmoothingEnabled = true;
|
|
2781
|
+
g.imageSmoothingQuality = "high";
|
|
2782
|
+
g.drawImage(canvas, 0, 0, opts.W, opts.H);
|
|
2783
|
+
src = out;
|
|
2784
|
+
}
|
|
2758
2785
|
const png = await new Promise(
|
|
2759
|
-
(res) =>
|
|
2786
|
+
(res) => src.toBlob((b) => res(b), "image/png")
|
|
2760
2787
|
);
|
|
2761
2788
|
if (!png) throw new Error("toBlob returned null");
|
|
2762
2789
|
await fetch("/save?name=" + opts.outName, { method: "POST", body: png });
|
|
@@ -2774,8 +2801,9 @@ async function renderPosterStills(browser, config, serveDir, shots, time) {
|
|
|
2774
2801
|
});
|
|
2775
2802
|
try {
|
|
2776
2803
|
for (const shot of shots) {
|
|
2804
|
+
const ss = stillSupersample(shot.width, shot.height);
|
|
2777
2805
|
const context = await browser.newContext({
|
|
2778
|
-
viewport: { width: shot.width, height: shot.height }
|
|
2806
|
+
viewport: { width: shot.width * ss, height: shot.height * ss }
|
|
2779
2807
|
});
|
|
2780
2808
|
try {
|
|
2781
2809
|
const page = await context.newPage();
|
|
@@ -2794,6 +2822,7 @@ async function renderPosterStills(browser, config, serveDir, shots, time) {
|
|
|
2794
2822
|
animationCode,
|
|
2795
2823
|
W: shot.width,
|
|
2796
2824
|
H: shot.height,
|
|
2825
|
+
ss,
|
|
2797
2826
|
time,
|
|
2798
2827
|
outName: shot.name
|
|
2799
2828
|
}).catch(() => {
|
|
@@ -3325,7 +3354,8 @@ function blurPlane(src, w, h, r, passes = 3) {
|
|
|
3325
3354
|
for (let y = 0; y < h; y++) {
|
|
3326
3355
|
let acc = 0;
|
|
3327
3356
|
const row = y * w;
|
|
3328
|
-
for (let x = -r; x <= r; x++)
|
|
3357
|
+
for (let x = -r; x <= r; x++)
|
|
3358
|
+
acc += src[row + Math.min(w - 1, Math.max(0, x))];
|
|
3329
3359
|
for (let x = 0; x < w; x++) {
|
|
3330
3360
|
tmp[row + x] = acc / (2 * r + 1);
|
|
3331
3361
|
const add = src[row + Math.min(w - 1, x + r + 1)];
|
|
@@ -3335,7 +3365,8 @@ function blurPlane(src, w, h, r, passes = 3) {
|
|
|
3335
3365
|
}
|
|
3336
3366
|
for (let x = 0; x < w; x++) {
|
|
3337
3367
|
let acc = 0;
|
|
3338
|
-
for (let y = -r; y <= r; y++)
|
|
3368
|
+
for (let y = -r; y <= r; y++)
|
|
3369
|
+
acc += tmp[Math.min(h - 1, Math.max(0, y)) * w + x];
|
|
3339
3370
|
for (let y = 0; y < h; y++) {
|
|
3340
3371
|
src[y * w + x] = acc / (2 * r + 1);
|
|
3341
3372
|
const add = tmp[Math.min(h - 1, y + r + 1) * w + x];
|
|
@@ -3351,7 +3382,8 @@ function roundedCoverage(x, y, rect, r) {
|
|
|
3351
3382
|
for (const dx of [0.25, 0.75]) {
|
|
3352
3383
|
const px = x + dx;
|
|
3353
3384
|
const py = y + dy;
|
|
3354
|
-
if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h)
|
|
3385
|
+
if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h)
|
|
3386
|
+
continue;
|
|
3355
3387
|
const cx = Math.min(Math.max(px, rect.x + r), rect.x + rect.w - r);
|
|
3356
3388
|
const cy = Math.min(Math.max(py, rect.y + r), rect.y + rect.h - r);
|
|
3357
3389
|
if ((px - cx) ** 2 + (py - cy) ** 2 <= r * r) inside++;
|
|
@@ -3362,24 +3394,34 @@ function roundedCoverage(x, y, rect, r) {
|
|
|
3362
3394
|
function bakeShot(shot, opts = {}) {
|
|
3363
3395
|
const margin = Math.round(shot.w * (opts.margin ?? 0.06));
|
|
3364
3396
|
const radius = shot.w * (opts.radius ?? 0.014);
|
|
3365
|
-
const shadowA = opts.shadow ?? 0.
|
|
3366
|
-
const blur = Math.round(shot.w * (opts.blur ?? 0.
|
|
3367
|
-
const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.
|
|
3397
|
+
const shadowA = opts.shadow ?? 0.3;
|
|
3398
|
+
const blur = Math.round(shot.w * (opts.blur ?? 0.05));
|
|
3399
|
+
const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.02));
|
|
3368
3400
|
const hair = opts.hairline ?? 0;
|
|
3369
3401
|
const w = shot.w + margin * 2;
|
|
3370
3402
|
const h = shot.h + margin * 2;
|
|
3371
3403
|
const out = new Uint8Array(w * h * 4);
|
|
3372
3404
|
const rect = { x: margin, y: margin, w: shot.w, h: shot.h };
|
|
3373
3405
|
if (shadowA > 0) {
|
|
3374
|
-
const
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3406
|
+
const layers = [
|
|
3407
|
+
[Math.max(1, Math.round(blur / 4)), Math.round(offsetY / 5), 0.4],
|
|
3408
|
+
[blur, offsetY, 0.6]
|
|
3409
|
+
];
|
|
3410
|
+
const acc = new Float32Array(w * h);
|
|
3411
|
+
for (const [lb, lo, share] of layers) {
|
|
3412
|
+
const mask = new Float32Array(w * h);
|
|
3413
|
+
for (let y = 0; y < h; y++)
|
|
3414
|
+
for (let x = 0; x < w; x++) {
|
|
3415
|
+
const c = roundedCoverage(x, y - lo, rect, radius);
|
|
3416
|
+
if (c > 0) mask[y * w + x] = c;
|
|
3417
|
+
}
|
|
3418
|
+
blurPlane(mask, w, h, Math.max(1, Math.round(lb / 2)));
|
|
3419
|
+
const a = shadowA * share;
|
|
3420
|
+
for (let i = 0; i < w * h; i++)
|
|
3421
|
+
acc[i] = 1 - (1 - acc[i]) * (1 - mask[i] * a);
|
|
3422
|
+
}
|
|
3381
3423
|
for (let i = 0; i < w * h; i++) {
|
|
3382
|
-
const a =
|
|
3424
|
+
const a = acc[i];
|
|
3383
3425
|
if (a <= 2e-3) continue;
|
|
3384
3426
|
out[i * 4] = 0;
|
|
3385
3427
|
out[i * 4 + 1] = 0;
|
|
@@ -3399,7 +3441,12 @@ function bakeShot(shot, opts = {}) {
|
|
|
3399
3441
|
let g = shot.data[si + 1];
|
|
3400
3442
|
let b = shot.data[si + 2];
|
|
3401
3443
|
if (hair > 0) {
|
|
3402
|
-
const edge = Math.min(
|
|
3444
|
+
const edge = Math.min(
|
|
3445
|
+
x - rect.x,
|
|
3446
|
+
rect.x + rect.w - x,
|
|
3447
|
+
y - rect.y,
|
|
3448
|
+
rect.y + rect.h - y
|
|
3449
|
+
);
|
|
3403
3450
|
if (edge < 1.5) {
|
|
3404
3451
|
r = Math.round(r * (1 - hair));
|
|
3405
3452
|
g = Math.round(g * (1 - hair));
|
|
@@ -3924,12 +3971,18 @@ var rgb = (hex2) => {
|
|
|
3924
3971
|
const n = parseInt(m[1], 16);
|
|
3925
3972
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
3926
3973
|
};
|
|
3927
|
-
var toHex = (c) => "#" + c.map(
|
|
3974
|
+
var toHex = (c) => "#" + c.map(
|
|
3975
|
+
(v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")
|
|
3976
|
+
).join("");
|
|
3928
3977
|
function mixHex(a, b, t) {
|
|
3929
3978
|
const pa = rgb(a);
|
|
3930
3979
|
const pb = rgb(b);
|
|
3931
3980
|
if (!pa || !pb) return a;
|
|
3932
|
-
return toHex([
|
|
3981
|
+
return toHex([
|
|
3982
|
+
pa[0] + (pb[0] - pa[0]) * t,
|
|
3983
|
+
pa[1] + (pb[1] - pa[1]) * t,
|
|
3984
|
+
pa[2] + (pb[2] - pa[2]) * t
|
|
3985
|
+
]);
|
|
3933
3986
|
}
|
|
3934
3987
|
function rgba(hex2, alpha) {
|
|
3935
3988
|
const c = rgb(hex2) ?? [255, 255, 255];
|
|
@@ -3945,12 +3998,14 @@ function resolveFace(family, weights) {
|
|
|
3945
3998
|
const first = family.split(",")[0].replace(/['"]/g, "").replace(/\s+variable$/i, "").trim();
|
|
3946
3999
|
const entry = findFontFamily2(first);
|
|
3947
4000
|
if (!entry) return null;
|
|
3948
|
-
const fonts = [
|
|
4001
|
+
const fonts = [
|
|
4002
|
+
...new Set(weights.map((w) => nearestFontWeight(entry, w)))
|
|
4003
|
+
].map((w) => ({
|
|
3949
4004
|
family: entry.family,
|
|
3950
4005
|
url: fontFaceUrl(entry.slug, w),
|
|
3951
4006
|
weight: w
|
|
3952
4007
|
}));
|
|
3953
|
-
return { stack: fontStack(entry), fonts };
|
|
4008
|
+
return { stack: fontStack(entry), category: entry.category, fonts };
|
|
3954
4009
|
}
|
|
3955
4010
|
function posterValues(brand, words) {
|
|
3956
4011
|
const b = brand ?? {};
|
|
@@ -3973,7 +4028,11 @@ function posterValues(brand, words) {
|
|
|
3973
4028
|
if (bgA) values.grain = lightGround ? 10 : 22;
|
|
3974
4029
|
if (accent) values.blobA = rgba(accent, lightGround ? 0.28 : 0.4);
|
|
3975
4030
|
if (bgC) values.blobB = rgba(bgC, 0.75);
|
|
3976
|
-
if (accent)
|
|
4031
|
+
if (accent)
|
|
4032
|
+
values.blobC = rgba(
|
|
4033
|
+
mixHex(accent, "#ffffff", 0.55),
|
|
4034
|
+
lightGround ? 0.35 : 0.25
|
|
4035
|
+
);
|
|
3977
4036
|
if (wordmark) values.brand = wordmark;
|
|
3978
4037
|
const headline = (words.headline ?? "").trim();
|
|
3979
4038
|
if (headline) values.headline = headline;
|
|
@@ -3993,6 +4052,224 @@ function posterValues(brand, words) {
|
|
|
3993
4052
|
return { values, fonts, lightGround };
|
|
3994
4053
|
}
|
|
3995
4054
|
|
|
4055
|
+
// src/plugin/stages.ts
|
|
4056
|
+
var str = (v, fallback) => typeof v === "string" && v.trim() ? v.trim() : fallback;
|
|
4057
|
+
var firstFamily = (stack) => stack.split(",")[0].replace(/['"]/g, "").trim();
|
|
4058
|
+
function stageSplitCover(input) {
|
|
4059
|
+
const R = input.size.w / Math.max(1, input.size.h);
|
|
4060
|
+
const portrait = R < 0.87;
|
|
4061
|
+
const square = !portrait && R <= 1.15;
|
|
4062
|
+
const v = input.values;
|
|
4063
|
+
const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
|
|
4064
|
+
const ink = str(v.ink, "#fff6ec");
|
|
4065
|
+
const inkSoft = str(v.inkSoft, ink);
|
|
4066
|
+
const display = firstFamily(str(v.fontDisplay, "Lexend"));
|
|
4067
|
+
const body = firstFamily(str(v.fontBody, "Lexend"));
|
|
4068
|
+
const headline = str(v.headline, "");
|
|
4069
|
+
const kicker = str(v.kicker, "");
|
|
4070
|
+
const brand = str(v.brand, "");
|
|
4071
|
+
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 };
|
|
4072
|
+
const shot = {
|
|
4073
|
+
x: inset.left,
|
|
4074
|
+
y: inset.top,
|
|
4075
|
+
w: 1 - inset.left - inset.right,
|
|
4076
|
+
h: 1 - inset.top - inset.bottom
|
|
4077
|
+
};
|
|
4078
|
+
const set = [
|
|
4079
|
+
`frame.background=${JSON.stringify(ground)}`,
|
|
4080
|
+
"frame.backgroundMedia=null",
|
|
4081
|
+
"frame.fit=cover",
|
|
4082
|
+
`frame.inset=${JSON.stringify(inset)}`,
|
|
4083
|
+
'frame.focus={"cx":0,"cy":0}',
|
|
4084
|
+
"frame.radius=16",
|
|
4085
|
+
"frame.shadow=0.5",
|
|
4086
|
+
"frame.shadowContact=0",
|
|
4087
|
+
"frame.border=0",
|
|
4088
|
+
"zoom=[]",
|
|
4089
|
+
`tilt=[{"id":"stage","in":0,"out":${input.sourceSeconds.toFixed(3)},"rx":3,"ry":${portrait || square ? 0 : 10}}]`,
|
|
4090
|
+
"cursor.visible=false",
|
|
4091
|
+
"cursor.clickFx.style=none"
|
|
4092
|
+
];
|
|
4093
|
+
const designW = 1080 * input.size.w / input.size.h;
|
|
4094
|
+
const boxes = [];
|
|
4095
|
+
const clips = [];
|
|
4096
|
+
const column = portrait || square ? 0.84 : 0.36;
|
|
4097
|
+
const left = portrait || square ? 0.08 : 0.07;
|
|
4098
|
+
const size = portrait ? 60 : square ? 68 : 84;
|
|
4099
|
+
const word = (id, text, preset, family, px, y, color, extra, role) => {
|
|
4100
|
+
if (!text) return;
|
|
4101
|
+
const lines = text.split("\n");
|
|
4102
|
+
const longest = Math.max(...lines.map((l) => l.length));
|
|
4103
|
+
const widest = Math.min(
|
|
4104
|
+
column,
|
|
4105
|
+
longest * px * (preset === "title" ? 0.5 : 0.62) / designW
|
|
4106
|
+
);
|
|
4107
|
+
const lh = preset === "title" ? 1.05 : 1.2;
|
|
4108
|
+
const h = lines.length * px * lh / 1080;
|
|
4109
|
+
clips.push({
|
|
4110
|
+
id,
|
|
4111
|
+
kind: "text",
|
|
4112
|
+
text,
|
|
4113
|
+
preset,
|
|
4114
|
+
family,
|
|
4115
|
+
size: px,
|
|
4116
|
+
color,
|
|
4117
|
+
align: "left",
|
|
4118
|
+
maxWidth: column,
|
|
4119
|
+
lineHeight: lh,
|
|
4120
|
+
transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
|
|
4121
|
+
// Words on the ground carry no footage shadow: a drop shadow under a
|
|
4122
|
+
// headline on a plate is the old-web tell.
|
|
4123
|
+
shadow: 0,
|
|
4124
|
+
start: 0,
|
|
4125
|
+
duration: +input.outputSeconds.toFixed(3),
|
|
4126
|
+
enter: "none",
|
|
4127
|
+
exit: "none",
|
|
4128
|
+
...extra
|
|
4129
|
+
});
|
|
4130
|
+
boxes.push({
|
|
4131
|
+
x: left,
|
|
4132
|
+
y: y - h / 2,
|
|
4133
|
+
w: widest,
|
|
4134
|
+
h,
|
|
4135
|
+
color,
|
|
4136
|
+
role,
|
|
4137
|
+
label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text
|
|
4138
|
+
});
|
|
4139
|
+
};
|
|
4140
|
+
const kickerY = portrait ? 0.1 : square ? 0.12 : 0.24;
|
|
4141
|
+
const titleY = portrait ? 0.24 : square ? 0.28 : 0.46;
|
|
4142
|
+
const brandY = portrait ? 0.94 : square ? 0.94 : 0.86;
|
|
4143
|
+
word(
|
|
4144
|
+
"stage-kicker",
|
|
4145
|
+
kicker,
|
|
4146
|
+
"label",
|
|
4147
|
+
"JetBrains Mono",
|
|
4148
|
+
19,
|
|
4149
|
+
kickerY,
|
|
4150
|
+
inkSoft,
|
|
4151
|
+
{ letterSpacing: 6, weight: 400 },
|
|
4152
|
+
"body"
|
|
4153
|
+
);
|
|
4154
|
+
word(
|
|
4155
|
+
"stage-title",
|
|
4156
|
+
headline,
|
|
4157
|
+
"title",
|
|
4158
|
+
display,
|
|
4159
|
+
size,
|
|
4160
|
+
titleY,
|
|
4161
|
+
ink,
|
|
4162
|
+
{ weight: 600, letterSpacing: -Math.round(size * 0.02) },
|
|
4163
|
+
"headline"
|
|
4164
|
+
);
|
|
4165
|
+
word(
|
|
4166
|
+
"stage-brand",
|
|
4167
|
+
brand,
|
|
4168
|
+
"label",
|
|
4169
|
+
body,
|
|
4170
|
+
25,
|
|
4171
|
+
brandY,
|
|
4172
|
+
ink,
|
|
4173
|
+
{ weight: 700, letterSpacing: 1 },
|
|
4174
|
+
"body"
|
|
4175
|
+
);
|
|
4176
|
+
set.push(`overlays=${JSON.stringify(clips)}`);
|
|
4177
|
+
return { set, text: boxes, shot };
|
|
4178
|
+
}
|
|
4179
|
+
var TILE_MAX_PX = 700;
|
|
4180
|
+
function isTileSize(size) {
|
|
4181
|
+
return Math.max(size.w, size.h) < TILE_MAX_PX;
|
|
4182
|
+
}
|
|
4183
|
+
function stageTile(input) {
|
|
4184
|
+
const R = input.size.w / Math.max(1, input.size.h);
|
|
4185
|
+
const square = R <= 1.15;
|
|
4186
|
+
const v = input.values;
|
|
4187
|
+
const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
|
|
4188
|
+
const ink = str(v.ink, "#fff6ec");
|
|
4189
|
+
const display = firstFamily(str(v.fontDisplay, "Lexend"));
|
|
4190
|
+
const headline = str(v.headline, "") || str(v.brand, "Release");
|
|
4191
|
+
const wordless = input.text === "none";
|
|
4192
|
+
const aspect = input.footageAspect && input.footageAspect > 0 ? input.footageAspect : 16 / 9;
|
|
4193
|
+
const left = square ? 0.08 : 0.07;
|
|
4194
|
+
const top = wordless ? square ? 0.1 : 0.12 : square ? 0.42 : 0.47;
|
|
4195
|
+
const visible = wordless ? square ? 0.66 : 0.8 : square ? 0.56 : 0.62;
|
|
4196
|
+
const cardW = (1 - left) / visible;
|
|
4197
|
+
const cardH = cardW * R / aspect;
|
|
4198
|
+
const inset = {
|
|
4199
|
+
left,
|
|
4200
|
+
right: +(1 - left - cardW).toFixed(4),
|
|
4201
|
+
top,
|
|
4202
|
+
bottom: +(1 - top - cardH).toFixed(4)
|
|
4203
|
+
};
|
|
4204
|
+
const shot = {
|
|
4205
|
+
x: inset.left,
|
|
4206
|
+
y: inset.top,
|
|
4207
|
+
w: 1 - inset.left - inset.right,
|
|
4208
|
+
h: 1 - inset.top - inset.bottom
|
|
4209
|
+
};
|
|
4210
|
+
const set = [
|
|
4211
|
+
`frame.background=${JSON.stringify(ground)}`,
|
|
4212
|
+
"frame.backgroundMedia=null",
|
|
4213
|
+
"frame.fit=cover",
|
|
4214
|
+
`frame.inset=${JSON.stringify(inset)}`,
|
|
4215
|
+
'frame.focus={"cx":0,"cy":0}',
|
|
4216
|
+
"frame.radius=24",
|
|
4217
|
+
"frame.shadow=0.5",
|
|
4218
|
+
"frame.shadowContact=0",
|
|
4219
|
+
"frame.border=0",
|
|
4220
|
+
"zoom=[]",
|
|
4221
|
+
"tilt=[]",
|
|
4222
|
+
"cursor.visible=false",
|
|
4223
|
+
"cursor.clickFx.style=none"
|
|
4224
|
+
];
|
|
4225
|
+
const designW = 1080 * input.size.w / input.size.h;
|
|
4226
|
+
const column = square ? 0.84 : 0.86;
|
|
4227
|
+
const px = square ? 96 : 108;
|
|
4228
|
+
const lines = headline.split("\n");
|
|
4229
|
+
const longest = Math.max(...lines.map((l) => l.length));
|
|
4230
|
+
const widest = Math.min(column, longest * px * 0.5 / designW);
|
|
4231
|
+
const lh = 1.05;
|
|
4232
|
+
const h = lines.length * px * lh / 1080;
|
|
4233
|
+
const y = square ? 0.2 : 0.24;
|
|
4234
|
+
const clip = {
|
|
4235
|
+
id: "stage-title",
|
|
4236
|
+
kind: "text",
|
|
4237
|
+
text: headline,
|
|
4238
|
+
preset: "title",
|
|
4239
|
+
family: display,
|
|
4240
|
+
size: px,
|
|
4241
|
+
color: ink,
|
|
4242
|
+
align: "left",
|
|
4243
|
+
maxWidth: column,
|
|
4244
|
+
lineHeight: lh,
|
|
4245
|
+
weight: 600,
|
|
4246
|
+
letterSpacing: -Math.round(px * 0.02),
|
|
4247
|
+
transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
|
|
4248
|
+
shadow: 0,
|
|
4249
|
+
start: 0,
|
|
4250
|
+
duration: +input.outputSeconds.toFixed(3),
|
|
4251
|
+
enter: "none",
|
|
4252
|
+
exit: "none"
|
|
4253
|
+
};
|
|
4254
|
+
if (wordless) {
|
|
4255
|
+
set.push("overlays=[]");
|
|
4256
|
+
return { set, text: [], shot };
|
|
4257
|
+
}
|
|
4258
|
+
set.push(`overlays=${JSON.stringify([clip])}`);
|
|
4259
|
+
const text = [
|
|
4260
|
+
{
|
|
4261
|
+
x: left,
|
|
4262
|
+
y: y - h / 2,
|
|
4263
|
+
w: widest,
|
|
4264
|
+
h,
|
|
4265
|
+
color: ink,
|
|
4266
|
+
role: "headline",
|
|
4267
|
+
label: headline.length > 24 ? `${headline.slice(0, 24)}\u2026` : headline
|
|
4268
|
+
}
|
|
4269
|
+
];
|
|
4270
|
+
return { set, text, shot };
|
|
4271
|
+
}
|
|
4272
|
+
|
|
3996
4273
|
// src/plugin/motionPlan.ts
|
|
3997
4274
|
import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
|
|
3998
4275
|
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
@@ -4208,6 +4485,10 @@ function templateForCard(d, opts) {
|
|
|
4208
4485
|
if (opts.poster === null) return null;
|
|
4209
4486
|
if (opts.poster) return { config: opts.poster.config, from: opts.poster.from };
|
|
4210
4487
|
if (!d.template) return null;
|
|
4488
|
+
if (isTileSize(d.px)) {
|
|
4489
|
+
const config2 = templateByName(d.template) ?? templateByName("card-on-gradient");
|
|
4490
|
+
if (config2) return { config: config2, from: "stage tile", stage: "tile" };
|
|
4491
|
+
}
|
|
4211
4492
|
let name = d.template;
|
|
4212
4493
|
let note;
|
|
4213
4494
|
const wants = templateOf(templateByName(name) ?? {});
|
|
@@ -4218,6 +4499,8 @@ function templateForCard(d, opts) {
|
|
|
4218
4499
|
}
|
|
4219
4500
|
const config = templateByName(name);
|
|
4220
4501
|
if (!config) return null;
|
|
4502
|
+
if (name === "split-cover")
|
|
4503
|
+
return { config, from: "stage split-cover", note, stage: "split-cover" };
|
|
4221
4504
|
return { config, from: `template ${name}`, note };
|
|
4222
4505
|
}
|
|
4223
4506
|
function lookOverrides(look, placement, size, video, opts) {
|
|
@@ -4238,7 +4521,12 @@ function lookOverrides(look, placement, size, video, opts) {
|
|
|
4238
4521
|
}
|
|
4239
4522
|
if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
|
|
4240
4523
|
if (opts.still) {
|
|
4241
|
-
set.push(
|
|
4524
|
+
set.push(
|
|
4525
|
+
"zoom=[]",
|
|
4526
|
+
"tilt=[]",
|
|
4527
|
+
"cursor.visible=false",
|
|
4528
|
+
"cursor.clickFx.style=none"
|
|
4529
|
+
);
|
|
4242
4530
|
}
|
|
4243
4531
|
return set;
|
|
4244
4532
|
}
|
|
@@ -4410,9 +4698,14 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4410
4698
|
if (!set.length) return opts.overrides;
|
|
4411
4699
|
return { ...opts.overrides, set };
|
|
4412
4700
|
};
|
|
4413
|
-
const cardPlans = destinations.filter(
|
|
4701
|
+
const cardPlans = destinations.filter(
|
|
4702
|
+
(d) => d.kind !== "video" && d.genre === "card" && !NOT_FROM_FOOTAGE[d.id]
|
|
4703
|
+
).map((d) => ({ d, plan: templateForCard(d, opts) })).filter(
|
|
4704
|
+
(p) => p.plan !== null
|
|
4705
|
+
);
|
|
4414
4706
|
const posterCardIds = new Set(cardPlans.map((p) => p.d.id));
|
|
4415
|
-
for (const p of cardPlans)
|
|
4707
|
+
for (const p of cardPlans)
|
|
4708
|
+
if (p.plan.note) skipped.push(`note: ${p.plan.note}`);
|
|
4416
4709
|
if (cardPlans.length) {
|
|
4417
4710
|
const meta = doc.source.meta;
|
|
4418
4711
|
const heroTime = opts.shotTime ?? (stillTimes.length ? stillTimes[0] : duration / 2);
|
|
@@ -4448,11 +4741,66 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4448
4741
|
const baked = bakeShot(raw, {
|
|
4449
4742
|
margin: PAD,
|
|
4450
4743
|
hairline: fill.lightGround ? 0.14 : 0,
|
|
4451
|
-
shadow: fill.lightGround ? 0.
|
|
4744
|
+
shadow: fill.lightGround ? 0.28 : 0.4
|
|
4452
4745
|
});
|
|
4453
4746
|
await writeFile6(join5(serveDir, "shot.png"), encodePng(baked));
|
|
4454
4747
|
const shotAspect = raw.w / raw.h;
|
|
4455
4748
|
for (const { d, plan } of cardPlans) {
|
|
4749
|
+
if (plan.stage) {
|
|
4750
|
+
const stageInput = {
|
|
4751
|
+
size: d.px,
|
|
4752
|
+
values: fill.values,
|
|
4753
|
+
sourceSeconds: meta.durationMs / 1e3,
|
|
4754
|
+
outputSeconds: duration,
|
|
4755
|
+
text: d.text,
|
|
4756
|
+
footageAspect: (meta.captureWidth ?? meta.width) / (meta.captureHeight ?? meta.height)
|
|
4757
|
+
};
|
|
4758
|
+
const staged = plan.stage === "tile" ? stageTile(stageInput) : stageSplitCover(stageInput);
|
|
4759
|
+
opts.onPhase?.(
|
|
4760
|
+
`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`
|
|
4761
|
+
);
|
|
4762
|
+
const shotDir = await mkdtemp(join5(tmpdir(), "vos-stage-"));
|
|
4763
|
+
try {
|
|
4764
|
+
const captured = await framesTake(browser, dir, {
|
|
4765
|
+
times: [heroTime],
|
|
4766
|
+
width: d.px.w,
|
|
4767
|
+
height: d.px.h,
|
|
4768
|
+
outDir: shotDir,
|
|
4769
|
+
overrides: {
|
|
4770
|
+
...opts.overrides,
|
|
4771
|
+
set: [...staged.set, ...opts.overrides?.set ?? []]
|
|
4772
|
+
}
|
|
4773
|
+
});
|
|
4774
|
+
const to2 = join5(outDir, `${d.id}.png`);
|
|
4775
|
+
await rename4(captured.frames[0].file, to2);
|
|
4776
|
+
const bytes2 = (await stat(to2)).size;
|
|
4777
|
+
if (d.maxBytes !== void 0 && bytes2 > d.maxBytes) {
|
|
4778
|
+
skipped.push(
|
|
4779
|
+
`${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`
|
|
4780
|
+
);
|
|
4781
|
+
continue;
|
|
4782
|
+
}
|
|
4783
|
+
assets.push({
|
|
4784
|
+
channel: d.channel,
|
|
4785
|
+
asset: d.asset,
|
|
4786
|
+
destination: d.id,
|
|
4787
|
+
path: relative(outDir, to2),
|
|
4788
|
+
w: d.px.w,
|
|
4789
|
+
h: d.px.h,
|
|
4790
|
+
bytes: bytes2,
|
|
4791
|
+
seconds: null,
|
|
4792
|
+
frameTime: heroTime,
|
|
4793
|
+
source: "stage",
|
|
4794
|
+
template: `${plan.stage}-stage`,
|
|
4795
|
+
text: staged.text,
|
|
4796
|
+
shot: staged.shot,
|
|
4797
|
+
...plan.stage === "tile" ? { crop: true } : {}
|
|
4798
|
+
});
|
|
4799
|
+
} finally {
|
|
4800
|
+
await rm3(shotDir, { recursive: true, force: true });
|
|
4801
|
+
}
|
|
4802
|
+
continue;
|
|
4803
|
+
}
|
|
4456
4804
|
const problems = templateProblems(plan.config);
|
|
4457
4805
|
if (problems.length) {
|
|
4458
4806
|
skipped.push(
|
|
@@ -4485,7 +4833,9 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4485
4833
|
opts.posterTime ?? posterDuration * 0.9,
|
|
4486
4834
|
Math.max(0, posterDuration - 0.05)
|
|
4487
4835
|
);
|
|
4488
|
-
opts.onPhase?.(
|
|
4836
|
+
opts.onPhase?.(
|
|
4837
|
+
`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`
|
|
4838
|
+
);
|
|
4489
4839
|
await renderPosterStills(
|
|
4490
4840
|
browser,
|
|
4491
4841
|
config,
|
|
@@ -4677,7 +5027,8 @@ var RUBRIC = [
|
|
|
4677
5027
|
"Name three ways this asset acknowledges THIS product (its data, its state, its brand), not a generic window."
|
|
4678
5028
|
];
|
|
4679
5029
|
function rolesFor(asset) {
|
|
4680
|
-
if (asset.source === "poster"
|
|
5030
|
+
if ((asset.source === "poster" || asset.source === "stage") && asset.template)
|
|
5031
|
+
return [asset.template.replace(/-stage$/, "")];
|
|
4681
5032
|
const spec = DESTINATIONS2.find((d) => d.id === asset.destination);
|
|
4682
5033
|
if (spec?.kind === "video") {
|
|
4683
5034
|
const portrait = spec.px.w < spec.px.h;
|
|
@@ -4844,9 +5195,11 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
4844
5195
|
return { sheets, skipped, outDir: out, verdictFile };
|
|
4845
5196
|
}
|
|
4846
5197
|
function winRate(verdicts) {
|
|
4847
|
-
const
|
|
4848
|
-
const wins =
|
|
4849
|
-
|
|
5198
|
+
const judgedList = verdicts.filter((v) => v.win !== null || (v.reasons?.length ?? 0) > 0);
|
|
5199
|
+
const wins = judgedList.filter((v) => v.win === true).length;
|
|
5200
|
+
const ties = judgedList.filter((v) => v.win === null).length;
|
|
5201
|
+
const judged = judgedList.length;
|
|
5202
|
+
return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
|
|
4850
5203
|
}
|
|
4851
5204
|
|
|
4852
5205
|
// src/plugin/kitPicture.ts
|
|
@@ -4897,7 +5250,7 @@ function stillFindings(a, img, m) {
|
|
|
4897
5250
|
const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
|
|
4898
5251
|
const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
|
|
4899
5252
|
const ink = inkCoverage(img, rect);
|
|
4900
|
-
if (ink < BLANK_INK) {
|
|
5253
|
+
if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
|
|
4901
5254
|
out.push({
|
|
4902
5255
|
code: "blank",
|
|
4903
5256
|
severity: "error",
|
|
@@ -4907,7 +5260,7 @@ function stillFindings(a, img, m) {
|
|
|
4907
5260
|
bbox: rect
|
|
4908
5261
|
});
|
|
4909
5262
|
}
|
|
4910
|
-
if (a.shot.w < 0.5 || a.shot.w > 1.2) {
|
|
5263
|
+
if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
|
|
4911
5264
|
out.push({
|
|
4912
5265
|
code: "subject",
|
|
4913
5266
|
severity: "error",
|
|
@@ -4995,7 +5348,12 @@ function stillFindings(a, img, m) {
|
|
|
4995
5348
|
function textFindings(a, img) {
|
|
4996
5349
|
const out = [];
|
|
4997
5350
|
for (const t of a.text ?? []) {
|
|
4998
|
-
const box = {
|
|
5351
|
+
const box = {
|
|
5352
|
+
x: t.x * img.w,
|
|
5353
|
+
y: t.y * img.h,
|
|
5354
|
+
w: t.w * img.w,
|
|
5355
|
+
h: t.h * img.h
|
|
5356
|
+
};
|
|
4999
5357
|
const name = t.label ? `"${t.label}"` : "a text box";
|
|
5000
5358
|
if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
|
|
5001
5359
|
out.push({
|
|
@@ -5069,19 +5427,38 @@ function duplicateFindings(stills) {
|
|
|
5069
5427
|
}
|
|
5070
5428
|
if (g.length > 1) groups.push(g);
|
|
5071
5429
|
}
|
|
5072
|
-
return groups.map((g) =>
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5430
|
+
return groups.map((g) => {
|
|
5431
|
+
const composed = g.every(
|
|
5432
|
+
(s) => stills.find((x) => x.destination === s.destination)?.composed
|
|
5433
|
+
);
|
|
5434
|
+
return {
|
|
5435
|
+
code: "duplicate",
|
|
5436
|
+
severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
|
|
5437
|
+
asset: g.map((s) => s.destination).join(", "),
|
|
5438
|
+
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(", ")}`,
|
|
5439
|
+
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"
|
|
5440
|
+
};
|
|
5441
|
+
});
|
|
5079
5442
|
}
|
|
5080
5443
|
async function videoFrame(file, at2, ffmpeg) {
|
|
5081
5444
|
try {
|
|
5082
5445
|
const { stdout } = await execFileP(
|
|
5083
5446
|
ffmpeg,
|
|
5084
|
-
[
|
|
5447
|
+
[
|
|
5448
|
+
"-v",
|
|
5449
|
+
"error",
|
|
5450
|
+
"-ss",
|
|
5451
|
+
at2.toFixed(3),
|
|
5452
|
+
"-i",
|
|
5453
|
+
file,
|
|
5454
|
+
"-frames:v",
|
|
5455
|
+
"1",
|
|
5456
|
+
"-f",
|
|
5457
|
+
"image2pipe",
|
|
5458
|
+
"-vcodec",
|
|
5459
|
+
"png",
|
|
5460
|
+
"-"
|
|
5461
|
+
],
|
|
5085
5462
|
{ encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
|
|
5086
5463
|
);
|
|
5087
5464
|
return decodePng(new Uint8Array(stdout));
|
|
@@ -5124,7 +5501,8 @@ async function pictureChecks(assets) {
|
|
|
5124
5501
|
destination: a.destination,
|
|
5125
5502
|
hash: m.hash,
|
|
5126
5503
|
time: null,
|
|
5127
|
-
genre: a.spec?.genre
|
|
5504
|
+
genre: a.spec?.genre,
|
|
5505
|
+
composed: !!a.shot
|
|
5128
5506
|
});
|
|
5129
5507
|
continue;
|
|
5130
5508
|
}
|
|
@@ -5143,14 +5521,19 @@ async function pictureChecks(assets) {
|
|
|
5143
5521
|
}
|
|
5144
5522
|
const seconds = a.seconds ?? 0;
|
|
5145
5523
|
const first = await videoFrame(a.file, 0, "ffmpeg");
|
|
5146
|
-
const last = await videoFrame(
|
|
5524
|
+
const last = await videoFrame(
|
|
5525
|
+
a.file,
|
|
5526
|
+
Math.max(0, seconds - 0.1),
|
|
5527
|
+
"ffmpeg"
|
|
5528
|
+
);
|
|
5147
5529
|
for (const [name, img] of [
|
|
5148
5530
|
["first", first],
|
|
5149
5531
|
["last", last]
|
|
5150
5532
|
]) {
|
|
5151
5533
|
if (!img) continue;
|
|
5152
5534
|
const m = measureStill(img);
|
|
5153
|
-
if (name === "first")
|
|
5535
|
+
if (name === "first")
|
|
5536
|
+
measured.push({ destination: a.destination, measure: m });
|
|
5154
5537
|
const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5155
5538
|
const ink = inkCoverage(img, subject);
|
|
5156
5539
|
if (ink < BLANK_INK) {
|
|
@@ -5176,7 +5559,11 @@ async function pictureChecks(assets) {
|
|
|
5176
5559
|
}
|
|
5177
5560
|
}
|
|
5178
5561
|
findings.push(...duplicateFindings(stills));
|
|
5179
|
-
const order = {
|
|
5562
|
+
const order = {
|
|
5563
|
+
error: 0,
|
|
5564
|
+
warning: 1,
|
|
5565
|
+
info: 2
|
|
5566
|
+
};
|
|
5180
5567
|
findings.sort((a, b) => order[a.severity] - order[b.severity]);
|
|
5181
5568
|
return { findings, measured };
|
|
5182
5569
|
}
|
|
@@ -5328,7 +5715,8 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5328
5715
|
text: a.text,
|
|
5329
5716
|
seconds,
|
|
5330
5717
|
composed: a.composed,
|
|
5331
|
-
shot: a.source === "poster" ? a.shot : void 0
|
|
5718
|
+
shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
|
|
5719
|
+
crop: a.crop
|
|
5332
5720
|
});
|
|
5333
5721
|
if (!spec) {
|
|
5334
5722
|
if (a.channel !== "demo")
|
|
@@ -7900,7 +8288,7 @@ function mostCommon(values) {
|
|
|
7900
8288
|
}
|
|
7901
8289
|
return best;
|
|
7902
8290
|
}
|
|
7903
|
-
function
|
|
8291
|
+
function firstFamily2(fontFamily) {
|
|
7904
8292
|
const first = fontFamily.split(",")[0]?.trim() ?? "";
|
|
7905
8293
|
return first.replace(/^["']|["']$/g, "");
|
|
7906
8294
|
}
|
|
@@ -8032,10 +8420,10 @@ function composeBrand(input) {
|
|
|
8032
8420
|
const bgC = mixHex2(bgA, accent, 0.14);
|
|
8033
8421
|
p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
|
|
8034
8422
|
p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
|
|
8035
|
-
const fontDisplay = design?.fonts[0] ?? (w.h1 ?
|
|
8423
|
+
const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily2(w.h1.fontFamily) : firstFamily2(w.body.fontFamily));
|
|
8036
8424
|
p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
|
|
8037
8425
|
const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
|
|
8038
|
-
const fontBody = designBody ??
|
|
8426
|
+
const fontBody = designBody ?? firstFamily2(w.body.fontFamily);
|
|
8039
8427
|
p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
|
|
8040
8428
|
const logoUrl = design?.logos.find((u) => /wordmark|logo/i.test(u)) ?? null;
|
|
8041
8429
|
const iconUrl = w.icons.find((u) => /apple-touch/i.test(u)) ?? (w.icons.length ? w.icons[0] : null);
|
|
@@ -8847,9 +9235,10 @@ ground (a paper site is a plate, a dark site is dark), and with no brand
|
|
|
8847
9235
|
the house gradient. Screenshot-genre stills never take a look.
|
|
8848
9236
|
Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the CWS
|
|
8849
9237
|
tile + marquee, GitHub social preview) COMPOSE by default: each renders
|
|
8850
|
-
from its destination's poster TEMPLATE (split-cover
|
|
8851
|
-
|
|
8852
|
-
|
|
9238
|
+
from its destination's poster TEMPLATE (split-cover is a STAGE, the
|
|
9239
|
+
take's own card leaning in perspective with its chrome and shadow beside
|
|
9240
|
+
a serif headline column, on the brand's ground; card-on-gradient is the
|
|
9241
|
+
shot alone on a mesh, the store's tile rule), filled with BRAND.md's colours and faces and the
|
|
8853
9242
|
release's words, the shot baked as an object (padded, rounded, shadowed,
|
|
8854
9243
|
a hairline on a light ground), PNG at exact pixels; kit.json records the
|
|
8855
9244
|
template and the text boxes. The headline is LAUNCH.md's headline role
|
|
@@ -9717,7 +10106,7 @@ async function cmdJudge(argv) {
|
|
|
9717
10106
|
${lines.join("\n ")}` : "") + (result.skipped.length ? `
|
|
9718
10107
|
skipped: ${result.skipped.join("\n skipped: ")}` : "") + `
|
|
9719
10108
|
Judge each pair both ways (the rubric is beside it) and fill ${result.verdictFile}` + (rate.judged ? `
|
|
9720
|
-
Win rate so far: ${rate.wins}
|
|
10109
|
+
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
10110
|
);
|
|
9722
10111
|
return EXIT_OK;
|
|
9723
10112
|
}
|
|
@@ -10008,4 +10397,4 @@ export {
|
|
|
10008
10397
|
convertAgentBrowser,
|
|
10009
10398
|
run
|
|
10010
10399
|
};
|
|
10011
|
-
//# sourceMappingURL=chunk-
|
|
10400
|
+
//# sourceMappingURL=chunk-PKHORUJ2.js.map
|