edgellm / app.py
wu981526092's picture
Fix HF Spaces startup - use port 7860 and simplify startup logic
360b135
raw
history blame
7.38 kB
"""
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)