Abdelkader HASSINE commited on
Commit
3038c10
·
1 Parent(s): 4fe5dcd

Deploy CU1-X to Hugging Face Spaces

Browse files

- Multi-model AI pipeline (RF-DETR, CLIP, OCR, BLIP)
- Unified API architecture
- Gradio web interface
- Full model weights included via Git LFS
- Ready for production deployment

Files changed (5) hide show
  1. api/endpoints.py +50 -0
  2. app.py +31 -4
  3. app_api.py +1 -0
  4. ui/detection_wrapper.py +8 -4
  5. ui/shared_interface.py +5 -1
api/endpoints.py CHANGED
@@ -51,6 +51,7 @@ async def root():
51
  "endpoints": {
52
  "/detect": "POST - Detect UI elements in an image",
53
  "/health": "GET - Health check",
 
54
  "/docs": "GET - Interactive API documentation"
55
  },
56
  "example": {
@@ -73,6 +74,40 @@ async def health_check():
73
  }
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  @app.post("/detect")
77
  async def detect_ui_elements(
78
  image: UploadFile = File(..., description="Image file to process"),
@@ -172,9 +207,15 @@ async def detect_ui_elements(
172
  )
173
 
174
  # Standard detection path: Use detection service
 
 
 
 
175
  service = get_detection_service()
176
 
177
  # Run analysis (pass parameters directly to avoid race conditions)
 
 
178
  analysis = service.analyze(
179
  pil_image,
180
  confidence_threshold=confidence_threshold,
@@ -187,8 +228,12 @@ async def detect_ui_elements(
187
  preprocess_mode=preprocess_mode,
188
  preprocess_preset=preprocess_preset
189
  )
 
 
190
 
191
  # Generate annotated image
 
 
192
  annotated = service.get_prediction_image(
193
  pil_image,
194
  confidence_threshold=confidence_threshold,
@@ -197,6 +242,11 @@ async def detect_ui_elements(
197
  return_format="numpy",
198
  analysis=analysis
199
  )
 
 
 
 
 
200
 
201
  # Build response
202
  return response_builder.build_detection_response(
 
51
  "endpoints": {
52
  "/detect": "POST - Detect UI elements in an image",
53
  "/health": "GET - Health check",
54
+ "/warmup": "POST - Preload models to avoid timeout on first request",
55
  "/docs": "GET - Interactive API documentation"
56
  },
57
  "example": {
 
74
  }
75
 
76
 
77
+ @app.post("/warmup")
78
+ async def warmup_models():
79
+ """
80
+ Warmup endpoint to preload models before first detection request.
81
+ This helps avoid timeout on the first run.
82
+ """
83
+ try:
84
+ service = get_detection_service()
85
+ # Force loading of all models by accessing them
86
+ # RF-DETR is already loaded in __init__
87
+ service._load_ocr() # Load OCR if enabled
88
+ service._load_clip() # Load CLIP if enabled
89
+ service._load_blip() # Load BLIP if enabled
90
+
91
+ return {
92
+ "status": "success",
93
+ "message": "Models warmed up successfully",
94
+ "models_loaded": {
95
+ "rfdetr": service.model is not None,
96
+ "ocr": service.ocr_reader is not None if service.enable_ocr else None,
97
+ "clip": service.clip_processor is not None if service.enable_clip else None,
98
+ "blip": service.blip_model is not None if service.enable_blip else None
99
+ }
100
+ }
101
+ except Exception as e:
102
+ import traceback
103
+ error_msg = f"Error during warmup: {str(e)}"
104
+ print(f"{error_msg}\n{traceback.format_exc()}")
105
+ return {
106
+ "status": "error",
107
+ "message": error_msg
108
+ }
109
+
110
+
111
  @app.post("/detect")
112
  async def detect_ui_elements(
113
  image: UploadFile = File(..., description="Image file to process"),
 
207
  )
208
 
209
  # Standard detection path: Use detection service
210
+ import time
211
+ start_time = time.time()
212
+ print(f"[API] Starting detection - Image size: {pil_image.size}, CLIP: {enable_clip}, OCR: {enable_ocr}, BLIP: {enable_blip}")
213
+
214
  service = get_detection_service()
215
 
216
  # Run analysis (pass parameters directly to avoid race conditions)
217
+ print(f"[API] Calling service.analyze()...")
218
+ analysis_start = time.time()
219
  analysis = service.analyze(
220
  pil_image,
221
  confidence_threshold=confidence_threshold,
 
228
  preprocess_mode=preprocess_mode,
229
  preprocess_preset=preprocess_preset
230
  )
231
+ analysis_time = time.time() - analysis_start
232
+ print(f"[API] service.analyze() completed in {analysis_time:.2f}s - Found {len(analysis.get('detections', []))} detections")
233
 
234
  # Generate annotated image
235
+ print(f"[API] Generating annotated image...")
236
+ annotated_start = time.time()
237
  annotated = service.get_prediction_image(
238
  pil_image,
239
  confidence_threshold=confidence_threshold,
 
242
  return_format="numpy",
243
  analysis=analysis
244
  )
245
+ annotated_time = time.time() - annotated_start
246
+ print(f"[API] Annotated image generated in {annotated_time:.2f}s")
247
+
248
+ total_time = time.time() - start_time
249
+ print(f"[API] Total detection time: {total_time:.2f}s")
250
 
251
  # Build response
252
  return response_builder.build_detection_response(
app.py CHANGED
@@ -73,6 +73,24 @@ def start_api_server():
73
  response = requests.get(f"{API_URL}/health", timeout=2)
74
  if response.status_code == 200:
75
  print(f"✅ API server ready at {API_URL}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  return api_process
77
  except requests.exceptions.RequestException:
78
  pass
@@ -142,19 +160,28 @@ def main():
142
 
143
  # Launch Gradio with automatic port fallback
144
  # API is automatically exposed at /api/predict for HF Spaces
 
145
  try:
146
- demo.queue().launch(
 
 
 
147
  server_name=UI_HOST,
148
  server_port=UI_PORT,
149
- share=False
 
150
  )
151
  except OSError as e:
152
  if "Cannot find empty port" in str(e):
153
  print(f"⚠️ Port {UI_PORT} is busy, trying to find a free port...")
154
- demo.queue().launch(
 
 
 
155
  server_name=UI_HOST,
156
  server_port=None, # Auto-select free port
157
- share=False
 
158
  )
159
  else:
160
  raise
 
73
  response = requests.get(f"{API_URL}/health", timeout=2)
74
  if response.status_code == 200:
75
  print(f"✅ API server ready at {API_URL}")
76
+
77
+ # Optional: Warmup models to avoid timeout on first request
78
+ # This is especially useful for CPU-only environments
79
+ warmup_enabled = os.getenv("CU1_WARMUP_MODELS", "true").lower() in {"1", "true", "yes", "y"}
80
+ if warmup_enabled:
81
+ print("🔥 Warming up models (this may take 1-3 minutes on first run)...")
82
+ try:
83
+ warmup_timeout = int(os.getenv("CU1_WARMUP_TIMEOUT", "180")) # 3 minutes default
84
+ warmup_response = requests.post(f"{API_URL}/warmup", timeout=warmup_timeout)
85
+ if warmup_response.status_code == 200:
86
+ print("✅ Models warmed up successfully!")
87
+ else:
88
+ print(f"⚠️ Warmup returned status {warmup_response.status_code}, continuing anyway...")
89
+ except requests.exceptions.Timeout:
90
+ print("⚠️ Warmup timed out, but API is ready. First request may be slower.")
91
+ except requests.exceptions.RequestException as e:
92
+ print(f"⚠️ Warmup failed: {e}, but API is ready. First request may be slower.")
93
+
94
  return api_process
95
  except requests.exceptions.RequestException:
96
  pass
 
160
 
161
  # Launch Gradio with automatic port fallback
162
  # API is automatically exposed at /api/predict for HF Spaces
163
+ # Configure queue with longer timeout for CPU processing and model loading
164
  try:
165
+ demo.queue(
166
+ max_size=10, # Allow up to 10 queued requests
167
+ default_concurrency_limit=1 # Process one at a time to avoid memory issues
168
+ ).launch(
169
  server_name=UI_HOST,
170
  server_port=UI_PORT,
171
+ share=False,
172
+ max_threads=1 # Single thread to avoid memory issues
173
  )
174
  except OSError as e:
175
  if "Cannot find empty port" in str(e):
176
  print(f"⚠️ Port {UI_PORT} is busy, trying to find a free port...")
177
+ demo.queue(
178
+ max_size=10,
179
+ default_concurrency_limit=1
180
+ ).launch(
181
  server_name=UI_HOST,
182
  server_port=None, # Auto-select free port
183
+ share=False,
184
+ max_threads=1
185
  )
186
  else:
187
  raise
app_api.py CHANGED
@@ -38,6 +38,7 @@ def main():
38
  print(f" - Root: http://localhost:{port}")
39
  print(f" - Detect: http://localhost:{port}/detect")
40
  print(f" - Health: http://localhost:{port}/health")
 
41
  print(f" - Docs: http://localhost:{port}/docs")
42
  print("\n💡 Tip: The Gradio UI connects to this API")
43
  print(" Run 'python app_ui.py' in another terminal")
 
38
  print(f" - Root: http://localhost:{port}")
39
  print(f" - Detect: http://localhost:{port}/detect")
40
  print(f" - Health: http://localhost:{port}/health")
41
+ print(f" - Warmup: http://localhost:{port}/warmup (preload models)")
42
  print(f" - Docs: http://localhost:{port}/docs")
43
  print("\n💡 Tip: The Gradio UI connects to this API")
44
  print(" Run 'python app_ui.py' in another terminal")
ui/detection_wrapper.py CHANGED
@@ -200,12 +200,14 @@ def detect_with_api(
200
  }
201
 
202
  # Call API
 
 
203
  try:
204
  response = requests.post(
205
  f"{api_url}/detect",
206
  files=files,
207
  data=data,
208
- timeout=120
209
  )
210
  response.raise_for_status()
211
  except requests.exceptions.ConnectionError:
@@ -225,19 +227,21 @@ Cannot connect to API server at `{api_url}`
225
  You can change this by setting the `CU1_API_URL` environment variable.
226
  """, None
227
  except requests.exceptions.Timeout:
 
228
  return None, f"""❌ **Timeout Error**
229
 
230
- The API request timed out after 120 seconds.
231
 
232
  This might happen with:
233
  - Very large images
234
- - First run (models need to download)
235
  - CPU-only processing (slower than GPU)
236
 
237
  **Try:**
238
  - Using a smaller image
239
- - Waiting for model downloads to complete
240
  - Checking API server logs for errors
 
241
  """, None
242
  except requests.exceptions.HTTPError as e:
243
  error_detail = "Unknown error"
 
200
  }
201
 
202
  # Call API
203
+ # Use configurable timeout (default 300s = 5min for CPU processing and model loading)
204
+ timeout_seconds = int(os.getenv("CU1_API_TIMEOUT", "300"))
205
  try:
206
  response = requests.post(
207
  f"{api_url}/detect",
208
  files=files,
209
  data=data,
210
+ timeout=timeout_seconds
211
  )
212
  response.raise_for_status()
213
  except requests.exceptions.ConnectionError:
 
227
  You can change this by setting the `CU1_API_URL` environment variable.
228
  """, None
229
  except requests.exceptions.Timeout:
230
+ timeout_seconds = int(os.getenv("CU1_API_TIMEOUT", "300"))
231
  return None, f"""❌ **Timeout Error**
232
 
233
+ The API request timed out after {timeout_seconds} seconds.
234
 
235
  This might happen with:
236
  - Very large images
237
+ - First run (models need to download - can take 2-5 minutes)
238
  - CPU-only processing (slower than GPU)
239
 
240
  **Try:**
241
  - Using a smaller image
242
+ - Waiting for model downloads to complete (check API server logs)
243
  - Checking API server logs for errors
244
+ - Increasing timeout: export CU1_API_TIMEOUT=600 (10 minutes)
245
  """, None
246
  except requests.exceptions.HTTPError as e:
247
  error_detail = "Unknown error"
ui/shared_interface.py CHANGED
@@ -7,6 +7,7 @@ different detection backends (direct service or API client).
7
  This eliminates code duplication between app.py and ui/gradio_interface.py
8
  """
9
 
 
10
  import gradio as gr
11
  from typing import Callable, Optional
12
 
@@ -261,6 +262,8 @@ def create_interface(
261
 
262
  # Connect detection button
263
  # api_name exposes this function as /api/predict endpoint for Hugging Face Spaces
 
 
264
  detect_button.click(
265
  fn=detection_fn,
266
  inputs=[
@@ -277,7 +280,8 @@ def create_interface(
277
  preprocess_preset_dropdown
278
  ],
279
  outputs=[output_image, summary_output, json_output],
280
- api_name="predict" # Expose as /api/predict endpoint
 
281
  )
282
 
283
  # Build footer markdown
 
7
  This eliminates code duplication between app.py and ui/gradio_interface.py
8
  """
9
 
10
+ import os
11
  import gradio as gr
12
  from typing import Callable, Optional
13
 
 
262
 
263
  # Connect detection button
264
  # api_name exposes this function as /api/predict endpoint for Hugging Face Spaces
265
+ # max_time increases Gradio's function timeout (default is 60s, we set to 300s = 5min)
266
+ max_time_seconds = int(os.getenv("GRADIO_MAX_TIME", "300")) # 5 minutes default
267
  detect_button.click(
268
  fn=detection_fn,
269
  inputs=[
 
280
  preprocess_preset_dropdown
281
  ],
282
  outputs=[output_image, summary_output, json_output],
283
+ api_name="predict", # Expose as /api/predict endpoint
284
+ max_time=max_time_seconds # Increase Gradio function timeout
285
  )
286
 
287
  # Build footer markdown