@vosjs/cli 0.68.0 → 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 +7 -10
- package/dist/{chunk-AUFY3M22.js → chunk-2KEQ3LKH.js} +2 -2
- package/dist/chunk-2KEQ3LKH.js.map +1 -0
- package/dist/{chunk-ZS3L7CEN.js → chunk-5DUXN3RX.js} +985 -2186
- package/dist/chunk-5DUXN3RX.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/manifest-QLQKVSML.js +8 -0
- package/dist/{run-L4CDAJLT.js → run-NDBUFOIG.js} +2 -2
- package/package.json +6 -6
- 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-AUFY3M22.js.map +0 -1
- package/dist/chunk-ZS3L7CEN.js.map +0 -1
- package/dist/manifest-ZBK6IERM.js +0 -8
- /package/dist/{manifest-ZBK6IERM.js.map → manifest-QLQKVSML.js.map} +0 -0
- /package/dist/{run-L4CDAJLT.js.map → run-NDBUFOIG.js.map} +0 -0
|
@@ -131,13 +131,13 @@ function landedLines(l) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// src/plugin/run.ts
|
|
134
|
-
import { mkdir as
|
|
135
|
-
import { existsSync as
|
|
136
|
-
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";
|
|
137
137
|
import {
|
|
138
138
|
END_CARD_FROM as END_CARD_FROM2,
|
|
139
139
|
docOutputDuration as docOutputDuration3,
|
|
140
|
-
migrateHostedDoc as
|
|
140
|
+
migrateHostedDoc as migrateHostedDoc4
|
|
141
141
|
} from "@vosjs/studio-core";
|
|
142
142
|
|
|
143
143
|
// src/plugin/output.ts
|
|
@@ -2120,10 +2120,10 @@ function startTakeServer(rootDir, pages) {
|
|
|
2120
2120
|
res.writeHead(404).end();
|
|
2121
2121
|
});
|
|
2122
2122
|
return new Promise(
|
|
2123
|
-
(
|
|
2123
|
+
(resolve14) => server.listen(0, () => {
|
|
2124
2124
|
const addr = server.address();
|
|
2125
2125
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
2126
|
-
|
|
2126
|
+
resolve14({ base: `http://localhost:${port}`, close: () => server.close() });
|
|
2127
2127
|
})
|
|
2128
2128
|
);
|
|
2129
2129
|
}
|
|
@@ -2173,8 +2173,8 @@ function renderPageHtml() {
|
|
|
2173
2173
|
${tweenRuntimeCode}
|
|
2174
2174
|
// Vos Element System (bundled IIFE) \u2014 the same injection the engine's
|
|
2175
2175
|
// render template performs. Take configs never touch ctx.elements, but a
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2176
|
+
// program may draw element text and images over its ground, and without
|
|
2177
|
+
// this the elements silently never draw.
|
|
2178
2178
|
${elementsBundleCode}
|
|
2179
2179
|
window.__vos__ = window.__vos__ || {}
|
|
2180
2180
|
window.__vos__.elements = __vosElementsFactory.createVosElements(THREE)
|
|
@@ -2360,14 +2360,14 @@ async function renderInPage(opts) {
|
|
|
2360
2360
|
w.__error = String(e instanceof Error && e.stack || e);
|
|
2361
2361
|
}
|
|
2362
2362
|
}
|
|
2363
|
-
async function renderChunk(context2, serverBase,
|
|
2364
|
-
const outName = `render-chunk-${
|
|
2363
|
+
async function renderChunk(context2, serverBase, chunk, opts, onChunkProgress) {
|
|
2364
|
+
const outName = `render-chunk-${chunk.index}.tmp`;
|
|
2365
2365
|
const page = await context2.newPage();
|
|
2366
2366
|
try {
|
|
2367
2367
|
page.on("console", (m) => {
|
|
2368
2368
|
if (m.type() === "error" || process.env.VOS_RENDER_DEBUG)
|
|
2369
2369
|
process.stderr.write(
|
|
2370
|
-
` [render page ${
|
|
2370
|
+
` [render page ${chunk.index} ${m.type()}] ${m.text()}
|
|
2371
2371
|
`
|
|
2372
2372
|
);
|
|
2373
2373
|
});
|
|
@@ -2375,7 +2375,7 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
|
|
|
2375
2375
|
page.on(
|
|
2376
2376
|
"pageerror",
|
|
2377
2377
|
(e) => process.stderr.write(
|
|
2378
|
-
` [render pageerror ${
|
|
2378
|
+
` [render pageerror ${chunk.index}] ${String(e)}
|
|
2379
2379
|
`
|
|
2380
2380
|
)
|
|
2381
2381
|
);
|
|
@@ -2394,8 +2394,8 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
|
|
|
2394
2394
|
W: opts.width,
|
|
2395
2395
|
H: opts.height,
|
|
2396
2396
|
fps: opts.fps,
|
|
2397
|
-
startFrame:
|
|
2398
|
-
endFrame:
|
|
2397
|
+
startFrame: chunk.startFrame + frameOffset,
|
|
2398
|
+
endFrame: chunk.endFrame + frameOffset,
|
|
2399
2399
|
format: opts.format,
|
|
2400
2400
|
bitrate: opts.bitrate ?? 1e7,
|
|
2401
2401
|
mediabunnyUrl: MEDIABUNNY_URL,
|
|
@@ -2406,10 +2406,10 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
|
|
|
2406
2406
|
audioDuration: audio?.duration
|
|
2407
2407
|
}).catch(() => {
|
|
2408
2408
|
});
|
|
2409
|
-
const timeoutMs = 12e4 +
|
|
2409
|
+
const timeoutMs = 12e4 + chunk.frameCount * 500;
|
|
2410
2410
|
await waitForPageDone(
|
|
2411
2411
|
page,
|
|
2412
|
-
`render chunk ${
|
|
2412
|
+
`render chunk ${chunk.index}`,
|
|
2413
2413
|
onChunkProgress,
|
|
2414
2414
|
timeoutMs
|
|
2415
2415
|
);
|
|
@@ -2441,8 +2441,8 @@ async function renderAnimation(browser, opts) {
|
|
|
2441
2441
|
};
|
|
2442
2442
|
const outNames = await Promise.all(
|
|
2443
2443
|
chunks.map(
|
|
2444
|
-
(
|
|
2445
|
-
progress[
|
|
2444
|
+
(chunk) => renderChunk(context2, server.base, chunk, effOpts, (fraction) => {
|
|
2445
|
+
progress[chunk.index] = fraction;
|
|
2446
2446
|
reportProgress();
|
|
2447
2447
|
})
|
|
2448
2448
|
)
|
|
@@ -2457,9 +2457,9 @@ async function renderAnimation(browser, opts) {
|
|
|
2457
2457
|
return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
|
|
2458
2458
|
}
|
|
2459
2459
|
const { bytes } = await concatEncodedVideo(
|
|
2460
|
-
chunks.map((
|
|
2460
|
+
chunks.map((chunk, i) => ({
|
|
2461
2461
|
data: new Uint8Array(files[i]),
|
|
2462
|
-
duration:
|
|
2462
|
+
duration: chunk.duration
|
|
2463
2463
|
})),
|
|
2464
2464
|
{ format: opts.format, frameRate: opts.fps }
|
|
2465
2465
|
);
|
|
@@ -3401,11 +3401,10 @@ async function writeIndexJson(result) {
|
|
|
3401
3401
|
}
|
|
3402
3402
|
|
|
3403
3403
|
// src/plugin/deliver.ts
|
|
3404
|
-
import { mkdir as mkdir3,
|
|
3405
|
-
import {
|
|
3406
|
-
import {
|
|
3407
|
-
import {
|
|
3408
|
-
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";
|
|
3409
3408
|
import { parseFrontmatter } from "@vosjs/shared/frontmatter";
|
|
3410
3409
|
import {
|
|
3411
3410
|
CHANNEL_SPECS_VERIFIED,
|
|
@@ -3523,614 +3522,119 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
3523
3522
|
};
|
|
3524
3523
|
}
|
|
3525
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
|
+
|
|
3526
3539
|
// src/plugin/moments.ts
|
|
3527
|
-
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
|
+
}
|
|
3528
3548
|
|
|
3529
|
-
// src/plugin/
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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
|
|
3550
3580
|
);
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
colorType = bytes[start + 9];
|
|
3557
|
-
interlace = bytes[start + 12];
|
|
3558
|
-
} else if (type === "IDAT") {
|
|
3559
|
-
idat.push(bytes.subarray(start, start + len));
|
|
3560
|
-
} else if (type === "IEND") break;
|
|
3561
|
-
pos = start + len + 4;
|
|
3562
|
-
}
|
|
3563
|
-
if (!w || !h || depth !== 8 || interlace !== 0) return null;
|
|
3564
|
-
const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
|
|
3565
|
-
if (!channels) return null;
|
|
3566
|
-
const total = idat.reduce((n, c) => n + c.length, 0);
|
|
3567
|
-
const joined = new Uint8Array(total);
|
|
3568
|
-
let off2 = 0;
|
|
3569
|
-
for (const c of idat) {
|
|
3570
|
-
joined.set(c, off2);
|
|
3571
|
-
off2 += c.length;
|
|
3572
|
-
}
|
|
3573
|
-
let raw;
|
|
3574
|
-
try {
|
|
3575
|
-
raw = new Uint8Array(inflateSync(joined));
|
|
3576
|
-
} catch {
|
|
3577
|
-
return null;
|
|
3578
|
-
}
|
|
3579
|
-
const stride = w * channels;
|
|
3580
|
-
if (raw.length < (stride + 1) * h) return null;
|
|
3581
|
-
const out = new Uint8Array(w * h * 4);
|
|
3582
|
-
const prev = new Uint8Array(stride);
|
|
3583
|
-
const cur = new Uint8Array(stride);
|
|
3584
|
-
for (let y = 0; y < h; y++) {
|
|
3585
|
-
const filter = raw[y * (stride + 1)];
|
|
3586
|
-
const rowStart = y * (stride + 1) + 1;
|
|
3587
|
-
for (let i = 0; i < stride; i++) {
|
|
3588
|
-
const x = raw[rowStart + i];
|
|
3589
|
-
const a = i >= channels ? cur[i - channels] : 0;
|
|
3590
|
-
const b = prev[i];
|
|
3591
|
-
const c = i >= channels ? prev[i - channels] : 0;
|
|
3592
|
-
let v;
|
|
3593
|
-
switch (filter) {
|
|
3594
|
-
case 0:
|
|
3595
|
-
v = x;
|
|
3596
|
-
break;
|
|
3597
|
-
case 1:
|
|
3598
|
-
v = x + a;
|
|
3599
|
-
break;
|
|
3600
|
-
case 2:
|
|
3601
|
-
v = x + b;
|
|
3602
|
-
break;
|
|
3603
|
-
case 3:
|
|
3604
|
-
v = x + (a + b >> 1);
|
|
3605
|
-
break;
|
|
3606
|
-
case 4: {
|
|
3607
|
-
const p = a + b - c;
|
|
3608
|
-
const pa = Math.abs(p - a);
|
|
3609
|
-
const pb = Math.abs(p - b);
|
|
3610
|
-
const pc = Math.abs(p - c);
|
|
3611
|
-
v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
|
3612
|
-
break;
|
|
3613
|
-
}
|
|
3614
|
-
default:
|
|
3615
|
-
return null;
|
|
3616
|
-
}
|
|
3617
|
-
cur[i] = v & 255;
|
|
3618
|
-
}
|
|
3619
|
-
for (let px = 0; px < w; px++) {
|
|
3620
|
-
const o = (y * w + px) * 4;
|
|
3621
|
-
const s = px * channels;
|
|
3622
|
-
if (channels >= 3) {
|
|
3623
|
-
out[o] = cur[s];
|
|
3624
|
-
out[o + 1] = cur[s + 1];
|
|
3625
|
-
out[o + 2] = cur[s + 2];
|
|
3626
|
-
out[o + 3] = channels === 4 ? cur[s + 3] : 255;
|
|
3627
|
-
} else {
|
|
3628
|
-
out[o] = out[o + 1] = out[o + 2] = cur[s];
|
|
3629
|
-
out[o + 3] = channels === 2 ? cur[s + 1] : 255;
|
|
3630
|
-
}
|
|
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;
|
|
3631
3586
|
}
|
|
3632
|
-
|
|
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++;
|
|
3633
3594
|
}
|
|
3634
|
-
return {
|
|
3595
|
+
return { doc, count, skipped };
|
|
3635
3596
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
const p = at(img, x, y);
|
|
3654
|
-
r.push(p[0]);
|
|
3655
|
-
g.push(p[1]);
|
|
3656
|
-
b.push(p[2]);
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
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));
|
|
3659
3614
|
}
|
|
3660
|
-
return
|
|
3615
|
+
return out;
|
|
3661
3616
|
}
|
|
3662
|
-
var
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
if (x > x1) x1 = x;
|
|
3676
|
-
if (y > y1) y1 = y;
|
|
3677
|
-
}
|
|
3678
|
-
}
|
|
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;
|
|
3679
3630
|
}
|
|
3680
|
-
if (
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
let n = 0;
|
|
3688
|
-
const sx = Math.max(1, Math.round((x1 - x0) / 60));
|
|
3689
|
-
const sy = Math.max(1, Math.round((y1 - y0) / 60));
|
|
3690
|
-
for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
|
|
3691
|
-
for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
|
|
3692
|
-
sum += delta(at(img, x, y), ground);
|
|
3693
|
-
n++;
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
|
-
return n ? sum / n : 0;
|
|
3697
|
-
};
|
|
3698
|
-
const rise = (bands) => {
|
|
3699
|
-
const [outer, mid] = bands;
|
|
3700
|
-
return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
|
|
3701
|
-
};
|
|
3702
|
-
const readings = [];
|
|
3703
|
-
const yA = card.y + card.h * 0.2;
|
|
3704
|
-
const yB = card.y + card.h * 0.8;
|
|
3705
|
-
if (card.x >= 1 && card.w > band * 4) {
|
|
3706
|
-
const x0 = card.x;
|
|
3707
|
-
readings.push(
|
|
3708
|
-
rise([
|
|
3709
|
-
meanRect(x0, yA, x0 + band, yB),
|
|
3710
|
-
meanRect(x0 + band, yA, x0 + band * 2, yB),
|
|
3711
|
-
meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
|
|
3712
|
-
])
|
|
3713
|
-
);
|
|
3714
|
-
}
|
|
3715
|
-
if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
|
|
3716
|
-
const x1 = card.x + card.w;
|
|
3717
|
-
readings.push(
|
|
3718
|
-
rise([
|
|
3719
|
-
meanRect(x1 - band, yA, x1, yB),
|
|
3720
|
-
meanRect(x1 - band * 2, yA, x1 - band, yB),
|
|
3721
|
-
meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
|
|
3722
|
-
])
|
|
3723
|
-
);
|
|
3724
|
-
}
|
|
3725
|
-
if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
|
|
3726
|
-
const y1 = card.y + card.h;
|
|
3727
|
-
const xA = card.x + card.w * 0.2;
|
|
3728
|
-
const xB = card.x + card.w * 0.8;
|
|
3729
|
-
readings.push(
|
|
3730
|
-
rise([
|
|
3731
|
-
meanRect(xA, y1 - band, xB, y1),
|
|
3732
|
-
meanRect(xA, y1 - band * 2, xB, y1 - band),
|
|
3733
|
-
meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
|
|
3734
|
-
])
|
|
3735
|
-
);
|
|
3736
|
-
}
|
|
3737
|
-
return readings.length ? Math.max(...readings) : null;
|
|
3738
|
-
}
|
|
3739
|
-
function edgeContrast(img, ground, card) {
|
|
3740
|
-
const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
|
|
3741
|
-
const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
|
|
3742
|
-
const col = (x) => {
|
|
3743
|
-
if (x < 0 || x >= img.w) return 0;
|
|
3744
|
-
const ds = [];
|
|
3745
|
-
const step = Math.max(1, Math.round((yB - yA) / 80));
|
|
3746
|
-
for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
|
|
3747
|
-
return median(ds);
|
|
3748
|
-
};
|
|
3749
|
-
const depth = Math.min(24, Math.floor(card.w / 8));
|
|
3750
|
-
let best = 0;
|
|
3751
|
-
for (let d = 0; d < depth; d++) {
|
|
3752
|
-
best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
|
|
3753
|
-
}
|
|
3754
|
-
return best;
|
|
3755
|
-
}
|
|
3756
|
-
function inkCoverage(img, rect, threshold = 48) {
|
|
3757
|
-
const x0 = Math.max(0, Math.floor(rect.x));
|
|
3758
|
-
const y0 = Math.max(0, Math.floor(rect.y));
|
|
3759
|
-
const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
|
|
3760
|
-
const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
|
|
3761
|
-
if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
|
|
3762
|
-
const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
|
|
3763
|
-
const r = [];
|
|
3764
|
-
const g = [];
|
|
3765
|
-
const b = [];
|
|
3766
|
-
const pts = [];
|
|
3767
|
-
for (let y = y0; y < y1; y += step) {
|
|
3768
|
-
for (let x = x0; x < x1; x += step) {
|
|
3769
|
-
const p = at(img, x, y);
|
|
3770
|
-
pts.push(p);
|
|
3771
|
-
r.push(p[0]);
|
|
3772
|
-
g.push(p[1]);
|
|
3773
|
-
b.push(p[2]);
|
|
3774
|
-
}
|
|
3775
|
-
}
|
|
3776
|
-
const base = [median(r), median(g), median(b)];
|
|
3777
|
-
let ink = 0;
|
|
3778
|
-
for (const p of pts) if (delta(p, base) > threshold) ink++;
|
|
3779
|
-
return pts.length ? ink / pts.length : 0;
|
|
3780
|
-
}
|
|
3781
|
-
function differenceHash(img, rect) {
|
|
3782
|
-
const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
3783
|
-
const cols = 9;
|
|
3784
|
-
const rows = 8;
|
|
3785
|
-
const cell = [];
|
|
3786
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3787
|
-
for (let cx = 0; cx < cols; cx++) {
|
|
3788
|
-
const x0 = Math.floor(r.x + cx * r.w / cols);
|
|
3789
|
-
const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
|
|
3790
|
-
const y0 = Math.floor(r.y + cy * r.h / rows);
|
|
3791
|
-
const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
|
|
3792
|
-
let sum = 0;
|
|
3793
|
-
let n = 0;
|
|
3794
|
-
const sx = Math.max(1, Math.floor((x1 - x0) / 8));
|
|
3795
|
-
const sy = Math.max(1, Math.floor((y1 - y0) / 8));
|
|
3796
|
-
for (let y = y0; y < y1 && y < img.h; y += sy) {
|
|
3797
|
-
for (let x = x0; x < x1 && x < img.w; x += sx) {
|
|
3798
|
-
const p = at(img, Math.max(0, x), Math.max(0, y));
|
|
3799
|
-
sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
|
|
3800
|
-
n++;
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
cell.push(n ? sum / n : 0);
|
|
3804
|
-
}
|
|
3805
|
-
}
|
|
3806
|
-
let bits = "";
|
|
3807
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3808
|
-
let byte = 0;
|
|
3809
|
-
for (let cx = 0; cx < cols - 1; cx++) {
|
|
3810
|
-
const l = cell[cy * cols + cx];
|
|
3811
|
-
const rr = cell[cy * cols + cx + 1];
|
|
3812
|
-
byte = byte << 1 | (l > rr ? 1 : 0);
|
|
3813
|
-
}
|
|
3814
|
-
bits += byte.toString(16).padStart(2, "0");
|
|
3815
|
-
}
|
|
3816
|
-
return bits;
|
|
3817
|
-
}
|
|
3818
|
-
function hammingDistance(a, b) {
|
|
3819
|
-
let d = 0;
|
|
3820
|
-
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
3821
|
-
let x = parseInt(a[i], 16) ^ parseInt(b[i], 16);
|
|
3822
|
-
while (x) {
|
|
3823
|
-
d += x & 1;
|
|
3824
|
-
x >>= 1;
|
|
3825
|
-
}
|
|
3826
|
-
}
|
|
3827
|
-
return d + Math.abs(a.length - b.length) * 4;
|
|
3828
|
-
}
|
|
3829
|
-
function luminance(c) {
|
|
3830
|
-
const lin = (v) => {
|
|
3831
|
-
const s = v / 255;
|
|
3832
|
-
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
3833
|
-
};
|
|
3834
|
-
return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
|
|
3835
|
-
}
|
|
3836
|
-
function lightness(c) {
|
|
3837
|
-
const y = luminance(c);
|
|
3838
|
-
return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
|
|
3839
|
-
}
|
|
3840
|
-
function medianColour(img, rect) {
|
|
3841
|
-
const r = [];
|
|
3842
|
-
const g = [];
|
|
3843
|
-
const b = [];
|
|
3844
|
-
const x0 = Math.max(0, Math.floor(rect.x));
|
|
3845
|
-
const y0 = Math.max(0, Math.floor(rect.y));
|
|
3846
|
-
const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
|
|
3847
|
-
const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
|
|
3848
|
-
const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
|
|
3849
|
-
for (let y = y0; y < y1; y += step) {
|
|
3850
|
-
for (let x = x0; x < x1; x += step) {
|
|
3851
|
-
const p = at(img, x, y);
|
|
3852
|
-
r.push(p[0]);
|
|
3853
|
-
g.push(p[1]);
|
|
3854
|
-
b.push(p[2]);
|
|
3855
|
-
}
|
|
3856
|
-
}
|
|
3857
|
-
return [median(r), median(g), median(b)];
|
|
3858
|
-
}
|
|
3859
|
-
function edgeEnergy(img, width) {
|
|
3860
|
-
const cols = Math.max(2, Math.min(width, img.w));
|
|
3861
|
-
const rows = Math.max(2, Math.round(cols * img.h / img.w));
|
|
3862
|
-
const lum = [];
|
|
3863
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3864
|
-
for (let cx = 0; cx < cols; cx++) {
|
|
3865
|
-
const x = Math.min(img.w - 1, Math.floor((cx + 0.5) * img.w / cols));
|
|
3866
|
-
const y = Math.min(img.h - 1, Math.floor((cy + 0.5) * img.h / rows));
|
|
3867
|
-
const p = at(img, x, y);
|
|
3868
|
-
lum.push(0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]);
|
|
3869
|
-
}
|
|
3870
|
-
}
|
|
3871
|
-
let sum = 0;
|
|
3872
|
-
let n = 0;
|
|
3873
|
-
for (let cy = 0; cy < rows; cy++) {
|
|
3874
|
-
for (let cx = 1; cx < cols; cx++) {
|
|
3875
|
-
sum += Math.abs(lum[cy * cols + cx] - lum[cy * cols + cx - 1]);
|
|
3876
|
-
n++;
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
return n ? sum / n : 0;
|
|
3880
|
-
}
|
|
3881
|
-
function measureStill(img) {
|
|
3882
|
-
const g = groundColour(img);
|
|
3883
|
-
const strong = cardBounds(img, g, 60);
|
|
3884
|
-
const faint = cardBounds(img, g, 12) ?? strong;
|
|
3885
|
-
const card = faint;
|
|
3886
|
-
const bleed = [];
|
|
3887
|
-
let pad = null;
|
|
3888
|
-
let widthPct = null;
|
|
3889
|
-
let shadow = null;
|
|
3890
|
-
let edge = 0;
|
|
3891
|
-
let separation = null;
|
|
3892
|
-
if (card) {
|
|
3893
|
-
pad = {
|
|
3894
|
-
left: card.x / img.w,
|
|
3895
|
-
right: (img.w - card.x - card.w) / img.w,
|
|
3896
|
-
top: card.y / img.h,
|
|
3897
|
-
bottom: (img.h - card.y - card.h) / img.h
|
|
3898
|
-
};
|
|
3899
|
-
for (const side of ["left", "right", "top", "bottom"])
|
|
3900
|
-
if (pad[side] < 5e-3) bleed.push(side);
|
|
3901
|
-
widthPct = card.w / img.w;
|
|
3902
|
-
shadow = haloReading(img, g, card);
|
|
3903
|
-
edge = edgeContrast(img, g, card);
|
|
3904
|
-
const body = strong ?? card;
|
|
3905
|
-
const inner = {
|
|
3906
|
-
x: body.x + body.w * 0.1,
|
|
3907
|
-
y: body.y + body.h * 0.1,
|
|
3908
|
-
w: body.w * 0.8,
|
|
3909
|
-
h: body.h * 0.8
|
|
3910
|
-
};
|
|
3911
|
-
separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
|
|
3912
|
-
}
|
|
3913
|
-
const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
3914
|
-
return {
|
|
3915
|
-
w: img.w,
|
|
3916
|
-
h: img.h,
|
|
3917
|
-
ground: hex(g),
|
|
3918
|
-
card,
|
|
3919
|
-
widthPct,
|
|
3920
|
-
pad,
|
|
3921
|
-
bleed,
|
|
3922
|
-
shadow,
|
|
3923
|
-
edge,
|
|
3924
|
-
ink: inkCoverage(img, inkRect),
|
|
3925
|
-
separation,
|
|
3926
|
-
hash: differenceHash(img, inkRect)
|
|
3927
|
-
};
|
|
3928
|
-
}
|
|
3929
|
-
|
|
3930
|
-
// src/plugin/moments.ts
|
|
3931
|
-
var STEP_SETTLE_SECONDS = 0.4;
|
|
3932
|
-
var NOT_A_GESTURE = /* @__PURE__ */ new Set(["wait", "goto", "sleep"]);
|
|
3933
|
-
function momentCandidates(doc, duration) {
|
|
3934
|
-
const rated = ratedSegments3(doc);
|
|
3935
|
-
const out = [];
|
|
3936
|
-
const push = (c) => {
|
|
3937
|
-
if (c.time < 0 || c.time > duration) return;
|
|
3938
|
-
if (out.some((o) => Math.abs(o.time - c.time) < 0.25)) return;
|
|
3939
|
-
out.push(c);
|
|
3940
|
-
};
|
|
3941
|
-
const steps = doc.source.meta.steps ?? [];
|
|
3942
|
-
for (let i = 0; i < steps.length; i++) {
|
|
3943
|
-
const s = steps[i];
|
|
3944
|
-
if (s.skipped || NOT_A_GESTURE.has(s.do)) continue;
|
|
3945
|
-
const t = stepOutputTime(rated, s, settleAfter(steps, i));
|
|
3946
|
-
if (t === null) continue;
|
|
3947
|
-
push({ time: t, source: "step", step: s.id ?? s.step });
|
|
3948
|
-
}
|
|
3949
|
-
let dropped = 0;
|
|
3950
|
-
const apexes = [];
|
|
3951
|
-
for (const z of doc.zoom) {
|
|
3952
|
-
const ext = spanOutputExtent3(rated, z.in, z.out);
|
|
3953
|
-
if (ext) apexes.push((ext.start + ext.end) / 2);
|
|
3954
|
-
else dropped++;
|
|
3955
|
-
}
|
|
3956
|
-
for (const t of apexes.sort((a, b) => a - b))
|
|
3957
|
-
push({ time: t, source: "zoom" });
|
|
3958
|
-
if (!out.length) {
|
|
3959
|
-
for (const p of [0.1, 0.3, 0.5, 0.7, 0.9])
|
|
3960
|
-
push({ time: p * duration, source: "spread" });
|
|
3961
|
-
}
|
|
3962
|
-
return { candidates: out, dropped };
|
|
3963
|
-
}
|
|
3964
|
-
function settleAfter(steps, i) {
|
|
3965
|
-
const next = steps[i + 1];
|
|
3966
|
-
if (next && next.do === "wait" && !next.skipped)
|
|
3967
|
-
return Math.max(0, next.tEnd - steps[i].tEnd);
|
|
3968
|
-
return STEP_SETTLE_SECONDS;
|
|
3969
|
-
}
|
|
3970
|
-
function stepOutputTime(rated, step, settle) {
|
|
3971
|
-
const src = step.tEnd + settle;
|
|
3972
|
-
const ext = spanOutputExtent3(rated, src, src + 1e-3);
|
|
3973
|
-
if (!ext) return null;
|
|
3974
|
-
return ext.start;
|
|
3975
|
-
}
|
|
3976
|
-
function resolveStepTime(doc, entry) {
|
|
3977
|
-
const m = /^step:([^+-]+)([+-]\d+(?:\.\d+)?)?$/.exec(entry.trim());
|
|
3978
|
-
if (!m) return null;
|
|
3979
|
-
const key = m[1];
|
|
3980
|
-
const offset = m[2] ? Number(m[2]) : STEP_SETTLE_SECONDS;
|
|
3981
|
-
const steps = doc.source.meta.steps ?? [];
|
|
3982
|
-
const step = steps.find((s) => s.id === key) ?? (/^\d+$/.test(key) ? steps.find((s) => s.step === Number(key)) : void 0);
|
|
3983
|
-
if (!step) {
|
|
3984
|
-
const known = steps.map((s) => s.id ?? String(s.step)).filter((s) => s.length);
|
|
3985
|
-
throw new Error(
|
|
3986
|
-
`--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)"}`
|
|
3987
|
-
);
|
|
3988
|
-
}
|
|
3989
|
-
const t = stepOutputTime(ratedSegments3(doc), step, offset);
|
|
3990
|
-
if (t === null)
|
|
3991
|
-
throw new Error(
|
|
3992
|
-
`--times ${entry}: step "${key}" (${step.tEnd.toFixed(2)}s in the recording) falls outside the cut`
|
|
3993
|
-
);
|
|
3994
|
-
return t;
|
|
3995
|
-
}
|
|
3996
|
-
var DUPLICATE_BITS = 6;
|
|
3997
|
-
var POPULATED_INK = 0.12;
|
|
3998
|
-
function pickMoments(measured, opts = {}) {
|
|
3999
|
-
const minInk = opts.minInk ?? 0.06;
|
|
4000
|
-
const relative4 = opts.relativeInk ?? 0.4;
|
|
4001
|
-
const bits = opts.duplicateBits ?? DUPLICATE_BITS;
|
|
4002
|
-
const inks = measured.map((m) => m.ink).sort((a, b) => a - b);
|
|
4003
|
-
const median2 = inks[inks.length >> 1] ?? 0;
|
|
4004
|
-
const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative4));
|
|
4005
|
-
const kept = [];
|
|
4006
|
-
const dropped = [];
|
|
4007
|
-
for (const m of measured) {
|
|
4008
|
-
if (m.ink < floor) {
|
|
4009
|
-
dropped.push(
|
|
4010
|
-
`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)}%)`
|
|
4011
|
-
);
|
|
4012
|
-
continue;
|
|
4013
|
-
}
|
|
4014
|
-
const twin = kept.find((k) => hammingDistance(k.hash, m.hash) <= bits);
|
|
4015
|
-
if (twin) {
|
|
4016
|
-
dropped.push(
|
|
4017
|
-
`${m.time.toFixed(2)}s is the same frame as ${twin.time.toFixed(2)}s`
|
|
4018
|
-
);
|
|
4019
|
-
continue;
|
|
4020
|
-
}
|
|
4021
|
-
kept.push(m);
|
|
4022
|
-
}
|
|
4023
|
-
if (!kept.length && measured.length) {
|
|
4024
|
-
const best = [...measured].sort((a, b) => b.ink - a.ink)[0];
|
|
4025
|
-
kept.push(best);
|
|
4026
|
-
dropped.push(
|
|
4027
|
-
`every candidate is under the blank floor; kept ${best.time.toFixed(2)}s (${Math.round(best.ink * 100)}% ink) as the least empty`
|
|
4028
|
-
);
|
|
4029
|
-
}
|
|
4030
|
-
return { times: kept.map((k) => k.time), dropped };
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
// src/plugin/motionPlan.ts
|
|
4034
|
-
import {
|
|
4035
|
-
END_CARD_FROM,
|
|
4036
|
-
END_CARD_SECONDS,
|
|
4037
|
-
applyTemplate,
|
|
4038
|
-
docOutputDuration,
|
|
4039
|
-
dropTemplate,
|
|
4040
|
-
endCardClips,
|
|
4041
|
-
migrateMotion,
|
|
4042
|
-
ratedSegments as ratedSegments4,
|
|
4043
|
-
sameMedia,
|
|
4044
|
-
spanOutputExtent as spanOutputExtent4
|
|
4045
|
-
} from "@vosjs/studio-core";
|
|
4046
|
-
var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
|
|
4047
|
-
"x-feed-cut",
|
|
4048
|
-
"youtube-main-demo",
|
|
4049
|
-
"shorts-linkedin-vertical-cut"
|
|
4050
|
-
]);
|
|
4051
|
-
var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
4052
|
-
var BED_ID = "bed";
|
|
4053
|
-
var CLICK_ID_PREFIX = "click-";
|
|
4054
|
-
var CAPTION_ID_PREFIX = "caption-";
|
|
4055
|
-
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
4056
|
-
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4057
|
-
var TRANSITION_CLIP_MIN = 1;
|
|
4058
|
-
function proposeTransitions(input, kind) {
|
|
4059
|
-
const doc = structuredClone(input);
|
|
4060
|
-
const skipped = [];
|
|
4061
|
-
let count = 0;
|
|
4062
|
-
const steps = doc.source.meta.steps ?? [];
|
|
4063
|
-
for (const step of steps) {
|
|
4064
|
-
if (!step.navigated || step.skipped) continue;
|
|
4065
|
-
const name = step.id ?? String(step.step);
|
|
4066
|
-
const wait = step.do === "wait";
|
|
4067
|
-
const outEnd = round3(wait ? step.tStart : step.tEnd);
|
|
4068
|
-
const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
|
|
4069
|
-
const segs = doc.segments;
|
|
4070
|
-
if (segs.some(
|
|
4071
|
-
(s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
|
|
4072
|
-
))
|
|
4073
|
-
continue;
|
|
4074
|
-
const i = segs.findIndex(
|
|
4075
|
-
(s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
|
|
4076
|
-
);
|
|
4077
|
-
if (i < 0) {
|
|
4078
|
-
skipped.push(
|
|
4079
|
-
`no transition at step ${name}: the page change sits too close to a clip's edge`
|
|
4080
|
-
);
|
|
4081
|
-
continue;
|
|
4082
|
-
}
|
|
4083
|
-
const seg = segs[i];
|
|
4084
|
-
const before = { ...seg, out: outEnd, anim: { exit: kind } };
|
|
4085
|
-
const after = { ...seg, in: inStart, anim: { enter: kind } };
|
|
4086
|
-
if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
|
|
4087
|
-
if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
|
|
4088
|
-
doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
|
|
4089
|
-
count++;
|
|
4090
|
-
}
|
|
4091
|
-
return { doc, count, skipped };
|
|
4092
|
-
}
|
|
4093
|
-
function pickTrack(catalog, ask) {
|
|
4094
|
-
if (!catalog || !ask || off(ask)) return null;
|
|
4095
|
-
const want = ask.trim().toLowerCase();
|
|
4096
|
-
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
4097
|
-
}
|
|
4098
|
-
function clickTimes(doc, range) {
|
|
4099
|
-
const rated = ratedSegments4(doc);
|
|
4100
|
-
const out = [];
|
|
4101
|
-
for (const e of doc.source.cursor) {
|
|
4102
|
-
if (e.type !== "down") continue;
|
|
4103
|
-
const src = e.t / 1e3;
|
|
4104
|
-
const ext = spanOutputExtent4(rated, src, src + 1e-3);
|
|
4105
|
-
if (!ext) continue;
|
|
4106
|
-
const t = ext.start;
|
|
4107
|
-
if (t < range[0] || t > range[1]) continue;
|
|
4108
|
-
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
4109
|
-
out.push(+(t - range[0]).toFixed(3));
|
|
4110
|
-
}
|
|
4111
|
-
return out;
|
|
4112
|
-
}
|
|
4113
|
-
var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
|
|
4114
|
-
const rate = s.rate && s.rate > 0 ? s.rate : 1;
|
|
4115
|
-
return acc + (s.out - s.in) / rate;
|
|
4116
|
-
}, 0);
|
|
4117
|
-
function templateWords(words2) {
|
|
4118
|
-
const headline = (words2.headline ?? "").trim();
|
|
4119
|
-
const kicker = (words2.kicker ?? "").trim();
|
|
4120
|
-
const brand = (words2.brand ?? "").trim();
|
|
4121
|
-
const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
|
|
4122
|
-
const out = {};
|
|
4123
|
-
if (headline) {
|
|
4124
|
-
out["stage-title"] = headline;
|
|
4125
|
-
out["endcard-title"] = headline;
|
|
4126
|
-
}
|
|
4127
|
-
if (kicker) out["stage-kicker"] = kicker;
|
|
4128
|
-
if (brand) {
|
|
4129
|
-
out["stage-brand"] = brand;
|
|
4130
|
-
out["endcard-mark"] = brand;
|
|
4131
|
-
}
|
|
4132
|
-
if (sub && sub !== headline) out["endcard-sub"] = sub;
|
|
4133
|
-
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;
|
|
4134
3638
|
}
|
|
4135
3639
|
function resolveAnchor(doc, at2) {
|
|
4136
3640
|
if (typeof at2 !== "object") return at2;
|
|
@@ -4331,7 +3835,6 @@ function destinationMechanics(d, doc) {
|
|
|
4331
3835
|
const set = [];
|
|
4332
3836
|
const unset = [];
|
|
4333
3837
|
const notes = [];
|
|
4334
|
-
if (d.kind !== "video") return { set, unset, notes };
|
|
4335
3838
|
const loop = LOOP_DESTINATIONS.has(d.id);
|
|
4336
3839
|
const sound = SOUND_DESTINATIONS.has(d.id);
|
|
4337
3840
|
const portrait = d.px.w / d.px.h < 0.9;
|
|
@@ -4383,161 +3886,12 @@ function isLightHexGround(hex2) {
|
|
|
4383
3886
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
|
|
4384
3887
|
}
|
|
4385
3888
|
|
|
4386
|
-
// src/plugin/posterDoc.ts
|
|
4387
|
-
import { existsSync as existsSync4 } from "fs";
|
|
4388
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
4389
|
-
import { isAbsolute, join as join4, resolve as resolve3 } from "path";
|
|
4390
|
-
import {
|
|
4391
|
-
computeCardLayout,
|
|
4392
|
-
docStillTime as docStillTime2,
|
|
4393
|
-
migrateHostedDoc,
|
|
4394
|
-
overlayPlainText,
|
|
4395
|
-
overlayRect,
|
|
4396
|
-
resolveOverlayStyle as resolveOverlayStyle2
|
|
4397
|
-
} from "@vosjs/studio-core";
|
|
4398
|
-
var POSTER_CLASSES = [
|
|
4399
|
-
"landscape",
|
|
4400
|
-
"square",
|
|
4401
|
-
"portrait",
|
|
4402
|
-
"tile"
|
|
4403
|
-
];
|
|
4404
|
-
var TILE_MAX_PX = 700;
|
|
4405
|
-
function posterClassFor(px) {
|
|
4406
|
-
if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
|
|
4407
|
-
const r = px.w / Math.max(1, px.h);
|
|
4408
|
-
if (r < 0.87) return "portrait";
|
|
4409
|
-
if (r <= 1.15) return "square";
|
|
4410
|
-
return "landscape";
|
|
4411
|
-
}
|
|
4412
|
-
var ROLE_ALL = "poster";
|
|
4413
|
-
var roleFor = (cls) => `poster-${cls}`;
|
|
4414
|
-
async function readPosterAt(cls, path, takeDir, from) {
|
|
4415
|
-
let file = path;
|
|
4416
|
-
let docDir = path;
|
|
4417
|
-
if (existsSync4(join4(path, "doc.json"))) {
|
|
4418
|
-
file = join4(path, "doc.json");
|
|
4419
|
-
} else if (!/\.json$/i.test(path) || !existsSync4(path)) {
|
|
4420
|
-
return null;
|
|
4421
|
-
} else {
|
|
4422
|
-
docDir = resolve3(path, "..");
|
|
4423
|
-
}
|
|
4424
|
-
let raw;
|
|
4425
|
-
try {
|
|
4426
|
-
raw = JSON.parse(await readFile4(file, "utf8"));
|
|
4427
|
-
} catch {
|
|
4428
|
-
return null;
|
|
4429
|
-
}
|
|
4430
|
-
if (!raw || typeof raw !== "object") return null;
|
|
4431
|
-
const source = raw.source;
|
|
4432
|
-
if (!source || typeof source.videoKey !== "string") return null;
|
|
4433
|
-
const doc = migrateHostedDoc(
|
|
4434
|
-
raw
|
|
4435
|
-
);
|
|
4436
|
-
const ownFootage = existsSync4(join4(docDir, "meta.json")) && existsSync4(join4(docDir, source.videoKey));
|
|
4437
|
-
const sync = readSyncState(docDir);
|
|
4438
|
-
return {
|
|
4439
|
-
cls,
|
|
4440
|
-
file,
|
|
4441
|
-
takeDir: ownFootage ? docDir : takeDir,
|
|
4442
|
-
ownFootage,
|
|
4443
|
-
doc,
|
|
4444
|
-
...sync?.vosId ? { vosId: sync.vosId } : {},
|
|
4445
|
-
from
|
|
4446
|
-
};
|
|
4447
|
-
}
|
|
4448
|
-
async function findPosterDocs(takeDir, launchRoles) {
|
|
4449
|
-
const out = {};
|
|
4450
|
-
const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
|
|
4451
|
-
for (const cls of POSTER_CLASSES) {
|
|
4452
|
-
const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
|
|
4453
|
-
const candidates = [];
|
|
4454
|
-
if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
|
|
4455
|
-
const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
|
|
4456
|
-
candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
|
|
4457
|
-
}
|
|
4458
|
-
candidates.push({
|
|
4459
|
-
path: join4(takeDir, "poster", cls),
|
|
4460
|
-
from: `poster/${cls}`
|
|
4461
|
-
});
|
|
4462
|
-
candidates.push({ path: join4(takeDir, "poster"), from: "poster" });
|
|
4463
|
-
for (const c of candidates) {
|
|
4464
|
-
const ref = await readPosterAt(cls, c.path, takeDir, c.from);
|
|
4465
|
-
if (ref) {
|
|
4466
|
-
out[cls] = ref;
|
|
4467
|
-
break;
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
}
|
|
4471
|
-
return out;
|
|
4472
|
-
}
|
|
4473
|
-
function posterStillTime(doc, duration) {
|
|
4474
|
-
return Math.max(0, Math.min(duration, docStillTime2(doc)));
|
|
4475
|
-
}
|
|
4476
|
-
var DESIGN_H = 1080;
|
|
4477
|
-
function designFrame(px) {
|
|
4478
|
-
return {
|
|
4479
|
-
W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
|
|
4480
|
-
H: DESIGN_H
|
|
4481
|
-
};
|
|
4482
|
-
}
|
|
4483
|
-
function posterShotRect(doc, px) {
|
|
4484
|
-
const { W, H } = designFrame(px);
|
|
4485
|
-
const meta = doc.source.meta;
|
|
4486
|
-
const layout = computeCardLayout(
|
|
4487
|
-
doc.frame,
|
|
4488
|
-
{
|
|
4489
|
-
width: meta.captureWidth ?? meta.width,
|
|
4490
|
-
height: meta.captureHeight ?? meta.height
|
|
4491
|
-
},
|
|
4492
|
-
W,
|
|
4493
|
-
H
|
|
4494
|
-
);
|
|
4495
|
-
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4496
|
-
return {
|
|
4497
|
-
x: r3(layout.cardX / W),
|
|
4498
|
-
y: r3(layout.cardY / H),
|
|
4499
|
-
w: r3(layout.cardW / W),
|
|
4500
|
-
h: r3(layout.cardH / H)
|
|
4501
|
-
};
|
|
4502
|
-
}
|
|
4503
|
-
function estimateWidth(text, font, letterSpacingPx = 0) {
|
|
4504
|
-
const m = /(\d+(?:\.\d+)?)px/.exec(font);
|
|
4505
|
-
const px = m ? Number(m[1]) : 32;
|
|
4506
|
-
return text.length * (px * 0.55 + letterSpacingPx);
|
|
4507
|
-
}
|
|
4508
|
-
function posterTextBoxes(doc, px, time) {
|
|
4509
|
-
const { W, H } = designFrame(px);
|
|
4510
|
-
const out = [];
|
|
4511
|
-
for (const clip of doc.overlays ?? []) {
|
|
4512
|
-
if (clip.kind !== "text") continue;
|
|
4513
|
-
if (time < clip.start || time > clip.start + clip.duration) continue;
|
|
4514
|
-
const rect = overlayRect(clip, estimateWidth, W, H);
|
|
4515
|
-
const style = resolveOverlayStyle2(clip);
|
|
4516
|
-
const r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4517
|
-
out.push({
|
|
4518
|
-
x: r3((rect.cx - rect.w / 2) / W),
|
|
4519
|
-
y: r3((rect.cy - rect.h / 2) / H),
|
|
4520
|
-
w: r3(rect.w / W),
|
|
4521
|
-
h: r3(rect.h / H),
|
|
4522
|
-
role: clip.preset === "title" ? "headline" : "body",
|
|
4523
|
-
label: overlayPlainText(clip).split("\n")[0].slice(0, 40),
|
|
4524
|
-
.../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
|
|
4525
|
-
});
|
|
4526
|
-
}
|
|
4527
|
-
return out;
|
|
4528
|
-
}
|
|
4529
|
-
|
|
4530
3889
|
// src/plugin/deliver.ts
|
|
4531
3890
|
var CHANNEL_ALIASES = {
|
|
4532
|
-
ph: "producthunt",
|
|
4533
3891
|
yt: "youtube",
|
|
4534
3892
|
gh: "github",
|
|
4535
|
-
li: "linkedin",
|
|
4536
3893
|
shorts: "shorts-linkedin"
|
|
4537
3894
|
};
|
|
4538
|
-
var NOT_FROM_FOOTAGE = {
|
|
4539
|
-
"cws-icon": "an icon is brand art, not footage \u2014 supply it from the kit"
|
|
4540
|
-
};
|
|
4541
3895
|
function resolveChannels(raw) {
|
|
4542
3896
|
const known = [...new Set(DESTINATIONS.map((d) => d.channel))];
|
|
4543
3897
|
const wanted = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
@@ -4548,26 +3902,26 @@ function resolveChannels(raw) {
|
|
|
4548
3902
|
const id = CHANNEL_ALIASES[w] ?? w;
|
|
4549
3903
|
if (!known.includes(id))
|
|
4550
3904
|
throw new Error(
|
|
4551
|
-
`unknown channel "${w}" \u2014
|
|
3905
|
+
`unknown channel "${w}" \u2014 deliver renders video cuts for: ${known.join(", ")} (or all)`
|
|
4552
3906
|
);
|
|
4553
3907
|
if (!out.includes(id)) out.push(id);
|
|
4554
3908
|
}
|
|
4555
3909
|
return out;
|
|
4556
3910
|
}
|
|
4557
3911
|
async function readBrandBesideTake(dir, explicit) {
|
|
4558
|
-
const candidates = explicit ? [explicit] : [
|
|
3912
|
+
const candidates = explicit ? [explicit] : [join4(dir, "BRAND.md"), join4(dir, "..", "BRAND.md")];
|
|
4559
3913
|
for (const file of candidates) {
|
|
4560
|
-
if (!
|
|
4561
|
-
const roles = parseFrontmatter(await
|
|
3914
|
+
if (!existsSync4(file)) continue;
|
|
3915
|
+
const roles = parseFrontmatter(await readFile4(file, "utf8"));
|
|
4562
3916
|
return { file, roles };
|
|
4563
3917
|
}
|
|
4564
3918
|
return null;
|
|
4565
3919
|
}
|
|
4566
3920
|
async function readLaunchBesideTake(dir, explicit) {
|
|
4567
|
-
const candidates = explicit ? [explicit] : [
|
|
3921
|
+
const candidates = explicit ? [explicit] : [join4(dir, "LAUNCH.md"), join4(dir, "..", "LAUNCH.md")];
|
|
4568
3922
|
for (const file of candidates) {
|
|
4569
|
-
if (!
|
|
4570
|
-
return { file, roles: parseFrontmatter(await
|
|
3923
|
+
if (!existsSync4(file)) continue;
|
|
3924
|
+
return { file, roles: parseFrontmatter(await readFile4(file, "utf8")) };
|
|
4571
3925
|
}
|
|
4572
3926
|
return null;
|
|
4573
3927
|
}
|
|
@@ -4592,7 +3946,7 @@ async function resolveLook(dir, opts) {
|
|
|
4592
3946
|
roles
|
|
4593
3947
|
};
|
|
4594
3948
|
}
|
|
4595
|
-
function lookOverrides(look, placement, size, video, opts) {
|
|
3949
|
+
function lookOverrides(look, placement, size, video, opts = {}) {
|
|
4596
3950
|
const inset = cardInset(look, size, video, placement);
|
|
4597
3951
|
const set = [
|
|
4598
3952
|
"frame.fit=contain",
|
|
@@ -4609,70 +3963,8 @@ function lookOverrides(look, placement, size, video, opts) {
|
|
|
4609
3963
|
set.push(`frame.borderColor=${look.borderColor ?? "#000000"}`);
|
|
4610
3964
|
}
|
|
4611
3965
|
if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
|
|
4612
|
-
if (opts.still) {
|
|
4613
|
-
set.push(
|
|
4614
|
-
"zoom=[]",
|
|
4615
|
-
"tilt=[]",
|
|
4616
|
-
"cursor.visible=false",
|
|
4617
|
-
"cursor.clickFx.style=none"
|
|
4618
|
-
);
|
|
4619
|
-
}
|
|
4620
3966
|
return set;
|
|
4621
3967
|
}
|
|
4622
|
-
var PROBE_WIDTH = 640;
|
|
4623
|
-
async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
4624
|
-
const { candidates, dropped } = momentCandidates(doc, duration);
|
|
4625
|
-
const notes = [];
|
|
4626
|
-
if (dropped > 0)
|
|
4627
|
-
notes.push(
|
|
4628
|
-
`${dropped} of ${doc.zoom.length} zoom apex(es) fall outside the cut`
|
|
4629
|
-
);
|
|
4630
|
-
if (!candidates.length) return { times: [], moments: [], notes };
|
|
4631
|
-
const meta = doc.source.meta;
|
|
4632
|
-
const vw = meta.captureWidth ?? meta.width;
|
|
4633
|
-
const vh = meta.captureHeight ?? meta.height;
|
|
4634
|
-
const probeDir = await mkdtemp(join5(tmpdir(), "vos-moments-"));
|
|
4635
|
-
try {
|
|
4636
|
-
const probe3 = await framesTake(browser, dir, {
|
|
4637
|
-
times: candidates.map((c) => c.time),
|
|
4638
|
-
width: PROBE_WIDTH,
|
|
4639
|
-
height: Math.max(2, Math.round(PROBE_WIDTH * vh / vw / 2) * 2),
|
|
4640
|
-
outDir: probeDir,
|
|
4641
|
-
overrides: {
|
|
4642
|
-
...opts.overrides,
|
|
4643
|
-
set: [...SCREENSHOT_DEFAULTS, ...opts.overrides?.set ?? []]
|
|
4644
|
-
}
|
|
4645
|
-
});
|
|
4646
|
-
const byTime = /* @__PURE__ */ new Map();
|
|
4647
|
-
for (const frame of probe3.frames) {
|
|
4648
|
-
const img = decodePng(new Uint8Array(await readFile5(frame.file)));
|
|
4649
|
-
if (!img) continue;
|
|
4650
|
-
const whole = { x: 0, y: 0, w: img.w, h: img.h };
|
|
4651
|
-
byTime.set(+frame.time.toFixed(3), {
|
|
4652
|
-
ink: inkCoverage(img, whole),
|
|
4653
|
-
hash: differenceHash(img, whole)
|
|
4654
|
-
});
|
|
4655
|
-
}
|
|
4656
|
-
const measured = [];
|
|
4657
|
-
for (const c of candidates) {
|
|
4658
|
-
const m = byTime.get(+c.time.toFixed(3));
|
|
4659
|
-
if (m) measured.push({ time: c.time, ...m });
|
|
4660
|
-
}
|
|
4661
|
-
const pick = pickMoments(measured);
|
|
4662
|
-
const kept = candidates.filter((c) => pick.times.includes(c.time));
|
|
4663
|
-
const bySource = (s) => candidates.filter((c) => c.source === s).length;
|
|
4664
|
-
notes.push(
|
|
4665
|
-
`${candidates.length} candidate(s): ${bySource("step")} from steps, ${bySource("zoom")} from zoom apexes, ${bySource("spread")} from the spread; ${kept.length} kept`
|
|
4666
|
-
);
|
|
4667
|
-
return {
|
|
4668
|
-
times: pick.times,
|
|
4669
|
-
moments: kept,
|
|
4670
|
-
notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
|
|
4671
|
-
};
|
|
4672
|
-
} finally {
|
|
4673
|
-
await rm2(probeDir, { recursive: true, force: true });
|
|
4674
|
-
}
|
|
4675
|
-
}
|
|
4676
3968
|
var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
|
|
4677
3969
|
var specWords = (d) => {
|
|
4678
3970
|
const parts = [`${d.px.w}x${d.px.h}`];
|
|
@@ -4682,62 +3974,17 @@ var specWords = (d) => {
|
|
|
4682
3974
|
parts.push(`\u2264${(d.maxBytes / 1024 / 1024).toFixed(0)}MB`);
|
|
4683
3975
|
return parts.join(" ");
|
|
4684
3976
|
};
|
|
4685
|
-
var FULL_BLEED = [
|
|
4686
|
-
"frame.padding=0",
|
|
4687
|
-
"frame.radius=0",
|
|
4688
|
-
"frame.shadow=0",
|
|
4689
|
-
"frame.border=0",
|
|
4690
|
-
"frame.browserBar.kind=none",
|
|
4691
|
-
"cursor.visible=false",
|
|
4692
|
-
"cursor.clickFx.style=none"
|
|
4693
|
-
];
|
|
4694
|
-
var SCREENSHOT_DEFAULTS = [...FULL_BLEED, "zoom=[]", "tilt=[]"];
|
|
4695
|
-
function stillOverridesFor(d, opts, video) {
|
|
4696
|
-
const set = [];
|
|
4697
|
-
if (d.genre === "screenshot" && !opts.composed) {
|
|
4698
|
-
if (d.fit === "cover") set.push("frame.fit=cover");
|
|
4699
|
-
set.push(...SCREENSHOT_DEFAULTS);
|
|
4700
|
-
} else if (d.genre === "card" && !opts.composed && opts.look && video && d.px) {
|
|
4701
|
-
set.push(
|
|
4702
|
-
...lookOverrides(opts.look, "card", d.px, video, {
|
|
4703
|
-
still: true,
|
|
4704
|
-
keepMedia: opts.overrides?.background !== void 0
|
|
4705
|
-
})
|
|
4706
|
-
);
|
|
4707
|
-
} else {
|
|
4708
|
-
if (d.fit === "cover") set.push("frame.fit=cover");
|
|
4709
|
-
if (d.genre === "card" && !opts.composed) set.push(...FULL_BLEED);
|
|
4710
|
-
}
|
|
4711
|
-
set.push(...opts.overrides?.set ?? []);
|
|
4712
|
-
if (!set.length) return opts.overrides;
|
|
4713
|
-
return { ...opts.overrides, set };
|
|
4714
|
-
}
|
|
4715
3977
|
async function deliverTake(browser, dir, opts) {
|
|
4716
3978
|
const take = await loadTake(dir);
|
|
4717
3979
|
if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
|
|
4718
3980
|
const doc = take.doc;
|
|
4719
3981
|
const duration = docOutputDuration2(doc);
|
|
4720
3982
|
const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
|
|
4721
|
-
const outDir =
|
|
3983
|
+
const outDir = resolve3(opts.outDir ?? join4(dir, "kit"));
|
|
4722
3984
|
await mkdir3(outDir, { recursive: true });
|
|
4723
3985
|
const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
|
|
4724
3986
|
const assets = [];
|
|
4725
3987
|
const skipped = [];
|
|
4726
|
-
let stillTimes;
|
|
4727
|
-
let moments;
|
|
4728
|
-
if (opts.times?.length) {
|
|
4729
|
-
stillTimes = opts.times;
|
|
4730
|
-
moments = opts.times.map((time) => ({ time, source: "times" }));
|
|
4731
|
-
} else {
|
|
4732
|
-
opts.onPhase?.("moments (the step timeline, the zoom apexes, the spread)");
|
|
4733
|
-
const picked = await pickStillTimes(browser, dir, doc, duration, opts);
|
|
4734
|
-
stillTimes = picked.times;
|
|
4735
|
-
moments = picked.moments;
|
|
4736
|
-
for (const n of picked.notes) {
|
|
4737
|
-
if (n.startsWith("moment: ")) skipped.push(n);
|
|
4738
|
-
else opts.onPhase?.(`note: ${n}`);
|
|
4739
|
-
}
|
|
4740
|
-
}
|
|
4741
3988
|
const meta0 = doc.source.meta;
|
|
4742
3989
|
const video = {
|
|
4743
3990
|
w: meta0.captureWidth ?? meta0.width,
|
|
@@ -4748,7 +3995,6 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4748
3995
|
if (opts.look) {
|
|
4749
3996
|
set.push(
|
|
4750
3997
|
...lookOverrides(opts.look, "hero", d.px, video, {
|
|
4751
|
-
still: false,
|
|
4752
3998
|
keepMedia: opts.overrides?.background !== void 0
|
|
4753
3999
|
})
|
|
4754
4000
|
);
|
|
@@ -4760,181 +4006,59 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4760
4006
|
if (!set.length && !mech.unset.length) return opts.overrides;
|
|
4761
4007
|
return { ...opts.overrides, set, unset: mech.unset };
|
|
4762
4008
|
};
|
|
4763
|
-
const posters = await findPosterDocs(dir, opts.launchRoles);
|
|
4764
|
-
const posterCardIds = /* @__PURE__ */ new Set();
|
|
4765
|
-
const missing = /* @__PURE__ */ new Set();
|
|
4766
|
-
for (const d of destinations) {
|
|
4767
|
-
if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
|
|
4768
|
-
continue;
|
|
4769
|
-
const cls = posterClassFor(d.px);
|
|
4770
|
-
const ref = posters[cls];
|
|
4771
|
-
if (!ref) {
|
|
4772
|
-
missing.add(cls);
|
|
4773
|
-
continue;
|
|
4774
|
-
}
|
|
4775
|
-
posterCardIds.add(d.id);
|
|
4776
|
-
const label = `${d.channel} ${d.asset}`;
|
|
4777
|
-
const posterDuration = docOutputDuration2(ref.doc);
|
|
4778
|
-
const time = posterStillTime(ref.doc, posterDuration);
|
|
4779
|
-
opts.onPhase?.(
|
|
4780
|
-
`${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
|
|
4781
|
-
);
|
|
4782
|
-
const shotDir = await mkdtemp(join5(tmpdir(), "vos-poster-"));
|
|
4783
|
-
try {
|
|
4784
|
-
const captured = await framesTake(browser, ref.takeDir, {
|
|
4785
|
-
times: [time],
|
|
4786
|
-
width: d.px.w,
|
|
4787
|
-
height: d.px.h,
|
|
4788
|
-
outDir: shotDir,
|
|
4789
|
-
doc: ref.doc,
|
|
4790
|
-
overrides: opts.overrides
|
|
4791
|
-
});
|
|
4792
|
-
const to = join5(outDir, `${d.id}.png`);
|
|
4793
|
-
await rename3(captured.frames[0].file, to);
|
|
4794
|
-
const bytes = (await stat(to)).size;
|
|
4795
|
-
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4796
|
-
skipped.push(
|
|
4797
|
-
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
4798
|
-
);
|
|
4799
|
-
continue;
|
|
4800
|
-
}
|
|
4801
|
-
const shot = posterShotRect(ref.doc, d.px);
|
|
4802
|
-
assets.push({
|
|
4803
|
-
channel: d.channel,
|
|
4804
|
-
asset: d.asset,
|
|
4805
|
-
destination: d.id,
|
|
4806
|
-
path: relative(outDir, to),
|
|
4807
|
-
w: d.px.w,
|
|
4808
|
-
h: d.px.h,
|
|
4809
|
-
bytes,
|
|
4810
|
-
seconds: null,
|
|
4811
|
-
frameTime: time,
|
|
4812
|
-
source: "poster",
|
|
4813
|
-
poster: {
|
|
4814
|
-
class: cls,
|
|
4815
|
-
file: relative(dir, ref.file),
|
|
4816
|
-
...ref.vosId ? { vosId: ref.vosId } : {}
|
|
4817
|
-
},
|
|
4818
|
-
text: posterTextBoxes(ref.doc, d.px, time),
|
|
4819
|
-
shot,
|
|
4820
|
-
...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
|
|
4821
|
-
});
|
|
4822
|
-
} finally {
|
|
4823
|
-
await rm2(shotDir, { recursive: true, force: true });
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
4826
|
-
if (missing.size) {
|
|
4827
|
-
const classes = [...missing].sort().join(", ");
|
|
4828
|
-
skipped.push(
|
|
4829
|
-
`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`
|
|
4830
|
-
);
|
|
4831
|
-
}
|
|
4832
4009
|
for (const d of destinations) {
|
|
4833
4010
|
const label = `${d.channel} ${d.asset}`;
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
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
|
+
);
|
|
4837
4015
|
continue;
|
|
4838
4016
|
}
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
|
|
4850
|
-
if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
|
|
4851
|
-
range = [0, d.maxSeconds];
|
|
4852
|
-
seconds = d.maxSeconds;
|
|
4853
|
-
opts.onPhase?.(
|
|
4854
|
-
`note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
|
|
4855
|
-
);
|
|
4856
|
-
} else {
|
|
4857
|
-
skipped.push(
|
|
4858
|
-
`${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
|
|
4859
|
-
);
|
|
4860
|
-
continue;
|
|
4861
|
-
}
|
|
4862
|
-
}
|
|
4863
|
-
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
4864
|
-
const outFile = join5(outDir, `${d.id}.${d.format}`);
|
|
4865
|
-
const bitrate = d.maxBytes !== void 0 ? Math.min(
|
|
4866
|
-
1e7,
|
|
4867
|
-
Math.floor(d.maxBytes * 8 / seconds * 0.85)
|
|
4868
|
-
) : void 0;
|
|
4869
|
-
const result = await renderTake(browser, dir, outFile, {
|
|
4870
|
-
width: d.px.w,
|
|
4871
|
-
height: d.px.h,
|
|
4872
|
-
format: "mp4",
|
|
4873
|
-
parallel: opts.parallel,
|
|
4874
|
-
range,
|
|
4875
|
-
bitrate,
|
|
4876
|
-
overrides: videoOverrides(d),
|
|
4877
|
-
onProgress: opts.onProgress
|
|
4878
|
-
});
|
|
4879
|
-
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 {
|
|
4880
4027
|
skipped.push(
|
|
4881
|
-
`${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)`
|
|
4882
4029
|
);
|
|
4883
4030
|
continue;
|
|
4884
4031
|
}
|
|
4885
|
-
assets.push({
|
|
4886
|
-
channel: d.channel,
|
|
4887
|
-
asset: d.asset,
|
|
4888
|
-
destination: d.id,
|
|
4889
|
-
path: relative(outDir, outFile),
|
|
4890
|
-
w: result.width,
|
|
4891
|
-
h: result.height,
|
|
4892
|
-
bytes: result.bytes,
|
|
4893
|
-
seconds: result.duration,
|
|
4894
|
-
frameTime: null
|
|
4895
|
-
});
|
|
4896
|
-
continue;
|
|
4897
4032
|
}
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
const
|
|
4901
|
-
const
|
|
4902
|
-
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, {
|
|
4903
4037
|
width: d.px.w,
|
|
4904
4038
|
height: d.px.h,
|
|
4905
|
-
|
|
4906
|
-
|
|
4039
|
+
format: "mp4",
|
|
4040
|
+
parallel: opts.parallel,
|
|
4041
|
+
range,
|
|
4042
|
+
bitrate,
|
|
4043
|
+
overrides: videoOverrides(d),
|
|
4044
|
+
onProgress: opts.onProgress
|
|
4907
4045
|
});
|
|
4908
|
-
|
|
4909
|
-
const frame = captured.frames[i];
|
|
4910
|
-
const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
|
|
4911
|
-
const to = join5(outDir, name);
|
|
4912
|
-
await rename3(frame.file, to);
|
|
4913
|
-
const bytes = (await stat(to)).size;
|
|
4914
|
-
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4915
|
-
skipped.push(
|
|
4916
|
-
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
4917
|
-
);
|
|
4918
|
-
continue;
|
|
4919
|
-
}
|
|
4920
|
-
assets.push({
|
|
4921
|
-
channel: d.channel,
|
|
4922
|
-
asset: d.asset,
|
|
4923
|
-
destination: d.id,
|
|
4924
|
-
path: relative(outDir, to),
|
|
4925
|
-
w: captured.width,
|
|
4926
|
-
h: captured.height,
|
|
4927
|
-
bytes,
|
|
4928
|
-
seconds: null,
|
|
4929
|
-
frameTime: frame.time,
|
|
4930
|
-
...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
|
|
4931
|
-
});
|
|
4932
|
-
}
|
|
4933
|
-
if (d.count && captured.frames.length < d.count.min) {
|
|
4046
|
+
if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
|
|
4934
4047
|
skipped.push(
|
|
4935
|
-
`${label}:
|
|
4048
|
+
`${label}: ${overCeiling(result.bytes, d.maxBytes)} (kept at ${outFile})`
|
|
4936
4049
|
);
|
|
4050
|
+
continue;
|
|
4937
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
|
+
});
|
|
4938
4062
|
}
|
|
4939
4063
|
const kit = {
|
|
4940
4064
|
release: opts.release ?? null,
|
|
@@ -4942,19 +4066,18 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4942
4066
|
take: dir,
|
|
4943
4067
|
produced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4944
4068
|
specsVerified: CHANNEL_SPECS_VERIFIED,
|
|
4945
|
-
moments,
|
|
4946
4069
|
skipped,
|
|
4947
4070
|
assets
|
|
4948
4071
|
};
|
|
4949
|
-
const kitFile =
|
|
4072
|
+
const kitFile = join4(outDir, "kit.json");
|
|
4950
4073
|
await writeFile5(kitFile, JSON.stringify(kit, null, 2));
|
|
4951
4074
|
return { kit, kitFile, outDir };
|
|
4952
4075
|
}
|
|
4953
4076
|
|
|
4954
4077
|
// src/plugin/markAsset.ts
|
|
4955
|
-
import { mkdir as mkdir4, readFile as
|
|
4956
|
-
import { existsSync as
|
|
4957
|
-
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";
|
|
4958
4081
|
function markExtension(url) {
|
|
4959
4082
|
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4960
4083
|
if (!m) return null;
|
|
@@ -5006,418 +4129,493 @@ function imageAspect(bytes, ext) {
|
|
|
5006
4129
|
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
5007
4130
|
return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
|
|
5008
4131
|
}
|
|
5009
|
-
i += 2 + len;
|
|
4132
|
+
i += 2 + len;
|
|
4133
|
+
}
|
|
4134
|
+
return null;
|
|
4135
|
+
}
|
|
4136
|
+
return null;
|
|
4137
|
+
}
|
|
4138
|
+
function ratio(w, h) {
|
|
4139
|
+
return w > 0 && h > 0 ? w / h : null;
|
|
4140
|
+
}
|
|
4141
|
+
async function fetchBrandMarks(takeDir, roles) {
|
|
4142
|
+
const out = { light: null, dark: null, notes: [] };
|
|
4143
|
+
if (!roles) return out;
|
|
4144
|
+
const one = async (role, name) => {
|
|
4145
|
+
const src = (roles[role] ?? "").trim();
|
|
4146
|
+
if (!src) return null;
|
|
4147
|
+
const ext = markExtension(src);
|
|
4148
|
+
if (!ext) {
|
|
4149
|
+
out.notes.push(
|
|
4150
|
+
`${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
|
|
4151
|
+
);
|
|
4152
|
+
return null;
|
|
4153
|
+
}
|
|
4154
|
+
let bytes;
|
|
4155
|
+
try {
|
|
4156
|
+
if (/^https?:/i.test(src)) {
|
|
4157
|
+
const res = await fetch(src);
|
|
4158
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
4159
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
4160
|
+
} else {
|
|
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
|
+
}
|
|
5010
4379
|
}
|
|
5011
|
-
return null;
|
|
5012
4380
|
}
|
|
5013
|
-
return
|
|
5014
|
-
}
|
|
5015
|
-
function ratio(w, h) {
|
|
5016
|
-
return w > 0 && h > 0 ? w / h : null;
|
|
4381
|
+
return [median(r), median(g), median(b)];
|
|
5017
4382
|
}
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
)
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
if (/^https?:/i.test(src)) {
|
|
5034
|
-
const res = await fetch(src);
|
|
5035
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
5036
|
-
bytes = new Uint8Array(await res.arrayBuffer());
|
|
5037
|
-
} else {
|
|
5038
|
-
const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
|
|
5039
|
-
if (!existsSync6(file)) throw new Error("no such file");
|
|
5040
|
-
bytes = new Uint8Array(await readFile6(file));
|
|
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;
|
|
5041
4398
|
}
|
|
5042
|
-
} catch (e) {
|
|
5043
|
-
out.notes.push(
|
|
5044
|
-
`${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
|
|
5045
|
-
);
|
|
5046
|
-
return null;
|
|
5047
4399
|
}
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
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++;
|
|
4415
|
+
}
|
|
5054
4416
|
}
|
|
5055
|
-
|
|
5056
|
-
await writeFile6(join6(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
5057
|
-
return { key: `/brand/${name}.${ext}`, aspect };
|
|
4417
|
+
return n ? sum / n : 0;
|
|
5058
4418
|
};
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
}
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
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
|
+
);
|
|
5075
4435
|
}
|
|
5076
|
-
|
|
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;
|
|
5077
4459
|
}
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
const
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
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);
|
|
5088
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;
|
|
5089
4476
|
}
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
const
|
|
5101
|
-
for (let
|
|
5102
|
-
let
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
let c = 4294967295;
|
|
5110
|
-
for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
|
|
5111
|
-
return (c ^ 4294967295) >>> 0;
|
|
5112
|
-
}
|
|
5113
|
-
function chunk(type, body) {
|
|
5114
|
-
const out = new Uint8Array(12 + body.length);
|
|
5115
|
-
const dv = new DataView(out.buffer);
|
|
5116
|
-
dv.setUint32(0, body.length);
|
|
5117
|
-
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
5118
|
-
out.set(body, 8);
|
|
5119
|
-
dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
|
|
5120
|
-
return out;
|
|
5121
|
-
}
|
|
5122
|
-
function encodePng(img) {
|
|
5123
|
-
const stride = img.w * 4;
|
|
5124
|
-
const raw = new Uint8Array((stride + 1) * img.h);
|
|
5125
|
-
for (let y = 0; y < img.h; y++) {
|
|
5126
|
-
raw[y * (stride + 1)] = 2;
|
|
5127
|
-
for (let i = 0; i < stride; i++) {
|
|
5128
|
-
const cur = img.data[y * stride + i];
|
|
5129
|
-
const up = y ? img.data[(y - 1) * stride + i] : 0;
|
|
5130
|
-
raw[y * (stride + 1) + 1 + i] = cur - up & 255;
|
|
5131
|
-
}
|
|
5132
|
-
}
|
|
5133
|
-
const ihdr = new Uint8Array(13);
|
|
5134
|
-
const dv = new DataView(ihdr.buffer);
|
|
5135
|
-
dv.setUint32(0, img.w);
|
|
5136
|
-
dv.setUint32(4, img.h);
|
|
5137
|
-
ihdr[8] = 8;
|
|
5138
|
-
ihdr[9] = 6;
|
|
5139
|
-
const parts = [
|
|
5140
|
-
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
5141
|
-
chunk("IHDR", ihdr),
|
|
5142
|
-
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
5143
|
-
chunk("IEND", new Uint8Array(0))
|
|
5144
|
-
];
|
|
5145
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
5146
|
-
let o = 0;
|
|
5147
|
-
for (const p of parts) {
|
|
5148
|
-
out.set(p, o);
|
|
5149
|
-
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
|
+
}
|
|
5150
4496
|
}
|
|
5151
|
-
|
|
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;
|
|
5152
4501
|
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
"The poster test: would you post this frame as a still, alone?",
|
|
5166
|
-
"The thumbnail test: does it read at half size?",
|
|
5167
|
-
"Name three ways this asset acknowledges THIS product (its data, its state, its brand), not a generic window."
|
|
5168
|
-
];
|
|
5169
|
-
function rolesFor(asset) {
|
|
5170
|
-
if ((asset.source === "poster" || asset.source === "stage") && asset.template)
|
|
5171
|
-
return [asset.template.replace(/-stage$/, "")];
|
|
5172
|
-
const spec = DESTINATIONS2.find((d) => d.id === asset.destination);
|
|
5173
|
-
if (spec?.kind === "video") {
|
|
5174
|
-
const portrait = spec.px.w < spec.px.h;
|
|
5175
|
-
return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
|
|
5176
|
-
}
|
|
5177
|
-
if (spec?.genre === "card")
|
|
5178
|
-
return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
|
|
5179
|
-
if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
|
|
5180
|
-
return [];
|
|
5181
|
-
}
|
|
5182
|
-
function resample(img, w, h) {
|
|
5183
|
-
const out = new Uint8Array(w * h * 4);
|
|
5184
|
-
const sx = img.w / w;
|
|
5185
|
-
const sy = img.h / h;
|
|
5186
|
-
for (let y = 0; y < h; y++) {
|
|
5187
|
-
const y0 = Math.floor(y * sy);
|
|
5188
|
-
const y1 = Math.max(y0 + 1, Math.floor((y + 1) * sy));
|
|
5189
|
-
for (let x = 0; x < w; x++) {
|
|
5190
|
-
const x0 = Math.floor(x * sx);
|
|
5191
|
-
const x1 = Math.max(x0 + 1, Math.floor((x + 1) * sx));
|
|
5192
|
-
let r = 0;
|
|
5193
|
-
let g = 0;
|
|
5194
|
-
let b = 0;
|
|
5195
|
-
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;
|
|
5196
4514
|
let n = 0;
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
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];
|
|
5204
4521
|
n++;
|
|
5205
4522
|
}
|
|
5206
4523
|
}
|
|
5207
|
-
|
|
5208
|
-
out[o] = n ? r / n : 0;
|
|
5209
|
-
out[o + 1] = n ? g / n : 0;
|
|
5210
|
-
out[o + 2] = n ? b / n : 0;
|
|
5211
|
-
out[o + 3] = n ? a / n : 0;
|
|
4524
|
+
cell.push(n ? sum / n : 0);
|
|
5212
4525
|
}
|
|
5213
4526
|
}
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
const margin = 24;
|
|
5222
|
-
const band = 8;
|
|
5223
|
-
const w = margin * 2 + lw + gutter + rw;
|
|
5224
|
-
const h = margin * 2 + height + band + 6;
|
|
5225
|
-
const out = new Uint8Array(w * h * 4);
|
|
5226
|
-
for (let i = 0; i < w * h; i++) {
|
|
5227
|
-
out[i * 4] = 240;
|
|
5228
|
-
out[i * 4 + 1] = 242;
|
|
5229
|
-
out[i * 4 + 2] = 244;
|
|
5230
|
-
out[i * 4 + 3] = 255;
|
|
5231
|
-
}
|
|
5232
|
-
const blit = (src, x0, y0) => {
|
|
5233
|
-
for (let y = 0; y < src.h; y++) {
|
|
5234
|
-
for (let x = 0; x < src.w; x++) {
|
|
5235
|
-
const si = (y * src.w + x) * 4;
|
|
5236
|
-
const a = src.data[si + 3] / 255;
|
|
5237
|
-
const o = ((y0 + y) * w + x0 + x) * 4;
|
|
5238
|
-
for (let c = 0; c < 3; c++)
|
|
5239
|
-
out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
|
|
5240
|
-
}
|
|
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);
|
|
5241
4534
|
}
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
const fill = (x0, x1, y0, y1, v) => {
|
|
5246
|
-
for (let y = y0; y < y1; y++)
|
|
5247
|
-
for (let x = x0; x < x1; x++) {
|
|
5248
|
-
const o = (y * w + x) * 4;
|
|
5249
|
-
out[o] = v;
|
|
5250
|
-
out[o + 1] = v;
|
|
5251
|
-
out[o + 2] = v;
|
|
5252
|
-
}
|
|
5253
|
-
};
|
|
5254
|
-
fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
|
|
5255
|
-
fill(
|
|
5256
|
-
margin + lw + gutter,
|
|
5257
|
-
margin + lw + gutter + rw,
|
|
5258
|
-
margin + height + 6,
|
|
5259
|
-
margin + height + 6 + band,
|
|
5260
|
-
200
|
|
5261
|
-
);
|
|
5262
|
-
return { w, h, data: out };
|
|
4535
|
+
bits += byte.toString(16).padStart(2, "0");
|
|
4536
|
+
}
|
|
4537
|
+
return bits;
|
|
5263
4538
|
}
|
|
5264
|
-
|
|
5265
|
-
const
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
);
|
|
5269
|
-
const manifestDir = dirname(resolve6(manifestPath));
|
|
5270
|
-
const kitDir = dirname(resolve6(kitPath));
|
|
5271
|
-
const out = resolve6(outDir ?? join7(kitDir, "judge"));
|
|
5272
|
-
await mkdir5(out, { recursive: true });
|
|
5273
|
-
const sheets = [];
|
|
5274
|
-
const skipped = [];
|
|
5275
|
-
const refCache = /* @__PURE__ */ new Map();
|
|
5276
|
-
const loadRef = async (file) => {
|
|
5277
|
-
if (refCache.has(file)) return refCache.get(file) ?? null;
|
|
5278
|
-
const p = join7(manifestDir, file);
|
|
5279
|
-
const img = existsSync7(p) ? decodePng(new Uint8Array(await readFile7(p))) : null;
|
|
5280
|
-
refCache.set(file, img);
|
|
5281
|
-
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);
|
|
5282
4543
|
};
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
)
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
);
|
|
5304
|
-
continue;
|
|
5305
|
-
}
|
|
5306
|
-
const refImg = await loadRef(ref.file);
|
|
5307
|
-
if (!refImg) {
|
|
5308
|
-
skipped.push(
|
|
5309
|
-
`${a.destination ?? a.path}: reference ${ref.file} unreadable`
|
|
5310
|
-
);
|
|
5311
|
-
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]);
|
|
5312
4565
|
}
|
|
5313
|
-
const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
|
|
5314
|
-
const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
|
|
5315
|
-
const nameA = `${id}--A.png`;
|
|
5316
|
-
const nameB = `${id}--B.png`;
|
|
5317
|
-
await writeFile7(join7(out, nameA), encodePng(composeSheet(img, refImg)));
|
|
5318
|
-
await writeFile7(join7(out, nameB), encodePng(composeSheet(refImg, img)));
|
|
5319
|
-
const rubric = [
|
|
5320
|
-
`# ${id} against ${ref.id} (${ref.file})`,
|
|
5321
|
-
"",
|
|
5322
|
-
`Sheet A: the kit asset LEFT (dark band), the reference RIGHT (light band). Sheet B: the reverse.`,
|
|
5323
|
-
`Judge each sheet on its own, both orders, and write the verdict where the two agree; a disagreement is a tie.`,
|
|
5324
|
-
"",
|
|
5325
|
-
`## The reference`,
|
|
5326
|
-
ref.layout,
|
|
5327
|
-
...ref.facts ? ["", "```json", JSON.stringify(ref.facts, null, 2), "```"] : [],
|
|
5328
|
-
"",
|
|
5329
|
-
`Rule: ${ref.rule}`,
|
|
5330
|
-
"",
|
|
5331
|
-
"## The rubric",
|
|
5332
|
-
...RUBRIC.map((r, i) => `${i + 1}. ${r}`),
|
|
5333
|
-
"",
|
|
5334
|
-
"## Verdict",
|
|
5335
|
-
"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.",
|
|
5336
|
-
""
|
|
5337
|
-
].join("\n");
|
|
5338
|
-
await writeFile7(join7(out, `${id}.md`), rubric);
|
|
5339
|
-
sheets.push({
|
|
5340
|
-
asset: id,
|
|
5341
|
-
reference: ref.id,
|
|
5342
|
-
sheetA: nameA,
|
|
5343
|
-
sheetB: nameB,
|
|
5344
|
-
rubric: `${id}.md`
|
|
5345
|
-
});
|
|
5346
|
-
}
|
|
5347
|
-
const verdictFile = join7(out, "judge.json");
|
|
5348
|
-
if (!existsSync7(verdictFile)) {
|
|
5349
|
-
await writeFile7(
|
|
5350
|
-
verdictFile,
|
|
5351
|
-
JSON.stringify(
|
|
5352
|
-
{
|
|
5353
|
-
kit: kitPath,
|
|
5354
|
-
manifest: manifestPath,
|
|
5355
|
-
verdicts: sheets.map((s) => ({
|
|
5356
|
-
asset: s.asset,
|
|
5357
|
-
reference: s.reference,
|
|
5358
|
-
A: null,
|
|
5359
|
-
B: null,
|
|
5360
|
-
win: null,
|
|
5361
|
-
reasons: []
|
|
5362
|
-
}))
|
|
5363
|
-
},
|
|
5364
|
-
null,
|
|
5365
|
-
2
|
|
5366
|
-
)
|
|
5367
|
-
);
|
|
5368
4566
|
}
|
|
5369
|
-
return
|
|
4567
|
+
return [median(r), median(g), median(b)];
|
|
5370
4568
|
}
|
|
5371
|
-
function
|
|
5372
|
-
const
|
|
5373
|
-
|
|
5374
|
-
);
|
|
5375
|
-
const
|
|
5376
|
-
const
|
|
5377
|
-
|
|
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));
|
|
4600
|
+
}
|
|
4601
|
+
const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5378
4602
|
return {
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
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)
|
|
5383
4615
|
};
|
|
5384
4616
|
}
|
|
5385
4617
|
|
|
5386
|
-
// src/plugin/fontName.ts
|
|
5387
|
-
import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
|
|
5388
|
-
function catalogFamily(family) {
|
|
5389
|
-
const name = family.trim();
|
|
5390
|
-
if (findFontFamily2(name)) return name;
|
|
5391
|
-
const base = name.replace(/\s+Variable$/i, "");
|
|
5392
|
-
if (base !== name) {
|
|
5393
|
-
const hit = findFontFamily2(base);
|
|
5394
|
-
if (hit) return hit.family;
|
|
5395
|
-
}
|
|
5396
|
-
return family;
|
|
5397
|
-
}
|
|
5398
|
-
|
|
5399
4618
|
// src/plugin/callout.ts
|
|
5400
|
-
import { existsSync as existsSync8 } from "fs";
|
|
5401
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
5402
|
-
import { join as join8 } from "path";
|
|
5403
|
-
import { lerpArray, sample } from "@vosjs/timeline";
|
|
5404
|
-
import {
|
|
5405
|
-
CALLOUT_DEFAULT_BODY_PX,
|
|
5406
|
-
CALLOUT_SHAPES,
|
|
5407
|
-
calloutClip,
|
|
5408
|
-
calloutLook,
|
|
5409
|
-
calloutGroundOf,
|
|
5410
|
-
cameraModel as cameraModel2,
|
|
5411
|
-
deltaE,
|
|
5412
|
-
docCardLayout as docCardLayout2,
|
|
5413
|
-
docZoomTrack as docZoomTrack2,
|
|
5414
|
-
hexToRgb,
|
|
5415
|
-
parseCssColour,
|
|
5416
|
-
pinBox as pinBox2,
|
|
5417
|
-
ratedSegments as ratedSegments5,
|
|
5418
|
-
resolvePins as resolvePins2,
|
|
5419
|
-
spanOutputExtent as spanOutputExtent5
|
|
5420
|
-
} from "@vosjs/studio-core";
|
|
5421
4619
|
var CALLOUT_DEFAULT_SECONDS = 3;
|
|
5422
4620
|
var CALLOUT_LEAD_SECONDS = 0.2;
|
|
5423
4621
|
var CALLOUT_DELTA_E_PROBLEM = 8;
|
|
@@ -5596,13 +4794,13 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
|
5596
4794
|
const out = /* @__PURE__ */ new Map();
|
|
5597
4795
|
if (!times.length) return out;
|
|
5598
4796
|
const bare = { ...doc, overlays: [] };
|
|
5599
|
-
const outDir =
|
|
4797
|
+
const outDir = join6(dir, ".ground-check");
|
|
5600
4798
|
const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
|
|
5601
4799
|
for (const f of res.frames) {
|
|
5602
|
-
if (!
|
|
4800
|
+
if (!existsSync6(f.file)) continue;
|
|
5603
4801
|
out.set(
|
|
5604
4802
|
+f.time.toFixed(3),
|
|
5605
|
-
decodePng(new Uint8Array(await
|
|
4803
|
+
decodePng(new Uint8Array(await readFile6(f.file)))
|
|
5606
4804
|
);
|
|
5607
4805
|
}
|
|
5608
4806
|
return out;
|
|
@@ -5611,241 +4809,9 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
|
5611
4809
|
// src/plugin/kitPicture.ts
|
|
5612
4810
|
import { execFile } from "child_process";
|
|
5613
4811
|
import { promisify } from "util";
|
|
5614
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
5615
4812
|
var execFileP = promisify(execFile);
|
|
5616
|
-
var SUBJECT_BAND = { min: 0.6, max: 0.92 };
|
|
5617
4813
|
var BLANK_INK = 0.12;
|
|
5618
|
-
var SEPARATION_L = 8;
|
|
5619
|
-
var SHADOW_PRESENT = 6;
|
|
5620
|
-
var EDGE_PRESENT = 40;
|
|
5621
|
-
var DUPLICATE_BITS2 = 6;
|
|
5622
|
-
var HALFSIZE_KEEP = 0.45;
|
|
5623
|
-
function apcaContrast(text, bg) {
|
|
5624
|
-
const lum = (c) => {
|
|
5625
|
-
const ch = (v) => Math.pow(v / 255, 2.4);
|
|
5626
|
-
let y = 0.2126729 * ch(c[0]) + 0.7151522 * ch(c[1]) + 0.072175 * ch(c[2]);
|
|
5627
|
-
if (y < 0.022) y += Math.pow(0.022 - y, 1.414);
|
|
5628
|
-
return y;
|
|
5629
|
-
};
|
|
5630
|
-
const yt = lum(text);
|
|
5631
|
-
const yb = lum(bg);
|
|
5632
|
-
if (Math.abs(yb - yt) < 5e-4) return 0;
|
|
5633
|
-
let sapc;
|
|
5634
|
-
if (yb > yt) {
|
|
5635
|
-
sapc = (Math.pow(yb, 0.56) - Math.pow(yt, 0.57)) * 1.14;
|
|
5636
|
-
return sapc < 0.1 ? 0 : (sapc - 0.027) * 100;
|
|
5637
|
-
}
|
|
5638
|
-
sapc = (Math.pow(yb, 0.65) - Math.pow(yt, 0.62)) * 1.14;
|
|
5639
|
-
return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
|
|
5640
|
-
}
|
|
5641
4814
|
var pct = (v) => `${Math.round(v * 100)}%`;
|
|
5642
|
-
var hexToRgb2 = (h) => {
|
|
5643
|
-
const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
|
|
5644
|
-
if (!m) return null;
|
|
5645
|
-
const n = parseInt(m[1], 16);
|
|
5646
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
5647
|
-
};
|
|
5648
|
-
function stillFindings(a, img, m) {
|
|
5649
|
-
const out = [];
|
|
5650
|
-
const genre = a.spec?.genre;
|
|
5651
|
-
const bledAll = m.bleed.length === 4;
|
|
5652
|
-
if (a.shot) {
|
|
5653
|
-
const sx = Math.max(0, a.shot.x) * img.w;
|
|
5654
|
-
const sy = Math.max(0, a.shot.y) * img.h;
|
|
5655
|
-
const sw = Math.min(1, a.shot.x + a.shot.w) * img.w - sx;
|
|
5656
|
-
const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
|
|
5657
|
-
const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
|
|
5658
|
-
const ink = inkCoverage(img, rect);
|
|
5659
|
-
if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
|
|
5660
|
-
out.push({
|
|
5661
|
-
code: "blank",
|
|
5662
|
-
severity: "error",
|
|
5663
|
-
asset: a.destination,
|
|
5664
|
-
message: `${pct(ink)} ink inside the shot: the moment shows a wallpaper, an empty canvas or a flat panel`,
|
|
5665
|
-
fixHint: "pick a moment after a gesture landed (--shot-time, or --times step:<id>) and stage the set before recording",
|
|
5666
|
-
bbox: rect
|
|
5667
|
-
});
|
|
5668
|
-
}
|
|
5669
|
-
if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
|
|
5670
|
-
out.push({
|
|
5671
|
-
code: "subject",
|
|
5672
|
-
severity: "error",
|
|
5673
|
-
asset: a.destination,
|
|
5674
|
-
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`,
|
|
5675
|
-
fixHint: "the template layout places the slot; fix its place for this aspect",
|
|
5676
|
-
bbox: rect
|
|
5677
|
-
});
|
|
5678
|
-
}
|
|
5679
|
-
for (const f of textFindings(a, img)) out.push(f);
|
|
5680
|
-
return out;
|
|
5681
|
-
}
|
|
5682
|
-
const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5683
|
-
if (m.ink < BLANK_INK) {
|
|
5684
|
-
out.push({
|
|
5685
|
-
code: "blank",
|
|
5686
|
-
severity: "error",
|
|
5687
|
-
asset: a.destination,
|
|
5688
|
-
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`,
|
|
5689
|
-
fixHint: "pick a moment after a gesture landed (--times step:<id>) and stage the set before recording: real data, a populated state",
|
|
5690
|
-
bbox: subject
|
|
5691
|
-
});
|
|
5692
|
-
}
|
|
5693
|
-
if (genre === "card") {
|
|
5694
|
-
if (bledAll || m.widthPct === null) {
|
|
5695
|
-
out.push({
|
|
5696
|
-
code: "subject",
|
|
5697
|
-
severity: "error",
|
|
5698
|
-
asset: a.destination,
|
|
5699
|
-
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",
|
|
5700
|
-
fixHint: "present the card in a look (vos deliver reads BRAND.md or --look) or render this destination from a poster template"
|
|
5701
|
-
});
|
|
5702
|
-
} else if (m.widthPct < SUBJECT_BAND.min || m.widthPct > SUBJECT_BAND.max) {
|
|
5703
|
-
out.push({
|
|
5704
|
-
code: "subject",
|
|
5705
|
-
severity: "error",
|
|
5706
|
-
asset: a.destination,
|
|
5707
|
-
message: `the card is ${pct(m.widthPct)} of the width; the references sit at ${pct(SUBJECT_BAND.min)} to ${pct(SUBJECT_BAND.max)}`,
|
|
5708
|
-
fixHint: "the look places the card at 84% (frame.inset); a poster template sets its own placement",
|
|
5709
|
-
bbox: m.card ?? void 0
|
|
5710
|
-
});
|
|
5711
|
-
}
|
|
5712
|
-
if (m.card && !bledAll) {
|
|
5713
|
-
const sep = m.separation ?? 0;
|
|
5714
|
-
const shadow = m.shadow ?? 0;
|
|
5715
|
-
if (sep < SEPARATION_L && shadow < SHADOW_PRESENT && m.edge < EDGE_PRESENT) {
|
|
5716
|
-
out.push({
|
|
5717
|
-
code: "separation",
|
|
5718
|
-
severity: "error",
|
|
5719
|
-
asset: a.destination,
|
|
5720
|
-
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`,
|
|
5721
|
-
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",
|
|
5722
|
-
bbox: m.card
|
|
5723
|
-
});
|
|
5724
|
-
}
|
|
5725
|
-
}
|
|
5726
|
-
}
|
|
5727
|
-
if (genre === "screenshot" && a.composed && m.card && (m.widthPct ?? 0) < 0.98) {
|
|
5728
|
-
out.push({
|
|
5729
|
-
code: "subject",
|
|
5730
|
-
severity: "error",
|
|
5731
|
-
asset: a.destination,
|
|
5732
|
-
message: `a store screenshot must be the real page full bleed; this one shows a ${pct(m.widthPct ?? 0)} card on a ground`,
|
|
5733
|
-
fixHint: "drop --composed: the screenshot genre renders the page with no chrome and no padding",
|
|
5734
|
-
bbox: m.card
|
|
5735
|
-
});
|
|
5736
|
-
}
|
|
5737
|
-
if (img.w <= 500 && (a.spec?.text === "none" || genre === "card")) {
|
|
5738
|
-
const full = edgeEnergy(img, Math.min(img.w, 400));
|
|
5739
|
-
const half = edgeEnergy(img, Math.min(img.w, 400) >> 1);
|
|
5740
|
-
const keep = full > 0 ? half / full : 1;
|
|
5741
|
-
if (keep < HALFSIZE_KEEP) {
|
|
5742
|
-
out.push({
|
|
5743
|
-
code: "halfsize",
|
|
5744
|
-
severity: "warning",
|
|
5745
|
-
asset: a.destination,
|
|
5746
|
-
message: `at half size the tile keeps ${pct(keep)} of its edges: fine text and thin lines vanish where the store shows it small`,
|
|
5747
|
-
fixHint: "a tile is the subject large and saturated with no text (the store rule); render it from the card-on-gradient template"
|
|
5748
|
-
});
|
|
5749
|
-
}
|
|
5750
|
-
}
|
|
5751
|
-
for (const f of textFindings(a, img)) out.push(f);
|
|
5752
|
-
return out;
|
|
5753
|
-
}
|
|
5754
|
-
function textFindings(a, img) {
|
|
5755
|
-
const out = [];
|
|
5756
|
-
for (const t of a.text ?? []) {
|
|
5757
|
-
const box = {
|
|
5758
|
-
x: t.x * img.w,
|
|
5759
|
-
y: t.y * img.h,
|
|
5760
|
-
w: t.w * img.w,
|
|
5761
|
-
h: t.h * img.h
|
|
5762
|
-
};
|
|
5763
|
-
const name = t.label ? `"${t.label}"` : "a text box";
|
|
5764
|
-
if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
|
|
5765
|
-
out.push({
|
|
5766
|
-
code: "sliced",
|
|
5767
|
-
severity: "error",
|
|
5768
|
-
asset: a.destination,
|
|
5769
|
-
message: `${name} crosses the frame edge: cut mid-word`,
|
|
5770
|
-
fixHint: "shorten the line, or let the template recompose (headline lines at 12 to 14% of the height, inside the safe rect)",
|
|
5771
|
-
bbox: box
|
|
5772
|
-
});
|
|
5773
|
-
}
|
|
5774
|
-
const s = a.spec?.safe;
|
|
5775
|
-
if (s) {
|
|
5776
|
-
const sx = s.x * img.w;
|
|
5777
|
-
const sy = s.y * img.h;
|
|
5778
|
-
const sw = s.w * img.w;
|
|
5779
|
-
const sh = s.h * img.h;
|
|
5780
|
-
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) {
|
|
5781
|
-
out.push({
|
|
5782
|
-
code: "safe",
|
|
5783
|
-
severity: "warning",
|
|
5784
|
-
asset: a.destination,
|
|
5785
|
-
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`,
|
|
5786
|
-
fixHint: "move the text inside the safe rect; the template reads channel-specs safe",
|
|
5787
|
-
bbox: box
|
|
5788
|
-
});
|
|
5789
|
-
}
|
|
5790
|
-
}
|
|
5791
|
-
const rgb = t.color ? hexToRgb2(t.color) : null;
|
|
5792
|
-
if (rgb) {
|
|
5793
|
-
const ground = medianColour(img, box);
|
|
5794
|
-
const lc = apcaContrast(rgb, ground);
|
|
5795
|
-
const floor = t.role === "body" ? 75 : 60;
|
|
5796
|
-
if (lc < floor) {
|
|
5797
|
-
out.push({
|
|
5798
|
-
code: "contrast",
|
|
5799
|
-
severity: "warning",
|
|
5800
|
-
asset: a.destination,
|
|
5801
|
-
message: `${name} reads APCA Lc ${lc.toFixed(0)} on its ground; ${t.role === "body" ? "body" : "a headline"} wants ${floor}`,
|
|
5802
|
-
fixHint: "darken the ink or lighten the ground under the text (BRAND.md ink on bgA/bgB, never on the shot)",
|
|
5803
|
-
bbox: box
|
|
5804
|
-
});
|
|
5805
|
-
}
|
|
5806
|
-
}
|
|
5807
|
-
}
|
|
5808
|
-
if (a.spec?.text === "none" && (a.text?.length ?? 0) > 0) {
|
|
5809
|
-
out.push({
|
|
5810
|
-
code: "safe",
|
|
5811
|
-
severity: "warning",
|
|
5812
|
-
asset: a.destination,
|
|
5813
|
-
message: "this destination wants no text (the picture carries it alone) and the kit put words on it",
|
|
5814
|
-
fixHint: "render the tile from the card-on-gradient template with no headline"
|
|
5815
|
-
});
|
|
5816
|
-
}
|
|
5817
|
-
return out;
|
|
5818
|
-
}
|
|
5819
|
-
function duplicateFindings(stills) {
|
|
5820
|
-
const groups = [];
|
|
5821
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5822
|
-
const comparable = (a, b) => a.genre === "screenshot" || b.genre === "screenshot" ? a.destination === b.destination : true;
|
|
5823
|
-
for (let i = 0; i < stills.length; i++) {
|
|
5824
|
-
if (seen.has(i)) continue;
|
|
5825
|
-
const g = [stills[i]];
|
|
5826
|
-
for (let j = i + 1; j < stills.length; j++) {
|
|
5827
|
-
if (seen.has(j)) continue;
|
|
5828
|
-
if (!comparable(stills[i], stills[j])) continue;
|
|
5829
|
-
if (hammingDistance(stills[i].hash, stills[j].hash) <= DUPLICATE_BITS2) {
|
|
5830
|
-
g.push(stills[j]);
|
|
5831
|
-
seen.add(j);
|
|
5832
|
-
}
|
|
5833
|
-
}
|
|
5834
|
-
if (g.length > 1) groups.push(g);
|
|
5835
|
-
}
|
|
5836
|
-
return groups.map((g) => {
|
|
5837
|
-
const composed = g.every(
|
|
5838
|
-
(s) => stills.find((x) => x.destination === s.destination)?.composed
|
|
5839
|
-
);
|
|
5840
|
-
return {
|
|
5841
|
-
code: "duplicate",
|
|
5842
|
-
severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
|
|
5843
|
-
asset: g.map((s) => s.destination).join(", "),
|
|
5844
|
-
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(", ")}`,
|
|
5845
|
-
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"
|
|
5846
|
-
};
|
|
5847
|
-
});
|
|
5848
|
-
}
|
|
5849
4815
|
async function videoFrame(file, at2, ffmpeg2) {
|
|
5850
4816
|
try {
|
|
5851
4817
|
const { stdout } = await execFileP(
|
|
@@ -5880,91 +4846,62 @@ async function onPath(bin) {
|
|
|
5880
4846
|
return false;
|
|
5881
4847
|
}
|
|
5882
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
|
+
}
|
|
5883
4873
|
async function pictureChecks(assets) {
|
|
5884
4874
|
const findings = [];
|
|
5885
4875
|
const measured = [];
|
|
5886
|
-
const stills = [];
|
|
5887
4876
|
let ffmpeg2 = null;
|
|
5888
4877
|
for (const a of assets) {
|
|
5889
|
-
if (
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
continue;
|
|
5901
|
-
}
|
|
5902
|
-
const m = measureStill(img);
|
|
5903
|
-
measured.push({ destination: a.destination, measure: m });
|
|
5904
|
-
findings.push(...stillFindings(a, img, m));
|
|
5905
|
-
if (a.spec?.kind !== "video")
|
|
5906
|
-
stills.push({
|
|
5907
|
-
destination: a.destination,
|
|
5908
|
-
hash: m.hash,
|
|
5909
|
-
time: null,
|
|
5910
|
-
genre: a.spec?.genre,
|
|
5911
|
-
composed: !!a.shot
|
|
5912
|
-
});
|
|
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 });
|
|
5913
4889
|
continue;
|
|
5914
4890
|
}
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
measured.push({ destination: a.destination, measure:
|
|
5926
|
-
|
|
5927
|
-
}
|
|
5928
|
-
const seconds = a.seconds ?? 0;
|
|
5929
|
-
const first = await videoFrame(a.file, 0, "ffmpeg");
|
|
5930
|
-
const last = await videoFrame(
|
|
5931
|
-
a.file,
|
|
5932
|
-
Math.max(0, seconds - 0.1),
|
|
5933
|
-
"ffmpeg"
|
|
5934
|
-
);
|
|
5935
|
-
for (const [name, img] of [
|
|
5936
|
-
["first", first],
|
|
5937
|
-
["last", last]
|
|
5938
|
-
]) {
|
|
5939
|
-
if (!img) continue;
|
|
5940
|
-
const m = measureStill(img);
|
|
5941
|
-
if (name === "first")
|
|
5942
|
-
measured.push({ destination: a.destination, measure: m });
|
|
5943
|
-
const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
|
|
5944
|
-
const ink = inkCoverage(img, subject);
|
|
5945
|
-
if (ink < BLANK_INK) {
|
|
5946
|
-
findings.push({
|
|
5947
|
-
code: "firstlast",
|
|
5948
|
-
severity: "error",
|
|
5949
|
-
asset: a.destination,
|
|
5950
|
-
message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
|
|
5951
|
-
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)"
|
|
5952
|
-
});
|
|
5953
|
-
}
|
|
5954
|
-
if (m.bleed.length === 4 && a.spec?.genre !== "screenshot") {
|
|
5955
|
-
findings.push({
|
|
5956
|
-
code: "firstlast",
|
|
5957
|
-
severity: "warning",
|
|
5958
|
-
asset: a.destination,
|
|
5959
|
-
message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
|
|
5960
|
-
fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
|
|
5961
|
-
});
|
|
5962
|
-
}
|
|
5963
|
-
}
|
|
5964
|
-
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));
|
|
5965
4903
|
}
|
|
5966
4904
|
}
|
|
5967
|
-
findings.push(...duplicateFindings(stills));
|
|
5968
4905
|
const order = {
|
|
5969
4906
|
error: 0,
|
|
5970
4907
|
warning: 1,
|
|
@@ -5980,28 +4917,15 @@ function formatFinding(f) {
|
|
|
5980
4917
|
}
|
|
5981
4918
|
|
|
5982
4919
|
// src/plugin/validateKit.ts
|
|
5983
|
-
import { existsSync as
|
|
5984
|
-
import { readFile as
|
|
5985
|
-
import { dirname
|
|
5986
|
-
import { DESTINATIONS as
|
|
5987
|
-
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5988
|
-
function pngDimensions(bytes) {
|
|
5989
|
-
if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIG2)) return null;
|
|
5990
|
-
if (bytes.toString("ascii", 12, 16) !== "IHDR") return null;
|
|
5991
|
-
return { w: bytes.readUInt32BE(16), h: bytes.readUInt32BE(20) };
|
|
5992
|
-
}
|
|
5993
|
-
function sniffImage(bytes) {
|
|
5994
|
-
if (bytes.subarray(0, 8).equals(PNG_SIG2)) return "png";
|
|
5995
|
-
if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
|
|
5996
|
-
return "webp";
|
|
5997
|
-
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216) return "jpeg";
|
|
5998
|
-
return "unknown";
|
|
5999
|
-
}
|
|
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";
|
|
6000
4924
|
async function probeVideo(path) {
|
|
6001
4925
|
const MB = await import("mediabunny");
|
|
6002
4926
|
const input = new MB.Input({
|
|
6003
4927
|
formats: MB.ALL_FORMATS,
|
|
6004
|
-
source: new MB.BufferSource(new Uint8Array(await
|
|
4928
|
+
source: new MB.BufferSource(new Uint8Array(await readFile7(path)))
|
|
6005
4929
|
});
|
|
6006
4930
|
try {
|
|
6007
4931
|
const track = await input.getPrimaryVideoTrack();
|
|
@@ -6013,7 +4937,7 @@ async function probeVideo(path) {
|
|
|
6013
4937
|
}
|
|
6014
4938
|
}
|
|
6015
4939
|
var specById = new Map(
|
|
6016
|
-
|
|
4940
|
+
DESTINATIONS2.map((d) => [d.id, d])
|
|
6017
4941
|
);
|
|
6018
4942
|
var near = (a, b, tol) => Math.abs(a - b) <= tol;
|
|
6019
4943
|
async function validateKit(kitPath, opts = {}) {
|
|
@@ -6022,7 +4946,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6022
4946
|
const measured = [];
|
|
6023
4947
|
let kit;
|
|
6024
4948
|
try {
|
|
6025
|
-
kit = JSON.parse(await
|
|
4949
|
+
kit = JSON.parse(await readFile7(kitPath, "utf8"));
|
|
6026
4950
|
} catch (e) {
|
|
6027
4951
|
return {
|
|
6028
4952
|
valid: false,
|
|
@@ -6039,13 +4963,12 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6039
4963
|
measured
|
|
6040
4964
|
};
|
|
6041
4965
|
}
|
|
6042
|
-
const base =
|
|
4966
|
+
const base = dirname(kitPath);
|
|
6043
4967
|
const resolvePath = (p) => {
|
|
6044
|
-
if (
|
|
6045
|
-
const beside =
|
|
4968
|
+
if (isAbsolute2(p) && existsSync7(p)) return p;
|
|
4969
|
+
const beside = join7(base, p.split("/").pop() ?? p);
|
|
6046
4970
|
return beside;
|
|
6047
4971
|
};
|
|
6048
|
-
const perDestination = /* @__PURE__ */ new Map();
|
|
6049
4972
|
const pictureAssets = [];
|
|
6050
4973
|
for (const a of kit.assets) {
|
|
6051
4974
|
const id = a.destination ?? `${a.channel}-${a.asset}`;
|
|
@@ -6053,12 +4976,11 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6053
4976
|
const file = resolvePath(a.path);
|
|
6054
4977
|
let bytes;
|
|
6055
4978
|
try {
|
|
6056
|
-
bytes = (await
|
|
4979
|
+
bytes = (await stat(file)).size;
|
|
6057
4980
|
} catch {
|
|
6058
4981
|
problems.push(`${label}: ${a.path} is missing`);
|
|
6059
4982
|
continue;
|
|
6060
4983
|
}
|
|
6061
|
-
perDestination.set(id, (perDestination.get(id) ?? 0) + 1);
|
|
6062
4984
|
if (bytes !== a.bytes)
|
|
6063
4985
|
problems.push(
|
|
6064
4986
|
`${label}: manifest says ${a.bytes} bytes, the file is ${bytes}`
|
|
@@ -6067,25 +4989,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6067
4989
|
let w = null;
|
|
6068
4990
|
let h = null;
|
|
6069
4991
|
let seconds = null;
|
|
6070
|
-
if (/\.(
|
|
6071
|
-
const head = Buffer.from(await readFile10(file));
|
|
6072
|
-
const kind = sniffImage(head);
|
|
6073
|
-
if (file.toLowerCase().endsWith(".png") && kind !== "png")
|
|
6074
|
-
problems.push(
|
|
6075
|
-
`${label}: ${a.path} is named .png but its bytes are ${kind} \u2014 stores refuse a mislabelled image`
|
|
6076
|
-
);
|
|
6077
|
-
const dims = kind === "png" ? pngDimensions(head) : null;
|
|
6078
|
-
if (dims) {
|
|
6079
|
-
w = dims.w;
|
|
6080
|
-
h = dims.h;
|
|
6081
|
-
if (w !== a.w || h !== a.h)
|
|
6082
|
-
problems.push(
|
|
6083
|
-
`${label}: manifest says ${a.w}x${a.h}, the file is ${w}x${h}`
|
|
6084
|
-
);
|
|
6085
|
-
} else if (kind === "png") {
|
|
6086
|
-
problems.push(`${label}: ${a.path} has no readable IHDR`);
|
|
6087
|
-
}
|
|
6088
|
-
} else if (/\.(mp4|webm|mov)$/i.test(file)) {
|
|
4992
|
+
if (/\.(mp4|webm|mov)$/i.test(file)) {
|
|
6089
4993
|
try {
|
|
6090
4994
|
const v = await probeVideo(file);
|
|
6091
4995
|
if (!v) problems.push(`${label}: ${a.path} has no video track`);
|
|
@@ -6108,21 +5012,14 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6108
5012
|
);
|
|
6109
5013
|
}
|
|
6110
5014
|
} else {
|
|
6111
|
-
warnings.push(
|
|
6112
|
-
`${label}: ${a.path} is not an image or video the kit verifies`
|
|
6113
|
-
);
|
|
5015
|
+
warnings.push(`${label}: ${a.path} is not a video the kit verifies`);
|
|
6114
5016
|
}
|
|
6115
5017
|
measured.push({ destination: id, path: a.path, w, h, bytes, seconds });
|
|
6116
5018
|
pictureAssets.push({
|
|
6117
5019
|
destination: id,
|
|
6118
5020
|
path: a.path,
|
|
6119
5021
|
file,
|
|
6120
|
-
|
|
6121
|
-
text: a.text,
|
|
6122
|
-
seconds,
|
|
6123
|
-
composed: a.composed,
|
|
6124
|
-
shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
|
|
6125
|
-
crop: a.crop
|
|
5022
|
+
seconds
|
|
6126
5023
|
});
|
|
6127
5024
|
if (!spec) {
|
|
6128
5025
|
if (a.channel !== "demo")
|
|
@@ -6149,17 +5046,6 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6149
5046
|
`${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
|
|
6150
5047
|
);
|
|
6151
5048
|
}
|
|
6152
|
-
if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
|
|
6153
|
-
warnings.push(
|
|
6154
|
-
`${label}: spec renders png, the file is ${file.split(".").pop()}`
|
|
6155
|
-
);
|
|
6156
|
-
}
|
|
6157
|
-
for (const [id, n] of perDestination) {
|
|
6158
|
-
const spec = specById.get(id);
|
|
6159
|
-
if (spec?.count && (n < spec.count.min || n > spec.count.max))
|
|
6160
|
-
problems.push(
|
|
6161
|
-
`${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
|
|
6162
|
-
);
|
|
6163
5049
|
}
|
|
6164
5050
|
if (!opts.picture) {
|
|
6165
5051
|
return { valid: problems.length === 0, problems, warnings, measured };
|
|
@@ -6178,7 +5064,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6178
5064
|
|
|
6179
5065
|
// src/plugin/recorder.ts
|
|
6180
5066
|
import { writeFileSync } from "fs";
|
|
6181
|
-
import { join as
|
|
5067
|
+
import { join as join8 } from "path";
|
|
6182
5068
|
import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
|
|
6183
5069
|
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
6184
5070
|
const dry = opts.dryRun === true;
|
|
@@ -6188,7 +5074,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6188
5074
|
actions,
|
|
6189
5075
|
{
|
|
6190
5076
|
frame: (file, bytes) => {
|
|
6191
|
-
writeFileSync(
|
|
5077
|
+
writeFileSync(join8(paths.framesDir, file), bytes);
|
|
6192
5078
|
}
|
|
6193
5079
|
},
|
|
6194
5080
|
log,
|
|
@@ -6240,11 +5126,11 @@ import {
|
|
|
6240
5126
|
|
|
6241
5127
|
// src/plugin/session.ts
|
|
6242
5128
|
import { spawn } from "child_process";
|
|
6243
|
-
import { existsSync as
|
|
5129
|
+
import { existsSync as existsSync8, mkdirSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
|
|
6244
5130
|
import { homedir } from "os";
|
|
6245
|
-
import { join as
|
|
5131
|
+
import { join as join9 } from "path";
|
|
6246
5132
|
import { chromium } from "playwright";
|
|
6247
|
-
var SESSIONS_DIR =
|
|
5133
|
+
var SESSIONS_DIR = join9(homedir(), ".config", "vos", "sessions");
|
|
6248
5134
|
var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
6249
5135
|
function sessionNameVerdict(name) {
|
|
6250
5136
|
if (!name) return "a session needs a name: --name <app>";
|
|
@@ -6253,20 +5139,20 @@ function sessionNameVerdict(name) {
|
|
|
6253
5139
|
return null;
|
|
6254
5140
|
}
|
|
6255
5141
|
function sessionDir(name) {
|
|
6256
|
-
return
|
|
5142
|
+
return join9(SESSIONS_DIR, name);
|
|
6257
5143
|
}
|
|
6258
5144
|
function systemChromePath() {
|
|
6259
5145
|
const explicit = process.env.VOS_BROWSER_PATH;
|
|
6260
5146
|
if (explicit) return explicit;
|
|
6261
5147
|
const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
|
|
6262
|
-
|
|
5148
|
+
join9(
|
|
6263
5149
|
process.env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
6264
5150
|
"Google",
|
|
6265
5151
|
"Chrome",
|
|
6266
5152
|
"Application",
|
|
6267
5153
|
"chrome.exe"
|
|
6268
5154
|
),
|
|
6269
|
-
|
|
5155
|
+
join9(
|
|
6270
5156
|
process.env["LOCALAPPDATA"] ?? "",
|
|
6271
5157
|
"Google",
|
|
6272
5158
|
"Chrome",
|
|
@@ -6278,7 +5164,7 @@ function systemChromePath() {
|
|
|
6278
5164
|
"/usr/bin/google-chrome-stable",
|
|
6279
5165
|
"/opt/google/chrome/chrome"
|
|
6280
5166
|
];
|
|
6281
|
-
const found = candidates.find((c) =>
|
|
5167
|
+
const found = candidates.find((c) => existsSync8(c));
|
|
6282
5168
|
if (!found)
|
|
6283
5169
|
throw new Error(
|
|
6284
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"
|
|
@@ -6301,15 +5187,15 @@ async function openSession(name, url, opts = { log: () => {
|
|
|
6301
5187
|
opts.log(
|
|
6302
5188
|
`a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
|
|
6303
5189
|
);
|
|
6304
|
-
return new Promise((
|
|
5190
|
+
return new Promise((resolve14, reject) => {
|
|
6305
5191
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
6306
5192
|
child.on("error", reject);
|
|
6307
|
-
child.on("exit", (code) =>
|
|
5193
|
+
child.on("exit", (code) => resolve14({ code }));
|
|
6308
5194
|
});
|
|
6309
5195
|
}
|
|
6310
5196
|
async function withSession(name, fn, opts = {}) {
|
|
6311
5197
|
const dir = sessionDir(name);
|
|
6312
|
-
if (!
|
|
5198
|
+
if (!existsSync8(dir))
|
|
6313
5199
|
throw new Error(`no session "${name}" (vos session list)`);
|
|
6314
5200
|
const explicit = process.env.VOS_BROWSER_PATH;
|
|
6315
5201
|
const ctx = await chromium.launchPersistentContext(dir, {
|
|
@@ -6358,9 +5244,9 @@ function summaryLine(s) {
|
|
|
6358
5244
|
return `session "${s.name}" holds cookies for ${hosts}${more}; ${exp}`;
|
|
6359
5245
|
}
|
|
6360
5246
|
function listSessions() {
|
|
6361
|
-
if (!
|
|
5247
|
+
if (!existsSync8(SESSIONS_DIR)) return [];
|
|
6362
5248
|
return readdirSync(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
|
|
6363
|
-
const st = statSync2(
|
|
5249
|
+
const st = statSync2(join9(SESSIONS_DIR, d.name));
|
|
6364
5250
|
return {
|
|
6365
5251
|
name: d.name,
|
|
6366
5252
|
ageDays: Math.floor(
|
|
@@ -6371,7 +5257,7 @@ function listSessions() {
|
|
|
6371
5257
|
}
|
|
6372
5258
|
function removeSession(name) {
|
|
6373
5259
|
const dir = sessionDir(name);
|
|
6374
|
-
if (!
|
|
5260
|
+
if (!existsSync8(dir)) return false;
|
|
6375
5261
|
rmSync(dir, { recursive: true, force: true });
|
|
6376
5262
|
return true;
|
|
6377
5263
|
}
|
|
@@ -6436,9 +5322,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6436
5322
|
}
|
|
6437
5323
|
|
|
6438
5324
|
// src/plugin/plan.ts
|
|
6439
|
-
import { existsSync as
|
|
6440
|
-
import { readFile as
|
|
6441
|
-
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";
|
|
6442
5328
|
import {
|
|
6443
5329
|
DEFAULT_FRAME_STYLE,
|
|
6444
5330
|
STYLE_FIELDS,
|
|
@@ -6660,10 +5546,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6660
5546
|
// src/plugin/plan.ts
|
|
6661
5547
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6662
5548
|
async function readDigestActivity(dir) {
|
|
6663
|
-
const file =
|
|
6664
|
-
if (!
|
|
5549
|
+
const file = join10(dir, "digest", "digest.json");
|
|
5550
|
+
if (!existsSync9(file)) return null;
|
|
6665
5551
|
try {
|
|
6666
|
-
const d = JSON.parse(await
|
|
5552
|
+
const d = JSON.parse(await readFile8(file, "utf8"));
|
|
6667
5553
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
6668
5554
|
} catch {
|
|
6669
5555
|
return null;
|
|
@@ -6877,17 +5763,17 @@ async function planTake(dir, opts = {}) {
|
|
|
6877
5763
|
|
|
6878
5764
|
// src/plugin/sync.ts
|
|
6879
5765
|
import { createHash } from "crypto";
|
|
6880
|
-
import { existsSync as
|
|
6881
|
-
import { readFile as
|
|
5766
|
+
import { existsSync as existsSync10, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
5767
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
6882
5768
|
import { createInterface } from "readline/promises";
|
|
6883
|
-
import { basename, join as
|
|
6884
|
-
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";
|
|
6885
5771
|
|
|
6886
5772
|
// src/plugin/folder.ts
|
|
6887
|
-
import { mkdir as
|
|
6888
|
-
import { join as
|
|
5773
|
+
import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
|
|
5774
|
+
import { join as join11 } from "path";
|
|
6889
5775
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6890
|
-
import { migrateHostedDoc
|
|
5776
|
+
import { migrateHostedDoc } from "@vosjs/studio-core";
|
|
6891
5777
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
|
|
6892
5778
|
async function listFolders(origin, key) {
|
|
6893
5779
|
const r = await apiJson(origin, "/api/folders", { key });
|
|
@@ -7061,25 +5947,25 @@ async function cmdPull(argv) {
|
|
|
7061
5947
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7062
5948
|
const payload = res.body;
|
|
7063
5949
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7064
|
-
await
|
|
5950
|
+
await mkdir5(join11(out, "recipes"), { recursive: true });
|
|
7065
5951
|
const lines = [];
|
|
7066
5952
|
const recipes = [];
|
|
7067
5953
|
for (const rec of payload.recipes) {
|
|
7068
|
-
const path =
|
|
7069
|
-
await
|
|
5954
|
+
const path = join11(out, "recipes", safeName(rec.filename));
|
|
5955
|
+
await writeFile7(path, rec.body ?? "");
|
|
7070
5956
|
recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
|
|
7071
5957
|
}
|
|
7072
5958
|
for (const rec of payload.inheritedRecipes) {
|
|
7073
5959
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7074
|
-
await
|
|
7075
|
-
const path =
|
|
5960
|
+
await mkdir5(join11(out, "recipes", "_inherited", from), { recursive: true });
|
|
5961
|
+
const path = join11(
|
|
7076
5962
|
out,
|
|
7077
5963
|
"recipes",
|
|
7078
5964
|
"_inherited",
|
|
7079
5965
|
from,
|
|
7080
5966
|
safeName(rec.filename)
|
|
7081
5967
|
);
|
|
7082
|
-
await
|
|
5968
|
+
await writeFile7(path, rec.body ?? "");
|
|
7083
5969
|
recipes.push({
|
|
7084
5970
|
path,
|
|
7085
5971
|
line: `${recipeLine(rec.filename, rec.body ?? "")} [inherited from ${rec.folderName ?? from}]`
|
|
@@ -7087,15 +5973,15 @@ async function cmdPull(argv) {
|
|
|
7087
5973
|
}
|
|
7088
5974
|
const members = [];
|
|
7089
5975
|
for (const v of payload.voses) {
|
|
7090
|
-
const dir =
|
|
7091
|
-
await
|
|
5976
|
+
const dir = join11(out, "members", safeName(v.slug || v.title || v.id));
|
|
5977
|
+
await mkdir5(dir, { recursive: true });
|
|
7092
5978
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7093
5979
|
if (cfg.status !== 200) {
|
|
7094
5980
|
lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
|
|
7095
5981
|
continue;
|
|
7096
5982
|
}
|
|
7097
|
-
await
|
|
7098
|
-
|
|
5983
|
+
await writeFile7(
|
|
5984
|
+
join11(dir, "config.json"),
|
|
7099
5985
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7100
5986
|
);
|
|
7101
5987
|
let take = false;
|
|
@@ -7106,13 +5992,13 @@ async function cmdPull(argv) {
|
|
|
7106
5992
|
{ key }
|
|
7107
5993
|
);
|
|
7108
5994
|
if (doc.status === 200) {
|
|
7109
|
-
await
|
|
7110
|
-
|
|
5995
|
+
await writeFile7(
|
|
5996
|
+
join11(dir, "doc.json"),
|
|
7111
5997
|
JSON.stringify(doc.body, null, 2)
|
|
7112
5998
|
);
|
|
7113
5999
|
take = true;
|
|
7114
6000
|
if (flags.media === true) {
|
|
7115
|
-
const hosted =
|
|
6001
|
+
const hosted = migrateHostedDoc(doc.body);
|
|
7116
6002
|
await pullMedia({ origin, key }, dir, hosted, r.log);
|
|
7117
6003
|
}
|
|
7118
6004
|
}
|
|
@@ -7233,8 +6119,8 @@ function storageStateInDir(dir) {
|
|
|
7233
6119
|
if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
|
|
7234
6120
|
continue;
|
|
7235
6121
|
try {
|
|
7236
|
-
const v = JSON.parse(readFileSync2(
|
|
7237
|
-
if (looksLikeStorageState(v)) return
|
|
6122
|
+
const v = JSON.parse(readFileSync2(join12(dir, f), "utf8"));
|
|
6123
|
+
if (looksLikeStorageState(v)) return join12(dir, f);
|
|
7238
6124
|
} catch {
|
|
7239
6125
|
}
|
|
7240
6126
|
}
|
|
@@ -7255,7 +6141,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7255
6141
|
throw new Error(`doc.json fails lint:
|
|
7256
6142
|
${lint.problems.join("\n ")}`);
|
|
7257
6143
|
}
|
|
7258
|
-
if (!
|
|
6144
|
+
if (!existsSync10(take.paths.recording)) {
|
|
7259
6145
|
throw new Error(
|
|
7260
6146
|
"no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
|
|
7261
6147
|
);
|
|
@@ -7304,7 +6190,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7304
6190
|
throw new Error("push cancelled");
|
|
7305
6191
|
}
|
|
7306
6192
|
}
|
|
7307
|
-
const bytes = await
|
|
6193
|
+
const bytes = await readFile9(take.paths.recording);
|
|
7308
6194
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7309
6195
|
const { type: contentType } = resolveMediaType({
|
|
7310
6196
|
head: bytes.subarray(0, MEDIA_HEAD_BYTES),
|
|
@@ -7476,9 +6362,9 @@ async function pullTake(dir, flags, r) {
|
|
|
7476
6362
|
};
|
|
7477
6363
|
}
|
|
7478
6364
|
let media2;
|
|
7479
|
-
if (flags.media &&
|
|
6365
|
+
if (flags.media && existsSync10(join12(dir, "doc.json"))) {
|
|
7480
6366
|
const local = JSON.parse(
|
|
7481
|
-
await
|
|
6367
|
+
await readFile9(join12(dir, "doc.json"), "utf8")
|
|
7482
6368
|
);
|
|
7483
6369
|
media2 = await pullMedia(ctx, dir, local, r.log);
|
|
7484
6370
|
}
|
|
@@ -7536,13 +6422,13 @@ async function pullTake(dir, flags, r) {
|
|
|
7536
6422
|
docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
|
|
7537
6423
|
);
|
|
7538
6424
|
}
|
|
7539
|
-
const hostedDoc =
|
|
6425
|
+
const hostedDoc = migrateHostedDoc2(docRes.json);
|
|
7540
6426
|
const doc = hostedDoc;
|
|
7541
6427
|
const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
7542
|
-
if (
|
|
6428
|
+
if (existsSync10(localRecording)) {
|
|
7543
6429
|
doc.source.videoKey = basename(localRecording);
|
|
7544
6430
|
}
|
|
7545
|
-
await writeJson(
|
|
6431
|
+
await writeJson(join12(dir, "doc.json"), doc, true);
|
|
7546
6432
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
7547
6433
|
const versions = await api(ctx, `/vos/${vosId}/versions`);
|
|
7548
6434
|
const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
|
|
@@ -7598,8 +6484,8 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
7598
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\``
|
|
7599
6485
|
);
|
|
7600
6486
|
}
|
|
7601
|
-
const path =
|
|
7602
|
-
const now =
|
|
6487
|
+
const path = join12(dir, file);
|
|
6488
|
+
const now = existsSync10(path) ? await fileSha256(path) : null;
|
|
7603
6489
|
if (now !== origin2.sha256) {
|
|
7604
6490
|
throw new Error(
|
|
7605
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`
|
|
@@ -7633,16 +6519,16 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
7633
6519
|
}
|
|
7634
6520
|
|
|
7635
6521
|
// src/plugin/program.ts
|
|
7636
|
-
import { existsSync as
|
|
7637
|
-
import { mkdir as
|
|
6522
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6523
|
+
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
7638
6524
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7639
|
-
import { basename as basename3, dirname as
|
|
6525
|
+
import { basename as basename3, dirname as dirname2, join as join13 } from "path";
|
|
7640
6526
|
import {
|
|
7641
6527
|
CURRENT_CONFIG_VERSION,
|
|
7642
6528
|
migrateConfig,
|
|
7643
6529
|
vosConfigJsonSchema
|
|
7644
6530
|
} from "@vosjs/core";
|
|
7645
|
-
import { lowerProgramDoc, migrateHostedDoc as
|
|
6531
|
+
import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
7646
6532
|
|
|
7647
6533
|
// src/plugin/login.ts
|
|
7648
6534
|
import { hostname } from "os";
|
|
@@ -7770,12 +6656,12 @@ async function handoffLogin(url, r, opts = {}) {
|
|
|
7770
6656
|
|
|
7771
6657
|
// src/plugin/programFiles.ts
|
|
7772
6658
|
import { basename as basename2, extname as extname2, relative as relative2 } from "path";
|
|
7773
|
-
import { existsSync as
|
|
6659
|
+
import { existsSync as existsSync11, realpathSync } from "fs";
|
|
7774
6660
|
function isLocalName(value) {
|
|
7775
6661
|
if (/^(https?:|data:|blob:|asset:|media:)/.test(value)) return false;
|
|
7776
6662
|
if (value.startsWith("$")) return false;
|
|
7777
6663
|
if (value.startsWith("<svg") || value.startsWith("<?xml")) return false;
|
|
7778
|
-
if (value.startsWith("/")) return
|
|
6664
|
+
if (value.startsWith("/")) return existsSync11(value);
|
|
7779
6665
|
return true;
|
|
7780
6666
|
}
|
|
7781
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;
|
|
@@ -7950,8 +6836,8 @@ function preflightConfig(parsed) {
|
|
|
7950
6836
|
}
|
|
7951
6837
|
function resolveConfigPath(target) {
|
|
7952
6838
|
if (target.endsWith(".json")) return target;
|
|
7953
|
-
const inDir =
|
|
7954
|
-
if (
|
|
6839
|
+
const inDir = join13(target, "config.json");
|
|
6840
|
+
if (existsSync12(inDir)) return inDir;
|
|
7955
6841
|
throw new UsageError(
|
|
7956
6842
|
`${target} has no config.json (and is not a take \u2014 no doc.json)`
|
|
7957
6843
|
);
|
|
@@ -7977,9 +6863,9 @@ async function cmdFetch(argv) {
|
|
|
7977
6863
|
throw new Error(apiError(`fetch config for ${vosId}`, cfg));
|
|
7978
6864
|
const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
|
|
7979
6865
|
const out = strFlag(flags, "out") ?? slug;
|
|
7980
|
-
await
|
|
7981
|
-
await
|
|
7982
|
-
|
|
6866
|
+
await mkdir6(out, { recursive: true });
|
|
6867
|
+
await writeFile8(
|
|
6868
|
+
join13(out, "config.json"),
|
|
7983
6869
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7984
6870
|
);
|
|
7985
6871
|
const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
|
|
@@ -8001,15 +6887,15 @@ async function cmdFetch(argv) {
|
|
|
8001
6887
|
}
|
|
8002
6888
|
);
|
|
8003
6889
|
if (doc.status === 200 && !("source" in doc.body)) {
|
|
8004
|
-
const hosted =
|
|
6890
|
+
const hosted = migrateHostedDoc3(doc.body);
|
|
8005
6891
|
const { config: own } = await writeProgramDoc(out, hosted);
|
|
8006
6892
|
if (own) {
|
|
8007
|
-
await
|
|
6893
|
+
await writeFile8(join13(out, "config.json"), JSON.stringify(own, null, 2));
|
|
8008
6894
|
}
|
|
8009
6895
|
} else if (doc.status === 200) {
|
|
8010
6896
|
take = true;
|
|
8011
|
-
const hosted =
|
|
8012
|
-
await
|
|
6897
|
+
const hosted = migrateHostedDoc3(doc.body);
|
|
6898
|
+
await writeFile8(join13(out, "doc.json"), JSON.stringify(hosted, null, 2));
|
|
8013
6899
|
if (flags.media === true) {
|
|
8014
6900
|
const media = await pullMedia({ origin, key }, out, hosted, r.log);
|
|
8015
6901
|
await recordHostedMedia(out, vosId, media);
|
|
@@ -8019,15 +6905,15 @@ async function cmdFetch(argv) {
|
|
|
8019
6905
|
}
|
|
8020
6906
|
let assetsLine = "";
|
|
8021
6907
|
if (!take) {
|
|
8022
|
-
const configPath =
|
|
8023
|
-
const written = JSON.parse(await
|
|
6908
|
+
const configPath = join13(out, "config.json");
|
|
6909
|
+
const written = JSON.parse(await readFile10(configPath, "utf8"));
|
|
8024
6910
|
const hostedCount = manifestRefs(written).filter(
|
|
8025
6911
|
({ ref }) => hostedAssetId(ref, [origin, "https://vos.so", "https://www.vos.so"])
|
|
8026
6912
|
).length;
|
|
8027
6913
|
if (hostedCount && flags.media === true) {
|
|
8028
6914
|
const pulled = await pullManifest({ origin, key }, out, written, r.log);
|
|
8029
6915
|
if (pulled) {
|
|
8030
|
-
await
|
|
6916
|
+
await writeFile8(
|
|
8031
6917
|
configPath,
|
|
8032
6918
|
JSON.stringify({ ...written, assets: pulled.assets }, null, 2)
|
|
8033
6919
|
);
|
|
@@ -8146,8 +7032,8 @@ async function cmdPushProgram(argv) {
|
|
|
8146
7032
|
origin: strFlag(flags, "origin"),
|
|
8147
7033
|
api: strFlag(flags, "api")
|
|
8148
7034
|
});
|
|
8149
|
-
const dir =
|
|
8150
|
-
const parsed = JSON.parse(await
|
|
7035
|
+
const dir = dirname2(source);
|
|
7036
|
+
const parsed = JSON.parse(await readFile10(source, "utf8"));
|
|
8151
7037
|
const pre = preflightConfig(parsed);
|
|
8152
7038
|
if (!pre.ok || !pre.config) {
|
|
8153
7039
|
for (const issue of pre.issues) r.log(`error ${issue}`);
|
|
@@ -8430,7 +7316,7 @@ ${landedLines(landed2)}` : "")
|
|
|
8430
7316
|
async function cmdPullProgram(argv) {
|
|
8431
7317
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
|
|
8432
7318
|
const target = positionals[0] ?? ".";
|
|
8433
|
-
const dir = target.endsWith(".json") ?
|
|
7319
|
+
const dir = target.endsWith(".json") ? dirname2(target) : target;
|
|
8434
7320
|
const r = createReporter(flags.json === true);
|
|
8435
7321
|
const origin = platformOrigin({
|
|
8436
7322
|
origin: strFlag(flags, "origin"),
|
|
@@ -8495,10 +7381,10 @@ async function cmdPullProgram(argv) {
|
|
|
8495
7381
|
const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
|
|
8496
7382
|
if (cfg.status !== 200)
|
|
8497
7383
|
throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
|
|
8498
|
-
const configPath =
|
|
7384
|
+
const configPath = join13(dir, "config.json");
|
|
8499
7385
|
let backedUp = false;
|
|
8500
|
-
if (
|
|
8501
|
-
await
|
|
7386
|
+
if (existsSync12(configPath)) {
|
|
7387
|
+
await writeFile8(join13(dir, "config.backup.json"), await readFile10(configPath));
|
|
8502
7388
|
backedUp = true;
|
|
8503
7389
|
}
|
|
8504
7390
|
let headConfig = cfg.body.config;
|
|
@@ -8512,12 +7398,12 @@ async function cmdPullProgram(argv) {
|
|
|
8512
7398
|
if (docRes.status === 200 && !("source" in docRes.body)) {
|
|
8513
7399
|
const { config: own } = await writeProgramDoc(
|
|
8514
7400
|
dir,
|
|
8515
|
-
|
|
7401
|
+
migrateHostedDoc3(docRes.body)
|
|
8516
7402
|
);
|
|
8517
7403
|
if (own) headConfig = own;
|
|
8518
7404
|
}
|
|
8519
7405
|
}
|
|
8520
|
-
await
|
|
7406
|
+
await writeFile8(configPath, JSON.stringify(headConfig, null, 2));
|
|
8521
7407
|
writeSyncState(dir, {
|
|
8522
7408
|
vosId,
|
|
8523
7409
|
versionId: typeof head.id === "string" ? head.id : since
|
|
@@ -8531,7 +7417,7 @@ async function cmdPullProgram(argv) {
|
|
|
8531
7417
|
protected: protectedIds,
|
|
8532
7418
|
changes,
|
|
8533
7419
|
out: configPath,
|
|
8534
|
-
backup: backedUp ?
|
|
7420
|
+
backup: backedUp ? join13(dir, "config.backup.json") : null
|
|
8535
7421
|
},
|
|
8536
7422
|
`Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
|
|
8537
7423
|
Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
|
|
@@ -8639,9 +7525,9 @@ function storedProgramConfig(config, programDoc) {
|
|
|
8639
7525
|
return lowerProgramDoc(doc).config;
|
|
8640
7526
|
}
|
|
8641
7527
|
async function readProgramDoc(dir, config) {
|
|
8642
|
-
const docPath =
|
|
8643
|
-
if (!
|
|
8644
|
-
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"));
|
|
8645
7531
|
if (typeof parsed !== "object" || parsed === null || "source" in parsed)
|
|
8646
7532
|
return null;
|
|
8647
7533
|
const raw = parsed;
|
|
@@ -8649,23 +7535,23 @@ async function readProgramDoc(dir, config) {
|
|
|
8649
7535
|
return { ...raw, program: { ...program, config } };
|
|
8650
7536
|
}
|
|
8651
7537
|
async function setProgramStill(dir, t) {
|
|
8652
|
-
const docPath =
|
|
7538
|
+
const docPath = join13(dir, "doc.json");
|
|
8653
7539
|
let doc = { program: {}, audio: [] };
|
|
8654
|
-
if (
|
|
8655
|
-
const parsed = JSON.parse(await
|
|
7540
|
+
if (existsSync12(docPath)) {
|
|
7541
|
+
const parsed = JSON.parse(await readFile10(docPath, "utf8"));
|
|
8656
7542
|
if (!parsed || typeof parsed !== "object" || "source" in parsed)
|
|
8657
7543
|
throw new UsageError(
|
|
8658
7544
|
`${docPath} is a take's document; a take's cover is its own "still" field`
|
|
8659
7545
|
);
|
|
8660
7546
|
doc = parsed;
|
|
8661
7547
|
}
|
|
8662
|
-
await
|
|
7548
|
+
await writeFile8(docPath, JSON.stringify({ ...doc, still: t }, null, 2) + "\n");
|
|
8663
7549
|
}
|
|
8664
7550
|
async function writeProgramDoc(dir, hosted) {
|
|
8665
7551
|
const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
|
|
8666
7552
|
const { config, ...rest } = program;
|
|
8667
7553
|
const onDisk = { ...hosted, program: rest };
|
|
8668
|
-
await
|
|
7554
|
+
await writeFile8(join13(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
|
|
8669
7555
|
return {
|
|
8670
7556
|
config: config && typeof config === "object" ? config : null
|
|
8671
7557
|
};
|
|
@@ -9077,8 +7963,8 @@ async function cmdAsset(argv) {
|
|
|
9077
7963
|
}
|
|
9078
7964
|
|
|
9079
7965
|
// src/plugin/remove.ts
|
|
9080
|
-
import { existsSync as
|
|
9081
|
-
import { join as
|
|
7966
|
+
import { existsSync as existsSync13, rmSync as rmSync2, statSync as statSync3 } from "fs";
|
|
7967
|
+
import { join as join14 } from "path";
|
|
9082
7968
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
9083
7969
|
|
|
9084
7970
|
// src/plugin/trash.ts
|
|
@@ -9257,7 +8143,7 @@ async function cmdDelete(argv) {
|
|
|
9257
8143
|
api: strFlag(flags, "api")
|
|
9258
8144
|
}).replace(/\/+$/, "");
|
|
9259
8145
|
const key = requireCredential(strFlag(flags, "key"));
|
|
9260
|
-
const dir =
|
|
8146
|
+
const dir = existsSync13(target) && statSync3(target).isDirectory() ? target : null;
|
|
9261
8147
|
const vosId = dir ? readSyncState(dir)?.vosId : parseVosId(target);
|
|
9262
8148
|
if (!vosId)
|
|
9263
8149
|
throw new UsageError(
|
|
@@ -9300,7 +8186,7 @@ async function cmdDelete(argv) {
|
|
|
9300
8186
|
const tracking = dir ?? trackingCwd(vosId);
|
|
9301
8187
|
let unlinked = false;
|
|
9302
8188
|
if (tracking) {
|
|
9303
|
-
rmSync2(
|
|
8189
|
+
rmSync2(join14(tracking, SYNC_STATE_NAME), { force: true });
|
|
9304
8190
|
unlinked = true;
|
|
9305
8191
|
}
|
|
9306
8192
|
const trashed = res.body.trashed === true;
|
|
@@ -9325,13 +8211,13 @@ async function cmdDelete(argv) {
|
|
|
9325
8211
|
}
|
|
9326
8212
|
function trackingCwd(vosId) {
|
|
9327
8213
|
const cwd = process.cwd();
|
|
9328
|
-
if (!
|
|
8214
|
+
if (!existsSync13(join14(cwd, SYNC_STATE_NAME))) return null;
|
|
9329
8215
|
return readSyncState(cwd)?.vosId === vosId ? cwd : null;
|
|
9330
8216
|
}
|
|
9331
8217
|
|
|
9332
8218
|
// src/plugin/buildProgram.ts
|
|
9333
8219
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
9334
|
-
import { basename as basename5, dirname as
|
|
8220
|
+
import { basename as basename5, dirname as dirname3, join as join15, resolve as resolve5 } from "path";
|
|
9335
8221
|
import { pathToFileURL } from "url";
|
|
9336
8222
|
import { parse } from "acorn";
|
|
9337
8223
|
import { fullAncestor } from "acorn-walk";
|
|
@@ -9531,7 +8417,7 @@ async function cmdBuild(argv) {
|
|
|
9531
8417
|
if (!source)
|
|
9532
8418
|
throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
|
|
9533
8419
|
const r = createReporter(flags.json === true);
|
|
9534
|
-
const file =
|
|
8420
|
+
const file = resolve5(source);
|
|
9535
8421
|
const text = readFileSync4(file, "utf8");
|
|
9536
8422
|
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
9537
8423
|
const program = mod.default ?? mod.program;
|
|
@@ -9548,8 +8434,8 @@ async function cmdBuild(argv) {
|
|
|
9548
8434
|
);
|
|
9549
8435
|
return EXIT_ERROR;
|
|
9550
8436
|
}
|
|
9551
|
-
const out =
|
|
9552
|
-
strFlag(flags, "out") ??
|
|
8437
|
+
const out = resolve5(
|
|
8438
|
+
strFlag(flags, "out") ?? join15(dirname3(file), "config.json")
|
|
9553
8439
|
);
|
|
9554
8440
|
writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
|
|
9555
8441
|
const sidecar = out.replace(/\.json$/, "") + ".sources.json";
|
|
@@ -9566,8 +8452,8 @@ async function cmdBuild(argv) {
|
|
|
9566
8452
|
|
|
9567
8453
|
// src/plugin/compare.ts
|
|
9568
8454
|
import { spawnSync } from "child_process";
|
|
9569
|
-
import { existsSync as
|
|
9570
|
-
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";
|
|
9571
8457
|
var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9572
8458
|
function sampleTimes(duration, every) {
|
|
9573
8459
|
const step = every > 0 ? every : 1;
|
|
@@ -9628,13 +8514,13 @@ async function cmdCompare(argv) {
|
|
|
9628
8514
|
);
|
|
9629
8515
|
requireFfmpeg();
|
|
9630
8516
|
const r = createReporter(flags.json === true);
|
|
9631
|
-
const ref =
|
|
8517
|
+
const ref = resolve6(against);
|
|
9632
8518
|
const loaded = await loadVosConfig(source);
|
|
9633
8519
|
for (const w of loaded.warnings) r.log(`note: ${w}`);
|
|
9634
8520
|
const config = loaded.config;
|
|
9635
8521
|
const duration = configDuration(config) ?? 5;
|
|
9636
8522
|
const assets = await programAssets(config, {
|
|
9637
|
-
baseDir: /^https?:\/\//.test(source) ? null :
|
|
8523
|
+
baseDir: /^https?:\/\//.test(source) ? null : existsSync14(source) && statSync4(source).isDirectory() ? source : dirname4(source),
|
|
9638
8524
|
origin: platformOrigin({}),
|
|
9639
8525
|
key: resolveCredential(),
|
|
9640
8526
|
log: (line) => r.log(line)
|
|
@@ -9650,16 +8536,16 @@ async function cmdCompare(argv) {
|
|
|
9650
8536
|
const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
|
|
9651
8537
|
const timesRaw = strFlag(flags, "times");
|
|
9652
8538
|
const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
|
|
9653
|
-
const outDir =
|
|
8539
|
+
const outDir = resolve6(strFlag(flags, "out") ?? "compare");
|
|
9654
8540
|
mkdirSync2(outDir, { recursive: true });
|
|
9655
8541
|
const browser = await launchBrowser();
|
|
9656
8542
|
const scores = [];
|
|
9657
8543
|
try {
|
|
9658
8544
|
for (const t of times) {
|
|
9659
8545
|
const tag = t.toFixed(2);
|
|
9660
|
-
const refPng =
|
|
9661
|
-
const vosPng =
|
|
9662
|
-
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`);
|
|
9663
8549
|
const grab = ffmpeg([
|
|
9664
8550
|
"-v",
|
|
9665
8551
|
"error",
|
|
@@ -9716,7 +8602,7 @@ async function cmdCompare(argv) {
|
|
|
9716
8602
|
} finally {
|
|
9717
8603
|
await browser.close();
|
|
9718
8604
|
}
|
|
9719
|
-
const overview =
|
|
8605
|
+
const overview = join16(outDir, "overview.png");
|
|
9720
8606
|
if (scores.length) {
|
|
9721
8607
|
const inputs = scores.flatMap((s) => ["-i", s.sheet]);
|
|
9722
8608
|
const scaled = scores.map((_, i) => `[${i}:v]scale=iw/3:-2[s${i}]`).join(";");
|
|
@@ -9742,7 +8628,7 @@ async function cmdCompare(argv) {
|
|
|
9742
8628
|
overview
|
|
9743
8629
|
};
|
|
9744
8630
|
writeFileSync3(
|
|
9745
|
-
|
|
8631
|
+
join16(outDir, "report.json"),
|
|
9746
8632
|
JSON.stringify(report, null, 2) + "\n"
|
|
9747
8633
|
);
|
|
9748
8634
|
r.done(
|
|
@@ -9753,28 +8639,28 @@ async function cmdCompare(argv) {
|
|
|
9753
8639
|
}
|
|
9754
8640
|
|
|
9755
8641
|
// src/plugin/port.ts
|
|
9756
|
-
import { existsSync as
|
|
9757
|
-
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";
|
|
9758
8644
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
9759
8645
|
|
|
9760
8646
|
// src/plugin/portInventory.ts
|
|
9761
8647
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
9762
8648
|
import {
|
|
9763
|
-
existsSync as
|
|
8649
|
+
existsSync as existsSync16,
|
|
9764
8650
|
mkdirSync as mkdirSync3,
|
|
9765
8651
|
readFileSync as readFileSync6,
|
|
9766
8652
|
readdirSync as readdirSync4,
|
|
9767
8653
|
statSync as statSync6
|
|
9768
8654
|
} from "fs";
|
|
9769
|
-
import { dirname as
|
|
8655
|
+
import { dirname as dirname5, join as join18, relative as relative3, resolve as resolve7 } from "path";
|
|
9770
8656
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
9771
8657
|
import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
|
|
9772
8658
|
|
|
9773
8659
|
// src/plugin/portRemotion.ts
|
|
9774
|
-
import { existsSync as
|
|
8660
|
+
import { existsSync as existsSync15, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
|
|
9775
8661
|
import { createServer as createServer2 } from "http";
|
|
9776
8662
|
import { createRequire } from "module";
|
|
9777
|
-
import { basename as basename6, extname as extname3, join as
|
|
8663
|
+
import { basename as basename6, extname as extname3, join as join17 } from "path";
|
|
9778
8664
|
function themeColors(sources) {
|
|
9779
8665
|
const out = {};
|
|
9780
8666
|
const re = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*['"](#[0-9a-fA-F]{3,8})['"]/g;
|
|
@@ -9840,14 +8726,14 @@ function sourceTexts(dir) {
|
|
|
9840
8726
|
if (depth > 4 || out.length > 200) return;
|
|
9841
8727
|
for (const name of readdirSync3(d)) {
|
|
9842
8728
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
9843
|
-
const p =
|
|
8729
|
+
const p = join17(d, name);
|
|
9844
8730
|
if (statSync5(p).isDirectory()) walk(p, depth + 1);
|
|
9845
8731
|
else if (/\.(tsx?|jsx?|mjs)$/.test(name))
|
|
9846
8732
|
out.push(readFileSync5(p, "utf8"));
|
|
9847
8733
|
}
|
|
9848
8734
|
};
|
|
9849
|
-
const src =
|
|
9850
|
-
walk(
|
|
8735
|
+
const src = join17(dir, "src");
|
|
8736
|
+
walk(existsSync15(src) ? src : dir, 0);
|
|
9851
8737
|
return out;
|
|
9852
8738
|
}
|
|
9853
8739
|
function entryPoint(dir) {
|
|
@@ -9859,7 +8745,7 @@ function entryPoint(dir) {
|
|
|
9859
8745
|
"remotion/index.ts",
|
|
9860
8746
|
"remotion/index.tsx"
|
|
9861
8747
|
])
|
|
9862
|
-
if (
|
|
8748
|
+
if (existsSync15(join17(dir, p))) return join17(dir, p);
|
|
9863
8749
|
return null;
|
|
9864
8750
|
}
|
|
9865
8751
|
var TYPES = {
|
|
@@ -9934,7 +8820,7 @@ async function readRemotion(dir, log) {
|
|
|
9934
8820
|
const entry = entryPoint(dir);
|
|
9935
8821
|
let bundler;
|
|
9936
8822
|
try {
|
|
9937
|
-
bundler = createRequire(
|
|
8823
|
+
bundler = createRequire(join17(dir, "package.json"))("@remotion/bundler");
|
|
9938
8824
|
} catch {
|
|
9939
8825
|
log?.(
|
|
9940
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"
|
|
@@ -9951,9 +8837,9 @@ async function readRemotion(dir, log) {
|
|
|
9951
8837
|
} });
|
|
9952
8838
|
const server = createServer2((rq, rs) => {
|
|
9953
8839
|
const path = decodeURIComponent(new URL(rq.url ?? "/", "http://x").pathname);
|
|
9954
|
-
let f =
|
|
9955
|
-
if (!f.startsWith(out) || !
|
|
9956
|
-
f =
|
|
8840
|
+
let f = join17(out, path);
|
|
8841
|
+
if (!f.startsWith(out) || !existsSync15(f) || statSync5(f).isDirectory())
|
|
8842
|
+
f = join17(out, "index.html");
|
|
9957
8843
|
rs.writeHead(200, {
|
|
9958
8844
|
"content-type": TYPES[extname3(f)] ?? "application/octet-stream"
|
|
9959
8845
|
});
|
|
@@ -10052,10 +8938,10 @@ async function readRemotion(dir, log) {
|
|
|
10052
8938
|
perScene.push({ scene: s.name, texts: read.texts ?? [] });
|
|
10053
8939
|
}
|
|
10054
8940
|
const sources = sourceTexts(dir);
|
|
10055
|
-
const publicDir =
|
|
8941
|
+
const publicDir = join17(dir, "public");
|
|
10056
8942
|
const media = tree.audio.map((a) => {
|
|
10057
8943
|
const name = basename6(a.src.split("?")[0]);
|
|
10058
|
-
const local =
|
|
8944
|
+
const local = existsSync15(join17(publicDir, name)) ? `public/${name}` : a.src;
|
|
10059
8945
|
return {
|
|
10060
8946
|
kind: "audio",
|
|
10061
8947
|
src: local,
|
|
@@ -10096,9 +8982,9 @@ async function readRemotion(dir, log) {
|
|
|
10096
8982
|
|
|
10097
8983
|
// src/plugin/portInventory.ts
|
|
10098
8984
|
function detectEngine(dir) {
|
|
10099
|
-
if (
|
|
10100
|
-
const pkg =
|
|
10101
|
-
if (
|
|
8985
|
+
if (existsSync16(join18(dir, "hyperframes.json"))) return "hyperframes";
|
|
8986
|
+
const pkg = join18(dir, "package.json");
|
|
8987
|
+
if (existsSync16(pkg)) {
|
|
10102
8988
|
try {
|
|
10103
8989
|
const p = JSON.parse(readFileSync6(pkg, "utf8"));
|
|
10104
8990
|
const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
|
|
@@ -10112,14 +8998,14 @@ function detectEngine(dir) {
|
|
|
10112
8998
|
return "page";
|
|
10113
8999
|
}
|
|
10114
9000
|
function sourcePage(source) {
|
|
10115
|
-
if (!
|
|
9001
|
+
if (!existsSync16(source)) return null;
|
|
10116
9002
|
if (statSync6(source).isFile()) return /\.html?$/i.test(source) ? source : null;
|
|
10117
|
-
const index =
|
|
10118
|
-
return
|
|
9003
|
+
const index = join18(source, "index.html");
|
|
9004
|
+
return existsSync16(index) ? index : null;
|
|
10119
9005
|
}
|
|
10120
9006
|
function findRender(dir) {
|
|
10121
|
-
const videos = (d) =>
|
|
10122
|
-
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]) {
|
|
10123
9009
|
const found = videos(d);
|
|
10124
9010
|
if (found.length) return found[0];
|
|
10125
9011
|
}
|
|
@@ -10389,8 +9275,8 @@ var SEEK_AND_READ = `((args) => {
|
|
|
10389
9275
|
return out
|
|
10390
9276
|
})`;
|
|
10391
9277
|
async function readInventory(source, opts) {
|
|
10392
|
-
const abs =
|
|
10393
|
-
const dir =
|
|
9278
|
+
const abs = resolve7(source);
|
|
9279
|
+
const dir = existsSync16(abs) && statSync6(abs).isDirectory() ? abs : dirname5(abs);
|
|
10394
9280
|
const engine = detectEngine(dir);
|
|
10395
9281
|
const page = sourcePage(abs);
|
|
10396
9282
|
const render = opts.render === void 0 ? findRender(dir) : opts.render;
|
|
@@ -10493,7 +9379,7 @@ async function readInventory(source, opts) {
|
|
|
10493
9379
|
if (render && duration) {
|
|
10494
9380
|
const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
|
|
10495
9381
|
if (has) {
|
|
10496
|
-
const refDir =
|
|
9382
|
+
const refDir = join18(opts.outDir, "ref");
|
|
10497
9383
|
mkdirSync3(refDir, { recursive: true });
|
|
10498
9384
|
const times = read.scenes.length >= 3 ? read.scenes.map(
|
|
10499
9385
|
(s) => s.start + (s.duration ?? 1) / 2
|
|
@@ -10502,7 +9388,7 @@ async function readInventory(source, opts) {
|
|
|
10502
9388
|
(_, i) => i + 0.5
|
|
10503
9389
|
);
|
|
10504
9390
|
for (const t of times.filter((t2) => t2 < duration)) {
|
|
10505
|
-
const file =
|
|
9391
|
+
const file = join18(refDir, `src-${t.toFixed(2)}.png`);
|
|
10506
9392
|
const ok = spawnSync2("ffmpeg", [
|
|
10507
9393
|
"-v",
|
|
10508
9394
|
"error",
|
|
@@ -10532,7 +9418,7 @@ async function readInventory(source, opts) {
|
|
|
10532
9418
|
const scriptStrings = scriptStringsOf(read.scriptText);
|
|
10533
9419
|
return {
|
|
10534
9420
|
source: relative3(process.cwd(), abs) || ".",
|
|
10535
|
-
root: abs.endsWith(".html") ?
|
|
9421
|
+
root: abs.endsWith(".html") ? dirname5(abs) : abs,
|
|
10536
9422
|
engine,
|
|
10537
9423
|
width: read.width,
|
|
10538
9424
|
height: read.height,
|
|
@@ -10556,8 +9442,8 @@ async function readInventory(source, opts) {
|
|
|
10556
9442
|
|
|
10557
9443
|
// src/plugin/portScaffold.ts
|
|
10558
9444
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
10559
|
-
import { copyFileSync, existsSync as
|
|
10560
|
-
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";
|
|
10561
9447
|
import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
10562
9448
|
|
|
10563
9449
|
// src/plugin/painterLib.ts
|
|
@@ -10966,9 +9852,9 @@ function planScaffold(inv, opts) {
|
|
|
10966
9852
|
const audio = [];
|
|
10967
9853
|
const copies = [];
|
|
10968
9854
|
inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
|
|
10969
|
-
const from =
|
|
9855
|
+
const from = resolve8(opts.sourceDir, m.src);
|
|
10970
9856
|
const name = basename7(from);
|
|
10971
|
-
if (!
|
|
9857
|
+
if (!existsSync17(from)) {
|
|
10972
9858
|
report.push(
|
|
10973
9859
|
`- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
|
|
10974
9860
|
);
|
|
@@ -11166,13 +10052,13 @@ function reportText(inv, r) {
|
|
|
11166
10052
|
}
|
|
11167
10053
|
function writeScaffold(plan, outDir) {
|
|
11168
10054
|
mkdirSync4(outDir, { recursive: true });
|
|
11169
|
-
writeFileSync4(
|
|
10055
|
+
writeFileSync4(join19(outDir, "program.mjs"), plan.program);
|
|
11170
10056
|
writeFileSync4(
|
|
11171
|
-
|
|
10057
|
+
join19(outDir, "doc.json"),
|
|
11172
10058
|
JSON.stringify(plan.doc, null, 2) + "\n"
|
|
11173
10059
|
);
|
|
11174
|
-
writeFileSync4(
|
|
11175
|
-
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));
|
|
11176
10062
|
}
|
|
11177
10063
|
function probeAudioDuration(file) {
|
|
11178
10064
|
const res = spawnSync3(
|
|
@@ -11198,15 +10084,15 @@ async function inventory(argv) {
|
|
|
11198
10084
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
|
|
11199
10085
|
const source = positionals[0] ?? ".";
|
|
11200
10086
|
const r = createReporter(flags.json === true);
|
|
11201
|
-
const outDir =
|
|
10087
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "port");
|
|
11202
10088
|
mkdirSync5(outDir, { recursive: true });
|
|
11203
10089
|
const renderFlag = strFlag(flags, "render");
|
|
11204
10090
|
const inv = await readInventory(source, {
|
|
11205
|
-
render: renderFlag === void 0 ? void 0 :
|
|
10091
|
+
render: renderFlag === void 0 ? void 0 : resolve9(renderFlag),
|
|
11206
10092
|
outDir,
|
|
11207
10093
|
log: (l) => r.log(`note: ${l}`)
|
|
11208
10094
|
});
|
|
11209
|
-
const file =
|
|
10095
|
+
const file = join20(outDir, "inventory.json");
|
|
11210
10096
|
writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
|
|
11211
10097
|
const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
|
|
11212
10098
|
r.log(
|
|
@@ -11238,38 +10124,38 @@ function summary(inv) {
|
|
|
11238
10124
|
async function scaffold(argv) {
|
|
11239
10125
|
const { flags } = parseArgs(argv, BOOLEAN_FLAGS8);
|
|
11240
10126
|
const r = createReporter(flags.json === true);
|
|
11241
|
-
const from =
|
|
11242
|
-
if (!
|
|
10127
|
+
const from = resolve9(strFlag(flags, "from") ?? join20("port", "inventory.json"));
|
|
10128
|
+
if (!existsSync18(from))
|
|
11243
10129
|
throw new UsageError(
|
|
11244
10130
|
`no inventory at ${from}: run npx vos port inventory <source> first`
|
|
11245
10131
|
);
|
|
11246
10132
|
const inv = JSON.parse(readFileSync7(from, "utf8"));
|
|
11247
|
-
const outDir =
|
|
11248
|
-
strFlag(flags, "out") ??
|
|
10133
|
+
const outDir = resolve9(
|
|
10134
|
+
strFlag(flags, "out") ?? join20(dirname7(from), "program")
|
|
11249
10135
|
);
|
|
11250
10136
|
const plan = planScaffold(inv, {
|
|
11251
10137
|
sourceDir: scaffoldSourceDir(inv),
|
|
11252
10138
|
probeDuration: probeAudioDuration
|
|
11253
10139
|
});
|
|
11254
10140
|
writeScaffold(plan, outDir);
|
|
11255
|
-
const programFile =
|
|
10141
|
+
const programFile = join20(outDir, "program.mjs");
|
|
11256
10142
|
const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
|
|
11257
10143
|
const built = buildProgram(mod.default, "program.mjs", plan.program);
|
|
11258
10144
|
for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
|
|
11259
10145
|
if (built.problems.length) return EXIT_ERROR;
|
|
11260
10146
|
writeFileSync5(
|
|
11261
|
-
|
|
10147
|
+
join20(outDir, "config.json"),
|
|
11262
10148
|
JSON.stringify(built.config, null, 2) + "\n"
|
|
11263
10149
|
);
|
|
11264
10150
|
r.done(
|
|
11265
|
-
{ program: outDir, report:
|
|
11266
|
-
`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>`
|
|
11267
10153
|
);
|
|
11268
10154
|
return EXIT_OK;
|
|
11269
10155
|
}
|
|
11270
10156
|
function scaffoldSourceDir(inv) {
|
|
11271
10157
|
if (inv.root) return inv.root;
|
|
11272
|
-
return inv.source.endsWith(".html") ?
|
|
10158
|
+
return inv.source.endsWith(".html") ? dirname7(resolve9(inv.source)) : resolve9(inv.source);
|
|
11273
10159
|
}
|
|
11274
10160
|
|
|
11275
10161
|
// src/plugin/recipe.ts
|
|
@@ -11360,8 +10246,8 @@ async function cmdRecipe(argv) {
|
|
|
11360
10246
|
}
|
|
11361
10247
|
|
|
11362
10248
|
// src/plugin/brand.ts
|
|
11363
|
-
import { writeFile as
|
|
11364
|
-
import { resolve as
|
|
10249
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
10250
|
+
import { resolve as resolve10 } from "path";
|
|
11365
10251
|
import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
|
|
11366
10252
|
import { lookKindForGround } from "@vosjs/studio-core";
|
|
11367
10253
|
var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
|
|
@@ -11649,7 +10535,7 @@ function renderBrandMd(c, claim) {
|
|
|
11649
10535
|
"",
|
|
11650
10536
|
"## Use",
|
|
11651
10537
|
"",
|
|
11652
|
-
`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\`.`
|
|
11653
10539
|
];
|
|
11654
10540
|
if (c.avoid.length) {
|
|
11655
10541
|
lines.push("", "## Avoid (the site says)", "");
|
|
@@ -11680,7 +10566,7 @@ async function cmdBrand(argv) {
|
|
|
11680
10566
|
throw new UsageError(`not a URL: ${raw}`);
|
|
11681
10567
|
}
|
|
11682
10568
|
const r = createReporter(flags.json === true);
|
|
11683
|
-
const out =
|
|
10569
|
+
const out = resolve10(strFlag(flags, "out") ?? "BRAND.md");
|
|
11684
10570
|
r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
|
|
11685
10571
|
const [designText, llmsText] = await Promise.all([
|
|
11686
10572
|
fetchText(`${origin}/design.md`),
|
|
@@ -11714,7 +10600,7 @@ async function cmdBrand(argv) {
|
|
|
11714
10600
|
composition,
|
|
11715
10601
|
llms?.claim ?? design?.description ?? null
|
|
11716
10602
|
);
|
|
11717
|
-
await
|
|
10603
|
+
await writeFile9(out, md);
|
|
11718
10604
|
const k = composition.kit;
|
|
11719
10605
|
r.done(
|
|
11720
10606
|
{
|
|
@@ -11829,8 +10715,8 @@ async function recordHosted(input) {
|
|
|
11829
10715
|
}
|
|
11830
10716
|
|
|
11831
10717
|
// src/plugin/agentBrowser.ts
|
|
11832
|
-
import { writeFile as
|
|
11833
|
-
import { resolve as
|
|
10718
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
10719
|
+
import { resolve as resolve11 } from "path";
|
|
11834
10720
|
function parseAgentBrowserLog(text) {
|
|
11835
10721
|
const records = [];
|
|
11836
10722
|
const problems = [];
|
|
@@ -12301,7 +11187,7 @@ async function cmdActions(argv) {
|
|
|
12301
11187
|
);
|
|
12302
11188
|
}
|
|
12303
11189
|
const r = createReporter(flags.json === true);
|
|
12304
|
-
const out =
|
|
11190
|
+
const out = resolve11(strFlag(flags, "out") ?? "actions.json");
|
|
12305
11191
|
const viewportFlag = strFlag(flags, "viewport");
|
|
12306
11192
|
let viewport;
|
|
12307
11193
|
if (viewportFlag) {
|
|
@@ -12309,8 +11195,8 @@ async function cmdActions(argv) {
|
|
|
12309
11195
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
12310
11196
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
12311
11197
|
}
|
|
12312
|
-
const { readFile:
|
|
12313
|
-
const text = await
|
|
11198
|
+
const { readFile: readFile13 } = await import("fs/promises");
|
|
11199
|
+
const text = await readFile13(resolve11(input), "utf8");
|
|
12314
11200
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
12315
11201
|
for (const p of problems) r.log(` ${p}`);
|
|
12316
11202
|
const result = convertAgentBrowser(records, {
|
|
@@ -12327,7 +11213,7 @@ async function cmdActions(argv) {
|
|
|
12327
11213
|
);
|
|
12328
11214
|
return EXIT_ERROR;
|
|
12329
11215
|
}
|
|
12330
|
-
await
|
|
11216
|
+
await writeFile10(out, `${JSON.stringify(result.actions, null, 2)}
|
|
12331
11217
|
`);
|
|
12332
11218
|
r.done(
|
|
12333
11219
|
{ ok: true, out, url: result.actions.url ?? null, ...summary2(result) },
|
|
@@ -12347,9 +11233,9 @@ function summary2(result) {
|
|
|
12347
11233
|
}
|
|
12348
11234
|
async function cmdActionsScript(input, flags) {
|
|
12349
11235
|
const r = createReporter(flags.json === true);
|
|
12350
|
-
const { readFile:
|
|
11236
|
+
const { readFile: readFile13 } = await import("fs/promises");
|
|
12351
11237
|
const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
|
|
12352
|
-
const raw = JSON.parse(await
|
|
11238
|
+
const raw = JSON.parse(await readFile13(resolve11(input), "utf8"));
|
|
12353
11239
|
const errors = validateActions(raw);
|
|
12354
11240
|
if (errors.length) throw new UsageError(`${input}:
|
|
12355
11241
|
${errors.join("\n ")}`);
|
|
@@ -12359,9 +11245,9 @@ async function cmdActionsScript(input, flags) {
|
|
|
12359
11245
|
}
|
|
12360
11246
|
|
|
12361
11247
|
// src/plugin/ingest.ts
|
|
12362
|
-
import { existsSync as
|
|
12363
|
-
import { copyFile, mkdir as
|
|
12364
|
-
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";
|
|
12365
11251
|
import { inflateRawSync } from "zlib";
|
|
12366
11252
|
import {
|
|
12367
11253
|
cursorFromPlaywrightTrace,
|
|
@@ -12449,7 +11335,7 @@ async function readTraceFile(path, offsetMs) {
|
|
|
12449
11335
|
);
|
|
12450
11336
|
}
|
|
12451
11337
|
if (kind === "zip") {
|
|
12452
|
-
const bytes = new Uint8Array(await
|
|
11338
|
+
const bytes = new Uint8Array(await readFile11(path));
|
|
12453
11339
|
const entry = playwrightTraceEntry(zipEntries(bytes));
|
|
12454
11340
|
if (!entry) {
|
|
12455
11341
|
throw new UsageError(
|
|
@@ -12462,7 +11348,7 @@ async function readTraceFile(path, offsetMs) {
|
|
|
12462
11348
|
);
|
|
12463
11349
|
return cursorFromPlaywrightTrace(text2, { offsetMs });
|
|
12464
11350
|
}
|
|
12465
|
-
const text = await
|
|
11351
|
+
const text = await readFile11(path, "utf8");
|
|
12466
11352
|
return cursorFromTrace(basename9(path), text, { offsetMs });
|
|
12467
11353
|
}
|
|
12468
11354
|
function parseViewport(v) {
|
|
@@ -12478,10 +11364,10 @@ async function cmdIngest(argv) {
|
|
|
12478
11364
|
throw new UsageError(
|
|
12479
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]"
|
|
12480
11366
|
);
|
|
12481
|
-
const videoPath =
|
|
12482
|
-
if (!
|
|
11367
|
+
const videoPath = resolve12(source);
|
|
11368
|
+
if (!existsSync19(videoPath)) throw new UsageError(`${source}: no such file`);
|
|
12483
11369
|
const r = createReporter(flags.json === true);
|
|
12484
|
-
const outDir =
|
|
11370
|
+
const outDir = resolve12(strFlag(flags, "out") ?? "take");
|
|
12485
11371
|
const offsetMs = numFlag(flags, "offset", 0);
|
|
12486
11372
|
const viewportFlag = parseViewport(strFlag(flags, "viewport"));
|
|
12487
11373
|
const cursorPath = strFlag(flags, "cursor");
|
|
@@ -12503,18 +11389,18 @@ async function cmdIngest(argv) {
|
|
|
12503
11389
|
}
|
|
12504
11390
|
let trace = null;
|
|
12505
11391
|
if (cursorPath) {
|
|
12506
|
-
trace = await readTraceFile(
|
|
11392
|
+
trace = await readTraceFile(resolve12(cursorPath), offsetMs);
|
|
12507
11393
|
for (const n of trace.notes) r.log(`note: ${n}`);
|
|
12508
11394
|
}
|
|
12509
|
-
if (
|
|
11395
|
+
if (existsSync19(join21(outDir, "meta.json"))) {
|
|
12510
11396
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
12511
11397
|
r.log(
|
|
12512
11398
|
`note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
|
|
12513
11399
|
);
|
|
12514
11400
|
}
|
|
12515
|
-
await
|
|
11401
|
+
await mkdir7(outDir, { recursive: true });
|
|
12516
11402
|
const paths = await ensureTakeDir(outDir);
|
|
12517
|
-
const recording =
|
|
11403
|
+
const recording = join21(outDir, `recording.${probed.ext}`);
|
|
12518
11404
|
r.log("copying the recording\u2026");
|
|
12519
11405
|
const remuxed = await copyRecording(videoPath, recording, probed.ext);
|
|
12520
11406
|
const viewport = viewportFlag ?? trace?.viewport ?? {
|
|
@@ -12522,7 +11408,7 @@ async function cmdIngest(argv) {
|
|
|
12522
11408
|
height: probed.height
|
|
12523
11409
|
};
|
|
12524
11410
|
const events = trace?.events ?? [];
|
|
12525
|
-
const fileStat = await
|
|
11411
|
+
const fileStat = await stat2(videoPath);
|
|
12526
11412
|
const meta = {
|
|
12527
11413
|
dpr: 1,
|
|
12528
11414
|
zoom: 1,
|
|
@@ -12582,19 +11468,19 @@ async function cmdIngest(argv) {
|
|
|
12582
11468
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
12583
11469
|
import {
|
|
12584
11470
|
cpSync,
|
|
12585
|
-
existsSync as
|
|
11471
|
+
existsSync as existsSync21,
|
|
12586
11472
|
mkdirSync as mkdirSync6,
|
|
12587
11473
|
readFileSync as readFileSync10,
|
|
12588
11474
|
rmSync as rmSync3,
|
|
12589
11475
|
writeFileSync as writeFileSync6
|
|
12590
11476
|
} from "fs";
|
|
12591
11477
|
import { homedir as homedir2 } from "os";
|
|
12592
|
-
import { join as
|
|
11478
|
+
import { join as join23 } from "path";
|
|
12593
11479
|
import { chromium as chromium2 } from "playwright";
|
|
12594
11480
|
|
|
12595
11481
|
// src/plugin/machine.ts
|
|
12596
|
-
import { existsSync as
|
|
12597
|
-
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";
|
|
12598
11484
|
var AGENT_NAMES = [
|
|
12599
11485
|
"claude",
|
|
12600
11486
|
"cursor",
|
|
@@ -12614,17 +11500,17 @@ var HOME_DIRS = {
|
|
|
12614
11500
|
copilot: [".copilot"]
|
|
12615
11501
|
};
|
|
12616
11502
|
function detectAgents(root, opts = {}) {
|
|
12617
|
-
const exists = opts.exists ??
|
|
11503
|
+
const exists = opts.exists ?? existsSync20;
|
|
12618
11504
|
const out = [];
|
|
12619
11505
|
for (const agent of AGENT_NAMES) {
|
|
12620
11506
|
for (const rel of PROJECT_DIRS[agent]) {
|
|
12621
|
-
const dir =
|
|
11507
|
+
const dir = join22(root, rel);
|
|
12622
11508
|
if (exists(dir)) out.push({ agent, dir, scope: "project" });
|
|
12623
11509
|
}
|
|
12624
|
-
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"))) {
|
|
12625
11511
|
out.push({
|
|
12626
11512
|
agent,
|
|
12627
|
-
dir:
|
|
11513
|
+
dir: join22(root, ".github", "copilot"),
|
|
12628
11514
|
scope: "project"
|
|
12629
11515
|
});
|
|
12630
11516
|
}
|
|
@@ -12632,7 +11518,7 @@ function detectAgents(root, opts = {}) {
|
|
|
12632
11518
|
if (opts.global && opts.home) {
|
|
12633
11519
|
for (const agent of AGENT_NAMES) {
|
|
12634
11520
|
for (const rel of HOME_DIRS[agent]) {
|
|
12635
|
-
const dir =
|
|
11521
|
+
const dir = join22(opts.home, rel);
|
|
12636
11522
|
if (exists(dir)) out.push({ agent, dir, scope: "global" });
|
|
12637
11523
|
}
|
|
12638
11524
|
}
|
|
@@ -12650,18 +11536,18 @@ function agentsFromFlag(value, root) {
|
|
|
12650
11536
|
return {
|
|
12651
11537
|
homes: wanted.map((agent) => ({
|
|
12652
11538
|
agent,
|
|
12653
|
-
dir:
|
|
11539
|
+
dir: join22(root, PROJECT_DIRS[agent][0]),
|
|
12654
11540
|
scope: "project"
|
|
12655
11541
|
}))
|
|
12656
11542
|
};
|
|
12657
11543
|
}
|
|
12658
11544
|
function skillsDirFor(home) {
|
|
12659
|
-
return
|
|
11545
|
+
return join22(home.dir, "skills");
|
|
12660
11546
|
}
|
|
12661
11547
|
function fallbackSkillsHome(root) {
|
|
12662
|
-
return { agent: "codex", dir:
|
|
11548
|
+
return { agent: "codex", dir: join22(root, ".agents"), scope: "project" };
|
|
12663
11549
|
}
|
|
12664
|
-
function installedSkills(dir, exists =
|
|
11550
|
+
function installedSkills(dir, exists = existsSync20, list = (p) => {
|
|
12665
11551
|
try {
|
|
12666
11552
|
return readdirSync5(p);
|
|
12667
11553
|
} catch {
|
|
@@ -12669,7 +11555,7 @@ function installedSkills(dir, exists = existsSync22, list = (p) => {
|
|
|
12669
11555
|
}
|
|
12670
11556
|
}) {
|
|
12671
11557
|
if (!exists(dir)) return [];
|
|
12672
|
-
return list(dir).filter((name) => exists(
|
|
11558
|
+
return list(dir).filter((name) => exists(join22(dir, name, "SKILL.md"))).sort();
|
|
12673
11559
|
}
|
|
12674
11560
|
var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
|
|
12675
11561
|
var RULES_END = "<!-- vos:end -->";
|
|
@@ -12702,10 +11588,10 @@ ${RULES_BLOCK}
|
|
|
12702
11588
|
}
|
|
12703
11589
|
return { text, changed: text !== current };
|
|
12704
11590
|
}
|
|
12705
|
-
function rulesFileFor(root, exists =
|
|
12706
|
-
const agents =
|
|
11591
|
+
function rulesFileFor(root, exists = existsSync20) {
|
|
11592
|
+
const agents = join22(root, "AGENTS.md");
|
|
12707
11593
|
if (exists(agents)) return agents;
|
|
12708
|
-
const claude =
|
|
11594
|
+
const claude = join22(root, "CLAUDE.md");
|
|
12709
11595
|
if (exists(claude)) return claude;
|
|
12710
11596
|
return agents;
|
|
12711
11597
|
}
|
|
@@ -12714,14 +11600,14 @@ function chromeCandidates(platform, env) {
|
|
|
12714
11600
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
12715
11601
|
if (platform === "win32")
|
|
12716
11602
|
return [
|
|
12717
|
-
|
|
11603
|
+
join22(
|
|
12718
11604
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
12719
11605
|
"Google",
|
|
12720
11606
|
"Chrome",
|
|
12721
11607
|
"Application",
|
|
12722
11608
|
"chrome.exe"
|
|
12723
11609
|
),
|
|
12724
|
-
|
|
11610
|
+
join22(
|
|
12725
11611
|
env["LOCALAPPDATA"] ?? "",
|
|
12726
11612
|
"Google",
|
|
12727
11613
|
"Chrome",
|
|
@@ -12806,20 +11692,20 @@ function doctorReport(facts) {
|
|
|
12806
11692
|
return { ok: nodeOk && browserOk, lines, next_step, facts };
|
|
12807
11693
|
}
|
|
12808
11694
|
function bundledSkillsDir(from) {
|
|
12809
|
-
let dir =
|
|
11695
|
+
let dir = dirname8(from);
|
|
12810
11696
|
for (let i = 0; i < 6; i++) {
|
|
12811
|
-
const pkg =
|
|
12812
|
-
if (
|
|
11697
|
+
const pkg = join22(dir, "package.json");
|
|
11698
|
+
if (existsSync20(pkg)) {
|
|
12813
11699
|
try {
|
|
12814
11700
|
const name = JSON.parse(readFileSync9(pkg, "utf8")).name;
|
|
12815
11701
|
if (name === "@vosjs/cli") {
|
|
12816
|
-
const skills =
|
|
12817
|
-
return
|
|
11702
|
+
const skills = join22(dir, "skills");
|
|
11703
|
+
return existsSync20(skills) && statSync7(skills).isDirectory() ? skills : null;
|
|
12818
11704
|
}
|
|
12819
11705
|
} catch {
|
|
12820
11706
|
}
|
|
12821
11707
|
}
|
|
12822
|
-
const up =
|
|
11708
|
+
const up = dirname8(dir);
|
|
12823
11709
|
if (up === dir) break;
|
|
12824
11710
|
dir = up;
|
|
12825
11711
|
}
|
|
@@ -12845,7 +11731,7 @@ function npx() {
|
|
|
12845
11731
|
function bundledChromium() {
|
|
12846
11732
|
try {
|
|
12847
11733
|
const p = chromium2.executablePath();
|
|
12848
|
-
return p &&
|
|
11734
|
+
return p && existsSync21(p) ? p : null;
|
|
12849
11735
|
} catch {
|
|
12850
11736
|
return null;
|
|
12851
11737
|
}
|
|
@@ -12854,7 +11740,7 @@ function probe2() {
|
|
|
12854
11740
|
return probeBrowser({
|
|
12855
11741
|
platform: process.platform,
|
|
12856
11742
|
env: process.env,
|
|
12857
|
-
exists:
|
|
11743
|
+
exists: existsSync21,
|
|
12858
11744
|
bundled: bundledChromium
|
|
12859
11745
|
});
|
|
12860
11746
|
}
|
|
@@ -12958,7 +11844,7 @@ async function cmdSetup(argv) {
|
|
|
12958
11844
|
const dest = skillsDirFor(home);
|
|
12959
11845
|
mkdirSync6(dest, { recursive: true });
|
|
12960
11846
|
for (const name of installedSkills(bundled)) {
|
|
12961
|
-
cpSync(
|
|
11847
|
+
cpSync(join23(bundled, name), join23(dest, name), { recursive: true });
|
|
12962
11848
|
}
|
|
12963
11849
|
}
|
|
12964
11850
|
skillsVia = "bundled";
|
|
@@ -12993,7 +11879,7 @@ async function cmdSetup(argv) {
|
|
|
12993
11879
|
let rules = null;
|
|
12994
11880
|
if (!noRules) {
|
|
12995
11881
|
const file = rulesFileFor(root);
|
|
12996
|
-
const existing =
|
|
11882
|
+
const existing = existsSync21(file) ? readFileSync10(file, "utf8") : null;
|
|
12997
11883
|
const { text, changed } = applyRulesBlock(existing);
|
|
12998
11884
|
if (changed) writeFileSync6(file, text);
|
|
12999
11885
|
rules = { file, changed };
|
|
@@ -13107,7 +11993,7 @@ async function cmdWhoami(argv) {
|
|
|
13107
11993
|
function cmdLogout(argv) {
|
|
13108
11994
|
const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
13109
11995
|
const r = reporterFor(flags);
|
|
13110
|
-
const had =
|
|
11996
|
+
const had = existsSync21(CREDENTIALS_PATH);
|
|
13111
11997
|
rmSync3(CREDENTIALS_PATH, { force: true });
|
|
13112
11998
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
13113
11999
|
r.done(
|
|
@@ -13139,7 +12025,6 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
|
|
|
13139
12025
|
"media",
|
|
13140
12026
|
"print",
|
|
13141
12027
|
"yes",
|
|
13142
|
-
"composed",
|
|
13143
12028
|
"check",
|
|
13144
12029
|
"motion",
|
|
13145
12030
|
"leader",
|
|
@@ -13161,13 +12046,12 @@ Take pipeline
|
|
|
13161
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]
|
|
13162
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]
|
|
13163
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]
|
|
13164
|
-
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]
|
|
13165
12050
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
13166
12051
|
vos brand <url> [--out BRAND.md] [--json]
|
|
13167
12052
|
vos open <take> [--studio <url>] [--print]
|
|
13168
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]
|
|
13169
12054
|
vos validate <actions.json|take|kit.json> [--picture] [--json]
|
|
13170
|
-
vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
|
|
13171
12055
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
13172
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]
|
|
13173
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
|
|
@@ -13329,47 +12213,28 @@ recording cap, read live from GET /api/limits (30 min when the origin is
|
|
|
13329
12213
|
unreachable): the capture stops there and says so in one line.
|
|
13330
12214
|
frames captures PNG stills at OUTPUT times (seconds or % of duration);
|
|
13331
12215
|
--at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
|
|
13332
|
-
the agent's eyes
|
|
12216
|
+
the agent's eyes (--size WxH for an exact size). render
|
|
13333
12217
|
--range a..b --draft spot-checks a doc edit in seconds (half res, low
|
|
13334
12218
|
bitrate; range in OUTPUT seconds); a --range render keeps its audio.
|
|
13335
|
-
deliver renders the take to a release's destinations in one pass \u2014
|
|
13336
|
-
channel specs (schema/channel-specs.json, verified sizes for
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
(px, bytes, duration; misses land in skipped[] with the reason),
|
|
13340
|
-
kit.json beside the assets is the manifest.
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
role or its own ground, and with no brand the house gradient.
|
|
13355
|
-
Screenshot-genre stills never take a look.
|
|
13356
|
-
A POSTER is a document, never a template: a plain take whose card sits
|
|
13357
|
-
where the poster wants it (frame.inset), leans (the tilt track), carries
|
|
13358
|
-
the words and the mark as clips (ids stage-title, stage-kicker,
|
|
13359
|
-
stage-brand, stage-mark) and ends on a trailing freeze whose start is the
|
|
13360
|
-
still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
|
|
13361
|
-
CWS tile + marquee, GitHub social preview) render from the poster
|
|
13362
|
-
document of their aspect CLASS (landscape, square, portrait, tile), found
|
|
13363
|
-
beside the take as poster/<class>/doc.json (poster/doc.json serves every
|
|
13364
|
-
class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
|
|
13365
|
-
the document's rest; kit.json records source poster, the class, the file,
|
|
13366
|
-
the vos it tracks, the shot rect and the text boxes read FROM the
|
|
13367
|
-
document. A class with no document is the take's own frame, said once.
|
|
13368
|
-
deliver renders and verifies; it composes nothing. The composition is
|
|
13369
|
-
made with plan --style <poster>, which copies a poster's layout onto a
|
|
13370
|
-
take (its card placement, its stage clips with the release's words
|
|
13371
|
-
patched in from LAUNCH.md's headline and kicker roles or --headline and
|
|
13372
|
-
--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.
|
|
13373
12238
|
The cut's MOTION is the document's too, in ONE vocabulary: every visual
|
|
13374
12239
|
thing (the card, a text, image or video clip, a prop) carries anim.enter,
|
|
13375
12240
|
anim.exit and anim.idle; the output lasts until the last clip ends, and
|
|
@@ -13393,40 +12258,21 @@ deleted on a refresh. deliver applies each destination's MECHANICS and
|
|
|
13393
12258
|
nothing more: the README loop drops the card's motion, every clip a
|
|
13394
12259
|
template placed and every sound; a channel that autoplays muted drops the
|
|
13395
12260
|
bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
|
|
13396
|
-
the camera.
|
|
13397
|
-
destinations (CWS screenshots, the PH gallery) are the real page at that
|
|
13398
|
-
moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
|
|
13399
|
-
policy); --composed keeps the cut's camera and chrome instead. --set
|
|
12261
|
+
the camera. --set
|
|
13400
12262
|
path=value overrides the doc in memory for every render here (the user's
|
|
13401
|
-
sets apply last).
|
|
13402
|
-
directory, then vos validate <kit.json> re-measures every
|
|
13403
|
-
bytes against the channel specs; --picture adds what each
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
separation (a light card on a light ground with no shadow), halfsize (a
|
|
13408
|
-
tile that loses its edges when the store shrinks it), and, where the kit
|
|
13409
|
-
records its text boxes, sliced, safe and contrast (APCA Lc 60/75); a
|
|
13410
|
-
video's first and last frames are read through ffmpeg. Every finding
|
|
13411
|
-
carries a code, a severity, a fix hint and a box; an error fails the
|
|
13412
|
-
verdict beside the spec problems.
|
|
13413
|
-
judge puts the kit beside its REFERENCES: for every still that has a
|
|
13414
|
-
reference of its role in the manifest (a template family, a card genre),
|
|
13415
|
-
two sheets at a common height (the asset left, then right, so order
|
|
13416
|
-
cannot bias the call) and the rubric in words, plus judge.json, a slot
|
|
13417
|
-
per pair the judge fills (win true, false or null for a tie, with the
|
|
13418
|
-
rule numbers). No model runs inside the verb: the skill judges the
|
|
13419
|
-
sheets pairwise, both orders, and the verb reports the win rate beside
|
|
13420
|
-
the spec and picture counts. The reference set is the maker's own
|
|
13421
|
-
fixture folder with a MANIFEST.json (id, file, role, layout, facts,
|
|
13422
|
-
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.
|
|
13423
12269
|
brand writes the product's BRAND.md, witnessed: it reads /design.md when
|
|
13424
12270
|
the site publishes one (the convention beside /llms.txt: fonts, logo assets,
|
|
13425
12271
|
an avoid list), /llms.txt for the name and the claim, then the page itself
|
|
13426
12272
|
in a browser (the body ground, the surfaces, the h1 and body faces and inks,
|
|
13427
12273
|
the accent buttons and links agree on, theme-color, the icons, og:image).
|
|
13428
|
-
The frontmatter carries the roles
|
|
13429
|
-
|
|
12274
|
+
The frontmatter carries the brand's roles (bgA/bgB/bgC, ink, accent,
|
|
12275
|
+
fontDisplay, logoUrl); the prose says where every value came
|
|
13430
12276
|
from and quotes the site's own avoid list. Resolve the brand BEFORE any
|
|
13431
12277
|
asset is authored; file it with vos recipe push BRAND.md --folder <slug>.
|
|
13432
12278
|
digest is how an agent SEES a recording before it cuts: writes digest/ with
|
|
@@ -13482,7 +12328,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
13482
12328
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
13483
12329
|
`;
|
|
13484
12330
|
async function loadActions(file) {
|
|
13485
|
-
const raw = JSON.parse(await
|
|
12331
|
+
const raw = JSON.parse(await readFile12(file, "utf8"));
|
|
13486
12332
|
const errors = validateActions(raw);
|
|
13487
12333
|
if (errors.length)
|
|
13488
12334
|
throw new UsageError(`invalid actions file:
|
|
@@ -13544,8 +12390,8 @@ function takeBrowserArgs(multi) {
|
|
|
13544
12390
|
function takeStorageState(flags) {
|
|
13545
12391
|
const raw = strFlag(flags, "storage-state");
|
|
13546
12392
|
if (!raw) return void 0;
|
|
13547
|
-
const path =
|
|
13548
|
-
if (!
|
|
12393
|
+
const path = resolve13(raw);
|
|
12394
|
+
if (!existsSync22(path))
|
|
13549
12395
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
13550
12396
|
return path;
|
|
13551
12397
|
}
|
|
@@ -13558,7 +12404,7 @@ function takeSessionName(flags, storageState) {
|
|
|
13558
12404
|
throw new UsageError(
|
|
13559
12405
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
13560
12406
|
);
|
|
13561
|
-
if (!
|
|
12407
|
+
if (!existsSync22(sessionDir(name)))
|
|
13562
12408
|
throw new UsageError(
|
|
13563
12409
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
13564
12410
|
);
|
|
@@ -13627,7 +12473,7 @@ async function cmdRecord(argv) {
|
|
|
13627
12473
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13628
12474
|
if (!url)
|
|
13629
12475
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13630
|
-
const outDir =
|
|
12476
|
+
const outDir = resolve13(strFlag(flags, "out") ?? "take");
|
|
13631
12477
|
const storageState = takeStorageState(flags);
|
|
13632
12478
|
const sessionName = takeSessionName(flags, storageState);
|
|
13633
12479
|
const browserArgs = takeBrowserArgs(multi);
|
|
@@ -13781,13 +12627,13 @@ ${lines.join("\n")}
|
|
|
13781
12627
|
}
|
|
13782
12628
|
const paths = takePaths(outDir);
|
|
13783
12629
|
const prepare = async () => {
|
|
13784
|
-
if (
|
|
12630
|
+
if (existsSync22(join24(outDir, "meta.json"))) {
|
|
13785
12631
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13786
12632
|
r.log(
|
|
13787
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" : "")
|
|
13788
12634
|
);
|
|
13789
12635
|
}
|
|
13790
|
-
await
|
|
12636
|
+
await mkdir8(outDir, { recursive: true });
|
|
13791
12637
|
await ensureTakeDir(outDir);
|
|
13792
12638
|
};
|
|
13793
12639
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13846,7 +12692,7 @@ ${lines.join("\n")}
|
|
|
13846
12692
|
},
|
|
13847
12693
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
13848
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}
|
|
13849
|
-
Next: edit ${
|
|
12695
|
+
Next: edit ${join24(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
|
|
13850
12696
|
);
|
|
13851
12697
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
13852
12698
|
} finally {
|
|
@@ -13870,9 +12716,9 @@ async function cmdCreate2(argv) {
|
|
|
13870
12716
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13871
12717
|
if (!url)
|
|
13872
12718
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13873
|
-
const outDir =
|
|
12719
|
+
const outDir = resolve13(strFlag(flags, "out") ?? "take");
|
|
13874
12720
|
const format = videoFormat(strFlag(flags, "format"), positionals[0]);
|
|
13875
|
-
const out = positionals[0] ??
|
|
12721
|
+
const out = positionals[0] ?? join24(outDir, `out.${format}`);
|
|
13876
12722
|
const parallel = numFlag(flags, "parallel", 1);
|
|
13877
12723
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
13878
12724
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -13885,13 +12731,13 @@ async function cmdCreate2(argv) {
|
|
|
13885
12731
|
const headers = takeHeaders(multi);
|
|
13886
12732
|
const paths = takePaths(outDir);
|
|
13887
12733
|
const prepare = async () => {
|
|
13888
|
-
if (
|
|
12734
|
+
if (existsSync22(join24(outDir, "meta.json"))) {
|
|
13889
12735
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13890
12736
|
r.log(
|
|
13891
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" : "")
|
|
13892
12738
|
);
|
|
13893
12739
|
}
|
|
13894
|
-
await
|
|
12740
|
+
await mkdir8(outDir, { recursive: true });
|
|
13895
12741
|
await ensureTakeDir(outDir);
|
|
13896
12742
|
};
|
|
13897
12743
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13992,24 +12838,24 @@ async function cmdPlan(argv) {
|
|
|
13992
12838
|
);
|
|
13993
12839
|
const r = createReporter(flags.json === true);
|
|
13994
12840
|
if (flags.fresh === true) {
|
|
13995
|
-
await
|
|
12841
|
+
await rm2(join24(dir, "doc.json"), { force: true });
|
|
13996
12842
|
r.log("note: --fresh discarded the existing doc.json");
|
|
13997
12843
|
}
|
|
13998
12844
|
let reuse;
|
|
13999
12845
|
if (flags.reuse === true) {
|
|
14000
|
-
const from =
|
|
14001
|
-
if (!
|
|
12846
|
+
const from = resolve13(strFlag(flags, "from") ?? join24(dir, PREV_DOC_NAME));
|
|
12847
|
+
if (!existsSync22(from)) {
|
|
14002
12848
|
throw new UsageError(
|
|
14003
12849
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
14004
12850
|
);
|
|
14005
12851
|
}
|
|
14006
12852
|
reuse = {
|
|
14007
12853
|
from,
|
|
14008
|
-
doc: JSON.parse(await
|
|
12854
|
+
doc: JSON.parse(await readFile12(from, "utf8"))
|
|
14009
12855
|
};
|
|
14010
12856
|
}
|
|
14011
12857
|
const style = await resolveStyleRef(flags);
|
|
14012
|
-
const hasDoc =
|
|
12858
|
+
const hasDoc = existsSync22(join24(dir, "doc.json"));
|
|
14013
12859
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
14014
12860
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
14015
12861
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
@@ -14055,7 +12901,7 @@ async function cmdPlan(argv) {
|
|
|
14055
12901
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
14056
12902
|
r.done(
|
|
14057
12903
|
{
|
|
14058
|
-
take:
|
|
12904
|
+
take: resolve13(dir),
|
|
14059
12905
|
fresh: s.fresh,
|
|
14060
12906
|
cursorKept: s.cursorKept,
|
|
14061
12907
|
zoomAuto: s.zoomAuto,
|
|
@@ -14067,7 +12913,7 @@ async function cmdPlan(argv) {
|
|
|
14067
12913
|
...s.motion ? { motion: s.motion } : {},
|
|
14068
12914
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
14069
12915
|
},
|
|
14070
|
-
`${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}`
|
|
14071
12917
|
);
|
|
14072
12918
|
return EXIT_OK;
|
|
14073
12919
|
}
|
|
@@ -14190,7 +13036,7 @@ async function cmdRender(argv) {
|
|
|
14190
13036
|
);
|
|
14191
13037
|
const r = createReporter(flags.json === true);
|
|
14192
13038
|
const format = videoFormat(strFlag(flags, "format"), positionals[1]);
|
|
14193
|
-
const out = positionals[1] ??
|
|
13039
|
+
const out = positionals[1] ?? join24(dir, `out.${format}`);
|
|
14194
13040
|
const parallel = numFlag(flags, "parallel", 1);
|
|
14195
13041
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
14196
13042
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -14328,7 +13174,7 @@ async function cmdDeliver(argv) {
|
|
|
14328
13174
|
const toRaw = strFlag(flags, "to");
|
|
14329
13175
|
if (!dir || !toRaw)
|
|
14330
13176
|
throw new UsageError(
|
|
14331
|
-
"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)"
|
|
14332
13178
|
);
|
|
14333
13179
|
const r = createReporter(flags.json === true);
|
|
14334
13180
|
if (!isTakeDir(dir))
|
|
@@ -14355,17 +13201,6 @@ async function cmdDeliver(argv) {
|
|
|
14355
13201
|
}
|
|
14356
13202
|
const take = await loadTake(dir);
|
|
14357
13203
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
14358
|
-
const duration = docOutputDuration3(take.doc);
|
|
14359
|
-
const timesRaw = strFlag(flags, "times");
|
|
14360
|
-
let times;
|
|
14361
|
-
if (timesRaw !== void 0) {
|
|
14362
|
-
try {
|
|
14363
|
-
const doc = take.doc;
|
|
14364
|
-
times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes2(s, duration)[0]);
|
|
14365
|
-
} catch (e) {
|
|
14366
|
-
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
14367
|
-
}
|
|
14368
|
-
}
|
|
14369
13204
|
const parallel = numFlag(flags, "parallel", 1);
|
|
14370
13205
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
14371
13206
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -14380,20 +13215,15 @@ async function cmdDeliver(argv) {
|
|
|
14380
13215
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
14381
13216
|
}
|
|
14382
13217
|
r.log(`look: ${lookPick.from}`);
|
|
14383
|
-
const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
|
|
14384
|
-
if (launch) r.log(`launch: ${launch.file}`);
|
|
14385
13218
|
const browser = await launchBrowser();
|
|
14386
13219
|
try {
|
|
14387
13220
|
const result = await deliverTake(browser, dir, {
|
|
14388
13221
|
look: lookPick.look,
|
|
14389
|
-
launchRoles: launch?.roles ?? null,
|
|
14390
13222
|
channels,
|
|
14391
13223
|
outDir: strFlag(flags, "out"),
|
|
14392
13224
|
release: strFlag(flags, "release"),
|
|
14393
|
-
times,
|
|
14394
13225
|
range,
|
|
14395
13226
|
parallel,
|
|
14396
|
-
composed: hasFlag(flags, "composed"),
|
|
14397
13227
|
overrides: {
|
|
14398
13228
|
set: multi.set,
|
|
14399
13229
|
background: strFlag(flags, "background"),
|
|
@@ -14410,7 +13240,7 @@ async function cmdDeliver(argv) {
|
|
|
14410
13240
|
});
|
|
14411
13241
|
const { kit } = result;
|
|
14412
13242
|
const assetLines = kit.assets.map(
|
|
14413
|
-
(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)`
|
|
14414
13244
|
);
|
|
14415
13245
|
const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
|
|
14416
13246
|
r.done(
|
|
@@ -14423,7 +13253,7 @@ async function cmdDeliver(argv) {
|
|
|
14423
13253
|
`Wrote ${kit.assets.length} asset(s) + kit.json to ${result.outDir}` + (assetLines.length ? `
|
|
14424
13254
|
${assetLines.join("\n ")}` : "") + (skippedLines.length ? `
|
|
14425
13255
|
${skippedLines.join("\n ")}` : "") + `
|
|
14426
|
-
|
|
13256
|
+
Uploads stay manual: hand the human this directory and the manifest.`
|
|
14427
13257
|
);
|
|
14428
13258
|
return kit.assets.length > 0 ? EXIT_OK : EXIT_ERROR;
|
|
14429
13259
|
} finally {
|
|
@@ -14437,15 +13267,15 @@ async function resolveStyleRef(flags) {
|
|
|
14437
13267
|
}
|
|
14438
13268
|
async function resolveDocRef(ref, flags, what) {
|
|
14439
13269
|
const styleRef = ref;
|
|
14440
|
-
const file =
|
|
13270
|
+
const file = existsSync22(styleRef) ? resolve13(
|
|
14441
13271
|
styleRef,
|
|
14442
|
-
|
|
13272
|
+
existsSync22(join24(styleRef, "doc.json")) ? "doc.json" : ""
|
|
14443
13273
|
) : null;
|
|
14444
|
-
if (file &&
|
|
13274
|
+
if (file && existsSync22(file)) {
|
|
14445
13275
|
return {
|
|
14446
13276
|
from: file,
|
|
14447
|
-
doc:
|
|
14448
|
-
JSON.parse(await
|
|
13277
|
+
doc: migrateHostedDoc4(
|
|
13278
|
+
JSON.parse(await readFile12(file, "utf8"))
|
|
14449
13279
|
)
|
|
14450
13280
|
};
|
|
14451
13281
|
}
|
|
@@ -14473,7 +13303,7 @@ async function resolveDocRef(ref, flags, what) {
|
|
|
14473
13303
|
throw new UsageError(`${what}: ${styleRef} carries no doc (is it a take?)`);
|
|
14474
13304
|
return {
|
|
14475
13305
|
from: `${origin}/vos/${styleRef}`,
|
|
14476
|
-
doc:
|
|
13306
|
+
doc: migrateHostedDoc4(doc.body)
|
|
14477
13307
|
};
|
|
14478
13308
|
}
|
|
14479
13309
|
async function officialTemplate(origin, ref) {
|
|
@@ -14497,7 +13327,7 @@ async function cmdDigest(argv) {
|
|
|
14497
13327
|
const take = await loadTake(dir);
|
|
14498
13328
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
14499
13329
|
const transcriptPath = strFlag(flags, "transcript");
|
|
14500
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
13330
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile12(transcriptPath, "utf8"))) : null;
|
|
14501
13331
|
const style = await resolveStyleRef(flags);
|
|
14502
13332
|
const noFrames = flags["no-frames"] === true;
|
|
14503
13333
|
let browser = null;
|
|
@@ -14528,7 +13358,7 @@ async function cmdDigest(argv) {
|
|
|
14528
13358
|
kinds,
|
|
14529
13359
|
frames: d.images.full,
|
|
14530
13360
|
crops: d.images.crop,
|
|
14531
|
-
sheet: d.images.sheet ?
|
|
13361
|
+
sheet: d.images.sheet ? join24(result.outDir, d.images.sheet) : null,
|
|
14532
13362
|
scenes: kinds.scene ?? 0,
|
|
14533
13363
|
sourceDuration: d.take.sourceDuration,
|
|
14534
13364
|
outputDuration: d.take.outputDuration,
|
|
@@ -14556,14 +13386,14 @@ async function cmdOpen(argv) {
|
|
|
14556
13386
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
14557
13387
|
const r = createReporter(flags.json === true);
|
|
14558
13388
|
const take = await loadTake(dir);
|
|
14559
|
-
if (!
|
|
13389
|
+
if (!existsSync22(take.paths.recording)) {
|
|
14560
13390
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
14561
13391
|
}
|
|
14562
13392
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
14563
13393
|
/\/+$/,
|
|
14564
13394
|
""
|
|
14565
13395
|
);
|
|
14566
|
-
const server = await startTakeServer(
|
|
13396
|
+
const server = await startTakeServer(resolve13(dir), {});
|
|
14567
13397
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
14568
13398
|
r.event({ event: "open", server: server.base, url });
|
|
14569
13399
|
r.log(`take served at ${server.base}`);
|
|
@@ -14588,35 +13418,6 @@ async function cmdOpen(argv) {
|
|
|
14588
13418
|
});
|
|
14589
13419
|
return EXIT_OK;
|
|
14590
13420
|
}
|
|
14591
|
-
async function cmdJudge(argv) {
|
|
14592
|
-
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
|
|
14593
|
-
const target = positionals[0];
|
|
14594
|
-
const against = strFlag(flags, "against");
|
|
14595
|
-
if (!target || !against)
|
|
14596
|
-
throw new UsageError(
|
|
14597
|
-
"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)"
|
|
14598
|
-
);
|
|
14599
|
-
const r = createReporter(flags.json === true);
|
|
14600
|
-
const kitFile = target.endsWith("kit.json") ? target : join26(target, "kit.json");
|
|
14601
|
-
if (!existsSync24(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
14602
|
-
if (!existsSync24(against))
|
|
14603
|
-
throw new UsageError(`${against}: no reference manifest`);
|
|
14604
|
-
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
14605
|
-
const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
|
|
14606
|
-
const rate = winRate(verdicts);
|
|
14607
|
-
const lines = result.sheets.map(
|
|
14608
|
-
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
14609
|
-
);
|
|
14610
|
-
r.done(
|
|
14611
|
-
{ ...result, winRate: rate },
|
|
14612
|
-
`Wrote ${result.sheets.length} sheet pair(s) to ${result.outDir}` + (lines.length ? `
|
|
14613
|
-
${lines.join("\n ")}` : "") + (result.skipped.length ? `
|
|
14614
|
-
skipped: ${result.skipped.join("\n skipped: ")}` : "") + `
|
|
14615
|
-
Judge each pair both ways (the rubric is beside it) and fill ${result.verdictFile}` + (rate.judged ? `
|
|
14616
|
-
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%` : "")
|
|
14617
|
-
);
|
|
14618
|
-
return EXIT_OK;
|
|
14619
|
-
}
|
|
14620
13421
|
async function cmdCallout(argv) {
|
|
14621
13422
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
|
|
14622
13423
|
const dir = positionals[0];
|
|
@@ -14642,7 +13443,7 @@ async function cmdCallout(argv) {
|
|
|
14642
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`
|
|
14643
13444
|
);
|
|
14644
13445
|
const codeFile = strFlag(flags, "code-file");
|
|
14645
|
-
const code = codeFile ? await
|
|
13446
|
+
const code = codeFile ? await readFile12(codeFile, "utf8") : strFlag(flags, "code");
|
|
14646
13447
|
const words2 = {
|
|
14647
13448
|
kicker: strFlag(flags, "kicker"),
|
|
14648
13449
|
title: strFlag(flags, "title"),
|
|
@@ -14721,7 +13522,7 @@ async function cmdValidate(argv) {
|
|
|
14721
13522
|
const target = positionals[0];
|
|
14722
13523
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
14723
13524
|
const r = createReporter(flags.json === true);
|
|
14724
|
-
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;
|
|
14725
13526
|
if (kitTarget) {
|
|
14726
13527
|
const picture = flags.picture === true;
|
|
14727
13528
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -14753,9 +13554,9 @@ async function cmdValidate(argv) {
|
|
|
14753
13554
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
14754
13555
|
return EXIT_OK;
|
|
14755
13556
|
}
|
|
14756
|
-
if (!isTakeDir(target) &&
|
|
13557
|
+
if (!isTakeDir(target) && existsSync22(join24(target, "config.json"))) {
|
|
14757
13558
|
const parsed = JSON.parse(
|
|
14758
|
-
await
|
|
13559
|
+
await readFile12(join24(target, "config.json"), "utf8")
|
|
14759
13560
|
);
|
|
14760
13561
|
const pre = preflightConfig(parsed);
|
|
14761
13562
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -14796,7 +13597,7 @@ async function cmdValidate(argv) {
|
|
|
14796
13597
|
const take = await loadTake(target);
|
|
14797
13598
|
const problems = [];
|
|
14798
13599
|
const warnings = [];
|
|
14799
|
-
if (!
|
|
13600
|
+
if (!existsSync22(take.paths.recording)) {
|
|
14800
13601
|
problems.push("missing recording (re-run record)");
|
|
14801
13602
|
} else {
|
|
14802
13603
|
const mismatch = containerMismatch(
|
|
@@ -14889,7 +13690,7 @@ async function cmdPush3(argv) {
|
|
|
14889
13690
|
);
|
|
14890
13691
|
const refusal = takePushRefusal({
|
|
14891
13692
|
vos: strFlag(flags, "vos"),
|
|
14892
|
-
trackedVosId: readSyncState(
|
|
13693
|
+
trackedVosId: readSyncState(resolve13(dir))?.vosId ?? null,
|
|
14893
13694
|
programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
|
|
14894
13695
|
(name) => hasFlag(flags, name)
|
|
14895
13696
|
)
|
|
@@ -14905,7 +13706,7 @@ async function cmdPush3(argv) {
|
|
|
14905
13706
|
`--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
|
|
14906
13707
|
);
|
|
14907
13708
|
const claim = await pushTakeClaimable(
|
|
14908
|
-
|
|
13709
|
+
resolve13(dir),
|
|
14909
13710
|
{
|
|
14910
13711
|
api: strFlag(flags, "api"),
|
|
14911
13712
|
origin: strFlag(flags, "origin"),
|
|
@@ -14927,7 +13728,7 @@ reference and the only credential.`
|
|
|
14927
13728
|
}
|
|
14928
13729
|
const overrides = multi.override;
|
|
14929
13730
|
const result = await pushTake(
|
|
14930
|
-
|
|
13731
|
+
resolve13(dir),
|
|
14931
13732
|
{
|
|
14932
13733
|
key: strFlag(flags, "key"),
|
|
14933
13734
|
api: strFlag(flags, "api"),
|
|
@@ -14981,7 +13782,7 @@ async function cmdPull2(argv) {
|
|
|
14981
13782
|
const dir = positionals[0] ?? ".";
|
|
14982
13783
|
const r = createReporter(flags.json === true);
|
|
14983
13784
|
const result = await pullTake(
|
|
14984
|
-
|
|
13785
|
+
resolve13(dir),
|
|
14985
13786
|
{
|
|
14986
13787
|
key: strFlag(flags, "key"),
|
|
14987
13788
|
api: strFlag(flags, "api"),
|
|
@@ -15056,8 +13857,6 @@ async function run(argv) {
|
|
|
15056
13857
|
return await cmdValidate(rest);
|
|
15057
13858
|
case "callout":
|
|
15058
13859
|
return await cmdCallout(rest);
|
|
15059
|
-
case "judge":
|
|
15060
|
-
return await cmdJudge(rest);
|
|
15061
13860
|
case "fetch":
|
|
15062
13861
|
return await cmdFetch(rest);
|
|
15063
13862
|
case "duplicate":
|
|
@@ -15183,7 +13982,7 @@ async function cmdSession(argv) {
|
|
|
15183
13982
|
if (!arg) throw new UsageError(usage);
|
|
15184
13983
|
const bad = sessionNameVerdict(arg);
|
|
15185
13984
|
if (bad) throw new UsageError(bad);
|
|
15186
|
-
if (!
|
|
13985
|
+
if (!existsSync22(sessionDir(arg)))
|
|
15187
13986
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
15188
13987
|
const summary3 = await summarizeSession(arg);
|
|
15189
13988
|
const url = strFlag(flags, "url");
|
|
@@ -15283,4 +14082,4 @@ export {
|
|
|
15283
14082
|
verbHelp,
|
|
15284
14083
|
run
|
|
15285
14084
|
};
|
|
15286
|
-
//# sourceMappingURL=chunk-
|
|
14085
|
+
//# sourceMappingURL=chunk-5DUXN3RX.js.map
|