mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-12 02:56:32 +02:00
style: unify section dividers and logger variable naming
- cli.py section dividers -> the codebase-majority `# ── Title ──` form - logger variable unified to `logger` across identify, isobmff, c2pa, trustmark_detector, invisible_watermark (matching the 9-file majority) - cosmetic only, no behavior change (719 tests / pyright / ruff green) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user