cazyundee commited on
Commit
e3186fc
·
verified ·
1 Parent(s): a76c80d

Fix route injection: try demo.app directly, fallback to monkey-patch

Browse files
Files changed (1) hide show
  1. app.py +118 -145
app.py CHANGED
@@ -54,7 +54,6 @@ def _read_cgroup_file(path):
54
 
55
  def _parse_cpu_count():
56
  """Read actual CPU allocation from cgroup limits."""
57
- # cgroup v2: /sys/fs/cgroup/cpu.max -> "quota period" or "max period"
58
  cpu_max = _read_cgroup_file("/sys/fs/cgroup/cpu.max")
59
  if cpu_max:
60
  parts = cpu_max.split()
@@ -66,7 +65,6 @@ def _parse_cpu_count():
66
  except ValueError:
67
  pass
68
 
69
- # cgroup v1: /sys/fs/cgroup/cpu/cpu.cfs_quota_us and cpu.cfs_period_us
70
  quota = _read_cgroup_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
71
  period = _read_cgroup_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us")
72
  if quota and period:
@@ -77,7 +75,6 @@ def _parse_cpu_count():
77
  except ValueError:
78
  pass
79
 
80
- # cpuset: /sys/fs/cgroup/cpuset.cpus -> "0-3" or "0,1,2,3"
81
  cpuset = _read_cgroup_file("/sys/fs/cgroup/cpuset.cpus")
82
  if cpuset:
83
  count = 0
@@ -95,28 +92,24 @@ def _parse_cpu_count():
95
 
96
  def _parse_ram_bytes():
97
  """Read actual memory limit from cgroup limits."""
98
- # cgroup v2: /sys/fs/cgroup/memory.max
99
  mem_max = _read_cgroup_file("/sys/fs/cgroup/memory.max")
100
  if mem_max and mem_max != "max":
101
  try:
102
  val = int(mem_max)
103
- # cgroup v2 reports in bytes
104
- if val < 2**62: # sanity check: not unlimited
105
  return val
106
  except ValueError:
107
  pass
108
 
109
- # cgroup v1: /sys/fs/cgroup/memory/memory.limit_in_bytes
110
  mem_limit = _read_cgroup_file("/sys/fs/cgroup/memory/memory.limit_in_bytes")
111
  if mem_limit:
112
  try:
113
  val = int(mem_limit)
114
- if val < 2**62: # sanity check: not unlimited (reported as huge number)
115
  return val
116
  except ValueError:
117
  pass
118
 
119
- # Fallback to /proc/meminfo
120
  try:
121
  with open("/proc/meminfo", "r", encoding="utf-8") as meminfo:
122
  for line in meminfo:
@@ -160,7 +153,7 @@ API_RESOURCES = {
160
  "region": "string (default 'wt-wt') - e.g. 'us-en', 'gb-en', 'de-de'",
161
  },
162
  "output": "JSON with title, url, content per result. News results include date and source. Extracted results include extracted_content.",
163
- }
164
  }
165
 
166
  API_SPECS = {
@@ -185,7 +178,6 @@ def _get_runtime_specs():
185
  hardware = os.environ.get("SPACE_HARDWARE", "zero-a10g")
186
  is_zerogpu = "zero" in hardware.lower()
187
 
188
- # Read actual container limits from cgroup, falling back to host values
189
  allocated_cores = _parse_cpu_count()
190
  allocated_ram = _parse_ram_bytes()
191
  host_cores = os.cpu_count()
@@ -216,15 +208,6 @@ try:
216
  from ddgs import DDGS
217
 
218
  def _search_web(query, backend="text", max_results=10, extract_top=0, region="wt-wt"):
219
- """Search the web via DuckDuckGo.
220
-
221
- Args:
222
- query: Search query string.
223
- backend: "text" (general), "news" (news articles), or "images".
224
- max_results: Number of results to return (1-50).
225
- extract_top: Fetch full page content for top N results (0-5).
226
- region: DuckDuckGo region code (e.g. "us-en", "gb-en", "wt-wt" for worldwide).
227
- """
228
  extract_top = max(0, min(5, extract_top))
229
  max_results = max(1, min(50, max_results))
230
 
@@ -267,7 +250,6 @@ try:
267
  "content": r.get("body", ""),
268
  })
269
 
270
- # Optionally extract full page content from top results
271
  if extract_top > 0 and results and backend != "images":
272
  urls_to_extract = [r["url"] for r in results[:extract_top] if r.get("url")]
273
  with DDGS() as ddgs:
@@ -276,7 +258,6 @@ try:
276
  extracted = ddgs.extract(url)
277
  page_content = extracted.get("content", "")
278
  if page_content:
279
- # Find the matching result and add extracted content
280
  for r in results:
281
  if r["url"] == url:
282
  r["extracted_content"] = page_content[:8000]
@@ -299,94 +280,9 @@ except ImportError:
299
 
300
 
301
  # ---------------------------------------------------------------------------
302
- # Model cache
303
  # ---------------------------------------------------------------------------
304
 
305
- MODEL_CACHE = {}
306
-
307
-
308
- def load_model(model_name):
309
- if model_name not in MODEL_CACHE:
310
- _log(f"Loading {model_name} model...")
311
- model = StableAudioModel.from_pretrained(model_name, device="cpu")
312
- MODEL_CACHE[model_name] = model
313
- _log(f"{model_name} loaded successfully!")
314
- return MODEL_CACHE[model_name]
315
-
316
-
317
- def generate_audio(prompt, duration, steps, cfg_scale, seed, model_name):
318
- _log(
319
- f"Generating with {model_name}: prompt='{prompt}', "
320
- f"duration={duration}s, steps={steps}, cfg={cfg_scale}, seed={seed}"
321
- )
322
- model = load_model(model_name)
323
- audio = model.generate(
324
- prompt=prompt, duration=duration, steps=steps,
325
- cfg_scale=cfg_scale, seed=seed, batch_size=1,
326
- )
327
- audio = rearrange(audio, "b d n -> d (b n)")
328
- audio = audio.to(torch.float32).clamp(-1, 1).mul(32767).to(torch.int16).cpu()
329
- output_path = os.path.join(
330
- tempfile.gettempdir(),
331
- f"stable_audio_{seed}_{hash(prompt) & 0xFFFFFFFF:08x}.wav",
332
- )
333
- torchaudio.save(output_path, audio, 44100)
334
- _log("Generation complete!")
335
- return output_path
336
-
337
-
338
- # ---------------------------------------------------------------------------
339
- # Gradio UI
340
- # ---------------------------------------------------------------------------
341
-
342
- with gr.Blocks(title="Respite API") as demo:
343
- gr.Markdown("# Respite API - Music & SFX Generation")
344
- gr.Markdown(
345
- "Generate music and sound effects using "
346
- "Stability AI's Stable Audio 3 Small models."
347
- )
348
-
349
- with gr.Row():
350
- with gr.Column():
351
- model_name = gr.Dropdown(
352
- choices=["small-music", "small-sfx"],
353
- value="small-music", label="Model",
354
- )
355
- prompt = gr.Textbox(
356
- label="Prompt",
357
- placeholder="Describe the music or sound effect you want to generate...",
358
- lines=2,
359
- )
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")
363
- seed = gr.Number(value=-1, label="Seed (-1 for random)")
364
- btn = gr.Button("Generate", variant="primary")
365
-
366
- with gr.Column():
367
- audio_output = gr.Audio(label="Generated Audio", type="filepath")
368
-
369
- btn.click(
370
- fn=generate_audio,
371
- inputs=[prompt, duration, steps, cfg_scale, seed, model_name],
372
- outputs=audio_output,
373
- )
374
-
375
-
376
- # ---------------------------------------------------------------------------
377
- # Inject discovery routes into the Gradio FastAPI app.
378
- #
379
- # Gradio's Blocks.launch() creates a new FastAPI app via App.create_app()
380
- # and any routes on demo.app before launch() are lost. We intercept the
381
- # app creation by wrapping the FastAPI app returned by the Gradio server's
382
- # configure_app / create_app lifecycle, then add our routes at startup.
383
- # ---------------------------------------------------------------------------
384
-
385
- from gradio.routes import App as GradioApp
386
-
387
- _orig_create_app = GradioApp.create_app
388
-
389
- # CPU LLM models for benchmarking via llama.cpp binary
390
  _BENCHMARK_MODELS = [
391
  {
392
  "name": "Gemma 4 E4B",
@@ -411,8 +307,8 @@ _BENCHMARK_MODELS = [
411
  },
412
  ]
413
  _BENCHMARK_CACHE = os.path.join(tempfile.gettempdir(), "llm_benchmark")
414
- _BENCHMARK_RESULTS = [] # cached after first run
415
- _LLAMA_CPP_BIN = None # path to downloaded llama-cli binary
416
 
417
 
418
  def _ensure_llama_cpp():
@@ -431,12 +327,10 @@ def _ensure_llama_cpp():
431
  _LLAMA_CPP_BIN = llama_cli
432
  return llama_cli
433
 
434
- # Clone and build llama.cpp from source
435
  _log("Benchmark: cloning llama.cpp...")
436
  src_dir = os.path.join(_BENCHMARK_CACHE, "llama_src")
437
  if os.path.isdir(src_dir):
438
- import shutil as _shutil
439
- _shutil.rmtree(src_dir, ignore_errors=True)
440
 
441
  subprocess.check_call(
442
  ["git", "clone", "--depth=1", "https://github.com/ggml-org/llama.cpp.git", src_dir],
@@ -456,12 +350,10 @@ def _ensure_llama_cpp():
456
  ["cmake", "--build", build_dir, "-j", str(n_threads), "--target", "llama-cli"],
457
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
458
  )
459
- # Find the built binary
460
  import glob as _glob
461
  candidates = _glob.glob(os.path.join(build_dir, "**", "llama-cli"), recursive=True)
462
  if candidates:
463
- import shutil as _shutil2
464
- _shutil2.copy2(candidates[0], llama_cli)
465
  os.chmod(llama_cli, 0o755)
466
  _log(f"Benchmark: llama.cpp built at {llama_cli}")
467
  else:
@@ -472,16 +364,13 @@ def _ensure_llama_cpp():
472
 
473
 
474
  def _run_benchmark(model_filter=None):
475
- """Run CPU benchmark on models using llama.cpp binary.
476
-
477
- Args:
478
- model_filter: If provided, only run this model name (e.g. 'Gemma 4 E4B').
479
- """
480
  global _BENCHMARK_RESULTS
481
  if _BENCHMARK_RESULTS and not model_filter:
482
  return _BENCHMARK_RESULTS
483
 
484
  import subprocess
 
485
  from huggingface_hub import hf_hub_download
486
 
487
  os.makedirs(_BENCHMARK_CACHE, exist_ok=True)
@@ -510,7 +399,7 @@ def _run_benchmark(model_filter=None):
510
  env = os.environ.copy()
511
  bin_dir = os.path.dirname(llama_cli)
512
  env["LD_LIBRARY_PATH"] = bin_dir + ":" + env.get("LD_LIBRARY_PATH", "")
513
- # 27B model needs more tokens limit and longer timeout
514
  n_predict = 128 if model_info["size_gb"] <= 6 else 256
515
  timeout_sec = 300 if model_info["size_gb"] <= 6 else 600
516
  cmd = [
@@ -525,9 +414,7 @@ def _run_benchmark(model_filter=None):
525
  "--no-display-prompt",
526
  "--log-disable",
527
  ]
528
- _log(f"Benchmark: cmd: {' '.join(cmd[:6])}...")
529
  _log(f"Benchmark: model exists: {os.path.isfile(model_path)}, size: {os.path.getsize(model_path) if os.path.isfile(model_path) else 0}")
530
- _log(f"Benchmark: LD_LIBRARY_PATH={bin_dir}")
531
  t0 = time.time()
532
  try:
533
  proc = subprocess.run(
@@ -541,20 +428,13 @@ def _run_benchmark(model_filter=None):
541
  output_text = proc.stdout.strip()
542
 
543
  _log(f"Benchmark: {model_info['name']} exit code: {proc.returncode}")
544
- _log(f"Benchmark: {model_info['name']} stdout ({len(output_text)} chars): {output_text[:300]}")
545
- _log(f"Benchmark: {model_info['name']} stderr ({len(proc.stderr)} chars): {proc.stderr[-600:]}")
546
 
547
- # Parse llama.cpp stderr for timing info
548
- # Various formats depending on version:
549
- # "prompt eval time = 1234.56 ms / 12 tokens (102.88 ms per token, 9.72 tokens per second)"
550
- # "eval time = 5678.90 ms / 64 tokens (88.73 ms per token, 11.27 tokens per second)"
551
  prompt_tps = 0.0
552
  gen_tps = 0.0
553
  prompt_tok = 0
554
  gen_tok = 0
555
- import re
556
  for line in proc.stderr.splitlines():
557
- # Match: "N.NN tokens per second"
558
  tps_match = re.search(r"(\d+\.?\d*) tokens per second", line)
559
  tok_match = re.search(r"/ (\d+) tokens", line)
560
  if "prompt eval" in line.lower() or "prompt_eval" in line.lower():
@@ -568,10 +448,8 @@ def _run_benchmark(model_filter=None):
568
  if tok_match:
569
  gen_tok = int(tok_match.group(1))
570
 
571
- # If parsing failed, try counting newlines as rough token estimate
572
  if gen_tok == 0 and output_text:
573
  gen_tok = len(output_text.split())
574
- _log(f"Benchmark: parsing failed, estimated {gen_tok} tokens from word count")
575
 
576
  result.update({
577
  "status": "success",
@@ -590,7 +468,7 @@ def _run_benchmark(model_filter=None):
590
 
591
  results.append(result)
592
 
593
- # Free disk cache between models to make room for next
594
  import glob as _glob
595
  for f in _glob.glob(os.path.join(_BENCHMARK_CACHE, "**", "*.gguf"), recursive=True):
596
  try:
@@ -603,8 +481,92 @@ def _run_benchmark(model_filter=None):
603
  return results
604
 
605
 
606
- def _patched_create_app(blocks, **kwargs):
607
- fa_app = _orig_create_app(blocks, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
 
609
  @fa_app.get("/respite/resources")
610
  def _resources():
@@ -628,7 +590,6 @@ def _patched_create_app(blocks, **kwargs):
628
 
629
  @fa_app.get("/respite/benchmark")
630
  def _benchmark(force: bool = False, model: str = ""):
631
- """Run CPU LLM benchmark. Pass ?model=Gemma 4 E4B to run a single model."""
632
  try:
633
  global _BENCHMARK_RESULTS
634
  if force:
@@ -649,15 +610,12 @@ def _patched_create_app(blocks, **kwargs):
649
 
650
  @fa_app.get("/respite/benchmark/test")
651
  def _benchmark_test():
652
- """Quick test: verify llama.cpp binary works."""
653
  import subprocess as _sp
654
  try:
655
  llama_cli = _ensure_llama_cpp()
656
- # Test binary
657
  r = _sp.run([llama_cli, "--version"], capture_output=True, text=True, timeout=10)
658
  binary_ok = r.returncode == 0
659
  binary_info = r.stdout.strip()[:200] if binary_ok else r.stderr.strip()[:200]
660
- # Test model download
661
  from huggingface_hub import hf_hub_download
662
  model_path = hf_hub_download(
663
  repo_id="unsloth/gemma-4-E4B-it-GGUF",
@@ -678,11 +636,26 @@ def _patched_create_app(blocks, **kwargs):
678
  except Exception as e:
679
  return JSONResponse({"error": str(e)}, status_code=500)
680
 
681
- _log("Respite API discovery routes injected")
682
- return fa_app
 
 
 
 
 
 
 
 
 
 
 
683
 
 
 
 
 
684
 
685
- GradioApp.create_app = staticmethod(_patched_create_app)
686
 
687
  # Disable the Gradio Spaces file watcher thread which crashes because
688
  # importing `spaces` in a new thread after CUDA init fails.
@@ -691,4 +664,4 @@ if hasattr(_gr_utils, "watchfn_spaces"):
691
  _gr_utils.watchfn_spaces = lambda *a, **kw: None
692
 
693
  demo.queue(max_size=4, default_concurrency_limit=1)
694
- demo.launch(ssr_mode=False)
 
54
 
55
  def _parse_cpu_count():
56
  """Read actual CPU allocation from cgroup limits."""
 
57
  cpu_max = _read_cgroup_file("/sys/fs/cgroup/cpu.max")
58
  if cpu_max:
59
  parts = cpu_max.split()
 
65
  except ValueError:
66
  pass
67
 
 
68
  quota = _read_cgroup_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
69
  period = _read_cgroup_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us")
70
  if quota and period:
 
75
  except ValueError:
76
  pass
77
 
 
78
  cpuset = _read_cgroup_file("/sys/fs/cgroup/cpuset.cpus")
79
  if cpuset:
80
  count = 0
 
92
 
93
  def _parse_ram_bytes():
94
  """Read actual memory limit from cgroup limits."""
 
95
  mem_max = _read_cgroup_file("/sys/fs/cgroup/memory.max")
96
  if mem_max and mem_max != "max":
97
  try:
98
  val = int(mem_max)
99
+ if val < 2**62:
 
100
  return val
101
  except ValueError:
102
  pass
103
 
 
104
  mem_limit = _read_cgroup_file("/sys/fs/cgroup/memory/memory.limit_in_bytes")
105
  if mem_limit:
106
  try:
107
  val = int(mem_limit)
108
+ if val < 2**62:
109
  return val
110
  except ValueError:
111
  pass
112
 
 
113
  try:
114
  with open("/proc/meminfo", "r", encoding="utf-8") as meminfo:
115
  for line in meminfo:
 
153
  "region": "string (default 'wt-wt') - e.g. 'us-en', 'gb-en', 'de-de'",
154
  },
155
  "output": "JSON with title, url, content per result. News results include date and source. Extracted results include extracted_content.",
156
+ },
157
  }
158
 
159
  API_SPECS = {
 
178
  hardware = os.environ.get("SPACE_HARDWARE", "zero-a10g")
179
  is_zerogpu = "zero" in hardware.lower()
180
 
 
181
  allocated_cores = _parse_cpu_count()
182
  allocated_ram = _parse_ram_bytes()
183
  host_cores = os.cpu_count()
 
208
  from ddgs import DDGS
209
 
210
  def _search_web(query, backend="text", max_results=10, extract_top=0, region="wt-wt"):
 
 
 
 
 
 
 
 
 
211
  extract_top = max(0, min(5, extract_top))
212
  max_results = max(1, min(50, max_results))
213
 
 
250
  "content": r.get("body", ""),
251
  })
252
 
 
253
  if extract_top > 0 and results and backend != "images":
254
  urls_to_extract = [r["url"] for r in results[:extract_top] if r.get("url")]
255
  with DDGS() as ddgs:
 
258
  extracted = ddgs.extract(url)
259
  page_content = extracted.get("content", "")
260
  if page_content:
 
261
  for r in results:
262
  if r["url"] == url:
263
  r["extracted_content"] = page_content[:8000]
 
280
 
281
 
282
  # ---------------------------------------------------------------------------
283
+ # CPU LLM benchmark (llama.cpp)
284
  # ---------------------------------------------------------------------------
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  _BENCHMARK_MODELS = [
287
  {
288
  "name": "Gemma 4 E4B",
 
307
  },
308
  ]
309
  _BENCHMARK_CACHE = os.path.join(tempfile.gettempdir(), "llm_benchmark")
310
+ _BENCHMARK_RESULTS = []
311
+ _LLAMA_CPP_BIN = None
312
 
313
 
314
  def _ensure_llama_cpp():
 
327
  _LLAMA_CPP_BIN = llama_cli
328
  return llama_cli
329
 
 
330
  _log("Benchmark: cloning llama.cpp...")
331
  src_dir = os.path.join(_BENCHMARK_CACHE, "llama_src")
332
  if os.path.isdir(src_dir):
333
+ shutil.rmtree(src_dir, ignore_errors=True)
 
334
 
335
  subprocess.check_call(
336
  ["git", "clone", "--depth=1", "https://github.com/ggml-org/llama.cpp.git", src_dir],
 
350
  ["cmake", "--build", build_dir, "-j", str(n_threads), "--target", "llama-cli"],
351
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
352
  )
 
353
  import glob as _glob
354
  candidates = _glob.glob(os.path.join(build_dir, "**", "llama-cli"), recursive=True)
355
  if candidates:
356
+ shutil.copy2(candidates[0], llama_cli)
 
357
  os.chmod(llama_cli, 0o755)
358
  _log(f"Benchmark: llama.cpp built at {llama_cli}")
359
  else:
 
364
 
365
 
366
  def _run_benchmark(model_filter=None):
367
+ """Run CPU benchmark on models using llama.cpp binary."""
 
 
 
 
368
  global _BENCHMARK_RESULTS
369
  if _BENCHMARK_RESULTS and not model_filter:
370
  return _BENCHMARK_RESULTS
371
 
372
  import subprocess
373
+ import re
374
  from huggingface_hub import hf_hub_download
375
 
376
  os.makedirs(_BENCHMARK_CACHE, exist_ok=True)
 
399
  env = os.environ.copy()
400
  bin_dir = os.path.dirname(llama_cli)
401
  env["LD_LIBRARY_PATH"] = bin_dir + ":" + env.get("LD_LIBRARY_PATH", "")
402
+
403
  n_predict = 128 if model_info["size_gb"] <= 6 else 256
404
  timeout_sec = 300 if model_info["size_gb"] <= 6 else 600
405
  cmd = [
 
414
  "--no-display-prompt",
415
  "--log-disable",
416
  ]
 
417
  _log(f"Benchmark: model exists: {os.path.isfile(model_path)}, size: {os.path.getsize(model_path) if os.path.isfile(model_path) else 0}")
 
418
  t0 = time.time()
419
  try:
420
  proc = subprocess.run(
 
428
  output_text = proc.stdout.strip()
429
 
430
  _log(f"Benchmark: {model_info['name']} exit code: {proc.returncode}")
431
+ _log(f"Benchmark: stderr tail: {proc.stderr[-600:]}")
 
432
 
 
 
 
 
433
  prompt_tps = 0.0
434
  gen_tps = 0.0
435
  prompt_tok = 0
436
  gen_tok = 0
 
437
  for line in proc.stderr.splitlines():
 
438
  tps_match = re.search(r"(\d+\.?\d*) tokens per second", line)
439
  tok_match = re.search(r"/ (\d+) tokens", line)
440
  if "prompt eval" in line.lower() or "prompt_eval" in line.lower():
 
448
  if tok_match:
449
  gen_tok = int(tok_match.group(1))
450
 
 
451
  if gen_tok == 0 and output_text:
452
  gen_tok = len(output_text.split())
 
453
 
454
  result.update({
455
  "status": "success",
 
468
 
469
  results.append(result)
470
 
471
+ # Free disk cache between models
472
  import glob as _glob
473
  for f in _glob.glob(os.path.join(_BENCHMARK_CACHE, "**", "*.gguf"), recursive=True):
474
  try:
 
481
  return results
482
 
483
 
484
+ # ---------------------------------------------------------------------------
485
+ # Model cache & audio generation
486
+ # ---------------------------------------------------------------------------
487
+
488
+ MODEL_CACHE = {}
489
+
490
+
491
+ def load_model(model_name):
492
+ if model_name not in MODEL_CACHE:
493
+ _log(f"Loading {model_name} model...")
494
+ model = StableAudioModel.from_pretrained(model_name, device="cpu")
495
+ MODEL_CACHE[model_name] = model
496
+ _log(f"{model_name} loaded successfully!")
497
+ return MODEL_CACHE[model_name]
498
+
499
+
500
+ def generate_audio(prompt, duration, steps, cfg_scale, seed, model_name):
501
+ _log(
502
+ f"Generating with {model_name}: prompt='{prompt}', "
503
+ f"duration={duration}s, steps={steps}, cfg={cfg_scale}, seed={seed}"
504
+ )
505
+ model = load_model(model_name)
506
+ audio = model.generate(
507
+ prompt=prompt, duration=duration, steps=steps,
508
+ cfg_scale=cfg_scale, seed=seed, batch_size=1,
509
+ )
510
+ audio = rearrange(audio, "b d n -> d (b n)")
511
+ audio = audio.to(torch.float32).clamp(-1, 1).mul(32767).to(torch.int16).cpu()
512
+ output_path = os.path.join(
513
+ tempfile.gettempdir(),
514
+ f"stable_audio_{seed}_{hash(prompt) & 0xFFFFFFFF:08x}.wav",
515
+ )
516
+ torchaudio.save(output_path, audio, 44100)
517
+ _log("Generation complete!")
518
+ return output_path
519
+
520
+
521
+ # ---------------------------------------------------------------------------
522
+ # Gradio UI
523
+ # ---------------------------------------------------------------------------
524
+
525
+ with gr.Blocks(title="Respite API") as demo:
526
+ gr.Markdown("# Respite API - Music & SFX Generation")
527
+ gr.Markdown(
528
+ "Generate music and sound effects using "
529
+ "Stability AI's Stable Audio 3 Small models."
530
+ )
531
+
532
+ with gr.Row():
533
+ with gr.Column():
534
+ model_name = gr.Dropdown(
535
+ choices=["small-music", "small-sfx"],
536
+ value="small-music", label="Model",
537
+ )
538
+ prompt = gr.Textbox(
539
+ label="Prompt",
540
+ placeholder="Describe the music or sound effect you want to generate...",
541
+ lines=2,
542
+ )
543
+ duration = gr.Slider(minimum=1, maximum=120, value=30, step=1, label="Duration (seconds)")
544
+ steps = gr.Slider(minimum=1, maximum=50, value=8, step=1, label="Steps")
545
+ cfg_scale = gr.Slider(minimum=0.0, maximum=10.0, value=1.0, step=0.1, label="CFG Scale")
546
+ seed = gr.Number(value=-1, label="Seed (-1 for random)")
547
+ btn = gr.Button("Generate", variant="primary")
548
+
549
+ with gr.Column():
550
+ audio_output = gr.Audio(label="Generated Audio", type="filepath")
551
+
552
+ btn.click(
553
+ fn=generate_audio,
554
+ inputs=[prompt, duration, steps, cfg_scale, seed, model_name],
555
+ outputs=audio_output,
556
+ )
557
+
558
+
559
+ # ---------------------------------------------------------------------------
560
+ # Add custom routes to demo.app AFTER the Blocks context closes.
561
+ #
562
+ # In Gradio 6.x, demo.app is set to a FastAPI app once the `with gr.Blocks()`
563
+ # context exits. We add our routes here. When the HF runner calls
564
+ # demo.launch(), it wraps demo.app — our routes survive because they're
565
+ # already on the same object.
566
+ # ---------------------------------------------------------------------------
567
+
568
+ def _add_routes(fa_app):
569
+ """Add Respite API routes to a FastAPI app."""
570
 
571
  @fa_app.get("/respite/resources")
572
  def _resources():
 
590
 
591
  @fa_app.get("/respite/benchmark")
592
  def _benchmark(force: bool = False, model: str = ""):
 
593
  try:
594
  global _BENCHMARK_RESULTS
595
  if force:
 
610
 
611
  @fa_app.get("/respite/benchmark/test")
612
  def _benchmark_test():
 
613
  import subprocess as _sp
614
  try:
615
  llama_cli = _ensure_llama_cpp()
 
616
  r = _sp.run([llama_cli, "--version"], capture_output=True, text=True, timeout=10)
617
  binary_ok = r.returncode == 0
618
  binary_info = r.stdout.strip()[:200] if binary_ok else r.stderr.strip()[:200]
 
619
  from huggingface_hub import hf_hub_download
620
  model_path = hf_hub_download(
621
  repo_id="unsloth/gemma-4-E4B-it-GGUF",
 
636
  except Exception as e:
637
  return JSONResponse({"error": str(e)}, status_code=500)
638
 
639
+ _log("Respite API routes registered")
640
+
641
+
642
+ # Try to add routes to demo.app (available after Blocks context exits)
643
+ if hasattr(demo, "app") and demo.app is not None:
644
+ _add_routes(demo.app)
645
+ _log("Routes added to demo.app directly")
646
+ else:
647
+ # demo.app might not exist yet. Use monkey-patch as fallback.
648
+ _log("demo.app not available, using monkey-patch fallback")
649
+
650
+ from gradio.routes import App as GradioApp
651
+ _orig_create_app = GradioApp.create_app
652
 
653
+ def _patched_create_app(blocks, **kwargs):
654
+ fa_app = _orig_create_app(blocks, **kwargs)
655
+ _add_routes(fa_app)
656
+ return fa_app
657
 
658
+ GradioApp.create_app = _patched_create_app
659
 
660
  # Disable the Gradio Spaces file watcher thread which crashes because
661
  # importing `spaces` in a new thread after CUDA init fails.
 
664
  _gr_utils.watchfn_spaces = lambda *a, **kw: None
665
 
666
  demo.queue(max_size=4, default_concurrency_limit=1)
667
+ demo.launch()