@vosjs/cli 0.36.0 → 0.38.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 +10 -2
- package/dist/{chunk-NHKHTIDR.js → chunk-FACYY7VV.js} +98 -6
- package/dist/chunk-FACYY7VV.js.map +1 -0
- package/dist/{chunk-MIZWK5AN.js → chunk-LGRNHJ2U.js} +786 -470
- package/dist/chunk-LGRNHJ2U.js.map +1 -0
- package/dist/cli.js +22 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +2 -2
- package/dist/run-ZNQKSGAF.js +15 -0
- package/package.json +4 -4
- package/dist/chunk-MIZWK5AN.js.map +0 -1
- package/dist/chunk-NHKHTIDR.js.map +0 -1
- package/dist/run-LHMYTKV7.js +0 -9
- /package/dist/{run-LHMYTKV7.js.map → run-ZNQKSGAF.js.map} +0 -0
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
BrowserUnavailableError,
|
|
4
4
|
directoryKind,
|
|
5
|
-
launchBrowser
|
|
6
|
-
|
|
5
|
+
launchBrowser,
|
|
6
|
+
noteGivenFlag,
|
|
7
|
+
trackFlags
|
|
8
|
+
} from "./chunk-FACYY7VV.js";
|
|
7
9
|
|
|
8
10
|
// src/plugin/run.ts
|
|
9
|
-
import { mkdir as
|
|
11
|
+
import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
|
|
10
12
|
import { existsSync as existsSync15 } from "fs";
|
|
11
13
|
import { join as join18, resolve as resolve9 } from "path";
|
|
12
14
|
import {
|
|
@@ -52,7 +54,9 @@ function parseArgs(argv, booleanFlags, multiFlags = /* @__PURE__ */ new Set()) {
|
|
|
52
54
|
}
|
|
53
55
|
positionals.push(arg);
|
|
54
56
|
}
|
|
55
|
-
|
|
57
|
+
for (const name of Object.keys(flags)) noteGivenFlag(name);
|
|
58
|
+
for (const name of Object.keys(multi)) noteGivenFlag(name);
|
|
59
|
+
return { positionals, flags: trackFlags(flags), multi: trackFlags(multi) };
|
|
56
60
|
}
|
|
57
61
|
function numFlag(flags, name, fallback) {
|
|
58
62
|
const v = flags[name];
|
|
@@ -943,6 +947,17 @@ function lintDoc(docIn) {
|
|
|
943
947
|
}
|
|
944
948
|
const TRANSITIONS = ["none", "fade", "rise"];
|
|
945
949
|
const PRESETS = ["title", "caption", "label"];
|
|
950
|
+
let footageEnd = null;
|
|
951
|
+
if (recording && Array.isArray(doc.segments)) {
|
|
952
|
+
try {
|
|
953
|
+
footageEnd = ratedSegments(doc).reduce(
|
|
954
|
+
(a, s) => a + (s.out - s.in) / (s.rate ?? 1),
|
|
955
|
+
0
|
|
956
|
+
);
|
|
957
|
+
} catch {
|
|
958
|
+
footageEnd = null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
946
961
|
for (let i = 0; i < (overlays?.length ?? 0); i++) {
|
|
947
962
|
const o = overlays[i];
|
|
948
963
|
const name = `overlays[${i}]`;
|
|
@@ -960,6 +975,14 @@ function lintDoc(docIn) {
|
|
|
960
975
|
if (!isNum(o.duration) || o.duration <= 0) {
|
|
961
976
|
problems.push(`${name}.duration must be > 0 (seconds)`);
|
|
962
977
|
}
|
|
978
|
+
if (footageEnd !== null && footageEnd > 0 && isNum(o.start) && isNum(o.duration)) {
|
|
979
|
+
const over = o.start + o.duration - footageEnd;
|
|
980
|
+
if (over > 0.05) {
|
|
981
|
+
warnings.push(
|
|
982
|
+
o.start >= footageEnd ? `${name} starts at ${o.start.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays on bare backdrop and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), or put a freeze under it` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the output extends over bare backdrop. Shorten it, or put a freeze under it`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
963
986
|
checkPin(o, name, docIn, recording, problems, warnings);
|
|
964
987
|
if (o.kind === "text") {
|
|
965
988
|
if (typeof o.text !== "string")
|
|
@@ -1635,7 +1658,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1635
1658
|
if (pinned) {
|
|
1636
1659
|
if (pinned.clamped > 4) {
|
|
1637
1660
|
warnings.push(
|
|
1638
|
-
`overlays[${i}] is pinned ${pinned.side} of its referent but the frame has no room there \u2014 it sits ${Math.round(pinned.clamped)} design px off; a smaller box, another side, or a lower zoom level keeps it beside what it names`
|
|
1661
|
+
`overlays[${i}] is pinned ${pinned.side} of its referent but the frame has no room there \u2014 it sits ${Math.round(pinned.clamped)} design px off; a smaller box, another side, or a lower zoom level keeps it beside what it names. A pin resolves THROUGH the camera, so author the zoom first: a callout composed before it is measured against a frame the viewer never sees`
|
|
1639
1662
|
);
|
|
1640
1663
|
}
|
|
1641
1664
|
return;
|
|
@@ -1681,6 +1704,392 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1681
1704
|
}
|
|
1682
1705
|
}
|
|
1683
1706
|
|
|
1707
|
+
// src/plugin/container.ts
|
|
1708
|
+
var OCTET_STREAM = "application/octet-stream";
|
|
1709
|
+
var TYPES_BY_EXTENSION = {
|
|
1710
|
+
svg: "image/svg+xml",
|
|
1711
|
+
png: "image/png",
|
|
1712
|
+
jpg: "image/jpeg",
|
|
1713
|
+
jpeg: "image/jpeg",
|
|
1714
|
+
webp: "image/webp",
|
|
1715
|
+
gif: "image/gif",
|
|
1716
|
+
avif: "image/avif",
|
|
1717
|
+
webm: "video/webm",
|
|
1718
|
+
mp4: "video/mp4",
|
|
1719
|
+
mov: "video/quicktime",
|
|
1720
|
+
mp3: "audio/mpeg",
|
|
1721
|
+
wav: "audio/wav",
|
|
1722
|
+
ogg: "audio/ogg",
|
|
1723
|
+
m4a: "audio/mp4"
|
|
1724
|
+
};
|
|
1725
|
+
var EXTENSION_BY_TYPE = {
|
|
1726
|
+
"image/jpeg": ".jpg",
|
|
1727
|
+
"video/mp4": ".mp4",
|
|
1728
|
+
"audio/mp4": ".m4a"
|
|
1729
|
+
};
|
|
1730
|
+
var TAKE_MEDIA_EXTENSIONS = [
|
|
1731
|
+
".webm",
|
|
1732
|
+
".mp4",
|
|
1733
|
+
".mov",
|
|
1734
|
+
".m4a",
|
|
1735
|
+
".mp3",
|
|
1736
|
+
".ogg",
|
|
1737
|
+
".wav"
|
|
1738
|
+
];
|
|
1739
|
+
var MEDIA_HEAD_BYTES = 16;
|
|
1740
|
+
var ISO_BRANDS = {
|
|
1741
|
+
"qt ": "video/quicktime",
|
|
1742
|
+
"M4A ": "audio/mp4",
|
|
1743
|
+
"M4B ": "audio/mp4",
|
|
1744
|
+
avif: "image/avif",
|
|
1745
|
+
avis: "image/avif"
|
|
1746
|
+
};
|
|
1747
|
+
var RIFF_FORMS = {
|
|
1748
|
+
WEBP: "image/webp",
|
|
1749
|
+
WAVE: "audio/wav",
|
|
1750
|
+
"AVI ": "video/x-msvideo"
|
|
1751
|
+
};
|
|
1752
|
+
function sniffMediaType(head) {
|
|
1753
|
+
const ascii = (at2, len) => head.length >= at2 + len ? String.fromCharCode(...head.subarray(at2, at2 + len)) : "";
|
|
1754
|
+
const startsWith = (...bytes) => bytes.every((b, i) => head[i] === b);
|
|
1755
|
+
if (startsWith(26, 69, 223, 163)) return "video/webm";
|
|
1756
|
+
if (ascii(4, 4) === "ftyp") return ISO_BRANDS[ascii(8, 4)] ?? "video/mp4";
|
|
1757
|
+
if (ascii(0, 4) === "RIFF") return RIFF_FORMS[ascii(8, 4)] ?? null;
|
|
1758
|
+
if (startsWith(137, 80, 78, 71, 13, 10, 26, 10))
|
|
1759
|
+
return "image/png";
|
|
1760
|
+
if (startsWith(255, 216, 255)) return "image/jpeg";
|
|
1761
|
+
if (ascii(0, 4) === "GIF8") return "image/gif";
|
|
1762
|
+
if (ascii(0, 4) === "OggS") return "audio/ogg";
|
|
1763
|
+
if (ascii(0, 3) === "ID3") return "audio/mpeg";
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
function bareType(value) {
|
|
1767
|
+
const type = (value ?? "").split(";")[0].trim().toLowerCase();
|
|
1768
|
+
return !type || type === OCTET_STREAM || type === "binary/octet-stream" ? "" : type;
|
|
1769
|
+
}
|
|
1770
|
+
function mediaContentType(file) {
|
|
1771
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
|
|
1772
|
+
return TYPES_BY_EXTENSION[ext] ?? OCTET_STREAM;
|
|
1773
|
+
}
|
|
1774
|
+
function extensionFor(contentType) {
|
|
1775
|
+
const type = (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
1776
|
+
if (EXTENSION_BY_TYPE[type]) return EXTENSION_BY_TYPE[type];
|
|
1777
|
+
const ext = Object.keys(TYPES_BY_EXTENSION).find(
|
|
1778
|
+
(e) => TYPES_BY_EXTENSION[e] === type
|
|
1779
|
+
);
|
|
1780
|
+
return ext ? `.${ext}` : "";
|
|
1781
|
+
}
|
|
1782
|
+
function resolveMediaType(claims) {
|
|
1783
|
+
const sniffed = claims.head?.length ? sniffMediaType(claims.head) : null;
|
|
1784
|
+
if (sniffed) return { type: sniffed, from: "bytes" };
|
|
1785
|
+
const declared = bareType(claims.declared);
|
|
1786
|
+
if (declared) return { type: declared, from: "declared" };
|
|
1787
|
+
const named = mediaContentType(claims.filename ?? "");
|
|
1788
|
+
if (named !== OCTET_STREAM) return { type: named, from: "filename" };
|
|
1789
|
+
return { type: OCTET_STREAM, from: "unknown" };
|
|
1790
|
+
}
|
|
1791
|
+
function nameForType(filename, type) {
|
|
1792
|
+
const ext = extensionFor(type);
|
|
1793
|
+
if (!ext) return filename;
|
|
1794
|
+
const current = /\.[a-z0-9]+$/i.exec(filename)?.[0] ?? "";
|
|
1795
|
+
if (current && mediaContentType(`x${current}`) === type) return filename;
|
|
1796
|
+
return `${filename.slice(0, filename.length - current.length)}${ext}`;
|
|
1797
|
+
}
|
|
1798
|
+
function containerMismatch(filename, head) {
|
|
1799
|
+
const actual = sniffMediaType(head);
|
|
1800
|
+
if (!actual) return null;
|
|
1801
|
+
const claimed = mediaContentType(filename);
|
|
1802
|
+
if (claimed === actual) return null;
|
|
1803
|
+
return `${filename} is ${actual}, not the ${claimed} its name claims \u2014 rename it to ${nameForType(filename, actual)}`;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/plugin/media.ts
|
|
1807
|
+
import { isKeyedOverlay } from "@vosjs/studio-core";
|
|
1808
|
+
import { createWriteStream, existsSync as existsSync2 } from "fs";
|
|
1809
|
+
import {
|
|
1810
|
+
mkdir as mkdir2,
|
|
1811
|
+
open,
|
|
1812
|
+
readdir,
|
|
1813
|
+
rename as rename2,
|
|
1814
|
+
rm as rm2,
|
|
1815
|
+
stat,
|
|
1816
|
+
writeFile as writeFile2
|
|
1817
|
+
} from "fs/promises";
|
|
1818
|
+
import { basename as basename2, dirname, join as join2 } from "path";
|
|
1819
|
+
import { Readable } from "stream";
|
|
1820
|
+
import { pipeline } from "stream/promises";
|
|
1821
|
+
|
|
1822
|
+
// src/plugin/take.ts
|
|
1823
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1824
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
1825
|
+
import { basename, join } from "path";
|
|
1826
|
+
var RECORDING_NAME = "recording.webm";
|
|
1827
|
+
function takeMediaFiles(dir, stem) {
|
|
1828
|
+
return TAKE_MEDIA_EXTENSIONS.map((ext) => join(dir, `${stem}${ext}`)).filter(
|
|
1829
|
+
(file) => existsSync(file)
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
function takeMediaFile(dir, stem, fallback) {
|
|
1833
|
+
return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback);
|
|
1834
|
+
}
|
|
1835
|
+
var PREV_DOC_NAME = "doc.prev.json";
|
|
1836
|
+
function takePaths(dir) {
|
|
1837
|
+
const recording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
1838
|
+
return {
|
|
1839
|
+
dir,
|
|
1840
|
+
recording,
|
|
1841
|
+
recordingName: basename(recording),
|
|
1842
|
+
framesDir: join(dir, "frames"),
|
|
1843
|
+
framesIndex: join(dir, "frames.json"),
|
|
1844
|
+
cursor: join(dir, "cursor.json"),
|
|
1845
|
+
meta: join(dir, "meta.json"),
|
|
1846
|
+
actions: join(dir, "actions.json"),
|
|
1847
|
+
doc: join(dir, "doc.json")
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
async function ensureTakeDir(dir) {
|
|
1851
|
+
const p = takePaths(dir);
|
|
1852
|
+
await mkdir(p.framesDir, { recursive: true });
|
|
1853
|
+
return p;
|
|
1854
|
+
}
|
|
1855
|
+
async function dropScreencastFrames(p) {
|
|
1856
|
+
if (!existsSync(p.recording) || statSync(p.recording).size === 0) return 0;
|
|
1857
|
+
if (!existsSync(p.framesDir)) return 0;
|
|
1858
|
+
let freed = 0;
|
|
1859
|
+
for (const name of readdirSync(p.framesDir)) {
|
|
1860
|
+
try {
|
|
1861
|
+
freed += statSync(join(p.framesDir, name)).size;
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
await rm(p.framesDir, { recursive: true, force: true });
|
|
1866
|
+
await rm(p.framesIndex, { force: true });
|
|
1867
|
+
return freed;
|
|
1868
|
+
}
|
|
1869
|
+
async function prepareReRecord(dir) {
|
|
1870
|
+
const p = takePaths(dir);
|
|
1871
|
+
const kept = [];
|
|
1872
|
+
let prevDoc = false;
|
|
1873
|
+
if (existsSync(p.doc)) {
|
|
1874
|
+
await rename(p.doc, join(dir, PREV_DOC_NAME));
|
|
1875
|
+
prevDoc = true;
|
|
1876
|
+
kept.push(PREV_DOC_NAME);
|
|
1877
|
+
}
|
|
1878
|
+
if (existsSync(p.actions)) kept.push("actions.json");
|
|
1879
|
+
if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
|
|
1880
|
+
for (const stale of [
|
|
1881
|
+
// Every container, not just the one `record` writes: a pulled take's
|
|
1882
|
+
// footage may be mp4, and a survivor would outrank the new recording.
|
|
1883
|
+
...takeMediaFiles(dir, "recording"),
|
|
1884
|
+
...takeMediaFiles(dir, "mic"),
|
|
1885
|
+
...takeMediaFiles(dir, "cam"),
|
|
1886
|
+
p.cursor,
|
|
1887
|
+
p.meta,
|
|
1888
|
+
p.framesIndex,
|
|
1889
|
+
p.framesDir,
|
|
1890
|
+
join(dir, "stills"),
|
|
1891
|
+
join(dir, "digest")
|
|
1892
|
+
]) {
|
|
1893
|
+
await rm(stale, { recursive: true, force: true });
|
|
1894
|
+
}
|
|
1895
|
+
return { prevDoc, kept };
|
|
1896
|
+
}
|
|
1897
|
+
async function readJson(file) {
|
|
1898
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
1899
|
+
}
|
|
1900
|
+
async function writeJson(file, value, pretty = false) {
|
|
1901
|
+
await writeFile(
|
|
1902
|
+
file,
|
|
1903
|
+
pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
async function loadTake(dir) {
|
|
1907
|
+
const paths = takePaths(dir);
|
|
1908
|
+
if (!existsSync(paths.meta)) {
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
`${dir} is not a take directory (missing meta.json) \u2014 run record first`
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
const meta = await readJson(paths.meta);
|
|
1914
|
+
const cursor = existsSync(paths.cursor) ? await readJson(paths.cursor) : [];
|
|
1915
|
+
const actions = existsSync(paths.actions) ? await readJson(paths.actions) : null;
|
|
1916
|
+
const doc = existsSync(paths.doc) ? await readJson(paths.doc) : null;
|
|
1917
|
+
return { paths, cursor, meta, actions, doc };
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/plugin/media.ts
|
|
1921
|
+
var SOURCE_MEDIA = [
|
|
1922
|
+
{ key: "videoKey", stem: "recording", fallbackExt: ".webm" },
|
|
1923
|
+
{ key: "micKey", stem: "mic", fallbackExt: ".webm" },
|
|
1924
|
+
{ key: "camKey", stem: "cam", fallbackExt: ".webm" }
|
|
1925
|
+
];
|
|
1926
|
+
function isTakeRelativeKey(key) {
|
|
1927
|
+
return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
|
|
1928
|
+
}
|
|
1929
|
+
var takeRelativeFile = (key) => key.replace(/^\/+/, "");
|
|
1930
|
+
function docMediaRefs(doc, keep = isTakeRelativeKey) {
|
|
1931
|
+
const out = [];
|
|
1932
|
+
for (const m of doc.media ?? []) {
|
|
1933
|
+
if (keep(m.videoKey))
|
|
1934
|
+
out.push({
|
|
1935
|
+
where: `media ${m.id}`,
|
|
1936
|
+
key: m.videoKey,
|
|
1937
|
+
set: (next) => {
|
|
1938
|
+
m.videoKey = next;
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
const mic = m.micKey;
|
|
1942
|
+
if (mic && keep(mic))
|
|
1943
|
+
out.push({
|
|
1944
|
+
where: `media ${m.id} mic`,
|
|
1945
|
+
key: mic,
|
|
1946
|
+
set: (next) => {
|
|
1947
|
+
m.micKey = next;
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
const cam = m.camKey;
|
|
1951
|
+
if (cam && keep(cam))
|
|
1952
|
+
out.push({
|
|
1953
|
+
where: `media ${m.id} cam`,
|
|
1954
|
+
key: cam,
|
|
1955
|
+
set: (next) => {
|
|
1956
|
+
m.camKey = next;
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
for (const clip of doc.overlays ?? []) {
|
|
1961
|
+
if (!isKeyedOverlay(clip) || !keep(clip.key)) continue;
|
|
1962
|
+
out.push({
|
|
1963
|
+
where: `overlay ${clip.id}`,
|
|
1964
|
+
key: clip.key,
|
|
1965
|
+
set: (next) => {
|
|
1966
|
+
clip.key = next;
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
const mark = doc.endCard?.mark;
|
|
1971
|
+
if (mark && keep(mark.key))
|
|
1972
|
+
out.push({
|
|
1973
|
+
where: "end card mark",
|
|
1974
|
+
key: mark.key,
|
|
1975
|
+
set: (next) => {
|
|
1976
|
+
mark.key = next;
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
const bg = doc.frame.backgroundMedia;
|
|
1980
|
+
if (bg && keep(bg.key))
|
|
1981
|
+
out.push({
|
|
1982
|
+
where: "background",
|
|
1983
|
+
key: bg.key,
|
|
1984
|
+
set: (next) => {
|
|
1985
|
+
bg.key = next;
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
return out;
|
|
1989
|
+
}
|
|
1990
|
+
function assetIdOf(url) {
|
|
1991
|
+
if (!url) return null;
|
|
1992
|
+
const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
|
|
1993
|
+
return m ? m[1] : null;
|
|
1994
|
+
}
|
|
1995
|
+
async function readMediaHead(file) {
|
|
1996
|
+
const handle = await open(file, "r");
|
|
1997
|
+
try {
|
|
1998
|
+
const buf = Buffer.alloc(MEDIA_HEAD_BYTES);
|
|
1999
|
+
const { bytesRead } = await handle.read(buf, 0, MEDIA_HEAD_BYTES, 0);
|
|
2000
|
+
return new Uint8Array(buf.subarray(0, bytesRead));
|
|
2001
|
+
} finally {
|
|
2002
|
+
await handle.close();
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
async function downloadMedia(ctx, url, target) {
|
|
2006
|
+
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
2007
|
+
const res = await fetch(abs, {
|
|
2008
|
+
headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
|
|
2009
|
+
});
|
|
2010
|
+
if (!res.ok || !res.body) {
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
`download of ${target.describe} failed (${res.status})${res.status === 404 && target.notFoundHint ? ` \u2014 ${target.notFoundHint}` : ""}`
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
const into = target.subdir ? join2(target.dir, target.subdir) : target.dir;
|
|
2016
|
+
const partial = join2(into, `${target.stem}.part`);
|
|
2017
|
+
await mkdir2(dirname(partial), { recursive: true });
|
|
2018
|
+
try {
|
|
2019
|
+
await pipeline(
|
|
2020
|
+
Readable.fromWeb(res.body),
|
|
2021
|
+
createWriteStream(partial)
|
|
2022
|
+
);
|
|
2023
|
+
const { type } = resolveMediaType({
|
|
2024
|
+
head: await readMediaHead(partial),
|
|
2025
|
+
declared: res.headers.get("content-type"),
|
|
2026
|
+
filename: `${target.stem}${target.fallbackExt}`
|
|
2027
|
+
});
|
|
2028
|
+
const name = `${target.stem}${extensionFor(type) || target.fallbackExt}`;
|
|
2029
|
+
await rename2(partial, join2(into, name));
|
|
2030
|
+
const file = target.subdir ? `${target.subdir}/${name}` : name;
|
|
2031
|
+
return { file, bytes: (await stat(join2(target.dir, file))).size };
|
|
2032
|
+
} finally {
|
|
2033
|
+
await rm2(partial, { force: true });
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
async function pullMedia(ctx, dir, doc, log) {
|
|
2037
|
+
const result = { downloaded: [], kept: [] };
|
|
2038
|
+
for (const { key, stem, fallbackExt } of SOURCE_MEDIA) {
|
|
2039
|
+
const url = doc.source[key];
|
|
2040
|
+
const assetId = assetIdOf(url);
|
|
2041
|
+
if (!assetId) continue;
|
|
2042
|
+
const present = takeMediaFiles(dir, stem)[0];
|
|
2043
|
+
if (present) {
|
|
2044
|
+
result.kept.push(basename2(present));
|
|
2045
|
+
doc.source[key] = basename2(present);
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
const got = await downloadMedia(ctx, url, {
|
|
2049
|
+
dir,
|
|
2050
|
+
stem,
|
|
2051
|
+
fallbackExt,
|
|
2052
|
+
describe: stem,
|
|
2053
|
+
notFoundHint: "a private recording needs a content key of its owner"
|
|
2054
|
+
});
|
|
2055
|
+
result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
|
|
2056
|
+
log(` ${got.file} \u2190 asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`);
|
|
2057
|
+
doc.source[key] = got.file;
|
|
2058
|
+
}
|
|
2059
|
+
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
2060
|
+
const assetId = assetIdOf(ref.key);
|
|
2061
|
+
const existing = existsSync2(join2(dir, "media")) ? (await readdir(join2(dir, "media"))).find(
|
|
2062
|
+
(f) => f === assetId || f.startsWith(`${assetId}.`)
|
|
2063
|
+
) : void 0;
|
|
2064
|
+
if (existing) {
|
|
2065
|
+
const file = `media/${existing}`;
|
|
2066
|
+
result.kept.push(file);
|
|
2067
|
+
ref.set(file);
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
const got = await downloadMedia(ctx, ref.key, {
|
|
2071
|
+
dir,
|
|
2072
|
+
subdir: "media",
|
|
2073
|
+
stem: assetId,
|
|
2074
|
+
// A type with no extension here (an svg served as such) keeps the bare
|
|
2075
|
+
// asset id, exactly as before.
|
|
2076
|
+
fallbackExt: "",
|
|
2077
|
+
describe: `${ref.where} (${ref.key})`
|
|
2078
|
+
});
|
|
2079
|
+
result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
|
|
2080
|
+
log(
|
|
2081
|
+
` ${got.file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`
|
|
2082
|
+
);
|
|
2083
|
+
ref.set(got.file);
|
|
2084
|
+
}
|
|
2085
|
+
const metaPath = join2(dir, "meta.json");
|
|
2086
|
+
if (!existsSync2(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
2087
|
+
const cursorPath = join2(dir, "cursor.json");
|
|
2088
|
+
if (!existsSync2(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
2089
|
+
await writeFile2(join2(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
2090
|
+
return result;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1684
2093
|
// src/plugin/mediaProbe.ts
|
|
1685
2094
|
var MEDIA_EXT = /\.(png|jpe?g|webp|gif|webm|mp4|mov|mp3|wav|m4a|ogg|glb|gltf|hdr|exr|woff2?)($|\?)/i;
|
|
1686
2095
|
var isRemote = (v) => typeof v === "string" && /^https?:\/\//i.test(v);
|
|
@@ -1766,9 +2175,9 @@ function mediaProbeLints(results) {
|
|
|
1766
2175
|
}
|
|
1767
2176
|
|
|
1768
2177
|
// src/plugin/framesTake.ts
|
|
1769
|
-
import { mkdir as
|
|
1770
|
-
import { existsSync as
|
|
1771
|
-
import { join as
|
|
2178
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile5 } from "fs/promises";
|
|
2179
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2180
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
1772
2181
|
import { compileVosConfig } from "@vosjs/core";
|
|
1773
2182
|
import {
|
|
1774
2183
|
docStillTime,
|
|
@@ -1780,83 +2189,11 @@ import {
|
|
|
1780
2189
|
spanOutputExtent as spanOutputExtent2
|
|
1781
2190
|
} from "@vosjs/studio-core";
|
|
1782
2191
|
|
|
1783
|
-
// src/plugin/take.ts
|
|
1784
|
-
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1785
|
-
import { existsSync } from "fs";
|
|
1786
|
-
import { join } from "path";
|
|
1787
|
-
var RECORDING_NAME = "recording.webm";
|
|
1788
|
-
var PREV_DOC_NAME = "doc.prev.json";
|
|
1789
|
-
function takePaths(dir) {
|
|
1790
|
-
return {
|
|
1791
|
-
dir,
|
|
1792
|
-
recording: join(dir, RECORDING_NAME),
|
|
1793
|
-
framesDir: join(dir, "frames"),
|
|
1794
|
-
framesIndex: join(dir, "frames.json"),
|
|
1795
|
-
cursor: join(dir, "cursor.json"),
|
|
1796
|
-
meta: join(dir, "meta.json"),
|
|
1797
|
-
actions: join(dir, "actions.json"),
|
|
1798
|
-
doc: join(dir, "doc.json")
|
|
1799
|
-
};
|
|
1800
|
-
}
|
|
1801
|
-
async function ensureTakeDir(dir) {
|
|
1802
|
-
const p = takePaths(dir);
|
|
1803
|
-
await mkdir(p.framesDir, { recursive: true });
|
|
1804
|
-
return p;
|
|
1805
|
-
}
|
|
1806
|
-
async function prepareReRecord(dir) {
|
|
1807
|
-
const p = takePaths(dir);
|
|
1808
|
-
const kept = [];
|
|
1809
|
-
let prevDoc = false;
|
|
1810
|
-
if (existsSync(p.doc)) {
|
|
1811
|
-
await rename(p.doc, join(dir, PREV_DOC_NAME));
|
|
1812
|
-
prevDoc = true;
|
|
1813
|
-
kept.push(PREV_DOC_NAME);
|
|
1814
|
-
}
|
|
1815
|
-
if (existsSync(p.actions)) kept.push("actions.json");
|
|
1816
|
-
if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
|
|
1817
|
-
for (const stale of [
|
|
1818
|
-
p.recording,
|
|
1819
|
-
p.cursor,
|
|
1820
|
-
p.meta,
|
|
1821
|
-
p.framesIndex,
|
|
1822
|
-
p.framesDir,
|
|
1823
|
-
join(dir, "stills"),
|
|
1824
|
-
join(dir, "digest"),
|
|
1825
|
-
join(dir, "mic.webm"),
|
|
1826
|
-
join(dir, "cam.webm")
|
|
1827
|
-
]) {
|
|
1828
|
-
await rm(stale, { recursive: true, force: true });
|
|
1829
|
-
}
|
|
1830
|
-
return { prevDoc, kept };
|
|
1831
|
-
}
|
|
1832
|
-
async function readJson(file) {
|
|
1833
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
1834
|
-
}
|
|
1835
|
-
async function writeJson(file, value, pretty = false) {
|
|
1836
|
-
await writeFile(
|
|
1837
|
-
file,
|
|
1838
|
-
pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
async function loadTake(dir) {
|
|
1842
|
-
const paths = takePaths(dir);
|
|
1843
|
-
if (!existsSync(paths.meta)) {
|
|
1844
|
-
throw new Error(
|
|
1845
|
-
`${dir} is not a take directory (missing meta.json) \u2014 run record first`
|
|
1846
|
-
);
|
|
1847
|
-
}
|
|
1848
|
-
const meta = await readJson(paths.meta);
|
|
1849
|
-
const cursor = existsSync(paths.cursor) ? await readJson(paths.cursor) : [];
|
|
1850
|
-
const actions = existsSync(paths.actions) ? await readJson(paths.actions) : null;
|
|
1851
|
-
const doc = existsSync(paths.doc) ? await readJson(paths.doc) : null;
|
|
1852
|
-
return { paths, cursor, meta, actions, doc };
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
2192
|
// src/plugin/server.ts
|
|
1856
2193
|
import { createServer } from "http";
|
|
1857
|
-
import { existsSync as
|
|
1858
|
-
import { writeFile as
|
|
1859
|
-
import { extname, join as
|
|
2194
|
+
import { existsSync as existsSync3, readFileSync, statSync as statSync2 } from "fs";
|
|
2195
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2196
|
+
import { extname, join as join3 } from "path";
|
|
1860
2197
|
var MIME = {
|
|
1861
2198
|
".html": "text/html",
|
|
1862
2199
|
".js": "text/javascript",
|
|
@@ -1890,7 +2227,7 @@ function startTakeServer(rootDir, pages) {
|
|
|
1890
2227
|
const chunks = [];
|
|
1891
2228
|
req.on("data", (c) => chunks.push(c));
|
|
1892
2229
|
req.on("end", () => {
|
|
1893
|
-
void
|
|
2230
|
+
void writeFile3(join3(rootDir, name), Buffer.concat(chunks)).then(
|
|
1894
2231
|
() => res.writeHead(200).end("ok"),
|
|
1895
2232
|
(e) => res.writeHead(500).end(String(e))
|
|
1896
2233
|
);
|
|
@@ -1902,11 +2239,11 @@ function startTakeServer(rootDir, pages) {
|
|
|
1902
2239
|
res.end(pages[url.pathname]);
|
|
1903
2240
|
return;
|
|
1904
2241
|
}
|
|
1905
|
-
const file =
|
|
1906
|
-
if (
|
|
1907
|
-
const
|
|
1908
|
-
const size = statSync(file).size;
|
|
2242
|
+
const file = join3(rootDir, decodeURIComponent(url.pathname));
|
|
2243
|
+
if (existsSync3(file) && statSync2(file).isFile()) {
|
|
2244
|
+
const size = statSync2(file).size;
|
|
1909
2245
|
const buf = readFileSync(file);
|
|
2246
|
+
const type = sniffMediaType(buf.subarray(0, MEDIA_HEAD_BYTES)) ?? MIME[extname(file)] ?? "application/octet-stream";
|
|
1910
2247
|
const range = req.headers.range;
|
|
1911
2248
|
const m = range && /^bytes=(\d*)-(\d*)$/.exec(range);
|
|
1912
2249
|
if (m) {
|
|
@@ -1969,7 +2306,7 @@ async function waitForPageDone(page, label, onProgress, timeoutMs) {
|
|
|
1969
2306
|
}
|
|
1970
2307
|
|
|
1971
2308
|
// src/plugin/renderAnimation.ts
|
|
1972
|
-
import { readFile as readFile2, rm as
|
|
2309
|
+
import { readFile as readFile2, rm as rm3 } from "fs/promises";
|
|
1973
2310
|
import { tweenRuntimeCode } from "@vosjs/tween/bundle";
|
|
1974
2311
|
import { elementsBundleCode } from "@vosjs/elements/bundle";
|
|
1975
2312
|
import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
|
|
@@ -2267,7 +2604,7 @@ async function renderAnimation(browser, opts) {
|
|
|
2267
2604
|
outNames.map((name) => readFile2(`${opts.workDir}/${name}`))
|
|
2268
2605
|
);
|
|
2269
2606
|
await Promise.all(
|
|
2270
|
-
outNames.map((name) =>
|
|
2607
|
+
outNames.map((name) => rm3(`${opts.workDir}/${name}`, { force: true }))
|
|
2271
2608
|
);
|
|
2272
2609
|
if (chunks.length === 1) {
|
|
2273
2610
|
return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
|
|
@@ -2522,9 +2859,9 @@ function applyAndValidate(doc, o) {
|
|
|
2522
2859
|
}
|
|
2523
2860
|
|
|
2524
2861
|
// src/plugin/digestTake.ts
|
|
2525
|
-
import { mkdir as
|
|
2526
|
-
import { existsSync as
|
|
2527
|
-
import { join as
|
|
2862
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile4 } from "fs/promises";
|
|
2863
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2864
|
+
import { join as join4, resolve } from "path";
|
|
2528
2865
|
import {
|
|
2529
2866
|
DIGEST_CROP_MAX,
|
|
2530
2867
|
DIGEST_FULL_MAX,
|
|
@@ -2746,8 +3083,8 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2746
3083
|
const meta = doc.source.meta;
|
|
2747
3084
|
let plan = planForDigest(doc);
|
|
2748
3085
|
const transcript = opts.transcript ?? await discoverTranscript(dir);
|
|
2749
|
-
const outDir = resolve(opts.outDir ??
|
|
2750
|
-
await
|
|
3086
|
+
const outDir = resolve(opts.outDir ?? join4(dir, DIGEST_DIR));
|
|
3087
|
+
await mkdir3(outDir, { recursive: true });
|
|
2751
3088
|
const fullMax = opts.full ?? DIGEST_FULL_MAX;
|
|
2752
3089
|
const cropMax = opts.crop ?? DIGEST_CROP_MAX;
|
|
2753
3090
|
const wantFrames = opts.frames !== false && browser !== null;
|
|
@@ -2759,8 +3096,11 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2759
3096
|
let sheet = null;
|
|
2760
3097
|
let moments;
|
|
2761
3098
|
const boxes = /* @__PURE__ */ new Map();
|
|
2762
|
-
let source = {
|
|
2763
|
-
|
|
3099
|
+
let source = {
|
|
3100
|
+
kind: "video",
|
|
3101
|
+
url: `/${take.paths.recordingName}`
|
|
3102
|
+
};
|
|
3103
|
+
if (existsSync4(take.paths.framesIndex) && existsSync4(take.paths.framesDir)) {
|
|
2764
3104
|
const index = await readJson(
|
|
2765
3105
|
take.paths.framesIndex
|
|
2766
3106
|
);
|
|
@@ -2775,9 +3115,9 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2775
3115
|
}
|
|
2776
3116
|
}
|
|
2777
3117
|
if (wantFrames) {
|
|
2778
|
-
if (source.kind === "video" && !
|
|
3118
|
+
if (source.kind === "video" && !existsSync4(take.paths.recording))
|
|
2779
3119
|
throw new Error(
|
|
2780
|
-
`${dir} has no
|
|
3120
|
+
`${dir} has no recording \u2014 pull it with \`vos pull --media\` first`
|
|
2781
3121
|
);
|
|
2782
3122
|
const server = await startTakeServer(dir, {
|
|
2783
3123
|
"/digest.html": digestPageHtml()
|
|
@@ -2848,9 +3188,9 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2848
3188
|
);
|
|
2849
3189
|
sizes = f.sizes;
|
|
2850
3190
|
for (const name of Object.keys(sizes)) {
|
|
2851
|
-
const from =
|
|
2852
|
-
const to =
|
|
2853
|
-
if (from !== to &&
|
|
3191
|
+
const from = join4(dir, name);
|
|
3192
|
+
const to = join4(outDir, name);
|
|
3193
|
+
if (from !== to && existsSync4(from)) await rename3(from, to);
|
|
2854
3194
|
}
|
|
2855
3195
|
if (sizes["sheet.png"]) sheet = "sheet.png";
|
|
2856
3196
|
} finally {
|
|
@@ -2876,12 +3216,12 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2876
3216
|
if (!p) continue;
|
|
2877
3217
|
for (const f of [p.full, p.crop]) {
|
|
2878
3218
|
if (!f) continue;
|
|
2879
|
-
const size = await pngSize(
|
|
3219
|
+
const size = await pngSize(join4(outDir, f));
|
|
2880
3220
|
if (size) sizes[f] = size;
|
|
2881
3221
|
}
|
|
2882
3222
|
if (p.box) boxes.set(m.id, p.box);
|
|
2883
3223
|
}
|
|
2884
|
-
if (prior.images.sheet &&
|
|
3224
|
+
if (prior.images.sheet && existsSync4(join4(outDir, prior.images.sheet)))
|
|
2885
3225
|
sheet = prior.images.sheet;
|
|
2886
3226
|
}
|
|
2887
3227
|
}
|
|
@@ -2910,14 +3250,14 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2910
3250
|
style: opts.style ?? null,
|
|
2911
3251
|
transcript
|
|
2912
3252
|
});
|
|
2913
|
-
const file =
|
|
3253
|
+
const file = join4(outDir, "digest.json");
|
|
2914
3254
|
const body = JSON.stringify(digest, null, 2);
|
|
2915
|
-
await
|
|
3255
|
+
await writeFile4(file, body);
|
|
2916
3256
|
return { file, outDir, digest, bytes: Buffer.byteLength(body) };
|
|
2917
3257
|
}
|
|
2918
3258
|
async function discoverTranscript(dir) {
|
|
2919
|
-
const file =
|
|
2920
|
-
if (!
|
|
3259
|
+
const file = join4(dir, "transcript.json");
|
|
3260
|
+
if (!existsSync4(file)) return null;
|
|
2921
3261
|
try {
|
|
2922
3262
|
return parseTranscript(JSON.parse(await readFile3(file, "utf8")));
|
|
2923
3263
|
} catch {
|
|
@@ -2927,7 +3267,7 @@ async function discoverTranscript(dir) {
|
|
|
2927
3267
|
async function readPriorDigest(outDir) {
|
|
2928
3268
|
try {
|
|
2929
3269
|
const raw = JSON.parse(
|
|
2930
|
-
await readFile3(
|
|
3270
|
+
await readFile3(join4(outDir, "digest.json"), "utf8")
|
|
2931
3271
|
);
|
|
2932
3272
|
return raw.digestVersion === DIGEST_VERSION && Array.isArray(raw.moments) ? raw : null;
|
|
2933
3273
|
} catch {
|
|
@@ -3111,8 +3451,8 @@ async function framesTake(browser, dir, opts) {
|
|
|
3111
3451
|
throw new Error(
|
|
3112
3452
|
"no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
|
|
3113
3453
|
);
|
|
3114
|
-
const outDir = resolve2(opts.outDir ??
|
|
3115
|
-
await
|
|
3454
|
+
const outDir = resolve2(opts.outDir ?? join5(dir, "stills"));
|
|
3455
|
+
await mkdir4(outDir, { recursive: true });
|
|
3116
3456
|
const named = shots.map((s, i) => ({
|
|
3117
3457
|
...s,
|
|
3118
3458
|
name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${String(i).padStart(2, "0")}-${s.time.toFixed(2)}s.png`
|
|
@@ -3140,7 +3480,7 @@ async function framesTake(browser, dir, opts) {
|
|
|
3140
3480
|
void page.evaluate(stillsInPage, {
|
|
3141
3481
|
animationCode,
|
|
3142
3482
|
token: VIDEO_TOKEN,
|
|
3143
|
-
videoUrl: `/${
|
|
3483
|
+
videoUrl: `/${take.paths.recordingName}`,
|
|
3144
3484
|
W: width,
|
|
3145
3485
|
H: height,
|
|
3146
3486
|
ss,
|
|
@@ -3156,9 +3496,9 @@ async function framesTake(browser, dir, opts) {
|
|
|
3156
3496
|
);
|
|
3157
3497
|
const frames = [];
|
|
3158
3498
|
for (const n of named) {
|
|
3159
|
-
const from =
|
|
3160
|
-
const to =
|
|
3161
|
-
if (from !== to) await
|
|
3499
|
+
const from = join5(dir, n.name);
|
|
3500
|
+
const to = join5(outDir, n.name);
|
|
3501
|
+
if (from !== to) await rename4(from, to);
|
|
3162
3502
|
frames.push({
|
|
3163
3503
|
file: to,
|
|
3164
3504
|
time: n.time,
|
|
@@ -3173,8 +3513,8 @@ async function framesTake(browser, dir, opts) {
|
|
|
3173
3513
|
}
|
|
3174
3514
|
}
|
|
3175
3515
|
async function momentsFor(dir, doc) {
|
|
3176
|
-
const file =
|
|
3177
|
-
if (
|
|
3516
|
+
const file = join5(dir, DIGEST_DIR, "digest.json");
|
|
3517
|
+
if (existsSync5(file)) {
|
|
3178
3518
|
try {
|
|
3179
3519
|
const d = JSON.parse(await readFile4(file, "utf8"));
|
|
3180
3520
|
if (Array.isArray(d.moments)) return d.moments;
|
|
@@ -3196,8 +3536,8 @@ function parseTimes(spec, duration) {
|
|
|
3196
3536
|
});
|
|
3197
3537
|
}
|
|
3198
3538
|
async function writeIndexJson(result) {
|
|
3199
|
-
const index =
|
|
3200
|
-
await
|
|
3539
|
+
const index = join5(result.outDir, "stills.json");
|
|
3540
|
+
await writeFile5(
|
|
3201
3541
|
index,
|
|
3202
3542
|
JSON.stringify(
|
|
3203
3543
|
{
|
|
@@ -3214,10 +3554,10 @@ async function writeIndexJson(result) {
|
|
|
3214
3554
|
}
|
|
3215
3555
|
|
|
3216
3556
|
// src/plugin/deliver.ts
|
|
3217
|
-
import { mkdir as
|
|
3557
|
+
import { mkdir as mkdir5, mkdtemp, rename as rename5, rm as rm4, stat as stat2, writeFile as writeFile7 } from "fs/promises";
|
|
3218
3558
|
import { tmpdir } from "os";
|
|
3219
|
-
import { join as
|
|
3220
|
-
import { existsSync as
|
|
3559
|
+
import { join as join8, relative, resolve as resolve4 } from "path";
|
|
3560
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3221
3561
|
import { readFile as readFile6 } from "fs/promises";
|
|
3222
3562
|
import { parseFrontmatter } from "@vosjs/shared/frontmatter";
|
|
3223
3563
|
import {
|
|
@@ -3232,7 +3572,7 @@ import {
|
|
|
3232
3572
|
} from "@vosjs/studio-core";
|
|
3233
3573
|
|
|
3234
3574
|
// src/plugin/renderTake.ts
|
|
3235
|
-
import { writeFile as
|
|
3575
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
3236
3576
|
import { compileVosConfig as compileVosConfig2 } from "@vosjs/core";
|
|
3237
3577
|
import {
|
|
3238
3578
|
audioProducerCode,
|
|
@@ -3304,7 +3644,7 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
3304
3644
|
const result = await renderAnimation(browser, {
|
|
3305
3645
|
animationCode,
|
|
3306
3646
|
workDir: dir,
|
|
3307
|
-
videoFile:
|
|
3647
|
+
videoFile: take.paths.recordingName,
|
|
3308
3648
|
videoToken: VIDEO_TOKEN2,
|
|
3309
3649
|
width,
|
|
3310
3650
|
height,
|
|
@@ -3321,7 +3661,7 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
3321
3661
|
} : void 0,
|
|
3322
3662
|
onProgress: opts.onProgress
|
|
3323
3663
|
});
|
|
3324
|
-
await
|
|
3664
|
+
await writeFile6(outFile, result.bytes);
|
|
3325
3665
|
return {
|
|
3326
3666
|
out: outFile,
|
|
3327
3667
|
bytes: result.bytes.length,
|
|
@@ -4197,9 +4537,9 @@ function isLightHexGround(hex2) {
|
|
|
4197
4537
|
}
|
|
4198
4538
|
|
|
4199
4539
|
// src/plugin/posterDoc.ts
|
|
4200
|
-
import { existsSync as
|
|
4540
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4201
4541
|
import { readFile as readFile5 } from "fs/promises";
|
|
4202
|
-
import { isAbsolute, join as
|
|
4542
|
+
import { isAbsolute, join as join7, resolve as resolve3 } from "path";
|
|
4203
4543
|
import {
|
|
4204
4544
|
computeCardLayout,
|
|
4205
4545
|
docStillTime as docStillTime2,
|
|
@@ -4211,7 +4551,7 @@ import {
|
|
|
4211
4551
|
// src/plugin/platform.ts
|
|
4212
4552
|
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4213
4553
|
import { homedir } from "os";
|
|
4214
|
-
import { join as
|
|
4554
|
+
import { join as join6 } from "path";
|
|
4215
4555
|
function platformOrigin(flags = {}) {
|
|
4216
4556
|
const legacyEnv = process.env.VOS_API_BASE?.trim();
|
|
4217
4557
|
const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
|
|
@@ -4244,7 +4584,7 @@ function parseVosId(input) {
|
|
|
4244
4584
|
function deriveSlug(title) {
|
|
4245
4585
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
|
|
4246
4586
|
}
|
|
4247
|
-
var CREDENTIALS_PATH =
|
|
4587
|
+
var CREDENTIALS_PATH = join6(homedir(), ".config", "vos", "credentials");
|
|
4248
4588
|
function resolveCredential(explicit) {
|
|
4249
4589
|
const flag = explicit?.trim();
|
|
4250
4590
|
if (flag) return flag;
|
|
@@ -4267,7 +4607,7 @@ function requireCredential(explicit) {
|
|
|
4267
4607
|
return key;
|
|
4268
4608
|
}
|
|
4269
4609
|
function writeCredential(key) {
|
|
4270
|
-
mkdirSync(
|
|
4610
|
+
mkdirSync(join6(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
|
|
4271
4611
|
writeFileSync(CREDENTIALS_PATH, `${key.trim()}
|
|
4272
4612
|
`, { mode: 384 });
|
|
4273
4613
|
return CREDENTIALS_PATH;
|
|
@@ -4311,7 +4651,7 @@ function readJsonFile(file) {
|
|
|
4311
4651
|
}
|
|
4312
4652
|
}
|
|
4313
4653
|
function readSyncState(dir) {
|
|
4314
|
-
const own = readJsonFile(
|
|
4654
|
+
const own = readJsonFile(join6(dir, SYNC_STATE_NAME));
|
|
4315
4655
|
if (own && typeof own.vosId === "string") {
|
|
4316
4656
|
return {
|
|
4317
4657
|
vosId: own.vosId,
|
|
@@ -4322,7 +4662,7 @@ function readSyncState(dir) {
|
|
|
4322
4662
|
...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
|
|
4323
4663
|
};
|
|
4324
4664
|
}
|
|
4325
|
-
const push = readJsonFile(
|
|
4665
|
+
const push = readJsonFile(join6(dir, "push.json"));
|
|
4326
4666
|
if (push && typeof push.vosId === "string") {
|
|
4327
4667
|
return {
|
|
4328
4668
|
vosId: push.vosId,
|
|
@@ -4330,7 +4670,7 @@ function readSyncState(dir) {
|
|
|
4330
4670
|
...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
|
|
4331
4671
|
};
|
|
4332
4672
|
}
|
|
4333
|
-
const meta = readJsonFile(
|
|
4673
|
+
const meta = readJsonFile(join6(dir, "meta.json"));
|
|
4334
4674
|
if (meta && typeof meta.id === "string") {
|
|
4335
4675
|
return {
|
|
4336
4676
|
vosId: meta.id,
|
|
@@ -4348,7 +4688,7 @@ function writeSyncState(dir, patch) {
|
|
|
4348
4688
|
...patch
|
|
4349
4689
|
};
|
|
4350
4690
|
writeFileSync(
|
|
4351
|
-
|
|
4691
|
+
join6(dir, SYNC_STATE_NAME),
|
|
4352
4692
|
`${JSON.stringify(merged, null, 2)}
|
|
4353
4693
|
`
|
|
4354
4694
|
);
|
|
@@ -4387,9 +4727,9 @@ var roleFor = (cls) => `poster-${cls}`;
|
|
|
4387
4727
|
async function readPosterAt(cls, path, takeDir, from) {
|
|
4388
4728
|
let file = path;
|
|
4389
4729
|
let docDir = path;
|
|
4390
|
-
if (
|
|
4391
|
-
file =
|
|
4392
|
-
} else if (!/\.json$/i.test(path) || !
|
|
4730
|
+
if (existsSync6(join7(path, "doc.json"))) {
|
|
4731
|
+
file = join7(path, "doc.json");
|
|
4732
|
+
} else if (!/\.json$/i.test(path) || !existsSync6(path)) {
|
|
4393
4733
|
return null;
|
|
4394
4734
|
} else {
|
|
4395
4735
|
docDir = resolve3(path, "..");
|
|
@@ -4406,7 +4746,7 @@ async function readPosterAt(cls, path, takeDir, from) {
|
|
|
4406
4746
|
const doc = migrateHostedDoc(
|
|
4407
4747
|
raw
|
|
4408
4748
|
);
|
|
4409
|
-
const ownFootage =
|
|
4749
|
+
const ownFootage = existsSync6(join7(docDir, "meta.json")) && existsSync6(join7(docDir, source.videoKey));
|
|
4410
4750
|
const sync = readSyncState(docDir);
|
|
4411
4751
|
return {
|
|
4412
4752
|
cls,
|
|
@@ -4429,10 +4769,10 @@ async function findPosterDocs(takeDir, launchRoles) {
|
|
|
4429
4769
|
candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
|
|
4430
4770
|
}
|
|
4431
4771
|
candidates.push({
|
|
4432
|
-
path:
|
|
4772
|
+
path: join7(takeDir, "poster", cls),
|
|
4433
4773
|
from: `poster/${cls}`
|
|
4434
4774
|
});
|
|
4435
|
-
candidates.push({ path:
|
|
4775
|
+
candidates.push({ path: join7(takeDir, "poster"), from: "poster" });
|
|
4436
4776
|
for (const c of candidates) {
|
|
4437
4777
|
const ref = await readPosterAt(cls, c.path, takeDir, c.from);
|
|
4438
4778
|
if (ref) {
|
|
@@ -4528,18 +4868,18 @@ function resolveChannels(raw) {
|
|
|
4528
4868
|
return out;
|
|
4529
4869
|
}
|
|
4530
4870
|
async function readBrandBesideTake(dir, explicit) {
|
|
4531
|
-
const candidates = explicit ? [explicit] : [
|
|
4871
|
+
const candidates = explicit ? [explicit] : [join8(dir, "BRAND.md"), join8(dir, "..", "BRAND.md")];
|
|
4532
4872
|
for (const file of candidates) {
|
|
4533
|
-
if (!
|
|
4873
|
+
if (!existsSync7(file)) continue;
|
|
4534
4874
|
const roles = parseFrontmatter(await readFile6(file, "utf8"));
|
|
4535
4875
|
return { file, roles };
|
|
4536
4876
|
}
|
|
4537
4877
|
return null;
|
|
4538
4878
|
}
|
|
4539
4879
|
async function readLaunchBesideTake(dir, explicit) {
|
|
4540
|
-
const candidates = explicit ? [explicit] : [
|
|
4880
|
+
const candidates = explicit ? [explicit] : [join8(dir, "LAUNCH.md"), join8(dir, "..", "LAUNCH.md")];
|
|
4541
4881
|
for (const file of candidates) {
|
|
4542
|
-
if (!
|
|
4882
|
+
if (!existsSync7(file)) continue;
|
|
4543
4883
|
return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
|
|
4544
4884
|
}
|
|
4545
4885
|
return null;
|
|
@@ -4604,7 +4944,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4604
4944
|
const meta = doc.source.meta;
|
|
4605
4945
|
const vw = meta.captureWidth ?? meta.width;
|
|
4606
4946
|
const vh = meta.captureHeight ?? meta.height;
|
|
4607
|
-
const probeDir = await mkdtemp(
|
|
4947
|
+
const probeDir = await mkdtemp(join8(tmpdir(), "vos-moments-"));
|
|
4608
4948
|
try {
|
|
4609
4949
|
const probe = await framesTake(browser, dir, {
|
|
4610
4950
|
times: candidates.map((c) => c.time),
|
|
@@ -4643,7 +4983,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4643
4983
|
notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
|
|
4644
4984
|
};
|
|
4645
4985
|
} finally {
|
|
4646
|
-
await
|
|
4986
|
+
await rm4(probeDir, { recursive: true, force: true });
|
|
4647
4987
|
}
|
|
4648
4988
|
}
|
|
4649
4989
|
var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
|
|
@@ -4691,8 +5031,8 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4691
5031
|
const doc = take.doc;
|
|
4692
5032
|
const duration = docOutputDuration2(doc);
|
|
4693
5033
|
const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
|
|
4694
|
-
const outDir = resolve4(opts.outDir ??
|
|
4695
|
-
await
|
|
5034
|
+
const outDir = resolve4(opts.outDir ?? join8(dir, "kit"));
|
|
5035
|
+
await mkdir5(outDir, { recursive: true });
|
|
4696
5036
|
const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
|
|
4697
5037
|
const assets = [];
|
|
4698
5038
|
const skipped = [];
|
|
@@ -4752,7 +5092,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4752
5092
|
opts.onPhase?.(
|
|
4753
5093
|
`${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
|
|
4754
5094
|
);
|
|
4755
|
-
const shotDir = await mkdtemp(
|
|
5095
|
+
const shotDir = await mkdtemp(join8(tmpdir(), "vos-poster-"));
|
|
4756
5096
|
try {
|
|
4757
5097
|
const captured = await framesTake(browser, ref.takeDir, {
|
|
4758
5098
|
times: [time],
|
|
@@ -4762,9 +5102,9 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4762
5102
|
doc: ref.doc,
|
|
4763
5103
|
overrides: opts.overrides
|
|
4764
5104
|
});
|
|
4765
|
-
const to =
|
|
4766
|
-
await
|
|
4767
|
-
const bytes = (await
|
|
5105
|
+
const to = join8(outDir, `${d.id}.png`);
|
|
5106
|
+
await rename5(captured.frames[0].file, to);
|
|
5107
|
+
const bytes = (await stat2(to)).size;
|
|
4768
5108
|
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4769
5109
|
skipped.push(
|
|
4770
5110
|
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
@@ -4793,7 +5133,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4793
5133
|
...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
|
|
4794
5134
|
});
|
|
4795
5135
|
} finally {
|
|
4796
|
-
await
|
|
5136
|
+
await rm4(shotDir, { recursive: true, force: true });
|
|
4797
5137
|
}
|
|
4798
5138
|
}
|
|
4799
5139
|
if (missing.size) {
|
|
@@ -4834,7 +5174,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4834
5174
|
}
|
|
4835
5175
|
}
|
|
4836
5176
|
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
4837
|
-
const outFile =
|
|
5177
|
+
const outFile = join8(outDir, `${d.id}.${d.format}`);
|
|
4838
5178
|
const bitrate = d.maxBytes !== void 0 ? Math.min(
|
|
4839
5179
|
1e7,
|
|
4840
5180
|
Math.floor(d.maxBytes * 8 / seconds * 0.85)
|
|
@@ -4881,9 +5221,9 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4881
5221
|
for (let i = 0; i < captured.frames.length; i++) {
|
|
4882
5222
|
const frame = captured.frames[i];
|
|
4883
5223
|
const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
|
|
4884
|
-
const to =
|
|
4885
|
-
await
|
|
4886
|
-
const bytes = (await
|
|
5224
|
+
const to = join8(outDir, name);
|
|
5225
|
+
await rename5(frame.file, to);
|
|
5226
|
+
const bytes = (await stat2(to)).size;
|
|
4887
5227
|
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4888
5228
|
skipped.push(
|
|
4889
5229
|
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
@@ -4919,15 +5259,15 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4919
5259
|
skipped,
|
|
4920
5260
|
assets
|
|
4921
5261
|
};
|
|
4922
|
-
const kitFile =
|
|
4923
|
-
await
|
|
5262
|
+
const kitFile = join8(outDir, "kit.json");
|
|
5263
|
+
await writeFile7(kitFile, JSON.stringify(kit, null, 2));
|
|
4924
5264
|
return { kit, kitFile, outDir };
|
|
4925
5265
|
}
|
|
4926
5266
|
|
|
4927
5267
|
// src/plugin/markAsset.ts
|
|
4928
|
-
import { mkdir as
|
|
4929
|
-
import { existsSync as
|
|
4930
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5268
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
|
|
5269
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5270
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
|
|
4931
5271
|
function markExtension(url) {
|
|
4932
5272
|
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4933
5273
|
if (!m) return null;
|
|
@@ -5009,7 +5349,7 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
5009
5349
|
bytes = new Uint8Array(await res.arrayBuffer());
|
|
5010
5350
|
} else {
|
|
5011
5351
|
const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
|
|
5012
|
-
if (!
|
|
5352
|
+
if (!existsSync8(file)) throw new Error("no such file");
|
|
5013
5353
|
bytes = new Uint8Array(await readFile7(file));
|
|
5014
5354
|
}
|
|
5015
5355
|
} catch (e) {
|
|
@@ -5025,8 +5365,8 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
5025
5365
|
);
|
|
5026
5366
|
return null;
|
|
5027
5367
|
}
|
|
5028
|
-
await
|
|
5029
|
-
await
|
|
5368
|
+
await mkdir6(join9(takeDir, "brand"), { recursive: true });
|
|
5369
|
+
await writeFile8(join9(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
5030
5370
|
return { key: `/brand/${name}.${ext}`, aspect };
|
|
5031
5371
|
};
|
|
5032
5372
|
out.light = await one("logoUrl", "mark");
|
|
@@ -5062,9 +5402,9 @@ async function fetchMusicCatalog(origin) {
|
|
|
5062
5402
|
}
|
|
5063
5403
|
|
|
5064
5404
|
// src/plugin/judge.ts
|
|
5065
|
-
import { existsSync as
|
|
5066
|
-
import { mkdir as
|
|
5067
|
-
import { dirname, isAbsolute as isAbsolute3, join as
|
|
5405
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5406
|
+
import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
|
|
5407
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join10, resolve as resolve6 } from "path";
|
|
5068
5408
|
import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
|
|
5069
5409
|
|
|
5070
5410
|
// src/plugin/shotBake.ts
|
|
@@ -5147,7 +5487,8 @@ function rolesFor(asset) {
|
|
|
5147
5487
|
const portrait = spec.px.w < spec.px.h;
|
|
5148
5488
|
return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
|
|
5149
5489
|
}
|
|
5150
|
-
if (spec?.genre === "card")
|
|
5490
|
+
if (spec?.genre === "card")
|
|
5491
|
+
return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
|
|
5151
5492
|
if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
|
|
5152
5493
|
return [];
|
|
5153
5494
|
}
|
|
@@ -5207,7 +5548,8 @@ function composeSheet(left, right, height = 540, gutter = 32) {
|
|
|
5207
5548
|
const si = (y * src.w + x) * 4;
|
|
5208
5549
|
const a = src.data[si + 3] / 255;
|
|
5209
5550
|
const o = ((y0 + y) * w + x0 + x) * 4;
|
|
5210
|
-
for (let c = 0; c < 3; c++)
|
|
5551
|
+
for (let c = 0; c < 3; c++)
|
|
5552
|
+
out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
|
|
5211
5553
|
}
|
|
5212
5554
|
}
|
|
5213
5555
|
};
|
|
@@ -5223,51 +5565,70 @@ function composeSheet(left, right, height = 540, gutter = 32) {
|
|
|
5223
5565
|
}
|
|
5224
5566
|
};
|
|
5225
5567
|
fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
|
|
5226
|
-
fill(
|
|
5568
|
+
fill(
|
|
5569
|
+
margin + lw + gutter,
|
|
5570
|
+
margin + lw + gutter + rw,
|
|
5571
|
+
margin + height + 6,
|
|
5572
|
+
margin + height + 6 + band,
|
|
5573
|
+
200
|
|
5574
|
+
);
|
|
5227
5575
|
return { w, h, data: out };
|
|
5228
5576
|
}
|
|
5229
5577
|
async function judgeKit(kitPath, manifestPath, outDir) {
|
|
5230
5578
|
const kit = JSON.parse(await readFile8(kitPath, "utf8"));
|
|
5231
|
-
const manifest = JSON.parse(
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
const
|
|
5235
|
-
|
|
5579
|
+
const manifest = JSON.parse(
|
|
5580
|
+
await readFile8(manifestPath, "utf8")
|
|
5581
|
+
);
|
|
5582
|
+
const manifestDir = dirname2(resolve6(manifestPath));
|
|
5583
|
+
const kitDir = dirname2(resolve6(kitPath));
|
|
5584
|
+
const out = resolve6(outDir ?? join10(kitDir, "judge"));
|
|
5585
|
+
await mkdir7(out, { recursive: true });
|
|
5236
5586
|
const sheets = [];
|
|
5237
5587
|
const skipped = [];
|
|
5238
5588
|
const refCache = /* @__PURE__ */ new Map();
|
|
5239
5589
|
const loadRef = async (file) => {
|
|
5240
5590
|
if (refCache.has(file)) return refCache.get(file) ?? null;
|
|
5241
|
-
const p =
|
|
5242
|
-
const img =
|
|
5591
|
+
const p = join10(manifestDir, file);
|
|
5592
|
+
const img = existsSync9(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
|
|
5243
5593
|
refCache.set(file, img);
|
|
5244
5594
|
return img;
|
|
5245
5595
|
};
|
|
5246
5596
|
for (const a of kit.assets) {
|
|
5247
5597
|
if (!/\.png$/i.test(a.path)) continue;
|
|
5248
|
-
const file = isAbsolute3(a.path) &&
|
|
5249
|
-
const img =
|
|
5598
|
+
const file = isAbsolute3(a.path) && existsSync9(a.path) ? a.path : join10(kitDir, a.path.split("/").pop() ?? a.path);
|
|
5599
|
+
const img = existsSync9(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
|
|
5250
5600
|
if (!img) {
|
|
5251
5601
|
skipped.push(`${a.destination ?? a.path}: unreadable`);
|
|
5252
5602
|
continue;
|
|
5253
5603
|
}
|
|
5254
|
-
const roles = rolesFor({
|
|
5255
|
-
|
|
5604
|
+
const roles = rolesFor({
|
|
5605
|
+
destination: a.destination,
|
|
5606
|
+
source: a.source,
|
|
5607
|
+
template: a.template,
|
|
5608
|
+
path: a.path
|
|
5609
|
+
});
|
|
5610
|
+
const ref = manifest.assets.find(
|
|
5611
|
+
(r) => roles.includes(r.role) && r.genre !== "context"
|
|
5612
|
+
);
|
|
5256
5613
|
if (!ref) {
|
|
5257
|
-
skipped.push(
|
|
5614
|
+
skipped.push(
|
|
5615
|
+
`${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`
|
|
5616
|
+
);
|
|
5258
5617
|
continue;
|
|
5259
5618
|
}
|
|
5260
5619
|
const refImg = await loadRef(ref.file);
|
|
5261
5620
|
if (!refImg) {
|
|
5262
|
-
skipped.push(
|
|
5621
|
+
skipped.push(
|
|
5622
|
+
`${a.destination ?? a.path}: reference ${ref.file} unreadable`
|
|
5623
|
+
);
|
|
5263
5624
|
continue;
|
|
5264
5625
|
}
|
|
5265
5626
|
const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
|
|
5266
5627
|
const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
|
|
5267
5628
|
const nameA = `${id}--A.png`;
|
|
5268
5629
|
const nameB = `${id}--B.png`;
|
|
5269
|
-
await
|
|
5270
|
-
await
|
|
5630
|
+
await writeFile9(join10(out, nameA), encodePng(composeSheet(img, refImg)));
|
|
5631
|
+
await writeFile9(join10(out, nameB), encodePng(composeSheet(refImg, img)));
|
|
5271
5632
|
const rubric = [
|
|
5272
5633
|
`# ${id} against ${ref.id} (${ref.file})`,
|
|
5273
5634
|
"",
|
|
@@ -5287,18 +5648,31 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5287
5648
|
"In judge.json: `win` true when the kit asset is at least as good as the reference on the rules that apply to its genre, false when it is worse, null for a tie; `reasons` names the rules by number.",
|
|
5288
5649
|
""
|
|
5289
5650
|
].join("\n");
|
|
5290
|
-
await
|
|
5291
|
-
sheets.push({
|
|
5651
|
+
await writeFile9(join10(out, `${id}.md`), rubric);
|
|
5652
|
+
sheets.push({
|
|
5653
|
+
asset: id,
|
|
5654
|
+
reference: ref.id,
|
|
5655
|
+
sheetA: nameA,
|
|
5656
|
+
sheetB: nameB,
|
|
5657
|
+
rubric: `${id}.md`
|
|
5658
|
+
});
|
|
5292
5659
|
}
|
|
5293
|
-
const verdictFile =
|
|
5294
|
-
if (!
|
|
5295
|
-
await
|
|
5660
|
+
const verdictFile = join10(out, "judge.json");
|
|
5661
|
+
if (!existsSync9(verdictFile)) {
|
|
5662
|
+
await writeFile9(
|
|
5296
5663
|
verdictFile,
|
|
5297
5664
|
JSON.stringify(
|
|
5298
5665
|
{
|
|
5299
5666
|
kit: kitPath,
|
|
5300
5667
|
manifest: manifestPath,
|
|
5301
|
-
verdicts: sheets.map((s) => ({
|
|
5668
|
+
verdicts: sheets.map((s) => ({
|
|
5669
|
+
asset: s.asset,
|
|
5670
|
+
reference: s.reference,
|
|
5671
|
+
A: null,
|
|
5672
|
+
B: null,
|
|
5673
|
+
win: null,
|
|
5674
|
+
reasons: []
|
|
5675
|
+
}))
|
|
5302
5676
|
},
|
|
5303
5677
|
null,
|
|
5304
5678
|
2
|
|
@@ -5308,22 +5682,43 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5308
5682
|
return { sheets, skipped, outDir: out, verdictFile };
|
|
5309
5683
|
}
|
|
5310
5684
|
function winRate(verdicts) {
|
|
5311
|
-
const judgedList = verdicts.filter(
|
|
5685
|
+
const judgedList = verdicts.filter(
|
|
5686
|
+
(v) => v.win !== null || (v.reasons?.length ?? 0) > 0
|
|
5687
|
+
);
|
|
5312
5688
|
const wins = judgedList.filter((v) => v.win === true).length;
|
|
5313
5689
|
const ties = judgedList.filter((v) => v.win === null).length;
|
|
5314
5690
|
const judged = judgedList.length;
|
|
5315
|
-
return {
|
|
5691
|
+
return {
|
|
5692
|
+
wins,
|
|
5693
|
+
ties,
|
|
5694
|
+
judged,
|
|
5695
|
+
rate: judged ? (wins + ties / 2) / judged : null
|
|
5696
|
+
};
|
|
5697
|
+
}
|
|
5698
|
+
|
|
5699
|
+
// src/plugin/fontName.ts
|
|
5700
|
+
import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
|
|
5701
|
+
function catalogFamily(family) {
|
|
5702
|
+
const name = family.trim();
|
|
5703
|
+
if (findFontFamily2(name)) return name;
|
|
5704
|
+
const base = name.replace(/\s+Variable$/i, "");
|
|
5705
|
+
if (base !== name) {
|
|
5706
|
+
const hit = findFontFamily2(base);
|
|
5707
|
+
if (hit) return hit.family;
|
|
5708
|
+
}
|
|
5709
|
+
return family;
|
|
5316
5710
|
}
|
|
5317
5711
|
|
|
5318
5712
|
// src/plugin/callout.ts
|
|
5319
|
-
import { existsSync as
|
|
5713
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5320
5714
|
import { readFile as readFile9 } from "fs/promises";
|
|
5321
|
-
import { join as
|
|
5715
|
+
import { join as join11 } from "path";
|
|
5322
5716
|
import { lerpArray, sample } from "@vosjs/timeline";
|
|
5323
5717
|
import {
|
|
5324
5718
|
CALLOUT_DEFAULT_BODY_PX,
|
|
5325
5719
|
CALLOUT_SHAPES,
|
|
5326
5720
|
calloutClip,
|
|
5721
|
+
calloutLook,
|
|
5327
5722
|
calloutGroundOf,
|
|
5328
5723
|
cameraModel as cameraModel2,
|
|
5329
5724
|
deltaE,
|
|
@@ -5342,10 +5737,11 @@ var CALLOUT_DELTA_E_PROBLEM = 8;
|
|
|
5342
5737
|
var CALLOUT_DELTA_E_WARNING = 16;
|
|
5343
5738
|
function registerFrom(roles, flags) {
|
|
5344
5739
|
const ground = flags.ground ?? roles?.bgA;
|
|
5345
|
-
const accent = flags.accent ?? roles?.accent;
|
|
5740
|
+
const accent = flags.accent ?? roles?.callout ?? roles?.accent;
|
|
5346
5741
|
if (!ground || !accent) return null;
|
|
5347
5742
|
if (!hexToRgb(ground) || !hexToRgb(accent)) return null;
|
|
5348
|
-
const
|
|
5743
|
+
const named = flags.face ?? roles?.fontBody;
|
|
5744
|
+
const face = named ? catalogFamily(named) : named;
|
|
5349
5745
|
return {
|
|
5350
5746
|
ground,
|
|
5351
5747
|
accent,
|
|
@@ -5411,16 +5807,28 @@ function composeCallout(doc, register, ask) {
|
|
|
5411
5807
|
let n = 2;
|
|
5412
5808
|
while (taken.has(id) && !ask.id)
|
|
5413
5809
|
id = `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}-${n++}`;
|
|
5810
|
+
const seen = appScaleAt(doc, window2.start + 0.35);
|
|
5811
|
+
const scale = ask.bodyPxGiven ? 1 : seen;
|
|
5812
|
+
const look = calloutLook(register, scale);
|
|
5414
5813
|
const clip = calloutClip(ask.shape, register, ask.words, {
|
|
5415
5814
|
id,
|
|
5416
5815
|
start: window2.start,
|
|
5417
5816
|
duration: window2.duration,
|
|
5418
|
-
scale
|
|
5817
|
+
scale,
|
|
5419
5818
|
pin,
|
|
5420
5819
|
// The fallback place, in the margin: the lower third.
|
|
5421
5820
|
at: { x: 0.5, y: 0.82 }
|
|
5422
5821
|
});
|
|
5423
|
-
return {
|
|
5822
|
+
return {
|
|
5823
|
+
clip,
|
|
5824
|
+
window: window2,
|
|
5825
|
+
sizes: {
|
|
5826
|
+
scale: +seen.toFixed(3),
|
|
5827
|
+
kickerPx: look.kickerPx,
|
|
5828
|
+
titlePx: look.titlePx,
|
|
5829
|
+
bodyPx: look.bodyPx
|
|
5830
|
+
}
|
|
5831
|
+
};
|
|
5424
5832
|
}
|
|
5425
5833
|
function isCalloutShape(s) {
|
|
5426
5834
|
return CALLOUT_SHAPES.includes(s);
|
|
@@ -5501,10 +5909,10 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
|
5501
5909
|
const out = /* @__PURE__ */ new Map();
|
|
5502
5910
|
if (!times.length) return out;
|
|
5503
5911
|
const bare = { ...doc, overlays: [] };
|
|
5504
|
-
const outDir =
|
|
5912
|
+
const outDir = join11(dir, ".ground-check");
|
|
5505
5913
|
const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
|
|
5506
5914
|
for (const f of res.frames) {
|
|
5507
|
-
if (!
|
|
5915
|
+
if (!existsSync10(f.file)) continue;
|
|
5508
5916
|
out.set(
|
|
5509
5917
|
+f.time.toFixed(3),
|
|
5510
5918
|
decodePng(new Uint8Array(await readFile9(f.file)))
|
|
@@ -5885,9 +6293,9 @@ function formatFinding(f) {
|
|
|
5885
6293
|
}
|
|
5886
6294
|
|
|
5887
6295
|
// src/plugin/validateKit.ts
|
|
5888
|
-
import { existsSync as
|
|
5889
|
-
import { readFile as readFile11, stat as
|
|
5890
|
-
import { dirname as
|
|
6296
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6297
|
+
import { readFile as readFile11, stat as stat3 } from "fs/promises";
|
|
6298
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join12 } from "path";
|
|
5891
6299
|
import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
|
|
5892
6300
|
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5893
6301
|
function pngDimensions(bytes) {
|
|
@@ -5944,10 +6352,10 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5944
6352
|
measured
|
|
5945
6353
|
};
|
|
5946
6354
|
}
|
|
5947
|
-
const base =
|
|
6355
|
+
const base = dirname3(kitPath);
|
|
5948
6356
|
const resolvePath = (p) => {
|
|
5949
|
-
if (isAbsolute4(p) &&
|
|
5950
|
-
const beside =
|
|
6357
|
+
if (isAbsolute4(p) && existsSync11(p)) return p;
|
|
6358
|
+
const beside = join12(base, p.split("/").pop() ?? p);
|
|
5951
6359
|
return beside;
|
|
5952
6360
|
};
|
|
5953
6361
|
const perDestination = /* @__PURE__ */ new Map();
|
|
@@ -5958,7 +6366,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5958
6366
|
const file = resolvePath(a.path);
|
|
5959
6367
|
let bytes;
|
|
5960
6368
|
try {
|
|
5961
|
-
bytes = (await
|
|
6369
|
+
bytes = (await stat3(file)).size;
|
|
5962
6370
|
} catch {
|
|
5963
6371
|
problems.push(`${label}: ${a.path} is missing`);
|
|
5964
6372
|
continue;
|
|
@@ -6083,7 +6491,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6083
6491
|
|
|
6084
6492
|
// src/plugin/recorder.ts
|
|
6085
6493
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
6086
|
-
import { join as
|
|
6494
|
+
import { join as join13 } from "path";
|
|
6087
6495
|
|
|
6088
6496
|
// src/plugin/recordingCap.ts
|
|
6089
6497
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
@@ -6241,7 +6649,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6241
6649
|
const vh = actions.viewport?.height ?? 720;
|
|
6242
6650
|
const context = await browser.newContext({
|
|
6243
6651
|
viewport: { width: vw, height: vh },
|
|
6244
|
-
deviceScaleFactor: 1
|
|
6652
|
+
deviceScaleFactor: 1,
|
|
6653
|
+
...opts.storageState ? { storageState: opts.storageState } : {}
|
|
6245
6654
|
});
|
|
6246
6655
|
const page = await context.newPage();
|
|
6247
6656
|
page.on("console", (m) => {
|
|
@@ -6261,7 +6670,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6261
6670
|
cdp.on("Page.screencastFrame", (ev) => {
|
|
6262
6671
|
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
6263
6672
|
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
6264
|
-
writeFileSync2(
|
|
6673
|
+
writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
6265
6674
|
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
6266
6675
|
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
6267
6676
|
});
|
|
@@ -6523,7 +6932,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6523
6932
|
await sleep(200);
|
|
6524
6933
|
const pageTitle = await page.title().catch(() => "");
|
|
6525
6934
|
await context.close();
|
|
6526
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(
|
|
6935
|
+
const firstFrame = frames[0] ? jpegDims(readFileSync3(join13(paths.framesDir, frames[0].file))) : null;
|
|
6527
6936
|
const meta = {
|
|
6528
6937
|
dpr: 1,
|
|
6529
6938
|
zoom: 1,
|
|
@@ -6634,9 +7043,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6634
7043
|
}
|
|
6635
7044
|
|
|
6636
7045
|
// src/plugin/plan.ts
|
|
6637
|
-
import { existsSync as
|
|
7046
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6638
7047
|
import { readFile as readFile12 } from "fs/promises";
|
|
6639
|
-
import { join as
|
|
7048
|
+
import { join as join14 } from "path";
|
|
6640
7049
|
import {
|
|
6641
7050
|
DEFAULT_FRAME_STYLE,
|
|
6642
7051
|
STYLE_FIELDS,
|
|
@@ -6858,8 +7267,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6858
7267
|
// src/plugin/plan.ts
|
|
6859
7268
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6860
7269
|
async function readDigestActivity(dir) {
|
|
6861
|
-
const file =
|
|
6862
|
-
if (!
|
|
7270
|
+
const file = join14(dir, "digest", "digest.json");
|
|
7271
|
+
if (!existsSync12(file)) return null;
|
|
6863
7272
|
try {
|
|
6864
7273
|
const d = JSON.parse(await readFile12(file, "utf8"));
|
|
6865
7274
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
@@ -6928,11 +7337,11 @@ async function planTake(dir, opts = {}) {
|
|
|
6928
7337
|
if (opts.reuse) {
|
|
6929
7338
|
const prev = migrateMotion2(opts.reuse.doc);
|
|
6930
7339
|
const artifact = {
|
|
6931
|
-
videoKey:
|
|
7340
|
+
videoKey: take.paths.recordingName,
|
|
6932
7341
|
cursor,
|
|
6933
7342
|
meta
|
|
6934
7343
|
};
|
|
6935
|
-
doc = projectFromArtifact(artifact,
|
|
7344
|
+
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
6936
7345
|
doc = copyStyle(prev, doc);
|
|
6937
7346
|
const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
|
|
6938
7347
|
doc.segments = rt.segments;
|
|
@@ -7020,11 +7429,11 @@ async function planTake(dir, opts = {}) {
|
|
|
7020
7429
|
}
|
|
7021
7430
|
} else {
|
|
7022
7431
|
const artifact = {
|
|
7023
|
-
videoKey:
|
|
7432
|
+
videoKey: take.paths.recordingName,
|
|
7024
7433
|
cursor,
|
|
7025
7434
|
meta
|
|
7026
7435
|
};
|
|
7027
|
-
doc = projectFromArtifact(artifact,
|
|
7436
|
+
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
7028
7437
|
if (opts.style) {
|
|
7029
7438
|
const applied = applyStyle(opts.style.doc, doc, opts);
|
|
7030
7439
|
doc = applied.doc;
|
|
@@ -7074,207 +7483,11 @@ import { createHash } from "crypto";
|
|
|
7074
7483
|
import { existsSync as existsSync13 } from "fs";
|
|
7075
7484
|
import { readFile as readFile13 } from "fs/promises";
|
|
7076
7485
|
import { createInterface } from "readline/promises";
|
|
7077
|
-
import { basename, join as join16 } from "path";
|
|
7486
|
+
import { basename as basename3, join as join16 } from "path";
|
|
7078
7487
|
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
7079
7488
|
|
|
7080
|
-
// src/plugin/media.ts
|
|
7081
|
-
import { isKeyedOverlay } from "@vosjs/studio-core";
|
|
7082
|
-
import { createWriteStream, existsSync as existsSync12 } from "fs";
|
|
7083
|
-
import { writeFile as writeFile9 } from "fs/promises";
|
|
7084
|
-
import { join as join14 } from "path";
|
|
7085
|
-
import { Readable } from "stream";
|
|
7086
|
-
import { pipeline } from "stream/promises";
|
|
7087
|
-
var MIC_NAME = "mic.webm";
|
|
7088
|
-
var CAM_NAME = "cam.webm";
|
|
7089
|
-
var KEYS = [
|
|
7090
|
-
{ key: "videoKey", file: RECORDING_NAME },
|
|
7091
|
-
{ key: "micKey", file: MIC_NAME },
|
|
7092
|
-
{ key: "camKey", file: CAM_NAME }
|
|
7093
|
-
];
|
|
7094
|
-
function isTakeRelativeKey(key) {
|
|
7095
|
-
return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
|
|
7096
|
-
}
|
|
7097
|
-
var takeRelativeFile = (key) => key.replace(/^\/+/, "");
|
|
7098
|
-
function docMediaRefs(doc, keep = isTakeRelativeKey) {
|
|
7099
|
-
const out = [];
|
|
7100
|
-
for (const m of doc.media ?? []) {
|
|
7101
|
-
if (keep(m.videoKey))
|
|
7102
|
-
out.push({
|
|
7103
|
-
where: `media ${m.id}`,
|
|
7104
|
-
key: m.videoKey,
|
|
7105
|
-
set: (next) => {
|
|
7106
|
-
m.videoKey = next;
|
|
7107
|
-
}
|
|
7108
|
-
});
|
|
7109
|
-
const mic = m.micKey;
|
|
7110
|
-
if (mic && keep(mic))
|
|
7111
|
-
out.push({
|
|
7112
|
-
where: `media ${m.id} mic`,
|
|
7113
|
-
key: mic,
|
|
7114
|
-
set: (next) => {
|
|
7115
|
-
m.micKey = next;
|
|
7116
|
-
}
|
|
7117
|
-
});
|
|
7118
|
-
const cam = m.camKey;
|
|
7119
|
-
if (cam && keep(cam))
|
|
7120
|
-
out.push({
|
|
7121
|
-
where: `media ${m.id} cam`,
|
|
7122
|
-
key: cam,
|
|
7123
|
-
set: (next) => {
|
|
7124
|
-
m.camKey = next;
|
|
7125
|
-
}
|
|
7126
|
-
});
|
|
7127
|
-
}
|
|
7128
|
-
for (const clip of doc.overlays ?? []) {
|
|
7129
|
-
if (!isKeyedOverlay(clip) || !keep(clip.key)) continue;
|
|
7130
|
-
out.push({
|
|
7131
|
-
where: `overlay ${clip.id}`,
|
|
7132
|
-
key: clip.key,
|
|
7133
|
-
set: (next) => {
|
|
7134
|
-
clip.key = next;
|
|
7135
|
-
}
|
|
7136
|
-
});
|
|
7137
|
-
}
|
|
7138
|
-
const mark = doc.endCard?.mark;
|
|
7139
|
-
if (mark && keep(mark.key))
|
|
7140
|
-
out.push({
|
|
7141
|
-
where: "end card mark",
|
|
7142
|
-
key: mark.key,
|
|
7143
|
-
set: (next) => {
|
|
7144
|
-
mark.key = next;
|
|
7145
|
-
}
|
|
7146
|
-
});
|
|
7147
|
-
const bg = doc.frame.backgroundMedia;
|
|
7148
|
-
if (bg && keep(bg.key))
|
|
7149
|
-
out.push({
|
|
7150
|
-
where: "background",
|
|
7151
|
-
key: bg.key,
|
|
7152
|
-
set: (next) => {
|
|
7153
|
-
bg.key = next;
|
|
7154
|
-
}
|
|
7155
|
-
});
|
|
7156
|
-
return out;
|
|
7157
|
-
}
|
|
7158
|
-
function mediaContentType(file) {
|
|
7159
|
-
const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
|
|
7160
|
-
const types = {
|
|
7161
|
-
svg: "image/svg+xml",
|
|
7162
|
-
png: "image/png",
|
|
7163
|
-
jpg: "image/jpeg",
|
|
7164
|
-
jpeg: "image/jpeg",
|
|
7165
|
-
webp: "image/webp",
|
|
7166
|
-
gif: "image/gif",
|
|
7167
|
-
avif: "image/avif",
|
|
7168
|
-
webm: "video/webm",
|
|
7169
|
-
mp4: "video/mp4",
|
|
7170
|
-
mov: "video/quicktime",
|
|
7171
|
-
mp3: "audio/mpeg",
|
|
7172
|
-
wav: "audio/wav",
|
|
7173
|
-
ogg: "audio/ogg",
|
|
7174
|
-
m4a: "audio/mp4"
|
|
7175
|
-
};
|
|
7176
|
-
return types[ext] ?? "application/octet-stream";
|
|
7177
|
-
}
|
|
7178
|
-
function extensionFor(contentType) {
|
|
7179
|
-
const t = contentType.split(";")[0].trim().toLowerCase();
|
|
7180
|
-
const exts = {
|
|
7181
|
-
"image/svg+xml": ".svg",
|
|
7182
|
-
"image/png": ".png",
|
|
7183
|
-
"image/jpeg": ".jpg",
|
|
7184
|
-
"image/webp": ".webp",
|
|
7185
|
-
"image/gif": ".gif",
|
|
7186
|
-
"image/avif": ".avif",
|
|
7187
|
-
"video/webm": ".webm",
|
|
7188
|
-
"video/mp4": ".mp4",
|
|
7189
|
-
"video/quicktime": ".mov",
|
|
7190
|
-
"audio/mpeg": ".mp3",
|
|
7191
|
-
"audio/wav": ".wav",
|
|
7192
|
-
"audio/ogg": ".ogg",
|
|
7193
|
-
"audio/mp4": ".m4a"
|
|
7194
|
-
};
|
|
7195
|
-
return exts[t] ?? "";
|
|
7196
|
-
}
|
|
7197
|
-
function assetIdOf(url) {
|
|
7198
|
-
if (!url) return null;
|
|
7199
|
-
const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
|
|
7200
|
-
return m ? m[1] : null;
|
|
7201
|
-
}
|
|
7202
|
-
async function pullMedia(ctx, dir, doc, log) {
|
|
7203
|
-
const result = { downloaded: [], kept: [] };
|
|
7204
|
-
for (const { key, file } of KEYS) {
|
|
7205
|
-
const url = doc.source[key];
|
|
7206
|
-
const assetId = assetIdOf(url);
|
|
7207
|
-
if (!assetId) continue;
|
|
7208
|
-
const target = join14(dir, file);
|
|
7209
|
-
if (existsSync12(target)) {
|
|
7210
|
-
result.kept.push(file);
|
|
7211
|
-
} else {
|
|
7212
|
-
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
7213
|
-
const res = await fetch(abs, {
|
|
7214
|
-
headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
|
|
7215
|
-
});
|
|
7216
|
-
if (!res.ok || !res.body) {
|
|
7217
|
-
throw new Error(
|
|
7218
|
-
`download of ${file} failed (${res.status})${res.status === 404 ? " \u2014 a private recording needs a content key of its owner" : ""}`
|
|
7219
|
-
);
|
|
7220
|
-
}
|
|
7221
|
-
await pipeline(
|
|
7222
|
-
Readable.fromWeb(res.body),
|
|
7223
|
-
createWriteStream(target)
|
|
7224
|
-
);
|
|
7225
|
-
const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
|
|
7226
|
-
result.downloaded.push({ file, assetId, bytes });
|
|
7227
|
-
log(` ${file} \u2190 asset ${assetId} (${Math.round(bytes / 1024)} kB)`);
|
|
7228
|
-
}
|
|
7229
|
-
doc.source[key] = file;
|
|
7230
|
-
}
|
|
7231
|
-
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
7232
|
-
const assetId = assetIdOf(ref.key);
|
|
7233
|
-
const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
|
|
7234
|
-
const existing = existsSync12(join14(dir, "media")) ? (await import("fs/promises").then(
|
|
7235
|
-
(fs) => fs.readdir(join14(dir, "media"))
|
|
7236
|
-
)).find((f) => f.startsWith(`${assetId}.`)) : void 0;
|
|
7237
|
-
let file;
|
|
7238
|
-
if (existing) {
|
|
7239
|
-
file = `media/${existing}`;
|
|
7240
|
-
result.kept.push(file);
|
|
7241
|
-
} else {
|
|
7242
|
-
const res = await fetch(abs, {
|
|
7243
|
-
headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
|
|
7244
|
-
});
|
|
7245
|
-
if (!res.ok || !res.body) {
|
|
7246
|
-
throw new Error(
|
|
7247
|
-
`download of ${ref.where} (${ref.key}) failed (${res.status})`
|
|
7248
|
-
);
|
|
7249
|
-
}
|
|
7250
|
-
const ext = extensionFor(res.headers.get("content-type") ?? "");
|
|
7251
|
-
file = `media/${assetId}${ext}`;
|
|
7252
|
-
const target = join14(dir, file);
|
|
7253
|
-
await import("fs/promises").then(
|
|
7254
|
-
(fs) => fs.mkdir(join14(dir, "media"), { recursive: true })
|
|
7255
|
-
);
|
|
7256
|
-
await pipeline(
|
|
7257
|
-
Readable.fromWeb(res.body),
|
|
7258
|
-
createWriteStream(target)
|
|
7259
|
-
);
|
|
7260
|
-
const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
|
|
7261
|
-
result.downloaded.push({ file, assetId, bytes });
|
|
7262
|
-
log(
|
|
7263
|
-
` ${file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(bytes / 1024)} kB)`
|
|
7264
|
-
);
|
|
7265
|
-
}
|
|
7266
|
-
ref.set(file);
|
|
7267
|
-
}
|
|
7268
|
-
const metaPath = join14(dir, "meta.json");
|
|
7269
|
-
if (!existsSync12(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
7270
|
-
const cursorPath = join14(dir, "cursor.json");
|
|
7271
|
-
if (!existsSync12(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
7272
|
-
await writeFile9(join14(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
7273
|
-
return result;
|
|
7274
|
-
}
|
|
7275
|
-
|
|
7276
7489
|
// src/plugin/folder.ts
|
|
7277
|
-
import { mkdir as
|
|
7490
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
|
|
7278
7491
|
import { join as join15 } from "path";
|
|
7279
7492
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
7280
7493
|
import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
@@ -7451,7 +7664,7 @@ async function cmdPull(argv) {
|
|
|
7451
7664
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7452
7665
|
const payload = res.body;
|
|
7453
7666
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7454
|
-
await
|
|
7667
|
+
await mkdir8(join15(out, "recipes"), { recursive: true });
|
|
7455
7668
|
const lines = [];
|
|
7456
7669
|
const recipes = [];
|
|
7457
7670
|
for (const rec of payload.recipes) {
|
|
@@ -7461,7 +7674,7 @@ async function cmdPull(argv) {
|
|
|
7461
7674
|
}
|
|
7462
7675
|
for (const rec of payload.inheritedRecipes) {
|
|
7463
7676
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7464
|
-
await
|
|
7677
|
+
await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
|
|
7465
7678
|
const path = join15(
|
|
7466
7679
|
out,
|
|
7467
7680
|
"recipes",
|
|
@@ -7478,7 +7691,7 @@ async function cmdPull(argv) {
|
|
|
7478
7691
|
const members = [];
|
|
7479
7692
|
for (const v of payload.voses) {
|
|
7480
7693
|
const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
|
|
7481
|
-
await
|
|
7694
|
+
await mkdir8(dir, { recursive: true });
|
|
7482
7695
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7483
7696
|
if (cfg.status !== 200) {
|
|
7484
7697
|
lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
|
|
@@ -7742,6 +7955,15 @@ function printChanges(r, changes, protectedIds) {
|
|
|
7742
7955
|
r.event({ event: "protected", nodes: protectedIds });
|
|
7743
7956
|
}
|
|
7744
7957
|
}
|
|
7958
|
+
function takePushRefusal(ask) {
|
|
7959
|
+
if (ask.programOnly.length) {
|
|
7960
|
+
const list = ask.programOnly.map((f) => `--${f}`).join(", ");
|
|
7961
|
+
return `${list} ${ask.programOnly.length > 1 ? "belong" : "belongs"} to a PROGRAM push (a config.json); a take push reads --vos, --title, --label, --note, --folder, --override and --yes`;
|
|
7962
|
+
}
|
|
7963
|
+
if (ask.vos !== void 0 && ask.trackedVosId !== null && ask.trackedVosId !== ask.vos)
|
|
7964
|
+
return `this take tracks vos ${ask.trackedVosId} (its vos.json), not ${ask.vos}. Drop --vos to push there, or remove vos.json to adopt ${ask.vos} instead`;
|
|
7965
|
+
return null;
|
|
7966
|
+
}
|
|
7745
7967
|
async function pushTake(dir, flags, r) {
|
|
7746
7968
|
const take = await loadTake(dir);
|
|
7747
7969
|
if (!take.doc) {
|
|
@@ -7753,10 +7975,27 @@ async function pushTake(dir, flags, r) {
|
|
|
7753
7975
|
${lint.problems.join("\n ")}`);
|
|
7754
7976
|
}
|
|
7755
7977
|
if (!existsSync13(take.paths.recording)) {
|
|
7756
|
-
throw new Error(
|
|
7978
|
+
throw new Error(
|
|
7979
|
+
"no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
|
|
7980
|
+
);
|
|
7757
7981
|
}
|
|
7758
7982
|
const ctx = apiContext(flags);
|
|
7759
|
-
const
|
|
7983
|
+
const tracked = readSyncState(dir);
|
|
7984
|
+
const firstPush = tracked === null;
|
|
7985
|
+
let state = tracked;
|
|
7986
|
+
if (!state && flags.vos) {
|
|
7987
|
+
const meta = await api(ctx, `/vos/${flags.vos}`);
|
|
7988
|
+
const head = meta.json.vos?.currentVersionId;
|
|
7989
|
+
if (meta.status !== 200 || !head) {
|
|
7990
|
+
throw new Error(
|
|
7991
|
+
`--vos ${flags.vos}: cannot adopt it (${meta.status}${meta.json.error ? `: ${String(meta.json.error)}` : ""}) \u2014 it must be a vos this credential can read, with a version to build on`
|
|
7992
|
+
);
|
|
7993
|
+
}
|
|
7994
|
+
r.log(
|
|
7995
|
+
`adopting vos ${flags.vos}: this take becomes the next version after its head (${head})`
|
|
7996
|
+
);
|
|
7997
|
+
state = { vosId: flags.vos, versionId: head };
|
|
7998
|
+
}
|
|
7760
7999
|
let folderId;
|
|
7761
8000
|
if (flags.folder) {
|
|
7762
8001
|
if (state) {
|
|
@@ -7769,7 +8008,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7769
8008
|
flags.folder
|
|
7770
8009
|
).id;
|
|
7771
8010
|
}
|
|
7772
|
-
if (
|
|
8011
|
+
if (firstPush && !flags.yes) {
|
|
7773
8012
|
if (!process.stdin.isTTY) {
|
|
7774
8013
|
throw new Error(
|
|
7775
8014
|
"first push of this take uploads its recording \u2014 pass --yes to consent headlessly"
|
|
@@ -7786,12 +8025,22 @@ async function pushTake(dir, flags, r) {
|
|
|
7786
8025
|
}
|
|
7787
8026
|
const bytes = await readFile13(take.paths.recording);
|
|
7788
8027
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
8028
|
+
const { type: contentType } = resolveMediaType({
|
|
8029
|
+
head: bytes.subarray(0, MEDIA_HEAD_BYTES),
|
|
8030
|
+
filename: take.paths.recordingName
|
|
8031
|
+
});
|
|
8032
|
+
const filename = nameForType(take.paths.recordingName, contentType);
|
|
8033
|
+
if (filename !== take.paths.recordingName) {
|
|
8034
|
+
r.log(
|
|
8035
|
+
` ${take.paths.recordingName} holds ${contentType} \u2014 uploading it as ${filename}`
|
|
8036
|
+
);
|
|
8037
|
+
}
|
|
7789
8038
|
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7790
8039
|
let upload;
|
|
7791
8040
|
try {
|
|
7792
8041
|
upload = await uploadAsset(ctx, new Uint8Array(bytes), {
|
|
7793
|
-
filename
|
|
7794
|
-
contentType
|
|
8042
|
+
filename,
|
|
8043
|
+
contentType,
|
|
7795
8044
|
contentHash: hash,
|
|
7796
8045
|
// The take's length so the server holds it to the plan's cap.
|
|
7797
8046
|
...take.meta.durationMs > 0 ? { durationSeconds: take.meta.durationMs / 1e3 } : {},
|
|
@@ -7824,11 +8073,18 @@ async function pushTake(dir, flags, r) {
|
|
|
7824
8073
|
}
|
|
7825
8074
|
const media = await readFile13(file);
|
|
7826
8075
|
const mediaHash = createHash("sha256").update(media).digest("hex");
|
|
7827
|
-
const type =
|
|
8076
|
+
const { type } = resolveMediaType({
|
|
8077
|
+
head: media.subarray(0, MEDIA_HEAD_BYTES),
|
|
8078
|
+
filename: file
|
|
8079
|
+
});
|
|
8080
|
+
const name = nameForType(basename3(file), type);
|
|
8081
|
+
if (name !== basename3(file)) {
|
|
8082
|
+
r.log(` ${ref.where}: ${basename3(file)} holds ${type} \u2014 sent as ${name}`);
|
|
8083
|
+
}
|
|
7828
8084
|
let put;
|
|
7829
8085
|
try {
|
|
7830
8086
|
put = await uploadAsset(ctx, new Uint8Array(media), {
|
|
7831
|
-
filename:
|
|
8087
|
+
filename: name,
|
|
7832
8088
|
contentType: type,
|
|
7833
8089
|
contentHash: mediaHash
|
|
7834
8090
|
});
|
|
@@ -7845,7 +8101,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7845
8101
|
const lowered = lowerToComposition3(docForPush);
|
|
7846
8102
|
const config = { ...lowered.config, data: lowered.data };
|
|
7847
8103
|
if (!state) {
|
|
7848
|
-
const title = flags.title ?? `Take ${
|
|
8104
|
+
const title = flags.title ?? `Take ${basename3(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
7849
8105
|
const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "take"}-${Date.now().toString(36)}`;
|
|
7850
8106
|
const created = await api(ctx, "/vos", {
|
|
7851
8107
|
method: "POST",
|
|
@@ -8017,8 +8273,9 @@ async function pullTake(dir, flags, r) {
|
|
|
8017
8273
|
}
|
|
8018
8274
|
const hostedDoc = migrateHostedDoc3(docRes.json);
|
|
8019
8275
|
const doc = hostedDoc;
|
|
8020
|
-
|
|
8021
|
-
|
|
8276
|
+
const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
8277
|
+
if (existsSync13(localRecording)) {
|
|
8278
|
+
doc.source.videoKey = basename3(localRecording);
|
|
8022
8279
|
}
|
|
8023
8280
|
await writeJson(join16(dir, "doc.json"), doc, true);
|
|
8024
8281
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
@@ -8040,9 +8297,9 @@ async function pullTake(dir, flags, r) {
|
|
|
8040
8297
|
|
|
8041
8298
|
// src/plugin/program.ts
|
|
8042
8299
|
import { existsSync as existsSync14 } from "fs";
|
|
8043
|
-
import { mkdir as
|
|
8300
|
+
import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
8044
8301
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8045
|
-
import { basename as
|
|
8302
|
+
import { basename as basename4, dirname as dirname4, join as join17 } from "path";
|
|
8046
8303
|
import {
|
|
8047
8304
|
CURRENT_CONFIG_VERSION,
|
|
8048
8305
|
migrateConfig,
|
|
@@ -8239,7 +8496,7 @@ async function cmdFetch(argv) {
|
|
|
8239
8496
|
throw new Error(apiError(`fetch config for ${vosId}`, cfg));
|
|
8240
8497
|
const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
|
|
8241
8498
|
const out = strFlag(flags, "out") ?? slug;
|
|
8242
|
-
await
|
|
8499
|
+
await mkdir9(out, { recursive: true });
|
|
8243
8500
|
await writeFile11(
|
|
8244
8501
|
join17(out, "config.json"),
|
|
8245
8502
|
JSON.stringify(cfg.body.config, null, 2)
|
|
@@ -8383,7 +8640,7 @@ async function cmdPushProgram(argv) {
|
|
|
8383
8640
|
origin: strFlag(flags, "origin"),
|
|
8384
8641
|
api: strFlag(flags, "api")
|
|
8385
8642
|
});
|
|
8386
|
-
const dir =
|
|
8643
|
+
const dir = dirname4(source);
|
|
8387
8644
|
const parsed = JSON.parse(await readFile14(source, "utf8"));
|
|
8388
8645
|
const pre = preflightConfig(parsed);
|
|
8389
8646
|
if (!pre.ok || !pre.config) {
|
|
@@ -8411,7 +8668,7 @@ async function cmdPushProgram(argv) {
|
|
|
8411
8668
|
"--claimable creates a NEW claimable vos and cannot iterate (--vos). After the human claims it, iterate with their key: vos push --vos <id>"
|
|
8412
8669
|
);
|
|
8413
8670
|
}
|
|
8414
|
-
const fallback = state?.title ? `${state.title} remix` :
|
|
8671
|
+
const fallback = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos program";
|
|
8415
8672
|
const title2 = (strFlag(flags, "title") ?? fallback).slice(0, 100);
|
|
8416
8673
|
const body = { title: title2, config };
|
|
8417
8674
|
const slug = strFlag(flags, "slug");
|
|
@@ -8509,7 +8766,7 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
|
|
|
8509
8766
|
return EXIT_OK;
|
|
8510
8767
|
}
|
|
8511
8768
|
const remixOfId = strFlag(flags, "remix-of") ?? state?.vosId;
|
|
8512
|
-
const fallbackTitle = state?.title ? `${state.title} remix` :
|
|
8769
|
+
const fallbackTitle = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos remix";
|
|
8513
8770
|
const title = (strFlag(flags, "title") ?? fallbackTitle).slice(0, 100);
|
|
8514
8771
|
const folderId = folderRef ? resolveFolder(await listFolders(origin, key), folderRef).id : void 0;
|
|
8515
8772
|
const created = await createProgramVos({
|
|
@@ -8561,7 +8818,7 @@ Iterate with: vos push ${source} --vos ${created.id}`
|
|
|
8561
8818
|
async function cmdPullProgram(argv) {
|
|
8562
8819
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
|
|
8563
8820
|
const target = positionals[0] ?? ".";
|
|
8564
|
-
const dir = target.endsWith(".json") ?
|
|
8821
|
+
const dir = target.endsWith(".json") ? dirname4(target) : target;
|
|
8565
8822
|
const r = createReporter(flags.json === true);
|
|
8566
8823
|
const origin = platformOrigin({
|
|
8567
8824
|
origin: strFlag(flags, "origin"),
|
|
@@ -8744,7 +9001,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8744
9001
|
|
|
8745
9002
|
// src/plugin/asset.ts
|
|
8746
9003
|
import { readFileSync as readFileSync4 } from "fs";
|
|
8747
|
-
import { basename as
|
|
9004
|
+
import { basename as basename5, extname as extname2 } from "path";
|
|
8748
9005
|
var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
8749
9006
|
var MIME_BY_EXT = {
|
|
8750
9007
|
".png": "image/png",
|
|
@@ -8795,7 +9052,7 @@ async function cmdPush(argv) {
|
|
|
8795
9052
|
const folder = folderRef ? resolveFolder(await listFolders(origin, key), folderRef) : null;
|
|
8796
9053
|
const uploaded = [];
|
|
8797
9054
|
for (const file of positionals) {
|
|
8798
|
-
const name =
|
|
9055
|
+
const name = basename5(file);
|
|
8799
9056
|
const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
|
|
8800
9057
|
const body = readFileSync4(file);
|
|
8801
9058
|
const form = new FormData();
|
|
@@ -8852,11 +9109,11 @@ async function cmdAsset(argv) {
|
|
|
8852
9109
|
|
|
8853
9110
|
// src/plugin/recipe.ts
|
|
8854
9111
|
import { readFileSync as readFileSync5 } from "fs";
|
|
8855
|
-
import { basename as
|
|
9112
|
+
import { basename as basename6 } from "path";
|
|
8856
9113
|
var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
8857
9114
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
8858
9115
|
function recipeFilename(file) {
|
|
8859
|
-
const name =
|
|
9116
|
+
const name = basename6(file);
|
|
8860
9117
|
if (!/\.md$/i.test(name)) {
|
|
8861
9118
|
throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
|
|
8862
9119
|
}
|
|
@@ -9143,10 +9400,12 @@ function composeBrand(input) {
|
|
|
9143
9400
|
const bgC = mixHex(bgA, accent, 0.14);
|
|
9144
9401
|
p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
|
|
9145
9402
|
p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
|
|
9146
|
-
const fontDisplay =
|
|
9403
|
+
const fontDisplay = catalogFamily(
|
|
9404
|
+
design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily))
|
|
9405
|
+
);
|
|
9147
9406
|
p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
|
|
9148
9407
|
const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
|
|
9149
|
-
const fontBody = designBody ?? firstFamily(w.body.fontFamily);
|
|
9408
|
+
const fontBody = catalogFamily(designBody ?? firstFamily(w.body.fontFamily));
|
|
9150
9409
|
p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
|
|
9151
9410
|
const isIcon = (u) => /favicon|apple-touch|icon/i.test(u);
|
|
9152
9411
|
const onDark = (u) => /on-dark|white/i.test(u);
|
|
@@ -9848,14 +10107,15 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
9848
10107
|
"composed",
|
|
9849
10108
|
"check",
|
|
9850
10109
|
"motion",
|
|
9851
|
-
"leader"
|
|
10110
|
+
"leader",
|
|
10111
|
+
"keep-frames"
|
|
9852
10112
|
]);
|
|
9853
|
-
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
|
|
10113
|
+
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg"]);
|
|
9854
10114
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
9855
10115
|
|
|
9856
10116
|
Take pipeline
|
|
9857
|
-
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
|
|
9858
|
-
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
|
|
10117
|
+
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
|
|
10118
|
+
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
|
|
9859
10119
|
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
|
|
9860
10120
|
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]
|
|
9861
10121
|
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
@@ -9863,7 +10123,7 @@ Take pipeline
|
|
|
9863
10123
|
vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
|
|
9864
10124
|
vos brand <url> [--out BRAND.md] [--json]
|
|
9865
10125
|
vos open <take> [--studio <url>] [--print]
|
|
9866
|
-
vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px
|
|
10126
|
+
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]
|
|
9867
10127
|
vos validate <actions.json|take|kit.json> [--picture] [--json]
|
|
9868
10128
|
vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
|
|
9869
10129
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
@@ -9883,7 +10143,10 @@ Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
|
|
|
9883
10143
|
a take DIRECTORY (doc.json) pushes recording + doc; a config.json
|
|
9884
10144
|
pushes the program. No --vos: create a PRIVATE vos; with --vos:
|
|
9885
10145
|
add a version against the tracked base \u2014 a stale push 409s WITH
|
|
9886
|
-
the platform's typed changelog.
|
|
10146
|
+
the platform's typed changelog. A TAKE pushes to the vos in its
|
|
10147
|
+
vos.json; --vos on a take with none ADOPTS that vos (the take
|
|
10148
|
+
becomes its next version, built on its head), and --vos that
|
|
10149
|
+
disagrees with vos.json is refused before anything uploads. --claimable (programs only, NO
|
|
9887
10150
|
credential): creates a 72h claim link instead \u2014 hand it to the
|
|
9888
10151
|
user and nowhere else; unclaimed work is deleted after 72h.
|
|
9889
10152
|
--override consents to touching
|
|
@@ -10130,6 +10393,18 @@ function strictReason(rec) {
|
|
|
10130
10393
|
if (rec.capped) parts.push("stopped at --max-duration");
|
|
10131
10394
|
return parts.join(" + ");
|
|
10132
10395
|
}
|
|
10396
|
+
function takeBrowserArgs(multi) {
|
|
10397
|
+
const given = multi["browser-arg"];
|
|
10398
|
+
return (given ?? []).filter(Boolean);
|
|
10399
|
+
}
|
|
10400
|
+
function takeStorageState(flags) {
|
|
10401
|
+
const raw = strFlag(flags, "storage-state");
|
|
10402
|
+
if (!raw) return void 0;
|
|
10403
|
+
const path = resolve9(raw);
|
|
10404
|
+
if (!existsSync15(path))
|
|
10405
|
+
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
10406
|
+
return path;
|
|
10407
|
+
}
|
|
10133
10408
|
async function takeBackdrop(flags, r) {
|
|
10134
10409
|
const { backdrop, note } = await openingBackdrop(
|
|
10135
10410
|
strFlag(flags, "background"),
|
|
@@ -10141,8 +10416,20 @@ async function takeBackdrop(flags, r) {
|
|
|
10141
10416
|
if (note) r.log(`note: ${note}`);
|
|
10142
10417
|
return backdrop;
|
|
10143
10418
|
}
|
|
10419
|
+
async function releaseFrames(paths, flags, log) {
|
|
10420
|
+
if (flags["keep-frames"] === true) return;
|
|
10421
|
+
const freed = await dropScreencastFrames(paths);
|
|
10422
|
+
if (freed >= 1e6)
|
|
10423
|
+
log(
|
|
10424
|
+
`released ${Math.round(freed / 1e6)} MB of screencast frames (the recording holds them now; --keep-frames keeps them)`
|
|
10425
|
+
);
|
|
10426
|
+
}
|
|
10144
10427
|
async function cmdRecord(argv) {
|
|
10145
|
-
const { positionals, flags } = parseArgs(
|
|
10428
|
+
const { positionals, flags, multi } = parseArgs(
|
|
10429
|
+
argv,
|
|
10430
|
+
BOOLEAN_FLAGS5,
|
|
10431
|
+
MULTI_FLAGS2
|
|
10432
|
+
);
|
|
10146
10433
|
const actionsPath = strFlag(flags, "actions") ?? positionals[0];
|
|
10147
10434
|
if (!actionsPath)
|
|
10148
10435
|
throw new UsageError(
|
|
@@ -10156,20 +10443,23 @@ async function cmdRecord(argv) {
|
|
|
10156
10443
|
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
10157
10444
|
const backdrop = await takeBackdrop(flags, r);
|
|
10158
10445
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
10446
|
+
const storageState = takeStorageState(flags);
|
|
10447
|
+
const browserArgs = takeBrowserArgs(multi);
|
|
10159
10448
|
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
10160
10449
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
10161
10450
|
r.log(
|
|
10162
10451
|
`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" : "")
|
|
10163
10452
|
);
|
|
10164
10453
|
}
|
|
10165
|
-
await
|
|
10454
|
+
await mkdir10(outDir, { recursive: true });
|
|
10166
10455
|
const paths = await ensureTakeDir(outDir);
|
|
10167
|
-
const browser = await launchBrowser();
|
|
10456
|
+
const browser = await launchBrowser(browserArgs);
|
|
10168
10457
|
try {
|
|
10169
10458
|
r.log("recording\u2026");
|
|
10170
10459
|
r.event({ event: "phase", phase: "record" });
|
|
10171
10460
|
const rec = await recordTake(browser, url, actions, paths, r.log, {
|
|
10172
|
-
maxDurationSeconds
|
|
10461
|
+
maxDurationSeconds,
|
|
10462
|
+
storageState
|
|
10173
10463
|
});
|
|
10174
10464
|
r.event({ event: "phase", phase: "encode" });
|
|
10175
10465
|
r.log("encoding\u2026");
|
|
@@ -10178,6 +10468,7 @@ async function cmdRecord(argv) {
|
|
|
10178
10468
|
outDir,
|
|
10179
10469
|
(p) => r.event({ event: "progress", phase: "encode", fraction: p })
|
|
10180
10470
|
);
|
|
10471
|
+
await releaseFrames(paths, flags, r.log);
|
|
10181
10472
|
r.event({ event: "phase", phase: "plan" });
|
|
10182
10473
|
const plan = await planTake(outDir, { backdrop });
|
|
10183
10474
|
const clicks = rec.events.filter((e) => e.type === "down").length;
|
|
@@ -10240,20 +10531,23 @@ async function cmdCreate2(argv) {
|
|
|
10240
10531
|
}
|
|
10241
10532
|
const backdrop = await takeBackdrop(flags, r);
|
|
10242
10533
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
10534
|
+
const storageState = takeStorageState(flags);
|
|
10535
|
+
const browserArgs = takeBrowserArgs(multi);
|
|
10243
10536
|
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
10244
10537
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
10245
10538
|
r.log(
|
|
10246
10539
|
`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" : "")
|
|
10247
10540
|
);
|
|
10248
10541
|
}
|
|
10249
|
-
await
|
|
10542
|
+
await mkdir10(outDir, { recursive: true });
|
|
10250
10543
|
const paths = await ensureTakeDir(outDir);
|
|
10251
|
-
const browser = await launchBrowser();
|
|
10544
|
+
const browser = await launchBrowser(browserArgs);
|
|
10252
10545
|
try {
|
|
10253
10546
|
r.log("recording\u2026");
|
|
10254
10547
|
r.event({ event: "phase", phase: "record" });
|
|
10255
10548
|
const rec = await recordTake(browser, url, actions, paths, r.log, {
|
|
10256
|
-
maxDurationSeconds
|
|
10549
|
+
maxDurationSeconds,
|
|
10550
|
+
storageState
|
|
10257
10551
|
});
|
|
10258
10552
|
r.event({ event: "phase", phase: "encode" });
|
|
10259
10553
|
r.log("encoding\u2026");
|
|
@@ -10262,6 +10556,7 @@ async function cmdCreate2(argv) {
|
|
|
10262
10556
|
outDir,
|
|
10263
10557
|
(p) => r.event({ event: "progress", phase: "encode", fraction: p })
|
|
10264
10558
|
);
|
|
10559
|
+
await releaseFrames(paths, flags, r.log);
|
|
10265
10560
|
r.event({ event: "phase", phase: "plan" });
|
|
10266
10561
|
await planTake(outDir, { backdrop });
|
|
10267
10562
|
if (flags.strict === true && (rec.skipped.length > 0 || rec.navTimeout || rec.capped)) {
|
|
@@ -10331,7 +10626,7 @@ async function cmdPlan(argv) {
|
|
|
10331
10626
|
);
|
|
10332
10627
|
const r = createReporter(flags.json === true);
|
|
10333
10628
|
if (flags.fresh === true) {
|
|
10334
|
-
await
|
|
10629
|
+
await rm5(join18(dir, "doc.json"), { force: true });
|
|
10335
10630
|
r.log("note: --fresh discarded the existing doc.json");
|
|
10336
10631
|
}
|
|
10337
10632
|
let reuse;
|
|
@@ -10895,7 +11190,7 @@ async function cmdOpen(argv) {
|
|
|
10895
11190
|
const r = createReporter(flags.json === true);
|
|
10896
11191
|
const take = await loadTake(dir);
|
|
10897
11192
|
if (!existsSync15(take.paths.recording)) {
|
|
10898
|
-
throw new UsageError(`${dir} has no recording
|
|
11193
|
+
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
10899
11194
|
}
|
|
10900
11195
|
const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
|
|
10901
11196
|
/\/+$/,
|
|
@@ -10961,7 +11256,7 @@ async function cmdCallout(argv) {
|
|
|
10961
11256
|
const shape = positionals[1];
|
|
10962
11257
|
if (!dir || !shape || !isCalloutShape(shape))
|
|
10963
11258
|
throw new UsageError(
|
|
10964
|
-
'vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side \u2026] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex --accent #hex] [--font "\u2026"] [--body-px
|
|
11259
|
+
'vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side \u2026] [--mark ring|underline|none] [--leader] [--color #hex (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]'
|
|
10965
11260
|
);
|
|
10966
11261
|
const r = createReporter(flags.json === true);
|
|
10967
11262
|
const take = await loadTake(dir);
|
|
@@ -11012,15 +11307,16 @@ async function cmdCallout(argv) {
|
|
|
11012
11307
|
side,
|
|
11013
11308
|
mark,
|
|
11014
11309
|
leader: flags.leader === true,
|
|
11015
|
-
color: strFlag(flags, "color")
|
|
11310
|
+
color: strFlag(flags, "color"),
|
|
11311
|
+
bodyPxGiven: bodyPx !== void 0
|
|
11016
11312
|
});
|
|
11017
11313
|
} catch (e) {
|
|
11018
11314
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
11019
11315
|
}
|
|
11020
|
-
const { clip, window: window2 } = composed;
|
|
11316
|
+
const { clip, window: window2, sizes } = composed;
|
|
11021
11317
|
if (flags.print === true) {
|
|
11022
11318
|
r.done(
|
|
11023
|
-
{ clip, window: window2, register, brand: brand?.file ?? null },
|
|
11319
|
+
{ clip, window: window2, sizes, register, brand: brand?.file ?? null },
|
|
11024
11320
|
JSON.stringify(clip, null, 2)
|
|
11025
11321
|
);
|
|
11026
11322
|
return EXIT_OK;
|
|
@@ -11042,11 +11338,12 @@ async function cmdCallout(argv) {
|
|
|
11042
11338
|
{
|
|
11043
11339
|
clip,
|
|
11044
11340
|
window: window2,
|
|
11341
|
+
sizes,
|
|
11045
11342
|
register,
|
|
11046
11343
|
brand: brand?.file ?? null,
|
|
11047
11344
|
warnings: lint.warnings
|
|
11048
11345
|
},
|
|
11049
|
-
`${shape} "${clip.id}" at ${window2.start.toFixed(2)}s for ${window2.duration.toFixed(2)}s${clip.pin ? `, pinned to ${clip.pin.step !== void 0 ? `step ${String(clip.pin.step)}` : `the press at ${String(clip.pin.press)}s`}` : ""}, ground ${register.ground} \u2192 card in the product's hue, written to ${take.paths.doc}${lint.warnings.length ? `
|
|
11346
|
+
`${shape} "${clip.id}" at ${window2.start.toFixed(2)}s for ${window2.duration.toFixed(2)}s${clip.pin ? `, pinned to ${clip.pin.step !== void 0 ? `step ${String(clip.pin.step)}` : `the press at ${String(clip.pin.press)}s`}` : ""}, ground ${register.ground} \u2192 card in the product's hue, type ${sizes.kickerPx} / ${sizes.titlePx} / ${sizes.bodyPx} px (kicker / title / body; ${bodyPx !== void 0 ? `--body-px ${bodyPx} as delivered` : `the app's body as seen, footage scale ${sizes.scale}; --body-px <n> sets the delivered size`}), written to ${take.paths.doc}${lint.warnings.length ? `
|
|
11050
11347
|
warnings:
|
|
11051
11348
|
${lint.warnings.join("\n ")}` : ""}`
|
|
11052
11349
|
);
|
|
@@ -11132,8 +11429,15 @@ async function cmdValidate(argv) {
|
|
|
11132
11429
|
const take = await loadTake(target);
|
|
11133
11430
|
const problems = [];
|
|
11134
11431
|
const warnings = [];
|
|
11135
|
-
if (!existsSync15(take.paths.recording))
|
|
11136
|
-
problems.push("missing recording
|
|
11432
|
+
if (!existsSync15(take.paths.recording)) {
|
|
11433
|
+
problems.push("missing recording (re-run record)");
|
|
11434
|
+
} else {
|
|
11435
|
+
const mismatch = containerMismatch(
|
|
11436
|
+
take.paths.recordingName,
|
|
11437
|
+
await readMediaHead(take.paths.recording)
|
|
11438
|
+
);
|
|
11439
|
+
if (mismatch) warnings.push(mismatch);
|
|
11440
|
+
}
|
|
11137
11441
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
11138
11442
|
if (take.doc) {
|
|
11139
11443
|
const lint = lintDoc(take.doc);
|
|
@@ -11202,8 +11506,16 @@ async function cmdPush3(argv) {
|
|
|
11202
11506
|
const dir = positionals[0];
|
|
11203
11507
|
if (!dir)
|
|
11204
11508
|
throw new UsageError(
|
|
11205
|
-
"vos push <take> [--title|--label|--note|--folder|--override|--yes|--key|--api]"
|
|
11509
|
+
"vos push <take> [--vos|--title|--label|--note|--folder|--override|--yes|--key|--api]"
|
|
11206
11510
|
);
|
|
11511
|
+
const refusal = takePushRefusal({
|
|
11512
|
+
vos: strFlag(flags, "vos"),
|
|
11513
|
+
trackedVosId: readSyncState(resolve9(dir))?.vosId ?? null,
|
|
11514
|
+
programOnly: ["slug", "desc", "tags", "base", "remix-of"].filter(
|
|
11515
|
+
(name) => hasFlag(flags, name)
|
|
11516
|
+
)
|
|
11517
|
+
});
|
|
11518
|
+
if (refusal) throw new UsageError(refusal);
|
|
11207
11519
|
const r = createReporter(flags.json === true);
|
|
11208
11520
|
const overrides = multi.override;
|
|
11209
11521
|
const result = await pushTake(
|
|
@@ -11213,6 +11525,7 @@ async function cmdPush3(argv) {
|
|
|
11213
11525
|
api: strFlag(flags, "api"),
|
|
11214
11526
|
origin: strFlag(flags, "origin"),
|
|
11215
11527
|
yes: flags.yes === true,
|
|
11528
|
+
vos: strFlag(flags, "vos"),
|
|
11216
11529
|
title: strFlag(flags, "title"),
|
|
11217
11530
|
label: strFlag(flags, "label"),
|
|
11218
11531
|
note: strFlag(flags, "note"),
|
|
@@ -11353,6 +11666,7 @@ export {
|
|
|
11353
11666
|
RECORDING_NAME,
|
|
11354
11667
|
takePaths,
|
|
11355
11668
|
loadTake,
|
|
11669
|
+
pullMedia,
|
|
11356
11670
|
startTakeServer,
|
|
11357
11671
|
waitForPageDone,
|
|
11358
11672
|
renderAnimation,
|
|
@@ -11369,10 +11683,12 @@ export {
|
|
|
11369
11683
|
recordTake,
|
|
11370
11684
|
encodeRecording,
|
|
11371
11685
|
planTake,
|
|
11372
|
-
pullMedia,
|
|
11373
11686
|
parseAgentBrowserLog,
|
|
11374
11687
|
splitCommand,
|
|
11375
11688
|
convertAgentBrowser,
|
|
11689
|
+
MULTI_FLAGS2 as MULTI_FLAGS,
|
|
11690
|
+
HELP,
|
|
11691
|
+
takeBrowserArgs,
|
|
11376
11692
|
run
|
|
11377
11693
|
};
|
|
11378
|
-
//# sourceMappingURL=chunk-
|
|
11694
|
+
//# sourceMappingURL=chunk-LGRNHJ2U.js.map
|