Releases: ivanusto/unmark-web
Release list
v0.3.0 — upstream sync and a browser-side keyed-Gumbel detector
Two things: the ports caught up with upstream guillaumemeyer/watermarks-remover, and the Inspector gained its first statistical detector that actually works on this hosted page.
Keyed-Gumbel (EXP), in the browser
js/gumbel.js, a port of upstream's detect_gumbel.py. No sidecar, no model weights, no network: the whole test is HMAC arithmetic over the text, so it reaches a verdict here on GitHub Pages where every other statistical row can only say unavailable. Open Keyed-Gumbel (EXP) key on the Inspector tab and paste the key the text was generated with.
SHA-256 and HMAC-SHA256 are implemented in the module rather than taken from crypto.subtle, which is async, needs a secure context, and would cost roughly two thousand importKey + sign round trips on a 1000-word input. Token ids are BigInt and the uniform is computed as Number(id) + 0.5 before the division, matching how CPython evaluates the same expression; that ordering is what makes the statistic reproduce bit for bit.
Read the result the way upstream intends it. This is a same-key replay, valid only against the key, tokenizer and PRF layout used at generation, so a clean verdict means not this key and never no watermark. The PRF layout is a clean-room instantiation, auditable but not bit-compatible with any particular engine's kernel. The key is held in the page for the length of the run and is never stored, transmitted, or written into the JSON report.
Layer A hardening (upstream #133)
U+180F(Mongolian FVS4, assigned in Unicode 14),U+3164andU+FFA0(Hangul fillers) join the strip set and their script-glue sets: kept next to their own script, stripped when they float.- Unicode noncharacters and reserved default-ignorable code points become strip-class, with
noncharacterandreserved_ignorableinspect kinds. - Visible-layout format controls are kept next to their own script: Egyptian hieroglyph quadrat, Duployan shorthand, musical beaming.
Image containers (upstream #176, #182, #183)
- A dropped ISOBMFF box is overwritten with an equal-size
freebox instead of being spliced out, so absolute media offsets stay valid. A cleaned AVIF or HEIC now keeps its original length; the metadata is still gone, thefreepayload is zeroed. - A truncated PNG chunk or ISOBMFF box keeps its tail, copied through verbatim and reported as an action, instead of being dropped while the run claims the file was already clean.
inspectIsobmffruns the whole-file C2PA byte scan even when no box parses, which is exactly when it matters most.
Upstream #156 is deliberately not mirrored: it hardens error handling around the external c2patool subprocess, and a browser has neither. Recorded in scripts/upstream-sources.json.
Parity anchors
service/scripts/text_unicode.py=ab0197b06263ad83a489032be69103e9a8725c72ddc3cf002a260c87b02647acservice/scripts/image_meta.py=78e5a67db243a4eac2a97b97cea036faae448bdaa2d77c59182af2c972657794service/scripts/score_stylometry.py=cd3dae134d5f641120218fd525afd523d4a0bfb52d4d73c0e92dc04e3bcc6d59(unchanged)service/scripts/detect_gumbel.py=f908272084cd6783a2daa015d51cd1348f56e4bb45fe69168bd85f57b62cd4c1(new)
476 parity tests against upstream main. New in this release: 83 keyed-Gumbel cases, including one that forges a winner sequence and asserts it lights up under the generation key and falls back to chance under another, and an i18n test that every locale carries every key and every data-i18n attribute resolves.
This repository still does not contain the upstream server; it is a client plus an optional local sidecar.
v0.2.0 — Watermark Inspector
Watermark Inspector
A third tab that runs every detector on one input and reports each separately, grouped into character, metadata and statistical layers, all through one result contract (js/detectors.js, exported verbatim by Copy JSON report):
status ∈ detected | clean | uncertain | unavailable | not_tested | not_applicable | error, plus confidence / score / threshold / evidence[] / note / requires_key / requires_model / local / heuristic / meta.
- Detector and cleaner are separate. Clean (Layer A) & re-inspect shows every detector before/after and says whether the statistical verdicts moved.
- A heuristic never says "detected". Stylometry (new
js/stylometry.js, port of upstreamscore_stylometry.py, parity-tested) is capped at uncertain. - "Unavailable" is not "clean". On the hosted page the statistical rows report unavailable and the Overall line says statistical watermarks were not tested.
Statistical sidecar (optional, local only)
sidecar/unmark_stat.py — Kirchenbauer/KGW and SynthID-Text detection with the reference transformers detectors, public experiment keys and the MIT Bayesian detector bundles from xlr8harder/synthid for Qwen/Qwen3-4B-Instruct-2507, plus demo generation with a chosen key. serve_local.py --stat-upstream … proxies it same-origin (/stat/*, key server-side, browser Authorization never forwarded). Off by default; see sidecar/README.md.
Parity anchors
service/scripts/text_unicode.py=cd21c5700184a6bb4a43089f546531a2692bf140413954ea9b93f4a67f1f3a5e(unchanged)service/scripts/image_meta.py=422dbbea5eff172b78a6f38ade753d11857d62ff4811b97084000062c5622e4a(unchanged)service/scripts/score_stylometry.py=cd3dae134d5f641120218fd525afd523d4a0bfb52d4d73c0e92dc04e3bcc6d59(new)
This repository still does not contain the upstream server; it is a client plus an optional local sidecar.
v0.1.0
First tagged release. Unmark is a browser-first web client for guillaumemeyer/watermarks-remover — independent, not affiliated with the upstream project.
What this release pins
The whole point of a tag here is the parity anchor. The JavaScript engines are ports of two upstream Python modules, and a test suite asserts identical output. This release was verified against these exact upstream files:
| upstream file | sha256 | last changed by |
|---|---|---|
service/scripts/text_unicode.py |
cd21c5700184a6bb4a43089f546531a2692bf140413954ea9b93f4a67f1f3a5e |
c1d1e5a (#96) |
service/scripts/image_meta.py |
422dbbea5eff172b78a6f38ade753d11857d62ff4811b97084000062c5622e4a |
360c8e8 (#127) |
274 parity assertions pass against upstream main: same characters kept and stripped, same bytes out of the image parsers, same findings strings. scripts/check-upstream.mjs exits 0 against those hashes, and a scheduled workflow re-checks daily and files an issue when either signal moves.
What is in the box
- Text (Layer A) — invisible Unicode: zero-width and bidi controls, variation selectors, tag characters, PUA, other
Cf; space homoglyphs; optional NFKC and Latin confusables. Load-bearing invisibles are preserved exactly as upstream does — emoji ZWJ/VS16, Persian and Indic ZWNJ, flag tags, Mongolian FVS, Khmer vowels, Hangul fillers, ArabicCf. - Image metadata — PNG, JPEG, WebP, AVIF, HEIC, BMP, GIF and TIFF (classic and BigTIFF). C2PA, EXIF, XMP, text chunks. Byte-level container surgery; pixels are never re-encoded.
- PNG generator detection —
Software,Creatorandparameterstext-chunk values are matched against known AI generator product names, including inside zlib-compressedzTXtandiTXtchunks. Deliberately key-scoped: "Gemini", "Sora" and "Firefly" in free text are not evidence of anything. - Three locales — English, 繁體中文, 简体中文. Light and dark. Keyboard accessible.
- Optional AI rewrite — see below.
Two things to be clear about
This repository does not contain a server. server.py belongs to upstream. What ships here is the web UI plus serve_local.py, a loopback-only static server and reverse proxy. To use the server-backed formats (PDF, DOCX, ODT, EPUB, full container cleaning) you run upstream's service and point this UI at it.
The AI rewrite is local-only and off by default. It appears only when you run serve_local.py --llm-upstream … against an OpenAI-compatible endpoint of your own; the key stays server-side and never enters the browser. It cannot work on the hosted GitHub Pages build, because browsers block an HTTPS page from calling a plain-HTTP local endpoint. Cleaning itself is unaffected and still never leaves the page.
Install
There is nothing to build and nothing to install. Clone the repository, or open https://ivanusto.github.io/unmark-web/. No package.json, no runtime dependencies; serve_local.py is Python stdlib only. The source archives below are generated by GitHub from the tag.
MIT. The character tables, decision rules and container parsers are derived from watermarks-remover, © watermarks-remover contributors, MIT — see NOTICE.