design-clone 1.0.0
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.
- package/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/SKILL.md +239 -0
- package/bin/cli.js +45 -0
- package/bin/commands/help.js +29 -0
- package/bin/commands/init.js +126 -0
- package/bin/commands/verify.js +99 -0
- package/bin/utils/copy.js +65 -0
- package/bin/utils/validate.js +122 -0
- package/docs/basic-clone.md +63 -0
- package/docs/cli-reference.md +94 -0
- package/docs/design-clone-architecture.md +247 -0
- package/docs/pixel-perfect.md +86 -0
- package/docs/troubleshooting.md +97 -0
- package/package.json +57 -0
- package/requirements.txt +5 -0
- package/src/ai/analyze-structure.py +305 -0
- package/src/ai/extract-design-tokens.py +439 -0
- package/src/ai/prompts/__init__.py +2 -0
- package/src/ai/prompts/design_tokens.py +183 -0
- package/src/ai/prompts/structure_analysis.py +273 -0
- package/src/core/cookie-handler.js +76 -0
- package/src/core/css-extractor.js +107 -0
- package/src/core/dimension-extractor.js +366 -0
- package/src/core/dimension-output.js +208 -0
- package/src/core/extract-assets.js +468 -0
- package/src/core/filter-css.js +499 -0
- package/src/core/html-extractor.js +102 -0
- package/src/core/lazy-loader.js +188 -0
- package/src/core/page-readiness.js +161 -0
- package/src/core/screenshot.js +380 -0
- package/src/post-process/enhance-assets.js +157 -0
- package/src/post-process/fetch-images.js +398 -0
- package/src/post-process/inject-icons.js +311 -0
- package/src/utils/__init__.py +16 -0
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/browser.js +103 -0
- package/src/utils/env.js +153 -0
- package/src/utils/env.py +134 -0
- package/src/utils/helpers.js +71 -0
- package/src/utils/puppeteer.js +281 -0
- package/src/verification/verify-layout.js +424 -0
- package/src/verification/verify-menu.js +422 -0
- package/templates/base.css +705 -0
- package/templates/base.html +293 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Extract design tokens from website screenshots using Gemini Vision API.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python extract-design-tokens.py --screenshots ./analysis --output ./output
|
|
7
|
+
python extract-design-tokens.py -s ./analysis -o ./out --css source.css
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--screenshots Directory containing desktop.png, tablet.png, mobile.png
|
|
11
|
+
--output Output directory for design-tokens.json and tokens.css
|
|
12
|
+
--css Path to filtered CSS file for exact token extraction (optional)
|
|
13
|
+
--model Gemini model (default: gemini-2.5-flash)
|
|
14
|
+
--verbose Enable verbose output
|
|
15
|
+
|
|
16
|
+
Output:
|
|
17
|
+
- design-tokens.json: Machine-readable tokens
|
|
18
|
+
- tokens.css: CSS custom properties
|
|
19
|
+
|
|
20
|
+
When CSS provided, extracts EXACT colors/fonts from source instead of estimating.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Dict, Optional
|
|
30
|
+
|
|
31
|
+
# Add src directory to path for local imports
|
|
32
|
+
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
33
|
+
SRC_DIR = SCRIPT_DIR.parent
|
|
34
|
+
sys.path.insert(0, str(SRC_DIR))
|
|
35
|
+
|
|
36
|
+
# Import local env resolver (portable)
|
|
37
|
+
try:
|
|
38
|
+
from utils.env import resolve_env, load_env
|
|
39
|
+
load_env() # Load .env files on startup
|
|
40
|
+
except ImportError:
|
|
41
|
+
# Fallback: simple env getter
|
|
42
|
+
def resolve_env(key, default=None):
|
|
43
|
+
return os.environ.get(key, default)
|
|
44
|
+
|
|
45
|
+
# Check for google-genai dependency
|
|
46
|
+
try:
|
|
47
|
+
from google import genai
|
|
48
|
+
from google.genai import types
|
|
49
|
+
except ImportError:
|
|
50
|
+
print(json.dumps({
|
|
51
|
+
"success": False,
|
|
52
|
+
"error": "google-genai not installed",
|
|
53
|
+
"hint": "Run: pip install google-genai"
|
|
54
|
+
}, indent=2))
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
# Import prompts from extracted module
|
|
58
|
+
from prompts.design_tokens import build_extraction_prompt
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Default tokens (fallback)
|
|
62
|
+
DEFAULT_TOKENS = {
|
|
63
|
+
"colors": {
|
|
64
|
+
"primary": "#2563eb",
|
|
65
|
+
"secondary": "#64748b",
|
|
66
|
+
"accent": "#f59e0b",
|
|
67
|
+
"background": "#ffffff",
|
|
68
|
+
"surface": "#f8fafc",
|
|
69
|
+
"text": {
|
|
70
|
+
"primary": "#0f172a",
|
|
71
|
+
"secondary": "#475569",
|
|
72
|
+
"muted": "#94a3b8"
|
|
73
|
+
},
|
|
74
|
+
"border": "#e2e8f0"
|
|
75
|
+
},
|
|
76
|
+
"typography": {
|
|
77
|
+
"fontFamily": {
|
|
78
|
+
"heading": "Inter, sans-serif",
|
|
79
|
+
"body": "Inter, sans-serif"
|
|
80
|
+
},
|
|
81
|
+
"fontSize": {
|
|
82
|
+
"xs": "12px",
|
|
83
|
+
"sm": "14px",
|
|
84
|
+
"base": "16px",
|
|
85
|
+
"lg": "18px",
|
|
86
|
+
"xl": "20px",
|
|
87
|
+
"2xl": "24px",
|
|
88
|
+
"3xl": "30px",
|
|
89
|
+
"4xl": "36px"
|
|
90
|
+
},
|
|
91
|
+
"fontWeight": {
|
|
92
|
+
"normal": 400,
|
|
93
|
+
"medium": 500,
|
|
94
|
+
"semibold": 600,
|
|
95
|
+
"bold": 700
|
|
96
|
+
},
|
|
97
|
+
"lineHeight": {
|
|
98
|
+
"tight": 1.25,
|
|
99
|
+
"normal": 1.5,
|
|
100
|
+
"relaxed": 1.75
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"spacing": {
|
|
104
|
+
"1": "4px",
|
|
105
|
+
"2": "8px",
|
|
106
|
+
"3": "12px",
|
|
107
|
+
"4": "16px",
|
|
108
|
+
"6": "24px",
|
|
109
|
+
"8": "32px",
|
|
110
|
+
"12": "48px",
|
|
111
|
+
"16": "64px"
|
|
112
|
+
},
|
|
113
|
+
"borderRadius": {
|
|
114
|
+
"sm": "4px",
|
|
115
|
+
"md": "8px",
|
|
116
|
+
"lg": "16px",
|
|
117
|
+
"full": "9999px"
|
|
118
|
+
},
|
|
119
|
+
"shadows": {
|
|
120
|
+
"sm": "0 1px 2px rgba(0,0,0,0.05)",
|
|
121
|
+
"md": "0 4px 6px rgba(0,0,0,0.1)",
|
|
122
|
+
"lg": "0 10px 15px rgba(0,0,0,0.1)"
|
|
123
|
+
},
|
|
124
|
+
"notes": ["Using default tokens - extraction failed or was not performed"]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_api_key() -> Optional[str]:
|
|
129
|
+
"""Get Gemini API key from environment (supports GEMINI_API_KEY or GOOGLE_API_KEY)."""
|
|
130
|
+
return resolve_env('GEMINI_API_KEY') or resolve_env('GOOGLE_API_KEY')
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_hex_color(color: str) -> bool:
|
|
134
|
+
"""Validate hex color format."""
|
|
135
|
+
return bool(re.match(r'^#[0-9A-Fa-f]{6}$', color))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_tokens(tokens: Dict[str, Any]) -> tuple[bool, list[str]]:
|
|
139
|
+
"""Validate extracted tokens, return (is_valid, errors)."""
|
|
140
|
+
errors = []
|
|
141
|
+
|
|
142
|
+
# Check colors
|
|
143
|
+
if 'colors' in tokens:
|
|
144
|
+
colors = tokens['colors']
|
|
145
|
+
for key in ['primary', 'secondary', 'accent', 'background', 'surface', 'border']:
|
|
146
|
+
if key in colors and not validate_hex_color(colors[key]):
|
|
147
|
+
errors.append(f"Invalid hex color: colors.{key} = {colors[key]}")
|
|
148
|
+
|
|
149
|
+
if 'text' in colors:
|
|
150
|
+
for key in ['primary', 'secondary', 'muted']:
|
|
151
|
+
if key in colors['text'] and not validate_hex_color(colors['text'][key]):
|
|
152
|
+
errors.append(f"Invalid hex color: colors.text.{key} = {colors['text'][key]}")
|
|
153
|
+
|
|
154
|
+
return len(errors) == 0, errors
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def merge_with_defaults(tokens: Dict[str, Any]) -> Dict[str, Any]:
|
|
158
|
+
"""Merge extracted tokens with defaults for missing values."""
|
|
159
|
+
def deep_merge(base: dict, override: dict) -> dict:
|
|
160
|
+
result = base.copy()
|
|
161
|
+
for key, value in override.items():
|
|
162
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
163
|
+
result[key] = deep_merge(result[key], value)
|
|
164
|
+
else:
|
|
165
|
+
result[key] = value
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
return deep_merge(DEFAULT_TOKENS.copy(), tokens)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def generate_tokens_css(tokens: Dict[str, Any]) -> str:
|
|
172
|
+
"""Generate tokens.css from design tokens."""
|
|
173
|
+
lines = [
|
|
174
|
+
"/* Design Tokens - Auto-generated */",
|
|
175
|
+
"/* Edit values below to customize the design */",
|
|
176
|
+
"",
|
|
177
|
+
":root {",
|
|
178
|
+
" /* Colors */",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
colors = tokens.get('colors', {})
|
|
182
|
+
lines.append(f" --color-primary: {colors.get('primary', '#2563eb')};")
|
|
183
|
+
lines.append(f" --color-secondary: {colors.get('secondary', '#64748b')};")
|
|
184
|
+
lines.append(f" --color-accent: {colors.get('accent', '#f59e0b')};")
|
|
185
|
+
lines.append(f" --color-background: {colors.get('background', '#ffffff')};")
|
|
186
|
+
lines.append(f" --color-surface: {colors.get('surface', '#f8fafc')};")
|
|
187
|
+
|
|
188
|
+
text_colors = colors.get('text', {})
|
|
189
|
+
lines.append(f" --color-text-primary: {text_colors.get('primary', '#0f172a')};")
|
|
190
|
+
lines.append(f" --color-text-secondary: {text_colors.get('secondary', '#475569')};")
|
|
191
|
+
lines.append(f" --color-text-muted: {text_colors.get('muted', '#94a3b8')};")
|
|
192
|
+
lines.append(f" --color-border: {colors.get('border', '#e2e8f0')};")
|
|
193
|
+
|
|
194
|
+
lines.append("")
|
|
195
|
+
lines.append(" /* Typography */")
|
|
196
|
+
|
|
197
|
+
typography = tokens.get('typography', {})
|
|
198
|
+
font_family = typography.get('fontFamily', {})
|
|
199
|
+
lines.append(f" --font-heading: {font_family.get('heading', 'Inter, sans-serif')};")
|
|
200
|
+
lines.append(f" --font-body: {font_family.get('body', 'Inter, sans-serif')};")
|
|
201
|
+
|
|
202
|
+
font_sizes = typography.get('fontSize', {})
|
|
203
|
+
for key in ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl']:
|
|
204
|
+
default = DEFAULT_TOKENS['typography']['fontSize'].get(key, '16px')
|
|
205
|
+
lines.append(f" --font-size-{key}: {font_sizes.get(key, default)};")
|
|
206
|
+
|
|
207
|
+
font_weights = typography.get('fontWeight', {})
|
|
208
|
+
for key in ['normal', 'medium', 'semibold', 'bold']:
|
|
209
|
+
default = DEFAULT_TOKENS['typography']['fontWeight'].get(key, 400)
|
|
210
|
+
lines.append(f" --font-weight-{key}: {font_weights.get(key, default)};")
|
|
211
|
+
|
|
212
|
+
line_heights = typography.get('lineHeight', {})
|
|
213
|
+
for key in ['tight', 'normal', 'relaxed']:
|
|
214
|
+
default = DEFAULT_TOKENS['typography']['lineHeight'].get(key, 1.5)
|
|
215
|
+
lines.append(f" --line-height-{key}: {line_heights.get(key, default)};")
|
|
216
|
+
|
|
217
|
+
lines.append("")
|
|
218
|
+
lines.append(" /* Spacing */")
|
|
219
|
+
|
|
220
|
+
spacing = tokens.get('spacing', {})
|
|
221
|
+
for key in ['1', '2', '3', '4', '6', '8', '12', '16']:
|
|
222
|
+
default = DEFAULT_TOKENS['spacing'].get(key, '16px')
|
|
223
|
+
lines.append(f" --space-{key}: {spacing.get(key, default)};")
|
|
224
|
+
|
|
225
|
+
lines.append("")
|
|
226
|
+
lines.append(" /* Border Radius */")
|
|
227
|
+
|
|
228
|
+
border_radius = tokens.get('borderRadius', {})
|
|
229
|
+
for key in ['sm', 'md', 'lg', 'full']:
|
|
230
|
+
default = DEFAULT_TOKENS['borderRadius'].get(key, '8px')
|
|
231
|
+
lines.append(f" --radius-{key}: {border_radius.get(key, default)};")
|
|
232
|
+
|
|
233
|
+
lines.append("")
|
|
234
|
+
lines.append(" /* Shadows */")
|
|
235
|
+
|
|
236
|
+
shadows = tokens.get('shadows', {})
|
|
237
|
+
for key in ['sm', 'md', 'lg']:
|
|
238
|
+
default = DEFAULT_TOKENS['shadows'].get(key, '0 1px 2px rgba(0,0,0,0.05)')
|
|
239
|
+
lines.append(f" --shadow-{key}: {shadows.get(key, default)};")
|
|
240
|
+
|
|
241
|
+
lines.append("}")
|
|
242
|
+
lines.append("")
|
|
243
|
+
|
|
244
|
+
return "\n".join(lines)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def extract_tokens(
|
|
248
|
+
screenshots_dir: str,
|
|
249
|
+
css_path: str = None,
|
|
250
|
+
model: str = "gemini-2.5-flash",
|
|
251
|
+
verbose: bool = False
|
|
252
|
+
) -> Dict[str, Any]:
|
|
253
|
+
"""Extract design tokens from screenshots using Gemini Vision.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
screenshots_dir: Directory containing screenshots
|
|
257
|
+
css_path: Optional path to filtered CSS (improves accuracy)
|
|
258
|
+
model: Gemini model to use
|
|
259
|
+
verbose: Enable verbose output
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Design tokens dictionary
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
api_key = get_api_key()
|
|
266
|
+
if not api_key:
|
|
267
|
+
if verbose:
|
|
268
|
+
print("Warning: GEMINI_API_KEY not found, using default tokens")
|
|
269
|
+
return DEFAULT_TOKENS.copy()
|
|
270
|
+
|
|
271
|
+
# Load CSS if provided
|
|
272
|
+
css_content = None
|
|
273
|
+
if css_path and Path(css_path).exists():
|
|
274
|
+
with open(css_path, 'r', encoding='utf-8') as f:
|
|
275
|
+
css_content = f.read()
|
|
276
|
+
if verbose:
|
|
277
|
+
print(f"Loaded CSS: {len(css_content)} chars")
|
|
278
|
+
|
|
279
|
+
# Build prompt with context
|
|
280
|
+
prompt = build_extraction_prompt(css_content)
|
|
281
|
+
|
|
282
|
+
if verbose and css_content:
|
|
283
|
+
print("Using enhanced prompt with CSS context")
|
|
284
|
+
|
|
285
|
+
# Find screenshots
|
|
286
|
+
screenshots_path = Path(screenshots_dir)
|
|
287
|
+
desktop = screenshots_path / "desktop.png"
|
|
288
|
+
tablet = screenshots_path / "tablet.png"
|
|
289
|
+
mobile = screenshots_path / "mobile.png"
|
|
290
|
+
|
|
291
|
+
# Check which files exist
|
|
292
|
+
available_images = []
|
|
293
|
+
for img in [desktop, tablet, mobile]:
|
|
294
|
+
if img.exists():
|
|
295
|
+
available_images.append(img)
|
|
296
|
+
if verbose:
|
|
297
|
+
print(f"Found: {img}")
|
|
298
|
+
|
|
299
|
+
if not available_images:
|
|
300
|
+
if verbose:
|
|
301
|
+
print("Warning: No screenshots found, using default tokens")
|
|
302
|
+
return DEFAULT_TOKENS.copy()
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
# Initialize client
|
|
306
|
+
client = genai.Client(api_key=api_key)
|
|
307
|
+
|
|
308
|
+
# Build content with images
|
|
309
|
+
content = [prompt]
|
|
310
|
+
|
|
311
|
+
for img_path in available_images:
|
|
312
|
+
with open(img_path, 'rb') as f:
|
|
313
|
+
img_bytes = f.read()
|
|
314
|
+
content.append(
|
|
315
|
+
types.Part.from_bytes(data=img_bytes, mime_type='image/png')
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if verbose:
|
|
319
|
+
print(f"Sending {len(available_images)} images to {model}...")
|
|
320
|
+
|
|
321
|
+
# Request structured JSON output
|
|
322
|
+
config = types.GenerateContentConfig(
|
|
323
|
+
response_mime_type='application/json'
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
response = client.models.generate_content(
|
|
327
|
+
model=model,
|
|
328
|
+
contents=content,
|
|
329
|
+
config=config
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Parse response
|
|
333
|
+
if hasattr(response, 'text') and response.text:
|
|
334
|
+
tokens = json.loads(response.text)
|
|
335
|
+
|
|
336
|
+
# Validate
|
|
337
|
+
is_valid, errors = validate_tokens(tokens)
|
|
338
|
+
if not is_valid:
|
|
339
|
+
if verbose:
|
|
340
|
+
print(f"Validation warnings: {errors}")
|
|
341
|
+
tokens['notes'] = tokens.get('notes', []) + errors
|
|
342
|
+
|
|
343
|
+
# Merge with defaults for missing values
|
|
344
|
+
tokens = merge_with_defaults(tokens)
|
|
345
|
+
|
|
346
|
+
if verbose:
|
|
347
|
+
print("Tokens extracted successfully")
|
|
348
|
+
|
|
349
|
+
return tokens
|
|
350
|
+
else:
|
|
351
|
+
if verbose:
|
|
352
|
+
print("Warning: Empty response, using default tokens")
|
|
353
|
+
return DEFAULT_TOKENS.copy()
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
if verbose:
|
|
357
|
+
print(f"Error during extraction: {e}")
|
|
358
|
+
|
|
359
|
+
# Return defaults with error note
|
|
360
|
+
tokens = DEFAULT_TOKENS.copy()
|
|
361
|
+
tokens['notes'] = [f"Extraction failed: {str(e)}"]
|
|
362
|
+
return tokens
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def main():
|
|
366
|
+
parser = argparse.ArgumentParser(
|
|
367
|
+
description="Extract design tokens from screenshots using Gemini Vision"
|
|
368
|
+
)
|
|
369
|
+
parser.add_argument(
|
|
370
|
+
'--screenshots', '-s',
|
|
371
|
+
required=True,
|
|
372
|
+
help='Directory containing screenshots (desktop.png, tablet.png, mobile.png)'
|
|
373
|
+
)
|
|
374
|
+
parser.add_argument(
|
|
375
|
+
'--output', '-o',
|
|
376
|
+
required=True,
|
|
377
|
+
help='Output directory for design-tokens.json and tokens.css'
|
|
378
|
+
)
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
'--css',
|
|
381
|
+
default=None,
|
|
382
|
+
help='Path to filtered CSS file for exact token extraction (optional)'
|
|
383
|
+
)
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
'--model', '-m',
|
|
386
|
+
default='gemini-2.5-flash',
|
|
387
|
+
help='Gemini model to use (default: gemini-2.5-flash)'
|
|
388
|
+
)
|
|
389
|
+
parser.add_argument(
|
|
390
|
+
'--verbose', '-v',
|
|
391
|
+
action='store_true',
|
|
392
|
+
help='Enable verbose output'
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
args = parser.parse_args()
|
|
396
|
+
|
|
397
|
+
# Create output directory
|
|
398
|
+
output_path = Path(args.output)
|
|
399
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
400
|
+
|
|
401
|
+
# Extract tokens
|
|
402
|
+
tokens = extract_tokens(
|
|
403
|
+
screenshots_dir=args.screenshots,
|
|
404
|
+
css_path=args.css,
|
|
405
|
+
model=args.model,
|
|
406
|
+
verbose=args.verbose
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Save design-tokens.json
|
|
410
|
+
json_path = output_path / "design-tokens.json"
|
|
411
|
+
with open(json_path, 'w') as f:
|
|
412
|
+
json.dump(tokens, f, indent=2)
|
|
413
|
+
|
|
414
|
+
if args.verbose:
|
|
415
|
+
print(f"Saved: {json_path}")
|
|
416
|
+
|
|
417
|
+
# Generate and save tokens.css
|
|
418
|
+
css_content = generate_tokens_css(tokens)
|
|
419
|
+
css_path = output_path / "tokens.css"
|
|
420
|
+
with open(css_path, 'w') as f:
|
|
421
|
+
f.write(css_content)
|
|
422
|
+
|
|
423
|
+
if args.verbose:
|
|
424
|
+
print(f"Saved: {css_path}")
|
|
425
|
+
|
|
426
|
+
# Output result as JSON
|
|
427
|
+
result = {
|
|
428
|
+
"success": True,
|
|
429
|
+
"tokens_json": str(json_path),
|
|
430
|
+
"tokens_css": str(css_path),
|
|
431
|
+
"model": args.model,
|
|
432
|
+
"notes": tokens.get('notes', [])
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
print(json.dumps(result, indent=2))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if __name__ == '__main__':
|
|
439
|
+
main()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Design Token Extraction Prompts
|
|
3
|
+
|
|
4
|
+
Prompts for extracting design tokens from screenshots using Gemini Vision.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Design token extraction prompt (basic - screenshot only)
|
|
8
|
+
EXTRACTION_PROMPT = """Analyze these website screenshots (desktop, tablet, mobile) and extract design tokens.
|
|
9
|
+
|
|
10
|
+
Return ONLY valid JSON in this exact format:
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
"colors": {
|
|
14
|
+
"primary": "#hex",
|
|
15
|
+
"secondary": "#hex",
|
|
16
|
+
"accent": "#hex",
|
|
17
|
+
"background": "#hex",
|
|
18
|
+
"surface": "#hex",
|
|
19
|
+
"text": {
|
|
20
|
+
"primary": "#hex",
|
|
21
|
+
"secondary": "#hex",
|
|
22
|
+
"muted": "#hex"
|
|
23
|
+
},
|
|
24
|
+
"border": "#hex"
|
|
25
|
+
},
|
|
26
|
+
"typography": {
|
|
27
|
+
"fontFamily": {
|
|
28
|
+
"heading": "Font Name, sans-serif",
|
|
29
|
+
"body": "Font Name, sans-serif"
|
|
30
|
+
},
|
|
31
|
+
"fontSize": {
|
|
32
|
+
"xs": "12px",
|
|
33
|
+
"sm": "14px",
|
|
34
|
+
"base": "16px",
|
|
35
|
+
"lg": "18px",
|
|
36
|
+
"xl": "20px",
|
|
37
|
+
"2xl": "24px",
|
|
38
|
+
"3xl": "30px",
|
|
39
|
+
"4xl": "36px"
|
|
40
|
+
},
|
|
41
|
+
"fontWeight": {
|
|
42
|
+
"normal": 400,
|
|
43
|
+
"medium": 500,
|
|
44
|
+
"semibold": 600,
|
|
45
|
+
"bold": 700
|
|
46
|
+
},
|
|
47
|
+
"lineHeight": {
|
|
48
|
+
"tight": 1.25,
|
|
49
|
+
"normal": 1.5,
|
|
50
|
+
"relaxed": 1.75
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"spacing": {
|
|
54
|
+
"1": "4px",
|
|
55
|
+
"2": "8px",
|
|
56
|
+
"3": "12px",
|
|
57
|
+
"4": "16px",
|
|
58
|
+
"6": "24px",
|
|
59
|
+
"8": "32px",
|
|
60
|
+
"12": "48px",
|
|
61
|
+
"16": "64px"
|
|
62
|
+
},
|
|
63
|
+
"borderRadius": {
|
|
64
|
+
"sm": "4px",
|
|
65
|
+
"md": "8px",
|
|
66
|
+
"lg": "16px",
|
|
67
|
+
"full": "9999px"
|
|
68
|
+
},
|
|
69
|
+
"shadows": {
|
|
70
|
+
"sm": "0 1px 2px rgba(0,0,0,0.05)",
|
|
71
|
+
"md": "0 4px 6px rgba(0,0,0,0.1)",
|
|
72
|
+
"lg": "0 10px 15px rgba(0,0,0,0.1)"
|
|
73
|
+
},
|
|
74
|
+
"notes": []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
RULES:
|
|
78
|
+
1. Use exact 6-digit hex codes (#RRGGBB), not color names
|
|
79
|
+
2. Identify Google Fonts: Inter, Roboto, Open Sans, Poppins, Montserrat, Lato, Nunito, Raleway, Playfair Display, Merriweather
|
|
80
|
+
3. If font unknown, use reasonable fallback (sans-serif or serif)
|
|
81
|
+
4. Extract observed values; use sensible defaults if unclear
|
|
82
|
+
5. Detect spacing patterns (8px grid common)
|
|
83
|
+
6. Add any observations or accessibility concerns to notes array"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Enhanced prompt when CSS context is available
|
|
87
|
+
EXTRACTION_PROMPT_WITH_CSS = """Extract design tokens from the provided CSS and screenshots.
|
|
88
|
+
|
|
89
|
+
You have access to:
|
|
90
|
+
1. Screenshots showing the visual design
|
|
91
|
+
2. The actual CSS used on the page
|
|
92
|
+
|
|
93
|
+
CRITICAL: Extract EXACT values from the CSS. Do not estimate colors or fonts.
|
|
94
|
+
|
|
95
|
+
## Source CSS
|
|
96
|
+
```css
|
|
97
|
+
{css_content}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
Based on the CSS above, return ONLY valid JSON:
|
|
103
|
+
|
|
104
|
+
{{
|
|
105
|
+
"colors": {{
|
|
106
|
+
"primary": "[exact hex from CSS, look for primary/brand colors]",
|
|
107
|
+
"secondary": "[exact hex from CSS]",
|
|
108
|
+
"accent": "[exact hex for accent/highlight colors]",
|
|
109
|
+
"background": "[exact hex, usually body background]",
|
|
110
|
+
"surface": "[exact hex for cards/sections]",
|
|
111
|
+
"text": {{
|
|
112
|
+
"primary": "[exact hex, usually body color]",
|
|
113
|
+
"secondary": "[exact hex for muted text]",
|
|
114
|
+
"muted": "[exact hex for very light text]"
|
|
115
|
+
}},
|
|
116
|
+
"border": "[exact hex for borders]"
|
|
117
|
+
}},
|
|
118
|
+
"typography": {{
|
|
119
|
+
"fontFamily": {{
|
|
120
|
+
"heading": "[exact font-family from CSS h1-h6 rules]",
|
|
121
|
+
"body": "[exact font-family from CSS body rule]"
|
|
122
|
+
}},
|
|
123
|
+
"fontSize": {{
|
|
124
|
+
"xs": "[smallest font-size from CSS]",
|
|
125
|
+
"sm": "[small font-size]",
|
|
126
|
+
"base": "[body font-size]",
|
|
127
|
+
"lg": "[larger font-size]",
|
|
128
|
+
"xl": "[heading font-size]",
|
|
129
|
+
"2xl": "[h3 font-size]",
|
|
130
|
+
"3xl": "[h2 font-size]",
|
|
131
|
+
"4xl": "[h1 font-size]"
|
|
132
|
+
}},
|
|
133
|
+
"fontWeight": {{
|
|
134
|
+
"normal": [normal weight from CSS],
|
|
135
|
+
"medium": [medium weight],
|
|
136
|
+
"semibold": [semibold weight],
|
|
137
|
+
"bold": [bold weight from CSS]
|
|
138
|
+
}},
|
|
139
|
+
"lineHeight": {{
|
|
140
|
+
"tight": [tight line-height, usually 1.2-1.3],
|
|
141
|
+
"normal": [normal line-height, usually 1.5-1.6],
|
|
142
|
+
"relaxed": [relaxed line-height, usually 1.7-1.8]
|
|
143
|
+
}}
|
|
144
|
+
}},
|
|
145
|
+
"spacing": {{
|
|
146
|
+
"1": "[4px or smallest spacing]",
|
|
147
|
+
"2": "[8px]",
|
|
148
|
+
"3": "[12px]",
|
|
149
|
+
"4": "[16px - common padding]",
|
|
150
|
+
"6": "[24px]",
|
|
151
|
+
"8": "[32px]",
|
|
152
|
+
"12": "[48px - section padding]",
|
|
153
|
+
"16": "[64px - large section padding]"
|
|
154
|
+
}},
|
|
155
|
+
"borderRadius": {{
|
|
156
|
+
"sm": "[small radius from CSS]",
|
|
157
|
+
"md": "[medium radius]",
|
|
158
|
+
"lg": "[large radius]",
|
|
159
|
+
"full": "9999px"
|
|
160
|
+
}},
|
|
161
|
+
"shadows": {{
|
|
162
|
+
"sm": "[small shadow from CSS box-shadow]",
|
|
163
|
+
"md": "[medium shadow]",
|
|
164
|
+
"lg": "[large shadow]"
|
|
165
|
+
}},
|
|
166
|
+
"notes": ["List exact CSS custom properties/variables if found", "Note any @import URLs"]
|
|
167
|
+
}}
|
|
168
|
+
|
|
169
|
+
RULES:
|
|
170
|
+
1. Extract EXACT hex codes from CSS, not approximate
|
|
171
|
+
2. Copy font-family values exactly as written
|
|
172
|
+
3. Extract actual px/rem values, convert rem to px if needed (1rem = 16px)
|
|
173
|
+
4. Look for CSS custom properties (--color-*, --font-*, --space-*)
|
|
174
|
+
5. If a value isn't in CSS, use screenshot to estimate"""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_extraction_prompt(css_content: str = None) -> str:
|
|
178
|
+
"""Build prompt with or without CSS context."""
|
|
179
|
+
if css_content:
|
|
180
|
+
# Truncate if too long (15KB limit)
|
|
181
|
+
css_snippet = css_content[:15000] if len(css_content) > 15000 else css_content
|
|
182
|
+
return EXTRACTION_PROMPT_WITH_CSS.format(css_content=css_snippet)
|
|
183
|
+
return EXTRACTION_PROMPT
|