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,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
@@ -0,0 +1,5 @@
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
+ """CLI commands for CUDAG."""
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()