cazyundee commited on
Commit
c7f0d89
·
verified ·
1 Parent(s): 5e3fbbf

Background thread inject routes into running Gradio server

Browse files
Files changed (1) hide show
  1. app.py +85 -146
app.py CHANGED
@@ -13,7 +13,6 @@ from fastapi.responses import JSONResponse
13
  from einops import rearrange
14
  from huggingface_hub import login
15
  from stable_audio_3 import StableAudioModel
16
- from contextlib import asynccontextmanager
17
  import urllib.request
18
  import urllib.parse
19
  import json as _json
@@ -133,15 +132,15 @@ API_RESOURCES = {
133
  },
134
  "web_search": {
135
  "name": "Web search",
136
- "description": "Search the web via DuckDuckGo. Supports general, news, and image search. No API key required.",
137
  "endpoint": "/respite/search",
138
  "method": "GET",
139
  "input": {
140
  "q": "string (search query)",
141
  "backend": "string: 'text' (default), 'news', or 'images'",
142
  "max_results": "integer (1-50, default 10)",
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
  },
@@ -168,15 +167,12 @@ def _get_runtime_specs():
168
  storage = shutil.disk_usage(os.getcwd())
169
  hardware = os.environ.get("SPACE_HARDWARE", "zero-a10g")
170
  is_zerogpu = "zero" in hardware.lower()
171
- allocated_cores = _parse_cpu_count()
172
- allocated_ram = _parse_ram_bytes()
173
- host_cores = os.cpu_count()
174
  return {
175
  "platform": platform.platform(),
176
  "python_version": platform.python_version(),
177
- "cpu_cores": allocated_cores,
178
- "host_cpu_cores": host_cores,
179
- "ram_bytes": allocated_ram,
180
  "storage_total_bytes": storage.total,
181
  "storage_used_bytes": storage.used,
182
  "storage_free_bytes": storage.free,
@@ -202,12 +198,7 @@ try:
202
  with DDGS() as ddgs:
203
  if backend == "news":
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]
@@ -226,24 +217,23 @@ try:
226
  if r["url"] == url:
227
  r["extracted_content"] = page_content[:8000]
228
  break
229
- except Exception as e:
230
- _log(f"Warning: failed to extract {url}: {e}")
231
  return {"query": query, "backend": backend, "results": results, "total_results": len(results)}
232
 
233
  except ImportError:
234
- _log("Warning: duckduckgo_search not installed, search disabled")
235
- def _search_web(query, backend="text", max_results=10, extract_top=0, region="wt-wt"):
236
- raise RuntimeError("Search not available: duckduckgo_search not installed")
237
 
238
 
239
  # ---------------------------------------------------------------------------
240
- # CPU LLM benchmark (llama.cpp)
241
  # ---------------------------------------------------------------------------
242
 
243
  _BENCHMARK_MODELS = [
244
- {"name": "Gemma 4 E4B", "repo": "unsloth/gemma-4-E4B-it-GGUF", "file": "gemma-4-E4B-it-Q4_K_M.gguf", "size_gb": 2.5, "prompt": "Explain recursion in programming in 3 sentences."},
245
- {"name": "Qwen3 8B", "repo": "Qwen/Qwen3-8B-GGUF", "file": "Qwen3-8B-Q4_K_M.gguf", "size_gb": 5.0, "prompt": "Explain recursion in programming in 3 sentences."},
246
- {"name": "Qwen3.8 27B", "repo": "unsloth/Qwen3.8-27B-GGUF", "file": "Qwen3.8-27B-Q4_0.gguf", "size_gb": 16.0, "prompt": "Explain recursion in programming in 3 sentences."},
247
  ]
248
  _BENCHMARK_CACHE = os.path.join(tempfile.gettempdir(), "llm_benchmark")
249
  _BENCHMARK_RESULTS = []
@@ -261,12 +251,11 @@ def _ensure_llama_cpp():
261
  if os.path.isfile(llama_cli):
262
  _LLAMA_CPP_BIN = llama_cli
263
  return llama_cli
264
- _log("Benchmark: cloning llama.cpp...")
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)
@@ -278,9 +267,8 @@ def _ensure_llama_cpp():
278
  if candidates:
279
  shutil.copy2(candidates[0], llama_cli)
280
  os.chmod(llama_cli, 0o755)
281
- _log(f"Benchmark: llama.cpp built at {llama_cli}")
282
  else:
283
- raise RuntimeError(f"llama-cli not found after build in {build_dir}")
284
  _LLAMA_CPP_BIN = llama_cli
285
  return llama_cli
286
 
@@ -294,53 +282,37 @@ def _run_benchmark(model_filter=None):
294
  os.makedirs(_BENCHMARK_CACHE, exist_ok=True)
295
  llama_cli = _ensure_llama_cpp()
296
  n_threads = _parse_cpu_count() or 8
297
- results = []
298
  prompt = "Explain recursion in programming in 3 sentences."
299
- models = _BENCHMARK_MODELS
300
- if model_filter:
301
- models = [m for m in _BENCHMARK_MODELS if m["name"] == model_filter]
302
- if not models:
303
- raise ValueError(f"Unknown model '{model_filter}'. Available: {[m['name'] for m in _BENCHMARK_MODELS]}")
304
  for model_info in models:
305
  result = {"name": model_info["name"], "status": "error"}
306
  try:
307
- _log(f"Benchmark: downloading {model_info['name']} (~{model_info['size_gb']} GB)...")
308
  model_path = hf_hub_download(repo_id=model_info["repo"], filename=model_info["file"], cache_dir=_BENCHMARK_CACHE)
309
- _log(f"Benchmark: running {model_info['name']} with {n_threads} threads...")
310
  env = os.environ.copy()
311
- bin_dir = os.path.dirname(llama_cli)
312
- env["LD_LIBRARY_PATH"] = bin_dir + ":" + env.get("LD_LIBRARY_PATH", "")
313
  n_predict = 128 if model_info["size_gb"] <= 6 else 256
314
  timeout_sec = 300 if model_info["size_gb"] <= 6 else 600
315
  cmd = [llama_cli, "-m", model_path, "-t", str(n_threads), "-c", "2048", "-n", str(n_predict), "--temp", "0.7", "--top-p", "0.9", "-p", prompt, "--no-display-prompt", "--log-disable"]
316
  t0 = time.time()
317
- try:
318
- proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec, env=env)
319
- except subprocess.TimeoutExpired:
320
- result["error"] = "Timeout"
321
- results.append(result)
322
- continue
323
  wall_time = time.time() - t0
324
- output_text = proc.stdout.strip()
325
- _log(f"Benchmark: {model_info['name']} exit code: {proc.returncode}")
326
  prompt_tps = gen_tps = 0.0
327
  prompt_tok = gen_tok = 0
328
  for line in proc.stderr.splitlines():
329
- tps_match = re.search(r"(\d+\.?\d*) tokens per second", line)
330
- tok_match = re.search(r"/ (\d+) tokens", line)
331
  if "prompt eval" in line.lower():
332
- if tps_match: prompt_tps = float(tps_match.group(1))
333
- if tok_match: prompt_tok = int(tok_match.group(1))
334
  elif "eval time" in line.lower() and "prompt" not in line.lower():
335
- if tps_match: gen_tps = float(tps_match.group(1))
336
- if tok_match: gen_tok = int(tok_match.group(1))
337
- if gen_tok == 0 and output_text:
338
- gen_tok = len(output_text.split())
339
- result.update({"status": "success", "total_time_sec": round(wall_time, 2), "prompt_tokens": prompt_tok, "completion_tokens": gen_tok, "prompt_tokens_per_sec": round(prompt_tps, 1), "generation_tokens_per_sec": round(gen_tps, 1), "output": output_text[:500]})
340
- _log(f"Benchmark: {model_info['name']} -> {gen_tps:.1f} gen tok/s, {prompt_tps:.1f} prompt tok/s")
341
  except Exception as e:
342
  result["error"] = str(e)
343
- _log(f"Benchmark ERROR: {model_info['name']}: {e}")
344
  results.append(result)
345
  import glob as _glob
346
  for f in _glob.glob(os.path.join(_BENCHMARK_CACHE, "**", "*.gguf"), recursive=True):
@@ -352,7 +324,7 @@ def _run_benchmark(model_filter=None):
352
 
353
 
354
  # ---------------------------------------------------------------------------
355
- # Model cache & audio generation
356
  # ---------------------------------------------------------------------------
357
 
358
  MODEL_CACHE = {}
@@ -360,22 +332,18 @@ MODEL_CACHE = {}
360
 
361
  def load_model(model_name):
362
  if model_name not in MODEL_CACHE:
363
- _log(f"Loading {model_name} model...")
364
  model = StableAudioModel.from_pretrained(model_name, device="cpu")
365
  MODEL_CACHE[model_name] = model
366
- _log(f"{model_name} loaded successfully!")
367
  return MODEL_CACHE[model_name]
368
 
369
 
370
  def generate_audio(prompt, duration, steps, cfg_scale, seed, model_name):
371
- _log(f"Generating with {model_name}: prompt='{prompt}', duration={duration}s, steps={steps}, cfg={cfg_scale}, seed={seed}")
372
  model = load_model(model_name)
373
  audio = model.generate(prompt=prompt, duration=duration, steps=steps, cfg_scale=cfg_scale, seed=seed, batch_size=1)
374
  audio = rearrange(audio, "b d n -> d (b n)")
375
  audio = audio.to(torch.float32).clamp(-1, 1).mul(32767).to(torch.int16).cpu()
376
  output_path = os.path.join(tempfile.gettempdir(), f"stable_audio_{seed}_{hash(prompt) & 0xFFFFFFFF:08x}.wav")
377
  torchaudio.save(output_path, audio, 44100)
378
- _log("Generation complete!")
379
  return output_path
380
 
381
 
@@ -384,12 +352,11 @@ def generate_audio(prompt, duration, steps, cfg_scale, seed, model_name):
384
  # ---------------------------------------------------------------------------
385
 
386
  with gr.Blocks(title="Respite API") as demo:
387
- gr.Markdown("# Respite API - Music & SFX Generation")
388
- gr.Markdown("Generate music and sound effects using Stability AI's Stable Audio 3 Small models.")
389
  with gr.Row():
390
  with gr.Column():
391
  model_name = gr.Dropdown(choices=["small-music", "small-sfx"], value="small-music", label="Model")
392
- prompt = gr.Textbox(label="Prompt", placeholder="Describe the music or sound effect you want to generate...", lines=2)
393
  duration = gr.Slider(minimum=1, maximum=120, value=30, step=1, label="Duration (seconds)")
394
  steps = gr.Slider(minimum=1, maximum=50, value=8, step=1, label="Steps")
395
  cfg_scale = gr.Slider(minimum=0.0, maximum=10.0, value=1.0, step=0.1, label="CFG Scale")
@@ -401,100 +368,72 @@ with gr.Blocks(title="Respite API") as demo:
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
- # After launch, inject routes into the running server.
462
  # ---------------------------------------------------------------------------
463
 
464
- import atexit
465
-
466
- def _inject_routes_after_launch():
467
- """Wait for Gradio server to start, then add our routes."""
468
- for attempt in range(30):
469
  time.sleep(2)
470
- server = getattr(demo, 'server', None)
 
471
  if server is None:
472
  continue
473
- fa_app = getattr(server, 'app', None)
474
- if fa_app is None:
475
  continue
476
- _log(f"Injecting routes into running server (attempt {attempt+1})...")
477
  try:
478
- fa_app.add_api_route("/respite/resources", _resources_handler, methods=["GET"])
479
- fa_app.add_api_route("/respite/specs", _specs_handler, methods=["GET"])
480
- fa_app.add_api_route("/respite/search", _search_handler, methods=["GET"])
481
- fa_app.add_api_route("/respite/benchmark", _benchmark_handler, methods=["GET"])
482
- fa_app.add_api_route("/respite/benchmark/test", _benchmark_test_handler, methods=["GET"])
483
- _log(f"Routes injected into running server on attempt {attempt+1}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  return
485
  except Exception as e:
486
- _log(f"Route injection attempt {attempt+1} failed: {e}")
487
- _log("Failed to inject routes after 30 attempts")
488
 
489
 
490
- _thread = threading.Thread(target=_inject_routes_after_launch, daemon=True)
491
- _thread.start()
 
492
 
493
- # Disable the Gradio Spaces file watcher thread which crashes because
494
- # importing `spaces` in a new thread after CUDA init fails.
495
  import gradio.utils as _gr_utils
496
  if hasattr(_gr_utils, "watchfn_spaces"):
497
  _gr_utils.watchfn_spaces = lambda *a, **kw: None
498
 
 
499
  demo.queue(max_size=4, default_concurrency_limit=1)
500
- demo.launch(ssr_mode=False)
 
13
  from einops import rearrange
14
  from huggingface_hub import login
15
  from stable_audio_3 import StableAudioModel
 
16
  import urllib.request
17
  import urllib.parse
18
  import json as _json
 
132
  },
133
  "web_search": {
134
  "name": "Web search",
135
+ "description": "Search the web via DuckDuckGo.",
136
  "endpoint": "/respite/search",
137
  "method": "GET",
138
  "input": {
139
  "q": "string (search query)",
140
  "backend": "string: 'text' (default), 'news', or 'images'",
141
  "max_results": "integer (1-50, default 10)",
142
+ "extract_top": "integer (0-5, default 0)",
143
+ "region": "string (default 'wt-wt')",
144
  },
145
  "output": "JSON with title, url, content per result.",
146
  },
 
167
  storage = shutil.disk_usage(os.getcwd())
168
  hardware = os.environ.get("SPACE_HARDWARE", "zero-a10g")
169
  is_zerogpu = "zero" in hardware.lower()
 
 
 
170
  return {
171
  "platform": platform.platform(),
172
  "python_version": platform.python_version(),
173
+ "cpu_cores": _parse_cpu_count(),
174
+ "host_cpu_cores": os.cpu_count(),
175
+ "ram_bytes": _parse_ram_bytes(),
176
  "storage_total_bytes": storage.total,
177
  "storage_used_bytes": storage.used,
178
  "storage_free_bytes": storage.free,
 
198
  with DDGS() as ddgs:
199
  if backend == "news":
200
  hits = list(ddgs.news(query, max_results=max_results, region=region))
201
+ results = [{"title": r.get("title", ""), "url": r.get("url", ""), "content": r.get("body", ""), "source": r.get("source", ""), "date": r.get("date", "")} for r in hits]
 
 
 
 
 
202
  elif backend == "images":
203
  hits = list(ddgs.images(query, max_results=max_results, region=region))
204
  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]
 
217
  if r["url"] == url:
218
  r["extracted_content"] = page_content[:8000]
219
  break
220
+ except Exception:
221
+ pass
222
  return {"query": query, "backend": backend, "results": results, "total_results": len(results)}
223
 
224
  except ImportError:
225
+ def _search_web(query, **kw):
226
+ raise RuntimeError("Search not available")
 
227
 
228
 
229
  # ---------------------------------------------------------------------------
230
+ # CPU LLM benchmark
231
  # ---------------------------------------------------------------------------
232
 
233
  _BENCHMARK_MODELS = [
234
+ {"name": "Gemma 4 E4B", "repo": "unsloth/gemma-4-E4B-it-GGUF", "file": "gemma-4-E4B-it-Q4_K_M.gguf", "size_gb": 2.5},
235
+ {"name": "Qwen3 8B", "repo": "Qwen/Qwen3-8B-GGUF", "file": "Qwen3-8B-Q4_K_M.gguf", "size_gb": 5.0},
236
+ {"name": "Qwen3.8 27B", "repo": "unsloth/Qwen3.8-27B-GGUF", "file": "Qwen3.8-27B-Q4_0.gguf", "size_gb": 16.0},
237
  ]
238
  _BENCHMARK_CACHE = os.path.join(tempfile.gettempdir(), "llm_benchmark")
239
  _BENCHMARK_RESULTS = []
 
251
  if os.path.isfile(llama_cli):
252
  _LLAMA_CPP_BIN = llama_cli
253
  return llama_cli
254
+ _log("Benchmark: building llama.cpp from source...")
255
  src_dir = os.path.join(_BENCHMARK_CACHE, "llama_src")
256
  if os.path.isdir(src_dir):
257
  shutil.rmtree(src_dir, ignore_errors=True)
258
  subprocess.check_call(["git", "clone", "--depth=1", "https://github.com/ggml-org/llama.cpp.git", src_dir], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
 
259
  n_threads = _parse_cpu_count() or 8
260
  build_dir = os.path.join(_BENCHMARK_CACHE, "llama_build")
261
  os.makedirs(build_dir, exist_ok=True)
 
267
  if candidates:
268
  shutil.copy2(candidates[0], llama_cli)
269
  os.chmod(llama_cli, 0o755)
 
270
  else:
271
+ raise RuntimeError("llama-cli not found after build")
272
  _LLAMA_CPP_BIN = llama_cli
273
  return llama_cli
274
 
 
282
  os.makedirs(_BENCHMARK_CACHE, exist_ok=True)
283
  llama_cli = _ensure_llama_cpp()
284
  n_threads = _parse_cpu_count() or 8
 
285
  prompt = "Explain recursion in programming in 3 sentences."
286
+ models = [m for m in _BENCHMARK_MODELS if m["name"] == model_filter] if model_filter else _BENCHMARK_MODELS
287
+ if model_filter and not models:
288
+ raise ValueError(f"Unknown model. Available: {[m['name'] for m in _BENCHMARK_MODELS]}")
289
+ results = []
 
290
  for model_info in models:
291
  result = {"name": model_info["name"], "status": "error"}
292
  try:
 
293
  model_path = hf_hub_download(repo_id=model_info["repo"], filename=model_info["file"], cache_dir=_BENCHMARK_CACHE)
 
294
  env = os.environ.copy()
295
+ env["LD_LIBRARY_PATH"] = os.path.dirname(llama_cli)
 
296
  n_predict = 128 if model_info["size_gb"] <= 6 else 256
297
  timeout_sec = 300 if model_info["size_gb"] <= 6 else 600
298
  cmd = [llama_cli, "-m", model_path, "-t", str(n_threads), "-c", "2048", "-n", str(n_predict), "--temp", "0.7", "--top-p", "0.9", "-p", prompt, "--no-display-prompt", "--log-disable"]
299
  t0 = time.time()
300
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec, env=env)
 
 
 
 
 
301
  wall_time = time.time() - t0
 
 
302
  prompt_tps = gen_tps = 0.0
303
  prompt_tok = gen_tok = 0
304
  for line in proc.stderr.splitlines():
305
+ tps_m = re.search(r"(\d+\.?\d*) tokens per second", line)
306
+ tok_m = re.search(r"/ (\d+) tokens", line)
307
  if "prompt eval" in line.lower():
308
+ if tps_m: prompt_tps = float(tps_m.group(1))
309
+ if tok_m: prompt_tok = int(tok_m.group(1))
310
  elif "eval time" in line.lower() and "prompt" not in line.lower():
311
+ if tps_m: gen_tps = float(tps_m.group(1))
312
+ if tok_m: gen_tok = int(tok_m.group(1))
313
+ result.update({"status": "success", "total_time_sec": round(wall_time, 2), "prompt_tokens": prompt_tok, "completion_tokens": gen_tok, "prompt_tokens_per_sec": round(prompt_tps, 1), "generation_tokens_per_sec": round(gen_tps, 1), "output": proc.stdout.strip()[:500]})
 
 
 
314
  except Exception as e:
315
  result["error"] = str(e)
 
316
  results.append(result)
317
  import glob as _glob
318
  for f in _glob.glob(os.path.join(_BENCHMARK_CACHE, "**", "*.gguf"), recursive=True):
 
324
 
325
 
326
  # ---------------------------------------------------------------------------
327
+ # Audio generation
328
  # ---------------------------------------------------------------------------
329
 
330
  MODEL_CACHE = {}
 
332
 
333
  def load_model(model_name):
334
  if model_name not in MODEL_CACHE:
 
335
  model = StableAudioModel.from_pretrained(model_name, device="cpu")
336
  MODEL_CACHE[model_name] = model
 
337
  return MODEL_CACHE[model_name]
338
 
339
 
340
  def generate_audio(prompt, duration, steps, cfg_scale, seed, model_name):
 
341
  model = load_model(model_name)
342
  audio = model.generate(prompt=prompt, duration=duration, steps=steps, cfg_scale=cfg_scale, seed=seed, batch_size=1)
343
  audio = rearrange(audio, "b d n -> d (b n)")
344
  audio = audio.to(torch.float32).clamp(-1, 1).mul(32767).to(torch.int16).cpu()
345
  output_path = os.path.join(tempfile.gettempdir(), f"stable_audio_{seed}_{hash(prompt) & 0xFFFFFFFF:08x}.wav")
346
  torchaudio.save(output_path, audio, 44100)
 
347
  return output_path
348
 
349
 
 
352
  # ---------------------------------------------------------------------------
353
 
354
  with gr.Blocks(title="Respite API") as demo:
355
+ gr.Markdown("# Respite API")
 
356
  with gr.Row():
357
  with gr.Column():
358
  model_name = gr.Dropdown(choices=["small-music", "small-sfx"], value="small-music", label="Model")
359
+ prompt = gr.Textbox(label="Prompt", placeholder="Describe the audio...", lines=2)
360
  duration = gr.Slider(minimum=1, maximum=120, value=30, step=1, label="Duration (seconds)")
361
  steps = gr.Slider(minimum=1, maximum=50, value=8, step=1, label="Steps")
362
  cfg_scale = gr.Slider(minimum=0.0, maximum=10.0, value=1.0, step=0.1, label="CFG Scale")
 
368
 
369
 
370
  # ---------------------------------------------------------------------------
371
+ # After Gradio is launched, inject routes into the running server.
372
+ #
373
+ # Strategy: Gradio blocks while the server runs, so we start a thread that
374
+ # waits for the server to be ready, then adds our routes to it.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  # ---------------------------------------------------------------------------
376
 
377
+ def _inject_routes():
378
+ """Wait for Gradio server to start, then add API routes."""
379
+ for attempt in range(60):
 
 
380
  time.sleep(2)
381
+ # demo.server is set after launch() starts uvicorn
382
+ server = getattr(demo, "server", None)
383
  if server is None:
384
  continue
385
+ app = getattr(server, "app", None)
386
+ if app is None:
387
  continue
388
+ _log(f"Injecting routes (attempt {attempt+1})...")
389
  try:
390
+ # Add all API routes
391
+ from fastapi import Request as _Req
392
+
393
+ @app.get("/respite/resources")
394
+ async def _resources():
395
+ return JSONResponse({"resources": API_RESOURCES})
396
+
397
+ @app.get("/respite/specs")
398
+ async def _specs():
399
+ return JSONResponse({**API_SPECS, "runtime": _get_runtime_specs()})
400
+
401
+ @app.get("/respite/search")
402
+ async def _search(q: str = "", backend: str = "text", max_results: int = 10, extract_top: int = 0, region: str = "wt-wt"):
403
+ if not q:
404
+ return JSONResponse({"error": "Missing 'q' parameter"}, status_code=400)
405
+ try:
406
+ return JSONResponse(_search_web(q, backend=backend, max_results=max_results, extract_top=extract_top, region=region))
407
+ except Exception as e:
408
+ return JSONResponse({"error": str(e)}, status_code=502)
409
+
410
+ @app.get("/respite/benchmark")
411
+ async def _benchmark(force: bool = False, model: str = ""):
412
+ try:
413
+ global _BENCHMARK_RESULTS
414
+ if force:
415
+ _BENCHMARK_RESULTS = []
416
+ results = _run_benchmark(model_filter=model.strip() or None)
417
+ return JSONResponse({"status": "complete", "models": results, "system": {"cpu_cores": _parse_cpu_count(), "ram_bytes": _parse_ram_bytes(), "threads": _parse_cpu_count() or 8}})
418
+ except Exception as e:
419
+ return JSONResponse({"error": str(e)}, status_code=500)
420
+
421
+ _log("Routes injected into running server!")
422
  return
423
  except Exception as e:
424
+ _log(f"Route injection failed: {e}")
425
+ _log("WARNING: Could not inject routes after 120s")
426
 
427
 
428
+ # Start route injection in background
429
+ _inject_thread = threading.Thread(target=_inject_routes, daemon=True)
430
+ _inject_thread.start()
431
 
432
+ # Disable file watcher
 
433
  import gradio.utils as _gr_utils
434
  if hasattr(_gr_utils, "watchfn_spaces"):
435
  _gr_utils.watchfn_spaces = lambda *a, **kw: None
436
 
437
+ _log("Starting Gradio server...")
438
  demo.queue(max_size=4, default_concurrency_limit=1)
439
+ demo.launch()