@vosjs/cli 0.77.0 → 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-SB622PZM.js → chunk-INRUYQ6J.js} +3228 -328
- 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-2TY7KEKT.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 +5 -5
- 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-KDIRW72G.js.map +0 -1
- package/dist/chunk-SB622PZM.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-2TY7KEKT.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";
|
|
@@ -2204,9 +2329,11 @@ import { readFile, rm } from "fs/promises";
|
|
|
2204
2329
|
import { tweenRuntimeCode } from "@vosjs/tween/bundle";
|
|
2205
2330
|
import { elementsBundleCode } from "@vosjs/elements/bundle";
|
|
2206
2331
|
import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
|
|
2207
|
-
|
|
2208
|
-
var
|
|
2209
|
-
var
|
|
2332
|
+
import { esmShModules } from "@vosjs/core/addons";
|
|
2333
|
+
var MODULES = esmShModules();
|
|
2334
|
+
var MEDIABUNNY_URL = MODULES.mediabunny;
|
|
2335
|
+
var THREE_URL = MODULES.three;
|
|
2336
|
+
var THREE_ADDONS = MODULES.threeAddons;
|
|
2210
2337
|
function renderPageHtml() {
|
|
2211
2338
|
const importmap = JSON.stringify({
|
|
2212
2339
|
imports: { three: THREE_URL, "three/addons/": THREE_ADDONS }
|
|
@@ -3277,6 +3404,55 @@ var VIDEO_TOKEN = "__VOS_CLI_VIDEO__";
|
|
|
3277
3404
|
function stillSupersample(w, h) {
|
|
3278
3405
|
return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
|
|
3279
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
|
+
}
|
|
3280
3456
|
async function stillsInPage(opts) {
|
|
3281
3457
|
const w = window;
|
|
3282
3458
|
const RW = opts.W * opts.ss;
|
|
@@ -3285,11 +3461,9 @@ async function stillsInPage(opts) {
|
|
|
3285
3461
|
const vblob = await (await fetch(opts.videoUrl)).blob();
|
|
3286
3462
|
const vurl = URL.createObjectURL(vblob);
|
|
3287
3463
|
const code = opts.animationCode.split(opts.token).join(vurl);
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
URL.createObjectURL(
|
|
3291
|
-
new Blob([code], { type: "text/javascript" })
|
|
3292
|
-
)
|
|
3464
|
+
const load = new Function("u", "return import(u)");
|
|
3465
|
+
const mod = await load(
|
|
3466
|
+
URL.createObjectURL(new Blob([code], { type: "text/javascript" }))
|
|
3293
3467
|
);
|
|
3294
3468
|
w.__vos__ = w.__vos__ || {};
|
|
3295
3469
|
w.__vos__.isPaused = true;
|
|
@@ -3365,6 +3539,25 @@ async function stillsInPage(opts) {
|
|
|
3365
3539
|
}
|
|
3366
3540
|
}
|
|
3367
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 = {};
|
|
3368
3561
|
for (let i = 0; i < opts.shots.length; i++) {
|
|
3369
3562
|
const shot = opts.shots[i];
|
|
3370
3563
|
timeline.seek(shot.time, false);
|
|
@@ -3376,14 +3569,56 @@ async function stillsInPage(opts) {
|
|
|
3376
3569
|
}
|
|
3377
3570
|
await waitVideosSettled(8e3);
|
|
3378
3571
|
const png = await toPng();
|
|
3572
|
+
if (sigWanted.has(shot.name)) sigs[shot.name] = signature();
|
|
3379
3573
|
await fetch("/save?name=" + shot.name, { method: "POST", body: png });
|
|
3380
3574
|
w.__progress = (i + 1) / opts.shots.length;
|
|
3381
3575
|
}
|
|
3382
|
-
w.__done = { ok: true, count: opts.shots.length };
|
|
3576
|
+
w.__done = { ok: true, count: opts.shots.length, sigs };
|
|
3383
3577
|
} catch (e) {
|
|
3384
3578
|
w.__error = String(e instanceof Error && e.stack || e);
|
|
3385
3579
|
}
|
|
3386
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
|
+
}
|
|
3387
3622
|
async function framesTake(browser, dir, opts) {
|
|
3388
3623
|
const take = await loadTake(dir);
|
|
3389
3624
|
const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
|
|
@@ -3406,13 +3641,17 @@ async function framesTake(browser, dir, opts) {
|
|
|
3406
3641
|
time: clamp(t),
|
|
3407
3642
|
kind: "time"
|
|
3408
3643
|
}));
|
|
3644
|
+
let skippedZooms;
|
|
3409
3645
|
if (opts.atZooms) {
|
|
3410
|
-
const
|
|
3411
|
-
for (const
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
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;
|
|
3416
3655
|
}
|
|
3417
3656
|
if (opts.atMoments) {
|
|
3418
3657
|
for (const m of await momentsFor(dir, doc)) {
|
|
@@ -3423,16 +3662,19 @@ async function framesTake(browser, dir, opts) {
|
|
|
3423
3662
|
if (opts.atStill)
|
|
3424
3663
|
shots.push({ time: clamp(docStillTime(doc)), kind: "still" });
|
|
3425
3664
|
shots.sort((a, b) => a.time - b.time);
|
|
3426
|
-
if (!shots.length)
|
|
3665
|
+
if (!shots.length && !skippedZooms?.length)
|
|
3427
3666
|
throw new Error(
|
|
3428
3667
|
"no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
|
|
3429
3668
|
);
|
|
3430
3669
|
const outDir = resolve3(opts.outDir ?? join3(dir, "stills"));
|
|
3431
3670
|
await mkdir2(outDir, { recursive: true });
|
|
3671
|
+
const stamp = (i, t) => `${String(i).padStart(2, "0")}-${t.toFixed(2)}s.png`;
|
|
3432
3672
|
const named = shots.map((s, i) => ({
|
|
3433
3673
|
...s,
|
|
3434
|
-
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
|
|
3435
3676
|
}));
|
|
3677
|
+
const zoomShots = named.filter((n) => n.kind === "zoom");
|
|
3436
3678
|
const server = await startTakeServer(dir, {
|
|
3437
3679
|
"/render.html": renderPageHtml()
|
|
3438
3680
|
});
|
|
@@ -3440,49 +3682,73 @@ async function framesTake(browser, dir, opts) {
|
|
|
3440
3682
|
const context2 = await browser.newContext({
|
|
3441
3683
|
viewport: { width: width * ss, height: height * ss }
|
|
3442
3684
|
});
|
|
3685
|
+
const videoUrl = `/${take.paths.recordingName}`;
|
|
3443
3686
|
try {
|
|
3444
|
-
const
|
|
3445
|
-
page.on("console", (m) => {
|
|
3446
|
-
if (m.type() === "error")
|
|
3447
|
-
process.stderr.write(` [stills page] ${m.text()}
|
|
3448
|
-
`);
|
|
3449
|
-
});
|
|
3450
|
-
await page.addInitScript(() => {
|
|
3451
|
-
;
|
|
3452
|
-
globalThis.__name = (f) => f;
|
|
3453
|
-
});
|
|
3454
|
-
await page.goto(`${server.base}/render.html`);
|
|
3455
|
-
await page.waitForFunction("window.__pageReady__ === true");
|
|
3456
|
-
void page.evaluate(stillsInPage, {
|
|
3687
|
+
const sigs = named.length ? await captureShots(context2, server.base, {
|
|
3457
3688
|
animationCode,
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
H: height,
|
|
3689
|
+
videoUrl,
|
|
3690
|
+
width,
|
|
3691
|
+
height,
|
|
3462
3692
|
ss,
|
|
3463
|
-
shots: named.map(({ time, name }) => ({ time, name }))
|
|
3464
|
-
|
|
3465
|
-
});
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
(
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
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
|
+
}
|
|
3473
3713
|
const frames = [];
|
|
3474
3714
|
for (const n of named) {
|
|
3475
3715
|
const from = join3(dir, n.name);
|
|
3476
3716
|
const to = join3(outDir, n.name);
|
|
3477
3717
|
if (from !== to) await rename2(from, to);
|
|
3478
|
-
|
|
3718
|
+
const frame = {
|
|
3479
3719
|
file: to,
|
|
3480
3720
|
time: n.time,
|
|
3481
3721
|
kind: n.kind,
|
|
3482
|
-
...n.momentId ? { momentId: n.momentId } : {}
|
|
3483
|
-
|
|
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);
|
|
3484
3743
|
}
|
|
3485
|
-
return {
|
|
3744
|
+
return {
|
|
3745
|
+
frames,
|
|
3746
|
+
width,
|
|
3747
|
+
height,
|
|
3748
|
+
duration,
|
|
3749
|
+
outDir,
|
|
3750
|
+
...skippedZooms ? { skippedZooms } : {}
|
|
3751
|
+
};
|
|
3486
3752
|
} finally {
|
|
3487
3753
|
await context2.close();
|
|
3488
3754
|
server.close();
|
|
@@ -3520,7 +3786,8 @@ async function writeIndexJson(result) {
|
|
|
3520
3786
|
width: result.width,
|
|
3521
3787
|
height: result.height,
|
|
3522
3788
|
duration: result.duration,
|
|
3523
|
-
frames: result.frames
|
|
3789
|
+
frames: result.frames,
|
|
3790
|
+
...result.skippedZooms?.length ? { skippedZooms: result.skippedZooms } : {}
|
|
3524
3791
|
},
|
|
3525
3792
|
null,
|
|
3526
3793
|
2
|
|
@@ -3668,8 +3935,8 @@ import {
|
|
|
3668
3935
|
// src/plugin/moments.ts
|
|
3669
3936
|
import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
|
|
3670
3937
|
var STEP_SETTLE_SECONDS = 0.4;
|
|
3671
|
-
function stepOutputTime(rated, step,
|
|
3672
|
-
const src = step.tEnd +
|
|
3938
|
+
function stepOutputTime(rated, step, settle2) {
|
|
3939
|
+
const src = step.tEnd + settle2;
|
|
3673
3940
|
const ext = spanOutputExtent3(rated, src, src + 1e-3);
|
|
3674
3941
|
if (!ext) return null;
|
|
3675
3942
|
return ext.start;
|
|
@@ -3685,6 +3952,20 @@ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
|
|
|
3685
3952
|
var BED_ID = "bed";
|
|
3686
3953
|
var CLICK_ID_PREFIX = "click-";
|
|
3687
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
|
+
}
|
|
3688
3969
|
var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
|
|
3689
3970
|
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3690
3971
|
var TRANSITION_CLIP_MIN = 1;
|
|
@@ -3765,7 +4046,7 @@ function templateWords(words2) {
|
|
|
3765
4046
|
if (sub && sub !== headline) out["endcard-sub"] = sub;
|
|
3766
4047
|
return out;
|
|
3767
4048
|
}
|
|
3768
|
-
function
|
|
4049
|
+
function resolveAnchor2(doc, at2) {
|
|
3769
4050
|
if (typeof at2 !== "object") return at2;
|
|
3770
4051
|
const steps = doc.source.meta.steps ?? [];
|
|
3771
4052
|
const step = steps.find((s) => s.id === at2.step || String(s.step) === at2.step);
|
|
@@ -3810,7 +4091,7 @@ function proposeMotion(input, opts) {
|
|
|
3810
4091
|
const tWords = templateWords(words2);
|
|
3811
4092
|
const keys = opts.mark ? { "stage-mark": opts.mark.key, "endcard-markimg": opts.mark.key } : void 0;
|
|
3812
4093
|
for (const t of templates) {
|
|
3813
|
-
const at2 =
|
|
4094
|
+
const at2 = resolveAnchor2(doc, t.at);
|
|
3814
4095
|
if (at2 === null) {
|
|
3815
4096
|
skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
|
|
3816
4097
|
continue;
|
|
@@ -3891,18 +4172,14 @@ function proposeMotion(input, opts) {
|
|
|
3891
4172
|
if (!step || step.skipped) continue;
|
|
3892
4173
|
const t = stepOutputTime(rated, step, 0.2);
|
|
3893
4174
|
if (t === null || t < 0 || t > footage - 1) continue;
|
|
3894
|
-
captionClips.push(
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
anim: { enter: "rise", exit: "fade" },
|
|
3903
|
-
align: "center",
|
|
3904
|
-
box: { color: "rgba(17,17,17,0.72)" }
|
|
3905
|
-
});
|
|
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
|
+
);
|
|
3906
4183
|
}
|
|
3907
4184
|
if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
|
|
3908
4185
|
}
|
|
@@ -5330,8 +5607,24 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5330
5607
|
|
|
5331
5608
|
// src/plugin/recorder.ts
|
|
5332
5609
|
import { writeFileSync } from "fs";
|
|
5610
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
5333
5611
|
import { join as join8 } from "path";
|
|
5334
|
-
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
|
+
}
|
|
5335
5628
|
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
5336
5629
|
const dry = opts.dryRun === true;
|
|
5337
5630
|
const result = await recordTakeCore(
|
|
@@ -5346,6 +5639,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5346
5639
|
log,
|
|
5347
5640
|
{
|
|
5348
5641
|
...opts,
|
|
5642
|
+
outputs: outputsSink(paths.dir),
|
|
5349
5643
|
env: process.env,
|
|
5350
5644
|
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
|
|
5351
5645
|
}
|
|
@@ -5359,6 +5653,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
5359
5653
|
return result;
|
|
5360
5654
|
}
|
|
5361
5655
|
|
|
5656
|
+
// src/plugin/run.ts
|
|
5657
|
+
import {
|
|
5658
|
+
RecordRefusedError,
|
|
5659
|
+
guardVerdict,
|
|
5660
|
+
guardedSteps,
|
|
5661
|
+
hostedRefusals
|
|
5662
|
+
} from "@vosjs/render-core/record";
|
|
5663
|
+
|
|
5362
5664
|
// src/plugin/wall.ts
|
|
5363
5665
|
import {
|
|
5364
5666
|
WALL_PROBE,
|
|
@@ -5453,10 +5755,10 @@ async function openSession(name, url, opts = { log: () => {
|
|
|
5453
5755
|
opts.log(
|
|
5454
5756
|
`a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
|
|
5455
5757
|
);
|
|
5456
|
-
return new Promise((
|
|
5758
|
+
return new Promise((resolve19, reject) => {
|
|
5457
5759
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
5458
5760
|
child.on("error", reject);
|
|
5459
|
-
child.on("exit", (code) =>
|
|
5761
|
+
child.on("exit", (code) => resolve19({ code }));
|
|
5460
5762
|
});
|
|
5461
5763
|
}
|
|
5462
5764
|
async function withSession(name, fn, opts = {}) {
|
|
@@ -5605,9 +5907,14 @@ import {
|
|
|
5605
5907
|
withBackdrop,
|
|
5606
5908
|
migrateMotion as migrateMotion2
|
|
5607
5909
|
} from "@vosjs/studio-core";
|
|
5910
|
+
import { splitSegmentsAtSkips } from "@vosjs/render-core/record";
|
|
5608
5911
|
|
|
5609
5912
|
// src/plugin/reuse.ts
|
|
5610
|
-
import {
|
|
5913
|
+
import {
|
|
5914
|
+
docFreezes,
|
|
5915
|
+
ratedSegments as ratedSegments6,
|
|
5916
|
+
resolveAnchor as resolveLayerAnchor
|
|
5917
|
+
} from "@vosjs/studio-core";
|
|
5611
5918
|
var MIN_SPAN = 0.15;
|
|
5612
5919
|
var AFTER_STEP = 1;
|
|
5613
5920
|
var LAYER_AFTER_STEP = 5;
|
|
@@ -5683,7 +5990,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
|
|
|
5683
5990
|
};
|
|
5684
5991
|
return { map, unmatched, matched };
|
|
5685
5992
|
}
|
|
5686
|
-
function
|
|
5993
|
+
function resolveAnchor3(anchor, newSteps) {
|
|
5687
5994
|
const step = stepByRef(newSteps, anchor.step);
|
|
5688
5995
|
if (!step || step.skipped) return null;
|
|
5689
5996
|
const base = anchor.at === "end" ? step.tEnd : step.tStart;
|
|
@@ -5703,7 +6010,7 @@ function framed(span) {
|
|
|
5703
6010
|
}
|
|
5704
6011
|
function placeMoment(t, anchor, ctx) {
|
|
5705
6012
|
if (anchor) {
|
|
5706
|
-
const at2 =
|
|
6013
|
+
const at2 = resolveAnchor3(anchor, ctx.newSteps);
|
|
5707
6014
|
if (at2 === null) return { gone: String(anchor.step) };
|
|
5708
6015
|
ctx.report.anchored++;
|
|
5709
6016
|
return { at: at2, via: "anchor" };
|
|
@@ -5871,6 +6178,17 @@ function retimeOverlays(prev, next, report) {
|
|
|
5871
6178
|
return { ...o, start: +start.toFixed(3) };
|
|
5872
6179
|
};
|
|
5873
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
|
+
}
|
|
5874
6192
|
const pin = o.pin;
|
|
5875
6193
|
if (pin?.step !== void 0) {
|
|
5876
6194
|
const was = stepByRef(oldSteps, pin.step);
|
|
@@ -6001,7 +6319,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6001
6319
|
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
6002
6320
|
doc = copyStyle(prev, doc);
|
|
6003
6321
|
const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
|
|
6004
|
-
doc.segments = rt.segments;
|
|
6322
|
+
doc.segments = splitSegmentsAtSkips(rt.segments, meta.skips);
|
|
6005
6323
|
if (rt.freeze.length) doc.freeze = rt.freeze;
|
|
6006
6324
|
if (rt.rejected.length) doc.rejected = rt.rejected;
|
|
6007
6325
|
const manualZoom = rt.zoom;
|
|
@@ -6099,6 +6417,7 @@ async function planTake(dir, opts = {}) {
|
|
|
6099
6417
|
meta
|
|
6100
6418
|
};
|
|
6101
6419
|
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
6420
|
+
doc.segments = splitSegmentsAtSkips(doc.segments, meta.skips);
|
|
6102
6421
|
if (opts.style) {
|
|
6103
6422
|
const applied = applyStyle(opts.style.doc, doc, opts);
|
|
6104
6423
|
doc = applied.doc;
|
|
@@ -6210,7 +6529,7 @@ async function writeScriptBeside(dir, doc) {
|
|
|
6210
6529
|
}
|
|
6211
6530
|
|
|
6212
6531
|
// src/plugin/folder.ts
|
|
6213
|
-
import { mkdir as
|
|
6532
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
6214
6533
|
import { join as join12 } from "path";
|
|
6215
6534
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
6216
6535
|
import { migrateHostedDoc } from "@vosjs/studio-core";
|
|
@@ -6419,7 +6738,7 @@ async function cmdPull(argv) {
|
|
|
6419
6738
|
const payload = res.body;
|
|
6420
6739
|
const safeName = (v, id) => safePathName((v ?? "").toLowerCase(), id);
|
|
6421
6740
|
const out = strFlag(flags, "out") ?? safeName(folder.slug, folder.id);
|
|
6422
|
-
await
|
|
6741
|
+
await mkdir6(join12(out, "recipes"), { recursive: true });
|
|
6423
6742
|
const lines = [];
|
|
6424
6743
|
const recipes = [];
|
|
6425
6744
|
for (const rec of payload.recipes) {
|
|
@@ -6429,7 +6748,7 @@ async function cmdPull(argv) {
|
|
|
6429
6748
|
}
|
|
6430
6749
|
for (const rec of payload.inheritedRecipes) {
|
|
6431
6750
|
const from = safeName(rec.folderSlug ?? "inherited", "inherited");
|
|
6432
|
-
await
|
|
6751
|
+
await mkdir6(join12(out, "recipes", "_inherited", from), { recursive: true });
|
|
6433
6752
|
const path = join12(
|
|
6434
6753
|
out,
|
|
6435
6754
|
"recipes",
|
|
@@ -6446,7 +6765,7 @@ async function cmdPull(argv) {
|
|
|
6446
6765
|
const members = [];
|
|
6447
6766
|
for (const v of payload.voses) {
|
|
6448
6767
|
const dir = join12(out, "members", safeName(v.slug || v.title, v.id));
|
|
6449
|
-
await
|
|
6768
|
+
await mkdir6(dir, { recursive: true });
|
|
6450
6769
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
6451
6770
|
if (cfg.status !== 200) {
|
|
6452
6771
|
lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
|
|
@@ -6996,7 +7315,7 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
6996
7315
|
|
|
6997
7316
|
// src/plugin/program.ts
|
|
6998
7317
|
import { existsSync as existsSync13 } from "fs";
|
|
6999
|
-
import { mkdir as
|
|
7318
|
+
import { mkdir as mkdir7, readFile as readFile11 } from "fs/promises";
|
|
7000
7319
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7001
7320
|
import { basename as basename3, dirname as dirname2, join as join14 } from "path";
|
|
7002
7321
|
import {
|
|
@@ -7215,7 +7534,7 @@ async function cmdFetch(argv) {
|
|
|
7215
7534
|
throw new Error(apiError(`fetch config for ${vosId}`, cfg));
|
|
7216
7535
|
const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
|
|
7217
7536
|
const out = strFlag(flags, "out") ?? safePathName(slug, vosId);
|
|
7218
|
-
await
|
|
7537
|
+
await mkdir7(out, { recursive: true });
|
|
7219
7538
|
await writeFileSafe(
|
|
7220
7539
|
join14(out, "config.json"),
|
|
7221
7540
|
JSON.stringify(cfg.body.config, null, 2)
|
|
@@ -10869,13 +11188,13 @@ function parseLlmsTxt(text) {
|
|
|
10869
11188
|
const h1 = /^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? null;
|
|
10870
11189
|
const block = /^>\s*.+(?:\n>\s*.*)*/m.exec(text)?.[0];
|
|
10871
11190
|
const joined = block ? block.split("\n").map((line) => line.replace(/^>\s*/, "").trim()).filter(Boolean).join(" ") : "";
|
|
10872
|
-
const
|
|
11191
|
+
const quote2 = joined ? /^(.*?[.!?])(?:\s|$)/.exec(joined)?.[1] ?? joined : null;
|
|
10873
11192
|
let name = h1;
|
|
10874
11193
|
if (h1) {
|
|
10875
11194
|
const split = /^(.+?)\s+[—–-]\s+(.+)$/.exec(h1);
|
|
10876
11195
|
if (split) name = split[1].trim();
|
|
10877
11196
|
}
|
|
10878
|
-
return { name, claim:
|
|
11197
|
+
return { name, claim: quote2 ?? (h1 && h1 !== name ? h1 : null) };
|
|
10879
11198
|
}
|
|
10880
11199
|
var WITNESS_IN_PAGE = `(() => {
|
|
10881
11200
|
const cs = (el) => el ? getComputedStyle(el) : null
|
|
@@ -11236,7 +11555,7 @@ async function recordHosted(input) {
|
|
|
11236
11555
|
}
|
|
11237
11556
|
|
|
11238
11557
|
// src/plugin/run.ts
|
|
11239
|
-
import { urlSchemeProblem } from "@vosjs/render-core/record";
|
|
11558
|
+
import { urlSchemeProblem as urlSchemeProblem2 } from "@vosjs/render-core/record";
|
|
11240
11559
|
|
|
11241
11560
|
// src/plugin/agentBrowser.ts
|
|
11242
11561
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
@@ -11286,18 +11605,18 @@ function parseAgentBrowserLog(text) {
|
|
|
11286
11605
|
function splitCommand(command) {
|
|
11287
11606
|
const out = [];
|
|
11288
11607
|
let cur = "";
|
|
11289
|
-
let
|
|
11608
|
+
let quote2 = null;
|
|
11290
11609
|
let has = false;
|
|
11291
11610
|
for (let i = 0; i < command.length; i++) {
|
|
11292
11611
|
const c = command[i];
|
|
11293
|
-
if (
|
|
11294
|
-
if (c ===
|
|
11612
|
+
if (quote2) {
|
|
11613
|
+
if (c === quote2) quote2 = null;
|
|
11295
11614
|
else if (c === "\\" && i + 1 < command.length) cur += command[++i];
|
|
11296
11615
|
else cur += c;
|
|
11297
11616
|
continue;
|
|
11298
11617
|
}
|
|
11299
11618
|
if (c === '"' || c === "'") {
|
|
11300
|
-
|
|
11619
|
+
quote2 = c;
|
|
11301
11620
|
has = true;
|
|
11302
11621
|
continue;
|
|
11303
11622
|
}
|
|
@@ -11341,8 +11660,8 @@ var quoteAttr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
|
11341
11660
|
function hrefSelector(href) {
|
|
11342
11661
|
try {
|
|
11343
11662
|
const u = new URL(href);
|
|
11344
|
-
const
|
|
11345
|
-
return `a[href=${quoteAttr(
|
|
11663
|
+
const relative6 = `${u.pathname}${u.search}${u.hash}`;
|
|
11664
|
+
return `a[href=${quoteAttr(relative6)}], a[href=${quoteAttr(href)}]`;
|
|
11346
11665
|
} catch {
|
|
11347
11666
|
return `a[href=${quoteAttr(href)}]`;
|
|
11348
11667
|
}
|
|
@@ -11719,8 +12038,8 @@ async function cmdActions(argv) {
|
|
|
11719
12038
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
11720
12039
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
11721
12040
|
}
|
|
11722
|
-
const { readFile:
|
|
11723
|
-
const text = await
|
|
12041
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
12042
|
+
const text = await readFile16(resolve12(input), "utf8");
|
|
11724
12043
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
11725
12044
|
for (const p of problems) r.log(` ${p}`);
|
|
11726
12045
|
const result = convertAgentBrowser(records, {
|
|
@@ -11757,9 +12076,9 @@ function summary2(result) {
|
|
|
11757
12076
|
}
|
|
11758
12077
|
async function cmdActionsScript(input, flags) {
|
|
11759
12078
|
const r = createReporter(flags.json === true);
|
|
11760
|
-
const { readFile:
|
|
11761
|
-
const { shotList, formatShotList } = await import("./shotList-
|
|
11762
|
-
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"));
|
|
11763
12082
|
const errors = validateActions(raw);
|
|
11764
12083
|
if (errors.length) throw new UsageError(`${input}:
|
|
11765
12084
|
${errors.join("\n ")}`);
|
|
@@ -11770,7 +12089,7 @@ async function cmdActionsScript(input, flags) {
|
|
|
11770
12089
|
|
|
11771
12090
|
// src/plugin/ingest.ts
|
|
11772
12091
|
import { existsSync as existsSync20 } from "fs";
|
|
11773
|
-
import { copyFile, mkdir as
|
|
12092
|
+
import { copyFile, mkdir as mkdir8, readFile as readFile12, stat as stat2 } from "fs/promises";
|
|
11774
12093
|
import { basename as basename9, join as join22, resolve as resolve13 } from "path";
|
|
11775
12094
|
|
|
11776
12095
|
// src/plugin/traceInflate.ts
|
|
@@ -11872,7 +12191,11 @@ async function copyRecording(src, dest, ext) {
|
|
|
11872
12191
|
target
|
|
11873
12192
|
});
|
|
11874
12193
|
try {
|
|
11875
|
-
const conversion = await Conversion.init({
|
|
12194
|
+
const conversion = await Conversion.init({
|
|
12195
|
+
input,
|
|
12196
|
+
output,
|
|
12197
|
+
showWarnings: false
|
|
12198
|
+
});
|
|
11876
12199
|
if (!conversion.isValid || conversion.discardedTracks.length > 0) {
|
|
11877
12200
|
await conversion.cancel?.();
|
|
11878
12201
|
await copyFile(src, dest);
|
|
@@ -11958,7 +12281,7 @@ async function cmdIngest(argv) {
|
|
|
11958
12281
|
`note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
|
|
11959
12282
|
);
|
|
11960
12283
|
}
|
|
11961
|
-
await
|
|
12284
|
+
await mkdir8(outDir, { recursive: true });
|
|
11962
12285
|
const paths = await ensureTakeDir(outDir);
|
|
11963
12286
|
const recording = join22(outDir, `recording.${probed.ext}`);
|
|
11964
12287
|
r.log("copying the recording\u2026");
|
|
@@ -12024,115 +12347,2465 @@ async function cmdIngest(argv) {
|
|
|
12024
12347
|
return EXIT_OK;
|
|
12025
12348
|
}
|
|
12026
12349
|
|
|
12027
|
-
// src/plugin/
|
|
12028
|
-
import {
|
|
12029
|
-
import {
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
readFileSync as readFileSync11,
|
|
12034
|
-
rmSync as rmSync3,
|
|
12035
|
-
writeFileSync as writeFileSync6
|
|
12036
|
-
} from "fs";
|
|
12037
|
-
import { homedir as homedir2 } from "os";
|
|
12038
|
-
import { join as join24 } from "path";
|
|
12039
|
-
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";
|
|
12040
12356
|
|
|
12041
|
-
// src/plugin/
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
var
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
var
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
};
|
|
12056
|
-
var HOME_DIRS = {
|
|
12057
|
-
claude: [".claude"],
|
|
12058
|
-
cursor: [".cursor"],
|
|
12059
|
-
codex: [".codex", ".agents"],
|
|
12060
|
-
copilot: [".copilot"]
|
|
12061
|
-
};
|
|
12062
|
-
function detectAgents(root, opts = {}) {
|
|
12063
|
-
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) {
|
|
12064
12371
|
const out = [];
|
|
12065
|
-
for (
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
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
|
+
});
|
|
12077
12389
|
}
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
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++;
|
|
12085
12401
|
}
|
|
12086
12402
|
return out;
|
|
12087
12403
|
}
|
|
12088
|
-
function
|
|
12089
|
-
const
|
|
12090
|
-
|
|
12091
|
-
|
|
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;
|
|
12092
12426
|
return {
|
|
12093
|
-
|
|
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
|
|
12094
12432
|
};
|
|
12095
|
-
|
|
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;
|
|
12096
12464
|
return {
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
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
|
+
}
|
|
12102
12504
|
};
|
|
12103
12505
|
}
|
|
12104
|
-
function
|
|
12105
|
-
return
|
|
12106
|
-
}
|
|
12107
|
-
function fallbackSkillsHome(root) {
|
|
12108
|
-
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);
|
|
12109
12509
|
}
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
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
|
+
});
|
|
12115
12553
|
}
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
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
|
+
};
|
|
12119
12562
|
}
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
${
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
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);
|
|
12136
14809
|
const end = current.indexOf(RULES_END);
|
|
12137
14810
|
let text;
|
|
12138
14811
|
if (begin !== -1 && end !== -1 && end > begin) {
|
|
@@ -12148,10 +14821,10 @@ ${RULES_BLOCK}
|
|
|
12148
14821
|
}
|
|
12149
14822
|
return { text, changed: text !== current };
|
|
12150
14823
|
}
|
|
12151
|
-
function rulesFileFor(root, exists =
|
|
12152
|
-
const agents =
|
|
14824
|
+
function rulesFileFor(root, exists = existsSync25) {
|
|
14825
|
+
const agents = join27(root, "AGENTS.md");
|
|
12153
14826
|
if (exists(agents)) return agents;
|
|
12154
|
-
const claude =
|
|
14827
|
+
const claude = join27(root, "CLAUDE.md");
|
|
12155
14828
|
if (exists(claude)) return claude;
|
|
12156
14829
|
return agents;
|
|
12157
14830
|
}
|
|
@@ -12160,14 +14833,14 @@ function chromeCandidates(platform, env) {
|
|
|
12160
14833
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
12161
14834
|
if (platform === "win32")
|
|
12162
14835
|
return [
|
|
12163
|
-
|
|
14836
|
+
join27(
|
|
12164
14837
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
12165
14838
|
"Google",
|
|
12166
14839
|
"Chrome",
|
|
12167
14840
|
"Application",
|
|
12168
14841
|
"chrome.exe"
|
|
12169
14842
|
),
|
|
12170
|
-
|
|
14843
|
+
join27(
|
|
12171
14844
|
env["LOCALAPPDATA"] ?? "",
|
|
12172
14845
|
"Google",
|
|
12173
14846
|
"Chrome",
|
|
@@ -12253,27 +14926,27 @@ function doctorReport(facts) {
|
|
|
12253
14926
|
}
|
|
12254
14927
|
function bundledSkillsVersion(dir) {
|
|
12255
14928
|
try {
|
|
12256
|
-
const v = readFileSync10(
|
|
14929
|
+
const v = readFileSync10(join27(dir, "VERSION"), "utf8").trim();
|
|
12257
14930
|
return /^\d+\.\d+\.\d+$/.test(v) ? v : null;
|
|
12258
14931
|
} catch {
|
|
12259
14932
|
return null;
|
|
12260
14933
|
}
|
|
12261
14934
|
}
|
|
12262
14935
|
function bundledSkillsDir(from) {
|
|
12263
|
-
let dir =
|
|
14936
|
+
let dir = dirname10(from);
|
|
12264
14937
|
for (let i = 0; i < 6; i++) {
|
|
12265
|
-
const pkg =
|
|
12266
|
-
if (
|
|
14938
|
+
const pkg = join27(dir, "package.json");
|
|
14939
|
+
if (existsSync25(pkg)) {
|
|
12267
14940
|
try {
|
|
12268
14941
|
const name = JSON.parse(readFileSync10(pkg, "utf8")).name;
|
|
12269
14942
|
if (name === "@vosjs/cli") {
|
|
12270
|
-
const skills =
|
|
12271
|
-
return
|
|
14943
|
+
const skills = join27(dir, "skills");
|
|
14944
|
+
return existsSync25(skills) && statSync8(skills).isDirectory() ? skills : null;
|
|
12272
14945
|
}
|
|
12273
14946
|
} catch {
|
|
12274
14947
|
}
|
|
12275
14948
|
}
|
|
12276
|
-
const up =
|
|
14949
|
+
const up = dirname10(dir);
|
|
12277
14950
|
if (up === dir) break;
|
|
12278
14951
|
dir = up;
|
|
12279
14952
|
}
|
|
@@ -12281,7 +14954,7 @@ function bundledSkillsDir(from) {
|
|
|
12281
14954
|
}
|
|
12282
14955
|
|
|
12283
14956
|
// src/plugin/machineCmd.ts
|
|
12284
|
-
var
|
|
14957
|
+
var BOOLEAN_FLAGS14 = /* @__PURE__ */ new Set([
|
|
12285
14958
|
"json",
|
|
12286
14959
|
"help",
|
|
12287
14960
|
"global",
|
|
@@ -12304,7 +14977,7 @@ function npx() {
|
|
|
12304
14977
|
function bundledChromium() {
|
|
12305
14978
|
try {
|
|
12306
14979
|
const p = chromium2.executablePath();
|
|
12307
|
-
return p &&
|
|
14980
|
+
return p && existsSync26(p) ? p : null;
|
|
12308
14981
|
} catch {
|
|
12309
14982
|
return null;
|
|
12310
14983
|
}
|
|
@@ -12313,7 +14986,7 @@ function probe2() {
|
|
|
12313
14986
|
return probeBrowser({
|
|
12314
14987
|
platform: process.platform,
|
|
12315
14988
|
env: process.env,
|
|
12316
|
-
exists:
|
|
14989
|
+
exists: existsSync26,
|
|
12317
14990
|
bundled: bundledChromium
|
|
12318
14991
|
});
|
|
12319
14992
|
}
|
|
@@ -12364,7 +15037,7 @@ async function doctorFacts(opts) {
|
|
|
12364
15037
|
};
|
|
12365
15038
|
}
|
|
12366
15039
|
async function cmdSetup(argv) {
|
|
12367
|
-
const { flags } = parseArgs(argv,
|
|
15040
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12368
15041
|
const r = reporterFor(flags);
|
|
12369
15042
|
const root = process.cwd();
|
|
12370
15043
|
const global = flags.global === true;
|
|
@@ -12398,7 +15071,7 @@ async function cmdSetup(argv) {
|
|
|
12398
15071
|
const dest = skillsDirFor(home);
|
|
12399
15072
|
mkdirSync6(dest, { recursive: true });
|
|
12400
15073
|
for (const name of installedSkills(dir)) {
|
|
12401
|
-
cpSync(
|
|
15074
|
+
cpSync(join28(dir, name), join28(dest, name), { recursive: true });
|
|
12402
15075
|
}
|
|
12403
15076
|
}
|
|
12404
15077
|
};
|
|
@@ -12466,7 +15139,7 @@ async function cmdSetup(argv) {
|
|
|
12466
15139
|
let rules = null;
|
|
12467
15140
|
if (!noRules) {
|
|
12468
15141
|
const file = rulesFileFor(root);
|
|
12469
|
-
const existing =
|
|
15142
|
+
const existing = existsSync26(file) ? readFileSync11(file, "utf8") : null;
|
|
12470
15143
|
const { text, changed } = applyRulesBlock(existing);
|
|
12471
15144
|
if (changed) writeFileSync6(file, text);
|
|
12472
15145
|
rules = { file, changed };
|
|
@@ -12497,7 +15170,7 @@ Next: ${report.next_step}`
|
|
|
12497
15170
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
12498
15171
|
}
|
|
12499
15172
|
async function cmdDoctor(argv) {
|
|
12500
|
-
const { flags } = parseArgs(argv,
|
|
15173
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12501
15174
|
const r = reporterFor(flags);
|
|
12502
15175
|
const root = process.cwd();
|
|
12503
15176
|
const homes = detectAgents(root, { home: homedir2(), global: true });
|
|
@@ -12516,7 +15189,7 @@ Next: ${report.next_step}`
|
|
|
12516
15189
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
12517
15190
|
}
|
|
12518
15191
|
async function cmdWhoami(argv) {
|
|
12519
|
-
const { flags } = parseArgs(argv,
|
|
15192
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12520
15193
|
const r = reporterFor(flags);
|
|
12521
15194
|
const origin = platformOrigin({
|
|
12522
15195
|
origin: strFlag(flags, "origin"),
|
|
@@ -12578,9 +15251,9 @@ async function cmdWhoami(argv) {
|
|
|
12578
15251
|
return EXIT_OK;
|
|
12579
15252
|
}
|
|
12580
15253
|
function cmdLogout(argv) {
|
|
12581
|
-
const { flags } = parseArgs(argv,
|
|
15254
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
12582
15255
|
const r = reporterFor(flags);
|
|
12583
|
-
const had =
|
|
15256
|
+
const had = existsSync26(CREDENTIALS_PATH);
|
|
12584
15257
|
rmSync3(CREDENTIALS_PATH, { force: true });
|
|
12585
15258
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
12586
15259
|
r.done(
|
|
@@ -12596,7 +15269,7 @@ function cmdLogout(argv) {
|
|
|
12596
15269
|
}
|
|
12597
15270
|
|
|
12598
15271
|
// src/plugin/run.ts
|
|
12599
|
-
var
|
|
15272
|
+
var BOOLEAN_FLAGS15 = /* @__PURE__ */ new Set([
|
|
12600
15273
|
"json",
|
|
12601
15274
|
"help",
|
|
12602
15275
|
"wait",
|
|
@@ -12620,19 +15293,29 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
|
|
|
12620
15293
|
"allow-wall",
|
|
12621
15294
|
"hosted",
|
|
12622
15295
|
"claimable",
|
|
12623
|
-
"share"
|
|
15296
|
+
"share",
|
|
15297
|
+
"verbose"
|
|
15298
|
+
]);
|
|
15299
|
+
var MULTI_FLAGS2 = /* @__PURE__ */ new Set([
|
|
15300
|
+
"set",
|
|
15301
|
+
"override",
|
|
15302
|
+
"browser-arg",
|
|
15303
|
+
"header",
|
|
15304
|
+
"confirm"
|
|
12624
15305
|
]);
|
|
12625
|
-
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
|
|
12626
15306
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
12627
15307
|
|
|
12628
15308
|
Take pipeline
|
|
12629
|
-
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]
|
|
12630
|
-
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]
|
|
12631
15311
|
vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
|
|
12632
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
|
|
12633
|
-
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]
|
|
12634
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]
|
|
12635
|
-
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
|
|
12636
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]
|
|
12637
15320
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
12638
15321
|
vos brand <url> [--out BRAND.md] [--json]
|
|
@@ -12642,6 +15325,10 @@ Take pipeline
|
|
|
12642
15325
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
12643
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]
|
|
12644
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
|
|
12645
15332
|
vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
|
|
12646
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
|
|
12647
15334
|
vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
|
|
@@ -12650,6 +15337,8 @@ Take pipeline
|
|
|
12650
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
|
|
12651
15338
|
vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--threshold 0.95] [--out compare] [--json]
|
|
12652
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
|
|
12653
15342
|
vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
|
|
12654
15343
|
vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
|
|
12655
15344
|
vos session list | rm <name> what exists and how old; delete one
|
|
@@ -12798,11 +15487,25 @@ reuse block NAMES whatever could not follow \u2014 fix by exception, never
|
|
|
12798
15487
|
re-cut from scratch. Give steps ids in actions.json so re-timing survives
|
|
12799
15488
|
script edits.
|
|
12800
15489
|
|
|
12801
|
-
record --strict exits 2 when any selector was skipped,
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
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.
|
|
12806
15509
|
frames captures PNG stills at OUTPUT times (seconds or % of duration);
|
|
12807
15510
|
--at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
|
|
12808
15511
|
the agent's eyes (--size WxH for an exact size). render
|
|
@@ -12920,7 +15623,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
12920
15623
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
12921
15624
|
`;
|
|
12922
15625
|
async function loadActions(file) {
|
|
12923
|
-
const raw = JSON.parse(await
|
|
15626
|
+
const raw = JSON.parse(await readFile15(file, "utf8"));
|
|
12924
15627
|
const errors = validateActions(raw);
|
|
12925
15628
|
if (errors.length)
|
|
12926
15629
|
throw new UsageError(`invalid actions file:
|
|
@@ -12949,7 +15652,10 @@ async function maxDuration(flags, r) {
|
|
|
12949
15652
|
}
|
|
12950
15653
|
function strictReason(rec) {
|
|
12951
15654
|
const parts = [`${rec.skipped.length} skipped step(s)`];
|
|
12952
|
-
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`);
|
|
12953
15659
|
if (rec.capped) parts.push("stopped at --max-duration");
|
|
12954
15660
|
const missed = maskMisses(rec);
|
|
12955
15661
|
if (missed.length)
|
|
@@ -12958,6 +15664,15 @@ function strictReason(rec) {
|
|
|
12958
15664
|
);
|
|
12959
15665
|
return parts.join(" + ");
|
|
12960
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
|
+
}
|
|
12961
15676
|
function maskMisses(rec) {
|
|
12962
15677
|
return (rec.masks ?? []).filter((m) => m.hits === 0).map((m) => m.selector);
|
|
12963
15678
|
}
|
|
@@ -12968,6 +15683,53 @@ function exposureNote(rec) {
|
|
|
12968
15683
|
${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
|
|
12969
15684
|
${EXPOSURE_ADVICE}`;
|
|
12970
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
|
+
}
|
|
12971
15733
|
function takeHeaders(multi) {
|
|
12972
15734
|
try {
|
|
12973
15735
|
return parseHeaders(multi.header);
|
|
@@ -12982,8 +15744,8 @@ function takeBrowserArgs(multi) {
|
|
|
12982
15744
|
function takeStorageState(flags) {
|
|
12983
15745
|
const raw = strFlag(flags, "storage-state");
|
|
12984
15746
|
if (!raw) return void 0;
|
|
12985
|
-
const path =
|
|
12986
|
-
if (!
|
|
15747
|
+
const path = resolve18(raw);
|
|
15748
|
+
if (!existsSync27(path))
|
|
12987
15749
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
12988
15750
|
return path;
|
|
12989
15751
|
}
|
|
@@ -12996,7 +15758,7 @@ function takeSessionName(flags, storageState) {
|
|
|
12996
15758
|
throw new UsageError(
|
|
12997
15759
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
12998
15760
|
);
|
|
12999
|
-
if (!
|
|
15761
|
+
if (!existsSync27(sessionDir(name)))
|
|
13000
15762
|
throw new UsageError(
|
|
13001
15763
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
13002
15764
|
);
|
|
@@ -13052,7 +15814,7 @@ function wallGate(flags, r, opts) {
|
|
|
13052
15814
|
async function cmdRecord(argv) {
|
|
13053
15815
|
const { positionals, flags, multi } = parseArgs(
|
|
13054
15816
|
argv,
|
|
13055
|
-
|
|
15817
|
+
BOOLEAN_FLAGS15,
|
|
13056
15818
|
MULTI_FLAGS2
|
|
13057
15819
|
);
|
|
13058
15820
|
const actionsPath = strFlag(flags, "actions") ?? positionals[0];
|
|
@@ -13065,14 +15827,18 @@ async function cmdRecord(argv) {
|
|
|
13065
15827
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13066
15828
|
if (!url)
|
|
13067
15829
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13068
|
-
const scheme =
|
|
15830
|
+
const scheme = urlSchemeProblem2(url);
|
|
13069
15831
|
if (scheme) throw new UsageError(scheme);
|
|
13070
15832
|
announceEnvNames(actions, r);
|
|
13071
|
-
const outDir =
|
|
15833
|
+
const outDir = resolve18(strFlag(flags, "out") ?? "take");
|
|
13072
15834
|
const storageState = takeStorageState(flags);
|
|
13073
15835
|
const sessionName = takeSessionName(flags, storageState);
|
|
13074
15836
|
const browserArgs = takeBrowserArgs(multi);
|
|
13075
15837
|
const headers = takeHeaders(multi);
|
|
15838
|
+
const confirm = flags.hosted === true ? [] : takeConfirm(multi, actions, {
|
|
15839
|
+
dryRun: flags["dry-run"] === true,
|
|
15840
|
+
r
|
|
15841
|
+
});
|
|
13076
15842
|
if (flags.hosted === true) {
|
|
13077
15843
|
if (flags["dry-run"] === true)
|
|
13078
15844
|
throw new UsageError(
|
|
@@ -13082,6 +15848,15 @@ async function cmdRecord(argv) {
|
|
|
13082
15848
|
throw new UsageError(
|
|
13083
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.'
|
|
13084
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
|
+
);
|
|
13085
15860
|
const origin = platformOrigin({
|
|
13086
15861
|
origin: strFlag(flags, "origin"),
|
|
13087
15862
|
api: strFlag(flags, "api")
|
|
@@ -13181,6 +15956,8 @@ async function cmdRecord(argv) {
|
|
|
13181
15956
|
headers,
|
|
13182
15957
|
context: context2,
|
|
13183
15958
|
dryRun: true,
|
|
15959
|
+
verbose: flags.verbose === true,
|
|
15960
|
+
onEvent: (e) => r.event(e),
|
|
13184
15961
|
// A rehearsal is always strict, and it touches no directory.
|
|
13185
15962
|
onArrival: wallGate(flags, r, { strict: true })
|
|
13186
15963
|
})
|
|
@@ -13189,9 +15966,28 @@ async function cmdRecord(argv) {
|
|
|
13189
15966
|
const lines = steps.map((s) => {
|
|
13190
15967
|
const rect = s.rect ? ` rect ${s.rect.x},${s.rect.y} ${s.rect.w}\xD7${s.rect.h}` : "";
|
|
13191
15968
|
const what = s.selector ? ` ${s.selector}` : "";
|
|
13192
|
-
|
|
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("")}`;
|
|
13193
15982
|
});
|
|
13194
|
-
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);
|
|
13195
15991
|
const carried = [
|
|
13196
15992
|
...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
|
|
13197
15993
|
...browserArgs.map((a) => `--browser-arg=${a}`),
|
|
@@ -13199,9 +15995,11 @@ async function cmdRecord(argv) {
|
|
|
13199
15995
|
// printed.
|
|
13200
15996
|
...Object.keys(headers).map((k) => `--header ${k}=\u2026`),
|
|
13201
15997
|
...sessionName ? [`--session ${sessionName}`] : [],
|
|
13202
|
-
...flags["allow-wall"] === true ? ["--allow-wall"] : []
|
|
15998
|
+
...flags["allow-wall"] === true ? ["--allow-wall"] : [],
|
|
15999
|
+
...confirm.map((c) => `--confirm ${c}`)
|
|
13203
16000
|
].join(" ");
|
|
13204
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)` : "";
|
|
13205
16003
|
r.done(
|
|
13206
16004
|
{
|
|
13207
16005
|
dryRun: true,
|
|
@@ -13209,28 +16007,33 @@ async function cmdRecord(argv) {
|
|
|
13209
16007
|
seconds: +((Date.now() - started) / 1e3).toFixed(1),
|
|
13210
16008
|
steps,
|
|
13211
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 } : {},
|
|
13212
16015
|
navTimeout: rec.navTimeout,
|
|
13213
16016
|
exposures: rec.exposures,
|
|
13214
16017
|
masks: rec.masks
|
|
13215
16018
|
},
|
|
13216
|
-
`${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)" : ""}
|
|
13217
16020
|
${lines.join("\n")}
|
|
13218
|
-
${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)}`
|
|
13219
16022
|
);
|
|
13220
|
-
return failed ? EXIT_USAGE : EXIT_OK;
|
|
16023
|
+
return rec.stopped ? EXIT_ERROR : failed ? EXIT_USAGE : EXIT_OK;
|
|
13221
16024
|
} finally {
|
|
13222
16025
|
await browser2.close();
|
|
13223
16026
|
}
|
|
13224
16027
|
}
|
|
13225
16028
|
const paths = takePaths(outDir);
|
|
13226
16029
|
const prepare = async () => {
|
|
13227
|
-
if (
|
|
16030
|
+
if (existsSync27(join29(outDir, "meta.json"))) {
|
|
13228
16031
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13229
16032
|
r.log(
|
|
13230
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" : "")
|
|
13231
16034
|
);
|
|
13232
16035
|
}
|
|
13233
|
-
await
|
|
16036
|
+
await mkdir13(outDir, { recursive: true });
|
|
13234
16037
|
await ensureTakeDir(outDir);
|
|
13235
16038
|
};
|
|
13236
16039
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13245,6 +16048,9 @@ ${lines.join("\n")}
|
|
|
13245
16048
|
storageState,
|
|
13246
16049
|
headers,
|
|
13247
16050
|
context: context2,
|
|
16051
|
+
confirm,
|
|
16052
|
+
verbose: flags.verbose === true,
|
|
16053
|
+
onEvent: (e) => r.event(e),
|
|
13248
16054
|
onArrival: wallGate(flags, r, {
|
|
13249
16055
|
strict: flags.strict === true,
|
|
13250
16056
|
prepare
|
|
@@ -13263,9 +16069,29 @@ ${lines.join("\n")}
|
|
|
13263
16069
|
const plan = await planTake(outDir, { backdrop });
|
|
13264
16070
|
const clicks = rec.events.filter((e) => e.type === "down").length;
|
|
13265
16071
|
const strict = flags.strict === true;
|
|
13266
|
-
const strictFail = strict && (rec
|
|
16072
|
+
const strictFail = strict && strictFails(rec);
|
|
13267
16073
|
const skippedNote = rec.skipped.length ? `
|
|
13268
|
-
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
|
+
}
|
|
13269
16095
|
r.done(
|
|
13270
16096
|
{
|
|
13271
16097
|
take: outDir,
|
|
@@ -13285,11 +16111,16 @@ ${lines.join("\n")}
|
|
|
13285
16111
|
...rec.wall ? { wall: rec.wall } : {},
|
|
13286
16112
|
exposures: rec.exposures,
|
|
13287
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,
|
|
13288
16119
|
...strictFail ? { strictFailed: true } : {}
|
|
13289
16120
|
},
|
|
13290
|
-
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
13291
|
-
${(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}
|
|
13292
|
-
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)}`
|
|
13293
16124
|
);
|
|
13294
16125
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
13295
16126
|
} finally {
|
|
@@ -13299,7 +16130,7 @@ ${lines.join("\n")}
|
|
|
13299
16130
|
async function cmdCreate2(argv) {
|
|
13300
16131
|
const { positionals, flags, multi } = parseArgs(
|
|
13301
16132
|
argv,
|
|
13302
|
-
|
|
16133
|
+
BOOLEAN_FLAGS15,
|
|
13303
16134
|
MULTI_FLAGS2
|
|
13304
16135
|
);
|
|
13305
16136
|
const actionsPath = strFlag(flags, "actions");
|
|
@@ -13313,12 +16144,12 @@ async function cmdCreate2(argv) {
|
|
|
13313
16144
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
13314
16145
|
if (!url)
|
|
13315
16146
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
13316
|
-
const scheme =
|
|
16147
|
+
const scheme = urlSchemeProblem2(url);
|
|
13317
16148
|
if (scheme) throw new UsageError(scheme);
|
|
13318
16149
|
announceEnvNames(actions, r);
|
|
13319
|
-
const outDir =
|
|
16150
|
+
const outDir = resolve18(strFlag(flags, "out") ?? "take");
|
|
13320
16151
|
const format = videoFormat(strFlag(flags, "format"), positionals[0]);
|
|
13321
|
-
const out = positionals[0] ??
|
|
16152
|
+
const out = positionals[0] ?? join29(outDir, `out.${format}`);
|
|
13322
16153
|
const parallel = numFlag(flags, "parallel", 1);
|
|
13323
16154
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
13324
16155
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -13329,15 +16160,16 @@ async function cmdCreate2(argv) {
|
|
|
13329
16160
|
const sessionName = takeSessionName(flags, storageState);
|
|
13330
16161
|
const browserArgs = takeBrowserArgs(multi);
|
|
13331
16162
|
const headers = takeHeaders(multi);
|
|
16163
|
+
const confirm = takeConfirm(multi, actions, { dryRun: false, r });
|
|
13332
16164
|
const paths = takePaths(outDir);
|
|
13333
16165
|
const prepare = async () => {
|
|
13334
|
-
if (
|
|
16166
|
+
if (existsSync27(join29(outDir, "meta.json"))) {
|
|
13335
16167
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
13336
16168
|
r.log(
|
|
13337
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" : "")
|
|
13338
16170
|
);
|
|
13339
16171
|
}
|
|
13340
|
-
await
|
|
16172
|
+
await mkdir13(outDir, { recursive: true });
|
|
13341
16173
|
await ensureTakeDir(outDir);
|
|
13342
16174
|
};
|
|
13343
16175
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -13352,6 +16184,9 @@ async function cmdCreate2(argv) {
|
|
|
13352
16184
|
storageState,
|
|
13353
16185
|
headers,
|
|
13354
16186
|
context: context2,
|
|
16187
|
+
confirm,
|
|
16188
|
+
verbose: flags.verbose === true,
|
|
16189
|
+
onEvent: (e) => r.event(e),
|
|
13355
16190
|
onArrival: wallGate(flags, r, {
|
|
13356
16191
|
strict: flags.strict === true,
|
|
13357
16192
|
prepare
|
|
@@ -13368,11 +16203,25 @@ async function cmdCreate2(argv) {
|
|
|
13368
16203
|
await releaseFrames(paths, flags, r.log);
|
|
13369
16204
|
r.event({ event: "phase", phase: "plan" });
|
|
13370
16205
|
await planTake(outDir, { backdrop });
|
|
13371
|
-
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)) {
|
|
13372
16220
|
r.done(
|
|
13373
16221
|
{
|
|
13374
16222
|
take: outDir,
|
|
13375
16223
|
skipped: rec.skipped,
|
|
16224
|
+
disabled: rec.disabled,
|
|
13376
16225
|
navTimeout: rec.navTimeout,
|
|
13377
16226
|
capped: rec.capped,
|
|
13378
16227
|
strictFailed: true,
|
|
@@ -13415,20 +16264,26 @@ async function cmdCreate2(argv) {
|
|
|
13415
16264
|
dead: rec.dead,
|
|
13416
16265
|
exposures: rec.exposures,
|
|
13417
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,
|
|
13418
16272
|
...flags.draft === true ? { draft: true } : {}
|
|
13419
16273
|
},
|
|
13420
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" : ""})
|
|
13421
|
-
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)}`
|
|
13422
16276
|
);
|
|
13423
16277
|
return EXIT_OK;
|
|
13424
16278
|
} finally {
|
|
13425
16279
|
await browser.close();
|
|
13426
16280
|
}
|
|
13427
16281
|
}
|
|
13428
|
-
async function cmdPlan(
|
|
16282
|
+
async function cmdPlan(argvIn) {
|
|
16283
|
+
const argv = bareCaptionsFlag(argvIn);
|
|
13429
16284
|
const { positionals, flags, multi } = parseArgs(
|
|
13430
16285
|
argv,
|
|
13431
|
-
|
|
16286
|
+
BOOLEAN_FLAGS15,
|
|
13432
16287
|
/* @__PURE__ */ new Set(["with"])
|
|
13433
16288
|
);
|
|
13434
16289
|
const dir = positionals[0];
|
|
@@ -13438,26 +16293,28 @@ async function cmdPlan(argv) {
|
|
|
13438
16293
|
);
|
|
13439
16294
|
const r = createReporter(flags.json === true);
|
|
13440
16295
|
if (flags.fresh === true) {
|
|
13441
|
-
await
|
|
16296
|
+
await rm5(join29(dir, "doc.json"), { force: true });
|
|
13442
16297
|
r.log("note: --fresh discarded the existing doc.json");
|
|
13443
16298
|
}
|
|
13444
16299
|
let reuse;
|
|
13445
16300
|
if (flags.reuse === true) {
|
|
13446
|
-
const from =
|
|
13447
|
-
if (!
|
|
16301
|
+
const from = resolve18(strFlag(flags, "from") ?? join29(dir, PREV_DOC_NAME));
|
|
16302
|
+
if (!existsSync27(from)) {
|
|
13448
16303
|
throw new UsageError(
|
|
13449
16304
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
13450
16305
|
);
|
|
13451
16306
|
}
|
|
13452
16307
|
reuse = {
|
|
13453
16308
|
from,
|
|
13454
|
-
doc: JSON.parse(await
|
|
16309
|
+
doc: JSON.parse(await readFile15(from, "utf8"))
|
|
13455
16310
|
};
|
|
13456
16311
|
}
|
|
13457
16312
|
const style = await resolveStyleRef(flags);
|
|
13458
|
-
const hasDoc =
|
|
16313
|
+
const hasDoc = existsSync27(join29(dir, "doc.json"));
|
|
13459
16314
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
13460
16315
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
16316
|
+
const stepCaptions = strFlag(flags, "captions") === "steps";
|
|
16317
|
+
if (stepCaptions) release.launchRoles.captions = "none";
|
|
13461
16318
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
13462
16319
|
const stillRaw = strFlag(flags, "still") ?? release.launchRoles.still;
|
|
13463
16320
|
const still = stillRaw === void 0 ? null : Number(stillRaw);
|
|
@@ -13485,6 +16342,16 @@ async function cmdPlan(argv) {
|
|
|
13485
16342
|
}
|
|
13486
16343
|
} : {}
|
|
13487
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` : "") : "";
|
|
13488
16355
|
const layoutLines = s.layout ? `
|
|
13489
16356
|
layout from ${s.styleFrom}: ${[
|
|
13490
16357
|
s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
|
|
@@ -13501,7 +16368,7 @@ async function cmdPlan(argv) {
|
|
|
13501
16368
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
13502
16369
|
r.done(
|
|
13503
16370
|
{
|
|
13504
|
-
take:
|
|
16371
|
+
take: resolve18(dir),
|
|
13505
16372
|
fresh: s.fresh,
|
|
13506
16373
|
cursorKept: s.cursorKept,
|
|
13507
16374
|
zoomAuto: s.zoomAuto,
|
|
@@ -13511,9 +16378,10 @@ async function cmdPlan(argv) {
|
|
|
13511
16378
|
...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
|
|
13512
16379
|
...s.layout ? { layout: s.layout } : {},
|
|
13513
16380
|
...s.motion ? { motion: s.motion } : {},
|
|
13514
|
-
...s.reuse ? { reuse: s.reuse } : {}
|
|
16381
|
+
...s.reuse ? { reuse: s.reuse } : {},
|
|
16382
|
+
...captions ? { captions } : {}
|
|
13515
16383
|
},
|
|
13516
|
-
`${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}`
|
|
13517
16385
|
);
|
|
13518
16386
|
return EXIT_OK;
|
|
13519
16387
|
}
|
|
@@ -13626,7 +16494,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
|
|
|
13626
16494
|
async function cmdRender(argv) {
|
|
13627
16495
|
const { positionals, flags, multi } = parseArgs(
|
|
13628
16496
|
argv,
|
|
13629
|
-
|
|
16497
|
+
BOOLEAN_FLAGS15,
|
|
13630
16498
|
MULTI_FLAGS2
|
|
13631
16499
|
);
|
|
13632
16500
|
const dir = positionals[0];
|
|
@@ -13636,7 +16504,7 @@ async function cmdRender(argv) {
|
|
|
13636
16504
|
);
|
|
13637
16505
|
const r = createReporter(flags.json === true);
|
|
13638
16506
|
const format = videoFormat(strFlag(flags, "format"), positionals[1]);
|
|
13639
|
-
const out = positionals[1] ??
|
|
16507
|
+
const out = positionals[1] ?? join29(dir, `out.${format}`);
|
|
13640
16508
|
const parallel = numFlag(flags, "parallel", 1);
|
|
13641
16509
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
13642
16510
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -13694,7 +16562,7 @@ async function cmdRender(argv) {
|
|
|
13694
16562
|
async function cmdFrames(argv) {
|
|
13695
16563
|
const { positionals, flags, multi } = parseArgs(
|
|
13696
16564
|
argv,
|
|
13697
|
-
|
|
16565
|
+
BOOLEAN_FLAGS15,
|
|
13698
16566
|
MULTI_FLAGS2
|
|
13699
16567
|
);
|
|
13700
16568
|
const dir = positionals[0];
|
|
@@ -13712,12 +16580,13 @@ async function cmdFrames(argv) {
|
|
|
13712
16580
|
if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
|
|
13713
16581
|
size = { width: Number(m[1]), height: Number(m[2]) };
|
|
13714
16582
|
}
|
|
13715
|
-
const duration =
|
|
16583
|
+
const duration = docOutputDuration4(take.doc);
|
|
13716
16584
|
const frameRaw = strFlag(flags, "frame");
|
|
13717
16585
|
const timesRaw = strFlag(flags, "times");
|
|
13718
16586
|
const atZooms = flags["at-zooms"] === true;
|
|
13719
16587
|
const atMoments = flags["at-moments"] === true;
|
|
13720
16588
|
const atStill = flags["at-still"] === true;
|
|
16589
|
+
const strict = flags.strict === true;
|
|
13721
16590
|
let times;
|
|
13722
16591
|
try {
|
|
13723
16592
|
times = frameRaw !== void 0 ? parseTimes2(frameRaw, duration) : timesRaw !== void 0 ? parseTimes2(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes2("0,25%,50%,75%,100%", duration);
|
|
@@ -13747,6 +16616,13 @@ async function cmdFrames(argv) {
|
|
|
13747
16616
|
}
|
|
13748
16617
|
});
|
|
13749
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" : ""}`;
|
|
13750
16626
|
r.done(
|
|
13751
16627
|
{
|
|
13752
16628
|
outDir: result.outDir,
|
|
@@ -13754,12 +16630,14 @@ async function cmdFrames(argv) {
|
|
|
13754
16630
|
width: result.width,
|
|
13755
16631
|
height: result.height,
|
|
13756
16632
|
duration: result.duration,
|
|
13757
|
-
frames: result.frames
|
|
16633
|
+
frames: result.frames,
|
|
16634
|
+
...result.skippedZooms ? { skippedZooms: result.skippedZooms } : {},
|
|
16635
|
+
...atZooms ? { unrenderedZooms: unrendered.length } : {}
|
|
13758
16636
|
},
|
|
13759
16637
|
`Wrote ${result.frames.length} still(s) to ${result.outDir} (${result.width}x${result.height})
|
|
13760
|
-
${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 ")}`
|
|
13761
16639
|
);
|
|
13762
|
-
return EXIT_OK;
|
|
16640
|
+
return unrendered.length && strict ? EXIT_USAGE : EXIT_OK;
|
|
13763
16641
|
} finally {
|
|
13764
16642
|
await browser.close();
|
|
13765
16643
|
}
|
|
@@ -13767,7 +16645,7 @@ async function cmdFrames(argv) {
|
|
|
13767
16645
|
async function cmdDeliver(argv) {
|
|
13768
16646
|
const { positionals, flags, multi } = parseArgs(
|
|
13769
16647
|
argv,
|
|
13770
|
-
|
|
16648
|
+
BOOLEAN_FLAGS15,
|
|
13771
16649
|
MULTI_FLAGS2
|
|
13772
16650
|
);
|
|
13773
16651
|
const dir = positionals[0];
|
|
@@ -13867,15 +16745,15 @@ async function resolveStyleRef(flags) {
|
|
|
13867
16745
|
}
|
|
13868
16746
|
async function resolveDocRef(ref, flags, what) {
|
|
13869
16747
|
const styleRef = ref;
|
|
13870
|
-
const file =
|
|
16748
|
+
const file = existsSync27(styleRef) ? resolve18(
|
|
13871
16749
|
styleRef,
|
|
13872
|
-
|
|
16750
|
+
existsSync27(join29(styleRef, "doc.json")) ? "doc.json" : ""
|
|
13873
16751
|
) : null;
|
|
13874
|
-
if (file &&
|
|
16752
|
+
if (file && existsSync27(file)) {
|
|
13875
16753
|
return {
|
|
13876
16754
|
from: file,
|
|
13877
16755
|
doc: migrateHostedDoc4(
|
|
13878
|
-
JSON.parse(await
|
|
16756
|
+
JSON.parse(await readFile15(file, "utf8"))
|
|
13879
16757
|
)
|
|
13880
16758
|
};
|
|
13881
16759
|
}
|
|
@@ -13917,7 +16795,7 @@ async function officialTemplate(origin, ref) {
|
|
|
13917
16795
|
}
|
|
13918
16796
|
}
|
|
13919
16797
|
async function cmdDigest(argv) {
|
|
13920
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16798
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
13921
16799
|
const dir = positionals[0];
|
|
13922
16800
|
if (!dir)
|
|
13923
16801
|
throw new UsageError(
|
|
@@ -13927,7 +16805,7 @@ async function cmdDigest(argv) {
|
|
|
13927
16805
|
const take = await loadTake(dir);
|
|
13928
16806
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
13929
16807
|
const transcriptPath = strFlag(flags, "transcript");
|
|
13930
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
16808
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
|
|
13931
16809
|
const style = await resolveStyleRef(flags);
|
|
13932
16810
|
const noFrames = flags["no-frames"] === true;
|
|
13933
16811
|
let browser = null;
|
|
@@ -13958,7 +16836,7 @@ async function cmdDigest(argv) {
|
|
|
13958
16836
|
kinds,
|
|
13959
16837
|
frames: d.images.full,
|
|
13960
16838
|
crops: d.images.crop,
|
|
13961
|
-
sheet: d.images.sheet ?
|
|
16839
|
+
sheet: d.images.sheet ? join29(result.outDir, d.images.sheet) : null,
|
|
13962
16840
|
scenes: kinds.scene ?? 0,
|
|
13963
16841
|
sourceDuration: d.take.sourceDuration,
|
|
13964
16842
|
outputDuration: d.take.outputDuration,
|
|
@@ -13981,19 +16859,19 @@ async function cmdDigest(argv) {
|
|
|
13981
16859
|
}
|
|
13982
16860
|
}
|
|
13983
16861
|
async function cmdOpen(argv) {
|
|
13984
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16862
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
13985
16863
|
const dir = positionals[0];
|
|
13986
16864
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
13987
16865
|
const r = createReporter(flags.json === true);
|
|
13988
16866
|
const take = await loadTake(dir);
|
|
13989
|
-
if (!
|
|
16867
|
+
if (!existsSync27(take.paths.recording)) {
|
|
13990
16868
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
13991
16869
|
}
|
|
13992
16870
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
13993
16871
|
/\/+$/,
|
|
13994
16872
|
""
|
|
13995
16873
|
);
|
|
13996
|
-
const server = await startTakeServer(
|
|
16874
|
+
const server = await startTakeServer(resolve18(dir), {});
|
|
13997
16875
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
13998
16876
|
r.event({ event: "open", server: server.base, url });
|
|
13999
16877
|
r.log(`take served at ${server.base}`);
|
|
@@ -14016,7 +16894,7 @@ async function cmdOpen(argv) {
|
|
|
14016
16894
|
return EXIT_OK;
|
|
14017
16895
|
}
|
|
14018
16896
|
async function cmdCallout(argv) {
|
|
14019
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16897
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
14020
16898
|
const dir = positionals[0];
|
|
14021
16899
|
const shape = positionals[1];
|
|
14022
16900
|
if (!dir || !shape || !isCalloutShape(shape))
|
|
@@ -14040,7 +16918,7 @@ async function cmdCallout(argv) {
|
|
|
14040
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`
|
|
14041
16919
|
);
|
|
14042
16920
|
const codeFile = strFlag(flags, "code-file");
|
|
14043
|
-
const code = codeFile ? await
|
|
16921
|
+
const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
|
|
14044
16922
|
const words2 = {
|
|
14045
16923
|
kicker: strFlag(flags, "kicker"),
|
|
14046
16924
|
title: strFlag(flags, "title"),
|
|
@@ -14115,11 +16993,11 @@ async function cmdCallout(argv) {
|
|
|
14115
16993
|
return EXIT_OK;
|
|
14116
16994
|
}
|
|
14117
16995
|
async function cmdValidate(argv) {
|
|
14118
|
-
const { positionals, flags } = parseArgs(argv,
|
|
16996
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
14119
16997
|
const target = positionals[0];
|
|
14120
16998
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
14121
16999
|
const r = createReporter(flags.json === true);
|
|
14122
|
-
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;
|
|
14123
17001
|
if (kitTarget) {
|
|
14124
17002
|
const picture = flags.picture === true;
|
|
14125
17003
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -14151,9 +17029,9 @@ async function cmdValidate(argv) {
|
|
|
14151
17029
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
14152
17030
|
return EXIT_OK;
|
|
14153
17031
|
}
|
|
14154
|
-
if (!isTakeDir(target) &&
|
|
17032
|
+
if (!isTakeDir(target) && existsSync27(join29(target, "config.json"))) {
|
|
14155
17033
|
const parsed = JSON.parse(
|
|
14156
|
-
await
|
|
17034
|
+
await readFile15(join29(target, "config.json"), "utf8")
|
|
14157
17035
|
);
|
|
14158
17036
|
const pre = preflightConfig(parsed);
|
|
14159
17037
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -14194,7 +17072,8 @@ async function cmdValidate(argv) {
|
|
|
14194
17072
|
const take = await loadTake(target);
|
|
14195
17073
|
const problems = [];
|
|
14196
17074
|
const warnings = [];
|
|
14197
|
-
|
|
17075
|
+
const notes = [];
|
|
17076
|
+
if (!existsSync27(take.paths.recording)) {
|
|
14198
17077
|
problems.push("missing recording (re-run record)");
|
|
14199
17078
|
} else {
|
|
14200
17079
|
const mismatch = containerMismatch(
|
|
@@ -14220,6 +17099,7 @@ async function cmdValidate(argv) {
|
|
|
14220
17099
|
const lint = lintDoc(take.doc);
|
|
14221
17100
|
problems.push(...lint.problems.map((p) => `doc.json: ${p}`));
|
|
14222
17101
|
warnings.push(...lint.warnings.map((w) => `doc.json: ${w}`));
|
|
17102
|
+
notes.push(...(lint.notes ?? []).map((n) => `doc.json: ${n}`));
|
|
14223
17103
|
const probe3 = mediaProbeLints(
|
|
14224
17104
|
await probeMediaUrls(collectDocMediaUrls(take.doc))
|
|
14225
17105
|
);
|
|
@@ -14258,26 +17138,37 @@ async function cmdValidate(argv) {
|
|
|
14258
17138
|
}
|
|
14259
17139
|
if (problems.length) {
|
|
14260
17140
|
r.done(
|
|
14261
|
-
{
|
|
17141
|
+
{
|
|
17142
|
+
valid: false,
|
|
17143
|
+
target,
|
|
17144
|
+
problems,
|
|
17145
|
+
warnings,
|
|
17146
|
+
...notes.length ? { notes } : {}
|
|
17147
|
+
},
|
|
14262
17148
|
`${target}:
|
|
14263
17149
|
${problems.join("\n ")}${warnings.length ? `
|
|
14264
17150
|
warnings:
|
|
14265
|
-
${warnings.join("\n ")}` : ""}`
|
|
17151
|
+
${warnings.join("\n ")}` : ""}${notes.length ? `
|
|
17152
|
+
notes:
|
|
17153
|
+
${notes.join("\n ")}` : ""}`
|
|
14266
17154
|
);
|
|
14267
17155
|
return EXIT_ERROR;
|
|
14268
17156
|
}
|
|
17157
|
+
const noteLines = notes.length ? `
|
|
17158
|
+
notes:
|
|
17159
|
+
${notes.join("\n ")}` : "";
|
|
14269
17160
|
r.done(
|
|
14270
|
-
{ valid: true, target, warnings },
|
|
17161
|
+
{ valid: true, target, warnings, ...notes.length ? { notes } : {} },
|
|
14271
17162
|
`${target}: take is complete and renderable${warnings.length ? `
|
|
14272
17163
|
warnings:
|
|
14273
|
-
${warnings.join("\n ")}` : ""}`
|
|
17164
|
+
${warnings.join("\n ")}` : ""}${noteLines}`
|
|
14274
17165
|
);
|
|
14275
17166
|
return EXIT_OK;
|
|
14276
17167
|
}
|
|
14277
17168
|
async function cmdPush3(argv) {
|
|
14278
17169
|
const { positionals, flags, multi } = parseArgs(
|
|
14279
17170
|
argv,
|
|
14280
|
-
|
|
17171
|
+
BOOLEAN_FLAGS15,
|
|
14281
17172
|
MULTI_FLAGS2
|
|
14282
17173
|
);
|
|
14283
17174
|
const dir = positionals[0];
|
|
@@ -14287,7 +17178,7 @@ async function cmdPush3(argv) {
|
|
|
14287
17178
|
);
|
|
14288
17179
|
const refusal = takePushRefusal({
|
|
14289
17180
|
vos: strFlag(flags, "vos"),
|
|
14290
|
-
trackedVosId: readSyncState(
|
|
17181
|
+
trackedVosId: readSyncState(resolve18(dir))?.vosId ?? null,
|
|
14291
17182
|
programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
|
|
14292
17183
|
(name) => hasFlag(flags, name)
|
|
14293
17184
|
)
|
|
@@ -14303,7 +17194,7 @@ async function cmdPush3(argv) {
|
|
|
14303
17194
|
`--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
|
|
14304
17195
|
);
|
|
14305
17196
|
const claim = await pushTakeClaimable(
|
|
14306
|
-
|
|
17197
|
+
resolve18(dir),
|
|
14307
17198
|
{
|
|
14308
17199
|
api: strFlag(flags, "api"),
|
|
14309
17200
|
origin: strFlag(flags, "origin"),
|
|
@@ -14325,7 +17216,7 @@ reference and the only credential.`
|
|
|
14325
17216
|
}
|
|
14326
17217
|
const overrides = multi.override;
|
|
14327
17218
|
const result = await pushTake(
|
|
14328
|
-
|
|
17219
|
+
resolve18(dir),
|
|
14329
17220
|
{
|
|
14330
17221
|
key: strFlag(flags, "key"),
|
|
14331
17222
|
api: strFlag(flags, "api"),
|
|
@@ -14375,11 +17266,11 @@ ${landedLines(landed)}` : "")
|
|
|
14375
17266
|
return EXIT_OK;
|
|
14376
17267
|
}
|
|
14377
17268
|
async function cmdPull2(argv) {
|
|
14378
|
-
const { positionals, flags } = parseArgs(argv,
|
|
17269
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15, MULTI_FLAGS2);
|
|
14379
17270
|
const dir = positionals[0] ?? ".";
|
|
14380
17271
|
const r = createReporter(flags.json === true);
|
|
14381
17272
|
const result = await pullTake(
|
|
14382
|
-
|
|
17273
|
+
resolve18(dir),
|
|
14383
17274
|
{
|
|
14384
17275
|
key: strFlag(flags, "key"),
|
|
14385
17276
|
api: strFlag(flags, "api"),
|
|
@@ -14511,6 +17402,14 @@ async function run(argv) {
|
|
|
14511
17402
|
return await cmdActions(rest);
|
|
14512
17403
|
case "ingest":
|
|
14513
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);
|
|
14514
17413
|
default:
|
|
14515
17414
|
throw new UsageError(`unknown command "${cmd}" \u2014 run: vos help`);
|
|
14516
17415
|
}
|
|
@@ -14520,7 +17419,7 @@ async function run(argv) {
|
|
|
14520
17419
|
`);
|
|
14521
17420
|
return EXIT_USAGE;
|
|
14522
17421
|
}
|
|
14523
|
-
if (e instanceof SetupError || e instanceof SetupEnvError) {
|
|
17422
|
+
if (e instanceof SetupError || e instanceof SetupEnvError || e instanceof RecordRefusedError) {
|
|
14524
17423
|
process.stderr.write(`${e.message}
|
|
14525
17424
|
`);
|
|
14526
17425
|
return EXIT_USAGE;
|
|
@@ -14543,7 +17442,7 @@ async function run(argv) {
|
|
|
14543
17442
|
}
|
|
14544
17443
|
}
|
|
14545
17444
|
async function cmdSession(argv) {
|
|
14546
|
-
const { positionals, flags } = parseArgs(argv,
|
|
17445
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
|
|
14547
17446
|
const [sub, arg] = positionals;
|
|
14548
17447
|
const r = createReporter(flags.json === true);
|
|
14549
17448
|
const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
|
|
@@ -14579,7 +17478,7 @@ async function cmdSession(argv) {
|
|
|
14579
17478
|
if (!arg) throw new UsageError(usage);
|
|
14580
17479
|
const bad = sessionNameVerdict(arg);
|
|
14581
17480
|
if (bad) throw new UsageError(bad);
|
|
14582
|
-
if (!
|
|
17481
|
+
if (!existsSync27(sessionDir(arg)))
|
|
14583
17482
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
14584
17483
|
const summary3 = await summarizeSession(arg);
|
|
14585
17484
|
const url = strFlag(flags, "url");
|
|
@@ -14674,9 +17573,10 @@ export {
|
|
|
14674
17573
|
convertAgentBrowser,
|
|
14675
17574
|
MULTI_FLAGS2 as MULTI_FLAGS,
|
|
14676
17575
|
HELP,
|
|
17576
|
+
strictFails,
|
|
14677
17577
|
takeBrowserArgs,
|
|
14678
17578
|
takeBackdrop,
|
|
14679
17579
|
verbHelp,
|
|
14680
17580
|
run
|
|
14681
17581
|
};
|
|
14682
|
-
//# sourceMappingURL=chunk-
|
|
17582
|
+
//# sourceMappingURL=chunk-INRUYQ6J.js.map
|