@vosjs/cli 0.67.6 → 0.69.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 +10 -13
- package/dist/{assetManifest-DU5ZEIDE.js → assetManifest-3QYCLT2H.js} +3 -3
- package/dist/{chunk-AUFY3M22.js → chunk-2KEQ3LKH.js} +2 -2
- package/dist/chunk-2KEQ3LKH.js.map +1 -0
- package/dist/{chunk-42ZU5XUW.js → chunk-5DUXN3RX.js} +1077 -2165
- package/dist/chunk-5DUXN3RX.js.map +1 -0
- package/dist/{chunk-MDRYSHOF.js → chunk-5GZ3GPAK.js} +2 -2
- package/dist/{chunk-C5VKACGQ.js → chunk-FL3E27WE.js} +13 -2
- package/dist/{chunk-C5VKACGQ.js.map → chunk-FL3E27WE.js.map} +1 -1
- package/dist/{chunk-OFTRK3Y3.js → chunk-Y5E672GT.js} +23 -4
- package/dist/chunk-Y5E672GT.js.map +1 -0
- package/dist/cli.js +7 -7
- package/dist/index.d.ts +17 -1
- package/dist/index.js +5 -5
- package/dist/manifest-QLQKVSML.js +8 -0
- package/dist/{run-4VSB2XHR.js → run-NDBUFOIG.js} +5 -5
- package/package.json +7 -7
- package/schema/channel-specs.json +5 -233
- package/schema/doc.schema.json +3 -3
- package/skills/VERSION +1 -1
- package/skills/launch-kit/SKILL.md +101 -223
- package/skills/launch-kit/references/channel-specs.md +14 -44
- package/skills/product-video/SKILL.md +9 -14
- package/skills/product-video/references/destinations.md +12 -23
- package/skills/vos-footage/SKILL.md +1 -1
- package/skills/vos-migrate/SKILL.md +3 -3
- package/dist/chunk-42ZU5XUW.js.map +0 -1
- package/dist/chunk-AUFY3M22.js.map +0 -1
- package/dist/chunk-OFTRK3Y3.js.map +0 -1
- package/dist/manifest-ZBK6IERM.js +0 -8
- /package/dist/{assetManifest-DU5ZEIDE.js.map → assetManifest-3QYCLT2H.js.map} +0 -0
- /package/dist/{chunk-MDRYSHOF.js.map → chunk-5GZ3GPAK.js.map} +0 -0
- /package/dist/{manifest-ZBK6IERM.js.map → manifest-QLQKVSML.js.map} +0 -0
- /package/dist/{run-4VSB2XHR.js.map → run-NDBUFOIG.js.map} +0 -0
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
reencodeStill,
|
|
10
10
|
renderStill,
|
|
11
11
|
videoFormat
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-5GZ3GPAK.js";
|
|
13
13
|
import {
|
|
14
14
|
PREV_DOC_NAME,
|
|
15
15
|
RECORDING_NAME,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
docMediaRefs,
|
|
18
18
|
dropScreencastFrames,
|
|
19
19
|
ensureTakeDir,
|
|
20
|
+
fileSha256,
|
|
20
21
|
hostedLiteralWarnings,
|
|
21
22
|
isTakeRelativeKey,
|
|
22
23
|
loadTake,
|
|
@@ -26,14 +27,16 @@ import {
|
|
|
26
27
|
pullMedia,
|
|
27
28
|
readJson,
|
|
28
29
|
readMediaHead,
|
|
30
|
+
recordHostedMedia,
|
|
29
31
|
sourceSidecarRefs,
|
|
30
32
|
takeMediaFile,
|
|
31
33
|
takePaths,
|
|
34
|
+
takeRelativeFile,
|
|
32
35
|
uploadAsset,
|
|
33
36
|
uploadDocRefs,
|
|
34
37
|
uploadManifest,
|
|
35
38
|
writeJson
|
|
36
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-Y5E672GT.js";
|
|
37
40
|
import {
|
|
38
41
|
CREDENTIALS_PATH,
|
|
39
42
|
MEDIA_HEAD_BYTES,
|
|
@@ -66,7 +69,7 @@ import {
|
|
|
66
69
|
strFlag,
|
|
67
70
|
writeCredential,
|
|
68
71
|
writeSyncState
|
|
69
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-FL3E27WE.js";
|
|
70
73
|
import {
|
|
71
74
|
deadLine,
|
|
72
75
|
deadWarns
|
|
@@ -128,13 +131,13 @@ function landedLines(l) {
|
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
// src/plugin/run.ts
|
|
131
|
-
import { mkdir as
|
|
132
|
-
import { existsSync as
|
|
133
|
-
import { join as
|
|
134
|
+
import { mkdir as mkdir8, readFile as readFile12, rm as rm2 } from "fs/promises";
|
|
135
|
+
import { existsSync as existsSync22 } from "fs";
|
|
136
|
+
import { join as join24, resolve as resolve13 } from "path";
|
|
134
137
|
import {
|
|
135
138
|
END_CARD_FROM as END_CARD_FROM2,
|
|
136
139
|
docOutputDuration as docOutputDuration3,
|
|
137
|
-
migrateHostedDoc as
|
|
140
|
+
migrateHostedDoc as migrateHostedDoc4
|
|
138
141
|
} from "@vosjs/studio-core";
|
|
139
142
|
|
|
140
143
|
// src/plugin/output.ts
|
|
@@ -2117,10 +2120,10 @@ function startTakeServer(rootDir, pages) {
|
|
|
2117
2120
|
res.writeHead(404).end();
|
|
2118
2121
|
});
|
|
2119
2122
|
return new Promise(
|
|
2120
|
-
(
|
|
2123
|
+
(resolve14) => server.listen(0, () => {
|
|
2121
2124
|
const addr = server.address();
|
|
2122
2125
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
2123
|
-
|
|
2126
|
+
resolve14({ base: `http://localhost:${port}`, close: () => server.close() });
|
|
2124
2127
|
})
|
|
2125
2128
|
);
|
|
2126
2129
|
}
|
|
@@ -2170,8 +2173,8 @@ function renderPageHtml() {
|
|
|
2170
2173
|
${tweenRuntimeCode}
|
|
2171
2174
|
// Vos Element System (bundled IIFE) \u2014 the same injection the engine's
|
|
2172
2175
|
// render template performs. Take configs never touch ctx.elements, but a
|
|
2173
|
-
//
|
|
2174
|
-
//
|
|
2176
|
+
// program may draw element text and images over its ground, and without
|
|
2177
|
+
// this the elements silently never draw.
|
|
2175
2178
|
${elementsBundleCode}
|
|
2176
2179
|
window.__vos__ = window.__vos__ || {}
|
|
2177
2180
|
window.__vos__.elements = __vosElementsFactory.createVosElements(THREE)
|
|
@@ -2357,14 +2360,14 @@ async function renderInPage(opts) {
|
|
|
2357
2360
|
w.__error = String(e instanceof Error && e.stack || e);
|
|
2358
2361
|
}
|
|
2359
2362
|
}
|
|
2360
|
-
async function renderChunk(context2, serverBase,
|
|
2361
|
-
const outName = `render-chunk-${
|
|
2363
|
+
async function renderChunk(context2, serverBase, chunk, opts, onChunkProgress) {
|
|
2364
|
+
const outName = `render-chunk-${chunk.index}.tmp`;
|
|
2362
2365
|
const page = await context2.newPage();
|
|
2363
2366
|
try {
|
|
2364
2367
|
page.on("console", (m) => {
|
|
2365
2368
|
if (m.type() === "error" || process.env.VOS_RENDER_DEBUG)
|
|
2366
2369
|
process.stderr.write(
|
|
2367
|
-
` [render page ${
|
|
2370
|
+
` [render page ${chunk.index} ${m.type()}] ${m.text()}
|
|
2368
2371
|
`
|
|
2369
2372
|
);
|
|
2370
2373
|
});
|
|
@@ -2372,7 +2375,7 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
|
|
|
2372
2375
|
page.on(
|
|
2373
2376
|
"pageerror",
|
|
2374
2377
|
(e) => process.stderr.write(
|
|
2375
|
-
` [render pageerror ${
|
|
2378
|
+
` [render pageerror ${chunk.index}] ${String(e)}
|
|
2376
2379
|
`
|
|
2377
2380
|
)
|
|
2378
2381
|
);
|
|
@@ -2391,8 +2394,8 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
|
|
|
2391
2394
|
W: opts.width,
|
|
2392
2395
|
H: opts.height,
|
|
2393
2396
|
fps: opts.fps,
|
|
2394
|
-
startFrame:
|
|
2395
|
-
endFrame:
|
|
2397
|
+
startFrame: chunk.startFrame + frameOffset,
|
|
2398
|
+
endFrame: chunk.endFrame + frameOffset,
|
|
2396
2399
|
format: opts.format,
|
|
2397
2400
|
bitrate: opts.bitrate ?? 1e7,
|
|
2398
2401
|
mediabunnyUrl: MEDIABUNNY_URL,
|
|
@@ -2403,10 +2406,10 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
|
|
|
2403
2406
|
audioDuration: audio?.duration
|
|
2404
2407
|
}).catch(() => {
|
|
2405
2408
|
});
|
|
2406
|
-
const timeoutMs = 12e4 +
|
|
2409
|
+
const timeoutMs = 12e4 + chunk.frameCount * 500;
|
|
2407
2410
|
await waitForPageDone(
|
|
2408
2411
|
page,
|
|
2409
|
-
`render chunk ${
|
|
2412
|
+
`render chunk ${chunk.index}`,
|
|
2410
2413
|
onChunkProgress,
|
|
2411
2414
|
timeoutMs
|
|
2412
2415
|
);
|
|
@@ -2438,8 +2441,8 @@ async function renderAnimation(browser, opts) {
|
|
|
2438
2441
|
};
|
|
2439
2442
|
const outNames = await Promise.all(
|
|
2440
2443
|
chunks.map(
|
|
2441
|
-
(
|
|
2442
|
-
progress[
|
|
2444
|
+
(chunk) => renderChunk(context2, server.base, chunk, effOpts, (fraction) => {
|
|
2445
|
+
progress[chunk.index] = fraction;
|
|
2443
2446
|
reportProgress();
|
|
2444
2447
|
})
|
|
2445
2448
|
)
|
|
@@ -2454,9 +2457,9 @@ async function renderAnimation(browser, opts) {
|
|
|
2454
2457
|
return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
|
|
2455
2458
|
}
|
|
2456
2459
|
const { bytes } = await concatEncodedVideo(
|
|
2457
|
-
chunks.map((
|
|
2460
|
+
chunks.map((chunk, i) => ({
|
|
2458
2461
|
data: new Uint8Array(files[i]),
|
|
2459
|
-
duration:
|
|
2462
|
+
duration: chunk.duration
|
|
2460
2463
|
})),
|
|
2461
2464
|
{ format: opts.format, frameRate: opts.fps }
|
|
2462
2465
|
);
|
|
@@ -3398,11 +3401,10 @@ async function writeIndexJson(result) {
|
|
|
3398
3401
|
}
|
|
3399
3402
|
|
|
3400
3403
|
// src/plugin/deliver.ts
|
|
3401
|
-
import { mkdir as mkdir3,
|
|
3402
|
-
import {
|
|
3403
|
-
import {
|
|
3404
|
-
import {
|
|
3405
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
3404
|
+
import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
|
|
3405
|
+
import { join as join4, relative, resolve as resolve3 } from "path";
|
|
3406
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3407
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3406
3408
|
import { parseFrontmatter } from "@vosjs/shared/frontmatter";
|
|
3407
3409
|
import {
|
|
3408
3410
|
CHANNEL_SPECS_VERIFIED,
|
|
@@ -3520,614 +3522,119 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
3520
3522
|
};
|
|
3521
3523
|
}
|
|
3522
3524
|
|
|
3525
|
+
// src/plugin/motionPlan.ts
|
|
3526
|
+
import {
|
|
3527
|
+
END_CARD_FROM,
|
|
3528
|
+
END_CARD_SECONDS,
|
|
3529
|
+
applyTemplate,
|
|
3530
|
+
docOutputDuration,
|
|
3531
|
+
dropTemplate,
|
|
3532
|
+
endCardClips,
|
|
3533
|
+
migrateMotion,
|
|
3534
|
+
ratedSegments as ratedSegments4,
|
|
3535
|
+
sameMedia,
|
|
3536
|
+
spanOutputExtent as spanOutputExtent4
|
|
3537
|
+
} from "@vosjs/studio-core";
|
|
3538
|
+
|
|
3523
3539
|
// src/plugin/moments.ts
|
|
3524
|
-
import {
|
|
3540
|
+
import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
|
|
3541
|
+
var STEP_SETTLE_SECONDS = 0.4;
|
|
3542
|
+
function stepOutputTime(rated, step, settle) {
|
|
3543
|
+
const src = step.tEnd + settle;
|
|
3544
|
+
const ext = spanOutputExtent3(rated, src, src + 1e-3);
|
|
3545
|
+
if (!ext) return null;
|
|
3546
|
+
return ext.start;
|
|
3547
|
+
}
|
|
3525
3548
|
|
|
3526
|
-
// src/plugin/
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3549
|
+
// src/plugin/motionPlan.ts
|
|
3550
|
+
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
3551
|
+
"x-feed-cut",
|
|
3552
|
+
"youtube-main-demo",
|
|
3553
|
+
"shorts-linkedin-vertical-cut"
|
|
3554
|
+
]);
|
|
3555
|
+
var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
3556
|
+
var BED_ID = "bed";
|
|
3557
|
+
var CLICK_ID_PREFIX = "click-";
|
|
3558
|
+
var CAPTION_ID_PREFIX = "caption-";
|
|
3559
|
+
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
3560
|
+
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3561
|
+
var TRANSITION_CLIP_MIN = 1;
|
|
3562
|
+
function proposeTransitions(input, kind) {
|
|
3563
|
+
const doc = structuredClone(input);
|
|
3564
|
+
const skipped = [];
|
|
3565
|
+
let count = 0;
|
|
3566
|
+
const steps = doc.source.meta.steps ?? [];
|
|
3567
|
+
for (const step of steps) {
|
|
3568
|
+
if (!step.navigated || step.skipped) continue;
|
|
3569
|
+
const name = step.id ?? String(step.step);
|
|
3570
|
+
const wait = step.do === "wait";
|
|
3571
|
+
const outEnd = round3(wait ? step.tStart : step.tEnd);
|
|
3572
|
+
const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
|
|
3573
|
+
const segs = doc.segments;
|
|
3574
|
+
if (segs.some(
|
|
3575
|
+
(s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
|
|
3576
|
+
))
|
|
3577
|
+
continue;
|
|
3578
|
+
const i = segs.findIndex(
|
|
3579
|
+
(s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
|
|
3547
3580
|
);
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
colorType = bytes[start + 9];
|
|
3554
|
-
interlace = bytes[start + 12];
|
|
3555
|
-
} else if (type === "IDAT") {
|
|
3556
|
-
idat.push(bytes.subarray(start, start + len));
|
|
3557
|
-
} else if (type === "IEND") break;
|
|
3558
|
-
pos = start + len + 4;
|
|
3559
|
-
}
|
|
3560
|
-
if (!w || !h || depth !== 8 || interlace !== 0) return null;
|
|
3561
|
-
const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
|
|
3562
|
-
if (!channels) return null;
|
|
3563
|
-
const total = idat.reduce((n, c) => n + c.length, 0);
|
|
3564
|
-
const joined = new Uint8Array(total);
|
|
3565
|
-
let off2 = 0;
|
|
3566
|
-
for (const c of idat) {
|
|
3567
|
-
joined.set(c, off2);
|
|
3568
|
-
off2 += c.length;
|
|
3569
|
-
}
|
|
3570
|
-
let raw;
|
|
3571
|
-
try {
|
|
3572
|
-
raw = new Uint8Array(inflateSync(joined));
|
|
3573
|
-
} catch {
|
|
3574
|
-
return null;
|
|
3575
|
-
}
|
|
3576
|
-
const stride = w * channels;
|
|
3577
|
-
if (raw.length < (stride + 1) * h) return null;
|
|
3578
|
-
const out = new Uint8Array(w * h * 4);
|
|
3579
|
-
const prev = new Uint8Array(stride);
|
|
3580
|
-
const cur = new Uint8Array(stride);
|
|
3581
|
-
for (let y = 0; y < h; y++) {
|
|
3582
|
-
const filter = raw[y * (stride + 1)];
|
|
3583
|
-
const rowStart = y * (stride + 1) + 1;
|
|
3584
|
-
for (let i = 0; i < stride; i++) {
|
|
3585
|
-
const x = raw[rowStart + i];
|
|
3586
|
-
const a = i >= channels ? cur[i - channels] : 0;
|
|
3587
|
-
const b = prev[i];
|
|
3588
|
-
const c = i >= channels ? prev[i - channels] : 0;
|
|
3589
|
-
let v;
|
|
3590
|
-
switch (filter) {
|
|
3591
|
-
case 0:
|
|
3592
|
-
v = x;
|
|
3593
|
-
break;
|
|
3594
|
-
case 1:
|
|
3595
|
-
v = x + a;
|
|
3596
|
-
break;
|
|
3597
|
-
case 2:
|
|
3598
|
-
v = x + b;
|
|
3599
|
-
break;
|
|
3600
|
-
case 3:
|
|
3601
|
-
v = x + (a + b >> 1);
|
|
3602
|
-
break;
|
|
3603
|
-
case 4: {
|
|
3604
|
-
const p = a + b - c;
|
|
3605
|
-
const pa = Math.abs(p - a);
|
|
3606
|
-
const pb = Math.abs(p - b);
|
|
3607
|
-
const pc = Math.abs(p - c);
|
|
3608
|
-
v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
|
3609
|
-
break;
|
|
3610
|
-
}
|
|
3611
|
-
default:
|
|
3612
|
-
return null;
|
|
3613
|
-
}
|
|
3614
|
-
cur[i] = v & 255;
|
|
3615
|
-
}
|
|
3616
|
-
for (let px = 0; px < w; px++) {
|
|
3617
|
-
const o = (y * w + px) * 4;
|
|
3618
|
-
const s = px * channels;
|
|
3619
|
-
if (channels >= 3) {
|
|
3620
|
-
out[o] = cur[s];
|
|
3621
|
-
out[o + 1] = cur[s + 1];
|
|
3622
|
-
out[o + 2] = cur[s + 2];
|
|
3623
|
-
out[o + 3] = channels === 4 ? cur[s + 3] : 255;
|
|
3624
|
-
} else {
|
|
3625
|
-
out[o] = out[o + 1] = out[o + 2] = cur[s];
|
|
3626
|
-
out[o + 3] = channels === 2 ? cur[s + 1] : 255;
|
|
3627
|
-
}
|
|
3581
|
+
if (i < 0) {
|
|
3582
|
+
skipped.push(
|
|
3583
|
+
`no transition at step ${name}: the page change sits too close to a clip's edge`
|
|
3584
|
+
);
|
|
3585
|
+
continue;
|
|
3628
3586
|
}
|
|
3629
|
-
|
|
3587
|
+
const seg = segs[i];
|
|
3588
|
+
const before = { ...seg, out: outEnd, anim: { exit: kind } };
|
|
3589
|
+
const after = { ...seg, in: inStart, anim: { enter: kind } };
|
|
3590
|
+
if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
|
|
3591
|
+
if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
|
|
3592
|
+
doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
|
|
3593
|
+
count++;
|
|
3630
3594
|
}
|
|
3631
|
-
return {
|
|
3595
|
+
return { doc, count, skipped };
|
|
3632
3596
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
const p = at(img, x, y);
|
|
3651
|
-
r.push(p[0]);
|
|
3652
|
-
g.push(p[1]);
|
|
3653
|
-
b.push(p[2]);
|
|
3654
|
-
}
|
|
3655
|
-
}
|
|
3597
|
+
function pickTrack(catalog, ask) {
|
|
3598
|
+
if (!catalog || !ask || off(ask)) return null;
|
|
3599
|
+
const want = ask.trim().toLowerCase();
|
|
3600
|
+
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
3601
|
+
}
|
|
3602
|
+
function clickTimes(doc, range) {
|
|
3603
|
+
const rated = ratedSegments4(doc);
|
|
3604
|
+
const out = [];
|
|
3605
|
+
for (const e of doc.source.cursor) {
|
|
3606
|
+
if (e.type !== "down") continue;
|
|
3607
|
+
const src = e.t / 1e3;
|
|
3608
|
+
const ext = spanOutputExtent4(rated, src, src + 1e-3);
|
|
3609
|
+
if (!ext) continue;
|
|
3610
|
+
const t = ext.start;
|
|
3611
|
+
if (t < range[0] || t > range[1]) continue;
|
|
3612
|
+
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
3613
|
+
out.push(+(t - range[0]).toFixed(3));
|
|
3656
3614
|
}
|
|
3657
|
-
return
|
|
3615
|
+
return out;
|
|
3658
3616
|
}
|
|
3659
|
-
var
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
const
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
if (x > x1) x1 = x;
|
|
3673
|
-
if (y > y1) y1 = y;
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3617
|
+
var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
|
|
3618
|
+
const rate = s.rate && s.rate > 0 ? s.rate : 1;
|
|
3619
|
+
return acc + (s.out - s.in) / rate;
|
|
3620
|
+
}, 0);
|
|
3621
|
+
function templateWords(words2) {
|
|
3622
|
+
const headline = (words2.headline ?? "").trim();
|
|
3623
|
+
const kicker = (words2.kicker ?? "").trim();
|
|
3624
|
+
const brand = (words2.brand ?? "").trim();
|
|
3625
|
+
const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
|
|
3626
|
+
const out = {};
|
|
3627
|
+
if (headline) {
|
|
3628
|
+
out["stage-title"] = headline;
|
|
3629
|
+
out["endcard-title"] = headline;
|
|
3676
3630
|
}
|
|
3677
|
-
if (
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
let n = 0;
|
|
3685
|
-
const sx = Math.max(1, Math.round((x1 - x0) / 60));
|
|
3686
|
-
const sy = Math.max(1, Math.round((y1 - y0) / 60));
|
|
3687
|
-
for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
|
|
3688
|
-
for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
|
|
3689
|
-
sum += delta(at(img, x, y), ground);
|
|
3690
|
-
n++;
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
return n ? sum / n : 0;
|
|
3694
|
-
};
|
|
3695
|
-
const rise = (bands) => {
|
|
3696
|
-
const [outer, mid] = bands;
|
|
3697
|
-
return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
|
|
3698
|
-
};
|
|
3699
|
-
const readings = [];
|
|
3700
|
-
const yA = card.y + card.h * 0.2;
|
|
3701
|
-
const yB = card.y + card.h * 0.8;
|
|
3702
|
-
if (card.x >= 1 && card.w > band * 4) {
|
|
3703
|
-
const x0 = card.x;
|
|
3704
|
-
readings.push(
|
|
3705
|
-
rise([
|
|
3706
|
-
meanRect(x0, yA, x0 + band, yB),
|
|
3707
|
-
meanRect(x0 + band, yA, x0 + band * 2, yB),
|
|
3708
|
-
meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
|
|
3709
|
-
])
|
|
3710
|
-
);
|
|
3711
|
-
}
|
|
3712
|
-
if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
|
|
3713
|
-
const x1 = card.x + card.w;
|
|
3714
|
-
readings.push(
|
|
3715
|
-
rise([
|
|
3716
|
-
meanRect(x1 - band, yA, x1, yB),
|
|
3717
|
-
meanRect(x1 - band * 2, yA, x1 - band, yB),
|
|
3718
|
-
meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
|
|
3719
|
-
])
|
|
3720
|
-
);
|
|
3721
|
-
}
|
|
3722
|
-
if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
|
|
3723
|
-
const y1 = card.y + card.h;
|
|
3724
|
-
const xA = card.x + card.w * 0.2;
|
|
3725
|
-
const xB = card.x + card.w * 0.8;
|
|
3726
|
-
readings.push(
|
|
3727
|
-
rise([
|
|
3728
|
-
meanRect(xA, y1 - band, xB, y1),
|
|
3729
|
-
meanRect(xA, y1 - band * 2, xB, y1 - band),
|
|
3730
|
-
meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
|
|
3731
|
-
])
|
|
3732
|
-
);
|
|
3733
|
-
}
|
|
3734
|
-
return readings.length ? Math.max(...readings) : null;
|
|
3735
|
-
}
|
|
3736
|
-
function edgeContrast(img, ground, card) {
|
|
3737
|
-
const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
|
|
3738
|
-
const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
|
|
3739
|
-
const col = (x) => {
|
|
3740
|
-
if (x < 0 || x >= img.w) return 0;
|
|
3741
|
-
const ds = [];
|
|
3742
|
-
const step = Math.max(1, Math.round((yB - yA) / 80));
|
|
3743
|
-
for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
|
|
3744
|
-
return median(ds);
|
|
3745
|
-
};
|
|
3746
|
-
const depth = Math.min(24, Math.floor(card.w / 8));
|
|
3747
|
-
let best = 0;
|
|
3748
|
-
for (let d = 0; d < depth; d++) {
|
|
3749
|
-
best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
|
|
3750
|
-
}
|
|
3751
|
-
return best;
|
|
3752
|
-
}
|
|
3753
|
-
function inkCoverage(img, rect, threshold = 48) {
|
|
3754
|
-
const x0 = Math.max(0, Math.floor(rect.x));
|
|
3755
|
-
const y0 = Math.max(0, Math.floor(rect.y));
|
|
3756
|
-
const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
|
|
3757
|
-
const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
|
|
3758
|
-
if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
|
|
3759
|
-
const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
|
|
3760
|
-
const r = [];
|
|
3761
|
-
const g = [];
|
|
3762
|
-
const b = [];
|
|
3763
|
-
const pts = [];
|
|
3764
|
-
for (let y = y0; y < y1; y += step) {
|
|
3765
|
-
for (let x = x0; x < x1; x += step) {
|
|
3766
|
-
const p = at(img, x, y);
|
|
3767
|
-
pts.push(p);
|
|
3768
|
-
r.push(p[0]);
|
|
3769
|
-
g.push(p[1]);
|
|
3770
|
-
b.push(p[2]);
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
const base = [median(r), median(g), median(b)];
|
|
3774
|
-
let ink = 0;
|
|
3775
|
-
for (const p of pts) if (delta(p, base) > threshold) ink++;
|
|
3776
|
-
return pts.length ? ink / pts.length : 0;
|
|
3777
|
-
}
|
|
3778
|
-
function differenceHash(img, rect) {
|
|
3779
|
-
const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
3780
|
-
const cols = 9;
|
|
3781
|
-
const rows = 8;
|
|
3782
|
-
const cell = [];
|
|
3783
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3784
|
-
for (let cx = 0; cx < cols; cx++) {
|
|
3785
|
-
const x0 = Math.floor(r.x + cx * r.w / cols);
|
|
3786
|
-
const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
|
|
3787
|
-
const y0 = Math.floor(r.y + cy * r.h / rows);
|
|
3788
|
-
const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
|
|
3789
|
-
let sum = 0;
|
|
3790
|
-
let n = 0;
|
|
3791
|
-
const sx = Math.max(1, Math.floor((x1 - x0) / 8));
|
|
3792
|
-
const sy = Math.max(1, Math.floor((y1 - y0) / 8));
|
|
3793
|
-
for (let y = y0; y < y1 && y < img.h; y += sy) {
|
|
3794
|
-
for (let x = x0; x < x1 && x < img.w; x += sx) {
|
|
3795
|
-
const p = at(img, Math.max(0, x), Math.max(0, y));
|
|
3796
|
-
sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
|
|
3797
|
-
n++;
|
|
3798
|
-
}
|
|
3799
|
-
}
|
|
3800
|
-
cell.push(n ? sum / n : 0);
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
let bits = "";
|
|
3804
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3805
|
-
let byte = 0;
|
|
3806
|
-
for (let cx = 0; cx < cols - 1; cx++) {
|
|
3807
|
-
const l = cell[cy * cols + cx];
|
|
3808
|
-
const rr = cell[cy * cols + cx + 1];
|
|
3809
|
-
byte = byte << 1 | (l > rr ? 1 : 0);
|
|
3810
|
-
}
|
|
3811
|
-
bits += byte.toString(16).padStart(2, "0");
|
|
3812
|
-
}
|
|
3813
|
-
return bits;
|
|
3814
|
-
}
|
|
3815
|
-
function hammingDistance(a, b) {
|
|
3816
|
-
let d = 0;
|
|
3817
|
-
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
3818
|
-
let x = parseInt(a[i], 16) ^ parseInt(b[i], 16);
|
|
3819
|
-
while (x) {
|
|
3820
|
-
d += x & 1;
|
|
3821
|
-
x >>= 1;
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
|
-
return d + Math.abs(a.length - b.length) * 4;
|
|
3825
|
-
}
|
|
3826
|
-
function luminance(c) {
|
|
3827
|
-
const lin = (v) => {
|
|
3828
|
-
const s = v / 255;
|
|
3829
|
-
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
3830
|
-
};
|
|
3831
|
-
return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
|
|
3832
|
-
}
|
|
3833
|
-
function lightness(c) {
|
|
3834
|
-
const y = luminance(c);
|
|
3835
|
-
return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
|
|
3836
|
-
}
|
|
3837
|
-
function medianColour(img, rect) {
|
|
3838
|
-
const r = [];
|
|
3839
|
-
const g = [];
|
|
3840
|
-
const b = [];
|
|
3841
|
-
const x0 = Math.max(0, Math.floor(rect.x));
|
|
3842
|
-
const y0 = Math.max(0, Math.floor(rect.y));
|
|
3843
|
-
const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
|
|
3844
|
-
const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
|
|
3845
|
-
const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
|
|
3846
|
-
for (let y = y0; y < y1; y += step) {
|
|
3847
|
-
for (let x = x0; x < x1; x += step) {
|
|
3848
|
-
const p = at(img, x, y);
|
|
3849
|
-
r.push(p[0]);
|
|
3850
|
-
g.push(p[1]);
|
|
3851
|
-
b.push(p[2]);
|
|
3852
|
-
}
|
|
3853
|
-
}
|
|
3854
|
-
return [median(r), median(g), median(b)];
|
|
3855
|
-
}
|
|
3856
|
-
function edgeEnergy(img, width) {
|
|
3857
|
-
const cols = Math.max(2, Math.min(width, img.w));
|
|
3858
|
-
const rows = Math.max(2, Math.round(cols * img.h / img.w));
|
|
3859
|
-
const lum = [];
|
|
3860
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3861
|
-
for (let cx = 0; cx < cols; cx++) {
|
|
3862
|
-
const x = Math.min(img.w - 1, Math.floor((cx + 0.5) * img.w / cols));
|
|
3863
|
-
const y = Math.min(img.h - 1, Math.floor((cy + 0.5) * img.h / rows));
|
|
3864
|
-
const p = at(img, x, y);
|
|
3865
|
-
lum.push(0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]);
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
let sum = 0;
|
|
3869
|
-
let n = 0;
|
|
3870
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3871
|
-
for (let cx = 1; cx < cols; cx++) {
|
|
3872
|
-
sum += Math.abs(lum[cy * cols + cx] - lum[cy * cols + cx - 1]);
|
|
3873
|
-
n++;
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
return n ? sum / n : 0;
|
|
3877
|
-
}
|
|
3878
|
-
function measureStill(img) {
|
|
3879
|
-
const g = groundColour(img);
|
|
3880
|
-
const strong = cardBounds(img, g, 60);
|
|
3881
|
-
const faint = cardBounds(img, g, 12) ?? strong;
|
|
3882
|
-
const card = faint;
|
|
3883
|
-
const bleed = [];
|
|
3884
|
-
let pad = null;
|
|
3885
|
-
let widthPct = null;
|
|
3886
|
-
let shadow = null;
|
|
3887
|
-
let edge = 0;
|
|
3888
|
-
let separation = null;
|
|
3889
|
-
if (card) {
|
|
3890
|
-
pad = {
|
|
3891
|
-
left: card.x / img.w,
|
|
3892
|
-
right: (img.w - card.x - card.w) / img.w,
|
|
3893
|
-
top: card.y / img.h,
|
|
3894
|
-
bottom: (img.h - card.y - card.h) / img.h
|
|
3895
|
-
};
|
|
3896
|
-
for (const side of ["left", "right", "top", "bottom"])
|
|
3897
|
-
if (pad[side] < 5e-3) bleed.push(side);
|
|
3898
|
-
widthPct = card.w / img.w;
|
|
3899
|
-
shadow = haloReading(img, g, card);
|
|
3900
|
-
edge = edgeContrast(img, g, card);
|
|
3901
|
-
const body = strong ?? card;
|
|
3902
|
-
const inner = {
|
|
3903
|
-
x: body.x + body.w * 0.1,
|
|
3904
|
-
y: body.y + body.h * 0.1,
|
|
3905
|
-
w: body.w * 0.8,
|
|
3906
|
-
h: body.h * 0.8
|
|
3907
|
-
};
|
|
3908
|
-
separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
|
|
3909
|
-
}
|
|
3910
|
-
const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
3911
|
-
return {
|
|
3912
|
-
w: img.w,
|
|
3913
|
-
h: img.h,
|
|
3914
|
-
ground: hex(g),
|
|
3915
|
-
card,
|
|
3916
|
-
widthPct,
|
|
3917
|
-
pad,
|
|
3918
|
-
bleed,
|
|
3919
|
-
shadow,
|
|
3920
|
-
edge,
|
|
3921
|
-
ink: inkCoverage(img, inkRect),
|
|
3922
|
-
separation,
|
|
3923
|
-
hash: differenceHash(img, inkRect)
|
|
3924
|
-
};
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
|
-
// src/plugin/moments.ts
|
|
3928
|
-
var STEP_SETTLE_SECONDS = 0.4;
|
|
3929
|
-
var NOT_A_GESTURE = /* @__PURE__ */ new Set(["wait", "goto", "sleep"]);
|
|
3930
|
-
function momentCandidates(doc, duration) {
|
|
3931
|
-
const rated = ratedSegments3(doc);
|
|
3932
|
-
const out = [];
|
|
3933
|
-
const push = (c) => {
|
|
3934
|
-
if (c.time < 0 || c.time > duration) return;
|
|
3935
|
-
if (out.some((o) => Math.abs(o.time - c.time) < 0.25)) return;
|
|
3936
|
-
out.push(c);
|
|
3937
|
-
};
|
|
3938
|
-
const steps = doc.source.meta.steps ?? [];
|
|
3939
|
-
for (let i = 0; i < steps.length; i++) {
|
|
3940
|
-
const s = steps[i];
|
|
3941
|
-
if (s.skipped || NOT_A_GESTURE.has(s.do)) continue;
|
|
3942
|
-
const t = stepOutputTime(rated, s, settleAfter(steps, i));
|
|
3943
|
-
if (t === null) continue;
|
|
3944
|
-
push({ time: t, source: "step", step: s.id ?? s.step });
|
|
3945
|
-
}
|
|
3946
|
-
let dropped = 0;
|
|
3947
|
-
const apexes = [];
|
|
3948
|
-
for (const z of doc.zoom) {
|
|
3949
|
-
const ext = spanOutputExtent3(rated, z.in, z.out);
|
|
3950
|
-
if (ext) apexes.push((ext.start + ext.end) / 2);
|
|
3951
|
-
else dropped++;
|
|
3952
|
-
}
|
|
3953
|
-
for (const t of apexes.sort((a, b) => a - b))
|
|
3954
|
-
push({ time: t, source: "zoom" });
|
|
3955
|
-
if (!out.length) {
|
|
3956
|
-
for (const p of [0.1, 0.3, 0.5, 0.7, 0.9])
|
|
3957
|
-
push({ time: p * duration, source: "spread" });
|
|
3958
|
-
}
|
|
3959
|
-
return { candidates: out, dropped };
|
|
3960
|
-
}
|
|
3961
|
-
function settleAfter(steps, i) {
|
|
3962
|
-
const next = steps[i + 1];
|
|
3963
|
-
if (next && next.do === "wait" && !next.skipped)
|
|
3964
|
-
return Math.max(0, next.tEnd - steps[i].tEnd);
|
|
3965
|
-
return STEP_SETTLE_SECONDS;
|
|
3966
|
-
}
|
|
3967
|
-
function stepOutputTime(rated, step, settle) {
|
|
3968
|
-
const src = step.tEnd + settle;
|
|
3969
|
-
const ext = spanOutputExtent3(rated, src, src + 1e-3);
|
|
3970
|
-
if (!ext) return null;
|
|
3971
|
-
return ext.start;
|
|
3972
|
-
}
|
|
3973
|
-
function resolveStepTime(doc, entry) {
|
|
3974
|
-
const m = /^step:([^+-]+)([+-]\d+(?:\.\d+)?)?$/.exec(entry.trim());
|
|
3975
|
-
if (!m) return null;
|
|
3976
|
-
const key = m[1];
|
|
3977
|
-
const offset = m[2] ? Number(m[2]) : STEP_SETTLE_SECONDS;
|
|
3978
|
-
const steps = doc.source.meta.steps ?? [];
|
|
3979
|
-
const step = steps.find((s) => s.id === key) ?? (/^\d+$/.test(key) ? steps.find((s) => s.step === Number(key)) : void 0);
|
|
3980
|
-
if (!step) {
|
|
3981
|
-
const known = steps.map((s) => s.id ?? String(s.step)).filter((s) => s.length);
|
|
3982
|
-
throw new Error(
|
|
3983
|
-
`--times ${entry}: no step "${key}" in the take's step timeline${known.length ? ` (steps: ${known.join(", ")})` : " (the take carries none: record it with the current CLI)"}`
|
|
3984
|
-
);
|
|
3985
|
-
}
|
|
3986
|
-
const t = stepOutputTime(ratedSegments3(doc), step, offset);
|
|
3987
|
-
if (t === null)
|
|
3988
|
-
throw new Error(
|
|
3989
|
-
`--times ${entry}: step "${key}" (${step.tEnd.toFixed(2)}s in the recording) falls outside the cut`
|
|
3990
|
-
);
|
|
3991
|
-
return t;
|
|
3992
|
-
}
|
|
3993
|
-
var DUPLICATE_BITS = 6;
|
|
3994
|
-
var POPULATED_INK = 0.12;
|
|
3995
|
-
function pickMoments(measured, opts = {}) {
|
|
3996
|
-
const minInk = opts.minInk ?? 0.06;
|
|
3997
|
-
const relative4 = opts.relativeInk ?? 0.4;
|
|
3998
|
-
const bits = opts.duplicateBits ?? DUPLICATE_BITS;
|
|
3999
|
-
const inks = measured.map((m) => m.ink).sort((a, b) => a - b);
|
|
4000
|
-
const median2 = inks[inks.length >> 1] ?? 0;
|
|
4001
|
-
const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative4));
|
|
4002
|
-
const kept = [];
|
|
4003
|
-
const dropped = [];
|
|
4004
|
-
for (const m of measured) {
|
|
4005
|
-
if (m.ink < floor) {
|
|
4006
|
-
dropped.push(
|
|
4007
|
-
`blank at ${m.time.toFixed(2)}s: ${Math.round(m.ink * 100)}% ink (the floor is ${Math.round(floor * 100)}%; the take's median is ${Math.round(median2 * 100)}%)`
|
|
4008
|
-
);
|
|
4009
|
-
continue;
|
|
4010
|
-
}
|
|
4011
|
-
const twin = kept.find((k) => hammingDistance(k.hash, m.hash) <= bits);
|
|
4012
|
-
if (twin) {
|
|
4013
|
-
dropped.push(
|
|
4014
|
-
`${m.time.toFixed(2)}s is the same frame as ${twin.time.toFixed(2)}s`
|
|
4015
|
-
);
|
|
4016
|
-
continue;
|
|
4017
|
-
}
|
|
4018
|
-
kept.push(m);
|
|
4019
|
-
}
|
|
4020
|
-
if (!kept.length && measured.length) {
|
|
4021
|
-
const best = [...measured].sort((a, b) => b.ink - a.ink)[0];
|
|
4022
|
-
kept.push(best);
|
|
4023
|
-
dropped.push(
|
|
4024
|
-
`every candidate is under the blank floor; kept ${best.time.toFixed(2)}s (${Math.round(best.ink * 100)}% ink) as the least empty`
|
|
4025
|
-
);
|
|
4026
|
-
}
|
|
4027
|
-
return { times: kept.map((k) => k.time), dropped };
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
// src/plugin/motionPlan.ts
|
|
4031
|
-
import {
|
|
4032
|
-
END_CARD_FROM,
|
|
4033
|
-
END_CARD_SECONDS,
|
|
4034
|
-
applyTemplate,
|
|
4035
|
-
docOutputDuration,
|
|
4036
|
-
dropTemplate,
|
|
4037
|
-
endCardClips,
|
|
4038
|
-
migrateMotion,
|
|
4039
|
-
ratedSegments as ratedSegments4,
|
|
4040
|
-
sameMedia,
|
|
4041
|
-
spanOutputExtent as spanOutputExtent4
|
|
4042
|
-
} from "@vosjs/studio-core";
|
|
4043
|
-
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
4044
|
-
"x-feed-cut",
|
|
4045
|
-
"youtube-main-demo",
|
|
4046
|
-
"shorts-linkedin-vertical-cut"
|
|
4047
|
-
]);
|
|
4048
|
-
var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
4049
|
-
var BED_ID = "bed";
|
|
4050
|
-
var CLICK_ID_PREFIX = "click-";
|
|
4051
|
-
var CAPTION_ID_PREFIX = "caption-";
|
|
4052
|
-
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
4053
|
-
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4054
|
-
var TRANSITION_CLIP_MIN = 1;
|
|
4055
|
-
function proposeTransitions(input, kind) {
|
|
4056
|
-
const doc = structuredClone(input);
|
|
4057
|
-
const skipped = [];
|
|
4058
|
-
let count = 0;
|
|
4059
|
-
const steps = doc.source.meta.steps ?? [];
|
|
4060
|
-
for (const step of steps) {
|
|
4061
|
-
if (!step.navigated || step.skipped) continue;
|
|
4062
|
-
const name = step.id ?? String(step.step);
|
|
4063
|
-
const wait = step.do === "wait";
|
|
4064
|
-
const outEnd = round3(wait ? step.tStart : step.tEnd);
|
|
4065
|
-
const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
|
|
4066
|
-
const segs = doc.segments;
|
|
4067
|
-
if (segs.some(
|
|
4068
|
-
(s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
|
|
4069
|
-
))
|
|
4070
|
-
continue;
|
|
4071
|
-
const i = segs.findIndex(
|
|
4072
|
-
(s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
|
|
4073
|
-
);
|
|
4074
|
-
if (i < 0) {
|
|
4075
|
-
skipped.push(
|
|
4076
|
-
`no transition at step ${name}: the page change sits too close to a clip's edge`
|
|
4077
|
-
);
|
|
4078
|
-
continue;
|
|
4079
|
-
}
|
|
4080
|
-
const seg = segs[i];
|
|
4081
|
-
const before = { ...seg, out: outEnd, anim: { exit: kind } };
|
|
4082
|
-
const after = { ...seg, in: inStart, anim: { enter: kind } };
|
|
4083
|
-
if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
|
|
4084
|
-
if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
|
|
4085
|
-
doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
|
|
4086
|
-
count++;
|
|
4087
|
-
}
|
|
4088
|
-
return { doc, count, skipped };
|
|
4089
|
-
}
|
|
4090
|
-
function pickTrack(catalog, ask) {
|
|
4091
|
-
if (!catalog || !ask || off(ask)) return null;
|
|
4092
|
-
const want = ask.trim().toLowerCase();
|
|
4093
|
-
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
4094
|
-
}
|
|
4095
|
-
function clickTimes(doc, range) {
|
|
4096
|
-
const rated = ratedSegments4(doc);
|
|
4097
|
-
const out = [];
|
|
4098
|
-
for (const e of doc.source.cursor) {
|
|
4099
|
-
if (e.type !== "down") continue;
|
|
4100
|
-
const src = e.t / 1e3;
|
|
4101
|
-
const ext = spanOutputExtent4(rated, src, src + 1e-3);
|
|
4102
|
-
if (!ext) continue;
|
|
4103
|
-
const t = ext.start;
|
|
4104
|
-
if (t < range[0] || t > range[1]) continue;
|
|
4105
|
-
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
4106
|
-
out.push(+(t - range[0]).toFixed(3));
|
|
4107
|
-
}
|
|
4108
|
-
return out;
|
|
4109
|
-
}
|
|
4110
|
-
var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
|
|
4111
|
-
const rate = s.rate && s.rate > 0 ? s.rate : 1;
|
|
4112
|
-
return acc + (s.out - s.in) / rate;
|
|
4113
|
-
}, 0);
|
|
4114
|
-
function templateWords(words2) {
|
|
4115
|
-
const headline = (words2.headline ?? "").trim();
|
|
4116
|
-
const kicker = (words2.kicker ?? "").trim();
|
|
4117
|
-
const brand = (words2.brand ?? "").trim();
|
|
4118
|
-
const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
|
|
4119
|
-
const out = {};
|
|
4120
|
-
if (headline) {
|
|
4121
|
-
out["stage-title"] = headline;
|
|
4122
|
-
out["endcard-title"] = headline;
|
|
4123
|
-
}
|
|
4124
|
-
if (kicker) out["stage-kicker"] = kicker;
|
|
4125
|
-
if (brand) {
|
|
4126
|
-
out["stage-brand"] = brand;
|
|
4127
|
-
out["endcard-mark"] = brand;
|
|
4128
|
-
}
|
|
4129
|
-
if (sub && sub !== headline) out["endcard-sub"] = sub;
|
|
4130
|
-
return out;
|
|
3631
|
+
if (kicker) out["stage-kicker"] = kicker;
|
|
3632
|
+
if (brand) {
|
|
3633
|
+
out["stage-brand"] = brand;
|
|
3634
|
+
out["endcard-mark"] = brand;
|
|
3635
|
+
}
|
|
3636
|
+
if (sub && sub !== headline) out["endcard-sub"] = sub;
|
|
3637
|
+
return out;
|
|
4131
3638
|
}
|
|
4132
3639
|
function resolveAnchor(doc, at2) {
|
|
4133
3640
|
if (typeof at2 !== "object") return at2;
|
|
@@ -4328,7 +3835,6 @@ function destinationMechanics(d, doc) {
|
|
|
4328
3835
|
const set = [];
|
|
4329
3836
|
const unset = [];
|
|
4330
3837
|
const notes = [];
|
|
4331
|
-
if (d.kind !== "video") return { set, unset, notes };
|
|
4332
3838
|
const loop = LOOP_DESTINATIONS.has(d.id);
|
|
4333
3839
|
const sound = SOUND_DESTINATIONS.has(d.id);
|
|
4334
3840
|
const portrait = d.px.w / d.px.h < 0.9;
|
|
@@ -4380,161 +3886,12 @@ function isLightHexGround(hex2) {
|
|
|
4380
3886
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
|
|
4381
3887
|
}
|
|
4382
3888
|
|
|
4383
|
-
// src/plugin/posterDoc.ts
|
|
4384
|
-
import { existsSync as existsSync4 } from "fs";
|
|
4385
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
4386
|
-
import { isAbsolute, join as join4, resolve as resolve3 } from "path";
|
|
4387
|
-
import {
|
|
4388
|
-
computeCardLayout,
|
|
4389
|
-
docStillTime as docStillTime2,
|
|
4390
|
-
migrateHostedDoc,
|
|
4391
|
-
overlayPlainText,
|
|
4392
|
-
overlayRect,
|
|
4393
|
-
resolveOverlayStyle as resolveOverlayStyle2
|
|
4394
|
-
} from "@vosjs/studio-core";
|
|
4395
|
-
var POSTER_CLASSES = [
|
|
4396
|
-
"landscape",
|
|
4397
|
-
"square",
|
|
4398
|
-
"portrait",
|
|
4399
|
-
"tile"
|
|
4400
|
-
];
|
|
4401
|
-
var TILE_MAX_PX = 700;
|
|
4402
|
-
function posterClassFor(px) {
|
|
4403
|
-
if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
|
|
4404
|
-
const r = px.w / Math.max(1, px.h);
|
|
4405
|
-
if (r < 0.87) return "portrait";
|
|
4406
|
-
if (r <= 1.15) return "square";
|
|
4407
|
-
return "landscape";
|
|
4408
|
-
}
|
|
4409
|
-
var ROLE_ALL = "poster";
|
|
4410
|
-
var roleFor = (cls) => `poster-${cls}`;
|
|
4411
|
-
async function readPosterAt(cls, path, takeDir, from) {
|
|
4412
|
-
let file = path;
|
|
4413
|
-
let docDir = path;
|
|
4414
|
-
if (existsSync4(join4(path, "doc.json"))) {
|
|
4415
|
-
file = join4(path, "doc.json");
|
|
4416
|
-
} else if (!/\.json$/i.test(path) || !existsSync4(path)) {
|
|
4417
|
-
return null;
|
|
4418
|
-
} else {
|
|
4419
|
-
docDir = resolve3(path, "..");
|
|
4420
|
-
}
|
|
4421
|
-
let raw;
|
|
4422
|
-
try {
|
|
4423
|
-
raw = JSON.parse(await readFile4(file, "utf8"));
|
|
4424
|
-
} catch {
|
|
4425
|
-
return null;
|
|
4426
|
-
}
|
|
4427
|
-
if (!raw || typeof raw !== "object") return null;
|
|
4428
|
-
const source = raw.source;
|
|
4429
|
-
if (!source || typeof source.videoKey !== "string") return null;
|
|
4430
|
-
const doc = migrateHostedDoc(
|
|
4431
|
-
raw
|
|
4432
|
-
);
|
|
4433
|
-
const ownFootage = existsSync4(join4(docDir, "meta.json")) && existsSync4(join4(docDir, source.videoKey));
|
|
4434
|
-
const sync = readSyncState(docDir);
|
|
4435
|
-
return {
|
|
4436
|
-
cls,
|
|
4437
|
-
file,
|
|
4438
|
-
takeDir: ownFootage ? docDir : takeDir,
|
|
4439
|
-
ownFootage,
|
|
4440
|
-
doc,
|
|
4441
|
-
...sync?.vosId ? { vosId: sync.vosId } : {},
|
|
4442
|
-
from
|
|
4443
|
-
};
|
|
4444
|
-
}
|
|
4445
|
-
async function findPosterDocs(takeDir, launchRoles) {
|
|
4446
|
-
const out = {};
|
|
4447
|
-
const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
|
|
4448
|
-
for (const cls of POSTER_CLASSES) {
|
|
4449
|
-
const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
|
|
4450
|
-
const candidates = [];
|
|
4451
|
-
if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
|
|
4452
|
-
const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
|
|
4453
|
-
candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
|
|
4454
|
-
}
|
|
4455
|
-
candidates.push({
|
|
4456
|
-
path: join4(takeDir, "poster", cls),
|
|
4457
|
-
from: `poster/${cls}`
|
|
4458
|
-
});
|
|
4459
|
-
candidates.push({ path: join4(takeDir, "poster"), from: "poster" });
|
|
4460
|
-
for (const c of candidates) {
|
|
4461
|
-
const ref = await readPosterAt(cls, c.path, takeDir, c.from);
|
|
4462
|
-
if (ref) {
|
|
4463
|
-
out[cls] = ref;
|
|
4464
|
-
break;
|
|
4465
|
-
}
|
|
4466
|
-
}
|
|
4467
|
-
}
|
|
4468
|
-
return out;
|
|
4469
|
-
}
|
|
4470
|
-
function posterStillTime(doc, duration) {
|
|
4471
|
-
return Math.max(0, Math.min(duration, docStillTime2(doc)));
|
|
4472
|
-
}
|
|
4473
|
-
var DESIGN_H = 1080;
|
|
4474
|
-
function designFrame(px) {
|
|
4475
|
-
return {
|
|
4476
|
-
W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
|
|
4477
|
-
H: DESIGN_H
|
|
4478
|
-
};
|
|
4479
|
-
}
|
|
4480
|
-
function posterShotRect(doc, px) {
|
|
4481
|
-
const { W, H } = designFrame(px);
|
|
4482
|
-
const meta = doc.source.meta;
|
|
4483
|
-
const layout = computeCardLayout(
|
|
4484
|
-
doc.frame,
|
|
4485
|
-
{
|
|
4486
|
-
width: meta.captureWidth ?? meta.width,
|
|
4487
|
-
height: meta.captureHeight ?? meta.height
|
|
4488
|
-
},
|
|
4489
|
-
W,
|
|
4490
|
-
H
|
|
4491
|
-
);
|
|
4492
|
-
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4493
|
-
return {
|
|
4494
|
-
x: r3(layout.cardX / W),
|
|
4495
|
-
y: r3(layout.cardY / H),
|
|
4496
|
-
w: r3(layout.cardW / W),
|
|
4497
|
-
h: r3(layout.cardH / H)
|
|
4498
|
-
};
|
|
4499
|
-
}
|
|
4500
|
-
function estimateWidth(text, font, letterSpacingPx = 0) {
|
|
4501
|
-
const m = /(\d+(?:\.\d+)?)px/.exec(font);
|
|
4502
|
-
const px = m ? Number(m[1]) : 32;
|
|
4503
|
-
return text.length * (px * 0.55 + letterSpacingPx);
|
|
4504
|
-
}
|
|
4505
|
-
function posterTextBoxes(doc, px, time) {
|
|
4506
|
-
const { W, H } = designFrame(px);
|
|
4507
|
-
const out = [];
|
|
4508
|
-
for (const clip of doc.overlays ?? []) {
|
|
4509
|
-
if (clip.kind !== "text") continue;
|
|
4510
|
-
if (time < clip.start || time > clip.start + clip.duration) continue;
|
|
4511
|
-
const rect = overlayRect(clip, estimateWidth, W, H);
|
|
4512
|
-
const style = resolveOverlayStyle2(clip);
|
|
4513
|
-
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4514
|
-
out.push({
|
|
4515
|
-
x: r3((rect.cx - rect.w / 2) / W),
|
|
4516
|
-
y: r3((rect.cy - rect.h / 2) / H),
|
|
4517
|
-
w: r3(rect.w / W),
|
|
4518
|
-
h: r3(rect.h / H),
|
|
4519
|
-
role: clip.preset === "title" ? "headline" : "body",
|
|
4520
|
-
label: overlayPlainText(clip).split("\n")[0].slice(0, 40),
|
|
4521
|
-
.../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
|
|
4522
|
-
});
|
|
4523
|
-
}
|
|
4524
|
-
return out;
|
|
4525
|
-
}
|
|
4526
|
-
|
|
4527
3889
|
// src/plugin/deliver.ts
|
|
4528
3890
|
var CHANNEL_ALIASES = {
|
|
4529
|
-
ph: "producthunt",
|
|
4530
3891
|
yt: "youtube",
|
|
4531
3892
|
gh: "github",
|
|
4532
|
-
li: "linkedin",
|
|
4533
3893
|
shorts: "shorts-linkedin"
|
|
4534
3894
|
};
|
|
4535
|
-
var NOT_FROM_FOOTAGE = {
|
|
4536
|
-
"cws-icon": "an icon is brand art, not footage \u2014 supply it from the kit"
|
|
4537
|
-
};
|
|
4538
3895
|
function resolveChannels(raw) {
|
|
4539
3896
|
const known = [...new Set(DESTINATIONS.map((d) => d.channel))];
|
|
4540
3897
|
const wanted = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
@@ -4545,26 +3902,26 @@ function resolveChannels(raw) {
|
|
|
4545
3902
|
const id = CHANNEL_ALIASES[w] ?? w;
|
|
4546
3903
|
if (!known.includes(id))
|
|
4547
3904
|
throw new Error(
|
|
4548
|
-
`unknown channel "${w}" \u2014
|
|
3905
|
+
`unknown channel "${w}" \u2014 deliver renders video cuts for: ${known.join(", ")} (or all)`
|
|
4549
3906
|
);
|
|
4550
3907
|
if (!out.includes(id)) out.push(id);
|
|
4551
3908
|
}
|
|
4552
3909
|
return out;
|
|
4553
3910
|
}
|
|
4554
3911
|
async function readBrandBesideTake(dir, explicit) {
|
|
4555
|
-
const candidates = explicit ? [explicit] : [
|
|
3912
|
+
const candidates = explicit ? [explicit] : [join4(dir, "BRAND.md"), join4(dir, "..", "BRAND.md")];
|
|
4556
3913
|
for (const file of candidates) {
|
|
4557
|
-
if (!
|
|
4558
|
-
const roles = parseFrontmatter(await
|
|
3914
|
+
if (!existsSync4(file)) continue;
|
|
3915
|
+
const roles = parseFrontmatter(await readFile4(file, "utf8"));
|
|
4559
3916
|
return { file, roles };
|
|
4560
3917
|
}
|
|
4561
3918
|
return null;
|
|
4562
3919
|
}
|
|
4563
3920
|
async function readLaunchBesideTake(dir, explicit) {
|
|
4564
|
-
const candidates = explicit ? [explicit] : [
|
|
3921
|
+
const candidates = explicit ? [explicit] : [join4(dir, "LAUNCH.md"), join4(dir, "..", "LAUNCH.md")];
|
|
4565
3922
|
for (const file of candidates) {
|
|
4566
|
-
if (!
|
|
4567
|
-
return { file, roles: parseFrontmatter(await
|
|
3923
|
+
if (!existsSync4(file)) continue;
|
|
3924
|
+
return { file, roles: parseFrontmatter(await readFile4(file, "utf8")) };
|
|
4568
3925
|
}
|
|
4569
3926
|
return null;
|
|
4570
3927
|
}
|
|
@@ -4589,7 +3946,7 @@ async function resolveLook(dir, opts) {
|
|
|
4589
3946
|
roles
|
|
4590
3947
|
};
|
|
4591
3948
|
}
|
|
4592
|
-
function lookOverrides(look, placement, size, video, opts) {
|
|
3949
|
+
function lookOverrides(look, placement, size, video, opts = {}) {
|
|
4593
3950
|
const inset = cardInset(look, size, video, placement);
|
|
4594
3951
|
const set = [
|
|
4595
3952
|
"frame.fit=contain",
|
|
@@ -4606,70 +3963,8 @@ function lookOverrides(look, placement, size, video, opts) {
|
|
|
4606
3963
|
set.push(`frame.borderColor=${look.borderColor ?? "#000000"}`);
|
|
4607
3964
|
}
|
|
4608
3965
|
if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
|
|
4609
|
-
if (opts.still) {
|
|
4610
|
-
set.push(
|
|
4611
|
-
"zoom=[]",
|
|
4612
|
-
"tilt=[]",
|
|
4613
|
-
"cursor.visible=false",
|
|
4614
|
-
"cursor.clickFx.style=none"
|
|
4615
|
-
);
|
|
4616
|
-
}
|
|
4617
3966
|
return set;
|
|
4618
3967
|
}
|
|
4619
|
-
var PROBE_WIDTH = 640;
|
|
4620
|
-
async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
4621
|
-
const { candidates, dropped } = momentCandidates(doc, duration);
|
|
4622
|
-
const notes = [];
|
|
4623
|
-
if (dropped > 0)
|
|
4624
|
-
notes.push(
|
|
4625
|
-
`${dropped} of ${doc.zoom.length} zoom apex(es) fall outside the cut`
|
|
4626
|
-
);
|
|
4627
|
-
if (!candidates.length) return { times: [], moments: [], notes };
|
|
4628
|
-
const meta = doc.source.meta;
|
|
4629
|
-
const vw = meta.captureWidth ?? meta.width;
|
|
4630
|
-
const vh = meta.captureHeight ?? meta.height;
|
|
4631
|
-
const probeDir = await mkdtemp(join5(tmpdir(), "vos-moments-"));
|
|
4632
|
-
try {
|
|
4633
|
-
const probe3 = await framesTake(browser, dir, {
|
|
4634
|
-
times: candidates.map((c) => c.time),
|
|
4635
|
-
width: PROBE_WIDTH,
|
|
4636
|
-
height: Math.max(2, Math.round(PROBE_WIDTH * vh / vw / 2) * 2),
|
|
4637
|
-
outDir: probeDir,
|
|
4638
|
-
overrides: {
|
|
4639
|
-
...opts.overrides,
|
|
4640
|
-
set: [...SCREENSHOT_DEFAULTS, ...opts.overrides?.set ?? []]
|
|
4641
|
-
}
|
|
4642
|
-
});
|
|
4643
|
-
const byTime = /* @__PURE__ */ new Map();
|
|
4644
|
-
for (const frame of probe3.frames) {
|
|
4645
|
-
const img = decodePng(new Uint8Array(await readFile5(frame.file)));
|
|
4646
|
-
if (!img) continue;
|
|
4647
|
-
const whole = { x: 0, y: 0, w: img.w, h: img.h };
|
|
4648
|
-
byTime.set(+frame.time.toFixed(3), {
|
|
4649
|
-
ink: inkCoverage(img, whole),
|
|
4650
|
-
hash: differenceHash(img, whole)
|
|
4651
|
-
});
|
|
4652
|
-
}
|
|
4653
|
-
const measured = [];
|
|
4654
|
-
for (const c of candidates) {
|
|
4655
|
-
const m = byTime.get(+c.time.toFixed(3));
|
|
4656
|
-
if (m) measured.push({ time: c.time, ...m });
|
|
4657
|
-
}
|
|
4658
|
-
const pick = pickMoments(measured);
|
|
4659
|
-
const kept = candidates.filter((c) => pick.times.includes(c.time));
|
|
4660
|
-
const bySource = (s) => candidates.filter((c) => c.source === s).length;
|
|
4661
|
-
notes.push(
|
|
4662
|
-
`${candidates.length} candidate(s): ${bySource("step")} from steps, ${bySource("zoom")} from zoom apexes, ${bySource("spread")} from the spread; ${kept.length} kept`
|
|
4663
|
-
);
|
|
4664
|
-
return {
|
|
4665
|
-
times: pick.times,
|
|
4666
|
-
moments: kept,
|
|
4667
|
-
notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
|
|
4668
|
-
};
|
|
4669
|
-
} finally {
|
|
4670
|
-
await rm2(probeDir, { recursive: true, force: true });
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
3968
|
var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
|
|
4674
3969
|
var specWords = (d) => {
|
|
4675
3970
|
const parts = [`${d.px.w}x${d.px.h}`];
|
|
@@ -4679,62 +3974,17 @@ var specWords = (d) => {
|
|
|
4679
3974
|
parts.push(`\u2264${(d.maxBytes / 1024 / 1024).toFixed(0)}MB`);
|
|
4680
3975
|
return parts.join(" ");
|
|
4681
3976
|
};
|
|
4682
|
-
var FULL_BLEED = [
|
|
4683
|
-
"frame.padding=0",
|
|
4684
|
-
"frame.radius=0",
|
|
4685
|
-
"frame.shadow=0",
|
|
4686
|
-
"frame.border=0",
|
|
4687
|
-
"frame.browserBar.kind=none",
|
|
4688
|
-
"cursor.visible=false",
|
|
4689
|
-
"cursor.clickFx.style=none"
|
|
4690
|
-
];
|
|
4691
|
-
var SCREENSHOT_DEFAULTS = [...FULL_BLEED, "zoom=[]", "tilt=[]"];
|
|
4692
|
-
function stillOverridesFor(d, opts, video) {
|
|
4693
|
-
const set = [];
|
|
4694
|
-
if (d.genre === "screenshot" && !opts.composed) {
|
|
4695
|
-
if (d.fit === "cover") set.push("frame.fit=cover");
|
|
4696
|
-
set.push(...SCREENSHOT_DEFAULTS);
|
|
4697
|
-
} else if (d.genre === "card" && !opts.composed && opts.look && video && d.px) {
|
|
4698
|
-
set.push(
|
|
4699
|
-
...lookOverrides(opts.look, "card", d.px, video, {
|
|
4700
|
-
still: true,
|
|
4701
|
-
keepMedia: opts.overrides?.background !== void 0
|
|
4702
|
-
})
|
|
4703
|
-
);
|
|
4704
|
-
} else {
|
|
4705
|
-
if (d.fit === "cover") set.push("frame.fit=cover");
|
|
4706
|
-
if (d.genre === "card" && !opts.composed) set.push(...FULL_BLEED);
|
|
4707
|
-
}
|
|
4708
|
-
set.push(...opts.overrides?.set ?? []);
|
|
4709
|
-
if (!set.length) return opts.overrides;
|
|
4710
|
-
return { ...opts.overrides, set };
|
|
4711
|
-
}
|
|
4712
3977
|
async function deliverTake(browser, dir, opts) {
|
|
4713
3978
|
const take = await loadTake(dir);
|
|
4714
3979
|
if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
|
|
4715
3980
|
const doc = take.doc;
|
|
4716
3981
|
const duration = docOutputDuration2(doc);
|
|
4717
3982
|
const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
|
|
4718
|
-
const outDir =
|
|
3983
|
+
const outDir = resolve3(opts.outDir ?? join4(dir, "kit"));
|
|
4719
3984
|
await mkdir3(outDir, { recursive: true });
|
|
4720
3985
|
const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
|
|
4721
3986
|
const assets = [];
|
|
4722
3987
|
const skipped = [];
|
|
4723
|
-
let stillTimes;
|
|
4724
|
-
let moments;
|
|
4725
|
-
if (opts.times?.length) {
|
|
4726
|
-
stillTimes = opts.times;
|
|
4727
|
-
moments = opts.times.map((time) => ({ time, source: "times" }));
|
|
4728
|
-
} else {
|
|
4729
|
-
opts.onPhase?.("moments (the step timeline, the zoom apexes, the spread)");
|
|
4730
|
-
const picked = await pickStillTimes(browser, dir, doc, duration, opts);
|
|
4731
|
-
stillTimes = picked.times;
|
|
4732
|
-
moments = picked.moments;
|
|
4733
|
-
for (const n of picked.notes) {
|
|
4734
|
-
if (n.startsWith("moment: ")) skipped.push(n);
|
|
4735
|
-
else opts.onPhase?.(`note: ${n}`);
|
|
4736
|
-
}
|
|
4737
|
-
}
|
|
4738
3988
|
const meta0 = doc.source.meta;
|
|
4739
3989
|
const video = {
|
|
4740
3990
|
w: meta0.captureWidth ?? meta0.width,
|
|
@@ -4745,7 +3995,6 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4745
3995
|
if (opts.look) {
|
|
4746
3996
|
set.push(
|
|
4747
3997
|
...lookOverrides(opts.look, "hero", d.px, video, {
|
|
4748
|
-
still: false,
|
|
4749
3998
|
keepMedia: opts.overrides?.background !== void 0
|
|
4750
3999
|
})
|
|
4751
4000
|
);
|
|
@@ -4757,181 +4006,59 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4757
4006
|
if (!set.length && !mech.unset.length) return opts.overrides;
|
|
4758
4007
|
return { ...opts.overrides, set, unset: mech.unset };
|
|
4759
4008
|
};
|
|
4760
|
-
const posters = await findPosterDocs(dir, opts.launchRoles);
|
|
4761
|
-
const posterCardIds = /* @__PURE__ */ new Set();
|
|
4762
|
-
const missing = /* @__PURE__ */ new Set();
|
|
4763
|
-
for (const d of destinations) {
|
|
4764
|
-
if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
|
|
4765
|
-
continue;
|
|
4766
|
-
const cls = posterClassFor(d.px);
|
|
4767
|
-
const ref = posters[cls];
|
|
4768
|
-
if (!ref) {
|
|
4769
|
-
missing.add(cls);
|
|
4770
|
-
continue;
|
|
4771
|
-
}
|
|
4772
|
-
posterCardIds.add(d.id);
|
|
4773
|
-
const label = `${d.channel} ${d.asset}`;
|
|
4774
|
-
const posterDuration = docOutputDuration2(ref.doc);
|
|
4775
|
-
const time = posterStillTime(ref.doc, posterDuration);
|
|
4776
|
-
opts.onPhase?.(
|
|
4777
|
-
`${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
|
|
4778
|
-
);
|
|
4779
|
-
const shotDir = await mkdtemp(join5(tmpdir(), "vos-poster-"));
|
|
4780
|
-
try {
|
|
4781
|
-
const captured = await framesTake(browser, ref.takeDir, {
|
|
4782
|
-
times: [time],
|
|
4783
|
-
width: d.px.w,
|
|
4784
|
-
height: d.px.h,
|
|
4785
|
-
outDir: shotDir,
|
|
4786
|
-
doc: ref.doc,
|
|
4787
|
-
overrides: opts.overrides
|
|
4788
|
-
});
|
|
4789
|
-
const to = join5(outDir, `${d.id}.png`);
|
|
4790
|
-
await rename3(captured.frames[0].file, to);
|
|
4791
|
-
const bytes = (await stat(to)).size;
|
|
4792
|
-
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4793
|
-
skipped.push(
|
|
4794
|
-
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
4795
|
-
);
|
|
4796
|
-
continue;
|
|
4797
|
-
}
|
|
4798
|
-
const shot = posterShotRect(ref.doc, d.px);
|
|
4799
|
-
assets.push({
|
|
4800
|
-
channel: d.channel,
|
|
4801
|
-
asset: d.asset,
|
|
4802
|
-
destination: d.id,
|
|
4803
|
-
path: relative(outDir, to),
|
|
4804
|
-
w: d.px.w,
|
|
4805
|
-
h: d.px.h,
|
|
4806
|
-
bytes,
|
|
4807
|
-
seconds: null,
|
|
4808
|
-
frameTime: time,
|
|
4809
|
-
source: "poster",
|
|
4810
|
-
poster: {
|
|
4811
|
-
class: cls,
|
|
4812
|
-
file: relative(dir, ref.file),
|
|
4813
|
-
...ref.vosId ? { vosId: ref.vosId } : {}
|
|
4814
|
-
},
|
|
4815
|
-
text: posterTextBoxes(ref.doc, d.px, time),
|
|
4816
|
-
shot,
|
|
4817
|
-
...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
|
|
4818
|
-
});
|
|
4819
|
-
} finally {
|
|
4820
|
-
await rm2(shotDir, { recursive: true, force: true });
|
|
4821
|
-
}
|
|
4822
|
-
}
|
|
4823
|
-
if (missing.size) {
|
|
4824
|
-
const classes = [...missing].sort().join(", ");
|
|
4825
|
-
skipped.push(
|
|
4826
|
-
`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`
|
|
4827
|
-
);
|
|
4828
|
-
}
|
|
4829
4009
|
for (const d of destinations) {
|
|
4830
4010
|
const label = `${d.channel} ${d.asset}`;
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4011
|
+
if (d.minSeconds !== void 0 && videoSeconds < d.minSeconds) {
|
|
4012
|
+
skipped.push(
|
|
4013
|
+
`${label}: spec wants ${d.minSeconds}-${d.maxSeconds ?? "\u221E"}s, the ${opts.range ? "range" : "take"} is ${videoSeconds.toFixed(0)}s`
|
|
4014
|
+
);
|
|
4834
4015
|
continue;
|
|
4835
4016
|
}
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
|
|
4847
|
-
if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
|
|
4848
|
-
range = [0, d.maxSeconds];
|
|
4849
|
-
seconds = d.maxSeconds;
|
|
4850
|
-
opts.onPhase?.(
|
|
4851
|
-
`note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
|
|
4852
|
-
);
|
|
4853
|
-
} else {
|
|
4854
|
-
skipped.push(
|
|
4855
|
-
`${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
|
|
4856
|
-
);
|
|
4857
|
-
continue;
|
|
4858
|
-
}
|
|
4859
|
-
}
|
|
4860
|
-
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
4861
|
-
const outFile = join5(outDir, `${d.id}.${d.format}`);
|
|
4862
|
-
const bitrate = d.maxBytes !== void 0 ? Math.min(
|
|
4863
|
-
1e7,
|
|
4864
|
-
Math.floor(d.maxBytes * 8 / seconds * 0.85)
|
|
4865
|
-
) : void 0;
|
|
4866
|
-
const result = await renderTake(browser, dir, outFile, {
|
|
4867
|
-
width: d.px.w,
|
|
4868
|
-
height: d.px.h,
|
|
4869
|
-
format: "mp4",
|
|
4870
|
-
parallel: opts.parallel,
|
|
4871
|
-
range,
|
|
4872
|
-
bitrate,
|
|
4873
|
-
overrides: videoOverrides(d),
|
|
4874
|
-
onProgress: opts.onProgress
|
|
4875
|
-
});
|
|
4876
|
-
if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
|
|
4017
|
+
let range = opts.range;
|
|
4018
|
+
let seconds = videoSeconds;
|
|
4019
|
+
if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
|
|
4020
|
+
if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
|
|
4021
|
+
range = [0, d.maxSeconds];
|
|
4022
|
+
seconds = d.maxSeconds;
|
|
4023
|
+
opts.onPhase?.(
|
|
4024
|
+
`note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
|
|
4025
|
+
);
|
|
4026
|
+
} else {
|
|
4877
4027
|
skipped.push(
|
|
4878
|
-
`${label}: ${
|
|
4028
|
+
`${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
|
|
4879
4029
|
);
|
|
4880
4030
|
continue;
|
|
4881
4031
|
}
|
|
4882
|
-
assets.push({
|
|
4883
|
-
channel: d.channel,
|
|
4884
|
-
asset: d.asset,
|
|
4885
|
-
destination: d.id,
|
|
4886
|
-
path: relative(outDir, outFile),
|
|
4887
|
-
w: result.width,
|
|
4888
|
-
h: result.height,
|
|
4889
|
-
bytes: result.bytes,
|
|
4890
|
-
seconds: result.duration,
|
|
4891
|
-
frameTime: null
|
|
4892
|
-
});
|
|
4893
|
-
continue;
|
|
4894
4032
|
}
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
const
|
|
4898
|
-
const
|
|
4899
|
-
times: wanted,
|
|
4033
|
+
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
4034
|
+
const outFile = join4(outDir, `${d.id}.${d.format}`);
|
|
4035
|
+
const bitrate = d.maxBytes !== void 0 ? Math.min(1e7, Math.floor(d.maxBytes * 8 / seconds * 0.85)) : void 0;
|
|
4036
|
+
const result = await renderTake(browser, dir, outFile, {
|
|
4900
4037
|
width: d.px.w,
|
|
4901
4038
|
height: d.px.h,
|
|
4902
|
-
|
|
4903
|
-
|
|
4039
|
+
format: "mp4",
|
|
4040
|
+
parallel: opts.parallel,
|
|
4041
|
+
range,
|
|
4042
|
+
bitrate,
|
|
4043
|
+
overrides: videoOverrides(d),
|
|
4044
|
+
onProgress: opts.onProgress
|
|
4904
4045
|
});
|
|
4905
|
-
|
|
4906
|
-
const frame = captured.frames[i];
|
|
4907
|
-
const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
|
|
4908
|
-
const to = join5(outDir, name);
|
|
4909
|
-
await rename3(frame.file, to);
|
|
4910
|
-
const bytes = (await stat(to)).size;
|
|
4911
|
-
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4912
|
-
skipped.push(
|
|
4913
|
-
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
4914
|
-
);
|
|
4915
|
-
continue;
|
|
4916
|
-
}
|
|
4917
|
-
assets.push({
|
|
4918
|
-
channel: d.channel,
|
|
4919
|
-
asset: d.asset,
|
|
4920
|
-
destination: d.id,
|
|
4921
|
-
path: relative(outDir, to),
|
|
4922
|
-
w: captured.width,
|
|
4923
|
-
h: captured.height,
|
|
4924
|
-
bytes,
|
|
4925
|
-
seconds: null,
|
|
4926
|
-
frameTime: frame.time,
|
|
4927
|
-
...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
|
|
4928
|
-
});
|
|
4929
|
-
}
|
|
4930
|
-
if (d.count && captured.frames.length < d.count.min) {
|
|
4046
|
+
if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
|
|
4931
4047
|
skipped.push(
|
|
4932
|
-
`${label}:
|
|
4048
|
+
`${label}: ${overCeiling(result.bytes, d.maxBytes)} (kept at ${outFile})`
|
|
4933
4049
|
);
|
|
4050
|
+
continue;
|
|
4934
4051
|
}
|
|
4052
|
+
assets.push({
|
|
4053
|
+
channel: d.channel,
|
|
4054
|
+
asset: d.asset,
|
|
4055
|
+
destination: d.id,
|
|
4056
|
+
path: relative(outDir, outFile),
|
|
4057
|
+
w: result.width,
|
|
4058
|
+
h: result.height,
|
|
4059
|
+
bytes: result.bytes,
|
|
4060
|
+
seconds: result.duration
|
|
4061
|
+
});
|
|
4935
4062
|
}
|
|
4936
4063
|
const kit = {
|
|
4937
4064
|
release: opts.release ?? null,
|
|
@@ -4939,19 +4066,18 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4939
4066
|
take: dir,
|
|
4940
4067
|
produced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4941
4068
|
specsVerified: CHANNEL_SPECS_VERIFIED,
|
|
4942
|
-
moments,
|
|
4943
4069
|
skipped,
|
|
4944
4070
|
assets
|
|
4945
4071
|
};
|
|
4946
|
-
const kitFile =
|
|
4072
|
+
const kitFile = join4(outDir, "kit.json");
|
|
4947
4073
|
await writeFile5(kitFile, JSON.stringify(kit, null, 2));
|
|
4948
4074
|
return { kit, kitFile, outDir };
|
|
4949
4075
|
}
|
|
4950
4076
|
|
|
4951
4077
|
// src/plugin/markAsset.ts
|
|
4952
|
-
import { mkdir as mkdir4, readFile as
|
|
4953
|
-
import { existsSync as
|
|
4954
|
-
import { isAbsolute
|
|
4078
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
4079
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4080
|
+
import { isAbsolute, join as join5, resolve as resolve4 } from "path";
|
|
4955
4081
|
function markExtension(url) {
|
|
4956
4082
|
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4957
4083
|
if (!m) return null;
|
|
@@ -5032,389 +4158,464 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
5032
4158
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
5033
4159
|
bytes = new Uint8Array(await res.arrayBuffer());
|
|
5034
4160
|
} else {
|
|
5035
|
-
const file =
|
|
5036
|
-
if (!
|
|
5037
|
-
bytes = new Uint8Array(await
|
|
4161
|
+
const file = isAbsolute(src) ? src : resolve4(takeDir, src);
|
|
4162
|
+
if (!existsSync5(file)) throw new Error("no such file");
|
|
4163
|
+
bytes = new Uint8Array(await readFile5(file));
|
|
4164
|
+
}
|
|
4165
|
+
} catch (e) {
|
|
4166
|
+
out.notes.push(
|
|
4167
|
+
`${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
|
|
4168
|
+
);
|
|
4169
|
+
return null;
|
|
4170
|
+
}
|
|
4171
|
+
const aspect = imageAspect(bytes, ext);
|
|
4172
|
+
if (!aspect) {
|
|
4173
|
+
out.notes.push(
|
|
4174
|
+
`${role}: ${src} has no readable size; the wordmark stands in`
|
|
4175
|
+
);
|
|
4176
|
+
return null;
|
|
4177
|
+
}
|
|
4178
|
+
await mkdir4(join5(takeDir, "brand"), { recursive: true });
|
|
4179
|
+
await writeFile6(join5(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
4180
|
+
return { key: `/brand/${name}.${ext}`, aspect };
|
|
4181
|
+
};
|
|
4182
|
+
out.light = await one("logoUrl", "mark");
|
|
4183
|
+
out.dark = await one("logoOnDarkUrl", "mark-on-dark");
|
|
4184
|
+
return out;
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
// src/plugin/officialTemplates.ts
|
|
4188
|
+
var OFFICIAL_END_CARD_TITLE = "End card";
|
|
4189
|
+
function templateKey(s) {
|
|
4190
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
4191
|
+
}
|
|
4192
|
+
function findOfficialByRef(rows, ref) {
|
|
4193
|
+
const key = templateKey(ref);
|
|
4194
|
+
if (!key) return null;
|
|
4195
|
+
for (const row of rows) {
|
|
4196
|
+
if (templateKey(row.title) === key) return row;
|
|
4197
|
+
if (row.slug && templateKey(row.slug) === key) return row;
|
|
4198
|
+
}
|
|
4199
|
+
return null;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
// src/plugin/music.ts
|
|
4203
|
+
async function fetchMusicCatalog(origin) {
|
|
4204
|
+
const base = origin.replace(/\/+$/, "");
|
|
4205
|
+
const res = await fetch(`${base}/api/music`);
|
|
4206
|
+
if (!res.ok) throw new Error(`GET ${base}/api/music answered ${res.status}`);
|
|
4207
|
+
const body = await res.json();
|
|
4208
|
+
return {
|
|
4209
|
+
tracks: Array.isArray(body.tracks) ? body.tracks : [],
|
|
4210
|
+
sfx: Array.isArray(body.sfx) ? body.sfx : []
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
// src/plugin/fontName.ts
|
|
4215
|
+
import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
|
|
4216
|
+
function catalogFamily(family) {
|
|
4217
|
+
const name = family.trim();
|
|
4218
|
+
if (findFontFamily2(name)) return name;
|
|
4219
|
+
const base = name.replace(/\s+Variable$/i, "");
|
|
4220
|
+
if (base !== name) {
|
|
4221
|
+
const hit = findFontFamily2(base);
|
|
4222
|
+
if (hit) return hit.family;
|
|
4223
|
+
}
|
|
4224
|
+
return family;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
// src/plugin/callout.ts
|
|
4228
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4229
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
4230
|
+
import { join as join6 } from "path";
|
|
4231
|
+
import { lerpArray, sample } from "@vosjs/timeline";
|
|
4232
|
+
import {
|
|
4233
|
+
CALLOUT_DEFAULT_BODY_PX,
|
|
4234
|
+
CALLOUT_SHAPES,
|
|
4235
|
+
calloutClip,
|
|
4236
|
+
calloutLook,
|
|
4237
|
+
calloutGroundOf,
|
|
4238
|
+
cameraModel as cameraModel2,
|
|
4239
|
+
deltaE,
|
|
4240
|
+
docCardLayout as docCardLayout2,
|
|
4241
|
+
docZoomTrack as docZoomTrack2,
|
|
4242
|
+
hexToRgb,
|
|
4243
|
+
parseCssColour,
|
|
4244
|
+
pinBox as pinBox2,
|
|
4245
|
+
ratedSegments as ratedSegments5,
|
|
4246
|
+
resolvePins as resolvePins2,
|
|
4247
|
+
spanOutputExtent as spanOutputExtent5
|
|
4248
|
+
} from "@vosjs/studio-core";
|
|
4249
|
+
|
|
4250
|
+
// src/plugin/picture.ts
|
|
4251
|
+
import { inflateSync } from "zlib";
|
|
4252
|
+
var PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
4253
|
+
function decodePng(bytes) {
|
|
4254
|
+
if (bytes.length < 33) return null;
|
|
4255
|
+
for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIG[i]) return null;
|
|
4256
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4257
|
+
let pos = 8;
|
|
4258
|
+
let w = 0;
|
|
4259
|
+
let h = 0;
|
|
4260
|
+
let depth = 0;
|
|
4261
|
+
let colorType = 0;
|
|
4262
|
+
let interlace = 0;
|
|
4263
|
+
const idat = [];
|
|
4264
|
+
while (pos + 8 <= bytes.length) {
|
|
4265
|
+
const len = view.getUint32(pos);
|
|
4266
|
+
const type = String.fromCharCode(
|
|
4267
|
+
bytes[pos + 4],
|
|
4268
|
+
bytes[pos + 5],
|
|
4269
|
+
bytes[pos + 6],
|
|
4270
|
+
bytes[pos + 7]
|
|
4271
|
+
);
|
|
4272
|
+
const start = pos + 8;
|
|
4273
|
+
if (type === "IHDR") {
|
|
4274
|
+
w = view.getUint32(start);
|
|
4275
|
+
h = view.getUint32(start + 4);
|
|
4276
|
+
depth = bytes[start + 8];
|
|
4277
|
+
colorType = bytes[start + 9];
|
|
4278
|
+
interlace = bytes[start + 12];
|
|
4279
|
+
} else if (type === "IDAT") {
|
|
4280
|
+
idat.push(bytes.subarray(start, start + len));
|
|
4281
|
+
} else if (type === "IEND") break;
|
|
4282
|
+
pos = start + len + 4;
|
|
4283
|
+
}
|
|
4284
|
+
if (!w || !h || depth !== 8 || interlace !== 0) return null;
|
|
4285
|
+
const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
|
|
4286
|
+
if (!channels) return null;
|
|
4287
|
+
const total = idat.reduce((n, c) => n + c.length, 0);
|
|
4288
|
+
const joined = new Uint8Array(total);
|
|
4289
|
+
let off2 = 0;
|
|
4290
|
+
for (const c of idat) {
|
|
4291
|
+
joined.set(c, off2);
|
|
4292
|
+
off2 += c.length;
|
|
4293
|
+
}
|
|
4294
|
+
let raw;
|
|
4295
|
+
try {
|
|
4296
|
+
raw = new Uint8Array(inflateSync(joined));
|
|
4297
|
+
} catch {
|
|
4298
|
+
return null;
|
|
4299
|
+
}
|
|
4300
|
+
const stride = w * channels;
|
|
4301
|
+
if (raw.length < (stride + 1) * h) return null;
|
|
4302
|
+
const out = new Uint8Array(w * h * 4);
|
|
4303
|
+
const prev = new Uint8Array(stride);
|
|
4304
|
+
const cur = new Uint8Array(stride);
|
|
4305
|
+
for (let y = 0; y < h; y++) {
|
|
4306
|
+
const filter = raw[y * (stride + 1)];
|
|
4307
|
+
const rowStart = y * (stride + 1) + 1;
|
|
4308
|
+
for (let i = 0; i < stride; i++) {
|
|
4309
|
+
const x = raw[rowStart + i];
|
|
4310
|
+
const a = i >= channels ? cur[i - channels] : 0;
|
|
4311
|
+
const b = prev[i];
|
|
4312
|
+
const c = i >= channels ? prev[i - channels] : 0;
|
|
4313
|
+
let v;
|
|
4314
|
+
switch (filter) {
|
|
4315
|
+
case 0:
|
|
4316
|
+
v = x;
|
|
4317
|
+
break;
|
|
4318
|
+
case 1:
|
|
4319
|
+
v = x + a;
|
|
4320
|
+
break;
|
|
4321
|
+
case 2:
|
|
4322
|
+
v = x + b;
|
|
4323
|
+
break;
|
|
4324
|
+
case 3:
|
|
4325
|
+
v = x + (a + b >> 1);
|
|
4326
|
+
break;
|
|
4327
|
+
case 4: {
|
|
4328
|
+
const p = a + b - c;
|
|
4329
|
+
const pa = Math.abs(p - a);
|
|
4330
|
+
const pb = Math.abs(p - b);
|
|
4331
|
+
const pc = Math.abs(p - c);
|
|
4332
|
+
v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
|
4333
|
+
break;
|
|
4334
|
+
}
|
|
4335
|
+
default:
|
|
4336
|
+
return null;
|
|
4337
|
+
}
|
|
4338
|
+
cur[i] = v & 255;
|
|
4339
|
+
}
|
|
4340
|
+
for (let px = 0; px < w; px++) {
|
|
4341
|
+
const o = (y * w + px) * 4;
|
|
4342
|
+
const s = px * channels;
|
|
4343
|
+
if (channels >= 3) {
|
|
4344
|
+
out[o] = cur[s];
|
|
4345
|
+
out[o + 1] = cur[s + 1];
|
|
4346
|
+
out[o + 2] = cur[s + 2];
|
|
4347
|
+
out[o + 3] = channels === 4 ? cur[s + 3] : 255;
|
|
4348
|
+
} else {
|
|
4349
|
+
out[o] = out[o + 1] = out[o + 2] = cur[s];
|
|
4350
|
+
out[o + 3] = channels === 2 ? cur[s + 1] : 255;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
prev.set(cur);
|
|
4354
|
+
}
|
|
4355
|
+
return { w, h, data: out };
|
|
4356
|
+
}
|
|
4357
|
+
var at = (img, x, y) => {
|
|
4358
|
+
const o = (y * img.w + x) * 4;
|
|
4359
|
+
return [img.data[o], img.data[o + 1], img.data[o + 2]];
|
|
4360
|
+
};
|
|
4361
|
+
var median = (a) => {
|
|
4362
|
+
const s = [...a].sort((x, y) => x - y);
|
|
4363
|
+
return s[s.length >> 1] ?? 0;
|
|
4364
|
+
};
|
|
4365
|
+
function groundColour(img) {
|
|
4366
|
+
const ring = Math.max(1, Math.round(Math.min(img.w, img.h) * 0.01));
|
|
4367
|
+
const r = [];
|
|
4368
|
+
const g = [];
|
|
4369
|
+
const b = [];
|
|
4370
|
+
const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 400));
|
|
4371
|
+
for (let y = 0; y < img.h; y += step) {
|
|
4372
|
+
for (let x = 0; x < img.w; x += step) {
|
|
4373
|
+
if (x < ring || y < ring || x >= img.w - ring || y >= img.h - ring) {
|
|
4374
|
+
const p = at(img, x, y);
|
|
4375
|
+
r.push(p[0]);
|
|
4376
|
+
g.push(p[1]);
|
|
4377
|
+
b.push(p[2]);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
return [median(r), median(g), median(b)];
|
|
4382
|
+
}
|
|
4383
|
+
var hex = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
4384
|
+
var delta = (p, q) => Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
|
|
4385
|
+
function cardBounds(img, ground, threshold = 60) {
|
|
4386
|
+
let x0 = img.w;
|
|
4387
|
+
let y0 = img.h;
|
|
4388
|
+
let x1 = -1;
|
|
4389
|
+
let y1 = -1;
|
|
4390
|
+
const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 700));
|
|
4391
|
+
for (let y = 0; y < img.h; y += step) {
|
|
4392
|
+
for (let x = 0; x < img.w; x += step) {
|
|
4393
|
+
if (delta(at(img, x, y), ground) > threshold) {
|
|
4394
|
+
if (x < x0) x0 = x;
|
|
4395
|
+
if (y < y0) y0 = y;
|
|
4396
|
+
if (x > x1) x1 = x;
|
|
4397
|
+
if (y > y1) y1 = y;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
if (x1 < 0) return null;
|
|
4402
|
+
return { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 };
|
|
4403
|
+
}
|
|
4404
|
+
function haloReading(img, ground, card) {
|
|
4405
|
+
const band = Math.max(2, Math.round(Math.min(img.w, img.h) * 0.012));
|
|
4406
|
+
const meanRect = (x0, y0, x1, y1) => {
|
|
4407
|
+
let sum = 0;
|
|
4408
|
+
let n = 0;
|
|
4409
|
+
const sx = Math.max(1, Math.round((x1 - x0) / 60));
|
|
4410
|
+
const sy = Math.max(1, Math.round((y1 - y0) / 60));
|
|
4411
|
+
for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
|
|
4412
|
+
for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
|
|
4413
|
+
sum += delta(at(img, x, y), ground);
|
|
4414
|
+
n++;
|
|
5038
4415
|
}
|
|
5039
|
-
} catch (e) {
|
|
5040
|
-
out.notes.push(
|
|
5041
|
-
`${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
|
|
5042
|
-
);
|
|
5043
|
-
return null;
|
|
5044
|
-
}
|
|
5045
|
-
const aspect = imageAspect(bytes, ext);
|
|
5046
|
-
if (!aspect) {
|
|
5047
|
-
out.notes.push(
|
|
5048
|
-
`${role}: ${src} has no readable size; the wordmark stands in`
|
|
5049
|
-
);
|
|
5050
|
-
return null;
|
|
5051
4416
|
}
|
|
5052
|
-
|
|
5053
|
-
await writeFile6(join6(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
5054
|
-
return { key: `/brand/${name}.${ext}`, aspect };
|
|
4417
|
+
return n ? sum / n : 0;
|
|
5055
4418
|
};
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
4419
|
+
const rise = (bands) => {
|
|
4420
|
+
const [outer, mid] = bands;
|
|
4421
|
+
return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
|
|
4422
|
+
};
|
|
4423
|
+
const readings = [];
|
|
4424
|
+
const yA = card.y + card.h * 0.2;
|
|
4425
|
+
const yB = card.y + card.h * 0.8;
|
|
4426
|
+
if (card.x >= 1 && card.w > band * 4) {
|
|
4427
|
+
const x0 = card.x;
|
|
4428
|
+
readings.push(
|
|
4429
|
+
rise([
|
|
4430
|
+
meanRect(x0, yA, x0 + band, yB),
|
|
4431
|
+
meanRect(x0 + band, yA, x0 + band * 2, yB),
|
|
4432
|
+
meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
|
|
4433
|
+
])
|
|
4434
|
+
);
|
|
5072
4435
|
}
|
|
5073
|
-
|
|
4436
|
+
if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
|
|
4437
|
+
const x1 = card.x + card.w;
|
|
4438
|
+
readings.push(
|
|
4439
|
+
rise([
|
|
4440
|
+
meanRect(x1 - band, yA, x1, yB),
|
|
4441
|
+
meanRect(x1 - band * 2, yA, x1 - band, yB),
|
|
4442
|
+
meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
|
|
4443
|
+
])
|
|
4444
|
+
);
|
|
4445
|
+
}
|
|
4446
|
+
if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
|
|
4447
|
+
const y1 = card.y + card.h;
|
|
4448
|
+
const xA = card.x + card.w * 0.2;
|
|
4449
|
+
const xB = card.x + card.w * 0.8;
|
|
4450
|
+
readings.push(
|
|
4451
|
+
rise([
|
|
4452
|
+
meanRect(xA, y1 - band, xB, y1),
|
|
4453
|
+
meanRect(xA, y1 - band * 2, xB, y1 - band),
|
|
4454
|
+
meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
|
|
4455
|
+
])
|
|
4456
|
+
);
|
|
4457
|
+
}
|
|
4458
|
+
return readings.length ? Math.max(...readings) : null;
|
|
5074
4459
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
const
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
sfx: Array.isArray(body.sfx) ? body.sfx : []
|
|
4460
|
+
function edgeContrast(img, ground, card) {
|
|
4461
|
+
const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
|
|
4462
|
+
const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
|
|
4463
|
+
const col = (x) => {
|
|
4464
|
+
if (x < 0 || x >= img.w) return 0;
|
|
4465
|
+
const ds = [];
|
|
4466
|
+
const step = Math.max(1, Math.round((yB - yA) / 80));
|
|
4467
|
+
for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
|
|
4468
|
+
return median(ds);
|
|
5085
4469
|
};
|
|
4470
|
+
const depth = Math.min(24, Math.floor(card.w / 8));
|
|
4471
|
+
let best = 0;
|
|
4472
|
+
for (let d = 0; d < depth; d++) {
|
|
4473
|
+
best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
|
|
4474
|
+
}
|
|
4475
|
+
return best;
|
|
5086
4476
|
}
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
const
|
|
5098
|
-
for (let
|
|
5099
|
-
let
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
let c = 4294967295;
|
|
5107
|
-
for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
|
|
5108
|
-
return (c ^ 4294967295) >>> 0;
|
|
5109
|
-
}
|
|
5110
|
-
function chunk(type, body) {
|
|
5111
|
-
const out = new Uint8Array(12 + body.length);
|
|
5112
|
-
const dv = new DataView(out.buffer);
|
|
5113
|
-
dv.setUint32(0, body.length);
|
|
5114
|
-
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
5115
|
-
out.set(body, 8);
|
|
5116
|
-
dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
|
|
5117
|
-
return out;
|
|
5118
|
-
}
|
|
5119
|
-
function encodePng(img) {
|
|
5120
|
-
const stride = img.w * 4;
|
|
5121
|
-
const raw = new Uint8Array((stride + 1) * img.h);
|
|
5122
|
-
for (let y = 0; y < img.h; y++) {
|
|
5123
|
-
raw[y * (stride + 1)] = 2;
|
|
5124
|
-
for (let i = 0; i < stride; i++) {
|
|
5125
|
-
const cur = img.data[y * stride + i];
|
|
5126
|
-
const up = y ? img.data[(y - 1) * stride + i] : 0;
|
|
5127
|
-
raw[y * (stride + 1) + 1 + i] = cur - up & 255;
|
|
5128
|
-
}
|
|
5129
|
-
}
|
|
5130
|
-
const ihdr = new Uint8Array(13);
|
|
5131
|
-
const dv = new DataView(ihdr.buffer);
|
|
5132
|
-
dv.setUint32(0, img.w);
|
|
5133
|
-
dv.setUint32(4, img.h);
|
|
5134
|
-
ihdr[8] = 8;
|
|
5135
|
-
ihdr[9] = 6;
|
|
5136
|
-
const parts = [
|
|
5137
|
-
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
5138
|
-
chunk("IHDR", ihdr),
|
|
5139
|
-
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
5140
|
-
chunk("IEND", new Uint8Array(0))
|
|
5141
|
-
];
|
|
5142
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
5143
|
-
let o = 0;
|
|
5144
|
-
for (const p of parts) {
|
|
5145
|
-
out.set(p, o);
|
|
5146
|
-
o += p.length;
|
|
4477
|
+
function inkCoverage(img, rect, threshold = 48) {
|
|
4478
|
+
const x0 = Math.max(0, Math.floor(rect.x));
|
|
4479
|
+
const y0 = Math.max(0, Math.floor(rect.y));
|
|
4480
|
+
const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
|
|
4481
|
+
const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
|
|
4482
|
+
if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
|
|
4483
|
+
const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
|
|
4484
|
+
const r = [];
|
|
4485
|
+
const g = [];
|
|
4486
|
+
const b = [];
|
|
4487
|
+
const pts = [];
|
|
4488
|
+
for (let y = y0; y < y1; y += step) {
|
|
4489
|
+
for (let x = x0; x < x1; x += step) {
|
|
4490
|
+
const p = at(img, x, y);
|
|
4491
|
+
pts.push(p);
|
|
4492
|
+
r.push(p[0]);
|
|
4493
|
+
g.push(p[1]);
|
|
4494
|
+
b.push(p[2]);
|
|
4495
|
+
}
|
|
5147
4496
|
}
|
|
5148
|
-
|
|
4497
|
+
const base = [median(r), median(g), median(b)];
|
|
4498
|
+
let ink = 0;
|
|
4499
|
+
for (const p of pts) if (delta(p, base) > threshold) ink++;
|
|
4500
|
+
return pts.length ? ink / pts.length : 0;
|
|
5149
4501
|
}
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
"The poster test: would you post this frame as a still, alone?",
|
|
5163
|
-
"The thumbnail test: does it read at half size?",
|
|
5164
|
-
"Name three ways this asset acknowledges THIS product (its data, its state, its brand), not a generic window."
|
|
5165
|
-
];
|
|
5166
|
-
function rolesFor(asset) {
|
|
5167
|
-
if ((asset.source === "poster" || asset.source === "stage") && asset.template)
|
|
5168
|
-
return [asset.template.replace(/-stage$/, "")];
|
|
5169
|
-
const spec = DESTINATIONS2.find((d) => d.id === asset.destination);
|
|
5170
|
-
if (spec?.kind === "video") {
|
|
5171
|
-
const portrait = spec.px.w < spec.px.h;
|
|
5172
|
-
return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
|
|
5173
|
-
}
|
|
5174
|
-
if (spec?.genre === "card")
|
|
5175
|
-
return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
|
|
5176
|
-
if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
|
|
5177
|
-
return [];
|
|
5178
|
-
}
|
|
5179
|
-
function resample(img, w, h) {
|
|
5180
|
-
const out = new Uint8Array(w * h * 4);
|
|
5181
|
-
const sx = img.w / w;
|
|
5182
|
-
const sy = img.h / h;
|
|
5183
|
-
for (let y = 0; y < h; y++) {
|
|
5184
|
-
const y0 = Math.floor(y * sy);
|
|
5185
|
-
const y1 = Math.max(y0 + 1, Math.floor((y + 1) * sy));
|
|
5186
|
-
for (let x = 0; x < w; x++) {
|
|
5187
|
-
const x0 = Math.floor(x * sx);
|
|
5188
|
-
const x1 = Math.max(x0 + 1, Math.floor((x + 1) * sx));
|
|
5189
|
-
let r = 0;
|
|
5190
|
-
let g = 0;
|
|
5191
|
-
let b = 0;
|
|
5192
|
-
let a = 0;
|
|
4502
|
+
function differenceHash(img, rect) {
|
|
4503
|
+
const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
4504
|
+
const cols = 9;
|
|
4505
|
+
const rows = 8;
|
|
4506
|
+
const cell = [];
|
|
4507
|
+
for (let cy = 0; cy < rows; cy++) {
|
|
4508
|
+
for (let cx = 0; cx < cols; cx++) {
|
|
4509
|
+
const x0 = Math.floor(r.x + cx * r.w / cols);
|
|
4510
|
+
const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
|
|
4511
|
+
const y0 = Math.floor(r.y + cy * r.h / rows);
|
|
4512
|
+
const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
|
|
4513
|
+
let sum = 0;
|
|
5193
4514
|
let n = 0;
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
a += img.data[o2 + 3];
|
|
4515
|
+
const sx = Math.max(1, Math.floor((x1 - x0) / 8));
|
|
4516
|
+
const sy = Math.max(1, Math.floor((y1 - y0) / 8));
|
|
4517
|
+
for (let y = y0; y < y1 && y < img.h; y += sy) {
|
|
4518
|
+
for (let x = x0; x < x1 && x < img.w; x += sx) {
|
|
4519
|
+
const p = at(img, Math.max(0, x), Math.max(0, y));
|
|
4520
|
+
sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
|
|
5201
4521
|
n++;
|
|
5202
4522
|
}
|
|
5203
4523
|
}
|
|
5204
|
-
|
|
5205
|
-
out[o] = n ? r / n : 0;
|
|
5206
|
-
out[o + 1] = n ? g / n : 0;
|
|
5207
|
-
out[o + 2] = n ? b / n : 0;
|
|
5208
|
-
out[o + 3] = n ? a / n : 0;
|
|
4524
|
+
cell.push(n ? sum / n : 0);
|
|
5209
4525
|
}
|
|
5210
4526
|
}
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
const margin = 24;
|
|
5219
|
-
const band = 8;
|
|
5220
|
-
const w = margin * 2 + lw + gutter + rw;
|
|
5221
|
-
const h = margin * 2 + height + band + 6;
|
|
5222
|
-
const out = new Uint8Array(w * h * 4);
|
|
5223
|
-
for (let i = 0; i < w * h; i++) {
|
|
5224
|
-
out[i * 4] = 240;
|
|
5225
|
-
out[i * 4 + 1] = 242;
|
|
5226
|
-
out[i * 4 + 2] = 244;
|
|
5227
|
-
out[i * 4 + 3] = 255;
|
|
5228
|
-
}
|
|
5229
|
-
const blit = (src, x0, y0) => {
|
|
5230
|
-
for (let y = 0; y < src.h; y++) {
|
|
5231
|
-
for (let x = 0; x < src.w; x++) {
|
|
5232
|
-
const si = (y * src.w + x) * 4;
|
|
5233
|
-
const a = src.data[si + 3] / 255;
|
|
5234
|
-
const o = ((y0 + y) * w + x0 + x) * 4;
|
|
5235
|
-
for (let c = 0; c < 3; c++)
|
|
5236
|
-
out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
|
|
5237
|
-
}
|
|
4527
|
+
let bits = "";
|
|
4528
|
+
for (let cy = 0; cy < rows; cy++) {
|
|
4529
|
+
let byte = 0;
|
|
4530
|
+
for (let cx = 0; cx < cols - 1; cx++) {
|
|
4531
|
+
const l = cell[cy * cols + cx];
|
|
4532
|
+
const rr = cell[cy * cols + cx + 1];
|
|
4533
|
+
byte = byte << 1 | (l > rr ? 1 : 0);
|
|
5238
4534
|
}
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
const fill = (x0, x1, y0, y1, v) => {
|
|
5243
|
-
for (let y = y0; y < y1; y++)
|
|
5244
|
-
for (let x = x0; x < x1; x++) {
|
|
5245
|
-
const o = (y * w + x) * 4;
|
|
5246
|
-
out[o] = v;
|
|
5247
|
-
out[o + 1] = v;
|
|
5248
|
-
out[o + 2] = v;
|
|
5249
|
-
}
|
|
5250
|
-
};
|
|
5251
|
-
fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
|
|
5252
|
-
fill(
|
|
5253
|
-
margin + lw + gutter,
|
|
5254
|
-
margin + lw + gutter + rw,
|
|
5255
|
-
margin + height + 6,
|
|
5256
|
-
margin + height + 6 + band,
|
|
5257
|
-
200
|
|
5258
|
-
);
|
|
5259
|
-
return { w, h, data: out };
|
|
4535
|
+
bits += byte.toString(16).padStart(2, "0");
|
|
4536
|
+
}
|
|
4537
|
+
return bits;
|
|
5260
4538
|
}
|
|
5261
|
-
|
|
5262
|
-
const
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
);
|
|
5266
|
-
const manifestDir = dirname(resolve6(manifestPath));
|
|
5267
|
-
const kitDir = dirname(resolve6(kitPath));
|
|
5268
|
-
const out = resolve6(outDir ?? join7(kitDir, "judge"));
|
|
5269
|
-
await mkdir5(out, { recursive: true });
|
|
5270
|
-
const sheets = [];
|
|
5271
|
-
const skipped = [];
|
|
5272
|
-
const refCache = /* @__PURE__ */ new Map();
|
|
5273
|
-
const loadRef = async (file) => {
|
|
5274
|
-
if (refCache.has(file)) return refCache.get(file) ?? null;
|
|
5275
|
-
const p = join7(manifestDir, file);
|
|
5276
|
-
const img = existsSync7(p) ? decodePng(new Uint8Array(await readFile7(p))) : null;
|
|
5277
|
-
refCache.set(file, img);
|
|
5278
|
-
return img;
|
|
4539
|
+
function luminance(c) {
|
|
4540
|
+
const lin = (v) => {
|
|
4541
|
+
const s = v / 255;
|
|
4542
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
5279
4543
|
};
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
)
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
);
|
|
5301
|
-
continue;
|
|
5302
|
-
}
|
|
5303
|
-
const refImg = await loadRef(ref.file);
|
|
5304
|
-
if (!refImg) {
|
|
5305
|
-
skipped.push(
|
|
5306
|
-
`${a.destination ?? a.path}: reference ${ref.file} unreadable`
|
|
5307
|
-
);
|
|
5308
|
-
continue;
|
|
4544
|
+
return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
|
|
4545
|
+
}
|
|
4546
|
+
function lightness(c) {
|
|
4547
|
+
const y = luminance(c);
|
|
4548
|
+
return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
|
|
4549
|
+
}
|
|
4550
|
+
function medianColour(img, rect) {
|
|
4551
|
+
const r = [];
|
|
4552
|
+
const g = [];
|
|
4553
|
+
const b = [];
|
|
4554
|
+
const x0 = Math.max(0, Math.floor(rect.x));
|
|
4555
|
+
const y0 = Math.max(0, Math.floor(rect.y));
|
|
4556
|
+
const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
|
|
4557
|
+
const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
|
|
4558
|
+
const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
|
|
4559
|
+
for (let y = y0; y < y1; y += step) {
|
|
4560
|
+
for (let x = x0; x < x1; x += step) {
|
|
4561
|
+
const p = at(img, x, y);
|
|
4562
|
+
r.push(p[0]);
|
|
4563
|
+
g.push(p[1]);
|
|
4564
|
+
b.push(p[2]);
|
|
5309
4565
|
}
|
|
5310
|
-
const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
|
|
5311
|
-
const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
|
|
5312
|
-
const nameA = `${id}--A.png`;
|
|
5313
|
-
const nameB = `${id}--B.png`;
|
|
5314
|
-
await writeFile7(join7(out, nameA), encodePng(composeSheet(img, refImg)));
|
|
5315
|
-
await writeFile7(join7(out, nameB), encodePng(composeSheet(refImg, img)));
|
|
5316
|
-
const rubric = [
|
|
5317
|
-
`# ${id} against ${ref.id} (${ref.file})`,
|
|
5318
|
-
"",
|
|
5319
|
-
`Sheet A: the kit asset LEFT (dark band), the reference RIGHT (light band). Sheet B: the reverse.`,
|
|
5320
|
-
`Judge each sheet on its own, both orders, and write the verdict where the two agree; a disagreement is a tie.`,
|
|
5321
|
-
"",
|
|
5322
|
-
`## The reference`,
|
|
5323
|
-
ref.layout,
|
|
5324
|
-
...ref.facts ? ["", "```json", JSON.stringify(ref.facts, null, 2), "```"] : [],
|
|
5325
|
-
"",
|
|
5326
|
-
`Rule: ${ref.rule}`,
|
|
5327
|
-
"",
|
|
5328
|
-
"## The rubric",
|
|
5329
|
-
...RUBRIC.map((r, i) => `${i + 1}. ${r}`),
|
|
5330
|
-
"",
|
|
5331
|
-
"## Verdict",
|
|
5332
|
-
"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.",
|
|
5333
|
-
""
|
|
5334
|
-
].join("\n");
|
|
5335
|
-
await writeFile7(join7(out, `${id}.md`), rubric);
|
|
5336
|
-
sheets.push({
|
|
5337
|
-
asset: id,
|
|
5338
|
-
reference: ref.id,
|
|
5339
|
-
sheetA: nameA,
|
|
5340
|
-
sheetB: nameB,
|
|
5341
|
-
rubric: `${id}.md`
|
|
5342
|
-
});
|
|
5343
4566
|
}
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
)
|
|
4567
|
+
return [median(r), median(g), median(b)];
|
|
4568
|
+
}
|
|
4569
|
+
function measureStill(img) {
|
|
4570
|
+
const g = groundColour(img);
|
|
4571
|
+
const strong = cardBounds(img, g, 60);
|
|
4572
|
+
const faint = cardBounds(img, g, 12) ?? strong;
|
|
4573
|
+
const card = faint;
|
|
4574
|
+
const bleed = [];
|
|
4575
|
+
let pad = null;
|
|
4576
|
+
let widthPct = null;
|
|
4577
|
+
let shadow = null;
|
|
4578
|
+
let edge = 0;
|
|
4579
|
+
let separation = null;
|
|
4580
|
+
if (card) {
|
|
4581
|
+
pad = {
|
|
4582
|
+
left: card.x / img.w,
|
|
4583
|
+
right: (img.w - card.x - card.w) / img.w,
|
|
4584
|
+
top: card.y / img.h,
|
|
4585
|
+
bottom: (img.h - card.y - card.h) / img.h
|
|
4586
|
+
};
|
|
4587
|
+
for (const side of ["left", "right", "top", "bottom"])
|
|
4588
|
+
if (pad[side] < 5e-3) bleed.push(side);
|
|
4589
|
+
widthPct = card.w / img.w;
|
|
4590
|
+
shadow = haloReading(img, g, card);
|
|
4591
|
+
edge = edgeContrast(img, g, card);
|
|
4592
|
+
const body = strong ?? card;
|
|
4593
|
+
const inner = {
|
|
4594
|
+
x: body.x + body.w * 0.1,
|
|
4595
|
+
y: body.y + body.h * 0.1,
|
|
4596
|
+
w: body.w * 0.8,
|
|
4597
|
+
h: body.h * 0.8
|
|
4598
|
+
};
|
|
4599
|
+
separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
|
|
5365
4600
|
}
|
|
5366
|
-
|
|
5367
|
-
}
|
|
5368
|
-
function winRate(verdicts) {
|
|
5369
|
-
const judgedList = verdicts.filter(
|
|
5370
|
-
(v) => v.win !== null || (v.reasons?.length ?? 0) > 0
|
|
5371
|
-
);
|
|
5372
|
-
const wins = judgedList.filter((v) => v.win === true).length;
|
|
5373
|
-
const ties = judgedList.filter((v) => v.win === null).length;
|
|
5374
|
-
const judged = judgedList.length;
|
|
4601
|
+
const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5375
4602
|
return {
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
4603
|
+
w: img.w,
|
|
4604
|
+
h: img.h,
|
|
4605
|
+
ground: hex(g),
|
|
4606
|
+
card,
|
|
4607
|
+
widthPct,
|
|
4608
|
+
pad,
|
|
4609
|
+
bleed,
|
|
4610
|
+
shadow,
|
|
4611
|
+
edge,
|
|
4612
|
+
ink: inkCoverage(img, inkRect),
|
|
4613
|
+
separation,
|
|
4614
|
+
hash: differenceHash(img, inkRect)
|
|
5380
4615
|
};
|
|
5381
4616
|
}
|
|
5382
4617
|
|
|
5383
|
-
// src/plugin/fontName.ts
|
|
5384
|
-
import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
|
|
5385
|
-
function catalogFamily(family) {
|
|
5386
|
-
const name = family.trim();
|
|
5387
|
-
if (findFontFamily2(name)) return name;
|
|
5388
|
-
const base = name.replace(/\s+Variable$/i, "");
|
|
5389
|
-
if (base !== name) {
|
|
5390
|
-
const hit = findFontFamily2(base);
|
|
5391
|
-
if (hit) return hit.family;
|
|
5392
|
-
}
|
|
5393
|
-
return family;
|
|
5394
|
-
}
|
|
5395
|
-
|
|
5396
4618
|
// src/plugin/callout.ts
|
|
5397
|
-
import { existsSync as existsSync8 } from "fs";
|
|
5398
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
5399
|
-
import { join as join8 } from "path";
|
|
5400
|
-
import { lerpArray, sample } from "@vosjs/timeline";
|
|
5401
|
-
import {
|
|
5402
|
-
CALLOUT_DEFAULT_BODY_PX,
|
|
5403
|
-
CALLOUT_SHAPES,
|
|
5404
|
-
calloutClip,
|
|
5405
|
-
calloutLook,
|
|
5406
|
-
calloutGroundOf,
|
|
5407
|
-
cameraModel as cameraModel2,
|
|
5408
|
-
deltaE,
|
|
5409
|
-
docCardLayout as docCardLayout2,
|
|
5410
|
-
docZoomTrack as docZoomTrack2,
|
|
5411
|
-
hexToRgb,
|
|
5412
|
-
parseCssColour,
|
|
5413
|
-
pinBox as pinBox2,
|
|
5414
|
-
ratedSegments as ratedSegments5,
|
|
5415
|
-
resolvePins as resolvePins2,
|
|
5416
|
-
spanOutputExtent as spanOutputExtent5
|
|
5417
|
-
} from "@vosjs/studio-core";
|
|
5418
4619
|
var CALLOUT_DEFAULT_SECONDS = 3;
|
|
5419
4620
|
var CALLOUT_LEAD_SECONDS = 0.2;
|
|
5420
4621
|
var CALLOUT_DELTA_E_PROBLEM = 8;
|
|
@@ -5593,13 +4794,13 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
|
5593
4794
|
const out = /* @__PURE__ */ new Map();
|
|
5594
4795
|
if (!times.length) return out;
|
|
5595
4796
|
const bare = { ...doc, overlays: [] };
|
|
5596
|
-
const outDir =
|
|
4797
|
+
const outDir = join6(dir, ".ground-check");
|
|
5597
4798
|
const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
|
|
5598
4799
|
for (const f of res.frames) {
|
|
5599
|
-
if (!
|
|
4800
|
+
if (!existsSync6(f.file)) continue;
|
|
5600
4801
|
out.set(
|
|
5601
4802
|
+f.time.toFixed(3),
|
|
5602
|
-
decodePng(new Uint8Array(await
|
|
4803
|
+
decodePng(new Uint8Array(await readFile6(f.file)))
|
|
5603
4804
|
);
|
|
5604
4805
|
}
|
|
5605
4806
|
return out;
|
|
@@ -5608,241 +4809,9 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
|
5608
4809
|
// src/plugin/kitPicture.ts
|
|
5609
4810
|
import { execFile } from "child_process";
|
|
5610
4811
|
import { promisify } from "util";
|
|
5611
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
5612
4812
|
var execFileP = promisify(execFile);
|
|
5613
|
-
var SUBJECT_BAND = { min: 0.6, max: 0.92 };
|
|
5614
4813
|
var BLANK_INK = 0.12;
|
|
5615
|
-
var SEPARATION_L = 8;
|
|
5616
|
-
var SHADOW_PRESENT = 6;
|
|
5617
|
-
var EDGE_PRESENT = 40;
|
|
5618
|
-
var DUPLICATE_BITS2 = 6;
|
|
5619
|
-
var HALFSIZE_KEEP = 0.45;
|
|
5620
|
-
function apcaContrast(text, bg) {
|
|
5621
|
-
const lum = (c) => {
|
|
5622
|
-
const ch = (v) => Math.pow(v / 255, 2.4);
|
|
5623
|
-
let y = 0.2126729 * ch(c[0]) + 0.7151522 * ch(c[1]) + 0.072175 * ch(c[2]);
|
|
5624
|
-
if (y < 0.022) y += Math.pow(0.022 - y, 1.414);
|
|
5625
|
-
return y;
|
|
5626
|
-
};
|
|
5627
|
-
const yt = lum(text);
|
|
5628
|
-
const yb = lum(bg);
|
|
5629
|
-
if (Math.abs(yb - yt) < 5e-4) return 0;
|
|
5630
|
-
let sapc;
|
|
5631
|
-
if (yb > yt) {
|
|
5632
|
-
sapc = (Math.pow(yb, 0.56) - Math.pow(yt, 0.57)) * 1.14;
|
|
5633
|
-
return sapc < 0.1 ? 0 : (sapc - 0.027) * 100;
|
|
5634
|
-
}
|
|
5635
|
-
sapc = (Math.pow(yb, 0.65) - Math.pow(yt, 0.62)) * 1.14;
|
|
5636
|
-
return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
|
|
5637
|
-
}
|
|
5638
4814
|
var pct = (v) => `${Math.round(v * 100)}%`;
|
|
5639
|
-
var hexToRgb2 = (h) => {
|
|
5640
|
-
const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
|
|
5641
|
-
if (!m) return null;
|
|
5642
|
-
const n = parseInt(m[1], 16);
|
|
5643
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
5644
|
-
};
|
|
5645
|
-
function stillFindings(a, img, m) {
|
|
5646
|
-
const out = [];
|
|
5647
|
-
const genre = a.spec?.genre;
|
|
5648
|
-
const bledAll = m.bleed.length === 4;
|
|
5649
|
-
if (a.shot) {
|
|
5650
|
-
const sx = Math.max(0, a.shot.x) * img.w;
|
|
5651
|
-
const sy = Math.max(0, a.shot.y) * img.h;
|
|
5652
|
-
const sw = Math.min(1, a.shot.x + a.shot.w) * img.w - sx;
|
|
5653
|
-
const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
|
|
5654
|
-
const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
|
|
5655
|
-
const ink = inkCoverage(img, rect);
|
|
5656
|
-
if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
|
|
5657
|
-
out.push({
|
|
5658
|
-
code: "blank",
|
|
5659
|
-
severity: "error",
|
|
5660
|
-
asset: a.destination,
|
|
5661
|
-
message: `${pct(ink)} ink inside the shot: the moment shows a wallpaper, an empty canvas or a flat panel`,
|
|
5662
|
-
fixHint: "pick a moment after a gesture landed (--shot-time, or --times step:<id>) and stage the set before recording",
|
|
5663
|
-
bbox: rect
|
|
5664
|
-
});
|
|
5665
|
-
}
|
|
5666
|
-
if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
|
|
5667
|
-
out.push({
|
|
5668
|
-
code: "subject",
|
|
5669
|
-
severity: "error",
|
|
5670
|
-
asset: a.destination,
|
|
5671
|
-
message: `the shot is ${pct(a.shot.w)} of the width; a card sits at 60 to 92%, a bleed a little past the edge`,
|
|
5672
|
-
fixHint: "the template layout places the slot; fix its place for this aspect",
|
|
5673
|
-
bbox: rect
|
|
5674
|
-
});
|
|
5675
|
-
}
|
|
5676
|
-
for (const f of textFindings(a, img)) out.push(f);
|
|
5677
|
-
return out;
|
|
5678
|
-
}
|
|
5679
|
-
const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5680
|
-
if (m.ink < BLANK_INK) {
|
|
5681
|
-
out.push({
|
|
5682
|
-
code: "blank",
|
|
5683
|
-
severity: "error",
|
|
5684
|
-
asset: a.destination,
|
|
5685
|
-
message: `${pct(m.ink)} ink inside the ${m.card ? "card" : "frame"}: a wallpaper, an empty canvas or a flat panel, not the product doing something`,
|
|
5686
|
-
fixHint: "pick a moment after a gesture landed (--times step:<id>) and stage the set before recording: real data, a populated state",
|
|
5687
|
-
bbox: subject
|
|
5688
|
-
});
|
|
5689
|
-
}
|
|
5690
|
-
if (genre === "card") {
|
|
5691
|
-
if (bledAll || m.widthPct === null) {
|
|
5692
|
-
out.push({
|
|
5693
|
-
code: "subject",
|
|
5694
|
-
severity: "error",
|
|
5695
|
-
asset: a.destination,
|
|
5696
|
-
message: bledAll ? "the picture fills the frame on all four sides: a crop, not a card on a ground" : "no card found: the frame is one flat tone",
|
|
5697
|
-
fixHint: "present the card in a look (vos deliver reads BRAND.md or --look) or render this destination from a poster template"
|
|
5698
|
-
});
|
|
5699
|
-
} else if (m.widthPct < SUBJECT_BAND.min || m.widthPct > SUBJECT_BAND.max) {
|
|
5700
|
-
out.push({
|
|
5701
|
-
code: "subject",
|
|
5702
|
-
severity: "error",
|
|
5703
|
-
asset: a.destination,
|
|
5704
|
-
message: `the card is ${pct(m.widthPct)} of the width; the references sit at ${pct(SUBJECT_BAND.min)} to ${pct(SUBJECT_BAND.max)}`,
|
|
5705
|
-
fixHint: "the look places the card at 84% (frame.inset); a poster template sets its own placement",
|
|
5706
|
-
bbox: m.card ?? void 0
|
|
5707
|
-
});
|
|
5708
|
-
}
|
|
5709
|
-
if (m.card && !bledAll) {
|
|
5710
|
-
const sep = m.separation ?? 0;
|
|
5711
|
-
const shadow = m.shadow ?? 0;
|
|
5712
|
-
if (sep < SEPARATION_L && shadow < SHADOW_PRESENT && m.edge < EDGE_PRESENT) {
|
|
5713
|
-
out.push({
|
|
5714
|
-
code: "separation",
|
|
5715
|
-
severity: "error",
|
|
5716
|
-
asset: a.destination,
|
|
5717
|
-
message: `the card and the ground are ${sep.toFixed(1)} L* apart with no shadow halo (${shadow.toFixed(1)}) and no drawn edge (${m.edge.toFixed(0)}): the card dissolves into the plate`,
|
|
5718
|
-
fixHint: "a contact shadow (frame.shadowContact) plus a hairline (frame.border with borderColor) makes a light card sit on a light ground; the plate look sets both",
|
|
5719
|
-
bbox: m.card
|
|
5720
|
-
});
|
|
5721
|
-
}
|
|
5722
|
-
}
|
|
5723
|
-
}
|
|
5724
|
-
if (genre === "screenshot" && a.composed && m.card && (m.widthPct ?? 0) < 0.98) {
|
|
5725
|
-
out.push({
|
|
5726
|
-
code: "subject",
|
|
5727
|
-
severity: "error",
|
|
5728
|
-
asset: a.destination,
|
|
5729
|
-
message: `a store screenshot must be the real page full bleed; this one shows a ${pct(m.widthPct ?? 0)} card on a ground`,
|
|
5730
|
-
fixHint: "drop --composed: the screenshot genre renders the page with no chrome and no padding",
|
|
5731
|
-
bbox: m.card
|
|
5732
|
-
});
|
|
5733
|
-
}
|
|
5734
|
-
if (img.w <= 500 && (a.spec?.text === "none" || genre === "card")) {
|
|
5735
|
-
const full = edgeEnergy(img, Math.min(img.w, 400));
|
|
5736
|
-
const half = edgeEnergy(img, Math.min(img.w, 400) >> 1);
|
|
5737
|
-
const keep = full > 0 ? half / full : 1;
|
|
5738
|
-
if (keep < HALFSIZE_KEEP) {
|
|
5739
|
-
out.push({
|
|
5740
|
-
code: "halfsize",
|
|
5741
|
-
severity: "warning",
|
|
5742
|
-
asset: a.destination,
|
|
5743
|
-
message: `at half size the tile keeps ${pct(keep)} of its edges: fine text and thin lines vanish where the store shows it small`,
|
|
5744
|
-
fixHint: "a tile is the subject large and saturated with no text (the store rule); render it from the card-on-gradient template"
|
|
5745
|
-
});
|
|
5746
|
-
}
|
|
5747
|
-
}
|
|
5748
|
-
for (const f of textFindings(a, img)) out.push(f);
|
|
5749
|
-
return out;
|
|
5750
|
-
}
|
|
5751
|
-
function textFindings(a, img) {
|
|
5752
|
-
const out = [];
|
|
5753
|
-
for (const t of a.text ?? []) {
|
|
5754
|
-
const box = {
|
|
5755
|
-
x: t.x * img.w,
|
|
5756
|
-
y: t.y * img.h,
|
|
5757
|
-
w: t.w * img.w,
|
|
5758
|
-
h: t.h * img.h
|
|
5759
|
-
};
|
|
5760
|
-
const name = t.label ? `"${t.label}"` : "a text box";
|
|
5761
|
-
if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
|
|
5762
|
-
out.push({
|
|
5763
|
-
code: "sliced",
|
|
5764
|
-
severity: "error",
|
|
5765
|
-
asset: a.destination,
|
|
5766
|
-
message: `${name} crosses the frame edge: cut mid-word`,
|
|
5767
|
-
fixHint: "shorten the line, or let the template recompose (headline lines at 12 to 14% of the height, inside the safe rect)",
|
|
5768
|
-
bbox: box
|
|
5769
|
-
});
|
|
5770
|
-
}
|
|
5771
|
-
const s = a.spec?.safe;
|
|
5772
|
-
if (s) {
|
|
5773
|
-
const sx = s.x * img.w;
|
|
5774
|
-
const sy = s.y * img.h;
|
|
5775
|
-
const sw = s.w * img.w;
|
|
5776
|
-
const sh = s.h * img.h;
|
|
5777
|
-
if (box.x < sx - 0.5 || box.y < sy - 0.5 || box.x + box.w > sx + sw + 0.5 || box.y + box.h > sy + sh + 0.5) {
|
|
5778
|
-
out.push({
|
|
5779
|
-
code: "safe",
|
|
5780
|
-
severity: "warning",
|
|
5781
|
-
asset: a.destination,
|
|
5782
|
-
message: `${name} sits outside the destination's safe rect (${pct(s.w)}x${pct(s.h)} at ${pct(s.x)},${pct(s.y)}): the platform's chrome or crop covers it`,
|
|
5783
|
-
fixHint: "move the text inside the safe rect; the template reads channel-specs safe",
|
|
5784
|
-
bbox: box
|
|
5785
|
-
});
|
|
5786
|
-
}
|
|
5787
|
-
}
|
|
5788
|
-
const rgb = t.color ? hexToRgb2(t.color) : null;
|
|
5789
|
-
if (rgb) {
|
|
5790
|
-
const ground = medianColour(img, box);
|
|
5791
|
-
const lc = apcaContrast(rgb, ground);
|
|
5792
|
-
const floor = t.role === "body" ? 75 : 60;
|
|
5793
|
-
if (lc < floor) {
|
|
5794
|
-
out.push({
|
|
5795
|
-
code: "contrast",
|
|
5796
|
-
severity: "warning",
|
|
5797
|
-
asset: a.destination,
|
|
5798
|
-
message: `${name} reads APCA Lc ${lc.toFixed(0)} on its ground; ${t.role === "body" ? "body" : "a headline"} wants ${floor}`,
|
|
5799
|
-
fixHint: "darken the ink or lighten the ground under the text (BRAND.md ink on bgA/bgB, never on the shot)",
|
|
5800
|
-
bbox: box
|
|
5801
|
-
});
|
|
5802
|
-
}
|
|
5803
|
-
}
|
|
5804
|
-
}
|
|
5805
|
-
if (a.spec?.text === "none" && (a.text?.length ?? 0) > 0) {
|
|
5806
|
-
out.push({
|
|
5807
|
-
code: "safe",
|
|
5808
|
-
severity: "warning",
|
|
5809
|
-
asset: a.destination,
|
|
5810
|
-
message: "this destination wants no text (the picture carries it alone) and the kit put words on it",
|
|
5811
|
-
fixHint: "render the tile from the card-on-gradient template with no headline"
|
|
5812
|
-
});
|
|
5813
|
-
}
|
|
5814
|
-
return out;
|
|
5815
|
-
}
|
|
5816
|
-
function duplicateFindings(stills) {
|
|
5817
|
-
const groups = [];
|
|
5818
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5819
|
-
const comparable = (a, b) => a.genre === "screenshot" || b.genre === "screenshot" ? a.destination === b.destination : true;
|
|
5820
|
-
for (let i = 0; i < stills.length; i++) {
|
|
5821
|
-
if (seen.has(i)) continue;
|
|
5822
|
-
const g = [stills[i]];
|
|
5823
|
-
for (let j = i + 1; j < stills.length; j++) {
|
|
5824
|
-
if (seen.has(j)) continue;
|
|
5825
|
-
if (!comparable(stills[i], stills[j])) continue;
|
|
5826
|
-
if (hammingDistance(stills[i].hash, stills[j].hash) <= DUPLICATE_BITS2) {
|
|
5827
|
-
g.push(stills[j]);
|
|
5828
|
-
seen.add(j);
|
|
5829
|
-
}
|
|
5830
|
-
}
|
|
5831
|
-
if (g.length > 1) groups.push(g);
|
|
5832
|
-
}
|
|
5833
|
-
return groups.map((g) => {
|
|
5834
|
-
const composed = g.every(
|
|
5835
|
-
(s) => stills.find((x) => x.destination === s.destination)?.composed
|
|
5836
|
-
);
|
|
5837
|
-
return {
|
|
5838
|
-
code: "duplicate",
|
|
5839
|
-
severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
|
|
5840
|
-
asset: g.map((s) => s.destination).join(", "),
|
|
5841
|
-
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(", ")}`,
|
|
5842
|
-
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"
|
|
5843
|
-
};
|
|
5844
|
-
});
|
|
5845
|
-
}
|
|
5846
4815
|
async function videoFrame(file, at2, ffmpeg2) {
|
|
5847
4816
|
try {
|
|
5848
4817
|
const { stdout } = await execFileP(
|
|
@@ -5877,91 +4846,62 @@ async function onPath(bin) {
|
|
|
5877
4846
|
return false;
|
|
5878
4847
|
}
|
|
5879
4848
|
}
|
|
4849
|
+
function frameFindings(asset, name, img, m) {
|
|
4850
|
+
const out = [];
|
|
4851
|
+
const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
4852
|
+
const ink = inkCoverage(img, subject);
|
|
4853
|
+
if (ink < BLANK_INK) {
|
|
4854
|
+
out.push({
|
|
4855
|
+
code: "firstlast",
|
|
4856
|
+
severity: "error",
|
|
4857
|
+
asset,
|
|
4858
|
+
message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
|
|
4859
|
+
fixHint: name === "first" ? "open on the product (trim the head, or an entrance over a populated frame)" : "end on a resolved state or an end card"
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
if (m.bleed.length === 4) {
|
|
4863
|
+
out.push({
|
|
4864
|
+
code: "firstlast",
|
|
4865
|
+
severity: "warning",
|
|
4866
|
+
asset,
|
|
4867
|
+
message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
|
|
4868
|
+
fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
|
|
4869
|
+
});
|
|
4870
|
+
}
|
|
4871
|
+
return out;
|
|
4872
|
+
}
|
|
5880
4873
|
async function pictureChecks(assets) {
|
|
5881
4874
|
const findings = [];
|
|
5882
4875
|
const measured = [];
|
|
5883
|
-
const stills = [];
|
|
5884
4876
|
let ffmpeg2 = null;
|
|
5885
4877
|
for (const a of assets) {
|
|
5886
|
-
if (
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
continue;
|
|
5898
|
-
}
|
|
5899
|
-
const m = measureStill(img);
|
|
5900
|
-
measured.push({ destination: a.destination, measure: m });
|
|
5901
|
-
findings.push(...stillFindings(a, img, m));
|
|
5902
|
-
if (a.spec?.kind !== "video")
|
|
5903
|
-
stills.push({
|
|
5904
|
-
destination: a.destination,
|
|
5905
|
-
hash: m.hash,
|
|
5906
|
-
time: null,
|
|
5907
|
-
genre: a.spec?.genre,
|
|
5908
|
-
composed: !!a.shot
|
|
5909
|
-
});
|
|
4878
|
+
if (!/\.(mp4|webm|mov)$/i.test(a.file)) continue;
|
|
4879
|
+
if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
|
|
4880
|
+
if (!ffmpeg2) {
|
|
4881
|
+
findings.push({
|
|
4882
|
+
code: "firstlast",
|
|
4883
|
+
severity: "info",
|
|
4884
|
+
asset: a.destination,
|
|
4885
|
+
message: "ffmpeg is not on PATH, so the first and last frames were not read",
|
|
4886
|
+
fixHint: "install ffmpeg to have the video checks run"
|
|
4887
|
+
});
|
|
4888
|
+
measured.push({ destination: a.destination, measure: null });
|
|
5910
4889
|
continue;
|
|
5911
4890
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
measured.push({ destination: a.destination, measure:
|
|
5923
|
-
|
|
5924
|
-
}
|
|
5925
|
-
const seconds = a.seconds ?? 0;
|
|
5926
|
-
const first = await videoFrame(a.file, 0, "ffmpeg");
|
|
5927
|
-
const last = await videoFrame(
|
|
5928
|
-
a.file,
|
|
5929
|
-
Math.max(0, seconds - 0.1),
|
|
5930
|
-
"ffmpeg"
|
|
5931
|
-
);
|
|
5932
|
-
for (const [name, img] of [
|
|
5933
|
-
["first", first],
|
|
5934
|
-
["last", last]
|
|
5935
|
-
]) {
|
|
5936
|
-
if (!img) continue;
|
|
5937
|
-
const m = measureStill(img);
|
|
5938
|
-
if (name === "first")
|
|
5939
|
-
measured.push({ destination: a.destination, measure: m });
|
|
5940
|
-
const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5941
|
-
const ink = inkCoverage(img, subject);
|
|
5942
|
-
if (ink < BLANK_INK) {
|
|
5943
|
-
findings.push({
|
|
5944
|
-
code: "firstlast",
|
|
5945
|
-
severity: "error",
|
|
5946
|
-
asset: a.destination,
|
|
5947
|
-
message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
|
|
5948
|
-
fixHint: name === "first" ? "open on the product (trim the head, or an entrance over a populated frame)" : "end on a resolved state or an end card (the last frame is the poster)"
|
|
5949
|
-
});
|
|
5950
|
-
}
|
|
5951
|
-
if (m.bleed.length === 4 && a.spec?.genre !== "screenshot") {
|
|
5952
|
-
findings.push({
|
|
5953
|
-
code: "firstlast",
|
|
5954
|
-
severity: "warning",
|
|
5955
|
-
asset: a.destination,
|
|
5956
|
-
message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
|
|
5957
|
-
fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
|
|
5958
|
-
});
|
|
5959
|
-
}
|
|
5960
|
-
}
|
|
5961
|
-
continue;
|
|
4891
|
+
const seconds = a.seconds ?? 0;
|
|
4892
|
+
const first = await videoFrame(a.file, 0, "ffmpeg");
|
|
4893
|
+
const last = await videoFrame(a.file, Math.max(0, seconds - 0.1), "ffmpeg");
|
|
4894
|
+
for (const [name, img] of [
|
|
4895
|
+
["first", first],
|
|
4896
|
+
["last", last]
|
|
4897
|
+
]) {
|
|
4898
|
+
if (!img) continue;
|
|
4899
|
+
const m = measureStill(img);
|
|
4900
|
+
if (name === "first")
|
|
4901
|
+
measured.push({ destination: a.destination, measure: m });
|
|
4902
|
+
findings.push(...frameFindings(a.destination, name, img, m));
|
|
5962
4903
|
}
|
|
5963
4904
|
}
|
|
5964
|
-
findings.push(...duplicateFindings(stills));
|
|
5965
4905
|
const order = {
|
|
5966
4906
|
error: 0,
|
|
5967
4907
|
warning: 1,
|
|
@@ -5977,28 +4917,15 @@ function formatFinding(f) {
|
|
|
5977
4917
|
}
|
|
5978
4918
|
|
|
5979
4919
|
// src/plugin/validateKit.ts
|
|
5980
|
-
import { existsSync as
|
|
5981
|
-
import { readFile as
|
|
5982
|
-
import { dirname
|
|
5983
|
-
import { DESTINATIONS as
|
|
5984
|
-
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5985
|
-
function pngDimensions(bytes) {
|
|
5986
|
-
if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIG2)) return null;
|
|
5987
|
-
if (bytes.toString("ascii", 12, 16) !== "IHDR") return null;
|
|
5988
|
-
return { w: bytes.readUInt32BE(16), h: bytes.readUInt32BE(20) };
|
|
5989
|
-
}
|
|
5990
|
-
function sniffImage(bytes) {
|
|
5991
|
-
if (bytes.subarray(0, 8).equals(PNG_SIG2)) return "png";
|
|
5992
|
-
if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
|
|
5993
|
-
return "webp";
|
|
5994
|
-
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216) return "jpeg";
|
|
5995
|
-
return "unknown";
|
|
5996
|
-
}
|
|
4920
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4921
|
+
import { readFile as readFile7, stat } from "fs/promises";
|
|
4922
|
+
import { dirname, isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
4923
|
+
import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
|
|
5997
4924
|
async function probeVideo(path) {
|
|
5998
4925
|
const MB = await import("mediabunny");
|
|
5999
4926
|
const input = new MB.Input({
|
|
6000
4927
|
formats: MB.ALL_FORMATS,
|
|
6001
|
-
source: new MB.BufferSource(new Uint8Array(await
|
|
4928
|
+
source: new MB.BufferSource(new Uint8Array(await readFile7(path)))
|
|
6002
4929
|
});
|
|
6003
4930
|
try {
|
|
6004
4931
|
const track = await input.getPrimaryVideoTrack();
|
|
@@ -6010,7 +4937,7 @@ async function probeVideo(path) {
|
|
|
6010
4937
|
}
|
|
6011
4938
|
}
|
|
6012
4939
|
var specById = new Map(
|
|
6013
|
-
|
|
4940
|
+
DESTINATIONS2.map((d) => [d.id, d])
|
|
6014
4941
|
);
|
|
6015
4942
|
var near = (a, b, tol) => Math.abs(a - b) <= tol;
|
|
6016
4943
|
async function validateKit(kitPath, opts = {}) {
|
|
@@ -6019,7 +4946,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6019
4946
|
const measured = [];
|
|
6020
4947
|
let kit;
|
|
6021
4948
|
try {
|
|
6022
|
-
kit = JSON.parse(await
|
|
4949
|
+
kit = JSON.parse(await readFile7(kitPath, "utf8"));
|
|
6023
4950
|
} catch (e) {
|
|
6024
4951
|
return {
|
|
6025
4952
|
valid: false,
|
|
@@ -6036,13 +4963,12 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6036
4963
|
measured
|
|
6037
4964
|
};
|
|
6038
4965
|
}
|
|
6039
|
-
const base =
|
|
4966
|
+
const base = dirname(kitPath);
|
|
6040
4967
|
const resolvePath = (p) => {
|
|
6041
|
-
if (
|
|
6042
|
-
const beside =
|
|
4968
|
+
if (isAbsolute2(p) && existsSync7(p)) return p;
|
|
4969
|
+
const beside = join7(base, p.split("/").pop() ?? p);
|
|
6043
4970
|
return beside;
|
|
6044
4971
|
};
|
|
6045
|
-
const perDestination = /* @__PURE__ */ new Map();
|
|
6046
4972
|
const pictureAssets = [];
|
|
6047
4973
|
for (const a of kit.assets) {
|
|
6048
4974
|
const id = a.destination ?? `${a.channel}-${a.asset}`;
|
|
@@ -6050,12 +4976,11 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6050
4976
|
const file = resolvePath(a.path);
|
|
6051
4977
|
let bytes;
|
|
6052
4978
|
try {
|
|
6053
|
-
bytes = (await
|
|
4979
|
+
bytes = (await stat(file)).size;
|
|
6054
4980
|
} catch {
|
|
6055
4981
|
problems.push(`${label}: ${a.path} is missing`);
|
|
6056
4982
|
continue;
|
|
6057
4983
|
}
|
|
6058
|
-
perDestination.set(id, (perDestination.get(id) ?? 0) + 1);
|
|
6059
4984
|
if (bytes !== a.bytes)
|
|
6060
4985
|
problems.push(
|
|
6061
4986
|
`${label}: manifest says ${a.bytes} bytes, the file is ${bytes}`
|
|
@@ -6064,25 +4989,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6064
4989
|
let w = null;
|
|
6065
4990
|
let h = null;
|
|
6066
4991
|
let seconds = null;
|
|
6067
|
-
if (/\.(
|
|
6068
|
-
const head = Buffer.from(await readFile10(file));
|
|
6069
|
-
const kind = sniffImage(head);
|
|
6070
|
-
if (file.toLowerCase().endsWith(".png") && kind !== "png")
|
|
6071
|
-
problems.push(
|
|
6072
|
-
`${label}: ${a.path} is named .png but its bytes are ${kind} \u2014 stores refuse a mislabelled image`
|
|
6073
|
-
);
|
|
6074
|
-
const dims = kind === "png" ? pngDimensions(head) : null;
|
|
6075
|
-
if (dims) {
|
|
6076
|
-
w = dims.w;
|
|
6077
|
-
h = dims.h;
|
|
6078
|
-
if (w !== a.w || h !== a.h)
|
|
6079
|
-
problems.push(
|
|
6080
|
-
`${label}: manifest says ${a.w}x${a.h}, the file is ${w}x${h}`
|
|
6081
|
-
);
|
|
6082
|
-
} else if (kind === "png") {
|
|
6083
|
-
problems.push(`${label}: ${a.path} has no readable IHDR`);
|
|
6084
|
-
}
|
|
6085
|
-
} else if (/\.(mp4|webm|mov)$/i.test(file)) {
|
|
4992
|
+
if (/\.(mp4|webm|mov)$/i.test(file)) {
|
|
6086
4993
|
try {
|
|
6087
4994
|
const v = await probeVideo(file);
|
|
6088
4995
|
if (!v) problems.push(`${label}: ${a.path} has no video track`);
|
|
@@ -6105,21 +5012,14 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6105
5012
|
);
|
|
6106
5013
|
}
|
|
6107
5014
|
} else {
|
|
6108
|
-
warnings.push(
|
|
6109
|
-
`${label}: ${a.path} is not an image or video the kit verifies`
|
|
6110
|
-
);
|
|
5015
|
+
warnings.push(`${label}: ${a.path} is not a video the kit verifies`);
|
|
6111
5016
|
}
|
|
6112
5017
|
measured.push({ destination: id, path: a.path, w, h, bytes, seconds });
|
|
6113
5018
|
pictureAssets.push({
|
|
6114
5019
|
destination: id,
|
|
6115
5020
|
path: a.path,
|
|
6116
5021
|
file,
|
|
6117
|
-
|
|
6118
|
-
text: a.text,
|
|
6119
|
-
seconds,
|
|
6120
|
-
composed: a.composed,
|
|
6121
|
-
shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
|
|
6122
|
-
crop: a.crop
|
|
5022
|
+
seconds
|
|
6123
5023
|
});
|
|
6124
5024
|
if (!spec) {
|
|
6125
5025
|
if (a.channel !== "demo")
|
|
@@ -6146,17 +5046,6 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6146
5046
|
`${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
6147
5047
|
);
|
|
6148
5048
|
}
|
|
6149
|
-
if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
|
|
6150
|
-
warnings.push(
|
|
6151
|
-
`${label}: spec renders png, the file is ${file.split(".").pop()}`
|
|
6152
|
-
);
|
|
6153
|
-
}
|
|
6154
|
-
for (const [id, n] of perDestination) {
|
|
6155
|
-
const spec = specById.get(id);
|
|
6156
|
-
if (spec?.count && (n < spec.count.min || n > spec.count.max))
|
|
6157
|
-
problems.push(
|
|
6158
|
-
`${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
|
|
6159
|
-
);
|
|
6160
5049
|
}
|
|
6161
5050
|
if (!opts.picture) {
|
|
6162
5051
|
return { valid: problems.length === 0, problems, warnings, measured };
|
|
@@ -6175,7 +5064,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6175
5064
|
|
|
6176
5065
|
// src/plugin/recorder.ts
|
|
6177
5066
|
import { writeFileSync } from "fs";
|
|
6178
|
-
import { join as
|
|
5067
|
+
import { join as join8 } from "path";
|
|
6179
5068
|
import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
|
|
6180
5069
|
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
6181
5070
|
const dry = opts.dryRun === true;
|
|
@@ -6185,7 +5074,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6185
5074
|
actions,
|
|
6186
5075
|
{
|
|
6187
5076
|
frame: (file, bytes) => {
|
|
6188
|
-
writeFileSync(
|
|
5077
|
+
writeFileSync(join8(paths.framesDir, file), bytes);
|
|
6189
5078
|
}
|
|
6190
5079
|
},
|
|
6191
5080
|
log,
|
|
@@ -6237,11 +5126,11 @@ import {
|
|
|
6237
5126
|
|
|
6238
5127
|
// src/plugin/session.ts
|
|
6239
5128
|
import { spawn } from "child_process";
|
|
6240
|
-
import { existsSync as
|
|
5129
|
+
import { existsSync as existsSync8, mkdirSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
|
|
6241
5130
|
import { homedir } from "os";
|
|
6242
|
-
import { join as
|
|
5131
|
+
import { join as join9 } from "path";
|
|
6243
5132
|
import { chromium } from "playwright";
|
|
6244
|
-
var SESSIONS_DIR =
|
|
5133
|
+
var SESSIONS_DIR = join9(homedir(), ".config", "vos", "sessions");
|
|
6245
5134
|
var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
6246
5135
|
function sessionNameVerdict(name) {
|
|
6247
5136
|
if (!name) return "a session needs a name: --name <app>";
|
|
@@ -6250,20 +5139,20 @@ function sessionNameVerdict(name) {
|
|
|
6250
5139
|
return null;
|
|
6251
5140
|
}
|
|
6252
5141
|
function sessionDir(name) {
|
|
6253
|
-
return
|
|
5142
|
+
return join9(SESSIONS_DIR, name);
|
|
6254
5143
|
}
|
|
6255
5144
|
function systemChromePath() {
|
|
6256
5145
|
const explicit = process.env.VOS_BROWSER_PATH;
|
|
6257
5146
|
if (explicit) return explicit;
|
|
6258
5147
|
const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
|
|
6259
|
-
|
|
5148
|
+
join9(
|
|
6260
5149
|
process.env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
6261
5150
|
"Google",
|
|
6262
5151
|
"Chrome",
|
|
6263
5152
|
"Application",
|
|
6264
5153
|
"chrome.exe"
|
|
6265
5154
|
),
|
|
6266
|
-
|
|
5155
|
+
join9(
|
|
6267
5156
|
process.env["LOCALAPPDATA"] ?? "",
|
|
6268
5157
|
"Google",
|
|
6269
5158
|
"Chrome",
|
|
@@ -6275,7 +5164,7 @@ function systemChromePath() {
|
|
|
6275
5164
|
"/usr/bin/google-chrome-stable",
|
|
6276
5165
|
"/opt/google/chrome/chrome"
|
|
6277
5166
|
];
|
|
6278
|
-
const found = candidates.find((c) =>
|
|
5167
|
+
const found = candidates.find((c) => existsSync8(c));
|
|
6279
5168
|
if (!found)
|
|
6280
5169
|
throw new Error(
|
|
6281
5170
|
"no Google Chrome found: a session needs the system Chrome (the bundled Chromium cannot read a profile Chrome wrote). Install Chrome, or set VOS_BROWSER_PATH"
|
|
@@ -6298,15 +5187,15 @@ async function openSession(name, url, opts = { log: () => {
|
|
|
6298
5187
|
opts.log(
|
|
6299
5188
|
`a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
|
|
6300
5189
|
);
|
|
6301
|
-
return new Promise((
|
|
5190
|
+
return new Promise((resolve14, reject) => {
|
|
6302
5191
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
6303
5192
|
child.on("error", reject);
|
|
6304
|
-
child.on("exit", (code) =>
|
|
5193
|
+
child.on("exit", (code) => resolve14({ code }));
|
|
6305
5194
|
});
|
|
6306
5195
|
}
|
|
6307
5196
|
async function withSession(name, fn, opts = {}) {
|
|
6308
5197
|
const dir = sessionDir(name);
|
|
6309
|
-
if (!
|
|
5198
|
+
if (!existsSync8(dir))
|
|
6310
5199
|
throw new Error(`no session "${name}" (vos session list)`);
|
|
6311
5200
|
const explicit = process.env.VOS_BROWSER_PATH;
|
|
6312
5201
|
const ctx = await chromium.launchPersistentContext(dir, {
|
|
@@ -6355,9 +5244,9 @@ function summaryLine(s) {
|
|
|
6355
5244
|
return `session "${s.name}" holds cookies for ${hosts}${more}; ${exp}`;
|
|
6356
5245
|
}
|
|
6357
5246
|
function listSessions() {
|
|
6358
|
-
if (!
|
|
5247
|
+
if (!existsSync8(SESSIONS_DIR)) return [];
|
|
6359
5248
|
return readdirSync(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
|
|
6360
|
-
const st = statSync2(
|
|
5249
|
+
const st = statSync2(join9(SESSIONS_DIR, d.name));
|
|
6361
5250
|
return {
|
|
6362
5251
|
name: d.name,
|
|
6363
5252
|
ageDays: Math.floor(
|
|
@@ -6368,7 +5257,7 @@ function listSessions() {
|
|
|
6368
5257
|
}
|
|
6369
5258
|
function removeSession(name) {
|
|
6370
5259
|
const dir = sessionDir(name);
|
|
6371
|
-
if (!
|
|
5260
|
+
if (!existsSync8(dir)) return false;
|
|
6372
5261
|
rmSync(dir, { recursive: true, force: true });
|
|
6373
5262
|
return true;
|
|
6374
5263
|
}
|
|
@@ -6433,9 +5322,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6433
5322
|
}
|
|
6434
5323
|
|
|
6435
5324
|
// src/plugin/plan.ts
|
|
6436
|
-
import { existsSync as
|
|
6437
|
-
import { readFile as
|
|
6438
|
-
import { join as
|
|
5325
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5326
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5327
|
+
import { join as join10 } from "path";
|
|
6439
5328
|
import {
|
|
6440
5329
|
DEFAULT_FRAME_STYLE,
|
|
6441
5330
|
STYLE_FIELDS,
|
|
@@ -6657,10 +5546,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6657
5546
|
// src/plugin/plan.ts
|
|
6658
5547
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6659
5548
|
async function readDigestActivity(dir) {
|
|
6660
|
-
const file =
|
|
6661
|
-
if (!
|
|
5549
|
+
const file = join10(dir, "digest", "digest.json");
|
|
5550
|
+
if (!existsSync9(file)) return null;
|
|
6662
5551
|
try {
|
|
6663
|
-
const d = JSON.parse(await
|
|
5552
|
+
const d = JSON.parse(await readFile8(file, "utf8"));
|
|
6664
5553
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
6665
5554
|
} catch {
|
|
6666
5555
|
return null;
|
|
@@ -6874,17 +5763,17 @@ async function planTake(dir, opts = {}) {
|
|
|
6874
5763
|
|
|
6875
5764
|
// src/plugin/sync.ts
|
|
6876
5765
|
import { createHash } from "crypto";
|
|
6877
|
-
import { existsSync as
|
|
6878
|
-
import { readFile as
|
|
5766
|
+
import { existsSync as existsSync10, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
5767
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
6879
5768
|
import { createInterface } from "readline/promises";
|
|
6880
|
-
import { basename, join as
|
|
6881
|
-
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as
|
|
5769
|
+
import { basename, join as join12 } from "path";
|
|
5770
|
+
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
6882
5771
|
|
|
6883
5772
|
// src/plugin/folder.ts
|
|
6884
|
-
import { mkdir as
|
|
6885
|
-
import { join as
|
|
5773
|
+
import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
|
|
5774
|
+
import { join as join11 } from "path";
|
|
6886
5775
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6887
|
-
import { migrateHostedDoc
|
|
5776
|
+
import { migrateHostedDoc } from "@vosjs/studio-core";
|
|
6888
5777
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
|
|
6889
5778
|
async function listFolders(origin, key) {
|
|
6890
5779
|
const r = await apiJson(origin, "/api/folders", { key });
|
|
@@ -7058,25 +5947,25 @@ async function cmdPull(argv) {
|
|
|
7058
5947
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7059
5948
|
const payload = res.body;
|
|
7060
5949
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7061
|
-
await
|
|
5950
|
+
await mkdir5(join11(out, "recipes"), { recursive: true });
|
|
7062
5951
|
const lines = [];
|
|
7063
5952
|
const recipes = [];
|
|
7064
5953
|
for (const rec of payload.recipes) {
|
|
7065
|
-
const path =
|
|
7066
|
-
await
|
|
5954
|
+
const path = join11(out, "recipes", safeName(rec.filename));
|
|
5955
|
+
await writeFile7(path, rec.body ?? "");
|
|
7067
5956
|
recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
|
|
7068
5957
|
}
|
|
7069
5958
|
for (const rec of payload.inheritedRecipes) {
|
|
7070
5959
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7071
|
-
await
|
|
7072
|
-
const path =
|
|
5960
|
+
await mkdir5(join11(out, "recipes", "_inherited", from), { recursive: true });
|
|
5961
|
+
const path = join11(
|
|
7073
5962
|
out,
|
|
7074
5963
|
"recipes",
|
|
7075
5964
|
"_inherited",
|
|
7076
5965
|
from,
|
|
7077
5966
|
safeName(rec.filename)
|
|
7078
5967
|
);
|
|
7079
|
-
await
|
|
5968
|
+
await writeFile7(path, rec.body ?? "");
|
|
7080
5969
|
recipes.push({
|
|
7081
5970
|
path,
|
|
7082
5971
|
line: `${recipeLine(rec.filename, rec.body ?? "")} [inherited from ${rec.folderName ?? from}]`
|
|
@@ -7084,15 +5973,15 @@ async function cmdPull(argv) {
|
|
|
7084
5973
|
}
|
|
7085
5974
|
const members = [];
|
|
7086
5975
|
for (const v of payload.voses) {
|
|
7087
|
-
const dir =
|
|
7088
|
-
await
|
|
5976
|
+
const dir = join11(out, "members", safeName(v.slug || v.title || v.id));
|
|
5977
|
+
await mkdir5(dir, { recursive: true });
|
|
7089
5978
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7090
5979
|
if (cfg.status !== 200) {
|
|
7091
5980
|
lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
|
|
7092
5981
|
continue;
|
|
7093
5982
|
}
|
|
7094
|
-
await
|
|
7095
|
-
|
|
5983
|
+
await writeFile7(
|
|
5984
|
+
join11(dir, "config.json"),
|
|
7096
5985
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7097
5986
|
);
|
|
7098
5987
|
let take = false;
|
|
@@ -7103,13 +5992,13 @@ async function cmdPull(argv) {
|
|
|
7103
5992
|
{ key }
|
|
7104
5993
|
);
|
|
7105
5994
|
if (doc.status === 200) {
|
|
7106
|
-
await
|
|
7107
|
-
|
|
5995
|
+
await writeFile7(
|
|
5996
|
+
join11(dir, "doc.json"),
|
|
7108
5997
|
JSON.stringify(doc.body, null, 2)
|
|
7109
5998
|
);
|
|
7110
5999
|
take = true;
|
|
7111
6000
|
if (flags.media === true) {
|
|
7112
|
-
const hosted =
|
|
6001
|
+
const hosted = migrateHostedDoc(doc.body);
|
|
7113
6002
|
await pullMedia({ origin, key }, dir, hosted, r.log);
|
|
7114
6003
|
}
|
|
7115
6004
|
}
|
|
@@ -7230,8 +6119,8 @@ function storageStateInDir(dir) {
|
|
|
7230
6119
|
if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
|
|
7231
6120
|
continue;
|
|
7232
6121
|
try {
|
|
7233
|
-
const v = JSON.parse(readFileSync2(
|
|
7234
|
-
if (looksLikeStorageState(v)) return
|
|
6122
|
+
const v = JSON.parse(readFileSync2(join12(dir, f), "utf8"));
|
|
6123
|
+
if (looksLikeStorageState(v)) return join12(dir, f);
|
|
7235
6124
|
} catch {
|
|
7236
6125
|
}
|
|
7237
6126
|
}
|
|
@@ -7252,7 +6141,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7252
6141
|
throw new Error(`doc.json fails lint:
|
|
7253
6142
|
${lint.problems.join("\n ")}`);
|
|
7254
6143
|
}
|
|
7255
|
-
if (!
|
|
6144
|
+
if (!existsSync10(take.paths.recording)) {
|
|
7256
6145
|
throw new Error(
|
|
7257
6146
|
"no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
|
|
7258
6147
|
);
|
|
@@ -7301,7 +6190,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7301
6190
|
throw new Error("push cancelled");
|
|
7302
6191
|
}
|
|
7303
6192
|
}
|
|
7304
|
-
const bytes = await
|
|
6193
|
+
const bytes = await readFile9(take.paths.recording);
|
|
7305
6194
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7306
6195
|
const { type: contentType } = resolveMediaType({
|
|
7307
6196
|
head: bytes.subarray(0, MEDIA_HEAD_BYTES),
|
|
@@ -7473,9 +6362,9 @@ async function pullTake(dir, flags, r) {
|
|
|
7473
6362
|
};
|
|
7474
6363
|
}
|
|
7475
6364
|
let media2;
|
|
7476
|
-
if (flags.media &&
|
|
6365
|
+
if (flags.media && existsSync10(join12(dir, "doc.json"))) {
|
|
7477
6366
|
const local = JSON.parse(
|
|
7478
|
-
await
|
|
6367
|
+
await readFile9(join12(dir, "doc.json"), "utf8")
|
|
7479
6368
|
);
|
|
7480
6369
|
media2 = await pullMedia(ctx, dir, local, r.log);
|
|
7481
6370
|
}
|
|
@@ -7533,13 +6422,13 @@ async function pullTake(dir, flags, r) {
|
|
|
7533
6422
|
docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
|
|
7534
6423
|
);
|
|
7535
6424
|
}
|
|
7536
|
-
const hostedDoc =
|
|
6425
|
+
const hostedDoc = migrateHostedDoc2(docRes.json);
|
|
7537
6426
|
const doc = hostedDoc;
|
|
7538
6427
|
const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
7539
|
-
if (
|
|
6428
|
+
if (existsSync10(localRecording)) {
|
|
7540
6429
|
doc.source.videoKey = basename(localRecording);
|
|
7541
6430
|
}
|
|
7542
|
-
await writeJson(
|
|
6431
|
+
await writeJson(join12(dir, "doc.json"), doc, true);
|
|
7543
6432
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
7544
6433
|
const versions = await api(ctx, `/vos/${vosId}/versions`);
|
|
7545
6434
|
const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
|
|
@@ -7556,18 +6445,90 @@ async function pullTake(dir, flags, r) {
|
|
|
7556
6445
|
media
|
|
7557
6446
|
};
|
|
7558
6447
|
}
|
|
6448
|
+
async function pushTakeClaimable(dir, flags, r) {
|
|
6449
|
+
const stateFile = storageStateInDir(dir);
|
|
6450
|
+
if (stateFile)
|
|
6451
|
+
throw new Error(
|
|
6452
|
+
`${stateFile} looks like a Playwright storage state (cookies + origins): a live session in the take directory would be sent with it. Move it out of the take and push again`
|
|
6453
|
+
);
|
|
6454
|
+
const take = await loadTake(dir);
|
|
6455
|
+
if (!take.doc) {
|
|
6456
|
+
throw new Error("no doc.json in this take \u2014 run `vos plan` first");
|
|
6457
|
+
}
|
|
6458
|
+
const lint = lintDoc(take.doc);
|
|
6459
|
+
if (lint.problems.length) {
|
|
6460
|
+
throw new Error(`doc.json fails lint:
|
|
6461
|
+
${lint.problems.join("\n ")}`);
|
|
6462
|
+
}
|
|
6463
|
+
const state = readSyncState(dir);
|
|
6464
|
+
const hosted = state?.hostedMedia ?? {};
|
|
6465
|
+
const docForPush = structuredClone(take.doc);
|
|
6466
|
+
const refs = [
|
|
6467
|
+
...isTakeRelativeKey(docForPush.source.videoKey) ? [
|
|
6468
|
+
{
|
|
6469
|
+
where: "the recording",
|
|
6470
|
+
key: docForPush.source.videoKey,
|
|
6471
|
+
set: (next) => {
|
|
6472
|
+
docForPush.source.videoKey = next;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
] : [],
|
|
6476
|
+
...sourceSidecarRefs(docForPush),
|
|
6477
|
+
...docMediaRefs(docForPush)
|
|
6478
|
+
];
|
|
6479
|
+
for (const ref of refs) {
|
|
6480
|
+
const file = takeRelativeFile(ref.key);
|
|
6481
|
+
const origin2 = hosted[file];
|
|
6482
|
+
if (!origin2) {
|
|
6483
|
+
throw new Error(
|
|
6484
|
+
`${ref.where} (${file}) is not a file this take fetched from vos.so, and a claimable push never uploads footage. Push the take with an account (vos login, then vos push ${dir}), or remix a public take fetched with \`vos fetch <url> --media\``
|
|
6485
|
+
);
|
|
6486
|
+
}
|
|
6487
|
+
const path = join12(dir, file);
|
|
6488
|
+
const now = existsSync10(path) ? await fileSha256(path) : null;
|
|
6489
|
+
if (now !== origin2.sha256) {
|
|
6490
|
+
throw new Error(
|
|
6491
|
+
`${ref.where} (${file}) changed since it was fetched, so it is no longer the hosted file. A claimable push names only the original; push with an account to upload the new one`
|
|
6492
|
+
);
|
|
6493
|
+
}
|
|
6494
|
+
ref.set(origin2.ref);
|
|
6495
|
+
}
|
|
6496
|
+
const lowered = lowerToComposition3(docForPush);
|
|
6497
|
+
const config = { ...lowered.config, data: lowered.data };
|
|
6498
|
+
const title = (flags.title ?? (state?.title ? `${state.title} remix` : basename(dir))).slice(0, 200);
|
|
6499
|
+
const body = { title, config, doc: docForPush };
|
|
6500
|
+
if (state?.vosId) body.remixOfId = state.vosId;
|
|
6501
|
+
if (flags.prompt) body.prompt = flags.prompt;
|
|
6502
|
+
if (flags.share) body.share = true;
|
|
6503
|
+
const origin = platformOrigin(flags);
|
|
6504
|
+
r.log(`creating a claimable push of ${title}\u2026`);
|
|
6505
|
+
const res = await apiJson(origin, "/api/claim", { method: "POST", body });
|
|
6506
|
+
if (res.status !== 201) {
|
|
6507
|
+
throw new Error(
|
|
6508
|
+
`claimable push failed (${res.status}): ${String(res.body.error ?? "")}${res.body.hint ? ` (${String(res.body.hint)})` : ""}`
|
|
6509
|
+
);
|
|
6510
|
+
}
|
|
6511
|
+
serverWarnings(res.body, (l) => r.log(l));
|
|
6512
|
+
const created = res.body.vos ?? {};
|
|
6513
|
+
return {
|
|
6514
|
+
id: created.id ?? null,
|
|
6515
|
+
title,
|
|
6516
|
+
claimUrl: String(res.body.claimUrl ?? ""),
|
|
6517
|
+
expiresAt: String(res.body.expiresAt ?? "")
|
|
6518
|
+
};
|
|
6519
|
+
}
|
|
7559
6520
|
|
|
7560
6521
|
// src/plugin/program.ts
|
|
7561
|
-
import { existsSync as
|
|
7562
|
-
import { mkdir as
|
|
6522
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6523
|
+
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
7563
6524
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7564
|
-
import { basename as basename3, dirname as
|
|
6525
|
+
import { basename as basename3, dirname as dirname2, join as join13 } from "path";
|
|
7565
6526
|
import {
|
|
7566
6527
|
CURRENT_CONFIG_VERSION,
|
|
7567
6528
|
migrateConfig,
|
|
7568
6529
|
vosConfigJsonSchema
|
|
7569
6530
|
} from "@vosjs/core";
|
|
7570
|
-
import { lowerProgramDoc, migrateHostedDoc as
|
|
6531
|
+
import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
7571
6532
|
|
|
7572
6533
|
// src/plugin/login.ts
|
|
7573
6534
|
import { hostname } from "os";
|
|
@@ -7695,12 +6656,12 @@ async function handoffLogin(url, r, opts = {}) {
|
|
|
7695
6656
|
|
|
7696
6657
|
// src/plugin/programFiles.ts
|
|
7697
6658
|
import { basename as basename2, extname as extname2, relative as relative2 } from "path";
|
|
7698
|
-
import { existsSync as
|
|
6659
|
+
import { existsSync as existsSync11, realpathSync } from "fs";
|
|
7699
6660
|
function isLocalName(value) {
|
|
7700
6661
|
if (/^(https?:|data:|blob:|asset:|media:)/.test(value)) return false;
|
|
7701
6662
|
if (value.startsWith("$")) return false;
|
|
7702
6663
|
if (value.startsWith("<svg") || value.startsWith("<?xml")) return false;
|
|
7703
|
-
if (value.startsWith("/")) return
|
|
6664
|
+
if (value.startsWith("/")) return existsSync11(value);
|
|
7704
6665
|
return true;
|
|
7705
6666
|
}
|
|
7706
6667
|
var FILE_EXT = /\.(png|jpe?g|webp|gif|avif|svg|mp4|webm|mov|mp3|m4a|ogg|wav|flac|aac|glb|gltf|woff2?|ttf|otf|hdr|exr)$/i;
|
|
@@ -7875,8 +6836,8 @@ function preflightConfig(parsed) {
|
|
|
7875
6836
|
}
|
|
7876
6837
|
function resolveConfigPath(target) {
|
|
7877
6838
|
if (target.endsWith(".json")) return target;
|
|
7878
|
-
const inDir =
|
|
7879
|
-
if (
|
|
6839
|
+
const inDir = join13(target, "config.json");
|
|
6840
|
+
if (existsSync12(inDir)) return inDir;
|
|
7880
6841
|
throw new UsageError(
|
|
7881
6842
|
`${target} has no config.json (and is not a take \u2014 no doc.json)`
|
|
7882
6843
|
);
|
|
@@ -7902,9 +6863,9 @@ async function cmdFetch(argv) {
|
|
|
7902
6863
|
throw new Error(apiError(`fetch config for ${vosId}`, cfg));
|
|
7903
6864
|
const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
|
|
7904
6865
|
const out = strFlag(flags, "out") ?? slug;
|
|
7905
|
-
await
|
|
7906
|
-
await
|
|
7907
|
-
|
|
6866
|
+
await mkdir6(out, { recursive: true });
|
|
6867
|
+
await writeFile8(
|
|
6868
|
+
join13(out, "config.json"),
|
|
7908
6869
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7909
6870
|
);
|
|
7910
6871
|
const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
|
|
@@ -7926,32 +6887,33 @@ async function cmdFetch(argv) {
|
|
|
7926
6887
|
}
|
|
7927
6888
|
);
|
|
7928
6889
|
if (doc.status === 200 && !("source" in doc.body)) {
|
|
7929
|
-
const hosted =
|
|
6890
|
+
const hosted = migrateHostedDoc3(doc.body);
|
|
7930
6891
|
const { config: own } = await writeProgramDoc(out, hosted);
|
|
7931
6892
|
if (own) {
|
|
7932
|
-
await
|
|
6893
|
+
await writeFile8(join13(out, "config.json"), JSON.stringify(own, null, 2));
|
|
7933
6894
|
}
|
|
7934
6895
|
} else if (doc.status === 200) {
|
|
7935
6896
|
take = true;
|
|
7936
|
-
const hosted =
|
|
7937
|
-
await
|
|
6897
|
+
const hosted = migrateHostedDoc3(doc.body);
|
|
6898
|
+
await writeFile8(join13(out, "doc.json"), JSON.stringify(hosted, null, 2));
|
|
7938
6899
|
if (flags.media === true) {
|
|
7939
6900
|
const media = await pullMedia({ origin, key }, out, hosted, r.log);
|
|
6901
|
+
await recordHostedMedia(out, vosId, media);
|
|
7940
6902
|
mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
|
|
7941
6903
|
}
|
|
7942
6904
|
}
|
|
7943
6905
|
}
|
|
7944
6906
|
let assetsLine = "";
|
|
7945
6907
|
if (!take) {
|
|
7946
|
-
const configPath =
|
|
7947
|
-
const written = JSON.parse(await
|
|
6908
|
+
const configPath = join13(out, "config.json");
|
|
6909
|
+
const written = JSON.parse(await readFile10(configPath, "utf8"));
|
|
7948
6910
|
const hostedCount = manifestRefs(written).filter(
|
|
7949
6911
|
({ ref }) => hostedAssetId(ref, [origin, "https://vos.so", "https://www.vos.so"])
|
|
7950
6912
|
).length;
|
|
7951
6913
|
if (hostedCount && flags.media === true) {
|
|
7952
6914
|
const pulled = await pullManifest({ origin, key }, out, written, r.log);
|
|
7953
6915
|
if (pulled) {
|
|
7954
|
-
await
|
|
6916
|
+
await writeFile8(
|
|
7955
6917
|
configPath,
|
|
7956
6918
|
JSON.stringify({ ...written, assets: pulled.assets }, null, 2)
|
|
7957
6919
|
);
|
|
@@ -8070,8 +7032,8 @@ async function cmdPushProgram(argv) {
|
|
|
8070
7032
|
origin: strFlag(flags, "origin"),
|
|
8071
7033
|
api: strFlag(flags, "api")
|
|
8072
7034
|
});
|
|
8073
|
-
const dir =
|
|
8074
|
-
const parsed = JSON.parse(await
|
|
7035
|
+
const dir = dirname2(source);
|
|
7036
|
+
const parsed = JSON.parse(await readFile10(source, "utf8"));
|
|
8075
7037
|
const pre = preflightConfig(parsed);
|
|
8076
7038
|
if (!pre.ok || !pre.config) {
|
|
8077
7039
|
for (const issue of pre.issues) r.log(`error ${issue}`);
|
|
@@ -8142,6 +7104,9 @@ async function cmdPushProgram(argv) {
|
|
|
8142
7104
|
if (uploadToken) body.uploadToken = uploadToken;
|
|
8143
7105
|
if (prompt) body.prompt = prompt;
|
|
8144
7106
|
if (flags.share === true) body.share = true;
|
|
7107
|
+
const remixOf = strFlag(flags, "remix-of");
|
|
7108
|
+
const remixOfId2 = remixOf ? parseVosId(remixOf) : state?.vosId;
|
|
7109
|
+
if (remixOfId2) body.remixOfId = remixOfId2;
|
|
8145
7110
|
const slug = strFlag(flags, "slug");
|
|
8146
7111
|
if (slug) body.slug = slug;
|
|
8147
7112
|
const res = await apiJson(origin, "/api/claim", { method: "POST", body });
|
|
@@ -8351,7 +7316,7 @@ ${landedLines(landed2)}` : "")
|
|
|
8351
7316
|
async function cmdPullProgram(argv) {
|
|
8352
7317
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
|
|
8353
7318
|
const target = positionals[0] ?? ".";
|
|
8354
|
-
const dir = target.endsWith(".json") ?
|
|
7319
|
+
const dir = target.endsWith(".json") ? dirname2(target) : target;
|
|
8355
7320
|
const r = createReporter(flags.json === true);
|
|
8356
7321
|
const origin = platformOrigin({
|
|
8357
7322
|
origin: strFlag(flags, "origin"),
|
|
@@ -8416,10 +7381,10 @@ async function cmdPullProgram(argv) {
|
|
|
8416
7381
|
const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
|
|
8417
7382
|
if (cfg.status !== 200)
|
|
8418
7383
|
throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
|
|
8419
|
-
const configPath =
|
|
7384
|
+
const configPath = join13(dir, "config.json");
|
|
8420
7385
|
let backedUp = false;
|
|
8421
|
-
if (
|
|
8422
|
-
await
|
|
7386
|
+
if (existsSync12(configPath)) {
|
|
7387
|
+
await writeFile8(join13(dir, "config.backup.json"), await readFile10(configPath));
|
|
8423
7388
|
backedUp = true;
|
|
8424
7389
|
}
|
|
8425
7390
|
let headConfig = cfg.body.config;
|
|
@@ -8433,12 +7398,12 @@ async function cmdPullProgram(argv) {
|
|
|
8433
7398
|
if (docRes.status === 200 && !("source" in docRes.body)) {
|
|
8434
7399
|
const { config: own } = await writeProgramDoc(
|
|
8435
7400
|
dir,
|
|
8436
|
-
|
|
7401
|
+
migrateHostedDoc3(docRes.body)
|
|
8437
7402
|
);
|
|
8438
7403
|
if (own) headConfig = own;
|
|
8439
7404
|
}
|
|
8440
7405
|
}
|
|
8441
|
-
await
|
|
7406
|
+
await writeFile8(configPath, JSON.stringify(headConfig, null, 2));
|
|
8442
7407
|
writeSyncState(dir, {
|
|
8443
7408
|
vosId,
|
|
8444
7409
|
versionId: typeof head.id === "string" ? head.id : since
|
|
@@ -8452,7 +7417,7 @@ async function cmdPullProgram(argv) {
|
|
|
8452
7417
|
protected: protectedIds,
|
|
8453
7418
|
changes,
|
|
8454
7419
|
out: configPath,
|
|
8455
|
-
backup: backedUp ?
|
|
7420
|
+
backup: backedUp ? join13(dir, "config.backup.json") : null
|
|
8456
7421
|
},
|
|
8457
7422
|
`Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
|
|
8458
7423
|
Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
|
|
@@ -8560,9 +7525,9 @@ function storedProgramConfig(config, programDoc) {
|
|
|
8560
7525
|
return lowerProgramDoc(doc).config;
|
|
8561
7526
|
}
|
|
8562
7527
|
async function readProgramDoc(dir, config) {
|
|
8563
|
-
const docPath =
|
|
8564
|
-
if (!
|
|
8565
|
-
const parsed = JSON.parse(await
|
|
7528
|
+
const docPath = join13(dir, "doc.json");
|
|
7529
|
+
if (!existsSync12(docPath)) return null;
|
|
7530
|
+
const parsed = JSON.parse(await readFile10(docPath, "utf8"));
|
|
8566
7531
|
if (typeof parsed !== "object" || parsed === null || "source" in parsed)
|
|
8567
7532
|
return null;
|
|
8568
7533
|
const raw = parsed;
|
|
@@ -8570,23 +7535,23 @@ async function readProgramDoc(dir, config) {
|
|
|
8570
7535
|
return { ...raw, program: { ...program, config } };
|
|
8571
7536
|
}
|
|
8572
7537
|
async function setProgramStill(dir, t) {
|
|
8573
|
-
const docPath =
|
|
7538
|
+
const docPath = join13(dir, "doc.json");
|
|
8574
7539
|
let doc = { program: {}, audio: [] };
|
|
8575
|
-
if (
|
|
8576
|
-
const parsed = JSON.parse(await
|
|
7540
|
+
if (existsSync12(docPath)) {
|
|
7541
|
+
const parsed = JSON.parse(await readFile10(docPath, "utf8"));
|
|
8577
7542
|
if (!parsed || typeof parsed !== "object" || "source" in parsed)
|
|
8578
7543
|
throw new UsageError(
|
|
8579
7544
|
`${docPath} is a take's document; a take's cover is its own "still" field`
|
|
8580
7545
|
);
|
|
8581
7546
|
doc = parsed;
|
|
8582
7547
|
}
|
|
8583
|
-
await
|
|
7548
|
+
await writeFile8(docPath, JSON.stringify({ ...doc, still: t }, null, 2) + "\n");
|
|
8584
7549
|
}
|
|
8585
7550
|
async function writeProgramDoc(dir, hosted) {
|
|
8586
7551
|
const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
|
|
8587
7552
|
const { config, ...rest } = program;
|
|
8588
7553
|
const onDisk = { ...hosted, program: rest };
|
|
8589
|
-
await
|
|
7554
|
+
await writeFile8(join13(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
|
|
8590
7555
|
return {
|
|
8591
7556
|
config: config && typeof config === "object" ? config : null
|
|
8592
7557
|
};
|
|
@@ -8998,8 +7963,8 @@ async function cmdAsset(argv) {
|
|
|
8998
7963
|
}
|
|
8999
7964
|
|
|
9000
7965
|
// src/plugin/remove.ts
|
|
9001
|
-
import { existsSync as
|
|
9002
|
-
import { join as
|
|
7966
|
+
import { existsSync as existsSync13, rmSync as rmSync2, statSync as statSync3 } from "fs";
|
|
7967
|
+
import { join as join14 } from "path";
|
|
9003
7968
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
9004
7969
|
|
|
9005
7970
|
// src/plugin/trash.ts
|
|
@@ -9178,7 +8143,7 @@ async function cmdDelete(argv) {
|
|
|
9178
8143
|
api: strFlag(flags, "api")
|
|
9179
8144
|
}).replace(/\/+$/, "");
|
|
9180
8145
|
const key = requireCredential(strFlag(flags, "key"));
|
|
9181
|
-
const dir =
|
|
8146
|
+
const dir = existsSync13(target) && statSync3(target).isDirectory() ? target : null;
|
|
9182
8147
|
const vosId = dir ? readSyncState(dir)?.vosId : parseVosId(target);
|
|
9183
8148
|
if (!vosId)
|
|
9184
8149
|
throw new UsageError(
|
|
@@ -9221,7 +8186,7 @@ async function cmdDelete(argv) {
|
|
|
9221
8186
|
const tracking = dir ?? trackingCwd(vosId);
|
|
9222
8187
|
let unlinked = false;
|
|
9223
8188
|
if (tracking) {
|
|
9224
|
-
rmSync2(
|
|
8189
|
+
rmSync2(join14(tracking, SYNC_STATE_NAME), { force: true });
|
|
9225
8190
|
unlinked = true;
|
|
9226
8191
|
}
|
|
9227
8192
|
const trashed = res.body.trashed === true;
|
|
@@ -9246,13 +8211,13 @@ async function cmdDelete(argv) {
|
|
|
9246
8211
|
}
|
|
9247
8212
|
function trackingCwd(vosId) {
|
|
9248
8213
|
const cwd = process.cwd();
|
|
9249
|
-
if (!
|
|
8214
|
+
if (!existsSync13(join14(cwd, SYNC_STATE_NAME))) return null;
|
|
9250
8215
|
return readSyncState(cwd)?.vosId === vosId ? cwd : null;
|
|
9251
8216
|
}
|
|
9252
8217
|
|
|
9253
8218
|
// src/plugin/buildProgram.ts
|
|
9254
8219
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
9255
|
-
import { basename as basename5, dirname as
|
|
8220
|
+
import { basename as basename5, dirname as dirname3, join as join15, resolve as resolve5 } from "path";
|
|
9256
8221
|
import { pathToFileURL } from "url";
|
|
9257
8222
|
import { parse } from "acorn";
|
|
9258
8223
|
import { fullAncestor } from "acorn-walk";
|
|
@@ -9452,7 +8417,7 @@ async function cmdBuild(argv) {
|
|
|
9452
8417
|
if (!source)
|
|
9453
8418
|
throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
|
|
9454
8419
|
const r = createReporter(flags.json === true);
|
|
9455
|
-
const file =
|
|
8420
|
+
const file = resolve5(source);
|
|
9456
8421
|
const text = readFileSync4(file, "utf8");
|
|
9457
8422
|
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
9458
8423
|
const program = mod.default ?? mod.program;
|
|
@@ -9469,8 +8434,8 @@ async function cmdBuild(argv) {
|
|
|
9469
8434
|
);
|
|
9470
8435
|
return EXIT_ERROR;
|
|
9471
8436
|
}
|
|
9472
|
-
const out =
|
|
9473
|
-
strFlag(flags, "out") ??
|
|
8437
|
+
const out = resolve5(
|
|
8438
|
+
strFlag(flags, "out") ?? join15(dirname3(file), "config.json")
|
|
9474
8439
|
);
|
|
9475
8440
|
writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
|
|
9476
8441
|
const sidecar = out.replace(/\.json$/, "") + ".sources.json";
|
|
@@ -9487,8 +8452,8 @@ async function cmdBuild(argv) {
|
|
|
9487
8452
|
|
|
9488
8453
|
// src/plugin/compare.ts
|
|
9489
8454
|
import { spawnSync } from "child_process";
|
|
9490
|
-
import { existsSync as
|
|
9491
|
-
import { dirname as
|
|
8455
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
8456
|
+
import { dirname as dirname4, join as join16, resolve as resolve6 } from "path";
|
|
9492
8457
|
var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9493
8458
|
function sampleTimes(duration, every) {
|
|
9494
8459
|
const step = every > 0 ? every : 1;
|
|
@@ -9549,13 +8514,13 @@ async function cmdCompare(argv) {
|
|
|
9549
8514
|
);
|
|
9550
8515
|
requireFfmpeg();
|
|
9551
8516
|
const r = createReporter(flags.json === true);
|
|
9552
|
-
const ref =
|
|
8517
|
+
const ref = resolve6(against);
|
|
9553
8518
|
const loaded = await loadVosConfig(source);
|
|
9554
8519
|
for (const w of loaded.warnings) r.log(`note: ${w}`);
|
|
9555
8520
|
const config = loaded.config;
|
|
9556
8521
|
const duration = configDuration(config) ?? 5;
|
|
9557
8522
|
const assets = await programAssets(config, {
|
|
9558
|
-
baseDir: /^https?:\/\//.test(source) ? null :
|
|
8523
|
+
baseDir: /^https?:\/\//.test(source) ? null : existsSync14(source) && statSync4(source).isDirectory() ? source : dirname4(source),
|
|
9559
8524
|
origin: platformOrigin({}),
|
|
9560
8525
|
key: resolveCredential(),
|
|
9561
8526
|
log: (line) => r.log(line)
|
|
@@ -9571,16 +8536,16 @@ async function cmdCompare(argv) {
|
|
|
9571
8536
|
const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
|
|
9572
8537
|
const timesRaw = strFlag(flags, "times");
|
|
9573
8538
|
const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
|
|
9574
|
-
const outDir =
|
|
8539
|
+
const outDir = resolve6(strFlag(flags, "out") ?? "compare");
|
|
9575
8540
|
mkdirSync2(outDir, { recursive: true });
|
|
9576
8541
|
const browser = await launchBrowser();
|
|
9577
8542
|
const scores = [];
|
|
9578
8543
|
try {
|
|
9579
8544
|
for (const t of times) {
|
|
9580
8545
|
const tag = t.toFixed(2);
|
|
9581
|
-
const refPng =
|
|
9582
|
-
const vosPng =
|
|
9583
|
-
const sheet =
|
|
8546
|
+
const refPng = join16(outDir, `src-${tag}.png`);
|
|
8547
|
+
const vosPng = join16(outDir, `vos-${tag}.png`);
|
|
8548
|
+
const sheet = join16(outDir, `sheet-${tag}.png`);
|
|
9584
8549
|
const grab = ffmpeg([
|
|
9585
8550
|
"-v",
|
|
9586
8551
|
"error",
|
|
@@ -9637,7 +8602,7 @@ async function cmdCompare(argv) {
|
|
|
9637
8602
|
} finally {
|
|
9638
8603
|
await browser.close();
|
|
9639
8604
|
}
|
|
9640
|
-
const overview =
|
|
8605
|
+
const overview = join16(outDir, "overview.png");
|
|
9641
8606
|
if (scores.length) {
|
|
9642
8607
|
const inputs = scores.flatMap((s) => ["-i", s.sheet]);
|
|
9643
8608
|
const scaled = scores.map((_, i) => `[${i}:v]scale=iw/3:-2[s${i}]`).join(";");
|
|
@@ -9663,7 +8628,7 @@ async function cmdCompare(argv) {
|
|
|
9663
8628
|
overview
|
|
9664
8629
|
};
|
|
9665
8630
|
writeFileSync3(
|
|
9666
|
-
|
|
8631
|
+
join16(outDir, "report.json"),
|
|
9667
8632
|
JSON.stringify(report, null, 2) + "\n"
|
|
9668
8633
|
);
|
|
9669
8634
|
r.done(
|
|
@@ -9674,28 +8639,28 @@ async function cmdCompare(argv) {
|
|
|
9674
8639
|
}
|
|
9675
8640
|
|
|
9676
8641
|
// src/plugin/port.ts
|
|
9677
|
-
import { existsSync as
|
|
9678
|
-
import { dirname as
|
|
8642
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
8643
|
+
import { dirname as dirname7, join as join20, resolve as resolve9 } from "path";
|
|
9679
8644
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
9680
8645
|
|
|
9681
8646
|
// src/plugin/portInventory.ts
|
|
9682
8647
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
9683
8648
|
import {
|
|
9684
|
-
existsSync as
|
|
8649
|
+
existsSync as existsSync16,
|
|
9685
8650
|
mkdirSync as mkdirSync3,
|
|
9686
8651
|
readFileSync as readFileSync6,
|
|
9687
8652
|
readdirSync as readdirSync4,
|
|
9688
8653
|
statSync as statSync6
|
|
9689
8654
|
} from "fs";
|
|
9690
|
-
import { dirname as
|
|
8655
|
+
import { dirname as dirname5, join as join18, relative as relative3, resolve as resolve7 } from "path";
|
|
9691
8656
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
9692
8657
|
import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
|
|
9693
8658
|
|
|
9694
8659
|
// src/plugin/portRemotion.ts
|
|
9695
|
-
import { existsSync as
|
|
8660
|
+
import { existsSync as existsSync15, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
|
|
9696
8661
|
import { createServer as createServer2 } from "http";
|
|
9697
8662
|
import { createRequire } from "module";
|
|
9698
|
-
import { basename as basename6, extname as extname3, join as
|
|
8663
|
+
import { basename as basename6, extname as extname3, join as join17 } from "path";
|
|
9699
8664
|
function themeColors(sources) {
|
|
9700
8665
|
const out = {};
|
|
9701
8666
|
const re = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*['"](#[0-9a-fA-F]{3,8})['"]/g;
|
|
@@ -9761,14 +8726,14 @@ function sourceTexts(dir) {
|
|
|
9761
8726
|
if (depth > 4 || out.length > 200) return;
|
|
9762
8727
|
for (const name of readdirSync3(d)) {
|
|
9763
8728
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
9764
|
-
const p =
|
|
8729
|
+
const p = join17(d, name);
|
|
9765
8730
|
if (statSync5(p).isDirectory()) walk(p, depth + 1);
|
|
9766
8731
|
else if (/\.(tsx?|jsx?|mjs)$/.test(name))
|
|
9767
8732
|
out.push(readFileSync5(p, "utf8"));
|
|
9768
8733
|
}
|
|
9769
8734
|
};
|
|
9770
|
-
const src =
|
|
9771
|
-
walk(
|
|
8735
|
+
const src = join17(dir, "src");
|
|
8736
|
+
walk(existsSync15(src) ? src : dir, 0);
|
|
9772
8737
|
return out;
|
|
9773
8738
|
}
|
|
9774
8739
|
function entryPoint(dir) {
|
|
@@ -9780,7 +8745,7 @@ function entryPoint(dir) {
|
|
|
9780
8745
|
"remotion/index.ts",
|
|
9781
8746
|
"remotion/index.tsx"
|
|
9782
8747
|
])
|
|
9783
|
-
if (
|
|
8748
|
+
if (existsSync15(join17(dir, p))) return join17(dir, p);
|
|
9784
8749
|
return null;
|
|
9785
8750
|
}
|
|
9786
8751
|
var TYPES = {
|
|
@@ -9855,7 +8820,7 @@ async function readRemotion(dir, log) {
|
|
|
9855
8820
|
const entry = entryPoint(dir);
|
|
9856
8821
|
let bundler;
|
|
9857
8822
|
try {
|
|
9858
|
-
bundler = createRequire(
|
|
8823
|
+
bundler = createRequire(join17(dir, "package.json"))("@remotion/bundler");
|
|
9859
8824
|
} catch {
|
|
9860
8825
|
log?.(
|
|
9861
8826
|
"a Remotion project whose dependencies are not installed: run `npm install` in it so vos can read its compositions (words, scenes, faces); reading only the render"
|
|
@@ -9872,9 +8837,9 @@ async function readRemotion(dir, log) {
|
|
|
9872
8837
|
} });
|
|
9873
8838
|
const server = createServer2((rq, rs) => {
|
|
9874
8839
|
const path = decodeURIComponent(new URL(rq.url ?? "/", "http://x").pathname);
|
|
9875
|
-
let f =
|
|
9876
|
-
if (!f.startsWith(out) || !
|
|
9877
|
-
f =
|
|
8840
|
+
let f = join17(out, path);
|
|
8841
|
+
if (!f.startsWith(out) || !existsSync15(f) || statSync5(f).isDirectory())
|
|
8842
|
+
f = join17(out, "index.html");
|
|
9878
8843
|
rs.writeHead(200, {
|
|
9879
8844
|
"content-type": TYPES[extname3(f)] ?? "application/octet-stream"
|
|
9880
8845
|
});
|
|
@@ -9973,10 +8938,10 @@ async function readRemotion(dir, log) {
|
|
|
9973
8938
|
perScene.push({ scene: s.name, texts: read.texts ?? [] });
|
|
9974
8939
|
}
|
|
9975
8940
|
const sources = sourceTexts(dir);
|
|
9976
|
-
const publicDir =
|
|
8941
|
+
const publicDir = join17(dir, "public");
|
|
9977
8942
|
const media = tree.audio.map((a) => {
|
|
9978
8943
|
const name = basename6(a.src.split("?")[0]);
|
|
9979
|
-
const local =
|
|
8944
|
+
const local = existsSync15(join17(publicDir, name)) ? `public/${name}` : a.src;
|
|
9980
8945
|
return {
|
|
9981
8946
|
kind: "audio",
|
|
9982
8947
|
src: local,
|
|
@@ -10017,9 +8982,9 @@ async function readRemotion(dir, log) {
|
|
|
10017
8982
|
|
|
10018
8983
|
// src/plugin/portInventory.ts
|
|
10019
8984
|
function detectEngine(dir) {
|
|
10020
|
-
if (
|
|
10021
|
-
const pkg =
|
|
10022
|
-
if (
|
|
8985
|
+
if (existsSync16(join18(dir, "hyperframes.json"))) return "hyperframes";
|
|
8986
|
+
const pkg = join18(dir, "package.json");
|
|
8987
|
+
if (existsSync16(pkg)) {
|
|
10023
8988
|
try {
|
|
10024
8989
|
const p = JSON.parse(readFileSync6(pkg, "utf8"));
|
|
10025
8990
|
const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
|
|
@@ -10033,14 +8998,14 @@ function detectEngine(dir) {
|
|
|
10033
8998
|
return "page";
|
|
10034
8999
|
}
|
|
10035
9000
|
function sourcePage(source) {
|
|
10036
|
-
if (!
|
|
9001
|
+
if (!existsSync16(source)) return null;
|
|
10037
9002
|
if (statSync6(source).isFile()) return /\.html?$/i.test(source) ? source : null;
|
|
10038
|
-
const index =
|
|
10039
|
-
return
|
|
9003
|
+
const index = join18(source, "index.html");
|
|
9004
|
+
return existsSync16(index) ? index : null;
|
|
10040
9005
|
}
|
|
10041
9006
|
function findRender(dir) {
|
|
10042
|
-
const videos = (d) =>
|
|
10043
|
-
for (const d of [
|
|
9007
|
+
const videos = (d) => existsSync16(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join18(d, f)) : [];
|
|
9008
|
+
for (const d of [join18(dir, "renders"), join18(dir, "out"), dir]) {
|
|
10044
9009
|
const found = videos(d);
|
|
10045
9010
|
if (found.length) return found[0];
|
|
10046
9011
|
}
|
|
@@ -10310,8 +9275,8 @@ var SEEK_AND_READ = `((args) => {
|
|
|
10310
9275
|
return out
|
|
10311
9276
|
})`;
|
|
10312
9277
|
async function readInventory(source, opts) {
|
|
10313
|
-
const abs =
|
|
10314
|
-
const dir =
|
|
9278
|
+
const abs = resolve7(source);
|
|
9279
|
+
const dir = existsSync16(abs) && statSync6(abs).isDirectory() ? abs : dirname5(abs);
|
|
10315
9280
|
const engine = detectEngine(dir);
|
|
10316
9281
|
const page = sourcePage(abs);
|
|
10317
9282
|
const render = opts.render === void 0 ? findRender(dir) : opts.render;
|
|
@@ -10414,7 +9379,7 @@ async function readInventory(source, opts) {
|
|
|
10414
9379
|
if (render && duration) {
|
|
10415
9380
|
const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
|
|
10416
9381
|
if (has) {
|
|
10417
|
-
const refDir =
|
|
9382
|
+
const refDir = join18(opts.outDir, "ref");
|
|
10418
9383
|
mkdirSync3(refDir, { recursive: true });
|
|
10419
9384
|
const times = read.scenes.length >= 3 ? read.scenes.map(
|
|
10420
9385
|
(s) => s.start + (s.duration ?? 1) / 2
|
|
@@ -10423,7 +9388,7 @@ async function readInventory(source, opts) {
|
|
|
10423
9388
|
(_, i) => i + 0.5
|
|
10424
9389
|
);
|
|
10425
9390
|
for (const t of times.filter((t2) => t2 < duration)) {
|
|
10426
|
-
const file =
|
|
9391
|
+
const file = join18(refDir, `src-${t.toFixed(2)}.png`);
|
|
10427
9392
|
const ok = spawnSync2("ffmpeg", [
|
|
10428
9393
|
"-v",
|
|
10429
9394
|
"error",
|
|
@@ -10453,7 +9418,7 @@ async function readInventory(source, opts) {
|
|
|
10453
9418
|
const scriptStrings = scriptStringsOf(read.scriptText);
|
|
10454
9419
|
return {
|
|
10455
9420
|
source: relative3(process.cwd(), abs) || ".",
|
|
10456
|
-
root: abs.endsWith(".html") ?
|
|
9421
|
+
root: abs.endsWith(".html") ? dirname5(abs) : abs,
|
|
10457
9422
|
engine,
|
|
10458
9423
|
width: read.width,
|
|
10459
9424
|
height: read.height,
|
|
@@ -10477,8 +9442,8 @@ async function readInventory(source, opts) {
|
|
|
10477
9442
|
|
|
10478
9443
|
// src/plugin/portScaffold.ts
|
|
10479
9444
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
10480
|
-
import { copyFileSync, existsSync as
|
|
10481
|
-
import { basename as basename7, dirname as
|
|
9445
|
+
import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
9446
|
+
import { basename as basename7, dirname as dirname6, join as join19, resolve as resolve8 } from "path";
|
|
10482
9447
|
import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
10483
9448
|
|
|
10484
9449
|
// src/plugin/painterLib.ts
|
|
@@ -10887,9 +9852,9 @@ function planScaffold(inv, opts) {
|
|
|
10887
9852
|
const audio = [];
|
|
10888
9853
|
const copies = [];
|
|
10889
9854
|
inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
|
|
10890
|
-
const from =
|
|
9855
|
+
const from = resolve8(opts.sourceDir, m.src);
|
|
10891
9856
|
const name = basename7(from);
|
|
10892
|
-
if (!
|
|
9857
|
+
if (!existsSync17(from)) {
|
|
10893
9858
|
report.push(
|
|
10894
9859
|
`- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
|
|
10895
9860
|
);
|
|
@@ -11087,13 +10052,13 @@ function reportText(inv, r) {
|
|
|
11087
10052
|
}
|
|
11088
10053
|
function writeScaffold(plan, outDir) {
|
|
11089
10054
|
mkdirSync4(outDir, { recursive: true });
|
|
11090
|
-
writeFileSync4(
|
|
10055
|
+
writeFileSync4(join19(outDir, "program.mjs"), plan.program);
|
|
11091
10056
|
writeFileSync4(
|
|
11092
|
-
|
|
10057
|
+
join19(outDir, "doc.json"),
|
|
11093
10058
|
JSON.stringify(plan.doc, null, 2) + "\n"
|
|
11094
10059
|
);
|
|
11095
|
-
writeFileSync4(
|
|
11096
|
-
for (const [from, name] of plan.copies) copyFileSync(from,
|
|
10060
|
+
writeFileSync4(join19(dirname6(outDir), "REPORT.md"), plan.report);
|
|
10061
|
+
for (const [from, name] of plan.copies) copyFileSync(from, join19(outDir, name));
|
|
11097
10062
|
}
|
|
11098
10063
|
function probeAudioDuration(file) {
|
|
11099
10064
|
const res = spawnSync3(
|
|
@@ -11119,15 +10084,15 @@ async function inventory(argv) {
|
|
|
11119
10084
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
|
|
11120
10085
|
const source = positionals[0] ?? ".";
|
|
11121
10086
|
const r = createReporter(flags.json === true);
|
|
11122
|
-
const outDir =
|
|
10087
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "port");
|
|
11123
10088
|
mkdirSync5(outDir, { recursive: true });
|
|
11124
10089
|
const renderFlag = strFlag(flags, "render");
|
|
11125
10090
|
const inv = await readInventory(source, {
|
|
11126
|
-
render: renderFlag === void 0 ? void 0 :
|
|
10091
|
+
render: renderFlag === void 0 ? void 0 : resolve9(renderFlag),
|
|
11127
10092
|
outDir,
|
|
11128
10093
|
log: (l) => r.log(`note: ${l}`)
|
|
11129
10094
|
});
|
|
11130
|
-
const file =
|
|
10095
|
+
const file = join20(outDir, "inventory.json");
|
|
11131
10096
|
writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
|
|
11132
10097
|
const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
|
|
11133
10098
|
r.log(
|
|
@@ -11159,38 +10124,38 @@ function summary(inv) {
|
|
|
11159
10124
|
async function scaffold(argv) {
|
|
11160
10125
|
const { flags } = parseArgs(argv, BOOLEAN_FLAGS8);
|
|
11161
10126
|
const r = createReporter(flags.json === true);
|
|
11162
|
-
const from =
|
|
11163
|
-
if (!
|
|
10127
|
+
const from = resolve9(strFlag(flags, "from") ?? join20("port", "inventory.json"));
|
|
10128
|
+
if (!existsSync18(from))
|
|
11164
10129
|
throw new UsageError(
|
|
11165
10130
|
`no inventory at ${from}: run npx vos port inventory <source> first`
|
|
11166
10131
|
);
|
|
11167
10132
|
const inv = JSON.parse(readFileSync7(from, "utf8"));
|
|
11168
|
-
const outDir =
|
|
11169
|
-
strFlag(flags, "out") ??
|
|
10133
|
+
const outDir = resolve9(
|
|
10134
|
+
strFlag(flags, "out") ?? join20(dirname7(from), "program")
|
|
11170
10135
|
);
|
|
11171
10136
|
const plan = planScaffold(inv, {
|
|
11172
10137
|
sourceDir: scaffoldSourceDir(inv),
|
|
11173
10138
|
probeDuration: probeAudioDuration
|
|
11174
10139
|
});
|
|
11175
10140
|
writeScaffold(plan, outDir);
|
|
11176
|
-
const programFile =
|
|
10141
|
+
const programFile = join20(outDir, "program.mjs");
|
|
11177
10142
|
const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
|
|
11178
10143
|
const built = buildProgram(mod.default, "program.mjs", plan.program);
|
|
11179
10144
|
for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
|
|
11180
10145
|
if (built.problems.length) return EXIT_ERROR;
|
|
11181
10146
|
writeFileSync5(
|
|
11182
|
-
|
|
10147
|
+
join20(outDir, "config.json"),
|
|
11183
10148
|
JSON.stringify(built.config, null, 2) + "\n"
|
|
11184
10149
|
);
|
|
11185
10150
|
r.done(
|
|
11186
|
-
{ program: outDir, report:
|
|
11187
|
-
`Wrote ${outDir}/program.mjs, config.json and doc.json, and ${
|
|
10151
|
+
{ program: outDir, report: join20(dirname7(outDir), "REPORT.md") },
|
|
10152
|
+
`Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join20(dirname7(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
|
|
11188
10153
|
);
|
|
11189
10154
|
return EXIT_OK;
|
|
11190
10155
|
}
|
|
11191
10156
|
function scaffoldSourceDir(inv) {
|
|
11192
10157
|
if (inv.root) return inv.root;
|
|
11193
|
-
return inv.source.endsWith(".html") ?
|
|
10158
|
+
return inv.source.endsWith(".html") ? dirname7(resolve9(inv.source)) : resolve9(inv.source);
|
|
11194
10159
|
}
|
|
11195
10160
|
|
|
11196
10161
|
// src/plugin/recipe.ts
|
|
@@ -11281,8 +10246,8 @@ async function cmdRecipe(argv) {
|
|
|
11281
10246
|
}
|
|
11282
10247
|
|
|
11283
10248
|
// src/plugin/brand.ts
|
|
11284
|
-
import { writeFile as
|
|
11285
|
-
import { resolve as
|
|
10249
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
10250
|
+
import { resolve as resolve10 } from "path";
|
|
11286
10251
|
import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
|
|
11287
10252
|
import { lookKindForGround } from "@vosjs/studio-core";
|
|
11288
10253
|
var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
|
|
@@ -11570,7 +10535,7 @@ function renderBrandMd(c, claim) {
|
|
|
11570
10535
|
"",
|
|
11571
10536
|
"## Use",
|
|
11572
10537
|
"",
|
|
11573
|
-
`The
|
|
10538
|
+
`The look, the titles and the end card bind these roles: \`bgA/bgB/bgC\` for the ground, \`ink\` for type, \`fontDisplay\` for the headline (if it is not in the hosted catalog, \`vos check\` warns and the nearest hosted serif or sans stands in), \`logoUrl\` for the mark (placed beside the wordmark on the end card; \`logoOnDarkUrl\` stands in on a dark ground). A site that publishes only a favicon or an app icon gets the icon here, tile and all: publish a bare mark and name it in design.md. A take's frame ground is a gradient of \`accent\` toward \`bgC\` unless the brand is paper, in which case \`bgB\` toward \`bgC\`.`
|
|
11574
10539
|
];
|
|
11575
10540
|
if (c.avoid.length) {
|
|
11576
10541
|
lines.push("", "## Avoid (the site says)", "");
|
|
@@ -11601,7 +10566,7 @@ async function cmdBrand(argv) {
|
|
|
11601
10566
|
throw new UsageError(`not a URL: ${raw}`);
|
|
11602
10567
|
}
|
|
11603
10568
|
const r = createReporter(flags.json === true);
|
|
11604
|
-
const out =
|
|
10569
|
+
const out = resolve10(strFlag(flags, "out") ?? "BRAND.md");
|
|
11605
10570
|
r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
|
|
11606
10571
|
const [designText, llmsText] = await Promise.all([
|
|
11607
10572
|
fetchText(`${origin}/design.md`),
|
|
@@ -11635,7 +10600,7 @@ async function cmdBrand(argv) {
|
|
|
11635
10600
|
composition,
|
|
11636
10601
|
llms?.claim ?? design?.description ?? null
|
|
11637
10602
|
);
|
|
11638
|
-
await
|
|
10603
|
+
await writeFile9(out, md);
|
|
11639
10604
|
const k = composition.kit;
|
|
11640
10605
|
r.done(
|
|
11641
10606
|
{
|
|
@@ -11750,8 +10715,8 @@ async function recordHosted(input) {
|
|
|
11750
10715
|
}
|
|
11751
10716
|
|
|
11752
10717
|
// src/plugin/agentBrowser.ts
|
|
11753
|
-
import { writeFile as
|
|
11754
|
-
import { resolve as
|
|
10718
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
10719
|
+
import { resolve as resolve11 } from "path";
|
|
11755
10720
|
function parseAgentBrowserLog(text) {
|
|
11756
10721
|
const records = [];
|
|
11757
10722
|
const problems = [];
|
|
@@ -12222,7 +11187,7 @@ async function cmdActions(argv) {
|
|
|
12222
11187
|
);
|
|
12223
11188
|
}
|
|
12224
11189
|
const r = createReporter(flags.json === true);
|
|
12225
|
-
const out =
|
|
11190
|
+
const out = resolve11(strFlag(flags, "out") ?? "actions.json");
|
|
12226
11191
|
const viewportFlag = strFlag(flags, "viewport");
|
|
12227
11192
|
let viewport;
|
|
12228
11193
|
if (viewportFlag) {
|
|
@@ -12230,8 +11195,8 @@ async function cmdActions(argv) {
|
|
|
12230
11195
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
12231
11196
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
12232
11197
|
}
|
|
12233
|
-
const { readFile:
|
|
12234
|
-
const text = await
|
|
11198
|
+
const { readFile: readFile13 } = await import("fs/promises");
|
|
11199
|
+
const text = await readFile13(resolve11(input), "utf8");
|
|
12235
11200
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
12236
11201
|
for (const p of problems) r.log(` ${p}`);
|
|
12237
11202
|
const result = convertAgentBrowser(records, {
|
|
@@ -12248,7 +11213,7 @@ async function cmdActions(argv) {
|
|
|
12248
11213
|
);
|
|
12249
11214
|
return EXIT_ERROR;
|
|
12250
11215
|
}
|
|
12251
|
-
await
|
|
11216
|
+
await writeFile10(out, `${JSON.stringify(result.actions, null, 2)}
|
|
12252
11217
|
`);
|
|
12253
11218
|
r.done(
|
|
12254
11219
|
{ ok: true, out, url: result.actions.url ?? null, ...summary2(result) },
|
|
@@ -12268,9 +11233,9 @@ function summary2(result) {
|
|
|
12268
11233
|
}
|
|
12269
11234
|
async function cmdActionsScript(input, flags) {
|
|
12270
11235
|
const r = createReporter(flags.json === true);
|
|
12271
|
-
const { readFile:
|
|
11236
|
+
const { readFile: readFile13 } = await import("fs/promises");
|
|
12272
11237
|
const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
|
|
12273
|
-
const raw = JSON.parse(await
|
|
11238
|
+
const raw = JSON.parse(await readFile13(resolve11(input), "utf8"));
|
|
12274
11239
|
const errors = validateActions(raw);
|
|
12275
11240
|
if (errors.length) throw new UsageError(`${input}:
|
|
12276
11241
|
${errors.join("\n ")}`);
|
|
@@ -12280,9 +11245,9 @@ async function cmdActionsScript(input, flags) {
|
|
|
12280
11245
|
}
|
|
12281
11246
|
|
|
12282
11247
|
// src/plugin/ingest.ts
|
|
12283
|
-
import { existsSync as
|
|
12284
|
-
import { copyFile, mkdir as
|
|
12285
|
-
import { basename as basename9, join as
|
|
11248
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11249
|
+
import { copyFile, mkdir as mkdir7, readFile as readFile11, stat as stat2 } from "fs/promises";
|
|
11250
|
+
import { basename as basename9, join as join21, resolve as resolve12 } from "path";
|
|
12286
11251
|
import { inflateRawSync } from "zlib";
|
|
12287
11252
|
import {
|
|
12288
11253
|
cursorFromPlaywrightTrace,
|
|
@@ -12370,7 +11335,7 @@ async function readTraceFile(path, offsetMs) {
|
|
|
12370
11335
|
);
|
|
12371
11336
|
}
|
|
12372
11337
|
if (kind === "zip") {
|
|
12373
|
-
const bytes = new Uint8Array(await
|
|
11338
|
+
const bytes = new Uint8Array(await readFile11(path));
|
|
12374
11339
|
const entry = playwrightTraceEntry(zipEntries(bytes));
|
|
12375
11340
|
if (!entry) {
|
|
12376
11341
|
throw new UsageError(
|
|
@@ -12383,7 +11348,7 @@ async function readTraceFile(path, offsetMs) {
|
|
|
12383
11348
|
);
|
|
12384
11349
|
return cursorFromPlaywrightTrace(text2, { offsetMs });
|
|
12385
11350
|
}
|
|
12386
|
-
const text = await
|
|
11351
|
+
const text = await readFile11(path, "utf8");
|
|
12387
11352
|
return cursorFromTrace(basename9(path), text, { offsetMs });
|
|
12388
11353
|
}
|
|
12389
11354
|
function parseViewport(v) {
|
|
@@ -12399,10 +11364,10 @@ async function cmdIngest(argv) {
|
|
|
12399
11364
|
throw new UsageError(
|
|
12400
11365
|
"vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]"
|
|
12401
11366
|
);
|
|
12402
|
-
const videoPath =
|
|
12403
|
-
if (!
|
|
11367
|
+
const videoPath = resolve12(source);
|
|
11368
|
+
if (!existsSync19(videoPath)) throw new UsageError(`${source}: no such file`);
|
|
12404
11369
|
const r = createReporter(flags.json === true);
|
|
12405
|
-
const outDir =
|
|
11370
|
+
const outDir = resolve12(strFlag(flags, "out") ?? "take");
|
|
12406
11371
|
const offsetMs = numFlag(flags, "offset", 0);
|
|
12407
11372
|
const viewportFlag = parseViewport(strFlag(flags, "viewport"));
|
|
12408
11373
|
const cursorPath = strFlag(flags, "cursor");
|
|
@@ -12424,18 +11389,18 @@ async function cmdIngest(argv) {
|
|
|
12424
11389
|
}
|
|
12425
11390
|
let trace = null;
|
|
12426
11391
|
if (cursorPath) {
|
|
12427
|
-
trace = await readTraceFile(
|
|
11392
|
+
trace = await readTraceFile(resolve12(cursorPath), offsetMs);
|
|
12428
11393
|
for (const n of trace.notes) r.log(`note: ${n}`);
|
|
12429
11394
|
}
|
|
12430
|
-
if (
|
|
11395
|
+
if (existsSync19(join21(outDir, "meta.json"))) {
|
|
12431
11396
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
12432
11397
|
r.log(
|
|
12433
11398
|
`note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
|
|
12434
11399
|
);
|
|
12435
11400
|
}
|
|
12436
|
-
await
|
|
11401
|
+
await mkdir7(outDir, { recursive: true });
|
|
12437
11402
|
const paths = await ensureTakeDir(outDir);
|
|
12438
|
-
const recording =
|
|
11403
|
+
const recording = join21(outDir, `recording.${probed.ext}`);
|
|
12439
11404
|
r.log("copying the recording\u2026");
|
|
12440
11405
|
const remuxed = await copyRecording(videoPath, recording, probed.ext);
|
|
12441
11406
|
const viewport = viewportFlag ?? trace?.viewport ?? {
|
|
@@ -12443,7 +11408,7 @@ async function cmdIngest(argv) {
|
|
|
12443
11408
|
height: probed.height
|
|
12444
11409
|
};
|
|
12445
11410
|
const events = trace?.events ?? [];
|
|
12446
|
-
const fileStat = await
|
|
11411
|
+
const fileStat = await stat2(videoPath);
|
|
12447
11412
|
const meta = {
|
|
12448
11413
|
dpr: 1,
|
|
12449
11414
|
zoom: 1,
|
|
@@ -12503,19 +11468,19 @@ async function cmdIngest(argv) {
|
|
|
12503
11468
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
12504
11469
|
import {
|
|
12505
11470
|
cpSync,
|
|
12506
|
-
existsSync as
|
|
11471
|
+
existsSync as existsSync21,
|
|
12507
11472
|
mkdirSync as mkdirSync6,
|
|
12508
11473
|
readFileSync as readFileSync10,
|
|
12509
11474
|
rmSync as rmSync3,
|
|
12510
11475
|
writeFileSync as writeFileSync6
|
|
12511
11476
|
} from "fs";
|
|
12512
11477
|
import { homedir as homedir2 } from "os";
|
|
12513
|
-
import { join as
|
|
11478
|
+
import { join as join23 } from "path";
|
|
12514
11479
|
import { chromium as chromium2 } from "playwright";
|
|
12515
11480
|
|
|
12516
11481
|
// src/plugin/machine.ts
|
|
12517
|
-
import { existsSync as
|
|
12518
|
-
import { dirname as
|
|
11482
|
+
import { existsSync as existsSync20, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
11483
|
+
import { dirname as dirname8, join as join22 } from "path";
|
|
12519
11484
|
var AGENT_NAMES = [
|
|
12520
11485
|
"claude",
|
|
12521
11486
|
"cursor",
|
|
@@ -12535,17 +11500,17 @@ var HOME_DIRS = {
|
|
|
12535
11500
|
copilot: [".copilot"]
|
|
12536
11501
|
};
|
|
12537
11502
|
function detectAgents(root, opts = {}) {
|
|
12538
|
-
const exists = opts.exists ??
|
|
11503
|
+
const exists = opts.exists ?? existsSync20;
|
|
12539
11504
|
const out = [];
|
|
12540
11505
|
for (const agent of AGENT_NAMES) {
|
|
12541
11506
|
for (const rel of PROJECT_DIRS[agent]) {
|
|
12542
|
-
const dir =
|
|
11507
|
+
const dir = join22(root, rel);
|
|
12543
11508
|
if (exists(dir)) out.push({ agent, dir, scope: "project" });
|
|
12544
11509
|
}
|
|
12545
|
-
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(
|
|
11510
|
+
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join22(root, ".github", "copilot-instructions.md"))) {
|
|
12546
11511
|
out.push({
|
|
12547
11512
|
agent,
|
|
12548
|
-
dir:
|
|
11513
|
+
dir: join22(root, ".github", "copilot"),
|
|
12549
11514
|
scope: "project"
|
|
12550
11515
|
});
|
|
12551
11516
|
}
|
|
@@ -12553,7 +11518,7 @@ function detectAgents(root, opts = {}) {
|
|
|
12553
11518
|
if (opts.global && opts.home) {
|
|
12554
11519
|
for (const agent of AGENT_NAMES) {
|
|
12555
11520
|
for (const rel of HOME_DIRS[agent]) {
|
|
12556
|
-
const dir =
|
|
11521
|
+
const dir = join22(opts.home, rel);
|
|
12557
11522
|
if (exists(dir)) out.push({ agent, dir, scope: "global" });
|
|
12558
11523
|
}
|
|
12559
11524
|
}
|
|
@@ -12571,18 +11536,18 @@ function agentsFromFlag(value, root) {
|
|
|
12571
11536
|
return {
|
|
12572
11537
|
homes: wanted.map((agent) => ({
|
|
12573
11538
|
agent,
|
|
12574
|
-
dir:
|
|
11539
|
+
dir: join22(root, PROJECT_DIRS[agent][0]),
|
|
12575
11540
|
scope: "project"
|
|
12576
11541
|
}))
|
|
12577
11542
|
};
|
|
12578
11543
|
}
|
|
12579
11544
|
function skillsDirFor(home) {
|
|
12580
|
-
return
|
|
11545
|
+
return join22(home.dir, "skills");
|
|
12581
11546
|
}
|
|
12582
11547
|
function fallbackSkillsHome(root) {
|
|
12583
|
-
return { agent: "codex", dir:
|
|
11548
|
+
return { agent: "codex", dir: join22(root, ".agents"), scope: "project" };
|
|
12584
11549
|
}
|
|
12585
|
-
function installedSkills(dir, exists =
|
|
11550
|
+
function installedSkills(dir, exists = existsSync20, list = (p) => {
|
|
12586
11551
|
try {
|
|
12587
11552
|
return readdirSync5(p);
|
|
12588
11553
|
} catch {
|
|
@@ -12590,7 +11555,7 @@ function installedSkills(dir, exists = existsSync22, list = (p) => {
|
|
|
12590
11555
|
}
|
|
12591
11556
|
}) {
|
|
12592
11557
|
if (!exists(dir)) return [];
|
|
12593
|
-
return list(dir).filter((name) => exists(
|
|
11558
|
+
return list(dir).filter((name) => exists(join22(dir, name, "SKILL.md"))).sort();
|
|
12594
11559
|
}
|
|
12595
11560
|
var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
|
|
12596
11561
|
var RULES_END = "<!-- vos:end -->";
|
|
@@ -12623,10 +11588,10 @@ ${RULES_BLOCK}
|
|
|
12623
11588
|
}
|
|
12624
11589
|
return { text, changed: text !== current };
|
|
12625
11590
|
}
|
|
12626
|
-
function rulesFileFor(root, exists =
|
|
12627
|
-
const agents =
|
|
11591
|
+
function rulesFileFor(root, exists = existsSync20) {
|
|
11592
|
+
const agents = join22(root, "AGENTS.md");
|
|
12628
11593
|
if (exists(agents)) return agents;
|
|
12629
|
-
const claude =
|
|
11594
|
+
const claude = join22(root, "CLAUDE.md");
|
|
12630
11595
|
if (exists(claude)) return claude;
|
|
12631
11596
|
return agents;
|
|
12632
11597
|
}
|
|
@@ -12635,14 +11600,14 @@ function chromeCandidates(platform, env) {
|
|
|
12635
11600
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
12636
11601
|
if (platform === "win32")
|
|
12637
11602
|
return [
|
|
12638
|
-
|
|
11603
|
+
join22(
|
|
12639
11604
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
12640
11605
|
"Google",
|
|
12641
11606
|
"Chrome",
|
|
12642
11607
|
"Application",
|
|
12643
11608
|
"chrome.exe"
|
|
12644
11609
|
),
|
|
12645
|
-
|
|
11610
|
+
join22(
|
|
12646
11611
|
env["LOCALAPPDATA"] ?? "",
|
|
12647
11612
|
"Google",
|
|
12648
11613
|
"Chrome",
|
|
@@ -12727,20 +11692,20 @@ function doctorReport(facts) {
|
|
|
12727
11692
|
return { ok: nodeOk && browserOk, lines, next_step, facts };
|
|
12728
11693
|
}
|
|
12729
11694
|
function bundledSkillsDir(from) {
|
|
12730
|
-
let dir =
|
|
11695
|
+
let dir = dirname8(from);
|
|
12731
11696
|
for (let i = 0; i < 6; i++) {
|
|
12732
|
-
const pkg =
|
|
12733
|
-
if (
|
|
11697
|
+
const pkg = join22(dir, "package.json");
|
|
11698
|
+
if (existsSync20(pkg)) {
|
|
12734
11699
|
try {
|
|
12735
11700
|
const name = JSON.parse(readFileSync9(pkg, "utf8")).name;
|
|
12736
11701
|
if (name === "@vosjs/cli") {
|
|
12737
|
-
const skills =
|
|
12738
|
-
return
|
|
11702
|
+
const skills = join22(dir, "skills");
|
|
11703
|
+
return existsSync20(skills) && statSync7(skills).isDirectory() ? skills : null;
|
|
12739
11704
|
}
|
|
12740
11705
|
} catch {
|
|
12741
11706
|
}
|
|
12742
11707
|
}
|
|
12743
|
-
const up =
|
|
11708
|
+
const up = dirname8(dir);
|
|
12744
11709
|
if (up === dir) break;
|
|
12745
11710
|
dir = up;
|
|
12746
11711
|
}
|
|
@@ -12766,7 +11731,7 @@ function npx() {
|
|
|
12766
11731
|
function bundledChromium() {
|
|
12767
11732
|
try {
|
|
12768
11733
|
const p = chromium2.executablePath();
|
|
12769
|
-
return p &&
|
|
11734
|
+
return p && existsSync21(p) ? p : null;
|
|
12770
11735
|
} catch {
|
|
12771
11736
|
return null;
|
|
12772
11737
|
}
|
|
@@ -12775,7 +11740,7 @@ function probe2() {
|
|
|
12775
11740
|
return probeBrowser({
|
|
12776
11741
|
platform: process.platform,
|
|
12777
11742
|
env: process.env,
|
|
12778
|
-
exists:
|
|
11743
|
+
exists: existsSync21,
|
|
12779
11744
|
bundled: bundledChromium
|
|
12780
11745
|
});
|
|
12781
11746
|
}
|
|
@@ -12879,7 +11844,7 @@ async function cmdSetup(argv) {
|
|
|
12879
11844
|
const dest = skillsDirFor(home);
|
|
12880
11845
|
mkdirSync6(dest, { recursive: true });
|
|
12881
11846
|
for (const name of installedSkills(bundled)) {
|
|
12882
|
-
cpSync(
|
|
11847
|
+
cpSync(join23(bundled, name), join23(dest, name), { recursive: true });
|
|
12883
11848
|
}
|
|
12884
11849
|
}
|
|
12885
11850
|
skillsVia = "bundled";
|
|
@@ -12914,7 +11879,7 @@ async function cmdSetup(argv) {
|
|
|
12914
11879
|
let rules = null;
|
|
12915
11880
|
if (!noRules) {
|
|
12916
11881
|
const file = rulesFileFor(root);
|
|
12917
|
-
const existing =
|
|
11882
|
+
const existing = existsSync21(file) ? readFileSync10(file, "utf8") : null;
|
|
12918
11883
|
const { text, changed } = applyRulesBlock(existing);
|
|
12919
11884
|
if (changed) writeFileSync6(file, text);
|
|
12920
11885
|
rules = { file, changed };
|
|
@@ -13028,7 +11993,7 @@ async function cmdWhoami(argv) {
|
|
|
13028
11993
|
function cmdLogout(argv) {
|
|
13029
11994
|
const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
13030
11995
|
const r = reporterFor(flags);
|
|
13031
|
-
const had =
|
|
11996
|
+
const had = existsSync21(CREDENTIALS_PATH);
|
|
13032
11997
|
rmSync3(CREDENTIALS_PATH, { force: true });
|
|
13033
11998
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
13034
11999
|
r.done(
|
|
@@ -13060,14 +12025,15 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
|
|
|
13060
12025
|
"media",
|
|
13061
12026
|
"print",
|
|
13062
12027
|
"yes",
|
|
13063
|
-
"composed",
|
|
13064
12028
|
"check",
|
|
13065
12029
|
"motion",
|
|
13066
12030
|
"leader",
|
|
13067
12031
|
"keep-frames",
|
|
13068
12032
|
"dry-run",
|
|
13069
12033
|
"allow-wall",
|
|
13070
|
-
"hosted"
|
|
12034
|
+
"hosted",
|
|
12035
|
+
"claimable",
|
|
12036
|
+
"share"
|
|
13071
12037
|
]);
|
|
13072
12038
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
|
|
13073
12039
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -13080,13 +12046,12 @@ Take pipeline
|
|
|
13080
12046
|
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--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|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
|
|
13081
12047
|
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]
|
|
13082
12048
|
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
13083
|
-
vos deliver <take> --to
|
|
12049
|
+
vos deliver <take> --to youtube,x,shorts,github (or all) [--look plate|gradient|dark|none] [--brand BRAND.md] [--set path=value] [--release v2.1] [--out dir] [--range a..b] [--parallel N] [--json]
|
|
13084
12050
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
13085
12051
|
vos brand <url> [--out BRAND.md] [--json]
|
|
13086
12052
|
vos open <take> [--studio <url>] [--print]
|
|
13087
12053
|
vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex (the mark and leader; the kicker is --accent)] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px <n> (the body size on the delivered frame)] [--id x] [--print] [--json]
|
|
13088
12054
|
vos validate <actions.json|take|kit.json> [--picture] [--json]
|
|
13089
|
-
vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
|
|
13090
12055
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
13091
12056
|
vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
|
|
13092
12057
|
a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so. Without a trace the file opens as FINISHED footage (a render from HyperFrames or Remotion, a film): no card, no browser bar, no drawn cursor, its own frame rate; --as take gives a screen demo the card
|
|
@@ -13140,9 +12105,12 @@ Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
|
|
|
13140
12105
|
the platform's typed changelog. A TAKE pushes to the vos in its
|
|
13141
12106
|
vos.json; --vos on a take with none ADOPTS that vos (the take
|
|
13142
12107
|
becomes its next version, built on its head), and --vos that
|
|
13143
|
-
disagrees with vos.json is refused before anything uploads. --claimable (
|
|
12108
|
+
disagrees with vos.json is refused before anything uploads. --claimable (NO
|
|
13144
12109
|
credential): creates a 72h claim link instead \u2014 hand it to the
|
|
13145
|
-
user and nowhere else; unclaimed work is deleted after 72h.
|
|
12110
|
+
user and nowhere else; unclaimed work is deleted after 72h. On a
|
|
12111
|
+
take it is for a REMIX of a public take fetched with --media: it
|
|
12112
|
+
uploads nothing, so every file must be one the fetch brought home,
|
|
12113
|
+
unchanged, and it credits the take it came from.
|
|
13146
12114
|
--override consents to touching
|
|
13147
12115
|
protected (human-edited) nodes, ONLY when the user asked.
|
|
13148
12116
|
Without --folder a new vos lands unfiled, at the root of your
|
|
@@ -13245,47 +12213,28 @@ recording cap, read live from GET /api/limits (30 min when the origin is
|
|
|
13245
12213
|
unreachable): the capture stops there and says so in one line.
|
|
13246
12214
|
frames captures PNG stills at OUTPUT times (seconds or % of duration);
|
|
13247
12215
|
--at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
|
|
13248
|
-
the agent's eyes
|
|
12216
|
+
the agent's eyes (--size WxH for an exact size). render
|
|
13249
12217
|
--range a..b --draft spot-checks a doc edit in seconds (half res, low
|
|
13250
12218
|
bitrate; range in OUTPUT seconds); a --range render keeps its audio.
|
|
13251
|
-
deliver renders the take to a release's destinations in one pass \u2014
|
|
13252
|
-
channel specs (schema/channel-specs.json, verified sizes for
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
(px, bytes, duration; misses land in skipped[] with the reason),
|
|
13256
|
-
kit.json beside the assets is the manifest.
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
role or its own ground, and with no brand the house gradient.
|
|
13271
|
-
Screenshot-genre stills never take a look.
|
|
13272
|
-
A POSTER is a document, never a template: a plain take whose card sits
|
|
13273
|
-
where the poster wants it (frame.inset), leans (the tilt track), carries
|
|
13274
|
-
the words and the mark as clips (ids stage-title, stage-kicker,
|
|
13275
|
-
stage-brand, stage-mark) and ends on a trailing freeze whose start is the
|
|
13276
|
-
still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
|
|
13277
|
-
CWS tile + marquee, GitHub social preview) render from the poster
|
|
13278
|
-
document of their aspect CLASS (landscape, square, portrait, tile), found
|
|
13279
|
-
beside the take as poster/<class>/doc.json (poster/doc.json serves every
|
|
13280
|
-
class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
|
|
13281
|
-
the document's rest; kit.json records source poster, the class, the file,
|
|
13282
|
-
the vos it tracks, the shot rect and the text boxes read FROM the
|
|
13283
|
-
document. A class with no document is the take's own frame, said once.
|
|
13284
|
-
deliver renders and verifies; it composes nothing. The composition is
|
|
13285
|
-
made with plan --style <poster>, which copies a poster's layout onto a
|
|
13286
|
-
take (its card placement, its stage clips with the release's words
|
|
13287
|
-
patched in from LAUNCH.md's headline and kicker roles or --headline and
|
|
13288
|
-
--kicker, its rest lean, its freeze), or by hand in doc.json.
|
|
12219
|
+
deliver renders the take to a release's video destinations in one pass \u2014
|
|
12220
|
+
the channel specs (schema/channel-specs.json, verified sizes for a YouTube
|
|
12221
|
+
demo, an X feed cut, a 9:16 cut for Shorts and LinkedIn, a GitHub README
|
|
12222
|
+
loop) drive each cut at exact pixels, every artifact is VERIFIED against
|
|
12223
|
+
its spec (px, bytes, duration; misses land in skipped[] with the reason),
|
|
12224
|
+
and kit.json beside the assets is the manifest. --range cuts every
|
|
12225
|
+
destination. A channel with no video destination is refused in words.
|
|
12226
|
+
The LOOK presents the card: every cut sits on a ground (a cream plate,
|
|
12227
|
+
the house gradient, a dark plate with a light streak) at ~84% of the width
|
|
12228
|
+
with headroom, a soft ambient shadow plus a tight contact shadow, and a
|
|
12229
|
+
hairline when card and ground are both light; a wide frame runs the card
|
|
12230
|
+
off the bottom. --look picks a house look (or none for the doc's own
|
|
12231
|
+
frame); with no flag the BRAND.md beside the take (or --brand <file>)
|
|
12232
|
+
decides from its look role or its own ground, and with no brand the house
|
|
12233
|
+
gradient.
|
|
12234
|
+
plan --style <doc.json|vosId> copies a signed-off take's layout onto this
|
|
12235
|
+
one (its card placement, its stage clips with the release's words patched
|
|
12236
|
+
in from LAUNCH.md's headline and kicker roles or --headline and --kicker,
|
|
12237
|
+
its rest lean, its freeze), or edit doc.json by hand.
|
|
13289
12238
|
The cut's MOTION is the document's too, in ONE vocabulary: every visual
|
|
13290
12239
|
thing (the card, a text, image or video clip, a prop) carries anim.enter,
|
|
13291
12240
|
anim.exit and anim.idle; the output lasts until the last clip ends, and
|
|
@@ -13309,40 +12258,21 @@ deleted on a refresh. deliver applies each destination's MECHANICS and
|
|
|
13309
12258
|
nothing more: the README loop drops the card's motion, every clip a
|
|
13310
12259
|
template placed and every sound; a channel that autoplays muted drops the
|
|
13311
12260
|
bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
|
|
13312
|
-
the camera.
|
|
13313
|
-
destinations (CWS screenshots, the PH gallery) are the real page at that
|
|
13314
|
-
moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
|
|
13315
|
-
policy); --composed keeps the cut's camera and chrome instead. --set
|
|
12261
|
+
the camera. --set
|
|
13316
12262
|
path=value overrides the doc in memory for every render here (the user's
|
|
13317
|
-
sets apply last).
|
|
13318
|
-
directory, then vos validate <kit.json> re-measures every
|
|
13319
|
-
bytes against the channel specs; --picture adds what each
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
separation (a light card on a light ground with no shadow), halfsize (a
|
|
13324
|
-
tile that loses its edges when the store shrinks it), and, where the kit
|
|
13325
|
-
records its text boxes, sliced, safe and contrast (APCA Lc 60/75); a
|
|
13326
|
-
video's first and last frames are read through ffmpeg. Every finding
|
|
13327
|
-
carries a code, a severity, a fix hint and a box; an error fails the
|
|
13328
|
-
verdict beside the spec problems.
|
|
13329
|
-
judge puts the kit beside its REFERENCES: for every still that has a
|
|
13330
|
-
reference of its role in the manifest (a template family, a card genre),
|
|
13331
|
-
two sheets at a common height (the asset left, then right, so order
|
|
13332
|
-
cannot bias the call) and the rubric in words, plus judge.json, a slot
|
|
13333
|
-
per pair the judge fills (win true, false or null for a tie, with the
|
|
13334
|
-
rule numbers). No model runs inside the verb: the skill judges the
|
|
13335
|
-
sheets pairwise, both orders, and the verb reports the win rate beside
|
|
13336
|
-
the spec and picture counts. The reference set is the maker's own
|
|
13337
|
-
fixture folder with a MANIFEST.json (id, file, role, layout, facts,
|
|
13338
|
-
rule per asset).
|
|
12263
|
+
sets apply last). Uploads stay manual: hand the human the kit
|
|
12264
|
+
directory, then vos validate <kit.json> re-measures every cut from its
|
|
12265
|
+
bytes against the channel specs; --picture adds what each cut LOOKS like:
|
|
12266
|
+
its first and last frames, read through ffmpeg, must not be blank or bled
|
|
12267
|
+
on all four sides. Every finding carries a code, a severity and a fix
|
|
12268
|
+
hint; an error fails the verdict beside the spec problems.
|
|
13339
12269
|
brand writes the product's BRAND.md, witnessed: it reads /design.md when
|
|
13340
12270
|
the site publishes one (the convention beside /llms.txt: fonts, logo assets,
|
|
13341
12271
|
an avoid list), /llms.txt for the name and the claim, then the page itself
|
|
13342
12272
|
in a browser (the body ground, the surfaces, the h1 and body faces and inks,
|
|
13343
12273
|
the accent buttons and links agree on, theme-color, the icons, og:image).
|
|
13344
|
-
The frontmatter carries the roles
|
|
13345
|
-
|
|
12274
|
+
The frontmatter carries the brand's roles (bgA/bgB/bgC, ink, accent,
|
|
12275
|
+
fontDisplay, logoUrl); the prose says where every value came
|
|
13346
12276
|
from and quotes the site's own avoid list. Resolve the brand BEFORE any
|
|
13347
12277
|
asset is authored; file it with vos recipe push BRAND.md --folder <slug>.
|
|
13348
12278
|
digest is how an agent SEES a recording before it cuts: writes digest/ with
|
|
@@ -13398,7 +12328,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
13398
12328
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
13399
12329
|
`;
|
|
13400
12330
|
async function loadActions(file) {
|
|
13401
|
-
const raw = JSON.parse(await
|
|
12331
|
+
const raw = JSON.parse(await readFile12(file, "utf8"));
|
|
13402
12332
|
const errors = validateActions(raw);
|
|
13403
12333
|
if (errors.length)
|
|
13404
12334
|
throw new UsageError(`invalid actions file:
|
|
@@ -13460,8 +12390,8 @@ function takeBrowserArgs(multi) {
|
|
|
13460
12390
|
function takeStorageState(flags) {
|
|
13461
12391
|
const raw = strFlag(flags, "storage-state");
|
|
13462
12392
|
if (!raw) return void 0;
|
|
13463
|
-
const path =
|
|
13464
|
-
if (!
|
|
12393
|
+
const path = resolve13(raw);
|
|
12394
|
+
if (!existsSync22(path))
|
|
13465
12395
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
13466
12396
|
return path;
|
|
13467
12397
|
}
|
|
@@ -13474,7 +12404,7 @@ function takeSessionName(flags, storageState) {
|
|
|
13474
12404
|
throw new UsageError(
|
|
13475
12405
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
13476
12406
|
);
|
|
13477
|
-
if (!
|
|
12407
|
+
if (!existsSync22(sessionDir(name)))
|
|
13478
12408
|
throw new UsageError(
|
|
13479
12409
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
13480
12410
|
);
|
|
@@ -13543,7 +12473,7 @@ async function cmdRecord(argv) {
|
|
|
13543
12473
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13544
12474
|
if (!url)
|
|
13545
12475
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13546
|
-
const outDir =
|
|
12476
|
+
const outDir = resolve13(strFlag(flags, "out") ?? "take");
|
|
13547
12477
|
const storageState = takeStorageState(flags);
|
|
13548
12478
|
const sessionName = takeSessionName(flags, storageState);
|
|
13549
12479
|
const browserArgs = takeBrowserArgs(multi);
|
|
@@ -13697,13 +12627,13 @@ ${lines.join("\n")}
|
|
|
13697
12627
|
}
|
|
13698
12628
|
const paths = takePaths(outDir);
|
|
13699
12629
|
const prepare = async () => {
|
|
13700
|
-
if (
|
|
12630
|
+
if (existsSync22(join24(outDir, "meta.json"))) {
|
|
13701
12631
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13702
12632
|
r.log(
|
|
13703
12633
|
`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" : "")
|
|
13704
12634
|
);
|
|
13705
12635
|
}
|
|
13706
|
-
await
|
|
12636
|
+
await mkdir8(outDir, { recursive: true });
|
|
13707
12637
|
await ensureTakeDir(outDir);
|
|
13708
12638
|
};
|
|
13709
12639
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13762,7 +12692,7 @@ ${lines.join("\n")}
|
|
|
13762
12692
|
},
|
|
13763
12693
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
13764
12694
|
${(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 \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
|
|
13765
|
-
Next: edit ${
|
|
12695
|
+
Next: edit ${join24(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
|
|
13766
12696
|
);
|
|
13767
12697
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
13768
12698
|
} finally {
|
|
@@ -13786,9 +12716,9 @@ async function cmdCreate2(argv) {
|
|
|
13786
12716
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13787
12717
|
if (!url)
|
|
13788
12718
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13789
|
-
const outDir =
|
|
12719
|
+
const outDir = resolve13(strFlag(flags, "out") ?? "take");
|
|
13790
12720
|
const format = videoFormat(strFlag(flags, "format"), positionals[0]);
|
|
13791
|
-
const out = positionals[0] ??
|
|
12721
|
+
const out = positionals[0] ?? join24(outDir, `out.${format}`);
|
|
13792
12722
|
const parallel = numFlag(flags, "parallel", 1);
|
|
13793
12723
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
13794
12724
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -13801,13 +12731,13 @@ async function cmdCreate2(argv) {
|
|
|
13801
12731
|
const headers = takeHeaders(multi);
|
|
13802
12732
|
const paths = takePaths(outDir);
|
|
13803
12733
|
const prepare = async () => {
|
|
13804
|
-
if (
|
|
12734
|
+
if (existsSync22(join24(outDir, "meta.json"))) {
|
|
13805
12735
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13806
12736
|
r.log(
|
|
13807
12737
|
`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" : "")
|
|
13808
12738
|
);
|
|
13809
12739
|
}
|
|
13810
|
-
await
|
|
12740
|
+
await mkdir8(outDir, { recursive: true });
|
|
13811
12741
|
await ensureTakeDir(outDir);
|
|
13812
12742
|
};
|
|
13813
12743
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13908,24 +12838,24 @@ async function cmdPlan(argv) {
|
|
|
13908
12838
|
);
|
|
13909
12839
|
const r = createReporter(flags.json === true);
|
|
13910
12840
|
if (flags.fresh === true) {
|
|
13911
|
-
await
|
|
12841
|
+
await rm2(join24(dir, "doc.json"), { force: true });
|
|
13912
12842
|
r.log("note: --fresh discarded the existing doc.json");
|
|
13913
12843
|
}
|
|
13914
12844
|
let reuse;
|
|
13915
12845
|
if (flags.reuse === true) {
|
|
13916
|
-
const from =
|
|
13917
|
-
if (!
|
|
12846
|
+
const from = resolve13(strFlag(flags, "from") ?? join24(dir, PREV_DOC_NAME));
|
|
12847
|
+
if (!existsSync22(from)) {
|
|
13918
12848
|
throw new UsageError(
|
|
13919
12849
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
13920
12850
|
);
|
|
13921
12851
|
}
|
|
13922
12852
|
reuse = {
|
|
13923
12853
|
from,
|
|
13924
|
-
doc: JSON.parse(await
|
|
12854
|
+
doc: JSON.parse(await readFile12(from, "utf8"))
|
|
13925
12855
|
};
|
|
13926
12856
|
}
|
|
13927
12857
|
const style = await resolveStyleRef(flags);
|
|
13928
|
-
const hasDoc =
|
|
12858
|
+
const hasDoc = existsSync22(join24(dir, "doc.json"));
|
|
13929
12859
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
13930
12860
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
13931
12861
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
@@ -13971,7 +12901,7 @@ async function cmdPlan(argv) {
|
|
|
13971
12901
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
13972
12902
|
r.done(
|
|
13973
12903
|
{
|
|
13974
|
-
take:
|
|
12904
|
+
take: resolve13(dir),
|
|
13975
12905
|
fresh: s.fresh,
|
|
13976
12906
|
cursorKept: s.cursorKept,
|
|
13977
12907
|
zoomAuto: s.zoomAuto,
|
|
@@ -13983,7 +12913,7 @@ async function cmdPlan(argv) {
|
|
|
13983
12913
|
...s.motion ? { motion: s.motion } : {},
|
|
13984
12914
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
13985
12915
|
},
|
|
13986
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
12916
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join24(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
|
|
13987
12917
|
);
|
|
13988
12918
|
return EXIT_OK;
|
|
13989
12919
|
}
|
|
@@ -14106,7 +13036,7 @@ async function cmdRender(argv) {
|
|
|
14106
13036
|
);
|
|
14107
13037
|
const r = createReporter(flags.json === true);
|
|
14108
13038
|
const format = videoFormat(strFlag(flags, "format"), positionals[1]);
|
|
14109
|
-
const out = positionals[1] ??
|
|
13039
|
+
const out = positionals[1] ?? join24(dir, `out.${format}`);
|
|
14110
13040
|
const parallel = numFlag(flags, "parallel", 1);
|
|
14111
13041
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
14112
13042
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -14244,7 +13174,7 @@ async function cmdDeliver(argv) {
|
|
|
14244
13174
|
const toRaw = strFlag(flags, "to");
|
|
14245
13175
|
if (!dir || !toRaw)
|
|
14246
13176
|
throw new UsageError(
|
|
14247
|
-
"vos deliver <take> --to
|
|
13177
|
+
"vos deliver <take> --to youtube,x,shorts,github (or all) [--release v2.1] [--out dir] [--range a..b] [--json]\n(hosted work: vos pull <id> --media first \u2014 deliver runs on a local take)"
|
|
14248
13178
|
);
|
|
14249
13179
|
const r = createReporter(flags.json === true);
|
|
14250
13180
|
if (!isTakeDir(dir))
|
|
@@ -14271,17 +13201,6 @@ async function cmdDeliver(argv) {
|
|
|
14271
13201
|
}
|
|
14272
13202
|
const take = await loadTake(dir);
|
|
14273
13203
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
14274
|
-
const duration = docOutputDuration3(take.doc);
|
|
14275
|
-
const timesRaw = strFlag(flags, "times");
|
|
14276
|
-
let times;
|
|
14277
|
-
if (timesRaw !== void 0) {
|
|
14278
|
-
try {
|
|
14279
|
-
const doc = take.doc;
|
|
14280
|
-
times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes2(s, duration)[0]);
|
|
14281
|
-
} catch (e) {
|
|
14282
|
-
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
14283
|
-
}
|
|
14284
|
-
}
|
|
14285
13204
|
const parallel = numFlag(flags, "parallel", 1);
|
|
14286
13205
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
14287
13206
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -14296,20 +13215,15 @@ async function cmdDeliver(argv) {
|
|
|
14296
13215
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
14297
13216
|
}
|
|
14298
13217
|
r.log(`look: ${lookPick.from}`);
|
|
14299
|
-
const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
|
|
14300
|
-
if (launch) r.log(`launch: ${launch.file}`);
|
|
14301
13218
|
const browser = await launchBrowser();
|
|
14302
13219
|
try {
|
|
14303
13220
|
const result = await deliverTake(browser, dir, {
|
|
14304
13221
|
look: lookPick.look,
|
|
14305
|
-
launchRoles: launch?.roles ?? null,
|
|
14306
13222
|
channels,
|
|
14307
13223
|
outDir: strFlag(flags, "out"),
|
|
14308
13224
|
release: strFlag(flags, "release"),
|
|
14309
|
-
times,
|
|
14310
13225
|
range,
|
|
14311
13226
|
parallel,
|
|
14312
|
-
composed: hasFlag(flags, "composed"),
|
|
14313
13227
|
overrides: {
|
|
14314
13228
|
set: multi.set,
|
|
14315
13229
|
background: strFlag(flags, "background"),
|
|
@@ -14326,7 +13240,7 @@ async function cmdDeliver(argv) {
|
|
|
14326
13240
|
});
|
|
14327
13241
|
const { kit } = result;
|
|
14328
13242
|
const assetLines = kit.assets.map(
|
|
14329
|
-
(a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB
|
|
13243
|
+
(a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB, ${a.seconds.toFixed(1)}s)`
|
|
14330
13244
|
);
|
|
14331
13245
|
const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
|
|
14332
13246
|
r.done(
|
|
@@ -14339,7 +13253,7 @@ async function cmdDeliver(argv) {
|
|
|
14339
13253
|
`Wrote ${kit.assets.length} asset(s) + kit.json to ${result.outDir}` + (assetLines.length ? `
|
|
14340
13254
|
${assetLines.join("\n ")}` : "") + (skippedLines.length ? `
|
|
14341
13255
|
${skippedLines.join("\n ")}` : "") + `
|
|
14342
|
-
|
|
13256
|
+
Uploads stay manual: hand the human this directory and the manifest.`
|
|
14343
13257
|
);
|
|
14344
13258
|
return kit.assets.length > 0 ? EXIT_OK : EXIT_ERROR;
|
|
14345
13259
|
} finally {
|
|
@@ -14353,15 +13267,15 @@ async function resolveStyleRef(flags) {
|
|
|
14353
13267
|
}
|
|
14354
13268
|
async function resolveDocRef(ref, flags, what) {
|
|
14355
13269
|
const styleRef = ref;
|
|
14356
|
-
const file =
|
|
13270
|
+
const file = existsSync22(styleRef) ? resolve13(
|
|
14357
13271
|
styleRef,
|
|
14358
|
-
|
|
13272
|
+
existsSync22(join24(styleRef, "doc.json")) ? "doc.json" : ""
|
|
14359
13273
|
) : null;
|
|
14360
|
-
if (file &&
|
|
13274
|
+
if (file && existsSync22(file)) {
|
|
14361
13275
|
return {
|
|
14362
13276
|
from: file,
|
|
14363
|
-
doc:
|
|
14364
|
-
JSON.parse(await
|
|
13277
|
+
doc: migrateHostedDoc4(
|
|
13278
|
+
JSON.parse(await readFile12(file, "utf8"))
|
|
14365
13279
|
)
|
|
14366
13280
|
};
|
|
14367
13281
|
}
|
|
@@ -14389,7 +13303,7 @@ async function resolveDocRef(ref, flags, what) {
|
|
|
14389
13303
|
throw new UsageError(`${what}: ${styleRef} carries no doc (is it a take?)`);
|
|
14390
13304
|
return {
|
|
14391
13305
|
from: `${origin}/vos/${styleRef}`,
|
|
14392
|
-
doc:
|
|
13306
|
+
doc: migrateHostedDoc4(doc.body)
|
|
14393
13307
|
};
|
|
14394
13308
|
}
|
|
14395
13309
|
async function officialTemplate(origin, ref) {
|
|
@@ -14413,7 +13327,7 @@ async function cmdDigest(argv) {
|
|
|
14413
13327
|
const take = await loadTake(dir);
|
|
14414
13328
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
14415
13329
|
const transcriptPath = strFlag(flags, "transcript");
|
|
14416
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
13330
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile12(transcriptPath, "utf8"))) : null;
|
|
14417
13331
|
const style = await resolveStyleRef(flags);
|
|
14418
13332
|
const noFrames = flags["no-frames"] === true;
|
|
14419
13333
|
let browser = null;
|
|
@@ -14444,7 +13358,7 @@ async function cmdDigest(argv) {
|
|
|
14444
13358
|
kinds,
|
|
14445
13359
|
frames: d.images.full,
|
|
14446
13360
|
crops: d.images.crop,
|
|
14447
|
-
sheet: d.images.sheet ?
|
|
13361
|
+
sheet: d.images.sheet ? join24(result.outDir, d.images.sheet) : null,
|
|
14448
13362
|
scenes: kinds.scene ?? 0,
|
|
14449
13363
|
sourceDuration: d.take.sourceDuration,
|
|
14450
13364
|
outputDuration: d.take.outputDuration,
|
|
@@ -14472,14 +13386,14 @@ async function cmdOpen(argv) {
|
|
|
14472
13386
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
14473
13387
|
const r = createReporter(flags.json === true);
|
|
14474
13388
|
const take = await loadTake(dir);
|
|
14475
|
-
if (!
|
|
13389
|
+
if (!existsSync22(take.paths.recording)) {
|
|
14476
13390
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
14477
13391
|
}
|
|
14478
13392
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
14479
13393
|
/\/+$/,
|
|
14480
13394
|
""
|
|
14481
13395
|
);
|
|
14482
|
-
const server = await startTakeServer(
|
|
13396
|
+
const server = await startTakeServer(resolve13(dir), {});
|
|
14483
13397
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
14484
13398
|
r.event({ event: "open", server: server.base, url });
|
|
14485
13399
|
r.log(`take served at ${server.base}`);
|
|
@@ -14504,35 +13418,6 @@ async function cmdOpen(argv) {
|
|
|
14504
13418
|
});
|
|
14505
13419
|
return EXIT_OK;
|
|
14506
13420
|
}
|
|
14507
|
-
async function cmdJudge(argv) {
|
|
14508
|
-
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
|
|
14509
|
-
const target = positionals[0];
|
|
14510
|
-
const against = strFlag(flags, "against");
|
|
14511
|
-
if (!target || !against)
|
|
14512
|
-
throw new UsageError(
|
|
14513
|
-
"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)"
|
|
14514
|
-
);
|
|
14515
|
-
const r = createReporter(flags.json === true);
|
|
14516
|
-
const kitFile = target.endsWith("kit.json") ? target : join26(target, "kit.json");
|
|
14517
|
-
if (!existsSync24(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
14518
|
-
if (!existsSync24(against))
|
|
14519
|
-
throw new UsageError(`${against}: no reference manifest`);
|
|
14520
|
-
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
14521
|
-
const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
|
|
14522
|
-
const rate = winRate(verdicts);
|
|
14523
|
-
const lines = result.sheets.map(
|
|
14524
|
-
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
14525
|
-
);
|
|
14526
|
-
r.done(
|
|
14527
|
-
{ ...result, winRate: rate },
|
|
14528
|
-
`Wrote ${result.sheets.length} sheet pair(s) to ${result.outDir}` + (lines.length ? `
|
|
14529
|
-
${lines.join("\n ")}` : "") + (result.skipped.length ? `
|
|
14530
|
-
skipped: ${result.skipped.join("\n skipped: ")}` : "") + `
|
|
14531
|
-
Judge each pair both ways (the rubric is beside it) and fill ${result.verdictFile}` + (rate.judged ? `
|
|
14532
|
-
Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judged (${(rate.rate * 100).toFixed(0)}%; a tie counts a half); parity with the references is 50%, the marketability bar is 40%` : "")
|
|
14533
|
-
);
|
|
14534
|
-
return EXIT_OK;
|
|
14535
|
-
}
|
|
14536
13421
|
async function cmdCallout(argv) {
|
|
14537
13422
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
|
|
14538
13423
|
const dir = positionals[0];
|
|
@@ -14558,7 +13443,7 @@ async function cmdCallout(argv) {
|
|
|
14558
13443
|
`no register: put a BRAND.md beside the take (vos brand <url> --out BRAND.md; its bgA and accent are the ground and the accent), or pass --ground #hex --accent #hex`
|
|
14559
13444
|
);
|
|
14560
13445
|
const codeFile = strFlag(flags, "code-file");
|
|
14561
|
-
const code = codeFile ? await
|
|
13446
|
+
const code = codeFile ? await readFile12(codeFile, "utf8") : strFlag(flags, "code");
|
|
14562
13447
|
const words2 = {
|
|
14563
13448
|
kicker: strFlag(flags, "kicker"),
|
|
14564
13449
|
title: strFlag(flags, "title"),
|
|
@@ -14637,7 +13522,7 @@ async function cmdValidate(argv) {
|
|
|
14637
13522
|
const target = positionals[0];
|
|
14638
13523
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
14639
13524
|
const r = createReporter(flags.json === true);
|
|
14640
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
13525
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync22(join24(target, "kit.json")) && !isTakeDir(target) ? join24(target, "kit.json") : null;
|
|
14641
13526
|
if (kitTarget) {
|
|
14642
13527
|
const picture = flags.picture === true;
|
|
14643
13528
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -14669,9 +13554,9 @@ async function cmdValidate(argv) {
|
|
|
14669
13554
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
14670
13555
|
return EXIT_OK;
|
|
14671
13556
|
}
|
|
14672
|
-
if (!isTakeDir(target) &&
|
|
13557
|
+
if (!isTakeDir(target) && existsSync22(join24(target, "config.json"))) {
|
|
14673
13558
|
const parsed = JSON.parse(
|
|
14674
|
-
await
|
|
13559
|
+
await readFile12(join24(target, "config.json"), "utf8")
|
|
14675
13560
|
);
|
|
14676
13561
|
const pre = preflightConfig(parsed);
|
|
14677
13562
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -14712,7 +13597,7 @@ async function cmdValidate(argv) {
|
|
|
14712
13597
|
const take = await loadTake(target);
|
|
14713
13598
|
const problems = [];
|
|
14714
13599
|
const warnings = [];
|
|
14715
|
-
if (!
|
|
13600
|
+
if (!existsSync22(take.paths.recording)) {
|
|
14716
13601
|
problems.push("missing recording (re-run record)");
|
|
14717
13602
|
} else {
|
|
14718
13603
|
const mismatch = containerMismatch(
|
|
@@ -14801,20 +13686,49 @@ async function cmdPush3(argv) {
|
|
|
14801
13686
|
const dir = positionals[0];
|
|
14802
13687
|
if (!dir)
|
|
14803
13688
|
throw new UsageError(
|
|
14804
|
-
"vos push <take> [--vos|--title|--label|--note|--prompt|--folder|--override|--yes|--key|--api]"
|
|
13689
|
+
"vos push <take> [--vos|--title|--label|--note|--prompt|--folder|--override|--yes|--key|--api|--claimable [--share]]"
|
|
14805
13690
|
);
|
|
14806
13691
|
const refusal = takePushRefusal({
|
|
14807
13692
|
vos: strFlag(flags, "vos"),
|
|
14808
|
-
trackedVosId: readSyncState(
|
|
13693
|
+
trackedVosId: readSyncState(resolve13(dir))?.vosId ?? null,
|
|
14809
13694
|
programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
|
|
14810
13695
|
(name) => hasFlag(flags, name)
|
|
14811
13696
|
)
|
|
14812
13697
|
});
|
|
14813
13698
|
if (refusal) throw new UsageError(refusal);
|
|
14814
13699
|
const r = createReporter(flags.json === true);
|
|
13700
|
+
if (flags.claimable === true) {
|
|
13701
|
+
const iterating = ["vos", "folder", "override", "base"].filter(
|
|
13702
|
+
(name) => hasFlag(flags, name)
|
|
13703
|
+
);
|
|
13704
|
+
if (iterating.length)
|
|
13705
|
+
throw new UsageError(
|
|
13706
|
+
`--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
|
|
13707
|
+
);
|
|
13708
|
+
const claim = await pushTakeClaimable(
|
|
13709
|
+
resolve13(dir),
|
|
13710
|
+
{
|
|
13711
|
+
api: strFlag(flags, "api"),
|
|
13712
|
+
origin: strFlag(flags, "origin"),
|
|
13713
|
+
title: strFlag(flags, "title"),
|
|
13714
|
+
prompt: strFlag(flags, "prompt"),
|
|
13715
|
+
share: flags.share === true
|
|
13716
|
+
},
|
|
13717
|
+
r
|
|
13718
|
+
);
|
|
13719
|
+
r.done(
|
|
13720
|
+
{ ...claim },
|
|
13721
|
+
`Claimable push created (${claim.title})
|
|
13722
|
+
claim: ${claim.claimUrl}
|
|
13723
|
+
expires: ${claim.expiresAt} \u2014 unclaimed work is deleted after 72h
|
|
13724
|
+
Hand the claim link to the user and NOWHERE else \u2014 it is the only
|
|
13725
|
+
reference and the only credential.`
|
|
13726
|
+
);
|
|
13727
|
+
return 0;
|
|
13728
|
+
}
|
|
14815
13729
|
const overrides = multi.override;
|
|
14816
13730
|
const result = await pushTake(
|
|
14817
|
-
|
|
13731
|
+
resolve13(dir),
|
|
14818
13732
|
{
|
|
14819
13733
|
key: strFlag(flags, "key"),
|
|
14820
13734
|
api: strFlag(flags, "api"),
|
|
@@ -14868,7 +13782,7 @@ async function cmdPull2(argv) {
|
|
|
14868
13782
|
const dir = positionals[0] ?? ".";
|
|
14869
13783
|
const r = createReporter(flags.json === true);
|
|
14870
13784
|
const result = await pullTake(
|
|
14871
|
-
|
|
13785
|
+
resolve13(dir),
|
|
14872
13786
|
{
|
|
14873
13787
|
key: strFlag(flags, "key"),
|
|
14874
13788
|
api: strFlag(flags, "api"),
|
|
@@ -14943,8 +13857,6 @@ async function run(argv) {
|
|
|
14943
13857
|
return await cmdValidate(rest);
|
|
14944
13858
|
case "callout":
|
|
14945
13859
|
return await cmdCallout(rest);
|
|
14946
|
-
case "judge":
|
|
14947
|
-
return await cmdJudge(rest);
|
|
14948
13860
|
case "fetch":
|
|
14949
13861
|
return await cmdFetch(rest);
|
|
14950
13862
|
case "duplicate":
|
|
@@ -15070,7 +13982,7 @@ async function cmdSession(argv) {
|
|
|
15070
13982
|
if (!arg) throw new UsageError(usage);
|
|
15071
13983
|
const bad = sessionNameVerdict(arg);
|
|
15072
13984
|
if (bad) throw new UsageError(bad);
|
|
15073
|
-
if (!
|
|
13985
|
+
if (!existsSync22(sessionDir(arg)))
|
|
15074
13986
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
15075
13987
|
const summary3 = await summarizeSession(arg);
|
|
15076
13988
|
const url = strFlag(flags, "url");
|
|
@@ -15170,4 +14082,4 @@ export {
|
|
|
15170
14082
|
verbHelp,
|
|
15171
14083
|
run
|
|
15172
14084
|
};
|
|
15173
|
-
//# sourceMappingURL=chunk-
|
|
14085
|
+
//# sourceMappingURL=chunk-5DUXN3RX.js.map
|