From 163cd652aef0de6065723b4702d963815e3bd3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B7=9D=E5=B4=8E=E7=A0=81=E5=86=9Cmmk?= <67063866@qq.com> Date: Sat, 7 Nov 2026 20:14:44 +0800 Subject: [PATCH] 0.1.9: tone-map HDR, refuse impossible targets, fix email presets HDR sources (iPhone HLG, HDR10/PQ) are converted to SDR BT.709 in the same encode, and the quality check tone-maps the reference frame so VMAF/SSIM measure compression instead of the transfer curve. A target below the bitrate floor now fails before encoding with the real minimum, and a two-pass overshoot re-encodes once. --for email is 18 MB because base64 turns a 20 MB attachment into a 26.7 MB message; --for outlook covers the 20 MB message cap. --- CHANGELOG.md | 16 + README.md | 21 +- README.zh-CN.md | 15 +- package.json | 2 +- site/20mb/index.html | 2 +- site/discord/index.html | 2 +- site/email/index.html | 2 +- site/ffmpeg-compress-to-size/index.html | 2 +- site/index.html | 11 +- site/llms.txt | 11 +- site/offline/index.html | 2 +- site/wechat/index.html | 2 +- site/whatsapp/index.html | 2 +- site/zh/20mb/index.html | 2 +- site/zh/email/index.html | 2 +- site/zh/ffmpeg-compress-to-size/index.html | 2 +- site/zh/index.html | 8 +- site/zh/offline/index.html | 2 +- site/zh/wechat/index.html | 2 +- skills/halfsize/SKILL.md | 5 +- src/cli.mjs | 1041 ++++++++++---------- src/encode.mjs | 38 + src/hdr.mjs | 32 + src/plan.mjs | 54 +- src/probe.mjs | 14 +- src/quality.mjs | 13 +- src/server.mjs | 18 +- test/cli.test.mjs | 8 +- test/e2e.test.mjs | 53 + test/plan.test.mjs | 57 +- test/server.test.mjs | 18 + web/index.html | 14 +- 32 files changed, 903 insertions(+), 570 deletions(-) create mode 100644 src/hdr.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c669294..aac1426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.1.9 + +- HDR sources (iPhone HLG, HDR10/PQ) are converted to SDR BT.709 inside the same + encode. Before, the output kept HDR tags on 8-bit pixels, so iPhone videos + looked washed out after compression +- Quality checks tone-map the reference frame too, so VMAF and SSIM measure + compression loss instead of the HDR-to-SDR transfer curve +- A target below the minimum bitrate for the video's duration now fails before + encoding and reports the smallest achievable size, instead of spending an + encode that cannot fit. `--force` still runs it +- When a two-pass encode lands more than 0.5% over target, halfsize re-encodes + once with a corrected bitrate instead of handing back an oversized file +- `--for email` now targets 18 MB, because base64 makes a 20 MB attachment a + 26.7 MB message that bounces at Gmail's 25 MB limit. New `--for outlook` + targets 15 MB for the 20 MB message cap + ## 0.1.8 Failure modes, mostly. The kind that leave you with a file you think is fine. diff --git a/README.md b/README.md index f4b576f..8a43880 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,10 @@

- New in 0.1.8: compare the original and the compressed video in the web app, - cancel a running encode, and no more half-written output files. - Release notes + New in 0.1.9: HDR clips (iPhone HLG, HDR10) are tone-mapped to SDR instead of + looking washed out, a target that cannot fit now fails before the encode with the real + minimum, and the email presets account for the base64 attachment tax. + Release notes

--- @@ -47,7 +48,7 @@ It computes the bitrate, picks the resolution that actually fits, encodes twice, the result with **SSIM** and **VMAF** so you know whether it still looks the same. ``` - halfsize 0.1.8 + halfsize 0.1.9 source.mp4 23.2 MB 8s 1920x1080 30.00 fps h264 @@ -171,7 +172,8 @@ compression plus a measured quality report, instead of guessing a bitrate. the encoder gets to spend its bits on fewer pixels. 4. **Encode twice.** Two-pass ABR with `libx264` or `libx265`. Pass one learns where the bits are needed; pass two spends them. Output is `yuv420p`, AAC audio, `+faststart`, and tagged - `hvc1` for H.265 so it plays on Apple devices. + `hvc1` for H.265 so it plays on Apple devices. HLG/PQ sources are tone-mapped to SDR BT.709 + in the same pass; without that, 8-bit pixels keep their HDR tags and look grey. 5. **Prove it.** Sample three points in the video, compute SSIM at each, and VMAF at the midpoint. Compare against thresholds and print a verdict in plain language. @@ -211,7 +213,7 @@ more quality at the same size if your player supports it. halfsize [options] --to Target size: 25mb, 700k, 1.5gb - --for Target cap for discord, whatsapp, wechat, email, twitter + --for Target cap for discord, whatsapp, wechat, gmail, outlook, email, twitter --crf Quality mode instead of size mode (18 best, 23 balanced, 28 small) --codec h264 (default, maximum compatibility) or h265 (about 30% smaller) --preset ultrafast .. veryslow (default: medium) @@ -219,6 +221,7 @@ halfsize [options] --no-audio Drop the audio track --no-verify Skip SSIM/VMAF --no-downscale Never reduce resolution (the target may then be missed) + --force Encode even when the target is below the minimum for this duration --json Machine-readable result --dry-run Print the plan, do not encode --web Start the local web app @@ -231,8 +234,9 @@ halfsize [options] Sizes accept `b`, `k`/`kb`, `m`/`mb`, `g`/`gb`, with or without a space. No unit means MB. `--for` uses the current free-tier caps (checked November 2026): `discord` 20 MB, -`whatsapp` 16 MB, `wechat` 20 MB, `email` 20 MB, `twitter` 512 MB. Caps change, so `--to` -always overrides. +`whatsapp` 16 MB, `wechat` 20 MB, `gmail`/`email` 18 MB, `outlook` 15 MB, `twitter` +512 MB. Email caps apply to the base64-encoded message, so an 18 MB file is the largest +that stays under Gmail's 25 MB. Caps change, so `--to` always overrides. Exit codes: `0` success, `1` error. `--json` returns the plan, the measured quality, and the target accuracy, which makes it easy to wrap in a script. @@ -324,7 +328,6 @@ more than half". The tool reports the real number either way. - Hardware encoding (NVENC / QSV / VideoToolbox) as an opt-in fast path - A folder watcher that compresses new files as they appear - Subtitle and multiple audio track passthrough -- HDR-to-SDR tonemapping, so iPhone HDR clips keep their colors - AV1 (`libsvtav1`) when the source is already AV1-friendly - A single-file binary, so Node.js is not required diff --git a/README.zh-CN.md b/README.zh-CN.md index 425ae2e..6c6bfa5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -26,8 +26,8 @@

- 0.1.8 新功能:网页版可直接对比原片和压缩后、随时取消编码,失败时不再留下半截文件。 - 更新说明 + 0.1.9 新功能:iPhone 拍的 HDR 视频会自动转成 SDR,压完不再发灰;压不到的目标在编码前就报出真实最小值;邮件预设按 base64 后的实际上限来算。 + 更新说明

--- @@ -45,7 +45,7 @@ npx halfsize 婚礼.mp4 --to 25mb 画质到底还在不在。 ``` - halfsize 0.1.8 + halfsize 0.1.9 source.mp4 23.2 MB 8s 1920x1080 30.00 fps h264 @@ -161,6 +161,7 @@ npx skills add YKLovePython/halfsize 让编码器把有限的比特花在更少的像素上,而不是硬撑 4K。 4. **压两遍。** `libx264` 或 `libx265` 两遍 ABR。第一遍找出哪里需要比特,第二遍把比特花在那里。 输出 `yuv420p`、AAC 音频、`+faststart`,H.265 额外打 `hvc1` 标签,保证苹果设备能播。 + HLG/PQ 片源会在同一次编码里转成 SDR BT.709;不转的话,8-bit 像素还带着 HDR 标签,播出来就发灰。 5. **证明。** 在视频里取三个采样点算 SSIM,在中间点算 VMAF,对照阈值给一句人话结论。 ``` @@ -197,7 +198,7 @@ SSIM 取三个采样点。实际数字会随内容变化:新闻口播会好很 halfsize [options] --to 目标大小:25mb、700k、1.5gb - --for 按平台上限:discord、whatsapp、wechat、email、twitter + --for 按平台上限:discord、whatsapp、wechat、gmail、outlook、email、twitter --crf 质量模式,不按大小(18 最好,23 均衡,28 更小) --codec h264(默认,兼容性最好)或 h265(大约再小 30%) --preset ultrafast .. veryslow(默认 medium) @@ -205,6 +206,7 @@ halfsize [options] --no-audio 去掉音轨 --no-verify 跳过 SSIM/VMAF 检测 --no-downscale 绝不降分辨率(可能会压不到目标) + --force 目标低于该时长的最小值时仍然编码 --json 机器可读结果 --dry-run 只打印方案,不编码 --web 启动本地网页版 @@ -217,7 +219,9 @@ halfsize [options] 大小支持 `b`、`k`/`kb`、`m`/`mb`、`g`/`gb`,中间可以有空格。不写单位按 MB 算。 `--for` 用的是当前免费档上限(2026 年 11 月核对):`discord` 20 MB、`whatsapp` 16 MB、 -`wechat` 20 MB、`email` 20 MB、`twitter` 512 MB。平台上限会变,写死不如随时用 `--to` 覆盖。 +`wechat` 20 MB、`gmail`/`email` 18 MB、`outlook` 15 MB、`twitter` 512 MB。邮件的上限算的是 +base64 编码后的整封邮件,所以 18 MB 的文件才是 Gmail 25 MB 限制下真正塞得进去的大小。 +平台上限会变,写死不如随时用 `--to` 覆盖。 退出码:`0` 成功,`1` 出错。`--json` 会返回方案、画质测量和目标误差,方便包进脚本。 @@ -310,7 +314,6 @@ npx halfsize *.mp4 --to 25mb - 硬件编码(NVENC / QSV / VideoToolbox)作为可选的快速路径 - 文件夹监听,新文件一出现就自动压 - 字幕和多音轨原样保留 -- HDR 转 SDR(让 iPhone 拍的 HDR 视频保住颜色) - 源已经是 AV1 时改用 AV1(`libsvtav1`) - 单文件二进制,不依赖 Node.js diff --git a/package.json b/package.json index 51c8b61..584b7d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "halfsize", - "version": "0.1.8", + "version": "0.1.9", "description": "Compress any video to an exact size (Discord or WeChat 20 MB, WhatsApp 16 MB) locally with FFmpeg and verify the quality with VMAF/SSIM.", "type": "module", "bin": { diff --git a/site/20mb/index.html b/site/20mb/index.html index 64a01dc..b2d4de3 100644 --- a/site/20mb/index.html +++ b/site/20mb/index.html @@ -65,7 +65,7 @@

Compress a video to exactly 20 MB

$ npx halfsize video.mp4 --to 20mb
 
-  halfsize 0.1.8
+  halfsize 0.1.9
   video.mp4
   136 MB  60s  1920x1080  30.00 fps  h264
 
diff --git a/site/discord/index.html b/site/discord/index.html
index 0b1bd06..961d028 100644
--- a/site/discord/index.html
+++ b/site/discord/index.html
@@ -71,7 +71,7 @@ 

Step by step

$ npx halfsize clip.mp4 --for discord
 
-  halfsize 0.1.8
+  halfsize 0.1.9
   clip.mp4
   136 MB  60s  1920x1080  30.00 fps  h264
 
diff --git a/site/email/index.html b/site/email/index.html
index 50983c3..6461571 100644
--- a/site/email/index.html
+++ b/site/email/index.html
@@ -81,7 +81,7 @@ 

One command

214 MB → 17.62 MB 12x smaller 2.1% under target VMAF 87.4 SSIM 0.968 slight softening
-

The --for email shortcut targets 20 MB, which is right for Outlook-class servers. For Gmail, --to 18mb is the safer number because of the base64 tax.

+

The --for email shortcut targets 18 MB, the Gmail-safe number after the base64 tax. --for outlook targets 15 MB for the 20 MB message cap. --to overrides either one.

What it does to the picture

Eighteen megabytes for a one-minute clip is about 2.2 Mbps. That is comfortable for 720p and cramped for 1080p, so the tool downscales instead of starving the larger frame. Measured on a 60-second 1080p30 source at 18 Mbps:

diff --git a/site/ffmpeg-compress-to-size/index.html b/site/ffmpeg-compress-to-size/index.html index e30671a..76cc8b2 100644 --- a/site/ffmpeg-compress-to-size/index.html +++ b/site/ffmpeg-compress-to-size/index.html @@ -104,7 +104,7 @@

What halfsize automates

  • Computes the budget, then picks the largest resolution that stays above a bits-per-pixel floor — 1080p when it fits, 720p or 540p when 1080p would look starved.
  • Runs both passes, writes to a temporary file, and only renames it when the encode succeeds.
  • Measures VMAF and SSIM, then prints a plain-language verdict instead of a number you have to interpret.
  • -
  • Adds --for discord, --for wechat, --for whatsapp, --for email and --for twitter so you do not have to look the cap up.
  • +
  • Adds --for discord, --for wechat, --for whatsapp, --for email, --for outlook and --for twitter so you do not have to look the cap up.
  • $ npx halfsize input.mp4 --to 20mb
    diff --git a/site/index.html b/site/index.html
    index e3a6367..e09cd6a 100644
    --- a/site/index.html
    +++ b/site/index.html
    @@ -35,7 +35,7 @@
       "codeRepository": "https://github.com/YKLovePython/halfsize",
       "license": "https://github.com/YKLovePython/halfsize/blob/main/LICENSE",
       "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
    -  "softwareVersion": "0.1.8"
    +  "softwareVersion": "0.1.9"
     }