223 lines
7.9 KiB
Python
Executable File
223 lines
7.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
start.py — Start the AI Video Upscaler web application.
|
|
|
|
Usage:
|
|
python start.py # Start on default port 8000 (binds to 0.0.0.0 for LAN access)
|
|
python start.py --port 9000 # Start on a custom port
|
|
python start.py --host 127.0.0.1 # Bind to loopback only (no LAN access)
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import webbrowser
|
|
|
|
# ── Paths ────────────────────────────────────────────────────────────────────
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PID_FILE = os.path.join(BASE_DIR, ".uvicorn.pid")
|
|
LOG_FILE = os.path.join(BASE_DIR, "server.log")
|
|
|
|
VENV_PYTHON = sys.executable # default fallback
|
|
|
|
def ensure_venv():
|
|
"""Detect or create the virtual environment and install dependencies."""
|
|
global VENV_PYTHON
|
|
|
|
# 1. Already inside an activated venv — use it
|
|
if sys.prefix != sys.base_prefix:
|
|
VENV_PYTHON = sys.executable
|
|
return
|
|
|
|
venv_dir = os.path.join(BASE_DIR, "venv")
|
|
dot_venv_dir = os.path.join(BASE_DIR, ".venv")
|
|
|
|
# Check if either venv/ or .venv/ exists with a python interpreter
|
|
selected_venv = None
|
|
if os.path.isdir(os.path.join(venv_dir, "bin")):
|
|
selected_venv = venv_dir
|
|
elif os.path.isdir(os.path.join(dot_venv_dir, "bin")):
|
|
selected_venv = dot_venv_dir
|
|
|
|
if selected_venv:
|
|
python_exe = os.path.join(selected_venv, "bin", "python")
|
|
if os.path.isfile(python_exe):
|
|
VENV_PYTHON = python_exe
|
|
return
|
|
|
|
# No valid venv found — let's build it!
|
|
print("⚙️ Virtual environment not detected. Initializing setup...")
|
|
print(f" Creating virtual environment at: {venv_dir}")
|
|
try:
|
|
import venv
|
|
venv.create(venv_dir, with_pip=True)
|
|
print("✅ Virtual environment created.")
|
|
except Exception as e:
|
|
print(f"❌ Failed to create virtual environment via 'venv' module: {e}")
|
|
print(" Attempting subprocess fallback...")
|
|
try:
|
|
subprocess.run([sys.executable, "-m", "venv", venv_dir], check=True)
|
|
print("✅ Virtual environment created (fallback).")
|
|
except Exception as err:
|
|
print(f"❌ Subprocess fallback failed: {err}")
|
|
sys.exit(1)
|
|
|
|
VENV_PYTHON = os.path.join(venv_dir, "bin", "python")
|
|
pip_exe = os.path.join(venv_dir, "bin", "pip")
|
|
|
|
# Install dependencies
|
|
requirements_file = os.path.join(BASE_DIR, "requirements.txt")
|
|
if os.path.isfile(requirements_file):
|
|
print("📦 Installing Python dependencies from requirements.txt...")
|
|
try:
|
|
# Upgrade pip
|
|
subprocess.run([pip_exe, "install", "--upgrade", "pip", "--quiet"], check=True)
|
|
# Install requirements
|
|
subprocess.run([pip_exe, "install", "-r", requirements_file], check=True)
|
|
print("✅ All Python dependencies installed successfully.")
|
|
except Exception as e:
|
|
print(f"❌ Error installing dependencies: {e}")
|
|
sys.exit(1)
|
|
else:
|
|
print("⚠ requirements.txt not found. Skipping dependency installation.")
|
|
|
|
# Configure Real-ESRGAN binary permissions
|
|
binary_path = os.path.join(BASE_DIR, "realesrgan-bin", "realesrgan-ncnn-vulkan")
|
|
if os.path.isfile(binary_path):
|
|
try:
|
|
os.chmod(binary_path, 0o755)
|
|
print("✅ Real-ESRGAN binary marked as executable.")
|
|
except Exception as e:
|
|
print(f"⚠ Failed to change binary permissions: {e}")
|
|
|
|
# Create working directories
|
|
for d in ["uploads", "outputs", "temp"]:
|
|
os.makedirs(os.path.join(BASE_DIR, d), exist_ok=True)
|
|
print("✅ Working directories verified.")
|
|
|
|
|
|
def _is_running(pid: int) -> bool:
|
|
"""Check whether a process with the given PID is still alive and is our server."""
|
|
try:
|
|
os.kill(pid, 0)
|
|
except OSError:
|
|
return False
|
|
|
|
# Check /proc/{pid}/cmdline to verify it's our server process
|
|
try:
|
|
cmdline_path = f"/proc/{pid}/cmdline"
|
|
if os.path.exists(cmdline_path):
|
|
with open(cmdline_path, "r") as f:
|
|
cmdline = f.read()
|
|
# Check for indicators of our server
|
|
return "uvicorn" in cmdline or "app.main" in cmdline
|
|
except Exception:
|
|
pass
|
|
# If we can't read cmdline, default to True (safer fallback)
|
|
return True
|
|
|
|
|
|
def _read_pid() -> int | None:
|
|
"""Read and validate the PID from the pidfile."""
|
|
if not os.path.isfile(PID_FILE):
|
|
return None
|
|
try:
|
|
pid = int(open(PID_FILE).read().strip())
|
|
return pid if _is_running(pid) else None
|
|
except (ValueError, OSError):
|
|
return None
|
|
|
|
|
|
def _get_local_ip() -> str:
|
|
"""Get the primary local network IP address."""
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
try:
|
|
# Dummy connection to google DNS to get local route IP
|
|
s.connect(("8.8.8.8", 80))
|
|
ip = s.getsockname()[0]
|
|
except Exception:
|
|
try:
|
|
ip = socket.gethostbyname(socket.gethostname())
|
|
except Exception:
|
|
ip = "127.0.0.1"
|
|
finally:
|
|
s.close()
|
|
return ip
|
|
|
|
|
|
def start(host: str, port: int, open_browser: bool = True) -> None:
|
|
existing = _read_pid()
|
|
if existing:
|
|
print(f"⚠ Server is already running (PID {existing}).")
|
|
if host == "0.0.0.0":
|
|
local_ip = _get_local_ip()
|
|
print(f" → Local: http://localhost:{port}")
|
|
if local_ip and local_ip != "127.0.0.1":
|
|
print(f" → Network: http://{local_ip}:{port}")
|
|
else:
|
|
print(f" → http://{host}:{port}")
|
|
return
|
|
|
|
print(f"🚀 Starting AI Video Upscaler …")
|
|
print(f" Host : {host}")
|
|
print(f" Port : {port}")
|
|
print(f" Log : {LOG_FILE}")
|
|
|
|
log_fh = open(LOG_FILE, "a")
|
|
|
|
proc = subprocess.Popen(
|
|
[
|
|
VENV_PYTHON, "-m", "uvicorn",
|
|
"app.main:app",
|
|
"--host", host,
|
|
"--port", str(port),
|
|
],
|
|
cwd=BASE_DIR,
|
|
stdout=log_fh,
|
|
stderr=log_fh,
|
|
start_new_session=True, # detach from terminal
|
|
)
|
|
|
|
# Write PID
|
|
with open(PID_FILE, "w") as f:
|
|
f.write(str(proc.pid))
|
|
|
|
# Wait briefly to make sure it didn't crash immediately
|
|
time.sleep(1.5)
|
|
if proc.poll() is not None:
|
|
print("❌ Server failed to start. Check server.log for details.")
|
|
os.remove(PID_FILE)
|
|
sys.exit(1)
|
|
|
|
url = f"http://{'localhost' if host == '0.0.0.0' else host}:{port}"
|
|
print(f"✅ Server started (PID {proc.pid})")
|
|
if host == "0.0.0.0":
|
|
local_ip = _get_local_ip()
|
|
print(f" → Local: http://localhost:{port}")
|
|
if local_ip and local_ip != "127.0.0.1":
|
|
print(f" → Network: http://{local_ip}:{port}")
|
|
else:
|
|
print(f" → {url}")
|
|
|
|
if open_browser:
|
|
webbrowser.open(url)
|
|
|
|
|
|
# ── CLI ──────────────────────────────────────────────────────────────────────
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Start the AI Video Upscaler")
|
|
parser.add_argument("--host", default="0.0.0.0",
|
|
help="Bind address (default: 0.0.0.0 to bind all interfaces, use 127.0.0.1 for local loopback only)")
|
|
parser.add_argument("--port", type=int, default=8000,
|
|
help="Port to listen on (default: 8000)")
|
|
parser.add_argument("--no-browser", action="store_true",
|
|
help="Don't open the browser automatically")
|
|
args = parser.parse_args()
|
|
|
|
ensure_venv()
|
|
start(args.host, args.port, open_browser=not args.no_browser)
|