authentasdk 0.2.3__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.
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ from io import BytesIO
4
+ from typing import Dict, Any, List, Optional
5
+ import os
6
+ import mimetypes
7
+
8
+ import cv2
9
+ import requests
10
+ from PIL import Image
11
+ from pathlib import Path
12
+
13
+
14
+
15
+ # -------------------------
16
+ # Internal helpers
17
+ # -------------------------
18
+
19
+ def _ensure_dir(path: str) -> None:
20
+ """Create parent directory for a file path if needed."""
21
+ directory = os.path.dirname(path)
22
+ if directory:
23
+ os.makedirs(directory, exist_ok=True)
24
+
25
+
26
+ # -------------------------
27
+ # Heatmap helpers
28
+ # -------------------------
29
+
30
+ # def save_heatmap_image(
31
+ # media: Dict[str, Any],
32
+ # out_path: str,
33
+ # participant_id: int = 0,
34
+ # ) -> str:
35
+ # """
36
+ # Download and save the heatmap as an image for one participant.
37
+
38
+ # Intended mainly for image model outputs (e.g. AC-1).
39
+ # Returns the output path.
40
+ # """
41
+ # participant = media["participants"][participant_id]
42
+ # heatmap_url = participant.get("heatmap", "")
43
+ # if not heatmap_url:
44
+ # raise RuntimeError("No heatmap URL found for this participant")
45
+
46
+ # resp = requests.get(heatmap_url, timeout=30)
47
+ # if resp.status_code == 404:
48
+ # raise RuntimeError(
49
+ # "Heatmap not available (404). The presigned URL may have expired; "
50
+ # "please re-run detection and save the heatmap soon after."
51
+ # )
52
+ # resp.raise_for_status()
53
+
54
+ # img = Image.open(BytesIO(resp.content)).convert("RGB")
55
+ # _ensure_dir(out_path)
56
+ # img.save(out_path)
57
+ # return out_path
58
+
59
+
60
+ # def save_heatmap_video(
61
+ # media: Dict[str, Any],
62
+ # out_path: str,
63
+ # participant_id: int = 0,
64
+ # ) -> str:
65
+ # """
66
+ # Download and save the heatmap video for one participant.
67
+
68
+ # Intended for video model outputs (e.g. DF-1), where `heatmap`
69
+ # is a presigned video URL.
70
+ # Returns the output path.
71
+ # """
72
+ # participant = media["participants"][participant_id]
73
+ # heatmap_url = participant.get("heatmap", "")
74
+ # if not heatmap_url:
75
+ # raise RuntimeError("No heatmap URL found for this participant")
76
+
77
+ # resp = requests.get(heatmap_url, stream=True, timeout=60)
78
+ # if resp.status_code == 404:
79
+ # raise RuntimeError(
80
+ # "Heatmap not available (404). The presigned URL may have expired; "
81
+ # "please re-run detection and save the heatmap soon after."
82
+ # )
83
+ # resp.raise_for_status()
84
+
85
+ # _ensure_dir(out_path)
86
+ # with open(out_path, "wb") as f:
87
+ # for chunk in resp.iter_content(chunk_size=8192):
88
+ # if chunk:
89
+ # f.write(chunk)
90
+
91
+ # return out_path
92
+
93
+
94
+ # def save_heatmap(
95
+ # media: Dict[str, Any],
96
+ # out_path: str,
97
+ # participant_id: int = 0,
98
+ # model_type: Optional[str] = None,
99
+ # ) -> str:
100
+ # """
101
+ # Convenience wrapper that chooses image or video heatmap saver
102
+ # based on model_type (if provided) or URL extension.
103
+
104
+ # Returns the output path.
105
+ # """
106
+ # participant = media["participants"][participant_id]
107
+ # heatmap_url = participant.get("heatmap", "")
108
+ # if not heatmap_url:
109
+ # raise RuntimeError("No heatmap URL found for this participant")
110
+
111
+ # # Prefer explicit model_type if caller passes it
112
+ # if model_type is not None:
113
+ # mt = model_type.upper()
114
+ # if mt.startswith("AC-"):
115
+ # return save_heatmap_image(media, out_path, participant_id=participant_id)
116
+ # if mt.startswith("DF-"):
117
+ # return save_heatmap_video(media, out_path, participant_id=participant_id)
118
+
119
+ # # Fallback: simple heuristic based on URL
120
+ # lower = heatmap_url.lower()
121
+ # if any(lower.endswith(ext) for ext in (".jpg", ".jpeg", ".png", ".bmp")):
122
+ # return save_heatmap_image(media, out_path, participant_id=participant_id)
123
+ # return save_heatmap_video(media, out_path, participant_id=participant_id)
124
+
125
+ def save_heatmap_image(
126
+ media: Dict[str, Any],
127
+ out_path: str,
128
+ ) -> str:
129
+ """
130
+ Download and save the heatmap as an image.
131
+ Returns the output path.
132
+ """
133
+ # AC-1: heatmapURL
134
+ heatmap_url = media.get("heatmapURL", "")
135
+ if not heatmap_url:
136
+ raise RuntimeError("No heatmapURL found in media for image heatmap")
137
+
138
+ resp = requests.get(heatmap_url, timeout=30)
139
+ if resp.status_code == 404:
140
+ raise RuntimeError(
141
+ "Heatmap not available (404)"
142
+ )
143
+ resp.raise_for_status()
144
+
145
+ img = Image.open(BytesIO(resp.content)).convert("RGB")
146
+ _ensure_dir(out_path)
147
+ img.save(out_path)
148
+ return out_path
149
+
150
+ def save_heatmap_video(
151
+ media: Dict[str, Any],
152
+ out_dir: str,
153
+ base_name: str = "heatmap",
154
+ ) -> List[str]:
155
+ participants = media.get("participants") or []
156
+ print(f"found {len(participants)} participants in media")
157
+ if not participants:
158
+ raise RuntimeError("No participants found in media for video heatmap")
159
+
160
+ parent = Path(out_dir)
161
+ parent.mkdir(parents=True, exist_ok=True)
162
+
163
+ outputs: List[str] = []
164
+
165
+ for idx, p in enumerate(participants):
166
+ heatmap_url = p.get("heatmap")
167
+ print(f"participant {idx} heatmap URL: {heatmap_url}")
168
+ if not heatmap_url:
169
+ print(f"[warn] no heatmap URL for participant {idx}, skipping")
170
+ continue
171
+
172
+ print(f"downloading participant {idx}…")
173
+ resp = requests.get(heatmap_url, stream=True, timeout=120)
174
+ if resp.status_code in (403, 404):
175
+ print(f"[warn] participant {idx} heatmap returned {resp.status_code}, skipping")
176
+ continue
177
+ resp.raise_for_status()
178
+
179
+ content_type = resp.headers.get("Content-Type", "")
180
+ ext = mimetypes.guess_extension(content_type.split(";")[0]) or ".mp4"
181
+
182
+ dest = parent / f"{base_name}_p{idx}{ext}"
183
+ with open(dest, "wb") as f:
184
+ for chunk in resp.iter_content(chunk_size=8192):
185
+ if chunk:
186
+ f.write(chunk)
187
+
188
+ outputs.append(str(dest))
189
+ print(f"saved {dest}")
190
+
191
+ return outputs
192
+
193
+ def save_heatmap(
194
+ media: Dict[str, Any],
195
+ out_path: str,
196
+ model_type: Optional[str] = None,
197
+ ) -> str:
198
+ """
199
+ wrapper that chooses image or video heatmap saver
200
+ based on model_type (if provided) or media["type"].
201
+ Returns the output path.
202
+ """
203
+ if model_type is not None:
204
+ mt = model_type.upper()
205
+ if mt.startswith("AC-"):
206
+ return save_heatmap_image(media, out_path)
207
+ if mt.startswith("DF-"):
208
+ return save_heatmap_video(media, out_path)
209
+
210
+ # Fallback: infer from media["type"]
211
+ mtype = (media.get("type") or "").lower()
212
+ if mtype == "image":
213
+ return save_heatmap_image(media, out_path)
214
+ if mtype == "video":
215
+ return save_heatmap_video(media, out_path)
216
+
217
+ # Last resort: try heatmapURL vs participants
218
+ if "heatmapURL" in media:
219
+ return save_heatmap_image(media, out_path)
220
+ return save_heatmap_video(media, out_path)
221
+
222
+
223
+ # -------------------------
224
+ # BBox
225
+ # -------------------------
226
+
227
+ def draw_bounding_boxes(
228
+ video_path: str,
229
+ sequence_dict: Dict[int, List[Dict[str, Any]]],
230
+ result_video_path: str,
231
+ ) -> None:
232
+ """
233
+ Original bounding-box renderer.
234
+
235
+ sequence_dict:
236
+ frame_index (int) -> list of {
237
+ "data": [x1, y1, x2, y2],
238
+ "class": "real" | "fake",
239
+ "confidence": float in [0,1]
240
+ }
241
+ """
242
+ cap = cv2.VideoCapture(video_path)
243
+ width = int(cap.get(3))
244
+ height = int(cap.get(4))
245
+ fps = int(cap.get(5)) or 25
246
+
247
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
248
+ _ensure_dir(result_video_path)
249
+ output = cv2.VideoWriter(result_video_path, fourcc, fps, (width, height))
250
+
251
+ frame_index = 0
252
+
253
+ while True:
254
+ ret, frame = cap.read()
255
+ if not ret:
256
+ break
257
+
258
+ if frame_index in sequence_dict:
259
+ for item in sequence_dict[frame_index]:
260
+ bbox = item["data"]
261
+ class_label = item["class"]
262
+ color = (0, 255, 0) if class_label == "real" else (0, 0, 255)
263
+ confidence = round(item["confidence"] * 100, 2)
264
+
265
+ # Note: keep original *2 scaling
266
+ xmin, ymin, xmax, ymax = map(int, [b * 2 for b in bbox])
267
+
268
+ # Draw the bounding box
269
+ cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color, 2)
270
+
271
+ cv2.putText(
272
+ frame,
273
+ f"{class_label} {confidence}%",
274
+ (xmin, ymin - 10),
275
+ cv2.FONT_HERSHEY_SIMPLEX,
276
+ 0.9,
277
+ color,
278
+ 2,
279
+ )
280
+
281
+ output.write(frame)
282
+ frame_index += 1
283
+
284
+ output.release()
285
+ cap.release()
286
+
287
+
288
+ # -------------------------
289
+ # Adapter: Authenta -> sequence_dict
290
+ # -------------------------
291
+
292
+ # def authenta_to_sequence_dict(
293
+ # media: Dict[str, Any],
294
+ # participant_id: int = 0,
295
+ # default_class: str = "fake",
296
+ # default_confidence: float = 1.0,
297
+ # ) -> Dict[int, List[Dict[str, Any]]]:
298
+ # """
299
+ # Convert Authenta boundingBoxes JSON into sequence_dict format
300
+ # expected by draw_bounding_boxes.
301
+ # """
302
+ # detail_resp = requests.get(media["resultURL"], timeout=30)
303
+ # detail_resp.raise_for_status()
304
+ # detail = detail_resp.json()
305
+
306
+ # # detail["boundingBoxes"][str(participant_id)]["boundingBox"]
307
+ # # -> frame_idx(str) -> [x1, y1, x2, y2]
308
+ # bbox_dict = detail["boundingBoxes"][str(participant_id)]["boundingBox"]
309
+
310
+ # sequence_dict: Dict[int, List[Dict[str, Any]]] = {}
311
+
312
+ # for frame_str, bbox in bbox_dict.items():
313
+ # frame_idx = int(frame_str)
314
+ # item = {
315
+ # "data": bbox, # [x1, y1, x2, y2] as-is
316
+ # "class": default_class, # "fake" or "real"
317
+ # "confidence": default_confidence,
318
+ # }
319
+ # if frame_idx not in sequence_dict:
320
+ # sequence_dict[frame_idx] = []
321
+ # sequence_dict[frame_idx].append(item)
322
+
323
+ # return sequence_dict
324
+
325
+
326
+ # def save_bounding_box_video(
327
+ # media: Dict[str, Any],
328
+ # src_video_path: str,
329
+ # out_video_path: str,
330
+ # participant_id: int = 0,
331
+ # ) -> str:
332
+ # """
333
+ # Build sequence_dict from Authenta results and call draw_bounding_boxes.
334
+
335
+ # Returns the output path.
336
+ # """
337
+ # sequence_dict = authenta_to_sequence_dict(
338
+ # media,
339
+ # participant_id=participant_id,
340
+ # default_class="fake", # Authenta: participant.fake == True
341
+ # default_confidence=1.0, # or map from detail JSON if available
342
+ # )
343
+ # draw_bounding_boxes(src_video_path, sequence_dict, out_video_path)
344
+ # return out_video_path
345
+
346
+ def authenta_to_sequence_dict(
347
+ media: Dict[str, Any],
348
+ default_class: str = "fake",
349
+ default_confidence: float = 1.0,
350
+ ) -> Dict[int, List[Dict[str, Any]]]:
351
+ """
352
+ Convert Authenta boundingBoxes JSON into sequence_dict format
353
+ expected by draw_bounding_boxes.
354
+ """
355
+ detail_resp = requests.get(media["resultURL"], timeout=30)
356
+ detail_resp.raise_for_status()
357
+ detail = detail_resp.json()
358
+
359
+ participants = media.get("participants") or []
360
+ if not participants:
361
+ raise RuntimeError("No participants found in media for bounding boxes")
362
+
363
+ # first participant
364
+ participant_id = 0
365
+
366
+ bbox_dict = detail["boundingBoxes"][str(participant_id)]["boundingBox"]
367
+ sequence_dict: Dict[int, List[Dict[str, Any]]] = {}
368
+
369
+ for frame_str, bbox in bbox_dict.items():
370
+ frame_idx = int(frame_str)
371
+ item = {
372
+ "data": bbox,
373
+ "class": default_class,
374
+ "confidence": default_confidence,
375
+ }
376
+ sequence_dict.setdefault(frame_idx, []).append(item)
377
+
378
+ return sequence_dict
379
+
380
+
381
+ def save_bounding_box_video(
382
+ media: Dict[str, Any],
383
+ src_video_path: str,
384
+ out_video_path: str,
385
+ ) -> str:
386
+ """
387
+ Build sequence_dict from Authenta results and call draw_bounding_boxes.
388
+ Returns the output path.
389
+ """
390
+ sequence_dict = authenta_to_sequence_dict(
391
+ media,
392
+ default_class="fake",
393
+ default_confidence=1.0,
394
+ )
395
+ draw_bounding_boxes(src_video_path, sequence_dict, out_video_path)
396
+ return out_video_path
397
+
398
+ # -------------------------
399
+ # Artefact savers
400
+ # -------------------------
401
+
402
+ def save_image_artefacts(
403
+ media: Dict[str, Any],
404
+ out_dir: str,
405
+ base_name: str = "image",
406
+ ) -> Dict[str, str]:
407
+ """
408
+ Save all visual artefacts for an image (AC-1).
409
+ Args:
410
+ media: AC-1 media JSON returned by AuthentaClient.process(...).
411
+ out_dir: Output directory where artefacts will be saved.
412
+ base_name: Base name for files (default: "image").
413
+ Returns:
414
+ Dict with keys like:
415
+ {
416
+ "heatmap": "<out_dir>/<base_name>_heatmap.jpg"
417
+ }
418
+ """
419
+ out_path = str(Path(out_dir) / f"{base_name}_heatmap.jpg")
420
+ path = save_heatmap_image(media, out_path)
421
+ return {"heatmap": path}
422
+
423
+
424
+ def save_video_artefacts(
425
+ media: Dict[str, Any],
426
+ src_video_path: str,
427
+ out_dir: str,
428
+ base_name: str = "video",
429
+ ) -> Dict[str, Any]:
430
+ """
431
+ Save all visual artefacts for a deepfake video (DF-1).
432
+ Args:
433
+ media: DF-1 media JSON returned by AuthentaClient.process(...).
434
+ src_video_path: Path to the original input video.
435
+ out_dir: Output directory where artefacts will be saved.
436
+ base_name: Base name for files (default: "video").
437
+ Returns:
438
+ Dict with keys like:
439
+ {
440
+ "heatmaps": [
441
+ "<out_dir>/<base_name>_heatmap_p0.mp4",
442
+ "<out_dir>/<base_name>_heatmap_p1.mp4",
443
+ ...
444
+ ],
445
+ "bbox_video": "<out_dir>/<base_name>_bbox.mp4",
446
+ }
447
+ """
448
+ out_dir_path = Path(out_dir)
449
+ out_dir_path.mkdir(parents=True, exist_ok=True)
450
+
451
+ # 1) Heatmap videos per participant
452
+ heatmap_base = f"{base_name}_heatmap"
453
+ heatmap_paths = save_heatmap_video(
454
+ media=media,
455
+ out_dir=str(out_dir_path),
456
+ base_name=heatmap_base,
457
+ )
458
+
459
+ # 2) Bounding-box annotated video
460
+ bbox_path = str(out_dir_path / f"{base_name}_bbox.mp4")
461
+ save_bounding_box_video(
462
+ media=media,
463
+ src_video_path=src_video_path,
464
+ out_video_path=bbox_path,
465
+ )
466
+
467
+ return {
468
+ "heatmap": heatmap_paths,
469
+ "bbox_video": bbox_path,
470
+ }