FresherDifference commited on
Commit
7c0fe60
·
verified ·
1 Parent(s): e22ee16

Add Gradio app

Browse files
Files changed (1) hide show
  1. app.py +873 -0
app.py ADDED
@@ -0,0 +1,873 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """audio.cpp on Hugging Face ZeroGPU.
2
+
3
+ A Gradio front end for https://github.com/0xShug0/audio.cpp: 100+ TTS, voice
4
+ cloning, ASR, diarization, separation, voice conversion and music models,
5
+ served by the native audio.cpp CUDA runtime. GPU work runs inside a ZeroGPU
6
+ call; when no GPU is available the same binary runs on CPU.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import spaces # import first so ZeroGPU can patch the process
11
+
12
+ import html
13
+ import json
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ import time
21
+ import traceback
22
+ import urllib.parse
23
+ import urllib.request
24
+ import uuid
25
+ import zipfile
26
+ from pathlib import Path
27
+
28
+ import gradio as gr
29
+
30
+ import audiocpp_runtime as ac
31
+
32
+ APP_DIR = Path(__file__).resolve().parent
33
+ ALLOW_PYTHON = os.environ.get("ALLOW_PYTHON", "1").lower() not in {"0", "false", "no", "off"}
34
+ MAX_BATCH = int(os.environ.get("MAX_BATCH", "24"))
35
+ MAX_GPU_SECONDS = int(os.environ.get("MAX_GPU_SECONDS", "240"))
36
+ MAX_DOWNLOAD_BYTES = int(os.environ.get("MAX_DOWNLOAD_BYTES", str(300 * 1024 * 1024)))
37
+ SAMPLE_URL = "https://raw.githubusercontent.com/0xShug0/audio.cpp/main/assets/resources/{name}"
38
+ SAMPLE_DIR = ac.HOME / "samples"
39
+
40
+
41
+ def sample_file(name: str) -> str:
42
+ """Fetch a small sample clip from the audio.cpp repo (cached)."""
43
+ SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
44
+ dest = SAMPLE_DIR / name
45
+ if not dest.exists():
46
+ urllib.request.urlretrieve(SAMPLE_URL.format(name=name), dest)
47
+ return str(dest)
48
+
49
+ # --------------------------------------------------------------------------- boot
50
+ SETUP_ERROR = ""
51
+ try:
52
+ ac.ensure_runtime()
53
+ CATALOG = ac.catalog()
54
+ except Exception as exc: # keep the UI alive so the error is visible
55
+ SETUP_ERROR = f"{type(exc).__name__}: {exc}"
56
+ traceback.print_exc()
57
+ CATALOG = {}
58
+
59
+ try:
60
+ MODEL_PARAMS = json.loads((APP_DIR / "model_params.json").read_text(encoding="utf-8"))
61
+ except Exception:
62
+ MODEL_PARAMS = {}
63
+
64
+ TASK_ORDER = ["tts", "clone", "design", "asr", "align", "diar", "vad", "sep", "vc", "svc",
65
+ "s2s", "music", "sfx", "edit", "midi", "turn"]
66
+ TASKS = [t for t in TASK_ORDER if any(t in f["tasks"] for f in CATALOG.values())]
67
+ TASK_CHOICES = [(ac.TASK_LABELS.get(t, t), t) for t in TASKS]
68
+
69
+ TEXT_TASKS = {"tts", "clone", "design", "align", "music", "sfx", "edit", "s2s"}
70
+ AUDIO_TASKS = {"asr", "align", "vad", "diar", "sep", "vc", "svc", "s2s", "edit", "midi", "turn"}
71
+ VOICE_TASKS = {"tts", "clone", "design", "vc", "svc"}
72
+ INSTRUCT_TASKS = {"tts", "clone", "design", "s2s", "edit"}
73
+ MUSIC_TASKS = {"music", "sfx", "edit"}
74
+ TEXT_LABELS = {
75
+ "tts": "Text to speak", "clone": "Text to speak", "design": "Text to speak",
76
+ "align": "Transcript to align", "music": "Music prompt / caption", "sfx": "Sound prompt",
77
+ "edit": "Edit instruction / prompt", "s2s": "Optional text prompt",
78
+ }
79
+
80
+ FAMILY_PREFS = {
81
+ "tts": ["supertonic", "kokoro_tts", "pocket_tts", "omnivoice", "qwen3_tts"],
82
+ "clone": ["pocket_tts", "omnivoice", "chatterbox", "qwen3_tts", "index_tts2"],
83
+ "design": ["omnivoice", "qwen3_tts", "maya1"],
84
+ "asr": ["qwen3_asr", "parakeet_tdt", "nemotron_asr", "sense_asr", "fun_asr_nano"],
85
+ "align": ["qwen3_forced_aligner"],
86
+ "diar": ["moss_transcribe_diarize", "nemotron_3_diar", "sortformer_diar"],
87
+ "vad": ["silero_vad", "pulsevad"],
88
+ "sep": ["htdemucs", "mel_band_roformer", "bs_roformer"],
89
+ "vc": ["seed_vc", "chatterbox", "meanvc2"],
90
+ "music": ["ace_step", "stable_audio", "heartmula"],
91
+ "sfx": ["stable_audio", "midashenglm_gen"],
92
+ }
93
+
94
+
95
+ def families_for(task: str) -> list[tuple[str, str]]:
96
+ fams = ac.families_for_task(task) if CATALOG else []
97
+ prefs = [f for f in FAMILY_PREFS.get(task, []) if f in fams]
98
+ ordered = prefs + [f for f in fams if f not in prefs]
99
+ return [(f"{CATALOG[f]['display_name']} ({f})", f) for f in ordered]
100
+
101
+
102
+ def packages_for(family: str) -> list[tuple[str, str]]:
103
+ if family not in CATALOG:
104
+ return []
105
+ rows = []
106
+ for p in CATALOG[family]["packages"]:
107
+ mark = " [downloaded]" if ac.is_installed(p["id"]) else ""
108
+ fmt = p.get("format", "")
109
+ label = f"{p.get('display_name') or p['id']} [{fmt}]{mark}"
110
+ rows.append((label, p["id"]))
111
+ return rows
112
+
113
+
114
+ def _human(n: int | None) -> str:
115
+ if not n:
116
+ return "unknown size"
117
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
118
+ if n < 1024:
119
+ return f"{n:.1f} {unit}"
120
+ n /= 1024
121
+ return f"{n:.1f} PB"
122
+
123
+
124
+ def family_info(family: str, pkg: str | None) -> str:
125
+ if family not in CATALOG:
126
+ return ""
127
+ f = CATALOG[family]
128
+ langs = ", ".join(f["languages"][:30]) + (" ..." if len(f["languages"]) > 30 else "")
129
+ docs = " · ".join(f"[{Path(d).name}](https://github.com/0xShug0/audio.cpp/blob/main/{d})" for d in f["docs"][:4])
130
+ lines = [f"**{f['display_name']}** · tasks: `{', '.join(f['tasks'])}`" + (f" · status: {f['status']}" if f['status'] else ""),
131
+ f["description"]]
132
+ if langs:
133
+ lines.append(f"Languages: {langs}")
134
+ if f["voices"]:
135
+ lines.append(f"Built-in voices: {len(f['voices'])}")
136
+ if pkg:
137
+ state = "downloaded" if ac.is_installed(pkg) else "downloads on first run"
138
+ lines.append(f"Package `{pkg}`: {state}")
139
+ if docs:
140
+ lines.append(f"Docs: {docs}")
141
+ return "\n\n".join(lines)
142
+
143
+
144
+ def param_defaults(family: str) -> tuple[str, str, str]:
145
+ """(request options JSON, session options JSON, markdown reference table)."""
146
+ req, sess = {}, {}
147
+ for p in MODEL_PARAMS.get(family, []) if isinstance(MODEL_PARAMS.get(family), list) else []:
148
+ default = p.get("default")
149
+ if default in (None, ""):
150
+ continue
151
+ (sess if p.get("scope") == "session" else req)[p["name"]] = default
152
+ rows = ["| option | type | default | description |", "|---|---|---|---|"]
153
+ for o in CATALOG.get(family, {}).get("request_options", [])[:60]:
154
+ desc = str(o.get("description", "")).replace("|", "/")
155
+ rng = ""
156
+ if "min" in o or "max" in o:
157
+ rng = f" ({o.get('min', '')}..{o.get('max', '')})"
158
+ vals = f" values: {', '.join(map(str, o['values']))}" if o.get("values") else ""
159
+ rows.append(f"| `{o['name']}` | {o.get('type', '')}{rng} | {o.get('default', '')} | {desc}{vals} |")
160
+ table = "\n".join(rows) if len(rows) > 2 else "_This model's spec declares no request options._"
161
+ return (json.dumps(req, indent=2, ensure_ascii=False) if req else "{}",
162
+ json.dumps(sess, indent=2, ensure_ascii=False) if sess else "{}",
163
+ table)
164
+
165
+
166
+ # --------------------------------------------------------------------------- text input helpers
167
+ def extract_text(path: str) -> str:
168
+ p = Path(path)
169
+ ext = p.suffix.lower()
170
+ if ext == ".pdf":
171
+ from pypdf import PdfReader
172
+ return "\n\n".join((page.extract_text() or "") for page in PdfReader(str(p)).pages)
173
+ if ext == ".docx":
174
+ import docx
175
+ return "\n".join(par.text for par in docx.Document(str(p)).paragraphs)
176
+ if ext == ".epub":
177
+ parts = []
178
+ with zipfile.ZipFile(p) as z:
179
+ for name in sorted(z.namelist()):
180
+ if name.lower().endswith((".xhtml", ".html", ".htm")):
181
+ parts.append(strip_html(z.read(name).decode("utf-8", "replace")))
182
+ return "\n\n".join(parts)
183
+ raw = p.read_bytes().decode("utf-8", "replace")
184
+ if ext in {".html", ".htm", ".xhtml", ".xml"}:
185
+ return strip_html(raw)
186
+ if ext in {".srt", ".vtt"}:
187
+ return strip_subtitles(raw)
188
+ if ext == ".json":
189
+ try:
190
+ data = json.loads(raw)
191
+ if isinstance(data, str):
192
+ return data
193
+ if isinstance(data, dict):
194
+ for key in ("text", "input", "content", "transcript"):
195
+ if isinstance(data.get(key), str):
196
+ return data[key]
197
+ except Exception:
198
+ pass
199
+ return raw
200
+
201
+
202
+ def strip_html(raw: str) -> str:
203
+ raw = re.sub(r"(?is)<(script|style|head).*?</\1>", " ", raw)
204
+ raw = re.sub(r"(?i)<br\s*/?>|</p>|</div>|</h\d>|</li>", "\n", raw)
205
+ raw = re.sub(r"<[^>]+>", " ", raw)
206
+ raw = html.unescape(raw)
207
+ return re.sub(r"[ \t]+", " ", re.sub(r"\n\s*\n+", "\n\n", raw)).strip()
208
+
209
+
210
+ def strip_subtitles(raw: str) -> str:
211
+ out = []
212
+ for line in raw.splitlines():
213
+ s = line.strip()
214
+ if not s or s.isdigit() or "-->" in s or s.upper().startswith("WEBVTT"):
215
+ continue
216
+ out.append(re.sub(r"<[^>]+>", "", s))
217
+ return " ".join(out)
218
+
219
+
220
+ def fetch_url(url: str, dest_dir: Path) -> Path:
221
+ url = url.strip()
222
+ if not re.match(r"^https?://", url):
223
+ raise gr.Error("URLs must start with http:// or https://")
224
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (audio.cpp Space)"})
225
+ with urllib.request.urlopen(req, timeout=60) as resp:
226
+ ctype = resp.headers.get("Content-Type", "")
227
+ name = Path(urllib.parse.urlparse(resp.geturl()).path).name or "download"
228
+ if "." not in name:
229
+ guess = {"text/html": ".html", "application/pdf": ".pdf", "audio/mpeg": ".mp3",
230
+ "audio/wav": ".wav", "text/plain": ".txt"}.get(ctype.split(";")[0].strip(), "")
231
+ name += guess
232
+ dest = dest_dir / f"{uuid.uuid4().hex[:6]}-{name[-80:]}"
233
+ size = 0
234
+ with open(dest, "wb") as out:
235
+ while chunk := resp.read(1 << 20):
236
+ size += len(chunk)
237
+ if size > MAX_DOWNLOAD_BYTES:
238
+ raise gr.Error(f"Download is larger than {_human(MAX_DOWNLOAD_BYTES)}.")
239
+ out.write(chunk)
240
+ return dest
241
+
242
+
243
+ def load_text(files, url: str, current: str, mode: str) -> str:
244
+ chunks = []
245
+ tmp = Path(tempfile.mkdtemp(prefix="text-"))
246
+ for f in files or []:
247
+ path = f if isinstance(f, str) else getattr(f, "name", str(f))
248
+ chunks.append(extract_text(path).strip())
249
+ if url and url.strip():
250
+ chunks.append(extract_text(str(fetch_url(url, tmp))).strip())
251
+ if not chunks:
252
+ raise gr.Error("Add a text file or a URL first.")
253
+ loaded = "\n\n".join(c for c in chunks if c)
254
+ if mode == "Append" and current and current.strip():
255
+ return current.rstrip() + "\n\n" + loaded
256
+ return loaded
257
+
258
+
259
+ def split_text(text: str, mode: str) -> list[str]:
260
+ text = text.strip()
261
+ if mode == "One clip per paragraph":
262
+ parts = [p.strip() for p in re.split(r"\n\s*\n", text)]
263
+ elif mode == "One clip per line":
264
+ parts = [p.strip() for p in text.splitlines()]
265
+ else:
266
+ parts = [text]
267
+ return [p for p in parts if p]
268
+
269
+
270
+ # --------------------------------------------------------------------------- GPU entry points
271
+ def _gpu_seconds(job: dict, seconds: int) -> int:
272
+ return int(max(20, min(int(seconds or 90), MAX_GPU_SECONDS)))
273
+
274
+
275
+ @spaces.GPU(duration=_gpu_seconds)
276
+ def gpu_execute(job: dict, seconds: int) -> dict:
277
+ return ac.execute(job, prefer_gpu=True)
278
+
279
+
280
+ def _python_seconds(code_path: str, seconds: int) -> int:
281
+ return int(max(20, min(int(seconds or 90), MAX_GPU_SECONDS)))
282
+
283
+
284
+ @spaces.GPU(duration=_python_seconds)
285
+ def gpu_python(code_path: str, seconds: int) -> dict:
286
+ return run_python_file(code_path, seconds)
287
+
288
+
289
+ def _looks_like_gpu_unavailable(err: Exception) -> bool:
290
+ msg = str(err).lower()
291
+ return any(k in msg for k in ("quota", "zerogpu", "no gpu", "gpu task aborted", "unlogged",
292
+ "gpu is not available", "too many requests"))
293
+
294
+
295
+ # --------------------------------------------------------------------------- main run
296
+ def _parse_json(text: str, what: str) -> dict:
297
+ text = (text or "").strip()
298
+ if not text:
299
+ return {}
300
+ try:
301
+ val = json.loads(text)
302
+ except json.JSONDecodeError as err:
303
+ raise gr.Error(f"{what} is not valid JSON: {err}")
304
+ if not isinstance(val, dict):
305
+ raise gr.Error(f"{what} must be a JSON object.")
306
+ return val
307
+
308
+
309
+ def _collect_audio(mic, files, url, work: Path) -> list[str]:
310
+ paths: list[str] = []
311
+ if mic:
312
+ paths.append(mic)
313
+ for f in files or []:
314
+ paths.append(f if isinstance(f, str) else getattr(f, "name", str(f)))
315
+ if url and url.strip():
316
+ for u in re.split(r"[\s,]+", url.strip()):
317
+ if u:
318
+ paths.append(str(fetch_url(u, work)))
319
+ return paths
320
+
321
+
322
+ def _fmt_time(sample: int | float | None, rate: int | None) -> str:
323
+ if sample is None or not rate:
324
+ return "?"
325
+ s = float(sample) / float(rate)
326
+ return f"{int(s // 60):02d}:{s % 60:06.3f}"
327
+
328
+
329
+ def _srt_time(sec: float) -> str:
330
+ ms = int(round(sec * 1000))
331
+ h, ms = divmod(ms, 3600000)
332
+ m, ms = divmod(ms, 60000)
333
+ s, ms = divmod(ms, 1000)
334
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
335
+
336
+
337
+ def describe_result(res: dict, label: str, out_dir: Path) -> tuple[str, str | None]:
338
+ """Human-readable text for one result, plus an SRT file when timings exist."""
339
+ lines = [f"### {label}"] if label else []
340
+ rate = res.get("sample_rate")
341
+ if res.get("text"):
342
+ lines.append(res["text"])
343
+ if res.get("language"):
344
+ lines.append(f"_language: {res['language']}_")
345
+ cues = []
346
+ for key, title in (("speaker_turns", "Speaker turns"), ("segments", "Segments")):
347
+ items = res.get(key) or []
348
+ if items:
349
+ lines.append(f"\n**{title}**")
350
+ for it in items[:2000]:
351
+ who = f" speaker {it['speaker_id']}:" if "speaker_id" in it else ""
352
+ txt = f" {it['text']}" if it.get("text") else ""
353
+ conf = f" ({it['confidence']:.2f})" if isinstance(it.get("confidence"), (int, float)) and not txt else ""
354
+ lines.append(f"[{_fmt_time(it.get('start_sample'), rate)} - {_fmt_time(it.get('end_sample'), rate)}]{who}{txt}{conf}")
355
+ if rate and it.get("text"):
356
+ cues.append((it["start_sample"] / rate, it["end_sample"] / rate,
357
+ (f"[{it['speaker_id']}] " if "speaker_id" in it else "") + it["text"]))
358
+ words = res.get("words") or []
359
+ if words:
360
+ shown = " ".join(f"{w.get('word')}@{_fmt_time(w.get('start_sample'), rate)}" for w in words[:400])
361
+ lines.append(f"\n**Words** ({len(words)}): {shown}")
362
+ if not cues and rate:
363
+ group: list[dict] = []
364
+ for w in words:
365
+ group.append(w)
366
+ if len(group) >= 10 or str(w.get("word", "")).endswith((".", "?", "!")):
367
+ cues.append((group[0]["start_sample"] / rate, group[-1]["end_sample"] / rate,
368
+ " ".join(str(g.get("word", "")) for g in group)))
369
+ group = []
370
+ if group:
371
+ cues.append((group[0]["start_sample"] / rate, group[-1]["end_sample"] / rate,
372
+ " ".join(str(g.get("word", "")) for g in group)))
373
+ if res.get("custom_schema_output"):
374
+ lines.append("```json\n" + json.dumps(res["custom_schema_output"], indent=1, ensure_ascii=False)[:20000] + "\n```")
375
+ srt = None
376
+ if cues:
377
+ srt_path = out_dir / f"{re.sub(r'[^A-Za-z0-9_-]+', '_', label or 'transcript')}.srt"
378
+ srt_path.write_text("\n".join(f"{i}\n{_srt_time(a)} --> {_srt_time(b)}\n{t}\n"
379
+ for i, (a, b, t) in enumerate(cues, 1)), encoding="utf-8")
380
+ srt = str(srt_path)
381
+ return "\n".join(lines), srt
382
+
383
+
384
+ def concat_wavs(paths: list[str], out: Path) -> str | None:
385
+ if len(paths) < 2:
386
+ return None
387
+ lst = out.with_suffix(".txt")
388
+ lst.write_text("".join(f"file '{p}'\n" for p in paths))
389
+ res = subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-f", "concat", "-safe", "0",
390
+ "-i", str(lst), "-c:a", "pcm_s16le", str(out)], capture_output=True)
391
+ return str(out) if res.returncode == 0 else None
392
+
393
+
394
+ def run_task(task, family, pkg, device, gpu_seconds, text, split_mode, audio_mic, audio_files, audio_url,
395
+ voice_id, voice_ref, ref_text, instruct, lyrics, language, seed, duration, speaking_rate,
396
+ extra_json, load_json, session_json, raw_json, progress=gr.Progress()):
397
+ if SETUP_ERROR:
398
+ raise gr.Error("audio.cpp failed to start: " + SETUP_ERROR)
399
+ if not family or family not in CATALOG:
400
+ raise gr.Error("Pick a model family.")
401
+ pkg = pkg or ac.default_package(family)
402
+ work = ac.WORK_ROOT / f"{time.strftime('%Y%m%d-%H%M%S')}-{uuid.uuid4().hex[:6]}"
403
+ inputs = work / "inputs"
404
+ inputs.mkdir(parents=True, exist_ok=True)
405
+ status = []
406
+
407
+ # ---- base request
408
+ options = _parse_json(extra_json, "Model options")
409
+ base: dict = {}
410
+ if language and str(language).strip():
411
+ base["language"] = str(language).strip()
412
+ if task in VOICE_TASKS and voice_id and str(voice_id).strip():
413
+ base["voice_id"] = str(voice_id).strip()
414
+ if task in VOICE_TASKS and voice_ref:
415
+ progress(0.02, desc="Preparing reference voice")
416
+ base["voice_ref"] = ac.to_wav(voice_ref, inputs, mono=True, max_seconds=60)
417
+ if ref_text and ref_text.strip() and task in VOICE_TASKS:
418
+ base["reference_text"] = ref_text.strip()
419
+ if instruct and instruct.strip() and task in INSTRUCT_TASKS:
420
+ base["instruct"] = instruct.strip()
421
+ if task in MUSIC_TASKS:
422
+ if lyrics and lyrics.strip():
423
+ base["lyrics"] = lyrics.strip()
424
+ if duration and float(duration) > 0:
425
+ base["duration_seconds"] = float(duration)
426
+ if seed is not None and int(seed) >= 0:
427
+ base["seed"] = int(seed)
428
+ if speaking_rate and abs(float(speaking_rate) - 1.0) > 1e-6 and task in {"tts", "clone", "design"}:
429
+ base["speaking_rate"] = float(speaking_rate)
430
+ if options:
431
+ base["options"] = options
432
+ base.update(_parse_json(raw_json, "Raw request JSON"))
433
+
434
+ if task == "clone" and "voice_ref" not in base:
435
+ raise gr.Error("Voice cloning needs a reference voice recording.")
436
+ if task in {"vc", "svc"} and "voice_ref" not in base:
437
+ raise gr.Error("Voice conversion needs a target voice recording.")
438
+
439
+ # ---- requests
440
+ requests: list[dict] = []
441
+ rate, mono = ac.input_format(task, family)
442
+ audio_inputs = _collect_audio(audio_mic, audio_files, audio_url, inputs) if task in AUDIO_TASKS else []
443
+ if task in AUDIO_TASKS and not audio_inputs and task not in {"s2s", "edit"} and "audio" not in base:
444
+ raise gr.Error("Add input audio (upload, record, file, or URL).")
445
+ texts = split_text(text or "", split_mode) if task in TEXT_TASKS else []
446
+ if task in {"tts", "clone", "design", "music", "sfx", "align"} and not texts and "text" not in base:
447
+ raise gr.Error("Enter some text (or load it from a file / URL).")
448
+
449
+ if audio_inputs:
450
+ for i, src in enumerate(audio_inputs[:MAX_BATCH]):
451
+ progress(0.05 + 0.1 * i / max(1, len(audio_inputs)), desc=f"Converting {Path(src).name}")
452
+ r = dict(base)
453
+ r["audio"] = ac.to_wav(src, inputs, rate=rate, mono=mono)
454
+ if texts:
455
+ r["text"] = "\n\n".join(texts)
456
+ r["id"] = re.sub(r"[^A-Za-z0-9_-]+", "_", Path(src).stem)[:40] + f"-{i + 1}"
457
+ requests.append(r)
458
+ if len(audio_inputs) > MAX_BATCH:
459
+ status.append(f"Only the first {MAX_BATCH} audio inputs were processed.")
460
+ elif texts:
461
+ for i, t in enumerate(texts[:MAX_BATCH]):
462
+ requests.append({**base, "text": t, "id": f"clip{i + 1:02d}"})
463
+ if len(texts) > MAX_BATCH:
464
+ status.append(f"Only the first {MAX_BATCH} text chunks were processed.")
465
+ else:
466
+ requests.append({**base, "id": "out01"})
467
+
468
+ # ---- model download (CPU side, before the GPU is attached)
469
+ if not ac.is_installed(pkg):
470
+ progress(0.15, desc=f"Downloading {pkg}")
471
+ model_path = ac.install(pkg, progress=progress)
472
+ sess = {**ac.session_options_for(pkg), **_parse_json(session_json, "Session options")}
473
+ job = {"family": family, "task": task, "model_path": str(model_path), "requests": requests,
474
+ "load_options": _parse_json(load_json, "Load options"), "session_options": sess,
475
+ "out_dir": str(work), "timeout": max(30, int(gpu_seconds) - 5)}
476
+
477
+ # ---- run
478
+ progress(0.6, desc="Running on ZeroGPU" if device == "gpu" else "Running on CPU")
479
+ t0 = time.time()
480
+ try:
481
+ if device == "gpu":
482
+ try:
483
+ out = gpu_execute(job, int(gpu_seconds))
484
+ except Exception as err:
485
+ if not _looks_like_gpu_unavailable(err):
486
+ raise
487
+ status.append(f"ZeroGPU unavailable ({str(err)[:160]}). Fell back to CPU.")
488
+ job["timeout"] = 1800
489
+ out = ac.execute(job, prefer_gpu=False)
490
+ else:
491
+ job["timeout"] = 1800
492
+ out = ac.execute(job, prefer_gpu=False)
493
+ except gr.Error:
494
+ raise
495
+ except Exception as err:
496
+ raise gr.Error(str(err)[-3000:], duration=None)
497
+
498
+ # ---- outputs
499
+ files, audios, texts_out = [], [], []
500
+ for req, res in zip(requests, out["results"]):
501
+ files.extend(res.get("files", []))
502
+ if res.get("audio_file"):
503
+ audios.append(res["audio_file"])
504
+ desc, srt = describe_result(res, req["id"] if len(requests) > 1 else "", work)
505
+ if desc.strip():
506
+ texts_out.append(desc)
507
+ if srt:
508
+ files.append(srt)
509
+ combined = concat_wavs(audios, work / "combined.wav") if task in {"tts", "clone", "design"} else None
510
+ if combined:
511
+ files.insert(0, combined)
512
+ main_audio = combined or (audios[0] if audios else next((f for f in files if f.endswith(".wav")), None))
513
+ transcript = "\n\n".join(texts_out)
514
+ if transcript:
515
+ tpath = work / "result.md"
516
+ tpath.write_text(transcript, encoding="utf-8")
517
+ files.append(str(tpath))
518
+ status.insert(0, f"Done on **{out['backend'].upper()}** in {time.time() - t0:.1f}s "
519
+ f"({len(requests)} request{'s' if len(requests) != 1 else ''}, package `{pkg}`).")
520
+ details = {"backend": out["backend"], "elapsed_s": out["elapsed_s"], "package": pkg,
521
+ "model_path": str(model_path), "requests": requests, "results": out["results"]}
522
+ return (main_audio, files or None, transcript or "", details, "\n\n".join(status), out.get("log", ""),
523
+ gr.update(choices=packages_for(family), value=pkg))
524
+
525
+
526
+ # --------------------------------------------------------------------------- UI callbacks
527
+ def on_task(task):
528
+ fams = families_for(task)
529
+ fam = fams[0][1] if fams else None
530
+ is_text, is_audio = task in TEXT_TASKS, task in AUDIO_TASKS
531
+ return (
532
+ gr.update(choices=fams, value=fam),
533
+ gr.update(visible=is_text, label=TEXT_LABELS.get(task, "Text")),
534
+ gr.update(visible=is_text),
535
+ gr.update(visible=is_audio),
536
+ gr.update(visible=task in VOICE_TASKS),
537
+ gr.update(visible=task in INSTRUCT_TASKS,
538
+ label="Voice description" if task == "design" else "Instruction / style (optional)"),
539
+ gr.update(visible=task in MUSIC_TASKS),
540
+ gr.update(visible=task in {"tts", "clone", "design"}),
541
+ )
542
+
543
+
544
+ def on_family(family):
545
+ if family not in CATALOG:
546
+ return gr.update(choices=[], value=None), "", gr.update(), "{}", "{}", ""
547
+ pkg = ac.default_package(family)
548
+ f = CATALOG[family]
549
+ voices = f["voices"]
550
+ table = param_defaults(family)[2]
551
+ return (gr.update(choices=packages_for(family), value=pkg),
552
+ family_info(family, pkg),
553
+ gr.update(choices=voices, value=f["default_voice"] if f["default_voice"] in voices else None),
554
+ "{}", "{}", table)
555
+
556
+
557
+ def on_package(family, pkg):
558
+ return family_info(family, pkg)
559
+
560
+
561
+ def check_size(pkg):
562
+ if not pkg:
563
+ return "Pick a package."
564
+ size = ac.package_size(pkg)
565
+ return f"`{pkg}`: {_human(size)} ({'downloaded' if ac.is_installed(pkg) else 'not downloaded yet'})"
566
+
567
+
568
+ # --------------------------------------------------------------------------- models tab
569
+ def catalog_rows():
570
+ rows = []
571
+ for f in sorted(CATALOG.values(), key=lambda x: x["family"]):
572
+ inst = [p["id"] for p in f["packages"] if ac.is_installed(p["id"])]
573
+ rows.append([f["family"], f["display_name"], ", ".join(f["tasks"]), len(f["packages"]),
574
+ ", ".join(inst), (f["description"] or "")[:160]])
575
+ return rows
576
+
577
+
578
+ def all_package_choices():
579
+ return [(f"{p['id']} ({f['family']})", p["id"]) for f in CATALOG.values() for p in f["packages"]]
580
+
581
+
582
+ def disk_report():
583
+ used = 0
584
+ for p in ac.MODELS_ROOT.rglob("*"):
585
+ if p.is_file():
586
+ used += p.stat().st_size
587
+ free = shutil.disk_usage(ac.MODELS_ROOT).free if ac.MODELS_ROOT.exists() else 0
588
+ return f"Models folder `{ac.MODELS_ROOT}` uses {_human(used)}, {_human(free)} free."
589
+
590
+
591
+ def models_download(pkg, progress=gr.Progress()):
592
+ if not pkg:
593
+ raise gr.Error("Pick a package.")
594
+ ac.install(pkg, progress=progress)
595
+ return f"Downloaded `{pkg}`.\n\n{disk_report()}", catalog_rows()
596
+
597
+
598
+ def models_delete(pkg):
599
+ if not pkg:
600
+ raise gr.Error("Pick a package.")
601
+ ac.uninstall(pkg)
602
+ return f"Removed `{pkg}`.\n\n{disk_report()}", catalog_rows()
603
+
604
+
605
+ # --------------------------------------------------------------------------- python tab
606
+ PY_EXAMPLE = '''# Helper module: audiocpp_runtime (imported as ac). Files you write to the
607
+ # current directory show up below (audio is previewed).
608
+ import audiocpp_runtime as ac
609
+
610
+ print("tasks:", sorted({t for r in ac.list_packages() for t in r["tasks"]}))
611
+ print("GPU visible:", ac.gpu_available())
612
+
613
+ result = ac.run(
614
+ family="supertonic", task="tts",
615
+ request={"text": "Hello from audio.cpp, running inside a Hugging Face Space."},
616
+ out_dir=".",
617
+ )
618
+ print(result["backend"], result["results"][0]["files"])
619
+ '''
620
+
621
+ SECRET_HINTS = ("TOKEN", "SECRET", "PASSWORD", "API_KEY", "ACCESS_KEY", "PRIVATE")
622
+
623
+
624
+ def run_python_file(code_path: str, seconds: int) -> dict:
625
+ workdir = Path(code_path).parent
626
+ env = {k: v for k, v in os.environ.items() if not any(h in k.upper() for h in SECRET_HINTS)}
627
+ env["PYTHONPATH"] = str(APP_DIR) + (":" + env["PYTHONPATH"] if env.get("PYTHONPATH") else "")
628
+ env["MPLBACKEND"] = "Agg"
629
+ t0 = time.time()
630
+ try:
631
+ proc = subprocess.run([sys.executable, "-u", code_path], cwd=workdir, env=env,
632
+ capture_output=True, text=True, timeout=max(10, int(seconds) - 3))
633
+ out, code = proc.stdout + ("\n" + proc.stderr if proc.stderr else ""), proc.returncode
634
+ except subprocess.TimeoutExpired as err:
635
+ out = (err.stdout or "") + "\n" + (err.stderr or "") if isinstance(err.stdout, str) else ""
636
+ out += f"\n[timed out after {seconds}s]"
637
+ code = -1
638
+ files = sorted(str(p) for p in workdir.rglob("*") if p.is_file() and p.name != "main.py"
639
+ and not p.name.startswith("server-"))
640
+ return {"output": out[-50000:], "returncode": code, "files": files[:50], "seconds": round(time.time() - t0, 1)}
641
+
642
+
643
+ def run_python(code, on_gpu, seconds):
644
+ if not ALLOW_PYTHON:
645
+ raise gr.Error("The Python runner is disabled on this Space (ALLOW_PYTHON=0).")
646
+ if not code or not code.strip():
647
+ raise gr.Error("Write some code first.")
648
+ workdir = ac.WORK_ROOT / f"py-{uuid.uuid4().hex[:8]}"
649
+ workdir.mkdir(parents=True, exist_ok=True)
650
+ code_path = workdir / "main.py"
651
+ code_path.write_text(code, encoding="utf-8")
652
+ if on_gpu:
653
+ try:
654
+ res = gpu_python(str(code_path), int(seconds))
655
+ except Exception as err:
656
+ if not _looks_like_gpu_unavailable(err):
657
+ raise gr.Error(str(err)[-2000:])
658
+ res = run_python_file(str(code_path), int(seconds))
659
+ res["output"] = f"[ZeroGPU unavailable, ran on CPU: {str(err)[:160]}]\n" + res["output"]
660
+ else:
661
+ res = run_python_file(str(code_path), int(seconds))
662
+ audio = next((f for f in res["files"] if f.lower().endswith((".wav", ".mp3", ".flac", ".ogg"))), None)
663
+ header = f"exit code {res['returncode']} · {res['seconds']}s\n" + "-" * 40 + "\n"
664
+ return header + res["output"], res["files"] or None, audio
665
+
666
+
667
+ # --------------------------------------------------------------------------- layout
668
+ CSS = """
669
+ #title {margin-bottom: 0}
670
+ .small-note {font-size: 0.85em; opacity: 0.8}
671
+ """
672
+
673
+ TEXT_FILE_TYPES = [".txt", ".md", ".markdown", ".srt", ".vtt", ".json", ".csv", ".html", ".htm",
674
+ ".xml", ".pdf", ".docx", ".epub", ".rtf", ".log"]
675
+ AUDIO_FILE_TYPES = ["audio", "video", ".wav", ".mp3", ".flac", ".ogg", ".opus", ".m4a", ".aac", ".webm",
676
+ ".mp4", ".mkv", ".mov"]
677
+
678
+
679
+ def build_ui() -> gr.Blocks:
680
+ first_task = TASKS[0] if TASKS else "tts"
681
+ first_fams = families_for(first_task)
682
+ first_fam = first_fams[0][1] if first_fams else None
683
+ first_table = param_defaults(first_fam)[2] if first_fam else ""
684
+ first_voices = CATALOG.get(first_fam, {}).get("voices", []) if first_fam else []
685
+
686
+ with gr.Blocks(title="audio.cpp · ZeroGPU") as demo:
687
+ gr.Markdown(
688
+ "# audio.cpp on ZeroGPU\n"
689
+ "Run [audio.cpp](https://github.com/0xShug0/audio.cpp) (a native C++/ggml engine for 100+ audio models) "
690
+ "in the browser: TTS, voice cloning and design, ASR, alignment, diarization, VAD, stem separation, "
691
+ "voice conversion, restoration, and music or sound generation. Models download on first use from "
692
+ "[audio-cpp/audio.cpp-gguf](https://huggingface.co/audio-cpp/audio.cpp-gguf).",
693
+ elem_id="title")
694
+ if SETUP_ERROR:
695
+ gr.Markdown(f"**Startup error:** `{SETUP_ERROR}`")
696
+
697
+ with gr.Tabs():
698
+ # ------------------------------------------------------------- run tab
699
+ with gr.Tab("Run"):
700
+ with gr.Row():
701
+ task = gr.Dropdown(TASK_CHOICES, value=first_task, label="Task", scale=2)
702
+ family = gr.Dropdown(first_fams, value=first_fam, label="Model family", scale=3)
703
+ pkg = gr.Dropdown(packages_for(first_fam) if first_fam else [],
704
+ value=ac.default_package(first_fam) if first_fam else None,
705
+ label="Package / precision", scale=3)
706
+ with gr.Row():
707
+ device = gr.Radio([("ZeroGPU (CPU fallback)", "gpu"), ("CPU only", "cpu")], value="gpu",
708
+ label="Device", scale=2)
709
+ gpu_seconds = gr.Slider(30, MAX_GPU_SECONDS, value=120, step=10,
710
+ label="GPU time budget (seconds, includes model load)", scale=3)
711
+ size_btn = gr.Button("Check download size", scale=1)
712
+ info = gr.Markdown(family_info(first_fam, ac.default_package(first_fam)) if first_fam else "")
713
+
714
+ with gr.Row():
715
+ with gr.Column(scale=5):
716
+ text = gr.Textbox(label=TEXT_LABELS.get(first_task, "Text"), lines=7,
717
+ placeholder="Type or paste text here, or load it from files / a URL below.",
718
+ visible=first_task in TEXT_TASKS)
719
+ with gr.Accordion("Load text from files or a URL", open=False,
720
+ visible=first_task in TEXT_TASKS) as text_loader:
721
+ text_files = gr.File(label="Text files (txt, md, srt, vtt, pdf, docx, epub, html, json)",
722
+ file_count="multiple", file_types=TEXT_FILE_TYPES)
723
+ text_url = gr.Textbox(label="Or a web page / text URL", placeholder="https://...")
724
+ with gr.Row():
725
+ load_mode = gr.Radio(["Replace", "Append"], value="Replace", label="Load mode")
726
+ load_btn = gr.Button("Load into text box")
727
+ split_mode = gr.Radio(["Whole text", "One clip per paragraph", "One clip per line"],
728
+ value="Whole text", label="Batch mode")
729
+
730
+ with gr.Group(visible=first_task in AUDIO_TASKS) as audio_group:
731
+ audio_mic = gr.Audio(sources=["upload", "microphone"], type="filepath",
732
+ label="Input audio (upload or record)")
733
+ audio_files = gr.File(label="More audio / video files (batch, any format ffmpeg reads)",
734
+ file_count="multiple", file_types=AUDIO_FILE_TYPES)
735
+ audio_url = gr.Textbox(label="Or audio / video URLs (space or comma separated)",
736
+ placeholder="https://.../speech.mp3")
737
+ sample_audio_btn = gr.Button("Use a sample speech clip", size="sm")
738
+
739
+ with gr.Group(visible=first_task in VOICE_TASKS) as voice_group:
740
+ voice_id = gr.Dropdown(first_voices, value=None, allow_custom_value=True,
741
+ label="Built-in voice id (optional)")
742
+ voice_ref = gr.Audio(sources=["upload", "microphone"], type="filepath",
743
+ label="Reference / target voice (cloning and conversion)")
744
+ ref_text = gr.Textbox(label="Reference transcript (helps some cloning models)", lines=2)
745
+ sample_voice_btn = gr.Button("Use a sample reference voice", size="sm")
746
+
747
+ instruct = gr.Textbox(label="Instruction / style (optional)", lines=2,
748
+ visible=first_task in INSTRUCT_TASKS)
749
+ with gr.Group(visible=first_task in MUSIC_TASKS) as music_group:
750
+ lyrics = gr.Textbox(label="Lyrics (optional)", lines=5)
751
+ duration = gr.Number(label="Duration in seconds (0 = model default)", value=30)
752
+
753
+ with gr.Row():
754
+ language = gr.Textbox(label="Language (optional, e.g. en, zh, auto)", scale=2)
755
+ seed = gr.Number(label="Seed (-1 = random)", value=-1, precision=0, scale=1)
756
+ speaking_rate = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="Speaking rate",
757
+ visible=first_task in {"tts", "clone", "design"}, scale=2)
758
+
759
+ with gr.Accordion("Advanced model options", open=False):
760
+ gr.Markdown("Values here are sent as per-request options (overriding the fields above). "
761
+ "The table lists every option the model's spec declares.",
762
+ elem_classes="small-note")
763
+ extra_json = gr.Code("{}", language="json", label="Request options (JSON)", lines=6)
764
+ defaults_btn = gr.Button("Insert this model's suggested defaults", size="sm")
765
+ session_json = gr.Code("{}", language="json", label="Session options (JSON)", lines=3)
766
+ load_json = gr.Code("{}", language="json", label="Load options (JSON)", lines=3)
767
+ raw_json = gr.Code("{}", language="json",
768
+ label="Raw request fields merged last (JSON, e.g. {\"emotion\": \"happy\"})",
769
+ lines=3)
770
+ options_table = gr.Markdown(first_table)
771
+
772
+ run_btn = gr.Button("Run", variant="primary", size="lg")
773
+
774
+ with gr.Column(scale=4):
775
+ out_audio = gr.Audio(label="Output audio", type="filepath", interactive=False)
776
+ out_text = gr.Markdown(label="Text output")
777
+ out_files = gr.File(label="All output files (wav, stems, srt, midi, ...)", file_count="multiple",
778
+ interactive=False)
779
+ out_status = gr.Markdown()
780
+ with gr.Accordion("Details (JSON)", open=False):
781
+ out_json = gr.JSON()
782
+ with gr.Accordion("Server log", open=False):
783
+ out_log = gr.Textbox(lines=12, max_lines=30, show_label=False)
784
+
785
+ task.change(on_task, task, [family, text, text_loader, audio_group, voice_group, instruct,
786
+ music_group, speaking_rate])
787
+ family.change(on_family, family, [pkg, info, voice_id, extra_json, session_json, options_table])
788
+ defaults_btn.click(lambda f: param_defaults(f)[:2] if f else ("{}", "{}"), family,
789
+ [extra_json, session_json])
790
+ pkg.change(on_package, [family, pkg], info)
791
+ size_btn.click(check_size, pkg, info)
792
+ sample_audio_btn.click(lambda: sample_file("sample_16k.wav"), None, audio_mic)
793
+ sample_voice_btn.click(lambda: sample_file("b.wav"), None, voice_ref)
794
+ load_btn.click(load_text, [text_files, text_url, text, load_mode], text)
795
+ run_btn.click(
796
+ run_task,
797
+ [task, family, pkg, device, gpu_seconds, text, split_mode, audio_mic, audio_files, audio_url,
798
+ voice_id, voice_ref, ref_text, instruct, lyrics, language, seed, duration, speaking_rate,
799
+ extra_json, load_json, session_json, raw_json],
800
+ [out_audio, out_files, out_text, out_json, out_status, out_log, pkg],
801
+ concurrency_limit=2,
802
+ )
803
+
804
+ # ------------------------------------------------------------- models tab
805
+ with gr.Tab("Models"):
806
+ gr.Markdown("Browse every model family in the audio.cpp catalog and pre-download packages so the "
807
+ "first run starts faster. Downloads are lost when the Space restarts unless it has "
808
+ "persistent storage.")
809
+ with gr.Row():
810
+ m_pkg = gr.Dropdown(all_package_choices(), label="Package", scale=4, filterable=True)
811
+ m_size = gr.Button("Size", scale=1)
812
+ m_dl = gr.Button("Download", variant="primary", scale=1)
813
+ m_rm = gr.Button("Delete", variant="stop", scale=1)
814
+ m_status = gr.Markdown(disk_report() if CATALOG else "")
815
+ m_table = gr.Dataframe(catalog_rows(), headers=["family", "name", "tasks", "packages", "downloaded",
816
+ "description"],
817
+ interactive=False, wrap=True)
818
+ m_size.click(check_size, m_pkg, m_status)
819
+ m_dl.click(models_download, m_pkg, [m_status, m_table])
820
+ m_rm.click(models_delete, m_pkg, [m_status, m_table])
821
+
822
+ # ------------------------------------------------------------- python tab
823
+ with gr.Tab("Python"):
824
+ if ALLOW_PYTHON:
825
+ gr.Markdown("Run Python with the `audiocpp_runtime` helpers (`ac.run`, `ac.install`, "
826
+ "`ac.list_packages`, `ac.execute`, `ac.to_wav`). Code runs in a separate process with "
827
+ "a time limit and no Space secrets. Anything written to the working directory is "
828
+ "returned below.")
829
+ py_code = gr.Code(PY_EXAMPLE, language="python", lines=18, label="Code")
830
+ with gr.Row():
831
+ py_gpu = gr.Checkbox(value=True, label="Run inside a ZeroGPU call")
832
+ py_secs = gr.Slider(20, MAX_GPU_SECONDS, value=120, step=10, label="Time limit (seconds)")
833
+ py_btn = gr.Button("Run Python", variant="primary")
834
+ py_out = gr.Textbox(lines=16, max_lines=40, label="Output")
835
+ py_audio = gr.Audio(label="First audio file", type="filepath", interactive=False)
836
+ py_files = gr.File(label="Files", file_count="multiple", interactive=False)
837
+ py_btn.click(run_python, [py_code, py_gpu, py_secs], [py_out, py_files, py_audio],
838
+ concurrency_limit=2)
839
+ else:
840
+ gr.Markdown("The Python runner is disabled on this Space.")
841
+
842
+ # ------------------------------------------------------------- about tab
843
+ with gr.Tab("About"):
844
+ gr.Markdown(f"""
845
+ **How it works.** The Space downloads the official audio.cpp `{ac.AUDIOCPP_VERSION}` CUDA 12.8 Linux build
846
+ (the same prebuilt used by the project's Colab notebook) plus NVIDIA's CUDA 12 runtime wheels. Each run
847
+ starts `audiocpp_server` with a one-model config inside a ZeroGPU call, sends the request to `/v1/tasks/run`,
848
+ then shuts it down. If ZeroGPU is unavailable (no quota, or the Space is on CPU hardware), the same binary
849
+ runs on CPU through a small `libcuda` stub, so everything still works, only slower.
850
+
851
+ **Tips**
852
+ - The GPU budget includes model loading. Small models (Supertonic, Kokoro, PocketTTS, Qwen3-ASR 0.6B,
853
+ HTDemucs) finish in a few seconds; 4B to 8B models may need 120 s or more.
854
+ - Use **Batch mode** to turn each paragraph or line into its own clip; TTS clips are also joined into
855
+ `combined.wav`. ASR, separation and the other audio tasks accept many files at once.
856
+ - Some families expose extra controls; open **Advanced model options** for the full option table.
857
+ - Packages marked `[safetensors]` may need a conversion step that this Space does not run. Prefer GGUF.
858
+
859
+ **Credits.** audio.cpp by [0xShug0](https://github.com/0xShug0) and contributors (Apache-2.0). Model weights
860
+ keep their original licenses; see the project's
861
+ [model license list](https://github.com/0xShug0/audio.cpp/blob/main/docs/model_licenses.md).
862
+ Per-model option defaults come from the project's WebUI config.
863
+ """)
864
+ return demo
865
+
866
+
867
+ demo = build_ui()
868
+
869
+ if __name__ == "__main__":
870
+ demo.queue(default_concurrency_limit=4, max_size=64).launch(
871
+ theme=gr.themes.Soft(), css=CSS, ssr_mode=False, show_error=True,
872
+ allowed_paths=[str(ac.WORK_ROOT), str(SAMPLE_DIR)],
873
+ )