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 to service/scripts/ and runs behind server.py, a stdlib HTTP entrypoint (/health, /inspect, /clean, /capabilities)
HTTP service: server.py exposes the cleaning pipeline over JSON/base64 with hardening that mirrors the CLIs — size caps, binary guard, atomic writes, loopback-only bind by default, and optional WATERMARKS_SERVER_API_KEY bearer auth
OpenAPI: GET /openapi.json serves 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 with openapi-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.yaml brings up the whole infra — core always, markllm / markdiffusion behind profile: harness, ctrlregen / synthid behind profile: heavy as local-only builds. Services are prefixed wr-, one-shot CLIs exit cleanly under compose up, and make compose-check validates the running stack
GHCR publishing: .github/workflows/release-images.yml publishes core, markllm, and markdiffusion images automatically on every v* tag. ctrlregen / synthid are never published (upstream licensing)
Env configuration: .env.example + service configuration guide; docker compose auto-loads .env; .env is gitignored deny-by-default so your keys stay local
Repo hygiene: .gitignore and service/.dockerignore are now deny-by-default — only explicitly allowed paths can be committed or sent in a build context (image contexts only ship service/scripts/, which is all the Dockerfiles COPY)
Tests: tests/test_http_server.py (13 cases) for the HTTP service; all suites re-pointed at service/scripts/
MarkDiffusion image-watermark harness (optional)
New optional harness wrapping the external THU-BPM/MarkDiffusion toolkit (Apache-2.0): markdiffusion_harness.py with watermark / detect / purify subcommands for nine image schemes (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL)
clean_image.py --remove-pixel diffusion runs the MarkDiffusion DiffusionPurification regeneration 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.sh bootstrap (PyPI pin 1.0.2, --checkout for an editable clone at a pinned commit) + Dockerfile.markdiffusion + Makefile targets
Mock-based tests (no torch in CI) and a references/markdiffusion.md doc
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/MarkLLM checkout (Apache-2.0): detect_text_watermark.py with detect / watermark subcommands for KGW and SynthID schemes
rewrite_text.py --markllm-scheme runs before/after detection around a Layer B rewrite (env-gated; reports cleared) — so you can prove a mark clears under your rewrite
Hardening: --offline cache-only model loading (zero HF egress, no remote code), 1 MiB config cap, optional WATERMARKS_MARKLLM_RLIMIT_AS on 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.py now sends reasoning_effort: "none" by default for openai-compatible backends (--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORT). Reasoning models like deepseek-v4-flash otherwise burn ~100s of chain-of-thought on a one-line rewrite (9,894 vs 12 completion tokens!)
Fix markllm image build: requirements-markllm.txt pinned tokenizers==0.23.1, which conflicts with transformers==5.15.0 (caps tokenizers<=0.23.0; no 0.23.0 release exists) — now pinned tokenizers==0.22.2; torch moved to the CPU wheel index so the image is CPU-only like Dockerfile.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.markllm and Dockerfile.markdiffusion never copied common.py into /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-text overrides (#24)
--json no longer suppresses the residual-signal exit code (#30)
inspect_file prints 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. 🎉
v0.4.0 adds optional CtrlRegen pixel removal via an external backend, a finding-confidence taxonomy with aggregate audits, and a batch of Windows and false-positive fixes.
Optional pixel-domain watermark removal via an external mertizci/noai-watermark checkout: clean_ctrlregen.py adapter + setup_ctrlregen.sh bootstrap (pinned commit, sparse checkout, venv, SHA verification), plus Dockerfile.ctrlregen and make bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregen
clean_image.py --remove-pixel ctrlregen runs metadata strip → CtrlRegen removal → optional reverse-SynthID before/after score; inspect_image.py hints at the flag on a high SynthID score
Conservative default strength 0.25 (presets 0.15/0.25/0.35/0.5/0.7); the 512×512-native pipeline is auto-tiled by the backend for larger images; the torch subprocess gets higher env-overridable resource caps
Backend is never bundled: noai-watermark ships no LICENSE file (treated as all-rights-reserved), and its auto-install/restart code paths are bypassed by using CtrlRegenEngine directly
Finding confidence and aggregate audits
Findings are now classified confirmed / probable / informational / likely_false_positive, exposed in text/image/container JSON and human reports
New audit_dir.py (recursive tree) and audit_website.py (sitemap discovery + crawl) aggregate reports; documented in SKILL.md
False-positive fixes
DOCX: scan only docProps/customXml, not the visible body (#14)
Text Layer A: preserve emoji VS16/ZWJ after an emoji base; new --strip-emoji-glue paranoid flag (#22)
HTML: treat CMS generator tags as informational, not AI metadata (#13)
PDF: exclude stream payloads from the AI-marker byte scan (#13)
rewrite_text.py default paraphrase now performs an explicit word-choice + syntax attack (clause order, connectors, transition words, sentence boundaries, function words) rather than a generic rewrite
New --strength humanize: zero-shot "write like a human" pass targeting formulaic AI-style phrasing
New --strength code: rewrites comments, docstrings, and string literals, and renames local identifiers while preserving behavior and public API names
Structural pass now emits "natural, varied human prose" instead of AI-typical "clear professional style"
New --temperature (default 0.9) for both Ollama and OpenAI-compatible backends
New --candidates N: generates N rewrites and selects the most lexically diverged (bigram Jaccard distance) with a length-drift guard
Stronger model hygiene: prefer local open-weight models and avoid any known-watermarked vendor, not just the suspected origin
Residual-risk reporting now distinguishes short/highly predictable text (lower risk) from long, high-entropy prose (higher risk)
Docs updated in SKILL.md, removal-matrix.md, and vendor-notes.md; tests cover new prompts, divergence scoring, and candidate selection
inspect_image.py / clean_image.py surface the score via REVERSE_SYNTHID_DIR or --synthid-dir
setup_synthid.sh bootstraps the checkout (scorer-only dependencies; --full installs upstream requirements)
Dockerfile.synthid plus make docker-synthid-build / docker-synthid-help
New Makefile targets smoke-synthid and bootstrap-synthid
Tests for the scorer adapter, CLI unavailable path, JSON parsing, and runtime errors
Notes
The upstream scorer is not bundled; it loads from a local checkout at runtime and remains under its non-commercial Research License. Pixel watermark removal and C2PA soft binding remain out of scope.
Bug-fix release: has_manifest no longer reports a clean asset as carrying a C2PA manifest.
Fixed
image_meta.py: parenthesised the positive has_manifest substring matches so the negative markers (no claim, no jumbf) veto all of them. c2patool reports a missing manifest as Error: No claim found, which contains the substring claim and previously matched the first positive alternative — flagging every clean PNG, JPEG, PDF, SVG and DOCX as carrying C2PA. See #3.
Added tests/test_c2patool_report.py — 4 cases: No claim found, No JUMBF data found, a genuine manifest, and c2patool absent.
Docs
Fixed broken c2patool links (repo moved to contentauth/c2pa-rs).
Added a disclaimer on the quality cost of text-watermark removal.
Minor release after v0.0.1: packaging polish and clearer honesty around the industry two-layer provenance model (C2PA + soft binding / SynthID-class media).
What's new
Packaging
Makefile with test, smoke, and install-skill
pytest.ini
Fixture samples: Markdown, HTML, SVG
PDF degraded-clean test path
Docs / honesty
Two-layer model: hard-bound C2PA (in scope) vs soft binding / pixel-audio-video watermarks (out of scope)
README Residual risk after a clean table + external verify links (Content Credentials, SynthID)
First public release: agent skill + stdlib Python scripts to strip multi-vendor AI provenance marks from text and files (privacy / hygiene on content you own).