synapse-sdk 1.0.0b5__py3-none-any.whl → 2025.12.3__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 (167) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/code_server.py +305 -33
  3. synapse_sdk/clients/agent/__init__.py +2 -1
  4. synapse_sdk/clients/agent/container.py +143 -0
  5. synapse_sdk/clients/agent/ray.py +296 -38
  6. synapse_sdk/clients/backend/annotation.py +1 -1
  7. synapse_sdk/clients/backend/core.py +31 -4
  8. synapse_sdk/clients/backend/data_collection.py +82 -7
  9. synapse_sdk/clients/backend/hitl.py +1 -1
  10. synapse_sdk/clients/backend/ml.py +1 -1
  11. synapse_sdk/clients/base.py +211 -61
  12. synapse_sdk/loggers.py +46 -0
  13. synapse_sdk/plugins/README.md +1340 -0
  14. synapse_sdk/plugins/categories/base.py +59 -9
  15. synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
  16. synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
  17. synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
  18. synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
  19. synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
  20. synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
  21. synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
  22. synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
  23. synapse_sdk/plugins/categories/export/templates/config.yaml +19 -1
  24. synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
  25. synapse_sdk/plugins/categories/export/templates/plugin/export.py +153 -177
  26. synapse_sdk/plugins/categories/neural_net/actions/train.py +1130 -32
  27. synapse_sdk/plugins/categories/neural_net/actions/tune.py +157 -4
  28. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +7 -4
  29. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  30. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  31. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  32. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  33. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
  34. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  35. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
  39. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  40. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  41. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
  42. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  43. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  44. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
  45. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
  46. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  47. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
  48. synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
  49. synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
  50. synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
  51. synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
  52. synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
  53. synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
  54. synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
  55. synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
  56. synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
  57. synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
  58. synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
  59. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
  60. synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
  61. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
  62. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
  63. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
  64. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
  65. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
  66. synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
  67. synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
  68. synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
  69. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
  70. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
  71. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
  72. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
  73. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
  74. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
  75. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
  76. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
  77. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
  78. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
  79. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
  80. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
  81. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
  82. synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
  83. synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
  84. synapse_sdk/plugins/categories/upload/templates/config.yaml +28 -2
  85. synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
  86. synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +82 -20
  87. synapse_sdk/plugins/models.py +111 -9
  88. synapse_sdk/plugins/templates/plugin-config-schema.json +7 -0
  89. synapse_sdk/plugins/templates/schema.json +7 -0
  90. synapse_sdk/plugins/utils/__init__.py +3 -0
  91. synapse_sdk/plugins/utils/ray_gcs.py +66 -0
  92. synapse_sdk/shared/__init__.py +25 -0
  93. synapse_sdk/utils/converters/dm/__init__.py +42 -41
  94. synapse_sdk/utils/converters/dm/base.py +137 -0
  95. synapse_sdk/utils/converters/dm/from_v1.py +208 -562
  96. synapse_sdk/utils/converters/dm/to_v1.py +258 -304
  97. synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
  98. synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
  99. synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
  100. synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
  101. synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
  102. synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
  103. synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
  104. synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
  105. synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
  106. synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
  107. synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
  108. synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
  109. synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
  110. synapse_sdk/utils/converters/dm/types.py +168 -0
  111. synapse_sdk/utils/converters/dm/utils.py +162 -0
  112. synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
  113. synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
  114. synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
  115. synapse_sdk/utils/file/__init__.py +58 -0
  116. synapse_sdk/utils/file/archive.py +32 -0
  117. synapse_sdk/utils/file/checksum.py +56 -0
  118. synapse_sdk/utils/file/chunking.py +31 -0
  119. synapse_sdk/utils/file/download.py +385 -0
  120. synapse_sdk/utils/file/encoding.py +40 -0
  121. synapse_sdk/utils/file/io.py +22 -0
  122. synapse_sdk/utils/file/upload.py +165 -0
  123. synapse_sdk/utils/file/video/__init__.py +29 -0
  124. synapse_sdk/utils/file/video/transcode.py +307 -0
  125. synapse_sdk/utils/{file.py → file.py.backup} +77 -0
  126. synapse_sdk/utils/network.py +272 -0
  127. synapse_sdk/utils/storage/__init__.py +6 -2
  128. synapse_sdk/utils/storage/providers/file_system.py +6 -0
  129. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/METADATA +19 -2
  130. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/RECORD +134 -74
  131. synapse_sdk/devtools/docs/.gitignore +0 -20
  132. synapse_sdk/devtools/docs/README.md +0 -41
  133. synapse_sdk/devtools/docs/blog/2019-05-28-first-blog-post.md +0 -12
  134. synapse_sdk/devtools/docs/blog/2019-05-29-long-blog-post.md +0 -44
  135. synapse_sdk/devtools/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
  136. synapse_sdk/devtools/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  137. synapse_sdk/devtools/docs/blog/2021-08-26-welcome/index.md +0 -29
  138. synapse_sdk/devtools/docs/blog/authors.yml +0 -25
  139. synapse_sdk/devtools/docs/blog/tags.yml +0 -19
  140. synapse_sdk/devtools/docs/docusaurus.config.ts +0 -138
  141. synapse_sdk/devtools/docs/package-lock.json +0 -17455
  142. synapse_sdk/devtools/docs/package.json +0 -47
  143. synapse_sdk/devtools/docs/sidebars.ts +0 -44
  144. synapse_sdk/devtools/docs/src/components/HomepageFeatures/index.tsx +0 -71
  145. synapse_sdk/devtools/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  146. synapse_sdk/devtools/docs/src/css/custom.css +0 -30
  147. synapse_sdk/devtools/docs/src/pages/index.module.css +0 -23
  148. synapse_sdk/devtools/docs/src/pages/index.tsx +0 -21
  149. synapse_sdk/devtools/docs/src/pages/markdown-page.md +0 -7
  150. synapse_sdk/devtools/docs/static/.nojekyll +0 -0
  151. synapse_sdk/devtools/docs/static/img/docusaurus-social-card.jpg +0 -0
  152. synapse_sdk/devtools/docs/static/img/docusaurus.png +0 -0
  153. synapse_sdk/devtools/docs/static/img/favicon.ico +0 -0
  154. synapse_sdk/devtools/docs/static/img/logo.png +0 -0
  155. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  156. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_react.svg +0 -170
  157. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  158. synapse_sdk/devtools/docs/tsconfig.json +0 -8
  159. synapse_sdk/plugins/categories/export/actions/export.py +0 -346
  160. synapse_sdk/plugins/categories/export/enums.py +0 -7
  161. synapse_sdk/plugins/categories/neural_net/actions/gradio.py +0 -151
  162. synapse_sdk/plugins/categories/pre_annotation/actions/to_task.py +0 -943
  163. synapse_sdk/plugins/categories/upload/actions/upload.py +0 -954
  164. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +0 -0
  165. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
  166. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/licenses/LICENSE +0 -0
  167. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
@@ -1,367 +1,321 @@
1
- import random
2
- import string
1
+ """
2
+ DM Schema V2 → V1 Converter
3
3
 
4
- from . import BaseDMConverter
4
+ Created: 2025-12-11
5
5
 
6
+ V2→V1 conversion:
7
+ - If both annotation_data and annotation_meta exist, complete V1 restoration
8
+ - If only annotation_data exists, convert to V1 using defaults
9
+ """
6
10
 
7
- class DMV2ToV1Converter(BaseDMConverter):
8
- """DM v2 to v1 format converter class."""
11
+ from typing import Any
9
12
 
10
- def __init__(self, new_dm_data={}, file_type=None):
11
- """Initialize the converter.
13
+ from .base import BaseDMConverter
14
+ from .types import (
15
+ MEDIA_TYPE_REVERSE_MAP,
16
+ AnnotationMeta,
17
+ V2ConversionResult,
18
+ )
12
19
 
13
- Args:
14
- new_dm_data (dict): DM v2 format data to be converted
15
- file_type (str, optional): Type of file being converted (image, video, pcd, text, audio)
16
- """
17
- # Auto-detect file type if not provided
18
- if file_type is None:
19
- file_type = self._detect_file_type(new_dm_data)
20
20
 
21
- super().__init__(file_type=file_type)
22
- self.new_dm_data = new_dm_data
23
- self.annotations = {}
24
- self.annotations_data = {}
25
- self.extra = {}
26
- self.relations = {}
27
- self.annotation_groups = {}
21
+ class DMV2ToV1Converter(BaseDMConverter):
22
+ """Converter from DM Schema V2 to V1
28
23
 
29
- def _detect_file_type(self, data):
30
- """Auto-detect file type from the data structure.
24
+ V2→V1 conversion:
25
+ - If both annotation_data and annotation_meta exist, complete V1 restoration
26
+ - If only annotation_data exists, convert to V1 using defaults
31
27
 
32
- Args:
33
- data (dict): DM v2 format data
28
+ Example:
29
+ >>> converter = DMV2ToV1Converter()
30
+ >>> # Complete conversion
31
+ >>> v1_data = converter.convert(v2_result)
32
+ >>> # Convert with annotation_data only
33
+ >>> v1_data = converter.convert({"annotation_data": annotation_data})
34
+ """
34
35
 
35
- Returns:
36
- str: Detected file type (image, video, pcd, text, audio)
37
- """
38
- if not data:
39
- return None
40
-
41
- # Check for media type keys (plural forms)
42
- if 'images' in data:
43
- return 'image'
44
- elif 'videos' in data:
45
- return 'video'
46
- elif 'pcds' in data:
47
- return 'pcd'
48
- elif 'texts' in data:
49
- return 'text'
50
- elif 'audios' in data:
51
- return 'audio'
52
-
53
- return None
54
-
55
- def convert(self):
56
- """Convert DM v2 data to v1 format.
36
+ def _setup_tool_processors(self) -> None:
37
+ """Register tool processors"""
38
+ from .tools.bounding_box import BoundingBoxProcessor
57
39
 
58
- Returns:
59
- dict: Converted data in DM v1 format
60
- """
61
- # Reset state
62
- new_dm_data = self.new_dm_data
63
- self.annotations = {}
64
- self.annotations_data = {}
65
- self.extra = {}
66
- self.relations = {}
67
- self.annotation_groups = {}
68
-
69
- # Process each media type (images, videos, etc.)
70
- for media_type_plural, media_items in new_dm_data.items():
71
- if media_type_plural == 'classification':
72
- continue
40
+ self.register_processor(BoundingBoxProcessor())
73
41
 
74
- media_type = self._singularize_media_type(media_type_plural)
75
-
76
- for index, media_item in enumerate(media_items, 1):
77
- media_id = f'{media_type}_{index}'
78
-
79
- # Initialize structures for this media
80
- self.annotations[media_id] = []
81
- self.annotations_data[media_id] = []
82
- self.extra[media_id] = {}
83
- self.relations[media_id] = []
84
- self.annotation_groups[media_id] = []
85
-
86
- # Process each tool type in the media item
87
- for tool_type, tool_data in media_item.items():
88
- self._process_tool_data(media_id, tool_type, tool_data)
89
-
90
- # Build final result
91
- result = {
92
- 'extra': self.extra,
93
- 'relations': self.relations,
94
- 'annotations': self.annotations,
95
- 'annotationsData': self.annotations_data,
96
- 'annotationGroups': self.annotation_groups,
97
- }
42
+ # polygon to be added later
43
+ try:
44
+ from .tools.polygon import PolygonProcessor
98
45
 
99
- return result
46
+ self.register_processor(PolygonProcessor())
47
+ except ImportError:
48
+ pass
100
49
 
101
- def _process_tool_data(self, media_id, tool_type, tool_data):
102
- """Process tool data for a specific media item.
50
+ try:
51
+ from .tools.polyline import PolylineProcessor
103
52
 
104
- Args:
105
- media_id (str): ID of the media item
106
- tool_type (str): Type of annotation tool
107
- tool_data (list): List of annotation data for this tool
108
- """
109
- for annotation in tool_data:
110
- annotation_id = annotation['id']
111
- classification = annotation['classification']
112
- attrs = annotation.get('attrs', [])
113
- data = annotation.get('data', {})
114
-
115
- # Create annotation entry
116
- annotation_entry = {
117
- 'id': annotation_id,
118
- 'tool': tool_type,
119
- 'isLocked': False,
120
- 'isVisible': True,
121
- 'classification': {'class': classification},
122
- }
123
-
124
- # Add additional classification attributes from attrs
125
- for attr in attrs:
126
- attr_name = attr.get('name')
127
- attr_value = attr.get('value')
128
- if attr_name and attr_value is not None:
129
- annotation_entry['classification'][attr_name] = attr_value
130
-
131
- # Add special attributes for specific tools
132
- if tool_type == 'keypoint':
133
- annotation_entry['shape'] = 'circle'
134
-
135
- self.annotations[media_id].append(annotation_entry)
136
-
137
- # Create annotations data entry using tool processor
138
- processor = self.tool_processors.get(tool_type)
139
- if processor:
140
- processor(annotation_id, data, self.annotations_data[media_id])
141
- else:
142
- self._handle_unknown_tool(tool_type, annotation_id)
143
-
144
- def _convert_bounding_box(self, annotation_id, data, annotations_data):
145
- """Process bounding box annotation data.
53
+ self.register_processor(PolylineProcessor())
54
+ except ImportError:
55
+ pass
146
56
 
147
- Args:
148
- annotation_id (str): ID of the annotation
149
- data (list): Bounding box data [x1, y1, x2, y2]
150
- annotations_data (list): List to append the processed data
151
- """
152
- if len(data) >= 4:
153
- x1, y1, width, height = data[:4]
154
- coordinate = {'x': x1, 'y': y1, 'width': width, 'height': height}
57
+ try:
58
+ from .tools.keypoint import KeypointProcessor
155
59
 
156
- annotations_data.append({'id': annotation_id, 'coordinate': coordinate})
60
+ self.register_processor(KeypointProcessor())
61
+ except ImportError:
62
+ pass
157
63
 
158
- def _convert_named_entity(self, annotation_id, data, annotations_data):
159
- """Process named entity annotation data.
64
+ try:
65
+ from .tools.bounding_box_3d import BoundingBox3DProcessor
160
66
 
161
- Args:
162
- annotation_id (str): ID of the annotation
163
- data (dict): Named entity data with ranges and content
164
- annotations_data (list): List to append the processed data
165
- """
166
- entity_data = {'id': annotation_id}
67
+ self.register_processor(BoundingBox3DProcessor())
68
+ except ImportError:
69
+ pass
167
70
 
168
- if 'ranges' in data:
169
- entity_data['ranges'] = data['ranges']
71
+ try:
72
+ from .tools.segmentation import SegmentationProcessor
170
73
 
171
- if 'content' in data:
172
- entity_data['content'] = data['content']
74
+ self.register_processor(SegmentationProcessor())
75
+ except ImportError:
76
+ pass
173
77
 
174
- annotations_data.append(entity_data)
78
+ try:
79
+ from .tools.named_entity import NamedEntityProcessor
175
80
 
176
- def _convert_classification(self, annotation_id, data, annotations_data):
177
- """Process classification annotation data.
81
+ self.register_processor(NamedEntityProcessor())
82
+ except ImportError:
83
+ pass
178
84
 
179
- Args:
180
- annotation_id (str): ID of the annotation
181
- data (dict): Classification data (usually empty)
182
- annotations_data (list): List to append the processed data
183
- """
184
- # Classification data is typically empty in v2, so we just add the ID
185
- annotations_data.append({'id': annotation_id})
85
+ try:
86
+ from .tools.segmentation_3d import Segmentation3DProcessor
186
87
 
187
- def _convert_polyline(self, annotation_id, data, annotations_data):
188
- """Process polyline annotation data.
88
+ self.register_processor(Segmentation3DProcessor())
89
+ except ImportError:
90
+ pass
189
91
 
190
- Args:
191
- annotation_id (str): ID of the annotation
192
- data (list): Polyline data - can be flat [x1, y1, x2, y2, ...] or nested [[x1, y1], [x2, y2], ...]
193
- annotations_data (list): List to append the processed data
194
- """
195
- coordinates = []
92
+ try:
93
+ from .tools.classification import ClassificationProcessor
196
94
 
197
- if data and isinstance(data[0], list):
198
- # Nested format: [[x1, y1], [x2, y2], ...]
199
- for point in data:
200
- if len(point) >= 2:
201
- coordinates.append({'x': point[0], 'y': point[1], 'id': self._generate_random_id()})
202
- else:
203
- # Flat format: [x1, y1, x2, y2, ...]
204
- for i in range(0, len(data), 2):
205
- if i + 1 < len(data):
206
- coordinates.append({'x': data[i], 'y': data[i + 1], 'id': self._generate_random_id()})
95
+ self.register_processor(ClassificationProcessor())
96
+ except ImportError:
97
+ pass
207
98
 
208
- annotations_data.append({'id': annotation_id, 'coordinate': coordinates})
99
+ try:
100
+ from .tools.relation import RelationProcessor
209
101
 
210
- def _convert_keypoint(self, annotation_id, data, annotations_data):
211
- """Process keypoint annotation data.
102
+ self.register_processor(RelationProcessor())
103
+ except ImportError:
104
+ pass
212
105
 
213
- Args:
214
- annotation_id (str): ID of the annotation
215
- data (list): Keypoint data [x, y]
216
- annotations_data (list): List to append the processed data
217
- """
218
- if len(data) >= 2:
219
- coordinate = {'x': data[0], 'y': data[1]}
106
+ try:
107
+ from .tools.prompt import PromptProcessor
220
108
 
221
- annotations_data.append({'id': annotation_id, 'coordinate': coordinate})
109
+ self.register_processor(PromptProcessor())
110
+ except ImportError:
111
+ pass
222
112
 
223
- def _convert_3d_bounding_box(self, annotation_id, data, annotations_data):
224
- """Process 3D bounding box annotation data.
113
+ try:
114
+ from .tools.answer import AnswerProcessor
225
115
 
226
- Args:
227
- annotation_id (str): ID of the annotation
228
- data (dict): 3D bounding box PSR data
229
- annotations_data (list): List to append the processed data
230
- """
231
- annotations_data.append({'id': annotation_id, 'psr': data})
116
+ self.register_processor(AnswerProcessor())
117
+ except ImportError:
118
+ pass
232
119
 
233
- def _convert_image_segmentation(self, annotation_id, data, annotations_data):
234
- """Process segmentation annotation data.
120
+ def convert(
121
+ self,
122
+ v2_data: V2ConversionResult | dict[str, Any],
123
+ annotation_meta: AnnotationMeta | None = None,
124
+ ) -> dict[str, Any]:
125
+ """Convert V2 data to V1 format
235
126
 
236
127
  Args:
237
- annotation_id (str): ID of the annotation
238
- data (list or dict): Segmentation data (pixel_indices or section)
239
- annotations_data (list): List to append the processed data
240
- """
241
- annotation_data = {'id': annotation_id}
242
-
243
- if isinstance(data, list):
244
- # Pixel-based segmentation
245
- annotation_data['pixel_indices'] = data
246
- elif isinstance(data, dict):
247
- # Section-based segmentation (video)
248
- annotation_data['section'] = data
128
+ v2_data: DM Schema V2 format data
129
+ annotation_meta: Optional V1 top-level structure passed separately
249
130
 
250
- annotations_data.append(annotation_data)
251
-
252
- def _convert_video_segmentation(self, annotation_id, data, annotations_data):
253
- """Process video segmentation annotation data.
131
+ Returns:
132
+ DM Schema V1 format data
254
133
 
255
- Args:
256
- annotation_id (str): ID of the annotation
257
- data (list or dict): Segmentation data (pixel_indices or section)
258
- annotations_data (list): List to append the processed data
134
+ Raises:
135
+ ValueError: Missing required fields or invalid format
259
136
  """
260
- annotation_data = {'id': annotation_id}
137
+ # Extract annotation_data
138
+ if 'annotation_data' in v2_data:
139
+ annotation_data = v2_data['annotation_data']
140
+ # Extract annotation_meta (use from v2_data if present, else use parameter)
141
+ meta = v2_data.get('annotation_meta') or annotation_meta
142
+ else:
143
+ # annotation_data passed directly
144
+ annotation_data = v2_data
145
+ meta = annotation_meta
261
146
 
262
- if isinstance(data, list):
263
- # Pixel-based segmentation
264
- annotation_data['pixel_indices'] = data
265
- elif isinstance(data, dict):
266
- # Section-based segmentation (video)
267
- annotation_data['section'] = data
147
+ # Input validation
148
+ if not annotation_data:
149
+ raise ValueError("V2 data requires 'annotation_data'")
268
150
 
269
- annotations_data.append(annotation_data)
151
+ # Build V1 data
152
+ return self._merge_data_and_meta(annotation_data, meta)
270
153
 
271
- def _convert_3d_segmentation(self, annotation_id, data, annotations_data):
272
- """Process 3D segmentation annotation data.
154
+ def _merge_data_and_meta(
155
+ self,
156
+ annotation_data: dict[str, Any],
157
+ annotation_meta: AnnotationMeta | None,
158
+ ) -> dict[str, Any]:
159
+ """Merge annotation_data and annotation_meta to create V1 format
273
160
 
274
161
  Args:
275
- annotation_id (str): ID of the annotation
276
- data (list or dict): 3D segmentation data
277
- annotations_data (list): List to append the processed data
278
- """
279
- annotation_data = {'id': annotation_id}
162
+ annotation_data: V2 common annotation structure
163
+ annotation_meta: V1 top-level structure (restores meta info if present)
280
164
 
281
- if isinstance(data, list):
282
- # Pixel-based segmentation
283
- annotation_data['pixel_indices'] = data
284
- elif isinstance(data, dict):
285
- # Section-based segmentation
286
- annotation_data['section'] = data
287
-
288
- annotations_data.append(annotation_data)
289
-
290
- def _convert_prompt(self, annotation_id, data, annotations_data):
291
- """Process prompt annotation data.
292
-
293
- Args:
294
- annotation_id (str): ID of the annotation
295
- data (dict): Prompt data
296
- annotations_data (list): List to append the processed data
165
+ Returns:
166
+ Merged V1 format data
297
167
  """
298
- annotation_data = {'id': annotation_id}
168
+ annotations: dict[str, list[dict[str, Any]]] = {}
169
+ annotations_data: dict[str, list[dict[str, Any]]] = {}
299
170
 
300
- if isinstance(data, dict):
301
- annotation_data.update(data)
171
+ # Process by media type
172
+ media_index_by_type: dict[str, int] = {}
302
173
 
303
- annotations_data.append(annotation_data)
174
+ for plural_type in ['images', 'videos', 'pcds', 'texts', 'audios', 'prompts']:
175
+ if plural_type not in annotation_data:
176
+ continue
304
177
 
305
- def _convert_answer(self, annotation_id, data, annotations_data):
306
- """Process answer annotation data.
178
+ singular_type = MEDIA_TYPE_REVERSE_MAP.get(plural_type, plural_type.rstrip('s'))
179
+ media_index_by_type[singular_type] = 0
307
180
 
308
- Args:
309
- annotation_id (str): ID of the annotation
310
- data (dict): Answer data
311
- annotations_data (list): List to append the processed data
312
- """
313
- annotation_data = {'id': annotation_id}
314
-
315
- if isinstance(data, dict):
316
- annotation_data.update(data)
181
+ for media_item in annotation_data[plural_type]:
182
+ # Generate media ID
183
+ media_index_by_type[singular_type] += 1
184
+ media_id = f'{singular_type}_{media_index_by_type[singular_type]}'
317
185
 
318
- annotations_data.append(annotation_data)
186
+ # Convert by tool
187
+ ann_list, data_list = self._convert_media_item(media_item, media_id, annotation_meta)
319
188
 
320
- def _convert_polygon(self, annotation_id, data, annotations_data):
321
- """Process polygon annotation data.
189
+ if ann_list:
190
+ annotations[media_id] = ann_list
191
+ if data_list:
192
+ annotations_data[media_id] = data_list
322
193
 
323
- Args:
324
- annotation_id (str): ID of the annotation
325
- data (list): Polygon data - can be flat [x1, y1, x2, y2, ...] or nested [[x1, y1], [x2, y2], ...]
326
- annotations_data (list): List to append the processed data
327
- """
328
- coordinates = []
194
+ # Build V1 result
195
+ result: dict[str, Any] = {
196
+ 'annotations': annotations,
197
+ 'annotationsData': annotations_data,
198
+ }
329
199
 
330
- if data and isinstance(data[0], list):
331
- # Nested format: [[x1, y1], [x2, y2], ...]
332
- for point in data:
333
- if len(point) >= 2:
334
- coordinates.append({'x': point[0], 'y': point[1], 'id': self._generate_random_id()})
200
+ # Restore additional fields if annotation_meta exists
201
+ if annotation_meta:
202
+ result['extra'] = annotation_meta.get('extra', {})
203
+ result['relations'] = annotation_meta.get('relations', {})
204
+ result['annotationGroups'] = annotation_meta.get('annotationGroups', {})
205
+ result['assignmentId'] = annotation_meta.get('assignmentId')
335
206
  else:
336
- # Flat format: [x1, y1, x2, y2, ...]
337
- for i in range(0, len(data), 2):
338
- if i + 1 < len(data):
339
- coordinates.append({'x': data[i], 'y': data[i + 1], 'id': self._generate_random_id()})
207
+ # Default values
208
+ result['extra'] = {}
209
+ result['relations'] = {}
210
+ result['annotationGroups'] = {}
211
+ result['assignmentId'] = None
340
212
 
341
- annotations_data.append({'id': annotation_id, 'coordinate': coordinates})
213
+ return result
342
214
 
343
- def _convert_relation(self, annotation_id, data, annotations_data):
344
- """Process relation annotation data.
215
+ def _convert_media_item(
216
+ self,
217
+ media_item: dict[str, Any],
218
+ media_id: str,
219
+ annotation_meta: AnnotationMeta | None,
220
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
221
+ """Convert V2 media item to V1 annotations/annotationsData
345
222
 
346
223
  Args:
347
- annotation_id (str): ID of the annotation
348
- data (list): Relation data
349
- annotations_data (list): List to append the processed data
224
+ media_item: V2 media item
225
+ media_id: Media ID to generate
226
+ annotation_meta: V1 top-level structure (for meta info restoration)
227
+
228
+ Returns:
229
+ (V1 annotations list, V1 annotationsData list)
350
230
  """
351
- annotations_data.append({'id': annotation_id, 'data': data})
231
+ annotations: list[dict[str, Any]] = []
232
+ annotations_data: list[dict[str, Any]] = []
233
+
234
+ # Process by tool
235
+ for tool_name, v2_annotations in media_item.items():
236
+ processor = self.get_processor(tool_name)
237
+ if not processor:
238
+ continue
352
239
 
353
- def _convert_group(self, annotation_id, data, annotations_data):
354
- """Process group annotation data.
240
+ for v2_ann in v2_annotations:
241
+ # Convert to V1
242
+ v1_ann, v1_data = processor.to_v1(v2_ann)
243
+
244
+ # Restore meta info from annotation_meta
245
+ if annotation_meta:
246
+ v1_ann = self._restore_meta_fields(v1_ann, annotation_meta, v2_ann.get('id', ''), media_id)
247
+ else:
248
+ # Set default values
249
+ v1_ann.setdefault('isLocked', False)
250
+ v1_ann.setdefault('isVisible', True)
251
+ v1_ann.setdefault('isValid', False)
252
+ v1_ann.setdefault('isDrawCompleted', True)
253
+ v1_ann.setdefault('label', [])
254
+
255
+ annotations.append(v1_ann)
256
+ annotations_data.append(v1_data)
257
+
258
+ return annotations, annotations_data
259
+
260
+ def _restore_meta_fields(
261
+ self,
262
+ v1_annotation: dict[str, Any],
263
+ annotation_meta: AnnotationMeta,
264
+ annotation_id: str,
265
+ media_id: str,
266
+ ) -> dict[str, Any]:
267
+ """Restore V1 annotation meta fields from annotation_meta
355
268
 
356
269
  Args:
357
- annotation_id (str): ID of the annotation
358
- data (list): Group data
359
- annotations_data (list): List to append the processed data
360
- """
361
- annotations_data.append({'id': annotation_id, 'data': data})
270
+ v1_annotation: Base converted V1 annotation
271
+ annotation_meta: V1 top-level structure
272
+ annotation_id: Annotation ID
273
+ media_id: Media ID
362
274
 
363
- def _generate_random_id(self):
364
- """Generate a random ID similar to the original format."""
365
- # Generate 10-character random string with letters, numbers, and symbols
366
- chars = string.ascii_letters + string.digits + '-_'
367
- return ''.join(random.choices(chars, k=10))
275
+ Returns:
276
+ V1 annotation with restored meta fields
277
+ """
278
+ # Find annotation in annotation_meta
279
+ meta_annotations = annotation_meta.get('annotations', {})
280
+
281
+ # Try to find by media_id
282
+ source_media_id = None
283
+ for mid in meta_annotations:
284
+ for ann in meta_annotations[mid]:
285
+ if ann.get('id') == annotation_id:
286
+ source_media_id = mid
287
+ break
288
+ if source_media_id:
289
+ break
290
+
291
+ if not source_media_id:
292
+ # Use defaults if not found
293
+ v1_annotation.setdefault('isLocked', False)
294
+ v1_annotation.setdefault('isVisible', True)
295
+ v1_annotation.setdefault('isValid', False)
296
+ v1_annotation.setdefault('isDrawCompleted', True)
297
+ v1_annotation.setdefault('label', [])
298
+ return v1_annotation
299
+
300
+ # Restore meta info from the found annotation
301
+ for meta_ann in meta_annotations[source_media_id]:
302
+ if meta_ann.get('id') == annotation_id:
303
+ # Restore meta fields
304
+ v1_annotation['isLocked'] = meta_ann.get('isLocked', False)
305
+ v1_annotation['isVisible'] = meta_ann.get('isVisible', True)
306
+ v1_annotation['isValid'] = meta_ann.get('isValid', False)
307
+ v1_annotation['isDrawCompleted'] = meta_ann.get('isDrawCompleted', True)
308
+ v1_annotation['label'] = meta_ann.get('label', [])
309
+
310
+ # Merge classification if present in meta
311
+ meta_classification = meta_ann.get('classification')
312
+ if meta_classification:
313
+ # Keep existing class from classification and merge other fields
314
+ current_class = v1_annotation.get('classification', {}).get('class')
315
+ v1_annotation['classification'] = meta_classification.copy()
316
+ if current_class:
317
+ v1_annotation['classification']['class'] = current_class
318
+
319
+ break
320
+
321
+ return v1_annotation