Skip to content

Commit 2c9c63b

Browse files
authored
Merge pull request #1 from Shaan-alpha/showcase-readiness-fixes
Showcase-readiness hardening: offline UI, reliable install, upload safety, screenshots
2 parents e4e21bd + 7aa28a7 commit 2c9c63b

24 files changed

Lines changed: 1370 additions & 146 deletions

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ body:
1616
options:
1717
- label: I have verified that OpenCV ONNX models (`face_detection_yunet_2023mar.onnx` and `face_recognition_sface_2021dec.onnx`) are present in `models/`.
1818
required: true
19-
- label: I am using Python 3.8 or higher.
19+
- label: I am using Python 3.11 or higher.
2020
required: true
2121
- type: dropdown
2222
id: os

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ __pycache__/
1111

1212
# ── Data (local state) ───────────────────────────────────────
1313
data/
14+
instance/
15+
16+
# ── Node / Tailwind build tooling ────────────────────────────
17+
node_modules/
18+
# NOTE: the compiled face_sort/app/static/css/tailwind.css IS committed so
19+
# Python-only users don't need Node; rebuild it with `npm run build:css`.
1420

1521
# ── Secrets ───────────────────────────────────────────────────
1622
.env

.vscode/tasks.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
{
1313
"label": "Face Sort Studio: Run",
1414
"type": "shell",
15-
"command": "python",
15+
"command": "${workspaceFolder}\\venv\\Scripts\\python.exe",
1616
"args": ["run.py"],
1717
"group": "build",
1818
"presentation": { "reveal": "always", "panel": "new" },
@@ -22,7 +22,7 @@
2222
{
2323
"label": "Face Sort Studio: Test",
2424
"type": "shell",
25-
"command": "python",
25+
"command": "${workspaceFolder}\\venv\\Scripts\\python.exe",
2626
"args": ["-m", "pytest", "tests/", "-v"],
2727
"group": "test",
2828
"presentation": { "reveal": "always", "panel": "new" }

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
include VERSION
2+
include requirements.txt
23
recursive-include face_sort/app/static *
34
recursive-include face_sort/app/templates *

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44

55
**Local deep-learning photo organization — powered by face recognition.**
66

7+
<p align="center">
8+
<img src="docs/assets/ui-create-light.png" alt="Face Sort Studio — start a sorting job" width="760" />
9+
</p>
10+
<p align="center">
11+
<img src="docs/assets/ui-create-dark.png" alt="Dark mode" width="380" />
12+
<img src="docs/assets/ui-dashboard-light.png" alt="Analytics dashboard" width="380" />
13+
<br />
14+
<em>Polished, fully offline browser UI — with a built-in dark mode and an analytics dashboard.</em>
15+
</p>
16+
717
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.
818

919
Everything runs locally on your hardware. No cloud uploads, no API keys, and no data leaves your machine.
@@ -63,6 +73,21 @@ If you want to run the app without installing Python or any libraries, you can b
6373

6474
## 🚀 Quick Start (Local Setup)
6575

76+
**Windows, in three steps** (full setup options are in [Setup](#setup) below):
77+
78+
```powershell
79+
git clone https://github.com/Shaan-alpha/face-sort-studio.git
80+
cd face-sort-studio
81+
powershell -ExecutionPolicy Bypass -File .\scripts\setup.ps1 # creates a venv + downloads models
82+
powershell -ExecutionPolicy Bypass -File .\scripts\run.ps1 # starts the app + opens your browser
83+
```
84+
85+
> **First run downloads ~37 MB of AI models** (a one-time step). After that, everything runs fully offline.
86+
87+
**macOS / Linux:** the Flask core is pure Python — see the cross-platform steps in [Setup → Option C](#setup).
88+
89+
---
90+
6691
## Project Structure
6792

6893
```
@@ -105,8 +130,7 @@ face-sort-studio/
105130
106131
└── .vscode/
107132
├── tasks.json # Setup / Run / Test tasks
108-
├── launch.json # Debug profile
109-
└── settings.json # Python interpreter config
133+
└── launch.json # Debug profile
110134
```
111135

112136
---
@@ -180,7 +204,7 @@ Photos are copied (never moved) into the output folders.
180204
## Prerequisites
181205

182206
- **Python 3.11+**[python.org](https://python.org/downloads)
183-
- **Windows** — scripts are PowerShell; the core app runs on any OS
207+
- **Primary support: Windows**the automation scripts and portable EXE are Windows-only. The Flask core is pure Python and runs on macOS/Linux via the manual steps ([Option C](#setup)); the system-tray + auto-open-browser launcher (`run.py`) is a Windows-desktop convenience, while `face-sort` is the cross-platform headless command.
184208
- **Tailscale** (optional) — only needed for the public sharing flow
185209

186210
---
@@ -353,7 +377,6 @@ These are planned or suggested improvements for future versions:
353377
- **Person renaming** — rename "Person_01" to actual names in the UI
354378
- **Face bounding box preview** — overlay detected faces on image thumbnails
355379
- **Batch operations** — re-run a previous job with different settings
356-
- **Dark mode** — toggle between light and dark themes
357380

358381
### Medium-Term
359382
- **TensorFlow integration** — swap OpenCV models for TensorFlow-based detection/recognition for improved accuracy on challenging photos
@@ -377,7 +400,7 @@ These are planned or suggested improvements for future versions:
377400
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.
378401

379402
**Why OpenCV DNN instead of TensorFlow?**
380-
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.
403+
The OpenCV models (YuNet + SFace) are compact ONNX files (~37 MB total, downloaded once on first run) 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.
381404

382405
**Why SQLite?**
383406
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`.

docs/assets/ui-create-dark.png

294 KB
Loading

docs/assets/ui-create-light.png

293 KB
Loading

docs/assets/ui-dashboard-light.png

89 KB
Loading

docs/index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,22 @@
1313
<body>
1414
<div class="page-shell">
1515
<header class="hero">
16-
<p class="eyebrow">v2.1.1 Release</p>
16+
<p class="eyebrow">v2.1.4 Release</p>
1717
<h1>Face Sort Studio</h1>
1818
<p class="hero-copy">
19-
Professional local photo organization. Sort galleries by face with
19+
Professional local photo organization. Sort galleries by face with
2020
high-performance deep learning models and a polished browser UI—all 100% offline.
2121
</p>
2222
<div class="hero-actions">
23-
<a class="button button-primary" href="#setup">Local Setup</a>
23+
<a class="button button-primary" href="https://github.com/Shaan-alpha/face-sort-studio">View on GitHub</a>
24+
<a class="button button-secondary" href="#setup">Local Setup</a>
2425
<a class="button button-secondary" href="#hosting">Hosting Notes</a>
2526
</div>
27+
<img
28+
src="./assets/ui-create-light.png"
29+
alt="Face Sort Studio browser interface"
30+
style="margin-top:2rem;width:100%;max-width:880px;height:auto;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,0.18);"
31+
/>
2632
</header>
2733

2834
<main>
@@ -76,7 +82,7 @@ <h2>Why Pages Still Helps</h2>
7682
<section id="setup" class="card card-wide">
7783
<h2>Run It Locally</h2>
7884
<ol>
79-
<li>Install Python 3.13 or newer.</li>
85+
<li>Install Python 3.11 or newer.</li>
8086
<li>Install dependencies with <code>python -m pip install -r requirements.txt</code>.</li>
8187
<li>Create the data folders or run <code>scripts/setup.ps1</code>.</li>
8288
<li>Start the app with <code>python run.py</code> or <code>scripts/run.ps1</code>.</li>

face_sort/app/bootstrap.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
==============================
44
Downloads YuNet and SFace ONNX models from the OpenCV zoo
55
if they aren't already present in data/models/.
6+
7+
Downloads are written to a temporary ``*.part`` file and only renamed into
8+
place once they pass a minimum-size check, so an interrupted download can
9+
never leave a truncated file that later fails to load.
610
"""
711

812
import os
@@ -20,21 +24,63 @@
2024
),
2125
}
2226

27+
# Minimum expected sizes (bytes) — used to detect truncated/partial files.
28+
# Actual sizes: YuNet ~0.23 MB, SFace ~36.9 MB. Floors are set well below
29+
# the real sizes so a complete file always passes but a truncated one fails.
30+
MODEL_MIN_BYTES = {
31+
"face_detection_yunet_2023mar.onnx": 150_000,
32+
"face_recognition_sface_2021dec.onnx": 30_000_000,
33+
}
34+
35+
36+
def _is_complete(path: str, filename: str) -> bool:
37+
"""A model file is considered complete only if it meets its size floor."""
38+
if not os.path.exists(path):
39+
return False
40+
min_bytes = MODEL_MIN_BYTES.get(filename, 1)
41+
return os.path.getsize(path) >= min_bytes
42+
2343

2444
def ensure_models_exist(models_dir: str) -> None:
25-
"""Download any missing model files into *models_dir*."""
45+
"""Download any missing or incomplete model files into *models_dir*."""
2646
os.makedirs(models_dir, exist_ok=True)
2747

2848
for filename, url in MODEL_URLS.items():
2949
dest = os.path.join(models_dir, filename)
30-
if os.path.exists(dest):
50+
51+
if _is_complete(dest, filename):
3152
continue
53+
54+
# Remove a stale/truncated file left by a previous interrupted run.
55+
if os.path.exists(dest):
56+
print(
57+
f"[bootstrap] {filename} looks incomplete — re-downloading …",
58+
file=sys.stderr,
59+
)
60+
try:
61+
os.remove(dest)
62+
except OSError:
63+
pass
64+
65+
tmp = dest + ".part"
3266
print(f"[bootstrap] Downloading {filename} …")
3367
try:
34-
urllib.request.urlretrieve(url, dest)
68+
urllib.request.urlretrieve(url, tmp)
69+
if not _is_complete(tmp, filename):
70+
raise IOError(
71+
f"downloaded file is smaller than expected "
72+
f"({os.path.getsize(tmp)} bytes)"
73+
)
74+
os.replace(tmp, dest) # atomic rename — only a complete file lands
3575
size_mb = os.path.getsize(dest) / (1024 * 1024)
3676
print(f"[bootstrap] Saved {filename} ({size_mb:.1f} MB)")
3777
except Exception as exc:
78+
# Clean up the partial download so the next run retries cleanly.
79+
if os.path.exists(tmp):
80+
try:
81+
os.remove(tmp)
82+
except OSError:
83+
pass
3884
print(
3985
f"[bootstrap] WARNING — could not download {filename}: {exc}",
4086
file=sys.stderr,

0 commit comments

Comments
 (0)