diff --git a/src/remove_ai_watermarks/cli.py b/src/remove_ai_watermarks/cli.py index f050109..fe4207a 100644 --- a/src/remove_ai_watermarks/cli.py +++ b/src/remove_ai_watermarks/cli.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from numpy.typing import NDArray -# --- plain-text output layer (replaces rich: no colors, no markup, no boxes) --- +# ── plain-text output layer (replaces rich: no colors, no markup, no boxes) ── class _Table: @@ -498,9 +498,7 @@ def _should_skip_invisible_scrub(force: bool, image_path: Path) -> bool: return not has_invisible_target(image_path) -# -- Main group ------------------------------------------------------- - - +# ── Main group ── @click.group(invoke_without_command=True) @click.version_option(__version__, prog_name="remove-ai-watermarks") @click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging.") @@ -520,9 +518,7 @@ def main(ctx: click.Context, verbose: bool) -> None: click.echo(ctx.get_help()) -# -- Visible (Gemini) watermark removal ------------------------------- - - +# ── Visible (Gemini) watermark removal ── @main.command("visible") @click.argument("source", type=click.Path(exists=True, path_type=Path)) @click.option( @@ -637,9 +633,7 @@ def cmd_visible( console.print(f" Saved: {output} ({size_kb:.0f} KB, {elapsed:.2f}s)") -# -- Universal region eraser ----------------------------------------- - - +# ── Universal region eraser ── def _parse_region(spec: str) -> tuple[int, int, int, int]: """Parse an ``x,y,w,h`` region string into a 4-int tuple.""" parts = spec.replace(" ", "").split(",") @@ -729,9 +723,7 @@ def cmd_erase( console.print(f" Erased {len(boxes)} region(s) -> {output} ({size_kb:.0f} KB, {elapsed:.2f}s)") -# -- Invisible watermark removal ------------------------------------- - - +# ── Invisible watermark removal ── @main.command("invisible") @click.argument("source", type=click.Path(exists=True, path_type=Path)) @click.option( @@ -867,9 +859,7 @@ def cmd_invisible( console.print(f"\n Saved: {result_path} ({size_kb:.0f} KB, {elapsed:.1f}s)") -# -- Metadata operations --------------------------------------------- - - +# ── Metadata operations ── @main.command("metadata") @click.argument("source", type=click.Path(exists=True, path_type=Path)) @click.option("--check", is_flag=True, help="Check for AI metadata (don't modify).") @@ -926,9 +916,7 @@ def cmd_metadata( console.print(f" AI metadata stripped -> {out}") -# -- Provenance identification --------------------------------------- - - +# ── Provenance identification ── @main.command("identify") @click.argument("source", type=click.Path(exists=True, path_type=Path)) @click.option( @@ -997,9 +985,7 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo console.print(f" - {c}") -# -- Combined "all" mode ---------------------------------------------- - - +# ── Combined "all" mode ── @main.command("all") @click.argument("source", type=click.Path(exists=True, path_type=Path)) @click.option( @@ -1102,7 +1088,7 @@ def cmd_all( os.close(tmp_fd) - # -- Step 1: Visible watermark -------------------------------- + # ── Step 1: Visible watermark ── console.print("\n 1) Visible watermark removal") image, alpha = image_io.read_bgr_and_alpha(source) if image is None: @@ -1124,7 +1110,7 @@ def cmd_all( # Save to temp file for invisible engine input (preserve alpha if present) image_io.write_bgr_with_alpha(tmp_path, result, alpha) - # -- Step 2: Invisible watermark ------------------------------ + # ── Step 2: Invisible watermark ── console.print("\n 2) Invisible watermark removal") from remove_ai_watermarks.invisible_engine import is_available as invisible_available @@ -1188,7 +1174,7 @@ def cmd_all( ) console.print(" Invisible watermark removed") - # -- Step 3: Metadata ----------------------------------------- + # ── Step 3: Metadata ── console.print("\n 3) AI metadata stripping") try: from remove_ai_watermarks.metadata import remove_ai_metadata @@ -1198,7 +1184,7 @@ def cmd_all( except Exception as e: console.print(f" Warning: Metadata strip failed: {e}") - # -- Write final result ---------------------------------------- + # ── Write final result ── # The invisible step (and downstream cv2.IMREAD_COLOR paths) drops alpha, # so re-attach the original alpha plane unchanged when writing the final # output for transparent formats. @@ -1214,7 +1200,7 @@ def cmd_all( if tmp_path.exists(): tmp_path.unlink() - # -- Done ----------------------------------------------------- + # ── Done ── elapsed = time.monotonic() - t0 size_kb = output.stat().st_size / 1024 console.print(f"\n Done: {output} ({size_kb:.0f} KB, {elapsed:.1f}s total)") @@ -1238,9 +1224,7 @@ def cmd_all( raise SystemExit(1) -# -- Batch command ---------------------------------------------------- - - +# ── Batch command ── def _process_batch_image( ctx: click.Context, img_path: Path, diff --git a/src/remove_ai_watermarks/identify.py b/src/remove_ai_watermarks/identify.py index d54b1dd..d38da43 100644 --- a/src/remove_ai_watermarks/identify.py +++ b/src/remove_ai_watermarks/identify.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: from remove_ai_watermarks.watermark_registry import MarkDetection -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # How much of a non-PNG container to binary-scan for the C2PA issuer. _SCAN_BYTES = 1024 * 1024 @@ -393,7 +393,7 @@ def _visible_sparkle(image_path: Path, *, image: NDArray[Any] | None = None) -> try: from remove_ai_watermarks.gemini_engine import detect_sparkle_confidence except Exception as exc: # cv2/engine assets missing - log.debug("visible-sparkle detector unavailable: %s", exc) + logger.debug("visible-sparkle detector unavailable: %s", exc) return None return detect_sparkle_confidence(image_path, image=image) @@ -424,7 +424,7 @@ def _visible_text_marks(image_path: Path, *, image: NDArray[Any] | None = None) from remove_ai_watermarks.image_io import imread from remove_ai_watermarks.watermark_registry import get_mark except Exception as exc: # cv2/engine assets missing - log.debug("visible-mark detectors unavailable: %s", exc) + logger.debug("visible-mark detectors unavailable: %s", exc) return [] if image is None: image = imread(image_path) @@ -435,7 +435,7 @@ def _visible_text_marks(image_path: Path, *, image: NDArray[Any] | None = None) try: det = get_mark(key).detect(image) except Exception as exc: # one engine failing must not break identify - log.debug("visible-mark %s detector failed: %s", key, exc) + logger.debug("visible-mark %s detector failed: %s", key, exc) continue if det.detected: detections.append(det) @@ -719,7 +719,7 @@ def identify(image_path: Path, *, check_visible: bool = True, check_invisible: b vis_image = imread(image_path) except Exception as exc: # cv2 missing - detectors fall back / no-op - log.debug("visible-mark decode unavailable: %s", exc) + logger.debug("visible-mark decode unavailable: %s", exc) # ── Visible Gemini sparkle (fallback for stripped-metadata case) ─ sparkle_conf = _visible_sparkle(image_path, image=vis_image) if check_visible else None @@ -798,6 +798,6 @@ def has_invisible_target(image_path: Path) -> bool: try: report = identify(image_path, check_visible=False, check_invisible=True) except Exception: # unreadable / detector error -> do not skip the removal - log.debug("has_invisible_target: identify failed, defaulting to run", exc_info=True) + logger.debug("has_invisible_target: identify failed, defaulting to run", exc_info=True) return True return report.ai_from_metadata diff --git a/src/remove_ai_watermarks/invisible_watermark.py b/src/remove_ai_watermarks/invisible_watermark.py index 9a2cdbb..45cb57a 100644 --- a/src/remove_ai_watermarks/invisible_watermark.py +++ b/src/remove_ai_watermarks/invisible_watermark.py @@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from pathlib import Path -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # Known 48-bit ``bits`` watermarks (dwtDct, no key), name -> message integer. _BITS_48: dict[str, int] = { @@ -96,7 +96,7 @@ def detect_invisible_watermark(image_path: Path) -> str | None: if _bits_match(value, ref) >= _MATCH_48: return name except Exception as exc: # decode can fail on tiny images - log.debug("48-bit watermark decode failed for %s: %s", image_path, exc) + logger.debug("48-bit watermark decode failed for %s: %s", image_path, exc) # 136-bit default string watermark (SD 1.x / 2.x). try: @@ -104,6 +104,6 @@ def detect_invisible_watermark(image_path: Path) -> str | None: if _bytes_match_frac(raw, _SD1_STRING) >= _MATCH_SD1_FRAC: return "Stable Diffusion 1.x / 2.x" except Exception as exc: - log.debug("string watermark decode failed for %s: %s", image_path, exc) + logger.debug("string watermark decode failed for %s: %s", image_path, exc) return None diff --git a/src/remove_ai_watermarks/noai/c2pa.py b/src/remove_ai_watermarks/noai/c2pa.py index 72a5386..27ad603 100644 --- a/src/remove_ai_watermarks/noai/c2pa.py +++ b/src/remove_ai_watermarks/noai/c2pa.py @@ -41,7 +41,7 @@ from remove_ai_watermarks.noai.constants import ( SYNTHID_C2PA_ISSUERS, ) -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # Official C2PA reader (c2pa-python, a core dependency). It is the primary, # spec-tracking manifest parser; the hand-rolled caBX/CBOR scanner below stays as @@ -96,7 +96,7 @@ def _read_manifest_store_impl(path_str: str) -> str | None: try: reader = _C2paReader.try_create(path_str) except Exception as exc: # malformed manifest, unsupported container, etc. - log.debug("c2pa Reader could not parse %s: %s", path_str, exc) + logger.debug("c2pa Reader could not parse %s: %s", path_str, exc) return None if reader is None: return None @@ -104,7 +104,7 @@ def _read_manifest_store_impl(path_str: str) -> str | None: with reader: return reader.json() except Exception as exc: # pragma: no cover - reader opened but json() failed - log.debug("c2pa Reader.json() failed on %s: %s", path_str, exc) + logger.debug("c2pa Reader.json() failed on %s: %s", path_str, exc) return None diff --git a/src/remove_ai_watermarks/noai/isobmff.py b/src/remove_ai_watermarks/noai/isobmff.py index c595186..676fb81 100644 --- a/src/remove_ai_watermarks/noai/isobmff.py +++ b/src/remove_ai_watermarks/noai/isobmff.py @@ -33,7 +33,7 @@ from remove_ai_watermarks.metadata import ( IPTC_AI_MARKERS, ) -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # Top-level box types that may carry AI provenance. ``uuid`` boxes are checked # against ``C2PA_UUID`` / AI-label markers before being stripped; ``jumb`` boxes @@ -188,7 +188,7 @@ def strip_c2pa_boxes(data: bytes) -> tuple[bytes, int]: # would silently truncate the file from the bad box to EOF -- worse than not # stripping. If the walk did not consume the whole input, return it unchanged. if consumed != len(data): - log.warning( + logger.warning( "ISOBMFF box walk stopped at offset %d of %d (malformed box); " "returning input unchanged to avoid truncation", consumed, diff --git a/src/remove_ai_watermarks/trustmark_detector.py b/src/remove_ai_watermarks/trustmark_detector.py index 013505d..a846430 100644 --- a/src/remove_ai_watermarks/trustmark_detector.py +++ b/src/remove_ai_watermarks/trustmark_detector.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from pathlib import Path -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # Adobe ships Variant P in production (com.adobe.trustmark.P). _MODEL_TYPE = "P" @@ -94,10 +94,10 @@ def detect_trustmark(image_path: Path) -> str | None: if not wm_present: return None if not _survives_reencode(decoder, cover, wm_schema): - log.debug("TrustMark decode for %s did not survive re-encode; treating as false positive", image_path) + logger.debug("TrustMark decode for %s did not survive re-encode; treating as false positive", image_path) return None except Exception as exc: # model download / decode failure / unreadable image - log.debug("TrustMark decode failed for %s: %s", image_path, exc) + logger.debug("TrustMark decode failed for %s: %s", image_path, exc) return None return f"Adobe TrustMark (variant {_MODEL_TYPE}, schema {wm_schema})"