facekey 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- facekey-0.0.1/.gitattributes +4 -0
- facekey-0.0.1/.gitignore +30 -0
- facekey-0.0.1/LICENSE +36 -0
- facekey-0.0.1/PKG-INFO +174 -0
- facekey-0.0.1/README.md +136 -0
- facekey-0.0.1/examples/desktop_unlock.py +29 -0
- facekey-0.0.1/examples/web_fastapi.py +67 -0
- facekey-0.0.1/pyproject.toml +73 -0
- facekey-0.0.1/src/facekey/__init__.py +71 -0
- facekey-0.0.1/src/facekey/__main__.py +4 -0
- facekey-0.0.1/src/facekey/_image.py +68 -0
- facekey-0.0.1/src/facekey/camera.py +117 -0
- facekey-0.0.1/src/facekey/cli.py +111 -0
- facekey-0.0.1/src/facekey/core.py +198 -0
- facekey-0.0.1/src/facekey/embedders/__init__.py +54 -0
- facekey-0.0.1/src/facekey/embedders/insightface.py +103 -0
- facekey-0.0.1/src/facekey/enrollment.py +135 -0
- facekey-0.0.1/src/facekey/errors.py +47 -0
- facekey-0.0.1/src/facekey/liveness.py +114 -0
- facekey-0.0.1/src/facekey/matching.py +66 -0
- facekey-0.0.1/src/facekey/models.py +84 -0
- facekey-0.0.1/src/facekey/pose.py +78 -0
- facekey-0.0.1/src/facekey/stores/__init__.py +64 -0
- facekey-0.0.1/src/facekey/stores/file.py +105 -0
- facekey-0.0.1/src/facekey/stores/sqlite.py +61 -0
- facekey-0.0.1/tests/__init__.py +0 -0
- facekey-0.0.1/tests/conftest.py +68 -0
- facekey-0.0.1/tests/test_core.py +110 -0
- facekey-0.0.1/tests/test_enrollment.py +101 -0
- facekey-0.0.1/tests/test_matching.py +74 -0
- facekey-0.0.1/tests/test_pose.py +46 -0
- facekey-0.0.1/tests/test_stores.py +74 -0
facekey-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.installed.cfg
|
|
9
|
+
|
|
10
|
+
# Virtual envs
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
|
|
15
|
+
# Tooling
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# Editors / OS
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# facekey local data (enrolled templates, downloaded model cache)
|
|
29
|
+
.facekey/
|
|
30
|
+
*.facekey.json
|
facekey-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adm145
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
NOTE ON BUNDLED MODEL WEIGHTS
|
|
26
|
+
|
|
27
|
+
This MIT license covers the facekey source code only. facekey downloads and
|
|
28
|
+
uses third-party model weights at runtime, which carry their own licenses:
|
|
29
|
+
|
|
30
|
+
* InsightFace "buffalo_l" / "buffalo_s" recognition and detection models are
|
|
31
|
+
released by their authors for NON-COMMERCIAL RESEARCH USE ONLY. Commercial
|
|
32
|
+
use requires a separate license from InsightFace
|
|
33
|
+
(recognition-oss-pack@insightface.ai). See EMBEDDER LICENSING in README.md.
|
|
34
|
+
|
|
35
|
+
You are responsible for complying with the license of whichever model backend
|
|
36
|
+
you choose to run.
|
facekey-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: facekey
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Face-ID-style unlock for your app: enroll a face from several angles, then authenticate against a live camera frame.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Adm145/facekey
|
|
6
|
+
Project-URL: Issues, https://github.com/Adm145/facekey/issues
|
|
7
|
+
Author: Adm145
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: authentication,biometrics,face id,face recognition,face unlock,insightface,liveness
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: insightface>=0.7.3
|
|
23
|
+
Requires-Dist: numpy>=1.21
|
|
24
|
+
Requires-Dist: onnxruntime>=1.15
|
|
25
|
+
Requires-Dist: opencv-python-headless>=4.7
|
|
26
|
+
Requires-Dist: platformdirs>=3.0
|
|
27
|
+
Provides-Extra: camera
|
|
28
|
+
Requires-Dist: opencv-python>=4.7; extra == 'camera'
|
|
29
|
+
Provides-Extra: crypto
|
|
30
|
+
Requires-Dist: cryptography>=41; extra == 'crypto'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
35
|
+
Provides-Extra: gpu
|
|
36
|
+
Requires-Dist: onnxruntime-gpu>=1.15; extra == 'gpu'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# facekey
|
|
40
|
+
|
|
41
|
+
> ⚠️ **Alpha — the API will change.** Pin an exact version if you depend on it.
|
|
42
|
+
|
|
43
|
+
Face-ID-style unlock for your Python app. Enroll a face from a few angles, then
|
|
44
|
+
on each unlock grab a frame from the camera, compare it to the enrolled
|
|
45
|
+
template, and grant access on a match.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from facekey import FaceLock
|
|
49
|
+
|
|
50
|
+
lock = FaceLock() # single enrolled face, stored under a per-user data dir
|
|
51
|
+
|
|
52
|
+
# one-time enrollment (needs a camera; pip install "facekey[camera]")
|
|
53
|
+
lock.run_enrollment(camera=0, on_prompt=print)
|
|
54
|
+
|
|
55
|
+
# later, guard something
|
|
56
|
+
if lock.unlock(camera=0, timeout=5):
|
|
57
|
+
reveal_secret()
|
|
58
|
+
else:
|
|
59
|
+
fall_back_to_password()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
No camera in your process? Feed frames yourself — that's the real API; the
|
|
63
|
+
camera helper is a thin convenience on top:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import numpy as np
|
|
67
|
+
from facekey import FaceLock
|
|
68
|
+
|
|
69
|
+
lock = FaceLock()
|
|
70
|
+
|
|
71
|
+
session = lock.enroll()
|
|
72
|
+
while session.needs: # ["frontal", "left", "right", "up", "down"]
|
|
73
|
+
frame = my_capture() # H x W x 3 BGR ndarray, or JPEG/PNG bytes
|
|
74
|
+
result = session.capture(frame)
|
|
75
|
+
print(result.reason, "still need:", session.needs)
|
|
76
|
+
session.commit()
|
|
77
|
+
|
|
78
|
+
auth = lock.authenticate(my_capture())
|
|
79
|
+
print(auth.granted, auth.score, auth.liveness)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## This is not a security boundary
|
|
83
|
+
|
|
84
|
+
facekey uses an ordinary RGB camera. There is no infrared or depth sensing like
|
|
85
|
+
a phone's Face ID. Passive liveness catches a casual printed photo or a phone
|
|
86
|
+
screen held up to the lens, but it is beatable by replay video, masks, and
|
|
87
|
+
deepfakes.
|
|
88
|
+
|
|
89
|
+
Treat it as a **convenience factor**, not a lock:
|
|
90
|
+
|
|
91
|
+
- Never make it the *only* thing standing between someone and sensitive data.
|
|
92
|
+
- Always ship a PIN / password fallback.
|
|
93
|
+
- Store only embeddings, never the enrollment images (facekey does this).
|
|
94
|
+
- Let people delete their template (`lock.remove()`).
|
|
95
|
+
- Get explicit consent before enrolling anyone. Face templates are biometric
|
|
96
|
+
data and are regulated (EU GDPR Art. 9, Illinois BIPA, others). Consent,
|
|
97
|
+
retention limits, and deletion are your responsibility.
|
|
98
|
+
|
|
99
|
+
## Install
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install facekey # core: frame-in, decision-out
|
|
103
|
+
pip install "facekey[camera]" # + OpenCV highgui for the camera helper / preview
|
|
104
|
+
pip install "facekey[gpu]" # + onnxruntime-gpu
|
|
105
|
+
pip install "facekey[crypto]" # + encryption at rest for stored templates
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
First use downloads the InsightFace model pack (~300 MB) into a local cache.
|
|
109
|
+
Do it ahead of time with:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
facekey download-models
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`insightface` may build from source on some platforms; you need a C/C++
|
|
116
|
+
toolchain if no wheel is available for your Python/OS.
|
|
117
|
+
|
|
118
|
+
## EMBEDDER LICENSING — read before shipping commercially
|
|
119
|
+
|
|
120
|
+
The default recognition backend is InsightFace **`buffalo_l`**. Its *code* is
|
|
121
|
+
MIT, but its *pretrained weights* are released **for non-commercial research use
|
|
122
|
+
only** (the training datasets forbid commercial use). Commercial use needs a
|
|
123
|
+
license from InsightFace: `recognition-oss-pack@insightface.ai`.
|
|
124
|
+
|
|
125
|
+
facekey emits a `FaceKeyLicenseWarning` the first time the default backend
|
|
126
|
+
loads. To use facekey in a commercial product without that license, supply a
|
|
127
|
+
differently-licensed embedder:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from facekey import FaceLock
|
|
131
|
+
from facekey.embedders import Embedder # implement this protocol over your own model
|
|
132
|
+
|
|
133
|
+
lock = FaceLock(embedder=MyPermissiveArcFace())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Suppress the warning (once you understand the terms) with
|
|
137
|
+
`FACEKEY_SUPPRESS_LICENSE_WARNING=1` in the environment.
|
|
138
|
+
|
|
139
|
+
## API sketch
|
|
140
|
+
|
|
141
|
+
| Call | Purpose |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `FaceLock(store=None, *, threshold=0.45, liveness=True, model="buffalo_l", max_enrollees=1, gpu=False)` | Construct a lock. `store`: `None` → per-user data dir, a path, or a `Store` instance. |
|
|
144
|
+
| `lock.enroll(label="default") -> EnrollmentSession` | Start guided multi-angle enrollment. |
|
|
145
|
+
| `session.capture(frame) -> CaptureResult` / `session.needs` / `session.commit()` | Feed frames until every pose bucket is covered, then persist. |
|
|
146
|
+
| `lock.authenticate(frame, enrollee=None) -> AuthResult` | 1:1 verify. `enrollee` optional while only one face is enrolled. |
|
|
147
|
+
| `lock.identify(frame) -> AuthResult` | 1:N — best match across all enrolled faces. |
|
|
148
|
+
| `lock.unlock(camera=0, timeout=5) -> bool` | Camera helper: grab frames until match or timeout. |
|
|
149
|
+
| `lock.run_enrollment(camera=0, on_prompt=print)` | Camera helper: drives the "turn your head" flow. |
|
|
150
|
+
| `lock.enrolled()` / `lock.is_enrolled()` / `lock.remove(label="default")` | Manage templates. |
|
|
151
|
+
|
|
152
|
+
`max_enrollees` is `1` today. The store, matching, and `identify()` are already
|
|
153
|
+
multi-face internally — raise the cap (up to a handful) when you want a shared
|
|
154
|
+
device.
|
|
155
|
+
|
|
156
|
+
## Tuning the threshold
|
|
157
|
+
|
|
158
|
+
`threshold` is cosine similarity on L2-normalized embeddings; higher is
|
|
159
|
+
stricter. `0.45` is a deliberately conservative default for an auth use case.
|
|
160
|
+
Lower it if legitimate users are rejected too often (higher false-accept rate),
|
|
161
|
+
raise it to make impostor matches harder (higher false-reject rate). Measure on
|
|
162
|
+
your own camera and lighting before trusting a number.
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
|
|
166
|
+
Alpha. `matching`, `stores`, `enrollment`, `models`, `errors` are implemented;
|
|
167
|
+
`embedders.insightface`, `liveness`, and `camera` are minimal and expected to
|
|
168
|
+
change. Passive liveness currently ships a lightweight texture heuristic
|
|
169
|
+
(`HeuristicLiveness`); a MiniFASNet backend is the planned upgrade.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT for the source (see `LICENSE`). Model weights carry their own terms — see
|
|
174
|
+
EMBEDDER LICENSING above.
|
facekey-0.0.1/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# facekey
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Alpha — the API will change.** Pin an exact version if you depend on it.
|
|
4
|
+
|
|
5
|
+
Face-ID-style unlock for your Python app. Enroll a face from a few angles, then
|
|
6
|
+
on each unlock grab a frame from the camera, compare it to the enrolled
|
|
7
|
+
template, and grant access on a match.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from facekey import FaceLock
|
|
11
|
+
|
|
12
|
+
lock = FaceLock() # single enrolled face, stored under a per-user data dir
|
|
13
|
+
|
|
14
|
+
# one-time enrollment (needs a camera; pip install "facekey[camera]")
|
|
15
|
+
lock.run_enrollment(camera=0, on_prompt=print)
|
|
16
|
+
|
|
17
|
+
# later, guard something
|
|
18
|
+
if lock.unlock(camera=0, timeout=5):
|
|
19
|
+
reveal_secret()
|
|
20
|
+
else:
|
|
21
|
+
fall_back_to_password()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No camera in your process? Feed frames yourself — that's the real API; the
|
|
25
|
+
camera helper is a thin convenience on top:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import numpy as np
|
|
29
|
+
from facekey import FaceLock
|
|
30
|
+
|
|
31
|
+
lock = FaceLock()
|
|
32
|
+
|
|
33
|
+
session = lock.enroll()
|
|
34
|
+
while session.needs: # ["frontal", "left", "right", "up", "down"]
|
|
35
|
+
frame = my_capture() # H x W x 3 BGR ndarray, or JPEG/PNG bytes
|
|
36
|
+
result = session.capture(frame)
|
|
37
|
+
print(result.reason, "still need:", session.needs)
|
|
38
|
+
session.commit()
|
|
39
|
+
|
|
40
|
+
auth = lock.authenticate(my_capture())
|
|
41
|
+
print(auth.granted, auth.score, auth.liveness)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## This is not a security boundary
|
|
45
|
+
|
|
46
|
+
facekey uses an ordinary RGB camera. There is no infrared or depth sensing like
|
|
47
|
+
a phone's Face ID. Passive liveness catches a casual printed photo or a phone
|
|
48
|
+
screen held up to the lens, but it is beatable by replay video, masks, and
|
|
49
|
+
deepfakes.
|
|
50
|
+
|
|
51
|
+
Treat it as a **convenience factor**, not a lock:
|
|
52
|
+
|
|
53
|
+
- Never make it the *only* thing standing between someone and sensitive data.
|
|
54
|
+
- Always ship a PIN / password fallback.
|
|
55
|
+
- Store only embeddings, never the enrollment images (facekey does this).
|
|
56
|
+
- Let people delete their template (`lock.remove()`).
|
|
57
|
+
- Get explicit consent before enrolling anyone. Face templates are biometric
|
|
58
|
+
data and are regulated (EU GDPR Art. 9, Illinois BIPA, others). Consent,
|
|
59
|
+
retention limits, and deletion are your responsibility.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install facekey # core: frame-in, decision-out
|
|
65
|
+
pip install "facekey[camera]" # + OpenCV highgui for the camera helper / preview
|
|
66
|
+
pip install "facekey[gpu]" # + onnxruntime-gpu
|
|
67
|
+
pip install "facekey[crypto]" # + encryption at rest for stored templates
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
First use downloads the InsightFace model pack (~300 MB) into a local cache.
|
|
71
|
+
Do it ahead of time with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
facekey download-models
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`insightface` may build from source on some platforms; you need a C/C++
|
|
78
|
+
toolchain if no wheel is available for your Python/OS.
|
|
79
|
+
|
|
80
|
+
## EMBEDDER LICENSING — read before shipping commercially
|
|
81
|
+
|
|
82
|
+
The default recognition backend is InsightFace **`buffalo_l`**. Its *code* is
|
|
83
|
+
MIT, but its *pretrained weights* are released **for non-commercial research use
|
|
84
|
+
only** (the training datasets forbid commercial use). Commercial use needs a
|
|
85
|
+
license from InsightFace: `recognition-oss-pack@insightface.ai`.
|
|
86
|
+
|
|
87
|
+
facekey emits a `FaceKeyLicenseWarning` the first time the default backend
|
|
88
|
+
loads. To use facekey in a commercial product without that license, supply a
|
|
89
|
+
differently-licensed embedder:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from facekey import FaceLock
|
|
93
|
+
from facekey.embedders import Embedder # implement this protocol over your own model
|
|
94
|
+
|
|
95
|
+
lock = FaceLock(embedder=MyPermissiveArcFace())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Suppress the warning (once you understand the terms) with
|
|
99
|
+
`FACEKEY_SUPPRESS_LICENSE_WARNING=1` in the environment.
|
|
100
|
+
|
|
101
|
+
## API sketch
|
|
102
|
+
|
|
103
|
+
| Call | Purpose |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `FaceLock(store=None, *, threshold=0.45, liveness=True, model="buffalo_l", max_enrollees=1, gpu=False)` | Construct a lock. `store`: `None` → per-user data dir, a path, or a `Store` instance. |
|
|
106
|
+
| `lock.enroll(label="default") -> EnrollmentSession` | Start guided multi-angle enrollment. |
|
|
107
|
+
| `session.capture(frame) -> CaptureResult` / `session.needs` / `session.commit()` | Feed frames until every pose bucket is covered, then persist. |
|
|
108
|
+
| `lock.authenticate(frame, enrollee=None) -> AuthResult` | 1:1 verify. `enrollee` optional while only one face is enrolled. |
|
|
109
|
+
| `lock.identify(frame) -> AuthResult` | 1:N — best match across all enrolled faces. |
|
|
110
|
+
| `lock.unlock(camera=0, timeout=5) -> bool` | Camera helper: grab frames until match or timeout. |
|
|
111
|
+
| `lock.run_enrollment(camera=0, on_prompt=print)` | Camera helper: drives the "turn your head" flow. |
|
|
112
|
+
| `lock.enrolled()` / `lock.is_enrolled()` / `lock.remove(label="default")` | Manage templates. |
|
|
113
|
+
|
|
114
|
+
`max_enrollees` is `1` today. The store, matching, and `identify()` are already
|
|
115
|
+
multi-face internally — raise the cap (up to a handful) when you want a shared
|
|
116
|
+
device.
|
|
117
|
+
|
|
118
|
+
## Tuning the threshold
|
|
119
|
+
|
|
120
|
+
`threshold` is cosine similarity on L2-normalized embeddings; higher is
|
|
121
|
+
stricter. `0.45` is a deliberately conservative default for an auth use case.
|
|
122
|
+
Lower it if legitimate users are rejected too often (higher false-accept rate),
|
|
123
|
+
raise it to make impostor matches harder (higher false-reject rate). Measure on
|
|
124
|
+
your own camera and lighting before trusting a number.
|
|
125
|
+
|
|
126
|
+
## Status
|
|
127
|
+
|
|
128
|
+
Alpha. `matching`, `stores`, `enrollment`, `models`, `errors` are implemented;
|
|
129
|
+
`embedders.insightface`, `liveness`, and `camera` are minimal and expected to
|
|
130
|
+
change. Passive liveness currently ships a lightweight texture heuristic
|
|
131
|
+
(`HeuristicLiveness`); a MiniFASNet backend is the planned upgrade.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT for the source (see `LICENSE`). Model weights carry their own terms — see
|
|
136
|
+
EMBEDDER LICENSING above.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Enroll once, then gate a secret behind a face unlock.
|
|
2
|
+
|
|
3
|
+
pip install "facekey[camera]"
|
|
4
|
+
python examples/desktop_unlock.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from facekey import FaceLock
|
|
10
|
+
|
|
11
|
+
SECRET = "the launch codes are 0000"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
lock = FaceLock() # stores the template under your per-user data dir
|
|
16
|
+
|
|
17
|
+
if not lock.is_enrolled():
|
|
18
|
+
print("No face on file - starting enrollment.")
|
|
19
|
+
lock.run_enrollment(camera=0, on_prompt=print)
|
|
20
|
+
|
|
21
|
+
print("\nLook at the camera to unlock...")
|
|
22
|
+
if lock.unlock(camera=0, timeout=6):
|
|
23
|
+
print("Unlocked:", SECRET)
|
|
24
|
+
else:
|
|
25
|
+
print("Denied. Fall back to a password here.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Face unlock over HTTP: the browser captures frames, this server matches them.
|
|
2
|
+
|
|
3
|
+
pip install facekey fastapi "uvicorn[standard]" python-multipart
|
|
4
|
+
uvicorn examples.web_fastapi:app --reload
|
|
5
|
+
|
|
6
|
+
POST a still image (multipart field ``frame``) to:
|
|
7
|
+
/enroll/capture until the response shows no remaining ``needs``
|
|
8
|
+
/enroll/commit to persist
|
|
9
|
+
/authenticate to check a face
|
|
10
|
+
|
|
11
|
+
This keeps the frame-based API server-side; no camera on the host is involved.
|
|
12
|
+
Enrollment state is held in memory here for brevity - use a real session store
|
|
13
|
+
in production.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, File, UploadFile
|
|
19
|
+
|
|
20
|
+
from facekey import FaceLock
|
|
21
|
+
from facekey.errors import FaceKeyError
|
|
22
|
+
|
|
23
|
+
app = FastAPI(title="facekey web example")
|
|
24
|
+
lock = FaceLock()
|
|
25
|
+
_session = {"obj": None}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.post("/enroll/capture")
|
|
29
|
+
async def enroll_capture(frame: UploadFile = File(...)):
|
|
30
|
+
if _session["obj"] is None:
|
|
31
|
+
_session["obj"] = lock.enroll()
|
|
32
|
+
result = _session["obj"].capture(await frame.read())
|
|
33
|
+
return {
|
|
34
|
+
"accepted": result.accepted,
|
|
35
|
+
"reason": result.reason,
|
|
36
|
+
"bucket": result.bucket.value if result.bucket else None,
|
|
37
|
+
"needs": result.needs,
|
|
38
|
+
"progress": result.progress,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.post("/enroll/commit")
|
|
43
|
+
async def enroll_commit():
|
|
44
|
+
session = _session["obj"]
|
|
45
|
+
if session is None:
|
|
46
|
+
return {"error": "no enrollment in progress"}
|
|
47
|
+
try:
|
|
48
|
+
template = session.commit()
|
|
49
|
+
except FaceKeyError as exc:
|
|
50
|
+
return {"error": str(exc)}
|
|
51
|
+
lock.store.put(template)
|
|
52
|
+
_session["obj"] = None
|
|
53
|
+
return {"enrolled": template.label, "poses": sorted(template.embeddings)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.post("/authenticate")
|
|
57
|
+
async def authenticate(frame: UploadFile = File(...)):
|
|
58
|
+
try:
|
|
59
|
+
result = lock.authenticate(await frame.read())
|
|
60
|
+
except FaceKeyError as exc:
|
|
61
|
+
return {"granted": False, "reason": str(exc)}
|
|
62
|
+
return {
|
|
63
|
+
"granted": result.granted,
|
|
64
|
+
"score": result.score,
|
|
65
|
+
"liveness": result.liveness,
|
|
66
|
+
"reason": result.reason,
|
|
67
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "facekey"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Face-ID-style unlock for your app: enroll a face from several angles, then authenticate against a live camera frame."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Adm145" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"face recognition",
|
|
15
|
+
"face unlock",
|
|
16
|
+
"face id",
|
|
17
|
+
"authentication",
|
|
18
|
+
"biometrics",
|
|
19
|
+
"liveness",
|
|
20
|
+
"insightface",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
32
|
+
"Topic :: Security",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"numpy>=1.21",
|
|
36
|
+
"opencv-python-headless>=4.7",
|
|
37
|
+
"insightface>=0.7.3",
|
|
38
|
+
"onnxruntime>=1.15",
|
|
39
|
+
"platformdirs>=3.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
# Full OpenCV build: adds the highgui preview window used by the camera helper.
|
|
44
|
+
# The base install already has cv2.VideoCapture via opencv-python-headless.
|
|
45
|
+
camera = ["opencv-python>=4.7"]
|
|
46
|
+
gpu = ["onnxruntime-gpu>=1.15"]
|
|
47
|
+
crypto = ["cryptography>=41"]
|
|
48
|
+
dev = ["pytest>=7", "ruff>=0.4", "build>=1.0"]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
facekey = "facekey.cli:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://github.com/Adm145/facekey"
|
|
55
|
+
Issues = "https://github.com/Adm145/facekey/issues"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/facekey"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py39"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
pythonpath = ["src"]
|
|
70
|
+
filterwarnings = [
|
|
71
|
+
"ignore::facekey.errors.FaceKeyLicenseWarning",
|
|
72
|
+
"ignore::facekey.errors.WeakLivenessWarning",
|
|
73
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""facekey - Face-ID-style unlock for your app.
|
|
2
|
+
|
|
3
|
+
See the README for the security caveats and model licensing. Quick start::
|
|
4
|
+
|
|
5
|
+
from facekey import FaceLock
|
|
6
|
+
|
|
7
|
+
lock = FaceLock()
|
|
8
|
+
lock.run_enrollment(camera=0, on_prompt=print)
|
|
9
|
+
if lock.unlock(camera=0, timeout=5):
|
|
10
|
+
...
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .core import FaceLock, __version__
|
|
16
|
+
from .embedders import Embedder, get_embedder, register
|
|
17
|
+
from .enrollment import EnrollmentSession
|
|
18
|
+
from .errors import (
|
|
19
|
+
EnrollmentIncomplete,
|
|
20
|
+
FaceKeyError,
|
|
21
|
+
FaceKeyLicenseWarning,
|
|
22
|
+
LivenessCheckFailed,
|
|
23
|
+
LowQualityFrame,
|
|
24
|
+
ModelDownloadError,
|
|
25
|
+
MultipleFacesDetected,
|
|
26
|
+
NoFaceDetected,
|
|
27
|
+
NotEnrolled,
|
|
28
|
+
StoreFull,
|
|
29
|
+
WeakLivenessWarning,
|
|
30
|
+
)
|
|
31
|
+
from .liveness import HeuristicLiveness, LivenessChecker, NullLiveness
|
|
32
|
+
from .matching import DEFAULT_THRESHOLD, cosine_similarity
|
|
33
|
+
from .models import AuthResult, CaptureResult, FaceObservation, FaceTemplate, PoseBucket
|
|
34
|
+
from .stores import FileStore, SQLiteStore, Store
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"__version__",
|
|
38
|
+
"FaceLock",
|
|
39
|
+
"EnrollmentSession",
|
|
40
|
+
"AuthResult",
|
|
41
|
+
"CaptureResult",
|
|
42
|
+
"FaceObservation",
|
|
43
|
+
"FaceTemplate",
|
|
44
|
+
"PoseBucket",
|
|
45
|
+
"DEFAULT_THRESHOLD",
|
|
46
|
+
"cosine_similarity",
|
|
47
|
+
# embedders
|
|
48
|
+
"Embedder",
|
|
49
|
+
"get_embedder",
|
|
50
|
+
"register",
|
|
51
|
+
# liveness
|
|
52
|
+
"LivenessChecker",
|
|
53
|
+
"HeuristicLiveness",
|
|
54
|
+
"NullLiveness",
|
|
55
|
+
# stores
|
|
56
|
+
"Store",
|
|
57
|
+
"FileStore",
|
|
58
|
+
"SQLiteStore",
|
|
59
|
+
# errors / warnings
|
|
60
|
+
"FaceKeyError",
|
|
61
|
+
"NoFaceDetected",
|
|
62
|
+
"MultipleFacesDetected",
|
|
63
|
+
"LowQualityFrame",
|
|
64
|
+
"LivenessCheckFailed",
|
|
65
|
+
"NotEnrolled",
|
|
66
|
+
"EnrollmentIncomplete",
|
|
67
|
+
"StoreFull",
|
|
68
|
+
"ModelDownloadError",
|
|
69
|
+
"FaceKeyLicenseWarning",
|
|
70
|
+
"WeakLivenessWarning",
|
|
71
|
+
]
|