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.

Releasing (maintainer notes)

Releases are token-less via PyPI Trusted Publishing — GitHub Actions OIDC. No tokens live in CI secrets.

To cut a new release:

# 1. Bump version in pyproject.toml and src/lensgrep/__init__.py
# 2. Commit
git commit -am "chore: bump to v0.2.0"
git push

# 3. Tag and push the tag — the release workflow handles the rest
git tag -a v0.2.0 -m "v0.2.0 — feature summary"
git push origin v0.2.0

GitHub Actions builds the wheel and sdist, verifies the tag matches pyproject.toml, and publishes to pypi.org via OIDC.

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.2.tar.gz (36.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.2-py3-none-any.whl (34.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: lensgrep-0.1.2.tar.gz
  • Upload date:
  • Size: 36.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for lensgrep-0.1.2.tar.gz
Algorithm Hash digest
SHA256 23dfe00f15e2b7299265a47dfd3a8fa6ef4002114e9bea04b14d44a0ecd5be16
MD5 60581b1653b4a85419337faee0ffc5ca
BLAKE2b-256 5f1d1c94e0eb4d8dd1c5983ca56cfa90fe43312c2885bac1d18076dd1341058a

See more details on using hashes here.

Provenance

The following attestation bundles were made for lensgrep-0.1.2.tar.gz:

Publisher: release.yml on Kris-O/lensgrep

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: lensgrep-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 34.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for lensgrep-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 89fdb81aeb30eae45c4129feccd944847164d7d7bb117e16a17f122e15fb014a
MD5 04abdcc7d6e376e97bc63d299261ad1e
BLAKE2b-256 b9e9bc0a0c3f8dac0321950bb460a3e205827713bb7b996957d6aeaeb7218a3a

See more details on using hashes here.

Provenance

The following attestation bundles were made for lensgrep-0.1.2-py3-none-any.whl:

Publisher: release.yml on Kris-O/lensgrep

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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