bytecut 0.0.0-stage → 0.0.1
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/dist/audit-HVW4EOLP.js +97 -0
- package/dist/check-YONIQTM7.js +348 -0
- package/dist/chunk-326CQQWX.js +188 -0
- package/dist/chunk-3DBJKHOD.js +109 -0
- package/dist/chunk-4XOB3C34.js +186 -0
- package/dist/chunk-5R7LR5BB.js +282 -0
- package/dist/chunk-7IECTPJB.js +319 -0
- package/dist/chunk-CEZK3RQJ.js +2170 -0
- package/dist/chunk-DJ45NQZD.js +29 -0
- package/dist/chunk-G53LNJ5O.js +57 -0
- package/dist/chunk-GMHOU4Y5.js +59 -0
- package/dist/chunk-J2XJO4JZ.js +21 -0
- package/dist/chunk-LHTI3JMC.js +212 -0
- package/dist/chunk-LTWUPQDT.js +26 -0
- package/dist/chunk-M6ZDSATP.js +59 -0
- package/dist/chunk-N536JFSF.js +614 -0
- package/dist/chunk-NAKV7HS5.js +370 -0
- package/dist/chunk-TWI2OZJZ.js +1432 -0
- package/dist/chunk-U4KJMKI5.js +229 -0
- package/dist/chunk-WE7AEDVI.js +536 -0
- package/dist/chunk-XFJ3HW4X.js +248 -0
- package/dist/chunk-YBEC5QGV.js +463 -0
- package/dist/cli.js +138 -0
- package/dist/compare-6PCE6533.js +84 -0
- package/dist/doctor-R226JU6P.js +77 -0
- package/dist/explain-SGN5BOR7.js +265 -0
- package/dist/init-WHWUANOB.js +92 -0
- package/dist/optimize-UXM6JEDA.js +216 -0
- package/dist/snippet-2WVGMCNV.js +114 -0
- package/dist/tui-J4Y44IL7.js +1691 -0
- package/package.json +46 -3
- package/README.md +0 -3
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/video/codecs.ts
|
|
4
|
+
import { open } from "fs/promises";
|
|
5
|
+
function* boxes(buf, start, end) {
|
|
6
|
+
let at = start;
|
|
7
|
+
while (at + 8 <= end) {
|
|
8
|
+
let size = buf.readUInt32BE(at);
|
|
9
|
+
const type = buf.toString("latin1", at + 4, at + 8);
|
|
10
|
+
let header = 8;
|
|
11
|
+
if (size === 1) {
|
|
12
|
+
if (at + 16 > end) return;
|
|
13
|
+
size = Number(buf.readBigUInt64BE(at + 8));
|
|
14
|
+
header = 16;
|
|
15
|
+
} else if (size === 0) {
|
|
16
|
+
size = end - at;
|
|
17
|
+
}
|
|
18
|
+
if (size < header || at + size > end) return;
|
|
19
|
+
yield { type, start: at + header, end: at + size };
|
|
20
|
+
at += size;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function find(buf, parent, type) {
|
|
24
|
+
for (const box of boxes(buf, parent.start, parent.end)) if (box.type === type) return box;
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
var hex2 = (n) => n.toString(16).padStart(2, "0");
|
|
28
|
+
function avcCodecString(avcC) {
|
|
29
|
+
if (avcC.length < 4) return null;
|
|
30
|
+
return `avc1.${hex2(avcC[1])}${hex2(avcC[2])}${hex2(avcC[3])}`;
|
|
31
|
+
}
|
|
32
|
+
function av1CodecString(av1C) {
|
|
33
|
+
if (av1C.length < 4 || (av1C[0] & 128) === 0) return null;
|
|
34
|
+
const profile = av1C[1] >> 5;
|
|
35
|
+
const level = av1C[1] & 31;
|
|
36
|
+
const tier = av1C[2] >> 7 & 1;
|
|
37
|
+
const highBitdepth = av1C[2] >> 6 & 1;
|
|
38
|
+
const twelveBit = av1C[2] >> 5 & 1;
|
|
39
|
+
const depth = highBitdepth ? twelveBit ? 12 : 10 : 8;
|
|
40
|
+
return `av01.${profile}.${String(level).padStart(2, "0")}${tier ? "H" : "M"}.${String(depth).padStart(2, "0")}`;
|
|
41
|
+
}
|
|
42
|
+
async function readVideoCodecString(path) {
|
|
43
|
+
const file = await open(path, "r");
|
|
44
|
+
try {
|
|
45
|
+
const { size } = await file.stat();
|
|
46
|
+
const header = Buffer.alloc(16);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (let guard = 0; guard < 64 && offset + 8 <= size; guard++) {
|
|
49
|
+
const { bytesRead } = await file.read(header, 0, 16, offset);
|
|
50
|
+
if (bytesRead < 8) return null;
|
|
51
|
+
let boxSize = header.readUInt32BE(0);
|
|
52
|
+
const type = header.toString("latin1", 4, 8);
|
|
53
|
+
let headerSize = 8;
|
|
54
|
+
if (boxSize === 1) {
|
|
55
|
+
if (bytesRead < 16) return null;
|
|
56
|
+
boxSize = Number(header.readBigUInt64BE(8));
|
|
57
|
+
headerSize = 16;
|
|
58
|
+
} else if (boxSize === 0) {
|
|
59
|
+
boxSize = size - offset;
|
|
60
|
+
}
|
|
61
|
+
if (boxSize < headerSize) return null;
|
|
62
|
+
if (type === "moov") {
|
|
63
|
+
const payload = Buffer.alloc(boxSize - headerSize);
|
|
64
|
+
await file.read(payload, 0, payload.length, offset + headerSize);
|
|
65
|
+
return codecFromMoov(payload);
|
|
66
|
+
}
|
|
67
|
+
offset += boxSize;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
} finally {
|
|
71
|
+
await file.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function codecFromMoov(moov) {
|
|
75
|
+
const root = { start: 0, end: moov.length };
|
|
76
|
+
for (const trak of boxes(moov, root.start, root.end)) {
|
|
77
|
+
if (trak.type !== "trak") continue;
|
|
78
|
+
const mdia = find(moov, trak, "mdia");
|
|
79
|
+
if (!mdia) continue;
|
|
80
|
+
const hdlr = find(moov, mdia, "hdlr");
|
|
81
|
+
if (!hdlr || moov.toString("latin1", hdlr.start + 8, hdlr.start + 12) !== "vide") continue;
|
|
82
|
+
const minf = find(moov, mdia, "minf");
|
|
83
|
+
const stbl = minf && find(moov, minf, "stbl");
|
|
84
|
+
const stsd = stbl && find(moov, stbl, "stsd");
|
|
85
|
+
if (!stsd) continue;
|
|
86
|
+
for (const entry of boxes(moov, stsd.start + 8, stsd.end)) {
|
|
87
|
+
const childStart = entry.start + 78;
|
|
88
|
+
if (entry.type === "avc1" || entry.type === "avc3") {
|
|
89
|
+
const avcC = find(moov, { start: childStart, end: entry.end }, "avcC");
|
|
90
|
+
if (avcC) return avcCodecString(moov.subarray(avcC.start, avcC.end));
|
|
91
|
+
}
|
|
92
|
+
if (entry.type === "av01") {
|
|
93
|
+
const av1C = find(moov, { start: childStart, end: entry.end }, "av1C");
|
|
94
|
+
if (av1C) return av1CodecString(moov.subarray(av1C.start, av1C.end));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function fallbackAv1CodecString(width, height) {
|
|
101
|
+
const pixels = width * height;
|
|
102
|
+
const level = pixels <= 147456 ? 0 : pixels <= 278784 ? 1 : pixels <= 665856 ? 4 : pixels <= 1065024 ? 5 : pixels <= 2359296 ? 8 : pixels <= 8912896 ? 12 : 16;
|
|
103
|
+
return `av01.0.${String(level).padStart(2, "0")}M.08`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
readVideoCodecString,
|
|
108
|
+
fallbackAv1CodecString
|
|
109
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isFile,
|
|
4
|
+
readOsReleaseId,
|
|
5
|
+
run
|
|
6
|
+
} from "./chunk-M6ZDSATP.js";
|
|
7
|
+
import {
|
|
8
|
+
createFfmpegVideoEngine
|
|
9
|
+
} from "./chunk-WE7AEDVI.js";
|
|
10
|
+
import {
|
|
11
|
+
installHint,
|
|
12
|
+
resolveBinary,
|
|
13
|
+
resolveConfig
|
|
14
|
+
} from "./chunk-YBEC5QGV.js";
|
|
15
|
+
import {
|
|
16
|
+
BytecutError,
|
|
17
|
+
loadConfig
|
|
18
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
19
|
+
|
|
20
|
+
// src/cli/shared.ts
|
|
21
|
+
import { existsSync } from "fs";
|
|
22
|
+
import { resolve } from "path";
|
|
23
|
+
|
|
24
|
+
// src/core/quality/native.ts
|
|
25
|
+
var counter = 0;
|
|
26
|
+
function createNativeScorer(options) {
|
|
27
|
+
return {
|
|
28
|
+
kind: "native",
|
|
29
|
+
label: `ssimulacra2 (${options.binary})`,
|
|
30
|
+
async score(reference, distorted) {
|
|
31
|
+
const id = `${process.pid}-${counter++}`;
|
|
32
|
+
const paths = [];
|
|
33
|
+
try {
|
|
34
|
+
paths.push(
|
|
35
|
+
await options.fs.writeTemp(
|
|
36
|
+
`.bytecut-cache/tmp/${id}-a.png`,
|
|
37
|
+
await options.toPng(reference)
|
|
38
|
+
),
|
|
39
|
+
await options.fs.writeTemp(
|
|
40
|
+
`.bytecut-cache/tmp/${id}-b.png`,
|
|
41
|
+
await options.toPng(distorted)
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
const { stdout } = await options.run(options.binary, paths);
|
|
45
|
+
const value = Number.parseFloat(stdout.trim().split(/\s+/).pop() ?? "");
|
|
46
|
+
if (!Number.isFinite(value)) {
|
|
47
|
+
throw new BytecutError(
|
|
48
|
+
"SCORER_UNAVAILABLE",
|
|
49
|
+
`${options.binary} printed something that is not a score: ${stdout.trim().slice(0, 80)}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
} finally {
|
|
54
|
+
await Promise.all(paths.map((p) => options.fs.discardTemp(p)));
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async close() {
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/quality/wasm.ts
|
|
63
|
+
async function createWasmScorer(options = {}) {
|
|
64
|
+
let lib;
|
|
65
|
+
try {
|
|
66
|
+
lib = await import("bytecut-ssimulacra2");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new BytecutError(
|
|
69
|
+
"SCORER_UNAVAILABLE",
|
|
70
|
+
`The built-in SSIMULACRA2 scorer could not be loaded: ${error instanceof Error ? error.message : String(error)}`,
|
|
71
|
+
{ cause: error }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const pool = lib.createPool({ size: options.size ?? 2 });
|
|
75
|
+
return {
|
|
76
|
+
kind: "wasm",
|
|
77
|
+
label: "bytecut-ssimulacra2",
|
|
78
|
+
score: (reference, distorted) => pool.score(reference, distorted),
|
|
79
|
+
close: () => pool.close()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/core/quality/select.ts
|
|
84
|
+
function resolveScorerBinary(options) {
|
|
85
|
+
return resolveBinary(
|
|
86
|
+
"ssimulacra2",
|
|
87
|
+
{ flag: options.flag, config: options.config, env: options.env, platform: options.platform },
|
|
88
|
+
options.deps
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
async function selectScorer(options) {
|
|
92
|
+
const resolved = resolveScorerBinary(options);
|
|
93
|
+
const notes = [];
|
|
94
|
+
if (resolved.found) {
|
|
95
|
+
return {
|
|
96
|
+
scorer: createNativeScorer({
|
|
97
|
+
binary: resolved.path,
|
|
98
|
+
fs: options.fs,
|
|
99
|
+
toPng: options.toPng,
|
|
100
|
+
run: options.run
|
|
101
|
+
}),
|
|
102
|
+
notes
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (resolved.attempted) {
|
|
106
|
+
notes.push(
|
|
107
|
+
`ssimulacra2 not found at ${resolved.attempted} (from ${resolved.source}); using the built-in WebAssembly scorer.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
scorer: await createWasmScorer(options.size !== void 0 ? { size: options.size } : {}),
|
|
112
|
+
notes
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/cli/shared.ts
|
|
117
|
+
import { availableParallelism } from "os";
|
|
118
|
+
var configArg = {
|
|
119
|
+
config: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Path to a config file (default: bytecut.config.{ts,js,json,toml} in the project)"
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
async function discover(inputs, outDirArg, configPath) {
|
|
125
|
+
const projectRoot = process.cwd();
|
|
126
|
+
const config = await resolveConfig(projectRoot, await loadConfig(projectRoot, configPath));
|
|
127
|
+
const { project } = config;
|
|
128
|
+
const scanInputs = inputs.length > 0 ? inputs : project.include.filter((p) => existsSync(resolve(projectRoot, p)));
|
|
129
|
+
return { projectRoot, project, scanInputs, outDir: outDirArg ?? project.outDir, config };
|
|
130
|
+
}
|
|
131
|
+
function ffmpegStatus(config) {
|
|
132
|
+
const resolved = resolveBinary(
|
|
133
|
+
"ffmpeg",
|
|
134
|
+
{ env: process.env, platform: process.platform, config: config?.ffmpegPath },
|
|
135
|
+
{ isFile }
|
|
136
|
+
);
|
|
137
|
+
return { found: resolved.found, installHint: installHint(process.platform, readOsReleaseId()) };
|
|
138
|
+
}
|
|
139
|
+
function noAssetsMessage(command, project) {
|
|
140
|
+
return `bytecut${command ? ` ${command}` : ""}: no asset folder found (looked for ${project.include.join(", ")}). Pass files or folders.`;
|
|
141
|
+
}
|
|
142
|
+
function createVideoEngine(flags = {}, config = {}) {
|
|
143
|
+
return createFfmpegVideoEngine({
|
|
144
|
+
ffmpegFlag: flags.ffmpeg,
|
|
145
|
+
ffprobeFlag: flags.ffprobe,
|
|
146
|
+
ffmpegConfig: config.ffmpegPath,
|
|
147
|
+
ffprobeConfig: config.ffprobePath,
|
|
148
|
+
env: process.env,
|
|
149
|
+
platform: process.platform,
|
|
150
|
+
deps: { isFile },
|
|
151
|
+
run
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function makeScorerFactory(flag, config = {}) {
|
|
155
|
+
return async (fs) => {
|
|
156
|
+
const sharp = (await import("sharp")).default;
|
|
157
|
+
return selectScorer({
|
|
158
|
+
flag,
|
|
159
|
+
config: config.ssimulacra2Path,
|
|
160
|
+
env: process.env,
|
|
161
|
+
platform: process.platform,
|
|
162
|
+
deps: { isFile },
|
|
163
|
+
fs,
|
|
164
|
+
run,
|
|
165
|
+
size: Math.max(1, Math.min(4, availableParallelism() - 1)),
|
|
166
|
+
toPng: async (image) => new Uint8Array(
|
|
167
|
+
await sharp(image.data, {
|
|
168
|
+
raw: { width: image.width, height: image.height, channels: 3 }
|
|
169
|
+
}).png().toBuffer()
|
|
170
|
+
)
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function ffmpegInstallText() {
|
|
175
|
+
return `install ffmpeg with: ${installHint(process.platform, readOsReleaseId())}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
configArg,
|
|
180
|
+
discover,
|
|
181
|
+
ffmpegStatus,
|
|
182
|
+
noAssetsMessage,
|
|
183
|
+
createVideoEngine,
|
|
184
|
+
makeScorerFactory,
|
|
185
|
+
ffmpegInstallText
|
|
186
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BytecutError
|
|
4
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
5
|
+
|
|
6
|
+
// src/core/plan/schema.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
var safeRelativePath = z.string().min(1).refine(
|
|
9
|
+
(p) => !p.startsWith("/") && !/^[A-Za-z]:/.test(p) && !p.includes("\\") && p.split("/").every((s) => s !== "" && s !== "." && s !== ".."),
|
|
10
|
+
"must be a relative POSIX path without '..' segments"
|
|
11
|
+
);
|
|
12
|
+
var outputSchema = z.object({
|
|
13
|
+
path: safeRelativePath,
|
|
14
|
+
format: z.enum(["avif", "webp"]),
|
|
15
|
+
width: z.number().int().positive(),
|
|
16
|
+
lossless: z.boolean(),
|
|
17
|
+
quality: z.number().int().min(1).max(100),
|
|
18
|
+
effort: z.number().int().min(0).max(9),
|
|
19
|
+
search: z.object({
|
|
20
|
+
metric: z.literal("ssimulacra2"),
|
|
21
|
+
target: z.number().min(1).max(100),
|
|
22
|
+
floor: z.number().min(1).max(100).optional(),
|
|
23
|
+
maxBytes: z.number().int().positive().optional()
|
|
24
|
+
}).strict().optional()
|
|
25
|
+
}).strict();
|
|
26
|
+
var itemSchema = z.object({
|
|
27
|
+
source: safeRelativePath,
|
|
28
|
+
sourceBytes: z.number().int().nonnegative(),
|
|
29
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
30
|
+
sourceWidth: z.number().int().nonnegative(),
|
|
31
|
+
sourceHeight: z.number().int().nonnegative(),
|
|
32
|
+
preset: z.string().min(1),
|
|
33
|
+
classification: z.object({
|
|
34
|
+
reason: z.string(),
|
|
35
|
+
confidence: z.enum(["high", "medium", "low"]).optional()
|
|
36
|
+
}).strict().optional(),
|
|
37
|
+
outputs: z.array(outputSchema),
|
|
38
|
+
skip: z.object({ reason: z.string() }).strict().optional()
|
|
39
|
+
}).strict();
|
|
40
|
+
var videoOutputSchema = z.discriminatedUnion("role", [
|
|
41
|
+
z.object({
|
|
42
|
+
role: z.literal("video"),
|
|
43
|
+
path: safeRelativePath,
|
|
44
|
+
codec: z.enum(["av1", "h264"]),
|
|
45
|
+
width: z.number().int().positive(),
|
|
46
|
+
crf: z.number().int().min(0).max(63),
|
|
47
|
+
speed: z.string().min(1),
|
|
48
|
+
maxWidth: z.number().int().positive().nullable(),
|
|
49
|
+
maxFps: z.number().positive().nullable(),
|
|
50
|
+
audio: z.enum(["strip", "keep"]),
|
|
51
|
+
audioBitrate: z.string().regex(/^\d+k$/),
|
|
52
|
+
search: z.object({ metric: z.enum(["ssimulacra2", "vmaf"]), target: z.number().min(1).max(100) }).strict().optional()
|
|
53
|
+
}).strict(),
|
|
54
|
+
z.object({
|
|
55
|
+
role: z.literal("poster"),
|
|
56
|
+
path: safeRelativePath,
|
|
57
|
+
format: z.literal("avif"),
|
|
58
|
+
width: z.number().int().positive(),
|
|
59
|
+
quality: z.number().int().min(1).max(100),
|
|
60
|
+
effort: z.number().int().min(0).max(9),
|
|
61
|
+
at: z.number().min(0).max(1)
|
|
62
|
+
}).strict()
|
|
63
|
+
]);
|
|
64
|
+
var videoItemSchema = z.object({
|
|
65
|
+
source: safeRelativePath,
|
|
66
|
+
sourceBytes: z.number().int().nonnegative(),
|
|
67
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
68
|
+
sourceWidth: z.number().int().nonnegative(),
|
|
69
|
+
sourceHeight: z.number().int().nonnegative(),
|
|
70
|
+
durationSec: z.number().nonnegative(),
|
|
71
|
+
fps: z.number().nonnegative(),
|
|
72
|
+
hasAudio: z.boolean(),
|
|
73
|
+
videoCodec: z.string(),
|
|
74
|
+
preset: z.string().min(1),
|
|
75
|
+
classification: z.object({
|
|
76
|
+
reason: z.string(),
|
|
77
|
+
confidence: z.enum(["high", "medium", "low"]).optional()
|
|
78
|
+
}).strict().optional(),
|
|
79
|
+
outputs: z.array(videoOutputSchema),
|
|
80
|
+
skip: z.object({ reason: z.string() }).strict().optional()
|
|
81
|
+
}).strict();
|
|
82
|
+
var planSchema = z.object({
|
|
83
|
+
version: z.literal(1),
|
|
84
|
+
// May legitimately contain `..` (an output folder beside the project); SafeFs validates it.
|
|
85
|
+
outDir: z.string().min(1).refine((p) => !p.startsWith("/") && !/^[A-Za-z]:/.test(p) && !p.includes("\\")),
|
|
86
|
+
publicPath: z.string().min(1),
|
|
87
|
+
items: z.array(itemSchema),
|
|
88
|
+
videos: z.array(videoItemSchema).optional()
|
|
89
|
+
}).strict().superRefine((plan, ctx) => {
|
|
90
|
+
const seen = /* @__PURE__ */ new Set();
|
|
91
|
+
const outputs = [
|
|
92
|
+
...plan.items.flatMap((i) => i.outputs),
|
|
93
|
+
...(plan.videos ?? []).flatMap((v) => v.outputs)
|
|
94
|
+
];
|
|
95
|
+
for (const out of outputs) {
|
|
96
|
+
if (seen.has(out.path)) {
|
|
97
|
+
ctx.addIssue({ code: "custom", message: `duplicate output path ${out.path}` });
|
|
98
|
+
}
|
|
99
|
+
seen.add(out.path);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
function formatZodIssues(error) {
|
|
103
|
+
return error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
104
|
+
}
|
|
105
|
+
function parsePlan(text, origin = "plan") {
|
|
106
|
+
let json;
|
|
107
|
+
try {
|
|
108
|
+
json = JSON.parse(text);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new BytecutError("PLAN_INVALID", `${origin} is not valid JSON.`);
|
|
111
|
+
}
|
|
112
|
+
const result = planSchema.safeParse(json);
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
throw new BytecutError(
|
|
115
|
+
"PLAN_INVALID",
|
|
116
|
+
`${origin} is not a valid bytecut plan: ${formatZodIssues(result.error)}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return result.data;
|
|
120
|
+
}
|
|
121
|
+
function serializePlan(plan) {
|
|
122
|
+
return `${JSON.stringify(plan, null, 2)}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/core/placeholders/types.ts
|
|
127
|
+
var PLACEHOLDER_KINDS = ["dominant-color", "lqip", "blurhash"];
|
|
128
|
+
function isPlaceholderKind(value) {
|
|
129
|
+
return PLACEHOLDER_KINDS.includes(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/core/manifest/types.ts
|
|
133
|
+
var MANIFEST_FILE = "bytecut-manifest.json";
|
|
134
|
+
var MANIFEST_VERSION = 3;
|
|
135
|
+
var MANIFEST_MIN_VERSION = 2;
|
|
136
|
+
|
|
137
|
+
// src/core/manifest/io.ts
|
|
138
|
+
import { z as z2 } from "zod";
|
|
139
|
+
var outputSchema2 = z2.object({
|
|
140
|
+
path: z2.string().min(1),
|
|
141
|
+
url: z2.string(),
|
|
142
|
+
format: z2.enum(["avif", "webp"]),
|
|
143
|
+
width: z2.number().int().nonnegative(),
|
|
144
|
+
height: z2.number().int().nonnegative(),
|
|
145
|
+
bytes: z2.number().int().nonnegative(),
|
|
146
|
+
sha256: z2.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
147
|
+
lossless: z2.boolean(),
|
|
148
|
+
quality: z2.number().int(),
|
|
149
|
+
effort: z2.number().int(),
|
|
150
|
+
encoder: z2.string(),
|
|
151
|
+
status: z2.enum(["written", "already-optimal", "stale"]),
|
|
152
|
+
search: z2.object({
|
|
153
|
+
metric: z2.literal("ssimulacra2"),
|
|
154
|
+
target: z2.number(),
|
|
155
|
+
floor: z2.number().optional(),
|
|
156
|
+
maxBytes: z2.number().int().optional(),
|
|
157
|
+
plannedPath: z2.string().optional()
|
|
158
|
+
}).strict().optional(),
|
|
159
|
+
score: z2.number().optional(),
|
|
160
|
+
scorer: z2.enum(["wasm", "native"]).optional()
|
|
161
|
+
}).strict();
|
|
162
|
+
var placeholderSchema = z2.object({ kind: z2.enum(PLACEHOLDER_KINDS), value: z2.string() }).strict();
|
|
163
|
+
var videoOutputSchema2 = z2.object({
|
|
164
|
+
path: z2.string().min(1),
|
|
165
|
+
url: z2.string(),
|
|
166
|
+
role: z2.enum(["video", "poster"]),
|
|
167
|
+
format: z2.enum(["mp4", "avif"]),
|
|
168
|
+
codec: z2.enum(["av1", "h264"]).optional(),
|
|
169
|
+
width: z2.number().int().nonnegative(),
|
|
170
|
+
height: z2.number().int().nonnegative(),
|
|
171
|
+
fps: z2.number().nonnegative().optional(),
|
|
172
|
+
bytes: z2.number().int().nonnegative(),
|
|
173
|
+
sha256: z2.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
174
|
+
crf: z2.number().int().optional(),
|
|
175
|
+
speed: z2.string().optional(),
|
|
176
|
+
maxWidth: z2.number().int().nullable().optional(),
|
|
177
|
+
maxFps: z2.number().nullable().optional(),
|
|
178
|
+
audio: z2.enum(["strip", "keep"]).optional(),
|
|
179
|
+
quality: z2.number().int().optional(),
|
|
180
|
+
effort: z2.number().int().optional(),
|
|
181
|
+
at: z2.number().optional(),
|
|
182
|
+
codecs: z2.string().optional(),
|
|
183
|
+
encoder: z2.string(),
|
|
184
|
+
status: z2.enum(["written", "already-optimal", "stale"]),
|
|
185
|
+
search: z2.object({ metric: z2.enum(["ssimulacra2", "vmaf"]), target: z2.number() }).strict().optional(),
|
|
186
|
+
score: z2.number().optional(),
|
|
187
|
+
scorer: z2.enum(["wasm", "native", "libvmaf"]).optional()
|
|
188
|
+
}).strict();
|
|
189
|
+
var manifestSchema = z2.object({
|
|
190
|
+
version: z2.union([z2.literal(2), z2.literal(3)]),
|
|
191
|
+
tools: z2.record(z2.string(), z2.string()),
|
|
192
|
+
publicPath: z2.string(),
|
|
193
|
+
sources: z2.record(
|
|
194
|
+
z2.string(),
|
|
195
|
+
z2.object({
|
|
196
|
+
sha256: z2.string().regex(/^[0-9a-f]{64}$/),
|
|
197
|
+
bytes: z2.number().int().nonnegative(),
|
|
198
|
+
width: z2.number().int().nonnegative(),
|
|
199
|
+
height: z2.number().int().nonnegative(),
|
|
200
|
+
preset: z2.string(),
|
|
201
|
+
priority: z2.boolean().optional(),
|
|
202
|
+
placeholder: placeholderSchema.optional(),
|
|
203
|
+
outputs: z2.array(outputSchema2)
|
|
204
|
+
}).strict()
|
|
205
|
+
),
|
|
206
|
+
// Absent in version 2.
|
|
207
|
+
videos: z2.record(
|
|
208
|
+
z2.string(),
|
|
209
|
+
z2.object({
|
|
210
|
+
sha256: z2.string().regex(/^[0-9a-f]{64}$/),
|
|
211
|
+
bytes: z2.number().int().nonnegative(),
|
|
212
|
+
width: z2.number().int().nonnegative(),
|
|
213
|
+
height: z2.number().int().nonnegative(),
|
|
214
|
+
durationSec: z2.number().nonnegative(),
|
|
215
|
+
preset: z2.string(),
|
|
216
|
+
priority: z2.boolean().optional(),
|
|
217
|
+
placeholder: placeholderSchema.optional(),
|
|
218
|
+
outputs: z2.array(videoOutputSchema2)
|
|
219
|
+
}).strict()
|
|
220
|
+
).optional()
|
|
221
|
+
}).strict();
|
|
222
|
+
function parseManifest(text) {
|
|
223
|
+
let json;
|
|
224
|
+
try {
|
|
225
|
+
json = JSON.parse(text);
|
|
226
|
+
} catch {
|
|
227
|
+
throw new BytecutError(
|
|
228
|
+
"MANIFEST_INVALID",
|
|
229
|
+
"bytecut-manifest.json is not valid JSON. Fix or delete it (outputs are regenerated from sources)."
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const version = json?.version;
|
|
233
|
+
if (typeof version === "number" && version < MANIFEST_MIN_VERSION) {
|
|
234
|
+
throw new BytecutError(
|
|
235
|
+
"MANIFEST_OUTDATED",
|
|
236
|
+
`bytecut-manifest.json is version ${version}, written by a pre-release bytecut with an older output layout. Delete the output folder and re-run to regenerate it.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (typeof version === "number" && version > MANIFEST_VERSION) {
|
|
240
|
+
throw new BytecutError(
|
|
241
|
+
"MANIFEST_UNSUPPORTED",
|
|
242
|
+
`bytecut-manifest.json is version ${version}, but this bytecut understands up to ${MANIFEST_VERSION}. Upgrade bytecut.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const result = manifestSchema.safeParse(json);
|
|
246
|
+
if (!result.success) {
|
|
247
|
+
throw new BytecutError(
|
|
248
|
+
"MANIFEST_INVALID",
|
|
249
|
+
`bytecut-manifest.json is not a valid manifest: ${formatZodIssues(result.error)}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
...result.data,
|
|
254
|
+
version: MANIFEST_VERSION,
|
|
255
|
+
videos: result.data.videos ?? {}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function serializeManifest(manifest) {
|
|
259
|
+
const sorted = {
|
|
260
|
+
version: manifest.version,
|
|
261
|
+
tools: Object.fromEntries(Object.entries(manifest.tools).sort(([a], [b]) => a < b ? -1 : 1)),
|
|
262
|
+
publicPath: manifest.publicPath,
|
|
263
|
+
sources: Object.fromEntries(
|
|
264
|
+
Object.entries(manifest.sources).sort(([a], [b]) => a < b ? -1 : 1)
|
|
265
|
+
),
|
|
266
|
+
videos: Object.fromEntries(
|
|
267
|
+
Object.entries(manifest.videos).sort(([a], [b]) => a < b ? -1 : 1)
|
|
268
|
+
)
|
|
269
|
+
};
|
|
270
|
+
return `${JSON.stringify(sorted, null, 2)}
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export {
|
|
275
|
+
parsePlan,
|
|
276
|
+
serializePlan,
|
|
277
|
+
isPlaceholderKind,
|
|
278
|
+
MANIFEST_FILE,
|
|
279
|
+
MANIFEST_VERSION,
|
|
280
|
+
parseManifest,
|
|
281
|
+
serializeManifest
|
|
282
|
+
};
|