|
|
""" |
|
|
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': |
|
|
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: |
|
|
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() |
|
|
|
|
|
|
|
|
old_pattern = "window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''" |
|
|
new_pattern = old_pattern |
|
|
|
|
|
|
|
|
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""" |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
try: |
|
|
dist_time = os.path.getmtime('frontend/dist/index.html') |
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
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__": |
|
|
|
|
|
signal.signal(signal.SIGINT, cleanup_handler) |
|
|
signal.signal(signal.SIGTERM, cleanup_handler) |
|
|
|
|
|
print("π Starting Edge LLM...") |
|
|
|
|
|
|
|
|
port = int(os.getenv("PORT", "7860")) |
|
|
print(f"π‘ Using port: {port}") |
|
|
|
|
|
|
|
|
is_hf_space = os.getenv("SPACE_ID") is not None |
|
|
|
|
|
if not is_hf_space: |
|
|
|
|
|
kill_processes_on_port(port) |
|
|
|
|
|
|
|
|
if should_rebuild_frontend(): |
|
|
print("π¨ Building frontend...") |
|
|
build_frontend() |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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!") |
|
|
|
|
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=port) |
|
|
|
|
|
except Exception as e: |
|
|
print(f"β Error starting server: {e}") |
|
|
sys.exit(1) |
|
|
|