supervisely 6.73.357__py3-none-any.whl → 6.73.359__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/_utils.py +12 -0
- supervisely/api/annotation_api.py +3 -0
- supervisely/api/api.py +2 -2
- supervisely/api/app_api.py +27 -2
- supervisely/api/entity_annotation/tag_api.py +0 -1
- supervisely/api/nn/__init__.py +0 -0
- supervisely/api/nn/deploy_api.py +821 -0
- supervisely/api/nn/neural_network_api.py +248 -0
- supervisely/api/task_api.py +26 -467
- supervisely/app/fastapi/subapp.py +1 -0
- supervisely/nn/__init__.py +2 -1
- supervisely/nn/artifacts/artifacts.py +5 -5
- supervisely/nn/benchmark/object_detection/metric_provider.py +3 -0
- supervisely/nn/experiments.py +28 -5
- supervisely/nn/inference/cache.py +178 -114
- supervisely/nn/inference/gui/gui.py +18 -35
- supervisely/nn/inference/gui/serving_gui.py +3 -1
- supervisely/nn/inference/inference.py +1421 -1265
- supervisely/nn/inference/inference_request.py +412 -0
- supervisely/nn/inference/object_detection_3d/object_detection_3d.py +31 -24
- supervisely/nn/inference/session.py +2 -2
- supervisely/nn/inference/tracking/base_tracking.py +45 -79
- supervisely/nn/inference/tracking/bbox_tracking.py +220 -155
- supervisely/nn/inference/tracking/mask_tracking.py +274 -250
- supervisely/nn/inference/tracking/tracker_interface.py +23 -0
- supervisely/nn/inference/uploader.py +164 -0
- supervisely/nn/model/__init__.py +0 -0
- supervisely/nn/model/model_api.py +259 -0
- supervisely/nn/model/prediction.py +311 -0
- supervisely/nn/model/prediction_session.py +632 -0
- supervisely/nn/tracking/__init__.py +1 -0
- supervisely/nn/tracking/boxmot.py +114 -0
- supervisely/nn/tracking/tracking.py +24 -0
- supervisely/nn/training/train_app.py +61 -19
- supervisely/nn/utils.py +43 -3
- supervisely/task/progress.py +12 -2
- supervisely/video/video.py +107 -1
- {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/METADATA +2 -1
- {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/RECORD +43 -32
- supervisely/api/neural_network_api.py +0 -202
- {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/LICENSE +0 -0
- {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/WHEEL +0 -0
- {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/top_level.txt +0 -0
supervisely/api/task_api.py
CHANGED
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
# docs
|
|
11
11
|
from typing import Any, Callable, Dict, List, Literal, NamedTuple, Optional, Union
|
|
12
12
|
|
|
13
|
+
import requests
|
|
13
14
|
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
|
14
15
|
from tqdm import tqdm
|
|
15
16
|
|
|
@@ -307,56 +308,6 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
307
308
|
f"Waiting time exceeded: total waiting time {wait_attempts * effective_wait_timeout} seconds, i.e. {wait_attempts} attempts for {effective_wait_timeout} seconds each"
|
|
308
309
|
)
|
|
309
310
|
|
|
310
|
-
def upload_dtl_archive(
|
|
311
|
-
self,
|
|
312
|
-
task_id: int,
|
|
313
|
-
archive_path: str,
|
|
314
|
-
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
315
|
-
):
|
|
316
|
-
"""upload_dtl_archive"""
|
|
317
|
-
encoder = MultipartEncoder(
|
|
318
|
-
{
|
|
319
|
-
"id": str(task_id).encode("utf-8"),
|
|
320
|
-
"name": get_file_name(archive_path),
|
|
321
|
-
"archive": (
|
|
322
|
-
os.path.basename(archive_path),
|
|
323
|
-
open(archive_path, "rb"),
|
|
324
|
-
"application/x-tar",
|
|
325
|
-
),
|
|
326
|
-
}
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
def callback(monitor_instance):
|
|
330
|
-
read_mb = monitor_instance.bytes_read / 1024.0 / 1024.0
|
|
331
|
-
if progress_cb is not None:
|
|
332
|
-
progress_cb(read_mb)
|
|
333
|
-
|
|
334
|
-
monitor = MultipartEncoderMonitor(encoder, callback)
|
|
335
|
-
self._api.post("tasks.upload.dtl_archive", monitor)
|
|
336
|
-
|
|
337
|
-
def _deploy_model(
|
|
338
|
-
self,
|
|
339
|
-
agent_id,
|
|
340
|
-
model_id,
|
|
341
|
-
plugin_id=None,
|
|
342
|
-
version=None,
|
|
343
|
-
restart_policy=RestartPolicy.NEVER,
|
|
344
|
-
settings=None,
|
|
345
|
-
):
|
|
346
|
-
"""_deploy_model"""
|
|
347
|
-
response = self._api.post(
|
|
348
|
-
"tasks.run.deploy",
|
|
349
|
-
{
|
|
350
|
-
ApiField.AGENT_ID: agent_id,
|
|
351
|
-
ApiField.MODEL_ID: model_id,
|
|
352
|
-
ApiField.RESTART_POLICY: restart_policy.value,
|
|
353
|
-
ApiField.SETTINGS: settings or {"gpu_device": 0},
|
|
354
|
-
ApiField.PLUGIN_ID: plugin_id,
|
|
355
|
-
ApiField.VERSION: version,
|
|
356
|
-
},
|
|
357
|
-
)
|
|
358
|
-
return response.json()[ApiField.TASK_ID]
|
|
359
|
-
|
|
360
311
|
def get_context(self, id: int) -> Dict:
|
|
361
312
|
"""
|
|
362
313
|
Get context information by task ID.
|
|
@@ -397,113 +348,6 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
397
348
|
"""_convert_json_info"""
|
|
398
349
|
return info
|
|
399
350
|
|
|
400
|
-
def run_dtl(self, workspace_id: int, dtl_graph: Dict, agent_id: Optional[int] = None):
|
|
401
|
-
"""run_dtl"""
|
|
402
|
-
response = self._api.post(
|
|
403
|
-
"tasks.run.dtl",
|
|
404
|
-
{
|
|
405
|
-
ApiField.WORKSPACE_ID: workspace_id,
|
|
406
|
-
ApiField.CONFIG: dtl_graph,
|
|
407
|
-
"advanced": {ApiField.AGENT_ID: agent_id},
|
|
408
|
-
},
|
|
409
|
-
)
|
|
410
|
-
return response.json()[ApiField.TASK_ID]
|
|
411
|
-
|
|
412
|
-
def _run_plugin_task(
|
|
413
|
-
self,
|
|
414
|
-
task_type,
|
|
415
|
-
agent_id,
|
|
416
|
-
plugin_id,
|
|
417
|
-
version,
|
|
418
|
-
config,
|
|
419
|
-
input_projects,
|
|
420
|
-
input_models,
|
|
421
|
-
result_name,
|
|
422
|
-
):
|
|
423
|
-
"""_run_plugin_task"""
|
|
424
|
-
response = self._api.post(
|
|
425
|
-
"tasks.run.plugin",
|
|
426
|
-
{
|
|
427
|
-
"taskType": task_type,
|
|
428
|
-
ApiField.AGENT_ID: agent_id,
|
|
429
|
-
ApiField.PLUGIN_ID: plugin_id,
|
|
430
|
-
ApiField.VERSION: version,
|
|
431
|
-
ApiField.CONFIG: config,
|
|
432
|
-
"projects": input_projects,
|
|
433
|
-
"models": input_models,
|
|
434
|
-
ApiField.NAME: result_name,
|
|
435
|
-
},
|
|
436
|
-
)
|
|
437
|
-
return response.json()[ApiField.TASK_ID]
|
|
438
|
-
|
|
439
|
-
def run_train(
|
|
440
|
-
self,
|
|
441
|
-
agent_id: int,
|
|
442
|
-
input_project_id: int,
|
|
443
|
-
input_model_id: int,
|
|
444
|
-
result_nn_name: str,
|
|
445
|
-
train_config: Optional[Dict] = None,
|
|
446
|
-
):
|
|
447
|
-
"""run_train"""
|
|
448
|
-
model_info = self._api.model.get_info_by_id(input_model_id)
|
|
449
|
-
return self._run_plugin_task(
|
|
450
|
-
task_type=TaskApi.PluginTaskType.TRAIN.value,
|
|
451
|
-
agent_id=agent_id,
|
|
452
|
-
plugin_id=model_info.plugin_id,
|
|
453
|
-
version=None,
|
|
454
|
-
input_projects=[input_project_id],
|
|
455
|
-
input_models=[input_model_id],
|
|
456
|
-
result_name=result_nn_name,
|
|
457
|
-
config={} if train_config is None else train_config,
|
|
458
|
-
)
|
|
459
|
-
|
|
460
|
-
def run_inference(
|
|
461
|
-
self,
|
|
462
|
-
agent_id: int,
|
|
463
|
-
input_project_id: int,
|
|
464
|
-
input_model_id: int,
|
|
465
|
-
result_project_name: str,
|
|
466
|
-
inference_config: Optional[Dict] = None,
|
|
467
|
-
):
|
|
468
|
-
"""run_inference"""
|
|
469
|
-
model_info = self._api.model.get_info_by_id(input_model_id)
|
|
470
|
-
return self._run_plugin_task(
|
|
471
|
-
task_type=TaskApi.PluginTaskType.INFERENCE.value,
|
|
472
|
-
agent_id=agent_id,
|
|
473
|
-
plugin_id=model_info.plugin_id,
|
|
474
|
-
version=None,
|
|
475
|
-
input_projects=[input_project_id],
|
|
476
|
-
input_models=[input_model_id],
|
|
477
|
-
result_name=result_project_name,
|
|
478
|
-
config={} if inference_config is None else inference_config,
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
def get_training_metrics(self, task_id: int):
|
|
482
|
-
"""get_training_metrics"""
|
|
483
|
-
response = self._get_response_by_id(
|
|
484
|
-
id=task_id, method="tasks.train-metrics", id_field=ApiField.TASK_ID
|
|
485
|
-
)
|
|
486
|
-
return response.json() if (response is not None) else None
|
|
487
|
-
|
|
488
|
-
def deploy_model(self, agent_id: int, model_id: int) -> int:
|
|
489
|
-
"""deploy_model"""
|
|
490
|
-
task_ids = self._api.model.get_deploy_tasks(model_id)
|
|
491
|
-
if len(task_ids) == 0:
|
|
492
|
-
task_id = self._deploy_model(agent_id, model_id)
|
|
493
|
-
else:
|
|
494
|
-
task_id = task_ids[0]
|
|
495
|
-
self.wait(task_id, self.Status.DEPLOYED)
|
|
496
|
-
return task_id
|
|
497
|
-
|
|
498
|
-
def deploy_model_async(self, agent_id: int, model_id: int) -> int:
|
|
499
|
-
"""deploy_model_async"""
|
|
500
|
-
task_ids = self._api.model.get_deploy_tasks(model_id)
|
|
501
|
-
if len(task_ids) == 0:
|
|
502
|
-
task_id = self._deploy_model(agent_id, model_id)
|
|
503
|
-
else:
|
|
504
|
-
task_id = task_ids[0]
|
|
505
|
-
return task_id
|
|
506
|
-
|
|
507
351
|
def start(
|
|
508
352
|
self,
|
|
509
353
|
agent_id,
|
|
@@ -628,36 +472,6 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
628
472
|
response = self._api.post("tasks.stop", {ApiField.ID: id})
|
|
629
473
|
return self.Status(response.json()[ApiField.STATUS])
|
|
630
474
|
|
|
631
|
-
def get_import_files_list(self, id: int) -> Union[Dict, None]:
|
|
632
|
-
"""get_import_files_list"""
|
|
633
|
-
response = self._api.post("tasks.import.files_list", {ApiField.ID: id})
|
|
634
|
-
return response.json() if (response is not None) else None
|
|
635
|
-
|
|
636
|
-
def download_import_file(self, id, file_path, save_path):
|
|
637
|
-
"""download_import_file"""
|
|
638
|
-
response = self._api.post(
|
|
639
|
-
"tasks.import.download_file",
|
|
640
|
-
{ApiField.ID: id, ApiField.FILENAME: file_path},
|
|
641
|
-
stream=True,
|
|
642
|
-
)
|
|
643
|
-
|
|
644
|
-
ensure_base_path(save_path)
|
|
645
|
-
with open(save_path, "wb") as fd:
|
|
646
|
-
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
|
647
|
-
fd.write(chunk)
|
|
648
|
-
|
|
649
|
-
def create_task_detached(self, workspace_id: int, task_type: Optional[str] = None):
|
|
650
|
-
"""create_task_detached"""
|
|
651
|
-
response = self._api.post(
|
|
652
|
-
"tasks.run.python",
|
|
653
|
-
{
|
|
654
|
-
ApiField.WORKSPACE_ID: workspace_id,
|
|
655
|
-
ApiField.SCRIPT: "xxx",
|
|
656
|
-
ApiField.ADVANCED: {ApiField.IGNORE_AGENT: True},
|
|
657
|
-
},
|
|
658
|
-
)
|
|
659
|
-
return response.json()[ApiField.TASK_ID]
|
|
660
|
-
|
|
661
475
|
def submit_logs(self, logs) -> None:
|
|
662
476
|
"""submit_logs"""
|
|
663
477
|
response = self._api.post("tasks.logs.add", {ApiField.LOGS: logs})
|
|
@@ -791,28 +605,6 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
791
605
|
result = self.get_fields(task_id, [field])
|
|
792
606
|
return result[field]
|
|
793
607
|
|
|
794
|
-
def _validate_checkpoints_support(self, task_id):
|
|
795
|
-
"""_validate_checkpoints_support"""
|
|
796
|
-
# pylint: disable=too-few-format-args
|
|
797
|
-
info = self.get_info_by_id(task_id)
|
|
798
|
-
if info["type"] != str(TaskApi.PluginTaskType.TRAIN):
|
|
799
|
-
raise RuntimeError(
|
|
800
|
-
"Task (id={!r}) has type {!r}. "
|
|
801
|
-
"Checkpoints are available only for tasks of type {!r}".format()
|
|
802
|
-
)
|
|
803
|
-
|
|
804
|
-
def list_checkpoints(self, task_id: int):
|
|
805
|
-
"""list_checkpoints"""
|
|
806
|
-
self._validate_checkpoints_support(task_id)
|
|
807
|
-
resp = self._api.post("tasks.checkpoints.list", {ApiField.ID: task_id})
|
|
808
|
-
return resp.json()
|
|
809
|
-
|
|
810
|
-
def delete_unused_checkpoints(self, task_id: int) -> Dict:
|
|
811
|
-
"""delete_unused_checkpoints"""
|
|
812
|
-
self._validate_checkpoints_support(task_id)
|
|
813
|
-
resp = self._api.post("tasks.checkpoints.clear", {ApiField.ID: task_id})
|
|
814
|
-
return resp.json()
|
|
815
|
-
|
|
816
608
|
def _set_output(self):
|
|
817
609
|
"""_set_output"""
|
|
818
610
|
pass
|
|
@@ -1229,267 +1021,34 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
1229
1021
|
)
|
|
1230
1022
|
return resp.json()
|
|
1231
1023
|
|
|
1232
|
-
def
|
|
1233
|
-
self.send_request(
|
|
1234
|
-
task_id,
|
|
1235
|
-
"deploy_from_api",
|
|
1236
|
-
data={"deploy_params": deploy_params},
|
|
1237
|
-
raise_error=True,
|
|
1238
|
-
)
|
|
1239
|
-
|
|
1240
|
-
def deploy_model_app(
|
|
1241
|
-
self,
|
|
1242
|
-
module_id: int,
|
|
1243
|
-
workspace_id: int,
|
|
1244
|
-
agent_id: Optional[int] = None,
|
|
1245
|
-
description: Optional[str] = "application description",
|
|
1246
|
-
params: Dict[str, Any] = None,
|
|
1247
|
-
log_level: Optional[Literal["info", "debug", "warning", "error"]] = "info",
|
|
1248
|
-
users_ids: Optional[List[int]] = None,
|
|
1249
|
-
app_version: Optional[str] = "",
|
|
1250
|
-
is_branch: Optional[bool] = False,
|
|
1251
|
-
task_name: Optional[str] = "pythonSpawned",
|
|
1252
|
-
restart_policy: Optional[Literal["never", "on_error"]] = "never",
|
|
1253
|
-
proxy_keep_url: Optional[bool] = False,
|
|
1254
|
-
redirect_requests: Optional[Dict[str, int]] = {},
|
|
1255
|
-
limit_by_workspace: bool = False,
|
|
1256
|
-
deploy_params: Dict[str, Any] = None,
|
|
1257
|
-
timeout: int = 100,
|
|
1258
|
-
):
|
|
1259
|
-
if deploy_params is None:
|
|
1260
|
-
deploy_params = {}
|
|
1261
|
-
task_info = self.start(
|
|
1262
|
-
agent_id=agent_id,
|
|
1263
|
-
workspace_id=workspace_id,
|
|
1264
|
-
module_id=module_id,
|
|
1265
|
-
description=description,
|
|
1266
|
-
params=params,
|
|
1267
|
-
log_level=log_level,
|
|
1268
|
-
users_ids=users_ids,
|
|
1269
|
-
app_version=app_version,
|
|
1270
|
-
is_branch=is_branch,
|
|
1271
|
-
task_name=task_name,
|
|
1272
|
-
restart_policy=restart_policy,
|
|
1273
|
-
proxy_keep_url=proxy_keep_url,
|
|
1274
|
-
redirect_requests=redirect_requests,
|
|
1275
|
-
limit_by_workspace=limit_by_workspace,
|
|
1276
|
-
)
|
|
1277
|
-
|
|
1278
|
-
attempt_delay_sec = 10
|
|
1279
|
-
attempts = (timeout + attempt_delay_sec) // attempt_delay_sec
|
|
1280
|
-
ready = self._api.app.wait_until_ready_for_api_calls(
|
|
1281
|
-
task_info["id"], attempts, attempt_delay_sec
|
|
1282
|
-
)
|
|
1283
|
-
if not ready:
|
|
1284
|
-
raise TimeoutError(
|
|
1285
|
-
f"Task {task_info['id']} is not ready for API calls after {timeout} seconds."
|
|
1286
|
-
)
|
|
1287
|
-
logger.info("Deploying model from API")
|
|
1288
|
-
self.deploy_model_from_api(task_info["id"], deploy_params=deploy_params)
|
|
1289
|
-
return task_info
|
|
1290
|
-
|
|
1291
|
-
def deploy_custom_model(
|
|
1292
|
-
self,
|
|
1293
|
-
workspace_id: int,
|
|
1294
|
-
artifacts_dir: str,
|
|
1295
|
-
checkpoint_name: str = None,
|
|
1296
|
-
agent_id: int = None,
|
|
1297
|
-
device: str = "cuda",
|
|
1298
|
-
) -> int:
|
|
1024
|
+
def is_running(self, task_id: int) -> bool:
|
|
1299
1025
|
"""
|
|
1300
|
-
|
|
1026
|
+
Check if the task is running.
|
|
1301
1027
|
|
|
1302
|
-
:param
|
|
1303
|
-
:type
|
|
1304
|
-
:
|
|
1305
|
-
:
|
|
1306
|
-
:param checkpoint_name: Checkpoint name (with extension) to deploy.
|
|
1307
|
-
:type checkpoint_name: Optional[str]
|
|
1308
|
-
:param agent_id: Agent ID in Supervisely.
|
|
1309
|
-
:type agent_id: Optional[int]
|
|
1310
|
-
:param device: Device string (default is "cuda").
|
|
1311
|
-
:type device: str
|
|
1312
|
-
:raises ValueError: if validations fail.
|
|
1028
|
+
:param task_id: Task ID in Supervisely.
|
|
1029
|
+
:type task_id: int
|
|
1030
|
+
:return: True if the task is running, False otherwise.
|
|
1031
|
+
:rtype: bool
|
|
1313
1032
|
"""
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
Detectron2,
|
|
1320
|
-
MMClassification,
|
|
1321
|
-
MMDetection,
|
|
1322
|
-
MMDetection3,
|
|
1323
|
-
MMSegmentation,
|
|
1324
|
-
UNet,
|
|
1325
|
-
YOLOv5,
|
|
1326
|
-
YOLOv5v2,
|
|
1327
|
-
YOLOv8,
|
|
1328
|
-
)
|
|
1329
|
-
from supervisely.nn.experiments import get_experiment_info_by_artifacts_dir
|
|
1330
|
-
from supervisely.nn.utils import ModelSource, RuntimeType
|
|
1331
|
-
|
|
1332
|
-
if not isinstance(workspace_id, int) or workspace_id <= 0:
|
|
1333
|
-
raise ValueError(f"workspace_id must be a positive integer. Received: {workspace_id}")
|
|
1334
|
-
if not isinstance(artifacts_dir, str) or not artifacts_dir.strip():
|
|
1335
|
-
raise ValueError("artifacts_dir must be a non-empty string.")
|
|
1336
|
-
|
|
1337
|
-
workspace_info = self._api.workspace.get_info_by_id(workspace_id)
|
|
1338
|
-
if workspace_info is None:
|
|
1339
|
-
raise ValueError(f"Workspace with ID '{workspace_id}' not found.")
|
|
1340
|
-
|
|
1341
|
-
team_id = workspace_info.team_id
|
|
1342
|
-
logger.debug(
|
|
1343
|
-
f"Starting model deployment. Team: {team_id}, Workspace: {workspace_id}, Artifacts Dir: '{artifacts_dir}'"
|
|
1344
|
-
)
|
|
1345
|
-
|
|
1346
|
-
# Train V1 logic (if artifacts_dir does not start with '/experiments')
|
|
1347
|
-
if not artifacts_dir.startswith("/experiments"):
|
|
1348
|
-
logger.debug("Deploying model from Train V1 artifacts")
|
|
1349
|
-
frameworks = {
|
|
1350
|
-
"/detectron2": Detectron2,
|
|
1351
|
-
"/mmclassification": MMClassification,
|
|
1352
|
-
"/mmdetection": MMDetection,
|
|
1353
|
-
"/mmdetection-3": MMDetection3,
|
|
1354
|
-
"/mmsegmentation": MMSegmentation,
|
|
1355
|
-
"/RITM_training": RITM,
|
|
1356
|
-
"/RT-DETR": RTDETR,
|
|
1357
|
-
"/unet": UNet,
|
|
1358
|
-
"/yolov5_train": YOLOv5,
|
|
1359
|
-
"/yolov5_2.0_train": YOLOv5v2,
|
|
1360
|
-
"/yolov8_train": YOLOv8,
|
|
1361
|
-
}
|
|
1033
|
+
try:
|
|
1034
|
+
self.send_request(task_id, "is_running", {}, retries=1, raise_error=True)
|
|
1035
|
+
except requests.exceptions.HTTPError as e:
|
|
1036
|
+
return False
|
|
1037
|
+
return True
|
|
1362
1038
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
)
|
|
1367
|
-
if not framework_cls:
|
|
1368
|
-
raise ValueError(f"Unsupported framework for artifacts_dir: '{artifacts_dir}'")
|
|
1369
|
-
|
|
1370
|
-
framework = framework_cls(team_id)
|
|
1371
|
-
if framework_cls is RITM or framework_cls is YOLOv5:
|
|
1372
|
-
raise ValueError(
|
|
1373
|
-
f"{framework.framework_name} framework is not supported for deployment"
|
|
1374
|
-
)
|
|
1375
|
-
|
|
1376
|
-
logger.debug(f"Detected framework: '{framework.framework_name}'")
|
|
1377
|
-
|
|
1378
|
-
module_id = self._api.app.get_ecosystem_module_id(framework.serve_slug)
|
|
1379
|
-
serve_app_name = framework.serve_app_name
|
|
1380
|
-
logger.debug(f"Module ID fetched:' {module_id}'. App name: '{serve_app_name}'")
|
|
1381
|
-
|
|
1382
|
-
train_info = framework.get_info_by_artifacts_dir(artifacts_dir.rstrip("/"))
|
|
1383
|
-
if not hasattr(train_info, "checkpoints") or not train_info.checkpoints:
|
|
1384
|
-
raise ValueError("No checkpoints found in train info.")
|
|
1385
|
-
|
|
1386
|
-
checkpoint = None
|
|
1387
|
-
if checkpoint_name is not None:
|
|
1388
|
-
for cp in train_info.checkpoints:
|
|
1389
|
-
if cp.name == checkpoint_name:
|
|
1390
|
-
checkpoint = cp
|
|
1391
|
-
break
|
|
1392
|
-
if checkpoint is None:
|
|
1393
|
-
raise ValueError(f"Checkpoint '{checkpoint_name}' not found in train info.")
|
|
1394
|
-
else:
|
|
1395
|
-
logger.debug("Checkpoint name not provided. Using the last checkpoint.")
|
|
1396
|
-
checkpoint = train_info.checkpoints[-1]
|
|
1397
|
-
|
|
1398
|
-
checkpoint_name = checkpoint.name
|
|
1399
|
-
deploy_params = {
|
|
1400
|
-
"device": device,
|
|
1401
|
-
"model_source": ModelSource.CUSTOM,
|
|
1402
|
-
"task_type": train_info.task_type,
|
|
1403
|
-
"checkpoint_name": checkpoint_name,
|
|
1404
|
-
"checkpoint_url": checkpoint.path,
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
if getattr(train_info, "config_path", None) is not None:
|
|
1408
|
-
deploy_params["config_url"] = train_info.config_path
|
|
1409
|
-
|
|
1410
|
-
if framework.require_runtime:
|
|
1411
|
-
deploy_params["runtime"] = RuntimeType.PYTORCH
|
|
1412
|
-
|
|
1413
|
-
else: # Train V2 logic (when artifacts_dir starts with '/experiments')
|
|
1414
|
-
logger.debug("Deploying model from Train V2 artifacts")
|
|
1415
|
-
|
|
1416
|
-
def get_framework_from_artifacts_dir(artifacts_dir: str) -> str:
|
|
1417
|
-
clean_path = artifacts_dir.rstrip("/")
|
|
1418
|
-
parts = clean_path.split("/")
|
|
1419
|
-
if not parts or "_" not in parts[-1]:
|
|
1420
|
-
raise ValueError(f"Invalid artifacts_dir format: '{artifacts_dir}'")
|
|
1421
|
-
return parts[-1].split("_", 1)[1]
|
|
1422
|
-
|
|
1423
|
-
# TODO: temporary solution, need to add Serve App Name into config.json
|
|
1424
|
-
framework_name = get_framework_from_artifacts_dir(artifacts_dir)
|
|
1425
|
-
logger.debug(f"Detected framework: {framework_name}")
|
|
1426
|
-
|
|
1427
|
-
modules = self._api.app.get_list_all_pages(
|
|
1428
|
-
method="ecosystem.list",
|
|
1429
|
-
data={"filter": [], "search": framework_name, "categories": ["serve"]},
|
|
1430
|
-
convert_json_info_cb=lambda x: x,
|
|
1431
|
-
)
|
|
1432
|
-
if not modules:
|
|
1433
|
-
raise ValueError(f"No serve apps found for framework: '{framework_name}'")
|
|
1434
|
-
|
|
1435
|
-
module = modules[0]
|
|
1436
|
-
module_id = module["id"]
|
|
1437
|
-
serve_app_name = module["name"]
|
|
1438
|
-
logger.debug(f"Serving app delected: '{serve_app_name}'. Module ID: '{module_id}'")
|
|
1039
|
+
def is_ready(self, task_id: int) -> bool:
|
|
1040
|
+
"""
|
|
1041
|
+
Check if the task is ready.
|
|
1439
1042
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1043
|
+
:param task_id: Task ID in Supervisely.
|
|
1044
|
+
:type task_id: int
|
|
1045
|
+
:return: True if the task is ready, False otherwise.
|
|
1046
|
+
:rtype: bool
|
|
1047
|
+
"""
|
|
1048
|
+
try:
|
|
1049
|
+
return (
|
|
1050
|
+
self.send_request(task_id, "is_ready", {}, retries=1, raise_error=True)["status"]
|
|
1051
|
+
== "ready"
|
|
1442
1052
|
)
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
f"Failed to retrieve experiment info for artifacts_dir: '{artifacts_dir}'"
|
|
1446
|
-
)
|
|
1447
|
-
|
|
1448
|
-
if len(experiment_info.checkpoints) == 0:
|
|
1449
|
-
raise ValueError(f"No checkpoints found in: '{artifacts_dir}'.")
|
|
1450
|
-
|
|
1451
|
-
checkpoint = None
|
|
1452
|
-
if checkpoint_name is not None:
|
|
1453
|
-
for checkpoint_path in experiment_info.checkpoints:
|
|
1454
|
-
if get_file_name_with_ext(checkpoint_path) == checkpoint_name:
|
|
1455
|
-
checkpoint = get_file_name_with_ext(checkpoint_path)
|
|
1456
|
-
break
|
|
1457
|
-
if checkpoint is None:
|
|
1458
|
-
raise ValueError(
|
|
1459
|
-
f"Provided checkpoint '{checkpoint_name}' not found. Using the best checkpoint."
|
|
1460
|
-
)
|
|
1461
|
-
else:
|
|
1462
|
-
logger.debug("Checkpoint name not provided. Using the best checkpoint.")
|
|
1463
|
-
checkpoint = experiment_info.best_checkpoint
|
|
1464
|
-
|
|
1465
|
-
checkpoint_name = checkpoint
|
|
1466
|
-
deploy_params = {
|
|
1467
|
-
"device": device,
|
|
1468
|
-
"model_source": ModelSource.CUSTOM,
|
|
1469
|
-
"model_files": {
|
|
1470
|
-
"checkpoint": f"{experiment_info.artifacts_dir}checkpoints/{checkpoint_name}"
|
|
1471
|
-
},
|
|
1472
|
-
"model_info": asdict(experiment_info),
|
|
1473
|
-
"runtime": RuntimeType.PYTORCH,
|
|
1474
|
-
}
|
|
1475
|
-
# TODO: add support for **kwargs
|
|
1476
|
-
|
|
1477
|
-
config = experiment_info.model_files.get("config")
|
|
1478
|
-
if config is not None:
|
|
1479
|
-
deploy_params["model_files"]["config"] = f"{experiment_info.artifacts_dir}{config}"
|
|
1480
|
-
logger.debug(f"Config file added: {experiment_info.artifacts_dir}{config}")
|
|
1481
|
-
|
|
1482
|
-
logger.info(
|
|
1483
|
-
f"{serve_app_name} app deployment started. Checkpoint: '{checkpoint_name}'. Deploy params: '{deploy_params}'"
|
|
1484
|
-
)
|
|
1485
|
-
task_info = self.deploy_model_app(
|
|
1486
|
-
module_id,
|
|
1487
|
-
workspace_id,
|
|
1488
|
-
agent_id,
|
|
1489
|
-
description=f"Deployed via deploy_custom_model",
|
|
1490
|
-
task_name=f"{serve_app_name} ({checkpoint_name})",
|
|
1491
|
-
deploy_params=deploy_params,
|
|
1492
|
-
)
|
|
1493
|
-
if task_info is None:
|
|
1494
|
-
raise RuntimeError(f"Failed to run '{serve_app_name}'.")
|
|
1495
|
-
return task_info["id"]
|
|
1053
|
+
except requests.exceptions.HTTPError as e:
|
|
1054
|
+
return False
|
|
@@ -1018,6 +1018,7 @@ class Application(metaclass=Singleton):
|
|
|
1018
1018
|
|
|
1019
1019
|
@server.get("/readyz")
|
|
1020
1020
|
@server.get("/is_ready")
|
|
1021
|
+
@server.post("/is_ready")
|
|
1021
1022
|
async def is_ready(response: Response, request: Request):
|
|
1022
1023
|
is_ready = True
|
|
1023
1024
|
if self._ready_check_function is not None:
|
supervisely/nn/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ import supervisely.nn.artifacts as artifacts
|
|
|
2
2
|
import supervisely.nn.benchmark as benchmark
|
|
3
3
|
import supervisely.nn.inference as inference
|
|
4
4
|
from supervisely.nn.artifacts.artifacts import BaseTrainArtifacts, TrainInfo
|
|
5
|
+
from supervisely.nn.experiments import ExperimentInfo, get_experiment_infos
|
|
5
6
|
from supervisely.nn.prediction_dto import (
|
|
6
7
|
Prediction,
|
|
7
8
|
PredictionAlphaMask,
|
|
@@ -14,4 +15,4 @@ from supervisely.nn.prediction_dto import (
|
|
|
14
15
|
)
|
|
15
16
|
from supervisely.nn.task_type import TaskType
|
|
16
17
|
from supervisely.nn.utils import ModelSource, RuntimeType
|
|
17
|
-
from supervisely.nn.
|
|
18
|
+
from supervisely.nn.model.model_api import ModelAPI
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import random
|
|
2
3
|
import string
|
|
3
4
|
from abc import abstractmethod
|
|
@@ -9,15 +10,14 @@ from json import JSONDecodeError
|
|
|
9
10
|
from os.path import dirname, join
|
|
10
11
|
from time import time
|
|
11
12
|
from typing import Any, Dict, List, Literal, NamedTuple, Union
|
|
12
|
-
|
|
13
13
|
import requests
|
|
14
14
|
|
|
15
15
|
from supervisely import logger
|
|
16
16
|
from supervisely._utils import abs_url, is_development
|
|
17
|
-
from supervisely.api.api import Api, ApiField
|
|
18
17
|
from supervisely.api.file_api import FileInfo
|
|
19
18
|
from supervisely.io.fs import get_file_name_with_ext, silent_remove
|
|
20
19
|
from supervisely.io.json import dump_json_file
|
|
20
|
+
from supervisely.api.api import Api, ApiField
|
|
21
21
|
from supervisely.nn.experiments import ExperimentInfo
|
|
22
22
|
|
|
23
23
|
|
|
@@ -578,7 +578,7 @@ class BaseTrainArtifacts:
|
|
|
578
578
|
|
|
579
579
|
def convert_train_to_experiment_info(
|
|
580
580
|
self, train_info: TrainInfo
|
|
581
|
-
) -> Union[ExperimentInfo, None]:
|
|
581
|
+
) -> Union['ExperimentInfo', None]:
|
|
582
582
|
try:
|
|
583
583
|
checkpoints = []
|
|
584
584
|
for chk in train_info.checkpoints:
|
|
@@ -637,7 +637,7 @@ class BaseTrainArtifacts:
|
|
|
637
637
|
|
|
638
638
|
def get_list_experiment_info(
|
|
639
639
|
self, sort: Literal["desc", "asc"] = "desc"
|
|
640
|
-
) -> List[ExperimentInfo]:
|
|
640
|
+
) -> List['ExperimentInfo']:
|
|
641
641
|
train_infos = self.get_list(sort)
|
|
642
642
|
|
|
643
643
|
# Sync version
|
|
@@ -671,7 +671,7 @@ class BaseTrainArtifacts:
|
|
|
671
671
|
self,
|
|
672
672
|
artifacts_dir: str,
|
|
673
673
|
return_type: Literal["train_info", "experiment_info"] = "train_info",
|
|
674
|
-
) -> Union[TrainInfo, ExperimentInfo, None]:
|
|
674
|
+
) -> Union[TrainInfo, 'ExperimentInfo', None]:
|
|
675
675
|
"""
|
|
676
676
|
Get training info by artifacts directory.
|
|
677
677
|
|
|
@@ -4,6 +4,7 @@ from copy import deepcopy
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
|
+
from supervisely._utils import logger
|
|
7
8
|
from supervisely.nn.benchmark.utils.detection import metrics
|
|
8
9
|
|
|
9
10
|
METRIC_NAMES = {
|
|
@@ -106,6 +107,8 @@ class MetricProvider:
|
|
|
106
107
|
|
|
107
108
|
# Confidence threshold that will be used in visualizations
|
|
108
109
|
self.conf_threshold = self.custom_conf_threshold or self.f1_optimal_conf
|
|
110
|
+
if self.conf_threshold is None:
|
|
111
|
+
raise RuntimeError("Model predicted no TP matches. Cannot calculate metrics.")
|
|
109
112
|
|
|
110
113
|
# Filter by optimal confidence threshold
|
|
111
114
|
if self.conf_threshold is not None:
|
supervisely/nn/experiments.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
-
from dataclasses import dataclass, fields
|
|
2
|
+
from dataclasses import MISSING, dataclass, fields
|
|
3
3
|
from json import JSONDecodeError
|
|
4
4
|
from os.path import dirname, join
|
|
5
|
-
from typing import List, Optional, Union
|
|
5
|
+
from typing import Dict, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
@@ -59,6 +59,26 @@ class ExperimentInfo:
|
|
|
59
59
|
logs: Optional[dict] = None
|
|
60
60
|
"""Dictionary with link and type of logger"""
|
|
61
61
|
|
|
62
|
+
def __init__(self, **kwargs):
|
|
63
|
+
required_fieds = {
|
|
64
|
+
field.name for field in fields(self.__class__) if field.default is MISSING
|
|
65
|
+
}
|
|
66
|
+
missing_fields = required_fieds - set(kwargs.keys())
|
|
67
|
+
if missing_fields:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"ExperimentInfo missing required arguments: '{', '.join(missing_fields)}'"
|
|
70
|
+
)
|
|
71
|
+
field_names = set(f.name for f in fields(self.__class__))
|
|
72
|
+
kwargs = {k: v for k, v in kwargs.items() if k in field_names}
|
|
73
|
+
for key, value in kwargs.items():
|
|
74
|
+
setattr(self, key, value)
|
|
75
|
+
|
|
76
|
+
def to_json(self) -> Dict:
|
|
77
|
+
data = {}
|
|
78
|
+
for field in fields(self.__class__):
|
|
79
|
+
value = getattr(self, field.name)
|
|
80
|
+
data[field.name] = value
|
|
81
|
+
|
|
62
82
|
|
|
63
83
|
def get_experiment_infos(api: Api, team_id: int, framework_name: str) -> List[ExperimentInfo]:
|
|
64
84
|
"""
|
|
@@ -128,7 +148,8 @@ def get_experiment_infos(api: Api, team_id: int, framework_name: str) -> List[Ex
|
|
|
128
148
|
f"Missing required fields: {missing_required_fields} for '{experiment_path}'. Skipping."
|
|
129
149
|
)
|
|
130
150
|
return None
|
|
131
|
-
|
|
151
|
+
field_names = {field.name for field in fields(ExperimentInfo)}
|
|
152
|
+
return ExperimentInfo(**{k: v for k, v in response_json.items() if k in field_names})
|
|
132
153
|
except requests.exceptions.RequestException as e:
|
|
133
154
|
logger.debug(f"Request failed for '{experiment_path}': {e}")
|
|
134
155
|
except JSONDecodeError as e:
|
|
@@ -166,9 +187,11 @@ def _fetch_experiment_data(api, team_id: int, experiment_path: str) -> Union[Exp
|
|
|
166
187
|
response.raise_for_status()
|
|
167
188
|
response_json = response.json()
|
|
168
189
|
required_fields = {
|
|
169
|
-
field.name for field in fields(ExperimentInfo) if field.default is
|
|
190
|
+
field.name for field in fields(ExperimentInfo) if field.default is MISSING
|
|
191
|
+
}
|
|
192
|
+
optional_fields = {
|
|
193
|
+
field.name for field in fields(ExperimentInfo) if field.default is not MISSING
|
|
170
194
|
}
|
|
171
|
-
optional_fields = {field.name for field in fields(ExperimentInfo) if field.default is None}
|
|
172
195
|
|
|
173
196
|
missing_optional_fields = optional_fields - response_json.keys()
|
|
174
197
|
if missing_optional_fields:
|