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.
Files changed (44) hide show
  1. supervisely/_utils.py +12 -0
  2. supervisely/api/annotation_api.py +3 -0
  3. supervisely/api/api.py +2 -2
  4. supervisely/api/app_api.py +27 -2
  5. supervisely/api/entity_annotation/tag_api.py +0 -1
  6. supervisely/api/nn/__init__.py +0 -0
  7. supervisely/api/nn/deploy_api.py +821 -0
  8. supervisely/api/nn/neural_network_api.py +248 -0
  9. supervisely/api/task_api.py +26 -467
  10. supervisely/app/fastapi/subapp.py +1 -0
  11. supervisely/nn/__init__.py +2 -1
  12. supervisely/nn/artifacts/artifacts.py +5 -5
  13. supervisely/nn/benchmark/object_detection/metric_provider.py +3 -0
  14. supervisely/nn/experiments.py +28 -5
  15. supervisely/nn/inference/cache.py +178 -114
  16. supervisely/nn/inference/gui/gui.py +18 -35
  17. supervisely/nn/inference/gui/serving_gui.py +3 -1
  18. supervisely/nn/inference/inference.py +1421 -1265
  19. supervisely/nn/inference/inference_request.py +412 -0
  20. supervisely/nn/inference/object_detection_3d/object_detection_3d.py +31 -24
  21. supervisely/nn/inference/session.py +2 -2
  22. supervisely/nn/inference/tracking/base_tracking.py +45 -79
  23. supervisely/nn/inference/tracking/bbox_tracking.py +220 -155
  24. supervisely/nn/inference/tracking/mask_tracking.py +274 -250
  25. supervisely/nn/inference/tracking/tracker_interface.py +23 -0
  26. supervisely/nn/inference/uploader.py +164 -0
  27. supervisely/nn/model/__init__.py +0 -0
  28. supervisely/nn/model/model_api.py +259 -0
  29. supervisely/nn/model/prediction.py +311 -0
  30. supervisely/nn/model/prediction_session.py +632 -0
  31. supervisely/nn/tracking/__init__.py +1 -0
  32. supervisely/nn/tracking/boxmot.py +114 -0
  33. supervisely/nn/tracking/tracking.py +24 -0
  34. supervisely/nn/training/train_app.py +61 -19
  35. supervisely/nn/utils.py +43 -3
  36. supervisely/task/progress.py +12 -2
  37. supervisely/video/video.py +107 -1
  38. {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/METADATA +2 -1
  39. {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/RECORD +43 -32
  40. supervisely/api/neural_network_api.py +0 -202
  41. {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/LICENSE +0 -0
  42. {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/WHEEL +0 -0
  43. {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/entry_points.txt +0 -0
  44. {supervisely-6.73.357.dist-info → supervisely-6.73.359.dist-info}/top_level.txt +0 -0
@@ -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 deploy_model_from_api(self, task_id, deploy_params):
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
- Deploy a custom model based on the artifacts directory.
1026
+ Check if the task is running.
1301
1027
 
1302
- :param workspace_id: Workspace ID in Supervisely.
1303
- :type workspace_id: int
1304
- :param artifacts_dir: Path to the artifacts directory.
1305
- :type artifacts_dir: str
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
- from dataclasses import asdict
1315
-
1316
- from supervisely.nn.artifacts import (
1317
- RITM,
1318
- RTDETR,
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
- framework_cls = next(
1364
- (cls for prefix, cls in frameworks.items() if artifacts_dir.startswith(prefix)),
1365
- None,
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
- experiment_info = get_experiment_info_by_artifacts_dir(
1441
- self._api, team_id, artifacts_dir
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
- if not experiment_info:
1444
- raise ValueError(
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:
@@ -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.experiments import ExperimentInfo, get_experiment_infos
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:
@@ -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
- return ExperimentInfo(**response_json)
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 not None
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: