unitlab 3.0.4__tar.gz → 3.1.0__tar.gz
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.
- {unitlab-3.0.4 → unitlab-3.1.0}/PKG-INFO +149 -3
- {unitlab-3.0.4 → unitlab-3.1.0}/README.md +147 -1
- {unitlab-3.0.4 → unitlab-3.1.0}/pyproject.toml +2 -2
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/__init__.py +4 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_http.py +14 -2
- unitlab-3.1.0/src/unitlab/_nuscenes.py +531 -0
- unitlab-3.1.0/src/unitlab/_scene_uploader.py +604 -0
- unitlab-3.1.0/src/unitlab/_scenes.py +833 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_uploader.py +152 -17
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/cli.py +198 -1
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/__init__.py +2 -0
- unitlab-3.1.0/src/unitlab/resources/annotation_imports.py +182 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/assets.py +69 -17
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/projects.py +70 -13
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/types.py +9 -0
- unitlab-3.1.0/tests/test_annotation_imports.py +326 -0
- unitlab-3.1.0/tests/test_http.py +243 -0
- unitlab-3.1.0/tests/test_nuscenes.py +484 -0
- unitlab-3.1.0/tests/test_scene_cli.py +250 -0
- unitlab-3.1.0/tests/test_scene_uploads.py +565 -0
- unitlab-3.1.0/tests/test_scenes.py +650 -0
- unitlab-3.0.4/tests/test_http.py +0 -99
- {unitlab-3.0.4 → unitlab-3.1.0}/.gitignore +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/LICENSE.md +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/__main__.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_config.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_downloader.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_grouping.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_waiter.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/client.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/exceptions.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/ontology.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/py.typed +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/_base.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/cloud.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/datasets.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/embeddings.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/ontologies.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/releases.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/workflow.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_cli.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_client.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_downloader.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_embeddings.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_tiled_uploads.py +0 -0
- {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_uploader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitlab
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Python SDK for the Unitlab.ai data annotation platform
|
|
5
5
|
Project-URL: Homepage, https://unitlab.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.unitlab.ai
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
-
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: httpx[socks]>=0.27
|
|
23
23
|
Requires-Dist: tqdm>=4.60
|
|
24
24
|
Requires-Dist: typer>=0.9
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -113,7 +113,8 @@ print(batch.uploaded, batch.batch_queue_id)
|
|
|
113
113
|
|
|
114
114
|
The directory may mix images, video, audio, text, PDFs, DICOM, NIfTI, and NRRD.
|
|
115
115
|
One call creates one Batch Queue. Call `batch.wait()` when the next operation
|
|
116
|
-
depends on server-side processing.
|
|
116
|
+
depends on server-side processing. Lidar scenes are uploaded with
|
|
117
|
+
`generic_type="pointcloud"`; see [Lidar scenes](#lidar-scenes).
|
|
117
118
|
|
|
118
119
|
### 2. Curate in Assets, publish a dataset, and attach it
|
|
119
120
|
|
|
@@ -234,6 +235,76 @@ upload directly to storage in bounded parts. Tiled processing can take hours,
|
|
|
234
235
|
so use a longer timeout such as `batch.wait(timeout=25200)` when the next
|
|
235
236
|
operation depends on the generated COG/DZI derivative.
|
|
236
237
|
|
|
238
|
+
Video, medical (NIfTI, NRRD, DICOM), audio and PDF files larger than 64 MiB
|
|
239
|
+
also upload directly to storage, up to 500 MiB each; smaller ones keep the
|
|
240
|
+
ordinary upload. A large medical file becomes one volume on its own and is not
|
|
241
|
+
grouped with other files of the batch into a series.
|
|
242
|
+
|
|
243
|
+
DICOM files named without a usable extension (`IM00001`, or a UID) are found by
|
|
244
|
+
their content when a folder is uploaded and are sent as medical; nothing needs
|
|
245
|
+
to be renamed.
|
|
246
|
+
|
|
247
|
+
### Lidar scenes
|
|
248
|
+
|
|
249
|
+
A lidar scene is one item whatever it is made of. It is uploaded only when the
|
|
250
|
+
type is named; without it `.pcd`, `.ply`, `.bin` and scene `.zip` files are
|
|
251
|
+
skipped. The workspace needs lidar scenes enabled.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from unitlab import detect_scenes
|
|
255
|
+
|
|
256
|
+
# Look first: what would be uploaded, nothing is sent.
|
|
257
|
+
for scene in detect_scenes("./recordings").scenes:
|
|
258
|
+
print(scene.name, scene.frame_count, scene.point_sensors, scene.cameras)
|
|
259
|
+
|
|
260
|
+
batch = project.upload("./recordings", generic_type="pointcloud")
|
|
261
|
+
batch.wait()
|
|
262
|
+
|
|
263
|
+
for item_id in batch.data_item_ids:
|
|
264
|
+
status = project.scene_status(item_id)
|
|
265
|
+
print(status.status, status.frame_count, status.error)
|
|
266
|
+
|
|
267
|
+
project.retry_scene(item_id) # a failed scene, about a minute after it failed
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
What `source` may be:
|
|
271
|
+
|
|
272
|
+
- One file: a `.pcd`, `.ply` or `.bin` cloud, or a `.zip` that contains such
|
|
273
|
+
frames or a `scene.json`. Any other `.zip` is refused.
|
|
274
|
+
- A folder with a `scene.json`, or with the point clouds of one sensor
|
|
275
|
+
(`drive_01/lidar/000.pcd`, ...). Image folders whose file names match the
|
|
276
|
+
frames become its cameras (`drive_01/cam_front/000.jpg`).
|
|
277
|
+
- A folder with one subfolder per sensor that share file names. This is read as
|
|
278
|
+
one scene with several sensors; pass `split_sensor_folders=True` when each
|
|
279
|
+
folder is a recording of its own.
|
|
280
|
+
- A folder that holds several such scenes: each one is uploaded as its own
|
|
281
|
+
item. Scene files left over in the folder are one scene each.
|
|
282
|
+
- A nuScenes dataset (the folder with `samples/` and `v1.0-*/`): one scene per
|
|
283
|
+
recording, key frames only, with the calibration taken from its tables.
|
|
284
|
+
|
|
285
|
+
Other files in the folder are not uploaded by this call; upload the folder once
|
|
286
|
+
more without the type for ordinary media. A scene is limited to 20 GiB and
|
|
287
|
+
1,000 frames, and one data unit is used per frame.
|
|
288
|
+
|
|
289
|
+
`batch.wait()` covers scenes like any other upload. A failed scene is not in
|
|
290
|
+
`project.data_units()`; find it in the Batch Queue (`queue.data()`) and ask
|
|
291
|
+
`project.scene_status(item_id)` for the reason.
|
|
292
|
+
|
|
293
|
+
Scenes go into Assets the same way, and the existing Asset methods apply:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
result = client.assets.upload(
|
|
297
|
+
"./recordings", generic_type="pointcloud", folder="Lidar"
|
|
298
|
+
)
|
|
299
|
+
asset = result.assets[0]
|
|
300
|
+
status = asset.wait() # status.frame_count, status.error
|
|
301
|
+
asset.retry() # a failed scene
|
|
302
|
+
asset = client.assets.asset("ASSET_ID") # a handle by ID, for status and retry
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`release.download_files()` writes a scene that was uploaded as a folder under
|
|
306
|
+
`<item file name>/` with the paths it was uploaded with.
|
|
307
|
+
|
|
237
308
|
### Data Units and project lifecycle
|
|
238
309
|
|
|
239
310
|
```python
|
|
@@ -345,6 +416,72 @@ batch = project.import_cloud(
|
|
|
345
416
|
status = batch.wait()
|
|
346
417
|
```
|
|
347
418
|
|
|
419
|
+
### Import annotation JSON
|
|
420
|
+
|
|
421
|
+
Media can come from S3 while the annotation JSON stays on your computer:
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
project = client.projects.get("PROJECT_ID")
|
|
425
|
+
media = project.import_cloud("CLOUD_STORAGE_ID", ["images/"])
|
|
426
|
+
status = media.wait()
|
|
427
|
+
if status.failed:
|
|
428
|
+
raise RuntimeError("Some media failed processing; inspect the Batch Queue.")
|
|
429
|
+
|
|
430
|
+
batch = project.annotation_imports.create(
|
|
431
|
+
"./predictions.json",
|
|
432
|
+
source_name="my-model", # Reuse this namespace for the same prediction source.
|
|
433
|
+
)
|
|
434
|
+
print(batch.preview["counts"], batch.preview["issues"])
|
|
435
|
+
# Inspect the preview before saving labels. Approval is a separate review step.
|
|
436
|
+
report = batch.commit()
|
|
437
|
+
print(report)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Supported inputs are COCO JSON, Unitlab exports (UUEF item JSON, Audio JSON and
|
|
441
|
+
GeoJSON, as `.json` or `.geojson`), `unitlab-image/1` and `unitlab-video/1`. They
|
|
442
|
+
cover image, video, medical, document, audio, time series, text, HTML, geospatial
|
|
443
|
+
and pathology items. A UUEF file holds one item: import each file as its own batch.
|
|
444
|
+
Each JSON file is limited to 600 MiB. The backend matches original media paths/IDs,
|
|
445
|
+
with a unique-filename fallback; ambiguous names require an explicit mapping.
|
|
446
|
+
YOLO text/ZIP files and other tools' project files are not accepted by this importer.
|
|
447
|
+
|
|
448
|
+
For COCO video, pass `frame_mapping_file="./frames.json"` in
|
|
449
|
+
`unitlab-frame-map/1` format. Use `track_profile="attributes.track_id"` only for
|
|
450
|
+
COCO exports with that explicit track-ID convention. Import video media at its
|
|
451
|
+
original frame rate; annotations for frames omitted by sampling are rejected.
|
|
452
|
+
|
|
453
|
+
`project.annotation_imports.sources()` lists saved prediction sources, and
|
|
454
|
+
`project.annotation_imports.media(search="road.jpg")` finds media IDs for mappings.
|
|
455
|
+
Pass `source_id=` instead of `source_name=` to reuse a source by ID. Set
|
|
456
|
+
`options={"stable_ids": True}` only when your exporter keeps annotation IDs stable
|
|
457
|
+
between prediction runs. Otherwise the default protects existing objects from
|
|
458
|
+
unconfirmed ID reuse.
|
|
459
|
+
|
|
460
|
+
After processing completes or you supply mappings, call `batch.refresh()` to
|
|
461
|
+
rebuild the preview. `batch.refresh(options={...})` replaces all import options;
|
|
462
|
+
omitting `options` preserves them. Supported options are `stable_ids`,
|
|
463
|
+
`media_mapping`, `class_mapping`, `object_mapping`, and `review_mapping`.
|
|
464
|
+
|
|
465
|
+
`batch.commit()` preserves conflicts and human deletions by default. To resolve
|
|
466
|
+
a specific preview annotation, pass `decisions={annotation_key: "keep"}` (or
|
|
467
|
+
`"incoming"` / `"skip"`). Invalid records prevent the whole commit unless you
|
|
468
|
+
explicitly choose `allow_partial=True`. The report includes created, updated,
|
|
469
|
+
kept, unchanged and skipped counts when applicable. A stale preview raises
|
|
470
|
+
`ConflictError`; inspect a refreshed preview before committing again. If a
|
|
471
|
+
commit response is lost, repeat the same batch's `commit()` with the same
|
|
472
|
+
decisions; the server returns the saved report without duplicating labels.
|
|
473
|
+
|
|
474
|
+
For annotations already in connected storage, omit the local file and pass
|
|
475
|
+
`cloud_storage=storage`, `annotation_key="labels/predictions.json"`, and optionally
|
|
476
|
+
`frame_mapping_key="labels/frames.json"`. Keys are relative to that connection's
|
|
477
|
+
prefix. Media and annotations must resolve within the API key's workspace and
|
|
478
|
+
the selected project.
|
|
479
|
+
|
|
480
|
+
This requires a backend with the `/api/sdk/projects/{id}/annotation-imports/`
|
|
481
|
+
routes and the annotation-import database migration installed. Commit requires
|
|
482
|
+
a user-created API key so saved labels have an audit author. Legacy keys without
|
|
483
|
+
a creator can preview but receive `PermissionDeniedError` when committing.
|
|
484
|
+
|
|
348
485
|
### Data Groups
|
|
349
486
|
|
|
350
487
|
Let Unitlab suggest a filename pattern:
|
|
@@ -423,6 +560,12 @@ unitlab project sources PROJECT_ID
|
|
|
423
560
|
unitlab project detach-source PROJECT_ID SOURCE_LINK_ID --preview
|
|
424
561
|
unitlab project upload PROJECT_ID --source ./data
|
|
425
562
|
unitlab project upload PROJECT_ID --source ./maps
|
|
563
|
+
unitlab project upload PROJECT_ID --source ./recordings --type pointcloud --dry-run
|
|
564
|
+
unitlab project upload PROJECT_ID --source ./recordings --type pointcloud
|
|
565
|
+
unitlab project upload PROJECT_ID --source ./rig --type pointcloud \
|
|
566
|
+
--split-sensor-folders
|
|
567
|
+
unitlab project scene-status PROJECT_ID ITEM_ID
|
|
568
|
+
unitlab project scene-retry PROJECT_ID ITEM_ID
|
|
426
569
|
unitlab batch-queue list PROJECT_ID
|
|
427
570
|
unitlab batch-queue detail PROJECT_ID BATCH_QUEUE_ID
|
|
428
571
|
unitlab batch-queue status PROJECT_ID BATCH_QUEUE_ID
|
|
@@ -438,6 +581,9 @@ unitlab project attach PROJECT_ID --dataset-version DATASET_ID:2
|
|
|
438
581
|
# Assets
|
|
439
582
|
unitlab assets upload ./data --folder "Raw data" --tag incoming
|
|
440
583
|
unitlab assets upload ./slides --folder "Slides"
|
|
584
|
+
unitlab assets upload ./recordings --folder "Lidar" --type pointcloud
|
|
585
|
+
unitlab assets status ASSET_ID
|
|
586
|
+
unitlab assets retry ASSET_ID
|
|
441
587
|
|
|
442
588
|
# Folders
|
|
443
589
|
unitlab folders create "Raw data"
|
|
@@ -80,7 +80,8 @@ print(batch.uploaded, batch.batch_queue_id)
|
|
|
80
80
|
|
|
81
81
|
The directory may mix images, video, audio, text, PDFs, DICOM, NIfTI, and NRRD.
|
|
82
82
|
One call creates one Batch Queue. Call `batch.wait()` when the next operation
|
|
83
|
-
depends on server-side processing.
|
|
83
|
+
depends on server-side processing. Lidar scenes are uploaded with
|
|
84
|
+
`generic_type="pointcloud"`; see [Lidar scenes](#lidar-scenes).
|
|
84
85
|
|
|
85
86
|
### 2. Curate in Assets, publish a dataset, and attach it
|
|
86
87
|
|
|
@@ -201,6 +202,76 @@ upload directly to storage in bounded parts. Tiled processing can take hours,
|
|
|
201
202
|
so use a longer timeout such as `batch.wait(timeout=25200)` when the next
|
|
202
203
|
operation depends on the generated COG/DZI derivative.
|
|
203
204
|
|
|
205
|
+
Video, medical (NIfTI, NRRD, DICOM), audio and PDF files larger than 64 MiB
|
|
206
|
+
also upload directly to storage, up to 500 MiB each; smaller ones keep the
|
|
207
|
+
ordinary upload. A large medical file becomes one volume on its own and is not
|
|
208
|
+
grouped with other files of the batch into a series.
|
|
209
|
+
|
|
210
|
+
DICOM files named without a usable extension (`IM00001`, or a UID) are found by
|
|
211
|
+
their content when a folder is uploaded and are sent as medical; nothing needs
|
|
212
|
+
to be renamed.
|
|
213
|
+
|
|
214
|
+
### Lidar scenes
|
|
215
|
+
|
|
216
|
+
A lidar scene is one item whatever it is made of. It is uploaded only when the
|
|
217
|
+
type is named; without it `.pcd`, `.ply`, `.bin` and scene `.zip` files are
|
|
218
|
+
skipped. The workspace needs lidar scenes enabled.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from unitlab import detect_scenes
|
|
222
|
+
|
|
223
|
+
# Look first: what would be uploaded, nothing is sent.
|
|
224
|
+
for scene in detect_scenes("./recordings").scenes:
|
|
225
|
+
print(scene.name, scene.frame_count, scene.point_sensors, scene.cameras)
|
|
226
|
+
|
|
227
|
+
batch = project.upload("./recordings", generic_type="pointcloud")
|
|
228
|
+
batch.wait()
|
|
229
|
+
|
|
230
|
+
for item_id in batch.data_item_ids:
|
|
231
|
+
status = project.scene_status(item_id)
|
|
232
|
+
print(status.status, status.frame_count, status.error)
|
|
233
|
+
|
|
234
|
+
project.retry_scene(item_id) # a failed scene, about a minute after it failed
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
What `source` may be:
|
|
238
|
+
|
|
239
|
+
- One file: a `.pcd`, `.ply` or `.bin` cloud, or a `.zip` that contains such
|
|
240
|
+
frames or a `scene.json`. Any other `.zip` is refused.
|
|
241
|
+
- A folder with a `scene.json`, or with the point clouds of one sensor
|
|
242
|
+
(`drive_01/lidar/000.pcd`, ...). Image folders whose file names match the
|
|
243
|
+
frames become its cameras (`drive_01/cam_front/000.jpg`).
|
|
244
|
+
- A folder with one subfolder per sensor that share file names. This is read as
|
|
245
|
+
one scene with several sensors; pass `split_sensor_folders=True` when each
|
|
246
|
+
folder is a recording of its own.
|
|
247
|
+
- A folder that holds several such scenes: each one is uploaded as its own
|
|
248
|
+
item. Scene files left over in the folder are one scene each.
|
|
249
|
+
- A nuScenes dataset (the folder with `samples/` and `v1.0-*/`): one scene per
|
|
250
|
+
recording, key frames only, with the calibration taken from its tables.
|
|
251
|
+
|
|
252
|
+
Other files in the folder are not uploaded by this call; upload the folder once
|
|
253
|
+
more without the type for ordinary media. A scene is limited to 20 GiB and
|
|
254
|
+
1,000 frames, and one data unit is used per frame.
|
|
255
|
+
|
|
256
|
+
`batch.wait()` covers scenes like any other upload. A failed scene is not in
|
|
257
|
+
`project.data_units()`; find it in the Batch Queue (`queue.data()`) and ask
|
|
258
|
+
`project.scene_status(item_id)` for the reason.
|
|
259
|
+
|
|
260
|
+
Scenes go into Assets the same way, and the existing Asset methods apply:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
result = client.assets.upload(
|
|
264
|
+
"./recordings", generic_type="pointcloud", folder="Lidar"
|
|
265
|
+
)
|
|
266
|
+
asset = result.assets[0]
|
|
267
|
+
status = asset.wait() # status.frame_count, status.error
|
|
268
|
+
asset.retry() # a failed scene
|
|
269
|
+
asset = client.assets.asset("ASSET_ID") # a handle by ID, for status and retry
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`release.download_files()` writes a scene that was uploaded as a folder under
|
|
273
|
+
`<item file name>/` with the paths it was uploaded with.
|
|
274
|
+
|
|
204
275
|
### Data Units and project lifecycle
|
|
205
276
|
|
|
206
277
|
```python
|
|
@@ -312,6 +383,72 @@ batch = project.import_cloud(
|
|
|
312
383
|
status = batch.wait()
|
|
313
384
|
```
|
|
314
385
|
|
|
386
|
+
### Import annotation JSON
|
|
387
|
+
|
|
388
|
+
Media can come from S3 while the annotation JSON stays on your computer:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
project = client.projects.get("PROJECT_ID")
|
|
392
|
+
media = project.import_cloud("CLOUD_STORAGE_ID", ["images/"])
|
|
393
|
+
status = media.wait()
|
|
394
|
+
if status.failed:
|
|
395
|
+
raise RuntimeError("Some media failed processing; inspect the Batch Queue.")
|
|
396
|
+
|
|
397
|
+
batch = project.annotation_imports.create(
|
|
398
|
+
"./predictions.json",
|
|
399
|
+
source_name="my-model", # Reuse this namespace for the same prediction source.
|
|
400
|
+
)
|
|
401
|
+
print(batch.preview["counts"], batch.preview["issues"])
|
|
402
|
+
# Inspect the preview before saving labels. Approval is a separate review step.
|
|
403
|
+
report = batch.commit()
|
|
404
|
+
print(report)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Supported inputs are COCO JSON, Unitlab exports (UUEF item JSON, Audio JSON and
|
|
408
|
+
GeoJSON, as `.json` or `.geojson`), `unitlab-image/1` and `unitlab-video/1`. They
|
|
409
|
+
cover image, video, medical, document, audio, time series, text, HTML, geospatial
|
|
410
|
+
and pathology items. A UUEF file holds one item: import each file as its own batch.
|
|
411
|
+
Each JSON file is limited to 600 MiB. The backend matches original media paths/IDs,
|
|
412
|
+
with a unique-filename fallback; ambiguous names require an explicit mapping.
|
|
413
|
+
YOLO text/ZIP files and other tools' project files are not accepted by this importer.
|
|
414
|
+
|
|
415
|
+
For COCO video, pass `frame_mapping_file="./frames.json"` in
|
|
416
|
+
`unitlab-frame-map/1` format. Use `track_profile="attributes.track_id"` only for
|
|
417
|
+
COCO exports with that explicit track-ID convention. Import video media at its
|
|
418
|
+
original frame rate; annotations for frames omitted by sampling are rejected.
|
|
419
|
+
|
|
420
|
+
`project.annotation_imports.sources()` lists saved prediction sources, and
|
|
421
|
+
`project.annotation_imports.media(search="road.jpg")` finds media IDs for mappings.
|
|
422
|
+
Pass `source_id=` instead of `source_name=` to reuse a source by ID. Set
|
|
423
|
+
`options={"stable_ids": True}` only when your exporter keeps annotation IDs stable
|
|
424
|
+
between prediction runs. Otherwise the default protects existing objects from
|
|
425
|
+
unconfirmed ID reuse.
|
|
426
|
+
|
|
427
|
+
After processing completes or you supply mappings, call `batch.refresh()` to
|
|
428
|
+
rebuild the preview. `batch.refresh(options={...})` replaces all import options;
|
|
429
|
+
omitting `options` preserves them. Supported options are `stable_ids`,
|
|
430
|
+
`media_mapping`, `class_mapping`, `object_mapping`, and `review_mapping`.
|
|
431
|
+
|
|
432
|
+
`batch.commit()` preserves conflicts and human deletions by default. To resolve
|
|
433
|
+
a specific preview annotation, pass `decisions={annotation_key: "keep"}` (or
|
|
434
|
+
`"incoming"` / `"skip"`). Invalid records prevent the whole commit unless you
|
|
435
|
+
explicitly choose `allow_partial=True`. The report includes created, updated,
|
|
436
|
+
kept, unchanged and skipped counts when applicable. A stale preview raises
|
|
437
|
+
`ConflictError`; inspect a refreshed preview before committing again. If a
|
|
438
|
+
commit response is lost, repeat the same batch's `commit()` with the same
|
|
439
|
+
decisions; the server returns the saved report without duplicating labels.
|
|
440
|
+
|
|
441
|
+
For annotations already in connected storage, omit the local file and pass
|
|
442
|
+
`cloud_storage=storage`, `annotation_key="labels/predictions.json"`, and optionally
|
|
443
|
+
`frame_mapping_key="labels/frames.json"`. Keys are relative to that connection's
|
|
444
|
+
prefix. Media and annotations must resolve within the API key's workspace and
|
|
445
|
+
the selected project.
|
|
446
|
+
|
|
447
|
+
This requires a backend with the `/api/sdk/projects/{id}/annotation-imports/`
|
|
448
|
+
routes and the annotation-import database migration installed. Commit requires
|
|
449
|
+
a user-created API key so saved labels have an audit author. Legacy keys without
|
|
450
|
+
a creator can preview but receive `PermissionDeniedError` when committing.
|
|
451
|
+
|
|
315
452
|
### Data Groups
|
|
316
453
|
|
|
317
454
|
Let Unitlab suggest a filename pattern:
|
|
@@ -390,6 +527,12 @@ unitlab project sources PROJECT_ID
|
|
|
390
527
|
unitlab project detach-source PROJECT_ID SOURCE_LINK_ID --preview
|
|
391
528
|
unitlab project upload PROJECT_ID --source ./data
|
|
392
529
|
unitlab project upload PROJECT_ID --source ./maps
|
|
530
|
+
unitlab project upload PROJECT_ID --source ./recordings --type pointcloud --dry-run
|
|
531
|
+
unitlab project upload PROJECT_ID --source ./recordings --type pointcloud
|
|
532
|
+
unitlab project upload PROJECT_ID --source ./rig --type pointcloud \
|
|
533
|
+
--split-sensor-folders
|
|
534
|
+
unitlab project scene-status PROJECT_ID ITEM_ID
|
|
535
|
+
unitlab project scene-retry PROJECT_ID ITEM_ID
|
|
393
536
|
unitlab batch-queue list PROJECT_ID
|
|
394
537
|
unitlab batch-queue detail PROJECT_ID BATCH_QUEUE_ID
|
|
395
538
|
unitlab batch-queue status PROJECT_ID BATCH_QUEUE_ID
|
|
@@ -405,6 +548,9 @@ unitlab project attach PROJECT_ID --dataset-version DATASET_ID:2
|
|
|
405
548
|
# Assets
|
|
406
549
|
unitlab assets upload ./data --folder "Raw data" --tag incoming
|
|
407
550
|
unitlab assets upload ./slides --folder "Slides"
|
|
551
|
+
unitlab assets upload ./recordings --folder "Lidar" --type pointcloud
|
|
552
|
+
unitlab assets status ASSET_ID
|
|
553
|
+
unitlab assets retry ASSET_ID
|
|
408
554
|
|
|
409
555
|
# Folders
|
|
410
556
|
unitlab folders create "Raw data"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitlab"
|
|
7
|
-
version = "3.0
|
|
7
|
+
version = "3.1.0"
|
|
8
8
|
description = "Python SDK for the Unitlab.ai data annotation platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"httpx>=0.27",
|
|
26
|
+
"httpx[socks]>=0.27",
|
|
27
27
|
"tqdm>=4.60",
|
|
28
28
|
"typer>=0.9",
|
|
29
29
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from importlib.metadata import version
|
|
2
2
|
|
|
3
3
|
from ._grouping import tiles_from_template
|
|
4
|
+
from ._scene_uploader import detect_scenes
|
|
4
5
|
from .client import UnitlabClient
|
|
5
6
|
from .exceptions import (
|
|
6
7
|
AmbiguousUploadCompletionError,
|
|
@@ -26,6 +27,7 @@ from .ontology import (
|
|
|
26
27
|
TextAttribute,
|
|
27
28
|
)
|
|
28
29
|
from .resources import (
|
|
30
|
+
AnnotationImport,
|
|
29
31
|
Asset,
|
|
30
32
|
AttachedSource,
|
|
31
33
|
BatchQueue,
|
|
@@ -58,6 +60,7 @@ __version__ = version("unitlab")
|
|
|
58
60
|
|
|
59
61
|
__all__ = [
|
|
60
62
|
"AmbiguousUploadCompletionError",
|
|
63
|
+
"AnnotationImport",
|
|
61
64
|
"Asset",
|
|
62
65
|
"AssetUploadResult",
|
|
63
66
|
"AttachPreview",
|
|
@@ -102,5 +105,6 @@ __all__ = [
|
|
|
102
105
|
"WorkflowStage",
|
|
103
106
|
"WorkflowTask",
|
|
104
107
|
"__version__",
|
|
108
|
+
"detect_scenes",
|
|
105
109
|
"tiles_from_template",
|
|
106
110
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import time
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
@@ -16,6 +17,11 @@ from .exceptions import (
|
|
|
16
17
|
ValidationError,
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
# Match HTTPTransport(retries=3): one initial attempt, then three retries for
|
|
21
|
+
# connection failures only. Leave transport construction to HTTPX so that its
|
|
22
|
+
# standard HTTP_PROXY/HTTPS_PROXY/ALL_PROXY and NO_PROXY handling stays active.
|
|
23
|
+
_CONNECT_RETRY_DELAYS = (0.0, 0.5, 1.0)
|
|
24
|
+
|
|
19
25
|
|
|
20
26
|
def _extract_error_message(response: httpx.Response) -> str:
|
|
21
27
|
try:
|
|
@@ -90,7 +96,6 @@ class HttpApi:
|
|
|
90
96
|
self.client = httpx.Client(
|
|
91
97
|
base_url=api_url,
|
|
92
98
|
headers={"Authorization": f"Api-Key {api_key}"},
|
|
93
|
-
transport=httpx.HTTPTransport(retries=3),
|
|
94
99
|
timeout=60.0,
|
|
95
100
|
)
|
|
96
101
|
|
|
@@ -124,7 +129,14 @@ class HttpApi:
|
|
|
124
129
|
}
|
|
125
130
|
if timeout is not None:
|
|
126
131
|
kwargs["timeout"] = timeout
|
|
127
|
-
|
|
132
|
+
for attempt in range(len(_CONNECT_RETRY_DELAYS) + 1):
|
|
133
|
+
try:
|
|
134
|
+
response = self.client.request(method, endpoint, **kwargs)
|
|
135
|
+
break
|
|
136
|
+
except (httpx.ConnectError, httpx.ConnectTimeout):
|
|
137
|
+
if attempt == len(_CONNECT_RETRY_DELAYS):
|
|
138
|
+
raise
|
|
139
|
+
time.sleep(_CONNECT_RETRY_DELAYS[attempt])
|
|
128
140
|
except httpx.TimeoutException as exc:
|
|
129
141
|
raise RequestTimeoutError(str(exc), exc) from exc
|
|
130
142
|
except httpx.HTTPError as exc:
|