"""Scan a retained corpus for registered and still-uncovered visible marks. Why this exists separately from `visible_removal_audit.py`: that audit is single-process, so repeated full-dataset sweeps waste time. Its expensive half is DETECTION, and detection does not depend on the fill backend. Splitting it out means detecting once in parallel and then feeding the positives to the audit via its `--paths-file` seam, over a few thousand images instead of forty thousand. CRASH TOLERANCE IS NOT OPTIONAL AT THIS SCALE cv2/libpng can crash natively on malformed images. A plain `ProcessPoolExecutor.map` over a large dataset can deadlock: the worker dies without a Python traceback and the parent waits forever on a result that never arrives (observed 2026-07-19 -- 26 min of work lost because results were only written at the end). So this script: * writes every result to JSONL as it arrives -- a kill never costs more than a batch; * is resumable -- an interrupted run skips what it already recorded; * runs each image in a fresh interpreter with a per-image timeout and bounded concurrency; a poisoned file is recorded without retrying it in the parent. Treat input datasets as sensitive and read-only, and keep output gitignored. The JSONL is also the input to visible-mark discovery. Each row records strict and metadata-corroborated detector results plus the metadata cohort independently of the pixels. ``--sheets`` then makes blinded, native-detail top/bottom bands from: * quiet TC260 producer cohorts, including producer identities absent from the visible-mark registry; * quiet named-platform cohorts; * an unbiased sample of every other quiet image, so a completely novel mark with no useful metadata is not excluded by construction. The sheets are candidates for human adjudication, not evidence that a watermark is present. Their manifest is deliberately separate and must stay closed until the image review finishes. uv run python scripts/visible_positives.py --corpus .local-eval/originals --jobs 6 uv run python scripts/visible_positives.py --since 2026-09-01 --sheets .local-eval/visible-fresh-sheets """ from __future__ import annotations import argparse import collections import json import os import random import shutil import subprocess import sys from datetime import date from importlib.metadata import version from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent)) from _corpus_scan import corpus_files, repair_jsonl_tail from _isolated_image_workers import run_batch # The package's own format set. An inlined copy here silently skipped .heif, which # CLAUDE.md documents as supported. from remove_ai_watermarks._internal.constants import SUPPORTED_FORMATS as _EXTS REPO = Path(__file__).resolve().parents[1] CORPUS = REPO / ".local-eval" / "originals" OUT = REPO / ".local-eval" / "visible-positives.jsonl" _SHEET_BAND_FRACTION = 0.12 _SCHEMA_VERSION = 2 def _one(path: str) -> dict[str, object]: from remove_ai_watermarks.api import _provenance_from_report from remove_ai_watermarks.identify import identify from remove_ai_watermarks.image_io import imread from remove_ai_watermarks.metadata import aigc_label, uscc_of from remove_ai_watermarks.watermark_registry import _provenance_confirms_product, known_marks try: source = Path(path) img = imread(source) if img is None: return _error_record(path, "unreadable") report = identify(source, check_visible=False, check_invisible=False) provenance = _provenance_from_report(report, source) strict = [] accepted = [] for mark in known_marks(): strict_detection, relaxed_detection = mark.detect_both(img) if strict_detection.detected: strict.append(mark.key) detection = ( relaxed_detection if _provenance_confirms_product(mark.product, provenance) else strict_detection ) if detection.detected: accepted.append(mark.key) label = aigc_label(source) or {} producer = str(label.get("ContentProducer") or "") return { "schema_version": _SCHEMA_VERSION, "path": path, "keys": accepted, "strict_keys": strict, "provenance": sorted(provenance), "platform": report.platform or "", "producer": producer, "uscc": uscc_of(producer) if producer else "", "status": "ok", "error": "", } except Exception as e: return _error_record(path, f"error:{type(e).__name__}", str(e)) def _error_record(path: str, status: str, error: str = "") -> dict[str, object]: return { "schema_version": _SCHEMA_VERSION, "path": path, "keys": [], "strict_keys": [], "provenance": [], "platform": "", "producer": "", "uscc": "", "status": status, "error": error[:300].replace("\n", " "), } def _run_batch(batch: list[str], jobs: int, timeout: int) -> list[dict[str, object]]: """Bound each image in an independent interpreter, including native failures.""" return run_batch(Path(__file__), batch, jobs=jobs, timeout=timeout) def _files(corpus: Path, since: date | None) -> list[Path]: return corpus_files(corpus, since, suffixes=_EXTS) def _read_records(path: Path) -> list[dict[str, Any]]: rows: dict[str, dict[str, Any]] = {} if not path.exists(): return [] with path.open(encoding="utf-8") as stream: for line in stream: try: row = json.loads(line) except (json.JSONDecodeError, UnicodeDecodeError): continue if isinstance(row, dict) and isinstance(row.get("path"), str): rows[row["path"]] = row return list(rows.values()) def _repair_checkpoint(path: Path) -> None: """Drop a torn tail before append, otherwise the next row joins invalid JSON.""" repair_jsonl_tail(path) def _discovery_candidates( rows: list[dict[str, Any]], *, per_cohort: int, random_quiet: int, seed: int ) -> list[dict[str, Any]]: """Stratify quiet rows without treating provenance as a pixel-level label.""" rng = random.Random(seed) # noqa: S311 -- reproducible review sampling quiet = [row for row in rows if row.get("status") == "ok" and not row.get("keys")] strata: dict[str, list[dict[str, Any]]] = collections.defaultdict(list) for row in quiet: if row.get("uscc"): strata[f"tc260:{row['uscc']}"].append(row) if row.get("platform"): strata[f"platform:{row['platform']}"].append(row) selected: dict[str, dict[str, Any]] = {} labels: dict[str, set[str]] = collections.defaultdict(set) for label, members in sorted(strata.items()): for row in rng.sample(members, k=min(per_cohort, len(members))): selected[row["path"]] = row labels[row["path"]].add(label) for row in rng.sample(quiet, k=min(random_quiet, len(quiet))): selected[row["path"]] = row labels[row["path"]].add("unbiased-quiet") return [{**row, "discovery_strata": sorted(labels[path])} for path, row in sorted(selected.items())] def _bands(path: str) -> tuple[Any, Any] | None: from remove_ai_watermarks.image_io import imread image = imread(path) if image is None: return None height = image.shape[0] band_height = max(24, int(height * _SHEET_BAND_FRACTION)) return image[:band_height], image[height - band_height :] def _write_discovery_sheets(rows: list[dict[str, Any]], output: Path) -> int: import csv import cv2 import numpy as np if output.exists() and any(output.iterdir()): raise ValueError(f"discovery output must be empty: {output}") output.mkdir(parents=True, exist_ok=True) manifest: list[dict[str, Any]] = [] written = 0 for row in rows: bands = _bands(row["path"]) if bands is None: continue top, bottom = bands width = max(top.shape[1], bottom.shape[1]) divider = np.full((6, width, 3), 90, np.uint8) header = np.full((28, width, 3), 40, np.uint8) cv2.putText(header, f"{written:05d}", (8, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1) cv2.imwrite(str(output / f"{written:05d}.png"), np.vstack((header, top, divider, bottom))) manifest.append({"idx": written, **row}) written += 1 if manifest: fields = list(manifest[0]) with (output / "MANIFEST_DO_NOT_OPEN.csv").open("w", newline="", encoding="utf-8") as stream: writer = csv.DictWriter(stream, fieldnames=fields, extrasaction="ignore") writer.writeheader() writer.writerows(manifest) return written def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--corpus", type=Path, default=CORPUS) ap.add_argument("--since", type=date.fromisoformat, help="include dated corpus directories on or after YYYY-MM-DD") ap.add_argument("--jobs", type=int, default=max(1, (os.cpu_count() or 4) - 2)) ap.add_argument("--limit", type=int, default=0) ap.add_argument("--batch", type=int, default=200) ap.add_argument("--timeout", type=int, default=600, help="seconds per image before terminating its isolated worker") ap.add_argument("--out", type=Path, default=OUT) ap.add_argument("--restart", action="store_true") ap.add_argument("--sheets", type=Path, help="write blinded discovery bands after the scan") ap.add_argument("--per-cohort", type=int, default=12, help="quiet images sampled from each metadata cohort") ap.add_argument("--random-quiet", type=int, default=400, help="unbiased quiet images sampled across the corpus") ap.add_argument("--seed", type=int, default=2026) a = ap.parse_args() files = [str(path) for path in _files(a.corpus, a.since)] if a.limit: files = files[: a.limit] done: set[str] = set() if a.restart and a.out.exists(): a.out.unlink() # --restart must TRUNCATE; the file is reopened in append mode below _repair_checkpoint(a.out) if a.out.exists() and not a.restart: done = {row["path"] for row in _read_records(a.out) if row.get("schema_version") == _SCHEMA_VERSION} todo = [p for p in files if p not in done] print(f"images {len(files)} already done {len(done)} to do {len(todo)} jobs {a.jobs}", flush=True) seen = 0 a.out.parent.mkdir(parents=True, exist_ok=True) with open(a.out, "a", encoding="utf-8") as fh: for start in range(0, len(todo), a.batch): for rec in _run_batch(todo[start : start + a.batch], a.jobs, a.timeout): fh.write(json.dumps(rec) + "\n") seen += 1 fh.flush() print(f" {seen}/{len(todo)}", flush=True) records = _read_records(a.out) selected_paths = set(files) records = [row for row in records if row["path"] in selected_paths] hits = [row["path"] for row in records if row.get("keys")] counts = collections.Counter(str(key) for row in records for key in row.get("keys", [])) strict_counts = collections.Counter(str(key) for row in records for key in row.get("strict_keys", [])) status = collections.Counter(str(row.get("status")) for row in records) # Derive the paths file from --out so a trial run with a scratch --out cannot # overwrite the shared list a full sweep produced. paths_out = a.out.with_suffix(".txt") paths_tmp = paths_out.with_suffix(paths_out.suffix + ".tmp") paths_tmp.write_text("\n".join(sorted(set(hits))) + ("\n" if hits else ""), encoding="utf-8") paths_tmp.replace(paths_out) print(f"\npositives: {len(set(hits))} images") for k, v in counts.most_common(): print(f" {v:6d} {k} ({strict_counts[k]} strict)") print(f"statuses: {dict(status)}\npaths -> {paths_out}\nrecords -> {a.out}") if a.sheets: candidates = _discovery_candidates( records, per_cohort=a.per_cohort, random_quiet=a.random_quiet, seed=a.seed, ) written = _write_discovery_sheets(candidates, a.sheets) print(f"discovery candidates: {len(candidates)} sheets written: {written} -> {a.sheets}") from remove_ai_watermarks.watermark_registry import mark_keys git = shutil.which("git") commit = ( subprocess.run( # noqa: S603 -- resolved git binary and fixed read-only arguments [git, "rev-parse", "HEAD"], cwd=REPO, capture_output=True, text=True, check=False, ).stdout.strip() if git else "" ) metadata = { "command": [str(Path(__file__).relative_to(REPO)), *sys.argv[1:]], "library_commit": commit or None, "library_version": version("remove-ai-watermarks"), "corpus": str(a.corpus.resolve()), "since": a.since.isoformat() if a.since else None, "selected_images": len(files), "complete_records": len(records), "registry_keys": mark_keys(), "schema_version": _SCHEMA_VERSION, } a.out.with_suffix(a.out.suffix + ".run.json").write_text( json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) if __name__ == "__main__": main()