@vosjs/cli 0.31.1 → 0.33.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 -3
- package/dist/{chunk-APVJ4MDJ.js → chunk-6O55OD6D.js} +607 -116
- package/dist/chunk-6O55OD6D.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-ULH2OMJM.js → run-X6XJCMIZ.js} +2 -2
- package/package.json +6 -6
- package/schema/doc.schema.json +49 -1
- package/dist/chunk-APVJ4MDJ.js.map +0 -1
- /package/dist/{run-ULH2OMJM.js.map → run-X6XJCMIZ.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,
|
|
@@ -210,9 +210,16 @@ import {
|
|
|
210
210
|
ZOOM_LEVEL_MIN,
|
|
211
211
|
ZOOM_SPAN_MIN,
|
|
212
212
|
docCardLayout,
|
|
213
|
+
docZoomTrack,
|
|
214
|
+
findStep,
|
|
213
215
|
outputEnd,
|
|
216
|
+
pinBox,
|
|
217
|
+
pinCandidates,
|
|
218
|
+
pinRectOnScreen,
|
|
219
|
+
pinReferent,
|
|
214
220
|
ratedSegments,
|
|
215
221
|
recommendedExportResolution,
|
|
222
|
+
resolvePins,
|
|
216
223
|
spanOutputExtent,
|
|
217
224
|
zoomCoversRect,
|
|
218
225
|
cameraModel
|
|
@@ -944,6 +951,7 @@ function lintDoc(docIn) {
|
|
|
944
951
|
if (!isNum(o.duration) || o.duration <= 0) {
|
|
945
952
|
problems.push(`${name}.duration must be > 0 (seconds)`);
|
|
946
953
|
}
|
|
954
|
+
checkPin(o, name, docIn, recording, problems, warnings);
|
|
947
955
|
if (o.kind === "text") {
|
|
948
956
|
if (typeof o.text !== "string")
|
|
949
957
|
problems.push(`${name}.text must be a string`);
|
|
@@ -1455,6 +1463,106 @@ function clicksWithRects(source) {
|
|
|
1455
1463
|
}
|
|
1456
1464
|
return out;
|
|
1457
1465
|
}
|
|
1466
|
+
var PIN_SIDES = ["auto", "right", "left", "below", "above"];
|
|
1467
|
+
var PIN_MARKS = ["none", "ring", "underline"];
|
|
1468
|
+
function checkPin(o, name, docIn, recording, problems, warnings) {
|
|
1469
|
+
if (o.pin === void 0) return;
|
|
1470
|
+
if (!isObj(o.pin)) {
|
|
1471
|
+
problems.push(
|
|
1472
|
+
`${name}.pin must be { step | press | rect, side?, gap?, mark?, leader?, color? }`
|
|
1473
|
+
);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const p = o.pin;
|
|
1477
|
+
const named = ["step", "press", "rect"].filter((k) => p[k] !== void 0);
|
|
1478
|
+
if (named.length !== 1) {
|
|
1479
|
+
problems.push(
|
|
1480
|
+
`${name}.pin names its referent by exactly one of step (a recorder step id or index), press (a source second) or rect (video fractions)${named.length ? ` \u2014 got ${named.join(" and ")}` : ""}`
|
|
1481
|
+
);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (p.step !== void 0 && typeof p.step !== "string" && !(isNum(p.step) && Number.isInteger(p.step) && p.step >= 0)) {
|
|
1485
|
+
problems.push(
|
|
1486
|
+
`${name}.pin.step must be a step id (string) or index (integer \u2265 0)`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
if (p.press !== void 0 && !isNum(p.press))
|
|
1490
|
+
problems.push(`${name}.pin.press must be a source second (number)`);
|
|
1491
|
+
if (p.rect !== void 0) {
|
|
1492
|
+
const r = isObj(p.rect) ? p.rect : null;
|
|
1493
|
+
if (!r || !isNum(r.x) || !isNum(r.y) || !isNum(r.w) || !isNum(r.h)) {
|
|
1494
|
+
problems.push(`${name}.pin.rect must be { x, y, w, h }`);
|
|
1495
|
+
} else if (Math.abs(r.x) > 2 || Math.abs(r.y) > 2 || r.w > 2 || r.h > 2) {
|
|
1496
|
+
problems.push(
|
|
1497
|
+
`${name}.pin.rect looks like PIXELS (${String(r.x)}, ${String(r.y)}, ${String(r.w)}\xD7${String(r.h)}) \u2014 it is FRACTIONS of the video frame [0..1] (the zoom cx/cy convention)`
|
|
1498
|
+
);
|
|
1499
|
+
} else if (r.w <= 0 || r.h <= 0) {
|
|
1500
|
+
problems.push(`${name}.pin.rect must have w > 0 and h > 0`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (p.side !== void 0 && !PIN_SIDES.includes(p.side))
|
|
1504
|
+
problems.push(`${name}.pin.side must be one of ${PIN_SIDES.join(" | ")}`);
|
|
1505
|
+
if (p.gap !== void 0 && (!isNum(p.gap) || p.gap < 0))
|
|
1506
|
+
problems.push(`${name}.pin.gap must be design px \u2265 0`);
|
|
1507
|
+
if (p.mark !== void 0 && !PIN_MARKS.includes(p.mark))
|
|
1508
|
+
problems.push(`${name}.pin.mark must be one of ${PIN_MARKS.join(" | ")}`);
|
|
1509
|
+
if (p.leader !== void 0 && typeof p.leader !== "boolean")
|
|
1510
|
+
problems.push(`${name}.pin.leader must be true or false`);
|
|
1511
|
+
if (p.color !== void 0 && typeof p.color !== "string")
|
|
1512
|
+
problems.push(`${name}.pin.color must be a CSS colour string`);
|
|
1513
|
+
if (problems.some((m) => m.startsWith(`${name}.pin`))) return;
|
|
1514
|
+
if (!recording) {
|
|
1515
|
+
problems.push(
|
|
1516
|
+
`${name}.pin names something on a recording's page; a program has no referents \u2014 place the layer with transform`
|
|
1517
|
+
);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const doc = docIn;
|
|
1521
|
+
const steps = doc.source.meta.steps ?? [];
|
|
1522
|
+
if (p.step !== void 0) {
|
|
1523
|
+
const step = findStep(steps, p.step);
|
|
1524
|
+
if (!step) {
|
|
1525
|
+
const known = steps.filter((s) => s.selector).map((s) => s.id !== void 0 ? s.id : String(s.step));
|
|
1526
|
+
problems.push(
|
|
1527
|
+
`${name}.pin.step "${String(p.step)}" is not a step of this take${known.length ? ` \u2014 steps with an element: ${known.join(", ")}` : " \u2014 the take has no step timeline (a human recording); pin by press or rect"}`
|
|
1528
|
+
);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (step.skipped) {
|
|
1532
|
+
problems.push(
|
|
1533
|
+
`${name}.pin.step "${String(p.step)}" was skipped at record time (its selector never became visible), so it has no element to point at`
|
|
1534
|
+
);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
let ref;
|
|
1539
|
+
try {
|
|
1540
|
+
ref = pinReferent(doc, p);
|
|
1541
|
+
} catch {
|
|
1542
|
+
ref = null;
|
|
1543
|
+
}
|
|
1544
|
+
if (!ref) {
|
|
1545
|
+
problems.push(
|
|
1546
|
+
p.step !== void 0 ? `${name}.pin.step "${String(p.step)}" has no element rect and no press inside its window (a wait, scroll or move step touches nothing) \u2014 pin a hover, click, type or drag step, or a press` : p.press !== void 0 ? `${name}.pin.press ${String(p.press)}s has no press with an element rect within half a second \u2014 the presses are the cursor track's down events` : `${name}.pin.rect could not be read`
|
|
1547
|
+
);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (ref.at !== null && isNum(o.start) && isNum(o.duration)) {
|
|
1551
|
+
const rated = ratedSegments(doc);
|
|
1552
|
+
for (const s of steps) {
|
|
1553
|
+
if (s.tStart <= ref.at) continue;
|
|
1554
|
+
if (s.do !== "scroll" && !s.navigated) continue;
|
|
1555
|
+
const out = spanOutputExtent(rated, s.tStart, s.tEnd);
|
|
1556
|
+
if (!out) continue;
|
|
1557
|
+
if (out.start < o.start + o.duration && out.end > o.start) {
|
|
1558
|
+
warnings.push(
|
|
1559
|
+
`${name} is pinned to something measured at ${ref.at.toFixed(1)}s, but ${s.do === "scroll" ? "a scroll" : "a navigation"} at ${s.tStart.toFixed(1)}s (output ${out.start.toFixed(1)}s) falls inside the layer's window \u2014 the referent may have moved; end the layer before it`
|
|
1560
|
+
);
|
|
1561
|
+
break;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1458
1566
|
function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
1459
1567
|
try {
|
|
1460
1568
|
const source = isObj(doc.source) ? doc.source : {};
|
|
@@ -1496,24 +1604,65 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1496
1604
|
}
|
|
1497
1605
|
}
|
|
1498
1606
|
const rated = ratedSegments(docIn);
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
});
|
|
1607
|
+
const camera = cameraModel(docIn.frame);
|
|
1608
|
+
const zoomTrack = docZoomTrack(docIn);
|
|
1609
|
+
const pins = resolvePins(docIn, layout, camera, zoomTrack);
|
|
1503
1610
|
overlays.forEach((o, i) => {
|
|
1504
|
-
if (!isObj(o) || o.
|
|
1611
|
+
if (!isObj(o) || !isNum(o.start) || !isNum(o.duration)) return;
|
|
1505
1612
|
const tf = isObj(o.transform) ? o.transform : null;
|
|
1506
|
-
if (!tf || !isNum(tf.x) || !isNum(tf.y)
|
|
1613
|
+
if (!tf || !isNum(tf.x) || !isNum(tf.y)) return;
|
|
1614
|
+
const pinned = pins.get(String(o.id));
|
|
1615
|
+
if (!pinned && o.pin === void 0 && (o.kind === "html" || o.kind === "image" || o.kind === "video")) {
|
|
1616
|
+
const cands = pinCandidates(docIn, {
|
|
1617
|
+
start: o.start,
|
|
1618
|
+
duration: o.duration
|
|
1619
|
+
});
|
|
1620
|
+
if (cands.length) {
|
|
1621
|
+
warnings.push(
|
|
1622
|
+
`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)}" }`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (pinned) {
|
|
1627
|
+
if (pinned.clamped > 4) {
|
|
1628
|
+
warnings.push(
|
|
1629
|
+
`overlays[${i}] is pinned ${pinned.side} of its referent but the frame has no room there \u2014 it sits ${Math.round(pinned.clamped)} design px off; a smaller box, another side, or a lower zoom level keeps it beside what it names`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1507
1632
|
return;
|
|
1633
|
+
}
|
|
1634
|
+
let box;
|
|
1635
|
+
try {
|
|
1636
|
+
box = pinBox(o, layout);
|
|
1637
|
+
} catch {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1508
1640
|
for (const c of clicks) {
|
|
1509
1641
|
const out = spanOutputExtent(rated, c.t, c.t + 1e-3);
|
|
1510
1642
|
if (!out || out.start < o.start || out.start > o.start + o.duration)
|
|
1511
1643
|
continue;
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1644
|
+
const target = pinRectOnScreen(
|
|
1645
|
+
{
|
|
1646
|
+
x: c.rect.x / w,
|
|
1647
|
+
y: c.rect.y / h,
|
|
1648
|
+
w: c.rect.w / w,
|
|
1649
|
+
h: c.rect.h / h
|
|
1650
|
+
},
|
|
1651
|
+
out.start,
|
|
1652
|
+
layout,
|
|
1653
|
+
camera,
|
|
1654
|
+
zoomTrack
|
|
1655
|
+
);
|
|
1656
|
+
const L = {
|
|
1657
|
+
x: tf.x * layout.W - box.w / 2,
|
|
1658
|
+
y: tf.y * layout.H - box.h / 2,
|
|
1659
|
+
w: box.w,
|
|
1660
|
+
h: box.h
|
|
1661
|
+
};
|
|
1662
|
+
const overlaps2 = L.x < target.x + target.w && L.x + L.w > target.x && L.y < target.y + target.h && L.y + L.h > target.y;
|
|
1663
|
+
if (overlaps2) {
|
|
1515
1664
|
warnings.push(
|
|
1516
|
-
`overlays[${i}] sits over the thing being clicked at ${c.t.toFixed(1)}s (output ${out.start.toFixed(1)}s) \u2014
|
|
1665
|
+
`overlays[${i}] sits over the thing being clicked at ${c.t.toFixed(1)}s (output ${out.start.toFixed(1)}s) \u2014 pin it to that step (pin: { step }) so it sits beside the target and follows it, or move it off`
|
|
1517
1666
|
);
|
|
1518
1667
|
break;
|
|
1519
1668
|
}
|
|
@@ -5157,10 +5306,208 @@ function winRate(verdicts) {
|
|
|
5157
5306
|
return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
|
|
5158
5307
|
}
|
|
5159
5308
|
|
|
5309
|
+
// src/plugin/callout.ts
|
|
5310
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5311
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5312
|
+
import { join as join10 } from "path";
|
|
5313
|
+
import { lerpArray, sample } from "@vosjs/timeline";
|
|
5314
|
+
import {
|
|
5315
|
+
CALLOUT_DEFAULT_BODY_PX,
|
|
5316
|
+
CALLOUT_SHAPES,
|
|
5317
|
+
calloutClip,
|
|
5318
|
+
calloutGroundOf,
|
|
5319
|
+
cameraModel as cameraModel2,
|
|
5320
|
+
deltaE,
|
|
5321
|
+
docCardLayout as docCardLayout2,
|
|
5322
|
+
docZoomTrack as docZoomTrack2,
|
|
5323
|
+
hexToRgb,
|
|
5324
|
+
parseCssColour,
|
|
5325
|
+
pinBox as pinBox2,
|
|
5326
|
+
ratedSegments as ratedSegments5,
|
|
5327
|
+
resolvePins as resolvePins2,
|
|
5328
|
+
spanOutputExtent as spanOutputExtent5
|
|
5329
|
+
} from "@vosjs/studio-core";
|
|
5330
|
+
var CALLOUT_DEFAULT_SECONDS = 3;
|
|
5331
|
+
var CALLOUT_LEAD_SECONDS = 0.2;
|
|
5332
|
+
var CALLOUT_DELTA_E_PROBLEM = 8;
|
|
5333
|
+
var CALLOUT_DELTA_E_WARNING = 16;
|
|
5334
|
+
function registerFrom(roles, flags) {
|
|
5335
|
+
const ground = flags.ground ?? roles?.bgA;
|
|
5336
|
+
const accent = flags.accent ?? roles?.accent;
|
|
5337
|
+
if (!ground || !accent) return null;
|
|
5338
|
+
if (!hexToRgb(ground) || !hexToRgb(accent)) return null;
|
|
5339
|
+
const face = flags.face ?? roles?.fontBody;
|
|
5340
|
+
return {
|
|
5341
|
+
ground,
|
|
5342
|
+
accent,
|
|
5343
|
+
...face && !/^ui-sans-serif$|^system-ui$/.test(face) ? { face } : {},
|
|
5344
|
+
...flags.mono ? { mono: flags.mono } : {},
|
|
5345
|
+
body: flags.body ?? CALLOUT_DEFAULT_BODY_PX
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
function appScaleAt(doc, t) {
|
|
5349
|
+
const layout = docCardLayout2(doc);
|
|
5350
|
+
const meta = doc.source.meta;
|
|
5351
|
+
const base = layout.dw / Math.max(1, meta.width);
|
|
5352
|
+
const track = docZoomTrack2(doc);
|
|
5353
|
+
if (!track || !track.keyframes.length) return base;
|
|
5354
|
+
const z = sample(track, t, lerpArray);
|
|
5355
|
+
return base * Math.max(1, z[0]);
|
|
5356
|
+
}
|
|
5357
|
+
function windowForStep(doc, step, seconds = CALLOUT_DEFAULT_SECONDS) {
|
|
5358
|
+
const steps = doc.source.meta.steps ?? [];
|
|
5359
|
+
const s = steps.find((x) => x.id !== void 0 && x.id === step) ?? steps.find((x) => String(x.step) === String(step));
|
|
5360
|
+
if (!s || s.skipped) return null;
|
|
5361
|
+
const rated = ratedSegments5(doc);
|
|
5362
|
+
const ext = spanOutputExtent5(rated, s.tStart, s.tEnd);
|
|
5363
|
+
if (!ext) return null;
|
|
5364
|
+
const at2 = s.do === "click" || s.do === "drag" ? ext.start : ext.end;
|
|
5365
|
+
const start = +(at2 + CALLOUT_LEAD_SECONDS).toFixed(3);
|
|
5366
|
+
let end = start + seconds;
|
|
5367
|
+
for (const n of steps) {
|
|
5368
|
+
if (n.tStart <= s.tStart) continue;
|
|
5369
|
+
if (n.do !== "scroll" && !n.navigated) continue;
|
|
5370
|
+
const ne = spanOutputExtent5(rated, n.tStart, n.tEnd);
|
|
5371
|
+
if (ne && ne.start > start && ne.start < end) end = ne.start - 0.05;
|
|
5372
|
+
}
|
|
5373
|
+
return { start, duration: +Math.max(0.5, end - start).toFixed(3) };
|
|
5374
|
+
}
|
|
5375
|
+
function composeCallout(doc, register, ask) {
|
|
5376
|
+
let window2 = null;
|
|
5377
|
+
if (ask.step !== void 0) {
|
|
5378
|
+
window2 = windowForStep(doc, ask.step, ask.seconds);
|
|
5379
|
+
if (!window2)
|
|
5380
|
+
throw new Error(
|
|
5381
|
+
`step "${String(ask.step)}" is not in the take's step timeline, was skipped, or falls outside the cut`
|
|
5382
|
+
);
|
|
5383
|
+
} else if (ask.at !== void 0) {
|
|
5384
|
+
window2 = {
|
|
5385
|
+
start: ask.at,
|
|
5386
|
+
duration: ask.seconds ?? CALLOUT_DEFAULT_SECONDS
|
|
5387
|
+
};
|
|
5388
|
+
} else {
|
|
5389
|
+
throw new Error(
|
|
5390
|
+
"name the step the callout is about (--step <id>), or --at <output seconds>"
|
|
5391
|
+
);
|
|
5392
|
+
}
|
|
5393
|
+
const pin = ask.step !== void 0 ? { step: ask.step } : ask.press !== void 0 ? { press: ask.press } : void 0;
|
|
5394
|
+
if (pin) {
|
|
5395
|
+
if (ask.side && ask.side !== "auto") pin.side = ask.side;
|
|
5396
|
+
if (ask.mark && ask.mark !== "none") pin.mark = ask.mark;
|
|
5397
|
+
if (ask.leader) pin.leader = true;
|
|
5398
|
+
if (ask.color) pin.color = ask.color;
|
|
5399
|
+
}
|
|
5400
|
+
const taken = new Set((doc.overlays ?? []).map((o) => o.id));
|
|
5401
|
+
let id = ask.id ?? `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}`;
|
|
5402
|
+
let n = 2;
|
|
5403
|
+
while (taken.has(id) && !ask.id)
|
|
5404
|
+
id = `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}-${n++}`;
|
|
5405
|
+
const clip = calloutClip(ask.shape, register, ask.words, {
|
|
5406
|
+
id,
|
|
5407
|
+
start: window2.start,
|
|
5408
|
+
duration: window2.duration,
|
|
5409
|
+
scale: appScaleAt(doc, window2.start + 0.35),
|
|
5410
|
+
pin,
|
|
5411
|
+
// The fallback place, in the margin: the lower third.
|
|
5412
|
+
at: { x: 0.5, y: 0.82 }
|
|
5413
|
+
});
|
|
5414
|
+
return { clip, window: window2 };
|
|
5415
|
+
}
|
|
5416
|
+
function isCalloutShape(s) {
|
|
5417
|
+
return CALLOUT_SHAPES.includes(s);
|
|
5418
|
+
}
|
|
5419
|
+
async function groundFindings(doc, frameAt) {
|
|
5420
|
+
const out = [];
|
|
5421
|
+
const overlays = doc.overlays ?? [];
|
|
5422
|
+
const layout = docCardLayout2(doc);
|
|
5423
|
+
const camera = cameraModel2(doc.frame);
|
|
5424
|
+
const track = docZoomTrack2(doc);
|
|
5425
|
+
const pins = resolvePins2(doc, layout, camera, track);
|
|
5426
|
+
for (let i = 0; i < overlays.length; i++) {
|
|
5427
|
+
const o = overlays[i];
|
|
5428
|
+
if (o.kind !== "html") continue;
|
|
5429
|
+
const card = calloutGroundOf(o.css ?? "");
|
|
5430
|
+
if (!card) {
|
|
5431
|
+
out.push({
|
|
5432
|
+
index: i,
|
|
5433
|
+
id: o.id,
|
|
5434
|
+
level: "note",
|
|
5435
|
+
deltaE: null,
|
|
5436
|
+
card: null,
|
|
5437
|
+
footage: null,
|
|
5438
|
+
message: `overlays[${i}] declares no readable background on its root rule \u2014 the ground check cannot read it`
|
|
5439
|
+
});
|
|
5440
|
+
continue;
|
|
5441
|
+
}
|
|
5442
|
+
const t = o.start + Math.min(0.35, o.duration / 2);
|
|
5443
|
+
const img = await frameAt(t);
|
|
5444
|
+
if (!img) {
|
|
5445
|
+
out.push({
|
|
5446
|
+
index: i,
|
|
5447
|
+
id: o.id,
|
|
5448
|
+
level: "note",
|
|
5449
|
+
deltaE: null,
|
|
5450
|
+
card,
|
|
5451
|
+
footage: null,
|
|
5452
|
+
message: `overlays[${i}]: the frame at ${t.toFixed(2)}s could not be read`
|
|
5453
|
+
});
|
|
5454
|
+
continue;
|
|
5455
|
+
}
|
|
5456
|
+
const box = pinBox2(o, layout);
|
|
5457
|
+
const pinned = pins.get(o.id);
|
|
5458
|
+
let cx = o.transform.x * layout.W;
|
|
5459
|
+
let cy = o.transform.y * layout.H;
|
|
5460
|
+
if (pinned) {
|
|
5461
|
+
const v = sample(pinned.track, t - o.start, lerpArray);
|
|
5462
|
+
cx = v[0] * layout.W;
|
|
5463
|
+
cy = v[1] * layout.H;
|
|
5464
|
+
}
|
|
5465
|
+
const sx = img.w / layout.W;
|
|
5466
|
+
const sy = img.h / layout.H;
|
|
5467
|
+
const rect = {
|
|
5468
|
+
x: (cx - box.w / 2) * sx,
|
|
5469
|
+
y: (cy - box.h / 2) * sy,
|
|
5470
|
+
w: box.w * sx,
|
|
5471
|
+
h: box.h * sy
|
|
5472
|
+
};
|
|
5473
|
+
const footage = medianColour(img, rect);
|
|
5474
|
+
const cardRgb = parseCssColour(card);
|
|
5475
|
+
if (!cardRgb) continue;
|
|
5476
|
+
const d = deltaE(cardRgb, footage);
|
|
5477
|
+
const fh = `#${footage.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
5478
|
+
const level = d < CALLOUT_DELTA_E_PROBLEM ? "problem" : d < CALLOUT_DELTA_E_WARNING ? "warning" : "note";
|
|
5479
|
+
out.push({
|
|
5480
|
+
index: i,
|
|
5481
|
+
id: o.id,
|
|
5482
|
+
level,
|
|
5483
|
+
deltaE: +d.toFixed(1),
|
|
5484
|
+
card,
|
|
5485
|
+
footage: fh,
|
|
5486
|
+
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`
|
|
5487
|
+
});
|
|
5488
|
+
}
|
|
5489
|
+
return out;
|
|
5490
|
+
}
|
|
5491
|
+
async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
5492
|
+
const out = /* @__PURE__ */ new Map();
|
|
5493
|
+
if (!times.length) return out;
|
|
5494
|
+
const bare = { ...doc, overlays: [] };
|
|
5495
|
+
const outDir = join10(dir, ".ground-check");
|
|
5496
|
+
const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
|
|
5497
|
+
for (const f of res.frames) {
|
|
5498
|
+
if (!existsSync9(f.file)) continue;
|
|
5499
|
+
out.set(
|
|
5500
|
+
+f.time.toFixed(3),
|
|
5501
|
+
decodePng(new Uint8Array(await readFile9(f.file)))
|
|
5502
|
+
);
|
|
5503
|
+
}
|
|
5504
|
+
return out;
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5160
5507
|
// src/plugin/kitPicture.ts
|
|
5161
5508
|
import { execFile } from "child_process";
|
|
5162
5509
|
import { promisify } from "util";
|
|
5163
|
-
import { readFile as
|
|
5510
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5164
5511
|
var execFileP = promisify(execFile);
|
|
5165
5512
|
var SUBJECT_BAND = { min: 0.6, max: 0.92 };
|
|
5166
5513
|
var BLANK_INK = 0.12;
|
|
@@ -5188,7 +5535,7 @@ function apcaContrast(text, bg) {
|
|
|
5188
5535
|
return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
|
|
5189
5536
|
}
|
|
5190
5537
|
var pct = (v) => `${Math.round(v * 100)}%`;
|
|
5191
|
-
var
|
|
5538
|
+
var hexToRgb2 = (h) => {
|
|
5192
5539
|
const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
|
|
5193
5540
|
if (!m) return null;
|
|
5194
5541
|
const n = parseInt(m[1], 16);
|
|
@@ -5337,7 +5684,7 @@ function textFindings(a, img) {
|
|
|
5337
5684
|
});
|
|
5338
5685
|
}
|
|
5339
5686
|
}
|
|
5340
|
-
const rgb = t.color ?
|
|
5687
|
+
const rgb = t.color ? hexToRgb2(t.color) : null;
|
|
5341
5688
|
if (rgb) {
|
|
5342
5689
|
const ground = medianColour(img, box);
|
|
5343
5690
|
const lc = apcaContrast(rgb, ground);
|
|
@@ -5436,7 +5783,7 @@ async function pictureChecks(assets) {
|
|
|
5436
5783
|
let ffmpeg = null;
|
|
5437
5784
|
for (const a of assets) {
|
|
5438
5785
|
if (/\.png$/i.test(a.file)) {
|
|
5439
|
-
const img = decodePng(new Uint8Array(await
|
|
5786
|
+
const img = decodePng(new Uint8Array(await readFile10(a.file)));
|
|
5440
5787
|
if (!img) {
|
|
5441
5788
|
findings.push({
|
|
5442
5789
|
code: "unreadable",
|
|
@@ -5529,9 +5876,9 @@ function formatFinding(f) {
|
|
|
5529
5876
|
}
|
|
5530
5877
|
|
|
5531
5878
|
// src/plugin/validateKit.ts
|
|
5532
|
-
import { existsSync as
|
|
5533
|
-
import { readFile as
|
|
5534
|
-
import { dirname as dirname2, isAbsolute as isAbsolute4, join as
|
|
5879
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5880
|
+
import { readFile as readFile11, stat as stat2 } from "fs/promises";
|
|
5881
|
+
import { dirname as dirname2, isAbsolute as isAbsolute4, join as join11 } from "path";
|
|
5535
5882
|
import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
|
|
5536
5883
|
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5537
5884
|
function pngDimensions(bytes) {
|
|
@@ -5550,7 +5897,7 @@ async function probeVideo(path) {
|
|
|
5550
5897
|
const MB = await import("mediabunny");
|
|
5551
5898
|
const input = new MB.Input({
|
|
5552
5899
|
formats: MB.ALL_FORMATS,
|
|
5553
|
-
source: new MB.BufferSource(new Uint8Array(await
|
|
5900
|
+
source: new MB.BufferSource(new Uint8Array(await readFile11(path)))
|
|
5554
5901
|
});
|
|
5555
5902
|
try {
|
|
5556
5903
|
const track = await input.getPrimaryVideoTrack();
|
|
@@ -5571,7 +5918,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5571
5918
|
const measured = [];
|
|
5572
5919
|
let kit;
|
|
5573
5920
|
try {
|
|
5574
|
-
kit = JSON.parse(await
|
|
5921
|
+
kit = JSON.parse(await readFile11(kitPath, "utf8"));
|
|
5575
5922
|
} catch (e) {
|
|
5576
5923
|
return {
|
|
5577
5924
|
valid: false,
|
|
@@ -5590,8 +5937,8 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5590
5937
|
}
|
|
5591
5938
|
const base = dirname2(kitPath);
|
|
5592
5939
|
const resolvePath = (p) => {
|
|
5593
|
-
if (isAbsolute4(p) &&
|
|
5594
|
-
const beside =
|
|
5940
|
+
if (isAbsolute4(p) && existsSync10(p)) return p;
|
|
5941
|
+
const beside = join11(base, p.split("/").pop() ?? p);
|
|
5595
5942
|
return beside;
|
|
5596
5943
|
};
|
|
5597
5944
|
const perDestination = /* @__PURE__ */ new Map();
|
|
@@ -5617,7 +5964,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5617
5964
|
let h = null;
|
|
5618
5965
|
let seconds = null;
|
|
5619
5966
|
if (/\.(png|jpe?g|webp)$/i.test(file)) {
|
|
5620
|
-
const head = Buffer.from(await
|
|
5967
|
+
const head = Buffer.from(await readFile11(file));
|
|
5621
5968
|
const kind = sniffImage(head);
|
|
5622
5969
|
if (file.toLowerCase().endsWith(".png") && kind !== "png")
|
|
5623
5970
|
problems.push(
|
|
@@ -5727,7 +6074,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5727
6074
|
|
|
5728
6075
|
// src/plugin/recorder.ts
|
|
5729
6076
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5730
|
-
import { join as
|
|
6077
|
+
import { join as join12 } from "path";
|
|
5731
6078
|
|
|
5732
6079
|
// src/plugin/recordingCap.ts
|
|
5733
6080
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
@@ -5815,7 +6162,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5815
6162
|
cdp.on("Page.screencastFrame", (ev) => {
|
|
5816
6163
|
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
5817
6164
|
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
5818
|
-
writeFileSync2(
|
|
6165
|
+
writeFileSync2(join12(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
5819
6166
|
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
5820
6167
|
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
5821
6168
|
});
|
|
@@ -5894,6 +6241,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5894
6241
|
const stepStart = now();
|
|
5895
6242
|
const skippedBefore = skipped.length;
|
|
5896
6243
|
const urlBefore = page.url();
|
|
6244
|
+
let stepRect = null;
|
|
5897
6245
|
switch (step.do) {
|
|
5898
6246
|
case "wait":
|
|
5899
6247
|
await sleep(clampWait(step.ms, now(), maxSeconds));
|
|
@@ -5903,6 +6251,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5903
6251
|
break;
|
|
5904
6252
|
case "hover": {
|
|
5905
6253
|
const rect = await boxOf(step.selector);
|
|
6254
|
+
stepRect = rect;
|
|
5906
6255
|
if (rect) {
|
|
5907
6256
|
await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
5908
6257
|
log(`hover ${step.selector}`);
|
|
@@ -5913,6 +6262,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5913
6262
|
}
|
|
5914
6263
|
case "click": {
|
|
5915
6264
|
const rect = await boxOf(step.selector);
|
|
6265
|
+
stepRect = rect;
|
|
5916
6266
|
if (rect) {
|
|
5917
6267
|
await clickAt(rect);
|
|
5918
6268
|
log(`click ${step.selector}`);
|
|
@@ -5923,6 +6273,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5923
6273
|
}
|
|
5924
6274
|
case "type": {
|
|
5925
6275
|
const rect = await boxOf(step.selector);
|
|
6276
|
+
stepRect = rect;
|
|
5926
6277
|
if (rect) {
|
|
5927
6278
|
if (step.focus !== false) await clickAt(rect);
|
|
5928
6279
|
const delay = step.delayMs ?? 40;
|
|
@@ -5964,6 +6315,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5964
6315
|
let rect;
|
|
5965
6316
|
if (step.selector) {
|
|
5966
6317
|
const box = await boxOf(step.selector);
|
|
6318
|
+
stepRect = box;
|
|
5967
6319
|
if (box) {
|
|
5968
6320
|
rect = box;
|
|
5969
6321
|
start = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
|
|
@@ -6026,7 +6378,15 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6026
6378
|
tStart: +(stepStart / 1e3).toFixed(3),
|
|
6027
6379
|
tEnd: +(now() / 1e3).toFixed(3),
|
|
6028
6380
|
...skipped.length > skippedBefore ? { skipped: true } : {},
|
|
6029
|
-
...page.url() !== urlBefore ? { navigated: true } : {}
|
|
6381
|
+
...page.url() !== urlBefore ? { navigated: true } : {},
|
|
6382
|
+
...stepRect ? {
|
|
6383
|
+
rect: {
|
|
6384
|
+
x: +stepRect.x.toFixed(1),
|
|
6385
|
+
y: +stepRect.y.toFixed(1),
|
|
6386
|
+
w: +stepRect.w.toFixed(1),
|
|
6387
|
+
h: +stepRect.h.toFixed(1)
|
|
6388
|
+
}
|
|
6389
|
+
} : {}
|
|
6030
6390
|
});
|
|
6031
6391
|
}
|
|
6032
6392
|
if (!capped) await sleep(600);
|
|
@@ -6040,7 +6400,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6040
6400
|
await sleep(200);
|
|
6041
6401
|
const pageTitle = await page.title().catch(() => "");
|
|
6042
6402
|
await context.close();
|
|
6043
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(
|
|
6403
|
+
const firstFrame = frames[0] ? jpegDims(readFileSync3(join12(paths.framesDir, frames[0].file))) : null;
|
|
6044
6404
|
const meta = {
|
|
6045
6405
|
dpr: 1,
|
|
6046
6406
|
zoom: 1,
|
|
@@ -6148,9 +6508,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6148
6508
|
}
|
|
6149
6509
|
|
|
6150
6510
|
// src/plugin/plan.ts
|
|
6151
|
-
import { existsSync as
|
|
6152
|
-
import { readFile as
|
|
6153
|
-
import { join as
|
|
6511
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6512
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6513
|
+
import { join as join13 } from "path";
|
|
6154
6514
|
import {
|
|
6155
6515
|
DEFAULT_FRAME_STYLE,
|
|
6156
6516
|
STYLE_FIELDS,
|
|
@@ -6372,10 +6732,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6372
6732
|
// src/plugin/plan.ts
|
|
6373
6733
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6374
6734
|
async function readDigestActivity(dir) {
|
|
6375
|
-
const file =
|
|
6376
|
-
if (!
|
|
6735
|
+
const file = join13(dir, "digest", "digest.json");
|
|
6736
|
+
if (!existsSync11(file)) return null;
|
|
6377
6737
|
try {
|
|
6378
|
-
const d = JSON.parse(await
|
|
6738
|
+
const d = JSON.parse(await readFile12(file, "utf8"));
|
|
6379
6739
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
6380
6740
|
} catch {
|
|
6381
6741
|
return null;
|
|
@@ -6585,17 +6945,17 @@ async function planTake(dir, opts = {}) {
|
|
|
6585
6945
|
|
|
6586
6946
|
// src/plugin/sync.ts
|
|
6587
6947
|
import { createHash } from "crypto";
|
|
6588
|
-
import { existsSync as
|
|
6589
|
-
import { readFile as
|
|
6948
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6949
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6590
6950
|
import { createInterface } from "readline/promises";
|
|
6591
|
-
import { basename, join as
|
|
6951
|
+
import { basename, join as join16 } from "path";
|
|
6592
6952
|
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
6593
6953
|
|
|
6594
6954
|
// src/plugin/media.ts
|
|
6595
6955
|
import { isKeyedOverlay } from "@vosjs/studio-core";
|
|
6596
|
-
import { createWriteStream, existsSync as
|
|
6956
|
+
import { createWriteStream, existsSync as existsSync12 } from "fs";
|
|
6597
6957
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
6598
|
-
import { join as
|
|
6958
|
+
import { join as join14 } from "path";
|
|
6599
6959
|
import { Readable } from "stream";
|
|
6600
6960
|
import { pipeline } from "stream/promises";
|
|
6601
6961
|
var MIC_NAME = "mic.webm";
|
|
@@ -6719,8 +7079,8 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6719
7079
|
const url = doc.source[key];
|
|
6720
7080
|
const assetId = assetIdOf(url);
|
|
6721
7081
|
if (!assetId) continue;
|
|
6722
|
-
const target =
|
|
6723
|
-
if (
|
|
7082
|
+
const target = join14(dir, file);
|
|
7083
|
+
if (existsSync12(target)) {
|
|
6724
7084
|
result.kept.push(file);
|
|
6725
7085
|
} else {
|
|
6726
7086
|
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
@@ -6745,8 +7105,8 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6745
7105
|
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
6746
7106
|
const assetId = assetIdOf(ref.key);
|
|
6747
7107
|
const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
|
|
6748
|
-
const existing =
|
|
6749
|
-
(fs) => fs.readdir(
|
|
7108
|
+
const existing = existsSync12(join14(dir, "media")) ? (await import("fs/promises").then(
|
|
7109
|
+
(fs) => fs.readdir(join14(dir, "media"))
|
|
6750
7110
|
)).find((f) => f.startsWith(`${assetId}.`)) : void 0;
|
|
6751
7111
|
let file;
|
|
6752
7112
|
if (existing) {
|
|
@@ -6763,9 +7123,9 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6763
7123
|
}
|
|
6764
7124
|
const ext = extensionFor(res.headers.get("content-type") ?? "");
|
|
6765
7125
|
file = `media/${assetId}${ext}`;
|
|
6766
|
-
const target =
|
|
7126
|
+
const target = join14(dir, file);
|
|
6767
7127
|
await import("fs/promises").then(
|
|
6768
|
-
(fs) => fs.mkdir(
|
|
7128
|
+
(fs) => fs.mkdir(join14(dir, "media"), { recursive: true })
|
|
6769
7129
|
);
|
|
6770
7130
|
await pipeline(
|
|
6771
7131
|
Readable.fromWeb(res.body),
|
|
@@ -6779,17 +7139,17 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
6779
7139
|
}
|
|
6780
7140
|
ref.set(file);
|
|
6781
7141
|
}
|
|
6782
|
-
const metaPath =
|
|
6783
|
-
if (!
|
|
6784
|
-
const cursorPath =
|
|
6785
|
-
if (!
|
|
6786
|
-
await writeFile9(
|
|
7142
|
+
const metaPath = join14(dir, "meta.json");
|
|
7143
|
+
if (!existsSync12(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
7144
|
+
const cursorPath = join14(dir, "cursor.json");
|
|
7145
|
+
if (!existsSync12(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
7146
|
+
await writeFile9(join14(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
6787
7147
|
return result;
|
|
6788
7148
|
}
|
|
6789
7149
|
|
|
6790
7150
|
// src/plugin/folder.ts
|
|
6791
7151
|
import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
|
|
6792
|
-
import { join as
|
|
7152
|
+
import { join as join15 } from "path";
|
|
6793
7153
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6794
7154
|
import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
6795
7155
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
|
|
@@ -6965,18 +7325,18 @@ async function cmdPull(argv) {
|
|
|
6965
7325
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
6966
7326
|
const payload = res.body;
|
|
6967
7327
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
6968
|
-
await mkdir7(
|
|
7328
|
+
await mkdir7(join15(out, "recipes"), { recursive: true });
|
|
6969
7329
|
const lines = [];
|
|
6970
7330
|
const recipes = [];
|
|
6971
7331
|
for (const rec of payload.recipes) {
|
|
6972
|
-
const path =
|
|
7332
|
+
const path = join15(out, "recipes", safeName(rec.filename));
|
|
6973
7333
|
await writeFile10(path, rec.body ?? "");
|
|
6974
7334
|
recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
|
|
6975
7335
|
}
|
|
6976
7336
|
for (const rec of payload.inheritedRecipes) {
|
|
6977
7337
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
6978
|
-
await mkdir7(
|
|
6979
|
-
const path =
|
|
7338
|
+
await mkdir7(join15(out, "recipes", "_inherited", from), { recursive: true });
|
|
7339
|
+
const path = join15(
|
|
6980
7340
|
out,
|
|
6981
7341
|
"recipes",
|
|
6982
7342
|
"_inherited",
|
|
@@ -6991,7 +7351,7 @@ async function cmdPull(argv) {
|
|
|
6991
7351
|
}
|
|
6992
7352
|
const members = [];
|
|
6993
7353
|
for (const v of payload.voses) {
|
|
6994
|
-
const dir =
|
|
7354
|
+
const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
|
|
6995
7355
|
await mkdir7(dir, { recursive: true });
|
|
6996
7356
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
6997
7357
|
if (cfg.status !== 200) {
|
|
@@ -6999,7 +7359,7 @@ async function cmdPull(argv) {
|
|
|
6999
7359
|
continue;
|
|
7000
7360
|
}
|
|
7001
7361
|
await writeFile10(
|
|
7002
|
-
|
|
7362
|
+
join15(dir, "config.json"),
|
|
7003
7363
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7004
7364
|
);
|
|
7005
7365
|
let take = false;
|
|
@@ -7011,7 +7371,7 @@ async function cmdPull(argv) {
|
|
|
7011
7371
|
);
|
|
7012
7372
|
if (doc.status === 200) {
|
|
7013
7373
|
await writeFile10(
|
|
7014
|
-
|
|
7374
|
+
join15(dir, "doc.json"),
|
|
7015
7375
|
JSON.stringify(doc.body, null, 2)
|
|
7016
7376
|
);
|
|
7017
7377
|
take = true;
|
|
@@ -7128,7 +7488,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7128
7488
|
throw new Error(`doc.json fails lint:
|
|
7129
7489
|
${lint.problems.join("\n ")}`);
|
|
7130
7490
|
}
|
|
7131
|
-
if (!
|
|
7491
|
+
if (!existsSync13(take.paths.recording)) {
|
|
7132
7492
|
throw new Error("no recording.webm in this take");
|
|
7133
7493
|
}
|
|
7134
7494
|
const ctx = apiContext(flags);
|
|
@@ -7160,7 +7520,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7160
7520
|
throw new Error("push cancelled");
|
|
7161
7521
|
}
|
|
7162
7522
|
}
|
|
7163
|
-
const bytes = await
|
|
7523
|
+
const bytes = await readFile13(take.paths.recording);
|
|
7164
7524
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7165
7525
|
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7166
7526
|
const upload = await api(ctx, "/assets/recording", {
|
|
@@ -7199,12 +7559,12 @@ async function pushTake(dir, flags, r) {
|
|
|
7199
7559
|
delete docForPush.source.micKey;
|
|
7200
7560
|
}
|
|
7201
7561
|
for (const ref of docMediaRefs(docForPush)) {
|
|
7202
|
-
const file =
|
|
7203
|
-
if (!
|
|
7562
|
+
const file = join16(dir, takeRelativeFile(ref.key));
|
|
7563
|
+
if (!existsSync13(file)) {
|
|
7204
7564
|
r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
|
|
7205
7565
|
continue;
|
|
7206
7566
|
}
|
|
7207
|
-
const media = await
|
|
7567
|
+
const media = await readFile13(file);
|
|
7208
7568
|
const mediaHash = createHash("sha256").update(media).digest("hex");
|
|
7209
7569
|
const type = mediaContentType(file);
|
|
7210
7570
|
const put = await api(ctx, "/assets/recording", {
|
|
@@ -7340,9 +7700,9 @@ async function pullTake(dir, flags, r) {
|
|
|
7340
7700
|
};
|
|
7341
7701
|
}
|
|
7342
7702
|
let media2;
|
|
7343
|
-
if (flags.media &&
|
|
7703
|
+
if (flags.media && existsSync13(join16(dir, "doc.json"))) {
|
|
7344
7704
|
const local = JSON.parse(
|
|
7345
|
-
await
|
|
7705
|
+
await readFile13(join16(dir, "doc.json"), "utf8")
|
|
7346
7706
|
);
|
|
7347
7707
|
media2 = await pullMedia(ctx, dir, local, r.log);
|
|
7348
7708
|
}
|
|
@@ -7402,10 +7762,10 @@ async function pullTake(dir, flags, r) {
|
|
|
7402
7762
|
}
|
|
7403
7763
|
const hostedDoc = migrateHostedDoc3(docRes.json);
|
|
7404
7764
|
const doc = hostedDoc;
|
|
7405
|
-
if (
|
|
7765
|
+
if (existsSync13(join16(dir, RECORDING_NAME))) {
|
|
7406
7766
|
doc.source.videoKey = RECORDING_NAME;
|
|
7407
7767
|
}
|
|
7408
|
-
await writeJson(
|
|
7768
|
+
await writeJson(join16(dir, "doc.json"), doc, true);
|
|
7409
7769
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
7410
7770
|
const versions = await api(ctx, `/vos/${vosId}/versions`);
|
|
7411
7771
|
const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
|
|
@@ -7424,10 +7784,10 @@ async function pullTake(dir, flags, r) {
|
|
|
7424
7784
|
}
|
|
7425
7785
|
|
|
7426
7786
|
// src/plugin/program.ts
|
|
7427
|
-
import { existsSync as
|
|
7428
|
-
import { mkdir as mkdir8, readFile as
|
|
7787
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7788
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
7429
7789
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7430
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
7790
|
+
import { basename as basename2, dirname as dirname3, join as join17 } from "path";
|
|
7431
7791
|
import {
|
|
7432
7792
|
CURRENT_CONFIG_VERSION,
|
|
7433
7793
|
migrateConfig,
|
|
@@ -7597,8 +7957,8 @@ function preflightConfig(parsed) {
|
|
|
7597
7957
|
}
|
|
7598
7958
|
function resolveConfigPath(target) {
|
|
7599
7959
|
if (target.endsWith(".json")) return target;
|
|
7600
|
-
const inDir =
|
|
7601
|
-
if (
|
|
7960
|
+
const inDir = join17(target, "config.json");
|
|
7961
|
+
if (existsSync14(inDir)) return inDir;
|
|
7602
7962
|
throw new UsageError(
|
|
7603
7963
|
`${target} has no config.json (and is not a take \u2014 no doc.json)`
|
|
7604
7964
|
);
|
|
@@ -7626,7 +7986,7 @@ async function cmdFetch(argv) {
|
|
|
7626
7986
|
const out = strFlag(flags, "out") ?? slug;
|
|
7627
7987
|
await mkdir8(out, { recursive: true });
|
|
7628
7988
|
await writeFile11(
|
|
7629
|
-
|
|
7989
|
+
join17(out, "config.json"),
|
|
7630
7990
|
JSON.stringify(cfg.body.config, null, 2)
|
|
7631
7991
|
);
|
|
7632
7992
|
const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
|
|
@@ -7651,12 +8011,12 @@ async function cmdFetch(argv) {
|
|
|
7651
8011
|
const hosted = migrateHostedDoc4(doc.body);
|
|
7652
8012
|
const { config: own } = await writeProgramDoc(out, hosted);
|
|
7653
8013
|
if (own) {
|
|
7654
|
-
await writeFile11(
|
|
8014
|
+
await writeFile11(join17(out, "config.json"), JSON.stringify(own, null, 2));
|
|
7655
8015
|
}
|
|
7656
8016
|
} else if (doc.status === 200) {
|
|
7657
8017
|
take = true;
|
|
7658
8018
|
const hosted = migrateHostedDoc4(doc.body);
|
|
7659
|
-
await writeFile11(
|
|
8019
|
+
await writeFile11(join17(out, "doc.json"), JSON.stringify(hosted, null, 2));
|
|
7660
8020
|
if (flags.media === true) {
|
|
7661
8021
|
const media = await pullMedia({ origin, key }, out, hosted, r.log);
|
|
7662
8022
|
mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
|
|
@@ -7769,7 +8129,7 @@ async function cmdPushProgram(argv) {
|
|
|
7769
8129
|
api: strFlag(flags, "api")
|
|
7770
8130
|
});
|
|
7771
8131
|
const dir = dirname3(source);
|
|
7772
|
-
const parsed = JSON.parse(await
|
|
8132
|
+
const parsed = JSON.parse(await readFile14(source, "utf8"));
|
|
7773
8133
|
const pre = preflightConfig(parsed);
|
|
7774
8134
|
if (!pre.ok || !pre.config) {
|
|
7775
8135
|
for (const issue of pre.issues) r.log(`error ${issue}`);
|
|
@@ -8010,10 +8370,10 @@ async function cmdPullProgram(argv) {
|
|
|
8010
8370
|
const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
|
|
8011
8371
|
if (cfg.status !== 200)
|
|
8012
8372
|
throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
|
|
8013
|
-
const configPath =
|
|
8373
|
+
const configPath = join17(dir, "config.json");
|
|
8014
8374
|
let backedUp = false;
|
|
8015
|
-
if (
|
|
8016
|
-
await writeFile11(
|
|
8375
|
+
if (existsSync14(configPath)) {
|
|
8376
|
+
await writeFile11(join17(dir, "config.backup.json"), await readFile14(configPath));
|
|
8017
8377
|
backedUp = true;
|
|
8018
8378
|
}
|
|
8019
8379
|
let headConfig = cfg.body.config;
|
|
@@ -8046,7 +8406,7 @@ async function cmdPullProgram(argv) {
|
|
|
8046
8406
|
protected: protectedIds,
|
|
8047
8407
|
changes,
|
|
8048
8408
|
out: configPath,
|
|
8049
|
-
backup: backedUp ?
|
|
8409
|
+
backup: backedUp ? join17(dir, "config.backup.json") : null
|
|
8050
8410
|
},
|
|
8051
8411
|
`Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
|
|
8052
8412
|
Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
|
|
@@ -8108,9 +8468,9 @@ function storedProgramConfig(config, programDoc) {
|
|
|
8108
8468
|
return lowerProgramDoc(doc).config;
|
|
8109
8469
|
}
|
|
8110
8470
|
async function readProgramDoc(dir, config) {
|
|
8111
|
-
const docPath =
|
|
8112
|
-
if (!
|
|
8113
|
-
const parsed = JSON.parse(await
|
|
8471
|
+
const docPath = join17(dir, "doc.json");
|
|
8472
|
+
if (!existsSync14(docPath)) return null;
|
|
8473
|
+
const parsed = JSON.parse(await readFile14(docPath, "utf8"));
|
|
8114
8474
|
if (typeof parsed !== "object" || parsed === null || "source" in parsed)
|
|
8115
8475
|
return null;
|
|
8116
8476
|
const raw = parsed;
|
|
@@ -8121,7 +8481,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8121
8481
|
const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
|
|
8122
8482
|
const { config, ...rest } = program;
|
|
8123
8483
|
const onDisk = { ...hosted, program: rest };
|
|
8124
|
-
await writeFile11(
|
|
8484
|
+
await writeFile11(join17(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
|
|
8125
8485
|
return {
|
|
8126
8486
|
config: config && typeof config === "object" ? config : null
|
|
8127
8487
|
};
|
|
@@ -9177,8 +9537,8 @@ async function cmdActions(argv) {
|
|
|
9177
9537
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
9178
9538
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
9179
9539
|
}
|
|
9180
|
-
const { readFile:
|
|
9181
|
-
const text = await
|
|
9540
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
9541
|
+
const text = await readFile16(resolve8(input), "utf8");
|
|
9182
9542
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
9183
9543
|
for (const p of problems) r.log(` ${p}`);
|
|
9184
9544
|
const result = convertAgentBrowser(records, {
|
|
@@ -9232,7 +9592,8 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
9232
9592
|
"yes",
|
|
9233
9593
|
"composed",
|
|
9234
9594
|
"check",
|
|
9235
|
-
"motion"
|
|
9595
|
+
"motion",
|
|
9596
|
+
"leader"
|
|
9236
9597
|
]);
|
|
9237
9598
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
|
|
9238
9599
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -9247,6 +9608,7 @@ Take pipeline
|
|
|
9247
9608
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
9248
9609
|
vos brand <url> [--out BRAND.md] [--json]
|
|
9249
9610
|
vos open <take> [--studio <url>] [--print]
|
|
9611
|
+
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]
|
|
9250
9612
|
vos validate <actions.json|take|kit.json> [--picture] [--json]
|
|
9251
9613
|
vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
|
|
9252
9614
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
@@ -9480,7 +9842,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
9480
9842
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
9481
9843
|
`;
|
|
9482
9844
|
async function loadActions(file) {
|
|
9483
|
-
const raw = JSON.parse(await
|
|
9845
|
+
const raw = JSON.parse(await readFile15(file, "utf8"));
|
|
9484
9846
|
const errors = validateActions(raw);
|
|
9485
9847
|
if (errors.length)
|
|
9486
9848
|
throw new UsageError(`invalid actions file:
|
|
@@ -9539,7 +9901,7 @@ async function cmdRecord(argv) {
|
|
|
9539
9901
|
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
9540
9902
|
const backdrop = await takeBackdrop(flags, r);
|
|
9541
9903
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9542
|
-
if (
|
|
9904
|
+
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
9543
9905
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9544
9906
|
r.log(
|
|
9545
9907
|
`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" : "")
|
|
@@ -9586,7 +9948,7 @@ async function cmdRecord(argv) {
|
|
|
9586
9948
|
},
|
|
9587
9949
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
|
|
9588
9950
|
${(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}
|
|
9589
|
-
Next: edit ${
|
|
9951
|
+
Next: edit ${join18(outDir, "doc.json")} (optional), then: vos render ${outDir}`
|
|
9590
9952
|
);
|
|
9591
9953
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
9592
9954
|
} finally {
|
|
@@ -9615,14 +9977,14 @@ async function cmdCreate2(argv) {
|
|
|
9615
9977
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9616
9978
|
throw new UsageError("--format must be webm or mp4");
|
|
9617
9979
|
const format = fmtRaw;
|
|
9618
|
-
const out = positionals[0] ??
|
|
9980
|
+
const out = positionals[0] ?? join18(outDir, `out.${format}`);
|
|
9619
9981
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9620
9982
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9621
9983
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
9622
9984
|
}
|
|
9623
9985
|
const backdrop = await takeBackdrop(flags, r);
|
|
9624
9986
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
9625
|
-
if (
|
|
9987
|
+
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
9626
9988
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
9627
9989
|
r.log(
|
|
9628
9990
|
`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" : "")
|
|
@@ -9713,24 +10075,24 @@ async function cmdPlan(argv) {
|
|
|
9713
10075
|
);
|
|
9714
10076
|
const r = createReporter(flags.json === true);
|
|
9715
10077
|
if (flags.fresh === true) {
|
|
9716
|
-
await rm4(
|
|
10078
|
+
await rm4(join18(dir, "doc.json"), { force: true });
|
|
9717
10079
|
r.log("note: --fresh discarded the existing doc.json");
|
|
9718
10080
|
}
|
|
9719
10081
|
let reuse;
|
|
9720
10082
|
if (flags.reuse === true) {
|
|
9721
|
-
const from = resolve9(strFlag(flags, "from") ??
|
|
9722
|
-
if (!
|
|
10083
|
+
const from = resolve9(strFlag(flags, "from") ?? join18(dir, PREV_DOC_NAME));
|
|
10084
|
+
if (!existsSync15(from)) {
|
|
9723
10085
|
throw new UsageError(
|
|
9724
10086
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
9725
10087
|
);
|
|
9726
10088
|
}
|
|
9727
10089
|
reuse = {
|
|
9728
10090
|
from,
|
|
9729
|
-
doc: JSON.parse(await
|
|
10091
|
+
doc: JSON.parse(await readFile15(from, "utf8"))
|
|
9730
10092
|
};
|
|
9731
10093
|
}
|
|
9732
10094
|
const style = await resolveStyleRef(flags);
|
|
9733
|
-
const hasDoc =
|
|
10095
|
+
const hasDoc = existsSync15(join18(dir, "doc.json"));
|
|
9734
10096
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
9735
10097
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
9736
10098
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
@@ -9788,7 +10150,7 @@ async function cmdPlan(argv) {
|
|
|
9788
10150
|
...s.motion ? { motion: s.motion } : {},
|
|
9789
10151
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
9790
10152
|
},
|
|
9791
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
10153
|
+
`${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}`
|
|
9792
10154
|
);
|
|
9793
10155
|
return EXIT_OK;
|
|
9794
10156
|
}
|
|
@@ -9914,7 +10276,7 @@ async function cmdRender(argv) {
|
|
|
9914
10276
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
9915
10277
|
throw new UsageError("--format must be webm or mp4");
|
|
9916
10278
|
const format = fmtRaw;
|
|
9917
|
-
const out = positionals[1] ??
|
|
10279
|
+
const out = positionals[1] ?? join18(dir, `out.${format}`);
|
|
9918
10280
|
const parallel = numFlag(flags, "parallel", 1);
|
|
9919
10281
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
9920
10282
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -10161,15 +10523,15 @@ async function resolveStyleRef(flags) {
|
|
|
10161
10523
|
}
|
|
10162
10524
|
async function resolveDocRef(ref, flags, what) {
|
|
10163
10525
|
const styleRef = ref;
|
|
10164
|
-
const file =
|
|
10526
|
+
const file = existsSync15(styleRef) ? resolve9(
|
|
10165
10527
|
styleRef,
|
|
10166
|
-
|
|
10528
|
+
existsSync15(join18(styleRef, "doc.json")) ? "doc.json" : ""
|
|
10167
10529
|
) : null;
|
|
10168
|
-
if (file &&
|
|
10530
|
+
if (file && existsSync15(file)) {
|
|
10169
10531
|
return {
|
|
10170
10532
|
from: file,
|
|
10171
10533
|
doc: migrateHostedDoc5(
|
|
10172
|
-
JSON.parse(await
|
|
10534
|
+
JSON.parse(await readFile15(file, "utf8"))
|
|
10173
10535
|
)
|
|
10174
10536
|
};
|
|
10175
10537
|
}
|
|
@@ -10221,7 +10583,7 @@ async function cmdDigest(argv) {
|
|
|
10221
10583
|
const take = await loadTake(dir);
|
|
10222
10584
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
10223
10585
|
const transcriptPath = strFlag(flags, "transcript");
|
|
10224
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
10586
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
|
|
10225
10587
|
const style = await resolveStyleRef(flags);
|
|
10226
10588
|
const noFrames = flags["no-frames"] === true;
|
|
10227
10589
|
let browser = null;
|
|
@@ -10252,7 +10614,7 @@ async function cmdDigest(argv) {
|
|
|
10252
10614
|
kinds,
|
|
10253
10615
|
frames: d.images.full,
|
|
10254
10616
|
crops: d.images.crop,
|
|
10255
|
-
sheet: d.images.sheet ?
|
|
10617
|
+
sheet: d.images.sheet ? join18(result.outDir, d.images.sheet) : null,
|
|
10256
10618
|
scenes: kinds.scene ?? 0,
|
|
10257
10619
|
sourceDuration: d.take.sourceDuration,
|
|
10258
10620
|
outputDuration: d.take.outputDuration,
|
|
@@ -10276,7 +10638,7 @@ async function cmdOpen(argv) {
|
|
|
10276
10638
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
10277
10639
|
const r = createReporter(flags.json === true);
|
|
10278
10640
|
const take = await loadTake(dir);
|
|
10279
|
-
if (!
|
|
10641
|
+
if (!existsSync15(take.paths.recording)) {
|
|
10280
10642
|
throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
|
|
10281
10643
|
}
|
|
10282
10644
|
const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
|
|
@@ -10317,12 +10679,12 @@ async function cmdJudge(argv) {
|
|
|
10317
10679
|
"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)"
|
|
10318
10680
|
);
|
|
10319
10681
|
const r = createReporter(flags.json === true);
|
|
10320
|
-
const kitFile = target.endsWith("kit.json") ? target :
|
|
10321
|
-
if (!
|
|
10322
|
-
if (!
|
|
10682
|
+
const kitFile = target.endsWith("kit.json") ? target : join18(target, "kit.json");
|
|
10683
|
+
if (!existsSync15(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
10684
|
+
if (!existsSync15(against))
|
|
10323
10685
|
throw new UsageError(`${against}: no reference manifest`);
|
|
10324
10686
|
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
10325
|
-
const verdicts = JSON.parse(await
|
|
10687
|
+
const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
|
|
10326
10688
|
const rate = winRate(verdicts);
|
|
10327
10689
|
const lines = result.sheets.map(
|
|
10328
10690
|
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
@@ -10337,12 +10699,109 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
|
|
|
10337
10699
|
);
|
|
10338
10700
|
return EXIT_OK;
|
|
10339
10701
|
}
|
|
10702
|
+
async function cmdCallout(argv) {
|
|
10703
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
|
|
10704
|
+
const dir = positionals[0];
|
|
10705
|
+
const shape = positionals[1];
|
|
10706
|
+
if (!dir || !shape || !isCalloutShape(shape))
|
|
10707
|
+
throw new UsageError(
|
|
10708
|
+
'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]'
|
|
10709
|
+
);
|
|
10710
|
+
const r = createReporter(flags.json === true);
|
|
10711
|
+
const take = await loadTake(dir);
|
|
10712
|
+
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
10713
|
+
const brand = await readBrandBesideTake(dir, strFlag(flags, "brand"));
|
|
10714
|
+
const bodyPx = strFlag(flags, "body-px");
|
|
10715
|
+
const register = registerFrom(brand?.roles ?? null, {
|
|
10716
|
+
ground: strFlag(flags, "ground"),
|
|
10717
|
+
accent: strFlag(flags, "accent"),
|
|
10718
|
+
face: strFlag(flags, "font"),
|
|
10719
|
+
mono: strFlag(flags, "mono"),
|
|
10720
|
+
body: bodyPx !== void 0 ? Number(bodyPx) : void 0
|
|
10721
|
+
});
|
|
10722
|
+
if (!register)
|
|
10723
|
+
throw new UsageError(
|
|
10724
|
+
`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`
|
|
10725
|
+
);
|
|
10726
|
+
const codeFile = strFlag(flags, "code-file");
|
|
10727
|
+
const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
|
|
10728
|
+
const words = {
|
|
10729
|
+
kicker: strFlag(flags, "kicker"),
|
|
10730
|
+
title: strFlag(flags, "title"),
|
|
10731
|
+
body: strFlag(flags, "body"),
|
|
10732
|
+
code: code?.replace(/\\n/g, "\n")
|
|
10733
|
+
};
|
|
10734
|
+
if (shape === "code" && !words.code)
|
|
10735
|
+
throw new UsageError(
|
|
10736
|
+
'a code callout needs --code "\u2026" or --code-file <file>'
|
|
10737
|
+
);
|
|
10738
|
+
if (shape === "note" && !words.title)
|
|
10739
|
+
throw new UsageError('a note needs --title "\u2026"');
|
|
10740
|
+
if (shape === "tag" && !words.kicker && !words.title)
|
|
10741
|
+
throw new UsageError('a tag needs --kicker "\u2026"');
|
|
10742
|
+
const stepRaw = strFlag(flags, "step");
|
|
10743
|
+
const atRaw = strFlag(flags, "at");
|
|
10744
|
+
const secondsRaw = strFlag(flags, "seconds");
|
|
10745
|
+
const side = strFlag(flags, "side");
|
|
10746
|
+
const mark = strFlag(flags, "mark");
|
|
10747
|
+
let composed;
|
|
10748
|
+
try {
|
|
10749
|
+
composed = composeCallout(take.doc, register, {
|
|
10750
|
+
shape,
|
|
10751
|
+
words,
|
|
10752
|
+
id: strFlag(flags, "id"),
|
|
10753
|
+
step: stepRaw !== void 0 ? /^\d+$/.test(stepRaw) ? Number(stepRaw) : stepRaw : void 0,
|
|
10754
|
+
at: atRaw !== void 0 ? Number(atRaw) : void 0,
|
|
10755
|
+
seconds: secondsRaw !== void 0 ? Number(secondsRaw) : void 0,
|
|
10756
|
+
side,
|
|
10757
|
+
mark,
|
|
10758
|
+
leader: flags.leader === true,
|
|
10759
|
+
color: strFlag(flags, "color")
|
|
10760
|
+
});
|
|
10761
|
+
} catch (e) {
|
|
10762
|
+
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
10763
|
+
}
|
|
10764
|
+
const { clip, window: window2 } = composed;
|
|
10765
|
+
if (flags.print === true) {
|
|
10766
|
+
r.done(
|
|
10767
|
+
{ clip, window: window2, register, brand: brand?.file ?? null },
|
|
10768
|
+
JSON.stringify(clip, null, 2)
|
|
10769
|
+
);
|
|
10770
|
+
return EXIT_OK;
|
|
10771
|
+
}
|
|
10772
|
+
const doc = take.doc;
|
|
10773
|
+
const overlays = (doc.overlays ?? []).filter((o) => o.id !== clip.id);
|
|
10774
|
+
const next = { ...doc, overlays: [...overlays, clip] };
|
|
10775
|
+
const lint = lintDoc(next);
|
|
10776
|
+
if (lint.problems.length) {
|
|
10777
|
+
r.done(
|
|
10778
|
+
{ valid: false, problems: lint.problems, warnings: lint.warnings, clip },
|
|
10779
|
+
`the callout would not validate:
|
|
10780
|
+
${lint.problems.join("\n ")}`
|
|
10781
|
+
);
|
|
10782
|
+
return EXIT_ERROR;
|
|
10783
|
+
}
|
|
10784
|
+
await writeJson(take.paths.doc, next, true);
|
|
10785
|
+
r.done(
|
|
10786
|
+
{
|
|
10787
|
+
clip,
|
|
10788
|
+
window: window2,
|
|
10789
|
+
register,
|
|
10790
|
+
brand: brand?.file ?? null,
|
|
10791
|
+
warnings: lint.warnings
|
|
10792
|
+
},
|
|
10793
|
+
`${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 ? `
|
|
10794
|
+
warnings:
|
|
10795
|
+
${lint.warnings.join("\n ")}` : ""}`
|
|
10796
|
+
);
|
|
10797
|
+
return EXIT_OK;
|
|
10798
|
+
}
|
|
10340
10799
|
async function cmdValidate(argv) {
|
|
10341
10800
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
|
|
10342
10801
|
const target = positionals[0];
|
|
10343
10802
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
10344
10803
|
const r = createReporter(flags.json === true);
|
|
10345
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
10804
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync15(join18(target, "kit.json")) && !isTakeDir(target) ? join18(target, "kit.json") : null;
|
|
10346
10805
|
if (kitTarget) {
|
|
10347
10806
|
const picture = flags.picture === true;
|
|
10348
10807
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -10374,9 +10833,9 @@ async function cmdValidate(argv) {
|
|
|
10374
10833
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
10375
10834
|
return EXIT_OK;
|
|
10376
10835
|
}
|
|
10377
|
-
if (!isTakeDir(target) &&
|
|
10836
|
+
if (!isTakeDir(target) && existsSync15(join18(target, "config.json"))) {
|
|
10378
10837
|
const parsed = JSON.parse(
|
|
10379
|
-
await
|
|
10838
|
+
await readFile15(join18(target, "config.json"), "utf8")
|
|
10380
10839
|
);
|
|
10381
10840
|
const pre = preflightConfig(parsed);
|
|
10382
10841
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -10417,7 +10876,7 @@ async function cmdValidate(argv) {
|
|
|
10417
10876
|
const take = await loadTake(target);
|
|
10418
10877
|
const problems = [];
|
|
10419
10878
|
const warnings = [];
|
|
10420
|
-
if (!
|
|
10879
|
+
if (!existsSync15(take.paths.recording))
|
|
10421
10880
|
problems.push("missing recording.webm (re-run record)");
|
|
10422
10881
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
10423
10882
|
if (take.doc) {
|
|
@@ -10429,6 +10888,36 @@ async function cmdValidate(argv) {
|
|
|
10429
10888
|
);
|
|
10430
10889
|
problems.push(...probe.problems);
|
|
10431
10890
|
warnings.push(...probe.warnings);
|
|
10891
|
+
if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
|
|
10892
|
+
const doc = take.doc;
|
|
10893
|
+
const times = [
|
|
10894
|
+
...new Set(
|
|
10895
|
+
(doc.overlays ?? []).filter((o) => o.kind === "html").map((o) => +(o.start + Math.min(0.35, o.duration / 2)).toFixed(3))
|
|
10896
|
+
)
|
|
10897
|
+
];
|
|
10898
|
+
const browser = await launchBrowser();
|
|
10899
|
+
try {
|
|
10900
|
+
const frames = await footageFrames(
|
|
10901
|
+
browser,
|
|
10902
|
+
target,
|
|
10903
|
+
doc,
|
|
10904
|
+
times,
|
|
10905
|
+
framesTake
|
|
10906
|
+
);
|
|
10907
|
+
const findings = await groundFindings(
|
|
10908
|
+
doc,
|
|
10909
|
+
async (t) => frames.get(+t.toFixed(3)) ?? null
|
|
10910
|
+
);
|
|
10911
|
+
for (const f of findings) {
|
|
10912
|
+
if (f.level === "problem") problems.push(`doc.json: ${f.message}`);
|
|
10913
|
+
else if (f.level === "warning")
|
|
10914
|
+
warnings.push(`doc.json: ${f.message}`);
|
|
10915
|
+
else r.event({ event: "ground", ...f });
|
|
10916
|
+
}
|
|
10917
|
+
} finally {
|
|
10918
|
+
await browser.close();
|
|
10919
|
+
}
|
|
10920
|
+
}
|
|
10432
10921
|
}
|
|
10433
10922
|
if (problems.length) {
|
|
10434
10923
|
r.done(
|
|
@@ -10545,6 +11034,8 @@ async function run(argv) {
|
|
|
10545
11034
|
return await cmdOpen(rest);
|
|
10546
11035
|
case "validate":
|
|
10547
11036
|
return await cmdValidate(rest);
|
|
11037
|
+
case "callout":
|
|
11038
|
+
return await cmdCallout(rest);
|
|
10548
11039
|
case "judge":
|
|
10549
11040
|
return await cmdJudge(rest);
|
|
10550
11041
|
case "fetch":
|
|
@@ -10628,4 +11119,4 @@ export {
|
|
|
10628
11119
|
convertAgentBrowser,
|
|
10629
11120
|
run
|
|
10630
11121
|
};
|
|
10631
|
-
//# sourceMappingURL=chunk-
|
|
11122
|
+
//# sourceMappingURL=chunk-6O55OD6D.js.map
|