Files
shpm/worker/hash.js
T
nmemmertandClaude Sonnet 4.6 4f7dd3b49b fix: dedupe scan 500 error, SQLite limit crash, add keep-best action
- Replace BigInt hammingDistance with 32-bit popcount — safer (no throw
  on malformed hashes) and ~10× faster
- Make scan() async with setImmediate chunking so the event loop stays
  responsive on large libraries
- Batch getGroups() photo lookup in slices of 900 to stay under SQLite's
  999-variable limit
- Add POST /api/duplicates/keep-best endpoint: scores by resolution then
  filesize, soft-deletes the losers, dismisses the pair
- DuplicateGroup: green "Keep" badge on the best photo, green "Keep best"
  button in footer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-19 15:45:46 -04:00

37 lines
1.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sharp from 'sharp';
// dHash (difference hash): 9×8 → 64-bit comparison → 16-char hex string.
// Two images with Hamming distance ≤ 10 are near-duplicates.
export async function computeDHash(filepath) {
const { data } = await sharp(filepath)
.resize(9, 8, { fit: 'fill' })
.greyscale()
.raw()
.toBuffer({ resolveWithObject: true });
let hash = 0n;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const left = data[row * 9 + col];
const right = data[row * 9 + col + 1];
hash = (hash << 1n) | (left < right ? 1n : 0n);
}
}
return hash.toString(16).padStart(16, '0');
}
function popcount32(n) {
n = n >>> 0;
n -= (n >> 1) & 0x55555555;
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n + (n >> 4)) & 0x0f0f0f0f;
return Math.imul(n, 0x01010101) >>> 24;
}
export function hammingDistance(a, b) {
if (!a || !b || a.length !== 16 || b.length !== 16) return 64;
const xorHi = (parseInt(a.slice(0, 8), 16) ^ parseInt(b.slice(0, 8), 16)) >>> 0;
const xorLo = (parseInt(a.slice(8), 16) ^ parseInt(b.slice(8), 16)) >>> 0;
return popcount32(xorHi) + popcount32(xorLo);
}