""" Edge LLM API - Main application entry point with integrated frontend This entry point handles both backend API and frontend serving, with automatic port detection and process management. """ import uvicorn import socket import subprocess import sys import os import time import signal import webbrowser from backend.main import app def find_free_port(start_port=8000, max_attempts=50): """Find a free port starting from start_port""" for port in range(start_port, start_port + max_attempts): try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('localhost', port)) return port except OSError: continue raise RuntimeError(f"Could not find a free port in range {start_port}-{start_port + max_attempts}") def kill_processes_on_port(port): """Kill processes using the specified port""" try: if os.name == 'nt': # Windows result = subprocess.run(['netstat', '-ano'], capture_output=True, text=True) lines = result.stdout.split('\n') for line in lines: if f':{port}' in line and 'LISTENING' in line: parts = line.split() if len(parts) >= 5: pid = parts[-1] try: subprocess.run(['taskkill', '/pid', pid, '/f'], capture_output=True, check=True) print(f"✅ Killed process {pid} on port {port}") except subprocess.CalledProcessError: pass else: # Unix/Linux/macOS try: result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True) pids = result.stdout.strip().split('\n') for pid in pids: if pid: subprocess.run(['kill', '-9', pid], capture_output=True) print(f"✅ Killed process {pid} on port {port}") except subprocess.CalledProcessError: pass except Exception as e: print(f"⚠️ Warning: Could not kill processes on port {port}: {e}") def update_frontend_config(port): """Update frontend configuration to use the correct backend port""" frontend_files = [ 'frontend/src/pages/Models.tsx', 'frontend/src/pages/Playground.tsx' ] for file_path in frontend_files: if os.path.exists(file_path): try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # Update the baseUrl to use the current port (no longer needed with dynamic ports) old_pattern = "window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''" new_pattern = old_pattern # No change needed since it's already dynamic # No need to update frontend files since they use dynamic origins now print(f"✅ Frontend uses dynamic origins - no port updates needed") except Exception as e: print(f"⚠️ Warning: Could not update {file_path}: {e}") def build_frontend(): """Build the frontend if needed""" if not os.path.exists('frontend/dist') or not os.listdir('frontend/dist'): print("🔨 Building frontend...") try: os.chdir('frontend') subprocess.run(['npm', 'install'], check=True, capture_output=True) subprocess.run(['npm', 'run', 'build'], check=True, capture_output=True) os.chdir('..') print("✅ Frontend built successfully") except subprocess.CalledProcessError as e: print(f"❌ Frontend build failed: {e}") os.chdir('..') return False except FileNotFoundError: print("❌ npm not found. Please install Node.js") return False return True def should_rebuild_frontend(): """Check if frontend needs to be rebuilt""" # Check if build exists if not (os.path.exists('frontend/dist/index.html') and os.path.exists('frontend/dist/assets')): print("⚠️ Frontend build not found - will build it") return True # Check if source is newer than build try: dist_time = os.path.getmtime('frontend/dist/index.html') # Check key source files source_files = [ 'frontend/src', 'frontend/package.json', 'frontend/vite.config.ts', 'frontend/tsconfig.json' ] for src_path in source_files: if os.path.exists(src_path): if os.path.isdir(src_path): # Check all files in directory for root, dirs, files in os.walk(src_path): for file in files: file_path = os.path.join(root, file) if os.path.getmtime(file_path) > dist_time: print(f"🔄 Source files changed - will rebuild frontend") return True else: if os.path.getmtime(src_path) > dist_time: print(f"🔄 {src_path} changed - will rebuild frontend") return True print("✅ Frontend build is up to date") return False except Exception as e: print(f"⚠️ Error checking build status: {e} - will rebuild") return True def cleanup_handler(signum, frame): """Handle cleanup on exit""" print("\n🛑 Shutting down Edge LLM...") sys.exit(0) if __name__ == "__main__": # Set up signal handlers signal.signal(signal.SIGINT, cleanup_handler) signal.signal(signal.SIGTERM, cleanup_handler) print("🚀 Starting Edge LLM...") # Use HF Spaces default port (7860) or environment variable port = int(os.getenv("PORT", "7860")) print(f"📡 Using port: {port}") # For Hugging Face Spaces, skip complex port logic and auto-build is_hf_space = os.getenv("SPACE_ID") is not None if not is_hf_space: # Local development: kill existing processes and auto-build kill_processes_on_port(port) # Auto-build frontend if needed if should_rebuild_frontend(): print("🔨 Building frontend...") build_frontend() # Auto-open browser for local development def open_browser(): time.sleep(2) webbrowser.open(f'http://localhost:{port}') import threading browser_thread = threading.Thread(target=open_browser) browser_thread.daemon = True browser_thread.start() try: # Start the backend server print(f"🌐 Starting server on http://{'0.0.0.0' if is_hf_space else 'localhost'}:{port}") print("🎯 Frontend and Backend integrated - ready to use!") # Start the server uvicorn.run(app, host="0.0.0.0", port=port) except Exception as e: print(f"❌ Error starting server: {e}") sys.exit(1)