feat: initial Luma implementation
Full self-hosted photo manager with: - Ingestion worker (chokidar + sharp + exifr, dHash perceptual hashing) - SQLite schema with WAL mode (photos, tags, collections, duplicate_pairs) - Express API with keyset pagination, tag/collection/search/date filters - React + Vite UI: timeline, lightbox, detail panel, tag/collection editors - Duplicate detection (Union-Find grouping) with review + dismiss UI - Map view (Leaflet + OpenStreetMap, marker clustering) - Docker multi-stage build + Compose with inline env config - GitHub Actions: CI build check + ghcr.io image publish on main/tags Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
03e6d6b628
commit
e0f4e8ff0d
32 files changed
+4845
No files matched your search
@@ -0,0 +1,31 @@
|
||||
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');
|
||||
}
|
||||
|
||||
export function hammingDistance(a, b) {
|
||||
let xor = BigInt('0x' + a) ^ BigInt('0x' + b);
|
||||
let dist = 0;
|
||||
while (xor > 0n) {
|
||||
dist += Number(xor & 1n);
|
||||
xor >>= 1n;
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
Reference in new issue
Block a user