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.
@@ -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
@@ -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
@@ -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)