Skip to content

Releases: ivanusto/unmark-web

v0.3.0 — upstream sync and a browser-side keyed-Gumbel detector

Choose a tag to compare

@ivanusto ivanusto released this 21 Aug 05:34

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+3164 and U+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 noncharacter and reserved_ignorable inspect 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 free box 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, the free payload 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.
  • inspectIsobmff runs 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 = ab0197b06263ad83a489032be69103e9a8725c72ddc3cf002a260c87b02647ac
  • service/scripts/image_meta.py = 78e5a67db243a4eac2a97b97cea036faae448bdaa2d77c59182af2c972657794
  • service/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

Choose a tag to compare

@ivanusto ivanusto released this 19 Aug 16:03

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 upstream score_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

Choose a tag to compare

@ivanusto ivanusto released this 19 Aug 05:21

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, Arabic Cf.
  • 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 detectionSoftware, Creator and parameters text-chunk values are matched against known AI generator product names, including inside zlib-compressed zTXt and iTXt chunks. 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.