DevEmmy commited on
Commit
85d2c5b
Β·
1 Parent(s): 74d52f2

Let Gradio bind the port and graft the REST routes onto it

Browse files

Our own uvicorn raced the runner for 7860; a bare ASGI app with no listener
left nothing serving. Gradio launches instead, and /asr, /translate and
/health are prepended to its router so its frontend catch-all cannot shadow
them. Health moves off / because Gradio owns / for the UI.

Files changed (2) hide show
  1. README.md +1 -1
  2. app.py +29 -16
README.md CHANGED
@@ -34,7 +34,7 @@ have to hold ~2GB of model weights in a 512MB process.
34
  - `HF_TOKEN` β€” a read token from the account that accepted the licence
35
  - `SERVICE_TOKEN` β€” any random string; the backend must send the same value
36
 
37
- The first request downloads weights and can take several minutes. `GET /`
38
  answers immediately throughout and reports load state, so you can watch it come
39
  up without holding a request open.
40
 
 
34
  - `HF_TOKEN` β€” a read token from the account that accepted the licence
35
  - `SERVICE_TOKEN` β€” any random string; the backend must send the same value
36
 
37
+ Gradio serves its status page at `/`. The first request downloads weights and can take several minutes. `GET /`
38
  answers immediately throughout and reports load state, so you can watch it come
39
  up without holding a request open.
40
 
app.py CHANGED
@@ -88,7 +88,7 @@ def _auth(token):
88
  raise HTTPException(status_code=401, detail="bad service token")
89
 
90
 
91
- @app.get("/")
92
  def health():
93
  return {
94
  "status": "ok",
@@ -225,14 +225,14 @@ def translate(body: TranslateIn, x_service_token: str = Header(default=None)):
225
 
226
 
227
  # ------------------------------------------------------------------- LAUNCH ---
228
- # The Space runner serves the module-level `app` itself. Binding a port here as
229
- # well is what produced "address already in use" and killed the container: two
230
- # servers competing for 7860. So this module defines the ASGI app and never
231
- # listens.
232
  #
233
- # The Blocks UI is grafted onto that same app at import time, which keeps /asr
234
- # and /translate at the root where the backend expects them, with a status page
235
- # at /ui.
236
  def _ui_check():
237
  import json as _json
238
  return _json.dumps(health(), indent=2)
@@ -242,9 +242,10 @@ _UI_TEXT = (
242
  "## ScriptFlow inference service\n\n"
243
  "Hausa ASR + Hausa->English MT. The REST API is the real interface:\n\n"
244
  "- `POST /asr` β€” raw audio bytes as the body\n"
245
- "- `POST /translate` β€” `{\"texts\": [...]}`\n\n"
246
- "Both take an `X-Service-Token` header. The first call after a cold start "
247
- "downloads ~1GB of weights and takes several minutes."
 
248
  )
249
 
250
 
@@ -252,13 +253,25 @@ def _build_ui():
252
  import gradio as gr
253
  with gr.Blocks(title="ScriptFlow Hausa inference") as demo:
254
  gr.Markdown(_UI_TEXT)
255
- out = gr.Code(label="GET /", language="json")
256
  gr.Button("Check status").click(_ui_check, inputs=None, outputs=out)
257
  return demo
258
 
259
 
260
- try:
261
  import gradio as gr
262
- app = gr.mount_gradio_app(app, _build_ui(), path="/ui")
263
- except Exception as _e: # never let the UI break the API
264
- print(f"gradio UI unavailable ({type(_e).__name__}: {_e}) β€” REST only", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  raise HTTPException(status_code=401, detail="bad service token")
89
 
90
 
91
+ @app.get("/health")
92
  def health():
93
  return {
94
  "status": "ok",
 
225
 
226
 
227
  # ------------------------------------------------------------------- LAUNCH ---
228
+ # A Gradio-SDK Space expects Gradio itself to bind the port. Two earlier shapes
229
+ # both failed here: running our own uvicorn raced whatever the runner had
230
+ # already put on 7860 ("address already in use"), and defining a bare ASGI app
231
+ # with no listener left the container with nothing serving at all.
232
  #
233
+ # So Gradio launches, and the REST routes are grafted onto the server it starts.
234
+ # They are *prepended*, because Gradio registers catch-all routes for its own
235
+ # frontend that would otherwise shadow /asr and /translate.
236
  def _ui_check():
237
  import json as _json
238
  return _json.dumps(health(), indent=2)
 
242
  "## ScriptFlow inference service\n\n"
243
  "Hausa ASR + Hausa->English MT. The REST API is the real interface:\n\n"
244
  "- `POST /asr` β€” raw audio bytes as the body\n"
245
+ "- `POST /translate` β€” `{\"texts\": [...]}`\n"
246
+ "- `GET /health` β€” model load state\n\n"
247
+ "Both POSTs take an `X-Service-Token` header. The first call after a cold "
248
+ "start downloads ~1GB of weights and takes several minutes."
249
  )
250
 
251
 
 
253
  import gradio as gr
254
  with gr.Blocks(title="ScriptFlow Hausa inference") as demo:
255
  gr.Markdown(_UI_TEXT)
256
+ out = gr.Code(label="GET /health", language="json")
257
  gr.Button("Check status").click(_ui_check, inputs=None, outputs=out)
258
  return demo
259
 
260
 
261
+ if __name__ == "__main__":
262
  import gradio as gr
263
+
264
+ demo = _build_ui()
265
+ # prevent_thread_lock so this returns and the routes can be attached; the
266
+ # process is held open by block_thread() at the end instead.
267
+ demo.launch(
268
+ server_name="0.0.0.0",
269
+ server_port=int(os.environ.get("PORT", 7860)),
270
+ prevent_thread_lock=True,
271
+ show_error=True,
272
+ )
273
+ demo.app.router.routes[0:0] = app.router.routes
274
+ print("REST routes attached: " +
275
+ ", ".join(sorted(r.path for r in app.router.routes if hasattr(r, "methods"))),
276
+ flush=True)
277
+ demo.block_thread()