Spaces:
Running on Zero
Running on Zero
Fix: patch App.__init__ to inject routes into the actual served app
Browse files
app.py
CHANGED
|
@@ -27,8 +27,6 @@ if hf_token:
|
|
| 27 |
login(token=hf_token)
|
| 28 |
|
| 29 |
|
| 30 |
-
# ZeroGPU Spaces require at least one @spaces.GPU function to be defined
|
| 31 |
-
# during startup. This minimal touch satisfies that requirement.
|
| 32 |
@spaces.GPU(duration=1)
|
| 33 |
def _gpu_startup_touch():
|
| 34 |
return "ok"
|
|
@@ -145,7 +143,7 @@ API_RESOURCES = {
|
|
| 145 |
"extract_top": "integer (0-5, default 0) - fetch full page content for top N results",
|
| 146 |
"region": "string (default 'wt-wt') - e.g. 'us-en', 'gb-en', 'de-de'",
|
| 147 |
},
|
| 148 |
-
"output": "JSON with title, url, content per result.
|
| 149 |
},
|
| 150 |
}
|
| 151 |
|
|
@@ -206,28 +204,13 @@ try:
|
|
| 206 |
hits = list(ddgs.news(query, max_results=max_results, region=region))
|
| 207 |
results = []
|
| 208 |
for r in hits:
|
| 209 |
-
entry = {
|
| 210 |
-
"title": r.get("title", ""),
|
| 211 |
-
"url": r.get("url", ""),
|
| 212 |
-
"content": r.get("body", ""),
|
| 213 |
-
"source": r.get("source", ""),
|
| 214 |
-
"date": r.get("date", ""),
|
| 215 |
-
}
|
| 216 |
if r.get("image"):
|
| 217 |
entry["image"] = r["image"]
|
| 218 |
results.append(entry)
|
| 219 |
elif backend == "images":
|
| 220 |
hits = list(ddgs.images(query, max_results=max_results, region=region))
|
| 221 |
-
results = []
|
| 222 |
-
for r in hits:
|
| 223 |
-
results.append({
|
| 224 |
-
"title": r.get("title", ""),
|
| 225 |
-
"url": r.get("image", ""),
|
| 226 |
-
"source": r.get("source", ""),
|
| 227 |
-
"thumbnail": r.get("thumbnail", ""),
|
| 228 |
-
"width": r.get("width"),
|
| 229 |
-
"height": r.get("height"),
|
| 230 |
-
})
|
| 231 |
else:
|
| 232 |
hits = list(ddgs.text(query, max_results=max_results, region=region))
|
| 233 |
results = [{"title": r.get("title", ""), "url": r.get("href", ""), "content": r.get("body", "")} for r in hits]
|
|
@@ -282,23 +265,14 @@ def _ensure_llama_cpp():
|
|
| 282 |
src_dir = os.path.join(_BENCHMARK_CACHE, "llama_src")
|
| 283 |
if os.path.isdir(src_dir):
|
| 284 |
shutil.rmtree(src_dir, ignore_errors=True)
|
| 285 |
-
subprocess.check_call(
|
| 286 |
-
["git", "clone", "--depth=1", "https://github.com/ggml-org/llama.cpp.git", src_dir],
|
| 287 |
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 288 |
-
)
|
| 289 |
_log("Benchmark: building llama.cpp (this takes 2-5 minutes)...")
|
| 290 |
n_threads = _parse_cpu_count() or 8
|
| 291 |
build_dir = os.path.join(_BENCHMARK_CACHE, "llama_build")
|
| 292 |
os.makedirs(build_dir, exist_ok=True)
|
| 293 |
os.makedirs(bin_dir, exist_ok=True)
|
| 294 |
-
subprocess.check_call(
|
| 295 |
-
|
| 296 |
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 297 |
-
)
|
| 298 |
-
subprocess.check_call(
|
| 299 |
-
["cmake", "--build", build_dir, "-j", str(n_threads), "--target", "llama-cli"],
|
| 300 |
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 301 |
-
)
|
| 302 |
import glob as _glob
|
| 303 |
candidates = _glob.glob(os.path.join(build_dir, "**", "llama-cli"), recursive=True)
|
| 304 |
if candidates:
|
|
@@ -427,78 +401,84 @@ with gr.Blocks(title="Respite API") as demo:
|
|
| 427 |
|
| 428 |
|
| 429 |
# ---------------------------------------------------------------------------
|
| 430 |
-
#
|
| 431 |
-
#
|
| 432 |
-
# Gradio's Blocks.launch() calls App.create_app() to build a NEW FastAPI app,
|
| 433 |
-
# discarding any routes added to demo.app earlier. We wrap that method so our
|
| 434 |
-
# routes get added to the app that is actually served.
|
| 435 |
# ---------------------------------------------------------------------------
|
| 436 |
|
| 437 |
-
from gradio.routes import App as GradioApp
|
| 438 |
|
| 439 |
-
|
|
|
|
| 440 |
|
| 441 |
|
| 442 |
-
def
|
| 443 |
-
|
| 444 |
|
| 445 |
-
@fa_app.get("/respite/resources")
|
| 446 |
-
def _resources():
|
| 447 |
-
return JSONResponse({"resources": API_RESOURCES})
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
return JSONResponse({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
@fa_app.get("/respite/search")
|
| 454 |
-
def _search(q: str = "", backend: str = "text", max_results: int = 10, extract_top: int = 0, region: str = "wt-wt"):
|
| 455 |
-
if not q:
|
| 456 |
-
return JSONResponse({"error": "Missing query parameter 'q'"}, status_code=400)
|
| 457 |
-
if backend not in ("text", "news", "images"):
|
| 458 |
-
return JSONResponse({"error": f"Invalid backend '{backend}'. Use 'text', 'news', or 'images'."}, status_code=400)
|
| 459 |
-
try:
|
| 460 |
-
results = _search_web(q, backend=backend, max_results=max_results, extract_top=extract_top, region=region)
|
| 461 |
-
return JSONResponse(results)
|
| 462 |
-
except Exception as e:
|
| 463 |
-
return JSONResponse({"error": str(e)}, status_code=502)
|
| 464 |
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
|
| 481 |
-
@fa_app.get("/respite/benchmark/test")
|
| 482 |
-
def _benchmark_test():
|
| 483 |
-
import subprocess as _sp
|
| 484 |
-
try:
|
| 485 |
-
llama_cli = _ensure_llama_cpp()
|
| 486 |
-
r = _sp.run([llama_cli, "--version"], capture_output=True, text=True, timeout=10)
|
| 487 |
-
binary_ok = r.returncode == 0
|
| 488 |
-
binary_info = r.stdout.strip()[:200] if binary_ok else r.stderr.strip()[:200]
|
| 489 |
-
from huggingface_hub import hf_hub_download
|
| 490 |
-
model_path = hf_hub_download(repo_id="unsloth/gemma-4-E4B-it-GGUF", filename="gemma-4-E4B-it-Q4_K_M.gguf", cache_dir=_BENCHMARK_CACHE)
|
| 491 |
-
model_ok = os.path.isfile(model_path)
|
| 492 |
-
model_size = os.path.getsize(model_path) if model_ok else 0
|
| 493 |
-
return JSONResponse({"binary_path": llama_cli, "binary_exists": os.path.isfile(llama_cli), "binary_ok": binary_ok, "binary_info": binary_info, "model_path": model_path, "model_ok": model_ok, "model_size_gb": round(model_size / 1024**3, 2)})
|
| 494 |
-
except Exception as e:
|
| 495 |
-
return JSONResponse({"error": str(e)}, status_code=500)
|
| 496 |
|
| 497 |
-
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
|
| 501 |
-
|
| 502 |
|
| 503 |
# Disable the Gradio Spaces file watcher thread which crashes because
|
| 504 |
# importing `spaces` in a new thread after CUDA init fails.
|
|
|
|
| 27 |
login(token=hf_token)
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
| 30 |
@spaces.GPU(duration=1)
|
| 31 |
def _gpu_startup_touch():
|
| 32 |
return "ok"
|
|
|
|
| 143 |
"extract_top": "integer (0-5, default 0) - fetch full page content for top N results",
|
| 144 |
"region": "string (default 'wt-wt') - e.g. 'us-en', 'gb-en', 'de-de'",
|
| 145 |
},
|
| 146 |
+
"output": "JSON with title, url, content per result.",
|
| 147 |
},
|
| 148 |
}
|
| 149 |
|
|
|
|
| 204 |
hits = list(ddgs.news(query, max_results=max_results, region=region))
|
| 205 |
results = []
|
| 206 |
for r in hits:
|
| 207 |
+
entry = {"title": r.get("title", ""), "url": r.get("url", ""), "content": r.get("body", ""), "source": r.get("source", ""), "date": r.get("date", "")}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
if r.get("image"):
|
| 209 |
entry["image"] = r["image"]
|
| 210 |
results.append(entry)
|
| 211 |
elif backend == "images":
|
| 212 |
hits = list(ddgs.images(query, max_results=max_results, region=region))
|
| 213 |
+
results = [{"title": r.get("title", ""), "url": r.get("image", ""), "source": r.get("source", ""), "thumbnail": r.get("thumbnail", ""), "width": r.get("width"), "height": r.get("height")} for r in hits]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
else:
|
| 215 |
hits = list(ddgs.text(query, max_results=max_results, region=region))
|
| 216 |
results = [{"title": r.get("title", ""), "url": r.get("href", ""), "content": r.get("body", "")} for r in hits]
|
|
|
|
| 265 |
src_dir = os.path.join(_BENCHMARK_CACHE, "llama_src")
|
| 266 |
if os.path.isdir(src_dir):
|
| 267 |
shutil.rmtree(src_dir, ignore_errors=True)
|
| 268 |
+
subprocess.check_call(["git", "clone", "--depth=1", "https://github.com/ggml-org/llama.cpp.git", src_dir], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
|
|
|
|
| 269 |
_log("Benchmark: building llama.cpp (this takes 2-5 minutes)...")
|
| 270 |
n_threads = _parse_cpu_count() or 8
|
| 271 |
build_dir = os.path.join(_BENCHMARK_CACHE, "llama_build")
|
| 272 |
os.makedirs(build_dir, exist_ok=True)
|
| 273 |
os.makedirs(bin_dir, exist_ok=True)
|
| 274 |
+
subprocess.check_call(["cmake", "-S", src_dir, "-B", build_dir, "-DLLAMA_NATIVE=ON", "-DBUILD_SHARED_LIBS=OFF"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 275 |
+
subprocess.check_call(["cmake", "--build", build_dir, "-j", str(n_threads), "--target", "llama-cli"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
import glob as _glob
|
| 277 |
candidates = _glob.glob(os.path.join(build_dir, "**", "llama-cli"), recursive=True)
|
| 278 |
if candidates:
|
|
|
|
| 401 |
|
| 402 |
|
| 403 |
# ---------------------------------------------------------------------------
|
| 404 |
+
# Route handlers
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
# ---------------------------------------------------------------------------
|
| 406 |
|
|
|
|
| 407 |
|
| 408 |
+
def _resources_handler():
|
| 409 |
+
return JSONResponse({"resources": API_RESOURCES})
|
| 410 |
|
| 411 |
|
| 412 |
+
def _specs_handler():
|
| 413 |
+
return JSONResponse({**API_SPECS, "runtime": _get_runtime_specs()})
|
| 414 |
|
|
|
|
|
|
|
|
|
|
| 415 |
|
| 416 |
+
def _search_handler(q: str = "", backend: str = "text", max_results: int = 10, extract_top: int = 0, region: str = "wt-wt"):
|
| 417 |
+
if not q:
|
| 418 |
+
return JSONResponse({"error": "Missing query parameter 'q'"}, status_code=400)
|
| 419 |
+
if backend not in ("text", "news", "images"):
|
| 420 |
+
return JSONResponse({"error": f"Invalid backend '{backend}'. Use 'text', 'news', or 'images'."}, status_code=400)
|
| 421 |
+
try:
|
| 422 |
+
results = _search_web(q, backend=backend, max_results=max_results, extract_top=extract_top, region=region)
|
| 423 |
+
return JSONResponse(results)
|
| 424 |
+
except Exception as e:
|
| 425 |
+
return JSONResponse({"error": str(e)}, status_code=502)
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
+
def _benchmark_handler(force: bool = False, model: str = ""):
|
| 429 |
+
try:
|
| 430 |
+
global _BENCHMARK_RESULTS
|
| 431 |
+
if force:
|
| 432 |
+
_BENCHMARK_RESULTS = []
|
| 433 |
+
model_filter = model.strip() if model.strip() else None
|
| 434 |
+
results = _run_benchmark(model_filter=model_filter)
|
| 435 |
+
return JSONResponse({
|
| 436 |
+
"status": "complete",
|
| 437 |
+
"models": results,
|
| 438 |
+
"system": {"cpu_cores": _parse_cpu_count(), "ram_bytes": _parse_ram_bytes(), "threads": _parse_cpu_count() or 8},
|
| 439 |
+
})
|
| 440 |
+
except Exception as e:
|
| 441 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def _benchmark_test_handler():
|
| 445 |
+
import subprocess as _sp
|
| 446 |
+
try:
|
| 447 |
+
llama_cli = _ensure_llama_cpp()
|
| 448 |
+
r = _sp.run([llama_cli, "--version"], capture_output=True, text=True, timeout=10)
|
| 449 |
+
binary_ok = r.returncode == 0
|
| 450 |
+
binary_info = r.stdout.strip()[:200] if binary_ok else r.stderr.strip()[:200]
|
| 451 |
+
from huggingface_hub import hf_hub_download
|
| 452 |
+
model_path = hf_hub_download(repo_id="unsloth/gemma-4-E4B-it-GGUF", filename="gemma-4-E4B-it-Q4_K_M.gguf", cache_dir=_BENCHMARK_CACHE)
|
| 453 |
+
model_ok = os.path.isfile(model_path)
|
| 454 |
+
model_size = os.path.getsize(model_path) if model_ok else 0
|
| 455 |
+
return JSONResponse({"binary_path": llama_cli, "binary_exists": os.path.isfile(llama_cli), "binary_ok": binary_ok, "binary_info": binary_info, "model_path": model_path, "model_ok": model_ok, "model_size_gb": round(model_size / 1024**3, 2)})
|
| 456 |
+
except Exception as e:
|
| 457 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 458 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
|
| 460 |
+
# ---------------------------------------------------------------------------
|
| 461 |
+
# Monkey-patch: intercept Gradio server creation to inject routes.
|
| 462 |
+
# ---------------------------------------------------------------------------
|
| 463 |
+
|
| 464 |
+
_orig_routes = gr.routes.App.__init__
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def _patched_init(self, *args, **kwargs):
|
| 468 |
+
_orig_routes(self, *args, **kwargs)
|
| 469 |
+
_log("Gradio server __init__ patched - adding routes")
|
| 470 |
+
try:
|
| 471 |
+
self.add_api_route("/respite/resources", _resources_handler, methods=["GET"])
|
| 472 |
+
self.add_api_route("/respite/specs", _specs_handler, methods=["GET"])
|
| 473 |
+
self.add_api_route("/respite/search", _search_handler, methods=["GET"])
|
| 474 |
+
self.add_api_route("/respite/benchmark", _benchmark_handler, methods=["GET"])
|
| 475 |
+
self.add_api_route("/respite/benchmark/test", _benchmark_test_handler, methods=["GET"])
|
| 476 |
+
_log("Routes added via __init__ patch")
|
| 477 |
+
except Exception as e:
|
| 478 |
+
_log(f"Route patching failed: {e}")
|
| 479 |
|
| 480 |
|
| 481 |
+
gr.routes.App.__init__ = _patched_init
|
| 482 |
|
| 483 |
# Disable the Gradio Spaces file watcher thread which crashes because
|
| 484 |
# importing `spaces` in a new thread after CUDA init fails.
|