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:
Victor Kuznetsov
2026-06-09 17:03:17 -07:00
co-authored by Claude Fable 5
parent 826cfdb82a
commit 295e7ada2b
15 changed files with 784 additions and 304 deletions
+1 -2
View File
@@ -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
+3 -3
View File
@@ -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.
+6 -4
View File
@@ -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:
+7 -2
View File
@@ -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)
+28
View File
@@ -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
+2 -2
View File
@@ -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:
+5 -4
View File
@@ -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: