AdarshJi commited on
Commit
ade7305
Β·
verified Β·
1 Parent(s): 569d399

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +111 -43
server.py CHANGED
@@ -1,21 +1,23 @@
1
- # perchance_server.py
2
  """
3
  Perchance Image-Generation Server v2.0
4
 
5
- Changes from v1:
6
- β€’ Auto-detects 'invalid_key' and re-fetches userKey via zendriver
7
- without manual intervention.
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
- # IMPORTS
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
- def _run_uvicorn():
1241
  import uvicorn
1242
- uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
1243
-
1244
 
1245
- if __name__ == "__main__":
1246
- skip = os.environ.get("NO_INITIAL_FETCH", "") in ("1", "true", "True")
1247
 
1248
- if not skip:
1249
- log.info("Pre-startup: fetching userKey via browser (timeout %ds) …", ZD_TIMEOUT)
1250
- try:
1251
- found = asyncio.run(
1252
- fetch_key_via_browser(timeout=ZD_TIMEOUT, headless=ZD_HEADLESS)
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
- _run_uvicorn()
 
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)))