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:
nmemmertandClaude Sonnet 4.6 committed 2026-08-17 16:33:55 -04:00
1 parent 03e6d6b628
commit e0f4e8ff0d
32 files changed
+4845

No files matched your search

+31
View File
@@ -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;
}