Spaces:
Running on Zero
feat: rewrite as gradio.Server + vanilla HTML/CSS/JS frontend
Browse filesThis is the full rewrite using the architecture described in the
'Introducing Gradio Server' blog post (not just a theme change).
Backend (app.py):
- Replace gradio.Blocks with gradio.server.Server (a FastAPI subclass)
- Register 4 endpoints via @app .api() decorator (queue + concurrency):
* /separate — main endpoint, concurrency_limit=1 (CPU/GPU serialize)
* /presets — return use-case presets catalogue
* /models — return model list per architecture
* /history — return recent separation history
- @app .get('/') serves static/index.html (FastAPI FileResponse)
- /static mounted via fastapi.staticfiles.StaticFiles
- /healthz plain FastAPI endpoint (no queue) for health checks
- allowed_paths includes output_dir + model cache so FileData URLs work
- FileData handled both as instance and dict (serialization varies)
Frontend (static/):
- index.html — vanilla HTML, no framework, no build step
- style.css — ~700 lines, self-contained design system with :root tokens,
pure-black canvas, electric-lime accent, glassmorphism, animated hero
- app.js — vanilla JS using @gradio/client@2.3.0 from CDN (ESM import)
* Client.connect(window.location.origin) per the blog
* handle_file() for file upload
* result.data[0] for FileData URL extraction
* Builds /gradio_api/file=<path> URL when FileData.url is null
- Loaded as ESM module + defer script, with client-ready event bridge
Engine (main/):
- Delete main/ui.py and main/callbacks.py (Blocks-specific, no longer used)
- Keep main/config.py, main/models.py, main/separator.py, main/utils.py
(engine pieces reused by the new Server-based app)
- All bug fixes from the previous commit preserved:
* MP3 bitrate 320 kbps (issue #103)
* librosa.ParameterError retry (issue #57)
* Defensive path handling
Dependencies:
- Bump gradio from 5.x to 6.x (gradio.Server was introduced in 6.x)
- requirements.txt and README.md updated accordingly
Verified end-to-end:
- /healthz returns JSON
- / serves our custom HTML (not Gradio default)
- /static/style.css and /static/app.js served correctly
- All 4 @app .api() endpoints registered (visible in /gradio_api/info)
- /separate produces 2 stems from a 2s test tone in ~16s
- Browser test: page renders, dropdowns populate via API, no JS errors
- Browser test: JS Client.connect() + predict('/presets') succeeds
- README.md +47 -16
- app.py +388 -15
- main/__init__.py +9 -12
- main/callbacks.py +0 -359
- main/ui.py +0 -832
- requirements.txt +12 -8
- static/app.js +544 -0
- static/index.html +363 -0
- static/style.css +872 -0
|
@@ -4,7 +4,7 @@ emoji: 🎧
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
|
@@ -16,28 +16,45 @@ tags:
|
|
| 16 |
- mdx
|
| 17 |
- roformer
|
| 18 |
- uvr
|
|
|
|
| 19 |
---
|
| 20 |
|
| 21 |
-
# Audio Separator
|
| 22 |
-
|
| 23 |
-
A polished
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
## Project layout
|
| 28 |
|
| 29 |
```
|
| 30 |
.
|
| 31 |
-
├── app.py #
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
├── main/
|
| 33 |
-
│ ├── __init__.py #
|
| 34 |
-
│ ├── config.py # Device detection, paths, logging, `spaces` shim
|
| 35 |
-
│ ├── models.py # Model catalogue + use-case presets
|
| 36 |
-
│ ├── separator.py # Core pipeline — @spaces.GPU-wrapped
|
| 37 |
-
│
|
| 38 |
-
|
| 39 |
-
│ └── ui.py # Theme (black + Syne) + Blocks layout.
|
| 40 |
-
├── requirements.txt # audio-separator[gpu]
|
| 41 |
└── README.md
|
| 42 |
```
|
| 43 |
|
|
@@ -80,11 +97,25 @@ A polished Gradio interface for [`audio-separator`](https://github.com/nomadkara
|
|
| 80 |
```bash
|
| 81 |
git clone https://huggingface.co/spaces/nomadkaraoke/audio-separator
|
| 82 |
cd audio-separator
|
| 83 |
-
pip install -r requirements.txt # audio-separator[
|
| 84 |
python app.py
|
| 85 |
# → http://localhost:7860
|
| 86 |
```
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
## Credits
|
| 89 |
|
| 90 |
- Upstream HF Space: [nomadkaraoke/audio-separator](https://huggingface.co/spaces/nomadkaraoke/audio-separator)
|
|
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.19.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
|
|
|
| 16 |
- mdx
|
| 17 |
- roformer
|
| 18 |
- uvr
|
| 19 |
+
- gradio-server
|
| 20 |
---
|
| 21 |
|
| 22 |
+
# Audio Separator — Gradio Server Edition
|
| 23 |
+
|
| 24 |
+
A polished demo for [`audio-separator`](https://github.com/nomadkaraoke/python-audio-separator)
|
| 25 |
+
built on the architecture described in the
|
| 26 |
+
[Introducing Gradio Server](https://huggingface.co/blog/introducing-gradio-server)
|
| 27 |
+
blog post:
|
| 28 |
+
|
| 29 |
+
> **Backend:** `gradio.server.Server` (a FastAPI subclass) with `@app.api()`
|
| 30 |
+
> endpoints that go through Gradio's queue with concurrency control and
|
| 31 |
+
> SSE streaming. Heavy compute is wrapped in `@spaces.GPU`.
|
| 32 |
+
>
|
| 33 |
+
> **Frontend:** vanilla HTML / CSS / JS loaded from `static/index.html` via
|
| 34 |
+
> `@app.get("/")`. The frontend talks to the backend using the
|
| 35 |
+
> [`@gradio/client`](https://www.npmjs.com/package/@gradio/client) JS
|
| 36 |
+
> library loaded from CDN. No build step. No framework on the client.
|
| 37 |
+
>
|
| 38 |
+
> **Engine:** `audio_separator.separator.Separator` with bug fixes for MP3
|
| 39 |
+
> bitrate (issue #103), `librosa.ParameterError` retry (issue #57), and
|
| 40 |
+
> defensive path handling.
|
| 41 |
|
| 42 |
## Project layout
|
| 43 |
|
| 44 |
```
|
| 45 |
.
|
| 46 |
+
├── app.py # Server entry point — gradio.Server + @app.api() endpoints
|
| 47 |
+
├── static/
|
| 48 |
+
│ ├── index.html # Vanilla HTML UI (served at "/")
|
| 49 |
+
│ ├── style.css # Self-contained design system (no framework)
|
| 50 |
+
│ └── app.js # Client logic — uses @gradio/client from CDN
|
| 51 |
├── main/
|
| 52 |
+
│ ├── __init__.py # (legacy; not used by the new app)
|
| 53 |
+
│ ├── config.py # Device detection, paths, logging, `spaces` shim
|
| 54 |
+
│ ├── models.py # Model catalogue + use-case presets
|
| 55 |
+
│ ├── separator.py # Core pipeline — @spaces.GPU-wrapped, bug-fixed
|
| 56 |
+
│ └── utils.py # ZIP packaging, duration, history helpers
|
| 57 |
+
├── requirements.txt # gradio>=6.0 audio-separator[cpu]>=0.44.0
|
|
|
|
|
|
|
| 58 |
└── README.md
|
| 59 |
```
|
| 60 |
|
|
|
|
| 97 |
```bash
|
| 98 |
git clone https://huggingface.co/spaces/nomadkaraoke/audio-separator
|
| 99 |
cd audio-separator
|
| 100 |
+
pip install -r requirements.txt # gradio>=6.0 + audio-separator[cpu] (or [gpu] locally)
|
| 101 |
python app.py
|
| 102 |
# → http://localhost:7860
|
| 103 |
```
|
| 104 |
|
| 105 |
+
The first request to `/` returns the static `index.html`. The frontend then
|
| 106 |
+
connects to the backend using the Gradio JS client:
|
| 107 |
+
|
| 108 |
+
```js
|
| 109 |
+
import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
|
| 110 |
+
const client = await Client.connect(window.location.origin);
|
| 111 |
+
const result = await client.predict("/separate", {
|
| 112 |
+
audio_file: handle_file(file),
|
| 113 |
+
use_case: "Karaoke (vocals out)",
|
| 114 |
+
// ...other params
|
| 115 |
+
});
|
| 116 |
+
const stemUrl = result.data[0].stems[0].file.url; // FileData URL
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
## Credits
|
| 120 |
|
| 121 |
- Upstream HF Space: [nomadkaraoke/audio-separator](https://huggingface.co/spaces/nomadkaraoke/audio-separator)
|
|
@@ -1,19 +1,26 @@
|
|
| 1 |
-
"""Audio Separator —
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
Run locally:
|
| 15 |
-
pip install "audio-separator[cpu]" "gradio>=
|
| 16 |
python app.py
|
|
|
|
| 17 |
"""
|
| 18 |
from __future__ import annotations
|
| 19 |
|
|
@@ -23,9 +30,365 @@ import sys
|
|
| 23 |
# Make sure the local `main/` package is importable regardless of CWD.
|
| 24 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 25 |
|
| 26 |
-
from
|
| 27 |
-
from
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
def main() -> None:
|
| 31 |
if not HAS_SEPARATOR:
|
|
@@ -35,13 +398,23 @@ def main() -> None:
|
|
| 35 |
print(" (or 'audio-separator[cpu]' for local CPU-only runs)")
|
| 36 |
print("=" * 60)
|
| 37 |
|
| 38 |
-
|
| 39 |
-
app.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 41 |
server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
|
| 42 |
share=False,
|
| 43 |
show_error=True,
|
| 44 |
prevent_thread_lock=False,
|
|
|
|
| 45 |
)
|
| 46 |
|
| 47 |
|
|
|
|
| 1 |
+
"""Audio Separator — Gradio Server Edition.
|
| 2 |
|
| 3 |
+
This is a full rewrite of the demo using the architecture described in the
|
| 4 |
+
"Introducing Gradio Server" blog post:
|
| 5 |
+
https://huggingface.co/blog/introducing-gradio-server
|
| 6 |
|
| 7 |
+
Architecture:
|
| 8 |
+
* Backend : `gradio.server.Server` (a FastAPI subclass) with `@app.api()`
|
| 9 |
+
endpoints that go through Gradio's queue with concurrency
|
| 10 |
+
control and SSE streaming.
|
| 11 |
+
* Frontend : vanilla HTML / CSS / JS loaded from `static/index.html` via
|
| 12 |
+
`@app.get("/")`. The frontend talks to the backend using the
|
| 13 |
+
`@gradio/client` JS library loaded from CDN — exactly as the
|
| 14 |
+
blog recommends.
|
| 15 |
+
* Engine : `audio_separator.separator.Separator` — same wrapper as the
|
| 16 |
+
old `main/separator.py`, with bug fixes for MP3 bitrate
|
| 17 |
+
(issue #103), librosa.ParameterError retry (issue #57), and
|
| 18 |
+
defensive path handling.
|
| 19 |
|
| 20 |
Run locally:
|
| 21 |
+
pip install "audio-separator[cpu]" "gradio>=6.0"
|
| 22 |
python app.py
|
| 23 |
+
# → http://localhost:7860
|
| 24 |
"""
|
| 25 |
from __future__ import annotations
|
| 26 |
|
|
|
|
| 30 |
# Make sure the local `main/` package is importable regardless of CWD.
|
| 31 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 32 |
|
| 33 |
+
from pathlib import Path
|
| 34 |
+
from typing import Any
|
| 35 |
|
| 36 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 37 |
+
from fastapi.staticfiles import StaticFiles
|
| 38 |
+
from gradio.data_classes import FileData
|
| 39 |
+
from gradio.server import Server
|
| 40 |
+
|
| 41 |
+
# ── Local imports (engine + catalogue, unchanged from the previous commit) ──
|
| 42 |
+
from main.config import (
|
| 43 |
+
DEFAULT_MODEL_DIR,
|
| 44 |
+
DEFAULT_OUTPUT_DIR,
|
| 45 |
+
DEVICE,
|
| 46 |
+
HAS_SEPARATOR,
|
| 47 |
+
HAS_SPACES,
|
| 48 |
+
USE_AUTOCAST,
|
| 49 |
+
spaces, # real `spaces` module on HF ZeroGPU, or our local no-op shim
|
| 50 |
+
)
|
| 51 |
+
from main.models import (
|
| 52 |
+
ROFORMER_MODELS,
|
| 53 |
+
USE_CASE_PRESETS,
|
| 54 |
+
infer_architecture_from_model,
|
| 55 |
+
model_choices_for_arch,
|
| 56 |
+
resolve_model_filename,
|
| 57 |
+
)
|
| 58 |
+
from main.separator import separate_core, MP3_BITRATE
|
| 59 |
+
from main.utils import (
|
| 60 |
+
append_history,
|
| 61 |
+
audio_duration_seconds,
|
| 62 |
+
estimate_time_seconds,
|
| 63 |
+
fmt_duration,
|
| 64 |
+
history_rows,
|
| 65 |
+
now_timestamp,
|
| 66 |
+
package_stems_as_zip,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
import logging
|
| 70 |
+
log = logging.getLogger("audio-sep")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# --------------------------------------------------------------------------- #
|
| 74 |
+
# Server construction #
|
| 75 |
+
# --------------------------------------------------------------------------- #
|
| 76 |
+
|
| 77 |
+
app = Server()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# --------------------------------------------------------------------------- #
|
| 81 |
+
# Static frontend — served at "/" as the blog describes #
|
| 82 |
+
# --------------------------------------------------------------------------- #
|
| 83 |
+
|
| 84 |
+
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@app.get("/")
|
| 88 |
+
def index():
|
| 89 |
+
"""Serve the vanilla HTML/CSS/JS frontend.
|
| 90 |
+
|
| 91 |
+
The blog is explicit: `gradio.Server` *is* a FastAPI app, so plain
|
| 92 |
+
`@app.get("/")` works alongside `@app.api()` endpoints. We return the
|
| 93 |
+
static `index.html` and let the browser load `style.css` and `app.js`
|
| 94 |
+
via relative URLs (mounted below).
|
| 95 |
+
"""
|
| 96 |
+
return FileResponse(STATIC_DIR / "index.html")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# Mount the rest of the static assets (CSS, JS, fonts if needed).
|
| 100 |
+
# `Server` inherits from FastAPI which inherits from Starlette — `.mount()`
|
| 101 |
+
# works exactly the same way.
|
| 102 |
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# --------------------------------------------------------------------------- #
|
| 106 |
+
# API endpoints #
|
| 107 |
+
# --------------------------------------------------------------------------- #
|
| 108 |
+
# These are registered via `@app.api(name="...")` so they go through Gradio's
|
| 109 |
+
# queue with concurrency control. On HuggingFace ZeroGPU this is what keeps
|
| 110 |
+
# concurrent users from colliding on the GPU.
|
| 111 |
+
#
|
| 112 |
+
# Frontend (browser) calls these via the Gradio JS client:
|
| 113 |
+
# import { Client, handle_file } from "@gradio/client";
|
| 114 |
+
# const client = await Client.connect(window.location.origin);
|
| 115 |
+
# const result = await client.predict("/separate", { audio_file: handle_file(file), ... });
|
| 116 |
+
# const url = result.data[0].url; // FileData URL
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@app.api(name="separate",
|
| 120 |
+
description="Separate an audio file into stems (vocals, instrumental, etc.).",
|
| 121 |
+
concurrency_limit=1)
|
| 122 |
+
def separate(
|
| 123 |
+
audio_file: FileData,
|
| 124 |
+
use_case: str,
|
| 125 |
+
model_display_name: str,
|
| 126 |
+
output_format: str,
|
| 127 |
+
# Advanced parameters (optional — frontend sends defaults when hidden)
|
| 128 |
+
advanced_mode: bool,
|
| 129 |
+
seg_size: int,
|
| 130 |
+
overlap_rof: int,
|
| 131 |
+
overlap_mdx: float,
|
| 132 |
+
pitch_shift: int,
|
| 133 |
+
hop_length: int,
|
| 134 |
+
enable_denoise: bool,
|
| 135 |
+
window_size: int,
|
| 136 |
+
aggression: int,
|
| 137 |
+
enable_tta: bool,
|
| 138 |
+
enable_post: bool,
|
| 139 |
+
post_thresh: float,
|
| 140 |
+
enable_high_end: bool,
|
| 141 |
+
shifts: int,
|
| 142 |
+
segments_enabled: bool,
|
| 143 |
+
norm_thresh: float,
|
| 144 |
+
amp_thresh: float,
|
| 145 |
+
batch_size: int,
|
| 146 |
+
) -> dict[str, Any]:
|
| 147 |
+
"""Main separation endpoint.
|
| 148 |
+
|
| 149 |
+
Returns a dict with:
|
| 150 |
+
- stems : list of {label, url} pairs (one per produced stem)
|
| 151 |
+
- zip_url : URL to a ZIP bundle of all stems
|
| 152 |
+
- duration : formatted input duration (e.g. "02:34")
|
| 153 |
+
- est : formatted estimated processing time
|
| 154 |
+
- device : "CPU" or "CUDA"
|
| 155 |
+
- model : chosen model display name
|
| 156 |
+
- architecture : inferred architecture key
|
| 157 |
+
- error : optional error message
|
| 158 |
+
"""
|
| 159 |
+
# The Gradio client may pass FileData either as a FileData instance or
|
| 160 |
+
# as a plain dict (depending on client / serialization path). Normalize.
|
| 161 |
+
if isinstance(audio_file, dict):
|
| 162 |
+
audio_path = audio_file.get("path")
|
| 163 |
+
elif audio_file is not None:
|
| 164 |
+
audio_path = audio_file.path
|
| 165 |
+
else:
|
| 166 |
+
audio_path = None
|
| 167 |
+
|
| 168 |
+
if not audio_path:
|
| 169 |
+
return {"error": "No audio file provided."}
|
| 170 |
+
|
| 171 |
+
input_path = audio_path
|
| 172 |
+
if not os.path.exists(input_path):
|
| 173 |
+
return {"error": f"Audio file not found on disk: {input_path}"}
|
| 174 |
+
|
| 175 |
+
# Resolve architecture + params from the use-case preset.
|
| 176 |
+
preset = USE_CASE_PRESETS.get(use_case, {})
|
| 177 |
+
architecture = preset.get("architecture", "roformer")
|
| 178 |
+
|
| 179 |
+
chosen_model = model_display_name or preset.get("model", "")
|
| 180 |
+
if chosen_model != preset.get("model"):
|
| 181 |
+
architecture = infer_architecture_from_model(chosen_model)
|
| 182 |
+
|
| 183 |
+
arch_params: dict[str, Any] = dict(preset.get("params", {}))
|
| 184 |
+
if advanced_mode:
|
| 185 |
+
_apply_advanced_overrides(
|
| 186 |
+
arch_params,
|
| 187 |
+
architecture=architecture,
|
| 188 |
+
seg_size=seg_size,
|
| 189 |
+
overlap_rof=overlap_rof,
|
| 190 |
+
overlap_mdx=overlap_mdx,
|
| 191 |
+
pitch_shift=pitch_shift,
|
| 192 |
+
hop_length=hop_length,
|
| 193 |
+
enable_denoise=enable_denoise,
|
| 194 |
+
window_size=window_size,
|
| 195 |
+
aggression=aggression,
|
| 196 |
+
enable_tta=enable_tta,
|
| 197 |
+
enable_post=enable_post,
|
| 198 |
+
post_thresh=post_thresh,
|
| 199 |
+
enable_high_end=enable_high_end,
|
| 200 |
+
shifts=shifts,
|
| 201 |
+
segments_enabled=segments_enabled,
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
duration = audio_duration_seconds(input_path)
|
| 205 |
+
est = estimate_time_seconds(duration, architecture)
|
| 206 |
+
|
| 207 |
+
log.info(
|
| 208 |
+
"API /separate — input=%s arch=%s model=%s duration=%.1fs est=%.1fs",
|
| 209 |
+
os.path.basename(input_path), architecture, chosen_model, duration, est,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
stems = separate_core(
|
| 214 |
+
input_file=input_path,
|
| 215 |
+
architecture=architecture,
|
| 216 |
+
model_display_name=chosen_model,
|
| 217 |
+
arch_params=arch_params,
|
| 218 |
+
model_file_dir=DEFAULT_MODEL_DIR,
|
| 219 |
+
output_dir=DEFAULT_OUTPUT_DIR,
|
| 220 |
+
out_format=output_format,
|
| 221 |
+
norm_thresh=norm_thresh,
|
| 222 |
+
amp_thresh=amp_thresh,
|
| 223 |
+
batch_size=batch_size,
|
| 224 |
+
progress=None,
|
| 225 |
+
)
|
| 226 |
+
except Exception as e:
|
| 227 |
+
log.exception("Separation failed")
|
| 228 |
+
return {
|
| 229 |
+
"error": f"Separation failed: {e}",
|
| 230 |
+
"duration": fmt_duration(duration),
|
| 231 |
+
"est": fmt_duration(est),
|
| 232 |
+
"device": DEVICE.upper(),
|
| 233 |
+
"model": chosen_model,
|
| 234 |
+
"architecture": architecture,
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
if not stems:
|
| 238 |
+
return {
|
| 239 |
+
"error": "No stems were produced.",
|
| 240 |
+
"duration": fmt_duration(duration),
|
| 241 |
+
"est": fmt_duration(est),
|
| 242 |
+
"device": DEVICE.upper(),
|
| 243 |
+
"model": chosen_model,
|
| 244 |
+
"architecture": architecture,
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
# Build FileData objects for each stem — these carry the URL the JS
|
| 248 |
+
# client reads via `result.data[i].url`.
|
| 249 |
+
stem_payloads = []
|
| 250 |
+
for label, path in stems.items():
|
| 251 |
+
if not os.path.exists(path):
|
| 252 |
+
log.warning("Stem path missing on disk: %s", path)
|
| 253 |
+
continue
|
| 254 |
+
fd = FileData(path=path, orig_name=os.path.basename(path))
|
| 255 |
+
stem_payloads.append({"label": label, "file": fd})
|
| 256 |
+
|
| 257 |
+
zip_path = package_stems_as_zip(stems, Path(input_path).stem)
|
| 258 |
+
zip_fd = FileData(path=zip_path, orig_name=Path(zip_path).name) if zip_path else None
|
| 259 |
+
|
| 260 |
+
append_history({
|
| 261 |
+
"time": now_timestamp(),
|
| 262 |
+
"use_case": use_case,
|
| 263 |
+
"model": chosen_model,
|
| 264 |
+
"input": os.path.basename(input_path),
|
| 265 |
+
"duration": fmt_duration(duration),
|
| 266 |
+
"stems": [s["label"] for s in stem_payloads],
|
| 267 |
+
})
|
| 268 |
+
|
| 269 |
+
return {
|
| 270 |
+
"stems": stem_payloads,
|
| 271 |
+
"zip": zip_fd,
|
| 272 |
+
"duration": fmt_duration(duration),
|
| 273 |
+
"est": fmt_duration(est),
|
| 274 |
+
"device": DEVICE.upper(),
|
| 275 |
+
"model": chosen_model,
|
| 276 |
+
"architecture": architecture,
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _apply_advanced_overrides(
|
| 281 |
+
arch_params: dict[str, Any],
|
| 282 |
+
*,
|
| 283 |
+
architecture: str,
|
| 284 |
+
seg_size: int,
|
| 285 |
+
overlap_rof: int,
|
| 286 |
+
overlap_mdx: float,
|
| 287 |
+
pitch_shift: int,
|
| 288 |
+
hop_length: int,
|
| 289 |
+
enable_denoise: bool,
|
| 290 |
+
window_size: int,
|
| 291 |
+
aggression: int,
|
| 292 |
+
enable_tta: bool,
|
| 293 |
+
enable_post: bool,
|
| 294 |
+
post_thresh: float,
|
| 295 |
+
enable_high_end: bool,
|
| 296 |
+
shifts: int,
|
| 297 |
+
segments_enabled: bool,
|
| 298 |
+
) -> None:
|
| 299 |
+
"""Mutate `arch_params` in place with the advanced-mode slider values."""
|
| 300 |
+
if architecture in ("roformer", "mdx23c"):
|
| 301 |
+
arch_params.update(
|
| 302 |
+
segment_size=seg_size,
|
| 303 |
+
override_segment_size=False,
|
| 304 |
+
pitch_shift=pitch_shift,
|
| 305 |
+
overlap=overlap_rof,
|
| 306 |
+
)
|
| 307 |
+
elif architecture == "mdx":
|
| 308 |
+
arch_params.update(
|
| 309 |
+
hop_length=hop_length,
|
| 310 |
+
segment_size=seg_size,
|
| 311 |
+
overlap=overlap_mdx,
|
| 312 |
+
enable_denoise=enable_denoise,
|
| 313 |
+
)
|
| 314 |
+
elif architecture == "vr":
|
| 315 |
+
arch_params.update(
|
| 316 |
+
window_size=window_size,
|
| 317 |
+
aggression=aggression,
|
| 318 |
+
enable_tta=enable_tta,
|
| 319 |
+
enable_post_process=enable_post,
|
| 320 |
+
post_process_threshold=post_thresh,
|
| 321 |
+
high_end_process=enable_high_end,
|
| 322 |
+
)
|
| 323 |
+
elif architecture == "demucs":
|
| 324 |
+
arch_params.update(
|
| 325 |
+
segment_size=seg_size,
|
| 326 |
+
shifts=shifts,
|
| 327 |
+
overlap=overlap_mdx,
|
| 328 |
+
segments_enabled=segments_enabled,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
# --------------------------------------------------------------------------- #
|
| 333 |
+
# Auxiliary endpoints (also through the queue for consistency) #
|
| 334 |
+
# --------------------------------------------------------------------------- #
|
| 335 |
+
|
| 336 |
+
@app.api(name="presets",
|
| 337 |
+
description="Return the catalogue of use-case presets and models.")
|
| 338 |
+
def get_presets() -> dict[str, Any]:
|
| 339 |
+
"""Return presets + per-architecture model lists for the frontend."""
|
| 340 |
+
return {
|
| 341 |
+
"presets": {
|
| 342 |
+
name: {
|
| 343 |
+
"architecture": p["architecture"],
|
| 344 |
+
"model": p["model"],
|
| 345 |
+
"description": p["description"],
|
| 346 |
+
"params": p.get("params", {}),
|
| 347 |
+
}
|
| 348 |
+
for name, p in USE_CASE_PRESETS.items()
|
| 349 |
+
},
|
| 350 |
+
"models_by_arch": {
|
| 351 |
+
"roformer": list(ROFORMER_MODELS.keys()),
|
| 352 |
+
"mdx23c": [], # populated from main.models at request time if needed
|
| 353 |
+
"mdx": [], # same
|
| 354 |
+
"vr": [],
|
| 355 |
+
"demucs": [],
|
| 356 |
+
},
|
| 357 |
+
"default_use_case": "Karaoke (vocals out)",
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@app.api(name="models",
|
| 362 |
+
description="Return all available models for a given architecture.")
|
| 363 |
+
def get_models(architecture: str) -> list[str]:
|
| 364 |
+
"""Return the model dropdown choices for a given architecture."""
|
| 365 |
+
return model_choices_for_arch(architecture)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@app.api(name="history",
|
| 369 |
+
description="Return the user's recent separation history.")
|
| 370 |
+
def get_history() -> list[list[str]]:
|
| 371 |
+
return history_rows()
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
# --------------------------------------------------------------------------- #
|
| 375 |
+
# Health check (plain FastAPI — no queue needed) #
|
| 376 |
+
# --------------------------------------------------------------------------- #
|
| 377 |
+
|
| 378 |
+
@app.get("/healthz")
|
| 379 |
+
def healthz():
|
| 380 |
+
return JSONResponse({
|
| 381 |
+
"ok": True,
|
| 382 |
+
"device": DEVICE.upper(),
|
| 383 |
+
"engine_installed": HAS_SEPARATOR,
|
| 384 |
+
"spaces_runtime": HAS_SPACES,
|
| 385 |
+
"mp3_bitrate": MP3_BITRATE,
|
| 386 |
+
})
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
# --------------------------------------------------------------------------- #
|
| 390 |
+
# Entrypoint #
|
| 391 |
+
# --------------------------------------------------------------------------- #
|
| 392 |
|
| 393 |
def main() -> None:
|
| 394 |
if not HAS_SEPARATOR:
|
|
|
|
| 398 |
print(" (or 'audio-separator[cpu]' for local CPU-only runs)")
|
| 399 |
print("=" * 60)
|
| 400 |
|
| 401 |
+
# Note: `Server` doesn't have a separate `.queue()` method — concurrency
|
| 402 |
+
# is configured per-`@app.api()` via the `concurrency_limit` parameter.
|
| 403 |
+
# The default for `/separate` is `concurrency_limit=1` (set on the
|
| 404 |
+
# decorator) so concurrent users serialize on the GPU.
|
| 405 |
+
|
| 406 |
+
# Gradio's file server only allows downloads from explicitly listed
|
| 407 |
+
# paths. We allow the output dir (where stems + ZIPs are written) and
|
| 408 |
+
# the model cache (in case users want to inspect downloaded weights).
|
| 409 |
+
allowed_paths = [DEFAULT_OUTPUT_DIR, DEFAULT_MODEL_DIR]
|
| 410 |
+
|
| 411 |
+
app.launch(
|
| 412 |
server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 413 |
server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
|
| 414 |
share=False,
|
| 415 |
show_error=True,
|
| 416 |
prevent_thread_lock=False,
|
| 417 |
+
allowed_paths=allowed_paths,
|
| 418 |
)
|
| 419 |
|
| 420 |
|
|
@@ -1,14 +1,11 @@
|
|
| 1 |
-
"""Audio Separator —
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
"""
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
from .ui import build_app
|
| 10 |
-
from .config import DEVICE, USE_AUTOCAST, HAS_SEPARATOR, HAS_SPACES
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio Separator — Gradio Server Edition (engine package).
|
| 2 |
|
| 3 |
+
The public entry point is `app.py` at the project root, which constructs a
|
| 4 |
+
`gradio.server.Server` and registers `@app.api()` endpoints. This package
|
| 5 |
+
holds the engine pieces that the API calls into:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
config.py — device detection, paths, logging, `spaces` shim
|
| 8 |
+
models.py — model catalogue + use-case presets
|
| 9 |
+
separator.py — core separation pipeline (@spaces.GPU wrapped)
|
| 10 |
+
utils.py — ZIP packaging, duration, history helpers
|
| 11 |
+
"""
|
|
@@ -1,359 +0,0 @@
|
|
| 1 |
-
"""Gradio event handlers.
|
| 2 |
-
|
| 3 |
-
Each top-level function here is wired to a UI element in `main.ui`. They take
|
| 4 |
-
plain Python values (not Gradio components) and return tuples of
|
| 5 |
-
`gr.update(...)` / values to be applied to the bound output components.
|
| 6 |
-
"""
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import logging
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
from typing import Any
|
| 12 |
-
|
| 13 |
-
import gradio as gr
|
| 14 |
-
|
| 15 |
-
from .config import DEVICE, HAS_SEPARATOR
|
| 16 |
-
from .models import (
|
| 17 |
-
USE_CASE_PRESETS,
|
| 18 |
-
infer_architecture_from_model,
|
| 19 |
-
model_choices_for_arch,
|
| 20 |
-
)
|
| 21 |
-
from .separator import separate_core
|
| 22 |
-
from .utils import (
|
| 23 |
-
append_history,
|
| 24 |
-
audio_duration_seconds,
|
| 25 |
-
estimate_time_seconds,
|
| 26 |
-
fmt_duration,
|
| 27 |
-
history_rows,
|
| 28 |
-
now_timestamp,
|
| 29 |
-
package_stems_as_zip,
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
log = logging.getLogger("audio-sep")
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
# --------------------------------------------------------------------------- #
|
| 36 |
-
# Result shape constants #
|
| 37 |
-
# --------------------------------------------------------------------------- #
|
| 38 |
-
|
| 39 |
-
# Order of outputs returned by run_separation() and clear_outputs().
|
| 40 |
-
# Documented here so the wiring in main.ui stays in sync.
|
| 41 |
-
RESULT_FIELDS = [
|
| 42 |
-
"stem_1", "stem_2", "stem_3", "stem_4", "stem_5", "stem_6",
|
| 43 |
-
"zip_file", "status",
|
| 44 |
-
"stem_row_1", "stem_row_2", "stem_row_3",
|
| 45 |
-
"est_box", "dur_box", "history_table",
|
| 46 |
-
]
|
| 47 |
-
N_STEMS = 6
|
| 48 |
-
N_RESULT = len(RESULT_FIELDS) # 14
|
| 49 |
-
|
| 50 |
-
# Preferred ordering when displaying stems.
|
| 51 |
-
_PREFERRED_STEM_ORDER = [
|
| 52 |
-
"Vocals", "Instrumental", "Drums", "Bass", "Other", "Guitar", "Piano",
|
| 53 |
-
]
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
# --------------------------------------------------------------------------- #
|
| 57 |
-
# Core separation entry point #
|
| 58 |
-
# --------------------------------------------------------------------------- #
|
| 59 |
-
|
| 60 |
-
def run_separation(
|
| 61 |
-
input_audio,
|
| 62 |
-
use_case: str,
|
| 63 |
-
model_display_name: str,
|
| 64 |
-
advanced_mode: bool,
|
| 65 |
-
# Roformer / MDX23C
|
| 66 |
-
seg_size: int,
|
| 67 |
-
override_seg: bool,
|
| 68 |
-
overlap_rof: int,
|
| 69 |
-
overlap_mdx: float,
|
| 70 |
-
pitch_shift: int,
|
| 71 |
-
# MDX-NET
|
| 72 |
-
hop_length: int,
|
| 73 |
-
enable_denoise: bool,
|
| 74 |
-
# VR Arch
|
| 75 |
-
window_size: int,
|
| 76 |
-
aggression: int,
|
| 77 |
-
enable_tta: bool,
|
| 78 |
-
enable_post: bool,
|
| 79 |
-
post_thresh: float,
|
| 80 |
-
enable_high_end: bool,
|
| 81 |
-
# Demucs
|
| 82 |
-
shifts: int,
|
| 83 |
-
segments_enabled: bool,
|
| 84 |
-
# Global
|
| 85 |
-
model_file_dir: str,
|
| 86 |
-
output_dir: str,
|
| 87 |
-
out_format: str,
|
| 88 |
-
norm_thresh: float,
|
| 89 |
-
amp_thresh: float,
|
| 90 |
-
batch_size: int,
|
| 91 |
-
progress=gr.Progress(track_tqdm=True),
|
| 92 |
-
):
|
| 93 |
-
"""Single entry point wired to the "Separate" button."""
|
| 94 |
-
if not input_audio:
|
| 95 |
-
gr.Warning("Please upload or record an audio file first.")
|
| 96 |
-
return _empty_result("⚠️ No input audio provided.")
|
| 97 |
-
|
| 98 |
-
# Resolve architecture + params from the use-case preset.
|
| 99 |
-
preset = USE_CASE_PRESETS.get(use_case, {})
|
| 100 |
-
architecture = preset.get("architecture", "roformer")
|
| 101 |
-
|
| 102 |
-
chosen_model = model_display_name or preset.get("model", "")
|
| 103 |
-
if chosen_model != preset.get("model"):
|
| 104 |
-
architecture = infer_architecture_from_model(chosen_model)
|
| 105 |
-
|
| 106 |
-
arch_params: dict[str, Any] = dict(preset.get("params", {}))
|
| 107 |
-
if advanced_mode:
|
| 108 |
-
_apply_advanced_overrides(
|
| 109 |
-
arch_params,
|
| 110 |
-
architecture=architecture,
|
| 111 |
-
seg_size=seg_size,
|
| 112 |
-
override_seg=override_seg,
|
| 113 |
-
overlap_rof=overlap_rof,
|
| 114 |
-
overlap_mdx=overlap_mdx,
|
| 115 |
-
pitch_shift=pitch_shift,
|
| 116 |
-
hop_length=hop_length,
|
| 117 |
-
enable_denoise=enable_denoise,
|
| 118 |
-
window_size=window_size,
|
| 119 |
-
aggression=aggression,
|
| 120 |
-
enable_tta=enable_tta,
|
| 121 |
-
enable_post=enable_post,
|
| 122 |
-
post_thresh=post_thresh,
|
| 123 |
-
enable_high_end=enable_high_end,
|
| 124 |
-
shifts=shifts,
|
| 125 |
-
segments_enabled=segments_enabled,
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
duration = audio_duration_seconds(input_audio)
|
| 129 |
-
est = estimate_time_seconds(duration, architecture)
|
| 130 |
-
|
| 131 |
-
status_running = (
|
| 132 |
-
f"🎙️ Input: `{Path(input_audio).name}` • "
|
| 133 |
-
f"Duration: **{fmt_duration(duration)}** • "
|
| 134 |
-
f"Model: **{chosen_model}** • "
|
| 135 |
-
f"Architecture: **{architecture}** • "
|
| 136 |
-
f"Device: **{DEVICE.upper()}** • "
|
| 137 |
-
f"Estimated time: **{fmt_duration(est)}**"
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
try:
|
| 141 |
-
stems = separate_core(
|
| 142 |
-
input_file=input_audio,
|
| 143 |
-
architecture=architecture,
|
| 144 |
-
model_display_name=chosen_model,
|
| 145 |
-
arch_params=arch_params,
|
| 146 |
-
model_file_dir=model_file_dir,
|
| 147 |
-
output_dir=output_dir,
|
| 148 |
-
out_format=out_format,
|
| 149 |
-
norm_thresh=norm_thresh,
|
| 150 |
-
amp_thresh=amp_thresh,
|
| 151 |
-
batch_size=batch_size,
|
| 152 |
-
progress=progress,
|
| 153 |
-
)
|
| 154 |
-
except Exception as e:
|
| 155 |
-
log.exception("Separation failed")
|
| 156 |
-
gr.Warning(f"Separation failed: {e}")
|
| 157 |
-
return _empty_result(
|
| 158 |
-
f"❌ **Error:** {e}",
|
| 159 |
-
est=fmt_duration(est),
|
| 160 |
-
dur=fmt_duration(duration),
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
if not stems:
|
| 164 |
-
return _empty_result(
|
| 165 |
-
"⚠️ No stems were produced.",
|
| 166 |
-
est=fmt_duration(est),
|
| 167 |
-
dur=fmt_duration(duration),
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
return _build_success_result(
|
| 171 |
-
stems=stems,
|
| 172 |
-
input_audio=input_audio,
|
| 173 |
-
use_case=use_case,
|
| 174 |
-
chosen_model=chosen_model,
|
| 175 |
-
duration=duration,
|
| 176 |
-
est=est,
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def _apply_advanced_overrides(
|
| 181 |
-
arch_params: dict[str, Any],
|
| 182 |
-
*,
|
| 183 |
-
architecture: str,
|
| 184 |
-
seg_size: int,
|
| 185 |
-
override_seg: bool,
|
| 186 |
-
overlap_rof: int,
|
| 187 |
-
overlap_mdx: float,
|
| 188 |
-
pitch_shift: int,
|
| 189 |
-
hop_length: int,
|
| 190 |
-
enable_denoise: bool,
|
| 191 |
-
window_size: int,
|
| 192 |
-
aggression: int,
|
| 193 |
-
enable_tta: bool,
|
| 194 |
-
enable_post: bool,
|
| 195 |
-
post_thresh: float,
|
| 196 |
-
enable_high_end: bool,
|
| 197 |
-
shifts: int,
|
| 198 |
-
segments_enabled: bool,
|
| 199 |
-
) -> None:
|
| 200 |
-
"""Mutate `arch_params` in place with the advanced-mode slider values."""
|
| 201 |
-
if architecture in ("roformer", "mdx23c"):
|
| 202 |
-
arch_params.update(
|
| 203 |
-
segment_size=seg_size,
|
| 204 |
-
override_segment_size=override_seg,
|
| 205 |
-
pitch_shift=pitch_shift,
|
| 206 |
-
overlap=overlap_rof,
|
| 207 |
-
)
|
| 208 |
-
elif architecture == "mdx":
|
| 209 |
-
arch_params.update(
|
| 210 |
-
hop_length=hop_length,
|
| 211 |
-
segment_size=seg_size,
|
| 212 |
-
overlap=overlap_mdx,
|
| 213 |
-
enable_denoise=enable_denoise,
|
| 214 |
-
)
|
| 215 |
-
elif architecture == "vr":
|
| 216 |
-
arch_params.update(
|
| 217 |
-
window_size=window_size,
|
| 218 |
-
aggression=aggression,
|
| 219 |
-
enable_tta=enable_tta,
|
| 220 |
-
enable_post_process=enable_post,
|
| 221 |
-
post_process_threshold=post_thresh,
|
| 222 |
-
high_end_process=enable_high_end,
|
| 223 |
-
)
|
| 224 |
-
elif architecture == "demucs":
|
| 225 |
-
arch_params.update(
|
| 226 |
-
segment_size=seg_size,
|
| 227 |
-
shifts=shifts,
|
| 228 |
-
overlap=overlap_mdx,
|
| 229 |
-
segments_enabled=segments_enabled,
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
def _build_success_result(
|
| 234 |
-
*,
|
| 235 |
-
stems: dict[str, str],
|
| 236 |
-
input_audio: str,
|
| 237 |
-
use_case: str,
|
| 238 |
-
chosen_model: str,
|
| 239 |
-
duration: float,
|
| 240 |
-
est: float,
|
| 241 |
-
):
|
| 242 |
-
"""Compose the 14-tuple return value for a successful separation."""
|
| 243 |
-
# Stable ordering: prefer common stem names first.
|
| 244 |
-
ordered_labels = [l for l in _PREFERRED_STEM_ORDER if l in stems]
|
| 245 |
-
ordered_labels += [l for l in stems.keys() if l not in ordered_labels]
|
| 246 |
-
|
| 247 |
-
out_paths: list[str | None] = [None] * N_STEMS
|
| 248 |
-
out_labels: list[str | None] = [None] * N_STEMS
|
| 249 |
-
for i, label in enumerate(ordered_labels[:N_STEMS]):
|
| 250 |
-
out_paths[i] = stems[label]
|
| 251 |
-
out_labels[i] = label
|
| 252 |
-
|
| 253 |
-
zip_path = package_stems_as_zip(stems, Path(input_audio).stem)
|
| 254 |
-
|
| 255 |
-
append_history({
|
| 256 |
-
"time": now_timestamp(),
|
| 257 |
-
"use_case": use_case,
|
| 258 |
-
"model": chosen_model,
|
| 259 |
-
"input": Path(input_audio).name,
|
| 260 |
-
"duration": fmt_duration(duration),
|
| 261 |
-
"stems": ordered_labels,
|
| 262 |
-
})
|
| 263 |
-
|
| 264 |
-
# Per-stem gr.update() with label-aware component updates.
|
| 265 |
-
stem_updates = []
|
| 266 |
-
for i in range(N_STEMS):
|
| 267 |
-
if out_paths[i] is not None:
|
| 268 |
-
stem_updates.append(
|
| 269 |
-
gr.update(value=out_paths[i], visible=True,
|
| 270 |
-
label=f"🎵 Stem: {out_labels[i]}")
|
| 271 |
-
)
|
| 272 |
-
else:
|
| 273 |
-
stem_updates.append(gr.update(value=None, visible=False))
|
| 274 |
-
|
| 275 |
-
n_stems = len(stems)
|
| 276 |
-
n_rows_visible = sum([n_stems > 0, n_stems > 2, n_stems > 4])
|
| 277 |
-
|
| 278 |
-
status_done = (
|
| 279 |
-
f"✅ **Done!** {n_stems} stem(s) produced: {', '.join(ordered_labels)} • "
|
| 280 |
-
f"Input duration {fmt_duration(duration)} • "
|
| 281 |
-
f"Model `{chosen_model}` • Device {DEVICE.upper()}"
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
return (
|
| 285 |
-
stem_updates[0], stem_updates[1], stem_updates[2],
|
| 286 |
-
stem_updates[3], stem_updates[4], stem_updates[5],
|
| 287 |
-
zip_path,
|
| 288 |
-
status_done,
|
| 289 |
-
gr.update(visible=n_rows_visible >= 1),
|
| 290 |
-
gr.update(visible=n_rows_visible >= 2),
|
| 291 |
-
gr.update(visible=n_rows_visible >= 3),
|
| 292 |
-
fmt_duration(est),
|
| 293 |
-
fmt_duration(duration),
|
| 294 |
-
history_rows(),
|
| 295 |
-
)
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
def _empty_result(
|
| 299 |
-
status: str = "Ready.",
|
| 300 |
-
est: str = "—",
|
| 301 |
-
dur: str = "—",
|
| 302 |
-
):
|
| 303 |
-
"""Compose the 14-tuple return value for an empty / cleared state."""
|
| 304 |
-
return (
|
| 305 |
-
gr.update(value=None, visible=False),
|
| 306 |
-
gr.update(value=None, visible=False),
|
| 307 |
-
gr.update(value=None, visible=False),
|
| 308 |
-
gr.update(value=None, visible=False),
|
| 309 |
-
gr.update(value=None, visible=False),
|
| 310 |
-
gr.update(value=None, visible=False),
|
| 311 |
-
None,
|
| 312 |
-
status,
|
| 313 |
-
gr.update(visible=False),
|
| 314 |
-
gr.update(visible=False),
|
| 315 |
-
gr.update(visible=False),
|
| 316 |
-
est,
|
| 317 |
-
dur,
|
| 318 |
-
history_rows(),
|
| 319 |
-
)
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
# --------------------------------------------------------------------------- #
|
| 323 |
-
# UI helper callbacks #
|
| 324 |
-
# --------------------------------------------------------------------------- #
|
| 325 |
-
|
| 326 |
-
def on_preset_change(use_case: str):
|
| 327 |
-
"""When the user picks a use-case preset, sync the model dropdown,
|
| 328 |
-
show a friendly description, and reveal the matching advanced group.
|
| 329 |
-
"""
|
| 330 |
-
preset = USE_CASE_PRESETS.get(use_case, {})
|
| 331 |
-
arch = preset.get("architecture", "roformer")
|
| 332 |
-
model = preset.get("model", "")
|
| 333 |
-
desc = preset.get("description", "")
|
| 334 |
-
return (
|
| 335 |
-
gr.update(value=model, choices=model_choices_for_arch(arch)),
|
| 336 |
-
f"ℹ️ {desc}" if desc else "",
|
| 337 |
-
gr.update(visible=arch in ("roformer", "mdx23c")),
|
| 338 |
-
gr.update(visible=arch == "mdx"),
|
| 339 |
-
gr.update(visible=arch == "vr"),
|
| 340 |
-
gr.update(visible=arch == "demucs"),
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
def on_model_change(model_name: str):
|
| 345 |
-
"""When the user manually picks a different model, infer its
|
| 346 |
-
architecture and reveal the matching advanced group.
|
| 347 |
-
"""
|
| 348 |
-
arch = infer_architecture_from_model(model_name)
|
| 349 |
-
return (
|
| 350 |
-
gr.update(visible=arch in ("roformer", "mdx23c")),
|
| 351 |
-
gr.update(visible=arch == "mdx"),
|
| 352 |
-
gr.update(visible=arch == "vr"),
|
| 353 |
-
gr.update(visible=arch == "demucs"),
|
| 354 |
-
)
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
def clear_outputs():
|
| 358 |
-
"""Reset all output components to their initial empty state."""
|
| 359 |
-
return _empty_result("Ready.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,832 +0,0 @@
|
|
| 1 |
-
"""Gradio UI for Audio Separator — Improved Demo.
|
| 2 |
-
|
| 3 |
-
Theme philosophy (inspired by the Gradio Server blog's "bring your own
|
| 4 |
-
frontend" aesthetic — dark canvas, expressive display type, glass surfaces,
|
| 5 |
-
electric-lime accent matching the waveform).
|
| 6 |
-
"""
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import os
|
| 10 |
-
|
| 11 |
-
import gradio as gr
|
| 12 |
-
|
| 13 |
-
from .config import DEFAULT_MODEL_DIR, DEFAULT_OUTPUT_DIR, DEVICE, HAS_SEPARATOR
|
| 14 |
-
from .models import ROFORMER_MODELS, USE_CASE_PRESETS
|
| 15 |
-
from .utils import history_rows
|
| 16 |
-
from .callbacks import (
|
| 17 |
-
clear_outputs,
|
| 18 |
-
on_model_change,
|
| 19 |
-
on_preset_change,
|
| 20 |
-
run_separation,
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# --------------------------------------------------------------------------- #
|
| 25 |
-
# Theme #
|
| 26 |
-
# --------------------------------------------------------------------------- #
|
| 27 |
-
# `gr.themes.Base` is the only Gradio base that lets us push true black (#000)
|
| 28 |
-
# through every layer. `Soft` and `Default` bake in subtle gray washes that
|
| 29 |
-
# fight the look we want.
|
| 30 |
-
|
| 31 |
-
theme = gr.themes.Base(
|
| 32 |
-
primary_hue=gr.themes.Color(
|
| 33 |
-
c50="#fafff0", c100="#f0ffcc", c200="#e0ff99", c300="#c6ff00",
|
| 34 |
-
c400="#aee000", c500="#86b300", c600="#5f8000", c700="#3d5200",
|
| 35 |
-
c800="#1f2b00", c900="#0e1500", c950="#070a00",
|
| 36 |
-
),
|
| 37 |
-
secondary_hue=gr.themes.Color(
|
| 38 |
-
c50="#f5f5f7", c100="#ebebef", c200="#d7d7df", c300="#b8b8c4",
|
| 39 |
-
c400="#8f8fa0", c500="#6b6b7a", c600="#4f4f5c", c700="#363642",
|
| 40 |
-
c800="#1f1f29", c900="#121218", c950="#08080c",
|
| 41 |
-
),
|
| 42 |
-
neutral_hue=gr.themes.Color(
|
| 43 |
-
c50="#fafafa", c100="#f0f0f0", c200="#d8d8d8", c300="#b4b4b4",
|
| 44 |
-
c400="#888888", c500="#5e5e5e", c600="#3e3e3e", c700="#262626",
|
| 45 |
-
c800="#161616", c900="#0a0a0a", c950="#000000",
|
| 46 |
-
),
|
| 47 |
-
font=[gr.themes.GoogleFont("Syne"), "Arial", "sans-serif"],
|
| 48 |
-
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
|
| 49 |
-
radius_size="lg",
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# --------------------------------------------------------------------------- #
|
| 54 |
-
# Custom CSS — the heart of the "cooler theme" #
|
| 55 |
-
# --------------------------------------------------------------------------- #
|
| 56 |
-
# Gradio emits its own CSS first; ours loads after via `Blocks(css=...)` and
|
| 57 |
-
# wins on equal specificity. We use high-specificity selectors (id + class
|
| 58 |
-
# or `:where(...)`) to avoid `!important` where possible, but a few rules
|
| 59 |
-
# do need `!important` to override Gradio's inline styles.
|
| 60 |
-
|
| 61 |
-
CUSTOM_CSS = """
|
| 62 |
-
/* =========================================================== *
|
| 63 |
-
* Root tokens — single source of truth for the look
|
| 64 |
-
* =========================================================== */
|
| 65 |
-
:root {
|
| 66 |
-
--as-bg: #000000;
|
| 67 |
-
--as-bg-elev-1: #0a0a0c;
|
| 68 |
-
--as-bg-elev-2: #121217;
|
| 69 |
-
--as-bg-elev-3: #1b1b22;
|
| 70 |
-
--as-border: #232330;
|
| 71 |
-
--as-border-soft: #16161c;
|
| 72 |
-
--as-text: #f5f5f7;
|
| 73 |
-
--as-text-dim: #9b9bab;
|
| 74 |
-
--as-text-mute: #5e5e6a;
|
| 75 |
-
--as-accent: #c6ff00;
|
| 76 |
-
--as-accent-dim: #86b300;
|
| 77 |
-
--as-accent-glow: rgba(198, 255, 0, 0.18);
|
| 78 |
-
--as-danger: #ff4d6d;
|
| 79 |
-
--as-success: #00d68f;
|
| 80 |
-
--as-warning: #ffb800;
|
| 81 |
-
--as-radius: 14px;
|
| 82 |
-
--as-radius-sm: 8px;
|
| 83 |
-
--as-shadow: 0 1px 0 rgba(255,255,255,0.04) inset,
|
| 84 |
-
0 8px 24px rgba(0,0,0,0.45);
|
| 85 |
-
--as-blur: blur(14px) saturate(140%);
|
| 86 |
-
--as-font-display: "Syne", "Arial", sans-serif;
|
| 87 |
-
--as-font-body: "Inter", system-ui, -apple-system, sans-serif;
|
| 88 |
-
--as-font-mono: "JetBrains Mono", ui-monospace, monospace;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
/* =========================================================== *
|
| 92 |
-
* Body / canvas
|
| 93 |
-
* =========================================================== */
|
| 94 |
-
html, body {
|
| 95 |
-
background: var(--as-bg) !important;
|
| 96 |
-
color: var(--as-text);
|
| 97 |
-
font-family: var(--as-font-body);
|
| 98 |
-
-webkit-font-smoothing: antialiased;
|
| 99 |
-
-moz-osx-font-smoothing: grayscale;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
/* Subtle radial glow that lives behind everything — gives the black some
|
| 103 |
-
depth without being noisy. Pinned to the viewport so it doesn't scroll. */
|
| 104 |
-
body::before {
|
| 105 |
-
content: "";
|
| 106 |
-
position: fixed;
|
| 107 |
-
inset: 0;
|
| 108 |
-
z-index: 0;
|
| 109 |
-
pointer-events: none;
|
| 110 |
-
background:
|
| 111 |
-
radial-gradient(900px 600px at 12% -10%, rgba(198,255,0,0.08), transparent 60%),
|
| 112 |
-
radial-gradient(700px 500px at 92% 8%, rgba(120,80,255,0.10), transparent 65%),
|
| 113 |
-
radial-gradient(800px 800px at 50% 120%, rgba(0,140,255,0.06), transparent 70%);
|
| 114 |
-
}
|
| 115 |
-
.gradio-container, .gradio-container > * {
|
| 116 |
-
position: relative;
|
| 117 |
-
z-index: 1;
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
/* Constrain width and add breathing room. */
|
| 121 |
-
.gradio-container {
|
| 122 |
-
max-width: 1320px !important;
|
| 123 |
-
padding: 32px 28px 80px !important;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
/* =========================================================== *
|
| 127 |
-
* Hero
|
| 128 |
-
* =========================================================== */
|
| 129 |
-
#hero {
|
| 130 |
-
position: relative;
|
| 131 |
-
padding: 40px 32px 32px;
|
| 132 |
-
margin: -8px -8px 28px;
|
| 133 |
-
border-radius: var(--as-radius);
|
| 134 |
-
background:
|
| 135 |
-
linear-gradient(135deg, rgba(198,255,0,0.06) 0%, transparent 40%),
|
| 136 |
-
linear-gradient(180deg, rgba(255,255,255,0.025), rgba(0,0,0,0));
|
| 137 |
-
border: 1px solid var(--as-border);
|
| 138 |
-
overflow: hidden;
|
| 139 |
-
}
|
| 140 |
-
#hero::before {
|
| 141 |
-
content: "";
|
| 142 |
-
position: absolute;
|
| 143 |
-
top: -1px; left: -1px; right: -1px;
|
| 144 |
-
height: 1px;
|
| 145 |
-
background: linear-gradient(90deg,
|
| 146 |
-
transparent, var(--as-accent) 30%, var(--as-accent) 70%, transparent);
|
| 147 |
-
opacity: 0.55;
|
| 148 |
-
}
|
| 149 |
-
#hero h1 {
|
| 150 |
-
font-family: var(--as-font-display);
|
| 151 |
-
font-weight: 800;
|
| 152 |
-
font-size: clamp(2.4rem, 5vw, 3.6rem);
|
| 153 |
-
line-height: 1.05;
|
| 154 |
-
letter-spacing: -0.02em;
|
| 155 |
-
margin: 0 0 14px;
|
| 156 |
-
background: linear-gradient(180deg, #ffffff 0%, #b0b0b8 100%);
|
| 157 |
-
-webkit-background-clip: text;
|
| 158 |
-
background-clip: text;
|
| 159 |
-
-webkit-text-fill-color: transparent;
|
| 160 |
-
}
|
| 161 |
-
#hero h1::after {
|
| 162 |
-
content: "";
|
| 163 |
-
display: inline-block;
|
| 164 |
-
width: 10px; height: 10px;
|
| 165 |
-
margin-left: 6px;
|
| 166 |
-
border-radius: 50%;
|
| 167 |
-
background: var(--as-accent);
|
| 168 |
-
box-shadow: 0 0 12px var(--as-accent);
|
| 169 |
-
vertical-align: middle;
|
| 170 |
-
transform: translateY(-0.5em);
|
| 171 |
-
animation: as-pulse 2.2s ease-in-out infinite;
|
| 172 |
-
}
|
| 173 |
-
#hero p {
|
| 174 |
-
font-size: 1rem;
|
| 175 |
-
line-height: 1.55;
|
| 176 |
-
color: var(--as-text-dim);
|
| 177 |
-
max-width: 720px;
|
| 178 |
-
margin: 0;
|
| 179 |
-
}
|
| 180 |
-
#hero code {
|
| 181 |
-
font-family: var(--as-font-mono);
|
| 182 |
-
color: var(--as-accent);
|
| 183 |
-
background: rgba(198,255,0,0.08);
|
| 184 |
-
border: 1px solid rgba(198,255,0,0.15);
|
| 185 |
-
padding: 1px 6px;
|
| 186 |
-
border-radius: 4px;
|
| 187 |
-
font-size: 0.86em;
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
@keyframes as-pulse {
|
| 191 |
-
0%, 100% { opacity: 1; transform: translateY(-0.5em) scale(1); }
|
| 192 |
-
50% { opacity: 0.4; transform: translateY(-0.5em) scale(0.75); }
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
/* =========================================================== *
|
| 196 |
-
* Section labels — small caps divider
|
| 197 |
-
* =========================================================== */
|
| 198 |
-
.section-label {
|
| 199 |
-
font-family: var(--as-font-mono);
|
| 200 |
-
font-size: 0.7rem;
|
| 201 |
-
font-weight: 600;
|
| 202 |
-
letter-spacing: 0.18em;
|
| 203 |
-
text-transform: uppercase;
|
| 204 |
-
color: var(--as-accent);
|
| 205 |
-
padding: 6px 0 14px;
|
| 206 |
-
margin: 0 0 14px;
|
| 207 |
-
border-bottom: 1px solid var(--as-border-soft);
|
| 208 |
-
display: flex;
|
| 209 |
-
align-items: center;
|
| 210 |
-
gap: 8px;
|
| 211 |
-
}
|
| 212 |
-
.section-label::before {
|
| 213 |
-
content: "";
|
| 214 |
-
width: 6px; height: 6px;
|
| 215 |
-
background: var(--as-accent);
|
| 216 |
-
border-radius: 50%;
|
| 217 |
-
box-shadow: 0 0 8px var(--as-accent);
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
/* =========================================================== *
|
| 221 |
-
* Gradio panel / column surfaces — glassmorphism
|
| 222 |
-
* =========================================================== */
|
| 223 |
-
.gradio-container .form,
|
| 224 |
-
.gradio-container .block,
|
| 225 |
-
.gradio-container .gr-block,
|
| 226 |
-
.gradio-container .gr-panel,
|
| 227 |
-
.gradio-container .gap,
|
| 228 |
-
.gradio-container .wrap {
|
| 229 |
-
background: var(--as-bg-elev-1);
|
| 230 |
-
border-color: var(--as-border) !important;
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
/* The two main columns. */
|
| 234 |
-
.gradio-container > .containing > * {
|
| 235 |
-
/* No-op: just a hook in case we want column-level tweaks. */
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
/* =========================================================== *
|
| 239 |
-
* Metric tiles — duration / estimated time
|
| 240 |
-
* =========================================================== */
|
| 241 |
-
.metric-tile {
|
| 242 |
-
background: linear-gradient(180deg, var(--as-bg-elev-2), var(--as-bg-elev-1)) !important;
|
| 243 |
-
border: 1px solid var(--as-border) !important;
|
| 244 |
-
border-left: 3px solid var(--as-accent) !important;
|
| 245 |
-
border-radius: var(--as-radius-sm) !important;
|
| 246 |
-
padding: 14px 16px !important;
|
| 247 |
-
box-shadow: var(--as-shadow);
|
| 248 |
-
}
|
| 249 |
-
.metric-tile label {
|
| 250 |
-
font-family: var(--as-font-mono) !important;
|
| 251 |
-
font-size: 0.65rem !important;
|
| 252 |
-
letter-spacing: 0.16em !important;
|
| 253 |
-
text-transform: uppercase !important;
|
| 254 |
-
color: var(--as-text-mute) !important;
|
| 255 |
-
font-weight: 500 !important;
|
| 256 |
-
}
|
| 257 |
-
.metric-tile input,
|
| 258 |
-
.metric-tile textarea {
|
| 259 |
-
font-family: var(--as-font-display) !important;
|
| 260 |
-
font-size: 1.5rem !important;
|
| 261 |
-
font-weight: 700 !important;
|
| 262 |
-
color: var(--as-text) !important;
|
| 263 |
-
background: transparent !important;
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
/* =========================================================== *
|
| 267 |
-
* Status box
|
| 268 |
-
* =========================================================== */
|
| 269 |
-
#status-box {
|
| 270 |
-
background: var(--as-bg-elev-1) !important;
|
| 271 |
-
border: 1px solid var(--as-border) !important;
|
| 272 |
-
border-left: 3px solid var(--as-accent) !important;
|
| 273 |
-
border-radius: var(--as-radius-sm) !important;
|
| 274 |
-
padding: 14px 18px !important;
|
| 275 |
-
min-height: 64px;
|
| 276 |
-
font-family: var(--as-font-body) !important;
|
| 277 |
-
color: var(--as-text) !important;
|
| 278 |
-
}
|
| 279 |
-
#status-box p { margin: 0; line-height: 1.55; }
|
| 280 |
-
#status-box code {
|
| 281 |
-
font-family: var(--as-font-mono);
|
| 282 |
-
color: var(--as-accent);
|
| 283 |
-
background: rgba(198,255,0,0.08);
|
| 284 |
-
padding: 1px 5px;
|
| 285 |
-
border-radius: 3px;
|
| 286 |
-
font-size: 0.88em;
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
/* =========================================================== *
|
| 290 |
-
* Buttons — primary glows electric-lime
|
| 291 |
-
* =========================================================== */
|
| 292 |
-
.gradio-container button.primary,
|
| 293 |
-
.gradio-container button[data-testid="primary-button"],
|
| 294 |
-
.gradio-container .primary {
|
| 295 |
-
background: var(--as-accent) !important;
|
| 296 |
-
color: #000 !important;
|
| 297 |
-
border: 1px solid var(--as-accent) !important;
|
| 298 |
-
font-family: var(--as-font-display) !important;
|
| 299 |
-
font-weight: 700 !important;
|
| 300 |
-
letter-spacing: 0.01em;
|
| 301 |
-
box-shadow:
|
| 302 |
-
0 0 0 1px rgba(198,255,0,0.0) inset,
|
| 303 |
-
0 6px 18px rgba(198,255,0,0.20),
|
| 304 |
-
0 1px 0 rgba(255,255,255,0.18) inset !important;
|
| 305 |
-
transition: transform 0.12s ease, box-shadow 0.18s ease, filter 0.18s ease !important;
|
| 306 |
-
}
|
| 307 |
-
.gradio-container button.primary:hover,
|
| 308 |
-
.gradio-container .primary:hover {
|
| 309 |
-
filter: brightness(1.08);
|
| 310 |
-
transform: translateY(-1px);
|
| 311 |
-
box-shadow:
|
| 312 |
-
0 0 0 1px rgba(198,255,0,0.30) inset,
|
| 313 |
-
0 10px 28px rgba(198,255,0,0.35),
|
| 314 |
-
0 1px 0 rgba(255,255,255,0.22) inset !important;
|
| 315 |
-
}
|
| 316 |
-
.gradio-container button.primary:active,
|
| 317 |
-
.gradio-container .primary:active { transform: translateY(0); filter: brightness(0.95); }
|
| 318 |
-
|
| 319 |
-
/* Secondary / default buttons — ghost style */
|
| 320 |
-
.gradio-container button:not(.primary):not(.gradio-icon-button):not(.icon-button) {
|
| 321 |
-
background: var(--as-bg-elev-2) !important;
|
| 322 |
-
color: var(--as-text) !important;
|
| 323 |
-
border: 1px solid var(--as-border) !important;
|
| 324 |
-
font-family: var(--as-font-display) !important;
|
| 325 |
-
font-weight: 600 !important;
|
| 326 |
-
transition: all 0.15s ease !important;
|
| 327 |
-
}
|
| 328 |
-
.gradio-container button:not(.primary):not(.gradio-icon-button):not(.icon-button):hover {
|
| 329 |
-
border-color: var(--as-accent) !important;
|
| 330 |
-
color: var(--as-accent) !important;
|
| 331 |
-
background: rgba(198,255,0,0.04) !important;
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
/* =========================================================== *
|
| 335 |
-
* Inputs — textboxes, dropdowns, sliders
|
| 336 |
-
* =========================================================== */
|
| 337 |
-
.gradio-container input[type="text"],
|
| 338 |
-
.gradio-container input[type="number"],
|
| 339 |
-
.gradio-container textarea,
|
| 340 |
-
.gradio-container select {
|
| 341 |
-
background: var(--as-bg-elev-2) !important;
|
| 342 |
-
border-color: var(--as-border) !important;
|
| 343 |
-
color: var(--as-text) !important;
|
| 344 |
-
border-radius: var(--as-radius-sm) !important;
|
| 345 |
-
font-family: var(--as-font-body) !important;
|
| 346 |
-
transition: border-color 0.15s ease, box-shadow 0.15s ease !important;
|
| 347 |
-
}
|
| 348 |
-
.gradio-container input[type="text"]:focus,
|
| 349 |
-
.gradio-container input[type="number"]:focus,
|
| 350 |
-
.gradio-container textarea:focus,
|
| 351 |
-
.gradio-container select:focus {
|
| 352 |
-
outline: none !important;
|
| 353 |
-
border-color: var(--as-accent) !important;
|
| 354 |
-
box-shadow: 0 0 0 3px var(--as-accent-glow) !important;
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
/* Labels above inputs */
|
| 358 |
-
.gradio-container label,
|
| 359 |
-
.gradio-container .label-wrap > label {
|
| 360 |
-
color: var(--as-text-dim) !important;
|
| 361 |
-
font-family: var(--as-font-mono) !important;
|
| 362 |
-
font-size: 0.72rem !important;
|
| 363 |
-
letter-spacing: 0.08em !important;
|
| 364 |
-
text-transform: uppercase !important;
|
| 365 |
-
font-weight: 500 !important;
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
/* Slider track + thumb — electric-lime */
|
| 369 |
-
.gradio-container input[type="range"] {
|
| 370 |
-
accent-color: var(--as-accent);
|
| 371 |
-
}
|
| 372 |
-
.gradio-container .range-wrap .range-info { color: var(--as-text-dim); }
|
| 373 |
-
|
| 374 |
-
/* Checkboxes */
|
| 375 |
-
.gradio-container input[type="checkbox"] {
|
| 376 |
-
accent-color: var(--as-accent);
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
/* =========================================================== *
|
| 380 |
-
* Accordion — clean dark surfaces, accent on hover
|
| 381 |
-
* =========================================================== */
|
| 382 |
-
.gradio-container .gr-accordion {
|
| 383 |
-
background: var(--as-bg-elev-1) !important;
|
| 384 |
-
border-color: var(--as-border) !important;
|
| 385 |
-
border-radius: var(--as-radius) !important;
|
| 386 |
-
overflow: hidden;
|
| 387 |
-
}
|
| 388 |
-
.gradio-container .gr-accordion > .label-wrap {
|
| 389 |
-
background: var(--as-bg-elev-2) !important;
|
| 390 |
-
border-bottom: 1px solid var(--as-border) !important;
|
| 391 |
-
color: var(--as-text) !important;
|
| 392 |
-
font-family: var(--as-font-display) !important;
|
| 393 |
-
font-weight: 600 !important;
|
| 394 |
-
padding: 14px 18px !important;
|
| 395 |
-
transition: background 0.15s ease !important;
|
| 396 |
-
}
|
| 397 |
-
.gradio-container .gr-accordion > .label-wrap:hover {
|
| 398 |
-
background: var(--as-bg-elev-3) !important;
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
/* =========================================================== *
|
| 402 |
-
* Audio component — recolor the waveform area
|
| 403 |
-
* =========================================================== */
|
| 404 |
-
.gradio-container .gradio-audio,
|
| 405 |
-
.gradio-container .audio-container,
|
| 406 |
-
.gradio-container [data-testid="audio"] {
|
| 407 |
-
background: var(--as-bg-elev-1) !important;
|
| 408 |
-
border: 1px solid var(--as-border) !important;
|
| 409 |
-
border-radius: var(--as-radius-sm) !important;
|
| 410 |
-
padding: 6px !important;
|
| 411 |
-
}
|
| 412 |
-
.gradio-container .gradio-audio label,
|
| 413 |
-
.gradio-container .audio-container label {
|
| 414 |
-
color: var(--as-text-dim) !important;
|
| 415 |
-
font-family: var(--as-font-mono) !important;
|
| 416 |
-
font-size: 0.7rem !important;
|
| 417 |
-
letter-spacing: 0.12em !important;
|
| 418 |
-
text-transform: uppercase !important;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
/* =========================================================== *
|
| 422 |
-
* File download — accent border for ZIP
|
| 423 |
-
* =========================================================== */
|
| 424 |
-
.gradio-container .gradio-file,
|
| 425 |
-
.gradio-container [data-testid="file"] {
|
| 426 |
-
background: var(--as-bg-elev-1) !important;
|
| 427 |
-
border: 1px solid var(--as-border) !important;
|
| 428 |
-
border-left: 3px solid var(--as-accent) !important;
|
| 429 |
-
border-radius: var(--as-radius-sm) !important;
|
| 430 |
-
padding: 14px !important;
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
/* =========================================================== *
|
| 434 |
-
* DataFrame (history table)
|
| 435 |
-
* =========================================================== */
|
| 436 |
-
.gradio-container .gradio-dataframe table {
|
| 437 |
-
background: var(--as-bg-elev-1) !important;
|
| 438 |
-
color: var(--as-text) !important;
|
| 439 |
-
border-radius: var(--as-radius-sm) !important;
|
| 440 |
-
overflow: hidden;
|
| 441 |
-
}
|
| 442 |
-
.gradio-container .gradio-dataframe th {
|
| 443 |
-
background: var(--as-bg-elev-3) !important;
|
| 444 |
-
color: var(--as-accent) !important;
|
| 445 |
-
font-family: var(--as-font-mono) !important;
|
| 446 |
-
font-size: 0.7rem !important;
|
| 447 |
-
letter-spacing: 0.10em !important;
|
| 448 |
-
text-transform: uppercase !important;
|
| 449 |
-
font-weight: 600 !important;
|
| 450 |
-
border-bottom: 1px solid var(--as-border) !important;
|
| 451 |
-
}
|
| 452 |
-
.gradio-container .gradio-dataframe td {
|
| 453 |
-
border-color: var(--as-border-soft) !important;
|
| 454 |
-
font-family: var(--as-font-body) !important;
|
| 455 |
-
color: var(--as-text-dim) !important;
|
| 456 |
-
}
|
| 457 |
-
.gradio-container .gradio-dataframe tr:hover td {
|
| 458 |
-
background: rgba(198,255,0,0.04) !important;
|
| 459 |
-
color: var(--as-text) !important;
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
/* =========================================================== *
|
| 463 |
-
* Markdown content
|
| 464 |
-
* =========================================================== */
|
| 465 |
-
.gradio-container .prose,
|
| 466 |
-
.gradio-container .markdown {
|
| 467 |
-
color: var(--as-text-dim) !important;
|
| 468 |
-
font-family: var(--as-font-body) !important;
|
| 469 |
-
line-height: 1.6;
|
| 470 |
-
}
|
| 471 |
-
.gradio-container .prose h1,
|
| 472 |
-
.gradio-container .prose h2,
|
| 473 |
-
.gradio-container .prose h3,
|
| 474 |
-
.gradio-container .prose h4 {
|
| 475 |
-
color: var(--as-text) !important;
|
| 476 |
-
font-family: var(--as-font-display) !important;
|
| 477 |
-
font-weight: 700 !important;
|
| 478 |
-
letter-spacing: -0.01em;
|
| 479 |
-
}
|
| 480 |
-
.gradio-container .prose code {
|
| 481 |
-
color: var(--as-accent);
|
| 482 |
-
background: rgba(198,255,0,0.08);
|
| 483 |
-
border: 1px solid rgba(198,255,0,0.15);
|
| 484 |
-
padding: 1px 6px;
|
| 485 |
-
border-radius: 4px;
|
| 486 |
-
font-family: var(--as-font-mono);
|
| 487 |
-
font-size: 0.88em;
|
| 488 |
-
}
|
| 489 |
-
.gradio-container .prose a {
|
| 490 |
-
color: var(--as-accent) !important;
|
| 491 |
-
text-decoration: none;
|
| 492 |
-
border-bottom: 1px dashed rgba(198,255,0,0.4);
|
| 493 |
-
}
|
| 494 |
-
.gradio-container .prose a:hover { border-bottom-style: solid; }
|
| 495 |
-
|
| 496 |
-
/* =========================================================== *
|
| 497 |
-
* Footer
|
| 498 |
-
* =========================================================== */
|
| 499 |
-
.app-footer {
|
| 500 |
-
margin-top: 36px;
|
| 501 |
-
padding: 18px 22px;
|
| 502 |
-
border-top: 1px solid var(--as-border);
|
| 503 |
-
color: var(--as-text-mute);
|
| 504 |
-
font-family: var(--as-font-mono);
|
| 505 |
-
font-size: 0.72rem;
|
| 506 |
-
letter-spacing: 0.10em;
|
| 507 |
-
text-transform: uppercase;
|
| 508 |
-
display: flex;
|
| 509 |
-
flex-wrap: wrap;
|
| 510 |
-
gap: 18px;
|
| 511 |
-
align-items: center;
|
| 512 |
-
}
|
| 513 |
-
.app-footer b {
|
| 514 |
-
color: var(--as-accent);
|
| 515 |
-
font-weight: 700;
|
| 516 |
-
margin-left: 6px;
|
| 517 |
-
}
|
| 518 |
-
.app-footer .dot {
|
| 519 |
-
color: var(--as-text-mute);
|
| 520 |
-
opacity: 0.4;
|
| 521 |
-
}
|
| 522 |
-
|
| 523 |
-
/* =========================================================== *
|
| 524 |
-
* Scrollbar — thin, dark, accent on hover
|
| 525 |
-
* =========================================================== */
|
| 526 |
-
::-webkit-scrollbar { width: 10px; height: 10px; }
|
| 527 |
-
::-webkit-scrollbar-track { background: var(--as-bg); }
|
| 528 |
-
::-webkit-scrollbar-thumb {
|
| 529 |
-
background: var(--as-bg-elev-3);
|
| 530 |
-
border-radius: 5px;
|
| 531 |
-
border: 2px solid var(--as-bg);
|
| 532 |
-
}
|
| 533 |
-
::-webkit-scrollbar-thumb:hover { background: #2a2a36; }
|
| 534 |
-
|
| 535 |
-
/* =========================================================== *
|
| 536 |
-
* Selection
|
| 537 |
-
* =========================================================== */
|
| 538 |
-
::selection {
|
| 539 |
-
background: var(--as-accent);
|
| 540 |
-
color: #000;
|
| 541 |
-
}
|
| 542 |
-
"""
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
# --------------------------------------------------------------------------- #
|
| 546 |
-
# App layout #
|
| 547 |
-
# --------------------------------------------------------------------------- #
|
| 548 |
-
|
| 549 |
-
def build_app() -> gr.Blocks:
|
| 550 |
-
"""Construct the Gradio Blocks UI. Does not launch."""
|
| 551 |
-
|
| 552 |
-
with gr.Blocks(
|
| 553 |
-
title="Audio Separator — Improved Demo",
|
| 554 |
-
theme=theme,
|
| 555 |
-
css=CUSTOM_CSS,
|
| 556 |
-
analytics_enabled=False,
|
| 557 |
-
) as app:
|
| 558 |
-
# ---- Hero ----
|
| 559 |
-
gr.HTML(
|
| 560 |
-
"<div id='hero'>"
|
| 561 |
-
"<h1>Audio Separator</h1>"
|
| 562 |
-
"<p>Upload → pick a use case → get labelled stems, ZIP download. "
|
| 563 |
-
"Powered by <code>audio-separator</code> + Gradio.</p>"
|
| 564 |
-
"</div>"
|
| 565 |
-
)
|
| 566 |
-
|
| 567 |
-
with gr.Row(equal_height=False):
|
| 568 |
-
# ================ LEFT COLUMN: input + presets ================
|
| 569 |
-
with gr.Column(scale=1, min_width=380):
|
| 570 |
-
gr.HTML('<div class="section-label">Input & Use Case</div>')
|
| 571 |
-
|
| 572 |
-
input_audio = gr.Audio(
|
| 573 |
-
label="Input Audio",
|
| 574 |
-
type="filepath",
|
| 575 |
-
waveform_options=gr.WaveformOptions(
|
| 576 |
-
show_recording_waveform=True,
|
| 577 |
-
waveform_color="#c6ff00",
|
| 578 |
-
waveform_progress_color="#ffffff",
|
| 579 |
-
),
|
| 580 |
-
)
|
| 581 |
-
|
| 582 |
-
use_case = gr.Dropdown(
|
| 583 |
-
label="Use Case (one-click preset)",
|
| 584 |
-
choices=list(USE_CASE_PRESETS.keys()),
|
| 585 |
-
value="Karaoke (vocals out)",
|
| 586 |
-
info="Pick a goal — we'll choose the best model + params.",
|
| 587 |
-
)
|
| 588 |
-
preset_desc = gr.Markdown(
|
| 589 |
-
f"ℹ️ {USE_CASE_PRESETS['Karaoke (vocals out)']['description']}",
|
| 590 |
-
)
|
| 591 |
-
|
| 592 |
-
model_display_name = gr.Dropdown(
|
| 593 |
-
label="Model (auto-selected, override anytime)",
|
| 594 |
-
choices=list(ROFORMER_MODELS.keys()),
|
| 595 |
-
value="MelBand Roformer | Karaoke V2 by Gabox",
|
| 596 |
-
interactive=True,
|
| 597 |
-
allow_custom_value=True,
|
| 598 |
-
)
|
| 599 |
-
|
| 600 |
-
advanced_toggle = gr.Checkbox(
|
| 601 |
-
label="🔧 Advanced mode (show all parameters)",
|
| 602 |
-
value=False,
|
| 603 |
-
)
|
| 604 |
-
|
| 605 |
-
with gr.Accordion("📂 Global settings", open=False):
|
| 606 |
-
model_file_dir = gr.Textbox(
|
| 607 |
-
value=DEFAULT_MODEL_DIR,
|
| 608 |
-
label="Model cache directory",
|
| 609 |
-
info="Where downloaded model weights are stored.",
|
| 610 |
-
)
|
| 611 |
-
output_dir = gr.Textbox(
|
| 612 |
-
value=DEFAULT_OUTPUT_DIR,
|
| 613 |
-
label="Output directory",
|
| 614 |
-
info="Where separated stems are written.",
|
| 615 |
-
)
|
| 616 |
-
out_format = gr.Dropdown(
|
| 617 |
-
value="wav",
|
| 618 |
-
choices=["wav", "flac", "mp3"],
|
| 619 |
-
label="Output format",
|
| 620 |
-
info="MP3 is encoded at 320 kbps (not the library's 128 kbps default).",
|
| 621 |
-
)
|
| 622 |
-
norm_thresh = gr.Slider(
|
| 623 |
-
0.1, 1.0, 0.1, 0.9,
|
| 624 |
-
label="Normalization threshold",
|
| 625 |
-
)
|
| 626 |
-
amp_thresh = gr.Slider(
|
| 627 |
-
0.1, 1.0, 0.1, 0.6,
|
| 628 |
-
label="Amplification threshold",
|
| 629 |
-
)
|
| 630 |
-
batch_size = gr.Slider(
|
| 631 |
-
1, 16, 1, 1,
|
| 632 |
-
label="Batch size (higher = more RAM, possibly faster)",
|
| 633 |
-
)
|
| 634 |
-
|
| 635 |
-
separate_btn = gr.Button(
|
| 636 |
-
"🚀 Separate Audio",
|
| 637 |
-
variant="primary",
|
| 638 |
-
size="lg",
|
| 639 |
-
)
|
| 640 |
-
clear_btn = gr.Button("🧹 Clear outputs", variant="secondary")
|
| 641 |
-
|
| 642 |
-
# ================ RIGHT COLUMN: outputs ================
|
| 643 |
-
with gr.Column(scale=1, min_width=380):
|
| 644 |
-
gr.HTML('<div class="section-label">Output Stems</div>')
|
| 645 |
-
|
| 646 |
-
with gr.Row():
|
| 647 |
-
dur_box = gr.Textbox(
|
| 648 |
-
"—", label="Input duration", interactive=False,
|
| 649 |
-
elem_classes=["metric-tile"],
|
| 650 |
-
)
|
| 651 |
-
est_box = gr.Textbox(
|
| 652 |
-
"—", label="Estimated processing time",
|
| 653 |
-
interactive=False,
|
| 654 |
-
elem_classes=["metric-tile"],
|
| 655 |
-
)
|
| 656 |
-
|
| 657 |
-
status_box = gr.Markdown(
|
| 658 |
-
"Ready. Upload an audio file and click **Separate Audio**.",
|
| 659 |
-
elem_id="status-box",
|
| 660 |
-
)
|
| 661 |
-
|
| 662 |
-
# Up to 6 stems (Demucs 6s), arranged 2-per-row.
|
| 663 |
-
with gr.Row(visible=False) as stem_row_1:
|
| 664 |
-
stem_1 = gr.Audio(label="🎵 Stem: Vocals",
|
| 665 |
-
type="filepath", interactive=False, visible=False)
|
| 666 |
-
stem_2 = gr.Audio(label="🎵 Stem: Instrumental",
|
| 667 |
-
type="filepath", interactive=False, visible=False)
|
| 668 |
-
with gr.Row(visible=False) as stem_row_2:
|
| 669 |
-
stem_3 = gr.Audio(label="🎵 Stem: Drums",
|
| 670 |
-
type="filepath", interactive=False, visible=False)
|
| 671 |
-
stem_4 = gr.Audio(label="🎵 Stem: Bass",
|
| 672 |
-
type="filepath", interactive=False, visible=False)
|
| 673 |
-
with gr.Row(visible=False) as stem_row_3:
|
| 674 |
-
stem_5 = gr.Audio(label="🎵 Stem: Other",
|
| 675 |
-
type="filepath", interactive=False, visible=False)
|
| 676 |
-
stem_6 = gr.Audio(label="🎵 Stem: Guitar/Piano",
|
| 677 |
-
type="filepath", interactive=False, visible=False)
|
| 678 |
-
|
| 679 |
-
zip_file = gr.File(label="📦 Download all stems (.zip)")
|
| 680 |
-
|
| 681 |
-
# ================ Advanced parameters (per-architecture) ================
|
| 682 |
-
with gr.Accordion("⚙️ Advanced parameters (architecture-specific)",
|
| 683 |
-
open=False):
|
| 684 |
-
with gr.Group(visible=True) as adv_rof:
|
| 685 |
-
gr.HTML('<div class="section-label">Roformer / MDX23C</div>')
|
| 686 |
-
with gr.Row():
|
| 687 |
-
seg_size = gr.Slider(32, 4000, 32, 256, label="Segment size")
|
| 688 |
-
override_seg = gr.Checkbox(False, label="Override model seg size")
|
| 689 |
-
overlap_rof = gr.Slider(2, 50, 1, 8, label="Overlap")
|
| 690 |
-
pitch_shift = gr.Slider(-12, 12, 1, 0, label="Pitch shift (semitones)")
|
| 691 |
-
|
| 692 |
-
with gr.Group(visible=False) as adv_mdx:
|
| 693 |
-
gr.HTML('<div class="section-label">MDX-NET</div>')
|
| 694 |
-
with gr.Row():
|
| 695 |
-
hop_length = gr.Slider(32, 2048, 32, 1024, label="Hop length")
|
| 696 |
-
overlap_mdx = gr.Slider(0.001, 0.999, 0.001, 0.25,
|
| 697 |
-
label="Overlap (MDX/Demucs)")
|
| 698 |
-
enable_denoise = gr.Checkbox(False, label="Enable denoise")
|
| 699 |
-
|
| 700 |
-
with gr.Group(visible=False) as adv_vr:
|
| 701 |
-
gr.HTML('<div class="section-label">VR Arch</div>')
|
| 702 |
-
with gr.Row():
|
| 703 |
-
window_size = gr.Slider(320, 1024, 32, 512, label="Window size")
|
| 704 |
-
aggression = gr.Slider(1, 50, 1, 5, label="Aggression")
|
| 705 |
-
enable_tta = gr.Checkbox(False, label="Enable TTA")
|
| 706 |
-
enable_post = gr.Checkbox(False, label="Enable post-process")
|
| 707 |
-
post_thresh = gr.Slider(0.1, 0.3, 0.1, 0.2,
|
| 708 |
-
label="Post-process threshold")
|
| 709 |
-
enable_high_end = gr.Checkbox(False, label="High-end processing")
|
| 710 |
-
|
| 711 |
-
with gr.Group(visible=False) as adv_demucs:
|
| 712 |
-
gr.HTML('<div class="section-label">Demucs</div>')
|
| 713 |
-
with gr.Row():
|
| 714 |
-
shifts = gr.Slider(0, 20, 1, 2, label="Shifts")
|
| 715 |
-
segments_enabled = gr.Checkbox(True, label="Enable segments")
|
| 716 |
-
|
| 717 |
-
# ================ History ================
|
| 718 |
-
with gr.Accordion("🕘 Recent separations", open=False):
|
| 719 |
-
history_table = gr.Dataframe(
|
| 720 |
-
headers=["Time", "Use Case", "Model", "Input", "Stems"],
|
| 721 |
-
datatype=["str", "str", "str", "str", "str"],
|
| 722 |
-
value=history_rows(),
|
| 723 |
-
wrap=True,
|
| 724 |
-
interactive=False,
|
| 725 |
-
)
|
| 726 |
-
|
| 727 |
-
# ================ FAQ ================
|
| 728 |
-
with gr.Accordion("📖 How to use / FAQ", open=False):
|
| 729 |
-
gr.Markdown(_FAQ_MARKDOWN)
|
| 730 |
-
|
| 731 |
-
# ================ Footer ================
|
| 732 |
-
sep_status = ("installed" if HAS_SEPARATOR
|
| 733 |
-
else "MISSING — pip install 'audio-separator[gpu]'")
|
| 734 |
-
gr.HTML(
|
| 735 |
-
f"<div class='app-footer'>"
|
| 736 |
-
f"<span>DEVICE <b>{DEVICE.upper()}</b></span>"
|
| 737 |
-
f"<span class='dot'>·</span>"
|
| 738 |
-
f"<span>ENGINE <b>{sep_status}</b></span>"
|
| 739 |
-
f"<span class='dot'>·</span>"
|
| 740 |
-
f"<span>THEME <b>BLACK / SYNE</b></span>"
|
| 741 |
-
f"</div>"
|
| 742 |
-
)
|
| 743 |
-
|
| 744 |
-
# ---- Event wiring ----
|
| 745 |
-
use_case.change(
|
| 746 |
-
on_preset_change,
|
| 747 |
-
inputs=[use_case],
|
| 748 |
-
outputs=[
|
| 749 |
-
model_display_name, preset_desc,
|
| 750 |
-
adv_rof, adv_mdx, adv_vr, adv_demucs,
|
| 751 |
-
],
|
| 752 |
-
)
|
| 753 |
-
model_display_name.change(
|
| 754 |
-
on_model_change,
|
| 755 |
-
inputs=[model_display_name],
|
| 756 |
-
outputs=[adv_rof, adv_mdx, adv_vr, adv_demucs],
|
| 757 |
-
)
|
| 758 |
-
|
| 759 |
-
separate_btn.click(
|
| 760 |
-
run_separation,
|
| 761 |
-
inputs=[
|
| 762 |
-
input_audio, use_case, model_display_name, advanced_toggle,
|
| 763 |
-
seg_size, override_seg, overlap_rof, overlap_mdx, pitch_shift,
|
| 764 |
-
hop_length, enable_denoise,
|
| 765 |
-
window_size, aggression, enable_tta, enable_post, post_thresh,
|
| 766 |
-
enable_high_end,
|
| 767 |
-
shifts, segments_enabled,
|
| 768 |
-
model_file_dir, output_dir, out_format,
|
| 769 |
-
norm_thresh, amp_thresh, batch_size,
|
| 770 |
-
],
|
| 771 |
-
outputs=[
|
| 772 |
-
stem_1, stem_2, stem_3, stem_4, stem_5, stem_6,
|
| 773 |
-
zip_file, status_box,
|
| 774 |
-
stem_row_1, stem_row_2, stem_row_3,
|
| 775 |
-
est_box, dur_box, history_table,
|
| 776 |
-
],
|
| 777 |
-
)
|
| 778 |
-
|
| 779 |
-
clear_btn.click(
|
| 780 |
-
clear_outputs,
|
| 781 |
-
outputs=[
|
| 782 |
-
stem_1, stem_2, stem_3, stem_4, stem_5, stem_6,
|
| 783 |
-
zip_file, status_box,
|
| 784 |
-
stem_row_1, stem_row_2, stem_row_3,
|
| 785 |
-
est_box, dur_box, history_table,
|
| 786 |
-
],
|
| 787 |
-
)
|
| 788 |
-
|
| 789 |
-
return app
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
# --------------------------------------------------------------------------- #
|
| 793 |
-
# FAQ content (kept here so it lives next to the UI it renders into) #
|
| 794 |
-
# --------------------------------------------------------------------------- #
|
| 795 |
-
|
| 796 |
-
_FAQ_MARKDOWN = """
|
| 797 |
-
#### Quick start
|
| 798 |
-
1. **Upload or record** audio in the input box on the left.
|
| 799 |
-
2. **Pick a use case** from the dropdown — the right model is auto-selected.
|
| 800 |
-
3. Click **🚀 Separate Audio**. The first run downloads the model (can be hundreds of MB).
|
| 801 |
-
4. Listen to each stem, A/B against the input, and download a ZIP of everything.
|
| 802 |
-
|
| 803 |
-
#### When to override the model
|
| 804 |
-
- **Karaoke** → `MelBand Roformer | Karaoke V2 by Gabox` is a great default.
|
| 805 |
-
- **Clean a cappella** → `BS-Roformer-Viperx-1297` (highest reported SDR).
|
| 806 |
-
- **De-reverb** → `MelBand Roformer | De-Reverb by anvuew`.
|
| 807 |
-
- **De-noise** → `Mel-Roformer-Denoise-Aufr33`.
|
| 808 |
-
- **Multi-stem remix** → `htdemucs_ft.yaml` (4 stems) or `htdemucs_6s.yaml` (6 stems).
|
| 809 |
-
- **CPU-only / speed** → `UVR-MDX-NET-Inst_HQ_3.onnx`.
|
| 810 |
-
|
| 811 |
-
#### Advanced parameters cheat sheet
|
| 812 |
-
| Param | Effect |
|
| 813 |
-
|---|---|
|
| 814 |
-
| `segment_size` | Larger = better quality, more RAM. |
|
| 815 |
-
| `overlap` | Higher = better quality, slower. |
|
| 816 |
-
| `pitch_shift` | Shifts pitch ±12 semitones during processing (Roformer/MDX23C). |
|
| 817 |
-
| `enable_denoise` | MDX-NET only — removes residual noise on stems. |
|
| 818 |
-
| `enable_tta` | VR Arch — Test-Time Augmentation, slower but cleaner. |
|
| 819 |
-
| `shifts` | Demucs — number of random-shift passes (higher = better, slower). |
|
| 820 |
-
|
| 821 |
-
#### Tips
|
| 822 |
-
- For best results use lossless inputs (WAV/FLAC).
|
| 823 |
-
- Models auto-cache in `Model cache directory` — first run is slow, later runs are fast.
|
| 824 |
-
- Use **Advanced mode** to expose every parameter the upstream HF Space exposes.
|
| 825 |
-
- MP3 output is encoded at **320 kbps** (the upstream library's 128 kbps default is overridden).
|
| 826 |
-
- Transient `librosa.ParameterError` failures are automatically retried once.
|
| 827 |
-
|
| 828 |
-
#### Credits
|
| 829 |
-
- Upstream: [nomadkaraoke/audio-separator](https://huggingface.co/spaces/nomadkaraoke/audio-separator) HF Space.
|
| 830 |
-
- Engine: [`audio-separator`](https://github.com/nomadkaraoke/python-audio-separator) by @berevadb.
|
| 831 |
-
- Models: various community contributors (UVR, Gabox, Aufr33, Viperx, Sucial, anvuew, Unwa, …).
|
| 832 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,23 +1,26 @@
|
|
| 1 |
-
# Audio Separator —
|
| 2 |
# =================================================
|
| 3 |
#
|
|
|
|
|
|
|
|
|
|
| 4 |
# Recommended install (CPU, ~1.5 GB on disk):
|
| 5 |
#
|
| 6 |
# python3 -m venv venv
|
| 7 |
# source venv/bin/activate
|
| 8 |
#
|
| 9 |
# # 1) Install CPU-only torch FIRST (skips CUDA libs, saves ~4 GB).
|
| 10 |
-
# pip install --index-url https://download.pytorch.org/whl/cpu torch torchaudio
|
| 11 |
#
|
| 12 |
# # 2) Then install the rest. `--no-deps` for audio-separator prevents
|
| 13 |
# # pip from re-installing the CUDA-enabled torch.
|
| 14 |
-
# pip install "gradio>=
|
| 15 |
# pip install --no-deps "audio-separator[cpu]>=0.44.0"
|
| 16 |
# pip install --no-deps \
|
| 17 |
# librosa ml_collections einops julius rotary-embedding-torch \
|
| 18 |
-
# onnx2torch-py313
|
| 19 |
# beartype pydub soundfile lazy_loader numba llvmlite pooch \
|
| 20 |
-
# ml_dtypes cffi
|
| 21 |
#
|
| 22 |
# # 3) System dependency:
|
| 23 |
# # sudo apt-get install -y ffmpeg # Debian/Ubuntu
|
|
@@ -25,15 +28,16 @@
|
|
| 25 |
#
|
| 26 |
# Simpler one-liner that works if disk space is not a concern:
|
| 27 |
#
|
| 28 |
-
# pip install "audio-separator[cpu]>=0.44.0" "gradio>=
|
| 29 |
#
|
| 30 |
# (This pulls in the default CUDA-enabled torch on Linux, ~5 GB.)
|
| 31 |
#
|
| 32 |
# For NVIDIA GPU acceleration (CUDA 12.x):
|
| 33 |
#
|
| 34 |
-
# pip install "audio-separator[gpu]>=0.44.0" "gradio>=
|
| 35 |
#
|
| 36 |
# =================================================
|
| 37 |
|
| 38 |
-
gradio>=
|
| 39 |
audio-separator[cpu]>=0.44.0
|
|
|
|
|
|
| 1 |
+
# Audio Separator — Gradio Server Edition: dependencies
|
| 2 |
# =================================================
|
| 3 |
#
|
| 4 |
+
# This version uses `gradio.Server` (introduced in gradio 6.x) — see
|
| 5 |
+
# https://huggingface.co/blog/introducing-gradio-server
|
| 6 |
+
#
|
| 7 |
# Recommended install (CPU, ~1.5 GB on disk):
|
| 8 |
#
|
| 9 |
# python3 -m venv venv
|
| 10 |
# source venv/bin/activate
|
| 11 |
#
|
| 12 |
# # 1) Install CPU-only torch FIRST (skips CUDA libs, saves ~4 GB).
|
| 13 |
+
# pip install --index-url https://download.pytorch.org/whl/cpu torch torchaudio
|
| 14 |
#
|
| 15 |
# # 2) Then install the rest. `--no-deps` for audio-separator prevents
|
| 16 |
# # pip from re-installing the CUDA-enabled torch.
|
| 17 |
+
# pip install "gradio>=6.0"
|
| 18 |
# pip install --no-deps "audio-separator[cpu]>=0.44.0"
|
| 19 |
# pip install --no-deps \
|
| 20 |
# librosa ml_collections einops julius rotary-embedding-torch \
|
| 21 |
+
# onnx2torch-py313 resampy samplerate diffq \
|
| 22 |
# beartype pydub soundfile lazy_loader numba llvmlite pooch \
|
| 23 |
+
# ml_dtypes cffi onnxruntime
|
| 24 |
#
|
| 25 |
# # 3) System dependency:
|
| 26 |
# # sudo apt-get install -y ffmpeg # Debian/Ubuntu
|
|
|
|
| 28 |
#
|
| 29 |
# Simpler one-liner that works if disk space is not a concern:
|
| 30 |
#
|
| 31 |
+
# pip install "audio-separator[cpu]>=0.44.0" "gradio>=6.0"
|
| 32 |
#
|
| 33 |
# (This pulls in the default CUDA-enabled torch on Linux, ~5 GB.)
|
| 34 |
#
|
| 35 |
# For NVIDIA GPU acceleration (CUDA 12.x):
|
| 36 |
#
|
| 37 |
+
# pip install "audio-separator[gpu]>=0.44.0" "gradio>=6.0"
|
| 38 |
#
|
| 39 |
# =================================================
|
| 40 |
|
| 41 |
+
gradio>=6.0
|
| 42 |
audio-separator[cpu]>=0.44.0
|
| 43 |
+
|
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================ *
|
| 2 |
+
* Audio Separator — Gradio Server Edition
|
| 3 |
+
* Vanilla JS — talks to the backend via the Gradio JS client.
|
| 4 |
+
*
|
| 5 |
+
* Pattern (straight from the blog):
|
| 6 |
+
* import { Client, handle_file } from "@gradio/client";
|
| 7 |
+
* const client = await Client.connect(window.location.origin);
|
| 8 |
+
* const result = await client.predict("/api_name", { ...payload });
|
| 9 |
+
* const url = result.data[0].url; // FileData URL
|
| 10 |
+
* ============================================================ */
|
| 11 |
+
|
| 12 |
+
(function () {
|
| 13 |
+
"use strict";
|
| 14 |
+
|
| 15 |
+
// The Gradio JS client is loaded as a global from CDN (see index.html).
|
| 16 |
+
// It exposes `window.gradio` with `Client` and `handle_file`.
|
| 17 |
+
const { Client, handle_file } = window.gradio;
|
| 18 |
+
|
| 19 |
+
// ------------------------------------------------------------------ //
|
| 20 |
+
// State //
|
| 21 |
+
// ------------------------------------------------------------------ //
|
| 22 |
+
const state = {
|
| 23 |
+
presets: null,
|
| 24 |
+
selectedFile: null,
|
| 25 |
+
isWorking: false,
|
| 26 |
+
client: null,
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// ------------------------------------------------------------------ //
|
| 30 |
+
// DOM helpers //
|
| 31 |
+
// ------------------------------------------------------------------ //
|
| 32 |
+
const $ = (sel) => document.querySelector(sel);
|
| 33 |
+
const $$ = (sel) => document.querySelectorAll(sel);
|
| 34 |
+
|
| 35 |
+
function el(tag, attrs = {}, ...children) {
|
| 36 |
+
const node = document.createElement(tag);
|
| 37 |
+
for (const [k, v] of Object.entries(attrs)) {
|
| 38 |
+
if (k === "class") node.className = v;
|
| 39 |
+
else if (k === "html") node.innerHTML = v;
|
| 40 |
+
else if (k.startsWith("on") && typeof v === "function") {
|
| 41 |
+
node.addEventListener(k.slice(2).toLowerCase(), v);
|
| 42 |
+
} else if (v !== null && v !== undefined) {
|
| 43 |
+
node.setAttribute(k, v);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
for (const child of children) {
|
| 47 |
+
if (child === null || child === undefined) continue;
|
| 48 |
+
node.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
| 49 |
+
}
|
| 50 |
+
return node;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// ------------------------------------------------------------------ //
|
| 54 |
+
// Status / metrics //
|
| 55 |
+
// ------------------------------------------------------------------ //
|
| 56 |
+
function setStatus(text, kind = "ready") {
|
| 57 |
+
const box = $("#status");
|
| 58 |
+
box.className = `status ${kind}`;
|
| 59 |
+
box.innerHTML = text;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function setMetrics({ dur = "—", est = "—", stems = "—" } = {}) {
|
| 63 |
+
$("#metric-dur").textContent = dur;
|
| 64 |
+
$("#metric-est").textContent = est;
|
| 65 |
+
$("#metric-stems").textContent = stems;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// ------------------------------------------------------------------ //
|
| 69 |
+
// Slider value display //
|
| 70 |
+
// ------------------------------------------------------------------ //
|
| 71 |
+
function bindSliderDisplay(sliderId, displayId, formatter = (v) => v) {
|
| 72 |
+
const slider = document.getElementById(sliderId);
|
| 73 |
+
const display = document.getElementById(displayId);
|
| 74 |
+
if (!slider || !display) return;
|
| 75 |
+
const update = () => { display.textContent = formatter(slider.value); };
|
| 76 |
+
slider.addEventListener("input", update);
|
| 77 |
+
update();
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// ------------------------------------------------------------------ //
|
| 81 |
+
// Dropzone & file input //
|
| 82 |
+
// ------------------------------------------------------------------ //
|
| 83 |
+
function initDropzone() {
|
| 84 |
+
const dz = $("#dropzone");
|
| 85 |
+
const input = $("#file-input");
|
| 86 |
+
const preview = $("#preview");
|
| 87 |
+
const previewWrap = $("#preview-wrap");
|
| 88 |
+
const previewName = $("#preview-name");
|
| 89 |
+
const previewDur = $("#preview-dur");
|
| 90 |
+
const clearBtn = $("#clear-input");
|
| 91 |
+
const separateBtn = $("#separate-btn");
|
| 92 |
+
|
| 93 |
+
function handleFile(file) {
|
| 94 |
+
if (!file) return;
|
| 95 |
+
if (!file.type.startsWith("audio/") && !/\.(wav|mp3|flac|m4a|ogg|aac|opus)$/i.test(file.name)) {
|
| 96 |
+
setStatus(`⚠️ "${file.name}" doesn't look like an audio file. Try anyway?`, "error");
|
| 97 |
+
}
|
| 98 |
+
state.selectedFile = file;
|
| 99 |
+
const url = URL.createObjectURL(file);
|
| 100 |
+
preview.src = url;
|
| 101 |
+
previewName.textContent = file.name;
|
| 102 |
+
previewDur.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`;
|
| 103 |
+
previewWrap.hidden = false;
|
| 104 |
+
dz.style.display = "none";
|
| 105 |
+
separateBtn.disabled = false;
|
| 106 |
+
setStatus(`Ready. Click <strong>Separate audio</strong> to process <code>${file.name}</code>.`);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function clearFile() {
|
| 110 |
+
state.selectedFile = null;
|
| 111 |
+
preview.src = "";
|
| 112 |
+
previewWrap.hidden = true;
|
| 113 |
+
dz.style.display = "";
|
| 114 |
+
separateBtn.disabled = true;
|
| 115 |
+
setStatus("Ready. Upload audio and click <strong>Separate</strong>.");
|
| 116 |
+
setMetrics({});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Click to browse
|
| 120 |
+
dz.addEventListener("click", () => input.click());
|
| 121 |
+
dz.addEventListener("keydown", (e) => {
|
| 122 |
+
if (e.key === "Enter" || e.key === " ") {
|
| 123 |
+
e.preventDefault();
|
| 124 |
+
input.click();
|
| 125 |
+
}
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
// File picker
|
| 129 |
+
input.addEventListener("change", (e) => {
|
| 130 |
+
const file = e.target.files && e.target.files[0];
|
| 131 |
+
if (file) handleFile(file);
|
| 132 |
+
});
|
| 133 |
+
|
| 134 |
+
// Drag & drop
|
| 135 |
+
["dragenter", "dragover"].forEach((ev) => {
|
| 136 |
+
dz.addEventListener(ev, (e) => {
|
| 137 |
+
e.preventDefault();
|
| 138 |
+
dz.classList.add("dragging");
|
| 139 |
+
});
|
| 140 |
+
});
|
| 141 |
+
["dragleave", "drop"].forEach((ev) => {
|
| 142 |
+
dz.addEventListener(ev, (e) => {
|
| 143 |
+
e.preventDefault();
|
| 144 |
+
dz.classList.remove("dragging");
|
| 145 |
+
});
|
| 146 |
+
});
|
| 147 |
+
dz.addEventListener("drop", (e) => {
|
| 148 |
+
const file = e.dataTransfer.files && e.dataTransfer.files[0];
|
| 149 |
+
if (file) handleFile(file);
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
clearBtn.addEventListener("click", clearFile);
|
| 153 |
+
|
| 154 |
+
// Compute duration once metadata loads
|
| 155 |
+
preview.addEventListener("loadedmetadata", () => {
|
| 156 |
+
const s = preview.duration;
|
| 157 |
+
if (isFinite(s)) {
|
| 158 |
+
const mm = Math.floor(s / 60).toString().padStart(2, "0");
|
| 159 |
+
const ss = Math.floor(s % 60).toString().padStart(2, "0");
|
| 160 |
+
previewDur.textContent = `${mm}:${ss} · ${(state.selectedFile.size / 1024 / 1024).toFixed(2)} MB`;
|
| 161 |
+
}
|
| 162 |
+
});
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// ------------------------------------------------------------------ //
|
| 166 |
+
// Preset / model dropdowns //
|
| 167 |
+
// ------------------------------------------------------------------ //
|
| 168 |
+
async function loadPresets() {
|
| 169 |
+
try {
|
| 170 |
+
const result = await state.client.predict("/presets", {});
|
| 171 |
+
const data = result.data[0];
|
| 172 |
+
state.presets = data;
|
| 173 |
+
|
| 174 |
+
const useCaseSel = $("#use-case");
|
| 175 |
+
useCaseSel.innerHTML = "";
|
| 176 |
+
Object.keys(data.presets).forEach((name) => {
|
| 177 |
+
const opt = el("option", { value: name }, name);
|
| 178 |
+
if (name === data.default_use_case) opt.selected = true;
|
| 179 |
+
useCaseSel.appendChild(opt);
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
// Load model list for the default architecture
|
| 183 |
+
await refreshModelDropdown(data.default_use_case);
|
| 184 |
+
|
| 185 |
+
// Trigger initial preset description
|
| 186 |
+
useCaseSel.dispatchEvent(new Event("change"));
|
| 187 |
+
} catch (err) {
|
| 188 |
+
console.error("Failed to load presets:", err);
|
| 189 |
+
setStatus(`⚠️ Failed to load presets: ${err.message || err}`, "error");
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
async function refreshModelDropdown(useCaseName) {
|
| 194 |
+
if (!state.presets) return;
|
| 195 |
+
const preset = state.presets.presets[useCaseName];
|
| 196 |
+
if (!preset) return;
|
| 197 |
+
|
| 198 |
+
const arch = preset.architecture;
|
| 199 |
+
try {
|
| 200 |
+
const result = await state.client.predict("/models", { architecture: arch });
|
| 201 |
+
const models = result.data[0];
|
| 202 |
+
const sel = $("#model");
|
| 203 |
+
sel.innerHTML = "";
|
| 204 |
+
models.forEach((name) => {
|
| 205 |
+
const opt = el("option", { value: name }, name);
|
| 206 |
+
if (name === preset.model) opt.selected = true;
|
| 207 |
+
sel.appendChild(opt);
|
| 208 |
+
});
|
| 209 |
+
} catch (err) {
|
| 210 |
+
console.error("Failed to load models:", err);
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
function onUseCaseChange() {
|
| 215 |
+
const useCaseName = $("#use-case").value;
|
| 216 |
+
if (!state.presets) return;
|
| 217 |
+
const preset = state.presets.presets[useCaseName];
|
| 218 |
+
if (!preset) return;
|
| 219 |
+
|
| 220 |
+
$("#preset-desc").textContent = `ℹ️ ${preset.description}`;
|
| 221 |
+
refreshModelDropdown(useCaseName);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function onModelChange() {
|
| 225 |
+
// Architecture groups visibility is auto-managed by the backend's
|
| 226 |
+
// response, but since we don't get the architecture back from /models,
|
| 227 |
+
// we re-derive it client-side from the preset.
|
| 228 |
+
const modelName = $("#model").value;
|
| 229 |
+
const useCaseName = $("#use-case").value;
|
| 230 |
+
if (!state.presets) return;
|
| 231 |
+
const preset = state.presets.presets[useCaseName];
|
| 232 |
+
if (!preset) return;
|
| 233 |
+
const arch = (modelName === preset.model) ? preset.architecture : inferArchFromModel(modelName);
|
| 234 |
+
updateAdvancedGroups(arch);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function inferArchFromModel(name) {
|
| 238 |
+
if (!name) return "roformer";
|
| 239 |
+
if (name.endsWith(".yaml")) return "demucs";
|
| 240 |
+
if (name.endsWith(".pth")) return "vr";
|
| 241 |
+
if (name.endsWith(".onnx")) return "mdx";
|
| 242 |
+
if (name.endsWith(".ckpt")) return "roformer";
|
| 243 |
+
return "roformer";
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
function updateAdvancedGroups(arch) {
|
| 247 |
+
$("#adv-rof").hidden = !(arch === "roformer" || arch === "mdx23c");
|
| 248 |
+
$("#adv-mdx").hidden = !(arch === "mdx");
|
| 249 |
+
$("#adv-vr").hidden = !(arch === "vr");
|
| 250 |
+
$("#adv-demucs").hidden = !(arch === "demucs");
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// ------------------------------------------------------------------ //
|
| 254 |
+
// Advanced toggle //
|
| 255 |
+
// ------------------------------------------------------------------ //
|
| 256 |
+
function initAdvancedToggle() {
|
| 257 |
+
const cb = $("#advanced");
|
| 258 |
+
const acc = $("#adv-accordion");
|
| 259 |
+
cb.addEventListener("change", () => {
|
| 260 |
+
acc.hidden = !cb.checked;
|
| 261 |
+
if (cb.checked) acc.open = true;
|
| 262 |
+
});
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// ------------------------------------------------------------------ //
|
| 266 |
+
// History //
|
| 267 |
+
// ------------------------------------------------------------------ //
|
| 268 |
+
async function loadHistory() {
|
| 269 |
+
try {
|
| 270 |
+
const result = await state.client.predict("/history", {});
|
| 271 |
+
const rows = result.data[0] || [];
|
| 272 |
+
const body = $("#history-body");
|
| 273 |
+
body.innerHTML = "";
|
| 274 |
+
if (!rows.length) {
|
| 275 |
+
body.appendChild(el("tr", { class: "empty-row" }, el("td", { colspan: "5" }, "No history yet.")));
|
| 276 |
+
return;
|
| 277 |
+
}
|
| 278 |
+
rows.forEach((r) => {
|
| 279 |
+
body.appendChild(el("tr",
|
| 280 |
+
null,
|
| 281 |
+
el("td", null, r[0] || ""),
|
| 282 |
+
el("td", null, r[1] || ""),
|
| 283 |
+
el("td", null, r[2] || ""),
|
| 284 |
+
el("td", null, r[3] || ""),
|
| 285 |
+
el("td", null, r[4] || "")
|
| 286 |
+
));
|
| 287 |
+
});
|
| 288 |
+
} catch (err) {
|
| 289 |
+
// Silent — history is best-effort
|
| 290 |
+
console.warn("History load failed:", err);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// ------------------------------------------------------------------ //
|
| 295 |
+
// Separation //
|
| 296 |
+
// ------------------------------------------------------------------ //
|
| 297 |
+
async function runSeparation() {
|
| 298 |
+
if (state.isWorking) return;
|
| 299 |
+
if (!state.selectedFile) {
|
| 300 |
+
setStatus("⚠️ Please upload an audio file first.", "error");
|
| 301 |
+
return;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
state.isWorking = true;
|
| 305 |
+
const btn = $("#separate-btn");
|
| 306 |
+
btn.disabled = true;
|
| 307 |
+
$(".btn-label", btn).textContent = "Working…";
|
| 308 |
+
$(".btn-spinner", btn).hidden = false;
|
| 309 |
+
|
| 310 |
+
setStatus(`Working on <code>${state.selectedFile.name}</code>… First run downloads the model, so this can take a while.`, "working");
|
| 311 |
+
setMetrics({ stems: "…" });
|
| 312 |
+
|
| 313 |
+
// Build payload
|
| 314 |
+
const payload = {
|
| 315 |
+
audio_file: handle_file(state.selectedFile),
|
| 316 |
+
use_case: $("#use-case").value,
|
| 317 |
+
model_display_name: $("#model").value,
|
| 318 |
+
output_format: $("#output-format").value,
|
| 319 |
+
advanced_mode: $("#advanced").checked,
|
| 320 |
+
seg_size: parseInt($("#seg-size").value, 10),
|
| 321 |
+
overlap_rof: parseInt($("#overlap-rof").value, 10),
|
| 322 |
+
overlap_mdx: parseFloat($("#overlap-mdx").value),
|
| 323 |
+
pitch_shift: parseInt($("#pitch-shift").value, 10),
|
| 324 |
+
hop_length: parseInt($("#hop-length").value, 10),
|
| 325 |
+
enable_denoise: $("#enable-denoise").checked,
|
| 326 |
+
window_size: parseInt($("#window-size").value, 10),
|
| 327 |
+
aggression: parseInt($("#aggression").value, 10),
|
| 328 |
+
enable_tta: $("#enable-tta").checked,
|
| 329 |
+
enable_post: $("#enable-post").checked,
|
| 330 |
+
post_thresh: parseFloat($("#post-thresh").value),
|
| 331 |
+
enable_high_end: $("#enable-high-end").checked,
|
| 332 |
+
shifts: parseInt($("#shifts").value, 10),
|
| 333 |
+
segments_enabled: $("#segments-enabled").checked,
|
| 334 |
+
norm_thresh: parseFloat($("#norm-thresh").value),
|
| 335 |
+
amp_thresh: parseFloat($("#amp-thresh").value),
|
| 336 |
+
batch_size: parseInt($("#batch-size").value, 10),
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
try {
|
| 340 |
+
const result = await state.client.predict("/separate", payload);
|
| 341 |
+
const data = result.data[0];
|
| 342 |
+
|
| 343 |
+
if (data.error) {
|
| 344 |
+
setStatus(`❌ ${data.error}`, "error");
|
| 345 |
+
setMetrics({
|
| 346 |
+
dur: data.duration || "—",
|
| 347 |
+
est: data.est || "—",
|
| 348 |
+
stems: "0",
|
| 349 |
+
});
|
| 350 |
+
return;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
renderStems(data.stems || []);
|
| 354 |
+
renderZip(data.zip);
|
| 355 |
+
|
| 356 |
+
const stemNames = (data.stems || []).map((s) => s.label).join(", ");
|
| 357 |
+
setStatus(
|
| 358 |
+
`✅ <strong>Done.</strong> ${data.stems.length} stem(s): ${stemNames} · ` +
|
| 359 |
+
`Model <code>${data.model}</code> · Architecture <code>${data.architecture}</code> · ` +
|
| 360 |
+
`Device <code>${data.device}</code>`,
|
| 361 |
+
"success"
|
| 362 |
+
);
|
| 363 |
+
setMetrics({
|
| 364 |
+
dur: data.duration || "—",
|
| 365 |
+
est: data.est || "—",
|
| 366 |
+
stems: String(data.stems.length),
|
| 367 |
+
});
|
| 368 |
+
|
| 369 |
+
loadHistory();
|
| 370 |
+
} catch (err) {
|
| 371 |
+
console.error("Separation failed:", err);
|
| 372 |
+
setStatus(`❌ <strong>Error:</strong> ${err.message || err}`, "error");
|
| 373 |
+
} finally {
|
| 374 |
+
state.isWorking = false;
|
| 375 |
+
btn.disabled = false;
|
| 376 |
+
$(".btn-label", btn).textContent = "Separate audio";
|
| 377 |
+
$(".btn-spinner", btn).hidden = true;
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
function renderStems(stems) {
|
| 382 |
+
const container = $("#stems");
|
| 383 |
+
container.innerHTML = "";
|
| 384 |
+
|
| 385 |
+
if (!stems.length) {
|
| 386 |
+
$("#empty-state").classList.remove("hidden");
|
| 387 |
+
return;
|
| 388 |
+
}
|
| 389 |
+
$("#empty-state").classList.add("hidden");
|
| 390 |
+
|
| 391 |
+
stems.forEach((stem) => {
|
| 392 |
+
// stem.file is a FileData — its `.url` is the playable URL.
|
| 393 |
+
// Some serialization paths leave `url` null and only populate `path`;
|
| 394 |
+
// in that case we build the URL ourselves via Gradio's /file= endpoint.
|
| 395 |
+
const url = fileDataUrl(stem.file);
|
| 396 |
+
if (!url) return;
|
| 397 |
+
|
| 398 |
+
const card = el("div", { class: "stem-card" },
|
| 399 |
+
el("div", { class: "stem-header" },
|
| 400 |
+
el("div", { class: "stem-label" }, `🎵 ${stem.label}`),
|
| 401 |
+
el("a", { class: "stem-download", href: url, download: "" }, "download ↓")
|
| 402 |
+
),
|
| 403 |
+
el("audio", { controls: "", preload: "metadata", src: url })
|
| 404 |
+
);
|
| 405 |
+
container.appendChild(card);
|
| 406 |
+
});
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
function renderZip(zipFileData) {
|
| 410 |
+
const link = $("#zip-link");
|
| 411 |
+
if (!zipFileData) {
|
| 412 |
+
link.hidden = true;
|
| 413 |
+
return;
|
| 414 |
+
}
|
| 415 |
+
const url = fileDataUrl(zipFileData);
|
| 416 |
+
if (!url) {
|
| 417 |
+
link.hidden = true;
|
| 418 |
+
return;
|
| 419 |
+
}
|
| 420 |
+
link.href = url;
|
| 421 |
+
link.hidden = false;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
/**
|
| 425 |
+
* Resolve a FileData (or string path) to a playable/downloadable URL.
|
| 426 |
+
* The Gradio wire protocol gives us either:
|
| 427 |
+
* - { url: "https://...", path: "/abs/path" } — use url directly
|
| 428 |
+
* - { url: null, path: "/abs/path" } — build via /gradio_api/file=
|
| 429 |
+
* - "/abs/path" — build via /gradio_api/file=
|
| 430 |
+
* - "https://..." — use directly
|
| 431 |
+
*/
|
| 432 |
+
function fileDataUrl(fileData) {
|
| 433 |
+
if (!fileData) return null;
|
| 434 |
+
if (typeof fileData === "string") {
|
| 435 |
+
if (/^https?:\/\//.test(fileData)) return fileData;
|
| 436 |
+
// Gradio expects the path UNencoded after "file=" — it does its own
|
| 437 |
+
// path parsing. Encoding with encodeURIComponent would break it.
|
| 438 |
+
return `${window.location.origin}/gradio_api/file=${fileData}`;
|
| 439 |
+
}
|
| 440 |
+
if (fileData.url) return fileData.url;
|
| 441 |
+
if (fileData.path) {
|
| 442 |
+
return `${window.location.origin}/gradio_api/file=${fileData.path}`;
|
| 443 |
+
}
|
| 444 |
+
return null;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// ------------------------------------------------------------------ //
|
| 448 |
+
// Clear //
|
| 449 |
+
// ------------------------------------------------------------------ //
|
| 450 |
+
function clearOutputs() {
|
| 451 |
+
$("#stems").innerHTML = "";
|
| 452 |
+
$("#zip-link").hidden = true;
|
| 453 |
+
$("#empty-state").classList.remove("hidden");
|
| 454 |
+
setMetrics({});
|
| 455 |
+
setStatus("Ready. Upload audio and click <strong>Separate</strong>.", "ready");
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
// ------------------------------------------------------------------ //
|
| 459 |
+
// Health badge //
|
| 460 |
+
// ------------------------------------------------------------------ //
|
| 461 |
+
async function loadHealth() {
|
| 462 |
+
try {
|
| 463 |
+
const res = await fetch("/healthz");
|
| 464 |
+
const data = await res.json();
|
| 465 |
+
$("#device-badge").textContent = `device: ${data.device || "—"}`;
|
| 466 |
+
$("#engine-badge").textContent = `engine: ${data.engine_installed ? "ok" : "missing"}`;
|
| 467 |
+
$("#footer-device").textContent = data.device || "—";
|
| 468 |
+
$("#footer-engine").textContent = data.engine_installed ? "installed" : "missing";
|
| 469 |
+
} catch (err) {
|
| 470 |
+
$("#device-badge").textContent = "device: ?";
|
| 471 |
+
$("#engine-badge").textContent = "engine: ?";
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// ------------------------------------------------------------------ //
|
| 476 |
+
// Init //
|
| 477 |
+
// ------------------------------------------------------------------ //
|
| 478 |
+
async function init() {
|
| 479 |
+
// Bind slider value displays
|
| 480 |
+
bindSliderDisplay("norm-thresh", "norm-thresh-val");
|
| 481 |
+
bindSliderDisplay("amp-thresh", "amp-thresh-val");
|
| 482 |
+
bindSliderDisplay("batch-size", "batch-size-val");
|
| 483 |
+
bindSliderDisplay("seg-size", "seg-size-val");
|
| 484 |
+
bindSliderDisplay("overlap-rof", "overlap-rof-val");
|
| 485 |
+
bindSliderDisplay("overlap-mdx", "overlap-mdx-val", (v) => parseFloat(v).toFixed(3));
|
| 486 |
+
bindSliderDisplay("pitch-shift", "pitch-shift-val");
|
| 487 |
+
bindSliderDisplay("hop-length", "hop-length-val");
|
| 488 |
+
bindSliderDisplay("window-size", "window-size-val");
|
| 489 |
+
bindSliderDisplay("aggression", "aggression-val");
|
| 490 |
+
bindSliderDisplay("post-thresh", "post-thresh-val");
|
| 491 |
+
bindSliderDisplay("shifts", "shifts-val");
|
| 492 |
+
|
| 493 |
+
initDropzone();
|
| 494 |
+
initAdvancedToggle();
|
| 495 |
+
|
| 496 |
+
// Wire up events
|
| 497 |
+
$("#use-case").addEventListener("change", onUseCaseChange);
|
| 498 |
+
$("#model").addEventListener("change", onModelChange);
|
| 499 |
+
$("#separate-btn").addEventListener("click", runSeparation);
|
| 500 |
+
$("#clear-btn").addEventListener("click", clearOutputs);
|
| 501 |
+
|
| 502 |
+
// Connect Gradio client to the current origin (as the blog shows)
|
| 503 |
+
try {
|
| 504 |
+
state.client = await Client.connect(window.location.origin);
|
| 505 |
+
} catch (err) {
|
| 506 |
+
console.error("Failed to connect Gradio client:", err);
|
| 507 |
+
setStatus(`Failed to connect to backend: ${err.message || err}`, "error");
|
| 508 |
+
return;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
await Promise.all([loadHealth(), loadPresets(), loadHistory()]);
|
| 512 |
+
|
| 513 |
+
// Initial advanced-group visibility (matches default preset's arch)
|
| 514 |
+
onModelChange();
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
// The Gradio client is loaded as an ESM module via a separate <script type="module">
|
| 518 |
+
// tag, which executes asynchronously. Wait for it to expose `window.gradio`
|
| 519 |
+
// before initializing.
|
| 520 |
+
function waitForClient() {
|
| 521 |
+
return new Promise((resolve) => {
|
| 522 |
+
if (window.gradio) return resolve();
|
| 523 |
+
window.addEventListener("gradio-client-ready", () => resolve(), { once: true });
|
| 524 |
+
// Safety timeout — if the CDN fails, surface an error after 15s.
|
| 525 |
+
setTimeout(() => resolve(), 15000);
|
| 526 |
+
});
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
async function bootstrap() {
|
| 530 |
+
await waitForClient();
|
| 531 |
+
if (!window.gradio) {
|
| 532 |
+
setStatus("Failed to load the Gradio JS client from CDN. Check your network connection.", "error");
|
| 533 |
+
return;
|
| 534 |
+
}
|
| 535 |
+
await init();
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
// The Gradio client ESM loads async; app.js is `defer` so DOM is ready.
|
| 539 |
+
if (document.readyState === "loading") {
|
| 540 |
+
document.addEventListener("DOMContentLoaded", bootstrap);
|
| 541 |
+
} else {
|
| 542 |
+
bootstrap();
|
| 543 |
+
}
|
| 544 |
+
})();
|
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Audio Separator — Gradio Server Edition</title>
|
| 7 |
+
<meta name="description" content="Separate any audio into stems (vocals, instrumental, drums, bass…). Powered by audio-separator + Gradio Server." />
|
| 8 |
+
|
| 9 |
+
<!-- Fonts: Syne (display) + JetBrains Mono (code/labels) + Inter (body) -->
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 11 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
| 13 |
+
|
| 14 |
+
<!-- App styles -->
|
| 15 |
+
<link rel="stylesheet" href="/static/style.css" />
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
<!-- Background gradient layer -->
|
| 19 |
+
<div class="bg-glow" aria-hidden="true"></div>
|
| 20 |
+
|
| 21 |
+
<!-- =========================================================== -->
|
| 22 |
+
<!-- Top nav (slim) -->
|
| 23 |
+
<!-- =========================================================== -->
|
| 24 |
+
<nav class="topbar">
|
| 25 |
+
<div class="topbar-brand">
|
| 26 |
+
<span class="logo-dot"></span>
|
| 27 |
+
<span class="brand-name">audio<span class="accent">·</span>separator</span>
|
| 28 |
+
</div>
|
| 29 |
+
<div class="topbar-meta">
|
| 30 |
+
<span id="device-badge" class="badge">device: —</span>
|
| 31 |
+
<span id="engine-badge" class="badge">engine: —</span>
|
| 32 |
+
<a class="topbar-link" href="https://huggingface.co/blog/introducing-gradio-server" target="_blank" rel="noopener">built on gradio.server ↗</a>
|
| 33 |
+
</div>
|
| 34 |
+
</nav>
|
| 35 |
+
|
| 36 |
+
<!-- =========================================================== -->
|
| 37 |
+
<!-- Hero -->
|
| 38 |
+
<!-- =========================================================== -->
|
| 39 |
+
<header class="hero">
|
| 40 |
+
<h1>Separate any track into <span class="accent-text">stems</span>.</h1>
|
| 41 |
+
<p>
|
| 42 |
+
Upload a song, pick a use case, and the right model loads automatically.
|
| 43 |
+
Powered by <a href="https://github.com/nomadkaraoke/python-audio-separator" target="_blank" rel="noopener"><code>audio-separator</code></a>
|
| 44 |
+
on the backend and a hand-built vanilla frontend talking to a
|
| 45 |
+
<code>gradio.Server</code> API. No build step. No framework on the client.
|
| 46 |
+
</p>
|
| 47 |
+
</header>
|
| 48 |
+
|
| 49 |
+
<!-- =========================================================== -->
|
| 50 |
+
<!-- Main grid -->
|
| 51 |
+
<!-- =========================================================== -->
|
| 52 |
+
<main class="grid">
|
| 53 |
+
|
| 54 |
+
<!-- ============ LEFT: input + controls ============ -->
|
| 55 |
+
<section class="panel panel-input">
|
| 56 |
+
<div class="section-label">Input & use case</div>
|
| 57 |
+
|
| 58 |
+
<!-- Dropzone (click or drag) -->
|
| 59 |
+
<div id="dropzone" class="dropzone" tabindex="0" role="button" aria-label="Upload audio">
|
| 60 |
+
<input id="file-input" type="file" accept="audio/*" hidden />
|
| 61 |
+
<div class="dropzone-inner">
|
| 62 |
+
<svg class="dropzone-icon" viewBox="0 0 24 24" width="36" height="36" aria-hidden="true">
|
| 63 |
+
<path d="M12 3v12m0-12l-4 4m4-4l4 4M5 21h14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
| 64 |
+
</svg>
|
| 65 |
+
<div class="dropzone-text">
|
| 66 |
+
<strong>Drop audio here</strong>
|
| 67 |
+
<span>or click to browse · wav, mp3, flac, m4a, ogg</span>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Audio preview + duration -->
|
| 73 |
+
<div id="preview-wrap" class="preview-wrap" hidden>
|
| 74 |
+
<audio id="preview" controls preload="metadata"></audio>
|
| 75 |
+
<div class="preview-meta">
|
| 76 |
+
<span id="preview-name" class="preview-name">—</span>
|
| 77 |
+
<span id="preview-dur" class="preview-dur">—</span>
|
| 78 |
+
<button id="clear-input" type="button" class="link-btn">remove</button>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- Use case dropdown -->
|
| 83 |
+
<div class="field">
|
| 84 |
+
<label for="use-case">Use case <span class="hint">— picks the right model for you</span></label>
|
| 85 |
+
<select id="use-case"></select>
|
| 86 |
+
<p id="preset-desc" class="preset-desc"></p>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<!-- Model dropdown -->
|
| 90 |
+
<div class="field">
|
| 91 |
+
<label for="model">Model <span class="hint">— auto-selected, override anytime</span></label>
|
| 92 |
+
<select id="model"></select>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<!-- Advanced toggle -->
|
| 96 |
+
<label class="toggle-row">
|
| 97 |
+
<input id="advanced" type="checkbox" />
|
| 98 |
+
<span>Advanced mode <span class="hint">(show all parameters)</span></span>
|
| 99 |
+
</label>
|
| 100 |
+
|
| 101 |
+
<!-- Global settings (always visible — small) -->
|
| 102 |
+
<details class="sub-accordion">
|
| 103 |
+
<summary>Global settings</summary>
|
| 104 |
+
<div class="details-body">
|
| 105 |
+
<div class="field">
|
| 106 |
+
<label for="output-format">Output format</label>
|
| 107 |
+
<select id="output-format">
|
| 108 |
+
<option value="wav" selected>WAV (lossless)</option>
|
| 109 |
+
<option value="flac">FLAC (lossless, compressed)</option>
|
| 110 |
+
<option value="mp3">MP3 (320 kbps)</option>
|
| 111 |
+
</select>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="field">
|
| 114 |
+
<label for="norm-thresh">Normalization threshold: <span class="val" id="norm-thresh-val">0.9</span></label>
|
| 115 |
+
<input id="norm-thresh" type="range" min="0.1" max="1.0" step="0.1" value="0.9" />
|
| 116 |
+
</div>
|
| 117 |
+
<div class="field">
|
| 118 |
+
<label for="amp-thresh">Amplification threshold: <span class="val" id="amp-thresh-val">0.6</span></label>
|
| 119 |
+
<input id="amp-thresh" type="range" min="0.1" max="1.0" step="0.1" value="0.6" />
|
| 120 |
+
</div>
|
| 121 |
+
<div class="field">
|
| 122 |
+
<label for="batch-size">Batch size: <span class="val" id="batch-size-val">1</span></label>
|
| 123 |
+
<input id="batch-size" type="range" min="1" max="16" step="1" value="1" />
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</details>
|
| 127 |
+
|
| 128 |
+
<!-- Advanced parameters (per-architecture, hidden by default) -->
|
| 129 |
+
<details id="adv-accordion" class="sub-accordion" hidden>
|
| 130 |
+
<summary>Advanced parameters <span class="hint">(architecture-specific)</span></summary>
|
| 131 |
+
<div class="details-body">
|
| 132 |
+
|
| 133 |
+
<div id="adv-rof" class="adv-group" hidden>
|
| 134 |
+
<div class="section-label small">Roformer / MDX23C</div>
|
| 135 |
+
<div class="field">
|
| 136 |
+
<label for="seg-size">Segment size: <span class="val" id="seg-size-val">256</span></label>
|
| 137 |
+
<input id="seg-size" type="range" min="32" max="4000" step="32" value="256" />
|
| 138 |
+
</div>
|
| 139 |
+
<div class="field">
|
| 140 |
+
<label for="overlap-rof">Overlap: <span class="val" id="overlap-rof-val">8</span></label>
|
| 141 |
+
<input id="overlap-rof" type="range" min="2" max="50" step="1" value="8" />
|
| 142 |
+
</div>
|
| 143 |
+
<div class="field">
|
| 144 |
+
<label for="pitch-shift">Pitch shift (semitones): <span class="val" id="pitch-shift-val">0</span></label>
|
| 145 |
+
<input id="pitch-shift" type="range" min="-12" max="12" step="1" value="0" />
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<div id="adv-mdx" class="adv-group" hidden>
|
| 150 |
+
<div class="section-label small">MDX-NET</div>
|
| 151 |
+
<div class="field">
|
| 152 |
+
<label for="hop-length">Hop length: <span class="val" id="hop-length-val">1024</span></label>
|
| 153 |
+
<input id="hop-length" type="range" min="32" max="2048" step="32" value="1024" />
|
| 154 |
+
</div>
|
| 155 |
+
<div class="field">
|
| 156 |
+
<label for="overlap-mdx">Overlap (MDX/Demucs): <span class="val" id="overlap-mdx-val">0.25</span></label>
|
| 157 |
+
<input id="overlap-mdx" type="range" min="0.001" max="0.999" step="0.001" value="0.25" />
|
| 158 |
+
</div>
|
| 159 |
+
<label class="toggle-row">
|
| 160 |
+
<input id="enable-denoise" type="checkbox" />
|
| 161 |
+
<span>Enable denoise</span>
|
| 162 |
+
</label>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<div id="adv-vr" class="adv-group" hidden>
|
| 166 |
+
<div class="section-label small">VR Arch</div>
|
| 167 |
+
<div class="field">
|
| 168 |
+
<label for="window-size">Window size: <span class="val" id="window-size-val">512</span></label>
|
| 169 |
+
<input id="window-size" type="range" min="320" max="1024" step="32" value="512" />
|
| 170 |
+
</div>
|
| 171 |
+
<div class="field">
|
| 172 |
+
<label for="aggression">Aggression: <span class="val" id="aggression-val">5</span></label>
|
| 173 |
+
<input id="aggression" type="range" min="1" max="50" step="1" value="5" />
|
| 174 |
+
</div>
|
| 175 |
+
<label class="toggle-row">
|
| 176 |
+
<input id="enable-tta" type="checkbox" />
|
| 177 |
+
<span>Enable TTA</span>
|
| 178 |
+
</label>
|
| 179 |
+
<label class="toggle-row">
|
| 180 |
+
<input id="enable-post" type="checkbox" />
|
| 181 |
+
<span>Enable post-process</span>
|
| 182 |
+
</label>
|
| 183 |
+
<div class="field">
|
| 184 |
+
<label for="post-thresh">Post-process threshold: <span class="val" id="post-thresh-val">0.2</span></label>
|
| 185 |
+
<input id="post-thresh" type="range" min="0.1" max="0.3" step="0.1" value="0.2" />
|
| 186 |
+
</div>
|
| 187 |
+
<label class="toggle-row">
|
| 188 |
+
<input id="enable-high-end" type="checkbox" />
|
| 189 |
+
<span>High-end processing</span>
|
| 190 |
+
</label>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<div id="adv-demucs" class="adv-group" hidden>
|
| 194 |
+
<div class="section-label small">Demucs</div>
|
| 195 |
+
<div class="field">
|
| 196 |
+
<label for="shifts">Shifts: <span class="val" id="shifts-val">2</span></label>
|
| 197 |
+
<input id="shifts" type="range" min="0" max="20" step="1" value="2" />
|
| 198 |
+
</div>
|
| 199 |
+
<label class="toggle-row">
|
| 200 |
+
<input id="segments-enabled" type="checkbox" checked />
|
| 201 |
+
<span>Enable segments</span>
|
| 202 |
+
</label>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
</div>
|
| 206 |
+
</details>
|
| 207 |
+
|
| 208 |
+
<!-- Action buttons -->
|
| 209 |
+
<div class="actions">
|
| 210 |
+
<button id="separate-btn" class="btn btn-primary" disabled>
|
| 211 |
+
<span class="btn-label">Separate audio</span>
|
| 212 |
+
<span class="btn-spinner" hidden></span>
|
| 213 |
+
</button>
|
| 214 |
+
<button id="clear-btn" class="btn btn-secondary" type="button">Clear</button>
|
| 215 |
+
</div>
|
| 216 |
+
</section>
|
| 217 |
+
|
| 218 |
+
<!-- ============ RIGHT: output ============ -->
|
| 219 |
+
<section class="panel panel-output">
|
| 220 |
+
<div class="section-label">Output stems</div>
|
| 221 |
+
|
| 222 |
+
<!-- Metric tiles -->
|
| 223 |
+
<div class="metrics">
|
| 224 |
+
<div class="metric-tile">
|
| 225 |
+
<div class="metric-label">Input duration</div>
|
| 226 |
+
<div class="metric-value" id="metric-dur">—</div>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="metric-tile">
|
| 229 |
+
<div class="metric-label">Estimated time</div>
|
| 230 |
+
<div class="metric-value" id="metric-est">—</div>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="metric-tile">
|
| 233 |
+
<div class="metric-label">Stems</div>
|
| 234 |
+
<div class="metric-value" id="metric-stems">—</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<!-- Status -->
|
| 239 |
+
<div id="status" class="status ready">
|
| 240 |
+
Ready. Upload audio and click <strong>Separate</strong>.
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<!-- Stem cards (rendered dynamically) -->
|
| 244 |
+
<div id="stems" class="stems"></div>
|
| 245 |
+
|
| 246 |
+
<!-- ZIP download -->
|
| 247 |
+
<a id="zip-link" class="zip-card" hidden download>
|
| 248 |
+
<div class="zip-icon">📦</div>
|
| 249 |
+
<div class="zip-text">
|
| 250 |
+
<strong>Download all stems</strong>
|
| 251 |
+
<span>as a .zip archive</span>
|
| 252 |
+
</div>
|
| 253 |
+
<div class="zip-arrow">↓</div>
|
| 254 |
+
</a>
|
| 255 |
+
|
| 256 |
+
<!-- Empty-state illustration when no stems yet -->
|
| 257 |
+
<div id="empty-state" class="empty-state">
|
| 258 |
+
<svg viewBox="0 0 80 80" width="64" height="64" aria-hidden="true">
|
| 259 |
+
<circle cx="40" cy="40" r="36" fill="none" stroke="currentColor" stroke-width="1.2" stroke-dasharray="2 6" opacity="0.5"/>
|
| 260 |
+
<path d="M20 40 L20 40 M28 40 L28 40 M36 40 L36 40 M44 40 L44 40 M52 40 L52 40 M60 40 L60 40" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 261 |
+
</svg>
|
| 262 |
+
<p>Your stems will appear here.</p>
|
| 263 |
+
</div>
|
| 264 |
+
</section>
|
| 265 |
+
</main>
|
| 266 |
+
|
| 267 |
+
<!-- =========================================================== -->
|
| 268 |
+
<!-- History -->
|
| 269 |
+
<!-- =========================================================== -->
|
| 270 |
+
<section class="panel panel-history">
|
| 271 |
+
<details>
|
| 272 |
+
<summary>Recent separations</summary>
|
| 273 |
+
<div class="details-body">
|
| 274 |
+
<table class="history-table">
|
| 275 |
+
<thead>
|
| 276 |
+
<tr>
|
| 277 |
+
<th>Time</th><th>Use case</th><th>Model</th><th>Input</th><th>Stems</th>
|
| 278 |
+
</tr>
|
| 279 |
+
</thead>
|
| 280 |
+
<tbody id="history-body">
|
| 281 |
+
<tr class="empty-row"><td colspan="5">No history yet.</td></tr>
|
| 282 |
+
</tbody>
|
| 283 |
+
</table>
|
| 284 |
+
</div>
|
| 285 |
+
</details>
|
| 286 |
+
</section>
|
| 287 |
+
|
| 288 |
+
<!-- =========================================================== -->
|
| 289 |
+
<!-- FAQ -->
|
| 290 |
+
<!-- =========================================================== -->
|
| 291 |
+
<section class="panel panel-faq">
|
| 292 |
+
<details>
|
| 293 |
+
<summary>How to use / FAQ</summary>
|
| 294 |
+
<div class="details-body prose">
|
| 295 |
+
<h4>Quick start</h4>
|
| 296 |
+
<ol>
|
| 297 |
+
<li><strong>Upload</strong> an audio file (or drag it onto the dropzone).</li>
|
| 298 |
+
<li><strong>Pick a use case</strong> — the right model auto-selects.</li>
|
| 299 |
+
<li>Click <strong>Separate audio</strong>. First run downloads the model (can be hundreds of MB).</li>
|
| 300 |
+
<li>Listen to each stem and download the ZIP if you want them all.</li>
|
| 301 |
+
</ol>
|
| 302 |
+
|
| 303 |
+
<h4>When to override the model</h4>
|
| 304 |
+
<ul>
|
| 305 |
+
<li><strong>Karaoke</strong> → <code>MelBand Roformer | Karaoke V2 by Gabox</code></li>
|
| 306 |
+
<li><strong>Clean a cappella</strong> → <code>BS-Roformer-Viperx-1297</code> (highest reported SDR)</li>
|
| 307 |
+
<li><strong>De-reverb</strong> → <code>MelBand Roformer | De-Reverb by anvuew</code></li>
|
| 308 |
+
<li><strong>De-noise</strong> → <code>Mel-Roformer-Denoise-Aufr33</code></li>
|
| 309 |
+
<li><strong>Multi-stem remix</strong> → <code>htdemucs_ft.yaml</code> (4 stems) or <code>htdemucs_6s.yaml</code> (6 stems)</li>
|
| 310 |
+
<li><strong>CPU-only / speed</strong> → <code>UVR-MDX-NET-Inst_HQ_3.onnx</code></li>
|
| 311 |
+
</ul>
|
| 312 |
+
|
| 313 |
+
<h4>Tips</h4>
|
| 314 |
+
<ul>
|
| 315 |
+
<li>For best results use lossless inputs (WAV/FLAC).</li>
|
| 316 |
+
<li>Models auto-cache — first run is slow, later runs are fast.</li>
|
| 317 |
+
<li>MP3 output is encoded at <strong>320 kbps</strong> (not the library's 128 kbps default).</li>
|
| 318 |
+
<li>Transient <code>librosa.ParameterError</code> failures are retried automatically.</li>
|
| 319 |
+
</ul>
|
| 320 |
+
|
| 321 |
+
<h4>Credits</h4>
|
| 322 |
+
<ul>
|
| 323 |
+
<li>Engine: <a href="https://github.com/nomadkaraoke/python-audio-separator" target="_blank" rel="noopener"><code>audio-separator</code></a> by @berevadb</li>
|
| 324 |
+
<li>Models: UVR community contributors (Gabox, Aufr33, Viperx, Sucial, anvuew, Unwa, …)</li>
|
| 325 |
+
<li>Architecture: <a href="https://huggingface.co/blog/introducing-gradio-server" target="_blank" rel="noopener">Gradio Server</a> + vanilla HTML/CSS/JS frontend</li>
|
| 326 |
+
</ul>
|
| 327 |
+
</div>
|
| 328 |
+
</details>
|
| 329 |
+
</section>
|
| 330 |
+
|
| 331 |
+
<!-- =========================================================== -->
|
| 332 |
+
<!-- Footer -->
|
| 333 |
+
<!-- =========================================================== -->
|
| 334 |
+
<footer class="app-footer">
|
| 335 |
+
<span>DEVICE <b id="footer-device">—</b></span>
|
| 336 |
+
<span class="dot">·</span>
|
| 337 |
+
<span>ENGINE <b id="footer-engine">—</b></span>
|
| 338 |
+
<span class="dot">·</span>
|
| 339 |
+
<span>FRONTEND <b>VANILLA JS</b></span>
|
| 340 |
+
<span class="dot">·</span>
|
| 341 |
+
<span>BACKEND <b>GRADIO.SERVER</b></span>
|
| 342 |
+
</footer>
|
| 343 |
+
|
| 344 |
+
<!-- =========================================================== -->
|
| 345 |
+
<!-- Gradio JS client (loaded as ESM from CDN, as the blog -->
|
| 346 |
+
<!-- describes — no build step, no bundler on the client) -->
|
| 347 |
+
<!-- -->
|
| 348 |
+
<!-- We pin to @gradio/client@2.3.0 — the version that ships the -->
|
| 349 |
+
<!-- `Client.connect()` API the blog uses. Newer versions may -->
|
| 350 |
+
<!-- change the surface; older versions (<2.0) use a different -->
|
| 351 |
+
<!-- factory API (`client()` lowercase). -->
|
| 352 |
+
<!-- =========================================================== -->
|
| 353 |
+
<script type="module">
|
| 354 |
+
import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client@2.3.0/dist/index.min.js";
|
| 355 |
+
window.gradio = { Client, handle_file };
|
| 356 |
+
// Signal to app.js that the client is ready
|
| 357 |
+
window.dispatchEvent(new Event("gradio-client-ready"));
|
| 358 |
+
</script>
|
| 359 |
+
|
| 360 |
+
<!-- App logic (defer so it runs after DOM parse; waits for the client-ready event before init) -->
|
| 361 |
+
<script src="/static/app.js" defer></script>
|
| 362 |
+
</body>
|
| 363 |
+
</html>
|
|
@@ -0,0 +1,872 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================ *
|
| 2 |
+
* Audio Separator — Gradio Server Edition
|
| 3 |
+
* Vanilla CSS — no framework, no build step.
|
| 4 |
+
*
|
| 5 |
+
* Design philosophy (mirrors the Gradio Server blog post):
|
| 6 |
+
* "Bring your own frontend" — we ship a self-contained design
|
| 7 |
+
* system. No Gradio theme tokens on the client.
|
| 8 |
+
* ============================================================ */
|
| 9 |
+
|
| 10 |
+
/* ---------------------------------------------------------------- *
|
| 11 |
+
* Design tokens *
|
| 12 |
+
* ---------------------------------------------------------------- */
|
| 13 |
+
:root {
|
| 14 |
+
/* Palette — pure black canvas, electric-lime accent */
|
| 15 |
+
--bg: #000000;
|
| 16 |
+
--bg-elev-1: #0a0a0c;
|
| 17 |
+
--bg-elev-2: #121217;
|
| 18 |
+
--bg-elev-3: #1b1b22;
|
| 19 |
+
--bg-elev-4: #24242d;
|
| 20 |
+
|
| 21 |
+
--border: #232330;
|
| 22 |
+
--border-soft: #16161c;
|
| 23 |
+
|
| 24 |
+
--text: #f5f5f7;
|
| 25 |
+
--text-dim: #9b9bab;
|
| 26 |
+
--text-mute: #5e5e6a;
|
| 27 |
+
|
| 28 |
+
--accent: #c6ff00;
|
| 29 |
+
--accent-dim: #86b300;
|
| 30 |
+
--accent-glow: rgba(198, 255, 0, 0.18);
|
| 31 |
+
--accent-soft: rgba(198, 255, 0, 0.06);
|
| 32 |
+
|
| 33 |
+
--danger: #ff4d6d;
|
| 34 |
+
--success: #00d68f;
|
| 35 |
+
--warning: #ffb800;
|
| 36 |
+
|
| 37 |
+
/* Typography */
|
| 38 |
+
--font-display: "Syne", "Arial", sans-serif;
|
| 39 |
+
--font-body: "Inter", system-ui, -apple-system, sans-serif;
|
| 40 |
+
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
| 41 |
+
|
| 42 |
+
/* Geometry */
|
| 43 |
+
--radius: 14px;
|
| 44 |
+
--radius-sm: 8px;
|
| 45 |
+
--radius-xs: 4px;
|
| 46 |
+
|
| 47 |
+
/* Effects */
|
| 48 |
+
--shadow: 0 1px 0 rgba(255,255,255,0.04) inset,
|
| 49 |
+
0 8px 24px rgba(0,0,0,0.45);
|
| 50 |
+
--shadow-lg: 0 1px 0 rgba(255,255,255,0.06) inset,
|
| 51 |
+
0 16px 48px rgba(0,0,0,0.55);
|
| 52 |
+
--blur: blur(14px) saturate(140%);
|
| 53 |
+
|
| 54 |
+
/* Layout */
|
| 55 |
+
--max-width: 1320px;
|
| 56 |
+
--gutter: 28px;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* ---------------------------------------------------------------- *
|
| 60 |
+
* Reset & base *
|
| 61 |
+
* ---------------------------------------------------------------- */
|
| 62 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 63 |
+
|
| 64 |
+
html, body {
|
| 65 |
+
margin: 0;
|
| 66 |
+
padding: 0;
|
| 67 |
+
background: var(--bg);
|
| 68 |
+
color: var(--text);
|
| 69 |
+
font-family: var(--font-body);
|
| 70 |
+
font-size: 15px;
|
| 71 |
+
line-height: 1.55;
|
| 72 |
+
-webkit-font-smoothing: antialiased;
|
| 73 |
+
-moz-osx-font-smoothing: grayscale;
|
| 74 |
+
min-height: 100vh;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
body {
|
| 78 |
+
position: relative;
|
| 79 |
+
overflow-x: hidden;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Fixed radial-gradient backdrop for depth on pure black */
|
| 83 |
+
.bg-glow {
|
| 84 |
+
position: fixed;
|
| 85 |
+
inset: 0;
|
| 86 |
+
z-index: 0;
|
| 87 |
+
pointer-events: none;
|
| 88 |
+
background:
|
| 89 |
+
radial-gradient(900px 600px at 12% -10%, rgba(198,255,0,0.08), transparent 60%),
|
| 90 |
+
radial-gradient(700px 500px at 92% 8%, rgba(120,80,255,0.10), transparent 65%),
|
| 91 |
+
radial-gradient(800px 800px at 50% 120%, rgba(0,140,255,0.06), transparent 70%);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* All content sits above the backdrop */
|
| 95 |
+
.topbar, .hero, main, .panel-history, .panel-faq, .app-footer {
|
| 96 |
+
position: relative;
|
| 97 |
+
z-index: 1;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Constrain content width */
|
| 101 |
+
.topbar, .hero, main, .panel-history, .panel-faq, .app-footer {
|
| 102 |
+
max-width: var(--max-width);
|
| 103 |
+
margin-left: auto;
|
| 104 |
+
margin-right: auto;
|
| 105 |
+
padding-left: var(--gutter);
|
| 106 |
+
padding-right: var(--gutter);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
a {
|
| 110 |
+
color: var(--accent);
|
| 111 |
+
text-decoration: none;
|
| 112 |
+
border-bottom: 1px dashed rgba(198,255,0,0.4);
|
| 113 |
+
transition: border-style 0.15s ease;
|
| 114 |
+
}
|
| 115 |
+
a:hover { border-bottom-style: solid; }
|
| 116 |
+
|
| 117 |
+
code {
|
| 118 |
+
font-family: var(--font-mono);
|
| 119 |
+
color: var(--accent);
|
| 120 |
+
background: var(--accent-soft);
|
| 121 |
+
border: 1px solid rgba(198,255,0,0.15);
|
| 122 |
+
padding: 1px 6px;
|
| 123 |
+
border-radius: var(--radius-xs);
|
| 124 |
+
font-size: 0.86em;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
::selection { background: var(--accent); color: #000; }
|
| 128 |
+
|
| 129 |
+
/* ---------------------------------------------------------------- *
|
| 130 |
+
* Scrollbar *
|
| 131 |
+
* ---------------------------------------------------------------- */
|
| 132 |
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
| 133 |
+
::-webkit-scrollbar-track { background: var(--bg); }
|
| 134 |
+
::-webkit-scrollbar-thumb {
|
| 135 |
+
background: var(--bg-elev-3);
|
| 136 |
+
border-radius: 5px;
|
| 137 |
+
border: 2px solid var(--bg);
|
| 138 |
+
}
|
| 139 |
+
::-webkit-scrollbar-thumb:hover { background: #2a2a36; }
|
| 140 |
+
|
| 141 |
+
/* ---------------------------------------------------------------- *
|
| 142 |
+
* Topbar *
|
| 143 |
+
* ---------------------------------------------------------------- */
|
| 144 |
+
.topbar {
|
| 145 |
+
display: flex;
|
| 146 |
+
align-items: center;
|
| 147 |
+
justify-content: space-between;
|
| 148 |
+
padding-top: 18px;
|
| 149 |
+
padding-bottom: 18px;
|
| 150 |
+
border-bottom: 1px solid var(--border-soft);
|
| 151 |
+
}
|
| 152 |
+
.topbar-brand {
|
| 153 |
+
display: flex;
|
| 154 |
+
align-items: center;
|
| 155 |
+
gap: 10px;
|
| 156 |
+
font-family: var(--font-display);
|
| 157 |
+
font-weight: 700;
|
| 158 |
+
font-size: 1.05rem;
|
| 159 |
+
letter-spacing: -0.01em;
|
| 160 |
+
}
|
| 161 |
+
.topbar-brand .accent { color: var(--accent); }
|
| 162 |
+
.logo-dot {
|
| 163 |
+
width: 10px; height: 10px;
|
| 164 |
+
background: var(--accent);
|
| 165 |
+
border-radius: 50%;
|
| 166 |
+
box-shadow: 0 0 12px var(--accent);
|
| 167 |
+
animation: pulse 2.2s ease-in-out infinite;
|
| 168 |
+
}
|
| 169 |
+
@keyframes pulse {
|
| 170 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 171 |
+
50% { opacity: 0.4; transform: scale(0.75); }
|
| 172 |
+
}
|
| 173 |
+
.topbar-meta {
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
gap: 14px;
|
| 177 |
+
font-family: var(--font-mono);
|
| 178 |
+
font-size: 0.72rem;
|
| 179 |
+
}
|
| 180 |
+
.badge {
|
| 181 |
+
padding: 4px 10px;
|
| 182 |
+
background: var(--bg-elev-2);
|
| 183 |
+
border: 1px solid var(--border);
|
| 184 |
+
border-radius: 999px;
|
| 185 |
+
color: var(--text-dim);
|
| 186 |
+
letter-spacing: 0.05em;
|
| 187 |
+
}
|
| 188 |
+
.topbar-link {
|
| 189 |
+
border: none;
|
| 190 |
+
color: var(--text-dim);
|
| 191 |
+
font-family: var(--font-mono);
|
| 192 |
+
font-size: 0.72rem;
|
| 193 |
+
}
|
| 194 |
+
.topbar-link:hover { color: var(--accent); }
|
| 195 |
+
|
| 196 |
+
@media (max-width: 720px) {
|
| 197 |
+
.topbar { flex-direction: column; gap: 12px; align-items: flex-start; }
|
| 198 |
+
.topbar-meta { flex-wrap: wrap; gap: 8px; }
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/* ---------------------------------------------------------------- *
|
| 202 |
+
* Hero *
|
| 203 |
+
* ---------------------------------------------------------------- */
|
| 204 |
+
.hero {
|
| 205 |
+
padding: 56px 0 40px;
|
| 206 |
+
}
|
| 207 |
+
.hero h1 {
|
| 208 |
+
font-family: var(--font-display);
|
| 209 |
+
font-weight: 800;
|
| 210 |
+
font-size: clamp(2.4rem, 5.5vw, 4rem);
|
| 211 |
+
line-height: 1.05;
|
| 212 |
+
letter-spacing: -0.02em;
|
| 213 |
+
margin: 0 0 18px;
|
| 214 |
+
background: linear-gradient(180deg, #ffffff 0%, #b0b0b8 100%);
|
| 215 |
+
-webkit-background-clip: text;
|
| 216 |
+
background-clip: text;
|
| 217 |
+
-webkit-text-fill-color: transparent;
|
| 218 |
+
}
|
| 219 |
+
.hero .accent-text {
|
| 220 |
+
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-dim) 100%);
|
| 221 |
+
-webkit-background-clip: text;
|
| 222 |
+
background-clip: text;
|
| 223 |
+
-webkit-text-fill-color: transparent;
|
| 224 |
+
}
|
| 225 |
+
.hero p {
|
| 226 |
+
font-size: 1.05rem;
|
| 227 |
+
color: var(--text-dim);
|
| 228 |
+
max-width: 720px;
|
| 229 |
+
margin: 0;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* ---------------------------------------------------------------- *
|
| 233 |
+
* Main grid — two columns on desktop, stacked on mobile *
|
| 234 |
+
* ---------------------------------------------------------------- */
|
| 235 |
+
.grid {
|
| 236 |
+
display: grid;
|
| 237 |
+
grid-template-columns: 1fr 1fr;
|
| 238 |
+
gap: 24px;
|
| 239 |
+
padding-top: 8px;
|
| 240 |
+
padding-bottom: 32px;
|
| 241 |
+
}
|
| 242 |
+
@media (max-width: 980px) {
|
| 243 |
+
.grid { grid-template-columns: 1fr; }
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* ---------------------------------------------------------------- *
|
| 247 |
+
* Panel — generic surface *
|
| 248 |
+
* ---------------------------------------------------------------- */
|
| 249 |
+
.panel {
|
| 250 |
+
background: var(--bg-elev-1);
|
| 251 |
+
border: 1px solid var(--border);
|
| 252 |
+
border-radius: var(--radius);
|
| 253 |
+
padding: 24px;
|
| 254 |
+
box-shadow: var(--shadow);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.section-label {
|
| 258 |
+
font-family: var(--font-mono);
|
| 259 |
+
font-size: 0.7rem;
|
| 260 |
+
font-weight: 600;
|
| 261 |
+
letter-spacing: 0.18em;
|
| 262 |
+
text-transform: uppercase;
|
| 263 |
+
color: var(--accent);
|
| 264 |
+
padding-bottom: 14px;
|
| 265 |
+
margin-bottom: 18px;
|
| 266 |
+
border-bottom: 1px solid var(--border-soft);
|
| 267 |
+
display: flex;
|
| 268 |
+
align-items: center;
|
| 269 |
+
gap: 8px;
|
| 270 |
+
}
|
| 271 |
+
.section-label::before {
|
| 272 |
+
content: "";
|
| 273 |
+
width: 6px; height: 6px;
|
| 274 |
+
background: var(--accent);
|
| 275 |
+
border-radius: 50%;
|
| 276 |
+
box-shadow: 0 0 8px var(--accent);
|
| 277 |
+
}
|
| 278 |
+
.section-label.small {
|
| 279 |
+
font-size: 0.62rem;
|
| 280 |
+
letter-spacing: 0.14em;
|
| 281 |
+
padding-bottom: 8px;
|
| 282 |
+
margin-bottom: 12px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* ---------------------------------------------------------------- *
|
| 286 |
+
* Dropzone *
|
| 287 |
+
* ---------------------------------------------------------------- */
|
| 288 |
+
.dropzone {
|
| 289 |
+
border: 2px dashed var(--border);
|
| 290 |
+
border-radius: var(--radius);
|
| 291 |
+
padding: 32px 24px;
|
| 292 |
+
text-align: center;
|
| 293 |
+
cursor: pointer;
|
| 294 |
+
transition: all 0.2s ease;
|
| 295 |
+
background: var(--bg-elev-2);
|
| 296 |
+
margin-bottom: 18px;
|
| 297 |
+
}
|
| 298 |
+
.dropzone:hover, .dropzone:focus-visible {
|
| 299 |
+
border-color: var(--accent);
|
| 300 |
+
background: var(--accent-soft);
|
| 301 |
+
outline: none;
|
| 302 |
+
}
|
| 303 |
+
.dropzone.dragging {
|
| 304 |
+
border-color: var(--accent);
|
| 305 |
+
background: var(--accent-soft);
|
| 306 |
+
transform: scale(1.01);
|
| 307 |
+
}
|
| 308 |
+
.dropzone-inner {
|
| 309 |
+
display: flex;
|
| 310 |
+
flex-direction: column;
|
| 311 |
+
align-items: center;
|
| 312 |
+
gap: 10px;
|
| 313 |
+
color: var(--text-dim);
|
| 314 |
+
}
|
| 315 |
+
.dropzone-icon { color: var(--accent); }
|
| 316 |
+
.dropzone-text { display: flex; flex-direction: column; gap: 2px; }
|
| 317 |
+
.dropzone-text strong {
|
| 318 |
+
font-family: var(--font-display);
|
| 319 |
+
color: var(--text);
|
| 320 |
+
font-weight: 700;
|
| 321 |
+
font-size: 1rem;
|
| 322 |
+
}
|
| 323 |
+
.dropzone-text span { font-size: 0.82rem; color: var(--text-mute); }
|
| 324 |
+
|
| 325 |
+
/* ---------------------------------------------------------------- *
|
| 326 |
+
* Preview *
|
| 327 |
+
* ---------------------------------------------------------------- */
|
| 328 |
+
.preview-wrap {
|
| 329 |
+
margin-bottom: 18px;
|
| 330 |
+
padding: 14px;
|
| 331 |
+
background: var(--bg-elev-2);
|
| 332 |
+
border: 1px solid var(--border);
|
| 333 |
+
border-left: 3px solid var(--accent);
|
| 334 |
+
border-radius: var(--radius-sm);
|
| 335 |
+
}
|
| 336 |
+
.preview-wrap audio {
|
| 337 |
+
width: 100%;
|
| 338 |
+
margin-bottom: 8px;
|
| 339 |
+
}
|
| 340 |
+
.preview-meta {
|
| 341 |
+
display: flex;
|
| 342 |
+
align-items: center;
|
| 343 |
+
gap: 12px;
|
| 344 |
+
font-family: var(--font-mono);
|
| 345 |
+
font-size: 0.78rem;
|
| 346 |
+
color: var(--text-dim);
|
| 347 |
+
}
|
| 348 |
+
.preview-name {
|
| 349 |
+
flex: 1;
|
| 350 |
+
color: var(--text);
|
| 351 |
+
overflow: hidden;
|
| 352 |
+
text-overflow: ellipsis;
|
| 353 |
+
white-space: nowrap;
|
| 354 |
+
}
|
| 355 |
+
.link-btn {
|
| 356 |
+
background: none;
|
| 357 |
+
border: none;
|
| 358 |
+
color: var(--text-mute);
|
| 359 |
+
font-family: var(--font-mono);
|
| 360 |
+
font-size: 0.74rem;
|
| 361 |
+
cursor: pointer;
|
| 362 |
+
padding: 2px 6px;
|
| 363 |
+
border-bottom: 1px dashed var(--text-mute);
|
| 364 |
+
}
|
| 365 |
+
.link-btn:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
| 366 |
+
|
| 367 |
+
/* ---------------------------------------------------------------- *
|
| 368 |
+
* Fields, inputs, sliders *
|
| 369 |
+
* ---------------------------------------------------------------- */
|
| 370 |
+
.field {
|
| 371 |
+
margin-bottom: 16px;
|
| 372 |
+
}
|
| 373 |
+
.field label {
|
| 374 |
+
display: block;
|
| 375 |
+
font-family: var(--font-mono);
|
| 376 |
+
font-size: 0.72rem;
|
| 377 |
+
letter-spacing: 0.08em;
|
| 378 |
+
text-transform: uppercase;
|
| 379 |
+
color: var(--text-dim);
|
| 380 |
+
font-weight: 500;
|
| 381 |
+
margin-bottom: 6px;
|
| 382 |
+
}
|
| 383 |
+
.field label .hint {
|
| 384 |
+
text-transform: none;
|
| 385 |
+
letter-spacing: 0;
|
| 386 |
+
color: var(--text-mute);
|
| 387 |
+
font-weight: 400;
|
| 388 |
+
}
|
| 389 |
+
.field label .val {
|
| 390 |
+
color: var(--accent);
|
| 391 |
+
font-weight: 600;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
select, input[type="text"], input[type="number"], textarea {
|
| 395 |
+
width: 100%;
|
| 396 |
+
padding: 10px 12px;
|
| 397 |
+
background: var(--bg-elev-2);
|
| 398 |
+
border: 1px solid var(--border);
|
| 399 |
+
border-radius: var(--radius-sm);
|
| 400 |
+
color: var(--text);
|
| 401 |
+
font-family: var(--font-body);
|
| 402 |
+
font-size: 0.92rem;
|
| 403 |
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
| 404 |
+
appearance: none;
|
| 405 |
+
-webkit-appearance: none;
|
| 406 |
+
}
|
| 407 |
+
select {
|
| 408 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239b9bab' d='M6 9L1 3h10z'/%3E%3C/svg%3E");
|
| 409 |
+
background-repeat: no-repeat;
|
| 410 |
+
background-position: right 12px center;
|
| 411 |
+
padding-right: 32px;
|
| 412 |
+
}
|
| 413 |
+
select:focus, input:focus, textarea:focus {
|
| 414 |
+
outline: none;
|
| 415 |
+
border-color: var(--accent);
|
| 416 |
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
/* Range slider — electric-lime */
|
| 420 |
+
input[type="range"] {
|
| 421 |
+
-webkit-appearance: none;
|
| 422 |
+
appearance: none;
|
| 423 |
+
width: 100%;
|
| 424 |
+
height: 4px;
|
| 425 |
+
background: var(--bg-elev-3);
|
| 426 |
+
border-radius: 2px;
|
| 427 |
+
outline: none;
|
| 428 |
+
}
|
| 429 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 430 |
+
-webkit-appearance: none;
|
| 431 |
+
appearance: none;
|
| 432 |
+
width: 16px;
|
| 433 |
+
height: 16px;
|
| 434 |
+
background: var(--accent);
|
| 435 |
+
border-radius: 50%;
|
| 436 |
+
cursor: pointer;
|
| 437 |
+
border: 2px solid var(--bg);
|
| 438 |
+
box-shadow: 0 0 0 1px var(--accent-dim), 0 0 8px var(--accent-glow);
|
| 439 |
+
transition: transform 0.1s ease;
|
| 440 |
+
}
|
| 441 |
+
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
| 442 |
+
input[type="range"]::-moz-range-thumb {
|
| 443 |
+
width: 16px;
|
| 444 |
+
height: 16px;
|
| 445 |
+
background: var(--accent);
|
| 446 |
+
border-radius: 50%;
|
| 447 |
+
cursor: pointer;
|
| 448 |
+
border: 2px solid var(--bg);
|
| 449 |
+
box-shadow: 0 0 0 1px var(--accent-dim);
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/* Toggle row (checkbox + label) */
|
| 453 |
+
.toggle-row {
|
| 454 |
+
display: flex;
|
| 455 |
+
align-items: center;
|
| 456 |
+
gap: 10px;
|
| 457 |
+
padding: 8px 0;
|
| 458 |
+
cursor: pointer;
|
| 459 |
+
user-select: none;
|
| 460 |
+
}
|
| 461 |
+
.toggle-row input[type="checkbox"] {
|
| 462 |
+
appearance: none;
|
| 463 |
+
-webkit-appearance: none;
|
| 464 |
+
width: 18px;
|
| 465 |
+
height: 18px;
|
| 466 |
+
background: var(--bg-elev-3);
|
| 467 |
+
border: 1px solid var(--border);
|
| 468 |
+
border-radius: 4px;
|
| 469 |
+
cursor: pointer;
|
| 470 |
+
position: relative;
|
| 471 |
+
flex-shrink: 0;
|
| 472 |
+
transition: all 0.15s ease;
|
| 473 |
+
}
|
| 474 |
+
.toggle-row input[type="checkbox"]:checked {
|
| 475 |
+
background: var(--accent);
|
| 476 |
+
border-color: var(--accent);
|
| 477 |
+
}
|
| 478 |
+
.toggle-row input[type="checkbox"]:checked::after {
|
| 479 |
+
content: "";
|
| 480 |
+
position: absolute;
|
| 481 |
+
top: 2px; left: 5px;
|
| 482 |
+
width: 5px; height: 9px;
|
| 483 |
+
border: solid #000;
|
| 484 |
+
border-width: 0 2px 2px 0;
|
| 485 |
+
transform: rotate(45deg);
|
| 486 |
+
}
|
| 487 |
+
.toggle-row span { color: var(--text); font-size: 0.92rem; }
|
| 488 |
+
.toggle-row .hint { color: var(--text-mute); font-size: 0.82rem; }
|
| 489 |
+
|
| 490 |
+
/* ---------------------------------------------------------------- *
|
| 491 |
+
* Sub-accordion (details/summary) *
|
| 492 |
+
* ---------------------------------------------------------------- */
|
| 493 |
+
.sub-accordion, .panel-history details, .panel-faq details {
|
| 494 |
+
margin-top: 12px;
|
| 495 |
+
border: 1px solid var(--border);
|
| 496 |
+
border-radius: var(--radius-sm);
|
| 497 |
+
background: var(--bg-elev-2);
|
| 498 |
+
overflow: hidden;
|
| 499 |
+
}
|
| 500 |
+
.sub-accordion summary,
|
| 501 |
+
.panel-history details summary,
|
| 502 |
+
.panel-faq details summary {
|
| 503 |
+
padding: 12px 14px;
|
| 504 |
+
cursor: pointer;
|
| 505 |
+
font-family: var(--font-display);
|
| 506 |
+
font-weight: 600;
|
| 507 |
+
font-size: 0.92rem;
|
| 508 |
+
color: var(--text);
|
| 509 |
+
background: var(--bg-elev-3);
|
| 510 |
+
border-bottom: 1px solid transparent;
|
| 511 |
+
list-style: none;
|
| 512 |
+
transition: background 0.15s ease;
|
| 513 |
+
}
|
| 514 |
+
.sub-accordion summary::-webkit-details-marker,
|
| 515 |
+
.panel-history details summary::-webkit-details-marker,
|
| 516 |
+
.panel-faq details summary::-webkit-details-marker { display: none; }
|
| 517 |
+
.sub-accordion summary::before,
|
| 518 |
+
.panel-history details summary::before,
|
| 519 |
+
.panel-faq details summary::before {
|
| 520 |
+
content: "›";
|
| 521 |
+
display: inline-block;
|
| 522 |
+
margin-right: 8px;
|
| 523 |
+
color: var(--accent);
|
| 524 |
+
transform: rotate(0deg);
|
| 525 |
+
transition: transform 0.15s ease;
|
| 526 |
+
}
|
| 527 |
+
.sub-accordion[open] summary::before,
|
| 528 |
+
.panel-history details[open] summary::before,
|
| 529 |
+
.panel-faq details[open] summary::before {
|
| 530 |
+
transform: rotate(90deg);
|
| 531 |
+
}
|
| 532 |
+
.sub-accordion summary:hover,
|
| 533 |
+
.panel-history details summary:hover,
|
| 534 |
+
.panel-faq details summary:hover {
|
| 535 |
+
background: var(--bg-elev-4);
|
| 536 |
+
}
|
| 537 |
+
.details-body {
|
| 538 |
+
padding: 16px;
|
| 539 |
+
}
|
| 540 |
+
.adv-group {
|
| 541 |
+
margin-bottom: 16px;
|
| 542 |
+
}
|
| 543 |
+
.adv-group:last-child { margin-bottom: 0; }
|
| 544 |
+
|
| 545 |
+
/* ---------------------------------------------------------------- *
|
| 546 |
+
* Action buttons *
|
| 547 |
+
* ---------------------------------------------------------------- */
|
| 548 |
+
.actions {
|
| 549 |
+
display: flex;
|
| 550 |
+
gap: 10px;
|
| 551 |
+
margin-top: 24px;
|
| 552 |
+
}
|
| 553 |
+
.btn {
|
| 554 |
+
flex: 1;
|
| 555 |
+
padding: 14px 20px;
|
| 556 |
+
border-radius: var(--radius-sm);
|
| 557 |
+
font-family: var(--font-display);
|
| 558 |
+
font-weight: 700;
|
| 559 |
+
font-size: 0.95rem;
|
| 560 |
+
cursor: pointer;
|
| 561 |
+
border: 1px solid transparent;
|
| 562 |
+
transition: all 0.15s ease;
|
| 563 |
+
display: inline-flex;
|
| 564 |
+
align-items: center;
|
| 565 |
+
justify-content: center;
|
| 566 |
+
gap: 10px;
|
| 567 |
+
}
|
| 568 |
+
.btn-primary {
|
| 569 |
+
background: var(--accent);
|
| 570 |
+
color: #000;
|
| 571 |
+
border-color: var(--accent);
|
| 572 |
+
box-shadow:
|
| 573 |
+
0 6px 18px rgba(198,255,0,0.20),
|
| 574 |
+
0 1px 0 rgba(255,255,255,0.18) inset;
|
| 575 |
+
}
|
| 576 |
+
.btn-primary:hover:not(:disabled) {
|
| 577 |
+
filter: brightness(1.08);
|
| 578 |
+
transform: translateY(-1px);
|
| 579 |
+
box-shadow:
|
| 580 |
+
0 10px 28px rgba(198,255,0,0.35),
|
| 581 |
+
0 1px 0 rgba(255,255,255,0.22) inset;
|
| 582 |
+
}
|
| 583 |
+
.btn-primary:active:not(:disabled) { transform: translateY(0); filter: brightness(0.95); }
|
| 584 |
+
.btn-primary:disabled {
|
| 585 |
+
background: var(--bg-elev-3);
|
| 586 |
+
color: var(--text-mute);
|
| 587 |
+
border-color: var(--border);
|
| 588 |
+
box-shadow: none;
|
| 589 |
+
cursor: not-allowed;
|
| 590 |
+
}
|
| 591 |
+
.btn-secondary {
|
| 592 |
+
flex: 0 0 auto;
|
| 593 |
+
background: var(--bg-elev-2);
|
| 594 |
+
color: var(--text);
|
| 595 |
+
border-color: var(--border);
|
| 596 |
+
padding: 14px 18px;
|
| 597 |
+
}
|
| 598 |
+
.btn-secondary:hover {
|
| 599 |
+
border-color: var(--accent);
|
| 600 |
+
color: var(--accent);
|
| 601 |
+
background: var(--accent-soft);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
/* Spinner */
|
| 605 |
+
.btn-spinner {
|
| 606 |
+
width: 14px;
|
| 607 |
+
height: 14px;
|
| 608 |
+
border: 2px solid rgba(0,0,0,0.25);
|
| 609 |
+
border-top-color: #000;
|
| 610 |
+
border-radius: 50%;
|
| 611 |
+
animation: spin 0.7s linear infinite;
|
| 612 |
+
}
|
| 613 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 614 |
+
|
| 615 |
+
/* ---------------------------------------------------------------- *
|
| 616 |
+
* Metrics *
|
| 617 |
+
* ---------------------------------------------------------------- */
|
| 618 |
+
.metrics {
|
| 619 |
+
display: grid;
|
| 620 |
+
grid-template-columns: repeat(3, 1fr);
|
| 621 |
+
gap: 10px;
|
| 622 |
+
margin-bottom: 16px;
|
| 623 |
+
}
|
| 624 |
+
@media (max-width: 540px) {
|
| 625 |
+
.metrics { grid-template-columns: 1fr; }
|
| 626 |
+
}
|
| 627 |
+
.metric-tile {
|
| 628 |
+
background: linear-gradient(180deg, var(--bg-elev-2), var(--bg-elev-1));
|
| 629 |
+
border: 1px solid var(--border);
|
| 630 |
+
border-left: 3px solid var(--accent);
|
| 631 |
+
border-radius: var(--radius-sm);
|
| 632 |
+
padding: 14px 16px;
|
| 633 |
+
box-shadow: var(--shadow);
|
| 634 |
+
}
|
| 635 |
+
.metric-label {
|
| 636 |
+
font-family: var(--font-mono);
|
| 637 |
+
font-size: 0.62rem;
|
| 638 |
+
letter-spacing: 0.16em;
|
| 639 |
+
text-transform: uppercase;
|
| 640 |
+
color: var(--text-mute);
|
| 641 |
+
font-weight: 500;
|
| 642 |
+
margin-bottom: 4px;
|
| 643 |
+
}
|
| 644 |
+
.metric-value {
|
| 645 |
+
font-family: var(--font-display);
|
| 646 |
+
font-size: 1.5rem;
|
| 647 |
+
font-weight: 700;
|
| 648 |
+
color: var(--text);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
/* ---------------------------------------------------------------- *
|
| 652 |
+
* Status box *
|
| 653 |
+
* ---------------------------------------------------------------- */
|
| 654 |
+
.status {
|
| 655 |
+
padding: 14px 18px;
|
| 656 |
+
background: var(--bg-elev-1);
|
| 657 |
+
border: 1px solid var(--border);
|
| 658 |
+
border-left: 3px solid var(--accent);
|
| 659 |
+
border-radius: var(--radius-sm);
|
| 660 |
+
margin-bottom: 18px;
|
| 661 |
+
font-size: 0.92rem;
|
| 662 |
+
color: var(--text);
|
| 663 |
+
transition: all 0.2s ease;
|
| 664 |
+
}
|
| 665 |
+
.status.ready { border-left-color: var(--text-mute); }
|
| 666 |
+
.status.working { border-left-color: var(--warning); }
|
| 667 |
+
.status.success { border-left-color: var(--success); }
|
| 668 |
+
.status.error { border-left-color: var(--danger); }
|
| 669 |
+
.status code {
|
| 670 |
+
color: var(--accent);
|
| 671 |
+
background: var(--accent-soft);
|
| 672 |
+
padding: 1px 5px;
|
| 673 |
+
border-radius: 3px;
|
| 674 |
+
font-size: 0.88em;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
/* ---------------------------------------------------------------- *
|
| 678 |
+
* Stem cards *
|
| 679 |
+
* ---------------------------------------------------------------- */
|
| 680 |
+
.stems {
|
| 681 |
+
display: flex;
|
| 682 |
+
flex-direction: column;
|
| 683 |
+
gap: 12px;
|
| 684 |
+
margin-bottom: 16px;
|
| 685 |
+
}
|
| 686 |
+
.stem-card {
|
| 687 |
+
background: var(--bg-elev-2);
|
| 688 |
+
border: 1px solid var(--border);
|
| 689 |
+
border-left: 3px solid var(--accent);
|
| 690 |
+
border-radius: var(--radius-sm);
|
| 691 |
+
padding: 14px;
|
| 692 |
+
animation: stem-in 0.3s ease;
|
| 693 |
+
}
|
| 694 |
+
@keyframes stem-in {
|
| 695 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 696 |
+
to { opacity: 1; transform: translateY(0); }
|
| 697 |
+
}
|
| 698 |
+
.stem-header {
|
| 699 |
+
display: flex;
|
| 700 |
+
align-items: center;
|
| 701 |
+
justify-content: space-between;
|
| 702 |
+
margin-bottom: 8px;
|
| 703 |
+
}
|
| 704 |
+
.stem-label {
|
| 705 |
+
font-family: var(--font-display);
|
| 706 |
+
font-weight: 700;
|
| 707 |
+
font-size: 1rem;
|
| 708 |
+
color: var(--text);
|
| 709 |
+
}
|
| 710 |
+
.stem-download {
|
| 711 |
+
font-family: var(--font-mono);
|
| 712 |
+
font-size: 0.74rem;
|
| 713 |
+
color: var(--text-dim);
|
| 714 |
+
background: var(--bg-elev-3);
|
| 715 |
+
padding: 4px 8px;
|
| 716 |
+
border-radius: 4px;
|
| 717 |
+
border: 1px solid var(--border);
|
| 718 |
+
transition: all 0.15s ease;
|
| 719 |
+
}
|
| 720 |
+
.stem-download:hover {
|
| 721 |
+
color: var(--accent);
|
| 722 |
+
border-color: var(--accent);
|
| 723 |
+
}
|
| 724 |
+
.stem-card audio { width: 100%; }
|
| 725 |
+
|
| 726 |
+
/* ---------------------------------------------------------------- *
|
| 727 |
+
* ZIP download card *
|
| 728 |
+
* ---------------------------------------------------------------- */
|
| 729 |
+
.zip-card {
|
| 730 |
+
display: flex;
|
| 731 |
+
align-items: center;
|
| 732 |
+
gap: 14px;
|
| 733 |
+
padding: 16px 18px;
|
| 734 |
+
background: linear-gradient(135deg, var(--accent-soft), transparent 60%);
|
| 735 |
+
border: 1px solid var(--border);
|
| 736 |
+
border-left: 3px solid var(--accent);
|
| 737 |
+
border-radius: var(--radius-sm);
|
| 738 |
+
color: var(--text);
|
| 739 |
+
text-decoration: none;
|
| 740 |
+
transition: all 0.15s ease;
|
| 741 |
+
}
|
| 742 |
+
.zip-card:hover {
|
| 743 |
+
border-color: var(--accent);
|
| 744 |
+
transform: translateY(-1px);
|
| 745 |
+
box-shadow: 0 8px 24px rgba(198,255,0,0.15);
|
| 746 |
+
}
|
| 747 |
+
.zip-card .zip-icon { font-size: 1.6rem; }
|
| 748 |
+
.zip-card .zip-text {
|
| 749 |
+
flex: 1;
|
| 750 |
+
display: flex;
|
| 751 |
+
flex-direction: column;
|
| 752 |
+
}
|
| 753 |
+
.zip-card .zip-text strong {
|
| 754 |
+
font-family: var(--font-display);
|
| 755 |
+
font-weight: 700;
|
| 756 |
+
}
|
| 757 |
+
.zip-card .zip-text span {
|
| 758 |
+
font-size: 0.82rem;
|
| 759 |
+
color: var(--text-dim);
|
| 760 |
+
}
|
| 761 |
+
.zip-card .zip-arrow {
|
| 762 |
+
font-size: 1.4rem;
|
| 763 |
+
color: var(--accent);
|
| 764 |
+
font-weight: 700;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
/* ---------------------------------------------------------------- *
|
| 768 |
+
* Empty state *
|
| 769 |
+
* ---------------------------------------------------------------- */
|
| 770 |
+
.empty-state {
|
| 771 |
+
text-align: center;
|
| 772 |
+
padding: 40px 20px;
|
| 773 |
+
color: var(--text-mute);
|
| 774 |
+
display: flex;
|
| 775 |
+
flex-direction: column;
|
| 776 |
+
align-items: center;
|
| 777 |
+
gap: 12px;
|
| 778 |
+
}
|
| 779 |
+
.empty-state svg { color: var(--text-mute); opacity: 0.6; }
|
| 780 |
+
.empty-state p {
|
| 781 |
+
margin: 0;
|
| 782 |
+
font-family: var(--font-mono);
|
| 783 |
+
font-size: 0.82rem;
|
| 784 |
+
letter-spacing: 0.08em;
|
| 785 |
+
}
|
| 786 |
+
.empty-state.hidden { display: none; }
|
| 787 |
+
|
| 788 |
+
/* ---------------------------------------------------------------- *
|
| 789 |
+
* History table *
|
| 790 |
+
* ---------------------------------------------------------------- */
|
| 791 |
+
.history-table {
|
| 792 |
+
width: 100%;
|
| 793 |
+
border-collapse: collapse;
|
| 794 |
+
font-size: 0.86rem;
|
| 795 |
+
}
|
| 796 |
+
.history-table th {
|
| 797 |
+
text-align: left;
|
| 798 |
+
background: var(--bg-elev-3);
|
| 799 |
+
color: var(--accent);
|
| 800 |
+
font-family: var(--font-mono);
|
| 801 |
+
font-size: 0.68rem;
|
| 802 |
+
letter-spacing: 0.10em;
|
| 803 |
+
text-transform: uppercase;
|
| 804 |
+
font-weight: 600;
|
| 805 |
+
padding: 10px 12px;
|
| 806 |
+
border-bottom: 1px solid var(--border);
|
| 807 |
+
}
|
| 808 |
+
.history-table td {
|
| 809 |
+
padding: 10px 12px;
|
| 810 |
+
border-bottom: 1px solid var(--border-soft);
|
| 811 |
+
color: var(--text-dim);
|
| 812 |
+
}
|
| 813 |
+
.history-table tr:hover td {
|
| 814 |
+
background: var(--accent-soft);
|
| 815 |
+
color: var(--text);
|
| 816 |
+
}
|
| 817 |
+
.history-table .empty-row td {
|
| 818 |
+
text-align: center;
|
| 819 |
+
color: var(--text-mute);
|
| 820 |
+
font-family: var(--font-mono);
|
| 821 |
+
font-size: 0.78rem;
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
/* ---------------------------------------------------------------- *
|
| 825 |
+
* FAQ prose *
|
| 826 |
+
* ---------------------------------------------------------------- */
|
| 827 |
+
.prose h4 {
|
| 828 |
+
font-family: var(--font-display);
|
| 829 |
+
font-weight: 700;
|
| 830 |
+
color: var(--text);
|
| 831 |
+
margin: 18px 0 8px;
|
| 832 |
+
font-size: 1.05rem;
|
| 833 |
+
}
|
| 834 |
+
.prose h4:first-child { margin-top: 0; }
|
| 835 |
+
.prose ul, .prose ol { padding-left: 22px; }
|
| 836 |
+
.prose li { margin: 4px 0; color: var(--text-dim); }
|
| 837 |
+
|
| 838 |
+
/* ---------------------------------------------------------------- *
|
| 839 |
+
* Footer *
|
| 840 |
+
* ---------------------------------------------------------------- */
|
| 841 |
+
.app-footer {
|
| 842 |
+
margin-top: 40px;
|
| 843 |
+
padding: 20px 0 60px;
|
| 844 |
+
border-top: 1px solid var(--border);
|
| 845 |
+
display: flex;
|
| 846 |
+
flex-wrap: wrap;
|
| 847 |
+
gap: 14px;
|
| 848 |
+
align-items: center;
|
| 849 |
+
font-family: var(--font-mono);
|
| 850 |
+
font-size: 0.72rem;
|
| 851 |
+
letter-spacing: 0.08em;
|
| 852 |
+
text-transform: uppercase;
|
| 853 |
+
color: var(--text-mute);
|
| 854 |
+
}
|
| 855 |
+
.app-footer b {
|
| 856 |
+
color: var(--accent);
|
| 857 |
+
font-weight: 700;
|
| 858 |
+
margin-left: 6px;
|
| 859 |
+
}
|
| 860 |
+
.app-footer .dot { opacity: 0.4; }
|
| 861 |
+
|
| 862 |
+
/* ---------------------------------------------------------------- *
|
| 863 |
+
* Reduce motion *
|
| 864 |
+
* ---------------------------------------------------------------- */
|
| 865 |
+
@media (prefers-reduced-motion: reduce) {
|
| 866 |
+
*, *::before, *::after {
|
| 867 |
+
animation-duration: 0.01ms !important;
|
| 868 |
+
animation-iteration-count: 1 !important;
|
| 869 |
+
transition-duration: 0.01ms !important;
|
| 870 |
+
}
|
| 871 |
+
.logo-dot { animation: none; opacity: 1; }
|
| 872 |
+
}
|