Skip to main content

Grep for your photo library — find any person across thousands of photos, locally and privately. Built-in LoRA dataset builder.

Project description

lensgrep

grep for your photo library. Find people. Remove duplicates. Build LoRA datasets. All locally, all private, no cloud.

PyPI Python License: MIT CI

$ lensgrep scan ~/Photos
scanned 12,847 files  faces+= 38,219  clustered 412 groups

$ lensgrep name 3 "Mom"
✓ 'Mom' ← cluster 3, assigned 1,204 face(s)

$ lensgrep find Mom -n 5 --paths
/home/me/Photos/2023/birthday/IMG_0341.jpg
/home/me/Photos/2024/cabin/DSC_8821.jpg
/home/me/Photos/2024/cabin/DSC_8824.jpg
/home/me/Photos/2025/holidays/IMG_1129.jpg
/home/me/Photos/2025/holidays/IMG_1131.jpg

$ lensgrep lora Mom ./mom_dataset --size 50
✓ LoRA dataset: 50 images at 1024×1024 → ./mom_dataset
  Token: mom
  Next: open AI Toolkit / kohya_ss → point dataset path here → train.

$ lensgrep dupes --near --threshold 5
Found 412 group(s), 1,847 extra file(s), could free 14,203.5 MB.
…
Read-only listing. Re-run with --mode delete or --mode symlink to act.

What it is

lensgrep is a single-purpose tool that does one thing well: find photos of a specific person in a folder of photos, no matter how big the folder is. SQLite under the hood, InsightFace ArcFace embeddings, FAISS for vector search. Nothing leaves your machine.

It's intentionally small. If you want a full self-hosted Google Photos clone, run PhotoPrism or Immich. If you want grep for faces, run this.

Killer features

Duplicate detection — reclaim disk space

Most photo libraries have 10–30% wasted space: phone-sync dupes, edited copies, burst shots, JPEG-vs-RAW pairs, "the same file in three folders because cloud sync went sideways."

lensgrep dupes                          # exact (bit-identical) only — read-only listing
lensgrep dupes --near --threshold 5     # near-duplicates: resized / re-saved / minor edits
lensgrep dupes --near --mode delete     # actually delete the victims
lensgrep dupes --near --mode symlink    # replace victims with symlinks to the keeper
lensgrep dupes --keep newest            # alternative: oldest | newest | shortest-path | largest (default)

Two detection modes:

  • Exact — SHA-256 hash of the file, zero false positives, catches bit-identical copies regardless of filename.
  • Near — perceptual hash (pHash) with configurable Hamming-distance threshold. Catches resized copies, JPEG re-saves, minor color edits, thumbnails — the stuff that exact match misses.

Safety by default: without --mode delete or --mode symlink, the command is read-only. You'll always see what it would do before it does anything.

LoRA dataset builder — flagship

Training a face LoRA (Flux / SDXL) usually means manually picking 40–60 diverse photos of one person from your library. lensgrep does this in one command:

lensgrep lora "Krzysiek" ./dataset --size 50 --resolution 1024

This:

  1. Pulls all photos containing Krzysiek from the indexed library
  2. Scores quality (sharpness, face area, detection confidence)
  3. Picks 50 diverse photos via greedy farthest-point sampling on ArcFace embeddings — no near-duplicates, balanced mix of closeup / portrait / half-body / full-body
  4. Crops to 1024×1024 with the face in the upper third (rule-of-thirds)
  5. Writes .txt caption files next to each image, ready for AI Toolkit or kohya_ss

You go from "8 000 raw photos" to "trainable Flux LoRA dataset" in one command.

Install

pip install lensgrep

Optional: GPU support (10× faster scan)

pip install "lensgrep[gpu]"

Requires Python 3.10+. First run downloads ~280 MB of InsightFace models to ~/.insightface/.

Quickstart

# 1. scan your photo library (one time, ~1.5h CPU / ~10 min GPU for 10k photos)
lensgrep scan ~/Photos

# 2. browse detected face clusters
lensgrep people --min-size 10

# 3. name the ones you care about
lensgrep name 3 "Mom"
lensgrep name 7 "Dad"
lensgrep name 12 "Krzysiek"

# 4. find photos of someone
lensgrep find Mom -n 100
lensgrep find Mom --paths | xargs -I{} cp {} ./mom_photos/

# 5. export all photos of a person
lensgrep export Mom ./mom_photos --mode symlink

# 6. build a LoRA training dataset
lensgrep lora Krzysiek ./krzysiek_dataset --size 50

# 7. quick stats
lensgrep stats

Commands

Command What it does
scan <PATH> Recursively scan folders for faces. Incremental by default (skips unchanged files).
people List detected face clusters, with face counts.
name <CLUSTER> <NAME> Tag a cluster with a person's name.
find <NAME> List photos of a tagged person, ranked by face quality.
export <NAME> <DIR> Copy/symlink/hardlink all matching photos to a folder.
lora <NAME> <DIR> Build a Flux/SDXL LoRA training dataset.
dupes Find (and optionally remove) duplicate photos — exact or perceptual.
recluster Re-run DBSCAN with custom parameters.
stats Database statistics.

Run lensgrep <command> --help for full options on any command.

How it works

                 ┌────────────────┐
   ~/Photos ───▶ │  scan          │ ──▶ images table  (SQLite)
                 │  - detect      │
                 │  - embed (512d)│ ──▶ faces table   (SQLite, embedding as BLOB)
                 │  - score       │
                 └────────────────┘
                          │
                          ▼
                 ┌────────────────┐
                 │  cluster       │ ──▶ persons table (named clusters)
                 │  (DBSCAN)      │
                 └────────────────┘
                          │
                          ▼
                 ┌────────────────┐
                 │  find / export │
                 │  / lora        │
                 └────────────────┘
  • Detection + embedding: InsightFace buffalo_l (SCRFD detector + ArcFace r50 recognition). State-of-the-art, ONNX, runs on CPU or CUDA.
  • Storage: Plain SQLite — no Docker, no daemons, no cloud accounts. The whole index lives in one file at the platform-standard data dir.
  • Vector search: FAISS IndexFlatIP on L2-normalized embeddings — exact cosine similarity, sub-millisecond on a million faces.
  • Clustering: DBSCAN over cosine distance, eps=0.35, min_samples=5. Tune via lensgrep recluster --eps 0.4 --min-samples 8.
  • LoRA selection: Greedy farthest-point sampling on embeddings — maximizes diversity. Quality scoring weights sharpness (Laplacian variance), face area, detection confidence.

Why this exists

In 2026, the photo-management landscape has two extremes:

  • Bloated cloud apps that ingest your library and "do AI on it": Google Photos, Apple Photos, Amazon Photos. Hand over your private life.
  • Self-hosted galleries with face features bolted on: PhotoPrism, Immich, digiKam. Powerful but heavy — Docker, databases, web UIs, hours of setup, weeks of curating.

Neither helps if you just want to answer one question:

"Where are all the photos of [person] in this big folder?"

…or:

"Why does this folder weigh 80 GB? How many of these photos are actual duplicates?"

…or:

"I'm training a Flux LoRA of my partner. Give me 50 diverse, high-quality photos."

That's the niche. One job. Done well.

Comparison

lensgrep PhotoPrism Immich digiKam face_recognition
CLI-first ✅ (library)
No Docker / DB setup
Duplicate detection (exact + perceptual) partial partial partial
LoRA dataset export
FAISS vector search
Modern face model (ArcFace) ❌ (older) ❌ (dlib)
Local-only / no cloud
Install in 10 seconds

Privacy

Everything stays on your machine. No telemetry, no analytics, no network calls except the one-time model download from GitHub releases on first run. The SQLite database is plain text — you can sqlite3 lensgrep.db ".dump" and read it.

If you want to verify: pip install lensgrep ships ~600 lines of pure Python source. Audit it in an afternoon.

Performance

Rough numbers on a 2020 laptop (Quadro RTX 5000 mobile, 16 GB VRAM):

Library size CPU scan GPU scan (CUDA)
1 000 photos ~3 min ~30 s
10 000 photos ~30 min ~5 min
100 000 photos ~5 h ~50 min

Scan is the only slow part. After indexing, find / export / lora are sub-second on hundreds of thousands of faces.

CPU works fine if you're willing to leave it running overnight once. GPU setup needs CUDA Toolkit 12 + cuDNN 9 (pip install "lensgrep[gpu]" is necessary but not sufficient — see docs/gpu.md).

Roadmap

  • CLI MVP (scan, people, name, find, export, lora, dupes, stats)
  • Per-person nearest-neighbor add: lensgrep find-similar <photo.jpg>
  • EXIF date timeline filter: lensgrep find Mom --year 2024
  • EXIF GPS map output
  • HEIC support
  • lensgrep serve — read-only web UI (FastAPI + minimal frontend) for browsing
  • BLIP / Florence-2 auto-captions for richer LoRA training data

If the project gains traction, the web UI is next. Open an issue if you want to push it forward.

Contributing

PRs welcome. Project conventions:

  • Ruff for lint + format (ruff check . && ruff format .)
  • Pytest for tests (pytest)
  • Conventional commits (feat:, fix:, docs:, refactor:)
git clone https://github.com/Kris-O/lensgrep
cd lensgrep
pip install -e ".[dev]"
pytest

License

MIT — see LICENSE.

Acknowledgments

Standing on the shoulders of:

Inspired by the question: why isn't this just one command yet?

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lensgrep-0.1.1.tar.gz (34.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lensgrep-0.1.1-py3-none-any.whl (34.0 kB view details)

Uploaded Python 3

File details

Details for the file lensgrep-0.1.1.tar.gz.

File metadata

  • Download URL: lensgrep-0.1.1.tar.gz
  • Upload date:
  • Size: 34.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"AMD64","implementation":{"name":"CPython","version":"3.12.10"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.0.16 11 Feb 2025","python":"3.12.10","system":{"name":"Windows","release":"11"}} HTTPX2/2.4.0

File hashes

Hashes for lensgrep-0.1.1.tar.gz
Algorithm Hash digest
SHA256 956c5400a454eea1c0f51f74e0243ef8df0d2750e02b29c52417a2b9dca8e133
MD5 e0d1f2a490c7403735944fc0c2391765
BLAKE2b-256 e3a687048aae1c8d67431338098cc386c71daef91821bc930782ed44f4ae1c0c

See more details on using hashes here.

File details

Details for the file lensgrep-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: lensgrep-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 34.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"AMD64","implementation":{"name":"CPython","version":"3.12.10"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.0.16 11 Feb 2025","python":"3.12.10","system":{"name":"Windows","release":"11"}} HTTPX2/2.4.0

File hashes

Hashes for lensgrep-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9bdbbc48b2be30c2413d83036cfe434fddfd6756bb5ab7bad1a8540e36a1eb97
MD5 d27e61bd9b4139786d720eddf12f10a6
BLAKE2b-256 c263c30c2bbe675e7dd25d8b7031836cc17d327892b08c7857b9616bd62b2e29

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page