voxsplit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxsplit-0.1.0/LICENSE +21 -0
- voxsplit-0.1.0/PKG-INFO +162 -0
- voxsplit-0.1.0/README.md +107 -0
- voxsplit-0.1.0/backend/__init__.py +0 -0
- voxsplit-0.1.0/backend/engine.py +119 -0
- voxsplit-0.1.0/backend/frontend/app.js +183 -0
- voxsplit-0.1.0/backend/frontend/index.html +45 -0
- voxsplit-0.1.0/backend/frontend/style.css +175 -0
- voxsplit-0.1.0/backend/jobs.py +97 -0
- voxsplit-0.1.0/backend/main.py +121 -0
- voxsplit-0.1.0/pyproject.toml +73 -0
- voxsplit-0.1.0/setup.cfg +4 -0
- voxsplit-0.1.0/tests/test_jobs.py +77 -0
- voxsplit-0.1.0/voxsplit.egg-info/PKG-INFO +162 -0
- voxsplit-0.1.0/voxsplit.egg-info/SOURCES.txt +17 -0
- voxsplit-0.1.0/voxsplit.egg-info/dependency_links.txt +1 -0
- voxsplit-0.1.0/voxsplit.egg-info/entry_points.txt +2 -0
- voxsplit-0.1.0/voxsplit.egg-info/requires.txt +10 -0
- voxsplit-0.1.0/voxsplit.egg-info/top_level.txt +1 -0
voxsplit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Borck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
voxsplit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voxsplit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first AI stem separation with a modern web UI
|
|
5
|
+
Author: Michael Borck
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Michael Borck
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://voxsplit.borck.dev
|
|
29
|
+
Project-URL: Repository, https://github.com/michael-borck/voxsplit
|
|
30
|
+
Project-URL: Issues, https://github.com/michael-borck/voxsplit/issues
|
|
31
|
+
Keywords: audio,stems,source-separation,vocal-remover,karaoke,demucs,roformer,music
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Environment :: Web Environment
|
|
34
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
License-File: LICENSE
|
|
45
|
+
Requires-Dist: fastapi>=0.115
|
|
46
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
47
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
48
|
+
Requires-Dist: audio-separator[cpu]>=0.30
|
|
49
|
+
Requires-Dist: audioread>=3.0
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
52
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
53
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# VoxSplit
|
|
57
|
+
|
|
58
|
+
**[voxsplit.borck.dev](https://voxsplit.borck.dev)** — hear real separation output, then self-host.
|
|
59
|
+
|
|
60
|
+
Local-first AI stem separation with a modern web UI. Drop in a song, get back
|
|
61
|
+
vocals, instrumental, or full 4-stem splits — everything runs on your own
|
|
62
|
+
machine, no accounts, no uploads to the cloud.
|
|
63
|
+
|
|
64
|
+
VoxSplit is a greenfield web app built on proven open-source separation
|
|
65
|
+
engines. It is inspired by [Ultimate Vocal Remover GUI](https://github.com/Anjok07/ultimatevocalremovergui)
|
|
66
|
+
and stands on the shoulders of that project and its model authors.
|
|
67
|
+
|
|
68
|
+
## Status
|
|
69
|
+
|
|
70
|
+
Working proof of concept: upload → separate with per-job progress → play/download
|
|
71
|
+
stems. Models auto-use the best available accelerator: CUDA on Linux/NVIDIA,
|
|
72
|
+
MPS on Apple Silicon (torch-based models), CPU everywhere.
|
|
73
|
+
|
|
74
|
+
## Models
|
|
75
|
+
|
|
76
|
+
Weights are fetched on demand (first use of each model) into `data/models/`
|
|
77
|
+
from the community model repos maintained by the UVR and python-audio-separator
|
|
78
|
+
projects. No manual downloads needed.
|
|
79
|
+
|
|
80
|
+
| Model | Family | Stems | Notes |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| Mel-Band RoFormer (aufr33/viperx) | RoFormer | 2 | SOTA karaoke quality |
|
|
83
|
+
| Mel-Band RoFormer (becruily) | RoFormer | 2 | SOTA karaoke alt |
|
|
84
|
+
| Mel-Band RoFormer (KimberleyJSN) | RoFormer | 2 | SOTA vocals |
|
|
85
|
+
| UVR-MDX-NET-Inst HQ 3 | MDX-Net | 2 | Fast, high quality |
|
|
86
|
+
| Kim Vocal 2 | MDX-Net | 2 | Vocals, male-leaning |
|
|
87
|
+
| UVR MDXNET Kara 2 | MDX-Net | 2 | Karaoke-optimized |
|
|
88
|
+
| htdemucs_4s | Demucs | 4 | Vocals/drums/bass/other |
|
|
89
|
+
|
|
90
|
+
Accelerator notes: RoFormer and Demucs run on CUDA/MPS automatically. MDX-Net
|
|
91
|
+
runs via onnxruntime — CPU on macOS, CUDA on Linux with `audio-separator[gpu]`.
|
|
92
|
+
|
|
93
|
+
## Quickstart
|
|
94
|
+
|
|
95
|
+
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/) (or plain `pip` —
|
|
96
|
+
see note below).
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone https://github.com/michael-borck/voxsplit
|
|
100
|
+
cd voxsplit
|
|
101
|
+
uv venv
|
|
102
|
+
source .venv/bin/activate
|
|
103
|
+
uv pip install -e .
|
|
104
|
+
voxsplit
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Without uv: `python3 -m venv .venv && source .venv/bin/activate && pip install -e .`
|
|
108
|
+
|
|
109
|
+
Open http://127.0.0.1:8399, drop a `.wav`/`.mp3`/`.flac`, pick a model, hit
|
|
110
|
+
**Split**. The first run downloads the selected model (~60 MB) to `data/models/`.
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
browser (vanilla JS) ──► FastAPI (backend/main.py)
|
|
116
|
+
├── jobs.py thread-safe job store, 1-worker queue
|
|
117
|
+
├── engine.py python-audio-separator wrapper (lazy import)
|
|
118
|
+
└── data/ uploads, outputs, model weights
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- **REST API**: `POST /api/jobs` (upload + model), `GET /api/jobs/{id}` (status),
|
|
122
|
+
`GET /api/jobs/{id}/files/{name}` (stream stem), `DELETE /api/jobs/{id}`.
|
|
123
|
+
- **Engines**: models run via `python-audio-separator`, which supports UVR's
|
|
124
|
+
MDX-Net/VR models, Demucs, and Mel-Band RoFormers.
|
|
125
|
+
|
|
126
|
+
## Credits
|
|
127
|
+
|
|
128
|
+
This project exists because of the people who built the models and the
|
|
129
|
+
original app:
|
|
130
|
+
|
|
131
|
+
- **Anjok07 & aufr33** — [Ultimate Vocal Remover GUI](https://github.com/Anjok07/ultimatevocalremovergui),
|
|
132
|
+
the project that made high-quality local stem separation accessible.
|
|
133
|
+
- **nomadkaraoke** — [python-audio-separator](https://github.com/nomadkaraoke/python-audio-separator),
|
|
134
|
+
the maintained headless engine VoxSplit wraps.
|
|
135
|
+
- **ZFTurbo** — MDX23C weights and [Music-Source-Separation-Training](https://github.com/ZFTurbo/Music-Source-Separation-Training).
|
|
136
|
+
- **Kuielab & Woosung Choi** — original MDX-Net AI code.
|
|
137
|
+
- **tsurumeso** — original VR architecture code.
|
|
138
|
+
- **Alexandre Défossez & Meta (Demucs)** — Demucs AI code.
|
|
139
|
+
- **Bas Curtiz** — original UVR logo/icon design.
|
|
140
|
+
|
|
141
|
+
## Deployment (NVIDIA)
|
|
142
|
+
|
|
143
|
+
On a Linux box with NVIDIA GPUs and Docker + nvidia-container-toolkit:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
cd deploy
|
|
147
|
+
docker compose up -d --build
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Then browse to `http://<box-ip>:8399`. Model weights persist in `deploy/data/`.
|
|
151
|
+
|
|
152
|
+
## Roadmap
|
|
153
|
+
|
|
154
|
+
- [x] Per-job progress reporting (tqdm hooks on torch models)
|
|
155
|
+
- [x] Mel-Band RoFormer models (current SOTA)
|
|
156
|
+
- [x] Apple Silicon MPS acceleration (automatic for torch models)
|
|
157
|
+
- [ ] Ensemble mode + post-processing (from UVR's playbook)
|
|
158
|
+
- [ ] Optional Tauri desktop wrapper
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT — see [LICENSE](LICENSE).
|
voxsplit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# VoxSplit
|
|
2
|
+
|
|
3
|
+
**[voxsplit.borck.dev](https://voxsplit.borck.dev)** — hear real separation output, then self-host.
|
|
4
|
+
|
|
5
|
+
Local-first AI stem separation with a modern web UI. Drop in a song, get back
|
|
6
|
+
vocals, instrumental, or full 4-stem splits — everything runs on your own
|
|
7
|
+
machine, no accounts, no uploads to the cloud.
|
|
8
|
+
|
|
9
|
+
VoxSplit is a greenfield web app built on proven open-source separation
|
|
10
|
+
engines. It is inspired by [Ultimate Vocal Remover GUI](https://github.com/Anjok07/ultimatevocalremovergui)
|
|
11
|
+
and stands on the shoulders of that project and its model authors.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Working proof of concept: upload → separate with per-job progress → play/download
|
|
16
|
+
stems. Models auto-use the best available accelerator: CUDA on Linux/NVIDIA,
|
|
17
|
+
MPS on Apple Silicon (torch-based models), CPU everywhere.
|
|
18
|
+
|
|
19
|
+
## Models
|
|
20
|
+
|
|
21
|
+
Weights are fetched on demand (first use of each model) into `data/models/`
|
|
22
|
+
from the community model repos maintained by the UVR and python-audio-separator
|
|
23
|
+
projects. No manual downloads needed.
|
|
24
|
+
|
|
25
|
+
| Model | Family | Stems | Notes |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| Mel-Band RoFormer (aufr33/viperx) | RoFormer | 2 | SOTA karaoke quality |
|
|
28
|
+
| Mel-Band RoFormer (becruily) | RoFormer | 2 | SOTA karaoke alt |
|
|
29
|
+
| Mel-Band RoFormer (KimberleyJSN) | RoFormer | 2 | SOTA vocals |
|
|
30
|
+
| UVR-MDX-NET-Inst HQ 3 | MDX-Net | 2 | Fast, high quality |
|
|
31
|
+
| Kim Vocal 2 | MDX-Net | 2 | Vocals, male-leaning |
|
|
32
|
+
| UVR MDXNET Kara 2 | MDX-Net | 2 | Karaoke-optimized |
|
|
33
|
+
| htdemucs_4s | Demucs | 4 | Vocals/drums/bass/other |
|
|
34
|
+
|
|
35
|
+
Accelerator notes: RoFormer and Demucs run on CUDA/MPS automatically. MDX-Net
|
|
36
|
+
runs via onnxruntime — CPU on macOS, CUDA on Linux with `audio-separator[gpu]`.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/) (or plain `pip` —
|
|
41
|
+
see note below).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/michael-borck/voxsplit
|
|
45
|
+
cd voxsplit
|
|
46
|
+
uv venv
|
|
47
|
+
source .venv/bin/activate
|
|
48
|
+
uv pip install -e .
|
|
49
|
+
voxsplit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Without uv: `python3 -m venv .venv && source .venv/bin/activate && pip install -e .`
|
|
53
|
+
|
|
54
|
+
Open http://127.0.0.1:8399, drop a `.wav`/`.mp3`/`.flac`, pick a model, hit
|
|
55
|
+
**Split**. The first run downloads the selected model (~60 MB) to `data/models/`.
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
browser (vanilla JS) ──► FastAPI (backend/main.py)
|
|
61
|
+
├── jobs.py thread-safe job store, 1-worker queue
|
|
62
|
+
├── engine.py python-audio-separator wrapper (lazy import)
|
|
63
|
+
└── data/ uploads, outputs, model weights
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **REST API**: `POST /api/jobs` (upload + model), `GET /api/jobs/{id}` (status),
|
|
67
|
+
`GET /api/jobs/{id}/files/{name}` (stream stem), `DELETE /api/jobs/{id}`.
|
|
68
|
+
- **Engines**: models run via `python-audio-separator`, which supports UVR's
|
|
69
|
+
MDX-Net/VR models, Demucs, and Mel-Band RoFormers.
|
|
70
|
+
|
|
71
|
+
## Credits
|
|
72
|
+
|
|
73
|
+
This project exists because of the people who built the models and the
|
|
74
|
+
original app:
|
|
75
|
+
|
|
76
|
+
- **Anjok07 & aufr33** — [Ultimate Vocal Remover GUI](https://github.com/Anjok07/ultimatevocalremovergui),
|
|
77
|
+
the project that made high-quality local stem separation accessible.
|
|
78
|
+
- **nomadkaraoke** — [python-audio-separator](https://github.com/nomadkaraoke/python-audio-separator),
|
|
79
|
+
the maintained headless engine VoxSplit wraps.
|
|
80
|
+
- **ZFTurbo** — MDX23C weights and [Music-Source-Separation-Training](https://github.com/ZFTurbo/Music-Source-Separation-Training).
|
|
81
|
+
- **Kuielab & Woosung Choi** — original MDX-Net AI code.
|
|
82
|
+
- **tsurumeso** — original VR architecture code.
|
|
83
|
+
- **Alexandre Défossez & Meta (Demucs)** — Demucs AI code.
|
|
84
|
+
- **Bas Curtiz** — original UVR logo/icon design.
|
|
85
|
+
|
|
86
|
+
## Deployment (NVIDIA)
|
|
87
|
+
|
|
88
|
+
On a Linux box with NVIDIA GPUs and Docker + nvidia-container-toolkit:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd deploy
|
|
92
|
+
docker compose up -d --build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then browse to `http://<box-ip>:8399`. Model weights persist in `deploy/data/`.
|
|
96
|
+
|
|
97
|
+
## Roadmap
|
|
98
|
+
|
|
99
|
+
- [x] Per-job progress reporting (tqdm hooks on torch models)
|
|
100
|
+
- [x] Mel-Band RoFormer models (current SOTA)
|
|
101
|
+
- [x] Apple Silicon MPS acceleration (automatic for torch models)
|
|
102
|
+
- [ ] Ensemble mode + post-processing (from UVR's playbook)
|
|
103
|
+
- [ ] Optional Tauri desktop wrapper
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT — see [LICENSE](LICENSE).
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Engine wrapper around python-audio-separator.
|
|
2
|
+
|
|
3
|
+
The heavy audio_separator import happens lazily so the FastAPI app,
|
|
4
|
+
tests, and CI can run without torch installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
MODELS = [
|
|
13
|
+
{"id": "UVR-MDX-NET-Inst_HQ_3.onnx", "family": "MDX-Net", "stems": 2, "note": "HQ instrumental/vocals"},
|
|
14
|
+
{"id": "Kim_Vocal_2.onnx", "family": "MDX-Net", "stems": 2, "note": "Vocals, stronger on male vocals"},
|
|
15
|
+
{"id": "UVR_MDXNET_KARA_2.onnx", "family": "MDX-Net", "stems": 2, "note": "Karaoke-optimized"},
|
|
16
|
+
{
|
|
17
|
+
"id": "mel_band_roformer_karaoke_aufr33_viperx_sdr_10.1956.ckpt",
|
|
18
|
+
"family": "Mel-Band RoFormer",
|
|
19
|
+
"stems": 2,
|
|
20
|
+
"note": "SOTA karaoke (~1GB download)",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "mel_band_roformer_karaoke_becruily.ckpt",
|
|
24
|
+
"family": "Mel-Band RoFormer",
|
|
25
|
+
"stems": 2,
|
|
26
|
+
"note": "SOTA karaoke alt (~1GB download)",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "vocals_mel_band_roformer.ckpt",
|
|
30
|
+
"family": "Mel-Band RoFormer",
|
|
31
|
+
"stems": 2,
|
|
32
|
+
"note": "SOTA vocals (~1GB download)",
|
|
33
|
+
},
|
|
34
|
+
{"id": "htdemucs_4s.yaml", "family": "Demucs", "stems": 4, "note": "Vocals/drums/bass/other"},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
DEFAULT_MODEL = "UVR-MDX-NET-Inst_HQ_3.onnx"
|
|
38
|
+
|
|
39
|
+
PHASES = ("loading_model", "separating", "writing")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def list_models() -> list[dict]:
|
|
43
|
+
return MODELS
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_known_model(model: str) -> bool:
|
|
47
|
+
return model in {m["id"] for m in MODELS}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def detect_accelerator() -> str:
|
|
51
|
+
try:
|
|
52
|
+
import torch
|
|
53
|
+
|
|
54
|
+
if torch.cuda.is_available():
|
|
55
|
+
return "cuda"
|
|
56
|
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
|
57
|
+
return "mps"
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return "cpu"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _install_progress_hook(progress_cb):
|
|
64
|
+
"""Patch tqdm inside torch-based architectures so chunk loops report progress.
|
|
65
|
+
|
|
66
|
+
Returns a list of (module, attr, original) tuples to restore in finally.
|
|
67
|
+
MDX-Net models run through onnxruntime without tqdm loops, so callers get
|
|
68
|
+
phase-level progress only for that family.
|
|
69
|
+
"""
|
|
70
|
+
from audio_separator.separator.architectures import demucs_separator, mdxc_separator
|
|
71
|
+
from tqdm import tqdm
|
|
72
|
+
|
|
73
|
+
class HookedTqdm(tqdm):
|
|
74
|
+
def update(self, n=1):
|
|
75
|
+
super().update(n)
|
|
76
|
+
if self.total:
|
|
77
|
+
progress_cb("separating", min(100.0, 100.0 * self.n / self.total))
|
|
78
|
+
|
|
79
|
+
saved = []
|
|
80
|
+
for module in (mdxc_separator, demucs_separator):
|
|
81
|
+
if hasattr(module, "tqdm"):
|
|
82
|
+
saved.append((module, "tqdm", module.tqdm))
|
|
83
|
+
module.tqdm = HookedTqdm
|
|
84
|
+
return saved
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_separation(
|
|
88
|
+
input_path: Path,
|
|
89
|
+
output_dir: Path,
|
|
90
|
+
model: str,
|
|
91
|
+
model_dir: Path,
|
|
92
|
+
progress_cb=None,
|
|
93
|
+
) -> list[Path]:
|
|
94
|
+
from audio_separator.separator import Separator
|
|
95
|
+
|
|
96
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
model_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
def report(phase: str, percent: float | None = None) -> None:
|
|
100
|
+
if progress_cb:
|
|
101
|
+
progress_cb(phase, percent)
|
|
102
|
+
|
|
103
|
+
report("loading_model", 0.0)
|
|
104
|
+
logger.info("Loading model %s", model)
|
|
105
|
+
saved = _install_progress_hook(report)
|
|
106
|
+
try:
|
|
107
|
+
separator = Separator(
|
|
108
|
+
model_file_dir=str(model_dir),
|
|
109
|
+
output_dir=str(output_dir),
|
|
110
|
+
output_format="WAV",
|
|
111
|
+
)
|
|
112
|
+
separator.load_model(model_filename=model)
|
|
113
|
+
output_files = separator.separate(str(input_path))
|
|
114
|
+
finally:
|
|
115
|
+
for module, attr, original in saved:
|
|
116
|
+
setattr(module, attr, original)
|
|
117
|
+
report("writing", 99.0)
|
|
118
|
+
logger.info("Separation produced %d files", len(output_files))
|
|
119
|
+
return [output_dir / Path(f).name for f in output_files]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const fileInput = document.getElementById("file-input");
|
|
2
|
+
const browseBtn = document.getElementById("browse");
|
|
3
|
+
const dropzone = document.getElementById("dropzone");
|
|
4
|
+
const fileName = document.getElementById("file-name");
|
|
5
|
+
const modelSelect = document.getElementById("model-select");
|
|
6
|
+
const splitBtn = document.getElementById("split-btn");
|
|
7
|
+
const jobList = document.getElementById("job-list");
|
|
8
|
+
|
|
9
|
+
let selectedFile = null;
|
|
10
|
+
const pollers = new Map();
|
|
11
|
+
|
|
12
|
+
async function loadModels() {
|
|
13
|
+
const res = await fetch("/api/models");
|
|
14
|
+
const models = await res.json();
|
|
15
|
+
for (const m of models) {
|
|
16
|
+
const opt = document.createElement("option");
|
|
17
|
+
opt.value = m.id;
|
|
18
|
+
opt.textContent = `${m.id} — ${m.note} (${m.stems} stems)`;
|
|
19
|
+
modelSelect.appendChild(opt);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
browseBtn.addEventListener("click", () => fileInput.click());
|
|
24
|
+
|
|
25
|
+
fileInput.addEventListener("change", () => {
|
|
26
|
+
selectedFile = fileInput.files[0] || null;
|
|
27
|
+
fileName.textContent = selectedFile ? selectedFile.name : "";
|
|
28
|
+
splitBtn.disabled = !selectedFile;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
["dragenter", "dragover"].forEach((evt) =>
|
|
32
|
+
dropzone.addEventListener(evt, (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
dropzone.classList.add("dragover");
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
["dragleave", "drop"].forEach((evt) =>
|
|
39
|
+
dropzone.addEventListener(evt, (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
dropzone.classList.remove("dragover");
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
dropzone.addEventListener("drop", (e) => {
|
|
46
|
+
const file = e.dataTransfer.files[0];
|
|
47
|
+
if (file) {
|
|
48
|
+
selectedFile = file;
|
|
49
|
+
fileName.textContent = file.name;
|
|
50
|
+
splitBtn.disabled = false;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
splitBtn.addEventListener("click", async () => {
|
|
55
|
+
if (!selectedFile) return;
|
|
56
|
+
splitBtn.disabled = true;
|
|
57
|
+
splitBtn.textContent = "Uploading…";
|
|
58
|
+
try {
|
|
59
|
+
const form = new FormData();
|
|
60
|
+
form.append("file", selectedFile);
|
|
61
|
+
form.append("model", modelSelect.value);
|
|
62
|
+
const res = await fetch("/api/jobs", { method: "POST", body: form });
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
65
|
+
throw new Error(err.detail || "Upload failed");
|
|
66
|
+
}
|
|
67
|
+
const job = await res.json();
|
|
68
|
+
addJobCard(job);
|
|
69
|
+
pollJob(job.id);
|
|
70
|
+
selectedFile = null;
|
|
71
|
+
fileInput.value = "";
|
|
72
|
+
fileName.textContent = "";
|
|
73
|
+
} catch (err) {
|
|
74
|
+
alert(err.message);
|
|
75
|
+
} finally {
|
|
76
|
+
splitBtn.disabled = !selectedFile;
|
|
77
|
+
splitBtn.textContent = "Split";
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function stemLabel(path) {
|
|
82
|
+
const base = path.split("/").pop();
|
|
83
|
+
const m = base.match(/_(vocals|instrumental|drums|bass|other|no_vocals)\./i);
|
|
84
|
+
return m ? m[1].replace("_", " ") : base;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addJobCard(job) {
|
|
88
|
+
const empty = jobList.querySelector(".empty");
|
|
89
|
+
if (empty) empty.remove();
|
|
90
|
+
|
|
91
|
+
const div = document.createElement("div");
|
|
92
|
+
div.className = "job";
|
|
93
|
+
div.id = `job-${job.id}`;
|
|
94
|
+
div.innerHTML = `
|
|
95
|
+
<div class="job-head">
|
|
96
|
+
<span class="job-title">${escapeHtml(job.filename)}</span>
|
|
97
|
+
<span class="job-model">${escapeHtml(job.model)}</span>
|
|
98
|
+
<span class="status ${job.status}">${job.status}</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="job-body"></div>
|
|
101
|
+
`;
|
|
102
|
+
jobList.prepend(div);
|
|
103
|
+
renderJobBody(div, job);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const PHASE_LABELS = {
|
|
107
|
+
loading_model: "Loading model…",
|
|
108
|
+
separating: "Separating…",
|
|
109
|
+
writing: "Writing stems…",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function renderJobBody(card, job) {
|
|
113
|
+
const body = card.querySelector(".job-body");
|
|
114
|
+
const status = card.querySelector(".status");
|
|
115
|
+
status.textContent = job.status;
|
|
116
|
+
status.className = `status ${job.status}`;
|
|
117
|
+
|
|
118
|
+
if (job.status === "failed") {
|
|
119
|
+
body.innerHTML = `<p class="job-error">${escapeHtml(job.error || "Unknown error")}</p>`;
|
|
120
|
+
} else if (job.status === "completed") {
|
|
121
|
+
const ul = document.createElement("ul");
|
|
122
|
+
ul.className = "stems";
|
|
123
|
+
for (const f of job.files) {
|
|
124
|
+
const name = f.split("/").pop();
|
|
125
|
+
const li = document.createElement("li");
|
|
126
|
+
li.innerHTML = `
|
|
127
|
+
<span class="stem-name">${escapeHtml(stemLabel(name))}</span>
|
|
128
|
+
<audio controls preload="none" src="/api/jobs/${job.id}/files/${encodeURIComponent(name)}"></audio>
|
|
129
|
+
<a class="download" href="/api/jobs/${job.id}/files/${encodeURIComponent(name)}" download>download</a>
|
|
130
|
+
`;
|
|
131
|
+
ul.appendChild(li);
|
|
132
|
+
}
|
|
133
|
+
body.innerHTML = "";
|
|
134
|
+
body.appendChild(ul);
|
|
135
|
+
} else {
|
|
136
|
+
const phase = PHASE_LABELS[job.phase] || (job.status === "queued" ? "Waiting…" : "Processing…");
|
|
137
|
+
let bar;
|
|
138
|
+
if (job.progress != null) {
|
|
139
|
+
bar = `<div class="bar"><div class="bar-fill" style="width:${job.progress}%"></div></div>`;
|
|
140
|
+
} else {
|
|
141
|
+
bar = `<div class="bar"><div class="bar-fill indeterminate"></div></div>`;
|
|
142
|
+
}
|
|
143
|
+
body.innerHTML = `<p class="phase">${escapeHtml(phase)}</p>${bar}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pollJob(jobId) {
|
|
148
|
+
if (pollers.has(jobId)) return;
|
|
149
|
+
const timer = setInterval(async () => {
|
|
150
|
+
const res = await fetch(`/api/jobs/${jobId}`);
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
clearInterval(timer);
|
|
153
|
+
pollers.delete(jobId);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const job = await res.json();
|
|
157
|
+
const card = document.getElementById(`job-${jobId}`);
|
|
158
|
+
if (card) renderJobBody(card, job);
|
|
159
|
+
if (job.status === "completed" || job.status === "failed") {
|
|
160
|
+
clearInterval(timer);
|
|
161
|
+
pollers.delete(jobId);
|
|
162
|
+
}
|
|
163
|
+
}, 1500);
|
|
164
|
+
pollers.set(jobId, timer);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function loadExistingJobs() {
|
|
168
|
+
const res = await fetch("/api/jobs");
|
|
169
|
+
const jobs = await res.json();
|
|
170
|
+
for (const job of jobs.slice(0, 20)) {
|
|
171
|
+
addJobCard(job);
|
|
172
|
+
if (job.status === "queued" || job.status === "processing") pollJob(job.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function escapeHtml(s) {
|
|
177
|
+
return s.replace(/[&<>"']/g, (c) => ({
|
|
178
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
179
|
+
}[c]));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
loadModels();
|
|
183
|
+
loadExistingJobs();
|
|
@@ -0,0 +1,45 @@
|
|
|
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>VoxSplit — AI Stem Separation</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<h1><span class="accent">Vox</span>Split</h1>
|
|
12
|
+
<p class="tagline">Local-first AI stem separation</p>
|
|
13
|
+
</header>
|
|
14
|
+
|
|
15
|
+
<main>
|
|
16
|
+
<section class="card" id="dropzone" aria-label="Upload a song">
|
|
17
|
+
<input type="file" id="file-input" accept=".wav,.mp3,.flac,.ogg,.m4a,.aac,.wma" hidden />
|
|
18
|
+
<p class="drop-msg">Drop an audio file here, or <button type="button" class="link" id="browse">browse</button></p>
|
|
19
|
+
<p class="file-name" id="file-name"></p>
|
|
20
|
+
<div class="controls">
|
|
21
|
+
<label for="model-select">Model</label>
|
|
22
|
+
<select id="model-select"></select>
|
|
23
|
+
<button type="button" id="split-btn" class="primary" disabled>Split</button>
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
<section id="jobs">
|
|
28
|
+
<h2>Jobs</h2>
|
|
29
|
+
<div id="job-list" class="job-list">
|
|
30
|
+
<p class="empty">No jobs yet — split your first song above.</p>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
</main>
|
|
34
|
+
|
|
35
|
+
<footer>
|
|
36
|
+
<p>
|
|
37
|
+
VoxSplit is free software (MIT). Powered by
|
|
38
|
+
<a href="https://github.com/nomadkaraoke/python-audio-separator" target="_blank" rel="noopener">python-audio-separator</a>.
|
|
39
|
+
Inspired by <a href="https://github.com/Anjok07/ultimatevocalremovergui" target="_blank" rel="noopener">Ultimate Vocal Remover</a>.
|
|
40
|
+
</p>
|
|
41
|
+
</footer>
|
|
42
|
+
|
|
43
|
+
<script src="/app.js"></script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|