sleeper371 commited on
Commit
b00667f
·
verified ·
1 Parent(s): 129c82d

Stable Audio 3 Medium music API (caller-supplied token)

Browse files
Files changed (3) hide show
  1. README.md +54 -6
  2. __pycache__/app.cpython-313.pyc +0 -0
  3. app.py +298 -0
README.md CHANGED
@@ -1,13 +1,61 @@
1
  ---
2
- title: Stable Audio 3 Medium Api
3
- emoji: 🐢
4
  colorFrom: indigo
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.27.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Stable Audio 3 Medium API
3
+ emoji: 🎵
4
  colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.14.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: other
11
+ short_description: Music generation API proxying stable-audio-3-medium.
12
+ models:
13
+ - stabilityai/stable-audio-3-medium
14
  ---
15
 
16
+ # Stable Audio 3 Medium — Music API
17
+
18
+ A thin, stable API wrapper around
19
+ [`stabilityai/stable-audio-3`](https://huggingface.co/spaces/stabilityai/stable-audio-3),
20
+ pinned to the **Medium — general audio** variant
21
+ ([`stabilityai/stable-audio-3-medium`](https://huggingface.co/stabilityai/stable-audio-3-medium)).
22
+
23
+ This Space uses no GPU of its own — it forwards to the upstream ZeroGPU Space and
24
+ exposes a single keyword-addressable endpoint, `/generate`, so callers don't have
25
+ to track upstream's positional, UI-derived signature.
26
+
27
+ ## Endpoint
28
+
29
+ `POST /generate` → `(wav_filepath, status_string)`
30
+
31
+ | param | type | default | notes |
32
+ |---|---|---|---|
33
+ | `prompt` | str | — | required |
34
+ | `duration` | float | 60 | seconds, 1–380 |
35
+ | `steps` | float | 8 | diffusion steps |
36
+ | `cfg_scale` | float | 1.0 | |
37
+ | `sampler_type` | str | `pingpong` | `pingpong`, `euler`, `rk4`, `dpmpp` |
38
+ | `seed` | int | 0 | 0 = random |
39
+ | `negative_prompt` | str | `""` | routes via upstream `/infer_advanced` when set |
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ from gradio_client import Client
45
+
46
+ client = Client("SPACE_ID", token="hf_...")
47
+ wav_path, status = client.predict(
48
+ prompt="Warm analog house groove, deep sub bass, shuffled hats 122 BPM",
49
+ duration=30,
50
+ api_name="/generate",
51
+ )
52
+ print(status, wav_path)
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ | variable | purpose |
58
+ |---|---|
59
+ | `HF_TOKEN` | Space secret. Presented to upstream so ZeroGPU quota is charged to its owner. Without it, calls run anonymously and hit tighter limits. |
60
+ | `UPSTREAM_SPACE` | Override the upstream Space id. Defaults to `stabilityai/stable-audio-3`. |
61
+ | `JOB_TIMEOUT` | Wall-clock budget per upstream job, including queue wait. Defaults to `900` seconds. |
__pycache__/app.cpython-313.pyc ADDED
Binary file (12.7 kB). View file
 
app.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Thin API wrapper around stabilityai/stable-audio-3 (Medium — general audio).
2
+
3
+ This Space owns no GPU and holds no credentials. It proxies to the upstream
4
+ ZeroGPU Space with **the caller's own token**, pinning
5
+ ``variant_key="medium"`` (``stabilityai/stable-audio-3-medium``) and exposing one
6
+ clean, stable endpoint: ``/generate``.
7
+
8
+ Why a wrapper: upstream's ``/infer`` signature is positional and tracks its UI,
9
+ so it shifts whenever the demo's layout changes. ``/generate`` is keyword-only
10
+ and stays put.
11
+
12
+ Why the caller's token: ZeroGPU quota is charged to whoever's token is
13
+ presented. This Space deliberately has no ``HF_TOKEN`` secret, so a caller can
14
+ only ever spend their own quota — never the Space owner's.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import os
21
+ import shutil
22
+ import tempfile
23
+ import threading
24
+ import time
25
+ from collections import OrderedDict
26
+ from typing import Optional
27
+
28
+ import gradio as gr
29
+ import httpx
30
+ from gradio_client import Client
31
+
32
+ UPSTREAM = os.environ.get("UPSTREAM_SPACE", "stabilityai/stable-audio-3")
33
+ VARIANT = "medium" # stabilityai/stable-audio-3-medium — general audio (largest)
34
+
35
+ # Upstream medium: sample_size // sample_rate == 380s.
36
+ MAX_DURATION = 380
37
+ SAMPLERS = ["pingpong", "euler", "rk4", "dpmpp"]
38
+
39
+ # Wall-clock budget for one upstream job, including ZeroGPU queue wait.
40
+ JOB_TIMEOUT = float(os.environ.get("JOB_TIMEOUT", "900"))
41
+
42
+ # gradio_client's default httpx timeout kills the *download* of the result wav:
43
+ # a full 380s render is ~67 MB and reliably exceeds it. Budget generously.
44
+ HTTPX_TIMEOUT = httpx.Timeout(
45
+ float(os.environ.get("HTTPX_TIMEOUT", "600")), connect=30.0
46
+ )
47
+
48
+ # Building a Client refetches the upstream config (seconds), so keep one per
49
+ # caller. Keyed by a digest, never the token itself, and bounded so a stream of
50
+ # distinct callers can't grow it without limit.
51
+ _MAX_CLIENTS = 32
52
+ _clients: "OrderedDict[str, Client]" = OrderedDict()
53
+ _clients_lock = threading.Lock()
54
+
55
+
56
+ def _key(token: Optional[str]) -> str:
57
+ if not token:
58
+ return "anonymous"
59
+ return hashlib.sha256(token.encode()).hexdigest()
60
+
61
+
62
+ def _scrub(text: str, token: Optional[str]) -> str:
63
+ """Never let a caller's token escape into an error message or log line."""
64
+ if token and token in text:
65
+ text = text.replace(token, "<token>")
66
+ return text
67
+
68
+
69
+ def _get_client(token: Optional[str], force_new: bool = False) -> Client:
70
+ key = _key(token)
71
+ with _clients_lock:
72
+ if force_new:
73
+ _clients.pop(key, None)
74
+ client = _clients.get(key)
75
+ if client is not None:
76
+ _clients.move_to_end(key)
77
+ return client
78
+
79
+ # Build outside the lock: this does network I/O and would otherwise stall
80
+ # every other caller.
81
+ client = Client(
82
+ UPSTREAM,
83
+ token=token or None,
84
+ verbose=False,
85
+ httpx_kwargs={"timeout": HTTPX_TIMEOUT},
86
+ )
87
+
88
+ with _clients_lock:
89
+ _clients[key] = client
90
+ _clients.move_to_end(key)
91
+ while len(_clients) > _MAX_CLIENTS:
92
+ _clients.popitem(last=False)
93
+ return client
94
+
95
+
96
+ def _persist(path: str) -> str:
97
+ """gradio_client writes downloads into a temp dir it may later reap. Copy
98
+ into our own temp dir so the file outlives the call."""
99
+ out_dir = tempfile.mkdtemp(prefix="sa3_")
100
+ dest = os.path.join(out_dir, "stable_audio_3_medium.wav")
101
+ shutil.copyfile(path, dest)
102
+ return dest
103
+
104
+
105
+ def _call_upstream(args: tuple, api_name: str, token: Optional[str],
106
+ retry: bool = True):
107
+ try:
108
+ job = _get_client(token).submit(*args, api_name=api_name)
109
+ return job.result(timeout=JOB_TIMEOUT)
110
+ except Exception as exc: # noqa: BLE001
111
+ # A stale cached client (upstream restarted / slept) surfaces as a
112
+ # connection or schema error. Rebuild once, then give up.
113
+ if retry:
114
+ _get_client(token, force_new=True)
115
+ return _call_upstream(args, api_name, token, retry=False)
116
+ raise exc
117
+
118
+
119
+ def generate(
120
+ prompt: str,
121
+ duration: float = 60,
122
+ steps: float = 8,
123
+ cfg_scale: float = 1.0,
124
+ sampler_type: str = "pingpong",
125
+ seed: int = 0,
126
+ negative_prompt: str = "",
127
+ hf_token: str = "",
128
+ ):
129
+ """Generate audio with Stable Audio 3 Medium (general audio).
130
+
131
+ ``hf_token`` is your own Hugging Face token; upstream ZeroGPU quota is
132
+ charged to it. Returns ``(wav_path, status)``.
133
+ """
134
+ prompt = (prompt or "").strip()
135
+ if not prompt:
136
+ raise gr.Error("Please enter a prompt.")
137
+
138
+ token = (hf_token or "").strip() or None
139
+ if token is None:
140
+ raise gr.Error(
141
+ "An `hf_token` is required. This Space forwards your own token to "
142
+ f"{UPSTREAM} so the ZeroGPU quota is charged to you, not the Space "
143
+ "owner. Create one at https://huggingface.co/settings/tokens "
144
+ "(a read token is enough) and pass it as `hf_token`."
145
+ )
146
+
147
+ duration = int(max(1, min(float(duration), MAX_DURATION)))
148
+ steps = int(max(1, min(float(steps), 500)))
149
+ cfg_scale = float(cfg_scale)
150
+ sampler_type = sampler_type if sampler_type in SAMPLERS else "pingpong"
151
+ seed = int(seed)
152
+ negative_prompt = (negative_prompt or "").strip()
153
+
154
+ t0 = time.time()
155
+ try:
156
+ if negative_prompt:
157
+ # Only the advanced endpoint accepts a negative prompt. Its audio
158
+ # inputs are optional in practice despite the schema marking them
159
+ # required; if that ever stops holding, fall back to /infer.
160
+ try:
161
+ result = _call_upstream(
162
+ (
163
+ VARIANT, prompt, negative_prompt, duration, steps,
164
+ cfg_scale, sampler_type, seed,
165
+ 1.0, # sigma_max
166
+ 1.0, # apg_scale
167
+ 6.0, # duration_padding_sec
168
+ True, # cut_to_seconds_total
169
+ None, # init_audio
170
+ 0.9, # init_noise_level
171
+ None, # inpaint_audio
172
+ 0.0, # mask_start_sec
173
+ 0.0, # mask_end_sec
174
+ 0, # preview_every
175
+ ),
176
+ api_name="/infer_advanced",
177
+ token=token,
178
+ )
179
+ audio_path = result[0] if isinstance(result, (list, tuple)) else result
180
+ except Exception as exc: # noqa: BLE001
181
+ print("[generate] /infer_advanced failed "
182
+ f"({_scrub(str(exc), token)}); retrying without negative "
183
+ "prompt", flush=True)
184
+ audio_path = _call_upstream(
185
+ (VARIANT, prompt, duration, steps, cfg_scale, sampler_type, seed),
186
+ api_name="/infer",
187
+ token=token,
188
+ )
189
+ else:
190
+ audio_path = _call_upstream(
191
+ (VARIANT, prompt, duration, steps, cfg_scale, sampler_type, seed),
192
+ api_name="/infer",
193
+ token=token,
194
+ )
195
+ except Exception as exc: # noqa: BLE001
196
+ raise gr.Error(
197
+ f"Upstream {UPSTREAM} failed: {_scrub(str(exc), token)}"
198
+ ) from None
199
+
200
+ if isinstance(audio_path, (list, tuple)):
201
+ audio_path = audio_path[0]
202
+ if isinstance(audio_path, dict):
203
+ audio_path = audio_path.get("path") or audio_path.get("name")
204
+ if not audio_path or not os.path.exists(audio_path):
205
+ raise gr.Error(f"Upstream returned no audio file (got {audio_path!r}).")
206
+
207
+ elapsed = time.time() - t0
208
+ status = (
209
+ f"stable-audio-3-medium · {duration}s · {steps} steps · "
210
+ f"cfg {cfg_scale} · {sampler_type} · seed {seed} · {elapsed:.1f}s"
211
+ )
212
+ return _persist(audio_path), status
213
+
214
+
215
+ DESCRIPTION = f"""
216
+ # 🎵 Stable Audio 3 Medium — Music API
217
+
218
+ Text-to-music via **`stabilityai/stable-audio-3-medium`** (general audio, the largest
219
+ SA3 variant), proxied from [`{UPSTREAM}`](https://huggingface.co/spaces/{UPSTREAM}).
220
+
221
+ **Bring your own token.** This Space stores no credentials — it forwards the
222
+ `hf_token` you pass straight to upstream, so the ZeroGPU quota is charged to
223
+ *you*. A plain [read token](https://huggingface.co/settings/tokens) is enough.
224
+
225
+ ```python
226
+ from gradio_client import Client
227
+
228
+ client = Client("SPACE_ID")
229
+ wav_path, status = client.predict(
230
+ prompt="Warm analog house groove, deep sub bass, shuffled hats 122 BPM",
231
+ duration=30,
232
+ hf_token="hf_...", # your token — your quota
233
+ api_name="/generate",
234
+ )
235
+ ```
236
+ """
237
+
238
+ with gr.Blocks(theme=gr.themes.Citrus(), title="Stable Audio 3 Medium API") as demo:
239
+ gr.Markdown(DESCRIPTION)
240
+
241
+ with gr.Row():
242
+ with gr.Column(scale=2):
243
+ prompt = gr.Textbox(
244
+ label="Prompt",
245
+ placeholder="A dream-like Synthpop instrumental that would accompany "
246
+ "a dream-sequence in a surrealist movie 120 BPM",
247
+ lines=3,
248
+ )
249
+ hf_token = gr.Textbox(
250
+ label="Your Hugging Face token",
251
+ info="Forwarded to the upstream ZeroGPU Space so the quota is "
252
+ "charged to you. Never stored.",
253
+ placeholder="hf_...",
254
+ type="password",
255
+ )
256
+ negative_prompt = gr.Textbox(
257
+ label="Negative prompt (optional)",
258
+ placeholder="lo-fi, distorted, vocals",
259
+ lines=1,
260
+ )
261
+ duration = gr.Slider(
262
+ 1, MAX_DURATION, value=60, step=1,
263
+ label=f"Duration (s) · model max {MAX_DURATION}s",
264
+ )
265
+ with gr.Accordion("Advanced settings", open=False):
266
+ steps = gr.Slider(1, 100, value=8, step=1, label="Steps")
267
+ cfg_scale = gr.Slider(0.0, 25.0, value=1.0, step=0.1, label="CFG scale")
268
+ sampler_type = gr.Dropdown(SAMPLERS, value="pingpong", label="Sampler")
269
+ seed = gr.Number(value=0, precision=0, label="Seed (0 = random)")
270
+ run_btn = gr.Button("🎼 Generate", variant="primary", size="lg")
271
+
272
+ with gr.Column(scale=1):
273
+ audio_out = gr.Audio(label="Output", type="filepath", autoplay=True)
274
+ status_out = gr.Textbox(label="Status", lines=2, interactive=False)
275
+
276
+ gr.Examples(
277
+ examples=[
278
+ ["House music that encapsulates the feeling of being at a festival in "
279
+ "the sunny weather with all your friends 124 BPM", 30],
280
+ ["Rainy night lo-fi hip-hop beat with vinyl crackle, mellow piano chords, "
281
+ "soft kick and snare 80 BPM", 30],
282
+ ["Sweeping orchestral score, soaring strings and brass, heroic and "
283
+ "cinematic, 90 BPM", 45],
284
+ ],
285
+ inputs=[prompt, duration],
286
+ label="Examples",
287
+ )
288
+
289
+ run_btn.click(
290
+ fn=generate,
291
+ inputs=[prompt, duration, steps, cfg_scale, sampler_type, seed,
292
+ negative_prompt, hf_token],
293
+ outputs=[audio_out, status_out],
294
+ api_name="generate",
295
+ )
296
+
297
+ if __name__ == "__main__":
298
+ demo.queue(max_size=20).launch(show_api=True)