authentasdk 0.2.8__tar.gz → 0.2.9__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.
- {authentasdk-0.2.8 → authentasdk-0.2.9}/PKG-INFO +120 -3
- {authentasdk-0.2.8 → authentasdk-0.2.9}/README.md +119 -2
- {authentasdk-0.2.8 → authentasdk-0.2.9}/setup.cfg +1 -1
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authenta/authenta_client.py +135 -6
- authentasdk-0.2.9/src/authenta/face_auth.py +176 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authentasdk.egg-info/PKG-INFO +120 -3
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authentasdk.egg-info/SOURCES.txt +1 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/pyproject.toml +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authenta/__init__.py +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authenta/async_authenta_client.py +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authenta/authenta_exceptions.py +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authenta/visualization.py +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authentasdk.egg-info/dependency_links.txt +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authentasdk.egg-info/requires.txt +0 -0
- {authentasdk-0.2.8 → authentasdk-0.2.9}/src/authentasdk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authentasdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Python SDK for the Authenta API to detect deepfakes and manipulated media
|
|
5
5
|
Home-page: https://github.com/phospheneai/authenta-python-sdk
|
|
6
6
|
Author: Arjun Adhithya
|
|
@@ -32,7 +32,8 @@ Welcome to the official documentation for the **Authenta Python SDK** — your g
|
|
|
32
32
|
- [4.3 FI-1 — Face Intelligence](#43-fi-1--face-intelligence)
|
|
33
33
|
- [4.4 FE-1 — Face Embedding](#44-fe-1--face-embedding)
|
|
34
34
|
- [4.5 DI-1 — Document Forgery Detection](#45-di-1--document-forgery-detection)
|
|
35
|
-
- [4.6
|
|
35
|
+
- [4.6 Face Indexing](#46-face-indexing)
|
|
36
|
+
- [4.7 Media Management](#47-media-management)
|
|
36
37
|
5. [Visualization](#5-visualization)
|
|
37
38
|
6. [Error Handling](#6-error-handling)
|
|
38
39
|
7. [API Reference](#7-api-reference)
|
|
@@ -650,7 +651,89 @@ asyncio.run(detect_document())
|
|
|
650
651
|
|
|
651
652
|
---
|
|
652
653
|
|
|
653
|
-
### 4.6
|
|
654
|
+
### 4.6 Face Indexing
|
|
655
|
+
|
|
656
|
+
Index a person's photos, then find that face again later. This is separate from
|
|
657
|
+
the detection models — it runs no FI-1/DF-1/AC-1 job, and uses three endpoints
|
|
658
|
+
on the same host and API key:
|
|
659
|
+
|
|
660
|
+
| Method | Endpoint |
|
|
661
|
+
|---|---|
|
|
662
|
+
| `faceEnroll(images)` | `POST /api/v1/facesim/v1/enroll` |
|
|
663
|
+
| `tenants()` | `GET /api/v1/facesim/v1/subjects` |
|
|
664
|
+
| `faceSearch(image, limit)` | `POST /api/v1/facesim/v1/search` |
|
|
665
|
+
|
|
666
|
+
#### Enrol a face
|
|
667
|
+
|
|
668
|
+
```python
|
|
669
|
+
enrolled = client.faceEnroll([
|
|
670
|
+
"data_samples/face_similiar/person_1/A.jpeg",
|
|
671
|
+
"data_samples/face_similiar/person_1/B.jpeg",
|
|
672
|
+
])
|
|
673
|
+
|
|
674
|
+
print(enrolled["subject_id"])
|
|
675
|
+
for face in enrolled["faces"]:
|
|
676
|
+
print(face["face_id"], face["status"]) # "uploaded" or "failed"
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Accepts **1–10 images** of the same person, and only **JPEG, PNG, or WebP**.
|
|
680
|
+
Every file is validated before anything is sent, so a bad path never leaves a
|
|
681
|
+
half-built subject on the server.
|
|
682
|
+
|
|
683
|
+
`faceEnroll` returns as soon as the photos reach S3. The embeddings are
|
|
684
|
+
generated in the background, so each face comes back as `uploaded`, not
|
|
685
|
+
`processed` — poll `tenants()` to see them settle. One failed upload marks only
|
|
686
|
+
its own face rather than sinking the batch.
|
|
687
|
+
|
|
688
|
+
#### Check indexing progress
|
|
689
|
+
|
|
690
|
+
```python
|
|
691
|
+
for subject in client.tenants()["subjects"]:
|
|
692
|
+
for face in subject["faces"]:
|
|
693
|
+
print(subject["subject_id"], face["name"], face["status"], face["error"])
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
| Status | Meaning |
|
|
697
|
+
|---|---|
|
|
698
|
+
| `pending` | Upload URL created; the object has not been acknowledged yet |
|
|
699
|
+
| `uploaded` | Stored; waiting for a worker |
|
|
700
|
+
| `processing` | A worker is generating the embedding |
|
|
701
|
+
| `processed` | Embedding stored — the face is searchable |
|
|
702
|
+
| `failed` | Upload validation or embedding failed — see `error` |
|
|
703
|
+
|
|
704
|
+
Only `processed` faces are searched, so searching too early returns `count: 0`.
|
|
705
|
+
|
|
706
|
+
#### Search a face
|
|
707
|
+
|
|
708
|
+
```python
|
|
709
|
+
matches = client.faceSearch("data_samples/face_similiar/person_1/A.jpeg", limit=10)
|
|
710
|
+
|
|
711
|
+
print(matches["count"])
|
|
712
|
+
for match in matches["results"]:
|
|
713
|
+
print(match["rank"], match["subject_id"], match["similarity_score"])
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The photo is posted as `multipart/form-data` rather than Base64 in a JSON body,
|
|
717
|
+
which keeps it clear of the server's 100 KiB JSON limit. `limit` is clamped to
|
|
718
|
+
1–50 and defaults to 50.
|
|
719
|
+
|
|
720
|
+
Search is independent of enrolment: it matches everything already indexed on the
|
|
721
|
+
account, including from earlier sessions. The same subject can appear more than
|
|
722
|
+
once because every enrolled face has its own embedding, and results run from
|
|
723
|
+
highest similarity down.
|
|
724
|
+
|
|
725
|
+
Unsupported formats are converted to JPEG when **Pillow** is installed
|
|
726
|
+
(`pip install Pillow`); without it you get a clear `ValidationError` instead of
|
|
727
|
+
a server-side `invalid_image`. The conversion also bakes any EXIF rotation into
|
|
728
|
+
the pixels — an image left with a stale orientation tag is read sideways by face
|
|
729
|
+
detection and comes back as `no_face_detected`.
|
|
730
|
+
|
|
731
|
+
A full runnable script lives in
|
|
732
|
+
[`examples/test_face_indexing.py`](examples/test_face_indexing.py).
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
### 4.7 Media Management
|
|
654
737
|
|
|
655
738
|
#### Get Media
|
|
656
739
|
|
|
@@ -960,6 +1043,40 @@ Raises `ValueError` if no result artifact is found.
|
|
|
960
1043
|
|
|
961
1044
|
---
|
|
962
1045
|
|
|
1046
|
+
#### `faceEnroll(images) -> Dict`
|
|
1047
|
+
|
|
1048
|
+
Enrol 1–10 photos of one person: creates the subject, then uploads each image
|
|
1049
|
+
to its presigned S3 URL.
|
|
1050
|
+
|
|
1051
|
+
| Parameter | Type | Description |
|
|
1052
|
+
|---|---|---|
|
|
1053
|
+
| `images` | `list[str]` | Local paths. JPEG, PNG, or WebP only |
|
|
1054
|
+
|
|
1055
|
+
Returns the enroll response — `subject_id`, `status`, and a `faces` array whose
|
|
1056
|
+
`status` is set to `uploaded` or `failed` per image. Embeddings follow out of
|
|
1057
|
+
band; poll `tenants()` for `processed`.
|
|
1058
|
+
|
|
1059
|
+
Raises `ValidationError` for an empty list, more than 10 images, a missing file,
|
|
1060
|
+
or an unsupported image type — all checked before any request is sent.
|
|
1061
|
+
|
|
1062
|
+
#### `tenants() -> Dict`
|
|
1063
|
+
|
|
1064
|
+
Every subject and face on the account: `{"tenant_id", "subjects"}`, where each
|
|
1065
|
+
subject holds `face_id`, `name`, `status`, `embedding`, `image_url`, and `error`.
|
|
1066
|
+
|
|
1067
|
+
#### `faceSearch(image, limit=50, timeout=120.0) -> Dict`
|
|
1068
|
+
|
|
1069
|
+
Rank enrolled faces against a query photo, posted as `multipart/form-data`.
|
|
1070
|
+
|
|
1071
|
+
| Parameter | Type | Default | Description |
|
|
1072
|
+
|---|---|---|---|
|
|
1073
|
+
| `image` | `str` | — | Local path to the query photo |
|
|
1074
|
+
| `limit` | `int` | `50` | Matches to return, clamped to 1–50 |
|
|
1075
|
+
| `timeout` | `float` | `120.0` | Seconds — embedding takes longer than a plain request |
|
|
1076
|
+
|
|
1077
|
+
Returns `{"tenant_id", "count", "results"}` with results ordered from highest
|
|
1078
|
+
similarity down. Only faces with `status: "processed"` are searched.
|
|
1079
|
+
|
|
963
1080
|
#### `get_media(jobid) -> Dict`
|
|
964
1081
|
|
|
965
1082
|
`GET /api/v1/jobs/{jobid}` — fetch the current state of a media record.
|
|
@@ -15,7 +15,8 @@ Welcome to the official documentation for the **Authenta Python SDK** — your g
|
|
|
15
15
|
- [4.3 FI-1 — Face Intelligence](#43-fi-1--face-intelligence)
|
|
16
16
|
- [4.4 FE-1 — Face Embedding](#44-fe-1--face-embedding)
|
|
17
17
|
- [4.5 DI-1 — Document Forgery Detection](#45-di-1--document-forgery-detection)
|
|
18
|
-
- [4.6
|
|
18
|
+
- [4.6 Face Indexing](#46-face-indexing)
|
|
19
|
+
- [4.7 Media Management](#47-media-management)
|
|
19
20
|
5. [Visualization](#5-visualization)
|
|
20
21
|
6. [Error Handling](#6-error-handling)
|
|
21
22
|
7. [API Reference](#7-api-reference)
|
|
@@ -633,7 +634,89 @@ asyncio.run(detect_document())
|
|
|
633
634
|
|
|
634
635
|
---
|
|
635
636
|
|
|
636
|
-
### 4.6
|
|
637
|
+
### 4.6 Face Indexing
|
|
638
|
+
|
|
639
|
+
Index a person's photos, then find that face again later. This is separate from
|
|
640
|
+
the detection models — it runs no FI-1/DF-1/AC-1 job, and uses three endpoints
|
|
641
|
+
on the same host and API key:
|
|
642
|
+
|
|
643
|
+
| Method | Endpoint |
|
|
644
|
+
|---|---|
|
|
645
|
+
| `faceEnroll(images)` | `POST /api/v1/facesim/v1/enroll` |
|
|
646
|
+
| `tenants()` | `GET /api/v1/facesim/v1/subjects` |
|
|
647
|
+
| `faceSearch(image, limit)` | `POST /api/v1/facesim/v1/search` |
|
|
648
|
+
|
|
649
|
+
#### Enrol a face
|
|
650
|
+
|
|
651
|
+
```python
|
|
652
|
+
enrolled = client.faceEnroll([
|
|
653
|
+
"data_samples/face_similiar/person_1/A.jpeg",
|
|
654
|
+
"data_samples/face_similiar/person_1/B.jpeg",
|
|
655
|
+
])
|
|
656
|
+
|
|
657
|
+
print(enrolled["subject_id"])
|
|
658
|
+
for face in enrolled["faces"]:
|
|
659
|
+
print(face["face_id"], face["status"]) # "uploaded" or "failed"
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Accepts **1–10 images** of the same person, and only **JPEG, PNG, or WebP**.
|
|
663
|
+
Every file is validated before anything is sent, so a bad path never leaves a
|
|
664
|
+
half-built subject on the server.
|
|
665
|
+
|
|
666
|
+
`faceEnroll` returns as soon as the photos reach S3. The embeddings are
|
|
667
|
+
generated in the background, so each face comes back as `uploaded`, not
|
|
668
|
+
`processed` — poll `tenants()` to see them settle. One failed upload marks only
|
|
669
|
+
its own face rather than sinking the batch.
|
|
670
|
+
|
|
671
|
+
#### Check indexing progress
|
|
672
|
+
|
|
673
|
+
```python
|
|
674
|
+
for subject in client.tenants()["subjects"]:
|
|
675
|
+
for face in subject["faces"]:
|
|
676
|
+
print(subject["subject_id"], face["name"], face["status"], face["error"])
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
| Status | Meaning |
|
|
680
|
+
|---|---|
|
|
681
|
+
| `pending` | Upload URL created; the object has not been acknowledged yet |
|
|
682
|
+
| `uploaded` | Stored; waiting for a worker |
|
|
683
|
+
| `processing` | A worker is generating the embedding |
|
|
684
|
+
| `processed` | Embedding stored — the face is searchable |
|
|
685
|
+
| `failed` | Upload validation or embedding failed — see `error` |
|
|
686
|
+
|
|
687
|
+
Only `processed` faces are searched, so searching too early returns `count: 0`.
|
|
688
|
+
|
|
689
|
+
#### Search a face
|
|
690
|
+
|
|
691
|
+
```python
|
|
692
|
+
matches = client.faceSearch("data_samples/face_similiar/person_1/A.jpeg", limit=10)
|
|
693
|
+
|
|
694
|
+
print(matches["count"])
|
|
695
|
+
for match in matches["results"]:
|
|
696
|
+
print(match["rank"], match["subject_id"], match["similarity_score"])
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
The photo is posted as `multipart/form-data` rather than Base64 in a JSON body,
|
|
700
|
+
which keeps it clear of the server's 100 KiB JSON limit. `limit` is clamped to
|
|
701
|
+
1–50 and defaults to 50.
|
|
702
|
+
|
|
703
|
+
Search is independent of enrolment: it matches everything already indexed on the
|
|
704
|
+
account, including from earlier sessions. The same subject can appear more than
|
|
705
|
+
once because every enrolled face has its own embedding, and results run from
|
|
706
|
+
highest similarity down.
|
|
707
|
+
|
|
708
|
+
Unsupported formats are converted to JPEG when **Pillow** is installed
|
|
709
|
+
(`pip install Pillow`); without it you get a clear `ValidationError` instead of
|
|
710
|
+
a server-side `invalid_image`. The conversion also bakes any EXIF rotation into
|
|
711
|
+
the pixels — an image left with a stale orientation tag is read sideways by face
|
|
712
|
+
detection and comes back as `no_face_detected`.
|
|
713
|
+
|
|
714
|
+
A full runnable script lives in
|
|
715
|
+
[`examples/test_face_indexing.py`](examples/test_face_indexing.py).
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
### 4.7 Media Management
|
|
637
720
|
|
|
638
721
|
#### Get Media
|
|
639
722
|
|
|
@@ -943,6 +1026,40 @@ Raises `ValueError` if no result artifact is found.
|
|
|
943
1026
|
|
|
944
1027
|
---
|
|
945
1028
|
|
|
1029
|
+
#### `faceEnroll(images) -> Dict`
|
|
1030
|
+
|
|
1031
|
+
Enrol 1–10 photos of one person: creates the subject, then uploads each image
|
|
1032
|
+
to its presigned S3 URL.
|
|
1033
|
+
|
|
1034
|
+
| Parameter | Type | Description |
|
|
1035
|
+
|---|---|---|
|
|
1036
|
+
| `images` | `list[str]` | Local paths. JPEG, PNG, or WebP only |
|
|
1037
|
+
|
|
1038
|
+
Returns the enroll response — `subject_id`, `status`, and a `faces` array whose
|
|
1039
|
+
`status` is set to `uploaded` or `failed` per image. Embeddings follow out of
|
|
1040
|
+
band; poll `tenants()` for `processed`.
|
|
1041
|
+
|
|
1042
|
+
Raises `ValidationError` for an empty list, more than 10 images, a missing file,
|
|
1043
|
+
or an unsupported image type — all checked before any request is sent.
|
|
1044
|
+
|
|
1045
|
+
#### `tenants() -> Dict`
|
|
1046
|
+
|
|
1047
|
+
Every subject and face on the account: `{"tenant_id", "subjects"}`, where each
|
|
1048
|
+
subject holds `face_id`, `name`, `status`, `embedding`, `image_url`, and `error`.
|
|
1049
|
+
|
|
1050
|
+
#### `faceSearch(image, limit=50, timeout=120.0) -> Dict`
|
|
1051
|
+
|
|
1052
|
+
Rank enrolled faces against a query photo, posted as `multipart/form-data`.
|
|
1053
|
+
|
|
1054
|
+
| Parameter | Type | Default | Description |
|
|
1055
|
+
|---|---|---|---|
|
|
1056
|
+
| `image` | `str` | — | Local path to the query photo |
|
|
1057
|
+
| `limit` | `int` | `50` | Matches to return, clamped to 1–50 |
|
|
1058
|
+
| `timeout` | `float` | `120.0` | Seconds — embedding takes longer than a plain request |
|
|
1059
|
+
|
|
1060
|
+
Returns `{"tenant_id", "count", "results"}` with results ordered from highest
|
|
1061
|
+
similarity down. Only faces with `status: "processed"` are searched.
|
|
1062
|
+
|
|
946
1063
|
#### `get_media(jobid) -> Dict`
|
|
947
1064
|
|
|
948
1065
|
`GET /api/v1/jobs/{jobid}` — fetch the current state of a media record.
|
|
@@ -21,6 +21,12 @@ from .authenta_exceptions import (
|
|
|
21
21
|
ValidationError,
|
|
22
22
|
ServerError,
|
|
23
23
|
)
|
|
24
|
+
from .face_auth import (
|
|
25
|
+
BASE as FACESIM_BASE,
|
|
26
|
+
MAX_SEARCH_LIMIT,
|
|
27
|
+
FaceAuth,
|
|
28
|
+
clamp_limit,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
def _raise_for_authenta_error(resp: requests.Response) -> None:
|
|
26
32
|
"""
|
|
@@ -101,18 +107,23 @@ class AuthentaClient:
|
|
|
101
107
|
Create new Authenta client.
|
|
102
108
|
|
|
103
109
|
Args:
|
|
104
|
-
base_url: Authenta API base URL, e.g. "https://platform
|
|
110
|
+
base_url: Authenta API base URL, e.g. "https://platform.authenta.ai".
|
|
105
111
|
api_key: Your Authenta API key.
|
|
106
112
|
"""
|
|
107
113
|
self.base_url = base_url.rstrip("/")
|
|
108
114
|
self.api_key = api_key
|
|
109
115
|
self.auth_enabled = bool(api_key)
|
|
110
116
|
|
|
111
|
-
def _headers(self) -> Dict[str, str]:
|
|
112
|
-
"""
|
|
113
|
-
headers
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
def _headers(self, content_type: Optional[str] = "application/json") -> Dict[str, str]:
|
|
118
|
+
"""
|
|
119
|
+
Return default headers for Authenta API calls.
|
|
120
|
+
|
|
121
|
+
Pass ``content_type=None`` for multipart uploads — requests generates
|
|
122
|
+
the boundary itself, and setting the header by hand breaks it.
|
|
123
|
+
"""
|
|
124
|
+
headers = {}
|
|
125
|
+
if content_type:
|
|
126
|
+
headers["Content-Type"] = content_type
|
|
116
127
|
if self.auth_enabled:
|
|
117
128
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
118
129
|
|
|
@@ -376,6 +387,124 @@ class AuthentaClient:
|
|
|
376
387
|
|
|
377
388
|
return media
|
|
378
389
|
|
|
390
|
+
|
|
391
|
+
# ── Face indexing ──────────────────────────────────────────────────────
|
|
392
|
+
# Three endpoints on this same host and API key. See face_auth.py.
|
|
393
|
+
|
|
394
|
+
def faceEnroll(self, images: list) -> Dict[str, Any]:
|
|
395
|
+
"""
|
|
396
|
+
Enrol photos of one person: create the subject, then upload each image.
|
|
397
|
+
|
|
398
|
+
Returns as soon as S3 has the bytes. Embeddings are generated out of
|
|
399
|
+
band, so call :meth:`tenants` afterwards to watch each face reach
|
|
400
|
+
``processed``.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
images: Local paths to 1-10 photos of the same person. Only
|
|
404
|
+
JPEG, PNG, and WebP are accepted.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
The enroll response, with each face's ``status`` updated to
|
|
408
|
+
``uploaded`` or ``failed`` to reflect its own upload outcome.
|
|
409
|
+
"""
|
|
410
|
+
face_auth = FaceAuth()
|
|
411
|
+
described_images = face_auth.EnrollFace(images)
|
|
412
|
+
|
|
413
|
+
resp = requests.post(
|
|
414
|
+
f"{self.base_url}{FACESIM_BASE}/enroll",
|
|
415
|
+
json={"images": described_images},
|
|
416
|
+
headers=self._headers(),
|
|
417
|
+
timeout=30,
|
|
418
|
+
)
|
|
419
|
+
if not resp.ok:
|
|
420
|
+
_raise_for_authenta_error(resp)
|
|
421
|
+
enrollment = _safe_json(resp)
|
|
422
|
+
|
|
423
|
+
faces = enrollment.get("faces", [])
|
|
424
|
+
if len(faces) != len(images):
|
|
425
|
+
raise RuntimeError(
|
|
426
|
+
f"Enrollment returned {len(faces)} upload URLs for {len(images)} images"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# The response preserves the order of `images`. One failed PUT marks
|
|
430
|
+
# only its own face, rather than sinking the whole batch.
|
|
431
|
+
for face, image, described in zip(faces, images, described_images):
|
|
432
|
+
upload_url = face.get("upload_url")
|
|
433
|
+
if not upload_url:
|
|
434
|
+
face["status"] = "failed"
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
content_type = (face.get("headers") or {}).get(
|
|
438
|
+
"Content-Type", described["contentType"]
|
|
439
|
+
)
|
|
440
|
+
print(f"Uploading {os.path.basename(image)}...")
|
|
441
|
+
status = face_auth.putSignedUrl(upload_url, image, content_type)
|
|
442
|
+
face["status"] = "uploaded" if 200 <= status < 300 else "failed"
|
|
443
|
+
|
|
444
|
+
return enrollment
|
|
445
|
+
|
|
446
|
+
def tenants(self) -> Dict[str, Any]:
|
|
447
|
+
"""
|
|
448
|
+
GET /api/v1/facesim/v1/subjects: every subject and face on the account.
|
|
449
|
+
|
|
450
|
+
Use it to follow enrolment progress — a face is searchable once its
|
|
451
|
+
status reaches ``processed``.
|
|
452
|
+
"""
|
|
453
|
+
resp = requests.get(
|
|
454
|
+
f"{self.base_url}{FACESIM_BASE}/tenant",
|
|
455
|
+
headers=self._headers(),
|
|
456
|
+
timeout=30,
|
|
457
|
+
)
|
|
458
|
+
if not resp.ok:
|
|
459
|
+
_raise_for_authenta_error(resp)
|
|
460
|
+
return _safe_json(resp)
|
|
461
|
+
|
|
462
|
+
def faceSearch(
|
|
463
|
+
self,
|
|
464
|
+
image: str,
|
|
465
|
+
limit: int = MAX_SEARCH_LIMIT,
|
|
466
|
+
timeout: float = 120.0,
|
|
467
|
+
) -> Dict[str, Any]:
|
|
468
|
+
"""
|
|
469
|
+
POST /api/v1/facesim/v1/search: rank enrolled faces against a photo.
|
|
470
|
+
|
|
471
|
+
The image is posted as ``multipart/form-data`` rather than Base64 in a
|
|
472
|
+
JSON body, which keeps it clear of the server's 100 KiB JSON limit.
|
|
473
|
+
|
|
474
|
+
Search is independent of enrolment: it matches everything already
|
|
475
|
+
indexed on the account, including from earlier sessions. The same
|
|
476
|
+
subject can appear more than once because every enrolled face has its
|
|
477
|
+
own embedding.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
image: Local path to the query photo.
|
|
481
|
+
limit: How many matches to return, clamped to 1-50.
|
|
482
|
+
timeout: Seconds to wait — embedding a face takes longer than a
|
|
483
|
+
plain request.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
``{"tenant_id", "count", "results"}`` with results ordered from
|
|
487
|
+
highest similarity down.
|
|
488
|
+
"""
|
|
489
|
+
face_auth = FaceAuth()
|
|
490
|
+
path, content_type = face_auth.prepare_search_image(image)
|
|
491
|
+
capped = clamp_limit(limit)
|
|
492
|
+
|
|
493
|
+
print(f"Searching faces with {os.path.basename(path)} (limit {capped})...")
|
|
494
|
+
with open(path, "rb") as handle:
|
|
495
|
+
resp = requests.post(
|
|
496
|
+
f"{self.base_url}{FACESIM_BASE}/search",
|
|
497
|
+
# No Content-Type header here: requests sets the multipart
|
|
498
|
+
# boundary itself, and overriding it breaks the upload.
|
|
499
|
+
headers=self._headers(content_type=None),
|
|
500
|
+
files={"image": (os.path.basename(path), handle, content_type)},
|
|
501
|
+
data={"limit": str(capped)},
|
|
502
|
+
timeout=timeout,
|
|
503
|
+
)
|
|
504
|
+
if not resp.ok:
|
|
505
|
+
_raise_for_authenta_error(resp)
|
|
506
|
+
return _safe_json(resp)
|
|
507
|
+
|
|
379
508
|
def get_result(self, media: Dict[str, Any]) -> Dict[str, Any]:
|
|
380
509
|
"""
|
|
381
510
|
Fetch the detection result JSON from the media's resultURL.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Face indexing helpers — enrol faces, list subjects, search a face.
|
|
3
|
+
|
|
4
|
+
Mirrors ``packages/core/src/core/faceauth.ts`` in the React Native SDK. Three
|
|
5
|
+
endpoints, all on the same host and API key as the rest of the platform:
|
|
6
|
+
|
|
7
|
+
POST /api/v1/facesim/v1/enroll create a subject + presigned upload URLs
|
|
8
|
+
GET /api/v1/facesim/v1/subjects every subject and face
|
|
9
|
+
POST /api/v1/facesim/v1/search rank enrolled faces against a photo
|
|
10
|
+
|
|
11
|
+
The functions here do the local work — validating files, deriving names and
|
|
12
|
+
content types, uploading to S3. The HTTP calls live on
|
|
13
|
+
``AuthentaClient.faceEnroll`` / ``faceSearch`` / ``tenants``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import mimetypes
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
from .authenta_exceptions import ValidationError
|
|
23
|
+
|
|
24
|
+
# ── Contract limits, matching the React Native SDK ──────────────────────────
|
|
25
|
+
|
|
26
|
+
BASE = "/api/v1/facesim/v1"
|
|
27
|
+
|
|
28
|
+
#: Content types the face indexing service can decode.
|
|
29
|
+
SUPPORTED_FACE_IMAGE_TYPES = ("image/jpeg", "image/png", "image/webp")
|
|
30
|
+
|
|
31
|
+
#: Enrollment accepts between 1 and 10 images per subject.
|
|
32
|
+
MIN_ENROLL_IMAGES = 1
|
|
33
|
+
MAX_ENROLL_IMAGES = 10
|
|
34
|
+
|
|
35
|
+
#: Search returns at most 50 ranked faces.
|
|
36
|
+
MAX_SEARCH_LIMIT = 50
|
|
37
|
+
|
|
38
|
+
#: A face has settled once it can no longer change.
|
|
39
|
+
TERMINAL_FACE_STATUSES = ("processed", "failed")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def guess_content_type(path: str) -> str:
|
|
43
|
+
"""Guess the MIME type of a file from its name."""
|
|
44
|
+
content_type, _ = mimetypes.guess_type(path)
|
|
45
|
+
return (content_type or "application/octet-stream").lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def describe_image(image: str) -> Dict[str, str]:
|
|
49
|
+
"""
|
|
50
|
+
Build the ``{"name", "contentType"}`` descriptor the enroll endpoint expects.
|
|
51
|
+
|
|
52
|
+
The full file name is kept — including the extension — because the server
|
|
53
|
+
uses it to label the face, and the content type is derived from it.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValidationError: if the file is missing, unnamed, or an unsupported type.
|
|
57
|
+
"""
|
|
58
|
+
if not isinstance(image, str) or not image.strip():
|
|
59
|
+
raise ValidationError("Image path must be a non-empty string.")
|
|
60
|
+
if not os.path.isfile(image):
|
|
61
|
+
raise ValidationError(f"Image file does not exist: {image}")
|
|
62
|
+
|
|
63
|
+
name = os.path.basename(image).strip()
|
|
64
|
+
if not name or len(name) > 255:
|
|
65
|
+
raise ValidationError(f'Image name must be 1-255 characters - received: "{name}"')
|
|
66
|
+
|
|
67
|
+
content_type = guess_content_type(name)
|
|
68
|
+
if content_type not in SUPPORTED_FACE_IMAGE_TYPES:
|
|
69
|
+
raise ValidationError(
|
|
70
|
+
f'Unsupported image type "{content_type}" for {name}. '
|
|
71
|
+
f"Face indexing accepts {', '.join(SUPPORTED_FACE_IMAGE_TYPES)}."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return {"name": name, "contentType": content_type}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def image_basename(image: str) -> Dict[str, str]:
|
|
78
|
+
"""Backwards-compatible alias of :func:`describe_image`."""
|
|
79
|
+
return describe_image(image)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def clamp_limit(limit: Optional[int]) -> int:
|
|
83
|
+
"""Clamp a search limit into the 1-50 range the API accepts."""
|
|
84
|
+
try:
|
|
85
|
+
value = int(limit)
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
value = MAX_SEARCH_LIMIT
|
|
88
|
+
return max(1, min(value, MAX_SEARCH_LIMIT))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FaceAuth:
|
|
92
|
+
"""Local file handling for the face indexing endpoints."""
|
|
93
|
+
|
|
94
|
+
def EnrollFace(self, images: List[str]) -> List[Dict[str, str]]:
|
|
95
|
+
"""
|
|
96
|
+
Validate the images and build the ``images`` array for ``POST /enroll``.
|
|
97
|
+
|
|
98
|
+
Every file is checked before anything is sent, so a bad path fails
|
|
99
|
+
before it can leave a half-built subject on the server.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
images: Local paths to 1-10 photos of the same person.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
One ``{"name", "contentType"}`` descriptor per image, in order.
|
|
106
|
+
"""
|
|
107
|
+
if not isinstance(images, (list, tuple)):
|
|
108
|
+
raise ValidationError("images must be a list of file paths.")
|
|
109
|
+
if len(images) < MIN_ENROLL_IMAGES or len(images) > MAX_ENROLL_IMAGES:
|
|
110
|
+
raise ValidationError(
|
|
111
|
+
f"Enrollment accepts {MIN_ENROLL_IMAGES}-{MAX_ENROLL_IMAGES} images "
|
|
112
|
+
f"- received {len(images)}."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return [describe_image(image) for image in images]
|
|
116
|
+
|
|
117
|
+
def putSignedUrl(
|
|
118
|
+
self,
|
|
119
|
+
url: str,
|
|
120
|
+
image: str,
|
|
121
|
+
content_type: Optional[str] = None,
|
|
122
|
+
) -> int:
|
|
123
|
+
"""
|
|
124
|
+
PUT one image to its presigned S3 URL.
|
|
125
|
+
|
|
126
|
+
The ``Content-Type`` must match the one the enroll response returned, or
|
|
127
|
+
S3 rejects the signature with 403.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The HTTP status code from S3.
|
|
131
|
+
"""
|
|
132
|
+
headers = {"Content-Type": content_type or guess_content_type(image)}
|
|
133
|
+
with open(image, "rb") as handle:
|
|
134
|
+
response = requests.put(url, data=handle, headers=headers, timeout=300)
|
|
135
|
+
return response.status_code
|
|
136
|
+
|
|
137
|
+
def prepare_search_image(self, image: str) -> Tuple[str, str]:
|
|
138
|
+
"""
|
|
139
|
+
Check a query photo and report how to send it.
|
|
140
|
+
|
|
141
|
+
Unsupported formats (HEIC, TIFF, ...) are converted to JPEG when Pillow
|
|
142
|
+
is installed; without Pillow the caller gets a clear error instead of a
|
|
143
|
+
server-side ``invalid_image``.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
``(path, content_type)`` ready for a multipart upload.
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(image, str) or not image.strip():
|
|
149
|
+
raise ValidationError("A query image is required to search faces.")
|
|
150
|
+
if not os.path.isfile(image):
|
|
151
|
+
raise ValidationError(f"Image file does not exist: {image}")
|
|
152
|
+
|
|
153
|
+
content_type = guess_content_type(image)
|
|
154
|
+
if content_type in SUPPORTED_FACE_IMAGE_TYPES:
|
|
155
|
+
return image, content_type
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
from PIL import Image # optional dependency
|
|
159
|
+
except ImportError:
|
|
160
|
+
raise ValidationError(
|
|
161
|
+
f'Unsupported image type "{content_type}" for {os.path.basename(image)}. '
|
|
162
|
+
f"Face indexing accepts {', '.join(SUPPORTED_FACE_IMAGE_TYPES)} - "
|
|
163
|
+
"install Pillow to convert other formats automatically."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
converted = f"{os.path.splitext(image)[0]}.authenta.jpg"
|
|
167
|
+
with Image.open(image) as handle:
|
|
168
|
+
# Convert to RGB and save: this also bakes any EXIF rotation into
|
|
169
|
+
# the pixels, which is what face detection relies on.
|
|
170
|
+
handle.convert("RGB").save(converted, "JPEG", quality=95)
|
|
171
|
+
return converted, "image/jpeg"
|
|
172
|
+
|
|
173
|
+
def search_files(self, image: str) -> Dict[str, Any]:
|
|
174
|
+
"""Build the ``files=`` mapping for a multipart search request."""
|
|
175
|
+
path, content_type = self.prepare_search_image(image)
|
|
176
|
+
return {"path": path, "content_type": content_type}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authentasdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Python SDK for the Authenta API to detect deepfakes and manipulated media
|
|
5
5
|
Home-page: https://github.com/phospheneai/authenta-python-sdk
|
|
6
6
|
Author: Arjun Adhithya
|
|
@@ -32,7 +32,8 @@ Welcome to the official documentation for the **Authenta Python SDK** — your g
|
|
|
32
32
|
- [4.3 FI-1 — Face Intelligence](#43-fi-1--face-intelligence)
|
|
33
33
|
- [4.4 FE-1 — Face Embedding](#44-fe-1--face-embedding)
|
|
34
34
|
- [4.5 DI-1 — Document Forgery Detection](#45-di-1--document-forgery-detection)
|
|
35
|
-
- [4.6
|
|
35
|
+
- [4.6 Face Indexing](#46-face-indexing)
|
|
36
|
+
- [4.7 Media Management](#47-media-management)
|
|
36
37
|
5. [Visualization](#5-visualization)
|
|
37
38
|
6. [Error Handling](#6-error-handling)
|
|
38
39
|
7. [API Reference](#7-api-reference)
|
|
@@ -650,7 +651,89 @@ asyncio.run(detect_document())
|
|
|
650
651
|
|
|
651
652
|
---
|
|
652
653
|
|
|
653
|
-
### 4.6
|
|
654
|
+
### 4.6 Face Indexing
|
|
655
|
+
|
|
656
|
+
Index a person's photos, then find that face again later. This is separate from
|
|
657
|
+
the detection models — it runs no FI-1/DF-1/AC-1 job, and uses three endpoints
|
|
658
|
+
on the same host and API key:
|
|
659
|
+
|
|
660
|
+
| Method | Endpoint |
|
|
661
|
+
|---|---|
|
|
662
|
+
| `faceEnroll(images)` | `POST /api/v1/facesim/v1/enroll` |
|
|
663
|
+
| `tenants()` | `GET /api/v1/facesim/v1/subjects` |
|
|
664
|
+
| `faceSearch(image, limit)` | `POST /api/v1/facesim/v1/search` |
|
|
665
|
+
|
|
666
|
+
#### Enrol a face
|
|
667
|
+
|
|
668
|
+
```python
|
|
669
|
+
enrolled = client.faceEnroll([
|
|
670
|
+
"data_samples/face_similiar/person_1/A.jpeg",
|
|
671
|
+
"data_samples/face_similiar/person_1/B.jpeg",
|
|
672
|
+
])
|
|
673
|
+
|
|
674
|
+
print(enrolled["subject_id"])
|
|
675
|
+
for face in enrolled["faces"]:
|
|
676
|
+
print(face["face_id"], face["status"]) # "uploaded" or "failed"
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Accepts **1–10 images** of the same person, and only **JPEG, PNG, or WebP**.
|
|
680
|
+
Every file is validated before anything is sent, so a bad path never leaves a
|
|
681
|
+
half-built subject on the server.
|
|
682
|
+
|
|
683
|
+
`faceEnroll` returns as soon as the photos reach S3. The embeddings are
|
|
684
|
+
generated in the background, so each face comes back as `uploaded`, not
|
|
685
|
+
`processed` — poll `tenants()` to see them settle. One failed upload marks only
|
|
686
|
+
its own face rather than sinking the batch.
|
|
687
|
+
|
|
688
|
+
#### Check indexing progress
|
|
689
|
+
|
|
690
|
+
```python
|
|
691
|
+
for subject in client.tenants()["subjects"]:
|
|
692
|
+
for face in subject["faces"]:
|
|
693
|
+
print(subject["subject_id"], face["name"], face["status"], face["error"])
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
| Status | Meaning |
|
|
697
|
+
|---|---|
|
|
698
|
+
| `pending` | Upload URL created; the object has not been acknowledged yet |
|
|
699
|
+
| `uploaded` | Stored; waiting for a worker |
|
|
700
|
+
| `processing` | A worker is generating the embedding |
|
|
701
|
+
| `processed` | Embedding stored — the face is searchable |
|
|
702
|
+
| `failed` | Upload validation or embedding failed — see `error` |
|
|
703
|
+
|
|
704
|
+
Only `processed` faces are searched, so searching too early returns `count: 0`.
|
|
705
|
+
|
|
706
|
+
#### Search a face
|
|
707
|
+
|
|
708
|
+
```python
|
|
709
|
+
matches = client.faceSearch("data_samples/face_similiar/person_1/A.jpeg", limit=10)
|
|
710
|
+
|
|
711
|
+
print(matches["count"])
|
|
712
|
+
for match in matches["results"]:
|
|
713
|
+
print(match["rank"], match["subject_id"], match["similarity_score"])
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The photo is posted as `multipart/form-data` rather than Base64 in a JSON body,
|
|
717
|
+
which keeps it clear of the server's 100 KiB JSON limit. `limit` is clamped to
|
|
718
|
+
1–50 and defaults to 50.
|
|
719
|
+
|
|
720
|
+
Search is independent of enrolment: it matches everything already indexed on the
|
|
721
|
+
account, including from earlier sessions. The same subject can appear more than
|
|
722
|
+
once because every enrolled face has its own embedding, and results run from
|
|
723
|
+
highest similarity down.
|
|
724
|
+
|
|
725
|
+
Unsupported formats are converted to JPEG when **Pillow** is installed
|
|
726
|
+
(`pip install Pillow`); without it you get a clear `ValidationError` instead of
|
|
727
|
+
a server-side `invalid_image`. The conversion also bakes any EXIF rotation into
|
|
728
|
+
the pixels — an image left with a stale orientation tag is read sideways by face
|
|
729
|
+
detection and comes back as `no_face_detected`.
|
|
730
|
+
|
|
731
|
+
A full runnable script lives in
|
|
732
|
+
[`examples/test_face_indexing.py`](examples/test_face_indexing.py).
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
### 4.7 Media Management
|
|
654
737
|
|
|
655
738
|
#### Get Media
|
|
656
739
|
|
|
@@ -960,6 +1043,40 @@ Raises `ValueError` if no result artifact is found.
|
|
|
960
1043
|
|
|
961
1044
|
---
|
|
962
1045
|
|
|
1046
|
+
#### `faceEnroll(images) -> Dict`
|
|
1047
|
+
|
|
1048
|
+
Enrol 1–10 photos of one person: creates the subject, then uploads each image
|
|
1049
|
+
to its presigned S3 URL.
|
|
1050
|
+
|
|
1051
|
+
| Parameter | Type | Description |
|
|
1052
|
+
|---|---|---|
|
|
1053
|
+
| `images` | `list[str]` | Local paths. JPEG, PNG, or WebP only |
|
|
1054
|
+
|
|
1055
|
+
Returns the enroll response — `subject_id`, `status`, and a `faces` array whose
|
|
1056
|
+
`status` is set to `uploaded` or `failed` per image. Embeddings follow out of
|
|
1057
|
+
band; poll `tenants()` for `processed`.
|
|
1058
|
+
|
|
1059
|
+
Raises `ValidationError` for an empty list, more than 10 images, a missing file,
|
|
1060
|
+
or an unsupported image type — all checked before any request is sent.
|
|
1061
|
+
|
|
1062
|
+
#### `tenants() -> Dict`
|
|
1063
|
+
|
|
1064
|
+
Every subject and face on the account: `{"tenant_id", "subjects"}`, where each
|
|
1065
|
+
subject holds `face_id`, `name`, `status`, `embedding`, `image_url`, and `error`.
|
|
1066
|
+
|
|
1067
|
+
#### `faceSearch(image, limit=50, timeout=120.0) -> Dict`
|
|
1068
|
+
|
|
1069
|
+
Rank enrolled faces against a query photo, posted as `multipart/form-data`.
|
|
1070
|
+
|
|
1071
|
+
| Parameter | Type | Default | Description |
|
|
1072
|
+
|---|---|---|---|
|
|
1073
|
+
| `image` | `str` | — | Local path to the query photo |
|
|
1074
|
+
| `limit` | `int` | `50` | Matches to return, clamped to 1–50 |
|
|
1075
|
+
| `timeout` | `float` | `120.0` | Seconds — embedding takes longer than a plain request |
|
|
1076
|
+
|
|
1077
|
+
Returns `{"tenant_id", "count", "results"}` with results ordered from highest
|
|
1078
|
+
similarity down. Only faces with `status: "processed"` are searched.
|
|
1079
|
+
|
|
963
1080
|
#### `get_media(jobid) -> Dict`
|
|
964
1081
|
|
|
965
1082
|
`GET /api/v1/jobs/{jobid}` — fetch the current state of a media record.
|
|
@@ -5,6 +5,7 @@ src/authenta/__init__.py
|
|
|
5
5
|
src/authenta/async_authenta_client.py
|
|
6
6
|
src/authenta/authenta_client.py
|
|
7
7
|
src/authenta/authenta_exceptions.py
|
|
8
|
+
src/authenta/face_auth.py
|
|
8
9
|
src/authenta/visualization.py
|
|
9
10
|
src/authentasdk.egg-info/PKG-INFO
|
|
10
11
|
src/authentasdk.egg-info/SOURCES.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|