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
cudag/core/icon.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
"""Icon abstraction for clickable UI icons.
|
|
6
|
+
|
|
7
|
+
Provides reusable Icon classes for any icon-based UI element:
|
|
8
|
+
- Desktop icons (large, with labels)
|
|
9
|
+
- Taskbar icons (small, no labels)
|
|
10
|
+
- Application icons
|
|
11
|
+
- Toolbar buttons
|
|
12
|
+
|
|
13
|
+
The Icon class handles:
|
|
14
|
+
- Placement (position, size)
|
|
15
|
+
- Tolerance calculation
|
|
16
|
+
- Center point for clicks
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class IconSpec:
|
|
28
|
+
"""Specification for an icon type.
|
|
29
|
+
|
|
30
|
+
Defines the size and characteristics of a class of icons
|
|
31
|
+
(e.g., all desktop icons are 54x54 with labels).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
width: int
|
|
35
|
+
"""Icon width in pixels."""
|
|
36
|
+
|
|
37
|
+
height: int
|
|
38
|
+
"""Icon height in pixels."""
|
|
39
|
+
|
|
40
|
+
has_label: bool = False
|
|
41
|
+
"""Whether this icon type has a text label."""
|
|
42
|
+
|
|
43
|
+
label_height: int = 0
|
|
44
|
+
"""Height of the label area below icon."""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def total_height(self) -> int:
|
|
48
|
+
"""Total height including label."""
|
|
49
|
+
return self.height + (self.label_height if self.has_label else 0)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def tolerance_pixels(self) -> tuple[int, int]:
|
|
53
|
+
"""Natural tolerance for this icon size (70% of dimensions)."""
|
|
54
|
+
return (int(self.width * 0.7), int(self.height * 0.7))
|
|
55
|
+
|
|
56
|
+
def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
|
|
57
|
+
"""Tolerance in RU units for normalized coordinates.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
image_size: (width, height) of the image in pixels
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
(x_tolerance, y_tolerance) in RU units (0-1000 scale)
|
|
64
|
+
"""
|
|
65
|
+
x_ru = (self.width * 0.7 / image_size[0]) * 1000
|
|
66
|
+
y_ru = (self.height * 0.7 / image_size[1]) * 1000
|
|
67
|
+
return (int(x_ru), int(y_ru))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Common icon specs
|
|
71
|
+
DESKTOP_ICON = IconSpec(width=54, height=54, has_label=True, label_height=20)
|
|
72
|
+
TASKBAR_ICON = IconSpec(width=27, height=28, has_label=False)
|
|
73
|
+
TOOLBAR_ICON = IconSpec(width=24, height=24, has_label=False)
|
|
74
|
+
APP_ICON_LARGE = IconSpec(width=48, height=48, has_label=True, label_height=16)
|
|
75
|
+
APP_ICON_SMALL = IconSpec(width=32, height=32, has_label=False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class IconPlacement:
|
|
80
|
+
"""An icon placed at a specific location.
|
|
81
|
+
|
|
82
|
+
Represents a single icon instance with its position and metadata.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
icon_id: str
|
|
86
|
+
"""Unique identifier for this icon (e.g., 'chrome', 'od')."""
|
|
87
|
+
|
|
88
|
+
x: int
|
|
89
|
+
"""X position of icon top-left corner."""
|
|
90
|
+
|
|
91
|
+
y: int
|
|
92
|
+
"""Y position of icon top-left corner."""
|
|
93
|
+
|
|
94
|
+
spec: IconSpec
|
|
95
|
+
"""Icon specification (size, etc.)."""
|
|
96
|
+
|
|
97
|
+
label: str = ""
|
|
98
|
+
"""Display label for the icon."""
|
|
99
|
+
|
|
100
|
+
image_file: str | Path = ""
|
|
101
|
+
"""Path to the icon image file."""
|
|
102
|
+
|
|
103
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
104
|
+
"""Additional metadata."""
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def width(self) -> int:
|
|
108
|
+
"""Icon width."""
|
|
109
|
+
return self.spec.width
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def height(self) -> int:
|
|
113
|
+
"""Icon height (excluding label)."""
|
|
114
|
+
return self.spec.height
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def center(self) -> tuple[int, int]:
|
|
118
|
+
"""Center point of the icon (for clicking)."""
|
|
119
|
+
return (
|
|
120
|
+
self.x + self.spec.width // 2,
|
|
121
|
+
self.y + self.spec.height // 2,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def bounds(self) -> tuple[int, int, int, int]:
|
|
126
|
+
"""Bounding box as (x, y, width, height)."""
|
|
127
|
+
return (self.x, self.y, self.spec.width, self.spec.height)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def tolerance_pixels(self) -> tuple[int, int]:
|
|
131
|
+
"""Natural tolerance for this icon."""
|
|
132
|
+
return self.spec.tolerance_pixels
|
|
133
|
+
|
|
134
|
+
def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
|
|
135
|
+
"""Tolerance in RU units."""
|
|
136
|
+
return self.spec.tolerance_ru(image_size)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class IconLayout:
|
|
141
|
+
"""Layout manager for icons in a region.
|
|
142
|
+
|
|
143
|
+
Handles placement of multiple icons in a grid or list layout.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
spec: IconSpec
|
|
147
|
+
"""Icon specification for all icons in this layout."""
|
|
148
|
+
|
|
149
|
+
start_x: int = 0
|
|
150
|
+
"""Starting X position."""
|
|
151
|
+
|
|
152
|
+
start_y: int = 0
|
|
153
|
+
"""Starting Y position."""
|
|
154
|
+
|
|
155
|
+
padding: int = 10
|
|
156
|
+
"""Padding between icons."""
|
|
157
|
+
|
|
158
|
+
direction: str = "vertical"
|
|
159
|
+
"""Layout direction: 'vertical', 'horizontal', or 'grid'."""
|
|
160
|
+
|
|
161
|
+
cols: int = 1
|
|
162
|
+
"""Number of columns for grid layout."""
|
|
163
|
+
|
|
164
|
+
def place_icons(
|
|
165
|
+
self,
|
|
166
|
+
icon_ids: list[str],
|
|
167
|
+
labels: dict[str, str] | None = None,
|
|
168
|
+
image_files: dict[str, str | Path] | None = None,
|
|
169
|
+
) -> list[IconPlacement]:
|
|
170
|
+
"""Place icons according to layout rules.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
icon_ids: List of icon identifiers to place
|
|
174
|
+
labels: Optional dict mapping icon_id to label
|
|
175
|
+
image_files: Optional dict mapping icon_id to image path
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
List of IconPlacement objects
|
|
179
|
+
"""
|
|
180
|
+
labels = labels or {}
|
|
181
|
+
image_files = image_files or {}
|
|
182
|
+
placements: list[IconPlacement] = []
|
|
183
|
+
|
|
184
|
+
for i, icon_id in enumerate(icon_ids):
|
|
185
|
+
if self.direction == "vertical":
|
|
186
|
+
x = self.start_x
|
|
187
|
+
y = self.start_y + i * (self.spec.total_height + self.padding)
|
|
188
|
+
elif self.direction == "horizontal":
|
|
189
|
+
x = self.start_x + i * (self.spec.width + self.padding)
|
|
190
|
+
y = self.start_y
|
|
191
|
+
else: # grid
|
|
192
|
+
row, col = divmod(i, self.cols)
|
|
193
|
+
x = self.start_x + col * (self.spec.width + self.padding)
|
|
194
|
+
y = self.start_y + row * (self.spec.total_height + self.padding)
|
|
195
|
+
|
|
196
|
+
placements.append(
|
|
197
|
+
IconPlacement(
|
|
198
|
+
icon_id=icon_id,
|
|
199
|
+
x=x,
|
|
200
|
+
y=y,
|
|
201
|
+
spec=self.spec,
|
|
202
|
+
label=labels.get(icon_id, ""),
|
|
203
|
+
image_file=image_files.get(icon_id, ""),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return placements
|
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
"""Base class for iconlist element tasks.
|
|
6
|
+
|
|
7
|
+
This module provides an abstract base class for tasks that target iconlist
|
|
8
|
+
elements in annotation.json. When a task targets an iconlist, it generates
|
|
9
|
+
one sample per icon in that list.
|
|
10
|
+
|
|
11
|
+
Element type determines generation behavior:
|
|
12
|
+
- iconlist -> generate one sample per icon
|
|
13
|
+
- button -> generate one sample (not handled here)
|
|
14
|
+
- loading -> handled separately by wait tasks
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import abstractmethod
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from random import Random
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
23
|
+
|
|
24
|
+
from cudag.annotation.config import AnnotatedElement, AnnotatedTask, AnnotationConfig
|
|
25
|
+
from cudag.core.task import BaseTask, TaskContext, TaskSample, TestCase
|
|
26
|
+
from cudag.prompts.tools import ToolCall
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from cudag.core.renderer import BaseRenderer
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IconInfo(Protocol):
|
|
33
|
+
"""Protocol for icon placement info from state."""
|
|
34
|
+
|
|
35
|
+
icon_id: str
|
|
36
|
+
center: tuple[int, int]
|
|
37
|
+
bounds: tuple[int, int, int, int]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def make_tool_call(
|
|
41
|
+
action: str, coord: tuple[int, int], wait_time: float = 0
|
|
42
|
+
) -> ToolCall:
|
|
43
|
+
"""Create a ToolCall based on action string from annotation.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
action: Action string from annotation (double_click, left_click, etc.)
|
|
47
|
+
coord: (x, y) pixel coordinates
|
|
48
|
+
wait_time: Wait time in seconds for wait actions
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
ToolCall instance for the action
|
|
52
|
+
"""
|
|
53
|
+
if action == "double_click":
|
|
54
|
+
return ToolCall.double_click(coord)
|
|
55
|
+
elif action == "left_click":
|
|
56
|
+
return ToolCall.left_click(coord)
|
|
57
|
+
elif action == "right_click":
|
|
58
|
+
return ToolCall.right_click(coord)
|
|
59
|
+
elif action == "wait":
|
|
60
|
+
return ToolCall.wait(wait_time)
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unknown action '{action}' - must be one of: double_click, left_click, right_click, wait")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class IconListTaskBase(BaseTask):
|
|
66
|
+
"""Abstract base class for tasks targeting iconlist elements.
|
|
67
|
+
|
|
68
|
+
This base class handles the common pattern for iconlist tasks:
|
|
69
|
+
1. Iterate over tasks in annotation
|
|
70
|
+
2. For each task targeting an iconlist element, generate one sample per icon
|
|
71
|
+
3. Use task.prompt_template, task.action, task.task_type from annotation
|
|
72
|
+
4. Use element.tolerance_x/y from annotation
|
|
73
|
+
|
|
74
|
+
Subclasses must implement:
|
|
75
|
+
- get_annotation_config(): Return the AnnotationConfig
|
|
76
|
+
- get_icons_for_element(): Return icon placements and info for an element
|
|
77
|
+
- generate_state(): Generate the screen state
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
class ClickIconTask(IconListTaskBase):
|
|
81
|
+
def get_annotation_config(self) -> AnnotationConfig:
|
|
82
|
+
return ANNOTATION_CONFIG
|
|
83
|
+
|
|
84
|
+
def get_icons_for_element(
|
|
85
|
+
self, element: AnnotatedElement, state: Any
|
|
86
|
+
) -> tuple[list[IconInfo], dict[str, dict]]:
|
|
87
|
+
if element.label == "desktop":
|
|
88
|
+
return state.desktop_icons, get_desktop_icons()
|
|
89
|
+
elif element.label == "taskbar":
|
|
90
|
+
return state.taskbar_icons, get_taskbar_icons()
|
|
91
|
+
return [], {}
|
|
92
|
+
|
|
93
|
+
def generate_state(self, rng: Random, **kwargs) -> Any:
|
|
94
|
+
return DesktopState.generate(rng=rng, **kwargs)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
task_type: str = "iconlist"
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def get_annotation_config(self) -> AnnotationConfig | None:
|
|
101
|
+
"""Return the annotation config for this generator.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
AnnotationConfig loaded from annotation.json, or None
|
|
105
|
+
"""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def get_icons_for_element(
|
|
110
|
+
self, element: AnnotatedElement, state: Any
|
|
111
|
+
) -> tuple[list[Any], dict[str, dict[str, Any]]]:
|
|
112
|
+
"""Get icons and their info for an element.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
element: The annotated element (iconlist type)
|
|
116
|
+
state: The generated screen state
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tuple of (icon_placements, icons_info_dict)
|
|
120
|
+
- icon_placements: List of IconPlacement-like objects with icon_id, center, bounds
|
|
121
|
+
- icons_info_dict: Dict mapping icon_id to {label, required, ...}
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def generate_state(self, rng: Random, **kwargs: Any) -> Any:
|
|
127
|
+
"""Generate the screen state.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
rng: Random number generator
|
|
131
|
+
**kwargs: Additional arguments for state generation
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
State object for rendering
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def generate_samples(self, ctx: TaskContext) -> list[TaskSample]:
|
|
139
|
+
"""Generate samples for ALL iconlist tasks in annotation.
|
|
140
|
+
|
|
141
|
+
Iterates over tasks in annotation.json. For each task targeting an
|
|
142
|
+
iconlist element, generates one sample per icon in that list.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
ctx: Task context with RNG, index, output directory
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of TaskSample, one per icon per iconlist task
|
|
149
|
+
"""
|
|
150
|
+
config = self.get_annotation_config()
|
|
151
|
+
if config is None:
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
# Generate state - subclass provides kwargs
|
|
155
|
+
state = self.generate_state(ctx.rng)
|
|
156
|
+
|
|
157
|
+
# Render once - shared by all samples
|
|
158
|
+
image, metadata = self.renderer.render(state)
|
|
159
|
+
image_path = self.save_image(image, ctx)
|
|
160
|
+
|
|
161
|
+
samples: list[TaskSample] = []
|
|
162
|
+
|
|
163
|
+
# Iterate over ALL tasks in annotation (except wait and grounding tasks)
|
|
164
|
+
for task in config.tasks:
|
|
165
|
+
if task.action == "wait":
|
|
166
|
+
continue # Wait tasks handled separately
|
|
167
|
+
if task.action == "grounding":
|
|
168
|
+
continue # Grounding tasks handled by GroundingTask
|
|
169
|
+
|
|
170
|
+
# Get target element
|
|
171
|
+
element = config.get_element(task.target_element_id)
|
|
172
|
+
if element is None:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Only handle iconlist elements
|
|
176
|
+
if element.element_type != "iconlist":
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Get icons for this element from state
|
|
180
|
+
icons_in_state, icons_info = self.get_icons_for_element(element, state)
|
|
181
|
+
if not icons_in_state:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
tolerance = (element.tolerance_x, element.tolerance_y)
|
|
185
|
+
|
|
186
|
+
# Generate a sample for each icon
|
|
187
|
+
for icon in icons_in_state:
|
|
188
|
+
icon_info = icons_info.get(icon.icon_id, {})
|
|
189
|
+
label = icon_info.get("label", icon.icon_id)
|
|
190
|
+
|
|
191
|
+
prompt = task.prompt_template.replace("[icon_label]", label)
|
|
192
|
+
click_x, click_y = icon.center
|
|
193
|
+
|
|
194
|
+
tool_call = make_tool_call(task.action, (click_x, click_y))
|
|
195
|
+
|
|
196
|
+
samples.append(
|
|
197
|
+
TaskSample(
|
|
198
|
+
id=self.build_id(ctx, f"_{element.label}_{icon.icon_id}"),
|
|
199
|
+
image_path=image_path,
|
|
200
|
+
human_prompt=prompt,
|
|
201
|
+
tool_call=tool_call,
|
|
202
|
+
pixel_coords=(click_x, click_y),
|
|
203
|
+
metadata={
|
|
204
|
+
"task_type": task.task_type,
|
|
205
|
+
"element_type": element.element_type,
|
|
206
|
+
"element_label": element.label,
|
|
207
|
+
"icon_id": icon.icon_id,
|
|
208
|
+
"icon_label": label,
|
|
209
|
+
"icon_bounds": icon.bounds,
|
|
210
|
+
"ground_truth": (
|
|
211
|
+
state.to_ground_truth()
|
|
212
|
+
if hasattr(state, "to_ground_truth")
|
|
213
|
+
else {}
|
|
214
|
+
),
|
|
215
|
+
"tolerance": list(tolerance),
|
|
216
|
+
},
|
|
217
|
+
image_size=config.image_size,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return samples
|
|
222
|
+
|
|
223
|
+
def generate_sample(self, ctx: TaskContext) -> TaskSample:
|
|
224
|
+
"""Generate samples - returns first sample."""
|
|
225
|
+
samples = self.generate_samples(ctx)
|
|
226
|
+
if not samples:
|
|
227
|
+
raise ValueError("No samples generated")
|
|
228
|
+
return samples[0]
|
|
229
|
+
|
|
230
|
+
def generate_tests(self, ctx: TaskContext) -> list[TestCase]:
|
|
231
|
+
"""Generate test cases for required icons in iconlist tasks.
|
|
232
|
+
|
|
233
|
+
Only generates tests for icons marked as required=true in annotation.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
ctx: Task context
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
List of TestCase for required icons
|
|
240
|
+
"""
|
|
241
|
+
config = self.get_annotation_config()
|
|
242
|
+
if config is None:
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
state = self.generate_state(ctx.rng)
|
|
246
|
+
image, metadata = self.renderer.render(state)
|
|
247
|
+
image_path = self.save_image(image, ctx)
|
|
248
|
+
|
|
249
|
+
tests: list[TestCase] = []
|
|
250
|
+
|
|
251
|
+
for task in config.tasks:
|
|
252
|
+
if task.action == "wait":
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
element = config.get_element(task.target_element_id)
|
|
256
|
+
if element is None or element.element_type != "iconlist":
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
icons_in_state, icons_info = self.get_icons_for_element(element, state)
|
|
260
|
+
tolerance = (element.tolerance_x, element.tolerance_y)
|
|
261
|
+
|
|
262
|
+
# Test on required icons only
|
|
263
|
+
for icon in icons_in_state:
|
|
264
|
+
icon_info = icons_info.get(icon.icon_id, {})
|
|
265
|
+
if not icon_info.get("required", False):
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
label = icon_info.get("label", icon.icon_id)
|
|
269
|
+
prompt = task.prompt_template.replace("[icon_label]", label)
|
|
270
|
+
click_x, click_y = icon.center
|
|
271
|
+
|
|
272
|
+
tool_call = make_tool_call(task.action, (click_x, click_y))
|
|
273
|
+
|
|
274
|
+
tests.append(
|
|
275
|
+
TestCase(
|
|
276
|
+
test_id=f"test_{ctx.index:04d}_{element.label}_{icon.icon_id}",
|
|
277
|
+
screenshot=image_path,
|
|
278
|
+
prompt=prompt,
|
|
279
|
+
expected_action=tool_call.to_dict(),
|
|
280
|
+
tolerance=tolerance,
|
|
281
|
+
metadata={
|
|
282
|
+
"task_type": task.task_type,
|
|
283
|
+
"element_type": element.element_type,
|
|
284
|
+
"element_label": element.label,
|
|
285
|
+
"icon_id": icon.icon_id,
|
|
286
|
+
"icon_label": label,
|
|
287
|
+
"icon_bounds": icon.bounds,
|
|
288
|
+
"image_size": image.size,
|
|
289
|
+
},
|
|
290
|
+
pixel_coords=(click_x, click_y),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return tests
|
|
295
|
+
|
|
296
|
+
def generate_test(self, ctx: TaskContext) -> TestCase:
|
|
297
|
+
"""Generate test - returns first."""
|
|
298
|
+
tests = self.generate_tests(ctx)
|
|
299
|
+
if not tests:
|
|
300
|
+
raise ValueError("No tests generated")
|
|
301
|
+
return tests[0]
|