mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-06-10 12:53:56 +02:00
70e8b3a517
Per the 2026-06-08 deep-research synthesis (docs/synthid-robust-identity- research-2026-06-08.md), the entire ArcFace-class identity-adapter ecosystem for SDXL is blocked from commercial use by InsightFace's non-commercial model packs (antelopev2 / buffalo_l). No commercial-safe ArcFace-grade identity stack exists today. The user explicitly opted into shipping a non-commercial restore path (research / personal use; raiw.cc must NOT install the extra). Architectural choice: InstantID over PhotoMaker-V2 as the default. - PhotoMaker-V2 (CLIP+ArcFace dual encoder, txt2img only): documented upstream identity drift on Asian male faces, visually confirmed in our cert sweep (tatsunari rendered as a generic woman; group photo collapsed into a patchwork). - InstantID (ArcFace cross-attention + landmark ControlNet): semantic identity branch + spatial weak landmark control, decoupled. Per InstantID paper (arXiv:2401.07519) and the research report, stronger identity fidelity on single portraits. Critically: NO original face pixels enter the diffusion (ArcFace embedding is semantic, landmark stick figure is pure geometry), so SynthID is not transported. Implementation: - New `src/remove_ai_watermarks/instantid_restore.py` mirrors the `photomaker_restore.py` shape (lazy singletons for pipeline + FaceAnalysis, per-face crop + _composite_faces from photomaker_restore). Loads the InstantID community pipeline via `DiffusionPipeline.from_pretrained( custom_pipeline="pipeline_stable_diffusion_xl_instantid")` -- no upstream Python package needed; diffusers fetches the file from its community examples. - New `instantid` extra in pyproject (insightface + onnxruntime + huggingface-hub). NON-COMMERCIAL block in the comment explains why. - CLI: `--restore-faces-method [instantid|photomaker]`, default `instantid`. Both methods explicitly labeled NON-COMMERCIAL in the help text. - Engine: dispatch on `restore_faces_method` to either `_restore_faces_instantid` or `_restore_faces_photomaker`. - 9 control-flow tests for InstantID without model download (mirror the photomaker_restore.py test pattern + draw_kps helper checks). 587/587 pass. Diffusers-0.38 compat verified by upstream code inspection: the InstantID pipeline inherits from `StableDiffusionXLControlNetPipeline`, uses only public diffusers APIs (`encode_prompt`, `prepare_image`, `prepare_latents`, `get_guidance_scale_embedding`), uses legacy attention processor API which diffusers preserves for backward compat. No PhotoMaker-V1-style internal text_encoder access. End-to-end execution will be validated by the Modal cert sweep in the next step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
241 lines
11 KiB
TOML
241 lines
11 KiB
TOML
[project]
|
|
name = "remove-ai-watermarks"
|
|
version = "0.8.9"
|
|
description = "Remove visible and invisible AI watermarks from images (Gemini / Nano Banana, ChatGPT, Stable Diffusion)"
|
|
readme = "README.md"
|
|
requires-python = ">=3.10"
|
|
license = {text = "MIT"}
|
|
classifiers = [
|
|
"License :: OSI Approved :: MIT License",
|
|
"Operating System :: OS Independent",
|
|
"Programming Language :: Python :: 3.10",
|
|
"Programming Language :: Python :: 3.11",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Programming Language :: Python :: 3.13",
|
|
"Topic :: Multimedia :: Graphics",
|
|
"Topic :: Scientific/Engineering :: Image Processing",
|
|
]
|
|
dependencies = [
|
|
"pillow>=10.0.0",
|
|
"piexif>=1.1.3",
|
|
"numpy>=1.24.0",
|
|
"opencv-python-headless>=4.8.0",
|
|
"click>=8.0.0",
|
|
"python-dotenv>=1.0.0",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
gpu = [
|
|
"torch>=2.0.0",
|
|
# The default PyPI torch wheel is a CPU/CUDA build. To drive an Intel GPU
|
|
# (Arc / Data Center) via ``--device xpu`` you need an XPU-enabled torch
|
|
# from PyTorch's XPU wheel index (Linux/Windows only -- there is no macOS
|
|
# XPU build). Install that build first, then this extra (torch is then
|
|
# already satisfied and won't be re-pulled):
|
|
# pip install torch --index-url https://download.pytorch.org/whl/xpu
|
|
# pip install 'remove-ai-watermarks[gpu]'
|
|
# uv users can target the ``pytorch-xpu`` index declared under [tool.uv]:
|
|
# uv pip install torch --index-url https://download.pytorch.org/whl/xpu
|
|
"diffusers>=0.38.0",
|
|
# diffusers 0.38's auto-pipeline registry imports ``Qwen3VLForConditional
|
|
# Generation`` (its ``nucleusmoe_image`` pipeline), which only exists in
|
|
# transformers 5.x -- so ``from diffusers import AutoPipelineForImage2Image``
|
|
# fails on transformers 4.x. The real SDXL-loading break was NOT transformers
|
|
# 5.x but the tokenizers *release candidate* (0.23.0rc0) that the global
|
|
# ``prerelease = "allow"`` drags in: its CLIP tokenizer raises
|
|
# ``RobertaProcessing.__new__() got an unexpected keyword argument 'cls'``.
|
|
# Cap tokenizers to the stable 0.22 line (transformers 5.x accepts
|
|
# >=0.22,<=0.23.0) so the rc is excluded while SDXL still loads.
|
|
"transformers>=5,<6",
|
|
"tokenizers>=0.22,<0.23",
|
|
"accelerate>=0.25.0",
|
|
"safetensors",
|
|
]
|
|
# Open invisible-watermark (imwatermark) decoder for detecting the DWT-DCT
|
|
# watermarks embedded by Stable Diffusion / SDXL / FLUX. Optional because it
|
|
# pulls non-headless opencv AND torch (invisible-watermark declares torch a hard
|
|
# dependency, and WatermarkDecoder eagerly imports rivaGan -> torch at import
|
|
# time, so the dwtDct-only detect path still needs torch present even though it
|
|
# never runs on GPU). So `detect` alone pulls torch -- no need to add `gpu` for
|
|
# detection. identify() guards the import and skips the signal when absent.
|
|
detect = [
|
|
"invisible-watermark>=0.2.0",
|
|
]
|
|
# Adobe TrustMark decoder -- the open, keyless watermark behind Adobe Durable
|
|
# Content Credentials (soft-binding alg ``com.adobe.trustmark.P``). Optional
|
|
# because it pulls torch and downloads model weights on first use. identify()
|
|
# guards the import and skips the TrustMark signal when absent.
|
|
trustmark = [
|
|
"trustmark>=0.8.0",
|
|
]
|
|
# Universal region eraser backend -- big-LaMa via onnxruntime (Carve/LaMa-ONNX,
|
|
# Apache-2.0). CPU, no torch. Model (~200 MB) is downloaded on first use and
|
|
# cached by huggingface_hub; it is never bundled in this repo. The default cv2
|
|
# eraser backend needs none of this.
|
|
lama = [
|
|
"onnxruntime>=1.16.0",
|
|
"huggingface-hub>=0.20.0",
|
|
]
|
|
# **NON-COMMERCIAL.** Optional PhotoMaker-V2 SynthID-robust face-identity post-pass.
|
|
# PhotoMaker-V2's ID encoder pulls an InsightFace ArcFace embedding at runtime, and
|
|
# the pretrained InsightFace model packs (antelopev2, buffalo_l) are released under a
|
|
# non-commercial / research-only license. A paid service (raiw.cc, any monetized SaaS)
|
|
# MUST NOT use this extra. See `src/remove_ai_watermarks/photomaker_restore.py` and
|
|
# `docs/synthid-robust-identity-research.md`. The PhotoMaker adapter weights
|
|
# (photomaker-v2.bin) are Apache-2.0 and download on first use; the InsightFace model
|
|
# packs download on first FaceAnalysis() (only triggered inside PhotoMaker's V2 forward).
|
|
# Pins beyond the upstream PhotoMaker package itself patch missing declarations that
|
|
# would otherwise break the load chain (verified empirically via the Modal cert sweep
|
|
# 2026-06-04): einops (used in forward), peft (required by diffusers.fuse_lora),
|
|
# onnxruntime (transitive via insightface), and insightface itself (required for the
|
|
# package's __init__.py to even import).
|
|
photomaker = [
|
|
"photomaker @ git+https://github.com/TencentARC/PhotoMaker.git",
|
|
"huggingface-hub>=0.20.0",
|
|
"einops>=0.7.0",
|
|
"insightface>=0.7.3",
|
|
"onnxruntime>=1.16.0",
|
|
"peft>=0.10.0",
|
|
]
|
|
# **NON-COMMERCIAL.** Optional InstantID SynthID-robust face-identity post-pass.
|
|
# InstantID adapter weights (IdentityNet ControlNet + ip-adapter.bin) are Apache-2.0
|
|
# from InstantX/InstantID on HuggingFace, BUT the runtime depends on InsightFace's
|
|
# antelopev2 ArcFace pack (non-commercial / research-only). InstantX's maintainers
|
|
# explicitly acknowledged this on HF (discussion #2) and stated intent to retrain
|
|
# on commercial embedders -- as of the 2026-06-08 deep-research synthesis
|
|
# (docs/synthid-robust-identity-research-2026-06-08.md) that retrain has not
|
|
# shipped. A paid service (raiw.cc, any monetized SaaS) MUST NOT use this extra.
|
|
# See `src/remove_ai_watermarks/instantid_restore.py`.
|
|
#
|
|
# Compared to the `photomaker` extra: InstantID adds spatial landmark conditioning
|
|
# alongside the ArcFace semantic branch, giving stronger identity fidelity on
|
|
# single portraits per the InstantID paper (arXiv:2401.07519). Both extras are
|
|
# non-commercial; pick `instantid` by default for better identity, `photomaker`
|
|
# when the InstantID community pipeline can't load.
|
|
#
|
|
# Loads via diffusers' community-pipeline mechanism (no upstream `instantid`
|
|
# Python package on PyPI). Only direct deps are insightface (MIT code, the
|
|
# non-commercial blocker is its MODEL packs) + onnxruntime (transitive via
|
|
# insightface) + huggingface-hub (weights download).
|
|
instantid = [
|
|
"insightface>=0.7.3",
|
|
"onnxruntime>=1.16.0",
|
|
"huggingface-hub>=0.20.0",
|
|
]
|
|
# Optional pre-diffusion super-resolution for small inputs (Real-ESRGAN). Loaded via
|
|
# spandrel (MIT) -- a pure model-loader with NO basicsr dependency (it pulls only
|
|
# torch / torchvision / safetensors / numpy / einops).
|
|
# The Real-ESRGAN weights (BSD-3-Clause) download on first use and are cached; they
|
|
# are never bundled. CPU works but is slow on large inputs -- it is meant for the
|
|
# pre-diffusion upscale of SMALL inputs (and the GPU worker). Guarded by
|
|
# upscaler.is_available(); the default upscaler stays Lanczos (cv2, no deps). The
|
|
# weights are fetched with torch.hub (bundled with spandrel's torch), so no extra
|
|
# download dependency is needed.
|
|
esrgan = [
|
|
"spandrel>=0.3.0",
|
|
]
|
|
dev = [
|
|
"pytest>=8.0.0",
|
|
"pytest-cov>=4.1.0",
|
|
"ruff>=0.4.0",
|
|
"pyright>=1.1.0",
|
|
"invisible-watermark>=0.2.0",
|
|
]
|
|
all = ["remove-ai-watermarks[gpu,detect,trustmark,lama,dev]"]
|
|
|
|
# diffusers 0.38.0 (security fix for GHSA-98h9-4798-4q5v) declares a dependency
|
|
# on safetensors>=0.8.0rc0 — a pre-release. Allow pre-releases globally so the
|
|
# resolver can satisfy that. Drop once diffusers publishes a release with a
|
|
# stable safetensors pin (or once safetensors 0.8.0 stable is out).
|
|
[tool.uv]
|
|
prerelease = "allow"
|
|
|
|
# PyTorch Intel-GPU (XPU) wheel index. ``explicit = true`` keeps it inert for
|
|
# the default CPU/CUDA install: uv consults it only when a torch install
|
|
# explicitly targets it (see the ``gpu`` extra comment), so it does not alter
|
|
# the locked CPU/CUDA resolution. Linux/Windows only -- no macOS XPU build.
|
|
[[tool.uv.index]]
|
|
name = "pytorch-xpu"
|
|
url = "https://download.pytorch.org/whl/xpu"
|
|
explicit = true
|
|
|
|
[project.scripts]
|
|
remove-ai-watermarks = "remove_ai_watermarks.cli:main"
|
|
|
|
[project.urls]
|
|
Repository = "https://github.com/wiltodelta/remove-ai-watermarks"
|
|
|
|
[build-system]
|
|
# Pin hatchling < 1.31. hatchling 1.30.0 made Metadata-Version 2.5 (PEP 794) the
|
|
# default, which the twine bundled in pypa/gh-action-pypi-publish@release/v1 rejects
|
|
# ("'2.5' is not a valid Metadata-Version"), failing the v0.8.3 PyPI upload
|
|
# (2026-06-01) when unpinned requires = ["hatchling"] pulled 1.30.0. hatchling 1.30.1
|
|
# reverted the default to 2.4 ("kept at 2.4 until more tools support 2.5"), and
|
|
# 1.27-1.29 were always 2.4 -- so < 1.31 keeps `uv build` on a 2.4-emitting hatchling
|
|
# (it resolves to the latest allowed, 1.30.1). The publish workflow now uses
|
|
# `uv publish`, whose uploader accepts 2.5, so this pin is belt-and-suspenders, not
|
|
# load-bearing: keeping it makes the first uv-publish release ship 2.4 metadata
|
|
# (isolating the uploader swap from the metadata-version bump). Drop to
|
|
# `requires = ["hatchling"]` once that release confirms the path.
|
|
requires = ["hatchling<1.31"]
|
|
build-backend = "hatchling.build"
|
|
|
|
# Allow the `photomaker` extra to reference the upstream git URL directly (the
|
|
# TencentARC/PhotoMaker package is not on PyPI). The extra itself is NON-COMMERCIAL
|
|
# (see the photomaker block above and `photomaker_restore.py`).
|
|
[tool.hatch.metadata]
|
|
allow-direct-references = true
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["src/remove_ai_watermarks"]
|
|
|
|
[tool.hatch.build.targets.sdist]
|
|
# Keep the source distribution small: ship the package + metadata, not the
|
|
# committed test corpora / calibration captures under data/ (tens of MB --
|
|
# synthid_corpus images + the visible-mark captures), which pushed the 0.8.0
|
|
# sdist past PyPI's per-project file-size limit (the wheel ships only src/).
|
|
exclude = ["/data"]
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
pythonpath = ["src"]
|
|
addopts = "-v --tb=short"
|
|
|
|
[tool.ruff]
|
|
target-version = "py310"
|
|
line-length = 120
|
|
exclude = ["_refs"]
|
|
|
|
[tool.ruff.lint]
|
|
select = ["E", "F", "B", "I", "S", "UP", "SIM", "RET", "COM", "C4", "G", "PT", "PIE", "T20", "DTZ", "ICN", "TCH", "RUF", "ANN"]
|
|
ignore = [
|
|
"COM812", # missing trailing comma (conflicts with ruff formatter)
|
|
"ANN401", # typing.Any — sometimes unavoidable with third-party libs
|
|
]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
"tests/*.py" = ["ANN", "S101", "S105", "S106", "S108"]
|
|
"src/remove_ai_watermarks/noai/watermark_remover.py" = ["S603", "S606", "S607", "T201"] # subprocess calls for auto-install/CUDA fix
|
|
"src/remove_ai_watermarks/noai/c2pa.py" = ["S110"] # try-except-pass for corrupt file handling
|
|
|
|
[tool.ruff.format]
|
|
quote-style = "double"
|
|
indent-style = "space"
|
|
|
|
[tool.pyright]
|
|
pythonVersion = "3.10"
|
|
typeCheckingMode = "strict"
|
|
exclude = ["_refs"]
|
|
|
|
[[tool.pyright.executionEnvironments]]
|
|
root = "tests"
|
|
extraPaths = ["."]
|
|
reportAttributeAccessIssue = false
|
|
reportOptionalSubscript = false
|
|
reportOptionalMemberAccess = false
|
|
reportArgumentType = false
|
|
reportUnknownMemberType = false
|
|
reportUnknownArgumentType = false
|
|
reportUnknownVariableType = false
|
|
reportMissingTypeArgument = false
|