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
The operator reported a 15-image variant pack landing as 3 records, then
reported variants STILL being dropped with phash_threshold at 0 — the floor
of the dial. No setting could have fixed it: at hash_size=8 a pHash is 64
bits of coarse light/dark layout, so two variants sharing a composition
produce the SAME bits. Distance 0 meant "identical hash", not "identical
image", and the dial was simultaneously too coarse to keep variants and too
tight to catch a re-encoded rescale.
The hash no longer decides a merge on its own. find_similar now runs three
gates, cheapest first: the threshold proposes candidates, aspect ratio
(ASPECT_TOL, matching the tier-1 video path) rejects crops and re-canvases,
and a pixel-level confirm on the two files accepts. Every gate fails closed —
unknown dimensions, an unreadable candidate, a hash of the wrong width all
mean "not a duplicate", because too strict keeps a redundant copy the
operator can see while too loose deletes artwork only a source re-walk
returns.
- utils/phash.py: HASH_SIZE 8 -> 16 (256-bit, what ImageRepo always used);
aspect_matches, fingerprint/fingerprint_path/fingerprints_match (PIL-only,
mean drift + changed-pixel fraction), find_similar gains `confirm`.
- importer: _pixel_confirmer supplies gate 3 on both dedup sites, lazily and
cached, so a non-matching import costs no extra I/O.
- 0098: widens image_record.phash to 64 chars and NULLs every value — a
stored 64-bit hash cannot be compared to a 256-bit one, and backfill_phash
is NULL-only, keyset-paginated and now on the daily beat, so the library
re-hashes itself. Dedup degrades to sha256 until it finishes.
- phash_threshold counts bits and the denominator went 64 -> 256, so the
setting is reset to the new default of 24 (there is no honest carry-over)
and the slider is rescaled to 0-64.
- gallery_service dup_threshold 8 -> 32: the same fraction of the hash, so
the Explore rail keeps the variance the operator tuned in on 2026-07-01.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR