@vosjs/cli 0.32.0 → 0.34.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 +3 -1
- package/dist/{chunk-QJORXTJM.js → chunk-43FCXWNS.js} +608 -147
- package/dist/chunk-43FCXWNS.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.js +1 -1
- package/dist/{run-PYK3XJNH.js → run-EOIIBITT.js} +2 -2
- package/package.json +4 -4
- package/schema/actions.schema.json +24 -38
- package/dist/chunk-QJORXTJM.js.map +0 -1
- /package/dist/{run-PYK3XJNH.js.map → run-EOIIBITT.js.map} +0 -0
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
} from "./chunk-NHKHTIDR.js";
|
|
7
7
|
|
|
8
8
|
// src/plugin/run.ts
|
|
9
|
-
import { mkdir as mkdir9, readFile as
|
|
10
|
-
import { existsSync as
|
|
11
|
-
import { join as
|
|
9
|
+
import { mkdir as mkdir9, readFile as readFile15, rm as rm4 } from "fs/promises";
|
|
10
|
+
import { existsSync as existsSync15 } from "fs";
|
|
11
|
+
import { join as join18, resolve as resolve9 } from "path";
|
|
12
12
|
import {
|
|
13
13
|
END_CARD_FROM as END_CARD_FROM2,
|
|
14
14
|
docOutputDuration as docOutputDuration3,
|
|
@@ -156,6 +156,8 @@ function validateActions(value) {
|
|
|
156
156
|
errors.push(`${at2}: ${s.do} needs a selector`);
|
|
157
157
|
if (s.do === "wait" && typeof s.ms !== "number")
|
|
158
158
|
errors.push(`${at2}: wait needs ms`);
|
|
159
|
+
if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
|
|
160
|
+
errors.push(`${at2}: ms must be a number of milliseconds \u2265 0`);
|
|
159
161
|
if (s.do === "type" && typeof s.text !== "string")
|
|
160
162
|
errors.push(`${at2}: type needs text`);
|
|
161
163
|
if (s.do === "type" && s.focus !== void 0 && typeof s.focus !== "boolean")
|
|
@@ -214,6 +216,7 @@ import {
|
|
|
214
216
|
findStep,
|
|
215
217
|
outputEnd,
|
|
216
218
|
pinBox,
|
|
219
|
+
pinCandidates,
|
|
217
220
|
pinRectOnScreen,
|
|
218
221
|
pinReferent,
|
|
219
222
|
ratedSegments,
|
|
@@ -1611,6 +1614,17 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1611
1614
|
const tf = isObj(o.transform) ? o.transform : null;
|
|
1612
1615
|
if (!tf || !isNum(tf.x) || !isNum(tf.y)) return;
|
|
1613
1616
|
const pinned = pins.get(String(o.id));
|
|
1617
|
+
if (!pinned && o.pin === void 0 && (o.kind === "html" || o.kind === "image" || o.kind === "video")) {
|
|
1618
|
+
const cands = pinCandidates(docIn, {
|
|
1619
|
+
start: o.start,
|
|
1620
|
+
duration: o.duration
|
|
1621
|
+
});
|
|
1622
|
+
if (cands.length) {
|
|
1623
|
+
warnings.push(
|
|
1624
|
+
`overlays[${i}] is not pinned, and ${cands.length === 1 ? "a step with an element sits" : "steps with an element sit"} inside its window: ${cands.map((c) => `${String(c.step)} (${c.do}${c.selector ? ` ${c.selector}` : ""})`).join(", ")} \u2014 a callout about one of them says so: pin: { step: "${String(cands[0].step)}" }`
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1614
1628
|
if (pinned) {
|
|
1615
1629
|
if (pinned.clamped > 4) {
|
|
1616
1630
|
warnings.push(
|
|
@@ -5294,10 +5308,208 @@ function winRate(verdicts) {
|
|
|
5294
5308
|
return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
|
|
5295
5309
|
}
|
|
5296
5310
|
|
|
5311
|
+
// src/plugin/callout.ts
|
|
5312
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5313
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5314
|
+
import { join as join10 } from "path";
|
|
5315
|
+
import { lerpArray, sample } from "@vosjs/timeline";
|
|
5316
|
+
import {
|
|
5317
|
+
CALLOUT_DEFAULT_BODY_PX,
|
|
5318
|
+
CALLOUT_SHAPES,
|
|
5319
|
+
calloutClip,
|
|
5320
|
+
calloutGroundOf,
|
|
5321
|
+
cameraModel as cameraModel2,
|
|
5322
|
+
deltaE,
|
|
5323
|
+
docCardLayout as docCardLayout2,
|
|
5324
|
+
docZoomTrack as docZoomTrack2,
|
|
5325
|
+
hexToRgb,
|
|
5326
|
+
parseCssColour,
|
|
5327
|
+
pinBox as pinBox2,
|
|
5328
|
+
ratedSegments as ratedSegments5,
|
|
5329
|
+
resolvePins as resolvePins2,
|
|
5330
|
+
spanOutputExtent as spanOutputExtent5
|
|
5331
|
+
} from "@vosjs/studio-core";
|
|
5332
|
+
var CALLOUT_DEFAULT_SECONDS = 3;
|
|
5333
|
+
var CALLOUT_LEAD_SECONDS = 0.2;
|
|
5334
|
+
var CALLOUT_DELTA_E_PROBLEM = 8;
|
|
5335
|
+
var CALLOUT_DELTA_E_WARNING = 16;
|
|
5336
|
+
function registerFrom(roles, flags) {
|
|
5337
|
+
const ground = flags.ground ?? roles?.bgA;
|
|
5338
|
+
const accent = flags.accent ?? roles?.accent;
|
|
5339
|
+
if (!ground || !accent) return null;
|
|
5340
|
+
if (!hexToRgb(ground) || !hexToRgb(accent)) return null;
|
|
5341
|
+
const face = flags.face ?? roles?.fontBody;
|
|
5342
|
+
return {
|
|
5343
|
+
ground,
|
|
5344
|
+
accent,
|
|
5345
|
+
...face && !/^ui-sans-serif$|^system-ui$/.test(face) ? { face } : {},
|
|
5346
|
+
...flags.mono ? { mono: flags.mono } : {},
|
|
5347
|
+
body: flags.body ?? CALLOUT_DEFAULT_BODY_PX
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
function appScaleAt(doc, t) {
|
|
5351
|
+
const layout = docCardLayout2(doc);
|
|
5352
|
+
const meta = doc.source.meta;
|
|
5353
|
+
const base = layout.dw / Math.max(1, meta.width);
|
|
5354
|
+
const track = docZoomTrack2(doc);
|
|
5355
|
+
if (!track || !track.keyframes.length) return base;
|
|
5356
|
+
const z = sample(track, t, lerpArray);
|
|
5357
|
+
return base * Math.max(1, z[0]);
|
|
5358
|
+
}
|
|
5359
|
+
function windowForStep(doc, step, seconds = CALLOUT_DEFAULT_SECONDS) {
|
|
5360
|
+
const steps = doc.source.meta.steps ?? [];
|
|
5361
|
+
const s = steps.find((x) => x.id !== void 0 && x.id === step) ?? steps.find((x) => String(x.step) === String(step));
|
|
5362
|
+
if (!s || s.skipped) return null;
|
|
5363
|
+
const rated = ratedSegments5(doc);
|
|
5364
|
+
const ext = spanOutputExtent5(rated, s.tStart, s.tEnd);
|
|
5365
|
+
if (!ext) return null;
|
|
5366
|
+
const at2 = s.do === "click" || s.do === "drag" ? ext.start : ext.end;
|
|
5367
|
+
const start = +(at2 + CALLOUT_LEAD_SECONDS).toFixed(3);
|
|
5368
|
+
let end = start + seconds;
|
|
5369
|
+
for (const n of steps) {
|
|
5370
|
+
if (n.tStart <= s.tStart) continue;
|
|
5371
|
+
if (n.do !== "scroll" && !n.navigated) continue;
|
|
5372
|
+
const ne = spanOutputExtent5(rated, n.tStart, n.tEnd);
|
|
5373
|
+
if (ne && ne.start > start && ne.start < end) end = ne.start - 0.05;
|
|
5374
|
+
}
|
|
5375
|
+
return { start, duration: +Math.max(0.5, end - start).toFixed(3) };
|
|
5376
|
+
}
|
|
5377
|
+
function composeCallout(doc, register, ask) {
|
|
5378
|
+
let window2 = null;
|
|
5379
|
+
if (ask.step !== void 0) {
|
|
5380
|
+
window2 = windowForStep(doc, ask.step, ask.seconds);
|
|
5381
|
+
if (!window2)
|
|
5382
|
+
throw new Error(
|
|
5383
|
+
`step "${String(ask.step)}" is not in the take's step timeline, was skipped, or falls outside the cut`
|
|
5384
|
+
);
|
|
5385
|
+
} else if (ask.at !== void 0) {
|
|
5386
|
+
window2 = {
|
|
5387
|
+
start: ask.at,
|
|
5388
|
+
duration: ask.seconds ?? CALLOUT_DEFAULT_SECONDS
|
|
5389
|
+
};
|
|
5390
|
+
} else {
|
|
5391
|
+
throw new Error(
|
|
5392
|
+
"name the step the callout is about (--step <id>), or --at <output seconds>"
|
|
5393
|
+
);
|
|
5394
|
+
}
|
|
5395
|
+
const pin = ask.step !== void 0 ? { step: ask.step } : ask.press !== void 0 ? { press: ask.press } : void 0;
|
|
5396
|
+
if (pin) {
|
|
5397
|
+
if (ask.side && ask.side !== "auto") pin.side = ask.side;
|
|
5398
|
+
if (ask.mark && ask.mark !== "none") pin.mark = ask.mark;
|
|
5399
|
+
if (ask.leader) pin.leader = true;
|
|
5400
|
+
if (ask.color) pin.color = ask.color;
|
|
5401
|
+
}
|
|
5402
|
+
const taken = new Set((doc.overlays ?? []).map((o) => o.id));
|
|
5403
|
+
let id = ask.id ?? `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}`;
|
|
5404
|
+
let n = 2;
|
|
5405
|
+
while (taken.has(id) && !ask.id)
|
|
5406
|
+
id = `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}-${n++}`;
|
|
5407
|
+
const clip = calloutClip(ask.shape, register, ask.words, {
|
|
5408
|
+
id,
|
|
5409
|
+
start: window2.start,
|
|
5410
|
+
duration: window2.duration,
|
|
5411
|
+
scale: appScaleAt(doc, window2.start + 0.35),
|
|
5412
|
+
pin,
|
|
5413
|
+
// The fallback place, in the margin: the lower third.
|
|
5414
|
+
at: { x: 0.5, y: 0.82 }
|
|
5415
|
+
});
|
|
5416
|
+
return { clip, window: window2 };
|
|
5417
|
+
}
|
|
5418
|
+
function isCalloutShape(s) {
|
|
5419
|
+
return CALLOUT_SHAPES.includes(s);
|
|
5420
|
+
}
|
|
5421
|
+
async function groundFindings(doc, frameAt) {
|
|
5422
|
+
const out = [];
|
|
5423
|
+
const overlays = doc.overlays ?? [];
|
|
5424
|
+
const layout = docCardLayout2(doc);
|
|
5425
|
+
const camera = cameraModel2(doc.frame);
|
|
5426
|
+
const track = docZoomTrack2(doc);
|
|
5427
|
+
const pins = resolvePins2(doc, layout, camera, track);
|
|
5428
|
+
for (let i = 0; i < overlays.length; i++) {
|
|
5429
|
+
const o = overlays[i];
|
|
5430
|
+
if (o.kind !== "html") continue;
|
|
5431
|
+
const card = calloutGroundOf(o.css ?? "");
|
|
5432
|
+
if (!card) {
|
|
5433
|
+
out.push({
|
|
5434
|
+
index: i,
|
|
5435
|
+
id: o.id,
|
|
5436
|
+
level: "note",
|
|
5437
|
+
deltaE: null,
|
|
5438
|
+
card: null,
|
|
5439
|
+
footage: null,
|
|
5440
|
+
message: `overlays[${i}] declares no readable background on its root rule \u2014 the ground check cannot read it`
|
|
5441
|
+
});
|
|
5442
|
+
continue;
|
|
5443
|
+
}
|
|
5444
|
+
const t = o.start + Math.min(0.35, o.duration / 2);
|
|
5445
|
+
const img = await frameAt(t);
|
|
5446
|
+
if (!img) {
|
|
5447
|
+
out.push({
|
|
5448
|
+
index: i,
|
|
5449
|
+
id: o.id,
|
|
5450
|
+
level: "note",
|
|
5451
|
+
deltaE: null,
|
|
5452
|
+
card,
|
|
5453
|
+
footage: null,
|
|
5454
|
+
message: `overlays[${i}]: the frame at ${t.toFixed(2)}s could not be read`
|
|
5455
|
+
});
|
|
5456
|
+
continue;
|
|
5457
|
+
}
|
|
5458
|
+
const box = pinBox2(o, layout);
|
|
5459
|
+
const pinned = pins.get(o.id);
|
|
5460
|
+
let cx = o.transform.x * layout.W;
|
|
5461
|
+
let cy = o.transform.y * layout.H;
|
|
5462
|
+
if (pinned) {
|
|
5463
|
+
const v = sample(pinned.track, t - o.start, lerpArray);
|
|
5464
|
+
cx = v[0] * layout.W;
|
|
5465
|
+
cy = v[1] * layout.H;
|
|
5466
|
+
}
|
|
5467
|
+
const sx = img.w / layout.W;
|
|
5468
|
+
const sy = img.h / layout.H;
|
|
5469
|
+
const rect = {
|
|
5470
|
+
x: (cx - box.w / 2) * sx,
|
|
5471
|
+
y: (cy - box.h / 2) * sy,
|
|
5472
|
+
w: box.w * sx,
|
|
5473
|
+
h: box.h * sy
|
|
5474
|
+
};
|
|
5475
|
+
const footage = medianColour(img, rect);
|
|
5476
|
+
const cardRgb = parseCssColour(card);
|
|
5477
|
+
if (!cardRgb) continue;
|
|
5478
|
+
const d = deltaE(cardRgb, footage);
|
|
5479
|
+
const fh = `#${footage.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
5480
|
+
const level = d < CALLOUT_DELTA_E_PROBLEM ? "problem" : d < CALLOUT_DELTA_E_WARNING ? "warning" : "note";
|
|
5481
|
+
out.push({
|
|
5482
|
+
index: i,
|
|
5483
|
+
id: o.id,
|
|
5484
|
+
level,
|
|
5485
|
+
deltaE: +d.toFixed(1),
|
|
5486
|
+
card,
|
|
5487
|
+
footage: fh,
|
|
5488
|
+
message: level === "note" ? `overlays[${i}] ground ${card} over footage ${fh}: \u0394E ${d.toFixed(1)}, reads as lifted` : `overlays[${i}] ground ${card} is within \u0394E ${d.toFixed(1)} of the footage it covers (${fh}) at ${t.toFixed(2)}s \u2014 it reads as one more panel of the app; invert its value (vos callout does) or move it to the margin`
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
return out;
|
|
5492
|
+
}
|
|
5493
|
+
async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
5494
|
+
const out = /* @__PURE__ */ new Map();
|
|
5495
|
+
if (!times.length) return out;
|
|
5496
|
+
const bare = { ...doc, overlays: [] };
|
|
5497
|
+
const outDir = join10(dir, ".ground-check");
|
|
5498
|
+
const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
|
|
5499
|
+
for (const f of res.frames) {
|
|
5500
|
+
if (!existsSync9(f.file)) continue;
|
|
5501
|
+
out.set(
|
|
5502
|
+
+f.time.toFixed(3),
|
|
5503
|
+
decodePng(new Uint8Array(await readFile9(f.file)))
|
|
5504
|
+
);
|
|
5505
|
+
}
|
|
5506
|
+
return out;
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5297
5509
|
// src/plugin/kitPicture.ts
|
|
5298
5510
|
import { execFile } from "child_process";
|
|
5299
5511
|
import { promisify } from "util";
|
|
5300
|
-
import { readFile as
|
|
5512
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5301
5513
|
var execFileP = promisify(execFile);
|
|
5302
5514
|
var SUBJECT_BAND = { min: 0.6, max: 0.92 };
|
|
5303
5515
|
var BLANK_INK = 0.12;
|
|
@@ -5325,7 +5537,7 @@ function apcaContrast(text, bg) {
|
|
|
5325
5537
|
return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
|
|
5326
5538
|
}
|
|
5327
5539
|
var pct = (v) => `${Math.round(v * 100)}%`;
|
|
5328
|
-
var
|
|
5540
|
+
var hexToRgb2 = (h) => {
|
|
5329
5541
|
const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
|
|
5330
5542
|
if (!m) return null;
|
|
5331
5543
|
const n = parseInt(m[1], 16);
|
|
@@ -5474,7 +5686,7 @@ function textFindings(a, img) {
|
|
|
5474
5686
|
});
|
|
5475
5687
|
}
|
|
5476
5688
|
}
|
|
5477
|
-
const rgb = t.color ?
|
|
5689
|
+
const rgb = t.color ? hexToRgb2(t.color) : null;
|
|
5478
5690
|
if (rgb) {
|
|
5479
5691
|
const ground = medianColour(img, box);
|
|
5480
5692
|
const lc = apcaContrast(rgb, ground);
|
|
@@ -5573,7 +5785,7 @@ async function pictureChecks(assets) {
|
|
|
5573
5785
|
let ffmpeg = null;
|
|
5574
5786
|
for (const a of assets) {
|
|
5575
5787
|
if (/\.png$/i.test(a.file)) {
|
|
5576
|
-
const img = decodePng(new Uint8Array(await
|
|
5788
|
+
const img = decodePng(new Uint8Array(await readFile10(a.file)));
|
|
5577
5789
|
if (!img) {
|
|
5578
5790
|
findings.push({
|
|
5579
5791
|
code: "unreadable",
|
|
@@ -5666,9 +5878,9 @@ function formatFinding(f) {
|
|
|
5666
5878
|
}
|
|
5667
5879
|
|
|
5668
5880
|
// src/plugin/validateKit.ts
|
|
5669
|
-
import { existsSync as
|
|
5670
|
-
import { readFile as
|
|
5671
|
-
import { dirname as dirname2, isAbsolute as isAbsolute4, join as
|
|
5881
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5882
|
+
import { readFile as readFile11, stat as stat2 } from "fs/promises";
|
|
5883
|
+
import { dirname as dirname2, isAbsolute as isAbsolute4, join as join11 } from "path";
|
|
5672
5884
|
import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
|
|
5673
5885
|
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5674
5886
|
function pngDimensions(bytes) {
|
|
@@ -5687,7 +5899,7 @@ async function probeVideo(path) {
|
|
|
5687
5899
|
const MB = await import("mediabunny");
|
|
5688
5900
|
const input = new MB.Input({
|
|
5689
5901
|
formats: MB.ALL_FORMATS,
|
|
5690
|
-
source: new MB.BufferSource(new Uint8Array(await
|
|
5902
|
+
source: new MB.BufferSource(new Uint8Array(await readFile11(path)))
|
|
5691
5903
|
});
|
|
5692
5904
|
try {
|
|
5693
5905
|
const track = await input.getPrimaryVideoTrack();
|
|
@@ -5708,7 +5920,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5708
5920
|
const measured = [];
|
|
5709
5921
|
let kit;
|
|
5710
5922
|
try {
|
|
5711
|
-
kit = JSON.parse(await
|
|
5923
|
+
kit = JSON.parse(await readFile11(kitPath, "utf8"));
|
|
5712
5924
|
} catch (e) {
|
|
5713
5925
|
return {
|
|
5714
5926
|
valid: false,
|
|
@@ -5727,8 +5939,8 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5727
5939
|
}
|
|
5728
5940
|
const base = dirname2(kitPath);
|
|
5729
5941
|
const resolvePath = (p) => {
|
|
5730
|
-
if (isAbsolute4(p) &&
|
|
5731
|
-
const beside =
|
|
5942
|
+
if (isAbsolute4(p) && existsSync10(p)) return p;
|
|
5943
|
+
const beside = join11(base, p.split("/").pop() ?? p);
|
|
5732
5944
|
return beside;
|
|
5733
5945
|
};
|
|
5734
5946
|
const perDestination = /* @__PURE__ */ new Map();
|
|
@@ -5754,7 +5966,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5754
5966
|
let h = null;
|
|
5755
5967
|
let seconds = null;
|
|
5756
5968
|
if (/\.(png|jpe?g|webp)$/i.test(file)) {
|
|
5757
|
-
const head = Buffer.from(await
|
|
5969
|
+
const head = Buffer.from(await readFile11(file));
|
|
5758
5970
|
const kind = sniffImage(head);
|
|
5759
5971
|
if (file.toLowerCase().endsWith(".png") && kind !== "png")
|
|
5760
5972
|
problems.push(
|
|
@@ -5864,7 +6076,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5864
6076
|
|
|
5865
6077
|
// src/plugin/recorder.ts
|
|
5866
6078
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5867
|
-
import { join as
|
|
6079
|
+
import { join as join12 } from "path";
|
|
5868
6080
|
|
|
5869
6081
|
// src/plugin/recordingCap.ts
|
|
5870
6082
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
@@ -5910,9 +6122,99 @@ function cappedLine(capSeconds) {
|
|
|
5910
6122
|
return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
|
|
5911
6123
|
}
|
|
5912
6124
|
|
|
6125
|
+
// src/plugin/pace.ts
|
|
6126
|
+
var POINTER_MS_PER_PX = 0.7;
|
|
6127
|
+
var POINTER_MIN_MS = 250;
|
|
6128
|
+
var POINTER_MAX_MS = 800;
|
|
6129
|
+
var MOTION_TICK_MS = 16;
|
|
6130
|
+
var SETTLE_MS = {
|
|
6131
|
+
click: 150,
|
|
6132
|
+
type: 150,
|
|
6133
|
+
scroll: 200,
|
|
6134
|
+
drag: 80
|
|
6135
|
+
};
|
|
6136
|
+
var PRESS_LEAD_MS = 80;
|
|
6137
|
+
var PRESS_HOLD_MS = 70;
|
|
6138
|
+
var SCROLL_SETTLE_MS = 120;
|
|
6139
|
+
var TRAILING_HOLD_MS = 400;
|
|
6140
|
+
function pointerTravelMs(dist) {
|
|
6141
|
+
return Math.min(
|
|
6142
|
+
POINTER_MAX_MS,
|
|
6143
|
+
Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
|
|
6144
|
+
);
|
|
6145
|
+
}
|
|
6146
|
+
function settleMs(step) {
|
|
6147
|
+
if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
|
|
6148
|
+
const d = step.do;
|
|
6149
|
+
return SETTLE_MS[d] ?? 0;
|
|
6150
|
+
}
|
|
6151
|
+
var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
|
|
6152
|
+
async function clockMotion(dur, at2, clock) {
|
|
6153
|
+
const start = clock.now();
|
|
6154
|
+
let samples = 0;
|
|
6155
|
+
for (; ; ) {
|
|
6156
|
+
const elapsed = clock.now() - start;
|
|
6157
|
+
if (elapsed >= dur) break;
|
|
6158
|
+
await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
|
|
6159
|
+
samples++;
|
|
6160
|
+
const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
|
|
6161
|
+
const wait = Math.min(next, start + dur) - clock.now();
|
|
6162
|
+
if (wait > 0) await clock.sleep(wait);
|
|
6163
|
+
}
|
|
6164
|
+
await at2(1);
|
|
6165
|
+
return samples + 1;
|
|
6166
|
+
}
|
|
6167
|
+
async function clockTyping(chars, delay, type, clock) {
|
|
6168
|
+
const start = clock.now();
|
|
6169
|
+
for (let i = 0; i < chars.length; i++) {
|
|
6170
|
+
await type(chars[i], i);
|
|
6171
|
+
const due = start + (i + 1) * delay;
|
|
6172
|
+
const wait = due - clock.now();
|
|
6173
|
+
if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
function askedMs(step) {
|
|
6177
|
+
switch (step.do) {
|
|
6178
|
+
case "wait":
|
|
6179
|
+
return step.ms ?? 0;
|
|
6180
|
+
case "hover":
|
|
6181
|
+
return step.ms ?? 700;
|
|
6182
|
+
case "drag":
|
|
6183
|
+
return step.ms ?? 700;
|
|
6184
|
+
case "type":
|
|
6185
|
+
return (step.text?.length ?? 0) * (step.delayMs ?? 40);
|
|
6186
|
+
default:
|
|
6187
|
+
return 0;
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
function paceReport(steps) {
|
|
6191
|
+
const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
|
|
6192
|
+
const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
|
|
6193
|
+
const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
|
|
6194
|
+
const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
|
|
6195
|
+
const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
|
|
6196
|
+
step: s.step,
|
|
6197
|
+
do: s.do,
|
|
6198
|
+
askedMs: s.askedMs,
|
|
6199
|
+
wallMs: s.wallMs
|
|
6200
|
+
}));
|
|
6201
|
+
return {
|
|
6202
|
+
askedMs: askedMs2,
|
|
6203
|
+
gestureMs,
|
|
6204
|
+
wallMs,
|
|
6205
|
+
overheadMs,
|
|
6206
|
+
overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
|
|
6207
|
+
slow
|
|
6208
|
+
};
|
|
6209
|
+
}
|
|
6210
|
+
function paceLine(r) {
|
|
6211
|
+
const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
|
|
6212
|
+
const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
|
|
6213
|
+
return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
|
|
6214
|
+
}
|
|
6215
|
+
|
|
5913
6216
|
// src/plugin/recorder.ts
|
|
5914
6217
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5915
|
-
var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
|
|
5916
6218
|
function jpegDims(buf) {
|
|
5917
6219
|
let i = 2;
|
|
5918
6220
|
while (i < buf.length - 9) {
|
|
@@ -5952,7 +6254,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5952
6254
|
cdp.on("Page.screencastFrame", (ev) => {
|
|
5953
6255
|
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
5954
6256
|
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
5955
|
-
writeFileSync2(
|
|
6257
|
+
writeFileSync2(join12(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
5956
6258
|
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
5957
6259
|
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
5958
6260
|
});
|
|
@@ -5971,26 +6273,30 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5971
6273
|
const emit = (e) => events.push({ t: now(), ...e });
|
|
5972
6274
|
await page.mouse.move(cur.x, cur.y);
|
|
5973
6275
|
emit({ x: cur.x, y: cur.y, type: "move" });
|
|
6276
|
+
const clock = { now: () => Date.now(), sleep };
|
|
5974
6277
|
const moveTo = async (tx, ty) => {
|
|
5975
6278
|
const dist = Math.hypot(tx - cur.x, ty - cur.y);
|
|
5976
|
-
const dur = Math.min(1200, Math.max(350, dist * 1.4));
|
|
5977
|
-
const steps = Math.max(6, Math.round(dur / 16));
|
|
5978
6279
|
const from = { ...cur };
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
6280
|
+
await clockMotion(
|
|
6281
|
+
pointerTravelMs(dist),
|
|
6282
|
+
async (u) => {
|
|
6283
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
6284
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
6285
|
+
await page.mouse.move(cur.x, cur.y);
|
|
6286
|
+
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
|
|
6287
|
+
},
|
|
6288
|
+
clock
|
|
6289
|
+
);
|
|
5987
6290
|
};
|
|
5988
6291
|
const boxOf = async (selector) => {
|
|
5989
6292
|
const loc = page.locator(selector).first();
|
|
5990
6293
|
try {
|
|
5991
6294
|
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
6295
|
+
const before = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => [0, 0]);
|
|
5992
6296
|
await loc.scrollIntoViewIfNeeded();
|
|
5993
|
-
await
|
|
6297
|
+
const after = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => before);
|
|
6298
|
+
if (before[0] !== after[0] || before[1] !== after[1])
|
|
6299
|
+
await sleep(SCROLL_SETTLE_MS);
|
|
5994
6300
|
const bb = await loc.boundingBox();
|
|
5995
6301
|
if (!bb) return null;
|
|
5996
6302
|
return { x: bb.x, y: bb.y, w: bb.width, h: bb.height };
|
|
@@ -6001,7 +6307,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6001
6307
|
};
|
|
6002
6308
|
const clickAt = async (rect) => {
|
|
6003
6309
|
await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
6004
|
-
await sleep(
|
|
6310
|
+
await sleep(PRESS_LEAD_MS);
|
|
6005
6311
|
emit({
|
|
6006
6312
|
x: Math.round(cur.x),
|
|
6007
6313
|
y: Math.round(cur.y),
|
|
@@ -6010,7 +6316,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6010
6316
|
rect
|
|
6011
6317
|
});
|
|
6012
6318
|
await page.mouse.down();
|
|
6013
|
-
await sleep(
|
|
6319
|
+
await sleep(PRESS_HOLD_MS);
|
|
6014
6320
|
await page.mouse.up();
|
|
6015
6321
|
emit({
|
|
6016
6322
|
x: Math.round(cur.x),
|
|
@@ -6022,6 +6328,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6022
6328
|
};
|
|
6023
6329
|
const skipped = [];
|
|
6024
6330
|
const stepSpans = [];
|
|
6331
|
+
const paces = [];
|
|
6025
6332
|
let capped = false;
|
|
6026
6333
|
for (const [stepIdx, step] of actions.steps.entries()) {
|
|
6027
6334
|
if (capReached(now(), maxSeconds)) {
|
|
@@ -6029,6 +6336,13 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6029
6336
|
break;
|
|
6030
6337
|
}
|
|
6031
6338
|
const stepStart = now();
|
|
6339
|
+
let gestureMs = 0;
|
|
6340
|
+
const gesture = async (fn) => {
|
|
6341
|
+
const t = now();
|
|
6342
|
+
const out = await fn();
|
|
6343
|
+
gestureMs += now() - t;
|
|
6344
|
+
return out;
|
|
6345
|
+
};
|
|
6032
6346
|
const skippedBefore = skipped.length;
|
|
6033
6347
|
const urlBefore = page.url();
|
|
6034
6348
|
let stepRect = null;
|
|
@@ -6043,7 +6357,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6043
6357
|
const rect = await boxOf(step.selector);
|
|
6044
6358
|
stepRect = rect;
|
|
6045
6359
|
if (rect) {
|
|
6046
|
-
await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
6360
|
+
await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
|
|
6047
6361
|
log(`hover ${step.selector}`);
|
|
6048
6362
|
await sleep(step.ms ?? 700);
|
|
6049
6363
|
} else
|
|
@@ -6054,9 +6368,9 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6054
6368
|
const rect = await boxOf(step.selector);
|
|
6055
6369
|
stepRect = rect;
|
|
6056
6370
|
if (rect) {
|
|
6057
|
-
await clickAt(rect);
|
|
6371
|
+
await gesture(() => clickAt(rect));
|
|
6058
6372
|
log(`click ${step.selector}`);
|
|
6059
|
-
await sleep(
|
|
6373
|
+
await gesture(() => sleep(settleMs(step)));
|
|
6060
6374
|
} else
|
|
6061
6375
|
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
6062
6376
|
break;
|
|
@@ -6065,7 +6379,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6065
6379
|
const rect = await boxOf(step.selector);
|
|
6066
6380
|
stepRect = rect;
|
|
6067
6381
|
if (rect) {
|
|
6068
|
-
if (step.focus !== false) await clickAt(rect);
|
|
6382
|
+
if (step.focus !== false) await gesture(() => clickAt(rect));
|
|
6069
6383
|
const delay = step.delayMs ?? 40;
|
|
6070
6384
|
const ping = () => emit({
|
|
6071
6385
|
x: Math.round(rect.x + rect.w / 2),
|
|
@@ -6074,17 +6388,21 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6074
6388
|
rect
|
|
6075
6389
|
});
|
|
6076
6390
|
let lastPing = -Infinity;
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6391
|
+
await clockTyping(
|
|
6392
|
+
[...step.text],
|
|
6393
|
+
delay,
|
|
6394
|
+
async (ch) => {
|
|
6395
|
+
if (now() - lastPing >= 350) {
|
|
6396
|
+
lastPing = now();
|
|
6397
|
+
ping();
|
|
6398
|
+
}
|
|
6399
|
+
await page.keyboard.type(ch);
|
|
6400
|
+
},
|
|
6401
|
+
clock
|
|
6402
|
+
);
|
|
6085
6403
|
ping();
|
|
6086
6404
|
log(`type "${step.text}" into ${step.selector}`);
|
|
6087
|
-
await sleep(
|
|
6405
|
+
await gesture(() => sleep(settleMs(step)));
|
|
6088
6406
|
} else
|
|
6089
6407
|
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
6090
6408
|
break;
|
|
@@ -6097,7 +6415,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6097
6415
|
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "scroll" });
|
|
6098
6416
|
await sleep(40);
|
|
6099
6417
|
}
|
|
6100
|
-
await sleep(
|
|
6418
|
+
await gesture(() => sleep(settleMs(step)));
|
|
6101
6419
|
break;
|
|
6102
6420
|
}
|
|
6103
6421
|
case "drag": {
|
|
@@ -6121,8 +6439,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6121
6439
|
start = { x: step.x, y: step.y };
|
|
6122
6440
|
}
|
|
6123
6441
|
if (!start) break;
|
|
6124
|
-
await moveTo(start.x, start.y);
|
|
6125
|
-
await sleep(
|
|
6442
|
+
await gesture(() => moveTo(start.x, start.y));
|
|
6443
|
+
await gesture(() => sleep(PRESS_LEAD_MS));
|
|
6126
6444
|
emit({
|
|
6127
6445
|
x: Math.round(cur.x),
|
|
6128
6446
|
y: Math.round(cur.y),
|
|
@@ -6131,19 +6449,20 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6131
6449
|
rect
|
|
6132
6450
|
});
|
|
6133
6451
|
await page.mouse.down();
|
|
6134
|
-
await sleep(
|
|
6452
|
+
await gesture(() => sleep(PRESS_HOLD_MS));
|
|
6135
6453
|
const dur = step.ms ?? 700;
|
|
6136
|
-
const steps = Math.max(8, Math.round(dur / 16));
|
|
6137
6454
|
const from = { ...cur };
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6455
|
+
await clockMotion(
|
|
6456
|
+
dur,
|
|
6457
|
+
async (u) => {
|
|
6458
|
+
cur.x = from.x + (step.tx - from.x) * u;
|
|
6459
|
+
cur.y = from.y + (step.ty - from.y) * u;
|
|
6460
|
+
await page.mouse.move(cur.x, cur.y);
|
|
6461
|
+
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
|
|
6462
|
+
},
|
|
6463
|
+
clock
|
|
6464
|
+
);
|
|
6465
|
+
await gesture(() => sleep(settleMs({ do: "drag" })));
|
|
6147
6466
|
await page.mouse.up();
|
|
6148
6467
|
emit({
|
|
6149
6468
|
x: Math.round(cur.x),
|
|
@@ -6178,8 +6497,15 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6178
6497
|
}
|
|
6179
6498
|
} : {}
|
|
6180
6499
|
});
|
|
6500
|
+
paces.push({
|
|
6501
|
+
step: stepIdx,
|
|
6502
|
+
do: step.do,
|
|
6503
|
+
askedMs: askedMs(step),
|
|
6504
|
+
gestureMs,
|
|
6505
|
+
wallMs: now() - stepStart
|
|
6506
|
+
});
|
|
6181
6507
|
}
|
|
6182
|
-
if (!capped) await sleep(
|
|
6508
|
+
if (!capped) await sleep(TRAILING_HOLD_MS);
|
|
6183
6509
|
if (capped || capReached(now(), maxSeconds)) {
|
|
6184
6510
|
capped = true;
|
|
6185
6511
|
log(` ${cappedLine(maxSeconds)}`);
|
|
@@ -6190,7 +6516,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6190
6516
|
await sleep(200);
|
|
6191
6517
|
const pageTitle = await page.title().catch(() => "");
|
|
6192
6518
|
await context.close();
|
|
6193
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(
|
|
6519
|
+
const firstFrame = frames[0] ? jpegDims(readFileSync3(join12(paths.framesDir, frames[0].file))) : null;
|
|
6194
6520
|
const meta = {
|
|
6195
6521
|
dpr: 1,
|
|
6196
6522
|
zoom: 1,
|
|
@@ -6231,10 +6557,13 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6231
6557
|
await writeJson(paths.meta, meta, true);
|
|
6232
6558
|
await writeJson(paths.framesIndex, frames);
|
|
6233
6559
|
await writeJson(paths.actions, { ...actions, url }, true);
|
|
6560
|
+
const pace = paceReport(paces);
|
|
6561
|
+
log(` ${paceLine(pace)}`);
|
|
6234
6562
|
return {
|
|
6235
6563
|
events,
|
|
6236
6564
|
frames,
|
|
6237
6565
|
meta,
|
|
6566
|
+
pace,
|
|
6238
6567
|
skipped,
|
|
6239
6568
|
navTimeout,
|
|
6240
6569
|
freezes,
|
|
@@ -6298,9 +6627,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6298
6627
|
}
|
|
6299
6628
|
|
|
6300
6629
|
// src/plugin/plan.ts
|
|
6301
|
-
import { existsSync as
|
|
6302
|
-
import { readFile as
|
|
6303
|
-
import { join as
|
|
6630
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6631
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6632
|
+
import { join as join13 } from "path";
|
|
6304
6633
|
import {
|
|
6305
6634
|
DEFAULT_FRAME_STYLE,
|
|
6306
6635
|
STYLE_FIELDS,
|
|
@@ -6522,10 +6851,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6522
6851
|
// src/plugin/plan.ts
|
|
6523
6852
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6524
6853
|
async function readDigestActivity(dir) {
|
|
6525
|
-
const file =
|
|
6526
|
-
if (!
|
|
6854
|
+
const file = join13(dir, "digest", "digest.json");
|
|
6855
|
+
if (!existsSync11(file)) return null;
|
|
6527
6856
|
try {
|
|
6528
|
-
const d = JSON.parse(await
|
|
6857
|
+
const d = JSON.parse(await readFile12(file, "utf8"));
|
|
6529
6858
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
6530
6859
|
} catch {
|
|
6531
6860
|
return null;
|
|
@@ -6735,17 +7064,17 @@ async function planTake(dir, opts = {}) {
|
|
|
6735
7064
|
|
|
6736
7065
|
// src/plugin/sync.ts
|
|
6737
7066
|
import { createHash } from "crypto";
|
|
6738
|
-
import { existsSync as
|
|
6739
|
-
import { readFile as
|
|
7067
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7068
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6740
7069
|
import { createInterface } from "readline/promises";
|
|
6741
|
-
import { basename, join as
|
|
7070
|
+
import { basename, join as join16 } from "path";
|
|
6742
7071
|
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
6743
7072
|
|
|
6744
7073
|
// src/plugin/media.ts
|
|
6745
7074
|
import { isKeyedOverlay } from "@vosjs/studio-core";
|
|
6746
|
-
import { createWriteStream, existsSync as
|
|
7075
|
+
import { createWriteStream, existsSync as existsSync12 } from "fs";
|
|
6747
7076
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
6748
|
-
import { join as
|
|
7077
|
+
import { join as join14 } from "path";
|
|
6749
7078
|
import { Readable } from "stream";
|
|
6750
7079
|
import { pipeline } from "stream/promises";
|
|
6751
7080
|
var MIC_NAME = "mic.webm";
|
|
@@ -6869,8 +7198,8 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6869
7198
|
const url = doc.source[key];
|
|
6870
7199
|
const assetId = assetIdOf(url);
|
|
6871
7200
|
if (!assetId) continue;
|
|
6872
|
-
const target =
|
|
6873
|
-
if (
|
|
7201
|
+
const target = join14(dir, file);
|
|
7202
|
+
if (existsSync12(target)) {
|
|
6874
7203
|
result.kept.push(file);
|
|
6875
7204
|
} else {
|
|
6876
7205
|
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
@@ -6895,8 +7224,8 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6895
7224
|
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
6896
7225
|
const assetId = assetIdOf(ref.key);
|
|
6897
7226
|
const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
|
|
6898
|
-
const existing =
|
|
6899
|
-
(fs) => fs.readdir(
|
|
7227
|
+
const existing = existsSync12(join14(dir, "media")) ? (await import("fs/promises").then(
|
|
7228
|
+
(fs) => fs.readdir(join14(dir, "media"))
|
|
6900
7229
|
)).find((f) => f.startsWith(`${assetId}.`)) : void 0;
|
|
6901
7230
|
let file;
|
|
6902
7231
|
if (existing) {
|
|
@@ -6913,9 +7242,9 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6913
7242
|
}
|
|
6914
7243
|
const ext = extensionFor(res.headers.get("content-type") ?? "");
|
|
6915
7244
|
file = `media/${assetId}${ext}`;
|
|
6916
|
-
const target =
|
|
7245
|
+
const target = join14(dir, file);
|
|
6917
7246
|
await import("fs/promises").then(
|
|
6918
|
-
(fs) => fs.mkdir(
|
|
7247
|
+
(fs) => fs.mkdir(join14(dir, "media"), { recursive: true })
|
|
6919
7248
|
);
|
|
6920
7249
|
await pipeline(
|
|
6921
7250
|
Readable.fromWeb(res.body),
|
|
@@ -6929,17 +7258,17 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6929
7258
|
}
|
|
6930
7259
|
ref.set(file);
|
|
6931
7260
|
}
|
|
6932
|
-
const metaPath =
|
|
6933
|
-
if (!
|
|
6934
|
-
const cursorPath =
|
|
6935
|
-
if (!
|
|
6936
|
-
await writeFile9(
|
|
7261
|
+
const metaPath = join14(dir, "meta.json");
|
|
7262
|
+
if (!existsSync12(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
7263
|
+
const cursorPath = join14(dir, "cursor.json");
|
|
7264
|
+
if (!existsSync12(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
7265
|
+
await writeFile9(join14(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
6937
7266
|
return result;
|
|
6938
7267
|
}
|
|
6939
7268
|
|
|
6940
7269
|
// src/plugin/folder.ts
|
|
6941
7270
|
import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
|
|
6942
|
-
import { join as
|
|
7271
|
+
import { join as join15 } from "path";
|
|
6943
7272
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6944
7273
|
import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
6945
7274
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
|
|
@@ -7115,18 +7444,18 @@ async function cmdPull(argv) {
|
|
|
7115
7444
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7116
7445
|
const payload = res.body;
|
|
7117
7446
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7118
|
-
await mkdir7(
|
|
7447
|
+
await mkdir7(join15(out, "recipes"), { recursive: true });
|
|
7119
7448
|
const lines = [];
|
|
7120
7449
|
const recipes = [];
|
|
7121
7450
|
for (const rec of payload.recipes) {
|
|
7122
|
-
const path =
|
|
7451
|
+
const path = join15(out, "recipes", safeName(rec.filename));
|
|
7123
7452
|
await writeFile10(path, rec.body ?? "");
|
|
7124
7453
|
recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
|
|
7125
7454
|
}
|
|
7126
7455
|
for (const rec of payload.inheritedRecipes) {
|
|
7127
7456
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7128
|
-
await mkdir7(
|
|
7129
|
-
const path =
|
|
7457
|
+
await mkdir7(join15(out, "recipes", "_inherited", from), { recursive: true });
|
|
7458
|
+
const path = join15(
|
|
7130
7459
|
out,
|
|
7131
7460
|
"recipes",
|
|
7132
7461
|
"_inherited",
|
|
@@ -7141,7 +7470,7 @@ async function cmdPull(argv) {
|
|
|
7141
7470
|
}
|
|
7142
7471
|
const members = [];
|
|
7143
7472
|
for (const v of payload.voses) {
|
|
7144
|
-
const dir =
|
|
7473
|
+
const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
|
|
7145
7474
|
await mkdir7(dir, { recursive: true });
|
|
7146
7475
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7147
7476
|
if (cfg.status !== 200) {
|
|
@@ -7149,7 +7478,7 @@ async function cmdPull(argv) {
|
|
|
7149
7478
|
continue;
|
|
7150
7479
|
}
|
|
7151
7480
|
await writeFile10(
|
|
7152
|
-
|
|
7481
|
+
join15(dir, "config.json"),
|
|
7153
7482
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7154
7483
|
);
|
|
7155
7484
|
let take = false;
|
|
@@ -7161,7 +7490,7 @@ async function cmdPull(argv) {
|
|
|
7161
7490
|
);
|
|
7162
7491
|
if (doc.status === 200) {
|
|
7163
7492
|
await writeFile10(
|
|
7164
|
-
|
|
7493
|
+
join15(dir, "doc.json"),
|
|
7165
7494
|
JSON.stringify(doc.body, null, 2)
|
|
7166
7495
|
);
|
|
7167
7496
|
take = true;
|
|
@@ -7278,7 +7607,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7278
7607
|
throw new Error(`doc.json fails lint:
|
|
7279
7608
|
${lint.problems.join("\n ")}`);
|
|
7280
7609
|
}
|
|
7281
|
-
if (!
|
|
7610
|
+
if (!existsSync13(take.paths.recording)) {
|
|
7282
7611
|
throw new Error("no recording.webm in this take");
|
|
7283
7612
|
}
|
|
7284
7613
|
const ctx = apiContext(flags);
|
|
@@ -7310,7 +7639,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7310
7639
|
throw new Error("push cancelled");
|
|
7311
7640
|
}
|
|
7312
7641
|
}
|
|
7313
|
-
const bytes = await
|
|
7642
|
+
const bytes = await readFile13(take.paths.recording);
|
|
7314
7643
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7315
7644
|
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7316
7645
|
const upload = await api(ctx, "/assets/recording", {
|
|
@@ -7349,12 +7678,12 @@ async function pushTake(dir, flags, r) {
|
|
|
7349
7678
|
delete docForPush.source.micKey;
|
|
7350
7679
|
}
|
|
7351
7680
|
for (const ref of docMediaRefs(docForPush)) {
|
|
7352
|
-
const file =
|
|
7353
|
-
if (!
|
|
7681
|
+
const file = join16(dir, takeRelativeFile(ref.key));
|
|
7682
|
+
if (!existsSync13(file)) {
|
|
7354
7683
|
r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
|
|
7355
7684
|
continue;
|
|
7356
7685
|
}
|
|
7357
|
-
const media = await
|
|
7686
|
+
const media = await readFile13(file);
|
|
7358
7687
|
const mediaHash = createHash("sha256").update(media).digest("hex");
|
|
7359
7688
|
const type = mediaContentType(file);
|
|
7360
7689
|
const put = await api(ctx, "/assets/recording", {
|
|
@@ -7490,9 +7819,9 @@ async function pullTake(dir, flags, r) {
|
|
|
7490
7819
|
};
|
|
7491
7820
|
}
|
|
7492
7821
|
let media2;
|
|
7493
|
-
if (flags.media &&
|
|
7822
|
+
if (flags.media && existsSync13(join16(dir, "doc.json"))) {
|
|
7494
7823
|
const local = JSON.parse(
|
|
7495
|
-
await
|
|
7824
|
+
await readFile13(join16(dir, "doc.json"), "utf8")
|
|
7496
7825
|
);
|
|
7497
7826
|
media2 = await pullMedia(ctx, dir, local, r.log);
|
|
7498
7827
|
}
|
|
@@ -7552,10 +7881,10 @@ async function pullTake(dir, flags, r) {
|
|
|
7552
7881
|
}
|
|
7553
7882
|
const hostedDoc = migrateHostedDoc3(docRes.json);
|
|
7554
7883
|
const doc = hostedDoc;
|
|
7555
|
-
if (
|
|
7884
|
+
if (existsSync13(join16(dir, RECORDING_NAME))) {
|
|
7556
7885
|
doc.source.videoKey = RECORDING_NAME;
|
|
7557
7886
|
}
|
|
7558
|
-
await writeJson(
|
|
7887
|
+
await writeJson(join16(dir, "doc.json"), doc, true);
|
|
7559
7888
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
7560
7889
|
const versions = await api(ctx, `/vos/${vosId}/versions`);
|
|
7561
7890
|
const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
|
|
@@ -7574,10 +7903,10 @@ async function pullTake(dir, flags, r) {
|
|
|
7574
7903
|
}
|
|
7575
7904
|
|
|
7576
7905
|
// src/plugin/program.ts
|
|
7577
|
-
import { existsSync as
|
|
7578
|
-
import { mkdir as mkdir8, readFile as
|
|
7906
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7907
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
7579
7908
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7580
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
7909
|
+
import { basename as basename2, dirname as dirname3, join as join17 } from "path";
|
|
7581
7910
|
import {
|
|
7582
7911
|
CURRENT_CONFIG_VERSION,
|
|
7583
7912
|
migrateConfig,
|
|
@@ -7747,8 +8076,8 @@ function preflightConfig(parsed) {
|
|
|
7747
8076
|
}
|
|
7748
8077
|
function resolveConfigPath(target) {
|
|
7749
8078
|
if (target.endsWith(".json")) return target;
|
|
7750
|
-
const inDir =
|
|
7751
|
-
if (
|
|
8079
|
+
const inDir = join17(target, "config.json");
|
|
8080
|
+
if (existsSync14(inDir)) return inDir;
|
|
7752
8081
|
throw new UsageError(
|
|
7753
8082
|
`${target} has no config.json (and is not a take \u2014 no doc.json)`
|
|
7754
8083
|
);
|
|
@@ -7776,7 +8105,7 @@ async function cmdFetch(argv) {
|
|
|
7776
8105
|
const out = strFlag(flags, "out") ?? slug;
|
|
7777
8106
|
await mkdir8(out, { recursive: true });
|
|
7778
8107
|
await writeFile11(
|
|
7779
|
-
|
|
8108
|
+
join17(out, "config.json"),
|
|
7780
8109
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7781
8110
|
);
|
|
7782
8111
|
const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
|
|
@@ -7801,12 +8130,12 @@ async function cmdFetch(argv) {
|
|
|
7801
8130
|
const hosted = migrateHostedDoc4(doc.body);
|
|
7802
8131
|
const { config: own } = await writeProgramDoc(out, hosted);
|
|
7803
8132
|
if (own) {
|
|
7804
|
-
await writeFile11(
|
|
8133
|
+
await writeFile11(join17(out, "config.json"), JSON.stringify(own, null, 2));
|
|
7805
8134
|
}
|
|
7806
8135
|
} else if (doc.status === 200) {
|
|
7807
8136
|
take = true;
|
|
7808
8137
|
const hosted = migrateHostedDoc4(doc.body);
|
|
7809
|
-
await writeFile11(
|
|
8138
|
+
await writeFile11(join17(out, "doc.json"), JSON.stringify(hosted, null, 2));
|
|
7810
8139
|
if (flags.media === true) {
|
|
7811
8140
|
const media = await pullMedia({ origin, key }, out, hosted, r.log);
|
|
7812
8141
|
mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
|
|
@@ -7919,7 +8248,7 @@ async function cmdPushProgram(argv) {
|
|
|
7919
8248
|
api: strFlag(flags, "api")
|
|
7920
8249
|
});
|
|
7921
8250
|
const dir = dirname3(source);
|
|
7922
|
-
const parsed = JSON.parse(await
|
|
8251
|
+
const parsed = JSON.parse(await readFile14(source, "utf8"));
|
|
7923
8252
|
const pre = preflightConfig(parsed);
|
|
7924
8253
|
if (!pre.ok || !pre.config) {
|
|
7925
8254
|
for (const issue of pre.issues) r.log(`error ${issue}`);
|
|
@@ -8160,10 +8489,10 @@ async function cmdPullProgram(argv) {
|
|
|
8160
8489
|
const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
|
|
8161
8490
|
if (cfg.status !== 200)
|
|
8162
8491
|
throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
|
|
8163
|
-
const configPath =
|
|
8492
|
+
const configPath = join17(dir, "config.json");
|
|
8164
8493
|
let backedUp = false;
|
|
8165
|
-
if (
|
|
8166
|
-
await writeFile11(
|
|
8494
|
+
if (existsSync14(configPath)) {
|
|
8495
|
+
await writeFile11(join17(dir, "config.backup.json"), await readFile14(configPath));
|
|
8167
8496
|
backedUp = true;
|
|
8168
8497
|
}
|
|
8169
8498
|
let headConfig = cfg.body.config;
|
|
@@ -8196,7 +8525,7 @@ async function cmdPullProgram(argv) {
|
|
|
8196
8525
|
protected: protectedIds,
|
|
8197
8526
|
changes,
|
|
8198
8527
|
out: configPath,
|
|
8199
|
-
backup: backedUp ?
|
|
8528
|
+
backup: backedUp ? join17(dir, "config.backup.json") : null
|
|
8200
8529
|
},
|
|
8201
8530
|
`Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
|
|
8202
8531
|
Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
|
|
@@ -8258,9 +8587,9 @@ function storedProgramConfig(config, programDoc) {
|
|
|
8258
8587
|
return lowerProgramDoc(doc).config;
|
|
8259
8588
|
}
|
|
8260
8589
|
async function readProgramDoc(dir, config) {
|
|
8261
|
-
const docPath =
|
|
8262
|
-
if (!
|
|
8263
|
-
const parsed = JSON.parse(await
|
|
8590
|
+
const docPath = join17(dir, "doc.json");
|
|
8591
|
+
if (!existsSync14(docPath)) return null;
|
|
8592
|
+
const parsed = JSON.parse(await readFile14(docPath, "utf8"));
|
|
8264
8593
|
if (typeof parsed !== "object" || parsed === null || "source" in parsed)
|
|
8265
8594
|
return null;
|
|
8266
8595
|
const raw = parsed;
|
|
@@ -8271,7 +8600,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8271
8600
|
const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
|
|
8272
8601
|
const { config, ...rest } = program;
|
|
8273
8602
|
const onDisk = { ...hosted, program: rest };
|
|
8274
|
-
await writeFile11(
|
|
8603
|
+
await writeFile11(join17(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
|
|
8275
8604
|
return {
|
|
8276
8605
|
config: config && typeof config === "object" ? config : null
|
|
8277
8606
|
};
|
|
@@ -9327,8 +9656,8 @@ async function cmdActions(argv) {
|
|
|
9327
9656
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
9328
9657
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
9329
9658
|
}
|
|
9330
|
-
const { readFile:
|
|
9331
|
-
const text = await
|
|
9659
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
9660
|
+
const text = await readFile16(resolve8(input), "utf8");
|
|
9332
9661
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
9333
9662
|
for (const p of problems) r.log(` ${p}`);
|
|
9334
9663
|
const result = convertAgentBrowser(records, {
|
|
@@ -9382,7 +9711,8 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
9382
9711
|
"yes",
|
|
9383
9712
|
"composed",
|
|
9384
9713
|
"check",
|
|
9385
|
-
"motion"
|
|
9714
|
+
"motion",
|
|
9715
|
+
"leader"
|
|
9386
9716
|
]);
|
|
9387
9717
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
|
|
9388
9718
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -9397,6 +9727,7 @@ Take pipeline
|
|
|
9397
9727
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
9398
9728
|
vos brand <url> [--out BRAND.md] [--json]
|
|
9399
9729
|
vos open <take> [--studio <url>] [--print]
|
|
9730
|
+
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] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px 14] [--id x] [--print] [--json]
|
|
9400
9731
|
vos validate <actions.json|take|kit.json> [--picture] [--json]
|
|
9401
9732
|
vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
|
|
9402
9733
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
@@ -9630,7 +9961,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
9630
9961
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
9631
9962
|
`;
|
|
9632
9963
|
async function loadActions(file) {
|
|
9633
|
-
const raw = JSON.parse(await
|
|
9964
|
+
const raw = JSON.parse(await readFile15(file, "utf8"));
|
|
9634
9965
|
const errors = validateActions(raw);
|
|
9635
9966
|
if (errors.length)
|
|
9636
9967
|
throw new UsageError(`invalid actions file:
|
|
@@ -9689,7 +10020,7 @@ async function cmdRecord(argv) {
|
|
|
9689
10020
|
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
9690
10021
|
const backdrop = await takeBackdrop(flags, r);
|
|
9691
10022
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9692
|
-
if (
|
|
10023
|
+
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
9693
10024
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9694
10025
|
r.log(
|
|
9695
10026
|
`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" : "")
|
|
@@ -9732,11 +10063,12 @@ async function cmdRecord(argv) {
|
|
|
9732
10063
|
freezes: rec.freezes,
|
|
9733
10064
|
freezePct: rec.freezePct,
|
|
9734
10065
|
capped: rec.capped,
|
|
10066
|
+
pace: rec.pace,
|
|
9735
10067
|
...strictFail ? { strictFailed: true } : {}
|
|
9736
10068
|
},
|
|
9737
10069
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
|
|
9738
10070
|
${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen${rec.freezePct >= 25 ? " \u26A0 keep motion in frame or trim" : ""}${skippedNote}
|
|
9739
|
-
Next: edit ${
|
|
10071
|
+
Next: edit ${join18(outDir, "doc.json")} (optional), then: vos render ${outDir}`
|
|
9740
10072
|
);
|
|
9741
10073
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
9742
10074
|
} finally {
|
|
@@ -9765,14 +10097,14 @@ async function cmdCreate2(argv) {
|
|
|
9765
10097
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9766
10098
|
throw new UsageError("--format must be webm or mp4");
|
|
9767
10099
|
const format = fmtRaw;
|
|
9768
|
-
const out = positionals[0] ??
|
|
10100
|
+
const out = positionals[0] ?? join18(outDir, `out.${format}`);
|
|
9769
10101
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9770
10102
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9771
10103
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
9772
10104
|
}
|
|
9773
10105
|
const backdrop = await takeBackdrop(flags, r);
|
|
9774
10106
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9775
|
-
if (
|
|
10107
|
+
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
9776
10108
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9777
10109
|
r.log(
|
|
9778
10110
|
`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" : "")
|
|
@@ -9863,24 +10195,24 @@ async function cmdPlan(argv) {
|
|
|
9863
10195
|
);
|
|
9864
10196
|
const r = createReporter(flags.json === true);
|
|
9865
10197
|
if (flags.fresh === true) {
|
|
9866
|
-
await rm4(
|
|
10198
|
+
await rm4(join18(dir, "doc.json"), { force: true });
|
|
9867
10199
|
r.log("note: --fresh discarded the existing doc.json");
|
|
9868
10200
|
}
|
|
9869
10201
|
let reuse;
|
|
9870
10202
|
if (flags.reuse === true) {
|
|
9871
|
-
const from = resolve9(strFlag(flags, "from") ??
|
|
9872
|
-
if (!
|
|
10203
|
+
const from = resolve9(strFlag(flags, "from") ?? join18(dir, PREV_DOC_NAME));
|
|
10204
|
+
if (!existsSync15(from)) {
|
|
9873
10205
|
throw new UsageError(
|
|
9874
10206
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
9875
10207
|
);
|
|
9876
10208
|
}
|
|
9877
10209
|
reuse = {
|
|
9878
10210
|
from,
|
|
9879
|
-
doc: JSON.parse(await
|
|
10211
|
+
doc: JSON.parse(await readFile15(from, "utf8"))
|
|
9880
10212
|
};
|
|
9881
10213
|
}
|
|
9882
10214
|
const style = await resolveStyleRef(flags);
|
|
9883
|
-
const hasDoc =
|
|
10215
|
+
const hasDoc = existsSync15(join18(dir, "doc.json"));
|
|
9884
10216
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
9885
10217
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
9886
10218
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
@@ -9938,7 +10270,7 @@ async function cmdPlan(argv) {
|
|
|
9938
10270
|
...s.motion ? { motion: s.motion } : {},
|
|
9939
10271
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
9940
10272
|
},
|
|
9941
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
10273
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join18(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
|
|
9942
10274
|
);
|
|
9943
10275
|
return EXIT_OK;
|
|
9944
10276
|
}
|
|
@@ -10064,7 +10396,7 @@ async function cmdRender(argv) {
|
|
|
10064
10396
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
10065
10397
|
throw new UsageError("--format must be webm or mp4");
|
|
10066
10398
|
const format = fmtRaw;
|
|
10067
|
-
const out = positionals[1] ??
|
|
10399
|
+
const out = positionals[1] ?? join18(dir, `out.${format}`);
|
|
10068
10400
|
const parallel = numFlag(flags, "parallel", 1);
|
|
10069
10401
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
10070
10402
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -10311,15 +10643,15 @@ async function resolveStyleRef(flags) {
|
|
|
10311
10643
|
}
|
|
10312
10644
|
async function resolveDocRef(ref, flags, what) {
|
|
10313
10645
|
const styleRef = ref;
|
|
10314
|
-
const file =
|
|
10646
|
+
const file = existsSync15(styleRef) ? resolve9(
|
|
10315
10647
|
styleRef,
|
|
10316
|
-
|
|
10648
|
+
existsSync15(join18(styleRef, "doc.json")) ? "doc.json" : ""
|
|
10317
10649
|
) : null;
|
|
10318
|
-
if (file &&
|
|
10650
|
+
if (file && existsSync15(file)) {
|
|
10319
10651
|
return {
|
|
10320
10652
|
from: file,
|
|
10321
10653
|
doc: migrateHostedDoc5(
|
|
10322
|
-
JSON.parse(await
|
|
10654
|
+
JSON.parse(await readFile15(file, "utf8"))
|
|
10323
10655
|
)
|
|
10324
10656
|
};
|
|
10325
10657
|
}
|
|
@@ -10371,7 +10703,7 @@ async function cmdDigest(argv) {
|
|
|
10371
10703
|
const take = await loadTake(dir);
|
|
10372
10704
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
10373
10705
|
const transcriptPath = strFlag(flags, "transcript");
|
|
10374
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
10706
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
|
|
10375
10707
|
const style = await resolveStyleRef(flags);
|
|
10376
10708
|
const noFrames = flags["no-frames"] === true;
|
|
10377
10709
|
let browser = null;
|
|
@@ -10402,7 +10734,7 @@ async function cmdDigest(argv) {
|
|
|
10402
10734
|
kinds,
|
|
10403
10735
|
frames: d.images.full,
|
|
10404
10736
|
crops: d.images.crop,
|
|
10405
|
-
sheet: d.images.sheet ?
|
|
10737
|
+
sheet: d.images.sheet ? join18(result.outDir, d.images.sheet) : null,
|
|
10406
10738
|
scenes: kinds.scene ?? 0,
|
|
10407
10739
|
sourceDuration: d.take.sourceDuration,
|
|
10408
10740
|
outputDuration: d.take.outputDuration,
|
|
@@ -10426,7 +10758,7 @@ async function cmdOpen(argv) {
|
|
|
10426
10758
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
10427
10759
|
const r = createReporter(flags.json === true);
|
|
10428
10760
|
const take = await loadTake(dir);
|
|
10429
|
-
if (!
|
|
10761
|
+
if (!existsSync15(take.paths.recording)) {
|
|
10430
10762
|
throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
|
|
10431
10763
|
}
|
|
10432
10764
|
const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
|
|
@@ -10467,12 +10799,12 @@ async function cmdJudge(argv) {
|
|
|
10467
10799
|
"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)"
|
|
10468
10800
|
);
|
|
10469
10801
|
const r = createReporter(flags.json === true);
|
|
10470
|
-
const kitFile = target.endsWith("kit.json") ? target :
|
|
10471
|
-
if (!
|
|
10472
|
-
if (!
|
|
10802
|
+
const kitFile = target.endsWith("kit.json") ? target : join18(target, "kit.json");
|
|
10803
|
+
if (!existsSync15(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
10804
|
+
if (!existsSync15(against))
|
|
10473
10805
|
throw new UsageError(`${against}: no reference manifest`);
|
|
10474
10806
|
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
10475
|
-
const verdicts = JSON.parse(await
|
|
10807
|
+
const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
|
|
10476
10808
|
const rate = winRate(verdicts);
|
|
10477
10809
|
const lines = result.sheets.map(
|
|
10478
10810
|
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
@@ -10487,12 +10819,109 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
|
|
|
10487
10819
|
);
|
|
10488
10820
|
return EXIT_OK;
|
|
10489
10821
|
}
|
|
10822
|
+
async function cmdCallout(argv) {
|
|
10823
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
|
|
10824
|
+
const dir = positionals[0];
|
|
10825
|
+
const shape = positionals[1];
|
|
10826
|
+
if (!dir || !shape || !isCalloutShape(shape))
|
|
10827
|
+
throw new UsageError(
|
|
10828
|
+
'vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side \u2026] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex --accent #hex] [--font "\u2026"] [--body-px 14] [--id x] [--print] [--json]'
|
|
10829
|
+
);
|
|
10830
|
+
const r = createReporter(flags.json === true);
|
|
10831
|
+
const take = await loadTake(dir);
|
|
10832
|
+
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
10833
|
+
const brand = await readBrandBesideTake(dir, strFlag(flags, "brand"));
|
|
10834
|
+
const bodyPx = strFlag(flags, "body-px");
|
|
10835
|
+
const register = registerFrom(brand?.roles ?? null, {
|
|
10836
|
+
ground: strFlag(flags, "ground"),
|
|
10837
|
+
accent: strFlag(flags, "accent"),
|
|
10838
|
+
face: strFlag(flags, "font"),
|
|
10839
|
+
mono: strFlag(flags, "mono"),
|
|
10840
|
+
body: bodyPx !== void 0 ? Number(bodyPx) : void 0
|
|
10841
|
+
});
|
|
10842
|
+
if (!register)
|
|
10843
|
+
throw new UsageError(
|
|
10844
|
+
`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`
|
|
10845
|
+
);
|
|
10846
|
+
const codeFile = strFlag(flags, "code-file");
|
|
10847
|
+
const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
|
|
10848
|
+
const words = {
|
|
10849
|
+
kicker: strFlag(flags, "kicker"),
|
|
10850
|
+
title: strFlag(flags, "title"),
|
|
10851
|
+
body: strFlag(flags, "body"),
|
|
10852
|
+
code: code?.replace(/\\n/g, "\n")
|
|
10853
|
+
};
|
|
10854
|
+
if (shape === "code" && !words.code)
|
|
10855
|
+
throw new UsageError(
|
|
10856
|
+
'a code callout needs --code "\u2026" or --code-file <file>'
|
|
10857
|
+
);
|
|
10858
|
+
if (shape === "note" && !words.title)
|
|
10859
|
+
throw new UsageError('a note needs --title "\u2026"');
|
|
10860
|
+
if (shape === "tag" && !words.kicker && !words.title)
|
|
10861
|
+
throw new UsageError('a tag needs --kicker "\u2026"');
|
|
10862
|
+
const stepRaw = strFlag(flags, "step");
|
|
10863
|
+
const atRaw = strFlag(flags, "at");
|
|
10864
|
+
const secondsRaw = strFlag(flags, "seconds");
|
|
10865
|
+
const side = strFlag(flags, "side");
|
|
10866
|
+
const mark = strFlag(flags, "mark");
|
|
10867
|
+
let composed;
|
|
10868
|
+
try {
|
|
10869
|
+
composed = composeCallout(take.doc, register, {
|
|
10870
|
+
shape,
|
|
10871
|
+
words,
|
|
10872
|
+
id: strFlag(flags, "id"),
|
|
10873
|
+
step: stepRaw !== void 0 ? /^\d+$/.test(stepRaw) ? Number(stepRaw) : stepRaw : void 0,
|
|
10874
|
+
at: atRaw !== void 0 ? Number(atRaw) : void 0,
|
|
10875
|
+
seconds: secondsRaw !== void 0 ? Number(secondsRaw) : void 0,
|
|
10876
|
+
side,
|
|
10877
|
+
mark,
|
|
10878
|
+
leader: flags.leader === true,
|
|
10879
|
+
color: strFlag(flags, "color")
|
|
10880
|
+
});
|
|
10881
|
+
} catch (e) {
|
|
10882
|
+
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
10883
|
+
}
|
|
10884
|
+
const { clip, window: window2 } = composed;
|
|
10885
|
+
if (flags.print === true) {
|
|
10886
|
+
r.done(
|
|
10887
|
+
{ clip, window: window2, register, brand: brand?.file ?? null },
|
|
10888
|
+
JSON.stringify(clip, null, 2)
|
|
10889
|
+
);
|
|
10890
|
+
return EXIT_OK;
|
|
10891
|
+
}
|
|
10892
|
+
const doc = take.doc;
|
|
10893
|
+
const overlays = (doc.overlays ?? []).filter((o) => o.id !== clip.id);
|
|
10894
|
+
const next = { ...doc, overlays: [...overlays, clip] };
|
|
10895
|
+
const lint = lintDoc(next);
|
|
10896
|
+
if (lint.problems.length) {
|
|
10897
|
+
r.done(
|
|
10898
|
+
{ valid: false, problems: lint.problems, warnings: lint.warnings, clip },
|
|
10899
|
+
`the callout would not validate:
|
|
10900
|
+
${lint.problems.join("\n ")}`
|
|
10901
|
+
);
|
|
10902
|
+
return EXIT_ERROR;
|
|
10903
|
+
}
|
|
10904
|
+
await writeJson(take.paths.doc, next, true);
|
|
10905
|
+
r.done(
|
|
10906
|
+
{
|
|
10907
|
+
clip,
|
|
10908
|
+
window: window2,
|
|
10909
|
+
register,
|
|
10910
|
+
brand: brand?.file ?? null,
|
|
10911
|
+
warnings: lint.warnings
|
|
10912
|
+
},
|
|
10913
|
+
`${shape} "${clip.id}" at ${window2.start.toFixed(2)}s for ${window2.duration.toFixed(2)}s${clip.pin ? `, pinned to ${clip.pin.step !== void 0 ? `step ${String(clip.pin.step)}` : `the press at ${String(clip.pin.press)}s`}` : ""}, ground ${register.ground} \u2192 card in the product's hue, written to ${take.paths.doc}${lint.warnings.length ? `
|
|
10914
|
+
warnings:
|
|
10915
|
+
${lint.warnings.join("\n ")}` : ""}`
|
|
10916
|
+
);
|
|
10917
|
+
return EXIT_OK;
|
|
10918
|
+
}
|
|
10490
10919
|
async function cmdValidate(argv) {
|
|
10491
10920
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
|
|
10492
10921
|
const target = positionals[0];
|
|
10493
10922
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
10494
10923
|
const r = createReporter(flags.json === true);
|
|
10495
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
10924
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync15(join18(target, "kit.json")) && !isTakeDir(target) ? join18(target, "kit.json") : null;
|
|
10496
10925
|
if (kitTarget) {
|
|
10497
10926
|
const picture = flags.picture === true;
|
|
10498
10927
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -10524,9 +10953,9 @@ async function cmdValidate(argv) {
|
|
|
10524
10953
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
10525
10954
|
return EXIT_OK;
|
|
10526
10955
|
}
|
|
10527
|
-
if (!isTakeDir(target) &&
|
|
10956
|
+
if (!isTakeDir(target) && existsSync15(join18(target, "config.json"))) {
|
|
10528
10957
|
const parsed = JSON.parse(
|
|
10529
|
-
await
|
|
10958
|
+
await readFile15(join18(target, "config.json"), "utf8")
|
|
10530
10959
|
);
|
|
10531
10960
|
const pre = preflightConfig(parsed);
|
|
10532
10961
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -10567,7 +10996,7 @@ async function cmdValidate(argv) {
|
|
|
10567
10996
|
const take = await loadTake(target);
|
|
10568
10997
|
const problems = [];
|
|
10569
10998
|
const warnings = [];
|
|
10570
|
-
if (!
|
|
10999
|
+
if (!existsSync15(take.paths.recording))
|
|
10571
11000
|
problems.push("missing recording.webm (re-run record)");
|
|
10572
11001
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
10573
11002
|
if (take.doc) {
|
|
@@ -10579,6 +11008,36 @@ async function cmdValidate(argv) {
|
|
|
10579
11008
|
);
|
|
10580
11009
|
problems.push(...probe.problems);
|
|
10581
11010
|
warnings.push(...probe.warnings);
|
|
11011
|
+
if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
|
|
11012
|
+
const doc = take.doc;
|
|
11013
|
+
const times = [
|
|
11014
|
+
...new Set(
|
|
11015
|
+
(doc.overlays ?? []).filter((o) => o.kind === "html").map((o) => +(o.start + Math.min(0.35, o.duration / 2)).toFixed(3))
|
|
11016
|
+
)
|
|
11017
|
+
];
|
|
11018
|
+
const browser = await launchBrowser();
|
|
11019
|
+
try {
|
|
11020
|
+
const frames = await footageFrames(
|
|
11021
|
+
browser,
|
|
11022
|
+
target,
|
|
11023
|
+
doc,
|
|
11024
|
+
times,
|
|
11025
|
+
framesTake
|
|
11026
|
+
);
|
|
11027
|
+
const findings = await groundFindings(
|
|
11028
|
+
doc,
|
|
11029
|
+
async (t) => frames.get(+t.toFixed(3)) ?? null
|
|
11030
|
+
);
|
|
11031
|
+
for (const f of findings) {
|
|
11032
|
+
if (f.level === "problem") problems.push(`doc.json: ${f.message}`);
|
|
11033
|
+
else if (f.level === "warning")
|
|
11034
|
+
warnings.push(`doc.json: ${f.message}`);
|
|
11035
|
+
else r.event({ event: "ground", ...f });
|
|
11036
|
+
}
|
|
11037
|
+
} finally {
|
|
11038
|
+
await browser.close();
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
10582
11041
|
}
|
|
10583
11042
|
if (problems.length) {
|
|
10584
11043
|
r.done(
|
|
@@ -10695,6 +11154,8 @@ async function run(argv) {
|
|
|
10695
11154
|
return await cmdOpen(rest);
|
|
10696
11155
|
case "validate":
|
|
10697
11156
|
return await cmdValidate(rest);
|
|
11157
|
+
case "callout":
|
|
11158
|
+
return await cmdCallout(rest);
|
|
10698
11159
|
case "judge":
|
|
10699
11160
|
return await cmdJudge(rest);
|
|
10700
11161
|
case "fetch":
|
|
@@ -10778,4 +11239,4 @@ export {
|
|
|
10778
11239
|
convertAgentBrowser,
|
|
10779
11240
|
run
|
|
10780
11241
|
};
|
|
10781
|
-
//# sourceMappingURL=chunk-
|
|
11242
|
+
//# sourceMappingURL=chunk-43FCXWNS.js.map
|