Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -1,282 +1,50 @@
|
|
1 |
-
|
2 |
import os
|
3 |
os.environ.setdefault("GRADIO_USE_CDN", "true")
|
4 |
|
5 |
import spaces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
@spaces.GPU(duration=10)
|
8 |
def gpu_probe(a: int = 1, b: int = 1) -> int:
|
9 |
-
#
|
10 |
return a + b
|
11 |
|
12 |
-
|
13 |
@spaces.GPU(duration=10)
|
14 |
def gpu_echo(x: str = "ok") -> str:
|
15 |
return x
|
16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
|
19 |
-
from pathlib import Path
|
20 |
-
from typing import Optional, Tuple, List
|
21 |
-
import subprocess
|
22 |
-
import sys
|
23 |
-
import traceback
|
24 |
-
|
25 |
-
import gradio as gr
|
26 |
-
import numpy as np
|
27 |
-
import soundfile as sf
|
28 |
-
from huggingface_hub import hf_hub_download
|
29 |
-
|
30 |
-
# ---------- Config ----------
|
31 |
-
SPACE_ROOT = Path(__file__).parent.resolve()
|
32 |
-
REPO_DIR = SPACE_ROOT / "SonicMasterRepo"
|
33 |
-
REPO_URL = "https://github.com/AMAAI-Lab/SonicMaster"
|
34 |
-
WEIGHTS_REPO = "amaai-lab/SonicMaster"
|
35 |
-
WEIGHTS_FILE = "model.safetensors"
|
36 |
-
CACHE_DIR = SPACE_ROOT / "weights"
|
37 |
-
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
38 |
-
|
39 |
-
# ZeroGPU detection (heuristic)
|
40 |
-
USE_ZEROGPU = os.getenv("SPACE_RUNTIME", "").lower() == "zerogpu"
|
41 |
-
|
42 |
-
# ---------- Lazy resources ----------
|
43 |
-
_weights_path: Optional[Path] = None
|
44 |
-
_repo_ready: bool = False
|
45 |
-
|
46 |
-
def get_weights_path(progress: Optional[gr.Progress] = None) -> Path:
|
47 |
-
"""Fetch model weights lazily and cache the resolved path."""
|
48 |
-
global _weights_path
|
49 |
-
if _weights_path is None:
|
50 |
-
if progress:
|
51 |
-
progress(0.10, desc="Downloading model weights (first run)")
|
52 |
-
wp = hf_hub_download(
|
53 |
-
repo_id=WEIGHTS_REPO,
|
54 |
-
filename=WEIGHTS_FILE,
|
55 |
-
local_dir=str(CACHE_DIR),
|
56 |
-
local_dir_use_symlinks=False,
|
57 |
-
force_download=False,
|
58 |
-
resume_download=True,
|
59 |
-
)
|
60 |
-
_weights_path = Path(wp)
|
61 |
-
return _weights_path
|
62 |
-
|
63 |
-
def ensure_repo(progress: Optional[gr.Progress] = None) -> Path:
|
64 |
-
"""Clone the inference repo lazily and put it on sys.path."""
|
65 |
-
global _repo_ready
|
66 |
-
if not _repo_ready:
|
67 |
-
if not REPO_DIR.exists():
|
68 |
-
if progress:
|
69 |
-
progress(0.18, desc="Cloning SonicMaster repo (first run)")
|
70 |
-
subprocess.run(
|
71 |
-
["git", "clone", "--depth", "1", REPO_URL, REPO_DIR.as_posix()],
|
72 |
-
check=True,
|
73 |
-
)
|
74 |
-
if REPO_DIR.as_posix() not in sys.path:
|
75 |
-
sys.path.append(REPO_DIR.as_posix())
|
76 |
-
_repo_ready = True
|
77 |
-
return REPO_DIR
|
78 |
-
|
79 |
-
# ---------- Audio helpers ----------
|
80 |
-
def save_temp_wav(wav: np.ndarray, sr: int, path: Path):
|
81 |
-
# Ensure (N, C) shape for soundfile
|
82 |
-
if wav.ndim == 1:
|
83 |
-
data = wav
|
84 |
-
else:
|
85 |
-
# (channels, samples) -> (samples, channels)
|
86 |
-
data = wav.T if wav.shape[0] < wav.shape[1] else wav
|
87 |
-
if data.dtype == np.float64:
|
88 |
-
data = data.astype(np.float32)
|
89 |
-
sf.write(path.as_posix(), data, sr)
|
90 |
-
|
91 |
-
def read_audio(path: str) -> Tuple[np.ndarray, int]:
|
92 |
-
wav, sr = sf.read(path, always_2d=False)
|
93 |
-
if wav.dtype == np.float64:
|
94 |
-
wav = wav.astype(np.float32)
|
95 |
-
return wav, sr
|
96 |
-
|
97 |
-
# ---------- CLI runner ----------
|
98 |
-
def _candidate_commands(py: str, script: Path, ckpt: Path, inp: Path, prompt: str, out: Path) -> List[List[str]]:
|
99 |
-
"""Try multiple arg styles commonly found in repos."""
|
100 |
-
combos = [
|
101 |
-
# infer_single.py (common)
|
102 |
-
[py, script.as_posix(), "--ckpt", ckpt.as_posix(), "--input", inp.as_posix(), "--prompt", prompt, "--output", out.as_posix()],
|
103 |
-
[py, script.as_posix(), "--weights", ckpt.as_posix(), "--input", inp.as_posix(), "--text", prompt, "--out", out.as_posix()],
|
104 |
-
# other possible entrypoints
|
105 |
-
[py, script.as_posix(), "--ckpt", ckpt.as_posix(), "--input", inp.as_posix(), "--text", prompt, "--output", out.as_posix()],
|
106 |
-
]
|
107 |
-
return combos
|
108 |
-
|
109 |
-
def run_sonicmaster_cli(
|
110 |
-
input_wav_path: Path,
|
111 |
-
prompt: str,
|
112 |
-
out_path: Path,
|
113 |
-
progress: Optional[gr.Progress] = None,
|
114 |
-
) -> Tuple[bool, str]:
|
115 |
-
"""
|
116 |
-
Returns (ok, message). Captures stderr/stdout and returns first non-empty output file.
|
117 |
-
"""
|
118 |
-
if progress:
|
119 |
-
progress(0.14, desc="Preparing inference")
|
120 |
-
ckpt = get_weights_path(progress=progress)
|
121 |
-
repo = ensure_repo(progress=progress)
|
122 |
-
|
123 |
-
# Candidate scripts to try
|
124 |
-
script_candidates = [
|
125 |
-
repo / "infer_single.py",
|
126 |
-
repo / "inference_fullsong.py",
|
127 |
-
repo / "inference_ptload_batch.py",
|
128 |
-
]
|
129 |
-
scripts = [s for s in script_candidates if s.exists()]
|
130 |
-
if not scripts:
|
131 |
-
return False, "No inference script found in the repo (expected infer_single.py or similar)."
|
132 |
-
|
133 |
-
py = sys.executable or "python3"
|
134 |
-
env = os.environ.copy() # keep CUDA_VISIBLE_DEVICES etc.
|
135 |
-
|
136 |
-
last_err = ""
|
137 |
-
for idx, script in enumerate(scripts, start=1):
|
138 |
-
for jdx, cmd in enumerate(_candidate_commands(py, script, ckpt, input_wav_path, prompt, out_path), start=1):
|
139 |
-
try:
|
140 |
-
if progress:
|
141 |
-
progress(min(0.20 + 0.08 * (idx + jdx), 0.70), desc=f"Running {script.name} (try {idx}.{jdx})")
|
142 |
-
res = subprocess.run(cmd, capture_output=True, text=True, check=True, env=env)
|
143 |
-
if out_path.exists() and out_path.stat().st_size > 0:
|
144 |
-
if progress:
|
145 |
-
progress(0.88, desc="Post-processing output")
|
146 |
-
# Return any informative stdout as message
|
147 |
-
msg = (res.stdout or "").strip()
|
148 |
-
return True, msg if msg else "Inference completed."
|
149 |
-
else:
|
150 |
-
last_err = f"{script.name} produced no output file."
|
151 |
-
except subprocess.CalledProcessError as e:
|
152 |
-
# Collect stderr/stdout for the user
|
153 |
-
snippet = "\n".join(filter(None, [e.stdout or "", e.stderr or ""])).strip()
|
154 |
-
last_err = snippet if snippet else f"{script.name} failed with return code {e.returncode}."
|
155 |
-
except Exception as e:
|
156 |
-
last_err = f"Unexpected error: {e}\n{traceback.format_exc()}"
|
157 |
-
|
158 |
-
return False, last_err or "All candidate commands failed without an error message."
|
159 |
-
|
160 |
-
# ---------- REAL GPU function (called only if using ZeroGPU / GPU available) ----------
|
161 |
-
@spaces.GPU(duration=180)
|
162 |
-
def enhance_on_gpu(input_path: str, prompt: str, output_path: str) -> Tuple[bool, str]:
|
163 |
-
try:
|
164 |
-
# Initialize CUDA inside the GPU context
|
165 |
-
import torch # noqa: F401
|
166 |
-
except Exception:
|
167 |
-
pass
|
168 |
-
from pathlib import Path as _P
|
169 |
-
return run_sonicmaster_cli(_P(input_path), prompt, _P(output_path), progress=None)
|
170 |
-
|
171 |
-
def _has_cuda() -> bool:
|
172 |
-
try:
|
173 |
-
import torch
|
174 |
-
return torch.cuda.is_available()
|
175 |
-
except Exception:
|
176 |
-
return False
|
177 |
-
|
178 |
-
# ---------- UI callback ----------
|
179 |
-
def enhance_audio_ui(
|
180 |
-
audio_path: str,
|
181 |
-
prompt: str,
|
182 |
-
progress=gr.Progress(track_tqdm=True),
|
183 |
-
) -> Tuple[Optional[Tuple[int, np.ndarray]], str]:
|
184 |
-
"""
|
185 |
-
Returns (audio, message). On failure, audio=None and message=error text.
|
186 |
-
"""
|
187 |
-
try:
|
188 |
-
if not prompt:
|
189 |
-
raise gr.Error("Please provide a text prompt.")
|
190 |
-
if not audio_path:
|
191 |
-
raise gr.Error("Please upload or select an input audio file.")
|
192 |
-
|
193 |
-
wav, sr = read_audio(audio_path)
|
194 |
-
|
195 |
-
tmp_in = SPACE_ROOT / "tmp_in.wav"
|
196 |
-
tmp_out = SPACE_ROOT / "tmp_out.wav"
|
197 |
-
if tmp_out.exists():
|
198 |
-
try:
|
199 |
-
tmp_out.unlink()
|
200 |
-
except Exception:
|
201 |
-
pass
|
202 |
-
|
203 |
-
if progress:
|
204 |
-
progress(0.06, desc="Preparing audio")
|
205 |
-
save_temp_wav(wav, sr, tmp_in)
|
206 |
-
|
207 |
-
# Choose execution path: prefer real GPU if available, else CPU
|
208 |
-
use_gpu_call = USE_ZEROGPU or _has_cuda()
|
209 |
-
|
210 |
-
if progress:
|
211 |
-
progress(0.12, desc="Starting inference")
|
212 |
-
if use_gpu_call:
|
213 |
-
ok, msg = enhance_on_gpu(tmp_in.as_posix(), prompt, tmp_out.as_posix())
|
214 |
-
else:
|
215 |
-
ok, msg = run_sonicmaster_cli(tmp_in, prompt, tmp_out, progress=progress)
|
216 |
-
|
217 |
-
if ok and tmp_out.exists() and tmp_out.stat().st_size > 0:
|
218 |
-
# Return output audio by filepath (lighter than big arrays)
|
219 |
-
# Gradio Audio accepts a (sr, np.ndarray) OR a file path; giving file path is fine.
|
220 |
-
return (None, f"Saved output: {tmp_out.name}\n{msg or ''}") if False else (read_audio(tmp_out.as_posix()), msg or "Done.")
|
221 |
-
else:
|
222 |
-
# On failure: DON'T echo input audio — return None and the error message
|
223 |
-
if not msg:
|
224 |
-
msg = "Inference failed without a specific error message."
|
225 |
-
return (None, msg.strip())
|
226 |
-
|
227 |
-
except gr.Error as e:
|
228 |
-
return (None, str(e))
|
229 |
-
except Exception as e:
|
230 |
-
return (None, f"Unexpected error: {e}\n{traceback.format_exc()}")
|
231 |
-
|
232 |
-
# ---------- Gradio UI ----------
|
233 |
-
PROMPT_EXAMPLES = [
|
234 |
-
["Increase the clarity of this song by emphasizing treble frequencies."],
|
235 |
-
["Make this song sound more boomy by amplifying the low end bass frequencies."],
|
236 |
-
["Make the audio smoother and less distorted."],
|
237 |
-
["Improve the balance in this song."],
|
238 |
-
["Reduce roominess/echo (dereverb)."],
|
239 |
-
["Raise the level of the vocals."],
|
240 |
-
["Give the song a wider stereo image."],
|
241 |
-
]
|
242 |
-
|
243 |
-
with gr.Blocks(title="SonicMaster – Text-Guided Restoration & Mastering", fill_height=True) as demo:
|
244 |
-
gr.Markdown("## 🎧 SonicMaster\nUpload audio, enter a prompt, then click **Enhance**.\n"
|
245 |
-
"- Progress appears below during the first run (weights/repo download).\n"
|
246 |
-
"- If something fails, you'll see the **error message** instead of the input audio.")
|
247 |
-
with gr.Row():
|
248 |
-
with gr.Column(scale=1):
|
249 |
-
in_audio = gr.Audio(label="Input Audio", type="filepath")
|
250 |
-
prompt = gr.Textbox(label="Text Prompt", placeholder="e.g., Reduce reverb and brighten the vocals.")
|
251 |
-
run_btn = gr.Button("🚀 Enhance", variant="primary")
|
252 |
-
gr.Examples(
|
253 |
-
examples=PROMPT_EXAMPLES,
|
254 |
-
inputs=[prompt], # prompt-only examples to avoid heavy file ops at startup
|
255 |
-
label="Prompt Examples",
|
256 |
-
)
|
257 |
-
with gr.Column(scale=1):
|
258 |
-
out_audio = gr.Audio(label="Enhanced Audio (output)")
|
259 |
-
status = gr.Textbox(label="Status / Messages", interactive=False, lines=6)
|
260 |
-
|
261 |
-
# On click, return audio + message
|
262 |
-
run_btn.click(
|
263 |
-
fn=enhance_audio_ui,
|
264 |
-
inputs=[in_audio, prompt],
|
265 |
-
outputs=[out_audio, status],
|
266 |
-
concurrency_limit=1,
|
267 |
-
)
|
268 |
-
|
269 |
-
# Queue BEFORE mounting so the mounted app is ready immediately
|
270 |
-
demo = demo.queue(max_size=16)
|
271 |
-
|
272 |
-
# ---------- FastAPI mount & health ----------
|
273 |
-
from fastapi import FastAPI, Request
|
274 |
-
from starlette.responses import PlainTextResponse
|
275 |
-
try:
|
276 |
-
from starlette.exceptions import ClientDisconnect # Starlette ≥0.27
|
277 |
-
except Exception:
|
278 |
-
from starlette.requests import ClientDisconnect # fallback for older versions
|
279 |
|
|
|
280 |
app = FastAPI()
|
281 |
|
282 |
@app.get("/health")
|
@@ -284,12 +52,12 @@ def _health():
|
|
284 |
return {"ok": True}
|
285 |
|
286 |
@app.exception_handler(ClientDisconnect)
|
287 |
-
async def client_disconnect_handler(request
|
288 |
return PlainTextResponse("Client disconnected", status_code=499)
|
289 |
|
290 |
-
# Mount
|
291 |
app = gr.mount_gradio_app(app, demo, path="/")
|
292 |
|
293 |
if __name__ == "__main__":
|
294 |
import uvicorn
|
295 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
1 |
+
# ---------- MUST BE FIRST ----------
|
2 |
import os
|
3 |
os.environ.setdefault("GRADIO_USE_CDN", "true")
|
4 |
|
5 |
import spaces
|
6 |
+
import gradio as gr
|
7 |
+
from fastapi import FastAPI
|
8 |
+
from starlette.responses import PlainTextResponse
|
9 |
+
try:
|
10 |
+
from starlette.exceptions import ClientDisconnect # Starlette ≥0.27
|
11 |
+
except Exception:
|
12 |
+
from starlette.requests import ClientDisconnect # fallback
|
13 |
|
14 |
+
# Log versions to container logs for sanity
|
15 |
+
try:
|
16 |
+
print("== Sanity ==")
|
17 |
+
print("spaces.__version__:", getattr(spaces, "__version__", "unknown"))
|
18 |
+
import gradio
|
19 |
+
print("gradio.__version__:", getattr(gradio, "__version__", "unknown"))
|
20 |
+
import sys
|
21 |
+
print("python:", sys.version)
|
22 |
+
print("SPACE_RUNTIME:", os.getenv("SPACE_RUNTIME"))
|
23 |
+
print("HF_SPACE_ENTRYPOINT default: app.py")
|
24 |
+
except Exception as _e:
|
25 |
+
print("version log error:", _e)
|
26 |
+
|
27 |
+
# ---------- ZeroGPU probes (PUBLIC NAMES) ----------
|
28 |
@spaces.GPU(duration=10)
|
29 |
def gpu_probe(a: int = 1, b: int = 1) -> int:
|
30 |
+
# Not called; existence is enough for ZeroGPU startup check
|
31 |
return a + b
|
32 |
|
|
|
33 |
@spaces.GPU(duration=10)
|
34 |
def gpu_echo(x: str = "ok") -> str:
|
35 |
return x
|
36 |
|
37 |
+
# ---------- Tiny Gradio UI ----------
|
38 |
+
with gr.Blocks(title="ZeroGPU Probe") as demo:
|
39 |
+
gr.Markdown("### ✅ Minimal app is running.\n"
|
40 |
+
"If you see this UI, import succeeded and GPU probes are registered.")
|
41 |
+
t = gr.Textbox(label="Echo input", value="hello")
|
42 |
+
o = gr.Textbox(label="Echo output")
|
43 |
+
t.submit(lambda s: f"echo: {s}", t, o)
|
44 |
|
45 |
+
demo = demo.queue(max_size=8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
|
47 |
+
# ---------- ASGI app with fast health ----------
|
48 |
app = FastAPI()
|
49 |
|
50 |
@app.get("/health")
|
|
|
52 |
return {"ok": True}
|
53 |
|
54 |
@app.exception_handler(ClientDisconnect)
|
55 |
+
async def client_disconnect_handler(request, exc):
|
56 |
return PlainTextResponse("Client disconnected", status_code=499)
|
57 |
|
58 |
+
# Mount at root (Spaces health-check expects this)
|
59 |
app = gr.mount_gradio_app(app, demo, path="/")
|
60 |
|
61 |
if __name__ == "__main__":
|
62 |
import uvicorn
|
63 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|