synapse-sdk 1.0.0a11__py3-none-any.whl → 2026.1.1b2__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 synapse-sdk might be problematic. Click here for more details.

Files changed (261) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +9 -8
  3. synapse_sdk/cli/agent/__init__.py +25 -0
  4. synapse_sdk/cli/agent/config.py +104 -0
  5. synapse_sdk/cli/agent/select.py +197 -0
  6. synapse_sdk/cli/auth.py +104 -0
  7. synapse_sdk/cli/main.py +1025 -0
  8. synapse_sdk/cli/plugin/__init__.py +58 -0
  9. synapse_sdk/cli/plugin/create.py +566 -0
  10. synapse_sdk/cli/plugin/job.py +196 -0
  11. synapse_sdk/cli/plugin/publish.py +322 -0
  12. synapse_sdk/cli/plugin/run.py +131 -0
  13. synapse_sdk/cli/plugin/test.py +200 -0
  14. synapse_sdk/clients/README.md +239 -0
  15. synapse_sdk/clients/__init__.py +5 -0
  16. synapse_sdk/clients/_template.py +266 -0
  17. synapse_sdk/clients/agent/__init__.py +84 -29
  18. synapse_sdk/clients/agent/async_ray.py +289 -0
  19. synapse_sdk/clients/agent/container.py +83 -0
  20. synapse_sdk/clients/agent/plugin.py +101 -0
  21. synapse_sdk/clients/agent/ray.py +296 -39
  22. synapse_sdk/clients/backend/__init__.py +152 -12
  23. synapse_sdk/clients/backend/annotation.py +164 -22
  24. synapse_sdk/clients/backend/core.py +101 -0
  25. synapse_sdk/clients/backend/data_collection.py +292 -0
  26. synapse_sdk/clients/backend/hitl.py +87 -0
  27. synapse_sdk/clients/backend/integration.py +374 -46
  28. synapse_sdk/clients/backend/ml.py +134 -22
  29. synapse_sdk/clients/backend/models.py +247 -0
  30. synapse_sdk/clients/base.py +538 -59
  31. synapse_sdk/clients/exceptions.py +35 -7
  32. synapse_sdk/clients/pipeline/__init__.py +5 -0
  33. synapse_sdk/clients/pipeline/client.py +636 -0
  34. synapse_sdk/clients/protocols.py +178 -0
  35. synapse_sdk/clients/utils.py +86 -8
  36. synapse_sdk/clients/validation.py +58 -0
  37. synapse_sdk/enums.py +76 -0
  38. synapse_sdk/exceptions.py +168 -0
  39. synapse_sdk/integrations/__init__.py +74 -0
  40. synapse_sdk/integrations/_base.py +119 -0
  41. synapse_sdk/integrations/_context.py +53 -0
  42. synapse_sdk/integrations/ultralytics/__init__.py +78 -0
  43. synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
  44. synapse_sdk/integrations/ultralytics/_patches.py +124 -0
  45. synapse_sdk/loggers.py +476 -95
  46. synapse_sdk/mcp/MCP.md +69 -0
  47. synapse_sdk/mcp/__init__.py +48 -0
  48. synapse_sdk/mcp/__main__.py +6 -0
  49. synapse_sdk/mcp/config.py +349 -0
  50. synapse_sdk/mcp/prompts/__init__.py +4 -0
  51. synapse_sdk/mcp/resources/__init__.py +4 -0
  52. synapse_sdk/mcp/server.py +1352 -0
  53. synapse_sdk/mcp/tools/__init__.py +6 -0
  54. synapse_sdk/plugins/__init__.py +133 -9
  55. synapse_sdk/plugins/action.py +229 -0
  56. synapse_sdk/plugins/actions/__init__.py +82 -0
  57. synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
  58. synapse_sdk/plugins/actions/dataset/action.py +471 -0
  59. synapse_sdk/plugins/actions/export/__init__.py +55 -0
  60. synapse_sdk/plugins/actions/export/action.py +183 -0
  61. synapse_sdk/plugins/actions/export/context.py +59 -0
  62. synapse_sdk/plugins/actions/inference/__init__.py +84 -0
  63. synapse_sdk/plugins/actions/inference/action.py +285 -0
  64. synapse_sdk/plugins/actions/inference/context.py +81 -0
  65. synapse_sdk/plugins/actions/inference/deployment.py +322 -0
  66. synapse_sdk/plugins/actions/inference/serve.py +252 -0
  67. synapse_sdk/plugins/actions/train/__init__.py +54 -0
  68. synapse_sdk/plugins/actions/train/action.py +326 -0
  69. synapse_sdk/plugins/actions/train/context.py +57 -0
  70. synapse_sdk/plugins/actions/upload/__init__.py +49 -0
  71. synapse_sdk/plugins/actions/upload/action.py +165 -0
  72. synapse_sdk/plugins/actions/upload/context.py +61 -0
  73. synapse_sdk/plugins/config.py +98 -0
  74. synapse_sdk/plugins/context/__init__.py +109 -0
  75. synapse_sdk/plugins/context/env.py +113 -0
  76. synapse_sdk/plugins/datasets/__init__.py +113 -0
  77. synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
  78. synapse_sdk/plugins/datasets/converters/base.py +347 -0
  79. synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
  80. synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
  81. synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
  82. synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
  83. synapse_sdk/plugins/datasets/formats/dm.py +351 -0
  84. synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
  85. synapse_sdk/plugins/decorators.py +83 -0
  86. synapse_sdk/plugins/discovery.py +790 -0
  87. synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
  88. synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
  89. synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
  90. synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
  91. synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
  92. synapse_sdk/plugins/docs/README.md +513 -0
  93. synapse_sdk/plugins/docs/STEP.md +656 -0
  94. synapse_sdk/plugins/enums.py +70 -10
  95. synapse_sdk/plugins/errors.py +92 -0
  96. synapse_sdk/plugins/executors/__init__.py +43 -0
  97. synapse_sdk/plugins/executors/local.py +99 -0
  98. synapse_sdk/plugins/executors/ray/__init__.py +18 -0
  99. synapse_sdk/plugins/executors/ray/base.py +282 -0
  100. synapse_sdk/plugins/executors/ray/job.py +298 -0
  101. synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
  102. synapse_sdk/plugins/executors/ray/packaging.py +137 -0
  103. synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
  104. synapse_sdk/plugins/executors/ray/task.py +257 -0
  105. synapse_sdk/plugins/models/__init__.py +26 -0
  106. synapse_sdk/plugins/models/logger.py +173 -0
  107. synapse_sdk/plugins/models/pipeline.py +25 -0
  108. synapse_sdk/plugins/pipelines/__init__.py +81 -0
  109. synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
  110. synapse_sdk/plugins/pipelines/context.py +107 -0
  111. synapse_sdk/plugins/pipelines/display.py +311 -0
  112. synapse_sdk/plugins/runner.py +114 -0
  113. synapse_sdk/plugins/schemas/__init__.py +19 -0
  114. synapse_sdk/plugins/schemas/results.py +152 -0
  115. synapse_sdk/plugins/steps/__init__.py +63 -0
  116. synapse_sdk/plugins/steps/base.py +128 -0
  117. synapse_sdk/plugins/steps/context.py +90 -0
  118. synapse_sdk/plugins/steps/orchestrator.py +128 -0
  119. synapse_sdk/plugins/steps/registry.py +103 -0
  120. synapse_sdk/plugins/steps/utils/__init__.py +20 -0
  121. synapse_sdk/plugins/steps/utils/logging.py +85 -0
  122. synapse_sdk/plugins/steps/utils/timing.py +71 -0
  123. synapse_sdk/plugins/steps/utils/validation.py +68 -0
  124. synapse_sdk/plugins/templates/__init__.py +50 -0
  125. synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
  126. synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
  127. synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
  128. synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
  129. synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
  130. synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
  131. synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
  132. synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
  133. synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
  134. synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
  135. synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
  136. synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
  137. synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
  138. synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
  139. synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
  140. synapse_sdk/plugins/testing/__init__.py +25 -0
  141. synapse_sdk/plugins/testing/sample_actions.py +98 -0
  142. synapse_sdk/plugins/types.py +206 -0
  143. synapse_sdk/plugins/upload.py +595 -64
  144. synapse_sdk/plugins/utils.py +325 -37
  145. synapse_sdk/shared/__init__.py +25 -0
  146. synapse_sdk/utils/__init__.py +1 -0
  147. synapse_sdk/utils/auth.py +74 -0
  148. synapse_sdk/utils/file/__init__.py +58 -0
  149. synapse_sdk/utils/file/archive.py +449 -0
  150. synapse_sdk/utils/file/checksum.py +167 -0
  151. synapse_sdk/utils/file/download.py +286 -0
  152. synapse_sdk/utils/file/io.py +129 -0
  153. synapse_sdk/utils/file/requirements.py +36 -0
  154. synapse_sdk/utils/network.py +168 -0
  155. synapse_sdk/utils/storage/__init__.py +238 -0
  156. synapse_sdk/utils/storage/config.py +188 -0
  157. synapse_sdk/utils/storage/errors.py +52 -0
  158. synapse_sdk/utils/storage/providers/__init__.py +13 -0
  159. synapse_sdk/utils/storage/providers/base.py +76 -0
  160. synapse_sdk/utils/storage/providers/gcs.py +168 -0
  161. synapse_sdk/utils/storage/providers/http.py +250 -0
  162. synapse_sdk/utils/storage/providers/local.py +126 -0
  163. synapse_sdk/utils/storage/providers/s3.py +177 -0
  164. synapse_sdk/utils/storage/providers/sftp.py +208 -0
  165. synapse_sdk/utils/storage/registry.py +125 -0
  166. synapse_sdk/utils/websocket.py +99 -0
  167. synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
  168. synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
  169. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
  170. synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
  171. locale/en/LC_MESSAGES/messages.mo +0 -0
  172. locale/en/LC_MESSAGES/messages.po +0 -39
  173. locale/ko/LC_MESSAGES/messages.mo +0 -0
  174. locale/ko/LC_MESSAGES/messages.po +0 -34
  175. synapse_sdk/cli/create_plugin.py +0 -10
  176. synapse_sdk/clients/agent/core.py +0 -7
  177. synapse_sdk/clients/agent/service.py +0 -15
  178. synapse_sdk/clients/backend/dataset.py +0 -51
  179. synapse_sdk/clients/ray/__init__.py +0 -6
  180. synapse_sdk/clients/ray/core.py +0 -22
  181. synapse_sdk/clients/ray/serve.py +0 -20
  182. synapse_sdk/i18n.py +0 -35
  183. synapse_sdk/plugins/categories/__init__.py +0 -0
  184. synapse_sdk/plugins/categories/base.py +0 -235
  185. synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
  186. synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
  187. synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
  188. synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
  189. synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
  190. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
  191. synapse_sdk/plugins/categories/decorators.py +0 -13
  192. synapse_sdk/plugins/categories/export/__init__.py +0 -0
  193. synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
  194. synapse_sdk/plugins/categories/export/actions/export.py +0 -10
  195. synapse_sdk/plugins/categories/import/__init__.py +0 -0
  196. synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
  197. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  198. synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
  199. synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
  200. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
  201. synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
  202. synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
  203. synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
  204. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
  205. synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
  206. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
  207. synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
  208. synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
  209. synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
  210. synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
  211. synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
  212. synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
  213. synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
  214. synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
  215. synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
  216. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
  217. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
  218. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
  219. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
  220. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
  221. synapse_sdk/plugins/categories/registry.py +0 -16
  222. synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
  223. synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
  224. synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
  225. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
  226. synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
  227. synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
  228. synapse_sdk/plugins/categories/templates.py +0 -32
  229. synapse_sdk/plugins/cli/__init__.py +0 -21
  230. synapse_sdk/plugins/cli/publish.py +0 -37
  231. synapse_sdk/plugins/cli/run.py +0 -67
  232. synapse_sdk/plugins/exceptions.py +0 -22
  233. synapse_sdk/plugins/models.py +0 -121
  234. synapse_sdk/plugins/templates/cookiecutter.json +0 -11
  235. synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
  236. synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
  237. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  238. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  239. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
  240. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
  241. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
  242. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
  243. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  244. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
  245. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
  246. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
  247. synapse_sdk/shared/enums.py +0 -8
  248. synapse_sdk/utils/debug.py +0 -5
  249. synapse_sdk/utils/file.py +0 -87
  250. synapse_sdk/utils/module_loading.py +0 -29
  251. synapse_sdk/utils/pydantic/__init__.py +0 -0
  252. synapse_sdk/utils/pydantic/config.py +0 -4
  253. synapse_sdk/utils/pydantic/errors.py +0 -33
  254. synapse_sdk/utils/pydantic/validators.py +0 -7
  255. synapse_sdk/utils/storage.py +0 -91
  256. synapse_sdk/utils/string.py +0 -11
  257. synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
  258. synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
  259. synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
  260. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
  261. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,468 @@
1
+ """Convert Datamaker format to YOLO format.
2
+
3
+ Supports:
4
+ - DMv1 and DMv2 schemas
5
+ - Bounding box, polygon, and keypoint annotations
6
+ - Categorized (train/valid/test splits) and non-categorized datasets
7
+ - Single file conversion mode
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import shutil
14
+ from pathlib import Path
15
+ from typing import IO, Any
16
+
17
+ from synapse_sdk.plugins.datasets.converters.base import DatasetFormat, FromDMConverter
18
+ from synapse_sdk.plugins.datasets.formats.dm import DMVersion
19
+
20
+
21
+ class FromDMToYOLOConverter(FromDMConverter):
22
+ """Convert Datamaker dataset format to YOLO format.
23
+
24
+ Supports bounding boxes, polygons (native YOLO segmentation format),
25
+ and keypoints. Works with both DMv1 and DMv2 schemas.
26
+
27
+ Example:
28
+ >>> # Directory conversion
29
+ >>> converter = FromDMToYOLOConverter(
30
+ ... root_dir='/data/dm_dataset',
31
+ ... is_categorized=True,
32
+ ... dm_version=DMVersion.V2,
33
+ ... )
34
+ >>> converter.convert()
35
+ >>> converter.save_to_folder('/data/yolo_output')
36
+
37
+ >>> # Single file conversion
38
+ >>> converter = FromDMToYOLOConverter(is_single_conversion=True)
39
+ >>> result = converter.convert_single_file(dm_json, image_file)
40
+ """
41
+
42
+ target_format = DatasetFormat.YOLO
43
+
44
+ def __init__(
45
+ self,
46
+ root_dir: str | Path | None = None,
47
+ is_categorized: bool = False,
48
+ is_single_conversion: bool = False,
49
+ dm_version: DMVersion = DMVersion.V2,
50
+ ) -> None:
51
+ """Initialize converter.
52
+
53
+ Args:
54
+ root_dir: Root directory containing DM data.
55
+ is_categorized: Whether dataset has train/valid/test splits.
56
+ is_single_conversion: Whether converting single files only.
57
+ dm_version: Datamaker schema version (V1 or V2).
58
+ """
59
+ super().__init__(root_dir, is_categorized, is_single_conversion, dm_version)
60
+ self.dataset_yaml_content: str = ''
61
+
62
+ @staticmethod
63
+ def get_all_classes(list_of_dirs: list[Path], dm_version: DMVersion = DMVersion.V2) -> list[str]:
64
+ """Collect all unique class names from directories.
65
+
66
+ Args:
67
+ list_of_dirs: List of directories to scan.
68
+ dm_version: Datamaker schema version.
69
+
70
+ Returns:
71
+ Sorted list of unique class names.
72
+ """
73
+ classes: set[str] = set()
74
+
75
+ for d in list_of_dirs:
76
+ if not d or not d.is_dir():
77
+ continue
78
+
79
+ json_dir = d / 'json' if (d / 'json').is_dir() else d
80
+
81
+ for jfile in json_dir.glob('*.json'):
82
+ with open(jfile, encoding='utf-8') as f:
83
+ data = json.load(f)
84
+
85
+ if dm_version == DMVersion.V2:
86
+ for img_ann in data.get('images', []):
87
+ for key in ['bounding_box', 'polygon', 'keypoint']:
88
+ for ann in img_ann.get(key, []):
89
+ if 'classification' in ann:
90
+ classes.add(ann['classification'])
91
+ else:
92
+ # V1: annotations keyed by asset
93
+ for anns in data.get('annotations', {}).values():
94
+ for ann in anns:
95
+ classification = ann.get('classification', {})
96
+ if isinstance(classification, dict):
97
+ for val in classification.values():
98
+ if isinstance(val, str):
99
+ classes.add(val)
100
+
101
+ return sorted(classes)
102
+
103
+ @staticmethod
104
+ def polygon_to_yolo_string(polygon: list, width: int, height: int) -> str:
105
+ """Convert polygon points to normalized YOLO segmentation format.
106
+
107
+ YOLO segmentation format: x1 y1 x2 y2 x3 y3 ... (normalized 0-1)
108
+
109
+ Args:
110
+ polygon: List of [x, y] points.
111
+ width: Image width in pixels.
112
+ height: Image height in pixels.
113
+
114
+ Returns:
115
+ Space-separated normalized coordinate string.
116
+ """
117
+ if not polygon:
118
+ return ''
119
+
120
+ coords = []
121
+ for point in polygon:
122
+ x, y = point[0], point[1]
123
+ x_norm = x / width
124
+ y_norm = y / height
125
+ coords.extend([f'{x_norm:.6f}', f'{y_norm:.6f}'])
126
+
127
+ return ' '.join(coords)
128
+
129
+ @staticmethod
130
+ def keypoints_to_yolo_string(keypoints: list, width: int, height: int) -> str:
131
+ """Convert keypoints to normalized YOLO keypoint format.
132
+
133
+ YOLO keypoint format: x1 y1 v1 x2 y2 v2 ... (normalized, v=visibility)
134
+
135
+ Args:
136
+ keypoints: List of [x, y, visibility] points.
137
+ width: Image width in pixels.
138
+ height: Image height in pixels.
139
+
140
+ Returns:
141
+ Space-separated normalized coordinate string with visibility.
142
+ """
143
+ kp_strs = []
144
+ for kp in keypoints:
145
+ x, y = kp[0], kp[1]
146
+ v = kp[2] if len(kp) > 2 else 2 # Default visible
147
+ x_norm = x / width
148
+ y_norm = y / height
149
+ kp_strs.extend([f'{x_norm:.6f}', f'{y_norm:.6f}', str(int(v))])
150
+ return ' '.join(kp_strs)
151
+
152
+ def _convert_split_dir(self, split_dir: Path, split_name: str) -> list[dict[str, Any]]:
153
+ """Convert one split folder to YOLO format."""
154
+ if self.class_map is None:
155
+ raise ValueError('class_map not initialized. Call convert() first.')
156
+
157
+ json_dir = split_dir / 'json'
158
+ img_dir = split_dir / 'original_files'
159
+ entries = []
160
+
161
+ for jfile in json_dir.glob('*.json'):
162
+ base = jfile.stem
163
+
164
+ # Find corresponding image
165
+ img_path = self.find_image_for_label(base, img_dir)
166
+ if not img_path:
167
+ print(f'[{split_name}] Image for {base} not found, skipping.')
168
+ continue
169
+
170
+ width, height = self.get_image_size(img_path)
171
+
172
+ with open(jfile, encoding='utf-8') as f:
173
+ data = json.load(f)
174
+
175
+ label_lines = self._convert_dm_json_to_yolo_lines(data, width, height)
176
+
177
+ entries.append({
178
+ 'img_path': img_path,
179
+ 'img_name': img_path.name,
180
+ 'label_name': f'{base}.txt',
181
+ 'label_lines': label_lines,
182
+ })
183
+
184
+ return entries
185
+
186
+ def _convert_root_dir(self) -> list[dict[str, Any]]:
187
+ """Convert non-categorized dataset to YOLO format."""
188
+ return self._convert_split_dir(self.root_dir, 'root')
189
+
190
+ def _convert_dm_json_to_yolo_lines(
191
+ self,
192
+ data: dict[str, Any],
193
+ width: int,
194
+ height: int,
195
+ ) -> list[str]:
196
+ """Convert DM JSON data to YOLO label lines."""
197
+ label_lines = []
198
+
199
+ if self.dm_version == DMVersion.V2:
200
+ if 'images' in data and data['images']:
201
+ img_ann = data['images'][0]
202
+ label_lines.extend(self._convert_v2_annotations(img_ann, width, height))
203
+ else:
204
+ # V1: annotations keyed by asset
205
+ for anns in data.get('annotations', {}).values():
206
+ label_lines.extend(self._convert_v1_annotations(anns, width, height))
207
+
208
+ return label_lines
209
+
210
+ def _convert_v2_annotations(
211
+ self,
212
+ img_ann: dict[str, Any],
213
+ width: int,
214
+ height: int,
215
+ ) -> list[str]:
216
+ """Convert DMv2 image annotations to YOLO lines."""
217
+ lines = []
218
+
219
+ # Bounding boxes
220
+ for box in img_ann.get('bounding_box', []):
221
+ classification = box.get('classification')
222
+ if classification not in self.class_map:
223
+ continue
224
+
225
+ cidx = self.class_map[classification]
226
+ x, y, w, h = box['data']
227
+
228
+ # Convert to YOLO format: center_x, center_y, width, height (normalized)
229
+ cx = (x + w / 2) / width
230
+ cy = (y + h / 2) / height
231
+ nw = w / width
232
+ nh = h / height
233
+
234
+ lines.append(f'{cidx} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}')
235
+
236
+ # Polygons (YOLO segmentation format)
237
+ for poly in img_ann.get('polygon', []):
238
+ classification = poly.get('classification')
239
+ if classification not in self.class_map:
240
+ continue
241
+
242
+ cidx = self.class_map[classification]
243
+ poly_str = self.polygon_to_yolo_string(poly['data'], width, height)
244
+ if poly_str:
245
+ lines.append(f'{cidx} {poly_str}')
246
+
247
+ # Keypoints
248
+ for kp in img_ann.get('keypoint', []):
249
+ classification = kp.get('classification')
250
+ if classification not in self.class_map:
251
+ continue
252
+
253
+ cidx = self.class_map[classification]
254
+
255
+ # Get bounding box for keypoint (required for YOLO pose)
256
+ if 'bounding_box' in kp:
257
+ bbox = kp['bounding_box']
258
+ if isinstance(bbox, dict):
259
+ x, y, w, h = bbox['x'], bbox['y'], bbox['width'], bbox['height']
260
+ else:
261
+ x, y, w, h = bbox
262
+ cx = (x + w / 2) / width
263
+ cy = (y + h / 2) / height
264
+ nw = w / width
265
+ nh = h / height
266
+ else:
267
+ # Fallback to full image
268
+ cx, cy, nw, nh = 0.5, 0.5, 1.0, 1.0
269
+
270
+ kp_str = self.keypoints_to_yolo_string(kp['data'], width, height)
271
+ lines.append(f'{cidx} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f} {kp_str}')
272
+
273
+ return lines
274
+
275
+ def _convert_v1_annotations(
276
+ self,
277
+ annotations: list[dict[str, Any]],
278
+ width: int,
279
+ height: int,
280
+ ) -> list[str]:
281
+ """Convert DMv1 annotations to YOLO lines."""
282
+ lines = []
283
+
284
+ for ann in annotations:
285
+ tool = ann.get('tool', '')
286
+
287
+ # Get class name from classification
288
+ class_name = None
289
+ classification = ann.get('classification', {})
290
+ if isinstance(classification, dict):
291
+ class_name = classification.get('class') or classification.get('label')
292
+ if not class_name:
293
+ for val in classification.values():
294
+ if isinstance(val, str):
295
+ class_name = val
296
+ break
297
+
298
+ if not class_name or class_name not in self.class_map:
299
+ continue
300
+
301
+ cidx = self.class_map[class_name]
302
+ data = ann.get('data') or ann.get('points')
303
+ if not data:
304
+ continue
305
+
306
+ if tool in ('boundingBox', 'bounding_box'):
307
+ if isinstance(data, dict):
308
+ x, y, w, h = data['x'], data['y'], data['width'], data['height']
309
+ else:
310
+ x, y, w, h = data
311
+
312
+ cx = (x + w / 2) / width
313
+ cy = (y + h / 2) / height
314
+ nw = w / width
315
+ nh = h / height
316
+ lines.append(f'{cidx} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}')
317
+
318
+ elif tool == 'polygon':
319
+ poly_str = self.polygon_to_yolo_string(data, width, height)
320
+ if poly_str:
321
+ lines.append(f'{cidx} {poly_str}')
322
+
323
+ elif tool == 'keypoint':
324
+ bbox = ann.get('bounding_box')
325
+ if bbox:
326
+ if isinstance(bbox, dict):
327
+ x, y, w, h = bbox['x'], bbox['y'], bbox['width'], bbox['height']
328
+ else:
329
+ x, y, w, h = bbox
330
+ cx = (x + w / 2) / width
331
+ cy = (y + h / 2) / height
332
+ nw = w / width
333
+ nh = h / height
334
+ else:
335
+ cx, cy, nw, nh = 0.5, 0.5, 1.0, 1.0
336
+
337
+ kp_str = self.keypoints_to_yolo_string(data, width, height)
338
+ lines.append(f'{cidx} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f} {kp_str}')
339
+
340
+ return lines
341
+
342
+ def convert(self) -> dict[str, list[dict[str, Any]]] | list[dict[str, Any]]:
343
+ """Convert DM format to YOLO format.
344
+
345
+ Returns:
346
+ If categorized: dict mapping split names to list of entries.
347
+ If not categorized: list of entries.
348
+ Each entry contains img_path, img_name, label_name, label_lines.
349
+ """
350
+ yaml_lines = [f'path: {self.root_dir}']
351
+
352
+ if self.is_categorized:
353
+ splits = self._validate_splits(['train', 'valid'], ['test'])
354
+ self.class_names = self.get_all_classes(list(splits.values()), self.dm_version)
355
+ self.class_map = {name: idx for idx, name in enumerate(self.class_names)}
356
+
357
+ result = {}
358
+ for split, split_dir in splits.items():
359
+ result[split] = self._convert_split_dir(split_dir, split)
360
+ self.converted_data = result
361
+
362
+ yaml_lines.append('train: train/images')
363
+ yaml_lines.append('val: valid/images')
364
+ if 'test' in splits:
365
+ yaml_lines.append('test: test/images')
366
+ else:
367
+ self._validate_splits([], [])
368
+ self.class_names = self.get_all_classes([self.root_dir], self.dm_version)
369
+ self.class_map = {name: idx for idx, name in enumerate(self.class_names)}
370
+
371
+ result = self._convert_root_dir()
372
+ self.converted_data = result
373
+
374
+ yaml_lines.append('train: images')
375
+ yaml_lines.append('val: images')
376
+
377
+ yaml_lines.extend(['', f'nc: {len(self.class_names)}', f'names: {self.class_names}', ''])
378
+ self.dataset_yaml_content = '\n'.join(yaml_lines)
379
+
380
+ return self.converted_data
381
+
382
+ def save_to_folder(self, output_dir: str | Path | None = None) -> None:
383
+ """Save converted YOLO data to folder.
384
+
385
+ Args:
386
+ output_dir: Output directory. Defaults to root_dir.
387
+ """
388
+ output_dir = Path(output_dir) if output_dir else self.root_dir
389
+ self.ensure_dir(output_dir)
390
+
391
+ if self.converted_data is None:
392
+ self.converted_data = self.convert()
393
+
394
+ if self.is_categorized:
395
+ for split, entries in self.converted_data.items():
396
+ split_imgs = self.ensure_dir(output_dir / split / 'images')
397
+ split_labels = self.ensure_dir(output_dir / split / 'labels')
398
+
399
+ for entry in entries:
400
+ shutil.copy(entry['img_path'], split_imgs / entry['img_name'])
401
+ (split_labels / entry['label_name']).write_text('\n'.join(entry['label_lines']))
402
+ else:
403
+ imgs_dir = self.ensure_dir(output_dir / 'images')
404
+ labels_dir = self.ensure_dir(output_dir / 'labels')
405
+
406
+ for entry in self.converted_data:
407
+ shutil.copy(entry['img_path'], imgs_dir / entry['img_name'])
408
+ (labels_dir / entry['label_name']).write_text('\n'.join(entry['label_lines']))
409
+
410
+ # Write dataset.yaml and classes.txt
411
+ (output_dir / 'dataset.yaml').write_text(self.dataset_yaml_content)
412
+ (output_dir / 'classes.txt').write_text('\n'.join(self.class_names) + '\n')
413
+
414
+ def convert_single_file(
415
+ self,
416
+ data: dict[str, Any],
417
+ original_file: IO,
418
+ class_names: list[str] | None = None,
419
+ ) -> dict[str, Any]:
420
+ """Convert a single DM JSON and image to YOLO format.
421
+
422
+ Args:
423
+ data: DM format JSON data.
424
+ original_file: Image file object.
425
+ class_names: Optional class names. If not provided, extracted from data.
426
+
427
+ Returns:
428
+ Dictionary with label_lines, class_names, class_map.
429
+ """
430
+ if not self.is_single_conversion:
431
+ raise RuntimeError('convert_single_file only available when is_single_conversion=True')
432
+
433
+ # Extract class names if not provided
434
+ if class_names is None:
435
+ class_names = []
436
+ classes: set[str] = set()
437
+
438
+ if self.dm_version == DMVersion.V2:
439
+ for img_ann in data.get('images', []):
440
+ for key in ['bounding_box', 'polygon', 'keypoint']:
441
+ for ann in img_ann.get(key, []):
442
+ if 'classification' in ann:
443
+ classes.add(ann['classification'])
444
+ else:
445
+ for anns in data.get('annotations', {}).values():
446
+ for ann in anns:
447
+ classification = ann.get('classification', {})
448
+ if isinstance(classification, dict):
449
+ for val in classification.values():
450
+ if isinstance(val, str):
451
+ classes.add(val)
452
+
453
+ class_names = sorted(classes)
454
+
455
+ self.class_names = class_names
456
+ self.class_map = {name: idx for idx, name in enumerate(class_names)}
457
+
458
+ width, height = self.get_image_size(original_file)
459
+ label_lines = self._convert_dm_json_to_yolo_lines(data, width, height)
460
+
461
+ return {
462
+ 'label_lines': label_lines,
463
+ 'class_names': class_names,
464
+ 'class_map': self.class_map,
465
+ }
466
+
467
+
468
+ __all__ = ['FromDMToYOLOConverter']