feat: a report that shows what the near-dup gates decide about real artwork (4223)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 58s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m52s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m22s

The three pixel constants added with the #4223 fix were chosen without ever
measuring real files — CI only has synthetic split/solid fixtures, and FC
verifies nowhere else. This prints the measurements they should have been
chosen from: per pair, the hash distance, the mean drift, the changed-pixel
fraction, the verdict, and which gate produced it.

It drives the real find_similar with the real confirm rather than restating
the decision, so it cannot drift from what the importer does. Read-only:
opens files, touches no database.

Also splits fingerprint_diff out of fingerprints_match — same computation,
now returning the numbers instead of only the boolean, so the report can show
how far a pair sat from a limit rather than which side of it it fell on.

Runs inside the published :dev image (PIL + imagehash already there, no local
env needed) with the art folder mounted read-only — rule 147's channel, so
nothing has to reach main to be tried.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-21 06:30:54 -04:00
co-authored by Claude Opus 5
parent 84f873eb1b
commit 3313c3b10a
2 changed files with 226 additions and 13 deletions
+29 -13
View File
@@ -122,6 +122,31 @@ def fingerprint_path(path) -> Image.Image | None:
return None
def fingerprint_diff(
a, b, *, changed_level: int = FINGERPRINT_CHANGED_LEVEL,
) -> tuple[float, float] | None:
"""(mean absolute difference, fraction of pixels past `changed_level`) for
two fingerprints. None if either is missing or the comparison fails.
This is the MEASUREMENT behind `fingerprints_match`, split out so the
calibration report (scripts/phash_gate_report.py) can show how far a pair
sat from the limits instead of only which side of them it fell on. The
constants were chosen without a real-library sample; the numbers this
returns are what moves them.
"""
if a is None or b is None:
return None
try:
diff = ImageChops.difference(a, b)
hist = diff.histogram()
total = sum(hist)
if not total:
return None
return (ImageStat.Stat(diff).mean[0], sum(hist[changed_level:]) / total)
except Exception:
return None
def fingerprints_match(
a, b,
*,
@@ -132,20 +157,11 @@ def fingerprints_match(
"""True when two fingerprints are the same picture: no large global
drift AND no meaningful local region that differs. False on any
failure."""
if a is None or b is None:
return False
try:
diff = ImageChops.difference(a, b)
if ImageStat.Stat(diff).mean[0] > max_mean_diff:
return False
hist = diff.histogram()
total = sum(hist)
if not total:
return False
changed = sum(hist[changed_level:])
return (changed / total) <= max_changed_fraction
except Exception:
measured = fingerprint_diff(a, b, changed_level=changed_level)
if measured is None:
return False
mean, changed_fraction = measured
return mean <= max_mean_diff and changed_fraction <= max_changed_fraction
def aspect_matches(