riazmo commited on
Commit
7e4e20b
Β·
verified Β·
1 Parent(s): bbaf498

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +551 -0
app.py ADDED
@@ -0,0 +1,551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Design System Extractor v2 β€” Main Application
3
+ ==============================================
4
+
5
+ Flow:
6
+ 1. User enters URL
7
+ 2. Agent 1 discovers pages β†’ User confirms
8
+ 3. Agent 1 extracts tokens (Desktop + Mobile)
9
+ 4. Agent 2 normalizes tokens
10
+ 5. Stage 1 UI: User reviews tokens (accept/reject, Desktop↔Mobile toggle)
11
+ 6. Agent 3 proposes upgrades
12
+ 7. Stage 2 UI: User selects options with live preview
13
+ 8. Agent 4 generates JSON
14
+ 9. Stage 3 UI: User exports
15
+ """
16
+
17
+ import os
18
+ import asyncio
19
+ import json
20
+ import gradio as gr
21
+ from datetime import datetime
22
+ from typing import Optional
23
+
24
+ # Get HF token from environment
25
+ HF_TOKEN_FROM_ENV = os.getenv("HF_TOKEN", "")
26
+
27
+ # =============================================================================
28
+ # GLOBAL STATE
29
+ # =============================================================================
30
+
31
+ class AppState:
32
+ """Global application state."""
33
+ def __init__(self):
34
+ self.reset()
35
+
36
+ def reset(self):
37
+ self.discovered_pages = []
38
+ self.base_url = ""
39
+ self.desktop_raw = None # ExtractedTokens
40
+ self.mobile_raw = None # ExtractedTokens
41
+ self.desktop_normalized = None # NormalizedTokens
42
+ self.mobile_normalized = None # NormalizedTokens
43
+ self.logs = []
44
+
45
+ def log(self, message: str):
46
+ timestamp = datetime.now().strftime("%H:%M:%S")
47
+ self.logs.append(f"[{timestamp}] {message}")
48
+ if len(self.logs) > 100:
49
+ self.logs.pop(0)
50
+
51
+ def get_logs(self) -> str:
52
+ return "\n".join(self.logs)
53
+
54
+ state = AppState()
55
+
56
+
57
+ # =============================================================================
58
+ # LAZY IMPORTS
59
+ # =============================================================================
60
+
61
+ def get_crawler():
62
+ import agents.crawler
63
+ return agents.crawler
64
+
65
+ def get_extractor():
66
+ import agents.extractor
67
+ return agents.extractor
68
+
69
+ def get_normalizer():
70
+ import agents.normalizer
71
+ return agents.normalizer
72
+
73
+ def get_schema():
74
+ import core.token_schema
75
+ return core.token_schema
76
+
77
+
78
+ # =============================================================================
79
+ # PHASE 1: DISCOVER PAGES
80
+ # =============================================================================
81
+
82
+ async def discover_pages(url: str, progress=gr.Progress()):
83
+ """Discover pages from URL."""
84
+ state.reset()
85
+
86
+ if not url or not url.startswith(("http://", "https://")):
87
+ return "❌ Please enter a valid URL", "", None
88
+
89
+ state.log(f"πŸš€ Starting discovery for: {url}")
90
+ progress(0.1, desc="πŸ” Discovering pages...")
91
+
92
+ try:
93
+ crawler = get_crawler()
94
+ discoverer = crawler.PageDiscoverer()
95
+
96
+ pages = await discoverer.discover(url)
97
+
98
+ state.discovered_pages = pages
99
+ state.base_url = url
100
+
101
+ state.log(f"βœ… Found {len(pages)} pages")
102
+
103
+ # Format for display
104
+ pages_data = []
105
+ for page in pages:
106
+ pages_data.append([
107
+ True, # Selected by default
108
+ page.url,
109
+ page.title if page.title else "(No title)",
110
+ page.page_type.value,
111
+ "βœ“" if not page.error else f"⚠ {page.error}"
112
+ ])
113
+
114
+ progress(1.0, desc="βœ… Discovery complete!")
115
+
116
+ status = f"βœ… Found {len(pages)} pages. Review and click 'Extract Tokens' to continue."
117
+
118
+ return status, state.get_logs(), pages_data
119
+
120
+ except Exception as e:
121
+ import traceback
122
+ state.log(f"❌ Error: {str(e)}")
123
+ return f"❌ Error: {str(e)}", state.get_logs(), None
124
+
125
+
126
+ # =============================================================================
127
+ # PHASE 2: EXTRACT TOKENS
128
+ # =============================================================================
129
+
130
+ async def extract_tokens(pages_data, progress=gr.Progress()):
131
+ """Extract tokens from selected pages (both viewports)."""
132
+
133
+ if pages_data is None or len(pages_data) == 0:
134
+ return "❌ Please discover pages first", state.get_logs(), None, None
135
+
136
+ # Get selected URLs
137
+ selected_urls = []
138
+ for row in pages_data:
139
+ if isinstance(row, (list, tuple)) and len(row) >= 2:
140
+ if row[0]: # Checkbox is True
141
+ selected_urls.append(row[1])
142
+
143
+ if not selected_urls:
144
+ return "❌ Please select at least one page", state.get_logs(), None, None
145
+
146
+ state.log(f"πŸ“‹ Extracting from {len(selected_urls)} pages...")
147
+ progress(0.05, desc="πŸš€ Starting extraction...")
148
+
149
+ try:
150
+ schema = get_schema()
151
+ extractor_mod = get_extractor()
152
+ normalizer_mod = get_normalizer()
153
+
154
+ # === DESKTOP EXTRACTION ===
155
+ state.log("")
156
+ state.log("πŸ–₯️ DESKTOP EXTRACTION (1440px)")
157
+ progress(0.1, desc="πŸ–₯️ Extracting desktop tokens...")
158
+
159
+ desktop_extractor = extractor_mod.TokenExtractor(viewport=schema.Viewport.DESKTOP)
160
+
161
+ def desktop_progress(p):
162
+ progress(0.1 + (p * 0.35), desc=f"πŸ–₯️ Desktop... {int(p*100)}%")
163
+
164
+ state.desktop_raw = await desktop_extractor.extract(selected_urls, progress_callback=desktop_progress)
165
+
166
+ state.log(f" Raw: {len(state.desktop_raw.colors)} colors, {len(state.desktop_raw.typography)} typography, {len(state.desktop_raw.spacing)} spacing")
167
+
168
+ # Normalize desktop
169
+ state.log(" Normalizing...")
170
+ state.desktop_normalized = normalizer_mod.normalize_tokens(state.desktop_raw)
171
+ state.log(f" Normalized: {len(state.desktop_normalized.colors)} colors, {len(state.desktop_normalized.typography)} typography, {len(state.desktop_normalized.spacing)} spacing")
172
+
173
+ # === MOBILE EXTRACTION ===
174
+ state.log("")
175
+ state.log("πŸ“± MOBILE EXTRACTION (375px)")
176
+ progress(0.5, desc="πŸ“± Extracting mobile tokens...")
177
+
178
+ mobile_extractor = extractor_mod.TokenExtractor(viewport=schema.Viewport.MOBILE)
179
+
180
+ def mobile_progress(p):
181
+ progress(0.5 + (p * 0.35), desc=f"πŸ“± Mobile... {int(p*100)}%")
182
+
183
+ state.mobile_raw = await mobile_extractor.extract(selected_urls, progress_callback=mobile_progress)
184
+
185
+ state.log(f" Raw: {len(state.mobile_raw.colors)} colors, {len(state.mobile_raw.typography)} typography, {len(state.mobile_raw.spacing)} spacing")
186
+
187
+ # Normalize mobile
188
+ state.log(" Normalizing...")
189
+ state.mobile_normalized = normalizer_mod.normalize_tokens(state.mobile_raw)
190
+ state.log(f" Normalized: {len(state.mobile_normalized.colors)} colors, {len(state.mobile_normalized.typography)} typography, {len(state.mobile_normalized.spacing)} spacing")
191
+
192
+ progress(0.95, desc="πŸ“Š Preparing results...")
193
+
194
+ # Format results for Stage 1 UI
195
+ desktop_data = format_tokens_for_display(state.desktop_normalized)
196
+ mobile_data = format_tokens_for_display(state.mobile_normalized)
197
+
198
+ state.log("")
199
+ state.log("=" * 50)
200
+ state.log("βœ… EXTRACTION COMPLETE!")
201
+ state.log("=" * 50)
202
+
203
+ progress(1.0, desc="βœ… Complete!")
204
+
205
+ status = f"""## βœ… Extraction Complete!
206
+
207
+ | Viewport | Colors | Typography | Spacing |
208
+ |----------|--------|------------|---------|
209
+ | Desktop | {len(state.desktop_normalized.colors)} | {len(state.desktop_normalized.typography)} | {len(state.desktop_normalized.spacing)} |
210
+ | Mobile | {len(state.mobile_normalized.colors)} | {len(state.mobile_normalized.typography)} | {len(state.mobile_normalized.spacing)} |
211
+
212
+ **Next:** Review the tokens below. Accept or reject, then proceed to Stage 2.
213
+ """
214
+
215
+ return status, state.get_logs(), desktop_data, mobile_data
216
+
217
+ except Exception as e:
218
+ import traceback
219
+ state.log(f"❌ Error: {str(e)}")
220
+ state.log(traceback.format_exc())
221
+ return f"❌ Error: {str(e)}", state.get_logs(), None, None
222
+
223
+
224
+ def format_tokens_for_display(normalized) -> dict:
225
+ """Format normalized tokens for Gradio display."""
226
+ if normalized is None:
227
+ return {"colors": [], "typography": [], "spacing": []}
228
+
229
+ colors = []
230
+ for c in normalized.colors[:50]:
231
+ colors.append([
232
+ True, # Accept checkbox
233
+ c.value,
234
+ c.suggested_name or "",
235
+ c.frequency,
236
+ c.confidence.value if c.confidence else "medium",
237
+ f"{c.contrast_white:.1f}:1" if c.contrast_white else "N/A",
238
+ "βœ“" if c.wcag_aa_small_text else "βœ—",
239
+ ", ".join(c.contexts[:2]) if c.contexts else "",
240
+ ])
241
+
242
+ typography = []
243
+ for t in normalized.typography[:30]:
244
+ typography.append([
245
+ True, # Accept checkbox
246
+ t.font_family,
247
+ t.font_size,
248
+ str(t.font_weight),
249
+ t.line_height or "",
250
+ t.suggested_name or "",
251
+ t.frequency,
252
+ t.confidence.value if t.confidence else "medium",
253
+ ])
254
+
255
+ spacing = []
256
+ for s in normalized.spacing[:20]:
257
+ spacing.append([
258
+ True, # Accept checkbox
259
+ s.value,
260
+ f"{s.value_px}px",
261
+ s.suggested_name or "",
262
+ s.frequency,
263
+ "βœ“" if s.fits_base_8 else "",
264
+ s.confidence.value if s.confidence else "medium",
265
+ ])
266
+
267
+ return {
268
+ "colors": colors,
269
+ "typography": typography,
270
+ "spacing": spacing,
271
+ }
272
+
273
+
274
+ def switch_viewport(viewport: str):
275
+ """Switch between desktop and mobile view."""
276
+ if viewport == "Desktop (1440px)":
277
+ data = format_tokens_for_display(state.desktop_normalized)
278
+ else:
279
+ data = format_tokens_for_display(state.mobile_normalized)
280
+
281
+ return data["colors"], data["typography"], data["spacing"]
282
+
283
+
284
+ # =============================================================================
285
+ # STAGE 3: EXPORT
286
+ # =============================================================================
287
+
288
+ def export_tokens_json():
289
+ """Export tokens to JSON."""
290
+ result = {
291
+ "metadata": {
292
+ "source_url": state.base_url,
293
+ "extracted_at": datetime.now().isoformat(),
294
+ "version": "v1-extracted",
295
+ },
296
+ "desktop": None,
297
+ "mobile": None,
298
+ }
299
+
300
+ if state.desktop_normalized:
301
+ result["desktop"] = {
302
+ "colors": [
303
+ {"value": c.value, "name": c.suggested_name, "frequency": c.frequency,
304
+ "confidence": c.confidence.value if c.confidence else "medium"}
305
+ for c in state.desktop_normalized.colors
306
+ ],
307
+ "typography": [
308
+ {"font_family": t.font_family, "font_size": t.font_size,
309
+ "font_weight": t.font_weight, "line_height": t.line_height,
310
+ "name": t.suggested_name, "frequency": t.frequency}
311
+ for t in state.desktop_normalized.typography
312
+ ],
313
+ "spacing": [
314
+ {"value": s.value, "value_px": s.value_px, "name": s.suggested_name,
315
+ "frequency": s.frequency, "fits_base_8": s.fits_base_8}
316
+ for s in state.desktop_normalized.spacing
317
+ ],
318
+ }
319
+
320
+ if state.mobile_normalized:
321
+ result["mobile"] = {
322
+ "colors": [
323
+ {"value": c.value, "name": c.suggested_name, "frequency": c.frequency,
324
+ "confidence": c.confidence.value if c.confidence else "medium"}
325
+ for c in state.mobile_normalized.colors
326
+ ],
327
+ "typography": [
328
+ {"font_family": t.font_family, "font_size": t.font_size,
329
+ "font_weight": t.font_weight, "line_height": t.line_height,
330
+ "name": t.suggested_name, "frequency": t.frequency}
331
+ for t in state.mobile_normalized.typography
332
+ ],
333
+ "spacing": [
334
+ {"value": s.value, "value_px": s.value_px, "name": s.suggested_name,
335
+ "frequency": s.frequency, "fits_base_8": s.fits_base_8}
336
+ for s in state.mobile_normalized.spacing
337
+ ],
338
+ }
339
+
340
+ return json.dumps(result, indent=2, default=str)
341
+
342
+
343
+ # =============================================================================
344
+ # UI BUILDING
345
+ # =============================================================================
346
+
347
+ def create_ui():
348
+ """Create the Gradio interface."""
349
+
350
+ with gr.Blocks(
351
+ title="Design System Extractor v2",
352
+ theme=gr.themes.Soft(),
353
+ css="""
354
+ .color-swatch { display: inline-block; width: 24px; height: 24px; border-radius: 4px; margin-right: 8px; vertical-align: middle; }
355
+ """
356
+ ) as app:
357
+
358
+ gr.Markdown("""
359
+ # 🎨 Design System Extractor v2
360
+
361
+ **Reverse-engineer design systems from live websites.**
362
+
363
+ A semi-automated, human-in-the-loop system that extracts, normalizes, and upgrades design tokens.
364
+
365
+ ---
366
+ """)
367
+
368
+ # =================================================================
369
+ # CONFIGURATION
370
+ # =================================================================
371
+
372
+ with gr.Accordion("βš™οΈ Configuration", open=not bool(HF_TOKEN_FROM_ENV)):
373
+ gr.Markdown("**HuggingFace Token** β€” Required for Stage 2 (AI upgrades)")
374
+ with gr.Row():
375
+ hf_token_input = gr.Textbox(
376
+ label="HF Token", placeholder="hf_xxxx", type="password",
377
+ scale=4, value=HF_TOKEN_FROM_ENV,
378
+ )
379
+ save_token_btn = gr.Button("πŸ’Ύ Save", scale=1)
380
+ token_status = gr.Markdown("βœ… Token loaded" if HF_TOKEN_FROM_ENV else "⏳ Enter token")
381
+
382
+ def save_token(token):
383
+ if token and len(token) > 10:
384
+ os.environ["HF_TOKEN"] = token.strip()
385
+ return "βœ… Token saved!"
386
+ return "❌ Invalid token"
387
+
388
+ save_token_btn.click(save_token, [hf_token_input], [token_status])
389
+
390
+ # =================================================================
391
+ # URL INPUT & PAGE DISCOVERY
392
+ # =================================================================
393
+
394
+ with gr.Accordion("πŸ” Step 1: Discover Pages", open=True):
395
+ gr.Markdown("Enter your website URL to discover pages for extraction.")
396
+
397
+ with gr.Row():
398
+ url_input = gr.Textbox(label="Website URL", placeholder="https://example.com", scale=4)
399
+ discover_btn = gr.Button("πŸ” Discover Pages", variant="primary", scale=1)
400
+
401
+ discover_status = gr.Markdown("")
402
+
403
+ with gr.Row():
404
+ log_output = gr.Textbox(label="πŸ“‹ Log", lines=8, interactive=False)
405
+
406
+ pages_table = gr.Dataframe(
407
+ headers=["Select", "URL", "Title", "Type", "Status"],
408
+ datatype=["bool", "str", "str", "str", "str"],
409
+ label="Discovered Pages",
410
+ interactive=True,
411
+ visible=False,
412
+ )
413
+
414
+ extract_btn = gr.Button("πŸš€ Extract Tokens (Desktop + Mobile)", variant="primary", visible=False)
415
+
416
+ # =================================================================
417
+ # STAGE 1: EXTRACTION REVIEW
418
+ # =================================================================
419
+
420
+ with gr.Accordion("πŸ“Š Stage 1: Review Extracted Tokens", open=False) as stage1_accordion:
421
+
422
+ extraction_status = gr.Markdown("")
423
+
424
+ gr.Markdown("""
425
+ **Review the extracted tokens.** Toggle between Desktop and Mobile viewports.
426
+ Accept or reject tokens, then proceed to Stage 2 for AI-powered upgrades.
427
+ """)
428
+
429
+ viewport_toggle = gr.Radio(
430
+ choices=["Desktop (1440px)", "Mobile (375px)"],
431
+ value="Desktop (1440px)",
432
+ label="Viewport",
433
+ )
434
+
435
+ with gr.Tabs():
436
+ with gr.Tab("🎨 Colors"):
437
+ colors_table = gr.Dataframe(
438
+ headers=["Accept", "Color", "Suggested Name", "Frequency", "Confidence", "Contrast", "AA", "Context"],
439
+ datatype=["bool", "str", "str", "number", "str", "str", "str", "str"],
440
+ label="Colors",
441
+ interactive=True,
442
+ )
443
+
444
+ with gr.Tab("πŸ“ Typography"):
445
+ typography_table = gr.Dataframe(
446
+ headers=["Accept", "Font", "Size", "Weight", "Line Height", "Suggested Name", "Frequency", "Confidence"],
447
+ datatype=["bool", "str", "str", "str", "str", "str", "number", "str"],
448
+ label="Typography",
449
+ interactive=True,
450
+ )
451
+
452
+ with gr.Tab("πŸ“ Spacing"):
453
+ spacing_table = gr.Dataframe(
454
+ headers=["Accept", "Value", "Pixels", "Suggested Name", "Frequency", "Base 8", "Confidence"],
455
+ datatype=["bool", "str", "str", "str", "number", "str", "str"],
456
+ label="Spacing",
457
+ interactive=True,
458
+ )
459
+
460
+ proceed_stage2_btn = gr.Button("➑️ Proceed to Stage 2: AI Upgrades", variant="primary")
461
+
462
+ # =================================================================
463
+ # STAGE 2: AI UPGRADES (Placeholder)
464
+ # =================================================================
465
+
466
+ with gr.Accordion("🧠 Stage 2: AI-Powered Upgrades (Coming Soon)", open=False):
467
+ gr.Markdown("""
468
+ **Agent 3 (Design System Advisor)** will analyze your tokens and propose:
469
+
470
+ - **Type Scale Options:** Choose from A/B/C (1.25, 1.333, 1.414 ratios)
471
+ - **Color Ramp Generation:** AA-compliant tints and shades
472
+ - **Spacing System:** Aligned to 8px base grid
473
+ - **Naming Conventions:** Semantic token names
474
+
475
+ Each option will show a **live preview** so you can see the changes before accepting.
476
+
477
+ *Requires HuggingFace token for LLM inference.*
478
+ """)
479
+
480
+ # =================================================================
481
+ # STAGE 3: EXPORT
482
+ # =================================================================
483
+
484
+ with gr.Accordion("πŸ“¦ Stage 3: Export", open=False):
485
+ gr.Markdown("Export your design tokens to JSON (compatible with Figma Tokens Studio).")
486
+
487
+ export_btn = gr.Button("πŸ“₯ Export JSON", variant="secondary")
488
+ export_output = gr.Code(label="Tokens JSON", language="json", lines=20)
489
+
490
+ export_btn.click(export_tokens_json, outputs=[export_output])
491
+
492
+ # =================================================================
493
+ # EVENT HANDLERS
494
+ # =================================================================
495
+
496
+ # Store data for viewport toggle
497
+ desktop_data = gr.State({})
498
+ mobile_data = gr.State({})
499
+
500
+ # Discover pages
501
+ discover_btn.click(
502
+ fn=discover_pages,
503
+ inputs=[url_input],
504
+ outputs=[discover_status, log_output, pages_table],
505
+ ).then(
506
+ fn=lambda: (gr.update(visible=True), gr.update(visible=True)),
507
+ outputs=[pages_table, extract_btn],
508
+ )
509
+
510
+ # Extract tokens
511
+ extract_btn.click(
512
+ fn=extract_tokens,
513
+ inputs=[pages_table],
514
+ outputs=[extraction_status, log_output, desktop_data, mobile_data],
515
+ ).then(
516
+ fn=lambda d: (d.get("colors", []), d.get("typography", []), d.get("spacing", [])),
517
+ inputs=[desktop_data],
518
+ outputs=[colors_table, typography_table, spacing_table],
519
+ ).then(
520
+ fn=lambda: gr.update(open=True),
521
+ outputs=[stage1_accordion],
522
+ )
523
+
524
+ # Viewport toggle
525
+ viewport_toggle.change(
526
+ fn=switch_viewport,
527
+ inputs=[viewport_toggle],
528
+ outputs=[colors_table, typography_table, spacing_table],
529
+ )
530
+
531
+ # =================================================================
532
+ # FOOTER
533
+ # =================================================================
534
+
535
+ gr.Markdown("""
536
+ ---
537
+ **Design System Extractor v2** | Built with Playwright + Gradio + LangGraph + HuggingFace
538
+
539
+ *A semi-automated co-pilot for design system recovery and modernization.*
540
+ """)
541
+
542
+ return app
543
+
544
+
545
+ # =============================================================================
546
+ # MAIN
547
+ # =============================================================================
548
+
549
+ if __name__ == "__main__":
550
+ app = create_ui()
551
+ app.launch(server_name="0.0.0.0", server_port=7860)