File size: 6,837 Bytes
77da9e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3038c10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77da9e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff03012
3038c10
77da9e2
3038c10
 
 
 
77da9e2
 
3038c10
 
77da9e2
 
 
 
3038c10
 
 
 
77da9e2
 
3038c10
 
77da9e2
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""
Unified Entry Point - API Architecture

This file now uses a unified API-based architecture for all deployments.
Both local development and Hugging Face Spaces use the same API layer.

Architecture:
    1. Starts API server in background (subprocess)
    2. Starts Gradio UI that connects to the API
    3. Everything goes through HTTP/REST

Benefits:
    - Single code path to maintain
    - Consistent behavior everywhere
    - Easy to test and debug
    - Proper separation of concerns

Usage:
    python app.py
    
The script will automatically:
    - Start the API server on http://localhost:8000
    - Start the Gradio UI on http://localhost:7860
"""

import os
os.environ['PYTORCH_ENABLE_MPS_FALLBACK'] = '1'

import subprocess
import time
import sys
import signal
import requests
from functools import partial

# Use shared UI components
from ui.shared_interface import create_interface
from ui.detection_wrapper import detect_with_api


# Configuration
API_HOST = os.getenv("API_HOST", "0.0.0.0")
API_PORT = int(os.getenv("API_PORT", "8000"))
API_URL = f"http://localhost:{API_PORT}"

UI_HOST = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
UI_PORT = int(os.getenv("GRADIO_SERVER_PORT", "7860"))


def start_api_server():
    """Start the API server in a subprocess"""
    print("πŸš€ Starting API server...")
    
    # Start API server as subprocess
    api_process = subprocess.Popen(
        [sys.executable, "app_api.py"],
        env={**os.environ, "UVICORN_HOST": API_HOST, "UVICORN_PORT": str(API_PORT)},
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1
    )
    
    # Wait for API to be ready
    max_wait = 60  # seconds
    wait_interval = 0.5
    elapsed = 0
    
    print(f"⏳ Waiting for API server at {API_URL}...")
    
    while elapsed < max_wait:
        try:
            response = requests.get(f"{API_URL}/health", timeout=2)
            if response.status_code == 200:
                print(f"βœ… API server ready at {API_URL}")
                
                # Optional: Warmup models to avoid timeout on first request
                # This is especially useful for CPU-only environments
                warmup_enabled = os.getenv("CU1_WARMUP_MODELS", "true").lower() in {"1", "true", "yes", "y"}
                if warmup_enabled:
                    print("πŸ”₯ Warming up models (this may take 1-3 minutes on first run)...")
                    try:
                        warmup_timeout = int(os.getenv("CU1_WARMUP_TIMEOUT", "180"))  # 3 minutes default
                        warmup_response = requests.post(f"{API_URL}/warmup", timeout=warmup_timeout)
                        if warmup_response.status_code == 200:
                            print("βœ… Models warmed up successfully!")
                        else:
                            print(f"⚠️  Warmup returned status {warmup_response.status_code}, continuing anyway...")
                    except requests.exceptions.Timeout:
                        print("⚠️  Warmup timed out, but API is ready. First request may be slower.")
                    except requests.exceptions.RequestException as e:
                        print(f"⚠️  Warmup failed: {e}, but API is ready. First request may be slower.")
                
                return api_process
        except requests.exceptions.RequestException:
            pass
        
        time.sleep(wait_interval)
        elapsed += wait_interval
        
        # Check if process died
        if api_process.poll() is not None:
            print("❌ API server failed to start!")
            print("\nAPI server output:")
            if api_process.stdout:
                print(api_process.stdout.read())
            sys.exit(1)
    
    print(f"❌ API server did not start within {max_wait} seconds")
    api_process.terminate()
    sys.exit(1)


def main():
    """Main entry point - Unified API architecture"""
    
    print("=" * 70)
    print("🎯 CU-1 UI Element Detector - Unified API Mode")
    print("=" * 70)
    print("\nπŸ“‘ Architecture: All traffic goes through API layer")
    print(f"   - API Server: {API_URL}")
    print(f"   - Gradio UI: http://localhost:{UI_PORT}")
    print("\nπŸ—οΈ  Benefits:")
    print("   - Single code path (easier to maintain)")
    print("   - Consistent behavior everywhere")
    print("   - Proper microservices architecture")
    print("=" * 70 + "\n")
    
    # Start API server in background
    api_process = start_api_server()
    
    # Setup cleanup on exit
    def cleanup(signum=None, frame=None):
        print("\n\nπŸ›‘ Shutting down...")
        if api_process and api_process.poll() is None:
            print("   Stopping API server...")
            api_process.terminate()
            try:
                api_process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                api_process.kill()
        print("   Goodbye! πŸ‘‹")
        sys.exit(0)
    
    signal.signal(signal.SIGINT, cleanup)
    signal.signal(signal.SIGTERM, cleanup)
    
    try:
        # Create Gradio interface with API detection function
        detection_fn = partial(detect_with_api, api_url=API_URL)
        
        demo = create_interface(
            detection_fn=detection_fn,
            title_suffix="Unified API Mode",
            show_api_info=True,
            api_url=API_URL
        )
        
        print(f"\n🎨 Starting Gradio UI on http://localhost:{UI_PORT}...\n")
        
        # Launch Gradio with automatic port fallback
        # API is automatically exposed at /api/predict for HF Spaces
        # Configure queue with longer timeout for CPU processing and model loading
        try:
            demo.queue(
                max_size=10,  # Allow up to 10 queued requests
                default_concurrency_limit=1  # Process one at a time to avoid memory issues
            ).launch(
                server_name=UI_HOST,
                server_port=UI_PORT,
                share=False,
                max_threads=1  # Single thread to avoid memory issues
            )
        except OSError as e:
            if "Cannot find empty port" in str(e):
                print(f"⚠️  Port {UI_PORT} is busy, trying to find a free port...")
                demo.queue(
                    max_size=10,
                    default_concurrency_limit=1
                ).launch(
                    server_name=UI_HOST,
                    server_port=None,  # Auto-select free port
                    share=False,
                    max_threads=1
                )
            else:
                raise
    except KeyboardInterrupt:
        cleanup()
    except Exception as e:
        print(f"\n❌ Error: {e}")
        cleanup()
    finally:
        cleanup()


if __name__ == "__main__":
    main()