[project] name = "remove-ai-watermarks" version = "0.26.0" description = "AI watermark remover for visible, invisible, and provenance marks in images and video" readme = "README.md" requires-python = ">=3.10.1" license = {text = "Apache-2.0"} keywords = [ "ai-watermark", "ai-watermark-remover", "watermark-remover", "watermark-removal", "remove-watermark", "synthid", "c2pa", "content-credentials", "nano-banana", "gemini", "gemini-watermark", "ai-metadata", "metadata-removal", "provenance", "exif", "stable-diffusion", "comfyui", "ai-detection", ] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Image Processing", "Topic :: Security", "Topic :: Utilities", ] dependencies = [ "pillow>=10.0.0", "piexif>=1.1.3", "click>=8.0.0", "python-dotenv>=1.0.0", # Official C2PA reader (Content Authenticity Initiative, MIT/Apache-2.0). The # primary, spec-tracking manifest parser for the identify/metadata path; the # hand-rolled caBX/CBOR scanner in _internal/c2pa.py is kept only as a fallback for # synthetic/partial blobs the validator rejects. Binary wheel (Rust), but the # import is light (no torch/numpy) so it fits the dependency-light identify # host. Prebuilt wheels cover the full CI matrix (linux/macos/windows). "c2pa-python>=0.35.0", ] [project.optional-dependencies] pixels = [ "numpy>=1.24.0", "opencv-python-headless>=4.8.0", ] # Optional HEIC/AVIF pixel decode. Metadata scanning handles these containers # without this plugin; combine `heif` with any pixel feature only when needed. heif = [ "pillow-heif>=0.13.0", ] visible = ["remove-ai-watermarks[pixels]"] # Video visible removal uses the shared pixel runtime. PyAV packetizes processed # VFR frames with explicit PTS before system ffmpeg encodes them. PyAV 18 requires # Python 3.11; the 16.x wheel line still covers Python 3.10. video = [ "remove-ai-watermarks[visible]", "av>=16,<17; python_version < '3.11'", "av>=18,<19; python_version >= '3.11'", ] # Open DWT-DCT watermarks used by Stable Diffusion / SDXL / FLUX. The in-tree # decoder avoids the upstream invisible-watermark package's mandatory torch and # non-headless OpenCV dependencies. detect = [ "remove-ai-watermarks[pixels]", "PyWavelets>=1.1.1", ] diffusion = [ "remove-ai-watermarks[pixels]", # A CUDA-enabled torch build is required: invisible-watermark removal has no # CPU, MPS or XPU path. The default PyPI wheel carries CUDA on Linux/Windows; # on macOS there is no CUDA build and this extra installs only for the # non-diffusion imports it shares. "torch>=2.0.0", "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", ] # Full two-stage high-fidelity profile: Qwen-Image-2512 Lightning + DiffSynth # Canny ControlNet for the frame, then SAM-masked Z-Image Turbo face repair. # CUDA-only and intentionally separate from the normal diffusion extra because the # additional model stack and DiffSynth runtime are large. qwen-zimage = [ "remove-ai-watermarks[diffusion]", "diffsynth>=2.0.17,<3", "torchvision>=0.20.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 = [ "remove-ai-watermarks[visible]", # ONNX Runtime 1.24 dropped CPython 3.10 wheels; keep the project's # supported 3.10 line on the last compatible release series. "onnxruntime>=1.16.0,<1.24; python_version < '3.11'", "onnxruntime>=1.16.0; python_version >= '3.11'", "huggingface-hub>=0.20.0", ] # Lightweight inpaint backend -- MI-GAN via onnxruntime (andraniksargsyan/migan, # MIT). CPU, no torch. Model (~28 MB) downloaded on first use and cached by # huggingface_hub; never bundled. ~700-950 MB peak RAM / ~0.19 s/call -- the # memory-tight learned tier (vs big-LaMa's ~4.7 GB). Select it explicitly when # LaMa, the quality-first `auto` choice, is too large. Same runtime as `lama`. migan = [ "remove-ai-watermarks[visible]", "onnxruntime>=1.16.0,<1.24; python_version < '3.11'", "onnxruntime>=1.16.0; python_version >= '3.11'", "huggingface-hub>=0.20.0", ] dev = [ "remove-ai-watermarks[video]", "remove-ai-watermarks[detect]", "pytest>=8.0.0", "pytest-cov>=4.1.0", "pytest-xdist>=3.5.0", "packaging>=24.0", "ruff>=0.4.0", "pyright>=1.1.0", "invisible-watermark>=0.2.0", # maintain.sh helpers; they only support newer Pythons, so gate them by # marker to keep the py3.10 resolution (and CI matrix) solvable. "uv-outdated>=0.1.0; python_version >= '3.12'", "uv-secure>=0.12.0; python_version >= '3.12'", ] # ``qwen-zimage`` already pulls ``diffusion``; naming both would suggest diffusion is # independently sufficient for a removal, which it is not. all = ["remove-ai-watermarks[video,heif,detect,trustmark,qwen-zimage,lama,migan]"] [project.scripts] remove-ai-watermarks = "remove_ai_watermarks.cli:main" [project.urls] Repository = "https://github.com/wiltodelta/remove-ai-watermarks" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/remove_ai_watermarks"] [tool.hatch.build.targets.sdist] # Keep the source distribution small and public-safe: ship tracked source and # metadata, not corpora or local research/session artifacts. The wheel ships # only src/. include = [ "/src", "/LICENSE", "/README.md", "/pyproject.toml", ] exclude = [ "/data", "/tmp", "/.sc", ] [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["src"] addopts = "-v --tb=short" [tool.ruff] target-version = "py310" line-length = 120 exclude = ["_refs"] extend-exclude = ["*.md"] [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] "scripts/*.py" = ["G004", "S108", "S310", "T20"] "tests/*.py" = ["ANN", "S101", "S105", "S106", "S108"] "src/remove_ai_watermarks/_internal/watermark_remover.py" = ["S603", "S606", "S607"] # nvidia-smi capability probe "src/remove_ai_watermarks/_internal/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