Skip to content

Commit 761f01e

Browse files
authored
Merge pull request #763 from superannotateai/FRIDAY_3601
Add ability to donwload annotations for multimodal projects
2 parents 3522462 + 2b46008 commit 761f01e

File tree

8 files changed

+91
-19
lines changed

8 files changed

+91
-19
lines changed

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3809,6 +3809,7 @@ def download_annotations(
38093809
items: Optional[List[NotEmptyStr]] = None,
38103810
recursive: bool = False,
38113811
callback: Callable = None,
3812+
data_spec: Literal["default", "multimodal"] = "default",
38123813
):
38133814
"""Downloads annotation JSON files of the selected items to the local directory.
38143815
@@ -3834,6 +3835,31 @@ def download_annotations(
38343835
The function receives each annotation as an argument and the returned value will be applied to the download.
38353836
:type callback: callable
38363837
3838+
:param data_spec: Specifies the format for processing and transforming annotations before upload.
3839+
3840+
Options are:
3841+
- default: Retains the annotations in their original format.
3842+
- multimodal: Converts annotations for multimodal projects, optimizing for
3843+
compact and multimodal-specific data representation.
3844+
3845+
:type data_spec: str, optional
3846+
3847+
Example Usage of Multimodal Projects::
3848+
3849+
from superannotate import SAClient
3850+
3851+
3852+
sa = SAClient()
3853+
3854+
# Call the get_annotations function
3855+
response = sa.download_annotations(
3856+
project="project1/folder1",
3857+
path="path/to/download",
3858+
items=["item_1", "item_2"],
3859+
data_spec='multimodal'
3860+
)
3861+
3862+
38373863
:return: local path of the downloaded annotations folder.
38383864
:rtype: str
38393865
"""
@@ -3846,6 +3872,7 @@ def download_annotations(
38463872
recursive=recursive,
38473873
item_names=items,
38483874
callback=callback,
3875+
transform_version="llmJsonV2" if data_spec == "multimodal" else None,
38493876
)
38503877
if response.errors:
38513878
raise AppException(response.errors)

src/superannotate/lib/core/serviceproviders.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ async def download_big_annotation(
501501
download_path: str,
502502
item: entities.BaseItemEntity,
503503
callback: Callable = None,
504+
transform_version: str = None,
504505
):
505506
raise NotImplementedError
506507

@@ -513,6 +514,7 @@ async def download_small_annotations(
513514
download_path: str,
514515
item_ids: List[int],
515516
callback: Callable = None,
517+
transform_version: str = None,
516518
):
517519
raise NotImplementedError
518520

src/superannotate/lib/core/usecases/annotations.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,6 +1634,7 @@ def __init__(
16341634
item_names: List[str],
16351635
service_provider: BaseServiceProvider,
16361636
callback: Callable = None,
1637+
transform_version=None,
16371638
):
16381639
super().__init__(reporter)
16391640
self._config = config
@@ -1645,6 +1646,7 @@ def __init__(
16451646
self._service_provider = service_provider
16461647
self._callback = callback
16471648
self._big_file_queue = None
1649+
self._transform_version = transform_version
16481650

16491651
def validate_items(self):
16501652
if self._item_names:
@@ -1724,6 +1726,7 @@ async def download_small_annotations(
17241726
reporter=self.reporter,
17251727
download_path=f"{export_path}{'/' + self._folder.name if not self._folder.is_root else ''}",
17261728
callback=self._callback,
1729+
transform_version=self._transform_version,
17271730
)
17281731

17291732
async def run_workers(

src/superannotate/lib/infrastructure/controller.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,7 @@ def download(
932932
recursive: bool,
933933
item_names: Optional[List[str]],
934934
callback: Optional[Callable],
935+
transform_version: str,
935936
):
936937
use_case = usecases.DownloadAnnotations(
937938
config=self._config,
@@ -943,6 +944,7 @@ def download(
943944
item_names=item_names,
944945
service_provider=self.service_provider,
945946
callback=callback,
947+
transform_version=transform_version,
946948
)
947949
return use_case.execute()
948950

src/superannotate/lib/infrastructure/services/annotation.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ def get_schema(self, project_type: int, version: str):
6767
},
6868
)
6969

70-
async def _sync_large_annotation(self, team_id, project_id, item_id):
70+
async def _sync_large_annotation(
71+
self, team_id, project_id, item_id, transform_version: str = None
72+
):
7173
sync_params = {
7274
"team_id": team_id,
7375
"project_id": project_id,
@@ -77,6 +79,8 @@ async def _sync_large_annotation(self, team_id, project_id, item_id):
7779
"current_source": "main",
7880
"desired_source": "secondary",
7981
}
82+
if transform_version:
83+
sync_params["transform_version"] = transform_version
8084
sync_url = urljoin(
8185
self.get_assets_provider_url(),
8286
self.URL_START_FILE_SYNC.format(item_id=item_id),
@@ -120,11 +124,12 @@ async def get_big_annotation(
120124
"annotation_type": "MAIN",
121125
"version": "V1.00",
122126
}
123-
if transform_version:
124-
query_params["desired_transform_version"] = transform_version
125127

126128
await self._sync_large_annotation(
127-
team_id=project.team_id, project_id=project.id, item_id=item.id
129+
team_id=project.team_id,
130+
project_id=project.id,
131+
item_id=item.id,
132+
transform_version=transform_version,
128133
)
129134

130135
async with AIOHttpSession(
@@ -202,6 +207,7 @@ async def download_big_annotation(
202207
download_path: str,
203208
item: entities.BaseItemEntity,
204209
callback: Callable = None,
210+
transform_version: str = None,
205211
):
206212
item_id = item.id
207213
item_name = item.name
@@ -218,7 +224,10 @@ async def download_big_annotation(
218224
)
219225

220226
await self._sync_large_annotation(
221-
team_id=project.team_id, project_id=project.id, item_id=item_id
227+
team_id=project.team_id,
228+
project_id=project.id,
229+
item_id=item_id,
230+
transform_version=transform_version,
222231
)
223232

224233
async with AIOHttpSession(
@@ -252,12 +261,15 @@ async def download_small_annotations(
252261
download_path: str,
253262
item_ids: List[int],
254263
callback: Callable = None,
264+
transform_version: str = None,
255265
):
256266
query_params = {
257267
"team_id": project.team_id,
258268
"project_id": project.id,
259269
"folder_id": folder.id,
260270
}
271+
if transform_version:
272+
query_params["transform_version"] = transform_version
261273
handler = StreamedAnnotations(
262274
headers=self.client.default_headers,
263275
reporter=reporter,

tests/integration/annotations/test_upload_annotations.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import os
3+
import tempfile
34
import time
45
from pathlib import Path
56

@@ -133,23 +134,19 @@ def test_upload_large_annotations(self):
133134
) == 5
134135

135136

136-
class MultiModalUploadAnnotations(BaseTestCase):
137+
class MultiModalUploadDownloadAnnotations(BaseTestCase):
137138
PROJECT_NAME = "TestMultimodalUploadAnnotations"
138139
PROJECT_TYPE = "Multimodal"
139140
PROJECT_DESCRIPTION = "DESCRIPTION"
140-
EDITOR_TEMPLATE_PATH = os.path.join(
141-
Path(__file__).parent.parent.parent, "data_set/editor_templates/form1.json"
142-
)
143-
JSONL_ANNOTATIONS_PATH = os.path.join(
144-
DATA_SET_PATH, "multimodal/annotations/jsonl/form1.jsonl"
145-
)
146-
JSONL_ANNOTATIONS_WITH_CATEGORIES_PATH = os.path.join(
147-
DATA_SET_PATH, "multimodal/annotations/jsonl/form1_with_categories.jsonl"
148-
)
149-
CLASSES_TEMPLATE_PATH = os.path.join(
150-
Path(__file__).parent.parent.parent,
151-
"data_set/editor_templates/from1_classes.json",
141+
BASE_PATH = Path(__file__).parent.parent.parent
142+
DATA_SET_PATH = BASE_PATH / "data_set"
143+
144+
EDITOR_TEMPLATE_PATH = DATA_SET_PATH / "editor_templates/form1.json"
145+
JSONL_ANNOTATIONS_PATH = DATA_SET_PATH / "multimodal/annotations/jsonl/form1.jsonl"
146+
JSONL_ANNOTATIONS_WITH_CATEGORIES_PATH = (
147+
DATA_SET_PATH / "multimodal/annotations/jsonl/form1_with_categories.jsonl"
152148
)
149+
CLASSES_TEMPLATE_PATH = DATA_SET_PATH / "editor_templates" / "form1_classes.json"
153150

154151
def setUp(self, *args, **kwargs):
155152
self.tearDown()
@@ -244,3 +241,32 @@ def test_upload_with_integer_names(self):
244241
sa.get_annotations(
245242
f"{self.PROJECT_NAME}/test_folder", data_spec="multimodal"
246243
)
244+
245+
def test_download_annotations(self):
246+
with open(self.JSONL_ANNOTATIONS_PATH) as f:
247+
data = [json.loads(line) for line in f]
248+
sa.upload_annotations(
249+
self.PROJECT_NAME, annotations=data, data_spec="multimodal"
250+
)
251+
252+
annotations = sa.get_annotations(
253+
f"{self.PROJECT_NAME}/test_folder", data_spec="multimodal"
254+
)
255+
256+
with tempfile.TemporaryDirectory() as tmpdir:
257+
sa.download_annotations(
258+
f"{self.PROJECT_NAME}/test_folder", path=tmpdir, data_spec="multimodal"
259+
)
260+
downloaded_files = list(Path(f"{tmpdir}/test_folder").glob("*.json"))
261+
assert len(downloaded_files) > 0, "No annotations were downloaded"
262+
downloaded_data = []
263+
for file_path in downloaded_files:
264+
with open(file_path) as f:
265+
downloaded_data.append(json.load(f))
266+
267+
assert len(downloaded_data) == len(
268+
annotations
269+
), "Mismatch in annotation count"
270+
assert (
271+
downloaded_data == annotations
272+
), "Downloaded annotations do not match uploaded annotations"

tests/integration/items/test_item_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class TestMultimodalProjectBasic(BaseTestCase):
1919
)
2020
CLASSES_TEMPLATE_PATH = os.path.join(
2121
Path(__file__).parent.parent.parent,
22-
"data_set/editor_templates/from1_classes.json",
22+
"data_set/editor_templates/form1_classes.json",
2323
)
2424

2525
def setUp(self, *args, **kwargs):

0 commit comments

Comments
 (0)