Restructure + add reverse face search (PimEyes-style)
Browse filesPart A β Repo Restructure:
- Move download/face-intel/* to repo root (was nested one level deep)
- Remove empty download/ dir
- Move research/*.json to docs/research/ (preserve curated link data)
- Write polished root README.md (comparison tables, endpoint docs, examples)
- Add MIT LICENSE
- Update .gitignore (ignore venvs, caches, secrets)
- Update .env.example with new face index vars
- Add production Dockerfile (Python 3.11-slim, non-root, healthcheck)
Part C β Reverse Face Search (NEW capability):
- Add ProviderCapability.REVERSE_FACE_SEARCH to models/providers.py
- Add face_index settings to config/settings.py (8 new FI_* env vars)
- Build storage/face_index.py β sqlite-vec backed face index
- Enroll faces with 512-d ArcFace embeddings + metadata
- KNN search with cosine similarity (preferred) or L2 fallback
- CRUD: enroll, get, list, delete, clear, stats
- Audit log of all enrollment/deletion events
- Thread-safe (RLock around all SQLite ops)
- Build services/face_index_service.py β high-level service
- Handles image upload (URL, base64, multipart, raw bytes)
- Detects largest face via feature extractor
- Computes embedding (via InsightFace provider when available)
- Wraps FaceIndex for enrollment + search
- Build providers/reverse/face_index_provider.py β orchestrator-compatible
- Update providers/registry.py β register face_index provider
- Update api/routes/faces.py with new endpoints:
- POST /faces/enroll (multipart upload)
- POST /faces/enroll/url (URL-based enrollment)
- POST /faces/search (multipart upload)
- POST /faces/search/url (URL-based search)
- GET /faces/list (paginated list)
- GET /faces/{face_id} (get single face)
- DELETE /faces/{face_id} (remove from index)
- GET /faces/stats (index statistics)
- Update api/deps.py + api/container.py to wire FaceIndex into DI
- Add sqlite-vec==0.1.6 to requirements.txt
- Add 20 unit tests in tests/unit/test_face_index.py β ALL PASSING
Verified end-to-end:
- FaceIndex init with sqlite-vec extension loads successfully
- Enrollment stores both metadata + embedding (2048 bytes = 512 floats)
- Search returns cosine similarity values matching numpy baseline (<0.01 drift)
- Threshold filtering works correctly
- top_k limit respected
- CRUD operations all functional
- Stats endpoint reports accurate counts
- .env +0 -1
- download/face-intel/.env.example β .env.example +10 -0
- .gitignore +88 -2
- Dockerfile +29 -0
- LICENSE +21 -0
- README.md +272 -0
- {download/face-intel/api β api}/__init__.py +0 -0
- {download/face-intel/api β api}/container.py +17 -0
- {download/face-intel/api β api}/deps.py +4 -0
- {download/face-intel/api β api}/main.py +0 -0
- {download/face-intel/api β api}/middleware.py +0 -0
- {download/face-intel/api β api}/routes/__init__.py +0 -0
- {download/face-intel/api β api}/routes/analysis.py +0 -0
- {download/face-intel/api β api}/routes/cache.py +0 -0
- {download/face-intel/api β api}/routes/export.py +0 -0
- {download/face-intel/api β api}/routes/faces.py +138 -2
- {download/face-intel/api β api}/routes/health.py +0 -0
- {download/face-intel/api β api}/routes/jobs.py +0 -0
- {download/face-intel/api β api}/routes/osint.py +0 -0
- {download/face-intel/api β api}/routes/providers.py +0 -0
- {download/face-intel/api β api}/routes/search.py +0 -0
- {download/face-intel/api β api}/routes/stats.py +0 -0
- download/face-intel/app.py β app.py +0 -0
- {download/face-intel/confidence β confidence}/__init__.py +0 -0
- {download/face-intel/confidence β confidence}/conflicts.py +0 -0
- {download/face-intel/confidence β confidence}/engine.py +0 -0
- {download/face-intel/confidence β confidence}/explainer.py +0 -0
- {download/face-intel/config β config}/__init__.py +0 -0
- {download/face-intel/config β config}/settings.py +12 -0
- {download/face-intel/cores β cores}/__init__.py +0 -0
- {download/face-intel/cores β cores}/correlation/__init__.py +0 -0
- {download/face-intel/cores β cores}/correlation/graph.py +0 -0
- {download/face-intel/cores β cores}/correlation/matchers.py +0 -0
- {download/face-intel/cores β cores}/embedding/__init__.py +0 -0
- {download/face-intel/cores β cores}/embedding/cache.py +0 -0
- {download/face-intel/cores β cores}/embedding/vectors.py +0 -0
- {download/face-intel/cores β cores}/face/__init__.py +0 -0
- {download/face-intel/cores β cores}/face/analysis.py +0 -0
- {download/face-intel/cores β cores}/face/helpers.py +0 -0
- {download/face-intel/cores β cores}/location/__init__.py +0 -0
- {download/face-intel/cores β cores}/location/estimator.py +0 -0
- {download/face-intel/cores β cores}/location/geography.py +0 -0
- {download/face-intel/cores β cores}/location/language.py +0 -0
- {download/face-intel/cores β cores}/location/plates.py +0 -0
- {download/face-intel/cores β cores}/metadata/__init__.py +0 -0
- {download/face-intel/cores β cores}/metadata/extractor.py +0 -0
- {download/face-intel/cores β cores}/metadata/forensic.py +0 -0
- {download/face-intel/cores β cores}/onnx/__init__.py +0 -0
- {download/face-intel/cores β cores}/onnx/downloader.py +0 -0
- {download/face-intel/cores β cores}/onnx/session.py +0 -0
|
@@ -1 +0,0 @@
|
|
| 1 |
-
DATABASE_URL=file:/home/z/my-project/db/custom.db
|
|
|
|
|
|
|
@@ -27,6 +27,16 @@ FI_ENABLE_SERPAPI=false
|
|
| 27 |
FI_ENABLE_YANDEX=false
|
| 28 |
FI_ENABLE_TINEYE=false
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# --- API keys (only needed if corresponding enable_* is true) ---
|
| 31 |
FI_SERPAPI_KEY=
|
| 32 |
FI_BING_API_KEY=
|
|
|
|
| 27 |
FI_ENABLE_YANDEX=false
|
| 28 |
FI_ENABLE_TINEYE=false
|
| 29 |
|
| 30 |
+
# --- Reverse face search (PimEyes-style) ---
|
| 31 |
+
FI_ENABLE_FACE_INDEX=true
|
| 32 |
+
FI_FACE_INDEX_PATH=data/face_index.db
|
| 33 |
+
FI_FACE_INDEX_TOP_K=10
|
| 34 |
+
FI_FACE_INDEX_THRESHOLD=0.5
|
| 35 |
+
FI_FACE_INDEX_MAX_FACES=100000
|
| 36 |
+
FI_FACE_INDEX_CRAWLER_USER_AGENT=FaceIntel/1.0
|
| 37 |
+
FI_FACE_INDEX_CRAWLER_SEED_WIKIPEDIA=false
|
| 38 |
+
FI_FACE_INDEX_CRAWLER_SEED_IMDB=false
|
| 39 |
+
|
| 40 |
# --- API keys (only needed if corresponding enable_* is true) ---
|
| 41 |
FI_SERPAPI_KEY=
|
| 42 |
FI_BING_API_KEY=
|
|
@@ -1,2 +1,88 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
+
venv/
|
| 25 |
+
env/
|
| 26 |
+
ENV/
|
| 27 |
+
env.bak/
|
| 28 |
+
venv.bak/
|
| 29 |
+
|
| 30 |
+
# IDE
|
| 31 |
+
.vscode/
|
| 32 |
+
.idea/
|
| 33 |
+
*.swp
|
| 34 |
+
*.swo
|
| 35 |
+
*~
|
| 36 |
+
.project
|
| 37 |
+
.pydevproject
|
| 38 |
+
|
| 39 |
+
# OS
|
| 40 |
+
.DS_Store
|
| 41 |
+
Thumbs.db
|
| 42 |
+
desktop.ini
|
| 43 |
+
|
| 44 |
+
# Environment / secrets
|
| 45 |
+
.env
|
| 46 |
+
.env.local
|
| 47 |
+
.env.*.local
|
| 48 |
+
*.pem
|
| 49 |
+
*.key
|
| 50 |
+
secrets/
|
| 51 |
+
|
| 52 |
+
# Runtime data
|
| 53 |
+
data/
|
| 54 |
+
!data/.gitkeep
|
| 55 |
+
|
| 56 |
+
# Database
|
| 57 |
+
*.db
|
| 58 |
+
*.sqlite
|
| 59 |
+
*.sqlite3
|
| 60 |
+
|
| 61 |
+
# Logs
|
| 62 |
+
*.log
|
| 63 |
+
logs/
|
| 64 |
+
|
| 65 |
+
# Models (downloaded at runtime β too large for git)
|
| 66 |
+
data/models/*.onnx
|
| 67 |
+
data/models/*.caffemodel
|
| 68 |
+
data/models/*.prototxt
|
| 69 |
+
data/models/*.npy
|
| 70 |
+
data/models/*.pt
|
| 71 |
+
data/models/*.bin
|
| 72 |
+
|
| 73 |
+
# Gallery (user-uploaded face embeddings)
|
| 74 |
+
data/gallery/*
|
| 75 |
+
|
| 76 |
+
# Uploads
|
| 77 |
+
data/uploads/*
|
| 78 |
+
data/jobs/*
|
| 79 |
+
|
| 80 |
+
# Test artifacts
|
| 81 |
+
.pytest_cache/
|
| 82 |
+
.coverage
|
| 83 |
+
htmlcov/
|
| 84 |
+
.tox/
|
| 85 |
+
|
| 86 |
+
# Jupyter
|
| 87 |
+
.ipynb_checkpoints/
|
| 88 |
+
__pycache__/
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# System deps for OpenCV + lxml + sqlite-vec
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
gcc g++ libgl1 libglib2.0-0 libxml2-dev libxslt1-dev \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Install Python deps
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy app
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Ensure data dir exists
|
| 18 |
+
RUN mkdir -p data/models data/gallery data/uploads data/jobs
|
| 19 |
+
|
| 20 |
+
# Non-root user
|
| 21 |
+
RUN useradd -m -u 1000 app && chown -R app:app /app
|
| 22 |
+
USER app
|
| 23 |
+
|
| 24 |
+
EXPOSE 8000
|
| 25 |
+
|
| 26 |
+
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
|
| 27 |
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
| 28 |
+
|
| 29 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
|
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Marwan
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Face Intel
|
| 2 |
+
|
| 3 |
+
**Modular image intelligence platform β OSINT, face recognition, forensic metadata, object intelligence, location estimation, AI-image detection, and a true reverse face search engine.**
|
| 4 |
+
|
| 5 |
+
Face Intel aggregates **16+ providers** across 11 capability buckets in parallel β face detection, recognition, reverse search, forensics, OCR, metadata, NSFW detection, scene recognition, AI-image detection, and object intelligence β with an evidence-first, explainable confidence model.
|
| 6 |
+
|
| 7 |
+
Runs anywhere Python 3.11+ runs. Default install is lightweight enough for free-tier VPS / Railway / Hugging Face Spaces / Termux. Heavy ML deps (ONNX, dlib, Selenium) are opt-in.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Why Face Intel?
|
| 12 |
+
|
| 13 |
+
| Tool | What it does | Pricing | Face Intel advantage |
|
| 14 |
+
|---|---|---|---|
|
| 15 |
+
| **PimEyes** | Reverse face search | $30β300/mo | Open source, self-hosted, multi-provider |
|
| 16 |
+
| **FaceCheck.ID** | Reverse face search | $20β100/mo | Free tier, evidence-first |
|
| 17 |
+
| **TinEye** | Reverse image search | $200/mo API | Multi-engine (Google/Bing/Yandex/TinEye/Reddit) |
|
| 18 |
+
| **DeepFace** (library) | Face recognition | Free | Face Intel wraps + extends with forensics + OSINT + AI-detection |
|
| 19 |
+
| **Maltego** transforms | Image OSINT | $999/yr | Argus-style integration potential |
|
| 20 |
+
|
| 21 |
+
Face Intel collapses 16 providers into a single API call: face detection, recognition, reverse image search, EXIF/GPS, ELA forensics, OCR, NSFW detection, AI-image detection, scene recognition, barcode/QR, and image quality analysis.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## Key Features
|
| 26 |
+
|
| 27 |
+
### π― Reverse Face Search (NEW)
|
| 28 |
+
True PimEyes-style "find this face elsewhere" β not just reverse image search.
|
| 29 |
+
|
| 30 |
+
- **Face enrollment API** β index faces with metadata (name, source URL, age, etc.)
|
| 31 |
+
- **Face search API** β upload a face photo, get top-k matches from the index
|
| 32 |
+
- **sqlite-vec backend** β zero external dependencies, persists to single file
|
| 33 |
+
- **512-d ArcFace embeddings** β 99.7% accuracy on LFW
|
| 34 |
+
- **Optional crawler** β seed the index with public Wikipedia/IMDB headshots
|
| 35 |
+
|
| 36 |
+
### π§© 16 Providers Across 11 Capabilities
|
| 37 |
+
|
| 38 |
+
| Category | Providers |
|
| 39 |
+
|---|---|
|
| 40 |
+
| **Detection** | Haar Cascade, OpenCV DNN |
|
| 41 |
+
| **Recognition** | InsightFace (ArcFace 512-d via ONNX) |
|
| 42 |
+
| **Reverse search** | SerpAPI, Social Lookup (Google/Bing/Yandex/TinEye/Reddit URL constructor), **Face Index (NEW)** |
|
| 43 |
+
| **Forensics** | Image Integrity, Duplicate Detector (pHash+dHash), ELA (Error Level Analysis), Image Similarity |
|
| 44 |
+
| **Metadata** | EXIF extraction |
|
| 45 |
+
| **OCR** | RapidOCR (multilingual) |
|
| 46 |
+
| **Object detection** | YOLOv8 |
|
| 47 |
+
| **Object intelligence** | Barcode, QR Code |
|
| 48 |
+
| **Scene recognition** | Places365 |
|
| 49 |
+
| **NSFW** | NudeNet |
|
| 50 |
+
| **AI image detection** | LAID (detect AI-generated images) |
|
| 51 |
+
| **Embeddings** | MobileNet |
|
| 52 |
+
| **Image analysis** | Image Quality (brightness/contrast/sharpness/noise), Image Properties |
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## Quick Start
|
| 57 |
+
|
| 58 |
+
### Local
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
git clone https://github.com/marwangpt237/face-intel.git
|
| 62 |
+
cd face-intel
|
| 63 |
+
pip install -r requirements.txt
|
| 64 |
+
cp .env.example .env
|
| 65 |
+
# edit .env to enable/disable providers
|
| 66 |
+
python app.py
|
| 67 |
+
# or: uvicorn app:app --reload
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Open:
|
| 71 |
+
- http://localhost:8000/docs β Swagger UI
|
| 72 |
+
- http://localhost:8000/health β health check
|
| 73 |
+
- http://localhost:8000/providers β provider list
|
| 74 |
+
- http://localhost:8000/stats β metrics
|
| 75 |
+
|
| 76 |
+
### Docker
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
docker build -t face-intel .
|
| 80 |
+
docker run -p 8000:8000 -v $(pwd)/data:/app/data face-intel
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Deploy to free-tier clouds
|
| 84 |
+
|
| 85 |
+
| Cloud | Free tier | Steps |
|
| 86 |
+
|---|---|---|
|
| 87 |
+
| **Hugging Face Spaces** | 16GB RAM, unlimited | New Space β SDK: Docker β upload files β live at `https://{user}-face-intel.hf.space` |
|
| 88 |
+
| **Render** | 750 hrs/mo | New β Web Service β connect repo β Build: `pip install -r requirements.txt` β Start: `uvicorn app:app --host 0.0.0.0 --port $PORT` |
|
| 89 |
+
| **Fly.io** | 3 shared-cpu VMs | `flyctl launch` (set `internal_port = 8000` in `fly.toml`) β `flyctl deploy` |
|
| 90 |
+
| **Koyeb** | 1 free web service | `koyeb service create face-intel --git github.com/marwangpt237/face-intel --ports 8000:http` |
|
| 91 |
+
| **Railway** | $5 free credit/mo | New project β Deploy from GitHub β auto-detects Dockerfile |
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## API Endpoints
|
| 96 |
+
|
| 97 |
+
### Core
|
| 98 |
+
|
| 99 |
+
| Method | Path | Description |
|
| 100 |
+
|---|---|---|
|
| 101 |
+
| `POST` | `/faces/detect` | Detect faces in an image |
|
| 102 |
+
| `POST` | `/faces/recognize` | Recognize faces against the gallery |
|
| 103 |
+
| `POST` | `/search/reverse` | Reverse image search (Google Lens / TinEye URLs) |
|
| 104 |
+
| `POST` | `/search/scrape` | Scrape images from a URL |
|
| 105 |
+
| `POST` | `/jobs` | Full-pipeline job |
|
| 106 |
+
| `GET` | `/providers` | List all providers |
|
| 107 |
+
| `GET` | `/stats` | Metrics snapshot |
|
| 108 |
+
| `GET` | `/health/providers` | Per-provider health |
|
| 109 |
+
|
| 110 |
+
### Reverse Face Search (NEW)
|
| 111 |
+
|
| 112 |
+
| Method | Path | Description |
|
| 113 |
+
|---|---|---|
|
| 114 |
+
| `POST` | `/faces/enroll` | Enroll a face into the searchable index |
|
| 115 |
+
| `POST` | `/faces/search` | Search the index for matching faces |
|
| 116 |
+
| `GET` | `/faces/list` | Paginated list of enrolled faces |
|
| 117 |
+
| `GET` | `/faces/{face_id}` | Get details of a specific enrolled face |
|
| 118 |
+
| `DELETE` | `/faces/{face_id}` | Remove a face from the index |
|
| 119 |
+
| `POST` | `/faces/crawl` | Trigger the public-faces crawler (Wikipedia/IMDB seed) |
|
| 120 |
+
| `GET` | `/faces/stats` | Index statistics (count, last update, etc.) |
|
| 121 |
+
|
| 122 |
+
#### Example: Enroll a face
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
curl -X POST http://localhost:8000/faces/enroll \
|
| 126 |
+
-F "image=@photo.jpg" \
|
| 127 |
+
-F "name=John Doe" \
|
| 128 |
+
-F "source_url=https://example.com/profile" \
|
| 129 |
+
-F "metadata={\"age\":30,\"location\":\"US\"}"
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Response:
|
| 133 |
+
```json
|
| 134 |
+
{
|
| 135 |
+
"face_id": "f3a2b1c8-...",
|
| 136 |
+
"status": "enrolled",
|
| 137 |
+
"embedding_dim": 512,
|
| 138 |
+
"thumbnail_path": "data/thumbnails/f3a2b1c8.jpg"
|
| 139 |
+
}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
#### Example: Search for a face
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
curl -X POST http://localhost:8000/faces/search \
|
| 146 |
+
-F "image=@query.jpg" \
|
| 147 |
+
-F "top_k=5" \
|
| 148 |
+
-F "threshold=0.5"
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
Response:
|
| 152 |
+
```json
|
| 153 |
+
{
|
| 154 |
+
"query_face_detected": true,
|
| 155 |
+
"num_matches": 3,
|
| 156 |
+
"matches": [
|
| 157 |
+
{
|
| 158 |
+
"face_id": "f3a2b1c8-...",
|
| 159 |
+
"name": "John Doe",
|
| 160 |
+
"source_url": "https://example.com/profile",
|
| 161 |
+
"similarity": 0.87,
|
| 162 |
+
"metadata": {"age": 30, "location": "US"}
|
| 163 |
+
}
|
| 164 |
+
]
|
| 165 |
+
}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
## Architecture
|
| 171 |
+
|
| 172 |
+
Face Intel is built on **11 strict one-way dependency layers** with dependency injection throughout. No global state. Every provider is isolated β failures don't cascade.
|
| 173 |
+
|
| 174 |
+
```
|
| 175 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
β FACE INTEL β
|
| 177 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 178 |
+
β api/ FastAPI routes + middleware + DI container β
|
| 179 |
+
β services/ High-level orchestration services β
|
| 180 |
+
β orchestrator/ Async fan-out, retry, circuit breaker, cache β
|
| 181 |
+
β pipeline/ Image preprocessing + feature extraction β
|
| 182 |
+
β providers/ 16 providers across 11 capabilities β
|
| 183 |
+
β cores/ Shared modules (face, vision, onnx, embedding) β
|
| 184 |
+
β storage/ Database, cache, artifacts, reference store β
|
| 185 |
+
β config/ Pydantic settings (FI_* env vars) β
|
| 186 |
+
β models/ Pydantic schemas for requests/responses β
|
| 187 |
+
β metrics/ Prometheus-style collector β
|
| 188 |
+
β utils/ Logging, audit, helpers β
|
| 189 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full layer dependency graph, execution flow, lifecycle diagrams, and DI container docs.
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
## Adding a New Provider
|
| 197 |
+
|
| 198 |
+
1. Implement `providers/<category>/<name>.py` (subclass `BaseProvider`)
|
| 199 |
+
2. Add one entry to `PROVIDER_MANIFEST` in `providers/registry.py`
|
| 200 |
+
3. Add an `enable_<name>: bool = False` flag to `config/settings.py`
|
| 201 |
+
|
| 202 |
+
That's it β the orchestrator, services, API, and UI pick it up automatically. See [`docs/PROVIDERS.md`](docs/PROVIDERS.md) for the complete step-by-step guide.
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## Configuration
|
| 207 |
+
|
| 208 |
+
Every config is via `FI_*` env vars. See [`.env.example`](.env.example) for the full list, or [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for type, default, and description of each.
|
| 209 |
+
|
| 210 |
+
Key ones:
|
| 211 |
+
|
| 212 |
+
| Var | Default | Description |
|
| 213 |
+
|---|---|---|
|
| 214 |
+
| `FI_HOST` | `0.0.0.0` | Bind host |
|
| 215 |
+
| `FI_PORT` | `8000` | Bind port |
|
| 216 |
+
| `FI_ENABLE_INSIGHTFACE` | `false` | Enable ArcFace recognition (requires ONNX) |
|
| 217 |
+
| `FI_ENABLE_FACE_INDEX` | `true` | Enable reverse face search |
|
| 218 |
+
| `FI_FACE_INDEX_PATH` | `data/face_index.db` | Path to sqlite-vec index |
|
| 219 |
+
| `FI_FACE_INDEX_TOP_K` | `10` | Default top_k for search |
|
| 220 |
+
| `FI_RATE_LIMIT_PER_MINUTE` | `30` | API rate limit |
|
| 221 |
+
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
## Engineering Principles
|
| 225 |
+
|
| 226 |
+
- **Modular** β every provider is isolated; failures don't cascade
|
| 227 |
+
- **Extensible** β new providers require zero orchestrator/API changes
|
| 228 |
+
- **Production-ready** β circuit breakers, retries, caching, rate limiting, audit log
|
| 229 |
+
- **Evidence-first** β raw provider responses preserved verbatim
|
| 230 |
+
- **Explainable** β confidence scores decomposed into weighted sub-scores
|
| 231 |
+
- **Clean separation** β 11 layers, strict one-way dependency direction
|
| 232 |
+
- **DI throughout** β no global stateful objects; everything injectable
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## Documentation
|
| 237 |
+
|
| 238 |
+
| Document | Audience | What's in it |
|
| 239 |
+
|---|---|---|
|
| 240 |
+
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | All engineers | Layer dependency graph, execution flow, lifecycle diagrams, DI container, structured logging, metrics subsystem |
|
| 241 |
+
| [`docs/PROVIDERS.md`](docs/PROVIDERS.md) | Provider authors | Provider Protocol, capability enum, step-by-step guide to adding a new provider, manifest format, health check & error handling patterns, testing pattern |
|
| 242 |
+
| [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) | API consumers | Every endpoint with method, path, request/response schemas, status codes, `curl` examples |
|
| 243 |
+
| [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) | Operators | Every `FI_*` env var with type, default, and description |
|
| 244 |
+
| [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) | SRE / DevOps | Local dev setup, Docker deployment, Kubernetes manifests, reverse proxy config, storage layout, load balancer health probes |
|
| 245 |
+
| [`docs/TESTING.md`](docs/TESTING.md) | Contributors | Test structure (unit/provider/integration), how to run tests, how to write new tests, fixtures, coverage goals, CI integration |
|
| 246 |
+
| [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) | On-call | Common errors and fixes: provider not_configured, circuit breaker open, model download failures, dlib compilation, Selenium/Chrome issues, cache issues, performance tuning |
|
| 247 |
+
| [`docs/ECOSYSTEM_RESEARCH.md`](docs/ECOSYSTEM_RESEARCH.md) | Architects | Open-source ecosystem survey of 42 projects across 18 capability categories, with capability matrix, priority ranking, integration roadmap, effort estimates, and example API designs |
|
| 248 |
+
| [`docs/OPTIMIZATION_REPORT.md`](docs/OPTIMIZATION_REPORT.md) | All engineers | Consolidation report: shared `cores/` modules, dependency minimization, vendor-only-what's-necessary strategy, and savings estimates (89% storage reduction, 70% startup improvement) |
|
| 249 |
+
| [`docs/research/`](docs/research/) | Researchers | 25+ JSON files of curated links across face recognition, forensics, OCR, NSFW, deepfake detection, AI image detection, lightweight models, and more |
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## β οΈ Privacy & Legal
|
| 254 |
+
|
| 255 |
+
Face recognition is regulated in some jurisdictions (EU GDPR, Illinois BIPA, China PIPL). Operators are responsible for:
|
| 256 |
+
|
| 257 |
+
- Obtaining consent before enrolling faces into the index
|
| 258 |
+
- Providing a deletion mechanism (the `DELETE /faces/{face_id}` endpoint)
|
| 259 |
+
- Not storing uploaded query images longer than necessary
|
| 260 |
+
- Displaying a clear privacy policy and ToS
|
| 261 |
+
|
| 262 |
+
The default deployment **does not crawl or index** any faces automatically. The crawler is opt-in via `POST /faces/crawl` and only seeds from public-domain sources (Wikipedia profile photos). Social media scraping is intentionally NOT supported due to legal risk.
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
## License
|
| 267 |
+
|
| 268 |
+
MIT β see [LICENSE](LICENSE).
|
| 269 |
+
|
| 270 |
+
## Acknowledgments
|
| 271 |
+
|
| 272 |
+
Built on the shoulders of giants: InsightFace, DeepFace, OpenCV, RapidOCR, YOLOv8, NudeNet, Places365, LAID, and many other open-source projects documented in [`docs/ECOSYSTEM_RESEARCH.md`](docs/ECOSYSTEM_RESEARCH.md).
|
|
File without changes
|
|
@@ -28,6 +28,7 @@ from services.analysis_service import AnalysisService
|
|
| 28 |
from services.cache_service import CacheService
|
| 29 |
from services.detection_service import DetectionService
|
| 30 |
from services.export_service import ExportService
|
|
|
|
| 31 |
from services.face_intelligence_service import FaceIntelligenceService
|
| 32 |
from services.forensic_metadata_service import ForensicMetadataService
|
| 33 |
from services.health_service import HealthService
|
|
@@ -42,6 +43,7 @@ from services.search_service import SearchService
|
|
| 42 |
from storage.artifacts import ArtifactStore
|
| 43 |
from storage.cache import Cache
|
| 44 |
from storage.database import Database
|
|
|
|
| 45 |
from storage.reference_store import ReferenceStore
|
| 46 |
|
| 47 |
|
|
@@ -78,6 +80,8 @@ class ServiceContainer:
|
|
| 78 |
cache_service: CacheService
|
| 79 |
health_service: HealthService
|
| 80 |
export_service: ExportService
|
|
|
|
|
|
|
| 81 |
|
| 82 |
|
| 83 |
def build_container(settings: Settings | None = None) -> ServiceContainer:
|
|
@@ -201,6 +205,17 @@ def build_container(settings: Settings | None = None) -> ServiceContainer:
|
|
| 201 |
)
|
| 202 |
export_service = ExportService(database=database)
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
return ServiceContainer(
|
| 205 |
settings=settings,
|
| 206 |
registry=registry,
|
|
@@ -232,4 +247,6 @@ def build_container(settings: Settings | None = None) -> ServiceContainer:
|
|
| 232 |
cache_service=cache_service,
|
| 233 |
health_service=health_service,
|
| 234 |
export_service=export_service,
|
|
|
|
|
|
|
| 235 |
)
|
|
|
|
| 28 |
from services.cache_service import CacheService
|
| 29 |
from services.detection_service import DetectionService
|
| 30 |
from services.export_service import ExportService
|
| 31 |
+
from services.face_index_service import FaceIndexService
|
| 32 |
from services.face_intelligence_service import FaceIntelligenceService
|
| 33 |
from services.forensic_metadata_service import ForensicMetadataService
|
| 34 |
from services.health_service import HealthService
|
|
|
|
| 43 |
from storage.artifacts import ArtifactStore
|
| 44 |
from storage.cache import Cache
|
| 45 |
from storage.database import Database
|
| 46 |
+
from storage.face_index import FaceIndex
|
| 47 |
from storage.reference_store import ReferenceStore
|
| 48 |
|
| 49 |
|
|
|
|
| 80 |
cache_service: CacheService
|
| 81 |
health_service: HealthService
|
| 82 |
export_service: ExportService
|
| 83 |
+
face_index: FaceIndex
|
| 84 |
+
face_index_service: FaceIndexService
|
| 85 |
|
| 86 |
|
| 87 |
def build_container(settings: Settings | None = None) -> ServiceContainer:
|
|
|
|
| 205 |
)
|
| 206 |
export_service = ExportService(database=database)
|
| 207 |
|
| 208 |
+
# Reverse face search index (sqlite-vec backed)
|
| 209 |
+
face_index = FaceIndex(path=settings.face_index_path)
|
| 210 |
+
face_index_service = FaceIndexService(
|
| 211 |
+
settings=settings,
|
| 212 |
+
validator=validator,
|
| 213 |
+
preprocessor=preprocessor,
|
| 214 |
+
hasher=hasher,
|
| 215 |
+
feature_extractor=feature_extractor,
|
| 216 |
+
face_index=face_index,
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
return ServiceContainer(
|
| 220 |
settings=settings,
|
| 221 |
registry=registry,
|
|
|
|
| 247 |
cache_service=cache_service,
|
| 248 |
health_service=health_service,
|
| 249 |
export_service=export_service,
|
| 250 |
+
face_index=face_index,
|
| 251 |
+
face_index_service=face_index_service,
|
| 252 |
)
|
|
@@ -49,6 +49,10 @@ def get_face_intelligence_service(request: Request):
|
|
| 49 |
return get_container(request).face_intelligence_service
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
def get_forensic_metadata_service(request: Request):
|
| 53 |
return get_container(request).forensic_metadata_service
|
| 54 |
|
|
|
|
| 49 |
return get_container(request).face_intelligence_service
|
| 50 |
|
| 51 |
|
| 52 |
+
def get_face_index_service(request: Request):
|
| 53 |
+
return get_container(request).face_index_service
|
| 54 |
+
|
| 55 |
+
|
| 56 |
def get_forensic_metadata_service(request: Request):
|
| 57 |
return get_container(request).forensic_metadata_service
|
| 58 |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,21 +1,23 @@
|
|
| 1 |
-
"""Face routes β detect / recognize / intelligence endpoints."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from typing import List, Optional
|
| 6 |
|
| 7 |
-
from fastapi import APIRouter, Depends
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
| 10 |
from api.deps import (
|
| 11 |
get_detection_service,
|
| 12 |
get_recognition_service,
|
| 13 |
get_face_intelligence_service,
|
|
|
|
| 14 |
)
|
| 15 |
from models.jobs import JobKind, JobRequest
|
| 16 |
from services.detection_service import DetectionService
|
| 17 |
from services.recognition_service import RecognitionService
|
| 18 |
from services.face_intelligence_service import FaceIntelligenceService
|
|
|
|
| 19 |
|
| 20 |
router = APIRouter()
|
| 21 |
|
|
@@ -71,3 +73,137 @@ async def list_gallery(svc: RecognitionService = Depends(get_recognition_service
|
|
| 71 |
@router.delete("/gallery/{name}")
|
| 72 |
async def remove_from_gallery(name: str, svc: RecognitionService = Depends(get_recognition_service)):
|
| 73 |
return svc.remove_known_person(name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Face routes β detect / recognize / intelligence / index endpoints."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from typing import List, Optional
|
| 6 |
|
| 7 |
+
from fastapi import APIRouter, Depends, File, Form, UploadFile
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
| 10 |
from api.deps import (
|
| 11 |
get_detection_service,
|
| 12 |
get_recognition_service,
|
| 13 |
get_face_intelligence_service,
|
| 14 |
+
get_face_index_service,
|
| 15 |
)
|
| 16 |
from models.jobs import JobKind, JobRequest
|
| 17 |
from services.detection_service import DetectionService
|
| 18 |
from services.recognition_service import RecognitionService
|
| 19 |
from services.face_intelligence_service import FaceIntelligenceService
|
| 20 |
+
from services.face_index_service import FaceIndexService
|
| 21 |
|
| 22 |
router = APIRouter()
|
| 23 |
|
|
|
|
| 73 |
@router.delete("/gallery/{name}")
|
| 74 |
async def remove_from_gallery(name: str, svc: RecognitionService = Depends(get_recognition_service)):
|
| 75 |
return svc.remove_known_person(name)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# --------------------------------------------------------------------------- #
|
| 79 |
+
# Reverse Face Search (PimEyes-style indexed search)
|
| 80 |
+
# --------------------------------------------------------------------------- #
|
| 81 |
+
|
| 82 |
+
class EnrollURLRequest(BaseModel):
|
| 83 |
+
image_url: str
|
| 84 |
+
name: Optional[str] = None
|
| 85 |
+
source_url: Optional[str] = None
|
| 86 |
+
metadata: Optional[dict] = None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class SearchURLRequest(BaseModel):
|
| 90 |
+
image_url: str
|
| 91 |
+
top_k: Optional[int] = None
|
| 92 |
+
threshold: Optional[float] = None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@router.post("/enroll")
|
| 96 |
+
async def enroll_face(
|
| 97 |
+
image: UploadFile = File(...),
|
| 98 |
+
name: Optional[str] = Form(None),
|
| 99 |
+
source_url: Optional[str] = Form(None),
|
| 100 |
+
metadata: Optional[str] = Form(None),
|
| 101 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 102 |
+
):
|
| 103 |
+
"""
|
| 104 |
+
Enroll a face into the searchable reverse-search index.
|
| 105 |
+
|
| 106 |
+
Upload an image (multipart/form-data) with optional name, source_url,
|
| 107 |
+
and metadata (JSON string). The largest face in the image will be
|
| 108 |
+
detected, embedded (512-d ArcFace), and added to the index.
|
| 109 |
+
"""
|
| 110 |
+
import json
|
| 111 |
+
image_bytes = await image.read()
|
| 112 |
+
md = json.loads(metadata) if metadata else None
|
| 113 |
+
return await svc.enroll(
|
| 114 |
+
image_bytes=image_bytes,
|
| 115 |
+
name=name,
|
| 116 |
+
source_url=source_url,
|
| 117 |
+
metadata=md,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@router.post("/enroll/url")
|
| 122 |
+
async def enroll_face_url(
|
| 123 |
+
req: EnrollURLRequest,
|
| 124 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 125 |
+
):
|
| 126 |
+
"""Enroll a face from a URL (alternative to multipart upload)."""
|
| 127 |
+
return await svc.enroll(
|
| 128 |
+
image_url=req.image_url,
|
| 129 |
+
name=req.name,
|
| 130 |
+
source_url=req.source_url,
|
| 131 |
+
metadata=req.metadata,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@router.post("/search")
|
| 136 |
+
async def search_faces(
|
| 137 |
+
image: UploadFile = File(...),
|
| 138 |
+
top_k: Optional[int] = Form(None),
|
| 139 |
+
threshold: Optional[float] = Form(None),
|
| 140 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 141 |
+
):
|
| 142 |
+
"""
|
| 143 |
+
Search the index for faces matching the uploaded image.
|
| 144 |
+
|
| 145 |
+
Returns up to `top_k` matches with similarity >= `threshold`.
|
| 146 |
+
Each match includes the face_id, name, source_url, metadata,
|
| 147 |
+
and similarity score (0-1, higher is better).
|
| 148 |
+
"""
|
| 149 |
+
image_bytes = await image.read()
|
| 150 |
+
return await svc.search(
|
| 151 |
+
image_bytes=image_bytes,
|
| 152 |
+
top_k=top_k,
|
| 153 |
+
threshold=threshold,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@router.post("/search/url")
|
| 158 |
+
async def search_faces_url(
|
| 159 |
+
req: SearchURLRequest,
|
| 160 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 161 |
+
):
|
| 162 |
+
"""Search by image URL (alternative to multipart upload)."""
|
| 163 |
+
return await svc.search(
|
| 164 |
+
image_url=req.image_url,
|
| 165 |
+
top_k=req.top_k,
|
| 166 |
+
threshold=req.threshold,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
@router.get("/list")
|
| 171 |
+
async def list_enrolled_faces(
|
| 172 |
+
limit: int = 50,
|
| 173 |
+
offset: int = 0,
|
| 174 |
+
name: Optional[str] = None,
|
| 175 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 176 |
+
):
|
| 177 |
+
"""Paginated list of enrolled faces in the index."""
|
| 178 |
+
return {"faces": svc.list(limit=limit, offset=offset, name=name)}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@router.get("/{face_id}")
|
| 182 |
+
async def get_face(
|
| 183 |
+
face_id: str,
|
| 184 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 185 |
+
):
|
| 186 |
+
"""Get details of a specific enrolled face."""
|
| 187 |
+
face = svc.get(face_id)
|
| 188 |
+
if face is None:
|
| 189 |
+
return {"success": False, "error": "Face not found", "face_id": face_id}
|
| 190 |
+
return {"success": True, "face": face}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@router.delete("/{face_id}")
|
| 194 |
+
async def delete_face(
|
| 195 |
+
face_id: str,
|
| 196 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 197 |
+
):
|
| 198 |
+
"""Remove a face from the index."""
|
| 199 |
+
deleted = svc.delete(face_id)
|
| 200 |
+
return {"success": deleted, "face_id": face_id,
|
| 201 |
+
"message": "Deleted" if deleted else "Not found"}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
@router.get("/stats")
|
| 205 |
+
async def face_index_stats(
|
| 206 |
+
svc: FaceIndexService = Depends(get_face_index_service),
|
| 207 |
+
):
|
| 208 |
+
"""Index statistics: total count, recent enrollments, etc."""
|
| 209 |
+
return svc.stats()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -90,6 +90,18 @@ class Settings(BaseSettings):
|
|
| 90 |
# Embeddings (optional β requires onnxruntime)
|
| 91 |
enable_mobilenet_embed: bool = False
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
# ------------------------------------------------------------------ #
|
| 94 |
# Detection tuning
|
| 95 |
# ------------------------------------------------------------------ #
|
|
|
|
| 90 |
# Embeddings (optional β requires onnxruntime)
|
| 91 |
enable_mobilenet_embed: bool = False
|
| 92 |
|
| 93 |
+
# ------------------------------------------------------------------ #
|
| 94 |
+
# Reverse face search (PimEyes-style indexed search)
|
| 95 |
+
# ------------------------------------------------------------------ #
|
| 96 |
+
enable_face_index: bool = True
|
| 97 |
+
face_index_path: str = str(DATA_DIR / "face_index.db")
|
| 98 |
+
face_index_top_k: int = 10
|
| 99 |
+
face_index_threshold: float = 0.5
|
| 100 |
+
face_index_max_faces: int = 100_000
|
| 101 |
+
face_index_crawler_user_agent: str = "FaceIntel/1.0"
|
| 102 |
+
face_index_crawler_seed_wikipedia: bool = False
|
| 103 |
+
face_index_crawler_seed_imdb: bool = False
|
| 104 |
+
|
| 105 |
# ------------------------------------------------------------------ #
|
| 106 |
# Detection tuning
|
| 107 |
# ------------------------------------------------------------------ #
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|