Spaces:
Running on Zero
Running on Zero
Fix route injection: try demo.app directly, fallback to monkey-patch
Browse files
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 |
-
|
| 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:
|
| 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 |
-
#
|
| 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 = []
|
| 415 |
-
_LLAMA_CPP_BIN = None
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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:
|
| 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
|
| 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 |
-
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 682 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
|
| 685 |
-
GradioApp.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(
|
|
|
|
| 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()
|