repzo 1.0.292 → 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 +4530 -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 +5156 -1
@@ -0,0 +1,1306 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Repzo API - AI Object Detection Session Analysis
4
+ version: 1.0.0
5
+ description: |
6
+ **Session analysis** is the cross-frame fusion stage of the Object Detection
7
+ Sessions feature. Given a capture `session` (a stream of frames materialized
8
+ as `ai-object-detection-task` rows) plus a `model_version`, it produces a
9
+ versioned, audit-friendly analysis document with the fused 3D objects on the
10
+ shelf and a kept/ignored breakdown per label.
11
+
12
+ **Asynchronous.** `create` is non-blocking: it opens a `pending` analysis
13
+ document and returns it **immediately** (with a `message`), then runs the
14
+ heavy work in the background. Inferring every frame can take a while, so the
15
+ client should not wait on the request — poll the returned document (or list
16
+ analyses for the session) until `status` flips to `success`/`failed`. On
17
+ completion the backend emits `inferred-od-session`, which notifies reps
18
+ (`update-object-detection-task`) so the dashboard can refresh.
19
+
20
+ **What the background job does.**
21
+ 1. Loads the session and all its tasks, sorted in CAPTURE order
22
+ (`frame_meta.frame_id`, then device timestamp).
23
+ 2. Infers any task not yet placed — delegates to the per-task
24
+ `ai-object-detection-inference` path with the given `model_version`
25
+ (or the zero-shot VLM when `engine` requests it / nothing is trained).
26
+ Already-inferred tasks are reused as-is.
27
+ 3. Collects annotations FRAME BY FRAME; un-placeable ones become
28
+ `unplaced` ledger entries carrying the placement failure reason.
29
+ 4. **MERGE (frame walk)** — walks the frames in capture order: detections
30
+ matching an object seen in an EARLIER frame (same label, within the
31
+ merge radius, one per object per frame) merge into it as
32
+ re-observations; the rest spawn new objects. Same-frame drops happen
33
+ only for explicit quality edge cases (duplicate box, same world spot,
34
+ low confidence, implausible size) — a frame's own detection count is
35
+ authoritative, so two adjacent identical facings stay distinct.
36
+ 5. **ARBITRATION** — scores every member of a cluster with the weighted
37
+ 6-factor view score (detector confidence, depth confidence, tracking,
38
+ box centering, box size, label agreement) and picks the best VIEW as
39
+ the winner; the full per-member breakdown is persisted.
40
+ 6. Persists `objects[]` (kept + merged), `concluded_labels[]` (per-label
41
+ kept/merged/ignored/dropped counts), the per-detection `detections[]`
42
+ ledger and the `funnel` summary, then emits `inferred-od-session`.
43
+
44
+ **Storage & multi-tenancy.** Each run is a new document in the
45
+ `ai.objectDetectionSessionAnalyses` collection, scoped by
46
+ `company_namespace` (injected from the caller's token) and soft-deleted via
47
+ `disabled: true`. The run is opened with `status: pending`, flips to
48
+ `in_progress` when the background job starts, then to `success` (or
49
+ `failed` with `_errors`) on completion. References
50
+ `aiObjectDetectionSession`, `ai.objectDetectionModelVersions`, and
51
+ `sv.aiObjectDetectionLabels`.
52
+
53
+ **Who calls it.** Admins / reps with a valid JWT. `create` triggers analysis
54
+ (or, with `recompute: compose`, synchronously recomposes the shelf structure
55
+ of an existing analysis); `find` / `get` are standard reads; `update` (PUT)
56
+ is a raw document update with a server-stamped `editor`; `patch` is the bulk
57
+ `writeQuery` update over the find filters; `remove` soft-deletes.
58
+
59
+ **Population.** `?populatedKeys[]=` supports `session`, `model_version`,
60
+ `objects.label_id`, `objects.winning_image_media` and
61
+ `concluded_labels.label_id`; every key is populated IN PLACE (the id is
62
+ replaced by the referenced document).
63
+ servers:
64
+ - url: https://sv.api.repzo.me
65
+ security:
66
+ - ApiKeyAuth: []
67
+ - JwtAuth: []
68
+ paths:
69
+ /ai-object-detection-session-analysis:
70
+ get:
71
+ summary: List session analyses
72
+ description: Paginated list of analysis documents, newest first.
73
+ operationId: findAiObjectDetectionSessionAnalysis
74
+ parameters:
75
+ - in: query
76
+ name: _id
77
+ description: Filter by document `_id`.
78
+ schema:
79
+ oneOf:
80
+ - type: string
81
+ - type: array
82
+ items: { type: string }
83
+ - in: query
84
+ name: session
85
+ description: Filter by the parent session `_id` (one or many).
86
+ schema:
87
+ oneOf:
88
+ - type: string
89
+ - type: array
90
+ items: { type: string }
91
+ - in: query
92
+ name: model_version
93
+ description: Filter by the model version used (one or many).
94
+ schema:
95
+ oneOf:
96
+ - type: string
97
+ - type: array
98
+ items: { type: string }
99
+ - in: query
100
+ name: status
101
+ description: Filter by run status (one or many).
102
+ schema:
103
+ oneOf:
104
+ - type: string
105
+ enum: [pending, in_progress, success, failed]
106
+ - type: array
107
+ items:
108
+ type: string
109
+ enum: [pending, in_progress, success, failed]
110
+ - in: query
111
+ name: creator._id
112
+ description: Filter by the id of the rep/admin who triggered the analysis.
113
+ schema:
114
+ oneOf:
115
+ - type: string
116
+ - type: array
117
+ items: { type: string }
118
+ - in: query
119
+ name: from_createdAt
120
+ description: Return analyses created at/after this Unix timestamp (ms).
121
+ schema: { type: number }
122
+ - in: query
123
+ name: to_createdAt
124
+ description: Return analyses created at/before this Unix timestamp (ms).
125
+ schema: { type: number }
126
+ - in: query
127
+ name: from_updatedAt
128
+ description: Cursor — analyses with `updatedAt` greater than this Unix timestamp (ms).
129
+ schema: { type: number }
130
+ - in: query
131
+ name: to_updatedAt
132
+ description: Cursor — analyses with `updatedAt` less than this Unix timestamp (ms).
133
+ schema: { type: number }
134
+ - in: query
135
+ name: disabled
136
+ description: Include disabled (soft-deleted) analyses. Defaults to `false`.
137
+ schema: { type: boolean, default: false }
138
+ - in: query
139
+ name: per_page
140
+ description: Page size. Defaults to the server's configured pagination limit.
141
+ schema: { type: integer, minimum: 1, maximum: 500 }
142
+ example: 50
143
+ - in: query
144
+ name: page
145
+ description: 1-indexed page number.
146
+ schema: { type: integer, minimum: 1 }
147
+ example: 1
148
+ - in: query
149
+ name: sort
150
+ description: Field to sort by. Defaults to `_id`.
151
+ schema: { type: string, default: _id }
152
+ - in: query
153
+ name: sortPageOrder
154
+ description: Sort direction. Defaults to descending.
155
+ schema: { type: string, enum: [asc, dsc], default: dsc }
156
+ - in: query
157
+ name: populatedKeys
158
+ description: |
159
+ Embed referenced documents (populated in place). Supported:
160
+ `session`, `model_version`, `objects.label_id`,
161
+ `objects.winning_image_media`, `concluded_labels.label_id`.
162
+ schema:
163
+ type: array
164
+ items:
165
+ type: string
166
+ enum:
167
+ - session
168
+ - model_version
169
+ - objects.label_id
170
+ - objects.winning_image_media
171
+ - concluded_labels.label_id
172
+ responses:
173
+ "200":
174
+ description: Paginated list of analysis documents (standard envelope).
175
+ content:
176
+ application/json:
177
+ schema:
178
+ $ref: "#/components/schemas/AnalysisFindResult"
179
+ post:
180
+ summary: Start cross-frame analysis on a session (async)
181
+ description: |
182
+ Opens a `pending` analysis document and returns it immediately, then
183
+ infers any not-yet-placed tasks, clusters the kept world detections, and
184
+ arbitrates a winner per cluster **in the background**. Poll the returned
185
+ document (or list analyses for the session) until `status` becomes
186
+ `success`/`failed`.
187
+
188
+ **Model-version semantics.** When `model_version` is given, tasks whose
189
+ auto annotations came from a *different* version are re-inferred with
190
+ the requested one; tasks already inferred by the same version are
191
+ reused. Without `model_version`, any existing auto annotations are
192
+ reused as-is.
193
+
194
+ **Concurrency & staleness.** Only one live run per session: creating an
195
+ analysis while another is `pending`/`in_progress` returns `400`. Zombie
196
+ runs (a server restart lost the background job) are detected via
197
+ `heartbeat_at` — any run without a heartbeat for 15 minutes is
198
+ automatically marked `failed` by the next create, so a stuck run never
199
+ blocks the session permanently.
200
+
201
+ **Side effects on success.** The parent session's `status` transitions
202
+ `infer_in_progress → inferred` (or `failed`), its `detections_count` /
203
+ `objects_count` aggregates are refreshed, each contributing task
204
+ annotation gets its `cluster_id` set to the concluded object's `_id`
205
+ (stale links from earlier runs are cleared first), and best-effort
206
+ scene outputs are stored: the dominant plane (`scene.plane`), the
207
+ RANSAC shelf planes (`scene.planes`), and the voxel point cloud
208
+ (`scene.point_cloud`, packed blob in `bin` media — disable with
209
+ `config.build_point_cloud: false`).
210
+ operationId: createAiObjectDetectionSessionAnalysis
211
+ requestBody:
212
+ required: true
213
+ content:
214
+ application/json:
215
+ schema:
216
+ $ref: "#/components/schemas/AnalysisRequest"
217
+ responses:
218
+ "201":
219
+ description: |
220
+ The freshly-created analysis document with `status: pending` plus a
221
+ `message`. The heavy work runs in the background — the fused
222
+ `objects[]` / `concluded_labels[]` are filled in later, once the
223
+ status flips to `success`.
224
+ content:
225
+ application/json:
226
+ schema:
227
+ allOf:
228
+ - $ref: "#/components/schemas/SessionAnalysis"
229
+ - type: object
230
+ properties:
231
+ message:
232
+ type: string
233
+ description: Human-readable note that the job started in the background.
234
+ "400":
235
+ description: |
236
+ The session has no frames/tasks, or a live (non-stale) analysis is
237
+ already running for it — wait for that run to finish or for the
238
+ 15-minute stale watchdog to fail it.
239
+ patch:
240
+ summary: Bulk-update analyses
241
+ description: |
242
+ Bulk update via the `patch-action` writeQuery shape. The rows to
243
+ update are selected by the SAME query filters as the list endpoint
244
+ (`_id`, `session`, `model_version`, `status`, `creator._id`,
245
+ `from_/to_createdAt`, `from_/to_updatedAt`, `disabled`). Returns
246
+ `{ nFound, nModified }`.
247
+ operationId: patchAiObjectDetectionSessionAnalysis
248
+ parameters:
249
+ - in: query
250
+ name: _id
251
+ schema:
252
+ oneOf:
253
+ - type: string
254
+ - type: array
255
+ items: { type: string }
256
+ - in: query
257
+ name: session
258
+ schema:
259
+ oneOf:
260
+ - type: string
261
+ - type: array
262
+ items: { type: string }
263
+ - in: query
264
+ name: model_version
265
+ schema:
266
+ oneOf:
267
+ - type: string
268
+ - type: array
269
+ items: { type: string }
270
+ - in: query
271
+ name: status
272
+ schema:
273
+ oneOf:
274
+ - type: string
275
+ - type: array
276
+ items: { type: string }
277
+ - in: query
278
+ name: creator._id
279
+ schema:
280
+ oneOf:
281
+ - type: string
282
+ - type: array
283
+ items: { type: string }
284
+ - in: query
285
+ name: from_createdAt
286
+ schema: { type: number }
287
+ - in: query
288
+ name: to_createdAt
289
+ schema: { type: number }
290
+ - in: query
291
+ name: from_updatedAt
292
+ schema: { type: number }
293
+ - in: query
294
+ name: to_updatedAt
295
+ schema: { type: number }
296
+ - in: query
297
+ name: disabled
298
+ schema: { type: boolean }
299
+ requestBody:
300
+ required: true
301
+ content:
302
+ application/json:
303
+ schema:
304
+ $ref: "#/components/schemas/PatchActionBody"
305
+ responses:
306
+ "200":
307
+ description: Bulk-update result.
308
+ content:
309
+ application/json:
310
+ schema:
311
+ $ref: "#/components/schemas/PatchActionResult"
312
+ /ai-object-detection-session-analysis/{id}:
313
+ get:
314
+ summary: Get one session analysis
315
+ operationId: getAiObjectDetectionSessionAnalysis
316
+ parameters:
317
+ - in: path
318
+ name: id
319
+ required: true
320
+ schema: { type: string }
321
+ - in: query
322
+ name: populatedKeys
323
+ description: Same population keys as the list endpoint (populated in place).
324
+ schema:
325
+ type: array
326
+ items:
327
+ type: string
328
+ enum:
329
+ - session
330
+ - model_version
331
+ - objects.label_id
332
+ - objects.winning_image_media
333
+ - concluded_labels.label_id
334
+ responses:
335
+ "200":
336
+ description: The analysis document.
337
+ content:
338
+ application/json:
339
+ schema:
340
+ $ref: "#/components/schemas/SessionAnalysis"
341
+ put:
342
+ summary: Update one session analysis
343
+ description: |
344
+ Raw document update (`updateOne` with the body; `editor` is
345
+ server-stamped). Intended for minimal edits such as `disabled: true`
346
+ (soft-delete) — overwriting run output fields would corrupt the
347
+ analysis. The tenant key is derived from the caller's session.
348
+ operationId: updateAiObjectDetectionSessionAnalysis
349
+ parameters:
350
+ - in: path
351
+ name: id
352
+ required: true
353
+ schema: { type: string }
354
+ requestBody:
355
+ required: true
356
+ content:
357
+ application/json:
358
+ schema:
359
+ $ref: "#/components/schemas/AnalysisUpdateBody"
360
+ responses:
361
+ "200":
362
+ description: The analysis document after the update is applied.
363
+ content:
364
+ application/json:
365
+ schema:
366
+ $ref: "#/components/schemas/SessionAnalysis"
367
+ "404":
368
+ description: No analysis with that id in the caller's namespace.
369
+ delete:
370
+ summary: Soft-delete one session analysis
371
+ operationId: removeAiObjectDetectionSessionAnalysis
372
+ parameters:
373
+ - in: path
374
+ name: id
375
+ required: true
376
+ schema: { type: string }
377
+ responses:
378
+ "200":
379
+ description: The analysis document after `disabled` is set to true.
380
+ content:
381
+ application/json:
382
+ schema:
383
+ $ref: "#/components/schemas/SessionAnalysis"
384
+ components:
385
+ securitySchemes:
386
+ ApiKeyAuth:
387
+ type: apiKey
388
+ in: header
389
+ name: api-key
390
+ description: |
391
+ Server-issued API key. Also accepted via the `x-api-key` header or the
392
+ `?apiKey=` query parameter as fallbacks.
393
+ JwtAuth:
394
+ type: apiKey
395
+ in: header
396
+ name: Authorization
397
+ description: |
398
+ Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
399
+ Obtained from `POST /authenticate` (admin / rep / client login).
400
+ schemas:
401
+ AnalysisRequest:
402
+ type: object
403
+ required:
404
+ - session
405
+ properties:
406
+ session:
407
+ type: string
408
+ description: The `ai-object-detection-session` _id to analyze.
409
+ model_version:
410
+ type: string
411
+ description: |
412
+ `ai.objectDetectionModelVersions` _id used to infer any
413
+ not-yet-placed tasks.
414
+ model:
415
+ type: string
416
+ description: Optional object-detection model `_id` override.
417
+ engine:
418
+ type: string
419
+ enum: [auto, trained, zero_shot]
420
+ default: auto
421
+ description: |
422
+ Detector for tasks inferred during this run. `trained` uses the
423
+ Ultralytics lambda; `zero_shot` uses a hosted Qwen-VL prompted with
424
+ the namespace's label names (for sessions with no trained model
425
+ yet); `auto` (default) tries trained and falls back to zero-shot
426
+ when nothing trained resolves.
427
+ zero_shot_model:
428
+ type: string
429
+ description: |
430
+ Zero-shot VLM override, e.g. `qwen/qwen3-vl-8b-instruct`. Must be
431
+ one of the supported models (see `ai-object-detection-inference`).
432
+ conf:
433
+ type: number
434
+ description: Optional detector confidence override.
435
+ iou:
436
+ type: number
437
+ minimum: 0
438
+ maximum: 1
439
+ description: Optional detector NMS IoU override.
440
+ agnostic_nms:
441
+ type: boolean
442
+ description: Optional class-agnostic NMS override.
443
+ force_reinference:
444
+ type: boolean
445
+ default: false
446
+ description: |
447
+ Re-infer EVERY task even if it already has an auto annotation group
448
+ for this `model_version`. Normal reuse keys on the group's stored
449
+ `model_version`; if an earlier run stamped that version but the boxes
450
+ were produced by a different net (stale provenance), those boxes
451
+ would be reused forever. Set this once to overwrite them.
452
+ inference_concurrency:
453
+ type: integer
454
+ minimum: 1
455
+ maximum: 10
456
+ default: 4
457
+ description: |
458
+ Parallel inference calls for this run. Each call runs on its own
459
+ lambda instance (~30 s/frame on CPU), so a session's inference
460
+ wall-clock divides by this. `1` restores serial behavior; capped at
461
+ 10 until GPU inference lands. May also ride inside `config` (the
462
+ dashboard dialog and category model_settings put it there); this
463
+ top-level field wins when both are present.
464
+ recompute:
465
+ type: string
466
+ enum: [compose]
467
+ description: |
468
+ STAGE RECOMPUTE — "compose" re-runs ONLY the shelf composition of
469
+ the existing analysis given in `analysis`, synchronously and in
470
+ place, using the `shelf_*` keys from `config` merged over the
471
+ stored config (non-shelf keys in `config` are IGNORED on this
472
+ path — accepting them would update their stage fingerprints
473
+ without recomputing those stages). Fused objects, ledger and funnel are untouched
474
+ (their config slices didn't change). Milliseconds instead of a
475
+ full run — the path a shelf-knob change takes. Returns the
476
+ updated analysis document directly (no background job).
477
+ analysis:
478
+ type: string
479
+ description: The existing analysis _id to recompute (required with `recompute`).
480
+ config:
481
+ $ref: "#/components/schemas/SceneMathConfig"
482
+ company_namespace:
483
+ type: array
484
+ items: { type: string }
485
+ description: Optional tenant namespace override for SDK callers.
486
+ AnalysisUpdateBody:
487
+ type: object
488
+ description: |
489
+ Body for `PUT /{id}` — any stored field; `editor` is server-stamped.
490
+ Set `disabled: true` to soft-delete.
491
+ properties:
492
+ disabled: { type: boolean }
493
+ config:
494
+ $ref: "#/components/schemas/SceneMathConfig"
495
+ status:
496
+ type: string
497
+ enum: [pending, in_progress, success, failed]
498
+ additionalProperties: true
499
+ PatchActionWrite:
500
+ type: object
501
+ properties:
502
+ key: { type: string }
503
+ command:
504
+ type: string
505
+ enum: [set, addToSet, pull]
506
+ value: {}
507
+ PatchActionBody:
508
+ type: object
509
+ description: Bulk-update body. `writeQuery[]` describes the writes applied to filtered rows.
510
+ required: [writeQuery]
511
+ properties:
512
+ writeQuery:
513
+ type: array
514
+ items:
515
+ $ref: "#/components/schemas/PatchActionWrite"
516
+ PatchActionResult:
517
+ type: object
518
+ properties:
519
+ nFound: { type: number }
520
+ nModified: { type: number }
521
+ AnalysisFindResult:
522
+ type: object
523
+ description: Standard paginated result envelope.
524
+ properties:
525
+ data:
526
+ type: array
527
+ items:
528
+ $ref: "#/components/schemas/SessionAnalysis"
529
+ total_result: { type: number }
530
+ current_count: { type: number }
531
+ total_pages: { type: number }
532
+ current_page: { type: number }
533
+ per_page: { type: number }
534
+ first_page_url: { type: string }
535
+ last_page_url: { type: string }
536
+ next_page_url: { type: string, nullable: true }
537
+ prev_page_url: { type: string, nullable: true }
538
+ path: { type: string }
539
+ SceneMathConfig:
540
+ type: object
541
+ description: Optional clustering / back-projection tuning. Omit for defaults.
542
+ properties:
543
+ inference_concurrency:
544
+ type: integer
545
+ minimum: 1
546
+ maximum: 10
547
+ default: 4
548
+ description: |
549
+ Run-level alias of the top-level `inference_concurrency`, carried
550
+ inside `config` by the dashboard's analyze dialog and by category
551
+ `model_settings` (the category auto-analysis forwards only
552
+ `config`). The top-level field wins when both are present.
553
+ cluster_eps_m:
554
+ type: number
555
+ default: 0.08
556
+ description: Base world distance to merge detections into one object.
557
+ class_agree_bonus_m:
558
+ type: number
559
+ default: 0.02
560
+ description: Extra merge radius when labels match.
561
+ block_same_frame:
562
+ type: boolean
563
+ default: true
564
+ description: |
565
+ A scene object absorbs at most ONE detection per frame during the
566
+ walk — each frame's detection count stays authoritative for how
567
+ many objects exist there.
568
+ same_frame_iou_thresh:
569
+ type: number
570
+ default: 0.92
571
+ description: |
572
+ Same-frame quality gate — two same-label boxes with 2D IoU at/above
573
+ this are a detector double-fire; the weaker one is dropped
574
+ (`duplicate_box_in_frame`).
575
+ same_frame_min_separation_m:
576
+ type: number
577
+ default: 0.02
578
+ description: |
579
+ Same-frame quality gate — two same-label detections placed closer
580
+ than this in world coords cannot be two physical objects; the
581
+ weaker one is dropped (`same_spot_in_frame`).
582
+ min_detection_confidence:
583
+ type: number
584
+ default: 0
585
+ description: |
586
+ Same-frame quality gate — drop detections whose detector confidence
587
+ is below this floor (`below_min_confidence`). 0 disables the gate.
588
+ min_object_size_cm:
589
+ type: number
590
+ default: 0.5
591
+ description: Same-frame quality gate — drop implausibly small front faces.
592
+ max_object_size_cm:
593
+ type: number
594
+ default: 500
595
+ description: Same-frame quality gate — drop implausibly large front faces.
596
+ plane_merge:
597
+ type: boolean
598
+ default: true
599
+ description: |
600
+ Cross-frame matcher: project every placed detection onto the
601
+ session's consensus shelf plane (covariance normal over detection
602
+ positions, gravity-flattened; offset = median projection) and
603
+ merge same-label detections whose projected rects overlap at the
604
+ same plane depth. Falls back to the `cluster_eps_m` distance rule
605
+ when the plane is degenerate (< 3 detections, collinear stack) or
606
+ a detection has no world size.
607
+ plane_merge_iou:
608
+ type: number
609
+ default: 0.1
610
+ description: |
611
+ Minimum IoU of the two plane-projected front-face rects to treat
612
+ them as the same physical object. Adjacent identical facings
613
+ project side-by-side (IoU ~ 0), so they are never merged by this
614
+ rule. Default 0.1 — lenient so viewpoint-skewed or border-clipped
615
+ re-observations (whose projected overlap drops well below 0.3)
616
+ still merge, while side-by-side facings stay separate.
617
+ plane_merge_depth_delta_m:
618
+ type: number
619
+ default: 0.25
620
+ description: |
621
+ Maximum difference of the two detections' distances to the
622
+ consensus plane (metres) — the "virtual object depth". Overlapping
623
+ rects farther apart than this along the shelf normal are different
624
+ objects (front row vs back row), not a re-observation.
625
+ reclassify_labels:
626
+ type: boolean
627
+ default: false
628
+ description: |
629
+ Dims reclassifier master switch — re-labels a placed detection to
630
+ a SIBLING label (same label_group) when its MEASURED physical
631
+ size fits the sibling's expected dims better. Annotations keep
632
+ `original_label` + `reclassification_reason` provenance; running
633
+ with the switch OFF reverts earlier auto reclassifications.
634
+ reclassify_keep_dev:
635
+ type: number
636
+ default: 0.15
637
+ description: "Fit error at/below which the ORIGINAL label is kept outright (~ ±16% size). Raising it protects the detected label."
638
+ reclassify_target_dev:
639
+ type: number
640
+ default: 0.15
641
+ description: "A sibling must fit within this error to steal the detection. Raising it favors re-labeling."
642
+ reclassify_min_margin:
643
+ type: number
644
+ default: 0.06
645
+ description: "Base margin E(original) − E(best sibling) must exceed. Raising it protects the detected label."
646
+ reclassify_conf_margin_scale:
647
+ type: number
648
+ default: 0.5
649
+ description: "Margin scales ×(1 + this × detector confidence) — confident detections are harder to overturn; 0 ignores confidence."
650
+ reclassify_weight_scale:
651
+ type: number
652
+ default: 1.0
653
+ description: "Weight of the SIZE mismatch in the fit error. Raising it favors re-labeling (size separates variants)."
654
+ reclassify_weight_aspect:
655
+ type: number
656
+ default: 0.5
657
+ description: "Weight of the SHAPE mismatch in the fit error. Raising it protects the detected label (siblings share shape)."
658
+ reclassify_min_depth_confidence:
659
+ type: number
660
+ default: 0.5
661
+ description: "Detections with depth confidence below this are never reclassified."
662
+ group_consensus_merge:
663
+ type: boolean
664
+ default: true
665
+ description: |
666
+ Post-walk safety net (active only with reclassify_labels) — merge
667
+ clusters of DIFFERENT labels from the same label_group that
668
+ overlap on the consensus plane; the members are re-labeled to the
669
+ best dims fit.
670
+ size_gate:
671
+ type: boolean
672
+ default: true
673
+ description: |
674
+ Physical SIZE GATE — runs AFTER the reclassifier: a detection
675
+ whose measured size still exceeds (1+allowance)× its (possibly
676
+ re-labeled) label's expected width/height/area is DROPPED from
677
+ the walk (ledger disposition `size_rejected`; the annotation is
678
+ kept and flagged `size_rejected` + `size_reject_detail`).
679
+ One-sided by design — under-size is routinely a partially
680
+ occluded real product. No-op for labels without both dims.
681
+ size_gate_dims_allowance:
682
+ type: number
683
+ default: 0.35
684
+ description: "Per-axis allowance: reject when measured width or height > (1+this) × expected. Raising it keeps more (looser gate)."
685
+ size_gate_area_allowance:
686
+ type: number
687
+ default: 0.35
688
+ description: "Area allowance: reject when measured w·h > (1+this) × expected area. Noise compounds in area, so this is the binding check. Raising it keeps more."
689
+ size_gate_min_depth_confidence:
690
+ type: number
691
+ default: 0.5
692
+ description: "Detections with depth confidence below this are never size-gated (unmeasurable ≠ oversized)."
693
+ min_depth_m:
694
+ type: number
695
+ default: 0.05
696
+ max_depth_m:
697
+ type: number
698
+ default: 6
699
+ conf_threshold:
700
+ type: number
701
+ default: 1
702
+ front_percentile:
703
+ type: number
704
+ default: 30
705
+ shelf_tolerance_m:
706
+ type: number
707
+ default: 0.3
708
+ shelf_analysis:
709
+ type: boolean
710
+ default: true
711
+ description: |
712
+ Master switch for SHELF COMPOSITION — after fusion, build the
713
+ planogram structure (shelves → stacks → objects) from the fused
714
+ objects' world geometry and persist it as `shelf_composition`.
715
+ Shelf BOARDS are found first (density modes of object bottoms,
716
+ accepted strongest-first at a minimum physical spacing); a STACK
717
+ is then everything between two boards at one horizontal slot —
718
+ two stacks can never sit on top of each other by construction,
719
+ and stacking never hinges on fragile box-touch tolerances.
720
+ shelf_support_min_overlap:
721
+ type: number
722
+ default: 0.5
723
+ description: |
724
+ Min horizontal overlap ratio (of the narrower object, 0..1) for
725
+ an object to belong to the same column as the one below it.
726
+ Increase: demands straighter piles — offset cans become separate
727
+ stacks. Decrease: diagonal neighbours chain into one stack,
728
+ understating facing counts.
729
+ shelf_gap_split_m:
730
+ type: number
731
+ default: 0.06
732
+ description: |
733
+ Half-width (metres) of the bottom-density window used to find
734
+ candidate shelf levels — bottoms within this of a mode belong to
735
+ it. Increase: nearby levels blur into one candidate. Decrease:
736
+ measurement noise splinters a level into weak candidates.
737
+ shelf_min_spacing_m:
738
+ type: number
739
+ default: 0.12
740
+ description: |
741
+ Minimum vertical distance between two shelf BOARDS (board
742
+ thickness + product clearance — hard store physics). Candidate
743
+ levels are accepted strongest-first, each at least this far from
744
+ every accepted board, which is what keeps a jar-on-jar stacking
745
+ layer (bottoms +8–11 cm above their base) from ever becoming a
746
+ phantom shelf. Increase: genuinely shallow shelves read as
747
+ stacking. Decrease: stacking layers start reading as shelves.
748
+ shelf_stack_max_penetration_m:
749
+ type: number
750
+ default: 0.035
751
+ description: |
752
+ Measurement slack for stacked boxes (metres). Two objects cannot
753
+ co-occupy space: a joiner whose bottom sinks deeper than this
754
+ into an overlapped column member is a side-by-side facing with an
755
+ inflated box, never a pile member. Increase: inflated neighbours
756
+ glue into false towers. Decrease: noisy true piles split apart.
757
+ shelf_row_split_m:
758
+ type: number
759
+ default: 0.15
760
+ description: |
761
+ Plane-depth gap separating front/back rows within one shelf
762
+ (metres). Row 0 is the frontmost (closest to the shopper).
763
+ Increase: everything collapses into a single row. Decrease: minor
764
+ depth noise becomes extra rows.
765
+ shelf_min_stacks:
766
+ type: number
767
+ default: 1
768
+ description: |
769
+ A level must hold at least this many stacks to become a shelf;
770
+ leaner levels are reported under `unshelved` instead. Increase:
771
+ filters hook-wall/pegboard singletons and stray mis-placed bases
772
+ out of the shelf list. Decrease (1): every level becomes a shelf,
773
+ including one-off outliers.
774
+ build_point_cloud:
775
+ type: boolean
776
+ default: true
777
+ description: |
778
+ Build + store the scene voxel point cloud (and RANSAC shelf
779
+ planes) during the run. Downloads each frame's depth blob + RGB
780
+ image server-side, so disable for very large sessions when the 3D
781
+ scene's cloud layer is not needed.
782
+ pc_stride:
783
+ type: number
784
+ default: 2
785
+ description: Depth-grid stride when back-projecting the cloud (every Nth pixel).
786
+ pc_voxel_size_m:
787
+ type: number
788
+ default: 0.02
789
+ description: Voxel edge for point-cloud downsampling, metres.
790
+ ransac_max_planes:
791
+ type: number
792
+ default: 4
793
+ description: Max shelf planes to peel off the voxel cloud.
794
+ ransac_min_inlier_ratio:
795
+ type: number
796
+ default: 0.05
797
+ description: Stop peeling planes below this inlier share.
798
+ ransac_distance_thresh_m:
799
+ type: number
800
+ default: 0.02
801
+ description: Point-to-plane inlier distance for RANSAC, metres.
802
+ AnalysisObject:
803
+ type: object
804
+ description: |
805
+ A concluded object on the shelf (one per cross-frame cluster). `state` is
806
+ `kept` for a single facing or `merged` for several facings fused.
807
+ properties:
808
+ _id:
809
+ type: string
810
+ description: |
811
+ Cluster link target — contributing task annotations carry this id
812
+ in their `cluster_id` after the run succeeds.
813
+ label_id: { type: string }
814
+ label_name: { type: string }
815
+ state:
816
+ type: string
817
+ enum: [kept, merged]
818
+ confidence:
819
+ type: number
820
+ description: Fused placement confidence (cluster mean), 0..1.
821
+ world:
822
+ type: object
823
+ description: Centroid in world coordinates, **metres**.
824
+ properties:
825
+ x: { type: number }
826
+ y: { type: number }
827
+ z: { type: number }
828
+ size:
829
+ type: object
830
+ description: Physical front-face size, **centimetres**.
831
+ properties:
832
+ w: { type: number }
833
+ h: { type: number }
834
+ depth:
835
+ type: number
836
+ description: Distance from the camera, **metres**.
837
+ height_above_ground_m:
838
+ type: number
839
+ nullable: true
840
+ description: |
841
+ Metres above the device-locked floor (`session.ground.y_world`) —
842
+ the world frame is gravity-aligned, so this is the object's true
843
+ shelf height. Compare against `session.eye_level_m` for shelf-band
844
+ analytics (eye level ≈ premium band). `null` when the rep skipped
845
+ the floor point at capture entry.
846
+ yaw:
847
+ type: number
848
+ nullable: true
849
+ description: |
850
+ Facing rotation about world-Y, **radians** — the direction from the
851
+ object centroid to the winning frame's camera. `null` when the
852
+ winning task lacks a pose.
853
+ cluster_size:
854
+ type: number
855
+ description: How many facings/frames contributed.
856
+ bbox_3d:
857
+ type: object
858
+ properties:
859
+ min: { type: array, items: { type: number } }
860
+ max: { type: array, items: { type: number } }
861
+ winning_task: { type: string }
862
+ winning_annotation_id: { type: string }
863
+ winning_box:
864
+ type: object
865
+ description: |
866
+ Winner's 2D crop region on `winning_image_media` (YOLO-normalized
867
+ cx, cy, w, h). Front-ends texture the object with this sub-region.
868
+ properties:
869
+ x1: { type: number }
870
+ y1: { type: number }
871
+ x2: { type: number }
872
+ y2: { type: number }
873
+ winning_image_media:
874
+ type: string
875
+ description: |
876
+ `media.mediaStorages` id of the winning frame image (the crop
877
+ source). Populate with `objects.winning_image_media` to get its URL.
878
+ contributing_tasks:
879
+ type: array
880
+ items: { type: string }
881
+ arbitration:
882
+ type: object
883
+ description: |
884
+ Full winner-selection rationale: `strategy` (`weighted_view_score`),
885
+ the factor `weights`, and per-member `scores[]` — each with the six
886
+ normalized inputs (`det`, `depth`, `track`, `center`, `size`,
887
+ `agree`), the weighted `score`, and a `winner` flag. Renders the
888
+ dashboard's "why did this view win" inspector.
889
+ properties:
890
+ strategy: { type: string }
891
+ weights:
892
+ type: object
893
+ additionalProperties: { type: number }
894
+ scores:
895
+ type: array
896
+ items:
897
+ type: object
898
+ properties:
899
+ task_id: { type: string }
900
+ annotation_id: { type: string }
901
+ frame_id: { type: number }
902
+ score: { type: number }
903
+ winner: { type: boolean }
904
+ inputs:
905
+ type: object
906
+ properties:
907
+ det: { type: number }
908
+ depth: { type: number }
909
+ track: { type: number }
910
+ center: { type: number }
911
+ size: { type: number }
912
+ agree: { type: number }
913
+ ConcludedLabel:
914
+ type: object
915
+ description: Per-label conclusion — detection counts by outcome.
916
+ properties:
917
+ label_id: { type: string }
918
+ label_name: { type: string }
919
+ kept:
920
+ type: number
921
+ description: Detections placed as a single facing.
922
+ merged:
923
+ type: number
924
+ description: Detections fused into multi-facing objects.
925
+ ignored:
926
+ type: number
927
+ description: Detections that could not be placed.
928
+ dropped:
929
+ type: number
930
+ description: Detections removed by same-frame quality gates.
931
+ size_rejected:
932
+ type: number
933
+ description: Detections dropped by the physical-size gate.
934
+ object_count:
935
+ type: number
936
+ description: Resulting objects (kept + merged clusters).
937
+ IgnoredDetection:
938
+ type: object
939
+ description: A detection that could not be placed in world coordinates.
940
+ properties:
941
+ task: { type: string }
942
+ annotation_id: { type: string }
943
+ label_id: { type: string }
944
+ frame_id: { type: number }
945
+ reason:
946
+ type: string
947
+ description: |
948
+ Why placement was impossible: `no_pose`, `no_intrinsics`,
949
+ `no_depth`, `empty_depth_region`, `insufficient_depth_pixels`,
950
+ `behind_shelf`, or `not_placed` (legacy annotations without a
951
+ recorded reason).
952
+ DetectionLedgerEntry:
953
+ type: object
954
+ description: |
955
+ The audit trail of ONE detection through the fusion pipeline — where it
956
+ went and why. `new_object` spawned an object; `re_observation` merged
957
+ into an object first seen in an earlier frame; `dropped_same_frame` was
958
+ removed by a quality gate (see `reason` + `kept_by_annotation_id`);
959
+ `size_rejected` was dropped by the physical-size gate (annotation kept
960
+ and flagged); `unplaced` never had world data (see `reason`).
961
+ properties:
962
+ task: { type: string }
963
+ annotation_id: { type: string }
964
+ label_id: { type: string }
965
+ label_name: { type: string }
966
+ frame_id: { type: number }
967
+ disposition:
968
+ type: string
969
+ enum:
970
+ [
971
+ new_object,
972
+ re_observation,
973
+ dropped_same_frame,
974
+ size_rejected,
975
+ unplaced,
976
+ ]
977
+ reason:
978
+ type: string
979
+ description: |
980
+ Machine cause — `first_observation`, `re_observation`,
981
+ `duplicate_box_in_frame`, `same_spot_in_frame`,
982
+ `below_min_confidence`, `implausible_size`, `size_gate`, or an
983
+ unplaced reason (`no_pose`, `no_depth`, `behind_shelf`, ...).
984
+ detail:
985
+ type: string
986
+ description: Human-readable explanation, ready to render.
987
+ object_id:
988
+ type: string
989
+ description: The `objects[]._id` this detection created or merged into.
990
+ kept_by_annotation_id:
991
+ type: string
992
+ description: For same-frame drops — the stronger sibling that was kept.
993
+ matched_distance_m:
994
+ type: number
995
+ description: For re-observations — world distance to the object, metres.
996
+ world:
997
+ type: object
998
+ properties:
999
+ x: { type: number }
1000
+ y: { type: number }
1001
+ z: { type: number }
1002
+ box:
1003
+ type: object
1004
+ description: The detection's 2D box (YOLO-normalized cx, cy, w, h).
1005
+ properties:
1006
+ x1: { type: number }
1007
+ y1: { type: number }
1008
+ x2: { type: number }
1009
+ y2: { type: number }
1010
+ confidence: { type: number }
1011
+ placement_confidence: { type: number }
1012
+ AnalysisFunnel:
1013
+ type: object
1014
+ description: Where every detection went — the dashboard's fusion summary.
1015
+ properties:
1016
+ tasks: { type: number }
1017
+ tasks_manual:
1018
+ type: number
1019
+ description: |
1020
+ Frames whose boxes came from a confirmed HUMAN annotation group
1021
+ (manual / auto_edited) — the "computed on N human-verified
1022
+ frames" number business metrics report.
1023
+ detections_total: { type: number }
1024
+ unplaced: { type: number }
1025
+ size_rejected:
1026
+ type: number
1027
+ description: Detections dropped by the physical-size gate.
1028
+ frames: { type: number }
1029
+ detections_placed: { type: number }
1030
+ dropped_same_frame: { type: number }
1031
+ re_observations: { type: number }
1032
+ new_objects: { type: number }
1033
+ clusters_final: { type: number }
1034
+ SessionAnalysis:
1035
+ type: object
1036
+ properties:
1037
+ _id: { type: string }
1038
+ disabled: { type: boolean }
1039
+ session: { type: string }
1040
+ model_version: { type: string }
1041
+ config:
1042
+ $ref: "#/components/schemas/SceneMathConfig"
1043
+ frame_sources:
1044
+ type: array
1045
+ description: |
1046
+ PROVENANCE — which annotation group each frame contributed, chosen
1047
+ human-truth-first (a confirmed manual/auto_edited group beats
1048
+ every auto group regardless of the requested model version). The
1049
+ dashboard resolves the exact boxes this analysis consumed from
1050
+ here; `edit_time` lets the UI flag analyses older than a frame's
1051
+ latest correction.
1052
+ items:
1053
+ type: object
1054
+ properties:
1055
+ task: { type: string }
1056
+ group_id: { type: string }
1057
+ annotation_state:
1058
+ type: string
1059
+ enum: [auto, manual, auto_edited]
1060
+ model_version: { type: string }
1061
+ edit_time: { type: number }
1062
+ stage_fingerprints:
1063
+ type: object
1064
+ description: |
1065
+ Per-stage config hashes (detect / fuse / compose slices of
1066
+ `config`). Staleness is a fingerprint diff — a shelf-knob change
1067
+ flips only `compose`, unlocking the compose-only `recompute`.
1068
+ properties:
1069
+ detect: { type: string }
1070
+ fuse: { type: string }
1071
+ compose: { type: string }
1072
+ status:
1073
+ type: string
1074
+ enum: [pending, in_progress, success, failed]
1075
+ started_at: { type: number }
1076
+ finished_at: { type: number }
1077
+ heartbeat_at:
1078
+ type: number
1079
+ description: |
1080
+ Liveness heartbeat (epoch ms) refreshed by the background run after
1081
+ each task inference; the stale-run watchdog measures against it.
1082
+ tasks_total: { type: number }
1083
+ tasks_inferred:
1084
+ type: number
1085
+ description: Tasks inferred during this run (not previously placed).
1086
+ kept_count:
1087
+ type: number
1088
+ description: Detections kept (single facings).
1089
+ merged_count:
1090
+ type: number
1091
+ description: Detections merged into objects.
1092
+ ignored_count:
1093
+ type: number
1094
+ description: Detections that could not be placed.
1095
+ dropped_count:
1096
+ type: number
1097
+ description: Detections removed by same-frame quality gates.
1098
+ size_rejected_count:
1099
+ type: number
1100
+ description: Detections dropped by the physical-size gate.
1101
+ objects_count:
1102
+ type: number
1103
+ description: Resulting objects (kept + merged).
1104
+ objects:
1105
+ type: array
1106
+ items:
1107
+ $ref: "#/components/schemas/AnalysisObject"
1108
+ concluded_labels:
1109
+ type: array
1110
+ items:
1111
+ $ref: "#/components/schemas/ConcludedLabel"
1112
+ ignored:
1113
+ type: array
1114
+ items:
1115
+ $ref: "#/components/schemas/IgnoredDetection"
1116
+ detections:
1117
+ type: array
1118
+ description: Per-detection fusion audit trail (one entry per analyzed annotation).
1119
+ items:
1120
+ $ref: "#/components/schemas/DetectionLedgerEntry"
1121
+ funnel:
1122
+ $ref: "#/components/schemas/AnalysisFunnel"
1123
+ shelf_composition:
1124
+ type: object
1125
+ description: |
1126
+ PLANOGRAM STRUCTURE composed from the fused objects' world
1127
+ geometry (present unless `config.shelf_analysis: false` or nothing
1128
+ was placed). Ordering contract: `shelves[0]` is the LOWEST shelf,
1129
+ `shelves[].stacks[0]` is the SHOPPER's leftmost stack, and
1130
+ `stacks[].object_ids` list the pile bottom-up. Compliance rules
1131
+ (same-shelf exclusions, adjacency, share-of-shelf) read directly
1132
+ off this structure.
1133
+ properties:
1134
+ plane:
1135
+ type: object
1136
+ description: |
1137
+ Orthonormal world basis. `n` is the horizontal unit normal of
1138
+ the shelf face pointing TOWARD the shopper (the session's
1139
+ cameras); `u = up × n` points to the shopper's RIGHT. A plane
1140
+ coordinate maps back to world as `û·u + Ŷ·y + n̂·s`.
1141
+ properties:
1142
+ n:
1143
+ type: array
1144
+ items: { type: number }
1145
+ u:
1146
+ type: array
1147
+ items: { type: number }
1148
+ orientation:
1149
+ type: string
1150
+ enum: [shopper]
1151
+ description: Stacks are indexed in the shopper's frame (0 = leftmost).
1152
+ shelves:
1153
+ type: array
1154
+ items:
1155
+ type: object
1156
+ properties:
1157
+ index:
1158
+ type: number
1159
+ description: 0 = the lowest shelf.
1160
+ y_world:
1161
+ type: number
1162
+ description: |
1163
+ Board level (world Y, metres) — a low percentile (p15)
1164
+ of the member stack bases, so the board sits UNDER its
1165
+ products rather than through the middle of the noisy
1166
+ base band.
1167
+ height_above_ground_m:
1168
+ type: number
1169
+ nullable: true
1170
+ description: Shelf level above the device-locked floor; null when the floor was not locked.
1171
+ u_from:
1172
+ type: number
1173
+ description: Occupied horizontal extent start (metres, plane coords).
1174
+ u_to:
1175
+ type: number
1176
+ s:
1177
+ type: number
1178
+ description: Median plane depth of member stacks (metres).
1179
+ stacks:
1180
+ type: array
1181
+ items:
1182
+ type: object
1183
+ properties:
1184
+ index:
1185
+ type: number
1186
+ description: 0 = leftmost as the shopper sees the shelf.
1187
+ u_from: { type: number }
1188
+ u_to: { type: number }
1189
+ y_from: { type: number }
1190
+ y_to: { type: number }
1191
+ s: { type: number }
1192
+ row:
1193
+ type: number
1194
+ description: Depth row within the shelf — 0 = front row (closest to the shopper).
1195
+ object_ids:
1196
+ type: array
1197
+ items: { type: string }
1198
+ description: "Refs into `objects[]._id`, ordered bottom-up (index 0 sits on the shelf)."
1199
+ unshelved:
1200
+ type: array
1201
+ description: Objects that didn't land on a shelf level (hook walls, sparse levels).
1202
+ items:
1203
+ type: object
1204
+ properties:
1205
+ object_id: { type: string }
1206
+ reason: { type: string }
1207
+ scene:
1208
+ type: object
1209
+ description: |
1210
+ Scene-level outputs. All best-effort — each part is omitted when it
1211
+ can't be computed (too few points, media unavailable, or
1212
+ `config.build_point_cloud: false`).
1213
+ properties:
1214
+ plane:
1215
+ type: object
1216
+ description: |
1217
+ Dominant (shelf) plane fitted over all placed detections' world
1218
+ positions (covariance eigen; legacy single plane, kept for
1219
+ back-compat). Omitted when fewer than 3 placed detections
1220
+ exist or the fit fails.
1221
+ properties:
1222
+ normal:
1223
+ type: array
1224
+ items: { type: number }
1225
+ description: "Unit normal [x, y, z]."
1226
+ d:
1227
+ type: number
1228
+ description: "Plane equation term: n·p + d = 0."
1229
+ centroid:
1230
+ type: array
1231
+ items: { type: number }
1232
+ description: "Centroid [x, y, z], metres."
1233
+ inliers:
1234
+ type: number
1235
+ description: Points used in the fit.
1236
+ planes:
1237
+ type: array
1238
+ description: |
1239
+ Bounded shelf planes peeled off the voxel point cloud by
1240
+ iterative RANSAC, strongest first. Front-ends render each as a
1241
+ translucent rectangle sized by its extent corners.
1242
+ items:
1243
+ type: object
1244
+ properties:
1245
+ normal:
1246
+ type: array
1247
+ items: { type: number }
1248
+ description: "Unit normal [x, y, z]."
1249
+ point:
1250
+ type: array
1251
+ items: { type: number }
1252
+ description: "A point on the plane (inlier centroid), metres."
1253
+ extent_min:
1254
+ type: array
1255
+ items: { type: number }
1256
+ description: One corner of the fitted inlier rectangle (world coords).
1257
+ extent_max:
1258
+ type: array
1259
+ items: { type: number }
1260
+ description: The opposite corner.
1261
+ inlier_count: { type: number }
1262
+ inlier_ratio:
1263
+ type: number
1264
+ description: "Inliers / cloud points, 0..1."
1265
+ point_cloud:
1266
+ type: object
1267
+ description: |
1268
+ Voxel-downsampled scene point cloud metadata. The packed
1269
+ little-endian Float32 `[x, y, z, r, g, b, w]` blob is stored as
1270
+ `bin` media (`media`) — the `view-3d-scene` service inlines it
1271
+ as base64 for the dashboard.
1272
+ properties:
1273
+ media:
1274
+ type: string
1275
+ description: "`media.mediaStorages` `_id` of the packed blob."
1276
+ num_points: { type: number }
1277
+ voxel_size_m: { type: number }
1278
+ aabb_min:
1279
+ type: array
1280
+ items: { type: number }
1281
+ description: "Axis-aligned bounds min [x, y, z], metres."
1282
+ aabb_max:
1283
+ type: array
1284
+ items: { type: number }
1285
+ _errors:
1286
+ type: array
1287
+ items: { type: object }
1288
+ creator:
1289
+ $ref: "#/components/schemas/UserRef"
1290
+ editor:
1291
+ $ref: "#/components/schemas/UserRef"
1292
+ company_namespace:
1293
+ type: array
1294
+ items: { type: string }
1295
+ description: Tenant key. Server-injected — never accept from clients.
1296
+ createdAt: { type: string, format: date-time }
1297
+ updatedAt: { type: string, format: date-time }
1298
+ UserRef:
1299
+ type: object
1300
+ description: Compact actor reference (rep or admin).
1301
+ properties:
1302
+ _id: { type: string }
1303
+ type: { type: string, enum: [admin, rep] }
1304
+ name: { type: string }
1305
+ rep: { type: string }
1306
+ admin: { type: string }