@vosjs/cli 0.17.4 → 0.19.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-Y7BXFF2W.js → chunk-4LLBPFCW.js} +1411 -2046
- package/dist/chunk-4LLBPFCW.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-AE3NXEZ4.js → run-PJOOKJE2.js} +2 -2
- package/package.json +5 -5
- package/schema/doc.schema.json +163 -296
- package/dist/chunk-Y7BXFF2W.js.map +0 -1
- /package/dist/{run-AE3NXEZ4.js.map → run-PJOOKJE2.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 {
|
|
@@ -181,6 +181,13 @@ import {
|
|
|
181
181
|
CAM_SIZE_MAX,
|
|
182
182
|
CAM_SIZE_MIN,
|
|
183
183
|
CAM_SPAN_MIN,
|
|
184
|
+
CARD_ENTER_KINDS,
|
|
185
|
+
CARD_EXIT_KINDS,
|
|
186
|
+
IDLE_KINDS,
|
|
187
|
+
MEDIA_ENTER_KINDS,
|
|
188
|
+
MEDIA_EXIT_KINDS,
|
|
189
|
+
TEXT_ENTER_KINDS,
|
|
190
|
+
TEXT_EXIT_KINDS,
|
|
184
191
|
EXPORT_RESOLUTION_OPTIONS,
|
|
185
192
|
SPEED_RATE_MAX,
|
|
186
193
|
SPEED_RATE_MIN,
|
|
@@ -196,6 +203,79 @@ import {
|
|
|
196
203
|
zoomCoversRect
|
|
197
204
|
} from "@vosjs/studio-core";
|
|
198
205
|
import { TYPEFACE_CATALOG, findFontFamily, findTypeface } from "@vosjs/shared";
|
|
206
|
+
var STEP_KINDS_ALL = [
|
|
207
|
+
"none",
|
|
208
|
+
"fade",
|
|
209
|
+
"rise",
|
|
210
|
+
"pop",
|
|
211
|
+
"blur",
|
|
212
|
+
"typewriter",
|
|
213
|
+
"tilt-in",
|
|
214
|
+
"pull-out",
|
|
215
|
+
"recede"
|
|
216
|
+
];
|
|
217
|
+
var STEP_UNITS = ["block", "line", "word", "char"];
|
|
218
|
+
var STEP_DIRS = ["forward", "reverse", "center"];
|
|
219
|
+
function lintAnim(name, anim, kinds, problems) {
|
|
220
|
+
if (anim === void 0) return;
|
|
221
|
+
if (typeof anim !== "object" || anim === null || Array.isArray(anim)) {
|
|
222
|
+
problems.push(`${name}.anim must be { enter?, exit?, idle? }`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const a = anim;
|
|
226
|
+
for (const side of ["enter", "exit"]) {
|
|
227
|
+
const step = a[side];
|
|
228
|
+
if (step === void 0) continue;
|
|
229
|
+
const allowed = kinds[side];
|
|
230
|
+
const spelled = typeof step === "string" ? step : null;
|
|
231
|
+
const obj = typeof step === "object" && step !== null && !Array.isArray(step) ? step : null;
|
|
232
|
+
const kind = spelled ?? (obj ? obj.kind : void 0);
|
|
233
|
+
if (typeof kind !== "string" || !STEP_KINDS_ALL.includes(kind)) {
|
|
234
|
+
problems.push(
|
|
235
|
+
`${name}.anim.${side} must be a kind or { kind, seconds?${kinds.words ? ", unit?, direction?, stagger?" : ""} } (got ${JSON.stringify(step)})`
|
|
236
|
+
);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!allowed.includes(kind)) {
|
|
240
|
+
problems.push(
|
|
241
|
+
`${name}.anim.${side} cannot be "${kind}" here; one of ${allowed.join(" | ")}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (!obj) continue;
|
|
245
|
+
const secs = obj.seconds;
|
|
246
|
+
if (secs !== void 0 && (typeof secs !== "number" || !Number.isFinite(secs) || secs < 0.05 || secs > 3)) {
|
|
247
|
+
problems.push(`${name}.anim.${side}.seconds must be 0.05..3`);
|
|
248
|
+
}
|
|
249
|
+
for (const key of ["unit", "direction", "stagger"]) {
|
|
250
|
+
if (obj[key] === void 0) continue;
|
|
251
|
+
if (!kinds.words) {
|
|
252
|
+
problems.push(`${name}.anim.${side}.${key} is for words only`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (key === "unit" && (typeof obj.unit !== "string" || !STEP_UNITS.includes(obj.unit)))
|
|
256
|
+
problems.push(
|
|
257
|
+
`${name}.anim.${side}.unit must be ${STEP_UNITS.join("|")}`
|
|
258
|
+
);
|
|
259
|
+
if (key === "direction" && (typeof obj.direction !== "string" || !STEP_DIRS.includes(obj.direction)))
|
|
260
|
+
problems.push(
|
|
261
|
+
`${name}.anim.${side}.direction must be ${STEP_DIRS.join("|")}`
|
|
262
|
+
);
|
|
263
|
+
if (key === "stagger" && (typeof obj.stagger !== "number" || obj.stagger < 0 || obj.stagger > 2))
|
|
264
|
+
problems.push(`${name}.anim.${side}.stagger must be seconds in 0..2`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (a.idle !== void 0 && a.idle !== null) {
|
|
268
|
+
if (!kinds.idle) problems.push(`${name}.anim.idle is for props only`);
|
|
269
|
+
else if (typeof a.idle !== "string" || !kinds.idle.includes(a.idle))
|
|
270
|
+
problems.push(
|
|
271
|
+
`${name}.anim.idle must be ${kinds.idle.join(" | ")} | null`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function lintFrom(name, from, problems) {
|
|
276
|
+
if (from !== void 0 && (typeof from !== "string" || !from.length))
|
|
277
|
+
problems.push(`${name}.from must be the id of the template that placed it`);
|
|
278
|
+
}
|
|
199
279
|
var EPS = 1e-3;
|
|
200
280
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
201
281
|
var isObj = (v) => typeof v === "object" && v !== null;
|
|
@@ -328,19 +408,27 @@ function lintDoc(docIn) {
|
|
|
328
408
|
const endCard = doc.endCard;
|
|
329
409
|
if (endCard !== void 0) {
|
|
330
410
|
if (typeof endCard !== "object" || endCard === null) {
|
|
331
|
-
problems.push(
|
|
411
|
+
problems.push(
|
|
412
|
+
"endCard must be an object: {seconds?, headline?, sub?, wordmark?}"
|
|
413
|
+
);
|
|
332
414
|
} else {
|
|
333
415
|
const ec = endCard;
|
|
334
416
|
if (ec.seconds !== void 0 && (!isNum(ec.seconds) || ec.seconds < 1 || ec.seconds > 8)) {
|
|
335
|
-
problems.push(
|
|
417
|
+
problems.push(
|
|
418
|
+
`endCard.seconds must be 1..8 (got ${String(ec.seconds)}); absent = 2.5`
|
|
419
|
+
);
|
|
336
420
|
}
|
|
337
421
|
for (const k of ["headline", "sub", "wordmark"]) {
|
|
338
422
|
if (ec[k] !== void 0 && typeof ec[k] !== "string") {
|
|
339
423
|
problems.push(`endCard.${k} must be a string`);
|
|
340
424
|
}
|
|
341
425
|
}
|
|
342
|
-
if (!["headline", "sub", "wordmark"].some(
|
|
343
|
-
|
|
426
|
+
if (!["headline", "sub", "wordmark"].some(
|
|
427
|
+
(k) => typeof ec[k] === "string" && ec[k].trim()
|
|
428
|
+
)) {
|
|
429
|
+
warnings.push(
|
|
430
|
+
"endCard carries no words: it holds the last frame and recedes the card over nothing"
|
|
431
|
+
);
|
|
344
432
|
}
|
|
345
433
|
}
|
|
346
434
|
}
|
|
@@ -579,6 +667,17 @@ function lintDoc(docIn) {
|
|
|
579
667
|
}
|
|
580
668
|
}
|
|
581
669
|
}
|
|
670
|
+
lintAnim(
|
|
671
|
+
"frame",
|
|
672
|
+
frame.anim,
|
|
673
|
+
{
|
|
674
|
+
enter: CARD_ENTER_KINDS,
|
|
675
|
+
exit: CARD_EXIT_KINDS,
|
|
676
|
+
idle: null,
|
|
677
|
+
words: false
|
|
678
|
+
},
|
|
679
|
+
problems
|
|
680
|
+
);
|
|
582
681
|
if (frame.focusFollow !== void 0 && frame.focusFollow !== "camera") {
|
|
583
682
|
problems.push(
|
|
584
683
|
`frame.focusFollow must be "camera" (got ${String(frame.focusFollow)}); it reads under fit: cover only`
|
|
@@ -826,6 +925,23 @@ function lintDoc(docIn) {
|
|
|
826
925
|
problems.push(`${name}.${key} must be one of ${TRANSITIONS.join("|")}`);
|
|
827
926
|
}
|
|
828
927
|
}
|
|
928
|
+
lintAnim(
|
|
929
|
+
name,
|
|
930
|
+
o.anim,
|
|
931
|
+
o.kind === "text" ? {
|
|
932
|
+
enter: TEXT_ENTER_KINDS,
|
|
933
|
+
exit: TEXT_EXIT_KINDS,
|
|
934
|
+
idle: null,
|
|
935
|
+
words: true
|
|
936
|
+
} : {
|
|
937
|
+
enter: MEDIA_ENTER_KINDS,
|
|
938
|
+
exit: MEDIA_EXIT_KINDS,
|
|
939
|
+
idle: null,
|
|
940
|
+
words: false
|
|
941
|
+
},
|
|
942
|
+
problems
|
|
943
|
+
);
|
|
944
|
+
lintFrom(name, o.from, problems);
|
|
829
945
|
if (o.motion !== void 0) {
|
|
830
946
|
if (!Array.isArray(o.motion)) {
|
|
831
947
|
problems.push(`${name}.motion must be an array of poses`);
|
|
@@ -956,6 +1072,18 @@ function lintDoc(docIn) {
|
|
|
956
1072
|
if (o.animation !== void 0 && o.animation !== null && o.animation !== "spin" && o.animation !== "float") {
|
|
957
1073
|
problems.push(`${name}.animation must be "spin" | "float" | null`);
|
|
958
1074
|
}
|
|
1075
|
+
lintAnim(
|
|
1076
|
+
name,
|
|
1077
|
+
o.anim,
|
|
1078
|
+
{
|
|
1079
|
+
enter: MEDIA_ENTER_KINDS,
|
|
1080
|
+
exit: MEDIA_EXIT_KINDS,
|
|
1081
|
+
idle: IDLE_KINDS,
|
|
1082
|
+
words: false
|
|
1083
|
+
},
|
|
1084
|
+
problems
|
|
1085
|
+
);
|
|
1086
|
+
lintFrom(name, o.from, problems);
|
|
959
1087
|
if (o.motion !== void 0) {
|
|
960
1088
|
if (!Array.isArray(o.motion)) {
|
|
961
1089
|
problems.push(`${name}.motion must be an array of poses`);
|
|
@@ -1352,10 +1480,10 @@ function startTakeServer(rootDir, pages) {
|
|
|
1352
1480
|
res.writeHead(404).end();
|
|
1353
1481
|
});
|
|
1354
1482
|
return new Promise(
|
|
1355
|
-
(
|
|
1483
|
+
(resolve10) => server.listen(0, () => {
|
|
1356
1484
|
const addr = server.address();
|
|
1357
1485
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1358
|
-
|
|
1486
|
+
resolve10({ base: `http://localhost:${port}`, close: () => server.close() });
|
|
1359
1487
|
})
|
|
1360
1488
|
);
|
|
1361
1489
|
}
|
|
@@ -1806,7 +1934,7 @@ async function resolveBackdropSlug(o) {
|
|
|
1806
1934
|
o.backgroundDuration = hit.duration;
|
|
1807
1935
|
}
|
|
1808
1936
|
function hasOverrides(o) {
|
|
1809
|
-
return !!(o.set?.length || o.frame !== void 0 || o.background !== void 0);
|
|
1937
|
+
return !!(o.set?.length || o.unset?.length || o.frame !== void 0 || o.background !== void 0);
|
|
1810
1938
|
}
|
|
1811
1939
|
var FRAME_KINDS = {
|
|
1812
1940
|
macos: "mac-light",
|
|
@@ -1913,6 +2041,16 @@ function applyDocOverrides(doc, o) {
|
|
|
1913
2041
|
setPath(d, path, value);
|
|
1914
2042
|
applied.push(`${path} = ${JSON.stringify(value)}`);
|
|
1915
2043
|
}
|
|
2044
|
+
for (const path of o.unset ?? []) {
|
|
2045
|
+
const segs = parsePath(path);
|
|
2046
|
+
let node = d;
|
|
2047
|
+
for (let i = 0; i < segs.length - 1 && node && typeof node === "object"; i++)
|
|
2048
|
+
node = node[segs[i].key];
|
|
2049
|
+
if (node && typeof node === "object") {
|
|
2050
|
+
delete node[segs[segs.length - 1].key];
|
|
2051
|
+
applied.push(`${path} removed`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
1916
2054
|
return applied;
|
|
1917
2055
|
}
|
|
1918
2056
|
function applyAndValidate(doc, o) {
|
|
@@ -2476,8 +2614,8 @@ async function stillsInPage(opts) {
|
|
|
2476
2614
|
}
|
|
2477
2615
|
async function framesTake(browser, dir, opts) {
|
|
2478
2616
|
const take = await loadTake(dir);
|
|
2479
|
-
|
|
2480
|
-
|
|
2617
|
+
const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
|
|
2618
|
+
if (!doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
|
|
2481
2619
|
if (opts.overrides && hasOverrides(opts.overrides)) {
|
|
2482
2620
|
await resolveBackdropSlug(opts.overrides);
|
|
2483
2621
|
applyAndValidate(doc, opts.overrides);
|
|
@@ -2505,7 +2643,7 @@ async function framesTake(browser, dir, opts) {
|
|
|
2505
2643
|
}
|
|
2506
2644
|
}
|
|
2507
2645
|
if (opts.atMoments) {
|
|
2508
|
-
for (const m of await momentsFor(dir,
|
|
2646
|
+
for (const m of await momentsFor(dir, doc)) {
|
|
2509
2647
|
if (m.outputAt === null) continue;
|
|
2510
2648
|
shots.push({ time: clamp(m.outputAt), kind: "moment", momentId: m.id });
|
|
2511
2649
|
}
|
|
@@ -2618,9 +2756,9 @@ async function writeIndexJson(result) {
|
|
|
2618
2756
|
}
|
|
2619
2757
|
|
|
2620
2758
|
// src/plugin/deliver.ts
|
|
2621
|
-
import { mkdir as
|
|
2759
|
+
import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
|
|
2622
2760
|
import { tmpdir } from "os";
|
|
2623
|
-
import { join as
|
|
2761
|
+
import { join as join7, relative, resolve as resolve4 } from "path";
|
|
2624
2762
|
import { totalDuration } from "@vosjs/timeline";
|
|
2625
2763
|
import { existsSync as existsSync6 } from "fs";
|
|
2626
2764
|
import { readFile as readFile6 } from "fs/promises";
|
|
@@ -2741,114 +2879,6 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
2741
2879
|
};
|
|
2742
2880
|
}
|
|
2743
2881
|
|
|
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
2882
|
// src/plugin/moments.ts
|
|
2853
2883
|
import { ratedSegments as ratedSegments3, spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
|
|
2854
2884
|
|
|
@@ -3356,1242 +3386,516 @@ function pickMoments(measured, opts = {}) {
|
|
|
3356
3386
|
return { times: kept.map((k) => k.time), dropped };
|
|
3357
3387
|
}
|
|
3358
3388
|
|
|
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
|
-
|
|
3389
|
+
// src/plugin/motionPlan.ts
|
|
3390
|
+
import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
|
|
3391
|
+
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
3392
|
+
"x-feed-cut",
|
|
3393
|
+
"youtube-main-demo",
|
|
3394
|
+
"shorts-linkedin-vertical-cut"
|
|
3395
|
+
]);
|
|
3396
|
+
var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
3397
|
+
var BED_ID = "bed";
|
|
3398
|
+
var CLICK_ID_PREFIX = "click-";
|
|
3399
|
+
var CAPTION_ID_PREFIX = "caption-";
|
|
3400
|
+
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
3401
|
+
function pickTrack(catalog, ask) {
|
|
3402
|
+
if (!catalog || !ask || off(ask)) return null;
|
|
3403
|
+
const want = ask.trim().toLowerCase();
|
|
3404
|
+
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
3405
|
+
}
|
|
3406
|
+
function clickTimes(doc, range) {
|
|
3407
|
+
const rated = ratedSegments4(doc);
|
|
3408
|
+
const out = [];
|
|
3409
|
+
for (const e of doc.source.cursor) {
|
|
3410
|
+
if (e.type !== "down") continue;
|
|
3411
|
+
const src = e.t / 1e3;
|
|
3412
|
+
const ext = spanOutputExtent4(rated, src, src + 1e-3);
|
|
3413
|
+
if (!ext) continue;
|
|
3414
|
+
const t = ext.start;
|
|
3415
|
+
if (t < range[0] || t > range[1]) continue;
|
|
3416
|
+
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
3417
|
+
out.push(+(t - range[0]).toFixed(3));
|
|
3388
3418
|
}
|
|
3419
|
+
return out;
|
|
3389
3420
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
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);
|
|
3421
|
+
var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
|
|
3422
|
+
const rate = s.rate && s.rate > 0 ? s.rate : 1;
|
|
3423
|
+
return acc + (s.out - s.in) / rate;
|
|
3424
|
+
}, 0);
|
|
3425
|
+
function proposeMotion(input, opts) {
|
|
3426
|
+
const doc = structuredClone(input);
|
|
3427
|
+
const { words, launch, catalog } = opts;
|
|
3428
|
+
const notes = [];
|
|
3429
|
+
const skipped = [];
|
|
3430
|
+
const length = outputLength(doc);
|
|
3431
|
+
const range = [0, length];
|
|
3432
|
+
const entrance = launch.entrance;
|
|
3433
|
+
if (!off(entrance)) {
|
|
3434
|
+
const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
|
|
3435
|
+
doc.frame.entrance = { kind };
|
|
3436
|
+
notes.push(`entrance ${kind}`);
|
|
3437
|
+
} else {
|
|
3438
|
+
delete doc.frame.entrance;
|
|
3439
|
+
}
|
|
3440
|
+
if (!off(launch.endCard)) {
|
|
3441
|
+
const headline = (words.headline ?? "").trim();
|
|
3442
|
+
const brand = (words.brand ?? "").trim();
|
|
3443
|
+
const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
|
|
3444
|
+
if (headline || brand) {
|
|
3445
|
+
const card = { seconds: 2.5 };
|
|
3446
|
+
if (opts.ink) card.ink = opts.ink;
|
|
3447
|
+
if (headline) card.headline = headline;
|
|
3448
|
+
if (sub && sub !== headline) card.sub = sub;
|
|
3449
|
+
if (brand) card.wordmark = brand;
|
|
3450
|
+
if (opts.mark) card.mark = opts.mark;
|
|
3451
|
+
doc.endCard = card;
|
|
3452
|
+
notes.push("end card");
|
|
3453
|
+
} else {
|
|
3454
|
+
skipped.push(
|
|
3455
|
+
"no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)"
|
|
3456
|
+
);
|
|
3433
3457
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3458
|
+
} else {
|
|
3459
|
+
delete doc.endCard;
|
|
3460
|
+
}
|
|
3461
|
+
const kept = (doc.overlays ?? []).filter(
|
|
3462
|
+
(o) => !o.id.startsWith(CAPTION_ID_PREFIX)
|
|
3463
|
+
);
|
|
3464
|
+
const captionClips = [];
|
|
3465
|
+
if (opts.captions.length && !off(launch.captions)) {
|
|
3466
|
+
const rated = ratedSegments4(doc);
|
|
3467
|
+
const steps = doc.source.meta.steps ?? [];
|
|
3468
|
+
for (const c of opts.captions) {
|
|
3469
|
+
const step = steps.find(
|
|
3470
|
+
(s) => c.id !== void 0 && s.id === c.id || s.step === c.step
|
|
3471
|
+
);
|
|
3472
|
+
if (!step || step.skipped) continue;
|
|
3473
|
+
const t = stepOutputTime(rated, step, 0.2);
|
|
3474
|
+
if (t === null || t < 0 || t > length - 1) continue;
|
|
3475
|
+
captionClips.push({
|
|
3476
|
+
id: `${CAPTION_ID_PREFIX}${c.step}`,
|
|
3477
|
+
kind: "text",
|
|
3478
|
+
text: c.caption,
|
|
3479
|
+
preset: "caption",
|
|
3480
|
+
start: +t.toFixed(3),
|
|
3481
|
+
duration: Math.min(3.5, Math.max(2.5, length - t - 0.2)),
|
|
3482
|
+
transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
|
|
3483
|
+
enter: "rise",
|
|
3484
|
+
exit: "fade",
|
|
3485
|
+
align: "center",
|
|
3486
|
+
box: { color: "rgba(17,17,17,0.72)" }
|
|
3487
|
+
});
|
|
3441
3488
|
}
|
|
3489
|
+
if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
|
|
3442
3490
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3491
|
+
const overlays = [...kept, ...captionClips];
|
|
3492
|
+
if (overlays.length) doc.overlays = overlays;
|
|
3493
|
+
else delete doc.overlays;
|
|
3494
|
+
const clips = (doc.audio ?? []).filter(
|
|
3495
|
+
(a) => a.id !== BED_ID && !a.id.startsWith(CLICK_ID_PREFIX)
|
|
3496
|
+
);
|
|
3497
|
+
const track = pickTrack(catalog, launch.music);
|
|
3498
|
+
if (track) {
|
|
3499
|
+
const hasMic = !!doc.source.micKey;
|
|
3500
|
+
const fadeOut = Math.min(2.5, length * 0.15);
|
|
3501
|
+
clips.push({
|
|
3502
|
+
id: BED_ID,
|
|
3503
|
+
key: track.url,
|
|
3504
|
+
name: track.title,
|
|
3505
|
+
start: 0,
|
|
3506
|
+
in: 0,
|
|
3507
|
+
out: Math.min(track.duration, length),
|
|
3508
|
+
duration: track.duration,
|
|
3509
|
+
gain: hasMic ? 0.35 : 0.5,
|
|
3510
|
+
fadeIn: 0.6,
|
|
3511
|
+
fadeOut,
|
|
3512
|
+
loop: track.duration < length,
|
|
3513
|
+
loopLen: track.duration < length ? length : void 0,
|
|
3514
|
+
duck: hasMic
|
|
3515
|
+
});
|
|
3516
|
+
notes.push(`bed ${track.slug}`);
|
|
3517
|
+
} else if (launch.music && !off(launch.music)) {
|
|
3518
|
+
skipped.push(
|
|
3519
|
+
`music "${launch.music}" is not a catalog track or mood${catalog ? "" : " (the catalog could not be read)"}`
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
const click = catalog?.sfx.find((s) => s.slug === "sfx-click");
|
|
3523
|
+
if (click && !doc.source.micKey && !off(launch.clicks)) {
|
|
3524
|
+
const times = clickTimes(doc, range);
|
|
3525
|
+
for (const [i, t] of times.entries()) {
|
|
3526
|
+
clips.push({
|
|
3527
|
+
id: `${CLICK_ID_PREFIX}${i}`,
|
|
3528
|
+
key: click.url,
|
|
3529
|
+
name: click.title,
|
|
3530
|
+
start: t,
|
|
3531
|
+
in: 0,
|
|
3532
|
+
out: click.duration,
|
|
3533
|
+
duration: click.duration,
|
|
3534
|
+
gain: 0.4,
|
|
3535
|
+
fadeIn: 0,
|
|
3536
|
+
fadeOut: 0
|
|
3537
|
+
});
|
|
3475
3538
|
}
|
|
3539
|
+
if (times.length) notes.push(`${times.length} click sound(s)`);
|
|
3476
3540
|
}
|
|
3477
|
-
|
|
3541
|
+
doc.audio = clips;
|
|
3542
|
+
return { doc, notes, skipped };
|
|
3478
3543
|
}
|
|
3479
|
-
|
|
3480
|
-
const
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3544
|
+
function destinationMechanics(d, doc) {
|
|
3545
|
+
const set = [];
|
|
3546
|
+
const unset = [];
|
|
3547
|
+
const notes = [];
|
|
3548
|
+
if (d.kind !== "video") return { set, unset, notes };
|
|
3549
|
+
const loop = LOOP_DESTINATIONS.has(d.id);
|
|
3550
|
+
const sound = SOUND_DESTINATIONS.has(d.id);
|
|
3551
|
+
const portrait = d.px.w / d.px.h < 0.9;
|
|
3552
|
+
if (loop) {
|
|
3553
|
+
unset.push("frame.entrance", "endCard");
|
|
3554
|
+
notes.push("loop: no entrance, no end card");
|
|
3485
3555
|
}
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
3498
|
-
out.set(body, 8);
|
|
3499
|
-
dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
|
|
3500
|
-
return out;
|
|
3501
|
-
}
|
|
3502
|
-
function encodePng(img) {
|
|
3503
|
-
const stride = img.w * 4;
|
|
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;
|
|
3556
|
+
if (loop || !sound) {
|
|
3557
|
+
set.push("audio=[]");
|
|
3558
|
+
if (!loop) notes.push("silent channel");
|
|
3559
|
+
}
|
|
3560
|
+
if (loop || d.text === "none") {
|
|
3561
|
+
const kept = (doc.overlays ?? []).filter(
|
|
3562
|
+
(o) => !o.id.startsWith(CAPTION_ID_PREFIX)
|
|
3563
|
+
);
|
|
3564
|
+
if (kept.length !== (doc.overlays ?? []).length) {
|
|
3565
|
+
set.push(`overlays=${JSON.stringify(kept)}`);
|
|
3566
|
+
notes.push("no captions");
|
|
3511
3567
|
}
|
|
3512
3568
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
ihdr[9] = 6;
|
|
3519
|
-
const parts = [
|
|
3520
|
-
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
3521
|
-
chunk("IHDR", ihdr),
|
|
3522
|
-
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
3523
|
-
chunk("IEND", new Uint8Array(0))
|
|
3524
|
-
];
|
|
3525
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
3526
|
-
let o = 0;
|
|
3527
|
-
for (const p of parts) {
|
|
3528
|
-
out.set(p, o);
|
|
3529
|
-
o += p.length;
|
|
3569
|
+
if (portrait) {
|
|
3570
|
+
set.push("frame.fit=cover");
|
|
3571
|
+
set.push('frame.inset={"left":0.06,"right":0.06,"top":0.17,"bottom":0.17}');
|
|
3572
|
+
set.push("frame.focusFollow=camera");
|
|
3573
|
+
notes.push("vertical reframe follows the camera");
|
|
3530
3574
|
}
|
|
3531
|
-
return
|
|
3575
|
+
return { set, unset, notes };
|
|
3532
3576
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
const
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
if (r > 1.15) return "landscape";
|
|
3543
|
-
if (r < 0.87) return "portrait";
|
|
3544
|
-
return "square";
|
|
3577
|
+
function endCardInk(look, brand) {
|
|
3578
|
+
if (!look) return null;
|
|
3579
|
+
const ground = look.ground;
|
|
3580
|
+
const m = /#([0-9a-f]{6})/i.exec(ground);
|
|
3581
|
+
const hex2 = m ? m[0] : null;
|
|
3582
|
+
const light = look.kind === "plate" || (hex2 ? isLightHexGround(hex2) : look.kind === "gradient");
|
|
3583
|
+
if (!light) return "#ffffff";
|
|
3584
|
+
const ink = brand?.ink;
|
|
3585
|
+
return ink && /^#[0-9a-f]{6}$/i.test(ink) ? ink : "#111111";
|
|
3545
3586
|
}
|
|
3546
|
-
function
|
|
3547
|
-
const
|
|
3548
|
-
|
|
3549
|
-
const
|
|
3550
|
-
const
|
|
3551
|
-
|
|
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`);
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
return out;
|
|
3587
|
+
function isLightHexGround(hex2) {
|
|
3588
|
+
const n = parseInt(hex2.slice(1), 16);
|
|
3589
|
+
const r = n >> 16 & 255;
|
|
3590
|
+
const g = n >> 8 & 255;
|
|
3591
|
+
const b = n & 255;
|
|
3592
|
+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
|
|
3580
3593
|
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
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;
|
|
3650
|
-
}
|
|
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
|
-
}
|
|
3681
|
-
for (const id of layout.hide ?? []) {
|
|
3682
|
-
const el = byId.get(id);
|
|
3683
|
-
if (el && !t.text.some((x) => x.element === id)) el.opacity = 0;
|
|
3594
|
+
|
|
3595
|
+
// src/plugin/posterDoc.ts
|
|
3596
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3597
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3598
|
+
import { isAbsolute, join as join6, resolve as resolve3 } from "path";
|
|
3599
|
+
import {
|
|
3600
|
+
computeCardLayout,
|
|
3601
|
+
docRestTime,
|
|
3602
|
+
migrateHostedDoc,
|
|
3603
|
+
overlayRect,
|
|
3604
|
+
resolveOverlayStyle
|
|
3605
|
+
} from "@vosjs/studio-core";
|
|
3606
|
+
|
|
3607
|
+
// src/plugin/platform.ts
|
|
3608
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3609
|
+
import { homedir } from "os";
|
|
3610
|
+
import { join as join5 } from "path";
|
|
3611
|
+
function platformOrigin(flags = {}) {
|
|
3612
|
+
const legacyEnv = process.env.VOS_API_BASE?.trim();
|
|
3613
|
+
const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
|
|
3614
|
+
if (!flags.origin && !flags.api && !process.env.VOS_ORIGIN && legacyEnv) {
|
|
3615
|
+
process.stderr.write(
|
|
3616
|
+
"note: VOS_API_BASE is deprecated \u2014 set VOS_ORIGIN (an origin, no /api suffix)\n"
|
|
3617
|
+
);
|
|
3684
3618
|
}
|
|
3685
|
-
return
|
|
3619
|
+
return raw.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
3686
3620
|
}
|
|
3687
|
-
function
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
if (typeof v !== "string") continue;
|
|
3692
|
-
const words = v.trim().split(/\s+/).filter(Boolean).length;
|
|
3693
|
-
const lines = v.split("\n").length;
|
|
3694
|
-
if (x.maxWords && words > x.maxWords)
|
|
3695
|
-
out.push(`${x.param}: ${words} words, the template holds ${x.maxWords}`);
|
|
3696
|
-
if (x.lines && lines > x.lines)
|
|
3697
|
-
out.push(`${x.param}: ${lines} lines, the template holds ${x.lines}`);
|
|
3621
|
+
function parseVosId(input) {
|
|
3622
|
+
if (!/^https?:\/\//.test(input)) {
|
|
3623
|
+
if (/^[A-Za-z0-9_-]+$/.test(input)) return input;
|
|
3624
|
+
throw new UsageError(`"${input}" is not a vos id or URL`);
|
|
3698
3625
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
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
|
-
};
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
// src/plugin/templates/splitCover.ts
|
|
3797
|
-
var GROUND2 = `(ctx) => {
|
|
3798
|
-
const THREE = ctx.THREE
|
|
3799
|
-
const d = ctx.data
|
|
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
|
-
};
|
|
3626
|
+
let url;
|
|
3627
|
+
try {
|
|
3628
|
+
url = new URL(input);
|
|
3629
|
+
} catch {
|
|
3630
|
+
throw new UsageError(`"${input}" is not a valid URL`);
|
|
3631
|
+
}
|
|
3632
|
+
const query = url.searchParams.get("vos");
|
|
3633
|
+
if (query) return query;
|
|
3634
|
+
const path = url.pathname.match(/\/vos\/([A-Za-z0-9_-]+)/);
|
|
3635
|
+
if (path) return path[1];
|
|
3636
|
+
throw new UsageError(
|
|
3637
|
+
`could not find a vos id in ${input} \u2014 expected /vos/{id} or ?vos={id}`
|
|
3638
|
+
);
|
|
3958
3639
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
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;
|
|
3640
|
+
function deriveSlug(title) {
|
|
3641
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
|
|
3969
3642
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
const n = parseInt(m[1], 16);
|
|
3983
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
3984
|
-
};
|
|
3985
|
-
var toHex = (c) => "#" + c.map(
|
|
3986
|
-
(v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")
|
|
3987
|
-
).join("");
|
|
3988
|
-
function mixHex(a, b, t) {
|
|
3989
|
-
const pa = rgb(a);
|
|
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
|
-
]);
|
|
3643
|
+
var CREDENTIALS_PATH = join5(homedir(), ".config", "vos", "credentials");
|
|
3644
|
+
function resolveCredential(explicit) {
|
|
3645
|
+
const flag = explicit?.trim();
|
|
3646
|
+
if (flag) return flag;
|
|
3647
|
+
const env = process.env.VOS_API_KEY?.trim();
|
|
3648
|
+
if (env) return env;
|
|
3649
|
+
try {
|
|
3650
|
+
const first = readFileSync2(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
|
|
3651
|
+
return first || null;
|
|
3652
|
+
} catch {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3997
3655
|
}
|
|
3998
|
-
function
|
|
3999
|
-
const
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
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
|
|
3656
|
+
function requireCredential(explicit) {
|
|
3657
|
+
const key = resolveCredential(explicit);
|
|
3658
|
+
if (!key) {
|
|
3659
|
+
throw new Error(
|
|
3660
|
+
"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)"
|
|
4046
3661
|
);
|
|
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
|
-
}
|
|
4058
|
-
const body = resolveFace(b.fontBody, [700]);
|
|
4059
|
-
if (body) {
|
|
4060
|
-
values.fontBody = body.stack;
|
|
4061
|
-
fonts.push(...body.fonts);
|
|
4062
3662
|
}
|
|
4063
|
-
return
|
|
3663
|
+
return key;
|
|
4064
3664
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
const R = input.size.w / Math.max(1, input.size.h);
|
|
4071
|
-
const portrait = R < 0.87;
|
|
4072
|
-
const square = !portrait && R <= 1.15;
|
|
4073
|
-
const v = input.values;
|
|
4074
|
-
const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
|
|
4075
|
-
const ink = str(v.ink, "#fff6ec");
|
|
4076
|
-
const inkSoft = str(v.inkSoft, ink);
|
|
4077
|
-
const display = firstFamily(str(v.fontDisplay, "Lexend"));
|
|
4078
|
-
const body = firstFamily(str(v.fontBody, "Lexend"));
|
|
4079
|
-
const headline = str(v.headline, "");
|
|
4080
|
-
const kicker = str(v.kicker, "");
|
|
4081
|
-
const brand = str(v.brand, "");
|
|
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"
|
|
4197
|
-
);
|
|
4198
|
-
const brandX = lockup(25, brandY);
|
|
4199
|
-
if (!wideMark)
|
|
4200
|
-
word(
|
|
4201
|
-
"stage-brand",
|
|
4202
|
-
brand,
|
|
4203
|
-
"label",
|
|
4204
|
-
body,
|
|
4205
|
-
25,
|
|
4206
|
-
brandY,
|
|
4207
|
-
ink,
|
|
4208
|
-
{ weight: 700, letterSpacing: 1 },
|
|
4209
|
-
"body",
|
|
4210
|
-
brandX
|
|
4211
|
-
);
|
|
4212
|
-
set.push(`overlays=${JSON.stringify(clips)}`);
|
|
4213
|
-
return { set, text: boxes, shot };
|
|
3665
|
+
function writeCredential(key) {
|
|
3666
|
+
mkdirSync(join5(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
|
|
3667
|
+
writeFileSync(CREDENTIALS_PATH, `${key.trim()}
|
|
3668
|
+
`, { mode: 384 });
|
|
3669
|
+
return CREDENTIALS_PATH;
|
|
4214
3670
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
const
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
const ink = str(v.ink, "#fff6ec");
|
|
4225
|
-
const display = firstFamily(str(v.fontDisplay, "Lexend"));
|
|
4226
|
-
const headline = str(v.headline, "") || str(v.brand, "Release");
|
|
4227
|
-
const wordless = input.text === "none";
|
|
4228
|
-
const aspect = input.footageAspect && input.footageAspect > 0 ? input.footageAspect : 16 / 9;
|
|
4229
|
-
const left = square ? 0.08 : 0.07;
|
|
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"
|
|
3671
|
+
function clientId() {
|
|
3672
|
+
const env = process.env.VOS_CLIENT?.trim();
|
|
3673
|
+
if (env && env.length <= 60 && !/[\r\n]/.test(env)) return env;
|
|
3674
|
+
return "vos-cli";
|
|
3675
|
+
}
|
|
3676
|
+
async function apiJson(origin, path, init = {}) {
|
|
3677
|
+
const headers = {
|
|
3678
|
+
accept: "application/json",
|
|
3679
|
+
...init.headers
|
|
4289
3680
|
};
|
|
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 };
|
|
3681
|
+
if (init.key) headers.authorization = `Bearer ${init.key}`;
|
|
3682
|
+
if (init.body !== void 0) headers["content-type"] = "application/json";
|
|
3683
|
+
const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
|
|
3684
|
+
const res = await fetch(`${origin}${path}`, {
|
|
3685
|
+
method: init.method ?? "GET",
|
|
3686
|
+
headers,
|
|
3687
|
+
body: reqBody
|
|
3688
|
+
});
|
|
3689
|
+
let body = {};
|
|
3690
|
+
try {
|
|
3691
|
+
body = await res.json();
|
|
3692
|
+
} catch {
|
|
4318
3693
|
}
|
|
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 };
|
|
3694
|
+
return { status: res.status, body };
|
|
4332
3695
|
}
|
|
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;
|
|
3696
|
+
function apiError(what, r) {
|
|
3697
|
+
const detail = typeof r.body.error === "string" ? r.body.error : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
|
|
3698
|
+
const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
|
|
3699
|
+
return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
|
|
4343
3700
|
}
|
|
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]);
|
|
3701
|
+
var SYNC_STATE_NAME = "vos.json";
|
|
3702
|
+
function readJsonFile(file) {
|
|
3703
|
+
try {
|
|
3704
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
3705
|
+
} catch {
|
|
4354
3706
|
return null;
|
|
4355
3707
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
3708
|
+
}
|
|
3709
|
+
function readSyncState(dir) {
|
|
3710
|
+
const own = readJsonFile(join5(dir, SYNC_STATE_NAME));
|
|
3711
|
+
if (own && typeof own.vosId === "string") {
|
|
3712
|
+
return {
|
|
3713
|
+
vosId: own.vosId,
|
|
3714
|
+
versionId: typeof own.versionId === "string" ? own.versionId : null,
|
|
3715
|
+
...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
|
|
3716
|
+
...typeof own.title === "string" ? { title: own.title } : {},
|
|
3717
|
+
...typeof own.slug === "string" ? { slug: own.slug } : {},
|
|
3718
|
+
...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
|
|
3719
|
+
};
|
|
4360
3720
|
}
|
|
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;
|
|
3721
|
+
const push = readJsonFile(join5(dir, "push.json"));
|
|
3722
|
+
if (push && typeof push.vosId === "string") {
|
|
3723
|
+
return {
|
|
3724
|
+
vosId: push.vosId,
|
|
3725
|
+
versionId: typeof push.versionId === "string" ? push.versionId : null,
|
|
3726
|
+
...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
|
|
3727
|
+
};
|
|
4379
3728
|
}
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
i += 2 + len;
|
|
4390
|
-
}
|
|
4391
|
-
return null;
|
|
3729
|
+
const meta = readJsonFile(join5(dir, "meta.json"));
|
|
3730
|
+
if (meta && typeof meta.id === "string") {
|
|
3731
|
+
return {
|
|
3732
|
+
vosId: meta.id,
|
|
3733
|
+
versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
|
|
3734
|
+
...typeof meta.title === "string" ? { title: meta.title } : {},
|
|
3735
|
+
...typeof meta.slug === "string" ? { slug: meta.slug } : {},
|
|
3736
|
+
...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
|
|
3737
|
+
};
|
|
4392
3738
|
}
|
|
4393
3739
|
return null;
|
|
4394
3740
|
}
|
|
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 };
|
|
3741
|
+
function writeSyncState(dir, patch) {
|
|
3742
|
+
const merged = {
|
|
3743
|
+
...readSyncState(dir) ?? { versionId: null },
|
|
3744
|
+
...patch
|
|
4438
3745
|
};
|
|
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;
|
|
3746
|
+
writeFileSync(
|
|
3747
|
+
join5(dir, SYNC_STATE_NAME),
|
|
3748
|
+
`${JSON.stringify(merged, null, 2)}
|
|
3749
|
+
`
|
|
3750
|
+
);
|
|
3751
|
+
return merged;
|
|
4472
3752
|
}
|
|
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
|
-
}
|
|
3753
|
+
function formatChanges(changes) {
|
|
3754
|
+
const lines = [];
|
|
3755
|
+
for (const ch of changes) {
|
|
3756
|
+
const label = ch.label ? ` \xB7 ${ch.label}` : "";
|
|
3757
|
+
const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
|
|
3758
|
+
lines.push(
|
|
3759
|
+
`v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
|
|
3760
|
+
);
|
|
3761
|
+
if (ch.note) lines.push(` note: ${ch.note}`);
|
|
4507
3762
|
}
|
|
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
|
-
}
|
|
3763
|
+
return lines;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
// src/plugin/posterDoc.ts
|
|
3767
|
+
var POSTER_CLASSES = [
|
|
3768
|
+
"landscape",
|
|
3769
|
+
"square",
|
|
3770
|
+
"portrait",
|
|
3771
|
+
"tile"
|
|
3772
|
+
];
|
|
3773
|
+
var TILE_MAX_PX = 700;
|
|
3774
|
+
function posterClassFor(px) {
|
|
3775
|
+
if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
|
|
3776
|
+
const r = px.w / Math.max(1, px.h);
|
|
3777
|
+
if (r < 0.87) return "portrait";
|
|
3778
|
+
if (r <= 1.15) return "square";
|
|
3779
|
+
return "landscape";
|
|
3780
|
+
}
|
|
3781
|
+
var ROLE_ALL = "poster";
|
|
3782
|
+
var roleFor = (cls) => `poster-${cls}`;
|
|
3783
|
+
async function readPosterAt(cls, path, takeDir, from) {
|
|
3784
|
+
let file = path;
|
|
3785
|
+
let docDir = path;
|
|
3786
|
+
if (existsSync5(join6(path, "doc.json"))) {
|
|
3787
|
+
file = join6(path, "doc.json");
|
|
3788
|
+
} else if (!/\.json$/i.test(path) || !existsSync5(path)) {
|
|
3789
|
+
return null;
|
|
3790
|
+
} else {
|
|
3791
|
+
docDir = resolve3(path, "..");
|
|
4539
3792
|
}
|
|
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
|
-
|
|
3793
|
+
let raw;
|
|
3794
|
+
try {
|
|
3795
|
+
raw = JSON.parse(await readFile5(file, "utf8"));
|
|
3796
|
+
} catch {
|
|
3797
|
+
return null;
|
|
3798
|
+
}
|
|
3799
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3800
|
+
const source = raw.source;
|
|
3801
|
+
if (!source || typeof source.videoKey !== "string") return null;
|
|
3802
|
+
const doc = migrateHostedDoc(
|
|
3803
|
+
raw
|
|
3804
|
+
);
|
|
3805
|
+
const ownFootage = existsSync5(join6(docDir, "meta.json")) && existsSync5(join6(docDir, source.videoKey));
|
|
3806
|
+
const sync = readSyncState(docDir);
|
|
3807
|
+
return {
|
|
3808
|
+
cls,
|
|
3809
|
+
file,
|
|
3810
|
+
takeDir: ownFootage ? docDir : takeDir,
|
|
3811
|
+
ownFootage,
|
|
3812
|
+
doc,
|
|
3813
|
+
...sync?.vosId ? { vosId: sync.vosId } : {},
|
|
3814
|
+
from
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
async function findPosterDocs(takeDir, launchRoles) {
|
|
3818
|
+
const out = {};
|
|
3819
|
+
const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
|
|
3820
|
+
for (const cls of POSTER_CLASSES) {
|
|
3821
|
+
const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
|
|
3822
|
+
const candidates = [];
|
|
3823
|
+
if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
|
|
3824
|
+
const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
|
|
3825
|
+
candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
|
|
3826
|
+
}
|
|
3827
|
+
candidates.push({
|
|
3828
|
+
path: join6(takeDir, "poster", cls),
|
|
3829
|
+
from: `poster/${cls}`
|
|
3830
|
+
});
|
|
3831
|
+
candidates.push({ path: join6(takeDir, "poster"), from: "poster" });
|
|
3832
|
+
for (const c of candidates) {
|
|
3833
|
+
const ref = await readPosterAt(cls, c.path, takeDir, c.from);
|
|
3834
|
+
if (ref) {
|
|
3835
|
+
out[cls] = ref;
|
|
3836
|
+
break;
|
|
4583
3837
|
}
|
|
4584
|
-
if (times.length) notes.push(`${times.length} click sound(s)`);
|
|
4585
3838
|
}
|
|
4586
|
-
if (clips.length) set.push(`audio=${JSON.stringify(clips)}`);
|
|
4587
3839
|
}
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
3840
|
+
return out;
|
|
3841
|
+
}
|
|
3842
|
+
function posterStillTime(doc, duration) {
|
|
3843
|
+
const rest = docRestTime(doc);
|
|
3844
|
+
if (rest != null) return rest;
|
|
3845
|
+
return Math.max(0, duration - 1 / 30);
|
|
3846
|
+
}
|
|
3847
|
+
var DESIGN_H = 1080;
|
|
3848
|
+
function designFrame(px) {
|
|
3849
|
+
return {
|
|
3850
|
+
W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
|
|
3851
|
+
H: DESIGN_H
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function posterShotRect(doc, px) {
|
|
3855
|
+
const { W, H } = designFrame(px);
|
|
3856
|
+
const meta = doc.source.meta;
|
|
3857
|
+
const layout = computeCardLayout(
|
|
3858
|
+
doc.frame,
|
|
3859
|
+
{
|
|
3860
|
+
width: meta.captureWidth ?? meta.width,
|
|
3861
|
+
height: meta.captureHeight ?? meta.height
|
|
3862
|
+
},
|
|
3863
|
+
W,
|
|
3864
|
+
H
|
|
3865
|
+
);
|
|
3866
|
+
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3867
|
+
return {
|
|
3868
|
+
x: r3(layout.cardX / W),
|
|
3869
|
+
y: r3(layout.cardY / H),
|
|
3870
|
+
w: r3(layout.cardW / W),
|
|
3871
|
+
h: r3(layout.cardH / H)
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
function estimateWidth(text, font, letterSpacingPx = 0) {
|
|
3875
|
+
const m = /(\d+(?:\.\d+)?)px/.exec(font);
|
|
3876
|
+
const px = m ? Number(m[1]) : 32;
|
|
3877
|
+
return text.length * (px * 0.55 + letterSpacingPx);
|
|
3878
|
+
}
|
|
3879
|
+
function posterTextBoxes(doc, px, time) {
|
|
3880
|
+
const { W, H } = designFrame(px);
|
|
3881
|
+
const out = [];
|
|
3882
|
+
for (const clip of doc.overlays ?? []) {
|
|
3883
|
+
if (clip.kind !== "text") continue;
|
|
3884
|
+
if (time < clip.start || time > clip.start + clip.duration) continue;
|
|
3885
|
+
const rect = overlayRect(clip, estimateWidth, W, H);
|
|
3886
|
+
const style = resolveOverlayStyle(clip);
|
|
3887
|
+
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3888
|
+
out.push({
|
|
3889
|
+
x: r3((rect.cx - rect.w / 2) / W),
|
|
3890
|
+
y: r3((rect.cy - rect.h / 2) / H),
|
|
3891
|
+
w: r3(rect.w / W),
|
|
3892
|
+
h: r3(rect.h / H),
|
|
3893
|
+
role: clip.preset === "title" ? "headline" : "body",
|
|
3894
|
+
label: clip.text.split("\n")[0].slice(0, 40),
|
|
3895
|
+
.../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
|
|
3896
|
+
});
|
|
4593
3897
|
}
|
|
4594
|
-
return
|
|
3898
|
+
return out;
|
|
4595
3899
|
}
|
|
4596
3900
|
|
|
4597
3901
|
// src/plugin/deliver.ts
|
|
@@ -4622,7 +3926,7 @@ function resolveChannels(raw) {
|
|
|
4622
3926
|
return out;
|
|
4623
3927
|
}
|
|
4624
3928
|
async function readBrandBesideTake(dir, explicit) {
|
|
4625
|
-
const candidates = explicit ? [explicit] : [
|
|
3929
|
+
const candidates = explicit ? [explicit] : [join7(dir, "BRAND.md"), join7(dir, "..", "BRAND.md")];
|
|
4626
3930
|
for (const file of candidates) {
|
|
4627
3931
|
if (!existsSync6(file)) continue;
|
|
4628
3932
|
const roles = parseFrontmatter(await readFile6(file, "utf8"));
|
|
@@ -4631,7 +3935,7 @@ async function readBrandBesideTake(dir, explicit) {
|
|
|
4631
3935
|
return null;
|
|
4632
3936
|
}
|
|
4633
3937
|
async function readLaunchBesideTake(dir, explicit) {
|
|
4634
|
-
const candidates = explicit ? [explicit] : [
|
|
3938
|
+
const candidates = explicit ? [explicit] : [join7(dir, "LAUNCH.md"), join7(dir, "..", "LAUNCH.md")];
|
|
4635
3939
|
for (const file of candidates) {
|
|
4636
3940
|
if (!existsSync6(file)) continue;
|
|
4637
3941
|
return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
|
|
@@ -4659,28 +3963,6 @@ async function resolveLook(dir, opts) {
|
|
|
4659
3963
|
roles
|
|
4660
3964
|
};
|
|
4661
3965
|
}
|
|
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
3966
|
function lookOverrides(look, placement, size, video, opts) {
|
|
4685
3967
|
const inset = cardInset(look, size, video, placement);
|
|
4686
3968
|
const set = [
|
|
@@ -4708,23 +3990,6 @@ function lookOverrides(look, placement, size, video, opts) {
|
|
|
4708
3990
|
}
|
|
4709
3991
|
return set;
|
|
4710
3992
|
}
|
|
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
3993
|
var PROBE_WIDTH = 640;
|
|
4729
3994
|
async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
4730
3995
|
const { candidates, dropped } = momentCandidates(doc, duration);
|
|
@@ -4737,7 +4002,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4737
4002
|
const meta = doc.source.meta;
|
|
4738
4003
|
const vw = meta.captureWidth ?? meta.width;
|
|
4739
4004
|
const vh = meta.captureHeight ?? meta.height;
|
|
4740
|
-
const probeDir = await mkdtemp(
|
|
4005
|
+
const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
|
|
4741
4006
|
try {
|
|
4742
4007
|
const probe = await framesTake(browser, dir, {
|
|
4743
4008
|
times: candidates.map((c) => c.time),
|
|
@@ -4822,16 +4087,10 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4822
4087
|
const take = await loadTake(dir);
|
|
4823
4088
|
if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
|
|
4824
4089
|
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
4090
|
const duration = totalDuration(ratedSegments5(doc));
|
|
4832
4091
|
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
|
|
4092
|
+
const outDir = resolve4(opts.outDir ?? join7(dir, "kit"));
|
|
4093
|
+
await mkdir4(outDir, { recursive: true });
|
|
4835
4094
|
const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
|
|
4836
4095
|
const assets = [];
|
|
4837
4096
|
const skipped = [];
|
|
@@ -4855,7 +4114,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4855
4114
|
w: meta0.captureWidth ?? meta0.width,
|
|
4856
4115
|
h: meta0.captureHeight ?? meta0.height
|
|
4857
4116
|
};
|
|
4858
|
-
const videoOverrides = (d
|
|
4117
|
+
const videoOverrides = (d) => {
|
|
4859
4118
|
const set = [];
|
|
4860
4119
|
if (opts.look) {
|
|
4861
4120
|
set.push(
|
|
@@ -4865,203 +4124,82 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4865
4124
|
})
|
|
4866
4125
|
);
|
|
4867
4126
|
}
|
|
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}`);
|
|
4127
|
+
const mech = destinationMechanics(d, doc);
|
|
4128
|
+
set.push(...mech.set);
|
|
4129
|
+
if (mech.notes.length) opts.onPhase?.(`${d.id}: ${mech.notes.join(", ")}`);
|
|
4882
4130
|
set.push(...opts.overrides?.set ?? []);
|
|
4883
|
-
if (!set.length) return opts.overrides;
|
|
4884
|
-
return { ...opts.overrides, set };
|
|
4131
|
+
if (!set.length && !mech.unset.length) return opts.overrides;
|
|
4132
|
+
return { ...opts.overrides, set, unset: mech.unset };
|
|
4885
4133
|
};
|
|
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;
|
|
4134
|
+
const posters = await findPosterDocs(dir, opts.launchRoles);
|
|
4135
|
+
const posterCardIds = /* @__PURE__ */ new Set();
|
|
4136
|
+
const missing = /* @__PURE__ */ new Set();
|
|
4137
|
+
for (const d of destinations) {
|
|
4138
|
+
if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
|
|
4139
|
+
continue;
|
|
4140
|
+
const cls = posterClassFor(d.px);
|
|
4141
|
+
const ref = posters[cls];
|
|
4142
|
+
if (!ref) {
|
|
4143
|
+
missing.add(cls);
|
|
4144
|
+
continue;
|
|
4901
4145
|
}
|
|
4146
|
+
posterCardIds.add(d.id);
|
|
4147
|
+
const label = `${d.channel} ${d.asset}`;
|
|
4148
|
+
const posterDuration = totalDuration(ratedSegments5(ref.doc));
|
|
4149
|
+
const time = posterStillTime(ref.doc, posterDuration);
|
|
4902
4150
|
opts.onPhase?.(
|
|
4903
|
-
`
|
|
4151
|
+
`${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
|
|
4904
4152
|
);
|
|
4905
|
-
const
|
|
4153
|
+
const shotDir = await mkdtemp(join7(tmpdir(), "vos-poster-"));
|
|
4906
4154
|
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
|
|
4155
|
+
const captured = await framesTake(browser, ref.takeDir, {
|
|
4156
|
+
times: [time],
|
|
4157
|
+
width: d.px.w,
|
|
4158
|
+
height: d.px.h,
|
|
4159
|
+
outDir: shotDir,
|
|
4160
|
+
doc: ref.doc,
|
|
4161
|
+
overrides: opts.overrides
|
|
4934
4162
|
});
|
|
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
|
|
4163
|
+
const to = join7(outDir, `${d.id}.png`);
|
|
4164
|
+
await rename4(captured.frames[0].file, to);
|
|
4165
|
+
const bytes = (await stat(to)).size;
|
|
4166
|
+
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4167
|
+
skipped.push(
|
|
4168
|
+
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
5034
4169
|
);
|
|
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
|
-
});
|
|
4170
|
+
continue;
|
|
5060
4171
|
}
|
|
4172
|
+
const shot = posterShotRect(ref.doc, d.px);
|
|
4173
|
+
assets.push({
|
|
4174
|
+
channel: d.channel,
|
|
4175
|
+
asset: d.asset,
|
|
4176
|
+
destination: d.id,
|
|
4177
|
+
path: relative(outDir, to),
|
|
4178
|
+
w: d.px.w,
|
|
4179
|
+
h: d.px.h,
|
|
4180
|
+
bytes,
|
|
4181
|
+
seconds: null,
|
|
4182
|
+
frameTime: time,
|
|
4183
|
+
source: "poster",
|
|
4184
|
+
poster: {
|
|
4185
|
+
class: cls,
|
|
4186
|
+
file: relative(dir, ref.file),
|
|
4187
|
+
...ref.vosId ? { vosId: ref.vosId } : {}
|
|
4188
|
+
},
|
|
4189
|
+
text: posterTextBoxes(ref.doc, d.px, time),
|
|
4190
|
+
shot,
|
|
4191
|
+
...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
|
|
4192
|
+
});
|
|
5061
4193
|
} finally {
|
|
5062
|
-
await rm3(
|
|
4194
|
+
await rm3(shotDir, { recursive: true, force: true });
|
|
5063
4195
|
}
|
|
5064
4196
|
}
|
|
4197
|
+
if (missing.size) {
|
|
4198
|
+
const classes = [...missing].sort().join(", ");
|
|
4199
|
+
skipped.push(
|
|
4200
|
+
`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`
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
5065
4203
|
for (const d of destinations) {
|
|
5066
4204
|
const label = `${d.channel} ${d.asset}`;
|
|
5067
4205
|
const excuse = NOT_FROM_FOOTAGE[d.id];
|
|
@@ -5094,7 +4232,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5094
4232
|
}
|
|
5095
4233
|
}
|
|
5096
4234
|
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
5097
|
-
const outFile =
|
|
4235
|
+
const outFile = join7(outDir, `${d.id}.${d.format}`);
|
|
5098
4236
|
const bitrate = d.maxBytes !== void 0 ? Math.min(
|
|
5099
4237
|
1e7,
|
|
5100
4238
|
Math.floor(d.maxBytes * 8 / seconds * 0.85)
|
|
@@ -5106,7 +4244,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5106
4244
|
parallel: opts.parallel,
|
|
5107
4245
|
range,
|
|
5108
4246
|
bitrate,
|
|
5109
|
-
overrides: videoOverrides(d
|
|
4247
|
+
overrides: videoOverrides(d),
|
|
5110
4248
|
onProgress: opts.onProgress
|
|
5111
4249
|
});
|
|
5112
4250
|
if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
|
|
@@ -5141,7 +4279,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5141
4279
|
for (let i = 0; i < captured.frames.length; i++) {
|
|
5142
4280
|
const frame = captured.frames[i];
|
|
5143
4281
|
const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
|
|
5144
|
-
const to =
|
|
4282
|
+
const to = join7(outDir, name);
|
|
5145
4283
|
await rename4(frame.file, to);
|
|
5146
4284
|
const bytes = (await stat(to)).size;
|
|
5147
4285
|
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
@@ -5163,25 +4301,135 @@ async function deliverTake(browser, dir, opts) {
|
|
|
5163
4301
|
...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
|
|
5164
4302
|
});
|
|
5165
4303
|
}
|
|
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`
|
|
4304
|
+
if (d.count && captured.frames.length < d.count.min) {
|
|
4305
|
+
skipped.push(
|
|
4306
|
+
`${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
|
|
4307
|
+
);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
const kit = {
|
|
4311
|
+
release: opts.release ?? null,
|
|
4312
|
+
look: opts.look?.kind ?? null,
|
|
4313
|
+
take: dir,
|
|
4314
|
+
produced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4315
|
+
specsVerified: CHANNEL_SPECS_VERIFIED,
|
|
4316
|
+
moments,
|
|
4317
|
+
skipped,
|
|
4318
|
+
assets
|
|
4319
|
+
};
|
|
4320
|
+
const kitFile = join7(outDir, "kit.json");
|
|
4321
|
+
await writeFile6(kitFile, JSON.stringify(kit, null, 2));
|
|
4322
|
+
return { kit, kitFile, outDir };
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
// src/plugin/markAsset.ts
|
|
4326
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
|
|
4327
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4328
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve5 } from "path";
|
|
4329
|
+
function markExtension(url) {
|
|
4330
|
+
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4331
|
+
if (!m) return null;
|
|
4332
|
+
const e = m[1].toLowerCase();
|
|
4333
|
+
return e === "jpeg" ? "jpg" : e;
|
|
4334
|
+
}
|
|
4335
|
+
function imageAspect(bytes, ext) {
|
|
4336
|
+
if (ext === "svg") {
|
|
4337
|
+
const head = new TextDecoder().decode(bytes.subarray(0, 4096));
|
|
4338
|
+
const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(
|
|
4339
|
+
head
|
|
4340
|
+
);
|
|
4341
|
+
if (vb) return ratio(+vb[1], +vb[2]);
|
|
4342
|
+
const w = /<svg[^>]*\swidth\s*=\s*["']([\d.]+)/i.exec(head);
|
|
4343
|
+
const h = /<svg[^>]*\sheight\s*=\s*["']([\d.]+)/i.exec(head);
|
|
4344
|
+
if (w && h) return ratio(+w[1], +h[1]);
|
|
4345
|
+
return null;
|
|
4346
|
+
}
|
|
4347
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4348
|
+
if (ext === "png") {
|
|
4349
|
+
if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
|
|
4350
|
+
return ratio(dv.getUint32(16), dv.getUint32(20));
|
|
4351
|
+
}
|
|
4352
|
+
if (ext === "webp") {
|
|
4353
|
+
if (bytes.length < 30) return null;
|
|
4354
|
+
const tag = String.fromCharCode(...bytes.subarray(12, 16));
|
|
4355
|
+
if (tag === "VP8X")
|
|
4356
|
+
return ratio(
|
|
4357
|
+
1 + (dv.getUint32(24, true) & 16777215),
|
|
4358
|
+
1 + (dv.getUint32(27, true) & 16777215)
|
|
4359
|
+
);
|
|
4360
|
+
if (tag === "VP8L") {
|
|
4361
|
+
const b = dv.getUint32(21, true);
|
|
4362
|
+
return ratio(1 + (b & 16383), 1 + (b >> 14 & 16383));
|
|
4363
|
+
}
|
|
4364
|
+
if (tag === "VP8 ")
|
|
4365
|
+
return ratio(
|
|
4366
|
+
dv.getUint16(26, true) & 16383,
|
|
4367
|
+
dv.getUint16(28, true) & 16383
|
|
4368
|
+
);
|
|
4369
|
+
return null;
|
|
4370
|
+
}
|
|
4371
|
+
if (ext === "jpg") {
|
|
4372
|
+
let i = 2;
|
|
4373
|
+
while (i + 9 < bytes.length) {
|
|
4374
|
+
if (bytes[i] !== 255) return null;
|
|
4375
|
+
const marker = bytes[i + 1];
|
|
4376
|
+
const len = dv.getUint16(i + 2);
|
|
4377
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
4378
|
+
return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
|
|
4379
|
+
}
|
|
4380
|
+
i += 2 + len;
|
|
4381
|
+
}
|
|
4382
|
+
return null;
|
|
4383
|
+
}
|
|
4384
|
+
return null;
|
|
4385
|
+
}
|
|
4386
|
+
function ratio(w, h) {
|
|
4387
|
+
return w > 0 && h > 0 ? w / h : null;
|
|
4388
|
+
}
|
|
4389
|
+
async function fetchBrandMarks(takeDir, roles) {
|
|
4390
|
+
const out = { light: null, dark: null, notes: [] };
|
|
4391
|
+
if (!roles) return out;
|
|
4392
|
+
const one = async (role, name) => {
|
|
4393
|
+
const src = (roles[role] ?? "").trim();
|
|
4394
|
+
if (!src) return null;
|
|
4395
|
+
const ext = markExtension(src);
|
|
4396
|
+
if (!ext) {
|
|
4397
|
+
out.notes.push(
|
|
4398
|
+
`${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
|
|
4399
|
+
);
|
|
4400
|
+
return null;
|
|
4401
|
+
}
|
|
4402
|
+
let bytes;
|
|
4403
|
+
try {
|
|
4404
|
+
if (/^https?:/i.test(src)) {
|
|
4405
|
+
const res = await fetch(src);
|
|
4406
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
4407
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
4408
|
+
} else {
|
|
4409
|
+
const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
|
|
4410
|
+
if (!existsSync7(file)) throw new Error("no such file");
|
|
4411
|
+
bytes = new Uint8Array(await readFile7(file));
|
|
4412
|
+
}
|
|
4413
|
+
} catch (e) {
|
|
4414
|
+
out.notes.push(
|
|
4415
|
+
`${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
|
|
4416
|
+
);
|
|
4417
|
+
return null;
|
|
4418
|
+
}
|
|
4419
|
+
const aspect = imageAspect(bytes, ext);
|
|
4420
|
+
if (!aspect) {
|
|
4421
|
+
out.notes.push(
|
|
4422
|
+
`${role}: ${src} has no readable size; the wordmark stands in`
|
|
5169
4423
|
);
|
|
4424
|
+
return null;
|
|
5170
4425
|
}
|
|
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
|
|
4426
|
+
await mkdir5(join8(takeDir, "brand"), { recursive: true });
|
|
4427
|
+
await writeFile7(join8(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
4428
|
+
return { key: `/brand/${name}.${ext}`, aspect };
|
|
5181
4429
|
};
|
|
5182
|
-
|
|
5183
|
-
await
|
|
5184
|
-
return
|
|
4430
|
+
out.light = await one("logoUrl", "mark");
|
|
4431
|
+
out.dark = await one("logoOnDarkUrl", "mark-on-dark");
|
|
4432
|
+
return out;
|
|
5185
4433
|
}
|
|
5186
4434
|
|
|
5187
4435
|
// src/plugin/music.ts
|
|
@@ -5197,10 +4445,69 @@ async function fetchMusicCatalog(origin) {
|
|
|
5197
4445
|
}
|
|
5198
4446
|
|
|
5199
4447
|
// src/plugin/judge.ts
|
|
5200
|
-
import { existsSync as
|
|
5201
|
-
import { mkdir as mkdir6, readFile as
|
|
5202
|
-
import { dirname, isAbsolute as
|
|
4448
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4449
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
|
|
4450
|
+
import { dirname, isAbsolute as isAbsolute3, join as join9, resolve as resolve6 } from "path";
|
|
5203
4451
|
import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
|
|
4452
|
+
|
|
4453
|
+
// src/plugin/shotBake.ts
|
|
4454
|
+
import { deflateSync } from "zlib";
|
|
4455
|
+
var crcTable = (() => {
|
|
4456
|
+
const t = new Uint32Array(256);
|
|
4457
|
+
for (let n = 0; n < 256; n++) {
|
|
4458
|
+
let c = n;
|
|
4459
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
4460
|
+
t[n] = c >>> 0;
|
|
4461
|
+
}
|
|
4462
|
+
return t;
|
|
4463
|
+
})();
|
|
4464
|
+
function crc32(buf) {
|
|
4465
|
+
let c = 4294967295;
|
|
4466
|
+
for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
|
|
4467
|
+
return (c ^ 4294967295) >>> 0;
|
|
4468
|
+
}
|
|
4469
|
+
function chunk(type, body) {
|
|
4470
|
+
const out = new Uint8Array(12 + body.length);
|
|
4471
|
+
const dv = new DataView(out.buffer);
|
|
4472
|
+
dv.setUint32(0, body.length);
|
|
4473
|
+
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
4474
|
+
out.set(body, 8);
|
|
4475
|
+
dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
|
|
4476
|
+
return out;
|
|
4477
|
+
}
|
|
4478
|
+
function encodePng(img) {
|
|
4479
|
+
const stride = img.w * 4;
|
|
4480
|
+
const raw = new Uint8Array((stride + 1) * img.h);
|
|
4481
|
+
for (let y = 0; y < img.h; y++) {
|
|
4482
|
+
raw[y * (stride + 1)] = 2;
|
|
4483
|
+
for (let i = 0; i < stride; i++) {
|
|
4484
|
+
const cur = img.data[y * stride + i];
|
|
4485
|
+
const up = y ? img.data[(y - 1) * stride + i] : 0;
|
|
4486
|
+
raw[y * (stride + 1) + 1 + i] = cur - up & 255;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
const ihdr = new Uint8Array(13);
|
|
4490
|
+
const dv = new DataView(ihdr.buffer);
|
|
4491
|
+
dv.setUint32(0, img.w);
|
|
4492
|
+
dv.setUint32(4, img.h);
|
|
4493
|
+
ihdr[8] = 8;
|
|
4494
|
+
ihdr[9] = 6;
|
|
4495
|
+
const parts = [
|
|
4496
|
+
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
4497
|
+
chunk("IHDR", ihdr),
|
|
4498
|
+
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
4499
|
+
chunk("IEND", new Uint8Array(0))
|
|
4500
|
+
];
|
|
4501
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
4502
|
+
let o = 0;
|
|
4503
|
+
for (const p of parts) {
|
|
4504
|
+
out.set(p, o);
|
|
4505
|
+
o += p.length;
|
|
4506
|
+
}
|
|
4507
|
+
return out;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
// src/plugin/judge.ts
|
|
5204
4511
|
var RUBRIC = [
|
|
5205
4512
|
"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
4513
|
"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 +4610,26 @@ function composeSheet(left, right, height = 540, gutter = 32) {
|
|
|
5303
4610
|
return { w, h, data: out };
|
|
5304
4611
|
}
|
|
5305
4612
|
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 =
|
|
4613
|
+
const kit = JSON.parse(await readFile8(kitPath, "utf8"));
|
|
4614
|
+
const manifest = JSON.parse(await readFile8(manifestPath, "utf8"));
|
|
4615
|
+
const manifestDir = dirname(resolve6(manifestPath));
|
|
4616
|
+
const kitDir = dirname(resolve6(kitPath));
|
|
4617
|
+
const out = resolve6(outDir ?? join9(kitDir, "judge"));
|
|
5311
4618
|
await mkdir6(out, { recursive: true });
|
|
5312
4619
|
const sheets = [];
|
|
5313
4620
|
const skipped = [];
|
|
5314
4621
|
const refCache = /* @__PURE__ */ new Map();
|
|
5315
4622
|
const loadRef = async (file) => {
|
|
5316
4623
|
if (refCache.has(file)) return refCache.get(file) ?? null;
|
|
5317
|
-
const p =
|
|
5318
|
-
const img =
|
|
4624
|
+
const p = join9(manifestDir, file);
|
|
4625
|
+
const img = existsSync8(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
|
|
5319
4626
|
refCache.set(file, img);
|
|
5320
4627
|
return img;
|
|
5321
4628
|
};
|
|
5322
4629
|
for (const a of kit.assets) {
|
|
5323
4630
|
if (!/\.png$/i.test(a.path)) continue;
|
|
5324
|
-
const file =
|
|
5325
|
-
const img =
|
|
4631
|
+
const file = isAbsolute3(a.path) && existsSync8(a.path) ? a.path : join9(kitDir, a.path.split("/").pop() ?? a.path);
|
|
4632
|
+
const img = existsSync8(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
|
|
5326
4633
|
if (!img) {
|
|
5327
4634
|
skipped.push(`${a.destination ?? a.path}: unreadable`);
|
|
5328
4635
|
continue;
|
|
@@ -5342,8 +4649,8 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5342
4649
|
const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
|
|
5343
4650
|
const nameA = `${id}--A.png`;
|
|
5344
4651
|
const nameB = `${id}--B.png`;
|
|
5345
|
-
await writeFile8(
|
|
5346
|
-
await writeFile8(
|
|
4652
|
+
await writeFile8(join9(out, nameA), encodePng(composeSheet(img, refImg)));
|
|
4653
|
+
await writeFile8(join9(out, nameB), encodePng(composeSheet(refImg, img)));
|
|
5347
4654
|
const rubric = [
|
|
5348
4655
|
`# ${id} against ${ref.id} (${ref.file})`,
|
|
5349
4656
|
"",
|
|
@@ -5363,11 +4670,11 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5363
4670
|
"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
4671
|
""
|
|
5365
4672
|
].join("\n");
|
|
5366
|
-
await writeFile8(
|
|
4673
|
+
await writeFile8(join9(out, `${id}.md`), rubric);
|
|
5367
4674
|
sheets.push({ asset: id, reference: ref.id, sheetA: nameA, sheetB: nameB, rubric: `${id}.md` });
|
|
5368
4675
|
}
|
|
5369
|
-
const verdictFile =
|
|
5370
|
-
if (!
|
|
4676
|
+
const verdictFile = join9(out, "judge.json");
|
|
4677
|
+
if (!existsSync8(verdictFile)) {
|
|
5371
4678
|
await writeFile8(
|
|
5372
4679
|
verdictFile,
|
|
5373
4680
|
JSON.stringify(
|
|
@@ -5394,7 +4701,7 @@ function winRate(verdicts) {
|
|
|
5394
4701
|
// src/plugin/kitPicture.ts
|
|
5395
4702
|
import { execFile } from "child_process";
|
|
5396
4703
|
import { promisify } from "util";
|
|
5397
|
-
import { readFile as
|
|
4704
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5398
4705
|
var execFileP = promisify(execFile);
|
|
5399
4706
|
var SUBJECT_BAND = { min: 0.6, max: 0.92 };
|
|
5400
4707
|
var BLANK_INK = 0.12;
|
|
@@ -5571,10 +4878,10 @@ function textFindings(a, img) {
|
|
|
5571
4878
|
});
|
|
5572
4879
|
}
|
|
5573
4880
|
}
|
|
5574
|
-
const
|
|
5575
|
-
if (
|
|
4881
|
+
const rgb = t.color ? hexToRgb(t.color) : null;
|
|
4882
|
+
if (rgb) {
|
|
5576
4883
|
const ground = medianColour(img, box);
|
|
5577
|
-
const lc = apcaContrast(
|
|
4884
|
+
const lc = apcaContrast(rgb, ground);
|
|
5578
4885
|
const floor = t.role === "body" ? 75 : 60;
|
|
5579
4886
|
if (lc < floor) {
|
|
5580
4887
|
out.push({
|
|
@@ -5625,7 +4932,7 @@ function duplicateFindings(stills) {
|
|
|
5625
4932
|
severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
|
|
5626
4933
|
asset: g.map((s) => s.destination).join(", "),
|
|
5627
4934
|
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
|
|
4935
|
+
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
4936
|
};
|
|
5630
4937
|
});
|
|
5631
4938
|
}
|
|
@@ -5670,7 +4977,7 @@ async function pictureChecks(assets) {
|
|
|
5670
4977
|
let ffmpeg = null;
|
|
5671
4978
|
for (const a of assets) {
|
|
5672
4979
|
if (/\.png$/i.test(a.file)) {
|
|
5673
|
-
const img = decodePng(new Uint8Array(await
|
|
4980
|
+
const img = decodePng(new Uint8Array(await readFile9(a.file)));
|
|
5674
4981
|
if (!img) {
|
|
5675
4982
|
findings.push({
|
|
5676
4983
|
code: "unreadable",
|
|
@@ -5763,9 +5070,9 @@ function formatFinding(f) {
|
|
|
5763
5070
|
}
|
|
5764
5071
|
|
|
5765
5072
|
// src/plugin/validateKit.ts
|
|
5766
|
-
import { existsSync as
|
|
5767
|
-
import { readFile as
|
|
5768
|
-
import { dirname as dirname2, isAbsolute as
|
|
5073
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5074
|
+
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
5075
|
+
import { dirname as dirname2, isAbsolute as isAbsolute4, join as join10 } from "path";
|
|
5769
5076
|
import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
|
|
5770
5077
|
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5771
5078
|
function pngDimensions(bytes) {
|
|
@@ -5784,7 +5091,7 @@ async function probeVideo(path) {
|
|
|
5784
5091
|
const MB = await import("mediabunny");
|
|
5785
5092
|
const input = new MB.Input({
|
|
5786
5093
|
formats: MB.ALL_FORMATS,
|
|
5787
|
-
source: new MB.BufferSource(new Uint8Array(await
|
|
5094
|
+
source: new MB.BufferSource(new Uint8Array(await readFile10(path)))
|
|
5788
5095
|
});
|
|
5789
5096
|
try {
|
|
5790
5097
|
const track = await input.getPrimaryVideoTrack();
|
|
@@ -5805,7 +5112,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5805
5112
|
const measured = [];
|
|
5806
5113
|
let kit;
|
|
5807
5114
|
try {
|
|
5808
|
-
kit = JSON.parse(await
|
|
5115
|
+
kit = JSON.parse(await readFile10(kitPath, "utf8"));
|
|
5809
5116
|
} catch (e) {
|
|
5810
5117
|
return {
|
|
5811
5118
|
valid: false,
|
|
@@ -5824,8 +5131,8 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5824
5131
|
}
|
|
5825
5132
|
const base = dirname2(kitPath);
|
|
5826
5133
|
const resolvePath = (p) => {
|
|
5827
|
-
if (
|
|
5828
|
-
const beside =
|
|
5134
|
+
if (isAbsolute4(p) && existsSync9(p)) return p;
|
|
5135
|
+
const beside = join10(base, p.split("/").pop() ?? p);
|
|
5829
5136
|
return beside;
|
|
5830
5137
|
};
|
|
5831
5138
|
const perDestination = /* @__PURE__ */ new Map();
|
|
@@ -5851,7 +5158,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5851
5158
|
let h = null;
|
|
5852
5159
|
let seconds = null;
|
|
5853
5160
|
if (/\.(png|jpe?g|webp)$/i.test(file)) {
|
|
5854
|
-
const head = Buffer.from(await
|
|
5161
|
+
const head = Buffer.from(await readFile10(file));
|
|
5855
5162
|
const kind = sniffImage(head);
|
|
5856
5163
|
if (file.toLowerCase().endsWith(".png") && kind !== "png")
|
|
5857
5164
|
problems.push(
|
|
@@ -5899,228 +5206,69 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5899
5206
|
pictureAssets.push({
|
|
5900
5207
|
destination: id,
|
|
5901
5208
|
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
|
-
};
|
|
5209
|
+
file,
|
|
5210
|
+
spec,
|
|
5211
|
+
text: a.text,
|
|
5212
|
+
seconds,
|
|
5213
|
+
composed: a.composed,
|
|
5214
|
+
shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
|
|
5215
|
+
crop: a.crop
|
|
5216
|
+
});
|
|
5217
|
+
if (!spec) {
|
|
5218
|
+
if (a.channel !== "demo")
|
|
5219
|
+
warnings.push(
|
|
5220
|
+
`${label}: no channel spec for "${id}" \u2014 not verified against one`
|
|
5221
|
+
);
|
|
5222
|
+
continue;
|
|
5223
|
+
}
|
|
5224
|
+
if (w !== null && h !== null && (w !== spec.px.w || h !== spec.px.h))
|
|
5225
|
+
problems.push(
|
|
5226
|
+
`${label}: spec wants ${spec.px.w}x${spec.px.h}, the file is ${w}x${h}`
|
|
5227
|
+
);
|
|
5228
|
+
if (spec.maxBytes !== void 0 && bytes > spec.maxBytes)
|
|
5229
|
+
problems.push(
|
|
5230
|
+
`${label}: ${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(spec.maxBytes / 1024)} KB ceiling`
|
|
5231
|
+
);
|
|
5232
|
+
if (seconds !== null) {
|
|
5233
|
+
if (spec.minSeconds !== void 0 && seconds < spec.minSeconds - 0.25)
|
|
5234
|
+
problems.push(
|
|
5235
|
+
`${label}: spec wants at least ${spec.minSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
5236
|
+
);
|
|
5237
|
+
if (spec.maxSeconds !== void 0 && seconds > spec.maxSeconds + 0.25)
|
|
5238
|
+
problems.push(
|
|
5239
|
+
`${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
5240
|
+
);
|
|
5241
|
+
}
|
|
5242
|
+
if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
|
|
5243
|
+
warnings.push(
|
|
5244
|
+
`${label}: spec renders png, the file is ${file.split(".").pop()}`
|
|
5245
|
+
);
|
|
6075
5246
|
}
|
|
6076
|
-
const
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
};
|
|
5247
|
+
for (const [id, n] of perDestination) {
|
|
5248
|
+
const spec = specById.get(id);
|
|
5249
|
+
if (spec?.count && (n < spec.count.min || n > spec.count.max))
|
|
5250
|
+
problems.push(
|
|
5251
|
+
`${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
|
|
5252
|
+
);
|
|
6083
5253
|
}
|
|
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
|
-
};
|
|
5254
|
+
if (!opts.picture) {
|
|
5255
|
+
return { valid: problems.length === 0, problems, warnings, measured };
|
|
6093
5256
|
}
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
5257
|
+
const picture = await pictureChecks(pictureAssets);
|
|
5258
|
+
const pictureErrors = picture.findings.filter((f) => f.severity === "error");
|
|
5259
|
+
return {
|
|
5260
|
+
valid: problems.length === 0 && pictureErrors.length === 0,
|
|
5261
|
+
problems,
|
|
5262
|
+
warnings,
|
|
5263
|
+
measured,
|
|
5264
|
+
picture: picture.findings,
|
|
5265
|
+
pictureMeasured: picture.measured
|
|
6100
5266
|
};
|
|
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
5267
|
}
|
|
6120
5268
|
|
|
6121
5269
|
// src/plugin/recorder.ts
|
|
6122
5270
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
6123
|
-
import { join as
|
|
5271
|
+
import { join as join11 } from "path";
|
|
6124
5272
|
|
|
6125
5273
|
// src/plugin/recordingCap.ts
|
|
6126
5274
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
@@ -6208,7 +5356,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6208
5356
|
cdp.on("Page.screencastFrame", (ev) => {
|
|
6209
5357
|
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
6210
5358
|
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
6211
|
-
writeFileSync2(
|
|
5359
|
+
writeFileSync2(join11(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
6212
5360
|
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
6213
5361
|
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
6214
5362
|
});
|
|
@@ -6431,7 +5579,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6431
5579
|
await sleep(200);
|
|
6432
5580
|
const pageTitle = await page.title().catch(() => "");
|
|
6433
5581
|
await context.close();
|
|
6434
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(
|
|
5582
|
+
const firstFrame = frames[0] ? jpegDims(readFileSync3(join11(paths.framesDir, frames[0].file))) : null;
|
|
6435
5583
|
const meta = {
|
|
6436
5584
|
dpr: 1,
|
|
6437
5585
|
zoom: 1,
|
|
@@ -6539,14 +5687,17 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6539
5687
|
}
|
|
6540
5688
|
|
|
6541
5689
|
// src/plugin/plan.ts
|
|
6542
|
-
import { existsSync as
|
|
6543
|
-
import { readFile as
|
|
6544
|
-
import { join as
|
|
5690
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5691
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5692
|
+
import { join as join12 } from "path";
|
|
6545
5693
|
import {
|
|
6546
5694
|
DEFAULT_FRAME_STYLE,
|
|
6547
5695
|
STYLE_FIELDS,
|
|
5696
|
+
copyLayout,
|
|
6548
5697
|
copyStyle,
|
|
6549
5698
|
isRejected,
|
|
5699
|
+
isStageClip,
|
|
5700
|
+
layoutOf,
|
|
6550
5701
|
planAutoSpeed,
|
|
6551
5702
|
planAutoZoom,
|
|
6552
5703
|
projectFromArtifact,
|
|
@@ -6734,15 +5885,36 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6734
5885
|
// src/plugin/plan.ts
|
|
6735
5886
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6736
5887
|
async function readDigestActivity(dir) {
|
|
6737
|
-
const file =
|
|
6738
|
-
if (!
|
|
5888
|
+
const file = join12(dir, "digest", "digest.json");
|
|
5889
|
+
if (!existsSync10(file)) return null;
|
|
6739
5890
|
try {
|
|
6740
|
-
const d = JSON.parse(await
|
|
5891
|
+
const d = JSON.parse(await readFile11(file, "utf8"));
|
|
6741
5892
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
6742
5893
|
} catch {
|
|
6743
5894
|
return null;
|
|
6744
5895
|
}
|
|
6745
5896
|
}
|
|
5897
|
+
function patchStageWords(doc, words) {
|
|
5898
|
+
const map = {
|
|
5899
|
+
"stage-title": words.headline,
|
|
5900
|
+
"stage-kicker": words.kicker,
|
|
5901
|
+
"stage-brand": words.brand
|
|
5902
|
+
};
|
|
5903
|
+
for (const clip of doc.overlays ?? []) {
|
|
5904
|
+
if (clip.kind !== "text" || !isStageClip(clip)) continue;
|
|
5905
|
+
const v = map[clip.id];
|
|
5906
|
+
if (typeof v === "string" && v.trim()) clip.text = v.trim();
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
function applyStyle(seed, doc, opts) {
|
|
5910
|
+
const parts = layoutOf(seed);
|
|
5911
|
+
const carries = parts.clips.length > 0 || parts.lean || parts.hold;
|
|
5912
|
+
if (!carries) return { doc: copyStyle(seed, doc), layout: void 0 };
|
|
5913
|
+
const keys = opts.mark ? { "stage-mark": opts.mark.key } : void 0;
|
|
5914
|
+
const { doc: next, notes } = copyLayout(seed, copyStyle(seed, doc), { keys });
|
|
5915
|
+
if (opts.words) patchStageWords(next, opts.words);
|
|
5916
|
+
return { doc: next, layout: { ...parts, notes } };
|
|
5917
|
+
}
|
|
6746
5918
|
async function planTake(dir, opts = {}) {
|
|
6747
5919
|
const take = await loadTake(dir);
|
|
6748
5920
|
const { meta, cursor } = take;
|
|
@@ -6750,6 +5922,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6750
5922
|
const ingest = opts.backdrop ? { frame: withBackdrop(DEFAULT_FRAME_STYLE, opts.backdrop) } : {};
|
|
6751
5923
|
let doc;
|
|
6752
5924
|
let fresh;
|
|
5925
|
+
let layout;
|
|
6753
5926
|
if (opts.reuse) {
|
|
6754
5927
|
const prev = opts.reuse.doc;
|
|
6755
5928
|
const artifact = {
|
|
@@ -6761,6 +5934,10 @@ async function planTake(dir, opts = {}) {
|
|
|
6761
5934
|
doc = copyStyle(prev, doc);
|
|
6762
5935
|
const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
|
|
6763
5936
|
doc.segments = rt.segments;
|
|
5937
|
+
const prevHold = prev.segments.at(-1)?.hold;
|
|
5938
|
+
const last = doc.segments.at(-1);
|
|
5939
|
+
if (typeof prevHold === "number" && prevHold > 0 && last)
|
|
5940
|
+
last.hold = prevHold;
|
|
6764
5941
|
if (rt.rejected.length) doc.rejected = rt.rejected;
|
|
6765
5942
|
const manualZoom = rt.zoom;
|
|
6766
5943
|
const autoZoom = planAutoZoom(doc.source.cursor, {
|
|
@@ -6782,6 +5959,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6782
5959
|
if (prev.objects?.length) doc.objects = prev.objects;
|
|
6783
5960
|
if (prev.audio.length) doc.audio = prev.audio;
|
|
6784
5961
|
if (prev.camMotion?.length) doc.camMotion = prev.camMotion;
|
|
5962
|
+
if (prev.endCard) doc.endCard = structuredClone(prev.endCard);
|
|
6785
5963
|
await writeJson(take.paths.doc, doc, true);
|
|
6786
5964
|
return {
|
|
6787
5965
|
doc,
|
|
@@ -6793,7 +5971,13 @@ async function planTake(dir, opts = {}) {
|
|
|
6793
5971
|
};
|
|
6794
5972
|
}
|
|
6795
5973
|
if (take.doc) {
|
|
6796
|
-
|
|
5974
|
+
if (opts.style) {
|
|
5975
|
+
const applied = applyStyle(opts.style.doc, take.doc, opts);
|
|
5976
|
+
doc = applied.doc;
|
|
5977
|
+
layout = applied.layout;
|
|
5978
|
+
} else {
|
|
5979
|
+
doc = take.doc;
|
|
5980
|
+
}
|
|
6797
5981
|
fresh = false;
|
|
6798
5982
|
const manual = doc.zoom.filter((z) => z.source === "manual");
|
|
6799
5983
|
const auto = planAutoZoom(doc.source.cursor, {
|
|
@@ -6817,7 +6001,11 @@ async function planTake(dir, opts = {}) {
|
|
|
6817
6001
|
meta
|
|
6818
6002
|
};
|
|
6819
6003
|
doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
|
|
6820
|
-
if (opts.style)
|
|
6004
|
+
if (opts.style) {
|
|
6005
|
+
const applied = applyStyle(opts.style.doc, doc, opts);
|
|
6006
|
+
doc = applied.doc;
|
|
6007
|
+
layout = applied.layout;
|
|
6008
|
+
}
|
|
6821
6009
|
doc.zoom = planAutoZoom(doc.source.cursor, {
|
|
6822
6010
|
width: doc.source.meta.width,
|
|
6823
6011
|
height: doc.source.meta.height,
|
|
@@ -6831,6 +6019,12 @@ async function planTake(dir, opts = {}) {
|
|
|
6831
6019
|
});
|
|
6832
6020
|
fresh = true;
|
|
6833
6021
|
}
|
|
6022
|
+
let motion;
|
|
6023
|
+
if (opts.motion && (fresh || opts.motion.again)) {
|
|
6024
|
+
const proposed = proposeMotion(doc, opts.motion);
|
|
6025
|
+
doc = proposed.doc;
|
|
6026
|
+
motion = { notes: proposed.notes, skipped: proposed.skipped };
|
|
6027
|
+
}
|
|
6834
6028
|
await writeJson(take.paths.doc, doc, true);
|
|
6835
6029
|
return {
|
|
6836
6030
|
doc,
|
|
@@ -6844,22 +6038,24 @@ async function planTake(dir, opts = {}) {
|
|
|
6844
6038
|
styleFields: STYLE_FIELDS.filter(
|
|
6845
6039
|
(k) => opts.style.doc[k] !== void 0
|
|
6846
6040
|
)
|
|
6847
|
-
} : {}
|
|
6041
|
+
} : {},
|
|
6042
|
+
...layout ? { layout } : {},
|
|
6043
|
+
...motion ? { motion } : {}
|
|
6848
6044
|
};
|
|
6849
6045
|
}
|
|
6850
6046
|
|
|
6851
6047
|
// src/plugin/sync.ts
|
|
6852
6048
|
import { createHash } from "crypto";
|
|
6853
|
-
import { existsSync as
|
|
6854
|
-
import { readFile as
|
|
6049
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6050
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6855
6051
|
import { createInterface } from "readline/promises";
|
|
6856
|
-
import { basename, join as
|
|
6857
|
-
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as
|
|
6052
|
+
import { basename, join as join15 } from "path";
|
|
6053
|
+
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
6858
6054
|
|
|
6859
6055
|
// src/plugin/media.ts
|
|
6860
|
-
import { createWriteStream, existsSync as
|
|
6056
|
+
import { createWriteStream, existsSync as existsSync11 } from "fs";
|
|
6861
6057
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
6862
|
-
import { join as
|
|
6058
|
+
import { join as join13 } from "path";
|
|
6863
6059
|
import { Readable } from "stream";
|
|
6864
6060
|
import { pipeline } from "stream/promises";
|
|
6865
6061
|
var MIC_NAME = "mic.webm";
|
|
@@ -6869,6 +6065,81 @@ var KEYS = [
|
|
|
6869
6065
|
{ key: "micKey", file: MIC_NAME },
|
|
6870
6066
|
{ key: "camKey", file: CAM_NAME }
|
|
6871
6067
|
];
|
|
6068
|
+
function isTakeRelativeKey(key) {
|
|
6069
|
+
return !!key && !/^(https?:|blob:|data:|\/\/)/.test(key) && !key.startsWith("/api/");
|
|
6070
|
+
}
|
|
6071
|
+
var takeRelativeFile = (key) => key.replace(/^\/+/, "");
|
|
6072
|
+
function docMediaRefs(doc, keep = isTakeRelativeKey) {
|
|
6073
|
+
const out = [];
|
|
6074
|
+
for (const clip of doc.overlays ?? []) {
|
|
6075
|
+
if (clip.kind === "text" || !keep(clip.key)) continue;
|
|
6076
|
+
out.push({
|
|
6077
|
+
where: `overlay ${clip.id}`,
|
|
6078
|
+
key: clip.key,
|
|
6079
|
+
set: (next) => {
|
|
6080
|
+
clip.key = next;
|
|
6081
|
+
}
|
|
6082
|
+
});
|
|
6083
|
+
}
|
|
6084
|
+
const mark = doc.endCard?.mark;
|
|
6085
|
+
if (mark && keep(mark.key))
|
|
6086
|
+
out.push({
|
|
6087
|
+
where: "end card mark",
|
|
6088
|
+
key: mark.key,
|
|
6089
|
+
set: (next) => {
|
|
6090
|
+
mark.key = next;
|
|
6091
|
+
}
|
|
6092
|
+
});
|
|
6093
|
+
const bg = doc.frame.backgroundMedia;
|
|
6094
|
+
if (bg && keep(bg.key))
|
|
6095
|
+
out.push({
|
|
6096
|
+
where: "background",
|
|
6097
|
+
key: bg.key,
|
|
6098
|
+
set: (next) => {
|
|
6099
|
+
bg.key = next;
|
|
6100
|
+
}
|
|
6101
|
+
});
|
|
6102
|
+
return out;
|
|
6103
|
+
}
|
|
6104
|
+
function mediaContentType(file) {
|
|
6105
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
|
|
6106
|
+
const types = {
|
|
6107
|
+
svg: "image/svg+xml",
|
|
6108
|
+
png: "image/png",
|
|
6109
|
+
jpg: "image/jpeg",
|
|
6110
|
+
jpeg: "image/jpeg",
|
|
6111
|
+
webp: "image/webp",
|
|
6112
|
+
gif: "image/gif",
|
|
6113
|
+
avif: "image/avif",
|
|
6114
|
+
webm: "video/webm",
|
|
6115
|
+
mp4: "video/mp4",
|
|
6116
|
+
mov: "video/quicktime",
|
|
6117
|
+
mp3: "audio/mpeg",
|
|
6118
|
+
wav: "audio/wav",
|
|
6119
|
+
ogg: "audio/ogg",
|
|
6120
|
+
m4a: "audio/mp4"
|
|
6121
|
+
};
|
|
6122
|
+
return types[ext] ?? "application/octet-stream";
|
|
6123
|
+
}
|
|
6124
|
+
function extensionFor(contentType) {
|
|
6125
|
+
const t = contentType.split(";")[0].trim().toLowerCase();
|
|
6126
|
+
const exts = {
|
|
6127
|
+
"image/svg+xml": ".svg",
|
|
6128
|
+
"image/png": ".png",
|
|
6129
|
+
"image/jpeg": ".jpg",
|
|
6130
|
+
"image/webp": ".webp",
|
|
6131
|
+
"image/gif": ".gif",
|
|
6132
|
+
"image/avif": ".avif",
|
|
6133
|
+
"video/webm": ".webm",
|
|
6134
|
+
"video/mp4": ".mp4",
|
|
6135
|
+
"video/quicktime": ".mov",
|
|
6136
|
+
"audio/mpeg": ".mp3",
|
|
6137
|
+
"audio/wav": ".wav",
|
|
6138
|
+
"audio/ogg": ".ogg",
|
|
6139
|
+
"audio/mp4": ".m4a"
|
|
6140
|
+
};
|
|
6141
|
+
return exts[t] ?? "";
|
|
6142
|
+
}
|
|
6872
6143
|
function assetIdOf(url) {
|
|
6873
6144
|
if (!url) return null;
|
|
6874
6145
|
const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
|
|
@@ -6880,8 +6151,8 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6880
6151
|
const url = doc.source[key];
|
|
6881
6152
|
const assetId = assetIdOf(url);
|
|
6882
6153
|
if (!assetId) continue;
|
|
6883
|
-
const target =
|
|
6884
|
-
if (
|
|
6154
|
+
const target = join13(dir, file);
|
|
6155
|
+
if (existsSync11(target)) {
|
|
6885
6156
|
result.kept.push(file);
|
|
6886
6157
|
} else {
|
|
6887
6158
|
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
@@ -6903,19 +6174,56 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6903
6174
|
}
|
|
6904
6175
|
doc.source[key] = file;
|
|
6905
6176
|
}
|
|
6906
|
-
const
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6177
|
+
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
6178
|
+
const assetId = assetIdOf(ref.key);
|
|
6179
|
+
const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
|
|
6180
|
+
const existing = existsSync11(join13(dir, "media")) ? (await import("fs/promises").then(
|
|
6181
|
+
(fs) => fs.readdir(join13(dir, "media"))
|
|
6182
|
+
)).find((f) => f.startsWith(`${assetId}.`)) : void 0;
|
|
6183
|
+
let file;
|
|
6184
|
+
if (existing) {
|
|
6185
|
+
file = `media/${existing}`;
|
|
6186
|
+
result.kept.push(file);
|
|
6187
|
+
} else {
|
|
6188
|
+
const res = await fetch(abs, {
|
|
6189
|
+
headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
|
|
6190
|
+
});
|
|
6191
|
+
if (!res.ok || !res.body) {
|
|
6192
|
+
throw new Error(
|
|
6193
|
+
`download of ${ref.where} (${ref.key}) failed (${res.status})`
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
6196
|
+
const ext = extensionFor(res.headers.get("content-type") ?? "");
|
|
6197
|
+
file = `media/${assetId}${ext}`;
|
|
6198
|
+
const target = join13(dir, file);
|
|
6199
|
+
await import("fs/promises").then(
|
|
6200
|
+
(fs) => fs.mkdir(join13(dir, "media"), { recursive: true })
|
|
6201
|
+
);
|
|
6202
|
+
await pipeline(
|
|
6203
|
+
Readable.fromWeb(res.body),
|
|
6204
|
+
createWriteStream(target)
|
|
6205
|
+
);
|
|
6206
|
+
const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
|
|
6207
|
+
result.downloaded.push({ file, assetId, bytes });
|
|
6208
|
+
log(
|
|
6209
|
+
` ${file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(bytes / 1024)} kB)`
|
|
6210
|
+
);
|
|
6211
|
+
}
|
|
6212
|
+
ref.set(file);
|
|
6213
|
+
}
|
|
6214
|
+
const metaPath = join13(dir, "meta.json");
|
|
6215
|
+
if (!existsSync11(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
6216
|
+
const cursorPath = join13(dir, "cursor.json");
|
|
6217
|
+
if (!existsSync11(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
6218
|
+
await writeFile9(join13(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
6911
6219
|
return result;
|
|
6912
6220
|
}
|
|
6913
6221
|
|
|
6914
6222
|
// src/plugin/folder.ts
|
|
6915
6223
|
import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
|
|
6916
|
-
import { join as
|
|
6224
|
+
import { join as join14 } from "path";
|
|
6917
6225
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6918
|
-
import { migrateHostedDoc } from "@vosjs/studio-core";
|
|
6226
|
+
import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
6919
6227
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
|
|
6920
6228
|
async function listFolders(origin, key) {
|
|
6921
6229
|
const r = await apiJson(origin, "/api/folders", { key });
|
|
@@ -7089,18 +6397,18 @@ async function cmdPull(argv) {
|
|
|
7089
6397
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7090
6398
|
const payload = res.body;
|
|
7091
6399
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7092
|
-
await mkdir7(
|
|
6400
|
+
await mkdir7(join14(out, "recipes"), { recursive: true });
|
|
7093
6401
|
const lines = [];
|
|
7094
6402
|
const recipes = [];
|
|
7095
6403
|
for (const rec of payload.recipes) {
|
|
7096
|
-
const path =
|
|
6404
|
+
const path = join14(out, "recipes", safeName(rec.filename));
|
|
7097
6405
|
await writeFile10(path, rec.body ?? "");
|
|
7098
6406
|
recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
|
|
7099
6407
|
}
|
|
7100
6408
|
for (const rec of payload.inheritedRecipes) {
|
|
7101
6409
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7102
|
-
await mkdir7(
|
|
7103
|
-
const path =
|
|
6410
|
+
await mkdir7(join14(out, "recipes", "_inherited", from), { recursive: true });
|
|
6411
|
+
const path = join14(
|
|
7104
6412
|
out,
|
|
7105
6413
|
"recipes",
|
|
7106
6414
|
"_inherited",
|
|
@@ -7115,7 +6423,7 @@ async function cmdPull(argv) {
|
|
|
7115
6423
|
}
|
|
7116
6424
|
const members = [];
|
|
7117
6425
|
for (const v of payload.voses) {
|
|
7118
|
-
const dir =
|
|
6426
|
+
const dir = join14(out, "members", safeName(v.slug || v.title || v.id));
|
|
7119
6427
|
await mkdir7(dir, { recursive: true });
|
|
7120
6428
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7121
6429
|
if (cfg.status !== 200) {
|
|
@@ -7123,7 +6431,7 @@ async function cmdPull(argv) {
|
|
|
7123
6431
|
continue;
|
|
7124
6432
|
}
|
|
7125
6433
|
await writeFile10(
|
|
7126
|
-
|
|
6434
|
+
join14(dir, "config.json"),
|
|
7127
6435
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7128
6436
|
);
|
|
7129
6437
|
let take = false;
|
|
@@ -7135,12 +6443,12 @@ async function cmdPull(argv) {
|
|
|
7135
6443
|
);
|
|
7136
6444
|
if (doc.status === 200) {
|
|
7137
6445
|
await writeFile10(
|
|
7138
|
-
|
|
6446
|
+
join14(dir, "doc.json"),
|
|
7139
6447
|
JSON.stringify(doc.body, null, 2)
|
|
7140
6448
|
);
|
|
7141
6449
|
take = true;
|
|
7142
6450
|
if (flags.media === true) {
|
|
7143
|
-
const hosted =
|
|
6451
|
+
const hosted = migrateHostedDoc2(doc.body);
|
|
7144
6452
|
await pullMedia({ origin, key }, dir, hosted, r.log);
|
|
7145
6453
|
}
|
|
7146
6454
|
}
|
|
@@ -7252,7 +6560,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7252
6560
|
throw new Error(`doc.json fails lint:
|
|
7253
6561
|
${lint.problems.join("\n ")}`);
|
|
7254
6562
|
}
|
|
7255
|
-
if (!
|
|
6563
|
+
if (!existsSync12(take.paths.recording)) {
|
|
7256
6564
|
throw new Error("no recording.webm in this take");
|
|
7257
6565
|
}
|
|
7258
6566
|
const ctx = apiContext(flags);
|
|
@@ -7284,7 +6592,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7284
6592
|
throw new Error("push cancelled");
|
|
7285
6593
|
}
|
|
7286
6594
|
}
|
|
7287
|
-
const bytes = await
|
|
6595
|
+
const bytes = await readFile12(take.paths.recording);
|
|
7288
6596
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7289
6597
|
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7290
6598
|
const upload = await api(ctx, "/assets/recording", {
|
|
@@ -7322,6 +6630,35 @@ async function pushTake(dir, flags, r) {
|
|
|
7322
6630
|
r.log(" mic track is local-only \u2014 dropped from the push");
|
|
7323
6631
|
delete docForPush.source.micKey;
|
|
7324
6632
|
}
|
|
6633
|
+
for (const ref of docMediaRefs(docForPush)) {
|
|
6634
|
+
const file = join15(dir, takeRelativeFile(ref.key));
|
|
6635
|
+
if (!existsSync12(file)) {
|
|
6636
|
+
r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
|
|
6637
|
+
continue;
|
|
6638
|
+
}
|
|
6639
|
+
const media = await readFile12(file);
|
|
6640
|
+
const mediaHash = createHash("sha256").update(media).digest("hex");
|
|
6641
|
+
const type = mediaContentType(file);
|
|
6642
|
+
const put = await api(ctx, "/assets/recording", {
|
|
6643
|
+
method: "POST",
|
|
6644
|
+
headers: {
|
|
6645
|
+
"Content-Type": type,
|
|
6646
|
+
"Content-Length": String(media.length),
|
|
6647
|
+
"X-Filename": basename(file),
|
|
6648
|
+
"X-Content-Hash": mediaHash
|
|
6649
|
+
},
|
|
6650
|
+
raw: new Uint8Array(media)
|
|
6651
|
+
});
|
|
6652
|
+
if (put.status !== 201 && put.status !== 200) {
|
|
6653
|
+
throw new Error(
|
|
6654
|
+
`${ref.where} upload failed (${put.status}): ${String(put.json.error ?? "")}`
|
|
6655
|
+
);
|
|
6656
|
+
}
|
|
6657
|
+
ref.set(String(put.json.url));
|
|
6658
|
+
r.log(
|
|
6659
|
+
` ${ref.where}: ${ref.key} \u2192 asset ${String(put.json.id)}${put.json.reused === true ? " (reused)" : ""}`
|
|
6660
|
+
);
|
|
6661
|
+
}
|
|
7325
6662
|
const lowered = lowerToComposition3(docForPush);
|
|
7326
6663
|
const config = { ...lowered.config, data: lowered.data };
|
|
7327
6664
|
if (!state) {
|
|
@@ -7435,9 +6772,9 @@ async function pullTake(dir, flags, r) {
|
|
|
7435
6772
|
};
|
|
7436
6773
|
}
|
|
7437
6774
|
let media2;
|
|
7438
|
-
if (flags.media &&
|
|
6775
|
+
if (flags.media && existsSync12(join15(dir, "doc.json"))) {
|
|
7439
6776
|
const local = JSON.parse(
|
|
7440
|
-
await
|
|
6777
|
+
await readFile12(join15(dir, "doc.json"), "utf8")
|
|
7441
6778
|
);
|
|
7442
6779
|
media2 = await pullMedia(ctx, dir, local, r.log);
|
|
7443
6780
|
}
|
|
@@ -7495,12 +6832,12 @@ async function pullTake(dir, flags, r) {
|
|
|
7495
6832
|
docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
|
|
7496
6833
|
);
|
|
7497
6834
|
}
|
|
7498
|
-
const hostedDoc =
|
|
6835
|
+
const hostedDoc = migrateHostedDoc3(docRes.json);
|
|
7499
6836
|
const doc = hostedDoc;
|
|
7500
|
-
if (
|
|
6837
|
+
if (existsSync12(join15(dir, RECORDING_NAME))) {
|
|
7501
6838
|
doc.source.videoKey = RECORDING_NAME;
|
|
7502
6839
|
}
|
|
7503
|
-
await writeJson(
|
|
6840
|
+
await writeJson(join15(dir, "doc.json"), doc, true);
|
|
7504
6841
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
7505
6842
|
const versions = await api(ctx, `/vos/${vosId}/versions`);
|
|
7506
6843
|
const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
|
|
@@ -7519,16 +6856,16 @@ async function pullTake(dir, flags, r) {
|
|
|
7519
6856
|
}
|
|
7520
6857
|
|
|
7521
6858
|
// src/plugin/program.ts
|
|
7522
|
-
import { existsSync as
|
|
7523
|
-
import { mkdir as mkdir8, readFile as
|
|
6859
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6860
|
+
import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
|
|
7524
6861
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7525
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
6862
|
+
import { basename as basename2, dirname as dirname3, join as join16 } from "path";
|
|
7526
6863
|
import {
|
|
7527
6864
|
CURRENT_CONFIG_VERSION,
|
|
7528
6865
|
migrateConfig,
|
|
7529
6866
|
vosConfigJsonSchema
|
|
7530
6867
|
} from "@vosjs/core";
|
|
7531
|
-
import { migrateHostedDoc as
|
|
6868
|
+
import { migrateHostedDoc as migrateHostedDoc4 } from "@vosjs/studio-core";
|
|
7532
6869
|
|
|
7533
6870
|
// src/plugin/login.ts
|
|
7534
6871
|
import { hostname } from "os";
|
|
@@ -7692,8 +7029,8 @@ function preflightConfig(parsed) {
|
|
|
7692
7029
|
}
|
|
7693
7030
|
function resolveConfigPath(target) {
|
|
7694
7031
|
if (target.endsWith(".json")) return target;
|
|
7695
|
-
const inDir =
|
|
7696
|
-
if (
|
|
7032
|
+
const inDir = join16(target, "config.json");
|
|
7033
|
+
if (existsSync13(inDir)) return inDir;
|
|
7697
7034
|
throw new UsageError(
|
|
7698
7035
|
`${target} has no config.json (and is not a take \u2014 no doc.json)`
|
|
7699
7036
|
);
|
|
@@ -7721,7 +7058,7 @@ async function cmdFetch(argv) {
|
|
|
7721
7058
|
const out = strFlag(flags, "out") ?? slug;
|
|
7722
7059
|
await mkdir8(out, { recursive: true });
|
|
7723
7060
|
await writeFile11(
|
|
7724
|
-
|
|
7061
|
+
join16(out, "config.json"),
|
|
7725
7062
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7726
7063
|
);
|
|
7727
7064
|
const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
|
|
@@ -7743,15 +7080,15 @@ async function cmdFetch(argv) {
|
|
|
7743
7080
|
}
|
|
7744
7081
|
);
|
|
7745
7082
|
if (doc.status === 200 && !("source" in doc.body)) {
|
|
7746
|
-
const hosted =
|
|
7083
|
+
const hosted = migrateHostedDoc4(doc.body);
|
|
7747
7084
|
const { config: own } = await writeProgramDoc(out, hosted);
|
|
7748
7085
|
if (own) {
|
|
7749
|
-
await writeFile11(
|
|
7086
|
+
await writeFile11(join16(out, "config.json"), JSON.stringify(own, null, 2));
|
|
7750
7087
|
}
|
|
7751
7088
|
} else if (doc.status === 200) {
|
|
7752
7089
|
take = true;
|
|
7753
|
-
const hosted =
|
|
7754
|
-
await writeFile11(
|
|
7090
|
+
const hosted = migrateHostedDoc4(doc.body);
|
|
7091
|
+
await writeFile11(join16(out, "doc.json"), JSON.stringify(hosted, null, 2));
|
|
7755
7092
|
if (flags.media === true) {
|
|
7756
7093
|
const media = await pullMedia({ origin, key }, out, hosted, r.log);
|
|
7757
7094
|
mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
|
|
@@ -7863,7 +7200,7 @@ async function cmdPushProgram(argv) {
|
|
|
7863
7200
|
api: strFlag(flags, "api")
|
|
7864
7201
|
});
|
|
7865
7202
|
const dir = dirname3(source);
|
|
7866
|
-
const parsed = JSON.parse(await
|
|
7203
|
+
const parsed = JSON.parse(await readFile13(source, "utf8"));
|
|
7867
7204
|
const pre = preflightConfig(parsed);
|
|
7868
7205
|
if (!pre.ok || !pre.config) {
|
|
7869
7206
|
for (const issue of pre.issues) r.log(`error ${issue}`);
|
|
@@ -8096,10 +7433,10 @@ async function cmdPullProgram(argv) {
|
|
|
8096
7433
|
const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
|
|
8097
7434
|
if (cfg.status !== 200)
|
|
8098
7435
|
throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
|
|
8099
|
-
const configPath =
|
|
7436
|
+
const configPath = join16(dir, "config.json");
|
|
8100
7437
|
let backedUp = false;
|
|
8101
|
-
if (
|
|
8102
|
-
await writeFile11(
|
|
7438
|
+
if (existsSync13(configPath)) {
|
|
7439
|
+
await writeFile11(join16(dir, "config.backup.json"), await readFile13(configPath));
|
|
8103
7440
|
backedUp = true;
|
|
8104
7441
|
}
|
|
8105
7442
|
let headConfig = cfg.body.config;
|
|
@@ -8113,7 +7450,7 @@ async function cmdPullProgram(argv) {
|
|
|
8113
7450
|
if (docRes.status === 200 && !("source" in docRes.body)) {
|
|
8114
7451
|
const { config: own } = await writeProgramDoc(
|
|
8115
7452
|
dir,
|
|
8116
|
-
|
|
7453
|
+
migrateHostedDoc4(docRes.body)
|
|
8117
7454
|
);
|
|
8118
7455
|
if (own) headConfig = own;
|
|
8119
7456
|
}
|
|
@@ -8132,7 +7469,7 @@ async function cmdPullProgram(argv) {
|
|
|
8132
7469
|
protected: protectedIds,
|
|
8133
7470
|
changes,
|
|
8134
7471
|
out: configPath,
|
|
8135
|
-
backup: backedUp ?
|
|
7472
|
+
backup: backedUp ? join16(dir, "config.backup.json") : null
|
|
8136
7473
|
},
|
|
8137
7474
|
`Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
|
|
8138
7475
|
Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
|
|
@@ -8189,9 +7526,9 @@ function isTakeDir(target) {
|
|
|
8189
7526
|
return directoryKind(target) === "take";
|
|
8190
7527
|
}
|
|
8191
7528
|
async function readProgramDoc(dir, config) {
|
|
8192
|
-
const docPath =
|
|
8193
|
-
if (!
|
|
8194
|
-
const parsed = JSON.parse(await
|
|
7529
|
+
const docPath = join16(dir, "doc.json");
|
|
7530
|
+
if (!existsSync13(docPath)) return null;
|
|
7531
|
+
const parsed = JSON.parse(await readFile13(docPath, "utf8"));
|
|
8195
7532
|
if (typeof parsed !== "object" || parsed === null || "source" in parsed)
|
|
8196
7533
|
return null;
|
|
8197
7534
|
const raw = parsed;
|
|
@@ -8202,7 +7539,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8202
7539
|
const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
|
|
8203
7540
|
const { config, ...rest } = program;
|
|
8204
7541
|
const onDisk = { ...hosted, program: rest };
|
|
8205
|
-
await writeFile11(
|
|
7542
|
+
await writeFile11(join16(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
|
|
8206
7543
|
return {
|
|
8207
7544
|
config: config && typeof config === "object" ? config : null
|
|
8208
7545
|
};
|
|
@@ -8410,7 +7747,7 @@ async function cmdRecipe(argv) {
|
|
|
8410
7747
|
|
|
8411
7748
|
// src/plugin/brand.ts
|
|
8412
7749
|
import { writeFile as writeFile12 } from "fs/promises";
|
|
8413
|
-
import { resolve as
|
|
7750
|
+
import { resolve as resolve7 } from "path";
|
|
8414
7751
|
import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
|
|
8415
7752
|
import { lookKindForGround } from "@vosjs/studio-core";
|
|
8416
7753
|
var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
|
|
@@ -8459,7 +7796,7 @@ function isSaturated(hex2) {
|
|
|
8459
7796
|
return s >= 0.25 && l > 0.12 && l < 0.88;
|
|
8460
7797
|
}
|
|
8461
7798
|
var isLight = (hex2) => hsl(hex2).l >= 0.5;
|
|
8462
|
-
function
|
|
7799
|
+
function mixHex(a, b, t) {
|
|
8463
7800
|
const ch = (i) => Math.round(
|
|
8464
7801
|
parseInt(a.slice(i, i + 2), 16) * (1 - t) + parseInt(b.slice(i, i + 2), 16) * t
|
|
8465
7802
|
).toString(16).padStart(2, "0");
|
|
@@ -8477,7 +7814,7 @@ function mostCommon(values) {
|
|
|
8477
7814
|
}
|
|
8478
7815
|
return best;
|
|
8479
7816
|
}
|
|
8480
|
-
function
|
|
7817
|
+
function firstFamily(fontFamily) {
|
|
8481
7818
|
const first = fontFamily.split(",")[0]?.trim() ?? "";
|
|
8482
7819
|
return first.replace(/^["']|["']$/g, "");
|
|
8483
7820
|
}
|
|
@@ -8596,7 +7933,7 @@ function composeBrand(input) {
|
|
|
8596
7933
|
p.bgA = `the body's background`;
|
|
8597
7934
|
const surfaceHexes = w.surfaces.map(rgbToHex).filter((h) => h !== null && h !== bgA);
|
|
8598
7935
|
const neutralSurface = mostCommon(surfaceHexes.filter((h) => !isSaturated(h)));
|
|
8599
|
-
const bgB = neutralSurface ??
|
|
7936
|
+
const bgB = neutralSurface ?? mixHex(bgA, isLight(bgA) ? "#000000" : "#ffffff", 0.04);
|
|
8600
7937
|
p.bgB = neutralSurface ? `the most common section / card ground` : `no distinct surface found; bgA stepped 4% toward ${isLight(bgA) ? "black" : "white"}`;
|
|
8601
7938
|
const accentHexes = w.accents.map(rgbToHex).filter((h) => h !== null && isSaturated(h));
|
|
8602
7939
|
const themeHex = w.themeColor ? rgbToHex(w.themeColor) : null;
|
|
@@ -8606,13 +7943,13 @@ function composeBrand(input) {
|
|
|
8606
7943
|
const inkHex = rgbToHex(w.h1?.color ?? w.body.color) ?? (isLight(bgA) ? "#111111" : "#f5f5f5");
|
|
8607
7944
|
if (!accent) accent = inkHex;
|
|
8608
7945
|
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 =
|
|
7946
|
+
const bgC = mixHex(bgA, accent, 0.14);
|
|
8610
7947
|
p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
|
|
8611
7948
|
p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
|
|
8612
|
-
const fontDisplay = design?.fonts[0] ?? (w.h1 ?
|
|
7949
|
+
const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily));
|
|
8613
7950
|
p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
|
|
8614
7951
|
const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
|
|
8615
|
-
const fontBody = designBody ??
|
|
7952
|
+
const fontBody = designBody ?? firstFamily(w.body.fontFamily);
|
|
8616
7953
|
p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
|
|
8617
7954
|
const isIcon = (u) => /favicon|apple-touch|icon/i.test(u);
|
|
8618
7955
|
const onDark = (u) => /on-dark|white/i.test(u);
|
|
@@ -8727,7 +8064,7 @@ async function cmdBrand(argv) {
|
|
|
8727
8064
|
throw new UsageError(`not a URL: ${raw}`);
|
|
8728
8065
|
}
|
|
8729
8066
|
const r = createReporter(flags.json === true);
|
|
8730
|
-
const out =
|
|
8067
|
+
const out = resolve7(strFlag(flags, "out") ?? "BRAND.md");
|
|
8731
8068
|
r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
|
|
8732
8069
|
const [designText, llmsText] = await Promise.all([
|
|
8733
8070
|
fetchText(`${origin}/design.md`),
|
|
@@ -8780,7 +8117,7 @@ async function cmdBrand(argv) {
|
|
|
8780
8117
|
|
|
8781
8118
|
// src/plugin/agentBrowser.ts
|
|
8782
8119
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
8783
|
-
import { resolve as
|
|
8120
|
+
import { resolve as resolve8 } from "path";
|
|
8784
8121
|
function parseAgentBrowserLog(text) {
|
|
8785
8122
|
const records = [];
|
|
8786
8123
|
const problems = [];
|
|
@@ -9250,7 +8587,7 @@ async function cmdActions(argv) {
|
|
|
9250
8587
|
);
|
|
9251
8588
|
}
|
|
9252
8589
|
const r = createReporter(flags.json === true);
|
|
9253
|
-
const out =
|
|
8590
|
+
const out = resolve8(strFlag(flags, "out") ?? "actions.json");
|
|
9254
8591
|
const viewportFlag = strFlag(flags, "viewport");
|
|
9255
8592
|
let viewport;
|
|
9256
8593
|
if (viewportFlag) {
|
|
@@ -9258,8 +8595,8 @@ async function cmdActions(argv) {
|
|
|
9258
8595
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
9259
8596
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
9260
8597
|
}
|
|
9261
|
-
const { readFile:
|
|
9262
|
-
const text = await
|
|
8598
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
8599
|
+
const text = await readFile15(resolve8(input), "utf8");
|
|
9263
8600
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
9264
8601
|
for (const p of problems) r.log(` ${p}`);
|
|
9265
8602
|
const result = convertAgentBrowser(records, {
|
|
@@ -9311,7 +8648,8 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
9311
8648
|
"print",
|
|
9312
8649
|
"yes",
|
|
9313
8650
|
"composed",
|
|
9314
|
-
"check"
|
|
8651
|
+
"check",
|
|
8652
|
+
"motion"
|
|
9315
8653
|
]);
|
|
9316
8654
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
|
|
9317
8655
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -9319,10 +8657,10 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
|
|
|
9319
8657
|
Take pipeline
|
|
9320
8658
|
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
|
|
9321
8659
|
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]
|
|
8660
|
+
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
8661
|
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
8662
|
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) [--
|
|
8663
|
+
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
8664
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
9327
8665
|
vos brand <url> [--out BRAND.md] [--json]
|
|
9328
8666
|
vos open <take> [--studio <url>] [--print]
|
|
@@ -9422,44 +8760,47 @@ read once as the real page, blank ones (a wallpaper, an empty canvas) are
|
|
|
9422
8760
|
dropped and two of one frame collapse to one, with every drop said in
|
|
9423
8761
|
skipped[]. --times overrides with seconds, percents or step:<id>[+offset]
|
|
9424
8762
|
(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
|
-
|
|
8763
|
+
The LOOK presents the card: card-genre stills with no poster document
|
|
8764
|
+
and every video cut sit on a ground (a cream plate, the house gradient, a
|
|
8765
|
+
dark plate with a light streak) at ~84% of the width with headroom, a
|
|
8766
|
+
soft ambient shadow plus a tight contact shadow, and a hairline when card
|
|
8767
|
+
and ground are both light; a wide frame runs the card off the bottom.
|
|
8768
|
+
--look picks a house look (or none for the pre-look crops); with no flag
|
|
8769
|
+
the BRAND.md beside the take (or --brand <file>) decides from its look
|
|
8770
|
+
role or its own ground, and with no brand the house gradient.
|
|
8771
|
+
Screenshot-genre stills never take a look.
|
|
8772
|
+
A POSTER is a document, never a template: a plain take whose card sits
|
|
8773
|
+
where the poster wants it (frame.inset), leans (the tilt track), carries
|
|
8774
|
+
the words and the mark as clips (ids stage-title, stage-kicker,
|
|
8775
|
+
stage-brand, stage-mark) and ends on a trailing hold whose start is the
|
|
8776
|
+
still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
|
|
8777
|
+
CWS tile + marquee, GitHub social preview) render from the poster
|
|
8778
|
+
document of their aspect CLASS (landscape, square, portrait, tile), found
|
|
8779
|
+
beside the take as poster/<class>/doc.json (poster/doc.json serves every
|
|
8780
|
+
class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
|
|
8781
|
+
the document's rest; kit.json records source poster, the class, the file,
|
|
8782
|
+
the vos it tracks, the shot rect and the text boxes read FROM the
|
|
8783
|
+
document. A class with no document is the take's own frame, said once.
|
|
8784
|
+
deliver renders and verifies; it composes nothing. The composition is
|
|
8785
|
+
made with plan --style <poster>, which copies a poster's layout onto a
|
|
8786
|
+
take (its card placement, its stage clips with the release's words
|
|
8787
|
+
patched in from LAUNCH.md's headline and kicker roles or --headline and
|
|
8788
|
+
--kicker, its rest lean, its hold), or by hand in doc.json.
|
|
8789
|
+
The cut's MOTION is the document's too. plan proposes it on a fresh plan
|
|
8790
|
+
(--motion re-proposes onto an existing doc.json, replacing only its own
|
|
8791
|
+
proposals): the card's ENTRANCE (tilt-in by default: the card swings in
|
|
8792
|
+
from a perspective pose and settles), the END CARD (the last frame holds
|
|
8793
|
+
2.5 s while the card recedes and the headline, the release line and the
|
|
8794
|
+
wordmark rise; the brand's mark from BRAND.md logoUrl above them), a
|
|
8795
|
+
CAPTION per actions.json step at the step's moment, a music BED from
|
|
8796
|
+
LAUNCH.md's music role (a catalog slug or a mood) and a click sound on
|
|
8797
|
+
every press when the take has no mic. LAUNCH.md's entrance, endCard,
|
|
8798
|
+
captions, music and clicks roles, or the flags, change or switch each
|
|
8799
|
+
off; a deleted proposal stays deleted on a refresh. deliver applies each
|
|
8800
|
+
destination's MECHANICS and nothing more: the README loop plays no
|
|
8801
|
+
entrance, end card or sound; a channel that autoplays muted drops the
|
|
8802
|
+
bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
|
|
8803
|
+
the camera. Screenshot-genre
|
|
9463
8804
|
destinations (CWS screenshots, the PH gallery) are the real page at that
|
|
9464
8805
|
moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
|
|
9465
8806
|
policy); --composed keeps the cut's camera and chrome instead. --set
|
|
@@ -9547,7 +8888,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
9547
8888
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
9548
8889
|
`;
|
|
9549
8890
|
async function loadActions(file) {
|
|
9550
|
-
const raw = JSON.parse(await
|
|
8891
|
+
const raw = JSON.parse(await readFile14(file, "utf8"));
|
|
9551
8892
|
const errors = validateActions(raw);
|
|
9552
8893
|
if (errors.length)
|
|
9553
8894
|
throw new UsageError(`invalid actions file:
|
|
@@ -9603,10 +8944,10 @@ async function cmdRecord(argv) {
|
|
|
9603
8944
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
9604
8945
|
if (!url)
|
|
9605
8946
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
9606
|
-
const outDir =
|
|
8947
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
9607
8948
|
const backdrop = await takeBackdrop(flags, r);
|
|
9608
8949
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9609
|
-
if (
|
|
8950
|
+
if (existsSync14(join17(outDir, "meta.json"))) {
|
|
9610
8951
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9611
8952
|
r.log(
|
|
9612
8953
|
`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 +8994,7 @@ async function cmdRecord(argv) {
|
|
|
9653
8994
|
},
|
|
9654
8995
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
|
|
9655
8996
|
${(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 ${
|
|
8997
|
+
Next: edit ${join17(outDir, "doc.json")} (optional), then: vos render ${outDir}`
|
|
9657
8998
|
);
|
|
9658
8999
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
9659
9000
|
} finally {
|
|
@@ -9677,19 +9018,19 @@ async function cmdCreate2(argv) {
|
|
|
9677
9018
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
9678
9019
|
if (!url)
|
|
9679
9020
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
9680
|
-
const outDir =
|
|
9021
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
9681
9022
|
const fmtRaw = strFlag(flags, "format") ?? "webm";
|
|
9682
9023
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9683
9024
|
throw new UsageError("--format must be webm or mp4");
|
|
9684
9025
|
const format = fmtRaw;
|
|
9685
|
-
const out = positionals[0] ??
|
|
9026
|
+
const out = positionals[0] ?? join17(outDir, `out.${format}`);
|
|
9686
9027
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9687
9028
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9688
9029
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
9689
9030
|
}
|
|
9690
9031
|
const backdrop = await takeBackdrop(flags, r);
|
|
9691
9032
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9692
|
-
if (
|
|
9033
|
+
if (existsSync14(join17(outDir, "meta.json"))) {
|
|
9693
9034
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9694
9035
|
r.log(
|
|
9695
9036
|
`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 +9113,66 @@ async function cmdPlan(argv) {
|
|
|
9772
9113
|
const dir = positionals[0];
|
|
9773
9114
|
if (!dir)
|
|
9774
9115
|
throw new UsageError(
|
|
9775
|
-
|
|
9116
|
+
'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
|
|
9776
9117
|
);
|
|
9777
9118
|
const r = createReporter(flags.json === true);
|
|
9778
9119
|
if (flags.fresh === true) {
|
|
9779
|
-
await rm4(
|
|
9120
|
+
await rm4(join17(dir, "doc.json"), { force: true });
|
|
9780
9121
|
r.log("note: --fresh discarded the existing doc.json");
|
|
9781
9122
|
}
|
|
9782
9123
|
let reuse;
|
|
9783
9124
|
if (flags.reuse === true) {
|
|
9784
|
-
const from =
|
|
9785
|
-
if (!
|
|
9125
|
+
const from = resolve9(strFlag(flags, "from") ?? join17(dir, PREV_DOC_NAME));
|
|
9126
|
+
if (!existsSync14(from)) {
|
|
9786
9127
|
throw new UsageError(
|
|
9787
9128
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
9788
9129
|
);
|
|
9789
9130
|
}
|
|
9790
9131
|
reuse = {
|
|
9791
9132
|
from,
|
|
9792
|
-
doc: JSON.parse(await
|
|
9133
|
+
doc: JSON.parse(await readFile14(from, "utf8"))
|
|
9793
9134
|
};
|
|
9794
9135
|
}
|
|
9795
9136
|
const style = await resolveStyleRef(flags);
|
|
9796
|
-
const
|
|
9137
|
+
const hasDoc = existsSync14(join17(dir, "doc.json"));
|
|
9138
|
+
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
9139
|
+
const release = await releaseInputs(dir, flags, r);
|
|
9140
|
+
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
9797
9141
|
const s = await planTake(dir, {
|
|
9798
9142
|
...style ? { style } : {},
|
|
9799
9143
|
...reuse ? { reuse } : {},
|
|
9800
|
-
backdrop
|
|
9144
|
+
backdrop,
|
|
9145
|
+
words: release.words,
|
|
9146
|
+
mark: release.mark,
|
|
9147
|
+
...motionWanted ? {
|
|
9148
|
+
motion: {
|
|
9149
|
+
words: release.words,
|
|
9150
|
+
launch: release.launchRoles,
|
|
9151
|
+
ink: release.ink,
|
|
9152
|
+
mark: release.mark,
|
|
9153
|
+
captions: release.captions,
|
|
9154
|
+
catalog: release.catalog,
|
|
9155
|
+
again: flags.motion === true
|
|
9156
|
+
}
|
|
9157
|
+
} : {}
|
|
9801
9158
|
});
|
|
9159
|
+
const layoutLines = s.layout ? `
|
|
9160
|
+
layout from ${s.styleFrom}: ${[
|
|
9161
|
+
s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
|
|
9162
|
+
s.layout.lean ? "the rest lean" : "",
|
|
9163
|
+
s.layout.hold ? "the hold" : ""
|
|
9164
|
+
].filter(Boolean).join(", ")}` + (s.layout.notes.length ? `
|
|
9165
|
+
${s.layout.notes.join("\n ")}` : "") : "";
|
|
9166
|
+
const motionLines = s.motion ? (s.motion.notes.length ? `
|
|
9167
|
+
proposed: ${s.motion.notes.join(", ")}` : "") + (s.motion.skipped.length ? `
|
|
9168
|
+
not proposed: ${s.motion.skipped.join("; ")}` : "") : "";
|
|
9802
9169
|
const reuseLines = s.reuse ? `
|
|
9803
9170
|
reused ${s.reuse.from}: ${s.reuse.anchored} anchored + ${s.reuse.mapped} mapped span(s)` + (s.reuse.flagged.length ? `
|
|
9804
9171
|
flagged:
|
|
9805
9172
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
9806
9173
|
r.done(
|
|
9807
9174
|
{
|
|
9808
|
-
take:
|
|
9175
|
+
take: resolve9(dir),
|
|
9809
9176
|
fresh: s.fresh,
|
|
9810
9177
|
cursorKept: s.cursorKept,
|
|
9811
9178
|
zoomAuto: s.zoomAuto,
|
|
@@ -9813,12 +9180,78 @@ async function cmdPlan(argv) {
|
|
|
9813
9180
|
duration: s.doc.source.meta.durationMs / 1e3,
|
|
9814
9181
|
...s.backdrop !== void 0 ? { backdrop: s.backdrop } : {},
|
|
9815
9182
|
...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
|
|
9183
|
+
...s.layout ? { layout: s.layout } : {},
|
|
9184
|
+
...s.motion ? { motion: s.motion } : {},
|
|
9816
9185
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
9817
9186
|
},
|
|
9818
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
9187
|
+
`${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
9188
|
);
|
|
9820
9189
|
return EXIT_OK;
|
|
9821
9190
|
}
|
|
9191
|
+
async function releaseInputs(dir, flags, r) {
|
|
9192
|
+
let lookPick;
|
|
9193
|
+
try {
|
|
9194
|
+
lookPick = await resolveLook(dir, {
|
|
9195
|
+
look: strFlag(flags, "look"),
|
|
9196
|
+
brand: strFlag(flags, "brand")
|
|
9197
|
+
});
|
|
9198
|
+
} catch (e) {
|
|
9199
|
+
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
9200
|
+
}
|
|
9201
|
+
const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
|
|
9202
|
+
const lines = (v) => v === null || v === void 0 ? null : v.replace(/\\n/g, "\n");
|
|
9203
|
+
const words = {
|
|
9204
|
+
headline: lines(strFlag(flags, "headline") ?? launch?.roles.headline),
|
|
9205
|
+
kicker: lines(strFlag(flags, "kicker") ?? launch?.roles.kicker),
|
|
9206
|
+
brand: strFlag(flags, "brand-name") ?? lookPick.roles?.wordmark ?? null,
|
|
9207
|
+
release: strFlag(flags, "release") ?? null
|
|
9208
|
+
};
|
|
9209
|
+
if (launch) r.log(`words: ${launch.file}`);
|
|
9210
|
+
const launchRoles = { ...launch?.roles ?? {} };
|
|
9211
|
+
for (const [flag, role] of [
|
|
9212
|
+
["music", "music"],
|
|
9213
|
+
["entrance", "entrance"],
|
|
9214
|
+
["end-card", "endCard"],
|
|
9215
|
+
["captions", "captions"],
|
|
9216
|
+
["clicks", "clicks"]
|
|
9217
|
+
]) {
|
|
9218
|
+
const v = strFlag(flags, flag);
|
|
9219
|
+
if (v !== void 0) launchRoles[role] = v;
|
|
9220
|
+
}
|
|
9221
|
+
const marks = await fetchBrandMarks(dir, lookPick.roles);
|
|
9222
|
+
for (const n of marks.notes) r.log(`note: ${n}`);
|
|
9223
|
+
const mark = lookPick.look?.kind === "dark" ? marks.dark : marks.light;
|
|
9224
|
+
if (mark) r.log(`brand mark: ${mark.key} (${mark.aspect.toFixed(2)}:1)`);
|
|
9225
|
+
const off2 = (v) => v !== void 0 && /^(none|off|no|false)$/i.test(v);
|
|
9226
|
+
let catalog = null;
|
|
9227
|
+
if (launchRoles.music && !off2(launchRoles.music)) {
|
|
9228
|
+
try {
|
|
9229
|
+
catalog = await fetchMusicCatalog(
|
|
9230
|
+
platformOrigin({
|
|
9231
|
+
origin: strFlag(flags, "origin"),
|
|
9232
|
+
api: strFlag(flags, "api")
|
|
9233
|
+
})
|
|
9234
|
+
);
|
|
9235
|
+
} catch (e) {
|
|
9236
|
+
r.log(
|
|
9237
|
+
`music catalog: ${e instanceof Error ? e.message : String(e)} \u2014 no bed`
|
|
9238
|
+
);
|
|
9239
|
+
}
|
|
9240
|
+
}
|
|
9241
|
+
const take = await loadTake(dir);
|
|
9242
|
+
const captions = (take.actions?.steps ?? []).flatMap((st, i) => {
|
|
9243
|
+
const step = st;
|
|
9244
|
+
return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
|
|
9245
|
+
});
|
|
9246
|
+
return {
|
|
9247
|
+
words,
|
|
9248
|
+
launchRoles,
|
|
9249
|
+
ink: endCardInk(lookPick.look, lookPick.roles),
|
|
9250
|
+
mark,
|
|
9251
|
+
captions,
|
|
9252
|
+
catalog
|
|
9253
|
+
};
|
|
9254
|
+
}
|
|
9822
9255
|
async function cmdRender(argv) {
|
|
9823
9256
|
const { positionals, flags, multi } = parseArgs(
|
|
9824
9257
|
argv,
|
|
@@ -9835,7 +9268,7 @@ async function cmdRender(argv) {
|
|
|
9835
9268
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9836
9269
|
throw new UsageError("--format must be webm or mp4");
|
|
9837
9270
|
const format = fmtRaw;
|
|
9838
|
-
const out = positionals[1] ??
|
|
9271
|
+
const out = positionals[1] ?? join17(dir, `out.${format}`);
|
|
9839
9272
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9840
9273
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9841
9274
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -10013,34 +9446,6 @@ async function cmdDeliver(argv) {
|
|
|
10013
9446
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
10014
9447
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
10015
9448
|
}
|
|
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
9449
|
let lookPick;
|
|
10045
9450
|
try {
|
|
10046
9451
|
lookPick = await resolveLook(dir, {
|
|
@@ -10052,63 +9457,18 @@ async function cmdDeliver(argv) {
|
|
|
10052
9457
|
}
|
|
10053
9458
|
r.log(`look: ${lookPick.from}`);
|
|
10054
9459
|
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
|
-
});
|
|
9460
|
+
if (launch) r.log(`launch: ${launch.file}`);
|
|
10094
9461
|
const browser = await launchBrowser();
|
|
10095
9462
|
try {
|
|
10096
9463
|
const result = await deliverTake(browser, dir, {
|
|
10097
9464
|
look: lookPick.look,
|
|
10098
|
-
|
|
10099
|
-
launchRoles,
|
|
10100
|
-
catalog,
|
|
10101
|
-
captions,
|
|
10102
|
-
words,
|
|
9465
|
+
launchRoles: launch?.roles ?? null,
|
|
10103
9466
|
channels,
|
|
10104
9467
|
outDir: strFlag(flags, "out"),
|
|
10105
9468
|
release: strFlag(flags, "release"),
|
|
10106
9469
|
times,
|
|
10107
9470
|
range,
|
|
10108
9471
|
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
9472
|
composed: hasFlag(flags, "composed"),
|
|
10113
9473
|
overrides: {
|
|
10114
9474
|
set: multi.set,
|
|
@@ -10126,7 +9486,7 @@ async function cmdDeliver(argv) {
|
|
|
10126
9486
|
});
|
|
10127
9487
|
const { kit } = result;
|
|
10128
9488
|
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.
|
|
9489
|
+
(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
9490
|
);
|
|
10131
9491
|
const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
|
|
10132
9492
|
r.done(
|
|
@@ -10149,14 +9509,14 @@ Store uploads stay manual: hand the human this directory and the manifest.`
|
|
|
10149
9509
|
async function resolveStyleRef(flags) {
|
|
10150
9510
|
const styleRef = strFlag(flags, "style");
|
|
10151
9511
|
if (!styleRef) return null;
|
|
10152
|
-
const file =
|
|
9512
|
+
const file = existsSync14(styleRef) ? resolve9(
|
|
10153
9513
|
styleRef,
|
|
10154
|
-
|
|
9514
|
+
existsSync14(join17(styleRef, "doc.json")) ? "doc.json" : ""
|
|
10155
9515
|
) : null;
|
|
10156
|
-
if (file &&
|
|
9516
|
+
if (file && existsSync14(file)) {
|
|
10157
9517
|
return {
|
|
10158
9518
|
from: file,
|
|
10159
|
-
doc: JSON.parse(await
|
|
9519
|
+
doc: JSON.parse(await readFile14(file, "utf8"))
|
|
10160
9520
|
};
|
|
10161
9521
|
}
|
|
10162
9522
|
const origin = platformOrigin({
|
|
@@ -10179,7 +9539,7 @@ async function resolveStyleRef(flags) {
|
|
|
10179
9539
|
throw new UsageError(`--style: ${styleRef} carries no doc (is it a take?)`);
|
|
10180
9540
|
return {
|
|
10181
9541
|
from: `${origin}/vos/${styleRef}`,
|
|
10182
|
-
doc:
|
|
9542
|
+
doc: migrateHostedDoc5(doc.body)
|
|
10183
9543
|
};
|
|
10184
9544
|
}
|
|
10185
9545
|
async function cmdDigest(argv) {
|
|
@@ -10193,7 +9553,7 @@ async function cmdDigest(argv) {
|
|
|
10193
9553
|
const take = await loadTake(dir);
|
|
10194
9554
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
10195
9555
|
const transcriptPath = strFlag(flags, "transcript");
|
|
10196
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
9556
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile14(transcriptPath, "utf8"))) : null;
|
|
10197
9557
|
const style = await resolveStyleRef(flags);
|
|
10198
9558
|
const noFrames = flags["no-frames"] === true;
|
|
10199
9559
|
let browser = null;
|
|
@@ -10224,7 +9584,7 @@ async function cmdDigest(argv) {
|
|
|
10224
9584
|
kinds,
|
|
10225
9585
|
frames: d.images.full,
|
|
10226
9586
|
crops: d.images.crop,
|
|
10227
|
-
sheet: d.images.sheet ?
|
|
9587
|
+
sheet: d.images.sheet ? join17(result.outDir, d.images.sheet) : null,
|
|
10228
9588
|
scenes: kinds.scene ?? 0,
|
|
10229
9589
|
sourceDuration: d.take.sourceDuration,
|
|
10230
9590
|
outputDuration: d.take.outputDuration,
|
|
@@ -10248,14 +9608,14 @@ async function cmdOpen(argv) {
|
|
|
10248
9608
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
10249
9609
|
const r = createReporter(flags.json === true);
|
|
10250
9610
|
const take = await loadTake(dir);
|
|
10251
|
-
if (!
|
|
9611
|
+
if (!existsSync14(take.paths.recording)) {
|
|
10252
9612
|
throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
|
|
10253
9613
|
}
|
|
10254
9614
|
const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
|
|
10255
9615
|
/\/+$/,
|
|
10256
9616
|
""
|
|
10257
9617
|
);
|
|
10258
|
-
const server = await startTakeServer(
|
|
9618
|
+
const server = await startTakeServer(resolve9(dir), {});
|
|
10259
9619
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
10260
9620
|
r.event({ event: "open", server: server.base, url });
|
|
10261
9621
|
r.log(`take served at ${server.base}`);
|
|
@@ -10289,11 +9649,12 @@ async function cmdJudge(argv) {
|
|
|
10289
9649
|
"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
9650
|
);
|
|
10291
9651
|
const r = createReporter(flags.json === true);
|
|
10292
|
-
const kitFile = target.endsWith("kit.json") ? target :
|
|
10293
|
-
if (!
|
|
10294
|
-
if (!
|
|
9652
|
+
const kitFile = target.endsWith("kit.json") ? target : join17(target, "kit.json");
|
|
9653
|
+
if (!existsSync14(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
9654
|
+
if (!existsSync14(against))
|
|
9655
|
+
throw new UsageError(`${against}: no reference manifest`);
|
|
10295
9656
|
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
10296
|
-
const verdicts = JSON.parse(await
|
|
9657
|
+
const verdicts = JSON.parse(await readFile14(result.verdictFile, "utf8")).verdicts;
|
|
10297
9658
|
const rate = winRate(verdicts);
|
|
10298
9659
|
const lines = result.sheets.map(
|
|
10299
9660
|
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
@@ -10313,7 +9674,7 @@ async function cmdValidate(argv) {
|
|
|
10313
9674
|
const target = positionals[0];
|
|
10314
9675
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
10315
9676
|
const r = createReporter(flags.json === true);
|
|
10316
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
9677
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync14(join17(target, "kit.json")) && !isTakeDir(target) ? join17(target, "kit.json") : null;
|
|
10317
9678
|
if (kitTarget) {
|
|
10318
9679
|
const picture = flags.picture === true;
|
|
10319
9680
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -10345,9 +9706,9 @@ async function cmdValidate(argv) {
|
|
|
10345
9706
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
10346
9707
|
return EXIT_OK;
|
|
10347
9708
|
}
|
|
10348
|
-
if (!isTakeDir(target) &&
|
|
9709
|
+
if (!isTakeDir(target) && existsSync14(join17(target, "config.json"))) {
|
|
10349
9710
|
const parsed = JSON.parse(
|
|
10350
|
-
await
|
|
9711
|
+
await readFile14(join17(target, "config.json"), "utf8")
|
|
10351
9712
|
);
|
|
10352
9713
|
const pre = preflightConfig(parsed);
|
|
10353
9714
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -10388,7 +9749,7 @@ async function cmdValidate(argv) {
|
|
|
10388
9749
|
const take = await loadTake(target);
|
|
10389
9750
|
const problems = [];
|
|
10390
9751
|
const warnings = [];
|
|
10391
|
-
if (!
|
|
9752
|
+
if (!existsSync14(take.paths.recording))
|
|
10392
9753
|
problems.push("missing recording.webm (re-run record)");
|
|
10393
9754
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
10394
9755
|
if (take.doc) {
|
|
@@ -10433,7 +9794,7 @@ async function cmdPush3(argv) {
|
|
|
10433
9794
|
const r = createReporter(flags.json === true);
|
|
10434
9795
|
const overrides = multi.override;
|
|
10435
9796
|
const result = await pushTake(
|
|
10436
|
-
|
|
9797
|
+
resolve9(dir),
|
|
10437
9798
|
{
|
|
10438
9799
|
key: strFlag(flags, "key"),
|
|
10439
9800
|
api: strFlag(flags, "api"),
|
|
@@ -10447,11 +9808,15 @@ async function cmdPush3(argv) {
|
|
|
10447
9808
|
},
|
|
10448
9809
|
r
|
|
10449
9810
|
);
|
|
9811
|
+
const pushedTo = platformOrigin({
|
|
9812
|
+
origin: strFlag(flags, "origin"),
|
|
9813
|
+
api: strFlag(flags, "api")
|
|
9814
|
+
}).replace(/\/+$/, "");
|
|
10450
9815
|
r.done(
|
|
10451
9816
|
{ ...result },
|
|
10452
9817
|
`pushed v${result.versionNumber} \u2192 vos ${result.vosId}
|
|
10453
|
-
review:
|
|
10454
|
-
studio:
|
|
9818
|
+
review: ${pushedTo}/vos/${result.vosId}
|
|
9819
|
+
studio: ${pushedTo}/studio?vos=${result.vosId}`
|
|
10455
9820
|
);
|
|
10456
9821
|
return EXIT_OK;
|
|
10457
9822
|
}
|
|
@@ -10460,7 +9825,7 @@ async function cmdPull2(argv) {
|
|
|
10460
9825
|
const dir = positionals[0] ?? ".";
|
|
10461
9826
|
const r = createReporter(flags.json === true);
|
|
10462
9827
|
const result = await pullTake(
|
|
10463
|
-
|
|
9828
|
+
resolve9(dir),
|
|
10464
9829
|
{
|
|
10465
9830
|
key: strFlag(flags, "key"),
|
|
10466
9831
|
api: strFlag(flags, "api"),
|
|
@@ -10595,4 +9960,4 @@ export {
|
|
|
10595
9960
|
convertAgentBrowser,
|
|
10596
9961
|
run
|
|
10597
9962
|
};
|
|
10598
|
-
//# sourceMappingURL=chunk-
|
|
9963
|
+
//# sourceMappingURL=chunk-4LLBPFCW.js.map
|