@vosjs/cli 0.17.3 → 0.18.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/README.md +16 -12
- package/dist/{chunk-WJSNY6FL.js → chunk-HYUVNBEM.js} +1281 -2044
- package/dist/chunk-HYUVNBEM.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +75 -1
- package/dist/index.js +1 -1
- package/dist/{run-3RLBXNHV.js → run-R7KDKIVK.js} +2 -2
- package/package.json +8 -8
- package/dist/chunk-WJSNY6FL.js.map +0 -1
- /package/dist/{run-3RLBXNHV.js.map → run-R7KDKIVK.js.map} +0 -0
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
} from "./chunk-NHKHTIDR.js";
|
|
7
7
|
|
|
8
8
|
// src/plugin/run.ts
|
|
9
|
-
import { mkdir as mkdir9, readFile as
|
|
10
|
-
import { existsSync as
|
|
11
|
-
import { join as
|
|
9
|
+
import { mkdir as mkdir9, readFile as readFile14, rm as rm4 } from "fs/promises";
|
|
10
|
+
import { existsSync as existsSync14 } from "fs";
|
|
11
|
+
import { join as join17, resolve as resolve9 } from "path";
|
|
12
12
|
import { totalDuration as totalDuration2 } from "@vosjs/timeline";
|
|
13
|
-
import { migrateHostedDoc as
|
|
13
|
+
import { migrateHostedDoc as migrateHostedDoc5, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
|
|
14
14
|
|
|
15
15
|
// src/plugin/args.ts
|
|
16
16
|
var UsageError = class extends Error {
|
|
@@ -1352,10 +1352,10 @@ function startTakeServer(rootDir, pages) {
|
|
|
1352
1352
|
res.writeHead(404).end();
|
|
1353
1353
|
});
|
|
1354
1354
|
return new Promise(
|
|
1355
|
-
(
|
|
1355
|
+
(resolve10) => server.listen(0, () => {
|
|
1356
1356
|
const addr = server.address();
|
|
1357
1357
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1358
|
-
|
|
1358
|
+
resolve10({ base: `http://localhost:${port}`, close: () => server.close() });
|
|
1359
1359
|
})
|
|
1360
1360
|
);
|
|
1361
1361
|
}
|
|
@@ -1389,7 +1389,7 @@ import { readFile as readFile2, rm as rm2 } from "fs/promises";
|
|
|
1389
1389
|
import { tweenRuntimeCode } from "@vosjs/tween/bundle";
|
|
1390
1390
|
import { elementsBundleCode } from "@vosjs/elements/bundle";
|
|
1391
1391
|
import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
|
|
1392
|
-
var MEDIABUNNY_URL = "https://esm.sh/mediabunny@1.
|
|
1392
|
+
var MEDIABUNNY_URL = "https://esm.sh/mediabunny@1.55.7?target=es2022";
|
|
1393
1393
|
var THREE_URL = "https://esm.sh/three@0.183.0?target=es2022";
|
|
1394
1394
|
var THREE_ADDONS = "https://esm.sh/three@0.183.0&target=es2022/examples/jsm/";
|
|
1395
1395
|
function renderPageHtml() {
|
|
@@ -1806,7 +1806,7 @@ async function resolveBackdropSlug(o) {
|
|
|
1806
1806
|
o.backgroundDuration = hit.duration;
|
|
1807
1807
|
}
|
|
1808
1808
|
function hasOverrides(o) {
|
|
1809
|
-
return !!(o.set?.length || o.frame !== void 0 || o.background !== void 0);
|
|
1809
|
+
return !!(o.set?.length || o.unset?.length || o.frame !== void 0 || o.background !== void 0);
|
|
1810
1810
|
}
|
|
1811
1811
|
var FRAME_KINDS = {
|
|
1812
1812
|
macos: "mac-light",
|
|
@@ -1913,6 +1913,16 @@ function applyDocOverrides(doc, o) {
|
|
|
1913
1913
|
setPath(d, path, value);
|
|
1914
1914
|
applied.push(`${path} = ${JSON.stringify(value)}`);
|
|
1915
1915
|
}
|
|
1916
|
+
for (const path of o.unset ?? []) {
|
|
1917
|
+
const segs = parsePath(path);
|
|
1918
|
+
let node = d;
|
|
1919
|
+
for (let i = 0; i < segs.length - 1 && node && typeof node === "object"; i++)
|
|
1920
|
+
node = node[segs[i].key];
|
|
1921
|
+
if (node && typeof node === "object") {
|
|
1922
|
+
delete node[segs[segs.length - 1].key];
|
|
1923
|
+
applied.push(`${path} removed`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1916
1926
|
return applied;
|
|
1917
1927
|
}
|
|
1918
1928
|
function applyAndValidate(doc, o) {
|
|
@@ -2476,8 +2486,8 @@ async function stillsInPage(opts) {
|
|
|
2476
2486
|
}
|
|
2477
2487
|
async function framesTake(browser, dir, opts) {
|
|
2478
2488
|
const take = await loadTake(dir);
|
|
2479
|
-
|
|
2480
|
-
|
|
2489
|
+
const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
|
|
2490
|
+
if (!doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
|
|
2481
2491
|
if (opts.overrides && hasOverrides(opts.overrides)) {
|
|
2482
2492
|
await resolveBackdropSlug(opts.overrides);
|
|
2483
2493
|
applyAndValidate(doc, opts.overrides);
|
|
@@ -2505,7 +2515,7 @@ async function framesTake(browser, dir, opts) {
|
|
|
2505
2515
|
}
|
|
2506
2516
|
}
|
|
2507
2517
|
if (opts.atMoments) {
|
|
2508
|
-
for (const m of await momentsFor(dir,
|
|
2518
|
+
for (const m of await momentsFor(dir, doc)) {
|
|
2509
2519
|
if (m.outputAt === null) continue;
|
|
2510
2520
|
shots.push({ time: clamp(m.outputAt), kind: "moment", momentId: m.id });
|
|
2511
2521
|
}
|
|
@@ -2618,9 +2628,9 @@ async function writeIndexJson(result) {
|
|
|
2618
2628
|
}
|
|
2619
2629
|
|
|
2620
2630
|
// src/plugin/deliver.ts
|
|
2621
|
-
import { mkdir as
|
|
2631
|
+
import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
|
|
2622
2632
|
import { tmpdir } from "os";
|
|
2623
|
-
import { join as
|
|
2633
|
+
import { join as join7, relative, resolve as resolve4 } from "path";
|
|
2624
2634
|
import { totalDuration } from "@vosjs/timeline";
|
|
2625
2635
|
import { existsSync as existsSync6 } from "fs";
|
|
2626
2636
|
import { readFile as readFile6 } from "fs/promises";
|
|
@@ -2741,114 +2751,6 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
2741
2751
|
};
|
|
2742
2752
|
}
|
|
2743
2753
|
|
|
2744
|
-
// src/plugin/posterStill.ts
|
|
2745
|
-
import { compileVosConfig as compileVosConfig3 } from "@vosjs/core";
|
|
2746
|
-
async function posterStillInPage(opts) {
|
|
2747
|
-
const w = window;
|
|
2748
|
-
const RW = opts.W * opts.ss;
|
|
2749
|
-
const RH = opts.H * opts.ss;
|
|
2750
|
-
try {
|
|
2751
|
-
const mod = await import(
|
|
2752
|
-
/* @vite-ignore */
|
|
2753
|
-
URL.createObjectURL(
|
|
2754
|
-
new Blob([opts.animationCode], { type: "text/javascript" })
|
|
2755
|
-
)
|
|
2756
|
-
);
|
|
2757
|
-
w.__vos__ = w.__vos__ || {};
|
|
2758
|
-
w.__vos__.isPaused = true;
|
|
2759
|
-
const deps = {
|
|
2760
|
-
THREE: w.__THREE__,
|
|
2761
|
-
gsap: w.__gsap__,
|
|
2762
|
-
resolution: {
|
|
2763
|
-
width: RW,
|
|
2764
|
-
height: RH,
|
|
2765
|
-
pixelRatio: 1,
|
|
2766
|
-
drawingBufferWidth: RW,
|
|
2767
|
-
drawingBufferHeight: RH
|
|
2768
|
-
},
|
|
2769
|
-
preserveDrawingBuffer: true
|
|
2770
|
-
};
|
|
2771
|
-
const result = await mod.initVos(document.body, deps);
|
|
2772
|
-
if (result.assetsReady) await result.assetsReady;
|
|
2773
|
-
const { timeline } = result;
|
|
2774
|
-
timeline.pause();
|
|
2775
|
-
timeline.seek(opts.time, false);
|
|
2776
|
-
const canvas = document.querySelector("canvas");
|
|
2777
|
-
canvas.width = RW;
|
|
2778
|
-
canvas.height = RH;
|
|
2779
|
-
canvas.style.width = RW + "px";
|
|
2780
|
-
canvas.style.height = RH + "px";
|
|
2781
|
-
const raf = () => new Promise((r) => requestAnimationFrame(r));
|
|
2782
|
-
await raf();
|
|
2783
|
-
await raf();
|
|
2784
|
-
await raf();
|
|
2785
|
-
let src = canvas;
|
|
2786
|
-
if (opts.ss > 1) {
|
|
2787
|
-
const out = document.createElement("canvas");
|
|
2788
|
-
out.width = opts.W;
|
|
2789
|
-
out.height = opts.H;
|
|
2790
|
-
const g = out.getContext("2d");
|
|
2791
|
-
g.imageSmoothingEnabled = true;
|
|
2792
|
-
g.imageSmoothingQuality = "high";
|
|
2793
|
-
g.drawImage(canvas, 0, 0, opts.W, opts.H);
|
|
2794
|
-
src = out;
|
|
2795
|
-
}
|
|
2796
|
-
const png = await new Promise(
|
|
2797
|
-
(res) => src.toBlob((b) => res(b), "image/png")
|
|
2798
|
-
);
|
|
2799
|
-
if (!png) throw new Error("toBlob returned null");
|
|
2800
|
-
await fetch("/save?name=" + opts.outName, { method: "POST", body: png });
|
|
2801
|
-
w.__done = { ok: true };
|
|
2802
|
-
} catch (e) {
|
|
2803
|
-
w.__error = String(e instanceof Error && e.stack || e);
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
async function renderPosterStills(browser, config, serveDir, shots, time) {
|
|
2807
|
-
const animationCode = compileVosConfig3(config, {
|
|
2808
|
-
tweenEngine: "vos"
|
|
2809
|
-
});
|
|
2810
|
-
const server = await startTakeServer(serveDir, {
|
|
2811
|
-
"/render.html": renderPageHtml()
|
|
2812
|
-
});
|
|
2813
|
-
try {
|
|
2814
|
-
for (const shot of shots) {
|
|
2815
|
-
const ss = stillSupersample(shot.width, shot.height);
|
|
2816
|
-
const context = await browser.newContext({
|
|
2817
|
-
viewport: { width: shot.width * ss, height: shot.height * ss }
|
|
2818
|
-
});
|
|
2819
|
-
try {
|
|
2820
|
-
const page = await context.newPage();
|
|
2821
|
-
page.on("console", (m) => {
|
|
2822
|
-
if (m.type() === "error")
|
|
2823
|
-
process.stderr.write(` [poster page] ${m.text()}
|
|
2824
|
-
`);
|
|
2825
|
-
});
|
|
2826
|
-
await page.addInitScript(() => {
|
|
2827
|
-
;
|
|
2828
|
-
globalThis.__name = (f) => f;
|
|
2829
|
-
});
|
|
2830
|
-
await page.goto(`${server.base}/render.html`);
|
|
2831
|
-
await page.waitForFunction("window.__pageReady__ === true");
|
|
2832
|
-
void page.evaluate(posterStillInPage, {
|
|
2833
|
-
animationCode,
|
|
2834
|
-
W: shot.width,
|
|
2835
|
-
H: shot.height,
|
|
2836
|
-
ss,
|
|
2837
|
-
time,
|
|
2838
|
-
outName: shot.name
|
|
2839
|
-
}).catch(() => {
|
|
2840
|
-
});
|
|
2841
|
-
await waitForPageDone(page, `poster ${shot.name}`, () => {
|
|
2842
|
-
}, 12e4);
|
|
2843
|
-
} finally {
|
|
2844
|
-
await context.close();
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
} finally {
|
|
2848
|
-
server.close();
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
2754
|
// src/plugin/moments.ts
|
|
2853
2755
|
import { ratedSegments as ratedSegments3, spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
|
|
2854
2756
|
|
|
@@ -3356,1242 +3258,516 @@ function pickMoments(measured, opts = {}) {
|
|
|
3356
3258
|
return { times: kept.map((k) => k.time), dropped };
|
|
3357
3259
|
}
|
|
3358
3260
|
|
|
3359
|
-
// src/plugin/
|
|
3360
|
-
import {
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3261
|
+
// src/plugin/motionPlan.ts
|
|
3262
|
+
import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
|
|
3263
|
+
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
3264
|
+
"x-feed-cut",
|
|
3265
|
+
"youtube-main-demo",
|
|
3266
|
+
"shorts-linkedin-vertical-cut"
|
|
3267
|
+
]);
|
|
3268
|
+
var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
3269
|
+
var BED_ID = "bed";
|
|
3270
|
+
var CLICK_ID_PREFIX = "click-";
|
|
3271
|
+
var CAPTION_ID_PREFIX = "caption-";
|
|
3272
|
+
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
3273
|
+
function pickTrack(catalog, ask) {
|
|
3274
|
+
if (!catalog || !ask || off(ask)) return null;
|
|
3275
|
+
const want = ask.trim().toLowerCase();
|
|
3276
|
+
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
3277
|
+
}
|
|
3278
|
+
function clickTimes(doc, range) {
|
|
3279
|
+
const rated = ratedSegments4(doc);
|
|
3280
|
+
const out = [];
|
|
3281
|
+
for (const e of doc.source.cursor) {
|
|
3282
|
+
if (e.type !== "down") continue;
|
|
3283
|
+
const src = e.t / 1e3;
|
|
3284
|
+
const ext = spanOutputExtent4(rated, src, src + 1e-3);
|
|
3285
|
+
if (!ext) continue;
|
|
3286
|
+
const t = ext.start;
|
|
3287
|
+
if (t < range[0] || t > range[1]) continue;
|
|
3288
|
+
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
3289
|
+
out.push(+(t - range[0]).toFixed(3));
|
|
3388
3290
|
}
|
|
3291
|
+
return out;
|
|
3389
3292
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
const shadowA = opts.shadow ?? 0.3;
|
|
3409
|
-
const blur = Math.round(shot.w * (opts.blur ?? 0.05));
|
|
3410
|
-
const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.02));
|
|
3411
|
-
const hair = opts.hairline ?? 0;
|
|
3412
|
-
const w = shot.w + margin * 2;
|
|
3413
|
-
const h = shot.h + margin * 2;
|
|
3414
|
-
const out = new Uint8Array(w * h * 4);
|
|
3415
|
-
const rect = { x: margin, y: margin, w: shot.w, h: shot.h };
|
|
3416
|
-
if (shadowA > 0) {
|
|
3417
|
-
const layers = [
|
|
3418
|
-
[Math.max(1, Math.round(blur / 4)), Math.round(offsetY / 5), 0.4],
|
|
3419
|
-
[blur, offsetY, 0.6]
|
|
3420
|
-
];
|
|
3421
|
-
const acc = new Float32Array(w * h);
|
|
3422
|
-
for (const [lb, lo, share] of layers) {
|
|
3423
|
-
const mask = new Float32Array(w * h);
|
|
3424
|
-
for (let y = 0; y < h; y++)
|
|
3425
|
-
for (let x = 0; x < w; x++) {
|
|
3426
|
-
const c = roundedCoverage(x, y - lo, rect, radius);
|
|
3427
|
-
if (c > 0) mask[y * w + x] = c;
|
|
3428
|
-
}
|
|
3429
|
-
blurPlane(mask, w, h, Math.max(1, Math.round(lb / 2)));
|
|
3430
|
-
const a = shadowA * share;
|
|
3431
|
-
for (let i = 0; i < w * h; i++)
|
|
3432
|
-
acc[i] = 1 - (1 - acc[i]) * (1 - mask[i] * a);
|
|
3433
|
-
}
|
|
3434
|
-
for (let i = 0; i < w * h; i++) {
|
|
3435
|
-
const a = acc[i];
|
|
3436
|
-
if (a <= 2e-3) continue;
|
|
3437
|
-
out[i * 4] = 0;
|
|
3438
|
-
out[i * 4 + 1] = 0;
|
|
3439
|
-
out[i * 4 + 2] = 0;
|
|
3440
|
-
out[i * 4 + 3] = Math.round(a * 255);
|
|
3441
|
-
}
|
|
3293
|
+
var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
|
|
3294
|
+
const rate = s.rate && s.rate > 0 ? s.rate : 1;
|
|
3295
|
+
return acc + (s.out - s.in) / rate;
|
|
3296
|
+
}, 0);
|
|
3297
|
+
function proposeMotion(input, opts) {
|
|
3298
|
+
const doc = structuredClone(input);
|
|
3299
|
+
const { words, launch, catalog } = opts;
|
|
3300
|
+
const notes = [];
|
|
3301
|
+
const skipped = [];
|
|
3302
|
+
const length = outputLength(doc);
|
|
3303
|
+
const range = [0, length];
|
|
3304
|
+
const entrance = launch.entrance;
|
|
3305
|
+
if (!off(entrance)) {
|
|
3306
|
+
const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
|
|
3307
|
+
doc.frame.entrance = { kind };
|
|
3308
|
+
notes.push(`entrance ${kind}`);
|
|
3309
|
+
} else {
|
|
3310
|
+
delete doc.frame.entrance;
|
|
3442
3311
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
const
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
);
|
|
3461
|
-
if (edge < 1.5) {
|
|
3462
|
-
r = Math.round(r * (1 - hair));
|
|
3463
|
-
g = Math.round(g * (1 - hair));
|
|
3464
|
-
b = Math.round(b * (1 - hair));
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
const a = cov;
|
|
3468
|
-
const ba = out[o + 3] / 255;
|
|
3469
|
-
const outA = a + ba * (1 - a);
|
|
3470
|
-
const mix = (fg, bg) => outA > 0 ? Math.round((fg * a + bg * ba * (1 - a)) / outA) : 0;
|
|
3471
|
-
out[o] = mix(r, out[o]);
|
|
3472
|
-
out[o + 1] = mix(g, out[o + 1]);
|
|
3473
|
-
out[o + 2] = mix(b, out[o + 2]);
|
|
3474
|
-
out[o + 3] = Math.round(outA * 255);
|
|
3312
|
+
if (!off(launch.endCard)) {
|
|
3313
|
+
const headline = (words.headline ?? "").trim();
|
|
3314
|
+
const brand = (words.brand ?? "").trim();
|
|
3315
|
+
const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
|
|
3316
|
+
if (headline || brand) {
|
|
3317
|
+
const card = { seconds: 2.5 };
|
|
3318
|
+
if (opts.ink) card.ink = opts.ink;
|
|
3319
|
+
if (headline) card.headline = headline;
|
|
3320
|
+
if (sub && sub !== headline) card.sub = sub;
|
|
3321
|
+
if (brand) card.wordmark = brand;
|
|
3322
|
+
if (opts.mark) card.mark = opts.mark;
|
|
3323
|
+
doc.endCard = card;
|
|
3324
|
+
notes.push("end card");
|
|
3325
|
+
} else {
|
|
3326
|
+
skipped.push(
|
|
3327
|
+
"no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)"
|
|
3328
|
+
);
|
|
3475
3329
|
}
|
|
3330
|
+
} else {
|
|
3331
|
+
delete doc.endCard;
|
|
3476
3332
|
}
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
const
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
}
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
const raw = new Uint8Array((stride + 1) * img.h);
|
|
3505
|
-
for (let y = 0; y < img.h; y++) {
|
|
3506
|
-
raw[y * (stride + 1)] = 2;
|
|
3507
|
-
for (let i = 0; i < stride; i++) {
|
|
3508
|
-
const cur = img.data[y * stride + i];
|
|
3509
|
-
const up = y ? img.data[(y - 1) * stride + i] : 0;
|
|
3510
|
-
raw[y * (stride + 1) + 1 + i] = cur - up & 255;
|
|
3333
|
+
const kept = (doc.overlays ?? []).filter(
|
|
3334
|
+
(o) => !o.id.startsWith(CAPTION_ID_PREFIX)
|
|
3335
|
+
);
|
|
3336
|
+
const captionClips = [];
|
|
3337
|
+
if (opts.captions.length && !off(launch.captions)) {
|
|
3338
|
+
const rated = ratedSegments4(doc);
|
|
3339
|
+
const steps = doc.source.meta.steps ?? [];
|
|
3340
|
+
for (const c of opts.captions) {
|
|
3341
|
+
const step = steps.find(
|
|
3342
|
+
(s) => c.id !== void 0 && s.id === c.id || s.step === c.step
|
|
3343
|
+
);
|
|
3344
|
+
if (!step || step.skipped) continue;
|
|
3345
|
+
const t = stepOutputTime(rated, step, 0.2);
|
|
3346
|
+
if (t === null || t < 0 || t > length - 1) continue;
|
|
3347
|
+
captionClips.push({
|
|
3348
|
+
id: `${CAPTION_ID_PREFIX}${c.step}`,
|
|
3349
|
+
kind: "text",
|
|
3350
|
+
text: c.caption,
|
|
3351
|
+
preset: "caption",
|
|
3352
|
+
start: +t.toFixed(3),
|
|
3353
|
+
duration: Math.min(3.5, Math.max(2.5, length - t - 0.2)),
|
|
3354
|
+
transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
|
|
3355
|
+
enter: "rise",
|
|
3356
|
+
exit: "fade",
|
|
3357
|
+
align: "center",
|
|
3358
|
+
box: { color: "rgba(17,17,17,0.72)" }
|
|
3359
|
+
});
|
|
3511
3360
|
}
|
|
3361
|
+
if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
|
|
3512
3362
|
}
|
|
3513
|
-
const
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
const
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3363
|
+
const overlays = [...kept, ...captionClips];
|
|
3364
|
+
if (overlays.length) doc.overlays = overlays;
|
|
3365
|
+
else delete doc.overlays;
|
|
3366
|
+
const clips = (doc.audio ?? []).filter(
|
|
3367
|
+
(a) => a.id !== BED_ID && !a.id.startsWith(CLICK_ID_PREFIX)
|
|
3368
|
+
);
|
|
3369
|
+
const track = pickTrack(catalog, launch.music);
|
|
3370
|
+
if (track) {
|
|
3371
|
+
const hasMic = !!doc.source.micKey;
|
|
3372
|
+
const fadeOut = Math.min(2.5, length * 0.15);
|
|
3373
|
+
clips.push({
|
|
3374
|
+
id: BED_ID,
|
|
3375
|
+
key: track.url,
|
|
3376
|
+
name: track.title,
|
|
3377
|
+
start: 0,
|
|
3378
|
+
in: 0,
|
|
3379
|
+
out: Math.min(track.duration, length),
|
|
3380
|
+
duration: track.duration,
|
|
3381
|
+
gain: hasMic ? 0.35 : 0.5,
|
|
3382
|
+
fadeIn: 0.6,
|
|
3383
|
+
fadeOut,
|
|
3384
|
+
loop: track.duration < length,
|
|
3385
|
+
loopLen: track.duration < length ? length : void 0,
|
|
3386
|
+
duck: hasMic
|
|
3387
|
+
});
|
|
3388
|
+
notes.push(`bed ${track.slug}`);
|
|
3389
|
+
} else if (launch.music && !off(launch.music)) {
|
|
3390
|
+
skipped.push(
|
|
3391
|
+
`music "${launch.music}" is not a catalog track or mood${catalog ? "" : " (the catalog could not be read)"}`
|
|
3392
|
+
);
|
|
3530
3393
|
}
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
const t = templateOf(config);
|
|
3548
|
-
if (!t) return ["no template block: a poster template declares config.template"];
|
|
3549
|
-
const out = [];
|
|
3550
|
-
const elements = Array.isArray(config.elements) ? config.elements : [];
|
|
3551
|
-
const byId = new Map(elements.filter((e) => e.id).map((e) => [e.id, e]));
|
|
3552
|
-
const params = Array.isArray(config.params) ? config.params : [];
|
|
3553
|
-
const keys = new Set(params.map((p) => p.key).filter(Boolean));
|
|
3554
|
-
if (!t.family) out.push("template.family is missing");
|
|
3555
|
-
for (const s of t.slots ?? []) {
|
|
3556
|
-
const el = byId.get(s.id);
|
|
3557
|
-
if (!el) out.push(`template.slots: no element with id "${s.id}"`);
|
|
3558
|
-
else if (el.type !== s.kind)
|
|
3559
|
-
out.push(`template.slots: "${s.id}" is a ${String(el.type)} element, the slot wants ${s.kind}`);
|
|
3560
|
-
}
|
|
3561
|
-
for (const x of t.text ?? []) {
|
|
3562
|
-
const el = byId.get(x.element);
|
|
3563
|
-
if (!el) out.push(`template.text: no element with id "${x.element}"`);
|
|
3564
|
-
else if (el.type !== "text") out.push(`template.text: "${x.element}" is not a text element`);
|
|
3565
|
-
if (!keys.has(x.param)) out.push(`template.text: param "${x.param}" is not declared in config.params`);
|
|
3566
|
-
const bound = el?.content;
|
|
3567
|
-
if (el && (!bound || typeof bound !== "object" || bound.$data !== x.param))
|
|
3568
|
-
out.push(`template.text: "${x.element}" must bind its content to {$data: "${x.param}"}`);
|
|
3569
|
-
}
|
|
3570
|
-
for (const k of t.params?.required ?? []) {
|
|
3571
|
-
if (!keys.has(k)) out.push(`template.params.required: "${k}" is not declared in config.params`);
|
|
3572
|
-
}
|
|
3573
|
-
if (!t.layouts?.landscape) out.push("template.layouts.landscape is required");
|
|
3574
|
-
for (const [name, layout] of Object.entries(t.layouts ?? {})) {
|
|
3575
|
-
for (const s of t.slots ?? []) {
|
|
3576
|
-
if (!layout?.slots?.[s.id]) out.push(`template.layouts.${name}: slot "${s.id}" is not placed`);
|
|
3394
|
+
const click = catalog?.sfx.find((s) => s.slug === "sfx-click");
|
|
3395
|
+
if (click && !doc.source.micKey && !off(launch.clicks)) {
|
|
3396
|
+
const times = clickTimes(doc, range);
|
|
3397
|
+
for (const [i, t] of times.entries()) {
|
|
3398
|
+
clips.push({
|
|
3399
|
+
id: `${CLICK_ID_PREFIX}${i}`,
|
|
3400
|
+
key: click.url,
|
|
3401
|
+
name: click.title,
|
|
3402
|
+
start: t,
|
|
3403
|
+
in: 0,
|
|
3404
|
+
out: click.duration,
|
|
3405
|
+
duration: click.duration,
|
|
3406
|
+
gain: 0.4,
|
|
3407
|
+
fadeIn: 0,
|
|
3408
|
+
fadeOut: 0
|
|
3409
|
+
});
|
|
3577
3410
|
}
|
|
3411
|
+
if (times.length) notes.push(`${times.length} click sound(s)`);
|
|
3578
3412
|
}
|
|
3579
|
-
|
|
3413
|
+
doc.audio = clips;
|
|
3414
|
+
return { doc, notes, skipped };
|
|
3580
3415
|
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
const pad = src?.pad ?? 0;
|
|
3605
|
-
slotRects[s.id] = {
|
|
3606
|
-
x: place.x,
|
|
3607
|
-
y: place.y,
|
|
3608
|
-
w: place.w,
|
|
3609
|
-
h: place.w * (input.size.w / input.size.h) / (src?.aspect ?? 16 / 9)
|
|
3610
|
-
};
|
|
3611
|
-
const w = place.w * designW * (1 + 2 * pad);
|
|
3612
|
-
const x = place.x - place.w * pad;
|
|
3613
|
-
const y = place.y - place.w * pad * (input.size.w / input.size.h) / (src?.aspect ?? 16 / 9);
|
|
3614
|
-
el.position = { x: `${(x * 100).toFixed(2)}%`, y: `${(y * 100).toFixed(2)}%` };
|
|
3615
|
-
el.anchor = "top-left";
|
|
3616
|
-
el.size = { ...el.size ?? {}, width: Math.round(w), height: "auto" };
|
|
3617
|
-
if (!src && s.required) el.opacity = 0;
|
|
3618
|
-
}
|
|
3619
|
-
const data = out.data && typeof out.data === "object" ? { ...out.data } : {};
|
|
3620
|
-
const params = Array.isArray(out.params) ? out.params : [];
|
|
3621
|
-
const missing = [];
|
|
3622
|
-
for (const k of [...t.params.required ?? [], ...t.params.brand ?? []]) {
|
|
3623
|
-
if (input.values[k] === void 0 || input.values[k] === null || input.values[k] === "") {
|
|
3624
|
-
if ((t.params.required ?? []).includes(k)) missing.push(k);
|
|
3625
|
-
continue;
|
|
3626
|
-
}
|
|
3627
|
-
data[k] = input.values[k];
|
|
3628
|
-
const p = params.find((q) => q.key === k);
|
|
3629
|
-
if (p) p.default = input.values[k];
|
|
3630
|
-
}
|
|
3631
|
-
for (const [k, v] of Object.entries(input.values)) {
|
|
3632
|
-
if (v !== void 0 && v !== null && v !== "") {
|
|
3633
|
-
data[k] = v;
|
|
3634
|
-
const p = params.find((q) => q.key === k);
|
|
3635
|
-
if (p) p.default = v;
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
3638
|
-
out.data = data;
|
|
3639
|
-
const boxes = [];
|
|
3640
|
-
for (const x of t.text) {
|
|
3641
|
-
const el = byId.get(x.element);
|
|
3642
|
-
if (!el) continue;
|
|
3643
|
-
const pos = layout.text?.[x.element];
|
|
3644
|
-
if (pos) el.position = { x: pos.x, y: pos.y };
|
|
3645
|
-
const size = layout.size?.[x.element];
|
|
3646
|
-
if (size) el.font = { ...el.font ?? {}, size };
|
|
3647
|
-
if (layout.hide?.includes(x.element)) {
|
|
3648
|
-
el.opacity = 0;
|
|
3649
|
-
continue;
|
|
3416
|
+
function destinationMechanics(d, doc) {
|
|
3417
|
+
const set = [];
|
|
3418
|
+
const unset = [];
|
|
3419
|
+
const notes = [];
|
|
3420
|
+
if (d.kind !== "video") return { set, unset, notes };
|
|
3421
|
+
const loop = LOOP_DESTINATIONS.has(d.id);
|
|
3422
|
+
const sound = SOUND_DESTINATIONS.has(d.id);
|
|
3423
|
+
const portrait = d.px.w / d.px.h < 0.9;
|
|
3424
|
+
if (loop) {
|
|
3425
|
+
unset.push("frame.entrance", "endCard");
|
|
3426
|
+
notes.push("loop: no entrance, no end card");
|
|
3427
|
+
}
|
|
3428
|
+
if (loop || !sound) {
|
|
3429
|
+
set.push("audio=[]");
|
|
3430
|
+
if (!loop) notes.push("silent channel");
|
|
3431
|
+
}
|
|
3432
|
+
if (loop || d.text === "none") {
|
|
3433
|
+
const kept = (doc.overlays ?? []).filter(
|
|
3434
|
+
(o) => !o.id.startsWith(CAPTION_ID_PREFIX)
|
|
3435
|
+
);
|
|
3436
|
+
if (kept.length !== (doc.overlays ?? []).length) {
|
|
3437
|
+
set.push(`overlays=${JSON.stringify(kept)}`);
|
|
3438
|
+
notes.push("no captions");
|
|
3650
3439
|
}
|
|
3651
|
-
const value = data[x.param];
|
|
3652
|
-
if (typeof value !== "string" || !value.trim()) continue;
|
|
3653
|
-
const font = el.font ?? {};
|
|
3654
|
-
const px = typeof font.size === "number" ? font.size : 40;
|
|
3655
|
-
const lines = value.split("\n");
|
|
3656
|
-
const longest = Math.max(...lines.map((l) => l.length));
|
|
3657
|
-
const lh = typeof font.lineHeight === "number" ? font.lineHeight : 1.15;
|
|
3658
|
-
const boxW = longest * px * 0.56;
|
|
3659
|
-
const boxH = lines.length * px * lh;
|
|
3660
|
-
const ex = pctOf(el.position?.x) ?? 0;
|
|
3661
|
-
const ey = pctOf(el.position?.y) ?? 0;
|
|
3662
|
-
const anchor = String(el.anchor ?? "center");
|
|
3663
|
-
let left = ex * designW;
|
|
3664
|
-
let top = ey * designH;
|
|
3665
|
-
if (anchor.includes("right")) left -= boxW;
|
|
3666
|
-
else if (!anchor.includes("left")) left -= boxW / 2;
|
|
3667
|
-
if (anchor.includes("bottom")) top -= boxH;
|
|
3668
|
-
else if (!anchor.includes("top")) top -= boxH / 2;
|
|
3669
|
-
const rawColor = font.color;
|
|
3670
|
-
const color = typeof rawColor === "string" ? rawColor : rawColor && typeof rawColor === "object" && typeof rawColor.$data === "string" ? data[rawColor.$data] : void 0;
|
|
3671
|
-
boxes.push({
|
|
3672
|
-
x: left / designW,
|
|
3673
|
-
y: top / designH,
|
|
3674
|
-
w: boxW / designW,
|
|
3675
|
-
h: boxH / designH,
|
|
3676
|
-
color: color && /^#[0-9a-f]{6}$/i.test(color) ? color : void 0,
|
|
3677
|
-
role: x.role,
|
|
3678
|
-
label: value.length > 24 ? `${value.slice(0, 24)}\u2026` : value
|
|
3679
|
-
});
|
|
3680
3440
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3441
|
+
if (portrait) {
|
|
3442
|
+
set.push("frame.fit=cover");
|
|
3443
|
+
set.push('frame.inset={"left":0.06,"right":0.06,"top":0.17,"bottom":0.17}');
|
|
3444
|
+
set.push("frame.focusFollow=camera");
|
|
3445
|
+
notes.push("vertical reframe follows the camera");
|
|
3684
3446
|
}
|
|
3685
|
-
return {
|
|
3447
|
+
return { set, unset, notes };
|
|
3686
3448
|
}
|
|
3687
|
-
function
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
if (x.lines && lines > x.lines)
|
|
3697
|
-
out.push(`${x.param}: ${lines} lines, the template holds ${x.lines}`);
|
|
3698
|
-
}
|
|
3699
|
-
return out;
|
|
3449
|
+
function endCardInk(look, brand) {
|
|
3450
|
+
if (!look) return null;
|
|
3451
|
+
const ground = look.ground;
|
|
3452
|
+
const m = /#([0-9a-f]{6})/i.exec(ground);
|
|
3453
|
+
const hex2 = m ? m[0] : null;
|
|
3454
|
+
const light = look.kind === "plate" || (hex2 ? isLightHexGround(hex2) : look.kind === "gradient");
|
|
3455
|
+
if (!light) return "#ffffff";
|
|
3456
|
+
const ink = brand?.ink;
|
|
3457
|
+
return ink && /^#[0-9a-f]{6}$/i.test(ink) ? ink : "#111111";
|
|
3700
3458
|
}
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
const
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
canvas.width = 1280
|
|
3708
|
-
canvas.height = 720
|
|
3709
|
-
const c = canvas.getContext('2d')
|
|
3710
|
-
c.fillStyle = d.bgA
|
|
3711
|
-
c.fillRect(0, 0, 1280, 720)
|
|
3712
|
-
const blob = (x, y, r, color) => {
|
|
3713
|
-
const g = c.createRadialGradient(x, y, 0, x, y, r)
|
|
3714
|
-
g.addColorStop(0, color)
|
|
3715
|
-
g.addColorStop(1, 'rgba(0,0,0,0)')
|
|
3716
|
-
c.fillStyle = g
|
|
3717
|
-
c.fillRect(0, 0, 1280, 720)
|
|
3718
|
-
}
|
|
3719
|
-
blob(180, 120, 760, d.blobA)
|
|
3720
|
-
blob(1120, 620, 820, d.blobB)
|
|
3721
|
-
blob(760, 60, 620, d.blobC)
|
|
3722
|
-
let seed = 7
|
|
3723
|
-
const rand = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296 }
|
|
3724
|
-
const img = c.getImageData(0, 0, 1280, 720)
|
|
3725
|
-
const px = img.data
|
|
3726
|
-
const grain = d.grain
|
|
3727
|
-
for (let i = 0; i < px.length; i += 4) {
|
|
3728
|
-
const n = (rand() - 0.5) * grain
|
|
3729
|
-
px[i] += n; px[i + 1] += n; px[i + 2] += n
|
|
3730
|
-
}
|
|
3731
|
-
c.putImageData(img, 0, 0)
|
|
3732
|
-
const tex = new THREE.CanvasTexture(canvas)
|
|
3733
|
-
tex.colorSpace = THREE.SRGBColorSpace
|
|
3734
|
-
const plane = new THREE.Mesh(
|
|
3735
|
-
new THREE.PlaneGeometry(2, 2),
|
|
3736
|
-
new THREE.MeshBasicMaterial({ map: tex, depthTest: false }),
|
|
3737
|
-
)
|
|
3738
|
-
plane.renderOrder = -10
|
|
3739
|
-
ctx.scene.add(plane)
|
|
3740
|
-
return { refs: { plane }, dispose: () => { tex.dispose(); plane.geometry.dispose(); plane.material.dispose() } }
|
|
3741
|
-
}`;
|
|
3742
|
-
var TIMELINE = `(ctx, content, duration) => {
|
|
3743
|
-
const { gsap, elements } = ctx
|
|
3744
|
-
const tl = gsap.timeline({ paused: true })
|
|
3745
|
-
const shot = elements.get('shot')
|
|
3746
|
-
if (shot) tl.fromTo(shot.props, { opacity: 0, translateY: 30, scale: 0.97 }, { opacity: 1, translateY: 0, scale: 1, duration: 1.0, ease: 'power3.out' }, 0.2)
|
|
3747
|
-
return tl
|
|
3748
|
-
}`;
|
|
3749
|
-
function cardOnGradient() {
|
|
3750
|
-
return {
|
|
3751
|
-
version: 2,
|
|
3752
|
-
duration: 4,
|
|
3753
|
-
camera: { preset: "fullscreen" },
|
|
3754
|
-
template: {
|
|
3755
|
-
family: "card-on-gradient",
|
|
3756
|
-
slots: [{ id: "shot", kind: "image", required: true }],
|
|
3757
|
-
params: { required: [], brand: ["bgA", "blobA", "blobB", "blobC", "grain"] },
|
|
3758
|
-
text: [],
|
|
3759
|
-
layouts: {
|
|
3760
|
-
landscape: { slots: { shot: { x: 0.09, y: 0.12, w: 0.82 } } },
|
|
3761
|
-
square: { slots: { shot: { x: 0.08, y: 0.27, w: 0.84 } } },
|
|
3762
|
-
portrait: { slots: { shot: { x: 0.06, y: 0.3, w: 0.88 } } }
|
|
3763
|
-
}
|
|
3764
|
-
},
|
|
3765
|
-
params: [
|
|
3766
|
-
{ key: "bgA", type: "color", label: "Ground", default: "#f3efe8" },
|
|
3767
|
-
{ key: "blobA", type: "color", label: "Blob A", default: "rgba(255,183,146,0.55)" },
|
|
3768
|
-
{ key: "blobB", type: "color", label: "Blob B", default: "rgba(170,196,255,0.5)" },
|
|
3769
|
-
{ key: "blobC", type: "color", label: "Blob C", default: "rgba(255,236,170,0.45)" },
|
|
3770
|
-
{ key: "grain", type: "number", label: "Grain", default: 14, min: 0, max: 40, step: 1 }
|
|
3771
|
-
],
|
|
3772
|
-
data: {
|
|
3773
|
-
bgA: "#f3efe8",
|
|
3774
|
-
blobA: "rgba(255,183,146,0.55)",
|
|
3775
|
-
blobB: "rgba(170,196,255,0.5)",
|
|
3776
|
-
blobC: "rgba(255,236,170,0.45)",
|
|
3777
|
-
grain: 14
|
|
3778
|
-
},
|
|
3779
|
-
elements: [
|
|
3780
|
-
{
|
|
3781
|
-
type: "image",
|
|
3782
|
-
id: "shot",
|
|
3783
|
-
src: "",
|
|
3784
|
-
position: { x: "9%", y: "12%" },
|
|
3785
|
-
anchor: "top-left",
|
|
3786
|
-
size: { width: 1574, height: "auto", fit: "contain" },
|
|
3787
|
-
zIndex: 60,
|
|
3788
|
-
opacity: 0
|
|
3789
|
-
}
|
|
3790
|
-
],
|
|
3791
|
-
createContent: GROUND,
|
|
3792
|
-
createTimeline: TIMELINE
|
|
3793
|
-
};
|
|
3459
|
+
function isLightHexGround(hex2) {
|
|
3460
|
+
const n = parseInt(hex2.slice(1), 16);
|
|
3461
|
+
const r = n >> 16 & 255;
|
|
3462
|
+
const g = n >> 8 & 255;
|
|
3463
|
+
const b = n & 255;
|
|
3464
|
+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
|
|
3794
3465
|
}
|
|
3795
3466
|
|
|
3796
|
-
// src/plugin/
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
const canvas = document.createElement('canvas')
|
|
3801
|
-
canvas.width = 1280
|
|
3802
|
-
canvas.height = 720
|
|
3803
|
-
const c = canvas.getContext('2d')
|
|
3804
|
-
const g = c.createLinearGradient(0, 0, 1280, 720)
|
|
3805
|
-
g.addColorStop(0, d.bgA)
|
|
3806
|
-
g.addColorStop(0.55, d.bgB)
|
|
3807
|
-
g.addColorStop(1, d.bgC)
|
|
3808
|
-
c.fillStyle = g
|
|
3809
|
-
c.fillRect(0, 0, 1280, 720)
|
|
3810
|
-
const rg = c.createRadialGradient(980, 120, 40, 980, 120, 820)
|
|
3811
|
-
rg.addColorStop(0, d.streak)
|
|
3812
|
-
rg.addColorStop(1, 'rgba(0,0,0,0)')
|
|
3813
|
-
c.fillStyle = rg
|
|
3814
|
-
c.fillRect(0, 0, 1280, 720)
|
|
3815
|
-
let seed = 41
|
|
3816
|
-
const rand = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296 }
|
|
3817
|
-
const img = c.getImageData(0, 0, 1280, 720)
|
|
3818
|
-
const px = img.data
|
|
3819
|
-
const grain = d.grain
|
|
3820
|
-
for (let i = 0; i < px.length; i += 4) {
|
|
3821
|
-
const n = (rand() - 0.5) * grain
|
|
3822
|
-
px[i] += n; px[i + 1] += n; px[i + 2] += n
|
|
3823
|
-
}
|
|
3824
|
-
c.putImageData(img, 0, 0)
|
|
3825
|
-
const tex = new THREE.CanvasTexture(canvas)
|
|
3826
|
-
tex.colorSpace = THREE.SRGBColorSpace
|
|
3827
|
-
const plane = new THREE.Mesh(
|
|
3828
|
-
new THREE.PlaneGeometry(2, 2),
|
|
3829
|
-
new THREE.MeshBasicMaterial({ map: tex, depthTest: false }),
|
|
3830
|
-
)
|
|
3831
|
-
plane.renderOrder = -10
|
|
3832
|
-
ctx.scene.add(plane)
|
|
3833
|
-
return { refs: { plane }, dispose: () => { tex.dispose(); plane.geometry.dispose(); plane.material.dispose() } }
|
|
3834
|
-
}`;
|
|
3835
|
-
var TIMELINE2 = `(ctx, content, duration) => {
|
|
3836
|
-
const { gsap, elements } = ctx
|
|
3837
|
-
const tl = gsap.timeline({ paused: true })
|
|
3838
|
-
const kicker = elements.get('kicker')
|
|
3839
|
-
const title = elements.get('title')
|
|
3840
|
-
const brand = elements.get('brand')
|
|
3841
|
-
const shot = elements.get('shot')
|
|
3842
|
-
if (kicker) tl.fromTo(kicker.props, { opacity: 0, translateY: 14 }, { opacity: 1, translateY: 0, duration: 0.7, ease: 'power2.out' }, 0.25)
|
|
3843
|
-
if (title) tl.fromTo(title.props, { opacity: 0, translateY: 26 }, { opacity: 1, translateY: 0, duration: 0.9, ease: 'power3.out' }, 0.45)
|
|
3844
|
-
if (brand) tl.fromTo(brand.props, { opacity: 0 }, { opacity: 1, duration: 0.8, ease: 'power2.out' }, 0.9)
|
|
3845
|
-
if (shot) tl.fromTo(shot.props, { opacity: 0, translateX: 70 }, { opacity: 1, translateX: 0, duration: 1.1, ease: 'power3.out' }, 0.55)
|
|
3846
|
-
return tl
|
|
3847
|
-
}`;
|
|
3848
|
-
function splitCover() {
|
|
3849
|
-
return {
|
|
3850
|
-
version: 2,
|
|
3851
|
-
duration: 6,
|
|
3852
|
-
camera: { preset: "fullscreen" },
|
|
3853
|
-
fonts: [
|
|
3854
|
-
{ family: "Fraunces", url: "https://assets.vos.so/fonts/fraunces/600.woff2", weight: 600 },
|
|
3855
|
-
{ family: "Lexend", url: "https://assets.vos.so/fonts/lexend/700.woff2", weight: 700 },
|
|
3856
|
-
{ family: "JetBrains Mono", url: "https://assets.vos.so/fonts/jetbrains-mono/400.woff2", weight: 400 }
|
|
3857
|
-
],
|
|
3858
|
-
template: {
|
|
3859
|
-
family: "split-cover",
|
|
3860
|
-
slots: [{ id: "shot", kind: "image", required: true }],
|
|
3861
|
-
params: {
|
|
3862
|
-
required: ["headline", "brand"],
|
|
3863
|
-
brand: ["bgA", "bgB", "bgC", "ink", "inkSoft", "accent", "streak", "fontDisplay", "fontBody", "grain"]
|
|
3864
|
-
},
|
|
3865
|
-
text: [
|
|
3866
|
-
{ element: "title", param: "headline", role: "headline", maxWords: 8, lines: 3 },
|
|
3867
|
-
{ element: "kicker", param: "kicker", role: "body" },
|
|
3868
|
-
{ element: "brand", param: "brand", role: "body" }
|
|
3869
|
-
],
|
|
3870
|
-
layouts: {
|
|
3871
|
-
landscape: {
|
|
3872
|
-
slots: { shot: { x: 0.46, y: 0.28, w: 1.02 } },
|
|
3873
|
-
text: { kicker: { x: "7%", y: "24%" }, title: { x: "7%", y: "44%" }, brand: { x: "7%", y: "86%" } },
|
|
3874
|
-
size: { title: 84, kicker: 19, brand: 25 }
|
|
3875
|
-
},
|
|
3876
|
-
square: {
|
|
3877
|
-
slots: { shot: { x: 0.1, y: 0.5, w: 1 } },
|
|
3878
|
-
text: { kicker: { x: "8%", y: "12%" }, title: { x: "8%", y: "27%" }, brand: { x: "8%", y: "93%" } },
|
|
3879
|
-
size: { title: 72, kicker: 18, brand: 23 }
|
|
3880
|
-
},
|
|
3881
|
-
portrait: {
|
|
3882
|
-
slots: { shot: { x: 0.08, y: 0.44, w: 1.06 } },
|
|
3883
|
-
text: { kicker: { x: "8%", y: "12%" }, title: { x: "8%", y: "26%" }, brand: { x: "8%", y: "94%" } },
|
|
3884
|
-
size: { title: 64, kicker: 17, brand: 22 }
|
|
3885
|
-
}
|
|
3886
|
-
}
|
|
3887
|
-
},
|
|
3888
|
-
params: [
|
|
3889
|
-
{ key: "kicker", type: "text", label: "Kicker", default: "RELEASE" },
|
|
3890
|
-
{ key: "headline", type: "text", label: "Headline", default: "What shipped,\nin three lines" },
|
|
3891
|
-
{ key: "brand", type: "text", label: "Wordmark", default: "brand" },
|
|
3892
|
-
{ key: "bgA", type: "color", label: "Ground A", default: "#8a3d2a" },
|
|
3893
|
-
{ key: "bgB", type: "color", label: "Ground B", default: "#c0632f" },
|
|
3894
|
-
{ key: "bgC", type: "color", label: "Ground C", default: "#5c5a2e" },
|
|
3895
|
-
{ key: "ink", type: "color", label: "Ink", default: "#fff6ec" },
|
|
3896
|
-
{ key: "inkSoft", type: "color", label: "Ink, softened", default: "#e9d9c8" },
|
|
3897
|
-
{ key: "streak", type: "color", label: "Light streak", default: "rgba(255,220,170,0.18)" },
|
|
3898
|
-
{ key: "fontDisplay", type: "text", label: "Headline face", default: "Fraunces, serif" },
|
|
3899
|
-
{ key: "fontBody", type: "text", label: "Body face", default: "Lexend, sans-serif" },
|
|
3900
|
-
{ key: "grain", type: "number", label: "Grain", default: 22, min: 0, max: 40, step: 1 }
|
|
3901
|
-
],
|
|
3902
|
-
data: {
|
|
3903
|
-
kicker: "RELEASE",
|
|
3904
|
-
headline: "What shipped,\nin three lines",
|
|
3905
|
-
brand: "brand",
|
|
3906
|
-
bgA: "#8a3d2a",
|
|
3907
|
-
bgB: "#c0632f",
|
|
3908
|
-
bgC: "#5c5a2e",
|
|
3909
|
-
ink: "#fff6ec",
|
|
3910
|
-
inkSoft: "#e9d9c8",
|
|
3911
|
-
streak: "rgba(255,220,170,0.18)",
|
|
3912
|
-
fontDisplay: "Fraunces, serif",
|
|
3913
|
-
fontBody: "Lexend, sans-serif",
|
|
3914
|
-
grain: 22
|
|
3915
|
-
},
|
|
3916
|
-
elements: [
|
|
3917
|
-
{
|
|
3918
|
-
type: "text",
|
|
3919
|
-
id: "kicker",
|
|
3920
|
-
content: { $data: "kicker" },
|
|
3921
|
-
position: { x: "7%", y: "24%" },
|
|
3922
|
-
anchor: "center-left",
|
|
3923
|
-
font: { family: "JetBrains Mono, monospace", size: 19, weight: 400, color: { $data: "inkSoft" }, letterSpacing: 6, align: "left" },
|
|
3924
|
-
opacity: 0
|
|
3925
|
-
},
|
|
3926
|
-
{
|
|
3927
|
-
type: "text",
|
|
3928
|
-
id: "title",
|
|
3929
|
-
content: { $data: "headline" },
|
|
3930
|
-
position: { x: "7%", y: "44%" },
|
|
3931
|
-
anchor: "center-left",
|
|
3932
|
-
font: { family: { $data: "fontDisplay" }, size: 84, weight: 600, color: { $data: "ink" }, lineHeight: 1.08, align: "left", letterSpacing: 0 },
|
|
3933
|
-
opacity: 0
|
|
3934
|
-
},
|
|
3935
|
-
{
|
|
3936
|
-
type: "text",
|
|
3937
|
-
id: "brand",
|
|
3938
|
-
content: { $data: "brand" },
|
|
3939
|
-
position: { x: "7%", y: "86%" },
|
|
3940
|
-
anchor: "center-left",
|
|
3941
|
-
font: { family: { $data: "fontBody" }, size: 25, weight: 700, color: { $data: "ink" }, letterSpacing: 1, align: "left" },
|
|
3942
|
-
opacity: 0
|
|
3943
|
-
},
|
|
3944
|
-
{
|
|
3945
|
-
type: "image",
|
|
3946
|
-
id: "shot",
|
|
3947
|
-
src: "",
|
|
3948
|
-
position: { x: "46%", y: "28%" },
|
|
3949
|
-
anchor: "top-left",
|
|
3950
|
-
size: { width: 1400, height: "auto", fit: "contain" },
|
|
3951
|
-
zIndex: 60,
|
|
3952
|
-
opacity: 0
|
|
3953
|
-
}
|
|
3954
|
-
],
|
|
3955
|
-
createContent: GROUND2,
|
|
3956
|
-
createTimeline: TIMELINE2
|
|
3957
|
-
};
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
// src/plugin/templates/index.ts
|
|
3961
|
-
var TEMPLATES = {
|
|
3962
|
-
"split-cover": splitCover,
|
|
3963
|
-
"card-on-gradient": cardOnGradient
|
|
3964
|
-
};
|
|
3965
|
-
var TEMPLATE_NAMES = Object.keys(TEMPLATES);
|
|
3966
|
-
function templateByName(name) {
|
|
3967
|
-
const make = TEMPLATES[name];
|
|
3968
|
-
return make ? make() : null;
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
|
-
// src/plugin/posterValues.ts
|
|
3467
|
+
// src/plugin/posterDoc.ts
|
|
3468
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3469
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3470
|
+
import { isAbsolute, join as join6, resolve as resolve3 } from "path";
|
|
3972
3471
|
import {
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
}
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
const pb = rgb(b);
|
|
3991
|
-
if (!pa || !pb) return a;
|
|
3992
|
-
return toHex([
|
|
3993
|
-
pa[0] + (pb[0] - pa[0]) * t,
|
|
3994
|
-
pa[1] + (pb[1] - pa[1]) * t,
|
|
3995
|
-
pa[2] + (pb[2] - pa[2]) * t
|
|
3996
|
-
]);
|
|
3997
|
-
}
|
|
3998
|
-
function rgba(hex2, alpha) {
|
|
3999
|
-
const c = rgb(hex2) ?? [255, 255, 255];
|
|
4000
|
-
return `rgba(${c[0]},${c[1]},${c[2]},${alpha})`;
|
|
4001
|
-
}
|
|
4002
|
-
function isLightHex(hex2) {
|
|
4003
|
-
const c = rgb(hex2);
|
|
4004
|
-
if (!c) return true;
|
|
4005
|
-
return (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255 >= 0.6;
|
|
4006
|
-
}
|
|
4007
|
-
function resolveFace(family, weights) {
|
|
4008
|
-
if (!family) return null;
|
|
4009
|
-
const first = family.split(",")[0].replace(/['"]/g, "").replace(/\s+variable$/i, "").trim();
|
|
4010
|
-
const entry = findFontFamily2(first);
|
|
4011
|
-
if (!entry) return null;
|
|
4012
|
-
const fonts = [
|
|
4013
|
-
...new Set(weights.map((w) => nearestFontWeight(entry, w)))
|
|
4014
|
-
].map((w) => ({
|
|
4015
|
-
family: entry.family,
|
|
4016
|
-
url: fontFaceUrl(entry.slug, w),
|
|
4017
|
-
weight: w
|
|
4018
|
-
}));
|
|
4019
|
-
return { stack: fontStack(entry), category: entry.category, fonts };
|
|
4020
|
-
}
|
|
4021
|
-
function posterValues(brand, words) {
|
|
4022
|
-
const b = brand ?? {};
|
|
4023
|
-
const bgA = b.bgA && HEX.test(b.bgA) ? b.bgA : null;
|
|
4024
|
-
const bgB = b.bgB && HEX.test(b.bgB) ? b.bgB : bgA;
|
|
4025
|
-
const bgC = b.bgC && HEX.test(b.bgC) ? b.bgC : bgB;
|
|
4026
|
-
const ink = b.ink && HEX.test(b.ink) ? b.ink : null;
|
|
4027
|
-
const accent = b.accent && HEX.test(b.accent) ? b.accent : null;
|
|
4028
|
-
const wordmark = (words.brand ?? b.wordmark ?? b.name ?? "").trim();
|
|
4029
|
-
const release = (words.release ?? "").trim();
|
|
4030
|
-
const lightGround = bgA ? isLightHex(bgA) : false;
|
|
4031
|
-
const values = {};
|
|
4032
|
-
if (bgA) values.bgA = bgA;
|
|
4033
|
-
if (bgB) values.bgB = bgB;
|
|
4034
|
-
if (bgC) values.bgC = bgC;
|
|
4035
|
-
if (ink) values.ink = ink;
|
|
4036
|
-
if (accent) values.accent = accent;
|
|
4037
|
-
if (ink && bgA) values.inkSoft = mixHex(ink, bgA, 0.28);
|
|
4038
|
-
if (accent) values.streak = rgba(accent, lightGround ? 0.12 : 0.18);
|
|
4039
|
-
if (bgA) values.grain = lightGround ? 10 : 22;
|
|
4040
|
-
if (accent) values.blobA = rgba(accent, lightGround ? 0.28 : 0.4);
|
|
4041
|
-
if (bgC) values.blobB = rgba(bgC, 0.75);
|
|
4042
|
-
if (accent)
|
|
4043
|
-
values.blobC = rgba(
|
|
4044
|
-
mixHex(accent, "#ffffff", 0.55),
|
|
4045
|
-
lightGround ? 0.35 : 0.25
|
|
3472
|
+
computeCardLayout,
|
|
3473
|
+
docRestTime,
|
|
3474
|
+
migrateHostedDoc,
|
|
3475
|
+
overlayRect,
|
|
3476
|
+
resolveOverlayStyle
|
|
3477
|
+
} from "@vosjs/studio-core";
|
|
3478
|
+
|
|
3479
|
+
// src/plugin/platform.ts
|
|
3480
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3481
|
+
import { homedir } from "os";
|
|
3482
|
+
import { join as join5 } from "path";
|
|
3483
|
+
function platformOrigin(flags = {}) {
|
|
3484
|
+
const legacyEnv = process.env.VOS_API_BASE?.trim();
|
|
3485
|
+
const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
|
|
3486
|
+
if (!flags.origin && !flags.api && !process.env.VOS_ORIGIN && legacyEnv) {
|
|
3487
|
+
process.stderr.write(
|
|
3488
|
+
"note: VOS_API_BASE is deprecated \u2014 set VOS_ORIGIN (an origin, no /api suffix)\n"
|
|
4046
3489
|
);
|
|
4047
|
-
if (wordmark) values.brand = wordmark;
|
|
4048
|
-
const headline = (words.headline ?? "").trim();
|
|
4049
|
-
if (headline) values.headline = headline;
|
|
4050
|
-
const kicker = (words.kicker ?? "").trim();
|
|
4051
|
-
values.kicker = kicker ? kicker : [wordmark, release].filter(Boolean).join(" ").toUpperCase();
|
|
4052
|
-
const fonts = [];
|
|
4053
|
-
const display = resolveFace(b.fontDisplay, [600, 700]);
|
|
4054
|
-
if (display) {
|
|
4055
|
-
values.fontDisplay = display.stack;
|
|
4056
|
-
fonts.push(...display.fonts);
|
|
4057
3490
|
}
|
|
4058
|
-
|
|
4059
|
-
if (body) {
|
|
4060
|
-
values.fontBody = body.stack;
|
|
4061
|
-
fonts.push(...body.fonts);
|
|
4062
|
-
}
|
|
4063
|
-
return { values, fonts, lightGround };
|
|
3491
|
+
return raw.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
4064
3492
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
const
|
|
4077
|
-
|
|
4078
|
-
const
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
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 };
|
|
4083
|
-
const shot = {
|
|
4084
|
-
x: inset.left,
|
|
4085
|
-
y: inset.top,
|
|
4086
|
-
w: 1 - inset.left - inset.right,
|
|
4087
|
-
h: 1 - inset.top - inset.bottom
|
|
4088
|
-
};
|
|
4089
|
-
const set = [
|
|
4090
|
-
`frame.background=${JSON.stringify(ground)}`,
|
|
4091
|
-
"frame.backgroundMedia=null",
|
|
4092
|
-
"frame.fit=cover",
|
|
4093
|
-
`frame.inset=${JSON.stringify(inset)}`,
|
|
4094
|
-
'frame.focus={"cx":0,"cy":0}',
|
|
4095
|
-
"frame.radius=16",
|
|
4096
|
-
"frame.shadow=0.5",
|
|
4097
|
-
"frame.shadowContact=0",
|
|
4098
|
-
"frame.border=0",
|
|
4099
|
-
"zoom=[]",
|
|
4100
|
-
`tilt=[{"id":"stage","in":0,"out":${input.sourceSeconds.toFixed(3)},"rx":3,"ry":${portrait || square ? 0 : 10}}]`,
|
|
4101
|
-
"cursor.visible=false",
|
|
4102
|
-
"cursor.clickFx.style=none"
|
|
4103
|
-
];
|
|
4104
|
-
const designW = 1080 * input.size.w / input.size.h;
|
|
4105
|
-
const boxes = [];
|
|
4106
|
-
const clips = [];
|
|
4107
|
-
const column = portrait || square ? 0.84 : 0.36;
|
|
4108
|
-
const left = portrait || square ? 0.08 : 0.07;
|
|
4109
|
-
const size = portrait ? 60 : square ? 68 : 84;
|
|
4110
|
-
const word = (id, text, preset, family, px, y, color, extra, role, x0 = left) => {
|
|
4111
|
-
if (!text) return;
|
|
4112
|
-
const lines = text.split("\n");
|
|
4113
|
-
const longest = Math.max(...lines.map((l) => l.length));
|
|
4114
|
-
const widest = Math.min(
|
|
4115
|
-
column,
|
|
4116
|
-
longest * px * (preset === "title" ? 0.5 : 0.62) / designW
|
|
4117
|
-
);
|
|
4118
|
-
const lh = preset === "title" ? 1.05 : 1.2;
|
|
4119
|
-
const h = lines.length * px * lh / 1080;
|
|
4120
|
-
clips.push({
|
|
4121
|
-
id,
|
|
4122
|
-
kind: "text",
|
|
4123
|
-
text,
|
|
4124
|
-
preset,
|
|
4125
|
-
family,
|
|
4126
|
-
size: px,
|
|
4127
|
-
color,
|
|
4128
|
-
align: "left",
|
|
4129
|
-
maxWidth: column,
|
|
4130
|
-
lineHeight: lh,
|
|
4131
|
-
transform: { x: x0 + widest / 2, y, scale: 1, rotation: 0 },
|
|
4132
|
-
// Words on the ground carry no footage shadow: a drop shadow under a
|
|
4133
|
-
// headline on a plate is the old-web tell.
|
|
4134
|
-
shadow: 0,
|
|
4135
|
-
start: 0,
|
|
4136
|
-
duration: +input.outputSeconds.toFixed(3),
|
|
4137
|
-
enter: "none",
|
|
4138
|
-
exit: "none",
|
|
4139
|
-
...extra
|
|
4140
|
-
});
|
|
4141
|
-
boxes.push({
|
|
4142
|
-
x: x0,
|
|
4143
|
-
y: y - h / 2,
|
|
4144
|
-
w: widest,
|
|
4145
|
-
h,
|
|
4146
|
-
color,
|
|
4147
|
-
role,
|
|
4148
|
-
label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text
|
|
4149
|
-
});
|
|
4150
|
-
};
|
|
4151
|
-
const markKey = str(v.logoKey, "");
|
|
4152
|
-
const markAspect = typeof v.logoAspect === "number" && v.logoAspect > 0 ? v.logoAspect : 1;
|
|
4153
|
-
const wideMark = !!markKey && markAspect > 2.2;
|
|
4154
|
-
const lockup = (px, y) => {
|
|
4155
|
-
if (!markKey) return left;
|
|
4156
|
-
const hPx = wideMark ? px * 1.6 : px * 1.3;
|
|
4157
|
-
const w = Math.min(column, hPx * markAspect / designW);
|
|
4158
|
-
clips.push({
|
|
4159
|
-
id: "stage-mark",
|
|
4160
|
-
kind: "image",
|
|
4161
|
-
key: markKey,
|
|
4162
|
-
width: +w.toFixed(4),
|
|
4163
|
-
radius: 0,
|
|
4164
|
-
shadow: "none",
|
|
4165
|
-
transform: { x: left + w / 2, y, scale: 1, rotation: 0 },
|
|
4166
|
-
start: 0,
|
|
4167
|
-
duration: +input.outputSeconds.toFixed(3),
|
|
4168
|
-
enter: "none",
|
|
4169
|
-
exit: "none"
|
|
4170
|
-
});
|
|
4171
|
-
return left + w + px * 0.5 / designW;
|
|
4172
|
-
};
|
|
4173
|
-
const kickerY = portrait ? 0.1 : square ? 0.12 : 0.24;
|
|
4174
|
-
const titleY = portrait ? 0.24 : square ? 0.28 : 0.46;
|
|
4175
|
-
const brandY = portrait ? 0.94 : square ? 0.94 : 0.86;
|
|
4176
|
-
word(
|
|
4177
|
-
"stage-kicker",
|
|
4178
|
-
kicker,
|
|
4179
|
-
"label",
|
|
4180
|
-
"JetBrains Mono",
|
|
4181
|
-
19,
|
|
4182
|
-
kickerY,
|
|
4183
|
-
inkSoft,
|
|
4184
|
-
{ letterSpacing: 6, weight: 400 },
|
|
4185
|
-
"body"
|
|
4186
|
-
);
|
|
4187
|
-
word(
|
|
4188
|
-
"stage-title",
|
|
4189
|
-
headline,
|
|
4190
|
-
"title",
|
|
4191
|
-
display,
|
|
4192
|
-
size,
|
|
4193
|
-
titleY,
|
|
4194
|
-
ink,
|
|
4195
|
-
{ weight: 600, letterSpacing: -Math.round(size * 0.02) },
|
|
4196
|
-
"headline"
|
|
3493
|
+
function parseVosId(input) {
|
|
3494
|
+
if (!/^https?:\/\//.test(input)) {
|
|
3495
|
+
if (/^[A-Za-z0-9_-]+$/.test(input)) return input;
|
|
3496
|
+
throw new UsageError(`"${input}" is not a vos id or URL`);
|
|
3497
|
+
}
|
|
3498
|
+
let url;
|
|
3499
|
+
try {
|
|
3500
|
+
url = new URL(input);
|
|
3501
|
+
} catch {
|
|
3502
|
+
throw new UsageError(`"${input}" is not a valid URL`);
|
|
3503
|
+
}
|
|
3504
|
+
const query = url.searchParams.get("vos");
|
|
3505
|
+
if (query) return query;
|
|
3506
|
+
const path = url.pathname.match(/\/vos\/([A-Za-z0-9_-]+)/);
|
|
3507
|
+
if (path) return path[1];
|
|
3508
|
+
throw new UsageError(
|
|
3509
|
+
`could not find a vos id in ${input} \u2014 expected /vos/{id} or ?vos={id}`
|
|
4197
3510
|
);
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
3511
|
+
}
|
|
3512
|
+
function deriveSlug(title) {
|
|
3513
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
|
|
3514
|
+
}
|
|
3515
|
+
var CREDENTIALS_PATH = join5(homedir(), ".config", "vos", "credentials");
|
|
3516
|
+
function resolveCredential(explicit) {
|
|
3517
|
+
const flag = explicit?.trim();
|
|
3518
|
+
if (flag) return flag;
|
|
3519
|
+
const env = process.env.VOS_API_KEY?.trim();
|
|
3520
|
+
if (env) return env;
|
|
3521
|
+
try {
|
|
3522
|
+
const first = readFileSync2(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
|
|
3523
|
+
return first || null;
|
|
3524
|
+
} catch {
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
function requireCredential(explicit) {
|
|
3529
|
+
const key = resolveCredential(explicit);
|
|
3530
|
+
if (!key) {
|
|
3531
|
+
throw new Error(
|
|
3532
|
+
"no credential found \u2014 pass --key, set VOS_API_KEY, or run `vos login` (mint a content key at https://vos.so/app/api; a vos_rg_ remix grant works too)"
|
|
4211
3533
|
);
|
|
4212
|
-
|
|
4213
|
-
return
|
|
3534
|
+
}
|
|
3535
|
+
return key;
|
|
4214
3536
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
const
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
const
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
const top = wordless ? square ? 0.1 : 0.12 : square ? 0.42 : 0.47;
|
|
4231
|
-
const visible = wordless ? square ? 0.66 : 0.8 : square ? 0.56 : 0.62;
|
|
4232
|
-
const cardW = (1 - left) / visible;
|
|
4233
|
-
const cardH = cardW * R / aspect;
|
|
4234
|
-
const inset = {
|
|
4235
|
-
left,
|
|
4236
|
-
right: +(1 - left - cardW).toFixed(4),
|
|
4237
|
-
top,
|
|
4238
|
-
bottom: +(1 - top - cardH).toFixed(4)
|
|
4239
|
-
};
|
|
4240
|
-
const shot = {
|
|
4241
|
-
x: inset.left,
|
|
4242
|
-
y: inset.top,
|
|
4243
|
-
w: 1 - inset.left - inset.right,
|
|
4244
|
-
h: 1 - inset.top - inset.bottom
|
|
4245
|
-
};
|
|
4246
|
-
const set = [
|
|
4247
|
-
`frame.background=${JSON.stringify(ground)}`,
|
|
4248
|
-
"frame.backgroundMedia=null",
|
|
4249
|
-
"frame.fit=cover",
|
|
4250
|
-
`frame.inset=${JSON.stringify(inset)}`,
|
|
4251
|
-
'frame.focus={"cx":0,"cy":0}',
|
|
4252
|
-
"frame.radius=24",
|
|
4253
|
-
"frame.shadow=0.5",
|
|
4254
|
-
"frame.shadowContact=0",
|
|
4255
|
-
"frame.border=0",
|
|
4256
|
-
"zoom=[]",
|
|
4257
|
-
"tilt=[]",
|
|
4258
|
-
"cursor.visible=false",
|
|
4259
|
-
"cursor.clickFx.style=none"
|
|
4260
|
-
];
|
|
4261
|
-
const designW = 1080 * input.size.w / input.size.h;
|
|
4262
|
-
const column = square ? 0.84 : 0.86;
|
|
4263
|
-
const px = square ? 96 : 108;
|
|
4264
|
-
const lines = headline.split("\n");
|
|
4265
|
-
const longest = Math.max(...lines.map((l) => l.length));
|
|
4266
|
-
const widest = Math.min(column, longest * px * 0.5 / designW);
|
|
4267
|
-
const lh = 1.05;
|
|
4268
|
-
const h = lines.length * px * lh / 1080;
|
|
4269
|
-
const y = square ? 0.2 : 0.24;
|
|
4270
|
-
const clip = {
|
|
4271
|
-
id: "stage-title",
|
|
4272
|
-
kind: "text",
|
|
4273
|
-
text: headline,
|
|
4274
|
-
preset: "title",
|
|
4275
|
-
family: display,
|
|
4276
|
-
size: px,
|
|
4277
|
-
color: ink,
|
|
4278
|
-
align: "left",
|
|
4279
|
-
maxWidth: column,
|
|
4280
|
-
lineHeight: lh,
|
|
4281
|
-
weight: 600,
|
|
4282
|
-
letterSpacing: -Math.round(px * 0.02),
|
|
4283
|
-
transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
|
|
4284
|
-
shadow: 0,
|
|
4285
|
-
start: 0,
|
|
4286
|
-
duration: +input.outputSeconds.toFixed(3),
|
|
4287
|
-
enter: "none",
|
|
4288
|
-
exit: "none"
|
|
3537
|
+
function writeCredential(key) {
|
|
3538
|
+
mkdirSync(join5(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
|
|
3539
|
+
writeFileSync(CREDENTIALS_PATH, `${key.trim()}
|
|
3540
|
+
`, { mode: 384 });
|
|
3541
|
+
return CREDENTIALS_PATH;
|
|
3542
|
+
}
|
|
3543
|
+
function clientId() {
|
|
3544
|
+
const env = process.env.VOS_CLIENT?.trim();
|
|
3545
|
+
if (env && env.length <= 60 && !/[\r\n]/.test(env)) return env;
|
|
3546
|
+
return "vos-cli";
|
|
3547
|
+
}
|
|
3548
|
+
async function apiJson(origin, path, init = {}) {
|
|
3549
|
+
const headers = {
|
|
3550
|
+
accept: "application/json",
|
|
3551
|
+
...init.headers
|
|
4289
3552
|
};
|
|
4290
|
-
if (
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
id: "stage-mark",
|
|
4303
|
-
kind: "image",
|
|
4304
|
-
key: markKey,
|
|
4305
|
-
width: +w.toFixed(4),
|
|
4306
|
-
radius: 0,
|
|
4307
|
-
shadow: "none",
|
|
4308
|
-
transform: { x: left + w / 2, y: top / 2, scale: 1, rotation: 0 },
|
|
4309
|
-
start: 0,
|
|
4310
|
-
duration: +input.outputSeconds.toFixed(3),
|
|
4311
|
-
enter: "none",
|
|
4312
|
-
exit: "none"
|
|
4313
|
-
}
|
|
4314
|
-
])}`
|
|
4315
|
-
);
|
|
4316
|
-
} else set.push("overlays=[]");
|
|
4317
|
-
return { set, text: [], shot };
|
|
3553
|
+
if (init.key) headers.authorization = `Bearer ${init.key}`;
|
|
3554
|
+
if (init.body !== void 0) headers["content-type"] = "application/json";
|
|
3555
|
+
const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
|
|
3556
|
+
const res = await fetch(`${origin}${path}`, {
|
|
3557
|
+
method: init.method ?? "GET",
|
|
3558
|
+
headers,
|
|
3559
|
+
body: reqBody
|
|
3560
|
+
});
|
|
3561
|
+
let body = {};
|
|
3562
|
+
try {
|
|
3563
|
+
body = await res.json();
|
|
3564
|
+
} catch {
|
|
4318
3565
|
}
|
|
4319
|
-
|
|
4320
|
-
const text = [
|
|
4321
|
-
{
|
|
4322
|
-
x: left,
|
|
4323
|
-
y: y - h / 2,
|
|
4324
|
-
w: widest,
|
|
4325
|
-
h,
|
|
4326
|
-
color: ink,
|
|
4327
|
-
role: "headline",
|
|
4328
|
-
label: headline.length > 24 ? `${headline.slice(0, 24)}\u2026` : headline
|
|
4329
|
-
}
|
|
4330
|
-
];
|
|
4331
|
-
return { set, text, shot };
|
|
3566
|
+
return { status: res.status, body };
|
|
4332
3567
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
import { isAbsolute, join as join5, resolve as resolve3 } from "path";
|
|
4338
|
-
function markExtension(url) {
|
|
4339
|
-
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4340
|
-
if (!m) return null;
|
|
4341
|
-
const e = m[1].toLowerCase();
|
|
4342
|
-
return e === "jpeg" ? "jpg" : e;
|
|
3568
|
+
function apiError(what, r) {
|
|
3569
|
+
const detail = typeof r.body.error === "string" ? r.body.error : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
|
|
3570
|
+
const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
|
|
3571
|
+
return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
|
|
4343
3572
|
}
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
);
|
|
4350
|
-
if (vb) return ratio(+vb[1], +vb[2]);
|
|
4351
|
-
const w = /<svg[^>]*\swidth\s*=\s*["']([\d.]+)/i.exec(head);
|
|
4352
|
-
const h = /<svg[^>]*\sheight\s*=\s*["']([\d.]+)/i.exec(head);
|
|
4353
|
-
if (w && h) return ratio(+w[1], +h[1]);
|
|
3573
|
+
var SYNC_STATE_NAME = "vos.json";
|
|
3574
|
+
function readJsonFile(file) {
|
|
3575
|
+
try {
|
|
3576
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
3577
|
+
} catch {
|
|
4354
3578
|
return null;
|
|
4355
3579
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
3580
|
+
}
|
|
3581
|
+
function readSyncState(dir) {
|
|
3582
|
+
const own = readJsonFile(join5(dir, SYNC_STATE_NAME));
|
|
3583
|
+
if (own && typeof own.vosId === "string") {
|
|
3584
|
+
return {
|
|
3585
|
+
vosId: own.vosId,
|
|
3586
|
+
versionId: typeof own.versionId === "string" ? own.versionId : null,
|
|
3587
|
+
...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
|
|
3588
|
+
...typeof own.title === "string" ? { title: own.title } : {},
|
|
3589
|
+
...typeof own.slug === "string" ? { slug: own.slug } : {},
|
|
3590
|
+
...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
|
|
3591
|
+
};
|
|
4360
3592
|
}
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
);
|
|
4369
|
-
if (tag === "VP8L") {
|
|
4370
|
-
const b = dv.getUint32(21, true);
|
|
4371
|
-
return ratio(1 + (b & 16383), 1 + (b >> 14 & 16383));
|
|
4372
|
-
}
|
|
4373
|
-
if (tag === "VP8 ")
|
|
4374
|
-
return ratio(
|
|
4375
|
-
dv.getUint16(26, true) & 16383,
|
|
4376
|
-
dv.getUint16(28, true) & 16383
|
|
4377
|
-
);
|
|
4378
|
-
return null;
|
|
3593
|
+
const push = readJsonFile(join5(dir, "push.json"));
|
|
3594
|
+
if (push && typeof push.vosId === "string") {
|
|
3595
|
+
return {
|
|
3596
|
+
vosId: push.vosId,
|
|
3597
|
+
versionId: typeof push.versionId === "string" ? push.versionId : null,
|
|
3598
|
+
...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
|
|
3599
|
+
};
|
|
4379
3600
|
}
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
i += 2 + len;
|
|
4390
|
-
}
|
|
4391
|
-
return null;
|
|
3601
|
+
const meta = readJsonFile(join5(dir, "meta.json"));
|
|
3602
|
+
if (meta && typeof meta.id === "string") {
|
|
3603
|
+
return {
|
|
3604
|
+
vosId: meta.id,
|
|
3605
|
+
versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
|
|
3606
|
+
...typeof meta.title === "string" ? { title: meta.title } : {},
|
|
3607
|
+
...typeof meta.slug === "string" ? { slug: meta.slug } : {},
|
|
3608
|
+
...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
|
|
3609
|
+
};
|
|
4392
3610
|
}
|
|
4393
3611
|
return null;
|
|
4394
3612
|
}
|
|
4395
|
-
function
|
|
4396
|
-
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
const out = { light: null, dark: null, notes: [] };
|
|
4400
|
-
if (!roles) return out;
|
|
4401
|
-
const one = async (role, name) => {
|
|
4402
|
-
const src = (roles[role] ?? "").trim();
|
|
4403
|
-
if (!src) return null;
|
|
4404
|
-
const ext = markExtension(src);
|
|
4405
|
-
if (!ext) {
|
|
4406
|
-
out.notes.push(
|
|
4407
|
-
`${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
|
|
4408
|
-
);
|
|
4409
|
-
return null;
|
|
4410
|
-
}
|
|
4411
|
-
let bytes;
|
|
4412
|
-
try {
|
|
4413
|
-
if (/^https?:/i.test(src)) {
|
|
4414
|
-
const res = await fetch(src);
|
|
4415
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
4416
|
-
bytes = new Uint8Array(await res.arrayBuffer());
|
|
4417
|
-
} else {
|
|
4418
|
-
const file = isAbsolute(src) ? src : resolve3(takeDir, src);
|
|
4419
|
-
if (!existsSync5(file)) throw new Error("no such file");
|
|
4420
|
-
bytes = new Uint8Array(await readFile5(file));
|
|
4421
|
-
}
|
|
4422
|
-
} catch (e) {
|
|
4423
|
-
out.notes.push(
|
|
4424
|
-
`${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
|
|
4425
|
-
);
|
|
4426
|
-
return null;
|
|
4427
|
-
}
|
|
4428
|
-
const aspect = imageAspect(bytes, ext);
|
|
4429
|
-
if (!aspect) {
|
|
4430
|
-
out.notes.push(
|
|
4431
|
-
`${role}: ${src} has no readable size; the wordmark stands in`
|
|
4432
|
-
);
|
|
4433
|
-
return null;
|
|
4434
|
-
}
|
|
4435
|
-
await mkdir4(join5(takeDir, "brand"), { recursive: true });
|
|
4436
|
-
await writeFile6(join5(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
4437
|
-
return { key: `/brand/${name}.${ext}`, aspect };
|
|
3613
|
+
function writeSyncState(dir, patch) {
|
|
3614
|
+
const merged = {
|
|
3615
|
+
...readSyncState(dir) ?? { versionId: null },
|
|
3616
|
+
...patch
|
|
4438
3617
|
};
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
|
|
4446
|
-
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
4447
|
-
"x-feed-cut",
|
|
4448
|
-
"youtube-main-demo",
|
|
4449
|
-
"shorts-linkedin-vertical-cut"
|
|
4450
|
-
]);
|
|
4451
|
-
var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
4452
|
-
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
4453
|
-
function pickTrack(catalog, ask) {
|
|
4454
|
-
if (!catalog || !ask || off(ask)) return null;
|
|
4455
|
-
const want = ask.trim().toLowerCase();
|
|
4456
|
-
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
4457
|
-
}
|
|
4458
|
-
function clickTimes(doc, range) {
|
|
4459
|
-
const rated = ratedSegments4(doc);
|
|
4460
|
-
const out = [];
|
|
4461
|
-
for (const e of doc.source.cursor) {
|
|
4462
|
-
if (e.type !== "down") continue;
|
|
4463
|
-
const src = e.t / 1e3;
|
|
4464
|
-
const ext = spanOutputExtent4(rated, src, src + 1e-3);
|
|
4465
|
-
if (!ext) continue;
|
|
4466
|
-
const t = ext.start;
|
|
4467
|
-
if (t < range[0] || t > range[1]) continue;
|
|
4468
|
-
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
4469
|
-
out.push(+(t - range[0]).toFixed(3));
|
|
4470
|
-
}
|
|
4471
|
-
return out;
|
|
3618
|
+
writeFileSync(
|
|
3619
|
+
join5(dir, SYNC_STATE_NAME),
|
|
3620
|
+
`${JSON.stringify(merged, null, 2)}
|
|
3621
|
+
`
|
|
3622
|
+
);
|
|
3623
|
+
return merged;
|
|
4472
3624
|
}
|
|
4473
|
-
function
|
|
4474
|
-
const
|
|
4475
|
-
const
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
const portrait = d.px.w / d.px.h < 0.9;
|
|
4483
|
-
const entrance = launch.entrance;
|
|
4484
|
-
if (!loop && !off(entrance)) {
|
|
4485
|
-
const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
|
|
4486
|
-
set.push(`frame.entrance={"kind":"${kind}"}`);
|
|
4487
|
-
notes.push(`entrance ${kind}`);
|
|
4488
|
-
}
|
|
4489
|
-
if (!loop && !off(launch.endCard)) {
|
|
4490
|
-
const headline = (words.headline ?? "").trim();
|
|
4491
|
-
const brand = (words.brand ?? "").trim();
|
|
4492
|
-
const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
|
|
4493
|
-
if (headline || brand) {
|
|
4494
|
-
const card = { seconds: 2.5 };
|
|
4495
|
-
if (input.ink) card.ink = input.ink;
|
|
4496
|
-
if (headline) card.headline = headline;
|
|
4497
|
-
if (sub && sub !== headline) card.sub = sub;
|
|
4498
|
-
if (brand) card.wordmark = brand;
|
|
4499
|
-
if (input.mark) card.mark = input.mark;
|
|
4500
|
-
set.push(`endCard=${JSON.stringify(card)}`);
|
|
4501
|
-
notes.push("end card");
|
|
4502
|
-
} else {
|
|
4503
|
-
skipped.push(
|
|
4504
|
-
`${d.id}: no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)`
|
|
4505
|
-
);
|
|
4506
|
-
}
|
|
3625
|
+
function formatChanges(changes) {
|
|
3626
|
+
const lines = [];
|
|
3627
|
+
for (const ch of changes) {
|
|
3628
|
+
const label = ch.label ? ` \xB7 ${ch.label}` : "";
|
|
3629
|
+
const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
|
|
3630
|
+
lines.push(
|
|
3631
|
+
`v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
|
|
3632
|
+
);
|
|
3633
|
+
if (ch.note) lines.push(` note: ${ch.note}`);
|
|
4507
3634
|
}
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
notes.push(`${clips.length} caption(s)`);
|
|
4538
|
-
}
|
|
3635
|
+
return lines;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// src/plugin/posterDoc.ts
|
|
3639
|
+
var POSTER_CLASSES = [
|
|
3640
|
+
"landscape",
|
|
3641
|
+
"square",
|
|
3642
|
+
"portrait",
|
|
3643
|
+
"tile"
|
|
3644
|
+
];
|
|
3645
|
+
var TILE_MAX_PX = 700;
|
|
3646
|
+
function posterClassFor(px) {
|
|
3647
|
+
if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
|
|
3648
|
+
const r = px.w / Math.max(1, px.h);
|
|
3649
|
+
if (r < 0.87) return "portrait";
|
|
3650
|
+
if (r <= 1.15) return "square";
|
|
3651
|
+
return "landscape";
|
|
3652
|
+
}
|
|
3653
|
+
var ROLE_ALL = "poster";
|
|
3654
|
+
var roleFor = (cls) => `poster-${cls}`;
|
|
3655
|
+
async function readPosterAt(cls, path, takeDir, from) {
|
|
3656
|
+
let file = path;
|
|
3657
|
+
let docDir = path;
|
|
3658
|
+
if (existsSync5(join6(path, "doc.json"))) {
|
|
3659
|
+
file = join6(path, "doc.json");
|
|
3660
|
+
} else if (!/\.json$/i.test(path) || !existsSync5(path)) {
|
|
3661
|
+
return null;
|
|
3662
|
+
} else {
|
|
3663
|
+
docDir = resolve3(path, "..");
|
|
4539
3664
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
3665
|
+
let raw;
|
|
3666
|
+
try {
|
|
3667
|
+
raw = JSON.parse(await readFile5(file, "utf8"));
|
|
3668
|
+
} catch {
|
|
3669
|
+
return null;
|
|
3670
|
+
}
|
|
3671
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3672
|
+
const source = raw.source;
|
|
3673
|
+
if (!source || typeof source.videoKey !== "string") return null;
|
|
3674
|
+
const doc = migrateHostedDoc(
|
|
3675
|
+
raw
|
|
3676
|
+
);
|
|
3677
|
+
const ownFootage = existsSync5(join6(docDir, "meta.json")) && existsSync5(join6(docDir, source.videoKey));
|
|
3678
|
+
const sync = readSyncState(docDir);
|
|
3679
|
+
return {
|
|
3680
|
+
cls,
|
|
3681
|
+
file,
|
|
3682
|
+
takeDir: ownFootage ? docDir : takeDir,
|
|
3683
|
+
ownFootage,
|
|
3684
|
+
doc,
|
|
3685
|
+
...sync?.vosId ? { vosId: sync.vosId } : {},
|
|
3686
|
+
from
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
async function findPosterDocs(takeDir, launchRoles) {
|
|
3690
|
+
const out = {};
|
|
3691
|
+
const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
|
|
3692
|
+
for (const cls of POSTER_CLASSES) {
|
|
3693
|
+
const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
|
|
3694
|
+
const candidates = [];
|
|
3695
|
+
if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
|
|
3696
|
+
const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
|
|
3697
|
+
candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
|
|
3698
|
+
}
|
|
3699
|
+
candidates.push({
|
|
3700
|
+
path: join6(takeDir, "poster", cls),
|
|
3701
|
+
from: `poster/${cls}`
|
|
3702
|
+
});
|
|
3703
|
+
candidates.push({ path: join6(takeDir, "poster"), from: "poster" });
|
|
3704
|
+
for (const c of candidates) {
|
|
3705
|
+
const ref = await readPosterAt(cls, c.path, takeDir, c.from);
|
|
3706
|
+
if (ref) {
|
|
3707
|
+
out[cls] = ref;
|
|
3708
|
+
break;
|
|
4583
3709
|
}
|
|
4584
|
-
if (times.length) notes.push(`${times.length} click sound(s)`);
|
|
4585
3710
|
}
|
|
4586
|
-
if (clips.length) set.push(`audio=${JSON.stringify(clips)}`);
|
|
4587
3711
|
}
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
3712
|
+
return out;
|
|
3713
|
+
}
|
|
3714
|
+
function posterStillTime(doc, duration) {
|
|
3715
|
+
const rest = docRestTime(doc);
|
|
3716
|
+
if (rest != null) return rest;
|
|
3717
|
+
return Math.max(0, duration - 1 / 30);
|
|
3718
|
+
}
|
|
3719
|
+
var DESIGN_H = 1080;
|
|
3720
|
+
function designFrame(px) {
|
|
3721
|
+
return {
|
|
3722
|
+
W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
|
|
3723
|
+
H: DESIGN_H
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
function posterShotRect(doc, px) {
|
|
3727
|
+
const { W, H } = designFrame(px);
|
|
3728
|
+
const meta = doc.source.meta;
|
|
3729
|
+
const layout = computeCardLayout(
|
|
3730
|
+
doc.frame,
|
|
3731
|
+
{
|
|
3732
|
+
width: meta.captureWidth ?? meta.width,
|
|
3733
|
+
height: meta.captureHeight ?? meta.height
|
|
3734
|
+
},
|
|
3735
|
+
W,
|
|
3736
|
+
H
|
|
3737
|
+
);
|
|
3738
|
+
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3739
|
+
return {
|
|
3740
|
+
x: r3(layout.cardX / W),
|
|
3741
|
+
y: r3(layout.cardY / H),
|
|
3742
|
+
w: r3(layout.cardW / W),
|
|
3743
|
+
h: r3(layout.cardH / H)
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
function estimateWidth(text, font, letterSpacingPx = 0) {
|
|
3747
|
+
const m = /(\d+(?:\.\d+)?)px/.exec(font);
|
|
3748
|
+
const px = m ? Number(m[1]) : 32;
|
|
3749
|
+
return text.length * (px * 0.55 + letterSpacingPx);
|
|
3750
|
+
}
|
|
3751
|
+
function posterTextBoxes(doc, px, time) {
|
|
3752
|
+
const { W, H } = designFrame(px);
|
|
3753
|
+
const out = [];
|
|
3754
|
+
for (const clip of doc.overlays ?? []) {
|
|
3755
|
+
if (clip.kind !== "text") continue;
|
|
3756
|
+
if (time < clip.start || time > clip.start + clip.duration) continue;
|
|
3757
|
+
const rect = overlayRect(clip, estimateWidth, W, H);
|
|
3758
|
+
const style = resolveOverlayStyle(clip);
|
|
3759
|
+
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3760
|
+
out.push({
|
|
3761
|
+
x: r3((rect.cx - rect.w / 2) / W),
|
|
3762
|
+
y: r3((rect.cy - rect.h / 2) / H),
|
|
3763
|
+
w: r3(rect.w / W),
|
|
3764
|
+
h: r3(rect.h / H),
|
|
3765
|
+
role: clip.preset === "title" ? "headline" : "body",
|
|
3766
|
+
label: clip.text.split("\n")[0].slice(0, 40),
|
|
3767
|
+
.../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
|
|
3768
|
+
});
|
|
4593
3769
|
}
|
|
4594
|
-
return
|
|
3770
|
+
return out;
|
|
4595
3771
|
}
|
|
4596
3772
|
|
|
4597
3773
|
// src/plugin/deliver.ts
|
|
@@ -4622,7 +3798,7 @@ function resolveChannels(raw) {
|
|
|
4622
3798
|
return out;
|
|
4623
3799
|
}
|
|
4624
3800
|
async function readBrandBesideTake(dir, explicit) {
|
|
4625
|
-
const candidates = explicit ? [explicit] : [
|
|
3801
|
+
const candidates = explicit ? [explicit] : [join7(dir, "BRAND.md"), join7(dir, "..", "BRAND.md")];
|
|
4626
3802
|
for (const file of candidates) {
|
|
4627
3803
|
if (!existsSync6(file)) continue;
|
|
4628
3804
|
const roles = parseFrontmatter(await readFile6(file, "utf8"));
|
|
@@ -4631,7 +3807,7 @@ async function readBrandBesideTake(dir, explicit) {
|
|
|
4631
3807
|
return null;
|
|
4632
3808
|
}
|
|
4633
3809
|
async function readLaunchBesideTake(dir, explicit) {
|
|
4634
|
-
const candidates = explicit ? [explicit] : [
|
|
3810
|
+
const candidates = explicit ? [explicit] : [join7(dir, "LAUNCH.md"), join7(dir, "..", "LAUNCH.md")];
|
|
4635
3811
|
for (const file of candidates) {
|
|
4636
3812
|
if (!existsSync6(file)) continue;
|
|
4637
3813
|
return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
|
|
@@ -4659,28 +3835,6 @@ async function resolveLook(dir, opts) {
|
|
|
4659
3835
|
roles
|
|
4660
3836
|
};
|
|
4661
3837
|
}
|
|
4662
|
-
function templateForCard(d, opts) {
|
|
4663
|
-
if (opts.poster === null) return null;
|
|
4664
|
-
if (opts.poster) return { config: opts.poster.config, from: opts.poster.from };
|
|
4665
|
-
if (!d.template) return null;
|
|
4666
|
-
if (isTileSize(d.px)) {
|
|
4667
|
-
const config2 = templateByName(d.template) ?? templateByName("card-on-gradient");
|
|
4668
|
-
if (config2) return { config: config2, from: "stage tile", stage: "tile" };
|
|
4669
|
-
}
|
|
4670
|
-
let name = d.template;
|
|
4671
|
-
let note;
|
|
4672
|
-
const wants = templateOf(templateByName(name) ?? {});
|
|
4673
|
-
const needsHeadline = wants?.text.some((t) => t.role === "headline");
|
|
4674
|
-
if (needsHeadline && !opts.words?.headline?.trim()) {
|
|
4675
|
-
name = "card-on-gradient";
|
|
4676
|
-
note = `${d.id}: no headline (LAUNCH.md headline: or --headline), so the ${d.template} template stands down for card-on-gradient`;
|
|
4677
|
-
}
|
|
4678
|
-
const config = templateByName(name);
|
|
4679
|
-
if (!config) return null;
|
|
4680
|
-
if (name === "split-cover")
|
|
4681
|
-
return { config, from: "stage split-cover", note, stage: "split-cover" };
|
|
4682
|
-
return { config, from: `template ${name}`, note };
|
|
4683
|
-
}
|
|
4684
3838
|
function lookOverrides(look, placement, size, video, opts) {
|
|
4685
3839
|
const inset = cardInset(look, size, video, placement);
|
|
4686
3840
|
const set = [
|
|
@@ -4708,23 +3862,6 @@ function lookOverrides(look, placement, size, video, opts) {
|
|
|
4708
3862
|
}
|
|
4709
3863
|
return set;
|
|
4710
3864
|
}
|
|
4711
|
-
function endCardInk(look, brand) {
|
|
4712
|
-
if (!look) return null;
|
|
4713
|
-
const ground = look.ground;
|
|
4714
|
-
const m = /#([0-9a-f]{6})/i.exec(ground);
|
|
4715
|
-
const hex2 = m ? m[0] : null;
|
|
4716
|
-
const light = look.kind === "plate" || (hex2 ? isLightHexGround(hex2) : look.kind === "gradient");
|
|
4717
|
-
if (!light) return "#ffffff";
|
|
4718
|
-
const ink = brand?.ink;
|
|
4719
|
-
return ink && /^#[0-9a-f]{6}$/i.test(ink) ? ink : "#111111";
|
|
4720
|
-
}
|
|
4721
|
-
function isLightHexGround(hex2) {
|
|
4722
|
-
const n = parseInt(hex2.slice(1), 16);
|
|
4723
|
-
const r = n >> 16 & 255;
|
|
4724
|
-
const g = n >> 8 & 255;
|
|
4725
|
-
const b = n & 255;
|
|
4726
|
-
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
|
|
4727
|
-
}
|
|
4728
3865
|
var PROBE_WIDTH = 640;
|
|
4729
3866
|
async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
4730
3867
|
const { candidates, dropped } = momentCandidates(doc, duration);
|
|
@@ -4737,7 +3874,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4737
3874
|
const meta = doc.source.meta;
|
|
4738
3875
|
const vw = meta.captureWidth ?? meta.width;
|
|
4739
3876
|
const vh = meta.captureHeight ?? meta.height;
|
|
4740
|
-
const probeDir = await mkdtemp(
|
|
3877
|
+
const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
|
|
4741
3878
|
try {
|
|
4742
3879
|
const probe = await framesTake(browser, dir, {
|
|
4743
3880
|
times: candidates.map((c) => c.time),
|
|
@@ -4822,16 +3959,10 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4822
3959
|
const take = await loadTake(dir);
|
|
4823
3960
|
if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
|
|
4824
3961
|
const doc = take.doc;
|
|
4825
|
-
const marks = await fetchBrandMarks(dir, opts.brandRoles);
|
|
4826
|
-
for (const n of marks.notes) opts.onPhase?.(`note: ${n}`);
|
|
4827
|
-
const darkGround = opts.look?.kind === "dark";
|
|
4828
|
-
const mark = darkGround ? marks.dark : marks.light;
|
|
4829
|
-
if (mark)
|
|
4830
|
-
opts.onPhase?.(`brand mark: ${mark.key} (${mark.aspect.toFixed(2)}:1)`);
|
|
4831
3962
|
const duration = totalDuration(ratedSegments5(doc));
|
|
4832
3963
|
const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
|
|
4833
|
-
const outDir = resolve4(opts.outDir ??
|
|
4834
|
-
await
|
|
3964
|
+
const outDir = resolve4(opts.outDir ?? join7(dir, "kit"));
|
|
3965
|
+
await mkdir4(outDir, { recursive: true });
|
|
4835
3966
|
const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
|
|
4836
3967
|
const assets = [];
|
|
4837
3968
|
const skipped = [];
|
|
@@ -4855,7 +3986,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4855
3986
|
w: meta0.captureWidth ?? meta0.width,
|
|
4856
3987
|
h: meta0.captureHeight ?? meta0.height
|
|
4857
3988
|
};
|
|
4858
|
-
const videoOverrides = (d
|
|
3989
|
+
const videoOverrides = (d) => {
|
|
4859
3990
|
const set = [];
|
|
4860
3991
|
if (opts.look) {
|
|
4861
3992
|
set.push(
|
|
@@ -4865,203 +3996,82 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4865
3996
|
})
|
|
4866
3997
|
);
|
|
4867
3998
|
}
|
|
4868
|
-
const
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
range: range ?? [0, duration],
|
|
4872
|
-
words: opts.words ?? {},
|
|
4873
|
-
launch: opts.launchRoles ?? {},
|
|
4874
|
-
captions: opts.captions ?? [],
|
|
4875
|
-
catalog: opts.catalog ?? null,
|
|
4876
|
-
ink: endCardInk(opts.look, opts.brandRoles),
|
|
4877
|
-
mark
|
|
4878
|
-
});
|
|
4879
|
-
set.push(...plan.set);
|
|
4880
|
-
if (plan.notes.length) opts.onPhase?.(`${d.id}: ${plan.notes.join(", ")}`);
|
|
4881
|
-
for (const s of plan.skipped) skipped.push(`note: ${s}`);
|
|
3999
|
+
const mech = destinationMechanics(d, doc);
|
|
4000
|
+
set.push(...mech.set);
|
|
4001
|
+
if (mech.notes.length) opts.onPhase?.(`${d.id}: ${mech.notes.join(", ")}`);
|
|
4882
4002
|
set.push(...opts.overrides?.set ?? []);
|
|
4883
|
-
if (!set.length) return opts.overrides;
|
|
4884
|
-
return { ...opts.overrides, set };
|
|
4003
|
+
if (!set.length && !mech.unset.length) return opts.overrides;
|
|
4004
|
+
return { ...opts.overrides, set, unset: mech.unset };
|
|
4885
4005
|
};
|
|
4886
|
-
const
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
const fill = posterValues(opts.brandRoles, opts.words ?? {});
|
|
4898
|
-
if (mark) {
|
|
4899
|
-
fill.values.logoKey = mark.key;
|
|
4900
|
-
fill.values.logoAspect = mark.aspect;
|
|
4006
|
+
const posters = await findPosterDocs(dir, opts.launchRoles);
|
|
4007
|
+
const posterCardIds = /* @__PURE__ */ new Set();
|
|
4008
|
+
const missing = /* @__PURE__ */ new Set();
|
|
4009
|
+
for (const d of destinations) {
|
|
4010
|
+
if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
|
|
4011
|
+
continue;
|
|
4012
|
+
const cls = posterClassFor(d.px);
|
|
4013
|
+
const ref = posters[cls];
|
|
4014
|
+
if (!ref) {
|
|
4015
|
+
missing.add(cls);
|
|
4016
|
+
continue;
|
|
4901
4017
|
}
|
|
4018
|
+
posterCardIds.add(d.id);
|
|
4019
|
+
const label = `${d.channel} ${d.asset}`;
|
|
4020
|
+
const posterDuration = totalDuration(ratedSegments5(ref.doc));
|
|
4021
|
+
const time = posterStillTime(ref.doc, posterDuration);
|
|
4902
4022
|
opts.onPhase?.(
|
|
4903
|
-
`
|
|
4023
|
+
`${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
|
|
4904
4024
|
);
|
|
4905
|
-
const
|
|
4025
|
+
const shotDir = await mkdtemp(join7(tmpdir(), "vos-poster-"));
|
|
4906
4026
|
try {
|
|
4907
|
-
const
|
|
4908
|
-
times: [
|
|
4909
|
-
width:
|
|
4910
|
-
height:
|
|
4911
|
-
outDir:
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
set: [
|
|
4915
|
-
"frame.fit=contain",
|
|
4916
|
-
"frame.padding=0",
|
|
4917
|
-
"frame.radius=0",
|
|
4918
|
-
"frame.shadow=0",
|
|
4919
|
-
"frame.border=0",
|
|
4920
|
-
"frame.browserBar.kind=none",
|
|
4921
|
-
"cursor.visible=false",
|
|
4922
|
-
"cursor.clickFx.style=none",
|
|
4923
|
-
...opts.overrides?.set ?? []
|
|
4924
|
-
]
|
|
4925
|
-
}
|
|
4926
|
-
});
|
|
4927
|
-
const raw = decodePng(new Uint8Array(await readFile6(shot.frames[0].file)));
|
|
4928
|
-
if (!raw) throw new Error("the poster shot could not be decoded");
|
|
4929
|
-
const PAD = 0.06;
|
|
4930
|
-
const baked = bakeShot(raw, {
|
|
4931
|
-
margin: PAD,
|
|
4932
|
-
hairline: fill.lightGround ? 0.14 : 0,
|
|
4933
|
-
shadow: fill.lightGround ? 0.28 : 0.4
|
|
4027
|
+
const captured = await framesTake(browser, ref.takeDir, {
|
|
4028
|
+
times: [time],
|
|
4029
|
+
width: d.px.w,
|
|
4030
|
+
height: d.px.h,
|
|
4031
|
+
outDir: shotDir,
|
|
4032
|
+
doc: ref.doc,
|
|
4033
|
+
overrides: opts.overrides
|
|
4934
4034
|
});
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
values: fill.values,
|
|
4942
|
-
sourceSeconds: meta.durationMs / 1e3,
|
|
4943
|
-
outputSeconds: duration,
|
|
4944
|
-
text: d.text,
|
|
4945
|
-
footageAspect: (meta.captureWidth ?? meta.width) / (meta.captureHeight ?? meta.height)
|
|
4946
|
-
};
|
|
4947
|
-
const staged = plan.stage === "tile" ? stageTile(stageInput) : stageSplitCover(stageInput);
|
|
4948
|
-
opts.onPhase?.(
|
|
4949
|
-
`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`
|
|
4950
|
-
);
|
|
4951
|
-
const shotDir = await mkdtemp(join6(tmpdir(), "vos-stage-"));
|
|
4952
|
-
try {
|
|
4953
|
-
const captured = await framesTake(browser, dir, {
|
|
4954
|
-
times: [heroTime],
|
|
4955
|
-
width: d.px.w,
|
|
4956
|
-
height: d.px.h,
|
|
4957
|
-
outDir: shotDir,
|
|
4958
|
-
overrides: {
|
|
4959
|
-
...opts.overrides,
|
|
4960
|
-
set: [...staged.set, ...opts.overrides?.set ?? []]
|
|
4961
|
-
}
|
|
4962
|
-
});
|
|
4963
|
-
const to2 = join6(outDir, `${d.id}.png`);
|
|
4964
|
-
await rename4(captured.frames[0].file, to2);
|
|
4965
|
-
const bytes2 = (await stat(to2)).size;
|
|
4966
|
-
if (d.maxBytes !== void 0 && bytes2 > d.maxBytes) {
|
|
4967
|
-
skipped.push(
|
|
4968
|
-
`${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`
|
|
4969
|
-
);
|
|
4970
|
-
continue;
|
|
4971
|
-
}
|
|
4972
|
-
assets.push({
|
|
4973
|
-
channel: d.channel,
|
|
4974
|
-
asset: d.asset,
|
|
4975
|
-
destination: d.id,
|
|
4976
|
-
path: relative(outDir, to2),
|
|
4977
|
-
w: d.px.w,
|
|
4978
|
-
h: d.px.h,
|
|
4979
|
-
bytes: bytes2,
|
|
4980
|
-
seconds: null,
|
|
4981
|
-
frameTime: heroTime,
|
|
4982
|
-
source: "stage",
|
|
4983
|
-
template: `${plan.stage}-stage`,
|
|
4984
|
-
text: staged.text,
|
|
4985
|
-
shot: staged.shot,
|
|
4986
|
-
...plan.stage === "tile" ? { crop: true } : {}
|
|
4987
|
-
});
|
|
4988
|
-
} finally {
|
|
4989
|
-
await rm3(shotDir, { recursive: true, force: true });
|
|
4990
|
-
}
|
|
4991
|
-
continue;
|
|
4992
|
-
}
|
|
4993
|
-
const problems = templateProblems(plan.config);
|
|
4994
|
-
if (problems.length) {
|
|
4995
|
-
skipped.push(
|
|
4996
|
-
`${d.channel} ${d.asset}: ${plan.from} is not a valid template (${problems[0]}) \u2014 kept from the take`
|
|
4997
|
-
);
|
|
4998
|
-
posterCardIds.delete(d.id);
|
|
4999
|
-
continue;
|
|
5000
|
-
}
|
|
5001
|
-
const filled = fillTemplate(plan.config, {
|
|
5002
|
-
size: d.px,
|
|
5003
|
-
slots: { shot: { src: "/shot.png", aspect: shotAspect, pad: PAD } },
|
|
5004
|
-
values: fill.values
|
|
5005
|
-
});
|
|
5006
|
-
const limits = textLimitProblems(templateOf(plan.config), fill.values);
|
|
5007
|
-
for (const l of limits) skipped.push(`note: ${d.id}: ${l}`);
|
|
5008
|
-
if (filled.missing.length) {
|
|
5009
|
-
skipped.push(
|
|
5010
|
-
`${d.channel} ${d.asset}: ${plan.from} needs ${filled.missing.join(", ")} \u2014 kept from the take`
|
|
5011
|
-
);
|
|
5012
|
-
posterCardIds.delete(d.id);
|
|
5013
|
-
continue;
|
|
5014
|
-
}
|
|
5015
|
-
const config = filled.config;
|
|
5016
|
-
if (fill.fonts.length) {
|
|
5017
|
-
const declared = Array.isArray(config.fonts) ? config.fonts : [];
|
|
5018
|
-
config.fonts = [...declared, ...fill.fonts];
|
|
5019
|
-
}
|
|
5020
|
-
const posterDuration = typeof config.duration === "number" ? config.duration : 6;
|
|
5021
|
-
const time = Math.min(
|
|
5022
|
-
opts.posterTime ?? posterDuration * 0.9,
|
|
5023
|
-
Math.max(0, posterDuration - 0.05)
|
|
5024
|
-
);
|
|
5025
|
-
opts.onPhase?.(
|
|
5026
|
-
`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`
|
|
5027
|
-
);
|
|
5028
|
-
await renderPosterStills(
|
|
5029
|
-
browser,
|
|
5030
|
-
config,
|
|
5031
|
-
serveDir,
|
|
5032
|
-
[{ name: `${d.id}.png`, width: d.px.w, height: d.px.h }],
|
|
5033
|
-
time
|
|
4035
|
+
const to = join7(outDir, `${d.id}.png`);
|
|
4036
|
+
await rename4(captured.frames[0].file, to);
|
|
4037
|
+
const bytes = (await stat(to)).size;
|
|
4038
|
+
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4039
|
+
skipped.push(
|
|
4040
|
+
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
5034
4041
|
);
|
|
5035
|
-
|
|
5036
|
-
const to = join6(outDir, `${d.id}.png`);
|
|
5037
|
-
await rename4(from, to);
|
|
5038
|
-
const bytes = (await stat(to)).size;
|
|
5039
|
-
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
5040
|
-
skipped.push(
|
|
5041
|
-
`${d.channel} ${d.asset}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
5042
|
-
);
|
|
5043
|
-
continue;
|
|
5044
|
-
}
|
|
5045
|
-
assets.push({
|
|
5046
|
-
channel: d.channel,
|
|
5047
|
-
asset: d.asset,
|
|
5048
|
-
destination: d.id,
|
|
5049
|
-
path: relative(outDir, to),
|
|
5050
|
-
w: d.px.w,
|
|
5051
|
-
h: d.px.h,
|
|
5052
|
-
bytes,
|
|
5053
|
-
seconds: null,
|
|
5054
|
-
frameTime: heroTime,
|
|
5055
|
-
source: "poster",
|
|
5056
|
-
template: templateOf(plan.config)?.family ?? plan.from,
|
|
5057
|
-
text: filled.text,
|
|
5058
|
-
shot: filled.slots.shot
|
|
5059
|
-
});
|
|
4042
|
+
continue;
|
|
5060
4043
|
}
|
|
4044
|
+
const shot = posterShotRect(ref.doc, d.px);
|
|
4045
|
+
assets.push({
|
|
4046
|
+
channel: d.channel,
|
|
4047
|
+
asset: d.asset,
|
|
4048
|
+
destination: d.id,
|
|
4049
|
+
path: relative(outDir, to),
|
|
4050
|
+
w: d.px.w,
|
|
4051
|
+
h: d.px.h,
|
|
4052
|
+
bytes,
|
|
4053
|
+
seconds: null,
|
|
4054
|
+
frameTime: time,
|
|
4055
|
+
source: "poster",
|
|
4056
|
+
poster: {
|
|
4057
|
+
class: cls,
|
|
4058
|
+
file: relative(dir, ref.file),
|
|
4059
|
+
...ref.vosId ? { vosId: ref.vosId } : {}
|
|
4060
|
+
},
|
|
4061
|
+
text: posterTextBoxes(ref.doc, d.px, time),
|
|
4062
|
+
shot,
|
|
4063
|
+
...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
|
|
4064
|
+
});
|
|
5061
4065
|
} finally {
|
|
5062
|
-
await rm3(
|
|
4066
|
+
await rm3(shotDir, { recursive: true, force: true });
|
|
5063
4067
|
}
|
|
5064
4068
|
}
|
|
4069
|
+
if (missing.size) {
|
|
4070
|
+
const classes = [...missing].sort().join(", ");
|
|
4071
|
+
skipped.push(
|
|
4072
|
+
`note: no poster document for the ${classes} class${missing.size > 1 ? "es" : ""} beside the take (poster/<class>/doc.json, or LAUNCH.md poster:), so those cards are the take's own frame`
|
|
4073
|
+
);
|
|
4074
|
+
}
|
|
5065
4075
|
for (const d of destinations) {
|
|
5066
4076
|
const label = `${d.channel} ${d.asset}`;
|
|
5067
4077
|
const excuse = NOT_FROM_FOOTAGE[d.id];
|
|
@@ -5094,7 +4104,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5094
4104
|
}
|
|
5095
4105
|
}
|
|
5096
4106
|
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
5097
|
-
const outFile =
|
|
4107
|
+
const outFile = join7(outDir, `${d.id}.${d.format}`);
|
|
5098
4108
|
const bitrate = d.maxBytes !== void 0 ? Math.min(
|
|
5099
4109
|
1e7,
|
|
5100
4110
|
Math.floor(d.maxBytes * 8 / seconds * 0.85)
|
|
@@ -5106,7 +4116,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5106
4116
|
parallel: opts.parallel,
|
|
5107
4117
|
range,
|
|
5108
4118
|
bitrate,
|
|
5109
|
-
overrides: videoOverrides(d
|
|
4119
|
+
overrides: videoOverrides(d),
|
|
5110
4120
|
onProgress: opts.onProgress
|
|
5111
4121
|
});
|
|
5112
4122
|
if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
|
|
@@ -5141,7 +4151,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5141
4151
|
for (let i = 0; i < captured.frames.length; i++) {
|
|
5142
4152
|
const frame = captured.frames[i];
|
|
5143
4153
|
const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
|
|
5144
|
-
const to =
|
|
4154
|
+
const to = join7(outDir, name);
|
|
5145
4155
|
await rename4(frame.file, to);
|
|
5146
4156
|
const bytes = (await stat(to)).size;
|
|
5147
4157
|
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
@@ -5163,25 +4173,135 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5163
4173
|
...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
|
|
5164
4174
|
});
|
|
5165
4175
|
}
|
|
5166
|
-
if (d.count && captured.frames.length < d.count.min) {
|
|
5167
|
-
skipped.push(
|
|
5168
|
-
`${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
|
|
4176
|
+
if (d.count && captured.frames.length < d.count.min) {
|
|
4177
|
+
skipped.push(
|
|
4178
|
+
`${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
const kit = {
|
|
4183
|
+
release: opts.release ?? null,
|
|
4184
|
+
look: opts.look?.kind ?? null,
|
|
4185
|
+
take: dir,
|
|
4186
|
+
produced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4187
|
+
specsVerified: CHANNEL_SPECS_VERIFIED,
|
|
4188
|
+
moments,
|
|
4189
|
+
skipped,
|
|
4190
|
+
assets
|
|
4191
|
+
};
|
|
4192
|
+
const kitFile = join7(outDir, "kit.json");
|
|
4193
|
+
await writeFile6(kitFile, JSON.stringify(kit, null, 2));
|
|
4194
|
+
return { kit, kitFile, outDir };
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// src/plugin/markAsset.ts
|
|
4198
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
|
|
4199
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4200
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve5 } from "path";
|
|
4201
|
+
function markExtension(url) {
|
|
4202
|
+
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4203
|
+
if (!m) return null;
|
|
4204
|
+
const e = m[1].toLowerCase();
|
|
4205
|
+
return e === "jpeg" ? "jpg" : e;
|
|
4206
|
+
}
|
|
4207
|
+
function imageAspect(bytes, ext) {
|
|
4208
|
+
if (ext === "svg") {
|
|
4209
|
+
const head = new TextDecoder().decode(bytes.subarray(0, 4096));
|
|
4210
|
+
const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(
|
|
4211
|
+
head
|
|
4212
|
+
);
|
|
4213
|
+
if (vb) return ratio(+vb[1], +vb[2]);
|
|
4214
|
+
const w = /<svg[^>]*\swidth\s*=\s*["']([\d.]+)/i.exec(head);
|
|
4215
|
+
const h = /<svg[^>]*\sheight\s*=\s*["']([\d.]+)/i.exec(head);
|
|
4216
|
+
if (w && h) return ratio(+w[1], +h[1]);
|
|
4217
|
+
return null;
|
|
4218
|
+
}
|
|
4219
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4220
|
+
if (ext === "png") {
|
|
4221
|
+
if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
|
|
4222
|
+
return ratio(dv.getUint32(16), dv.getUint32(20));
|
|
4223
|
+
}
|
|
4224
|
+
if (ext === "webp") {
|
|
4225
|
+
if (bytes.length < 30) return null;
|
|
4226
|
+
const tag = String.fromCharCode(...bytes.subarray(12, 16));
|
|
4227
|
+
if (tag === "VP8X")
|
|
4228
|
+
return ratio(
|
|
4229
|
+
1 + (dv.getUint32(24, true) & 16777215),
|
|
4230
|
+
1 + (dv.getUint32(27, true) & 16777215)
|
|
4231
|
+
);
|
|
4232
|
+
if (tag === "VP8L") {
|
|
4233
|
+
const b = dv.getUint32(21, true);
|
|
4234
|
+
return ratio(1 + (b & 16383), 1 + (b >> 14 & 16383));
|
|
4235
|
+
}
|
|
4236
|
+
if (tag === "VP8 ")
|
|
4237
|
+
return ratio(
|
|
4238
|
+
dv.getUint16(26, true) & 16383,
|
|
4239
|
+
dv.getUint16(28, true) & 16383
|
|
4240
|
+
);
|
|
4241
|
+
return null;
|
|
4242
|
+
}
|
|
4243
|
+
if (ext === "jpg") {
|
|
4244
|
+
let i = 2;
|
|
4245
|
+
while (i + 9 < bytes.length) {
|
|
4246
|
+
if (bytes[i] !== 255) return null;
|
|
4247
|
+
const marker = bytes[i + 1];
|
|
4248
|
+
const len = dv.getUint16(i + 2);
|
|
4249
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
4250
|
+
return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
|
|
4251
|
+
}
|
|
4252
|
+
i += 2 + len;
|
|
4253
|
+
}
|
|
4254
|
+
return null;
|
|
4255
|
+
}
|
|
4256
|
+
return null;
|
|
4257
|
+
}
|
|
4258
|
+
function ratio(w, h) {
|
|
4259
|
+
return w > 0 && h > 0 ? w / h : null;
|
|
4260
|
+
}
|
|
4261
|
+
async function fetchBrandMarks(takeDir, roles) {
|
|
4262
|
+
const out = { light: null, dark: null, notes: [] };
|
|
4263
|
+
if (!roles) return out;
|
|
4264
|
+
const one = async (role, name) => {
|
|
4265
|
+
const src = (roles[role] ?? "").trim();
|
|
4266
|
+
if (!src) return null;
|
|
4267
|
+
const ext = markExtension(src);
|
|
4268
|
+
if (!ext) {
|
|
4269
|
+
out.notes.push(
|
|
4270
|
+
`${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
|
|
4271
|
+
);
|
|
4272
|
+
return null;
|
|
4273
|
+
}
|
|
4274
|
+
let bytes;
|
|
4275
|
+
try {
|
|
4276
|
+
if (/^https?:/i.test(src)) {
|
|
4277
|
+
const res = await fetch(src);
|
|
4278
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
4279
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
4280
|
+
} else {
|
|
4281
|
+
const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
|
|
4282
|
+
if (!existsSync7(file)) throw new Error("no such file");
|
|
4283
|
+
bytes = new Uint8Array(await readFile7(file));
|
|
4284
|
+
}
|
|
4285
|
+
} catch (e) {
|
|
4286
|
+
out.notes.push(
|
|
4287
|
+
`${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
|
|
4288
|
+
);
|
|
4289
|
+
return null;
|
|
4290
|
+
}
|
|
4291
|
+
const aspect = imageAspect(bytes, ext);
|
|
4292
|
+
if (!aspect) {
|
|
4293
|
+
out.notes.push(
|
|
4294
|
+
`${role}: ${src} has no readable size; the wordmark stands in`
|
|
5169
4295
|
);
|
|
4296
|
+
return null;
|
|
5170
4297
|
}
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
look: opts.look?.kind ?? null,
|
|
5175
|
-
take: dir,
|
|
5176
|
-
produced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5177
|
-
specsVerified: CHANNEL_SPECS_VERIFIED,
|
|
5178
|
-
moments,
|
|
5179
|
-
skipped,
|
|
5180
|
-
assets
|
|
4298
|
+
await mkdir5(join8(takeDir, "brand"), { recursive: true });
|
|
4299
|
+
await writeFile7(join8(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
4300
|
+
return { key: `/brand/${name}.${ext}`, aspect };
|
|
5181
4301
|
};
|
|
5182
|
-
|
|
5183
|
-
await
|
|
5184
|
-
return
|
|
4302
|
+
out.light = await one("logoUrl", "mark");
|
|
4303
|
+
out.dark = await one("logoOnDarkUrl", "mark-on-dark");
|
|
4304
|
+
return out;
|
|
5185
4305
|
}
|
|
5186
4306
|
|
|
5187
4307
|
// src/plugin/music.ts
|
|
@@ -5197,10 +4317,69 @@ async function fetchMusicCatalog(origin) {
|
|
|
5197
4317
|
}
|
|
5198
4318
|
|
|
5199
4319
|
// src/plugin/judge.ts
|
|
5200
|
-
import { existsSync as
|
|
5201
|
-
import { mkdir as mkdir6, readFile as
|
|
5202
|
-
import { dirname, isAbsolute as
|
|
4320
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4321
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
|
|
4322
|
+
import { dirname, isAbsolute as isAbsolute3, join as join9, resolve as resolve6 } from "path";
|
|
5203
4323
|
import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
|
|
4324
|
+
|
|
4325
|
+
// src/plugin/shotBake.ts
|
|
4326
|
+
import { deflateSync } from "zlib";
|
|
4327
|
+
var crcTable = (() => {
|
|
4328
|
+
const t = new Uint32Array(256);
|
|
4329
|
+
for (let n = 0; n < 256; n++) {
|
|
4330
|
+
let c = n;
|
|
4331
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
4332
|
+
t[n] = c >>> 0;
|
|
4333
|
+
}
|
|
4334
|
+
return t;
|
|
4335
|
+
})();
|
|
4336
|
+
function crc32(buf) {
|
|
4337
|
+
let c = 4294967295;
|
|
4338
|
+
for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
|
|
4339
|
+
return (c ^ 4294967295) >>> 0;
|
|
4340
|
+
}
|
|
4341
|
+
function chunk(type, body) {
|
|
4342
|
+
const out = new Uint8Array(12 + body.length);
|
|
4343
|
+
const dv = new DataView(out.buffer);
|
|
4344
|
+
dv.setUint32(0, body.length);
|
|
4345
|
+
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
4346
|
+
out.set(body, 8);
|
|
4347
|
+
dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
|
|
4348
|
+
return out;
|
|
4349
|
+
}
|
|
4350
|
+
function encodePng(img) {
|
|
4351
|
+
const stride = img.w * 4;
|
|
4352
|
+
const raw = new Uint8Array((stride + 1) * img.h);
|
|
4353
|
+
for (let y = 0; y < img.h; y++) {
|
|
4354
|
+
raw[y * (stride + 1)] = 2;
|
|
4355
|
+
for (let i = 0; i < stride; i++) {
|
|
4356
|
+
const cur = img.data[y * stride + i];
|
|
4357
|
+
const up = y ? img.data[(y - 1) * stride + i] : 0;
|
|
4358
|
+
raw[y * (stride + 1) + 1 + i] = cur - up & 255;
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
const ihdr = new Uint8Array(13);
|
|
4362
|
+
const dv = new DataView(ihdr.buffer);
|
|
4363
|
+
dv.setUint32(0, img.w);
|
|
4364
|
+
dv.setUint32(4, img.h);
|
|
4365
|
+
ihdr[8] = 8;
|
|
4366
|
+
ihdr[9] = 6;
|
|
4367
|
+
const parts = [
|
|
4368
|
+
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
4369
|
+
chunk("IHDR", ihdr),
|
|
4370
|
+
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
4371
|
+
chunk("IEND", new Uint8Array(0))
|
|
4372
|
+
];
|
|
4373
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
4374
|
+
let o = 0;
|
|
4375
|
+
for (const p of parts) {
|
|
4376
|
+
out.set(p, o);
|
|
4377
|
+
o += p.length;
|
|
4378
|
+
}
|
|
4379
|
+
return out;
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
// src/plugin/judge.ts
|
|
5204
4383
|
var RUBRIC = [
|
|
5205
4384
|
"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.",
|
|
5206
4385
|
"The window is an object: rounded corners and a shadow with weight (a soft wide one on a light plate, a tight glow on a dark one).",
|
|
@@ -5303,26 +4482,26 @@ function composeSheet(left, right, height = 540, gutter = 32) {
|
|
|
5303
4482
|
return { w, h, data: out };
|
|
5304
4483
|
}
|
|
5305
4484
|
async function judgeKit(kitPath, manifestPath, outDir) {
|
|
5306
|
-
const kit = JSON.parse(await
|
|
5307
|
-
const manifest = JSON.parse(await
|
|
5308
|
-
const manifestDir = dirname(
|
|
5309
|
-
const kitDir = dirname(
|
|
5310
|
-
const out =
|
|
4485
|
+
const kit = JSON.parse(await readFile8(kitPath, "utf8"));
|
|
4486
|
+
const manifest = JSON.parse(await readFile8(manifestPath, "utf8"));
|
|
4487
|
+
const manifestDir = dirname(resolve6(manifestPath));
|
|
4488
|
+
const kitDir = dirname(resolve6(kitPath));
|
|
4489
|
+
const out = resolve6(outDir ?? join9(kitDir, "judge"));
|
|
5311
4490
|
await mkdir6(out, { recursive: true });
|
|
5312
4491
|
const sheets = [];
|
|
5313
4492
|
const skipped = [];
|
|
5314
4493
|
const refCache = /* @__PURE__ */ new Map();
|
|
5315
4494
|
const loadRef = async (file) => {
|
|
5316
4495
|
if (refCache.has(file)) return refCache.get(file) ?? null;
|
|
5317
|
-
const p =
|
|
5318
|
-
const img =
|
|
4496
|
+
const p = join9(manifestDir, file);
|
|
4497
|
+
const img = existsSync8(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
|
|
5319
4498
|
refCache.set(file, img);
|
|
5320
4499
|
return img;
|
|
5321
4500
|
};
|
|
5322
4501
|
for (const a of kit.assets) {
|
|
5323
4502
|
if (!/\.png$/i.test(a.path)) continue;
|
|
5324
|
-
const file =
|
|
5325
|
-
const img =
|
|
4503
|
+
const file = isAbsolute3(a.path) && existsSync8(a.path) ? a.path : join9(kitDir, a.path.split("/").pop() ?? a.path);
|
|
4504
|
+
const img = existsSync8(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
|
|
5326
4505
|
if (!img) {
|
|
5327
4506
|
skipped.push(`${a.destination ?? a.path}: unreadable`);
|
|
5328
4507
|
continue;
|
|
@@ -5342,8 +4521,8 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5342
4521
|
const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
|
|
5343
4522
|
const nameA = `${id}--A.png`;
|
|
5344
4523
|
const nameB = `${id}--B.png`;
|
|
5345
|
-
await writeFile8(
|
|
5346
|
-
await writeFile8(
|
|
4524
|
+
await writeFile8(join9(out, nameA), encodePng(composeSheet(img, refImg)));
|
|
4525
|
+
await writeFile8(join9(out, nameB), encodePng(composeSheet(refImg, img)));
|
|
5347
4526
|
const rubric = [
|
|
5348
4527
|
`# ${id} against ${ref.id} (${ref.file})`,
|
|
5349
4528
|
"",
|
|
@@ -5363,11 +4542,11 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5363
4542
|
"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.",
|
|
5364
4543
|
""
|
|
5365
4544
|
].join("\n");
|
|
5366
|
-
await writeFile8(
|
|
4545
|
+
await writeFile8(join9(out, `${id}.md`), rubric);
|
|
5367
4546
|
sheets.push({ asset: id, reference: ref.id, sheetA: nameA, sheetB: nameB, rubric: `${id}.md` });
|
|
5368
4547
|
}
|
|
5369
|
-
const verdictFile =
|
|
5370
|
-
if (!
|
|
4548
|
+
const verdictFile = join9(out, "judge.json");
|
|
4549
|
+
if (!existsSync8(verdictFile)) {
|
|
5371
4550
|
await writeFile8(
|
|
5372
4551
|
verdictFile,
|
|
5373
4552
|
JSON.stringify(
|
|
@@ -5394,7 +4573,7 @@ function winRate(verdicts) {
|
|
|
5394
4573
|
// src/plugin/kitPicture.ts
|
|
5395
4574
|
import { execFile } from "child_process";
|
|
5396
4575
|
import { promisify } from "util";
|
|
5397
|
-
import { readFile as
|
|
4576
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5398
4577
|
var execFileP = promisify(execFile);
|
|
5399
4578
|
var SUBJECT_BAND = { min: 0.6, max: 0.92 };
|
|
5400
4579
|
var BLANK_INK = 0.12;
|
|
@@ -5571,10 +4750,10 @@ function textFindings(a, img) {
|
|
|
5571
4750
|
});
|
|
5572
4751
|
}
|
|
5573
4752
|
}
|
|
5574
|
-
const
|
|
5575
|
-
if (
|
|
4753
|
+
const rgb = t.color ? hexToRgb(t.color) : null;
|
|
4754
|
+
if (rgb) {
|
|
5576
4755
|
const ground = medianColour(img, box);
|
|
5577
|
-
const lc = apcaContrast(
|
|
4756
|
+
const lc = apcaContrast(rgb, ground);
|
|
5578
4757
|
const floor = t.role === "body" ? 75 : 60;
|
|
5579
4758
|
if (lc < floor) {
|
|
5580
4759
|
out.push({
|
|
@@ -5625,7 +4804,7 @@ function duplicateFindings(stills) {
|
|
|
5625
4804
|
severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
|
|
5626
4805
|
asset: g.map((s) => s.destination).join(", "),
|
|
5627
4806
|
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(", ")}`,
|
|
5628
|
-
fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster
|
|
4807
|
+
fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster document per class (poster/<class>/doc.json) or per destination (LAUNCH.md poster roles) gives each channel its own cover"
|
|
5629
4808
|
};
|
|
5630
4809
|
});
|
|
5631
4810
|
}
|
|
@@ -5670,7 +4849,7 @@ async function pictureChecks(assets) {
|
|
|
5670
4849
|
let ffmpeg = null;
|
|
5671
4850
|
for (const a of assets) {
|
|
5672
4851
|
if (/\.png$/i.test(a.file)) {
|
|
5673
|
-
const img = decodePng(new Uint8Array(await
|
|
4852
|
+
const img = decodePng(new Uint8Array(await readFile9(a.file)));
|
|
5674
4853
|
if (!img) {
|
|
5675
4854
|
findings.push({
|
|
5676
4855
|
code: "unreadable",
|
|
@@ -5763,9 +4942,9 @@ function formatFinding(f) {
|
|
|
5763
4942
|
}
|
|
5764
4943
|
|
|
5765
4944
|
// src/plugin/validateKit.ts
|
|
5766
|
-
import { existsSync as
|
|
5767
|
-
import { readFile as
|
|
5768
|
-
import { dirname as dirname2, isAbsolute as
|
|
4945
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4946
|
+
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
4947
|
+
import { dirname as dirname2, isAbsolute as isAbsolute4, join as join10 } from "path";
|
|
5769
4948
|
import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
|
|
5770
4949
|
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5771
4950
|
function pngDimensions(bytes) {
|
|
@@ -5784,7 +4963,7 @@ async function probeVideo(path) {
|
|
|
5784
4963
|
const MB = await import("mediabunny");
|
|
5785
4964
|
const input = new MB.Input({
|
|
5786
4965
|
formats: MB.ALL_FORMATS,
|
|
5787
|
-
source: new MB.BufferSource(new Uint8Array(await
|
|
4966
|
+
source: new MB.BufferSource(new Uint8Array(await readFile10(path)))
|
|
5788
4967
|
});
|
|
5789
4968
|
try {
|
|
5790
4969
|
const track = await input.getPrimaryVideoTrack();
|
|
@@ -5805,7 +4984,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5805
4984
|
const measured = [];
|
|
5806
4985
|
let kit;
|
|
5807
4986
|
try {
|
|
5808
|
-
kit = JSON.parse(await
|
|
4987
|
+
kit = JSON.parse(await readFile10(kitPath, "utf8"));
|
|
5809
4988
|
} catch (e) {
|
|
5810
4989
|
return {
|
|
5811
4990
|
valid: false,
|
|
@@ -5824,8 +5003,8 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5824
5003
|
}
|
|
5825
5004
|
const base = dirname2(kitPath);
|
|
5826
5005
|
const resolvePath = (p) => {
|
|
5827
|
-
if (
|
|
5828
|
-
const beside =
|
|
5006
|
+
if (isAbsolute4(p) && existsSync9(p)) return p;
|
|
5007
|
+
const beside = join10(base, p.split("/").pop() ?? p);
|
|
5829
5008
|
return beside;
|
|
5830
5009
|
};
|
|
5831
5010
|
const perDestination = /* @__PURE__ */ new Map();
|
|
@@ -5851,7 +5030,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5851
5030
|
let h = null;
|
|
5852
5031
|
let seconds = null;
|
|
5853
5032
|
if (/\.(png|jpe?g|webp)$/i.test(file)) {
|
|
5854
|
-
const head = Buffer.from(await
|
|
5033
|
+
const head = Buffer.from(await readFile10(file));
|
|
5855
5034
|
const kind = sniffImage(head);
|
|
5856
5035
|
if (file.toLowerCase().endsWith(".png") && kind !== "png")
|
|
5857
5036
|
problems.push(
|
|
@@ -5899,228 +5078,69 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5899
5078
|
pictureAssets.push({
|
|
5900
5079
|
destination: id,
|
|
5901
5080
|
path: a.path,
|
|
5902
|
-
file,
|
|
5903
|
-
spec,
|
|
5904
|
-
text: a.text,
|
|
5905
|
-
seconds,
|
|
5906
|
-
composed: a.composed,
|
|
5907
|
-
shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
|
|
5908
|
-
crop: a.crop
|
|
5909
|
-
});
|
|
5910
|
-
if (!spec) {
|
|
5911
|
-
if (a.channel !== "demo")
|
|
5912
|
-
warnings.push(
|
|
5913
|
-
`${label}: no channel spec for "${id}" \u2014 not verified against one`
|
|
5914
|
-
);
|
|
5915
|
-
continue;
|
|
5916
|
-
}
|
|
5917
|
-
if (w !== null && h !== null && (w !== spec.px.w || h !== spec.px.h))
|
|
5918
|
-
problems.push(
|
|
5919
|
-
`${label}: spec wants ${spec.px.w}x${spec.px.h}, the file is ${w}x${h}`
|
|
5920
|
-
);
|
|
5921
|
-
if (spec.maxBytes !== void 0 && bytes > spec.maxBytes)
|
|
5922
|
-
problems.push(
|
|
5923
|
-
`${label}: ${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(spec.maxBytes / 1024)} KB ceiling`
|
|
5924
|
-
);
|
|
5925
|
-
if (seconds !== null) {
|
|
5926
|
-
if (spec.minSeconds !== void 0 && seconds < spec.minSeconds - 0.25)
|
|
5927
|
-
problems.push(
|
|
5928
|
-
`${label}: spec wants at least ${spec.minSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
5929
|
-
);
|
|
5930
|
-
if (spec.maxSeconds !== void 0 && seconds > spec.maxSeconds + 0.25)
|
|
5931
|
-
problems.push(
|
|
5932
|
-
`${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
5933
|
-
);
|
|
5934
|
-
}
|
|
5935
|
-
if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
|
|
5936
|
-
warnings.push(
|
|
5937
|
-
`${label}: spec renders png, the file is ${file.split(".").pop()}`
|
|
5938
|
-
);
|
|
5939
|
-
}
|
|
5940
|
-
for (const [id, n] of perDestination) {
|
|
5941
|
-
const spec = specById.get(id);
|
|
5942
|
-
if (spec?.count && (n < spec.count.min || n > spec.count.max))
|
|
5943
|
-
problems.push(
|
|
5944
|
-
`${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
|
|
5945
|
-
);
|
|
5946
|
-
}
|
|
5947
|
-
if (!opts.picture) {
|
|
5948
|
-
return { valid: problems.length === 0, problems, warnings, measured };
|
|
5949
|
-
}
|
|
5950
|
-
const picture = await pictureChecks(pictureAssets);
|
|
5951
|
-
const pictureErrors = picture.findings.filter((f) => f.severity === "error");
|
|
5952
|
-
return {
|
|
5953
|
-
valid: problems.length === 0 && pictureErrors.length === 0,
|
|
5954
|
-
problems,
|
|
5955
|
-
warnings,
|
|
5956
|
-
measured,
|
|
5957
|
-
picture: picture.findings,
|
|
5958
|
-
pictureMeasured: picture.measured
|
|
5959
|
-
};
|
|
5960
|
-
}
|
|
5961
|
-
|
|
5962
|
-
// src/plugin/platform.ts
|
|
5963
|
-
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
5964
|
-
import { homedir } from "os";
|
|
5965
|
-
import { join as join9 } from "path";
|
|
5966
|
-
function platformOrigin(flags = {}) {
|
|
5967
|
-
const legacyEnv = process.env.VOS_API_BASE?.trim();
|
|
5968
|
-
const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
|
|
5969
|
-
if (!flags.origin && !flags.api && !process.env.VOS_ORIGIN && legacyEnv) {
|
|
5970
|
-
process.stderr.write(
|
|
5971
|
-
"note: VOS_API_BASE is deprecated \u2014 set VOS_ORIGIN (an origin, no /api suffix)\n"
|
|
5972
|
-
);
|
|
5973
|
-
}
|
|
5974
|
-
return raw.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
5975
|
-
}
|
|
5976
|
-
function parseVosId(input) {
|
|
5977
|
-
if (!/^https?:\/\//.test(input)) {
|
|
5978
|
-
if (/^[A-Za-z0-9_-]+$/.test(input)) return input;
|
|
5979
|
-
throw new UsageError(`"${input}" is not a vos id or URL`);
|
|
5980
|
-
}
|
|
5981
|
-
let url;
|
|
5982
|
-
try {
|
|
5983
|
-
url = new URL(input);
|
|
5984
|
-
} catch {
|
|
5985
|
-
throw new UsageError(`"${input}" is not a valid URL`);
|
|
5986
|
-
}
|
|
5987
|
-
const query = url.searchParams.get("vos");
|
|
5988
|
-
if (query) return query;
|
|
5989
|
-
const path = url.pathname.match(/\/vos\/([A-Za-z0-9_-]+)/);
|
|
5990
|
-
if (path) return path[1];
|
|
5991
|
-
throw new UsageError(
|
|
5992
|
-
`could not find a vos id in ${input} \u2014 expected /vos/{id} or ?vos={id}`
|
|
5993
|
-
);
|
|
5994
|
-
}
|
|
5995
|
-
function deriveSlug(title) {
|
|
5996
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
|
|
5997
|
-
}
|
|
5998
|
-
var CREDENTIALS_PATH = join9(homedir(), ".config", "vos", "credentials");
|
|
5999
|
-
function resolveCredential(explicit) {
|
|
6000
|
-
const flag = explicit?.trim();
|
|
6001
|
-
if (flag) return flag;
|
|
6002
|
-
const env = process.env.VOS_API_KEY?.trim();
|
|
6003
|
-
if (env) return env;
|
|
6004
|
-
try {
|
|
6005
|
-
const first = readFileSync2(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
|
|
6006
|
-
return first || null;
|
|
6007
|
-
} catch {
|
|
6008
|
-
return null;
|
|
6009
|
-
}
|
|
6010
|
-
}
|
|
6011
|
-
function requireCredential(explicit) {
|
|
6012
|
-
const key = resolveCredential(explicit);
|
|
6013
|
-
if (!key) {
|
|
6014
|
-
throw new Error(
|
|
6015
|
-
"no credential found \u2014 pass --key, set VOS_API_KEY, or run `vos login` (mint a content key at https://vos.so/app/api; a vos_rg_ remix grant works too)"
|
|
6016
|
-
);
|
|
6017
|
-
}
|
|
6018
|
-
return key;
|
|
6019
|
-
}
|
|
6020
|
-
function writeCredential(key) {
|
|
6021
|
-
mkdirSync(join9(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
|
|
6022
|
-
writeFileSync(CREDENTIALS_PATH, `${key.trim()}
|
|
6023
|
-
`, { mode: 384 });
|
|
6024
|
-
return CREDENTIALS_PATH;
|
|
6025
|
-
}
|
|
6026
|
-
function clientId() {
|
|
6027
|
-
const env = process.env.VOS_CLIENT?.trim();
|
|
6028
|
-
if (env && env.length <= 60 && !/[\r\n]/.test(env)) return env;
|
|
6029
|
-
return "vos-cli";
|
|
6030
|
-
}
|
|
6031
|
-
async function apiJson(origin, path, init = {}) {
|
|
6032
|
-
const headers = {
|
|
6033
|
-
accept: "application/json",
|
|
6034
|
-
...init.headers
|
|
6035
|
-
};
|
|
6036
|
-
if (init.key) headers.authorization = `Bearer ${init.key}`;
|
|
6037
|
-
if (init.body !== void 0) headers["content-type"] = "application/json";
|
|
6038
|
-
const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
|
|
6039
|
-
const res = await fetch(`${origin}${path}`, {
|
|
6040
|
-
method: init.method ?? "GET",
|
|
6041
|
-
headers,
|
|
6042
|
-
body: reqBody
|
|
6043
|
-
});
|
|
6044
|
-
let body = {};
|
|
6045
|
-
try {
|
|
6046
|
-
body = await res.json();
|
|
6047
|
-
} catch {
|
|
6048
|
-
}
|
|
6049
|
-
return { status: res.status, body };
|
|
6050
|
-
}
|
|
6051
|
-
function apiError(what, r) {
|
|
6052
|
-
const detail = typeof r.body.error === "string" ? r.body.error : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
|
|
6053
|
-
const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
|
|
6054
|
-
return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
|
|
6055
|
-
}
|
|
6056
|
-
var SYNC_STATE_NAME = "vos.json";
|
|
6057
|
-
function readJsonFile(file) {
|
|
6058
|
-
try {
|
|
6059
|
-
return JSON.parse(readFileSync2(file, "utf8"));
|
|
6060
|
-
} catch {
|
|
6061
|
-
return null;
|
|
6062
|
-
}
|
|
6063
|
-
}
|
|
6064
|
-
function readSyncState(dir) {
|
|
6065
|
-
const own = readJsonFile(join9(dir, SYNC_STATE_NAME));
|
|
6066
|
-
if (own && typeof own.vosId === "string") {
|
|
6067
|
-
return {
|
|
6068
|
-
vosId: own.vosId,
|
|
6069
|
-
versionId: typeof own.versionId === "string" ? own.versionId : null,
|
|
6070
|
-
...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
|
|
6071
|
-
...typeof own.title === "string" ? { title: own.title } : {},
|
|
6072
|
-
...typeof own.slug === "string" ? { slug: own.slug } : {},
|
|
6073
|
-
...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
|
|
6074
|
-
};
|
|
5081
|
+
file,
|
|
5082
|
+
spec,
|
|
5083
|
+
text: a.text,
|
|
5084
|
+
seconds,
|
|
5085
|
+
composed: a.composed,
|
|
5086
|
+
shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
|
|
5087
|
+
crop: a.crop
|
|
5088
|
+
});
|
|
5089
|
+
if (!spec) {
|
|
5090
|
+
if (a.channel !== "demo")
|
|
5091
|
+
warnings.push(
|
|
5092
|
+
`${label}: no channel spec for "${id}" \u2014 not verified against one`
|
|
5093
|
+
);
|
|
5094
|
+
continue;
|
|
5095
|
+
}
|
|
5096
|
+
if (w !== null && h !== null && (w !== spec.px.w || h !== spec.px.h))
|
|
5097
|
+
problems.push(
|
|
5098
|
+
`${label}: spec wants ${spec.px.w}x${spec.px.h}, the file is ${w}x${h}`
|
|
5099
|
+
);
|
|
5100
|
+
if (spec.maxBytes !== void 0 && bytes > spec.maxBytes)
|
|
5101
|
+
problems.push(
|
|
5102
|
+
`${label}: ${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(spec.maxBytes / 1024)} KB ceiling`
|
|
5103
|
+
);
|
|
5104
|
+
if (seconds !== null) {
|
|
5105
|
+
if (spec.minSeconds !== void 0 && seconds < spec.minSeconds - 0.25)
|
|
5106
|
+
problems.push(
|
|
5107
|
+
`${label}: spec wants at least ${spec.minSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
5108
|
+
);
|
|
5109
|
+
if (spec.maxSeconds !== void 0 && seconds > spec.maxSeconds + 0.25)
|
|
5110
|
+
problems.push(
|
|
5111
|
+
`${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
|
|
5115
|
+
warnings.push(
|
|
5116
|
+
`${label}: spec renders png, the file is ${file.split(".").pop()}`
|
|
5117
|
+
);
|
|
6075
5118
|
}
|
|
6076
|
-
const
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
};
|
|
5119
|
+
for (const [id, n] of perDestination) {
|
|
5120
|
+
const spec = specById.get(id);
|
|
5121
|
+
if (spec?.count && (n < spec.count.min || n > spec.count.max))
|
|
5122
|
+
problems.push(
|
|
5123
|
+
`${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
|
|
5124
|
+
);
|
|
6083
5125
|
}
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
return {
|
|
6087
|
-
vosId: meta.id,
|
|
6088
|
-
versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
|
|
6089
|
-
...typeof meta.title === "string" ? { title: meta.title } : {},
|
|
6090
|
-
...typeof meta.slug === "string" ? { slug: meta.slug } : {},
|
|
6091
|
-
...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
|
|
6092
|
-
};
|
|
5126
|
+
if (!opts.picture) {
|
|
5127
|
+
return { valid: problems.length === 0, problems, warnings, measured };
|
|
6093
5128
|
}
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
5129
|
+
const picture = await pictureChecks(pictureAssets);
|
|
5130
|
+
const pictureErrors = picture.findings.filter((f) => f.severity === "error");
|
|
5131
|
+
return {
|
|
5132
|
+
valid: problems.length === 0 && pictureErrors.length === 0,
|
|
5133
|
+
problems,
|
|
5134
|
+
warnings,
|
|
5135
|
+
measured,
|
|
5136
|
+
picture: picture.findings,
|
|
5137
|
+
pictureMeasured: picture.measured
|
|
6100
5138
|
};
|
|
6101
|
-
writeFileSync(
|
|
6102
|
-
join9(dir, SYNC_STATE_NAME),
|
|
6103
|
-
`${JSON.stringify(merged, null, 2)}
|
|
6104
|
-
`
|
|
6105
|
-
);
|
|
6106
|
-
return merged;
|
|
6107
|
-
}
|
|
6108
|
-
function formatChanges(changes) {
|
|
6109
|
-
const lines = [];
|
|
6110
|
-
for (const ch of changes) {
|
|
6111
|
-
const label = ch.label ? ` \xB7 ${ch.label}` : "";
|
|
6112
|
-
const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
|
|
6113
|
-
lines.push(
|
|
6114
|
-
`v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
|
|
6115
|
-
);
|
|
6116
|
-
if (ch.note) lines.push(` note: ${ch.note}`);
|
|
6117
|
-
}
|
|
6118
|
-
return lines;
|
|
6119
5139
|
}
|
|
6120
5140
|
|
|
6121
5141
|
// src/plugin/recorder.ts
|
|
6122
5142
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
6123
|
-
import { join as
|
|
5143
|
+
import { join as join11 } from "path";
|
|
6124
5144
|
|
|
6125
5145
|
// src/plugin/recordingCap.ts
|
|
6126
5146
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
@@ -6208,7 +5228,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6208
5228
|
cdp.on("Page.screencastFrame", (ev) => {
|
|
6209
5229
|
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
6210
5230
|
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
6211
|
-
writeFileSync2(
|
|
5231
|
+
writeFileSync2(join11(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
6212
5232
|
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
6213
5233
|
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
6214
5234
|
});
|
|
@@ -6431,7 +5451,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6431
5451
|
await sleep(200);
|
|
6432
5452
|
const pageTitle = await page.title().catch(() => "");
|
|
6433
5453
|
await context.close();
|
|
6434
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(
|
|
5454
|
+
const firstFrame = frames[0] ? jpegDims(readFileSync3(join11(paths.framesDir, frames[0].file))) : null;
|
|
6435
5455
|
const meta = {
|
|
6436
5456
|
dpr: 1,
|
|
6437
5457
|
zoom: 1,
|
|
@@ -6485,7 +5505,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6485
5505
|
}
|
|
6486
5506
|
|
|
6487
5507
|
// src/plugin/encode.ts
|
|
6488
|
-
var MEDIABUNNY_URL2 = "https://esm.sh/mediabunny@1.
|
|
5508
|
+
var MEDIABUNNY_URL2 = "https://esm.sh/mediabunny@1.55.7?target=es2022";
|
|
6489
5509
|
var ENCODE_HTML = `<!doctype html><html><head><meta charset="utf-8"></head><body>
|
|
6490
5510
|
<script type="module">
|
|
6491
5511
|
try {
|
|
@@ -6539,14 +5559,17 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6539
5559
|
}
|
|
6540
5560
|
|
|
6541
5561
|
// src/plugin/plan.ts
|
|
6542
|
-
import { existsSync as
|
|
6543
|
-
import { readFile as
|
|
6544
|
-
import { join as
|
|
5562
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5563
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5564
|
+
import { join as join12 } from "path";
|
|
6545
5565
|
import {
|
|
6546
5566
|
DEFAULT_FRAME_STYLE,
|
|
6547
5567
|
STYLE_FIELDS,
|
|
5568
|
+
copyLayout,
|
|
6548
5569
|
copyStyle,
|
|
6549
5570
|
isRejected,
|
|
5571
|
+
isStageClip,
|
|
5572
|
+
layoutOf,
|
|
6550
5573
|
planAutoSpeed,
|
|
6551
5574
|
planAutoZoom,
|
|
6552
5575
|
projectFromArtifact,
|
|
@@ -6734,15 +5757,36 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6734
5757
|
// src/plugin/plan.ts
|
|
6735
5758
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6736
5759
|
async function readDigestActivity(dir) {
|
|
6737
|
-
const file =
|
|
6738
|
-
if (!
|
|
5760
|
+
const file = join12(dir, "digest", "digest.json");
|
|
5761
|
+
if (!existsSync10(file)) return null;
|
|
6739
5762
|
try {
|
|
6740
|
-
const d = JSON.parse(await
|
|
5763
|
+
const d = JSON.parse(await readFile11(file, "utf8"));
|
|
6741
5764
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
6742
5765
|
} catch {
|
|
6743
5766
|
return null;
|
|
6744
5767
|
}
|
|
6745
5768
|
}
|
|
5769
|
+
function patchStageWords(doc, words) {
|
|
5770
|
+
const map = {
|
|
5771
|
+
"stage-title": words.headline,
|
|
5772
|
+
"stage-kicker": words.kicker,
|
|
5773
|
+
"stage-brand": words.brand
|
|
5774
|
+
};
|
|
5775
|
+
for (const clip of doc.overlays ?? []) {
|
|
5776
|
+
if (clip.kind !== "text" || !isStageClip(clip)) continue;
|
|
5777
|
+
const v = map[clip.id];
|
|
5778
|
+
if (typeof v === "string" && v.trim()) clip.text = v.trim();
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
function applyStyle(seed, doc, opts) {
|
|
5782
|
+
const parts = layoutOf(seed);
|
|
5783
|
+
const carries = parts.clips.length > 0 || parts.lean || parts.hold;
|
|
5784
|
+
if (!carries) return { doc: copyStyle(seed, doc), layout: void 0 };
|
|
5785
|
+
const keys = opts.mark ? { "stage-mark": opts.mark.key } : void 0;
|
|
5786
|
+
const { doc: next, notes } = copyLayout(seed, copyStyle(seed, doc), { keys });
|
|
5787
|
+
if (opts.words) patchStageWords(next, opts.words);
|
|
5788
|
+
return { doc: next, layout: { ...parts, notes } };
|
|
5789
|
+
}
|
|
6746
5790
|
async function planTake(dir, opts = {}) {
|
|
6747
5791
|
const take = await loadTake(dir);
|
|
6748
5792
|
const { meta, cursor } = take;
|
|
@@ -6750,6 +5794,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6750
5794
|
const ingest = opts.backdrop ? { frame: withBackdrop(DEFAULT_FRAME_STYLE, opts.backdrop) } : {};
|
|
6751
5795
|
let doc;
|
|
6752
5796
|
let fresh;
|
|
5797
|
+
let layout;
|
|
6753
5798
|
if (opts.reuse) {
|
|
6754
5799
|
const prev = opts.reuse.doc;
|
|
6755
5800
|
const artifact = {
|
|
@@ -6761,6 +5806,10 @@ async function planTake(dir, opts = {}) {
|
|
|
6761
5806
|
doc = copyStyle(prev, doc);
|
|
6762
5807
|
const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
|
|
6763
5808
|
doc.segments = rt.segments;
|
|
5809
|
+
const prevHold = prev.segments.at(-1)?.hold;
|
|
5810
|
+
const last = doc.segments.at(-1);
|
|
5811
|
+
if (typeof prevHold === "number" && prevHold > 0 && last)
|
|
5812
|
+
last.hold = prevHold;
|
|
6764
5813
|
if (rt.rejected.length) doc.rejected = rt.rejected;
|
|
6765
5814
|
const manualZoom = rt.zoom;
|
|
6766
5815
|
const autoZoom = planAutoZoom(doc.source.cursor, {
|
|
@@ -6782,6 +5831,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6782
5831
|
if (prev.objects?.length) doc.objects = prev.objects;
|
|
6783
5832
|
if (prev.audio.length) doc.audio = prev.audio;
|
|
6784
5833
|
if (prev.camMotion?.length) doc.camMotion = prev.camMotion;
|
|
5834
|
+
if (prev.endCard) doc.endCard = structuredClone(prev.endCard);
|
|
6785
5835
|
await writeJson(take.paths.doc, doc, true);
|
|
6786
5836
|
return {
|
|
6787
5837
|
doc,
|
|
@@ -6793,7 +5843,13 @@ async function planTake(dir, opts = {}) {
|
|
|
6793
5843
|
};
|
|
6794
5844
|
}
|
|
6795
5845
|
if (take.doc) {
|
|
6796
|
-
|
|
5846
|
+
if (opts.style) {
|
|
5847
|
+
const applied = applyStyle(opts.style.doc, take.doc, opts);
|
|
5848
|
+
doc = applied.doc;
|
|
5849
|
+
layout = applied.layout;
|
|
5850
|
+
} else {
|
|
5851
|
+
doc = take.doc;
|
|
5852
|
+
}
|
|
6797
5853
|
fresh = false;
|
|
6798
5854
|
const manual = doc.zoom.filter((z) => z.source === "manual");
|
|
6799
5855
|
const auto = planAutoZoom(doc.source.cursor, {
|
|
@@ -6817,7 +5873,11 @@ async function planTake(dir, opts = {}) {
|
|
|
6817
5873
|
meta
|
|
6818
5874
|
};
|
|
6819
5875
|
doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
|
|
6820
|
-
if (opts.style)
|
|
5876
|
+
if (opts.style) {
|
|
5877
|
+
const applied = applyStyle(opts.style.doc, doc, opts);
|
|
5878
|
+
doc = applied.doc;
|
|
5879
|
+
layout = applied.layout;
|
|
5880
|
+
}
|
|
6821
5881
|
doc.zoom = planAutoZoom(doc.source.cursor, {
|
|
6822
5882
|
width: doc.source.meta.width,
|
|
6823
5883
|
height: doc.source.meta.height,
|
|
@@ -6831,6 +5891,12 @@ async function planTake(dir, opts = {}) {
|
|
|
6831
5891
|
});
|
|
6832
5892
|
fresh = true;
|
|
6833
5893
|
}
|
|
5894
|
+
let motion;
|
|
5895
|
+
if (opts.motion && (fresh || opts.motion.again)) {
|
|
5896
|
+
const proposed = proposeMotion(doc, opts.motion);
|
|
5897
|
+
doc = proposed.doc;
|
|
5898
|
+
motion = { notes: proposed.notes, skipped: proposed.skipped };
|
|
5899
|
+
}
|
|
6834
5900
|
await writeJson(take.paths.doc, doc, true);
|
|
6835
5901
|
return {
|
|
6836
5902
|
doc,
|
|
@@ -6844,22 +5910,24 @@ async function planTake(dir, opts = {}) {
|
|
|
6844
5910
|
styleFields: STYLE_FIELDS.filter(
|
|
6845
5911
|
(k) => opts.style.doc[k] !== void 0
|
|
6846
5912
|
)
|
|
6847
|
-
} : {}
|
|
5913
|
+
} : {},
|
|
5914
|
+
...layout ? { layout } : {},
|
|
5915
|
+
...motion ? { motion } : {}
|
|
6848
5916
|
};
|
|
6849
5917
|
}
|
|
6850
5918
|
|
|
6851
5919
|
// src/plugin/sync.ts
|
|
6852
5920
|
import { createHash } from "crypto";
|
|
6853
|
-
import { existsSync as
|
|
6854
|
-
import { readFile as
|
|
5921
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5922
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6855
5923
|
import { createInterface } from "readline/promises";
|
|
6856
|
-
import { basename, join as
|
|
6857
|
-
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as
|
|
5924
|
+
import { basename, join as join15 } from "path";
|
|
5925
|
+
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
6858
5926
|
|
|
6859
5927
|
// src/plugin/media.ts
|
|
6860
|
-
import { createWriteStream, existsSync as
|
|
5928
|
+
import { createWriteStream, existsSync as existsSync11 } from "fs";
|
|
6861
5929
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
6862
|
-
import { join as
|
|
5930
|
+
import { join as join13 } from "path";
|
|
6863
5931
|
import { Readable } from "stream";
|
|
6864
5932
|
import { pipeline } from "stream/promises";
|
|
6865
5933
|
var MIC_NAME = "mic.webm";
|
|
@@ -6869,6 +5937,81 @@ var KEYS = [
|
|
|
6869
5937
|
{ key: "micKey", file: MIC_NAME },
|
|
6870
5938
|
{ key: "camKey", file: CAM_NAME }
|
|
6871
5939
|
];
|
|
5940
|
+
function isTakeRelativeKey(key) {
|
|
5941
|
+
return !!key && !/^(https?:|blob:|data:|\/\/)/.test(key) && !key.startsWith("/api/");
|
|
5942
|
+
}
|
|
5943
|
+
var takeRelativeFile = (key) => key.replace(/^\/+/, "");
|
|
5944
|
+
function docMediaRefs(doc, keep = isTakeRelativeKey) {
|
|
5945
|
+
const out = [];
|
|
5946
|
+
for (const clip of doc.overlays ?? []) {
|
|
5947
|
+
if (clip.kind === "text" || !keep(clip.key)) continue;
|
|
5948
|
+
out.push({
|
|
5949
|
+
where: `overlay ${clip.id}`,
|
|
5950
|
+
key: clip.key,
|
|
5951
|
+
set: (next) => {
|
|
5952
|
+
clip.key = next;
|
|
5953
|
+
}
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
5956
|
+
const mark = doc.endCard?.mark;
|
|
5957
|
+
if (mark && keep(mark.key))
|
|
5958
|
+
out.push({
|
|
5959
|
+
where: "end card mark",
|
|
5960
|
+
key: mark.key,
|
|
5961
|
+
set: (next) => {
|
|
5962
|
+
mark.key = next;
|
|
5963
|
+
}
|
|
5964
|
+
});
|
|
5965
|
+
const bg = doc.frame.backgroundMedia;
|
|
5966
|
+
if (bg && keep(bg.key))
|
|
5967
|
+
out.push({
|
|
5968
|
+
where: "background",
|
|
5969
|
+
key: bg.key,
|
|
5970
|
+
set: (next) => {
|
|
5971
|
+
bg.key = next;
|
|
5972
|
+
}
|
|
5973
|
+
});
|
|
5974
|
+
return out;
|
|
5975
|
+
}
|
|
5976
|
+
function mediaContentType(file) {
|
|
5977
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
|
|
5978
|
+
const types = {
|
|
5979
|
+
svg: "image/svg+xml",
|
|
5980
|
+
png: "image/png",
|
|
5981
|
+
jpg: "image/jpeg",
|
|
5982
|
+
jpeg: "image/jpeg",
|
|
5983
|
+
webp: "image/webp",
|
|
5984
|
+
gif: "image/gif",
|
|
5985
|
+
avif: "image/avif",
|
|
5986
|
+
webm: "video/webm",
|
|
5987
|
+
mp4: "video/mp4",
|
|
5988
|
+
mov: "video/quicktime",
|
|
5989
|
+
mp3: "audio/mpeg",
|
|
5990
|
+
wav: "audio/wav",
|
|
5991
|
+
ogg: "audio/ogg",
|
|
5992
|
+
m4a: "audio/mp4"
|
|
5993
|
+
};
|
|
5994
|
+
return types[ext] ?? "application/octet-stream";
|
|
5995
|
+
}
|
|
5996
|
+
function extensionFor(contentType) {
|
|
5997
|
+
const t = contentType.split(";")[0].trim().toLowerCase();
|
|
5998
|
+
const exts = {
|
|
5999
|
+
"image/svg+xml": ".svg",
|
|
6000
|
+
"image/png": ".png",
|
|
6001
|
+
"image/jpeg": ".jpg",
|
|
6002
|
+
"image/webp": ".webp",
|
|
6003
|
+
"image/gif": ".gif",
|
|
6004
|
+
"image/avif": ".avif",
|
|
6005
|
+
"video/webm": ".webm",
|
|
6006
|
+
"video/mp4": ".mp4",
|
|
6007
|
+
"video/quicktime": ".mov",
|
|
6008
|
+
"audio/mpeg": ".mp3",
|
|
6009
|
+
"audio/wav": ".wav",
|
|
6010
|
+
"audio/ogg": ".ogg",
|
|
6011
|
+
"audio/mp4": ".m4a"
|
|
6012
|
+
};
|
|
6013
|
+
return exts[t] ?? "";
|
|
6014
|
+
}
|
|
6872
6015
|
function assetIdOf(url) {
|
|
6873
6016
|
if (!url) return null;
|
|
6874
6017
|
const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
|
|
@@ -6880,8 +6023,8 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6880
6023
|
const url = doc.source[key];
|
|
6881
6024
|
const assetId = assetIdOf(url);
|
|
6882
6025
|
if (!assetId) continue;
|
|
6883
|
-
const target =
|
|
6884
|
-
if (
|
|
6026
|
+
const target = join13(dir, file);
|
|
6027
|
+
if (existsSync11(target)) {
|
|
6885
6028
|
result.kept.push(file);
|
|
6886
6029
|
} else {
|
|
6887
6030
|
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
@@ -6903,19 +6046,56 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6903
6046
|
}
|
|
6904
6047
|
doc.source[key] = file;
|
|
6905
6048
|
}
|
|
6906
|
-
const
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6049
|
+
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
6050
|
+
const assetId = assetIdOf(ref.key);
|
|
6051
|
+
const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
|
|
6052
|
+
const existing = existsSync11(join13(dir, "media")) ? (await import("fs/promises").then(
|
|
6053
|
+
(fs) => fs.readdir(join13(dir, "media"))
|
|
6054
|
+
)).find((f) => f.startsWith(`${assetId}.`)) : void 0;
|
|
6055
|
+
let file;
|
|
6056
|
+
if (existing) {
|
|
6057
|
+
file = `media/${existing}`;
|
|
6058
|
+
result.kept.push(file);
|
|
6059
|
+
} else {
|
|
6060
|
+
const res = await fetch(abs, {
|
|
6061
|
+
headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
|
|
6062
|
+
});
|
|
6063
|
+
if (!res.ok || !res.body) {
|
|
6064
|
+
throw new Error(
|
|
6065
|
+
`download of ${ref.where} (${ref.key}) failed (${res.status})`
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
6068
|
+
const ext = extensionFor(res.headers.get("content-type") ?? "");
|
|
6069
|
+
file = `media/${assetId}${ext}`;
|
|
6070
|
+
const target = join13(dir, file);
|
|
6071
|
+
await import("fs/promises").then(
|
|
6072
|
+
(fs) => fs.mkdir(join13(dir, "media"), { recursive: true })
|
|
6073
|
+
);
|
|
6074
|
+
await pipeline(
|
|
6075
|
+
Readable.fromWeb(res.body),
|
|
6076
|
+
createWriteStream(target)
|
|
6077
|
+
);
|
|
6078
|
+
const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
|
|
6079
|
+
result.downloaded.push({ file, assetId, bytes });
|
|
6080
|
+
log(
|
|
6081
|
+
` ${file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(bytes / 1024)} kB)`
|
|
6082
|
+
);
|
|
6083
|
+
}
|
|
6084
|
+
ref.set(file);
|
|
6085
|
+
}
|
|
6086
|
+
const metaPath = join13(dir, "meta.json");
|
|
6087
|
+
if (!existsSync11(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
6088
|
+
const cursorPath = join13(dir, "cursor.json");
|
|
6089
|
+
if (!existsSync11(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
6090
|
+
await writeFile9(join13(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
6911
6091
|
return result;
|
|
6912
6092
|
}
|
|
6913
6093
|
|
|
6914
6094
|
// src/plugin/folder.ts
|
|
6915
6095
|
import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
|
|
6916
|
-
import { join as
|
|
6096
|
+
import { join as join14 } from "path";
|
|
6917
6097
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6918
|
-
import { migrateHostedDoc } from "@vosjs/studio-core";
|
|
6098
|
+
import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
6919
6099
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
|
|
6920
6100
|
async function listFolders(origin, key) {
|
|
6921
6101
|
const r = await apiJson(origin, "/api/folders", { key });
|
|
@@ -7089,18 +6269,18 @@ async function cmdPull(argv) {
|
|
|
7089
6269
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7090
6270
|
const payload = res.body;
|
|
7091
6271
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7092
|
-
await mkdir7(
|
|
6272
|
+
await mkdir7(join14(out, "recipes"), { recursive: true });
|
|
7093
6273
|
const lines = [];
|
|
7094
6274
|
const recipes = [];
|
|
7095
6275
|
for (const rec of payload.recipes) {
|
|
7096
|
-
const path =
|
|
6276
|
+
const path = join14(out, "recipes", safeName(rec.filename));
|
|
7097
6277
|
await writeFile10(path, rec.body ?? "");
|
|
7098
6278
|
recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
|
|
7099
6279
|
}
|
|
7100
6280
|
for (const rec of payload.inheritedRecipes) {
|
|
7101
6281
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7102
|
-
await mkdir7(
|
|
7103
|
-
const path =
|
|
6282
|
+
await mkdir7(join14(out, "recipes", "_inherited", from), { recursive: true });
|
|
6283
|
+
const path = join14(
|
|
7104
6284
|
out,
|
|
7105
6285
|
"recipes",
|
|
7106
6286
|
"_inherited",
|
|
@@ -7115,7 +6295,7 @@ async function cmdPull(argv) {
|
|
|
7115
6295
|
}
|
|
7116
6296
|
const members = [];
|
|
7117
6297
|
for (const v of payload.voses) {
|
|
7118
|
-
const dir =
|
|
6298
|
+
const dir = join14(out, "members", safeName(v.slug || v.title || v.id));
|
|
7119
6299
|
await mkdir7(dir, { recursive: true });
|
|
7120
6300
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7121
6301
|
if (cfg.status !== 200) {
|
|
@@ -7123,7 +6303,7 @@ async function cmdPull(argv) {
|
|
|
7123
6303
|
continue;
|
|
7124
6304
|
}
|
|
7125
6305
|
await writeFile10(
|
|
7126
|
-
|
|
6306
|
+
join14(dir, "config.json"),
|
|
7127
6307
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7128
6308
|
);
|
|
7129
6309
|
let take = false;
|
|
@@ -7135,12 +6315,12 @@ async function cmdPull(argv) {
|
|
|
7135
6315
|
);
|
|
7136
6316
|
if (doc.status === 200) {
|
|
7137
6317
|
await writeFile10(
|
|
7138
|
-
|
|
6318
|
+
join14(dir, "doc.json"),
|
|
7139
6319
|
JSON.stringify(doc.body, null, 2)
|
|
7140
6320
|
);
|
|
7141
6321
|
take = true;
|
|
7142
6322
|
if (flags.media === true) {
|
|
7143
|
-
const hosted =
|
|
6323
|
+
const hosted = migrateHostedDoc2(doc.body);
|
|
7144
6324
|
await pullMedia({ origin, key }, dir, hosted, r.log);
|
|
7145
6325
|
}
|
|
7146
6326
|
}
|
|
@@ -7252,7 +6432,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7252
6432
|
throw new Error(`doc.json fails lint:
|
|
7253
6433
|
${lint.problems.join("\n ")}`);
|
|
7254
6434
|
}
|
|
7255
|
-
if (!
|
|
6435
|
+
if (!existsSync12(take.paths.recording)) {
|
|
7256
6436
|
throw new Error("no recording.webm in this take");
|
|
7257
6437
|
}
|
|
7258
6438
|
const ctx = apiContext(flags);
|
|
@@ -7284,7 +6464,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7284
6464
|
throw new Error("push cancelled");
|
|
7285
6465
|
}
|
|
7286
6466
|
}
|
|
7287
|
-
const bytes = await
|
|
6467
|
+
const bytes = await readFile12(take.paths.recording);
|
|
7288
6468
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7289
6469
|
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7290
6470
|
const upload = await api(ctx, "/assets/recording", {
|
|
@@ -7322,6 +6502,35 @@ async function pushTake(dir, flags, r) {
|
|
|
7322
6502
|
r.log(" mic track is local-only \u2014 dropped from the push");
|
|
7323
6503
|
delete docForPush.source.micKey;
|
|
7324
6504
|
}
|
|
6505
|
+
for (const ref of docMediaRefs(docForPush)) {
|
|
6506
|
+
const file = join15(dir, takeRelativeFile(ref.key));
|
|
6507
|
+
if (!existsSync12(file)) {
|
|
6508
|
+
r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
|
|
6509
|
+
continue;
|
|
6510
|
+
}
|
|
6511
|
+
const media = await readFile12(file);
|
|
6512
|
+
const mediaHash = createHash("sha256").update(media).digest("hex");
|
|
6513
|
+
const type = mediaContentType(file);
|
|
6514
|
+
const put = await api(ctx, "/assets/recording", {
|
|
6515
|
+
method: "POST",
|
|
6516
|
+
headers: {
|
|
6517
|
+
"Content-Type": type,
|
|
6518
|
+
"Content-Length": String(media.length),
|
|
6519
|
+
"X-Filename": basename(file),
|
|
6520
|
+
"X-Content-Hash": mediaHash
|
|
6521
|
+
},
|
|
6522
|
+
raw: new Uint8Array(media)
|
|
6523
|
+
});
|
|
6524
|
+
if (put.status !== 201 && put.status !== 200) {
|
|
6525
|
+
throw new Error(
|
|
6526
|
+
`${ref.where} upload failed (${put.status}): ${String(put.json.error ?? "")}`
|
|
6527
|
+
);
|
|
6528
|
+
}
|
|
6529
|
+
ref.set(String(put.json.url));
|
|
6530
|
+
r.log(
|
|
6531
|
+
` ${ref.where}: ${ref.key} \u2192 asset ${String(put.json.id)}${put.json.reused === true ? " (reused)" : ""}`
|
|
6532
|
+
);
|
|
6533
|
+
}
|
|
7325
6534
|
const lowered = lowerToComposition3(docForPush);
|
|
7326
6535
|
const config = { ...lowered.config, data: lowered.data };
|
|
7327
6536
|
if (!state) {
|
|
@@ -7435,9 +6644,9 @@ async function pullTake(dir, flags, r) {
|
|
|
7435
6644
|
};
|
|
7436
6645
|
}
|
|
7437
6646
|
let media2;
|
|
7438
|
-
if (flags.media &&
|
|
6647
|
+
if (flags.media && existsSync12(join15(dir, "doc.json"))) {
|
|
7439
6648
|
const local = JSON.parse(
|
|
7440
|
-
await
|
|
6649
|
+
await readFile12(join15(dir, "doc.json"), "utf8")
|
|
7441
6650
|
);
|
|
7442
6651
|
media2 = await pullMedia(ctx, dir, local, r.log);
|
|
7443
6652
|
}
|
|
@@ -7495,12 +6704,12 @@ async function pullTake(dir, flags, r) {
|
|
|
7495
6704
|
docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
|
|
7496
6705
|
);
|
|
7497
6706
|
}
|
|
7498
|
-
const hostedDoc =
|
|
6707
|
+
const hostedDoc = migrateHostedDoc3(docRes.json);
|
|
7499
6708
|
const doc = hostedDoc;
|
|
7500
|
-
if (
|
|
6709
|
+
if (existsSync12(join15(dir, RECORDING_NAME))) {
|
|
7501
6710
|
doc.source.videoKey = RECORDING_NAME;
|
|
7502
6711
|
}
|
|
7503
|
-
await writeJson(
|
|
6712
|
+
await writeJson(join15(dir, "doc.json"), doc, true);
|
|
7504
6713
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
7505
6714
|
const versions = await api(ctx, `/vos/${vosId}/versions`);
|
|
7506
6715
|
const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
|
|
@@ -7519,16 +6728,16 @@ async function pullTake(dir, flags, r) {
|
|
|
7519
6728
|
}
|
|
7520
6729
|
|
|
7521
6730
|
// src/plugin/program.ts
|
|
7522
|
-
import { existsSync as
|
|
7523
|
-
import { mkdir as mkdir8, readFile as
|
|
6731
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6732
|
+
import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
|
|
7524
6733
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7525
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
6734
|
+
import { basename as basename2, dirname as dirname3, join as join16 } from "path";
|
|
7526
6735
|
import {
|
|
7527
6736
|
CURRENT_CONFIG_VERSION,
|
|
7528
6737
|
migrateConfig,
|
|
7529
6738
|
vosConfigJsonSchema
|
|
7530
6739
|
} from "@vosjs/core";
|
|
7531
|
-
import { migrateHostedDoc as
|
|
6740
|
+
import { migrateHostedDoc as migrateHostedDoc4 } from "@vosjs/studio-core";
|
|
7532
6741
|
|
|
7533
6742
|
// src/plugin/login.ts
|
|
7534
6743
|
import { hostname } from "os";
|
|
@@ -7692,8 +6901,8 @@ function preflightConfig(parsed) {
|
|
|
7692
6901
|
}
|
|
7693
6902
|
function resolveConfigPath(target) {
|
|
7694
6903
|
if (target.endsWith(".json")) return target;
|
|
7695
|
-
const inDir =
|
|
7696
|
-
if (
|
|
6904
|
+
const inDir = join16(target, "config.json");
|
|
6905
|
+
if (existsSync13(inDir)) return inDir;
|
|
7697
6906
|
throw new UsageError(
|
|
7698
6907
|
`${target} has no config.json (and is not a take \u2014 no doc.json)`
|
|
7699
6908
|
);
|
|
@@ -7721,7 +6930,7 @@ async function cmdFetch(argv) {
|
|
|
7721
6930
|
const out = strFlag(flags, "out") ?? slug;
|
|
7722
6931
|
await mkdir8(out, { recursive: true });
|
|
7723
6932
|
await writeFile11(
|
|
7724
|
-
|
|
6933
|
+
join16(out, "config.json"),
|
|
7725
6934
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7726
6935
|
);
|
|
7727
6936
|
const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
|
|
@@ -7743,15 +6952,15 @@ async function cmdFetch(argv) {
|
|
|
7743
6952
|
}
|
|
7744
6953
|
);
|
|
7745
6954
|
if (doc.status === 200 && !("source" in doc.body)) {
|
|
7746
|
-
const hosted =
|
|
6955
|
+
const hosted = migrateHostedDoc4(doc.body);
|
|
7747
6956
|
const { config: own } = await writeProgramDoc(out, hosted);
|
|
7748
6957
|
if (own) {
|
|
7749
|
-
await writeFile11(
|
|
6958
|
+
await writeFile11(join16(out, "config.json"), JSON.stringify(own, null, 2));
|
|
7750
6959
|
}
|
|
7751
6960
|
} else if (doc.status === 200) {
|
|
7752
6961
|
take = true;
|
|
7753
|
-
const hosted =
|
|
7754
|
-
await writeFile11(
|
|
6962
|
+
const hosted = migrateHostedDoc4(doc.body);
|
|
6963
|
+
await writeFile11(join16(out, "doc.json"), JSON.stringify(hosted, null, 2));
|
|
7755
6964
|
if (flags.media === true) {
|
|
7756
6965
|
const media = await pullMedia({ origin, key }, out, hosted, r.log);
|
|
7757
6966
|
mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
|
|
@@ -7863,7 +7072,7 @@ async function cmdPushProgram(argv) {
|
|
|
7863
7072
|
api: strFlag(flags, "api")
|
|
7864
7073
|
});
|
|
7865
7074
|
const dir = dirname3(source);
|
|
7866
|
-
const parsed = JSON.parse(await
|
|
7075
|
+
const parsed = JSON.parse(await readFile13(source, "utf8"));
|
|
7867
7076
|
const pre = preflightConfig(parsed);
|
|
7868
7077
|
if (!pre.ok || !pre.config) {
|
|
7869
7078
|
for (const issue of pre.issues) r.log(`error ${issue}`);
|
|
@@ -8096,10 +7305,10 @@ async function cmdPullProgram(argv) {
|
|
|
8096
7305
|
const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
|
|
8097
7306
|
if (cfg.status !== 200)
|
|
8098
7307
|
throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
|
|
8099
|
-
const configPath =
|
|
7308
|
+
const configPath = join16(dir, "config.json");
|
|
8100
7309
|
let backedUp = false;
|
|
8101
|
-
if (
|
|
8102
|
-
await writeFile11(
|
|
7310
|
+
if (existsSync13(configPath)) {
|
|
7311
|
+
await writeFile11(join16(dir, "config.backup.json"), await readFile13(configPath));
|
|
8103
7312
|
backedUp = true;
|
|
8104
7313
|
}
|
|
8105
7314
|
let headConfig = cfg.body.config;
|
|
@@ -8113,7 +7322,7 @@ async function cmdPullProgram(argv) {
|
|
|
8113
7322
|
if (docRes.status === 200 && !("source" in docRes.body)) {
|
|
8114
7323
|
const { config: own } = await writeProgramDoc(
|
|
8115
7324
|
dir,
|
|
8116
|
-
|
|
7325
|
+
migrateHostedDoc4(docRes.body)
|
|
8117
7326
|
);
|
|
8118
7327
|
if (own) headConfig = own;
|
|
8119
7328
|
}
|
|
@@ -8132,7 +7341,7 @@ async function cmdPullProgram(argv) {
|
|
|
8132
7341
|
protected: protectedIds,
|
|
8133
7342
|
changes,
|
|
8134
7343
|
out: configPath,
|
|
8135
|
-
backup: backedUp ?
|
|
7344
|
+
backup: backedUp ? join16(dir, "config.backup.json") : null
|
|
8136
7345
|
},
|
|
8137
7346
|
`Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
|
|
8138
7347
|
Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
|
|
@@ -8189,9 +7398,9 @@ function isTakeDir(target) {
|
|
|
8189
7398
|
return directoryKind(target) === "take";
|
|
8190
7399
|
}
|
|
8191
7400
|
async function readProgramDoc(dir, config) {
|
|
8192
|
-
const docPath =
|
|
8193
|
-
if (!
|
|
8194
|
-
const parsed = JSON.parse(await
|
|
7401
|
+
const docPath = join16(dir, "doc.json");
|
|
7402
|
+
if (!existsSync13(docPath)) return null;
|
|
7403
|
+
const parsed = JSON.parse(await readFile13(docPath, "utf8"));
|
|
8195
7404
|
if (typeof parsed !== "object" || parsed === null || "source" in parsed)
|
|
8196
7405
|
return null;
|
|
8197
7406
|
const raw = parsed;
|
|
@@ -8202,7 +7411,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8202
7411
|
const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
|
|
8203
7412
|
const { config, ...rest } = program;
|
|
8204
7413
|
const onDisk = { ...hosted, program: rest };
|
|
8205
|
-
await writeFile11(
|
|
7414
|
+
await writeFile11(join16(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
|
|
8206
7415
|
return {
|
|
8207
7416
|
config: config && typeof config === "object" ? config : null
|
|
8208
7417
|
};
|
|
@@ -8410,7 +7619,7 @@ async function cmdRecipe(argv) {
|
|
|
8410
7619
|
|
|
8411
7620
|
// src/plugin/brand.ts
|
|
8412
7621
|
import { writeFile as writeFile12 } from "fs/promises";
|
|
8413
|
-
import { resolve as
|
|
7622
|
+
import { resolve as resolve7 } from "path";
|
|
8414
7623
|
import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
|
|
8415
7624
|
import { lookKindForGround } from "@vosjs/studio-core";
|
|
8416
7625
|
var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
|
|
@@ -8459,7 +7668,7 @@ function isSaturated(hex2) {
|
|
|
8459
7668
|
return s >= 0.25 && l > 0.12 && l < 0.88;
|
|
8460
7669
|
}
|
|
8461
7670
|
var isLight = (hex2) => hsl(hex2).l >= 0.5;
|
|
8462
|
-
function
|
|
7671
|
+
function mixHex(a, b, t) {
|
|
8463
7672
|
const ch = (i) => Math.round(
|
|
8464
7673
|
parseInt(a.slice(i, i + 2), 16) * (1 - t) + parseInt(b.slice(i, i + 2), 16) * t
|
|
8465
7674
|
).toString(16).padStart(2, "0");
|
|
@@ -8477,7 +7686,7 @@ function mostCommon(values) {
|
|
|
8477
7686
|
}
|
|
8478
7687
|
return best;
|
|
8479
7688
|
}
|
|
8480
|
-
function
|
|
7689
|
+
function firstFamily(fontFamily) {
|
|
8481
7690
|
const first = fontFamily.split(",")[0]?.trim() ?? "";
|
|
8482
7691
|
return first.replace(/^["']|["']$/g, "");
|
|
8483
7692
|
}
|
|
@@ -8596,7 +7805,7 @@ function composeBrand(input) {
|
|
|
8596
7805
|
p.bgA = `the body's background`;
|
|
8597
7806
|
const surfaceHexes = w.surfaces.map(rgbToHex).filter((h) => h !== null && h !== bgA);
|
|
8598
7807
|
const neutralSurface = mostCommon(surfaceHexes.filter((h) => !isSaturated(h)));
|
|
8599
|
-
const bgB = neutralSurface ??
|
|
7808
|
+
const bgB = neutralSurface ?? mixHex(bgA, isLight(bgA) ? "#000000" : "#ffffff", 0.04);
|
|
8600
7809
|
p.bgB = neutralSurface ? `the most common section / card ground` : `no distinct surface found; bgA stepped 4% toward ${isLight(bgA) ? "black" : "white"}`;
|
|
8601
7810
|
const accentHexes = w.accents.map(rgbToHex).filter((h) => h !== null && isSaturated(h));
|
|
8602
7811
|
const themeHex = w.themeColor ? rgbToHex(w.themeColor) : null;
|
|
@@ -8606,13 +7815,13 @@ function composeBrand(input) {
|
|
|
8606
7815
|
const inkHex = rgbToHex(w.h1?.color ?? w.body.color) ?? (isLight(bgA) ? "#111111" : "#f5f5f5");
|
|
8607
7816
|
if (!accent) accent = inkHex;
|
|
8608
7817
|
p.accent = accentHexes.length ? `the saturated colour buttons and links agree on` : themeHex && isSaturated(themeHex) ? `the theme-color meta` : design?.hexes.find(isSaturated) ? `a hex quoted in design.md` : `no saturated colour on the page; the ink stands in`;
|
|
8609
|
-
const bgC =
|
|
7818
|
+
const bgC = mixHex(bgA, accent, 0.14);
|
|
8610
7819
|
p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
|
|
8611
7820
|
p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
|
|
8612
|
-
const fontDisplay = design?.fonts[0] ?? (w.h1 ?
|
|
7821
|
+
const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily));
|
|
8613
7822
|
p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
|
|
8614
7823
|
const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
|
|
8615
|
-
const fontBody = designBody ??
|
|
7824
|
+
const fontBody = designBody ?? firstFamily(w.body.fontFamily);
|
|
8616
7825
|
p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
|
|
8617
7826
|
const isIcon = (u) => /favicon|apple-touch|icon/i.test(u);
|
|
8618
7827
|
const onDark = (u) => /on-dark|white/i.test(u);
|
|
@@ -8727,7 +7936,7 @@ async function cmdBrand(argv) {
|
|
|
8727
7936
|
throw new UsageError(`not a URL: ${raw}`);
|
|
8728
7937
|
}
|
|
8729
7938
|
const r = createReporter(flags.json === true);
|
|
8730
|
-
const out =
|
|
7939
|
+
const out = resolve7(strFlag(flags, "out") ?? "BRAND.md");
|
|
8731
7940
|
r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
|
|
8732
7941
|
const [designText, llmsText] = await Promise.all([
|
|
8733
7942
|
fetchText(`${origin}/design.md`),
|
|
@@ -8780,7 +7989,7 @@ async function cmdBrand(argv) {
|
|
|
8780
7989
|
|
|
8781
7990
|
// src/plugin/agentBrowser.ts
|
|
8782
7991
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
8783
|
-
import { resolve as
|
|
7992
|
+
import { resolve as resolve8 } from "path";
|
|
8784
7993
|
function parseAgentBrowserLog(text) {
|
|
8785
7994
|
const records = [];
|
|
8786
7995
|
const problems = [];
|
|
@@ -9250,7 +8459,7 @@ async function cmdActions(argv) {
|
|
|
9250
8459
|
);
|
|
9251
8460
|
}
|
|
9252
8461
|
const r = createReporter(flags.json === true);
|
|
9253
|
-
const out =
|
|
8462
|
+
const out = resolve8(strFlag(flags, "out") ?? "actions.json");
|
|
9254
8463
|
const viewportFlag = strFlag(flags, "viewport");
|
|
9255
8464
|
let viewport;
|
|
9256
8465
|
if (viewportFlag) {
|
|
@@ -9258,8 +8467,8 @@ async function cmdActions(argv) {
|
|
|
9258
8467
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
9259
8468
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
9260
8469
|
}
|
|
9261
|
-
const { readFile:
|
|
9262
|
-
const text = await
|
|
8470
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
8471
|
+
const text = await readFile15(resolve8(input), "utf8");
|
|
9263
8472
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
9264
8473
|
for (const p of problems) r.log(` ${p}`);
|
|
9265
8474
|
const result = convertAgentBrowser(records, {
|
|
@@ -9311,7 +8520,8 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
9311
8520
|
"print",
|
|
9312
8521
|
"yes",
|
|
9313
8522
|
"composed",
|
|
9314
|
-
"check"
|
|
8523
|
+
"check",
|
|
8524
|
+
"motion"
|
|
9315
8525
|
]);
|
|
9316
8526
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
|
|
9317
8527
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -9319,10 +8529,10 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
|
|
|
9319
8529
|
Take pipeline
|
|
9320
8530
|
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
|
|
9321
8531
|
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
|
|
9322
|
-
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--background <slug|url|none>] [--json]
|
|
8532
|
+
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|none] [--end-card none] [--captions none] [--clicks none] [--release v2.1] [--json]
|
|
9323
8533
|
vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
9324
8534
|
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
9325
|
-
vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--
|
|
8535
|
+
vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--launch LAUNCH.md] [--look plate|gradient|dark|none] [--brand BRAND.md] [--composed] [--set path=value] [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--parallel N] [--json]
|
|
9326
8536
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
9327
8537
|
vos brand <url> [--out BRAND.md] [--json]
|
|
9328
8538
|
vos open <take> [--studio <url>] [--print]
|
|
@@ -9422,44 +8632,47 @@ read once as the real page, blank ones (a wallpaper, an empty canvas) are
|
|
|
9422
8632
|
dropped and two of one frame collapse to one, with every drop said in
|
|
9423
8633
|
skipped[]. --times overrides with seconds, percents or step:<id>[+offset]
|
|
9424
8634
|
(the id from actions.json); --range cuts every video destination.
|
|
9425
|
-
The LOOK presents the card: card-genre stills with no poster
|
|
9426
|
-
video cut sit on a ground (a cream plate, the house gradient, a
|
|
9427
|
-
with a light streak) at ~84% of the width with headroom, a
|
|
9428
|
-
shadow plus a tight contact shadow, and a hairline when card
|
|
9429
|
-
are both light; a wide frame runs the card off the bottom.
|
|
9430
|
-
house look (or none for the pre-look crops); with no flag
|
|
9431
|
-
beside the take (or --brand <file>) decides from its look
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
8635
|
+
The LOOK presents the card: card-genre stills with no poster document
|
|
8636
|
+
and every video cut sit on a ground (a cream plate, the house gradient, a
|
|
8637
|
+
dark plate with a light streak) at ~84% of the width with headroom, a
|
|
8638
|
+
soft ambient shadow plus a tight contact shadow, and a hairline when card
|
|
8639
|
+
and ground are both light; a wide frame runs the card off the bottom.
|
|
8640
|
+
--look picks a house look (or none for the pre-look crops); with no flag
|
|
8641
|
+
the BRAND.md beside the take (or --brand <file>) decides from its look
|
|
8642
|
+
role or its own ground, and with no brand the house gradient.
|
|
8643
|
+
Screenshot-genre stills never take a look.
|
|
8644
|
+
A POSTER is a document, never a template: a plain take whose card sits
|
|
8645
|
+
where the poster wants it (frame.inset), leans (the tilt track), carries
|
|
8646
|
+
the words and the mark as clips (ids stage-title, stage-kicker,
|
|
8647
|
+
stage-brand, stage-mark) and ends on a trailing hold whose start is the
|
|
8648
|
+
still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
|
|
8649
|
+
CWS tile + marquee, GitHub social preview) render from the poster
|
|
8650
|
+
document of their aspect CLASS (landscape, square, portrait, tile), found
|
|
8651
|
+
beside the take as poster/<class>/doc.json (poster/doc.json serves every
|
|
8652
|
+
class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
|
|
8653
|
+
the document's rest; kit.json records source poster, the class, the file,
|
|
8654
|
+
the vos it tracks, the shot rect and the text boxes read FROM the
|
|
8655
|
+
document. A class with no document is the take's own frame, said once.
|
|
8656
|
+
deliver renders and verifies; it composes nothing. The composition is
|
|
8657
|
+
made with plan --style <poster>, which copies a poster's layout onto a
|
|
8658
|
+
take (its card placement, its stage clips with the release's words
|
|
8659
|
+
patched in from LAUNCH.md's headline and kicker roles or --headline and
|
|
8660
|
+
--kicker, its rest lean, its hold), or by hand in doc.json.
|
|
8661
|
+
The cut's MOTION is the document's too. plan proposes it on a fresh plan
|
|
8662
|
+
(--motion re-proposes onto an existing doc.json, replacing only its own
|
|
8663
|
+
proposals): the card's ENTRANCE (tilt-in by default: the card swings in
|
|
8664
|
+
from a perspective pose and settles), the END CARD (the last frame holds
|
|
8665
|
+
2.5 s while the card recedes and the headline, the release line and the
|
|
8666
|
+
wordmark rise; the brand's mark from BRAND.md logoUrl above them), a
|
|
8667
|
+
CAPTION per actions.json step at the step's moment, a music BED from
|
|
8668
|
+
LAUNCH.md's music role (a catalog slug or a mood) and a click sound on
|
|
8669
|
+
every press when the take has no mic. LAUNCH.md's entrance, endCard,
|
|
8670
|
+
captions, music and clicks roles, or the flags, change or switch each
|
|
8671
|
+
off; a deleted proposal stays deleted on a refresh. deliver applies each
|
|
8672
|
+
destination's MECHANICS and nothing more: the README loop plays no
|
|
8673
|
+
entrance, end card or sound; a channel that autoplays muted drops the
|
|
8674
|
+
bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
|
|
8675
|
+
the camera. Screenshot-genre
|
|
9463
8676
|
destinations (CWS screenshots, the PH gallery) are the real page at that
|
|
9464
8677
|
moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
|
|
9465
8678
|
policy); --composed keeps the cut's camera and chrome instead. --set
|
|
@@ -9547,7 +8760,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
9547
8760
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
9548
8761
|
`;
|
|
9549
8762
|
async function loadActions(file) {
|
|
9550
|
-
const raw = JSON.parse(await
|
|
8763
|
+
const raw = JSON.parse(await readFile14(file, "utf8"));
|
|
9551
8764
|
const errors = validateActions(raw);
|
|
9552
8765
|
if (errors.length)
|
|
9553
8766
|
throw new UsageError(`invalid actions file:
|
|
@@ -9603,10 +8816,10 @@ async function cmdRecord(argv) {
|
|
|
9603
8816
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
9604
8817
|
if (!url)
|
|
9605
8818
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
9606
|
-
const outDir =
|
|
8819
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
9607
8820
|
const backdrop = await takeBackdrop(flags, r);
|
|
9608
8821
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9609
|
-
if (
|
|
8822
|
+
if (existsSync14(join17(outDir, "meta.json"))) {
|
|
9610
8823
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9611
8824
|
r.log(
|
|
9612
8825
|
`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" : "")
|
|
@@ -9653,7 +8866,7 @@ async function cmdRecord(argv) {
|
|
|
9653
8866
|
},
|
|
9654
8867
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
|
|
9655
8868
|
${(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}
|
|
9656
|
-
Next: edit ${
|
|
8869
|
+
Next: edit ${join17(outDir, "doc.json")} (optional), then: vos render ${outDir}`
|
|
9657
8870
|
);
|
|
9658
8871
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
9659
8872
|
} finally {
|
|
@@ -9677,19 +8890,19 @@ async function cmdCreate2(argv) {
|
|
|
9677
8890
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
9678
8891
|
if (!url)
|
|
9679
8892
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
9680
|
-
const outDir =
|
|
8893
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
9681
8894
|
const fmtRaw = strFlag(flags, "format") ?? "webm";
|
|
9682
8895
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9683
8896
|
throw new UsageError("--format must be webm or mp4");
|
|
9684
8897
|
const format = fmtRaw;
|
|
9685
|
-
const out = positionals[0] ??
|
|
8898
|
+
const out = positionals[0] ?? join17(outDir, `out.${format}`);
|
|
9686
8899
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9687
8900
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9688
8901
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
9689
8902
|
}
|
|
9690
8903
|
const backdrop = await takeBackdrop(flags, r);
|
|
9691
8904
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9692
|
-
if (
|
|
8905
|
+
if (existsSync14(join17(outDir, "meta.json"))) {
|
|
9693
8906
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9694
8907
|
r.log(
|
|
9695
8908
|
`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" : "")
|
|
@@ -9772,40 +8985,66 @@ async function cmdPlan(argv) {
|
|
|
9772
8985
|
const dir = positionals[0];
|
|
9773
8986
|
if (!dir)
|
|
9774
8987
|
throw new UsageError(
|
|
9775
|
-
|
|
8988
|
+
'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
|
|
9776
8989
|
);
|
|
9777
8990
|
const r = createReporter(flags.json === true);
|
|
9778
8991
|
if (flags.fresh === true) {
|
|
9779
|
-
await rm4(
|
|
8992
|
+
await rm4(join17(dir, "doc.json"), { force: true });
|
|
9780
8993
|
r.log("note: --fresh discarded the existing doc.json");
|
|
9781
8994
|
}
|
|
9782
8995
|
let reuse;
|
|
9783
8996
|
if (flags.reuse === true) {
|
|
9784
|
-
const from =
|
|
9785
|
-
if (!
|
|
8997
|
+
const from = resolve9(strFlag(flags, "from") ?? join17(dir, PREV_DOC_NAME));
|
|
8998
|
+
if (!existsSync14(from)) {
|
|
9786
8999
|
throw new UsageError(
|
|
9787
9000
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
9788
9001
|
);
|
|
9789
9002
|
}
|
|
9790
9003
|
reuse = {
|
|
9791
9004
|
from,
|
|
9792
|
-
doc: JSON.parse(await
|
|
9005
|
+
doc: JSON.parse(await readFile14(from, "utf8"))
|
|
9793
9006
|
};
|
|
9794
9007
|
}
|
|
9795
9008
|
const style = await resolveStyleRef(flags);
|
|
9796
|
-
const
|
|
9009
|
+
const hasDoc = existsSync14(join17(dir, "doc.json"));
|
|
9010
|
+
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
9011
|
+
const release = await releaseInputs(dir, flags, r);
|
|
9012
|
+
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
9797
9013
|
const s = await planTake(dir, {
|
|
9798
9014
|
...style ? { style } : {},
|
|
9799
9015
|
...reuse ? { reuse } : {},
|
|
9800
|
-
backdrop
|
|
9016
|
+
backdrop,
|
|
9017
|
+
words: release.words,
|
|
9018
|
+
mark: release.mark,
|
|
9019
|
+
...motionWanted ? {
|
|
9020
|
+
motion: {
|
|
9021
|
+
words: release.words,
|
|
9022
|
+
launch: release.launchRoles,
|
|
9023
|
+
ink: release.ink,
|
|
9024
|
+
mark: release.mark,
|
|
9025
|
+
captions: release.captions,
|
|
9026
|
+
catalog: release.catalog,
|
|
9027
|
+
again: flags.motion === true
|
|
9028
|
+
}
|
|
9029
|
+
} : {}
|
|
9801
9030
|
});
|
|
9031
|
+
const layoutLines = s.layout ? `
|
|
9032
|
+
layout from ${s.styleFrom}: ${[
|
|
9033
|
+
s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
|
|
9034
|
+
s.layout.lean ? "the rest lean" : "",
|
|
9035
|
+
s.layout.hold ? "the hold" : ""
|
|
9036
|
+
].filter(Boolean).join(", ")}` + (s.layout.notes.length ? `
|
|
9037
|
+
${s.layout.notes.join("\n ")}` : "") : "";
|
|
9038
|
+
const motionLines = s.motion ? (s.motion.notes.length ? `
|
|
9039
|
+
proposed: ${s.motion.notes.join(", ")}` : "") + (s.motion.skipped.length ? `
|
|
9040
|
+
not proposed: ${s.motion.skipped.join("; ")}` : "") : "";
|
|
9802
9041
|
const reuseLines = s.reuse ? `
|
|
9803
9042
|
reused ${s.reuse.from}: ${s.reuse.anchored} anchored + ${s.reuse.mapped} mapped span(s)` + (s.reuse.flagged.length ? `
|
|
9804
9043
|
flagged:
|
|
9805
9044
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
9806
9045
|
r.done(
|
|
9807
9046
|
{
|
|
9808
|
-
take:
|
|
9047
|
+
take: resolve9(dir),
|
|
9809
9048
|
fresh: s.fresh,
|
|
9810
9049
|
cursorKept: s.cursorKept,
|
|
9811
9050
|
zoomAuto: s.zoomAuto,
|
|
@@ -9813,12 +9052,78 @@ async function cmdPlan(argv) {
|
|
|
9813
9052
|
duration: s.doc.source.meta.durationMs / 1e3,
|
|
9814
9053
|
...s.backdrop !== void 0 ? { backdrop: s.backdrop } : {},
|
|
9815
9054
|
...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
|
|
9055
|
+
...s.layout ? { layout: s.layout } : {},
|
|
9056
|
+
...s.motion ? { motion: s.motion } : {},
|
|
9816
9057
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
9817
9058
|
},
|
|
9818
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
9059
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join17(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
|
|
9819
9060
|
);
|
|
9820
9061
|
return EXIT_OK;
|
|
9821
9062
|
}
|
|
9063
|
+
async function releaseInputs(dir, flags, r) {
|
|
9064
|
+
let lookPick;
|
|
9065
|
+
try {
|
|
9066
|
+
lookPick = await resolveLook(dir, {
|
|
9067
|
+
look: strFlag(flags, "look"),
|
|
9068
|
+
brand: strFlag(flags, "brand")
|
|
9069
|
+
});
|
|
9070
|
+
} catch (e) {
|
|
9071
|
+
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
9072
|
+
}
|
|
9073
|
+
const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
|
|
9074
|
+
const lines = (v) => v === null || v === void 0 ? null : v.replace(/\\n/g, "\n");
|
|
9075
|
+
const words = {
|
|
9076
|
+
headline: lines(strFlag(flags, "headline") ?? launch?.roles.headline),
|
|
9077
|
+
kicker: lines(strFlag(flags, "kicker") ?? launch?.roles.kicker),
|
|
9078
|
+
brand: strFlag(flags, "brand-name") ?? lookPick.roles?.wordmark ?? null,
|
|
9079
|
+
release: strFlag(flags, "release") ?? null
|
|
9080
|
+
};
|
|
9081
|
+
if (launch) r.log(`words: ${launch.file}`);
|
|
9082
|
+
const launchRoles = { ...launch?.roles ?? {} };
|
|
9083
|
+
for (const [flag, role] of [
|
|
9084
|
+
["music", "music"],
|
|
9085
|
+
["entrance", "entrance"],
|
|
9086
|
+
["end-card", "endCard"],
|
|
9087
|
+
["captions", "captions"],
|
|
9088
|
+
["clicks", "clicks"]
|
|
9089
|
+
]) {
|
|
9090
|
+
const v = strFlag(flags, flag);
|
|
9091
|
+
if (v !== void 0) launchRoles[role] = v;
|
|
9092
|
+
}
|
|
9093
|
+
const marks = await fetchBrandMarks(dir, lookPick.roles);
|
|
9094
|
+
for (const n of marks.notes) r.log(`note: ${n}`);
|
|
9095
|
+
const mark = lookPick.look?.kind === "dark" ? marks.dark : marks.light;
|
|
9096
|
+
if (mark) r.log(`brand mark: ${mark.key} (${mark.aspect.toFixed(2)}:1)`);
|
|
9097
|
+
const off2 = (v) => v !== void 0 && /^(none|off|no|false)$/i.test(v);
|
|
9098
|
+
let catalog = null;
|
|
9099
|
+
if (launchRoles.music && !off2(launchRoles.music)) {
|
|
9100
|
+
try {
|
|
9101
|
+
catalog = await fetchMusicCatalog(
|
|
9102
|
+
platformOrigin({
|
|
9103
|
+
origin: strFlag(flags, "origin"),
|
|
9104
|
+
api: strFlag(flags, "api")
|
|
9105
|
+
})
|
|
9106
|
+
);
|
|
9107
|
+
} catch (e) {
|
|
9108
|
+
r.log(
|
|
9109
|
+
`music catalog: ${e instanceof Error ? e.message : String(e)} \u2014 no bed`
|
|
9110
|
+
);
|
|
9111
|
+
}
|
|
9112
|
+
}
|
|
9113
|
+
const take = await loadTake(dir);
|
|
9114
|
+
const captions = (take.actions?.steps ?? []).flatMap((st, i) => {
|
|
9115
|
+
const step = st;
|
|
9116
|
+
return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
|
|
9117
|
+
});
|
|
9118
|
+
return {
|
|
9119
|
+
words,
|
|
9120
|
+
launchRoles,
|
|
9121
|
+
ink: endCardInk(lookPick.look, lookPick.roles),
|
|
9122
|
+
mark,
|
|
9123
|
+
captions,
|
|
9124
|
+
catalog
|
|
9125
|
+
};
|
|
9126
|
+
}
|
|
9822
9127
|
async function cmdRender(argv) {
|
|
9823
9128
|
const { positionals, flags, multi } = parseArgs(
|
|
9824
9129
|
argv,
|
|
@@ -9835,7 +9140,7 @@ async function cmdRender(argv) {
|
|
|
9835
9140
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9836
9141
|
throw new UsageError("--format must be webm or mp4");
|
|
9837
9142
|
const format = fmtRaw;
|
|
9838
|
-
const out = positionals[1] ??
|
|
9143
|
+
const out = positionals[1] ?? join17(dir, `out.${format}`);
|
|
9839
9144
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9840
9145
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9841
9146
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -10013,34 +9318,6 @@ async function cmdDeliver(argv) {
|
|
|
10013
9318
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
10014
9319
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
10015
9320
|
}
|
|
10016
|
-
let poster;
|
|
10017
|
-
const posterRef = strFlag(flags, "poster");
|
|
10018
|
-
if (posterRef === "none") {
|
|
10019
|
-
poster = null;
|
|
10020
|
-
} else if (posterRef !== void 0 && templateByName(posterRef)) {
|
|
10021
|
-
poster = { from: `template ${posterRef}`, config: templateByName(posterRef) };
|
|
10022
|
-
} else if (posterRef !== void 0) {
|
|
10023
|
-
if (existsSync13(posterRef)) {
|
|
10024
|
-
poster = {
|
|
10025
|
-
from: resolve8(posterRef),
|
|
10026
|
-
config: JSON.parse(await readFile13(posterRef, "utf8"))
|
|
10027
|
-
};
|
|
10028
|
-
} else {
|
|
10029
|
-
const origin = platformOrigin({
|
|
10030
|
-
origin: strFlag(flags, "origin"),
|
|
10031
|
-
api: strFlag(flags, "api")
|
|
10032
|
-
});
|
|
10033
|
-
const key = await resolveCredential(strFlag(flags, "key"));
|
|
10034
|
-
const res = await apiJson(origin, `/api/vos/${posterRef}/config`, {
|
|
10035
|
-
key
|
|
10036
|
-
});
|
|
10037
|
-
if (!res.config)
|
|
10038
|
-
throw new UsageError(
|
|
10039
|
-
`--poster ${posterRef}: not a file on disk and the platform returned no config`
|
|
10040
|
-
);
|
|
10041
|
-
poster = { from: posterRef, config: res.config };
|
|
10042
|
-
}
|
|
10043
|
-
}
|
|
10044
9321
|
let lookPick;
|
|
10045
9322
|
try {
|
|
10046
9323
|
lookPick = await resolveLook(dir, {
|
|
@@ -10052,63 +9329,18 @@ async function cmdDeliver(argv) {
|
|
|
10052
9329
|
}
|
|
10053
9330
|
r.log(`look: ${lookPick.from}`);
|
|
10054
9331
|
const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
|
|
10055
|
-
|
|
10056
|
-
const words = {
|
|
10057
|
-
headline: lines(strFlag(flags, "headline") ?? launch?.roles.headline),
|
|
10058
|
-
kicker: lines(strFlag(flags, "kicker") ?? launch?.roles.kicker),
|
|
10059
|
-
brand: strFlag(flags, "brand-name") ?? lookPick.roles?.wordmark ?? null,
|
|
10060
|
-
release: strFlag(flags, "release") ?? null
|
|
10061
|
-
};
|
|
10062
|
-
if (launch) r.log(`words: ${launch.file}`);
|
|
10063
|
-
const launchRoles = { ...launch?.roles ?? {} };
|
|
10064
|
-
for (const [flag, role] of [
|
|
10065
|
-
["music", "music"],
|
|
10066
|
-
["entrance", "entrance"],
|
|
10067
|
-
["end-card", "endCard"],
|
|
10068
|
-
["captions", "captions"],
|
|
10069
|
-
["clicks", "clicks"]
|
|
10070
|
-
]) {
|
|
10071
|
-
const v = strFlag(flags, flag);
|
|
10072
|
-
if (v !== void 0) launchRoles[role] = v;
|
|
10073
|
-
}
|
|
10074
|
-
const soundWanted = channels.some(
|
|
10075
|
-
(c) => ["x", "youtube", "shorts-linkedin"].includes(c)
|
|
10076
|
-
);
|
|
10077
|
-
let catalog = null;
|
|
10078
|
-
if (soundWanted && !/^(none|off|no|false)$/i.test(launchRoles.music ?? "") && !/^(none|off|no|false)$/i.test(launchRoles.clicks ?? "")) {
|
|
10079
|
-
try {
|
|
10080
|
-
catalog = await fetchMusicCatalog(
|
|
10081
|
-
platformOrigin({
|
|
10082
|
-
origin: strFlag(flags, "origin"),
|
|
10083
|
-
api: strFlag(flags, "api")
|
|
10084
|
-
})
|
|
10085
|
-
);
|
|
10086
|
-
} catch (e) {
|
|
10087
|
-
r.log(`music catalog: ${e instanceof Error ? e.message : String(e)} \u2014 the cuts stay silent`);
|
|
10088
|
-
}
|
|
10089
|
-
}
|
|
10090
|
-
const captions = (take.actions?.steps ?? []).flatMap((s, i) => {
|
|
10091
|
-
const step = s;
|
|
10092
|
-
return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
|
|
10093
|
-
});
|
|
9332
|
+
if (launch) r.log(`launch: ${launch.file}`);
|
|
10094
9333
|
const browser = await launchBrowser();
|
|
10095
9334
|
try {
|
|
10096
9335
|
const result = await deliverTake(browser, dir, {
|
|
10097
9336
|
look: lookPick.look,
|
|
10098
|
-
|
|
10099
|
-
launchRoles,
|
|
10100
|
-
catalog,
|
|
10101
|
-
captions,
|
|
10102
|
-
words,
|
|
9337
|
+
launchRoles: launch?.roles ?? null,
|
|
10103
9338
|
channels,
|
|
10104
9339
|
outDir: strFlag(flags, "out"),
|
|
10105
9340
|
release: strFlag(flags, "release"),
|
|
10106
9341
|
times,
|
|
10107
9342
|
range,
|
|
10108
9343
|
parallel,
|
|
10109
|
-
poster,
|
|
10110
|
-
posterTime: hasFlag(flags, "poster-time") ? numFlag(flags, "poster-time", 0) : void 0,
|
|
10111
|
-
shotTime: hasFlag(flags, "shot-time") ? numFlag(flags, "shot-time", 0) : void 0,
|
|
10112
9344
|
composed: hasFlag(flags, "composed"),
|
|
10113
9345
|
overrides: {
|
|
10114
9346
|
set: multi.set,
|
|
@@ -10126,7 +9358,7 @@ async function cmdDeliver(argv) {
|
|
|
10126
9358
|
});
|
|
10127
9359
|
const { kit } = result;
|
|
10128
9360
|
const assetLines = kit.assets.map(
|
|
10129
|
-
(a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB${a.seconds !== null ? `, ${a.seconds.toFixed(1)}s` : ""}${a.frameTime !== null ? `, frame ${a.frameTime.toFixed(2)}s` : ""}${a.
|
|
9361
|
+
(a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB${a.seconds !== null ? `, ${a.seconds.toFixed(1)}s` : ""}${a.frameTime !== null ? `, frame ${a.frameTime.toFixed(2)}s` : ""}${a.poster ? `, poster ${a.poster.class}` : ""})`
|
|
10130
9362
|
);
|
|
10131
9363
|
const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
|
|
10132
9364
|
r.done(
|
|
@@ -10149,14 +9381,14 @@ Store uploads stay manual: hand the human this directory and the manifest.`
|
|
|
10149
9381
|
async function resolveStyleRef(flags) {
|
|
10150
9382
|
const styleRef = strFlag(flags, "style");
|
|
10151
9383
|
if (!styleRef) return null;
|
|
10152
|
-
const file =
|
|
9384
|
+
const file = existsSync14(styleRef) ? resolve9(
|
|
10153
9385
|
styleRef,
|
|
10154
|
-
|
|
9386
|
+
existsSync14(join17(styleRef, "doc.json")) ? "doc.json" : ""
|
|
10155
9387
|
) : null;
|
|
10156
|
-
if (file &&
|
|
9388
|
+
if (file && existsSync14(file)) {
|
|
10157
9389
|
return {
|
|
10158
9390
|
from: file,
|
|
10159
|
-
doc: JSON.parse(await
|
|
9391
|
+
doc: JSON.parse(await readFile14(file, "utf8"))
|
|
10160
9392
|
};
|
|
10161
9393
|
}
|
|
10162
9394
|
const origin = platformOrigin({
|
|
@@ -10179,7 +9411,7 @@ async function resolveStyleRef(flags) {
|
|
|
10179
9411
|
throw new UsageError(`--style: ${styleRef} carries no doc (is it a take?)`);
|
|
10180
9412
|
return {
|
|
10181
9413
|
from: `${origin}/vos/${styleRef}`,
|
|
10182
|
-
doc:
|
|
9414
|
+
doc: migrateHostedDoc5(doc.body)
|
|
10183
9415
|
};
|
|
10184
9416
|
}
|
|
10185
9417
|
async function cmdDigest(argv) {
|
|
@@ -10193,7 +9425,7 @@ async function cmdDigest(argv) {
|
|
|
10193
9425
|
const take = await loadTake(dir);
|
|
10194
9426
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
10195
9427
|
const transcriptPath = strFlag(flags, "transcript");
|
|
10196
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
9428
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile14(transcriptPath, "utf8"))) : null;
|
|
10197
9429
|
const style = await resolveStyleRef(flags);
|
|
10198
9430
|
const noFrames = flags["no-frames"] === true;
|
|
10199
9431
|
let browser = null;
|
|
@@ -10224,7 +9456,7 @@ async function cmdDigest(argv) {
|
|
|
10224
9456
|
kinds,
|
|
10225
9457
|
frames: d.images.full,
|
|
10226
9458
|
crops: d.images.crop,
|
|
10227
|
-
sheet: d.images.sheet ?
|
|
9459
|
+
sheet: d.images.sheet ? join17(result.outDir, d.images.sheet) : null,
|
|
10228
9460
|
scenes: kinds.scene ?? 0,
|
|
10229
9461
|
sourceDuration: d.take.sourceDuration,
|
|
10230
9462
|
outputDuration: d.take.outputDuration,
|
|
@@ -10248,14 +9480,14 @@ async function cmdOpen(argv) {
|
|
|
10248
9480
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
10249
9481
|
const r = createReporter(flags.json === true);
|
|
10250
9482
|
const take = await loadTake(dir);
|
|
10251
|
-
if (!
|
|
9483
|
+
if (!existsSync14(take.paths.recording)) {
|
|
10252
9484
|
throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
|
|
10253
9485
|
}
|
|
10254
9486
|
const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
|
|
10255
9487
|
/\/+$/,
|
|
10256
9488
|
""
|
|
10257
9489
|
);
|
|
10258
|
-
const server = await startTakeServer(
|
|
9490
|
+
const server = await startTakeServer(resolve9(dir), {});
|
|
10259
9491
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
10260
9492
|
r.event({ event: "open", server: server.base, url });
|
|
10261
9493
|
r.log(`take served at ${server.base}`);
|
|
@@ -10289,11 +9521,12 @@ async function cmdJudge(argv) {
|
|
|
10289
9521
|
"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)"
|
|
10290
9522
|
);
|
|
10291
9523
|
const r = createReporter(flags.json === true);
|
|
10292
|
-
const kitFile = target.endsWith("kit.json") ? target :
|
|
10293
|
-
if (!
|
|
10294
|
-
if (!
|
|
9524
|
+
const kitFile = target.endsWith("kit.json") ? target : join17(target, "kit.json");
|
|
9525
|
+
if (!existsSync14(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
9526
|
+
if (!existsSync14(against))
|
|
9527
|
+
throw new UsageError(`${against}: no reference manifest`);
|
|
10295
9528
|
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
10296
|
-
const verdicts = JSON.parse(await
|
|
9529
|
+
const verdicts = JSON.parse(await readFile14(result.verdictFile, "utf8")).verdicts;
|
|
10297
9530
|
const rate = winRate(verdicts);
|
|
10298
9531
|
const lines = result.sheets.map(
|
|
10299
9532
|
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
@@ -10313,7 +9546,7 @@ async function cmdValidate(argv) {
|
|
|
10313
9546
|
const target = positionals[0];
|
|
10314
9547
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
10315
9548
|
const r = createReporter(flags.json === true);
|
|
10316
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
9549
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync14(join17(target, "kit.json")) && !isTakeDir(target) ? join17(target, "kit.json") : null;
|
|
10317
9550
|
if (kitTarget) {
|
|
10318
9551
|
const picture = flags.picture === true;
|
|
10319
9552
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -10345,9 +9578,9 @@ async function cmdValidate(argv) {
|
|
|
10345
9578
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
10346
9579
|
return EXIT_OK;
|
|
10347
9580
|
}
|
|
10348
|
-
if (!isTakeDir(target) &&
|
|
9581
|
+
if (!isTakeDir(target) && existsSync14(join17(target, "config.json"))) {
|
|
10349
9582
|
const parsed = JSON.parse(
|
|
10350
|
-
await
|
|
9583
|
+
await readFile14(join17(target, "config.json"), "utf8")
|
|
10351
9584
|
);
|
|
10352
9585
|
const pre = preflightConfig(parsed);
|
|
10353
9586
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -10388,7 +9621,7 @@ async function cmdValidate(argv) {
|
|
|
10388
9621
|
const take = await loadTake(target);
|
|
10389
9622
|
const problems = [];
|
|
10390
9623
|
const warnings = [];
|
|
10391
|
-
if (!
|
|
9624
|
+
if (!existsSync14(take.paths.recording))
|
|
10392
9625
|
problems.push("missing recording.webm (re-run record)");
|
|
10393
9626
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
10394
9627
|
if (take.doc) {
|
|
@@ -10433,7 +9666,7 @@ async function cmdPush3(argv) {
|
|
|
10433
9666
|
const r = createReporter(flags.json === true);
|
|
10434
9667
|
const overrides = multi.override;
|
|
10435
9668
|
const result = await pushTake(
|
|
10436
|
-
|
|
9669
|
+
resolve9(dir),
|
|
10437
9670
|
{
|
|
10438
9671
|
key: strFlag(flags, "key"),
|
|
10439
9672
|
api: strFlag(flags, "api"),
|
|
@@ -10447,11 +9680,15 @@ async function cmdPush3(argv) {
|
|
|
10447
9680
|
},
|
|
10448
9681
|
r
|
|
10449
9682
|
);
|
|
9683
|
+
const pushedTo = platformOrigin({
|
|
9684
|
+
origin: strFlag(flags, "origin"),
|
|
9685
|
+
api: strFlag(flags, "api")
|
|
9686
|
+
}).replace(/\/+$/, "");
|
|
10450
9687
|
r.done(
|
|
10451
9688
|
{ ...result },
|
|
10452
9689
|
`pushed v${result.versionNumber} \u2192 vos ${result.vosId}
|
|
10453
|
-
review:
|
|
10454
|
-
studio:
|
|
9690
|
+
review: ${pushedTo}/vos/${result.vosId}
|
|
9691
|
+
studio: ${pushedTo}/studio?vos=${result.vosId}`
|
|
10455
9692
|
);
|
|
10456
9693
|
return EXIT_OK;
|
|
10457
9694
|
}
|
|
@@ -10460,7 +9697,7 @@ async function cmdPull2(argv) {
|
|
|
10460
9697
|
const dir = positionals[0] ?? ".";
|
|
10461
9698
|
const r = createReporter(flags.json === true);
|
|
10462
9699
|
const result = await pullTake(
|
|
10463
|
-
|
|
9700
|
+
resolve9(dir),
|
|
10464
9701
|
{
|
|
10465
9702
|
key: strFlag(flags, "key"),
|
|
10466
9703
|
api: strFlag(flags, "api"),
|
|
@@ -10595,4 +9832,4 @@ export {
|
|
|
10595
9832
|
convertAgentBrowser,
|
|
10596
9833
|
run
|
|
10597
9834
|
};
|
|
10598
|
-
//# sourceMappingURL=chunk-
|
|
9835
|
+
//# sourceMappingURL=chunk-HYUVNBEM.js.map
|