supervisely 6.73.452__py3-none-any.whl → 6.73.513__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supervisely/__init__.py +25 -1
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +13 -12
- supervisely/api/annotation_api.py +6 -3
- supervisely/api/api.py +2 -0
- supervisely/api/app_api.py +10 -1
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +28 -0
- 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 +4 -0
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +5 -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/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/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/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 +22 -2
- 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/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 +795 -199
- supervisely/nn/inference/inference_request.py +42 -9
- 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 +113 -34
- supervisely/nn/inference/tracking/tracker_interface.py +7 -2
- 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/prediction_dto.py +12 -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/visualize.py +87 -90
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/train_app.py +28 -29
- 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 +40 -11
- 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.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
supervisely/__init__.py
CHANGED
|
@@ -8,6 +8,22 @@ try:
|
|
|
8
8
|
except TypeError as e:
|
|
9
9
|
__version__ = "development"
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
class _ApiProtoNotAvailable:
|
|
13
|
+
"""Placeholder class that raises an error when accessing any attribute"""
|
|
14
|
+
|
|
15
|
+
def __getattr__(self, name):
|
|
16
|
+
from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
|
|
17
|
+
|
|
18
|
+
raise ImportError(f"Cannot access `api_proto.{name}` : " + PROTOBUF_REQUIRED_ERROR)
|
|
19
|
+
|
|
20
|
+
def __bool__(self):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def __repr__(self):
|
|
24
|
+
return "<api_proto: not available - install supervisely[agent] to enable>"
|
|
25
|
+
|
|
26
|
+
|
|
11
27
|
from supervisely.sly_logger import (
|
|
12
28
|
logger,
|
|
13
29
|
ServiceType,
|
|
@@ -90,6 +106,7 @@ from supervisely.geometry.graph import GraphNodes, Node
|
|
|
90
106
|
from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
|
|
91
107
|
from supervisely.geometry.alpha_mask import AlphaMask
|
|
92
108
|
from supervisely.geometry.cuboid_2d import Cuboid2d
|
|
109
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
93
110
|
|
|
94
111
|
from supervisely.geometry.helpers import geometry_to_bitmap
|
|
95
112
|
from supervisely.geometry.helpers import deserialize_geometry
|
|
@@ -112,7 +129,14 @@ from supervisely.worker_api.chunking import (
|
|
|
112
129
|
ChunkedFileWriter,
|
|
113
130
|
ChunkedFileReader,
|
|
114
131
|
)
|
|
115
|
-
|
|
132
|
+
|
|
133
|
+
# Global import of api_proto works only if protobuf is installed and compatible
|
|
134
|
+
# Otherwise, we use a placeholder that raises an error when accessed
|
|
135
|
+
try:
|
|
136
|
+
import supervisely.worker_proto.worker_api_pb2 as api_proto
|
|
137
|
+
except Exception:
|
|
138
|
+
api_proto = _ApiProtoNotAvailable()
|
|
139
|
+
|
|
116
140
|
|
|
117
141
|
from supervisely.api.api import Api, UserSession, ApiContext
|
|
118
142
|
from supervisely.api import api
|
|
@@ -26,6 +26,7 @@ from supervisely.geometry.bitmap import Bitmap
|
|
|
26
26
|
from supervisely.geometry.geometry import Geometry
|
|
27
27
|
from supervisely.geometry.image_rotator import ImageRotator
|
|
28
28
|
from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
|
|
29
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
29
30
|
from supervisely.geometry.polygon import Polygon
|
|
30
31
|
from supervisely.geometry.rectangle import Rectangle
|
|
31
32
|
from supervisely.imaging import font as sly_font
|
|
@@ -551,7 +552,7 @@ class Annotation:
|
|
|
551
552
|
:return: list of the Label class objects
|
|
552
553
|
"""
|
|
553
554
|
for label in labels:
|
|
554
|
-
if self.img_size.count(None) == 0:
|
|
555
|
+
if self.img_size.count(None) == 0 and not isinstance(label.geometry, OrientedBBox):
|
|
555
556
|
# image has resolution in DB
|
|
556
557
|
canvas_rect = Rectangle.from_size(self.img_size)
|
|
557
558
|
try:
|
|
@@ -566,6 +567,7 @@ class Annotation:
|
|
|
566
567
|
else:
|
|
567
568
|
# image was uploaded by link and does not have resolution in DB
|
|
568
569
|
# add label without normalization and validation
|
|
570
|
+
# OrientedBBox geometries can be outside of image bounds
|
|
569
571
|
dest.append(label)
|
|
570
572
|
|
|
571
573
|
def add_label(self, label: Label) -> Annotation:
|
|
@@ -1417,7 +1419,7 @@ class Annotation:
|
|
|
1417
1419
|
if draw_tags is True:
|
|
1418
1420
|
tags_font = self._get_font()
|
|
1419
1421
|
for label in self._labels:
|
|
1420
|
-
if not fill_rectangles and isinstance(label.geometry, Rectangle):
|
|
1422
|
+
if not fill_rectangles and isinstance(label.geometry, (Rectangle, OrientedBBox)):
|
|
1421
1423
|
label.draw_contour(
|
|
1422
1424
|
bitmap,
|
|
1423
1425
|
color=color,
|
|
@@ -2962,6 +2964,8 @@ class Annotation:
|
|
|
2962
2964
|
for label in data[AnnotationJsonFields.LABELS]:
|
|
2963
2965
|
if label[LabelJsonFields.GEOMETRY_TYPE] == Rectangle.geometry_name():
|
|
2964
2966
|
label = Rectangle._to_pixel_coordinate_system_json(label, image_size)
|
|
2967
|
+
elif label[LabelJsonFields.GEOMETRY_TYPE] == OrientedBBox.geometry_name():
|
|
2968
|
+
label = OrientedBBox._to_pixel_coordinate_system_json(label, image_size)
|
|
2965
2969
|
else:
|
|
2966
2970
|
label = Geometry._to_pixel_coordinate_system_json(label, image_size)
|
|
2967
2971
|
new_labels.append(label)
|
|
@@ -2988,6 +2992,8 @@ class Annotation:
|
|
|
2988
2992
|
for label in data[AnnotationJsonFields.LABELS]:
|
|
2989
2993
|
if label[LabelJsonFields.GEOMETRY_TYPE] == Rectangle.geometry_name():
|
|
2990
2994
|
label = Rectangle._to_subpixel_coordinate_system_json(label)
|
|
2995
|
+
elif label[LabelJsonFields.GEOMETRY_TYPE] == OrientedBBox.geometry_name():
|
|
2996
|
+
label = OrientedBBox._to_subpixel_coordinate_system_json(label)
|
|
2991
2997
|
else:
|
|
2992
2998
|
label = Geometry._to_subpixel_coordinate_system_json(label)
|
|
2993
2999
|
new_labels.append(label)
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
+
from supervisely.geometry.alpha_mask import AlphaMask
|
|
3
|
+
from supervisely.geometry.any_geometry import AnyGeometry
|
|
2
4
|
from supervisely.geometry.bitmap import Bitmap
|
|
3
|
-
from supervisely.geometry.
|
|
5
|
+
from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
|
|
4
6
|
from supervisely.geometry.cuboid import Cuboid
|
|
7
|
+
from supervisely.geometry.cuboid_2d import Cuboid2d
|
|
8
|
+
from supervisely.geometry.cuboid_3d import Cuboid3d
|
|
9
|
+
from supervisely.geometry.graph import GraphNodes
|
|
10
|
+
from supervisely.geometry.mask_3d import Mask3D
|
|
11
|
+
from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
|
|
12
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
5
13
|
from supervisely.geometry.point import Point
|
|
14
|
+
from supervisely.geometry.point_3d import Point3d
|
|
15
|
+
from supervisely.geometry.pointcloud import Pointcloud
|
|
6
16
|
from supervisely.geometry.polygon import Polygon
|
|
7
17
|
from supervisely.geometry.polyline import Polyline
|
|
8
|
-
from supervisely.geometry.rectangle import Rectangle
|
|
9
|
-
from supervisely.geometry.graph import GraphNodes
|
|
10
|
-
from supervisely.geometry.any_geometry import AnyGeometry
|
|
11
|
-
from supervisely.geometry.cuboid_3d import Cuboid3d
|
|
12
|
-
from supervisely.geometry.pointcloud import Pointcloud
|
|
13
|
-
from supervisely.geometry.point_3d import Point3d
|
|
14
|
-
from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
|
|
15
|
-
from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
|
|
16
|
-
from supervisely.geometry.alpha_mask import AlphaMask
|
|
17
|
-
from supervisely.geometry.cuboid_2d import Cuboid2d
|
|
18
18
|
from supervisely.geometry.polyline_3d import Polyline3D
|
|
19
|
-
|
|
19
|
+
from supervisely.geometry.rectangle import Rectangle
|
|
20
20
|
|
|
21
21
|
_INPUT_GEOMETRIES = [
|
|
22
22
|
Bitmap,
|
|
@@ -36,6 +36,7 @@ _INPUT_GEOMETRIES = [
|
|
|
36
36
|
AlphaMask,
|
|
37
37
|
Cuboid2d,
|
|
38
38
|
Polyline3D,
|
|
39
|
+
OrientedBBox,
|
|
39
40
|
]
|
|
40
41
|
_JSON_SHAPE_TO_GEOMETRY_TYPE = {
|
|
41
42
|
geometry.geometry_name(): geometry for geometry in _INPUT_GEOMETRIES
|
|
@@ -869,9 +869,12 @@ class AnnotationApi(ModuleApi):
|
|
|
869
869
|
)
|
|
870
870
|
|
|
871
871
|
# use context to avoid redundant API calls
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
872
|
+
image_info = self._api.image.get_info_by_id(img_ids[0], force_metadata_for_links=False)
|
|
873
|
+
if image_info is None:
|
|
874
|
+
raise RuntimeError(
|
|
875
|
+
f"Cannot get dataset ID from image info. Image with ID={img_ids[0]} not found."
|
|
876
|
+
)
|
|
877
|
+
dataset_id = image_info.dataset_id
|
|
875
878
|
context = self._api.optimization_context
|
|
876
879
|
context_dataset_id = context.get("dataset_id")
|
|
877
880
|
project_id = context.get("project_id")
|
supervisely/api/api.py
CHANGED
|
@@ -42,6 +42,7 @@ import supervisely.api.dataset_api as dataset_api
|
|
|
42
42
|
import supervisely.api.entities_collection_api as entities_collection_api
|
|
43
43
|
import supervisely.api.file_api as file_api
|
|
44
44
|
import supervisely.api.github_api as github_api
|
|
45
|
+
import supervisely.api.guides_api as guides_api
|
|
45
46
|
import supervisely.api.image_annotation_tool_api as image_annotation_tool_api
|
|
46
47
|
import supervisely.api.image_api as image_api
|
|
47
48
|
import supervisely.api.import_storage_api as import_stoarge_api
|
|
@@ -358,6 +359,7 @@ class Api:
|
|
|
358
359
|
self.user = user_api.UserApi(self)
|
|
359
360
|
self.labeling_job = labeling_job_api.LabelingJobApi(self)
|
|
360
361
|
self.labeling_queue = labeling_queue_api.LabelingQueueApi(self)
|
|
362
|
+
self.guides = guides_api.GuidesApi(self)
|
|
361
363
|
self.video = video_api.VideoApi(self)
|
|
362
364
|
# self.project_class = project_class_api.ProjectClassApi(self)
|
|
363
365
|
self.object_class = object_class_api.ObjectClassApi(self)
|
supervisely/api/app_api.py
CHANGED
|
@@ -1750,6 +1750,7 @@ class AppApi(TaskApi):
|
|
|
1750
1750
|
module_id: Optional[int] = None,
|
|
1751
1751
|
redirect_requests: Dict[str, int] = {},
|
|
1752
1752
|
kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]] = None,
|
|
1753
|
+
multi_user_session: bool = False,
|
|
1753
1754
|
) -> SessionInfo:
|
|
1754
1755
|
"""Start a new application session (task).
|
|
1755
1756
|
|
|
@@ -1783,13 +1784,20 @@ class AppApi(TaskApi):
|
|
|
1783
1784
|
:type redirect_requests: dict
|
|
1784
1785
|
:param kubernetes_settings: Kubernetes settings for the task. If not specified, default settings will be used.
|
|
1785
1786
|
:type kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]]
|
|
1787
|
+
:param multi_user_session: If True, the application session will be created as multi-user.
|
|
1788
|
+
In this case, multiple users will be able to connect to the same application session.
|
|
1789
|
+
All users will have separate application states.
|
|
1790
|
+
Available only for applications that support multi-user sessions.
|
|
1791
|
+
:type multi_user_session: bool, default is False
|
|
1786
1792
|
:return: SessionInfo object with information about the started task.
|
|
1787
1793
|
:rtype: SessionInfo
|
|
1788
1794
|
:raises ValueError: If both app_id and module_id are not provided.
|
|
1789
1795
|
:raises ValueError: If both app_id and module_id are provided.
|
|
1790
1796
|
"""
|
|
1791
1797
|
users_ids = None
|
|
1792
|
-
if users_id
|
|
1798
|
+
if isinstance(users_id, list) and all(isinstance(u, int) for u in users_id):
|
|
1799
|
+
users_ids = users_id
|
|
1800
|
+
elif isinstance(users_id, int):
|
|
1793
1801
|
users_ids = [users_id]
|
|
1794
1802
|
|
|
1795
1803
|
new_params = {}
|
|
@@ -1818,6 +1826,7 @@ class AppApi(TaskApi):
|
|
|
1818
1826
|
module_id=module_id,
|
|
1819
1827
|
redirect_requests=redirect_requests,
|
|
1820
1828
|
kubernetes_settings=kubernetes_settings,
|
|
1829
|
+
multi_user_session=multi_user_session,
|
|
1821
1830
|
)
|
|
1822
1831
|
if type(result) is not list:
|
|
1823
1832
|
result = [result]
|
supervisely/api/dataset_api.py
CHANGED
|
@@ -1021,13 +1021,66 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
1021
1021
|
|
|
1022
1022
|
return dataset_tree
|
|
1023
1023
|
|
|
1024
|
-
def
|
|
1024
|
+
def _yield_tree(
|
|
1025
|
+
self, tree: Dict[DatasetInfo, Dict], path: List[str]
|
|
1026
|
+
) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
|
|
1027
|
+
"""
|
|
1028
|
+
Helper method for recursive tree traversal.
|
|
1029
|
+
Yields tuples of (path, dataset) for all datasets in the tree. For each node (dataset) at the current level,
|
|
1030
|
+
yields its (path, dataset) before recursively traversing and yielding from its children.
|
|
1031
|
+
|
|
1032
|
+
:param tree: Tree structure to yield from.
|
|
1033
|
+
:type tree: Dict[DatasetInfo, Dict]
|
|
1034
|
+
:param path: Current path (used for recursion).
|
|
1035
|
+
:type path: List[str]
|
|
1036
|
+
:return: Generator of tuples of (path, dataset).
|
|
1037
|
+
:rtype: Generator[Tuple[List[str], DatasetInfo], None, None]
|
|
1038
|
+
"""
|
|
1039
|
+
for dataset, children in tree.items():
|
|
1040
|
+
yield path, dataset
|
|
1041
|
+
new_path = path + [dataset.name]
|
|
1042
|
+
if children:
|
|
1043
|
+
yield from self._yield_tree(children, new_path)
|
|
1044
|
+
|
|
1045
|
+
def _find_dataset_in_tree(
|
|
1046
|
+
self, tree: Dict[DatasetInfo, Dict], target_id: int, path: List[str] = None
|
|
1047
|
+
) -> Tuple[Optional[DatasetInfo], Optional[Dict], List[str]]:
|
|
1048
|
+
"""Find a specific dataset in the tree and return its subtree and path.
|
|
1049
|
+
|
|
1050
|
+
:param tree: Tree structure to search in.
|
|
1051
|
+
:type tree: Dict[DatasetInfo, Dict]
|
|
1052
|
+
:param target_id: ID of the dataset to find.
|
|
1053
|
+
:type target_id: int
|
|
1054
|
+
:param path: Current path (used for recursion).
|
|
1055
|
+
:type path: List[str], optional
|
|
1056
|
+
:return: Tuple of (found_dataset, its_subtree, path_to_dataset).
|
|
1057
|
+
:rtype: Tuple[Optional[DatasetInfo], Optional[Dict], List[str]]
|
|
1058
|
+
"""
|
|
1059
|
+
if path is None:
|
|
1060
|
+
path = []
|
|
1061
|
+
|
|
1062
|
+
for dataset, children in tree.items():
|
|
1063
|
+
if dataset.id == target_id:
|
|
1064
|
+
return dataset, children, path
|
|
1065
|
+
# Search in children
|
|
1066
|
+
if children:
|
|
1067
|
+
found_dataset, found_children, found_path = self._find_dataset_in_tree(
|
|
1068
|
+
children, target_id, path + [dataset.name]
|
|
1069
|
+
)
|
|
1070
|
+
if found_dataset is not None:
|
|
1071
|
+
return found_dataset, found_children, found_path
|
|
1072
|
+
return None, None, []
|
|
1073
|
+
|
|
1074
|
+
def tree(self, project_id: int, dataset_id: Optional[int] = None) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
|
|
1025
1075
|
"""Yields tuples of (path, dataset) for all datasets in the project.
|
|
1026
1076
|
Path of the dataset is a list of parents, e.g. ["ds1", "ds2", "ds3"].
|
|
1027
1077
|
For root datasets, the path is an empty list.
|
|
1028
1078
|
|
|
1029
1079
|
:param project_id: Project ID in which the Dataset is located.
|
|
1030
1080
|
:type project_id: int
|
|
1081
|
+
:param dataset_id: Optional Dataset ID to start the tree from. If provided, only yields
|
|
1082
|
+
the subtree starting from this dataset (including the dataset itself and all its children).
|
|
1083
|
+
:type dataset_id: Optional[int]
|
|
1031
1084
|
:return: Generator of tuples of (path, dataset).
|
|
1032
1085
|
:rtype: Generator[Tuple[List[str], DatasetInfo], None, None]
|
|
1033
1086
|
:Usage example:
|
|
@@ -1040,11 +1093,17 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
1040
1093
|
|
|
1041
1094
|
project_id = 123
|
|
1042
1095
|
|
|
1096
|
+
# Get all datasets in the project
|
|
1043
1097
|
for parents, dataset in api.dataset.tree(project_id):
|
|
1044
1098
|
parents: List[str]
|
|
1045
1099
|
dataset: sly.DatasetInfo
|
|
1046
1100
|
print(parents, dataset.name)
|
|
1047
1101
|
|
|
1102
|
+
# Get only a specific branch starting from dataset_id = 456
|
|
1103
|
+
for parents, dataset in api.dataset.tree(project_id, dataset_id=456):
|
|
1104
|
+
parents: List[str]
|
|
1105
|
+
dataset: sly.DatasetInfo
|
|
1106
|
+
print(parents, dataset.name)
|
|
1048
1107
|
|
|
1049
1108
|
# Output:
|
|
1050
1109
|
# [] ds1
|
|
@@ -1052,17 +1111,20 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
1052
1111
|
# ["ds1", "ds2"] ds3
|
|
1053
1112
|
"""
|
|
1054
1113
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1114
|
+
full_tree = self.get_tree(project_id)
|
|
1115
|
+
|
|
1116
|
+
if dataset_id is None:
|
|
1117
|
+
# Return the full tree
|
|
1118
|
+
yield from self._yield_tree(full_tree, [])
|
|
1119
|
+
else:
|
|
1120
|
+
# Find the specific dataset and return only its subtree
|
|
1121
|
+
target_dataset, subtree, dataset_path = self._find_dataset_in_tree(full_tree, dataset_id)
|
|
1122
|
+
if target_dataset is not None:
|
|
1123
|
+
# Yield the target dataset first, then its children
|
|
1124
|
+
yield dataset_path, target_dataset
|
|
1125
|
+
if subtree:
|
|
1126
|
+
new_path = dataset_path + [target_dataset.name]
|
|
1127
|
+
yield from self._yield_tree(subtree, new_path)
|
|
1066
1128
|
|
|
1067
1129
|
def get_nested(self, project_id: int, dataset_id: int) -> List[DatasetInfo]:
|
|
1068
1130
|
"""Returns a list of all nested datasets in the specified dataset.
|
|
@@ -281,6 +281,7 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
|
|
|
281
281
|
description: Optional[str] = None,
|
|
282
282
|
type: str = CollectionType.DEFAULT,
|
|
283
283
|
ai_search_key: Optional[str] = None,
|
|
284
|
+
change_name_if_conflict=False,
|
|
284
285
|
) -> EntitiesCollectionInfo:
|
|
285
286
|
"""
|
|
286
287
|
Creates Entities Collections.
|
|
@@ -295,6 +296,8 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
|
|
|
295
296
|
:type type: str
|
|
296
297
|
:param ai_search_key: AI search key for the collection. Defaults to None.
|
|
297
298
|
:type ai_search_key: Optional[str]
|
|
299
|
+
:param change_name_if_conflict: Checks if given name already exists and adds suffix to the end of the name. Defaults to False.
|
|
300
|
+
:type change_name_if_conflict: bool
|
|
298
301
|
:return: Information about new Entities Collection
|
|
299
302
|
:rtype: :class:`EntitiesCollectionInfo`
|
|
300
303
|
:Usage example:
|
|
@@ -316,6 +319,13 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
|
|
|
316
319
|
new_collection = api.entities_collection.create(project_id, name, description, type, ai_search_key)
|
|
317
320
|
print(new_collection)
|
|
318
321
|
"""
|
|
322
|
+
|
|
323
|
+
name = self._get_effective_new_name(
|
|
324
|
+
parent_id=project_id,
|
|
325
|
+
name=name,
|
|
326
|
+
change_name_if_conflict=change_name_if_conflict,
|
|
327
|
+
)
|
|
328
|
+
|
|
319
329
|
method = "entities-collections.add"
|
|
320
330
|
data = {
|
|
321
331
|
ApiField.PROJECT_ID: project_id,
|
|
@@ -1046,3 +1046,31 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
1046
1046
|
image_ids=image_ids,
|
|
1047
1047
|
skip_geometry=skip_geometry,
|
|
1048
1048
|
)
|
|
1049
|
+
|
|
1050
|
+
def restore_batch(self, ids: List[int], progress_cb: Optional[Callable] = None, batch_size: int = 50):
|
|
1051
|
+
"""
|
|
1052
|
+
Restore archived figures in batches from the Supervisely server.
|
|
1053
|
+
|
|
1054
|
+
:param ids: IDs of figures in Supervisely.
|
|
1055
|
+
:type ids: List[int]
|
|
1056
|
+
:param progress_cb: Optional callback to track restore progress. Receives number of restored figures in the current batch.
|
|
1057
|
+
:type progress_cb: Optional[Callable]
|
|
1058
|
+
:param batch_size: Number of figure IDs to send in a single request.
|
|
1059
|
+
:type batch_size: int
|
|
1060
|
+
"""
|
|
1061
|
+
for ids_batch in batched(ids, batch_size=batch_size):
|
|
1062
|
+
self._api.post(
|
|
1063
|
+
"figures.bulk.restore",
|
|
1064
|
+
{ApiField.FIGURE_IDS: ids_batch},
|
|
1065
|
+
)
|
|
1066
|
+
if progress_cb is not None:
|
|
1067
|
+
progress_cb(len(ids_batch))
|
|
1068
|
+
|
|
1069
|
+
def restore(self, id: int):
|
|
1070
|
+
"""
|
|
1071
|
+
Restore a single archived figure with the specified ID from the Supervisely server.
|
|
1072
|
+
|
|
1073
|
+
:param id: Figure ID in Supervisely.
|
|
1074
|
+
:type id: int
|
|
1075
|
+
"""
|
|
1076
|
+
self.restore_batch([id])
|
|
@@ -214,6 +214,7 @@ class ObjectApi(RemoveableBulkModuleApi):
|
|
|
214
214
|
objects,
|
|
215
215
|
key_id_map: KeyIdMap = None,
|
|
216
216
|
is_pointcloud=False,
|
|
217
|
+
is_video_multi_view: bool = False,
|
|
217
218
|
):
|
|
218
219
|
""""""
|
|
219
220
|
if len(objects) == 0:
|
|
@@ -225,8 +226,7 @@ class ObjectApi(RemoveableBulkModuleApi):
|
|
|
225
226
|
for obj in objects:
|
|
226
227
|
new_obj = {ApiField.CLASS_ID: objcls_name_id_map[obj.obj_class.name]}
|
|
227
228
|
|
|
228
|
-
if not is_pointcloud:
|
|
229
|
-
# if entity_id is not None:
|
|
229
|
+
if not is_video_multi_view and not is_pointcloud:
|
|
230
230
|
new_obj[ApiField.ENTITY_ID] = entity_id
|
|
231
231
|
items.append(new_obj)
|
|
232
232
|
|
|
@@ -238,7 +238,7 @@ class ObjectApi(RemoveableBulkModuleApi):
|
|
|
238
238
|
KeyIdMap.add_objects_to(key_id_map, [obj.key() for obj in objects], ids)
|
|
239
239
|
|
|
240
240
|
# add tags to objects
|
|
241
|
-
tag_api.append_to_objects(entity_id, project_id, objects, key_id_map)
|
|
241
|
+
tag_api.append_to_objects(entity_id, project_id, objects, key_id_map, is_video_multi_view)
|
|
242
242
|
|
|
243
243
|
return ids
|
|
244
244
|
|
|
@@ -5,6 +5,8 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
5
5
|
from supervisely._utils import batched
|
|
6
6
|
from supervisely.api.module_api import ApiField, ModuleApi
|
|
7
7
|
from supervisely.collection.key_indexed_collection import KeyIndexedCollection
|
|
8
|
+
from supervisely.project.project_meta import ProjectMeta
|
|
9
|
+
from supervisely.project.project_settings import LabelingInterface
|
|
8
10
|
from supervisely.task.progress import tqdm_sly
|
|
9
11
|
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
10
12
|
|
|
@@ -157,7 +159,12 @@ class TagApi(ModuleApi):
|
|
|
157
159
|
return ids
|
|
158
160
|
|
|
159
161
|
def append_to_objects(
|
|
160
|
-
self,
|
|
162
|
+
self,
|
|
163
|
+
entity_id: int,
|
|
164
|
+
project_id: int,
|
|
165
|
+
objects: KeyIndexedCollection,
|
|
166
|
+
key_id_map: KeyIdMap,
|
|
167
|
+
is_video_multi_view: bool = False,
|
|
161
168
|
):
|
|
162
169
|
"""
|
|
163
170
|
Add Tags to Annotation Objects for a specific entity (image etc.).
|
|
@@ -170,6 +177,8 @@ class TagApi(ModuleApi):
|
|
|
170
177
|
:type objects: KeyIndexedCollection
|
|
171
178
|
:param key_id_map: KeyIdMap object.
|
|
172
179
|
:type key_id_map: KeyIdMap
|
|
180
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
181
|
+
:type is_video_multi_view: bool
|
|
173
182
|
:return: List of tags IDs
|
|
174
183
|
:rtype: list
|
|
175
184
|
:Usage example:
|
|
@@ -210,12 +219,16 @@ class TagApi(ModuleApi):
|
|
|
210
219
|
raise RuntimeError("SDK error: len(tags_keys) != len(tags_to_add)")
|
|
211
220
|
if len(tags_keys) == 0:
|
|
212
221
|
return
|
|
213
|
-
ids = self.append_to_objects_json(entity_id, tags_to_add, project_id)
|
|
222
|
+
ids = self.append_to_objects_json(entity_id, tags_to_add, project_id, is_video_multi_view)
|
|
214
223
|
KeyIdMap.add_tags_to(key_id_map, tags_keys, ids)
|
|
215
224
|
return ids
|
|
216
225
|
|
|
217
226
|
def append_to_objects_json(
|
|
218
|
-
self,
|
|
227
|
+
self,
|
|
228
|
+
entity_id: int,
|
|
229
|
+
tags_json: List[Dict],
|
|
230
|
+
project_id: Optional[int] = None,
|
|
231
|
+
is_video_multi_view: bool = False,
|
|
219
232
|
) -> List[int]:
|
|
220
233
|
"""
|
|
221
234
|
Add Tags to Annotation Objects for specific entity (image etc.).
|
|
@@ -224,6 +237,11 @@ class TagApi(ModuleApi):
|
|
|
224
237
|
:type entity_id: int
|
|
225
238
|
:param tags_json: Collection of tags in JSON format
|
|
226
239
|
:type tags_json: dict
|
|
240
|
+
:param project_id: Project ID in Supervisely. Uses to get tag name to tag ID mapping.
|
|
241
|
+
Not required if `multi_view` is True.
|
|
242
|
+
:type project_id: int, optional
|
|
243
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
244
|
+
:type is_video_multi_view: bool
|
|
227
245
|
:return: List of tags IDs
|
|
228
246
|
:rtype: list
|
|
229
247
|
|
|
@@ -262,10 +280,15 @@ class TagApi(ModuleApi):
|
|
|
262
280
|
# 80421103
|
|
263
281
|
# ]
|
|
264
282
|
"""
|
|
283
|
+
project_meta = self._api.optimization_context.get("project_meta")
|
|
284
|
+
|
|
285
|
+
if isinstance(project_meta, ProjectMeta):
|
|
286
|
+
if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
|
|
287
|
+
is_video_multi_view = True
|
|
265
288
|
|
|
266
289
|
if len(tags_json) == 0:
|
|
267
290
|
return []
|
|
268
|
-
if project_id is not None:
|
|
291
|
+
if project_id is not None and not is_video_multi_view:
|
|
269
292
|
json_data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: tags_json}
|
|
270
293
|
else:
|
|
271
294
|
json_data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json}
|
|
@@ -280,6 +303,8 @@ class TagApi(ModuleApi):
|
|
|
280
303
|
batch_size: int = 100,
|
|
281
304
|
log_progress: bool = False,
|
|
282
305
|
progress: Optional[tqdm_sly] = None,
|
|
306
|
+
is_video_multi_view: bool = False,
|
|
307
|
+
entity_id: Optional[int] = None,
|
|
283
308
|
) -> List[Dict[str, Union[str, int, None]]]:
|
|
284
309
|
"""
|
|
285
310
|
For images project:
|
|
@@ -306,6 +331,11 @@ class TagApi(ModuleApi):
|
|
|
306
331
|
:type log_progress: bool
|
|
307
332
|
:param progress: Progress bar object to display progress.
|
|
308
333
|
:type progress: Optional[tqdm_sly]
|
|
334
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
335
|
+
:type is_video_multi_view: bool
|
|
336
|
+
:param entity_id: ID of the entity in Supervisely to add a tag to its objects.
|
|
337
|
+
Required if `is_video_multi_view` is True.
|
|
338
|
+
:type entity_id: Optional[int]
|
|
309
339
|
:return: List of tags infos as dictionaries.
|
|
310
340
|
:rtype: List[Dict[str, Union[str, int, None]]]
|
|
311
341
|
|
|
@@ -363,6 +393,12 @@ class TagApi(ModuleApi):
|
|
|
363
393
|
if progress is not None:
|
|
364
394
|
log_progress = False
|
|
365
395
|
|
|
396
|
+
project_meta = self._api.optimization_context.get("project_meta")
|
|
397
|
+
|
|
398
|
+
if isinstance(project_meta, ProjectMeta):
|
|
399
|
+
if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
|
|
400
|
+
is_video_multi_view = True
|
|
401
|
+
|
|
366
402
|
result = []
|
|
367
403
|
|
|
368
404
|
if len(tags_list) == 0:
|
|
@@ -373,7 +409,12 @@ class TagApi(ModuleApi):
|
|
|
373
409
|
total=len(tags_list),
|
|
374
410
|
)
|
|
375
411
|
for batch in batched(tags_list, batch_size):
|
|
376
|
-
|
|
412
|
+
if is_video_multi_view:
|
|
413
|
+
if entity_id is None:
|
|
414
|
+
raise ValueError("entity_id must be provided when is_video_multi_view is True")
|
|
415
|
+
data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: batch}
|
|
416
|
+
else:
|
|
417
|
+
data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
|
|
377
418
|
if type(self) is TagApi:
|
|
378
419
|
response = self._api.post("figures.tags.bulk.add", data)
|
|
379
420
|
else:
|
|
@@ -463,6 +504,8 @@ class TagApi(ModuleApi):
|
|
|
463
504
|
tags_map: Dict[int, Any],
|
|
464
505
|
batch_size: int = 100,
|
|
465
506
|
log_progress: bool = False,
|
|
507
|
+
is_video_multi_view: bool = False,
|
|
508
|
+
entity_id: Optional[int] = None,
|
|
466
509
|
) -> List[Dict[str, Union[str, int, None]]]:
|
|
467
510
|
"""
|
|
468
511
|
For images project:
|
|
@@ -483,8 +526,13 @@ class TagApi(ModuleApi):
|
|
|
483
526
|
:type batch_size: int
|
|
484
527
|
:param log_progress: If True, will display a progress bar.
|
|
485
528
|
:type log_progress: bool
|
|
529
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
530
|
+
:type is_video_multi_view: bool
|
|
531
|
+
:param entity_id: ID of the entity in Supervisely to add a tag to its objects.
|
|
532
|
+
Required if `is_video_multi_view` is True.
|
|
533
|
+
:type entity_id: Optional[int]
|
|
486
534
|
:return: List of tags infos as dictionaries.
|
|
487
|
-
:rtype: List[
|
|
535
|
+
:rtype: List[Dict[str, Union[str, int, None]]]
|
|
488
536
|
|
|
489
537
|
Usage example:
|
|
490
538
|
.. code-block:: python
|
|
@@ -527,11 +575,14 @@ class TagApi(ModuleApi):
|
|
|
527
575
|
raise ValueError(f"Tag {tag.name} meta has no sly_id")
|
|
528
576
|
|
|
529
577
|
data.append(
|
|
530
|
-
{
|
|
531
|
-
ApiField.TAG_ID: tag.meta.sly_id,
|
|
532
|
-
OBJ_ID_FIELD: obj_id,
|
|
533
|
-
**tag.to_json()
|
|
534
|
-
}
|
|
578
|
+
{ApiField.TAG_ID: tag.meta.sly_id, OBJ_ID_FIELD: obj_id, **tag.to_json()}
|
|
535
579
|
)
|
|
536
580
|
|
|
537
|
-
return self.add_to_objects(
|
|
581
|
+
return self.add_to_objects(
|
|
582
|
+
project_id,
|
|
583
|
+
data,
|
|
584
|
+
batch_size,
|
|
585
|
+
log_progress,
|
|
586
|
+
is_video_multi_view=is_video_multi_view,
|
|
587
|
+
entity_id=entity_id,
|
|
588
|
+
)
|