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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from supervisely.app import DataJson
|
|
6
|
+
from supervisely.app.widgets import Widget
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from typing import Literal
|
|
10
|
+
except ImportError:
|
|
11
|
+
from typing_extensions import Literal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ActivityFeed(Widget):
|
|
15
|
+
"""ActivityFeed is a widget that displays a vertical list of activity items with status indicators.
|
|
16
|
+
Similar to a timeline or activity log showing sequential events with their current status.
|
|
17
|
+
|
|
18
|
+
Each item can contain a custom widget as content and displays a status indicator (pending, in process, completed, failed).
|
|
19
|
+
Items are automatically numbered if no number is provided.
|
|
20
|
+
|
|
21
|
+
Read about it in `Developer Portal <https://developer.supervisely.com/app-development/widgets/layouts-and-containers/activity-feed>`_
|
|
22
|
+
(including screenshots and examples).
|
|
23
|
+
|
|
24
|
+
:param items: List of ActivityFeed.Item objects to display
|
|
25
|
+
:type items: Optional[List[ActivityFeed.Item]]
|
|
26
|
+
:param widget_id: An identifier of the widget.
|
|
27
|
+
:type widget_id: str, optional
|
|
28
|
+
|
|
29
|
+
:Usage example:
|
|
30
|
+
.. code-block:: python
|
|
31
|
+
|
|
32
|
+
from supervisely.app.widgets import ActivityFeed, Text
|
|
33
|
+
|
|
34
|
+
# Create items with custom content
|
|
35
|
+
item1 = ActivityFeed.Item(
|
|
36
|
+
content=Text("Processing dataset"),
|
|
37
|
+
status="completed"
|
|
38
|
+
)
|
|
39
|
+
item2 = ActivityFeed.Item(
|
|
40
|
+
content=Text("Training model"),
|
|
41
|
+
status="in_progress",
|
|
42
|
+
number=2
|
|
43
|
+
)
|
|
44
|
+
item3 = ActivityFeed.Item(
|
|
45
|
+
content=Text("Generating report"),
|
|
46
|
+
status="pending"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Create activity feed
|
|
50
|
+
feed = ActivityFeed(items=[item1, item2, item3])
|
|
51
|
+
|
|
52
|
+
# Add item during runtime
|
|
53
|
+
new_item = ActivityFeed.Item(
|
|
54
|
+
content=Text("Deploy model"),
|
|
55
|
+
status="pending"
|
|
56
|
+
)
|
|
57
|
+
feed.add_item(new_item)
|
|
58
|
+
|
|
59
|
+
# Update status by item number
|
|
60
|
+
feed.set_status(2, "completed")
|
|
61
|
+
|
|
62
|
+
# Get item status
|
|
63
|
+
status = feed.get_status(2)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
class Item:
|
|
67
|
+
"""Represents a single item in the ActivityFeed.
|
|
68
|
+
|
|
69
|
+
:param content: Widget to display as the item content
|
|
70
|
+
:type content: Widget
|
|
71
|
+
:param status: Status of the item (pending, in_progress, completed, failed)
|
|
72
|
+
:type status: Literal["pending", "in_progress", "completed", "failed"]
|
|
73
|
+
:param number: Position number in the feed (auto-assigned if not provided)
|
|
74
|
+
:type number: Optional[int]
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
content: Widget,
|
|
80
|
+
status: Literal["pending", "in_progress", "completed", "failed"] = "pending",
|
|
81
|
+
number: Optional[int] = None,
|
|
82
|
+
) -> ActivityFeed.Item:
|
|
83
|
+
self.content = content
|
|
84
|
+
self.status = status
|
|
85
|
+
self.number = number
|
|
86
|
+
self._validate_status()
|
|
87
|
+
|
|
88
|
+
def _validate_status(self):
|
|
89
|
+
valid_statuses = ["pending", "in_progress", "completed", "failed"]
|
|
90
|
+
if self.status not in valid_statuses:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Invalid status '{self.status}'. Must be one of: {', '.join(valid_statuses)}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def to_json(self):
|
|
96
|
+
return {
|
|
97
|
+
"number": self.number,
|
|
98
|
+
"status": self.status,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
items: Optional[List[ActivityFeed.Item]] = None,
|
|
104
|
+
widget_id: Optional[str] = None,
|
|
105
|
+
):
|
|
106
|
+
self._items = items if items is not None else []
|
|
107
|
+
self._auto_assign_numbers()
|
|
108
|
+
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
109
|
+
|
|
110
|
+
def _auto_assign_numbers(self):
|
|
111
|
+
"""Automatically assign numbers to items that don't have them."""
|
|
112
|
+
next_number = 1
|
|
113
|
+
for item in self._items:
|
|
114
|
+
if item.number is None:
|
|
115
|
+
item.number = next_number
|
|
116
|
+
next_number = max(next_number, item.number) + 1
|
|
117
|
+
|
|
118
|
+
def get_json_data(self) -> Dict:
|
|
119
|
+
"""Returns dictionary with widget data.
|
|
120
|
+
|
|
121
|
+
:return: Dictionary with items data
|
|
122
|
+
:rtype: Dict
|
|
123
|
+
"""
|
|
124
|
+
return {
|
|
125
|
+
"items": [item.to_json() for item in self._items],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def get_json_state(self) -> Dict:
|
|
129
|
+
"""Returns dictionary with widget state (empty for this widget).
|
|
130
|
+
|
|
131
|
+
:return: Empty dictionary
|
|
132
|
+
:rtype: Dict
|
|
133
|
+
"""
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
def add_item(
|
|
137
|
+
self,
|
|
138
|
+
item: Optional[ActivityFeed.Item] = None,
|
|
139
|
+
content: Optional[Widget] = None,
|
|
140
|
+
status: Literal["pending", "in_progress", "completed", "failed"] = "pending",
|
|
141
|
+
number: Optional[int] = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Add a new item to the activity feed.
|
|
144
|
+
|
|
145
|
+
You can either pass an ActivityFeed.Item object or provide content and status separately.
|
|
146
|
+
|
|
147
|
+
:param item: ActivityFeed.Item to add
|
|
148
|
+
:type item: Optional[ActivityFeed.Item]
|
|
149
|
+
:param content: Widget content (used if item is not provided)
|
|
150
|
+
:type content: Optional[Widget]
|
|
151
|
+
:param status: Status of the item (used if item is not provided)
|
|
152
|
+
:type status: Literal["pending", "in_progress", "completed", "failed"]
|
|
153
|
+
:param number: Position number (auto-assigned if not provided)
|
|
154
|
+
:type number: Optional[int]
|
|
155
|
+
"""
|
|
156
|
+
if item is None:
|
|
157
|
+
if content is None:
|
|
158
|
+
raise ValueError("Either 'item' or 'content' must be provided")
|
|
159
|
+
item = ActivityFeed.Item(content=content, status=status, number=number)
|
|
160
|
+
|
|
161
|
+
if item.number is None:
|
|
162
|
+
# Auto-assign number
|
|
163
|
+
if self._items:
|
|
164
|
+
item.number = max(i.number for i in self._items) + 1
|
|
165
|
+
else:
|
|
166
|
+
item.number = 1
|
|
167
|
+
|
|
168
|
+
self._items.append(item)
|
|
169
|
+
self.update_data()
|
|
170
|
+
DataJson().send_changes()
|
|
171
|
+
|
|
172
|
+
def remove_item(self, number: int) -> None:
|
|
173
|
+
"""Remove an item from the activity feed by its number.
|
|
174
|
+
|
|
175
|
+
:param number: Number of the item to remove. Starts from 1.
|
|
176
|
+
:type number: int
|
|
177
|
+
"""
|
|
178
|
+
self._items = [item for item in self._items if item.number != number]
|
|
179
|
+
self.update_data()
|
|
180
|
+
DataJson().send_changes()
|
|
181
|
+
|
|
182
|
+
def set_status(
|
|
183
|
+
self,
|
|
184
|
+
number: int,
|
|
185
|
+
status: Literal["pending", "in_progress", "completed", "failed"],
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Update the status of an item by its number.
|
|
188
|
+
|
|
189
|
+
:param number: Number of the item to update. Starts from 1.
|
|
190
|
+
:type number: int
|
|
191
|
+
:param status: New status for the item
|
|
192
|
+
:type status: Literal["pending", "in_progress", "completed", "failed"]
|
|
193
|
+
"""
|
|
194
|
+
for i, item in enumerate(self._items):
|
|
195
|
+
if item.number == number:
|
|
196
|
+
item.status = status
|
|
197
|
+
item._validate_status()
|
|
198
|
+
DataJson()[self.widget_id]["items"][i]["status"] = status
|
|
199
|
+
DataJson().send_changes()
|
|
200
|
+
return
|
|
201
|
+
raise ValueError(f"Item with number {number} not found")
|
|
202
|
+
|
|
203
|
+
def get_status(self, number: int) -> str:
|
|
204
|
+
"""Get the status of an item by its number.
|
|
205
|
+
|
|
206
|
+
:param number: Number of the item. Starts from 1.
|
|
207
|
+
:type number: int
|
|
208
|
+
:return: Status of the item
|
|
209
|
+
:rtype: str
|
|
210
|
+
"""
|
|
211
|
+
for item in self._items:
|
|
212
|
+
if item.number == number:
|
|
213
|
+
return item.status
|
|
214
|
+
raise ValueError(f"Item with number {number} not found")
|
|
215
|
+
|
|
216
|
+
def get_items(self) -> List[ActivityFeed.Item]:
|
|
217
|
+
"""Get all items in the activity feed.
|
|
218
|
+
|
|
219
|
+
:return: List of all items
|
|
220
|
+
:rtype: List[ActivityFeed.Item]
|
|
221
|
+
"""
|
|
222
|
+
return self._items
|
|
223
|
+
|
|
224
|
+
def clear(self) -> None:
|
|
225
|
+
"""Remove all items from the activity feed."""
|
|
226
|
+
self._items = []
|
|
227
|
+
self.update_data()
|
|
228
|
+
DataJson().send_changes()
|
|
229
|
+
|
|
230
|
+
def set_items(self, items: List[ActivityFeed.Item]) -> None:
|
|
231
|
+
"""Replace all items in the activity feed.
|
|
232
|
+
|
|
233
|
+
:param items: New list of items
|
|
234
|
+
:type items: List[ActivityFeed.Item]
|
|
235
|
+
"""
|
|
236
|
+
self._items = items
|
|
237
|
+
self._auto_assign_numbers()
|
|
238
|
+
self.update_data()
|
|
239
|
+
DataJson().send_changes()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.sly-activity-feed {
|
|
2
|
+
padding: 10px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.sly-activity-feed-item {
|
|
6
|
+
display: flex;
|
|
7
|
+
position: relative;
|
|
8
|
+
padding-bottom: 20px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.sly-activity-feed-item:last-child {
|
|
12
|
+
padding-bottom: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.sly-activity-feed-item:not(:last-child) .sly-activity-feed-line {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.sly-activity-feed-marker {
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
width: 40px;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
align-items: center;
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sly-activity-feed-circle {
|
|
29
|
+
width: 16px;
|
|
30
|
+
height: 16px;
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
font-size: 10px;
|
|
37
|
+
color: white;
|
|
38
|
+
z-index: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sly-activity-feed-circle.completed {
|
|
42
|
+
background-color: #67C23A;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.sly-activity-feed-circle.failed {
|
|
46
|
+
background-color: #F56C6C;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sly-activity-feed-circle.in_progress {
|
|
50
|
+
background-color: #409EFF;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sly-activity-feed-circle.pending {
|
|
54
|
+
background-color: #909399;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.sly-activity-feed-line {
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: 16px;
|
|
60
|
+
left: 50%;
|
|
61
|
+
transform: translateX(-50%);
|
|
62
|
+
width: 2px;
|
|
63
|
+
height: calc(100% + 4px);
|
|
64
|
+
background-color: #E4E7ED;
|
|
65
|
+
display: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sly-activity-feed-content {
|
|
69
|
+
flex: 1;
|
|
70
|
+
padding-left: 10px;
|
|
71
|
+
padding-top: 0px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.sly-activity-feed-number {
|
|
75
|
+
font-size: 11px;
|
|
76
|
+
color: #909399;
|
|
77
|
+
margin-top: 4px;
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<link rel="stylesheet" href="./sly/css/app/widgets/activity_feed/style.css" />
|
|
2
|
+
|
|
3
|
+
<div class="sly-activity-feed">
|
|
4
|
+
{% for item in widget._items %}
|
|
5
|
+
<div class="sly-activity-feed-item" v-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}]">
|
|
6
|
+
<div class="sly-activity-feed-marker">
|
|
7
|
+
<div class="sly-activity-feed-circle" :class="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status">
|
|
8
|
+
<i v-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'completed'" class="el-icon-check"></i>
|
|
9
|
+
<i v-else-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'failed'"
|
|
10
|
+
class="el-icon-close"></i>
|
|
11
|
+
<i v-else-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'in_progress'"
|
|
12
|
+
class="el-icon-loading"></i>
|
|
13
|
+
<i v-else class="el-icon-time"></i>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="sly-activity-feed-line"></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="sly-activity-feed-content">
|
|
18
|
+
{{{item.content}}}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
{% endfor %}
|
|
22
|
+
</div>
|
|
@@ -157,3 +157,23 @@ class Card(Widget):
|
|
|
157
157
|
:rtype: bool
|
|
158
158
|
"""
|
|
159
159
|
return self._disabled["disabled"]
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def description(self) -> Optional[str]:
|
|
163
|
+
"""Description of the card.
|
|
164
|
+
|
|
165
|
+
:return: Description of the card.
|
|
166
|
+
:rtype: Optional[str]
|
|
167
|
+
"""
|
|
168
|
+
return self._description
|
|
169
|
+
|
|
170
|
+
@description.setter
|
|
171
|
+
def description(self, value: str) -> None:
|
|
172
|
+
"""Sets the description of the card.
|
|
173
|
+
|
|
174
|
+
:param value: Description of the card.
|
|
175
|
+
:type value: str
|
|
176
|
+
"""
|
|
177
|
+
self._description = value
|
|
178
|
+
StateJson()[self.widget_id]["description"] = self._description
|
|
179
|
+
StateJson().send_changes()
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
from typing import List, Optional, Union
|
|
1
|
+
from typing import Callable, List, Optional, Union
|
|
2
2
|
|
|
3
3
|
from supervisely import ObjClass, ObjClassCollection
|
|
4
|
-
from supervisely.app import StateJson
|
|
5
|
-
from supervisely.app.widgets import
|
|
4
|
+
from supervisely.app import DataJson, StateJson
|
|
5
|
+
from supervisely.app.widgets import NotificationBox, Text, Widget
|
|
6
|
+
from supervisely.geometry.alpha_mask import AlphaMask
|
|
6
7
|
from supervisely.geometry.any_geometry import AnyGeometry
|
|
7
8
|
from supervisely.geometry.bitmap import Bitmap
|
|
8
|
-
from supervisely.geometry.alpha_mask import AlphaMask
|
|
9
9
|
from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
|
|
10
10
|
from supervisely.geometry.cuboid_2d import Cuboid2d
|
|
11
11
|
from supervisely.geometry.cuboid_3d import Cuboid3d
|
|
12
12
|
from supervisely.geometry.graph import GraphNodes
|
|
13
13
|
from supervisely.geometry.mask_3d import Mask3D
|
|
14
14
|
from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
|
|
15
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
15
16
|
from supervisely.geometry.point import Point
|
|
16
17
|
from supervisely.geometry.point_3d import Point3d
|
|
17
18
|
from supervisely.geometry.pointcloud import Pointcloud
|
|
18
19
|
from supervisely.geometry.polygon import Polygon
|
|
19
20
|
from supervisely.geometry.polyline import Polyline
|
|
20
21
|
from supervisely.geometry.rectangle import Rectangle
|
|
22
|
+
from supervisely.imaging.color import generate_rgb
|
|
21
23
|
|
|
22
24
|
type_to_shape_text = {
|
|
23
25
|
AnyGeometry: "any shape",
|
|
@@ -29,34 +31,57 @@ type_to_shape_text = {
|
|
|
29
31
|
Point: "point",
|
|
30
32
|
Cuboid2d: "cuboid 2d", #
|
|
31
33
|
Cuboid3d: "cuboid 3d",
|
|
32
|
-
Pointcloud: "pointcloud", #
|
|
34
|
+
Pointcloud: "pointcloud", # "zmdi zmdi-border-clear"
|
|
33
35
|
MultichannelBitmap: "n-channel mask", # "zmdi zmdi-collection-item"
|
|
34
36
|
Point3d: "point 3d", # "zmdi zmdi-select-all"
|
|
35
37
|
GraphNodes: "keypoints",
|
|
36
38
|
ClosedSurfaceMesh: "volume (3d mask)",
|
|
37
39
|
Mask3D: "3d mask",
|
|
40
|
+
OrientedBBox: "oriented bbox",
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
shape_text_to_type = {v: k for k, v in type_to_shape_text.items()}
|
|
44
|
+
|
|
45
|
+
# Geometry types available for creating new classes (excluding GraphNodes)
|
|
46
|
+
available_geometry_types = [
|
|
47
|
+
{"value": "rectangle", "label": "Rectangle"},
|
|
48
|
+
{"value": "polygon", "label": "Polygon"},
|
|
49
|
+
{"value": "bitmap (mask)", "label": "Bitmap (mask)"},
|
|
50
|
+
{"value": "polyline", "label": "Polyline"},
|
|
51
|
+
{"value": "point", "label": "Point"},
|
|
52
|
+
{"value": "any shape", "label": "Any shape"},
|
|
53
|
+
{"value": "oriented bbox", "label": "Oriented Bounding Box"},
|
|
54
|
+
]
|
|
55
|
+
|
|
40
56
|
|
|
41
57
|
class ClassesListSelector(Widget):
|
|
42
58
|
class Routes:
|
|
43
59
|
CHECKBOX_CHANGED = "checkbox_cb"
|
|
60
|
+
CLASS_CREATED = "class_created_cb"
|
|
44
61
|
|
|
45
62
|
def __init__(
|
|
46
63
|
self,
|
|
47
64
|
classes: Optional[Union[List[ObjClass], ObjClassCollection]] = [],
|
|
48
65
|
multiple: Optional[bool] = False,
|
|
49
66
|
empty_notification: Optional[NotificationBox] = None,
|
|
67
|
+
allow_new_classes: Optional[bool] = False,
|
|
50
68
|
widget_id: Optional[str] = None,
|
|
51
69
|
):
|
|
52
|
-
|
|
70
|
+
# Convert to list for internal use to allow mutations when adding new classes
|
|
71
|
+
if isinstance(classes, ObjClassCollection):
|
|
72
|
+
self._classes = list(classes)
|
|
73
|
+
else:
|
|
74
|
+
self._classes = list(classes) if classes else []
|
|
53
75
|
self._multiple = multiple
|
|
76
|
+
self._allow_new_classes = allow_new_classes
|
|
77
|
+
self._class_created_handled = False
|
|
54
78
|
if empty_notification is None:
|
|
55
79
|
empty_notification = NotificationBox(
|
|
56
80
|
title="No classes",
|
|
57
81
|
description="No classes to select.",
|
|
58
82
|
)
|
|
59
83
|
self.empty_notification = empty_notification
|
|
84
|
+
self._error_message = Text("", status="error", font_size=13)
|
|
60
85
|
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
61
86
|
|
|
62
87
|
def get_json_data(self):
|
|
@@ -65,14 +90,29 @@ class ClassesListSelector(Widget):
|
|
|
65
90
|
shape_text = type_to_shape_text.get(cls.geometry_type)
|
|
66
91
|
class_dict = {**cls.to_json(), "shape_text": shape_text.upper() if shape_text else ""}
|
|
67
92
|
classes_list.append(class_dict)
|
|
68
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
"classes": classes_list,
|
|
95
|
+
"availableGeometryTypes": available_geometry_types,
|
|
96
|
+
}
|
|
69
97
|
|
|
70
98
|
def get_json_state(self):
|
|
71
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
"selected": [False for _ in self._classes],
|
|
101
|
+
"createClassDialog": {
|
|
102
|
+
"visible": False,
|
|
103
|
+
"className": "",
|
|
104
|
+
"geometryType": "rectangle",
|
|
105
|
+
"showError": False,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
72
108
|
|
|
73
109
|
def set(self, classes: Union[List[ObjClass], ObjClassCollection]):
|
|
74
110
|
selected_classes = [cls.name for cls in self.get_selected_classes()]
|
|
75
|
-
|
|
111
|
+
# Convert to list for internal use
|
|
112
|
+
if isinstance(classes, ObjClassCollection):
|
|
113
|
+
self._classes = list(classes)
|
|
114
|
+
else:
|
|
115
|
+
self._classes = list(classes) if classes else []
|
|
76
116
|
StateJson()[self.widget_id]["selected"] = [
|
|
77
117
|
cls.name in selected_classes for cls in self._classes
|
|
78
118
|
]
|
|
@@ -110,6 +150,33 @@ class ClassesListSelector(Widget):
|
|
|
110
150
|
def get_all_classes(self):
|
|
111
151
|
return self._classes
|
|
112
152
|
|
|
153
|
+
def _show_error(self, message: str):
|
|
154
|
+
"""Show error message in the create class dialog."""
|
|
155
|
+
self._error_message.text = message
|
|
156
|
+
StateJson()[self.widget_id]["createClassDialog"]["showError"] = True
|
|
157
|
+
StateJson().send_changes()
|
|
158
|
+
|
|
159
|
+
def _hide_dialog(self):
|
|
160
|
+
"""Hide the create class dialog and reset its state."""
|
|
161
|
+
state_obj = StateJson()[self.widget_id]["createClassDialog"]
|
|
162
|
+
state_obj["visible"] = False
|
|
163
|
+
state_obj["className"] = ""
|
|
164
|
+
state_obj["showError"] = False
|
|
165
|
+
StateJson().send_changes()
|
|
166
|
+
|
|
167
|
+
def _add_new_class(self, new_class: ObjClass):
|
|
168
|
+
"""Add a new class to the widget and update the UI."""
|
|
169
|
+
# Add to classes list
|
|
170
|
+
self._classes.append(new_class)
|
|
171
|
+
|
|
172
|
+
# Add selection state for the new class (selected by default)
|
|
173
|
+
StateJson()[self.widget_id]["selected"].append(True)
|
|
174
|
+
|
|
175
|
+
# Update data to reflect the new class in the UI
|
|
176
|
+
self.update_data()
|
|
177
|
+
DataJson().send_changes()
|
|
178
|
+
StateJson().send_changes()
|
|
179
|
+
|
|
113
180
|
def selection_changed(self, func):
|
|
114
181
|
route_path = self.get_route_path(ClassesListSelector.Routes.CHECKBOX_CHANGED)
|
|
115
182
|
server = self._sly_app.get_server()
|
|
@@ -121,3 +188,48 @@ class ClassesListSelector(Widget):
|
|
|
121
188
|
func(selected)
|
|
122
189
|
|
|
123
190
|
return _click
|
|
191
|
+
|
|
192
|
+
def class_created(self, func: Callable[[ObjClass], None]):
|
|
193
|
+
"""
|
|
194
|
+
Decorator to handle new class creation event.
|
|
195
|
+
The decorated function receives the newly created ObjClass.
|
|
196
|
+
|
|
197
|
+
:param func: Function to be called when a new class is created
|
|
198
|
+
:type func: Callable[[ObjClass], None]
|
|
199
|
+
"""
|
|
200
|
+
route_path = self.get_route_path(ClassesListSelector.Routes.CLASS_CREATED)
|
|
201
|
+
server = self._sly_app.get_server()
|
|
202
|
+
self._class_created_handled = True
|
|
203
|
+
|
|
204
|
+
@server.post(route_path)
|
|
205
|
+
def _class_created():
|
|
206
|
+
state = StateJson()[self.widget_id]["createClassDialog"]
|
|
207
|
+
class_name = state["className"].strip()
|
|
208
|
+
geometry_type_str = state["geometryType"]
|
|
209
|
+
|
|
210
|
+
if not class_name:
|
|
211
|
+
self._show_error("Class name cannot be empty")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if any(cls.name == class_name for cls in self._classes):
|
|
215
|
+
self._show_error(f"Class '{class_name}' already exists")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
geometry_type = shape_text_to_type.get(geometry_type_str)
|
|
219
|
+
if geometry_type is None:
|
|
220
|
+
self._show_error("Invalid geometry type")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# Generate color for the new class
|
|
224
|
+
existing_colors = [cls.color for cls in self._classes]
|
|
225
|
+
new_color = generate_rgb(existing_colors)
|
|
226
|
+
|
|
227
|
+
# Create new class
|
|
228
|
+
new_class = ObjClass(name=class_name, geometry_type=geometry_type, color=new_color)
|
|
229
|
+
|
|
230
|
+
self._add_new_class(new_class)
|
|
231
|
+
self._hide_dialog()
|
|
232
|
+
|
|
233
|
+
func(new_class)
|
|
234
|
+
|
|
235
|
+
return _class_created
|