veri-sdk 0.1.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.
- veri_sdk-0.1.1/.gitignore +38 -0
- veri_sdk-0.1.1/PKG-INFO +220 -0
- veri_sdk-0.1.1/README.md +188 -0
- veri_sdk-0.1.1/pyproject.toml +82 -0
- veri_sdk-0.1.1/src/veri/__init__.py +36 -0
- veri_sdk-0.1.1/src/veri/client.py +468 -0
- veri_sdk-0.1.1/src/veri/errors.py +98 -0
- veri_sdk-0.1.1/src/veri/py.typed +17 -0
- veri_sdk-0.1.1/src/veri/types.py +60 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
*.pt
|
|
2
|
+
*.arrow
|
|
3
|
+
venv/
|
|
4
|
+
data/raw/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
*.pyd
|
|
9
|
+
.Python
|
|
10
|
+
.env
|
|
11
|
+
.file_locks/
|
|
12
|
+
.DS_Store
|
|
13
|
+
|
|
14
|
+
# Node
|
|
15
|
+
node_modules/
|
|
16
|
+
dist/
|
|
17
|
+
|
|
18
|
+
# Large model weights (download separately)
|
|
19
|
+
testing/models/CNNDetection/weights/*.pth
|
|
20
|
+
testing/models/FIRE/ckpt/*.pth
|
|
21
|
+
# Keep small weights that are under GitHub's limit
|
|
22
|
+
!testing/models/UniversalFakeDetect/pretrained_weights/fc_weights.pth
|
|
23
|
+
!testing/models/NPR-DeepfakeDetection/NPR.pth
|
|
24
|
+
!testing/models/NPR-DeepfakeDetection/model_epoch_last_3090.pth
|
|
25
|
+
|
|
26
|
+
# Terraform secrets
|
|
27
|
+
infrastructure/terraform/secrets.tfvars
|
|
28
|
+
terraform/environments/*/secrets.tfvars
|
|
29
|
+
*.tfstate
|
|
30
|
+
*.tfstate.backup
|
|
31
|
+
.terraform/
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Test dataset (large, don't commit)
|
|
37
|
+
notebooks/genAIdetect/data/test_dataset/
|
|
38
|
+
infrastructure/github-oidc-wrapper/config.sh
|
veri_sdk-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veri-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Official Python SDK for Veri AI Deepfake Detection API
|
|
5
|
+
Project-URL: Homepage, https://veri.studio
|
|
6
|
+
Project-URL: Documentation, https://docs.veri.studio
|
|
7
|
+
Author-email: Veri Team <support@veri.studio>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: ai,api,deepfake,detection,fake,image,machine-learning,sdk,veri
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.24.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# veri-sdk
|
|
34
|
+
|
|
35
|
+
Official Python SDK for the Veri AI Deepfake Detection API.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install veri-sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from veri import VeriClient
|
|
47
|
+
|
|
48
|
+
# Create a client with your API key
|
|
49
|
+
client = VeriClient(api_key="your-api-key-here")
|
|
50
|
+
|
|
51
|
+
# Detect an image
|
|
52
|
+
with open("image.jpg", "rb") as f:
|
|
53
|
+
result = client.detect(f)
|
|
54
|
+
|
|
55
|
+
print(f"Prediction: {result.prediction}")
|
|
56
|
+
print(f"Is AI-generated: {result.is_fake}")
|
|
57
|
+
print(f"Confidence: {result.confidence:.1%}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage Examples
|
|
61
|
+
|
|
62
|
+
### Detect from File Path
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from veri import VeriClient
|
|
67
|
+
|
|
68
|
+
client = VeriClient(api_key="your-api-key")
|
|
69
|
+
|
|
70
|
+
result = client.detect(Path("suspicious-image.jpg"))
|
|
71
|
+
|
|
72
|
+
if result.is_fake:
|
|
73
|
+
print("This image appears to be AI-generated")
|
|
74
|
+
print(f"Confidence: {result.confidence:.1%}")
|
|
75
|
+
print(f"Verdict: {result.verdict}")
|
|
76
|
+
else:
|
|
77
|
+
print("This image appears to be authentic")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Detect from Bytes
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import requests
|
|
84
|
+
from veri import VeriClient
|
|
85
|
+
|
|
86
|
+
client = VeriClient(api_key="your-api-key")
|
|
87
|
+
|
|
88
|
+
# Download image and detect
|
|
89
|
+
response = requests.get("https://example.com/image.jpg")
|
|
90
|
+
result = client.detect(response.content)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Detect from URL
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
result = client.detect_url("https://example.com/image.jpg")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### With Detection Options
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from veri import VeriClient, DetectionOptions
|
|
103
|
+
|
|
104
|
+
client = VeriClient(api_key="your-api-key")
|
|
105
|
+
|
|
106
|
+
options = DetectionOptions(
|
|
107
|
+
threshold=0.6,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
result = client.detect(image_bytes, options=options)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Get Profile
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
profile = client.get_profile()
|
|
117
|
+
print(f"User ID: {profile['userId']}")
|
|
118
|
+
print(f"Credits: {profile['credits']}")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Async Client
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import asyncio
|
|
125
|
+
from veri import AsyncVeriClient
|
|
126
|
+
|
|
127
|
+
async def main():
|
|
128
|
+
async with AsyncVeriClient(api_key="your-api-key") as client:
|
|
129
|
+
# Run multiple detections concurrently
|
|
130
|
+
tasks = [
|
|
131
|
+
client.detect(image1_bytes),
|
|
132
|
+
client.detect(image2_bytes),
|
|
133
|
+
client.detect(image3_bytes),
|
|
134
|
+
]
|
|
135
|
+
results = await asyncio.gather(*tasks)
|
|
136
|
+
|
|
137
|
+
for i, result in enumerate(results):
|
|
138
|
+
print(f"Image {i+1}: {'FAKE' if result.is_fake else 'REAL'}")
|
|
139
|
+
|
|
140
|
+
asyncio.run(main())
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Error Handling
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from veri import (
|
|
147
|
+
VeriClient,
|
|
148
|
+
VeriAPIError,
|
|
149
|
+
VeriValidationError,
|
|
150
|
+
VeriRateLimitError,
|
|
151
|
+
VeriTimeoutError,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
client = VeriClient(api_key="your-api-key")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = client.detect(image_bytes)
|
|
158
|
+
except VeriRateLimitError as e:
|
|
159
|
+
print(f"Rate limited. Retry after {e.retry_after} seconds")
|
|
160
|
+
except VeriTimeoutError as e:
|
|
161
|
+
print(f"Request timed out after {e.timeout_ms}ms")
|
|
162
|
+
except VeriAPIError as e:
|
|
163
|
+
print(f"API Error: {e.message} (code: {e.code})")
|
|
164
|
+
print(f"Request ID: {e.request_id}")
|
|
165
|
+
except VeriValidationError as e:
|
|
166
|
+
print(f"Validation Error: {e.message} (field: {e.field})")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Configuration
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
client = VeriClient(
|
|
173
|
+
api_key="your-api-key",
|
|
174
|
+
base_url="https://api.veri.studio/v1", # Custom API URL
|
|
175
|
+
timeout=30.0, # Request timeout (seconds)
|
|
176
|
+
max_retries=3, # Retry attempts
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Context Manager
|
|
181
|
+
|
|
182
|
+
Both sync and async clients support context managers:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Sync
|
|
186
|
+
with VeriClient(api_key="your-api-key") as client:
|
|
187
|
+
result = client.detect(image_bytes)
|
|
188
|
+
|
|
189
|
+
# Async
|
|
190
|
+
async with AsyncVeriClient(api_key="your-api-key") as client:
|
|
191
|
+
result = await client.detect(image_bytes)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Model
|
|
195
|
+
|
|
196
|
+
| Model | Description |
|
|
197
|
+
|-------|-------------|
|
|
198
|
+
| `veri_face` | DenseNet-121 + MoE face forgery detector |
|
|
199
|
+
|
|
200
|
+
## Type Hints
|
|
201
|
+
|
|
202
|
+
This SDK is fully typed. Import types for your IDE:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from veri import (
|
|
206
|
+
DetectionResult,
|
|
207
|
+
DetectionOptions,
|
|
208
|
+
ModelResult,
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Requirements
|
|
213
|
+
|
|
214
|
+
- Python 3.10+
|
|
215
|
+
- httpx
|
|
216
|
+
- pydantic
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
veri_sdk-0.1.1/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# veri-sdk
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the Veri AI Deepfake Detection API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install veri-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from veri import VeriClient
|
|
15
|
+
|
|
16
|
+
# Create a client with your API key
|
|
17
|
+
client = VeriClient(api_key="your-api-key-here")
|
|
18
|
+
|
|
19
|
+
# Detect an image
|
|
20
|
+
with open("image.jpg", "rb") as f:
|
|
21
|
+
result = client.detect(f)
|
|
22
|
+
|
|
23
|
+
print(f"Prediction: {result.prediction}")
|
|
24
|
+
print(f"Is AI-generated: {result.is_fake}")
|
|
25
|
+
print(f"Confidence: {result.confidence:.1%}")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage Examples
|
|
29
|
+
|
|
30
|
+
### Detect from File Path
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from veri import VeriClient
|
|
35
|
+
|
|
36
|
+
client = VeriClient(api_key="your-api-key")
|
|
37
|
+
|
|
38
|
+
result = client.detect(Path("suspicious-image.jpg"))
|
|
39
|
+
|
|
40
|
+
if result.is_fake:
|
|
41
|
+
print("This image appears to be AI-generated")
|
|
42
|
+
print(f"Confidence: {result.confidence:.1%}")
|
|
43
|
+
print(f"Verdict: {result.verdict}")
|
|
44
|
+
else:
|
|
45
|
+
print("This image appears to be authentic")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Detect from Bytes
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import requests
|
|
52
|
+
from veri import VeriClient
|
|
53
|
+
|
|
54
|
+
client = VeriClient(api_key="your-api-key")
|
|
55
|
+
|
|
56
|
+
# Download image and detect
|
|
57
|
+
response = requests.get("https://example.com/image.jpg")
|
|
58
|
+
result = client.detect(response.content)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Detect from URL
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
result = client.detect_url("https://example.com/image.jpg")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### With Detection Options
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from veri import VeriClient, DetectionOptions
|
|
71
|
+
|
|
72
|
+
client = VeriClient(api_key="your-api-key")
|
|
73
|
+
|
|
74
|
+
options = DetectionOptions(
|
|
75
|
+
threshold=0.6,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
result = client.detect(image_bytes, options=options)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Get Profile
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
profile = client.get_profile()
|
|
85
|
+
print(f"User ID: {profile['userId']}")
|
|
86
|
+
print(f"Credits: {profile['credits']}")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Async Client
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from veri import AsyncVeriClient
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
async with AsyncVeriClient(api_key="your-api-key") as client:
|
|
97
|
+
# Run multiple detections concurrently
|
|
98
|
+
tasks = [
|
|
99
|
+
client.detect(image1_bytes),
|
|
100
|
+
client.detect(image2_bytes),
|
|
101
|
+
client.detect(image3_bytes),
|
|
102
|
+
]
|
|
103
|
+
results = await asyncio.gather(*tasks)
|
|
104
|
+
|
|
105
|
+
for i, result in enumerate(results):
|
|
106
|
+
print(f"Image {i+1}: {'FAKE' if result.is_fake else 'REAL'}")
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Error Handling
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from veri import (
|
|
115
|
+
VeriClient,
|
|
116
|
+
VeriAPIError,
|
|
117
|
+
VeriValidationError,
|
|
118
|
+
VeriRateLimitError,
|
|
119
|
+
VeriTimeoutError,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
client = VeriClient(api_key="your-api-key")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = client.detect(image_bytes)
|
|
126
|
+
except VeriRateLimitError as e:
|
|
127
|
+
print(f"Rate limited. Retry after {e.retry_after} seconds")
|
|
128
|
+
except VeriTimeoutError as e:
|
|
129
|
+
print(f"Request timed out after {e.timeout_ms}ms")
|
|
130
|
+
except VeriAPIError as e:
|
|
131
|
+
print(f"API Error: {e.message} (code: {e.code})")
|
|
132
|
+
print(f"Request ID: {e.request_id}")
|
|
133
|
+
except VeriValidationError as e:
|
|
134
|
+
print(f"Validation Error: {e.message} (field: {e.field})")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
client = VeriClient(
|
|
141
|
+
api_key="your-api-key",
|
|
142
|
+
base_url="https://api.veri.studio/v1", # Custom API URL
|
|
143
|
+
timeout=30.0, # Request timeout (seconds)
|
|
144
|
+
max_retries=3, # Retry attempts
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Context Manager
|
|
149
|
+
|
|
150
|
+
Both sync and async clients support context managers:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# Sync
|
|
154
|
+
with VeriClient(api_key="your-api-key") as client:
|
|
155
|
+
result = client.detect(image_bytes)
|
|
156
|
+
|
|
157
|
+
# Async
|
|
158
|
+
async with AsyncVeriClient(api_key="your-api-key") as client:
|
|
159
|
+
result = await client.detect(image_bytes)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Model
|
|
163
|
+
|
|
164
|
+
| Model | Description |
|
|
165
|
+
|-------|-------------|
|
|
166
|
+
| `veri_face` | DenseNet-121 + MoE face forgery detector |
|
|
167
|
+
|
|
168
|
+
## Type Hints
|
|
169
|
+
|
|
170
|
+
This SDK is fully typed. Import types for your IDE:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from veri import (
|
|
174
|
+
DetectionResult,
|
|
175
|
+
DetectionOptions,
|
|
176
|
+
ModelResult,
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Requirements
|
|
181
|
+
|
|
182
|
+
- Python 3.10+
|
|
183
|
+
- httpx
|
|
184
|
+
- pydantic
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "veri-sdk"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Official Python SDK for Veri AI Deepfake Detection API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Veri Team", email = "support@veri.studio" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"veri",
|
|
17
|
+
"deepfake",
|
|
18
|
+
"detection",
|
|
19
|
+
"ai",
|
|
20
|
+
"image",
|
|
21
|
+
"fake",
|
|
22
|
+
"sdk",
|
|
23
|
+
"api",
|
|
24
|
+
"machine-learning"
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
37
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
38
|
+
"Typing :: Typed",
|
|
39
|
+
]
|
|
40
|
+
dependencies = [
|
|
41
|
+
"httpx>=0.24.0",
|
|
42
|
+
"pydantic>=2.0.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=7.0.0",
|
|
48
|
+
"pytest-asyncio>=0.21.0",
|
|
49
|
+
"pytest-cov>=4.0.0",
|
|
50
|
+
"mypy>=1.0.0",
|
|
51
|
+
"ruff>=0.1.0",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://veri.studio"
|
|
56
|
+
Documentation = "https://docs.veri.studio"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.sdist]
|
|
59
|
+
include = ["src/veri"]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/veri"]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
65
|
+
"src" = ""
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
target-version = "py310"
|
|
69
|
+
line-length = 100
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
python_version = "3.10"
|
|
76
|
+
strict = true
|
|
77
|
+
warn_return_any = true
|
|
78
|
+
warn_unused_ignores = true
|
|
79
|
+
|
|
80
|
+
[tool.pytest.ini_options]
|
|
81
|
+
asyncio_mode = "auto"
|
|
82
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Veri SDK - Python client for Veri Deepfake Detection API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from veri.client import AsyncVeriClient, VeriClient
|
|
6
|
+
from veri.errors import (
|
|
7
|
+
VeriAPIError,
|
|
8
|
+
VeriError,
|
|
9
|
+
VeriInsufficientCreditsError,
|
|
10
|
+
VeriRateLimitError,
|
|
11
|
+
VeriTimeoutError,
|
|
12
|
+
VeriValidationError,
|
|
13
|
+
)
|
|
14
|
+
from veri.types import (
|
|
15
|
+
DetectionOptions,
|
|
16
|
+
DetectionResult,
|
|
17
|
+
ModelResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.1"
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Clients
|
|
23
|
+
"VeriClient",
|
|
24
|
+
"AsyncVeriClient",
|
|
25
|
+
# Types
|
|
26
|
+
"DetectionResult",
|
|
27
|
+
"DetectionOptions",
|
|
28
|
+
"ModelResult",
|
|
29
|
+
# Errors
|
|
30
|
+
"VeriError",
|
|
31
|
+
"VeriAPIError",
|
|
32
|
+
"VeriValidationError",
|
|
33
|
+
"VeriTimeoutError",
|
|
34
|
+
"VeriRateLimitError",
|
|
35
|
+
"VeriInsufficientCreditsError",
|
|
36
|
+
]
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Veri API Client implementations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, BinaryIO, Union
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from veri.errors import (
|
|
15
|
+
VeriAPIError,
|
|
16
|
+
VeriInsufficientCreditsError,
|
|
17
|
+
VeriRateLimitError,
|
|
18
|
+
VeriTimeoutError,
|
|
19
|
+
VeriValidationError,
|
|
20
|
+
)
|
|
21
|
+
from veri.types import (
|
|
22
|
+
DetectionOptions,
|
|
23
|
+
DetectionResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://api.veri.studio/v1"
|
|
27
|
+
DEFAULT_TIMEOUT = 30.0
|
|
28
|
+
DEFAULT_MAX_RETRIES = 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ImageInput = Union[bytes, str, Path, BinaryIO] # noqa: UP007 - Union needed for mypy type alias
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VeriClient:
|
|
35
|
+
"""
|
|
36
|
+
Synchronous Veri API client.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> from veri import VeriClient
|
|
40
|
+
>>> client = VeriClient(api_key="your-api-key")
|
|
41
|
+
>>> result = client.detect(open("image.jpg", "rb"))
|
|
42
|
+
>>> print(f"Is fake: {result.is_fake} ({result.confidence:.1%})")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
api_key: str,
|
|
48
|
+
*,
|
|
49
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
50
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
51
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Initialize the Veri client.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
api_key: Your Veri API key
|
|
58
|
+
base_url: Base URL for the API
|
|
59
|
+
timeout: Request timeout in seconds
|
|
60
|
+
max_retries: Number of retry attempts for failed requests
|
|
61
|
+
"""
|
|
62
|
+
if not api_key:
|
|
63
|
+
raise VeriValidationError("API key is required", "api_key")
|
|
64
|
+
|
|
65
|
+
self.api_key = api_key
|
|
66
|
+
self.base_url = base_url.rstrip("/")
|
|
67
|
+
self.timeout = timeout
|
|
68
|
+
self.max_retries = max_retries
|
|
69
|
+
|
|
70
|
+
self._client = httpx.Client(
|
|
71
|
+
base_url=self.base_url,
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
headers={
|
|
74
|
+
"X-API-Key": api_key,
|
|
75
|
+
"User-Agent": "veri-sdk-python/0.1.0",
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> VeriClient:
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(self, *args: Any) -> None:
|
|
84
|
+
self.close()
|
|
85
|
+
|
|
86
|
+
def close(self) -> None:
|
|
87
|
+
"""Close the client and release resources."""
|
|
88
|
+
self._client.close()
|
|
89
|
+
|
|
90
|
+
def detect(
|
|
91
|
+
self,
|
|
92
|
+
image: ImageInput,
|
|
93
|
+
*,
|
|
94
|
+
options: DetectionOptions | None = None,
|
|
95
|
+
) -> DetectionResult:
|
|
96
|
+
"""
|
|
97
|
+
Detect whether an image is AI-generated.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
image: Image data as bytes, file path, file object, or base64 string
|
|
101
|
+
options: Detection options
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Detection result with confidence scores
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> # From file path
|
|
108
|
+
>>> result = client.detect(Path("image.jpg"))
|
|
109
|
+
>>>
|
|
110
|
+
>>> # From bytes
|
|
111
|
+
>>> result = client.detect(image_bytes)
|
|
112
|
+
>>>
|
|
113
|
+
>>> # With options
|
|
114
|
+
>>> result = client.detect(
|
|
115
|
+
... image_bytes,
|
|
116
|
+
... options=DetectionOptions(models=["veri_face"])
|
|
117
|
+
... )
|
|
118
|
+
"""
|
|
119
|
+
image_b64 = self._image_to_base64(image)
|
|
120
|
+
payload: dict[str, Any] = {"image": image_b64}
|
|
121
|
+
|
|
122
|
+
if options:
|
|
123
|
+
payload.update(options.model_dump(by_alias=True, exclude_none=True))
|
|
124
|
+
|
|
125
|
+
response = self._request("POST", "/api/detect", json=payload)
|
|
126
|
+
return DetectionResult.model_validate(response)
|
|
127
|
+
|
|
128
|
+
def detect_url(
|
|
129
|
+
self,
|
|
130
|
+
url: str,
|
|
131
|
+
*,
|
|
132
|
+
options: DetectionOptions | None = None,
|
|
133
|
+
) -> DetectionResult:
|
|
134
|
+
"""
|
|
135
|
+
Detect an image from a URL.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
url: URL of the image to analyze
|
|
139
|
+
options: Detection options
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Detection result
|
|
143
|
+
"""
|
|
144
|
+
if not url.startswith(("http://", "https://")):
|
|
145
|
+
raise VeriValidationError("Invalid URL format", "url")
|
|
146
|
+
|
|
147
|
+
payload: dict[str, Any] = {"url": url}
|
|
148
|
+
if options:
|
|
149
|
+
payload.update(options.model_dump(by_alias=True, exclude_none=True))
|
|
150
|
+
|
|
151
|
+
response = self._request("POST", "/api/detect/url", json=payload)
|
|
152
|
+
return DetectionResult.model_validate(response)
|
|
153
|
+
|
|
154
|
+
def get_profile(self) -> dict[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
Get the authenticated user's profile.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
User profile data including userId, email, credits, etc.
|
|
160
|
+
"""
|
|
161
|
+
return self._request("GET", "/api/user/profile")
|
|
162
|
+
|
|
163
|
+
# ============ Private methods ============
|
|
164
|
+
|
|
165
|
+
def _request(
|
|
166
|
+
self,
|
|
167
|
+
method: str,
|
|
168
|
+
endpoint: str,
|
|
169
|
+
**kwargs: Any,
|
|
170
|
+
) -> Any:
|
|
171
|
+
"""Make an HTTP request with retries."""
|
|
172
|
+
last_error: Exception | None = None
|
|
173
|
+
|
|
174
|
+
for attempt in range(self.max_retries):
|
|
175
|
+
try:
|
|
176
|
+
response = self._client.request(method, endpoint, **kwargs)
|
|
177
|
+
|
|
178
|
+
if response.status_code == 429:
|
|
179
|
+
retry_after = int(response.headers.get("retry-after", "60"))
|
|
180
|
+
request_id = response.headers.get("x-request-id")
|
|
181
|
+
raise VeriRateLimitError(
|
|
182
|
+
"Rate limit exceeded",
|
|
183
|
+
retry_after,
|
|
184
|
+
request_id,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if response.status_code == 402:
|
|
188
|
+
error_data = self._safe_json(response)
|
|
189
|
+
request_id = response.headers.get("x-request-id")
|
|
190
|
+
raise VeriInsufficientCreditsError(
|
|
191
|
+
error_data.get("message", "Insufficient credits"),
|
|
192
|
+
request_id,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if not response.is_success:
|
|
196
|
+
error_data = self._safe_json(response)
|
|
197
|
+
request_id = response.headers.get("x-request-id")
|
|
198
|
+
raise VeriAPIError(
|
|
199
|
+
error_data.get("message", f"Request failed: {response.status_code}"),
|
|
200
|
+
response.status_code,
|
|
201
|
+
error_data.get("code", "UNKNOWN_ERROR"),
|
|
202
|
+
request_id,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return self._safe_json(response)
|
|
206
|
+
|
|
207
|
+
except httpx.TimeoutException as e:
|
|
208
|
+
raise VeriTimeoutError(
|
|
209
|
+
f"Request timed out after {self.timeout}s",
|
|
210
|
+
int(self.timeout * 1000),
|
|
211
|
+
) from e
|
|
212
|
+
|
|
213
|
+
except VeriAPIError as e:
|
|
214
|
+
if not e.is_retryable:
|
|
215
|
+
raise
|
|
216
|
+
last_error = e
|
|
217
|
+
|
|
218
|
+
except httpx.HTTPError as e:
|
|
219
|
+
last_error = e
|
|
220
|
+
|
|
221
|
+
# Exponential backoff
|
|
222
|
+
if attempt < self.max_retries - 1:
|
|
223
|
+
time.sleep(2**attempt)
|
|
224
|
+
|
|
225
|
+
if last_error:
|
|
226
|
+
raise last_error
|
|
227
|
+
raise VeriAPIError("Request failed after retries", 500, "RETRY_EXHAUSTED")
|
|
228
|
+
|
|
229
|
+
def _safe_json(self, response: httpx.Response) -> dict[str, Any]:
|
|
230
|
+
"""Safely parse JSON response, returning empty dict on failure."""
|
|
231
|
+
if not response.content:
|
|
232
|
+
return {}
|
|
233
|
+
try:
|
|
234
|
+
return response.json()
|
|
235
|
+
except Exception:
|
|
236
|
+
return {"message": response.text[:500] if response.text else "Unknown error"}
|
|
237
|
+
|
|
238
|
+
def _image_to_base64(self, image: ImageInput) -> str:
|
|
239
|
+
"""Convert various image inputs to base64 string."""
|
|
240
|
+
# Already base64 string
|
|
241
|
+
if isinstance(image, str):
|
|
242
|
+
if image.startswith("data:"):
|
|
243
|
+
return image.split(",", 1)[1]
|
|
244
|
+
# Assume it's already base64 or a file path
|
|
245
|
+
if Path(image).exists():
|
|
246
|
+
with open(image, "rb") as f:
|
|
247
|
+
return base64.b64encode(f.read()).decode("utf-8")
|
|
248
|
+
return image
|
|
249
|
+
|
|
250
|
+
# Path object
|
|
251
|
+
if isinstance(image, Path):
|
|
252
|
+
with open(image, "rb") as f:
|
|
253
|
+
return base64.b64encode(f.read()).decode("utf-8")
|
|
254
|
+
|
|
255
|
+
# Bytes
|
|
256
|
+
if isinstance(image, bytes):
|
|
257
|
+
return base64.b64encode(image).decode("utf-8")
|
|
258
|
+
|
|
259
|
+
# File-like object
|
|
260
|
+
if hasattr(image, "read"):
|
|
261
|
+
content = image.read()
|
|
262
|
+
if isinstance(content, str):
|
|
263
|
+
content = content.encode("utf-8")
|
|
264
|
+
return base64.b64encode(content).decode("utf-8")
|
|
265
|
+
|
|
266
|
+
raise VeriValidationError(
|
|
267
|
+
"Invalid image format. Expected bytes, Path, file object, or base64 string",
|
|
268
|
+
"image",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class AsyncVeriClient:
|
|
273
|
+
"""
|
|
274
|
+
Asynchronous Veri API client.
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
>>> import asyncio
|
|
278
|
+
>>> from veri import AsyncVeriClient
|
|
279
|
+
>>>
|
|
280
|
+
>>> async def main():
|
|
281
|
+
... async with AsyncVeriClient(api_key="your-api-key") as client:
|
|
282
|
+
... result = await client.detect(image_bytes)
|
|
283
|
+
... print(f"Is fake: {result.is_fake}")
|
|
284
|
+
>>>
|
|
285
|
+
>>> asyncio.run(main())
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
api_key: str,
|
|
291
|
+
*,
|
|
292
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
293
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
294
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
295
|
+
) -> None:
|
|
296
|
+
if not api_key:
|
|
297
|
+
raise VeriValidationError("API key is required", "api_key")
|
|
298
|
+
|
|
299
|
+
self.api_key = api_key
|
|
300
|
+
self.base_url = base_url.rstrip("/")
|
|
301
|
+
self.timeout = timeout
|
|
302
|
+
self.max_retries = max_retries
|
|
303
|
+
|
|
304
|
+
self._client = httpx.AsyncClient(
|
|
305
|
+
base_url=self.base_url,
|
|
306
|
+
timeout=timeout,
|
|
307
|
+
headers={
|
|
308
|
+
"X-API-Key": api_key,
|
|
309
|
+
"User-Agent": "veri-sdk-python/0.1.0",
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
async def __aenter__(self) -> AsyncVeriClient:
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
318
|
+
await self.close()
|
|
319
|
+
|
|
320
|
+
async def close(self) -> None:
|
|
321
|
+
"""Close the client and release resources."""
|
|
322
|
+
await self._client.aclose()
|
|
323
|
+
|
|
324
|
+
async def detect(
|
|
325
|
+
self,
|
|
326
|
+
image: ImageInput,
|
|
327
|
+
*,
|
|
328
|
+
options: DetectionOptions | None = None,
|
|
329
|
+
) -> DetectionResult:
|
|
330
|
+
"""Detect whether an image is AI-generated (async version)."""
|
|
331
|
+
image_b64 = self._image_to_base64(image)
|
|
332
|
+
payload: dict[str, Any] = {"image": image_b64}
|
|
333
|
+
|
|
334
|
+
if options:
|
|
335
|
+
payload.update(options.model_dump(by_alias=True, exclude_none=True))
|
|
336
|
+
|
|
337
|
+
response = await self._request("POST", "/api/detect", json=payload)
|
|
338
|
+
return DetectionResult.model_validate(response)
|
|
339
|
+
|
|
340
|
+
async def detect_url(
|
|
341
|
+
self,
|
|
342
|
+
url: str,
|
|
343
|
+
*,
|
|
344
|
+
options: DetectionOptions | None = None,
|
|
345
|
+
) -> DetectionResult:
|
|
346
|
+
"""Detect an image from a URL (async version)."""
|
|
347
|
+
if not url.startswith(("http://", "https://")):
|
|
348
|
+
raise VeriValidationError("Invalid URL format", "url")
|
|
349
|
+
|
|
350
|
+
payload: dict[str, Any] = {"url": url}
|
|
351
|
+
if options:
|
|
352
|
+
payload.update(options.model_dump(by_alias=True, exclude_none=True))
|
|
353
|
+
|
|
354
|
+
response = await self._request("POST", "/api/detect/url", json=payload)
|
|
355
|
+
return DetectionResult.model_validate(response)
|
|
356
|
+
|
|
357
|
+
async def get_profile(self) -> dict[str, Any]:
|
|
358
|
+
"""Get the authenticated user's profile (async version)."""
|
|
359
|
+
return await self._request("GET", "/api/user/profile")
|
|
360
|
+
|
|
361
|
+
# ============ Private methods ============
|
|
362
|
+
|
|
363
|
+
async def _request(
|
|
364
|
+
self,
|
|
365
|
+
method: str,
|
|
366
|
+
endpoint: str,
|
|
367
|
+
**kwargs: Any,
|
|
368
|
+
) -> Any:
|
|
369
|
+
"""Make an async HTTP request with retries."""
|
|
370
|
+
import asyncio
|
|
371
|
+
|
|
372
|
+
last_error: Exception | None = None
|
|
373
|
+
|
|
374
|
+
for attempt in range(self.max_retries):
|
|
375
|
+
try:
|
|
376
|
+
response = await self._client.request(method, endpoint, **kwargs)
|
|
377
|
+
|
|
378
|
+
if response.status_code == 429:
|
|
379
|
+
retry_after = int(response.headers.get("retry-after", "60"))
|
|
380
|
+
request_id = response.headers.get("x-request-id")
|
|
381
|
+
raise VeriRateLimitError(
|
|
382
|
+
"Rate limit exceeded",
|
|
383
|
+
retry_after,
|
|
384
|
+
request_id,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if response.status_code == 402:
|
|
388
|
+
error_data = self._safe_json(response)
|
|
389
|
+
request_id = response.headers.get("x-request-id")
|
|
390
|
+
raise VeriInsufficientCreditsError(
|
|
391
|
+
error_data.get("message", "Insufficient credits"),
|
|
392
|
+
request_id,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if not response.is_success:
|
|
396
|
+
error_data = self._safe_json(response)
|
|
397
|
+
request_id = response.headers.get("x-request-id")
|
|
398
|
+
raise VeriAPIError(
|
|
399
|
+
error_data.get("message", f"Request failed: {response.status_code}"),
|
|
400
|
+
response.status_code,
|
|
401
|
+
error_data.get("code", "UNKNOWN_ERROR"),
|
|
402
|
+
request_id,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return self._safe_json(response)
|
|
406
|
+
|
|
407
|
+
except httpx.TimeoutException as e:
|
|
408
|
+
raise VeriTimeoutError(
|
|
409
|
+
f"Request timed out after {self.timeout}s",
|
|
410
|
+
int(self.timeout * 1000),
|
|
411
|
+
) from e
|
|
412
|
+
|
|
413
|
+
except VeriAPIError as e:
|
|
414
|
+
if not e.is_retryable:
|
|
415
|
+
raise
|
|
416
|
+
last_error = e
|
|
417
|
+
|
|
418
|
+
except httpx.HTTPError as e:
|
|
419
|
+
last_error = e
|
|
420
|
+
|
|
421
|
+
# Exponential backoff
|
|
422
|
+
if attempt < self.max_retries - 1:
|
|
423
|
+
await asyncio.sleep(2**attempt)
|
|
424
|
+
|
|
425
|
+
if last_error:
|
|
426
|
+
raise last_error
|
|
427
|
+
raise VeriAPIError("Request failed after retries", 500, "RETRY_EXHAUSTED")
|
|
428
|
+
|
|
429
|
+
def _safe_json(self, response: httpx.Response) -> dict[str, Any]:
|
|
430
|
+
"""Safely parse JSON response, returning empty dict on failure."""
|
|
431
|
+
if not response.content:
|
|
432
|
+
return {}
|
|
433
|
+
try:
|
|
434
|
+
return response.json()
|
|
435
|
+
except Exception:
|
|
436
|
+
return {"message": response.text[:500] if response.text else "Unknown error"}
|
|
437
|
+
|
|
438
|
+
def _image_to_base64(self, image: ImageInput) -> str:
|
|
439
|
+
"""Convert various image inputs to base64 string."""
|
|
440
|
+
# Already base64 string
|
|
441
|
+
if isinstance(image, str):
|
|
442
|
+
if image.startswith("data:"):
|
|
443
|
+
return image.split(",", 1)[1]
|
|
444
|
+
if Path(image).exists():
|
|
445
|
+
with open(image, "rb") as f:
|
|
446
|
+
return base64.b64encode(f.read()).decode("utf-8")
|
|
447
|
+
return image
|
|
448
|
+
|
|
449
|
+
# Path object
|
|
450
|
+
if isinstance(image, Path):
|
|
451
|
+
with open(image, "rb") as f:
|
|
452
|
+
return base64.b64encode(f.read()).decode("utf-8")
|
|
453
|
+
|
|
454
|
+
# Bytes
|
|
455
|
+
if isinstance(image, bytes):
|
|
456
|
+
return base64.b64encode(image).decode("utf-8")
|
|
457
|
+
|
|
458
|
+
# File-like object
|
|
459
|
+
if hasattr(image, "read"):
|
|
460
|
+
content = image.read()
|
|
461
|
+
if isinstance(content, str):
|
|
462
|
+
content = content.encode("utf-8")
|
|
463
|
+
return base64.b64encode(content).decode("utf-8")
|
|
464
|
+
|
|
465
|
+
raise VeriValidationError(
|
|
466
|
+
"Invalid image format. Expected bytes, Path, file object, or base64 string",
|
|
467
|
+
"image",
|
|
468
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exception classes for Veri SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VeriError(Exception):
|
|
9
|
+
"""Base exception for all Veri errors"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str) -> None:
|
|
12
|
+
self.message = message
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VeriAPIError(VeriError):
|
|
17
|
+
"""Exception raised when the API returns an error response"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str,
|
|
22
|
+
status_code: int,
|
|
23
|
+
code: str,
|
|
24
|
+
request_id: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.code = code
|
|
29
|
+
self.request_id = request_id
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_retryable(self) -> bool:
|
|
33
|
+
"""Whether the error is retryable"""
|
|
34
|
+
return (
|
|
35
|
+
self.status_code >= 500
|
|
36
|
+
or self.status_code == 429
|
|
37
|
+
or self.code in ("RATE_LIMITED", "SERVICE_UNAVAILABLE")
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
base = f"[{self.code}] {self.message} (HTTP {self.status_code})"
|
|
42
|
+
if self.request_id:
|
|
43
|
+
base += f" [Request ID: {self.request_id}]"
|
|
44
|
+
return base
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VeriValidationError(VeriError):
|
|
48
|
+
"""Exception raised when input validation fails"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, message: str, field: str | None = None) -> None:
|
|
51
|
+
super().__init__(message)
|
|
52
|
+
self.field = field
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
if self.field:
|
|
56
|
+
return f"Validation error on '{self.field}': {self.message}"
|
|
57
|
+
return f"Validation error: {self.message}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class VeriTimeoutError(VeriError):
|
|
61
|
+
"""Exception raised when a request times out"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str, timeout_ms: int) -> None:
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
self.timeout_ms = timeout_ms
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return f"Timeout after {self.timeout_ms}ms: {self.message}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class VeriRateLimitError(VeriAPIError):
|
|
72
|
+
"""Exception raised when rate limit is exceeded"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
message: str,
|
|
77
|
+
retry_after: int,
|
|
78
|
+
request_id: str | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__(message, 429, "RATE_LIMITED", request_id)
|
|
81
|
+
self.retry_after = retry_after
|
|
82
|
+
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
return f"Rate limited. Retry after {self.retry_after} seconds: {self.message}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class VeriInsufficientCreditsError(VeriAPIError):
|
|
88
|
+
"""Exception raised when user has insufficient credits (402 Payment Required)"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
message: str,
|
|
93
|
+
request_id: str | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
super().__init__(message, 402, "INSUFFICIENT_CREDITS", request_id)
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
return f"Insufficient credits: {self.message}"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Veri SDK - typed stub file
|
|
2
|
+
from veri.client import VeriClient as VeriClient, AsyncVeriClient as AsyncVeriClient
|
|
3
|
+
from veri.types import (
|
|
4
|
+
DetectionResult as DetectionResult,
|
|
5
|
+
DetectionOptions as DetectionOptions,
|
|
6
|
+
ModelResult as ModelResult,
|
|
7
|
+
)
|
|
8
|
+
from veri.errors import (
|
|
9
|
+
VeriError as VeriError,
|
|
10
|
+
VeriAPIError as VeriAPIError,
|
|
11
|
+
VeriValidationError as VeriValidationError,
|
|
12
|
+
VeriTimeoutError as VeriTimeoutError,
|
|
13
|
+
VeriRateLimitError as VeriRateLimitError,
|
|
14
|
+
VeriInsufficientCreditsError as VeriInsufficientCreditsError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__: str
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for Veri SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetectionModel(str, Enum):
|
|
13
|
+
"""Available detection models"""
|
|
14
|
+
|
|
15
|
+
VERI_FACE = "veri_face"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModelResult(BaseModel):
|
|
19
|
+
"""Individual model result from the detection pipeline"""
|
|
20
|
+
|
|
21
|
+
score: float | None = Field(default=None, description="Model score (0-1)")
|
|
22
|
+
status: str = Field(description="Model status (success/error)")
|
|
23
|
+
latency_ms: int | None = Field(default=None, description="Inference latency in ms")
|
|
24
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DetectionResult(BaseModel):
|
|
28
|
+
"""Complete detection result"""
|
|
29
|
+
|
|
30
|
+
# Original fields from the API
|
|
31
|
+
prediction: str = Field(description="Prediction: 'ai', 'real', or 'uncertain'")
|
|
32
|
+
confidence: float = Field(description="Overall confidence score (0-1)")
|
|
33
|
+
ensemble_score: float | None = Field(default=None, description="Weighted ensemble score")
|
|
34
|
+
verdict: str = Field(description="Verdict: 'ai_generated', 'uncertain', or 'likely_real'")
|
|
35
|
+
models_succeeded: int = Field(description="Number of models that succeeded")
|
|
36
|
+
models_total: int = Field(description="Total models invoked")
|
|
37
|
+
model_results: dict[str, ModelResult] = Field(description="Per-model results")
|
|
38
|
+
detection_latency_ms: int = Field(description="Total detection latency in ms")
|
|
39
|
+
|
|
40
|
+
# SDK-friendly fields
|
|
41
|
+
is_fake: bool | None = Field(
|
|
42
|
+
alias="isFake", default=None, description="Whether image is AI-generated"
|
|
43
|
+
)
|
|
44
|
+
processing_time_ms: int = Field(
|
|
45
|
+
alias="processingTimeMs", description="Total processing time in ms"
|
|
46
|
+
)
|
|
47
|
+
image_hash: str = Field(alias="imageHash", description="SHA-256 hash of image")
|
|
48
|
+
cached: bool = Field(description="Whether result was from cache")
|
|
49
|
+
timestamp: str = Field(description="Detection timestamp (ISO 8601)")
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DetectionOptions(BaseModel):
|
|
55
|
+
"""Options for detection request"""
|
|
56
|
+
|
|
57
|
+
models: list[str] | None = Field(default=None, description="Specific models to use")
|
|
58
|
+
threshold: float = Field(default=0.5, ge=0, le=1, description="Classification threshold")
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(populate_by_name=True)
|