Spaces:
Sleeping
Sleeping
Commit
·
06966eb
1
Parent(s):
addf2c4
Implement complete Design Token Extractor system
Browse files- Add Gradio-based web interface for UI screenshot analysis
- Implement multi-model extraction pipeline with color, spacing, and typography detection
- Support 5 output formats: CSS Variables, Tailwind Config, JSON Tokens, Style Dictionary, SCSS
- Add computer vision-based spacing detection using OpenCV
- Include Pix2Struct integration for component understanding
- Optimize for HuggingFace Spaces deployment with resource management
- Add comprehensive error handling and fallback mechanisms
- Create modular architecture with separate extraction and generation components
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
- README.md +91 -6
- app.py +292 -0
- examples/placeholder.txt +6 -0
- requirements.txt +9 -0
- test_structure.py +49 -0
- utils/__init__.py +1 -0
- utils/extractor.py +182 -0
- utils/token_generator.py +203 -0
README.md
CHANGED
@@ -1,13 +1,98 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
-
sdk_version:
|
8 |
app_file: app.py
|
|
|
9 |
pinned: false
|
10 |
-
|
|
|
11 |
---
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: Design Token Extractor
|
3 |
+
emoji: 🎨
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 4.44.1
|
8 |
app_file: app.py
|
9 |
+
python_version: 3.10
|
10 |
pinned: false
|
11 |
+
license: apache-2.0
|
12 |
+
short_description: 'Transform UI screenshots into structured design token libraries'
|
13 |
---
|
14 |
|
15 |
+
# 🎨 Design Token Extractor
|
16 |
+
|
17 |
+
Transform UI screenshots into structured design token libraries using AI-powered analysis.
|
18 |
+
|
19 |
+
## Features
|
20 |
+
|
21 |
+
- **Color Extraction**: Identifies dominant colors and creates semantic color roles
|
22 |
+
- **Spacing Detection**: Analyzes layout patterns to extract consistent spacing values
|
23 |
+
- **Typography Analysis**: Detects font styles and creates text hierarchy tokens
|
24 |
+
- **Component Recognition**: Uses vision models to understand UI components
|
25 |
+
- **Multiple Output Formats**: Export to CSS Variables, Tailwind Config, JSON Tokens, Style Dictionary, or SCSS
|
26 |
+
|
27 |
+
## How It Works
|
28 |
+
|
29 |
+
1. **Upload a UI Screenshot**: Drag and drop or paste from clipboard
|
30 |
+
2. **Select Output Format**: Choose your preferred token format
|
31 |
+
3. **Extract Tokens**: The system analyzes your screenshot using computer vision
|
32 |
+
4. **Download Results**: Get your design tokens in the selected format
|
33 |
+
|
34 |
+
## Technology Stack
|
35 |
+
|
36 |
+
- **Gradio**: Interactive web interface
|
37 |
+
- **Colorgram.py**: Fast color extraction
|
38 |
+
- **OpenCV**: Image processing and spacing detection
|
39 |
+
- **Pix2Struct**: Layout and component understanding
|
40 |
+
- **PyTorch**: Deep learning framework
|
41 |
+
|
42 |
+
## Output Formats
|
43 |
+
|
44 |
+
### CSS Variables
|
45 |
+
```css
|
46 |
+
:root {
|
47 |
+
--color-primary: #3B82F6;
|
48 |
+
--spacing-medium: 16px;
|
49 |
+
--font-heading: sans-serif;
|
50 |
+
}
|
51 |
+
```
|
52 |
+
|
53 |
+
### Tailwind Config
|
54 |
+
```javascript
|
55 |
+
module.exports = {
|
56 |
+
theme: {
|
57 |
+
extend: {
|
58 |
+
colors: {
|
59 |
+
primary: '#3B82F6'
|
60 |
+
}
|
61 |
+
}
|
62 |
+
}
|
63 |
+
}
|
64 |
+
```
|
65 |
+
|
66 |
+
### JSON Tokens (W3C Format)
|
67 |
+
```json
|
68 |
+
{
|
69 |
+
"color": {
|
70 |
+
"primary": {
|
71 |
+
"$value": "#3B82F6",
|
72 |
+
"$type": "color"
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
```
|
77 |
+
|
78 |
+
## Tips for Best Results
|
79 |
+
|
80 |
+
- Use high-quality screenshots (minimum 800px width)
|
81 |
+
- Include various UI elements for comprehensive extraction
|
82 |
+
- Screenshots with clear color hierarchy work best
|
83 |
+
- Ensure good contrast between elements
|
84 |
+
|
85 |
+
## Development
|
86 |
+
|
87 |
+
To run locally:
|
88 |
+
|
89 |
+
```bash
|
90 |
+
pip install -r requirements.txt
|
91 |
+
python app.py
|
92 |
+
```
|
93 |
+
|
94 |
+
## License
|
95 |
+
|
96 |
+
Apache 2.0
|
97 |
+
|
98 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import json
|
3 |
+
import os
|
4 |
+
from PIL import Image
|
5 |
+
import tempfile
|
6 |
+
from utils.extractor import DesignTokenExtractor
|
7 |
+
from utils.token_generator import TokenCodeGenerator
|
8 |
+
|
9 |
+
|
10 |
+
def create_token_preview(tokens):
|
11 |
+
"""Create HTML preview of extracted tokens"""
|
12 |
+
html = """
|
13 |
+
<div style="font-family: system-ui, sans-serif; padding: 20px; background: #f9fafb; border-radius: 8px;">
|
14 |
+
<h3 style="margin-top: 0; color: #1f2937;">Extracted Design Tokens</h3>
|
15 |
+
"""
|
16 |
+
|
17 |
+
# Color palette preview
|
18 |
+
if 'colors' in tokens and tokens['colors']:
|
19 |
+
html += """
|
20 |
+
<div style="margin-bottom: 24px;">
|
21 |
+
<h4 style="color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em;">Colors</h4>
|
22 |
+
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
23 |
+
"""
|
24 |
+
for name, color in tokens['colors'].items():
|
25 |
+
html += f"""
|
26 |
+
<div style="text-align: center;">
|
27 |
+
<div style="width: 80px; height: 80px; background: {color['hex']};
|
28 |
+
border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"></div>
|
29 |
+
<div style="margin-top: 8px;">
|
30 |
+
<div style="font-size: 12px; font-weight: 600; color: #374151;">{name}</div>
|
31 |
+
<div style="font-size: 11px; color: #9ca3af;">{color['hex']}</div>
|
32 |
+
<div style="font-size: 10px; color: #9ca3af;">{int(color.get('proportion', 0) * 100)}%</div>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
"""
|
36 |
+
html += "</div></div>"
|
37 |
+
|
38 |
+
# Spacing preview
|
39 |
+
if 'spacing' in tokens and tokens['spacing']:
|
40 |
+
html += """
|
41 |
+
<div style="margin-bottom: 24px;">
|
42 |
+
<h4 style="color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em;">Spacing</h4>
|
43 |
+
<div style="display: flex; gap: 16px; align-items: flex-end;">
|
44 |
+
"""
|
45 |
+
for name, value in tokens['spacing'].items():
|
46 |
+
try:
|
47 |
+
height = value.replace('px', '')
|
48 |
+
html += f"""
|
49 |
+
<div style="text-align: center;">
|
50 |
+
<div style="width: 60px; height: {height}px; background: #3b82f6;
|
51 |
+
border-radius: 4px; opacity: 0.8;"></div>
|
52 |
+
<div style="margin-top: 8px;">
|
53 |
+
<div style="font-size: 12px; font-weight: 600; color: #374151;">{name}</div>
|
54 |
+
<div style="font-size: 11px; color: #9ca3af;">{value}</div>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
"""
|
58 |
+
except:
|
59 |
+
pass
|
60 |
+
html += "</div></div>"
|
61 |
+
|
62 |
+
# Typography preview
|
63 |
+
if 'typography' in tokens and tokens['typography']:
|
64 |
+
html += """
|
65 |
+
<div style="margin-bottom: 24px;">
|
66 |
+
<h4 style="color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em;">Typography</h4>
|
67 |
+
"""
|
68 |
+
for name, props in tokens['typography'].items():
|
69 |
+
size = props.get('size', '16px')
|
70 |
+
weight = props.get('weight', '400')
|
71 |
+
family = props.get('family', 'sans-serif')
|
72 |
+
html += f"""
|
73 |
+
<div style="margin-bottom: 12px; padding: 12px; background: white; border-radius: 6px;">
|
74 |
+
<div style="font-size: {size}; font-weight: {weight}; font-family: {family}; color: #1f2937;">
|
75 |
+
Sample {name.title()} Text
|
76 |
+
</div>
|
77 |
+
<div style="font-size: 11px; color: #9ca3af; margin-top: 4px;">
|
78 |
+
{family} • {size} • Weight {weight}
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
"""
|
82 |
+
html += "</div>"
|
83 |
+
|
84 |
+
html += "</div>"
|
85 |
+
return html
|
86 |
+
|
87 |
+
|
88 |
+
def process_screenshot(image, output_format, progress=gr.Progress()):
|
89 |
+
"""Process uploaded screenshot and extract design tokens"""
|
90 |
+
if image is None:
|
91 |
+
return None, "Please upload a screenshot", None
|
92 |
+
|
93 |
+
extractor = DesignTokenExtractor()
|
94 |
+
generator = TokenCodeGenerator()
|
95 |
+
|
96 |
+
try:
|
97 |
+
progress(0.1, desc="Initializing extraction...")
|
98 |
+
|
99 |
+
# Resize image if needed
|
100 |
+
image = extractor.resize_for_processing(image)
|
101 |
+
|
102 |
+
# Save temporary file for colorgram
|
103 |
+
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
104 |
+
temp_path = tmp.name
|
105 |
+
image.save(temp_path)
|
106 |
+
|
107 |
+
progress(0.3, desc="Extracting colors...")
|
108 |
+
colors = extractor.extract_colors(temp_path)
|
109 |
+
|
110 |
+
progress(0.5, desc="Detecting spacing...")
|
111 |
+
spacing = extractor.detect_spacing(image)
|
112 |
+
|
113 |
+
progress(0.6, desc="Analyzing typography...")
|
114 |
+
typography = extractor.detect_typography(image)
|
115 |
+
|
116 |
+
progress(0.7, desc="Analyzing components...")
|
117 |
+
components = extractor.analyze_components(image)
|
118 |
+
|
119 |
+
# Combine all tokens
|
120 |
+
tokens = {
|
121 |
+
"colors": colors,
|
122 |
+
"spacing": spacing,
|
123 |
+
"typography": typography,
|
124 |
+
"components": components
|
125 |
+
}
|
126 |
+
|
127 |
+
progress(0.8, desc="Generating code...")
|
128 |
+
|
129 |
+
# Generate output based on selected format
|
130 |
+
if output_format == "CSS Variables":
|
131 |
+
code_output = generator.generate_css_variables(tokens)
|
132 |
+
file_ext = "css"
|
133 |
+
elif output_format == "Tailwind Config":
|
134 |
+
code_output = generator.generate_tailwind_config(tokens)
|
135 |
+
file_ext = "js"
|
136 |
+
elif output_format == "JSON Tokens":
|
137 |
+
code_output = generator.generate_json_tokens(tokens)
|
138 |
+
file_ext = "json"
|
139 |
+
elif output_format == "Style Dictionary":
|
140 |
+
code_output = generator.generate_style_dictionary(tokens)
|
141 |
+
file_ext = "json"
|
142 |
+
elif output_format == "SCSS Variables":
|
143 |
+
code_output = generator.generate_scss_variables(tokens)
|
144 |
+
file_ext = "scss"
|
145 |
+
else:
|
146 |
+
code_output = json.dumps(tokens, indent=2)
|
147 |
+
file_ext = "json"
|
148 |
+
|
149 |
+
# Save output file
|
150 |
+
output_filename = f"design_tokens.{file_ext}"
|
151 |
+
with open(output_filename, "w") as f:
|
152 |
+
f.write(code_output)
|
153 |
+
|
154 |
+
# Clean up temp file
|
155 |
+
try:
|
156 |
+
os.unlink(temp_path)
|
157 |
+
except:
|
158 |
+
pass
|
159 |
+
|
160 |
+
progress(1.0, desc="Complete!")
|
161 |
+
|
162 |
+
# Create preview visualization
|
163 |
+
preview_html = create_token_preview(tokens)
|
164 |
+
|
165 |
+
return preview_html, code_output, output_filename
|
166 |
+
|
167 |
+
except Exception as e:
|
168 |
+
return None, f"Error processing screenshot: {str(e)}", None
|
169 |
+
|
170 |
+
|
171 |
+
def create_gradio_app():
|
172 |
+
"""Create the main Gradio application"""
|
173 |
+
|
174 |
+
with gr.Blocks(
|
175 |
+
title="Design Token Extractor",
|
176 |
+
theme=gr.themes.Soft(),
|
177 |
+
css="""
|
178 |
+
.gradio-container {
|
179 |
+
font-family: 'Inter', system-ui, sans-serif;
|
180 |
+
}
|
181 |
+
.gr-button-primary {
|
182 |
+
background-color: #3b82f6 !important;
|
183 |
+
}
|
184 |
+
"""
|
185 |
+
) as app:
|
186 |
+
gr.Markdown(
|
187 |
+
"""
|
188 |
+
# 🎨 Design Token Extractor
|
189 |
+
|
190 |
+
Transform UI screenshots into structured design token libraries using AI-powered analysis.
|
191 |
+
Upload a screenshot to automatically extract colors, spacing, typography, and component tokens.
|
192 |
+
|
193 |
+
---
|
194 |
+
"""
|
195 |
+
)
|
196 |
+
|
197 |
+
with gr.Row():
|
198 |
+
with gr.Column(scale=1):
|
199 |
+
input_image = gr.Image(
|
200 |
+
label="Upload UI Screenshot",
|
201 |
+
type="pil",
|
202 |
+
sources=['upload', 'clipboard'],
|
203 |
+
height=400
|
204 |
+
)
|
205 |
+
|
206 |
+
output_format = gr.Radio(
|
207 |
+
choices=[
|
208 |
+
"CSS Variables",
|
209 |
+
"Tailwind Config",
|
210 |
+
"JSON Tokens",
|
211 |
+
"Style Dictionary",
|
212 |
+
"SCSS Variables"
|
213 |
+
],
|
214 |
+
value="CSS Variables",
|
215 |
+
label="Output Format",
|
216 |
+
info="Choose the format for your design tokens"
|
217 |
+
)
|
218 |
+
|
219 |
+
extract_btn = gr.Button(
|
220 |
+
"🚀 Extract Design Tokens",
|
221 |
+
variant="primary",
|
222 |
+
size="lg"
|
223 |
+
)
|
224 |
+
|
225 |
+
gr.Markdown(
|
226 |
+
"""
|
227 |
+
### Tips for best results:
|
228 |
+
- Use high-quality screenshots (min 800px width)
|
229 |
+
- Include various UI elements for comprehensive extraction
|
230 |
+
- Screenshots with clear color hierarchy work best
|
231 |
+
- Ensure good contrast between elements
|
232 |
+
"""
|
233 |
+
)
|
234 |
+
|
235 |
+
with gr.Column(scale=1):
|
236 |
+
preview = gr.HTML(
|
237 |
+
label="Token Preview",
|
238 |
+
value="<div style='padding: 20px; text-align: center; color: #9ca3af;'>Upload a screenshot to see extracted tokens</div>"
|
239 |
+
)
|
240 |
+
|
241 |
+
code_output = gr.Code(
|
242 |
+
label="Generated Code",
|
243 |
+
language="css",
|
244 |
+
lines=20,
|
245 |
+
value="// Your design tokens will appear here"
|
246 |
+
)
|
247 |
+
|
248 |
+
download_file = gr.File(
|
249 |
+
label="Download Tokens",
|
250 |
+
visible=True
|
251 |
+
)
|
252 |
+
|
253 |
+
# Add examples
|
254 |
+
gr.Markdown("### Example Screenshots")
|
255 |
+
gr.Examples(
|
256 |
+
examples=[
|
257 |
+
["examples/dashboard.png", "CSS Variables"],
|
258 |
+
["examples/landing_page.png", "Tailwind Config"],
|
259 |
+
["examples/mobile_app.png", "JSON Tokens"]
|
260 |
+
],
|
261 |
+
inputs=[input_image, output_format],
|
262 |
+
cache_examples=False
|
263 |
+
)
|
264 |
+
|
265 |
+
# Connect the extraction function
|
266 |
+
extract_btn.click(
|
267 |
+
fn=process_screenshot,
|
268 |
+
inputs=[input_image, output_format],
|
269 |
+
outputs=[preview, code_output, download_file]
|
270 |
+
)
|
271 |
+
|
272 |
+
# Add footer
|
273 |
+
gr.Markdown(
|
274 |
+
"""
|
275 |
+
---
|
276 |
+
|
277 |
+
### Features:
|
278 |
+
- **Color Extraction**: Identifies dominant colors and creates semantic color roles
|
279 |
+
- **Spacing Detection**: Analyzes layout patterns to extract consistent spacing values
|
280 |
+
- **Typography Analysis**: Detects font styles and creates text hierarchy tokens
|
281 |
+
- **Multiple Output Formats**: Export to CSS, Tailwind, JSON, Style Dictionary, or SCSS
|
282 |
+
|
283 |
+
Built with ❤️ using Gradio and computer vision models
|
284 |
+
"""
|
285 |
+
)
|
286 |
+
|
287 |
+
return app
|
288 |
+
|
289 |
+
|
290 |
+
if __name__ == "__main__":
|
291 |
+
app = create_gradio_app()
|
292 |
+
app.launch()
|
examples/placeholder.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Add your example screenshots here:
|
2 |
+
- dashboard.png
|
3 |
+
- landing_page.png
|
4 |
+
- mobile_app.png
|
5 |
+
|
6 |
+
These will be used as examples in the Gradio interface.
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
transformers>=4.35.0
|
2 |
+
torch>=2.1.0
|
3 |
+
torchvision>=0.16.0
|
4 |
+
Pillow>=10.0.0
|
5 |
+
opencv-python-headless==4.8.0.74
|
6 |
+
colorgram.py==1.2.0
|
7 |
+
gradio>=4.44.1
|
8 |
+
numpy>=1.24.0
|
9 |
+
huggingface-hub>=0.19.0
|
test_structure.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""Test script to verify project structure"""
|
3 |
+
|
4 |
+
import os
|
5 |
+
import sys
|
6 |
+
|
7 |
+
def check_file_exists(filepath, description):
|
8 |
+
if os.path.exists(filepath):
|
9 |
+
print(f"[OK] {description}: {filepath}")
|
10 |
+
return True
|
11 |
+
else:
|
12 |
+
print(f"[MISSING] {description}: {filepath} NOT FOUND")
|
13 |
+
return False
|
14 |
+
|
15 |
+
def main():
|
16 |
+
print("Design Token Extractor - Project Structure Check")
|
17 |
+
print("=" * 50)
|
18 |
+
|
19 |
+
checks = [
|
20 |
+
("app.py", "Main application file"),
|
21 |
+
("requirements.txt", "Dependencies file"),
|
22 |
+
("README.md", "Documentation with HF config"),
|
23 |
+
("utils/__init__.py", "Utils module init"),
|
24 |
+
("utils/extractor.py", "Core extraction pipeline"),
|
25 |
+
("utils/token_generator.py", "Token code generator"),
|
26 |
+
("examples/", "Examples directory"),
|
27 |
+
("models/", "Models directory"),
|
28 |
+
("assets/", "Assets directory")
|
29 |
+
]
|
30 |
+
|
31 |
+
all_good = True
|
32 |
+
for filepath, description in checks:
|
33 |
+
if not check_file_exists(filepath, description):
|
34 |
+
all_good = False
|
35 |
+
|
36 |
+
print("=" * 50)
|
37 |
+
if all_good:
|
38 |
+
print("[SUCCESS] All project files are in place!")
|
39 |
+
print("\nTo deploy to Hugging Face Spaces:")
|
40 |
+
print("1. Install Git LFS: git lfs install")
|
41 |
+
print("2. Add remote: git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/DesignTokenExtractor")
|
42 |
+
print("3. Push to HF: git push hf main")
|
43 |
+
else:
|
44 |
+
print("[ERROR] Some files are missing. Please check the structure.")
|
45 |
+
|
46 |
+
return 0 if all_good else 1
|
47 |
+
|
48 |
+
if __name__ == "__main__":
|
49 |
+
sys.exit(main())
|
utils/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
# Utils module for Design Token Extractor
|
utils/extractor.py
ADDED
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import colorgram
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
from PIL import Image
|
5 |
+
import json
|
6 |
+
import torch
|
7 |
+
from transformers import Pix2StructForConditionalGeneration, Pix2StructProcessor
|
8 |
+
import functools
|
9 |
+
|
10 |
+
|
11 |
+
class DesignTokenExtractor:
|
12 |
+
def __init__(self):
|
13 |
+
# Load models once at startup
|
14 |
+
self.pix2struct_model = None
|
15 |
+
self.pix2struct_processor = None
|
16 |
+
self._load_models()
|
17 |
+
|
18 |
+
@functools.lru_cache(maxsize=1)
|
19 |
+
def _load_models(self):
|
20 |
+
"""Load models with caching to prevent repeated initialization"""
|
21 |
+
try:
|
22 |
+
self.pix2struct_processor = Pix2StructProcessor.from_pretrained(
|
23 |
+
"google/pix2struct-screen2words-base"
|
24 |
+
)
|
25 |
+
self.pix2struct_model = Pix2StructForConditionalGeneration.from_pretrained(
|
26 |
+
"google/pix2struct-screen2words-base",
|
27 |
+
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
|
28 |
+
)
|
29 |
+
except Exception as e:
|
30 |
+
print(f"Warning: Could not load Pix2Struct model: {e}")
|
31 |
+
# Continue without the model for basic extraction
|
32 |
+
|
33 |
+
def extract_colors(self, image_path, num_colors=8):
|
34 |
+
"""Extract dominant colors using colorgram"""
|
35 |
+
try:
|
36 |
+
colors = colorgram.extract(image_path, num_colors)
|
37 |
+
palette = {}
|
38 |
+
|
39 |
+
for i, color in enumerate(colors):
|
40 |
+
# Determine semantic color role based on proportion
|
41 |
+
if i == 0 and color.proportion > 0.3:
|
42 |
+
name = "background"
|
43 |
+
elif i == 1:
|
44 |
+
name = "primary"
|
45 |
+
elif i == 2:
|
46 |
+
name = "secondary"
|
47 |
+
else:
|
48 |
+
name = f"accent-{i-2}"
|
49 |
+
|
50 |
+
palette[name] = {
|
51 |
+
"hex": f"#{color.rgb.r:02x}{color.rgb.g:02x}{color.rgb.b:02x}",
|
52 |
+
"rgb": f"rgb({color.rgb.r}, {color.rgb.g}, {color.rgb.b})",
|
53 |
+
"proportion": round(color.proportion, 3)
|
54 |
+
}
|
55 |
+
|
56 |
+
return palette
|
57 |
+
except Exception as e:
|
58 |
+
print(f"Error extracting colors: {e}")
|
59 |
+
return self._get_default_colors()
|
60 |
+
|
61 |
+
def detect_spacing(self, image):
|
62 |
+
"""Analyze spacing patterns using OpenCV"""
|
63 |
+
try:
|
64 |
+
gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
|
65 |
+
edges = cv2.Canny(gray, 50, 150)
|
66 |
+
|
67 |
+
# Find contours for element detection
|
68 |
+
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
69 |
+
|
70 |
+
# Calculate spacing between elements
|
71 |
+
bounding_boxes = [cv2.boundingRect(c) for c in contours if cv2.contourArea(c) > 100]
|
72 |
+
|
73 |
+
if len(bounding_boxes) > 1:
|
74 |
+
# Sort by y-coordinate to find vertical spacing
|
75 |
+
bounding_boxes.sort(key=lambda x: x[1])
|
76 |
+
|
77 |
+
vertical_gaps = []
|
78 |
+
for i in range(len(bounding_boxes)-1):
|
79 |
+
gap = bounding_boxes[i+1][1] - (bounding_boxes[i][1] + bounding_boxes[i][3])
|
80 |
+
if gap > 0:
|
81 |
+
vertical_gaps.append(gap)
|
82 |
+
|
83 |
+
# Find common spacing values using clustering
|
84 |
+
spacing_system = self._cluster_spacing_values(vertical_gaps)
|
85 |
+
return spacing_system
|
86 |
+
except Exception as e:
|
87 |
+
print(f"Error detecting spacing: {e}")
|
88 |
+
|
89 |
+
return {"small": "8px", "medium": "16px", "large": "32px"} # Defaults
|
90 |
+
|
91 |
+
def _cluster_spacing_values(self, gaps):
|
92 |
+
"""Group similar spacing values"""
|
93 |
+
if not gaps:
|
94 |
+
return {"small": "8px", "medium": "16px", "large": "32px"}
|
95 |
+
|
96 |
+
gaps.sort()
|
97 |
+
|
98 |
+
# Simple clustering for common spacing values
|
99 |
+
unique_gaps = list(set(gaps))
|
100 |
+
|
101 |
+
if len(unique_gaps) >= 3:
|
102 |
+
return {
|
103 |
+
"small": f"{unique_gaps[0]}px",
|
104 |
+
"medium": f"{unique_gaps[len(unique_gaps)//2]}px",
|
105 |
+
"large": f"{unique_gaps[-1]}px"
|
106 |
+
}
|
107 |
+
elif len(unique_gaps) == 2:
|
108 |
+
return {
|
109 |
+
"small": f"{unique_gaps[0]}px",
|
110 |
+
"large": f"{unique_gaps[1]}px"
|
111 |
+
}
|
112 |
+
|
113 |
+
return {"base": f"{unique_gaps[0]}px" if unique_gaps else "16px"}
|
114 |
+
|
115 |
+
def analyze_components(self, image):
|
116 |
+
"""Use Pix2Struct for component understanding"""
|
117 |
+
if self.pix2struct_model is None or self.pix2struct_processor is None:
|
118 |
+
# Fallback if model loading failed
|
119 |
+
return {
|
120 |
+
"detected_elements": "Model not available - basic extraction only",
|
121 |
+
"layout": "responsive"
|
122 |
+
}
|
123 |
+
|
124 |
+
try:
|
125 |
+
inputs = self.pix2struct_processor(images=image, return_tensors="pt")
|
126 |
+
|
127 |
+
with torch.no_grad():
|
128 |
+
generated_ids = self.pix2struct_model.generate(**inputs, max_length=100)
|
129 |
+
|
130 |
+
description = self.pix2struct_processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
131 |
+
|
132 |
+
# Parse description for component types
|
133 |
+
components = {
|
134 |
+
"detected_elements": description,
|
135 |
+
"layout": "responsive" if "responsive" in description.lower() else "fixed"
|
136 |
+
}
|
137 |
+
|
138 |
+
return components
|
139 |
+
except Exception as e:
|
140 |
+
print(f"Error analyzing components: {e}")
|
141 |
+
return {
|
142 |
+
"detected_elements": "Error during analysis",
|
143 |
+
"layout": "responsive"
|
144 |
+
}
|
145 |
+
|
146 |
+
def detect_typography(self, image):
|
147 |
+
"""Basic typography detection"""
|
148 |
+
# Simplified typography detection without EasyOCR for initial implementation
|
149 |
+
return {
|
150 |
+
"heading": {
|
151 |
+
"family": "sans-serif",
|
152 |
+
"size": "32px",
|
153 |
+
"weight": "700"
|
154 |
+
},
|
155 |
+
"body": {
|
156 |
+
"family": "sans-serif",
|
157 |
+
"size": "16px",
|
158 |
+
"weight": "400"
|
159 |
+
},
|
160 |
+
"caption": {
|
161 |
+
"family": "sans-serif",
|
162 |
+
"size": "14px",
|
163 |
+
"weight": "400"
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
def _get_default_colors(self):
|
168 |
+
"""Return default color palette"""
|
169 |
+
return {
|
170 |
+
"primary": {"hex": "#3B82F6", "rgb": "rgb(59, 130, 246)", "proportion": 0.25},
|
171 |
+
"secondary": {"hex": "#8B5CF6", "rgb": "rgb(139, 92, 246)", "proportion": 0.15},
|
172 |
+
"background": {"hex": "#FFFFFF", "rgb": "rgb(255, 255, 255)", "proportion": 0.40},
|
173 |
+
"text": {"hex": "#1F2937", "rgb": "rgb(31, 41, 55)", "proportion": 0.20}
|
174 |
+
}
|
175 |
+
|
176 |
+
def resize_for_processing(self, image, max_dimension=1024):
|
177 |
+
"""Resize large images while maintaining aspect ratio"""
|
178 |
+
if max(image.size) > max_dimension:
|
179 |
+
ratio = max_dimension / max(image.size)
|
180 |
+
new_size = tuple(int(dim * ratio) for dim in image.size)
|
181 |
+
return image.resize(new_size, Image.Resampling.LANCZOS)
|
182 |
+
return image
|
utils/token_generator.py
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
|
3 |
+
|
4 |
+
class TokenCodeGenerator:
|
5 |
+
def generate_css_variables(self, tokens):
|
6 |
+
"""Generate CSS custom properties"""
|
7 |
+
css = ":root {\n"
|
8 |
+
|
9 |
+
# Colors
|
10 |
+
for name, color in tokens.get('colors', {}).items():
|
11 |
+
css += f" --color-{name}: {color['hex']};\n"
|
12 |
+
css += f" --color-{name}-rgb: {color['rgb']};\n"
|
13 |
+
|
14 |
+
css += "\n"
|
15 |
+
|
16 |
+
# Spacing
|
17 |
+
for name, value in tokens.get('spacing', {}).items():
|
18 |
+
css += f" --spacing-{name}: {value};\n"
|
19 |
+
|
20 |
+
css += "\n"
|
21 |
+
|
22 |
+
# Typography
|
23 |
+
if 'typography' in tokens:
|
24 |
+
for name, props in tokens['typography'].items():
|
25 |
+
css += f" --font-{name}: {props.get('family', 'sans-serif')};\n"
|
26 |
+
css += f" --font-size-{name}: {props.get('size', '16px')};\n"
|
27 |
+
css += f" --font-weight-{name}: {props.get('weight', '400')};\n"
|
28 |
+
|
29 |
+
css += "}\n\n"
|
30 |
+
|
31 |
+
# Add example usage comments
|
32 |
+
css += "/* Example usage:\n"
|
33 |
+
css += " * color: var(--color-primary);\n"
|
34 |
+
css += " * padding: var(--spacing-medium);\n"
|
35 |
+
css += " * font-family: var(--font-body);\n"
|
36 |
+
css += " */\n"
|
37 |
+
|
38 |
+
return css
|
39 |
+
|
40 |
+
def generate_tailwind_config(self, tokens):
|
41 |
+
"""Generate Tailwind configuration"""
|
42 |
+
config = {
|
43 |
+
"theme": {
|
44 |
+
"extend": {
|
45 |
+
"colors": {},
|
46 |
+
"spacing": {},
|
47 |
+
"fontFamily": {},
|
48 |
+
"fontSize": {},
|
49 |
+
"fontWeight": {}
|
50 |
+
}
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
# Add colors
|
55 |
+
for name, color in tokens.get('colors', {}).items():
|
56 |
+
config["theme"]["extend"]["colors"][name] = color['hex']
|
57 |
+
|
58 |
+
# Add spacing
|
59 |
+
for name, value in tokens.get('spacing', {}).items():
|
60 |
+
config["theme"]["extend"]["spacing"][name] = value
|
61 |
+
|
62 |
+
# Add typography
|
63 |
+
if 'typography' in tokens:
|
64 |
+
for name, props in tokens['typography'].items():
|
65 |
+
if 'family' in props:
|
66 |
+
config["theme"]["extend"]["fontFamily"][name] = props['family']
|
67 |
+
if 'size' in props:
|
68 |
+
config["theme"]["extend"]["fontSize"][name] = props['size']
|
69 |
+
if 'weight' in props:
|
70 |
+
config["theme"]["extend"]["fontWeight"][name] = props['weight']
|
71 |
+
|
72 |
+
# Format as JavaScript module
|
73 |
+
output = "/** @type {import('tailwindcss').Config} */\n"
|
74 |
+
output += f"module.exports = {json.dumps(config, indent=2)}"
|
75 |
+
|
76 |
+
return output
|
77 |
+
|
78 |
+
def generate_json_tokens(self, tokens):
|
79 |
+
"""Generate W3C Design Token Community Group format"""
|
80 |
+
formatted_tokens = {
|
81 |
+
"$schema": "https://design-tokens.github.io/community-group/format.json",
|
82 |
+
"tokens": {}
|
83 |
+
}
|
84 |
+
|
85 |
+
# Colors
|
86 |
+
if 'colors' in tokens:
|
87 |
+
formatted_tokens["tokens"]["color"] = {}
|
88 |
+
for name, color in tokens['colors'].items():
|
89 |
+
formatted_tokens["tokens"]["color"][name] = {
|
90 |
+
"$value": color['hex'],
|
91 |
+
"$type": "color",
|
92 |
+
"$description": f"Color {name} - {color.get('proportion', 0)*100:.1f}% of design"
|
93 |
+
}
|
94 |
+
|
95 |
+
# Spacing
|
96 |
+
if 'spacing' in tokens:
|
97 |
+
formatted_tokens["tokens"]["spacing"] = {}
|
98 |
+
for name, value in tokens['spacing'].items():
|
99 |
+
formatted_tokens["tokens"]["spacing"][name] = {
|
100 |
+
"$value": value,
|
101 |
+
"$type": "dimension"
|
102 |
+
}
|
103 |
+
|
104 |
+
# Typography
|
105 |
+
if 'typography' in tokens:
|
106 |
+
formatted_tokens["tokens"]["typography"] = {}
|
107 |
+
for name, props in tokens['typography'].items():
|
108 |
+
formatted_tokens["tokens"]["typography"][name] = {
|
109 |
+
"fontFamily": {
|
110 |
+
"$value": props.get('family', 'sans-serif'),
|
111 |
+
"$type": "fontFamily"
|
112 |
+
},
|
113 |
+
"fontSize": {
|
114 |
+
"$value": props.get('size', '16px'),
|
115 |
+
"$type": "dimension"
|
116 |
+
},
|
117 |
+
"fontWeight": {
|
118 |
+
"$value": props.get('weight', '400'),
|
119 |
+
"$type": "fontWeight"
|
120 |
+
}
|
121 |
+
}
|
122 |
+
|
123 |
+
return json.dumps(formatted_tokens, indent=2)
|
124 |
+
|
125 |
+
def generate_style_dictionary(self, tokens):
|
126 |
+
"""Generate Style Dictionary format tokens"""
|
127 |
+
sd_tokens = {
|
128 |
+
"color": {},
|
129 |
+
"spacing": {},
|
130 |
+
"typography": {}
|
131 |
+
}
|
132 |
+
|
133 |
+
# Transform colors
|
134 |
+
for name, color in tokens.get('colors', {}).items():
|
135 |
+
sd_tokens["color"][name] = {
|
136 |
+
"value": color['hex'],
|
137 |
+
"type": "color",
|
138 |
+
"attributes": {
|
139 |
+
"rgb": color.get('rgb', ''),
|
140 |
+
"proportion": color.get('proportion', 0)
|
141 |
+
}
|
142 |
+
}
|
143 |
+
|
144 |
+
# Transform spacing
|
145 |
+
for name, value in tokens.get('spacing', {}).items():
|
146 |
+
sd_tokens["spacing"][name] = {
|
147 |
+
"value": value,
|
148 |
+
"type": "spacing"
|
149 |
+
}
|
150 |
+
|
151 |
+
# Transform typography
|
152 |
+
if 'typography' in tokens:
|
153 |
+
for name, props in tokens['typography'].items():
|
154 |
+
sd_tokens["typography"][name] = {
|
155 |
+
"fontFamily": {
|
156 |
+
"value": props.get('family', 'sans-serif')
|
157 |
+
},
|
158 |
+
"fontSize": {
|
159 |
+
"value": props.get('size', '16px')
|
160 |
+
},
|
161 |
+
"fontWeight": {
|
162 |
+
"value": props.get('weight', '400')
|
163 |
+
}
|
164 |
+
}
|
165 |
+
|
166 |
+
return json.dumps(sd_tokens, indent=2)
|
167 |
+
|
168 |
+
def generate_scss_variables(self, tokens):
|
169 |
+
"""Generate SCSS variables"""
|
170 |
+
scss = "// Design Tokens - SCSS Variables\n\n"
|
171 |
+
|
172 |
+
# Colors
|
173 |
+
scss += "// Colors\n"
|
174 |
+
for name, color in tokens.get('colors', {}).items():
|
175 |
+
scss += f"$color-{name}: {color['hex']};\n"
|
176 |
+
|
177 |
+
scss += "\n// Spacing\n"
|
178 |
+
for name, value in tokens.get('spacing', {}).items():
|
179 |
+
scss += f"$spacing-{name}: {value};\n"
|
180 |
+
|
181 |
+
scss += "\n// Typography\n"
|
182 |
+
if 'typography' in tokens:
|
183 |
+
for name, props in tokens['typography'].items():
|
184 |
+
scss += f"$font-{name}: {props.get('family', 'sans-serif')};\n"
|
185 |
+
scss += f"$font-size-{name}: {props.get('size', '16px')};\n"
|
186 |
+
scss += f"$font-weight-{name}: {props.get('weight', '400')};\n"
|
187 |
+
scss += "\n"
|
188 |
+
|
189 |
+
# Add mixins for common patterns
|
190 |
+
scss += "\n// Utility Mixins\n"
|
191 |
+
scss += "@mixin text-style($style) {\n"
|
192 |
+
scss += " @if $style == 'heading' {\n"
|
193 |
+
scss += " font-family: $font-heading;\n"
|
194 |
+
scss += " font-size: $font-size-heading;\n"
|
195 |
+
scss += " font-weight: $font-weight-heading;\n"
|
196 |
+
scss += " } @else if $style == 'body' {\n"
|
197 |
+
scss += " font-family: $font-body;\n"
|
198 |
+
scss += " font-size: $font-size-body;\n"
|
199 |
+
scss += " font-weight: $font-weight-body;\n"
|
200 |
+
scss += " }\n"
|
201 |
+
scss += "}\n"
|
202 |
+
|
203 |
+
return scss
|