bytecut 0.1.0 → 0.2.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/README.md CHANGED
@@ -18,7 +18,7 @@ landscape image is 1920 wide, a portrait one is 1920 tall, nothing is ever upsca
18
18
  and GPS are stripped, orientation is applied and the colour profile is kept.
19
19
 
20
20
  ```
21
- public/img/hero.jpg → public/_bytecut/img/hero/hero-1920.jpg
21
+ public/img/hero.jpg → public/_bytecut/img/hero-1920.jpg
22
22
  public/_bytecut/bytecut-manifest.json
23
23
  ```
24
24
 
@@ -30,6 +30,10 @@ public/img/hero.jpg → public/_bytecut/img/hero/hero-1920.jpg
30
30
  - **Videos and animated GIFs** become AV1 and H.264 MP4 plus a poster, using your own
31
31
  `ffmpeg` (see [Video](#video)).
32
32
 
33
+ Outputs are flat: an image at the top of your static folder lands directly in `_bytecut/`, and
34
+ one in a subfolder keeps that subfolder (`public/img/hero.jpg` → `_bytecut/img/hero-1920.jpg`).
35
+ There is no extra folder per image.
36
+
33
37
  Re-running is cheap: unchanged sources are skipped by content hash, and the manifest is
34
38
  byte-identical when nothing changed, so it diffs cleanly in pull requests.
35
39
 
@@ -104,6 +108,9 @@ SSIMULACRA2 runs as WebAssembly in worker threads, so no native binary is needed
104
108
 
105
109
  ## Video
106
110
 
111
+ Estimating a large video encodes real samples, so the first scan of a 4K file can take a minute or
112
+ more; the screen shows which sample it is on, and the result is cached by content hash.
113
+
107
114
  bytecut uses **your own** `ffmpeg` and `ffprobe` (`--ffmpeg`, `BYTECUT_FFMPEG`, `ffmpegPath`, or
108
115
  `PATH`). It never downloads or bundles one. Without ffmpeg images work fully and videos are
109
116
  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-4PWCQ4NF.js";
7
+ } from "./chunk-WI4ZO7GP.js";
8
8
  import {
9
9
  formatAuditReport
10
- } from "./chunk-P5PFI3AO.js";
10
+ } from "./chunk-HLBNRB3S.js";
11
11
  import "./chunk-3DBJKHOD.js";
12
12
  import "./chunk-TCQFQ354.js";
13
13
  import {
14
14
  sharpImageEngine
15
15
  } from "./chunk-CSFBJ3UH.js";
16
- import "./chunk-XUBQWBWH.js";
16
+ import "./chunk-RZ2V4PCS.js";
17
17
  import {
18
18
  configArg,
19
19
  createVideoEngine,
@@ -4,16 +4,16 @@ import {
4
4
  } from "./chunk-LTWUPQDT.js";
5
5
  import {
6
6
  audit
7
- } from "./chunk-4PWCQ4NF.js";
7
+ } from "./chunk-WI4ZO7GP.js";
8
8
  import {
9
9
  formatAuditReport
10
- } from "./chunk-P5PFI3AO.js";
10
+ } from "./chunk-HLBNRB3S.js";
11
11
  import "./chunk-3DBJKHOD.js";
12
12
  import "./chunk-TCQFQ354.js";
13
13
  import {
14
14
  sharpImageEngine
15
15
  } from "./chunk-CSFBJ3UH.js";
16
- import "./chunk-XUBQWBWH.js";
16
+ import "./chunk-RZ2V4PCS.js";
17
17
  import {
18
18
  configArg,
19
19
  createVideoEngine,
@@ -16,7 +16,7 @@ import {
16
16
  classify,
17
17
  sha256,
18
18
  sha256File
19
- } from "./chunk-XUBQWBWH.js";
19
+ } from "./chunk-RZ2V4PCS.js";
20
20
  import {
21
21
  AbortedError,
22
22
  outputWidth
@@ -161,7 +161,7 @@ async function buildPlan(options) {
161
161
  for (const width of outputWidths(preset, analysis.width, analysis.height)) {
162
162
  for (const format of formatsFor(preset, analysis, choice.confidence)) {
163
163
  const lossless = format === "png" || (preset.lossless ?? (analysis.format === "png" && format === "webp"));
164
- const path = posix.join(dir, name, `${name}-${width}.${format}`);
164
+ const path = posix.join(dir, `${name}-${width}.${format}`);
165
165
  const owner = claimed.get(path);
166
166
  if (owner !== void 0) {
167
167
  throw new BytecutError(
@@ -260,7 +260,7 @@ function plannedVideoOutputs(source, staticRoot, preset, probe) {
260
260
  for (const codec of preset.codecs) {
261
261
  outputs.push({
262
262
  role: "video",
263
- path: posix2.join(dir, name, `${name}-${width}.${codec}.mp4`),
263
+ path: posix2.join(dir, `${name}-${width}.${codec}.mp4`),
264
264
  codec,
265
265
  width,
266
266
  crf: preset.crf[codec],
@@ -274,7 +274,7 @@ function plannedVideoOutputs(source, staticRoot, preset, probe) {
274
274
  }
275
275
  outputs.push({
276
276
  role: "poster",
277
- path: posix2.join(dir, name, `${name}-poster.avif`),
277
+ path: posix2.join(dir, `${name}-poster.avif`),
278
278
  format: "avif",
279
279
  width,
280
280
  quality: preset.poster.quality,
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "bytecut",
6
- version: "0.1.0",
6
+ version: "0.2.1",
7
7
  description: "Audit and optimize images and videos in web projects.",
8
8
  type: "module",
9
9
  license: "MIT",
@@ -6,7 +6,7 @@ import {
6
6
  sortFindings,
7
7
  walkFiles,
8
8
  withVideos
9
- } from "./chunk-P5PFI3AO.js";
9
+ } from "./chunk-HLBNRB3S.js";
10
10
  import {
11
11
  MANIFEST_FILE,
12
12
  parseManifest
@@ -17,7 +17,7 @@ import {
17
17
  classify,
18
18
  classifyVideo,
19
19
  sha256
20
- } from "./chunk-XUBQWBWH.js";
20
+ } from "./chunk-RZ2V4PCS.js";
21
21
  import {
22
22
  isFastStart,
23
23
  toPosix
@@ -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);
@@ -247,6 +249,7 @@ 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
255
  streamBytes = Math.round(seconds > 0 ? total / seconds * item.durationSec : total);
@@ -444,7 +447,14 @@ async function audit(options) {
444
447
  {
445
448
  engine: options.videoEngine,
446
449
  cache,
447
- ffmpegVersion: videoVersions.ffmpeg ?? "unknown"
450
+ ffmpegVersion: videoVersions.ffmpeg ?? "unknown",
451
+ onStep: (step) => options.onProgress?.({
452
+ phase: "estimate",
453
+ done: probed - 1,
454
+ total,
455
+ source: item.source,
456
+ detail: `encoding sample ${step.done} of ${step.total}`
457
+ })
448
458
  },
449
459
  absolute
450
460
  );
package/dist/cli.js CHANGED
@@ -8,19 +8,19 @@ import {
8
8
  } from "./chunk-LTWUPQDT.js";
9
9
  import {
10
10
  audit
11
- } from "./chunk-4PWCQ4NF.js";
11
+ } from "./chunk-WI4ZO7GP.js";
12
12
  import {
13
13
  package_default
14
- } from "./chunk-AYYKUSRP.js";
14
+ } from "./chunk-SKANYRVT.js";
15
15
  import {
16
16
  formatAuditReport
17
- } from "./chunk-P5PFI3AO.js";
17
+ } from "./chunk-HLBNRB3S.js";
18
18
  import "./chunk-3DBJKHOD.js";
19
19
  import "./chunk-TCQFQ354.js";
20
20
  import {
21
21
  sharpImageEngine
22
22
  } from "./chunk-CSFBJ3UH.js";
23
- import "./chunk-XUBQWBWH.js";
23
+ import "./chunk-RZ2V4PCS.js";
24
24
  import {
25
25
  createVideoEngine,
26
26
  discover,
@@ -57,7 +57,7 @@ async function runDefault() {
57
57
  return;
58
58
  }
59
59
  if (isInteractive()) {
60
- const { runTui } = await import("./tui-CDF23I73.js");
60
+ const { runTui } = await import("./tui-6WSMCHZS.js");
61
61
  await runTui({
62
62
  projectRoot: found.projectRoot,
63
63
  project: found.project,
@@ -125,13 +125,13 @@ var main = defineCommand({
125
125
  await runDefault();
126
126
  }),
127
127
  subCommands: {
128
- audit: () => import("./audit-PEKVNXKA.js").then((m) => m.default),
129
- optimize: () => import("./optimize-47YALM3D.js").then((m) => m.default),
128
+ audit: () => import("./audit-EBKL6L65.js").then((m) => m.default),
129
+ optimize: () => import("./optimize-TLJG7UYF.js").then((m) => m.default),
130
130
  snippet: () => import("./snippet-IIPVF6PS.js").then((m) => m.default),
131
131
  compare: () => import("./compare-MPPGTUC5.js").then((m) => m.default),
132
- explain: () => import("./explain-TS6XF5TX.js").then((m) => m.default),
132
+ explain: () => import("./explain-LKOB7FRT.js").then((m) => m.default),
133
133
  init: () => import("./init-CWDWVJ55.js").then((m) => m.default),
134
- check: () => import("./check-BYNAWCDE.js").then((m) => m.default),
134
+ check: () => import("./check-XTTU5SCZ.js").then((m) => m.default),
135
135
  doctor: () => import("./doctor-PWCUEBD5.js").then((m) => m.default)
136
136
  }
137
137
  });
@@ -8,7 +8,7 @@ import {
8
8
  mirroredDir,
9
9
  outputWidths,
10
10
  plannedVideoOutputs
11
- } from "./chunk-XUBQWBWH.js";
11
+ } from "./chunk-RZ2V4PCS.js";
12
12
  import {
13
13
  configArg,
14
14
  createVideoEngine,
@@ -123,7 +123,7 @@ async function explainFile(options) {
123
123
  const dir = mirroredDir(source, config.project.staticRoot);
124
124
  const name = posix.basename(source, posix.extname(source));
125
125
  const outputs = resolved2 ? outputWidths(resolved2, analysis.width, analysis.height).flatMap(
126
- (w) => resolved2.formats.map((f) => posix.join(dir, name, `${name}-${w}.${f}`))
126
+ (w) => resolved2.formats.map((f) => posix.join(dir, `${name}-${w}.${f}`))
127
127
  ) : [];
128
128
  return {
129
129
  ...base,
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  package_default
4
- } from "./chunk-AYYKUSRP.js";
4
+ } from "./chunk-SKANYRVT.js";
5
5
  import {
6
6
  formatFindings,
7
7
  formatPlan,
8
8
  formatSummary,
9
9
  optimize
10
- } from "./chunk-P5PFI3AO.js";
10
+ } from "./chunk-HLBNRB3S.js";
11
11
  import "./chunk-3DBJKHOD.js";
12
12
  import {
13
13
  isPlaceholderKind,
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  sharpImageEngine
19
19
  } from "./chunk-CSFBJ3UH.js";
20
- import "./chunk-XUBQWBWH.js";
20
+ import "./chunk-RZ2V4PCS.js";
21
21
  import {
22
22
  configArg,
23
23
  createVideoEngine,
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  audit
4
- } from "./chunk-4PWCQ4NF.js";
4
+ } from "./chunk-WI4ZO7GP.js";
5
5
  import {
6
6
  formatSummary,
7
7
  optimize
8
- } from "./chunk-P5PFI3AO.js";
8
+ } from "./chunk-HLBNRB3S.js";
9
9
  import {
10
10
  buildSnippet,
11
11
  originalUrl,
@@ -19,7 +19,7 @@ import {
19
19
  MANIFEST_FILE,
20
20
  parseManifest
21
21
  } from "./chunk-TCQFQ354.js";
22
- import "./chunk-XUBQWBWH.js";
22
+ import "./chunk-RZ2V4PCS.js";
23
23
  import {
24
24
  toPosix
25
25
  } from "./chunk-QOG2F4SF.js";
@@ -454,7 +454,16 @@ function effectivePreset(state, row) {
454
454
  function reducer(state, action) {
455
455
  switch (action.type) {
456
456
  case "scan-progress":
457
- return { ...state, scan: { phase: action.phase, done: action.done, total: action.total } };
457
+ return {
458
+ ...state,
459
+ scan: {
460
+ phase: action.phase,
461
+ done: action.done,
462
+ total: action.total,
463
+ ...action.source ? { source: action.source } : {},
464
+ ...action.detail ? { detail: action.detail } : {}
465
+ }
466
+ };
458
467
  case "scan-done":
459
468
  return {
460
469
  ...state,
@@ -1579,10 +1588,18 @@ function App(props) {
1579
1588
  let body;
1580
1589
  if (state.view === "scanning") {
1581
1590
  const sc = state.scan;
1582
- body = /* @__PURE__ */ jsxs5(Text5, { children: [
1583
- "Scanning",
1584
- sc && sc.total > 0 ? ` ${Math.min(sc.done, sc.total)}/${sc.total} images (${sc.phase})` : "",
1585
- "\u2026"
1591
+ body = /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1592
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1593
+ "Scanning",
1594
+ sc && sc.total > 0 ? ` ${Math.min(sc.done, sc.total)}/${sc.total} files (${sc.phase})` : "",
1595
+ "\u2026"
1596
+ ] }),
1597
+ sc?.source && sc.detail ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1598
+ sc.source,
1599
+ ": ",
1600
+ sc.detail,
1601
+ ". Large videos take a while; the result is cached."
1602
+ ] }) : null
1586
1603
  ] });
1587
1604
  } else if (state.view === "error") {
1588
1605
  body = /* @__PURE__ */ jsx5(Text5, { color: "red", children: state.error });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bytecut",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Audit and optimize images and videos in web projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",