[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-95067":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":15,"stars7d":17,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":18,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":23,"hasPages":21,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":43,"readmeContent":44,"aiSummary":45,"trendingCount":16,"starSnapshotCount":16,"syncStatus":46,"lastSyncTime":47,"discoverSource":48},95067,"watermarks-remover","Leutenegger\u002Fwatermarks-remover","Leutenegger","Remove multi-vendor AI provenance traces: Unicode text sanitization, statistical rewriting techniques, and C2PA\u002Fmetadata stripping from PNG\u002FJPEG\u002FSVG\u002FPDF\u002FDOCX\u002FHTML\u002FMD files","",null,"Python",936,95,336,4,0,17,25,68.15,"MIT License",false,"main",true,[25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"claude","claude-code","claude-skills","codex","codex-cli","codex-desktop","codex-plugin","codex-skill","codex-skills","grok","grok-45","grok-ai","grok-build","grok-cli","watermark","watermark-detection","watermark-tools","watermarks","2026-08-24 04:01:23","```\n_ _ _ ____ ___ ____ ____ _  _ ____ ____ _  _ ____    ____ ____ _  _ ____ _  _ ____ ____\n| | | |__|  |  |___ |__\u002F |\\\u002F| |__| |__\u002F |_\u002F  [__  __ |__\u002F |___ |\\\u002F| |  | |  | |___ |__\u002F\n|_|_| |  |  |  |___ |  \\ |  | |  | |  \\ | \\_ ___]    |  \\ |___ |  | |__|  \\\u002F  |___ |  \\\n```\n\n# watermarks-remover\n\n\u003C!-- logo: figlet -d .figlet -f cybermedium -w 120 \"watermarks-remover\" -->\n\n[![CI](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Factions\u002Fworkflows\u002Fci.yml)\n[![Release](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fv\u002Frelease\u002Fguillaumemeyer\u002Fwatermarks-remover)](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases)\n[![Stars](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fstars\u002Fguillaumemeyer\u002Fwatermarks-remover)](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Fstargazers)\n[![Forks](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fforks\u002Fguillaumemeyer\u002Fwatermarks-remover)](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Fforks)\n\nAgent skill + stdlib Python service to strip **multi-vendor AI provenance marks** from text and files — for privacy and hygiene on content **you own**. The skill is a thin client: it drives the machinery over HTTP, so the agent host needs no Python.\n\n| Layer | Target | How |\n| --- | --- | --- |\n| **A** | Invisible Unicode, exotic spaces, bidi, tag chars | Deterministic Python scripts |\n| **B** | Statistical (token-sampling) text watermarks | Agent rewrite + optional `rewrite_text.py` hook |\n| **Files** | C2PA \u002F EXIF \u002F XMP \u002F doc props | PNG, JPEG, WebP, AVIF, HEIC, BMP, GIF, TIFF, SVG, PDF, DOCX, XLSX, PPTX, EPUB, ODT, HTML, Markdown, MP4\u002FMOV\u002FM4A\u002FM4V, WAV, MP3 |\n\nVendors \u002F ecosystems (class-level): **Claude**, **Gemini \u002F SynthID-Text**, **OpenAI** provenance surfaces, **open-LLM** Kirchenbauer-style marks.\n\n**Latest release:** [v0.5.0](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.5.0)\n\nSkill path: [`skills\u002Fremove-ai-marks\u002F`](skills\u002Fremove-ai-marks\u002F)  \nService path: [`service\u002F`](service\u002F)  \n(migration: formerly `remove-claude-marks`; slash alias `\u002Fremove-claude-marks` still documented)\n\n## Install (agent skill)\n\nThe skill ships **no code** — it calls the service over HTTP. Install the skill (markdown only) and start the service, then set `WATERMARKS_SERVICE_URL` if it is not `http:\u002F\u002F127.0.0.1:8765`.\n\n```bash\n# Grok Build \u002F project-local\nmkdir -p .grok\u002Fskills\nln -sfn \"$(pwd)\u002Fskills\u002Fremove-ai-marks\" .grok\u002Fskills\u002Fremove-ai-marks\n\n# User-global Grok\nmkdir -p ~\u002F.grok\u002Fskills\nln -sfn \"$(pwd)\u002Fskills\u002Fremove-ai-marks\" ~\u002F.grok\u002Fskills\u002Fremove-ai-marks\n```\n\nInvoke with `\u002Fremove-ai-marks` or ask to “strip AI watermarks \u002F C2PA \u002F Claude marks \u002F SynthID-class text.”\n\n### Optional Cursor text-only skill\n\n[`skills\u002Fclean-user-facing-text\u002F`](skills\u002Fclean-user-facing-text\u002F) is a\nself-contained Cursor skill for authorized manuscripts, documentation, and web\ncopy. It excludes image, C2PA, service, and external-model tooling.\n\nInstall it into `~\u002F.cursor\u002Fskills\u002Fclean-user-facing-text`:\n\n```bash\npython3 install_skill.py\n```\n\nOn Windows, use `py install_skill.py`. The `install-skill.sh` wrapper is\nprovided for macOS\u002FLinux shells. Existing installations are preserved unless\nyou pass `--force`; replacement is staged first and the previous install is\nkept as a uniquely named backup.\n\nSkill invocation is model-selected. Projects that explicitly adopt this\nworkflow can also copy the optional rule:\n\n```bash\nmkdir -p \u002Fpath\u002Fto\u002Fproject\u002F.cursor\u002Frules\ncp integrations\u002Fcursor\u002Fclean-user-facing-text.mdc \\\n  \u002Fpath\u002Fto\u002Fproject\u002F.cursor\u002Frules\u002Fclean-user-facing-text.mdc\n```\n\nFor all projects, put the same instruction in Cursor **User Rules** instead.\nRules improve consistency but remain model instructions; Cursor does not expose\na deterministic pre-send filter for final chat responses.\n\n### Start the service\n\nThe fastest path is a local HTTP server (Python 3.10+ stdlib only — no deps, no Docker):\n\n```bash\nmake serve                 # http:\u002F\u002F127.0.0.1:8765\n# or directly:\npython3 service\u002Fscripts\u002Fserver.py --host 127.0.0.1 --port 8765\n```\n\n### Windows (no Docker)\n\nSee [docs\u002Fwindows-autostart.md](docs\u002Fwindows-autostart.md) for auto-starting the service at Windows login without Docker.\n\nFor the whole infra (core + optional harness\u002Fheavy backends), see [Docker \u002F compose](#docker--compose) below.\n\nOptional system tools (auto-used when present — preinstalled in the core Docker image):\n\n| Tool | Role |\n| --- | --- |\n| [`c2patool`](https:\u002F\u002Fgithub.com\u002Fcontentauth\u002Fc2pa-rs\u002Ftree\u002Fmain\u002Fcli) | Inspect C2PA manifests |\n| [`exiftool`](https:\u002F\u002Fexiftool.org\u002F) | Residual metadata strip (esp. **PDF**) |\n| [`qpdf`](https:\u002F\u002Fqpdf.sourceforge.io\u002F) | Structural PDF rebuild — **required** for a real PDF strip (see below) |\n\nCore scripts need **Python 3.10+** stdlib only. Layer B model calls are optional.\n\n## Quick use (scripts)\n\n```bash\nSCRIPTS=service\u002Fscripts\n\n# Unified inspect \u002F clean\npython3 \"$SCRIPTS\u002Finspect_file.py\" draft.md\npython3 \"$SCRIPTS\u002Fclean_file.py\" draft.md -o draft.cleaned.md\npython3 \"$SCRIPTS\u002Fclean_file.py\" photo.png -o photo.cleaned.png\npython3 \"$SCRIPTS\u002Fclean_file.py\" notes.docx -o notes.cleaned.docx\n\n# Text Layer A\npython3 \"$SCRIPTS\u002Finspect_text.py\" draft.md\npython3 \"$SCRIPTS\u002Fclean_text.py\" draft.md -o draft.cleaned.md --stats\n\n# Layer B rewrite hook (default: print prompt only — no model required)\npython3 \"$SCRIPTS\u002Frewrite_text.py\" draft.md --backend print-prompt --strength paraphrase\n# Optional local Ollama (loopback only by default — remote endpoints require\n# WATERMARKS_REWRITE_ALLOW_REMOTE=1 or --allow-remote):\n# WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2 \\\n#   python3 \"$SCRIPTS\u002Frewrite_text.py\" draft.md -o draft.rewritten.md\n# API keys are read from WATERMARKS_REWRITE_API_KEY only (never argv).\n\n# Images\npython3 \"$SCRIPTS\u002Finspect_image.py\" shot.png\npython3 \"$SCRIPTS\u002Fclean_image.py\" shot.png -o shot.cleaned.png\n```\n\n### Text tools refuse binary input\n\n`inspect_text.py`, `clean_text.py` and `rewrite_text.py` operate on text. Pointed\nat a `.docx`, `.pdf` or image they used to decode the compressed bytes and report\nwhatever codepoints fell out — noise that tracks the compression, not the\ncontent — and `clean_text.py` then wrote those mangled bytes back, destroying the\nfile. They now refuse binary input and name the tool that handles it:\n\n```bash\npython3 \"$SCRIPTS\u002Finspect_text.py\" report.docx\n# refusing to treat report.docx as text: it looks like a ZIP container (DOCX, ODT, …).\n# Use inspect_file.py \u002F clean_file.py, which route by format,\n# or pass --force-text to scan the raw bytes anyway.\n```\n\nDetection is by magic number plus a control-byte ratio, so text in encodings\nother than UTF-8 keeps working. `--force-text` overrides it everywhere.\n\n### Unrecognized formats are never auto-cleaned\n\n`classify()` labels bytes that match no supported text, image or container\nformat as **`unknown`** — it no longer falls back to \"text\". In auto mode\n`clean_file.py` refuses such files (exit 2, no output written) instead of\ndecoding them as UTF-8 and writing back mangled bytes; `--as text` or\n`--force-text` are the explicit opt-ins. `inspect_file.py` reports the file\nas `unknown` (exit 0), and the HTTP service answers `\u002Finspect` with\n`kind: \"unknown\"` but rejects `\u002Fclean` of unknown formats (400 — send a\nfilename with a known extension, e.g. `notes.txt`).\n\n## HTTP service\n\nThe same machinery runs as a stdlib HTTP service (`service\u002Fscripts\u002Fserver.py`) — the interface the skill uses and the way any web app can integrate without vendoring:\n\n| Method | Path | Body | Returns |\n| --- | --- | --- | --- |\n| GET | `\u002Fhealth` | — | `{\"ok\": true, \"version\": ...}` |\n| GET | `\u002Fcapabilities` | — | optional tools \u002F backends present |\n| GET | `\u002Fopenapi.json` | — | dynamically generated OpenAPI 3.0.3 spec |\n| POST | `\u002Finspect` | `{\"file\": \"\u003Cbase64>\", \"name\": \"notes.md\"}` | `{\"ok\", \"kind\", \"suspicious\", \"report\"}` |\n| POST | `\u002Fdetect` | `{\"file\": \"\u003Cbase64>\", \"name\": \"notes.txt\"}` | `{\"ok\", \"kind\", \"detections\": [...]}` |\n| POST | `\u002Fclean` | `{\"file\": \"\u003Cbase64>\", \"name\": \"notes.md\", \"options\": {...}}` | `{\"ok\", \"kind\", \"cleaned\": \"\u003Cbase64>\", \"report\"}` |\n| POST | `\u002Finspect\u002Fbatch` | `{\"files\": [{\"file\": \"\u003Cbase64>\", \"name\": \"notes.md\"}, ...]}` | `{\"ok\", \"results\": [{\"name\", \"ok\", \"kind\", \"suspicious\", \"report\"}, ...]}` |\n| POST | `\u002Fclean\u002Fbatch` | `{\"files\": [{\"file\": \"\u003Cbase64>\", \"name\": \"notes.md\", \"options\": {...}}, ...]}` | `{\"ok\", \"results\": [{\"name\", \"ok\", \"kind\", \"cleaned\": \"\u003Cbase64>\", \"report\"}, ...]}` |\n\nBatch endpoints loop the same per-file pipeline as `\u002Finspect` and `\u002Fclean`, capped at `WATERMARKS_MAX_BATCH_FILES` files per request (default 50). A malformed entry (bad base64, unknown option, unrecognized format) surfaces as that entry's `\"ok\": false` with an `\"error\"` string — it never aborts the rest of the batch.\n\n```bash\nWM=\"http:\u002F\u002F127.0.0.1:8765\"\ncurl -s \"$WM\u002Fhealth\"                       # {\"ok\": true, \"version\": \"...\"}\ncurl -s \"$WM\u002Fopenapi.json\"                 # machine-readable OpenAPI 3.0.3 contract\ncurl -s -X POST \"$WM\u002Fclean\" -H 'Content-Type: application\u002Fjson' \\\n  -d \"{\\\"file\\\": \\\"$(base64 \u003C notes.md | tr -d '\\n')\\\", \\\"name\\\": \\\"notes.md\\\"}\"\n```\n\nThe service routes by filename extension then magic bytes, so text \u002F image \u002F container are auto-detected. Set `WATERMARKS_SERVER_API_KEY` to require `Authorization: Bearer \u003Ckey>` on every request. Loopback-only bind by default (`--host` to override); intended for a trusted network.\n\n### Watermark detection (`\u002Fdetect` and `detect_before` \u002F `detect_after`)\n\nDetection is a separate step from cleaning — the service never calls vendor\nAPIs unless you ask it to:\n\n- **`POST \u002Fdetect`** runs the configured watermark detectors on a file.\n  Text → vendor detectors + stylometry; image → SynthID pixel score.\n- **`\u002Finspect`** accepts an opt-in `\"detect\": true` flag that appends\n  detector results to the text report (and can flip `suspicious`).\n- **`\u002Fclean`** accepts `\"detect_before\"` \u002F `\"detect_after\"` options to\n  score the input and the cleaned output, so you can measure what a clean\n  actually changed.\n\nText detectors (see `\u002Fcapabilities` → `text_detectors`):\n\n| Detector | Activated by | Notes |\n| --- | --- | --- |\n| `markllm` | `MARKLLM_DIR` (host checkout) | Research harness (KGW \u002F SynthID schemes), same-config-only — not a vendor oracle. |\n| `claude-text` | — (placeholder) | Anthropic has announced a watermark detection API; this seam activates when it ships. |\n\nImage scoring: when `WATERMARKS_SYNTHID_SCORER_URL` is set, the service\nscores images through the `wr-synthid-score` sidecar (heavy profile); with a\nlocal `REVERSE_SYNTHID_DIR` it uses the checkout directly. Detection is\nfail-soft: unconfigured, timed-out, or errored detectors report\n`{\"available\": false, \"error\": ...}` and never block cleaning.\n\n## Docker \u002F compose\n\nPublished images (GHCR):\n\n| Image tag | Contents | Published? |\n| --- | --- | --- |\n| `ghcr.io\u002Fguillaumemeyer\u002Fwatermarks-remover:\u003Ctag>` \u002F `:latest` | Core HTTP service + all cleaners + exiftool \u002F qpdf \u002F c2patool | Yes |\n| `…:markllm-\u003Ctag>` \u002F `:markllm-latest` | MarkLLM text-watermark harness (Apache-2.0 upstream) | Yes |\n| `…:markdiffusion-\u003Ctag>` \u002F `:markdiffusion-latest` | MarkDiffusion image harness (Apache-2.0 upstream) | Yes |\n| `watermarks-remover-ctrlregen:local` | CtrlRegen pixel removal — **never published** (`noai-watermark` ships no LICENSE) | Local build only |\n| `watermarks-remover-synthid-scorer:local` | reverse-SynthID scorer — **never published** (non-commercial Research License) | Local build only (CLI scorer + optional `wr-synthid-score` HTTP sidecar under the `heavy` profile) |\n\nBuild and run the core service:\n\n```bash\nmake docker-core-build\ndocker run --rm -p 127.0.0.1:8765:8765 --read-only --tmpfs \u002Ftmp watermarks-remover\n# any CLI stays runnable by overriding the command:\ndocker run --rm -v \"$(pwd):\u002Fdata\" watermarks-remover \\\n  \u002Fapp\u002Fscripts\u002Fclean_file.py \u002Fdata\u002Fnotes.md -o \u002Fdata\u002Fnotes.cleaned.md\n```\n\nWhole-infra bring-up:\n\n```bash\ndocker compose up -d                         # core HTTP service only\ndocker compose --profile harness up -d       # + markllm \u002F markdiffusion\ndocker compose --profile heavy up -d         # + ctrlregen \u002F synthid (local builds)\ndocker compose --profile harness --profile heavy up -d   # all services\n```\n\nThe compose stack maps the core service to `127.0.0.1:8765`. The harness\u002Fheavy services are one-shot CLIs — invoke with `docker compose run --rm \u003Cservice> …` when you need verification or pixel work.\n\nValidate the running stack (exit code only, no output on success):\n\n```bash\nmake compose-check        # or: .\u002Fcompose-check.sh\n```\n\nChecks `wr-core` via `GET \u002Fhealth` and runs each harness\u002Fheavy service with `--help`, requiring exit `0`.\n\n### Configuration (env vars for docker compose)\n\n**Nothing is required to clean arbitrary text** — the core service works out of the box:\n\n```bash\necho \"Hello\\u200bWorld\\u00ad!\" > \u002Ftmp\u002Fsample.txt\ncurl -s -X POST http:\u002F\u002F127.0.0.1:8765\u002Fclean -H 'Content-Type: application\u002Fjson' \\\n  -d \"{\\\"file\\\": \\\"$(base64 \u003C \u002Ftmp\u002Fsample.txt | tr -d '\\n')\\\", \\\"name\\\": \\\"sample.txt\\\"}\"\n```\n\nEverything else is optional and lives in a `.env` file at the repo root. `docker compose` **auto-loads `.env`** and interpolates the `${VAR}` references in `compose.yaml` from it (shell exports win over `.env` if both are set).\n\n```bash\ncp .env.example .env       # then edit\ndocker compose up -d       # picks up .env automatically\n```\n\n`.env` is **gitignored** (deny-by-default) — never commit it. For host-side CLI runs (`rewrite_text.py`, the skill), export the same file into the environment:\n\n```bash\nset -a; . .\u002F.env; set +a; python3 service\u002Fscripts\u002Frewrite_text.py \u002Ftmp\u002Fx.txt -o \u002Ftmp\u002Fx.rewritten.txt\n```\n\n| Var | Reaches | Purpose |\n| --- | --- | --- |\n| `WATERMARKS_SERVER_API_KEY` | `wr-core` (via compose `environment`) | Require `Authorization: Bearer \u003Ckey>` on the HTTP API |\n| `WATERMARKS_GEMINI_*` | — | Removed Aug 2026: Google retired SynthID text watermarking on the API (see `vendor-notes.md`) |\n| `WATERMARKS_SYNTHID_SCORER_URL` | `wr-core` | Point core at the `wr-synthid-score` sidecar for SynthID image scoring (e.g. `http:\u002F\u002Fwr-synthid-score:8766` under the heavy profile) |\n| `WATERMARKS_SYNTHID_SCORER_API_KEY` | `wr-core` + `wr-synthid-score` | Shared bearer key for the scorer sidecar (empty = no auth) |\n| `WATERMARKS_MARKLLM_SCHEME` | `text_detectors.py` (host) | MarkLLM scheme for `\u002Fdetect`: `kgw` (default) \u002F `synthid` |\n| `HF_TOKEN` | harness\u002Fheavy services | Hugging Face token for gated models |\n| `WATERMARKS_SERVICE_URL` | client only (skill \u002F curl) | Where to reach the service; default `http:\u002F\u002F127.0.0.1:8765` |\n| `WATERMARKS_REWRITE_BACKEND` | `rewrite_text.py` hook | `print-prompt` (default) \u002F `ollama` \u002F `openai-compatible` |\n| `WATERMARKS_REWRITE_MODEL` | `rewrite_text.py` hook | Model name (e.g. `deepseek-v4-flash`) |\n| `WATERMARKS_REWRITE_BASE_URL` | `rewrite_text.py` hook | API base (e.g. `https:\u002F\u002Fapi.deepseek.com`) |\n| `WATERMARKS_REWRITE_API_KEY` | `rewrite_text.py` hook | API key — env only, never on argv |\n| `WATERMARKS_REWRITE_ALLOW_REMOTE` | `rewrite_text.py` hook | `1` to allow non-loopback endpoints |\n| `WATERMARKS_REWRITE_REASONING_EFFORT` | `rewrite_text.py` hook | `none` (default) \u002F `low` \u002F `medium` \u002F `high` \u002F `off` |\n\nLayer B is agent-orchestrated in the skill (it rewrites with its own model), so the `WATERMARKS_REWRITE_*` vars are only needed when driving `rewrite_text.py` directly.\n\nImages publish automatically on `v*` tags via [`.github\u002Fworkflows\u002Frelease-images.yml`](.github\u002Fworkflows\u002Frelease-images.yml).\n\n## Optional SynthID pixel scoring\n\n`inspect_image.py` and `clean_image.py` can report a pixel-domain SynthID\nconfidence score when an external checkout of\n[`aloshdenny\u002Freverse-SynthID`](https:\u002F\u002Fgithub.com\u002Faloshdenny\u002Freverse-SynthID)\nis available. The scorer is **not bundled**: it is loaded at runtime from your\ncheckout, and its code remains under the upstream project's non-commercial\nResearch License.\n\n### Option 1: one-command bootstrap (no Docker)\n\n```bash\nSCRIPTS=service\u002Fscripts\n\n# Clones upstream, creates a venv, and installs scorer-only dependencies.\n\"$SCRIPTS\u002Fsetup_synthid.sh\"\n\n# Score an image (default checkout: ~\u002Freverse-SynthID).\nREVERSE_SYNTHID_DIR=~\u002Freverse-SynthID \\\n~\u002Freverse-SynthID\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fscore_synthid.py\" shot.png\n\n# Or surface the score from inspect \u002F clean (same venv Python).\nREVERSE_SYNTHID_DIR=~\u002Freverse-SynthID \\\n~\u002Freverse-SynthID\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Finspect_image.py\" shot.png\n```\n\n`setup_synthid.sh` accepts `--dir PATH`, `--ref REF`, and `--full` (install the\nfull upstream `requirements.txt`, which adds `torch`\u002F`diffusers` for the\nupstream VAE bypass this project does not use).\n\nOn Windows use `setup_synthid.ps1` (`-Dir`, `-Ref`, `-Full`), which creates the\nvenv at `.venv\\Scripts\\` — the layout `image_meta.py` already looks for on\n`os.name == \"nt\"`.\n\n### Option 2: local Docker build\n\n```bash\nmake docker-synthid-build\n# Run unprivileged and with a read-only rootfs; the scorer only needs to read\n# \u002Fdata and write to stdout\u002Ftmp.\ndocker run --rm \\\n  --user \"$(id -u):$(id -g)\" \\\n  --read-only --tmpfs \u002Ftmp \\\n  -v \"$(pwd):\u002Fdata\" \\\n  watermarks-remover-synthid-scorer \u002Fdata\u002Fshot.png\n```\n\nThe image is built locally from the upstream source at build time. It is not\npublished, so it does not redistribute the upstream code.\n\n### Option 3: HTTP scorer sidecar (docker compose)\n\nUnder the `heavy` profile the compose stack also runs the scorer as an HTTP\nsidecar (`wr-synthid-score`) so the **published core service** can score\nimages before\u002Fafter cleaning without bundling the non-commercial upstream\ncode. Point `wr-core` at it and share a bearer key (see `.env.example`):\n\n```bash\n# .env\nWATERMARKS_SYNTHID_SCORER_URL=http:\u002F\u002Fwr-synthid-score:8766\nWATERMARKS_SYNTHID_SCORER_API_KEY=change-me\n\ndocker compose --profile heavy up -d\n```\n\nThen `POST \u002Fclean` with `{\"options\": {\"detect_before\": true,\n\"detect_after\": true}}` returns `synthid_before` \u002F `synthid_after` in the\nreport, and `POST \u002Fdetect` on an image returns the SynthID score. Fail-soft:\nif the sidecar is down or unconfigured, reports carry\n`{\"available\": false, \"error\": ...}` and cleaning still succeeds.\n\nV4 scoring uses `artifacts\u002Fspectral_codebook_v4.npz` from the upstream checkout\n(`220 MB). This is **detection\u002Fscoring only** — it does not remove pixel\nwatermarks.\n\n## Optional CtrlRegen pixel removal\n\nFor **pixel-domain** image watermarks (SynthID-class, StegaStamp, Tree-Ring,\nStableSignature), an optional external backend runs the CtrlRegen pipeline\n(ControlNet + DINOv2 IP-Adapter controllable regeneration). The backend is\n[`mertizci\u002Fnoai-watermark`](https:\u002F\u002Fgithub.com\u002Fmertizci\u002Fnoai-watermark), a\nmaintained reimplementation of the ICLR 2025\n[CtrlRegen](https:\u002F\u002Farxiv.org\u002Fabs\u002F2410.05470) method with automatic tiling.\n\nThe backend is **not bundled** and ships no LICENSE file, so it is treated as\nall-rights-reserved: it is cloned at a pinned commit and loaded at runtime.\nIts research-era dependency pins (`requirements-ctrlregen.txt` — e.g.\n`transformers==4.37.2`, `diffusers==0.27.2`) carry published advisories and\nare intentionally not current, so they are only ever installed inside the\ndedicated venv this script creates and never into the main service image;\n`setup_ctrlregen.sh` also re-verifies the pinned commit on existing\ncheckouts, not just fresh clones.\n\n### Bootstrap\n\n```bash\nSCRIPTS=service\u002Fscripts\n\n# Clones upstream (pinned commit), creates a venv, installs torch + deps.\n\"$SCRIPTS\u002Fsetup_ctrlregen.sh\"\n\n# Standalone removal (default checkout: ~\u002Fnoai-watermark).\nNOAI_WATERMARK_DIR=~\u002Fnoai-watermark \\\n~\u002Fnoai-watermark\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fclean_ctrlregen.py\" shot.png -o shot.ctrlregen.png\n```\n\nOn Windows use `setup_ctrlregen.ps1` (same flags as `-Dir`, `-Ref`, `-Python`);\nthe venv lands in `.venv\\Scripts\\`, which `clean_image.py` already resolves.\nIt probes the published PyTorch wheel indices and picks the highest one at or\nbelow the CUDA version `nvidia-smi` prints that actually exists — that number\nis the maximum the *driver* supports, and drivers are backward compatible, so a\ndriver reporting 13.1 (no published `cu131`) installs `cu130`. Below compute\ncapability 7.5 it forces `cu126`, the last index whose wheels still carry\nMaxwell\u002FPascal\u002FVolta kernels. It installs `torch` **and** `torchvision`\ntogether from that index so the dependency install cannot swap them for CPU\nbuilds from PyPI, then verifies after install that `torch.cuda.is_available()`\nis true — if a GPU was detected but torch ends up CPU-only, the script warns\nloudly and exits non-zero instead of pretending the setup succeeded.\n\n### From `clean_image.py`\n\n```bash\nNOAI_WATERMARK_DIR=~\u002Fnoai-watermark \\\n~\u002Fnoai-watermark\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fclean_image.py\" shot.png \\\n  -o shot.cleaned.png --remove-pixel ctrlregen\n```\n\nOrder of operations: metadata strip first, then CtrlRegen pixel removal, then\nan optional reverse-SynthID before\u002Fafter score (when `REVERSE_SYNTHID_DIR` is\nalso set).\n\n**Strength is conservative by default** (`--ctrlregen-strength 0.25`), because\nhigher strength removes more watermark but regenerates more of the image.\nDocumented presets: `0.15` minimal \u002F `0.25` default \u002F `0.35` balanced \u002F\n`0.5` aggressive \u002F `0.7` max (backend default is 0.5). `--ctrlregen-steps`\ndefaults to 50 (effective denoising steps ≈ steps × strength).\n\n### Image size (512×512 native limit)\n\nCtrlRegen is a 512×512 Stable Diffusion 1.5 ControlNet. The backend resolves\nthis for arbitrary inputs, so no extra tiling is exposed here:\n\n- **≤512 px:** single pass — center-crop\u002Fresize to 512, regenerate, resize back.\n- **>512 px:** automatic overlapping tiling (512 px tiles, 192 px overlap),\n  width\u002Fheight aligned to multiples of 8, then cosine-blended seams.\n- **Either path:** output is resized to the original size and color-matched to\n  the original image.\n\nVery large images (e.g. 4K) produce many tiles, so runs scale with tile count\n(slower and higher VRAM). Pre-downscale large inputs when practical; tile size\nand overlap are hardcoded upstream and are not exposed as flags.\n\n### Compute, gated models, and verification\n\nExpect ~10 GB of model downloads; a GPU is strongly recommended and CPU runs\nare slow. Some upstream models are gated, so export `HF_TOKEN` (env only —\nnever argv). `clean_ctrlregen.py` refuses to auto-install dependencies; run\n`setup_ctrlregen.sh` first.\n\nThere is no local detector for StegaStamp\u002FTree-Ring\u002FStableSignature, so the\nonly local signal is the reverse-SynthID score (a surrogate). When available,\n`clean_image.py --remove-pixel ctrlregen` reports that score before\u002Fafter; the\nofficial Google SynthID check remains the final authority.\n\n### Docker\n\n```bash\nmake docker-ctrlregen-build\ndocker run --rm -e HF_TOKEN=\"$HF_TOKEN\" \\\n  --user \"$(id -u):$(id -g)\" \\\n  -v \"$(pwd):\u002Fdata\" \\\n  watermarks-remover-ctrlregen \u002Fdata\u002Fshot.png -o \u002Fdata\u002Fshot.ctrlregen.png\n```\n\n## Optional MarkLLM text-watermark verification\n\nFor **controlled experiments**, an optional external harness wraps\n[`THU-BPM\u002FMarkLLM`](https:\u002F\u002Fgithub.com\u002FTHU-BPM\u002FMarkLLM) (Apache-2.0) to\nwatermark test text and re-detect it after a Layer B rewrite — e.g. prove that\na KGW (Kirchenbauer, your \"open-LLM\" row) or SynthID-Text (Gemini row) mark\ndisappears under your rewrite. It is a **verification harness, not an oracle**:\nMarkLLM detection is only valid against the *same* scheme config + keys used at\ngeneration, and it cannot certify a vendor detector will fail.\n\nThe backend is **not bundled**. `setup_markllm.sh` clones upstream at a pinned\ncommit, creates a venv, and installs pinned deps (torch + transformers); the\nscoring model (default `facebook\u002Fopt-1.3b`, Apache-2.0) downloads from Hugging\nFace on first run.\n\n```bash\nSCRIPTS=service\u002Fscripts\n\n# Bootstrap (clones upstream, creates ~\u002FMarkLLM\u002F.venv, installs deps).\n\"$SCRIPTS\u002Fsetup_markllm.sh\"\n\n# Generate watermarked + unwatermarked sample text under the KGW scheme.\nMARKLLM_DIR=~\u002FMarkLLM \\\n  ~\u002FMarkLLM\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fdetect_text_watermark.py\" watermark prompt.txt \\\n    --scheme kgw -o wm.txt -o2 plain.txt\n\n# Detect the scheme mark in a text file.\nMARKLLM_DIR=~\u002FMarkLLM \\\n  ~\u002FMarkLLM\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fdetect_text_watermark.py\" detect wm.txt --scheme kgw --json\n```\n\n**Verification around a Layer B rewrite:** pass `--markllm-scheme` to\n`rewrite_text.py` (with `--markllm-dir`), and it records the MarkLLM detection\nbefore\u002Fafter plus a `cleared` flag:\n\n```bash\nexport WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2\nMARKLLM_DIR=~\u002FMarkLLM \\\n  python3 \"$SCRIPTS\u002Frewrite_text.py\" wm.txt -o wm.rewritten.txt \\\n    --markllm-scheme kgw --markllm-dir \"$HOME\u002FMarkLLM\" --json-stats\n```\n\n**Detection-guided iterative rewriting:** Layer B now rewrites iteratively and\nstops as soon as an attempt passes evaluation. Each evaluation round generates\n`--candidates` variants (default **1**, `WATERMARKS_REWRITE_CANDIDATES`)\nand `--max-loops` caps how many rounds run before the best-effort variant is\nreturned (default **1**, `WATERMARKS_REWRITE_LOOPS`). Each variant is one\nrewrite call plus one evaluation, and a round exits early on the first attempt\nthe evaluator reports as not watermarked — so raising `--max-loops` retries\nnew variants until an evaluation passes (a typical clean rewrite costs one\nattempt). The evaluator is chosen by priority:\n\n1. **MarkLLM** — same-config research detection, when `--markllm-scheme` is\n   passed (with `--markllm-dir`). A vendor-detector slot is reserved above\n   MarkLLM for Google's SynthID-text detector, which Google retired on its API\n   in Aug 2026 — a future vendor endpoint can plug in there.\n2. **bigram-Jaccard lexical divergence** — when no detector is configured; no\n   pass\u002Ffail verdict, so every attempt is generated and the most lexically\n   diverged one is selected (the original behavior).\n\n`--json-stats` reports the evaluator, attempts made, pass\u002Ffail, and per-attempt\nrecords:\n\n```json\n{\n  \"evaluator\": \"markllm\",\n  \"candidates\": 1,\n  \"max_loops\": 2,\n  \"attempts_made\": 2,\n  \"passed\": true,\n  \"candidate_scores\": [\n    {\n      \"lexical_divergence\": 0.91,\n      \"selection_score\": 0.91,\n      \"selected\": false,\n      \"passed\": false,\n      \"evaluation\": {\"detector\": \"markllm\", \"available\": true, \"scheme\": \"kgw\",\n                     \"is_watermarked\": true, \"score\": 4.3, \"threshold\": 3.0}\n    },\n    {\n      \"lexical_divergence\": 0.84,\n      \"selection_score\": 0.84,\n      \"selected\": true,\n      \"passed\": true,\n      \"evaluation\": {\"detector\": \"markllm\", \"available\": true, \"scheme\": \"kgw\",\n                     \"is_watermarked\": false, \"score\": 1.7, \"threshold\": 3.0}\n    }\n  ],\n  \"markllm\": {\"scheme\": \"kgw\", \"before\": {\"...\": \"...\"}, \"after\": {\"...\": \"...\"},\n              \"cleared\": true, \"note\": \"same-config only\"}\n}\n```\n\nA detector that is unconfigured, times out, or errors yields an\n`\"available\": false` entry with an `error` reason and never fails the\nrewrite — that attempt simply cannot pass, and the loop falls back to\nlexical-divergence selection. When the max is exhausted without a pass, the\nleast-watermarked (lowest score) attempt is returned as best-effort with a\nnote.\n\nIf the backend is unconfigured or its deps are missing, the rewrite proceeds\nand the report notes verification was unavailable. A GPU is recommended; CPU\nruns work but are slow, and the model download is a few GB.\n\nHardening knobs:\n\n- `--offline` on the adapter (or any MarkLLM run) loads the scoring model from\n  the Hugging Face cache only — zero network egress; fails fast if not cached.\n  Custom remote code is never executed (transformers `trust_remote_code` is\n  never enabled).\n- `WATERMARKS_MARKLLM_RLIMIT_AS=\u003Cbytes>` (env, POSIX) applies an address-space\n  limit to the MarkLLM detector subprocess. Off by default because torch\u002FCUDA\n  usually needs large address spaces.\n- Config files are capped at 1 MiB; the upstream checkout and the base image\n  are pinned by SHA\u002Fdigest.\n\n### Docker\n\n```bash\nmake docker-markllm-build\ndocker run --rm --user \"$(id -u):$(id -g)\" -v \"$(pwd):\u002Fdata\" \\\n  watermarks-remover-markllm detect \u002Fdata\u002Fwm.txt --scheme kgw --json\n```\n\n## Optional SynthID-text removal benchmark\n\n[`bench_synthid_text.py`](service\u002Fscripts\u002Fbench_synthid_text.py) measures how\neffectively a Layer B rewrite clears SynthID-text-class watermarks and at\nwhat cost. It generates watermarked + unwatermarked samples with the MarkLLM\nSynthID scheme (same-config detection, sanity-gated), runs your rewrite\nvariants (strength × max rewrite attempts; the loop stops early on pass) plus\ncontrols (no-removal, Layer-A-only, optional re-stamp check), and writes a\nshareable `report.md` \u002F\n`results.json` \u002F `results.csv`. Full guide:\n[`docs\u002Fsynthid-text-benchmark.md`](docs\u002Fsynthid-text-benchmark.md).\n\nRequires a MarkLLM checkout (`setup_markllm.sh` \u002F `MARKLLM_DIR`) and a\nrewrite backend. **The rewriting model is an LLM you configure** — the same\n`rewrite_text.py` backend the skill uses. MarkLLM's default\n`facebook\u002Fopt-1.3b` (`--markllm-model`) is only the watermark\ngenerator\u002Fdetector; it never rewrites. Configure the rewrite model via env\nvars or benchmark flags (they mirror the\n[config table](#configuration-env-vars-for-docker-compose) above):\n\n| Env var | Benchmark flag | Default | Meaning |\n| --- | --- | --- | --- |\n| `WATERMARKS_REWRITE_BACKEND` | `--rewrite-backend` | `ollama` | `ollama` or `openai-compatible` |\n| `WATERMARKS_REWRITE_MODEL` | `--rewrite-model` | *(required)* | The LLM that performs the rewrite (e.g. `llama3.2`, `deepseek-v4-flash`) |\n| `WATERMARKS_REWRITE_BASE_URL` | `--rewrite-base-url` | `http:\u002F\u002F127.0.0.1:11434` | Endpoint; the Ollama default is loopback |\n| `WATERMARKS_REWRITE_API_KEY` | `--rewrite-api-key` | — | API key (env-only in the child process, never argv) |\n| `WATERMARKS_REWRITE_ALLOW_REMOTE=1` | `--rewrite-allow-remote` | off | Required to send content to non-loopback endpoints |\n\n```bash\n# Ollama (loopback):\npython3 service\u002Fscripts\u002Fbench_synthid_text.py --markllm-dir ~\u002FMarkLLM \\\n  --rewrite-backend ollama --rewrite-model llama3.2\n\n# OpenAI-compatible API (remote):\nWATERMARKS_REWRITE_API_KEY=... python3 service\u002Fscripts\u002Fbench_synthid_text.py \\\n  --markllm-dir ~\u002FMarkLLM --rewrite-backend openai-compatible \\\n  --rewrite-model deepseek-v4-flash --rewrite-base-url https:\u002F\u002Fapi.deepseek.com \\\n  --rewrite-allow-remote\n```\n\nUse a **non-origin model** for rewriting (do not rewrite with the same\nwatermarked model that generated the text) or the rewrite can re-stamp the\noutput; `--restamp-control` measures this.\n\n## Optional MarkDiffusion image-watermark harness\n\nFor **controlled experiments on images**, an optional external harness wraps\n[`THU-BPM\u002FMarkDiffusion`](https:\u002F\u002Fgithub.com\u002FTHU-BPM\u002FMarkDiffusion) (Apache-2.0),\na *generative watermarking* toolkit for latent diffusion models (it embeds marks\n— it does not remove them). We use it for three things:\n\n1. **Verification harness** (like MarkLLM, but for images): watermark a test\n   image with a scheme, run removal, and re-detect with the *same* scheme config\n   — e.g. prove a Tree-Ring-class mark clears under your pipeline. It is a\n   **verification harness, not an oracle**: detection requires the generating\n   model (and keys for key-based schemes), so it cannot certify a vendor\n   detector will fail on an arbitrary image.\n2. **Optional pixel-removal engine**: its `DiffusionPurification` regeneration\n   attack is exposed as `clean_image.py --remove-pixel diffusion`, an\n   alternative to CtrlRegen. It is **blind** regeneration (no ControlNet\n   conditioning), so it drifts image content more than CtrlRegen — conservative\n   strength default (`0.3`), treated as a fallback\u002Fcomparison, never a\n   guarantee.\n3. **Local same-scheme detector** for Tree-Ring-class marks, partially filling\n   the \"no local detector for StegaStamp\u002FTree-Ring\u002FStableSignature\" gap (it\n   covers Tree-Ring\u002FRing-ID\u002FGaussian-Shading etc., not StegaStamp \u002F\n   StableSignature \u002F SynthID-media).\n\nThe backend is **not bundled**. `setup_markdiffusion.sh` creates a venv and\ninstalls `markdiffusion==1.0.2` from PyPI (pinned), with torch installed from\nthe right platform index; `--checkout` installs an editable clone at a pinned\ncommit instead. The Stable Diffusion model (default\n`huanzi05\u002Fstable-diffusion-2-1-base`) downloads from Hugging Face on first run.\n\n```bash\nSCRIPTS=service\u002Fscripts\n\n# Bootstrap (PyPI pin default; creates ~\u002Fmarkdiffusion\u002F.venv, installs deps).\n\"$SCRIPTS\u002Fsetup_markdiffusion.sh\"\n\n# 1. Generate a Tree-Ring watermarked image (+ unwatermarked control).\necho \"a red fox in snow\" > \u002Ftmp\u002Fprompt.txt\nMARKDIFFUSION_DIR=~\u002Fmarkdiffusion \\\n  ~\u002Fmarkdiffusion\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fmarkdiffusion_harness.py\" watermark \\\n    \u002Ftmp\u002Fprompt.txt -o wm.png -o2 plain.png --scheme tr --json\n\n# 2. Remove with the DiffusionPurification regeneration attack.\nMARKDIFFUSION_DIR=~\u002Fmarkdiffusion \\\n  ~\u002Fmarkdiffusion\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fmarkdiffusion_harness.py\" purify \\\n    wm.png -o wm.purified.png --purification-strength 0.3 --json\n\n# 3. Re-detect with the SAME scheme config.\nMARKDIFFUSION_DIR=~\u002Fmarkdiffusion \\\n  ~\u002Fmarkdiffusion\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fmarkdiffusion_harness.py\" detect \\\n    wm.purified.png --scheme tr --detector-type l1_distance --json\n```\n\nOr run purification as part of the normal image pipeline:\n\n```bash\nMARKDIFFUSION_DIR=~\u002Fmarkdiffusion \\\n  ~\u002Fmarkdiffusion\u002F.venv\u002Fbin\u002Fpython \"$SCRIPTS\u002Fclean_image.py\" shot.png \\\n    -o shot.cleaned.png --remove-pixel diffusion\n```\n\nHardening knobs mirror the MarkLLM harness: `--offline` loads the model from\nthe Hugging Face cache only (zero network egress, no remote code), `HF_TOKEN`\nis env-only (never argv), algorithm configs are capped at 1 MiB, and the\nsubprocess gets the same higher resource caps as CtrlRegen.\n\n### Docker\n\n```bash\nmake docker-markdiffusion-build\ndocker run --rm --user \"$(id -u):$(id -g)\" -v \"$(pwd):\u002Fdata\" \\\n  watermarks-remover-markdiffusion detect \u002Fdata\u002Fwm.png --scheme tr --json\n```\n\nThe image installs a CPU torch; CUDA users should run `setup_markdiffusion.sh`\non the host instead. Model downloads still hit the HF hub on first run.\n\n## Coverage matrix\n\n| Channel | Claude | Gemini\u002FSynthID | OpenAI | Open-LLM |\n| --- | --- | --- | --- | --- |\n| Unicode \u002F edit-based text | Layer A | Layer A | Layer A | Layer A |\n| **Statistical sampling text** | Layer B best-effort (Claude seam when Anthropic's detection API ships) | Layer B best-effort (+ MarkLLM same-config harness; Google retired the vendor detector Aug 2026) | Layer B if present | Layer B best-effort + optional MarkLLM harness |\n| C2PA \u002F file metadata | Yes (listed formats) | Yes when present | Yes when present | Yes when present |\n| Pixel image marks | Out of scope | Optional SynthID score + CtrlRegen removal (external); optional MarkDiffusion same-scheme detect + DiffusionPurification removal (external) | Out of scope | Optional CtrlRegen \u002F MarkDiffusion removal (external) |\n| Training backdoors | Out of scope | Out of scope | Out of scope | Out of scope |\n\nDetails: [`skills\u002Fremove-ai-marks\u002Freferences\u002Fvendor-notes.md`](skills\u002Fremove-ai-marks\u002Freferences\u002Fvendor-notes.md), [`mark-classes.md`](skills\u002Fremove-ai-marks\u002Freferences\u002Fmark-classes.md).\n\n---\n\n## How text marking works (short)\n\nModern LLM watermarks often hide a signal in **which tokens are chosen** (generative \u002F sampling bias), not only in invisible characters. Edit-based schemes inject Unicode or synonym rules. File schemes attach **C2PA** or generator metadata.\n\n- **Layer A** removes edit-based Unicode carriers (testable).\n- **Layer B** attacks sampling watermarks via heavy rewrite (best-effort; literature-standard attacks such as paraphrase \u002F back-translation).\n- **File cleaners** strip C2PA\u002FXMP\u002Fprops from supported containers.\n\nUntil vendors ship public detectors and keys, **no tool can honestly certify** “this fails the official check.” Reports must separate verifiable vs best-effort work.\n\nPrefer a **non-origin** model for Layer B (do not rewrite Claude text with Claude if you are trying to avoid re-stamping).\n\n---\n\n## Disclaimer: what removing a text watermark costs\n\nText watermarks live in **the wording itself**: the signal is spread across token choices, so nearly every sentence carries a little of it. Two consequences follow, and they are why Layer B is honestly described as *best-effort* rather than a magic eraser.\n\n1. **Removal means rewording, not restructuring.** Shuffling paragraphs, changing headings, or light touch-ups barely move the signal. Stripping a statistical mark requires rewriting a substantial fraction of the text — sentence by sentence, not section by section.\n\n2. **Rewording degrades the copy.** Any rewrite replaces the original word choices with the rewriting model's, which flattens tone, voice, and precision. On production copy (SEO, marketing, client work) that degradation is real and often visible to the people who care most about the writing. It is like taking text from a top-tier model and asking a less capable model to rewrite it from scratch: the result cannot exceed the rewrite model's ceiling.\n\nWhich leads to the honest full-circle question:\n\n> If the plan is to rewrite the text with a cheaper model anyway, why pay for a premium model in the first place? Generating directly with the cheaper model is simpler, cheaper, and produces the same — or better — end result.\n\nLayer B makes sense when you specifically want the premium model's **thinking and drafting** and accept a rewrite pass to satisfy a hygiene or privacy requirement — not as a cheap route to mark-free text.\n\n**When to skip Layer B:**\n\n- **Quality matters more than hygiene:** use the lossless path — Layer A Unicode scrub plus the file metadata cleaners — and keep the original prose.\n- **Rewriting anyway:** use a **non-origin** model (rewriting with the origin model can re-stamp the text), and remember residual risk remains — no tool can certify a vendor detector will fail.\n\n---\n\n## File formats\n\n| Format | Inspect | Clean |\n| --- | --- | --- |\n| PNG \u002F JPEG \u002F WebP | C2PA chunks \u002F APP11 \u002F RIFF `C2PA`, AI XMP hints | Drop metadata segments |\n| AVIF \u002F HEIC | ISOBMFF `jumb` \u002F XMP `uuid` boxes | Drop boxes |\n| BMP | Trailing non-image bytes (no standardized channel) | Truncate trailing metadata, fix file-size field |\n| GIF | Comment \u002F XMP application extensions | Drop comment & XMP, keep `NETSCAPE2.0` loop |\n| TIFF (classic + BigTIFF) | IFD tags: XMP, EXIF, GPS, IPTC, MakerNote | Drop tags, zero payloads, keep strips |\n| SVG | `\u003Cmetadata>`, XMP | Strip blocks |\n| PDF | Byte\u002FXMP + optional tools | **exiftool** then **qpdf**; degraded without either |\n| DOCX | docProps \u002F customXml | Scrub props, drop customXml |\n| EPUB | OPF metadata, XHTML meta\u002FJSON-LD, embedded media | Scrub OPF, strip XHTML meta, clean media + Layer A (skips encrypted parts) |\n| ODT | meta.xml | Drop generator \u002F AI-ish meta |\n| HTML | meta, JSON-LD, data-ai* | Strip tags\u002Fattrs |\n| Markdown | YAML frontmatter AI keys | Drop keys + Layer A body |\n| MP4 \u002F MOV \u002F M4A \u002F M4V | ISOBMFF `jumb`\u002F`uuid` boxes (same mechanism as AVIF\u002FHEIC) + `moov\u002Fudta` generator tags | Drop boxes |\n| WAV | RIFF `LIST INFO` chunk, embedded `id3 ` chunk | Drop chunks |\n| MP3 | ID3v2 frames (v2.3\u002Fv2.4 per-frame; v2.2 whole-tag) | Drop matched frames or whole tag |\n\n#### Why PDF needs qpdf, not just exiftool\n\nExifTool writes PDFs **incrementally**. `exiftool -all=` appends a\n`%BeginExifToolUpdate` block that frees the Info object and drops `\u002FInfo` from\nthe trailer — but the original metadata bytes stay in the file verbatim, and\nexiftool itself can undo the edit with `-PDF-update:all=`. The command exits\n`0`, viewers show no metadata, and the file gets *larger*, which is the tell.\n\nFor a provenance-stripping tool that is a silent leak, so `clean_pdf` follows\nthe exiftool pass with `qpdf --linearize`, which re-serializes the document\nfrom its object graph and drops the now-unreferenced objects. Without `qpdf`\ninstalled the clean still runs, but it says so:\n\n```\nwarning: exiftool PDF edits are incremental — the original metadata bytes\nremain recoverable; install qpdf for a structural rewrite\n```\n\nPixel-domain watermark **removal** is now available as an optional external CtrlRegen backend (see above); it is a regenerating remover, not a guarantee. **C2PA soft binding** (in-content watermark that can re-link a remote Content Credentials manifest after metadata is stripped) remains **out of scope**. Stripping hard-bound C2PA does **not** clear those channels.\n\n### Residual risk after a clean\n\nThis tool reports **verifiable** removals (Unicode counts, metadata actions) and **best-effort** Layer B rewrites. It cannot certify that vendor detectors will fail.\n\nTo check residual signals yourself (optional, external):\n\n| Channel | What we remove | What may remain | External check (examples) |\n| --- | --- | --- | --- |\n| Hard-bound C2PA \u002F EXIF \u002F XMP | Yes | Soft-bound \u002F pixel marks | [c2patool](https:\u002F\u002Fgithub.com\u002Fcontentauth\u002Fc2pa-rs\u002Ftree\u002Fmain\u002Fcli), [Content Credentials verify](https:\u002F\u002Fcontentcredentials.org\u002Fverify) |\n| SynthID-class media | Optional pixel removal (external CtrlRegen); local score otherwise | Audio\u002Fvideo watermark; residual pixel watermark after removal | Provider tools (e.g. [Google SynthID](https:\u002F\u002Fdeepmind.google\u002Fscience\u002Fsynthid\u002F) \u002F Vertex detector where offered); optional local [reverse-SynthID](https:\u002F\u002Fgithub.com\u002Faloshdenny\u002Freverse-SynthID) scorer |\n| Statistical text | Best-effort rewrite | Strong marks after light edit | No public universal detector; vendor tools when available |\n\nIndustry two-layer context (C2PA + imperceptible watermark): [Institute of AI PM guide](https:\u002F\u002Fwww.institutepm.com\u002Fknowledge-hub\u002Fai-content-provenance-watermarking).\n\n---\n\n## Removal options (summary)\n\n| Option | Removes | Notes |\n| --- | --- | --- |\n| Unicode scrub (Layer A) | ZWSP, bidi, tags, exotic spaces, … | Safe default for text |\n| Rewrite (Layer B) | Statistical token marks (best-effort) | Always offered by skill; costs style — see [Disclaimer](#disclaimer-what-removing-a-text-watermark-costs) |\n| Container\u002Fmetadata strip | File provenance | See format table |\n| CtrlRegen pixel removal (optional) | Pixel-domain image marks (SynthID-class, StegaStamp, Tree-Ring, StableSignature) | External backend; heavy compute; conservative strength default |\n| DiffusionPurification pixel removal (optional) | Pixel-domain image marks (Tree-Ring-class) | MarkDiffusion backend; blind regeneration (more drift than CtrlRegen); conservative strength default |\n| Open-weight local models | Avoid re-stamping with origin model | Operational alternative |\n\nMatrix: [`skills\u002Fremove-ai-marks\u002Freferences\u002Fremoval-matrix.md`](skills\u002Fremove-ai-marks\u002Freferences\u002Fremoval-matrix.md).\n\n## Ethics and disclaimer\n\nSee [`skills\u002Fremove-ai-marks\u002Freferences\u002Fethics.md`](skills\u002Fremove-ai-marks\u002Freferences\u002Fethics.md). For privacy and research on **your** content — not academic fraud or false “human-written” claims.\n\n**Responsible use:** This project is for content you own or are authorized to process. Users must adhere to local regulations and use it responsibly. The developers disclaim any liability for potential misuse by users.\n\n## Ecosystem\n\nThird-party projects that wrap or complement this repository, listed for discoverability only. **They are not maintained, endorsed, or supported by this project.** This project does not review their code, vouch for their behavior or guarantees, or take responsibility for anything you install or run from this list. Each project is governed by its own license, maintainers, and documentation — read those before using it.\n\n### MetaClean — desktop GUI\n\n[MetaClean](https:\u002F\u002Fgithub.com\u002FMoresyl\u002Fmetaclean) is an independent MIT-licensed Rust\u002FTauri desktop application (Windows, macOS, Linux) providing a packaged native GUI for drag-and-drop metadata cleaning, with a system tray and Explorer integration. It is a separate codebase: it does not call this repository's Python service, and its supported formats and cleaning guarantees differ from this project's. See its README for details.\n\n### unmark-web — browser web UI\n\n[unmark-web](https:\u002F\u002Fgithub.com\u002Fivanusto\u002Funmark-web) is an independent, MIT-licensed static web client. It removes invisible Unicode marks from text and strips provenance metadata from images entirely in the browser, and can optionally call this repository's HTTP service for the formats it does not handle locally. It is a separate codebase and is not affiliated with this project; see its README for scope and limits.\n\n### Adding a project\n\nTo register a project here, open a PR adding a short entry — project name, what it wraps or adds, and a link to its own repository. Keep entries brief and factual; do not claim compatibility with, or endorsement by, this project. Please avoid names that start with or closely resemble `watermarks-remover` — look-alike names make it hard to tell which project is which.\n\n## Pre-commit hook\n\nCI gating already exists (`audit_dir.py`'s SARIF export, see [Coverage matrix](#coverage-matrix) context) — the [pre-commit](https:\u002F\u002Fpre-commit.com\u002F) hooks below catch the same class of problem earlier, before a marked file is even committed. Both wrap the existing CLIs (`audit_dir.py` \u002F `clean_file.py`) — no separate detection logic.\n\n```yaml\n# .pre-commit-config.yaml\nrepos:\n  - repo: https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\n    rev: v0.5.0   # pin to a tag\u002Fcommit\n    hooks:\n      - id: watermarks-remover-check   # fails the commit if marks are found\n      # - id: watermarks-remover-clean # opt-in: cleans staged files in place instead\n```\n\n`watermarks-remover-check` fails the commit and lists findings; `watermarks-remover-clean` is opt-in and rewrites staged files in place (exits non-zero so you review the diff and re-stage — the same convention as auto-fixing hooks like `ruff --fix`). Run either by hand with `python3 service\u002Fscripts\u002Fcheck_staged.py \u003Cfiles...>` \u002F `clean_staged.py \u003Cfiles...>`.\n\n## Tests\n\n```bash\npython3 -m venv .venv && .venv\u002Fbin\u002Fpip install pytest\n.venv\u002Fbin\u002Fpython -m pytest          # or: make test\nmake smoke                          # quick CLI smoke on fixtures\n```\n\n## Changelog\n\n### Unreleased — detection-guided iterative Layer B rewriting\n\n- **Layer B rewriting is now iterative and evaluation-driven**: each round\n  generates `--candidates` variants (default 1,\n  `WATERMARKS_REWRITE_CANDIDATES`) and `--max-loops` (default 1,\n  `WATERMARKS_REWRITE_LOOPS`) caps the evaluation rounds, stopping as soon\n  as an attempt passes watermark detection. Evaluator priority: MarkLLM (when\n  `--markllm-scheme`) > bigram-Jaccard lexical divergence (fallback; a\n  vendor-detector seam is reserved for a future SynthID-text endpoint).\n- `rewrite_text.py --json-stats` now reports `evaluator` \u002F\n  `max_loops` \u002F `attempts_made` \u002F `passed` and per-attempt\n  `candidate_scores` records (`loop`, `passed`, `evaluation`);\n  `markllm.before\u002Fafter\u002Fcleared` is unchanged.\n- **SynthID-text benchmark**: default variants `paraphrase:3`; report and CSV\n  now carry attempts per document (`mean_attempts`, `att` column;\n  `attempts` \u002F `evaluator` \u002F `passed` columns); `--rewrite-loops`\n  mirrors `--max-loops`.\n\n### [v0.5.0](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.5.0) — service & Docker distribution, HTTP API, and verification harnesses\n\n**Service \u002F Docker distribution**\n\n- **Skill\u002Fservice split**: the skill (`skills\u002Fremove-ai-marks\u002F`) is now a code-free remote client over HTTP; all implementation moved to `service\u002Fscripts\u002F` and runs behind `server.py`, a stdlib HTTP entrypoint (`\u002Fhealth`, `\u002Finspect`, `\u002Fclean`, `\u002Fcapabilities`)\n- **HTTP service**: `service\u002Fscripts\u002Fserver.py` exposes the cleaning pipeline over JSON\u002Fbase64; hardening mirrors the CLIs (size caps, binary guard, atomic writes, loopback default, optional `WATERMARKS_SERVER_API_KEY` bearer auth)\n- **OpenAPI**: `GET \u002Fopenapi.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`\n- **Core Docker image** (`service\u002FDockerfile`): full cleaning service with exiftool \u002F qpdf \u002F c2patool preinstalled; any CLI stays runnable by overriding the command\n- **Docker \u002F compose**: `compose.yaml` brings up the whole infra (`core` always; `markllm` \u002F `markdiffusion` behind `profile: harness`; `ctrlregen` \u002F `synthid` behind `profile: heavy` as local-only builds); services are prefixed `wr-`; harness\u002Fheavy services default to `command: [\"--help\"]` so `docker compose up --profile harness --profile heavy` exits cleanly (one-shot CLIs are run with `docker compose run`); new `make compose-check` \u002F `compose-check.sh` validates the running stack (exit code only)\n- **GHCR publishing**: `.github\u002Fworkflows\u002Frelease-images.yml` publishes `core`, `markllm`, `markdiffusion` images on `v*` tags; `ctrlregen` \u002F `synthid` are never published (upstream licensing)\n- **Env configuration**: `.env.example` + service configuration guide; `docker compose` auto-loads `.env`; `.env` is gitignored (deny-by-default)\n- **Repo hygiene**: `.gitignore` and `service\u002F.dockerignore` are now deny-by-default — only explicitly allowed paths can be committed or sent in a build context (image contexts only ship `service\u002Fscripts\u002F`, which is all the Dockerfiles COPY)\n- Tests: `tests\u002Ftest_http_server.py` (13 cases) for the HTTP service; all suites re-pointed at `service\u002Fscripts\u002F`\n\n**MarkDiffusion image-watermark harness (optional)**\n\n- New optional harness (external `THU-BPM\u002FMarkDiffusion`, Apache-2.0): `markdiffusion_harness.py` with `watermark` \u002F `detect` \u002F `purify` subcommands for nine image schemes (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL)\n- `clean_image.py --remove-pixel diffusion` runs the MarkDiffusion `DiffusionPurification` regeneration attack as an alternative pixel-removal engine (conservative strength 0.3 default)\n- `setup_markdiffusion.sh` bootstrap (PyPI pin `1.0.2`; `--checkout` editable clone at pinned commit) + `requirements-markdiffusion.txt` + `Dockerfile.markdiffusion` and Makefile `bootstrap-markdiffusion` \u002F `smoke-markdiffusion` \u002F `docker-markdiffusion-build` \u002F `docker-markdiffusion-help`\n- Mock-based tests (`tests\u002Ftest_markdiffusion_harness.py`) — no torch in CI; `references\u002Fmarkdiffusion.md` reference doc\n- Docs: same-scheme-only verification caveat (not a vendor-detector oracle) and blind-regeneration drift caveat in README, SKILL.md, `removal-matrix.md`, `markdiffusion.md`\n\n**MarkLLM text-watermark harness (optional)**\n\n- New optional harness (external `THU-BPM\u002FMarkLLM` checkout, Apache-2.0): `detect_text_watermark.py` with `detect` \u002F `watermark` subcommands for KGW and SynthID schemes\n- `rewrite_text.py --markllm-scheme` runs before\u002Fafter detection around a Layer B rewrite and per-candidate detection when `--candidates N>1` (env-gated; reports `cleared`)\n- `setup_markllm.sh` bootstrap + `requirements-markllm.txt` (pinned deps) + `Dockerfile.markllm` and Makefile `bootstrap-markllm` \u002F `smoke-markllm` \u002F `docker-markllm-build` \u002F `docker-markllm-help`\n- Hardening: `--offline` cache-only model loading (no HF egress, no remote code), 1 MiB config cap, optional `WATERMARKS_MARKLLM_RLIMIT_AS` on the rewrite subprocess, pinned torch in the Dockerfile, and clone-SHA verification in `Dockerfile.markllm`\n- Mock-based tests (`tests\u002Ftest_markllm_detect.py`, 21 cases) — no torch in CI; verification-harness caveat (same-config-only, not a vendor-detector oracle) documented in README, SKILL.md, `removal-matrix.md`, `vendor-notes.md`\n\n**Fixes and polish**\n\n- **Layer B**: `rewrite_text.py` now sends `reasoning_effort: \"none\"` by default for `openai-compatible` backends (`--reasoning-effort` \u002F `WATERMARKS_REWRITE_REASONING_EFFORT`; `off` omits it). Reasoning models like `deepseek-v4-flash` otherwise burn ~100s of chain-of-thought on a one-line rewrite (9,894 vs 12 completion tokens)\n- **Fix markllm image build**: `requirements-markllm.txt` pinned `tokenizers==0.23.1`, which conflicts with `transformers==5.15.0` (caps `tokenizers\u003C=0.23.0`; no 0.23.0 release exists) — now pinned `tokenizers==0.22.2`; torch moved to the CPU wheel index (`torch==2.13.0.*`) so the image is CPU-only like `Dockerfile.markdiffusion`\n- **Fix ctrlregen image build**: the 2023-era research pins (`safetensors==0.4.3`, `transformers==4.37.2` → `tokenizers\u003C0.19`) ship no Python 3.14 wheels, so the base image is now `python:3.11-slim` (digest-pinned, multi-arch)\n- **Fix harness images at runtime**: `Dockerfile.markllm` and `Dockerfile.markdiffusion` never copied `common.py` into `\u002Fapp` (pre-existing bug) — added\n- **WebP**: stdlib-only inspection and metadata cleaning for RIFF `C2PA`, XMP, EXIF, and ICC profile chunks (#37)\n- **BMP \u002F GIF \u002F TIFF**: stdlib-only detection, inspection, and metadata cleaning — GIF comment\u002FXMP extensions are dropped while `NETSCAPE2.0` looping is preserved; TIFF IFD metadata (XMP\u002FEXIF\u002FGPS\u002FIPTC\u002FMakerNote) is dropped with payloads zeroed and strip offsets kept, for both classic and BigTIFF; BMP trailing metadata is truncated with the file-size field rewritten\n- **EPUB**: stdlib-only container cleaning — OPF metadata and XHTML meta\u002FJSON-LD scrubbed, embedded raster\u002FSVG media stripped, Layer A applied to XHTML body text, marker-carrying metadata parts dropped, and OCF-encrypted parts passed through untouched\n- **Filename sanitization**: HTTP service refuses unsafe client-supplied output names\n- **Fix markdown frontmatter cleaner** crashing on and leaking nested AI keys (#25)\n- **Text tools refuse binary input**; `--force-text` overrides (#24)\n- **`--json` no longer suppresses the residual-signal exit code** (#30)\n- **`inspect_file` prints the filename** in its output (#50)\n- **Preserve mixed-case CMS generator meta tags** (#42)\n- **Preserve load-bearing script invisibles, strip PUA** in Layer A (#38, #52)\n- **Preserve script joiners, flag emoji, and Arabic Cf marks** in Layer A (#28)\n- **Harden website audit against SSRF and gzip bombs** (#49)\n- **SECURITY.md** only references the private advisories channel (#51)\n- **Windows**: PowerShell ports of the setup bootstraps (#40)\n- **Docs**: add stars\u002Fforks shields and drop star-history chart; add MarkLLM to README references; pull request template; plan for Docker CLI + API deployment\n\n### [v0.4.0](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.4.0) — pixel removal, finding confidence, Windows & false-positive fixes\n\n**Optional CtrlRegen pixel removal (external backend)**\n\n- Optional pixel-domain watermark removal via an external `mertizci\u002Fnoai-watermark` checkout: `clean_ctrlregen.py` adapter + `setup_ctrlregen.sh` bootstrap (pinned commit, sparse checkout, venv, SHA verification), plus `Dockerfile.ctrlregen` and `make bootstrap-ctrlregen` \u002F `docker-ctrlregen-build` \u002F `smoke-ctrlregen`\n- `clean_image.py --remove-pixel ctrlregen` runs metadata strip → CtrlRegen removal → optional reverse-SynthID before\u002Fafter score; `inspect_image.py` hints at the flag on a high SynthID score\n- Conservative default strength `0.25` (presets 0.15\u002F0.25\u002F0.35\u002F0.5\u002F0.7); the 512×512-native pipeline is auto-tiled by the backend for larger images; the torch subprocess gets higher env-overridable resource caps\n- Backend is never bundled: `noai-watermark` ships no LICENSE file (treated as all-rights-reserved), and its auto-install\u002Frestart code paths are bypassed by using `CtrlRegenEngine` directly\n\n**Finding confidence and aggregate audits**\n\n- Findings are now classified `confirmed` \u002F `probable` \u002F `informational` \u002F `likely_false_positive`, exposed in text\u002Fimage\u002Fcontainer JSON and human reports\n- New `audit_dir.py` (recursive tree) and `audit_website.py` (sitemap discovery + crawl) aggregate reports; documented in SKILL.md\n\n**False-positive fixes**\n\n- DOCX: scan only `docProps`\u002F`customXml`, not the visible body (#14)\n- Text Layer A: preserve emoji `VS16`\u002F`ZWJ` after an emoji base; new `--strip-emoji-glue` paranoid flag (#22)\n- HTML: treat CMS generator tags as informational, not AI metadata (#13)\n- PDF: exclude stream payloads from the AI-marker byte scan (#13)\n- Inspect reports note unsupported\u002Fbest-effort paths\n\n**Windows support**\n\n- Gate POSIX-only `preexec_fn` and `os.fchmod` so writes and optional tools run on Windows (#15, #23)\n- Reconfigure stdio to UTF-8 so redirected Windows streams no longer raise on invisible Unicode; Windows CI leg + CLI smoke run (#23)\n\n**Docs and supply chain**\n\n- README CtrlRegen section + research references (CtrlRegen, UnMarker, forensic-stealth caveat), responsible-use disclaimer; SKILL\u002Fmatrix\u002Fvendor-notes\u002Fethics updates\n- Dependabot config + security-path CODEOWNERS; bump scipy\u002Fnumpy\u002Fopencv-python\u002Fscikit-learn\u002Fpywavelets and the base image to Python 3.14-slim\n- Mock-based CtrlRegen tests (no torch in CI)\n\n### [v0.3.2](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.3.2) — security hardening (safe writes, HTTP client, CI supply chain)\n\n- **Safe, atomic output writes**: every cleaner now writes via temp-file + atomic rename (`safe_write_bytes` \u002F `safe_write_text`), refuses symlinked destinations, and creates `.bak` backups through the same safe path — pre-placed symlinks (e.g. in `\u002Ftmp` or download dirs) can no longer redirect a clean write onto an arbitrary file\n- **`rewrite_text.py` HTTP client hardening**: redirects are refused outright, so an API key in the `Authorization` header can never be re-sent to an unvalidated host; non-loopback endpoints are **denied by default** (opt in with `--allow-remote` or `WATERMARKS_REWRITE_ALLOW_REMOTE=1`); only http(s) schemes are accepted; `--api-key` was removed — keys are env-only via `WATERMARKS_REWRITE_API_KEY`\n- **Resource caps**: default max input 1 GiB → 256 MiB, new 64 MiB stdin cap, DOCX\u002FODT zip budget 512 MiB → 128 MiB, and `RLIMIT_AS`\u002F`RLIMIT_FSIZE` applied to exiftool\u002Fc2patool\u002FSynthID subprocesses (all caps env-overridable)\n- **Supply chain**: CI actions SHA-pinned with `permissions: contents: read`, pinned dev deps (`requirements-dev.txt`), a `pip-audit` step, and a new CodeQL workflow; the Docker image now runs as an unprivileged user with pip pinned\n- **Scorer deps**: Pillow bumped 10.4.0 → 12.3.0 (24 known CVEs); API usage verified against the pinned upstream commit\n- Tests: 18 new security regression tests (60 total, all passing)\n\n### [v0.3.1](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.3.1) — stronger Layer B statistical-watermark rewrite\n\n- `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\n- New `--strength humanize`: zero-shot \"write like a human\" pass targeting formulaic AI-style phrasing\n- New `--strength code`: rewrites comments, docstrings, and string literals, and renames local identifiers while preserving behavior and public API names\n- Structural pass now emits \"natural, varied human prose\" instead of AI-typical \"clear professional style\"\n- New `--temperature` (default `0.9`) for both Ollama and OpenAI-compatible backends\n- New `--candidates N`: generates N rewrites and selects the most lexically diverged (bigram Jaccard distance) with a length-drift guard\n- Stronger model hygiene: prefer local open-weight models and avoid any known-watermarked vendor, not just the suspected origin\n- Residual-risk reporting now distinguishes short\u002Fhighly predictable text (lower risk) from long, high-entropy prose (higher risk)\n- Docs updated in `SKILL.md`, `removal-matrix.md`, and `vendor-notes.md`; tests cover new prompts, divergence scoring, and candidate selection\n\n### [v0.3.0](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.3.0) — optional SynthID pixel scoring\n\n- Optional pixel-domain SynthID scorer via an external [`aloshdenny\u002Freverse-SynthID`](https:\u002F\u002Fgithub.com\u002Faloshdenny\u002Freverse-SynthID) checkout (`score_synthid.py`); surfaced in `inspect_image.py` \u002F `clean_image.py` with `REVERSE_SYNTHID_DIR` or `--synthid-dir`\n- `setup_synthid.sh` bootstrap (scorer-only dependencies; `--full` installs upstream requirements); `Dockerfile.synthid` plus `make docker-synthid-build` \u002F `docker-synthid-help`\n- Makefile `smoke-synthid` and `bootstrap-synthid` targets\n- Tests for the scorer adapter, CLI unavailable path, JSON parsing, and runtime errors\n- Docs: detection\u002Fscoring only (no pixel removal); upstream code is not bundled and remains under its non-commercial Research License\n\n### [v0.2.0](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.2.0) — c2patool false-positive fix\n\n- `image_meta.py`: `has_manifest` no longer flags `Error: No claim found` \u002F `No JUMBF data found` as a manifest (operator-precedence bug: the negative markers now veto every positive branch)\n- New `tests\u002Ftest_c2patool_report.py` (4 cases: no claim, no JUMBF, genuine manifest, tool absent)\n- Docs: fixed `c2patool` links (repo moved to `contentauth\u002Fc2pa-rs`); added a disclaimer on the quality cost of text-watermark removal\n\n### [v0.1.0](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.1.0) — packaging polish + provenance honesty\n\n- `Makefile` (`test` \u002F `smoke` \u002F `install-skill`) and `pytest.ini`\n- Fixture samples for Markdown, HTML, SVG; PDF degraded-clean test\n- Docs: industry **two-layer** model (hard-bound C2PA vs soft binding \u002F SynthID-media)\n- README residual-risk table + links to external verify tools\n- Reference: Institute of AI PM C2PA\u002FSynthID guide\n- Soft-binding and pixel\u002Faudio\u002Fvideo watermarks explicitly out of scope in skill\u002Fmatrix\u002Fethics\n\n### [v0.0.1](https:\u002F\u002Fgithub.com\u002Fguillaumemeyer\u002Fwatermarks-remover\u002Freleases\u002Ftag\u002Fv0.0.1) — initial multi-vendor release\n\n- Agent skill `remove-ai-marks` (replaces Claude-only `remove-claude-marks`)\n- **Layer A:** invisible Unicode \u002F bidi \u002F tag chars \u002F space homoglyphs (`inspect_text` \u002F `clean_text`)\n- **Layer B:** rewrite guidance + optional `rewrite_text.py` (print-prompt, Ollama, OpenAI-compatible)\n- **Files:** C2PA\u002FAI metadata strip for PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown\n- Unified `inspect_file.py` \u002F `clean_file.py`\n- Multi-vendor docs (Claude, Gemini\u002FSynthID-class, OpenAI, open-LLM)\n- Stdlib-first scripts; optional `c2patool` \u002F `exiftool`\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## References\n\n- [How Claude marks AI-generated content](https:\u002F\u002Fsupport.claude.com\u002Fen\u002Farticles\u002F16266773-how-claude-marks-ai-generated-content) (Anthropic)\n- Dathathri et al., [*Scalable watermarking for identifying large language model outputs*](https:\u002F\u002Fwww.nature.com\u002Farticles\u002Fs41586-024-08025-4) (SynthID-Text, Nature 2024)\n- Google AI for Developers, [*SynthID safeguards*](https:\u002F\u002Fai.google.dev\u002Fresponsible\u002Fdocs\u002Fsafeguards\u002Fsynthid) (Gemini API docs)\n- [C2PA](https:\u002F\u002Fc2pa.org\u002F) \u002F [c2patool](https:\u002F\u002Fgithub.com\u002Fcontentauth\u002Fc2pa-rs\u002Ftree\u002Fmain\u002Fcli)\n- Kirchenbauer et al., [*A Watermark for Large Language Models*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2301.10226)\n- [THU-BPM\u002FMarkLLM](https:\u002F\u002Fgithub.com\u002FTHU-BPM\u002FMarkLLM) (unified toolkit for evaluating LLM watermarking algorithms)\n- Pan et al., [*MarkDiffusion: An Open-Source Toolkit for Generative Watermarking of Latent Diffusion Models*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2509.10569) (JMLR) — the embedding toolkit this repo's optional image-watermark harness wraps — [code](https:\u002F\u002Fgithub.com\u002FTHU-BPM\u002FMarkDiffusion), [docs](https:\u002F\u002Fmarkdiffusion.readthedocs.io)\n- Zhang et al., [*Watermarks in the Sand: Impossibility of Strong Watermarking for Generative Models*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2311.04378v5) (ICML 2024)\n- Sander et al., [*Watermarking Makes Language Models Radioactive*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2402.14904) — watermarks survive fine-tuning and mark downstream models trained on watermarked data\n- Pan et al., [*Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation?*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2502.11598) — watermark-based provenance and protection against knowledge distillation\n- [google-deepmind\u002Fsynthid-text](https:\u002F\u002Fgithub.com\u002Fgoogle-deepmind\u002Fsynthid-text) (research reference; not used for detection here)\n- [aloshdenny\u002Freverse-SynthID](https:\u002F\u002Fgithub.com\u002Faloshdenny\u002Freverse-SynthID) (research reference)\n- Liu et al., [*Image Watermarks are Removable Using Controllable Regeneration from Clean Noise*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2410.05470) (ICLR 2025) — the pixel-regeneration method the optional CtrlRegen backend implements — [code](https:\u002F\u002Fgithub.com\u002Fyepengliu\u002FCtrlRegen)\n- Kassis & Hengartner, [*UnMarker: A Universal Attack on Defensive Image Watermarking*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2405.08363) (arXiv:2405.08363; IEEE S&P 2025) — a universal watermark attack compared on a different metric than CtrlRegen\n- Goonatilake & Ateniese, [*Removing the Watermark Is Not Enough: Forensic Stealth in Generative-AI Watermark Removal*](https:\u002F\u002Farxiv.org\u002Fabs\u002F2605.09203) (arXiv:2605.09203) — motivates the conservative-strength default: removal can still leave forensic traces\n- [mertizci\u002Fnoai-watermark](https:\u002F\u002Fgithub.com\u002Fmertizci\u002Fnoai-watermark) (CLI\u002FPython toolkit for SynthID\u002FStableSignature\u002FTreeRing removal and AI metadata stripping)\n- [0xROOTPLS\u002FDeSynth](https:\u002F\u002Fgithub.com\u002F0xROOTPLS\u002FDeSynth) (SynthID removal for OpenAI\u002FGoogle images)\n- Institute of AI PM, [*AI Content Provenance and Watermarking: The PM's Guide to C2PA and SynthID*](https:\u002F\u002Fwww.institutepm.com\u002Fknowledge-hub\u002Fai-content-provenance-watermarking) (two-layer industry model: C2PA + imperceptible watermark \u002F soft binding; SB 942 \u002F EU AI Act Art. 50 context","这是一个用于清除多厂商AI生成内容溯源痕迹的开源工具，支持去除文本中的隐形Unicode水印、统计类文本水印（如Claude、Gemini SynthID、OpenAI等模型嵌入的痕迹），以及各类文件格式中的元数据水印（C2PA、EXIF、XMP、文档属性等）。核心能力包括确定性Unicode净化、基于代理的文本重写、跨格式元数据剥离，采用Python实现，提供HTTP服务接口与轻量Agent技能集成。适用于内容创作者、开发者及企业对自有AI生成内容进行隐私脱敏和发布前合规清理。",2,"2026-08-21 02:30:03","CREATED_QUERY"]