Tools for finding and removing AI provenance signals from files you own. Four channels are covered: hidden Unicode in text, statistical token watermarks, visible marks burned into images, and metadata such as C2PA, EXIF, and XMP.
The core runs on Python 3.10+ using the standard library only. Anything that needs a network call, a model, a GPU, or a system binary sits behind an adapter you opt into, so the default install has no dependencies.
Honesty contract: deterministic cleaners report exactly what they removed. Rewrite, inpainting, and detector-evasion methods are labeled best-effort. Nothing here certifies that a vendor detector will fail.
| Layer | Target | Method | Result class |
|---|---|---|---|
| A | Hidden Unicode, bidi controls, tags, exotic spaces | Context-aware deterministic scrub | Verifiable |
| B | Token-distribution text watermarks | Paraphrase, back-translation, structural rewrite, or TSAPA-style evolutionary search | Best-effort |
| V | Visible image logos and overlays | Mask, hole fill, dilation, inpaint, restore | Mask removal verifiable, fidelity best-effort |
| M | C2PA, EXIF, XMP, document properties | Format-aware metadata rewrite | Verifiable per format |
| Soft binding | Remote manifests, in-content binding | Detection and warning only | Detection only |
| SynthID | Pixel-domain SynthID-class signal | Optional external adapter | Score only |
Optional edges, none required by the core:
pypdffor structural PDF rewriteexiftoolandc2patoolas system tools- a local Ollama or OpenAI-compatible endpoint for Layer B
- an external detector, LaMa, MI-GAN, or diffusion command for visible marks
gradiofor the demo UI
git clone https://github.com/Pythoughts-labs/watermark-remover.git
cd watermark-remover
SCRIPTS=skills/remove-ai-marks/scripts
# Inspect before changing anything
python3 "$SCRIPTS/inspect_file.py" draft.md
python3 "$SCRIPTS/inspect_file.py" image.png --soft-binding
# Unified clean
python3 "$SCRIPTS/clean_file.py" draft.md -o draft.cleaned.md
python3 "$SCRIPTS/clean_file.py" image.png -o image.cleaned.png# Project-local (Grok Build)
mkdir -p .grok/skills
ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks
# User-global
mkdir -p ~/.grok/skills
ln -sfn "$(pwd)/skills/remove-ai-marks" ~/.grok/skills/remove-ai-marksInvoke with /remove-ai-marks, or ask to inspect or clean C2PA, hidden Unicode, visible marks, or statistical text marks.
Directory mode preserves relative paths, skips its own .cleaned.*, .mask.*, and .bak artifacts, rejects output collisions, and returns non-zero when any file fails or retains a requested risk signal.
# All supported files below a directory
python3 "$SCRIPTS/clean_file.py" ./inputs -o ./cleaned --recursive
# Glob and extension allow-list
python3 "$SCRIPTS/clean_file.py" ./inputs -o ./cleaned \
--recursive --glob "*.png" --extensions png
# Inspect a tree
python3 "$SCRIPTS/inspect_file.py" ./inputs --recursive --glob "*.md" --jsonpython3 "$SCRIPTS/inspect_text.py" draft.txt --json
python3 "$SCRIPTS/clean_text.py" draft.txt -o draft.cleaned.txt --statsLayer A removes or normalizes configured hidden carriers. Some Unicode is meaningful, so preservation rules are context-aware rather than blanket deletion: ZWJ and variation selectors affect emoji and orthographies, and bidi controls affect display order.
# No model call: emit an execution prompt
python3 "$SCRIPTS/rewrite_text.py" draft.txt \
--backend print-prompt --strength paraphrase
# TSAPA-style operator pack, still no model call
python3 "$SCRIPTS/rewrite_text.py" draft.txt \
--backend print-prompt --strength tsapa --generations 5 --population 12
# Execute against a local OpenAI-compatible endpoint
WATERMARKS_REWRITE_BACKEND=openai-compatible \
WATERMARKS_REWRITE_BASE_URL=http://127.0.0.1:8080 \
WATERMARKS_REWRITE_MODEL=my-local-model \
python3 "$SCRIPTS/rewrite_text.py" draft.txt \
--strength tsapa --generations 5 --population 12
# Qwen/Transformers-compatible servers: suppress reasoning preambles
WATERMARKS_REWRITE_DISABLE_THINKING=true \
python3 "$SCRIPTS/rewrite_text.py" draft.txt \
--backend openai-compatible --base-url http://127.0.0.1:8080 \
--model my-thinking-model --strength paraphraseThe TSAPA-style engine is a real multi-objective evolutionary loop:
- Generate a diverse candidate population.
- Score attack fitness: PLL, n-gram diversity, lexical diversity.
- Score fidelity: embedding cosine similarity, with a labeled shingle-Jaccard fallback.
- Apply NSGA-II non-dominated sorting and crowding distance.
- Cross over at sentence boundaries, mutate the lowest-PLL sentence.
- Select the Pareto knee point.
A logprobs-capable /v1/completions endpoint supplies PLL, and /v1/embeddings supplies semantic similarity. Either can fail independently and degrade to an explicitly labeled standard-library proxy. --disable-thinking (or WATERMARKS_REWRITE_DISABLE_THINKING=true) opts into the Qwen/Transformers chat_template_kwargs.enable_thinking=false extension; it is never sent by default to generic OpenAI-compatible servers.
Cost: Layer B replaces the original wording and can flatten voice or precision. Prefer a non-origin model so the rewrite does not re-stamp the same scheme.
python3 "$SCRIPTS/perturb_text.py" draft.txt \
--mode zero-width --strength 0.10 --seed 42This is an opt-in anti-watermark transform inspired by 2026 character-perturbation research. It intentionally adds artifacts that Layer A removes. zero-width and space-swap are Layer-A reversible, confusable and case are not. It is not a hygiene pass and carries no detector guarantee.
morphomod.py never guesses a watermark region. Supply one of:
--mask mask.pgm|mask.png(white means remove)--box X,Y,W,H--detect-command '...'with{input},{mask},{prompt}placeholders
# Stdlib texture backend (default in clean_file): nearby-patch search
python3 "$SCRIPTS/morphomod.py" input.png -o output.png \
--box 900,920,120,60 --dilation 3 --backend texture
# Uniform-background fallback
python3 "$SCRIPTS/morphomod.py" input.png -o output.png \
--box 900,920,120,60 --dilation 3 --backend simple
# Production adapter: external detector and inpainter
python3 "$SCRIPTS/morphomod.py" input.png -o output.png \
--detect-command 'detector --input "{input}" --output "{mask}"' \
--backend external \
--command 'inpainter --image "{input}" --mask "{mask}" --output "{output}"'
# Combined visible pass, then metadata clean
python3 "$SCRIPTS/clean_file.py" input.png -o output.png \
--visible-mask mask.pgm --dilate 3 --visible-backend textureThe stdlib dilation is non-cascading and runs in O(width × height). PNG decoding supports non-interlaced 8-bit grayscale, RGB, and RGBA. The default texture backend searches nearby patches by boundary error and fully replaces the refined mask; optional feathering is available at the module interface. simple is retained for uniform backgrounds only. Neither is marketed as LaMa quality. JPEG visible cleaning requires an external backend, and that backend owns JPEG compositing.
Paper-reported MorphoMod improvements are not this implementation's measured results. The report includes initial and refined mask pixels and requires manual fidelity review.
# PNG, JPEG, HEIC, HEIF, AVIF
python3 "$SCRIPTS/clean_image.py" photo.heic -o photo.cleaned.heic
# Any supported format
python3 "$SCRIPTS/clean_file.py" document.pdf -o document.cleaned.pdf
# Detect soft-binding or remote-manifest risk
python3 "$SCRIPTS/inspect_soft_binding.py" image.png --jsonC2PA facts the parser relies on:
- Manifest Stores use JUMBF and BMFF structures, claims and assertions, and COSE signatures.
- JPEG embeds through APP11/JUMBF.
- PNG uses the private ancillary
caBXchunk, not generictEXt. - HEIF and AVIF are ISO-BMFF. Cleaning neutralizes matching JUMBF/C2PA UUID boxes and direct-file Exif and XMP extents in place, preserving offsets. Unsupported external or idat metadata layouts fail closed.
Soft-binding removal remains out of scope. The inspector warns when an in-content binding or remote manifest may re-link provenance after hard-bound metadata is stripped.
PDF cleaning uses this fallback order:
exiftool- optional
pypdffull-document clone (outlines, forms, attachments, and catalog retained, docinfo and XMP removed) - byte-exact unchanged copy with an explicit residual warning
python3 -m pip install pypdfEncrypted PDFs are never regex-edited.
The external aloshdenny/reverse-SynthID checkout is not bundled and stays under its upstream non-commercial research license.
SCRIPTS=skills/remove-ai-marks/scripts
"$SCRIPTS/setup_synthid.sh"
REVERSE_SYNTHID_DIR=~/reverse-SynthID \
~/reverse-SynthID/.venv/bin/python "$SCRIPTS/score_synthid.py" shot.pngOr build the scorer locally:
make docker-synthid-build
docker run --rm -v "$(pwd):/data" watermark-remover-synthid-scorer /data/shot.pngThis is scoring only. That repository's carrier model and success figures are maintainer-reported reverse-engineering claims, not public Google architecture and not an independent guarantee.
| Format | Inspect and clean behavior |
|---|---|
| PNG | caBX, text, XMP, and EXIF chunks, plus the optional visible pipeline |
| JPEG | APP11/JUMBF and APP metadata, external visible backend supported |
| HEIC, HEIF, AVIF | ISO-BMFF brands, JUMBF/C2PA UUID boxes, direct-file Exif and XMP extents. Unsupported external or idat layouts fail closed |
| SVG | <metadata>, XMP, provenance-like comments |
| XMP and docinfo via exiftool or a full-document pypdf clone, otherwise an unchanged copy | |
| DOCX | docProps cleaned, customXml inspected and preserved to avoid application-data loss |
| ODT | meta.xml and generator-like metadata |
| HTML | meta tags, provenance JSON-LD, data-ai* attributes |
| Markdown | AI-like YAML frontmatter plus a Layer A body pass |
| Text and code | Layer A, optional Layer B or character perturbation |
Context-aware Unicode scrubbing. text_unicode.py classifies every hidden carrier rather than deleting a blocklist. Zero-width joiners hold emoji sequences together and variation selectors change glyph form, so those survive. Tag characters and bidi overrides do not. Bidi handling follows the same directional model browsers expose through unicode-bidi, and cleaned text is normalized to NFC, the form described under String.prototype.normalize().
NSGA-II inside a text rewriter. tsapa.py is a genuine multi-objective loop, not a prompt template: population generation, non-dominated sorting, crowding distance, crossover at sentence boundaries, mutation targeted at the weakest sentence, and Pareto knee selection. Attack strength and semantic fidelity are optimized as two separate objectives so neither silently wins.
Scoring that names its own fallback. Pseudo-log-likelihood comes from an OpenAI-compatible endpoint with logprobs, and cosine similarity from an embeddings endpoint. Either can fail on its own and drop to a standard-library proxy, which is reported as a proxy rather than passed off as the real measurement.
Non-cascading dilation in pure Python. morphomod.py dilates a mask in a single pass against the original buffer, so growth does not compound across iterations. It runs in O(width × height) with no array library.
Patch-based inpainting with no model. The default visible backend searches nearby patches by boundary error, picks the best match, and fully replaces the refined mask so watermark pixels cannot bleed through. It is not LaMa quality and is not sold as such, but it needs nothing installed.
In-place ISO-BMFF neutralization. heif_meta.py overwrites JUMBF and C2PA UUID boxes and direct-file Exif and XMP extents without moving anything, so every byte offset in the file stays valid. Layouts it cannot prove safe fail closed instead of being rewritten.
PNG chunk surgery. image_meta.py walks the chunk stream, removes the private ancillary caBX chunk that C2PA actually uses in PNG, and recomputes CRCs with zlib.crc32.
Markup-aware container cleaning. container_meta.py targets SVG <metadata> elements, HTML <meta> tags, JSON-LD provenance blocks, and data-* attributes matching data-ai*. DOCX customXml is inspected and kept, because deleting it loses application data.
Fallback chains that degrade loudly. PDF cleaning tries exiftool, then a full-document pypdf clone, then a byte-exact copy with an explicit residual warning. The third case still returns a file, and still tells you nothing was removed.
The banner is drawn, not exported. assets/banner.svg uses two overlapping <clipPath> regions over one duplicated block of text, so the same codepoints render dim on the left and lit on the right. That puts the scrub line in the middle with no gradient mask and no raster asset.
Nothing below is required to run the core.
- pypdf for a structural PDF clone that keeps outlines, forms, and attachments while dropping docinfo and XMP.
- ExifTool and c2patool as system binaries, used when present.
- Gradio for
demo.py, which calls the same modules as the CLI rather than reimplementing them. - Ollama or any OpenAI-compatible endpoint for Layer B execution. Tests inject a fake callable instead.
- LaMa and MI-GAN as external inpainting commands behind the
externalbackend. - reverse-SynthID for optional pixel scoring. Not bundled, and non-commercial upstream.
- pytest and Ruff for the gate behind
make check. - The C2PA specification and JUMBF (ISO/IEC 19566-5) define the box structures the parsers walk.
The banner loads no web font. It requests a system monospace stack in order: ui-monospace, SF Mono, Menlo, Consolas, DejaVu Sans Mono, then generic monospace. GitHub proxies README images, so an external font request would be stripped anyway.
watermark-remover/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ └── workflows/
├── assets/
├── research/
├── skills/
│ └── remove-ai-marks/
│ ├── references/
│ └── scripts/
├── tests/
│ └── fixtures/
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DESIGN.md
├── Dockerfile.synthid
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── demo.py
├── pyproject.toml
├── pytest.ini
├── requirements-demo.txt
└── requirements-test.txt
skills/remove-ai-marks/ is the whole product. It is laid out as an agent skill so it can be symlinked into .grok/skills or ~/.grok/skills and invoked directly, but scripts/ is a set of ordinary CLIs that work on their own. references/ holds the source notes the parsers were built from, including ethics.md and the vendor behavior notes.
research/ is intentionally gitignored local evidence and dogfood material; the durable claims discipline is captured in DESIGN.md. assets/ holds repository images. tests/fixtures/ holds the small binary files the format parsers are tested against.
| Channel | What this does | What can remain |
|---|---|---|
| Hidden text | Deterministic scrub | Meaningful Unicode kept by policy |
| Statistical text | Best-effort rewrite or evolutionary search | A strong or updated detector signal |
| Visible images | Mask, dilation, inpaint | Missed regions, inpaint artifacts |
| Hard-bound metadata | Format-aware stripping | Soft bindings, remote manifests, pixel marks |
| Pixel SynthID-class | Optional external score | The pixel, audio, and video watermark itself |
| Training backdoors | Nothing | Out of scope |
No public universal text detector exists, and a detector miss does not prove every trace is gone. Stronger attacks trade fidelity for lower detectability, and provider implementations keep changing.
Built for privacy, hygiene, accessibility, and research on content you own. Not for academic fraud, evading disclosure requirements, or claiming output is proven human-written. See ethics.md.
python3 -m pip install -r requirements-demo.txt
python3 demo.pyThe demo calls the same modules as the CLI, it is not a second implementation. Layer B prompt generation is limited to plain-text uploads, and binary document extraction is deliberately not guessed.
python3 -m venv .venv
.venv/bin/pip install -r requirements-test.txt
make checkDESIGN.md, architecture, seams, guarantee classes, roadmapSKILL.md, agent workflowmark-classes.md,removal-matrix.md,vendor-notes.mdCONTRIBUTING.mdandSECURITY.md
MIT, see LICENSE.
- Anthropic, How Claude marks AI-generated content
- Dathathri et al., Scalable watermarking for identifying large language model outputs (SynthID-Text, Nature 2024)
- Zhao et al., Invisible Image Watermarks Are Provably Removable Using Generative AI (NeurIPS 2024)
- UnMarker (IEEE S&P 2025)
- CtrlRegen (ICLR 2025)
- C2PA specification
- Kirchenbauer et al., A Watermark for Large Language Models