@vosjs/cli 0.36.0 → 0.37.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 +9 -1
- package/dist/{chunk-MIZWK5AN.js → chunk-DD6OXVFJ.js} +614 -439
- package/dist/chunk-DD6OXVFJ.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/{run-LHMYTKV7.js → run-6T4NK5P2.js} +2 -2
- package/package.json +4 -4
- package/dist/chunk-MIZWK5AN.js.map +0 -1
- /package/dist/{run-LHMYTKV7.js.map → run-6T4NK5P2.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-NHKHTIDR.js";
|
|
7
7
|
|
|
8
8
|
// src/plugin/run.ts
|
|
9
|
-
import { mkdir as
|
|
9
|
+
import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
|
|
10
10
|
import { existsSync as existsSync15 } from "fs";
|
|
11
11
|
import { join as join18, resolve as resolve9 } from "path";
|
|
12
12
|
import {
|
|
@@ -1681,6 +1681,378 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
|
|
|
1681
1681
|
}
|
|
1682
1682
|
}
|
|
1683
1683
|
|
|
1684
|
+
// src/plugin/container.ts
|
|
1685
|
+
var OCTET_STREAM = "application/octet-stream";
|
|
1686
|
+
var TYPES_BY_EXTENSION = {
|
|
1687
|
+
svg: "image/svg+xml",
|
|
1688
|
+
png: "image/png",
|
|
1689
|
+
jpg: "image/jpeg",
|
|
1690
|
+
jpeg: "image/jpeg",
|
|
1691
|
+
webp: "image/webp",
|
|
1692
|
+
gif: "image/gif",
|
|
1693
|
+
avif: "image/avif",
|
|
1694
|
+
webm: "video/webm",
|
|
1695
|
+
mp4: "video/mp4",
|
|
1696
|
+
mov: "video/quicktime",
|
|
1697
|
+
mp3: "audio/mpeg",
|
|
1698
|
+
wav: "audio/wav",
|
|
1699
|
+
ogg: "audio/ogg",
|
|
1700
|
+
m4a: "audio/mp4"
|
|
1701
|
+
};
|
|
1702
|
+
var EXTENSION_BY_TYPE = {
|
|
1703
|
+
"image/jpeg": ".jpg",
|
|
1704
|
+
"video/mp4": ".mp4",
|
|
1705
|
+
"audio/mp4": ".m4a"
|
|
1706
|
+
};
|
|
1707
|
+
var TAKE_MEDIA_EXTENSIONS = [
|
|
1708
|
+
".webm",
|
|
1709
|
+
".mp4",
|
|
1710
|
+
".mov",
|
|
1711
|
+
".m4a",
|
|
1712
|
+
".mp3",
|
|
1713
|
+
".ogg",
|
|
1714
|
+
".wav"
|
|
1715
|
+
];
|
|
1716
|
+
var MEDIA_HEAD_BYTES = 16;
|
|
1717
|
+
var ISO_BRANDS = {
|
|
1718
|
+
"qt ": "video/quicktime",
|
|
1719
|
+
"M4A ": "audio/mp4",
|
|
1720
|
+
"M4B ": "audio/mp4",
|
|
1721
|
+
avif: "image/avif",
|
|
1722
|
+
avis: "image/avif"
|
|
1723
|
+
};
|
|
1724
|
+
var RIFF_FORMS = {
|
|
1725
|
+
WEBP: "image/webp",
|
|
1726
|
+
WAVE: "audio/wav",
|
|
1727
|
+
"AVI ": "video/x-msvideo"
|
|
1728
|
+
};
|
|
1729
|
+
function sniffMediaType(head) {
|
|
1730
|
+
const ascii = (at2, len) => head.length >= at2 + len ? String.fromCharCode(...head.subarray(at2, at2 + len)) : "";
|
|
1731
|
+
const startsWith = (...bytes) => bytes.every((b, i) => head[i] === b);
|
|
1732
|
+
if (startsWith(26, 69, 223, 163)) return "video/webm";
|
|
1733
|
+
if (ascii(4, 4) === "ftyp") return ISO_BRANDS[ascii(8, 4)] ?? "video/mp4";
|
|
1734
|
+
if (ascii(0, 4) === "RIFF") return RIFF_FORMS[ascii(8, 4)] ?? null;
|
|
1735
|
+
if (startsWith(137, 80, 78, 71, 13, 10, 26, 10))
|
|
1736
|
+
return "image/png";
|
|
1737
|
+
if (startsWith(255, 216, 255)) return "image/jpeg";
|
|
1738
|
+
if (ascii(0, 4) === "GIF8") return "image/gif";
|
|
1739
|
+
if (ascii(0, 4) === "OggS") return "audio/ogg";
|
|
1740
|
+
if (ascii(0, 3) === "ID3") return "audio/mpeg";
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
function bareType(value) {
|
|
1744
|
+
const type = (value ?? "").split(";")[0].trim().toLowerCase();
|
|
1745
|
+
return !type || type === OCTET_STREAM || type === "binary/octet-stream" ? "" : type;
|
|
1746
|
+
}
|
|
1747
|
+
function mediaContentType(file) {
|
|
1748
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
|
|
1749
|
+
return TYPES_BY_EXTENSION[ext] ?? OCTET_STREAM;
|
|
1750
|
+
}
|
|
1751
|
+
function extensionFor(contentType) {
|
|
1752
|
+
const type = (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
1753
|
+
if (EXTENSION_BY_TYPE[type]) return EXTENSION_BY_TYPE[type];
|
|
1754
|
+
const ext = Object.keys(TYPES_BY_EXTENSION).find(
|
|
1755
|
+
(e) => TYPES_BY_EXTENSION[e] === type
|
|
1756
|
+
);
|
|
1757
|
+
return ext ? `.${ext}` : "";
|
|
1758
|
+
}
|
|
1759
|
+
function resolveMediaType(claims) {
|
|
1760
|
+
const sniffed = claims.head?.length ? sniffMediaType(claims.head) : null;
|
|
1761
|
+
if (sniffed) return { type: sniffed, from: "bytes" };
|
|
1762
|
+
const declared = bareType(claims.declared);
|
|
1763
|
+
if (declared) return { type: declared, from: "declared" };
|
|
1764
|
+
const named = mediaContentType(claims.filename ?? "");
|
|
1765
|
+
if (named !== OCTET_STREAM) return { type: named, from: "filename" };
|
|
1766
|
+
return { type: OCTET_STREAM, from: "unknown" };
|
|
1767
|
+
}
|
|
1768
|
+
function nameForType(filename, type) {
|
|
1769
|
+
const ext = extensionFor(type);
|
|
1770
|
+
if (!ext) return filename;
|
|
1771
|
+
const current = /\.[a-z0-9]+$/i.exec(filename)?.[0] ?? "";
|
|
1772
|
+
if (current && mediaContentType(`x${current}`) === type) return filename;
|
|
1773
|
+
return `${filename.slice(0, filename.length - current.length)}${ext}`;
|
|
1774
|
+
}
|
|
1775
|
+
function containerMismatch(filename, head) {
|
|
1776
|
+
const actual = sniffMediaType(head);
|
|
1777
|
+
if (!actual) return null;
|
|
1778
|
+
const claimed = mediaContentType(filename);
|
|
1779
|
+
if (claimed === actual) return null;
|
|
1780
|
+
return `${filename} is ${actual}, not the ${claimed} its name claims \u2014 rename it to ${nameForType(filename, actual)}`;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/plugin/media.ts
|
|
1784
|
+
import { isKeyedOverlay } from "@vosjs/studio-core";
|
|
1785
|
+
import { createWriteStream, existsSync as existsSync2 } from "fs";
|
|
1786
|
+
import {
|
|
1787
|
+
mkdir as mkdir2,
|
|
1788
|
+
open,
|
|
1789
|
+
readdir,
|
|
1790
|
+
rename as rename2,
|
|
1791
|
+
rm as rm2,
|
|
1792
|
+
stat,
|
|
1793
|
+
writeFile as writeFile2
|
|
1794
|
+
} from "fs/promises";
|
|
1795
|
+
import { basename as basename2, dirname, join as join2 } from "path";
|
|
1796
|
+
import { Readable } from "stream";
|
|
1797
|
+
import { pipeline } from "stream/promises";
|
|
1798
|
+
|
|
1799
|
+
// src/plugin/take.ts
|
|
1800
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1801
|
+
import { existsSync } from "fs";
|
|
1802
|
+
import { basename, join } from "path";
|
|
1803
|
+
var RECORDING_NAME = "recording.webm";
|
|
1804
|
+
function takeMediaFiles(dir, stem) {
|
|
1805
|
+
return TAKE_MEDIA_EXTENSIONS.map((ext) => join(dir, `${stem}${ext}`)).filter(
|
|
1806
|
+
(file) => existsSync(file)
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
function takeMediaFile(dir, stem, fallback) {
|
|
1810
|
+
return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback);
|
|
1811
|
+
}
|
|
1812
|
+
var PREV_DOC_NAME = "doc.prev.json";
|
|
1813
|
+
function takePaths(dir) {
|
|
1814
|
+
const recording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
1815
|
+
return {
|
|
1816
|
+
dir,
|
|
1817
|
+
recording,
|
|
1818
|
+
recordingName: basename(recording),
|
|
1819
|
+
framesDir: join(dir, "frames"),
|
|
1820
|
+
framesIndex: join(dir, "frames.json"),
|
|
1821
|
+
cursor: join(dir, "cursor.json"),
|
|
1822
|
+
meta: join(dir, "meta.json"),
|
|
1823
|
+
actions: join(dir, "actions.json"),
|
|
1824
|
+
doc: join(dir, "doc.json")
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
async function ensureTakeDir(dir) {
|
|
1828
|
+
const p = takePaths(dir);
|
|
1829
|
+
await mkdir(p.framesDir, { recursive: true });
|
|
1830
|
+
return p;
|
|
1831
|
+
}
|
|
1832
|
+
async function prepareReRecord(dir) {
|
|
1833
|
+
const p = takePaths(dir);
|
|
1834
|
+
const kept = [];
|
|
1835
|
+
let prevDoc = false;
|
|
1836
|
+
if (existsSync(p.doc)) {
|
|
1837
|
+
await rename(p.doc, join(dir, PREV_DOC_NAME));
|
|
1838
|
+
prevDoc = true;
|
|
1839
|
+
kept.push(PREV_DOC_NAME);
|
|
1840
|
+
}
|
|
1841
|
+
if (existsSync(p.actions)) kept.push("actions.json");
|
|
1842
|
+
if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
|
|
1843
|
+
for (const stale of [
|
|
1844
|
+
// Every container, not just the one `record` writes: a pulled take's
|
|
1845
|
+
// footage may be mp4, and a survivor would outrank the new recording.
|
|
1846
|
+
...takeMediaFiles(dir, "recording"),
|
|
1847
|
+
...takeMediaFiles(dir, "mic"),
|
|
1848
|
+
...takeMediaFiles(dir, "cam"),
|
|
1849
|
+
p.cursor,
|
|
1850
|
+
p.meta,
|
|
1851
|
+
p.framesIndex,
|
|
1852
|
+
p.framesDir,
|
|
1853
|
+
join(dir, "stills"),
|
|
1854
|
+
join(dir, "digest")
|
|
1855
|
+
]) {
|
|
1856
|
+
await rm(stale, { recursive: true, force: true });
|
|
1857
|
+
}
|
|
1858
|
+
return { prevDoc, kept };
|
|
1859
|
+
}
|
|
1860
|
+
async function readJson(file) {
|
|
1861
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
1862
|
+
}
|
|
1863
|
+
async function writeJson(file, value, pretty = false) {
|
|
1864
|
+
await writeFile(
|
|
1865
|
+
file,
|
|
1866
|
+
pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
async function loadTake(dir) {
|
|
1870
|
+
const paths = takePaths(dir);
|
|
1871
|
+
if (!existsSync(paths.meta)) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
`${dir} is not a take directory (missing meta.json) \u2014 run record first`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
const meta = await readJson(paths.meta);
|
|
1877
|
+
const cursor = existsSync(paths.cursor) ? await readJson(paths.cursor) : [];
|
|
1878
|
+
const actions = existsSync(paths.actions) ? await readJson(paths.actions) : null;
|
|
1879
|
+
const doc = existsSync(paths.doc) ? await readJson(paths.doc) : null;
|
|
1880
|
+
return { paths, cursor, meta, actions, doc };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/plugin/media.ts
|
|
1884
|
+
var SOURCE_MEDIA = [
|
|
1885
|
+
{ key: "videoKey", stem: "recording", fallbackExt: ".webm" },
|
|
1886
|
+
{ key: "micKey", stem: "mic", fallbackExt: ".webm" },
|
|
1887
|
+
{ key: "camKey", stem: "cam", fallbackExt: ".webm" }
|
|
1888
|
+
];
|
|
1889
|
+
function isTakeRelativeKey(key) {
|
|
1890
|
+
return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
|
|
1891
|
+
}
|
|
1892
|
+
var takeRelativeFile = (key) => key.replace(/^\/+/, "");
|
|
1893
|
+
function docMediaRefs(doc, keep = isTakeRelativeKey) {
|
|
1894
|
+
const out = [];
|
|
1895
|
+
for (const m of doc.media ?? []) {
|
|
1896
|
+
if (keep(m.videoKey))
|
|
1897
|
+
out.push({
|
|
1898
|
+
where: `media ${m.id}`,
|
|
1899
|
+
key: m.videoKey,
|
|
1900
|
+
set: (next) => {
|
|
1901
|
+
m.videoKey = next;
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
const mic = m.micKey;
|
|
1905
|
+
if (mic && keep(mic))
|
|
1906
|
+
out.push({
|
|
1907
|
+
where: `media ${m.id} mic`,
|
|
1908
|
+
key: mic,
|
|
1909
|
+
set: (next) => {
|
|
1910
|
+
m.micKey = next;
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
const cam = m.camKey;
|
|
1914
|
+
if (cam && keep(cam))
|
|
1915
|
+
out.push({
|
|
1916
|
+
where: `media ${m.id} cam`,
|
|
1917
|
+
key: cam,
|
|
1918
|
+
set: (next) => {
|
|
1919
|
+
m.camKey = next;
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
for (const clip of doc.overlays ?? []) {
|
|
1924
|
+
if (!isKeyedOverlay(clip) || !keep(clip.key)) continue;
|
|
1925
|
+
out.push({
|
|
1926
|
+
where: `overlay ${clip.id}`,
|
|
1927
|
+
key: clip.key,
|
|
1928
|
+
set: (next) => {
|
|
1929
|
+
clip.key = next;
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
const mark = doc.endCard?.mark;
|
|
1934
|
+
if (mark && keep(mark.key))
|
|
1935
|
+
out.push({
|
|
1936
|
+
where: "end card mark",
|
|
1937
|
+
key: mark.key,
|
|
1938
|
+
set: (next) => {
|
|
1939
|
+
mark.key = next;
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
const bg = doc.frame.backgroundMedia;
|
|
1943
|
+
if (bg && keep(bg.key))
|
|
1944
|
+
out.push({
|
|
1945
|
+
where: "background",
|
|
1946
|
+
key: bg.key,
|
|
1947
|
+
set: (next) => {
|
|
1948
|
+
bg.key = next;
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
function assetIdOf(url) {
|
|
1954
|
+
if (!url) return null;
|
|
1955
|
+
const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
|
|
1956
|
+
return m ? m[1] : null;
|
|
1957
|
+
}
|
|
1958
|
+
async function readMediaHead(file) {
|
|
1959
|
+
const handle = await open(file, "r");
|
|
1960
|
+
try {
|
|
1961
|
+
const buf = Buffer.alloc(MEDIA_HEAD_BYTES);
|
|
1962
|
+
const { bytesRead } = await handle.read(buf, 0, MEDIA_HEAD_BYTES, 0);
|
|
1963
|
+
return new Uint8Array(buf.subarray(0, bytesRead));
|
|
1964
|
+
} finally {
|
|
1965
|
+
await handle.close();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function downloadMedia(ctx, url, target) {
|
|
1969
|
+
const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
|
|
1970
|
+
const res = await fetch(abs, {
|
|
1971
|
+
headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
|
|
1972
|
+
});
|
|
1973
|
+
if (!res.ok || !res.body) {
|
|
1974
|
+
throw new Error(
|
|
1975
|
+
`download of ${target.describe} failed (${res.status})${res.status === 404 && target.notFoundHint ? ` \u2014 ${target.notFoundHint}` : ""}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
const into = target.subdir ? join2(target.dir, target.subdir) : target.dir;
|
|
1979
|
+
const partial = join2(into, `${target.stem}.part`);
|
|
1980
|
+
await mkdir2(dirname(partial), { recursive: true });
|
|
1981
|
+
try {
|
|
1982
|
+
await pipeline(
|
|
1983
|
+
Readable.fromWeb(res.body),
|
|
1984
|
+
createWriteStream(partial)
|
|
1985
|
+
);
|
|
1986
|
+
const { type } = resolveMediaType({
|
|
1987
|
+
head: await readMediaHead(partial),
|
|
1988
|
+
declared: res.headers.get("content-type"),
|
|
1989
|
+
filename: `${target.stem}${target.fallbackExt}`
|
|
1990
|
+
});
|
|
1991
|
+
const name = `${target.stem}${extensionFor(type) || target.fallbackExt}`;
|
|
1992
|
+
await rename2(partial, join2(into, name));
|
|
1993
|
+
const file = target.subdir ? `${target.subdir}/${name}` : name;
|
|
1994
|
+
return { file, bytes: (await stat(join2(target.dir, file))).size };
|
|
1995
|
+
} finally {
|
|
1996
|
+
await rm2(partial, { force: true });
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
async function pullMedia(ctx, dir, doc, log) {
|
|
2000
|
+
const result = { downloaded: [], kept: [] };
|
|
2001
|
+
for (const { key, stem, fallbackExt } of SOURCE_MEDIA) {
|
|
2002
|
+
const url = doc.source[key];
|
|
2003
|
+
const assetId = assetIdOf(url);
|
|
2004
|
+
if (!assetId) continue;
|
|
2005
|
+
const present = takeMediaFiles(dir, stem)[0];
|
|
2006
|
+
if (present) {
|
|
2007
|
+
result.kept.push(basename2(present));
|
|
2008
|
+
doc.source[key] = basename2(present);
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
const got = await downloadMedia(ctx, url, {
|
|
2012
|
+
dir,
|
|
2013
|
+
stem,
|
|
2014
|
+
fallbackExt,
|
|
2015
|
+
describe: stem,
|
|
2016
|
+
notFoundHint: "a private recording needs a content key of its owner"
|
|
2017
|
+
});
|
|
2018
|
+
result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
|
|
2019
|
+
log(` ${got.file} \u2190 asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`);
|
|
2020
|
+
doc.source[key] = got.file;
|
|
2021
|
+
}
|
|
2022
|
+
for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
|
|
2023
|
+
const assetId = assetIdOf(ref.key);
|
|
2024
|
+
const existing = existsSync2(join2(dir, "media")) ? (await readdir(join2(dir, "media"))).find(
|
|
2025
|
+
(f) => f === assetId || f.startsWith(`${assetId}.`)
|
|
2026
|
+
) : void 0;
|
|
2027
|
+
if (existing) {
|
|
2028
|
+
const file = `media/${existing}`;
|
|
2029
|
+
result.kept.push(file);
|
|
2030
|
+
ref.set(file);
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
const got = await downloadMedia(ctx, ref.key, {
|
|
2034
|
+
dir,
|
|
2035
|
+
subdir: "media",
|
|
2036
|
+
stem: assetId,
|
|
2037
|
+
// A type with no extension here (an svg served as such) keeps the bare
|
|
2038
|
+
// asset id, exactly as before.
|
|
2039
|
+
fallbackExt: "",
|
|
2040
|
+
describe: `${ref.where} (${ref.key})`
|
|
2041
|
+
});
|
|
2042
|
+
result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
|
|
2043
|
+
log(
|
|
2044
|
+
` ${got.file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`
|
|
2045
|
+
);
|
|
2046
|
+
ref.set(got.file);
|
|
2047
|
+
}
|
|
2048
|
+
const metaPath = join2(dir, "meta.json");
|
|
2049
|
+
if (!existsSync2(metaPath)) await writeJson(metaPath, doc.source.meta, true);
|
|
2050
|
+
const cursorPath = join2(dir, "cursor.json");
|
|
2051
|
+
if (!existsSync2(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
|
|
2052
|
+
await writeFile2(join2(dir, "doc.json"), JSON.stringify(doc, null, 2));
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1684
2056
|
// src/plugin/mediaProbe.ts
|
|
1685
2057
|
var MEDIA_EXT = /\.(png|jpe?g|webp|gif|webm|mp4|mov|mp3|wav|m4a|ogg|glb|gltf|hdr|exr|woff2?)($|\?)/i;
|
|
1686
2058
|
var isRemote = (v) => typeof v === "string" && /^https?:\/\//i.test(v);
|
|
@@ -1766,9 +2138,9 @@ function mediaProbeLints(results) {
|
|
|
1766
2138
|
}
|
|
1767
2139
|
|
|
1768
2140
|
// src/plugin/framesTake.ts
|
|
1769
|
-
import { mkdir as
|
|
1770
|
-
import { existsSync as
|
|
1771
|
-
import { join as
|
|
2141
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile5 } from "fs/promises";
|
|
2142
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2143
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
1772
2144
|
import { compileVosConfig } from "@vosjs/core";
|
|
1773
2145
|
import {
|
|
1774
2146
|
docStillTime,
|
|
@@ -1780,83 +2152,11 @@ import {
|
|
|
1780
2152
|
spanOutputExtent as spanOutputExtent2
|
|
1781
2153
|
} from "@vosjs/studio-core";
|
|
1782
2154
|
|
|
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
2155
|
// src/plugin/server.ts
|
|
1856
2156
|
import { createServer } from "http";
|
|
1857
|
-
import { existsSync as
|
|
1858
|
-
import { writeFile as
|
|
1859
|
-
import { extname, join as
|
|
2157
|
+
import { existsSync as existsSync3, readFileSync, statSync } from "fs";
|
|
2158
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2159
|
+
import { extname, join as join3 } from "path";
|
|
1860
2160
|
var MIME = {
|
|
1861
2161
|
".html": "text/html",
|
|
1862
2162
|
".js": "text/javascript",
|
|
@@ -1890,7 +2190,7 @@ function startTakeServer(rootDir, pages) {
|
|
|
1890
2190
|
const chunks = [];
|
|
1891
2191
|
req.on("data", (c) => chunks.push(c));
|
|
1892
2192
|
req.on("end", () => {
|
|
1893
|
-
void
|
|
2193
|
+
void writeFile3(join3(rootDir, name), Buffer.concat(chunks)).then(
|
|
1894
2194
|
() => res.writeHead(200).end("ok"),
|
|
1895
2195
|
(e) => res.writeHead(500).end(String(e))
|
|
1896
2196
|
);
|
|
@@ -1902,11 +2202,11 @@ function startTakeServer(rootDir, pages) {
|
|
|
1902
2202
|
res.end(pages[url.pathname]);
|
|
1903
2203
|
return;
|
|
1904
2204
|
}
|
|
1905
|
-
const file =
|
|
1906
|
-
if (
|
|
1907
|
-
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
2205
|
+
const file = join3(rootDir, decodeURIComponent(url.pathname));
|
|
2206
|
+
if (existsSync3(file) && statSync(file).isFile()) {
|
|
1908
2207
|
const size = statSync(file).size;
|
|
1909
2208
|
const buf = readFileSync(file);
|
|
2209
|
+
const type = sniffMediaType(buf.subarray(0, MEDIA_HEAD_BYTES)) ?? MIME[extname(file)] ?? "application/octet-stream";
|
|
1910
2210
|
const range = req.headers.range;
|
|
1911
2211
|
const m = range && /^bytes=(\d*)-(\d*)$/.exec(range);
|
|
1912
2212
|
if (m) {
|
|
@@ -1969,7 +2269,7 @@ async function waitForPageDone(page, label, onProgress, timeoutMs) {
|
|
|
1969
2269
|
}
|
|
1970
2270
|
|
|
1971
2271
|
// src/plugin/renderAnimation.ts
|
|
1972
|
-
import { readFile as readFile2, rm as
|
|
2272
|
+
import { readFile as readFile2, rm as rm3 } from "fs/promises";
|
|
1973
2273
|
import { tweenRuntimeCode } from "@vosjs/tween/bundle";
|
|
1974
2274
|
import { elementsBundleCode } from "@vosjs/elements/bundle";
|
|
1975
2275
|
import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
|
|
@@ -2267,7 +2567,7 @@ async function renderAnimation(browser, opts) {
|
|
|
2267
2567
|
outNames.map((name) => readFile2(`${opts.workDir}/${name}`))
|
|
2268
2568
|
);
|
|
2269
2569
|
await Promise.all(
|
|
2270
|
-
outNames.map((name) =>
|
|
2570
|
+
outNames.map((name) => rm3(`${opts.workDir}/${name}`, { force: true }))
|
|
2271
2571
|
);
|
|
2272
2572
|
if (chunks.length === 1) {
|
|
2273
2573
|
return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
|
|
@@ -2522,9 +2822,9 @@ function applyAndValidate(doc, o) {
|
|
|
2522
2822
|
}
|
|
2523
2823
|
|
|
2524
2824
|
// src/plugin/digestTake.ts
|
|
2525
|
-
import { mkdir as
|
|
2526
|
-
import { existsSync as
|
|
2527
|
-
import { join as
|
|
2825
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile4 } from "fs/promises";
|
|
2826
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2827
|
+
import { join as join4, resolve } from "path";
|
|
2528
2828
|
import {
|
|
2529
2829
|
DIGEST_CROP_MAX,
|
|
2530
2830
|
DIGEST_FULL_MAX,
|
|
@@ -2746,8 +3046,8 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2746
3046
|
const meta = doc.source.meta;
|
|
2747
3047
|
let plan = planForDigest(doc);
|
|
2748
3048
|
const transcript = opts.transcript ?? await discoverTranscript(dir);
|
|
2749
|
-
const outDir = resolve(opts.outDir ??
|
|
2750
|
-
await
|
|
3049
|
+
const outDir = resolve(opts.outDir ?? join4(dir, DIGEST_DIR));
|
|
3050
|
+
await mkdir3(outDir, { recursive: true });
|
|
2751
3051
|
const fullMax = opts.full ?? DIGEST_FULL_MAX;
|
|
2752
3052
|
const cropMax = opts.crop ?? DIGEST_CROP_MAX;
|
|
2753
3053
|
const wantFrames = opts.frames !== false && browser !== null;
|
|
@@ -2759,8 +3059,11 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2759
3059
|
let sheet = null;
|
|
2760
3060
|
let moments;
|
|
2761
3061
|
const boxes = /* @__PURE__ */ new Map();
|
|
2762
|
-
let source = {
|
|
2763
|
-
|
|
3062
|
+
let source = {
|
|
3063
|
+
kind: "video",
|
|
3064
|
+
url: `/${take.paths.recordingName}`
|
|
3065
|
+
};
|
|
3066
|
+
if (existsSync4(take.paths.framesIndex) && existsSync4(take.paths.framesDir)) {
|
|
2764
3067
|
const index = await readJson(
|
|
2765
3068
|
take.paths.framesIndex
|
|
2766
3069
|
);
|
|
@@ -2775,9 +3078,9 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2775
3078
|
}
|
|
2776
3079
|
}
|
|
2777
3080
|
if (wantFrames) {
|
|
2778
|
-
if (source.kind === "video" && !
|
|
3081
|
+
if (source.kind === "video" && !existsSync4(take.paths.recording))
|
|
2779
3082
|
throw new Error(
|
|
2780
|
-
`${dir} has no
|
|
3083
|
+
`${dir} has no recording \u2014 pull it with \`vos pull --media\` first`
|
|
2781
3084
|
);
|
|
2782
3085
|
const server = await startTakeServer(dir, {
|
|
2783
3086
|
"/digest.html": digestPageHtml()
|
|
@@ -2848,9 +3151,9 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2848
3151
|
);
|
|
2849
3152
|
sizes = f.sizes;
|
|
2850
3153
|
for (const name of Object.keys(sizes)) {
|
|
2851
|
-
const from =
|
|
2852
|
-
const to =
|
|
2853
|
-
if (from !== to &&
|
|
3154
|
+
const from = join4(dir, name);
|
|
3155
|
+
const to = join4(outDir, name);
|
|
3156
|
+
if (from !== to && existsSync4(from)) await rename3(from, to);
|
|
2854
3157
|
}
|
|
2855
3158
|
if (sizes["sheet.png"]) sheet = "sheet.png";
|
|
2856
3159
|
} finally {
|
|
@@ -2876,12 +3179,12 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2876
3179
|
if (!p) continue;
|
|
2877
3180
|
for (const f of [p.full, p.crop]) {
|
|
2878
3181
|
if (!f) continue;
|
|
2879
|
-
const size = await pngSize(
|
|
3182
|
+
const size = await pngSize(join4(outDir, f));
|
|
2880
3183
|
if (size) sizes[f] = size;
|
|
2881
3184
|
}
|
|
2882
3185
|
if (p.box) boxes.set(m.id, p.box);
|
|
2883
3186
|
}
|
|
2884
|
-
if (prior.images.sheet &&
|
|
3187
|
+
if (prior.images.sheet && existsSync4(join4(outDir, prior.images.sheet)))
|
|
2885
3188
|
sheet = prior.images.sheet;
|
|
2886
3189
|
}
|
|
2887
3190
|
}
|
|
@@ -2910,14 +3213,14 @@ async function digestTake(browser, dir, opts = {}) {
|
|
|
2910
3213
|
style: opts.style ?? null,
|
|
2911
3214
|
transcript
|
|
2912
3215
|
});
|
|
2913
|
-
const file =
|
|
3216
|
+
const file = join4(outDir, "digest.json");
|
|
2914
3217
|
const body = JSON.stringify(digest, null, 2);
|
|
2915
|
-
await
|
|
3218
|
+
await writeFile4(file, body);
|
|
2916
3219
|
return { file, outDir, digest, bytes: Buffer.byteLength(body) };
|
|
2917
3220
|
}
|
|
2918
3221
|
async function discoverTranscript(dir) {
|
|
2919
|
-
const file =
|
|
2920
|
-
if (!
|
|
3222
|
+
const file = join4(dir, "transcript.json");
|
|
3223
|
+
if (!existsSync4(file)) return null;
|
|
2921
3224
|
try {
|
|
2922
3225
|
return parseTranscript(JSON.parse(await readFile3(file, "utf8")));
|
|
2923
3226
|
} catch {
|
|
@@ -2927,7 +3230,7 @@ async function discoverTranscript(dir) {
|
|
|
2927
3230
|
async function readPriorDigest(outDir) {
|
|
2928
3231
|
try {
|
|
2929
3232
|
const raw = JSON.parse(
|
|
2930
|
-
await readFile3(
|
|
3233
|
+
await readFile3(join4(outDir, "digest.json"), "utf8")
|
|
2931
3234
|
);
|
|
2932
3235
|
return raw.digestVersion === DIGEST_VERSION && Array.isArray(raw.moments) ? raw : null;
|
|
2933
3236
|
} catch {
|
|
@@ -3111,8 +3414,8 @@ async function framesTake(browser, dir, opts) {
|
|
|
3111
3414
|
throw new Error(
|
|
3112
3415
|
"no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
|
|
3113
3416
|
);
|
|
3114
|
-
const outDir = resolve2(opts.outDir ??
|
|
3115
|
-
await
|
|
3417
|
+
const outDir = resolve2(opts.outDir ?? join5(dir, "stills"));
|
|
3418
|
+
await mkdir4(outDir, { recursive: true });
|
|
3116
3419
|
const named = shots.map((s, i) => ({
|
|
3117
3420
|
...s,
|
|
3118
3421
|
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 +3443,7 @@ async function framesTake(browser, dir, opts) {
|
|
|
3140
3443
|
void page.evaluate(stillsInPage, {
|
|
3141
3444
|
animationCode,
|
|
3142
3445
|
token: VIDEO_TOKEN,
|
|
3143
|
-
videoUrl: `/${
|
|
3446
|
+
videoUrl: `/${take.paths.recordingName}`,
|
|
3144
3447
|
W: width,
|
|
3145
3448
|
H: height,
|
|
3146
3449
|
ss,
|
|
@@ -3156,9 +3459,9 @@ async function framesTake(browser, dir, opts) {
|
|
|
3156
3459
|
);
|
|
3157
3460
|
const frames = [];
|
|
3158
3461
|
for (const n of named) {
|
|
3159
|
-
const from =
|
|
3160
|
-
const to =
|
|
3161
|
-
if (from !== to) await
|
|
3462
|
+
const from = join5(dir, n.name);
|
|
3463
|
+
const to = join5(outDir, n.name);
|
|
3464
|
+
if (from !== to) await rename4(from, to);
|
|
3162
3465
|
frames.push({
|
|
3163
3466
|
file: to,
|
|
3164
3467
|
time: n.time,
|
|
@@ -3173,8 +3476,8 @@ async function framesTake(browser, dir, opts) {
|
|
|
3173
3476
|
}
|
|
3174
3477
|
}
|
|
3175
3478
|
async function momentsFor(dir, doc) {
|
|
3176
|
-
const file =
|
|
3177
|
-
if (
|
|
3479
|
+
const file = join5(dir, DIGEST_DIR, "digest.json");
|
|
3480
|
+
if (existsSync5(file)) {
|
|
3178
3481
|
try {
|
|
3179
3482
|
const d = JSON.parse(await readFile4(file, "utf8"));
|
|
3180
3483
|
if (Array.isArray(d.moments)) return d.moments;
|
|
@@ -3196,8 +3499,8 @@ function parseTimes(spec, duration) {
|
|
|
3196
3499
|
});
|
|
3197
3500
|
}
|
|
3198
3501
|
async function writeIndexJson(result) {
|
|
3199
|
-
const index =
|
|
3200
|
-
await
|
|
3502
|
+
const index = join5(result.outDir, "stills.json");
|
|
3503
|
+
await writeFile5(
|
|
3201
3504
|
index,
|
|
3202
3505
|
JSON.stringify(
|
|
3203
3506
|
{
|
|
@@ -3214,10 +3517,10 @@ async function writeIndexJson(result) {
|
|
|
3214
3517
|
}
|
|
3215
3518
|
|
|
3216
3519
|
// src/plugin/deliver.ts
|
|
3217
|
-
import { mkdir as
|
|
3520
|
+
import { mkdir as mkdir5, mkdtemp, rename as rename5, rm as rm4, stat as stat2, writeFile as writeFile7 } from "fs/promises";
|
|
3218
3521
|
import { tmpdir } from "os";
|
|
3219
|
-
import { join as
|
|
3220
|
-
import { existsSync as
|
|
3522
|
+
import { join as join8, relative, resolve as resolve4 } from "path";
|
|
3523
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3221
3524
|
import { readFile as readFile6 } from "fs/promises";
|
|
3222
3525
|
import { parseFrontmatter } from "@vosjs/shared/frontmatter";
|
|
3223
3526
|
import {
|
|
@@ -3232,7 +3535,7 @@ import {
|
|
|
3232
3535
|
} from "@vosjs/studio-core";
|
|
3233
3536
|
|
|
3234
3537
|
// src/plugin/renderTake.ts
|
|
3235
|
-
import { writeFile as
|
|
3538
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
3236
3539
|
import { compileVosConfig as compileVosConfig2 } from "@vosjs/core";
|
|
3237
3540
|
import {
|
|
3238
3541
|
audioProducerCode,
|
|
@@ -3304,7 +3607,7 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
3304
3607
|
const result = await renderAnimation(browser, {
|
|
3305
3608
|
animationCode,
|
|
3306
3609
|
workDir: dir,
|
|
3307
|
-
videoFile:
|
|
3610
|
+
videoFile: take.paths.recordingName,
|
|
3308
3611
|
videoToken: VIDEO_TOKEN2,
|
|
3309
3612
|
width,
|
|
3310
3613
|
height,
|
|
@@ -3321,7 +3624,7 @@ async function renderTake(browser, dir, outFile, opts) {
|
|
|
3321
3624
|
} : void 0,
|
|
3322
3625
|
onProgress: opts.onProgress
|
|
3323
3626
|
});
|
|
3324
|
-
await
|
|
3627
|
+
await writeFile6(outFile, result.bytes);
|
|
3325
3628
|
return {
|
|
3326
3629
|
out: outFile,
|
|
3327
3630
|
bytes: result.bytes.length,
|
|
@@ -4197,9 +4500,9 @@ function isLightHexGround(hex2) {
|
|
|
4197
4500
|
}
|
|
4198
4501
|
|
|
4199
4502
|
// src/plugin/posterDoc.ts
|
|
4200
|
-
import { existsSync as
|
|
4503
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4201
4504
|
import { readFile as readFile5 } from "fs/promises";
|
|
4202
|
-
import { isAbsolute, join as
|
|
4505
|
+
import { isAbsolute, join as join7, resolve as resolve3 } from "path";
|
|
4203
4506
|
import {
|
|
4204
4507
|
computeCardLayout,
|
|
4205
4508
|
docStillTime as docStillTime2,
|
|
@@ -4211,7 +4514,7 @@ import {
|
|
|
4211
4514
|
// src/plugin/platform.ts
|
|
4212
4515
|
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4213
4516
|
import { homedir } from "os";
|
|
4214
|
-
import { join as
|
|
4517
|
+
import { join as join6 } from "path";
|
|
4215
4518
|
function platformOrigin(flags = {}) {
|
|
4216
4519
|
const legacyEnv = process.env.VOS_API_BASE?.trim();
|
|
4217
4520
|
const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
|
|
@@ -4244,7 +4547,7 @@ function parseVosId(input) {
|
|
|
4244
4547
|
function deriveSlug(title) {
|
|
4245
4548
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
|
|
4246
4549
|
}
|
|
4247
|
-
var CREDENTIALS_PATH =
|
|
4550
|
+
var CREDENTIALS_PATH = join6(homedir(), ".config", "vos", "credentials");
|
|
4248
4551
|
function resolveCredential(explicit) {
|
|
4249
4552
|
const flag = explicit?.trim();
|
|
4250
4553
|
if (flag) return flag;
|
|
@@ -4267,7 +4570,7 @@ function requireCredential(explicit) {
|
|
|
4267
4570
|
return key;
|
|
4268
4571
|
}
|
|
4269
4572
|
function writeCredential(key) {
|
|
4270
|
-
mkdirSync(
|
|
4573
|
+
mkdirSync(join6(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
|
|
4271
4574
|
writeFileSync(CREDENTIALS_PATH, `${key.trim()}
|
|
4272
4575
|
`, { mode: 384 });
|
|
4273
4576
|
return CREDENTIALS_PATH;
|
|
@@ -4311,7 +4614,7 @@ function readJsonFile(file) {
|
|
|
4311
4614
|
}
|
|
4312
4615
|
}
|
|
4313
4616
|
function readSyncState(dir) {
|
|
4314
|
-
const own = readJsonFile(
|
|
4617
|
+
const own = readJsonFile(join6(dir, SYNC_STATE_NAME));
|
|
4315
4618
|
if (own && typeof own.vosId === "string") {
|
|
4316
4619
|
return {
|
|
4317
4620
|
vosId: own.vosId,
|
|
@@ -4322,7 +4625,7 @@ function readSyncState(dir) {
|
|
|
4322
4625
|
...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
|
|
4323
4626
|
};
|
|
4324
4627
|
}
|
|
4325
|
-
const push = readJsonFile(
|
|
4628
|
+
const push = readJsonFile(join6(dir, "push.json"));
|
|
4326
4629
|
if (push && typeof push.vosId === "string") {
|
|
4327
4630
|
return {
|
|
4328
4631
|
vosId: push.vosId,
|
|
@@ -4330,7 +4633,7 @@ function readSyncState(dir) {
|
|
|
4330
4633
|
...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
|
|
4331
4634
|
};
|
|
4332
4635
|
}
|
|
4333
|
-
const meta = readJsonFile(
|
|
4636
|
+
const meta = readJsonFile(join6(dir, "meta.json"));
|
|
4334
4637
|
if (meta && typeof meta.id === "string") {
|
|
4335
4638
|
return {
|
|
4336
4639
|
vosId: meta.id,
|
|
@@ -4348,7 +4651,7 @@ function writeSyncState(dir, patch) {
|
|
|
4348
4651
|
...patch
|
|
4349
4652
|
};
|
|
4350
4653
|
writeFileSync(
|
|
4351
|
-
|
|
4654
|
+
join6(dir, SYNC_STATE_NAME),
|
|
4352
4655
|
`${JSON.stringify(merged, null, 2)}
|
|
4353
4656
|
`
|
|
4354
4657
|
);
|
|
@@ -4387,9 +4690,9 @@ var roleFor = (cls) => `poster-${cls}`;
|
|
|
4387
4690
|
async function readPosterAt(cls, path, takeDir, from) {
|
|
4388
4691
|
let file = path;
|
|
4389
4692
|
let docDir = path;
|
|
4390
|
-
if (
|
|
4391
|
-
file =
|
|
4392
|
-
} else if (!/\.json$/i.test(path) || !
|
|
4693
|
+
if (existsSync6(join7(path, "doc.json"))) {
|
|
4694
|
+
file = join7(path, "doc.json");
|
|
4695
|
+
} else if (!/\.json$/i.test(path) || !existsSync6(path)) {
|
|
4393
4696
|
return null;
|
|
4394
4697
|
} else {
|
|
4395
4698
|
docDir = resolve3(path, "..");
|
|
@@ -4406,7 +4709,7 @@ async function readPosterAt(cls, path, takeDir, from) {
|
|
|
4406
4709
|
const doc = migrateHostedDoc(
|
|
4407
4710
|
raw
|
|
4408
4711
|
);
|
|
4409
|
-
const ownFootage =
|
|
4712
|
+
const ownFootage = existsSync6(join7(docDir, "meta.json")) && existsSync6(join7(docDir, source.videoKey));
|
|
4410
4713
|
const sync = readSyncState(docDir);
|
|
4411
4714
|
return {
|
|
4412
4715
|
cls,
|
|
@@ -4429,10 +4732,10 @@ async function findPosterDocs(takeDir, launchRoles) {
|
|
|
4429
4732
|
candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
|
|
4430
4733
|
}
|
|
4431
4734
|
candidates.push({
|
|
4432
|
-
path:
|
|
4735
|
+
path: join7(takeDir, "poster", cls),
|
|
4433
4736
|
from: `poster/${cls}`
|
|
4434
4737
|
});
|
|
4435
|
-
candidates.push({ path:
|
|
4738
|
+
candidates.push({ path: join7(takeDir, "poster"), from: "poster" });
|
|
4436
4739
|
for (const c of candidates) {
|
|
4437
4740
|
const ref = await readPosterAt(cls, c.path, takeDir, c.from);
|
|
4438
4741
|
if (ref) {
|
|
@@ -4528,18 +4831,18 @@ function resolveChannels(raw) {
|
|
|
4528
4831
|
return out;
|
|
4529
4832
|
}
|
|
4530
4833
|
async function readBrandBesideTake(dir, explicit) {
|
|
4531
|
-
const candidates = explicit ? [explicit] : [
|
|
4834
|
+
const candidates = explicit ? [explicit] : [join8(dir, "BRAND.md"), join8(dir, "..", "BRAND.md")];
|
|
4532
4835
|
for (const file of candidates) {
|
|
4533
|
-
if (!
|
|
4836
|
+
if (!existsSync7(file)) continue;
|
|
4534
4837
|
const roles = parseFrontmatter(await readFile6(file, "utf8"));
|
|
4535
4838
|
return { file, roles };
|
|
4536
4839
|
}
|
|
4537
4840
|
return null;
|
|
4538
4841
|
}
|
|
4539
4842
|
async function readLaunchBesideTake(dir, explicit) {
|
|
4540
|
-
const candidates = explicit ? [explicit] : [
|
|
4843
|
+
const candidates = explicit ? [explicit] : [join8(dir, "LAUNCH.md"), join8(dir, "..", "LAUNCH.md")];
|
|
4541
4844
|
for (const file of candidates) {
|
|
4542
|
-
if (!
|
|
4845
|
+
if (!existsSync7(file)) continue;
|
|
4543
4846
|
return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
|
|
4544
4847
|
}
|
|
4545
4848
|
return null;
|
|
@@ -4604,7 +4907,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4604
4907
|
const meta = doc.source.meta;
|
|
4605
4908
|
const vw = meta.captureWidth ?? meta.width;
|
|
4606
4909
|
const vh = meta.captureHeight ?? meta.height;
|
|
4607
|
-
const probeDir = await mkdtemp(
|
|
4910
|
+
const probeDir = await mkdtemp(join8(tmpdir(), "vos-moments-"));
|
|
4608
4911
|
try {
|
|
4609
4912
|
const probe = await framesTake(browser, dir, {
|
|
4610
4913
|
times: candidates.map((c) => c.time),
|
|
@@ -4643,7 +4946,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4643
4946
|
notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
|
|
4644
4947
|
};
|
|
4645
4948
|
} finally {
|
|
4646
|
-
await
|
|
4949
|
+
await rm4(probeDir, { recursive: true, force: true });
|
|
4647
4950
|
}
|
|
4648
4951
|
}
|
|
4649
4952
|
var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
|
|
@@ -4691,8 +4994,8 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4691
4994
|
const doc = take.doc;
|
|
4692
4995
|
const duration = docOutputDuration2(doc);
|
|
4693
4996
|
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
|
|
4997
|
+
const outDir = resolve4(opts.outDir ?? join8(dir, "kit"));
|
|
4998
|
+
await mkdir5(outDir, { recursive: true });
|
|
4696
4999
|
const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
|
|
4697
5000
|
const assets = [];
|
|
4698
5001
|
const skipped = [];
|
|
@@ -4752,7 +5055,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4752
5055
|
opts.onPhase?.(
|
|
4753
5056
|
`${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
|
|
4754
5057
|
);
|
|
4755
|
-
const shotDir = await mkdtemp(
|
|
5058
|
+
const shotDir = await mkdtemp(join8(tmpdir(), "vos-poster-"));
|
|
4756
5059
|
try {
|
|
4757
5060
|
const captured = await framesTake(browser, ref.takeDir, {
|
|
4758
5061
|
times: [time],
|
|
@@ -4762,9 +5065,9 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4762
5065
|
doc: ref.doc,
|
|
4763
5066
|
overrides: opts.overrides
|
|
4764
5067
|
});
|
|
4765
|
-
const to =
|
|
4766
|
-
await
|
|
4767
|
-
const bytes = (await
|
|
5068
|
+
const to = join8(outDir, `${d.id}.png`);
|
|
5069
|
+
await rename5(captured.frames[0].file, to);
|
|
5070
|
+
const bytes = (await stat2(to)).size;
|
|
4768
5071
|
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4769
5072
|
skipped.push(
|
|
4770
5073
|
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
@@ -4793,7 +5096,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4793
5096
|
...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
|
|
4794
5097
|
});
|
|
4795
5098
|
} finally {
|
|
4796
|
-
await
|
|
5099
|
+
await rm4(shotDir, { recursive: true, force: true });
|
|
4797
5100
|
}
|
|
4798
5101
|
}
|
|
4799
5102
|
if (missing.size) {
|
|
@@ -4834,7 +5137,7 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4834
5137
|
}
|
|
4835
5138
|
}
|
|
4836
5139
|
opts.onPhase?.(`${label} (${specWords(d)})`);
|
|
4837
|
-
const outFile =
|
|
5140
|
+
const outFile = join8(outDir, `${d.id}.${d.format}`);
|
|
4838
5141
|
const bitrate = d.maxBytes !== void 0 ? Math.min(
|
|
4839
5142
|
1e7,
|
|
4840
5143
|
Math.floor(d.maxBytes * 8 / seconds * 0.85)
|
|
@@ -4881,9 +5184,9 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4881
5184
|
for (let i = 0; i < captured.frames.length; i++) {
|
|
4882
5185
|
const frame = captured.frames[i];
|
|
4883
5186
|
const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
|
|
4884
|
-
const to =
|
|
4885
|
-
await
|
|
4886
|
-
const bytes = (await
|
|
5187
|
+
const to = join8(outDir, name);
|
|
5188
|
+
await rename5(frame.file, to);
|
|
5189
|
+
const bytes = (await stat2(to)).size;
|
|
4887
5190
|
if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
|
|
4888
5191
|
skipped.push(
|
|
4889
5192
|
`${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
|
|
@@ -4919,15 +5222,15 @@ async function deliverTake(browser, dir, opts) {
|
|
|
4919
5222
|
skipped,
|
|
4920
5223
|
assets
|
|
4921
5224
|
};
|
|
4922
|
-
const kitFile =
|
|
4923
|
-
await
|
|
5225
|
+
const kitFile = join8(outDir, "kit.json");
|
|
5226
|
+
await writeFile7(kitFile, JSON.stringify(kit, null, 2));
|
|
4924
5227
|
return { kit, kitFile, outDir };
|
|
4925
5228
|
}
|
|
4926
5229
|
|
|
4927
5230
|
// src/plugin/markAsset.ts
|
|
4928
|
-
import { mkdir as
|
|
4929
|
-
import { existsSync as
|
|
4930
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5231
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
|
|
5232
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5233
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
|
|
4931
5234
|
function markExtension(url) {
|
|
4932
5235
|
const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
|
|
4933
5236
|
if (!m) return null;
|
|
@@ -5009,7 +5312,7 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
5009
5312
|
bytes = new Uint8Array(await res.arrayBuffer());
|
|
5010
5313
|
} else {
|
|
5011
5314
|
const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
|
|
5012
|
-
if (!
|
|
5315
|
+
if (!existsSync8(file)) throw new Error("no such file");
|
|
5013
5316
|
bytes = new Uint8Array(await readFile7(file));
|
|
5014
5317
|
}
|
|
5015
5318
|
} catch (e) {
|
|
@@ -5025,8 +5328,8 @@ async function fetchBrandMarks(takeDir, roles) {
|
|
|
5025
5328
|
);
|
|
5026
5329
|
return null;
|
|
5027
5330
|
}
|
|
5028
|
-
await
|
|
5029
|
-
await
|
|
5331
|
+
await mkdir6(join9(takeDir, "brand"), { recursive: true });
|
|
5332
|
+
await writeFile8(join9(takeDir, "brand", `${name}.${ext}`), bytes);
|
|
5030
5333
|
return { key: `/brand/${name}.${ext}`, aspect };
|
|
5031
5334
|
};
|
|
5032
5335
|
out.light = await one("logoUrl", "mark");
|
|
@@ -5062,9 +5365,9 @@ async function fetchMusicCatalog(origin) {
|
|
|
5062
5365
|
}
|
|
5063
5366
|
|
|
5064
5367
|
// src/plugin/judge.ts
|
|
5065
|
-
import { existsSync as
|
|
5066
|
-
import { mkdir as
|
|
5067
|
-
import { dirname, isAbsolute as isAbsolute3, join as
|
|
5368
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5369
|
+
import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
|
|
5370
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join10, resolve as resolve6 } from "path";
|
|
5068
5371
|
import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
|
|
5069
5372
|
|
|
5070
5373
|
// src/plugin/shotBake.ts
|
|
@@ -5147,7 +5450,8 @@ function rolesFor(asset) {
|
|
|
5147
5450
|
const portrait = spec.px.w < spec.px.h;
|
|
5148
5451
|
return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
|
|
5149
5452
|
}
|
|
5150
|
-
if (spec?.genre === "card")
|
|
5453
|
+
if (spec?.genre === "card")
|
|
5454
|
+
return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
|
|
5151
5455
|
if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
|
|
5152
5456
|
return [];
|
|
5153
5457
|
}
|
|
@@ -5207,7 +5511,8 @@ function composeSheet(left, right, height = 540, gutter = 32) {
|
|
|
5207
5511
|
const si = (y * src.w + x) * 4;
|
|
5208
5512
|
const a = src.data[si + 3] / 255;
|
|
5209
5513
|
const o = ((y0 + y) * w + x0 + x) * 4;
|
|
5210
|
-
for (let c = 0; c < 3; c++)
|
|
5514
|
+
for (let c = 0; c < 3; c++)
|
|
5515
|
+
out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
|
|
5211
5516
|
}
|
|
5212
5517
|
}
|
|
5213
5518
|
};
|
|
@@ -5223,51 +5528,70 @@ function composeSheet(left, right, height = 540, gutter = 32) {
|
|
|
5223
5528
|
}
|
|
5224
5529
|
};
|
|
5225
5530
|
fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
|
|
5226
|
-
fill(
|
|
5531
|
+
fill(
|
|
5532
|
+
margin + lw + gutter,
|
|
5533
|
+
margin + lw + gutter + rw,
|
|
5534
|
+
margin + height + 6,
|
|
5535
|
+
margin + height + 6 + band,
|
|
5536
|
+
200
|
|
5537
|
+
);
|
|
5227
5538
|
return { w, h, data: out };
|
|
5228
5539
|
}
|
|
5229
5540
|
async function judgeKit(kitPath, manifestPath, outDir) {
|
|
5230
5541
|
const kit = JSON.parse(await readFile8(kitPath, "utf8"));
|
|
5231
|
-
const manifest = JSON.parse(
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
const
|
|
5235
|
-
|
|
5542
|
+
const manifest = JSON.parse(
|
|
5543
|
+
await readFile8(manifestPath, "utf8")
|
|
5544
|
+
);
|
|
5545
|
+
const manifestDir = dirname2(resolve6(manifestPath));
|
|
5546
|
+
const kitDir = dirname2(resolve6(kitPath));
|
|
5547
|
+
const out = resolve6(outDir ?? join10(kitDir, "judge"));
|
|
5548
|
+
await mkdir7(out, { recursive: true });
|
|
5236
5549
|
const sheets = [];
|
|
5237
5550
|
const skipped = [];
|
|
5238
5551
|
const refCache = /* @__PURE__ */ new Map();
|
|
5239
5552
|
const loadRef = async (file) => {
|
|
5240
5553
|
if (refCache.has(file)) return refCache.get(file) ?? null;
|
|
5241
|
-
const p =
|
|
5242
|
-
const img =
|
|
5554
|
+
const p = join10(manifestDir, file);
|
|
5555
|
+
const img = existsSync9(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
|
|
5243
5556
|
refCache.set(file, img);
|
|
5244
5557
|
return img;
|
|
5245
5558
|
};
|
|
5246
5559
|
for (const a of kit.assets) {
|
|
5247
5560
|
if (!/\.png$/i.test(a.path)) continue;
|
|
5248
|
-
const file = isAbsolute3(a.path) &&
|
|
5249
|
-
const img =
|
|
5561
|
+
const file = isAbsolute3(a.path) && existsSync9(a.path) ? a.path : join10(kitDir, a.path.split("/").pop() ?? a.path);
|
|
5562
|
+
const img = existsSync9(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
|
|
5250
5563
|
if (!img) {
|
|
5251
5564
|
skipped.push(`${a.destination ?? a.path}: unreadable`);
|
|
5252
5565
|
continue;
|
|
5253
5566
|
}
|
|
5254
|
-
const roles = rolesFor({
|
|
5255
|
-
|
|
5567
|
+
const roles = rolesFor({
|
|
5568
|
+
destination: a.destination,
|
|
5569
|
+
source: a.source,
|
|
5570
|
+
template: a.template,
|
|
5571
|
+
path: a.path
|
|
5572
|
+
});
|
|
5573
|
+
const ref = manifest.assets.find(
|
|
5574
|
+
(r) => roles.includes(r.role) && r.genre !== "context"
|
|
5575
|
+
);
|
|
5256
5576
|
if (!ref) {
|
|
5257
|
-
skipped.push(
|
|
5577
|
+
skipped.push(
|
|
5578
|
+
`${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`
|
|
5579
|
+
);
|
|
5258
5580
|
continue;
|
|
5259
5581
|
}
|
|
5260
5582
|
const refImg = await loadRef(ref.file);
|
|
5261
5583
|
if (!refImg) {
|
|
5262
|
-
skipped.push(
|
|
5584
|
+
skipped.push(
|
|
5585
|
+
`${a.destination ?? a.path}: reference ${ref.file} unreadable`
|
|
5586
|
+
);
|
|
5263
5587
|
continue;
|
|
5264
5588
|
}
|
|
5265
5589
|
const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
|
|
5266
5590
|
const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
|
|
5267
5591
|
const nameA = `${id}--A.png`;
|
|
5268
5592
|
const nameB = `${id}--B.png`;
|
|
5269
|
-
await
|
|
5270
|
-
await
|
|
5593
|
+
await writeFile9(join10(out, nameA), encodePng(composeSheet(img, refImg)));
|
|
5594
|
+
await writeFile9(join10(out, nameB), encodePng(composeSheet(refImg, img)));
|
|
5271
5595
|
const rubric = [
|
|
5272
5596
|
`# ${id} against ${ref.id} (${ref.file})`,
|
|
5273
5597
|
"",
|
|
@@ -5287,18 +5611,31 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5287
5611
|
"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
5612
|
""
|
|
5289
5613
|
].join("\n");
|
|
5290
|
-
await
|
|
5291
|
-
sheets.push({
|
|
5614
|
+
await writeFile9(join10(out, `${id}.md`), rubric);
|
|
5615
|
+
sheets.push({
|
|
5616
|
+
asset: id,
|
|
5617
|
+
reference: ref.id,
|
|
5618
|
+
sheetA: nameA,
|
|
5619
|
+
sheetB: nameB,
|
|
5620
|
+
rubric: `${id}.md`
|
|
5621
|
+
});
|
|
5292
5622
|
}
|
|
5293
|
-
const verdictFile =
|
|
5294
|
-
if (!
|
|
5295
|
-
await
|
|
5623
|
+
const verdictFile = join10(out, "judge.json");
|
|
5624
|
+
if (!existsSync9(verdictFile)) {
|
|
5625
|
+
await writeFile9(
|
|
5296
5626
|
verdictFile,
|
|
5297
5627
|
JSON.stringify(
|
|
5298
5628
|
{
|
|
5299
5629
|
kit: kitPath,
|
|
5300
5630
|
manifest: manifestPath,
|
|
5301
|
-
verdicts: sheets.map((s) => ({
|
|
5631
|
+
verdicts: sheets.map((s) => ({
|
|
5632
|
+
asset: s.asset,
|
|
5633
|
+
reference: s.reference,
|
|
5634
|
+
A: null,
|
|
5635
|
+
B: null,
|
|
5636
|
+
win: null,
|
|
5637
|
+
reasons: []
|
|
5638
|
+
}))
|
|
5302
5639
|
},
|
|
5303
5640
|
null,
|
|
5304
5641
|
2
|
|
@@ -5308,17 +5645,24 @@ async function judgeKit(kitPath, manifestPath, outDir) {
|
|
|
5308
5645
|
return { sheets, skipped, outDir: out, verdictFile };
|
|
5309
5646
|
}
|
|
5310
5647
|
function winRate(verdicts) {
|
|
5311
|
-
const judgedList = verdicts.filter(
|
|
5648
|
+
const judgedList = verdicts.filter(
|
|
5649
|
+
(v) => v.win !== null || (v.reasons?.length ?? 0) > 0
|
|
5650
|
+
);
|
|
5312
5651
|
const wins = judgedList.filter((v) => v.win === true).length;
|
|
5313
5652
|
const ties = judgedList.filter((v) => v.win === null).length;
|
|
5314
5653
|
const judged = judgedList.length;
|
|
5315
|
-
return {
|
|
5654
|
+
return {
|
|
5655
|
+
wins,
|
|
5656
|
+
ties,
|
|
5657
|
+
judged,
|
|
5658
|
+
rate: judged ? (wins + ties / 2) / judged : null
|
|
5659
|
+
};
|
|
5316
5660
|
}
|
|
5317
5661
|
|
|
5318
5662
|
// src/plugin/callout.ts
|
|
5319
|
-
import { existsSync as
|
|
5663
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5320
5664
|
import { readFile as readFile9 } from "fs/promises";
|
|
5321
|
-
import { join as
|
|
5665
|
+
import { join as join11 } from "path";
|
|
5322
5666
|
import { lerpArray, sample } from "@vosjs/timeline";
|
|
5323
5667
|
import {
|
|
5324
5668
|
CALLOUT_DEFAULT_BODY_PX,
|
|
@@ -5501,10 +5845,10 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
|
|
|
5501
5845
|
const out = /* @__PURE__ */ new Map();
|
|
5502
5846
|
if (!times.length) return out;
|
|
5503
5847
|
const bare = { ...doc, overlays: [] };
|
|
5504
|
-
const outDir =
|
|
5848
|
+
const outDir = join11(dir, ".ground-check");
|
|
5505
5849
|
const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
|
|
5506
5850
|
for (const f of res.frames) {
|
|
5507
|
-
if (!
|
|
5851
|
+
if (!existsSync10(f.file)) continue;
|
|
5508
5852
|
out.set(
|
|
5509
5853
|
+f.time.toFixed(3),
|
|
5510
5854
|
decodePng(new Uint8Array(await readFile9(f.file)))
|
|
@@ -5885,9 +6229,9 @@ function formatFinding(f) {
|
|
|
5885
6229
|
}
|
|
5886
6230
|
|
|
5887
6231
|
// src/plugin/validateKit.ts
|
|
5888
|
-
import { existsSync as
|
|
5889
|
-
import { readFile as readFile11, stat as
|
|
5890
|
-
import { dirname as
|
|
6232
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6233
|
+
import { readFile as readFile11, stat as stat3 } from "fs/promises";
|
|
6234
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join12 } from "path";
|
|
5891
6235
|
import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
|
|
5892
6236
|
var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5893
6237
|
function pngDimensions(bytes) {
|
|
@@ -5944,10 +6288,10 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5944
6288
|
measured
|
|
5945
6289
|
};
|
|
5946
6290
|
}
|
|
5947
|
-
const base =
|
|
6291
|
+
const base = dirname3(kitPath);
|
|
5948
6292
|
const resolvePath = (p) => {
|
|
5949
|
-
if (isAbsolute4(p) &&
|
|
5950
|
-
const beside =
|
|
6293
|
+
if (isAbsolute4(p) && existsSync11(p)) return p;
|
|
6294
|
+
const beside = join12(base, p.split("/").pop() ?? p);
|
|
5951
6295
|
return beside;
|
|
5952
6296
|
};
|
|
5953
6297
|
const perDestination = /* @__PURE__ */ new Map();
|
|
@@ -5958,7 +6302,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
5958
6302
|
const file = resolvePath(a.path);
|
|
5959
6303
|
let bytes;
|
|
5960
6304
|
try {
|
|
5961
|
-
bytes = (await
|
|
6305
|
+
bytes = (await stat3(file)).size;
|
|
5962
6306
|
} catch {
|
|
5963
6307
|
problems.push(`${label}: ${a.path} is missing`);
|
|
5964
6308
|
continue;
|
|
@@ -6083,7 +6427,7 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6083
6427
|
|
|
6084
6428
|
// src/plugin/recorder.ts
|
|
6085
6429
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
6086
|
-
import { join as
|
|
6430
|
+
import { join as join13 } from "path";
|
|
6087
6431
|
|
|
6088
6432
|
// src/plugin/recordingCap.ts
|
|
6089
6433
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
@@ -6261,7 +6605,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6261
6605
|
cdp.on("Page.screencastFrame", (ev) => {
|
|
6262
6606
|
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
6263
6607
|
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
6264
|
-
writeFileSync2(
|
|
6608
|
+
writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
6265
6609
|
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
6266
6610
|
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
6267
6611
|
});
|
|
@@ -6523,7 +6867,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6523
6867
|
await sleep(200);
|
|
6524
6868
|
const pageTitle = await page.title().catch(() => "");
|
|
6525
6869
|
await context.close();
|
|
6526
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(
|
|
6870
|
+
const firstFrame = frames[0] ? jpegDims(readFileSync3(join13(paths.framesDir, frames[0].file))) : null;
|
|
6527
6871
|
const meta = {
|
|
6528
6872
|
dpr: 1,
|
|
6529
6873
|
zoom: 1,
|
|
@@ -6634,9 +6978,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
|
|
|
6634
6978
|
}
|
|
6635
6979
|
|
|
6636
6980
|
// src/plugin/plan.ts
|
|
6637
|
-
import { existsSync as
|
|
6981
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6638
6982
|
import { readFile as readFile12 } from "fs/promises";
|
|
6639
|
-
import { join as
|
|
6983
|
+
import { join as join14 } from "path";
|
|
6640
6984
|
import {
|
|
6641
6985
|
DEFAULT_FRAME_STYLE,
|
|
6642
6986
|
STYLE_FIELDS,
|
|
@@ -6858,8 +7202,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
|
|
|
6858
7202
|
// src/plugin/plan.ts
|
|
6859
7203
|
var overlaps = (a, b) => a.in < b.out && b.in < a.out;
|
|
6860
7204
|
async function readDigestActivity(dir) {
|
|
6861
|
-
const file =
|
|
6862
|
-
if (!
|
|
7205
|
+
const file = join14(dir, "digest", "digest.json");
|
|
7206
|
+
if (!existsSync12(file)) return null;
|
|
6863
7207
|
try {
|
|
6864
7208
|
const d = JSON.parse(await readFile12(file, "utf8"));
|
|
6865
7209
|
return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
|
|
@@ -6928,11 +7272,11 @@ async function planTake(dir, opts = {}) {
|
|
|
6928
7272
|
if (opts.reuse) {
|
|
6929
7273
|
const prev = migrateMotion2(opts.reuse.doc);
|
|
6930
7274
|
const artifact = {
|
|
6931
|
-
videoKey:
|
|
7275
|
+
videoKey: take.paths.recordingName,
|
|
6932
7276
|
cursor,
|
|
6933
7277
|
meta
|
|
6934
7278
|
};
|
|
6935
|
-
doc = projectFromArtifact(artifact,
|
|
7279
|
+
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
6936
7280
|
doc = copyStyle(prev, doc);
|
|
6937
7281
|
const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
|
|
6938
7282
|
doc.segments = rt.segments;
|
|
@@ -7020,11 +7364,11 @@ async function planTake(dir, opts = {}) {
|
|
|
7020
7364
|
}
|
|
7021
7365
|
} else {
|
|
7022
7366
|
const artifact = {
|
|
7023
|
-
videoKey:
|
|
7367
|
+
videoKey: take.paths.recordingName,
|
|
7024
7368
|
cursor,
|
|
7025
7369
|
meta
|
|
7026
7370
|
};
|
|
7027
|
-
doc = projectFromArtifact(artifact,
|
|
7371
|
+
doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
|
|
7028
7372
|
if (opts.style) {
|
|
7029
7373
|
const applied = applyStyle(opts.style.doc, doc, opts);
|
|
7030
7374
|
doc = applied.doc;
|
|
@@ -7074,207 +7418,11 @@ import { createHash } from "crypto";
|
|
|
7074
7418
|
import { existsSync as existsSync13 } from "fs";
|
|
7075
7419
|
import { readFile as readFile13 } from "fs/promises";
|
|
7076
7420
|
import { createInterface } from "readline/promises";
|
|
7077
|
-
import { basename, join as join16 } from "path";
|
|
7421
|
+
import { basename as basename3, join as join16 } from "path";
|
|
7078
7422
|
import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
|
|
7079
7423
|
|
|
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
7424
|
// src/plugin/folder.ts
|
|
7277
|
-
import { mkdir as
|
|
7425
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
|
|
7278
7426
|
import { join as join15 } from "path";
|
|
7279
7427
|
import { recipeHints } from "@vosjs/shared/frontmatter";
|
|
7280
7428
|
import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
|
|
@@ -7451,7 +7599,7 @@ async function cmdPull(argv) {
|
|
|
7451
7599
|
throw new Error(apiError(`pull folder ${folder.slug}`, res));
|
|
7452
7600
|
const payload = res.body;
|
|
7453
7601
|
const out = strFlag(flags, "out") ?? folder.slug;
|
|
7454
|
-
await
|
|
7602
|
+
await mkdir8(join15(out, "recipes"), { recursive: true });
|
|
7455
7603
|
const lines = [];
|
|
7456
7604
|
const recipes = [];
|
|
7457
7605
|
for (const rec of payload.recipes) {
|
|
@@ -7461,7 +7609,7 @@ async function cmdPull(argv) {
|
|
|
7461
7609
|
}
|
|
7462
7610
|
for (const rec of payload.inheritedRecipes) {
|
|
7463
7611
|
const from = safeName(rec.folderSlug ?? "inherited");
|
|
7464
|
-
await
|
|
7612
|
+
await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
|
|
7465
7613
|
const path = join15(
|
|
7466
7614
|
out,
|
|
7467
7615
|
"recipes",
|
|
@@ -7478,7 +7626,7 @@ async function cmdPull(argv) {
|
|
|
7478
7626
|
const members = [];
|
|
7479
7627
|
for (const v of payload.voses) {
|
|
7480
7628
|
const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
|
|
7481
|
-
await
|
|
7629
|
+
await mkdir8(dir, { recursive: true });
|
|
7482
7630
|
const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
|
|
7483
7631
|
if (cfg.status !== 200) {
|
|
7484
7632
|
lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
|
|
@@ -7753,7 +7901,9 @@ async function pushTake(dir, flags, r) {
|
|
|
7753
7901
|
${lint.problems.join("\n ")}`);
|
|
7754
7902
|
}
|
|
7755
7903
|
if (!existsSync13(take.paths.recording)) {
|
|
7756
|
-
throw new Error(
|
|
7904
|
+
throw new Error(
|
|
7905
|
+
"no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
|
|
7906
|
+
);
|
|
7757
7907
|
}
|
|
7758
7908
|
const ctx = apiContext(flags);
|
|
7759
7909
|
const state = readSyncState(dir);
|
|
@@ -7786,12 +7936,22 @@ async function pushTake(dir, flags, r) {
|
|
|
7786
7936
|
}
|
|
7787
7937
|
const bytes = await readFile13(take.paths.recording);
|
|
7788
7938
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
7939
|
+
const { type: contentType } = resolveMediaType({
|
|
7940
|
+
head: bytes.subarray(0, MEDIA_HEAD_BYTES),
|
|
7941
|
+
filename: take.paths.recordingName
|
|
7942
|
+
});
|
|
7943
|
+
const filename = nameForType(take.paths.recordingName, contentType);
|
|
7944
|
+
if (filename !== take.paths.recordingName) {
|
|
7945
|
+
r.log(
|
|
7946
|
+
` ${take.paths.recordingName} holds ${contentType} \u2014 uploading it as ${filename}`
|
|
7947
|
+
);
|
|
7948
|
+
}
|
|
7789
7949
|
r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
|
|
7790
7950
|
let upload;
|
|
7791
7951
|
try {
|
|
7792
7952
|
upload = await uploadAsset(ctx, new Uint8Array(bytes), {
|
|
7793
|
-
filename
|
|
7794
|
-
contentType
|
|
7953
|
+
filename,
|
|
7954
|
+
contentType,
|
|
7795
7955
|
contentHash: hash,
|
|
7796
7956
|
// The take's length so the server holds it to the plan's cap.
|
|
7797
7957
|
...take.meta.durationMs > 0 ? { durationSeconds: take.meta.durationMs / 1e3 } : {},
|
|
@@ -7824,11 +7984,18 @@ async function pushTake(dir, flags, r) {
|
|
|
7824
7984
|
}
|
|
7825
7985
|
const media = await readFile13(file);
|
|
7826
7986
|
const mediaHash = createHash("sha256").update(media).digest("hex");
|
|
7827
|
-
const type =
|
|
7987
|
+
const { type } = resolveMediaType({
|
|
7988
|
+
head: media.subarray(0, MEDIA_HEAD_BYTES),
|
|
7989
|
+
filename: file
|
|
7990
|
+
});
|
|
7991
|
+
const name = nameForType(basename3(file), type);
|
|
7992
|
+
if (name !== basename3(file)) {
|
|
7993
|
+
r.log(` ${ref.where}: ${basename3(file)} holds ${type} \u2014 sent as ${name}`);
|
|
7994
|
+
}
|
|
7828
7995
|
let put;
|
|
7829
7996
|
try {
|
|
7830
7997
|
put = await uploadAsset(ctx, new Uint8Array(media), {
|
|
7831
|
-
filename:
|
|
7998
|
+
filename: name,
|
|
7832
7999
|
contentType: type,
|
|
7833
8000
|
contentHash: mediaHash
|
|
7834
8001
|
});
|
|
@@ -7845,7 +8012,7 @@ async function pushTake(dir, flags, r) {
|
|
|
7845
8012
|
const lowered = lowerToComposition3(docForPush);
|
|
7846
8013
|
const config = { ...lowered.config, data: lowered.data };
|
|
7847
8014
|
if (!state) {
|
|
7848
|
-
const title = flags.title ?? `Take ${
|
|
8015
|
+
const title = flags.title ?? `Take ${basename3(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
7849
8016
|
const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "take"}-${Date.now().toString(36)}`;
|
|
7850
8017
|
const created = await api(ctx, "/vos", {
|
|
7851
8018
|
method: "POST",
|
|
@@ -8017,8 +8184,9 @@ async function pullTake(dir, flags, r) {
|
|
|
8017
8184
|
}
|
|
8018
8185
|
const hostedDoc = migrateHostedDoc3(docRes.json);
|
|
8019
8186
|
const doc = hostedDoc;
|
|
8020
|
-
|
|
8021
|
-
|
|
8187
|
+
const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
8188
|
+
if (existsSync13(localRecording)) {
|
|
8189
|
+
doc.source.videoKey = basename3(localRecording);
|
|
8022
8190
|
}
|
|
8023
8191
|
await writeJson(join16(dir, "doc.json"), doc, true);
|
|
8024
8192
|
const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
|
|
@@ -8040,9 +8208,9 @@ async function pullTake(dir, flags, r) {
|
|
|
8040
8208
|
|
|
8041
8209
|
// src/plugin/program.ts
|
|
8042
8210
|
import { existsSync as existsSync14 } from "fs";
|
|
8043
|
-
import { mkdir as
|
|
8211
|
+
import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
8044
8212
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8045
|
-
import { basename as
|
|
8213
|
+
import { basename as basename4, dirname as dirname4, join as join17 } from "path";
|
|
8046
8214
|
import {
|
|
8047
8215
|
CURRENT_CONFIG_VERSION,
|
|
8048
8216
|
migrateConfig,
|
|
@@ -8239,7 +8407,7 @@ async function cmdFetch(argv) {
|
|
|
8239
8407
|
throw new Error(apiError(`fetch config for ${vosId}`, cfg));
|
|
8240
8408
|
const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
|
|
8241
8409
|
const out = strFlag(flags, "out") ?? slug;
|
|
8242
|
-
await
|
|
8410
|
+
await mkdir9(out, { recursive: true });
|
|
8243
8411
|
await writeFile11(
|
|
8244
8412
|
join17(out, "config.json"),
|
|
8245
8413
|
JSON.stringify(cfg.body.config, null, 2)
|
|
@@ -8383,7 +8551,7 @@ async function cmdPushProgram(argv) {
|
|
|
8383
8551
|
origin: strFlag(flags, "origin"),
|
|
8384
8552
|
api: strFlag(flags, "api")
|
|
8385
8553
|
});
|
|
8386
|
-
const dir =
|
|
8554
|
+
const dir = dirname4(source);
|
|
8387
8555
|
const parsed = JSON.parse(await readFile14(source, "utf8"));
|
|
8388
8556
|
const pre = preflightConfig(parsed);
|
|
8389
8557
|
if (!pre.ok || !pre.config) {
|
|
@@ -8411,7 +8579,7 @@ async function cmdPushProgram(argv) {
|
|
|
8411
8579
|
"--claimable creates a NEW claimable vos and cannot iterate (--vos). After the human claims it, iterate with their key: vos push --vos <id>"
|
|
8412
8580
|
);
|
|
8413
8581
|
}
|
|
8414
|
-
const fallback = state?.title ? `${state.title} remix` :
|
|
8582
|
+
const fallback = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos program";
|
|
8415
8583
|
const title2 = (strFlag(flags, "title") ?? fallback).slice(0, 100);
|
|
8416
8584
|
const body = { title: title2, config };
|
|
8417
8585
|
const slug = strFlag(flags, "slug");
|
|
@@ -8509,7 +8677,7 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
|
|
|
8509
8677
|
return EXIT_OK;
|
|
8510
8678
|
}
|
|
8511
8679
|
const remixOfId = strFlag(flags, "remix-of") ?? state?.vosId;
|
|
8512
|
-
const fallbackTitle = state?.title ? `${state.title} remix` :
|
|
8680
|
+
const fallbackTitle = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos remix";
|
|
8513
8681
|
const title = (strFlag(flags, "title") ?? fallbackTitle).slice(0, 100);
|
|
8514
8682
|
const folderId = folderRef ? resolveFolder(await listFolders(origin, key), folderRef).id : void 0;
|
|
8515
8683
|
const created = await createProgramVos({
|
|
@@ -8561,7 +8729,7 @@ Iterate with: vos push ${source} --vos ${created.id}`
|
|
|
8561
8729
|
async function cmdPullProgram(argv) {
|
|
8562
8730
|
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
|
|
8563
8731
|
const target = positionals[0] ?? ".";
|
|
8564
|
-
const dir = target.endsWith(".json") ?
|
|
8732
|
+
const dir = target.endsWith(".json") ? dirname4(target) : target;
|
|
8565
8733
|
const r = createReporter(flags.json === true);
|
|
8566
8734
|
const origin = platformOrigin({
|
|
8567
8735
|
origin: strFlag(flags, "origin"),
|
|
@@ -8744,7 +8912,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
8744
8912
|
|
|
8745
8913
|
// src/plugin/asset.ts
|
|
8746
8914
|
import { readFileSync as readFileSync4 } from "fs";
|
|
8747
|
-
import { basename as
|
|
8915
|
+
import { basename as basename5, extname as extname2 } from "path";
|
|
8748
8916
|
var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
8749
8917
|
var MIME_BY_EXT = {
|
|
8750
8918
|
".png": "image/png",
|
|
@@ -8795,7 +8963,7 @@ async function cmdPush(argv) {
|
|
|
8795
8963
|
const folder = folderRef ? resolveFolder(await listFolders(origin, key), folderRef) : null;
|
|
8796
8964
|
const uploaded = [];
|
|
8797
8965
|
for (const file of positionals) {
|
|
8798
|
-
const name =
|
|
8966
|
+
const name = basename5(file);
|
|
8799
8967
|
const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
|
|
8800
8968
|
const body = readFileSync4(file);
|
|
8801
8969
|
const form = new FormData();
|
|
@@ -8852,11 +9020,11 @@ async function cmdAsset(argv) {
|
|
|
8852
9020
|
|
|
8853
9021
|
// src/plugin/recipe.ts
|
|
8854
9022
|
import { readFileSync as readFileSync5 } from "fs";
|
|
8855
|
-
import { basename as
|
|
9023
|
+
import { basename as basename6 } from "path";
|
|
8856
9024
|
var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
8857
9025
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
8858
9026
|
function recipeFilename(file) {
|
|
8859
|
-
const name =
|
|
9027
|
+
const name = basename6(file);
|
|
8860
9028
|
if (!/\.md$/i.test(name)) {
|
|
8861
9029
|
throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
|
|
8862
9030
|
}
|
|
@@ -10162,7 +10330,7 @@ async function cmdRecord(argv) {
|
|
|
10162
10330
|
`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
10331
|
);
|
|
10164
10332
|
}
|
|
10165
|
-
await
|
|
10333
|
+
await mkdir10(outDir, { recursive: true });
|
|
10166
10334
|
const paths = await ensureTakeDir(outDir);
|
|
10167
10335
|
const browser = await launchBrowser();
|
|
10168
10336
|
try {
|
|
@@ -10246,7 +10414,7 @@ async function cmdCreate2(argv) {
|
|
|
10246
10414
|
`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
10415
|
);
|
|
10248
10416
|
}
|
|
10249
|
-
await
|
|
10417
|
+
await mkdir10(outDir, { recursive: true });
|
|
10250
10418
|
const paths = await ensureTakeDir(outDir);
|
|
10251
10419
|
const browser = await launchBrowser();
|
|
10252
10420
|
try {
|
|
@@ -10331,7 +10499,7 @@ async function cmdPlan(argv) {
|
|
|
10331
10499
|
);
|
|
10332
10500
|
const r = createReporter(flags.json === true);
|
|
10333
10501
|
if (flags.fresh === true) {
|
|
10334
|
-
await
|
|
10502
|
+
await rm5(join18(dir, "doc.json"), { force: true });
|
|
10335
10503
|
r.log("note: --fresh discarded the existing doc.json");
|
|
10336
10504
|
}
|
|
10337
10505
|
let reuse;
|
|
@@ -10895,7 +11063,7 @@ async function cmdOpen(argv) {
|
|
|
10895
11063
|
const r = createReporter(flags.json === true);
|
|
10896
11064
|
const take = await loadTake(dir);
|
|
10897
11065
|
if (!existsSync15(take.paths.recording)) {
|
|
10898
|
-
throw new UsageError(`${dir} has no recording
|
|
11066
|
+
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
10899
11067
|
}
|
|
10900
11068
|
const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
|
|
10901
11069
|
/\/+$/,
|
|
@@ -11132,8 +11300,15 @@ async function cmdValidate(argv) {
|
|
|
11132
11300
|
const take = await loadTake(target);
|
|
11133
11301
|
const problems = [];
|
|
11134
11302
|
const warnings = [];
|
|
11135
|
-
if (!existsSync15(take.paths.recording))
|
|
11136
|
-
problems.push("missing recording
|
|
11303
|
+
if (!existsSync15(take.paths.recording)) {
|
|
11304
|
+
problems.push("missing recording (re-run record)");
|
|
11305
|
+
} else {
|
|
11306
|
+
const mismatch = containerMismatch(
|
|
11307
|
+
take.paths.recordingName,
|
|
11308
|
+
await readMediaHead(take.paths.recording)
|
|
11309
|
+
);
|
|
11310
|
+
if (mismatch) warnings.push(mismatch);
|
|
11311
|
+
}
|
|
11137
11312
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
11138
11313
|
if (take.doc) {
|
|
11139
11314
|
const lint = lintDoc(take.doc);
|
|
@@ -11353,6 +11528,7 @@ export {
|
|
|
11353
11528
|
RECORDING_NAME,
|
|
11354
11529
|
takePaths,
|
|
11355
11530
|
loadTake,
|
|
11531
|
+
pullMedia,
|
|
11356
11532
|
startTakeServer,
|
|
11357
11533
|
waitForPageDone,
|
|
11358
11534
|
renderAnimation,
|
|
@@ -11369,10 +11545,9 @@ export {
|
|
|
11369
11545
|
recordTake,
|
|
11370
11546
|
encodeRecording,
|
|
11371
11547
|
planTake,
|
|
11372
|
-
pullMedia,
|
|
11373
11548
|
parseAgentBrowserLog,
|
|
11374
11549
|
splitCommand,
|
|
11375
11550
|
convertAgentBrowser,
|
|
11376
11551
|
run
|
|
11377
11552
|
};
|
|
11378
|
-
//# sourceMappingURL=chunk-
|
|
11553
|
+
//# sourceMappingURL=chunk-DD6OXVFJ.js.map
|