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.
- supervisely/__init__.py +137 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +14 -11
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +14 -11
- supervisely/api/api.py +59 -38
- supervisely/api/app_api.py +11 -2
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +52 -4
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +72 -1
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +9 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +24 -4
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +916 -222
- supervisely/nn/inference/inference_request.py +55 -10
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +10 -1
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/model/prediction.py +2 -1
- supervisely/nn/model/prediction_session.py +26 -14
- supervisely/nn/prediction_dto.py +19 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/utils.py +4 -5
- supervisely/nn/tracker/visualize.py +93 -93
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +46 -31
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- supervisely_lib/__init__.py +6 -1
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
supervisely/api/project_api.py
CHANGED
|
@@ -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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
|
1997
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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
|
|
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
|
|
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(
|
|
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[
|
|
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:
|
|
2640
|
-
:rtype:
|
|
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
|
-
|
|
2654
|
-
print(f"Recreated project {src_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(
|
supervisely/api/storage_api.py
CHANGED
|
@@ -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"]
|
|
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
|
|
supervisely/api/task_api.py
CHANGED
|
@@ -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
|
|
809
|
-
:param data: dict
|
|
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})
|
supervisely/api/team_api.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
1120
|
+
videos = []
|
|
1110
1121
|
for name, item, meta in batch:
|
|
1111
1122
|
item_tuple = func_item_to_kv(item)
|
|
1112
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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}"
|