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 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
+ """