alchemyface 0.1.0__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.
Files changed (32) hide show
  1. alchemyface-0.1.0/CHANGELOG.md +41 -0
  2. alchemyface-0.1.0/LICENSE +21 -0
  3. alchemyface-0.1.0/MANIFEST.in +16 -0
  4. alchemyface-0.1.0/PKG-INFO +192 -0
  5. alchemyface-0.1.0/README.md +160 -0
  6. alchemyface-0.1.0/pyproject.toml +95 -0
  7. alchemyface-0.1.0/requirements-dev.txt +10 -0
  8. alchemyface-0.1.0/requirements.txt +6 -0
  9. alchemyface-0.1.0/setup.cfg +4 -0
  10. alchemyface-0.1.0/src/alchemyface/__init__.py +38 -0
  11. alchemyface-0.1.0/src/alchemyface/capture.py +68 -0
  12. alchemyface-0.1.0/src/alchemyface/cli.py +140 -0
  13. alchemyface-0.1.0/src/alchemyface/detection/__init__.py +6 -0
  14. alchemyface-0.1.0/src/alchemyface/detection/base.py +18 -0
  15. alchemyface-0.1.0/src/alchemyface/detection/yunet.py +82 -0
  16. alchemyface-0.1.0/src/alchemyface/embedding/__init__.py +6 -0
  17. alchemyface-0.1.0/src/alchemyface/embedding/base.py +26 -0
  18. alchemyface-0.1.0/src/alchemyface/embedding/sface.py +48 -0
  19. alchemyface-0.1.0/src/alchemyface/errors.py +21 -0
  20. alchemyface-0.1.0/src/alchemyface/models.py +155 -0
  21. alchemyface-0.1.0/src/alchemyface/pipeline.py +98 -0
  22. alchemyface-0.1.0/src/alchemyface/py.typed +0 -0
  23. alchemyface-0.1.0/src/alchemyface/store/__init__.py +6 -0
  24. alchemyface-0.1.0/src/alchemyface/store/base.py +37 -0
  25. alchemyface-0.1.0/src/alchemyface/store/memory.py +129 -0
  26. alchemyface-0.1.0/src/alchemyface/types.py +59 -0
  27. alchemyface-0.1.0/src/alchemyface.egg-info/PKG-INFO +192 -0
  28. alchemyface-0.1.0/src/alchemyface.egg-info/SOURCES.txt +30 -0
  29. alchemyface-0.1.0/src/alchemyface.egg-info/dependency_links.txt +1 -0
  30. alchemyface-0.1.0/src/alchemyface.egg-info/entry_points.txt +2 -0
  31. alchemyface-0.1.0/src/alchemyface.egg-info/requires.txt +9 -0
  32. alchemyface-0.1.0/src/alchemyface.egg-info/top_level.txt +1 -0
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project follows [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ _Nothing yet._
11
+
12
+ ## [0.1.0] — 2026-08-28
13
+
14
+ First release. Extracted from an earlier prototype into a typed, installable library.
15
+
16
+ ### Added
17
+
18
+ - `Recognizer` facade sequencing three protocols — `Detector`, `Embedder` and
19
+ `FaceStore` — so a different gallery or embedding model is a drop-in.
20
+ - `YuNetDetector` for detection and `SFaceEmbedder` for 128-d embeddings, via
21
+ OpenCV's DNN runtime. Embeddings are L2-normalised, making cosine similarity
22
+ a dot product.
23
+ - `InMemoryStore`: a numpy gallery with `.npz` save/load. No database required.
24
+ - Runtime model resolution — explicit path, `$ALCHEMYFACE_MODEL_DIR`,
25
+ `~/.cache/alchemyface/models/`, then a SHA256-verified download from the
26
+ OpenCV Zoo. Weights are never vendored, keeping the wheel at ~20 KB.
27
+ - `VideoSource`, a context-managed wrapper over `cv2.VideoCapture`.
28
+ - CLI: `alchemyface download-models | enroll | identify | version`.
29
+ - Inline type information (`py.typed`), checked with mypy under
30
+ `disallow_untyped_defs`.
31
+
32
+ ### Notes
33
+
34
+ - Depends on `opencv-python-headless` rather than `opencv-python`: the package
35
+ makes no GUI calls, and the headless build installs cleanly on CI, in Docker
36
+ and on servers without `libGL`.
37
+ - 88 tests at 91% coverage. The default suite needs no models, camera or
38
+ network; model-backed tests are marked and skip when weights are absent.
39
+
40
+ [Unreleased]: https://github.com/kouya-marino/AlchemyFace/compare/v0.1.0...HEAD
41
+ [0.1.0]: https://github.com/kouya-marino/AlchemyFace/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kouya-marino
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,16 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include requirements.txt
5
+ include requirements-dev.txt
6
+ include src/alchemyface/py.typed
7
+
8
+ # Never ship: personal data, model weights, developer scaffolding.
9
+ prune _local
10
+ prune examples
11
+ prune tests
12
+ prune docs
13
+ prune htmlcov
14
+ global-exclude *.npy *.npz *.onnx *.mp3 *.pt
15
+ global-exclude *.py[cod]
16
+ global-exclude .DS_Store
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: alchemyface
3
+ Version: 0.1.0
4
+ Summary: Face detection and recognition built on YuNet and SFace — a small, typed, dependency-light Python library.
5
+ Author-email: Prashant Rawat <prashantrawatmailbox@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kouya-marino/AlchemyFace
8
+ Project-URL: Repository, https://github.com/kouya-marino/AlchemyFace
9
+ Project-URL: Issues, https://github.com/kouya-marino/AlchemyFace/issues
10
+ Keywords: face-recognition,face-detection,yunet,sface,opencv,embeddings
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: opencv-python-headless<5,>=4.9
24
+ Requires-Dist: numpy<3,>=1.24
25
+ Requires-Dist: typer<1,>=0.12
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.11; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # AlchemyFace
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/alchemyface.svg)](https://pypi.org/project/alchemyface/)
36
+ [![CI](https://github.com/kouya-marino/AlchemyFace/actions/workflows/ci.yml/badge.svg)](https://github.com/kouya-marino/AlchemyFace/actions/workflows/ci.yml)
37
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
38
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
39
+
40
+ Face detection and recognition built on [YuNet](https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet)
41
+ and [SFace](https://github.com/opencv/opencv_zoo/tree/main/models/face_recognition_sface).
42
+ Small, typed, and dependency-light: OpenCV, NumPy, Typer. Nothing else.
43
+
44
+ ## Why
45
+
46
+ Most Python face-recognition libraries pull in dlib, PyTorch or TensorFlow.
47
+ AlchemyFace uses two small ONNX models through OpenCV's own DNN runtime, so a
48
+ working install is a few megabytes of Python and about 37 MB of weights fetched
49
+ once, on first use.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install alchemyface
55
+ ```
56
+
57
+ ## Use
58
+
59
+ ```python
60
+ import cv2
61
+ from alchemyface import Recognizer
62
+
63
+ r = Recognizer() # weights download once, then cached
64
+
65
+ r.enroll("prashant", cv2.imread("me.jpg"))
66
+ r.enroll("alice", cv2.imread("alice.jpg"))
67
+
68
+ for recognition in r.identify(cv2.imread("group.jpg")):
69
+ face, match = recognition.face, recognition.match
70
+ if match:
71
+ print(f"{match.label} at {face.bbox} ({match.score:.2f})")
72
+ else:
73
+ print(f"unknown face at {face.bbox}")
74
+ ```
75
+
76
+ Enrolled faces live in memory. Persist them when you are done:
77
+
78
+ ```python
79
+ r.store.save("gallery.npz")
80
+ r.store.load("gallery.npz")
81
+ ```
82
+
83
+ ### Bring your own components
84
+
85
+ `Recognizer` is a thin facade over three protocols — `Detector`, `Embedder` and
86
+ `FaceStore`. Any object satisfying the protocol can be substituted, which is how
87
+ a pgvector-backed store or a different embedding model will slot in later
88
+ without touching the pipeline.
89
+
90
+ ```python
91
+ from alchemyface import Recognizer
92
+ from alchemyface.detection import YuNetDetector
93
+ from alchemyface.embedding import SFaceEmbedder
94
+ from alchemyface.store import InMemoryStore
95
+
96
+ r = Recognizer(
97
+ detector=YuNetDetector(score_threshold=0.8),
98
+ embedder=SFaceEmbedder(),
99
+ store=InMemoryStore(),
100
+ threshold=0.363,
101
+ )
102
+ ```
103
+
104
+ ### Live video
105
+
106
+ ```python
107
+ from alchemyface import Recognizer
108
+ from alchemyface.capture import VideoSource
109
+
110
+ r = Recognizer()
111
+ r.store.load("gallery.npz")
112
+
113
+ with VideoSource(0, width=1280, height=720) as camera:
114
+ for frame in camera.frames():
115
+ for recognition in r.identify(frame):
116
+ match = recognition.match
117
+ print(match.label if match else "unknown", recognition.face.bbox)
118
+ ```
119
+
120
+ ### CLI
121
+
122
+ ```bash
123
+ alchemyface download-models # pre-fetch weights
124
+ alchemyface enroll --name prashant --image me.jpg --gallery g.npz
125
+ alchemyface identify --image group.jpg --gallery g.npz
126
+ ```
127
+
128
+ ## Model weights
129
+
130
+ Weights are resolved in this order, first hit wins:
131
+
132
+ 1. `model_dir=` passed to `Recognizer`
133
+ 2. `$ALCHEMYFACE_MODEL_DIR`
134
+ 3. `~/.cache/alchemyface/models/`
135
+ 4. downloaded from the OpenCV Zoo and SHA256-verified
136
+
137
+ To work fully offline, point at a directory you already have:
138
+
139
+ ```bash
140
+ export ALCHEMYFACE_MODEL_DIR=/path/to/onnx
141
+ ```
142
+
143
+ ## The recognition threshold
144
+
145
+ The default cosine threshold is `0.363`, SFace's published operating point:
146
+ above it, two embeddings are treated as the same person. Raise it for fewer
147
+ false accepts, lower it for fewer false rejects. It is a tunable, not a
148
+ constant — validate it against your own data before relying on it.
149
+
150
+ ## Development
151
+
152
+ Requires [`pyenv`](https://github.com/pyenv/pyenv) with
153
+ [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv).
154
+
155
+ ```bash
156
+ pyenv install 3.10.6 # if not already present
157
+ pyenv virtualenv 3.10.6 alchemyface # .python-version activates it here
158
+ pip install -e ".[dev]"
159
+ ```
160
+
161
+ | Command | Does |
162
+ |---|---|
163
+ | `pytest tests/ -m "not models and not camera"` | the fast suite — no models, camera or network |
164
+ | `pytest tests/ -m "not camera"` | adds the tests that load the real ONNX weights |
165
+ | `ruff check src tests` | lint |
166
+ | `ruff format src tests` | format |
167
+ | `mypy src/alchemyface` | type check |
168
+ | `python -m build` | build the wheel and sdist |
169
+
170
+ Tests that need the real weights are marked `models` and skip unless
171
+ `ALCHEMYFACE_MODEL_DIR` points at a directory containing them:
172
+
173
+ ```bash
174
+ export ALCHEMYFACE_MODEL_DIR="$PWD/_local/onnx"
175
+ ```
176
+
177
+ ## A note on data
178
+
179
+ This repository contains a `_local/` directory that is **git-ignored and must
180
+ stay that way**. It holds face embeddings, name recordings and captured images
181
+ of real, identifiable people, carried over from the internal prototype this
182
+ library grew out of. Under Japan's APPI and GDPR Article 9 those are sensitive
183
+ personal data. They are development fixtures only: they are excluded from the
184
+ wheel, the sdist and version control, and they must never be published.
185
+
186
+ ## Licence
187
+
188
+ MIT — see [LICENSE](LICENSE).
189
+
190
+ The ONNX weights are distributed by the [OpenCV Zoo](https://github.com/opencv/opencv_zoo)
191
+ under their own terms — YuNet under MIT, SFace under Apache-2.0 — and are
192
+ downloaded at runtime rather than redistributed here.
@@ -0,0 +1,160 @@
1
+ # AlchemyFace
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/alchemyface.svg)](https://pypi.org/project/alchemyface/)
4
+ [![CI](https://github.com/kouya-marino/AlchemyFace/actions/workflows/ci.yml/badge.svg)](https://github.com/kouya-marino/AlchemyFace/actions/workflows/ci.yml)
5
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
7
+
8
+ Face detection and recognition built on [YuNet](https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet)
9
+ and [SFace](https://github.com/opencv/opencv_zoo/tree/main/models/face_recognition_sface).
10
+ Small, typed, and dependency-light: OpenCV, NumPy, Typer. Nothing else.
11
+
12
+ ## Why
13
+
14
+ Most Python face-recognition libraries pull in dlib, PyTorch or TensorFlow.
15
+ AlchemyFace uses two small ONNX models through OpenCV's own DNN runtime, so a
16
+ working install is a few megabytes of Python and about 37 MB of weights fetched
17
+ once, on first use.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install alchemyface
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```python
28
+ import cv2
29
+ from alchemyface import Recognizer
30
+
31
+ r = Recognizer() # weights download once, then cached
32
+
33
+ r.enroll("prashant", cv2.imread("me.jpg"))
34
+ r.enroll("alice", cv2.imread("alice.jpg"))
35
+
36
+ for recognition in r.identify(cv2.imread("group.jpg")):
37
+ face, match = recognition.face, recognition.match
38
+ if match:
39
+ print(f"{match.label} at {face.bbox} ({match.score:.2f})")
40
+ else:
41
+ print(f"unknown face at {face.bbox}")
42
+ ```
43
+
44
+ Enrolled faces live in memory. Persist them when you are done:
45
+
46
+ ```python
47
+ r.store.save("gallery.npz")
48
+ r.store.load("gallery.npz")
49
+ ```
50
+
51
+ ### Bring your own components
52
+
53
+ `Recognizer` is a thin facade over three protocols — `Detector`, `Embedder` and
54
+ `FaceStore`. Any object satisfying the protocol can be substituted, which is how
55
+ a pgvector-backed store or a different embedding model will slot in later
56
+ without touching the pipeline.
57
+
58
+ ```python
59
+ from alchemyface import Recognizer
60
+ from alchemyface.detection import YuNetDetector
61
+ from alchemyface.embedding import SFaceEmbedder
62
+ from alchemyface.store import InMemoryStore
63
+
64
+ r = Recognizer(
65
+ detector=YuNetDetector(score_threshold=0.8),
66
+ embedder=SFaceEmbedder(),
67
+ store=InMemoryStore(),
68
+ threshold=0.363,
69
+ )
70
+ ```
71
+
72
+ ### Live video
73
+
74
+ ```python
75
+ from alchemyface import Recognizer
76
+ from alchemyface.capture import VideoSource
77
+
78
+ r = Recognizer()
79
+ r.store.load("gallery.npz")
80
+
81
+ with VideoSource(0, width=1280, height=720) as camera:
82
+ for frame in camera.frames():
83
+ for recognition in r.identify(frame):
84
+ match = recognition.match
85
+ print(match.label if match else "unknown", recognition.face.bbox)
86
+ ```
87
+
88
+ ### CLI
89
+
90
+ ```bash
91
+ alchemyface download-models # pre-fetch weights
92
+ alchemyface enroll --name prashant --image me.jpg --gallery g.npz
93
+ alchemyface identify --image group.jpg --gallery g.npz
94
+ ```
95
+
96
+ ## Model weights
97
+
98
+ Weights are resolved in this order, first hit wins:
99
+
100
+ 1. `model_dir=` passed to `Recognizer`
101
+ 2. `$ALCHEMYFACE_MODEL_DIR`
102
+ 3. `~/.cache/alchemyface/models/`
103
+ 4. downloaded from the OpenCV Zoo and SHA256-verified
104
+
105
+ To work fully offline, point at a directory you already have:
106
+
107
+ ```bash
108
+ export ALCHEMYFACE_MODEL_DIR=/path/to/onnx
109
+ ```
110
+
111
+ ## The recognition threshold
112
+
113
+ The default cosine threshold is `0.363`, SFace's published operating point:
114
+ above it, two embeddings are treated as the same person. Raise it for fewer
115
+ false accepts, lower it for fewer false rejects. It is a tunable, not a
116
+ constant — validate it against your own data before relying on it.
117
+
118
+ ## Development
119
+
120
+ Requires [`pyenv`](https://github.com/pyenv/pyenv) with
121
+ [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv).
122
+
123
+ ```bash
124
+ pyenv install 3.10.6 # if not already present
125
+ pyenv virtualenv 3.10.6 alchemyface # .python-version activates it here
126
+ pip install -e ".[dev]"
127
+ ```
128
+
129
+ | Command | Does |
130
+ |---|---|
131
+ | `pytest tests/ -m "not models and not camera"` | the fast suite — no models, camera or network |
132
+ | `pytest tests/ -m "not camera"` | adds the tests that load the real ONNX weights |
133
+ | `ruff check src tests` | lint |
134
+ | `ruff format src tests` | format |
135
+ | `mypy src/alchemyface` | type check |
136
+ | `python -m build` | build the wheel and sdist |
137
+
138
+ Tests that need the real weights are marked `models` and skip unless
139
+ `ALCHEMYFACE_MODEL_DIR` points at a directory containing them:
140
+
141
+ ```bash
142
+ export ALCHEMYFACE_MODEL_DIR="$PWD/_local/onnx"
143
+ ```
144
+
145
+ ## A note on data
146
+
147
+ This repository contains a `_local/` directory that is **git-ignored and must
148
+ stay that way**. It holds face embeddings, name recordings and captured images
149
+ of real, identifiable people, carried over from the internal prototype this
150
+ library grew out of. Under Japan's APPI and GDPR Article 9 those are sensitive
151
+ personal data. They are development fixtures only: they are excluded from the
152
+ wheel, the sdist and version control, and they must never be published.
153
+
154
+ ## Licence
155
+
156
+ MIT — see [LICENSE](LICENSE).
157
+
158
+ The ONNX weights are distributed by the [OpenCV Zoo](https://github.com/opencv/opencv_zoo)
159
+ under their own terms — YuNet under MIT, SFace under Apache-2.0 — and are
160
+ downloaded at runtime rather than redistributed here.
@@ -0,0 +1,95 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "alchemyface"
7
+ version = "0.1.0"
8
+ description = "Face detection and recognition built on YuNet and SFace — a small, typed, dependency-light Python library."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Prashant Rawat", email = "prashantrawatmailbox@gmail.com"},
14
+ ]
15
+ keywords = ["face-recognition", "face-detection", "yunet", "sface", "opencv", "embeddings"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Scientific/Engineering :: Image Recognition",
25
+ "Typing :: Typed",
26
+ ]
27
+ # Deliberately minimal. Model weights are fetched at runtime rather than
28
+ # vendored, and everything the original prototype needed for its own deployment
29
+ # (SQLAlchemy, psycopg2, pgvector, pandas, scikit-learn, ultralytics, pygame)
30
+ # belongs to the application, not to this library.
31
+ #
32
+ # headless, not the GUI build: the package makes no imshow/waitKey calls, and
33
+ # plain opencv-python needs libGL, which breaks CI, Docker and headless servers.
34
+ dependencies = [
35
+ "opencv-python-headless>=4.9,<5",
36
+ "numpy>=1.24,<3",
37
+ "typer>=0.12,<1",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest>=8.0",
43
+ "pytest-cov>=5.0",
44
+ "ruff>=0.4.0",
45
+ "mypy>=1.11",
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/kouya-marino/AlchemyFace"
50
+ Repository = "https://github.com/kouya-marino/AlchemyFace"
51
+ Issues = "https://github.com/kouya-marino/AlchemyFace/issues"
52
+
53
+ [project.scripts]
54
+ alchemyface = "alchemyface.cli:app"
55
+
56
+ [tool.setuptools.packages.find]
57
+ where = ["src"]
58
+ include = ["alchemyface*"]
59
+
60
+ [tool.setuptools.package-data]
61
+ alchemyface = ["py.typed"]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ addopts = "-v --tb=short"
66
+ markers = [
67
+ "models: requires the real ONNX weights on disk (skipped when absent)",
68
+ "camera: requires an attached camera (skipped in CI)",
69
+ ]
70
+
71
+ [tool.coverage.run]
72
+ source = ["src/alchemyface"]
73
+ branch = true
74
+
75
+ [tool.ruff]
76
+ target-version = "py310"
77
+ line-length = 120
78
+
79
+ [tool.ruff.lint]
80
+ select = ["E", "F", "W", "I"]
81
+
82
+ [tool.mypy]
83
+ python_version = "3.10"
84
+ packages = ["alchemyface"]
85
+ mypy_path = "src"
86
+ warn_unused_ignores = true
87
+ warn_redundant_casts = true
88
+ disallow_untyped_defs = true
89
+ no_implicit_optional = true
90
+
91
+ # cv2 ships partial stubs. Use the `FaceDetectorYN.create` / `FaceRecognizerSF.create`
92
+ # class-method form, which they declare; the module-level `*_create` aliases are not.
93
+ [[tool.mypy.overrides]]
94
+ module = ["cv2.*"]
95
+ ignore_missing_imports = true
@@ -0,0 +1,10 @@
1
+ # Development-only dependencies. Install with:
2
+ # pip install -r requirements-dev.txt
3
+ # Production deps stay in requirements.txt.
4
+ #
5
+ # Kept in step with pyproject.toml's [project.optional-dependencies] dev extra —
6
+ # CI gates on ruff and mypy, so both belong here.
7
+ pytest>=8.0
8
+ pytest-cov>=5.0
9
+ ruff>=0.4.0
10
+ mypy>=1.11
@@ -0,0 +1,6 @@
1
+ # Runtime dependencies, kept in step with pyproject.toml's [project] dependencies.
2
+ # Installing the package itself (`pip install -e .`) is the usual route; this
3
+ # file exists for environments that pin from a requirements file.
4
+ opencv-python-headless>=4.9,<5
5
+ numpy>=1.24,<3
6
+ typer>=0.12,<1
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """AlchemyFace — face detection and recognition on YuNet and SFace.
2
+
3
+ The public surface is deliberately small::
4
+
5
+ from alchemyface import Recognizer
6
+
7
+ r = Recognizer()
8
+ r.enroll("prashant", image)
9
+ r.identify(frame)
10
+
11
+ Everything else is a seam. ``Detector``, ``Embedder`` and ``FaceStore`` are
12
+ protocols, so any conforming object can be substituted without touching the
13
+ pipeline.
14
+ """
15
+
16
+ from alchemyface.errors import (
17
+ AlchemyFaceError,
18
+ ModelDownloadError,
19
+ ModelNotFoundError,
20
+ NoFaceDetectedError,
21
+ )
22
+ from alchemyface.pipeline import DEFAULT_THRESHOLD, Recognizer
23
+ from alchemyface.types import Face, Match, Recognition
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ __all__ = [
28
+ "AlchemyFaceError",
29
+ "DEFAULT_THRESHOLD",
30
+ "Face",
31
+ "Match",
32
+ "ModelDownloadError",
33
+ "ModelNotFoundError",
34
+ "NoFaceDetectedError",
35
+ "Recognition",
36
+ "Recognizer",
37
+ "__version__",
38
+ ]
@@ -0,0 +1,68 @@
1
+ """Reading frames from a camera or a video file.
2
+
3
+ A thin wrapper over ``cv2.VideoCapture`` that fails loudly when the device
4
+ will not open, releases itself on the way out of a ``with`` block, and offers
5
+ an iterator so callers do not have to write the read-check-read loop by hand.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from types import TracebackType
11
+ from typing import Iterator
12
+
13
+ import cv2
14
+ import numpy as np
15
+ from numpy.typing import NDArray
16
+
17
+
18
+ class VideoSource:
19
+ """A camera index or a path to a video file, as a context manager."""
20
+
21
+ def __init__(
22
+ self,
23
+ source: int | str = 0,
24
+ *,
25
+ width: int | None = None,
26
+ height: int | None = None,
27
+ ) -> None:
28
+ self._capture = cv2.VideoCapture(source)
29
+ if not self._capture.isOpened():
30
+ self._capture.release()
31
+ raise RuntimeError(f"could not open video source {source!r}")
32
+ if width is not None:
33
+ self._capture.set(cv2.CAP_PROP_FRAME_WIDTH, float(width))
34
+ if height is not None:
35
+ self._capture.set(cv2.CAP_PROP_FRAME_HEIGHT, float(height))
36
+ self._released = False
37
+
38
+ def read(self) -> NDArray[np.uint8] | None:
39
+ """The next frame, or ``None`` once the stream is exhausted."""
40
+ ok, frame = self._capture.read()
41
+ if not ok:
42
+ return None
43
+ return np.asarray(frame, dtype=np.uint8)
44
+
45
+ def frames(self) -> Iterator[NDArray[np.uint8]]:
46
+ """Yield frames until the stream ends."""
47
+ while True:
48
+ frame = self.read()
49
+ if frame is None:
50
+ return
51
+ yield frame
52
+
53
+ def release(self) -> None:
54
+ """Release the device. Safe to call more than once."""
55
+ if not self._released:
56
+ self._capture.release()
57
+ self._released = True
58
+
59
+ def __enter__(self) -> "VideoSource":
60
+ return self
61
+
62
+ def __exit__(
63
+ self,
64
+ exc_type: type[BaseException] | None,
65
+ exc: BaseException | None,
66
+ traceback: TracebackType | None,
67
+ ) -> None:
68
+ self.release()