halfsize 0.1.2 → 0.1.3

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
@@ -164,6 +164,7 @@ halfsize <file> [options]
164
164
  --json Machine-readable result
165
165
  --dry-run Print the plan, do not encode
166
166
  --web Start the local web app
167
+ --port <n> Port for the web app (default 4174)
167
168
  --doctor Show which FFmpeg and encoders were found
168
169
  -h, --help Help
169
170
  -v, --version Version
package/README.zh-CN.md CHANGED
@@ -155,6 +155,7 @@ halfsize <file> [options]
155
155
  --json 机器可读结果
156
156
  --dry-run 只打印方案,不编码
157
157
  --web 启动本地网页版
158
+ --port <n> 网页版端口(默认 4174)
158
159
  --doctor 查看找到的 FFmpeg 和编码器
159
160
  -h, --help 帮助
160
161
  -v, --version 版本
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfsize",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Compress any video to an exact target size, locally, and prove the quality did not drop.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -7,7 +7,7 @@ import { encode } from "./encode.mjs";
7
7
  import { measureQuality, verdict } from "./quality.mjs";
8
8
  import { bytes, duration, bitrate, ratio, parseSize, clock } from "./human.mjs";
9
9
 
10
- const VERSION = "0.1.2";
10
+ const VERSION = "0.1.3";
11
11
 
12
12
  const HELP = `
13
13
  halfsize ${VERSION} - compress a video to an exact size, and prove it still looks the same.
@@ -17,6 +17,7 @@ const HELP = `
17
17
  halfsize <file> --to 25mb --codec h265
18
18
  halfsize <file> --crf 23 Quality-first mode
19
19
  halfsize --web Open the local app
20
+ halfsize --web --port 8080 Use a different port
20
21
  halfsize --doctor Check FFmpeg and hardware encoders
21
22
 
22
23
  Options
@@ -29,6 +30,7 @@ const HELP = `
29
30
  --no-downscale Never reduce resolution
30
31
  --json Machine-readable result
31
32
  --dry-run Print the plan, do not encode
33
+ --port <n> Port for --web (default 4174)
32
34
  -h, --help Show this help
33
35
  -v, --version Show version
34
36
 
@@ -52,6 +54,7 @@ function parseArgs(argv) {
52
54
  else if (a === "--no-downscale") args.flags.downscale = false;
53
55
  else if (a === "--web") args.flags.web = true;
54
56
  else if (a === "--doctor") args.flags.doctor = true;
57
+ else if (a === "--port") args.flags.port = Number(argv[++i]);
55
58
  else if (a === "--to") args.flags.to = argv[++i];
56
59
  else if (a === "--crf") args.flags.crf = Number(argv[++i]);
57
60
  else if (a === "--codec") args.flags.codec = argv[++i];
@@ -76,7 +79,7 @@ export async function main(argv) {
76
79
  return;
77
80
  }
78
81
  if (flags.doctor) return doctor();
79
- if (flags.web) return startWeb();
82
+ if (flags.web) return startWeb(flags.port);
80
83
 
81
84
  const input = path.resolve(args._[0]);
82
85
  if (!fs.existsSync(input)) throw new Error(`File not found: ${input}`);
@@ -192,7 +195,7 @@ class Progress {
192
195
  this.start = Date.now();
193
196
  }
194
197
 
195
- update({ ratio, speed }) {
198
+ update({ ratio, speed, pass, passes }) {
196
199
  if (!this.enabled) return;
197
200
  const now = Date.now();
198
201
  if (now - this.lastPrint < 120 && ratio < 1) return;
@@ -203,8 +206,9 @@ class Progress {
203
206
  const elapsed = (now - this.start) / 1000;
204
207
  const eta = ratio > 0.02 ? (elapsed / ratio) * (1 - ratio) : 0;
205
208
  const speedText = String(speed || "").replace("x", "");
209
+ const passText = passes > 1 ? `pass ${pass}/2 ` : "";
206
210
  process.stdout.write(
207
- `\r [${bar}] ${String(Math.round(ratio * 100)).padStart(3)}% ${speedText ? `${speedText}x` : ""} eta ${clock(eta)} `,
211
+ `\r [${bar}] ${String(Math.round(ratio * 100)).padStart(3)}% ${passText}${speedText ? `${speedText}x` : ""} eta ${clock(eta)} `,
208
212
  );
209
213
  }
210
214
 
@@ -271,9 +275,9 @@ async function doctor() {
271
275
  console.log("");
272
276
  }
273
277
 
274
- async function startWeb() {
278
+ async function startWeb(port) {
275
279
  const { startServer } = await import("./server.mjs");
276
- const url = await startServer();
280
+ const url = await startServer(port ? { port } : {});
277
281
  console.log("");
278
282
  console.log(` halfsize is running at ${url}`);
279
283
  console.log(" Press Ctrl+C to stop.");
package/src/encode.mjs CHANGED
@@ -46,7 +46,7 @@ export async function encode(input, output, plan, info, onProgress = () => {}) {
46
46
  "-f", "null",
47
47
  NULL_DEVICE,
48
48
  ];
49
- await runFfmpeg(ffmpeg, pass1, info.duration, () => {});
49
+ await runFfmpeg(ffmpeg, pass1, info.duration, (p) => onProgress({ ...p, ratio: p.ratio * 0.5, pass: 1, passes: 2 }));
50
50
  const pass2 = [
51
51
  ...baseArgs(input, plan, passlog),
52
52
  "-b:v", `${plan.videoKbps}k`,
@@ -55,7 +55,7 @@ export async function encode(input, output, plan, info, onProgress = () => {}) {
55
55
  "-movflags", "+faststart",
56
56
  output,
57
57
  ];
58
- await runFfmpeg(ffmpeg, pass2, info.duration, onProgress);
58
+ await runFfmpeg(ffmpeg, pass2, info.duration, (p) => onProgress({ ...p, ratio: 0.5 + p.ratio * 0.5, pass: 2, passes: 2 }));
59
59
  } else {
60
60
  const args = [...baseArgs(input, plan, null)];
61
61
  if (plan.mode === "size") {
@@ -64,7 +64,7 @@ export async function encode(input, output, plan, info, onProgress = () => {}) {
64
64
  args.push("-crf", String(plan.crf));
65
65
  }
66
66
  args.push(...audioArgs(plan), "-movflags", "+faststart", output);
67
- await runFfmpeg(ffmpeg, args, info.duration, onProgress);
67
+ await runFfmpeg(ffmpeg, args, info.duration, (p) => onProgress({ ...p, pass: 1, passes: 1 }));
68
68
  }
69
69
  } finally {
70
70
  cleanup(passlog);
package/web/index.html CHANGED
@@ -248,6 +248,7 @@ const I18N = {
248
248
  smallerLabel: "更小",
249
249
  accuracyUnder: (p) => `比目标小 ${p}%`,
250
250
  accuracyOver: (p) => `超出目标 ${p}%`,
251
+ passLabel: (n) => `第 ${n}/2 遍`,
251
252
  alreadySmall: (a, b) => `这个文件已经只有 ${a},小于 ${b} 的目标。换一个更小的目标。`,
252
253
  badSize: "看不懂这个大小。试试 25mb、700k 或 1.5gb。",
253
254
  failed: "压缩失败",
@@ -287,6 +288,7 @@ const I18N = {
287
288
  smallerLabel: "smaller",
288
289
  accuracyUnder: (p) => `${p}% under target`,
289
290
  accuracyOver: (p) => `${p}% over target`,
291
+ passLabel: (n) => `pass ${n}/2`,
290
292
  alreadySmall: (a, b) => `This file is already ${a}, smaller than the ${b} target. Pick a smaller target.`,
291
293
  badSize: "Can't read that size. Try 25mb, 700k or 1.5gb.",
292
294
  failed: "compression failed",
@@ -461,7 +463,8 @@ go.addEventListener("click", async () => {
461
463
  const percent = Math.round((p.ratio || 0) * 100);
462
464
  fill.style.width = `${percent}%`;
463
465
  pct.textContent = `${percent}%`;
464
- speed.textContent = `${p.speed || ""} eta ${fmtDur(p.eta)}`;
466
+ const pass = p.passes > 1 ? `${strings.passLabel(p.pass)} ` : "";
467
+ speed.textContent = `${pass}${p.speed || ""} eta ${fmtDur(p.eta)}`;
465
468
  });
466
469
  es.addEventListener("phase", () => {
467
470
  fill.style.width = "100%";