supervisely 6.73.410__py3-none-any.whl → 6.73.470__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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

Files changed (190) hide show
  1. supervisely/__init__.py +136 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/annotation/label.py +80 -3
  5. supervisely/api/annotation_api.py +9 -9
  6. supervisely/api/api.py +67 -43
  7. supervisely/api/app_api.py +72 -5
  8. supervisely/api/dataset_api.py +108 -33
  9. supervisely/api/entity_annotation/figure_api.py +113 -49
  10. supervisely/api/image_api.py +82 -0
  11. supervisely/api/module_api.py +10 -0
  12. supervisely/api/nn/deploy_api.py +15 -9
  13. supervisely/api/nn/ecosystem_models_api.py +201 -0
  14. supervisely/api/nn/neural_network_api.py +12 -3
  15. supervisely/api/pointcloud/pointcloud_api.py +38 -0
  16. supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
  17. supervisely/api/project_api.py +213 -6
  18. supervisely/api/task_api.py +11 -1
  19. supervisely/api/video/video_annotation_api.py +4 -2
  20. supervisely/api/video/video_api.py +79 -1
  21. supervisely/api/video/video_figure_api.py +24 -11
  22. supervisely/api/volume/volume_api.py +38 -0
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +14 -6
  25. supervisely/app/fastapi/__init__.py +1 -0
  26. supervisely/app/fastapi/custom_static_files.py +1 -1
  27. supervisely/app/fastapi/multi_user.py +88 -0
  28. supervisely/app/fastapi/subapp.py +175 -42
  29. supervisely/app/fastapi/templating.py +1 -1
  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 +11 -1
  35. supervisely/app/widgets/agent_selector/template.html +1 -0
  36. supervisely/app/widgets/card/card.py +20 -0
  37. supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
  38. supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
  39. supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
  40. supervisely/app/widgets/dialog/dialog.py +12 -0
  41. supervisely/app/widgets/dialog/template.html +2 -1
  42. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  43. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  44. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  45. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  46. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
  47. supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
  48. supervisely/app/widgets/fast_table/fast_table.py +713 -126
  49. supervisely/app/widgets/fast_table/script.js +492 -95
  50. supervisely/app/widgets/fast_table/style.css +54 -0
  51. supervisely/app/widgets/fast_table/template.html +45 -5
  52. supervisely/app/widgets/heatmap/__init__.py +0 -0
  53. supervisely/app/widgets/heatmap/heatmap.py +523 -0
  54. supervisely/app/widgets/heatmap/script.js +378 -0
  55. supervisely/app/widgets/heatmap/style.css +227 -0
  56. supervisely/app/widgets/heatmap/template.html +21 -0
  57. supervisely/app/widgets/input_tag/input_tag.py +102 -15
  58. supervisely/app/widgets/input_tag_list/__init__.py +0 -0
  59. supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
  60. supervisely/app/widgets/input_tag_list/template.html +70 -0
  61. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  62. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  63. supervisely/app/widgets/radio_tabs/template.html +1 -0
  64. supervisely/app/widgets/select/select.py +6 -4
  65. supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
  66. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
  67. supervisely/app/widgets/table/table.py +68 -13
  68. supervisely/app/widgets/tabs/tabs.py +22 -6
  69. supervisely/app/widgets/tabs/template.html +5 -1
  70. supervisely/app/widgets/transfer/style.css +3 -0
  71. supervisely/app/widgets/transfer/template.html +3 -1
  72. supervisely/app/widgets/transfer/transfer.py +48 -45
  73. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  74. supervisely/convert/image/csv/csv_converter.py +24 -15
  75. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
  76. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
  77. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
  78. supervisely/convert/video/video_converter.py +2 -2
  79. supervisely/geometry/polyline_3d.py +110 -0
  80. supervisely/io/env.py +161 -1
  81. supervisely/nn/artifacts/__init__.py +1 -1
  82. supervisely/nn/artifacts/artifacts.py +10 -2
  83. supervisely/nn/artifacts/detectron2.py +1 -0
  84. supervisely/nn/artifacts/hrda.py +1 -0
  85. supervisely/nn/artifacts/mmclassification.py +20 -0
  86. supervisely/nn/artifacts/mmdetection.py +5 -3
  87. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  88. supervisely/nn/artifacts/ritm.py +1 -0
  89. supervisely/nn/artifacts/rtdetr.py +1 -0
  90. supervisely/nn/artifacts/unet.py +1 -0
  91. supervisely/nn/artifacts/utils.py +3 -0
  92. supervisely/nn/artifacts/yolov5.py +2 -0
  93. supervisely/nn/artifacts/yolov8.py +1 -0
  94. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  95. supervisely/nn/experiments.py +9 -0
  96. supervisely/nn/inference/cache.py +37 -17
  97. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  98. supervisely/nn/inference/inference.py +953 -211
  99. supervisely/nn/inference/inference_request.py +15 -8
  100. supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
  101. supervisely/nn/inference/object_detection/object_detection.py +1 -0
  102. supervisely/nn/inference/predict_app/__init__.py +0 -0
  103. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  104. supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
  105. supervisely/nn/inference/predict_app/gui/gui.py +915 -0
  106. supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
  107. supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
  108. supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
  109. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  110. supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
  111. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  112. supervisely/nn/inference/predict_app/gui/utils.py +399 -0
  113. supervisely/nn/inference/predict_app/predict_app.py +176 -0
  114. supervisely/nn/inference/session.py +47 -39
  115. supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
  116. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  117. supervisely/nn/inference/tracking/tracker_interface.py +4 -0
  118. supervisely/nn/inference/uploader.py +9 -5
  119. supervisely/nn/model/model_api.py +44 -22
  120. supervisely/nn/model/prediction.py +15 -1
  121. supervisely/nn/model/prediction_session.py +70 -14
  122. supervisely/nn/prediction_dto.py +7 -0
  123. supervisely/nn/tracker/__init__.py +6 -8
  124. supervisely/nn/tracker/base_tracker.py +54 -0
  125. supervisely/nn/tracker/botsort/__init__.py +1 -0
  126. supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
  127. supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
  128. supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
  129. supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
  130. supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
  131. supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
  132. supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
  133. supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
  134. supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
  135. supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
  136. supervisely/nn/tracker/botsort_tracker.py +273 -0
  137. supervisely/nn/tracker/calculate_metrics.py +264 -0
  138. supervisely/nn/tracker/utils.py +273 -0
  139. supervisely/nn/tracker/visualize.py +520 -0
  140. supervisely/nn/training/gui/gui.py +152 -49
  141. supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
  142. supervisely/nn/training/gui/model_selector.py +8 -6
  143. supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
  144. supervisely/nn/training/gui/training_artifacts.py +3 -1
  145. supervisely/nn/training/train_app.py +225 -46
  146. supervisely/project/pointcloud_episode_project.py +12 -8
  147. supervisely/project/pointcloud_project.py +12 -8
  148. supervisely/project/project.py +221 -75
  149. supervisely/template/experiment/experiment.html.jinja +105 -55
  150. supervisely/template/experiment/experiment_generator.py +258 -112
  151. supervisely/template/experiment/header.html.jinja +31 -13
  152. supervisely/template/experiment/sly-style.css +7 -2
  153. supervisely/versions.json +3 -1
  154. supervisely/video/sampling.py +42 -20
  155. supervisely/video/video.py +41 -12
  156. supervisely/video_annotation/video_figure.py +38 -4
  157. supervisely/volume/stl_converter.py +2 -0
  158. supervisely/worker_api/agent_rpc.py +24 -1
  159. supervisely/worker_api/rpc_servicer.py +31 -7
  160. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
  161. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
  162. supervisely_lib/__init__.py +6 -1
  163. supervisely/app/widgets/experiment_selector/style.css +0 -27
  164. supervisely/app/widgets/experiment_selector/template.html +0 -61
  165. supervisely/nn/tracker/bot_sort/__init__.py +0 -21
  166. supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
  167. supervisely/nn/tracker/bot_sort/matching.py +0 -127
  168. supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
  169. supervisely/nn/tracker/deep_sort/__init__.py +0 -6
  170. supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
  171. supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
  172. supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
  173. supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
  174. supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
  175. supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
  176. supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
  177. supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
  178. supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
  179. supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
  180. supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
  181. supervisely/nn/tracker/tracker.py +0 -285
  182. supervisely/nn/tracker/utils/kalman_filter.py +0 -492
  183. supervisely/nn/tracking/__init__.py +0 -1
  184. supervisely/nn/tracking/boxmot.py +0 -114
  185. supervisely/nn/tracking/tracking.py +0 -24
  186. /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
  187. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
  188. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
  189. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
  190. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
@@ -12,10 +12,12 @@ from typing import (
12
12
  Any,
13
13
  Callable,
14
14
  Dict,
15
+ Generator,
15
16
  List,
16
17
  Literal,
17
18
  NamedTuple,
18
19
  Optional,
20
+ Tuple,
19
21
  Union,
20
22
  )
21
23
 
@@ -38,12 +40,14 @@ from supervisely.annotation.annotation import TagCollection
38
40
  from supervisely.annotation.obj_class import ObjClass
39
41
  from supervisely.annotation.obj_class_collection import ObjClassCollection
40
42
  from supervisely.annotation.tag_meta import TagMeta, TagValueType
43
+ from supervisely.api.dataset_api import DatasetInfo
41
44
  from supervisely.api.module_api import (
42
45
  ApiField,
43
46
  CloneableModuleApi,
44
47
  RemoveableModuleApi,
45
48
  UpdateableModule,
46
49
  )
50
+ from supervisely.io.env import upload_count, uploaded_ids
47
51
  from supervisely.io.json import dump_json_file, load_json_file
48
52
  from supervisely.project.project_meta import ProjectMeta
49
53
  from supervisely.project.project_meta import ProjectMetaJsonFields as MetaJsonF
@@ -264,9 +268,10 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
264
268
 
265
269
  def get_list(
266
270
  self,
267
- workspace_id: int,
271
+ workspace_id: Optional[int] = None,
268
272
  filters: Optional[List[Dict[str, str]]] = None,
269
273
  fields: List[str] = [],
274
+ team_id: Optional[int] = None,
270
275
  ) -> List[ProjectInfo]:
271
276
  """
272
277
  List of Projects in the given Workspace.
@@ -275,12 +280,13 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
275
280
  If you need version information, use :func:`get_info_by_id`.
276
281
 
277
282
  :param workspace_id: Workspace ID in which the Projects are located.
278
- :type workspace_id: int
283
+ :type workspace_id: int, optional
279
284
  :param filters: List of params to sort output Projects.
280
285
  :type filters: List[dict], optional
281
286
  :param fields: The list of api fields which will be returned with the response. You must specify all fields you want to receive, not just additional ones.
282
287
  :type fields: List[str]
283
-
288
+ :param team_id: Team ID in which the Projects are located.
289
+ :type team_id: int, optional
284
290
  :return: List of all projects with information for the given Workspace. See :class:`info_sequence<info_sequence>`
285
291
  :rtype: :class: `List[ProjectInfo]`
286
292
  :Usage example:
@@ -357,6 +363,11 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
357
363
  # ]
358
364
 
359
365
  """
366
+ if team_id is not None and workspace_id is not None:
367
+ raise ValueError(
368
+ "team_id and workspace_id cannot be used together. Please provide only one of them."
369
+ )
370
+
360
371
  method = "projects.list"
361
372
 
362
373
  debug_message = "While getting list of projects, the following fields are not available: "
@@ -367,11 +378,33 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
367
378
  self.debug_messages_sent["get_list_versions"] = True
368
379
  logger.debug(debug_message + "version. ")
369
380
 
381
+ default_fields = [
382
+ ApiField.ID,
383
+ ApiField.WORKSPACE_ID,
384
+ ApiField.TITLE,
385
+ ApiField.DESCRIPTION,
386
+ ApiField.SIZE,
387
+ ApiField.README,
388
+ ApiField.TYPE,
389
+ ApiField.CREATED_AT,
390
+ ApiField.UPDATED_AT,
391
+ ApiField.CUSTOM_DATA,
392
+ ApiField.GROUP_ID,
393
+ ApiField.CREATED_BY_ID[0][0],
394
+ ]
395
+
396
+ if fields:
397
+ merged_fields = list(set(default_fields + fields))
398
+ fields = list(dict.fromkeys(merged_fields))
399
+
370
400
  data = {
371
- ApiField.WORKSPACE_ID: workspace_id,
372
401
  ApiField.FILTER: filters or [],
373
402
  ApiField.FIELDS: fields,
374
403
  }
404
+ if workspace_id is not None:
405
+ data[ApiField.WORKSPACE_ID] = workspace_id
406
+ if team_id is not None:
407
+ data[ApiField.GROUP_ID] = team_id
375
408
 
376
409
  return self.get_list_all_pages(method, data)
377
410
 
@@ -2129,7 +2162,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2129
2162
  # reference_image_url = None,
2130
2163
  # custom_data = None,
2131
2164
  # backup_archive = None,
2132
- # teamd_id = 1,
2165
+ # team_id = 1,
2133
2166
  # import_settings = {},
2134
2167
  # ),
2135
2168
  # ProjectInfo(id = 23,
@@ -2147,7 +2180,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2147
2180
  # reference_image_url = None,
2148
2181
  # custom_data = None,
2149
2182
  # backup_archive = None),
2150
- # teamd_id = 1,
2183
+ # team_id = 1,
2151
2184
  # import_settings = {},
2152
2185
  # )
2153
2186
  # ]
@@ -2455,6 +2488,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2455
2488
  request_body = {
2456
2489
  ApiField.PROJECT_ID: project_id,
2457
2490
  ApiField.LIMIT: limit,
2491
+ ApiField.UNIQUE_ITEMS: limit, # the same as limit, but for diverse search
2458
2492
  }
2459
2493
 
2460
2494
  if dataset_id is not None:
@@ -2526,3 +2560,176 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2526
2560
  api.project.calculate_embeddings(project_id)
2527
2561
  """
2528
2562
  self._api.post("embeddings.calculate-project-embeddings", {ApiField.PROJECT_ID: id})
2563
+
2564
+ def recreate_structure_generator(
2565
+ self,
2566
+ src_project_id: int,
2567
+ dst_project_id: Optional[int] = None,
2568
+ dst_project_name: Optional[str] = None,
2569
+ ) -> Generator[Tuple[DatasetInfo, DatasetInfo], None, None]:
2570
+ """This method can be used to recreate a project with hierarchial datasets (without the data itself) and
2571
+ yields the tuple of source and destination DatasetInfo objects.
2572
+
2573
+ :param src_project_id: Source project ID
2574
+ :type src_project_id: int
2575
+ :param dst_project_id: Destination project ID
2576
+ :type dst_project_id: int, optional
2577
+ :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.
2578
+ :type dst_project_name: str, optional
2579
+
2580
+ :return: Generator of tuples of source and destination DatasetInfo objects
2581
+ :rtype: Generator[Tuple[DatasetInfo, DatasetInfo], None, None]
2582
+
2583
+ :Usage example:
2584
+
2585
+ .. code-block:: python
2586
+
2587
+ import supervisely as sly
2588
+
2589
+ api = sly.Api.from_env()
2590
+
2591
+ src_project_id = 123
2592
+ dst_project_id = api.project.create("new_project", "images").id
2593
+
2594
+ for src_ds, dst_ds in api.project.recreate_structure_generator(src_project_id, dst_project_id):
2595
+ print(f"Recreated dataset {src_ds.id} -> {dst_ds.id}")
2596
+ # Implement your logic here to process the datasets.
2597
+ """
2598
+ if dst_project_id is None:
2599
+ src_project_info = self._api.project.get_info_by_id(src_project_id)
2600
+ dst_project_info = self._api.project.create(
2601
+ src_project_info.workspace_id,
2602
+ dst_project_name or f"Recreation of {src_project_info.name}",
2603
+ src_project_info.type,
2604
+ src_project_info.description,
2605
+ change_name_if_conflict=True,
2606
+ )
2607
+ dst_project_id = dst_project_info.id
2608
+
2609
+ datasets = self._api.dataset.get_list(src_project_id, recursive=True, include_custom_data=True)
2610
+ src_to_dst_ids = {}
2611
+
2612
+ for src_dataset_info in datasets:
2613
+ dst_dataset_info = self._api.dataset.create(
2614
+ dst_project_id,
2615
+ src_dataset_info.name,
2616
+ description=src_dataset_info.description,
2617
+ parent_id=src_to_dst_ids.get(src_dataset_info.parent_id),
2618
+ custom_data=src_dataset_info.custom_data,
2619
+ )
2620
+ src_to_dst_ids[src_dataset_info.id] = dst_dataset_info.id
2621
+
2622
+ yield src_dataset_info, dst_dataset_info
2623
+
2624
+ def recreate_structure(
2625
+ self,
2626
+ src_project_id: int,
2627
+ dst_project_id: Optional[int] = None,
2628
+ dst_project_name: Optional[str] = None,
2629
+ ) -> Tuple[List[DatasetInfo], List[DatasetInfo]]:
2630
+ """This method can be used to recreate a project with hierarchial datasets (without the data itself).
2631
+
2632
+ :param src_project_id: Source project ID
2633
+ :type src_project_id: int
2634
+ :param dst_project_id: Destination project ID
2635
+ :type dst_project_id: int, optional
2636
+ :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
+ :type dst_project_name: str, optional
2638
+
2639
+ :return: Destination project ID
2640
+ :rtype: int
2641
+
2642
+ :Usage example:
2643
+
2644
+ .. code-block:: python
2645
+
2646
+ import supervisely as sly
2647
+
2648
+ api = sly.Api.from_env()
2649
+
2650
+ src_project_id = 123
2651
+ dst_project_name = "New Project"
2652
+
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}")
2655
+ """
2656
+ infos = []
2657
+ for src_info, dst_info in self.recreate_structure_generator(
2658
+ src_project_id, dst_project_id, dst_project_name
2659
+ ):
2660
+ infos.append((src_info, dst_info))
2661
+
2662
+ return infos
2663
+
2664
+ def add_import_history(self, id: int, task_id: int) -> None:
2665
+ """
2666
+ Adds import history to project info. Gets task info and adds it to project custom data.
2667
+
2668
+ :param id: Project ID
2669
+ :type id: int
2670
+ :param task_id: Task ID
2671
+ :type task_id: int
2672
+ :return: None
2673
+ :rtype: :class:`NoneType`
2674
+ :Usage example:
2675
+ .. code-block:: python
2676
+ import os
2677
+ from dotenv import load_dotenv
2678
+
2679
+ import supervisely as sly
2680
+
2681
+ # Load secrets and create API object from .env file (recommended)
2682
+ # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
2683
+ load_dotenv(os.path.expanduser("~/supervisely.env"))
2684
+ api = sly.Api.from_env()
2685
+
2686
+ project_id = 123
2687
+ task_id = 456
2688
+ api.project.add_import_history(project_id, task_id)
2689
+ """
2690
+
2691
+ task_info = self._api.task.get_info_by_id(task_id)
2692
+ module_id = task_info.get("meta", {}).get("app", {}).get("moduleId")
2693
+ slug = None
2694
+ if module_id is not None:
2695
+ module_info = self._api.app.get_ecosystem_module_info(module_id)
2696
+ slug = module_info.slug
2697
+
2698
+ items_count = upload_count()
2699
+ items_count = {int(k): v for k, v in items_count.items()}
2700
+ uploaded_images = uploaded_ids()
2701
+ uploaded_images = {int(k): v for k, v in uploaded_images.items()}
2702
+ total_items = sum(items_count.values()) if len(items_count) > 0 else 0
2703
+ app = task_info.get("meta", {}).get("app")
2704
+ app_name = app.get("name") if app else None
2705
+ app_version = app.get("version") if app else None
2706
+ data = {
2707
+ "task_id": task_id,
2708
+ "app": {"name": app_name, "version": app_version},
2709
+ "slug": slug,
2710
+ "status": task_info.get(ApiField.STATUS),
2711
+ "user_id": task_info.get(ApiField.USER_ID),
2712
+ "team_id": task_info.get(ApiField.TEAM_ID),
2713
+ "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
2714
+ "source_state": task_info.get("settings", {}).get("message", {}).get("state"),
2715
+ "items_count": total_items,
2716
+ "datasets": [
2717
+ {
2718
+ "id": ds,
2719
+ "items_count": items_count[ds],
2720
+ "uploaded_images": uploaded_images.get(ds, []),
2721
+ }
2722
+ for ds in items_count.keys()
2723
+ ],
2724
+ }
2725
+
2726
+ project_info = self.get_info_by_id(id)
2727
+
2728
+ custom_data = project_info.custom_data or {}
2729
+ if "import_history" not in custom_data:
2730
+ custom_data["import_history"] = {"tasks": []}
2731
+ if "tasks" not in custom_data["import_history"]:
2732
+ custom_data["import_history"]["tasks"] = []
2733
+ custom_data["import_history"]["tasks"].append(data)
2734
+
2735
+ self.edit_info(id, custom_data=custom_data)
@@ -24,6 +24,7 @@ from supervisely.api.module_api import (
24
24
  WaitingTimeExceeded,
25
25
  )
26
26
  from supervisely.collection.str_enum import StrEnum
27
+ from supervisely.io.env import app_categories
27
28
  from supervisely.io.fs import (
28
29
  ensure_base_path,
29
30
  get_file_hash,
@@ -652,6 +653,8 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
652
653
  project_preview: Optional[str] = None,
653
654
  ) -> Dict:
654
655
  """set_output_project"""
656
+ if "import" in app_categories():
657
+ self._api.project.add_import_history(project_id, task_id)
655
658
  if project_name is None:
656
659
  project = self._api.project.get_info_by_id(project_id, raise_error=True)
657
660
  project_name = project.name
@@ -1007,11 +1010,12 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
1007
1010
  Example of experiment_info:
1008
1011
 
1009
1012
  experiment_info = {
1010
- 'experiment_name': '247_Lemons_RT-DETRv2-M',
1013
+ 'experiment_name': '247 Lemons RT-DETRv2-M',
1011
1014
  'framework_name': 'RT-DETRv2',
1012
1015
  'model_name': 'RT-DETRv2-M',
1013
1016
  'task_type': 'object detection',
1014
1017
  'project_id': 76,
1018
+ 'project_version': {'id': 222, 'version': 4},
1015
1019
  'task_id': 247,
1016
1020
  'model_files': {'config': 'model_config.yml'},
1017
1021
  'checkpoints': ['checkpoints/best.pth', 'checkpoints/checkpoint0025.pth', 'checkpoints/checkpoint0050.pth', 'checkpoints/last.pth'],
@@ -1022,10 +1026,13 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
1022
1026
  'train_val_split': 'train_val_split.json',
1023
1027
  'train_size': 4,
1024
1028
  'val_size': 2,
1029
+ 'train_collection_id': 530,
1030
+ 'val_collection_id': 531,
1025
1031
  'hyperparameters': 'hyperparameters.yaml',
1026
1032
  'hyperparameters_id': 45234,
1027
1033
  'artifacts_dir': '/experiments/76_Lemons/247_RT-DETRv2/',
1028
1034
  'datetime': '2025-01-22 18:13:43',
1035
+ 'experiment_report_id': 87654,
1029
1036
  'evaluation_report_id': 12961,
1030
1037
  'evaluation_report_link': 'https://app.supervisely.com/model-benchmark?id=12961',
1031
1038
  'evaluation_metrics': {
@@ -1046,6 +1053,9 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
1046
1053
  'type': 'tensorboard',
1047
1054
  'link': '/experiments/76_Lemons/247_RT-DETRv2/logs/'
1048
1055
  },
1056
+ # These fields are present only in task_info
1057
+ 'project_preview': 'https://app.supervisely.com/...',
1058
+ 'has_report': True,
1049
1059
  }
1050
1060
  """
1051
1061
  output = {
@@ -236,11 +236,13 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
236
236
  dst_project_meta = ProjectMeta.from_json(
237
237
  self._api.project.get_meta(dst_dataset_info.project_id)
238
238
  )
239
- for src_ids_batch, dst_ids_batch in batched(list(zip(src_video_ids, dst_video_ids))):
239
+ for src_ids_batch, dst_ids_batch in zip(batched(src_video_ids), batched(dst_video_ids)):
240
240
  ann_jsons = self.download_bulk(src_dataset_id, src_ids_batch)
241
241
  for dst_id, ann_json in zip(dst_ids_batch, ann_jsons):
242
242
  try:
243
- ann = VideoAnnotation.from_json(ann_json, dst_project_meta)
243
+ ann = VideoAnnotation.from_json(
244
+ ann_json, dst_project_meta, key_id_map=KeyIdMap()
245
+ )
244
246
  except Exception as e:
245
247
  raise RuntimeError("Failed to validate Annotation") from e
246
248
  self.append(dst_id, ann)
@@ -5,6 +5,7 @@ import asyncio
5
5
  import datetime
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import urllib.parse
9
10
  from functools import partial
10
11
  from typing import (
@@ -23,7 +24,11 @@ from typing import (
23
24
  import aiofiles
24
25
  from numerize.numerize import numerize
25
26
  from requests import Response
26
- from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
27
+ from requests_toolbelt import (
28
+ MultipartDecoder,
29
+ MultipartEncoder,
30
+ MultipartEncoderMonitor,
31
+ )
27
32
  from tqdm import tqdm
28
33
 
29
34
  import supervisely.io.fs as sly_fs
@@ -1186,6 +1191,41 @@ class VideoApi(RemoveableBulkModuleApi):
1186
1191
  if progress_cb is not None:
1187
1192
  progress_cb(len(chunk))
1188
1193
 
1194
+ def download_frames(
1195
+ self, video_id: int, frames: List[int], paths: List[str], progress_cb=None
1196
+ ) -> None:
1197
+ endpoint = "videos.bulk.download-frame"
1198
+ response: Response = self._api.get(
1199
+ endpoint,
1200
+ params={},
1201
+ data={ApiField.VIDEO_ID: video_id, ApiField.FRAMES: frames},
1202
+ stream=True,
1203
+ )
1204
+ response.raise_for_status()
1205
+
1206
+ files = {frame_n: None for frame_n in frames}
1207
+ file_paths = {frame_n: path for frame_n, path in zip(frames, paths)}
1208
+
1209
+ try:
1210
+ decoder = MultipartDecoder.from_response(response)
1211
+ for part in decoder.parts:
1212
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
1213
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
1214
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
1215
+ frame_n = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
1216
+ if files[frame_n] is None:
1217
+ file_path = file_paths[frame_n]
1218
+ files[frame_n] = open(file_path, "wb")
1219
+ if progress_cb is not None:
1220
+ progress_cb(1)
1221
+ f = files[frame_n]
1222
+ f.write(part.content)
1223
+
1224
+ finally:
1225
+ for f in files.values():
1226
+ if f is not None:
1227
+ f.close()
1228
+
1189
1229
  def download_range_by_id(
1190
1230
  self,
1191
1231
  id: int,
@@ -2610,3 +2650,41 @@ class VideoApi(RemoveableBulkModuleApi):
2610
2650
 
2611
2651
  tasks.append(task)
2612
2652
  await asyncio.gather(*tasks)
2653
+
2654
+ def rename(
2655
+ self,
2656
+ id: int,
2657
+ name: str,
2658
+ ) -> VideoInfo:
2659
+ """Renames Video with given ID to a new name.
2660
+
2661
+ :param id: Video ID in Supervisely.
2662
+ :type id: int
2663
+ :param name: New Video name.
2664
+ :type name: str
2665
+ :return: Information about updated Video.
2666
+ :rtype: :class:`VideoInfo`
2667
+
2668
+ :Usage example:
2669
+
2670
+ .. code-block:: python
2671
+
2672
+ import supervisely as sly
2673
+
2674
+ api = sly.Api.from_env()
2675
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
2676
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
2677
+
2678
+ video_id = 123456
2679
+ new_video_name = "VID_3333_new.mp4"
2680
+
2681
+ api.video.rename(id=video_id, name=new_video_name)
2682
+ """
2683
+
2684
+ data = {
2685
+ ApiField.ID: id,
2686
+ ApiField.NAME: name,
2687
+ }
2688
+
2689
+ response = self._api.post("images.editInfo", data)
2690
+ return self._convert_json_info(response.json())
@@ -10,7 +10,7 @@ from supervisely.api.module_api import ApiField
10
10
  from supervisely.geometry.geometry import Geometry
11
11
  from supervisely.video_annotation.key_id_map import KeyIdMap
12
12
  from supervisely.video_annotation.video_figure import VideoFigure
13
-
13
+ from supervisely.annotation.label import LabelingStatus
14
14
 
15
15
  class VideoFigureApi(FigureApi):
16
16
  """
@@ -26,6 +26,7 @@ class VideoFigureApi(FigureApi):
26
26
  geometry_type: str,
27
27
  track_id: Optional[int] = None,
28
28
  meta: Optional[dict] = None,
29
+ status: Optional[LabelingStatus] = None,
29
30
  ) -> int:
30
31
  """
31
32
  Create new VideoFigure for given frame in given video ID.
@@ -42,6 +43,10 @@ class VideoFigureApi(FigureApi):
42
43
  :type geometry_type: str
43
44
  :param track_id: int, optional.
44
45
  :type track_id: int, optional
46
+ :param meta: Meta data for VideoFigure.
47
+ :type meta: dict, optional
48
+ :param status: Labeling status. Specifies if the VideoFigure was created by NN model, manually or created by NN and then manually corrected.
49
+ :type status: LabelingStatus, optional
45
50
  :return: New figure ID
46
51
  :rtype: :class:`int`
47
52
  :Usage example:
@@ -64,13 +69,16 @@ class VideoFigureApi(FigureApi):
64
69
  """
65
70
  if meta is None:
66
71
  meta = {}
72
+ meta = {**(meta or {}), ApiField.FRAME: frame_index}
73
+
67
74
  return super().create(
68
75
  video_id,
69
76
  object_id,
70
- {**meta, ApiField.FRAME: frame_index},
77
+ meta,
71
78
  geometry_json,
72
79
  geometry_type,
73
80
  track_id,
81
+ status=status,
74
82
  )
75
83
 
76
84
  def append_bulk(self, video_id: int, figures: List[VideoFigure], key_id_map: KeyIdMap) -> None:
@@ -115,13 +123,15 @@ class VideoFigureApi(FigureApi):
115
123
 
116
124
  self._append_bulk(video_id, figures_json, keys, key_id_map)
117
125
 
118
- def update(self, figure_id: int, geometry: Geometry) -> None:
126
+ def update(self, figure_id: int, geometry: Geometry, status: Optional[LabelingStatus] = None) -> None:
119
127
  """Updates figure feometry with given ID in Supervisely with new Geometry object.
120
128
 
121
129
  :param figure_id: ID of the figure to update
122
130
  :type figure_id: int
123
131
  :param geometry: Supervisely Gepmetry object
124
132
  :type geometry: Geometry
133
+ :param status: Labeling status. Specifies if the VideoFigure was created by NN model, manually or created by NN and then manually corrected.
134
+ :type status: LabelingStatus, optional
125
135
  :Usage example:
126
136
 
127
137
  .. code-block:: python
@@ -141,13 +151,17 @@ class VideoFigureApi(FigureApi):
141
151
 
142
152
  api.video.figure.update(figure_id, new_geometry)
143
153
  """
144
- self._api.post(
145
- "figures.editInfo",
146
- {
147
- ApiField.ID: figure_id,
148
- ApiField.GEOMETRY: geometry.to_json(),
149
- },
150
- )
154
+ payload = {
155
+ ApiField.ID: figure_id,
156
+ ApiField.GEOMETRY: geometry.to_json(),
157
+ }
158
+
159
+ if status is not None:
160
+ nn_created,nn_updated = LabelingStatus.to_flags(status)
161
+ payload[ApiField.NN_CREATED] = nn_created
162
+ payload[ApiField.NN_UPDATED] = nn_updated
163
+
164
+ self._api.post("figures.editInfo", payload)
151
165
 
152
166
  def download(
153
167
  self, dataset_id: int, video_ids: List[int] = None, skip_geometry: bool = False, **kwargs
@@ -161,7 +175,6 @@ class VideoFigureApi(FigureApi):
161
175
  :type video_ids: List[int], optional
162
176
  :param skip_geometry: Skip the download of figure geometry. May be useful for a significant api request speed increase in the large datasets.
163
177
  :type skip_geometry: bool
164
-
165
178
  :return: A dictionary where keys are video IDs and values are lists of figures.
166
179
  :rtype: :class: `Dict[int, List[FigureInfo]]`
167
180
  """
@@ -1455,3 +1455,41 @@ class VolumeApi(RemoveableBulkModuleApi):
1455
1455
  )
1456
1456
  tasks.append(task)
1457
1457
  await asyncio.gather(*tasks)
1458
+
1459
+ def rename(
1460
+ self,
1461
+ id: int,
1462
+ name: str,
1463
+ ) -> VolumeInfo:
1464
+ """Renames Volume with given ID to a new name.
1465
+
1466
+ :param id: Volume ID in Supervisely.
1467
+ :type id: int
1468
+ :param name: New Volume name.
1469
+ :type name: str
1470
+ :return: Information about updated Volume.
1471
+ :rtype: :class:`VolumeInfo`
1472
+
1473
+ :Usage example:
1474
+
1475
+ .. code-block:: python
1476
+
1477
+ import supervisely as sly
1478
+
1479
+ api = sly.Api.from_env()
1480
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1481
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1482
+
1483
+ volume_id = 123456
1484
+ new_volume_name = "3333_new.nrrd"
1485
+
1486
+ api.volume.rename(id=volume_id, name=new_volume_name)
1487
+ """
1488
+
1489
+ data = {
1490
+ ApiField.ID: id,
1491
+ ApiField.NAME: name,
1492
+ }
1493
+
1494
+ response = self._api.post("images.editInfo", data)
1495
+ return self._convert_json_info(response.json())
@@ -1,7 +1,7 @@
1
1
  from fastapi import FastAPI
2
2
  from supervisely.app.content import StateJson, DataJson
3
3
  from supervisely.app.content import get_data_dir, get_synced_data_dir
4
- from supervisely.app.fastapi.subapp import call_on_autostart
4
+ from supervisely.app.fastapi.subapp import call_on_autostart, session_user_api
5
5
  import supervisely.app.fastapi as fastapi
6
6
  import supervisely.app.widgets as widgets
7
7
  import supervisely.app.development as development
@@ -11,6 +11,7 @@ import threading
11
11
  import time
12
12
  import traceback
13
13
  from concurrent.futures import ThreadPoolExecutor
14
+ from typing import Optional, Union
14
15
 
15
16
  import jsonpatch
16
17
  from fastapi import Request
@@ -109,16 +110,22 @@ class _PatchableJson(dict):
109
110
  patch.apply(self._last, in_place=True)
110
111
  self._last = copy.deepcopy(self._last)
111
112
 
112
- async def synchronize_changes(self):
113
+ async def synchronize_changes(self, user_id: Optional[Union[int, str]] = None):
113
114
  patch = self._get_patch()
114
115
  await self._apply_patch(patch)
115
- await self._ws.broadcast(self.get_changes(patch))
116
+ await self._ws.broadcast(self.get_changes(patch), user_id=user_id)
116
117
 
117
118
  async def send_changes_async(self):
118
- await self.synchronize_changes()
119
+ user_id = None
120
+ if sly_env.is_multiuser_mode_enabled():
121
+ user_id = sly_env.user_from_multiuser_app()
122
+ await self.synchronize_changes(user_id=user_id)
119
123
 
120
124
  def send_changes(self):
121
- run_sync(self.synchronize_changes())
125
+ user_id = None
126
+ if sly_env.is_multiuser_mode_enabled():
127
+ user_id = sly_env.user_from_multiuser_app()
128
+ run_sync(self.synchronize_changes(user_id=user_id))
122
129
 
123
130
  def raise_for_key(self, key: str):
124
131
  if key in self:
@@ -139,7 +146,7 @@ class StateJson(_PatchableJson, metaclass=Singleton):
139
146
  await StateJson._replace_global(dict(self))
140
147
 
141
148
  @classmethod
142
- async def from_request(cls, request: Request) -> StateJson:
149
+ async def from_request(cls, request: Request, local: bool = True) -> StateJson:
143
150
  if "application/json" not in request.headers.get("Content-Type", ""):
144
151
  return None
145
152
  content = await request.json()
@@ -149,7 +156,8 @@ class StateJson(_PatchableJson, metaclass=Singleton):
149
156
  # TODO: should we always replace STATE with {}?
150
157
  d = content.get(Field.STATE, {})
151
158
  await cls._replace_global(d)
152
- return cls(d, __local__=True)
159
+
160
+ return cls(d, __local__=local)
153
161
 
154
162
  @classmethod
155
163
  async def _replace_global(cls, d: dict):
@@ -5,6 +5,7 @@ from supervisely.app.fastapi.subapp import (
5
5
  Application,
6
6
  get_name_from_env,
7
7
  _MainServer,
8
+ session_user_api,
8
9
  )
9
10
  from supervisely.app.fastapi.templating import Jinja2Templates
10
11
  from supervisely.app.fastapi.websocket import WebsocketManager