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.
Files changed (69) hide show
  1. cudag/__init__.py +334 -0
  2. cudag/annotation/__init__.py +77 -0
  3. cudag/annotation/codegen.py +648 -0
  4. cudag/annotation/config.py +545 -0
  5. cudag/annotation/loader.py +342 -0
  6. cudag/annotation/scaffold.py +121 -0
  7. cudag/annotation/transcription.py +296 -0
  8. cudag/cli/__init__.py +5 -0
  9. cudag/cli/main.py +315 -0
  10. cudag/cli/new.py +873 -0
  11. cudag/core/__init__.py +364 -0
  12. cudag/core/button.py +137 -0
  13. cudag/core/canvas.py +222 -0
  14. cudag/core/config.py +70 -0
  15. cudag/core/coords.py +233 -0
  16. cudag/core/data_grid.py +804 -0
  17. cudag/core/dataset.py +678 -0
  18. cudag/core/distribution.py +136 -0
  19. cudag/core/drawing.py +75 -0
  20. cudag/core/fonts.py +156 -0
  21. cudag/core/generator.py +163 -0
  22. cudag/core/grid.py +367 -0
  23. cudag/core/grounding_task.py +247 -0
  24. cudag/core/icon.py +207 -0
  25. cudag/core/iconlist_task.py +301 -0
  26. cudag/core/models.py +1251 -0
  27. cudag/core/random.py +130 -0
  28. cudag/core/renderer.py +190 -0
  29. cudag/core/screen.py +402 -0
  30. cudag/core/scroll_task.py +254 -0
  31. cudag/core/scrollable_grid.py +447 -0
  32. cudag/core/state.py +110 -0
  33. cudag/core/task.py +293 -0
  34. cudag/core/taskbar.py +350 -0
  35. cudag/core/text.py +212 -0
  36. cudag/core/utils.py +82 -0
  37. cudag/data/surnames.txt +5000 -0
  38. cudag/modal_apps/__init__.py +4 -0
  39. cudag/modal_apps/archive.py +103 -0
  40. cudag/modal_apps/extract.py +138 -0
  41. cudag/modal_apps/preprocess.py +529 -0
  42. cudag/modal_apps/upload.py +317 -0
  43. cudag/prompts/SYSTEM_PROMPT.txt +104 -0
  44. cudag/prompts/__init__.py +33 -0
  45. cudag/prompts/system.py +43 -0
  46. cudag/prompts/tools.py +382 -0
  47. cudag/py.typed +0 -0
  48. cudag/schemas/filesystem.json +90 -0
  49. cudag/schemas/test_record.schema.json +113 -0
  50. cudag/schemas/train_record.schema.json +90 -0
  51. cudag/server/__init__.py +21 -0
  52. cudag/server/app.py +232 -0
  53. cudag/server/services/__init__.py +9 -0
  54. cudag/server/services/generator.py +128 -0
  55. cudag/templates/scripts/archive.sh +35 -0
  56. cudag/templates/scripts/build.sh +13 -0
  57. cudag/templates/scripts/extract.sh +54 -0
  58. cudag/templates/scripts/generate.sh +116 -0
  59. cudag/templates/scripts/pre-commit.sh +44 -0
  60. cudag/templates/scripts/preprocess.sh +46 -0
  61. cudag/templates/scripts/upload.sh +63 -0
  62. cudag/templates/scripts/verify.py +428 -0
  63. cudag/validation/__init__.py +35 -0
  64. cudag/validation/validate.py +508 -0
  65. cudag-0.3.10.dist-info/METADATA +570 -0
  66. cudag-0.3.10.dist-info/RECORD +69 -0
  67. cudag-0.3.10.dist-info/WHEEL +4 -0
  68. cudag-0.3.10.dist-info/entry_points.txt +2 -0
  69. cudag-0.3.10.dist-info/licenses/LICENSE +66 -0
@@ -0,0 +1,648 @@
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
+ """Code generation utilities for scaffolding CUDAG projects.
6
+
7
+ Generates annotation-driven code that loads annotation.json at runtime
8
+ via AnnotationConfig, enabling UI updates without regenerating code.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from cudag.annotation.loader import ParsedAnnotation, ParsedElement, ParsedTask
14
+
15
+ COPYRIGHT_HEADER = '''# Auto-generated from annotation - feel free to modify.
16
+ '''
17
+
18
+
19
+ def generate_screen_py(annotation: ParsedAnnotation) -> str:
20
+ """Generate screen.py with runtime annotation loading."""
21
+ # Identify element types present
22
+ has_grid = any(el.region_type == "grid" for el in annotation.elements)
23
+ has_buttons = any(el.region_type == "button" for el in annotation.elements)
24
+ has_text = any(el.element_type == "text" for el in annotation.elements)
25
+
26
+ # Build helper functions based on element types
27
+ helpers: list[str] = []
28
+
29
+ if has_grid:
30
+ helpers.append(_generate_grid_helpers())
31
+
32
+ if has_buttons:
33
+ helpers.append(_generate_button_helpers())
34
+
35
+ if has_text:
36
+ helpers.append(_generate_text_helpers())
37
+
38
+ # Always include image path helper
39
+ helpers.append(_generate_image_helpers())
40
+
41
+ helpers_str = "\n\n".join(helpers)
42
+
43
+ return f'''{COPYRIGHT_HEADER}
44
+ """Screen definition for {annotation.screen_name}.
45
+
46
+ All UI data comes from the annotator (assets/annotations/annotation.json).
47
+ The generator only handles business logic (state randomization, valid click targets).
48
+
49
+ Data from annotation:
50
+ - Element positions, bboxes, tolerances
51
+ - Task prompts/templates
52
+ - Masked base image
53
+
54
+ Coordinate scaling:
55
+ - Annotation was made on {annotation.image_size} image
56
+ - Generator may produce different size images
57
+ - All coordinates are scaled at load time
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ from pathlib import Path
63
+
64
+ from cudag.annotation import AnnotatedElement, AnnotatedTask, AnnotationConfig
65
+
66
+
67
+ # -----------------------------------------------------------------------------
68
+ # Load Annotation (Single Source of Truth)
69
+ # -----------------------------------------------------------------------------
70
+
71
+ _ANNOTATIONS_DIR = Path(__file__).parent / "assets" / "annotations"
72
+
73
+ if not _ANNOTATIONS_DIR.exists():
74
+ raise FileNotFoundError(f"Annotations directory not found: {{_ANNOTATIONS_DIR}}")
75
+
76
+ ANNOTATION_CONFIG = AnnotationConfig.load(_ANNOTATIONS_DIR)
77
+
78
+
79
+ # -----------------------------------------------------------------------------
80
+ # Coordinate Scaling
81
+ # -----------------------------------------------------------------------------
82
+
83
+ _ANNOTATION_SIZE = ANNOTATION_CONFIG.image_size # {annotation.image_size}
84
+
85
+ # Output size - override these if generating different size images
86
+ IMAGE_WIDTH, IMAGE_HEIGHT = _ANNOTATION_SIZE
87
+ _GENERATOR_SIZE = (IMAGE_WIDTH, IMAGE_HEIGHT)
88
+
89
+ _SCALE_X = _GENERATOR_SIZE[0] / _ANNOTATION_SIZE[0]
90
+ _SCALE_Y = _GENERATOR_SIZE[1] / _ANNOTATION_SIZE[1]
91
+
92
+
93
+ def scale_coord(x: int | float, y: int | float) -> tuple[int, int]:
94
+ """Scale coordinates from annotation space to generator space."""
95
+ return (int(x * _SCALE_X), int(y * _SCALE_Y))
96
+
97
+
98
+ def scale_bbox(bbox: tuple[int, int, int, int]) -> tuple[int, int, int, int]:
99
+ """Scale bbox (x, y, width, height) from annotation to generator space."""
100
+ x, y, w, h = bbox
101
+ return (int(x * _SCALE_X), int(y * _SCALE_Y), int(w * _SCALE_X), int(h * _SCALE_Y))
102
+
103
+
104
+ def scale_tolerance(tol_x: int, tol_y: int) -> tuple[int, int]:
105
+ """Scale tolerance values from annotation to generator space."""
106
+ return (int(tol_x * _SCALE_X), int(tol_y * _SCALE_Y))
107
+
108
+
109
+ {helpers_str}
110
+ '''
111
+
112
+
113
+ def _generate_grid_helpers() -> str:
114
+ """Generate helper functions for grid elements."""
115
+ return '''# -----------------------------------------------------------------------------
116
+ # Grid Element Accessors
117
+ # -----------------------------------------------------------------------------
118
+
119
+ def get_grid_element(label: str = "grid") -> AnnotatedElement:
120
+ """Get a grid element by label."""
121
+ el = ANNOTATION_CONFIG.get_element_by_label(label)
122
+ if el is None:
123
+ raise ValueError(f"Grid element '{label}' not found in annotation")
124
+ return el
125
+
126
+
127
+ def get_grid_bbox(label: str = "grid") -> tuple[int, int, int, int]:
128
+ """Get scaled grid bounding box."""
129
+ return scale_bbox(get_grid_element(label).bbox)
130
+
131
+
132
+ def get_grid_dimensions(label: str = "grid") -> tuple[int, int]:
133
+ """Get grid rows and cols from annotation."""
134
+ el = get_grid_element(label)
135
+ return (el.rows, el.cols)
136
+
137
+
138
+ def get_grid_tolerance(label: str = "grid") -> tuple[int, int]:
139
+ """Get scaled grid cell tolerance."""
140
+ el = get_grid_element(label)
141
+ return scale_tolerance(el.tolerance_x, el.tolerance_y)'''
142
+
143
+
144
+ def _generate_button_helpers() -> str:
145
+ """Generate helper functions for button elements."""
146
+ return '''# -----------------------------------------------------------------------------
147
+ # Button Element Accessors
148
+ # -----------------------------------------------------------------------------
149
+
150
+ def get_button_element(name: str) -> AnnotatedElement:
151
+ """Get a button element by name."""
152
+ el = ANNOTATION_CONFIG.get_element_by_label(name)
153
+ if el is None:
154
+ raise ValueError(f"Button element '{name}' not found in annotation")
155
+ return el
156
+
157
+
158
+ def get_button_center(name: str) -> tuple[int, int]:
159
+ """Get scaled button center coordinates."""
160
+ el = get_button_element(name)
161
+ return scale_coord(el.center[0], el.center[1])
162
+
163
+
164
+ def get_button_tolerance(name: str) -> tuple[int, int]:
165
+ """Get scaled button tolerance."""
166
+ el = get_button_element(name)
167
+ return scale_tolerance(el.tolerance_x, el.tolerance_y)
168
+
169
+
170
+ def get_all_buttons() -> list[AnnotatedElement]:
171
+ """Get all button elements from annotation."""
172
+ return [el for el in ANNOTATION_CONFIG.elements if el.element_type == "button"]
173
+
174
+
175
+ def get_button_task(button_name: str) -> AnnotatedTask:
176
+ """Get the task for a specific button."""
177
+ el = get_button_element(button_name)
178
+ tasks = ANNOTATION_CONFIG.get_tasks_for_element(el.id)
179
+ if not tasks:
180
+ raise ValueError(f"No task found for button '{button_name}'")
181
+ return tasks[0]
182
+
183
+
184
+ def get_all_button_tasks() -> list[tuple[AnnotatedElement, AnnotatedTask]]:
185
+ """Get all button elements with their tasks."""
186
+ result: list[tuple[AnnotatedElement, AnnotatedTask]] = []
187
+ for el in get_all_buttons():
188
+ tasks = ANNOTATION_CONFIG.get_tasks_for_element(el.id)
189
+ if tasks:
190
+ result.append((el, tasks[0]))
191
+ return result'''
192
+
193
+
194
+ def _generate_text_helpers() -> str:
195
+ """Generate helper functions for text elements."""
196
+ return '''# -----------------------------------------------------------------------------
197
+ # Text Element Accessors
198
+ # -----------------------------------------------------------------------------
199
+
200
+ def get_text_element(name: str) -> AnnotatedElement:
201
+ """Get a text element by name."""
202
+ el = ANNOTATION_CONFIG.get_element_by_label(name)
203
+ if el is None:
204
+ raise ValueError(f"Text element '{name}' not found in annotation")
205
+ return el
206
+
207
+
208
+ def get_text_bbox(name: str) -> tuple[int, int, int, int]:
209
+ """Get scaled text element bounding box."""
210
+ el = get_text_element(name)
211
+ return scale_bbox(el.bbox)
212
+
213
+
214
+ def get_text_center(name: str) -> tuple[int, int]:
215
+ """Get scaled text element center position."""
216
+ el = get_text_element(name)
217
+ return scale_coord(el.center[0], el.center[1])'''
218
+
219
+
220
+ def _generate_image_helpers() -> str:
221
+ """Generate helper functions for image paths."""
222
+ return '''# -----------------------------------------------------------------------------
223
+ # Image Paths
224
+ # -----------------------------------------------------------------------------
225
+
226
+ def get_masked_image_path() -> Path:
227
+ """Get path to annotator's masked image."""
228
+ path = ANNOTATION_CONFIG.masked_image_path
229
+ if path is None or not path.exists():
230
+ raise FileNotFoundError("masked.png not found in annotations")
231
+ return path
232
+
233
+
234
+ def get_original_image_path() -> Path:
235
+ """Get path to original screenshot."""
236
+ path = ANNOTATION_CONFIG.original_image_path
237
+ if path is None or not path.exists():
238
+ raise FileNotFoundError("original.png not found in annotations")
239
+ return path'''
240
+
241
+
242
+ def generate_state_py(annotation: ParsedAnnotation) -> str:
243
+ """Generate state.py from annotation."""
244
+ class_name = _to_pascal_case(annotation.screen_name) + "State"
245
+
246
+ # Extract potential state fields from elements and tasks
247
+ state_fields = _extract_state_fields(annotation)
248
+ fields_str = "\n".join(f" {f}" for f in state_fields) if state_fields else " pass"
249
+
250
+ return f'''{COPYRIGHT_HEADER}
251
+ """State definition for {annotation.screen_name}."""
252
+
253
+ from dataclasses import dataclass
254
+ from random import Random
255
+ from typing import Any
256
+
257
+ from cudag import BaseState
258
+
259
+
260
+ @dataclass
261
+ class {class_name}(BaseState):
262
+ """State for rendering the screen.
263
+
264
+ Auto-generated from annotation. Add fields for dynamic content
265
+ that changes between samples (text, selections, etc.).
266
+ """
267
+
268
+ {fields_str}
269
+
270
+ @classmethod
271
+ def generate(cls, rng: Random) -> "{class_name}":
272
+ """Generate a random state for training.
273
+
274
+ Override this method to generate realistic variations
275
+ of the screen content.
276
+ """
277
+ return cls()
278
+ '''
279
+
280
+
281
+ def _extract_state_fields(annotation: ParsedAnnotation) -> list[str]:
282
+ """Extract potential state fields from annotation."""
283
+ fields: list[str] = []
284
+
285
+ # Look for text inputs
286
+ for el in annotation.elements:
287
+ if el.element_type == "textinput":
288
+ field_name = el.python_name + "_text"
289
+ fields.append(f'{field_name}: str = ""')
290
+
291
+ # Look for prior states in tasks
292
+ prior_state_fields = set()
293
+ for task in annotation.tasks:
294
+ for prior in task.prior_states:
295
+ field = prior.get("field", "")
296
+ if field:
297
+ prior_state_fields.add(field)
298
+
299
+ for field in sorted(prior_state_fields):
300
+ snake = _to_snake_case(field)
301
+ fields.append(f'{snake}: Any = None')
302
+
303
+ return fields
304
+
305
+
306
+ def generate_renderer_py(annotation: ParsedAnnotation) -> str:
307
+ """Generate renderer.py using masked.png from annotations."""
308
+ screen_class = _to_pascal_case(annotation.screen_name) + "Screen"
309
+ state_class = _to_pascal_case(annotation.screen_name) + "State"
310
+
311
+ return f'''{COPYRIGHT_HEADER}
312
+ """Renderer for {annotation.screen_name}."""
313
+
314
+ from pathlib import Path
315
+ from typing import Any
316
+
317
+ from PIL import Image
318
+
319
+ from cudag import BaseRenderer
320
+ from screen import get_masked_image_path, IMAGE_WIDTH, IMAGE_HEIGHT
321
+ from state import {state_class}
322
+
323
+
324
+ class {screen_class}Renderer(BaseRenderer[{state_class}]):
325
+ """Renderer for {annotation.screen_name} screen.
326
+
327
+ Uses masked.png from annotations as base image.
328
+ Customize the render() method to add dynamic content based on state.
329
+ """
330
+
331
+ def __init__(self) -> None:
332
+ super().__init__()
333
+ masked_path = get_masked_image_path()
334
+ self._base_image = Image.open(masked_path)
335
+ # Resize if generator uses different size than annotation
336
+ if self._base_image.size != (IMAGE_WIDTH, IMAGE_HEIGHT):
337
+ self._base_image = self._base_image.resize(
338
+ (IMAGE_WIDTH, IMAGE_HEIGHT), Image.Resampling.LANCZOS
339
+ )
340
+
341
+ def render(self, state: {state_class}) -> tuple[Image.Image, dict[str, Any]]:
342
+ """Render the screen with the given state.
343
+
344
+ Args:
345
+ state: State containing dynamic content
346
+
347
+ Returns:
348
+ Tuple of (rendered_image, metadata_dict)
349
+ """
350
+ # Start with base image
351
+ image = self._base_image.copy()
352
+
353
+ # TODO: Add dynamic rendering based on state
354
+ # Example:
355
+ # from PIL import ImageDraw
356
+ # draw = ImageDraw.Draw(image)
357
+ # draw.text((x, y), state.some_text, font=font, fill="black", anchor="mm")
358
+
359
+ metadata = {{
360
+ "screen_name": "{annotation.screen_name}",
361
+ "image_size": image.size,
362
+ }}
363
+
364
+ return image, metadata
365
+ '''
366
+
367
+
368
+ def generate_generator_py(annotation: ParsedAnnotation) -> str:
369
+ """Generate generator.py from annotation."""
370
+ screen_class = _to_pascal_case(annotation.screen_name) + "Screen"
371
+ state_class = _to_pascal_case(annotation.screen_name) + "State"
372
+ renderer_class = screen_class + "Renderer"
373
+
374
+ task_imports = []
375
+ task_registrations = []
376
+ for task in annotation.tasks:
377
+ task_imports.append(f"from tasks.{task.python_name} import {task.class_name}")
378
+ task_registrations.append(f' builder.register_task({task.class_name}(config, renderer))')
379
+
380
+ task_imports_str = "\n".join(task_imports) if task_imports else "# No tasks defined"
381
+ task_registrations_str = "\n".join(task_registrations) if task_registrations else " pass"
382
+
383
+ return f'''{COPYRIGHT_HEADER}
384
+ """Generator entry point for {annotation.screen_name}."""
385
+
386
+ import argparse
387
+ from pathlib import Path
388
+
389
+ from cudag import DatasetBuilder, DatasetConfig, run_generator, check_script_invocation
390
+
391
+ from state import {state_class}
392
+ from renderer import {renderer_class}
393
+ {task_imports_str}
394
+
395
+
396
+ def main() -> None:
397
+ """Run the generator."""
398
+ check_script_invocation(__file__)
399
+
400
+ parser = argparse.ArgumentParser(description="Generate {annotation.screen_name} dataset")
401
+ parser.add_argument("--samples", type=int, default=1000, help="Samples per task")
402
+ parser.add_argument("--output", type=str, default="datasets/{annotation.screen_name}", help="Output directory")
403
+ parser.add_argument("--seed", type=int, default=42, help="Random seed")
404
+ args = parser.parse_args()
405
+
406
+ config_path = Path(__file__).parent / "config" / "dataset.yaml"
407
+ config = DatasetConfig.from_yaml(config_path)
408
+
409
+ renderer = {renderer_class}()
410
+
411
+ builder = DatasetBuilder(
412
+ config=config,
413
+ output_dir=Path(args.output),
414
+ seed=args.seed,
415
+ )
416
+
417
+ # Register tasks
418
+ {task_registrations_str}
419
+
420
+ # Generate dataset
421
+ builder.build(samples_per_task=args.samples)
422
+
423
+
424
+ if __name__ == "__main__":
425
+ main()
426
+ '''
427
+
428
+
429
+ def generate_task_py(task: ParsedTask, annotation: ParsedAnnotation) -> str:
430
+ """Generate a task file using screen helpers for coordinates."""
431
+ state_class = _to_pascal_case(annotation.screen_name) + "State"
432
+
433
+ # Find target element
434
+ target_el = None
435
+ if task.target_element_id:
436
+ for el in annotation.elements:
437
+ if el.id == task.target_element_id:
438
+ target_el = el
439
+ break
440
+
441
+ # Determine which helper to use based on element type
442
+ if target_el:
443
+ if target_el.region_type == "button":
444
+ coord_code = f'pixel_coords = get_button_center("{target_el.label}")'
445
+ tolerance_code = f'tolerance = get_button_tolerance("{target_el.label}")'
446
+ imports = "get_button_center, get_button_tolerance"
447
+ elif target_el.region_type == "grid":
448
+ coord_code = '''# Grid element - coordinates depend on which cell
449
+ grid_bbox = get_grid_bbox()
450
+ # TODO: Calculate cell coordinates based on state
451
+ pixel_coords = (grid_bbox[0] + grid_bbox[2] // 2, grid_bbox[1] + grid_bbox[3] // 2)'''
452
+ tolerance_code = 'tolerance = get_grid_tolerance()'
453
+ imports = "get_grid_bbox, get_grid_tolerance"
454
+ else:
455
+ coord_code = f'''# Get element center
456
+ el = ANNOTATION_CONFIG.get_element_by_label("{target_el.label}")
457
+ pixel_coords = scale_coord(el.center[0], el.center[1])'''
458
+ tolerance_code = f'''el = ANNOTATION_CONFIG.get_element_by_label("{target_el.label}")
459
+ tolerance = scale_tolerance(el.tolerance_x, el.tolerance_y)'''
460
+ imports = "scale_coord, scale_tolerance, ANNOTATION_CONFIG"
461
+ else:
462
+ coord_code = "pixel_coords = (0, 0) # TODO: specify target coordinates"
463
+ tolerance_code = "tolerance = (50, 50) # TODO: calculate from element size"
464
+ imports = "scale_coord"
465
+
466
+ tool_call = _generate_tool_call(task)
467
+
468
+ return f'''{COPYRIGHT_HEADER}
469
+ """Task: {task.prompt or task.python_name}"""
470
+
471
+ from random import Random
472
+ from typing import Any
473
+
474
+ from cudag import BaseTask, TaskContext, TaskSample, TestCase, ToolCall, normalize_coord
475
+
476
+ from screen import {imports}, IMAGE_WIDTH, IMAGE_HEIGHT
477
+ from state import {state_class}
478
+
479
+
480
+ class {task.class_name}(BaseTask):
481
+ """Task for: {task.prompt}"""
482
+
483
+ task_type = "{task.task_type}"
484
+
485
+ def generate_sample(self, ctx: TaskContext) -> TaskSample:
486
+ """Generate a training sample."""
487
+ state = {state_class}.generate(ctx.rng)
488
+ image, metadata = self.renderer.render(state)
489
+
490
+ # Get target coordinates from annotation
491
+ {coord_code}
492
+ normalized = normalize_coord(pixel_coords, (IMAGE_WIDTH, IMAGE_HEIGHT))
493
+
494
+ image_path = self.save_image(image, ctx)
495
+
496
+ return TaskSample(
497
+ id=self.build_id(ctx),
498
+ image_path=image_path,
499
+ human_prompt="{task.prompt}",
500
+ tool_call={tool_call},
501
+ pixel_coords=pixel_coords,
502
+ metadata={{
503
+ "task_type": self.task_type,
504
+ **metadata,
505
+ }},
506
+ image_size=image.size,
507
+ )
508
+
509
+ def generate_test(self, ctx: TaskContext) -> TestCase:
510
+ """Generate a test case."""
511
+ sample = self.generate_sample(ctx)
512
+
513
+ # Get tolerance from annotation
514
+ {tolerance_code}
515
+
516
+ return TestCase(
517
+ test_id=f"test_{{sample.id}}",
518
+ screenshot=sample.image_path,
519
+ prompt=sample.human_prompt,
520
+ expected_action=sample.tool_call.to_dict(),
521
+ tolerance=tolerance,
522
+ metadata=sample.metadata,
523
+ pixel_coords=sample.pixel_coords,
524
+ )
525
+ '''
526
+
527
+
528
+ def _generate_tool_call(task: ParsedTask) -> str:
529
+ """Generate ToolCall constructor for a task."""
530
+ action = task.action
531
+ params = task.action_params
532
+
533
+ if action == "left_click":
534
+ return "ToolCall.left_click(normalized)"
535
+ elif action == "right_click":
536
+ return "ToolCall.right_click(normalized)"
537
+ elif action == "double_click":
538
+ return "ToolCall.double_click(normalized)"
539
+ elif action == "type":
540
+ text = params.get("text", "")
541
+ return f'ToolCall.type("{text}")'
542
+ elif action == "key":
543
+ keys = params.get("keys", [])
544
+ return f"ToolCall.key({keys})"
545
+ elif action == "scroll":
546
+ pixels = params.get("pixels", 100)
547
+ return f"ToolCall.scroll(normalized, pixels={pixels})"
548
+ elif action == "wait":
549
+ ms = params.get("ms", 1000)
550
+ return f"ToolCall.wait(ms={ms})"
551
+ elif action == "drag_to":
552
+ return "ToolCall.drag(normalized, end_coord)"
553
+ elif action == "mouse_move":
554
+ return "ToolCall.mouse_move(normalized)"
555
+ else:
556
+ return f"ToolCall.left_click(normalized) # TODO: implement {action}"
557
+
558
+
559
+ def generate_tasks_init_py(tasks: list[ParsedTask]) -> str:
560
+ """Generate tasks/__init__.py."""
561
+ imports = []
562
+ exports = []
563
+
564
+ for task in tasks:
565
+ imports.append(f"from tasks.{task.python_name} import {task.class_name}")
566
+ exports.append(f' "{task.class_name}",')
567
+
568
+ imports_str = "\n".join(imports) if imports else "# No tasks"
569
+ exports_str = "\n".join(exports) if exports else ""
570
+
571
+ return f'''{COPYRIGHT_HEADER}
572
+ """Task definitions for this generator."""
573
+
574
+ {imports_str}
575
+
576
+ __all__ = [
577
+ {exports_str}
578
+ ]
579
+ '''
580
+
581
+
582
+ def generate_config_yaml(annotation: ParsedAnnotation) -> str:
583
+ """Generate config/dataset.yaml with image output settings."""
584
+ task_counts = "\n".join(
585
+ f" {task.task_type}: 1000" for task in annotation.tasks
586
+ ) if annotation.tasks else " # No tasks defined"
587
+
588
+ return f'''# Dataset configuration for {annotation.screen_name}
589
+ # Auto-generated from annotation
590
+
591
+ dataset:
592
+ name: {annotation.screen_name}
593
+ version: "1.0.0"
594
+ description: "Training data for {annotation.screen_name}"
595
+
596
+ # Image output settings
597
+ # Defaults to annotation size - override to generate different size images
598
+ image:
599
+ width: {annotation.image_size[0]}
600
+ height: {annotation.image_size[1]}
601
+
602
+ generation:
603
+ seed: 42
604
+ train_split: 0.8
605
+ val_split: 0.1
606
+ test_split: 0.1
607
+
608
+ tasks:
609
+ {task_counts}
610
+
611
+ # Distribution types (optional)
612
+ # distributions:
613
+ # click_button:
614
+ # normal: 0.8
615
+ # edge_case: 0.15
616
+ # adversarial: 0.05
617
+ '''
618
+
619
+
620
+ def generate_pyproject_toml(name: str) -> str:
621
+ """Generate pyproject.toml."""
622
+ return f'''[project]
623
+ name = "{name}"
624
+ version = "0.1.0"
625
+ description = "CUDAG generator for {name}"
626
+ requires-python = ">=3.12"
627
+ dependencies = [
628
+ "cudag",
629
+ "pillow>=10.0.0",
630
+ ]
631
+
632
+ [build-system]
633
+ requires = ["setuptools>=64", "wheel"]
634
+ build-backend = "setuptools.build_meta"
635
+ '''
636
+
637
+
638
+ def _to_pascal_case(name: str) -> str:
639
+ """Convert name to PascalCase."""
640
+ parts = name.replace("-", "_").split("_")
641
+ return "".join(p.capitalize() for p in parts if p)
642
+
643
+
644
+ def _to_snake_case(name: str) -> str:
645
+ """Convert name to snake_case."""
646
+ import re
647
+ s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
648
+ return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()