Spaces:
Sleeping
Sleeping
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
- api/endpoints.py +50 -0
- app.py +31 -4
- app_api.py +1 -0
- ui/detection_wrapper.py +8 -4
- 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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
|
| 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
|