supervisely 6.73.452__py3-none-any.whl → 6.73.513__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,210 @@
1
+ # coding: utf-8
2
+ """create or manipulate guides that can be assigned to labeling jobs and labeling queues"""
3
+
4
+ # docs
5
+ from __future__ import annotations
6
+
7
+ from typing import Dict, List, NamedTuple, Optional
8
+
9
+ from supervisely.api.module_api import ApiField, ModuleApiBase
10
+
11
+
12
+ class GuideInfo(NamedTuple):
13
+ """
14
+ Information about a Guide.
15
+
16
+ :param id: Guide ID in Supervisely.
17
+ :type id: int
18
+ :param name: Guide name.
19
+ :type name: str
20
+ :param description: Guide description.
21
+ :type description: str
22
+ :param file_path: Path to the guide file (PDF or other).
23
+ :type file_path: str
24
+ :param created_at: Guide creation date.
25
+ :type created_at: str
26
+ :param updated_at: Guide last update date.
27
+ :type updated_at: str
28
+ :param created_by_id: ID of the User who created the Guide.
29
+ :type created_by_id: int
30
+ :param team_id: Team ID where the Guide is located.
31
+ :type team_id: int
32
+ :param video_id: ID of the video associated with the guide (if any).
33
+ :type video_id: Optional[int]
34
+ :param disabled_by: ID of the User who disabled the Guide (if disabled).
35
+ :type disabled_by: Optional[int]
36
+ :param disabled_at: Date when the Guide was disabled (if disabled).
37
+ :type disabled_at: Optional[str]
38
+ """
39
+
40
+ id: int
41
+ name: str
42
+ description: str
43
+ file_path: str
44
+ created_at: str
45
+ updated_at: str
46
+ created_by_id: int
47
+ team_id: int
48
+ video_id: Optional[int] = None
49
+ disabled_by: Optional[int] = None
50
+ disabled_at: Optional[str] = None
51
+
52
+
53
+ class GuidesApi(ModuleApiBase):
54
+ """
55
+ API for working with Guides. :class:`GuidesApi<GuidesApi>` object is immutable.
56
+
57
+ :param api: API connection to the server.
58
+ :type api: Api
59
+ :Usage example:
60
+
61
+ .. code-block:: python
62
+
63
+ import os
64
+ from dotenv import load_dotenv
65
+
66
+ import supervisely as sly
67
+
68
+ # Load secrets and create API object from .env file (recommended)
69
+ # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
70
+
71
+ api = sly.Api.from_env()
72
+
73
+ # Get list of guides in team
74
+ guides = api.guides.get_list(team_id=123)
75
+ """
76
+
77
+ @staticmethod
78
+ def info_sequence():
79
+ """
80
+ NamedTuple GuideInfo information about Guide.
81
+
82
+ :Example:
83
+
84
+ .. code-block:: python
85
+
86
+ GuideInfo(
87
+ id=1,
88
+ name='How to label objects',
89
+ description='Comprehensive guide on object labeling',
90
+ file_path='/path/to/guide.pdf',
91
+ created_at='2023-01-01T00:00:00.000Z',
92
+ updated_at='2025-11-17T18:21:10.217Z',
93
+ created_by_id=1,
94
+ team_id=1,
95
+ video_id=None,
96
+ disabled_by=None,
97
+ disabled_at=None
98
+ )
99
+ """
100
+ return [
101
+ ApiField.ID,
102
+ ApiField.NAME,
103
+ ApiField.DESCRIPTION,
104
+ ApiField.FILE_PATH,
105
+ ApiField.CREATED_AT,
106
+ ApiField.UPDATED_AT,
107
+ ApiField.CREATED_BY_ID,
108
+ ApiField.TEAM_ID,
109
+ ApiField.VIDEO_ID,
110
+ ApiField.DISABLED_BY,
111
+ ApiField.DISABLED_AT,
112
+ ]
113
+
114
+ @staticmethod
115
+ def info_tuple_name():
116
+ """
117
+ NamedTuple name - **GuideInfo**.
118
+ """
119
+ return "GuideInfo"
120
+
121
+ def get_list(
122
+ self, team_id: int, filters: Optional[List[Dict[str, str]]] = None
123
+ ) -> List[GuideInfo]:
124
+ """
125
+ Get list of Guides in the given Team.
126
+
127
+ :param team_id: Team ID in Supervisely.
128
+ :type team_id: int
129
+ :param filters: List of parameters to filter Guides.
130
+ :type filters: List[Dict[str, str]], optional
131
+ :return: List of information about Guides.
132
+ :rtype: :class:`List[GuideInfo]`
133
+ :Usage example:
134
+
135
+ .. code-block:: python
136
+
137
+ import os
138
+ from dotenv import load_dotenv
139
+
140
+ import supervisely as sly
141
+
142
+ # Load secrets and create API object from .env file (recommended)
143
+ # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
144
+
145
+ api = sly.Api.from_env()
146
+
147
+ team_id = 123
148
+ guides = api.guides.get_list(team_id)
149
+ print(guides)
150
+ # Output: [
151
+ # GuideInfo(
152
+ # id=1,
153
+ # name='How to label objects',
154
+ # description='Comprehensive guide on object labeling',
155
+ # file_path='/path/to/guide.pdf',
156
+ # created_at='2023-01-01T00:00:00.000Z',
157
+ # updated_at='2025-11-17T18:21:10.217Z',
158
+ # created_by_id=1,
159
+ # team_id=1,
160
+ # video_id=None,
161
+ # disabled_by=None,
162
+ # disabled_at=None
163
+ # )
164
+ # ]
165
+ """
166
+ return self.get_list_all_pages(
167
+ "guides.list",
168
+ {ApiField.TEAM_ID: team_id, ApiField.FILTER: filters or []},
169
+ )
170
+
171
+ def get_info_by_id(self, id: int) -> GuideInfo:
172
+ """
173
+ Get Guide information by ID.
174
+
175
+ :param id: Guide ID in Supervisely.
176
+ :type id: int
177
+ :return: Information about Guide.
178
+ :rtype: :class:`GuideInfo`
179
+ :Usage example:
180
+
181
+ .. code-block:: python
182
+
183
+ import os
184
+ from dotenv import load_dotenv
185
+
186
+ import supervisely as sly
187
+
188
+ # Load secrets and create API object from .env file (recommended)
189
+ # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
190
+
191
+ api = sly.Api.from_env()
192
+
193
+ guide_id = 1
194
+ guide_info = api.guides.get_info_by_id(guide_id)
195
+ print(guide_info)
196
+ # Output: GuideInfo(
197
+ # id=1,
198
+ # name='How to label objects',
199
+ # description='Comprehensive guide on object labeling',
200
+ # file_path='/path/to/guide.pdf',
201
+ # created_at='2023-01-01T00:00:00.000Z',
202
+ # updated_at='2025-11-17T18:21:10.217Z',
203
+ # created_by_id=1,
204
+ # team_id=1,
205
+ # video_id=None,
206
+ # disabled_by=None,
207
+ # disabled_at=None
208
+ # )
209
+ """
210
+ return self._get_info_by_id(id, "guides.info")
@@ -397,6 +397,9 @@ class ImageInfo(NamedTuple):
397
397
  #: Format: "YYYY-MM-DDTHH:MM:SS.sssZ"
398
398
  embeddings_updated_at: Optional[str] = None
399
399
 
400
+ #: :class:`int`: :class:`Dataset<supervisely.project.project.Project>` ID in Supervisely.
401
+ project_id: int = None
402
+
400
403
  # DO NOT DELETE THIS COMMENT
401
404
  #! New fields must be added with default values to keep backward compatibility.
402
405
 
@@ -476,6 +479,7 @@ class ImageApi(RemoveableBulkModuleApi):
476
479
  ApiField.OFFSET_END,
477
480
  ApiField.AI_SEARCH_META,
478
481
  ApiField.EMBEDDINGS_UPDATED_AT,
482
+ ApiField.PROJECT_ID,
479
483
  ]
480
484
 
481
485
  @staticmethod
@@ -89,6 +89,7 @@ class LabelingJobInfo(NamedTuple):
89
89
  exclude_images_with_tags: list
90
90
  entities: list
91
91
  priority: int
92
+ guide_id: Optional[int] = None
92
93
 
93
94
 
94
95
  class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
@@ -223,6 +224,7 @@ class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
223
224
  ApiField.EXCLUDE_IMAGES_WITH_TAGS,
224
225
  ApiField.ENTITIES,
225
226
  ApiField.PRIORITY,
227
+ ApiField.M_GUIDE_ID,
226
228
  ]
227
229
 
228
230
  @staticmethod
@@ -261,7 +263,10 @@ class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
261
263
  else:
262
264
  value = info[sub_name]
263
265
  else:
264
- value = value[sub_name]
266
+ if skip_missing is True:
267
+ value = value.get(sub_name, None)
268
+ else:
269
+ value = value[sub_name]
265
270
  else:
266
271
  raise RuntimeError("Can not parse field {!r}".format(field_name))
267
272
 
@@ -341,6 +346,8 @@ class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
341
346
  disable_submit: Optional[bool] = None,
342
347
  toolbox_settings: Optional[Dict] = None,
343
348
  enable_quality_check: Optional[bool] = None,
349
+ guide_id: Optional[int] = None,
350
+ allow_restore: bool = False,
344
351
  ) -> List[LabelingJobInfo]:
345
352
  """
346
353
  Creates Labeling Job and assigns given Users to it.
@@ -385,6 +392,10 @@ class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
385
392
  :type toolbox_settings: Dict, optional
386
393
  :param enable_quality_check: If True, adds an intermediate step between "review" and completing the Labeling Job.
387
394
  :type enable_quality_check: bool, optional
395
+ :param guide_id: Guide ID in Supervisely to assign a guide to the Labeling Job.
396
+ :type guide_id: int, optional
397
+ :param allow_restore: If True, allows restoring a previously deleted labeling job with the same name in the same dataset.
398
+ :type allow_restore: bool
388
399
  :return: List of information about new Labeling Job. See :class:`info_sequence<info_sequence>`
389
400
  :rtype: :class:`List[LabelingJobInfo]`
390
401
  :Usage example:
@@ -463,8 +474,18 @@ class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
463
474
  "entityIds": images_ids,
464
475
  "dynamicClasses": dynamic_classes,
465
476
  "dynamicTags": dynamic_tags,
477
+ "allowRestore": allow_restore,
466
478
  }
467
479
 
480
+ if guide_id is not None:
481
+ try:
482
+ guide_id = int(guide_id)
483
+ except Exception as e:
484
+ raise ValueError(
485
+ f"guide_id must be an integer, got {type(guide_id)} with value '{guide_id}'"
486
+ ) from None
487
+ meta["guide"] = guide_id
488
+
468
489
  if toolbox_settings is not None:
469
490
  dataset_info = self._api.dataset.get_info_by_id(dataset_id)
470
491
  project_id = dataset_info.project_id
@@ -1460,3 +1481,64 @@ class LabelingJobApi(RemoveableBulkModuleApi, ModuleWithStatus):
1460
1481
 
1461
1482
  response = self._api.post("jobs.restart", data).json()
1462
1483
  return response
1484
+
1485
+ def get_custom_data(self, id: int) -> dict:
1486
+ """
1487
+ Get custom data of Labeling Job with given ID.
1488
+
1489
+ :param id: Labeling Job ID in Supervisely.
1490
+ :type id: int
1491
+ :return: Custom data of the job
1492
+ :rtype: :class:`dict`
1493
+ :Usage example:
1494
+
1495
+ .. code-block:: python
1496
+
1497
+ import supervisely as sly
1498
+
1499
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1500
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1501
+ api = sly.Api.from_env()
1502
+
1503
+ custom_data = api.labeling_job.get_custom_data(9)
1504
+ print(custom_data)
1505
+ """
1506
+ method = "jobs.info"
1507
+ response = self._get_response_by_id(id, method, id_field=ApiField.ID)
1508
+ json_response = response.json() if response is not None else None
1509
+ if json_response is not None:
1510
+ return json_response.get(ApiField.CUSTOM_DATA, {})
1511
+ return {}
1512
+
1513
+ def set_custom_data(self, id: int, custom_data: dict, update: bool = True) -> None:
1514
+ """
1515
+ Update or replace custom data of Labeling Job with given ID.
1516
+ By default, updates existing custom data. To replace it entirely, set `update` to False.
1517
+
1518
+ :param id: Labeling Job ID in Supervisely.
1519
+ :type id: int
1520
+ :param custom_data: Custom data to set
1521
+ :type custom_data: dict
1522
+ :param update: Whether to update existing custom data or replace it entirely.
1523
+ :type update: bool
1524
+ :return: None
1525
+ :rtype: :class:`NoneType`
1526
+ :Usage example:
1527
+
1528
+ .. code-block:: python
1529
+
1530
+ import supervisely as sly
1531
+
1532
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1533
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1534
+ api = sly.Api.from_env()
1535
+
1536
+ api.labeling_job.set_custom_data(9, {"key": "value"})
1537
+ """
1538
+ method = "jobs.editInfo"
1539
+
1540
+ if update is True:
1541
+ existing_custom_data = self.get_custom_data(id)
1542
+ existing_custom_data.update(custom_data)
1543
+ custom_data = existing_custom_data
1544
+ self._api.post(method, {ApiField.ID: id, ApiField.CUSTOM_DATA: custom_data})
@@ -37,6 +37,7 @@ class LabelingQueueInfo(NamedTuple):
37
37
  in_progress_count: int
38
38
  pending_count: int
39
39
  meta: dict
40
+ collection_id: Optional[int] = None
40
41
 
41
42
 
42
43
  class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
@@ -93,7 +94,8 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
93
94
  annotated_count=3,
94
95
  in_progress_count=2,
95
96
  pending_count=1,
96
- meta={}
97
+ meta={},
98
+ collection_id=None,
97
99
  )
98
100
  """
99
101
  return [
@@ -115,6 +117,7 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
115
117
  ApiField.IN_PROGRESS_COUNT,
116
118
  ApiField.PENDING_COUNT,
117
119
  ApiField.META,
120
+ ApiField.COLLECTION_ID,
118
121
  ]
119
122
 
120
123
  @staticmethod
@@ -200,24 +203,24 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
200
203
  skip_complete_job_on_empty: Optional[bool] = False,
201
204
  enable_quality_check: Optional[bool] = None,
202
205
  quality_check_user_ids: Optional[List[int]] = None,
206
+ guide_id: Optional[int] = None,
207
+ description: Optional[str] = None,
203
208
  ) -> int:
204
209
  """
205
210
  Creates Labeling Queue and assigns given Users to it.
206
211
 
207
212
  :param name: Labeling Queue name in Supervisely.
208
213
  :type name: str
209
- :param dataset_id: Dataset ID in Supervisely.
210
- :type dataset_id: int
211
- :param collection_id: Entities Collection ID in Supervisely.
212
- :type collection_id: int, optional
213
214
  :param user_ids: User IDs in Supervisely to assign Users as labelers to Labeling Queue.
214
215
  :type user_ids: List[int]
215
216
  :param reviewer_ids: User IDs in Supervisely to assign Users as reviewers to Labeling Queue.
216
217
  :type reviewer_ids: List[int]
218
+ :param dataset_id: Dataset ID in Supervisely.
219
+ :type dataset_id: int
220
+ :param collection_id: Entities Collection ID in Supervisely.
221
+ :type collection_id: int, optional
217
222
  :param readme: Additional information about Labeling Queue.
218
223
  :type readme: str, optional
219
- :param description: Description of Labeling Queue.
220
- :type description: str, optional
221
224
  :param classes_to_label: List of classes to label in Dataset.
222
225
  :type classes_to_label: List[str], optional
223
226
  :param objects_limit_per_image: Limit the number of objects that the labeler can create on each image.
@@ -256,6 +259,10 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
256
259
  :type enable_quality_check: bool, optional
257
260
  :param quality_check_user_ids: List of User IDs in Supervisely to assign Users as Quality Checkers to Labeling Queue.
258
261
  :type quality_check_user_ids: List[int], optional
262
+ :param guide_id: Guide ID in Supervisely to assign a guide to the Labeling Queue.
263
+ :type guide_id: int, optional
264
+ :param description: Description of Labeling Queue.
265
+ :type description: str, optional
259
266
  :return: Labeling Queue ID in Supervisely.
260
267
  :rtype: int
261
268
  :Usage example:
@@ -340,6 +347,15 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
340
347
  if images_ids is not None:
341
348
  meta["entityIds"] = images_ids
342
349
 
350
+ if guide_id is not None:
351
+ try:
352
+ guide_id = int(guide_id)
353
+ except Exception as e:
354
+ raise ValueError(
355
+ f"guide_id must be an integer, got {type(guide_id)} with value '{guide_id}'"
356
+ ) from None
357
+ meta["guide"] = guide_id
358
+
343
359
  if toolbox_settings is not None:
344
360
  if dataset_id is not None:
345
361
  dataset_info = self._api.dataset.get_info_by_id(dataset_id)
@@ -399,6 +415,9 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
399
415
  if readme is not None:
400
416
  data[ApiField.README] = str(readme)
401
417
 
418
+ if description is not None:
419
+ data[ApiField.DESCRIPTION] = str(description)
420
+
402
421
  if images_range is not None and images_range != (None, None):
403
422
  if len(images_range) != 2:
404
423
  raise RuntimeError("images_range has to contain 2 elements (start, end)")
@@ -419,6 +438,7 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
419
438
  ids: Optional[List[int]] = None,
420
439
  names: Optional[List[str]] = None,
421
440
  show_disabled: Optional[bool] = False,
441
+ collection_id: Optional[int] = None,
422
442
  ) -> List[LabelingQueueInfo]:
423
443
  """
424
444
  Get list of information about Labeling Queues in the given Team.
@@ -435,6 +455,8 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
435
455
  :type names: List[str], optional
436
456
  :param show_disabled: Show disabled Labeling Queues.
437
457
  :type show_disabled: bool, optional
458
+ :param collection_id: Entities Collection ID in Supervisely.
459
+ :type collection_id: int, optional
438
460
  :return: List of information about Labeling Queues. See :class:`info_sequence<info_sequence>`
439
461
  :rtype: :class:`List[LabelingQueueInfo]`
440
462
  :Usage example:
@@ -455,6 +477,10 @@ class LabelingQueueApi(RemoveableBulkModuleApi, ModuleWithStatus):
455
477
  filters.append({"field": ApiField.PROJECT_ID, "operator": "=", "value": project_id})
456
478
  if dataset_id is not None:
457
479
  filters.append({"field": ApiField.DATASET_ID, "operator": "=", "value": dataset_id})
480
+ if collection_id is not None:
481
+ filters.append(
482
+ {"field": ApiField.COLLECTION_ID, "operator": "=", "value": collection_id}
483
+ )
458
484
  if names is not None:
459
485
  filters.append({"field": ApiField.NAME, "operator": "in", "value": names})
460
486
  if ids is not None:
@@ -713,6 +713,11 @@ class ApiField:
713
713
  """"""
714
714
  NN_UPDATED = "nnUpdated"
715
715
  """"""
716
+ M_GUIDE_ID = (["meta", "guide"], "guide_id")
717
+ """"""
718
+ GUIDE_ID = "guideId"
719
+ """"""
720
+ SINGLE_SESSION_MODE = "singleSessionMode"
716
721
 
717
722
 
718
723
  def _get_single_item(items):
@@ -52,6 +52,7 @@ from supervisely.io.json import dump_json_file, load_json_file
52
52
  from supervisely.project.project_meta import ProjectMeta
53
53
  from supervisely.project.project_meta import ProjectMetaJsonFields as MetaJsonF
54
54
  from supervisely.project.project_settings import (
55
+ LabelingInterface,
55
56
  ProjectSettings,
56
57
  ProjectSettingsJsonFields,
57
58
  )
@@ -691,6 +692,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
691
692
  type: ProjectType = ProjectType.IMAGES,
692
693
  description: Optional[str] = "",
693
694
  change_name_if_conflict: Optional[bool] = False,
695
+ readme: Optional[str] = None,
694
696
  ) -> ProjectInfo:
695
697
  """
696
698
  Create Project with given name in the given Workspace ID.
@@ -705,6 +707,8 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
705
707
  :type description: str
706
708
  :param change_name_if_conflict: Checks if given name already exists and adds suffix to the end of the name.
707
709
  :type change_name_if_conflict: bool, optional
710
+ :param readme: Project readme.
711
+ :type readme: str, optional
708
712
  :return: Information about Project. See :class:`info_sequence<info_sequence>`
709
713
  :rtype: :class:`ProjectInfo`
710
714
  :Usage example:
@@ -745,15 +749,15 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
745
749
  name=name,
746
750
  change_name_if_conflict=change_name_if_conflict,
747
751
  )
748
- response = self._api.post(
749
- "projects.add",
750
- {
751
- ApiField.WORKSPACE_ID: workspace_id,
752
- ApiField.NAME: effective_name,
753
- ApiField.DESCRIPTION: description,
754
- ApiField.TYPE: str(type),
755
- },
756
- )
752
+ payload = {
753
+ ApiField.NAME: effective_name,
754
+ ApiField.WORKSPACE_ID: workspace_id,
755
+ ApiField.DESCRIPTION: description,
756
+ ApiField.TYPE: str(type),
757
+ }
758
+ if readme is not None:
759
+ payload[ApiField.README] = readme
760
+ response = self._api.post("projects.add", payload)
757
761
  return self._convert_json_info(response.json())
758
762
 
759
763
  def _get_update_method(self):
@@ -1368,6 +1372,8 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
1368
1372
 
1369
1373
  def get_settings(self, id: int) -> Dict[str, str]:
1370
1374
  info = self._get_info_by_id(id, "projects.info")
1375
+ if info is None:
1376
+ raise ProjectNotFound(f"Project with id={id} not found")
1371
1377
  return info.settings
1372
1378
 
1373
1379
  def update_settings(self, id: int, settings: Dict[str, str]) -> None:
@@ -1993,8 +1999,11 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
1993
1999
  )
1994
2000
 
1995
2001
  def set_multiview_settings(self, project_id: int) -> None:
1996
- """Sets the project settings for multiview images.
1997
- Images will be grouped by tag and have synchronized view and labeling.
2002
+ """Sets the project settings for multiview mode.
2003
+ Automatically detects project type and applies appropriate settings:
2004
+
2005
+ - For IMAGE projects: Images are grouped by tag with synchronized view and labeling.
2006
+ - For VIDEO projects: Videos are grouped by datasets (each dataset = one group).
1998
2007
 
1999
2008
  :param project_id: Project ID to set multiview settings.
2000
2009
  :type project_id: int
@@ -2015,17 +2024,51 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2015
2024
  load_dotenv(os.path.expanduser("~/supervisely.env"))
2016
2025
  api = sly.Api.from_env()
2017
2026
 
2018
- api.project.set_multiview_settings(project_id=123)
2027
+ # For images project - will enable grouping by tags
2028
+ api.project.set_multiview_settings(image_project_id)
2029
+
2030
+ # For videos project - will enable grouping by datasets
2031
+ api.project.set_multiview_settings(video_project_id)
2019
2032
  """
2033
+ project_info = self.get_info_by_id(project_id)
2034
+ if project_info.type == ProjectType.IMAGES.value:
2035
+ self._set_custom_grouping_settings(
2036
+ id=project_id,
2037
+ group_images=True,
2038
+ tag_name=_MULTIVIEW_TAG_NAME,
2039
+ sync=False,
2040
+ label_group_tag_name=_LABEL_GROUP_TAG_NAME,
2041
+ )
2042
+ elif project_info.type == ProjectType.VIDEOS.value:
2043
+ self._set_custom_grouping_settings_video(project_id, sync=True)
2044
+ else:
2045
+ raise ValueError("Multiview settings can only be set for image or video projects")
2020
2046
 
2021
- self._set_custom_grouping_settings(
2022
- id=project_id,
2023
- group_images=True,
2024
- tag_name=_MULTIVIEW_TAG_NAME,
2025
- sync=False,
2026
- label_group_tag_name=_LABEL_GROUP_TAG_NAME,
2047
+ def _set_custom_grouping_settings_video(self, project_id: int, sync: bool = True) -> None:
2048
+ """Sets the project settings for multiview videos (private method).
2049
+ For video projects, videos are grouped by datasets (not by tags).
2050
+ Each dataset represents a group of videos that will be displayed together in multiview mode.
2051
+
2052
+ :param project_id: Project ID to set video multiview settings.
2053
+ :type project_id: int
2054
+ :param sync: If True, enables synchronized playback across video views.
2055
+ :type sync: bool
2056
+ :return: None
2057
+ :rtype: :class:`NoneType`
2058
+ """
2059
+ meta = ProjectMeta.from_json(self.get_meta(project_id, with_settings=True))
2060
+
2061
+ new_settings = ProjectSettings(
2062
+ multiview_enabled=True,
2063
+ multiview_tag_name=None, # Not used for videos
2064
+ multiview_tag_id=None, # Not used for videos
2065
+ multiview_is_synced=sync,
2066
+ labeling_interface=LabelingInterface.MULTIVIEW,
2027
2067
  )
2028
2068
 
2069
+ meta = meta.clone(project_settings=new_settings)
2070
+ self.update_meta(id=project_id, meta=meta)
2071
+
2029
2072
  def remove_permanently(
2030
2073
  self, ids: Union[int, List], batch_size: int = 50, progress_cb=None
2031
2074
  ) -> List[dict]:
@@ -2327,7 +2370,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2327
2370
  """
2328
2371
  info = self.get_info_by_id(id, extra_fields=[ApiField.EMBEDDINGS_IN_PROGRESS])
2329
2372
  if info is None:
2330
- raise RuntimeError(f"Project with ID {id} not found.")
2373
+ raise ProjectNotFound(f"Project with ID {id} not found.")
2331
2374
  if not hasattr(info, "embeddings_in_progress"):
2332
2375
  raise RuntimeError(
2333
2376
  f"Project with ID {id} does not have 'embeddings_in_progress' field in its info."
@@ -2392,7 +2435,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2392
2435
  """
2393
2436
  info = self.get_info_by_id(id, extra_fields=[ApiField.EMBEDDINGS_UPDATED_AT])
2394
2437
  if info is None:
2395
- raise RuntimeError(f"Project with ID {id} not found.")
2438
+ raise ProjectNotFound(f"Project with ID {id} not found.")
2396
2439
  if not hasattr(info, "embeddings_updated_at"):
2397
2440
  raise RuntimeError(
2398
2441
  f"Project with ID {id} does not have 'embeddings_updated_at' field in its info."
@@ -2606,7 +2649,9 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2606
2649
  )
2607
2650
  dst_project_id = dst_project_info.id
2608
2651
 
2609
- datasets = self._api.dataset.get_list(src_project_id, recursive=True, include_custom_data=True)
2652
+ datasets = self._api.dataset.get_list(
2653
+ src_project_id, recursive=True, include_custom_data=True
2654
+ )
2610
2655
  src_to_dst_ids = {}
2611
2656
 
2612
2657
  for src_dataset_info in datasets:
@@ -2626,7 +2671,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2626
2671
  src_project_id: int,
2627
2672
  dst_project_id: Optional[int] = None,
2628
2673
  dst_project_name: Optional[str] = None,
2629
- ) -> Tuple[List[DatasetInfo], List[DatasetInfo]]:
2674
+ ) -> List[Tuple[DatasetInfo, DatasetInfo]]:
2630
2675
  """This method can be used to recreate a project with hierarchial datasets (without the data itself).
2631
2676
 
2632
2677
  :param src_project_id: Source project ID
@@ -2636,8 +2681,8 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2636
2681
  :param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
2637
2682
  :type dst_project_name: str, optional
2638
2683
 
2639
- :return: Destination project ID
2640
- :rtype: int
2684
+ :return: List of tuples of source and destination DatasetInfo objects
2685
+ :rtype: List[Tuple[DatasetInfo, DatasetInfo]]
2641
2686
 
2642
2687
  :Usage example:
2643
2688
 
@@ -2650,8 +2695,8 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2650
2695
  src_project_id = 123
2651
2696
  dst_project_name = "New Project"
2652
2697
 
2653
- dst_project_id = api.project.recreate_structure(src_project_id, dst_project_name=dst_project_name)
2654
- print(f"Recreated project {src_project_id} -> {dst_project_id}")
2698
+ infos = api.project.recreate_structure(src_project_id, dst_project_name=dst_project_name)
2699
+ print(f"Recreated project {src_project_id}")
2655
2700
  """
2656
2701
  infos = []
2657
2702
  for src_info, dst_info in self.recreate_structure_generator(