Type "people laughing at the beach" and get the right photos back in milliseconds. Embeddings run on serverless GPUs via Runpod Flash β no Docker, scale-to-zero.
Your phone has thousands of photos and the only way to find one is to scroll. Filenames are
IMG_4821.jpg. There are no tags. PhotoFinder fixes that: it understands what's in each photo,
so you can search the way you actually think β "my dog on the beach", "that whiteboard from the
meeting", "sunset over mountains" β and it just finds them. It also spots near-duplicates so you
can reclaim storage.
It's powered by OpenAI's CLIP, which embeds images and text into the same vector space β so a text query can be compared directly against every photo. The heavy lifting runs on Runpod Flash: GPU-backed serverless endpoints defined in pure Python, no Dockerfile, scaling from zero.
| Question | Answer |
|---|---|
| What does the workload do? | Embeds images and text with CLIP into one shared vector space, then ranks photos against a query by cosine similarity. |
| Where does Flash remove friction? | No Dockerfile, no registry, no infra. A @Endpoint-decorated Python function is the GPU service. flash deploy β live HTTPS endpoint. Workers scale from zero and bill per-second. |
| What input does it accept? | A folder of images (indexing) and a natural-language query string (search). |
| What output does it produce? | Ranked photos with similarity scores, plus grouped near-duplicates. |
| What becomes easier in a real product? | Photo apps, e-commerce catalogs, DAMs, and content moderation all need "find by meaning" + dedupe. This is that core, in ~150 lines, with the GPU cost only when a job actually runs. |
Prereqs: Python 3.10β3.13,
uv, and a Runpod account with a verified email.
git clone https://github.com/vnmoorthy/photofinder.git
cd photofinder
uv venv
source .venv/bin/activate
uv pip install -r requirements.txt
uv run flash login # one-time: authorize in browser
flash deploy # builds + deploys the GPU/CPU endpoints (first build ~5 min)
python fetch_samples.py # OR point step below at your own photo folder
python index.py sample_images
python -m uvicorn app:app --port 8000 # open http://localhost:8000Best demo:
python index.py ~/Picturesβ "search my photos" is the moment that lands.
βββββββββββββββββββββββββ Runpod Flash (serverless GPU) ββββββββββββββββββββββββ
β β
index.py ββββββΌβββΆ clip_embed( images ) β BATCH: fans out N parallel jobs, scales from 0 β
(your photos) β β β
β βΌ β
β embeddings.npy + paths.json (local vector index) β
β β
app.py ββββββΌβββΆ clip_embed( "beach at sunset" ) β LIVE: real-time query embedding β
(FastAPI) ββββββΌβββββββββ cosine rank over the index ββββββββββ ranked photos + scores β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
One model, two roles, defined in endpoints.py:
- Index time (async batch):
index.pysplits the library into chunks and fires them at the GPU endpoint in parallel βclip_embedruns on whatever GPU Flash assigns and reports its name back as proof. - Query time (real-time endpoint): every search embeds the query text on the same endpoint, then a sub-millisecond cosine search over the local index returns the best matches.
- Optional CPU stage:
fetch_and_prep(a CPU@Endpoint) downloads + resizes images from URLs, showing Flash as a true multi-endpoint pipeline, not just a deploy shortcut.
| File | Role |
|---|---|
endpoints.py |
Flash endpoints β clip_embed (GPU) + fetch_and_prep (CPU) |
index.py |
Async batch indexer β fans out embeddings across GPU workers |
app.py |
FastAPI app β live /search, /duplicates, serves the UI |
static/index.html |
The product UI |
fetch_samples.py |
Grabs demo photos so you can try it in 30s |
CLIP ViT-B/32 (sentence-transformers) Β· Runpod Flash serverless GPU Β· FastAPI Β· NumPy cosine retrieval Β· vanilla-JS UI.
MIT Β© 2026 vnmoorthy. See LICENSE.