mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-07 14:38:35 +02:00
chore: project review (dev tools in extras, dep upgrades, optional-deps guard, stale cleanup)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
826cfdb82a
commit
295e7ada2b
@@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
||||
import click
|
||||
|
||||
from remove_ai_watermarks import __version__, watermark_registry
|
||||
from remove_ai_watermarks.noai.constants import SUPPORTED_FORMATS
|
||||
from remove_ai_watermarks.noai.watermark_profiles import (
|
||||
resolve_strength,
|
||||
strength_default_help,
|
||||
@@ -106,8 +107,6 @@ Progress = _Progress
|
||||
SpinnerColumn = BarColumn = TextColumn = TimeElapsedColumn = _column
|
||||
console = _Console()
|
||||
|
||||
SUPPORTED_FORMATS = {".png", ".jpg", ".jpeg", ".webp"}
|
||||
|
||||
|
||||
def _setup_logging(verbose: bool) -> None:
|
||||
level = logging.DEBUG if verbose else logging.WARNING
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
``apply_analog_humanizer`` injects film grain and chromatic aberration to defeat
|
||||
digital AI-perfection classifiers (ported from NeuralBleach); ``unsharp_mask``
|
||||
counters the soft, over-smoothed look that diffusion + face-restoration leave
|
||||
behind (itself a common "this is AI" tell).
|
||||
counters the soft, over-smoothed look that the diffusion pass leaves behind
|
||||
(itself a common "this is AI" tell).
|
||||
"""
|
||||
|
||||
# cv2/numpy boundary: third-party libs ship no usable element types; relax the
|
||||
@@ -63,7 +63,7 @@ def apply_analog_humanizer(image: NDArray, grain_intensity: float = 4.0, chromat
|
||||
def unsharp_mask(image: NDArray, amount: float = 0.5, sigma: float = 1.0) -> NDArray:
|
||||
"""Sharpen via unsharp masking: ``out = image + amount * (image - blur(image))``.
|
||||
|
||||
Counters the soft, over-smoothed look of the diffusion + GFPGAN passes, which
|
||||
Counters the soft, over-smoothed look of the diffusion pass, which
|
||||
reads as an AI tell. ``amount`` 0 = no-op (returns an unchanged copy); ~0.5-0.8
|
||||
is a safe range -- higher risks bright edge halos that are their own artifact.
|
||||
``sigma`` is the Gaussian radius of the unsharp kernel.
|
||||
|
||||
@@ -19,6 +19,8 @@ import warnings
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .noai.watermark_profiles import DEFAULT_MODEL_ID as DEFAULT_SDXL_MODEL_ID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -37,9 +39,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def is_available() -> bool:
|
||||
"""Check if invisible watermark removal dependencies are installed."""
|
||||
import importlib.util
|
||||
from .optional_deps import module_available
|
||||
|
||||
return importlib.util.find_spec("diffusers") is not None and importlib.util.find_spec("torch") is not None
|
||||
return module_available("diffusers", "torch")
|
||||
|
||||
|
||||
def _target_size(width: int, height: int, max_resolution: int, min_resolution: int = 0) -> tuple[int, int] | None:
|
||||
@@ -83,7 +85,7 @@ class InvisibleEngine:
|
||||
|
||||
# SDXL base is the default since May 2026; the vendor-adaptive strength
|
||||
# removes the current SynthID (see watermark_profiles + docs/synthid.md).
|
||||
DEFAULT_MODEL_ID = "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
DEFAULT_MODEL_ID = DEFAULT_SDXL_MODEL_ID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -176,7 +178,7 @@ class InvisibleEngine:
|
||||
output_path: Output path (None = overwrite source).
|
||||
strength: Denoising strength (0.0-1.0). None -> the vendor-adaptive
|
||||
default.
|
||||
steps: Number of denoising steps.
|
||||
num_inference_steps: Number of denoising steps.
|
||||
guidance_scale: Classifier-free guidance scale.
|
||||
seed: Random seed for reproducibility.
|
||||
humanize: Intensity of Analog Humanizer film grain (0 = off).
|
||||
|
||||
@@ -50,9 +50,9 @@ _MATCH_SD1_FRAC = 0.92 # fraction of the 136 string bits that must match
|
||||
|
||||
def is_available() -> bool:
|
||||
"""True if the optional imwatermark decoder is installed."""
|
||||
import importlib.util
|
||||
from .optional_deps import module_available
|
||||
|
||||
return importlib.util.find_spec("imwatermark") is not None
|
||||
return module_available("imwatermark")
|
||||
|
||||
|
||||
def _bits_match(value: int, ref: int, width: int = 48) -> int:
|
||||
|
||||
@@ -20,6 +20,11 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Smaller scan_head window for the cheap marker checks (has_ai_metadata,
|
||||
# samsung_genai); the full-detail scans use scan_head's 1 MB default. Sharing
|
||||
# one constant also keeps both call sites on the same memoized cache entry.
|
||||
_QUICK_SCAN_BYTES = 512 * 1024
|
||||
|
||||
# ── Known AI metadata keys ──────────────────────────────────────────
|
||||
|
||||
AI_METADATA_KEYS: frozenset[str] = frozenset(
|
||||
@@ -306,7 +311,7 @@ def has_ai_metadata(image_path: Path) -> bool:
|
||||
|
||||
# Binary scan covers C2PA (PNG caBX, JPEG APP11, AVIF/HEIF/JXL uuid boxes)
|
||||
# and IPTC AI markers in XMP. First 512KB (plus late ISOBMFF provenance boxes).
|
||||
data = scan_head(image_path, 512 * 1024)
|
||||
data = scan_head(image_path, _QUICK_SCAN_BYTES)
|
||||
if c2pa_marker_in(data):
|
||||
return True
|
||||
if any(marker in data for marker in AIGC_MARKERS):
|
||||
@@ -453,7 +458,7 @@ def samsung_genai(image_path: Path) -> int | None:
|
||||
gated on the ``PhotoEditor_Re_Edit_Data`` container so an incidental
|
||||
``genAIType`` token cannot false-positive.
|
||||
"""
|
||||
head = scan_head(image_path, 512 * 1024)
|
||||
head = scan_head(image_path, _QUICK_SCAN_BYTES)
|
||||
if _SAMSUNG_EDITOR_MARKER not in head:
|
||||
return None
|
||||
m = _SAMSUNG_GENAI_RE.search(head)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Shared availability guard for optional dependencies.
|
||||
|
||||
A bare ``importlib.util.find_spec(name) is not None`` check lies when only a
|
||||
leftover data directory exists in site-packages: e.g. ``trustmark`` downloads
|
||||
model weights into its own package dir, so after the package is uninstalled
|
||||
(``uv sync`` pruning an extra) a ``trustmark/models/`` remnant survives and
|
||||
``find_spec`` resolves it to a namespace-package spec (``loader is None``)
|
||||
while the actual import fails. Every ``is_available()`` guard routes through
|
||||
``module_available`` so a pure namespace package counts as absent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
|
||||
|
||||
def module_available(*names: str) -> bool:
|
||||
"""True when every named module resolves to a real, importable package.
|
||||
|
||||
A spec with ``loader is None`` is a pure namespace package -- for our
|
||||
optional deps that means a stale directory remnant, not an installed
|
||||
package -- so it is treated as not available.
|
||||
"""
|
||||
for name in names:
|
||||
spec = importlib.util.find_spec(name)
|
||||
if spec is None or spec.loader is None:
|
||||
return False
|
||||
return True
|
||||
@@ -82,9 +82,9 @@ def erase_cv2(
|
||||
|
||||
def lama_available() -> bool:
|
||||
"""True when the optional LaMa-ONNX backend can run (onnxruntime installed)."""
|
||||
import importlib.util
|
||||
from .optional_deps import module_available
|
||||
|
||||
return importlib.util.find_spec("onnxruntime") is not None
|
||||
return module_available("onnxruntime")
|
||||
|
||||
|
||||
def _get_lama_session() -> object:
|
||||
|
||||
@@ -40,9 +40,9 @@ _tm_lock = threading.Lock()
|
||||
|
||||
def is_available() -> bool:
|
||||
"""True if the optional ``trustmark`` package is installed."""
|
||||
import importlib.util
|
||||
from .optional_deps import module_available
|
||||
|
||||
return importlib.util.find_spec("trustmark") is not None
|
||||
return module_available("trustmark")
|
||||
|
||||
|
||||
def _decoder() -> Any:
|
||||
|
||||
@@ -8,8 +8,8 @@ The DEFAULT upscaler stays Lanczos (cv2, no deps); this is opt-in via the ``esrg
|
||||
extra and feeds the ``--upscaler esrgan`` path. ``spandrel`` is a pure model-loader
|
||||
(MIT) with NO basicsr dependency -- it pulls only torch/torchvision/safetensors/numpy/
|
||||
einops -- so it sidesteps the basicsr / ``torchvision.transforms.functional_tensor``
|
||||
breakage that the ``restore`` (GFPGAN) extra has to shim. Real-ESRGAN weights are
|
||||
BSD-3-Clause.
|
||||
breakage that the retired ``restore`` (GFPGAN) extra had to shim. Real-ESRGAN weights
|
||||
are BSD-3-Clause.
|
||||
|
||||
CPU works but is slow on large inputs, so this is meant for the pre-diffusion upscale of
|
||||
SMALL inputs (and the GPU worker). On a memory-constrained host it is a no-op (the extra
|
||||
@@ -21,7 +21,6 @@ is absent), and the caller falls back to Lanczos.
|
||||
# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingTypeArgument=false, reportMissingTypeStubs=false, reportMissingImports=false, reportArgumentType=false, reportAssignmentType=false, reportReturnType=false, reportCallIssue=false, reportIndexIssue=false, reportOperatorIssue=false, reportAttributeAccessIssue=false, reportPrivateImportUsage=false
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
@@ -45,7 +44,9 @@ _lock = threading.Lock()
|
||||
|
||||
def is_available() -> bool:
|
||||
"""True if the ``esrgan`` extra (spandrel + torch) is importable."""
|
||||
return importlib.util.find_spec("spandrel") is not None and importlib.util.find_spec("torch") is not None
|
||||
from .optional_deps import module_available
|
||||
|
||||
return module_available("spandrel", "torch")
|
||||
|
||||
|
||||
def _model_cache_path() -> Path:
|
||||
|
||||
Reference in New Issue
Block a user