supervisely 6.73.221__py3-none-any.whl → 6.73.222__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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

@@ -1,6 +1,8 @@
1
+ import asyncio
1
2
  import os
2
- from typing import Callable, List, NamedTuple, Optional, Union
3
+ from typing import AsyncGenerator, Callable, List, NamedTuple, Optional, Union
3
4
 
5
+ import aiofiles
4
6
  from tqdm import tqdm
5
7
  from tqdm.contrib.logging import logging_redirect_tqdm
6
8
 
@@ -19,6 +21,7 @@ from supervisely.io.fs import (
19
21
  ensure_base_path,
20
22
  get_bytes_hash,
21
23
  get_file_ext,
24
+ get_file_hash_async,
22
25
  get_file_name,
23
26
  get_file_name_with_ext,
24
27
  )
@@ -852,7 +855,7 @@ class VolumeApi(RemoveableBulkModuleApi):
852
855
 
853
856
  def _download(self, id: int, is_stream: bool = False):
854
857
  """
855
- Private method for volume volume downloading.
858
+ Private method for volume downloading.
856
859
 
857
860
  :param id: Volume ID in Supervisely.
858
861
  :type id: int
@@ -1236,3 +1239,221 @@ class VolumeApi(RemoveableBulkModuleApi):
1236
1239
  )
1237
1240
  )
1238
1241
  return volume_infos
1242
+
1243
+ async def _download_async(
1244
+ self,
1245
+ id: int,
1246
+ is_stream: bool = False,
1247
+ range_start: Optional[int] = None,
1248
+ range_end: Optional[int] = None,
1249
+ headers: Optional[dict] = None,
1250
+ chunk_size: int = 1024 * 1024,
1251
+ ) -> AsyncGenerator:
1252
+ """
1253
+ Download Volume with given ID asynchronously.
1254
+ If is_stream is True, returns stream of bytes, otherwise returns response object.
1255
+ For streaming, returns tuple of chunk and hash.
1256
+
1257
+ :param id: Volume ID in Supervisely.
1258
+ :type id: int
1259
+ :param is_stream: If True, returns stream of bytes, otherwise returns response object.
1260
+ :type is_stream: bool, optional
1261
+ :param range_start: Start byte of range for partial download.
1262
+ :type range_start: int, optional
1263
+ :param range_end: End byte of range for partial download.
1264
+ :type range_end: int, optional
1265
+ :param headers: Headers for request.
1266
+ :type headers: dict, optional
1267
+ :param chunk_size: Size of chunk for downloading. Default is 1MB.
1268
+ :type chunk_size: int, optional
1269
+ :return: Stream of bytes or response object.
1270
+ :rtype: AsyncGenerator
1271
+ """
1272
+ api_method_name = "volumes.download"
1273
+
1274
+ json_body = {ApiField.ID: id}
1275
+
1276
+ if is_stream:
1277
+ async for chunk, hhash in self._api.stream_async(
1278
+ api_method_name,
1279
+ "POST",
1280
+ json_body,
1281
+ headers=headers,
1282
+ range_start=range_start,
1283
+ range_end=range_end,
1284
+ chunk_size=chunk_size,
1285
+ ):
1286
+ yield chunk, hhash
1287
+ else:
1288
+ response = await self._api.post_async(api_method_name, json_body, headers=headers)
1289
+ yield response
1290
+
1291
+ async def download_path_async(
1292
+ self,
1293
+ id: int,
1294
+ path: str,
1295
+ semaphore: Optional[asyncio.Semaphore] = None,
1296
+ range_start: Optional[int] = None,
1297
+ range_end: Optional[int] = None,
1298
+ headers: Optional[dict] = None,
1299
+ chunk_size: int = 1024 * 1024,
1300
+ check_hash: bool = True,
1301
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1302
+ progress_cb_type: Literal["number", "size"] = "number",
1303
+ ) -> None:
1304
+ """
1305
+ Downloads Volume with given ID to local path.
1306
+
1307
+ :param id: Volume ID in Supervisely.
1308
+ :type id: int
1309
+ :param path: Local save path for Volume.
1310
+ :type path: str
1311
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1312
+ :type semaphore: :class:`asyncio.Semaphore`, optional
1313
+ :param range_start: Start byte of range for partial download.
1314
+ :type range_start: int, optional
1315
+ :param range_end: End byte of range for partial download.
1316
+ :type range_end: int, optional
1317
+ :param headers: Headers for request.
1318
+ :type headers: dict, optional
1319
+ :param chunk_size: Size of chunk for downloading. Default is 1MB.
1320
+ :type chunk_size: int, optional
1321
+ :param check_hash: If True, checks hash of downloaded file.
1322
+ Check is not supported for partial downloads.
1323
+ When range is set, hash check is disabled.
1324
+ :type check_hash: bool, optional
1325
+ :param progress_cb: Function for tracking download progress.
1326
+ :type progress_cb: tqdm or callable, optional
1327
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
1328
+ :type progress_cb_type: str, optional
1329
+ :return: None
1330
+ :rtype: :class:`NoneType`
1331
+ :Usage example:
1332
+
1333
+ .. code-block:: python
1334
+
1335
+ import supervisely as sly
1336
+ import asyncio
1337
+
1338
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1339
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1340
+ api = sly.Api.from_env()
1341
+
1342
+ volume_info = api.volume.get_info_by_id(770918)
1343
+ save_path = os.path.join("/path/to/save/", volume_info.name)
1344
+
1345
+ semaphore = asyncio.Semaphore(100)
1346
+ loop = asyncio.new_event_loop()
1347
+ asyncio.set_event_loop(loop)
1348
+ loop.run_until_complete(
1349
+ api.volume.download_path_async(volume_info.id, save_path, semaphore)
1350
+ )
1351
+ """
1352
+
1353
+ if range_start is not None or range_end is not None:
1354
+ check_hash = False # Hash check is not supported for partial downloads
1355
+ headers = headers or {}
1356
+ headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
1357
+ logger.debug(f"Image ID: {id}. Setting Range header: {headers['Range']}")
1358
+
1359
+ writing_method = "ab" if range_start not in [0, None] else "wb"
1360
+
1361
+ ensure_base_path(path)
1362
+ hash_to_check = None
1363
+ if semaphore is None:
1364
+ semaphore = self._api._get_default_semaphore()
1365
+ async with semaphore:
1366
+ async with aiofiles.open(path, writing_method) as fd:
1367
+ async for chunk, hhash in self._download_async(
1368
+ id,
1369
+ is_stream=True,
1370
+ headers=headers,
1371
+ range_start=range_start,
1372
+ range_end=range_end,
1373
+ chunk_size=chunk_size,
1374
+ ):
1375
+ await fd.write(chunk)
1376
+ hash_to_check = hhash
1377
+ if progress_cb is not None and progress_cb_type == "size":
1378
+ progress_cb(len(chunk))
1379
+ if check_hash:
1380
+ if hash_to_check is not None:
1381
+ downloaded_file_hash = await get_file_hash_async(path)
1382
+ if hash_to_check != downloaded_file_hash:
1383
+ raise RuntimeError(
1384
+ f"Downloaded hash of volume with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"
1385
+ )
1386
+ if progress_cb is not None and progress_cb_type == "number":
1387
+ progress_cb(1)
1388
+
1389
+ async def download_paths_async(
1390
+ self,
1391
+ ids: List[int],
1392
+ paths: List[str],
1393
+ semaphore: Optional[asyncio.Semaphore] = None,
1394
+ headers: Optional[dict] = None,
1395
+ chunk_size: int = 1024 * 1024,
1396
+ check_hash: bool = True,
1397
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1398
+ progress_cb_type: Literal["number", "size"] = "number",
1399
+ ) -> None:
1400
+ """
1401
+ Download Volumes with given IDs and saves them to given local paths asynchronously.
1402
+
1403
+ :param ids: List of Volume IDs in Supervisely.
1404
+ :type ids: :class:`List[int]`
1405
+ :param paths: Local save paths for Volumes.
1406
+ :type paths: :class:`List[str]`
1407
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1408
+ :type semaphore: :class:`asyncio.Semaphore`, optional
1409
+ :param headers: Headers for request.
1410
+ :type headers: dict, optional
1411
+ :param chunk_size: Size of chunk for downloading. Default is 1MB.
1412
+ :type chunk_size: int, optional
1413
+ :param check_hash: If True, checks hash of downloaded file.
1414
+ :type check_hash: bool, optional
1415
+ :param progress_cb: Function for tracking download progress.
1416
+ :type progress_cb: tqdm or callable, optional
1417
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
1418
+ :type progress_cb_type: str, optional
1419
+ :raises: :class:`ValueError` if len(ids) != len(paths)
1420
+ :return: None
1421
+ :rtype: :class:`NoneType`
1422
+
1423
+ :Usage example:
1424
+
1425
+ .. code-block:: python
1426
+
1427
+ import supervisely as sly
1428
+ import asyncio
1429
+
1430
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1431
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1432
+ api = sly.Api.from_env()
1433
+
1434
+ ids = [770914, 770915]
1435
+ paths = ["/path/to/save/volume1.nrrd", "/path/to/save/volume2.nrrd"]
1436
+ loop = asyncio.new_event_loop()
1437
+ asyncio.set_event_loop(loop)
1438
+ loop.run_until_complete(api.volume.download_paths_async(ids, paths))
1439
+ """
1440
+ if len(ids) == 0:
1441
+ return
1442
+ if len(ids) != len(paths):
1443
+ raise ValueError(f'Can not match "ids" and "paths" lists, {len(ids)} != {len(paths)}')
1444
+ if semaphore is None:
1445
+ semaphore = self._api._get_default_semaphore()
1446
+ tasks = []
1447
+ for img_id, img_path in zip(ids, paths):
1448
+ task = self.download_path_async(
1449
+ img_id,
1450
+ img_path,
1451
+ semaphore,
1452
+ headers=headers,
1453
+ chunk_size=chunk_size,
1454
+ check_hash=check_hash,
1455
+ progress_cb=progress_cb,
1456
+ progress_cb_type=progress_cb_type,
1457
+ )
1458
+ tasks.append(task)
1459
+ await asyncio.gather(*tasks)
@@ -5,11 +5,12 @@ from typing import Dict, List, Optional, Tuple, Union
5
5
 
6
6
  from tqdm import tqdm
7
7
 
8
- from supervisely._utils import is_production
8
+ from supervisely._utils import batched, is_production
9
9
  from supervisely.annotation.annotation import Annotation
10
10
  from supervisely.annotation.tag_meta import TagValueType
11
11
  from supervisely.api.api import Api
12
- from supervisely.io.fs import get_file_ext, get_file_name_with_ext
12
+ from supervisely.io.env import team_id
13
+ from supervisely.io.fs import get_file_ext, get_file_name_with_ext, silent_remove
13
14
  from supervisely.project.project_meta import ProjectMeta
14
15
  from supervisely.project.project_settings import LabelingInterface
15
16
  from supervisely.sly_logger import logger
@@ -145,12 +146,16 @@ class BaseConverter:
145
146
  remote_files_map: Optional[Dict[str, str]] = None,
146
147
  ):
147
148
  self._input_data: str = input_data
148
- self._items: List[self.BaseItem] = []
149
+ self._items: List[BaseConverter.BaseItem] = []
149
150
  self._meta: ProjectMeta = None
150
151
  self._labeling_interface = labeling_interface or LabelingInterface.DEFAULT
152
+
153
+ # import as links settings
151
154
  self._upload_as_links: bool = upload_as_links
152
155
  self._remote_files_map: Optional[Dict[str, str]] = remote_files_map
153
156
  self._supports_links = False # if converter supports uploading by links
157
+ self._api = Api.from_env() if self._upload_as_links else None
158
+ self._team_id = team_id() if self._upload_as_links else None
154
159
  self._converter = None
155
160
 
156
161
  if self._labeling_interface not in LabelingInterface.values():
@@ -419,3 +424,39 @@ class BaseConverter:
419
424
 
420
425
  return meta1.clone(project_settings=new_settings)
421
426
  return meta1
427
+
428
+ def _download_remote_ann_files(self) -> None:
429
+ """
430
+ Download all annotation files from Cloud Storage to the local storage.
431
+ Needed to detect annotation format if "upload_as_links" is enabled.
432
+ """
433
+ if not self.upload_as_links:
434
+ return
435
+
436
+ files_to_download = {
437
+ l: r for l, r in self._remote_files_map.items() if get_file_ext(l) == self.ann_ext
438
+ }
439
+ if not files_to_download:
440
+ return
441
+
442
+ import asyncio
443
+
444
+ loop = asyncio.get_event_loop()
445
+ _, progress_cb = self.get_progress(
446
+ len(files_to_download),
447
+ "Downloading annotation files from remote storage",
448
+ )
449
+
450
+ for local_path in files_to_download.keys():
451
+ silent_remove(local_path)
452
+
453
+ logger.info("Downloading annotation files from remote storage...")
454
+ loop.run_until_complete(
455
+ self._api.storage.download_bulk_async(
456
+ team_id=self._team_id,
457
+ remote_paths=list(files_to_download.values()),
458
+ local_save_paths=list(files_to_download.keys()),
459
+ progress_cb=progress_cb,
460
+ )
461
+ )
462
+ logger.info("Annotation files downloaded successfully")
@@ -1,15 +1,16 @@
1
1
  import os
2
2
  from pathlib import Path
3
+ from typing import Optional
3
4
 
4
5
  from tqdm import tqdm
5
6
 
6
- from typing import Literal, Optional
7
-
8
7
  from supervisely._utils import is_production
9
8
  from supervisely.api.api import Api
10
9
  from supervisely.app import get_data_dir
11
10
  from supervisely.convert.image.csv.csv_converter import CSVConverter
12
- from supervisely.convert.image.high_color.high_color_depth import HighColorDepthImageConverter
11
+ from supervisely.convert.image.high_color.high_color_depth import (
12
+ HighColorDepthImageConverter,
13
+ )
13
14
  from supervisely.convert.image.image_converter import ImageConverter
14
15
  from supervisely.convert.pointcloud.pointcloud_converter import PointcloudConverter
15
16
  from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import (
@@ -28,10 +29,10 @@ from supervisely.io.fs import (
28
29
  touch,
29
30
  unpack_archive,
30
31
  )
32
+ from supervisely.project.project_settings import LabelingInterface
31
33
  from supervisely.project.project_type import ProjectType
32
34
  from supervisely.sly_logger import logger
33
35
  from supervisely.task.progress import Progress
34
- from supervisely.project.project_settings import LabelingInterface
35
36
 
36
37
 
37
38
  class ImportManager:
@@ -164,7 +165,7 @@ class ImportManager:
164
165
  dir_path = remote_path.rstrip("/") if is_dir else os.path.dirname(remote_path)
165
166
  dir_name = os.path.basename(dir_path)
166
167
 
167
- local_path = os.path.join(get_data_dir(), dir_name)
168
+ local_path = os.path.abspath(os.path.join(get_data_dir(), dir_name))
168
169
  mkdir(local_path, remove_content_if_exists=True)
169
170
 
170
171
  if is_dir:
@@ -137,8 +137,8 @@ class ImageConverter(BaseConverter):
137
137
  if item.path is None:
138
138
  continue # image has failed validation
139
139
  item.name = f"{get_file_name(item.path)}{get_file_ext(item.path).lower()}"
140
- if self.upload_as_links:
141
- ann = None # TODO: implement
140
+ if self.upload_as_links and not self.supports_links:
141
+ ann = None
142
142
  else:
143
143
  ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
144
144
  name = generate_free_name(
@@ -160,19 +160,29 @@ class ImageConverter(BaseConverter):
160
160
  with ApiContext(
161
161
  api=api, project_id=project_id, dataset_id=dataset_id, project_meta=meta
162
162
  ):
163
- upload_method = (
164
- api.image.upload_links if self.upload_as_links else api.image.upload_paths
165
- )
166
- img_infos = upload_method(
167
- dataset_id,
168
- item_names,
169
- item_paths,
170
- metas=item_metas,
171
- conflict_resolution="rename",
172
- )
163
+ if self.upload_as_links:
164
+ img_infos = api.image.upload_links(
165
+ dataset_id,
166
+ item_names,
167
+ item_paths,
168
+ metas=item_metas,
169
+ conflict_resolution="rename",
170
+ force_metadata_for_links=False,
171
+ )
172
+ else:
173
+ img_infos = api.image.upload_paths(
174
+ dataset_id,
175
+ item_names,
176
+ item_paths,
177
+ metas=item_metas,
178
+ conflict_resolution="rename",
179
+ )
180
+
173
181
  img_ids = [img_info.id for img_info in img_infos]
174
182
  if len(anns) == len(img_ids):
175
- api.annotation.upload_anns(img_ids, anns)
183
+ api.annotation.upload_anns(
184
+ img_ids, anns, skip_bounds_validation=self.upload_as_links
185
+ )
176
186
 
177
187
  if log_progress:
178
188
  progress_cb(len(batch))
@@ -188,6 +198,9 @@ class ImageConverter(BaseConverter):
188
198
  return image_helper.validate_image(path)
189
199
 
190
200
  def is_image(self, path: str) -> bool:
201
+ if self._upload_as_links and self.supports_links:
202
+ ext = get_file_ext(path)
203
+ return ext.lower() in self.allowed_exts
191
204
  mimetypes.add_type("image/heic", ".heic") # to extend types_map
192
205
  mimetypes.add_type("image/heif", ".heif") # to extend types_map
193
206
  mimetypes.add_type("image/jpeg", ".jfif") # to extend types_map
@@ -20,6 +20,10 @@ from supervisely.convert.image.image_helper import validate_image_bounds
20
20
 
21
21
  class FastSlyImageConverter(SLYImageConverter, ImageConverter):
22
22
 
23
+ def __init__(self, *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+ self._supports_links = False
26
+
23
27
  def validate_format(self) -> bool:
24
28
 
25
29
  detected_ann_cnt = 0
@@ -2,21 +2,21 @@ import os
2
2
  from typing import Dict, Optional
3
3
 
4
4
  import supervisely.convert.image.sly.sly_image_helper as sly_image_helper
5
- from supervisely.convert.image.image_helper import validate_image_bounds
6
5
  from supervisely import (
7
6
  Annotation,
8
7
  Dataset,
8
+ Label,
9
9
  OpenMode,
10
10
  Project,
11
11
  ProjectMeta,
12
12
  Rectangle,
13
- Label,
14
13
  logger,
15
14
  )
16
15
  from supervisely._utils import generate_free_name
17
16
  from supervisely.api.api import Api
18
17
  from supervisely.convert.base_converter import AvailableImageConverters
19
18
  from supervisely.convert.image.image_converter import ImageConverter
19
+ from supervisely.convert.image.image_helper import validate_image_bounds
20
20
  from supervisely.io.fs import dirs_filter, file_exists, get_file_ext
21
21
  from supervisely.io.json import load_json_file
22
22
  from supervisely.project.project import find_project_dirs
@@ -31,6 +31,7 @@ class SLYImageConverter(ImageConverter):
31
31
  def __init__(self, *args, **kwargs):
32
32
  super().__init__(*args, **kwargs)
33
33
  self._project_structure = None
34
+ self._supports_links = True
34
35
 
35
36
  def __str__(self):
36
37
  return AvailableImageConverters.SLY
@@ -74,6 +75,8 @@ class SLYImageConverter(ImageConverter):
74
75
  return False
75
76
 
76
77
  def validate_format(self) -> bool:
78
+ if self.upload_as_links and self._supports_links:
79
+ self._download_remote_ann_files()
77
80
  if self.read_sly_project(self._input_data):
78
81
  return True
79
82
 
@@ -136,6 +139,8 @@ class SLYImageConverter(ImageConverter):
136
139
  meta = self._meta
137
140
 
138
141
  if item.ann_data is None:
142
+ if self._upload_as_links:
143
+ item.set_shape([None, None])
139
144
  return item.create_empty_annotation()
140
145
 
141
146
  try:
@@ -151,7 +156,7 @@ class SLYImageConverter(ImageConverter):
151
156
  )
152
157
  return Annotation.from_json(ann_json, meta).clone(labels=labels)
153
158
  except Exception as e:
154
- logger.warn(f"Failed to convert annotation: {repr(e)}")
159
+ logger.warning(f"Failed to convert annotation: {repr(e)}")
155
160
  return item.create_empty_annotation()
156
161
 
157
162
  def read_sly_project(self, input_data: str) -> bool:
@@ -163,7 +168,7 @@ class SLYImageConverter(ImageConverter):
163
168
  logger.debug("Trying to find Supervisely project format in the input data")
164
169
  project_dirs = [d for d in find_project_dirs(input_data)]
165
170
  if len(project_dirs) > 1:
166
- logger.info("Found multiple Supervisely projects")
171
+ logger.info("Found multiple possible Supervisely projects in the input data")
167
172
  meta = None
168
173
  for project_dir in project_dirs:
169
174
  project_fs = Project(project_dir, mode=OpenMode.READ)
@@ -12,6 +12,10 @@ from supervisely.video.video import validate_ext as validate_video_ext
12
12
 
13
13
  class SLYVideoConverter(VideoConverter):
14
14
 
15
+ def __init__(self, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ self._supports_links = True
18
+
15
19
  def __str__(self) -> str:
16
20
  return AvailableVideoConverters.SLY
17
21
 
@@ -45,6 +49,8 @@ class SLYVideoConverter(VideoConverter):
45
49
  return False
46
50
 
47
51
  def validate_format(self) -> bool:
52
+ if self.upload_as_links and self._supports_links:
53
+ self._download_remote_ann_files()
48
54
  detected_ann_cnt = 0
49
55
  videos_list, ann_dict = [], {}
50
56
  for root, _, files in os.walk(self._input_data):
@@ -103,6 +109,8 @@ class SLYVideoConverter(VideoConverter):
103
109
  meta = self._meta
104
110
 
105
111
  if item.ann_data is None:
112
+ if self._upload_as_links:
113
+ return None
106
114
  return item.create_empty_annotation()
107
115
 
108
116
  try:
@@ -113,5 +121,5 @@ class SLYVideoConverter(VideoConverter):
113
121
  ann_json = sly_video_helper.rename_in_json(ann_json, renamed_classes, renamed_tags)
114
122
  return VideoAnnotation.from_json(ann_json, meta)
115
123
  except Exception as e:
116
- logger.warn(f"Failed to convert annotation: {repr(e)}")
124
+ logger.warning(f"Failed to convert annotation: {repr(e)}")
117
125
  return item.create_empty_annotation()
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import subprocess
3
- from typing import Dict, Optional, Union
3
+ from typing import Dict, Optional, Tuple, Union
4
4
 
5
5
  import cv2
6
6
  import magic
@@ -57,10 +57,22 @@ class VideoConverter(BaseConverter):
57
57
  self._frame_count = frame_count
58
58
  self._custom_data = custom_data if custom_data is not None else {}
59
59
 
60
+ @property
61
+ def shape(self) -> Tuple[int, int]:
62
+ return self._shape
63
+
64
+ @shape.setter
65
+ def shape(self, shape: Optional[Tuple[int, int]] = None):
66
+ self._shape = shape if shape is not None else [None, None]
67
+
60
68
  @property
61
69
  def frame_count(self) -> int:
62
70
  return self._frame_count
63
71
 
72
+ @frame_count.setter
73
+ def frame_count(self, frame_count: int):
74
+ self._frame_count = frame_count
75
+
64
76
  @property
65
77
  def name(self) -> str:
66
78
  if self._name is not None:
@@ -75,11 +87,11 @@ class VideoConverter(BaseConverter):
75
87
  return VideoAnnotation(self._shape, self._frame_count)
76
88
 
77
89
  def __init__(
78
- self,
79
- input_data: str,
80
- labeling_interface: Optional[Union[LabelingInterface, str]],
81
- upload_as_links: bool,
82
- remote_files_map: Optional[Dict[str, str]] = None,
90
+ self,
91
+ input_data: str,
92
+ labeling_interface: Optional[Union[LabelingInterface, str]],
93
+ upload_as_links: bool,
94
+ remote_files_map: Optional[Dict[str, str]] = None,
83
95
  ):
84
96
  super().__init__(input_data, labeling_interface, upload_as_links, remote_files_map)
85
97
  self._key_id_map: KeyIdMap = None
@@ -114,7 +126,9 @@ class VideoConverter(BaseConverter):
114
126
  existing_names = set([vid.name for vid in api.video.get_list(dataset_id)])
115
127
 
116
128
  # check video codecs, mimetypes and convert if needed
117
- convert_progress, convert_progress_cb = self.get_progress(self.items_count, "Preparing videos...")
129
+ convert_progress, convert_progress_cb = self.get_progress(
130
+ self.items_count, "Preparing videos..."
131
+ )
118
132
  for item in self._items:
119
133
  item_name, item_path = self.convert_to_mp4_if_needed(item.path)
120
134
  item.name = item_name
@@ -124,11 +138,14 @@ class VideoConverter(BaseConverter):
124
138
  convert_progress.close()
125
139
 
126
140
  has_large_files = False
141
+ size_progress_cb = None
127
142
  progress_cb, progress, ann_progress, ann_progress_cb = None, None, None, None
128
143
  if log_progress and not self.upload_as_links:
129
144
  progress, progress_cb = self.get_progress(self.items_count, "Uploading videos...")
130
145
  file_sizes = [get_file_size(item.path) for item in self._items]
131
- has_large_files = any([self._check_video_file_size(file_size) for file_size in file_sizes])
146
+ has_large_files = any(
147
+ [self._check_video_file_size(file_size) for file_size in file_sizes]
148
+ )
132
149
  if has_large_files:
133
150
  upload_progress = []
134
151
  size_progress_cb = self._get_video_upload_progress(upload_progress)
@@ -146,17 +163,19 @@ class VideoConverter(BaseConverter):
146
163
  item_paths.append(item.path)
147
164
  item_names.append(item.name)
148
165
 
149
- if not self.upload_as_links:
150
- # TODO: implement generating annotations for remote videos
166
+ ann = None
167
+ if not self.upload_as_links or self.supports_links:
151
168
  ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
152
- figures_cnt += len(ann.figures)
153
- anns.append(ann)
169
+ if ann is not None:
170
+ figures_cnt += len(ann.figures)
171
+ anns.append(ann)
154
172
 
155
173
  if self.upload_as_links:
156
174
  vid_infos = api.video.upload_links(
157
175
  dataset_id,
158
176
  item_paths,
159
177
  item_names,
178
+ skip_download=True,
160
179
  )
161
180
  else:
162
181
  vid_infos = api.video.upload_paths(
@@ -164,22 +183,24 @@ class VideoConverter(BaseConverter):
164
183
  item_names,
165
184
  item_paths,
166
185
  progress_cb=progress_cb if log_progress else None,
167
- item_progress=size_progress_cb if log_progress and has_large_files else None, # pylint: disable=used-before-assignment
186
+ item_progress=(size_progress_cb if log_progress and has_large_files else None),
168
187
  )
169
- vid_ids = [vid_info.id for vid_info in vid_infos]
188
+ vid_ids = [vid_info.id for vid_info in vid_infos]
170
189
 
171
- if log_progress and has_large_files and figures_cnt > 0:
172
- ann_progress, ann_progress_cb = self.get_progress(figures_cnt, "Uploading annotations...")
190
+ if log_progress and has_large_files and figures_cnt > 0:
191
+ ann_progress, ann_progress_cb = self.get_progress(
192
+ figures_cnt, "Uploading annotations..."
193
+ )
173
194
 
174
- for video_id, ann in zip(vid_ids, anns):
175
- if ann is None:
176
- ann = VideoAnnotation(item.shape, item.frame_count)
177
- api.video.annotation.append(video_id, ann, progress_cb=ann_progress_cb)
195
+ for vid, ann, item, info in zip(vid_ids, anns, batch, vid_infos):
196
+ if ann is None:
197
+ ann = VideoAnnotation((info.frame_height, info.frame_width), info.frames_count)
198
+ api.video.annotation.append(vid, ann, progress_cb=ann_progress_cb)
178
199
 
179
200
  if log_progress and is_development():
180
- if progress is not None: # pylint: disable=possibly-used-before-assignment
201
+ if progress is not None:
181
202
  progress.close()
182
- if not self.upload_as_links and ann_progress is not None:
203
+ if ann_progress is not None:
183
204
  ann_progress.close()
184
205
  logger.info(f"Dataset ID:{dataset_id} has been successfully uploaded.")
185
206
 
@@ -268,7 +289,7 @@ class VideoConverter(BaseConverter):
268
289
  )
269
290
 
270
291
  def _check_video_file_size(self, file_size):
271
- return file_size > 20 * 1024 * 1024 # 20 MB
292
+ return file_size > 20 * 1024 * 1024 # 20 MB
272
293
 
273
294
  def _get_video_upload_progress(self, upload_progress):
274
295
  upload_progress = []