Adarshu07 commited on
Commit
c18b868
Β·
verified Β·
1 Parent(s): 3a20686

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +100 -38
server.py CHANGED
@@ -6,8 +6,8 @@ Perchance Image Generation API v3.0
6
  Lightweight, self-healing, scalable image generation server.
7
  Handles thousands of concurrent users on Hugging Face Spaces.
8
 
9
- Main Endpoint: POST /v1/generation/Image/Create
10
- SSE Stream: GET /v1/generation/stream/{task_id}
11
  """
12
 
13
  # ═══════════════════════════════════════════════════════════════
@@ -263,10 +263,6 @@ class Broadcaster:
263
  except asyncio.QueueFull:
264
  pass
265
 
266
- async def drop_channel(self, task_id: str) -> None:
267
- async with self._lock:
268
- self._subs.pop(task_id, None)
269
-
270
  @property
271
  def total_subscribers(self) -> int:
272
  return sum(len(v) for v in self._subs.values())
@@ -347,6 +343,18 @@ class KeyManager:
347
  self._refresh_gate = asyncio.Lock()
348
  self._last_ok: float = 0.0
349
  self._consec_fails: int = 0
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
  async def get(self) -> Optional[str]:
352
  await self._usable.wait()
@@ -359,6 +367,7 @@ class KeyManager:
359
  self._last_ok = time.time()
360
  self._consec_fails = 0
361
  self._usable.set()
 
362
  log.info("userKey set OK (len=%d)", len(new_key))
363
 
364
  def reset_failures(self) -> None:
@@ -448,7 +457,8 @@ class PerchanceClient:
448
  except Exception:
449
  pass
450
 
451
- def generate(self, *, prompt, negative, seed, resolution, guidance, style, user_key, ad_code) -> dict:
 
452
  rid = _rid()
453
  params = {
454
  "userKey": user_key, "requestId": rid,
@@ -722,6 +732,9 @@ _tasks = TaskStore()
722
  _sse = Broadcaster()
723
  _queue: Optional[asyncio.Queue] = None
724
 
 
 
 
725
 
726
  # ═══════════════════════════════════════════════════════════════
727
  # GENERATION ENGINE
@@ -842,6 +855,26 @@ async def _run_task(task: dict) -> None:
842
  loop = asyncio.get_running_loop()
843
  tid = task["id"]
844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
  task["status"] = "running"
846
  task["started_at"] = _now()
847
 
@@ -935,52 +968,88 @@ async def _janitor():
935
  log.exception("Janitor error")
936
 
937
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  # ═══════════════════════════════════════════════════════════════
939
  # FASTAPI β€” LIFESPAN
 
 
 
940
  # ═══════════════════════════════════════════════��═══════════════
941
 
942
  @asynccontextmanager
943
  async def lifespan(_app: FastAPI):
944
- global _queue
945
 
 
946
  _queue = asyncio.Queue(maxsize=QUEUE_CAPACITY)
947
  loop = asyncio.get_running_loop()
948
 
 
949
  await loop.run_in_executor(None, partial(_display.ensure, BROWSER_HEADLESS))
950
 
951
- skip = os.environ.get("NO_INITIAL_FETCH", "0") in ("1", "true", "True")
952
- if not skip:
953
- try:
954
- key = await extract_key_via_browser(BROWSER_TIMEOUT, BROWSER_HEADLESS)
955
- if key:
956
- await _keys.set(key)
957
- log.info("Startup key OK")
958
- else:
959
- log.warning("Startup key failed β€” use /v1/keys/set or /v1/keys/refresh")
960
- except Exception as exc:
961
- log.exception("Startup key error: %s", exc)
962
- else:
963
- log.info("NO_INITIAL_FETCH β†’ skipping browser key fetch")
964
-
965
  workers = [asyncio.create_task(_worker(i + 1)) for i in range(WORKER_COUNT)]
 
 
966
  janitor = asyncio.create_task(_janitor())
967
 
 
 
 
968
  log.info(
969
- "═══ SERVER READY ═══ workers=%d queue_cap=%d headless=%s",
970
  WORKER_COUNT, QUEUE_CAPACITY, BROWSER_HEADLESS,
971
  )
972
 
 
 
 
 
 
973
  yield
974
 
 
975
  log.info("Shutting down …")
 
 
 
 
 
 
 
 
 
976
  for _ in range(WORKER_COUNT):
977
  await _queue.put(None)
978
  await asyncio.gather(*workers, return_exceptions=True)
 
979
  janitor.cancel()
980
  try:
981
  await janitor
982
  except asyncio.CancelledError:
983
  pass
 
984
  _client.close()
985
  _executor.shutdown(wait=False)
986
  await loop.run_in_executor(None, _display.shutdown)
@@ -1001,21 +1070,18 @@ async def _global_err(request: Request, exc: Exception):
1001
  return JSONResponse(status_code=500, content={"error": "Internal server error"})
1002
 
1003
 
1004
- # ══════════════════════════════════════════════
1005
- # ROOT ← THIS IS THE FIX FOR "STUCK STARTING"
1006
- # ══════════════════════════════════════════════
1007
 
1008
  @app.get("/")
1009
  async def root():
1010
- """
1011
- Root endpoint β€” required by Hugging Face Spaces.
1012
- HF checks GET / to confirm the app is alive.
1013
- Without this, the Space shows 'Starting...' forever.
1014
- """
1015
  return {
1016
  "name": "Perchance Image Generation API",
1017
  "version": "3.0",
1018
  "status": "running",
 
 
1019
  "docs": "/docs",
1020
  "endpoints": {
1021
  "generate": "POST /v1/generation/Image/Create",
@@ -1026,7 +1092,6 @@ async def root():
1026
  "set_key": "POST /v1/keys/set",
1027
  "refresh_key": "POST /v1/keys/refresh",
1028
  },
1029
- "time": _now(),
1030
  }
1031
 
1032
 
@@ -1035,11 +1100,13 @@ async def health():
1035
  return {
1036
  "status": "healthy",
1037
  "has_key": _keys.key is not None,
 
1038
  "queue_size": _queue.qsize() if _queue else 0,
1039
  "queue_capacity": QUEUE_CAPACITY,
1040
  "active_tasks": _tasks.active_count(),
1041
  "stored_tasks": _tasks.size,
1042
  "sse_subscribers": _sse.total_subscribers,
 
1043
  "time": _now(),
1044
  }
1045
 
@@ -1128,7 +1195,6 @@ async def stream_task(request: Request, task_id: str):
1128
  "data": json.dumps({"time": _now()}),
1129
  }
1130
  if await request.is_disconnected():
1131
- log.info("SSE client disconnected task=%s", task_id[:8])
1132
  break
1133
  if task["status"] in ("completed", "failed"):
1134
  yield {
@@ -1143,13 +1209,9 @@ async def stream_task(request: Request, task_id: str):
1143
  continue
1144
 
1145
  etype = ev.get("event", "message")
1146
- yield {
1147
- "event": etype,
1148
- "data": json.dumps(ev),
1149
- }
1150
  if etype in ("completed", "failed"):
1151
  break
1152
-
1153
  finally:
1154
  await _sse.unsubscribe(task_id, sub_q)
1155
 
 
6
  Lightweight, self-healing, scalable image generation server.
7
  Handles thousands of concurrent users on Hugging Face Spaces.
8
 
9
+ Key fix: Server starts accepting connections IMMEDIATELY.
10
+ Browser key fetch runs in BACKGROUND after startup.
11
  """
12
 
13
  # ═══════════════════════════════════════════════════════════════
 
263
  except asyncio.QueueFull:
264
  pass
265
 
 
 
 
 
266
  @property
267
  def total_subscribers(self) -> int:
268
  return sum(len(v) for v in self._subs.values())
 
343
  self._refresh_gate = asyncio.Lock()
344
  self._last_ok: float = 0.0
345
  self._consec_fails: int = 0
346
+ self._ready = asyncio.Event() # set once first key obtained
347
+
348
+ @property
349
+ def is_ready(self) -> bool:
350
+ return self._ready.is_set()
351
+
352
+ async def wait_ready(self, timeout: float = None):
353
+ """Wait until at least one key has been obtained."""
354
+ if timeout:
355
+ await asyncio.wait_for(self._ready.wait(), timeout=timeout)
356
+ else:
357
+ await self._ready.wait()
358
 
359
  async def get(self) -> Optional[str]:
360
  await self._usable.wait()
 
367
  self._last_ok = time.time()
368
  self._consec_fails = 0
369
  self._usable.set()
370
+ self._ready.set()
371
  log.info("userKey set OK (len=%d)", len(new_key))
372
 
373
  def reset_failures(self) -> None:
 
457
  except Exception:
458
  pass
459
 
460
+ def generate(self, *, prompt, negative, seed, resolution,
461
+ guidance, style, user_key, ad_code) -> dict:
462
  rid = _rid()
463
  params = {
464
  "userKey": user_key, "requestId": rid,
 
732
  _sse = Broadcaster()
733
  _queue: Optional[asyncio.Queue] = None
734
 
735
+ # Track startup readiness (server is up but key may still be loading)
736
+ _server_start_time: float = 0.0
737
+
738
 
739
  # ═══════════════════════════════════════════════════════════════
740
  # GENERATION ENGINE
 
855
  loop = asyncio.get_running_loop()
856
  tid = task["id"]
857
 
858
+ # Wait for key to be available before starting
859
+ if not _keys.is_ready:
860
+ log.info("Task %s waiting for initial key …", tid[:8])
861
+ await _sse.emit(tid, {
862
+ "event": "waiting", "task_id": tid,
863
+ "message": "Waiting for server key initialization …",
864
+ "time": _now(),
865
+ })
866
+ try:
867
+ await _keys.wait_ready(timeout=120)
868
+ except asyncio.TimeoutError:
869
+ task["status"] = "failed"
870
+ task["error"] = "Key initialization timeout"
871
+ task["finished_at"] = _now()
872
+ await _sse.emit(tid, {
873
+ "event": "failed", "task_id": tid,
874
+ "error": "Key initialization timeout", "time": _now(),
875
+ })
876
+ return
877
+
878
  task["status"] = "running"
879
  task["started_at"] = _now()
880
 
 
968
  log.exception("Janitor error")
969
 
970
 
971
+ async def _background_key_fetch():
972
+ """
973
+ Fetch userKey via browser IN THE BACKGROUND.
974
+ Server is already accepting connections while this runs.
975
+ """
976
+ skip = os.environ.get("NO_INITIAL_FETCH", "0") in ("1", "true", "True")
977
+ if skip:
978
+ log.info("NO_INITIAL_FETCH β†’ skipping browser key fetch")
979
+ return
980
+
981
+ log.info("Background key fetch starting …")
982
+ try:
983
+ key = await extract_key_via_browser(BROWSER_TIMEOUT, BROWSER_HEADLESS)
984
+ if key:
985
+ await _keys.set(key)
986
+ log.info("Background key fetch OK")
987
+ else:
988
+ log.warning("Background key fetch failed β€” use /v1/keys/set or /v1/keys/refresh")
989
+ except Exception as exc:
990
+ log.exception("Background key fetch error: %s", exc)
991
+
992
+
993
  # ═══════════════════════════════════════════════════════════════
994
  # FASTAPI β€” LIFESPAN
995
+ #
996
+ # KEY CHANGE: yield IMMEDIATELY so HF Spaces sees port 7860
997
+ # key fetch runs as a background task AFTER yield
998
  # ═══════════════════════════════════════════════��═══════════════
999
 
1000
  @asynccontextmanager
1001
  async def lifespan(_app: FastAPI):
1002
+ global _queue, _server_start_time
1003
 
1004
+ _server_start_time = time.time()
1005
  _queue = asyncio.Queue(maxsize=QUEUE_CAPACITY)
1006
  loop = asyncio.get_running_loop()
1007
 
1008
+ # Start Xvfb (fast, synchronous)
1009
  await loop.run_in_executor(None, partial(_display.ensure, BROWSER_HEADLESS))
1010
 
1011
+ # Start workers (instant)
 
 
 
 
 
 
 
 
 
 
 
 
 
1012
  workers = [asyncio.create_task(_worker(i + 1)) for i in range(WORKER_COUNT)]
1013
+
1014
+ # Start janitor (instant)
1015
  janitor = asyncio.create_task(_janitor())
1016
 
1017
+ # Start key fetch IN BACKGROUND (non-blocking!)
1018
+ key_task = asyncio.create_task(_background_key_fetch())
1019
+
1020
  log.info(
1021
+ "═══ SERVER READY ═══ workers=%d queue_cap=%d headless=%s (key fetching in background)",
1022
  WORKER_COUNT, QUEUE_CAPACITY, BROWSER_HEADLESS,
1023
  )
1024
 
1025
+ # ════════════════════════════════════════
1026
+ # yield IMMEDIATELY β€” port 7860 opens NOW
1027
+ # HF Spaces sees 200 OK on GET /
1028
+ # No more "stuck on Starting"!
1029
+ # ════════════════════════════════════════
1030
  yield
1031
 
1032
+ # ──────── shutdown ────────
1033
  log.info("Shutting down …")
1034
+
1035
+ # Cancel background key fetch if still running
1036
+ if not key_task.done():
1037
+ key_task.cancel()
1038
+ try:
1039
+ await key_task
1040
+ except (asyncio.CancelledError, Exception):
1041
+ pass
1042
+
1043
  for _ in range(WORKER_COUNT):
1044
  await _queue.put(None)
1045
  await asyncio.gather(*workers, return_exceptions=True)
1046
+
1047
  janitor.cancel()
1048
  try:
1049
  await janitor
1050
  except asyncio.CancelledError:
1051
  pass
1052
+
1053
  _client.close()
1054
  _executor.shutdown(wait=False)
1055
  await loop.run_in_executor(None, _display.shutdown)
 
1070
  return JSONResponse(status_code=500, content={"error": "Internal server error"})
1071
 
1072
 
1073
+ # ══════════════════════════════════════
1074
+ # ROOT β€” HF Spaces readiness check
1075
+ # ══════════════════════════════════════
1076
 
1077
  @app.get("/")
1078
  async def root():
 
 
 
 
 
1079
  return {
1080
  "name": "Perchance Image Generation API",
1081
  "version": "3.0",
1082
  "status": "running",
1083
+ "key_ready": _keys.is_ready,
1084
+ "uptime": round(time.time() - _server_start_time, 1),
1085
  "docs": "/docs",
1086
  "endpoints": {
1087
  "generate": "POST /v1/generation/Image/Create",
 
1092
  "set_key": "POST /v1/keys/set",
1093
  "refresh_key": "POST /v1/keys/refresh",
1094
  },
 
1095
  }
1096
 
1097
 
 
1100
  return {
1101
  "status": "healthy",
1102
  "has_key": _keys.key is not None,
1103
+ "key_ready": _keys.is_ready,
1104
  "queue_size": _queue.qsize() if _queue else 0,
1105
  "queue_capacity": QUEUE_CAPACITY,
1106
  "active_tasks": _tasks.active_count(),
1107
  "stored_tasks": _tasks.size,
1108
  "sse_subscribers": _sse.total_subscribers,
1109
+ "uptime": round(time.time() - _server_start_time, 1),
1110
  "time": _now(),
1111
  }
1112
 
 
1195
  "data": json.dumps({"time": _now()}),
1196
  }
1197
  if await request.is_disconnected():
 
1198
  break
1199
  if task["status"] in ("completed", "failed"):
1200
  yield {
 
1209
  continue
1210
 
1211
  etype = ev.get("event", "message")
1212
+ yield {"event": etype, "data": json.dumps(ev)}
 
 
 
1213
  if etype in ("completed", "failed"):
1214
  break
 
1215
  finally:
1216
  await _sse.unsubscribe(task_id, sub_q)
1217