bytecut 0.2.0 → 0.3.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 +21 -4
- package/dist/{audit-LJOGSJS6.js → audit-RKJNHNDZ.js} +11 -11
- package/dist/{check-MEBXJBNG.js → check-MN7R7XOM.js} +10 -10
- package/dist/{chunk-IZ4JEL3Q.js → chunk-4AM2CCML.js} +1 -1
- package/dist/{chunk-4FIB6VNC.js → chunk-7RHHOWQT.js} +1 -1
- package/dist/{chunk-KN5QOB7R.js → chunk-ABHEWNYQ.js} +4 -4
- package/dist/{chunk-TCQFQ354.js → chunk-CSDZGNUN.js} +14 -3
- package/dist/{chunk-5JEA233Z.js → chunk-DZLFVN4G.js} +25 -12
- package/dist/{chunk-PQRBLHCC.js → chunk-EM7JMQND.js} +1 -1
- package/dist/{chunk-HLBNRB3S.js → chunk-GNW3OK5U.js} +198 -44
- package/dist/{chunk-RZ2V4PCS.js → chunk-JMMXC5UA.js} +3 -3
- package/dist/{chunk-FY53B4NB.js → chunk-KJK7XGWA.js} +24 -13
- package/dist/{chunk-QOG2F4SF.js → chunk-L5HLKZDW.js} +2 -2
- package/dist/{chunk-MV6J66AP.js → chunk-MUE3L5XT.js} +1 -1
- package/dist/{chunk-LJWOPIM6.js → chunk-NOTYXNMY.js} +1 -1
- package/dist/{chunk-3PF6BBFK.js → chunk-PE5NKZCH.js} +18 -8
- package/dist/{chunk-S3FQYUTI.js → chunk-SMR2764T.js} +1 -1
- package/dist/{chunk-7DHE25UE.js → chunk-UZ3Z2T5I.js} +1 -1
- package/dist/cli.js +22 -22
- package/dist/{compare-MPPGTUC5.js → compare-XRMDMCZL.js} +8 -8
- package/dist/{doctor-PWCUEBD5.js → doctor-YYTLIOVG.js} +5 -5
- package/dist/{explain-LKOB7FRT.js → explain-GZWGHYRY.js} +12 -7
- package/dist/{init-CWDWVJ55.js → init-3IQAWWBW.js} +4 -4
- package/dist/{optimize-SLLQGDDX.js → optimize-BDPL4Y2Z.js} +11 -11
- package/dist/{snippet-IIPVF6PS.js → snippet-6P7CMROL.js} +7 -7
- package/dist/{tui-ZAEXBY4L.js → tui-ETSPQFXB.js} +51 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,15 +74,26 @@ Everything is optional. Config is only needed to make a choice stick. Put it in
|
|
|
74
74
|
"maxSize": "150 KB",
|
|
75
75
|
"floor": 65
|
|
76
76
|
},
|
|
77
|
-
"loop": { "extends": "background-video", "vmaf": 92 }
|
|
77
|
+
"loop": { "extends": "background-video", "vmaf": 92 },
|
|
78
|
+
"promo": {
|
|
79
|
+
"extends": "content-video",
|
|
80
|
+
"ssimulacra2": 70,
|
|
81
|
+
"floor": 55,
|
|
82
|
+
"maxSize": "5 MB"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"assets": {
|
|
86
|
+
"public/gallery/**": "gallery",
|
|
87
|
+
"public/loops/*.mp4": "loop",
|
|
88
|
+
"public/promo.mp4": "promo"
|
|
78
89
|
},
|
|
79
|
-
"assets": { "public/gallery/**": "gallery", "public/loops/*.mp4": "loop" },
|
|
80
90
|
"budgets": { "maxFileSize": "250 KB", "maxTotalSize": "5 MB" }
|
|
81
91
|
}
|
|
82
92
|
```
|
|
83
93
|
|
|
84
94
|
- **Presets**: built-in `photo`, `hero`, `icon`, `background-video`, `content-video` and
|
|
85
|
-
`gif-to-video`. Add your own with `extends`; the first matching `assets` glob wins.
|
|
95
|
+
`gif-to-video`. Add your own with `extends`; the first matching `assets` glob wins. In the
|
|
96
|
+
TUI, `p` and `/preset` offer image presets for images and video presets for videos and GIFs.
|
|
86
97
|
- **Size**: `maxEdge` caps the longest side; `widths` lists widths instead (a responsive set).
|
|
87
98
|
- **Formats**: `jpg` (default), `png`, `webp`, `avif`. A `<picture>` lists AVIF, then WebP, and
|
|
88
99
|
the JPEG is the `<img>`.
|
|
@@ -98,7 +109,10 @@ that still meets it:
|
|
|
98
109
|
confirms on the full-size output.
|
|
99
110
|
- `maxSize` with a required `floor` lowers quality, then steps the width down, to fit a size
|
|
100
111
|
cap. A constraint that cannot be met writes nothing and fails the run; it is never silently
|
|
101
|
-
exceeded.
|
|
112
|
+
exceeded. On video the cap is per output file (AV1 and H.264 each): the CRF is raised until
|
|
113
|
+
the predicted size fits, never past the `floor`, and then the width steps down by a fifth at a
|
|
114
|
+
time, never below half. The finished file is measured, not just predicted. A narrower file is
|
|
115
|
+
named for its real width, e.g. `promo-1228.h264.mp4`.
|
|
102
116
|
- `vmaf: 92` (video) uses ffmpeg's `libvmaf`. Without it, the preset's fixed quality is used
|
|
103
117
|
and a warning says so.
|
|
104
118
|
|
|
@@ -108,6 +122,9 @@ SSIMULACRA2 runs as WebAssembly in worker threads, so no native binary is needed
|
|
|
108
122
|
|
|
109
123
|
## Video
|
|
110
124
|
|
|
125
|
+
Estimating a large video encodes real samples, so the first scan of a 4K file can take a minute or
|
|
126
|
+
more; the screen shows which sample it is on, and the result is cached by content hash.
|
|
127
|
+
|
|
111
128
|
bytecut uses **your own** `ffmpeg` and `ffprobe` (`--ffmpeg`, `BYTECUT_FFMPEG`, `ffmpegPath`, or
|
|
112
129
|
`PATH`). It never downloads or bundles one. Without ffmpeg images work fully and videos are
|
|
113
130
|
listed as "needs ffmpeg" with the install command for your OS. Without an AV1 encoder you still
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
} from "./chunk-LTWUPQDT.js";
|
|
5
5
|
import {
|
|
6
6
|
audit
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DZLFVN4G.js";
|
|
8
8
|
import {
|
|
9
9
|
formatAuditReport
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-GNW3OK5U.js";
|
|
11
11
|
import "./chunk-3DBJKHOD.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-CSDZGNUN.js";
|
|
13
13
|
import {
|
|
14
14
|
sharpImageEngine
|
|
15
15
|
} from "./chunk-CSFBJ3UH.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-JMMXC5UA.js";
|
|
17
17
|
import {
|
|
18
18
|
configArg,
|
|
19
19
|
createVideoEngine,
|
|
@@ -21,15 +21,15 @@ import {
|
|
|
21
21
|
ffmpegInstallText,
|
|
22
22
|
ffmpegStatus,
|
|
23
23
|
noAssetsMessage
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import "./chunk-
|
|
26
|
-
import "./chunk-
|
|
27
|
-
import "./chunk-
|
|
28
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-ABHEWNYQ.js";
|
|
25
|
+
import "./chunk-SMR2764T.js";
|
|
26
|
+
import "./chunk-L5HLKZDW.js";
|
|
27
|
+
import "./chunk-PE5NKZCH.js";
|
|
28
|
+
import "./chunk-4AM2CCML.js";
|
|
29
29
|
import {
|
|
30
30
|
guarded
|
|
31
|
-
} from "./chunk-
|
|
32
|
-
import "./chunk-
|
|
31
|
+
} from "./chunk-MUE3L5XT.js";
|
|
32
|
+
import "./chunk-KJK7XGWA.js";
|
|
33
33
|
|
|
34
34
|
// src/cli/commands/audit.ts
|
|
35
35
|
import { defineCommand } from "citty";
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
} from "./chunk-LTWUPQDT.js";
|
|
5
5
|
import {
|
|
6
6
|
audit
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DZLFVN4G.js";
|
|
8
8
|
import {
|
|
9
9
|
formatAuditReport
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-GNW3OK5U.js";
|
|
11
11
|
import "./chunk-3DBJKHOD.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-CSDZGNUN.js";
|
|
13
13
|
import {
|
|
14
14
|
sharpImageEngine
|
|
15
15
|
} from "./chunk-CSFBJ3UH.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-JMMXC5UA.js";
|
|
17
17
|
import {
|
|
18
18
|
configArg,
|
|
19
19
|
createVideoEngine,
|
|
@@ -21,22 +21,22 @@ import {
|
|
|
21
21
|
ffmpegInstallText,
|
|
22
22
|
ffmpegStatus,
|
|
23
23
|
noAssetsMessage
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ABHEWNYQ.js";
|
|
25
25
|
import {
|
|
26
26
|
run
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-SMR2764T.js";
|
|
28
28
|
import {
|
|
29
29
|
toPosix
|
|
30
|
-
} from "./chunk-
|
|
31
|
-
import "./chunk-
|
|
32
|
-
import "./chunk-
|
|
30
|
+
} from "./chunk-L5HLKZDW.js";
|
|
31
|
+
import "./chunk-PE5NKZCH.js";
|
|
32
|
+
import "./chunk-4AM2CCML.js";
|
|
33
33
|
import {
|
|
34
34
|
BytecutError,
|
|
35
35
|
CONFIG_FILES,
|
|
36
36
|
formatBytes,
|
|
37
37
|
percentChange,
|
|
38
38
|
plural
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-KJK7XGWA.js";
|
|
40
40
|
|
|
41
41
|
// src/cli/commands/check.ts
|
|
42
42
|
import { defineCommand } from "citty";
|
|
@@ -3,19 +3,19 @@ import {
|
|
|
3
3
|
isFile,
|
|
4
4
|
readOsReleaseId,
|
|
5
5
|
run
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SMR2764T.js";
|
|
7
7
|
import {
|
|
8
8
|
createFfmpegVideoEngine
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-L5HLKZDW.js";
|
|
10
10
|
import {
|
|
11
11
|
installHint,
|
|
12
12
|
resolveBinary,
|
|
13
13
|
resolveConfig
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-PE5NKZCH.js";
|
|
15
15
|
import {
|
|
16
16
|
BytecutError,
|
|
17
17
|
loadConfig
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-KJK7XGWA.js";
|
|
19
19
|
|
|
20
20
|
// src/cli/shared.ts
|
|
21
21
|
import { existsSync } from "fs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
BytecutError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KJK7XGWA.js";
|
|
5
5
|
|
|
6
6
|
// src/core/plan/schema.ts
|
|
7
7
|
import { z } from "zod";
|
|
@@ -49,7 +49,12 @@ var videoOutputSchema = z.discriminatedUnion("role", [
|
|
|
49
49
|
maxFps: z.number().positive().nullable(),
|
|
50
50
|
audio: z.enum(["strip", "keep"]),
|
|
51
51
|
audioBitrate: z.string().regex(/^\d+k$/),
|
|
52
|
-
search: z.object({
|
|
52
|
+
search: z.object({
|
|
53
|
+
metric: z.enum(["ssimulacra2", "vmaf"]),
|
|
54
|
+
target: z.number().min(1).max(100),
|
|
55
|
+
floor: z.number().min(1).max(100).optional(),
|
|
56
|
+
maxBytes: z.number().int().positive().optional()
|
|
57
|
+
}).strict().optional()
|
|
53
58
|
}).strict(),
|
|
54
59
|
z.object({
|
|
55
60
|
role: z.literal("poster"),
|
|
@@ -182,7 +187,13 @@ var videoOutputSchema2 = z2.object({
|
|
|
182
187
|
codecs: z2.string().optional(),
|
|
183
188
|
encoder: z2.string(),
|
|
184
189
|
status: z2.enum(["written", "already-optimal", "stale"]),
|
|
185
|
-
search: z2.object({
|
|
190
|
+
search: z2.object({
|
|
191
|
+
metric: z2.enum(["ssimulacra2", "vmaf"]),
|
|
192
|
+
target: z2.number(),
|
|
193
|
+
floor: z2.number().optional(),
|
|
194
|
+
maxBytes: z2.number().int().optional(),
|
|
195
|
+
plannedPath: z2.string().optional()
|
|
196
|
+
}).strict().optional(),
|
|
186
197
|
score: z2.number().optional(),
|
|
187
198
|
scorer: z2.enum(["wasm", "native", "libvmaf"]).optional()
|
|
188
199
|
}).strict();
|
|
@@ -6,38 +6,38 @@ import {
|
|
|
6
6
|
sortFindings,
|
|
7
7
|
walkFiles,
|
|
8
8
|
withVideos
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-GNW3OK5U.js";
|
|
10
10
|
import {
|
|
11
11
|
MANIFEST_FILE,
|
|
12
12
|
parseManifest
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-CSDZGNUN.js";
|
|
14
14
|
import {
|
|
15
15
|
ANIMATED_GIF_REASON,
|
|
16
16
|
buildPlan,
|
|
17
17
|
classify,
|
|
18
18
|
classifyVideo,
|
|
19
19
|
sha256
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-JMMXC5UA.js";
|
|
21
21
|
import {
|
|
22
22
|
isFastStart,
|
|
23
23
|
toPosix
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-L5HLKZDW.js";
|
|
25
25
|
import {
|
|
26
26
|
BUILTIN_POLICY,
|
|
27
27
|
getImagePreset,
|
|
28
28
|
pickAv1Encoder,
|
|
29
29
|
rootPreset
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-PE5NKZCH.js";
|
|
31
31
|
import {
|
|
32
32
|
SafeFs
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-4AM2CCML.js";
|
|
34
34
|
import {
|
|
35
35
|
ALL_RULES,
|
|
36
36
|
checkCache,
|
|
37
37
|
checkVideoCache,
|
|
38
38
|
matchesGlob,
|
|
39
39
|
savingOf
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-KJK7XGWA.js";
|
|
41
41
|
|
|
42
42
|
// src/core/commands/audit.ts
|
|
43
43
|
import { stat as stat2 } from "fs/promises";
|
|
@@ -212,6 +212,8 @@ async function estimateVideo(item, deps, sourcePath) {
|
|
|
212
212
|
const starts = sampleStarts(item.durationSec);
|
|
213
213
|
const exact = starts.length === 1 && starts[0].durationSec >= item.durationSec;
|
|
214
214
|
const perCodec = [];
|
|
215
|
+
const stepTotal = item.outputs.filter((o) => o.role === "video" && encoderFor(o.codec, caps)).length * starts.length;
|
|
216
|
+
let stepsDone = 0;
|
|
215
217
|
for (const out of item.outputs) {
|
|
216
218
|
if (out.role !== "video") continue;
|
|
217
219
|
const encoder = encoderFor(out.codec, caps);
|
|
@@ -230,10 +232,10 @@ async function estimateVideo(item, deps, sourcePath) {
|
|
|
230
232
|
].join("|");
|
|
231
233
|
let streamBytes = deps.cache.getEstimate(key)?.bytes;
|
|
232
234
|
if (streamBytes === void 0) {
|
|
233
|
-
let
|
|
235
|
+
let total2 = 0;
|
|
234
236
|
let seconds = 0;
|
|
235
237
|
for (const s of starts) {
|
|
236
|
-
|
|
238
|
+
total2 += await deps.engine.sampleBytes(
|
|
237
239
|
{
|
|
238
240
|
source: sourcePath,
|
|
239
241
|
encoder,
|
|
@@ -247,14 +249,18 @@ async function estimateVideo(item, deps, sourcePath) {
|
|
|
247
249
|
},
|
|
248
250
|
deps.signal
|
|
249
251
|
);
|
|
252
|
+
deps.onStep?.({ done: ++stepsDone, total: stepTotal });
|
|
250
253
|
seconds += s.durationSec;
|
|
251
254
|
}
|
|
252
|
-
streamBytes = Math.round(seconds > 0 ?
|
|
255
|
+
streamBytes = Math.round(seconds > 0 ? total2 / seconds * item.durationSec : total2);
|
|
253
256
|
deps.cache.setEstimate(key, { bytes: streamBytes, basis: exact ? "exact" : "extrapolated" });
|
|
254
257
|
}
|
|
258
|
+
const total = streamBytes + audioBytes(out, item);
|
|
255
259
|
perCodec.push({
|
|
256
260
|
codec: out.codec,
|
|
257
|
-
|
|
261
|
+
// A size cap is met by raising the CRF or narrowing the width, or the output is not
|
|
262
|
+
// written at all, so what a run will produce never exceeds it.
|
|
263
|
+
bytes: out.search?.maxBytes !== void 0 ? Math.min(total, out.search.maxBytes) : total,
|
|
258
264
|
basis: exact ? "exact" : "extrapolated"
|
|
259
265
|
});
|
|
260
266
|
}
|
|
@@ -444,7 +450,14 @@ async function audit(options) {
|
|
|
444
450
|
{
|
|
445
451
|
engine: options.videoEngine,
|
|
446
452
|
cache,
|
|
447
|
-
ffmpegVersion: videoVersions.ffmpeg ?? "unknown"
|
|
453
|
+
ffmpegVersion: videoVersions.ffmpeg ?? "unknown",
|
|
454
|
+
onStep: (step) => options.onProgress?.({
|
|
455
|
+
phase: "estimate",
|
|
456
|
+
done: probed - 1,
|
|
457
|
+
total,
|
|
458
|
+
source: item.source,
|
|
459
|
+
detail: `encoding sample ${step.done} of ${step.total}`
|
|
460
|
+
})
|
|
448
461
|
},
|
|
449
462
|
absolute
|
|
450
463
|
);
|