makeitfr commited on
Commit
47a6408
·
verified ·
1 Parent(s): b72c2f4

Upload app_hf_spaces_server.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app_hf_spaces_server.py +773 -0
app_hf_spaces_server.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ OmniParser UI Element Detection - FastAPI Server for HF Spaces
4
+ Full REST API + Web UI served on port 7860
5
+
6
+ Features:
7
+ - /api/analyze - POST image for UI element detection
8
+ - /api/health - Health check
9
+ - / - HTML web interface
10
+ - Automatic model initialization on startup
11
+ - CORS enabled for cross-origin requests
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import time
18
+ import base64
19
+ import cv2
20
+ import numpy as np
21
+ import io
22
+ import csv
23
+ from pathlib import Path
24
+ from typing import Dict, Any, Optional, Tuple, List
25
+ from contextlib import asynccontextmanager
26
+ import threading
27
+
28
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Request
29
+ from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
30
+ from fastapi.staticfiles import StaticFiles
31
+ from fastapi.middleware.cors import CORSMiddleware
32
+ import uvicorn
33
+ from PIL import Image
34
+
35
+ # Configure OmniParser
36
+ os.environ["OMP_NUM_THREADS"] = "4"
37
+
38
+ # Add OmniParser to path dynamically
39
+ omoi_root = Path(__file__).parent
40
+ sys.path.insert(0, str(omoi_root / 'OmniParser'))
41
+ from util.omniparser import Omniparser
42
+ from config import get_omniparser_config
43
+
44
+ # ============ Utility Functions ============
45
+
46
+ def to_rgb(img: np.ndarray) -> Optional[np.ndarray]:
47
+ """Convert image to BGR format."""
48
+ if img is None:
49
+ return None
50
+ if len(img.shape) == 2:
51
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
52
+ if img.shape[2] == 4:
53
+ return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
54
+ return img
55
+
56
+ def extract_coordinates(matches: List[Dict]) -> List[Dict]:
57
+ """Extract coordinates from matches for JSON export."""
58
+ coords = []
59
+ for i, match in enumerate(matches, 1):
60
+ bbox = match['bbox']
61
+ center = match['center']
62
+ coords.append({
63
+ 'element_id': f"crop_{i:04d}",
64
+ 'x': center['x'],
65
+ 'y': center['y'],
66
+ 'x1': bbox['x1'],
67
+ 'y1': bbox['y1'],
68
+ 'x2': bbox['x2'],
69
+ 'y2': bbox['y2'],
70
+ 'width': bbox['width'],
71
+ 'height': bbox['height'],
72
+ 'confidence': match['confidence'],
73
+ 'template_file': match['template_file']
74
+ })
75
+ return coords
76
+
77
+ def match_ui_elements(
78
+ original_image_array: np.ndarray,
79
+ cropped_images_dir: str,
80
+ threshold: float = 0.7
81
+ ) -> Tuple[list, Dict]:
82
+ """Match cropped UI templates against original image."""
83
+ original_img_rgb = to_rgb(original_image_array)
84
+ if original_img_rgb is None:
85
+ raise ValueError("Failed to convert original image")
86
+
87
+ img_height, img_width = original_img_rgb.shape[:2]
88
+
89
+ # Load templates
90
+ templates = {}
91
+ template_files = sorted(Path(cropped_images_dir).glob('crop_*.png'))
92
+
93
+ for template_file in template_files:
94
+ template_img = cv2.imread(str(template_file), cv2.IMREAD_UNCHANGED)
95
+ if template_img is not None:
96
+ template_img_rgb = to_rgb(template_img)
97
+ templates[template_file.name] = template_img_rgb
98
+
99
+ # Match templates
100
+ matches = []
101
+ for template_name, template_img in templates.items():
102
+ try:
103
+ if template_img.shape[0] > img_height or template_img.shape[1] > img_width:
104
+ continue
105
+ if template_img.shape[0] < 4 or template_img.shape[1] < 4:
106
+ continue
107
+
108
+ result = cv2.matchTemplate(original_img_rgb, template_img, cv2.TM_CCOEFF_NORMED)
109
+ _, max_val, _, max_loc = cv2.minMaxLoc(result)
110
+
111
+ if max_val >= threshold:
112
+ template_h, template_w = template_img.shape[:2]
113
+ x1, y1 = max_loc
114
+ x2 = x1 + template_w
115
+ y2 = y1 + template_h
116
+
117
+ center_x = (x1 + x2) / 2
118
+ center_y = (y1 + y2) / 2
119
+
120
+ matches.append({
121
+ 'template_id': template_name.replace('.png', ''),
122
+ 'template_file': template_name,
123
+ 'confidence': float(max_val),
124
+ 'bbox': {
125
+ 'x1': int(x1),
126
+ 'y1': int(y1),
127
+ 'x2': int(x2),
128
+ 'y2': int(y2),
129
+ 'width': int(template_w),
130
+ 'height': int(template_h)
131
+ },
132
+ 'center': {
133
+ 'x': int(center_x),
134
+ 'y': int(center_y)
135
+ }
136
+ })
137
+ except Exception:
138
+ continue
139
+
140
+ matches.sort(key=lambda x: x['confidence'], reverse=True)
141
+
142
+ metadata = {
143
+ 'image_size': {'width': img_width, 'height': img_height},
144
+ 'templates_loaded': len(templates),
145
+ 'threshold': threshold,
146
+ 'matches_found': len(matches)
147
+ }
148
+
149
+ return matches, metadata
150
+
151
+ def visualize_matches(
152
+ original_image_array: np.ndarray,
153
+ matches: list
154
+ ) -> np.ndarray:
155
+ """Create visualization with bounding boxes."""
156
+ img = original_image_array.copy()
157
+
158
+ for match in matches:
159
+ bbox = match['bbox']
160
+ center = match['center']
161
+ confidence = match['confidence']
162
+ template_id = match['template_id']
163
+
164
+ # Draw bounding box
165
+ color = (0, 255, 0) # Green
166
+ thickness = 2
167
+ cv2.rectangle(img, (bbox['x1'], bbox['y1']), (bbox['x2'], bbox['y2']), color, thickness)
168
+
169
+ # Draw center point
170
+ cv2.circle(img, (center['x'], center['y']), 3, (0, 0, 255), -1) # Red
171
+
172
+ # Draw label
173
+ label = f"{template_id} ({confidence:.2f})"
174
+ cv2.putText(img, label, (bbox['x1'], bbox['y1'] - 5),
175
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1)
176
+
177
+ return img
178
+
179
+ def matches_to_csv(matches: list) -> str:
180
+ """Convert matches to CSV format."""
181
+ output = io.StringIO()
182
+ writer = csv.writer(output)
183
+ writer.writerow([
184
+ 'Element_ID', 'X', 'Y', 'X1', 'Y1', 'X2', 'Y2', 'Width', 'Height', 'Confidence'
185
+ ])
186
+
187
+ for i, match in enumerate(matches, 1):
188
+ bbox = match['bbox']
189
+ center = match['center']
190
+
191
+ writer.writerow([
192
+ f"crop_{i:04d}",
193
+ center['x'], center['y'],
194
+ bbox['x1'], bbox['y1'], bbox['x2'], bbox['y2'],
195
+ bbox['width'], bbox['height'],
196
+ f"{match['confidence']:.4f}"
197
+ ])
198
+
199
+ return output.getvalue()
200
+
201
+ # ============ FastAPI Setup ============
202
+
203
+ # Global OmniParser instance
204
+ omniparser = None
205
+ omniparser_lock = threading.Lock()
206
+
207
+ @asynccontextmanager
208
+ async def lifespan(app: FastAPI):
209
+ """Initialize and cleanup on server startup/shutdown."""
210
+ global omniparser
211
+ print("\n" + "="*60)
212
+ print("🚀 Initializing OmniParser...")
213
+ print("="*60)
214
+
215
+ try:
216
+ with omniparser_lock:
217
+ config = get_omniparser_config()
218
+ print(f"✓ Config loaded from: {config['omniparser_dir']}")
219
+ print(f"✓ Loading YOLO model...")
220
+ omniparser = Omniparser(config)
221
+ print(f"✓ OmniParser initialized successfully!")
222
+ print("="*60 + "\n")
223
+ except Exception as e:
224
+ print(f"✗ ERROR during initialization: {str(e)}")
225
+ import traceback
226
+ traceback.print_exc()
227
+ print("="*60 + "\n")
228
+
229
+ yield # Application runs here
230
+
231
+ # Cleanup
232
+ print("\n[Server] Shutting down...")
233
+
234
+ # Create FastAPI app
235
+ app = FastAPI(
236
+ title="OmniParser UI Detection API",
237
+ description="Detects and locates UI elements in screenshots",
238
+ version="1.0.0",
239
+ lifespan=lifespan
240
+ )
241
+
242
+ # Add CORS middleware
243
+ app.add_middleware(
244
+ CORSMiddleware,
245
+ allow_origins=["*"],
246
+ allow_credentials=True,
247
+ allow_methods=["*"],
248
+ allow_headers=["*"],
249
+ )
250
+
251
+ # ============ Web Interface ============
252
+
253
+ HTML_UI = """
254
+ <!DOCTYPE html>
255
+ <html>
256
+ <head>
257
+ <meta charset="UTF-8">
258
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
259
+ <title>OmniParser UI Detector</title>
260
+ <style>
261
+ * {
262
+ margin: 0;
263
+ padding: 0;
264
+ box-sizing: border-box;
265
+ }
266
+
267
+ body {
268
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
269
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
270
+ min-height: 100vh;
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ padding: 20px;
275
+ }
276
+
277
+ .container {
278
+ background: white;
279
+ border-radius: 12px;
280
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
281
+ max-width: 1000px;
282
+ width: 100%;
283
+ padding: 40px;
284
+ }
285
+
286
+ h1 {
287
+ color: #333;
288
+ margin-bottom: 10px;
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 10px;
292
+ }
293
+
294
+ .subtitle {
295
+ color: #666;
296
+ margin-bottom: 30px;
297
+ font-size: 14px;
298
+ }
299
+
300
+ .upload-area {
301
+ border: 2px dashed #667eea;
302
+ border-radius: 8px;
303
+ padding: 40px;
304
+ text-align: center;
305
+ cursor: pointer;
306
+ transition: all 0.3s;
307
+ margin-bottom: 20px;
308
+ }
309
+
310
+ .upload-area:hover {
311
+ border-color: #764ba2;
312
+ background: #f8f9ff;
313
+ }
314
+
315
+ .upload-area.dragover {
316
+ border-color: #764ba2;
317
+ background: #f0f2ff;
318
+ }
319
+
320
+ input[type="file"] {
321
+ display: none;
322
+ }
323
+
324
+ .upload-text {
325
+ font-size: 16px;
326
+ color: #667eea;
327
+ margin-bottom: 10px;
328
+ }
329
+
330
+ .upload-hint {
331
+ font-size: 12px;
332
+ color: #999;
333
+ }
334
+
335
+ button {
336
+ background: #667eea;
337
+ color: white;
338
+ border: none;
339
+ padding: 12px 30px;
340
+ border-radius: 6px;
341
+ cursor: pointer;
342
+ font-size: 14px;
343
+ font-weight: 600;
344
+ transition: background 0.3s;
345
+ }
346
+
347
+ button:hover {
348
+ background: #764ba2;
349
+ }
350
+
351
+ button:disabled {
352
+ background: #ccc;
353
+ cursor: not-allowed;
354
+ }
355
+
356
+ .results {
357
+ display: none;
358
+ margin-top: 30px;
359
+ }
360
+
361
+ .results.active {
362
+ display: block;
363
+ }
364
+
365
+ .result-section {
366
+ margin-bottom: 25px;
367
+ }
368
+
369
+ .result-section h3 {
370
+ color: #333;
371
+ margin-bottom: 10px;
372
+ font-size: 14px;
373
+ text-transform: uppercase;
374
+ letter-spacing: 1px;
375
+ }
376
+
377
+ .preview-image {
378
+ max-width: 100%;
379
+ border-radius: 6px;
380
+ margin-bottom: 15px;
381
+ }
382
+
383
+ .stats {
384
+ display: grid;
385
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
386
+ gap: 15px;
387
+ margin-bottom: 20px;
388
+ }
389
+
390
+ .stat {
391
+ background: #f8f9ff;
392
+ padding: 15px;
393
+ border-radius: 6px;
394
+ border-left: 4px solid #667eea;
395
+ }
396
+
397
+ .stat-value {
398
+ font-size: 24px;
399
+ font-weight: bold;
400
+ color: #667eea;
401
+ }
402
+
403
+ .stat-label {
404
+ font-size: 12px;
405
+ color: #999;
406
+ margin-top: 5px;
407
+ }
408
+
409
+ textarea {
410
+ width: 100%;
411
+ min-height: 200px;
412
+ padding: 12px;
413
+ border: 1px solid #ddd;
414
+ border-radius: 6px;
415
+ font-family: 'Monaco', 'Courier New', monospace;
416
+ font-size: 12px;
417
+ resize: vertical;
418
+ }
419
+
420
+ .loading {
421
+ display: none;
422
+ text-align: center;
423
+ color: #667eea;
424
+ }
425
+
426
+ .loading.active {
427
+ display: block;
428
+ }
429
+
430
+ .spinner {
431
+ border: 3px solid #f3f3f3;
432
+ border-top: 3px solid #667eea;
433
+ border-radius: 50%;
434
+ width: 40px;
435
+ height: 40px;
436
+ animation: spin 1s linear infinite;
437
+ margin: 20px auto;
438
+ }
439
+
440
+ @keyframes spin {
441
+ 0% { transform: rotate(0deg); }
442
+ 100% { transform: rotate(360deg); }
443
+ }
444
+
445
+ .error {
446
+ background: #fee;
447
+ color: #c33;
448
+ padding: 12px;
449
+ border-radius: 6px;
450
+ margin-bottom: 20px;
451
+ display: none;
452
+ }
453
+
454
+ .error.active {
455
+ display: block;
456
+ }
457
+
458
+ .download-buttons {
459
+ display: flex;
460
+ gap: 10px;
461
+ margin-top: 15px;
462
+ flex-wrap: wrap;
463
+ }
464
+
465
+ .download-btn {
466
+ background: #28a745;
467
+ font-size: 13px;
468
+ padding: 10px 20px;
469
+ }
470
+
471
+ .download-btn:hover {
472
+ background: #218838;
473
+ }
474
+ </style>
475
+ </head>
476
+ <body>
477
+ <div class="container">
478
+ <h1>🎯 OmniParser UI Detector</h1>
479
+ <p class="subtitle">Upload a UI screenshot to detect and locate all UI elements</p>
480
+
481
+ <div class="upload-area" id="uploadArea">
482
+ <input type="file" id="fileInput" accept="image/*">
483
+ <div class="upload-text">Click to upload or drag and drop</div>
484
+ <div class="upload-hint">PNG, JPG (max 10MB)</div>
485
+ </div>
486
+
487
+ <button id="analyzeBtn" disabled>🔍 Analyze Image</button>
488
+
489
+ <div class="error" id="errorDiv"></div>
490
+
491
+ <div class="loading" id="loadingDiv">
492
+ <div class="spinner"></div>
493
+ <p>Processing image... (this may take 30-60 seconds)</p>
494
+ </div>
495
+
496
+ <div class="results" id="results">
497
+ <div class="result-section">
498
+ <h3>📊 Statistics</h3>
499
+ <div class="stats">
500
+ <div class="stat">
501
+ <div class="stat-value" id="elementCount">0</div>
502
+ <div class="stat-label">Elements Detected</div>
503
+ </div>
504
+ <div class="stat">
505
+ <div class="stat-value" id="processingTime">0s</div>
506
+ <div class="stat-label">Processing Time</div>
507
+ </div>
508
+ </div>
509
+ </div>
510
+
511
+ <div class="result-section">
512
+ <h3>🖼️ Visualization</h3>
513
+ <img id="vizImage" class="preview-image" src="">
514
+ </div>
515
+
516
+ <div class="result-section">
517
+ <h3>📋 Coordinates (JSON)</h3>
518
+ <textarea id="jsonOutput" readonly></textarea>
519
+ <div class="download-buttons">
520
+ <button class="download-btn" onclick="downloadJSON()">⬇️ Download JSON</button>
521
+ </div>
522
+ </div>
523
+
524
+ <div class="result-section">
525
+ <h3>📈 Coordinates (CSV)</h3>
526
+ <textarea id="csvOutput" readonly></textarea>
527
+ <div class="download-buttons">
528
+ <button class="download-btn" onclick="downloadCSV()">⬇️ Download CSV</button>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+
534
+ <script>
535
+ const uploadArea = document.getElementById('uploadArea');
536
+ const fileInput = document.getElementById('fileInput');
537
+ const analyzeBtn = document.getElementById('analyzeBtn');
538
+ const results = document.getElementById('results');
539
+ const loadingDiv = document.getElementById('loadingDiv');
540
+ const errorDiv = document.getElementById('errorDiv');
541
+
542
+ uploadArea.addEventListener('click', () => fileInput.click());
543
+
544
+ uploadArea.addEventListener('dragover', (e) => {
545
+ e.preventDefault();
546
+ uploadArea.classList.add('dragover');
547
+ });
548
+
549
+ uploadArea.addEventListener('dragleave', () => {
550
+ uploadArea.classList.remove('dragover');
551
+ });
552
+
553
+ uploadArea.addEventListener('drop', (e) => {
554
+ e.preventDefault();
555
+ uploadArea.classList.remove('dragover');
556
+ if (e.dataTransfer.files.length) {
557
+ fileInput.files = e.dataTransfer.files;
558
+ analyzeBtn.disabled = false;
559
+ }
560
+ });
561
+
562
+ fileInput.addEventListener('change', () => {
563
+ if (fileInput.files.length) {
564
+ analyzeBtn.disabled = false;
565
+ }
566
+ });
567
+
568
+ analyzeBtn.addEventListener('click', async () => {
569
+ if (!fileInput.files.length) return;
570
+
571
+ const file = fileInput.files[0];
572
+ const formData = new FormData();
573
+ formData.append('file', file);
574
+
575
+ errorDiv.classList.remove('active');
576
+ results.classList.remove('active');
577
+ loadingDiv.classList.add('active');
578
+ analyzeBtn.disabled = true;
579
+
580
+ try {
581
+ const response = await fetch('/api/analyze', {
582
+ method: 'POST',
583
+ body: formData
584
+ });
585
+
586
+ if (!response.ok) {
587
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
588
+ }
589
+
590
+ const data = await response.json();
591
+ displayResults(data);
592
+ } catch (error) {
593
+ showError(`Error: ${error.message}`);
594
+ } finally {
595
+ loadingDiv.classList.remove('active');
596
+ analyzeBtn.disabled = false;
597
+ }
598
+ });
599
+
600
+ function displayResults(data) {
601
+ document.getElementById('elementCount').textContent = data.analysis.total_elements_detected;
602
+ document.getElementById('processingTime').textContent = data.processing_time_seconds.toFixed(2) + 's';
603
+ document.getElementById('vizImage').src = 'data:image/png;base64,' + data.exports.visualization_png_base64;
604
+ document.getElementById('jsonOutput').value = JSON.stringify(data.analysis.elements, null, 2);
605
+ document.getElementById('csvOutput').value = data.exports.csv_data;
606
+
607
+ results.classList.add('active');
608
+ }
609
+
610
+ function showError(msg) {
611
+ errorDiv.textContent = msg;
612
+ errorDiv.classList.add('active');
613
+ }
614
+
615
+ function downloadJSON() {
616
+ const json = document.getElementById('jsonOutput').value;
617
+ const blob = new Blob([json], { type: 'application/json' });
618
+ const url = URL.createObjectURL(blob);
619
+ const a = document.createElement('a');
620
+ a.href = url;
621
+ a.download = 'coordinates.json';
622
+ a.click();
623
+ }
624
+
625
+ function downloadCSV() {
626
+ const csv = document.getElementById('csvOutput').value;
627
+ const blob = new Blob([csv], { type: 'text/csv' });
628
+ const url = URL.createObjectURL(blob);
629
+ const a = document.createElement('a');
630
+ a.href = url;
631
+ a.download = 'coordinates.csv';
632
+ a.click();
633
+ }
634
+ </script>
635
+ </body>
636
+ </html>
637
+ """
638
+
639
+ # ============ API Endpoints ============
640
+
641
+ @app.get("/")
642
+ async def web_ui():
643
+ """Serve web UI."""
644
+ return HTMLResponse(content=HTML_UI)
645
+
646
+ @app.get("/api/health")
647
+ async def health():
648
+ """Health check endpoint."""
649
+ status = "ok" if omniparser else "initializing"
650
+ return {
651
+ "status": status,
652
+ "service": "OmniParser UI Detection API",
653
+ "mode": "HF Spaces"
654
+ }
655
+
656
+ @app.post("/api/analyze")
657
+ async def analyze_image(file: UploadFile = File(...)):
658
+ """
659
+ Analyze an image for UI elements.
660
+
661
+ Returns detailed JSON with coordinates, visualization, and CSV data.
662
+ """
663
+
664
+ if not omniparser:
665
+ raise HTTPException(status_code=503, detail="OmniParser not initialized. Please try again in a moment.")
666
+
667
+ try:
668
+ print(f"\n[API] Analyzing: {file.filename}")
669
+ start_time = time.time()
670
+
671
+ # 1. Read image
672
+ print("[Step 1] Reading image...")
673
+ content = await file.read()
674
+ np_array = np.frombuffer(content, np.uint8)
675
+ original_img = cv2.imdecode(np_array, cv2.IMREAD_UNCHANGED)
676
+
677
+ if original_img is None:
678
+ raise HTTPException(status_code=400, detail="Failed to decode image")
679
+
680
+ print(f"[Step 1] ✓ Image loaded: {original_img.shape}")
681
+
682
+ # 2. Encode for OmniParser
683
+ print("[Step 2] Encoding for OmniParser...")
684
+ _, buffer = cv2.imencode('.png', original_img)
685
+ image_base64 = base64.b64encode(buffer).decode()
686
+
687
+ # 3. Run OmniParser
688
+ print("[Step 3] Running OmniParser...")
689
+ omni_start = time.time()
690
+ _, parsed_content = omniparser.parse(image_base64)
691
+ omni_time = time.time() - omni_start
692
+ print(f"[Step 3] ✓ Complete in {omni_time:.2f}s")
693
+
694
+ # 4. Match templates
695
+ print("[Step 4] Matching templates...")
696
+ cropped_dir = '/tmp/omoi_cropped_images'
697
+ if not Path(cropped_dir).exists():
698
+ print(f"⚠️ Creating cropped_images cache dir...")
699
+ Path(cropped_dir).mkdir(parents=True, exist_ok=True)
700
+
701
+ match_start = time.time()
702
+ matches, metadata = match_ui_elements(original_img, cropped_dir, threshold=0.7)
703
+ match_time = time.time() - match_start
704
+ print(f"[Step 4] ✓ Found {len(matches)} elements in {match_time:.2f}s")
705
+
706
+ # 5. Create visualization
707
+ print("[Step 5] Creating visualization...")
708
+ viz_img = visualize_matches(original_img, matches)
709
+ _, viz_buffer = cv2.imencode('.png', viz_img)
710
+ viz_base64 = base64.b64encode(viz_buffer).decode()
711
+
712
+ # 6. Extract coordinates
713
+ print("[Step 6] Extracting coordinates...")
714
+ coordinates = extract_coordinates(matches)
715
+
716
+ # 7. Generate CSV
717
+ print("[Step 7] Generating CSV...")
718
+ csv_data = matches_to_csv(matches)
719
+
720
+ # 8. Prepare response
721
+ total_time = time.time() - start_time
722
+ print(f"[API] ✓ Complete in {total_time:.2f}s\n")
723
+
724
+ return {
725
+ "status": "success",
726
+ "processing_time_seconds": total_time,
727
+ "timing": {
728
+ "omniparser_seconds": omni_time,
729
+ "template_matching_seconds": match_time
730
+ },
731
+ "image_info": {
732
+ "filename": file.filename,
733
+ "size": metadata['image_size']
734
+ },
735
+ "analysis": {
736
+ "total_elements_detected": len(coordinates),
737
+ "elements": coordinates
738
+ },
739
+ "exports": {
740
+ "csv_data": csv_data,
741
+ "visualization_png_base64": viz_base64
742
+ }
743
+ }
744
+
745
+ except HTTPException:
746
+ raise
747
+ except Exception as e:
748
+ print(f"[ERROR] {str(e)}")
749
+ import traceback
750
+ traceback.print_exc()
751
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
752
+
753
+ # ============ Main ============
754
+
755
+ if __name__ == "__main__":
756
+ import argparse
757
+
758
+ parser = argparse.ArgumentParser(description="OmniParser FastAPI Server for HF Spaces")
759
+ parser.add_argument("--port", type=int, default=7860, help="Server port (default: 7860)")
760
+ parser.add_argument("--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)")
761
+ args = parser.parse_args()
762
+
763
+ print(f"\n🚀 Starting server on {args.host}:{args.port}")
764
+ print(f" Web UI: http://localhost:{args.port}")
765
+ print(f" API Docs: http://localhost:{args.port}/docs")
766
+ print(f" Analyze endpoint: POST http://localhost:{args.port}/api/analyze\n")
767
+
768
+ uvicorn.run(
769
+ app,
770
+ host=args.host,
771
+ port=args.port,
772
+ loop="asyncio" # Use asyncio instead of uvloop (more compatible)
773
+ )