@vosjs/cli 0.79.0 → 0.80.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 +71 -25
- package/dist/{assetManifest-U25FJER6.js → assetManifest-ETT3C7ZA.js} +2 -2
- package/dist/{chunk-Z6TLHRFQ.js → chunk-B373NVJE.js} +6 -2
- package/dist/chunk-B373NVJE.js.map +1 -0
- package/dist/{chunk-4ZUHEXUX.js → chunk-PJ6MEJZV.js} +205 -25
- package/dist/chunk-PJ6MEJZV.js.map +1 -0
- package/dist/{chunk-TWAP4BKR.js → chunk-SP5OHTVB.js} +2441 -603
- package/dist/chunk-SP5OHTVB.js.map +1 -0
- package/dist/cli.js +13 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/{run-UKWKAGDK.js → run-2ELHMPTB.js} +4 -4
- package/dist/{shotList-EE3S5LVF.js → shotList-MSNWLVDN.js} +30 -3
- package/dist/shotList-MSNWLVDN.js.map +1 -0
- package/package.json +3 -3
- package/schema/actions.schema.json +200 -4
- package/schema/doc.schema.json +9 -4
- package/skills/VERSION +1 -1
- package/skills/launch-kit/SKILL.md +6 -2
- package/skills/product-video/SKILL.md +27 -0
- package/skills/product-video/references/taste.md +7 -3
- package/skills/vos-cut/SKILL.md +17 -2
- package/dist/chunk-4ZUHEXUX.js.map +0 -1
- package/dist/chunk-TWAP4BKR.js.map +0 -1
- package/dist/chunk-Z6TLHRFQ.js.map +0 -1
- package/dist/shotList-EE3S5LVF.js.map +0 -1
- /package/dist/{assetManifest-U25FJER6.js.map → assetManifest-ETT3C7ZA.js.map} +0 -0
- /package/dist/{run-UKWKAGDK.js.map → run-2ELHMPTB.js.map} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
BROWSER_SWITCHES,
|
|
3
4
|
BrowserUnavailableError,
|
|
4
5
|
configDuration,
|
|
5
6
|
directoryKind,
|
|
@@ -9,10 +10,12 @@ import {
|
|
|
9
10
|
reencodeStill,
|
|
10
11
|
renderStill,
|
|
11
12
|
videoFormat
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-B373NVJE.js";
|
|
13
14
|
import {
|
|
14
15
|
PREV_DOC_NAME,
|
|
16
|
+
PasteFileRefusal,
|
|
15
17
|
RECORDING_NAME,
|
|
18
|
+
claimablePasteFiles,
|
|
16
19
|
docAudioRefs,
|
|
17
20
|
docLayerRefs,
|
|
18
21
|
docMediaRefs,
|
|
@@ -30,8 +33,10 @@ import {
|
|
|
30
33
|
pullMedia,
|
|
31
34
|
readJson,
|
|
32
35
|
readMediaHead,
|
|
36
|
+
readPasteFiles,
|
|
33
37
|
recordHostedMedia,
|
|
34
38
|
safePathName,
|
|
39
|
+
sendPasteFiles,
|
|
35
40
|
sourceSidecarRefs,
|
|
36
41
|
takeMediaFile,
|
|
37
42
|
takePaths,
|
|
@@ -40,7 +45,7 @@ import {
|
|
|
40
45
|
uploadDocRefs,
|
|
41
46
|
uploadManifest,
|
|
42
47
|
writeJson
|
|
43
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-PJ6MEJZV.js";
|
|
44
49
|
import {
|
|
45
50
|
CREDENTIALS_PATH,
|
|
46
51
|
MEDIA_HEAD_BYTES,
|
|
@@ -87,7 +92,7 @@ import {
|
|
|
87
92
|
var absolute = (origin, url) => typeof url === "string" && url ? /^https?:/.test(url) ? url : `${origin}${url.startsWith("/") ? "" : "/"}${url}` : null;
|
|
88
93
|
async function waitForLanded(opts) {
|
|
89
94
|
const origin = opts.origin.replace(/\/+$/, "");
|
|
90
|
-
const sleep = opts.sleep ?? ((
|
|
95
|
+
const sleep = opts.sleep ?? ((ms2) => new Promise((r) => setTimeout(r, ms2)));
|
|
91
96
|
const now = opts.now ?? (() => Date.now());
|
|
92
97
|
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
93
98
|
const intervalMs = opts.intervalMs ?? 4e3;
|
|
@@ -139,12 +144,12 @@ function landedLines(l) {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
// src/plugin/run.ts
|
|
142
|
-
import { mkdir as mkdir13, readFile as
|
|
143
|
-
import { existsSync as
|
|
144
|
-
import { dirname as dirname11, join as
|
|
147
|
+
import { mkdir as mkdir13, readFile as readFile18, rm as rm6 } from "fs/promises";
|
|
148
|
+
import { existsSync as existsSync29 } from "fs";
|
|
149
|
+
import { dirname as dirname11, join as join33, resolve as resolve20 } from "path";
|
|
145
150
|
import {
|
|
146
151
|
END_CARD_FROM as END_CARD_FROM2,
|
|
147
|
-
docOutputDuration as
|
|
152
|
+
docOutputDuration as docOutputDuration5,
|
|
148
153
|
migrateHostedDoc as migrateHostedDoc4
|
|
149
154
|
} from "@vosjs/studio-core";
|
|
150
155
|
|
|
@@ -186,6 +191,24 @@ function createReporter(json) {
|
|
|
186
191
|
|
|
187
192
|
// src/plugin/actions.ts
|
|
188
193
|
import { validateActions } from "@vosjs/render-core/record";
|
|
194
|
+
function jsSyntaxProblems(actions) {
|
|
195
|
+
const out = [];
|
|
196
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
197
|
+
}).constructor;
|
|
198
|
+
actions.steps.forEach((s, i) => {
|
|
199
|
+
const js = s.do === "expect" ? s.js : s.do === "await" ? s.until.js : void 0;
|
|
200
|
+
if (typeof js !== "string") return;
|
|
201
|
+
try {
|
|
202
|
+
new AsyncFunction(`return await (${js}
|
|
203
|
+
)`);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
out.push(
|
|
206
|
+
`steps[${i}]: ${s.do === "await" ? "await until.js" : "expect js"} does not parse as an expression (${e instanceof Error ? e.message : String(e)})`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
189
212
|
|
|
190
213
|
// src/plugin/validateDoc.ts
|
|
191
214
|
import {
|
|
@@ -226,7 +249,9 @@ import {
|
|
|
226
249
|
formatElapsed,
|
|
227
250
|
hasElapsed,
|
|
228
251
|
outputEnd,
|
|
252
|
+
layerStart,
|
|
229
253
|
resolveAnchor,
|
|
254
|
+
resolveOverlayAnchors,
|
|
230
255
|
pinBox,
|
|
231
256
|
pinCandidates,
|
|
232
257
|
pinRectOnScreen,
|
|
@@ -238,6 +263,9 @@ import {
|
|
|
238
263
|
snapRunWeight,
|
|
239
264
|
RUN_STYLE_KEYS,
|
|
240
265
|
ratedSegments,
|
|
266
|
+
replayedSeconds,
|
|
267
|
+
cameraSpanExtents,
|
|
268
|
+
spanOutputExtents,
|
|
241
269
|
clipGain,
|
|
242
270
|
recommendedExportResolution,
|
|
243
271
|
resolvePins,
|
|
@@ -456,7 +484,7 @@ function lintLoudness(a, name, problems, warnings) {
|
|
|
456
484
|
`${name}: normalize ${a.normalize} asks for more than +24 dB, the most a target lifts; it plays under its target`
|
|
457
485
|
);
|
|
458
486
|
}
|
|
459
|
-
function checkSpanList(list, label, duration, problems) {
|
|
487
|
+
function checkSpanList(list, label, duration, problems, opts = {}) {
|
|
460
488
|
const sorted = [...list].sort(
|
|
461
489
|
(a, b) => (isNum(a.in) ? a.in : 0) - (isNum(b.in) ? b.in : 0)
|
|
462
490
|
);
|
|
@@ -480,6 +508,7 @@ function checkSpanList(list, label, duration, problems) {
|
|
|
480
508
|
);
|
|
481
509
|
}
|
|
482
510
|
}
|
|
511
|
+
if (opts.overlap) return;
|
|
483
512
|
for (let i = 1; i < sorted.length; i++) {
|
|
484
513
|
const prev = sorted[i - 1];
|
|
485
514
|
const curr = sorted[i];
|
|
@@ -530,11 +559,11 @@ function lintDoc(docIn) {
|
|
|
530
559
|
`media[${i}]${id ? ` (${id})` : ""}: videoKey must be a string`
|
|
531
560
|
);
|
|
532
561
|
const mm = isObj(m.meta) ? m.meta : {};
|
|
533
|
-
const
|
|
534
|
-
if (id) mediaLen.set(id,
|
|
562
|
+
const ms2 = isNum(mm.durationMs) ? mm.durationMs : 0;
|
|
563
|
+
if (id) mediaLen.set(id, ms2 / 1e3);
|
|
535
564
|
}
|
|
536
565
|
}
|
|
537
|
-
const checkByMedia = (list, label) => {
|
|
566
|
+
const checkByMedia = (list, label, opts = {}) => {
|
|
538
567
|
const groups = /* @__PURE__ */ new Map();
|
|
539
568
|
for (const s of list) {
|
|
540
569
|
const m = typeof s.media === "string" ? s.media : "";
|
|
@@ -551,7 +580,8 @@ function lintDoc(docIn) {
|
|
|
551
580
|
group,
|
|
552
581
|
label,
|
|
553
582
|
m ? mediaLen.get(m) ?? 0 : duration,
|
|
554
|
-
problems
|
|
583
|
+
problems,
|
|
584
|
+
opts
|
|
555
585
|
);
|
|
556
586
|
};
|
|
557
587
|
if (!recording) {
|
|
@@ -623,7 +653,7 @@ function lintDoc(docIn) {
|
|
|
623
653
|
} else {
|
|
624
654
|
const rated = ratedSegments(doc);
|
|
625
655
|
const end = outputEnd(
|
|
626
|
-
doc,
|
|
656
|
+
resolveOverlayAnchors(doc, rated),
|
|
627
657
|
rated.reduce((a, s) => a + (s.out - s.in) / (s.rate ?? 1), 0)
|
|
628
658
|
);
|
|
629
659
|
if (st > end + 1e-6)
|
|
@@ -705,7 +735,63 @@ function lintDoc(docIn) {
|
|
|
705
735
|
if (doc.segments !== void 0 && !Array.isArray(doc.segments)) {
|
|
706
736
|
problems.push("segments must be an array of {in, out} spans");
|
|
707
737
|
}
|
|
708
|
-
checkByMedia(entries(doc.segments), "segment");
|
|
738
|
+
checkByMedia(entries(doc.segments), "segment", { overlap: true });
|
|
739
|
+
{
|
|
740
|
+
const index = [];
|
|
741
|
+
const pieces = [];
|
|
742
|
+
for (const [i, seg] of entries(doc.segments).entries()) {
|
|
743
|
+
if (!isNum(seg.in) || !isNum(seg.out)) continue;
|
|
744
|
+
index.push(i);
|
|
745
|
+
pieces.push({
|
|
746
|
+
in: seg.in,
|
|
747
|
+
out: seg.out,
|
|
748
|
+
...typeof seg.media === "string" ? { media: seg.media } : {}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
const bySegment = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const r of replayedSeconds(pieces)) {
|
|
753
|
+
const list = bySegment.get(r.piece) ?? [];
|
|
754
|
+
list.push(r);
|
|
755
|
+
bySegment.set(r.piece, list);
|
|
756
|
+
}
|
|
757
|
+
for (const [p, spans] of bySegment) {
|
|
758
|
+
const m = spans[0].media;
|
|
759
|
+
const which = spans.map((r) => `${r.in.toFixed(2)}\u2013${r.out.toFixed(2)}s`).join(", ");
|
|
760
|
+
const segs2 = entries(doc.segments);
|
|
761
|
+
const rests = segs2[index[p]]?.camera === "rest";
|
|
762
|
+
const restedBefore = pieces.map((q, k) => ({ q, k })).filter(
|
|
763
|
+
({ q, k }) => k < p && (q.media ?? "") === m && segs2[index[k]]?.camera === "rest" && spans.some((r) => Math.min(q.out, r.out) > Math.max(q.in, r.in))
|
|
764
|
+
).map(({ k }) => `segments[${index[k]}]`);
|
|
765
|
+
const what = `segments[${index[p]}] replays ${which} of ${m ? `media "${m}"` : "the recording"}`;
|
|
766
|
+
notes.push(
|
|
767
|
+
rests ? `${what} with its camera at rest: clicks, freezes and speed spans over those seconds play again, zooms, tilts and cam moves do not` : restedBefore.length ? `${what}, which ${restedBefore.join(" and ")} played with the camera at rest: clicks, freezes and speed spans over those seconds play in every occurrence, zooms, tilts and cam moves only in segments[${index[p]}]` : `${what}: zooms, tilts, clicks, freezes and speed spans over those seconds play in every occurrence`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const segList = entries(doc.segments);
|
|
772
|
+
segList.forEach((seg, i) => {
|
|
773
|
+
if (seg.camera !== void 0 && seg.camera !== "rest")
|
|
774
|
+
problems.push(
|
|
775
|
+
`segments[${i}].camera must be "rest" (the camera rests through this clip's pass); absent = the camera plays here`
|
|
776
|
+
);
|
|
777
|
+
});
|
|
778
|
+
if (segList.some((s) => s.camera === "rest")) {
|
|
779
|
+
const rated = ratedSegments(doc);
|
|
780
|
+
for (const [label, list] of [
|
|
781
|
+
["zoom", zoom],
|
|
782
|
+
["tilt", entries(doc.tilt)],
|
|
783
|
+
["camMotion", entries(doc.camMotion)]
|
|
784
|
+
]) {
|
|
785
|
+
for (const s of list) {
|
|
786
|
+
if (!isNum(s.in) || !isNum(s.out)) continue;
|
|
787
|
+
const media = typeof s.media === "string" ? s.media : void 0;
|
|
788
|
+
if (spanOutputExtents(rated, s.in, s.out, media).length && !cameraSpanExtents(rated, s.in, s.out, media).length)
|
|
789
|
+
warnings.push(
|
|
790
|
+
`${spanName(label, s)}: it plays only in segments whose camera rests (segments[].camera: "rest"), so it never moves the camera`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
709
795
|
const segs = Array.isArray(doc.segments) ? doc.segments.filter(isObj) : [];
|
|
710
796
|
segs.forEach((seg, i) => {
|
|
711
797
|
lintAnim(
|
|
@@ -1133,8 +1219,10 @@ function lintDoc(docIn) {
|
|
|
1133
1219
|
`${name}.kind must be "text" | "image" | "video" | "html" (got ${String(o.kind)})`
|
|
1134
1220
|
);
|
|
1135
1221
|
}
|
|
1136
|
-
if (
|
|
1137
|
-
|
|
1222
|
+
if (o.anchor === void 0 || o.start !== void 0) {
|
|
1223
|
+
if (!isNum(o.start) || o.start < 0)
|
|
1224
|
+
problems.push(`${name}.start must be \u2265 0 (OUTPUT seconds)`);
|
|
1225
|
+
}
|
|
1138
1226
|
if (!isNum(o.duration) || o.duration <= 0) {
|
|
1139
1227
|
problems.push(`${name}.duration must be > 0 (seconds)`);
|
|
1140
1228
|
}
|
|
@@ -1730,6 +1818,24 @@ function lintDoc(docIn) {
|
|
|
1730
1818
|
framingWarnings(docIn, doc, zoom, overlays ?? [], warnings);
|
|
1731
1819
|
return { problems, warnings, ...notes.length ? { notes } : {} };
|
|
1732
1820
|
}
|
|
1821
|
+
function otherSceneAt(doc, start, anchorMedia) {
|
|
1822
|
+
const segs = Array.isArray(doc.segments) ? doc.segments : [];
|
|
1823
|
+
if (!segs.length) return null;
|
|
1824
|
+
const own = typeof anchorMedia === "string" ? anchorMedia : "";
|
|
1825
|
+
const mediaOf = (i) => segs[i]?.media === void 0 ? "" : String(segs[i].media);
|
|
1826
|
+
const ext2 = segmentOutputExtents(doc);
|
|
1827
|
+
const EPS2 = 1e-3;
|
|
1828
|
+
const at2 = (i) => start >= ext2[i].start - EPS2 && start < ext2[i].end - EPS2;
|
|
1829
|
+
if (ext2.some((_, i) => mediaOf(i) === own && at2(i))) return null;
|
|
1830
|
+
const index = ext2.findIndex((_, i) => mediaOf(i) !== own && at2(i));
|
|
1831
|
+
if (index < 0) return null;
|
|
1832
|
+
const before = ext2.filter((e, i) => mediaOf(i) === own && e.end <= start + EPS2).map((e) => e.end);
|
|
1833
|
+
return {
|
|
1834
|
+
index,
|
|
1835
|
+
media: mediaOf(index),
|
|
1836
|
+
ownEnd: before.length ? Math.max(...before) : null
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1733
1839
|
function checkOverlayAnchor(o, name, docIn, recording, pieces, problems, warnings, notes) {
|
|
1734
1840
|
const fallback = isNum(o.start) ? o.start : null;
|
|
1735
1841
|
const words2 = o.kind === "text" ? o.text : void 0;
|
|
@@ -1765,7 +1871,14 @@ function checkOverlayAnchor(o, name, docIn, recording, pieces, problems, warning
|
|
|
1765
1871
|
problems.push(`${name}.anchor.offset must be a number (OUTPUT seconds)`);
|
|
1766
1872
|
shaped = false;
|
|
1767
1873
|
}
|
|
1768
|
-
if (!shaped
|
|
1874
|
+
if (!shaped) return fallback;
|
|
1875
|
+
if (!recording || !pieces) {
|
|
1876
|
+
if (fallback === null)
|
|
1877
|
+
problems.push(
|
|
1878
|
+
recording ? `${name}.anchor: the cut could not be read to place step ${JSON.stringify(a.step)}, and the layer has no start to fall back to` : `${name}.anchor: names step ${JSON.stringify(a.step)}, but a program document has no recorder steps, and the layer has no start to fall back to; give it a start`
|
|
1879
|
+
);
|
|
1880
|
+
return fallback;
|
|
1881
|
+
}
|
|
1769
1882
|
const r = resolveAnchor(
|
|
1770
1883
|
docIn,
|
|
1771
1884
|
a,
|
|
@@ -1775,9 +1888,21 @@ function checkOverlayAnchor(o, name, docIn, recording, pieces, problems, warning
|
|
|
1775
1888
|
const at2 = fallback === null ? "nowhere" : `${fallback.toFixed(2)}s`;
|
|
1776
1889
|
if (r.start === null) {
|
|
1777
1890
|
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`;
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1891
|
+
if (fallback === null)
|
|
1892
|
+
problems.push(
|
|
1893
|
+
`${name}.anchor: ${why}, and the layer has no start to fall back to, so it would play nowhere; re-anchor it, keep the footage that step plays in, or give it a start`
|
|
1894
|
+
);
|
|
1895
|
+
else
|
|
1896
|
+
warnings.push(
|
|
1897
|
+
`${name}.anchor: ${why}, so it plays at its fallback start (${at2}); re-anchor it, or keep the footage that step plays in`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
if (r.start !== null) {
|
|
1901
|
+
const over = otherSceneAt(docIn, r.start, a.media);
|
|
1902
|
+
if (over)
|
|
1903
|
+
warnings.push(
|
|
1904
|
+
`${name}${typeof o.id === "string" ? ` (${o.id})` : ""}: anchored to ${step}, it starts at ${r.start.toFixed(2)}s, ${over.ownEnd === null ? "where" : `after that media's footage there ends (${over.ownEnd.toFixed(2)}s), where`} segments[${over.index}] plays ${over.media ? `media "${over.media}"` : "the recording"}: it speaks over another scene. Anchor it to the step's start (anchor.at: "start"), lower anchor.offset, or keep more of the footage it is about`
|
|
1905
|
+
);
|
|
1781
1906
|
}
|
|
1782
1907
|
if (elapsed && r.elapsedFrom && r.elapsedMs !== void 0)
|
|
1783
1908
|
notes.push(
|
|
@@ -1976,7 +2101,9 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1976
2101
|
const zoomTrack = docZoomTrack(docIn);
|
|
1977
2102
|
const pins = resolvePins(docIn, layout, camera, zoomTrack);
|
|
1978
2103
|
overlays.forEach((o, i) => {
|
|
1979
|
-
if (!isObj(o) || !isNum(o.
|
|
2104
|
+
if (!isObj(o) || !isNum(o.duration)) return;
|
|
2105
|
+
const start = o.anchor ? layerStart(o, docIn, rated) : isNum(o.start) ? o.start : null;
|
|
2106
|
+
if (start === null) return;
|
|
1980
2107
|
const tf = isObj(o.transform) ? o.transform : null;
|
|
1981
2108
|
if (!tf || !isNum(tf.x) || !isNum(tf.y)) return;
|
|
1982
2109
|
const pinned = pins.get(String(o.id));
|
|
@@ -1996,7 +2123,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1996
2123
|
}
|
|
1997
2124
|
const about = [];
|
|
1998
2125
|
for (const c of pinCandidates(docIn, {
|
|
1999
|
-
start
|
|
2126
|
+
start,
|
|
2000
2127
|
duration: o.duration
|
|
2001
2128
|
})) {
|
|
2002
2129
|
const named = c.selector ? selectorLabelWords(c.selector).filter((x) => words2.has(x)) : [];
|
|
@@ -2064,7 +2191,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
2064
2191
|
);
|
|
2065
2192
|
for (const { c, key, ev } of presses) {
|
|
2066
2193
|
const out = spanOutputExtent(rated, c.t, c.t + 1e-3, key || void 0);
|
|
2067
|
-
if (!out || out.start <
|
|
2194
|
+
if (!out || out.start < start || out.start > start + o.duration)
|
|
2068
2195
|
continue;
|
|
2069
2196
|
const target = pinRectOnScreen(
|
|
2070
2197
|
{
|
|
@@ -2084,8 +2211,8 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
2084
2211
|
w: box.w,
|
|
2085
2212
|
h: box.h
|
|
2086
2213
|
};
|
|
2087
|
-
const
|
|
2088
|
-
if (
|
|
2214
|
+
const overlaps3 = L.x < target.x + target.w && L.x + L.w > target.x && L.y < target.y + target.h && L.y + L.h > target.y;
|
|
2215
|
+
if (overlaps3) {
|
|
2089
2216
|
warnings.push(
|
|
2090
2217
|
`overlays[${i}] sits over the thing being clicked at ${c.t.toFixed(1)}s (output ${out.start.toFixed(1)}s) \u2014 pin it to that step (pin: { step }) so it sits beside the target and follows it, or move it off`
|
|
2091
2218
|
);
|
|
@@ -2193,7 +2320,8 @@ import {
|
|
|
2193
2320
|
planForDigest as planForDigest2,
|
|
2194
2321
|
ratedSegments as ratedSegments2,
|
|
2195
2322
|
resolveExportSize,
|
|
2196
|
-
|
|
2323
|
+
cameraSpanExtents as cameraSpanExtents2,
|
|
2324
|
+
spanOutputExtents as spanOutputExtents2
|
|
2197
2325
|
} from "@vosjs/studio-core";
|
|
2198
2326
|
|
|
2199
2327
|
// src/plugin/server.ts
|
|
@@ -2300,8 +2428,8 @@ function startTakeServer(rootDir, pages) {
|
|
|
2300
2428
|
const size = statSync(file).size;
|
|
2301
2429
|
const buf = readFileSync(file);
|
|
2302
2430
|
const type = sniffMediaType(buf.subarray(0, MEDIA_HEAD_BYTES)) ?? MIME[extname(file)] ?? "application/octet-stream";
|
|
2303
|
-
const
|
|
2304
|
-
const m =
|
|
2431
|
+
const range2 = req.headers.range;
|
|
2432
|
+
const m = range2 && /^bytes=(\d*)-(\d*)$/.exec(range2);
|
|
2305
2433
|
if (m) {
|
|
2306
2434
|
const start = m[1] ? parseInt(m[1], 10) : 0;
|
|
2307
2435
|
const end = m[2] ? Math.min(parseInt(m[2], 10), size - 1) : size - 1;
|
|
@@ -3234,7 +3362,7 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
3234
3362
|
});
|
|
3235
3363
|
await page.addInitScript(() => {
|
|
3236
3364
|
;
|
|
3237
|
-
globalThis.__name = (
|
|
3365
|
+
globalThis.__name = (f3) => f3;
|
|
3238
3366
|
});
|
|
3239
3367
|
await page.goto(`${server.base}/digest.html`);
|
|
3240
3368
|
await page.waitForFunction("window.__pageReady__ === true");
|
|
@@ -3461,24 +3589,25 @@ function zoomApexes(doc) {
|
|
|
3461
3589
|
});
|
|
3462
3590
|
continue;
|
|
3463
3591
|
}
|
|
3464
|
-
const
|
|
3465
|
-
if (!
|
|
3592
|
+
const exts = cameraSpanExtents2(rated, z.in, z.out, z.media);
|
|
3593
|
+
if (!exts.length) {
|
|
3466
3594
|
skipped.push({
|
|
3467
3595
|
id: z.id,
|
|
3468
3596
|
...of,
|
|
3469
3597
|
in: z.in,
|
|
3470
3598
|
out: z.out,
|
|
3471
|
-
reason: "the cut keeps none of it, so it never plays"
|
|
3599
|
+
reason: spanOutputExtents2(rated, z.in, z.out, z.media).length ? "every pass of it plays in a segment whose camera rests" : "the cut keeps none of it, so it never plays"
|
|
3472
3600
|
});
|
|
3473
3601
|
continue;
|
|
3474
3602
|
}
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3603
|
+
for (const ext2 of exts)
|
|
3604
|
+
apexes.push({
|
|
3605
|
+
id: z.id,
|
|
3606
|
+
...of,
|
|
3607
|
+
time: (ext2.start + ext2.end) / 2,
|
|
3608
|
+
start: ext2.start,
|
|
3609
|
+
end: ext2.end
|
|
3610
|
+
});
|
|
3482
3611
|
}
|
|
3483
3612
|
return { apexes, skipped };
|
|
3484
3613
|
}
|
|
@@ -3966,20 +4095,30 @@ import {
|
|
|
3966
4095
|
docOutputDuration,
|
|
3967
4096
|
dropTemplate,
|
|
3968
4097
|
endCardClips,
|
|
4098
|
+
hasReplay as hasReplay2,
|
|
3969
4099
|
migrateMotion,
|
|
4100
|
+
momentOutputs as momentOutputs2,
|
|
3970
4101
|
ratedSegments as ratedSegments4,
|
|
3971
4102
|
sameMedia,
|
|
3972
|
-
spanOutputExtent as
|
|
4103
|
+
spanOutputExtent as spanOutputExtent3
|
|
3973
4104
|
} from "@vosjs/studio-core";
|
|
3974
4105
|
|
|
3975
4106
|
// src/plugin/moments.ts
|
|
3976
|
-
import {
|
|
4107
|
+
import {
|
|
4108
|
+
hasReplay,
|
|
4109
|
+
longestOutputExtent,
|
|
4110
|
+
momentOutputs,
|
|
4111
|
+
spanOutputExtent as spanOutputExtent2
|
|
4112
|
+
} from "@vosjs/studio-core";
|
|
3977
4113
|
var STEP_SETTLE_SECONDS = 0.4;
|
|
3978
4114
|
function stepOutputTime(rated, step, settle2) {
|
|
3979
4115
|
const src = step.tEnd + settle2;
|
|
3980
|
-
const
|
|
3981
|
-
if (!
|
|
3982
|
-
return
|
|
4116
|
+
const ext2 = spanOutputExtent2(rated, src, src + 1e-3);
|
|
4117
|
+
if (!ext2) return null;
|
|
4118
|
+
if (!hasReplay(rated)) return ext2.start;
|
|
4119
|
+
const pass = longestOutputExtent(rated, step.tStart, step.tEnd);
|
|
4120
|
+
const after = pass ? momentOutputs(rated, src).find((o) => o >= pass.start - 1e-6) : void 0;
|
|
4121
|
+
return after ?? ext2.start;
|
|
3983
4122
|
}
|
|
3984
4123
|
|
|
3985
4124
|
// src/plugin/motionPlan.ts
|
|
@@ -4049,18 +4188,26 @@ function pickTrack(catalog, ask) {
|
|
|
4049
4188
|
const want = ask.trim().toLowerCase();
|
|
4050
4189
|
return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
|
|
4051
4190
|
}
|
|
4052
|
-
function clickTimes(doc,
|
|
4191
|
+
function clickTimes(doc, range2) {
|
|
4053
4192
|
const rated = ratedSegments4(doc);
|
|
4054
|
-
const
|
|
4193
|
+
const replayed = hasReplay2(rated);
|
|
4194
|
+
const at2 = [];
|
|
4055
4195
|
for (const e of doc.source.cursor) {
|
|
4056
4196
|
if (e.type !== "down") continue;
|
|
4057
4197
|
const src = e.t / 1e3;
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4198
|
+
if (replayed) {
|
|
4199
|
+
at2.push(...momentOutputs2(rated, src));
|
|
4200
|
+
continue;
|
|
4201
|
+
}
|
|
4202
|
+
const ext2 = spanOutputExtent3(rated, src, src + 1e-3);
|
|
4203
|
+
if (ext2) at2.push(ext2.start);
|
|
4204
|
+
}
|
|
4205
|
+
if (replayed) at2.sort((a, b) => a - b);
|
|
4206
|
+
const out = [];
|
|
4207
|
+
for (const t of at2) {
|
|
4208
|
+
if (t < range2[0] || t > range2[1]) continue;
|
|
4062
4209
|
if (out.length && t - out[out.length - 1] < 0.12) continue;
|
|
4063
|
-
out.push(+(t -
|
|
4210
|
+
out.push(+(t - range2[0]).toFixed(3));
|
|
4064
4211
|
}
|
|
4065
4212
|
return out;
|
|
4066
4213
|
}
|
|
@@ -4102,7 +4249,7 @@ function proposeMotion(input, opts) {
|
|
|
4102
4249
|
const notes = [];
|
|
4103
4250
|
const skipped = [];
|
|
4104
4251
|
const footage = outputLength(doc);
|
|
4105
|
-
const
|
|
4252
|
+
const range2 = [0, footage];
|
|
4106
4253
|
const entrance = launch.entrance;
|
|
4107
4254
|
delete doc.frame.entrance;
|
|
4108
4255
|
if (!off(entrance)) {
|
|
@@ -4257,7 +4404,7 @@ function proposeMotion(input, opts) {
|
|
|
4257
4404
|
}
|
|
4258
4405
|
const click = catalog?.sfx.find((s) => s.slug === "sfx-click");
|
|
4259
4406
|
if (click && !doc.source.micKey && !off(launch.clicks)) {
|
|
4260
|
-
const times = clickTimes(doc,
|
|
4407
|
+
const times = clickTimes(doc, range2);
|
|
4261
4408
|
for (const [i, t] of times.entries()) {
|
|
4262
4409
|
clips.push({
|
|
4263
4410
|
id: `${CLICK_ID_PREFIX}${i}`,
|
|
@@ -4460,11 +4607,11 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4460
4607
|
);
|
|
4461
4608
|
continue;
|
|
4462
4609
|
}
|
|
4463
|
-
let
|
|
4610
|
+
let range2 = opts.range;
|
|
4464
4611
|
let seconds = videoSeconds;
|
|
4465
4612
|
if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
|
|
4466
4613
|
if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
|
|
4467
|
-
|
|
4614
|
+
range2 = [0, d.maxSeconds];
|
|
4468
4615
|
seconds = d.maxSeconds;
|
|
4469
4616
|
opts.onPhase?.(
|
|
4470
4617
|
`note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
|
|
@@ -4484,7 +4631,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4484
4631
|
height: d.px.h,
|
|
4485
4632
|
format: "mp4",
|
|
4486
4633
|
parallel: opts.parallel,
|
|
4487
|
-
range,
|
|
4634
|
+
range: range2,
|
|
4488
4635
|
bitrate,
|
|
4489
4636
|
overrides: videoOverrides(d),
|
|
4490
4637
|
onProgress: opts.onProgress
|
|
@@ -4571,7 +4718,7 @@ async function browserLogin(origin, r, opts) {
|
|
|
4571
4718
|
if (opts.openBrowser && verifyUrlOk(verifyUrl, origin))
|
|
4572
4719
|
void launchDefaultBrowser(verifyUrl);
|
|
4573
4720
|
r.log("Waiting for approval\u2026 (Ctrl-C to cancel)");
|
|
4574
|
-
const sleep = opts.sleep ?? ((
|
|
4721
|
+
const sleep = opts.sleep ?? ((ms2) => new Promise((res) => setTimeout(res, ms2)));
|
|
4575
4722
|
let intervalMs = baseIntervalMs;
|
|
4576
4723
|
let failures = 0;
|
|
4577
4724
|
let capNoted = false;
|
|
@@ -4673,8 +4820,8 @@ function markExtension(url) {
|
|
|
4673
4820
|
const e = m[1].toLowerCase();
|
|
4674
4821
|
return e === "jpeg" ? "jpg" : e;
|
|
4675
4822
|
}
|
|
4676
|
-
function imageAspect(bytes,
|
|
4677
|
-
if (
|
|
4823
|
+
function imageAspect(bytes, ext2) {
|
|
4824
|
+
if (ext2 === "svg") {
|
|
4678
4825
|
const head = new TextDecoder().decode(bytes.subarray(0, 4096));
|
|
4679
4826
|
const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(
|
|
4680
4827
|
head
|
|
@@ -4686,11 +4833,11 @@ function imageAspect(bytes, ext) {
|
|
|
4686
4833
|
return null;
|
|
4687
4834
|
}
|
|
4688
4835
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4689
|
-
if (
|
|
4836
|
+
if (ext2 === "png") {
|
|
4690
4837
|
if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
|
|
4691
4838
|
return ratio(dv.getUint32(16), dv.getUint32(20));
|
|
4692
4839
|
}
|
|
4693
|
-
if (
|
|
4840
|
+
if (ext2 === "webp") {
|
|
4694
4841
|
if (bytes.length < 30) return null;
|
|
4695
4842
|
const tag = String.fromCharCode(...bytes.subarray(12, 16));
|
|
4696
4843
|
if (tag === "VP8X")
|
|
@@ -4709,7 +4856,7 @@ function imageAspect(bytes, ext) {
|
|
|
4709
4856
|
);
|
|
4710
4857
|
return null;
|
|
4711
4858
|
}
|
|
4712
|
-
if (
|
|
4859
|
+
if (ext2 === "jpg") {
|
|
4713
4860
|
let i = 2;
|
|
4714
4861
|
while (i + 9 < bytes.length) {
|
|
4715
4862
|
if (bytes[i] !== 255) return null;
|
|
@@ -4733,8 +4880,8 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
4733
4880
|
const one = async (role, name) => {
|
|
4734
4881
|
const src = (roles[role] ?? "").trim();
|
|
4735
4882
|
if (!src) return null;
|
|
4736
|
-
const
|
|
4737
|
-
if (!
|
|
4883
|
+
const ext2 = markExtension(src);
|
|
4884
|
+
if (!ext2) {
|
|
4738
4885
|
out.notes.push(
|
|
4739
4886
|
`${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
|
|
4740
4887
|
);
|
|
@@ -4757,7 +4904,7 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
4757
4904
|
);
|
|
4758
4905
|
return null;
|
|
4759
4906
|
}
|
|
4760
|
-
const aspect = imageAspect(bytes,
|
|
4907
|
+
const aspect = imageAspect(bytes, ext2);
|
|
4761
4908
|
if (!aspect) {
|
|
4762
4909
|
out.notes.push(
|
|
4763
4910
|
`${role}: ${src} has no readable size; the wordmark stands in`
|
|
@@ -4765,8 +4912,8 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
4765
4912
|
return null;
|
|
4766
4913
|
}
|
|
4767
4914
|
await mkdir4(join5(takeDir, "brand"), { recursive: true });
|
|
4768
|
-
await writeFile6(join5(takeDir, "brand", `${name}.${
|
|
4769
|
-
return { key: `/brand/${name}.${
|
|
4915
|
+
await writeFile6(join5(takeDir, "brand", `${name}.${ext2}`), bytes);
|
|
4916
|
+
return { key: `/brand/${name}.${ext2}`, aspect };
|
|
4770
4917
|
};
|
|
4771
4918
|
out.light = await one("logoUrl", "mark");
|
|
4772
4919
|
out.dark = await one("logoOnDarkUrl", "mark-on-dark");
|
|
@@ -4829,11 +4976,12 @@ import {
|
|
|
4829
4976
|
docCardLayout as docCardLayout2,
|
|
4830
4977
|
docZoomTrack as docZoomTrack2,
|
|
4831
4978
|
hexToRgb,
|
|
4979
|
+
layerStart as layerStart2,
|
|
4832
4980
|
parseCssColour,
|
|
4833
4981
|
pinBox as pinBox2,
|
|
4834
4982
|
ratedSegments as ratedSegments5,
|
|
4835
4983
|
resolvePins as resolvePins2,
|
|
4836
|
-
|
|
4984
|
+
longestOutputExtent as longestOutputExtent2
|
|
4837
4985
|
} from "@vosjs/studio-core";
|
|
4838
4986
|
|
|
4839
4987
|
// src/plugin/picture.ts
|
|
@@ -5238,15 +5386,15 @@ function windowForStep(doc, step, seconds = CALLOUT_DEFAULT_SECONDS) {
|
|
|
5238
5386
|
const s = steps.find((x) => x.id !== void 0 && x.id === step) ?? steps.find((x) => String(x.step) === String(step));
|
|
5239
5387
|
if (!s || s.skipped) return null;
|
|
5240
5388
|
const rated = ratedSegments5(doc);
|
|
5241
|
-
const
|
|
5242
|
-
if (!
|
|
5243
|
-
const at2 = s.do === "click" || s.do === "drag" ?
|
|
5389
|
+
const ext2 = longestOutputExtent2(rated, s.tStart, s.tEnd);
|
|
5390
|
+
if (!ext2) return null;
|
|
5391
|
+
const at2 = s.do === "click" || s.do === "drag" ? ext2.start : ext2.end;
|
|
5244
5392
|
const start = +(at2 + CALLOUT_LEAD_SECONDS).toFixed(3);
|
|
5245
5393
|
let end = start + seconds;
|
|
5246
5394
|
for (const n of steps) {
|
|
5247
5395
|
if (n.tStart <= s.tStart) continue;
|
|
5248
5396
|
if (n.do !== "scroll" && !n.navigated) continue;
|
|
5249
|
-
const ne =
|
|
5397
|
+
const ne = longestOutputExtent2(rated, n.tStart, n.tEnd);
|
|
5250
5398
|
if (ne && ne.start > start && ne.start < end) end = ne.start - 0.05;
|
|
5251
5399
|
}
|
|
5252
5400
|
return { start, duration: +Math.max(0.5, end - start).toFixed(3) };
|
|
@@ -5307,6 +5455,19 @@ function composeCallout(doc, register, ask) {
|
|
|
5307
5455
|
function isCalloutShape(s) {
|
|
5308
5456
|
return CALLOUT_SHAPES.includes(s);
|
|
5309
5457
|
}
|
|
5458
|
+
function groundTime(start, duration) {
|
|
5459
|
+
return +(start + Math.min(0.35, duration / 2)).toFixed(3);
|
|
5460
|
+
}
|
|
5461
|
+
function groundCheckTimes(doc) {
|
|
5462
|
+
const rated = ratedSegments5(doc);
|
|
5463
|
+
const times = /* @__PURE__ */ new Set();
|
|
5464
|
+
for (const o of doc.overlays ?? []) {
|
|
5465
|
+
if (o.kind !== "html") continue;
|
|
5466
|
+
const start = layerStart2(o, doc, rated);
|
|
5467
|
+
if (start !== null) times.add(groundTime(start, o.duration));
|
|
5468
|
+
}
|
|
5469
|
+
return [...times];
|
|
5470
|
+
}
|
|
5310
5471
|
async function groundFindings(doc, frameAt) {
|
|
5311
5472
|
const out = [];
|
|
5312
5473
|
const overlays = doc.overlays ?? [];
|
|
@@ -5314,9 +5475,12 @@ async function groundFindings(doc, frameAt) {
|
|
|
5314
5475
|
const camera = cameraModel2(doc.frame);
|
|
5315
5476
|
const track = docZoomTrack2(doc);
|
|
5316
5477
|
const pins = resolvePins2(doc, layout, camera, track);
|
|
5478
|
+
const rated = ratedSegments5(doc);
|
|
5317
5479
|
for (let i = 0; i < overlays.length; i++) {
|
|
5318
5480
|
const o = overlays[i];
|
|
5319
5481
|
if (o.kind !== "html") continue;
|
|
5482
|
+
const start = layerStart2(o, doc, rated);
|
|
5483
|
+
if (start === null) continue;
|
|
5320
5484
|
const card = calloutGroundOf(o.css ?? "");
|
|
5321
5485
|
if (!card) {
|
|
5322
5486
|
out.push({
|
|
@@ -5330,7 +5494,7 @@ async function groundFindings(doc, frameAt) {
|
|
|
5330
5494
|
});
|
|
5331
5495
|
continue;
|
|
5332
5496
|
}
|
|
5333
|
-
const t =
|
|
5497
|
+
const t = groundTime(start, o.duration);
|
|
5334
5498
|
const img = await frameAt(t);
|
|
5335
5499
|
if (!img) {
|
|
5336
5500
|
out.push({
|
|
@@ -5349,7 +5513,7 @@ async function groundFindings(doc, frameAt) {
|
|
|
5349
5513
|
let cx = o.transform.x * layout.W;
|
|
5350
5514
|
let cy = o.transform.y * layout.H;
|
|
5351
5515
|
if (pinned) {
|
|
5352
|
-
const v = sample(pinned.track, t -
|
|
5516
|
+
const v = sample(pinned.track, t - start, lerpArray);
|
|
5353
5517
|
cx = v[0] * layout.W;
|
|
5354
5518
|
cy = v[1] * layout.H;
|
|
5355
5519
|
}
|
|
@@ -5654,161 +5818,13 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5654
5818
|
// src/plugin/recorder.ts
|
|
5655
5819
|
import { writeFileSync } from "fs";
|
|
5656
5820
|
import { lstat, mkdir as mkdir5, readFile as readFile8, realpath } from "fs/promises";
|
|
5657
|
-
import { join as
|
|
5821
|
+
import { join as join10, resolve as resolve6, sep as sep2 } from "path";
|
|
5658
5822
|
import {
|
|
5659
5823
|
fileNameProblem,
|
|
5660
5824
|
inlineScript,
|
|
5661
5825
|
loadScriptFiles,
|
|
5662
5826
|
recordTake as recordTakeCore
|
|
5663
5827
|
} from "@vosjs/render-core/record";
|
|
5664
|
-
function outputsSink(dir) {
|
|
5665
|
-
const outDir = join8(dir, "outputs");
|
|
5666
|
-
return {
|
|
5667
|
-
save: async (name, bytes) => {
|
|
5668
|
-
const problem = fileNameProblem(name);
|
|
5669
|
-
if (problem) throw new Error(`outputs/${name}: ${problem}`);
|
|
5670
|
-
await refuseSymlink(outDir);
|
|
5671
|
-
await mkdir5(outDir, { recursive: true });
|
|
5672
|
-
await writeFileSafe(join8(outDir, name), bytes);
|
|
5673
|
-
}
|
|
5674
|
-
};
|
|
5675
|
-
}
|
|
5676
|
-
var PAGE_SOUND_DIR = "page-sound";
|
|
5677
|
-
function pageSoundSink(dir) {
|
|
5678
|
-
const soundDir = join8(dir, PAGE_SOUND_DIR);
|
|
5679
|
-
return {
|
|
5680
|
-
save: async (name, bytes) => {
|
|
5681
|
-
const problem = fileNameProblem(name);
|
|
5682
|
-
if (problem) throw new Error(`${PAGE_SOUND_DIR}/${name}: ${problem}`);
|
|
5683
|
-
await refuseSymlink(soundDir);
|
|
5684
|
-
await mkdir5(soundDir, { recursive: true });
|
|
5685
|
-
await writeFileSafe(join8(soundDir, name), bytes);
|
|
5686
|
-
return `${PAGE_SOUND_DIR}/${name}`;
|
|
5687
|
-
}
|
|
5688
|
-
};
|
|
5689
|
-
}
|
|
5690
|
-
function actionsDirFiles(dir) {
|
|
5691
|
-
return {
|
|
5692
|
-
read: async (name) => {
|
|
5693
|
-
const problem = fileNameProblem(name);
|
|
5694
|
-
if (problem) throw new Error(problem);
|
|
5695
|
-
const file = join8(dir, name);
|
|
5696
|
-
if ((await lstat(file)).isSymbolicLink())
|
|
5697
|
-
throw new Error(
|
|
5698
|
-
`${name} is a symbolic link, which a script never follows`
|
|
5699
|
-
);
|
|
5700
|
-
const [real, root] = await Promise.all([realpath(file), realpath(dir)]);
|
|
5701
|
-
if (!real.startsWith(root + sep2))
|
|
5702
|
-
throw new Error(`${name} resolves outside the actions file's directory`);
|
|
5703
|
-
return new Uint8Array(await readFile8(real));
|
|
5704
|
-
}
|
|
5705
|
-
};
|
|
5706
|
-
}
|
|
5707
|
-
var pasteCopyName = (name) => name.startsWith("paste-") ? name : `paste-${name}`;
|
|
5708
|
-
async function keepPasteFiles(actions, actionsDir, takeDir) {
|
|
5709
|
-
if (!actions.steps.some((s) => s.do === "paste" && s.file)) return actions;
|
|
5710
|
-
const files = actionsDirFiles(actionsDir);
|
|
5711
|
-
const steps = await Promise.all(
|
|
5712
|
-
actions.steps.map(async (s) => {
|
|
5713
|
-
if (s.do !== "paste" || !s.file) return s;
|
|
5714
|
-
const name = pasteCopyName(s.file);
|
|
5715
|
-
const same = resolve6(actionsDir) === resolve6(takeDir) && name === s.file;
|
|
5716
|
-
if (!same)
|
|
5717
|
-
await writeFileSafe(join8(takeDir, name), await files.read(s.file));
|
|
5718
|
-
return { ...s, file: name };
|
|
5719
|
-
})
|
|
5720
|
-
);
|
|
5721
|
-
return { ...actions, steps };
|
|
5722
|
-
}
|
|
5723
|
-
async function scriptForHost(actions, actionsDir) {
|
|
5724
|
-
const pathsOnly = {
|
|
5725
|
-
steps: actions.steps.map(
|
|
5726
|
-
(s) => s.do === "paste" ? { do: "wait", ms: 0 } : s
|
|
5727
|
-
)
|
|
5728
|
-
};
|
|
5729
|
-
const files = await loadScriptFiles(pathsOnly, actionsDirFiles(actionsDir));
|
|
5730
|
-
if ("problem" in files) throw new Error(files.problem);
|
|
5731
|
-
return inlineScript(actions, files);
|
|
5732
|
-
}
|
|
5733
|
-
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
5734
|
-
const dry = opts.dryRun === true;
|
|
5735
|
-
const result = await recordTakeCore(
|
|
5736
|
-
browser,
|
|
5737
|
-
url,
|
|
5738
|
-
actions,
|
|
5739
|
-
{
|
|
5740
|
-
frame: (file, bytes) => {
|
|
5741
|
-
writeFileSync(join8(paths.framesDir, file), bytes);
|
|
5742
|
-
}
|
|
5743
|
-
},
|
|
5744
|
-
log,
|
|
5745
|
-
{
|
|
5746
|
-
...opts,
|
|
5747
|
-
outputs: outputsSink(paths.dir),
|
|
5748
|
-
sounds: pageSoundSink(paths.dir),
|
|
5749
|
-
...opts.actionsDir ? { files: actionsDirFiles(opts.actionsDir) } : {},
|
|
5750
|
-
env: process.env,
|
|
5751
|
-
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
|
|
5752
|
-
}
|
|
5753
|
-
);
|
|
5754
|
-
if (!dry) {
|
|
5755
|
-
if (opts.actionsDir)
|
|
5756
|
-
result.actions = await keepPasteFiles(
|
|
5757
|
-
result.actions,
|
|
5758
|
-
opts.actionsDir,
|
|
5759
|
-
paths.dir
|
|
5760
|
-
);
|
|
5761
|
-
await writeJson(paths.cursor, result.events);
|
|
5762
|
-
await writeJson(paths.meta, result.meta, true);
|
|
5763
|
-
await writeJson(paths.framesIndex, result.frames);
|
|
5764
|
-
await writeJson(paths.actions, result.actions, true);
|
|
5765
|
-
}
|
|
5766
|
-
return result;
|
|
5767
|
-
}
|
|
5768
|
-
|
|
5769
|
-
// src/plugin/run.ts
|
|
5770
|
-
import {
|
|
5771
|
-
RecordRefusedError,
|
|
5772
|
-
guardVerdict,
|
|
5773
|
-
guardedSteps,
|
|
5774
|
-
hostedRefusals,
|
|
5775
|
-
readyProblem,
|
|
5776
|
-
overrunLine
|
|
5777
|
-
} from "@vosjs/render-core/record";
|
|
5778
|
-
|
|
5779
|
-
// src/plugin/wall.ts
|
|
5780
|
-
import {
|
|
5781
|
-
WALL_PROBE,
|
|
5782
|
-
WallError,
|
|
5783
|
-
isIdpHost,
|
|
5784
|
-
isSignInPath,
|
|
5785
|
-
redirectedAway,
|
|
5786
|
-
wallVerdict
|
|
5787
|
-
} from "@vosjs/render-core/record";
|
|
5788
|
-
|
|
5789
|
-
// src/plugin/exposure.ts
|
|
5790
|
-
import {
|
|
5791
|
-
EXPOSURE_ADVICE,
|
|
5792
|
-
EXPOSURE_MATCHERS_SRC,
|
|
5793
|
-
EXPOSURE_PROBE,
|
|
5794
|
-
ExposureLog,
|
|
5795
|
-
NoticeLog,
|
|
5796
|
-
exposureLine,
|
|
5797
|
-
maskInitScript,
|
|
5798
|
-
noticeAdvice,
|
|
5799
|
-
noticeLine
|
|
5800
|
-
} from "@vosjs/render-core/record";
|
|
5801
|
-
|
|
5802
|
-
// src/plugin/setup.ts
|
|
5803
|
-
import {
|
|
5804
|
-
SetupEnvError,
|
|
5805
|
-
SetupError,
|
|
5806
|
-
parseHeaders,
|
|
5807
|
-
resolveSetupText,
|
|
5808
|
-
runSetup,
|
|
5809
|
-
setupEnvNames,
|
|
5810
|
-
validateSetup
|
|
5811
|
-
} from "@vosjs/render-core/record";
|
|
5812
5828
|
|
|
5813
5829
|
// src/plugin/session.ts
|
|
5814
5830
|
import { spawn } from "child_process";
|
|
@@ -5821,13 +5837,13 @@ import {
|
|
|
5821
5837
|
statSync as statSync2
|
|
5822
5838
|
} from "fs";
|
|
5823
5839
|
import { homedir, tmpdir } from "os";
|
|
5824
|
-
import { join as
|
|
5840
|
+
import { join as join9 } from "path";
|
|
5825
5841
|
import { chromium } from "playwright";
|
|
5826
5842
|
|
|
5827
5843
|
// src/plugin/sessionWindow.ts
|
|
5828
5844
|
import { execFile as execFile2 } from "child_process";
|
|
5829
5845
|
import { existsSync as existsSync8 } from "fs";
|
|
5830
|
-
import { join as
|
|
5846
|
+
import { join as join8 } from "path";
|
|
5831
5847
|
import { createInterface } from "readline";
|
|
5832
5848
|
function parseProcessTable(out) {
|
|
5833
5849
|
const rows = [];
|
|
@@ -5863,19 +5879,19 @@ function chromeTree(rows, pid) {
|
|
|
5863
5879
|
}
|
|
5864
5880
|
function readChromeTree(pid) {
|
|
5865
5881
|
if (process.platform === "win32") return Promise.resolve(null);
|
|
5866
|
-
return new Promise((
|
|
5882
|
+
return new Promise((resolve21) => {
|
|
5867
5883
|
execFile2(
|
|
5868
5884
|
"ps",
|
|
5869
5885
|
["-A", "-o", "pid=", "-o", "ppid=", "-o", "command="],
|
|
5870
5886
|
{ maxBuffer: 16 * 1024 * 1024, timeout: 5e3 },
|
|
5871
5887
|
(err, stdout) => {
|
|
5872
|
-
if (err)
|
|
5873
|
-
else
|
|
5888
|
+
if (err) resolve21(null);
|
|
5889
|
+
else resolve21(chromeTree(parseProcessTable(stdout), pid));
|
|
5874
5890
|
}
|
|
5875
5891
|
);
|
|
5876
5892
|
});
|
|
5877
5893
|
}
|
|
5878
|
-
async function helpersGone(pids,
|
|
5894
|
+
async function helpersGone(pids, ms2 = 1e4, sleep = realSleep) {
|
|
5879
5895
|
const alive = (p) => {
|
|
5880
5896
|
try {
|
|
5881
5897
|
process.kill(p, 0);
|
|
@@ -5884,10 +5900,10 @@ async function helpersGone(pids, ms = 1e4, sleep = realSleep) {
|
|
|
5884
5900
|
return e.code === "EPERM";
|
|
5885
5901
|
}
|
|
5886
5902
|
};
|
|
5887
|
-
const until = Date.now() +
|
|
5903
|
+
const until = Date.now() + ms2;
|
|
5888
5904
|
while (pids.some(alive) && Date.now() < until) await sleep(100);
|
|
5889
5905
|
}
|
|
5890
|
-
var realSleep = (
|
|
5906
|
+
var realSleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
5891
5907
|
async function waitForFinish(w) {
|
|
5892
5908
|
const sleep = w.sleep ?? realSleep;
|
|
5893
5909
|
const pollMs = w.pollMs ?? 1e3;
|
|
@@ -5972,7 +5988,7 @@ function enterPressed() {
|
|
|
5972
5988
|
} };
|
|
5973
5989
|
const rl = createInterface({ input: process.stdin });
|
|
5974
5990
|
const pressed = new Promise(
|
|
5975
|
-
(
|
|
5991
|
+
(resolve21) => rl.once("line", () => resolve21())
|
|
5976
5992
|
);
|
|
5977
5993
|
return { pressed, close: () => rl.close() };
|
|
5978
5994
|
}
|
|
@@ -6004,8 +6020,8 @@ function cookieReport(state, rows, host) {
|
|
|
6004
6020
|
}
|
|
6005
6021
|
function cookieStore(profileDir) {
|
|
6006
6022
|
for (const p of [
|
|
6007
|
-
|
|
6008
|
-
|
|
6023
|
+
join8(profileDir, "Default", "Network", "Cookies"),
|
|
6024
|
+
join8(profileDir, "Default", "Cookies")
|
|
6009
6025
|
])
|
|
6010
6026
|
if (existsSync8(p)) return p;
|
|
6011
6027
|
return null;
|
|
@@ -6061,8 +6077,18 @@ function cookieWatch(profileDir, host, log) {
|
|
|
6061
6077
|
};
|
|
6062
6078
|
}
|
|
6063
6079
|
|
|
6080
|
+
// src/plugin/wall.ts
|
|
6081
|
+
import {
|
|
6082
|
+
WALL_PROBE,
|
|
6083
|
+
WallError,
|
|
6084
|
+
isIdpHost,
|
|
6085
|
+
isSignInPath,
|
|
6086
|
+
redirectedAway,
|
|
6087
|
+
wallVerdict
|
|
6088
|
+
} from "@vosjs/render-core/record";
|
|
6089
|
+
|
|
6064
6090
|
// src/plugin/session.ts
|
|
6065
|
-
var SESSIONS_DIR =
|
|
6091
|
+
var SESSIONS_DIR = join9(homedir(), ".config", "vos", "sessions");
|
|
6066
6092
|
var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
6067
6093
|
function sessionNameVerdict(name) {
|
|
6068
6094
|
if (!name) return "a session needs a name: --name <app>";
|
|
@@ -6071,20 +6097,20 @@ function sessionNameVerdict(name) {
|
|
|
6071
6097
|
return null;
|
|
6072
6098
|
}
|
|
6073
6099
|
function sessionDir(name) {
|
|
6074
|
-
return
|
|
6100
|
+
return join9(SESSIONS_DIR, name);
|
|
6075
6101
|
}
|
|
6076
6102
|
function systemChromePath() {
|
|
6077
6103
|
const explicit = process.env.VOS_BROWSER_PATH;
|
|
6078
6104
|
if (explicit) return explicit;
|
|
6079
6105
|
const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
|
|
6080
|
-
|
|
6106
|
+
join9(
|
|
6081
6107
|
process.env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
6082
6108
|
"Google",
|
|
6083
6109
|
"Chrome",
|
|
6084
6110
|
"Application",
|
|
6085
6111
|
"chrome.exe"
|
|
6086
6112
|
),
|
|
6087
|
-
|
|
6113
|
+
join9(
|
|
6088
6114
|
process.env["LOCALAPPDATA"] ?? "",
|
|
6089
6115
|
"Google",
|
|
6090
6116
|
"Chrome",
|
|
@@ -6135,14 +6161,14 @@ async function openSessionWindow(opts) {
|
|
|
6135
6161
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
6136
6162
|
let failed = null;
|
|
6137
6163
|
let code = null;
|
|
6138
|
-
const exited = new Promise((
|
|
6164
|
+
const exited = new Promise((resolve21) => {
|
|
6139
6165
|
child.once("exit", (c) => {
|
|
6140
6166
|
code = c;
|
|
6141
|
-
|
|
6167
|
+
resolve21();
|
|
6142
6168
|
});
|
|
6143
6169
|
child.once("error", (e) => {
|
|
6144
6170
|
failed = e;
|
|
6145
|
-
|
|
6171
|
+
resolve21();
|
|
6146
6172
|
});
|
|
6147
6173
|
});
|
|
6148
6174
|
try {
|
|
@@ -6189,6 +6215,7 @@ async function withProfile(dir, fn, opts = {}) {
|
|
|
6189
6215
|
...explicit ? { executablePath: explicit } : { channel: "chrome" },
|
|
6190
6216
|
// The real keychain: the whole reason a profile Chrome wrote can be read.
|
|
6191
6217
|
ignoreDefaultArgs: ["--use-mock-keychain"],
|
|
6218
|
+
args: [...BROWSER_SWITCHES],
|
|
6192
6219
|
...opts.viewport ? { viewport: opts.viewport, deviceScaleFactor: 1 } : {}
|
|
6193
6220
|
});
|
|
6194
6221
|
try {
|
|
@@ -6235,7 +6262,7 @@ async function profileVerdict(dir, url) {
|
|
|
6235
6262
|
return sessionVerdictOf(null, stranger);
|
|
6236
6263
|
}
|
|
6237
6264
|
async function strangerWall(url) {
|
|
6238
|
-
const dir = mkdtempSync(
|
|
6265
|
+
const dir = mkdtempSync(join9(tmpdir(), "vos-stranger-"));
|
|
6239
6266
|
try {
|
|
6240
6267
|
return await withProfile(dir, (ctx) => arrivalWall(ctx, url));
|
|
6241
6268
|
} finally {
|
|
@@ -6298,7 +6325,7 @@ function summaryLine(s) {
|
|
|
6298
6325
|
function listSessions() {
|
|
6299
6326
|
if (!existsSync9(SESSIONS_DIR)) return [];
|
|
6300
6327
|
return readdirSync(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
|
|
6301
|
-
const st = statSync2(
|
|
6328
|
+
const st = statSync2(join9(SESSIONS_DIR, d.name));
|
|
6302
6329
|
return {
|
|
6303
6330
|
name: d.name,
|
|
6304
6331
|
ageDays: Math.floor(
|
|
@@ -6319,33 +6346,201 @@ function looksLikeStorageState(value) {
|
|
|
6319
6346
|
return Array.isArray(v.cookies) && Array.isArray(v.origins);
|
|
6320
6347
|
}
|
|
6321
6348
|
|
|
6322
|
-
// src/plugin/
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
}
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6349
|
+
// src/plugin/recorder.ts
|
|
6350
|
+
function outputsSink(dir) {
|
|
6351
|
+
const outDir = join10(dir, "outputs");
|
|
6352
|
+
return {
|
|
6353
|
+
save: async (name, bytes) => {
|
|
6354
|
+
const problem = fileNameProblem(name);
|
|
6355
|
+
if (problem) throw new Error(`outputs/${name}: ${problem}`);
|
|
6356
|
+
await refuseSymlink(outDir);
|
|
6357
|
+
await mkdir5(outDir, { recursive: true });
|
|
6358
|
+
await writeFileSafe(join10(outDir, name), bytes);
|
|
6359
|
+
}
|
|
6360
|
+
};
|
|
6334
6361
|
}
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
const
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
}
|
|
6348
|
-
|
|
6362
|
+
var PAGE_SOUND_DIR = "page-sound";
|
|
6363
|
+
function pageSoundSink(dir) {
|
|
6364
|
+
const soundDir = join10(dir, PAGE_SOUND_DIR);
|
|
6365
|
+
return {
|
|
6366
|
+
save: async (name, bytes) => {
|
|
6367
|
+
const problem = fileNameProblem(name);
|
|
6368
|
+
if (problem) throw new Error(`${PAGE_SOUND_DIR}/${name}: ${problem}`);
|
|
6369
|
+
await refuseSymlink(soundDir);
|
|
6370
|
+
await mkdir5(soundDir, { recursive: true });
|
|
6371
|
+
await writeFileSafe(join10(soundDir, name), bytes);
|
|
6372
|
+
return `${PAGE_SOUND_DIR}/${name}`;
|
|
6373
|
+
}
|
|
6374
|
+
};
|
|
6375
|
+
}
|
|
6376
|
+
function actionsDirFiles(dir) {
|
|
6377
|
+
return {
|
|
6378
|
+
read: async (name) => {
|
|
6379
|
+
const problem = fileNameProblem(name);
|
|
6380
|
+
if (problem) throw new Error(problem);
|
|
6381
|
+
const file = join10(dir, name);
|
|
6382
|
+
if ((await lstat(file)).isSymbolicLink())
|
|
6383
|
+
throw new Error(
|
|
6384
|
+
`${name} is a symbolic link, which a script never follows`
|
|
6385
|
+
);
|
|
6386
|
+
const [real, root] = await Promise.all([realpath(file), realpath(dir)]);
|
|
6387
|
+
if (!real.startsWith(root + sep2))
|
|
6388
|
+
throw new Error(`${name} resolves outside the actions file's directory`);
|
|
6389
|
+
return new Uint8Array(await readFile8(real));
|
|
6390
|
+
}
|
|
6391
|
+
};
|
|
6392
|
+
}
|
|
6393
|
+
function peerStateFrom(dir) {
|
|
6394
|
+
return async (name) => {
|
|
6395
|
+
const file = resolve6(dir, name);
|
|
6396
|
+
let raw;
|
|
6397
|
+
try {
|
|
6398
|
+
raw = await readFile8(file, "utf8");
|
|
6399
|
+
} catch {
|
|
6400
|
+
throw new Error(
|
|
6401
|
+
`there is no file at ${file}; a storage state is never copied into a take, so name it by a path from the actions file (or an absolute one, such as one saved under ~/.config/vos)`
|
|
6402
|
+
);
|
|
6403
|
+
}
|
|
6404
|
+
let parsed;
|
|
6405
|
+
try {
|
|
6406
|
+
parsed = JSON.parse(raw);
|
|
6407
|
+
} catch {
|
|
6408
|
+
parsed = null;
|
|
6409
|
+
}
|
|
6410
|
+
if (!looksLikeStorageState(parsed))
|
|
6411
|
+
throw new Error(
|
|
6412
|
+
`${file} is not a Playwright storage state ({ cookies, origins }); save one with context.storageState({ path })`
|
|
6413
|
+
);
|
|
6414
|
+
return file;
|
|
6415
|
+
};
|
|
6416
|
+
}
|
|
6417
|
+
var pasteCopyName = (name) => name.startsWith("paste-") ? name : `paste-${name}`;
|
|
6418
|
+
async function keepPasteFiles(actions, actionsDir, takeDir) {
|
|
6419
|
+
if (!actions.steps.some((s) => s.do === "paste" && s.file)) return actions;
|
|
6420
|
+
const files = actionsDirFiles(actionsDir);
|
|
6421
|
+
const steps = await Promise.all(
|
|
6422
|
+
actions.steps.map(async (s) => {
|
|
6423
|
+
if (s.do !== "paste" || !s.file) return s;
|
|
6424
|
+
const name = pasteCopyName(s.file);
|
|
6425
|
+
const same = resolve6(actionsDir) === resolve6(takeDir) && name === s.file;
|
|
6426
|
+
if (!same)
|
|
6427
|
+
await writeFileSafe(join10(takeDir, name), await files.read(s.file));
|
|
6428
|
+
return { ...s, file: name };
|
|
6429
|
+
})
|
|
6430
|
+
);
|
|
6431
|
+
return { ...actions, steps };
|
|
6432
|
+
}
|
|
6433
|
+
async function scriptForHost(actions, actionsDir) {
|
|
6434
|
+
const pathsOnly = {
|
|
6435
|
+
steps: actions.steps.map(
|
|
6436
|
+
(s) => s.do === "paste" ? { do: "wait", ms: 0 } : s
|
|
6437
|
+
)
|
|
6438
|
+
};
|
|
6439
|
+
const files = await loadScriptFiles(pathsOnly, actionsDirFiles(actionsDir));
|
|
6440
|
+
if ("problem" in files) throw new Error(files.problem);
|
|
6441
|
+
return inlineScript(actions, files);
|
|
6442
|
+
}
|
|
6443
|
+
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
6444
|
+
const dry = opts.dryRun === true;
|
|
6445
|
+
const result = await recordTakeCore(
|
|
6446
|
+
browser,
|
|
6447
|
+
url,
|
|
6448
|
+
actions,
|
|
6449
|
+
{
|
|
6450
|
+
frame: (file, bytes) => {
|
|
6451
|
+
writeFileSync(join10(paths.framesDir, file), bytes);
|
|
6452
|
+
}
|
|
6453
|
+
},
|
|
6454
|
+
log,
|
|
6455
|
+
{
|
|
6456
|
+
...opts,
|
|
6457
|
+
outputs: outputsSink(paths.dir),
|
|
6458
|
+
sounds: pageSoundSink(paths.dir),
|
|
6459
|
+
...opts.actionsDir ? {
|
|
6460
|
+
files: actionsDirFiles(opts.actionsDir),
|
|
6461
|
+
peerStorageState: peerStateFrom(opts.actionsDir)
|
|
6462
|
+
} : {},
|
|
6463
|
+
env: process.env,
|
|
6464
|
+
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
|
|
6465
|
+
}
|
|
6466
|
+
);
|
|
6467
|
+
if (!dry) {
|
|
6468
|
+
if (opts.actionsDir)
|
|
6469
|
+
result.actions = await keepPasteFiles(
|
|
6470
|
+
result.actions,
|
|
6471
|
+
opts.actionsDir,
|
|
6472
|
+
paths.dir
|
|
6473
|
+
);
|
|
6474
|
+
await writeJson(paths.cursor, result.events);
|
|
6475
|
+
await writeJson(paths.meta, result.meta, true);
|
|
6476
|
+
await writeJson(paths.framesIndex, result.frames);
|
|
6477
|
+
await writeJson(paths.actions, result.actions, true);
|
|
6478
|
+
}
|
|
6479
|
+
return result;
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6482
|
+
// src/plugin/run.ts
|
|
6483
|
+
import {
|
|
6484
|
+
RecordRefusedError,
|
|
6485
|
+
guardVerdict,
|
|
6486
|
+
guardedSteps,
|
|
6487
|
+
hostedRefusals,
|
|
6488
|
+
readyProblem,
|
|
6489
|
+
overrunLine,
|
|
6490
|
+
sayValue
|
|
6491
|
+
} from "@vosjs/render-core/record";
|
|
6492
|
+
|
|
6493
|
+
// src/plugin/exposure.ts
|
|
6494
|
+
import {
|
|
6495
|
+
EXPOSURE_ADVICE,
|
|
6496
|
+
EXPOSURE_MATCHERS_SRC,
|
|
6497
|
+
EXPOSURE_PROBE,
|
|
6498
|
+
ExposureLog,
|
|
6499
|
+
NoticeLog,
|
|
6500
|
+
exposureLine,
|
|
6501
|
+
maskInitScript,
|
|
6502
|
+
noticeAdvice,
|
|
6503
|
+
noticeLine
|
|
6504
|
+
} from "@vosjs/render-core/record";
|
|
6505
|
+
|
|
6506
|
+
// src/plugin/setup.ts
|
|
6507
|
+
import {
|
|
6508
|
+
SetupEnvError,
|
|
6509
|
+
SetupError,
|
|
6510
|
+
parseHeaders,
|
|
6511
|
+
resolveSetupText,
|
|
6512
|
+
runSetup,
|
|
6513
|
+
setupEnvNames,
|
|
6514
|
+
validateSetup
|
|
6515
|
+
} from "@vosjs/render-core/record";
|
|
6516
|
+
|
|
6517
|
+
// src/plugin/recordingCap.ts
|
|
6518
|
+
import {
|
|
6519
|
+
capReached,
|
|
6520
|
+
cappedLine,
|
|
6521
|
+
clampWait,
|
|
6522
|
+
formatDurationCap
|
|
6523
|
+
} from "@vosjs/render-core/record";
|
|
6524
|
+
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
6525
|
+
function parseHostedCap(body) {
|
|
6526
|
+
const limits = body && typeof body === "object" ? body.limits : void 0;
|
|
6527
|
+
const cap = limits?.recordingMaxSeconds;
|
|
6528
|
+
return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
|
|
6529
|
+
}
|
|
6530
|
+
async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
|
|
6531
|
+
const ctl = new AbortController();
|
|
6532
|
+
const timer = setTimeout(() => ctl.abort(), 2e3);
|
|
6533
|
+
try {
|
|
6534
|
+
const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
|
|
6535
|
+
signal: ctl.signal,
|
|
6536
|
+
headers: bearerFor(key, origin)
|
|
6537
|
+
});
|
|
6538
|
+
if (!res.ok) return null;
|
|
6539
|
+
return parseHostedCap(await res.json());
|
|
6540
|
+
} catch {
|
|
6541
|
+
return null;
|
|
6542
|
+
} finally {
|
|
6543
|
+
clearTimeout(timer);
|
|
6349
6544
|
}
|
|
6350
6545
|
}
|
|
6351
6546
|
function defaultMaxDurationSeconds() {
|
|
@@ -6408,12 +6603,30 @@ var AFTER_STEP = 1;
|
|
|
6408
6603
|
var LAYER_AFTER_STEP = 5;
|
|
6409
6604
|
var WINDOW_SLACK = 0.05;
|
|
6410
6605
|
var LAYER_DRIFT = 0.5;
|
|
6411
|
-
function
|
|
6606
|
+
function matchByNeighbour(old, newSteps, oldSteps) {
|
|
6607
|
+
const named = oldSteps.filter(
|
|
6608
|
+
(s) => s.id && newSteps.some((n) => n.id === s.id && !n.skipped)
|
|
6609
|
+
);
|
|
6610
|
+
const before = named.filter((s) => s.step < old.step).sort((a, b) => b.step - a.step)[0];
|
|
6611
|
+
const after = named.filter((s) => s.step > old.step).sort((a, b) => a.step - b.step)[0];
|
|
6612
|
+
const found = [before, after].flatMap((anchor) => {
|
|
6613
|
+
if (!anchor) return [];
|
|
6614
|
+
const now = newSteps.find((n) => n.id === anchor.id);
|
|
6615
|
+
const hit = newSteps.find(
|
|
6616
|
+
(n) => n.step === now.step + old.step - anchor.step
|
|
6617
|
+
);
|
|
6618
|
+
return hit && !hit.skipped && !hit.id && hit.do === old.do && hit.selector === old.selector ? [hit] : [];
|
|
6619
|
+
});
|
|
6620
|
+
return found.length && found.every((h) => h === found[0]) ? found[0] : null;
|
|
6621
|
+
}
|
|
6622
|
+
function matchStep(old, newSteps, oldSteps = []) {
|
|
6412
6623
|
const usable = (s) => !!s && s.skipped !== true;
|
|
6413
6624
|
if (old.id) {
|
|
6414
6625
|
const byId = newSteps.find((s) => s.id === old.id);
|
|
6415
6626
|
return usable(byId) ? byId : null;
|
|
6416
6627
|
}
|
|
6628
|
+
const near2 = matchByNeighbour(old, newSteps, oldSteps);
|
|
6629
|
+
if (near2) return near2;
|
|
6417
6630
|
const atIndex = newSteps.find((s) => s.step === old.step);
|
|
6418
6631
|
if (usable(atIndex) && atIndex.do === old.do && atIndex.selector === old.selector) {
|
|
6419
6632
|
return atIndex;
|
|
@@ -6445,7 +6658,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
|
|
|
6445
6658
|
let matched = 0;
|
|
6446
6659
|
for (const old of oldSteps) {
|
|
6447
6660
|
if (old.skipped) continue;
|
|
6448
|
-
const hit = matchStep(old, newSteps);
|
|
6661
|
+
const hit = matchStep(old, newSteps, oldSteps);
|
|
6449
6662
|
if (!hit) {
|
|
6450
6663
|
unmatched.push(old);
|
|
6451
6664
|
continue;
|
|
@@ -6505,7 +6718,7 @@ function placeMoment(t, anchor, ctx) {
|
|
|
6505
6718
|
}
|
|
6506
6719
|
const under = stepUnder(t, ctx.oldSteps);
|
|
6507
6720
|
if (under) {
|
|
6508
|
-
const hit = matchStep(under, ctx.newSteps);
|
|
6721
|
+
const hit = matchStep(under, ctx.newSteps, ctx.oldSteps);
|
|
6509
6722
|
if (!hit) return { gone: under };
|
|
6510
6723
|
ctx.report.anchored++;
|
|
6511
6724
|
return { at: hit.tStart + (t - under.tStart), via: "step" };
|
|
@@ -6660,8 +6873,8 @@ function retimeOverlays(prev, next, report) {
|
|
|
6660
6873
|
const newDuration = next.source.meta.durationMs / 1e3;
|
|
6661
6874
|
const stepMap = buildStepMap(oldSteps, newSteps, oldDuration, newDuration);
|
|
6662
6875
|
const ctx = { oldSteps, newSteps, stepMap, report };
|
|
6663
|
-
const follow = (o, oldRef, newRef) => {
|
|
6664
|
-
const offset =
|
|
6876
|
+
const follow = (o, from, oldRef, newRef) => {
|
|
6877
|
+
const offset = from - sourceToOutput(oldPieces, oldRef);
|
|
6665
6878
|
const start = Math.max(0, sourceToOutput(newPieces, newRef) + offset);
|
|
6666
6879
|
return { ...o, start: +start.toFixed(3) };
|
|
6667
6880
|
};
|
|
@@ -6670,54 +6883,56 @@ function retimeOverlays(prev, next, report) {
|
|
|
6670
6883
|
const r = resolveLayerAnchor(next, o.anchor, newPieces);
|
|
6671
6884
|
if (r.start === null) {
|
|
6672
6885
|
report.flagged.push(
|
|
6673
|
-
`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`
|
|
6886
|
+
o.start === void 0 ? `overlay ${o.id}: anchored to step ${String(o.anchor.step)}, which is missing, skipped or cut in the new recording \u2014 it has no start to fall back to, so it plays nowhere; re-anchor it or give it a start` : `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`
|
|
6674
6887
|
);
|
|
6675
6888
|
return o;
|
|
6676
6889
|
}
|
|
6677
6890
|
report.anchored++;
|
|
6678
|
-
return { ...o, start: r.start };
|
|
6891
|
+
return o.start === void 0 ? o : { ...o, start: r.start };
|
|
6679
6892
|
}
|
|
6893
|
+
const at2 = o.start;
|
|
6894
|
+
if (at2 === void 0) return o;
|
|
6680
6895
|
const pin = o.pin;
|
|
6681
6896
|
if (pin?.step !== void 0) {
|
|
6682
6897
|
const was = stepByRef(oldSteps, pin.step);
|
|
6683
|
-
const now2 = was ? matchStep(was, newSteps) : stepByRef(newSteps, pin.step);
|
|
6898
|
+
const now2 = was ? matchStep(was, newSteps, oldSteps) : stepByRef(newSteps, pin.step);
|
|
6684
6899
|
if (!now2 || now2.skipped) {
|
|
6685
6900
|
report.flagged.push(
|
|
6686
|
-
`overlay ${o.id}: pinned to step ${String(pin.step)}, which is missing or skipped in the new recording \u2014 kept at ${
|
|
6901
|
+
`overlay ${o.id}: pinned to step ${String(pin.step)}, which is missing or skipped in the new recording \u2014 kept at ${at2.toFixed(2)}s; re-pin or re-place it`
|
|
6687
6902
|
);
|
|
6688
6903
|
return o;
|
|
6689
6904
|
}
|
|
6690
6905
|
report.anchored++;
|
|
6691
|
-
return follow(o, was?.tStart ?? now2.tStart, now2.tStart);
|
|
6906
|
+
return follow(o, at2, was?.tStart ?? now2.tStart, now2.tStart);
|
|
6692
6907
|
}
|
|
6693
6908
|
if (typeof pin?.press === "number") {
|
|
6694
6909
|
const placed = placeMoment(pin.press, void 0, ctx);
|
|
6695
6910
|
if ("gone" in placed) {
|
|
6696
6911
|
report.flagged.push(
|
|
6697
|
-
`overlay ${o.id}: pinned to a press at ${pin.press.toFixed(2)}s, and ${goneWords(placed.gone)} \u2014 kept at ${
|
|
6912
|
+
`overlay ${o.id}: pinned to a press at ${pin.press.toFixed(2)}s, and ${goneWords(placed.gone)} \u2014 kept at ${at2.toFixed(2)}s; re-pin or re-place it`
|
|
6698
6913
|
);
|
|
6699
6914
|
return o;
|
|
6700
6915
|
}
|
|
6701
|
-
const moved = follow(o, pin.press, placed.at);
|
|
6916
|
+
const moved = follow(o, at2, pin.press, placed.at);
|
|
6702
6917
|
return { ...moved, pin: { ...pin, press: +placed.at.toFixed(3) } };
|
|
6703
6918
|
}
|
|
6704
6919
|
const under = stepUnder(
|
|
6705
|
-
outputToSource(oldPieces,
|
|
6920
|
+
outputToSource(oldPieces, at2),
|
|
6706
6921
|
oldSteps,
|
|
6707
6922
|
LAYER_AFTER_STEP
|
|
6708
6923
|
);
|
|
6709
6924
|
if (!under) return o;
|
|
6710
|
-
const now = matchStep(under, newSteps);
|
|
6925
|
+
const now = matchStep(under, newSteps, oldSteps);
|
|
6711
6926
|
if (!now) {
|
|
6712
6927
|
report.flagged.push(
|
|
6713
|
-
`overlay ${o.id} (${
|
|
6928
|
+
`overlay ${o.id} (${at2.toFixed(2)}s): it sat over step ${stepName(under)}, which is missing or skipped in the new recording \u2014 kept at its output time; re-place it`
|
|
6714
6929
|
);
|
|
6715
6930
|
return o;
|
|
6716
6931
|
}
|
|
6717
|
-
const expected = follow(o, under.tStart, now.tStart).start;
|
|
6718
|
-
if (Math.abs(expected -
|
|
6932
|
+
const expected = follow(o, at2, under.tStart, now.tStart).start;
|
|
6933
|
+
if (Math.abs(expected - at2) > LAYER_DRIFT) {
|
|
6719
6934
|
report.flagged.push(
|
|
6720
|
-
`overlay ${o.id} (${
|
|
6935
|
+
`overlay ${o.id} (${at2.toFixed(2)}s): it sat over step ${stepName(under)}, which now plays at ${expected.toFixed(2)}s in the cut \u2014 kept at ${at2.toFixed(2)}s by the output-time contract; pin it to follow (pin: { step: ${JSON.stringify(under.id ?? under.step)} }) or move it`
|
|
6721
6936
|
);
|
|
6722
6937
|
}
|
|
6723
6938
|
return o;
|
|
@@ -6961,14 +7176,14 @@ import { createHash } from "crypto";
|
|
|
6961
7176
|
import { existsSync as existsSync12, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
6962
7177
|
import { readFile as readFile11 } from "fs/promises";
|
|
6963
7178
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
6964
|
-
import { basename, join as join14 } from "path";
|
|
7179
|
+
import { basename as basename2, join as join14 } from "path";
|
|
6965
7180
|
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
6966
7181
|
|
|
6967
7182
|
// src/plugin/takeScript.ts
|
|
6968
7183
|
import { existsSync as existsSync11 } from "fs";
|
|
6969
7184
|
import { readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
6970
7185
|
import { readFileSync as readFileSync2 } from "fs";
|
|
6971
|
-
import { join as join12 } from "path";
|
|
7186
|
+
import { basename, join as join12 } from "path";
|
|
6972
7187
|
function scriptFromText(text) {
|
|
6973
7188
|
try {
|
|
6974
7189
|
const v = JSON.parse(text);
|
|
@@ -6984,6 +7199,8 @@ function scriptToWrite(doc, hasLocal) {
|
|
|
6984
7199
|
`;
|
|
6985
7200
|
}
|
|
6986
7201
|
function publicScript(script) {
|
|
7202
|
+
const peers = publicPeers(script.peers);
|
|
7203
|
+
if (peers) script = { ...script, peers };
|
|
6987
7204
|
const setup = script.setup;
|
|
6988
7205
|
if (!Array.isArray(setup)) return script;
|
|
6989
7206
|
return {
|
|
@@ -7005,6 +7222,15 @@ function publicScript(script) {
|
|
|
7005
7222
|
})
|
|
7006
7223
|
};
|
|
7007
7224
|
}
|
|
7225
|
+
function publicPeers(peers) {
|
|
7226
|
+
if (!Array.isArray(peers)) return null;
|
|
7227
|
+
return peers.map((p) => {
|
|
7228
|
+
if (!p || typeof p !== "object") return p;
|
|
7229
|
+
const peer = p;
|
|
7230
|
+
if (typeof peer.storageState !== "string") return peer;
|
|
7231
|
+
return { ...peer, storageState: basename(peer.storageState) };
|
|
7232
|
+
});
|
|
7233
|
+
}
|
|
7008
7234
|
function embedScript(doc, dir) {
|
|
7009
7235
|
const path = join12(dir, "actions.json");
|
|
7010
7236
|
if (!existsSync11(path)) return;
|
|
@@ -7018,6 +7244,11 @@ async function writeScriptBeside(dir, doc) {
|
|
|
7018
7244
|
await writeFile7(path, text, "utf8");
|
|
7019
7245
|
return true;
|
|
7020
7246
|
}
|
|
7247
|
+
async function readScript(dir) {
|
|
7248
|
+
const path = join12(dir, "actions.json");
|
|
7249
|
+
if (!existsSync11(path)) return null;
|
|
7250
|
+
return scriptFromText(await readFile10(path, "utf8"));
|
|
7251
|
+
}
|
|
7021
7252
|
|
|
7022
7253
|
// src/plugin/folder.ts
|
|
7023
7254
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
@@ -7511,6 +7742,10 @@ async function pushTake(dir, flags, r) {
|
|
|
7511
7742
|
"no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
|
|
7512
7743
|
);
|
|
7513
7744
|
}
|
|
7745
|
+
const pasteFiles = await readPasteFiles(
|
|
7746
|
+
await readScript(dir) ?? take.doc.source.script,
|
|
7747
|
+
dir
|
|
7748
|
+
);
|
|
7514
7749
|
const ctx = apiContext(flags);
|
|
7515
7750
|
const tracked = readSyncState(dir);
|
|
7516
7751
|
const firstPush = tracked === null;
|
|
@@ -7567,12 +7802,6 @@ async function pushTake(dir, flags, r) {
|
|
|
7567
7802
|
filename: take.paths.recordingName
|
|
7568
7803
|
});
|
|
7569
7804
|
const filename = nameForType(take.paths.recordingName, contentType);
|
|
7570
|
-
if (filename !== take.paths.recordingName) {
|
|
7571
|
-
r.log(
|
|
7572
|
-
` ${take.paths.recordingName} holds ${contentType} \u2014 uploading it as ${filename}`
|
|
7573
|
-
);
|
|
7574
|
-
}
|
|
7575
|
-
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7576
7805
|
let upload;
|
|
7577
7806
|
try {
|
|
7578
7807
|
upload = await uploadAsset(ctx, new Uint8Array(bytes), {
|
|
@@ -7581,6 +7810,15 @@ async function pushTake(dir, flags, r) {
|
|
|
7581
7810
|
contentHash: hash,
|
|
7582
7811
|
// The take's length so the server holds it to the plan's cap.
|
|
7583
7812
|
...take.meta.durationMs > 0 ? { durationSeconds: take.meta.durationMs / 1e3 } : {},
|
|
7813
|
+
// Said once the platform asks for the bytes: a re-push of a hosted
|
|
7814
|
+
// take sends none, and says only that it reused them.
|
|
7815
|
+
onSending: () => {
|
|
7816
|
+
if (filename !== take.paths.recordingName)
|
|
7817
|
+
r.log(
|
|
7818
|
+
` ${take.paths.recordingName} holds ${contentType} \u2014 uploading it as ${filename}`
|
|
7819
|
+
);
|
|
7820
|
+
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7821
|
+
},
|
|
7584
7822
|
onPart: (done, total) => r.log(` part ${done}/${total}`)
|
|
7585
7823
|
});
|
|
7586
7824
|
} catch (e) {
|
|
@@ -7596,6 +7834,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7596
7834
|
const docForPush = structuredClone(sendDoc);
|
|
7597
7835
|
docForPush.source.videoKey = assetUrl;
|
|
7598
7836
|
embedScript(docForPush, dir);
|
|
7837
|
+
await sendPasteFiles(docForPush, pasteFiles, ctx, (l) => r.log(l));
|
|
7599
7838
|
await uploadDocRefs(sourceSidecarRefs(docForPush), dir, ctx, (l) => r.log(l));
|
|
7600
7839
|
for (const field of ["micKey", "camKey"]) {
|
|
7601
7840
|
const key = docForPush.source[field];
|
|
@@ -7610,7 +7849,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7610
7849
|
const lowered = lowerToComposition3(docForPush);
|
|
7611
7850
|
const config = { ...lowered.config, data: lowered.data };
|
|
7612
7851
|
if (!state) {
|
|
7613
|
-
const title = flags.title ?? `Take ${
|
|
7852
|
+
const title = flags.title ?? `Take ${basename2(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
7614
7853
|
const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "take"}-${Date.now().toString(36)}`;
|
|
7615
7854
|
const created = await api(ctx, "/vos", {
|
|
7616
7855
|
method: "POST",
|
|
@@ -7797,7 +8036,7 @@ async function pullTake(dir, flags, r) {
|
|
|
7797
8036
|
const doc = hostedDoc;
|
|
7798
8037
|
const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
7799
8038
|
if (existsSync12(localRecording)) {
|
|
7800
|
-
doc.source.videoKey =
|
|
8039
|
+
doc.source.videoKey = basename2(localRecording);
|
|
7801
8040
|
}
|
|
7802
8041
|
await writeJson(join14(dir, "doc.json"), doc, true);
|
|
7803
8042
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
@@ -7840,6 +8079,10 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
7840
8079
|
${lint.problems.join("\n ")}`
|
|
7841
8080
|
);
|
|
7842
8081
|
}
|
|
8082
|
+
const pasteFiles = await readPasteFiles(
|
|
8083
|
+
await readScript(dir) ?? take.doc.source.script,
|
|
8084
|
+
dir
|
|
8085
|
+
);
|
|
7843
8086
|
const state = readSyncState(dir);
|
|
7844
8087
|
const hosted = state?.hostedMedia ?? {};
|
|
7845
8088
|
if (!Object.keys(hosted).length)
|
|
@@ -7882,6 +8125,7 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
7882
8125
|
for (const line of plays.lines) r.log(line);
|
|
7883
8126
|
const docForPush = structuredClone(plays.doc);
|
|
7884
8127
|
embedScript(docForPush, dir);
|
|
8128
|
+
await claimablePasteFiles(docForPush, pasteFiles, hosted, (l) => r.log(l));
|
|
7885
8129
|
for (const ref of claimRefs(docForPush)) {
|
|
7886
8130
|
const origin2 = hosted[takeRelativeFile(ref.key)];
|
|
7887
8131
|
if (!origin2)
|
|
@@ -7892,7 +8136,7 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
7892
8136
|
}
|
|
7893
8137
|
const lowered = lowerToComposition3(docForPush);
|
|
7894
8138
|
const config = { ...lowered.config, data: lowered.data };
|
|
7895
|
-
const title = (flags.title ?? (state?.title ? `${state.title} remix` :
|
|
8139
|
+
const title = (flags.title ?? (state?.title ? `${state.title} remix` : basename2(dir))).slice(0, 200);
|
|
7896
8140
|
const body = { title, config, doc: docForPush };
|
|
7897
8141
|
if (state?.vosId) body.remixOfId = state.vosId;
|
|
7898
8142
|
if (flags.prompt) body.prompt = flags.prompt;
|
|
@@ -7919,7 +8163,7 @@ async function pushTakeClaimable(dir, flags, r) {
|
|
|
7919
8163
|
import { existsSync as existsSync14 } from "fs";
|
|
7920
8164
|
import { mkdir as mkdir7, readFile as readFile12 } from "fs/promises";
|
|
7921
8165
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
7922
|
-
import { basename as
|
|
8166
|
+
import { basename as basename4, dirname as dirname2, join as join15 } from "path";
|
|
7923
8167
|
import {
|
|
7924
8168
|
CURRENT_CONFIG_VERSION,
|
|
7925
8169
|
migrateConfig,
|
|
@@ -7928,7 +8172,7 @@ import {
|
|
|
7928
8172
|
import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
7929
8173
|
|
|
7930
8174
|
// src/plugin/programFiles.ts
|
|
7931
|
-
import { basename as
|
|
8175
|
+
import { basename as basename3, extname as extname2, relative as relative2 } from "path";
|
|
7932
8176
|
import { existsSync as existsSync13, realpathSync as realpathSync2 } from "fs";
|
|
7933
8177
|
function isLocalName(value) {
|
|
7934
8178
|
if (/^(https?:|data:|blob:|asset:|media:)/.test(value)) return false;
|
|
@@ -7939,7 +8183,7 @@ function isLocalName(value) {
|
|
|
7939
8183
|
}
|
|
7940
8184
|
var FILE_EXT = /\.(png|jpe?g|webp|gif|avif|svg|mp4|webm|mov|mp3|m4a|ogg|wav|flac|aac|glb|gltf|woff2?|ttf|otf|hdr|exr)$/i;
|
|
7941
8185
|
function manifestName(file, taken) {
|
|
7942
|
-
const stem =
|
|
8186
|
+
const stem = basename3(file, extname2(file)).replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=[0-9])/, "_") || "file";
|
|
7943
8187
|
let name = stem;
|
|
7944
8188
|
for (let n = 2; taken.has(name); n++) name = `${stem}_${n}`;
|
|
7945
8189
|
taken.add(name);
|
|
@@ -8358,7 +8602,7 @@ async function cmdPushProgram(argv) {
|
|
|
8358
8602
|
"--claimable creates a NEW claimable vos and cannot iterate (--vos). After the human claims it, iterate with their key: vos push --vos <id>"
|
|
8359
8603
|
);
|
|
8360
8604
|
}
|
|
8361
|
-
const fallback = state?.title ? `${state.title} remix` :
|
|
8605
|
+
const fallback = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos program";
|
|
8362
8606
|
const title2 = (strFlag(flags, "title") ?? fallback).slice(0, 100);
|
|
8363
8607
|
const files2 = await collectFiles(config, programDoc, dir, r);
|
|
8364
8608
|
let uploadToken;
|
|
@@ -8525,7 +8769,7 @@ ${landedLines(landed2)}` : "")
|
|
|
8525
8769
|
return EXIT_OK;
|
|
8526
8770
|
}
|
|
8527
8771
|
const remixOfId = pushTarget.kind === "create" ? pushTarget.remixOfId : void 0;
|
|
8528
|
-
const fallbackTitle = state?.title ? `${state.title} remix` :
|
|
8772
|
+
const fallbackTitle = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos remix";
|
|
8529
8773
|
const title = (strFlag(flags, "title") ?? fallbackTitle).slice(0, 100);
|
|
8530
8774
|
const pushFolder = folderRef ? await resolvePushFolder(origin, key, folderRef) : void 0;
|
|
8531
8775
|
const folderId = pushFolder?.id;
|
|
@@ -8851,7 +9095,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8851
9095
|
// src/plugin/asset.ts
|
|
8852
9096
|
import { createHash as createHash2 } from "crypto";
|
|
8853
9097
|
import { readFileSync as readFileSync4 } from "fs";
|
|
8854
|
-
import { basename as
|
|
9098
|
+
import { basename as basename5 } from "path";
|
|
8855
9099
|
|
|
8856
9100
|
// src/plugin/assetWords.ts
|
|
8857
9101
|
function hostedFile(raw) {
|
|
@@ -9116,7 +9360,7 @@ async function cmdPush(argv) {
|
|
|
9116
9360
|
const folder = folderRef ? resolveFolder(await listFolders(origin, key), folderRef) : null;
|
|
9117
9361
|
const uploaded = [];
|
|
9118
9362
|
for (const file of positionals) {
|
|
9119
|
-
const name =
|
|
9363
|
+
const name = basename5(file);
|
|
9120
9364
|
const body = readFileSync4(file);
|
|
9121
9365
|
const media = resolveMediaType({
|
|
9122
9366
|
head: body.subarray(0, MEDIA_HEAD_BYTES),
|
|
@@ -9634,7 +9878,7 @@ function trackingCwd(vosId) {
|
|
|
9634
9878
|
|
|
9635
9879
|
// src/plugin/buildProgram.ts
|
|
9636
9880
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
9637
|
-
import { basename as
|
|
9881
|
+
import { basename as basename6, dirname as dirname3, join as join17, resolve as resolve7 } from "path";
|
|
9638
9882
|
import { pathToFileURL } from "url";
|
|
9639
9883
|
import { parse } from "acorn";
|
|
9640
9884
|
import { fullAncestor } from "acorn-walk";
|
|
@@ -9842,7 +10086,7 @@ async function cmdBuild(argv) {
|
|
|
9842
10086
|
throw new UsageError(
|
|
9843
10087
|
`${source} exports no program: \`export default { version: 2, duration, camera, createContent, createTimeline, \u2026 }\``
|
|
9844
10088
|
);
|
|
9845
|
-
const built = buildProgram(program,
|
|
10089
|
+
const built = buildProgram(program, basename6(file), text);
|
|
9846
10090
|
for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
|
|
9847
10091
|
if (built.problems.length) {
|
|
9848
10092
|
r.done(
|
|
@@ -10077,7 +10321,7 @@ import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
|
|
|
10077
10321
|
import { existsSync as existsSync17, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
|
|
10078
10322
|
import { createServer as createServer2 } from "http";
|
|
10079
10323
|
import { createRequire } from "module";
|
|
10080
|
-
import { basename as
|
|
10324
|
+
import { basename as basename7, extname as extname3, join as join19 } from "path";
|
|
10081
10325
|
function themeColors(sources) {
|
|
10082
10326
|
const out = {};
|
|
10083
10327
|
const re = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*['"](#[0-9a-fA-F]{3,8})['"]/g;
|
|
@@ -10091,10 +10335,10 @@ function themeColors(sources) {
|
|
|
10091
10335
|
function remotionScenes(seqs, fps) {
|
|
10092
10336
|
const seen = /* @__PURE__ */ new Map();
|
|
10093
10337
|
const live = seqs.filter((s) => s.durationInFrames > 0);
|
|
10094
|
-
const
|
|
10338
|
+
const overlaps3 = (a, b) => a.from < b.from + b.durationInFrames && b.from < a.from + a.durationInFrames;
|
|
10095
10339
|
return live.filter(
|
|
10096
10340
|
(s) => !live.some(
|
|
10097
|
-
(o) => o !== s && o.durationInFrames > s.durationInFrames &&
|
|
10341
|
+
(o) => o !== s && o.durationInFrames > s.durationInFrames && overlaps3(o, s)
|
|
10098
10342
|
)
|
|
10099
10343
|
).sort((a, b) => a.from - b.from).map((s, i) => {
|
|
10100
10344
|
const base = s.name ?? `scene${i + 1}`;
|
|
@@ -10357,7 +10601,7 @@ async function readRemotion(dir, log) {
|
|
|
10357
10601
|
const sources = sourceTexts(dir);
|
|
10358
10602
|
const publicDir = join19(dir, "public");
|
|
10359
10603
|
const media = tree.audio.map((a) => {
|
|
10360
|
-
const name =
|
|
10604
|
+
const name = basename7(a.src.split("?")[0]);
|
|
10361
10605
|
const local = existsSync17(join19(publicDir, name)) ? `public/${name}` : a.src;
|
|
10362
10606
|
return {
|
|
10363
10607
|
kind: "audio",
|
|
@@ -10860,7 +11104,7 @@ async function readInventory(source, opts) {
|
|
|
10860
11104
|
// src/plugin/portScaffold.ts
|
|
10861
11105
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
10862
11106
|
import { copyFileSync, existsSync as existsSync19, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
10863
|
-
import { basename as
|
|
11107
|
+
import { basename as basename8, dirname as dirname6, join as join21, resolve as resolve10 } from "path";
|
|
10864
11108
|
import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
10865
11109
|
|
|
10866
11110
|
// src/plugin/painterLib.ts
|
|
@@ -11270,7 +11514,7 @@ function planScaffold(inv, opts) {
|
|
|
11270
11514
|
const copies = [];
|
|
11271
11515
|
inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
|
|
11272
11516
|
const from = resolve10(opts.sourceDir, m.src);
|
|
11273
|
-
const name =
|
|
11517
|
+
const name = basename8(from);
|
|
11274
11518
|
if (!existsSync19(from)) {
|
|
11275
11519
|
report.push(
|
|
11276
11520
|
`- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
|
|
@@ -11423,7 +11667,7 @@ ${sceneTodos}${springHint}
|
|
|
11423
11667
|
}
|
|
11424
11668
|
function reportText(inv, r) {
|
|
11425
11669
|
const lines = [
|
|
11426
|
-
`# Port report: ${inv.source === "." && inv.root ?
|
|
11670
|
+
`# Port report: ${inv.source === "." && inv.root ? basename8(inv.root) : inv.source}`,
|
|
11427
11671
|
"",
|
|
11428
11672
|
`${inv.engine} piece, ${inv.width}\xD7${inv.height}${inv.fps ? ` at ${inv.fps} fps` : ""}, ${inv.duration ?? "?"} s. A program has no size of its own: render it with \`--width ${inv.width} --height ${inv.height}\`.`,
|
|
11429
11673
|
"",
|
|
@@ -11587,11 +11831,11 @@ function scaffoldSourceDir(inv) {
|
|
|
11587
11831
|
// src/plugin/recipe.ts
|
|
11588
11832
|
import { createHash as createHash3 } from "crypto";
|
|
11589
11833
|
import { readFileSync as readFileSync9 } from "fs";
|
|
11590
|
-
import { basename as
|
|
11834
|
+
import { basename as basename9 } from "path";
|
|
11591
11835
|
var BOOLEAN_FLAGS9 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
11592
11836
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
11593
11837
|
function recipeFilename(file) {
|
|
11594
|
-
const name =
|
|
11838
|
+
const name = basename9(file);
|
|
11595
11839
|
if (!/\.md$/i.test(name)) {
|
|
11596
11840
|
throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
|
|
11597
11841
|
}
|
|
@@ -12077,7 +12321,7 @@ function isWallError(error) {
|
|
|
12077
12321
|
return !!error && /^(\[\w+\] )?The page is behind a sign-in/.test(error);
|
|
12078
12322
|
}
|
|
12079
12323
|
async function recordHosted(input) {
|
|
12080
|
-
const sleep = input.sleep ?? ((
|
|
12324
|
+
const sleep = input.sleep ?? ((ms2) => new Promise((r) => setTimeout(r, ms2)));
|
|
12081
12325
|
const posted = await apiJson(input.origin, "/api/record/jobs", {
|
|
12082
12326
|
method: "POST",
|
|
12083
12327
|
key: input.key,
|
|
@@ -12640,8 +12884,8 @@ async function cmdActions(argv) {
|
|
|
12640
12884
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
12641
12885
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
12642
12886
|
}
|
|
12643
|
-
const { readFile:
|
|
12644
|
-
const text = await
|
|
12887
|
+
const { readFile: readFile19 } = await import("fs/promises");
|
|
12888
|
+
const text = await readFile19(resolve13(input), "utf8");
|
|
12645
12889
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
12646
12890
|
for (const p of problems) r.log(` ${p}`);
|
|
12647
12891
|
const result = convertAgentBrowser(records, {
|
|
@@ -12678,9 +12922,9 @@ function summary2(result) {
|
|
|
12678
12922
|
}
|
|
12679
12923
|
async function cmdActionsScript(input, flags) {
|
|
12680
12924
|
const r = createReporter(flags.json === true);
|
|
12681
|
-
const { readFile:
|
|
12682
|
-
const { shotList, formatShotList } = await import("./shotList-
|
|
12683
|
-
const raw = JSON.parse(await
|
|
12925
|
+
const { readFile: readFile19 } = await import("fs/promises");
|
|
12926
|
+
const { shotList, formatShotList } = await import("./shotList-MSNWLVDN.js");
|
|
12927
|
+
const raw = JSON.parse(await readFile19(resolve13(input), "utf8"));
|
|
12684
12928
|
const errors = validateActions(raw);
|
|
12685
12929
|
if (errors.length) throw new UsageError(`${input}:
|
|
12686
12930
|
${errors.join("\n ")}`);
|
|
@@ -12692,7 +12936,7 @@ async function cmdActionsScript(input, flags) {
|
|
|
12692
12936
|
// src/plugin/ingest.ts
|
|
12693
12937
|
import { existsSync as existsSync21 } from "fs";
|
|
12694
12938
|
import { copyFile, mkdir as mkdir8, readFile as readFile13, stat as stat2 } from "fs/promises";
|
|
12695
|
-
import { basename as
|
|
12939
|
+
import { basename as basename10, join as join23, resolve as resolve14 } from "path";
|
|
12696
12940
|
|
|
12697
12941
|
// src/plugin/traceInflate.ts
|
|
12698
12942
|
import { inflateRawSync } from "zlib";
|
|
@@ -12765,7 +13009,7 @@ async function probeVideoFile(path) {
|
|
|
12765
13009
|
input.getPrimaryAudioTrack()
|
|
12766
13010
|
]);
|
|
12767
13011
|
const mimeType = format.mimeType;
|
|
12768
|
-
const
|
|
13012
|
+
const ext2 = mimeType.includes("mp4") ? "mp4" : mimeType.includes("quicktime") ? "mov" : "webm";
|
|
12769
13013
|
return {
|
|
12770
13014
|
width: video.displayWidth,
|
|
12771
13015
|
height: video.displayHeight,
|
|
@@ -12773,14 +13017,14 @@ async function probeVideoFile(path) {
|
|
|
12773
13017
|
fps: Math.round(stats.averagePacketRate * 100) / 100,
|
|
12774
13018
|
hasAudio: !!audio,
|
|
12775
13019
|
mimeType,
|
|
12776
|
-
ext
|
|
13020
|
+
ext: ext2
|
|
12777
13021
|
};
|
|
12778
13022
|
} finally {
|
|
12779
13023
|
await input.dispose?.();
|
|
12780
13024
|
}
|
|
12781
13025
|
}
|
|
12782
|
-
async function copyRecording(src, dest,
|
|
12783
|
-
if (
|
|
13026
|
+
async function copyRecording(src, dest, ext2) {
|
|
13027
|
+
if (ext2 === "mov") {
|
|
12784
13028
|
await copyFile(src, dest);
|
|
12785
13029
|
return false;
|
|
12786
13030
|
}
|
|
@@ -12788,7 +13032,7 @@ async function copyRecording(src, dest, ext) {
|
|
|
12788
13032
|
source: new FilePathSource(src),
|
|
12789
13033
|
formats: ALL_FORMATS
|
|
12790
13034
|
});
|
|
12791
|
-
const format = () =>
|
|
13035
|
+
const format = () => ext2 === "mp4" ? new Mp4OutputFormat({ fastStart: "in-memory" }) : new WebMOutputFormat();
|
|
12792
13036
|
try {
|
|
12793
13037
|
const probe3 = await Conversion.init({
|
|
12794
13038
|
input,
|
|
@@ -12819,7 +13063,7 @@ async function copyRecording(src, dest, ext) {
|
|
|
12819
13063
|
}
|
|
12820
13064
|
}
|
|
12821
13065
|
async function readTraceFile(path, offsetMs) {
|
|
12822
|
-
const kind = traceKindOf(
|
|
13066
|
+
const kind = traceKindOf(basename10(path));
|
|
12823
13067
|
if (!kind) {
|
|
12824
13068
|
throw new UsageError(
|
|
12825
13069
|
`${path}: not a trace the recorder reads (trace.zip, .trace, .jsonl, .json or .csv)`
|
|
@@ -12840,7 +13084,7 @@ async function readTraceFile(path, offsetMs) {
|
|
|
12840
13084
|
return cursorFromPlaywrightTrace(text2, { offsetMs });
|
|
12841
13085
|
}
|
|
12842
13086
|
const text = await readFile13(path, "utf8");
|
|
12843
|
-
return cursorFromTrace(
|
|
13087
|
+
return cursorFromTrace(basename10(path), text, { offsetMs });
|
|
12844
13088
|
}
|
|
12845
13089
|
function parseViewport(v) {
|
|
12846
13090
|
if (!v) return null;
|
|
@@ -12871,7 +13115,7 @@ async function cmdIngest(argv) {
|
|
|
12871
13115
|
"note: finished footage opens bare, so --background is not used (pass --as take for the screen-recording card and its ground)"
|
|
12872
13116
|
);
|
|
12873
13117
|
const backdrop = footage ? null : await takeBackdrop(flags, r);
|
|
12874
|
-
r.log(`probing ${
|
|
13118
|
+
r.log(`probing ${basename10(videoPath)}\u2026`);
|
|
12875
13119
|
const probed = await probeVideoFile(videoPath);
|
|
12876
13120
|
if (!(probed.durationMs > 0)) {
|
|
12877
13121
|
throw new UsageError(
|
|
@@ -12920,13 +13164,13 @@ async function cmdIngest(argv) {
|
|
|
12920
13164
|
const clicks = events.filter((e) => e.type === "down").length;
|
|
12921
13165
|
const seconds = (probed.durationMs / 1e3).toFixed(1);
|
|
12922
13166
|
const facts = `${seconds}s, ${probed.width}\xD7${probed.height} @ ${meta.fps} fps${probed.hasAudio ? ", audio" : ", no audio"}${remuxed ? "" : ", copied as is"}`;
|
|
12923
|
-
const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${
|
|
13167
|
+
const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${basename10(cursorPath)}${trace.dropped ? `, ${trace.dropped} not placed` : ""}; ${plan.zoomAuto} zoom${plan.zoomAuto === 1 ? "" : "s"} planned` : "no cursor track, so nothing was planned";
|
|
12924
13168
|
const opened = footage ? `opened as finished footage: no card, no browser bar, no drawn cursor, ${meta.fps > 30 ? 60 : 30} fps export (--as take for a screen recording's card)` : `opened as a screen recording: the card, its ground and the drawn cursor (--as footage to open it bare)`;
|
|
12925
13169
|
const next = footage && !trace ? `Next: vos push ${outDir} --wait \u2014 it lands as it is; trim or add to it in the studio` : trace ? `Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}` : `Add --cursor <trace> recorded beside the video (a Playwright trace.zip, stamped JSON records, or a t,x,y,type CSV) to plan zooms from its clicks; else place them by eye: vos frames ${outDir} --times 0,25%,50%,75%,100%, then edit doc.json`;
|
|
12926
13170
|
r.done(
|
|
12927
13171
|
{
|
|
12928
13172
|
out: outDir,
|
|
12929
|
-
recording:
|
|
13173
|
+
recording: basename10(recording),
|
|
12930
13174
|
durationMs: probed.durationMs,
|
|
12931
13175
|
width: probed.width,
|
|
12932
13176
|
height: probed.height,
|
|
@@ -12935,7 +13179,7 @@ async function cmdIngest(argv) {
|
|
|
12935
13179
|
hasAudio: probed.hasAudio,
|
|
12936
13180
|
remuxed,
|
|
12937
13181
|
cursor: trace ? {
|
|
12938
|
-
file:
|
|
13182
|
+
file: basename10(cursorPath),
|
|
12939
13183
|
kind: trace.kind,
|
|
12940
13184
|
events: events.length,
|
|
12941
13185
|
clicks,
|
|
@@ -12947,7 +13191,7 @@ async function cmdIngest(argv) {
|
|
|
12947
13191
|
backdrop: plan.backdrop ?? null,
|
|
12948
13192
|
as: footage ? "footage" : "take"
|
|
12949
13193
|
},
|
|
12950
|
-
`Ingested ${
|
|
13194
|
+
`Ingested ${basename10(videoPath)} \u2192 ${outDir}/${basename10(recording)} (${facts})
|
|
12951
13195
|
${opened}
|
|
12952
13196
|
${cursorLine}
|
|
12953
13197
|
${next}`
|
|
@@ -12959,12 +13203,13 @@ async function cmdIngest(argv) {
|
|
|
12959
13203
|
import { createRequire as createRequire2 } from "module";
|
|
12960
13204
|
import { existsSync as existsSync22 } from "fs";
|
|
12961
13205
|
import { mkdir as mkdir9, readFile as readFile14, stat as stat3, writeFile as writeFile10 } from "fs/promises";
|
|
12962
|
-
import { basename as
|
|
13206
|
+
import { basename as basename11, dirname as dirname8, extname as extname4, join as join24, resolve as resolve15 } from "path";
|
|
12963
13207
|
import { ALL_FORMATS as ALL_FORMATS2, FilePathSource as FilePathSource2, Input as Input2 } from "mediabunny";
|
|
12964
13208
|
|
|
12965
13209
|
// src/plugin/videoChecks.ts
|
|
12966
13210
|
var SCENE_CHANGE_AT = 0.12;
|
|
12967
13211
|
var CELL_CHANGED_AT = 0.03;
|
|
13212
|
+
var CELL_STRONG_AT = 0.06;
|
|
12968
13213
|
var SCENE_WINDOW = 5;
|
|
12969
13214
|
var UNIFORM_BELOW = 2;
|
|
12970
13215
|
var QUIET_BELOW_LUFS = -23;
|
|
@@ -12975,7 +13220,9 @@ var SCENE_SPIKE = 2.5;
|
|
|
12975
13220
|
var ISOLATED_CHANGE_AT = 0.03;
|
|
12976
13221
|
var ISOLATED_SPIKE = 8;
|
|
12977
13222
|
var ISOLATED_COVERAGE = 0.25;
|
|
12978
|
-
|
|
13223
|
+
var SCATTER_SHARE = 0.1;
|
|
13224
|
+
var SCATTER_BOX = 0.22;
|
|
13225
|
+
function sceneChanges2(times, changes, coverage, scatter) {
|
|
12979
13226
|
const out = [];
|
|
12980
13227
|
for (let i = 1; i < changes.length; i++) {
|
|
12981
13228
|
const c = changes[i];
|
|
@@ -12987,7 +13234,9 @@ function sceneChanges2(times, changes, coverage) {
|
|
|
12987
13234
|
const most = around.length ? around[around.length - 1] : 0;
|
|
12988
13235
|
const share = coverage ? coverage[i] : 1;
|
|
12989
13236
|
const standsOut = c >= SCENE_CHANGE_AT && c >= SCENE_SPIKE * mid;
|
|
12990
|
-
const
|
|
13237
|
+
const spread = scatter?.[i];
|
|
13238
|
+
const scattered = !!spread && spread.share >= SCATTER_SHARE && spread.box >= SCATTER_BOX;
|
|
13239
|
+
const standsAlone = c >= ISOLATED_CHANGE_AT && (share >= ISOLATED_COVERAGE || scattered) && c >= ISOLATED_SPIKE * most;
|
|
12991
13240
|
if (!standsOut && !standsAlone) continue;
|
|
12992
13241
|
out.push({
|
|
12993
13242
|
time: times[i],
|
|
@@ -13195,7 +13444,8 @@ function posterWarning(which, deviation) {
|
|
|
13195
13444
|
|
|
13196
13445
|
// src/plugin/checkVideo.ts
|
|
13197
13446
|
var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set(["json", "help", "strict"]);
|
|
13198
|
-
var THUMB_LONG_SIDE =
|
|
13447
|
+
var THUMB_LONG_SIDE = 160;
|
|
13448
|
+
var CELLS_LONG_SIDE = 16;
|
|
13199
13449
|
async function probeVideoFacts(file) {
|
|
13200
13450
|
const input = new Input2({
|
|
13201
13451
|
source: new FilePathSource2(file),
|
|
@@ -13295,22 +13545,43 @@ async function decodeInPage(opts) {
|
|
|
13295
13545
|
const coverageOf = (a, b) => {
|
|
13296
13546
|
const W = opts.thumbW;
|
|
13297
13547
|
const H = opts.thumbH;
|
|
13548
|
+
const C = opts.cellPx;
|
|
13298
13549
|
let cells = 0;
|
|
13299
13550
|
let changed = 0;
|
|
13300
|
-
|
|
13301
|
-
|
|
13551
|
+
let strong = 0;
|
|
13552
|
+
let x0 = W;
|
|
13553
|
+
let x1 = 0;
|
|
13554
|
+
let y0 = H;
|
|
13555
|
+
let y1 = 0;
|
|
13556
|
+
for (let cy = 0; cy < H; cy += C)
|
|
13557
|
+
for (let cx = 0; cx < W; cx += C) {
|
|
13302
13558
|
let s = 0;
|
|
13303
|
-
let
|
|
13304
|
-
for (let y = cy; y < Math.min(H, cy +
|
|
13305
|
-
for (let x = cx; x < Math.min(W, cx +
|
|
13559
|
+
let n2 = 0;
|
|
13560
|
+
for (let y = cy; y < Math.min(H, cy + C); y++)
|
|
13561
|
+
for (let x = cx; x < Math.min(W, cx + C); x++) {
|
|
13306
13562
|
const k = (y * W + x) * 3;
|
|
13307
13563
|
s += Math.abs(a[k] - b[k]) + Math.abs(a[k + 1] - b[k + 1]) + Math.abs(a[k + 2] - b[k + 2]);
|
|
13308
|
-
|
|
13564
|
+
n2 += 3;
|
|
13309
13565
|
}
|
|
13310
13566
|
cells++;
|
|
13311
|
-
|
|
13567
|
+
const m = s / Math.max(1, n2) / 255;
|
|
13568
|
+
if (m >= opts.cellChanged) changed++;
|
|
13569
|
+
if (m >= opts.cellStrong) {
|
|
13570
|
+
strong++;
|
|
13571
|
+
x0 = Math.min(x0, cx);
|
|
13572
|
+
x1 = Math.max(x1, Math.min(W, cx + C));
|
|
13573
|
+
y0 = Math.min(y0, cy);
|
|
13574
|
+
y1 = Math.max(y1, Math.min(H, cy + C));
|
|
13575
|
+
}
|
|
13576
|
+
}
|
|
13577
|
+
const n = Math.max(1, cells);
|
|
13578
|
+
return {
|
|
13579
|
+
share: changed / n,
|
|
13580
|
+
scatter: {
|
|
13581
|
+
share: strong / n,
|
|
13582
|
+
box: strong ? (x1 - x0) * (y1 - y0) / Math.max(1, W * H) : 0
|
|
13312
13583
|
}
|
|
13313
|
-
|
|
13584
|
+
};
|
|
13314
13585
|
};
|
|
13315
13586
|
const small = document.createElement("canvas");
|
|
13316
13587
|
small.width = opts.thumbW;
|
|
@@ -13323,6 +13594,8 @@ async function decodeInPage(opts) {
|
|
|
13323
13594
|
const times = [];
|
|
13324
13595
|
const changes = [];
|
|
13325
13596
|
const coverage = [];
|
|
13597
|
+
const scatter = [];
|
|
13598
|
+
const none = { share: 0, scatter: { share: 0, box: 0 } };
|
|
13326
13599
|
let next = -Infinity;
|
|
13327
13600
|
let prev = null;
|
|
13328
13601
|
let first = null;
|
|
@@ -13350,7 +13623,9 @@ async function decodeInPage(opts) {
|
|
|
13350
13623
|
}
|
|
13351
13624
|
times.push(t);
|
|
13352
13625
|
changes.push(prev ? difference(prev, g) : 0);
|
|
13353
|
-
|
|
13626
|
+
const cov = prev ? coverageOf(prev, g) : none;
|
|
13627
|
+
coverage.push(cov.share);
|
|
13628
|
+
scatter.push(cov.scatter);
|
|
13354
13629
|
prev = g;
|
|
13355
13630
|
lastKept = t;
|
|
13356
13631
|
lastGray = g;
|
|
@@ -13362,7 +13637,9 @@ async function decodeInPage(opts) {
|
|
|
13362
13637
|
if (tail && lastTime > lastKept) {
|
|
13363
13638
|
times.push(lastTime);
|
|
13364
13639
|
changes.push(prev ? difference(prev, tail) : 0);
|
|
13365
|
-
|
|
13640
|
+
const cov = prev ? coverageOf(prev, tail) : none;
|
|
13641
|
+
coverage.push(cov.share);
|
|
13642
|
+
scatter.push(cov.scatter);
|
|
13366
13643
|
lastGray = tail;
|
|
13367
13644
|
}
|
|
13368
13645
|
const toBase64 = (bytes) => {
|
|
@@ -13412,6 +13689,7 @@ async function decodeInPage(opts) {
|
|
|
13412
13689
|
times,
|
|
13413
13690
|
changes,
|
|
13414
13691
|
coverage,
|
|
13692
|
+
scatter,
|
|
13415
13693
|
firstDeviation: deviation(first),
|
|
13416
13694
|
lastDeviation: deviation(lastGray),
|
|
13417
13695
|
firstTime,
|
|
@@ -13447,12 +13725,14 @@ async function checkVideo(browser, file, outDir) {
|
|
|
13447
13725
|
void page.evaluate(decodeInPage, {
|
|
13448
13726
|
mbCode: await mediabunnyBundle(),
|
|
13449
13727
|
meterSrc: String(createLoudnessMeter),
|
|
13450
|
-
videoUrl: `/${encodeURIComponent(
|
|
13728
|
+
videoUrl: `/${encodeURIComponent(basename11(file))}`,
|
|
13451
13729
|
sampleFps,
|
|
13452
13730
|
thumbW: side(facts.width),
|
|
13453
13731
|
thumbH: side(facts.height),
|
|
13732
|
+
cellPx: Math.max(1, Math.round(THUMB_LONG_SIDE / CELLS_LONG_SIDE)),
|
|
13454
13733
|
subBlock: SUB_BLOCK_SECONDS,
|
|
13455
|
-
cellChanged: CELL_CHANGED_AT
|
|
13734
|
+
cellChanged: CELL_CHANGED_AT,
|
|
13735
|
+
cellStrong: CELL_STRONG_AT
|
|
13456
13736
|
}).catch(() => {
|
|
13457
13737
|
});
|
|
13458
13738
|
done = await waitForPageDone(
|
|
@@ -13471,7 +13751,12 @@ async function checkVideo(browser, file, outDir) {
|
|
|
13471
13751
|
const lastFile = join24(outDir, "last.png");
|
|
13472
13752
|
await writeFile10(firstFile, Buffer.from(done.firstPng, "base64"));
|
|
13473
13753
|
await writeFile10(lastFile, Buffer.from(done.lastPng, "base64"));
|
|
13474
|
-
const changes = sceneChanges2(
|
|
13754
|
+
const changes = sceneChanges2(
|
|
13755
|
+
done.times,
|
|
13756
|
+
done.changes,
|
|
13757
|
+
done.coverage,
|
|
13758
|
+
done.scatter
|
|
13759
|
+
);
|
|
13475
13760
|
const loudness = done.audio ? loudnessFromEnergies(done.audio.energies, done.audio.peak) : null;
|
|
13476
13761
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
13477
13762
|
const posters = {
|
|
@@ -13548,7 +13833,7 @@ async function cmdCheckVideo(argv) {
|
|
|
13548
13833
|
const r = createReporter(flags.json === true);
|
|
13549
13834
|
const strict = flags.strict === true;
|
|
13550
13835
|
const outDir = resolve15(
|
|
13551
|
-
strFlag(flags, "out") ?? join24(dirname8(file), `${
|
|
13836
|
+
strFlag(flags, "out") ?? join24(dirname8(file), `${basename11(file, extname4(file))}-check`)
|
|
13552
13837
|
);
|
|
13553
13838
|
const browser = await launchBrowser();
|
|
13554
13839
|
try {
|
|
@@ -13563,27 +13848,28 @@ async function cmdCheckVideo(argv) {
|
|
|
13563
13848
|
}
|
|
13564
13849
|
|
|
13565
13850
|
// src/plugin/join.ts
|
|
13566
|
-
import { existsSync as
|
|
13567
|
-
import { copyFile as copyFile3, mkdir as mkdir11, rm as rm4 } from "fs/promises";
|
|
13851
|
+
import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
13852
|
+
import { copyFile as copyFile3, mkdir as mkdir11, readFile as readFile17, rm as rm4 } from "fs/promises";
|
|
13568
13853
|
import {
|
|
13569
|
-
basename as
|
|
13854
|
+
basename as basename13,
|
|
13570
13855
|
dirname as dirname9,
|
|
13571
13856
|
isAbsolute as isAbsolute4,
|
|
13572
|
-
join as
|
|
13857
|
+
join as join27,
|
|
13573
13858
|
relative as relative5,
|
|
13574
13859
|
resolve as resolve17,
|
|
13575
13860
|
sep as sep4
|
|
13576
13861
|
} from "path";
|
|
13577
13862
|
import {
|
|
13578
|
-
mediaKey as
|
|
13579
|
-
mediaRefId,
|
|
13863
|
+
mediaKey as mediaKey4,
|
|
13864
|
+
mediaRefId as mediaRefId2,
|
|
13580
13865
|
migrateMotion as migrateMotion3,
|
|
13581
13866
|
projectFromArtifact as projectFromArtifact2,
|
|
13582
|
-
ratedSegments as
|
|
13583
|
-
|
|
13584
|
-
|
|
13867
|
+
ratedSegments as ratedSegments10,
|
|
13868
|
+
layerStart as layerStart4,
|
|
13869
|
+
resolveAnchor as resolveAnchor7,
|
|
13870
|
+
segmentOutputExtents as segmentOutputExtents4
|
|
13585
13871
|
} from "@vosjs/studio-core";
|
|
13586
|
-
import { totalDuration as
|
|
13872
|
+
import { totalDuration as totalDuration3 } from "@vosjs/timeline";
|
|
13587
13873
|
|
|
13588
13874
|
// src/plugin/score.ts
|
|
13589
13875
|
import { existsSync as existsSync23 } from "fs";
|
|
@@ -13596,7 +13882,7 @@ import {
|
|
|
13596
13882
|
writeFile as writeFile11
|
|
13597
13883
|
} from "fs/promises";
|
|
13598
13884
|
import { tmpdir as tmpdir2 } from "os";
|
|
13599
|
-
import { basename as
|
|
13885
|
+
import { basename as basename12, isAbsolute as isAbsolute3, join as join25, relative as relative4, resolve as resolve16, sep as sep3 } from "path";
|
|
13600
13886
|
import {
|
|
13601
13887
|
NORMALIZE_MAX_LUFS as NORMALIZE_MAX_LUFS2,
|
|
13602
13888
|
NORMALIZE_MIN_LUFS as NORMALIZE_MIN_LUFS2,
|
|
@@ -13911,7 +14197,7 @@ async function measureInBrowser(path) {
|
|
|
13911
14197
|
const bytes = await readFile15(path);
|
|
13912
14198
|
if (bytes.length > BROWSER_DECODE_MAX_BYTES)
|
|
13913
14199
|
throw new Error(
|
|
13914
|
-
`${
|
|
14200
|
+
`${basename12(path)} is ${Math.round(bytes.length / 1048576)} MB, past the ${BROWSER_DECODE_MAX_BYTES / 1048576} MB the browser decode takes whole`
|
|
13915
14201
|
);
|
|
13916
14202
|
const browser = await launchBrowser();
|
|
13917
14203
|
try {
|
|
@@ -14015,12 +14301,12 @@ async function resolveTrack(dir, ref, flags, dryRun) {
|
|
|
14015
14301
|
if (existsSync23(local)) {
|
|
14016
14302
|
const rel = relative4(resolve16(dir), local);
|
|
14017
14303
|
const inside = !!rel && !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute3(rel);
|
|
14018
|
-
const key = inside ? rel.split(sep3).join("/") : `music/${
|
|
14304
|
+
const key = inside ? rel.split(sep3).join("/") : `music/${basename12(local)}`;
|
|
14019
14305
|
if (!inside && !dryRun) {
|
|
14020
14306
|
await mkdir10(join25(dir, "music"), { recursive: true });
|
|
14021
14307
|
await copyFile2(local, join25(dir, key));
|
|
14022
14308
|
}
|
|
14023
|
-
return { path: local, key, name:
|
|
14309
|
+
return { path: local, key, name: basename12(local) };
|
|
14024
14310
|
}
|
|
14025
14311
|
const origin = platformOrigin({
|
|
14026
14312
|
origin: strFlag(flags, "origin"),
|
|
@@ -14035,7 +14321,7 @@ async function resolveTrack(dir, ref, flags, dryRun) {
|
|
|
14035
14321
|
const res = await fetch(track.url);
|
|
14036
14322
|
if (!res.ok) throw new Error(`GET ${track.url} answered ${res.status}`);
|
|
14037
14323
|
const tmp = await mkdtemp(join25(tmpdir2(), "vos-score-"));
|
|
14038
|
-
const path = join25(tmp,
|
|
14324
|
+
const path = join25(tmp, basename12(new URL(track.url).pathname) || "track");
|
|
14039
14325
|
await writeFile11(path, new Uint8Array(await res.arrayBuffer()));
|
|
14040
14326
|
return {
|
|
14041
14327
|
path,
|
|
@@ -14183,30 +14469,94 @@ import {
|
|
|
14183
14469
|
segmentOutputExtents as segmentOutputExtents2
|
|
14184
14470
|
} from "@vosjs/studio-core";
|
|
14185
14471
|
import { totalDuration } from "@vosjs/timeline";
|
|
14472
|
+
|
|
14473
|
+
// src/plugin/scriptStep.ts
|
|
14474
|
+
var asRaw = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
14475
|
+
function scriptStepOf(step, list, recorded = []) {
|
|
14476
|
+
if (!Array.isArray(list)) return void 0;
|
|
14477
|
+
const steps = list.map(asRaw);
|
|
14478
|
+
const indexOfId = (id) => steps.findIndex((s) => s?.id === id);
|
|
14479
|
+
if (step.id !== void 0) {
|
|
14480
|
+
const at2 = indexOfId(String(step.id));
|
|
14481
|
+
if (at2 >= 0) return steps[at2];
|
|
14482
|
+
}
|
|
14483
|
+
const recordedIds = new Set(
|
|
14484
|
+
recorded.filter((r) => r !== step && r.id !== void 0).map((r) => String(r.id))
|
|
14485
|
+
);
|
|
14486
|
+
const fits = (s) => !!s && s.do === step.do && !(typeof s.id === "string" && recordedIds.has(s.id)) && !(typeof s.selector === "string" && typeof step.selector === "string" && s.selector !== step.selector);
|
|
14487
|
+
const named = recorded.filter(
|
|
14488
|
+
(r) => r !== step && r.id !== void 0 && indexOfId(String(r.id)) >= 0
|
|
14489
|
+
);
|
|
14490
|
+
const before = named.filter((r) => r.step < step.step).sort((a, b) => b.step - a.step)[0];
|
|
14491
|
+
const after = named.filter((r) => r.step > step.step).sort((a, b) => a.step - b.step)[0];
|
|
14492
|
+
const found = /* @__PURE__ */ new Set();
|
|
14493
|
+
for (const anchor of [before, after]) {
|
|
14494
|
+
if (!anchor) continue;
|
|
14495
|
+
const hit = steps[indexOfId(String(anchor.id)) + step.step - anchor.step];
|
|
14496
|
+
if (fits(hit)) found.add(hit);
|
|
14497
|
+
}
|
|
14498
|
+
if (found.size === 1) return [...found][0];
|
|
14499
|
+
if (found.size > 1) return void 0;
|
|
14500
|
+
const own = steps[step.step];
|
|
14501
|
+
return fits(own) ? own : void 0;
|
|
14502
|
+
}
|
|
14503
|
+
function scriptDrift(recorded, list) {
|
|
14504
|
+
if (!Array.isArray(list) || !recorded.length) return null;
|
|
14505
|
+
const changed = [];
|
|
14506
|
+
for (const step of recorded) {
|
|
14507
|
+
const raw = scriptStepOf(step, list, recorded);
|
|
14508
|
+
const id = step.id !== void 0 ? { id: String(step.id) } : {};
|
|
14509
|
+
if (!raw) {
|
|
14510
|
+
changed.push({ step: step.step, ...id, was: step.do, now: null });
|
|
14511
|
+
continue;
|
|
14512
|
+
}
|
|
14513
|
+
const selectorMoved = typeof raw.selector === "string" && typeof step.selector === "string" && raw.selector !== step.selector;
|
|
14514
|
+
if (raw.do !== step.do || selectorMoved)
|
|
14515
|
+
changed.push({
|
|
14516
|
+
step: step.step,
|
|
14517
|
+
...id,
|
|
14518
|
+
was: step.selector ? `${step.do} ${step.selector}` : step.do,
|
|
14519
|
+
now: typeof raw.selector === "string" ? `${String(raw.do)} ${raw.selector}` : String(raw.do)
|
|
14520
|
+
});
|
|
14521
|
+
}
|
|
14522
|
+
if (list.length === recorded.length && !changed.length) return null;
|
|
14523
|
+
return { recorded: recorded.length, inScript: list.length, changed };
|
|
14524
|
+
}
|
|
14525
|
+
function scriptDriftLine(d, file = "actions.json") {
|
|
14526
|
+
const counts = d.inScript === d.recorded ? `${file} no longer matches what was recorded` : `${file} has ${d.inScript} step${d.inScript === 1 ? "" : "s"} and the take recorded ${d.recorded} (meta.steps)`;
|
|
14527
|
+
const shown = d.changed.slice(0, 3).map(
|
|
14528
|
+
(c) => `#${c.step}${c.id ? ` ${c.id}` : ""} was ${c.was}, ${c.now === null ? "not found in the file" : `now ${c.now}`}`
|
|
14529
|
+
);
|
|
14530
|
+
const changed = d.changed.length ? `; ${d.changed.length} recorded step${d.changed.length === 1 ? "" : "s"} changed (${shown.join("; ")}${d.changed.length > 3 ? "; \u2026" : ""})` : "";
|
|
14531
|
+
return `${counts}${changed}: captions and zoom marks are matched to the recorded steps by id, then by place beside a named step, then by index (vos info shows where each lands; record again to bring them back in step)`;
|
|
14532
|
+
}
|
|
14533
|
+
|
|
14534
|
+
// src/plugin/stepCaptions.ts
|
|
14186
14535
|
var STEP_CAPTIONS_FROM = "steps";
|
|
14187
|
-
var
|
|
14536
|
+
var STEP_CAPTION_AT = "start";
|
|
14188
14537
|
var CAPTION_MAX = 3;
|
|
14189
14538
|
var CAPTION_MIN = 1.2;
|
|
14190
14539
|
var CAPTION_GAP = 0.1;
|
|
14191
|
-
function captionOf(step, actions, script) {
|
|
14540
|
+
function captionOf(step, actions, script, recorded = []) {
|
|
14192
14541
|
const own = step.caption;
|
|
14193
14542
|
if (typeof own === "string" && own.trim()) return own.trim();
|
|
14194
14543
|
for (const list of [
|
|
14195
14544
|
actions?.steps,
|
|
14196
14545
|
Array.isArray(script?.steps) ? script.steps : void 0
|
|
14197
14546
|
]) {
|
|
14198
|
-
|
|
14199
|
-
const byId = step.id !== void 0 ? list.find((s) => s?.id === step.id) : void 0;
|
|
14200
|
-
const raw = byId ?? list[step.step];
|
|
14547
|
+
const raw = scriptStepOf(step, list, recorded);
|
|
14201
14548
|
if (typeof raw?.caption === "string" && raw.caption.trim())
|
|
14202
14549
|
return raw.caption.trim();
|
|
14203
14550
|
}
|
|
14204
14551
|
return null;
|
|
14205
14552
|
}
|
|
14206
14553
|
var safeId = (v) => String(v).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "step";
|
|
14207
|
-
function landStepCaptions(doc, actions) {
|
|
14554
|
+
function landStepCaptions(doc, actions, opts = {}) {
|
|
14208
14555
|
const report = { landed: [], skipped: [], uncaptioned: [] };
|
|
14209
|
-
const
|
|
14556
|
+
const mine = (o) => !opts.only || opts.only.has(mediaKey(o.anchor?.media));
|
|
14557
|
+
const kept = (doc.overlays ?? []).filter(
|
|
14558
|
+
(o) => o.from !== STEP_CAPTIONS_FROM || !mine(o)
|
|
14559
|
+
);
|
|
14210
14560
|
const rated = ratedSegments8(doc);
|
|
14211
14561
|
const footageEnd = totalDuration(rated);
|
|
14212
14562
|
const docSegments = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
|
|
@@ -14229,10 +14579,11 @@ function landStepCaptions(doc, actions) {
|
|
|
14229
14579
|
const fresh = [];
|
|
14230
14580
|
const taken = new Set(kept.map((o) => o.id));
|
|
14231
14581
|
for (const { id: media, source } of docMediaList2(doc)) {
|
|
14582
|
+
if (opts.only && !opts.only.has(media)) continue;
|
|
14232
14583
|
const steps = source.meta.steps ?? [];
|
|
14233
14584
|
let captioned = 0;
|
|
14234
14585
|
for (const step of steps) {
|
|
14235
|
-
const caption = captionOf(step, actions.get(media), source.script);
|
|
14586
|
+
const caption = captionOf(step, actions.get(media), source.script, steps);
|
|
14236
14587
|
if (!caption) continue;
|
|
14237
14588
|
captioned++;
|
|
14238
14589
|
const ref = step.id ?? step.step;
|
|
@@ -14248,8 +14599,7 @@ function landStepCaptions(doc, actions) {
|
|
|
14248
14599
|
const anchor = {
|
|
14249
14600
|
step: ref,
|
|
14250
14601
|
...media ? { media } : {},
|
|
14251
|
-
at:
|
|
14252
|
-
offset: STEP_CAPTION_OFFSET
|
|
14602
|
+
at: STEP_CAPTION_AT
|
|
14253
14603
|
};
|
|
14254
14604
|
const placed = resolveAnchor5(doc, anchor, rated);
|
|
14255
14605
|
if (placed.start === null) {
|
|
@@ -14267,6 +14617,7 @@ function landStepCaptions(doc, actions) {
|
|
|
14267
14617
|
taken.add(id);
|
|
14268
14618
|
const clip = {
|
|
14269
14619
|
...houseCaption(id, caption, placed.start, CAPTION_MAX),
|
|
14620
|
+
start: placed.start,
|
|
14270
14621
|
anchor,
|
|
14271
14622
|
from: STEP_CAPTIONS_FROM
|
|
14272
14623
|
};
|
|
@@ -14303,55 +14654,1072 @@ function bareCaptionsFlag(argv) {
|
|
|
14303
14654
|
);
|
|
14304
14655
|
}
|
|
14305
14656
|
|
|
14306
|
-
// src/plugin/
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
14312
|
-
|
|
14313
|
-
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
var FILE_SOUND_FADE = 0.15;
|
|
14317
|
-
var prefixed = (id, key) => `${id}/${takeRelativeFile(key)}`;
|
|
14318
|
-
function fileDoc(key, probed, t0 = 0) {
|
|
14319
|
-
const meta = {
|
|
14320
|
-
dpr: 1,
|
|
14321
|
-
zoom: 1,
|
|
14322
|
-
t0,
|
|
14323
|
-
durationMs: probed.durationMs,
|
|
14324
|
-
width: probed.width,
|
|
14325
|
-
height: probed.height,
|
|
14326
|
-
captureWidth: probed.width,
|
|
14327
|
-
captureHeight: probed.height,
|
|
14328
|
-
fps: probed.fps > 0 ? probed.fps : 30,
|
|
14329
|
-
hasAudio: probed.hasAudio,
|
|
14330
|
-
producer: "ingest"
|
|
14657
|
+
// src/plugin/beats.ts
|
|
14658
|
+
import { docMediaList as docMediaList3, mediaKey as mediaKey2, rejectSpan } from "@vosjs/studio-core";
|
|
14659
|
+
var PER_BEAT_NOTE = "one zoom per beat";
|
|
14660
|
+
var ZOOM_FALSE_NOTE = "zoom: false in the script";
|
|
14661
|
+
var ZOOM_MODES = ["per-beat"];
|
|
14662
|
+
function zoomModeOf(raw) {
|
|
14663
|
+
if (raw === void 0) return null;
|
|
14664
|
+
if (ZOOM_MODES.includes(raw)) return raw;
|
|
14665
|
+
return {
|
|
14666
|
+
error: `--zooms expects ${ZOOM_MODES.join(" or ")}, got "${raw}"`
|
|
14331
14667
|
};
|
|
14332
|
-
const doc = projectFromArtifact2({ videoKey: key, cursor: [], meta }, key, {
|
|
14333
|
-
footage: true
|
|
14334
|
-
}).doc;
|
|
14335
|
-
doc.segments = [{ in: 0, out: round32(probed.durationMs / 1e3) }];
|
|
14336
|
-
return doc;
|
|
14337
14668
|
}
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14669
|
+
var LAST_GESTURE_VERBS = [
|
|
14670
|
+
"click",
|
|
14671
|
+
"drag",
|
|
14672
|
+
"type",
|
|
14673
|
+
"paste"
|
|
14674
|
+
];
|
|
14675
|
+
function zoomMarkOf(step, actions, script, recorded = []) {
|
|
14676
|
+
const inList = (list) => {
|
|
14677
|
+
const raw = scriptStepOf(step, list, recorded);
|
|
14678
|
+
return typeof raw?.zoom === "boolean" ? raw.zoom : void 0;
|
|
14679
|
+
};
|
|
14680
|
+
return inList(actions?.steps) ?? (typeof step.zoom === "boolean" ? step.zoom : void 0) ?? inList(script?.steps);
|
|
14681
|
+
}
|
|
14682
|
+
var beatStep = (s) => ({
|
|
14683
|
+
step: s.step,
|
|
14684
|
+
...s.id ? { id: s.id } : {},
|
|
14685
|
+
do: s.do,
|
|
14686
|
+
in: s.tStart,
|
|
14687
|
+
out: Math.max(s.tStart, s.tEnd)
|
|
14688
|
+
});
|
|
14689
|
+
function markedSteps(source, actions) {
|
|
14690
|
+
return [...source.meta.steps ?? []].filter((s) => typeof s.tStart === "number" && !s.skipped).sort((a, b) => a.tStart - b.tStart || a.step - b.step).map((s) => ({
|
|
14691
|
+
s,
|
|
14692
|
+
mark: zoomMarkOf(s, actions, source.script, source.meta.steps ?? [])
|
|
14693
|
+
}));
|
|
14694
|
+
}
|
|
14695
|
+
function mediaBeats(media, source, actions) {
|
|
14696
|
+
const end = (source.meta.durationMs || 0) / 1e3;
|
|
14697
|
+
const starts = [];
|
|
14698
|
+
for (const step of source.meta.steps ?? []) {
|
|
14699
|
+
if (typeof step.tStart !== "number") continue;
|
|
14700
|
+
const caption = captionOf(
|
|
14701
|
+
step,
|
|
14702
|
+
actions,
|
|
14703
|
+
source.script,
|
|
14704
|
+
source.meta.steps ?? []
|
|
14705
|
+
);
|
|
14706
|
+
if (caption) starts.push({ step, caption });
|
|
14707
|
+
}
|
|
14708
|
+
if (!starts.length) return [];
|
|
14709
|
+
starts.sort((a, b) => a.step.tStart - b.step.tStart);
|
|
14710
|
+
const steps = markedSteps(source, actions);
|
|
14711
|
+
const beat = (step, caption, from, to, last) => {
|
|
14712
|
+
const mine = steps.filter(
|
|
14713
|
+
(x) => x.s.tStart >= from && (x.s.tStart < to || last)
|
|
14714
|
+
);
|
|
14715
|
+
const gesture = mine.filter((x) => LAST_GESTURE_VERBS.includes(x.s.do) && x.mark !== false).at(-1);
|
|
14716
|
+
return {
|
|
14717
|
+
media,
|
|
14718
|
+
step,
|
|
14719
|
+
caption,
|
|
14720
|
+
in: from,
|
|
14721
|
+
out: to,
|
|
14722
|
+
marked: mine.filter((x) => x.mark === true).map((x) => beatStep(x.s)),
|
|
14723
|
+
...gesture ? { lastGesture: beatStep(gesture.s) } : {}
|
|
14724
|
+
};
|
|
14725
|
+
};
|
|
14726
|
+
const beats = [];
|
|
14727
|
+
if (starts[0].step.tStart > 0)
|
|
14728
|
+
beats.push(beat(null, null, 0, starts[0].step.tStart, false));
|
|
14729
|
+
starts.forEach(({ step, caption }, i) => {
|
|
14730
|
+
const next = starts[i + 1];
|
|
14731
|
+
beats.push(
|
|
14732
|
+
beat(
|
|
14733
|
+
step.id ?? step.step,
|
|
14734
|
+
caption,
|
|
14735
|
+
step.tStart,
|
|
14736
|
+
next ? next.step.tStart : Math.max(end, step.tStart),
|
|
14737
|
+
!next
|
|
14738
|
+
)
|
|
14739
|
+
);
|
|
14345
14740
|
});
|
|
14741
|
+
return beats;
|
|
14742
|
+
}
|
|
14743
|
+
function playedSeconds(doc, z) {
|
|
14744
|
+
const key = mediaKey2(z.media);
|
|
14745
|
+
const segments = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
|
|
14746
|
+
let played = 0;
|
|
14747
|
+
for (const s of segments) {
|
|
14748
|
+
if (mediaKey2(s.media) !== key) continue;
|
|
14749
|
+
played += Math.max(0, Math.min(s.out, z.out) - Math.max(s.in, z.in));
|
|
14750
|
+
}
|
|
14751
|
+
return played;
|
|
14752
|
+
}
|
|
14753
|
+
var ms = (v) => Math.round(v * 1e3);
|
|
14754
|
+
function stepOverlap(z, step) {
|
|
14755
|
+
if (!step) return 0;
|
|
14756
|
+
const shared = Math.min(z.out, step.out) - Math.max(z.in, step.in);
|
|
14757
|
+
if (shared > 0) return shared;
|
|
14758
|
+
return step.out - step.in < 1e-3 && z.in <= step.in && step.in <= z.out ? 1e-6 : 0;
|
|
14759
|
+
}
|
|
14760
|
+
function onStep(z, step) {
|
|
14761
|
+
const shorter = Math.min(z.out - z.in, step.out - step.in);
|
|
14762
|
+
return shorter > 0 ? stepOverlap(z, step) > shorter / 2 : stepOverlap(z, step) > 0;
|
|
14763
|
+
}
|
|
14764
|
+
function stronger(doc) {
|
|
14765
|
+
return (a, b) => ms(playedSeconds(doc, b)) - ms(playedSeconds(doc, a)) || ms(b.out - b.in) - ms(a.out - a.in) || b.level - a.level || a.in - b.in;
|
|
14766
|
+
}
|
|
14767
|
+
var overlaps2 = (a, b) => a.in < b.out && b.in < a.out;
|
|
14768
|
+
function takeMarksZoom(doc, actions) {
|
|
14769
|
+
return docMediaList3(doc).some(
|
|
14770
|
+
({ id, source }) => markedSteps(source, actions.get(id)).some((x) => x.mark === true)
|
|
14771
|
+
);
|
|
14346
14772
|
}
|
|
14347
|
-
function
|
|
14348
|
-
|
|
14349
|
-
const
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14773
|
+
function rejectAll(doc, drop, note) {
|
|
14774
|
+
if (!drop.size) return 0;
|
|
14775
|
+
const rejected = [...doc.rejected ?? []];
|
|
14776
|
+
for (const z of doc.zoom) {
|
|
14777
|
+
if (!drop.has(z)) continue;
|
|
14778
|
+
const entry = rejectSpan(rejected, "zoom", z, note);
|
|
14779
|
+
if (z.media) entry.media = z.media;
|
|
14780
|
+
rejected.push(entry);
|
|
14781
|
+
}
|
|
14782
|
+
doc.rejected = rejected;
|
|
14783
|
+
doc.zoom = doc.zoom.filter((z) => !drop.has(z));
|
|
14784
|
+
return drop.size;
|
|
14785
|
+
}
|
|
14786
|
+
function oneZoomPerBeat(doc, actions, opts = {}) {
|
|
14787
|
+
const fallback = !takeMarksZoom(doc, actions);
|
|
14788
|
+
const report = {
|
|
14789
|
+
fallback,
|
|
14790
|
+
kept: [],
|
|
14791
|
+
none: [],
|
|
14792
|
+
manual: [],
|
|
14793
|
+
rejected: 0,
|
|
14794
|
+
uncaptioned: [],
|
|
14795
|
+
beats: 0
|
|
14796
|
+
};
|
|
14797
|
+
const drop = /* @__PURE__ */ new Set();
|
|
14798
|
+
const refused = /* @__PURE__ */ new Set();
|
|
14799
|
+
const order = stronger(doc);
|
|
14800
|
+
for (const { id: media, source } of docMediaList3(doc)) {
|
|
14801
|
+
if (opts.only && !opts.only.has(media)) continue;
|
|
14802
|
+
const beats = mediaBeats(media, source, actions.get(media));
|
|
14803
|
+
const spans = doc.zoom.filter((z) => mediaKey2(z.media) === media);
|
|
14804
|
+
if (!beats.length) {
|
|
14805
|
+
if (spans.length) report.uncaptioned.push(media);
|
|
14806
|
+
continue;
|
|
14807
|
+
}
|
|
14808
|
+
const never = markedSteps(source, actions.get(media)).filter((x) => x.mark === false).map((x) => beatStep(x.s));
|
|
14809
|
+
for (const z of spans) if (never.some((s) => onStep(z, s))) refused.add(z);
|
|
14810
|
+
for (const beat of beats) {
|
|
14811
|
+
const inBeat = spans.filter((z) => {
|
|
14812
|
+
const mid = (z.in + z.out) / 2;
|
|
14813
|
+
return mid >= beat.in && mid < beat.out;
|
|
14814
|
+
});
|
|
14815
|
+
if (inBeat.length) report.beats++;
|
|
14816
|
+
const manual = inBeat.filter((z) => z.source === "manual");
|
|
14817
|
+
const auto = inBeat.filter((z) => z.source !== "manual");
|
|
14818
|
+
if (manual.length) {
|
|
14819
|
+
for (const z of auto) drop.add(z);
|
|
14820
|
+
if (auto.length)
|
|
14821
|
+
report.manual.push({
|
|
14822
|
+
media,
|
|
14823
|
+
step: beat.step,
|
|
14824
|
+
ids: manual.map((z) => z.id)
|
|
14825
|
+
});
|
|
14826
|
+
continue;
|
|
14827
|
+
}
|
|
14828
|
+
const words2 = {
|
|
14829
|
+
media,
|
|
14830
|
+
step: beat.step,
|
|
14831
|
+
caption: beat.caption
|
|
14832
|
+
};
|
|
14833
|
+
if (!inBeat.length) {
|
|
14834
|
+
if (beat.step !== null)
|
|
14835
|
+
report.none.push({
|
|
14836
|
+
...words2,
|
|
14837
|
+
why: "empty",
|
|
14838
|
+
ids: [],
|
|
14839
|
+
...beat.marked.length ? {
|
|
14840
|
+
marked: beat.marked.map((m) => ({ step: m.step, do: m.do }))
|
|
14841
|
+
} : {}
|
|
14842
|
+
});
|
|
14843
|
+
continue;
|
|
14844
|
+
}
|
|
14845
|
+
const targets = fallback ? beat.step === null || !beat.lastGesture ? [] : [beat.lastGesture] : beat.marked;
|
|
14846
|
+
if (!targets.length) {
|
|
14847
|
+
for (const z of auto) drop.add(z);
|
|
14848
|
+
if (auto.length)
|
|
14849
|
+
report.none.push({
|
|
14850
|
+
...words2,
|
|
14851
|
+
why: !fallback ? "unmarked" : beat.step === null ? "opening" : "no-gesture",
|
|
14852
|
+
ids: auto.map((z) => z.id)
|
|
14853
|
+
});
|
|
14854
|
+
continue;
|
|
14855
|
+
}
|
|
14856
|
+
const candidates = auto.filter((z) => !never.some((s) => onStep(z, s)));
|
|
14857
|
+
const chosen = [];
|
|
14858
|
+
for (const target of targets) {
|
|
14859
|
+
const action = { step: target.step, do: target.do };
|
|
14860
|
+
const [best] = candidates.filter(
|
|
14861
|
+
(z) => stepOverlap(z, target) > 0 && !chosen.some((c) => c === z || overlaps2(c, z))
|
|
14862
|
+
).sort(
|
|
14863
|
+
(a, b) => ms(stepOverlap(b, target)) - ms(stepOverlap(a, target)) || order(a, b)
|
|
14864
|
+
);
|
|
14865
|
+
if (!best) {
|
|
14866
|
+
report.none.push({ ...words2, why: "no-proposal", action, ids: [] });
|
|
14867
|
+
continue;
|
|
14868
|
+
}
|
|
14869
|
+
chosen.push(best);
|
|
14870
|
+
report.kept.push({ ...words2, action, id: best.id });
|
|
14871
|
+
}
|
|
14872
|
+
for (const z of chosen) z.source = "manual";
|
|
14873
|
+
for (const z of auto) if (!chosen.includes(z)) drop.add(z);
|
|
14874
|
+
}
|
|
14875
|
+
}
|
|
14876
|
+
const onNever = new Set([...drop].filter((z) => refused.has(z)));
|
|
14877
|
+
for (const z of onNever) drop.delete(z);
|
|
14878
|
+
report.rejected = rejectAll(doc, onNever, ZOOM_FALSE_NOTE) + rejectAll(doc, drop, PER_BEAT_NOTE);
|
|
14879
|
+
return report;
|
|
14880
|
+
}
|
|
14881
|
+
function applyZoomMarks(doc, actions) {
|
|
14882
|
+
const report = { rejected: [], unproposed: [] };
|
|
14883
|
+
const drop = /* @__PURE__ */ new Set();
|
|
14884
|
+
for (const { id: media, source } of docMediaList3(doc)) {
|
|
14885
|
+
const spans = doc.zoom.filter((z) => mediaKey2(z.media) === media);
|
|
14886
|
+
for (const { s, mark } of markedSteps(source, actions.get(media))) {
|
|
14887
|
+
const step = beatStep(s);
|
|
14888
|
+
if (mark === false) {
|
|
14889
|
+
const on = spans.filter(
|
|
14890
|
+
(z) => z.source !== "manual" && !drop.has(z) && onStep(z, step)
|
|
14891
|
+
);
|
|
14892
|
+
for (const z of on) drop.add(z);
|
|
14893
|
+
if (on.length)
|
|
14894
|
+
report.rejected.push({
|
|
14895
|
+
media,
|
|
14896
|
+
step: s.step,
|
|
14897
|
+
ids: on.map((z) => z.id)
|
|
14898
|
+
});
|
|
14899
|
+
} else if (mark === true && !spans.some((z) => !drop.has(z) && stepOverlap(z, step) > 0)) {
|
|
14900
|
+
report.unproposed.push({
|
|
14901
|
+
media,
|
|
14902
|
+
step: s.step,
|
|
14903
|
+
...s.id ? { id: s.id } : {},
|
|
14904
|
+
do: s.do
|
|
14905
|
+
});
|
|
14906
|
+
}
|
|
14907
|
+
}
|
|
14908
|
+
}
|
|
14909
|
+
rejectAll(doc, drop, ZOOM_FALSE_NOTE);
|
|
14910
|
+
return report;
|
|
14911
|
+
}
|
|
14912
|
+
function zoomMarkLine(r, name = (m) => m ? `media ${m}` : "the primary") {
|
|
14913
|
+
const at2 = (x) => `#${x.step}${x.media ? ` of ${name(x.media)}` : ""}`;
|
|
14914
|
+
const parts = [];
|
|
14915
|
+
if (r.rejected.length)
|
|
14916
|
+
parts.push(
|
|
14917
|
+
`zoom: false rejected ${r.rejected.map((x) => `${x.ids.join(", ")} on step ${at2(x)}`).join("; ")}`
|
|
14918
|
+
);
|
|
14919
|
+
if (r.unproposed.length)
|
|
14920
|
+
parts.push(
|
|
14921
|
+
`zoom: true on ${r.unproposed.map((x) => `step ${at2(x)} ${x.id ? `${x.id} ` : ""}${x.do}`).join(", ")}, but the planner proposed no zoom there (none is invented)`
|
|
14922
|
+
);
|
|
14923
|
+
return parts.length ? parts.join("; ") : null;
|
|
14924
|
+
}
|
|
14925
|
+
function perBeatLine(r, name = (m) => m ? `media ${m}` : "the primary") {
|
|
14926
|
+
const parts = [
|
|
14927
|
+
`one zoom per beat: ${r.beats} beat${r.beats === 1 ? "" : "s"} held zooms, ${r.kept.length} kept (now manual), ${r.rejected} rejected into doc.rejected (no re-plan brings them back)`
|
|
14928
|
+
];
|
|
14929
|
+
if (r.fallback)
|
|
14930
|
+
parts.push(
|
|
14931
|
+
`no step is marked "zoom": falling back to each beat's last gesture`
|
|
14932
|
+
);
|
|
14933
|
+
const beatName = (b) => b.step === null ? `the opening${b.media ? ` of ${name(b.media)}` : ""}` : `beat ${b.caption ? JSON.stringify(b.caption) : String(b.step)}${b.media ? ` (${name(b.media)})` : ""}`;
|
|
14934
|
+
const target = (a) => `${r.fallback ? "last gesture" : "marked"} #${a.step} ${a.do}`;
|
|
14935
|
+
for (const b of r.kept)
|
|
14936
|
+
parts.push(`${beatName(b)}: ${target(b.action)}, kept ${b.id}`);
|
|
14937
|
+
for (const b of r.none)
|
|
14938
|
+
parts.push(
|
|
14939
|
+
`${beatName(b)}: ${b.why === "empty" ? `no proposal in this beat${b.marked?.length ? ` (marked ${b.marked.map((m) => `#${m.step} ${m.do}`).join(", ")})` : ""}` : b.why === "no-proposal" && b.action ? `${target(b.action)}, but no proposal on it` : b.why === "unmarked" ? 'no step marked "zoom", no zoom' : b.why === "no-gesture" ? "no gesture to stand in, no zoom" : "no zoom"}`
|
|
14940
|
+
);
|
|
14941
|
+
if (r.manual.length)
|
|
14942
|
+
parts.push(
|
|
14943
|
+
`${r.manual.length} beat${r.manual.length === 1 ? "" : "s"} kept ${r.manual.length === 1 ? "its" : "their"} manual zoom${r.manual.some((m) => m.ids.length > 1) ? "s" : ""}`
|
|
14944
|
+
);
|
|
14945
|
+
if (r.uncaptioned.length)
|
|
14946
|
+
parts.push(
|
|
14947
|
+
`no captioned step in ${r.uncaptioned.map(name).join(", ")}, so ${r.uncaptioned.length === 1 ? "its" : "their"} zooms stay as planned`
|
|
14948
|
+
);
|
|
14949
|
+
return parts.join("; ");
|
|
14950
|
+
}
|
|
14951
|
+
|
|
14952
|
+
// src/plugin/rejoin.ts
|
|
14953
|
+
import { createHash as createHash4 } from "crypto";
|
|
14954
|
+
import { createReadStream, existsSync as existsSync24 } from "fs";
|
|
14955
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
14956
|
+
import { join as join26 } from "path";
|
|
14957
|
+
import {
|
|
14958
|
+
isRejected as isRejected2,
|
|
14959
|
+
layerStart as layerStart3,
|
|
14960
|
+
mediaKey as mediaKey3,
|
|
14961
|
+
mediaRefId,
|
|
14962
|
+
ratedSegments as ratedSegments9,
|
|
14963
|
+
resolveAnchor as resolveAnchor6,
|
|
14964
|
+
segmentOutputExtents as segmentOutputExtents3
|
|
14965
|
+
} from "@vosjs/studio-core";
|
|
14966
|
+
import { totalDuration as totalDuration2 } from "@vosjs/timeline";
|
|
14967
|
+
import { splitSegmentsAtSkips as splitSegmentsAtSkips2 } from "@vosjs/render-core/record";
|
|
14968
|
+
var JOIN_RECORD_NAME = "join.json";
|
|
14969
|
+
var round32 = (v) => Math.round(v * 1e3) / 1e3;
|
|
14970
|
+
function fileHash(file) {
|
|
14971
|
+
return new Promise((done, fail) => {
|
|
14972
|
+
const h = createHash4("sha256");
|
|
14973
|
+
createReadStream(file).on("data", (c) => h.update(c)).on("error", fail).on("end", () => done(h.digest("hex")));
|
|
14974
|
+
});
|
|
14975
|
+
}
|
|
14976
|
+
async function partFootage(part) {
|
|
14977
|
+
if (part.file) return fileHash(part.file.path);
|
|
14978
|
+
const key = part.doc.source.videoKey;
|
|
14979
|
+
if (!isTakeRelativeKey(key))
|
|
14980
|
+
return `key:${createHash4("sha256").update(key).digest("hex")}`;
|
|
14981
|
+
const file = join26(part.dir, takeRelativeFile(key));
|
|
14982
|
+
return existsSync24(file) ? fileHash(file) : `key:${createHash4("sha256").update(key).digest("hex")}`;
|
|
14983
|
+
}
|
|
14984
|
+
async function readJoinRecord(dir) {
|
|
14985
|
+
const file = join26(dir, JOIN_RECORD_NAME);
|
|
14986
|
+
if (!existsSync24(file)) return null;
|
|
14987
|
+
try {
|
|
14988
|
+
const v = JSON.parse(await readFile16(file, "utf8"));
|
|
14989
|
+
return v && v.version === 1 && Array.isArray(v.inputs) ? v : null;
|
|
14990
|
+
} catch {
|
|
14991
|
+
return null;
|
|
14992
|
+
}
|
|
14993
|
+
}
|
|
14994
|
+
async function recordFromFootage(dir, doc) {
|
|
14995
|
+
const hash = async (key) => {
|
|
14996
|
+
if (!isTakeRelativeKey(key))
|
|
14997
|
+
return `key:${createHash4("sha256").update(key).digest("hex")}`;
|
|
14998
|
+
const file = join26(dir, takeRelativeFile(key));
|
|
14999
|
+
return existsSync24(file) ? fileHash(file) : `missing:${key}`;
|
|
15000
|
+
};
|
|
15001
|
+
const inputs = [
|
|
15002
|
+
{
|
|
15003
|
+
path: "",
|
|
15004
|
+
kind: "take",
|
|
15005
|
+
footage: await hash(doc.source.videoKey),
|
|
15006
|
+
media: ""
|
|
15007
|
+
}
|
|
15008
|
+
];
|
|
15009
|
+
for (const m of doc.media ?? [])
|
|
15010
|
+
inputs.push({
|
|
15011
|
+
path: "",
|
|
15012
|
+
kind: "take",
|
|
15013
|
+
footage: await hash(m.videoKey),
|
|
15014
|
+
media: m.id
|
|
15015
|
+
});
|
|
15016
|
+
return { version: 1, inputs };
|
|
15017
|
+
}
|
|
15018
|
+
function joinRecordOf(inputs, ids) {
|
|
15019
|
+
return {
|
|
15020
|
+
version: 1,
|
|
15021
|
+
inputs: inputs.map((x, i) => {
|
|
15022
|
+
const sub = ids[i]?.sub ?? {};
|
|
15023
|
+
return {
|
|
15024
|
+
path: x.path,
|
|
15025
|
+
kind: x.part.kind,
|
|
15026
|
+
footage: x.footage,
|
|
15027
|
+
media: ids[i]?.id ?? "",
|
|
15028
|
+
...Object.keys(sub).length ? { sub } : {}
|
|
15029
|
+
};
|
|
15030
|
+
})
|
|
15031
|
+
};
|
|
15032
|
+
}
|
|
15033
|
+
function matchInputs(prev, next) {
|
|
15034
|
+
const match = next.map(() => null);
|
|
15035
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
15036
|
+
const pass = (same) => {
|
|
15037
|
+
next.forEach((n, i) => {
|
|
15038
|
+
if (match[i] !== null) return;
|
|
15039
|
+
const j = prev.findIndex(
|
|
15040
|
+
(p, k) => !claimed.has(k) && p.footage === n.footage && same(p, n)
|
|
15041
|
+
);
|
|
15042
|
+
if (j >= 0) {
|
|
15043
|
+
match[i] = j;
|
|
15044
|
+
claimed.add(j);
|
|
15045
|
+
}
|
|
15046
|
+
});
|
|
15047
|
+
};
|
|
15048
|
+
pass((p, n) => p.path !== "" && p.path === n.path);
|
|
15049
|
+
pass(() => true);
|
|
15050
|
+
const changed = [];
|
|
15051
|
+
const removed = [];
|
|
15052
|
+
prev.forEach((p, j) => {
|
|
15053
|
+
if (claimed.has(j)) return;
|
|
15054
|
+
const back = p.path !== "" && next.some((n, i) => match[i] === null && n.path === p.path);
|
|
15055
|
+
(back ? changed : removed).push(j);
|
|
15056
|
+
});
|
|
15057
|
+
return { match, changed, removed };
|
|
15058
|
+
}
|
|
15059
|
+
var keysOf = (x) => [
|
|
15060
|
+
x.media,
|
|
15061
|
+
...Object.values(x.sub ?? {})
|
|
15062
|
+
];
|
|
15063
|
+
var scriptUrl = (m) => {
|
|
15064
|
+
const s = m?.script;
|
|
15065
|
+
return typeof s?.url === "string" ? s.url : void 0;
|
|
15066
|
+
};
|
|
15067
|
+
var SAME_SCRIPT = 0.5;
|
|
15068
|
+
function rerecordedInputs(prev, record, inputs, m) {
|
|
15069
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
15070
|
+
const mediaOf = (k) => (prev.media ?? []).find((x) => x.id === k);
|
|
15071
|
+
const fit = (j) => {
|
|
15072
|
+
const x = record.inputs[j];
|
|
15073
|
+
if (x.kind !== "take" || x.media === "" || Object.keys(x.sub ?? {}).length)
|
|
15074
|
+
return null;
|
|
15075
|
+
const media = mediaOf(x.media);
|
|
15076
|
+
const steps = media?.meta.steps ?? [];
|
|
15077
|
+
return media && steps.length ? { media, steps } : null;
|
|
15078
|
+
};
|
|
15079
|
+
const open = (i) => i > 0 && m.match[i] === null && !pairs.has(i) && inputs[i].part.kind === "take" && !(inputs[i].part.doc.media ?? []).length && !!inputs[i].part.doc.source.meta.steps?.length;
|
|
15080
|
+
for (const j of m.changed) {
|
|
15081
|
+
if (!fit(j)) continue;
|
|
15082
|
+
const i = inputs.findIndex(
|
|
15083
|
+
(n, at2) => open(at2) && n.path === record.inputs[j].path
|
|
15084
|
+
);
|
|
15085
|
+
if (i >= 0) pairs.set(i, j);
|
|
15086
|
+
}
|
|
15087
|
+
for (const j of m.removed) {
|
|
15088
|
+
const old = fit(j);
|
|
15089
|
+
if (!old) continue;
|
|
15090
|
+
const ids = [
|
|
15091
|
+
...new Set(old.steps.map((s) => s.id).filter((v) => !!v))
|
|
15092
|
+
];
|
|
15093
|
+
if (!ids.length) continue;
|
|
15094
|
+
const url = scriptUrl(old.media);
|
|
15095
|
+
let best = -1;
|
|
15096
|
+
let bestScore = SAME_SCRIPT;
|
|
15097
|
+
let tie = false;
|
|
15098
|
+
inputs.forEach((n, i) => {
|
|
15099
|
+
if (!open(i)) return;
|
|
15100
|
+
const there = scriptUrl(n.part.doc.source) ?? n.part.actions?.url;
|
|
15101
|
+
if (url && there && url !== there) return;
|
|
15102
|
+
const now = new Set((n.part.doc.source.meta.steps ?? []).map((s) => s.id));
|
|
15103
|
+
const score = ids.filter((id) => now.has(id)).length / ids.length;
|
|
15104
|
+
if (score > bestScore) {
|
|
15105
|
+
best = i;
|
|
15106
|
+
bestScore = score;
|
|
15107
|
+
tie = false;
|
|
15108
|
+
} else if (score === bestScore && best >= 0) tie = true;
|
|
15109
|
+
});
|
|
15110
|
+
if (best >= 0 && !tie) pairs.set(best, j);
|
|
15111
|
+
}
|
|
15112
|
+
return pairs;
|
|
15113
|
+
}
|
|
15114
|
+
function mediaView(doc, media, segments) {
|
|
15115
|
+
const own = (xs) => (xs ?? []).filter((x) => mediaKey3(x.media) === media.id).map((x) => {
|
|
15116
|
+
const { media: _, ...rest } = x;
|
|
15117
|
+
return rest;
|
|
15118
|
+
});
|
|
15119
|
+
return {
|
|
15120
|
+
...doc,
|
|
15121
|
+
source: {
|
|
15122
|
+
videoKey: media.videoKey,
|
|
15123
|
+
cursor: media.cursor,
|
|
15124
|
+
meta: media.meta
|
|
15125
|
+
},
|
|
15126
|
+
segments: own(segments),
|
|
15127
|
+
zoom: own(doc.zoom),
|
|
15128
|
+
speed: own(doc.speed),
|
|
15129
|
+
tilt: own(doc.tilt),
|
|
15130
|
+
freeze: own(doc.freeze),
|
|
15131
|
+
rejected: own(doc.rejected),
|
|
15132
|
+
media: void 0
|
|
15133
|
+
};
|
|
15134
|
+
}
|
|
15135
|
+
function rejoinDocs(prev, record, inputs, opts = {}) {
|
|
15136
|
+
const where = opts.where ?? ((p) => p);
|
|
15137
|
+
const { match, changed, removed } = matchInputs(record.inputs, inputs);
|
|
15138
|
+
const first = match[0] === null ? null : record.inputs[match[0]];
|
|
15139
|
+
if (!first || first.media !== "")
|
|
15140
|
+
throw new UsageError(
|
|
15141
|
+
`--keep-edits: ${inputs[0].part.label} is not the primary this take was joined from (its first input, footage unchanged). The primary carries the cut's frame and camera; keep it first, or join afresh with --force`
|
|
15142
|
+
);
|
|
15143
|
+
const redo = rerecordedInputs(prev, record, inputs, {
|
|
15144
|
+
match,
|
|
15145
|
+
changed,
|
|
15146
|
+
removed
|
|
15147
|
+
});
|
|
15148
|
+
const redoneFrom = new Set(redo.values());
|
|
15149
|
+
const isNew = (i) => match[i] === null && !redo.has(i);
|
|
15150
|
+
const parts = inputs.map((x, i) => {
|
|
15151
|
+
const j = match[i];
|
|
15152
|
+
if (i === 0 || j === null) return x.part;
|
|
15153
|
+
const was = record.inputs[j];
|
|
15154
|
+
return { ...x.part, id: was.media, ...was.sub ? { subIds: was.sub } : {} };
|
|
15155
|
+
});
|
|
15156
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
15157
|
+
...record.inputs.flatMap(keysOf),
|
|
15158
|
+
...(prev.media ?? []).map((m) => m.id)
|
|
15159
|
+
]);
|
|
15160
|
+
const fresh = joinDocs(parts, { reserved });
|
|
15161
|
+
const argOf = /* @__PURE__ */ new Map();
|
|
15162
|
+
const newIds = /* @__PURE__ */ new Set();
|
|
15163
|
+
const kept = /* @__PURE__ */ new Set();
|
|
15164
|
+
fresh.ids.forEach((ids, i) => {
|
|
15165
|
+
for (const k of [ids.id, ...Object.values(ids.sub)]) {
|
|
15166
|
+
argOf.set(k, i);
|
|
15167
|
+
if (isNew(i)) newIds.add(k);
|
|
15168
|
+
}
|
|
15169
|
+
});
|
|
15170
|
+
match.forEach((j, i) => {
|
|
15171
|
+
if (j === null) return;
|
|
15172
|
+
for (const k of keysOf(record.inputs[j])) kept.add(k);
|
|
15173
|
+
for (const k of [fresh.ids[i].id, ...Object.values(fresh.ids[i].sub)])
|
|
15174
|
+
kept.add(k);
|
|
15175
|
+
});
|
|
15176
|
+
kept.add("");
|
|
15177
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
15178
|
+
for (const k of [
|
|
15179
|
+
...record.inputs.flatMap(keysOf),
|
|
15180
|
+
...(prev.media ?? []).map((m) => m.id)
|
|
15181
|
+
])
|
|
15182
|
+
if (!kept.has(k)) dropped.add(k);
|
|
15183
|
+
const report = {
|
|
15184
|
+
kept: [],
|
|
15185
|
+
added: [],
|
|
15186
|
+
dropped: [],
|
|
15187
|
+
rerecorded: [],
|
|
15188
|
+
regrouped: false,
|
|
15189
|
+
shifted: [],
|
|
15190
|
+
notes: []
|
|
15191
|
+
};
|
|
15192
|
+
inputs.forEach((x, i) => {
|
|
15193
|
+
const j = match[i];
|
|
15194
|
+
if (j === null) {
|
|
15195
|
+
if (isNew(i))
|
|
15196
|
+
report.added.push({ label: x.part.label, id: fresh.ids[i].id });
|
|
15197
|
+
return;
|
|
15198
|
+
}
|
|
15199
|
+
const was = record.inputs[j];
|
|
15200
|
+
report.kept.push({
|
|
15201
|
+
label: x.part.label,
|
|
15202
|
+
id: was.media,
|
|
15203
|
+
...was.path && was.path !== x.path ? { moved: where(was.path) } : {}
|
|
15204
|
+
});
|
|
15205
|
+
});
|
|
15206
|
+
const doc = structuredClone(prev);
|
|
15207
|
+
const keyOf = (s) => mediaKey3(s.media);
|
|
15208
|
+
const prevSegments = prev.segments.length ? prev.segments : [{ in: 0, out: round32(prev.source.meta.durationMs / 1e3) }];
|
|
15209
|
+
const prevExt = segmentOutputExtents3(prev);
|
|
15210
|
+
const prevFootage = totalDuration2(ratedSegments9(prev));
|
|
15211
|
+
const redoByOld = /* @__PURE__ */ new Map();
|
|
15212
|
+
for (const [i, j] of redo) {
|
|
15213
|
+
const from = record.inputs[j].media;
|
|
15214
|
+
const to = fresh.ids[i].id;
|
|
15215
|
+
const was = (prev.media ?? []).find((m) => m.id === from);
|
|
15216
|
+
const view = mediaView(prev, was, prevSegments);
|
|
15217
|
+
const meta = inputs[i].part.doc.source.meta;
|
|
15218
|
+
const rt = retimeCut(view, meta.steps ?? [], meta.durationMs);
|
|
15219
|
+
const cut = view.segments.length ? splitSegmentsAtSkips2(rt.segments, meta.skips) : [];
|
|
15220
|
+
const x = inputs[i];
|
|
15221
|
+
redoByOld.set(from, {
|
|
15222
|
+
i,
|
|
15223
|
+
from,
|
|
15224
|
+
to,
|
|
15225
|
+
rt,
|
|
15226
|
+
segments: cut.map((sg) => ({ ...sg, media: to })),
|
|
15227
|
+
placed: false,
|
|
15228
|
+
entry: {
|
|
15229
|
+
label: x.part.label,
|
|
15230
|
+
from,
|
|
15231
|
+
id: to,
|
|
15232
|
+
...record.inputs[j].path && record.inputs[j].path !== x.path ? { was: where(record.inputs[j].path) } : {},
|
|
15233
|
+
segments: cut.length,
|
|
15234
|
+
spans: 0,
|
|
15235
|
+
layers: [],
|
|
15236
|
+
flagged: [...rt.report.flagged]
|
|
15237
|
+
}
|
|
15238
|
+
});
|
|
15239
|
+
}
|
|
15240
|
+
const redoTo = new Set([...redoByOld.values()].map((r) => r.to));
|
|
15241
|
+
let segments = prevSegments.flatMap((sg) => {
|
|
15242
|
+
const r = redoByOld.get(keyOf(sg));
|
|
15243
|
+
if (r) {
|
|
15244
|
+
if (r.placed) return [];
|
|
15245
|
+
r.placed = true;
|
|
15246
|
+
return r.segments;
|
|
15247
|
+
}
|
|
15248
|
+
return kept.has(keyOf(sg)) ? [sg] : [];
|
|
15249
|
+
});
|
|
15250
|
+
const keptOrder = match.filter((j) => j !== null);
|
|
15251
|
+
const inOrder = keptOrder.every((j, k) => k === 0 || keptOrder[k - 1] < j);
|
|
15252
|
+
if (!inOrder) {
|
|
15253
|
+
report.regrouped = true;
|
|
15254
|
+
segments = segments.map((s, at2) => ({ s, at: at2, arg: argOf.get(keyOf(s)) ?? 0 })).sort((a, b) => a.arg - b.arg || a.at - b.at).map((x) => x.s);
|
|
15255
|
+
}
|
|
15256
|
+
const freshSegs = fresh.doc.segments;
|
|
15257
|
+
const freshExt = segmentOutputExtents3(fresh.doc);
|
|
15258
|
+
const firstFresh = /* @__PURE__ */ new Map();
|
|
15259
|
+
inputs.forEach((_, i) => {
|
|
15260
|
+
if (!isNew(i)) return;
|
|
15261
|
+
const mine = freshSegs.map((s, at3) => ({ s, at: at3 })).filter((x) => argOf.get(keyOf(x.s)) === i);
|
|
15262
|
+
if (!mine.length) return;
|
|
15263
|
+
firstFresh.set(i, mine[0].at);
|
|
15264
|
+
let at2 = segments.length;
|
|
15265
|
+
for (let n = i + 1; n < inputs.length; n++) {
|
|
15266
|
+
if (isNew(n)) continue;
|
|
15267
|
+
const k = segments.findIndex((s) => argOf.get(keyOf(s)) === n);
|
|
15268
|
+
if (k >= 0) {
|
|
15269
|
+
at2 = k;
|
|
15270
|
+
break;
|
|
15271
|
+
}
|
|
15272
|
+
}
|
|
15273
|
+
segments.splice(at2, 0, ...mine.map((x) => x.s));
|
|
15274
|
+
});
|
|
15275
|
+
doc.segments = segments;
|
|
15276
|
+
const freshMedia = new Map((fresh.doc.media ?? []).map((m) => [m.id, m]));
|
|
15277
|
+
const media = [
|
|
15278
|
+
...(prev.media ?? []).filter((m) => kept.has(m.id)).map((m) => {
|
|
15279
|
+
const f = freshMedia.get(m.id);
|
|
15280
|
+
return !m.script && f?.script ? { ...m, script: f.script } : m;
|
|
15281
|
+
}),
|
|
15282
|
+
...(fresh.doc.media ?? []).filter(
|
|
15283
|
+
(m) => newIds.has(m.id) || redoTo.has(m.id)
|
|
15284
|
+
)
|
|
15285
|
+
].map((m, at2) => ({ m, at: at2, arg: argOf.get(m.id) ?? 0 })).sort((a, b) => a.arg - b.arg || a.at - b.at).map((x) => x.m);
|
|
15286
|
+
if (media.length) doc.media = media;
|
|
15287
|
+
else delete doc.media;
|
|
15288
|
+
const spanLanes = [
|
|
15289
|
+
"zoom",
|
|
15290
|
+
"speed",
|
|
15291
|
+
"tilt",
|
|
15292
|
+
"camMotion",
|
|
15293
|
+
"freeze",
|
|
15294
|
+
"rejected"
|
|
15295
|
+
];
|
|
15296
|
+
const dropCount = /* @__PURE__ */ new Map();
|
|
15297
|
+
const tally = (k, what) => {
|
|
15298
|
+
const c = dropCount.get(k) ?? { segments: 0, spans: 0 };
|
|
15299
|
+
c[what]++;
|
|
15300
|
+
dropCount.set(k, c);
|
|
15301
|
+
};
|
|
15302
|
+
for (const s of prevSegments)
|
|
15303
|
+
if (dropped.has(keyOf(s)) && !redoByOld.has(keyOf(s)))
|
|
15304
|
+
tally(keyOf(s), "segments");
|
|
15305
|
+
const redoSpans = (lane) => {
|
|
15306
|
+
const out = [];
|
|
15307
|
+
for (const r of redoByOld.values()) {
|
|
15308
|
+
const tag = (x) => ({ ...x, media: r.to });
|
|
15309
|
+
let mine;
|
|
15310
|
+
if (lane === "camMotion") {
|
|
15311
|
+
mine = (prev.camMotion ?? []).filter((x) => keyOf(x) === r.from).map(tag);
|
|
15312
|
+
} else {
|
|
15313
|
+
mine = r.rt[lane].map(tag);
|
|
15314
|
+
if (lane === "zoom" || lane === "speed" || lane === "tilt") {
|
|
15315
|
+
const rejected = r.rt.rejected;
|
|
15316
|
+
const proposed = (fresh.doc[lane] ?? []).filter(
|
|
15317
|
+
(x) => keyOf(x) === r.to && x.source === "auto" && !mine.some((m) => x.in < m.out && m.in < x.out) && !isRejected2(lane, x, rejected)
|
|
15318
|
+
);
|
|
15319
|
+
r.entry.spans += mine.length;
|
|
15320
|
+
mine = [...mine, ...proposed];
|
|
15321
|
+
} else if (lane === "rejected") {
|
|
15322
|
+
const fromTake = (fresh.doc.rejected ?? []).filter(
|
|
15323
|
+
(x) => keyOf(x) === r.to && !isRejected2(x.lane, x, r.rt.rejected)
|
|
15324
|
+
);
|
|
15325
|
+
r.entry.spans += mine.length;
|
|
15326
|
+
mine = [...mine, ...fromTake];
|
|
15327
|
+
} else r.entry.spans += mine.length;
|
|
15328
|
+
}
|
|
15329
|
+
if (lane === "camMotion") r.entry.spans += mine.length;
|
|
15330
|
+
out.push(...mine);
|
|
15331
|
+
}
|
|
15332
|
+
return out;
|
|
15333
|
+
};
|
|
15334
|
+
for (const lane of spanLanes) {
|
|
15335
|
+
const before = prev[lane] ?? [];
|
|
15336
|
+
for (const s of before)
|
|
15337
|
+
if (dropped.has(keyOf(s)) && !redoByOld.has(keyOf(s)))
|
|
15338
|
+
tally(keyOf(s), "spans");
|
|
15339
|
+
const next = [
|
|
15340
|
+
...before.filter((s) => kept.has(keyOf(s))),
|
|
15341
|
+
...redoSpans(lane),
|
|
15342
|
+
...(fresh.doc[lane] ?? []).filter(
|
|
15343
|
+
(s) => newIds.has(keyOf(s))
|
|
15344
|
+
)
|
|
15345
|
+
];
|
|
15346
|
+
if (next.length) doc[lane] = next;
|
|
15347
|
+
else delete doc[lane];
|
|
15348
|
+
}
|
|
15349
|
+
const newExt = segmentOutputExtents3(doc);
|
|
15350
|
+
const newFootage = totalDuration2(ratedSegments9(doc));
|
|
15351
|
+
const placeOf = /* @__PURE__ */ new Map();
|
|
15352
|
+
segments.forEach((s, i) => placeOf.set(s, i));
|
|
15353
|
+
const follow = (t) => {
|
|
15354
|
+
for (let i = 0; i < prevExt.length; i++) {
|
|
15355
|
+
const e = prevExt[i];
|
|
15356
|
+
if (t >= e.start - 1e-6 && t < e.end - 1e-6) {
|
|
15357
|
+
const at2 = placeOf.get(prevSegments[i]);
|
|
15358
|
+
if (at2 === void 0) return null;
|
|
15359
|
+
const moved = newExt[at2].start + (t - e.start);
|
|
15360
|
+
return Math.abs(moved - t) < 1e-9 ? t : round32(moved);
|
|
15361
|
+
}
|
|
15362
|
+
}
|
|
15363
|
+
if (t >= prevFootage - 1e-6) {
|
|
15364
|
+
const moved = newFootage + (t - prevFootage);
|
|
15365
|
+
return Math.abs(moved - t) < 1e-9 ? t : round32(moved);
|
|
15366
|
+
}
|
|
15367
|
+
return t;
|
|
15368
|
+
};
|
|
15369
|
+
const deltaOf = (k) => {
|
|
15370
|
+
const i = argOf.get(k);
|
|
15371
|
+
if (i === void 0) return 0;
|
|
15372
|
+
const at2 = firstFresh.get(i);
|
|
15373
|
+
if (at2 === void 0) return 0;
|
|
15374
|
+
const now = placeOf.get(freshSegs[at2]);
|
|
15375
|
+
return now === void 0 ? 0 : newExt[now].start - freshExt[at2].start;
|
|
15376
|
+
};
|
|
15377
|
+
const droppedItems = /* @__PURE__ */ new Map();
|
|
15378
|
+
const lose = (k, what, id) => {
|
|
15379
|
+
const c = droppedItems.get(k) ?? { layers: [], sound: [] };
|
|
15380
|
+
c[what].push(id);
|
|
15381
|
+
droppedItems.set(k, c);
|
|
15382
|
+
};
|
|
15383
|
+
const footageAt = (t) => {
|
|
15384
|
+
for (let i = 0; i < prevExt.length; i++) {
|
|
15385
|
+
const e = prevExt[i];
|
|
15386
|
+
if (t >= e.start - 1e-6 && t < e.end - 1e-6) return keyOf(prevSegments[i]);
|
|
15387
|
+
}
|
|
15388
|
+
return null;
|
|
15389
|
+
};
|
|
15390
|
+
const blockOf = (segs, ext2, k) => {
|
|
15391
|
+
const at2 = segs.flatMap((sg, i) => keyOf(sg) === k ? [ext2[i]] : []);
|
|
15392
|
+
return at2.length ? { start: at2[0].start, end: at2[at2.length - 1].end } : null;
|
|
15393
|
+
};
|
|
15394
|
+
const place2 = (t) => {
|
|
15395
|
+
const k = footageAt(t);
|
|
15396
|
+
const r = k === null ? void 0 : redoByOld.get(k);
|
|
15397
|
+
if (!r) return follow(t);
|
|
15398
|
+
const was = blockOf(prevSegments, prevExt, r.from);
|
|
15399
|
+
const now = blockOf(segments, newExt, r.to);
|
|
15400
|
+
if (!was || !now) return null;
|
|
15401
|
+
const moved = now.start + (t - was.start);
|
|
15402
|
+
if (moved >= now.end - 1e-6) return null;
|
|
15403
|
+
return Math.abs(moved - t) < 1e-9 ? t : round32(moved);
|
|
15404
|
+
};
|
|
15405
|
+
const loseOver = (t, what) => {
|
|
15406
|
+
const r = redoByOld.get(footageAt(t) ?? "\0");
|
|
15407
|
+
if (!r) return false;
|
|
15408
|
+
r.entry.flagged.push(
|
|
15409
|
+
`${what} (${t.toFixed(2)} s): it played over this input's footage past where its new recording ends; dropped`
|
|
15410
|
+
);
|
|
15411
|
+
return true;
|
|
15412
|
+
};
|
|
15413
|
+
const overlays = [];
|
|
15414
|
+
const redoLayers = /* @__PURE__ */ new Map();
|
|
15415
|
+
for (const prevLayer of prev.overlays ?? []) {
|
|
15416
|
+
let o = prevLayer;
|
|
15417
|
+
const refMedia = o.kind !== "text" && o.kind !== "html" ? mediaRefId(o.key) : null;
|
|
15418
|
+
const owner = o.anchor ? mediaKey3(o.anchor.media) : null;
|
|
15419
|
+
const goes = (k) => k !== null && dropped.has(k) && !redoByOld.has(k);
|
|
15420
|
+
const gone = goes(owner) && owner || goes(refMedia) && refMedia || null;
|
|
15421
|
+
if (gone) {
|
|
15422
|
+
lose(gone, "layers", o.id);
|
|
15423
|
+
continue;
|
|
15424
|
+
}
|
|
15425
|
+
const byOwner = owner === null ? void 0 : redoByOld.get(owner);
|
|
15426
|
+
if (byOwner && o.anchor) {
|
|
15427
|
+
o = { ...o, anchor: { ...o.anchor, media: byOwner.to } };
|
|
15428
|
+
redoLayers.set(o.id, byOwner);
|
|
15429
|
+
}
|
|
15430
|
+
const byRef = refMedia === null ? void 0 : redoByOld.get(refMedia);
|
|
15431
|
+
if (byRef && o.kind !== "text" && o.kind !== "html") {
|
|
15432
|
+
o = { ...o, key: `media:${byRef.to}` };
|
|
15433
|
+
redoLayers.set(o.id, byRef);
|
|
15434
|
+
}
|
|
15435
|
+
if (o.anchor || o.start === void 0) {
|
|
15436
|
+
overlays.push(o);
|
|
15437
|
+
continue;
|
|
15438
|
+
}
|
|
15439
|
+
const t = place2(o.start);
|
|
15440
|
+
if (t === null) {
|
|
15441
|
+
if (!loseOver(o.start, `layer ${o.id}`))
|
|
15442
|
+
lose(footageAt(o.start) ?? "", "layers", o.id);
|
|
15443
|
+
continue;
|
|
15444
|
+
}
|
|
15445
|
+
const over = redoByOld.get(footageAt(o.start) ?? "\0");
|
|
15446
|
+
if (over) redoLayers.set(o.id, over);
|
|
15447
|
+
if (t !== o.start) report.shifted.push(o.id);
|
|
15448
|
+
overlays.push(t === o.start ? o : { ...o, start: t });
|
|
15449
|
+
}
|
|
15450
|
+
for (const o of fresh.doc.overlays ?? []) {
|
|
15451
|
+
const k = fresh.owners.get(o.id);
|
|
15452
|
+
if (k === void 0 || !newIds.has(k)) continue;
|
|
15453
|
+
const d = o.anchor ? 0 : deltaOf(k);
|
|
15454
|
+
overlays.push(
|
|
15455
|
+
d && o.start !== void 0 ? { ...o, start: round32(o.start + d) } : o
|
|
15456
|
+
);
|
|
15457
|
+
}
|
|
15458
|
+
if (overlays.length) doc.overlays = overlays;
|
|
15459
|
+
else delete doc.overlays;
|
|
15460
|
+
const objects = [];
|
|
15461
|
+
for (const ob of prev.objects ?? []) {
|
|
15462
|
+
if (!ob.span) {
|
|
15463
|
+
objects.push(ob);
|
|
15464
|
+
continue;
|
|
15465
|
+
}
|
|
15466
|
+
const t = place2(ob.span.start);
|
|
15467
|
+
if (t === null) {
|
|
15468
|
+
if (!loseOver(ob.span.start, `prop ${ob.id}`))
|
|
15469
|
+
lose(footageAt(ob.span.start) ?? "", "layers", ob.id);
|
|
15470
|
+
continue;
|
|
15471
|
+
}
|
|
15472
|
+
if (t !== ob.span.start) report.shifted.push(ob.id);
|
|
15473
|
+
objects.push(
|
|
15474
|
+
t === ob.span.start ? ob : { ...ob, span: { ...ob.span, start: t } }
|
|
15475
|
+
);
|
|
15476
|
+
}
|
|
15477
|
+
for (const ob of fresh.doc.objects ?? []) {
|
|
15478
|
+
const k = fresh.owners.get(ob.id);
|
|
15479
|
+
if (k === void 0 || !newIds.has(k)) continue;
|
|
15480
|
+
const d = deltaOf(k);
|
|
15481
|
+
objects.push(
|
|
15482
|
+
d && ob.span ? { ...ob, span: { ...ob.span, start: round32(ob.span.start + d) } } : ob
|
|
15483
|
+
);
|
|
15484
|
+
}
|
|
15485
|
+
if (objects.length) doc.objects = objects;
|
|
15486
|
+
else delete doc.objects;
|
|
15487
|
+
const allPrev = /* @__PURE__ */ new Set([...dropped, ...kept]);
|
|
15488
|
+
const audio = [];
|
|
15489
|
+
for (const a of prev.audio) {
|
|
15490
|
+
const m = /^(.*)-sound$/.exec(a.id);
|
|
15491
|
+
const k = m && allPrev.has(m[1]) ? m[1] : null;
|
|
15492
|
+
if (k === null) {
|
|
15493
|
+
audio.push(a);
|
|
15494
|
+
continue;
|
|
15495
|
+
}
|
|
15496
|
+
const r = redoByOld.get(k);
|
|
15497
|
+
if (r) {
|
|
15498
|
+
r.entry.flagged.push(
|
|
15499
|
+
`sound ${a.id}: it was the old recording's own sound; dropped`
|
|
15500
|
+
);
|
|
15501
|
+
continue;
|
|
15502
|
+
}
|
|
15503
|
+
if (dropped.has(k)) {
|
|
15504
|
+
lose(k, "sound", a.id);
|
|
15505
|
+
continue;
|
|
15506
|
+
}
|
|
15507
|
+
const t = follow(a.start);
|
|
15508
|
+
if (t === null) {
|
|
15509
|
+
lose(k, "sound", a.id);
|
|
15510
|
+
continue;
|
|
15511
|
+
}
|
|
15512
|
+
if (t !== a.start) report.shifted.push(a.id);
|
|
15513
|
+
audio.push(t === a.start ? a : { ...a, start: t });
|
|
15514
|
+
}
|
|
15515
|
+
for (const a of fresh.doc.audio) {
|
|
15516
|
+
const k = fresh.owners.get(a.id);
|
|
15517
|
+
if (k === void 0 || !newIds.has(k)) continue;
|
|
15518
|
+
const d = deltaOf(k);
|
|
15519
|
+
audio.push(d ? { ...a, start: round32(a.start + d) } : a);
|
|
15520
|
+
}
|
|
15521
|
+
doc.audio = audio;
|
|
15522
|
+
if (typeof prev.still === "number") {
|
|
15523
|
+
const t = place2(prev.still);
|
|
15524
|
+
if (t === null) {
|
|
15525
|
+
delete doc.still;
|
|
15526
|
+
report.notes.push(
|
|
15527
|
+
`the still (${prev.still.toFixed(2)} s) played over footage that is gone; the cover falls back to its default (vos push --still sets one)`
|
|
15528
|
+
);
|
|
15529
|
+
} else if (t !== prev.still) {
|
|
15530
|
+
doc.still = t;
|
|
15531
|
+
report.shifted.push("still");
|
|
15532
|
+
}
|
|
15533
|
+
}
|
|
15534
|
+
const pieces = ratedSegments9(doc);
|
|
15535
|
+
if (doc.overlays) {
|
|
15536
|
+
doc.overlays = doc.overlays.flatMap((o) => {
|
|
15537
|
+
const r = o.anchor ? resolveAnchor6(doc, o.anchor, pieces) : null;
|
|
15538
|
+
const redone = redoLayers.get(o.id);
|
|
15539
|
+
if (o.anchor && r && r.start === null && redone) {
|
|
15540
|
+
redone.entry.flagged.push(
|
|
15541
|
+
`layer ${o.id}: anchored to step ${String(o.anchor.step)}, which the new recording does not have, skipped, or the cut leaves out; dropped`
|
|
15542
|
+
);
|
|
15543
|
+
return [];
|
|
15544
|
+
}
|
|
15545
|
+
if (redone) redone.entry.layers.push(o.id);
|
|
15546
|
+
return r && r.start !== null && o.start !== void 0 && r.start !== o.start ? [{ ...o, start: r.start }] : [o];
|
|
15547
|
+
});
|
|
15548
|
+
if (!doc.overlays.length) delete doc.overlays;
|
|
15549
|
+
}
|
|
15550
|
+
const prevPieces = ratedSegments9(prev);
|
|
15551
|
+
const wasEnding = new Set(
|
|
15552
|
+
(prev.overlays ?? []).filter((o) => {
|
|
15553
|
+
if (!o.anchor) return false;
|
|
15554
|
+
const r = resolveAnchor6(prev, o.anchor, prevPieces);
|
|
15555
|
+
return r.start !== null && r.start >= prevFootage - 1e-3;
|
|
15556
|
+
}).map((o) => o.id)
|
|
15557
|
+
);
|
|
15558
|
+
const overrun = (doc.overlays ?? []).filter((o) => {
|
|
15559
|
+
if (!wasEnding.has(o.id)) return false;
|
|
15560
|
+
const start = layerStart3(o, doc, pieces);
|
|
15561
|
+
return start !== null && start < newFootage - 1e-3;
|
|
15562
|
+
}).map((o) => o.id);
|
|
15563
|
+
if (overrun.length)
|
|
15564
|
+
report.notes.push(
|
|
15565
|
+
`${overrun.join(", ")} played after the footage and, anchored to ${overrun.length === 1 ? "its" : "their"} step, now ${overrun.length === 1 ? "lands" : "land"} over the footage that follows; anchor ${overrun.length === 1 ? "it" : "them"} to the last input's end, or move ${overrun.length === 1 ? "it" : "them"}`
|
|
15566
|
+
);
|
|
15567
|
+
const labelOf = (k) => {
|
|
15568
|
+
const x = record.inputs.find((r) => keysOf(r).includes(k));
|
|
15569
|
+
return x?.path ? where(x.path) : k ? `media ${k}` : "the primary";
|
|
15570
|
+
};
|
|
15571
|
+
for (const j of [...changed, ...removed]) {
|
|
15572
|
+
if (redoneFrom.has(j)) continue;
|
|
15573
|
+
const x = record.inputs[j];
|
|
15574
|
+
for (const k of keysOf(x)) {
|
|
15575
|
+
const c = dropCount.get(k) ?? { segments: 0, spans: 0 };
|
|
15576
|
+
const items = droppedItems.get(k) ?? { layers: [], sound: [] };
|
|
15577
|
+
report.dropped.push({
|
|
15578
|
+
input: labelOf(k),
|
|
15579
|
+
id: k,
|
|
15580
|
+
why: changed.includes(j) ? "changed" : "removed",
|
|
15581
|
+
segments: c.segments,
|
|
15582
|
+
spans: c.spans,
|
|
15583
|
+
layers: items.layers,
|
|
15584
|
+
sound: items.sound
|
|
15585
|
+
});
|
|
15586
|
+
}
|
|
15587
|
+
}
|
|
15588
|
+
for (const k of dropped) {
|
|
15589
|
+
if (report.dropped.some((d) => d.id === k) || redoByOld.has(k)) continue;
|
|
15590
|
+
const c = dropCount.get(k) ?? { segments: 0, spans: 0 };
|
|
15591
|
+
const items = droppedItems.get(k) ?? { layers: [], sound: [] };
|
|
15592
|
+
report.dropped.push({
|
|
15593
|
+
input: `media ${k}`,
|
|
15594
|
+
id: k,
|
|
15595
|
+
why: "removed",
|
|
15596
|
+
segments: c.segments,
|
|
15597
|
+
spans: c.spans,
|
|
15598
|
+
layers: items.layers,
|
|
15599
|
+
sound: items.sound
|
|
15600
|
+
});
|
|
15601
|
+
}
|
|
15602
|
+
if (Math.abs(newFootage - prevFootage) > 1e-3) {
|
|
15603
|
+
const film = doc.audio.filter((a) => !/-sound$/.test(a.id));
|
|
15604
|
+
if (film.length)
|
|
15605
|
+
report.notes.push(
|
|
15606
|
+
`the footage now runs ${newFootage.toFixed(2)} s (was ${prevFootage.toFixed(2)} s); ${film.map((a) => a.id).join(", ")} kept ${film.length === 1 ? "its" : "their"} seconds (vos score places a score again)`
|
|
15607
|
+
);
|
|
15608
|
+
}
|
|
15609
|
+
report.rerecorded = [...redoByOld.values()].map((r) => r.entry);
|
|
15610
|
+
const newLabels = inputs.filter((_, i) => isNew(i)).map((x) => x.part.label);
|
|
15611
|
+
const notes = fresh.notes.filter(
|
|
15612
|
+
(n) => newLabels.some((l) => n.startsWith(`${l}:`))
|
|
15613
|
+
);
|
|
15614
|
+
const copies = fresh.copies.filter(
|
|
15615
|
+
(c) => [...newIds, ...redoTo].some((k) => k !== "" && c.to.startsWith(`${k}/`))
|
|
15616
|
+
);
|
|
15617
|
+
return {
|
|
15618
|
+
plan: {
|
|
15619
|
+
doc,
|
|
15620
|
+
copies,
|
|
15621
|
+
media: fresh.media,
|
|
15622
|
+
actions: fresh.actions,
|
|
15623
|
+
notes,
|
|
15624
|
+
ids: fresh.ids,
|
|
15625
|
+
owners: fresh.owners
|
|
15626
|
+
},
|
|
15627
|
+
report,
|
|
15628
|
+
newIds,
|
|
15629
|
+
dropped
|
|
15630
|
+
};
|
|
15631
|
+
}
|
|
15632
|
+
function rejoinLines(r) {
|
|
15633
|
+
const lines = [];
|
|
15634
|
+
const n = (v, one, many = `${one}s`) => `${v} ${v === 1 ? one : many}`;
|
|
15635
|
+
if (r.kept.length)
|
|
15636
|
+
lines.push(
|
|
15637
|
+
`kept the edits of ${r.kept.map((k) => `${k.label} (${k.id || "primary"}${k.moved ? `, found at its new path; was ${k.moved}` : ""})`).join(", ")}`
|
|
15638
|
+
);
|
|
15639
|
+
if (r.added.length)
|
|
15640
|
+
lines.push(`added ${r.added.map((a) => `${a.label} \u2192 ${a.id}`).join(", ")}`);
|
|
15641
|
+
for (const x of r.rerecorded) {
|
|
15642
|
+
lines.push(
|
|
15643
|
+
`re-recorded ${x.label}${x.was ? ` (was ${x.was})` : ""}: ${x.from} \u2192 ${x.id}; its edits re-timed by its steps: ${[
|
|
15644
|
+
n(x.segments, "segment"),
|
|
15645
|
+
n(x.spans, "span"),
|
|
15646
|
+
x.layers.length ? `layers ${x.layers.join(", ")}` : "0 layers"
|
|
15647
|
+
].join(", ")}`
|
|
15648
|
+
);
|
|
15649
|
+
for (const f of x.flagged) lines.push(` ${f}`);
|
|
15650
|
+
}
|
|
15651
|
+
for (const d of r.dropped) {
|
|
15652
|
+
const what = [
|
|
15653
|
+
n(d.segments, "segment"),
|
|
15654
|
+
n(d.spans, "span"),
|
|
15655
|
+
...d.layers.length ? [`layers ${d.layers.join(", ")}`] : [],
|
|
15656
|
+
...d.sound.length ? [`sound ${d.sound.join(", ")}`] : []
|
|
15657
|
+
].join(", ");
|
|
15658
|
+
lines.push(
|
|
15659
|
+
`dropped ${d.input} (${d.id || "primary"}): ${d.why === "changed" ? "its footage changed since the join" : "no longer an input"}; ${what} went with it`
|
|
15660
|
+
);
|
|
15661
|
+
}
|
|
15662
|
+
if (r.regrouped)
|
|
15663
|
+
lines.push(
|
|
15664
|
+
"the inputs changed order, so the segments play in argument order again (each input\u2019s own segments in their edited order)"
|
|
15665
|
+
);
|
|
15666
|
+
if (r.shifted.length)
|
|
15667
|
+
lines.push(
|
|
15668
|
+
`${r.shifted.join(", ")} moved with the footage ${r.shifted.length === 1 ? "it plays" : "they play"} over`
|
|
15669
|
+
);
|
|
15670
|
+
lines.push(...r.notes);
|
|
15671
|
+
return lines;
|
|
15672
|
+
}
|
|
15673
|
+
|
|
15674
|
+
// src/plugin/join.ts
|
|
15675
|
+
var FULL_BLEED_FRAME = {
|
|
15676
|
+
inset: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
15677
|
+
radius: 0,
|
|
15678
|
+
shadow: 0,
|
|
15679
|
+
shadowContact: 0,
|
|
15680
|
+
border: 0,
|
|
15681
|
+
fit: "cover"
|
|
15682
|
+
};
|
|
15683
|
+
var round33 = (v) => Math.round(v * 1e3) / 1e3;
|
|
15684
|
+
var FILE_SOUND_FADE = 0.15;
|
|
15685
|
+
var prefixed = (id, key) => `${id}/${takeRelativeFile(key)}`;
|
|
15686
|
+
function fileDoc(key, probed, t0 = 0) {
|
|
15687
|
+
const meta = {
|
|
15688
|
+
dpr: 1,
|
|
15689
|
+
zoom: 1,
|
|
15690
|
+
t0,
|
|
15691
|
+
durationMs: probed.durationMs,
|
|
15692
|
+
width: probed.width,
|
|
15693
|
+
height: probed.height,
|
|
15694
|
+
captureWidth: probed.width,
|
|
15695
|
+
captureHeight: probed.height,
|
|
15696
|
+
fps: probed.fps > 0 ? probed.fps : 30,
|
|
15697
|
+
hasAudio: probed.hasAudio,
|
|
15698
|
+
producer: "ingest"
|
|
15699
|
+
};
|
|
15700
|
+
const doc = projectFromArtifact2({ videoKey: key, cursor: [], meta }, key, {
|
|
15701
|
+
footage: true
|
|
15702
|
+
}).doc;
|
|
15703
|
+
doc.segments = [{ in: 0, out: round33(probed.durationMs / 1e3) }];
|
|
15704
|
+
return doc;
|
|
15705
|
+
}
|
|
15706
|
+
function endingLayers(doc) {
|
|
15707
|
+
const pieces = ratedSegments10(doc);
|
|
15708
|
+
const end = totalDuration3(pieces);
|
|
15709
|
+
if (!(end > 0)) return [];
|
|
15710
|
+
return (doc.overlays ?? []).filter((o) => {
|
|
15711
|
+
const start = layerStart4(o, doc, pieces);
|
|
15712
|
+
return start !== null && start >= end - 1e-3;
|
|
15713
|
+
});
|
|
15714
|
+
}
|
|
15715
|
+
function dropEnding(d, label, notes) {
|
|
15716
|
+
const ending = endingLayers(d);
|
|
15717
|
+
const legacy = d.endCard !== void 0;
|
|
15718
|
+
if (!ending.length && !legacy) return;
|
|
15719
|
+
const gone = new Set(ending);
|
|
15720
|
+
d.overlays = (d.overlays ?? []).filter((o) => !gone.has(o));
|
|
15721
|
+
if (!d.overlays.length) delete d.overlays;
|
|
15722
|
+
if (legacy) delete d.endCard;
|
|
14355
15723
|
const named = [
|
|
14356
15724
|
...ending.map((o) => o.id),
|
|
14357
15725
|
...legacy ? ["its endCard"] : []
|
|
@@ -14361,9 +15729,9 @@ function dropEnding(d, label, notes) {
|
|
|
14361
15729
|
);
|
|
14362
15730
|
}
|
|
14363
15731
|
function segs0(d) {
|
|
14364
|
-
return d.segments.length ? d.segments[0] : { in: 0, out:
|
|
15732
|
+
return d.segments.length ? d.segments[0] : { in: 0, out: round33(d.source.meta.durationMs / 1e3) };
|
|
14365
15733
|
}
|
|
14366
|
-
function joinDocs(parts) {
|
|
15734
|
+
function joinDocs(parts, opts = {}) {
|
|
14367
15735
|
if (parts.length < 2)
|
|
14368
15736
|
throw new UsageError("vos join needs two inputs or more");
|
|
14369
15737
|
const [first, ...rest] = parts;
|
|
@@ -14390,14 +15758,28 @@ function joinDocs(parts) {
|
|
|
14390
15758
|
];
|
|
14391
15759
|
for (const ref of refs)
|
|
14392
15760
|
copy({
|
|
14393
|
-
from:
|
|
15761
|
+
from: join27(first.dir, takeRelativeFile(ref.key)),
|
|
14394
15762
|
to: takeRelativeFile(ref.key),
|
|
14395
15763
|
base: first.dir
|
|
14396
15764
|
});
|
|
14397
15765
|
}
|
|
14398
15766
|
if (!doc.segments.length)
|
|
14399
|
-
doc.segments = [{ in: 0, out:
|
|
14400
|
-
const taken = new Set(
|
|
15767
|
+
doc.segments = [{ in: 0, out: round33(doc.source.meta.durationMs / 1e3) }];
|
|
15768
|
+
const taken = /* @__PURE__ */ new Set([
|
|
15769
|
+
...(doc.media ?? []).map((m) => m.id),
|
|
15770
|
+
...opts.reserved ?? [],
|
|
15771
|
+
...rest.flatMap((p) => [
|
|
15772
|
+
...p.id ? [p.id] : [],
|
|
15773
|
+
...Object.values(p.subIds ?? {})
|
|
15774
|
+
])
|
|
15775
|
+
]);
|
|
15776
|
+
const ids = [
|
|
15777
|
+
{
|
|
15778
|
+
id: "",
|
|
15779
|
+
sub: Object.fromEntries((doc.media ?? []).map((m) => [m.id, m.id]))
|
|
15780
|
+
}
|
|
15781
|
+
];
|
|
15782
|
+
const owners = /* @__PURE__ */ new Map();
|
|
14401
15783
|
const allocate = () => {
|
|
14402
15784
|
let n = 1;
|
|
14403
15785
|
while (taken.has(`m${n}`)) n++;
|
|
@@ -14410,10 +15792,17 @@ function joinDocs(parts) {
|
|
|
14410
15792
|
for (const part of rest) {
|
|
14411
15793
|
const d = structuredClone(migrateMotion3(part.doc));
|
|
14412
15794
|
if (part !== rest[rest.length - 1]) dropEnding(d, part.label, notes);
|
|
14413
|
-
const pid = allocate();
|
|
15795
|
+
const pid = part.id ?? allocate();
|
|
14414
15796
|
const idMap = /* @__PURE__ */ new Map([["", pid]]);
|
|
14415
|
-
for (const m of d.media ?? [])
|
|
14416
|
-
|
|
15797
|
+
for (const m of d.media ?? [])
|
|
15798
|
+
idMap.set(m.id, part.subIds?.[m.id] ?? allocate());
|
|
15799
|
+
ids.push({
|
|
15800
|
+
id: pid,
|
|
15801
|
+
sub: Object.fromEntries(
|
|
15802
|
+
(d.media ?? []).map((m) => [m.id, idMap.get(m.id)])
|
|
15803
|
+
)
|
|
15804
|
+
});
|
|
15805
|
+
const mapMedia = (k) => idMap.get(mediaKey4(k)) ?? pid;
|
|
14417
15806
|
joined.push({ label: part.label, id: pid });
|
|
14418
15807
|
actions.set(pid, part.actions);
|
|
14419
15808
|
if (part.file) {
|
|
@@ -14452,20 +15841,22 @@ function joinDocs(parts) {
|
|
|
14452
15841
|
for (const ref of refs) {
|
|
14453
15842
|
const to = prefixed(pid, ref.key);
|
|
14454
15843
|
copy({
|
|
14455
|
-
from:
|
|
15844
|
+
from: join27(part.dir, takeRelativeFile(ref.key)),
|
|
14456
15845
|
to,
|
|
14457
15846
|
base: part.dir
|
|
14458
15847
|
});
|
|
14459
15848
|
ref.set(to);
|
|
14460
15849
|
}
|
|
14461
15850
|
}
|
|
15851
|
+
const script = !d.source.script && part.actions ? publicScript(part.actions) : void 0;
|
|
14462
15852
|
media.push({
|
|
14463
15853
|
...d.source,
|
|
14464
15854
|
id: pid,
|
|
15855
|
+
...script ? { script } : {},
|
|
14465
15856
|
...part.kind === "file" ? { frame: { ...FULL_BLEED_FRAME } } : {}
|
|
14466
15857
|
});
|
|
14467
15858
|
for (const m of d.media ?? []) media.push({ ...m, id: idMap.get(m.id) });
|
|
14468
|
-
const segs = d.segments.length ? d.segments : [{ in: 0, out:
|
|
15859
|
+
const segs = d.segments.length ? d.segments : [{ in: 0, out: round33(d.source.meta.durationMs / 1e3) }];
|
|
14469
15860
|
const at2 = doc.segments.length;
|
|
14470
15861
|
doc.segments.push(...segs.map((s) => ({ ...s, media: mapMedia(s.media) })));
|
|
14471
15862
|
const ns = (id) => `${pid}-${id}`;
|
|
@@ -14490,30 +15881,31 @@ function joinDocs(parts) {
|
|
|
14490
15881
|
placed.push({ part, d, pid, firstSegment: at2, mapMedia });
|
|
14491
15882
|
}
|
|
14492
15883
|
if (media.length) doc.media = media;
|
|
14493
|
-
const extents =
|
|
15884
|
+
const extents = segmentOutputExtents4(doc);
|
|
14494
15885
|
for (const { part, d, pid, firstSegment, mapMedia } of placed) {
|
|
14495
15886
|
const offset = extents[firstSegment]?.start ?? 0;
|
|
14496
|
-
const ownPieces =
|
|
15887
|
+
const ownPieces = ratedSegments10(d);
|
|
14497
15888
|
const ns = (id) => `${pid}-${id}`;
|
|
14498
15889
|
let dropped = 0;
|
|
14499
15890
|
for (const o of d.overlays ?? []) {
|
|
14500
15891
|
const next = { ...o, id: ns(o.id) };
|
|
14501
15892
|
if (next.kind !== "text" && next.kind !== "html") {
|
|
14502
|
-
const ref =
|
|
15893
|
+
const ref = mediaRefId2(next.key);
|
|
14503
15894
|
if (ref !== null) next.key = `media:${mapMedia(ref)}`;
|
|
14504
15895
|
}
|
|
14505
15896
|
if (o.anchor) {
|
|
14506
15897
|
next.anchor = { ...o.anchor, media: mapMedia(o.anchor.media) };
|
|
15898
|
+
} else if (o.start === void 0) {
|
|
14507
15899
|
} else if (o.pin?.step !== void 0) {
|
|
14508
|
-
const r =
|
|
15900
|
+
const r = resolveAnchor7(d, { step: o.pin.step }, ownPieces);
|
|
14509
15901
|
if (r.start !== null) {
|
|
14510
15902
|
next.anchor = {
|
|
14511
15903
|
step: o.pin.step,
|
|
14512
15904
|
media: pid,
|
|
14513
|
-
offset:
|
|
15905
|
+
offset: round33(o.start - r.start)
|
|
14514
15906
|
};
|
|
14515
15907
|
} else {
|
|
14516
|
-
next.start =
|
|
15908
|
+
next.start = round33(o.start + offset);
|
|
14517
15909
|
}
|
|
14518
15910
|
delete next.pin;
|
|
14519
15911
|
dropped++;
|
|
@@ -14522,10 +15914,11 @@ function joinDocs(parts) {
|
|
|
14522
15914
|
delete next.pin;
|
|
14523
15915
|
dropped++;
|
|
14524
15916
|
}
|
|
14525
|
-
next.start =
|
|
15917
|
+
next.start = round33(o.start + offset);
|
|
14526
15918
|
}
|
|
14527
15919
|
;
|
|
14528
15920
|
(doc.overlays ??= []).push(next);
|
|
15921
|
+
owners.set(next.id, pid);
|
|
14529
15922
|
}
|
|
14530
15923
|
if (dropped)
|
|
14531
15924
|
notes.push(
|
|
@@ -14538,18 +15931,19 @@ function joinDocs(parts) {
|
|
|
14538
15931
|
notes.push(`${part.label}: its own sound is left out (--mute)`);
|
|
14539
15932
|
} else if (seg && at2) {
|
|
14540
15933
|
const len = Math.max(0, seg.out - seg.in);
|
|
14541
|
-
const fade =
|
|
15934
|
+
const fade = round33(Math.min(FILE_SOUND_FADE, len / 4));
|
|
14542
15935
|
const id = `${pid}-sound`;
|
|
15936
|
+
owners.set(id, pid);
|
|
14543
15937
|
doc.audio = [
|
|
14544
15938
|
...doc.audio,
|
|
14545
15939
|
{
|
|
14546
15940
|
id,
|
|
14547
15941
|
key: d.source.videoKey,
|
|
14548
|
-
name:
|
|
14549
|
-
start:
|
|
14550
|
-
in:
|
|
14551
|
-
out:
|
|
14552
|
-
duration:
|
|
15942
|
+
name: basename13(part.file.path),
|
|
15943
|
+
start: round33(at2.start),
|
|
15944
|
+
in: round33(seg.in),
|
|
15945
|
+
out: round33(seg.out),
|
|
15946
|
+
duration: round33(d.source.meta.durationMs / 1e3),
|
|
14553
15947
|
gain: 1,
|
|
14554
15948
|
fadeIn: fade,
|
|
14555
15949
|
fadeOut: fade
|
|
@@ -14561,11 +15955,11 @@ function joinDocs(parts) {
|
|
|
14561
15955
|
}
|
|
14562
15956
|
}
|
|
14563
15957
|
for (const ob of d.objects ?? []) {
|
|
14564
|
-
;
|
|
15958
|
+
owners.set(ns(ob.id), pid);
|
|
14565
15959
|
(doc.objects ??= []).push({
|
|
14566
15960
|
...ob,
|
|
14567
15961
|
id: ns(ob.id),
|
|
14568
|
-
...ob.span ? { span: { ...ob.span, start:
|
|
15962
|
+
...ob.span ? { span: { ...ob.span, start: round33(ob.span.start + offset) } } : {}
|
|
14569
15963
|
});
|
|
14570
15964
|
}
|
|
14571
15965
|
}
|
|
@@ -14578,26 +15972,40 @@ function joinDocs(parts) {
|
|
|
14578
15972
|
notes.push(
|
|
14579
15973
|
`${first.label}: --mute leaves nothing out (the first input's sound is the film's own)`
|
|
14580
15974
|
);
|
|
14581
|
-
const pieces =
|
|
15975
|
+
const pieces = ratedSegments10(doc);
|
|
14582
15976
|
for (const o of doc.overlays ?? []) {
|
|
14583
|
-
if (!o.anchor) continue;
|
|
14584
|
-
const r =
|
|
15977
|
+
if (!o.anchor || o.start === void 0) continue;
|
|
15978
|
+
const r = resolveAnchor7(doc, o.anchor, pieces);
|
|
14585
15979
|
if (r.start !== null) o.start = r.start;
|
|
14586
15980
|
}
|
|
14587
|
-
return {
|
|
15981
|
+
return {
|
|
15982
|
+
doc,
|
|
15983
|
+
copies: [...copies.values()],
|
|
15984
|
+
media: joined,
|
|
15985
|
+
actions,
|
|
15986
|
+
notes,
|
|
15987
|
+
ids,
|
|
15988
|
+
owners
|
|
15989
|
+
};
|
|
14588
15990
|
}
|
|
14589
|
-
var BOOLEAN_FLAGS13 = /* @__PURE__ */ new Set([
|
|
15991
|
+
var BOOLEAN_FLAGS13 = /* @__PURE__ */ new Set([
|
|
15992
|
+
"json",
|
|
15993
|
+
"help",
|
|
15994
|
+
"force",
|
|
15995
|
+
"keep-edits",
|
|
15996
|
+
"captions"
|
|
15997
|
+
]);
|
|
14590
15998
|
var MULTI = /* @__PURE__ */ new Set(["mute"]);
|
|
14591
|
-
var USAGE3 = "vos join <take> <take|file.mp4>\u2026 --out <dir> [--force] [--captions] [--mute <file>]... [--json]";
|
|
15999
|
+
var USAGE3 = "vos join <take> <take|file.mp4>\u2026 --out <dir> [--keep-edits|--force] [--captions] [--zooms per-beat] [--mute <file>]... [--json]";
|
|
14592
16000
|
var within = (inner, outer) => {
|
|
14593
16001
|
const rel = relative5(outer, inner);
|
|
14594
16002
|
return rel === "" || !rel.startsWith(`..${sep4}`) && rel !== ".." && !isAbsolute4(rel);
|
|
14595
16003
|
};
|
|
14596
16004
|
async function readPart(arg) {
|
|
14597
16005
|
const path = resolve17(arg);
|
|
14598
|
-
if (!
|
|
16006
|
+
if (!existsSync25(path)) throw new UsageError(`${arg}: no such take or file`);
|
|
14599
16007
|
if (statSync7(path).isDirectory()) {
|
|
14600
|
-
if (!
|
|
16008
|
+
if (!existsSync25(join27(path, "meta.json")))
|
|
14601
16009
|
throw new UsageError(
|
|
14602
16010
|
`${arg}: not a take directory (no meta.json); a video file is joined by its path`
|
|
14603
16011
|
);
|
|
@@ -14634,8 +16042,16 @@ async function cmdJoin(argv) {
|
|
|
14634
16042
|
const r = createReporter(flags.json === true);
|
|
14635
16043
|
const outArg = strFlag(flags, "out");
|
|
14636
16044
|
const force = flags.force === true;
|
|
16045
|
+
const keepEdits = flags["keep-edits"] === true;
|
|
14637
16046
|
const withCaptions = flags.captions === true;
|
|
16047
|
+
const zoomMode = zoomModeOf(strFlag(flags, "zooms"));
|
|
16048
|
+
if (zoomMode && typeof zoomMode === "object")
|
|
16049
|
+
throw new UsageError(zoomMode.error);
|
|
14638
16050
|
if (positionals.length < 2 || !outArg) throw new UsageError(USAGE3);
|
|
16051
|
+
if (force && keepEdits)
|
|
16052
|
+
throw new UsageError(
|
|
16053
|
+
"--force replaces the joined take and --keep-edits keeps its edits; pass one"
|
|
16054
|
+
);
|
|
14639
16055
|
const out = resolve17(outArg);
|
|
14640
16056
|
const inputs = positionals.map((p) => resolve17(p));
|
|
14641
16057
|
const muted = /* @__PURE__ */ new Set();
|
|
@@ -14653,42 +16069,85 @@ async function cmdJoin(argv) {
|
|
|
14653
16069
|
`--out ${outArg} overlaps the input ${positionals[i]}; write the joined take somewhere of its own`
|
|
14654
16070
|
);
|
|
14655
16071
|
}
|
|
14656
|
-
|
|
14657
|
-
|
|
16072
|
+
let rejoin = false;
|
|
16073
|
+
if (existsSync25(out)) {
|
|
16074
|
+
if (!force && !keepEdits)
|
|
14658
16075
|
throw new UsageError(
|
|
14659
|
-
`--out ${outArg} exists; pass --force to replace it, or name a new directory`
|
|
14660
|
-
);
|
|
14661
|
-
const isTake = existsSync24(join26(out, "doc.json")) || existsSync24(join26(out, "meta.json"));
|
|
14662
|
-
if (!isTake && readdirSync5(out).length > 0)
|
|
14663
|
-
throw new UsageError(
|
|
14664
|
-
`--force replaces a take directory, and ${outArg} holds other files; name a new directory`
|
|
16076
|
+
`--out ${outArg} exists; pass --keep-edits to join again and keep every edit made since (its trims, spans, layers, frame, sound), --force to replace it, or name a new directory`
|
|
14665
16077
|
);
|
|
16078
|
+
if (keepEdits) {
|
|
16079
|
+
if (!existsSync25(join27(out, "doc.json")))
|
|
16080
|
+
throw new UsageError(
|
|
16081
|
+
`--keep-edits: ${outArg} holds no doc.json, so there are no edits to keep; name a new directory, or --force to replace it`
|
|
16082
|
+
);
|
|
16083
|
+
rejoin = true;
|
|
16084
|
+
} else {
|
|
16085
|
+
const isTake = existsSync25(join27(out, "doc.json")) || existsSync25(join27(out, "meta.json"));
|
|
16086
|
+
if (!isTake && readdirSync5(out).length > 0)
|
|
16087
|
+
throw new UsageError(
|
|
16088
|
+
`--force replaces a take directory, and ${outArg} holds other files; name a new directory`
|
|
16089
|
+
);
|
|
16090
|
+
}
|
|
14666
16091
|
if (within(process.cwd(), out))
|
|
14667
16092
|
throw new UsageError(
|
|
14668
16093
|
`--out ${outArg} is the working directory or above it; name a new directory`
|
|
14669
16094
|
);
|
|
14670
16095
|
}
|
|
14671
16096
|
const parts = [];
|
|
16097
|
+
const read = [];
|
|
14672
16098
|
for (const [i, p] of positionals.entries()) {
|
|
14673
16099
|
r.log(`reading ${p}\u2026`);
|
|
14674
|
-
|
|
16100
|
+
const part = {
|
|
14675
16101
|
...await readPart(p),
|
|
14676
16102
|
...muted.has(i) ? { mute: true } : {}
|
|
16103
|
+
};
|
|
16104
|
+
parts.push(part);
|
|
16105
|
+
const drift = part.kind === "take" ? scriptDrift(part.doc.source.meta.steps ?? [], part.actions?.steps) : null;
|
|
16106
|
+
if (drift) r.warn(`${p}: ${scriptDriftLine(drift)}`);
|
|
16107
|
+
read.push({
|
|
16108
|
+
part,
|
|
16109
|
+
path: relative5(out, inputs[i]) || ".",
|
|
16110
|
+
footage: await partFootage(part)
|
|
14677
16111
|
});
|
|
14678
16112
|
}
|
|
14679
|
-
|
|
16113
|
+
if (keepEdits && !rejoin)
|
|
16114
|
+
r.log(
|
|
16115
|
+
`note: --keep-edits: ${outArg} does not exist yet, so this is a fresh join`
|
|
16116
|
+
);
|
|
16117
|
+
let plan;
|
|
16118
|
+
let rejoined = null;
|
|
16119
|
+
if (rejoin) {
|
|
16120
|
+
const prev = JSON.parse(
|
|
16121
|
+
await readFile17(join27(out, "doc.json"), "utf8")
|
|
16122
|
+
);
|
|
16123
|
+
let record = await readJoinRecord(out);
|
|
16124
|
+
if (!record) {
|
|
16125
|
+
record = await recordFromFootage(out, prev);
|
|
16126
|
+
r.log(
|
|
16127
|
+
`note: ${outArg} carries no ${JOIN_RECORD_NAME} (joined by an older vos), so its inputs are matched by the footage it holds; a video file joined then was re-encoded on the way in and reads as new`
|
|
16128
|
+
);
|
|
16129
|
+
}
|
|
16130
|
+
rejoined = rejoinDocs(migrateMotion3(prev), record, read, {
|
|
16131
|
+
where: (p) => relative5(process.cwd(), resolve17(out, p)) || "."
|
|
16132
|
+
});
|
|
16133
|
+
plan = rejoined.plan;
|
|
16134
|
+
} else plan = joinDocs(parts);
|
|
16135
|
+
const only = rejoined ? rejoined.newIds : void 0;
|
|
16136
|
+
const labelOf = new Map([
|
|
16137
|
+
["", positionals[0]],
|
|
16138
|
+
...plan.media.map((m) => [m.id, m.label])
|
|
16139
|
+
]);
|
|
14680
16140
|
let captions = null;
|
|
14681
16141
|
if (withCaptions) {
|
|
14682
|
-
captions = landStepCaptions(plan.doc, plan.actions);
|
|
14683
|
-
const labelOf = new Map([
|
|
14684
|
-
["", positionals[0]],
|
|
14685
|
-
...plan.media.map((m) => [m.id, m.label])
|
|
14686
|
-
]);
|
|
16142
|
+
captions = landStepCaptions(plan.doc, plan.actions, { only });
|
|
14687
16143
|
for (const k of captions.uncaptioned)
|
|
14688
16144
|
plan.notes.push(
|
|
14689
16145
|
`no step in ${labelOf.get(k) ?? `media ${k}`} carries a caption`
|
|
14690
16146
|
);
|
|
14691
16147
|
}
|
|
16148
|
+
let perBeat = null;
|
|
16149
|
+
if (zoomMode === "per-beat")
|
|
16150
|
+
perBeat = oneZoomPerBeat(plan.doc, plan.actions, { only });
|
|
14692
16151
|
const lint = lintDoc(plan.doc);
|
|
14693
16152
|
if (lint.problems.length) {
|
|
14694
16153
|
r.log(
|
|
@@ -14706,25 +16165,36 @@ async function cmdJoin(argv) {
|
|
|
14706
16165
|
);
|
|
14707
16166
|
c.from = found.file;
|
|
14708
16167
|
}
|
|
14709
|
-
if (
|
|
14710
|
-
|
|
16168
|
+
if (rejoined) {
|
|
16169
|
+
for (const k of rejoined.dropped) {
|
|
16170
|
+
if (!/^[A-Za-z0-9_-]+$/.test(k)) continue;
|
|
16171
|
+
const dir = join27(out, k);
|
|
16172
|
+
if (existsSync25(dir) && statSync7(dir).isDirectory())
|
|
16173
|
+
await rm4(dir, { recursive: true, force: true });
|
|
16174
|
+
}
|
|
16175
|
+
} else {
|
|
16176
|
+
if (existsSync25(out)) await rm4(out, { recursive: true, force: true });
|
|
16177
|
+
await mkdir11(out, { recursive: true });
|
|
16178
|
+
}
|
|
14711
16179
|
for (const c of plan.copies) {
|
|
14712
|
-
const to =
|
|
16180
|
+
const to = join27(out, c.to);
|
|
14713
16181
|
await mkdir11(dirname9(to), { recursive: true });
|
|
14714
16182
|
if (c.remux) {
|
|
14715
16183
|
const remuxed = await copyRecording(c.from, to, c.remux);
|
|
14716
16184
|
if (!remuxed && c.remux !== "mov")
|
|
14717
16185
|
plan.notes.push(
|
|
14718
|
-
`could not read ${
|
|
16186
|
+
`could not read ${basename13(c.from)}'s audio in Node, so it was copied as it is; its length comes from the container`
|
|
14719
16187
|
);
|
|
14720
16188
|
} else await copyFile3(c.from, to);
|
|
14721
16189
|
}
|
|
14722
16190
|
const doc = plan.doc;
|
|
14723
|
-
const added = new Set(
|
|
16191
|
+
const added = new Set(
|
|
16192
|
+
plan.media.filter((m) => !only || only.has(m.id)).map((m) => `${m.id}-sound`)
|
|
16193
|
+
);
|
|
14724
16194
|
for (const clip of doc.audio) {
|
|
14725
16195
|
if (!added.has(clip.id) || !isTakeRelativeKey(clip.key)) continue;
|
|
14726
16196
|
try {
|
|
14727
|
-
const m = await measureTrack(
|
|
16197
|
+
const m = await measureTrack(join27(out, takeRelativeFile(clip.key)));
|
|
14728
16198
|
if (m.lufs !== null)
|
|
14729
16199
|
clip.loudness = loudnessFact(
|
|
14730
16200
|
{ lufs: m.lufs, peakDb: m.peakDb },
|
|
@@ -14736,33 +16206,350 @@ async function cmdJoin(argv) {
|
|
|
14736
16206
|
);
|
|
14737
16207
|
}
|
|
14738
16208
|
}
|
|
14739
|
-
await writeJson(
|
|
16209
|
+
await writeJson(join27(out, "meta.json"), doc.source.meta, true);
|
|
14740
16210
|
if (doc.source.cursor.length)
|
|
14741
|
-
await writeJson(
|
|
16211
|
+
await writeJson(join27(out, "cursor.json"), doc.source.cursor);
|
|
14742
16212
|
if (parts[0].actions)
|
|
14743
|
-
await writeJson(
|
|
14744
|
-
await writeJson(
|
|
14745
|
-
|
|
16213
|
+
await writeJson(join27(out, "actions.json"), parts[0].actions, true);
|
|
16214
|
+
await writeJson(join27(out, "doc.json"), doc, true);
|
|
16215
|
+
await writeJson(
|
|
16216
|
+
join27(out, JOIN_RECORD_NAME),
|
|
16217
|
+
joinRecordOf(read, plan.ids),
|
|
16218
|
+
true
|
|
16219
|
+
);
|
|
16220
|
+
const extents = segmentOutputExtents4(doc);
|
|
14746
16221
|
const length = extents.length ? extents[extents.length - 1].end : 0;
|
|
14747
16222
|
for (const n of plan.notes) r.log(`note: ${n}`);
|
|
14748
16223
|
for (const w of lint.warnings) r.log(`warning: ${w}`);
|
|
14749
|
-
const
|
|
14750
|
-
|
|
16224
|
+
const stepCount = (id) => (doc.media ?? []).find((m) => m.id === id)?.meta.steps?.length ?? 0;
|
|
16225
|
+
const newMedia = only ? plan.media.filter((m) => only.has(m.id)) : [];
|
|
16226
|
+
const stepped = newMedia.filter((m) => stepCount(m.id) > 0);
|
|
16227
|
+
const bare = newMedia.filter((m) => stepCount(m.id) === 0);
|
|
16228
|
+
const bareWords = bare.length ? `${bare.map((m) => m.label).join(", ")} ${bare.length === 1 ? "has" : "have"} no recorded steps` : "";
|
|
16229
|
+
const onNew = only ? ` on ${stepped.map((m) => m.label).join(", ")}` : "";
|
|
16230
|
+
const captionLine = !captions ? "" : only && !newMedia.length ? "\n --captions: no new input to caption (a re-join captions only the inputs it adds; the kept ones keep their captions as edited)" : only && !stepped.length ? `
|
|
16231
|
+
--captions: ${bareWords}, so there is nothing to caption` : `
|
|
16232
|
+
${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} landed${onNew}` + (captions.skipped.length ? `; not landed: ${captions.skipped.join("; ")}` : "") + (bareWords ? `; ${bareWords}` : "");
|
|
16233
|
+
const beatLine = !perBeat ? "" : only && !newMedia.length ? "\n --zooms per-beat: no new input to re-plan (a re-join re-plans only the inputs it adds; the kept ones keep their zooms as edited)" : only && !stepped.length ? `
|
|
16234
|
+
--zooms per-beat: ${bareWords}, so there are no beats to plan` : `
|
|
16235
|
+
${only ? `${stepped.map((m) => m.label).join(", ")}: ` : ""}${perBeatLine(perBeat, (k) => labelOf.get(k) ?? `media ${k}`)}${bareWords ? `; ${bareWords}` : ""}`;
|
|
16236
|
+
const rejoinText = rejoined ? rejoinLines(rejoined.report).map((l) => `
|
|
16237
|
+
${l}`).join("") : "";
|
|
14751
16238
|
r.done(
|
|
14752
16239
|
{
|
|
14753
16240
|
out,
|
|
14754
16241
|
primary: positionals[0],
|
|
14755
16242
|
media: plan.media,
|
|
14756
16243
|
segments: doc.segments.length,
|
|
14757
|
-
seconds:
|
|
16244
|
+
seconds: round33(length),
|
|
14758
16245
|
files: plan.copies.map((c) => c.to),
|
|
14759
16246
|
notes: plan.notes,
|
|
14760
16247
|
warnings: lint.warnings,
|
|
14761
|
-
...captions ? { captions } : {}
|
|
16248
|
+
...captions ? { captions } : {},
|
|
16249
|
+
...perBeat ? { perBeat } : {},
|
|
16250
|
+
...rejoined ? { keptEdits: rejoined.report } : {}
|
|
14762
16251
|
},
|
|
14763
|
-
|
|
14764
|
-
${positionals[0]} is the primary; ${plan.media.map((m) => `${m.label} \u2192 ${m.id}`).join(", ")}${captionLine}
|
|
14765
|
-
Next: vos frames ${outArg} --times 0,25%,50%,75%,100%; vos render ${outArg}`
|
|
16252
|
+
`${rejoined ? "Joined again, keeping the edits:" : "Joined"} ${parts.length} inputs \u2192 ${out} (${length.toFixed(1)}s, ${doc.segments.length} segments)
|
|
16253
|
+
${positionals[0]} is the primary; ${plan.media.map((m) => `${m.label} \u2192 ${m.id}`).join(", ")}${rejoinText}${captionLine}${beatLine}
|
|
16254
|
+
Next: vos info ${outArg}; vos frames ${outArg} --times 0,25%,50%,75%,100%; vos render ${outArg}`
|
|
16255
|
+
);
|
|
16256
|
+
return EXIT_OK;
|
|
16257
|
+
}
|
|
16258
|
+
|
|
16259
|
+
// src/plugin/timeline.ts
|
|
16260
|
+
import { join as join28, resolve as resolve18 } from "path";
|
|
16261
|
+
import {
|
|
16262
|
+
docFreezes as docFreezes2,
|
|
16263
|
+
docMediaList as docMediaList4,
|
|
16264
|
+
docOutputDuration as docOutputDuration4,
|
|
16265
|
+
cameraSpanExtents as cameraSpanExtents3,
|
|
16266
|
+
docStillTime as docStillTime2,
|
|
16267
|
+
freezeOutputExtents,
|
|
16268
|
+
mediaKey as mediaKey5,
|
|
16269
|
+
migrateMotion as migrateMotion4,
|
|
16270
|
+
overlayPlainText,
|
|
16271
|
+
longestOutputExtent as longestOutputExtent3,
|
|
16272
|
+
ratedSegments as ratedSegments11,
|
|
16273
|
+
resolveOverlayAnchors as resolveOverlayAnchors2,
|
|
16274
|
+
segmentOutputExtents as segmentOutputExtents5,
|
|
16275
|
+
spanOutputExtents as spanOutputExtents3
|
|
16276
|
+
} from "@vosjs/studio-core";
|
|
16277
|
+
import { totalDuration as totalDuration4 } from "@vosjs/timeline";
|
|
16278
|
+
var r32 = (v) => Math.round(v * 1e3) / 1e3;
|
|
16279
|
+
var ext = (e) => e ? { start: r32(e.start), end: r32(e.end) } : null;
|
|
16280
|
+
function ratedWithoutFreezes(doc) {
|
|
16281
|
+
return ratedSegments11({
|
|
16282
|
+
...doc,
|
|
16283
|
+
freeze: void 0,
|
|
16284
|
+
segments: doc.segments.map((s) => {
|
|
16285
|
+
const { hold: _hold, ...rest } = s;
|
|
16286
|
+
void _hold;
|
|
16287
|
+
return rest;
|
|
16288
|
+
})
|
|
16289
|
+
});
|
|
16290
|
+
}
|
|
16291
|
+
function takeTimeline(rawDoc, actions = /* @__PURE__ */ new Map()) {
|
|
16292
|
+
const doc = migrateMotion4(rawDoc);
|
|
16293
|
+
const rated = ratedSegments11(doc);
|
|
16294
|
+
const round = (e) => ({
|
|
16295
|
+
start: r32(e.start),
|
|
16296
|
+
end: r32(e.end)
|
|
16297
|
+
});
|
|
16298
|
+
const span = (sIn, sOut, media2) => {
|
|
16299
|
+
const all = spanOutputExtents3(rated, sIn, sOut, media2).map(round);
|
|
16300
|
+
return {
|
|
16301
|
+
output: all[0] ?? null,
|
|
16302
|
+
...all.length > 1 ? { also: all.slice(1) } : {}
|
|
16303
|
+
};
|
|
16304
|
+
};
|
|
16305
|
+
const stepSpan = (sIn, sOut, media2) => {
|
|
16306
|
+
const all = spanOutputExtents3(rated, sIn, sOut, media2).map(round);
|
|
16307
|
+
const longest = longestOutputExtent3(rated, sIn, sOut, media2);
|
|
16308
|
+
const main = longest ? all.find(
|
|
16309
|
+
(e) => Math.abs(e.start - r32(longest.start)) < 1e-6 && Math.abs(e.end - r32(longest.end)) < 1e-6
|
|
16310
|
+
) : void 0;
|
|
16311
|
+
const others = all.filter((e) => e !== main);
|
|
16312
|
+
return {
|
|
16313
|
+
output: main ?? null,
|
|
16314
|
+
...others.length ? { also: others } : {}
|
|
16315
|
+
};
|
|
16316
|
+
};
|
|
16317
|
+
const cameraSpan = (sIn, sOut, media2) => {
|
|
16318
|
+
const moves = cameraSpanExtents3(rated, sIn, sOut, media2).map(round);
|
|
16319
|
+
const rests = spanOutputExtents3(rated, sIn, sOut, media2).map(round).filter(
|
|
16320
|
+
(e) => !moves.some(
|
|
16321
|
+
(m) => m.start >= e.start - 1e-6 && m.end <= e.end + 1e-6
|
|
16322
|
+
)
|
|
16323
|
+
);
|
|
16324
|
+
return {
|
|
16325
|
+
output: moves[0] ?? null,
|
|
16326
|
+
...moves.length > 1 ? { also: moves.slice(1) } : {},
|
|
16327
|
+
...rests.length ? { rests } : {}
|
|
16328
|
+
};
|
|
16329
|
+
};
|
|
16330
|
+
const media = docMediaList4(doc).map(({ id, source }) => ({
|
|
16331
|
+
id,
|
|
16332
|
+
file: source.videoKey,
|
|
16333
|
+
duration: r32((source.meta.durationMs || 0) / 1e3),
|
|
16334
|
+
steps: (source.meta.steps ?? []).map((s) => {
|
|
16335
|
+
const caption = captionOf(
|
|
16336
|
+
s,
|
|
16337
|
+
actions.get(id),
|
|
16338
|
+
source.script,
|
|
16339
|
+
source.meta.steps ?? []
|
|
16340
|
+
);
|
|
16341
|
+
const out = Math.max(s.tEnd, s.tStart + 1e-3);
|
|
16342
|
+
return {
|
|
16343
|
+
step: s.step,
|
|
16344
|
+
...s.id !== void 0 ? { id: s.id } : {},
|
|
16345
|
+
do: s.do,
|
|
16346
|
+
in: r32(s.tStart),
|
|
16347
|
+
out: r32(s.tEnd),
|
|
16348
|
+
...caption ? { caption } : {},
|
|
16349
|
+
...s.skipped ? { skipped: true } : {},
|
|
16350
|
+
...stepSpan(s.tStart, out, id || void 0)
|
|
16351
|
+
};
|
|
16352
|
+
})
|
|
16353
|
+
}));
|
|
16354
|
+
const docSegments = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
|
|
16355
|
+
const extents = segmentOutputExtents5(doc);
|
|
16356
|
+
const segments = docSegments.map((s, index) => ({
|
|
16357
|
+
index,
|
|
16358
|
+
media: mediaKey5(s.media),
|
|
16359
|
+
in: r32(s.in),
|
|
16360
|
+
out: r32(s.out),
|
|
16361
|
+
start: r32(extents[index]?.start ?? 0),
|
|
16362
|
+
end: r32(extents[index]?.end ?? 0)
|
|
16363
|
+
}));
|
|
16364
|
+
const freezeMarks = freezeOutputExtents(
|
|
16365
|
+
ratedWithoutFreezes(doc),
|
|
16366
|
+
docFreezes2(doc)
|
|
16367
|
+
);
|
|
16368
|
+
const freeze = docFreezes2(doc).map((f) => {
|
|
16369
|
+
const m = freezeMarks.get(f.id);
|
|
16370
|
+
return {
|
|
16371
|
+
id: f.id,
|
|
16372
|
+
media: mediaKey5(f.media),
|
|
16373
|
+
at: r32(f.at),
|
|
16374
|
+
seconds: r32(f.seconds),
|
|
16375
|
+
output: m ? ext({ start: m.t, end: m.t + m.duration }) : null
|
|
16376
|
+
};
|
|
16377
|
+
});
|
|
16378
|
+
const resolved = resolveOverlayAnchors2(doc, rated);
|
|
16379
|
+
const rawById = new Map((doc.overlays ?? []).map((o) => [o.id, o]));
|
|
16380
|
+
const overlays = (resolved.overlays ?? []).flatMap((o) => {
|
|
16381
|
+
const start = o.start;
|
|
16382
|
+
if (start === void 0) return [];
|
|
16383
|
+
const raw = rawById.get(o.id);
|
|
16384
|
+
const text = o.kind === "text" ? overlayPlainText(o) : o.kind === "html" ? void 0 : o.key;
|
|
16385
|
+
return [
|
|
16386
|
+
{
|
|
16387
|
+
id: o.id,
|
|
16388
|
+
kind: o.kind ?? "text",
|
|
16389
|
+
...text !== void 0 ? { text } : {},
|
|
16390
|
+
start: r32(start),
|
|
16391
|
+
end: r32(start + o.duration),
|
|
16392
|
+
...raw?.anchor ? { anchor: raw.anchor } : {},
|
|
16393
|
+
...o.from ? { from: o.from } : {}
|
|
16394
|
+
}
|
|
16395
|
+
];
|
|
16396
|
+
});
|
|
16397
|
+
const footage = totalDuration4(rated);
|
|
16398
|
+
return {
|
|
16399
|
+
media,
|
|
16400
|
+
segments,
|
|
16401
|
+
speed: (doc.speed ?? []).map((s) => ({
|
|
16402
|
+
id: s.id,
|
|
16403
|
+
media: mediaKey5(s.media),
|
|
16404
|
+
in: r32(s.in),
|
|
16405
|
+
out: r32(s.out),
|
|
16406
|
+
rate: s.rate,
|
|
16407
|
+
...s.source ? { source: s.source } : {},
|
|
16408
|
+
...span(s.in, s.out, s.media)
|
|
16409
|
+
})),
|
|
16410
|
+
freeze,
|
|
16411
|
+
zoom: doc.zoom.map((z) => ({
|
|
16412
|
+
id: z.id,
|
|
16413
|
+
media: mediaKey5(z.media),
|
|
16414
|
+
in: r32(z.in),
|
|
16415
|
+
out: r32(z.out),
|
|
16416
|
+
level: z.level,
|
|
16417
|
+
...z.source ? { source: z.source } : {},
|
|
16418
|
+
...cameraSpan(z.in, z.out, z.media)
|
|
16419
|
+
})),
|
|
16420
|
+
tilt: (doc.tilt ?? []).map((t) => ({
|
|
16421
|
+
id: t.id,
|
|
16422
|
+
media: mediaKey5(t.media),
|
|
16423
|
+
in: r32(t.in),
|
|
16424
|
+
out: r32(t.out),
|
|
16425
|
+
...cameraSpan(t.in, t.out, t.media)
|
|
16426
|
+
})),
|
|
16427
|
+
overlays,
|
|
16428
|
+
objects: (doc.objects ?? []).map((ob) => ({
|
|
16429
|
+
id: ob.id,
|
|
16430
|
+
start: r32(ob.span?.start ?? 0),
|
|
16431
|
+
end: ob.span ? r32(ob.span.start + ob.span.duration) : null
|
|
16432
|
+
})),
|
|
16433
|
+
audio: doc.audio.map((a) => {
|
|
16434
|
+
const len = a.loop && a.loopLen ? a.loopLen : a.out - a.in;
|
|
16435
|
+
return {
|
|
16436
|
+
id: a.id,
|
|
16437
|
+
...a.name ? { name: a.name } : {},
|
|
16438
|
+
start: r32(a.start),
|
|
16439
|
+
end: r32(a.start + len),
|
|
16440
|
+
in: r32(a.in),
|
|
16441
|
+
out: r32(a.out)
|
|
16442
|
+
};
|
|
16443
|
+
}),
|
|
16444
|
+
footage: r32(footage),
|
|
16445
|
+
end: r32(docOutputDuration4(doc)),
|
|
16446
|
+
still: docStillTime2(doc)
|
|
16447
|
+
};
|
|
16448
|
+
}
|
|
16449
|
+
var f2 = (v) => v.toFixed(2);
|
|
16450
|
+
var range = (a, b) => `${f2(a)}\u2013${f2(b)}`;
|
|
16451
|
+
var ranges = (list) => list.map((a) => range(a.start, a.end)).join(", ");
|
|
16452
|
+
var outText = (e, also, rests) => `${e ? range(e.start, e.end) : rests?.length ? "(camera at rest)" : "(cut)"}${also?.length ? `, also ${ranges(also)}` : ""}${rests?.length ? `${e ? "," : ""} at rest in ${ranges(rests)}` : ""}`;
|
|
16453
|
+
var mediaName = (m) => m || "primary";
|
|
16454
|
+
function formatTimeline(t, label) {
|
|
16455
|
+
const lines = [];
|
|
16456
|
+
const pad = (s, n) => s.padEnd(n);
|
|
16457
|
+
lines.push(
|
|
16458
|
+
`${label}: output ${f2(t.end)} s (footage ${f2(t.footage)} s), ${t.media.length} media`
|
|
16459
|
+
);
|
|
16460
|
+
for (const m of t.media) {
|
|
16461
|
+
lines.push("");
|
|
16462
|
+
lines.push(
|
|
16463
|
+
`${mediaName(m.id)} ${m.file} ${f2(m.duration)} s source, ${m.steps.length} step${m.steps.length === 1 ? "" : "s"}`
|
|
16464
|
+
);
|
|
16465
|
+
if (m.steps.length)
|
|
16466
|
+
lines.push(
|
|
16467
|
+
` ${pad("step", 5)} ${pad("id", 16)} ${pad("do", 8)} ${pad("source", 14)} ${pad("output", 14)} caption`
|
|
16468
|
+
);
|
|
16469
|
+
for (const s of m.steps)
|
|
16470
|
+
lines.push(
|
|
16471
|
+
` ${pad(String(s.step), 5)} ${pad(s.id ?? "-", 16)} ${pad(s.do, 8)} ${pad(range(s.in, s.out), 14)} ${pad(s.skipped ? "(skipped)" : outText(s.output), 14)} ${s.caption ? JSON.stringify(s.caption) : ""}${s.also?.length && !s.skipped ? ` (also ${ranges(s.also)})` : ""}`.trimEnd()
|
|
16472
|
+
);
|
|
16473
|
+
}
|
|
16474
|
+
lines.push("");
|
|
16475
|
+
lines.push("output timeline (source \u2192 output seconds)");
|
|
16476
|
+
lines.push(" segments");
|
|
16477
|
+
for (const s of t.segments)
|
|
16478
|
+
lines.push(
|
|
16479
|
+
` #${pad(String(s.index), 4)} ${pad(mediaName(s.media), 8)} ${pad(range(s.in, s.out), 14)} \u2192 ${range(s.start, s.end)}`
|
|
16480
|
+
);
|
|
16481
|
+
if (t.speed.length) {
|
|
16482
|
+
lines.push(" speed");
|
|
16483
|
+
for (const s of t.speed)
|
|
16484
|
+
lines.push(
|
|
16485
|
+
` ${pad(s.id, 12)} ${pad(mediaName(s.media), 8)} ${pad(range(s.in, s.out), 14)} \xD7${pad(String(s.rate), 5)} ${pad(s.source ?? "", 6)} \u2192 ${outText(s.output, s.also)}`
|
|
16486
|
+
);
|
|
16487
|
+
}
|
|
16488
|
+
if (t.freeze.length) {
|
|
16489
|
+
lines.push(" freeze");
|
|
16490
|
+
for (const f of t.freeze)
|
|
16491
|
+
lines.push(
|
|
16492
|
+
` ${pad(f.id, 12)} ${pad(mediaName(f.media), 8)} at ${pad(f2(f.at), 11)} ${f2(f.seconds)} s \u2192 ${outText(f.output)}`
|
|
16493
|
+
);
|
|
16494
|
+
}
|
|
16495
|
+
if (t.zoom.length) {
|
|
16496
|
+
lines.push(" zoom");
|
|
16497
|
+
for (const z of t.zoom)
|
|
16498
|
+
lines.push(
|
|
16499
|
+
` ${pad(z.id, 12)} ${pad(mediaName(z.media), 8)} ${pad(range(z.in, z.out), 14)} L${pad(String(z.level), 5)} ${pad(z.source ?? "", 6)} \u2192 ${outText(z.output, z.also, z.rests)}`
|
|
16500
|
+
);
|
|
16501
|
+
}
|
|
16502
|
+
if (t.tilt.length) {
|
|
16503
|
+
lines.push(" tilt");
|
|
16504
|
+
for (const z of t.tilt)
|
|
16505
|
+
lines.push(
|
|
16506
|
+
` ${pad(z.id, 12)} ${pad(mediaName(z.media), 8)} ${pad(range(z.in, z.out), 14)} \u2192 ${outText(z.output, z.also, z.rests)}`
|
|
16507
|
+
);
|
|
16508
|
+
}
|
|
16509
|
+
if (t.overlays.length) {
|
|
16510
|
+
lines.push(" layers");
|
|
16511
|
+
for (const o of t.overlays) {
|
|
16512
|
+
const anchor = o.anchor ? ` anchor ${o.anchor.media ? `${o.anchor.media}:` : ""}${String(o.anchor.step)}${o.anchor.at === "end" ? " end" : ""}${o.anchor.offset ? ` ${o.anchor.offset > 0 ? "+" : ""}${o.anchor.offset}` : ""}` : "";
|
|
16513
|
+
const words2 = o.text !== void 0 ? ` ${JSON.stringify(o.text)}` : "";
|
|
16514
|
+
lines.push(
|
|
16515
|
+
` ${pad(o.id, 12)} ${pad(o.kind, 6)} ${range(o.start, o.end)}${anchor}${o.from ? ` (from ${o.from})` : ""}${words2}`
|
|
16516
|
+
);
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
16519
|
+
if (t.objects.length) {
|
|
16520
|
+
lines.push(" 3D");
|
|
16521
|
+
for (const o of t.objects)
|
|
16522
|
+
lines.push(
|
|
16523
|
+
` ${pad(o.id, 12)} ${o.end === null ? "always" : range(o.start, o.end)}`
|
|
16524
|
+
);
|
|
16525
|
+
}
|
|
16526
|
+
if (t.audio.length) {
|
|
16527
|
+
lines.push(" sound");
|
|
16528
|
+
for (const a of t.audio)
|
|
16529
|
+
lines.push(
|
|
16530
|
+
` ${pad(a.id, 12)} ${range(a.start, a.end)} (its file ${range(a.in, a.out)})${a.name ? ` ${a.name}` : ""}`
|
|
16531
|
+
);
|
|
16532
|
+
}
|
|
16533
|
+
lines.push(` still ${f2(t.still)}`);
|
|
16534
|
+
lines.push(` end ${f2(t.end)}`);
|
|
16535
|
+
return lines.join("\n");
|
|
16536
|
+
}
|
|
16537
|
+
async function cmdTakeInfo(argv) {
|
|
16538
|
+
const { positionals, flags } = parseArgs(argv, /* @__PURE__ */ new Set(["json", "help"]));
|
|
16539
|
+
const dir = positionals[0];
|
|
16540
|
+
if (!dir) throw new UsageError("vos info <take> [--json]");
|
|
16541
|
+
const r = createReporter(flags.json === true);
|
|
16542
|
+
const take = await loadTake(dir);
|
|
16543
|
+
if (!take.doc)
|
|
16544
|
+
throw new UsageError(
|
|
16545
|
+
`${dir} has no doc.json yet; vos plan ${dir} writes one`
|
|
16546
|
+
);
|
|
16547
|
+
const drift = scriptDrift(take.meta.steps ?? [], take.actions?.steps);
|
|
16548
|
+
if (drift) r.warn(scriptDriftLine(drift));
|
|
16549
|
+
const timeline = takeTimeline(take.doc, /* @__PURE__ */ new Map([["", take.actions]]));
|
|
16550
|
+
r.done(
|
|
16551
|
+
{ take: resolve18(dir), doc: join28(resolve18(dir), "doc.json"), ...timeline },
|
|
16552
|
+
formatTimeline(timeline, dir)
|
|
14766
16553
|
);
|
|
14767
16554
|
return EXIT_OK;
|
|
14768
16555
|
}
|
|
@@ -14770,7 +16557,7 @@ async function cmdJoin(argv) {
|
|
|
14770
16557
|
// src/plugin/audio.ts
|
|
14771
16558
|
import { mkdtemp as mkdtemp2, rm as rm5, writeFile as writeFile12 } from "fs/promises";
|
|
14772
16559
|
import { tmpdir as tmpdir3 } from "os";
|
|
14773
|
-
import { basename as
|
|
16560
|
+
import { basename as basename14, join as join29 } from "path";
|
|
14774
16561
|
import { clipGain as clipGain3 } from "@vosjs/studio-core";
|
|
14775
16562
|
var USAGE4 = "vos audio measure <take> [--json]";
|
|
14776
16563
|
async function fileFor(dir, key, fetchImpl) {
|
|
@@ -14782,8 +16569,8 @@ async function fileFor(dir, key, fetchImpl) {
|
|
|
14782
16569
|
if (/^https?:\/\//.test(key)) {
|
|
14783
16570
|
const res = await fetchImpl(key);
|
|
14784
16571
|
if (!res.ok) return { skipped: `GET ${key} answered ${res.status}` };
|
|
14785
|
-
const tmp = await mkdtemp2(
|
|
14786
|
-
const path =
|
|
16572
|
+
const tmp = await mkdtemp2(join29(tmpdir3(), "vos-audio-"));
|
|
16573
|
+
const path = join29(tmp, basename14(new URL(key).pathname) || "audio");
|
|
14787
16574
|
await writeFile12(path, new Uint8Array(await res.arrayBuffer()));
|
|
14788
16575
|
return { path, cleanup: () => rm5(tmp, { recursive: true, force: true }) };
|
|
14789
16576
|
}
|
|
@@ -14865,8 +16652,8 @@ async function cmdAudio(argv) {
|
|
|
14865
16652
|
|
|
14866
16653
|
// src/plugin/look.ts
|
|
14867
16654
|
import { mkdir as mkdir12 } from "fs/promises";
|
|
14868
|
-
import { existsSync as
|
|
14869
|
-
import { join as
|
|
16655
|
+
import { existsSync as existsSync26 } from "fs";
|
|
16656
|
+
import { join as join30, resolve as resolve19 } from "path";
|
|
14870
16657
|
import { headersFor, urlSchemeProblem } from "@vosjs/render-core/record";
|
|
14871
16658
|
function quote(value) {
|
|
14872
16659
|
return `"${value.replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
@@ -15318,7 +17105,7 @@ async function lookAt(page, url, opts) {
|
|
|
15318
17105
|
}
|
|
15319
17106
|
if (opts.waitMs && opts.waitMs > 0) await page.waitForTimeout(opts.waitMs);
|
|
15320
17107
|
await mkdir12(opts.outDir, { recursive: true });
|
|
15321
|
-
const screenshot =
|
|
17108
|
+
const screenshot = join30(opts.outDir, "look.png");
|
|
15322
17109
|
await page.screenshot({ path: screenshot });
|
|
15323
17110
|
const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
|
|
15324
17111
|
const step = ran.length ? ran.length - 1 : -1;
|
|
@@ -15408,15 +17195,15 @@ async function cmdLook(argv) {
|
|
|
15408
17195
|
if (scheme) throw new UsageError(scheme);
|
|
15409
17196
|
const r = createReporter(flags.json === true);
|
|
15410
17197
|
const viewport = parseViewport2(strFlag(flags, "viewport"));
|
|
15411
|
-
const outDir =
|
|
17198
|
+
const outDir = resolve19(strFlag(flags, "out") ?? "look");
|
|
15412
17199
|
const headed = flags.headed === true;
|
|
15413
17200
|
const waitRaw = strFlag(flags, "wait");
|
|
15414
17201
|
const waitMs = waitRaw === void 0 ? 0 : Number(waitRaw);
|
|
15415
17202
|
if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 6e4)
|
|
15416
17203
|
throw new UsageError("--wait expects milliseconds, 0 to 60000");
|
|
15417
17204
|
const storageRaw = strFlag(flags, "storage-state");
|
|
15418
|
-
const storageState = storageRaw ?
|
|
15419
|
-
if (storageState && !
|
|
17205
|
+
const storageState = storageRaw ? resolve19(storageRaw) : void 0;
|
|
17206
|
+
if (storageState && !existsSync26(storageState))
|
|
15420
17207
|
throw new UsageError(`--storage-state: no such file: ${storageState}`);
|
|
15421
17208
|
const sessionName = strFlag(flags, "session");
|
|
15422
17209
|
if (sessionName) {
|
|
@@ -15426,7 +17213,7 @@ async function cmdLook(argv) {
|
|
|
15426
17213
|
throw new UsageError(
|
|
15427
17214
|
"--session and --storage-state are two doors to one page: a persistent profile cannot take a storage state. Pass one"
|
|
15428
17215
|
);
|
|
15429
|
-
if (!
|
|
17216
|
+
if (!existsSync26(sessionDir(sessionName)))
|
|
15430
17217
|
throw new UsageError(
|
|
15431
17218
|
`no session "${sessionName}". Make one: vos session open <url> --name ${sessionName} (a Chrome window opens; sign in and close it)`
|
|
15432
17219
|
);
|
|
@@ -15489,19 +17276,19 @@ async function cmdLook(argv) {
|
|
|
15489
17276
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
15490
17277
|
import {
|
|
15491
17278
|
cpSync,
|
|
15492
|
-
existsSync as
|
|
17279
|
+
existsSync as existsSync28,
|
|
15493
17280
|
mkdirSync as mkdirSync6,
|
|
15494
17281
|
readFileSync as readFileSync11,
|
|
15495
17282
|
rmSync as rmSync3,
|
|
15496
17283
|
writeFileSync as writeFileSync6
|
|
15497
17284
|
} from "fs";
|
|
15498
17285
|
import { homedir as homedir2 } from "os";
|
|
15499
|
-
import { join as
|
|
17286
|
+
import { join as join32 } from "path";
|
|
15500
17287
|
import { chromium as chromium2 } from "playwright";
|
|
15501
17288
|
|
|
15502
17289
|
// src/plugin/machine.ts
|
|
15503
|
-
import { existsSync as
|
|
15504
|
-
import { dirname as dirname10, join as
|
|
17290
|
+
import { existsSync as existsSync27, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
17291
|
+
import { dirname as dirname10, join as join31 } from "path";
|
|
15505
17292
|
var AGENT_NAMES = [
|
|
15506
17293
|
"claude",
|
|
15507
17294
|
"cursor",
|
|
@@ -15521,17 +17308,17 @@ var HOME_DIRS = {
|
|
|
15521
17308
|
copilot: [".copilot"]
|
|
15522
17309
|
};
|
|
15523
17310
|
function detectAgents(root, opts = {}) {
|
|
15524
|
-
const exists = opts.exists ??
|
|
17311
|
+
const exists = opts.exists ?? existsSync27;
|
|
15525
17312
|
const out = [];
|
|
15526
17313
|
for (const agent of AGENT_NAMES) {
|
|
15527
17314
|
for (const rel of PROJECT_DIRS[agent]) {
|
|
15528
|
-
const dir =
|
|
17315
|
+
const dir = join31(root, rel);
|
|
15529
17316
|
if (exists(dir)) out.push({ agent, dir, scope: "project" });
|
|
15530
17317
|
}
|
|
15531
|
-
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(
|
|
17318
|
+
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join31(root, ".github", "copilot-instructions.md"))) {
|
|
15532
17319
|
out.push({
|
|
15533
17320
|
agent,
|
|
15534
|
-
dir:
|
|
17321
|
+
dir: join31(root, ".github", "copilot"),
|
|
15535
17322
|
scope: "project"
|
|
15536
17323
|
});
|
|
15537
17324
|
}
|
|
@@ -15539,7 +17326,7 @@ function detectAgents(root, opts = {}) {
|
|
|
15539
17326
|
if (opts.global && opts.home) {
|
|
15540
17327
|
for (const agent of AGENT_NAMES) {
|
|
15541
17328
|
for (const rel of HOME_DIRS[agent]) {
|
|
15542
|
-
const dir =
|
|
17329
|
+
const dir = join31(opts.home, rel);
|
|
15543
17330
|
if (exists(dir)) out.push({ agent, dir, scope: "global" });
|
|
15544
17331
|
}
|
|
15545
17332
|
}
|
|
@@ -15557,18 +17344,18 @@ function agentsFromFlag(value, root) {
|
|
|
15557
17344
|
return {
|
|
15558
17345
|
homes: wanted.map((agent) => ({
|
|
15559
17346
|
agent,
|
|
15560
|
-
dir:
|
|
17347
|
+
dir: join31(root, PROJECT_DIRS[agent][0]),
|
|
15561
17348
|
scope: "project"
|
|
15562
17349
|
}))
|
|
15563
17350
|
};
|
|
15564
17351
|
}
|
|
15565
17352
|
function skillsDirFor(home) {
|
|
15566
|
-
return
|
|
17353
|
+
return join31(home.dir, "skills");
|
|
15567
17354
|
}
|
|
15568
17355
|
function fallbackSkillsHome(root) {
|
|
15569
|
-
return { agent: "codex", dir:
|
|
17356
|
+
return { agent: "codex", dir: join31(root, ".agents"), scope: "project" };
|
|
15570
17357
|
}
|
|
15571
|
-
function installedSkills(dir, exists =
|
|
17358
|
+
function installedSkills(dir, exists = existsSync27, list = (p) => {
|
|
15572
17359
|
try {
|
|
15573
17360
|
return readdirSync6(p);
|
|
15574
17361
|
} catch {
|
|
@@ -15576,7 +17363,7 @@ function installedSkills(dir, exists = existsSync26, list = (p) => {
|
|
|
15576
17363
|
}
|
|
15577
17364
|
}) {
|
|
15578
17365
|
if (!exists(dir)) return [];
|
|
15579
|
-
return list(dir).filter((name) => exists(
|
|
17366
|
+
return list(dir).filter((name) => exists(join31(dir, name, "SKILL.md"))).sort();
|
|
15580
17367
|
}
|
|
15581
17368
|
var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
|
|
15582
17369
|
var RULES_END = "<!-- vos:end -->";
|
|
@@ -15609,10 +17396,10 @@ ${RULES_BLOCK}
|
|
|
15609
17396
|
}
|
|
15610
17397
|
return { text, changed: text !== current };
|
|
15611
17398
|
}
|
|
15612
|
-
function rulesFileFor(root, exists =
|
|
15613
|
-
const agents =
|
|
17399
|
+
function rulesFileFor(root, exists = existsSync27) {
|
|
17400
|
+
const agents = join31(root, "AGENTS.md");
|
|
15614
17401
|
if (exists(agents)) return agents;
|
|
15615
|
-
const claude =
|
|
17402
|
+
const claude = join31(root, "CLAUDE.md");
|
|
15616
17403
|
if (exists(claude)) return claude;
|
|
15617
17404
|
return agents;
|
|
15618
17405
|
}
|
|
@@ -15621,14 +17408,14 @@ function chromeCandidates(platform, env) {
|
|
|
15621
17408
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
15622
17409
|
if (platform === "win32")
|
|
15623
17410
|
return [
|
|
15624
|
-
|
|
17411
|
+
join31(
|
|
15625
17412
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
15626
17413
|
"Google",
|
|
15627
17414
|
"Chrome",
|
|
15628
17415
|
"Application",
|
|
15629
17416
|
"chrome.exe"
|
|
15630
17417
|
),
|
|
15631
|
-
|
|
17418
|
+
join31(
|
|
15632
17419
|
env["LOCALAPPDATA"] ?? "",
|
|
15633
17420
|
"Google",
|
|
15634
17421
|
"Chrome",
|
|
@@ -15714,7 +17501,7 @@ function doctorReport(facts) {
|
|
|
15714
17501
|
}
|
|
15715
17502
|
function bundledSkillsVersion(dir) {
|
|
15716
17503
|
try {
|
|
15717
|
-
const v = readFileSync10(
|
|
17504
|
+
const v = readFileSync10(join31(dir, "VERSION"), "utf8").trim();
|
|
15718
17505
|
return /^\d+\.\d+\.\d+$/.test(v) ? v : null;
|
|
15719
17506
|
} catch {
|
|
15720
17507
|
return null;
|
|
@@ -15723,13 +17510,13 @@ function bundledSkillsVersion(dir) {
|
|
|
15723
17510
|
function bundledSkillsDir(from) {
|
|
15724
17511
|
let dir = dirname10(from);
|
|
15725
17512
|
for (let i = 0; i < 6; i++) {
|
|
15726
|
-
const pkg =
|
|
15727
|
-
if (
|
|
17513
|
+
const pkg = join31(dir, "package.json");
|
|
17514
|
+
if (existsSync27(pkg)) {
|
|
15728
17515
|
try {
|
|
15729
17516
|
const name = JSON.parse(readFileSync10(pkg, "utf8")).name;
|
|
15730
17517
|
if (name === "@vosjs/cli") {
|
|
15731
|
-
const skills =
|
|
15732
|
-
return
|
|
17518
|
+
const skills = join31(dir, "skills");
|
|
17519
|
+
return existsSync27(skills) && statSync8(skills).isDirectory() ? skills : null;
|
|
15733
17520
|
}
|
|
15734
17521
|
} catch {
|
|
15735
17522
|
}
|
|
@@ -15765,7 +17552,7 @@ function npx() {
|
|
|
15765
17552
|
function bundledChromium() {
|
|
15766
17553
|
try {
|
|
15767
17554
|
const p = chromium2.executablePath();
|
|
15768
|
-
return p &&
|
|
17555
|
+
return p && existsSync28(p) ? p : null;
|
|
15769
17556
|
} catch {
|
|
15770
17557
|
return null;
|
|
15771
17558
|
}
|
|
@@ -15774,7 +17561,7 @@ function probe2() {
|
|
|
15774
17561
|
return probeBrowser({
|
|
15775
17562
|
platform: process.platform,
|
|
15776
17563
|
env: process.env,
|
|
15777
|
-
exists:
|
|
17564
|
+
exists: existsSync28,
|
|
15778
17565
|
bundled: bundledChromium
|
|
15779
17566
|
});
|
|
15780
17567
|
}
|
|
@@ -15859,7 +17646,7 @@ async function cmdSetup(argv) {
|
|
|
15859
17646
|
const dest = skillsDirFor(home);
|
|
15860
17647
|
mkdirSync6(dest, { recursive: true });
|
|
15861
17648
|
for (const name of installedSkills(dir)) {
|
|
15862
|
-
cpSync(
|
|
17649
|
+
cpSync(join32(dir, name), join32(dest, name), { recursive: true });
|
|
15863
17650
|
}
|
|
15864
17651
|
}
|
|
15865
17652
|
};
|
|
@@ -15927,7 +17714,7 @@ async function cmdSetup(argv) {
|
|
|
15927
17714
|
let rules = null;
|
|
15928
17715
|
if (!noRules) {
|
|
15929
17716
|
const file = rulesFileFor(root);
|
|
15930
|
-
const existing =
|
|
17717
|
+
const existing = existsSync28(file) ? readFileSync11(file, "utf8") : null;
|
|
15931
17718
|
const { text, changed } = applyRulesBlock(existing);
|
|
15932
17719
|
if (changed) writeFileSync6(file, text);
|
|
15933
17720
|
rules = { file, changed };
|
|
@@ -16041,7 +17828,7 @@ async function cmdWhoami(argv) {
|
|
|
16041
17828
|
function cmdLogout(argv) {
|
|
16042
17829
|
const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
|
|
16043
17830
|
const r = reporterFor(flags);
|
|
16044
|
-
const had =
|
|
17831
|
+
const had = existsSync28(CREDENTIALS_PATH);
|
|
16045
17832
|
rmSync3(CREDENTIALS_PATH, { force: true });
|
|
16046
17833
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
16047
17834
|
r.done(
|
|
@@ -16099,7 +17886,8 @@ Take pipeline
|
|
|
16099
17886
|
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--ready dom|networkidle] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--verbose] [--json]
|
|
16100
17887
|
vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
|
|
16101
17888
|
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
|
|
16102
|
-
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]
|
|
17889
|
+
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] [--zooms per-beat] [--json]
|
|
17890
|
+
--zooms per-beat keeps at most one zoom per beat: the beats are the captioned steps of actions.json (each runs until the next one starts, plus the opening before the first); a step marked "zoom": true in actions.json is its beat's payoff, and the beat keeps the proposal that overlaps it most (one per marked step, never two overlapping), promoted to manual; a beat with no marked step keeps no zoom; when no step of the take is marked, each beat's last click, drag, type or paste stands in (the opening keeps none) and the done line says so; a step marked "zoom": false is never zoomed on; the rest go into doc.rejected like a person deleting them, and a beat with a manual zoom keeps only its manual ones. A media with no captioned step keeps its zooms as planned, and the done line says so
|
|
16103
17891
|
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]
|
|
16104
17892
|
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]
|
|
16105
17893
|
--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)
|
|
@@ -16107,6 +17895,8 @@ Take pipeline
|
|
|
16107
17895
|
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
|
|
16108
17896
|
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]
|
|
16109
17897
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
17898
|
+
vos info <take> [--json]
|
|
17899
|
+
the take's two clocks, read-only: per media, every step with its id, what it did, its source seconds (meta.json tStart..tEnd), its caption and where it plays; then the output timeline after trims, speed and freezes: each segment, speed span, freeze, zoom and tilt span (or "(cut)"), every layer with its anchor resolved, each sound clip, the still and the film's end
|
|
16110
17900
|
vos brand <url> [--out BRAND.md] [--json]
|
|
16111
17901
|
vos open <take> [--studio <url>] [--print]
|
|
16112
17902
|
vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex (the mark and leader; the kicker is --accent)] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px <n> (the body size on the delivered frame)] [--id x] [--print] [--json]
|
|
@@ -16114,8 +17904,9 @@ Take pipeline
|
|
|
16114
17904
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
16115
17905
|
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]
|
|
16116
17906
|
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
|
|
16117
|
-
vos join <take> <take|file.mp4>... --out <dir> [--force] [--captions] [--mute <file>]... [--json]
|
|
16118
|
-
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
|
|
17907
|
+
vos join <take> <take|file.mp4>... --out <dir> [--keep-edits|--force] [--captions] [--zooms per-beat] [--mute <file>]... [--json]
|
|
17908
|
+
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, starting as the step begins (anchor.at "start"), each ending with its clip. --zooms per-beat keeps a beat's zoom on its steps marked "zoom": true (as vos plan --zooms per-beat). The result must pass vos validate before anything is written
|
|
17909
|
+
--keep-edits joins again over an edited joined take and keeps its edits: inputs are matched to their media by source file (join.json records each input's path and footage hash), a kept input keeps its id and everything on it (segments in their edited order, spans, rejections, anchored layers, its sound), the frame, export, styles and still stay, a new input is joined fresh under a new id (and only it is captioned or re-planned), a re-recorded input (changed footage at its path, or the same script at a new one) keeps its edits re-timed by its steps as vos plan --reuse does, and an input that is gone takes its edits with it, each named. A new input's segments go before the next kept input's; reordering the kept inputs regroups the segments in argument order. The first input must stay the primary. --force replaces the take instead
|
|
16119
17910
|
vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--normalize <lufs|off>] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]
|
|
16120
17911
|
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. The clip carries its measured loudness and a target, --normalize (default -16 LUFS), with --gain (default 0.5) a trim on it; --normalize off plays it at --gain alone
|
|
16121
17912
|
vos audio measure <take> [--json]
|
|
@@ -16162,7 +17953,8 @@ Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
|
|
|
16162
17953
|
signed-in person's line carries for a key, once, no click
|
|
16163
17954
|
vos fetch <vosId|watch-url> [--out dir] [--media] [--json]
|
|
16164
17955
|
writes config.json + vos.json (no auth needed for public programs);
|
|
16165
|
-
a take vos also gets its doc.json, and --media its footage
|
|
17956
|
+
a take vos also gets its doc.json, and --media its footage, its
|
|
17957
|
+
actions.json and every file that script pastes, beside it
|
|
16166
17958
|
vos push <config.json|take> [--vos id] [--title t] [--slug s] [--remix-of id]
|
|
16167
17959
|
[--desc d] [--tags a,b] [--folder <folderId|slug>]
|
|
16168
17960
|
[--note n] [--label l] [--base versionId] [--override <id>]... [--yes] [--json]
|
|
@@ -16184,7 +17976,9 @@ Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
|
|
|
16184
17976
|
Without --folder a new vos lands unfiled, at the root of your
|
|
16185
17977
|
shelf. A document's local files (doc.json: picture and video
|
|
16186
17978
|
overlays, 3D props, sound; a take's mic and cam too) upload
|
|
16187
|
-
with the push, each once
|
|
17979
|
+
with the push, each once, and so does every file a take's
|
|
17980
|
+
actions.json pastes (a picture through the upload door, a text
|
|
17981
|
+
file inside the document; one outside the take is refused). --wait stays until the version's still has
|
|
16188
17982
|
rendered and prints absolute still and preview links.
|
|
16189
17983
|
--still <seconds> sets a PROGRAM's cover (doc.json "still", kept
|
|
16190
17984
|
by every later push); without it the platform picks an early
|
|
@@ -16298,9 +18092,11 @@ networkidle was asked for and never came; neither is a strict failure. --max-dur
|
|
|
16298
18092
|
the hosted recording cap, read live from GET /api/limits (30 min when the
|
|
16299
18093
|
origin is unreachable): the capture stops there and says so in one line.
|
|
16300
18094
|
Three steps beyond the gestures: expect checks the page on camera
|
|
16301
|
-
(selector, text or gone; enabled;
|
|
16302
|
-
|
|
16303
|
-
|
|
18095
|
+
(selector, text or gone; enabled; or js, an expression evaluated in the
|
|
18096
|
+
page, truthy or with equals a value, for state an app keeps in script,
|
|
18097
|
+
said with the value it got and kept on meta.steps[].value; timeout 5 s);
|
|
18098
|
+
await waits OFF camera (until {selector|text|gone|js}, timeout 600 s,
|
|
18099
|
+
poll 2 s, reload, onTimeout fail|continue), so the footage has no gap and the cut lands in
|
|
16304
18100
|
meta.skips[] and becomes a segment boundary; download keeps a file the page
|
|
16305
18101
|
makes in the take's outputs/ (selector: the download a click starts; src:
|
|
16306
18102
|
the currentSrc of a video, audio or img; as: the file name), listed in
|
|
@@ -16314,7 +18110,14 @@ name) and a list of its recent items, with the mask that hides it;
|
|
|
16314
18110
|
notices are information, never a --strict failure. A click, press or type with "guard": "spend" costs
|
|
16315
18111
|
something real: --dry-run hovers it and never presses, and record presses
|
|
16316
18112
|
it only with --confirm <id> (repeatable), refusing before any capture
|
|
16317
|
-
otherwise. --hosted refuses guarded and
|
|
18113
|
+
otherwise. --hosted refuses guarded, download, paste and js steps.
|
|
18114
|
+
peers in actions.json add other people OFF camera: each its own browser
|
|
18115
|
+
context (storageState, by a path from the actions file) on the same url,
|
|
18116
|
+
its steps started with a take step or after one (by id); only the take's
|
|
18117
|
+
page is filmed and only its steps make the cursor track, so a peer's work
|
|
18118
|
+
reaches the footage through the app's own sync. A peer takes wait, move,
|
|
18119
|
+
hover, click, type, press, scroll, drag and expect; its misses are skips
|
|
18120
|
+
(--strict fails on them); meta.peers[] places its steps; --hosted refuses it.
|
|
16318
18121
|
Every step is bounded: a call into a page that stopped answering gives up
|
|
16319
18122
|
at the step's own timeout (an expect's or an await's, or a gesture's plan
|
|
16320
18123
|
plus 30 s), the step is a skip with the real time it took, and --strict
|
|
@@ -16437,8 +18240,9 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
16437
18240
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
16438
18241
|
`;
|
|
16439
18242
|
async function loadActions(file) {
|
|
16440
|
-
const raw = JSON.parse(await
|
|
18243
|
+
const raw = JSON.parse(await readFile18(file, "utf8"));
|
|
16441
18244
|
const errors = validateActions(raw);
|
|
18245
|
+
if (!errors.length) errors.push(...jsSyntaxProblems(raw));
|
|
16442
18246
|
if (errors.length)
|
|
16443
18247
|
throw new UsageError(`invalid actions file:
|
|
16444
18248
|
${errors.join("\n ")}`);
|
|
@@ -16579,7 +18383,7 @@ function stepReportNote(rec) {
|
|
|
16579
18383
|
${l}`).join("");
|
|
16580
18384
|
}
|
|
16581
18385
|
function skippedLine(s) {
|
|
16582
|
-
return
|
|
18386
|
+
return `${s.peer ? `peer ${s.peer} ` : ""}#${s.step}${s.id ? ` ${s.id}` : ""} ${s.do}${s.reason ? `: ${s.reason}` : ` ${s.selector}`}`;
|
|
16583
18387
|
}
|
|
16584
18388
|
function takeHeaders(multi) {
|
|
16585
18389
|
try {
|
|
@@ -16595,8 +18399,8 @@ function takeBrowserArgs(multi) {
|
|
|
16595
18399
|
function takeStorageState(flags) {
|
|
16596
18400
|
const raw = strFlag(flags, "storage-state");
|
|
16597
18401
|
if (!raw) return void 0;
|
|
16598
|
-
const path =
|
|
16599
|
-
if (!
|
|
18402
|
+
const path = resolve20(raw);
|
|
18403
|
+
if (!existsSync29(path))
|
|
16600
18404
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
16601
18405
|
return path;
|
|
16602
18406
|
}
|
|
@@ -16609,7 +18413,7 @@ function takeSessionName(flags, storageState) {
|
|
|
16609
18413
|
throw new UsageError(
|
|
16610
18414
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
16611
18415
|
);
|
|
16612
|
-
if (!
|
|
18416
|
+
if (!existsSync29(sessionDir(name)))
|
|
16613
18417
|
throw new UsageError(
|
|
16614
18418
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
16615
18419
|
);
|
|
@@ -16681,7 +18485,7 @@ async function cmdRecord(argv) {
|
|
|
16681
18485
|
const scheme = urlSchemeProblem2(url);
|
|
16682
18486
|
if (scheme) throw new UsageError(scheme);
|
|
16683
18487
|
announceEnvNames(actions, r);
|
|
16684
|
-
const outDir =
|
|
18488
|
+
const outDir = resolve20(strFlag(flags, "out") ?? "take");
|
|
16685
18489
|
const storageState = takeStorageState(flags);
|
|
16686
18490
|
const sessionName = takeSessionName(flags, storageState);
|
|
16687
18491
|
const browserArgs = takeBrowserArgs(multi);
|
|
@@ -16701,7 +18505,7 @@ async function cmdRecord(argv) {
|
|
|
16701
18505
|
);
|
|
16702
18506
|
const hostedActions = await scriptForHost(
|
|
16703
18507
|
actions,
|
|
16704
|
-
dirname11(
|
|
18508
|
+
dirname11(resolve20(actionsPath))
|
|
16705
18509
|
).catch((e) => {
|
|
16706
18510
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
16707
18511
|
});
|
|
@@ -16809,7 +18613,7 @@ async function cmdRecord(argv) {
|
|
|
16809
18613
|
sessionName,
|
|
16810
18614
|
actions,
|
|
16811
18615
|
(context2) => recordTake(browser2, url, actions, takePaths(outDir), r.log, {
|
|
16812
|
-
actionsDir: dirname11(
|
|
18616
|
+
actionsDir: dirname11(resolve20(actionsPath)),
|
|
16813
18617
|
storageState,
|
|
16814
18618
|
headers,
|
|
16815
18619
|
context: context2,
|
|
@@ -16836,7 +18640,8 @@ async function cmdRecord(argv) {
|
|
|
16836
18640
|
...s.offCameraMs !== void 0 ? [
|
|
16837
18641
|
`${s.timedOut ? "timed out after" : "held after"} ${(s.offCameraMs / 1e3).toFixed(1)} s off camera`
|
|
16838
18642
|
] : [],
|
|
16839
|
-
...out ? [`\u2192 ${out.name} (${out.bytes} bytes, not kept in a rehearsal)`] : []
|
|
18643
|
+
...out ? [`\u2192 ${out.name} (${out.bytes} bytes, not kept in a rehearsal)`] : [],
|
|
18644
|
+
...s.value !== void 0 && !s.skipped ? [`= ${sayValue(s.value)}`] : []
|
|
16840
18645
|
].map((n) => ` ${n}`);
|
|
16841
18646
|
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("")}`;
|
|
16842
18647
|
});
|
|
@@ -16845,6 +18650,16 @@ async function cmdRecord(argv) {
|
|
|
16845
18650
|
lines.push(
|
|
16846
18651
|
` \u2013 #${i}${step.id ? ` (${step.id})` : ""} ${step.do} not run`
|
|
16847
18652
|
);
|
|
18653
|
+
for (const pr of rec.peers) {
|
|
18654
|
+
const peer = actions.peers?.find((p) => p.id === pr.id);
|
|
18655
|
+
lines.push(` peer ${pr.id} (off camera):`);
|
|
18656
|
+
for (const ps of pr.steps)
|
|
18657
|
+
lines.push(
|
|
18658
|
+
` ${ps.skipped ? "\u2717" : "\u2713"} #${ps.step}${ps.id ? ` (${ps.id})` : ""} ${ps.do}${ps.selector ? ` ${ps.selector}` : ""}${ps.with ? ` with ${ps.with}` : ps.after ? ` after ${ps.after}` : ""}${ps.skipped ? ` ${ps.reason ?? "NOT FOUND"}` : ""}${ps.late ? " (late)" : ""}`
|
|
18659
|
+
);
|
|
18660
|
+
for (const i of pr.notRun)
|
|
18661
|
+
lines.push(` \u2013 #${i} ${peer?.steps[i]?.do ?? "step"} not run`);
|
|
18662
|
+
}
|
|
16848
18663
|
const resolved = steps.filter((s) => !s.skipped).length;
|
|
16849
18664
|
const failed = strictFails(rec) || rec.stopped !== null;
|
|
16850
18665
|
const unconfirmed = guardVerdict(actions, confirm);
|
|
@@ -16882,7 +18697,8 @@ async function cmdRecord(argv) {
|
|
|
16882
18697
|
ready: rec.ready,
|
|
16883
18698
|
exposures: rec.exposures,
|
|
16884
18699
|
notices: rec.notices,
|
|
16885
|
-
masks: rec.masks
|
|
18700
|
+
masks: rec.masks,
|
|
18701
|
+
...rec.peers.length ? { peers: rec.peers } : {}
|
|
16886
18702
|
},
|
|
16887
18703
|
`${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)" : rec.ready.capped ? " (the first page never stopped changing)" : ""}
|
|
16888
18704
|
${lines.join("\n")}
|
|
@@ -16895,7 +18711,7 @@ ${lines.join("\n")}
|
|
|
16895
18711
|
}
|
|
16896
18712
|
const paths = takePaths(outDir);
|
|
16897
18713
|
const prepare = async () => {
|
|
16898
|
-
if (
|
|
18714
|
+
if (existsSync29(join33(outDir, "meta.json"))) {
|
|
16899
18715
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
16900
18716
|
r.log(
|
|
16901
18717
|
`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" : "")
|
|
@@ -16912,7 +18728,7 @@ ${lines.join("\n")}
|
|
|
16912
18728
|
sessionName,
|
|
16913
18729
|
actions,
|
|
16914
18730
|
(context2) => recordTake(browser, url, actions, paths, r.log, {
|
|
16915
|
-
actionsDir: dirname11(
|
|
18731
|
+
actionsDir: dirname11(resolve20(actionsPath)),
|
|
16916
18732
|
maxDurationSeconds,
|
|
16917
18733
|
storageState,
|
|
16918
18734
|
headers,
|
|
@@ -16990,11 +18806,12 @@ ${lines.join("\n")}
|
|
|
16990
18806
|
...rec.media.length ? { media: rec.media } : {},
|
|
16991
18807
|
overruns: rec.overruns,
|
|
16992
18808
|
pageErrors: rec.pageErrors,
|
|
18809
|
+
...rec.peers.length ? { peers: rec.peers } : {},
|
|
16993
18810
|
...strictFail ? { strictFailed: true } : {}
|
|
16994
18811
|
},
|
|
16995
18812
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${stepNote}${navTimeoutNote(rec)}${exposureNote(rec)}${noticeNote(rec)}` : `Take ready: ${outDir}
|
|
16996
18813
|
${(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)}${pageSoundNote(rec)}
|
|
16997
|
-
Next: edit ${
|
|
18814
|
+
Next: edit ${join33(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}${noticeNote(rec)}`
|
|
16998
18815
|
);
|
|
16999
18816
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
17000
18817
|
} finally {
|
|
@@ -17021,9 +18838,9 @@ async function cmdCreate2(argv) {
|
|
|
17021
18838
|
const scheme = urlSchemeProblem2(url);
|
|
17022
18839
|
if (scheme) throw new UsageError(scheme);
|
|
17023
18840
|
announceEnvNames(actions, r);
|
|
17024
|
-
const outDir =
|
|
18841
|
+
const outDir = resolve20(strFlag(flags, "out") ?? "take");
|
|
17025
18842
|
const format = videoFormat(strFlag(flags, "format"), positionals[0]);
|
|
17026
|
-
const out = positionals[0] ??
|
|
18843
|
+
const out = positionals[0] ?? join33(outDir, `out.${format}`);
|
|
17027
18844
|
const parallel = numFlag(flags, "parallel", 1);
|
|
17028
18845
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
17029
18846
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -17037,7 +18854,7 @@ async function cmdCreate2(argv) {
|
|
|
17037
18854
|
const confirm = takeConfirm(multi, actions, { dryRun: false, r });
|
|
17038
18855
|
const paths = takePaths(outDir);
|
|
17039
18856
|
const prepare = async () => {
|
|
17040
|
-
if (
|
|
18857
|
+
if (existsSync29(join33(outDir, "meta.json"))) {
|
|
17041
18858
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
17042
18859
|
r.log(
|
|
17043
18860
|
`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" : "")
|
|
@@ -17054,7 +18871,7 @@ async function cmdCreate2(argv) {
|
|
|
17054
18871
|
sessionName,
|
|
17055
18872
|
actions,
|
|
17056
18873
|
(context2) => recordTake(browser, url, actions, paths, r.log, {
|
|
17057
|
-
actionsDir: dirname11(
|
|
18874
|
+
actionsDir: dirname11(resolve20(actionsPath)),
|
|
17058
18875
|
maxDurationSeconds,
|
|
17059
18876
|
storageState,
|
|
17060
18877
|
headers,
|
|
@@ -17170,31 +18987,34 @@ async function cmdPlan(argvIn) {
|
|
|
17170
18987
|
const dir = positionals[0];
|
|
17171
18988
|
if (!dir)
|
|
17172
18989
|
throw new UsageError(
|
|
17173
|
-
'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<seconds>]]... [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
|
|
18990
|
+
'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<seconds>]]... [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--zooms per-beat]'
|
|
17174
18991
|
);
|
|
17175
18992
|
const r = createReporter(flags.json === true);
|
|
17176
18993
|
if (flags.fresh === true) {
|
|
17177
|
-
await rm6(
|
|
18994
|
+
await rm6(join33(dir, "doc.json"), { force: true });
|
|
17178
18995
|
r.log("note: --fresh discarded the existing doc.json");
|
|
17179
18996
|
}
|
|
17180
18997
|
let reuse;
|
|
17181
18998
|
if (flags.reuse === true) {
|
|
17182
|
-
const from =
|
|
17183
|
-
if (!
|
|
18999
|
+
const from = resolve20(strFlag(flags, "from") ?? join33(dir, PREV_DOC_NAME));
|
|
19000
|
+
if (!existsSync29(from)) {
|
|
17184
19001
|
throw new UsageError(
|
|
17185
19002
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
17186
19003
|
);
|
|
17187
19004
|
}
|
|
17188
19005
|
reuse = {
|
|
17189
19006
|
from,
|
|
17190
|
-
doc: JSON.parse(await
|
|
19007
|
+
doc: JSON.parse(await readFile18(from, "utf8"))
|
|
17191
19008
|
};
|
|
17192
19009
|
}
|
|
17193
19010
|
const style = await resolveStyleRef(flags);
|
|
17194
|
-
const hasDoc =
|
|
19011
|
+
const hasDoc = existsSync29(join33(dir, "doc.json"));
|
|
17195
19012
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
17196
19013
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
17197
19014
|
const stepCaptions = strFlag(flags, "captions") === "steps";
|
|
19015
|
+
const zoomMode = zoomModeOf(strFlag(flags, "zooms"));
|
|
19016
|
+
if (zoomMode && typeof zoomMode === "object")
|
|
19017
|
+
throw new UsageError(zoomMode.error);
|
|
17198
19018
|
if (stepCaptions) release.launchRoles.captions = "none";
|
|
17199
19019
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
17200
19020
|
const stillRaw = strFlag(flags, "still") ?? release.launchRoles.still;
|
|
@@ -17229,6 +19049,23 @@ async function cmdPlan(argvIn) {
|
|
|
17229
19049
|
captions = landStepCaptions(s.doc, /* @__PURE__ */ new Map([["", take.actions]]));
|
|
17230
19050
|
await writeJson(take.paths.doc, s.doc, true);
|
|
17231
19051
|
}
|
|
19052
|
+
const marksTake = await loadTake(dir);
|
|
19053
|
+
const drift = scriptDrift(
|
|
19054
|
+
marksTake.meta.steps ?? [],
|
|
19055
|
+
marksTake.actions?.steps
|
|
19056
|
+
);
|
|
19057
|
+
if (drift) r.warn(scriptDriftLine(drift));
|
|
19058
|
+
const marks = applyZoomMarks(s.doc, /* @__PURE__ */ new Map([["", marksTake.actions]]));
|
|
19059
|
+
if (marks.rejected.length) await writeJson(marksTake.paths.doc, s.doc, true);
|
|
19060
|
+
const marksLine = zoomMarkLine(marks);
|
|
19061
|
+
let perBeat = null;
|
|
19062
|
+
if (zoomMode === "per-beat") {
|
|
19063
|
+
const take = await loadTake(dir);
|
|
19064
|
+
perBeat = oneZoomPerBeat(s.doc, /* @__PURE__ */ new Map([["", take.actions]]));
|
|
19065
|
+
await writeJson(take.paths.doc, s.doc, true);
|
|
19066
|
+
}
|
|
19067
|
+
const zoomAuto = s.doc.zoom.filter((z) => z.source !== "manual").length;
|
|
19068
|
+
const zoomManual = s.doc.zoom.length - zoomAuto;
|
|
17232
19069
|
const captionLines = captions ? `
|
|
17233
19070
|
${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} anchored to their steps` + (captions.skipped.length ? `
|
|
17234
19071
|
not landed: ${captions.skipped.join("; ")}` : "") + (captions.uncaptioned.length ? `
|
|
@@ -17249,20 +19086,23 @@ async function cmdPlan(argvIn) {
|
|
|
17249
19086
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
17250
19087
|
r.done(
|
|
17251
19088
|
{
|
|
17252
|
-
take:
|
|
19089
|
+
take: resolve20(dir),
|
|
17253
19090
|
fresh: s.fresh,
|
|
17254
19091
|
cursorKept: s.cursorKept,
|
|
17255
|
-
zoomAuto
|
|
17256
|
-
zoomManual
|
|
19092
|
+
zoomAuto,
|
|
19093
|
+
zoomManual,
|
|
17257
19094
|
duration: s.doc.source.meta.durationMs / 1e3,
|
|
17258
19095
|
...s.backdrop !== void 0 ? { backdrop: s.backdrop } : {},
|
|
17259
19096
|
...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
|
|
17260
19097
|
...s.layout ? { layout: s.layout } : {},
|
|
17261
19098
|
...s.motion ? { motion: s.motion } : {},
|
|
17262
19099
|
...s.reuse ? { reuse: s.reuse } : {},
|
|
17263
|
-
...captions ? { captions } : {}
|
|
19100
|
+
...captions ? { captions } : {},
|
|
19101
|
+
...perBeat ? { perBeat } : {}
|
|
17264
19102
|
},
|
|
17265
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
19103
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join33(dir, "doc.json")}: ${zoomAuto} auto + ${zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}${captionLines}${marksLine ? `
|
|
19104
|
+
${marksLine}` : ""}${perBeat ? `
|
|
19105
|
+
${perBeatLine(perBeat)}` : ""}`
|
|
17266
19106
|
);
|
|
17267
19107
|
return EXIT_OK;
|
|
17268
19108
|
}
|
|
@@ -17385,12 +19225,12 @@ async function cmdRender(argv) {
|
|
|
17385
19225
|
);
|
|
17386
19226
|
const r = createReporter(flags.json === true);
|
|
17387
19227
|
const format = videoFormat(strFlag(flags, "format"), positionals[1]);
|
|
17388
|
-
const out = positionals[1] ??
|
|
19228
|
+
const out = positionals[1] ?? join33(dir, `out.${format}`);
|
|
17389
19229
|
const parallel = numFlag(flags, "parallel", 1);
|
|
17390
19230
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
17391
19231
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
17392
19232
|
}
|
|
17393
|
-
let
|
|
19233
|
+
let range2;
|
|
17394
19234
|
const rangeRaw = strFlag(flags, "range");
|
|
17395
19235
|
if (rangeRaw !== void 0) {
|
|
17396
19236
|
const m = /^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/.exec(rangeRaw);
|
|
@@ -17398,8 +19238,8 @@ async function cmdRender(argv) {
|
|
|
17398
19238
|
throw new UsageError(
|
|
17399
19239
|
"--range expects a..b in OUTPUT seconds (e.g. --range 4..8)"
|
|
17400
19240
|
);
|
|
17401
|
-
|
|
17402
|
-
if (
|
|
19241
|
+
range2 = [Number(m[1]), Number(m[2])];
|
|
19242
|
+
if (range2[1] <= range2[0])
|
|
17403
19243
|
throw new UsageError("--range end must be > start");
|
|
17404
19244
|
}
|
|
17405
19245
|
const browser = await launchBrowser();
|
|
@@ -17410,7 +19250,7 @@ async function cmdRender(argv) {
|
|
|
17410
19250
|
fps: hasFlag(flags, "fps") ? numFlag(flags, "fps", 30) : void 0,
|
|
17411
19251
|
format,
|
|
17412
19252
|
parallel,
|
|
17413
|
-
range,
|
|
19253
|
+
range: range2,
|
|
17414
19254
|
draft: flags.draft === true,
|
|
17415
19255
|
overrides: {
|
|
17416
19256
|
set: multi.set,
|
|
@@ -17430,10 +19270,10 @@ async function cmdRender(argv) {
|
|
|
17430
19270
|
r.done(
|
|
17431
19271
|
{
|
|
17432
19272
|
...result,
|
|
17433
|
-
...
|
|
19273
|
+
...range2 ? { range: range2 } : {},
|
|
17434
19274
|
...flags.draft === true ? { draft: true } : {}
|
|
17435
19275
|
},
|
|
17436
|
-
`Wrote ${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" : ""}${result.chunks > 1 ? `, ${result.chunks} parallel chunks` : ""}${
|
|
19276
|
+
`Wrote ${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" : ""}${result.chunks > 1 ? `, ${result.chunks} parallel chunks` : ""}${range2 ? `, range ${range2[0]}..${range2[1]}s` : ""}${flags.draft === true ? ", DRAFT" : ""})`
|
|
17437
19277
|
);
|
|
17438
19278
|
return EXIT_OK;
|
|
17439
19279
|
} finally {
|
|
@@ -17461,7 +19301,7 @@ async function cmdFrames(argv) {
|
|
|
17461
19301
|
if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
|
|
17462
19302
|
size = { width: Number(m[1]), height: Number(m[2]) };
|
|
17463
19303
|
}
|
|
17464
|
-
const duration =
|
|
19304
|
+
const duration = docOutputDuration5(take.doc);
|
|
17465
19305
|
const frameRaw = strFlag(flags, "frame");
|
|
17466
19306
|
const timesRaw = strFlag(flags, "times");
|
|
17467
19307
|
const atZooms = flags["at-zooms"] === true;
|
|
@@ -17546,7 +19386,7 @@ async function cmdDeliver(argv) {
|
|
|
17546
19386
|
} catch (e) {
|
|
17547
19387
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
17548
19388
|
}
|
|
17549
|
-
let
|
|
19389
|
+
let range2;
|
|
17550
19390
|
const rangeRaw = strFlag(flags, "range");
|
|
17551
19391
|
if (rangeRaw !== void 0) {
|
|
17552
19392
|
const m = /^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/.exec(rangeRaw);
|
|
@@ -17554,8 +19394,8 @@ async function cmdDeliver(argv) {
|
|
|
17554
19394
|
throw new UsageError(
|
|
17555
19395
|
"--range expects a..b in OUTPUT seconds (e.g. --range 4..24)"
|
|
17556
19396
|
);
|
|
17557
|
-
|
|
17558
|
-
if (
|
|
19397
|
+
range2 = [Number(m[1]), Number(m[2])];
|
|
19398
|
+
if (range2[1] <= range2[0])
|
|
17559
19399
|
throw new UsageError("--range end must be > start");
|
|
17560
19400
|
}
|
|
17561
19401
|
const take = await loadTake(dir);
|
|
@@ -17581,7 +19421,7 @@ async function cmdDeliver(argv) {
|
|
|
17581
19421
|
channels,
|
|
17582
19422
|
outDir: strFlag(flags, "out"),
|
|
17583
19423
|
release: strFlag(flags, "release"),
|
|
17584
|
-
range,
|
|
19424
|
+
range: range2,
|
|
17585
19425
|
parallel,
|
|
17586
19426
|
overrides: {
|
|
17587
19427
|
set: multi.set,
|
|
@@ -17626,15 +19466,15 @@ async function resolveStyleRef(flags) {
|
|
|
17626
19466
|
}
|
|
17627
19467
|
async function resolveDocRef(ref, flags, what) {
|
|
17628
19468
|
const styleRef = ref;
|
|
17629
|
-
const file =
|
|
19469
|
+
const file = existsSync29(styleRef) ? resolve20(
|
|
17630
19470
|
styleRef,
|
|
17631
|
-
|
|
19471
|
+
existsSync29(join33(styleRef, "doc.json")) ? "doc.json" : ""
|
|
17632
19472
|
) : null;
|
|
17633
|
-
if (file &&
|
|
19473
|
+
if (file && existsSync29(file)) {
|
|
17634
19474
|
return {
|
|
17635
19475
|
from: file,
|
|
17636
19476
|
doc: migrateHostedDoc4(
|
|
17637
|
-
JSON.parse(await
|
|
19477
|
+
JSON.parse(await readFile18(file, "utf8"))
|
|
17638
19478
|
)
|
|
17639
19479
|
};
|
|
17640
19480
|
}
|
|
@@ -17686,7 +19526,7 @@ async function cmdDigest(argv) {
|
|
|
17686
19526
|
const take = await loadTake(dir);
|
|
17687
19527
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
17688
19528
|
const transcriptPath = strFlag(flags, "transcript");
|
|
17689
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
19529
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile18(transcriptPath, "utf8"))) : null;
|
|
17690
19530
|
const style = await resolveStyleRef(flags);
|
|
17691
19531
|
const noFrames = flags["no-frames"] === true;
|
|
17692
19532
|
let browser = null;
|
|
@@ -17717,7 +19557,7 @@ async function cmdDigest(argv) {
|
|
|
17717
19557
|
kinds,
|
|
17718
19558
|
frames: d.images.full,
|
|
17719
19559
|
crops: d.images.crop,
|
|
17720
|
-
sheet: d.images.sheet ?
|
|
19560
|
+
sheet: d.images.sheet ? join33(result.outDir, d.images.sheet) : null,
|
|
17721
19561
|
scenes: kinds.scene ?? 0,
|
|
17722
19562
|
sourceDuration: d.take.sourceDuration,
|
|
17723
19563
|
outputDuration: d.take.outputDuration,
|
|
@@ -17745,14 +19585,14 @@ async function cmdOpen(argv) {
|
|
|
17745
19585
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
17746
19586
|
const r = createReporter(flags.json === true);
|
|
17747
19587
|
const take = await loadTake(dir);
|
|
17748
|
-
if (!
|
|
19588
|
+
if (!existsSync29(take.paths.recording)) {
|
|
17749
19589
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
17750
19590
|
}
|
|
17751
19591
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
17752
19592
|
/\/+$/,
|
|
17753
19593
|
""
|
|
17754
19594
|
);
|
|
17755
|
-
const server = await startTakeServer(
|
|
19595
|
+
const server = await startTakeServer(resolve20(dir), {});
|
|
17756
19596
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
17757
19597
|
r.event({ event: "open", server: server.base, url });
|
|
17758
19598
|
r.log(`take served at ${server.base}`);
|
|
@@ -17799,7 +19639,7 @@ async function cmdCallout(argv) {
|
|
|
17799
19639
|
`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`
|
|
17800
19640
|
);
|
|
17801
19641
|
const codeFile = strFlag(flags, "code-file");
|
|
17802
|
-
const code = codeFile ? await
|
|
19642
|
+
const code = codeFile ? await readFile18(codeFile, "utf8") : strFlag(flags, "code");
|
|
17803
19643
|
const words2 = {
|
|
17804
19644
|
kicker: strFlag(flags, "kicker"),
|
|
17805
19645
|
title: strFlag(flags, "title"),
|
|
@@ -17878,7 +19718,7 @@ async function cmdValidate(argv) {
|
|
|
17878
19718
|
const target = positionals[0];
|
|
17879
19719
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
17880
19720
|
const r = createReporter(flags.json === true);
|
|
17881
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
19721
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync29(join33(target, "kit.json")) && !isTakeDir(target) ? join33(target, "kit.json") : null;
|
|
17882
19722
|
if (kitTarget) {
|
|
17883
19723
|
const picture = flags.picture === true;
|
|
17884
19724
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -17910,9 +19750,9 @@ async function cmdValidate(argv) {
|
|
|
17910
19750
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
17911
19751
|
return EXIT_OK;
|
|
17912
19752
|
}
|
|
17913
|
-
if (!isTakeDir(target) &&
|
|
19753
|
+
if (!isTakeDir(target) && existsSync29(join33(target, "config.json"))) {
|
|
17914
19754
|
const parsed = JSON.parse(
|
|
17915
|
-
await
|
|
19755
|
+
await readFile18(join33(target, "config.json"), "utf8")
|
|
17916
19756
|
);
|
|
17917
19757
|
const pre = preflightConfig(parsed);
|
|
17918
19758
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -17954,7 +19794,7 @@ async function cmdValidate(argv) {
|
|
|
17954
19794
|
const problems = [];
|
|
17955
19795
|
const warnings = [];
|
|
17956
19796
|
const notes = [];
|
|
17957
|
-
if (!
|
|
19797
|
+
if (!existsSync29(take.paths.recording)) {
|
|
17958
19798
|
problems.push("missing recording (re-run record)");
|
|
17959
19799
|
} else {
|
|
17960
19800
|
const mismatch = containerMismatch(
|
|
@@ -17995,16 +19835,12 @@ async function cmdValidate(argv) {
|
|
|
17995
19835
|
const found = localFile(rel, target);
|
|
17996
19836
|
if (!("refused" in found)) continue;
|
|
17997
19837
|
problems.push(
|
|
17998
|
-
|
|
19838
|
+
existsSync29(resolve20(target, rel)) ? `doc.json: ${ref.where}: ${ref.key} ${found.refused}` : `doc.json: ${ref.where}: ${ref.key} is not a file in the take, so a render plays the film without it (put the file there, or fix the key)`
|
|
17999
19839
|
);
|
|
18000
19840
|
}
|
|
18001
19841
|
if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
|
|
18002
19842
|
const doc = take.doc;
|
|
18003
|
-
const times =
|
|
18004
|
-
...new Set(
|
|
18005
|
-
(doc.overlays ?? []).filter((o) => o.kind === "html").map((o) => +(o.start + Math.min(0.35, o.duration / 2)).toFixed(3))
|
|
18006
|
-
)
|
|
18007
|
-
];
|
|
19843
|
+
const times = groundCheckTimes(doc);
|
|
18008
19844
|
const browser = await launchBrowser();
|
|
18009
19845
|
try {
|
|
18010
19846
|
const frames = await footageFrames(
|
|
@@ -18078,7 +19914,7 @@ async function cmdPush3(argv) {
|
|
|
18078
19914
|
);
|
|
18079
19915
|
const refusal = takePushRefusal({
|
|
18080
19916
|
vos: strFlag(flags, "vos"),
|
|
18081
|
-
trackedVosId: readSyncState(
|
|
19917
|
+
trackedVosId: readSyncState(resolve20(dir))?.vosId ?? null,
|
|
18082
19918
|
programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
|
|
18083
19919
|
(name) => hasFlag(flags, name)
|
|
18084
19920
|
)
|
|
@@ -18094,7 +19930,7 @@ async function cmdPush3(argv) {
|
|
|
18094
19930
|
`--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
|
|
18095
19931
|
);
|
|
18096
19932
|
const claim = await pushTakeClaimable(
|
|
18097
|
-
|
|
19933
|
+
resolve20(dir),
|
|
18098
19934
|
{
|
|
18099
19935
|
api: strFlag(flags, "api"),
|
|
18100
19936
|
origin: strFlag(flags, "origin"),
|
|
@@ -18117,7 +19953,7 @@ reference and the only credential.`
|
|
|
18117
19953
|
}
|
|
18118
19954
|
const overrides = multi.override;
|
|
18119
19955
|
const result = await pushTake(
|
|
18120
|
-
|
|
19956
|
+
resolve20(dir),
|
|
18121
19957
|
{
|
|
18122
19958
|
key: strFlag(flags, "key"),
|
|
18123
19959
|
api: strFlag(flags, "api"),
|
|
@@ -18172,7 +20008,7 @@ async function cmdPull2(argv) {
|
|
|
18172
20008
|
const dir = positionals[0] ?? ".";
|
|
18173
20009
|
const r = createReporter(flags.json === true);
|
|
18174
20010
|
const result = await pullTake(
|
|
18175
|
-
|
|
20011
|
+
resolve20(dir),
|
|
18176
20012
|
{
|
|
18177
20013
|
key: strFlag(flags, "key"),
|
|
18178
20014
|
api: strFlag(flags, "api"),
|
|
@@ -18301,6 +20137,8 @@ async function run(argv) {
|
|
|
18301
20137
|
return await cmdCheckVideo(rest);
|
|
18302
20138
|
case "join":
|
|
18303
20139
|
return await cmdJoin(rest);
|
|
20140
|
+
case "info":
|
|
20141
|
+
return await cmdTakeInfo(rest);
|
|
18304
20142
|
case "score":
|
|
18305
20143
|
return await cmdScore(rest);
|
|
18306
20144
|
case "audio":
|
|
@@ -18316,7 +20154,7 @@ async function run(argv) {
|
|
|
18316
20154
|
`);
|
|
18317
20155
|
return EXIT_USAGE;
|
|
18318
20156
|
}
|
|
18319
|
-
if (e instanceof SetupError || e instanceof SetupEnvError || e instanceof RecordRefusedError || e instanceof PlatformBehindError || e instanceof PushRefusedError) {
|
|
20157
|
+
if (e instanceof SetupError || e instanceof SetupEnvError || e instanceof RecordRefusedError || e instanceof PlatformBehindError || e instanceof PushRefusedError || e instanceof PasteFileRefusal) {
|
|
18320
20158
|
process.stderr.write(`${e.message}
|
|
18321
20159
|
`);
|
|
18322
20160
|
return EXIT_USAGE;
|
|
@@ -18389,7 +20227,7 @@ async function cmdSession(argv) {
|
|
|
18389
20227
|
if (!arg) throw new UsageError(usage);
|
|
18390
20228
|
const bad = sessionNameVerdict(arg);
|
|
18391
20229
|
if (bad) throw new UsageError(bad);
|
|
18392
|
-
if (!
|
|
20230
|
+
if (!existsSync29(sessionDir(arg)))
|
|
18393
20231
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
18394
20232
|
const summary3 = await summarizeSession(arg);
|
|
18395
20233
|
const url = strFlag(flags, "url");
|
|
@@ -18466,4 +20304,4 @@ export {
|
|
|
18466
20304
|
verbHelp,
|
|
18467
20305
|
run
|
|
18468
20306
|
};
|
|
18469
|
-
//# sourceMappingURL=chunk-
|
|
20307
|
+
//# sourceMappingURL=chunk-SP5OHTVB.js.map
|