supervisely 6.73.399__py3-none-any.whl → 6.73.400__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.
@@ -17,7 +17,7 @@ from collections import defaultdict
17
17
  from concurrent.futures import ThreadPoolExecutor
18
18
  from contextlib import contextmanager
19
19
  from dataclasses import dataclass
20
- from datetime import datetime
20
+ from datetime import datetime, timezone
21
21
  from functools import partial
22
22
  from math import ceil
23
23
  from pathlib import Path
@@ -57,6 +57,10 @@ from supervisely.annotation.tag import Tag
57
57
  from supervisely.annotation.tag_meta import TagApplicableTo, TagMeta, TagValueType
58
58
  from supervisely.api.constants import DOWNLOAD_BATCH_SIZE
59
59
  from supervisely.api.dataset_api import DatasetInfo
60
+ from supervisely.api.entities_collection_api import (
61
+ AiSearchThresholdDirection,
62
+ CollectionTypeFilter,
63
+ )
60
64
  from supervisely.api.entity_annotation.figure_api import FigureApi
61
65
  from supervisely.api.entity_annotation.tag_api import TagApi
62
66
  from supervisely.api.file_api import FileInfo
@@ -379,6 +383,15 @@ class ImageInfo(NamedTuple):
379
383
  #: :class:`int`: Bytes offset of the blob file that points to the end of the image data.
380
384
  offset_end: Optional[int] = None
381
385
 
386
+ #: :class:`dict`: Image meta that could have the confidence level of the image in Enntities Collection of type AI Search.
387
+ ai_search_meta: Optional[dict] = None
388
+
389
+ #: :class:`str`: Timestamp of the last update of the embeddings for the image.
390
+ #: This field is used to track when the embeddings were last updated.
391
+ #: It is set to None if the embeddings have never been computed for the image.
392
+ #: Format: "YYYY-MM-DDTHH:MM:SS.sssZ"
393
+ embeddings_updated_at: Optional[str] = None
394
+
382
395
  # DO NOT DELETE THIS COMMENT
383
396
  #! New fields must be added with default values to keep backward compatibility.
384
397
 
@@ -456,6 +469,8 @@ class ImageApi(RemoveableBulkModuleApi):
456
469
  ApiField.DOWNLOAD_ID,
457
470
  ApiField.OFFSET_START,
458
471
  ApiField.OFFSET_END,
472
+ ApiField.AI_SEARCH_META,
473
+ ApiField.EMBEDDINGS_UPDATED_AT,
459
474
  ]
460
475
 
461
476
  @staticmethod
@@ -602,6 +617,10 @@ class ImageApi(RemoveableBulkModuleApi):
602
617
  fields: Optional[List[str]] = None,
603
618
  recursive: Optional[bool] = False,
604
619
  entities_collection_id: Optional[int] = None,
620
+ ai_search_collection_id: Optional[int] = None,
621
+ ai_search_threshold: Optional[float] = None,
622
+ ai_search_threshold_direction: AiSearchThresholdDirection = AiSearchThresholdDirection.ABOVE,
623
+ extra_fields: Optional[List[str]] = None,
605
624
  ) -> List[ImageInfo]:
606
625
  """
607
626
  List of Images in the given :class:`Dataset<supervisely.project.project.Dataset>`.
@@ -628,8 +647,16 @@ class ImageApi(RemoveableBulkModuleApi):
628
647
  :type fields: List[str], optional
629
648
  :param recursive: If True, returns all images from dataset recursively (including images in nested datasets).
630
649
  :type recursive: bool, optional
631
- :param entities_collection_id: :class:`EntitiesCollection<supervisely.api.entities_collection_api.EntitiesCollectionApi>` ID to which the images belong. Can be used to filter images by specific entities collection.
650
+ :param entities_collection_id: :class:`EntitiesCollection` ID of `Default` type to which the images belong.
632
651
  :type entities_collection_id: int, optional
652
+ :param ai_search_collection_id: :class:`EntitiesCollection` ID of type `AI Search` to which the images belong.
653
+ :type ai_search_collection_id: int, optional
654
+ :param ai_search_threshold: Confidence level to filter images in AI Search collection.
655
+ :type ai_search_threshold: float, optional
656
+ :param ai_search_threshold_direction: Direction of the confidence level filter. One of {'above' (default), 'below'}.
657
+ :type ai_search_threshold_direction: str, optional
658
+ :param extra_fields: List of extra fields to return. If None, returns no extra fields.
659
+ :type extra_fields: List[str], optional
633
660
  :return: Objects with image information from Supervisely.
634
661
  :rtype: :class:`List[ImageInfo]<ImageInfo>`
635
662
  :Usage example:
@@ -703,20 +730,44 @@ class ImageApi(RemoveableBulkModuleApi):
703
730
  },
704
731
  }
705
732
  ]
706
- if entities_collection_id is not None:
733
+ # Handle collection filtering
734
+ collection_info = None
735
+ if entities_collection_id is not None and ai_search_collection_id is not None:
736
+ raise ValueError(
737
+ "You can use only one of entities_collection_id or ai_search_collection_id"
738
+ )
739
+ elif entities_collection_id is not None:
740
+ collection_info = (CollectionTypeFilter.DEFAULT, entities_collection_id)
741
+ elif ai_search_collection_id is not None:
742
+ collection_info = (CollectionTypeFilter.AI_SEARCH, ai_search_collection_id)
743
+
744
+ if collection_info is not None:
745
+ collection_type, collection_id = collection_info
707
746
  if ApiField.FILTERS not in data:
708
747
  data[ApiField.FILTERS] = []
748
+
749
+ collection_filter_data = {
750
+ ApiField.COLLECTION_ID: collection_id,
751
+ ApiField.INCLUDE: True,
752
+ }
753
+ if ai_search_threshold is not None:
754
+ if collection_type != CollectionTypeFilter.AI_SEARCH:
755
+ raise ValueError(
756
+ "ai_search_threshold is only available for AI Search collection"
757
+ )
758
+ collection_filter_data[ApiField.THRESHOLD] = ai_search_threshold
759
+ collection_filter_data[ApiField.THRESHOLD_DIRECTION] = ai_search_threshold_direction
709
760
  data[ApiField.FILTERS].append(
710
761
  {
711
- "type": "entities_collection",
712
- "data": {
713
- ApiField.COLLECTION_ID: entities_collection_id,
714
- ApiField.INCLUDE: True,
715
- },
762
+ ApiField.TYPE: collection_type,
763
+ ApiField.DATA: collection_filter_data,
716
764
  }
717
765
  )
766
+
718
767
  if fields is not None:
719
768
  data[ApiField.FIELDS] = fields
769
+ if extra_fields is not None:
770
+ data[ApiField.EXTRA_FIELDS] = extra_fields
720
771
  return self.get_list_all_pages(
721
772
  "images.list",
722
773
  data=data,
@@ -3679,9 +3730,7 @@ class ImageApi(RemoveableBulkModuleApi):
3679
3730
 
3680
3731
  return self.tag.add_to_entities_json(project_id, data, batch_size, log_progress)
3681
3732
 
3682
- def update_tag_value(
3683
- self, tag_id: int, value: Union[str, float]
3684
- ) -> Dict:
3733
+ def update_tag_value(self, tag_id: int, value: Union[str, float]) -> Dict:
3685
3734
  """
3686
3735
  Update tag value with given ID.
3687
3736
 
@@ -4412,11 +4461,14 @@ class ImageApi(RemoveableBulkModuleApi):
4412
4461
 
4413
4462
  def set_remote(self, images: List[int], links: List[str]):
4414
4463
  """
4415
- This method helps to change local source to remote for images without re-uploading them as new.
4464
+ Updates the source of existing images by setting new remote links.
4465
+ This method is used when an image was initially uploaded as a file or added via a link,
4466
+ but later it was decided to change its location (e.g., moved to another storage or re-uploaded elsewhere).
4467
+ By updating the link, the image source can be redirected to the new location.
4416
4468
 
4417
4469
  :param images: List of image ids.
4418
4470
  :type images: List[int]
4419
- :param links: List of remote links.
4471
+ :param links: List of new remote links.
4420
4472
  :type links: List[str]
4421
4473
  :return: json-encoded content of a response.
4422
4474
 
@@ -4424,17 +4476,17 @@ class ImageApi(RemoveableBulkModuleApi):
4424
4476
 
4425
4477
  .. code-block:: python
4426
4478
 
4427
- import supervisely as sly
4479
+ import supervisely as sly
4428
4480
 
4429
- api = sly.Api.from_env()
4481
+ api = sly.Api.from_env()
4430
4482
 
4431
- images = [123, 124, 125]
4432
- links = [
4433
- "s3://bucket/lemons/ds1/img/IMG_444.jpeg",
4434
- "s3://bucket/lemons/ds1/img/IMG_445.jpeg",
4435
- "s3://bucket/lemons/ds1/img/IMG_446.jpeg",
4436
- ]
4437
- result = api.image.set_remote(images, links)
4483
+ images = [123, 124, 125]
4484
+ links = [
4485
+ "s3://bucket/lemons/ds1/img/IMG_444.jpeg",
4486
+ "s3://bucket/lemons/ds1/img/IMG_445.jpeg",
4487
+ "s3://bucket/lemons/ds1/img/IMG_446.jpeg",
4488
+ ]
4489
+ result = api.image.set_remote(images, links)
4438
4490
  """
4439
4491
 
4440
4492
  if len(images) == 0:
@@ -5412,3 +5464,47 @@ class ImageApi(RemoveableBulkModuleApi):
5412
5464
  )
5413
5465
  tasks.append(task)
5414
5466
  await asyncio.gather(*tasks)
5467
+
5468
+ def set_embeddings_updated_at(self, ids: List[int], timestamps: Optional[List[str]] = None):
5469
+ """
5470
+ Updates the `updated_at` field of images with the timestamp of the embeddings were created.
5471
+
5472
+ :param ids: List of Image IDs in Supervisely.
5473
+ :type ids: List[int]
5474
+ :param timestamps: List of timestamps in ISO format. If None, uses current time.
5475
+ You could set timestamps to [None, ..., None] if you need to recreate embeddings for images.
5476
+ :type timestamps: List[str], optional
5477
+ :return: None
5478
+ :rtype: NoneType
5479
+ :Usage example:
5480
+
5481
+ .. code-block:: python
5482
+
5483
+ import supervisely as sly
5484
+ import datetime
5485
+
5486
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
5487
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
5488
+ api = sly.Api.from_env()
5489
+
5490
+ image_ids = [123, 456, 789]
5491
+ timestamps = [datetime.datetime.now().isoformat() for _ in image_ids]
5492
+ api.image.set_embeddings_updated_at(image_ids, timestamps)
5493
+ """
5494
+ method = "images.embeddings-updated-at.update"
5495
+
5496
+ if timestamps is None:
5497
+ timestamps = [datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") for _ in ids]
5498
+
5499
+ if len(ids) != len(timestamps):
5500
+ raise ValueError(
5501
+ f"Length of ids and timestamps should be equal. {len(ids)} != {len(timestamps)}"
5502
+ )
5503
+ images = [
5504
+ {ApiField.ID: image_id, ApiField.EMBEDDINGS_UPDATED_AT: timestamp}
5505
+ for image_id, timestamp in zip(ids, timestamps)
5506
+ ]
5507
+ self._api.post(
5508
+ method,
5509
+ {ApiField.IMAGES: images},
5510
+ )
@@ -663,6 +663,32 @@ class ApiField:
663
663
  """"""
664
664
  QUALITY_CHECK_USER_IDS = "qualityCheckUserIds"
665
665
  """"""
666
+ EMBEDDINGS = "embeddings"
667
+ """"""
668
+ EMBEDDINGS_ENABLED = "embeddingsEnabled"
669
+ """"""
670
+ EMBEDDINGS_UPDATED_AT = "embeddingsUpdatedAt"
671
+ """"""
672
+ EMBEDDINGS_IN_PROGRESS = "embeddingsInProgress"
673
+ """"""
674
+ AI_SEARCH_KEY = "aiSearchKey"
675
+ """"""
676
+ AI_SEARCH_META = "aiSearchMeta"
677
+ """"""
678
+ ENTITY_ITEMS = "entityItems"
679
+ """"""
680
+ SCORE = "score"
681
+ """"""
682
+ HARD_DELETE = "hardDelete"
683
+ """"""
684
+ THRESHOLD = "threshold"
685
+ """"""
686
+ THRESHOLD_DIRECTION = "thresholdDirection"
687
+ """"""
688
+ METHOD = "method"
689
+ """"""
690
+ PROMPT = "prompt"
691
+ """"""
666
692
  UPDATE_STRATEGY = "updateStrategy"
667
693
  """"""
668
694
 
@@ -24,7 +24,7 @@ from tqdm import tqdm
24
24
  if TYPE_CHECKING:
25
25
  from pandas.core.frame import DataFrame
26
26
 
27
- from datetime import datetime, timedelta
27
+ from datetime import datetime, timedelta, timezone
28
28
 
29
29
  from supervisely import logger
30
30
  from supervisely._utils import (
@@ -97,6 +97,9 @@ class ProjectInfo(NamedTuple):
97
97
  import_settings: dict
98
98
  version: dict
99
99
  created_by_id: int
100
+ embeddings_enabled: Optional[bool] = None
101
+ embeddings_updated_at: Optional[str] = None
102
+ embeddings_in_progress: Optional[bool] = None
100
103
 
101
104
  @property
102
105
  def image_preview_url(self):
@@ -145,6 +148,8 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
145
148
  project_info = api.project.get_info_by_id(project_id)
146
149
  """
147
150
 
151
+ debug_messages_sent = {"get_list_versions": False}
152
+
148
153
  @staticmethod
149
154
  def info_sequence():
150
155
  """
@@ -195,6 +200,9 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
195
200
  ApiField.IMPORT_SETTINGS,
196
201
  ApiField.VERSION,
197
202
  ApiField.CREATED_BY_ID,
203
+ ApiField.EMBEDDINGS_ENABLED,
204
+ ApiField.EMBEDDINGS_UPDATED_AT,
205
+ ApiField.EMBEDDINGS_IN_PROGRESS,
198
206
  ]
199
207
 
200
208
  @staticmethod
@@ -227,7 +235,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
227
235
  :type workspace_id: int
228
236
  :param filters: List of params to sort output Projects.
229
237
  :type filters: List[dict], optional
230
- :param fields: The list of api fields which will be returned with the response.
238
+ :param fields: The list of api fields which will be returned with the response. You must specify all fields you want to receive, not just additional ones.
231
239
  :type fields: List[str]
232
240
 
233
241
  :return: List of all projects with information for the given Workspace. See :class:`info_sequence<info_sequence>`
@@ -306,25 +314,29 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
306
314
  # ]
307
315
 
308
316
  """
317
+ method = "projects.list"
318
+
319
+ debug_message = "While getting list of projects, the following fields are not available: "
320
+
309
321
  if ApiField.VERSION in fields:
310
322
  fields.remove(ApiField.VERSION)
311
- logger.debug(
312
- "Project version information is not available while getting list of projects"
313
- )
314
- return self.get_list_all_pages(
315
- "projects.list",
316
- {
317
- ApiField.WORKSPACE_ID: workspace_id,
318
- ApiField.FILTER: filters or [],
319
- ApiField.FIELDS: fields,
320
- },
321
- )
323
+ if self.debug_messages_sent.get("get_list_versions", False) is False:
324
+ self.debug_messages_sent["get_list_versions"] = True
325
+ logger.debug(debug_message + "version. ")
326
+
327
+ data = {
328
+ ApiField.WORKSPACE_ID: workspace_id,
329
+ ApiField.FILTER: filters or [],
330
+ ApiField.FIELDS: fields,
331
+ }
332
+
333
+ return self.get_list_all_pages(method, data)
322
334
 
323
335
  def get_info_by_id(
324
336
  self,
325
337
  id: int,
326
338
  expected_type: Optional[str] = None,
327
- raise_error: Optional[bool] = False,
339
+ raise_error: bool = False,
328
340
  ) -> ProjectInfo:
329
341
  """
330
342
  Get Project information by ID.
@@ -892,8 +904,6 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
892
904
  def _convert_json_info(self, info: dict, skip_missing=True) -> ProjectInfo:
893
905
  """ """
894
906
  res = super()._convert_json_info(info, skip_missing=skip_missing)
895
- if res.reference_image_url is not None:
896
- res = res._replace(reference_image_url=res.reference_image_url)
897
907
  if res.items_count is None:
898
908
  res = res._replace(items_count=res.images_count)
899
909
  return ProjectInfo(**res._asdict())
@@ -1976,6 +1986,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
1976
1986
  per_page: Optional[int] = None,
1977
1987
  page: Union[int, Literal["all"]] = "all",
1978
1988
  account_type: Optional[str] = None,
1989
+ extra_fields: Optional[List[str]] = None,
1979
1990
  ) -> dict:
1980
1991
  """
1981
1992
  List all available projects from all available teams for the user that match the specified filtering criteria.
@@ -2009,6 +2020,9 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2009
2020
  :param account_type: (Deprecated) Type of user account
2010
2021
  :type account_type: str, optional
2011
2022
 
2023
+ :param extra_fields: List of additional fields to be included in the response.
2024
+ :type extra_fields: List[str], optional
2025
+
2012
2026
  :return: Search response information and 'ProjectInfo' of all projects that are searched by a given criterion.
2013
2027
  :rtype: dict
2014
2028
 
@@ -2101,6 +2115,8 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2101
2115
  logger.warning(
2102
2116
  "The 'account_type' parameter is deprecated. The result will not be filtered by account type. To filter received ProjectInfos, you could use the 'team_id' from the ProjectInfo object to get TeamInfo and check the account type."
2103
2117
  )
2118
+ if extra_fields is not None:
2119
+ request_body[ApiField.EXTRA_FIELDS] = extra_fields
2104
2120
 
2105
2121
  first_response = self._api.post(method, request_body).json()
2106
2122
 
@@ -2141,3 +2157,206 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2141
2157
  )
2142
2158
  _convert_entities(first_response)
2143
2159
  return first_response
2160
+
2161
+ def enable_embeddings(self, id: int, silent: bool = True) -> None:
2162
+ """
2163
+ Enable embeddings for the project.
2164
+
2165
+ :param id: Project ID
2166
+ :type id: int
2167
+ :param silent: Determines whether the `updatedAt` timestamp of the Project should be updated or not, if False - update `updatedAt`
2168
+ :type silent: bool
2169
+ :return: None
2170
+ :rtype: :class:`NoneType`
2171
+ """
2172
+ self._api.post(
2173
+ "projects.editInfo",
2174
+ {ApiField.ID: id, ApiField.EMBEDDINGS_ENABLED: True, ApiField.SILENT: silent},
2175
+ )
2176
+
2177
+ def disable_embeddings(self, id: int, silent: bool = True) -> None:
2178
+ """
2179
+ Disable embeddings for the project.
2180
+
2181
+ :param id: Project ID
2182
+ :type id: int
2183
+ :param silent: Determines whether the `updatedAt` timestamp of the Poject should be updated or not, if False - update `updatedAt`
2184
+ :type silent: bool
2185
+ :return: None
2186
+ :rtype: :class:`NoneType`
2187
+ """
2188
+ self._api.post(
2189
+ "projects.editInfo",
2190
+ {ApiField.ID: id, ApiField.EMBEDDINGS_ENABLED: False, ApiField.SILENT: silent},
2191
+ )
2192
+
2193
+ def set_embeddings_in_progress(self, id: int, in_progress: bool) -> None:
2194
+ """
2195
+ Set embeddings in progress status for the project.
2196
+ This method is used to indicate whether embeddings are currently being created for the project.
2197
+
2198
+ :param id: Project ID
2199
+ :type id: int
2200
+ :param in_progress: Status to set. If True, embeddings are in progress right now.
2201
+ :type in_progress: bool
2202
+ :return: None
2203
+ :rtype: :class:`NoneType`
2204
+ """
2205
+ self._api.post(
2206
+ "projects.embeddings-in-progress.update",
2207
+ {ApiField.ID: id, ApiField.EMBEDDINGS_IN_PROGRESS: in_progress},
2208
+ )
2209
+
2210
+ def set_embeddings_updated_at(
2211
+ self, id: int, timestamp: Optional[str] = None, silent: bool = True
2212
+ ) -> None:
2213
+ """
2214
+ Set the timestamp when embeddings were last updated for the project.
2215
+ If no timestamp is provided, uses the current UTC time.
2216
+
2217
+ :param id: Project ID
2218
+ :type id: int
2219
+ :param timestamp: ISO format timestamp (YYYY-MM-DDTHH:MM:SS.fffffZ). If None, current UTC time is used.
2220
+ :type timestamp: Optional[str]
2221
+ :param silent: Determines whether the `updatedAt` timestamp of the Project should be updated or not, if False - update `updatedAt`
2222
+ :type silent: bool
2223
+ :return: None
2224
+ :rtype: :class:`NoneType`
2225
+ :Usage example:
2226
+
2227
+ .. code-block:: python
2228
+
2229
+
2230
+ api = sly.Api.from_env()
2231
+ project_id = 123
2232
+
2233
+ # Set current time as embeddings update timestamp
2234
+ api.project.set_embeddings_updated_at(project_id)
2235
+
2236
+ # Set specific timestamp
2237
+ api.project.set_embeddings_updated_at(project_id, "2025-06-01T10:30:45.123456Z")
2238
+ """
2239
+ if timestamp is None:
2240
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
2241
+
2242
+ self._api.post(
2243
+ "projects.editInfo",
2244
+ {ApiField.ID: id, ApiField.EMBEDDINGS_UPDATED_AT: timestamp, ApiField.SILENT: silent},
2245
+ )
2246
+
2247
+ def perform_ai_search(
2248
+ self,
2249
+ project_id: int,
2250
+ dataset_id: Optional[int] = None,
2251
+ image_id: Optional[int] = None,
2252
+ prompt: Optional[str] = None,
2253
+ method: Optional[Literal["centroids", "random"]] = None,
2254
+ limit: int = 100,
2255
+ ) -> Optional[int]:
2256
+ """
2257
+ Send AI search request to initiate search process.
2258
+ This method allows you to search for similar images in a project using either a text prompt, an image ID, or a method type.
2259
+ It is mutually exclusive, meaning you can only provide one of the parameters: `prompt`, `image_id`, or `method`.
2260
+
2261
+ :param project_id: ID of the Project
2262
+ :type project_id: int
2263
+ :param dataset_id: ID of the Dataset. If not None - search will be limited to this dataset.
2264
+ :type dataset_id: Optional[int]
2265
+ :param image_id: ID of the Image. Searches for images similar to the specified image.
2266
+ :type image_id: Optional[int]
2267
+ :param prompt: Text prompt for search request. Searches for similar images based on a text description.
2268
+ :type prompt: Optional[str]
2269
+ :param method: Activates diverse search using one of the following methods: "centroids", "random".
2270
+ :type method: Optional[Literal["centroids", "random"]]
2271
+ :param limit: Limit for search request
2272
+ :type limit: int
2273
+ :return: Entitites Collection ID of the search results, or None if no collection was created.
2274
+ :rtype: Optional[int]
2275
+ :raises ValueError: only one of `prompt`, `image_id` or `method`must be provided, and `method` must be one of the allowed values.
2276
+ :Usage example:
2277
+
2278
+ .. code-block:: python
2279
+
2280
+ import supervisely as sly
2281
+
2282
+ api = sly.Api.from_env()
2283
+
2284
+ project_id = 123
2285
+ image_id = 789
2286
+ prompt = "person with a dog"
2287
+
2288
+ # Search with text prompt
2289
+ collection_id = api.project.perform_ai_search(
2290
+ project_id=project_id,
2291
+ prompt=prompt,
2292
+ )
2293
+
2294
+ # Search with method
2295
+ collection_id = api.project.perform_ai_search(
2296
+ project_id=project_id,
2297
+ method="centroids",
2298
+ )
2299
+
2300
+ # Search with image ID
2301
+ collection_id = api.project.perform_ai_search(
2302
+ project_id=project_id,
2303
+ image_id=image_id,
2304
+ )
2305
+ """
2306
+
2307
+ # Check that only one of prompt, method, or image_id is provided
2308
+ provided_params = sum([prompt is not None, method is not None, image_id is not None])
2309
+ if provided_params != 1:
2310
+ raise ValueError(
2311
+ "Must provide exactly one of 'prompt', 'method', or 'image_id' parameters. They are mutually exclusive."
2312
+ )
2313
+
2314
+ if prompt is None and method is None and image_id is None:
2315
+ raise ValueError("Must provide either 'prompt', 'method', or 'image_id' parameter.")
2316
+
2317
+ # Validate method values
2318
+ if method is not None and method not in ["centroids", "random"]:
2319
+ raise ValueError("Method must be either 'centroids' or 'random'.")
2320
+
2321
+ request_body = {
2322
+ ApiField.PROJECT_ID: project_id,
2323
+ ApiField.LIMIT: limit,
2324
+ }
2325
+
2326
+ if dataset_id is not None:
2327
+ request_body[ApiField.DATASET_ID] = dataset_id
2328
+
2329
+ if image_id is not None:
2330
+ request_body[ApiField.IMAGE_ID] = image_id
2331
+
2332
+ if prompt is not None:
2333
+ request_body[ApiField.PROMPT] = prompt
2334
+
2335
+ if method is not None:
2336
+ request_body[ApiField.METHOD] = method
2337
+
2338
+ response = self._api.post("projects.send-ai-search", request_body)
2339
+ return response.json().get(ApiField.COLLECTION_ID, None)
2340
+
2341
+ def calculate_embeddings(self, id: int) -> None:
2342
+ """
2343
+ Calculate embeddings for the project.
2344
+ This method is used to calculate embeddings for all images in the project.
2345
+
2346
+ :param id: Project ID
2347
+ :type id: int
2348
+ :return: None
2349
+ :rtype: :class:`NoneType`
2350
+ :Usage example:
2351
+
2352
+ .. code-block:: python
2353
+
2354
+ import supervisely as sly
2355
+
2356
+ api = sly.Api.from_env()
2357
+ project_id = 123
2358
+
2359
+ # Calculate embeddings for the project
2360
+ api.project.calculate_embeddings(project_id)
2361
+ """
2362
+ self._api.post("projects.calculate-project-embeddings", {ApiField.PROJECT_ID: id})
@@ -2355,11 +2355,14 @@ class VideoApi(RemoveableBulkModuleApi):
2355
2355
 
2356
2356
  def set_remote(self, videos: List[int], links: List[str]):
2357
2357
  """
2358
- This method helps to change local source to remote for videos without re-uploading them as new.
2358
+ Updates the source of existing videos by setting new remote links.
2359
+ This method is used when a video was initially uploaded as a file or added via a link,
2360
+ but later it was decided to change its location (e.g., moved to another storage or re-uploaded elsewhere).
2361
+ By updating the link, the video source can be redirected to the new location.
2359
2362
 
2360
2363
  :param videos: List of video ids.
2361
2364
  :type videos: List[int]
2362
- :param links: List of remote links.
2365
+ :param links: List of new remote links.
2363
2366
  :type links: List[str]
2364
2367
  :return: json-encoded content of a response.
2365
2368
 
@@ -2367,17 +2370,17 @@ class VideoApi(RemoveableBulkModuleApi):
2367
2370
 
2368
2371
  .. code-block:: python
2369
2372
 
2370
- import supervisely as sly
2373
+ import supervisely as sly
2371
2374
 
2372
- api = sly.Api.from_env()
2375
+ api = sly.Api.from_env()
2373
2376
 
2374
- videos = [123, 124, 125]
2375
- links = [
2376
- "s3://bucket/f1champ/ds1/lap_1.mp4",
2377
- "s3://bucket/f1champ/ds1/lap_2.mp4",
2378
- "s3://bucket/f1champ/ds1/lap_3.mp4",
2379
- ]
2380
- result = api.video.set_remote(videos, links)
2377
+ videos = [123, 124, 125]
2378
+ links = [
2379
+ "s3://bucket/f1champ/ds1/lap_1.mp4",
2380
+ "s3://bucket/f1champ/ds1/lap_2.mp4",
2381
+ "s3://bucket/f1champ/ds1/lap_3.mp4",
2382
+ ]
2383
+ result = api.video.set_remote(videos, links)
2381
2384
  """
2382
2385
 
2383
2386
  if len(videos) == 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.399
3
+ Version: 6.73.400
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely