Local deep-learning photo organization — powered by face recognition.
Face Sort Studio is a privacy-first tool that scans your photo gallery and automatically organizes images into folders based on the people in them. Using high-performance deep learning models, it detects every face and creates unique identity embeddings to match targets with precision—all while remaining 100% offline.
Everything runs locally on your hardware. No cloud uploads, no API keys, and no data leaves your machine.
- You upload reference photos containing the people you want to find.
- You provide a gallery — either by uploading photos or pointing to a local folder.
- The app detects every face, creates identity embeddings, and compares them.
- Photos are automatically sorted into folders:
| Folder | Contents |
|---|---|
matched/ |
Photos where the target people appear (based on your chosen mode) |
partial/ |
Photos where some but not all targets appear (All mode only) |
unmatched/ |
Photos with no target matches |
by_target/ |
Sub-folders per person — each person gets their own folder |
targets/ |
Cropped reference face thumbnails |
*.zip |
Compressed archives of each category for easy download |
report.json |
Full machine-readable results |
- 100% Offline: No data ever leaves your machine. All processing happens locally on your hardware.
- No Cloud Dependency: No external APIs (Azure, AWS, Google Cloud) are used for face analysis.
- Secure Persistence: Results and identity embeddings are stored in a local SQLite database.
- Zero Tracking: The application does not include telemetry or usage tracking.
| Layer | Technology |
|---|---|
| Backend | Flask (Python) |
| Frontend | HTML + Tailwind CSS + Vanilla JavaScript |
| Deep Learning | OpenCV DNN — YuNet face detector + SFace face recognizer |
| Database | SQLite via SQLAlchemy |
| Real-time Updates | Server-Sent Events (SSE) |
| Automation | PowerShell scripts + VS Code tasks |
- Portable EXE: Build a standalone Windows executable for easy distribution.
If you want to run the app without installing Python or any libraries, you can build a portable Windows .exe:
- Run
.\scripts\build.ps1in PowerShell. - Find
FaceSortStudio.exein thedist/folder. - You can move this EXE anywhere—it's self-contained!
face-sort-studio/
│
├── run.py # Entry point — starts Flask
├── requirements.txt # Python dependencies
│
├── face_sort/
│ └── app/
│ ├── main.py # Flask app factory + all routes
│ ├── config.py # Paths, thresholds, tunables
│ ├── database.py # SQLAlchemy models (Job, JobResult, TargetFace)
│ ├── bootstrap.py # Auto-downloads DL models if missing
│ │
│ ├── services/
│ │ ├── face_engine.py # Face detection + recognition wrapper
│ │ └── job_runner.py # Orchestrates a sorting run end-to-end
│ │
│ ├── templates/
│ │ └── index.html # Single-page UI (Jinja + Tailwind)
│ │
│ └── static/
│ ├── css/styles.css # Apple-inspired design system
│ └── js/app.js # Frontend logic (uploads, SSE, tabs)
│
├── scripts/
│ ├── setup.ps1 # One-click environment setup
│ ├── run.ps1 # One-click server start
│ └── share-with-tailscale.ps1 # Start app + open Tailscale Funnel
│
├── tests/
│ └── test_app.py # Smoke tests
│
├── data/ # Created at runtime
│ ├── database/ # SQLite file
│ ├── models/ # ONNX model files
│ ├── jobs/ # Temporary upload storage
│ └── outputs/ # Sorted results per job
│
└── .vscode/
├── tasks.json # Setup / Run / Test tasks
├── launch.json # Debug profile
└── settings.json # Python interpreter config
GitHub Pages can publish the static site in docs/, but it cannot run the Flask API, SQLite database, Server-Sent Events stream, or OpenCV face-sorting pipeline.
Use GitHub Pages for:
- a project landing page
- setup and usage documentation
- screenshots, demos, and repository links
Use a Python-capable environment for:
- the actual sorting application
- file uploads and local folder access
- model downloads and face matching jobs
To publish the static site:
- Push this repository to GitHub.
- In Settings > Pages, choose Deploy from a branch.
- Select your branch and the
/docsfolder. - Save, then wait for GitHub Pages to publish the site.
For the actual Flask + OpenCV app, the best fully free setup is to run it on your own machine and share it with Tailscale.
See docs/DEPLOY_FREE.md for:
- the safest GitHub setup for this project folder
- a one-command Tailscale sharing flow
- the shared-mode limitation around local folder paths
The pipeline runs in five stages:
Every image is passed through the YuNet deep neural network. YuNet is a lightweight face detector that outputs bounding boxes and facial landmarks. It handles multiple faces per image and works at various scales.
Each detected face is aligned using the landmarks and then passed through the SFace network. SFace produces a 128-dimensional normalised vector (an "embedding") that uniquely represents a person's face. Two embeddings of the same person will be close together in this vector space; two different people will be far apart.
All faces found in the reference photos are clustered by similarity. If you upload three photos of the same person, the app recognises them as one target (not three). This gives you a clean list of distinct people to search for.
Each gallery image is scanned. Every face found is compared against all target embeddings using cosine similarity. If the score exceeds your threshold, it is a match.
Based on your match mode:
- Any mode — a photo is "matched" if any target person appears in it.
- All mode — a photo is "matched" only if every target person appears. Photos with some (but not all) targets go into "partial".
Photos are copied (never moved) into the output folders.
- Python 3.11+ — python.org
- Windows — scripts are PowerShell; the core app runs on any OS
- Tailscale (optional) — only needed for the public sharing flow
- Open the project folder in VS Code.
- Press
Ctrl+Shift+P→ Tasks: Run Task → Face Sort Studio: Setup - Wait for setup to finish.
- Run task: Face Sort Studio: Run
- Open the URL printed in the terminal.
cd face-sort-studio
powershell -ExecutionPolicy Bypass -File .\scripts\setup.ps1
powershell -ExecutionPolicy Bypass -File .\scripts\run.ps1# 1. Create and activate virtual environment
python -m venv venv
# Windows:
.\venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate
# 2. Install dependencies
pip install -e .
# 3. Create data directories
python -c "from pathlib import Path; [Path(p).mkdir(parents=True, exist_ok=True) for p in ('data/database', 'data/jobs', 'data/outputs', 'data/models')]"
# 4. Run the app
face-sortThen open the URL printed by Flask.
Click or drag-and-drop one or more photos of the people you want to find. These can contain one face or many — the app detects and clusters all faces automatically.
Choose one of two methods:
- Upload Photos — drag-and-drop or select gallery images directly in the browser.
- Local Folder — paste the absolute path to a folder on your computer (e.g.
C:\Users\You\Pictures\Vacation).
- Match Mode
Any Person— photo matches if at least one target appears.All People— photo matches only if every target appears.
- Confidence Threshold (0.20 – 0.70)
- Higher = stricter matching (fewer false positives, may miss some real matches).
- Lower = more permissive (catches more real matches, may include some false positives).
- Default
0.38works well for most cases.
Click Start Sorting. The progress panel shows real-time updates via Server-Sent Events. You can see each image being processed and classified.
When complete, the results panel shows match/partial/unmatched counts. Output files are in:
data/outputs/<job-id>/
├── matched/
├── matched.zip # Compressed matched results
├── partial/
├── partial.zip # Compressed partial results
├── unmatched/
├── unmatched.zip # Compressed unmatched results
├── by_target/
│ ├── Person_01/
│ └── Person_02/
├── targets/
└── report.json
When the Flask backend is running, these endpoints are available:
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Serves the web UI |
POST |
/api/jobs |
Create a new sorting job (multipart form) |
GET |
/api/jobs |
List all jobs (newest first, max 50) |
GET |
/api/jobs/<id> |
Get status and stats for one job |
GET |
/api/jobs/<id>/stream |
SSE stream of real-time progress |
GET |
/api/jobs/<id>/report |
Download the full JSON report |
DELETE |
/api/jobs/<id> |
Delete a job and its files |
GET |
/api/analytics |
Dashboard summary statistics |
Form fields:
| Field | Type | Required | Description |
|---|---|---|---|
references |
File(s) | Yes | One or more reference photos |
gallery |
File(s) | If no path | Gallery photos to upload |
gallery_path |
String | If no files | Absolute path to a local folder |
match_mode |
String | No | any (default) or all |
threshold |
Float | No | 0.38 (default), range 0.20–0.70 |
SQLite database at data/database/face_sort_studio.db.
Three tables:
- jobs — one row per sorting run (status, settings, stats, timestamps)
- job_results — one row per gallery image (category, faces detected, matched targets)
- target_faces — one row per discovered target person (label, source file, embedding hash)
The schema is managed by SQLAlchemy and auto-created on first run.
python -m pytest tests/ -vTests cover: route availability, empty-state responses, config validation, and import checks.
- Clear, front-facing photos work best as references.
- Avoid very small, blurry, dark, or extreme-angle faces in references.
- If you get too many false matches → raise the threshold.
- If the app misses real matches → lower the threshold slightly.
- All mode is intentionally stricter — use it when you need group photos with everyone present.
- One good reference photo per person is usually enough; more help in marginal cases.
.jpg · .jpeg · .png · .bmp · .webp · .tif · .tiff
These are planned or suggested improvements for future versions:
- Person renaming — rename "Person_01" to actual names in the UI
- Face bounding box preview — overlay detected faces on image thumbnails
- Batch operations — re-run a previous job with different settings
- Dark mode — toggle between light and dark themes
- TensorFlow integration — swap OpenCV models for TensorFlow-based detection/recognition for improved accuracy on challenging photos
- Azure Blob Storage — optionally store outputs in Azure instead of local disk
- Azure SQL Database — swap SQLite for Azure SQL for multi-user deployments
- Power BI connector — export analytics data for Power BI dashboards
- WebSocket progress — replace SSE with WebSockets for bidirectional communication
- GPU acceleration — CUDA/DirectML support for faster processing of large galleries
- Multi-user mode — authentication and per-user job isolation
- REST API auth — API key or OAuth for programmatic access
- Docker container — one-command deployment
- Electron wrapper — native desktop app packaging
Why Flask instead of FastAPI? Flask's synchronous model is simpler to reason about for a local desktop-style app. Background jobs run in threads. The template engine (Jinja2) is built in. For a single-user local tool, Flask is the right balance of simplicity and power.
Why OpenCV DNN instead of TensorFlow? The OpenCV models (YuNet + SFace) are tiny ONNX files (~2 MB total) that run on CPU without CUDA. They provide excellent accuracy for face detection and recognition. TensorFlow integration is on the roadmap for users who want to push accuracy further on difficult photos.
Why SQLite?
Zero configuration, no separate database server, single file. Perfect for a local app. The SQLAlchemy ORM means swapping to PostgreSQL or Azure SQL later requires only changing the connection string in config.py.
Why SSE instead of WebSockets?
Server-Sent Events are simpler (one-directional: server → client), work over standard HTTP, and are all that is needed for progress streaming. The browser's EventSource API handles reconnection automatically.
MIT — see LICENSE.