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,296 @@
|
|
|
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
|
+
"""HTML transcription parsing for grid elements.
|
|
6
|
+
|
|
7
|
+
This module parses HTML table transcriptions from annotations into structured
|
|
8
|
+
data that generators can use to create similar synthetic data.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
from cudag.annotation import parse_transcription
|
|
12
|
+
|
|
13
|
+
html = "<table><tr><td>10/07/2025</td><td>John</td></tr></table>"
|
|
14
|
+
table = parse_transcription(html)
|
|
15
|
+
|
|
16
|
+
for row in table.rows:
|
|
17
|
+
print([cell.text for cell in row.cells])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from html.parser import HTMLParser
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TranscriptionCell:
|
|
30
|
+
"""A single cell from a transcribed table."""
|
|
31
|
+
|
|
32
|
+
text: str
|
|
33
|
+
"""Full cell text with line breaks converted to spaces."""
|
|
34
|
+
|
|
35
|
+
lines: list[str] = field(default_factory=list)
|
|
36
|
+
"""Cell content split by <br/> tags, preserving multi-line data."""
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def first_line(self) -> str:
|
|
40
|
+
"""Get first line of cell (useful for primary value)."""
|
|
41
|
+
return self.lines[0] if self.lines else self.text
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_empty(self) -> bool:
|
|
45
|
+
"""Check if cell has no content."""
|
|
46
|
+
return not self.text.strip()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_currency(self) -> bool:
|
|
50
|
+
"""Check if cell appears to be a currency value."""
|
|
51
|
+
return bool(re.match(r"^\$?[\d,]+\.?\d*$", self.text.strip()))
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_date(self) -> bool:
|
|
55
|
+
"""Check if cell appears to be a date (MM/DD/YYYY or similar)."""
|
|
56
|
+
return bool(re.match(r"^\d{1,2}/\d{1,2}/\d{2,4}$", self.text.strip()))
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_time(self) -> bool:
|
|
60
|
+
"""Check if cell appears to be a time (e.g., 11:16a, 3:25p)."""
|
|
61
|
+
return bool(re.match(r"^\d{1,2}:\d{2}[ap]?m?$", self.text.strip(), re.I))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class TranscriptionRow:
|
|
66
|
+
"""A single row from a transcribed table."""
|
|
67
|
+
|
|
68
|
+
cells: list[TranscriptionCell] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
def __len__(self) -> int:
|
|
71
|
+
return len(self.cells)
|
|
72
|
+
|
|
73
|
+
def __getitem__(self, index: int) -> TranscriptionCell:
|
|
74
|
+
return self.cells[index]
|
|
75
|
+
|
|
76
|
+
def get(self, index: int, default: str = "") -> str:
|
|
77
|
+
"""Get cell text by index with default."""
|
|
78
|
+
if 0 <= index < len(self.cells):
|
|
79
|
+
return self.cells[index].text
|
|
80
|
+
return default
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def values(self) -> list[str]:
|
|
84
|
+
"""Get all cell values as strings."""
|
|
85
|
+
return [cell.text for cell in self.cells]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class ParsedTranscription:
|
|
90
|
+
"""Structured data parsed from an HTML table transcription."""
|
|
91
|
+
|
|
92
|
+
rows: list[TranscriptionRow] = field(default_factory=list)
|
|
93
|
+
"""All data rows (excludes header if detected)."""
|
|
94
|
+
|
|
95
|
+
headers: list[str] = field(default_factory=list)
|
|
96
|
+
"""Header row values (if <thead> was present)."""
|
|
97
|
+
|
|
98
|
+
raw_html: str = ""
|
|
99
|
+
"""Original HTML for reference."""
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def num_rows(self) -> int:
|
|
103
|
+
"""Number of data rows."""
|
|
104
|
+
return len(self.rows)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def num_cols(self) -> int:
|
|
108
|
+
"""Number of columns (from first row or headers)."""
|
|
109
|
+
if self.headers:
|
|
110
|
+
return len(self.headers)
|
|
111
|
+
if self.rows:
|
|
112
|
+
return len(self.rows[0])
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
def column(self, index: int) -> list[str]:
|
|
116
|
+
"""Get all values from a specific column."""
|
|
117
|
+
return [row.get(index) for row in self.rows]
|
|
118
|
+
|
|
119
|
+
def sample_values(self, col_index: int, max_samples: int = 10) -> list[str]:
|
|
120
|
+
"""Get sample non-empty values from a column."""
|
|
121
|
+
values = []
|
|
122
|
+
for row in self.rows:
|
|
123
|
+
val = row.get(col_index).strip()
|
|
124
|
+
if val and val not in values:
|
|
125
|
+
values.append(val)
|
|
126
|
+
if len(values) >= max_samples:
|
|
127
|
+
break
|
|
128
|
+
return values
|
|
129
|
+
|
|
130
|
+
def infer_column_types(self) -> list[str]:
|
|
131
|
+
"""Infer data types for each column based on content.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of type hints: 'date', 'time', 'currency', 'text', 'multiline'
|
|
135
|
+
"""
|
|
136
|
+
if not self.rows:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
types = []
|
|
140
|
+
for col_idx in range(self.num_cols):
|
|
141
|
+
# Check first few non-empty cells
|
|
142
|
+
col_type = "text"
|
|
143
|
+
for row in self.rows[:5]:
|
|
144
|
+
if col_idx >= len(row.cells):
|
|
145
|
+
continue
|
|
146
|
+
cell = row.cells[col_idx]
|
|
147
|
+
if cell.is_empty:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if len(cell.lines) > 1:
|
|
151
|
+
col_type = "multiline"
|
|
152
|
+
break
|
|
153
|
+
elif cell.is_date:
|
|
154
|
+
col_type = "date"
|
|
155
|
+
break
|
|
156
|
+
elif cell.is_time:
|
|
157
|
+
col_type = "time"
|
|
158
|
+
break
|
|
159
|
+
elif cell.is_currency:
|
|
160
|
+
col_type = "currency"
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
types.append(col_type)
|
|
164
|
+
return types
|
|
165
|
+
|
|
166
|
+
def to_dict(self) -> dict[str, Any]:
|
|
167
|
+
"""Convert to dictionary for serialization."""
|
|
168
|
+
return {
|
|
169
|
+
"headers": self.headers,
|
|
170
|
+
"rows": [
|
|
171
|
+
[{"text": c.text, "lines": c.lines} for c in row.cells]
|
|
172
|
+
for row in self.rows
|
|
173
|
+
],
|
|
174
|
+
"num_rows": self.num_rows,
|
|
175
|
+
"num_cols": self.num_cols,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class _TableHTMLParser(HTMLParser):
|
|
180
|
+
"""Internal HTML parser for table extraction."""
|
|
181
|
+
|
|
182
|
+
def __init__(self) -> None:
|
|
183
|
+
super().__init__()
|
|
184
|
+
self.rows: list[TranscriptionRow] = []
|
|
185
|
+
self.headers: list[str] = []
|
|
186
|
+
self._current_row: TranscriptionRow | None = None
|
|
187
|
+
self._current_cell_lines: list[str] = []
|
|
188
|
+
self._current_cell_text: str = ""
|
|
189
|
+
self._in_thead = False
|
|
190
|
+
self._in_tbody = False
|
|
191
|
+
self._in_cell = False
|
|
192
|
+
|
|
193
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
194
|
+
tag = tag.lower()
|
|
195
|
+
if tag == "thead":
|
|
196
|
+
self._in_thead = True
|
|
197
|
+
elif tag == "tbody":
|
|
198
|
+
self._in_tbody = True
|
|
199
|
+
elif tag == "tr":
|
|
200
|
+
self._current_row = TranscriptionRow()
|
|
201
|
+
elif tag in ("td", "th"):
|
|
202
|
+
self._in_cell = True
|
|
203
|
+
self._current_cell_lines = []
|
|
204
|
+
self._current_cell_text = ""
|
|
205
|
+
elif tag == "br":
|
|
206
|
+
# Line break within cell - save current text as a line
|
|
207
|
+
if self._in_cell and self._current_cell_text:
|
|
208
|
+
self._current_cell_lines.append(self._current_cell_text.strip())
|
|
209
|
+
self._current_cell_text = ""
|
|
210
|
+
|
|
211
|
+
def handle_endtag(self, tag: str) -> None:
|
|
212
|
+
tag = tag.lower()
|
|
213
|
+
if tag == "thead":
|
|
214
|
+
self._in_thead = False
|
|
215
|
+
elif tag == "tbody":
|
|
216
|
+
self._in_tbody = False
|
|
217
|
+
elif tag == "tr":
|
|
218
|
+
if self._current_row is not None:
|
|
219
|
+
# If in thead and no headers yet, use first row as headers
|
|
220
|
+
if self._in_thead and not self.headers:
|
|
221
|
+
self.headers = [c.text for c in self._current_row.cells]
|
|
222
|
+
else:
|
|
223
|
+
self.rows.append(self._current_row)
|
|
224
|
+
self._current_row = None
|
|
225
|
+
elif tag in ("td", "th"):
|
|
226
|
+
if self._in_cell and self._current_row is not None:
|
|
227
|
+
# Finalize the current cell
|
|
228
|
+
if self._current_cell_text:
|
|
229
|
+
self._current_cell_lines.append(self._current_cell_text.strip())
|
|
230
|
+
|
|
231
|
+
# Build cell with text and lines
|
|
232
|
+
full_text = " ".join(self._current_cell_lines)
|
|
233
|
+
cell = TranscriptionCell(
|
|
234
|
+
text=full_text,
|
|
235
|
+
lines=self._current_cell_lines.copy(),
|
|
236
|
+
)
|
|
237
|
+
self._current_row.cells.append(cell)
|
|
238
|
+
|
|
239
|
+
self._in_cell = False
|
|
240
|
+
self._current_cell_lines = []
|
|
241
|
+
self._current_cell_text = ""
|
|
242
|
+
|
|
243
|
+
def handle_data(self, data: str) -> None:
|
|
244
|
+
if self._in_cell:
|
|
245
|
+
self._current_cell_text += data
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_transcription(html: str) -> ParsedTranscription:
|
|
249
|
+
"""Parse HTML table transcription into structured data.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
html: HTML string containing a <table> element
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
ParsedTranscription with rows, cells, and inferred types
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> html = "<table><tr><td>10/07/2025</td><td>$54.58</td></tr></table>"
|
|
259
|
+
>>> table = parse_transcription(html)
|
|
260
|
+
>>> table.rows[0].cells[0].text
|
|
261
|
+
'10/07/2025'
|
|
262
|
+
>>> table.rows[0].cells[0].is_date
|
|
263
|
+
True
|
|
264
|
+
"""
|
|
265
|
+
if not html or not html.strip():
|
|
266
|
+
return ParsedTranscription(raw_html=html)
|
|
267
|
+
|
|
268
|
+
parser = _TableHTMLParser()
|
|
269
|
+
try:
|
|
270
|
+
parser.feed(html)
|
|
271
|
+
except Exception:
|
|
272
|
+
# Return empty on parse error
|
|
273
|
+
return ParsedTranscription(raw_html=html)
|
|
274
|
+
|
|
275
|
+
return ParsedTranscription(
|
|
276
|
+
rows=parser.rows,
|
|
277
|
+
headers=parser.headers,
|
|
278
|
+
raw_html=html,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def parse_text_transcription(text: str) -> str:
|
|
283
|
+
"""Parse plain text transcription (non-table elements).
|
|
284
|
+
|
|
285
|
+
For text elements, the transcription is just unstructured text.
|
|
286
|
+
This function strips whitespace and returns the clean text.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
text: Raw transcription text
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Cleaned text string
|
|
293
|
+
"""
|
|
294
|
+
if not text:
|
|
295
|
+
return ""
|
|
296
|
+
return text.strip()
|
cudag/cli/__init__.py
ADDED
cudag/cli/main.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
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
|
+
"""Main CLI entrypoint for CUDAG."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from cudag import __version__
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.version_option(version=__version__)
|
|
18
|
+
def cli() -> None:
|
|
19
|
+
"""CUDAG - ComputerUseDataAugmentedGeneration framework.
|
|
20
|
+
|
|
21
|
+
Create generator projects with 'cudag new', then generate datasets
|
|
22
|
+
with 'cudag generate'.
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli.command()
|
|
28
|
+
@click.argument("name")
|
|
29
|
+
@click.option(
|
|
30
|
+
"--output-dir",
|
|
31
|
+
"-o",
|
|
32
|
+
type=click.Path(),
|
|
33
|
+
default=".",
|
|
34
|
+
help="Directory to create the project in (default: current directory)",
|
|
35
|
+
)
|
|
36
|
+
def new(name: str, output_dir: str) -> None:
|
|
37
|
+
"""Create a new CUDAG project.
|
|
38
|
+
|
|
39
|
+
NAME is the project name (e.g., 'appointment-picker').
|
|
40
|
+
"""
|
|
41
|
+
from cudag.cli.new import create_project
|
|
42
|
+
|
|
43
|
+
project_dir = create_project(name, Path(output_dir))
|
|
44
|
+
click.echo(f"Created project: {project_dir}")
|
|
45
|
+
click.echo("\nNext steps:")
|
|
46
|
+
click.echo(f" cd {project_dir}")
|
|
47
|
+
click.echo(" # Edit screen.py, state.py, renderer.py, and tasks/")
|
|
48
|
+
click.echo(" cudag generate --config config/dataset.yaml")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.command()
|
|
52
|
+
@click.option(
|
|
53
|
+
"--config",
|
|
54
|
+
"-c",
|
|
55
|
+
type=click.Path(exists=True),
|
|
56
|
+
required=True,
|
|
57
|
+
help="Path to dataset config YAML",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--output-dir",
|
|
61
|
+
"-o",
|
|
62
|
+
type=click.Path(),
|
|
63
|
+
help="Override output directory",
|
|
64
|
+
)
|
|
65
|
+
def generate(config: str, output_dir: str | None) -> None:
|
|
66
|
+
"""Generate a dataset from the current project.
|
|
67
|
+
|
|
68
|
+
Requires a dataset config file (YAML) and the project's screen/task definitions.
|
|
69
|
+
"""
|
|
70
|
+
config_path = Path(config)
|
|
71
|
+
click.echo(f"Loading config: {config_path}")
|
|
72
|
+
|
|
73
|
+
# TODO: Implement full generation by loading project modules
|
|
74
|
+
# For now, show what would be done
|
|
75
|
+
click.echo("Generation not yet implemented - use project's generate.py directly")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@cli.command()
|
|
79
|
+
@click.argument("dataset_dir", type=click.Path(exists=True))
|
|
80
|
+
def upload(dataset_dir: str) -> None:
|
|
81
|
+
"""Upload a dataset to Modal volume.
|
|
82
|
+
|
|
83
|
+
DATASET_DIR is the path to the generated dataset directory.
|
|
84
|
+
"""
|
|
85
|
+
click.echo(f"Uploading: {dataset_dir}")
|
|
86
|
+
click.echo("Upload not yet implemented")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cli.command()
|
|
90
|
+
@click.argument("dataset_dir", type=click.Path(exists=True))
|
|
91
|
+
@click.option(
|
|
92
|
+
"--verbose",
|
|
93
|
+
"-v",
|
|
94
|
+
is_flag=True,
|
|
95
|
+
help="Show all errors (default: first 10)",
|
|
96
|
+
)
|
|
97
|
+
def validate(dataset_dir: str, verbose: bool) -> None:
|
|
98
|
+
"""Validate a dataset against the CUDAG schema.
|
|
99
|
+
|
|
100
|
+
DATASET_DIR is the path to the generated dataset directory.
|
|
101
|
+
|
|
102
|
+
Checks:
|
|
103
|
+
- Required filesystem structure (images/, test/, etc.)
|
|
104
|
+
- Training record schema (data.jsonl, train.jsonl, val.jsonl)
|
|
105
|
+
- Test record schema (test/test.json)
|
|
106
|
+
- Image path validity (all referenced images exist)
|
|
107
|
+
|
|
108
|
+
Exit codes:
|
|
109
|
+
- 0: Dataset is valid
|
|
110
|
+
- 1: Validation errors found
|
|
111
|
+
"""
|
|
112
|
+
from cudag.validation import validate_dataset
|
|
113
|
+
|
|
114
|
+
dataset_path = Path(dataset_dir)
|
|
115
|
+
errors = validate_dataset(dataset_path)
|
|
116
|
+
|
|
117
|
+
if not errors:
|
|
118
|
+
click.secho(f"Dataset valid: {dataset_dir}", fg="green")
|
|
119
|
+
raise SystemExit(0)
|
|
120
|
+
|
|
121
|
+
# Show errors
|
|
122
|
+
click.secho(f"Found {len(errors)} validation error(s):", fg="red")
|
|
123
|
+
display_errors = errors if verbose else errors[:10]
|
|
124
|
+
for error in display_errors:
|
|
125
|
+
click.echo(f" {error}")
|
|
126
|
+
|
|
127
|
+
if not verbose and len(errors) > 10:
|
|
128
|
+
click.echo(f" ... and {len(errors) - 10} more (use -v to see all)")
|
|
129
|
+
|
|
130
|
+
raise SystemExit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@cli.group()
|
|
134
|
+
def eval() -> None:
|
|
135
|
+
"""Evaluation commands."""
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@eval.command("generate")
|
|
140
|
+
@click.option("--count", "-n", default=100, help="Number of eval cases")
|
|
141
|
+
@click.option("--dataset-dir", type=click.Path(exists=True), help="Dataset directory")
|
|
142
|
+
def eval_generate(count: int, dataset_dir: str | None) -> None:
|
|
143
|
+
"""Generate evaluation cases."""
|
|
144
|
+
click.echo(f"Generating {count} eval cases")
|
|
145
|
+
click.echo("Eval generation not yet implemented")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@eval.command("run")
|
|
149
|
+
@click.option("--checkpoint", type=click.Path(exists=True), help="Model checkpoint")
|
|
150
|
+
@click.option("--dataset-dir", type=click.Path(exists=True), help="Dataset directory")
|
|
151
|
+
def eval_run(checkpoint: str | None, dataset_dir: str | None) -> None:
|
|
152
|
+
"""Run evaluations on Modal."""
|
|
153
|
+
click.echo("Running evaluations")
|
|
154
|
+
click.echo("Eval running not yet implemented")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@cli.command()
|
|
158
|
+
def datasets() -> None:
|
|
159
|
+
"""List datasets on Modal volume."""
|
|
160
|
+
click.echo("Listing datasets on Modal volume...")
|
|
161
|
+
click.echo("Dataset listing not yet implemented")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@cli.command()
|
|
165
|
+
@click.option(
|
|
166
|
+
"--host",
|
|
167
|
+
"-h",
|
|
168
|
+
default="127.0.0.1",
|
|
169
|
+
help="Host to bind to (default: 127.0.0.1)",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--port",
|
|
173
|
+
"-p",
|
|
174
|
+
default=8420,
|
|
175
|
+
help="Port to listen on (default: 8420)",
|
|
176
|
+
)
|
|
177
|
+
@click.option(
|
|
178
|
+
"--reload",
|
|
179
|
+
is_flag=True,
|
|
180
|
+
help="Enable auto-reload for development",
|
|
181
|
+
)
|
|
182
|
+
def serve(host: str, port: int, reload: bool) -> None:
|
|
183
|
+
"""Start the CUDAG server for annotation integration.
|
|
184
|
+
|
|
185
|
+
The server provides a REST API that the Annotator UI can use
|
|
186
|
+
to generate CUDAG projects without using the terminal.
|
|
187
|
+
|
|
188
|
+
Endpoints:
|
|
189
|
+
GET /health - Health check
|
|
190
|
+
POST /api/v1/generate - Generate project from annotation
|
|
191
|
+
GET /api/v1/status/{job_id} - Check generation progress
|
|
192
|
+
"""
|
|
193
|
+
from cudag.server import run_server
|
|
194
|
+
|
|
195
|
+
click.echo(f"Starting CUDAG server on http://{host}:{port}")
|
|
196
|
+
click.echo("Press Ctrl+C to stop")
|
|
197
|
+
run_server(host=host, port=port, reload=reload)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@cli.command("from-annotation")
|
|
201
|
+
@click.argument("annotation_path", type=click.Path(exists=True))
|
|
202
|
+
@click.option(
|
|
203
|
+
"--output-dir",
|
|
204
|
+
"-o",
|
|
205
|
+
type=click.Path(),
|
|
206
|
+
default=".",
|
|
207
|
+
help="Directory to create the project in (default: current directory)",
|
|
208
|
+
)
|
|
209
|
+
@click.option(
|
|
210
|
+
"--name",
|
|
211
|
+
"-n",
|
|
212
|
+
help="Project name (default: derived from annotation)",
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--in-place",
|
|
216
|
+
"-i",
|
|
217
|
+
is_flag=True,
|
|
218
|
+
help="Write directly to output-dir without creating a subdirectory",
|
|
219
|
+
)
|
|
220
|
+
def from_annotation(
|
|
221
|
+
annotation_path: str, output_dir: str, name: str | None, in_place: bool
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Create a CUDAG project from an annotation folder or ZIP.
|
|
224
|
+
|
|
225
|
+
ANNOTATION_PATH is the path to an annotation folder or .zip file
|
|
226
|
+
exported from the Annotator application. The folder should contain:
|
|
227
|
+
- annotation.json: Element and task definitions
|
|
228
|
+
- original.png: Original screenshot
|
|
229
|
+
- masked.png: Screenshot with masked regions
|
|
230
|
+
- icons/: Optional folder with extracted icons
|
|
231
|
+
|
|
232
|
+
This generates a complete project structure with:
|
|
233
|
+
- screen.py: Screen definition with regions
|
|
234
|
+
- state.py: State class for dynamic content
|
|
235
|
+
- renderer.py: Renderer using the masked image
|
|
236
|
+
- tasks/: Task files for each defined task
|
|
237
|
+
- config/: Dataset configuration
|
|
238
|
+
- assets/: Base images and icons
|
|
239
|
+
"""
|
|
240
|
+
import zipfile
|
|
241
|
+
from cudag.annotation import AnnotationLoader, scaffold_generator
|
|
242
|
+
|
|
243
|
+
loader = AnnotationLoader()
|
|
244
|
+
annotation_source = Path(annotation_path)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
parsed = loader.load(annotation_source)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
click.secho(f"Error loading annotation: {e}", fg="red")
|
|
250
|
+
raise SystemExit(1)
|
|
251
|
+
|
|
252
|
+
project_name = name or parsed.screen_name
|
|
253
|
+
|
|
254
|
+
# Load images from folder or ZIP
|
|
255
|
+
if annotation_source.is_dir():
|
|
256
|
+
# Load from folder
|
|
257
|
+
original_path = annotation_source / "original.png"
|
|
258
|
+
masked_path = annotation_source / "masked.png"
|
|
259
|
+
icons_dir = annotation_source / "icons"
|
|
260
|
+
|
|
261
|
+
original_bytes = original_path.read_bytes() if original_path.exists() else None
|
|
262
|
+
masked_bytes = masked_path.read_bytes() if masked_path.exists() else None
|
|
263
|
+
|
|
264
|
+
icons: dict[str, bytes] = {}
|
|
265
|
+
if icons_dir.exists():
|
|
266
|
+
for icon_file in icons_dir.glob("*.png"):
|
|
267
|
+
icons[icon_file.stem] = icon_file.read_bytes()
|
|
268
|
+
else:
|
|
269
|
+
# Load from ZIP
|
|
270
|
+
if not annotation_source.suffix == ".zip":
|
|
271
|
+
click.secho("Error: Expected a folder or .zip file", fg="red")
|
|
272
|
+
raise SystemExit(1)
|
|
273
|
+
|
|
274
|
+
with zipfile.ZipFile(annotation_source) as zf:
|
|
275
|
+
original_bytes = (
|
|
276
|
+
zf.read("original.png") if "original.png" in zf.namelist() else None
|
|
277
|
+
)
|
|
278
|
+
masked_bytes = (
|
|
279
|
+
zf.read("masked.png") if "masked.png" in zf.namelist() else None
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
icons = {}
|
|
283
|
+
for filename in zf.namelist():
|
|
284
|
+
if filename.startswith("icons/") and filename.endswith(".png"):
|
|
285
|
+
icon_name = Path(filename).stem
|
|
286
|
+
icons[icon_name] = zf.read(filename)
|
|
287
|
+
|
|
288
|
+
# Scaffold project
|
|
289
|
+
output_path = Path(output_dir)
|
|
290
|
+
files = scaffold_generator(
|
|
291
|
+
name=project_name,
|
|
292
|
+
annotation=parsed,
|
|
293
|
+
output_dir=output_path,
|
|
294
|
+
original_image=original_bytes,
|
|
295
|
+
masked_image=masked_bytes,
|
|
296
|
+
icons=icons,
|
|
297
|
+
in_place=in_place,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
project_dir = output_path if in_place else output_path / project_name
|
|
301
|
+
click.secho(f"Created project: {project_dir}", fg="green")
|
|
302
|
+
click.echo(f"\nGenerated {len(files)} files:")
|
|
303
|
+
for f in files[:10]:
|
|
304
|
+
click.echo(f" {f.relative_to(project_dir)}")
|
|
305
|
+
if len(files) > 10:
|
|
306
|
+
click.echo(f" ... and {len(files) - 10} more")
|
|
307
|
+
|
|
308
|
+
click.echo("\nNext steps:")
|
|
309
|
+
click.echo(f" cd {project_dir}")
|
|
310
|
+
click.echo(" # Review and customize generated code")
|
|
311
|
+
click.echo(" python generator.py --samples 100")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
cli()
|