@vosjs/cli 0.77.1 → 0.78.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 +83 -16
- package/dist/{assetManifest-MQ7HO7RZ.js → assetManifest-S6H5F2VE.js} +2 -2
- package/dist/{chunk-IJZ3C435.js → chunk-INRUYQ6J.js} +3223 -325
- package/dist/chunk-INRUYQ6J.js.map +1 -0
- package/dist/{chunk-KDIRW72G.js → chunk-JYCW4EOA.js} +6 -3
- package/dist/chunk-JYCW4EOA.js.map +1 -0
- package/dist/{chunk-2KEQ3LKH.js → chunk-RUDDBHQ7.js} +18 -2
- package/dist/chunk-RUDDBHQ7.js.map +1 -0
- package/dist/{chunk-UY2N42P2.js → chunk-ZSJEISKL.js} +6 -2
- package/dist/{chunk-UY2N42P2.js.map → chunk-ZSJEISKL.js.map} +1 -1
- package/dist/cli.js +6 -6
- package/dist/index.d.ts +29 -26
- package/dist/index.js +4 -4
- package/dist/manifest-6DJSPKVH.js +8 -0
- package/dist/{run-UNOXIGZ3.js → run-TIFM3ZHI.js} +6 -4
- package/dist/{shotList-MAQVLZBY.js → shotList-SCA234OS.js} +19 -1
- package/dist/shotList-SCA234OS.js.map +1 -0
- package/package.json +4 -4
- package/schema/actions.schema.json +192 -0
- package/schema/doc.schema.json +26 -2
- package/dist/chunk-2KEQ3LKH.js.map +0 -1
- package/dist/chunk-IJZ3C435.js.map +0 -1
- package/dist/chunk-KDIRW72G.js.map +0 -1
- package/dist/manifest-QLQKVSML.js +0 -8
- package/dist/shotList-MAQVLZBY.js.map +0 -1
- /package/dist/{assetManifest-MQ7HO7RZ.js.map → assetManifest-S6H5F2VE.js.map} +0 -0
- /package/dist/{manifest-QLQKVSML.js.map → manifest-6DJSPKVH.js.map} +0 -0
- /package/dist/{run-UNOXIGZ3.js.map → run-TIFM3ZHI.js.map} +0 -0
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
reencodeStill,
|
|
10
10
|
renderStill,
|
|
11
11
|
videoFormat
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JYCW4EOA.js";
|
|
13
13
|
import {
|
|
14
14
|
PREV_DOC_NAME,
|
|
15
15
|
RECORDING_NAME,
|
|
16
16
|
docLayerRefs,
|
|
17
17
|
docMediaRefs,
|
|
18
|
+
docObjectRefs,
|
|
19
|
+
docOverlayRefs,
|
|
18
20
|
dropScreencastFrames,
|
|
19
21
|
ensureTakeDir,
|
|
20
22
|
fileSha256,
|
|
@@ -37,7 +39,7 @@ import {
|
|
|
37
39
|
uploadDocRefs,
|
|
38
40
|
uploadManifest,
|
|
39
41
|
writeJson
|
|
40
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-ZSJEISKL.js";
|
|
41
43
|
import {
|
|
42
44
|
CREDENTIALS_PATH,
|
|
43
45
|
MEDIA_HEAD_BYTES,
|
|
@@ -62,6 +64,7 @@ import {
|
|
|
62
64
|
programAssets,
|
|
63
65
|
promptHint,
|
|
64
66
|
readSyncState,
|
|
67
|
+
refuseSymlink,
|
|
65
68
|
requireCredential,
|
|
66
69
|
resolveCredential,
|
|
67
70
|
resolveMediaType,
|
|
@@ -133,12 +136,12 @@ function landedLines(l) {
|
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
// src/plugin/run.ts
|
|
136
|
-
import { mkdir as
|
|
137
|
-
import { existsSync as
|
|
138
|
-
import { join as
|
|
139
|
+
import { mkdir as mkdir13, readFile as readFile15, rm as rm5 } from "fs/promises";
|
|
140
|
+
import { existsSync as existsSync27 } from "fs";
|
|
141
|
+
import { join as join29, resolve as resolve18 } from "path";
|
|
139
142
|
import {
|
|
140
143
|
END_CARD_FROM as END_CARD_FROM2,
|
|
141
|
-
docOutputDuration as
|
|
144
|
+
docOutputDuration as docOutputDuration4,
|
|
142
145
|
migrateHostedDoc as migrateHostedDoc4
|
|
143
146
|
} from "@vosjs/studio-core";
|
|
144
147
|
|
|
@@ -215,7 +218,10 @@ import {
|
|
|
215
218
|
docCardLayout,
|
|
216
219
|
docZoomTrack,
|
|
217
220
|
findStep,
|
|
221
|
+
formatElapsed,
|
|
222
|
+
hasElapsed,
|
|
218
223
|
outputEnd,
|
|
224
|
+
resolveAnchor,
|
|
219
225
|
pinBox,
|
|
220
226
|
pinCandidates,
|
|
221
227
|
pinRectOnScreen,
|
|
@@ -451,6 +457,7 @@ function lintDoc(docIn) {
|
|
|
451
457
|
const doc = docIn;
|
|
452
458
|
const problems = [];
|
|
453
459
|
const warnings = [];
|
|
460
|
+
const notes = [];
|
|
454
461
|
const recording = isObj(doc.source);
|
|
455
462
|
const source = isObj(doc.source) ? doc.source : {};
|
|
456
463
|
const meta = isObj(source.meta) ? source.meta : {};
|
|
@@ -1047,6 +1054,15 @@ function lintDoc(docIn) {
|
|
|
1047
1054
|
footageEnd = null;
|
|
1048
1055
|
}
|
|
1049
1056
|
}
|
|
1057
|
+
let anchorPieces = null;
|
|
1058
|
+
if (recording) {
|
|
1059
|
+
try {
|
|
1060
|
+
anchorPieces = ratedSegments(doc);
|
|
1061
|
+
} catch {
|
|
1062
|
+
anchorPieces = null;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const captionTimes = [];
|
|
1050
1066
|
for (let i = 0; i < (overlays?.length ?? 0); i++) {
|
|
1051
1067
|
const o = overlays[i];
|
|
1052
1068
|
const name = `overlays[${i}]`;
|
|
@@ -1054,6 +1070,24 @@ function lintDoc(docIn) {
|
|
|
1054
1070
|
problems.push(`${name} must be an object`);
|
|
1055
1071
|
continue;
|
|
1056
1072
|
}
|
|
1073
|
+
const playsAt = checkOverlayAnchor(
|
|
1074
|
+
o,
|
|
1075
|
+
name,
|
|
1076
|
+
docIn,
|
|
1077
|
+
recording,
|
|
1078
|
+
anchorPieces,
|
|
1079
|
+
problems,
|
|
1080
|
+
warnings,
|
|
1081
|
+
notes
|
|
1082
|
+
);
|
|
1083
|
+
if (o.kind === "text" && o.preset === "caption" && o.pin === void 0 && isNum(playsAt) && isNum(o.duration) && o.duration > 0) {
|
|
1084
|
+
const label = typeof o.id === "string" ? `${name} (${o.id})` : name;
|
|
1085
|
+
captionTimes.push({
|
|
1086
|
+
name: label,
|
|
1087
|
+
start: playsAt,
|
|
1088
|
+
end: playsAt + o.duration
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1057
1091
|
if (o.kind !== "text" && o.kind !== "image" && o.kind !== "video" && o.kind !== "html") {
|
|
1058
1092
|
problems.push(
|
|
1059
1093
|
`${name}.kind must be "text" | "image" | "video" | "html" (got ${String(o.kind)})`
|
|
@@ -1064,14 +1098,14 @@ function lintDoc(docIn) {
|
|
|
1064
1098
|
if (!isNum(o.duration) || o.duration <= 0) {
|
|
1065
1099
|
problems.push(`${name}.duration must be > 0 (seconds)`);
|
|
1066
1100
|
}
|
|
1067
|
-
if (footageEnd !== null && footageEnd > 0 && isNum(
|
|
1068
|
-
const over =
|
|
1101
|
+
if (footageEnd !== null && footageEnd > 0 && isNum(playsAt) && isNum(o.duration)) {
|
|
1102
|
+
const over = playsAt + o.duration - footageEnd;
|
|
1069
1103
|
const frameAnim = isObj(docIn) && isObj(docIn.frame) && isObj(docIn.frame.anim) ? docIn.frame.anim : null;
|
|
1070
1104
|
const exit = frameAnim ? isObj(frameAnim.exit) ? frameAnim.exit.kind : frameAnim.exit : void 0;
|
|
1071
1105
|
const cardLeaves = exit !== void 0 && exit !== "none";
|
|
1072
1106
|
if (over > 0.05 && !cardLeaves) {
|
|
1073
1107
|
warnings.push(
|
|
1074
|
-
|
|
1108
|
+
playsAt >= footageEnd ? `${name} starts at ${playsAt.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays over the card's held last frame and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the card holds its last frame under it. Shorten it, put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')`
|
|
1075
1109
|
);
|
|
1076
1110
|
}
|
|
1077
1111
|
}
|
|
@@ -1616,6 +1650,17 @@ function lintDoc(docIn) {
|
|
|
1616
1650
|
}
|
|
1617
1651
|
}
|
|
1618
1652
|
}
|
|
1653
|
+
captionTimes.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1654
|
+
for (let i = 0; i < captionTimes.length; i++) {
|
|
1655
|
+
for (let j = i + 1; j < captionTimes.length; j++) {
|
|
1656
|
+
const a = captionTimes[i];
|
|
1657
|
+
const b = captionTimes[j];
|
|
1658
|
+
if (b.start >= a.end - EPS) break;
|
|
1659
|
+
warnings.push(
|
|
1660
|
+
`${a.name} (${a.start.toFixed(2)}\u2013${a.end.toFixed(2)}s) and ${b.name} (${b.start.toFixed(2)}\u2013${b.end.toFixed(2)}s) are captions on screen at the same time; shorten the first, move one (an anchor's offset), or drop one`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1619
1664
|
if (isObj(doc.export)) {
|
|
1620
1665
|
const resolution = doc.export.resolution;
|
|
1621
1666
|
const validResolution = EXPORT_RESOLUTION_OPTIONS.includes(resolution);
|
|
@@ -1643,7 +1688,66 @@ function lintDoc(docIn) {
|
|
|
1643
1688
|
}
|
|
1644
1689
|
if (recording)
|
|
1645
1690
|
framingWarnings(docIn, doc, zoom, overlays ?? [], warnings);
|
|
1646
|
-
return { problems, warnings };
|
|
1691
|
+
return { problems, warnings, ...notes.length ? { notes } : {} };
|
|
1692
|
+
}
|
|
1693
|
+
function checkOverlayAnchor(o, name, docIn, recording, pieces, problems, warnings, notes) {
|
|
1694
|
+
const fallback = isNum(o.start) ? o.start : null;
|
|
1695
|
+
const words2 = o.kind === "text" ? o.text : void 0;
|
|
1696
|
+
const elapsed = o.kind === "text" && hasElapsed(words2);
|
|
1697
|
+
if (o.anchor === void 0) {
|
|
1698
|
+
if (elapsed)
|
|
1699
|
+
warnings.push(
|
|
1700
|
+
`${name}: its words carry {elapsed}, which fills only from an anchored step's off-camera wait; anchor it (anchor: { step }) or write the duration`
|
|
1701
|
+
);
|
|
1702
|
+
return fallback;
|
|
1703
|
+
}
|
|
1704
|
+
const a = o.anchor;
|
|
1705
|
+
if (!isObj(a)) {
|
|
1706
|
+
problems.push(`${name}.anchor must be { step, media?, at?, offset? }`);
|
|
1707
|
+
return fallback;
|
|
1708
|
+
}
|
|
1709
|
+
let shaped = true;
|
|
1710
|
+
if (typeof a.step !== "string" && !(isNum(a.step) && Number.isInteger(a.step) && a.step >= 0)) {
|
|
1711
|
+
problems.push(
|
|
1712
|
+
`${name}.anchor.step must be a step id (string) or its index (integer \u2265 0)`
|
|
1713
|
+
);
|
|
1714
|
+
shaped = false;
|
|
1715
|
+
}
|
|
1716
|
+
if (a.media !== void 0 && typeof a.media !== "string") {
|
|
1717
|
+
problems.push(`${name}.anchor.media must be a media[].id (string)`);
|
|
1718
|
+
shaped = false;
|
|
1719
|
+
}
|
|
1720
|
+
if (a.at !== void 0 && a.at !== "start" && a.at !== "end") {
|
|
1721
|
+
problems.push(`${name}.anchor.at must be "start" or "end"`);
|
|
1722
|
+
shaped = false;
|
|
1723
|
+
}
|
|
1724
|
+
if (a.offset !== void 0 && !isNum(a.offset)) {
|
|
1725
|
+
problems.push(`${name}.anchor.offset must be a number (OUTPUT seconds)`);
|
|
1726
|
+
shaped = false;
|
|
1727
|
+
}
|
|
1728
|
+
if (!shaped || !recording || !pieces) return fallback;
|
|
1729
|
+
const r = resolveAnchor(
|
|
1730
|
+
docIn,
|
|
1731
|
+
a,
|
|
1732
|
+
pieces
|
|
1733
|
+
);
|
|
1734
|
+
const step = `step ${JSON.stringify(a.step)}${typeof a.media === "string" ? ` of media ${a.media}` : ""}`;
|
|
1735
|
+
const at2 = fallback === null ? "nowhere" : `${fallback.toFixed(2)}s`;
|
|
1736
|
+
if (r.start === null) {
|
|
1737
|
+
const why = r.miss === "media" ? `media "${String(a.media)}" is not one of media[].id` : r.miss === "step" ? `${step} is not in its meta.steps` : r.miss === "skipped" ? `${step} was skipped by the recorder` : `the cut leaves ${step} out`;
|
|
1738
|
+
warnings.push(
|
|
1739
|
+
`${name}.anchor: ${why}, so it plays at its fallback start (${at2}); re-anchor it, or keep the footage that step plays in`
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
if (elapsed && r.elapsedFrom && r.elapsedMs !== void 0)
|
|
1743
|
+
notes.push(
|
|
1744
|
+
`${name}: {elapsed} reads the off-camera wait of step ${JSON.stringify(r.elapsedFrom.id ?? r.elapsedFrom.step)} (${formatElapsed(r.elapsedMs)}), the nearest one before ${step}, which waited for nothing itself`
|
|
1745
|
+
);
|
|
1746
|
+
if (elapsed && r.elapsedMs === void 0 && r.miss !== "media" && r.miss !== "step")
|
|
1747
|
+
warnings.push(
|
|
1748
|
+
`${name}: {elapsed} stays as written, because neither ${step} nor any step before it recorded an off-camera wait (no meta.skips entry names one, and none has an offCameraMs)`
|
|
1749
|
+
);
|
|
1750
|
+
return r.start ?? fallback;
|
|
1647
1751
|
}
|
|
1648
1752
|
function clicksWithRects(source) {
|
|
1649
1753
|
const cursor = Array.isArray(source.cursor) ? source.cursor : [];
|
|
@@ -1759,15 +1863,33 @@ function checkPin(o, name, docIn, recording, problems, warnings) {
|
|
|
1759
1863
|
}
|
|
1760
1864
|
function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
1761
1865
|
try {
|
|
1762
|
-
const
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1866
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
1867
|
+
const others = Array.isArray(doc.media) ? doc.media : [];
|
|
1868
|
+
for (const [key, src] of [
|
|
1869
|
+
["", doc.source],
|
|
1870
|
+
...others.map((m) => [isObj(m) ? m.id : void 0, m])
|
|
1871
|
+
]) {
|
|
1872
|
+
if (typeof key !== "string" || !isObj(src)) continue;
|
|
1873
|
+
const meta = isObj(src.meta) ? src.meta : {};
|
|
1874
|
+
const w = isNum(meta.width) ? meta.width : 0;
|
|
1875
|
+
const h = isNum(meta.height) ? meta.height : 0;
|
|
1876
|
+
if (!(w > 0) || !(h > 0)) continue;
|
|
1877
|
+
const clicks = clicksWithRects(src);
|
|
1878
|
+
if (!clicks.length) continue;
|
|
1879
|
+
let layout2;
|
|
1880
|
+
try {
|
|
1881
|
+
layout2 = docCardLayout(docIn, key || null);
|
|
1882
|
+
} catch {
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
evidence.set(key, { w, h, clicks, layout: layout2 });
|
|
1886
|
+
}
|
|
1887
|
+
if (!evidence.size) return;
|
|
1769
1888
|
const layout = docCardLayout(docIn);
|
|
1770
1889
|
for (const z of zoom) {
|
|
1890
|
+
const ev = evidence.get(typeof z.media === "string" ? z.media : "");
|
|
1891
|
+
if (!ev) continue;
|
|
1892
|
+
const { w, h, clicks } = ev;
|
|
1771
1893
|
if (!isNum(z.in) || !isNum(z.out) || !isNum(z.level) || !isNum(z.cx) || !isNum(z.cy) || z.level <= 1.001)
|
|
1772
1894
|
continue;
|
|
1773
1895
|
const zin = z.in;
|
|
@@ -1794,7 +1916,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1794
1916
|
focusMode: z.focusMode
|
|
1795
1917
|
},
|
|
1796
1918
|
rect,
|
|
1797
|
-
layout,
|
|
1919
|
+
ev.layout,
|
|
1798
1920
|
0.02,
|
|
1799
1921
|
cameraModel(docIn.frame)
|
|
1800
1922
|
)) {
|
|
@@ -1897,19 +2019,22 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1897
2019
|
} catch {
|
|
1898
2020
|
return;
|
|
1899
2021
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
2022
|
+
const presses = [...evidence.entries()].flatMap(
|
|
2023
|
+
([key, ev]) => ev.clicks.map((c) => ({ c, key, ev }))
|
|
2024
|
+
);
|
|
2025
|
+
for (const { c, key, ev } of presses) {
|
|
2026
|
+
const out = spanOutputExtent(rated, c.t, c.t + 1e-3, key || void 0);
|
|
1902
2027
|
if (!out || out.start < o.start || out.start > o.start + o.duration)
|
|
1903
2028
|
continue;
|
|
1904
2029
|
const target = pinRectOnScreen(
|
|
1905
2030
|
{
|
|
1906
|
-
x: c.rect.x / w,
|
|
1907
|
-
y: c.rect.y / h,
|
|
1908
|
-
w: c.rect.w / w,
|
|
1909
|
-
h: c.rect.h / h
|
|
2031
|
+
x: c.rect.x / ev.w,
|
|
2032
|
+
y: c.rect.y / ev.h,
|
|
2033
|
+
w: c.rect.w / ev.w,
|
|
2034
|
+
h: c.rect.h / ev.h
|
|
1910
2035
|
},
|
|
1911
2036
|
out.start,
|
|
1912
|
-
layout,
|
|
2037
|
+
ev.layout,
|
|
1913
2038
|
camera,
|
|
1914
2039
|
zoomTrack
|
|
1915
2040
|
);
|
|
@@ -2017,7 +2142,7 @@ function mediaProbeLints(results) {
|
|
|
2017
2142
|
}
|
|
2018
2143
|
|
|
2019
2144
|
// src/plugin/framesTake.ts
|
|
2020
|
-
import { mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
2145
|
+
import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
2021
2146
|
import { existsSync as existsSync3 } from "fs";
|
|
2022
2147
|
import { join as join3, resolve as resolve3 } from "path";
|
|
2023
2148
|
import { compileVosConfig } from "@vosjs/core";
|
|
@@ -3279,6 +3404,55 @@ var VIDEO_TOKEN = "__VOS_CLI_VIDEO__";
|
|
|
3279
3404
|
function stillSupersample(w, h) {
|
|
3280
3405
|
return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
|
|
3281
3406
|
}
|
|
3407
|
+
function zoomApexes(doc) {
|
|
3408
|
+
const rated = ratedSegments2(doc);
|
|
3409
|
+
const media = new Set((doc.media ?? []).map((m) => m.id));
|
|
3410
|
+
const apexes = [];
|
|
3411
|
+
const skipped = [];
|
|
3412
|
+
for (const z of doc.zoom) {
|
|
3413
|
+
const of = z.media ? { media: z.media } : {};
|
|
3414
|
+
if (z.media && !media.has(z.media)) {
|
|
3415
|
+
skipped.push({
|
|
3416
|
+
id: z.id,
|
|
3417
|
+
...of,
|
|
3418
|
+
in: z.in,
|
|
3419
|
+
out: z.out,
|
|
3420
|
+
reason: `names media "${z.media}", which the take does not carry`
|
|
3421
|
+
});
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
const ext = spanOutputExtent2(rated, z.in, z.out, z.media);
|
|
3425
|
+
if (!ext) {
|
|
3426
|
+
skipped.push({
|
|
3427
|
+
id: z.id,
|
|
3428
|
+
...of,
|
|
3429
|
+
in: z.in,
|
|
3430
|
+
out: z.out,
|
|
3431
|
+
reason: "the cut keeps none of it, so it never plays"
|
|
3432
|
+
});
|
|
3433
|
+
continue;
|
|
3434
|
+
}
|
|
3435
|
+
apexes.push({
|
|
3436
|
+
id: z.id,
|
|
3437
|
+
...of,
|
|
3438
|
+
time: (ext.start + ext.end) / 2,
|
|
3439
|
+
start: ext.start,
|
|
3440
|
+
end: ext.end
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
return { apexes, skipped };
|
|
3444
|
+
}
|
|
3445
|
+
var ZOOM_UNRENDERED_BELOW = 4e-3;
|
|
3446
|
+
var SIGNATURE_LONG_SIDE = 96;
|
|
3447
|
+
function signatureDifference(a, b) {
|
|
3448
|
+
if (a.length !== b.length || a.length === 0) return 1;
|
|
3449
|
+
let sum = 0;
|
|
3450
|
+
for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
|
|
3451
|
+
return sum / a.length / 255;
|
|
3452
|
+
}
|
|
3453
|
+
function unrenderedZoomLine(f) {
|
|
3454
|
+
return `zoom ${f.zoomId ?? "?"} at ${f.time.toFixed(2)}s did not render (its frame matches the zoom-off frame${f.twin ? `: ${f.twin}` : ""})`;
|
|
3455
|
+
}
|
|
3282
3456
|
async function stillsInPage(opts) {
|
|
3283
3457
|
const w = window;
|
|
3284
3458
|
const RW = opts.W * opts.ss;
|
|
@@ -3287,11 +3461,9 @@ async function stillsInPage(opts) {
|
|
|
3287
3461
|
const vblob = await (await fetch(opts.videoUrl)).blob();
|
|
3288
3462
|
const vurl = URL.createObjectURL(vblob);
|
|
3289
3463
|
const code = opts.animationCode.split(opts.token).join(vurl);
|
|
3290
|
-
const
|
|
3291
|
-
|
|
3292
|
-
URL.createObjectURL(
|
|
3293
|
-
new Blob([code], { type: "text/javascript" })
|
|
3294
|
-
)
|
|
3464
|
+
const load = new Function("u", "return import(u)");
|
|
3465
|
+
const mod = await load(
|
|
3466
|
+
URL.createObjectURL(new Blob([code], { type: "text/javascript" }))
|
|
3295
3467
|
);
|
|
3296
3468
|
w.__vos__ = w.__vos__ || {};
|
|
3297
3469
|
w.__vos__.isPaused = true;
|
|
@@ -3367,6 +3539,25 @@ async function stillsInPage(opts) {
|
|
|
3367
3539
|
}
|
|
3368
3540
|
}
|
|
3369
3541
|
};
|
|
3542
|
+
const sigCanvas = document.createElement("canvas");
|
|
3543
|
+
sigCanvas.width = opts.sigW;
|
|
3544
|
+
sigCanvas.height = opts.sigH;
|
|
3545
|
+
const signature = () => {
|
|
3546
|
+
const g = sigCanvas.getContext("2d", { willReadFrequently: true });
|
|
3547
|
+
g.imageSmoothingEnabled = true;
|
|
3548
|
+
g.imageSmoothingQuality = "high";
|
|
3549
|
+
g.clearRect(0, 0, opts.sigW, opts.sigH);
|
|
3550
|
+
g.drawImage(canvas, 0, 0, opts.sigW, opts.sigH);
|
|
3551
|
+
const px = g.getImageData(0, 0, opts.sigW, opts.sigH).data;
|
|
3552
|
+
const out2 = new Array(opts.sigW * opts.sigH);
|
|
3553
|
+
for (let p = 0, q = 0; p < px.length; p += 4, q++)
|
|
3554
|
+
out2[q] = Math.round(
|
|
3555
|
+
0.299 * px[p] + 0.587 * px[p + 1] + 0.114 * px[p + 2]
|
|
3556
|
+
);
|
|
3557
|
+
return out2;
|
|
3558
|
+
};
|
|
3559
|
+
const sigWanted = new Set(opts.sigNames);
|
|
3560
|
+
const sigs = {};
|
|
3370
3561
|
for (let i = 0; i < opts.shots.length; i++) {
|
|
3371
3562
|
const shot = opts.shots[i];
|
|
3372
3563
|
timeline.seek(shot.time, false);
|
|
@@ -3378,14 +3569,56 @@ async function stillsInPage(opts) {
|
|
|
3378
3569
|
}
|
|
3379
3570
|
await waitVideosSettled(8e3);
|
|
3380
3571
|
const png = await toPng();
|
|
3572
|
+
if (sigWanted.has(shot.name)) sigs[shot.name] = signature();
|
|
3381
3573
|
await fetch("/save?name=" + shot.name, { method: "POST", body: png });
|
|
3382
3574
|
w.__progress = (i + 1) / opts.shots.length;
|
|
3383
3575
|
}
|
|
3384
|
-
w.__done = { ok: true, count: opts.shots.length };
|
|
3576
|
+
w.__done = { ok: true, count: opts.shots.length, sigs };
|
|
3385
3577
|
} catch (e) {
|
|
3386
3578
|
w.__error = String(e instanceof Error && e.stack || e);
|
|
3387
3579
|
}
|
|
3388
3580
|
}
|
|
3581
|
+
async function captureShots(context2, base, opts) {
|
|
3582
|
+
const long = Math.max(opts.width, opts.height);
|
|
3583
|
+
const side = (n) => Math.max(1, Math.round(n / long * SIGNATURE_LONG_SIDE));
|
|
3584
|
+
const page = await context2.newPage();
|
|
3585
|
+
try {
|
|
3586
|
+
page.on("console", (m) => {
|
|
3587
|
+
if (m.type() === "error")
|
|
3588
|
+
process.stderr.write(` [stills page] ${m.text()}
|
|
3589
|
+
`);
|
|
3590
|
+
});
|
|
3591
|
+
await page.addInitScript(() => {
|
|
3592
|
+
;
|
|
3593
|
+
globalThis.__name = (f) => f;
|
|
3594
|
+
});
|
|
3595
|
+
await page.goto(`${base}/render.html`);
|
|
3596
|
+
await page.waitForFunction("window.__pageReady__ === true");
|
|
3597
|
+
void page.evaluate(stillsInPage, {
|
|
3598
|
+
animationCode: opts.animationCode,
|
|
3599
|
+
token: VIDEO_TOKEN,
|
|
3600
|
+
videoUrl: opts.videoUrl,
|
|
3601
|
+
W: opts.width,
|
|
3602
|
+
H: opts.height,
|
|
3603
|
+
ss: opts.ss,
|
|
3604
|
+
shots: opts.shots,
|
|
3605
|
+
sigNames: opts.sigNames,
|
|
3606
|
+
sigW: side(opts.width),
|
|
3607
|
+
sigH: side(opts.height)
|
|
3608
|
+
}).catch(() => {
|
|
3609
|
+
});
|
|
3610
|
+
const done = await waitForPageDone(
|
|
3611
|
+
page,
|
|
3612
|
+
"stills",
|
|
3613
|
+
() => {
|
|
3614
|
+
},
|
|
3615
|
+
12e4 + opts.shots.length * 4e3
|
|
3616
|
+
);
|
|
3617
|
+
return done.sigs ?? {};
|
|
3618
|
+
} finally {
|
|
3619
|
+
await page.close();
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3389
3622
|
async function framesTake(browser, dir, opts) {
|
|
3390
3623
|
const take = await loadTake(dir);
|
|
3391
3624
|
const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
|
|
@@ -3408,13 +3641,17 @@ async function framesTake(browser, dir, opts) {
|
|
|
3408
3641
|
time: clamp(t),
|
|
3409
3642
|
kind: "time"
|
|
3410
3643
|
}));
|
|
3644
|
+
let skippedZooms;
|
|
3411
3645
|
if (opts.atZooms) {
|
|
3412
|
-
const
|
|
3413
|
-
for (const
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3646
|
+
const { apexes, skipped } = zoomApexes(doc);
|
|
3647
|
+
for (const a of apexes)
|
|
3648
|
+
shots.push({
|
|
3649
|
+
time: clamp(a.time),
|
|
3650
|
+
kind: "zoom",
|
|
3651
|
+
zoomId: a.id,
|
|
3652
|
+
...a.media ? { media: a.media } : {}
|
|
3653
|
+
});
|
|
3654
|
+
skippedZooms = skipped;
|
|
3418
3655
|
}
|
|
3419
3656
|
if (opts.atMoments) {
|
|
3420
3657
|
for (const m of await momentsFor(dir, doc)) {
|
|
@@ -3425,16 +3662,19 @@ async function framesTake(browser, dir, opts) {
|
|
|
3425
3662
|
if (opts.atStill)
|
|
3426
3663
|
shots.push({ time: clamp(docStillTime(doc)), kind: "still" });
|
|
3427
3664
|
shots.sort((a, b) => a.time - b.time);
|
|
3428
|
-
if (!shots.length)
|
|
3665
|
+
if (!shots.length && !skippedZooms?.length)
|
|
3429
3666
|
throw new Error(
|
|
3430
3667
|
"no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
|
|
3431
3668
|
);
|
|
3432
3669
|
const outDir = resolve3(opts.outDir ?? join3(dir, "stills"));
|
|
3433
3670
|
await mkdir2(outDir, { recursive: true });
|
|
3671
|
+
const stamp = (i, t) => `${String(i).padStart(2, "0")}-${t.toFixed(2)}s.png`;
|
|
3434
3672
|
const named = shots.map((s, i) => ({
|
|
3435
3673
|
...s,
|
|
3436
|
-
name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${
|
|
3674
|
+
name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${stamp(i, s.time)}`,
|
|
3675
|
+
twinName: s.kind === "zoom" ? `zoomoff-${stamp(i, s.time)}` : void 0
|
|
3437
3676
|
}));
|
|
3677
|
+
const zoomShots = named.filter((n) => n.kind === "zoom");
|
|
3438
3678
|
const server = await startTakeServer(dir, {
|
|
3439
3679
|
"/render.html": renderPageHtml()
|
|
3440
3680
|
});
|
|
@@ -3442,49 +3682,73 @@ async function framesTake(browser, dir, opts) {
|
|
|
3442
3682
|
const context2 = await browser.newContext({
|
|
3443
3683
|
viewport: { width: width * ss, height: height * ss }
|
|
3444
3684
|
});
|
|
3685
|
+
const videoUrl = `/${take.paths.recordingName}`;
|
|
3445
3686
|
try {
|
|
3446
|
-
const
|
|
3447
|
-
page.on("console", (m) => {
|
|
3448
|
-
if (m.type() === "error")
|
|
3449
|
-
process.stderr.write(` [stills page] ${m.text()}
|
|
3450
|
-
`);
|
|
3451
|
-
});
|
|
3452
|
-
await page.addInitScript(() => {
|
|
3453
|
-
;
|
|
3454
|
-
globalThis.__name = (f) => f;
|
|
3455
|
-
});
|
|
3456
|
-
await page.goto(`${server.base}/render.html`);
|
|
3457
|
-
await page.waitForFunction("window.__pageReady__ === true");
|
|
3458
|
-
void page.evaluate(stillsInPage, {
|
|
3687
|
+
const sigs = named.length ? await captureShots(context2, server.base, {
|
|
3459
3688
|
animationCode,
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
H: height,
|
|
3689
|
+
videoUrl,
|
|
3690
|
+
width,
|
|
3691
|
+
height,
|
|
3464
3692
|
ss,
|
|
3465
|
-
shots: named.map(({ time, name }) => ({ time, name }))
|
|
3466
|
-
|
|
3467
|
-
});
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
(
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3693
|
+
shots: named.map(({ time, name }) => ({ time, name })),
|
|
3694
|
+
sigNames: zoomShots.map((n) => n.name)
|
|
3695
|
+
}) : {};
|
|
3696
|
+
let twinSigs = {};
|
|
3697
|
+
if (zoomShots.length) {
|
|
3698
|
+
const twinDoc = structuredClone(doc);
|
|
3699
|
+
applyDocOverrides(twinDoc, { set: ["zoom=[]"] });
|
|
3700
|
+
const twinLowered = lowerToComposition(twinDoc);
|
|
3701
|
+
twinSigs = await captureShots(context2, server.base, {
|
|
3702
|
+
animationCode: compileVosConfig(twinLowered.config, {
|
|
3703
|
+
tweenEngine: "vos"
|
|
3704
|
+
}),
|
|
3705
|
+
videoUrl,
|
|
3706
|
+
width,
|
|
3707
|
+
height,
|
|
3708
|
+
ss,
|
|
3709
|
+
shots: zoomShots.map((n) => ({ time: n.time, name: n.twinName })),
|
|
3710
|
+
sigNames: zoomShots.map((n) => n.twinName)
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3475
3713
|
const frames = [];
|
|
3476
3714
|
for (const n of named) {
|
|
3477
3715
|
const from = join3(dir, n.name);
|
|
3478
3716
|
const to = join3(outDir, n.name);
|
|
3479
3717
|
if (from !== to) await rename2(from, to);
|
|
3480
|
-
|
|
3718
|
+
const frame = {
|
|
3481
3719
|
file: to,
|
|
3482
3720
|
time: n.time,
|
|
3483
3721
|
kind: n.kind,
|
|
3484
|
-
...n.momentId ? { momentId: n.momentId } : {}
|
|
3485
|
-
|
|
3722
|
+
...n.momentId ? { momentId: n.momentId } : {},
|
|
3723
|
+
...n.zoomId ? { zoomId: n.zoomId } : {},
|
|
3724
|
+
...n.media ? { media: n.media } : {}
|
|
3725
|
+
};
|
|
3726
|
+
if (n.twinName) {
|
|
3727
|
+
const a = sigs[n.name];
|
|
3728
|
+
const b = twinSigs[n.twinName];
|
|
3729
|
+
const twinFrom = join3(dir, n.twinName);
|
|
3730
|
+
if (a && b) {
|
|
3731
|
+
const difference = signatureDifference(a, b);
|
|
3732
|
+
frame.rendered = difference >= ZOOM_UNRENDERED_BELOW;
|
|
3733
|
+
frame.difference = Math.round(difference * 1e5) / 1e5;
|
|
3734
|
+
if (!frame.rendered) {
|
|
3735
|
+
const twinTo = join3(outDir, n.twinName);
|
|
3736
|
+
if (twinFrom !== twinTo) await rename2(twinFrom, twinTo);
|
|
3737
|
+
frame.twin = twinTo;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
if (!frame.twin) await rm2(twinFrom, { force: true });
|
|
3741
|
+
}
|
|
3742
|
+
frames.push(frame);
|
|
3486
3743
|
}
|
|
3487
|
-
return {
|
|
3744
|
+
return {
|
|
3745
|
+
frames,
|
|
3746
|
+
width,
|
|
3747
|
+
height,
|
|
3748
|
+
duration,
|
|
3749
|
+
outDir,
|
|
3750
|
+
...skippedZooms ? { skippedZooms } : {}
|
|
3751
|
+
};
|
|
3488
3752
|
} finally {
|
|
3489
3753
|
await context2.close();
|
|
3490
3754
|
server.close();
|
|
@@ -3522,7 +3786,8 @@ async function writeIndexJson(result) {
|
|
|
3522
3786
|
width: result.width,
|
|
3523
3787
|
height: result.height,
|
|
3524
3788
|
duration: result.duration,
|
|
3525
|
-
frames: result.frames
|
|
3789
|
+
frames: result.frames,
|
|
3790
|
+
...result.skippedZooms?.length ? { skippedZooms: result.skippedZooms } : {}
|
|
3526
3791
|
},
|
|
3527
3792
|
null,
|
|
3528
3793
|
2
|
|
@@ -3670,8 +3935,8 @@ import {
|
|
|
3670
3935
|
// src/plugin/moments.ts
|
|
3671
3936
|
import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
|
|
3672
3937
|
var STEP_SETTLE_SECONDS = 0.4;
|
|
3673
|
-
function stepOutputTime(rated, step,
|
|
3674
|
-
const src = step.tEnd +
|
|
3938
|
+
function stepOutputTime(rated, step, settle2) {
|
|
3939
|
+
const src = step.tEnd + settle2;
|
|
3675
3940
|
const ext = spanOutputExtent3(rated, src, src + 1e-3);
|
|
3676
3941
|
if (!ext) return null;
|
|
3677
3942
|
return ext.start;
|
|
@@ -3687,6 +3952,20 @@ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
|
3687
3952
|
var BED_ID = "bed";
|
|
3688
3953
|
var CLICK_ID_PREFIX = "click-";
|
|
3689
3954
|
var CAPTION_ID_PREFIX = "caption-";
|
|
3955
|
+
function houseCaption(id, text, start, duration) {
|
|
3956
|
+
return {
|
|
3957
|
+
id,
|
|
3958
|
+
kind: "text",
|
|
3959
|
+
text,
|
|
3960
|
+
preset: "caption",
|
|
3961
|
+
start,
|
|
3962
|
+
duration,
|
|
3963
|
+
transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
|
|
3964
|
+
anim: { enter: "rise", exit: "fade" },
|
|
3965
|
+
align: "center",
|
|
3966
|
+
box: { color: "rgba(17,17,17,0.72)" }
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3690
3969
|
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
3691
3970
|
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3692
3971
|
var TRANSITION_CLIP_MIN = 1;
|
|
@@ -3767,7 +4046,7 @@ function templateWords(words2) {
|
|
|
3767
4046
|
if (sub && sub !== headline) out["endcard-sub"] = sub;
|
|
3768
4047
|
return out;
|
|
3769
4048
|
}
|
|
3770
|
-
function
|
|
4049
|
+
function resolveAnchor2(doc, at2) {
|
|
3771
4050
|
if (typeof at2 !== "object") return at2;
|
|
3772
4051
|
const steps = doc.source.meta.steps ?? [];
|
|
3773
4052
|
const step = steps.find((s) => s.id === at2.step || String(s.step) === at2.step);
|
|
@@ -3812,7 +4091,7 @@ function proposeMotion(input, opts) {
|
|
|
3812
4091
|
const tWords = templateWords(words2);
|
|
3813
4092
|
const keys = opts.mark ? { "stage-mark": opts.mark.key, "endcard-markimg": opts.mark.key } : void 0;
|
|
3814
4093
|
for (const t of templates) {
|
|
3815
|
-
const at2 =
|
|
4094
|
+
const at2 = resolveAnchor2(doc, t.at);
|
|
3816
4095
|
if (at2 === null) {
|
|
3817
4096
|
skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
|
|
3818
4097
|
continue;
|
|
@@ -3893,18 +4172,14 @@ function proposeMotion(input, opts) {
|
|
|
3893
4172
|
if (!step || step.skipped) continue;
|
|
3894
4173
|
const t = stepOutputTime(rated, step, 0.2);
|
|
3895
4174
|
if (t === null || t < 0 || t > footage - 1) continue;
|
|
3896
|
-
captionClips.push(
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
anim: { enter: "rise", exit: "fade" },
|
|
3905
|
-
align: "center",
|
|
3906
|
-
box: { color: "rgba(17,17,17,0.72)" }
|
|
3907
|
-
});
|
|
4175
|
+
captionClips.push(
|
|
4176
|
+
houseCaption(
|
|
4177
|
+
`${CAPTION_ID_PREFIX}${c.step}`,
|
|
4178
|
+
c.caption,
|
|
4179
|
+
+t.toFixed(3),
|
|
4180
|
+
Math.min(3.5, Math.max(2.5, footage - t - 0.2))
|
|
4181
|
+
)
|
|
4182
|
+
);
|
|
3908
4183
|
}
|
|
3909
4184
|
if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
|
|
3910
4185
|
}
|
|
@@ -5332,8 +5607,24 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5332
5607
|
|
|
5333
5608
|
// src/plugin/recorder.ts
|
|
5334
5609
|
import { writeFileSync } from "fs";
|
|
5610
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
5335
5611
|
import { join as join8 } from "path";
|
|
5336
|
-
import {
|
|
5612
|
+
import {
|
|
5613
|
+
fileNameProblem,
|
|
5614
|
+
recordTake as recordTakeCore
|
|
5615
|
+
} from "@vosjs/render-core/record";
|
|
5616
|
+
function outputsSink(dir) {
|
|
5617
|
+
const outDir = join8(dir, "outputs");
|
|
5618
|
+
return {
|
|
5619
|
+
save: async (name, bytes) => {
|
|
5620
|
+
const problem = fileNameProblem(name);
|
|
5621
|
+
if (problem) throw new Error(`outputs/${name}: ${problem}`);
|
|
5622
|
+
await refuseSymlink(outDir);
|
|
5623
|
+
await mkdir5(outDir, { recursive: true });
|
|
5624
|
+
await writeFileSafe(join8(outDir, name), bytes);
|
|
5625
|
+
}
|
|
5626
|
+
};
|
|
5627
|
+
}
|
|
5337
5628
|
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
5338
5629
|
const dry = opts.dryRun === true;
|
|
5339
5630
|
const result = await recordTakeCore(
|
|
@@ -5348,6 +5639,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5348
5639
|
log,
|
|
5349
5640
|
{
|
|
5350
5641
|
...opts,
|
|
5642
|
+
outputs: outputsSink(paths.dir),
|
|
5351
5643
|
env: process.env,
|
|
5352
5644
|
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
|
|
5353
5645
|
}
|
|
@@ -5361,6 +5653,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5361
5653
|
return result;
|
|
5362
5654
|
}
|
|
5363
5655
|
|
|
5656
|
+
// src/plugin/run.ts
|
|
5657
|
+
import {
|
|
5658
|
+
RecordRefusedError,
|
|
5659
|
+
guardVerdict,
|
|
5660
|
+
guardedSteps,
|
|
5661
|
+
hostedRefusals
|
|
5662
|
+
} from "@vosjs/render-core/record";
|
|
5663
|
+
|
|
5364
5664
|
// src/plugin/wall.ts
|
|
5365
5665
|
import {
|
|
5366
5666
|
WALL_PROBE,
|
|
@@ -5455,10 +5755,10 @@ async function openSession(name, url, opts = { log: () => {
|
|
|
5455
5755
|
opts.log(
|
|
5456
5756
|
`a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
|
|
5457
5757
|
);
|
|
5458
|
-
return new Promise((
|
|
5758
|
+
return new Promise((resolve19, reject) => {
|
|
5459
5759
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
5460
5760
|
child.on("error", reject);
|
|
5461
|
-
child.on("exit", (code) =>
|
|
5761
|
+
child.on("exit", (code) => resolve19({ code }));
|
|
5462
5762
|
});
|
|
5463
5763
|
}
|
|
5464
5764
|
async function withSession(name, fn, opts = {}) {
|
|
@@ -5607,9 +5907,14 @@ import {
|
|
|
5607
5907
|
withBackdrop,
|
|
5608
5908
|
migrateMotion as migrateMotion2
|
|
5609
5909
|
} from "@vosjs/studio-core";
|
|
5910
|
+
import { splitSegmentsAtSkips } from "@vosjs/render-core/record";
|
|
5610
5911
|
|
|
5611
5912
|
// src/plugin/reuse.ts
|
|
5612
|
-
import {
|
|
5913
|
+
import {
|
|
5914
|
+
docFreezes,
|
|
5915
|
+
ratedSegments as ratedSegments6,
|
|
5916
|
+
resolveAnchor as resolveLayerAnchor
|
|
5917
|
+
} from "@vosjs/studio-core";
|
|
5613
5918
|
var MIN_SPAN = 0.15;
|
|
5614
5919
|
var AFTER_STEP = 1;
|
|
5615
5920
|
var LAYER_AFTER_STEP = 5;
|
|
@@ -5685,7 +5990,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
|
|
|
5685
5990
|
};
|
|
5686
5991
|
return { map, unmatched, matched };
|
|
5687
5992
|
}
|
|
5688
|
-
function
|
|
5993
|
+
function resolveAnchor3(anchor, newSteps) {
|
|
5689
5994
|
const step = stepByRef(newSteps, anchor.step);
|
|
5690
5995
|
if (!step || step.skipped) return null;
|
|
5691
5996
|
const base = anchor.at === "end" ? step.tEnd : step.tStart;
|
|
@@ -5705,7 +6010,7 @@ function framed(span) {
|
|
|
5705
6010
|
}
|
|
5706
6011
|
function placeMoment(t, anchor, ctx) {
|
|
5707
6012
|
if (anchor) {
|
|
5708
|
-
const at2 =
|
|
6013
|
+
const at2 = resolveAnchor3(anchor, ctx.newSteps);
|
|
5709
6014
|
if (at2 === null) return { gone: String(anchor.step) };
|
|
5710
6015
|
ctx.report.anchored++;
|
|
5711
6016
|
return { at: at2, via: "anchor" };
|
|
@@ -5873,6 +6178,17 @@ function retimeOverlays(prev, next, report) {
|
|
|
5873
6178
|
return { ...o, start: +start.toFixed(3) };
|
|
5874
6179
|
};
|
|
5875
6180
|
return overlays.map((o) => {
|
|
6181
|
+
if (o.anchor && typeof o.anchor === "object") {
|
|
6182
|
+
const r = resolveLayerAnchor(next, o.anchor, newPieces);
|
|
6183
|
+
if (r.start === null) {
|
|
6184
|
+
report.flagged.push(
|
|
6185
|
+
`overlay ${o.id}: anchored to step ${String(o.anchor.step)}, which is missing, skipped or cut in the new recording \u2014 it plays at its fallback ${o.start.toFixed(2)}s; re-anchor or re-place it`
|
|
6186
|
+
);
|
|
6187
|
+
return o;
|
|
6188
|
+
}
|
|
6189
|
+
report.anchored++;
|
|
6190
|
+
return { ...o, start: r.start };
|
|
6191
|
+
}
|
|
5876
6192
|
const pin = o.pin;
|
|
5877
6193
|
if (pin?.step !== void 0) {
|
|
5878
6194
|
const was = stepByRef(oldSteps, pin.step);
|
|
@@ -6003,7 +6319,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6003
6319
|
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
6004
6320
|
doc = copyStyle(prev, doc);
|
|
6005
6321
|
const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
|
|
6006
|
-
doc.segments = rt.segments;
|
|
6322
|
+
doc.segments = splitSegmentsAtSkips(rt.segments, meta.skips);
|
|
6007
6323
|
if (rt.freeze.length) doc.freeze = rt.freeze;
|
|
6008
6324
|
if (rt.rejected.length) doc.rejected = rt.rejected;
|
|
6009
6325
|
const manualZoom = rt.zoom;
|
|
@@ -6101,6 +6417,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6101
6417
|
meta
|
|
6102
6418
|
};
|
|
6103
6419
|
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
6420
|
+
doc.segments = splitSegmentsAtSkips(doc.segments, meta.skips);
|
|
6104
6421
|
if (opts.style) {
|
|
6105
6422
|
const applied = applyStyle(opts.style.doc, doc, opts);
|
|
6106
6423
|
doc = applied.doc;
|
|
@@ -6212,7 +6529,7 @@ async function writeScriptBeside(dir, doc) {
|
|
|
6212
6529
|
}
|
|
6213
6530
|
|
|
6214
6531
|
// src/plugin/folder.ts
|
|
6215
|
-
import { mkdir as
|
|
6532
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
6216
6533
|
import { join as join12 } from "path";
|
|
6217
6534
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6218
6535
|
import { migrateHostedDoc } from "@vosjs/studio-core";
|
|
@@ -6421,7 +6738,7 @@ async function cmdPull(argv) {
|
|
|
6421
6738
|
const payload = res.body;
|
|
6422
6739
|
const safeName = (v, id) => safePathName((v ?? "").toLowerCase(), id);
|
|
6423
6740
|
const out = strFlag(flags, "out") ?? safeName(folder.slug, folder.id);
|
|
6424
|
-
await
|
|
6741
|
+
await mkdir6(join12(out, "recipes"), { recursive: true });
|
|
6425
6742
|
const lines = [];
|
|
6426
6743
|
const recipes = [];
|
|
6427
6744
|
for (const rec of payload.recipes) {
|
|
@@ -6431,7 +6748,7 @@ async function cmdPull(argv) {
|
|
|
6431
6748
|
}
|
|
6432
6749
|
for (const rec of payload.inheritedRecipes) {
|
|
6433
6750
|
const from = safeName(rec.folderSlug ?? "inherited", "inherited");
|
|
6434
|
-
await
|
|
6751
|
+
await mkdir6(join12(out, "recipes", "_inherited", from), { recursive: true });
|
|
6435
6752
|
const path = join12(
|
|
6436
6753
|
out,
|
|
6437
6754
|
"recipes",
|
|
@@ -6448,7 +6765,7 @@ async function cmdPull(argv) {
|
|
|
6448
6765
|
const members = [];
|
|
6449
6766
|
for (const v of payload.voses) {
|
|
6450
6767
|
const dir = join12(out, "members", safeName(v.slug || v.title, v.id));
|
|
6451
|
-
await
|
|
6768
|
+
await mkdir6(dir, { recursive: true });
|
|
6452
6769
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
6453
6770
|
if (cfg.status !== 200) {
|
|
6454
6771
|
lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
|
|
@@ -6998,7 +7315,7 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
6998
7315
|
|
|
6999
7316
|
// src/plugin/program.ts
|
|
7000
7317
|
import { existsSync as existsSync13 } from "fs";
|
|
7001
|
-
import { mkdir as
|
|
7318
|
+
import { mkdir as mkdir7, readFile as readFile11 } from "fs/promises";
|
|
7002
7319
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7003
7320
|
import { basename as basename3, dirname as dirname2, join as join14 } from "path";
|
|
7004
7321
|
import {
|
|
@@ -7217,7 +7534,7 @@ async function cmdFetch(argv) {
|
|
|
7217
7534
|
throw new Error(apiError(`fetch config for ${vosId}`, cfg));
|
|
7218
7535
|
const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
|
|
7219
7536
|
const out = strFlag(flags, "out") ?? safePathName(slug, vosId);
|
|
7220
|
-
await
|
|
7537
|
+
await mkdir7(out, { recursive: true });
|
|
7221
7538
|
await writeFileSafe(
|
|
7222
7539
|
join14(out, "config.json"),
|
|
7223
7540
|
JSON.stringify(cfg.body.config, null, 2)
|
|
@@ -10871,13 +11188,13 @@ function parseLlmsTxt(text) {
|
|
|
10871
11188
|
const h1 = /^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? null;
|
|
10872
11189
|
const block = /^>\s*.+(?:\n>\s*.*)*/m.exec(text)?.[0];
|
|
10873
11190
|
const joined = block ? block.split("\n").map((line) => line.replace(/^>\s*/, "").trim()).filter(Boolean).join(" ") : "";
|
|
10874
|
-
const
|
|
11191
|
+
const quote2 = joined ? /^(.*?[.!?])(?:\s|$)/.exec(joined)?.[1] ?? joined : null;
|
|
10875
11192
|
let name = h1;
|
|
10876
11193
|
if (h1) {
|
|
10877
11194
|
const split = /^(.+?)\s+[—–-]\s+(.+)$/.exec(h1);
|
|
10878
11195
|
if (split) name = split[1].trim();
|
|
10879
11196
|
}
|
|
10880
|
-
return { name, claim:
|
|
11197
|
+
return { name, claim: quote2 ?? (h1 && h1 !== name ? h1 : null) };
|
|
10881
11198
|
}
|
|
10882
11199
|
var WITNESS_IN_PAGE = `(() => {
|
|
10883
11200
|
const cs = (el) => el ? getComputedStyle(el) : null
|
|
@@ -11238,7 +11555,7 @@ async function recordHosted(input) {
|
|
|
11238
11555
|
}
|
|
11239
11556
|
|
|
11240
11557
|
// src/plugin/run.ts
|
|
11241
|
-
import { urlSchemeProblem } from "@vosjs/render-core/record";
|
|
11558
|
+
import { urlSchemeProblem as urlSchemeProblem2 } from "@vosjs/render-core/record";
|
|
11242
11559
|
|
|
11243
11560
|
// src/plugin/agentBrowser.ts
|
|
11244
11561
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
@@ -11288,18 +11605,18 @@ function parseAgentBrowserLog(text) {
|
|
|
11288
11605
|
function splitCommand(command) {
|
|
11289
11606
|
const out = [];
|
|
11290
11607
|
let cur = "";
|
|
11291
|
-
let
|
|
11608
|
+
let quote2 = null;
|
|
11292
11609
|
let has = false;
|
|
11293
11610
|
for (let i = 0; i < command.length; i++) {
|
|
11294
11611
|
const c = command[i];
|
|
11295
|
-
if (
|
|
11296
|
-
if (c ===
|
|
11612
|
+
if (quote2) {
|
|
11613
|
+
if (c === quote2) quote2 = null;
|
|
11297
11614
|
else if (c === "\\" && i + 1 < command.length) cur += command[++i];
|
|
11298
11615
|
else cur += c;
|
|
11299
11616
|
continue;
|
|
11300
11617
|
}
|
|
11301
11618
|
if (c === '"' || c === "'") {
|
|
11302
|
-
|
|
11619
|
+
quote2 = c;
|
|
11303
11620
|
has = true;
|
|
11304
11621
|
continue;
|
|
11305
11622
|
}
|
|
@@ -11343,8 +11660,8 @@ var quoteAttr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
|
11343
11660
|
function hrefSelector(href) {
|
|
11344
11661
|
try {
|
|
11345
11662
|
const u = new URL(href);
|
|
11346
|
-
const
|
|
11347
|
-
return `a[href=${quoteAttr(
|
|
11663
|
+
const relative6 = `${u.pathname}${u.search}${u.hash}`;
|
|
11664
|
+
return `a[href=${quoteAttr(relative6)}], a[href=${quoteAttr(href)}]`;
|
|
11348
11665
|
} catch {
|
|
11349
11666
|
return `a[href=${quoteAttr(href)}]`;
|
|
11350
11667
|
}
|
|
@@ -11721,8 +12038,8 @@ async function cmdActions(argv) {
|
|
|
11721
12038
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
11722
12039
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
11723
12040
|
}
|
|
11724
|
-
const { readFile:
|
|
11725
|
-
const text = await
|
|
12041
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
12042
|
+
const text = await readFile16(resolve12(input), "utf8");
|
|
11726
12043
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
11727
12044
|
for (const p of problems) r.log(` ${p}`);
|
|
11728
12045
|
const result = convertAgentBrowser(records, {
|
|
@@ -11759,9 +12076,9 @@ function summary2(result) {
|
|
|
11759
12076
|
}
|
|
11760
12077
|
async function cmdActionsScript(input, flags) {
|
|
11761
12078
|
const r = createReporter(flags.json === true);
|
|
11762
|
-
const { readFile:
|
|
11763
|
-
const { shotList, formatShotList } = await import("./shotList-
|
|
11764
|
-
const raw = JSON.parse(await
|
|
12079
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
12080
|
+
const { shotList, formatShotList } = await import("./shotList-SCA234OS.js");
|
|
12081
|
+
const raw = JSON.parse(await readFile16(resolve12(input), "utf8"));
|
|
11765
12082
|
const errors = validateActions(raw);
|
|
11766
12083
|
if (errors.length) throw new UsageError(`${input}:
|
|
11767
12084
|
${errors.join("\n ")}`);
|
|
@@ -11772,7 +12089,7 @@ async function cmdActionsScript(input, flags) {
|
|
|
11772
12089
|
|
|
11773
12090
|
// src/plugin/ingest.ts
|
|
11774
12091
|
import { existsSync as existsSync20 } from "fs";
|
|
11775
|
-
import { copyFile, mkdir as
|
|
12092
|
+
import { copyFile, mkdir as mkdir8, readFile as readFile12, stat as stat2 } from "fs/promises";
|
|
11776
12093
|
import { basename as basename9, join as join22, resolve as resolve13 } from "path";
|
|
11777
12094
|
|
|
11778
12095
|
// src/plugin/traceInflate.ts
|
|
@@ -11874,7 +12191,11 @@ async function copyRecording(src, dest, ext) {
|
|
|
11874
12191
|
target
|
|
11875
12192
|
});
|
|
11876
12193
|
try {
|
|
11877
|
-
const conversion = await Conversion.init({
|
|
12194
|
+
const conversion = await Conversion.init({
|
|
12195
|
+
input,
|
|
12196
|
+
output,
|
|
12197
|
+
showWarnings: false
|
|
12198
|
+
});
|
|
11878
12199
|
if (!conversion.isValid || conversion.discardedTracks.length > 0) {
|
|
11879
12200
|
await conversion.cancel?.();
|
|
11880
12201
|
await copyFile(src, dest);
|
|
@@ -11960,7 +12281,7 @@ async function cmdIngest(argv) {
|
|
|
11960
12281
|
`note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
|
|
11961
12282
|
);
|
|
11962
12283
|
}
|
|
11963
|
-
await
|
|
12284
|
+
await mkdir8(outDir, { recursive: true });
|
|
11964
12285
|
const paths = await ensureTakeDir(outDir);
|
|
11965
12286
|
const recording = join22(outDir, `recording.${probed.ext}`);
|
|
11966
12287
|
r.log("copying the recording\u2026");
|
|
@@ -12026,115 +12347,2465 @@ async function cmdIngest(argv) {
|
|
|
12026
12347
|
return EXIT_OK;
|
|
12027
12348
|
}
|
|
12028
12349
|
|
|
12029
|
-
// src/plugin/
|
|
12030
|
-
import {
|
|
12031
|
-
import {
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
readFileSync as readFileSync11,
|
|
12036
|
-
rmSync as rmSync3,
|
|
12037
|
-
writeFileSync as writeFileSync6
|
|
12038
|
-
} from "fs";
|
|
12039
|
-
import { homedir as homedir2 } from "os";
|
|
12040
|
-
import { join as join24 } from "path";
|
|
12041
|
-
import { chromium as chromium2 } from "playwright";
|
|
12350
|
+
// src/plugin/checkVideo.ts
|
|
12351
|
+
import { createRequire as createRequire2 } from "module";
|
|
12352
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12353
|
+
import { mkdir as mkdir9, readFile as readFile13, stat as stat3, writeFile as writeFile10 } from "fs/promises";
|
|
12354
|
+
import { basename as basename10, dirname as dirname8, extname as extname4, join as join23, resolve as resolve14 } from "path";
|
|
12355
|
+
import { ALL_FORMATS as ALL_FORMATS2, FilePathSource as FilePathSource2, Input as Input2 } from "mediabunny";
|
|
12042
12356
|
|
|
12043
|
-
// src/plugin/
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
var
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
var
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
};
|
|
12058
|
-
var HOME_DIRS = {
|
|
12059
|
-
claude: [".claude"],
|
|
12060
|
-
cursor: [".cursor"],
|
|
12061
|
-
codex: [".codex", ".agents"],
|
|
12062
|
-
copilot: [".copilot"]
|
|
12063
|
-
};
|
|
12064
|
-
function detectAgents(root, opts = {}) {
|
|
12065
|
-
const exists = opts.exists ?? existsSync21;
|
|
12357
|
+
// src/plugin/videoChecks.ts
|
|
12358
|
+
var SCENE_CHANGE_AT = 0.12;
|
|
12359
|
+
var CELL_CHANGED_AT = 0.03;
|
|
12360
|
+
var SCENE_WINDOW = 5;
|
|
12361
|
+
var UNIFORM_BELOW = 2;
|
|
12362
|
+
var QUIET_BELOW_LUFS = -23;
|
|
12363
|
+
var LOUD_ABOVE_LUFS = -9;
|
|
12364
|
+
var SUB_BLOCK_SECONDS = 0.1;
|
|
12365
|
+
var SHORT_TERM_SECONDS = 3;
|
|
12366
|
+
var SCENE_SPIKE = 2.5;
|
|
12367
|
+
var ISOLATED_CHANGE_AT = 0.03;
|
|
12368
|
+
var ISOLATED_SPIKE = 8;
|
|
12369
|
+
var ISOLATED_COVERAGE = 0.25;
|
|
12370
|
+
function sceneChanges2(times, changes, coverage) {
|
|
12066
12371
|
const out = [];
|
|
12067
|
-
for (
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12372
|
+
for (let i = 1; i < changes.length; i++) {
|
|
12373
|
+
const c = changes[i];
|
|
12374
|
+
if (c < Math.min(SCENE_CHANGE_AT, ISOLATED_CHANGE_AT)) continue;
|
|
12375
|
+
if (i > 1 && changes[i - 1] > c) continue;
|
|
12376
|
+
if (i + 1 < changes.length && changes[i + 1] >= c) continue;
|
|
12377
|
+
const around = [i - 2, i - 1, i + 1, i + 2].filter((k) => k >= 1 && k < changes.length).map((k) => changes[k]).sort((a, b) => a - b);
|
|
12378
|
+
const mid = around.length ? around.length % 2 ? around[(around.length - 1) / 2] : (around[around.length / 2 - 1] + around[around.length / 2]) / 2 : 0;
|
|
12379
|
+
const most = around.length ? around[around.length - 1] : 0;
|
|
12380
|
+
const share = coverage ? coverage[i] : 1;
|
|
12381
|
+
const standsOut = c >= SCENE_CHANGE_AT && c >= SCENE_SPIKE * mid;
|
|
12382
|
+
const standsAlone = c >= ISOLATED_CHANGE_AT && share >= ISOLATED_COVERAGE && c >= ISOLATED_SPIKE * most;
|
|
12383
|
+
if (!standsOut && !standsAlone) continue;
|
|
12384
|
+
out.push({
|
|
12385
|
+
time: times[i],
|
|
12386
|
+
change: Math.round(c * 1e4) / 1e4,
|
|
12387
|
+
...coverage ? { coverage: Math.round(share * 1e3) / 1e3 } : {}
|
|
12388
|
+
});
|
|
12079
12389
|
}
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12390
|
+
return out;
|
|
12391
|
+
}
|
|
12392
|
+
function windowCounts(cuts, duration, window2 = SCENE_WINDOW) {
|
|
12393
|
+
const n = Math.max(1, Math.ceil(duration / window2 - 1e-9));
|
|
12394
|
+
const out = Array.from({ length: n }, (_, k) => ({
|
|
12395
|
+
start: k * window2,
|
|
12396
|
+
count: 0
|
|
12397
|
+
}));
|
|
12398
|
+
for (const c of cuts) {
|
|
12399
|
+
const k = Math.min(n - 1, Math.max(0, Math.floor(c.time / window2)));
|
|
12400
|
+
out[k].count++;
|
|
12087
12401
|
}
|
|
12088
12402
|
return out;
|
|
12089
12403
|
}
|
|
12090
|
-
function
|
|
12091
|
-
const
|
|
12092
|
-
|
|
12093
|
-
|
|
12404
|
+
function crowdedRuns(cuts, window2 = SCENE_WINDOW) {
|
|
12405
|
+
const runs = [];
|
|
12406
|
+
let run2 = [];
|
|
12407
|
+
for (const c of cuts) {
|
|
12408
|
+
if (run2.length && c.time - run2[run2.length - 1].time < window2) run2.push(c);
|
|
12409
|
+
else {
|
|
12410
|
+
if (run2.length > 1) runs.push(run2);
|
|
12411
|
+
run2 = [c];
|
|
12412
|
+
}
|
|
12413
|
+
}
|
|
12414
|
+
if (run2.length > 1) runs.push(run2);
|
|
12415
|
+
return runs;
|
|
12416
|
+
}
|
|
12417
|
+
function createLoudnessMeter(sampleRate, channelCount, subBlockSeconds) {
|
|
12418
|
+
const shelf = (() => {
|
|
12419
|
+
const f0 = 1681.974450955533;
|
|
12420
|
+
const G = 3.999843853973347;
|
|
12421
|
+
const Q = 0.7071752369554196;
|
|
12422
|
+
const K = Math.tan(Math.PI * f0 / sampleRate);
|
|
12423
|
+
const Vh = Math.pow(10, G / 20);
|
|
12424
|
+
const Vb = Math.pow(Vh, 0.4996667741545416);
|
|
12425
|
+
const a0 = 1 + K / Q + K * K;
|
|
12094
12426
|
return {
|
|
12095
|
-
|
|
12427
|
+
b0: (Vh + Vb * K / Q + K * K) / a0,
|
|
12428
|
+
b1: 2 * (K * K - Vh) / a0,
|
|
12429
|
+
b2: (Vh - Vb * K / Q + K * K) / a0,
|
|
12430
|
+
a1: 2 * (K * K - 1) / a0,
|
|
12431
|
+
a2: (1 - K / Q + K * K) / a0
|
|
12096
12432
|
};
|
|
12097
|
-
|
|
12433
|
+
})();
|
|
12434
|
+
const highpass = (() => {
|
|
12435
|
+
const f0 = 38.13547087602444;
|
|
12436
|
+
const Q = 0.5003270373238773;
|
|
12437
|
+
const K = Math.tan(Math.PI * f0 / sampleRate);
|
|
12438
|
+
const a0 = 1 + K / Q + K * K;
|
|
12439
|
+
return {
|
|
12440
|
+
b0: 1,
|
|
12441
|
+
b1: -2,
|
|
12442
|
+
b2: 1,
|
|
12443
|
+
a1: 2 * (K * K - 1) / a0,
|
|
12444
|
+
a2: (1 - K / Q + K * K) / a0
|
|
12445
|
+
};
|
|
12446
|
+
})();
|
|
12447
|
+
const per = Math.max(1, Math.round(sampleRate * subBlockSeconds));
|
|
12448
|
+
const state = Array.from({ length: channelCount }, () => ({
|
|
12449
|
+
// Direct form I history for each stage.
|
|
12450
|
+
x1: 0,
|
|
12451
|
+
x2: 0,
|
|
12452
|
+
y1: 0,
|
|
12453
|
+
y2: 0,
|
|
12454
|
+
u1: 0,
|
|
12455
|
+
u2: 0,
|
|
12456
|
+
w1: 0,
|
|
12457
|
+
w2: 0,
|
|
12458
|
+
sum: 0,
|
|
12459
|
+
count: 0,
|
|
12460
|
+
energies: []
|
|
12461
|
+
}));
|
|
12462
|
+
let peak = 0;
|
|
12463
|
+
let samples = 0;
|
|
12098
12464
|
return {
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12465
|
+
push(channels) {
|
|
12466
|
+
for (let c = 0; c < channelCount; c++) {
|
|
12467
|
+
const data = channels[c];
|
|
12468
|
+
const s = state[c];
|
|
12469
|
+
if (!data) continue;
|
|
12470
|
+
for (let i = 0; i < data.length; i++) {
|
|
12471
|
+
const x = data[i];
|
|
12472
|
+
const ax = x < 0 ? -x : x;
|
|
12473
|
+
if (ax > peak) peak = ax;
|
|
12474
|
+
const y = shelf.b0 * x + shelf.b1 * s.x1 + shelf.b2 * s.x2 - shelf.a1 * s.y1 - shelf.a2 * s.y2;
|
|
12475
|
+
s.x2 = s.x1;
|
|
12476
|
+
s.x1 = x;
|
|
12477
|
+
s.y2 = s.y1;
|
|
12478
|
+
s.y1 = y;
|
|
12479
|
+
const w = highpass.b0 * y + highpass.b1 * s.u1 + highpass.b2 * s.u2 - highpass.a1 * s.w1 - highpass.a2 * s.w2;
|
|
12480
|
+
s.u2 = s.u1;
|
|
12481
|
+
s.u1 = y;
|
|
12482
|
+
s.w2 = s.w1;
|
|
12483
|
+
s.w1 = w;
|
|
12484
|
+
s.sum += w * w;
|
|
12485
|
+
s.count++;
|
|
12486
|
+
if (s.count === per) {
|
|
12487
|
+
s.energies.push(s.sum / per);
|
|
12488
|
+
s.sum = 0;
|
|
12489
|
+
s.count = 0;
|
|
12490
|
+
}
|
|
12491
|
+
}
|
|
12492
|
+
if (c === 0) samples += data.length;
|
|
12493
|
+
}
|
|
12494
|
+
},
|
|
12495
|
+
finish() {
|
|
12496
|
+
return {
|
|
12497
|
+
energies: state.map(
|
|
12498
|
+
(s) => s.count > 0 ? [...s.energies, s.sum / s.count] : s.energies
|
|
12499
|
+
),
|
|
12500
|
+
peak,
|
|
12501
|
+
samples
|
|
12502
|
+
};
|
|
12503
|
+
}
|
|
12104
12504
|
};
|
|
12105
12505
|
}
|
|
12106
|
-
function
|
|
12107
|
-
return
|
|
12108
|
-
}
|
|
12109
|
-
function fallbackSkillsHome(root) {
|
|
12110
|
-
return { agent: "codex", dir: join23(root, ".agents"), scope: "project" };
|
|
12506
|
+
function channelWeights(count) {
|
|
12507
|
+
if (count === 6) return [1, 1, 1, 0, 1.41, 1.41];
|
|
12508
|
+
return Array.from({ length: count }, (_, i) => i < 3 ? 1 : 1.41);
|
|
12111
12509
|
}
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12510
|
+
var LOUDNESS_OFFSET = -0.691;
|
|
12511
|
+
var toLufs = (power) => power > 0 ? LOUDNESS_OFFSET + 10 * Math.log10(power) : null;
|
|
12512
|
+
function windowPower(energies, weights, from, count) {
|
|
12513
|
+
let total = 0;
|
|
12514
|
+
for (let c = 0; c < energies.length; c++) {
|
|
12515
|
+
const e = energies[c];
|
|
12516
|
+
let sum = 0;
|
|
12517
|
+
let n = 0;
|
|
12518
|
+
for (let k = from; k < from + count && k < e.length; k++) {
|
|
12519
|
+
sum += e[k];
|
|
12520
|
+
n++;
|
|
12521
|
+
}
|
|
12522
|
+
if (n) total += (weights[c] ?? 1) * (sum / n);
|
|
12523
|
+
}
|
|
12524
|
+
return total;
|
|
12525
|
+
}
|
|
12526
|
+
var round1 = (n) => n === null ? null : Math.round(n * 10) / 10;
|
|
12527
|
+
function loudnessFromEnergies(energies, peak, subBlock = SUB_BLOCK_SECONDS) {
|
|
12528
|
+
const weights = channelWeights(energies.length);
|
|
12529
|
+
const pieces = Math.max(0, ...energies.map((e) => e.length));
|
|
12530
|
+
const blockLen = Math.max(1, Math.round(0.4 / subBlock));
|
|
12531
|
+
const blocks = [];
|
|
12532
|
+
for (let k = 0; k + blockLen <= pieces; k++)
|
|
12533
|
+
blocks.push(windowPower(energies, weights, k, blockLen));
|
|
12534
|
+
if (!blocks.length && pieces > 0)
|
|
12535
|
+
blocks.push(windowPower(energies, weights, 0, pieces));
|
|
12536
|
+
const mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
12537
|
+
const loud = (p) => toLufs(p) ?? -Infinity;
|
|
12538
|
+
const absolute2 = blocks.filter((p) => loud(p) > -70);
|
|
12539
|
+
let integrated = null;
|
|
12540
|
+
if (absolute2.length) {
|
|
12541
|
+
const gate = loud(mean(absolute2)) - 10;
|
|
12542
|
+
const relative6 = absolute2.filter((p) => loud(p) > gate);
|
|
12543
|
+
integrated = toLufs(mean(relative6));
|
|
12544
|
+
}
|
|
12545
|
+
const stLen = Math.max(1, Math.round(SHORT_TERM_SECONDS / subBlock));
|
|
12546
|
+
const shortTerm = [];
|
|
12547
|
+
for (let k = 0; k < pieces; k += stLen) {
|
|
12548
|
+
if (pieces - k < blockLen && k > 0) break;
|
|
12549
|
+
shortTerm.push({
|
|
12550
|
+
time: Math.round(k * subBlock * 100) / 100,
|
|
12551
|
+
loudness: round1(toLufs(windowPower(energies, weights, k, stLen)))
|
|
12552
|
+
});
|
|
12117
12553
|
}
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12554
|
+
const st = shortTerm.map((s) => s.loudness).filter((l) => l !== null);
|
|
12555
|
+
return {
|
|
12556
|
+
unit: "LUFS",
|
|
12557
|
+
integrated: round1(integrated),
|
|
12558
|
+
shortTerm,
|
|
12559
|
+
maxShortTerm: st.length ? Math.max(...st) : null,
|
|
12560
|
+
samplePeakDb: peak > 0 ? round1(20 * Math.log10(peak)) : null
|
|
12561
|
+
};
|
|
12121
12562
|
}
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
${
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12563
|
+
function loudnessWarnings(l) {
|
|
12564
|
+
if (l.integrated === null)
|
|
12565
|
+
return [
|
|
12566
|
+
"the sound is silent: no 400 ms block reaches -70 LUFS, the floor loudness is measured from"
|
|
12567
|
+
];
|
|
12568
|
+
if (l.integrated < QUIET_BELOW_LUFS)
|
|
12569
|
+
return [
|
|
12570
|
+
`integrated loudness ${l.integrated} LUFS is below ${QUIET_BELOW_LUFS}: too quiet for a social feed, where a viewer turns sound up once and keeps it`
|
|
12571
|
+
];
|
|
12572
|
+
if (l.integrated > LOUD_ABOVE_LUFS)
|
|
12573
|
+
return [
|
|
12574
|
+
`integrated loudness ${l.integrated} LUFS is above ${LOUD_ABOVE_LUFS}: too loud, a platform turns it down and a peak may clip`
|
|
12575
|
+
];
|
|
12576
|
+
return [];
|
|
12577
|
+
}
|
|
12578
|
+
function sceneWarnings(cuts) {
|
|
12579
|
+
return crowdedRuns(cuts).map(
|
|
12580
|
+
(run2) => `${run2.length} full-frame changes within ${SCENE_WINDOW} s (at ${run2.map((c) => `${c.time.toFixed(2)}s`).join(", ")}): more than one cut in ${SCENE_WINDOW} s is hard to follow`
|
|
12581
|
+
);
|
|
12582
|
+
}
|
|
12583
|
+
function posterWarning(which, deviation) {
|
|
12584
|
+
if (deviation >= UNIFORM_BELOW) return null;
|
|
12585
|
+
return which === "first" ? `the first frame is near-uniform (deviation ${deviation.toFixed(2)} of 255): the video opens on an empty or faded frame, which is what a feed shows before it plays` : `the last frame is near-uniform (deviation ${deviation.toFixed(2)} of 255): the video ends on an empty or faded frame, which is what a player rests on`;
|
|
12586
|
+
}
|
|
12587
|
+
|
|
12588
|
+
// src/plugin/checkVideo.ts
|
|
12589
|
+
var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set(["json", "help", "strict"]);
|
|
12590
|
+
var THUMB_LONG_SIDE = 64;
|
|
12591
|
+
async function probeVideoFacts(file) {
|
|
12592
|
+
const input = new Input2({
|
|
12593
|
+
source: new FilePathSource2(file),
|
|
12594
|
+
formats: ALL_FORMATS2
|
|
12595
|
+
});
|
|
12596
|
+
try {
|
|
12597
|
+
const video = await input.getPrimaryVideoTrack();
|
|
12598
|
+
if (!video) throw new UsageError(`${file} has no video track`);
|
|
12599
|
+
const [format, duration, stats, audio, bytes] = await Promise.all([
|
|
12600
|
+
input.getFormat(),
|
|
12601
|
+
input.computeDuration(),
|
|
12602
|
+
video.computePacketStats(240),
|
|
12603
|
+
input.getPrimaryAudioTrack(),
|
|
12604
|
+
stat3(file).then((s) => s.size)
|
|
12605
|
+
]);
|
|
12606
|
+
return {
|
|
12607
|
+
file,
|
|
12608
|
+
bytes,
|
|
12609
|
+
duration: Math.round(duration * 1e3) / 1e3,
|
|
12610
|
+
width: video.displayWidth,
|
|
12611
|
+
height: video.displayHeight,
|
|
12612
|
+
fps: Math.round(stats.averagePacketRate * 100) / 100,
|
|
12613
|
+
container: format.mimeType,
|
|
12614
|
+
video: {
|
|
12615
|
+
codec: video.codec,
|
|
12616
|
+
codecString: await video.getCodecParameterString()
|
|
12617
|
+
},
|
|
12618
|
+
audio: audio ? {
|
|
12619
|
+
codec: audio.codec,
|
|
12620
|
+
codecString: await audio.getCodecParameterString(),
|
|
12621
|
+
sampleRate: audio.sampleRate,
|
|
12622
|
+
channels: audio.numberOfChannels
|
|
12623
|
+
} : null
|
|
12624
|
+
};
|
|
12625
|
+
} finally {
|
|
12626
|
+
input.dispose();
|
|
12627
|
+
}
|
|
12628
|
+
}
|
|
12629
|
+
async function mediabunnyBundle() {
|
|
12630
|
+
const entry = createRequire2(import.meta.url).resolve("mediabunny");
|
|
12631
|
+
const bundle = join23(dirname8(entry), "mediabunny.min.mjs");
|
|
12632
|
+
if (existsSync21(bundle)) return readFile13(bundle, "utf8");
|
|
12633
|
+
return readFile13(
|
|
12634
|
+
join23(entry.split("/dist/")[0], "dist/bundles/mediabunny.min.mjs"),
|
|
12635
|
+
"utf8"
|
|
12636
|
+
);
|
|
12637
|
+
}
|
|
12638
|
+
async function decodeInPage(opts) {
|
|
12639
|
+
const w = window;
|
|
12640
|
+
try {
|
|
12641
|
+
const load = new Function("u", "return import(u)");
|
|
12642
|
+
const MB = await load(
|
|
12643
|
+
URL.createObjectURL(new Blob([opts.mbCode], { type: "text/javascript" }))
|
|
12644
|
+
);
|
|
12645
|
+
const input = new MB.Input({
|
|
12646
|
+
source: new MB.UrlSource(opts.videoUrl),
|
|
12647
|
+
formats: MB.ALL_FORMATS
|
|
12648
|
+
});
|
|
12649
|
+
const track = await input.getPrimaryVideoTrack();
|
|
12650
|
+
if (!track) throw new Error("no video track");
|
|
12651
|
+
if (!await track.canDecode())
|
|
12652
|
+
throw new Error(`this browser cannot decode ${String(track.codec)}`);
|
|
12653
|
+
const pixels = (canvas) => {
|
|
12654
|
+
const g = canvas.getContext("2d", {
|
|
12655
|
+
willReadFrequently: true
|
|
12656
|
+
});
|
|
12657
|
+
const px = g.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
12658
|
+
const out = new Array(px.length / 4 * 3);
|
|
12659
|
+
for (let p = 0, q = 0; p < px.length; p += 4, q += 3) {
|
|
12660
|
+
out[q] = px[p];
|
|
12661
|
+
out[q + 1] = px[p + 1];
|
|
12662
|
+
out[q + 2] = px[p + 2];
|
|
12663
|
+
}
|
|
12664
|
+
return out;
|
|
12665
|
+
};
|
|
12666
|
+
const deviation = (a) => {
|
|
12667
|
+
let total = 0;
|
|
12668
|
+
for (let c = 0; c < 3; c++) {
|
|
12669
|
+
let s = 0;
|
|
12670
|
+
let n = 0;
|
|
12671
|
+
for (let i = c; i < a.length; i += 3) {
|
|
12672
|
+
s += a[i];
|
|
12673
|
+
n++;
|
|
12674
|
+
}
|
|
12675
|
+
const m = s / Math.max(1, n);
|
|
12676
|
+
let q = 0;
|
|
12677
|
+
for (let i = c; i < a.length; i += 3) q += (a[i] - m) * (a[i] - m);
|
|
12678
|
+
total += Math.sqrt(q / Math.max(1, n));
|
|
12679
|
+
}
|
|
12680
|
+
return total / 3;
|
|
12681
|
+
};
|
|
12682
|
+
const difference = (a, b) => {
|
|
12683
|
+
let s = 0;
|
|
12684
|
+
for (let i = 0; i < a.length; i++) s += Math.abs(a[i] - b[i]);
|
|
12685
|
+
return s / Math.max(1, a.length) / 255;
|
|
12686
|
+
};
|
|
12687
|
+
const coverageOf = (a, b) => {
|
|
12688
|
+
const W = opts.thumbW;
|
|
12689
|
+
const H = opts.thumbH;
|
|
12690
|
+
let cells = 0;
|
|
12691
|
+
let changed = 0;
|
|
12692
|
+
for (let cy = 0; cy < H; cy += 4)
|
|
12693
|
+
for (let cx = 0; cx < W; cx += 4) {
|
|
12694
|
+
let s = 0;
|
|
12695
|
+
let n = 0;
|
|
12696
|
+
for (let y = cy; y < Math.min(H, cy + 4); y++)
|
|
12697
|
+
for (let x = cx; x < Math.min(W, cx + 4); x++) {
|
|
12698
|
+
const k = (y * W + x) * 3;
|
|
12699
|
+
s += Math.abs(a[k] - b[k]) + Math.abs(a[k + 1] - b[k + 1]) + Math.abs(a[k + 2] - b[k + 2]);
|
|
12700
|
+
n += 3;
|
|
12701
|
+
}
|
|
12702
|
+
cells++;
|
|
12703
|
+
if (s / Math.max(1, n) / 255 >= opts.cellChanged) changed++;
|
|
12704
|
+
}
|
|
12705
|
+
return changed / Math.max(1, cells);
|
|
12706
|
+
};
|
|
12707
|
+
const small = document.createElement("canvas");
|
|
12708
|
+
small.width = opts.thumbW;
|
|
12709
|
+
small.height = opts.thumbH;
|
|
12710
|
+
const sctx = small.getContext("2d", { willReadFrequently: true });
|
|
12711
|
+
sctx.imageSmoothingEnabled = true;
|
|
12712
|
+
sctx.imageSmoothingQuality = "high";
|
|
12713
|
+
const samples = new MB.VideoSampleSink(track);
|
|
12714
|
+
const step = 1 / opts.sampleFps;
|
|
12715
|
+
const times = [];
|
|
12716
|
+
const changes = [];
|
|
12717
|
+
const coverage = [];
|
|
12718
|
+
let next = -Infinity;
|
|
12719
|
+
let prev = null;
|
|
12720
|
+
let first = null;
|
|
12721
|
+
let firstTime = 0;
|
|
12722
|
+
let lastTime = 0;
|
|
12723
|
+
let lastKept = -1;
|
|
12724
|
+
let lastGray = null;
|
|
12725
|
+
let tail = null;
|
|
12726
|
+
const duration = await input.computeDuration();
|
|
12727
|
+
for await (const sample2 of samples.samples()) {
|
|
12728
|
+
const t = sample2.timestamp;
|
|
12729
|
+
sctx.clearRect(0, 0, opts.thumbW, opts.thumbH);
|
|
12730
|
+
sample2.draw(sctx, 0, 0, opts.thumbW, opts.thumbH);
|
|
12731
|
+
sample2.close();
|
|
12732
|
+
lastTime = t;
|
|
12733
|
+
if (t + step / 2 < next) {
|
|
12734
|
+
tail = pixels(small);
|
|
12735
|
+
continue;
|
|
12736
|
+
}
|
|
12737
|
+
tail = null;
|
|
12738
|
+
const g = pixels(small);
|
|
12739
|
+
if (!first) {
|
|
12740
|
+
first = g;
|
|
12741
|
+
firstTime = t;
|
|
12742
|
+
}
|
|
12743
|
+
times.push(t);
|
|
12744
|
+
changes.push(prev ? difference(prev, g) : 0);
|
|
12745
|
+
coverage.push(prev ? coverageOf(prev, g) : 0);
|
|
12746
|
+
prev = g;
|
|
12747
|
+
lastKept = t;
|
|
12748
|
+
lastGray = g;
|
|
12749
|
+
next = next === -Infinity ? t + step : next + step;
|
|
12750
|
+
if (next <= t) next = t + step;
|
|
12751
|
+
w.__progress = duration > 0 ? Math.min(0.9, t / duration * 0.9) : 0;
|
|
12752
|
+
}
|
|
12753
|
+
if (!first || !lastGray) throw new Error("no video frame decoded");
|
|
12754
|
+
if (tail && lastTime > lastKept) {
|
|
12755
|
+
times.push(lastTime);
|
|
12756
|
+
changes.push(prev ? difference(prev, tail) : 0);
|
|
12757
|
+
coverage.push(prev ? coverageOf(prev, tail) : 0);
|
|
12758
|
+
lastGray = tail;
|
|
12759
|
+
}
|
|
12760
|
+
const toBase64 = (bytes) => {
|
|
12761
|
+
let s = "";
|
|
12762
|
+
for (let i = 0; i < bytes.length; i += 32768)
|
|
12763
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
12764
|
+
return btoa(s);
|
|
12765
|
+
};
|
|
12766
|
+
const full = new MB.CanvasSink(track);
|
|
12767
|
+
const png = async (t) => {
|
|
12768
|
+
const wc = await full.getCanvas(t);
|
|
12769
|
+
if (!wc) return "";
|
|
12770
|
+
const c = wc.canvas;
|
|
12771
|
+
const blob = "convertToBlob" in c ? await c.convertToBlob({ type: "image/png" }) : await new Promise(
|
|
12772
|
+
(res, rej) => c.toBlob(
|
|
12773
|
+
(b) => b ? res(b) : rej(new Error("toBlob failed")),
|
|
12774
|
+
"image/png"
|
|
12775
|
+
)
|
|
12776
|
+
);
|
|
12777
|
+
return toBase64(new Uint8Array(await blob.arrayBuffer()));
|
|
12778
|
+
};
|
|
12779
|
+
const firstPng = await png(firstTime);
|
|
12780
|
+
const lastPng = await png(lastTime);
|
|
12781
|
+
let audio = null;
|
|
12782
|
+
const at2 = await input.getPrimaryAudioTrack();
|
|
12783
|
+
if (at2 && await at2.canDecode()) {
|
|
12784
|
+
const make = new Function(`return (${opts.meterSrc})`)();
|
|
12785
|
+
const meter = make(at2.sampleRate, at2.numberOfChannels, opts.subBlock);
|
|
12786
|
+
const sink = new MB.AudioBufferSink(at2);
|
|
12787
|
+
for await (const wb of sink.buffers()) {
|
|
12788
|
+
const b = wb.buffer;
|
|
12789
|
+
const chans = [];
|
|
12790
|
+
for (let c = 0; c < at2.numberOfChannels; c++)
|
|
12791
|
+
chans.push(
|
|
12792
|
+
c < b.numberOfChannels ? b.getChannelData(c) : new Float32Array(0)
|
|
12793
|
+
);
|
|
12794
|
+
meter.push(chans);
|
|
12795
|
+
}
|
|
12796
|
+
audio = {
|
|
12797
|
+
sampleRate: at2.sampleRate,
|
|
12798
|
+
channels: at2.numberOfChannels,
|
|
12799
|
+
...meter.finish()
|
|
12800
|
+
};
|
|
12801
|
+
}
|
|
12802
|
+
input.dispose();
|
|
12803
|
+
w.__done = {
|
|
12804
|
+
times,
|
|
12805
|
+
changes,
|
|
12806
|
+
coverage,
|
|
12807
|
+
firstDeviation: deviation(first),
|
|
12808
|
+
lastDeviation: deviation(lastGray),
|
|
12809
|
+
firstTime,
|
|
12810
|
+
lastTime,
|
|
12811
|
+
firstPng,
|
|
12812
|
+
lastPng,
|
|
12813
|
+
audio
|
|
12814
|
+
};
|
|
12815
|
+
} catch (e) {
|
|
12816
|
+
w.__error = String(e instanceof Error && e.stack || e);
|
|
12817
|
+
}
|
|
12818
|
+
}
|
|
12819
|
+
async function checkVideo(browser, file, outDir) {
|
|
12820
|
+
const facts = await probeVideoFacts(file);
|
|
12821
|
+
const ceiling = facts.duration > 120 ? 10 : 30;
|
|
12822
|
+
const sampleFps = Math.max(
|
|
12823
|
+
Math.min(10, facts.fps || 10),
|
|
12824
|
+
Math.min(facts.fps || 30, ceiling)
|
|
12825
|
+
);
|
|
12826
|
+
const long = Math.max(facts.width, facts.height, 1);
|
|
12827
|
+
const side = (n) => Math.max(1, Math.round(n / long * THUMB_LONG_SIDE));
|
|
12828
|
+
const server = await startTakeServer(dirname8(resolve14(file)), {
|
|
12829
|
+
"/check.html": '<!doctype html><html><head><meta charset="utf-8"><link rel="icon" href="data:,"></head><body></body></html>'
|
|
12830
|
+
});
|
|
12831
|
+
const page = await browser.newPage();
|
|
12832
|
+
let done;
|
|
12833
|
+
try {
|
|
12834
|
+
await page.addInitScript(() => {
|
|
12835
|
+
;
|
|
12836
|
+
globalThis.__name = (f) => f;
|
|
12837
|
+
});
|
|
12838
|
+
await page.goto(`${server.base}/check.html`);
|
|
12839
|
+
void page.evaluate(decodeInPage, {
|
|
12840
|
+
mbCode: await mediabunnyBundle(),
|
|
12841
|
+
meterSrc: String(createLoudnessMeter),
|
|
12842
|
+
videoUrl: `/${encodeURIComponent(basename10(file))}`,
|
|
12843
|
+
sampleFps,
|
|
12844
|
+
thumbW: side(facts.width),
|
|
12845
|
+
thumbH: side(facts.height),
|
|
12846
|
+
subBlock: SUB_BLOCK_SECONDS,
|
|
12847
|
+
cellChanged: CELL_CHANGED_AT
|
|
12848
|
+
}).catch(() => {
|
|
12849
|
+
});
|
|
12850
|
+
done = await waitForPageDone(
|
|
12851
|
+
page,
|
|
12852
|
+
"check-video",
|
|
12853
|
+
() => {
|
|
12854
|
+
},
|
|
12855
|
+
12e4 + facts.duration * 4e3
|
|
12856
|
+
);
|
|
12857
|
+
} finally {
|
|
12858
|
+
await page.close();
|
|
12859
|
+
server.close();
|
|
12860
|
+
}
|
|
12861
|
+
await mkdir9(outDir, { recursive: true });
|
|
12862
|
+
const firstFile = join23(outDir, "first.png");
|
|
12863
|
+
const lastFile = join23(outDir, "last.png");
|
|
12864
|
+
await writeFile10(firstFile, Buffer.from(done.firstPng, "base64"));
|
|
12865
|
+
await writeFile10(lastFile, Buffer.from(done.lastPng, "base64"));
|
|
12866
|
+
const changes = sceneChanges2(done.times, done.changes, done.coverage);
|
|
12867
|
+
const loudness = done.audio ? loudnessFromEnergies(done.audio.energies, done.audio.peak) : null;
|
|
12868
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
12869
|
+
const posters = {
|
|
12870
|
+
first: {
|
|
12871
|
+
file: firstFile,
|
|
12872
|
+
time: round2(done.firstTime),
|
|
12873
|
+
deviation: round2(done.firstDeviation),
|
|
12874
|
+
uniform: posterWarning("first", done.firstDeviation) !== null
|
|
12875
|
+
},
|
|
12876
|
+
last: {
|
|
12877
|
+
file: lastFile,
|
|
12878
|
+
time: round2(done.lastTime),
|
|
12879
|
+
deviation: round2(done.lastDeviation),
|
|
12880
|
+
uniform: posterWarning("last", done.lastDeviation) !== null
|
|
12881
|
+
}
|
|
12882
|
+
};
|
|
12883
|
+
const warnings = [
|
|
12884
|
+
...sceneWarnings(changes),
|
|
12885
|
+
...loudness ? loudnessWarnings(loudness) : [],
|
|
12886
|
+
...[
|
|
12887
|
+
posterWarning("first", done.firstDeviation),
|
|
12888
|
+
posterWarning("last", done.lastDeviation)
|
|
12889
|
+
].filter((w) => w !== null)
|
|
12890
|
+
];
|
|
12891
|
+
const result = {
|
|
12892
|
+
...facts,
|
|
12893
|
+
scenes: {
|
|
12894
|
+
sampleFps: round2(sampleFps),
|
|
12895
|
+
threshold: SCENE_CHANGE_AT,
|
|
12896
|
+
isolatedThreshold: ISOLATED_CHANGE_AT,
|
|
12897
|
+
window: SCENE_WINDOW,
|
|
12898
|
+
changes: changes.map((c) => ({ ...c, time: round2(c.time) })),
|
|
12899
|
+
perWindow: windowCounts(changes, facts.duration)
|
|
12900
|
+
},
|
|
12901
|
+
loudness,
|
|
12902
|
+
posters,
|
|
12903
|
+
warnings,
|
|
12904
|
+
outDir
|
|
12905
|
+
};
|
|
12906
|
+
await writeFile10(
|
|
12907
|
+
join23(outDir, "check.json"),
|
|
12908
|
+
`${JSON.stringify(result, null, 2)}
|
|
12909
|
+
`
|
|
12910
|
+
);
|
|
12911
|
+
return result;
|
|
12912
|
+
}
|
|
12913
|
+
var mb = (n) => n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
12914
|
+
function checkVideoLines(r) {
|
|
12915
|
+
const codecs = [
|
|
12916
|
+
r.video.codecString ?? r.video.codec ?? "unknown",
|
|
12917
|
+
...r.audio ? [r.audio.codecString ?? r.audio.codec ?? "unknown"] : []
|
|
12918
|
+
].join(" + ");
|
|
12919
|
+
const cuts = r.scenes.changes;
|
|
12920
|
+
const l = r.loudness;
|
|
12921
|
+
const st = l?.shortTerm.map(
|
|
12922
|
+
(s) => s.loudness === null ? "-" : String(s.loudness)
|
|
12923
|
+
);
|
|
12924
|
+
return [
|
|
12925
|
+
`Checked ${r.file}: ${r.duration.toFixed(2)}s, ${r.width}x${r.height} @ ${r.fps} fps, ${mb(r.bytes)}, ${codecs}`,
|
|
12926
|
+
` scene changes: ${cuts.length}${cuts.length ? ` (${cuts.map((c) => `${c.time.toFixed(2)}s`).join(", ")})` : ""}; per ${r.scenes.window} s: ${r.scenes.perWindow.map((w) => w.count).join(", ")}`,
|
|
12927
|
+
r.audio ? ` loudness: ${l?.integrated ?? "silent"}${l?.integrated !== null ? " LUFS" : ""} integrated; short-term every 3 s: ${st?.join(", ")}; sample peak ${l?.samplePeakDb ?? "-inf"} dBFS` : " loudness: no audio track",
|
|
12928
|
+
` posters: ${r.posters.first.file} (deviation ${r.posters.first.deviation}), ${r.posters.last.file} (deviation ${r.posters.last.deviation})`,
|
|
12929
|
+
` ${r.warnings.length ? `${r.warnings.length} warning${r.warnings.length === 1 ? "" : "s"}` : "no warnings"}; ${join23(r.outDir, "check.json")}`
|
|
12930
|
+
].join("\n");
|
|
12931
|
+
}
|
|
12932
|
+
async function cmdCheckVideo(argv) {
|
|
12933
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
12934
|
+
const file = positionals[0];
|
|
12935
|
+
if (!file)
|
|
12936
|
+
throw new UsageError(
|
|
12937
|
+
"vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]"
|
|
12938
|
+
);
|
|
12939
|
+
if (!existsSync21(file)) throw new UsageError(`${file} does not exist`);
|
|
12940
|
+
const r = createReporter(flags.json === true);
|
|
12941
|
+
const strict = flags.strict === true;
|
|
12942
|
+
const outDir = resolve14(
|
|
12943
|
+
strFlag(flags, "out") ?? join23(dirname8(file), `${basename10(file, extname4(file))}-check`)
|
|
12944
|
+
);
|
|
12945
|
+
const browser = await launchBrowser();
|
|
12946
|
+
try {
|
|
12947
|
+
r.event({ event: "phase", phase: "check-video" });
|
|
12948
|
+
const result = await checkVideo(browser, file, outDir);
|
|
12949
|
+
for (const w of result.warnings) r.warn(w);
|
|
12950
|
+
r.done({ ...result }, checkVideoLines(result));
|
|
12951
|
+
return result.warnings.length && strict ? EXIT_USAGE : EXIT_OK;
|
|
12952
|
+
} finally {
|
|
12953
|
+
await browser.close();
|
|
12954
|
+
}
|
|
12955
|
+
}
|
|
12956
|
+
|
|
12957
|
+
// src/plugin/join.ts
|
|
12958
|
+
import { existsSync as existsSync22, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
12959
|
+
import { copyFile as copyFile2, mkdir as mkdir10, rm as rm3 } from "fs/promises";
|
|
12960
|
+
import {
|
|
12961
|
+
basename as basename11,
|
|
12962
|
+
dirname as dirname9,
|
|
12963
|
+
isAbsolute as isAbsolute3,
|
|
12964
|
+
join as join24,
|
|
12965
|
+
relative as relative4,
|
|
12966
|
+
resolve as resolve15,
|
|
12967
|
+
sep as sep2
|
|
12968
|
+
} from "path";
|
|
12969
|
+
import {
|
|
12970
|
+
mediaKey as mediaKey2,
|
|
12971
|
+
mediaRefId,
|
|
12972
|
+
migrateMotion as migrateMotion3,
|
|
12973
|
+
projectFromArtifact as projectFromArtifact2,
|
|
12974
|
+
ratedSegments as ratedSegments8,
|
|
12975
|
+
resolveAnchor as resolveAnchor5,
|
|
12976
|
+
segmentOutputExtents as segmentOutputExtents3
|
|
12977
|
+
} from "@vosjs/studio-core";
|
|
12978
|
+
import { totalDuration as totalDuration2 } from "@vosjs/timeline";
|
|
12979
|
+
|
|
12980
|
+
// src/plugin/stepCaptions.ts
|
|
12981
|
+
import {
|
|
12982
|
+
docMediaList,
|
|
12983
|
+
findStep as findStep2,
|
|
12984
|
+
mediaKey,
|
|
12985
|
+
ratedSegments as ratedSegments7,
|
|
12986
|
+
resolveAnchor as resolveAnchor4,
|
|
12987
|
+
segmentOutputExtents as segmentOutputExtents2
|
|
12988
|
+
} from "@vosjs/studio-core";
|
|
12989
|
+
import { totalDuration } from "@vosjs/timeline";
|
|
12990
|
+
var STEP_CAPTIONS_FROM = "steps";
|
|
12991
|
+
var STEP_CAPTION_OFFSET = 0.2;
|
|
12992
|
+
var CAPTION_MAX = 3;
|
|
12993
|
+
var CAPTION_MIN = 1.2;
|
|
12994
|
+
var CAPTION_GAP = 0.1;
|
|
12995
|
+
function captionOf(step, actions, script) {
|
|
12996
|
+
const own = step.caption;
|
|
12997
|
+
if (typeof own === "string" && own.trim()) return own.trim();
|
|
12998
|
+
for (const list of [
|
|
12999
|
+
actions?.steps,
|
|
13000
|
+
Array.isArray(script?.steps) ? script.steps : void 0
|
|
13001
|
+
]) {
|
|
13002
|
+
if (!Array.isArray(list)) continue;
|
|
13003
|
+
const byId = step.id !== void 0 ? list.find((s) => s?.id === step.id) : void 0;
|
|
13004
|
+
const raw = byId ?? list[step.step];
|
|
13005
|
+
if (typeof raw?.caption === "string" && raw.caption.trim())
|
|
13006
|
+
return raw.caption.trim();
|
|
13007
|
+
}
|
|
13008
|
+
return null;
|
|
13009
|
+
}
|
|
13010
|
+
var safeId = (v) => String(v).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "step";
|
|
13011
|
+
function landStepCaptions(doc, actions) {
|
|
13012
|
+
const report = { landed: [], skipped: [], uncaptioned: [] };
|
|
13013
|
+
const kept = (doc.overlays ?? []).filter((o) => o.from !== STEP_CAPTIONS_FROM);
|
|
13014
|
+
const rated = ratedSegments7(doc);
|
|
13015
|
+
const footageEnd = totalDuration(rated);
|
|
13016
|
+
const docSegments = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
|
|
13017
|
+
const extents = segmentOutputExtents2(doc);
|
|
13018
|
+
const clipEnd = (start, media) => {
|
|
13019
|
+
for (let i = 0; i < extents.length; i++) {
|
|
13020
|
+
const e = extents[i];
|
|
13021
|
+
if (mediaKey(docSegments[i]?.media) === media && start >= e.start - 1e-6 && start < e.end - 1e-6)
|
|
13022
|
+
return e.end;
|
|
13023
|
+
}
|
|
13024
|
+
return footageEnd;
|
|
13025
|
+
};
|
|
13026
|
+
const ends = /* @__PURE__ */ new Map();
|
|
13027
|
+
const spoken = (media, step, steps) => kept.some((o) => {
|
|
13028
|
+
if (o.kind !== "text" || o.preset !== "caption") return false;
|
|
13029
|
+
if (o.anchor && mediaKey(o.anchor.media) === media)
|
|
13030
|
+
return findStep2(steps, o.anchor.step) === step;
|
|
13031
|
+
return media === "" && o.id === `${CAPTION_ID_PREFIX}${step.step}`;
|
|
13032
|
+
});
|
|
13033
|
+
const fresh = [];
|
|
13034
|
+
const taken = new Set(kept.map((o) => o.id));
|
|
13035
|
+
for (const { id: media, source } of docMediaList(doc)) {
|
|
13036
|
+
const steps = source.meta.steps ?? [];
|
|
13037
|
+
let captioned = 0;
|
|
13038
|
+
for (const step of steps) {
|
|
13039
|
+
const caption = captionOf(step, actions.get(media), source.script);
|
|
13040
|
+
if (!caption) continue;
|
|
13041
|
+
captioned++;
|
|
13042
|
+
const ref = step.id ?? step.step;
|
|
13043
|
+
const label = `step ${JSON.stringify(ref)}${media ? ` of ${media}` : ""}`;
|
|
13044
|
+
if (step.skipped) {
|
|
13045
|
+
report.skipped.push(`${label}: the recorder skipped it`);
|
|
13046
|
+
continue;
|
|
13047
|
+
}
|
|
13048
|
+
if (spoken(media, step, steps)) {
|
|
13049
|
+
report.skipped.push(`${label}: a caption is already anchored to it`);
|
|
13050
|
+
continue;
|
|
13051
|
+
}
|
|
13052
|
+
const anchor = {
|
|
13053
|
+
step: ref,
|
|
13054
|
+
...media ? { media } : {},
|
|
13055
|
+
at: "end",
|
|
13056
|
+
offset: STEP_CAPTION_OFFSET
|
|
13057
|
+
};
|
|
13058
|
+
const placed = resolveAnchor4(doc, anchor, rated);
|
|
13059
|
+
if (placed.start === null) {
|
|
13060
|
+
report.skipped.push(`${label}: the cut leaves it out`);
|
|
13061
|
+
continue;
|
|
13062
|
+
}
|
|
13063
|
+
const end = clipEnd(placed.start, media);
|
|
13064
|
+
if (placed.start >= end - CAPTION_MIN) {
|
|
13065
|
+
report.skipped.push(`${label}: it plays in the last second of its clip`);
|
|
13066
|
+
continue;
|
|
13067
|
+
}
|
|
13068
|
+
const base = `step-${media ? `${media}-` : ""}${safeId(ref)}`;
|
|
13069
|
+
let id = base;
|
|
13070
|
+
for (let n = 2; taken.has(id); n++) id = `${base}-${n}`;
|
|
13071
|
+
taken.add(id);
|
|
13072
|
+
const clip = {
|
|
13073
|
+
...houseCaption(id, caption, placed.start, CAPTION_MAX),
|
|
13074
|
+
anchor,
|
|
13075
|
+
from: STEP_CAPTIONS_FROM
|
|
13076
|
+
};
|
|
13077
|
+
ends.set(clip, end);
|
|
13078
|
+
fresh.push(clip);
|
|
13079
|
+
report.landed.push({
|
|
13080
|
+
id,
|
|
13081
|
+
step: ref,
|
|
13082
|
+
...media ? { media } : {},
|
|
13083
|
+
start: placed.start
|
|
13084
|
+
});
|
|
13085
|
+
}
|
|
13086
|
+
if (steps.length && !captioned) report.uncaptioned.push(media);
|
|
13087
|
+
}
|
|
13088
|
+
fresh.sort((a, b) => a.start - b.start);
|
|
13089
|
+
fresh.forEach((c, i) => {
|
|
13090
|
+
const next = fresh[i + 1];
|
|
13091
|
+
const clipRoom = (ends.get(c) ?? footageEnd) - c.start;
|
|
13092
|
+
const nextRoom = next ? next.start - CAPTION_GAP - c.start : Infinity;
|
|
13093
|
+
c.duration = +Math.min(
|
|
13094
|
+
CAPTION_MAX,
|
|
13095
|
+
clipRoom,
|
|
13096
|
+
Math.max(CAPTION_MIN, nextRoom)
|
|
13097
|
+
).toFixed(3);
|
|
13098
|
+
});
|
|
13099
|
+
const overlays = [...kept, ...fresh];
|
|
13100
|
+
if (overlays.length) doc.overlays = overlays;
|
|
13101
|
+
else delete doc.overlays;
|
|
13102
|
+
return report;
|
|
13103
|
+
}
|
|
13104
|
+
function bareCaptionsFlag(argv) {
|
|
13105
|
+
return argv.map(
|
|
13106
|
+
(a, i) => a === "--captions" && (i + 1 >= argv.length || argv[i + 1].startsWith("--")) ? "--captions=steps" : a
|
|
13107
|
+
);
|
|
13108
|
+
}
|
|
13109
|
+
|
|
13110
|
+
// src/plugin/join.ts
|
|
13111
|
+
var FULL_BLEED_FRAME = {
|
|
13112
|
+
inset: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
13113
|
+
radius: 0,
|
|
13114
|
+
shadow: 0,
|
|
13115
|
+
shadowContact: 0,
|
|
13116
|
+
border: 0,
|
|
13117
|
+
fit: "cover"
|
|
13118
|
+
};
|
|
13119
|
+
var round32 = (v) => Math.round(v * 1e3) / 1e3;
|
|
13120
|
+
var FILE_SOUND_FADE = 0.15;
|
|
13121
|
+
var prefixed = (id, key) => `${id}/${takeRelativeFile(key)}`;
|
|
13122
|
+
function fileDoc(key, probed, t0 = 0) {
|
|
13123
|
+
const meta = {
|
|
13124
|
+
dpr: 1,
|
|
13125
|
+
zoom: 1,
|
|
13126
|
+
t0,
|
|
13127
|
+
durationMs: probed.durationMs,
|
|
13128
|
+
width: probed.width,
|
|
13129
|
+
height: probed.height,
|
|
13130
|
+
captureWidth: probed.width,
|
|
13131
|
+
captureHeight: probed.height,
|
|
13132
|
+
fps: probed.fps > 0 ? probed.fps : 30,
|
|
13133
|
+
hasAudio: probed.hasAudio,
|
|
13134
|
+
producer: "ingest"
|
|
13135
|
+
};
|
|
13136
|
+
const doc = projectFromArtifact2({ videoKey: key, cursor: [], meta }, key, {
|
|
13137
|
+
footage: true
|
|
13138
|
+
}).doc;
|
|
13139
|
+
doc.segments = [{ in: 0, out: round32(probed.durationMs / 1e3) }];
|
|
13140
|
+
return doc;
|
|
13141
|
+
}
|
|
13142
|
+
function endingLayers(doc) {
|
|
13143
|
+
const pieces = ratedSegments8(doc);
|
|
13144
|
+
const end = totalDuration2(pieces);
|
|
13145
|
+
if (!(end > 0)) return [];
|
|
13146
|
+
return (doc.overlays ?? []).filter((o) => {
|
|
13147
|
+
const start = o.anchor ? resolveAnchor5(doc, o.anchor, pieces).start ?? o.start : o.start;
|
|
13148
|
+
return start >= end - 1e-3;
|
|
13149
|
+
});
|
|
13150
|
+
}
|
|
13151
|
+
function dropEnding(d, label, notes) {
|
|
13152
|
+
const ending = endingLayers(d);
|
|
13153
|
+
const legacy = d.endCard !== void 0;
|
|
13154
|
+
if (!ending.length && !legacy) return;
|
|
13155
|
+
const gone = new Set(ending);
|
|
13156
|
+
d.overlays = (d.overlays ?? []).filter((o) => !gone.has(o));
|
|
13157
|
+
if (!d.overlays.length) delete d.overlays;
|
|
13158
|
+
if (legacy) delete d.endCard;
|
|
13159
|
+
const named = [
|
|
13160
|
+
...ending.map((o) => o.id),
|
|
13161
|
+
...legacy ? ["its endCard"] : []
|
|
13162
|
+
].join(", ");
|
|
13163
|
+
notes.push(
|
|
13164
|
+
`${label}: dropped ${named}, which played after its footage and would land over the next input (only the last input's ending stays, at the end of the film)`
|
|
13165
|
+
);
|
|
13166
|
+
}
|
|
13167
|
+
function segs0(d) {
|
|
13168
|
+
return d.segments.length ? d.segments[0] : { in: 0, out: round32(d.source.meta.durationMs / 1e3) };
|
|
13169
|
+
}
|
|
13170
|
+
function joinDocs(parts) {
|
|
13171
|
+
if (parts.length < 2)
|
|
13172
|
+
throw new UsageError("vos join needs two inputs or more");
|
|
13173
|
+
const [first, ...rest] = parts;
|
|
13174
|
+
const doc = structuredClone(first.doc);
|
|
13175
|
+
const notes = [];
|
|
13176
|
+
dropEnding(doc, first.label, notes);
|
|
13177
|
+
const copies = /* @__PURE__ */ new Map();
|
|
13178
|
+
const copy = (c) => {
|
|
13179
|
+
if (!copies.has(c.to)) copies.set(c.to, c);
|
|
13180
|
+
};
|
|
13181
|
+
const actions = /* @__PURE__ */ new Map([["", first.actions]]);
|
|
13182
|
+
if (first.file) {
|
|
13183
|
+
copy({
|
|
13184
|
+
from: first.file.path,
|
|
13185
|
+
to: takeRelativeFile(doc.source.videoKey),
|
|
13186
|
+
remux: first.file.ext
|
|
13187
|
+
});
|
|
13188
|
+
} else {
|
|
13189
|
+
const refs = [
|
|
13190
|
+
...isTakeRelativeKey(doc.source.videoKey) ? [{ where: "recording", key: doc.source.videoKey, set: () => {
|
|
13191
|
+
} }] : [],
|
|
13192
|
+
...sourceSidecarRefs(doc),
|
|
13193
|
+
...docMediaRefs(doc)
|
|
13194
|
+
];
|
|
13195
|
+
for (const ref of refs)
|
|
13196
|
+
copy({
|
|
13197
|
+
from: join24(first.dir, takeRelativeFile(ref.key)),
|
|
13198
|
+
to: takeRelativeFile(ref.key),
|
|
13199
|
+
base: first.dir
|
|
13200
|
+
});
|
|
13201
|
+
}
|
|
13202
|
+
if (!doc.segments.length)
|
|
13203
|
+
doc.segments = [{ in: 0, out: round32(doc.source.meta.durationMs / 1e3) }];
|
|
13204
|
+
const taken = new Set((doc.media ?? []).map((m) => m.id));
|
|
13205
|
+
const allocate = () => {
|
|
13206
|
+
let n = 1;
|
|
13207
|
+
while (taken.has(`m${n}`)) n++;
|
|
13208
|
+
taken.add(`m${n}`);
|
|
13209
|
+
return `m${n}`;
|
|
13210
|
+
};
|
|
13211
|
+
const media = [...doc.media ?? []];
|
|
13212
|
+
const joined = [];
|
|
13213
|
+
const placed = [];
|
|
13214
|
+
for (const part of rest) {
|
|
13215
|
+
const d = structuredClone(migrateMotion3(part.doc));
|
|
13216
|
+
if (part !== rest[rest.length - 1]) dropEnding(d, part.label, notes);
|
|
13217
|
+
const pid = allocate();
|
|
13218
|
+
const idMap = /* @__PURE__ */ new Map([["", pid]]);
|
|
13219
|
+
for (const m of d.media ?? []) idMap.set(m.id, allocate());
|
|
13220
|
+
const mapMedia = (k) => idMap.get(mediaKey2(k)) ?? pid;
|
|
13221
|
+
joined.push({ label: part.label, id: pid });
|
|
13222
|
+
actions.set(pid, part.actions);
|
|
13223
|
+
if (part.file) {
|
|
13224
|
+
const to = `${pid}/recording.${part.file.ext}`;
|
|
13225
|
+
copy({ from: part.file.path, to, remux: part.file.ext });
|
|
13226
|
+
d.source.videoKey = to;
|
|
13227
|
+
} else {
|
|
13228
|
+
const refs = [
|
|
13229
|
+
...isTakeRelativeKey(d.source.videoKey) ? [
|
|
13230
|
+
{
|
|
13231
|
+
where: "recording",
|
|
13232
|
+
key: d.source.videoKey,
|
|
13233
|
+
set: (next) => {
|
|
13234
|
+
d.source.videoKey = next;
|
|
13235
|
+
}
|
|
13236
|
+
}
|
|
13237
|
+
] : [],
|
|
13238
|
+
...sourceSidecarRefs(d),
|
|
13239
|
+
...(d.media ?? []).flatMap(
|
|
13240
|
+
(m) => [
|
|
13241
|
+
["videoKey", m.videoKey],
|
|
13242
|
+
["micKey", m.micKey],
|
|
13243
|
+
["camKey", m.camKey]
|
|
13244
|
+
].filter(([, k]) => isTakeRelativeKey(k)).map(([field, k]) => ({
|
|
13245
|
+
where: `media ${m.id}`,
|
|
13246
|
+
key: k,
|
|
13247
|
+
set: (next) => {
|
|
13248
|
+
;
|
|
13249
|
+
m[field] = next;
|
|
13250
|
+
}
|
|
13251
|
+
}))
|
|
13252
|
+
),
|
|
13253
|
+
...docOverlayRefs(d),
|
|
13254
|
+
...docObjectRefs(d)
|
|
13255
|
+
];
|
|
13256
|
+
for (const ref of refs) {
|
|
13257
|
+
const to = prefixed(pid, ref.key);
|
|
13258
|
+
copy({
|
|
13259
|
+
from: join24(part.dir, takeRelativeFile(ref.key)),
|
|
13260
|
+
to,
|
|
13261
|
+
base: part.dir
|
|
13262
|
+
});
|
|
13263
|
+
ref.set(to);
|
|
13264
|
+
}
|
|
13265
|
+
}
|
|
13266
|
+
media.push({
|
|
13267
|
+
...d.source,
|
|
13268
|
+
id: pid,
|
|
13269
|
+
...part.kind === "file" ? { frame: { ...FULL_BLEED_FRAME } } : {}
|
|
13270
|
+
});
|
|
13271
|
+
for (const m of d.media ?? []) media.push({ ...m, id: idMap.get(m.id) });
|
|
13272
|
+
const segs = d.segments.length ? d.segments : [{ in: 0, out: round32(d.source.meta.durationMs / 1e3) }];
|
|
13273
|
+
const at2 = doc.segments.length;
|
|
13274
|
+
doc.segments.push(...segs.map((s) => ({ ...s, media: mapMedia(s.media) })));
|
|
13275
|
+
const ns = (id) => `${pid}-${id}`;
|
|
13276
|
+
const tag = (s) => ({
|
|
13277
|
+
...s,
|
|
13278
|
+
id: ns(s.id),
|
|
13279
|
+
media: mapMedia(s.media)
|
|
13280
|
+
});
|
|
13281
|
+
doc.zoom = [...doc.zoom, ...d.zoom.map(tag)];
|
|
13282
|
+
if (d.speed?.length) doc.speed = [...doc.speed ?? [], ...d.speed.map(tag)];
|
|
13283
|
+
if (d.tilt?.length) doc.tilt = [...doc.tilt ?? [], ...d.tilt.map(tag)];
|
|
13284
|
+
if (d.camMotion?.length)
|
|
13285
|
+
doc.camMotion = [...doc.camMotion ?? [], ...d.camMotion.map(tag)];
|
|
13286
|
+
if (d.freeze?.length)
|
|
13287
|
+
doc.freeze = [...doc.freeze ?? [], ...d.freeze.map(tag)];
|
|
13288
|
+
if (d.rejected?.length)
|
|
13289
|
+
doc.rejected = [...doc.rejected ?? [], ...d.rejected.map(tag)];
|
|
13290
|
+
if (d.audio.length)
|
|
13291
|
+
notes.push(
|
|
13292
|
+
`${part.label}: its ${d.audio.length} sound clip${d.audio.length === 1 ? "" : "s"} stay behind (the film's sound is the first input's; add a score with vos score)`
|
|
13293
|
+
);
|
|
13294
|
+
placed.push({ part, d, pid, firstSegment: at2, mapMedia });
|
|
13295
|
+
}
|
|
13296
|
+
if (media.length) doc.media = media;
|
|
13297
|
+
const extents = segmentOutputExtents3(doc);
|
|
13298
|
+
for (const { part, d, pid, firstSegment, mapMedia } of placed) {
|
|
13299
|
+
const offset = extents[firstSegment]?.start ?? 0;
|
|
13300
|
+
const ownPieces = ratedSegments8(d);
|
|
13301
|
+
const ns = (id) => `${pid}-${id}`;
|
|
13302
|
+
let dropped = 0;
|
|
13303
|
+
for (const o of d.overlays ?? []) {
|
|
13304
|
+
const next = { ...o, id: ns(o.id) };
|
|
13305
|
+
if (next.kind !== "text" && next.kind !== "html") {
|
|
13306
|
+
const ref = mediaRefId(next.key);
|
|
13307
|
+
if (ref !== null) next.key = `media:${mapMedia(ref)}`;
|
|
13308
|
+
}
|
|
13309
|
+
if (o.anchor) {
|
|
13310
|
+
next.anchor = { ...o.anchor, media: mapMedia(o.anchor.media) };
|
|
13311
|
+
} else if (o.pin?.step !== void 0) {
|
|
13312
|
+
const r = resolveAnchor5(d, { step: o.pin.step }, ownPieces);
|
|
13313
|
+
if (r.start !== null) {
|
|
13314
|
+
next.anchor = {
|
|
13315
|
+
step: o.pin.step,
|
|
13316
|
+
media: pid,
|
|
13317
|
+
offset: round32(o.start - r.start)
|
|
13318
|
+
};
|
|
13319
|
+
} else {
|
|
13320
|
+
next.start = round32(o.start + offset);
|
|
13321
|
+
}
|
|
13322
|
+
delete next.pin;
|
|
13323
|
+
dropped++;
|
|
13324
|
+
} else {
|
|
13325
|
+
if (o.pin) {
|
|
13326
|
+
delete next.pin;
|
|
13327
|
+
dropped++;
|
|
13328
|
+
}
|
|
13329
|
+
next.start = round32(o.start + offset);
|
|
13330
|
+
}
|
|
13331
|
+
;
|
|
13332
|
+
(doc.overlays ??= []).push(next);
|
|
13333
|
+
}
|
|
13334
|
+
if (dropped)
|
|
13335
|
+
notes.push(
|
|
13336
|
+
`${part.label}: ${dropped} pinned layer${dropped === 1 ? "" : "s"} kept ${dropped === 1 ? "its" : "their"} time and lost the pin (a pin places a layer on the first input's footage only)`
|
|
13337
|
+
);
|
|
13338
|
+
if (part.file?.hasAudio) {
|
|
13339
|
+
const seg = segs0(d);
|
|
13340
|
+
const at2 = extents[firstSegment];
|
|
13341
|
+
if (part.mute) {
|
|
13342
|
+
notes.push(`${part.label}: its own sound is left out (--mute)`);
|
|
13343
|
+
} else if (seg && at2) {
|
|
13344
|
+
const len = Math.max(0, seg.out - seg.in);
|
|
13345
|
+
const fade = round32(Math.min(FILE_SOUND_FADE, len / 4));
|
|
13346
|
+
const id = `${pid}-sound`;
|
|
13347
|
+
doc.audio = [
|
|
13348
|
+
...doc.audio,
|
|
13349
|
+
{
|
|
13350
|
+
id,
|
|
13351
|
+
key: d.source.videoKey,
|
|
13352
|
+
name: basename11(part.file.path),
|
|
13353
|
+
start: round32(at2.start),
|
|
13354
|
+
in: round32(seg.in),
|
|
13355
|
+
out: round32(seg.out),
|
|
13356
|
+
duration: round32(d.source.meta.durationMs / 1e3),
|
|
13357
|
+
gain: 1,
|
|
13358
|
+
fadeIn: fade,
|
|
13359
|
+
fadeOut: fade
|
|
13360
|
+
}
|
|
13361
|
+
];
|
|
13362
|
+
notes.push(
|
|
13363
|
+
`${part.label}: its own sound plays as audio clip ${id}, ${at2.start.toFixed(2)}..${(at2.start + len).toFixed(2)}s (--mute ${part.label} to leave it out)`
|
|
13364
|
+
);
|
|
13365
|
+
}
|
|
13366
|
+
}
|
|
13367
|
+
for (const ob of d.objects ?? []) {
|
|
13368
|
+
;
|
|
13369
|
+
(doc.objects ??= []).push({
|
|
13370
|
+
...ob,
|
|
13371
|
+
id: ns(ob.id),
|
|
13372
|
+
...ob.span ? { span: { ...ob.span, start: round32(ob.span.start + offset) } } : {}
|
|
13373
|
+
});
|
|
13374
|
+
}
|
|
13375
|
+
}
|
|
13376
|
+
for (const part of rest)
|
|
13377
|
+
if (part.mute && !part.file?.hasAudio)
|
|
13378
|
+
notes.push(
|
|
13379
|
+
`${part.label}: --mute leaves nothing out (${part.file ? "the file carries no sound" : "a take's own footage sound is its recording's; its added sound stays behind anyway"})`
|
|
13380
|
+
);
|
|
13381
|
+
if (first.mute)
|
|
13382
|
+
notes.push(
|
|
13383
|
+
`${first.label}: --mute leaves nothing out (the first input's sound is the film's own)`
|
|
13384
|
+
);
|
|
13385
|
+
const pieces = ratedSegments8(doc);
|
|
13386
|
+
for (const o of doc.overlays ?? []) {
|
|
13387
|
+
if (!o.anchor) continue;
|
|
13388
|
+
const r = resolveAnchor5(doc, o.anchor, pieces);
|
|
13389
|
+
if (r.start !== null) o.start = r.start;
|
|
13390
|
+
}
|
|
13391
|
+
return { doc, copies: [...copies.values()], media: joined, actions, notes };
|
|
13392
|
+
}
|
|
13393
|
+
var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set(["json", "help", "force", "captions"]);
|
|
13394
|
+
var MULTI = /* @__PURE__ */ new Set(["mute"]);
|
|
13395
|
+
var USAGE2 = "vos join <take> <take|file.mp4>\u2026 --out <dir> [--force] [--captions] [--mute <file>]... [--json]";
|
|
13396
|
+
var within = (inner, outer) => {
|
|
13397
|
+
const rel = relative4(outer, inner);
|
|
13398
|
+
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute3(rel);
|
|
13399
|
+
};
|
|
13400
|
+
async function readPart(arg) {
|
|
13401
|
+
const path = resolve15(arg);
|
|
13402
|
+
if (!existsSync22(path)) throw new UsageError(`${arg}: no such take or file`);
|
|
13403
|
+
if (statSync7(path).isDirectory()) {
|
|
13404
|
+
if (!existsSync22(join24(path, "meta.json")))
|
|
13405
|
+
throw new UsageError(
|
|
13406
|
+
`${arg}: not a take directory (no meta.json); a video file is joined by its path`
|
|
13407
|
+
);
|
|
13408
|
+
const take = await loadTake(path);
|
|
13409
|
+
const doc2 = take.doc ?? projectFromArtifact2(
|
|
13410
|
+
{
|
|
13411
|
+
videoKey: take.paths.recordingName,
|
|
13412
|
+
cursor: take.cursor,
|
|
13413
|
+
meta: take.meta
|
|
13414
|
+
},
|
|
13415
|
+
take.paths.recordingName
|
|
13416
|
+
).doc;
|
|
13417
|
+
return { label: arg, kind: "take", doc: doc2, dir: path, actions: take.actions };
|
|
13418
|
+
}
|
|
13419
|
+
const probed = await probeVideoFile(path);
|
|
13420
|
+
if (!(probed.durationMs > 0))
|
|
13421
|
+
throw new UsageError(`${arg}: the file reports no duration`);
|
|
13422
|
+
const doc = fileDoc(
|
|
13423
|
+
`recording.${probed.ext}`,
|
|
13424
|
+
probed,
|
|
13425
|
+
Math.round(statSync7(path).mtimeMs)
|
|
13426
|
+
);
|
|
13427
|
+
return {
|
|
13428
|
+
label: arg,
|
|
13429
|
+
kind: "file",
|
|
13430
|
+
doc,
|
|
13431
|
+
dir: dirname9(path),
|
|
13432
|
+
actions: null,
|
|
13433
|
+
file: { path, ext: probed.ext, hasAudio: probed.hasAudio }
|
|
13434
|
+
};
|
|
13435
|
+
}
|
|
13436
|
+
async function cmdJoin(argv) {
|
|
13437
|
+
const { positionals, flags, multi } = parseArgs(argv, BOOLEAN_FLAGS12, MULTI);
|
|
13438
|
+
const r = createReporter(flags.json === true);
|
|
13439
|
+
const outArg = strFlag(flags, "out");
|
|
13440
|
+
const force = flags.force === true;
|
|
13441
|
+
const withCaptions = flags.captions === true;
|
|
13442
|
+
if (positionals.length < 2 || !outArg) throw new UsageError(USAGE2);
|
|
13443
|
+
const out = resolve15(outArg);
|
|
13444
|
+
const inputs = positionals.map((p) => resolve15(p));
|
|
13445
|
+
const muted = /* @__PURE__ */ new Set();
|
|
13446
|
+
for (const m of multi.mute ?? []) {
|
|
13447
|
+
const i = inputs.indexOf(resolve15(m));
|
|
13448
|
+
if (i < 0)
|
|
13449
|
+
throw new UsageError(
|
|
13450
|
+
`--mute ${m}: not one of the inputs; name a file as it is given to vos join`
|
|
13451
|
+
);
|
|
13452
|
+
muted.add(i);
|
|
13453
|
+
}
|
|
13454
|
+
for (const [i, input] of inputs.entries()) {
|
|
13455
|
+
if (within(out, input) || within(input, out))
|
|
13456
|
+
throw new UsageError(
|
|
13457
|
+
`--out ${outArg} overlaps the input ${positionals[i]}; write the joined take somewhere of its own`
|
|
13458
|
+
);
|
|
13459
|
+
}
|
|
13460
|
+
if (existsSync22(out)) {
|
|
13461
|
+
if (!force)
|
|
13462
|
+
throw new UsageError(
|
|
13463
|
+
`--out ${outArg} exists; pass --force to replace it, or name a new directory`
|
|
13464
|
+
);
|
|
13465
|
+
const isTake = existsSync22(join24(out, "doc.json")) || existsSync22(join24(out, "meta.json"));
|
|
13466
|
+
if (!isTake && readdirSync5(out).length > 0)
|
|
13467
|
+
throw new UsageError(
|
|
13468
|
+
`--force replaces a take directory, and ${outArg} holds other files; name a new directory`
|
|
13469
|
+
);
|
|
13470
|
+
if (within(process.cwd(), out))
|
|
13471
|
+
throw new UsageError(
|
|
13472
|
+
`--out ${outArg} is the working directory or above it; name a new directory`
|
|
13473
|
+
);
|
|
13474
|
+
}
|
|
13475
|
+
const parts = [];
|
|
13476
|
+
for (const [i, p] of positionals.entries()) {
|
|
13477
|
+
r.log(`reading ${p}\u2026`);
|
|
13478
|
+
parts.push({
|
|
13479
|
+
...await readPart(p),
|
|
13480
|
+
...muted.has(i) ? { mute: true } : {}
|
|
13481
|
+
});
|
|
13482
|
+
}
|
|
13483
|
+
const plan = joinDocs(parts);
|
|
13484
|
+
let captions = null;
|
|
13485
|
+
if (withCaptions) {
|
|
13486
|
+
captions = landStepCaptions(plan.doc, plan.actions);
|
|
13487
|
+
const labelOf = new Map([
|
|
13488
|
+
["", positionals[0]],
|
|
13489
|
+
...plan.media.map((m) => [m.id, m.label])
|
|
13490
|
+
]);
|
|
13491
|
+
for (const k of captions.uncaptioned)
|
|
13492
|
+
plan.notes.push(
|
|
13493
|
+
`no step in ${labelOf.get(k) ?? `media ${k}`} carries a caption`
|
|
13494
|
+
);
|
|
13495
|
+
}
|
|
13496
|
+
const lint = lintDoc(plan.doc);
|
|
13497
|
+
if (lint.problems.length) {
|
|
13498
|
+
r.log(
|
|
13499
|
+
`the joined document does not validate:
|
|
13500
|
+
${lint.problems.join("\n ")}`
|
|
13501
|
+
);
|
|
13502
|
+
return EXIT_ERROR;
|
|
13503
|
+
}
|
|
13504
|
+
for (const c of plan.copies) {
|
|
13505
|
+
if (!c.base) continue;
|
|
13506
|
+
const found = localFile(relative4(c.base, c.from), c.base);
|
|
13507
|
+
if ("refused" in found)
|
|
13508
|
+
throw new Error(
|
|
13509
|
+
`${relative4(process.cwd(), c.from) || c.from}, which an input's doc.json names, ${found.refused}; bring its media home first (vos fetch --media) or fix the key`
|
|
13510
|
+
);
|
|
13511
|
+
c.from = found.file;
|
|
13512
|
+
}
|
|
13513
|
+
if (existsSync22(out)) await rm3(out, { recursive: true, force: true });
|
|
13514
|
+
await mkdir10(out, { recursive: true });
|
|
13515
|
+
for (const c of plan.copies) {
|
|
13516
|
+
const to = join24(out, c.to);
|
|
13517
|
+
await mkdir10(dirname9(to), { recursive: true });
|
|
13518
|
+
if (c.remux) {
|
|
13519
|
+
const remuxed = await copyRecording(c.from, to, c.remux);
|
|
13520
|
+
if (!remuxed && c.remux !== "mov")
|
|
13521
|
+
plan.notes.push(
|
|
13522
|
+
`could not read ${basename11(c.from)}'s audio in Node, so it was copied as it is; its length comes from the container`
|
|
13523
|
+
);
|
|
13524
|
+
} else await copyFile2(c.from, to);
|
|
13525
|
+
}
|
|
13526
|
+
const doc = plan.doc;
|
|
13527
|
+
await writeJson(join24(out, "meta.json"), doc.source.meta, true);
|
|
13528
|
+
if (doc.source.cursor.length)
|
|
13529
|
+
await writeJson(join24(out, "cursor.json"), doc.source.cursor);
|
|
13530
|
+
if (parts[0].actions)
|
|
13531
|
+
await writeJson(join24(out, "actions.json"), parts[0].actions, true);
|
|
13532
|
+
await writeJson(join24(out, "doc.json"), doc, true);
|
|
13533
|
+
const extents = segmentOutputExtents3(doc);
|
|
13534
|
+
const length = extents.length ? extents[extents.length - 1].end : 0;
|
|
13535
|
+
for (const n of plan.notes) r.log(`note: ${n}`);
|
|
13536
|
+
for (const w of lint.warnings) r.log(`warning: ${w}`);
|
|
13537
|
+
const captionLine = captions ? `
|
|
13538
|
+
${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} landed` + (captions.skipped.length ? `; not landed: ${captions.skipped.join("; ")}` : "") : "";
|
|
13539
|
+
r.done(
|
|
13540
|
+
{
|
|
13541
|
+
out,
|
|
13542
|
+
primary: positionals[0],
|
|
13543
|
+
media: plan.media,
|
|
13544
|
+
segments: doc.segments.length,
|
|
13545
|
+
seconds: round32(length),
|
|
13546
|
+
files: plan.copies.map((c) => c.to),
|
|
13547
|
+
notes: plan.notes,
|
|
13548
|
+
warnings: lint.warnings,
|
|
13549
|
+
...captions ? { captions } : {}
|
|
13550
|
+
},
|
|
13551
|
+
`Joined ${parts.length} inputs \u2192 ${out} (${length.toFixed(1)}s, ${doc.segments.length} segments)
|
|
13552
|
+
${positionals[0]} is the primary; ${plan.media.map((m) => `${m.label} \u2192 ${m.id}`).join(", ")}${captionLine}
|
|
13553
|
+
Next: vos frames ${outArg} --times 0,25%,50%,75%,100%; vos render ${outArg}`
|
|
13554
|
+
);
|
|
13555
|
+
return EXIT_OK;
|
|
13556
|
+
}
|
|
13557
|
+
|
|
13558
|
+
// src/plugin/score.ts
|
|
13559
|
+
import { existsSync as existsSync23 } from "fs";
|
|
13560
|
+
import {
|
|
13561
|
+
copyFile as copyFile3,
|
|
13562
|
+
mkdir as mkdir11,
|
|
13563
|
+
mkdtemp,
|
|
13564
|
+
readFile as readFile14,
|
|
13565
|
+
rm as rm4,
|
|
13566
|
+
writeFile as writeFile11
|
|
13567
|
+
} from "fs/promises";
|
|
13568
|
+
import { tmpdir } from "os";
|
|
13569
|
+
import { basename as basename12, isAbsolute as isAbsolute4, join as join25, relative as relative5, resolve as resolve16, sep as sep3 } from "path";
|
|
13570
|
+
import {
|
|
13571
|
+
docMediaList as docMediaList2,
|
|
13572
|
+
docOutputDuration as docOutputDuration3,
|
|
13573
|
+
findStep as findStep3,
|
|
13574
|
+
ratedSegments as ratedSegments9,
|
|
13575
|
+
resolveAnchor as resolveAnchor6
|
|
13576
|
+
} from "@vosjs/studio-core";
|
|
13577
|
+
var SCORE_ID = "score";
|
|
13578
|
+
var MID_SONG_FADE_IN = 0.8;
|
|
13579
|
+
var BLOCK_SECONDS = 0.1;
|
|
13580
|
+
var WINDOW_SECONDS = 1;
|
|
13581
|
+
var HOP_SECONDS = 0.5;
|
|
13582
|
+
var FLOOR_DB = -70;
|
|
13583
|
+
function kWeighting(sampleRate) {
|
|
13584
|
+
const shelf = (() => {
|
|
13585
|
+
const f0 = 1681.974450955533;
|
|
13586
|
+
const G = 3.999843853973347;
|
|
13587
|
+
const Q = 0.7071752369554196;
|
|
13588
|
+
const K = Math.tan(Math.PI * f0 / sampleRate);
|
|
13589
|
+
const Vh = Math.pow(10, G / 20);
|
|
13590
|
+
const Vb = Math.pow(Vh, 0.4996667741545416);
|
|
13591
|
+
const a0 = 1 + K / Q + K * K;
|
|
13592
|
+
return {
|
|
13593
|
+
b0: (Vh + Vb * K / Q + K * K) / a0,
|
|
13594
|
+
b1: 2 * (K * K - Vh) / a0,
|
|
13595
|
+
b2: (Vh - Vb * K / Q + K * K) / a0,
|
|
13596
|
+
a1: 2 * (K * K - 1) / a0,
|
|
13597
|
+
a2: (1 - K / Q + K * K) / a0
|
|
13598
|
+
};
|
|
13599
|
+
})();
|
|
13600
|
+
const highPass = (() => {
|
|
13601
|
+
const f0 = 38.13547087602444;
|
|
13602
|
+
const Q = 0.5003270373238773;
|
|
13603
|
+
const K = Math.tan(Math.PI * f0 / sampleRate);
|
|
13604
|
+
const a0 = 1 + K / Q + K * K;
|
|
13605
|
+
return {
|
|
13606
|
+
b0: 1,
|
|
13607
|
+
b1: -2,
|
|
13608
|
+
b2: 1,
|
|
13609
|
+
a1: 2 * (K * K - 1) / a0,
|
|
13610
|
+
a2: (1 - K / Q + K * K) / a0
|
|
13611
|
+
};
|
|
13612
|
+
})();
|
|
13613
|
+
return [shelf, highPass];
|
|
13614
|
+
}
|
|
13615
|
+
var LoudnessMeter = class {
|
|
13616
|
+
constructor(sampleRate, channels) {
|
|
13617
|
+
this.sampleRate = sampleRate;
|
|
13618
|
+
this.channels = channels;
|
|
13619
|
+
this.blockLen = Math.max(1, Math.round(sampleRate * BLOCK_SECONDS));
|
|
13620
|
+
const weighted = channels >= 1 && channels <= 2 && sampleRate >= 8e3;
|
|
13621
|
+
this.unit = weighted ? "LUFS" : "dBFS";
|
|
13622
|
+
this.filters = weighted ? kWeighting(sampleRate) : null;
|
|
13623
|
+
this.state = Array.from({ length: channels }, () => [
|
|
13624
|
+
[0, 0, 0, 0],
|
|
13625
|
+
[0, 0, 0, 0]
|
|
13626
|
+
]);
|
|
13627
|
+
this.acc = new Array(channels).fill(0);
|
|
13628
|
+
}
|
|
13629
|
+
sampleRate;
|
|
13630
|
+
channels;
|
|
13631
|
+
unit;
|
|
13632
|
+
blockLen;
|
|
13633
|
+
filters;
|
|
13634
|
+
/** Per channel, per stage: [x1, x2, y1, y2]. */
|
|
13635
|
+
state;
|
|
13636
|
+
acc;
|
|
13637
|
+
filled = 0;
|
|
13638
|
+
blocks = [];
|
|
13639
|
+
/** One chunk: a Float32Array per channel, all the same length. */
|
|
13640
|
+
push(planes) {
|
|
13641
|
+
const n = planes[0]?.length ?? 0;
|
|
13642
|
+
for (let i = 0; i < n; i++) {
|
|
13643
|
+
for (let c = 0; c < this.channels; c++) {
|
|
13644
|
+
let v = planes[c]?.[i] ?? 0;
|
|
13645
|
+
if (this.filters) {
|
|
13646
|
+
for (let k = 0; k < 2; k++) {
|
|
13647
|
+
const f = this.filters[k];
|
|
13648
|
+
const s = this.state[c][k];
|
|
13649
|
+
const y = f.b0 * v + f.b1 * s[0] + f.b2 * s[1] - f.a1 * s[2] - f.a2 * s[3];
|
|
13650
|
+
s[1] = s[0];
|
|
13651
|
+
s[0] = v;
|
|
13652
|
+
s[3] = s[2];
|
|
13653
|
+
s[2] = y;
|
|
13654
|
+
v = y;
|
|
13655
|
+
}
|
|
13656
|
+
}
|
|
13657
|
+
this.acc[c] += v * v;
|
|
13658
|
+
}
|
|
13659
|
+
if (++this.filled === this.blockLen) this.flush();
|
|
13660
|
+
}
|
|
13661
|
+
}
|
|
13662
|
+
flush() {
|
|
13663
|
+
let power = 0;
|
|
13664
|
+
for (let c = 0; c < this.channels; c++) {
|
|
13665
|
+
power += this.acc[c] / this.filled;
|
|
13666
|
+
this.acc[c] = 0;
|
|
13667
|
+
}
|
|
13668
|
+
if (this.unit === "dBFS") power /= Math.max(1, this.channels);
|
|
13669
|
+
this.blocks.push(power);
|
|
13670
|
+
this.filled = 0;
|
|
13671
|
+
}
|
|
13672
|
+
/** The blocks, the last partial one included. */
|
|
13673
|
+
finish() {
|
|
13674
|
+
if (this.filled > this.blockLen / 2) this.flush();
|
|
13675
|
+
return this.blocks;
|
|
13676
|
+
}
|
|
13677
|
+
};
|
|
13678
|
+
function levelCurve(blocks, unit) {
|
|
13679
|
+
const per = Math.round(WINDOW_SECONDS / BLOCK_SECONDS);
|
|
13680
|
+
const hop = Math.round(HOP_SECONDS / BLOCK_SECONDS);
|
|
13681
|
+
const out = [];
|
|
13682
|
+
for (let i = 0; i + per <= blocks.length; i += hop) {
|
|
13683
|
+
let sum = 0;
|
|
13684
|
+
for (let j = i; j < i + per; j++) sum += blocks[j];
|
|
13685
|
+
const mean = sum / per;
|
|
13686
|
+
const db = mean > 0 ? (unit === "LUFS" ? -0.691 : 0) + 10 * Math.log10(mean) : -Infinity;
|
|
13687
|
+
out.push({
|
|
13688
|
+
t: +((i + per / 2) * BLOCK_SECONDS).toFixed(3),
|
|
13689
|
+
level: +Math.max(FLOOR_DB, db).toFixed(2)
|
|
13690
|
+
});
|
|
13691
|
+
}
|
|
13692
|
+
return out;
|
|
13693
|
+
}
|
|
13694
|
+
var SWELL_LOOKBACK = 15;
|
|
13695
|
+
var SWELL_SUSTAIN = 1.5;
|
|
13696
|
+
var SWELL_HOLD_DB = 3;
|
|
13697
|
+
var SWELL_ARRIVE_DB = 1;
|
|
13698
|
+
var SWELL_MIN_RISE = 3;
|
|
13699
|
+
var SWELL_FLOOR_DB = 12;
|
|
13700
|
+
var SWELL_SETTLE = 5;
|
|
13701
|
+
function findSwell(curve) {
|
|
13702
|
+
const step = HOP_SECONDS;
|
|
13703
|
+
const back = Math.round(SWELL_LOOKBACK / step);
|
|
13704
|
+
const hold = Math.round(SWELL_SUSTAIN / step);
|
|
13705
|
+
const floor = median2(curve.map((p) => p.level)) - SWELL_FLOOR_DB;
|
|
13706
|
+
const begun = curve.findIndex((p) => p.level >= floor);
|
|
13707
|
+
if (begun < 0) return null;
|
|
13708
|
+
const settled = begun + Math.round(SWELL_SETTLE / step);
|
|
13709
|
+
const levelAt = (j) => Math.max(curve[j].level, floor);
|
|
13710
|
+
let best = null;
|
|
13711
|
+
for (let i = settled + 1; i < curve.length; i++) {
|
|
13712
|
+
const p = curve[i];
|
|
13713
|
+
let held = true;
|
|
13714
|
+
for (let k = i + 1; k <= i + hold; k++) {
|
|
13715
|
+
if (k >= curve.length || curve[k].level < p.level - SWELL_HOLD_DB) {
|
|
13716
|
+
held = false;
|
|
13717
|
+
break;
|
|
13718
|
+
}
|
|
13719
|
+
}
|
|
13720
|
+
if (!held) continue;
|
|
13721
|
+
let lo = i;
|
|
13722
|
+
for (let j = Math.max(settled, i - back); j < i; j++)
|
|
13723
|
+
if (levelAt(j) < levelAt(lo)) lo = j;
|
|
13724
|
+
const rise = p.level - levelAt(lo);
|
|
13725
|
+
if (rise < SWELL_MIN_RISE) continue;
|
|
13726
|
+
if (i + 1 < curve.length && curve[i + 1].level > p.level) continue;
|
|
13727
|
+
if (best && rise <= best.rise + 1e-9) continue;
|
|
13728
|
+
let arrive = i;
|
|
13729
|
+
for (let k = lo; k <= i; k++) {
|
|
13730
|
+
if (curve[k].level >= p.level - SWELL_ARRIVE_DB) {
|
|
13731
|
+
arrive = k;
|
|
13732
|
+
break;
|
|
13733
|
+
}
|
|
13734
|
+
}
|
|
13735
|
+
best = {
|
|
13736
|
+
at: curve[arrive].t,
|
|
13737
|
+
from: curve[lo].t,
|
|
13738
|
+
peak: p.t,
|
|
13739
|
+
rise: +rise.toFixed(2),
|
|
13740
|
+
peakLevel: p.level
|
|
13741
|
+
};
|
|
13742
|
+
}
|
|
13743
|
+
return best;
|
|
13744
|
+
}
|
|
13745
|
+
var median2 = (xs) => {
|
|
13746
|
+
if (!xs.length) return FLOOR_DB;
|
|
13747
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
13748
|
+
const m = Math.floor(s.length / 2);
|
|
13749
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
13750
|
+
};
|
|
13751
|
+
function firstAboveMedian(curve) {
|
|
13752
|
+
const m = median2(curve.map((p) => p.level));
|
|
13753
|
+
const hit = curve.find((p) => p.level > m);
|
|
13754
|
+
return hit ? Math.max(0, +(hit.t - WINDOW_SECONDS / 2).toFixed(3)) : 0;
|
|
13755
|
+
}
|
|
13756
|
+
var r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
13757
|
+
function planScore(input) {
|
|
13758
|
+
const { trackDuration, film, swell } = input;
|
|
13759
|
+
const notes = [];
|
|
13760
|
+
let start;
|
|
13761
|
+
if (input.at !== null) {
|
|
13762
|
+
if (!swell) {
|
|
13763
|
+
notes.push(
|
|
13764
|
+
"the track has no swell (no sustained rise of 3 dB or more); it starts at its first rise instead"
|
|
13765
|
+
);
|
|
13766
|
+
start = firstAboveMedian(input.curve);
|
|
13767
|
+
} else {
|
|
13768
|
+
start = swell.at - input.at;
|
|
13769
|
+
if (start < 0) {
|
|
13770
|
+
notes.push(
|
|
13771
|
+
`the swell (${swell.at.toFixed(1)}s into the track) comes before ${input.at.toFixed(1)}s of film can play ahead of it; the track starts at its top and the swell lands at ${swell.at.toFixed(1)}s`
|
|
13772
|
+
);
|
|
13773
|
+
start = 0;
|
|
13774
|
+
}
|
|
13775
|
+
}
|
|
13776
|
+
} else {
|
|
13777
|
+
start = firstAboveMedian(input.curve);
|
|
13778
|
+
}
|
|
13779
|
+
if (trackDuration < film) {
|
|
13780
|
+
notes.push(
|
|
13781
|
+
`the track (${trackDuration.toFixed(1)}s) is shorter than the film (${film.toFixed(1)}s); it cannot cover it, so it starts at its top and ends early`
|
|
13782
|
+
);
|
|
13783
|
+
start = 0;
|
|
13784
|
+
} else if (start + film > trackDuration) {
|
|
13785
|
+
const fit = Math.max(0, trackDuration - film);
|
|
13786
|
+
notes.push(
|
|
13787
|
+
`from ${start.toFixed(1)}s the track ends before the film does; it starts at ${fit.toFixed(1)}s instead, so the film is covered${swell && input.at !== null ? ` and the swell lands at ${(swell.at - fit).toFixed(1)}s` : ""}`
|
|
13788
|
+
);
|
|
13789
|
+
start = fit;
|
|
13790
|
+
}
|
|
13791
|
+
const out = Math.min(trackDuration, start + film);
|
|
13792
|
+
const swellAt = swell && swell.at >= start && swell.at <= out ? r3(swell.at - start) : null;
|
|
13793
|
+
return { in: r3(start), out: r3(out), swellAt, notes };
|
|
13794
|
+
}
|
|
13795
|
+
function sparkline(curve, width = 64) {
|
|
13796
|
+
if (!curve.length) return "";
|
|
13797
|
+
const glyphs = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
13798
|
+
const step = Math.max(1, Math.ceil(curve.length / width));
|
|
13799
|
+
const pts = [];
|
|
13800
|
+
for (let i = 0; i < curve.length; i += step) {
|
|
13801
|
+
let m = -Infinity;
|
|
13802
|
+
for (let j = i; j < Math.min(curve.length, i + step); j++)
|
|
13803
|
+
m = Math.max(m, curve[j].level);
|
|
13804
|
+
pts.push(m);
|
|
13805
|
+
}
|
|
13806
|
+
const hi = Math.max(...pts);
|
|
13807
|
+
const lo = Math.max(Math.min(...pts), hi - 24);
|
|
13808
|
+
return pts.map(
|
|
13809
|
+
(v) => glyphs[hi > lo ? Math.max(
|
|
13810
|
+
0,
|
|
13811
|
+
Math.min(
|
|
13812
|
+
glyphs.length - 1,
|
|
13813
|
+
Math.floor((v - lo) / (hi - lo) * glyphs.length)
|
|
13814
|
+
)
|
|
13815
|
+
) : 0]
|
|
13816
|
+
).join("");
|
|
13817
|
+
}
|
|
13818
|
+
async function measureInNode(path) {
|
|
13819
|
+
const MB = await import("mediabunny");
|
|
13820
|
+
const input = new MB.Input({
|
|
13821
|
+
formats: MB.ALL_FORMATS,
|
|
13822
|
+
source: new MB.FilePathSource(path)
|
|
13823
|
+
});
|
|
13824
|
+
try {
|
|
13825
|
+
const track = await input.getPrimaryAudioTrack();
|
|
13826
|
+
if (!track) throw new UsageError(`${path} has no audio track`);
|
|
13827
|
+
if (!await track.canDecode()) return null;
|
|
13828
|
+
const meter = new LoudnessMeter(track.sampleRate, track.numberOfChannels);
|
|
13829
|
+
const sink = new MB.AudioSampleSink(track);
|
|
13830
|
+
let frames = 0;
|
|
13831
|
+
for await (const sample2 of sink.samples()) {
|
|
13832
|
+
const planes = [];
|
|
13833
|
+
for (let c = 0; c < track.numberOfChannels; c++) {
|
|
13834
|
+
const a = new Float32Array(sample2.numberOfFrames);
|
|
13835
|
+
sample2.copyTo(a, { planeIndex: c, format: "f32-planar" });
|
|
13836
|
+
planes.push(a);
|
|
13837
|
+
}
|
|
13838
|
+
frames += sample2.numberOfFrames;
|
|
13839
|
+
sample2.close();
|
|
13840
|
+
meter.push(planes);
|
|
13841
|
+
}
|
|
13842
|
+
return {
|
|
13843
|
+
sampleRate: track.sampleRate,
|
|
13844
|
+
channels: track.numberOfChannels,
|
|
13845
|
+
duration: frames / track.sampleRate,
|
|
13846
|
+
unit: meter.unit,
|
|
13847
|
+
blocks: meter.finish(),
|
|
13848
|
+
via: "node"
|
|
13849
|
+
};
|
|
13850
|
+
} finally {
|
|
13851
|
+
input.dispose();
|
|
13852
|
+
}
|
|
13853
|
+
}
|
|
13854
|
+
var PAGE_CHUNK_SECONDS = 10;
|
|
13855
|
+
async function measureInBrowser(path) {
|
|
13856
|
+
const bytes = await readFile14(path);
|
|
13857
|
+
const browser = await launchBrowser();
|
|
13858
|
+
try {
|
|
13859
|
+
const page = await browser.newPage();
|
|
13860
|
+
const info = await page.evaluate(`(async () => {
|
|
13861
|
+
const b64 = ${JSON.stringify(bytes.toString("base64"))}
|
|
13862
|
+
const bin = atob(b64)
|
|
13863
|
+
const u8 = new Uint8Array(bin.length)
|
|
13864
|
+
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i)
|
|
13865
|
+
const ctx = new OfflineAudioContext(1, 1, 48000)
|
|
13866
|
+
const buf = await ctx.decodeAudioData(u8.buffer)
|
|
13867
|
+
window.__vosScore = buf
|
|
13868
|
+
return { sampleRate: buf.sampleRate, channels: buf.numberOfChannels, length: buf.length }
|
|
13869
|
+
})()`);
|
|
13870
|
+
const meter = new LoudnessMeter(info.sampleRate, info.channels);
|
|
13871
|
+
const chunk = Math.round(info.sampleRate * PAGE_CHUNK_SECONDS);
|
|
13872
|
+
for (let from = 0; from < info.length; from += chunk) {
|
|
13873
|
+
const parts = await page.evaluate(`(() => {
|
|
13874
|
+
const buf = window.__vosScore
|
|
13875
|
+
const out = []
|
|
13876
|
+
for (let c = 0; c < buf.numberOfChannels; c++) {
|
|
13877
|
+
const a = buf.getChannelData(c).subarray(${from}, ${from + chunk})
|
|
13878
|
+
const u8 = new Uint8Array(a.buffer, a.byteOffset, a.byteLength)
|
|
13879
|
+
let s = ''
|
|
13880
|
+
for (let i = 0; i < u8.length; i += 0x8000)
|
|
13881
|
+
s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000))
|
|
13882
|
+
out.push(btoa(s))
|
|
13883
|
+
}
|
|
13884
|
+
return out
|
|
13885
|
+
})()`);
|
|
13886
|
+
meter.push(
|
|
13887
|
+
parts.map((p) => {
|
|
13888
|
+
const b = Buffer.from(p, "base64");
|
|
13889
|
+
return new Float32Array(b.buffer, b.byteOffset, b.byteLength / 4);
|
|
13890
|
+
})
|
|
13891
|
+
);
|
|
13892
|
+
}
|
|
13893
|
+
return {
|
|
13894
|
+
sampleRate: info.sampleRate,
|
|
13895
|
+
channels: info.channels,
|
|
13896
|
+
duration: info.length / info.sampleRate,
|
|
13897
|
+
unit: meter.unit,
|
|
13898
|
+
blocks: meter.finish(),
|
|
13899
|
+
via: "browser"
|
|
13900
|
+
};
|
|
13901
|
+
} finally {
|
|
13902
|
+
await browser.close();
|
|
13903
|
+
}
|
|
13904
|
+
}
|
|
13905
|
+
async function measureTrack(path) {
|
|
13906
|
+
return await measureInNode(path) ?? await measureInBrowser(path);
|
|
13907
|
+
}
|
|
13908
|
+
function resolveAt(doc, raw) {
|
|
13909
|
+
const trimmed = raw.trim();
|
|
13910
|
+
if (/^\d+(\.\d+)?s?$/.test(trimmed)) {
|
|
13911
|
+
const at2 = parseFloat(trimmed);
|
|
13912
|
+
return { at: at2, via: `${at2}s` };
|
|
13913
|
+
}
|
|
13914
|
+
const v = trimmed.startsWith("step:") ? trimmed.slice(5) : trimmed;
|
|
13915
|
+
const pieces = ratedSegments9(doc);
|
|
13916
|
+
const colon = v.indexOf(":");
|
|
13917
|
+
const named = colon > 0 ? { media: v.slice(0, colon), step: v.slice(colon + 1) } : null;
|
|
13918
|
+
const candidates = named ? [named] : docMediaList2(doc).filter(({ source }) => findStep3(source.meta.steps ?? [], v)).map(({ id }) => ({ media: id, step: v }));
|
|
13919
|
+
if (!candidates.length)
|
|
13920
|
+
throw new UsageError(
|
|
13921
|
+
`--at ${raw}: no step by that id or index in this take (and not a number of seconds)`
|
|
13922
|
+
);
|
|
13923
|
+
if (candidates.length > 1)
|
|
13924
|
+
throw new UsageError(
|
|
13925
|
+
`--at ${raw}: steps of several media carry it; say which, as <media>:<step> (${candidates.map((c2) => `${c2.media || "primary"}`).join(", ")})`
|
|
13926
|
+
);
|
|
13927
|
+
const c = candidates[0];
|
|
13928
|
+
const r = resolveAnchor6(
|
|
13929
|
+
doc,
|
|
13930
|
+
{ step: c.step, ...c.media ? { media: c.media } : {} },
|
|
13931
|
+
pieces
|
|
13932
|
+
);
|
|
13933
|
+
if (r.start === null)
|
|
13934
|
+
throw new UsageError(
|
|
13935
|
+
`--at ${raw}: that step ${r.miss === "cut" ? "is cut out of the film" : r.miss === "skipped" ? "was skipped by the recorder" : "is not in the take"}`
|
|
13936
|
+
);
|
|
13937
|
+
return { at: r.start, via: `step ${v} (${r.start.toFixed(2)}s)` };
|
|
13938
|
+
}
|
|
13939
|
+
async function resolveTrack(dir, ref, flags, dryRun) {
|
|
13940
|
+
const local = resolve16(ref);
|
|
13941
|
+
if (existsSync23(local)) {
|
|
13942
|
+
const rel = relative5(resolve16(dir), local);
|
|
13943
|
+
const inside = !!rel && !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute4(rel);
|
|
13944
|
+
const key = inside ? rel.split(sep3).join("/") : `music/${basename12(local)}`;
|
|
13945
|
+
if (!inside && !dryRun) {
|
|
13946
|
+
await mkdir11(join25(dir, "music"), { recursive: true });
|
|
13947
|
+
await copyFile3(local, join25(dir, key));
|
|
13948
|
+
}
|
|
13949
|
+
return { path: local, key, name: basename12(local) };
|
|
13950
|
+
}
|
|
13951
|
+
const origin = platformOrigin({
|
|
13952
|
+
origin: strFlag(flags, "origin"),
|
|
13953
|
+
api: strFlag(flags, "api")
|
|
13954
|
+
});
|
|
13955
|
+
const catalog = await fetchMusicCatalog(origin);
|
|
13956
|
+
const track = catalog.tracks.find((t) => t.slug === ref);
|
|
13957
|
+
if (!track)
|
|
13958
|
+
throw new UsageError(
|
|
13959
|
+
`--track ${ref}: no such file, and no catalog track by that slug (some: ${catalog.tracks.slice(0, 6).map((t) => t.slug).join(", ")}; the list is GET ${origin}/api/music)`
|
|
13960
|
+
);
|
|
13961
|
+
const res = await fetch(track.url);
|
|
13962
|
+
if (!res.ok) throw new Error(`GET ${track.url} answered ${res.status}`);
|
|
13963
|
+
const tmp = await mkdtemp(join25(tmpdir(), "vos-score-"));
|
|
13964
|
+
const path = join25(tmp, basename12(new URL(track.url).pathname) || "track");
|
|
13965
|
+
await writeFile11(path, new Uint8Array(await res.arrayBuffer()));
|
|
13966
|
+
return {
|
|
13967
|
+
path,
|
|
13968
|
+
key: track.url,
|
|
13969
|
+
name: track.title,
|
|
13970
|
+
cleanup: () => rm4(tmp, { recursive: true, force: true })
|
|
13971
|
+
};
|
|
13972
|
+
}
|
|
13973
|
+
var BOOLEAN_FLAGS13 = /* @__PURE__ */ new Set(["json", "help", "dry-run"]);
|
|
13974
|
+
var USAGE3 = "vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]";
|
|
13975
|
+
async function cmdScore(argv) {
|
|
13976
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS13);
|
|
13977
|
+
const dir = positionals[0];
|
|
13978
|
+
const trackRef = strFlag(flags, "track");
|
|
13979
|
+
if (!dir || !trackRef) throw new UsageError(USAGE3);
|
|
13980
|
+
const r = createReporter(flags.json === true);
|
|
13981
|
+
const dryRun = flags["dry-run"] === true;
|
|
13982
|
+
const gain = numFlag(flags, "gain", 0.5);
|
|
13983
|
+
if (!(gain >= 0 && gain <= 1))
|
|
13984
|
+
throw new UsageError("--gain expects a number from 0 to 1");
|
|
13985
|
+
const fadeOut = numFlag(flags, "fade-out", 1.5);
|
|
13986
|
+
if (!(fadeOut >= 0)) throw new UsageError("--fade-out expects seconds \u2265 0");
|
|
13987
|
+
const fadeInFlag = strFlag(flags, "fade-in");
|
|
13988
|
+
const fadeInAsked = numFlag(flags, "fade-in", MID_SONG_FADE_IN);
|
|
13989
|
+
if (!(fadeInAsked >= 0)) throw new UsageError("--fade-in expects seconds \u2265 0");
|
|
13990
|
+
const take = await loadTake(dir);
|
|
13991
|
+
if (!take.doc)
|
|
13992
|
+
throw new UsageError(`${dir} has no doc.json; run vos plan first`);
|
|
13993
|
+
const doc = take.doc;
|
|
13994
|
+
const atRaw = strFlag(flags, "at");
|
|
13995
|
+
const at2 = atRaw !== void 0 ? resolveAt(doc, atRaw) : null;
|
|
13996
|
+
const track = await resolveTrack(dir, trackRef, flags, dryRun);
|
|
13997
|
+
let measured;
|
|
13998
|
+
try {
|
|
13999
|
+
r.log(`measuring ${track.name}\u2026`);
|
|
14000
|
+
measured = await measureTrack(track.path);
|
|
14001
|
+
} finally {
|
|
14002
|
+
await track.cleanup?.();
|
|
14003
|
+
}
|
|
14004
|
+
const curve = levelCurve(measured.blocks, measured.unit);
|
|
14005
|
+
const swell = findSwell(curve);
|
|
14006
|
+
const film = docOutputDuration3(doc);
|
|
14007
|
+
const plan = planScore({
|
|
14008
|
+
trackDuration: measured.duration,
|
|
14009
|
+
film,
|
|
14010
|
+
curve,
|
|
14011
|
+
swell,
|
|
14012
|
+
at: at2 ? at2.at : null
|
|
14013
|
+
});
|
|
14014
|
+
const clip = {
|
|
14015
|
+
id: SCORE_ID,
|
|
14016
|
+
key: track.key,
|
|
14017
|
+
name: track.name,
|
|
14018
|
+
start: 0,
|
|
14019
|
+
in: plan.in,
|
|
14020
|
+
out: plan.out,
|
|
14021
|
+
duration: r3(measured.duration),
|
|
14022
|
+
gain,
|
|
14023
|
+
// A track entered mid-song opens on a fade, never a cut into the bar;
|
|
14024
|
+
// one that starts at its own top needs none unless asked.
|
|
14025
|
+
fadeIn: Math.min(
|
|
14026
|
+
fadeInFlag !== void 0 || plan.in > 0 ? fadeInAsked : 0,
|
|
14027
|
+
Math.max(0, plan.out - plan.in)
|
|
14028
|
+
),
|
|
14029
|
+
fadeOut: Math.min(fadeOut, Math.max(0, plan.out - plan.in)),
|
|
14030
|
+
...doc.source.micKey ? { duck: true } : {}
|
|
14031
|
+
};
|
|
14032
|
+
const levels = curve.map((p) => p.level);
|
|
14033
|
+
const envelope = {
|
|
14034
|
+
unit: measured.unit,
|
|
14035
|
+
weighted: measured.unit === "LUFS",
|
|
14036
|
+
decoder: measured.via,
|
|
14037
|
+
sampleRate: measured.sampleRate,
|
|
14038
|
+
channels: measured.channels,
|
|
14039
|
+
min: levels.length ? Math.min(...levels) : null,
|
|
14040
|
+
median: levels.length ? median2(levels) : null,
|
|
14041
|
+
max: levels.length ? Math.max(...levels) : null,
|
|
14042
|
+
points: curve
|
|
14043
|
+
};
|
|
14044
|
+
const others = doc.audio.filter((a) => a.id !== SCORE_ID && a.start < 0.05);
|
|
14045
|
+
if (others.length)
|
|
14046
|
+
plan.notes.push(
|
|
14047
|
+
`${others.map((a) => a.id).join(", ")} also play${others.length === 1 ? "s" : ""} from the top; two tracks under one film rarely mean to`
|
|
14048
|
+
);
|
|
14049
|
+
if (!dryRun) {
|
|
14050
|
+
doc.audio = [...doc.audio.filter((a) => a.id !== SCORE_ID), clip];
|
|
14051
|
+
await writeJson(take.paths.doc, doc, true);
|
|
14052
|
+
}
|
|
14053
|
+
const label = measured.unit === "LUFS" ? "LUFS, K-weighted" : "dBFS RMS";
|
|
14054
|
+
const swellLine = swell ? `swell: +${swell.rise.toFixed(1)} dB from ${swell.from.toFixed(1)}s, arriving ${swell.at.toFixed(1)}s into the track (peak ${swell.peakLevel.toFixed(1)} ${measured.unit} at ${swell.peak.toFixed(1)}s)` : "swell: none (no sustained rise of 3 dB or more)";
|
|
14055
|
+
const placeLine = at2 && plan.swellAt !== null ? `the track starts at ${plan.in.toFixed(2)}s, so the swell lands at ${plan.swellAt.toFixed(2)}s of film (asked: ${at2.via})` : `the track starts at ${plan.in.toFixed(2)}s${at2 ? ` (asked: ${at2.via})` : ", its first rise above its median level"}`;
|
|
14056
|
+
r.done(
|
|
14057
|
+
{
|
|
14058
|
+
take: resolve16(dir),
|
|
14059
|
+
dryRun,
|
|
14060
|
+
track: { key: track.key, name: track.name, duration: clip.duration },
|
|
14061
|
+
film: r3(film),
|
|
14062
|
+
swell,
|
|
14063
|
+
at: at2?.at ?? null,
|
|
14064
|
+
clip,
|
|
14065
|
+
notes: plan.notes,
|
|
14066
|
+
envelope
|
|
14067
|
+
},
|
|
14068
|
+
`${dryRun ? "Would score" : "Scored"} ${dir} with ${track.name} (${measured.duration.toFixed(1)}s, ${label})
|
|
14069
|
+
${sparkline(curve)}
|
|
14070
|
+
${swellLine}
|
|
14071
|
+
${placeLine}; it plays ${plan.in.toFixed(2)}..${plan.out.toFixed(2)} under the ${film.toFixed(1)}s film at gain ${gain}${plan.notes.length ? `
|
|
14072
|
+
note: ${plan.notes.join("\n note: ")}` : ""}${dryRun ? "\n (dry run: doc.json untouched)" : `
|
|
14073
|
+
audio clip "${SCORE_ID}" ${doc.audio.length > 1 ? "written beside the others" : "written"} in doc.json`}`
|
|
14074
|
+
);
|
|
14075
|
+
return EXIT_OK;
|
|
14076
|
+
}
|
|
14077
|
+
|
|
14078
|
+
// src/plugin/look.ts
|
|
14079
|
+
import { mkdir as mkdir12 } from "fs/promises";
|
|
14080
|
+
import { existsSync as existsSync24 } from "fs";
|
|
14081
|
+
import { join as join26, resolve as resolve17 } from "path";
|
|
14082
|
+
import { headersFor, urlSchemeProblem } from "@vosjs/render-core/record";
|
|
14083
|
+
function quote(value) {
|
|
14084
|
+
return `"${value.replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
14085
|
+
}
|
|
14086
|
+
var NAME_MAX = 80;
|
|
14087
|
+
function scopeSelector(s, kind) {
|
|
14088
|
+
if (s.testId) return `[${s.testId.attr}=${quote(s.testId.value)}]`;
|
|
14089
|
+
let base;
|
|
14090
|
+
if (s.role) base = `[role=${quote(s.role)}]`;
|
|
14091
|
+
else if (kind === "dialog" && s.tag === "dialog") base = "dialog[open]";
|
|
14092
|
+
else if (kind === "dialog") base = '[aria-modal="true"]';
|
|
14093
|
+
else base = s.tag;
|
|
14094
|
+
return s.label ? `${base}[aria-label=${quote(s.label)}]` : base;
|
|
14095
|
+
}
|
|
14096
|
+
function stableId(id) {
|
|
14097
|
+
if (!id || !/^[A-Za-z][\w-]*$/.test(id)) return false;
|
|
14098
|
+
if (/^(radix|headlessui|mui|react|ember|ext|rc|el|v)-/i.test(id)) return false;
|
|
14099
|
+
return !/\d{2,}/.test(id);
|
|
14100
|
+
}
|
|
14101
|
+
function selectorCandidates(c) {
|
|
14102
|
+
const out = [];
|
|
14103
|
+
const add = (selector, basis) => {
|
|
14104
|
+
if (!out.some((x) => x.selector === selector)) out.push({ selector, basis });
|
|
14105
|
+
};
|
|
14106
|
+
const dialog = c.dialog ? scopeSelector(c.dialog, "dialog") : null;
|
|
14107
|
+
const landmark = c.landmark ? scopeSelector(c.landmark, "landmark") : null;
|
|
14108
|
+
const css = (sel) => dialog ? `${dialog} ${sel}` : sel;
|
|
14109
|
+
const chain = (sel) => dialog ? `${dialog} >> ${sel}` : sel;
|
|
14110
|
+
if (c.testId) {
|
|
14111
|
+
const sel = `[${c.testId.attr}=${quote(c.testId.value)}]`;
|
|
14112
|
+
add(sel, "testid");
|
|
14113
|
+
if (dialog) add(`${dialog} ${sel}`, "testid");
|
|
14114
|
+
}
|
|
14115
|
+
if (c.ariaLabel && c.ariaLabel.length <= NAME_MAX)
|
|
14116
|
+
add(css(`[aria-label=${quote(c.ariaLabel)}]`), "aria-label");
|
|
14117
|
+
if (c.tag === "a" && c.hrefAttr && c.hrefAttr.length <= 200 && !/^\s*javascript:/i.test(c.hrefAttr) && c.hrefAttr.trim() !== "#")
|
|
14118
|
+
add(css(`a[href=${quote(c.hrefAttr)}]`), "href");
|
|
14119
|
+
if (c.role && c.name && c.name.length <= NAME_MAX) {
|
|
14120
|
+
const sel = `role=${c.role}[name=${quote(c.name)}]`;
|
|
14121
|
+
add(chain(sel), "role");
|
|
14122
|
+
if (!dialog && landmark) add(`${landmark} >> ${sel}`, "role");
|
|
14123
|
+
}
|
|
14124
|
+
const form = /^(input|textarea|select)$/.test(c.tag);
|
|
14125
|
+
if (form && c.nameAttr)
|
|
14126
|
+
add(css(`${c.tag}[name=${quote(c.nameAttr)}]`), "form");
|
|
14127
|
+
if (form && c.placeholder && c.placeholder.length <= NAME_MAX)
|
|
14128
|
+
add(css(`${c.tag}[placeholder=${quote(c.placeholder)}]`), "form");
|
|
14129
|
+
if (stableId(c.id)) add(`#${c.id}`, "id");
|
|
14130
|
+
if (!form && c.text && c.text.length <= 60) {
|
|
14131
|
+
const sel = `text=${quote(c.text)}`;
|
|
14132
|
+
if (dialog) add(`${dialog} >> ${sel}`, "text");
|
|
14133
|
+
else if (landmark) add(`${landmark} >> ${sel}`, "text");
|
|
14134
|
+
else add(sel, "text");
|
|
14135
|
+
}
|
|
14136
|
+
return out;
|
|
14137
|
+
}
|
|
14138
|
+
function judgeMatch(index, reached) {
|
|
14139
|
+
if (!reached.includes(String(index))) return "miss";
|
|
14140
|
+
return reached.length === 1 ? "unique" : "ambiguous";
|
|
14141
|
+
}
|
|
14142
|
+
function chooseProposal(index, candidates, reached) {
|
|
14143
|
+
let fallback = null;
|
|
14144
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
14145
|
+
const verdict = judgeMatch(index, reached[i] ?? []);
|
|
14146
|
+
if (verdict === "unique")
|
|
14147
|
+
return {
|
|
14148
|
+
selector: candidates[i].selector,
|
|
14149
|
+
basis: candidates[i].basis,
|
|
14150
|
+
match: "unique",
|
|
14151
|
+
matches: 1
|
|
14152
|
+
};
|
|
14153
|
+
if (verdict === "ambiguous" && !fallback)
|
|
14154
|
+
fallback = { c: candidates[i], n: reached[i].length };
|
|
14155
|
+
}
|
|
14156
|
+
if (fallback)
|
|
14157
|
+
return {
|
|
14158
|
+
selector: fallback.c.selector,
|
|
14159
|
+
basis: fallback.c.basis,
|
|
14160
|
+
match: "ambiguous",
|
|
14161
|
+
matches: fallback.n
|
|
14162
|
+
};
|
|
14163
|
+
return { selector: null, basis: null, match: "none", matches: 0 };
|
|
14164
|
+
}
|
|
14165
|
+
var SPEND_STEMS = [
|
|
14166
|
+
"delet",
|
|
14167
|
+
"remov",
|
|
14168
|
+
"revok",
|
|
14169
|
+
"eras",
|
|
14170
|
+
"discard",
|
|
14171
|
+
"publish",
|
|
14172
|
+
"deploy",
|
|
14173
|
+
"purchas",
|
|
14174
|
+
"subscri",
|
|
14175
|
+
"unsubscri",
|
|
14176
|
+
"pay",
|
|
14177
|
+
"buy",
|
|
14178
|
+
"generat",
|
|
14179
|
+
"submit",
|
|
14180
|
+
"send",
|
|
14181
|
+
"transfer",
|
|
14182
|
+
"upgrad",
|
|
14183
|
+
"confirm",
|
|
14184
|
+
"leav",
|
|
14185
|
+
"destroy"
|
|
14186
|
+
];
|
|
14187
|
+
var SPEND_PHRASES = [
|
|
14188
|
+
"move to trash",
|
|
14189
|
+
"empty trash",
|
|
14190
|
+
"cancel subscription",
|
|
14191
|
+
"cancel plan",
|
|
14192
|
+
"close account"
|
|
14193
|
+
];
|
|
14194
|
+
var SPEND = new RegExp(
|
|
14195
|
+
`\\b(?:${SPEND_PHRASES.map((p) => p.replace(/ /g, "\\s+")).join("|")}|(?:${SPEND_STEMS.join("|")})[a-z]*)`,
|
|
14196
|
+
"i"
|
|
14197
|
+
);
|
|
14198
|
+
function spendWord(words2) {
|
|
14199
|
+
for (const w of words2) {
|
|
14200
|
+
const m = w ? SPEND.exec(w.replace(/\s+/g, " ")) : null;
|
|
14201
|
+
if (m) return m[0].toLowerCase();
|
|
14202
|
+
}
|
|
14203
|
+
return null;
|
|
14204
|
+
}
|
|
14205
|
+
function refusalLine(selector, label, word) {
|
|
14206
|
+
return `refused: --click ${selector} reaches "${label}": "${word}" spends, sends or destroys, and vos look never presses one. Look at the page before it; a flow that needs the press has it as a step in actions.json, for vos record`;
|
|
14207
|
+
}
|
|
14208
|
+
var ENGINES = /^(css|xpath|text|role|id|nth|visible|data-testid|data-test-id|data-test|internal:[a-z-]+)$/;
|
|
14209
|
+
function splitTypeFlag(value) {
|
|
14210
|
+
let depth = 0;
|
|
14211
|
+
let inQuote = null;
|
|
14212
|
+
let segment = 0;
|
|
14213
|
+
for (let i = 0; i < value.length; i++) {
|
|
14214
|
+
const ch = value[i];
|
|
14215
|
+
if (inQuote) {
|
|
14216
|
+
if (ch === "\\") i++;
|
|
14217
|
+
else if (ch === inQuote) inQuote = null;
|
|
14218
|
+
continue;
|
|
14219
|
+
}
|
|
14220
|
+
if (ch === '"' || ch === "'") inQuote = ch;
|
|
14221
|
+
else if (ch === "[" || ch === "(") depth++;
|
|
14222
|
+
else if ((ch === "]" || ch === ")") && depth > 0) depth--;
|
|
14223
|
+
else if (ch === ">" && value[i + 1] === ">") {
|
|
14224
|
+
segment = i + 2;
|
|
14225
|
+
i++;
|
|
14226
|
+
} else if (ch === "=" && depth === 0) {
|
|
14227
|
+
if (ENGINES.test(value.slice(segment, i).trim())) continue;
|
|
14228
|
+
const selector = value.slice(0, i).trim();
|
|
14229
|
+
if (!selector) break;
|
|
14230
|
+
return { selector, text: value.slice(i + 1) };
|
|
14231
|
+
}
|
|
14232
|
+
}
|
|
14233
|
+
throw new UsageError(
|
|
14234
|
+
`--type expects <selector>=<text>, got "${value}" (the text follows the first = outside the selector's brackets and quotes)`
|
|
14235
|
+
);
|
|
14236
|
+
}
|
|
14237
|
+
function lookSteps(argv) {
|
|
14238
|
+
const steps = [];
|
|
14239
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14240
|
+
const arg = argv[i];
|
|
14241
|
+
if (arg === "--") break;
|
|
14242
|
+
const m = /^--(click|type)(?:=(.*))?$/s.exec(arg);
|
|
14243
|
+
if (!m) continue;
|
|
14244
|
+
let value = m[2];
|
|
14245
|
+
if (value === void 0) {
|
|
14246
|
+
value = argv[i + 1];
|
|
14247
|
+
if (value === void 0 || value.startsWith("--"))
|
|
14248
|
+
throw new UsageError(`--${m[1]} expects a value`);
|
|
14249
|
+
i++;
|
|
14250
|
+
}
|
|
14251
|
+
if (m[1] === "click") {
|
|
14252
|
+
if (!value.trim()) throw new UsageError("--click expects a selector");
|
|
14253
|
+
steps.push({ do: "click", selector: value });
|
|
14254
|
+
} else steps.push({ do: "type", ...splitTypeFlag(value) });
|
|
14255
|
+
}
|
|
14256
|
+
return steps;
|
|
14257
|
+
}
|
|
14258
|
+
function parseViewport2(value) {
|
|
14259
|
+
if (!value) return { width: 1280, height: 720 };
|
|
14260
|
+
const m = /^(\d+)x(\d+)$/i.exec(value);
|
|
14261
|
+
if (!m) throw new UsageError("--viewport expects WxH, like 1280x720");
|
|
14262
|
+
const width = Number(m[1]);
|
|
14263
|
+
const height = Number(m[2]);
|
|
14264
|
+
if (width < 200 || height < 200 || width > 3840 || height > 2160)
|
|
14265
|
+
throw new UsageError("--viewport wants a size from 200x200 to 3840x2160");
|
|
14266
|
+
return { width, height };
|
|
14267
|
+
}
|
|
14268
|
+
function displayName(c) {
|
|
14269
|
+
const words2 = c.name || c.text || c.placeholder || "";
|
|
14270
|
+
return words2.length > 60 ? `${words2.slice(0, 59).trimEnd()}\u2026` : words2;
|
|
14271
|
+
}
|
|
14272
|
+
function controlLine(c) {
|
|
14273
|
+
const what = c.role && c.role !== c.tag ? `${c.tag} (${c.role})` : c.tag;
|
|
14274
|
+
const name = c.redacted?.length ? ` [${c.redacted.join(", ")} withheld]` : c.name ? ` ${JSON.stringify(c.name)}` : "";
|
|
14275
|
+
const flags = [
|
|
14276
|
+
c.inDialog ? "in dialog" : "",
|
|
14277
|
+
c.disabled ? "disabled" : "",
|
|
14278
|
+
c.covered ? "covered" : ""
|
|
14279
|
+
].filter(Boolean);
|
|
14280
|
+
const proposal = c.selector ? `${c.selector} ${c.match === "unique" ? "UNIQUE" : `AMBIGUOUS (${c.matches})`}` : "no stable selector (use its rect)";
|
|
14281
|
+
const r = c.rect;
|
|
14282
|
+
return ` ${what}${name}${flags.length ? ` [${flags.join(", ")}]` : ""}
|
|
14283
|
+
${proposal} @ ${r.x},${r.y} ${r.w}\xD7${r.h}`;
|
|
14284
|
+
}
|
|
14285
|
+
var CONTROL_LIMIT = 150;
|
|
14286
|
+
var LOOK_PROBE = `(() => {
|
|
14287
|
+
const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
|
|
14288
|
+
const vw = innerWidth, vh = innerHeight
|
|
14289
|
+
const CONTROLS = 'button, a, [role=button], [role=tab], [role=menuitem], [role=menuitemcheckbox], [role=menuitemradio], [role=checkbox], [role=switch], [role=option], [role=radio], [role=link], input, textarea, select, [contenteditable], [data-testid], [data-test-id], [data-test], [data-cy]'
|
|
14290
|
+
const TEST_IDS = ['data-testid', 'data-test-id', 'data-test', 'data-cy']
|
|
14291
|
+
const DIALOGS = 'dialog[open], [role=dialog], [role=alertdialog], [aria-modal=true]'
|
|
14292
|
+
const LANDMARKS = 'nav, header, main, aside, footer, [role=navigation], [role=banner], [role=main], [role=complementary], [role=contentinfo], [role=search]'
|
|
14293
|
+
const squash = (s) => (s || '').replace(/\\s+/g, ' ').trim()
|
|
14294
|
+
const textOf = (el) => squash(typeof el.innerText === 'string' ? el.innerText : el.textContent)
|
|
14295
|
+
const shown = (el) => {
|
|
14296
|
+
const r = el.getBoundingClientRect()
|
|
14297
|
+
if (r.width < 2 || r.height < 2) return null
|
|
14298
|
+
if (el.checkVisibility && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return null
|
|
14299
|
+
const s = getComputedStyle(el)
|
|
14300
|
+
if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
|
|
14301
|
+
return r
|
|
14302
|
+
}
|
|
14303
|
+
const testIdOf = (el) => {
|
|
14304
|
+
for (const attr of TEST_IDS) { const value = el.getAttribute(attr); if (value) return { attr, value } }
|
|
14305
|
+
return null
|
|
14306
|
+
}
|
|
14307
|
+
const implicitRole = (el) => {
|
|
14308
|
+
const t = el.tagName
|
|
14309
|
+
if (t === 'BUTTON') return 'button'
|
|
14310
|
+
if (t === 'A') return el.hasAttribute('href') ? 'link' : ''
|
|
14311
|
+
if (t === 'TEXTAREA') return 'textbox'
|
|
14312
|
+
if (t === 'SELECT') return el.multiple || el.size > 1 ? 'listbox' : 'combobox'
|
|
14313
|
+
if (t === 'INPUT') {
|
|
14314
|
+
const type = (el.getAttribute('type') || 'text').toLowerCase()
|
|
14315
|
+
if (type === 'checkbox' || type === 'radio') return type
|
|
14316
|
+
if (type === 'range') return 'slider'
|
|
14317
|
+
if (type === 'number') return 'spinbutton'
|
|
14318
|
+
if (/^(button|submit|reset|image)$/.test(type)) return 'button'
|
|
14319
|
+
if (/^(text|email|tel|url|search)$/.test(type)) return el.hasAttribute('list') ? 'combobox' : type === 'search' ? 'searchbox' : 'textbox'
|
|
14320
|
+
return ''
|
|
14321
|
+
}
|
|
14322
|
+
if (el.isContentEditable && el.hasAttribute('contenteditable')) return 'textbox'
|
|
14323
|
+
return ''
|
|
14324
|
+
}
|
|
14325
|
+
const roleOf = (el) => (el.getAttribute('role') || '').trim().split(/\\s+/)[0] || implicitRole(el)
|
|
14326
|
+
const nameOf = (el) => {
|
|
14327
|
+
const aria = squash(el.getAttribute('aria-label'))
|
|
14328
|
+
if (aria) return aria
|
|
14329
|
+
const by = el.getAttribute('aria-labelledby')
|
|
14330
|
+
if (by) {
|
|
14331
|
+
const t = squash(by.split(/\\s+/).map((id) => document.getElementById(id)).filter(Boolean).map(textOf).join(' '))
|
|
14332
|
+
if (t) return t
|
|
14333
|
+
}
|
|
14334
|
+
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) {
|
|
14335
|
+
const type = (el.getAttribute('type') || '').toLowerCase()
|
|
14336
|
+
if (/^(button|submit|reset)$/.test(type)) return squash(el.value) || (type === 'submit' ? 'Submit' : type === 'reset' ? 'Reset' : '')
|
|
14337
|
+
if (type === 'image') return squash(el.getAttribute('alt'))
|
|
14338
|
+
const label = el.labels ? squash([...el.labels].map(textOf).join(' ')) : ''
|
|
14339
|
+
return label || squash(el.getAttribute('title')) || squash(el.getAttribute('placeholder'))
|
|
14340
|
+
}
|
|
14341
|
+
const text = textOf(el)
|
|
14342
|
+
if (text) return text
|
|
14343
|
+
const img = el.querySelector('img[alt]')
|
|
14344
|
+
return squash(img && img.getAttribute('alt')) || squash(el.getAttribute('title'))
|
|
14345
|
+
}
|
|
14346
|
+
const scopeOf = (el) => el ? {
|
|
14347
|
+
tag: el.tagName.toLowerCase(),
|
|
14348
|
+
role: (el.getAttribute('role') || '').trim().split(/\\s+/)[0] || '',
|
|
14349
|
+
label: squash(el.getAttribute('aria-label')) || null,
|
|
14350
|
+
testId: testIdOf(el),
|
|
14351
|
+
} : null
|
|
14352
|
+
const hrefOf = (el) => {
|
|
14353
|
+
if (el.tagName !== 'A' || !el.hasAttribute('href')) return null
|
|
14354
|
+
try { const u = new URL(el.href, location.href); const at = u.pathname + u.search; return u.origin === location.origin ? at : u.origin + at } catch { return null }
|
|
14355
|
+
}
|
|
14356
|
+
const out = []
|
|
14357
|
+
let offscreen = 0, more = 0
|
|
14358
|
+
for (const el of document.querySelectorAll(CONTROLS)) {
|
|
14359
|
+
if (el.tagName === 'INPUT' && (el.getAttribute('type') || '').toLowerCase() === 'hidden') continue
|
|
14360
|
+
if (el.getAttribute('contenteditable') === 'false') continue
|
|
14361
|
+
if (el.tagName === 'A' && !el.hasAttribute('href') && !el.getAttribute('role') && !testIdOf(el)) continue
|
|
14362
|
+
const r = shown(el)
|
|
14363
|
+
if (!r) continue
|
|
14364
|
+
if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) { offscreen++; continue }
|
|
14365
|
+
if (out.length >= ${CONTROL_LIMIT}) { more++; continue }
|
|
14366
|
+
const dlg = el.closest(DIALOGS)
|
|
14367
|
+
const lm = el.closest(LANDMARKS)
|
|
14368
|
+
let name = nameOf(el), text = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) ? '' : textOf(el)
|
|
14369
|
+
let ariaLabel = squash(el.getAttribute('aria-label')) || null
|
|
14370
|
+
let placeholder = squash(el.getAttribute('placeholder')) || null
|
|
14371
|
+
let hrefAttr = el.tagName === 'A' ? el.getAttribute('href') : null, href = hrefOf(el)
|
|
14372
|
+
const kinds = [...new Set([...kindsIn(name), ...kindsIn(text), ...kindsIn(ariaLabel || ''), ...kindsIn(placeholder || ''), ...kindsIn(hrefAttr || '')])]
|
|
14373
|
+
if (kinds.length) { name = ''; text = ''; ariaLabel = null; placeholder = null; hrefAttr = null; href = null }
|
|
14374
|
+
const cx = Math.min(vw - 1, Math.max(0, r.left + r.width / 2)), cy = Math.min(vh - 1, Math.max(0, r.top + r.height / 2))
|
|
14375
|
+
const hit = document.elementFromPoint(cx, cy)
|
|
14376
|
+
const covered = !!hit && !(hit === el || el.contains(hit) || (el.labels && [...el.labels].some((l) => l.contains(hit))))
|
|
14377
|
+
const index = out.length
|
|
14378
|
+
out.push({
|
|
14379
|
+
index,
|
|
14380
|
+
tag: el.tagName.toLowerCase(),
|
|
14381
|
+
role: roleOf(el),
|
|
14382
|
+
name,
|
|
14383
|
+
text,
|
|
14384
|
+
ariaLabel,
|
|
14385
|
+
testId: testIdOf(el),
|
|
14386
|
+
id: el.id || null,
|
|
14387
|
+
nameAttr: /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) ? el.getAttribute('name') || null : null,
|
|
14388
|
+
placeholder,
|
|
14389
|
+
href,
|
|
14390
|
+
hrefAttr,
|
|
14391
|
+
rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) },
|
|
14392
|
+
disabled: el.matches(':disabled') || el.getAttribute('aria-disabled') === 'true',
|
|
14393
|
+
covered,
|
|
14394
|
+
dialog: dlg && shown(dlg) ? scopeOf(dlg) : null,
|
|
14395
|
+
landmark: lm ? scopeOf(lm) : null,
|
|
14396
|
+
...(kinds.length ? { redacted: kinds } : {}),
|
|
14397
|
+
})
|
|
14398
|
+
el.setAttribute('data-vos-look', String(index))
|
|
14399
|
+
}
|
|
14400
|
+
return { controls: out, offscreen, more }
|
|
14401
|
+
})()`;
|
|
14402
|
+
var UNSTAMP = `(() => { for (const el of document.querySelectorAll('[data-vos-look]')) el.removeAttribute('data-vos-look') })()`;
|
|
14403
|
+
function settleScript(quietMs, capMs) {
|
|
14404
|
+
return `new Promise((resolve) => {
|
|
14405
|
+
let last = performance.now()
|
|
14406
|
+
const start = last
|
|
14407
|
+
const mo = new MutationObserver(() => { last = performance.now() })
|
|
14408
|
+
mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true })
|
|
14409
|
+
const tick = () => {
|
|
14410
|
+
const now = performance.now()
|
|
14411
|
+
if (now - last >= ${quietMs} || now - start >= ${capMs}) {
|
|
14412
|
+
mo.disconnect()
|
|
14413
|
+
resolve(now - last >= ${quietMs})
|
|
14414
|
+
} else setTimeout(tick, 50)
|
|
14415
|
+
}
|
|
14416
|
+
tick()
|
|
14417
|
+
})`;
|
|
14418
|
+
}
|
|
14419
|
+
var QUIET_MS = 800;
|
|
14420
|
+
var SETTLE_CAP_MS = 6e3;
|
|
14421
|
+
var STEP_TIMEOUT_MS = 8e3;
|
|
14422
|
+
async function settle(page) {
|
|
14423
|
+
const started = Date.now();
|
|
14424
|
+
for (let tries = 0; tries < 3; tries++) {
|
|
14425
|
+
await page.waitForLoadState("load", { timeout: 15e3 }).catch(() => {
|
|
14426
|
+
});
|
|
14427
|
+
const left = Math.max(QUIET_MS, SETTLE_CAP_MS - (Date.now() - started));
|
|
14428
|
+
try {
|
|
14429
|
+
return await page.evaluate(settleScript(QUIET_MS, left));
|
|
14430
|
+
} catch {
|
|
14431
|
+
}
|
|
14432
|
+
}
|
|
14433
|
+
return false;
|
|
14434
|
+
}
|
|
14435
|
+
var LookRefusal = class extends Error {
|
|
14436
|
+
constructor(message) {
|
|
14437
|
+
super(message);
|
|
14438
|
+
this.name = "LookRefusal";
|
|
14439
|
+
}
|
|
14440
|
+
};
|
|
14441
|
+
var place = (u) => {
|
|
14442
|
+
try {
|
|
14443
|
+
const x = new URL(u);
|
|
14444
|
+
return `${x.origin}${x.pathname}`;
|
|
14445
|
+
} catch {
|
|
14446
|
+
return u;
|
|
14447
|
+
}
|
|
14448
|
+
};
|
|
14449
|
+
function shownUrl(u) {
|
|
14450
|
+
try {
|
|
14451
|
+
const x = new URL(u);
|
|
14452
|
+
if (!x.username && !x.password) return u;
|
|
14453
|
+
x.username = "";
|
|
14454
|
+
x.password = "";
|
|
14455
|
+
return x.href;
|
|
14456
|
+
} catch {
|
|
14457
|
+
return u;
|
|
14458
|
+
}
|
|
14459
|
+
}
|
|
14460
|
+
async function lookAt(page, url, opts) {
|
|
14461
|
+
const log = opts.log ?? (() => {
|
|
14462
|
+
});
|
|
14463
|
+
log(`goto ${shownUrl(url)}`);
|
|
14464
|
+
const response = await page.goto(url, { waitUntil: "load", timeout: 45e3 }).catch((e) => {
|
|
14465
|
+
if (e instanceof Error && /net::ERR_/.test(e.message))
|
|
14466
|
+
throw new Error(
|
|
14467
|
+
`could not open ${shownUrl(url)}: ${(e.message.split("\n")[0] ?? "").split(url).join(shownUrl(url))}`
|
|
14468
|
+
);
|
|
14469
|
+
log(" (the load event never fired; looking anyway)");
|
|
14470
|
+
return null;
|
|
14471
|
+
});
|
|
14472
|
+
let settled = await settle(page);
|
|
14473
|
+
const seen = await page.evaluate(WALL_PROBE).catch(() => null);
|
|
14474
|
+
const wall = wallVerdict({
|
|
14475
|
+
askedUrl: url,
|
|
14476
|
+
landedUrl: page.url(),
|
|
14477
|
+
...response ? { status: response.status() } : {},
|
|
14478
|
+
passwordFields: seen?.passwordFields ?? 0,
|
|
14479
|
+
newPasswordField: seen?.newPasswordField ?? false,
|
|
14480
|
+
oneTimeCodeField: seen?.oneTimeCodeField ?? false
|
|
14481
|
+
});
|
|
14482
|
+
const ran = [];
|
|
14483
|
+
if (!wall || wall.level === "soft") {
|
|
14484
|
+
for (const step2 of opts.steps ?? []) {
|
|
14485
|
+
const all = page.locator(step2.selector);
|
|
14486
|
+
const target = all.first();
|
|
14487
|
+
try {
|
|
14488
|
+
await target.waitFor({ state: "visible", timeout: STEP_TIMEOUT_MS });
|
|
14489
|
+
} catch {
|
|
14490
|
+
throw new UsageError(
|
|
14491
|
+
`--${step2.do} ${step2.selector}: no visible element matches it after ${STEP_TIMEOUT_MS / 1e3} s. Look without the step and take a selector from the list`
|
|
14492
|
+
);
|
|
14493
|
+
}
|
|
14494
|
+
const count = await all.count();
|
|
14495
|
+
const note = count > 1 ? `the first of ${count} matches` : void 0;
|
|
14496
|
+
if (step2.do === "click") {
|
|
14497
|
+
const words2 = await target.evaluate(
|
|
14498
|
+
(el) => [
|
|
14499
|
+
el.getAttribute("aria-label"),
|
|
14500
|
+
el.innerText || el.textContent,
|
|
14501
|
+
el.value,
|
|
14502
|
+
el.getAttribute("title"),
|
|
14503
|
+
el.getAttribute("alt")
|
|
14504
|
+
].map(
|
|
14505
|
+
(s) => typeof s === "string" ? s.replace(/\s+/g, " ").trim() : ""
|
|
14506
|
+
)
|
|
14507
|
+
).catch(() => []);
|
|
14508
|
+
const word = spendWord(words2);
|
|
14509
|
+
if (word) {
|
|
14510
|
+
const label = words2.find((w) => spendWord([w])) ?? word;
|
|
14511
|
+
throw new LookRefusal(
|
|
14512
|
+
refusalLine(step2.selector, label.slice(0, 60), word)
|
|
14513
|
+
);
|
|
14514
|
+
}
|
|
14515
|
+
log(`click ${step2.selector}${note ? ` (${note})` : ""}`);
|
|
14516
|
+
await target.click({ timeout: STEP_TIMEOUT_MS });
|
|
14517
|
+
} else {
|
|
14518
|
+
log(
|
|
14519
|
+
`type ${step2.text.length} chars into ${step2.selector}${note ? ` (${note})` : ""}`
|
|
14520
|
+
);
|
|
14521
|
+
await target.fill(step2.text, { timeout: STEP_TIMEOUT_MS });
|
|
14522
|
+
}
|
|
14523
|
+
ran.push({
|
|
14524
|
+
do: step2.do,
|
|
14525
|
+
selector: step2.selector,
|
|
14526
|
+
...note ? { note } : {}
|
|
14527
|
+
});
|
|
14528
|
+
settled = await settle(page);
|
|
14529
|
+
}
|
|
14530
|
+
}
|
|
14531
|
+
if (opts.waitMs && opts.waitMs > 0) await page.waitForTimeout(opts.waitMs);
|
|
14532
|
+
await mkdir12(opts.outDir, { recursive: true });
|
|
14533
|
+
const screenshot = join26(opts.outDir, "look.png");
|
|
14534
|
+
await page.screenshot({ path: screenshot });
|
|
14535
|
+
const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
|
|
14536
|
+
const step = ran.length ? ran.length - 1 : -1;
|
|
14537
|
+
const exposures = (scan?.exposures ?? []).map((e) => ({
|
|
14538
|
+
step,
|
|
14539
|
+
...e,
|
|
14540
|
+
seen: 1
|
|
14541
|
+
}));
|
|
14542
|
+
const probe3 = await page.evaluate(LOOK_PROBE);
|
|
14543
|
+
const controls = [];
|
|
14544
|
+
try {
|
|
14545
|
+
for (const raw of probe3.controls) {
|
|
14546
|
+
const candidates = selectorCandidates(raw);
|
|
14547
|
+
const reached = [];
|
|
14548
|
+
for (const c of candidates) {
|
|
14549
|
+
const stamps = await page.locator(c.selector).evaluateAll(
|
|
14550
|
+
(els) => els.map((el) => el.getAttribute("data-vos-look"))
|
|
14551
|
+
).catch(() => []);
|
|
14552
|
+
reached.push(stamps);
|
|
14553
|
+
if (judgeMatch(raw.index, stamps) === "unique") break;
|
|
14554
|
+
}
|
|
14555
|
+
const proposal = chooseProposal(raw.index, candidates, reached);
|
|
14556
|
+
controls.push({
|
|
14557
|
+
tag: raw.tag,
|
|
14558
|
+
role: raw.role,
|
|
14559
|
+
name: displayName(raw),
|
|
14560
|
+
testId: raw.testId?.value ?? null,
|
|
14561
|
+
href: raw.href,
|
|
14562
|
+
rect: raw.rect,
|
|
14563
|
+
disabled: raw.disabled,
|
|
14564
|
+
inDialog: raw.dialog !== null,
|
|
14565
|
+
covered: raw.covered,
|
|
14566
|
+
...proposal,
|
|
14567
|
+
...raw.redacted ? { redacted: raw.redacted } : {}
|
|
14568
|
+
});
|
|
14569
|
+
}
|
|
14570
|
+
} finally {
|
|
14571
|
+
await page.evaluate(UNSTAMP).catch(() => {
|
|
14572
|
+
});
|
|
14573
|
+
}
|
|
14574
|
+
return {
|
|
14575
|
+
url: shownUrl(url),
|
|
14576
|
+
landed: place(page.url()),
|
|
14577
|
+
screenshot,
|
|
14578
|
+
viewport: page.viewportSize() ?? { width: 0, height: 0 },
|
|
14579
|
+
controls,
|
|
14580
|
+
offscreen: probe3.offscreen,
|
|
14581
|
+
more: probe3.more,
|
|
14582
|
+
exposures,
|
|
14583
|
+
wall,
|
|
14584
|
+
steps: ran,
|
|
14585
|
+
settled
|
|
14586
|
+
};
|
|
14587
|
+
}
|
|
14588
|
+
var USAGE4 = "vos look <url> [--session <name> | --storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--out <dir>] [--json]";
|
|
14589
|
+
function lookReport(r) {
|
|
14590
|
+
const inDialog = r.controls.filter((c) => c.inDialog).length;
|
|
14591
|
+
const head = `Looked at ${r.landed} (${r.viewport.width}\xD7${r.viewport.height}): ${r.screenshot}${r.settled ? "" : "\n note: the page never held still for 0.8 s; looked at it as it was"}`;
|
|
14592
|
+
const steps = r.steps.length ? `
|
|
14593
|
+
after ${r.steps.map((s) => `${s.do} ${s.selector}${s.note ? ` (${s.note})` : ""}`).join(", then ")}` : "";
|
|
14594
|
+
const wall = r.wall ? `
|
|
14595
|
+
${r.wall.level === "hard" ? "WALL" : "WARNING"}: ${r.wall.message}${r.wall.level === "hard" ? "\n The steps did not run; this is the page in front of the one asked for." : ""}` : "";
|
|
14596
|
+
const count = `
|
|
14597
|
+
${r.controls.length} control${r.controls.length === 1 ? "" : "s"} in view${inDialog ? ` (${inDialog} in an open dialog)` : ""}${r.offscreen ? `, ${r.offscreen} more outside it (scroll, or a taller --viewport)` : ""}${r.more ? `, ${r.more} past the listing's limit` : ""}:`;
|
|
14598
|
+
const list = r.controls.map(controlLine).join("\n");
|
|
14599
|
+
const exposed = r.exposures.length ? `
|
|
14600
|
+
EXPOSED in the frame (${r.exposures.length}):
|
|
14601
|
+
${r.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
|
|
14602
|
+
${EXPOSURE_ADVICE}` : "";
|
|
14603
|
+
const next = `
|
|
14604
|
+
Take a UNIQUE selector into actions.json as it is; an AMBIGUOUS one reaches several and the recorder takes the first. Then rehearse: vos record --actions actions.json --dry-run`;
|
|
14605
|
+
return `${head}${steps}${wall}${count}
|
|
14606
|
+
${list}${exposed}${next}`;
|
|
14607
|
+
}
|
|
14608
|
+
async function cmdLook(argv) {
|
|
14609
|
+
const steps = lookSteps(argv);
|
|
14610
|
+
const { positionals, flags, multi } = parseArgs(
|
|
14611
|
+
argv,
|
|
14612
|
+
/* @__PURE__ */ new Set(["json", "headed"]),
|
|
14613
|
+
/* @__PURE__ */ new Set(["click", "type", "header", "browser-arg"])
|
|
14614
|
+
);
|
|
14615
|
+
void multi.click;
|
|
14616
|
+
void multi.type;
|
|
14617
|
+
const url = positionals[0];
|
|
14618
|
+
if (!url) throw new UsageError(USAGE4);
|
|
14619
|
+
const scheme = urlSchemeProblem(url);
|
|
14620
|
+
if (scheme) throw new UsageError(scheme);
|
|
14621
|
+
const r = createReporter(flags.json === true);
|
|
14622
|
+
const viewport = parseViewport2(strFlag(flags, "viewport"));
|
|
14623
|
+
const outDir = resolve17(strFlag(flags, "out") ?? "look");
|
|
14624
|
+
const headed = flags.headed === true;
|
|
14625
|
+
const waitRaw = strFlag(flags, "wait");
|
|
14626
|
+
const waitMs = waitRaw === void 0 ? 0 : Number(waitRaw);
|
|
14627
|
+
if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 6e4)
|
|
14628
|
+
throw new UsageError("--wait expects milliseconds, 0 to 60000");
|
|
14629
|
+
const storageRaw = strFlag(flags, "storage-state");
|
|
14630
|
+
const storageState = storageRaw ? resolve17(storageRaw) : void 0;
|
|
14631
|
+
if (storageState && !existsSync24(storageState))
|
|
14632
|
+
throw new UsageError(`--storage-state: no such file: ${storageState}`);
|
|
14633
|
+
const sessionName = strFlag(flags, "session");
|
|
14634
|
+
if (sessionName) {
|
|
14635
|
+
const bad = sessionNameVerdict(sessionName);
|
|
14636
|
+
if (bad) throw new UsageError(bad);
|
|
14637
|
+
if (storageState)
|
|
14638
|
+
throw new UsageError(
|
|
14639
|
+
"--session and --storage-state are two doors to one page: a persistent profile cannot take a storage state. Pass one"
|
|
14640
|
+
);
|
|
14641
|
+
if (!existsSync24(sessionDir(sessionName)))
|
|
14642
|
+
throw new UsageError(
|
|
14643
|
+
`no session "${sessionName}". Make one: vos session open <url> --name ${sessionName} (a Chrome window opens; sign in and close it)`
|
|
14644
|
+
);
|
|
14645
|
+
}
|
|
14646
|
+
let headers;
|
|
14647
|
+
try {
|
|
14648
|
+
headers = parseHeaders(multi.header);
|
|
14649
|
+
} catch (e) {
|
|
14650
|
+
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
14651
|
+
}
|
|
14652
|
+
const browserArgs = (multi["browser-arg"] ?? []).filter(Boolean);
|
|
14653
|
+
const inContext = async (ctx) => {
|
|
14654
|
+
if (Object.keys(headers).length)
|
|
14655
|
+
await ctx.route("**/*", async (route) => {
|
|
14656
|
+
const req = route.request();
|
|
14657
|
+
const scoped = headersFor(req.url(), url, headers);
|
|
14658
|
+
await route.continue(
|
|
14659
|
+
scoped ? { headers: { ...req.headers(), ...scoped } } : void 0
|
|
14660
|
+
);
|
|
14661
|
+
});
|
|
14662
|
+
const page = ctx.pages()[0] ?? await ctx.newPage();
|
|
14663
|
+
await page.setViewportSize(viewport);
|
|
14664
|
+
return lookAt(page, url, { steps, waitMs, outDir, log: r.log });
|
|
14665
|
+
};
|
|
14666
|
+
let result;
|
|
14667
|
+
try {
|
|
14668
|
+
if (sessionName) {
|
|
14669
|
+
result = await withSession(sessionName, inContext, {
|
|
14670
|
+
headless: !headed,
|
|
14671
|
+
viewport
|
|
14672
|
+
});
|
|
14673
|
+
} else {
|
|
14674
|
+
const browser = await launchBrowser(browserArgs, { headless: !headed });
|
|
14675
|
+
try {
|
|
14676
|
+
const ctx = await browser.newContext({
|
|
14677
|
+
viewport,
|
|
14678
|
+
deviceScaleFactor: 1,
|
|
14679
|
+
...storageState ? { storageState } : {}
|
|
14680
|
+
});
|
|
14681
|
+
result = await inContext(ctx);
|
|
14682
|
+
} finally {
|
|
14683
|
+
await browser.close();
|
|
14684
|
+
}
|
|
14685
|
+
}
|
|
14686
|
+
} catch (e) {
|
|
14687
|
+
if (e instanceof LookRefusal) {
|
|
14688
|
+
r.done({ ok: false, refused: true, error: e.message }, e.message);
|
|
14689
|
+
return EXIT_USAGE;
|
|
14690
|
+
}
|
|
14691
|
+
throw e;
|
|
14692
|
+
}
|
|
14693
|
+
r.done(
|
|
14694
|
+
{ ok: !result.wall || result.wall.level === "soft", ...result },
|
|
14695
|
+
lookReport(result)
|
|
14696
|
+
);
|
|
14697
|
+
return result.wall?.level === "hard" ? EXIT_WALL : EXIT_OK;
|
|
14698
|
+
}
|
|
14699
|
+
|
|
14700
|
+
// src/plugin/machineCmd.ts
|
|
14701
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
14702
|
+
import {
|
|
14703
|
+
cpSync,
|
|
14704
|
+
existsSync as existsSync26,
|
|
14705
|
+
mkdirSync as mkdirSync6,
|
|
14706
|
+
readFileSync as readFileSync11,
|
|
14707
|
+
rmSync as rmSync3,
|
|
14708
|
+
writeFileSync as writeFileSync6
|
|
14709
|
+
} from "fs";
|
|
14710
|
+
import { homedir as homedir2 } from "os";
|
|
14711
|
+
import { join as join28 } from "path";
|
|
14712
|
+
import { chromium as chromium2 } from "playwright";
|
|
14713
|
+
|
|
14714
|
+
// src/plugin/machine.ts
|
|
14715
|
+
import { existsSync as existsSync25, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
14716
|
+
import { dirname as dirname10, join as join27 } from "path";
|
|
14717
|
+
var AGENT_NAMES = [
|
|
14718
|
+
"claude",
|
|
14719
|
+
"cursor",
|
|
14720
|
+
"codex",
|
|
14721
|
+
"copilot"
|
|
14722
|
+
];
|
|
14723
|
+
var PROJECT_DIRS = {
|
|
14724
|
+
claude: [".claude"],
|
|
14725
|
+
cursor: [".cursor"],
|
|
14726
|
+
codex: [".codex", ".agents"],
|
|
14727
|
+
copilot: [".github/copilot"]
|
|
14728
|
+
};
|
|
14729
|
+
var HOME_DIRS = {
|
|
14730
|
+
claude: [".claude"],
|
|
14731
|
+
cursor: [".cursor"],
|
|
14732
|
+
codex: [".codex", ".agents"],
|
|
14733
|
+
copilot: [".copilot"]
|
|
14734
|
+
};
|
|
14735
|
+
function detectAgents(root, opts = {}) {
|
|
14736
|
+
const exists = opts.exists ?? existsSync25;
|
|
14737
|
+
const out = [];
|
|
14738
|
+
for (const agent of AGENT_NAMES) {
|
|
14739
|
+
for (const rel of PROJECT_DIRS[agent]) {
|
|
14740
|
+
const dir = join27(root, rel);
|
|
14741
|
+
if (exists(dir)) out.push({ agent, dir, scope: "project" });
|
|
14742
|
+
}
|
|
14743
|
+
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join27(root, ".github", "copilot-instructions.md"))) {
|
|
14744
|
+
out.push({
|
|
14745
|
+
agent,
|
|
14746
|
+
dir: join27(root, ".github", "copilot"),
|
|
14747
|
+
scope: "project"
|
|
14748
|
+
});
|
|
14749
|
+
}
|
|
14750
|
+
}
|
|
14751
|
+
if (opts.global && opts.home) {
|
|
14752
|
+
for (const agent of AGENT_NAMES) {
|
|
14753
|
+
for (const rel of HOME_DIRS[agent]) {
|
|
14754
|
+
const dir = join27(opts.home, rel);
|
|
14755
|
+
if (exists(dir)) out.push({ agent, dir, scope: "global" });
|
|
14756
|
+
}
|
|
14757
|
+
}
|
|
14758
|
+
}
|
|
14759
|
+
return out;
|
|
14760
|
+
}
|
|
14761
|
+
function agentsFromFlag(value, root) {
|
|
14762
|
+
const wanted = value.trim() === "all" ? [...AGENT_NAMES] : value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
14763
|
+
const bad = wanted.filter((w) => !AGENT_NAMES.includes(w));
|
|
14764
|
+
if (bad.length)
|
|
14765
|
+
return {
|
|
14766
|
+
error: `--agent takes ${AGENT_NAMES.join(", ")} or all, not "${bad.join(", ")}"`
|
|
14767
|
+
};
|
|
14768
|
+
if (!wanted.length) return { error: "--agent expects a name" };
|
|
14769
|
+
return {
|
|
14770
|
+
homes: wanted.map((agent) => ({
|
|
14771
|
+
agent,
|
|
14772
|
+
dir: join27(root, PROJECT_DIRS[agent][0]),
|
|
14773
|
+
scope: "project"
|
|
14774
|
+
}))
|
|
14775
|
+
};
|
|
14776
|
+
}
|
|
14777
|
+
function skillsDirFor(home) {
|
|
14778
|
+
return join27(home.dir, "skills");
|
|
14779
|
+
}
|
|
14780
|
+
function fallbackSkillsHome(root) {
|
|
14781
|
+
return { agent: "codex", dir: join27(root, ".agents"), scope: "project" };
|
|
14782
|
+
}
|
|
14783
|
+
function installedSkills(dir, exists = existsSync25, list = (p) => {
|
|
14784
|
+
try {
|
|
14785
|
+
return readdirSync6(p);
|
|
14786
|
+
} catch {
|
|
14787
|
+
return [];
|
|
14788
|
+
}
|
|
14789
|
+
}) {
|
|
14790
|
+
if (!exists(dir)) return [];
|
|
14791
|
+
return list(dir).filter((name) => exists(join27(dir, name, "SKILL.md"))).sort();
|
|
14792
|
+
}
|
|
14793
|
+
var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
|
|
14794
|
+
var RULES_END = "<!-- vos:end -->";
|
|
14795
|
+
var RULES_BODY = `## Product videos (vosso)
|
|
14796
|
+
- Skill: product-video (npx skills add vosjs/skills) records the real product from actions.json
|
|
14797
|
+
- Look before you cut: vos frames take --at-zooms; edit take/doc.json and re-render, never re-record
|
|
14798
|
+
- Hand back: vos open take serves the take into the studio at vos.so, doc.json intact
|
|
14799
|
+
- Local by default: vos push only when asked for a link, the shelf or a cloud render
|
|
14800
|
+
- Deleting: vos delete moves a vos to Trash and vos restore brings it back; if you deleted by mistake, restore it and say so. Only the person can delete forever, at vos.so/app/trash
|
|
14801
|
+
- Credentials: VOS_API_KEY, then ~/.config/vos/credentials, then vos login; never print one
|
|
14802
|
+
- Reference: https://vos.so/llms-full.txt`;
|
|
14803
|
+
var RULES_BLOCK = `${RULES_BEGIN}
|
|
14804
|
+
${RULES_BODY}
|
|
14805
|
+
${RULES_END}`;
|
|
14806
|
+
function applyRulesBlock(existing) {
|
|
14807
|
+
const current = existing ?? "";
|
|
14808
|
+
const begin = current.indexOf(RULES_BEGIN);
|
|
12138
14809
|
const end = current.indexOf(RULES_END);
|
|
12139
14810
|
let text;
|
|
12140
14811
|
if (begin !== -1 && end !== -1 && end > begin) {
|
|
@@ -12150,10 +14821,10 @@ ${RULES_BLOCK}
|
|
|
12150
14821
|
}
|
|
12151
14822
|
return { text, changed: text !== current };
|
|
12152
14823
|
}
|
|
12153
|
-
function rulesFileFor(root, exists =
|
|
12154
|
-
const agents =
|
|
14824
|
+
function rulesFileFor(root, exists = existsSync25) {
|
|
14825
|
+
const agents = join27(root, "AGENTS.md");
|
|
12155
14826
|
if (exists(agents)) return agents;
|
|
12156
|
-
const claude =
|
|
14827
|
+
const claude = join27(root, "CLAUDE.md");
|
|
12157
14828
|
if (exists(claude)) return claude;
|
|
12158
14829
|
return agents;
|
|
12159
14830
|
}
|
|
@@ -12162,14 +14833,14 @@ function chromeCandidates(platform, env) {
|
|
|
12162
14833
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
12163
14834
|
if (platform === "win32")
|
|
12164
14835
|
return [
|
|
12165
|
-
|
|
14836
|
+
join27(
|
|
12166
14837
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
12167
14838
|
"Google",
|
|
12168
14839
|
"Chrome",
|
|
12169
14840
|
"Application",
|
|
12170
14841
|
"chrome.exe"
|
|
12171
14842
|
),
|
|
12172
|
-
|
|
14843
|
+
join27(
|
|
12173
14844
|
env["LOCALAPPDATA"] ?? "",
|
|
12174
14845
|
"Google",
|
|
12175
14846
|
"Chrome",
|
|
@@ -12255,27 +14926,27 @@ function doctorReport(facts) {
|
|
|
12255
14926
|
}
|
|
12256
14927
|
function bundledSkillsVersion(dir) {
|
|
12257
14928
|
try {
|
|
12258
|
-
const v = readFileSync10(
|
|
14929
|
+
const v = readFileSync10(join27(dir, "VERSION"), "utf8").trim();
|
|
12259
14930
|
return /^\d+\.\d+\.\d+$/.test(v) ? v : null;
|
|
12260
14931
|
} catch {
|
|
12261
14932
|
return null;
|
|
12262
14933
|
}
|
|
12263
14934
|
}
|
|
12264
14935
|
function bundledSkillsDir(from) {
|
|
12265
|
-
let dir =
|
|
14936
|
+
let dir = dirname10(from);
|
|
12266
14937
|
for (let i = 0; i < 6; i++) {
|
|
12267
|
-
const pkg =
|
|
12268
|
-
if (
|
|
14938
|
+
const pkg = join27(dir, "package.json");
|
|
14939
|
+
if (existsSync25(pkg)) {
|
|
12269
14940
|
try {
|
|
12270
14941
|
const name = JSON.parse(readFileSync10(pkg, "utf8")).name;
|
|
12271
14942
|
if (name === "@vosjs/cli") {
|
|
12272
|
-
const skills =
|
|
12273
|
-
return
|
|
14943
|
+
const skills = join27(dir, "skills");
|
|
14944
|
+
return existsSync25(skills) && statSync8(skills).isDirectory() ? skills : null;
|
|
12274
14945
|
}
|
|
12275
14946
|
} catch {
|
|
12276
14947
|
}
|
|
12277
14948
|
}
|
|
12278
|
-
const up =
|
|
14949
|
+
const up = dirname10(dir);
|
|
12279
14950
|
if (up === dir) break;
|
|
12280
14951
|
dir = up;
|
|
12281
14952
|
}
|
|
@@ -12283,7 +14954,7 @@ function bundledSkillsDir(from) {
|
|
|
12283
14954
|
}
|
|
12284
14955
|
|
|
12285
14956
|
// src/plugin/machineCmd.ts
|
|
12286
|
-
var
|
|
14957
|
+
var BOOLEAN_FLAGS14 = /* @__PURE__ */ new Set([
|
|
12287
14958
|
"json",
|
|
12288
14959
|
"help",
|
|
12289
14960
|
"global",
|
|
@@ -12306,7 +14977,7 @@ function npx() {
|
|
|
12306
14977
|
function bundledChromium() {
|
|
12307
14978
|
try {
|
|
12308
14979
|
const p = chromium2.executablePath();
|
|
12309
|
-
return p &&
|
|
14980
|
+
return p && existsSync26(p) ? p : null;
|
|
12310
14981
|
} catch {
|
|
12311
14982
|
return null;
|
|
12312
14983
|
}
|
|
@@ -12315,7 +14986,7 @@ function probe2() {
|
|
|
12315
14986
|
return probeBrowser({
|
|
12316
14987
|
platform: process.platform,
|
|
12317
14988
|
env: process.env,
|
|
12318
|
-
exists:
|
|
14989
|
+
exists: existsSync26,
|
|
12319
14990
|
bundled: bundledChromium
|
|
12320
14991
|
});
|
|
12321
14992
|
}
|
|
@@ -12366,7 +15037,7 @@ async function doctorFacts(opts) {
|
|
|
12366
15037
|
};
|
|
12367
15038
|
}
|
|
12368
15039
|
async function cmdSetup(argv) {
|
|
12369
|
-
const { flags } = parseArgs(argv,
|
|
15040
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12370
15041
|
const r = reporterFor(flags);
|
|
12371
15042
|
const root = process.cwd();
|
|
12372
15043
|
const global = flags.global === true;
|
|
@@ -12400,7 +15071,7 @@ async function cmdSetup(argv) {
|
|
|
12400
15071
|
const dest = skillsDirFor(home);
|
|
12401
15072
|
mkdirSync6(dest, { recursive: true });
|
|
12402
15073
|
for (const name of installedSkills(dir)) {
|
|
12403
|
-
cpSync(
|
|
15074
|
+
cpSync(join28(dir, name), join28(dest, name), { recursive: true });
|
|
12404
15075
|
}
|
|
12405
15076
|
}
|
|
12406
15077
|
};
|
|
@@ -12468,7 +15139,7 @@ async function cmdSetup(argv) {
|
|
|
12468
15139
|
let rules = null;
|
|
12469
15140
|
if (!noRules) {
|
|
12470
15141
|
const file = rulesFileFor(root);
|
|
12471
|
-
const existing =
|
|
15142
|
+
const existing = existsSync26(file) ? readFileSync11(file, "utf8") : null;
|
|
12472
15143
|
const { text, changed } = applyRulesBlock(existing);
|
|
12473
15144
|
if (changed) writeFileSync6(file, text);
|
|
12474
15145
|
rules = { file, changed };
|
|
@@ -12499,7 +15170,7 @@ Next: ${report.next_step}`
|
|
|
12499
15170
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
12500
15171
|
}
|
|
12501
15172
|
async function cmdDoctor(argv) {
|
|
12502
|
-
const { flags } = parseArgs(argv,
|
|
15173
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12503
15174
|
const r = reporterFor(flags);
|
|
12504
15175
|
const root = process.cwd();
|
|
12505
15176
|
const homes = detectAgents(root, { home: homedir2(), global: true });
|
|
@@ -12518,7 +15189,7 @@ Next: ${report.next_step}`
|
|
|
12518
15189
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
12519
15190
|
}
|
|
12520
15191
|
async function cmdWhoami(argv) {
|
|
12521
|
-
const { flags } = parseArgs(argv,
|
|
15192
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12522
15193
|
const r = reporterFor(flags);
|
|
12523
15194
|
const origin = platformOrigin({
|
|
12524
15195
|
origin: strFlag(flags, "origin"),
|
|
@@ -12580,9 +15251,9 @@ async function cmdWhoami(argv) {
|
|
|
12580
15251
|
return EXIT_OK;
|
|
12581
15252
|
}
|
|
12582
15253
|
function cmdLogout(argv) {
|
|
12583
|
-
const { flags } = parseArgs(argv,
|
|
15254
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12584
15255
|
const r = reporterFor(flags);
|
|
12585
|
-
const had =
|
|
15256
|
+
const had = existsSync26(CREDENTIALS_PATH);
|
|
12586
15257
|
rmSync3(CREDENTIALS_PATH, { force: true });
|
|
12587
15258
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
12588
15259
|
r.done(
|
|
@@ -12598,7 +15269,7 @@ function cmdLogout(argv) {
|
|
|
12598
15269
|
}
|
|
12599
15270
|
|
|
12600
15271
|
// src/plugin/run.ts
|
|
12601
|
-
var
|
|
15272
|
+
var BOOLEAN_FLAGS15 = /* @__PURE__ */ new Set([
|
|
12602
15273
|
"json",
|
|
12603
15274
|
"help",
|
|
12604
15275
|
"wait",
|
|
@@ -12622,19 +15293,29 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
|
|
|
12622
15293
|
"allow-wall",
|
|
12623
15294
|
"hosted",
|
|
12624
15295
|
"claimable",
|
|
12625
|
-
"share"
|
|
15296
|
+
"share",
|
|
15297
|
+
"verbose"
|
|
15298
|
+
]);
|
|
15299
|
+
var MULTI_FLAGS2 = /* @__PURE__ */ new Set([
|
|
15300
|
+
"set",
|
|
15301
|
+
"override",
|
|
15302
|
+
"browser-arg",
|
|
15303
|
+
"header",
|
|
15304
|
+
"confirm"
|
|
12626
15305
|
]);
|
|
12627
|
-
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
|
|
12628
15306
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
12629
15307
|
|
|
12630
15308
|
Take pipeline
|
|
12631
|
-
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
|
|
12632
|
-
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
|
|
15309
|
+
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
|
|
15310
|
+
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--verbose] [--json]
|
|
12633
15311
|
vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
|
|
12634
15312
|
the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
|
|
12635
|
-
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
|
|
15313
|
+
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions|--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
|
|
12636
15314
|
vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
12637
|
-
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
15315
|
+
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--strict] [--json]
|
|
15316
|
+
--at-zooms stills every zoom span's apex on every media of the take and renders it again with the zooms off: a zoom whose frame matches its zoom-off twin did not render, and says so (exit 2 under --strict)
|
|
15317
|
+
vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]
|
|
15318
|
+
the rendered file itself, no ffmpeg: duration, size, codecs, resolution, frame rate; full-frame scene changes (a warning when a 5 s window holds more than one); loudness (short-term every 3 s, integrated, sample peak; a warning below -23 or above -9 LUFS); the first and last frames as PNGs (a warning when either is near-uniform). Exit 2 on any warning under --strict
|
|
12638
15319
|
vos deliver <take> --to youtube,x,shorts,github (or all) [--look plate|gradient|dark|none] [--brand BRAND.md] [--set path=value] [--release v2.1] [--out dir] [--range a..b] [--parallel N] [--json]
|
|
12639
15320
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
12640
15321
|
vos brand <url> [--out BRAND.md] [--json]
|
|
@@ -12644,6 +15325,10 @@ Take pipeline
|
|
|
12644
15325
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
12645
15326
|
vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
|
|
12646
15327
|
a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so. Without a trace the file opens as FINISHED footage (a render from HyperFrames or Remotion, a film): no card, no browser bar, no drawn cursor, its own frame rate; --as take gives a screen demo the card
|
|
15328
|
+
vos join <take> <take|file.mp4>... --out <dir> [--force] [--captions] [--mute <file>]... [--json]
|
|
15329
|
+
several takes (and plain video files) as ONE take, cut in argument order: the first is the primary (its frame, cursor, export and sound), each next one a media m1, m2, \u2026 with its footage copied, its spans tagged and renamed, its layers moved to where it now starts; a file opens full-bleed and its own sound plays as an audio clip (--mute <file> leaves it out). --captions lands every step's caption anchored to its step, each ending with its clip. The result must pass vos validate before anything is written
|
|
15330
|
+
vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]
|
|
15331
|
+
the film's music as one audio clip (id score): reads the track's loudness, finds its strongest swell and offsets the track so the swell lands at --at (a step, resolved through the cut, or output seconds); without --at it starts where the track first rises above its median level
|
|
12647
15332
|
vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
|
|
12648
15333
|
a finished piece (a HyperFrames composition, a Remotion project bundled with its own bundler, a hand-rolled page) read as a browser renders it: words, palette, faces (catalog or missing), media, scenes, gaps \u2192 port/inventory.json, with stills from its render
|
|
12649
15334
|
vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
|
|
@@ -12652,6 +15337,8 @@ Take pipeline
|
|
|
12652
15337
|
real functions \u2192 config.json, refusing a module-scope read, syntax the page cannot run and non-JSON data; config.sources.json maps each function to its line
|
|
12653
15338
|
vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--threshold 0.95] [--out compare] [--json]
|
|
12654
15339
|
per-frame SSIM and source | vos | difference sheets; exit 1 when ANY frame is under the threshold (needs ffmpeg)
|
|
15340
|
+
vos look <url> [--session <name>|--storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--header name=value]... [--browser-arg=<switch>]... [--out look] [--json]
|
|
15341
|
+
see a page before scripting it, signed in the way vos record would be: <out>/look.png and every control in view with ONE proposed selector (test id, aria-label, role + name, then words in its dialog or landmark), UNIQUE or AMBIGUOUS as the recorder's own engine counts it, plus what the frame exposes. --click/--type run first, in order; a click on a spend or delete word is refused. Exit 4 at a sign-in wall
|
|
12655
15342
|
vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
|
|
12656
15343
|
vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
|
|
12657
15344
|
vos session list | rm <name> what exists and how old; delete one
|
|
@@ -12800,11 +15487,25 @@ reuse block NAMES whatever could not follow \u2014 fix by exception, never
|
|
|
12800
15487
|
re-cut from scratch. Give steps ids in actions.json so re-timing survives
|
|
12801
15488
|
script edits.
|
|
12802
15489
|
|
|
12803
|
-
record --strict exits 2 when any selector was skipped,
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
15490
|
+
record --strict exits 2 when any selector was skipped, an expect never
|
|
15491
|
+
held, a click landed on a disabled control, an await timed out, or the
|
|
15492
|
+
take hit --max-duration (the done event lists skipped[], disabled[] and
|
|
15493
|
+
capped either way). A first load that never reaches networkidle (a page
|
|
15494
|
+
holding a socket open) is a warning, navTimeout in the done event, never
|
|
15495
|
+
a strict failure. --max-duration defaults to
|
|
15496
|
+
the hosted recording cap, read live from GET /api/limits (30 min when the
|
|
15497
|
+
origin is unreachable): the capture stops there and says so in one line.
|
|
15498
|
+
Three steps beyond the gestures: expect checks the page on camera
|
|
15499
|
+
(selector, text or gone; enabled; timeout 5 s); await waits OFF camera
|
|
15500
|
+
(until {selector|text|gone}, timeout 600 s, poll 2 s, reload, onTimeout
|
|
15501
|
+
fail|continue), so the footage has no gap and the cut lands in
|
|
15502
|
+
meta.skips[] and becomes a segment boundary; download keeps a file the page
|
|
15503
|
+
makes in the take's outputs/ (selector: the download a click starts; src:
|
|
15504
|
+
the currentSrc of a video, audio or img; as: the file name), listed in
|
|
15505
|
+
meta.outputs[]. A click, press or type with "guard": "spend" costs
|
|
15506
|
+
something real: --dry-run hovers it and never presses, and record presses
|
|
15507
|
+
it only with --confirm <id> (repeatable), refusing before any capture
|
|
15508
|
+
otherwise. --hosted refuses guarded and download steps.
|
|
12808
15509
|
frames captures PNG stills at OUTPUT times (seconds or % of duration);
|
|
12809
15510
|
--at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
|
|
12810
15511
|
the agent's eyes (--size WxH for an exact size). render
|
|
@@ -12922,7 +15623,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
12922
15623
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
12923
15624
|
`;
|
|
12924
15625
|
async function loadActions(file) {
|
|
12925
|
-
const raw = JSON.parse(await
|
|
15626
|
+
const raw = JSON.parse(await readFile15(file, "utf8"));
|
|
12926
15627
|
const errors = validateActions(raw);
|
|
12927
15628
|
if (errors.length)
|
|
12928
15629
|
throw new UsageError(`invalid actions file:
|
|
@@ -12951,7 +15652,10 @@ async function maxDuration(flags, r) {
|
|
|
12951
15652
|
}
|
|
12952
15653
|
function strictReason(rec) {
|
|
12953
15654
|
const parts = [`${rec.skipped.length} skipped step(s)`];
|
|
12954
|
-
if (rec.
|
|
15655
|
+
if (rec.disabled?.length)
|
|
15656
|
+
parts.push(`${rec.disabled.length} click(s) on a disabled control`);
|
|
15657
|
+
const timedOut = timedOutAwaits(rec);
|
|
15658
|
+
if (timedOut) parts.push(`${timedOut} await(s) that timed out`);
|
|
12955
15659
|
if (rec.capped) parts.push("stopped at --max-duration");
|
|
12956
15660
|
const missed = maskMisses(rec);
|
|
12957
15661
|
if (missed.length)
|
|
@@ -12960,6 +15664,15 @@ function strictReason(rec) {
|
|
|
12960
15664
|
);
|
|
12961
15665
|
return parts.join(" + ");
|
|
12962
15666
|
}
|
|
15667
|
+
function timedOutAwaits(rec) {
|
|
15668
|
+
return (rec.meta?.steps ?? []).filter((s) => s.do === "await" && s.timedOut).length;
|
|
15669
|
+
}
|
|
15670
|
+
function strictFails(rec) {
|
|
15671
|
+
return rec.skipped.length > 0 || rec.disabled.length > 0 || timedOutAwaits(rec) > 0 || rec.capped || maskMisses(rec).length > 0;
|
|
15672
|
+
}
|
|
15673
|
+
function navTimeoutNote(rec) {
|
|
15674
|
+
return rec.navTimeout ? "\n note: the first load never reached networkidle (a page holding a socket open never does); recorded anyway" : "";
|
|
15675
|
+
}
|
|
12963
15676
|
function maskMisses(rec) {
|
|
12964
15677
|
return (rec.masks ?? []).filter((m) => m.hits === 0).map((m) => m.selector);
|
|
12965
15678
|
}
|
|
@@ -12970,6 +15683,53 @@ function exposureNote(rec) {
|
|
|
12970
15683
|
${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
|
|
12971
15684
|
${EXPOSURE_ADVICE}`;
|
|
12972
15685
|
}
|
|
15686
|
+
function takeConfirm(multi, actions, opts) {
|
|
15687
|
+
const given = (multi.confirm ?? []).filter(Boolean);
|
|
15688
|
+
if (opts.dryRun) return given;
|
|
15689
|
+
const verdict = guardVerdict(actions, given);
|
|
15690
|
+
if (!verdict.ok) {
|
|
15691
|
+
const guarded = guardedSteps(actions);
|
|
15692
|
+
for (const id of verdict.unconfirmed)
|
|
15693
|
+
opts.r.event({
|
|
15694
|
+
event: "refused",
|
|
15695
|
+
reason: verdict.message,
|
|
15696
|
+
step: guarded.find((g) => g.id === id)?.step ?? null,
|
|
15697
|
+
id
|
|
15698
|
+
});
|
|
15699
|
+
for (const id of verdict.unknown)
|
|
15700
|
+
opts.r.event({
|
|
15701
|
+
event: "refused",
|
|
15702
|
+
reason: verdict.message,
|
|
15703
|
+
step: null,
|
|
15704
|
+
id
|
|
15705
|
+
});
|
|
15706
|
+
throw new UsageError(verdict.message);
|
|
15707
|
+
}
|
|
15708
|
+
return given;
|
|
15709
|
+
}
|
|
15710
|
+
function stepReportNote(rec) {
|
|
15711
|
+
const lines = [];
|
|
15712
|
+
if (rec.disabled.length)
|
|
15713
|
+
lines.push(
|
|
15714
|
+
`DISABLED when clicked: ${rec.disabled.map((d) => `#${d.step}${d.id ? ` ${d.id}` : ""} ${d.selector}`).join(", ")} (the press likely did nothing; an expect with enabled: true before it waits for the control)`
|
|
15715
|
+
);
|
|
15716
|
+
if (rec.skips.length)
|
|
15717
|
+
lines.push(
|
|
15718
|
+
`off camera: ${rec.skips.map((s) => `#${s.step}${s.id ? ` ${s.id}` : ""} ${(s.ms / 1e3).toFixed(1)} s, cut at ${s.at.toFixed(2)}s${s.timedOut ? " (timed out)" : ""}`).join(", ")}`
|
|
15719
|
+
);
|
|
15720
|
+
if (rec.outputs.length)
|
|
15721
|
+
lines.push(
|
|
15722
|
+
`outputs/: ${rec.outputs.map((o) => `${o.name} (${o.bytes} bytes)`).join(", ")}`
|
|
15723
|
+
);
|
|
15724
|
+
if (rec.confirmed.length)
|
|
15725
|
+
lines.push(`pressed, as --confirm said: ${rec.confirmed.join(", ")}`);
|
|
15726
|
+
if (rec.stopped) lines.push(`STOPPED: ${rec.stopped.reason}`);
|
|
15727
|
+
return lines.map((l) => `
|
|
15728
|
+
${l}`).join("");
|
|
15729
|
+
}
|
|
15730
|
+
function skippedLine(s) {
|
|
15731
|
+
return `#${s.step}${s.id ? ` ${s.id}` : ""} ${s.do}${s.reason ? `: ${s.reason}` : ` ${s.selector}`}`;
|
|
15732
|
+
}
|
|
12973
15733
|
function takeHeaders(multi) {
|
|
12974
15734
|
try {
|
|
12975
15735
|
return parseHeaders(multi.header);
|
|
@@ -12984,8 +15744,8 @@ function takeBrowserArgs(multi) {
|
|
|
12984
15744
|
function takeStorageState(flags) {
|
|
12985
15745
|
const raw = strFlag(flags, "storage-state");
|
|
12986
15746
|
if (!raw) return void 0;
|
|
12987
|
-
const path =
|
|
12988
|
-
if (!
|
|
15747
|
+
const path = resolve18(raw);
|
|
15748
|
+
if (!existsSync27(path))
|
|
12989
15749
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
12990
15750
|
return path;
|
|
12991
15751
|
}
|
|
@@ -12998,7 +15758,7 @@ function takeSessionName(flags, storageState) {
|
|
|
12998
15758
|
throw new UsageError(
|
|
12999
15759
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
13000
15760
|
);
|
|
13001
|
-
if (!
|
|
15761
|
+
if (!existsSync27(sessionDir(name)))
|
|
13002
15762
|
throw new UsageError(
|
|
13003
15763
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
13004
15764
|
);
|
|
@@ -13054,7 +15814,7 @@ function wallGate(flags, r, opts) {
|
|
|
13054
15814
|
async function cmdRecord(argv) {
|
|
13055
15815
|
const { positionals, flags, multi } = parseArgs(
|
|
13056
15816
|
argv,
|
|
13057
|
-
|
|
15817
|
+
BOOLEAN_FLAGS15,
|
|
13058
15818
|
MULTI_FLAGS2
|
|
13059
15819
|
);
|
|
13060
15820
|
const actionsPath = strFlag(flags, "actions") ?? positionals[0];
|
|
@@ -13067,14 +15827,18 @@ async function cmdRecord(argv) {
|
|
|
13067
15827
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13068
15828
|
if (!url)
|
|
13069
15829
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13070
|
-
const scheme =
|
|
15830
|
+
const scheme = urlSchemeProblem2(url);
|
|
13071
15831
|
if (scheme) throw new UsageError(scheme);
|
|
13072
15832
|
announceEnvNames(actions, r);
|
|
13073
|
-
const outDir =
|
|
15833
|
+
const outDir = resolve18(strFlag(flags, "out") ?? "take");
|
|
13074
15834
|
const storageState = takeStorageState(flags);
|
|
13075
15835
|
const sessionName = takeSessionName(flags, storageState);
|
|
13076
15836
|
const browserArgs = takeBrowserArgs(multi);
|
|
13077
15837
|
const headers = takeHeaders(multi);
|
|
15838
|
+
const confirm = flags.hosted === true ? [] : takeConfirm(multi, actions, {
|
|
15839
|
+
dryRun: flags["dry-run"] === true,
|
|
15840
|
+
r
|
|
15841
|
+
});
|
|
13078
15842
|
if (flags.hosted === true) {
|
|
13079
15843
|
if (flags["dry-run"] === true)
|
|
13080
15844
|
throw new UsageError(
|
|
@@ -13084,6 +15848,15 @@ async function cmdRecord(argv) {
|
|
|
13084
15848
|
throw new UsageError(
|
|
13085
15849
|
'a session never reaches vos.so: --hosted takes no --session or --storage-state. Put the sign-in in the setup steps with { "env": "NAME" }, or record here with vos record.'
|
|
13086
15850
|
);
|
|
15851
|
+
const refusals = hostedRefusals(actions);
|
|
15852
|
+
if (refusals.length)
|
|
15853
|
+
throw new UsageError(
|
|
15854
|
+
`a hosted take cannot run this script: ${refusals.join("; ")}`
|
|
15855
|
+
);
|
|
15856
|
+
if (multi.confirm?.length)
|
|
15857
|
+
throw new UsageError(
|
|
15858
|
+
"--confirm presses a guarded step on this machine; a hosted take runs no guarded step, so drop --hosted to record it here"
|
|
15859
|
+
);
|
|
13087
15860
|
const origin = platformOrigin({
|
|
13088
15861
|
origin: strFlag(flags, "origin"),
|
|
13089
15862
|
api: strFlag(flags, "api")
|
|
@@ -13183,6 +15956,8 @@ async function cmdRecord(argv) {
|
|
|
13183
15956
|
headers,
|
|
13184
15957
|
context: context2,
|
|
13185
15958
|
dryRun: true,
|
|
15959
|
+
verbose: flags.verbose === true,
|
|
15960
|
+
onEvent: (e) => r.event(e),
|
|
13186
15961
|
// A rehearsal is always strict, and it touches no directory.
|
|
13187
15962
|
onArrival: wallGate(flags, r, { strict: true })
|
|
13188
15963
|
})
|
|
@@ -13191,9 +15966,28 @@ async function cmdRecord(argv) {
|
|
|
13191
15966
|
const lines = steps.map((s) => {
|
|
13192
15967
|
const rect = s.rect ? ` rect ${s.rect.x},${s.rect.y} ${s.rect.w}\xD7${s.rect.h}` : "";
|
|
13193
15968
|
const what = s.selector ? ` ${s.selector}` : "";
|
|
13194
|
-
|
|
15969
|
+
const out = rec.outputs.find((o) => o.step === s.step);
|
|
15970
|
+
const notes = [
|
|
15971
|
+
...rec.disabled.some((d) => d.step === s.step) ? ["DISABLED, pressed anyway"] : [],
|
|
15972
|
+
...s.guard && !s.skipped ? [`guarded (${s.guard}), hovered, not pressed`] : [],
|
|
15973
|
+
...s.followsGuard !== void 0 ? [
|
|
15974
|
+
`not waited: it follows #${s.followsGuard} (guarded, not pressed)`
|
|
15975
|
+
] : [],
|
|
15976
|
+
...s.offCameraMs !== void 0 ? [
|
|
15977
|
+
`${s.timedOut ? "timed out after" : "held after"} ${(s.offCameraMs / 1e3).toFixed(1)} s off camera`
|
|
15978
|
+
] : [],
|
|
15979
|
+
...out ? [`\u2192 ${out.name} (${out.bytes} bytes, not kept in a rehearsal)`] : []
|
|
15980
|
+
].map((n) => ` ${n}`);
|
|
15981
|
+
return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? ` ${s.reason ?? "NOT FOUND"}` : rect}${s.navigated ? " \u2192 navigated" : ""}${notes.join("")}`;
|
|
13195
15982
|
});
|
|
13196
|
-
const
|
|
15983
|
+
for (const [i, step] of actions.steps.entries())
|
|
15984
|
+
if (!steps.some((s) => s.step === i))
|
|
15985
|
+
lines.push(
|
|
15986
|
+
` \u2013 #${i}${step.id ? ` (${step.id})` : ""} ${step.do} not run`
|
|
15987
|
+
);
|
|
15988
|
+
const resolved = steps.filter((s) => !s.skipped).length;
|
|
15989
|
+
const failed = strictFails(rec) || rec.stopped !== null;
|
|
15990
|
+
const unconfirmed = guardVerdict(actions, confirm);
|
|
13197
15991
|
const carried = [
|
|
13198
15992
|
...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
|
|
13199
15993
|
...browserArgs.map((a) => `--browser-arg=${a}`),
|
|
@@ -13201,9 +15995,11 @@ async function cmdRecord(argv) {
|
|
|
13201
15995
|
// printed.
|
|
13202
15996
|
...Object.keys(headers).map((k) => `--header ${k}=\u2026`),
|
|
13203
15997
|
...sessionName ? [`--session ${sessionName}`] : [],
|
|
13204
|
-
...flags["allow-wall"] === true ? ["--allow-wall"] : []
|
|
15998
|
+
...flags["allow-wall"] === true ? ["--allow-wall"] : [],
|
|
15999
|
+
...confirm.map((c) => `--confirm ${c}`)
|
|
13205
16000
|
].join(" ");
|
|
13206
16001
|
const nextOut = strFlag(flags, "out") ?? "take";
|
|
16002
|
+
const guardNote = !unconfirmed.ok ? unconfirmed.unconfirmed.length ? ` (it refuses until you add ${unconfirmed.unconfirmed.map((id) => `--confirm ${id}`).join(" ")}: ${unconfirmed.unconfirmed.length === 1 ? "that step spends" : "those steps spend"} something real)` : ` (${unconfirmed.unknown.map((c) => `--confirm ${c}`).join(", ")} names no guarded step)` : "";
|
|
13207
16003
|
r.done(
|
|
13208
16004
|
{
|
|
13209
16005
|
dryRun: true,
|
|
@@ -13211,28 +16007,33 @@ async function cmdRecord(argv) {
|
|
|
13211
16007
|
seconds: +((Date.now() - started) / 1e3).toFixed(1),
|
|
13212
16008
|
steps,
|
|
13213
16009
|
skipped: rec.skipped,
|
|
16010
|
+
disabled: rec.disabled,
|
|
16011
|
+
skips: rec.skips,
|
|
16012
|
+
outputs: rec.outputs,
|
|
16013
|
+
pageErrors: rec.pageErrors,
|
|
16014
|
+
...rec.stopped ? { stopped: rec.stopped } : {},
|
|
13214
16015
|
navTimeout: rec.navTimeout,
|
|
13215
16016
|
exposures: rec.exposures,
|
|
13216
16017
|
masks: rec.masks
|
|
13217
16018
|
},
|
|
13218
|
-
`${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${
|
|
16019
|
+
`${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${resolved}/${actions.steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
|
|
13219
16020
|
${lines.join("\n")}
|
|
13220
|
-
${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict`}${exposureNote(rec)}`
|
|
16021
|
+
${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${rec.stopped ? `STOPPED: ${rec.stopped.reason}. ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict${guardNote}`}${exposureNote(rec)}`
|
|
13221
16022
|
);
|
|
13222
|
-
return failed ? EXIT_USAGE : EXIT_OK;
|
|
16023
|
+
return rec.stopped ? EXIT_ERROR : failed ? EXIT_USAGE : EXIT_OK;
|
|
13223
16024
|
} finally {
|
|
13224
16025
|
await browser2.close();
|
|
13225
16026
|
}
|
|
13226
16027
|
}
|
|
13227
16028
|
const paths = takePaths(outDir);
|
|
13228
16029
|
const prepare = async () => {
|
|
13229
|
-
if (
|
|
16030
|
+
if (existsSync27(join29(outDir, "meta.json"))) {
|
|
13230
16031
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13231
16032
|
r.log(
|
|
13232
16033
|
`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" : "")
|
|
13233
16034
|
);
|
|
13234
16035
|
}
|
|
13235
|
-
await
|
|
16036
|
+
await mkdir13(outDir, { recursive: true });
|
|
13236
16037
|
await ensureTakeDir(outDir);
|
|
13237
16038
|
};
|
|
13238
16039
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13247,6 +16048,9 @@ ${lines.join("\n")}
|
|
|
13247
16048
|
storageState,
|
|
13248
16049
|
headers,
|
|
13249
16050
|
context: context2,
|
|
16051
|
+
confirm,
|
|
16052
|
+
verbose: flags.verbose === true,
|
|
16053
|
+
onEvent: (e) => r.event(e),
|
|
13250
16054
|
onArrival: wallGate(flags, r, {
|
|
13251
16055
|
strict: flags.strict === true,
|
|
13252
16056
|
prepare
|
|
@@ -13265,9 +16069,29 @@ ${lines.join("\n")}
|
|
|
13265
16069
|
const plan = await planTake(outDir, { backdrop });
|
|
13266
16070
|
const clicks = rec.events.filter((e) => e.type === "down").length;
|
|
13267
16071
|
const strict = flags.strict === true;
|
|
13268
|
-
const strictFail = strict && (rec
|
|
16072
|
+
const strictFail = strict && strictFails(rec);
|
|
13269
16073
|
const skippedNote = rec.skipped.length ? `
|
|
13270
|
-
SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map(
|
|
16074
|
+
SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map(skippedLine).join(", ")}` : "";
|
|
16075
|
+
const stepNote = stepReportNote(rec);
|
|
16076
|
+
if (rec.stopped) {
|
|
16077
|
+
r.done(
|
|
16078
|
+
{
|
|
16079
|
+
take: outDir,
|
|
16080
|
+
ok: false,
|
|
16081
|
+
stopped: rec.stopped,
|
|
16082
|
+
durationMs: rec.meta.durationMs,
|
|
16083
|
+
skipped: rec.skipped,
|
|
16084
|
+
disabled: rec.disabled,
|
|
16085
|
+
confirmed: rec.confirmed,
|
|
16086
|
+
skips: rec.skips,
|
|
16087
|
+
outputs: rec.outputs,
|
|
16088
|
+
pageErrors: rec.pageErrors
|
|
16089
|
+
},
|
|
16090
|
+
`FAILED: ${rec.stopped.reason}.
|
|
16091
|
+
The footage up to it is in ${outDir}.${skippedNote}${stepNote}${navTimeoutNote(rec)}`
|
|
16092
|
+
);
|
|
16093
|
+
return EXIT_ERROR;
|
|
16094
|
+
}
|
|
13271
16095
|
r.done(
|
|
13272
16096
|
{
|
|
13273
16097
|
take: outDir,
|
|
@@ -13287,11 +16111,16 @@ ${lines.join("\n")}
|
|
|
13287
16111
|
...rec.wall ? { wall: rec.wall } : {},
|
|
13288
16112
|
exposures: rec.exposures,
|
|
13289
16113
|
...rec.masks.length ? { masks: rec.masks } : {},
|
|
16114
|
+
disabled: rec.disabled,
|
|
16115
|
+
confirmed: rec.confirmed,
|
|
16116
|
+
skips: rec.skips,
|
|
16117
|
+
outputs: rec.outputs,
|
|
16118
|
+
pageErrors: rec.pageErrors,
|
|
13290
16119
|
...strictFail ? { strictFailed: true } : {}
|
|
13291
16120
|
},
|
|
13292
|
-
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
13293
|
-
${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
|
|
13294
|
-
Next: edit ${
|
|
16121
|
+
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${stepNote}${navTimeoutNote(rec)}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
16122
|
+
${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}${stepNote}${navTimeoutNote(rec)}
|
|
16123
|
+
Next: edit ${join29(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
|
|
13295
16124
|
);
|
|
13296
16125
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
13297
16126
|
} finally {
|
|
@@ -13301,7 +16130,7 @@ ${lines.join("\n")}
|
|
|
13301
16130
|
async function cmdCreate2(argv) {
|
|
13302
16131
|
const { positionals, flags, multi } = parseArgs(
|
|
13303
16132
|
argv,
|
|
13304
|
-
|
|
16133
|
+
BOOLEAN_FLAGS15,
|
|
13305
16134
|
MULTI_FLAGS2
|
|
13306
16135
|
);
|
|
13307
16136
|
const actionsPath = strFlag(flags, "actions");
|
|
@@ -13315,12 +16144,12 @@ async function cmdCreate2(argv) {
|
|
|
13315
16144
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13316
16145
|
if (!url)
|
|
13317
16146
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13318
|
-
const scheme =
|
|
16147
|
+
const scheme = urlSchemeProblem2(url);
|
|
13319
16148
|
if (scheme) throw new UsageError(scheme);
|
|
13320
16149
|
announceEnvNames(actions, r);
|
|
13321
|
-
const outDir =
|
|
16150
|
+
const outDir = resolve18(strFlag(flags, "out") ?? "take");
|
|
13322
16151
|
const format = videoFormat(strFlag(flags, "format"), positionals[0]);
|
|
13323
|
-
const out = positionals[0] ??
|
|
16152
|
+
const out = positionals[0] ?? join29(outDir, `out.${format}`);
|
|
13324
16153
|
const parallel = numFlag(flags, "parallel", 1);
|
|
13325
16154
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
13326
16155
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -13331,15 +16160,16 @@ async function cmdCreate2(argv) {
|
|
|
13331
16160
|
const sessionName = takeSessionName(flags, storageState);
|
|
13332
16161
|
const browserArgs = takeBrowserArgs(multi);
|
|
13333
16162
|
const headers = takeHeaders(multi);
|
|
16163
|
+
const confirm = takeConfirm(multi, actions, { dryRun: false, r });
|
|
13334
16164
|
const paths = takePaths(outDir);
|
|
13335
16165
|
const prepare = async () => {
|
|
13336
|
-
if (
|
|
16166
|
+
if (existsSync27(join29(outDir, "meta.json"))) {
|
|
13337
16167
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13338
16168
|
r.log(
|
|
13339
16169
|
`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" : "")
|
|
13340
16170
|
);
|
|
13341
16171
|
}
|
|
13342
|
-
await
|
|
16172
|
+
await mkdir13(outDir, { recursive: true });
|
|
13343
16173
|
await ensureTakeDir(outDir);
|
|
13344
16174
|
};
|
|
13345
16175
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13354,6 +16184,9 @@ async function cmdCreate2(argv) {
|
|
|
13354
16184
|
storageState,
|
|
13355
16185
|
headers,
|
|
13356
16186
|
context: context2,
|
|
16187
|
+
confirm,
|
|
16188
|
+
verbose: flags.verbose === true,
|
|
16189
|
+
onEvent: (e) => r.event(e),
|
|
13357
16190
|
onArrival: wallGate(flags, r, {
|
|
13358
16191
|
strict: flags.strict === true,
|
|
13359
16192
|
prepare
|
|
@@ -13370,11 +16203,25 @@ async function cmdCreate2(argv) {
|
|
|
13370
16203
|
await releaseFrames(paths, flags, r.log);
|
|
13371
16204
|
r.event({ event: "phase", phase: "plan" });
|
|
13372
16205
|
await planTake(outDir, { backdrop });
|
|
13373
|
-
if (
|
|
16206
|
+
if (rec.stopped) {
|
|
16207
|
+
r.done(
|
|
16208
|
+
{
|
|
16209
|
+
take: outDir,
|
|
16210
|
+
ok: false,
|
|
16211
|
+
stopped: rec.stopped,
|
|
16212
|
+
skipped: rec.skipped,
|
|
16213
|
+
rendered: false
|
|
16214
|
+
},
|
|
16215
|
+
`FAILED: ${rec.stopped.reason}. NOT rendering; the footage up to it is in ${outDir}.`
|
|
16216
|
+
);
|
|
16217
|
+
return EXIT_ERROR;
|
|
16218
|
+
}
|
|
16219
|
+
if (flags.strict === true && strictFails(rec)) {
|
|
13374
16220
|
r.done(
|
|
13375
16221
|
{
|
|
13376
16222
|
take: outDir,
|
|
13377
16223
|
skipped: rec.skipped,
|
|
16224
|
+
disabled: rec.disabled,
|
|
13378
16225
|
navTimeout: rec.navTimeout,
|
|
13379
16226
|
capped: rec.capped,
|
|
13380
16227
|
strictFailed: true,
|
|
@@ -13417,20 +16264,26 @@ async function cmdCreate2(argv) {
|
|
|
13417
16264
|
dead: rec.dead,
|
|
13418
16265
|
exposures: rec.exposures,
|
|
13419
16266
|
...rec.masks.length ? { masks: rec.masks } : {},
|
|
16267
|
+
disabled: rec.disabled,
|
|
16268
|
+
confirmed: rec.confirmed,
|
|
16269
|
+
skips: rec.skips,
|
|
16270
|
+
outputs: rec.outputs,
|
|
16271
|
+
pageErrors: rec.pageErrors,
|
|
13420
16272
|
...flags.draft === true ? { draft: true } : {}
|
|
13421
16273
|
},
|
|
13422
16274
|
`Created ${result.out} (${(result.bytes / 1024).toFixed(0)} KB, ${result.width}x${result.height}@${result.fps}, ${result.duration.toFixed(1)}s, ${result.zoomSpans} zoom spans, ${result.clicks} click effects${result.audio ? ", audio" : ""}${flags.draft === true ? ", DRAFT" : ""})
|
|
13423
|
-
take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${exposureNote(rec)}`
|
|
16275
|
+
take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${stepReportNote(rec)}${navTimeoutNote(rec)}${exposureNote(rec)}`
|
|
13424
16276
|
);
|
|
13425
16277
|
return EXIT_OK;
|
|
13426
16278
|
} finally {
|
|
13427
16279
|
await browser.close();
|
|
13428
16280
|
}
|
|
13429
16281
|
}
|
|
13430
|
-
async function cmdPlan(
|
|
16282
|
+
async function cmdPlan(argvIn) {
|
|
16283
|
+
const argv = bareCaptionsFlag(argvIn);
|
|
13431
16284
|
const { positionals, flags, multi } = parseArgs(
|
|
13432
16285
|
argv,
|
|
13433
|
-
|
|
16286
|
+
BOOLEAN_FLAGS15,
|
|
13434
16287
|
/* @__PURE__ */ new Set(["with"])
|
|
13435
16288
|
);
|
|
13436
16289
|
const dir = positionals[0];
|
|
@@ -13440,26 +16293,28 @@ async function cmdPlan(argv) {
|
|
|
13440
16293
|
);
|
|
13441
16294
|
const r = createReporter(flags.json === true);
|
|
13442
16295
|
if (flags.fresh === true) {
|
|
13443
|
-
await
|
|
16296
|
+
await rm5(join29(dir, "doc.json"), { force: true });
|
|
13444
16297
|
r.log("note: --fresh discarded the existing doc.json");
|
|
13445
16298
|
}
|
|
13446
16299
|
let reuse;
|
|
13447
16300
|
if (flags.reuse === true) {
|
|
13448
|
-
const from =
|
|
13449
|
-
if (!
|
|
16301
|
+
const from = resolve18(strFlag(flags, "from") ?? join29(dir, PREV_DOC_NAME));
|
|
16302
|
+
if (!existsSync27(from)) {
|
|
13450
16303
|
throw new UsageError(
|
|
13451
16304
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
13452
16305
|
);
|
|
13453
16306
|
}
|
|
13454
16307
|
reuse = {
|
|
13455
16308
|
from,
|
|
13456
|
-
doc: JSON.parse(await
|
|
16309
|
+
doc: JSON.parse(await readFile15(from, "utf8"))
|
|
13457
16310
|
};
|
|
13458
16311
|
}
|
|
13459
16312
|
const style = await resolveStyleRef(flags);
|
|
13460
|
-
const hasDoc =
|
|
16313
|
+
const hasDoc = existsSync27(join29(dir, "doc.json"));
|
|
13461
16314
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
13462
16315
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
16316
|
+
const stepCaptions = strFlag(flags, "captions") === "steps";
|
|
16317
|
+
if (stepCaptions) release.launchRoles.captions = "none";
|
|
13463
16318
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
13464
16319
|
const stillRaw = strFlag(flags, "still") ?? release.launchRoles.still;
|
|
13465
16320
|
const still = stillRaw === void 0 ? null : Number(stillRaw);
|
|
@@ -13487,6 +16342,16 @@ async function cmdPlan(argv) {
|
|
|
13487
16342
|
}
|
|
13488
16343
|
} : {}
|
|
13489
16344
|
});
|
|
16345
|
+
let captions = null;
|
|
16346
|
+
if (stepCaptions) {
|
|
16347
|
+
const take = await loadTake(dir);
|
|
16348
|
+
captions = landStepCaptions(s.doc, /* @__PURE__ */ new Map([["", take.actions]]));
|
|
16349
|
+
await writeJson(take.paths.doc, s.doc, true);
|
|
16350
|
+
}
|
|
16351
|
+
const captionLines = captions ? `
|
|
16352
|
+
${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} anchored to their steps` + (captions.skipped.length ? `
|
|
16353
|
+
not landed: ${captions.skipped.join("; ")}` : "") + (captions.uncaptioned.length ? `
|
|
16354
|
+
no step in ${captions.uncaptioned.length === 1 && captions.uncaptioned[0] === "" ? "this take" : captions.uncaptioned.map((m) => m ? `media ${m}` : "the primary").join(", ")} carries a caption` : "") : "";
|
|
13490
16355
|
const layoutLines = s.layout ? `
|
|
13491
16356
|
layout from ${s.styleFrom}: ${[
|
|
13492
16357
|
s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
|
|
@@ -13503,7 +16368,7 @@ async function cmdPlan(argv) {
|
|
|
13503
16368
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
13504
16369
|
r.done(
|
|
13505
16370
|
{
|
|
13506
|
-
take:
|
|
16371
|
+
take: resolve18(dir),
|
|
13507
16372
|
fresh: s.fresh,
|
|
13508
16373
|
cursorKept: s.cursorKept,
|
|
13509
16374
|
zoomAuto: s.zoomAuto,
|
|
@@ -13513,9 +16378,10 @@ async function cmdPlan(argv) {
|
|
|
13513
16378
|
...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
|
|
13514
16379
|
...s.layout ? { layout: s.layout } : {},
|
|
13515
16380
|
...s.motion ? { motion: s.motion } : {},
|
|
13516
|
-
...s.reuse ? { reuse: s.reuse } : {}
|
|
16381
|
+
...s.reuse ? { reuse: s.reuse } : {},
|
|
16382
|
+
...captions ? { captions } : {}
|
|
13517
16383
|
},
|
|
13518
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
16384
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join29(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}${captionLines}`
|
|
13519
16385
|
);
|
|
13520
16386
|
return EXIT_OK;
|
|
13521
16387
|
}
|
|
@@ -13628,7 +16494,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
|
|
|
13628
16494
|
async function cmdRender(argv) {
|
|
13629
16495
|
const { positionals, flags, multi } = parseArgs(
|
|
13630
16496
|
argv,
|
|
13631
|
-
|
|
16497
|
+
BOOLEAN_FLAGS15,
|
|
13632
16498
|
MULTI_FLAGS2
|
|
13633
16499
|
);
|
|
13634
16500
|
const dir = positionals[0];
|
|
@@ -13638,7 +16504,7 @@ async function cmdRender(argv) {
|
|
|
13638
16504
|
);
|
|
13639
16505
|
const r = createReporter(flags.json === true);
|
|
13640
16506
|
const format = videoFormat(strFlag(flags, "format"), positionals[1]);
|
|
13641
|
-
const out = positionals[1] ??
|
|
16507
|
+
const out = positionals[1] ?? join29(dir, `out.${format}`);
|
|
13642
16508
|
const parallel = numFlag(flags, "parallel", 1);
|
|
13643
16509
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
13644
16510
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -13696,7 +16562,7 @@ async function cmdRender(argv) {
|
|
|
13696
16562
|
async function cmdFrames(argv) {
|
|
13697
16563
|
const { positionals, flags, multi } = parseArgs(
|
|
13698
16564
|
argv,
|
|
13699
|
-
|
|
16565
|
+
BOOLEAN_FLAGS15,
|
|
13700
16566
|
MULTI_FLAGS2
|
|
13701
16567
|
);
|
|
13702
16568
|
const dir = positionals[0];
|
|
@@ -13714,12 +16580,13 @@ async function cmdFrames(argv) {
|
|
|
13714
16580
|
if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
|
|
13715
16581
|
size = { width: Number(m[1]), height: Number(m[2]) };
|
|
13716
16582
|
}
|
|
13717
|
-
const duration =
|
|
16583
|
+
const duration = docOutputDuration4(take.doc);
|
|
13718
16584
|
const frameRaw = strFlag(flags, "frame");
|
|
13719
16585
|
const timesRaw = strFlag(flags, "times");
|
|
13720
16586
|
const atZooms = flags["at-zooms"] === true;
|
|
13721
16587
|
const atMoments = flags["at-moments"] === true;
|
|
13722
16588
|
const atStill = flags["at-still"] === true;
|
|
16589
|
+
const strict = flags.strict === true;
|
|
13723
16590
|
let times;
|
|
13724
16591
|
try {
|
|
13725
16592
|
times = frameRaw !== void 0 ? parseTimes2(frameRaw, duration) : timesRaw !== void 0 ? parseTimes2(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes2("0,25%,50%,75%,100%", duration);
|
|
@@ -13749,6 +16616,13 @@ async function cmdFrames(argv) {
|
|
|
13749
16616
|
}
|
|
13750
16617
|
});
|
|
13751
16618
|
const index = await writeIndexJson(result);
|
|
16619
|
+
for (const z of result.skippedZooms ?? [])
|
|
16620
|
+
r.warn(
|
|
16621
|
+
`zoom ${z.id}${z.media ? ` (media ${z.media})` : ""} has no apex to still: ${z.reason}`
|
|
16622
|
+
);
|
|
16623
|
+
const unrendered = result.frames.filter((f) => f.rendered === false);
|
|
16624
|
+
for (const f of unrendered) r.warn(unrenderedZoomLine(f));
|
|
16625
|
+
const zoomTag = (f) => `${f.zoomId ? ` ${f.zoomId}` : ""}${f.media ? ` on ${f.media}` : ""}${f.rendered === false ? ", DID NOT RENDER" : ""}`;
|
|
13752
16626
|
r.done(
|
|
13753
16627
|
{
|
|
13754
16628
|
outDir: result.outDir,
|
|
@@ -13756,12 +16630,14 @@ async function cmdFrames(argv) {
|
|
|
13756
16630
|
width: result.width,
|
|
13757
16631
|
height: result.height,
|
|
13758
16632
|
duration: result.duration,
|
|
13759
|
-
frames: result.frames
|
|
16633
|
+
frames: result.frames,
|
|
16634
|
+
...result.skippedZooms ? { skippedZooms: result.skippedZooms } : {},
|
|
16635
|
+
...atZooms ? { unrenderedZooms: unrendered.length } : {}
|
|
13760
16636
|
},
|
|
13761
16637
|
`Wrote ${result.frames.length} still(s) to ${result.outDir} (${result.width}x${result.height})
|
|
13762
|
-
${result.frames.map((f) => `${f.time.toFixed(2)}s${f.kind === "zoom" ?
|
|
16638
|
+
${result.frames.map((f) => `${f.time.toFixed(2)}s${f.kind === "zoom" ? ` [zoom apex${zoomTag(f)}]` : f.kind === "moment" ? ` [${f.momentId}]` : ""} \u2192 ${f.file}`).join("\n ")}`
|
|
13763
16639
|
);
|
|
13764
|
-
return EXIT_OK;
|
|
16640
|
+
return unrendered.length && strict ? EXIT_USAGE : EXIT_OK;
|
|
13765
16641
|
} finally {
|
|
13766
16642
|
await browser.close();
|
|
13767
16643
|
}
|
|
@@ -13769,7 +16645,7 @@ async function cmdFrames(argv) {
|
|
|
13769
16645
|
async function cmdDeliver(argv) {
|
|
13770
16646
|
const { positionals, flags, multi } = parseArgs(
|
|
13771
16647
|
argv,
|
|
13772
|
-
|
|
16648
|
+
BOOLEAN_FLAGS15,
|
|
13773
16649
|
MULTI_FLAGS2
|
|
13774
16650
|
);
|
|
13775
16651
|
const dir = positionals[0];
|
|
@@ -13869,15 +16745,15 @@ async function resolveStyleRef(flags) {
|
|
|
13869
16745
|
}
|
|
13870
16746
|
async function resolveDocRef(ref, flags, what) {
|
|
13871
16747
|
const styleRef = ref;
|
|
13872
|
-
const file =
|
|
16748
|
+
const file = existsSync27(styleRef) ? resolve18(
|
|
13873
16749
|
styleRef,
|
|
13874
|
-
|
|
16750
|
+
existsSync27(join29(styleRef, "doc.json")) ? "doc.json" : ""
|
|
13875
16751
|
) : null;
|
|
13876
|
-
if (file &&
|
|
16752
|
+
if (file && existsSync27(file)) {
|
|
13877
16753
|
return {
|
|
13878
16754
|
from: file,
|
|
13879
16755
|
doc: migrateHostedDoc4(
|
|
13880
|
-
JSON.parse(await
|
|
16756
|
+
JSON.parse(await readFile15(file, "utf8"))
|
|
13881
16757
|
)
|
|
13882
16758
|
};
|
|
13883
16759
|
}
|
|
@@ -13919,7 +16795,7 @@ async function officialTemplate(origin, ref) {
|
|
|
13919
16795
|
}
|
|
13920
16796
|
}
|
|
13921
16797
|
async function cmdDigest(argv) {
|
|
13922
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16798
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
13923
16799
|
const dir = positionals[0];
|
|
13924
16800
|
if (!dir)
|
|
13925
16801
|
throw new UsageError(
|
|
@@ -13929,7 +16805,7 @@ async function cmdDigest(argv) {
|
|
|
13929
16805
|
const take = await loadTake(dir);
|
|
13930
16806
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
13931
16807
|
const transcriptPath = strFlag(flags, "transcript");
|
|
13932
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
16808
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
|
|
13933
16809
|
const style = await resolveStyleRef(flags);
|
|
13934
16810
|
const noFrames = flags["no-frames"] === true;
|
|
13935
16811
|
let browser = null;
|
|
@@ -13960,7 +16836,7 @@ async function cmdDigest(argv) {
|
|
|
13960
16836
|
kinds,
|
|
13961
16837
|
frames: d.images.full,
|
|
13962
16838
|
crops: d.images.crop,
|
|
13963
|
-
sheet: d.images.sheet ?
|
|
16839
|
+
sheet: d.images.sheet ? join29(result.outDir, d.images.sheet) : null,
|
|
13964
16840
|
scenes: kinds.scene ?? 0,
|
|
13965
16841
|
sourceDuration: d.take.sourceDuration,
|
|
13966
16842
|
outputDuration: d.take.outputDuration,
|
|
@@ -13983,19 +16859,19 @@ async function cmdDigest(argv) {
|
|
|
13983
16859
|
}
|
|
13984
16860
|
}
|
|
13985
16861
|
async function cmdOpen(argv) {
|
|
13986
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16862
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
13987
16863
|
const dir = positionals[0];
|
|
13988
16864
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
13989
16865
|
const r = createReporter(flags.json === true);
|
|
13990
16866
|
const take = await loadTake(dir);
|
|
13991
|
-
if (!
|
|
16867
|
+
if (!existsSync27(take.paths.recording)) {
|
|
13992
16868
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
13993
16869
|
}
|
|
13994
16870
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
13995
16871
|
/\/+$/,
|
|
13996
16872
|
""
|
|
13997
16873
|
);
|
|
13998
|
-
const server = await startTakeServer(
|
|
16874
|
+
const server = await startTakeServer(resolve18(dir), {});
|
|
13999
16875
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
14000
16876
|
r.event({ event: "open", server: server.base, url });
|
|
14001
16877
|
r.log(`take served at ${server.base}`);
|
|
@@ -14018,7 +16894,7 @@ async function cmdOpen(argv) {
|
|
|
14018
16894
|
return EXIT_OK;
|
|
14019
16895
|
}
|
|
14020
16896
|
async function cmdCallout(argv) {
|
|
14021
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16897
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
14022
16898
|
const dir = positionals[0];
|
|
14023
16899
|
const shape = positionals[1];
|
|
14024
16900
|
if (!dir || !shape || !isCalloutShape(shape))
|
|
@@ -14042,7 +16918,7 @@ async function cmdCallout(argv) {
|
|
|
14042
16918
|
`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`
|
|
14043
16919
|
);
|
|
14044
16920
|
const codeFile = strFlag(flags, "code-file");
|
|
14045
|
-
const code = codeFile ? await
|
|
16921
|
+
const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
|
|
14046
16922
|
const words2 = {
|
|
14047
16923
|
kicker: strFlag(flags, "kicker"),
|
|
14048
16924
|
title: strFlag(flags, "title"),
|
|
@@ -14117,11 +16993,11 @@ async function cmdCallout(argv) {
|
|
|
14117
16993
|
return EXIT_OK;
|
|
14118
16994
|
}
|
|
14119
16995
|
async function cmdValidate(argv) {
|
|
14120
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16996
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
14121
16997
|
const target = positionals[0];
|
|
14122
16998
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
14123
16999
|
const r = createReporter(flags.json === true);
|
|
14124
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
17000
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync27(join29(target, "kit.json")) && !isTakeDir(target) ? join29(target, "kit.json") : null;
|
|
14125
17001
|
if (kitTarget) {
|
|
14126
17002
|
const picture = flags.picture === true;
|
|
14127
17003
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -14153,9 +17029,9 @@ async function cmdValidate(argv) {
|
|
|
14153
17029
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
14154
17030
|
return EXIT_OK;
|
|
14155
17031
|
}
|
|
14156
|
-
if (!isTakeDir(target) &&
|
|
17032
|
+
if (!isTakeDir(target) && existsSync27(join29(target, "config.json"))) {
|
|
14157
17033
|
const parsed = JSON.parse(
|
|
14158
|
-
await
|
|
17034
|
+
await readFile15(join29(target, "config.json"), "utf8")
|
|
14159
17035
|
);
|
|
14160
17036
|
const pre = preflightConfig(parsed);
|
|
14161
17037
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -14196,7 +17072,8 @@ async function cmdValidate(argv) {
|
|
|
14196
17072
|
const take = await loadTake(target);
|
|
14197
17073
|
const problems = [];
|
|
14198
17074
|
const warnings = [];
|
|
14199
|
-
|
|
17075
|
+
const notes = [];
|
|
17076
|
+
if (!existsSync27(take.paths.recording)) {
|
|
14200
17077
|
problems.push("missing recording (re-run record)");
|
|
14201
17078
|
} else {
|
|
14202
17079
|
const mismatch = containerMismatch(
|
|
@@ -14222,6 +17099,7 @@ async function cmdValidate(argv) {
|
|
|
14222
17099
|
const lint = lintDoc(take.doc);
|
|
14223
17100
|
problems.push(...lint.problems.map((p) => `doc.json: ${p}`));
|
|
14224
17101
|
warnings.push(...lint.warnings.map((w) => `doc.json: ${w}`));
|
|
17102
|
+
notes.push(...(lint.notes ?? []).map((n) => `doc.json: ${n}`));
|
|
14225
17103
|
const probe3 = mediaProbeLints(
|
|
14226
17104
|
await probeMediaUrls(collectDocMediaUrls(take.doc))
|
|
14227
17105
|
);
|
|
@@ -14260,26 +17138,37 @@ async function cmdValidate(argv) {
|
|
|
14260
17138
|
}
|
|
14261
17139
|
if (problems.length) {
|
|
14262
17140
|
r.done(
|
|
14263
|
-
{
|
|
17141
|
+
{
|
|
17142
|
+
valid: false,
|
|
17143
|
+
target,
|
|
17144
|
+
problems,
|
|
17145
|
+
warnings,
|
|
17146
|
+
...notes.length ? { notes } : {}
|
|
17147
|
+
},
|
|
14264
17148
|
`${target}:
|
|
14265
17149
|
${problems.join("\n ")}${warnings.length ? `
|
|
14266
17150
|
warnings:
|
|
14267
|
-
${warnings.join("\n ")}` : ""}`
|
|
17151
|
+
${warnings.join("\n ")}` : ""}${notes.length ? `
|
|
17152
|
+
notes:
|
|
17153
|
+
${notes.join("\n ")}` : ""}`
|
|
14268
17154
|
);
|
|
14269
17155
|
return EXIT_ERROR;
|
|
14270
17156
|
}
|
|
17157
|
+
const noteLines = notes.length ? `
|
|
17158
|
+
notes:
|
|
17159
|
+
${notes.join("\n ")}` : "";
|
|
14271
17160
|
r.done(
|
|
14272
|
-
{ valid: true, target, warnings },
|
|
17161
|
+
{ valid: true, target, warnings, ...notes.length ? { notes } : {} },
|
|
14273
17162
|
`${target}: take is complete and renderable${warnings.length ? `
|
|
14274
17163
|
warnings:
|
|
14275
|
-
${warnings.join("\n ")}` : ""}`
|
|
17164
|
+
${warnings.join("\n ")}` : ""}${noteLines}`
|
|
14276
17165
|
);
|
|
14277
17166
|
return EXIT_OK;
|
|
14278
17167
|
}
|
|
14279
17168
|
async function cmdPush3(argv) {
|
|
14280
17169
|
const { positionals, flags, multi } = parseArgs(
|
|
14281
17170
|
argv,
|
|
14282
|
-
|
|
17171
|
+
BOOLEAN_FLAGS15,
|
|
14283
17172
|
MULTI_FLAGS2
|
|
14284
17173
|
);
|
|
14285
17174
|
const dir = positionals[0];
|
|
@@ -14289,7 +17178,7 @@ async function cmdPush3(argv) {
|
|
|
14289
17178
|
);
|
|
14290
17179
|
const refusal = takePushRefusal({
|
|
14291
17180
|
vos: strFlag(flags, "vos"),
|
|
14292
|
-
trackedVosId: readSyncState(
|
|
17181
|
+
trackedVosId: readSyncState(resolve18(dir))?.vosId ?? null,
|
|
14293
17182
|
programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
|
|
14294
17183
|
(name) => hasFlag(flags, name)
|
|
14295
17184
|
)
|
|
@@ -14305,7 +17194,7 @@ async function cmdPush3(argv) {
|
|
|
14305
17194
|
`--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
|
|
14306
17195
|
);
|
|
14307
17196
|
const claim = await pushTakeClaimable(
|
|
14308
|
-
|
|
17197
|
+
resolve18(dir),
|
|
14309
17198
|
{
|
|
14310
17199
|
api: strFlag(flags, "api"),
|
|
14311
17200
|
origin: strFlag(flags, "origin"),
|
|
@@ -14327,7 +17216,7 @@ reference and the only credential.`
|
|
|
14327
17216
|
}
|
|
14328
17217
|
const overrides = multi.override;
|
|
14329
17218
|
const result = await pushTake(
|
|
14330
|
-
|
|
17219
|
+
resolve18(dir),
|
|
14331
17220
|
{
|
|
14332
17221
|
key: strFlag(flags, "key"),
|
|
14333
17222
|
api: strFlag(flags, "api"),
|
|
@@ -14377,11 +17266,11 @@ ${landedLines(landed)}` : "")
|
|
|
14377
17266
|
return EXIT_OK;
|
|
14378
17267
|
}
|
|
14379
17268
|
async function cmdPull2(argv) {
|
|
14380
|
-
const { positionals, flags } = parseArgs(argv,
|
|
17269
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15, MULTI_FLAGS2);
|
|
14381
17270
|
const dir = positionals[0] ?? ".";
|
|
14382
17271
|
const r = createReporter(flags.json === true);
|
|
14383
17272
|
const result = await pullTake(
|
|
14384
|
-
|
|
17273
|
+
resolve18(dir),
|
|
14385
17274
|
{
|
|
14386
17275
|
key: strFlag(flags, "key"),
|
|
14387
17276
|
api: strFlag(flags, "api"),
|
|
@@ -14513,6 +17402,14 @@ async function run(argv) {
|
|
|
14513
17402
|
return await cmdActions(rest);
|
|
14514
17403
|
case "ingest":
|
|
14515
17404
|
return await cmdIngest(rest);
|
|
17405
|
+
case "check-video":
|
|
17406
|
+
return await cmdCheckVideo(rest);
|
|
17407
|
+
case "join":
|
|
17408
|
+
return await cmdJoin(rest);
|
|
17409
|
+
case "score":
|
|
17410
|
+
return await cmdScore(rest);
|
|
17411
|
+
case "look":
|
|
17412
|
+
return await cmdLook(rest);
|
|
14516
17413
|
default:
|
|
14517
17414
|
throw new UsageError(`unknown command "${cmd}" \u2014 run: vos help`);
|
|
14518
17415
|
}
|
|
@@ -14522,7 +17419,7 @@ async function run(argv) {
|
|
|
14522
17419
|
`);
|
|
14523
17420
|
return EXIT_USAGE;
|
|
14524
17421
|
}
|
|
14525
|
-
if (e instanceof SetupError || e instanceof SetupEnvError) {
|
|
17422
|
+
if (e instanceof SetupError || e instanceof SetupEnvError || e instanceof RecordRefusedError) {
|
|
14526
17423
|
process.stderr.write(`${e.message}
|
|
14527
17424
|
`);
|
|
14528
17425
|
return EXIT_USAGE;
|
|
@@ -14545,7 +17442,7 @@ async function run(argv) {
|
|
|
14545
17442
|
}
|
|
14546
17443
|
}
|
|
14547
17444
|
async function cmdSession(argv) {
|
|
14548
|
-
const { positionals, flags } = parseArgs(argv,
|
|
17445
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
14549
17446
|
const [sub, arg] = positionals;
|
|
14550
17447
|
const r = createReporter(flags.json === true);
|
|
14551
17448
|
const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
|
|
@@ -14581,7 +17478,7 @@ async function cmdSession(argv) {
|
|
|
14581
17478
|
if (!arg) throw new UsageError(usage);
|
|
14582
17479
|
const bad = sessionNameVerdict(arg);
|
|
14583
17480
|
if (bad) throw new UsageError(bad);
|
|
14584
|
-
if (!
|
|
17481
|
+
if (!existsSync27(sessionDir(arg)))
|
|
14585
17482
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
14586
17483
|
const summary3 = await summarizeSession(arg);
|
|
14587
17484
|
const url = strFlag(flags, "url");
|
|
@@ -14676,9 +17573,10 @@ export {
|
|
|
14676
17573
|
convertAgentBrowser,
|
|
14677
17574
|
MULTI_FLAGS2 as MULTI_FLAGS,
|
|
14678
17575
|
HELP,
|
|
17576
|
+
strictFails,
|
|
14679
17577
|
takeBrowserArgs,
|
|
14680
17578
|
takeBackdrop,
|
|
14681
17579
|
verbHelp,
|
|
14682
17580
|
run
|
|
14683
17581
|
};
|
|
14684
|
-
//# sourceMappingURL=chunk-
|
|
17582
|
+
//# sourceMappingURL=chunk-INRUYQ6J.js.map
|