raidxai 0.1.0__py3-none-any.whl
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.
- raidxai/__init__.py +78 -0
- raidxai/_core.py +125 -0
- raidxai/_generated/__init__.py +0 -0
- raidxai/_generated/models.py +460 -0
- raidxai/_poll.py +69 -0
- raidxai/_specs.py +186 -0
- raidxai/aio.py +108 -0
- raidxai/client.py +115 -0
- raidxai/errors.py +88 -0
- raidxai/files.py +37 -0
- raidxai/models.py +63 -0
- raidxai/resources/__init__.py +0 -0
- raidxai/resources/audio.py +80 -0
- raidxai/resources/fact_checking.py +135 -0
- raidxai/resources/images.py +61 -0
- raidxai/resources/video.py +141 -0
- raidxai-0.1.0.dist-info/METADATA +167 -0
- raidxai-0.1.0.dist-info/RECORD +20 -0
- raidxai-0.1.0.dist-info/WHEEL +4 -0
- raidxai-0.1.0.dist-info/licenses/LICENSE +7 -0
raidxai/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Official Python SDK for the Raid AI detection API.
|
|
2
|
+
|
|
3
|
+
Detect AI-generated and manipulated media (images, audio, video) and fact-check
|
|
4
|
+
media against the public record.
|
|
5
|
+
|
|
6
|
+
from raidxai import RaidClient, FileInput
|
|
7
|
+
|
|
8
|
+
raid = RaidClient(api_key="<your-api-key>")
|
|
9
|
+
res = raid.images.process(FileInput.from_path("photo.jpg"))
|
|
10
|
+
print(res.images[0].verdict)
|
|
11
|
+
|
|
12
|
+
Full reference: https://docs.raidxai.com
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from ._specs import PagedResult
|
|
18
|
+
from .aio import AsyncRaidClient
|
|
19
|
+
from .client import RaidClient
|
|
20
|
+
from .errors import RaidApiError, RaidTimeoutError
|
|
21
|
+
from .files import FileInput
|
|
22
|
+
from .models import (
|
|
23
|
+
Claim,
|
|
24
|
+
FactCheckJob,
|
|
25
|
+
FactCheckResult,
|
|
26
|
+
Generators,
|
|
27
|
+
ImageFace,
|
|
28
|
+
ImageFaceAnalysis,
|
|
29
|
+
ImageForensicsResponse,
|
|
30
|
+
ImageResult,
|
|
31
|
+
JobStatus,
|
|
32
|
+
JobSubmitResponse,
|
|
33
|
+
Modality,
|
|
34
|
+
ProvenanceItem,
|
|
35
|
+
ProvenanceUrl,
|
|
36
|
+
Verdict,
|
|
37
|
+
VideoJob,
|
|
38
|
+
VideoProvider,
|
|
39
|
+
VideoResult,
|
|
40
|
+
VideoSubmitResponse,
|
|
41
|
+
VoiceAnalysisResponse,
|
|
42
|
+
VoiceWorkflowType,
|
|
43
|
+
)
|
|
44
|
+
from .resources.audio import VoiceWorkflow
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"RaidClient",
|
|
50
|
+
"AsyncRaidClient",
|
|
51
|
+
"FileInput",
|
|
52
|
+
"PagedResult",
|
|
53
|
+
"RaidApiError",
|
|
54
|
+
"RaidTimeoutError",
|
|
55
|
+
"VoiceWorkflow",
|
|
56
|
+
# models
|
|
57
|
+
"Claim",
|
|
58
|
+
"FactCheckJob",
|
|
59
|
+
"FactCheckResult",
|
|
60
|
+
"Generators",
|
|
61
|
+
"ImageFace",
|
|
62
|
+
"ImageFaceAnalysis",
|
|
63
|
+
"ImageForensicsResponse",
|
|
64
|
+
"ImageResult",
|
|
65
|
+
"JobStatus",
|
|
66
|
+
"JobSubmitResponse",
|
|
67
|
+
"Modality",
|
|
68
|
+
"ProvenanceItem",
|
|
69
|
+
"ProvenanceUrl",
|
|
70
|
+
"Verdict",
|
|
71
|
+
"VideoJob",
|
|
72
|
+
"VideoProvider",
|
|
73
|
+
"VideoResult",
|
|
74
|
+
"VideoSubmitResponse",
|
|
75
|
+
"VoiceAnalysisResponse",
|
|
76
|
+
"VoiceWorkflowType",
|
|
77
|
+
"__version__",
|
|
78
|
+
]
|
raidxai/_core.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Transport-agnostic request plumbing shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
A :class:`RequestSpec` is a pure description of one HTTP call (built by the
|
|
4
|
+
functions in ``_specs.py``). The sync and async clients each know how to execute
|
|
5
|
+
a spec with retries; everything else — header/URL building, response parsing,
|
|
6
|
+
error mapping, backoff math — lives here so both paths behave identically.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json as _json
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from email.utils import parsedate_to_datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from .errors import RaidApiError, extract_error
|
|
20
|
+
|
|
21
|
+
# A multipart file part: (field_name, (filename, data, content_type)).
|
|
22
|
+
FilePart = tuple[str, tuple[str, bytes, str]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RequestSpec:
|
|
27
|
+
method: str
|
|
28
|
+
path: str
|
|
29
|
+
json: Any = None
|
|
30
|
+
data: dict[str, str] = field(default_factory=dict)
|
|
31
|
+
files: list[FilePart] = field(default_factory=list)
|
|
32
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
def httpx_kwargs(self) -> dict[str, Any]:
|
|
35
|
+
kwargs: dict[str, Any] = {}
|
|
36
|
+
if self.files:
|
|
37
|
+
kwargs["files"] = self.files
|
|
38
|
+
if self.data:
|
|
39
|
+
kwargs["data"] = self.data
|
|
40
|
+
elif self.json is not None:
|
|
41
|
+
kwargs["json"] = self.json
|
|
42
|
+
if self.params:
|
|
43
|
+
kwargs["params"] = {k: v for k, v in self.params.items() if v is not None}
|
|
44
|
+
return kwargs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_headers(api_key: str, auth_header: str) -> dict[str, str]:
|
|
48
|
+
headers = {"Accept": "application/json"}
|
|
49
|
+
if auth_header == "x-api-key":
|
|
50
|
+
headers["X-Api-Key"] = api_key
|
|
51
|
+
else:
|
|
52
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
53
|
+
return headers
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_retryable_status(status: int) -> bool:
|
|
57
|
+
return status == 429 or status >= 500
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def backoff_seconds(attempt: int) -> float:
|
|
61
|
+
"""0.5s, 1s, 2s, … (attempt is 1-based)."""
|
|
62
|
+
return 0.5 * (2 ** (attempt - 1))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def retry_after_seconds(response: httpx.Response) -> float | None:
|
|
66
|
+
"""Parse a ``Retry-After`` header — either delta-seconds or an HTTP-date.
|
|
67
|
+
|
|
68
|
+
Returns ``None`` when absent/unparseable; ``0.0`` is a valid "retry now" value
|
|
69
|
+
and is preserved (callers must not treat it as falsy).
|
|
70
|
+
"""
|
|
71
|
+
header = response.headers.get("retry-after")
|
|
72
|
+
if not header:
|
|
73
|
+
return None
|
|
74
|
+
try:
|
|
75
|
+
return max(0.0, float(header))
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
# HTTP-date form, e.g. "Wed, 16 Jul 2026 12:00:05 GMT" (sent by many proxies/CDNs).
|
|
79
|
+
try:
|
|
80
|
+
when = parsedate_to_datetime(header)
|
|
81
|
+
except (TypeError, ValueError):
|
|
82
|
+
return None
|
|
83
|
+
if when is None:
|
|
84
|
+
return None
|
|
85
|
+
if when.tzinfo is None:
|
|
86
|
+
when = when.replace(tzinfo=timezone.utc)
|
|
87
|
+
return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def retry_delay(response: httpx.Response, attempt: int) -> float:
|
|
91
|
+
"""The delay before a retry: the server's ``Retry-After`` if given, else backoff.
|
|
92
|
+
|
|
93
|
+
Uses an explicit ``None`` check so a ``Retry-After: 0`` (retry immediately) is honored
|
|
94
|
+
rather than falling through to exponential backoff.
|
|
95
|
+
"""
|
|
96
|
+
after = retry_after_seconds(response)
|
|
97
|
+
return after if after is not None else backoff_seconds(attempt)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_response(response: httpx.Response, path: str) -> Any:
|
|
101
|
+
"""Return the parsed JSON body, or raise :class:`RaidApiError` on a non-2xx."""
|
|
102
|
+
request_id = response.headers.get("request-id") or response.headers.get("x-request-id")
|
|
103
|
+
|
|
104
|
+
if response.status_code == 204:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
text = response.text
|
|
108
|
+
parsed: Any = text
|
|
109
|
+
if "application/json" in response.headers.get("content-type", "") and text:
|
|
110
|
+
try:
|
|
111
|
+
parsed = _json.loads(text)
|
|
112
|
+
except ValueError:
|
|
113
|
+
parsed = text
|
|
114
|
+
|
|
115
|
+
if not response.is_success:
|
|
116
|
+
message, code = extract_error(parsed)
|
|
117
|
+
raise RaidApiError(
|
|
118
|
+
response.status_code,
|
|
119
|
+
message or f"API error {response.status_code}: {response.reason_phrase}",
|
|
120
|
+
code=code,
|
|
121
|
+
body=parsed,
|
|
122
|
+
request_id=request_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return parsed
|
|
File without changes
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: openapi.yaml
|
|
3
|
+
# timestamp: 2026-07-26T10:34:39+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
from pydantic import AwareDatetime, BaseModel, Field, RootModel
|
|
8
|
+
from enum import Enum, IntEnum
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Error1(BaseModel):
|
|
13
|
+
code: Annotated[str, Field(examples=["IMAGE_FORENSICS:FILE_TOO_LARGE"])]
|
|
14
|
+
"""
|
|
15
|
+
Stable machine-readable error code.
|
|
16
|
+
"""
|
|
17
|
+
message: str
|
|
18
|
+
"""
|
|
19
|
+
Human-readable explanation, safe to log.
|
|
20
|
+
"""
|
|
21
|
+
source_host: Annotated[str | None, Field(alias="sourceHost")] = None
|
|
22
|
+
"""
|
|
23
|
+
For URL-based endpoints, the host that could not be resolved.
|
|
24
|
+
"""
|
|
25
|
+
user_message: Annotated[str | None, Field(alias="userMessage")] = None
|
|
26
|
+
"""
|
|
27
|
+
A display-friendly variant of the message (URL-based endpoints only).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Error(BaseModel):
|
|
32
|
+
"""
|
|
33
|
+
Standard error envelope returned by every endpoint on failure.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
error: Error1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Verdict(Enum):
|
|
40
|
+
"""
|
|
41
|
+
Overall classification of the media.
|
|
42
|
+
- `real` — no manipulation detected
|
|
43
|
+
- `ai_generated` — produced by a generative model
|
|
44
|
+
- `ai_edited` — a real asset modified with AI tools
|
|
45
|
+
- `digitally_edited` — conventional (non-AI) editing
|
|
46
|
+
- `unknown` — the analysis was inconclusive
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
real = "real"
|
|
51
|
+
ai_generated = "ai_generated"
|
|
52
|
+
ai_edited = "ai_edited"
|
|
53
|
+
digitally_edited = "digitally_edited"
|
|
54
|
+
unknown = "unknown"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class JobStatus(Enum):
|
|
58
|
+
"""
|
|
59
|
+
Lifecycle state of an asynchronous job.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
queued = "Queued"
|
|
63
|
+
processing = "Processing"
|
|
64
|
+
completed = "Completed"
|
|
65
|
+
failed = "Failed"
|
|
66
|
+
cancelled = "Cancelled"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Modality(Enum):
|
|
70
|
+
"""
|
|
71
|
+
The kind of media being submitted.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
image = "image"
|
|
75
|
+
audio = "audio"
|
|
76
|
+
video = "video"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class VoiceWorkflowType(IntEnum):
|
|
80
|
+
"""
|
|
81
|
+
Which voice workflow to run:
|
|
82
|
+
- `1` — Transcription only
|
|
83
|
+
- `2` — Intelligence (text analysis) only
|
|
84
|
+
- `3` — Combined transcription + intelligence
|
|
85
|
+
- `4` — AI-voice detection only (default)
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
integer_1 = 1
|
|
90
|
+
integer_2 = 2
|
|
91
|
+
integer_3 = 3
|
|
92
|
+
integer_4 = 4
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Generators(RootModel[dict[str, Any] | None]):
|
|
96
|
+
root: dict[str, Any] | None
|
|
97
|
+
"""
|
|
98
|
+
Generator attribution as a map of generator name → score (0–1), e.g.
|
|
99
|
+
`{ "midjourney": 0.95 }`. Keys are returned dynamically by the detector.
|
|
100
|
+
Null on basic plans.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Stage(Enum):
|
|
106
|
+
"""
|
|
107
|
+
Which analysis stage decided the verdict.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
full_image = "full_image"
|
|
111
|
+
face_crop = "face_crop"
|
|
112
|
+
full_image_no_face = "full_image_no_face"
|
|
113
|
+
raw = "raw"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ImageFace(BaseModel):
|
|
117
|
+
"""
|
|
118
|
+
One detected face, located in original image pixels.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
x: float
|
|
122
|
+
"""
|
|
123
|
+
Left edge of the face rectangle, in pixels.
|
|
124
|
+
"""
|
|
125
|
+
y: float
|
|
126
|
+
"""
|
|
127
|
+
Top edge of the face rectangle, in pixels.
|
|
128
|
+
"""
|
|
129
|
+
width: float
|
|
130
|
+
height: float
|
|
131
|
+
confidence: Annotated[float, Field(ge=0.0, le=1.0)]
|
|
132
|
+
"""
|
|
133
|
+
Confidence that this region is a face.
|
|
134
|
+
"""
|
|
135
|
+
deepfake_score: Annotated[float, Field(alias="deepfakeScore", ge=0.0, le=1.0)]
|
|
136
|
+
"""
|
|
137
|
+
Likelihood this face is synthetic, from 0 to 1.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class VoiceAnalysisResponse(BaseModel):
|
|
142
|
+
is_successful: Annotated[bool, Field(alias="isSuccessful")]
|
|
143
|
+
error_message: Annotated[str | None, Field(alias="errorMessage")] = None
|
|
144
|
+
workflow_type: Annotated[VoiceWorkflowType, Field(alias="workflowType")]
|
|
145
|
+
transcript: str | None = None
|
|
146
|
+
"""
|
|
147
|
+
Transcribed text. Populated for Transcription and Combined workflows.
|
|
148
|
+
"""
|
|
149
|
+
analysis: str | None = None
|
|
150
|
+
"""
|
|
151
|
+
Intelligence analysis text. Populated for Intelligence and Combined workflows.
|
|
152
|
+
"""
|
|
153
|
+
context_used: Annotated[str | None, Field(alias="contextUsed")] = None
|
|
154
|
+
"""
|
|
155
|
+
The context hints that were applied, if any.
|
|
156
|
+
"""
|
|
157
|
+
is_ai_detected: Annotated[bool | None, Field(alias="isAiDetected")] = None
|
|
158
|
+
"""
|
|
159
|
+
Whether the voice is AI-generated. Populated for the AI-detection workflow.
|
|
160
|
+
"""
|
|
161
|
+
detection_confidence: Annotated[
|
|
162
|
+
float | None, Field(alias="detectionConfidence", ge=0.0, le=1.0)
|
|
163
|
+
] = None
|
|
164
|
+
"""
|
|
165
|
+
Confidence of the AI-voice detection (0–1).
|
|
166
|
+
"""
|
|
167
|
+
detection_classification: Annotated[str | None, Field(alias="detectionClassification")] = None
|
|
168
|
+
"""
|
|
169
|
+
Classification label for detection, e.g. `real`, `suspicious`, or `fake`.
|
|
170
|
+
"""
|
|
171
|
+
processing_time_ms: Annotated[int, Field(alias="processingTimeMs")]
|
|
172
|
+
credits_used: Annotated[int, Field(alias="creditsUsed")]
|
|
173
|
+
metadata: dict[str, Any] | None = None
|
|
174
|
+
"""
|
|
175
|
+
Additional workflow metadata, when available.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class JobSubmitResponse(BaseModel):
|
|
180
|
+
"""
|
|
181
|
+
Returned when an asynchronous fact-checking job is accepted.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
job_id: Annotated[UUID, Field(alias="jobId")]
|
|
185
|
+
"""
|
|
186
|
+
Identifier to poll for status and results.
|
|
187
|
+
"""
|
|
188
|
+
status: JobStatus | None = None
|
|
189
|
+
credits_reserved: Annotated[int, Field(alias="creditsReserved")]
|
|
190
|
+
"""
|
|
191
|
+
Credits held pending completion; confirmed on success and refunded on failure/cancel.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class VideoSubmitResponse(BaseModel):
|
|
196
|
+
"""
|
|
197
|
+
Returned when a video job is accepted.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
job_id: Annotated[UUID, Field(alias="jobId")]
|
|
201
|
+
"""
|
|
202
|
+
Identifier to poll for status and results.
|
|
203
|
+
"""
|
|
204
|
+
credits_reserved: Annotated[int, Field(alias="creditsReserved")]
|
|
205
|
+
"""
|
|
206
|
+
Credits held pending completion; confirmed on success and refunded on failure/cancel.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class VideoProvider(BaseModel):
|
|
211
|
+
"""
|
|
212
|
+
Per-provider breakdown (detailed plans only).
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
name: str
|
|
216
|
+
"""
|
|
217
|
+
Provider identifier.
|
|
218
|
+
"""
|
|
219
|
+
verdict: Verdict
|
|
220
|
+
confidence: Annotated[float, Field(ge=0.0, le=1.0)]
|
|
221
|
+
frames_analyzed: Annotated[int | None, Field(alias="framesAnalyzed")] = None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class VideoResult(BaseModel):
|
|
225
|
+
"""
|
|
226
|
+
Populated on a video job once `status` is `Completed`.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
verdict: Verdict
|
|
230
|
+
confidence: Annotated[float, Field(ge=0.0, le=1.0)]
|
|
231
|
+
is_ai_generated: Annotated[bool, Field(alias="isAiGenerated")]
|
|
232
|
+
frames_analyzed: Annotated[int, Field(alias="framesAnalyzed")]
|
|
233
|
+
duration_seconds: Annotated[float | None, Field(alias="durationSeconds")] = None
|
|
234
|
+
summary: str | None = None
|
|
235
|
+
"""
|
|
236
|
+
Short human-readable summary of the finding.
|
|
237
|
+
"""
|
|
238
|
+
providers: list[VideoProvider] | None = None
|
|
239
|
+
"""
|
|
240
|
+
Per-provider breakdown. Null on basic plans.
|
|
241
|
+
"""
|
|
242
|
+
has_detailed_report: Annotated[bool, Field(alias="hasDetailedReport")]
|
|
243
|
+
"""
|
|
244
|
+
True when detailed fields (e.g. `providers`) are populated. False on basic plans.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class VideoJob(BaseModel):
|
|
249
|
+
id: UUID
|
|
250
|
+
file_name: Annotated[str, Field(alias="fileName")]
|
|
251
|
+
file_size_bytes: Annotated[int | None, Field(alias="fileSizeBytes")] = None
|
|
252
|
+
duration_seconds: Annotated[float | None, Field(alias="durationSeconds")] = None
|
|
253
|
+
status: JobStatus
|
|
254
|
+
progress: Annotated[int, Field(ge=0, le=100)]
|
|
255
|
+
"""
|
|
256
|
+
Percent complete.
|
|
257
|
+
"""
|
|
258
|
+
status_message: Annotated[str | None, Field(alias="statusMessage")] = None
|
|
259
|
+
credits_reserved: Annotated[int, Field(alias="creditsReserved")]
|
|
260
|
+
credits_used: Annotated[int, Field(alias="creditsUsed")]
|
|
261
|
+
frames_analyzed: Annotated[int | None, Field(alias="framesAnalyzed")] = None
|
|
262
|
+
processing_time_ms: Annotated[int, Field(alias="processingTimeMs")]
|
|
263
|
+
error_message: Annotated[str | None, Field(alias="errorMessage")] = None
|
|
264
|
+
created_at: Annotated[AwareDatetime, Field(alias="createdAt")]
|
|
265
|
+
modified_at: Annotated[AwareDatetime | None, Field(alias="modifiedAt")] = None
|
|
266
|
+
result: VideoResult | None = None
|
|
267
|
+
"""
|
|
268
|
+
Null until the job completes.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class ProvenanceUrl(BaseModel):
|
|
273
|
+
url: str
|
|
274
|
+
page_title: Annotated[str | None, Field(alias="pageTitle")] = None
|
|
275
|
+
first_seen_at: Annotated[AwareDatetime | None, Field(alias="firstSeenAt")] = None
|
|
276
|
+
match_type: Annotated[str | None, Field(alias="matchType")] = None
|
|
277
|
+
"""
|
|
278
|
+
How the media matched this URL, e.g. `full`, `partial`, `page`, `entity`.
|
|
279
|
+
"""
|
|
280
|
+
relevance: str | None = None
|
|
281
|
+
"""
|
|
282
|
+
Assessed relevance — `high`, `medium`, or `low`.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ProvenanceItem(BaseModel):
|
|
287
|
+
"""
|
|
288
|
+
A domain where the media (or closely matching media) was found.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
domain: Annotated[str, Field(examples=["bbc.com"])]
|
|
292
|
+
tier: str
|
|
293
|
+
"""
|
|
294
|
+
Source tier — `authoritative`, `news`, `social`, or `unknown`.
|
|
295
|
+
"""
|
|
296
|
+
urls: list[ProvenanceUrl]
|
|
297
|
+
is_earliest_seen: Annotated[bool | None, Field(alias="isEarliestSeen")] = None
|
|
298
|
+
"""
|
|
299
|
+
True for the earliest-known appearance across the result.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Claim(BaseModel):
|
|
304
|
+
"""
|
|
305
|
+
A checkable claim extracted from audio/video. Null/absent for image jobs.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
claim_text: Annotated[str, Field(alias="claimText")]
|
|
309
|
+
verbatim_quote: Annotated[str | None, Field(alias="verbatimQuote")] = None
|
|
310
|
+
verdict: str | None = None
|
|
311
|
+
"""
|
|
312
|
+
Assessment — `supported`, `contradicted`, `mixed`, or `unverified`.
|
|
313
|
+
"""
|
|
314
|
+
confidence: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
|
|
315
|
+
speaker: str | None = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class FactCheckResult(BaseModel):
|
|
319
|
+
"""
|
|
320
|
+
Populated on a fact-checking job once `status` is `Completed`. Additional fields may be
|
|
321
|
+
present for detailed reports — clients should ignore unknown fields (see Versioning).
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
modality: Modality
|
|
326
|
+
has_detailed_report: Annotated[bool, Field(alias="hasDetailedReport")]
|
|
327
|
+
summary: str
|
|
328
|
+
"""
|
|
329
|
+
One-paragraph synthesis of the finding.
|
|
330
|
+
"""
|
|
331
|
+
provenance: list[ProvenanceItem]
|
|
332
|
+
"""
|
|
333
|
+
Domains where the media was found, grouped by source.
|
|
334
|
+
"""
|
|
335
|
+
claims: list[Claim] | None = None
|
|
336
|
+
"""
|
|
337
|
+
Extracted claims with verdicts (audio/video only).
|
|
338
|
+
"""
|
|
339
|
+
transcript: str | None = None
|
|
340
|
+
"""
|
|
341
|
+
Diarized transcript (audio/video only).
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class FactCheckJob(BaseModel):
|
|
346
|
+
id: UUID
|
|
347
|
+
modality: Modality
|
|
348
|
+
file_name: Annotated[str, Field(alias="fileName")]
|
|
349
|
+
file_size_bytes: Annotated[int | None, Field(alias="fileSizeBytes")] = None
|
|
350
|
+
duration_seconds: Annotated[float | None, Field(alias="durationSeconds")] = None
|
|
351
|
+
status: JobStatus
|
|
352
|
+
progress: Annotated[int, Field(ge=0, le=100)]
|
|
353
|
+
status_message: Annotated[str | None, Field(alias="statusMessage")] = None
|
|
354
|
+
credits_reserved: Annotated[int, Field(alias="creditsReserved")]
|
|
355
|
+
credits_used: Annotated[int, Field(alias="creditsUsed")]
|
|
356
|
+
processing_time_ms: Annotated[int, Field(alias="processingTimeMs")]
|
|
357
|
+
error_message: Annotated[str | None, Field(alias="errorMessage")] = None
|
|
358
|
+
user_context: Annotated[str | None, Field(alias="userContext")] = None
|
|
359
|
+
"""
|
|
360
|
+
The context you supplied at submit, echoed back.
|
|
361
|
+
"""
|
|
362
|
+
created_at: Annotated[AwareDatetime, Field(alias="createdAt")]
|
|
363
|
+
modified_at: Annotated[AwareDatetime | None, Field(alias="modifiedAt")] = None
|
|
364
|
+
result: FactCheckResult | None = None
|
|
365
|
+
"""
|
|
366
|
+
Null until the job completes.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ImageFaceAnalysis(BaseModel):
|
|
371
|
+
"""
|
|
372
|
+
How the verdict was reached at face level. When `stage` is `face_crop`, every
|
|
373
|
+
detected face was scored individually; when the whole image decided
|
|
374
|
+
(`full_image`, or `full_image_no_face` when no face was detected),
|
|
375
|
+
`decidingFace` and `faces` are null by design.
|
|
376
|
+
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
stage: Stage
|
|
380
|
+
"""
|
|
381
|
+
Which analysis stage decided the verdict.
|
|
382
|
+
"""
|
|
383
|
+
full_image_score: Annotated[float | None, Field(alias="fullImageScore", ge=0.0, le=1.0)] = None
|
|
384
|
+
"""
|
|
385
|
+
Whole image synthetic score from the first analysis stage.
|
|
386
|
+
"""
|
|
387
|
+
deciding_face: Annotated[ImageFace | None, Field(alias="decidingFace")] = None
|
|
388
|
+
"""
|
|
389
|
+
The face that determined the verdict; null when the whole image decided.
|
|
390
|
+
"""
|
|
391
|
+
faces: list[ImageFace] | None = None
|
|
392
|
+
"""
|
|
393
|
+
Every detected face, largest first; null when the whole image decided.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class ImageResult(BaseModel):
|
|
398
|
+
id: UUID | None = None
|
|
399
|
+
"""
|
|
400
|
+
Stable record ID for this image (null if the image failed to process).
|
|
401
|
+
"""
|
|
402
|
+
file_name: Annotated[str, Field(alias="fileName")]
|
|
403
|
+
"""
|
|
404
|
+
Original file name (or a derived name for URL inputs).
|
|
405
|
+
"""
|
|
406
|
+
verdict: Verdict
|
|
407
|
+
confidence: Annotated[float, Field(examples=[0.972], ge=0.0, le=1.0)]
|
|
408
|
+
"""
|
|
409
|
+
Confidence in the verdict, from 0 to 1.
|
|
410
|
+
"""
|
|
411
|
+
is_manipulated: Annotated[bool, Field(alias="isManipulated")]
|
|
412
|
+
"""
|
|
413
|
+
Convenience flag — true when `verdict` is anything other than `real`.
|
|
414
|
+
"""
|
|
415
|
+
credits_used: Annotated[int, Field(alias="creditsUsed")]
|
|
416
|
+
"""
|
|
417
|
+
Credits charged for this image.
|
|
418
|
+
"""
|
|
419
|
+
processing_time_ms: Annotated[int, Field(alias="processingTimeMs")]
|
|
420
|
+
"""
|
|
421
|
+
Server-side processing time in milliseconds.
|
|
422
|
+
"""
|
|
423
|
+
error_message: Annotated[str | None, Field(alias="errorMessage")] = None
|
|
424
|
+
"""
|
|
425
|
+
Failure reason when this image could not be analyzed; otherwise null.
|
|
426
|
+
"""
|
|
427
|
+
created_at: Annotated[AwareDatetime, Field(alias="createdAt")]
|
|
428
|
+
deepfake_score: Annotated[float | None, Field(alias="deepfakeScore")] = None
|
|
429
|
+
"""
|
|
430
|
+
Likelihood the image is a deepfake (0–1). Null on basic plans.
|
|
431
|
+
"""
|
|
432
|
+
generators: Generators | None = None
|
|
433
|
+
heatmap_url: Annotated[str | None, Field(alias="heatmapUrl")] = None
|
|
434
|
+
"""
|
|
435
|
+
URL to a manipulation heatmap visualization. Null on basic plans.
|
|
436
|
+
"""
|
|
437
|
+
face_analysis: Annotated[ImageFaceAnalysis | None, Field(alias="faceAnalysis")] = None
|
|
438
|
+
"""
|
|
439
|
+
Face level analysis of the detection. Present only when face details are
|
|
440
|
+
enabled on your account; null otherwise, and null on images where the
|
|
441
|
+
analysis did not run face by face.
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class ImageForensicsResponse(BaseModel):
|
|
447
|
+
images: list[ImageResult]
|
|
448
|
+
total_credits_used: Annotated[int, Field(alias="totalCreditsUsed")]
|
|
449
|
+
total_processing_time_ms: Annotated[int, Field(alias="totalProcessingTimeMs")]
|
|
450
|
+
is_successful: Annotated[bool, Field(alias="isSuccessful")]
|
|
451
|
+
"""
|
|
452
|
+
True when at least one image was analyzed successfully.
|
|
453
|
+
"""
|
|
454
|
+
error_message: Annotated[str | None, Field(alias="errorMessage")] = None
|
|
455
|
+
has_detailed_report: Annotated[bool, Field(alias="hasDetailedReport")]
|
|
456
|
+
"""
|
|
457
|
+
True when the response includes detailed fields (`generators`, `deepfakeScore`,
|
|
458
|
+
`heatmapUrl`). False on basic plans, where those fields are null.
|
|
459
|
+
|
|
460
|
+
"""
|