repzo 1.0.291 → 1.0.293

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.
Files changed (34) hide show
  1. package/changelog.md +28 -0
  2. package/lib/index.d.ts +244 -2
  3. package/lib/index.js +541 -0
  4. package/lib/types/index.d.ts +4533 -1
  5. package/package.json +1 -1
  6. package/src/index.ts +1270 -0
  7. package/src/oas/activity-ai-object-detection-session-frame.yaml +600 -0
  8. package/src/oas/ai-object-detection-assigned-missions.yaml +283 -0
  9. package/src/oas/ai-object-detection-assignment-rule.yaml +357 -0
  10. package/src/oas/ai-object-detection-category.yaml +338 -0
  11. package/src/oas/ai-object-detection-dataset.yaml +342 -0
  12. package/src/oas/ai-object-detection-detection-settings.yaml +410 -0
  13. package/src/oas/ai-object-detection-inference.yaml +818 -0
  14. package/src/oas/ai-object-detection-label-group.yaml +265 -0
  15. package/src/oas/ai-object-detection-label-report.yaml +366 -0
  16. package/src/oas/ai-object-detection-label.yaml +395 -0
  17. package/src/oas/ai-object-detection-metric-result.yaml +734 -0
  18. package/src/oas/ai-object-detection-metric.yaml +559 -0
  19. package/src/oas/ai-object-detection-mission-results.yaml +370 -0
  20. package/src/oas/ai-object-detection-mission-set.yaml +250 -0
  21. package/src/oas/ai-object-detection-mission.yaml +349 -0
  22. package/src/oas/ai-object-detection-model-version-epoch.yaml +214 -0
  23. package/src/oas/ai-object-detection-model-version-train-agent.yaml +88 -0
  24. package/src/oas/ai-object-detection-model-version.yaml +567 -0
  25. package/src/oas/ai-object-detection-model.yaml +369 -0
  26. package/src/oas/ai-object-detection-segment.yaml +301 -0
  27. package/src/oas/ai-object-detection-session-analysis.yaml +1306 -0
  28. package/src/oas/ai-object-detection-session-election.yaml +193 -0
  29. package/src/oas/ai-object-detection-session-insight.yaml +418 -0
  30. package/src/oas/ai-object-detection-session.yaml +840 -0
  31. package/src/oas/ai-object-detection-settings.yaml +249 -0
  32. package/src/oas/ai-object-detection-task.yaml +1007 -0
  33. package/src/oas/object-detection-analytics-report.yaml +674 -0
  34. package/src/types/index.ts +5159 -1
@@ -0,0 +1,818 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Repzo API - AI Object Detection Inference
4
+ version: 1.0.0
5
+ description: |
6
+ **Per-task inference** runs the Ultralytics object-detection model on a
7
+ single `ai-object-detection-task` image and (optionally) persists the
8
+ predictions as a fresh `auto` annotation group on that task. It is the
9
+ server-side proxy in front of the detection lambda.
10
+
11
+ **Engines.** `engine` selects the detector:
12
+ - `trained` — the Ultralytics lambda with a trained model (the classic path).
13
+ - `zero_shot` — a hosted vision-language model (Qwen-VL over OpenRouter)
14
+ prompted with the namespace's label names as the open-vocabulary
15
+ detection prompt. For datasets/namespaces with NO trained model yet.
16
+ The VLM's reply is normalized into the exact lambda result shape, so
17
+ mapping/placement/analysis work unchanged; the saved annotation group
18
+ carries `engine: zero_shot` + `zero_shot_model`. Needs at least one
19
+ label in the namespace (their names are the vocabulary) — else `400`.
20
+ - `auto` (default) — trained when a model resolves; when the classic
21
+ dataset path finds nothing trained, falls back to zero-shot (when
22
+ configured) instead of failing. Explicit `model` / `model_version`
23
+ requests stay strict (no fallback).
24
+
25
+ **Model resolution (trained).** Three paths, in priority order:
26
+ 1. `model_version` (and/or `model`) supplied in the body — used directly.
27
+ This **bypasses** the task's dataset/default-model requirement and is the
28
+ path used to place AR session frames. The lambda model id is
29
+ `model` if given, else the version's `model`; the YOLO class-index →
30
+ label fallback uses the version's `dataset_labels` order.
31
+ 2. `model` alone — used directly as the lambda model id; the model's
32
+ `current_model_version` (when set) supplies the label order.
33
+ 3. Neither — the classic path: the task's `task_dataset` must point at a
34
+ dataset with a `default_model` configured (else the zero-shot fallback
35
+ under `engine: auto`, or `400`).
36
+
37
+ **Label mapping.** Each prediction is mapped to a label by NAME
38
+ (case-insensitive, unique per namespace), falling back to the YOLO class
39
+ index into the version's `dataset_labels`. Unmapped predictions are
40
+ counted in `unmapped` and never become annotations.
41
+
42
+ **AR back-projection (session frames).** When the task carries `frame_meta`
43
+ (pose + intrinsics) and a `depth_media` blob, each detection is back-projected
44
+ into world coordinates: `world dims = bbox × depth × intrinsics × pose`.
45
+ Placed annotations get `world_position`, `world_size`, `depth_at_center`,
46
+ `depth_confidence`, `placement_confidence` and a `placed_box` snapshot;
47
+ un-placeable ones get an `ignore_reason` (`no_pose` | `no_intrinsics` |
48
+ `no_depth` | `empty_depth_region` | `insufficient_depth_pixels` |
49
+ `behind_shelf`) naming exactly why. Both are persisted so downstream
50
+ session analysis sees all of them — the kept/merged/ignored CONCLUSION is
51
+ made there, not here. For non-session tasks (no `frame_meta`) the
52
+ placement step is skipped. After placement the same DIMS RECLASSIFIER
53
+ (`config.reclassify_labels`, off by default — re-labels a detection whose
54
+ measured size fits a sibling label better, keeping `original_label` +
55
+ `reclassification_reason`) and SIZE GATE (`config.size_gate`, on by
56
+ default — flags `size_rejected` + `size_reject_detail` when the measured
57
+ size exceeds the label's expected dims beyond the allowance) run as in
58
+ the analysis stage.
59
+
60
+ **Explainability (XAI).** `explain: true` makes this service call the
61
+ dedicated DIAGNOSIS lambda (a separate function with its own memory
62
+ budget, so per-request inference stays lean) after the prediction. It
63
+ re-runs the identical forward pass with introspection hooks and returns an
64
+ `xai` object, passed through unchanged in this response: per-detection
65
+ top-k class scores ("why label A and not B"), an EigenCAM heatmap ("where
66
+ did the model look", an RGBA PNG whose alpha channel is the activation —
67
+ overlay it on the photo with an opacity slider), a TRUE gradient Grad-CAM
68
+ heatmap ("what evidence drove the detections" — class-discriminative,
69
+ computed via a gradient-enabled second pass), per-stage feature-map grids,
70
+ 2D UMAP/t-SNE/PCA embeddings of the detections, and the model version's
71
+ training-time confusion-matrix image URLs. When the group is saved, the
72
+ XAI results are persisted on it — heatmap/Grad-CAM/feature-map images are
73
+ uploaded to media storage (refs + publicUrl snapshots), never inline in
74
+ Mongo. Trained engine only; ignored for zero-shot. Each part degrades
75
+ independently into `xai.notes`, and a diagnosis-lambda failure (or a
76
+ missing `aiObjectDetectionDiagnosisUrl` config) degrades to `xai: null` —
77
+ XAI never fails the prediction. When the diagnosis pass's detections do
78
+ not align with this response's `predictions`, the index-bearing parts
79
+ (`class_scores`, `gradcam.per_detection`, `embeddings.points`) are
80
+ dropped with a note rather than mis-attributed.
81
+
82
+ **XAI backfill (`explain_only: true`).** Explains the EXISTING detections:
83
+ the detector is skipped, only the diagnosis lambda runs, and the XAI is
84
+ grafted onto the task's already-saved unconfirmed auto group of the
85
+ resolved model version — annotation `_id`s and provenance
86
+ (`original_label`, size gating, cluster ids) survive, so stored analysis
87
+ ledgers keep resolving. Falls through to a normal full inference when no
88
+ such group exists (or when `save` is false). Rejected with `400` for the
89
+ zero-shot engine (including the auto→zero-shot fallback): a VLM has no
90
+ trained internals to diagnose, and falling through would re-detect —
91
+ mutating the very group the caller asked to explain. This is how the
92
+ dashboard's per-group Explain action computes XAI for an already-annotated
93
+ task without churning its detections. (Session analysis itself does NOT
94
+ take explain options — explaining is a per-task, per-group action.)
95
+
96
+ **Saving.** With `save: true` (default) the mapped predictions become a
97
+ new `auto`, unconfirmed group (`time` = now, `usable` left to its default).
98
+ Only the unconfirmed auto group(s) from the SAME source are replaced
99
+ (trained: same `model_version`; zero-shot: same VLM) — auto groups from
100
+ other versions and every confirmed / manual group are kept. A re-run that
101
+ maps zero annotations never wipes a populated same-source group (fresh
102
+ XAI is grafted onto it instead). The write is a compare-and-swap on
103
+ `updatedAt` (5 attempts); losing every attempt answers `saved: false`.
104
+ `editor`, `edit_time` and `annotated` are restamped like a task update.
105
+
106
+ **Who calls it.** Admins / reps with a valid JWT or api-key. The tenant
107
+ key (`company_namespace`) is taken from the caller's token, not the body,
108
+ and the caller's own credential is forwarded to the lambdas. When
109
+ predictions are saved, the service emits `update-object-detection-task`
110
+ so the review canvas live-updates.
111
+
112
+ **Only `create` (POST) is allowed.** `find`, `get`, `update`, `patch`, and
113
+ `remove` all reject with `400`. Cross-frame fusion lives in
114
+ `ai-object-detection-session-analysis`. There is no stored document for
115
+ this service — the response is computed per call.
116
+ servers:
117
+ - url: https://sv.api.repzo.me
118
+ security:
119
+ - ApiKeyAuth: []
120
+ - JwtAuth: []
121
+ paths:
122
+ /ai-object-detection-inference:
123
+ post:
124
+ summary: Run detection on one task
125
+ description: |
126
+ Resolves the model, calls the detection lambda, maps predictions to
127
+ labels, optionally back-projects each detection into world coordinates
128
+ for AR session frames, and (by default) saves them as a fresh `auto`
129
+ annotation group on the task.
130
+ operationId: createAiObjectDetectionInference
131
+ requestBody:
132
+ required: true
133
+ content:
134
+ application/json:
135
+ schema:
136
+ $ref: "#/components/schemas/InferenceRequest"
137
+ responses:
138
+ "201":
139
+ description: The inference result, including mapped annotations and counts.
140
+ content:
141
+ application/json:
142
+ schema:
143
+ $ref: "#/components/schemas/InferenceResult"
144
+ "400":
145
+ description: |
146
+ `task_id` missing; the task has no media / public URL; the given
147
+ `model_version` / `model` was not found in the namespace; the task
148
+ is not assigned to a dataset with a default model (and no zero-shot
149
+ fallback applies); `explain_only` with the zero-shot engine; an
150
+ unsupported `zero_shot_model`; or zero-shot requested with no labels
151
+ in the namespace.
152
+ "404":
153
+ description: The task was not found in the caller's namespace.
154
+ "500":
155
+ description: The detection / zero-shot request failed, or the inference URL is not configured.
156
+ components:
157
+ securitySchemes:
158
+ ApiKeyAuth:
159
+ type: apiKey
160
+ in: header
161
+ name: api-key
162
+ description: |
163
+ Server-issued API key. Also accepted via the `x-api-key` header or the
164
+ `?apiKey=` query parameter as fallbacks.
165
+ JwtAuth:
166
+ type: apiKey
167
+ in: header
168
+ name: Authorization
169
+ description: |
170
+ Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
171
+ Obtained from `POST /authenticate` (admin / rep / client login).
172
+ schemas:
173
+ InferenceRequest:
174
+ type: object
175
+ required:
176
+ - task_id
177
+ properties:
178
+ task_id:
179
+ type: string
180
+ description: The `ai-object-detection-task` to infer on.
181
+ model_version:
182
+ type: string
183
+ description: |
184
+ `ai-object-detection-model-version` _id. When set, resolves the
185
+ lambda model + label order directly and bypasses the dataset
186
+ requirement.
187
+ model:
188
+ type: string
189
+ description: |
190
+ Object-detection model `_id` to infer with. Used directly as the
191
+ lambda model id when given.
192
+ engine:
193
+ type: string
194
+ enum: [auto, trained, zero_shot]
195
+ default: auto
196
+ description: |
197
+ Detector selection — see the intro. `zero_shot` needs no trained
198
+ model; `auto` falls back to zero-shot only when the classic dataset
199
+ path resolves nothing trained.
200
+ zero_shot_model:
201
+ type: string
202
+ enum:
203
+ - qwen/qwen3-vl-8b-instruct
204
+ - qwen/qwen3-vl-32b-instruct
205
+ - qwen/qwen3-vl-235b-a22b-instruct
206
+ - qwen/qwen2.5-vl-72b-instruct
207
+ description: |
208
+ Zero-shot VLM override. Defaults to the server-configured model,
209
+ else `qwen/qwen3-vl-8b-instruct`. Unsupported names are rejected
210
+ with `400`.
211
+ conf:
212
+ type: number
213
+ description: |
214
+ Detection confidence threshold. Defaults to the model's
215
+ `predict_settings[0].conf`, else `0.25`.
216
+ example: 0.25
217
+ iou:
218
+ type: number
219
+ minimum: 0
220
+ maximum: 1
221
+ description: |
222
+ NMS intersection-over-union threshold. Defaults to the model's
223
+ `predict_settings[0].iou`, else `0.7`.
224
+ example: 0.45
225
+ agnostic_nms:
226
+ type: boolean
227
+ description: |
228
+ Class-agnostic NMS. When true, non-max suppression merges
229
+ overlapping boxes across all classes instead of per-class. Defaults
230
+ to the model's `predict_settings[0].agnostic_nms`, else `false`.
231
+ Applies to the `trained` (YOLO) engine only; ignored for zero-shot.
232
+ example: true
233
+ save:
234
+ type: boolean
235
+ default: true
236
+ description: |
237
+ When true (default), the mapped predictions are persisted as a new
238
+ `auto` annotation group on the task (replacing only the unconfirmed
239
+ auto group from the same source).
240
+ explain:
241
+ type: boolean
242
+ default: false
243
+ description: |
244
+ Explainable-AI opt-in. When true (trained engine only), the
245
+ diagnosis lambda is called after the prediction, the response
246
+ gains an `xai` object and the saved annotation group persists it
247
+ (images as media refs + URL snapshots). Adds a second lambda
248
+ round-trip of latency; see the intro.
249
+ explain_parts:
250
+ type: array
251
+ items:
252
+ type: string
253
+ enum:
254
+ [
255
+ class_scores,
256
+ heatmap,
257
+ gradcam,
258
+ feature_maps,
259
+ embeddings,
260
+ confusion_matrix,
261
+ all,
262
+ ]
263
+ description: |
264
+ Subset of XAI parts to compute. Omit (or send empty / `["all"]`)
265
+ for every part. Unknown names are dropped server-side.
266
+ explain_topk:
267
+ type: integer
268
+ default: 5
269
+ minimum: 1
270
+ description: Candidate classes returned per detection in `class_scores`.
271
+ explain_gradcam_detections:
272
+ type: integer
273
+ default: 20
274
+ minimum: 0
275
+ description: |
276
+ Top-K cap on the per-detection Grad-CAM maps
277
+ (`xai.gradcam.per_detection`), highest-confidence first. Omitted,
278
+ the top 20 detections get their own map — each is a cheap
279
+ head-only backward, and the lambda sheds lowest-confidence maps
280
+ only if the response exceeds its ~6 MB payload limit. 0 disables
281
+ per-detection maps.
282
+ explain_embed_method:
283
+ type: string
284
+ enum: [auto, umap, tsne, pca]
285
+ default: auto
286
+ description: |
287
+ 2D projection for the embeddings scatter. `auto` picks by
288
+ detection count (UMAP from 10, t-SNE from 5, PCA below); an
289
+ explicit choice degrades down the umap -> tsne -> pca chain when
290
+ infeasible, and `xai.embeddings.method` reports what actually ran.
291
+ explain_only:
292
+ type: boolean
293
+ default: false
294
+ description: |
295
+ XAI backfill for the EXISTING detections. When true (implies
296
+ `explain`; trained engine + `save` only), the detector is skipped:
297
+ only the diagnosis lambda runs and its XAI is grafted onto the
298
+ task's already-saved unconfirmed auto group of the resolved model
299
+ version — annotation `_id`s and provenance stay untouched, and
300
+ the response carries `explained_existing: true`. Falls through to
301
+ a normal full inference (+explain) when no such group exists.
302
+ Rejected with `400` when the resolved engine is zero-shot
303
+ (explicit `engine: zero_shot`, or `auto` falling back to it) —
304
+ a VLM cannot be diagnosed, and falling through would re-detect
305
+ and replace the group instead of explaining it.
306
+ config:
307
+ $ref: "#/components/schemas/SceneMathConfig"
308
+ company_namespace:
309
+ type: array
310
+ items: { type: string }
311
+ description: Optional tenant namespace override for SDK callers. Not read by this endpoint — the namespace is taken from the caller's token.
312
+ SceneMathConfig:
313
+ type: object
314
+ description: |
315
+ Optional scene-engine tuning for AR session frames. Omit to use the
316
+ defaults. Inference honours the depth-sampling knobs, the dims
317
+ reclassifier and the size gate; any other `SceneMathConfig` key
318
+ (clustering, plane merge, shelf composition, point cloud, RANSAC — the
319
+ session-analysis knobs) is accepted and ignored here.
320
+ properties:
321
+ min_depth_m:
322
+ type: number
323
+ default: 0.05
324
+ description: Ignore depth samples below this (meters).
325
+ max_depth_m:
326
+ type: number
327
+ default: 6
328
+ description: Ignore depth samples above this (meters).
329
+ conf_threshold:
330
+ type: number
331
+ default: 1
332
+ description: Minimum ARKit depth-confidence (0/1/2) to keep a pixel.
333
+ front_percentile:
334
+ type: number
335
+ default: 30
336
+ description: Percentile of bbox depths to take (front-biased).
337
+ shelf_tolerance_m:
338
+ type: number
339
+ default: 0.3
340
+ description: Depth-gate slack beyond `distance_to_shelf_m`.
341
+ reclassify_labels:
342
+ type: boolean
343
+ default: false
344
+ description: Dims reclassifier master switch — re-label a placed detection to a sibling label (same label group) whose expected dims fit its measured size better.
345
+ reclassify_keep_dev:
346
+ type: number
347
+ default: 0.15
348
+ description: Fit deviation at/below which the detected label is kept outright.
349
+ reclassify_target_dev:
350
+ type: number
351
+ default: 0.15
352
+ description: A sibling must fit within this deviation to steal the detection.
353
+ reclassify_min_margin:
354
+ type: number
355
+ default: 0.06
356
+ description: Base margin the sibling's fit must beat the original's by.
357
+ reclassify_conf_margin_scale:
358
+ type: number
359
+ default: 0.5
360
+ description: "Margin × (1 + scale · detector_conf); 0 = ignore confidence."
361
+ reclassify_weight_scale:
362
+ type: number
363
+ default: 1.0
364
+ description: Weight of the size mismatch in the fit score.
365
+ reclassify_weight_aspect:
366
+ type: number
367
+ default: 0.5
368
+ description: Weight of the aspect mismatch in the fit score.
369
+ reclassify_min_depth_confidence:
370
+ type: number
371
+ default: 0.5
372
+ description: Skip detections whose depth confidence is below this.
373
+ size_gate:
374
+ type: boolean
375
+ default: true
376
+ description: "Size gate master switch — flag detections whose measured size exceeds the label's expected dims beyond the allowance (`size_rejected` + `size_reject_detail`)."
377
+ size_gate_dims_allowance:
378
+ type: number
379
+ default: 0.35
380
+ description: "Per-axis allowance: reject when measured w or h > (1 + this) × expected."
381
+ size_gate_area_allowance:
382
+ type: number
383
+ default: 0.35
384
+ description: "Area allowance: reject when measured w·h > (1 + this) × expected area."
385
+ size_gate_min_depth_confidence:
386
+ type: number
387
+ default: 0.5
388
+ description: Skip gating detections whose depth confidence is below this.
389
+ additionalProperties: true
390
+ Box:
391
+ type: object
392
+ description: "Normalized box stored YOLO-style: `x1` = center x, `y1` = center y, `x2` = width, `y2` = height."
393
+ properties:
394
+ x1: { type: number }
395
+ y1: { type: number }
396
+ x2: { type: number }
397
+ y2: { type: number }
398
+ Annotation:
399
+ type: object
400
+ description: A mapped detection (same schema as a task annotation). World-dim fields appear only for placed AR frames.
401
+ properties:
402
+ _id:
403
+ type: string
404
+ description: "Present only on an `explained_existing` response (the stored group's annotations are echoed)."
405
+ box:
406
+ $ref: "#/components/schemas/Box"
407
+ confidence:
408
+ type: number
409
+ label_id:
410
+ type: string
411
+ label_state:
412
+ type: string
413
+ enum: [auto, manual]
414
+ placed_box:
415
+ allOf:
416
+ - $ref: "#/components/schemas/Box"
417
+ description: Snapshot of the box geometry placement ran for (set whenever placement was attempted on a session frame).
418
+ ignore_reason:
419
+ type: string
420
+ enum:
421
+ - no_pose
422
+ - no_intrinsics
423
+ - no_depth
424
+ - empty_depth_region
425
+ - insufficient_depth_pixels
426
+ - behind_shelf
427
+ description: |
428
+ Present when an AR session-frame detection could NOT be placed in
429
+ world coordinates — names the exact failing stage. Unset for placed
430
+ detections and for non-session tasks.
431
+ world_position:
432
+ type: object
433
+ description: Back-projected centroid in world coordinates, metres (placed only).
434
+ properties:
435
+ x: { type: number }
436
+ y: { type: number }
437
+ z: { type: number }
438
+ world_size:
439
+ type: object
440
+ description: "Physical front-face size, centimetres (`w` = world-horizontal, `h` = vertical; placed only)."
441
+ properties:
442
+ w: { type: number }
443
+ h: { type: number }
444
+ depth_at_center:
445
+ type: number
446
+ description: Sampled depth at the box (meters, placed only).
447
+ depth_confidence:
448
+ type: number
449
+ description: Normalized 0..1 depth confidence (placed only).
450
+ placement_confidence:
451
+ type: number
452
+ description: detection_conf × depth_conf × tracking (placed only).
453
+ original_label:
454
+ type: string
455
+ description: Set when the dims reclassifier moved this detection to a sibling label — the label the detector originally produced.
456
+ reclassification_reason:
457
+ type: string
458
+ enum: [dims_match_sibling, group_consensus, manual]
459
+ size_rejected:
460
+ type: boolean
461
+ description: Flagged by the size gate when the measured size exceeded the label's expected dims beyond the allowance.
462
+ size_reject_detail:
463
+ type: object
464
+ properties:
465
+ exceeded: { type: string, enum: [width, height, area] }
466
+ measured_w_cm: { type: number }
467
+ measured_h_cm: { type: number }
468
+ expected_w_cm: { type: number }
469
+ expected_h_cm: { type: number }
470
+ ratio:
471
+ type: number
472
+ description: measured / (expected × (1 + allowance)) for the tripped check.
473
+ cluster_id:
474
+ type: string
475
+ description: "Present only on an `explained_existing` response — the analysis object this stored detection was clustered into."
476
+ Prediction:
477
+ type: object
478
+ description: |
479
+ One raw detector result (`images[0].results[]` of the Ultralytics
480
+ lambda; zero-shot replies are normalized to the same shape with
481
+ `class: -1`). Passed through unchanged.
482
+ properties:
483
+ name: { type: string }
484
+ class: { type: integer }
485
+ confidence: { type: number }
486
+ box:
487
+ type: object
488
+ properties:
489
+ x1:
490
+ { type: number, description: Pixel corner on the original image. }
491
+ y1: { type: number }
492
+ x2: { type: number }
493
+ y2: { type: number }
494
+ x_center: { type: number, description: Normalized 0..1. }
495
+ y_center: { type: number }
496
+ width: { type: number }
497
+ height: { type: number }
498
+ additionalProperties: true
499
+ InferenceResult:
500
+ type: object
501
+ properties:
502
+ task_id:
503
+ type: string
504
+ engine:
505
+ type: string
506
+ enum: [trained, zero_shot]
507
+ description: The detector that actually ran.
508
+ zero_shot_model:
509
+ type: string
510
+ nullable: true
511
+ description: The VLM used, when `engine` is `zero_shot`.
512
+ model:
513
+ type: string
514
+ nullable: true
515
+ description: "`null` for zero-shot runs."
516
+ model_version:
517
+ type: string
518
+ nullable: true
519
+ image_url:
520
+ type: string
521
+ shape:
522
+ type: array
523
+ items: { type: number }
524
+ nullable: true
525
+ description: |
526
+ [height, width] of the inferred image (detector-reported). `null`
527
+ on an `explained_existing` response — no detector ran.
528
+ conf:
529
+ type: number
530
+ saved:
531
+ type: boolean
532
+ explained_existing:
533
+ type: boolean
534
+ description: |
535
+ Present (true) when `explain_only` grafted the XAI onto the
536
+ existing annotation group — no detector ran: `predictions` is
537
+ empty, `shape` / `placed_count` / `unplaced_count` / `unmapped`
538
+ are null, and `annotations` echoes the existing group's
539
+ annotations.
540
+ annotations_count:
541
+ type: number
542
+ placed_count:
543
+ type: number
544
+ nullable: true
545
+ description: Annotations placed in world coordinates (0 for non-session tasks).
546
+ unplaced_count:
547
+ type: number
548
+ nullable: true
549
+ description: Annotations left 2D-only — each carries an `ignore_reason`.
550
+ unmapped:
551
+ type: number
552
+ nullable: true
553
+ description: Predictions that could not be mapped to a label.
554
+ annotations:
555
+ type: array
556
+ items:
557
+ $ref: "#/components/schemas/Annotation"
558
+ predictions:
559
+ type: array
560
+ items:
561
+ $ref: "#/components/schemas/Prediction"
562
+ description: Raw lambda predictions (passthrough).
563
+ xai:
564
+ nullable: true
565
+ allOf:
566
+ - $ref: "#/components/schemas/Xai"
567
+ description: |
568
+ Raw DIAGNOSIS-lambda Explainable-AI payload (passthrough) —
569
+ present only when the request set `explain` / `explain_only` on
570
+ the trained engine and the diagnosis lambda succeeded, else `null`.
571
+ task:
572
+ type: object
573
+ nullable: true
574
+ description: "The saved `ai-object-detection-task` document when the group (or grafted XAI) was persisted, else `null`."
575
+ Xai:
576
+ type: object
577
+ description: |
578
+ Explainable-AI results, all derived from the SAME forward pass as the
579
+ prediction. Any part can be missing — its failure reason is then
580
+ appended to `notes` (XAI never fails the prediction). `class_scores`
581
+ and `embeddings.points` reference detections by `index` into
582
+ `predictions` and are self-describing (box/name/confidence), because
583
+ unmapped predictions never become annotations.
584
+ properties:
585
+ parts:
586
+ type: array
587
+ items: { type: string }
588
+ description: The parts that were requested.
589
+ class_names:
590
+ type: object
591
+ additionalProperties: { type: string }
592
+ description: "YOLO class index (stringified) to class name, from the model weights."
593
+ notes:
594
+ type: array
595
+ items: { type: string }
596
+ description: Human-readable reasons for any part that could not be produced.
597
+ class_scores:
598
+ type: array
599
+ items:
600
+ $ref: "#/components/schemas/XaiClassScore"
601
+ heatmap:
602
+ $ref: "#/components/schemas/XaiHeatmap"
603
+ gradcam:
604
+ $ref: "#/components/schemas/XaiGradcam"
605
+ feature_maps:
606
+ type: array
607
+ items:
608
+ $ref: "#/components/schemas/XaiFeatureMap"
609
+ embeddings:
610
+ $ref: "#/components/schemas/XaiEmbeddings"
611
+ confusion_matrix:
612
+ nullable: true
613
+ allOf:
614
+ - $ref: "#/components/schemas/XaiConfusionMatrix"
615
+ XaiClassScore:
616
+ type: object
617
+ description: |
618
+ Per-detection class-score inspection — the full class-score row of the
619
+ pre-NMS anchor that produced the detection, top-k. A near-tie between
620
+ the top candidates means the model could not distinguish those classes.
621
+ properties:
622
+ index:
623
+ type: integer
624
+ description: "Position of the detection in `predictions`."
625
+ class:
626
+ type: integer
627
+ description: Predicted YOLO class index.
628
+ name:
629
+ type: string
630
+ description: Predicted class name.
631
+ confidence:
632
+ type: number
633
+ box:
634
+ type: object
635
+ description: Detection box in original-image pixels (xyxy).
636
+ properties:
637
+ x1: { type: number }
638
+ y1: { type: number }
639
+ x2: { type: number }
640
+ y2: { type: number }
641
+ candidates:
642
+ type: array
643
+ description: Top-k candidate classes of the winning anchor, best first.
644
+ items:
645
+ type: object
646
+ properties:
647
+ class: { type: integer }
648
+ name: { type: string }
649
+ score: { type: number }
650
+ match_iou:
651
+ type: number
652
+ description: |
653
+ IoU between the detection box and the matched pre-NMS anchor box —
654
+ values near 1 mean the anchor match is exact.
655
+ XaiHeatmap:
656
+ type: object
657
+ description: |
658
+ EigenCAM activation heatmap ("where did the model look") sized to the
659
+ original image aspect. The PNG is RGBA with alpha = activation, so the
660
+ dashboard overlays it directly on the photo with an opacity slider.
661
+ properties:
662
+ png_base64:
663
+ type: string
664
+ description: Base64 RGBA PNG (JET-colored, alpha-carrying).
665
+ method:
666
+ type: string
667
+ enum: [eigencam]
668
+ layers:
669
+ type: string
670
+ description: Which activations were used (detect-head inputs, averaged).
671
+ width: { type: integer }
672
+ height: { type: integer }
673
+ XaiGradcam:
674
+ type: object
675
+ description: |
676
+ TRUE gradient-based Grad-CAM ("what evidence drove the detections").
677
+ A second, gradient-enabled forward pass backpropagates the summed
678
+ confident class scores and weights each activation channel by its
679
+ pooled gradient — class-discriminative, unlike EigenCAM. Same overlay
680
+ contract: RGBA PNG with alpha = activation. Computed by the diagnosis
681
+ lambda only (the backward pass needs its memory budget).
682
+ properties:
683
+ png_base64:
684
+ type: string
685
+ description: Base64 RGBA PNG (JET-colored, alpha-carrying).
686
+ method:
687
+ type: string
688
+ enum: [gradcam]
689
+ layers:
690
+ type: string
691
+ target:
692
+ type: string
693
+ description: What was backpropagated (the Grad-CAM objective).
694
+ imgsz:
695
+ type: integer
696
+ description: |
697
+ Input size of the gradient pass — capped (default 960) so the
698
+ backward pass stays within the diagnosis lambda's memory,
699
+ independent of the inference imgsz.
700
+ width: { type: integer }
701
+ height: { type: integer }
702
+ per_detection:
703
+ type: array
704
+ description: |
705
+ One map PER DETECTION ("why did the model call THIS box this
706
+ class") — each backpropagates ONLY the matched anchor's score for
707
+ the detection's own class, with pixel-wise (LayerCAM-style)
708
+ gradient weighting so the evidence localizes to that instance
709
+ instead of lighting up every look-alike. The top
710
+ `explain_gradcam_detections` detections by confidence get a map
711
+ (default 20; `per_detection_note` reports truncation; the lambda
712
+ also sheds lowest-confidence maps if the response exceeds its
713
+ ~6 MB payload limit). `index` references the `predictions` array
714
+ like class_scores.
715
+ items:
716
+ type: object
717
+ properties:
718
+ index: { type: integer }
719
+ class: { type: integer }
720
+ name: { type: string }
721
+ confidence: { type: number }
722
+ box:
723
+ type: object
724
+ description: Detection box in original-image pixels (xyxy).
725
+ properties:
726
+ x1: { type: number }
727
+ y1: { type: number }
728
+ x2: { type: number }
729
+ y2: { type: number }
730
+ match_iou:
731
+ type: number
732
+ description: |
733
+ IoU between the detection and the anchor of the gradient
734
+ pass it was matched to — low values mean the (size-capped)
735
+ gradient pass barely saw this object and the map is weak
736
+ evidence.
737
+ png_base64:
738
+ type: string
739
+ description: Base64 RGBA PNG (JET-colored, alpha-carrying).
740
+ method: { type: string, enum: [gradcam] }
741
+ layers: { type: string }
742
+ target: { type: string }
743
+ width: { type: integer }
744
+ height: { type: integer }
745
+ per_detection_note:
746
+ type: string
747
+ description: Present when detections were truncated to the top-K.
748
+ XaiFeatureMap:
749
+ type: object
750
+ description: One per-stage feature-map grid from ultralytics visualize=True.
751
+ properties:
752
+ stage:
753
+ type: string
754
+ description: "Network stage, e.g. `stage12_C2f`."
755
+ jpg_base64:
756
+ type: string
757
+ description: Base64 JPEG of the stage's channel grid, downscaled.
758
+ XaiEmbeddings:
759
+ type: object
760
+ description: |
761
+ Per-detection embeddings (pooled from the finest detect-head input
762
+ scale) projected to 2D for a scatter plot — detections the model sees
763
+ as similar land near each other.
764
+ properties:
765
+ method:
766
+ type: string
767
+ enum: [umap, tsne, pca, none]
768
+ description: |
769
+ The projection that actually ran. With `explain_embed_method:
770
+ auto` — UMAP for 10+ detections, t-SNE for 5-9, PCA for 2-4,
771
+ `none` for a single point; explicit requests degrade down the
772
+ umap -> tsne -> pca chain when infeasible.
773
+ requested_method:
774
+ type: string
775
+ enum: [auto, umap, tsne, pca]
776
+ description: What the caller asked for (differs from `method` on fallback).
777
+ layer:
778
+ type: string
779
+ embedding_dim:
780
+ type: integer
781
+ description: Dimensionality of the pooled embedding before projection.
782
+ points:
783
+ type: array
784
+ items:
785
+ $ref: "#/components/schemas/XaiEmbeddingPoint"
786
+ XaiEmbeddingPoint:
787
+ type: object
788
+ properties:
789
+ index:
790
+ type: integer
791
+ description: "Position of the detection in `predictions`."
792
+ class: { type: integer }
793
+ name: { type: string }
794
+ confidence: { type: number }
795
+ x:
796
+ type: number
797
+ description: Projected coordinate, min-max normalized to [0, 1].
798
+ y:
799
+ type: number
800
+ description: Projected coordinate, min-max normalized to [0, 1].
801
+ XaiConfusionMatrix:
802
+ type: object
803
+ description: |
804
+ Training-time confusion-matrix images of the model version (computed on
805
+ the validation set during training — inference has no ground truth to
806
+ build one from). URLs point at the stored media artifacts.
807
+ properties:
808
+ url:
809
+ type: string
810
+ nullable: true
811
+ description: Rendered confusion-matrix image (absolute counts).
812
+ normalized_url:
813
+ type: string
814
+ nullable: true
815
+ description: Row-normalized variant.
816
+ source:
817
+ type: string
818
+ enum: [training_artifacts]