Update server.py
Browse files
server.py
CHANGED
|
@@ -1,21 +1,23 @@
|
|
| 1 |
-
#
|
| 2 |
"""
|
| 3 |
Perchance Image-Generation Server v2.0
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
β’ Only ONE browser launch at startup (no duplicate fetch).
|
| 9 |
-
β’ Uses FastAPI *lifespan* context-manager β zero deprecation warnings.
|
| 10 |
-
β’ Coordinated key refresh: one refresh at a time; other workers wait.
|
| 11 |
-
β’ SSE events: key_refreshing / key_refreshed / key_refresh_failed
|
| 12 |
-
so the client knows exactly what's happening.
|
| 13 |
-
β’ Cleaner separation of concerns, better error handling.
|
| 14 |
-
"""
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
import asyncio
|
| 21 |
import base64
|
|
@@ -41,6 +43,13 @@ from sse_starlette.sse import EventSourceResponse
|
|
| 41 |
import zendriver as zd
|
| 42 |
from zendriver import cdp
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
# CONFIGURATION
|
|
@@ -92,9 +101,6 @@ log = logging.getLogger("perchance")
|
|
| 92 |
|
| 93 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
# GLOBAL STATE
|
| 95 |
-
#
|
| 96 |
-
# Asyncio primitives (Lock, Event, Queue) are created inside
|
| 97 |
-
# the lifespan handler so they live in uvicorn's event loop.
|
| 98 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 99 |
|
| 100 |
USER_KEY: Optional[str] = None
|
|
@@ -114,6 +120,9 @@ TASK_QUEUES: Dict[str, asyncio.Queue] = {} # SSE event queues
|
|
| 114 |
EXECUTOR = ThreadPoolExecutor(max_workers=EXECUTOR_THREADS)
|
| 115 |
SCRAPER = cloudscraper.create_scraper()
|
| 116 |
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 119 |
# SMALL HELPERS
|
|
@@ -141,6 +150,55 @@ def _stamp() -> str:
|
|
| 141 |
return datetime.utcnow().strftime("%Y%m%dT%H%M%S")
|
| 142 |
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 145 |
# PERCHANCE HTTP CLIENT (blocking β runs in ThreadPoolExecutor)
|
| 146 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -464,6 +522,15 @@ async def fetch_key_via_browser(
|
|
| 464 |
timeout, headless,
|
| 465 |
)
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
try:
|
| 468 |
browser = await zd.start(headless=headless)
|
| 469 |
except Exception as exc:
|
|
@@ -967,6 +1034,15 @@ async def lifespan(app: FastAPI):
|
|
| 967 |
_key_refresh_lock = asyncio.Lock()
|
| 968 |
JOB_QUEUE = asyncio.Queue(maxsize=MAX_QUEUE_SIZE)
|
| 969 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
# ββ initial key fetch (skip if already set from __main__) ββ
|
| 971 |
if USER_KEY:
|
| 972 |
log.info("Using pre-fetched userKey (len=%d)", len(USER_KEY))
|
|
@@ -1014,12 +1090,19 @@ async def lifespan(app: FastAPI):
|
|
| 1014 |
except Exception:
|
| 1015 |
pass
|
| 1016 |
EXECUTOR.shutdown(wait=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
log.info("Shutdown complete")
|
| 1018 |
|
| 1019 |
|
| 1020 |
# ββ app ββ
|
| 1021 |
app = FastAPI(
|
| 1022 |
-
title="Perchance Image Generation Server v2",
|
| 1023 |
lifespan=lifespan,
|
| 1024 |
)
|
| 1025 |
app.add_middleware(
|
|
@@ -1234,34 +1317,19 @@ async def get_output(filename: str):
|
|
| 1234 |
|
| 1235 |
|
| 1236 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1237 |
-
# MAIN
|
| 1238 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1239 |
|
| 1240 |
-
|
| 1241 |
import uvicorn
|
| 1242 |
-
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
| 1243 |
-
|
| 1244 |
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
)
|
| 1254 |
-
if found:
|
| 1255 |
-
USER_KEY = found
|
| 1256 |
-
log.info("Pre-startup fetch OK (len=%d)", len(found))
|
| 1257 |
-
else:
|
| 1258 |
-
log.warning(
|
| 1259 |
-
"Pre-startup fetch returned nothing. "
|
| 1260 |
-
"Server will start; set key via /set_user_key."
|
| 1261 |
-
)
|
| 1262 |
-
except Exception as exc:
|
| 1263 |
-
log.exception("Pre-startup fetch error: %s", exc)
|
| 1264 |
-
else:
|
| 1265 |
-
log.info("NO_INITIAL_FETCH=1 β skipping pre-startup browser fetch")
|
| 1266 |
|
| 1267 |
-
|
|
|
|
| 1 |
+
# perchance_server_with_pyvirtualdisplay.py
|
| 2 |
"""
|
| 3 |
Perchance Image-Generation Server v2.0
|
| 4 |
|
| 5 |
+
This variant adds optional pyvirtualdisplay support so the server can be
|
| 6 |
+
hosted on headless environments (Hugging Face Spaces etc.) while keeping
|
| 7 |
+
all original behaviour unchanged.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
Behaviour:
|
| 10 |
+
- If ZD_HEADLESS is True, zendriver will run headless as before.
|
| 11 |
+
- If ZD_HEADLESS is False and a DISPLAY is not present, we attempt to
|
| 12 |
+
start a pyvirtualdisplay.Display (Xvfb) automatically before launching
|
| 13 |
+
browsers. If pyvirtualdisplay is not installed or starting Xvfb fails,
|
| 14 |
+
we log a warning and continue.
|
| 15 |
+
|
| 16 |
+
To run on Hugging Face Spaces, add `pyvirtualdisplay` to requirements.txt
|
| 17 |
+
and ensure `xvfb` is available in the runtime (HF Spaces typically provide it).
|
| 18 |
+
|
| 19 |
+
All original defaults and constants are preserved from the original file.
|
| 20 |
+
"""
|
| 21 |
|
| 22 |
import asyncio
|
| 23 |
import base64
|
|
|
|
| 43 |
import zendriver as zd
|
| 44 |
from zendriver import cdp
|
| 45 |
|
| 46 |
+
# Try to import pyvirtualdisplay (optional)
|
| 47 |
+
try:
|
| 48 |
+
from pyvirtualdisplay import Display
|
| 49 |
+
_HAS_PYVIRTUALDISPLAY = True
|
| 50 |
+
except Exception:
|
| 51 |
+
Display = None
|
| 52 |
+
_HAS_PYVIRTUALDISPLAY = False
|
| 53 |
|
| 54 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
# CONFIGURATION
|
|
|
|
| 101 |
|
| 102 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 103 |
# GLOBAL STATE
|
|
|
|
|
|
|
|
|
|
| 104 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 105 |
|
| 106 |
USER_KEY: Optional[str] = None
|
|
|
|
| 120 |
EXECUTOR = ThreadPoolExecutor(max_workers=EXECUTOR_THREADS)
|
| 121 |
SCRAPER = cloudscraper.create_scraper()
|
| 122 |
|
| 123 |
+
# pyvirtualdisplay handle (optional)
|
| 124 |
+
VDISPLAY: Optional[Display] = None
|
| 125 |
+
|
| 126 |
|
| 127 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 128 |
# SMALL HELPERS
|
|
|
|
| 150 |
return datetime.utcnow().strftime("%Y%m%dT%H%M%S")
|
| 151 |
|
| 152 |
|
| 153 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 154 |
+
# Virtual display helpers (pyvirtualdisplay)
|
| 155 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 156 |
+
|
| 157 |
+
def _start_virtual_display_if_needed(headless: bool):
|
| 158 |
+
"""
|
| 159 |
+
Start pyvirtualdisplay.Display() if we're running non-headless in an
|
| 160 |
+
environment without DISPLAY.
|
| 161 |
+
This function is synchronous and safe to be run in a thread executor.
|
| 162 |
+
"""
|
| 163 |
+
global VDISPLAY
|
| 164 |
+
|
| 165 |
+
if headless:
|
| 166 |
+
log.info("ZD_HEADLESS=True β not starting virtual display")
|
| 167 |
+
return
|
| 168 |
+
|
| 169 |
+
if os.environ.get("DISPLAY"):
|
| 170 |
+
log.info("DISPLAY already set: %s", os.environ.get("DISPLAY"))
|
| 171 |
+
return
|
| 172 |
+
|
| 173 |
+
if not _HAS_PYVIRTUALDISPLAY or Display is None:
|
| 174 |
+
log.warning(
|
| 175 |
+
"pyvirtualdisplay not installed β cannot create virtual DISPLAY. "
|
| 176 |
+
"Install pyvirtualdisplay in your environment to enable Xvfb.")
|
| 177 |
+
return
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
VDISPLAY = Display(visible=0, size=(1280, 720))
|
| 181 |
+
VDISPLAY.start()
|
| 182 |
+
# pyvirtualdisplay sets DISPLAY env itself; log for visibility
|
| 183 |
+
log.info("Started virtual display via pyvirtualdisplay (DISPLAY=%s)", os.environ.get("DISPLAY"))
|
| 184 |
+
except Exception as exc:
|
| 185 |
+
VDISPLAY = None
|
| 186 |
+
log.exception("Failed to start virtual display: %s", exc)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _stop_virtual_display_if_needed():
|
| 190 |
+
global VDISPLAY
|
| 191 |
+
if VDISPLAY is None:
|
| 192 |
+
return
|
| 193 |
+
try:
|
| 194 |
+
VDISPLAY.stop()
|
| 195 |
+
log.info("Stopped virtual display")
|
| 196 |
+
except Exception:
|
| 197 |
+
log.exception("Error while stopping virtual display")
|
| 198 |
+
finally:
|
| 199 |
+
VDISPLAY = None
|
| 200 |
+
|
| 201 |
+
|
| 202 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 203 |
# PERCHANCE HTTP CLIENT (blocking β runs in ThreadPoolExecutor)
|
| 204 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 522 |
timeout, headless,
|
| 523 |
)
|
| 524 |
|
| 525 |
+
# If we're in non-headless mode on a display-less host, ensure a virtual
|
| 526 |
+
# DISPLAY is started first. This call is synchronous so we run it in the
|
| 527 |
+
# event loop's default executor when called from async code.
|
| 528 |
+
loop = asyncio.get_running_loop()
|
| 529 |
+
try:
|
| 530 |
+
await loop.run_in_executor(None, partial(_start_virtual_display_if_needed, headless))
|
| 531 |
+
except Exception:
|
| 532 |
+
log.exception("Error while attempting to start virtual display")
|
| 533 |
+
|
| 534 |
try:
|
| 535 |
browser = await zd.start(headless=headless)
|
| 536 |
except Exception as exc:
|
|
|
|
| 1034 |
_key_refresh_lock = asyncio.Lock()
|
| 1035 |
JOB_QUEUE = asyncio.Queue(maxsize=MAX_QUEUE_SIZE)
|
| 1036 |
|
| 1037 |
+
# If running non-headless and no DISPLAY is present, attempt to start
|
| 1038 |
+
# a virtual display (pyvirtualdisplay/Xvfb). This runs in a thread
|
| 1039 |
+
# executor because pyvirtualdisplay is synchronous.
|
| 1040 |
+
loop = asyncio.get_running_loop()
|
| 1041 |
+
try:
|
| 1042 |
+
await loop.run_in_executor(None, partial(_start_virtual_display_if_needed, ZD_HEADLESS))
|
| 1043 |
+
except Exception:
|
| 1044 |
+
log.exception("Failed to ensure virtual display at startup")
|
| 1045 |
+
|
| 1046 |
# ββ initial key fetch (skip if already set from __main__) ββ
|
| 1047 |
if USER_KEY:
|
| 1048 |
log.info("Using pre-fetched userKey (len=%d)", len(USER_KEY))
|
|
|
|
| 1090 |
except Exception:
|
| 1091 |
pass
|
| 1092 |
EXECUTOR.shutdown(wait=True)
|
| 1093 |
+
|
| 1094 |
+
# Stop virtual display if we started one
|
| 1095 |
+
try:
|
| 1096 |
+
await loop.run_in_executor(None, _stop_virtual_display_if_needed)
|
| 1097 |
+
except Exception:
|
| 1098 |
+
log.exception("Failed to stop virtual display cleanly")
|
| 1099 |
+
|
| 1100 |
log.info("Shutdown complete")
|
| 1101 |
|
| 1102 |
|
| 1103 |
# ββ app ββ
|
| 1104 |
app = FastAPI(
|
| 1105 |
+
title="Perchance Image Generation Server v2 (pyvirtualdisplay)",
|
| 1106 |
lifespan=lifespan,
|
| 1107 |
)
|
| 1108 |
app.add_middleware(
|
|
|
|
| 1317 |
|
| 1318 |
|
| 1319 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1320 |
+
# MAIN (when run directly)
|
| 1321 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1322 |
|
| 1323 |
+
if __name__ == "__main__":
|
| 1324 |
import uvicorn
|
|
|
|
|
|
|
| 1325 |
|
| 1326 |
+
# Allow overriding via environment variables
|
| 1327 |
+
ZD_HEADLESS = os.environ.get("ZD_HEADLESS", str(ZD_HEADLESS)) in ("1", "true", "True")
|
| 1328 |
|
| 1329 |
+
# If running locally and not headless, start virtual display if needed
|
| 1330 |
+
try:
|
| 1331 |
+
_start_virtual_display_if_needed(ZD_HEADLESS)
|
| 1332 |
+
except Exception:
|
| 1333 |
+
log.exception("Failed to ensure virtual display in __main__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
|
| 1335 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
|