supervisely 6.73.438__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 (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -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(
@@ -228,7 +228,9 @@ class StorageApi(FileApi):
228
228
  path_infos = self.list(team_id, parent_dir, recursive=False, return_type="dict")
229
229
  for info in path_infos:
230
230
  if info["type"] == path_type:
231
- if info["path"].rstrip("/") == remote_path.rstrip("/"):
231
+ if path_type == "file" and info["path"] == remote_path:
232
+ return True
233
+ elif path_type == "folder" and info["path"].rstrip("/") == remote_path.rstrip("/"):
232
234
  return True
233
235
  return False
234
236
 
@@ -390,6 +390,7 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
390
390
  redirect_requests: Optional[Dict[str, int]] = {},
391
391
  limit_by_workspace: bool = False,
392
392
  kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]] = None,
393
+ multi_user_session: bool = False,
393
394
  ) -> Dict[str, Any]:
394
395
  """Starts the application task on the agent.
395
396
 
@@ -428,6 +429,11 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
428
429
  :type limit_by_workspace: bool, optional
429
430
  :param kubernetes_settings: Kubernetes settings for the application.
430
431
  :type kubernetes_settings: Union[KubernetesSettings, Dict[str, Any]], optional
432
+ :param multi_user_session: If True, the application session will be created as multi-user.
433
+ In this case, multiple users will be able to connect to the same application session.
434
+ All users will have separate application states.
435
+ Available only for applications that support multi-user sessions.
436
+ :type multi_user_session: bool, default is False
431
437
  :return: Task information in JSON format.
432
438
  :rtype: Dict[str, Any]
433
439
 
@@ -497,6 +503,11 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
497
503
  data[ApiField.APP_ID] = app_id
498
504
  if module_id is not None:
499
505
  data[ApiField.MODULE_ID] = module_id
506
+ if multi_user_session:
507
+ # * Enables single multi-user session mode for all users in the users_ids list.
508
+ # * Otherwise, if users_ids contains multiple IDs, separate single-user sessions will be created for each.
509
+ # * If users_ids is empty, a session is created only for the current user.
510
+ data[ApiField.SINGLE_SESSION_MODE] = multi_user_session
500
511
  resp = self._api.post(method="tasks.run.app", data=data)
501
512
  task = resp.json()[0]
502
513
  if "id" not in task:
@@ -805,8 +816,8 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
805
816
  ):
806
817
  """
807
818
  Update given task metadata
808
- :param id: int task id
809
- :param data: dict meta data to update
819
+ :param id: int - task id
820
+ :param data: dict - meta data to update
810
821
  """
811
822
  if type(data) == dict:
812
823
  data.update({"id": id})
@@ -132,7 +132,7 @@ class ActivityAction:
132
132
  class UsageInfo(NamedTuple):
133
133
  """ """
134
134
 
135
- plan: str
135
+ plan: Optional[str]
136
136
 
137
137
 
138
138
  class TeamInfo(NamedTuple):
@@ -144,7 +144,7 @@ class TeamInfo(NamedTuple):
144
144
  role: str
145
145
  created_at: str
146
146
  updated_at: str
147
- usage: UsageInfo
147
+ usage: Optional[UsageInfo]
148
148
 
149
149
 
150
150
  class TeamApi(ModuleNoParent, UpdateableModule):
@@ -565,5 +565,6 @@ class TeamApi(ModuleNoParent, UpdateableModule):
565
565
  res = super()._convert_json_info(info, skip_missing=skip_missing)
566
566
  res_dict = res._asdict()
567
567
  if isinstance(res_dict.get("usage"), dict):
568
- res_dict["usage"] = UsageInfo(**res_dict["usage"])
568
+ usage_dict = {f: res_dict["usage"].get(f) for f in UsageInfo._fields}
569
+ res_dict["usage"] = UsageInfo(**usage_dict)
569
570
  return TeamInfo(**res_dict)
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
+ from collections import defaultdict
5
6
  from typing import Callable, Dict, List, Optional, Union
6
7
 
7
8
  from tqdm import tqdm
@@ -13,6 +14,7 @@ from supervisely.io.json import load_json_file
13
14
  from supervisely.project.project_meta import ProjectMeta
14
15
  from supervisely.video_annotation.key_id_map import KeyIdMap
15
16
  from supervisely.video_annotation.video_annotation import VideoAnnotation
17
+ from supervisely.video_annotation.video_tag_collection import VideoTagCollection
16
18
 
17
19
 
18
20
  class VideoAnnotationAPI(EntityAnnotationAPI):
@@ -173,7 +175,6 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
173
175
  api.video.annotation.upload_paths(video_ids, ann_paths, meta)
174
176
  """
175
177
  # video_ids from the same dataset
176
-
177
178
  for video_id, ann_path in zip(video_ids, ann_paths):
178
179
  ann_json = load_json_file(ann_path)
179
180
  ann = VideoAnnotation.from_json(ann_json, project_meta)
@@ -183,6 +184,119 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
183
184
  if progress_cb is not None:
184
185
  progress_cb(1)
185
186
 
187
+ def upload_paths_multiview(
188
+ self,
189
+ video_ids: List[int],
190
+ ann_paths: List[str],
191
+ project_meta: ProjectMeta,
192
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
193
+ ) -> None:
194
+ """
195
+ Upload VideoAnnotations for multi-view video project.
196
+ All provided video ids must belong to the same project and dataset.
197
+
198
+ Objects with the same key are created only once and shared between videos.
199
+ In this mode annotation objects are created without binding to a specific entityId.
200
+
201
+ :param video_ids: Video IDs in Supervisely.
202
+ :type video_ids: List[int]
203
+ :param ann_paths: Paths to annotations on local machine.
204
+ :type ann_paths: List[str]
205
+ :param project_meta: Input :class:`ProjectMeta<supervisely.project.project_meta.ProjectMeta>` for VideoAnnotations.
206
+ :type project_meta: ProjectMeta
207
+ :param progress_cb: Function for tracking upload progress.
208
+ :type progress_cb: tqdm or callable, optional
209
+ :return: None
210
+ :rtype: :class:`NoneType`
211
+ """
212
+ if len(video_ids) != len(ann_paths):
213
+ raise RuntimeError(
214
+ f'Can not match "video_ids" and "ann_paths" lists, len(video_ids) != len(ann_paths): {len(video_ids)} != {len(ann_paths)}'
215
+ )
216
+ if len(video_ids) == 0:
217
+ return
218
+
219
+ anns = []
220
+ for ann_path in ann_paths:
221
+ ann_json = load_json_file(ann_path)
222
+ ann = VideoAnnotation.from_json(ann_json, project_meta)
223
+ anns.append(ann)
224
+
225
+ self.upload_anns_multiview(video_ids, anns, progress_cb)
226
+
227
+ def upload_anns_multiview(
228
+ self,
229
+ video_ids: List[int],
230
+ anns: List[VideoAnnotation],
231
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
232
+ ) -> None:
233
+ """
234
+ Upload already constructed VideoAnnotation objects for multi-view video project.
235
+ All provided video ids must belong to the same project and dataset.
236
+
237
+ Objects with the same key are created only once and shared between videos.
238
+ In this mode annotation objects are created without binding to a specific entityId.
239
+
240
+ :param video_ids: Video IDs in Supervisely.
241
+ :type video_ids: List[int]
242
+ :param anns: List of VideoAnnotation objects corresponding to the video_ids.
243
+ :type anns: List[VideoAnnotation]
244
+ :param progress_cb: Function for tracking upload progress (by number of figures).
245
+ :type progress_cb: tqdm or callable, optional
246
+ :return: None
247
+ :rtype: :class:`NoneType`
248
+ """
249
+ if len(video_ids) != len(anns):
250
+ raise RuntimeError(
251
+ 'Can not match "video_ids" and "anns" lists, len(video_ids) != len(anns)'
252
+ )
253
+ if len(video_ids) == 0:
254
+ return
255
+
256
+ try:
257
+ video_infos = self._api.video.get_info_by_id_batch(video_ids)
258
+ except RuntimeError as e:
259
+ raise RuntimeError("All videos must belong to the same project and dataset.") from e
260
+
261
+ project_id = video_infos[0].project_id
262
+ dataset_id = video_infos[0].dataset_id
263
+
264
+ tag_api = self._api.video.tag
265
+ object_api = self._api.video.object
266
+ figure_api = self._api.video.figure
267
+
268
+ key_id_map = KeyIdMap()
269
+ for video_id, ann in zip(video_ids, anns):
270
+ tag_api.append_to_entity(video_id, project_id, ann.tags, key_id_map=key_id_map)
271
+ new_objects = []
272
+ for obj in ann.objects:
273
+ if key_id_map.get_object_id(obj.key()) is None:
274
+ new_objects.append(obj)
275
+ if len(new_objects) > 0:
276
+ object_api._append_bulk(
277
+ tag_api=tag_api,
278
+ entity_id=video_id,
279
+ project_id=project_id,
280
+ dataset_id=dataset_id,
281
+ objects=new_objects,
282
+ key_id_map=key_id_map,
283
+ is_pointcloud=False,
284
+ is_video_multi_view=True,
285
+ )
286
+ tags_to_obj = {}
287
+ for obj in ann.objects:
288
+ obj_id = key_id_map.get_object_id(obj.key())
289
+ tags_to_obj[obj_id] = obj.tags
290
+ if len(tags_to_obj) > 0:
291
+ tag_api.add_tags_collection_to_objects(project_id, tags_to_obj, is_video_multi_view=True, entity_id=video_id)
292
+
293
+ figure_api.append_bulk(video_id, ann.figures, key_id_map)
294
+ if progress_cb is not None and len(ann.figures) > 0:
295
+ if hasattr(progress_cb, "update") and callable(getattr(progress_cb, "update")):
296
+ progress_cb.update(len(ann.figures))
297
+ else:
298
+ progress_cb(len(ann.figures))
299
+
186
300
  def copy_batch(
187
301
  self,
188
302
  src_video_ids: List[int],
@@ -236,11 +350,13 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
236
350
  dst_project_meta = ProjectMeta.from_json(
237
351
  self._api.project.get_meta(dst_dataset_info.project_id)
238
352
  )
239
- for src_ids_batch, dst_ids_batch in batched(list(zip(src_video_ids, dst_video_ids))):
353
+ for src_ids_batch, dst_ids_batch in zip(batched(src_video_ids), batched(dst_video_ids)):
240
354
  ann_jsons = self.download_bulk(src_dataset_id, src_ids_batch)
241
355
  for dst_id, ann_json in zip(dst_ids_batch, ann_jsons):
242
356
  try:
243
- ann = VideoAnnotation.from_json(ann_json, dst_project_meta)
357
+ ann = VideoAnnotation.from_json(
358
+ ann_json, dst_project_meta, key_id_map=KeyIdMap()
359
+ )
244
360
  except Exception as e:
245
361
  raise RuntimeError("Failed to validate Annotation") from e
246
362
  self.append(dst_id, ann)
@@ -5,8 +5,10 @@ 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
11
+ from itertools import zip_longest
10
12
  from typing import (
11
13
  AsyncGenerator,
12
14
  Callable,
@@ -23,7 +25,11 @@ from typing import (
23
25
  import aiofiles
24
26
  from numerize.numerize import numerize
25
27
  from requests import Response
26
- from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
28
+ from requests_toolbelt import (
29
+ MultipartDecoder,
30
+ MultipartEncoder,
31
+ MultipartEncoderMonitor,
32
+ )
27
33
  from tqdm import tqdm
28
34
 
29
35
  import supervisely.io.fs as sly_fs
@@ -46,6 +52,7 @@ from supervisely.io.fs import (
46
52
  get_file_hash,
47
53
  get_file_hash_async,
48
54
  get_file_hash_chunked,
55
+ get_file_hash_chunked_async,
49
56
  get_file_name_with_ext,
50
57
  get_file_size,
51
58
  list_files,
@@ -700,7 +707,7 @@ class VideoApi(RemoveableBulkModuleApi):
700
707
  return project_id, dataset_id
701
708
 
702
709
  def upload_hash(
703
- self, dataset_id: int, name: str, hash: str, stream_index: Optional[int] = None
710
+ self, dataset_id: int, name: str, hash: str, stream_index: Optional[int] = None, metadata: Optional[Dict] = None
704
711
  ) -> VideoInfo:
705
712
  """
706
713
  Upload Video from given hash to Dataset.
@@ -713,6 +720,8 @@ class VideoApi(RemoveableBulkModuleApi):
713
720
  :type hash: str
714
721
  :param stream_index: Index of video stream.
715
722
  :type stream_index: int, optional
723
+ :param metadata: Video metadata.
724
+ :type metadata: dict, optional
716
725
  :return: Information about Video. See :class:`info_sequence<info_sequence>`
717
726
  :rtype: :class:`VideoInfo`
718
727
  :Usage example:
@@ -781,6 +790,8 @@ class VideoApi(RemoveableBulkModuleApi):
781
790
  meta = {}
782
791
  if stream_index is not None and type(stream_index) is int:
783
792
  meta = {"videoStreamIndex": stream_index}
793
+ if metadata is not None:
794
+ meta.update(metadata)
784
795
  return self.upload_hashes(dataset_id, [name], [hash], [meta])[0]
785
796
 
786
797
  def upload_hashes(
@@ -1106,10 +1117,10 @@ class VideoApi(RemoveableBulkModuleApi):
1106
1117
  validate_ext(os.path.splitext(name)[1])
1107
1118
 
1108
1119
  for batch in batched(list(zip(names, items, metas))):
1109
- images = []
1120
+ videos = []
1110
1121
  for name, item, meta in batch:
1111
1122
  item_tuple = func_item_to_kv(item)
1112
- images.append(
1123
+ videos.append(
1113
1124
  {
1114
1125
  "title": name,
1115
1126
  item_tuple[0]: item_tuple[1],
@@ -1120,12 +1131,12 @@ class VideoApi(RemoveableBulkModuleApi):
1120
1131
  "videos.bulk.add",
1121
1132
  {
1122
1133
  ApiField.DATASET_ID: dataset_id,
1123
- ApiField.VIDEOS: images,
1134
+ ApiField.VIDEOS: videos,
1124
1135
  ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
1125
1136
  },
1126
1137
  )
1127
1138
  if progress_cb is not None:
1128
- progress_cb(len(images))
1139
+ progress_cb(len(videos))
1129
1140
 
1130
1141
  results = [self._convert_json_info(item) for item in response.json()]
1131
1142
  name_to_res = {img_info.name: img_info for img_info in results}
@@ -1186,6 +1197,41 @@ class VideoApi(RemoveableBulkModuleApi):
1186
1197
  if progress_cb is not None:
1187
1198
  progress_cb(len(chunk))
1188
1199
 
1200
+ def download_frames(
1201
+ self, video_id: int, frames: List[int], paths: List[str], progress_cb=None
1202
+ ) -> None:
1203
+ endpoint = "videos.bulk.download-frame"
1204
+ response: Response = self._api.get(
1205
+ endpoint,
1206
+ params={},
1207
+ data={ApiField.VIDEO_ID: video_id, ApiField.FRAMES: frames},
1208
+ stream=True,
1209
+ )
1210
+ response.raise_for_status()
1211
+
1212
+ files = {frame_n: None for frame_n in frames}
1213
+ file_paths = {frame_n: path for frame_n, path in zip(frames, paths)}
1214
+
1215
+ try:
1216
+ decoder = MultipartDecoder.from_response(response)
1217
+ for part in decoder.parts:
1218
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
1219
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
1220
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
1221
+ frame_n = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
1222
+ if files[frame_n] is None:
1223
+ file_path = file_paths[frame_n]
1224
+ files[frame_n] = open(file_path, "wb")
1225
+ if progress_cb is not None:
1226
+ progress_cb(1)
1227
+ f = files[frame_n]
1228
+ f.write(part.content)
1229
+
1230
+ finally:
1231
+ for f in files.values():
1232
+ if f is not None:
1233
+ f.close()
1234
+
1189
1235
  def download_range_by_id(
1190
1236
  self,
1191
1237
  id: int,
@@ -1536,15 +1582,20 @@ class VideoApi(RemoveableBulkModuleApi):
1536
1582
  for hash_value, meta in zip(unique_hashes, unique_metas):
1537
1583
  hash_meta_dict[hash_value] = meta
1538
1584
 
1539
- metas = [hash_meta_dict[hash_value] for hash_value in hashes]
1540
-
1541
- metas2 = [meta["meta"] for meta in metas]
1542
-
1585
+ video_metadatas = [hash_meta_dict[hash_value] for hash_value in hashes]
1586
+ video_metadatas2 = [meta["meta"] for meta in video_metadatas]
1543
1587
  names = self.get_free_names(dataset_id, names)
1544
1588
 
1545
- for name, hash, meta in zip(names, hashes, metas2):
1589
+ if metas is None:
1590
+ metas = [None] * len(names)
1591
+ if not isinstance(metas, list):
1592
+ raise ValueError("metas must be a list")
1593
+
1594
+ for name, hash, video_metadata, metadata in zip_longest(
1595
+ names, hashes, video_metadatas2, metas
1596
+ ):
1546
1597
  try:
1547
- all_streams = meta["streams"]
1598
+ all_streams = video_metadata["streams"]
1548
1599
  video_streams = get_video_streams(all_streams)
1549
1600
  for stream_info in video_streams:
1550
1601
  stream_index = stream_info["index"]
@@ -1559,7 +1610,7 @@ class VideoApi(RemoveableBulkModuleApi):
1559
1610
  # info = self._api.video.get_info_by_name(dataset_id, item_name)
1560
1611
  # if info is not None:
1561
1612
  # item_name = gen_video_stream_name(name, stream_index)
1562
- res = self.upload_hash(dataset_id, name, hash, stream_index)
1613
+ res = self.upload_hash(dataset_id, name, hash, stream_index, metadata)
1563
1614
  video_info_results.append(res)
1564
1615
  except Exception as e:
1565
1616
  from supervisely.io.exception_handlers import (
@@ -2531,7 +2582,7 @@ class VideoApi(RemoveableBulkModuleApi):
2531
2582
  progress_cb(len(chunk))
2532
2583
  if check_hash:
2533
2584
  if hash_to_check is not None:
2534
- downloaded_file_hash = await get_file_hash_async(path)
2585
+ downloaded_file_hash = await get_file_hash_chunked_async(path)
2535
2586
  if hash_to_check != downloaded_file_hash:
2536
2587
  raise RuntimeError(
2537
2588
  f"Downloaded hash of video with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"