No OTPs. No registers. No buddy punching. Just a selfie.
ZepIris is Zepto's purpose-built face authentication platform — open-sourced for teams running identity verification at operational scale.
It handles the full pipeline: face detection, embedding generation, vector search, and spoof/blur/nsfw flagging. Designed to work on budget smartphones, in low light, under high concurrency.
If you're running attendance or identity workflows at scale and don't want to stitch together multiple vendors, this is it.
Current version: v1.0.0. URL paths use /v1/... for the HTTP API; OpenAPI info.version and the Python package follow semver (pyproject.toml / zepiris.version).
- Overview
- Features
- Architecture
- Quick Start
- API Endpoints
- Database Schema
- Configuration
- Development
- Testing
- Troubleshooting
- Documentation
- License
- Citation
- Acknowledgments
- Support & Community
ZepIris simplifies face recognition and verification workflows by providing:
- Pre-integrated face embeddings using AuraFace-v1 OR InsightFace's buffalo_l model, both are 512-dimensional representations, based on the flag ML_SERVICE_FACE_MODEL set in .env
- 1-to-N face vector search via Milvus for fast COSINE similarity matching
- Automated content safety checks: nsfw detection, anti-spoofing, blur detection — all via a dedicated ML inference service
- Multi-tenant support — per-tenant face enrollment and search with isolated namespaces
- Full CRUD operations — insert, upsert, delete, and get for face records
- Production-ready microservice architecture with independent scaling for ML inference
- S3-compatible image storage via MinIO
- REST API with OpenAPI/Swagger auto-documentation and
requestIdtraceability
- Attendance Tracking — Enroll employee faces, query against live camera feeds
- Onboarding Workflows — Identity verification with liveness detection
- Face-Based Access Control — 1-to-N face matching with content safety validation
- Quality Assurance — Automatic detection of low-quality, spoofed, or unsafe images
| Capability | Description |
|---|---|
| Face Embedding | Extract 512-dimensional Normalized embeddings using AuraFace-v1 OR InsightFace's buffalo_l |
| 1-to-N Face Search | Query vectors against Milvus for fast COSINE similarity matching |
| Full CRUD API | Insert, upsert, delete, and retrieve face records with multi-tenant isolation |
| Content Safety (ML) | NSFW, spoof/deepfake, and blur detection via dedicated ML inference microservice |
| Multi-Tenant | Per-tenant face enrollment and search with isolated namespaces |
| Microservice Architecture | Separate ML inference service (port 8001) scales independently from main API (port 8000) |
| Image Storage | S3-compatible MinIO integration for persistent image archival |
| REST API | FastAPI with OpenAPI/Swagger docs, requestId on every response |
| Docker Ready | Multi-stage Dockerfiles for both services + Docker Compose with all dependencies |
| Configurable Thresholds | Fine-tune quality checks (blur sensitivity, spoof threshold, NSFW confidence) |
ZepIris consists of two independent FastAPI microservices that communicate via HTTP:
┌─────────────────────────────────────────────────┐
│ Client / Application │
└──────────────┬──────────────────────────────────┘
│ (REST API)
┌────────▼──────────────────────────────┐
│ Main API (port 8000) │
│ ├─ POST /v1/faces/search │
│ ├─ POST /v1/faces/insert │
│ ├─ POST /v1/faces/upsert │
│ ├─ DELETE /v1/faces/delete │
│ ├─ GET /v1/faces/get/{face_id} │
│ ├─ GET /healthz │
│ └─ GET /readyz │
└───┬────────────────┬──────────────────┘
│ │
┌──▼──┐ ┌──▼─────┐
│MinIO│ │ Milvus │
│ S3 │ │ Vector │
│Store│ │ Store │
└─────┘ └────────┘
│
┌────────────────────▼───────────────────┐
│ ML Inference (port 8001) │
│ ├─ POST /v1/embed (Face Embedding) │
│ ├─ POST /v1/NSFW (NSFW Detection) │
│ ├─ POST /v1/spoof (Spoof Detection) │
│ ├─ POST /v1/blur (Blur Detection) │
│ └─ POST /v1/assess (Combined IQA) │
└─────────────────────────────────────────┘
Responsibilities:
- Handle user-facing CRUD and search endpoints under
/v1/faces/ - Manage image uploads and storage via MinIO
- Coordinate with ML inference service for IQA and embedding extraction
- Index face embeddings in Milvus with multi-tenant support
- Return structured responses with
requestIdfor traceability
Dependencies:
- FastAPI, Uvicorn, Pydantic
- Milvus vector database (with Etcd for metadata)
- MinIO (S3-compatible object storage)
- HTTPx (for ML service communication)
Responsibilities:
- Run independent, parallelizable ML workloads
- Maintain 4 PyTorch models in memory:
- Face Embedding — AuraFace-v1 OR InsightFace's buffalo_l (640×640 input → 512-d output) based on the flag ML_SERVICE_FACE_MODEL set in .env
- NSFW Detection — MobileNetV2 (2-class classifier)
- Spoof Detection — MobileNetV3-Large (liveness detection)
- Blur Detection — ResNet18 (image quality assessment)
- Expose HTTP endpoints for individual or combined inference
- Combined IQA runs all 3 quality checks in parallel via
ThreadPoolExecutor
Benefits:
- Scale independently — run on GPU hardware if needed
- Reuse models across requests — no repeated loading
- Parallel execution — run all 3 quality checks simultaneously
- Python 3.10–3.14 (tested on 3.10–3.14)
- Poetry 2.x for dependency management (install here)
- Docker & Docker Compose (v1.29+; recommended for all-in-one setup)
- 4GB+ RAM for Milvus, 10GB+ free disk space
Check your versions:
python3 --version
poetry --versionFor a fully containerized local setup:
# Clone and navigate
git clone <repository-url>
cd zepiris
# Start all services (Milvus, MinIO, Etcd, API, ML inference)
docker-compose up -d
# Verify health
curl http://localhost:8000/healthz
# Open API documentation
# Visit: http://localhost:8000/docs
# Stop services
docker-compose downThe Compose file sets name: zepiris, so images are tagged zepiris-api and zepiris-ml-inference regardless of clone directory name.
This starts:
- Main API (port 8000)
- ML inference (port 8001)
- Milvus (port 19530)
- MinIO (host port 9002 → container 9000, console on 9001)
- Etcd (metadata store for Milvus)
Once running, visit these URLs in your browser:
- Main API: http://localhost:8000/docs (Swagger UI)
- ML Inference: http://localhost:8001/docs (Swagger UI)
curl http://localhost:8000/healthz
# {"status": "ok"}
curl http://localhost:8000/readyz
# {"status": "ok"}Register a new face with an ID and tenant:
curl -X POST http://localhost:8000/v1/faces/insert \
-F "id=employee_001" \
-F "tenant=acme_corp" \
-F "[email protected]"Response:
{
"requestId": "a1b2c3d4-e5f6-...",
"imageQualityAssessment": {
"passed": true,
"nsfw": {"is_safe": true, "probability": 0.02},
"spoof": {"is_spoof": false, "probability": 0.05},
"blur": {"is_sharp": true, "probability": 0.10}
},
"userOperationResult": {
"operation": "INSERT",
"status": "success"
}
}Parameters:
id(required) — unique face identifiertenant(required) — tenant namespace for isolationfile(required) — JPEG/PNG image file (max 5 MB)- Returns
409 Conflictif a face with the sameidalready exists - Returns
422 Unprocessable Entityif IQA fails or no face detected
Upload an image and find matching faces in the database:
curl -X POST "http://localhost:8000/v1/faces/search?top_k=5" \
-F "id=query_001" \
-F "tenant=acme_corp" \
-F "file=@query_face.jpg"Response:
{
"requestId": "d4e5f6a7-b8c9-...",
"imageQualityAssessment": {
"passed": true,
"nsfw": {"is_safe": true, "probability": 0.01},
"spoof": {"is_spoof": false, "probability": 0.03},
"blur": {"is_sharp": true, "probability": 0.08}
},
"searchResult": {
"matches": [
{"id": "employee_001", "score": 0.92}
]
}
}Parameters:
id(required, form) — identifier for this querytenant(required, form) — tenant namespacefile(required, form) — JPEG/PNG image filetop_k(optional, query, default: 5) — number of matches to returnthreshold(optional, query) — minimum similarity score; defaults toZEPIRIS_MILVUS_SEARCH_THRESHOLD- Returns
200 OKwith emptymatchesarray if IQA fails or no face detected
Insert or update a face record:
curl -X POST http://localhost:8000/v1/faces/upsert \
-F "id=employee_001" \
-F "tenant=acme_corp" \
-F "file=@updated_face.jpg"Response: Same structure as Insert, with "operation": "UPSERT".
Remove a face record by ID:
curl -X DELETE "http://localhost:8000/v1/faces/delete?id=employee_001"Response:
{
"requestId": "f6a7b8c9-d0e1-...",
"userOperationResult": {
"operation": "DELETE",
"status": "success"
}
}Retrieve a face record by ID:
curl http://localhost:8000/v1/faces/get/employee_001Response:
{
"face_id": "employee_001",
"tenant": "acme_corp",
"object_key": "faces/employee_001"
}All POST endpoints accept JSON bodies with base64-encoded images.
{
"image_b64": "<base64-encoded image bytes>"
}Example: Encode an image to base64:
base64 -i face.jpg | pbcopy # macOS
cat face.jpg | base64 # Linuxcurl http://localhost:8001/healthz
# {"status": "ok"}Check if image contains NSFW content:
curl -X POST http://localhost:8001/v1/nsfw \
-H "Content-Type: application/json" \
-d '{"image_b64": "..."}'Response:
{
"is_safe": true,
"probability": 0.02
}Check if face is real or spoofed/deepfake:
curl -X POST http://localhost:8001/v1/spoof \
-H "Content-Type: application/json" \
-d '{"image_b64": "..."}'Response:
{
"is_spoof": false,
"probability": 0.05
}Check if face image is sharp enough:
curl -X POST http://localhost:8001/v1/blur \
-H "Content-Type: application/json" \
-d '{"image_b64": "..."}'Response:
{
"is_sharp": true,
"probability": 0.10
}Generate a 512-dimensional face embedding:
curl -X POST http://localhost:8001/v1/embed \
-H "Content-Type: application/json" \
-d '{"image_b64": "..."}'Response:
{
"face_detected": true,
"embedding": [0.123, -0.456, 0.789, "..."],
"embedding_dim": 512
}Run all 3 quality checks in parallel:
curl -X POST http://localhost:8001/v1/assess \
-H "Content-Type: application/json" \
-d '{"image_b64": "..."}'Response:
{
"passed": true,
"nsfw": {"is_safe": true, "probability": 0.02},
"spoof": {"is_spoof": false, "probability": 0.05},
"blur": {"is_sharp": true, "probability": 0.10}
}passed is true when: nsfw.is_safe AND (NOT spoof.is_spoof) AND blur.is_sharp.
Collection: zepiris_faces (Milvus)
| Field | Type | Purpose |
|---|---|---|
face_id |
VARCHAR(128) | Primary key |
tenant |
VARCHAR(256) | Multi-tenancy support |
object_key |
VARCHAR(512) | MinIO image path |
embedding |
FLOAT_VECTOR(512) | Face embedding vector |
Index: FLAT with COSINE similarity metric.
Copy .env.example to .env and customize. All settings use environment variable prefixes.
| Variable | Default | Description |
|---|---|---|
ZEPIRIS_API_TITLE |
ZepIris |
API title (shown in docs) |
ZEPIRIS_API_VERSION |
1.0.0 |
API version (OpenAPI info.version) |
ZEPIRIS_API_HOST |
0.0.0.0 |
Bind host |
ZEPIRIS_API_PORT |
8000 |
Bind port |
ZEPIRIS_MINIO_ENDPOINT |
localhost:9002 |
MinIO S3 host:port |
ZEPIRIS_MINIO_ACCESS_KEY |
minioadmin |
MinIO access key |
ZEPIRIS_MINIO_SECRET_KEY |
minioadmin |
MinIO secret key |
ZEPIRIS_MINIO_BUCKET |
zepiris |
S3 bucket name |
ZEPIRIS_MINIO_SECURE |
false |
Use TLS for MinIO |
ZEPIRIS_MILVUS_HOST |
localhost |
Milvus vector database host |
ZEPIRIS_MILVUS_PORT |
19530 |
Milvus port |
ZEPIRIS_MILVUS_COLLECTION |
zepiris_faces |
Milvus collection name |
ZEPIRIS_MILVUS_EMBEDDING_DIM |
512 |
Face embedding dimension |
ZEPIRIS_MILVUS_SEARCH_THRESHOLD |
0.5 |
Default COSINE similarity threshold for search |
ZEPIRIS_ML_INFERENCE_SERVICE_URL |
(required) | URL of the ML inference service (e.g. http://localhost:8001) |
Note:
ZEPIRIS_ML_INFERENCE_SERVICE_URLis required. The main API will not start without it. Set it tohttp://ml-inference:8001in Docker Compose orhttp://localhost:8001when running locally.
| Variable | Default | Description |
|---|---|---|
ML_SERVICE_HOST |
0.0.0.0 |
Bind host |
ML_SERVICE_PORT |
8001 |
Bind port |
ML_SERVICE_ML_DEVICE |
cpu |
Inference device: cpu, cuda:0, mps |
ML_SERVICE_NSFW_LOCAL_MODEL_PATH |
/app/models/nsfw_model.pth |
NSFW model file |
ML_SERVICE_NSFW_HF_REPO_ID |
`` | HuggingFace repo for NSFW model (optional) |
ML_SERVICE_NSFW_THRESHOLD |
0.5 |
NSFW classification threshold (0–1) |
ML_SERVICE_SPOOF_LOCAL_MODEL_PATH |
/app/models/spoof_model.pth |
Spoof model file |
ML_SERVICE_SPOOF_HF_REPO_ID |
`` | HuggingFace repo for spoof model (optional) |
ML_SERVICE_SPOOF_THRESHOLD |
0.5 |
Spoof classification threshold (0–1) |
ML_SERVICE_BLUR_LOCAL_MODEL_PATH |
/app/models/blur_model.pth |
Blur model file |
ML_SERVICE_BLUR_HF_REPO_ID |
`` | HuggingFace repo for blur model (optional) |
ML_SERVICE_BLUR_THRESHOLD |
0.5 |
Blur classification threshold (0–1) |
ML_SERVICE_FACE_EMBEDDING_DIM |
512 |
Face embedding dimension |
ML_SERVICE_FACE_DETECTION_WIDTH |
640 |
Face detection input width |
ML_SERVICE_FACE_DETECTION_HEIGHT |
640 |
Face detection input height |
ML_SERVICE_FACE_AREA_THRESHOLD |
0.01 |
Minimum face area (fraction of image) |
To enable GPU inference, set:
export ML_SERVICE_ML_DEVICE=cuda:0
poetry run zepiris-ml-inference-apiOr in .env:
ML_SERVICE_ML_DEVICE=cuda:0Ensure PyTorch CUDA version matches your GPU driver.
zepiris/
├── pyproject.toml # Project metadata, dependencies
├── poetry.lock # Locked dependency versions
├── poetry.toml # Poetry config (in-project .venv)
├── .env.example # Environment variable template
├── Dockerfile # Main service container
├── ml_inference.Dockerfile # ML service container
├── docker-compose.yml # All services (MinIO, Etcd, Milvus, ML, API)
│
├── zepiris/
│ ├── main.py # Main FastAPI app factory + lifespan
│ ├── config.py # Pydantic settings (ZEPIRIS_* prefix)
│ ├── deps.py # FastAPI dependency injection
│ ├── exceptions.py # Domain exceptions (DuplicateFaceIdError, etc.)
│ ├── exception_handlers.py # Error response formatting
│ │
│ ├── api/routes/
│ │ ├── __init__.py # build_api_router()
│ │ ├── face.py # /v1/faces/ search, insert, upsert, delete, get
│ │ └── health.py # GET /healthz, /readyz
│ │
│ ├── ml_inference/
│ │ ├── app.py # ML service FastAPI app + MLServiceSettings
│ │ ├── routes.py # ML endpoints (/v1/nsfw, /spoof, /blur, /embed, /assess)
│ │ ├── deps.py # ML service dependency injection (503 if model missing)
│ │ ├── base.py # Base ModelService + ModelServiceConfig
│ │ ├── face_embedding.py # FaceEmbeddingService (AuraFace OR Buffalo_l)
│ │ ├── nsfw_detection.py # NSFWDetectionService (MobileNetV2)
│ │ ├── spoof_detection.py # SpoofDetectionService (MobileNetV3)
│ │ ├── blur_detection.py # BlurDetectionService (ResNet18)
│ │ ├── image_quality_assessment.py # Combined IQA (ThreadPoolExecutor)
│ │ └── models/ # PyTorch model definitions
│ │
│ ├── services/
│ │ ├── embedding.py # FaceEmbeddingProvider (ABC) + MLInferenceEmbeddingService
│ │ ├── iqa.py # MLInferenceIQAService → HTTP /v1/assess
│ │ ├── milvus_store.py # MilvusFaceStore (vector CRUD + search)
│ │ ├── minio_storage.py # MinioStorageService (S3 storage)
│ │ └── ml_client.py # MLInferenceClient (httpx, sync)
│ │
│ └── schemas/
│ ├── face.py # SearchResponse, UpsertResponse, DeleteResponse
│ └── ml_inference.py # FaceEmbeddingResult, IQA result schemas
│
└── models/ # Pre-trained model weights
├── nsfw_model.pth
├── spoof_model.pth
└── blur_model.pth
# Activate virtual environment
source .venv/bin/activate
# or use Poetry shell
poetry shellFor local development with hot-reloading:
# Terminal 1 — ML service with reload
uvicorn zepiris.ml_inference.app:app --reload --host 0.0.0.0 --port 8001
# Terminal 2 — Main API with reload
ZEPIRIS_ML_INFERENCE_SERVICE_URL=http://localhost:8001 \
uvicorn zepiris.main:app --reload --host 0.0.0.0 --port 8000# Add a runtime dependency
poetry add httpx-oauth
# Add a development-only tool
poetry add --group dev black ruff pytest
# After changing dependencies, commit both files
git add pyproject.toml poetry.lock
git commit -m "chore: add new dependencies"ZepIris includes test scripts for both unit and integration testing.
Tests all ML endpoints with real model inference:
python scripts/test_ml_service.py \
--test-image path/to/image.jpg \
--nsfw-model ./models/nsfw_model.pth \
--spoof-model ./models/spoof_model.pth \
--blur-model ./models/blur_model.pthOr test against a running service:
python scripts/test_ml_service.py \
--test-image path/to/image.jpg \
--no-server --port 8001Load and test models in-process without HTTP:
python scripts/test_models.py \
--test-image path/to/image.jpg \
--nsfw-model ./models/nsfw_model.pth \
--spoof-model ./models/spoof_model.pth \
--blur-model ./models/blur_model.pthUse curl, httpie, or Postman to test endpoints. Interactive Swagger docs available at /docs on both services.
Problem: poetry install fails with "No file/folder found"
Solution: This is expected on first run. Poetry will resolve dependencies. Try again.
Problem: Wrong Python version
Solution: Point Poetry to the correct interpreter:
poetry env use /path/to/python3.10
poetry installProblem: Main API can't connect to Milvus or MinIO
Solution: Verify external services are running:
# Check all services
docker compose ps
# Or check individually
curl http://localhost:9002/minio/health/live # MinIO (host port 9002)
curl http://localhost:8001/healthz # ML inferenceProblem: Main API fails to start with "ML_INFERENCE_SERVICE_URL required"
Solution: Set the required environment variable:
export ZEPIRIS_ML_INFERENCE_SERVICE_URL=http://localhost:8001Problem: ML service doesn't load models on startup
Solution: Check logs for model loading errors:
docker compose logs ml-inference # if using Docker Compose
# or
poetry run zepiris-ml-inference-api # if running locallyEnsure model files exist at configured paths. Default: /app/models/{nsfw,spoof,blur}_model.pth. If a model fails to load, that endpoint returns 503 Service Unavailable.
Problem: ML service ignores ML_SERVICE_ML_DEVICE=cuda
Solution:
- Verify PyTorch CUDA version matches your GPU driver:
python -c "import torch; print(torch.cuda.is_available())"- If unavailable, reinstall PyTorch with correct CUDA version:
poetry remove torch torchvision
poetry add torch torchvision --platform linux --python "^3.10"Problem: Requests hang or timeout to ML service
Solution:
- Ensure ML inference service is running and healthy (
GET /healthz) - Check if models are still loading (can take 30-60s on first startup)
- Check network connectivity between services
- Monitor resource usage (disk space for model downloads, RAM for inference)
- LOCAL_SETUP_AND_TEST.md — Step-by-step local testing guide (20-30 min)
- docs/API_REFERENCE.md — Complete API endpoint reference
- docs/CONFIGURATION.md — Environment variables & tuning
- CONTRIBUTING.md — Code guidelines & contribution workflow
- .env.example — Complete environment variable template
- AuraFace Documentation — Face embedding & detection model
- InsightFace Documentation — Wrapper over the face embedding & detection model
- Milvus Vector Database — Vector storage & search
- MinIO S3 SDK — Object storage
- FastAPI Best Practices — Web framework
This project is licensed under the MIT License — see LICENSE file for details.
The project is released under the LICENSE with contribution expectations described in CONTRIBUTING.md and CODE_OF_CONDUCT.md.
If you use ZepIris in research or production, please cite:
@software{zepiris2026,
title={ZepIris: Scalable Face Authentication},
author={Zepto Data Science Team},
year={2026},
url={https://github.com/zepto-labs/zepiris}
}ZepIris stands on the shoulders of excellent open-source projects:
- AuraFace & InsightFace — State-of-the-art face embedding models
- Milvus — High-performance vector database
- FastAPI — Modern async Python web framework
- PyTorch — Deep learning framework
- MinIO — S3-compatible object storage
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: [email protected]
Last Updated: April 2026 Status: v1.0.0