Spaces:
Sleeping
Sleeping
Sync from GitHub via hub-sync
Browse files- .gitattributes +1 -0
- Dockerfile +3 -2
- README.md +71 -0
- images/Densities.png +0 -0
- images/ROC.png +0 -0
- images/[Originals]/ROC.png +0 -0
- images/[Originals]/confusion_matrix.png +0 -0
- images/confusion_matrix.png +0 -0
- images/faceverification_demo.gif +3 -0
- pyproject.toml +7 -3
- requirements.txt +2 -26
- src/faceverification/config.py +1 -1
- uv.lock +14 -6
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
images/faceverification_demo.gif filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
CHANGED
|
@@ -8,14 +8,15 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
| 8 |
|
| 9 |
WORKDIR /app
|
| 10 |
|
| 11 |
-
RUN apt-get update && apt-get
|
|
|
|
| 12 |
libgl1 \
|
| 13 |
libglib2.0-0 \
|
| 14 |
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
|
| 16 |
COPY requirements.txt .
|
| 17 |
RUN pip install --no-cache-dir --upgrade pip setuptools "wheel>=0.46.2"
|
| 18 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
|
| 20 |
COPY pyproject.toml README.md ./
|
| 21 |
COPY src ./src
|
|
|
|
| 8 |
|
| 9 |
WORKDIR /app
|
| 10 |
|
| 11 |
+
RUN apt-get update && apt-get upgrade -y \
|
| 12 |
+
&& apt-get install -y --no-install-recommends \
|
| 13 |
libgl1 \
|
| 14 |
libglib2.0-0 \
|
| 15 |
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
|
| 17 |
COPY requirements.txt .
|
| 18 |
RUN pip install --no-cache-dir --upgrade pip setuptools "wheel>=0.46.2"
|
| 19 |
+
RUN pip install --no-cache-dir --extra-index-url https://download.pytorch.org/whl/cpu -r requirements.txt
|
| 20 |
|
| 21 |
COPY pyproject.toml README.md ./
|
| 22 |
COPY src ./src
|
README.md
CHANGED
|
@@ -23,6 +23,18 @@ Try it on Hugging Face Spaces: https://huggingface.co/spaces/leandrodevai/faceve
|
|
| 23 |
|
| 24 |
The app lets users add known people to a local embeddings database and verify whether a new face image matches one of the stored identities.
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
## Features
|
| 27 |
|
| 28 |
- Face detection and preprocessing from uploaded images
|
|
@@ -32,6 +44,65 @@ The app lets users add known people to a local embeddings database and verify wh
|
|
| 32 |
- FastAPI interface for containerized API deployments
|
| 33 |
- Docker-based Hugging Face Space deployment
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
## Tech Stack
|
| 36 |
|
| 37 |
- Python
|
|
|
|
| 23 |
|
| 24 |
The app lets users add known people to a local embeddings database and verify whether a new face image matches one of the stored identities.
|
| 25 |
|
| 26 |
+

|
| 27 |
+
|
| 28 |
+
This demo was built to show that FaceNet, despite being an older architecture,
|
| 29 |
+
is still a strong and practical baseline for face embedding workflows. Its
|
| 30 |
+
moderate runtime footprint also makes it an interesting candidate for constrained
|
| 31 |
+
deployments, including edge-style scenarios when hardware, latency, and accuracy
|
| 32 |
+
requirements are compatible.
|
| 33 |
+
|
| 34 |
+
The project is also intended as a reusable AI engineering template: the same
|
| 35 |
+
structure can be adapted to build CI/CD pipelines for other models, or extended
|
| 36 |
+
from image uploads into a video pipeline for near real-time face recognition.
|
| 37 |
+
|
| 38 |
## Features
|
| 39 |
|
| 40 |
- Face detection and preprocessing from uploaded images
|
|
|
|
| 44 |
- FastAPI interface for containerized API deployments
|
| 45 |
- Docker-based Hugging Face Space deployment
|
| 46 |
|
| 47 |
+
## Architecture
|
| 48 |
+
|
| 49 |
+
```mermaid
|
| 50 |
+
flowchart LR
|
| 51 |
+
A[Upload] --> B[MTCNN]
|
| 52 |
+
B --> C[Crop]
|
| 53 |
+
C --> D[FaceNet]
|
| 54 |
+
D --> E[Normalize]
|
| 55 |
+
E --> F[ChromaDB]
|
| 56 |
+
F --> G[Threshold]
|
| 57 |
+
G --> H[Match]
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Images are first passed through MTCNN for face detection and cropping. FaceNet
|
| 61 |
+
then produces an embedding, which is L2-normalized before being stored or
|
| 62 |
+
queried in ChromaDB. Verification compares the nearest stored embedding against
|
| 63 |
+
the calibrated distance threshold.
|
| 64 |
+
|
| 65 |
+
## Model Evaluation
|
| 66 |
+
|
| 67 |
+
The verification threshold was calibrated with a small study on the
|
| 68 |
+
`bitmind/lfw` dataset, a Hugging Face version of Labeled Faces in the Wild
|
| 69 |
+
(LFW). The study builds same-person and different-person image pairs, splits
|
| 70 |
+
them into calibration and held-out evaluation sets, and evaluates L2 distance
|
| 71 |
+
between L2-normalized FaceNet embeddings.
|
| 72 |
+
|
| 73 |
+
For this demo, the selected threshold is:
|
| 74 |
+
|
| 75 |
+
```text
|
| 76 |
+
same person if L2 distance <= 1.0764
|
| 77 |
+
different person if L2 distance > 1.0764
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
This value was chosen on the calibration split because it maximized balanced
|
| 81 |
+
accuracy while staying below the distance region where different-person pairs
|
| 82 |
+
start to dominate. The held-out evaluation split contains 720 same-person and
|
| 83 |
+
720 different-person pairs.
|
| 84 |
+
|
| 85 |
+

|
| 86 |
+
|
| 87 |
+
Held-out evaluation results:
|
| 88 |
+
|
| 89 |
+
| Metric | Value |
|
| 90 |
+
| --- | ---: |
|
| 91 |
+
| AUC-ROC | 0.9838 |
|
| 92 |
+
| Accuracy | 0.9653 |
|
| 93 |
+
| FAR | 0.0097 |
|
| 94 |
+
| FRR | 0.0597 |
|
| 95 |
+
| TPR | 0.9403 |
|
| 96 |
+
| Calibrated threshold | 1.0764 |
|
| 97 |
+
| Calibration EER threshold | 1.1857 |
|
| 98 |
+
|
| 99 |
+

|
| 100 |
+
|
| 101 |
+

|
| 102 |
+
|
| 103 |
+
These metrics are intended to justify the threshold for this demo dataset, not
|
| 104 |
+
as a production biometric benchmark.
|
| 105 |
+
|
| 106 |
## Tech Stack
|
| 107 |
|
| 108 |
- Python
|
images/Densities.png
ADDED
|
images/ROC.png
ADDED
|
images/[Originals]/ROC.png
ADDED
|
images/[Originals]/confusion_matrix.png
ADDED
|
images/confusion_matrix.png
ADDED
|
images/faceverification_demo.gif
ADDED
|
Git LFS Details
|
pyproject.toml
CHANGED
|
@@ -14,13 +14,10 @@ dependencies = [
|
|
| 14 |
"pydantic>=2.11.10,<=2.12.5",
|
| 15 |
"pydantic-settings>=2.14.1",
|
| 16 |
"chromadb>=1.5.9",
|
| 17 |
-
"pytest>=9.0.3",
|
| 18 |
-
"pytest-cov>=7.1.0",
|
| 19 |
"fastapi>=0.136.1",
|
| 20 |
"pyjwt>=2.10.1",
|
| 21 |
"python-multipart>=0.0.28",
|
| 22 |
"uvicorn[standard]>=0.46.0",
|
| 23 |
-
"ruff>=0.15.13",
|
| 24 |
"torch>=2.12.0",
|
| 25 |
"torchvision>=0.27.0",
|
| 26 |
]
|
|
@@ -60,3 +57,10 @@ src = ["src", "test"]
|
|
| 60 |
|
| 61 |
[tool.ruff.lint]
|
| 62 |
select = ["E", "F", "I", "B", "UP", "SIM"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"pydantic>=2.11.10,<=2.12.5",
|
| 15 |
"pydantic-settings>=2.14.1",
|
| 16 |
"chromadb>=1.5.9",
|
|
|
|
|
|
|
| 17 |
"fastapi>=0.136.1",
|
| 18 |
"pyjwt>=2.10.1",
|
| 19 |
"python-multipart>=0.0.28",
|
| 20 |
"uvicorn[standard]>=0.46.0",
|
|
|
|
| 21 |
"torch>=2.12.0",
|
| 22 |
"torchvision>=0.27.0",
|
| 23 |
]
|
|
|
|
| 57 |
|
| 58 |
[tool.ruff.lint]
|
| 59 |
select = ["E", "F", "I", "B", "UP", "SIM"]
|
| 60 |
+
|
| 61 |
+
[dependency-groups]
|
| 62 |
+
dev = [
|
| 63 |
+
"pytest>=9.0.3",
|
| 64 |
+
"pytest-cov>=7.1.0",
|
| 65 |
+
"ruff>=0.15.13",
|
| 66 |
+
]
|
requirements.txt
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
# This file was autogenerated by uv via the following command:
|
| 2 |
-
# uv --cache-dir .uv-cache export --locked --no-hashes --no-emit-project --format requirements.txt --output-file requirements.txt
|
| 3 |
-
--find-links https://download.pytorch.org/whl/cpu/torch/
|
| 4 |
-
--find-links https://download.pytorch.org/whl/cpu/torchvision/
|
| 5 |
aiohappyeyeballs==2.6.1
|
| 6 |
# via aiohttp
|
| 7 |
aiohttp==3.13.5
|
|
@@ -51,11 +49,8 @@ colorama==0.4.6 ; (os_name != 'nt' and sys_platform == 'win32') or (os_name == '
|
|
| 51 |
# via
|
| 52 |
# build
|
| 53 |
# click
|
| 54 |
-
# pytest
|
| 55 |
# tqdm
|
| 56 |
# uvicorn
|
| 57 |
-
coverage==7.14.0
|
| 58 |
-
# via pytest-cov
|
| 59 |
datasets==4.8.5
|
| 60 |
# via faceverification
|
| 61 |
dill==0.4.1
|
|
@@ -138,8 +133,6 @@ importlib-metadata==8.7.1
|
|
| 138 |
# via opentelemetry-api
|
| 139 |
importlib-resources==7.1.0
|
| 140 |
# via chromadb
|
| 141 |
-
iniconfig==2.3.0
|
| 142 |
-
# via pytest
|
| 143 |
jinja2==3.1.6
|
| 144 |
# via
|
| 145 |
# gradio
|
|
@@ -218,7 +211,6 @@ packaging==26.2
|
|
| 218 |
# gradio-client
|
| 219 |
# huggingface-hub
|
| 220 |
# onnxruntime
|
| 221 |
-
# pytest
|
| 222 |
pandas==3.0.2
|
| 223 |
# via
|
| 224 |
# datasets
|
|
@@ -228,10 +220,6 @@ pillow==12.2.0
|
|
| 228 |
# facenet-pytorch
|
| 229 |
# gradio
|
| 230 |
# torchvision
|
| 231 |
-
pluggy==1.6.0
|
| 232 |
-
# via
|
| 233 |
-
# pytest
|
| 234 |
-
# pytest-cov
|
| 235 |
propcache==0.5.2
|
| 236 |
# via
|
| 237 |
# aiohttp
|
|
@@ -261,21 +249,13 @@ pydantic-settings==2.14.1
|
|
| 261 |
pydub==0.25.1
|
| 262 |
# via gradio
|
| 263 |
pygments==2.20.0
|
| 264 |
-
# via
|
| 265 |
-
# pytest
|
| 266 |
-
# rich
|
| 267 |
pyjwt==2.12.1
|
| 268 |
# via faceverification
|
| 269 |
pypika==0.51.1
|
| 270 |
# via chromadb
|
| 271 |
pyproject-hooks==1.2.0
|
| 272 |
# via build
|
| 273 |
-
pytest==9.0.3
|
| 274 |
-
# via
|
| 275 |
-
# faceverification
|
| 276 |
-
# pytest-cov
|
| 277 |
-
pytest-cov==7.1.0
|
| 278 |
-
# via faceverification
|
| 279 |
python-dateutil==2.9.0.post0
|
| 280 |
# via
|
| 281 |
# kubernetes
|
|
@@ -319,8 +299,6 @@ rpds-py==0.30.0
|
|
| 319 |
# via
|
| 320 |
# jsonschema
|
| 321 |
# referencing
|
| 322 |
-
ruff==0.15.13
|
| 323 |
-
# via faceverification
|
| 324 |
safehttpx==0.1.7
|
| 325 |
# via gradio
|
| 326 |
semantic-version==2.10.0
|
|
@@ -343,8 +321,6 @@ tenacity==9.1.4
|
|
| 343 |
# via chromadb
|
| 344 |
tokenizers==0.23.1
|
| 345 |
# via chromadb
|
| 346 |
-
tomli==2.4.1 ; python_full_version <= '3.11'
|
| 347 |
-
# via coverage
|
| 348 |
tomlkit==0.14.0
|
| 349 |
# via gradio
|
| 350 |
torch==2.12.0 ; sys_platform == 'darwin'
|
|
|
|
| 1 |
# This file was autogenerated by uv via the following command:
|
| 2 |
+
# uv --cache-dir .uv-cache export --locked --no-dev --no-hashes --no-emit-project --format requirements.txt --index https://download.pytorch.org/whl/cpu --output-file requirements.txt
|
|
|
|
|
|
|
| 3 |
aiohappyeyeballs==2.6.1
|
| 4 |
# via aiohttp
|
| 5 |
aiohttp==3.13.5
|
|
|
|
| 49 |
# via
|
| 50 |
# build
|
| 51 |
# click
|
|
|
|
| 52 |
# tqdm
|
| 53 |
# uvicorn
|
|
|
|
|
|
|
| 54 |
datasets==4.8.5
|
| 55 |
# via faceverification
|
| 56 |
dill==0.4.1
|
|
|
|
| 133 |
# via opentelemetry-api
|
| 134 |
importlib-resources==7.1.0
|
| 135 |
# via chromadb
|
|
|
|
|
|
|
| 136 |
jinja2==3.1.6
|
| 137 |
# via
|
| 138 |
# gradio
|
|
|
|
| 211 |
# gradio-client
|
| 212 |
# huggingface-hub
|
| 213 |
# onnxruntime
|
|
|
|
| 214 |
pandas==3.0.2
|
| 215 |
# via
|
| 216 |
# datasets
|
|
|
|
| 220 |
# facenet-pytorch
|
| 221 |
# gradio
|
| 222 |
# torchvision
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
propcache==0.5.2
|
| 224 |
# via
|
| 225 |
# aiohttp
|
|
|
|
| 249 |
pydub==0.25.1
|
| 250 |
# via gradio
|
| 251 |
pygments==2.20.0
|
| 252 |
+
# via rich
|
|
|
|
|
|
|
| 253 |
pyjwt==2.12.1
|
| 254 |
# via faceverification
|
| 255 |
pypika==0.51.1
|
| 256 |
# via chromadb
|
| 257 |
pyproject-hooks==1.2.0
|
| 258 |
# via build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
python-dateutil==2.9.0.post0
|
| 260 |
# via
|
| 261 |
# kubernetes
|
|
|
|
| 299 |
# via
|
| 300 |
# jsonschema
|
| 301 |
# referencing
|
|
|
|
|
|
|
| 302 |
safehttpx==0.1.7
|
| 303 |
# via gradio
|
| 304 |
semantic-version==2.10.0
|
|
|
|
| 321 |
# via chromadb
|
| 322 |
tokenizers==0.23.1
|
| 323 |
# via chromadb
|
|
|
|
|
|
|
| 324 |
tomlkit==0.14.0
|
| 325 |
# via gradio
|
| 326 |
torch==2.12.0 ; sys_platform == 'darwin'
|
src/faceverification/config.py
CHANGED
|
@@ -11,7 +11,7 @@ class Settings(BaseSettings):
|
|
| 11 |
vector_db_collection: str = "face_embeddings"
|
| 12 |
vector_db_persist_directory: str | None = None
|
| 13 |
vector_db_n_results: int = 5
|
| 14 |
-
face_match_threshold: float = 1.
|
| 15 |
|
| 16 |
device: Literal["auto", "cpu", "cuda"] = "auto"
|
| 17 |
mtcnn_thresholds: tuple[float, float, float] = (0.6, 0.7, 0.95)
|
|
|
|
| 11 |
vector_db_collection: str = "face_embeddings"
|
| 12 |
vector_db_persist_directory: str | None = None
|
| 13 |
vector_db_n_results: int = 5
|
| 14 |
+
face_match_threshold: float = 1.0764
|
| 15 |
|
| 16 |
device: Literal["auto", "cpu", "cuda"] = "auto"
|
| 17 |
mtcnn_thresholds: tuple[float, float, float] = (0.6, 0.7, 0.95)
|
uv.lock
CHANGED
|
@@ -568,11 +568,8 @@ dependencies = [
|
|
| 568 |
{ name = "pydantic" },
|
| 569 |
{ name = "pydantic-settings" },
|
| 570 |
{ name = "pyjwt" },
|
| 571 |
-
{ name = "pytest" },
|
| 572 |
-
{ name = "pytest-cov" },
|
| 573 |
{ name = "python-dotenv" },
|
| 574 |
{ name = "python-multipart" },
|
| 575 |
-
{ name = "ruff" },
|
| 576 |
{ name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
| 577 |
{ name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
|
| 578 |
{ name = "torchvision", version = "0.27.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
|
@@ -580,6 +577,13 @@ dependencies = [
|
|
| 580 |
{ name = "uvicorn", extra = ["standard"] },
|
| 581 |
]
|
| 582 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
[package.metadata]
|
| 584 |
requires-dist = [
|
| 585 |
{ name = "chromadb", specifier = ">=1.5.9" },
|
|
@@ -592,16 +596,20 @@ requires-dist = [
|
|
| 592 |
{ name = "pydantic", specifier = ">=2.11.10,<=2.12.5" },
|
| 593 |
{ name = "pydantic-settings", specifier = ">=2.14.1" },
|
| 594 |
{ name = "pyjwt", specifier = ">=2.10.1" },
|
| 595 |
-
{ name = "pytest", specifier = ">=9.0.3" },
|
| 596 |
-
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
| 597 |
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
| 598 |
{ name = "python-multipart", specifier = ">=0.0.28" },
|
| 599 |
-
{ name = "ruff", specifier = ">=0.15.13" },
|
| 600 |
{ name = "torch", specifier = ">=2.12.0", index = "https://download.pytorch.org/whl/cpu" },
|
| 601 |
{ name = "torchvision", specifier = ">=0.27.0", index = "https://download.pytorch.org/whl/cpu" },
|
| 602 |
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.46.0" },
|
| 603 |
]
|
| 604 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
[[package]]
|
| 606 |
name = "fastapi"
|
| 607 |
version = "0.136.1"
|
|
|
|
| 568 |
{ name = "pydantic" },
|
| 569 |
{ name = "pydantic-settings" },
|
| 570 |
{ name = "pyjwt" },
|
|
|
|
|
|
|
| 571 |
{ name = "python-dotenv" },
|
| 572 |
{ name = "python-multipart" },
|
|
|
|
| 573 |
{ name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
| 574 |
{ name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
|
| 575 |
{ name = "torchvision", version = "0.27.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
|
|
|
| 577 |
{ name = "uvicorn", extra = ["standard"] },
|
| 578 |
]
|
| 579 |
|
| 580 |
+
[package.dev-dependencies]
|
| 581 |
+
dev = [
|
| 582 |
+
{ name = "pytest" },
|
| 583 |
+
{ name = "pytest-cov" },
|
| 584 |
+
{ name = "ruff" },
|
| 585 |
+
]
|
| 586 |
+
|
| 587 |
[package.metadata]
|
| 588 |
requires-dist = [
|
| 589 |
{ name = "chromadb", specifier = ">=1.5.9" },
|
|
|
|
| 596 |
{ name = "pydantic", specifier = ">=2.11.10,<=2.12.5" },
|
| 597 |
{ name = "pydantic-settings", specifier = ">=2.14.1" },
|
| 598 |
{ name = "pyjwt", specifier = ">=2.10.1" },
|
|
|
|
|
|
|
| 599 |
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
| 600 |
{ name = "python-multipart", specifier = ">=0.0.28" },
|
|
|
|
| 601 |
{ name = "torch", specifier = ">=2.12.0", index = "https://download.pytorch.org/whl/cpu" },
|
| 602 |
{ name = "torchvision", specifier = ">=0.27.0", index = "https://download.pytorch.org/whl/cpu" },
|
| 603 |
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.46.0" },
|
| 604 |
]
|
| 605 |
|
| 606 |
+
[package.metadata.requires-dev]
|
| 607 |
+
dev = [
|
| 608 |
+
{ name = "pytest", specifier = ">=9.0.3" },
|
| 609 |
+
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
| 610 |
+
{ name = "ruff", specifier = ">=0.15.13" },
|
| 611 |
+
]
|
| 612 |
+
|
| 613 |
[[package]]
|
| 614 |
name = "fastapi"
|
| 615 |
version = "0.136.1"
|