cudag 0.3.10__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.
- cudag/__init__.py +334 -0
- cudag/annotation/__init__.py +77 -0
- cudag/annotation/codegen.py +648 -0
- cudag/annotation/config.py +545 -0
- cudag/annotation/loader.py +342 -0
- cudag/annotation/scaffold.py +121 -0
- cudag/annotation/transcription.py +296 -0
- cudag/cli/__init__.py +5 -0
- cudag/cli/main.py +315 -0
- cudag/cli/new.py +873 -0
- cudag/core/__init__.py +364 -0
- cudag/core/button.py +137 -0
- cudag/core/canvas.py +222 -0
- cudag/core/config.py +70 -0
- cudag/core/coords.py +233 -0
- cudag/core/data_grid.py +804 -0
- cudag/core/dataset.py +678 -0
- cudag/core/distribution.py +136 -0
- cudag/core/drawing.py +75 -0
- cudag/core/fonts.py +156 -0
- cudag/core/generator.py +163 -0
- cudag/core/grid.py +367 -0
- cudag/core/grounding_task.py +247 -0
- cudag/core/icon.py +207 -0
- cudag/core/iconlist_task.py +301 -0
- cudag/core/models.py +1251 -0
- cudag/core/random.py +130 -0
- cudag/core/renderer.py +190 -0
- cudag/core/screen.py +402 -0
- cudag/core/scroll_task.py +254 -0
- cudag/core/scrollable_grid.py +447 -0
- cudag/core/state.py +110 -0
- cudag/core/task.py +293 -0
- cudag/core/taskbar.py +350 -0
- cudag/core/text.py +212 -0
- cudag/core/utils.py +82 -0
- cudag/data/surnames.txt +5000 -0
- cudag/modal_apps/__init__.py +4 -0
- cudag/modal_apps/archive.py +103 -0
- cudag/modal_apps/extract.py +138 -0
- cudag/modal_apps/preprocess.py +529 -0
- cudag/modal_apps/upload.py +317 -0
- cudag/prompts/SYSTEM_PROMPT.txt +104 -0
- cudag/prompts/__init__.py +33 -0
- cudag/prompts/system.py +43 -0
- cudag/prompts/tools.py +382 -0
- cudag/py.typed +0 -0
- cudag/schemas/filesystem.json +90 -0
- cudag/schemas/test_record.schema.json +113 -0
- cudag/schemas/train_record.schema.json +90 -0
- cudag/server/__init__.py +21 -0
- cudag/server/app.py +232 -0
- cudag/server/services/__init__.py +9 -0
- cudag/server/services/generator.py +128 -0
- cudag/templates/scripts/archive.sh +35 -0
- cudag/templates/scripts/build.sh +13 -0
- cudag/templates/scripts/extract.sh +54 -0
- cudag/templates/scripts/generate.sh +116 -0
- cudag/templates/scripts/pre-commit.sh +44 -0
- cudag/templates/scripts/preprocess.sh +46 -0
- cudag/templates/scripts/upload.sh +63 -0
- cudag/templates/scripts/verify.py +428 -0
- cudag/validation/__init__.py +35 -0
- cudag/validation/validate.py +508 -0
- cudag-0.3.10.dist-info/METADATA +570 -0
- cudag-0.3.10.dist-info/RECORD +69 -0
- cudag-0.3.10.dist-info/WHEEL +4 -0
- cudag-0.3.10.dist-info/entry_points.txt +2 -0
- cudag-0.3.10.dist-info/licenses/LICENSE +66 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Copyright (c) 2025 Tylt LLC. All rights reserved.
|
|
2
|
+
# CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
|
|
3
|
+
# is strictly prohibited. For licensing inquiries: hello@claimhawk.app
|
|
4
|
+
|
|
5
|
+
"""Annotation loader for parsing Annotator exports."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import zipfile
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, BinaryIO
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ParsedElement:
|
|
20
|
+
"""Parsed UI element from annotation."""
|
|
21
|
+
|
|
22
|
+
id: str
|
|
23
|
+
element_type: str
|
|
24
|
+
bounds: tuple[int, int, int, int] # x, y, width, height
|
|
25
|
+
label: str | None = None
|
|
26
|
+
|
|
27
|
+
# Grid properties
|
|
28
|
+
rows: int | None = None
|
|
29
|
+
cols: int | None = None
|
|
30
|
+
row_heights: list[float] | None = None
|
|
31
|
+
col_widths: list[float] | None = None
|
|
32
|
+
|
|
33
|
+
# Mask properties
|
|
34
|
+
mask: bool = True
|
|
35
|
+
mask_color: str | None = None
|
|
36
|
+
|
|
37
|
+
# Export properties
|
|
38
|
+
export_icon: bool = False
|
|
39
|
+
|
|
40
|
+
# Text properties
|
|
41
|
+
text: str | None = None
|
|
42
|
+
text_align: str | None = None
|
|
43
|
+
|
|
44
|
+
# Computed names for code generation
|
|
45
|
+
python_name: str = ""
|
|
46
|
+
region_type: str = ""
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
"""Compute derived fields."""
|
|
50
|
+
if not self.python_name:
|
|
51
|
+
self.python_name = self._to_snake_case(self.label or self.id)
|
|
52
|
+
if not self.region_type:
|
|
53
|
+
self.region_type = self._map_region_type()
|
|
54
|
+
|
|
55
|
+
def _to_snake_case(self, name: str) -> str:
|
|
56
|
+
"""Convert name to valid Python identifier."""
|
|
57
|
+
# Remove non-alphanumeric characters
|
|
58
|
+
clean = re.sub(r"[^a-zA-Z0-9]", "_", name)
|
|
59
|
+
# Convert camelCase to snake_case
|
|
60
|
+
snake = re.sub(r"([a-z])([A-Z])", r"\1_\2", clean).lower()
|
|
61
|
+
# Remove consecutive underscores
|
|
62
|
+
snake = re.sub(r"_+", "_", snake)
|
|
63
|
+
# Ensure it starts with a letter
|
|
64
|
+
if snake and snake[0].isdigit():
|
|
65
|
+
snake = "el_" + snake
|
|
66
|
+
return snake or "unnamed"
|
|
67
|
+
|
|
68
|
+
def _map_region_type(self) -> str:
|
|
69
|
+
"""Map element type to CUDAG region type."""
|
|
70
|
+
type_mapping = {
|
|
71
|
+
"button": "button",
|
|
72
|
+
"link": "button",
|
|
73
|
+
"tab": "button",
|
|
74
|
+
"menuitem": "button",
|
|
75
|
+
"checkbox": "button",
|
|
76
|
+
"radio": "button",
|
|
77
|
+
"textinput": "region",
|
|
78
|
+
"dropdown": "dropdown",
|
|
79
|
+
"listbox": "dropdown",
|
|
80
|
+
"grid": "grid",
|
|
81
|
+
"icon": "grid",
|
|
82
|
+
"scrollbar": "scrollable",
|
|
83
|
+
"panel": "region",
|
|
84
|
+
"dialog": "region",
|
|
85
|
+
"toolbar": "region",
|
|
86
|
+
"menubar": "region",
|
|
87
|
+
"text": "region",
|
|
88
|
+
"mask": "region",
|
|
89
|
+
"image": "region",
|
|
90
|
+
}
|
|
91
|
+
return type_mapping.get(self.element_type, "region")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ParsedTask:
|
|
96
|
+
"""Parsed task from annotation."""
|
|
97
|
+
|
|
98
|
+
id: str
|
|
99
|
+
prompt: str
|
|
100
|
+
target_element_id: str | None = None
|
|
101
|
+
action: str = "left_click"
|
|
102
|
+
action_params: dict[str, Any] = field(default_factory=dict)
|
|
103
|
+
prior_states: list[dict[str, Any]] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
# Computed names for code generation
|
|
106
|
+
class_name: str = ""
|
|
107
|
+
python_name: str = ""
|
|
108
|
+
task_type: str = ""
|
|
109
|
+
|
|
110
|
+
def __post_init__(self) -> None:
|
|
111
|
+
"""Compute derived fields."""
|
|
112
|
+
if not self.python_name:
|
|
113
|
+
self.python_name = self._derive_python_name()
|
|
114
|
+
if not self.class_name:
|
|
115
|
+
self.class_name = self._derive_class_name()
|
|
116
|
+
if not self.task_type:
|
|
117
|
+
self.task_type = self._derive_task_type()
|
|
118
|
+
|
|
119
|
+
def _derive_python_name(self) -> str:
|
|
120
|
+
"""Derive Python identifier from task."""
|
|
121
|
+
# Try to create name from action and target
|
|
122
|
+
base = f"{self.action}_{self.target_element_id or 'element'}"
|
|
123
|
+
# Clean up
|
|
124
|
+
clean = re.sub(r"[^a-zA-Z0-9]", "_", base).lower()
|
|
125
|
+
clean = re.sub(r"_+", "_", clean)
|
|
126
|
+
return clean or "task"
|
|
127
|
+
|
|
128
|
+
def _derive_class_name(self) -> str:
|
|
129
|
+
"""Derive class name from task."""
|
|
130
|
+
# Convert python_name to PascalCase
|
|
131
|
+
parts = self.python_name.split("_")
|
|
132
|
+
pascal = "".join(p.capitalize() for p in parts if p)
|
|
133
|
+
return f"{pascal}Task"
|
|
134
|
+
|
|
135
|
+
def _derive_task_type(self) -> str:
|
|
136
|
+
"""Derive task type identifier."""
|
|
137
|
+
return self.python_name.replace("_", "-")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class ParsedAnnotation:
|
|
142
|
+
"""Fully parsed annotation data."""
|
|
143
|
+
|
|
144
|
+
screen_name: str
|
|
145
|
+
image_size: tuple[int, int]
|
|
146
|
+
elements: list[ParsedElement]
|
|
147
|
+
tasks: list[ParsedTask]
|
|
148
|
+
image_path: str = ""
|
|
149
|
+
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
151
|
+
"""Convert back to annotation.json format."""
|
|
152
|
+
return {
|
|
153
|
+
"screenName": self.screen_name,
|
|
154
|
+
"imageSize": list(self.image_size),
|
|
155
|
+
"imagePath": self.image_path,
|
|
156
|
+
"elements": [self._element_to_dict(el) for el in self.elements],
|
|
157
|
+
"tasks": [self._task_to_dict(t) for t in self.tasks],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _element_to_dict(self, el: ParsedElement) -> dict[str, Any]:
|
|
161
|
+
"""Convert element back to dict format."""
|
|
162
|
+
result: dict[str, Any] = {
|
|
163
|
+
"id": el.id,
|
|
164
|
+
"type": el.element_type,
|
|
165
|
+
"bbox": {
|
|
166
|
+
"x": el.bounds[0],
|
|
167
|
+
"y": el.bounds[1],
|
|
168
|
+
"width": el.bounds[2],
|
|
169
|
+
"height": el.bounds[3],
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
if el.label:
|
|
173
|
+
result["text"] = el.label
|
|
174
|
+
if el.rows:
|
|
175
|
+
result["rows"] = el.rows
|
|
176
|
+
if el.cols:
|
|
177
|
+
result["cols"] = el.cols
|
|
178
|
+
if el.row_heights:
|
|
179
|
+
result["rowHeights"] = el.row_heights
|
|
180
|
+
if el.col_widths:
|
|
181
|
+
result["colWidths"] = el.col_widths
|
|
182
|
+
if el.mask_color:
|
|
183
|
+
result["maskColor"] = el.mask_color
|
|
184
|
+
if el.text_align:
|
|
185
|
+
result["textAlign"] = el.text_align
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
def _task_to_dict(self, t: ParsedTask) -> dict[str, Any]:
|
|
189
|
+
"""Convert task back to dict format."""
|
|
190
|
+
result: dict[str, Any] = {
|
|
191
|
+
"id": t.id,
|
|
192
|
+
"prompt": t.prompt,
|
|
193
|
+
"action": t.action,
|
|
194
|
+
}
|
|
195
|
+
if t.target_element_id:
|
|
196
|
+
result["targetElementId"] = t.target_element_id
|
|
197
|
+
if t.task_type:
|
|
198
|
+
result["taskType"] = t.task_type
|
|
199
|
+
if t.prior_states:
|
|
200
|
+
result["priorStates"] = t.prior_states
|
|
201
|
+
# Add action params
|
|
202
|
+
if t.action == "type" and "text" in t.action_params:
|
|
203
|
+
result["text"] = t.action_params["text"]
|
|
204
|
+
elif t.action == "key" and "keys" in t.action_params:
|
|
205
|
+
result["keys"] = t.action_params["keys"]
|
|
206
|
+
elif t.action == "scroll" and "pixels" in t.action_params:
|
|
207
|
+
result["scrollPixels"] = t.action_params["pixels"]
|
|
208
|
+
elif t.action == "wait" and "ms" in t.action_params:
|
|
209
|
+
result["waitMs"] = t.action_params["ms"]
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class AnnotationLoader:
|
|
214
|
+
"""Load and parse annotation data from various sources."""
|
|
215
|
+
|
|
216
|
+
def load(self, path: Path | str | BinaryIO) -> ParsedAnnotation:
|
|
217
|
+
"""Load annotation from a file, folder, or stream.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
path: Path to annotation.json, annotation.zip, annotation folder,
|
|
221
|
+
or a file-like object
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Parsed annotation data
|
|
225
|
+
"""
|
|
226
|
+
if isinstance(path, (str, Path)):
|
|
227
|
+
path = Path(path)
|
|
228
|
+
if path.is_dir():
|
|
229
|
+
return self._load_folder(path)
|
|
230
|
+
elif path.suffix == ".zip":
|
|
231
|
+
return self._load_zip(path)
|
|
232
|
+
else:
|
|
233
|
+
with open(path) as f:
|
|
234
|
+
data = json.load(f)
|
|
235
|
+
return self.parse_dict(data)
|
|
236
|
+
else:
|
|
237
|
+
# File-like object - assume ZIP
|
|
238
|
+
return self._load_zip_stream(path)
|
|
239
|
+
|
|
240
|
+
def _load_folder(self, path: Path) -> ParsedAnnotation:
|
|
241
|
+
"""Load annotation from unpacked folder."""
|
|
242
|
+
annotation_file = path / "annotation.json"
|
|
243
|
+
if not annotation_file.exists():
|
|
244
|
+
raise FileNotFoundError(f"No annotation.json found in {path}")
|
|
245
|
+
with open(annotation_file) as f:
|
|
246
|
+
data = json.load(f)
|
|
247
|
+
return self.parse_dict(data)
|
|
248
|
+
|
|
249
|
+
def _load_zip(self, path: Path) -> ParsedAnnotation:
|
|
250
|
+
"""Load annotation from ZIP file."""
|
|
251
|
+
with zipfile.ZipFile(path) as zf:
|
|
252
|
+
return self._parse_zip(zf)
|
|
253
|
+
|
|
254
|
+
def _load_zip_stream(self, stream: BinaryIO) -> ParsedAnnotation:
|
|
255
|
+
"""Load annotation from ZIP stream."""
|
|
256
|
+
with zipfile.ZipFile(stream) as zf:
|
|
257
|
+
return self._parse_zip(zf)
|
|
258
|
+
|
|
259
|
+
def _parse_zip(self, zf: zipfile.ZipFile) -> ParsedAnnotation:
|
|
260
|
+
"""Parse annotation from opened ZIP file."""
|
|
261
|
+
annotation_file = zf.read("annotation.json")
|
|
262
|
+
data = json.loads(annotation_file.decode("utf-8"))
|
|
263
|
+
return self.parse_dict(data)
|
|
264
|
+
|
|
265
|
+
def parse_dict(self, data: dict[str, Any]) -> ParsedAnnotation:
|
|
266
|
+
"""Parse annotation from dictionary.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
data: Raw annotation dictionary
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Parsed annotation data
|
|
273
|
+
"""
|
|
274
|
+
elements = [self._parse_element(el) for el in data.get("elements", [])]
|
|
275
|
+
tasks = [self._parse_task(t, elements) for t in data.get("tasks", [])]
|
|
276
|
+
|
|
277
|
+
return ParsedAnnotation(
|
|
278
|
+
screen_name=self._sanitize_name(data.get("screenName", "untitled")),
|
|
279
|
+
image_size=tuple(data.get("imageSize", [1000, 1000])),
|
|
280
|
+
elements=elements,
|
|
281
|
+
tasks=tasks,
|
|
282
|
+
image_path=data.get("imagePath", ""),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _parse_element(self, el: dict[str, Any]) -> ParsedElement:
|
|
286
|
+
"""Parse a single element."""
|
|
287
|
+
bbox = el.get("bbox", {})
|
|
288
|
+
return ParsedElement(
|
|
289
|
+
id=el.get("id", ""),
|
|
290
|
+
element_type=el.get("type", "button"),
|
|
291
|
+
bounds=(
|
|
292
|
+
bbox.get("x", 0),
|
|
293
|
+
bbox.get("y", 0),
|
|
294
|
+
bbox.get("width", 0),
|
|
295
|
+
bbox.get("height", 0),
|
|
296
|
+
),
|
|
297
|
+
label=el.get("text"),
|
|
298
|
+
rows=el.get("rows"),
|
|
299
|
+
cols=el.get("cols"),
|
|
300
|
+
row_heights=el.get("rowHeights"),
|
|
301
|
+
col_widths=el.get("colWidths"),
|
|
302
|
+
mask=el.get("mask", True),
|
|
303
|
+
mask_color=el.get("maskColor"),
|
|
304
|
+
export_icon=el.get("exportIcon", False),
|
|
305
|
+
text=el.get("text"),
|
|
306
|
+
text_align=el.get("textAlign"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def _parse_task(
|
|
310
|
+
self, t: dict[str, Any], elements: list[ParsedElement]
|
|
311
|
+
) -> ParsedTask:
|
|
312
|
+
"""Parse a single task."""
|
|
313
|
+
action = t.get("action", "left_click")
|
|
314
|
+
action_params: dict[str, Any] = {}
|
|
315
|
+
|
|
316
|
+
# Extract action-specific parameters
|
|
317
|
+
if action == "type":
|
|
318
|
+
action_params["text"] = t.get("text", "")
|
|
319
|
+
elif action == "key":
|
|
320
|
+
action_params["keys"] = t.get("keys", [])
|
|
321
|
+
elif action == "scroll":
|
|
322
|
+
action_params["pixels"] = t.get("scrollPixels", 100)
|
|
323
|
+
elif action == "wait":
|
|
324
|
+
action_params["ms"] = t.get("waitMs", 1000)
|
|
325
|
+
elif action in ("drag_to", "move_to"):
|
|
326
|
+
action_params["end_x"] = t.get("endX", 0)
|
|
327
|
+
action_params["end_y"] = t.get("endY", 0)
|
|
328
|
+
|
|
329
|
+
return ParsedTask(
|
|
330
|
+
id=t.get("id", ""),
|
|
331
|
+
prompt=t.get("prompt", ""),
|
|
332
|
+
target_element_id=t.get("targetElementId"),
|
|
333
|
+
action=action,
|
|
334
|
+
action_params=action_params,
|
|
335
|
+
prior_states=t.get("priorStates", []),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _sanitize_name(self, name: str) -> str:
|
|
339
|
+
"""Sanitize a name for use as a Python identifier."""
|
|
340
|
+
clean = re.sub(r"[^a-zA-Z0-9]", "_", name).lower()
|
|
341
|
+
clean = re.sub(r"_+", "_", clean).strip("_")
|
|
342
|
+
return clean or "untitled"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright (c) 2025 Tylt LLC. All rights reserved.
|
|
2
|
+
# CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
|
|
3
|
+
# is strictly prohibited. For licensing inquiries: hello@claimhawk.app
|
|
4
|
+
|
|
5
|
+
"""Generator scaffolding from parsed annotations."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from cudag.annotation.loader import ParsedAnnotation
|
|
13
|
+
from cudag.annotation.codegen import (
|
|
14
|
+
generate_screen_py,
|
|
15
|
+
generate_state_py,
|
|
16
|
+
generate_renderer_py,
|
|
17
|
+
generate_generator_py,
|
|
18
|
+
generate_task_py,
|
|
19
|
+
generate_tasks_init_py,
|
|
20
|
+
generate_config_yaml,
|
|
21
|
+
generate_pyproject_toml,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def scaffold_generator(
|
|
26
|
+
name: str,
|
|
27
|
+
annotation: ParsedAnnotation,
|
|
28
|
+
output_dir: Path,
|
|
29
|
+
original_image: bytes | None = None,
|
|
30
|
+
masked_image: bytes | None = None,
|
|
31
|
+
icons: dict[str, bytes] | None = None,
|
|
32
|
+
in_place: bool = False,
|
|
33
|
+
) -> list[Path]:
|
|
34
|
+
"""Scaffold a complete CUDAG generator project from annotation.
|
|
35
|
+
|
|
36
|
+
Uses annotation-driven architecture where annotation.json is loaded at
|
|
37
|
+
runtime via AnnotationConfig, enabling updates without regenerating code.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name: Project name (used for directory name)
|
|
41
|
+
annotation: Parsed annotation data
|
|
42
|
+
output_dir: Parent directory for the project
|
|
43
|
+
original_image: Original screenshot bytes
|
|
44
|
+
masked_image: Masked image bytes (with dynamic regions blanked)
|
|
45
|
+
icons: Map of icon names to image bytes (optional)
|
|
46
|
+
in_place: If True, write directly to output_dir without creating subdirectory
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of created file paths
|
|
50
|
+
"""
|
|
51
|
+
project_dir = output_dir if in_place else output_dir / name
|
|
52
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
created_files: list[Path] = []
|
|
55
|
+
|
|
56
|
+
# Create directory structure (annotation-driven layout)
|
|
57
|
+
(project_dir / "tasks").mkdir(exist_ok=True)
|
|
58
|
+
(project_dir / "assets" / "annotations").mkdir(parents=True, exist_ok=True)
|
|
59
|
+
(project_dir / "assets" / "icons").mkdir(exist_ok=True)
|
|
60
|
+
(project_dir / "config").mkdir(exist_ok=True)
|
|
61
|
+
|
|
62
|
+
# Save annotation.json (single source of truth)
|
|
63
|
+
annotation_json = project_dir / "assets" / "annotations" / "annotation.json"
|
|
64
|
+
annotation_json.write_text(json.dumps(annotation.to_dict(), indent=2))
|
|
65
|
+
created_files.append(annotation_json)
|
|
66
|
+
|
|
67
|
+
# Generate Python files
|
|
68
|
+
screen_py = project_dir / "screen.py"
|
|
69
|
+
screen_py.write_text(generate_screen_py(annotation))
|
|
70
|
+
created_files.append(screen_py)
|
|
71
|
+
|
|
72
|
+
state_py = project_dir / "state.py"
|
|
73
|
+
state_py.write_text(generate_state_py(annotation))
|
|
74
|
+
created_files.append(state_py)
|
|
75
|
+
|
|
76
|
+
renderer_py = project_dir / "renderer.py"
|
|
77
|
+
renderer_py.write_text(generate_renderer_py(annotation))
|
|
78
|
+
created_files.append(renderer_py)
|
|
79
|
+
|
|
80
|
+
generator_py = project_dir / "generator.py"
|
|
81
|
+
generator_py.write_text(generate_generator_py(annotation))
|
|
82
|
+
created_files.append(generator_py)
|
|
83
|
+
|
|
84
|
+
# Generate task files
|
|
85
|
+
tasks_init = project_dir / "tasks" / "__init__.py"
|
|
86
|
+
tasks_init.write_text(generate_tasks_init_py(annotation.tasks))
|
|
87
|
+
created_files.append(tasks_init)
|
|
88
|
+
|
|
89
|
+
for task in annotation.tasks:
|
|
90
|
+
task_file = project_dir / "tasks" / f"{task.python_name}.py"
|
|
91
|
+
task_file.write_text(generate_task_py(task, annotation))
|
|
92
|
+
created_files.append(task_file)
|
|
93
|
+
|
|
94
|
+
# Generate config
|
|
95
|
+
config_yaml = project_dir / "config" / "dataset.yaml"
|
|
96
|
+
config_yaml.write_text(generate_config_yaml(annotation))
|
|
97
|
+
created_files.append(config_yaml)
|
|
98
|
+
|
|
99
|
+
# Generate pyproject.toml
|
|
100
|
+
pyproject = project_dir / "pyproject.toml"
|
|
101
|
+
pyproject.write_text(generate_pyproject_toml(name))
|
|
102
|
+
created_files.append(pyproject)
|
|
103
|
+
|
|
104
|
+
# Save images to annotations directory
|
|
105
|
+
if original_image:
|
|
106
|
+
original_png = project_dir / "assets" / "annotations" / "original.png"
|
|
107
|
+
original_png.write_bytes(original_image)
|
|
108
|
+
created_files.append(original_png)
|
|
109
|
+
|
|
110
|
+
if masked_image:
|
|
111
|
+
masked_png = project_dir / "assets" / "annotations" / "masked.png"
|
|
112
|
+
masked_png.write_bytes(masked_image)
|
|
113
|
+
created_files.append(masked_png)
|
|
114
|
+
|
|
115
|
+
if icons:
|
|
116
|
+
for icon_name, icon_bytes in icons.items():
|
|
117
|
+
icon_path = project_dir / "assets" / "icons" / f"{icon_name}.png"
|
|
118
|
+
icon_path.write_bytes(icon_bytes)
|
|
119
|
+
created_files.append(icon_path)
|
|
120
|
+
|
|
121
|
+
return created_files
|