v0.5.0 is the biggest release yet: watermarks-remover is now a full service you can self-host — a stdlib HTTP API, Docker images on GHCR, a one-command compose stack, and two new optional research harnesses (MarkDiffusion for images, MarkLLM for text) for verifying that your pipeline actually clears a mark.
The skill is now a thin, code-free client that drives the machinery over HTTP, so your agent host doesn't even need Python. Everything from v0.4.0 keeps working — the CLIs are all still there and remain the primary interface for local use.
Service & Docker distribution
- Skill/service split: the skill (
skills/remove-ai-marks/) is now a code-free remote client over HTTP. All implementation moved toservice/scripts/and runs behindserver.py, a stdlib HTTP entrypoint (/health,/inspect,/clean,/capabilities) - HTTP service:
server.pyexposes the cleaning pipeline over JSON/base64 with hardening that mirrors the CLIs — size caps, binary guard, atomic writes, loopback-only bind by default, and optionalWATERMARKS_SERVER_API_KEYbearer auth - OpenAPI:
GET /openapi.jsonserves a dynamically generated OpenAPI 3.0.3 spec, built from the route table + live config so it never drifts from the real endpoints; CI validates it withopenapi-spec-validator - Core Docker image: full cleaning service with exiftool / qpdf / c2patool preinstalled. Any CLI stays runnable by overriding the command —
docker run watermarks-remover /app/scripts/clean_file.py ...just works - Docker / compose:
compose.yamlbrings up the whole infra —corealways,markllm/markdiffusionbehindprofile: harness,ctrlregen/synthidbehindprofile: heavyas local-only builds. Services are prefixedwr-, one-shot CLIs exit cleanly undercompose up, andmake compose-checkvalidates the running stack - GHCR publishing:
.github/workflows/release-images.ymlpublishescore,markllm, andmarkdiffusionimages automatically on everyv*tag.ctrlregen/synthidare never published (upstream licensing) - Env configuration:
.env.example+ service configuration guide;docker composeauto-loads.env;.envis gitignored deny-by-default so your keys stay local - Repo hygiene:
.gitignoreandservice/.dockerignoreare now deny-by-default — only explicitly allowed paths can be committed or sent in a build context (image contexts only shipservice/scripts/, which is all the Dockerfiles COPY) - Tests:
tests/test_http_server.py(13 cases) for the HTTP service; all suites re-pointed atservice/scripts/
MarkDiffusion image-watermark harness (optional)
- New optional harness wrapping the external
THU-BPM/MarkDiffusiontoolkit (Apache-2.0):markdiffusion_harness.pywithwatermark/detect/purifysubcommands for nine image schemes (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL) clean_image.py --remove-pixel diffusionruns the MarkDiffusionDiffusionPurificationregeneration attack as an alternative pixel-removal engine (conservative strength 0.3 default — it's blind regeneration, so it drifts content more than CtrlRegen)setup_markdiffusion.shbootstrap (PyPI pin1.0.2,--checkoutfor an editable clone at a pinned commit) +Dockerfile.markdiffusion+ Makefile targets- Mock-based tests (no torch in CI) and a
references/markdiffusion.mddoc - Caveats documented honestly: same-scheme-only verification (not a vendor-detector oracle) and the blind-regeneration drift trade-off
MarkLLM text-watermark harness (optional)
- New optional harness wrapping the external
THU-BPM/MarkLLMcheckout (Apache-2.0):detect_text_watermark.pywithdetect/watermarksubcommands for KGW and SynthID schemes rewrite_text.py --markllm-schemeruns before/after detection around a Layer B rewrite (env-gated; reportscleared) — so you can prove a mark clears under your rewritesetup_markllm.shbootstrap +Dockerfile.markllm+ Makefile targets- Hardening:
--offlinecache-only model loading (zero HF egress, no remote code), 1 MiB config cap, optionalWATERMARKS_MARKLLM_RLIMIT_ASon the rewrite subprocess, pinned torch, and clone-SHA verification in the Dockerfile - Mock-based tests (21 cases) — no torch in CI
Fixes and polish
- Layer B:
rewrite_text.pynow sendsreasoning_effort: "none"by default foropenai-compatiblebackends (--reasoning-effort/WATERMARKS_REWRITE_REASONING_EFFORT). Reasoning models likedeepseek-v4-flashotherwise burn ~100s of chain-of-thought on a one-line rewrite (9,894 vs 12 completion tokens!) - Fix markllm image build:
requirements-markllm.txtpinnedtokenizers==0.23.1, which conflicts withtransformers==5.15.0(capstokenizers<=0.23.0; no 0.23.0 release exists) — now pinnedtokenizers==0.22.2; torch moved to the CPU wheel index so the image is CPU-only likeDockerfile.markdiffusion - Fix ctrlregen image build: the 2023-era research pins ship no Python 3.14 wheels, so the base image is now
python:3.11-slim(digest-pinned, multi-arch) - Fix harness images at runtime:
Dockerfile.markllmandDockerfile.markdiffusionnever copiedcommon.pyinto/app(pre-existing bug) — added - WebP: stdlib-only inspection and metadata cleaning for RIFF
C2PA, XMP, EXIF, and ICC profile chunks (#37) - Filename sanitization: the HTTP service refuses unsafe client-supplied output names
- Markdown frontmatter: cleaner no longer crashes on or leaks nested AI keys (#25)
- Text tools refuse binary input;
--force-textoverrides (#24) --jsonno longer suppresses the residual-signal exit code (#30)inspect_fileprints the filename in its output (#50)- Mixed-case CMS generator meta tags preserved (#42)
- Layer A: preserve load-bearing script invisibles, strip PUA (#38, #52); preserve script joiners, flag emoji and Arabic Cf marks (#28)
- Website audit hardened against SSRF and gzip bombs (#49)
- SECURITY.md now only references the private advisories channel (#51)
- Windows: PowerShell ports of the setup bootstraps (#40)
- Docs: stars/forks shields, MarkLLM reference, pull request template, Docker CLI + API deployment plan
A big thanks to everyone who filed issues, reviewed PRs, and ran the harnesses — this release is a team effort. 🎉
Full changelog: README