Skip to content

Commit e842601

Browse files
committed
Added first pass at remote deployment mechanism for audio-separator on Modal, with consumable API client and docs
1 parent 16cd54c commit e842601

8 files changed

Lines changed: 1940 additions & 810 deletions

File tree

README.md

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ python -m pip install ort-nightly-gpu --index-url=https://aiinfra.pkgs.visualstu
159159
You can use Audio Separator via the command line, for example:
160160

161161
```sh
162-
audio-separator /path/to/your/input/audio.wav --model_filename UVR-MDX-NET-Inst_HQ_3.onnx
162+
audio-separator /path/to/your/input/audio.wav --model_filename model_bs_roformer_ep_317_sdr_12.9755.ckpt
163163
```
164164

165165
This command will download the specified model file, process the `audio.wav` input audio and generate two new files in the current directory, one containing vocals and one containing instrumental.
@@ -342,7 +342,7 @@ from audio_separator.separator import Separator
342342
separator = Separator()
343343
344344
# Load a model
345-
separator.load_model(model_filename='UVR-MDX-NET-Inst_HQ_3.onnx')
345+
separator.load_model(model_filename='model_bs_roformer_ep_317_sdr_12.9755.ckpt')
346346
347347
# Separate multiple audio files without reloading the model
348348
output_files = separator.separate(['audio1.wav', 'audio2.wav', 'audio3.wav'])
@@ -362,7 +362,7 @@ from audio_separator.separator import Separator
362362
separator = Separator()
363363
364364
# Load a model
365-
separator.load_model(model_filename='UVR-MDX-NET-Inst_HQ_3.onnx')
365+
separator.load_model(model_filename='model_bs_roformer_ep_317_sdr_12.9755.ckpt')
366366
367367
# Separate all audio files located in a folder
368368
output_files = separator.separate('path/to/audio_directory')
@@ -439,6 +439,179 @@ You can also rename specific stems:
439439
- **`demucs_params`:** (Optional) Demucs Architecture Specific Attributes & Defaults. `Default: {"segment_size": "Default", "shifts": 2, "overlap": 0.25, "segments_enabled": True}`
440440
- **`mdxc_params`:** (Optional) MDXC Architecture Specific Attributes & Defaults. `Default: {"segment_size": 256, "override_model_segment_size": False, "batch_size": 1, "overlap": 8, "pitch_shift": 0}`
441441
442+
443+
## Remote API Usage 🌐
444+
445+
Audio Separator includes a remote API client that allows you to connect to a deployed Audio Separator API service, enabling you to perform audio separation without running the models locally. The API uses asynchronous processing with job polling for efficient handling of separation tasks.
446+
447+
### Deploying the API Server
448+
449+
To use the remote API functionality, you'll need to deploy the Audio Separator API server. The easiest way is using Modal.com:
450+
451+
1. **Sign up for Modal.com** at [modal.com](https://modal.com)
452+
2. **Install the Modal CLI** and authenticate:
453+
```bash
454+
pip install modal
455+
modal setup
456+
```
457+
3. **Deploy the Audio Separator API**:
458+
```bash
459+
modal deploy audio_separator/remote/deploy_modal.py
460+
```
461+
4. **Get your API URL** from the deployment output. It will look like:
462+
```
463+
https://USERNAME--audio-separator-api.modal.run
464+
```
465+
466+
Set this API URL as an environment variable:
467+
```bash
468+
export AUDIO_SEPARATOR_API_URL="https://USERNAME--audio-separator-api.modal.run"
469+
```
470+
471+
Or pass it directly with the `--api_url` parameter.
472+
473+
### Remote API Client (Python)
474+
475+
You can use the `AudioSeparatorAPIClient` class to interact with a remote Audio Separator API:
476+
477+
```python
478+
import logging
479+
from audio_separator.remote import AudioSeparatorAPIClient
480+
481+
# Set up logging
482+
logger = logging.getLogger(__name__)
483+
484+
# Initialize the API client
485+
api_client = AudioSeparatorAPIClient("https://USERNAME--audio-separator-api.modal.run", logger)
486+
487+
# Simple example: separate audio and get results
488+
result = api_client.separate_audio_and_wait("audio.mp3")
489+
if result["status"] == "completed":
490+
print(f"✅ Separation completed! Downloaded files:")
491+
for file_path in result["downloaded_files"]:
492+
print(f" - {file_path}")
493+
else:
494+
print(f"❌ Separation failed: {result.get('error', 'Unknown error')}")
495+
496+
# Complex example with custom options
497+
result = api_client.separate_audio_and_wait(
498+
"path/to/audio.wav",
499+
model="model_bs_roformer_ep_317_sdr_12.9755.ckpt",
500+
timeout=300, # Wait up to 5 minutes
501+
poll_interval=10, # Check status every 10 seconds
502+
download=True, # Automatically download files
503+
output_dir="./output" # Save files to specific directory
504+
)
505+
506+
# Advanced approach: manual job management (for custom polling logic)
507+
result = api_client.separate_audio("path/to/audio.wav", model="model_bs_roformer_ep_317_sdr_12.9755.ckpt")
508+
task_id = result["task_id"]
509+
print(f"Job submitted! Task ID: {task_id}")
510+
511+
# Custom polling logic
512+
import time
513+
while True:
514+
status = api_client.get_job_status(task_id)
515+
print(f"Job status: {status['status']}")
516+
517+
if status["status"] == "completed":
518+
# Download files manually
519+
for filename in status["files"]:
520+
output_path = api_client.download_file(task_id, filename)
521+
print(f"Downloaded: {output_path}")
522+
break
523+
elif status["status"] == "error":
524+
print(f"Job failed: {status.get('error', 'Unknown error')}")
525+
break
526+
else:
527+
if "progress" in status:
528+
print(f"Progress: {status['progress']}%")
529+
time.sleep(10) # Wait 10 seconds
530+
531+
# List available models
532+
models = api_client.list_models()
533+
print(models["text"])
534+
535+
# Get server version
536+
version = api_client.get_server_version()
537+
print(f"Server version: {version}")
538+
```
539+
540+
### Remote API CLI
541+
542+
Audio Separator also provides a command-line interface for interacting with remote APIs:
543+
544+
#### Commands
545+
546+
**Separate audio files:**
547+
```bash
548+
# Separate audio file (asynchronous processing)
549+
audio-separator-remote separate audio.wav --model model_bs_roformer_ep_317_sdr_12.9755.ckpt
550+
551+
# Multiple files
552+
audio-separator-remote separate audio1.wav audio2.wav audio3.wav
553+
554+
# Use default model (if not specified)
555+
audio-separator-remote separate audio.wav
556+
```
557+
558+
**Check job status:**
559+
```bash
560+
audio-separator-remote status <task_id>
561+
```
562+
563+
**List available models:**
564+
```bash
565+
# Pretty formatted list
566+
audio-separator-remote models
567+
568+
# JSON output
569+
audio-separator-remote models --format json
570+
571+
# Filter by stem type
572+
audio-separator-remote models --filter vocals
573+
```
574+
575+
**Download specific files:**
576+
```bash
577+
audio-separator-remote download <task_id> filename1.wav filename2.wav
578+
```
579+
580+
**Get version information:**
581+
```bash
582+
audio-separator-remote --version
583+
```
584+
585+
#### CLI Options
586+
587+
- `--api_url`: Override the API URL
588+
- `--timeout`: Set timeout for polling (default: 600 seconds)
589+
- `--poll_interval`: Set polling interval (default: 10 seconds)
590+
- `--debug`: Enable debug logging
591+
- `--log_level`: Set log level (info, debug, warning, etc.)
592+
593+
#### Examples
594+
595+
```bash
596+
# Separate with custom settings
597+
audio-separator-remote separate song.mp3 \
598+
--model model_bs_roformer_ep_317_sdr_12.9755.ckpt \
599+
--api_url https://my-api.com \
600+
--timeout 300
601+
602+
# Check status with debug logging
603+
audio-separator-remote status abc123 --debug
604+
605+
# List vocal separation models in JSON format
606+
audio-separator-remote models --filter vocals --format json
607+
```
608+
609+
The remote API client automatically handles:
610+
- File uploading and downloading
611+
- Job polling and status updates
612+
- Error handling and retries
613+
- Progress reporting
614+
442615
## Requirements 📋
443616
444617
Python >= 3.10

audio_separator/remote/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .api_client import AudioSeparatorAPIClient
2+
3+
__all__ = ["AudioSeparatorAPIClient"]
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env python
2+
import os
3+
import logging
4+
from typing import Optional
5+
6+
import requests
7+
8+
9+
class AudioSeparatorAPIClient:
10+
"""Client for interacting with a remotely deployed Audio Separator API."""
11+
12+
def __init__(self, api_url: str, logger: logging.Logger):
13+
self.api_url = api_url
14+
self.logger = logger
15+
self.session = requests.Session()
16+
17+
def separate_audio(self, file_path: str, model: Optional[str] = None) -> dict:
18+
"""Submit audio separation job (asynchronous processing)."""
19+
if not os.path.exists(file_path):
20+
raise FileNotFoundError(f"Audio file not found: {file_path}")
21+
22+
files = {"file": (os.path.basename(file_path), open(file_path, "rb"))}
23+
data = {}
24+
25+
if model:
26+
data["model"] = model
27+
28+
try:
29+
# Increase timeout for large files (5 minutes)
30+
response = self.session.post(f"{self.api_url}/separate", files=files, data=data, timeout=300)
31+
response.raise_for_status()
32+
return response.json()
33+
except requests.RequestException as e:
34+
self.logger.error(f"Separation request failed: {e}")
35+
raise
36+
finally:
37+
files["file"][1].close()
38+
39+
def separate_audio_and_wait(self, file_path: str, model: Optional[str] = None, timeout: int = 600, poll_interval: int = 10, download: bool = True, output_dir: Optional[str] = None) -> dict:
40+
"""
41+
Submit audio separation job and wait for completion (convenience method).
42+
43+
This method handles the full workflow: submit job, poll for completion,
44+
and optionally download the result files.
45+
46+
Args:
47+
file_path: Path to the audio file to separate
48+
model: Model to use for separation (optional)
49+
timeout: Maximum time to wait for completion in seconds (default: 600)
50+
poll_interval: How often to check status in seconds (default: 10)
51+
download: Whether to automatically download result files (default: True)
52+
output_dir: Directory to save downloaded files (default: current directory)
53+
54+
Returns:
55+
dict with keys:
56+
- task_id: The job task ID
57+
- status: "completed" or "error"
58+
- files: List of output filenames
59+
- downloaded_files: List of local file paths (if download=True)
60+
- error: Error message (if status="error")
61+
"""
62+
import time
63+
64+
# Submit the separation job
65+
self.logger.info(f"Submitting separation job for '{file_path}'...")
66+
result = self.separate_audio(file_path, model)
67+
task_id = result["task_id"]
68+
self.logger.info(f"Job submitted! Task ID: {task_id}")
69+
70+
# Poll for completion
71+
self.logger.info("Waiting for separation to complete...")
72+
start_time = time.time()
73+
last_progress = -1
74+
75+
while time.time() - start_time < timeout:
76+
try:
77+
status = self.get_job_status(task_id)
78+
current_status = status.get("status", "unknown")
79+
80+
# Show progress if it changed
81+
if "progress" in status and status["progress"] != last_progress:
82+
self.logger.info(f"Progress: {status['progress']}%")
83+
last_progress = status["progress"]
84+
85+
# Check if completed
86+
if current_status == "completed":
87+
self.logger.info("✅ Separation completed!")
88+
89+
result = {"task_id": task_id, "status": "completed", "files": status.get("files", [])}
90+
91+
# Download files if requested
92+
if download:
93+
downloaded_files = []
94+
self.logger.info(f"📥 Downloading {len(status.get('files', []))} output files...")
95+
96+
for filename in status.get("files", []):
97+
try:
98+
if output_dir:
99+
output_path = f"{output_dir.rstrip('/')}/{filename}"
100+
else:
101+
output_path = filename
102+
103+
downloaded_path = self.download_file(task_id, filename, output_path)
104+
downloaded_files.append(downloaded_path)
105+
self.logger.info(f" ✅ Downloaded: {downloaded_path}")
106+
except Exception as e:
107+
self.logger.error(f" ❌ Failed to download {filename}: {e}")
108+
109+
result["downloaded_files"] = downloaded_files
110+
self.logger.info(f"🎉 Successfully downloaded {len(downloaded_files)} files!")
111+
112+
return result
113+
114+
elif current_status == "error":
115+
error_msg = status.get("error", "Unknown error")
116+
self.logger.error(f"❌ Job failed: {error_msg}")
117+
return {"task_id": task_id, "status": "error", "error": error_msg, "files": []}
118+
119+
# Wait before next poll
120+
time.sleep(poll_interval)
121+
122+
except Exception as e:
123+
self.logger.warning(f"Error polling status: {e}")
124+
time.sleep(poll_interval)
125+
126+
# Timeout reached
127+
self.logger.error(f"❌ Job polling timed out after {timeout} seconds")
128+
return {"task_id": task_id, "status": "timeout", "error": f"Job polling timed out after {timeout} seconds", "files": []}
129+
130+
def get_job_status(self, task_id: str) -> dict:
131+
"""Get job status."""
132+
try:
133+
response = self.session.get(f"{self.api_url}/status/{task_id}", timeout=10)
134+
response.raise_for_status()
135+
return response.json()
136+
except requests.RequestException as e:
137+
self.logger.error(f"Status request failed: {e}")
138+
raise
139+
140+
def download_file(self, task_id: str, filename: str, output_path: Optional[str] = None) -> str:
141+
"""Download a file from a completed job."""
142+
if output_path is None:
143+
output_path = filename
144+
145+
try:
146+
response = self.session.get(f"{self.api_url}/download/{task_id}/{filename}", timeout=60)
147+
response.raise_for_status()
148+
149+
with open(output_path, "wb") as f:
150+
f.write(response.content)
151+
152+
return output_path
153+
except requests.RequestException as e:
154+
self.logger.error(f"Download failed: {e}")
155+
raise
156+
157+
def list_models(self, format_type: str = "pretty", filter_by: Optional[str] = None) -> dict:
158+
"""List available models."""
159+
try:
160+
if format_type == "json":
161+
response = self.session.get(f"{self.api_url}/models-json", timeout=10)
162+
else:
163+
url = f"{self.api_url}/models"
164+
if filter_by:
165+
url += f"?filter_sort_by={filter_by}"
166+
response = self.session.get(url, timeout=10)
167+
168+
response.raise_for_status()
169+
170+
if format_type == "json":
171+
return response.json()
172+
else:
173+
return {"text": response.text}
174+
except requests.RequestException as e:
175+
self.logger.error(f"Models request failed: {e}")
176+
raise
177+
178+
def get_server_version(self) -> str:
179+
"""Get the server version."""
180+
try:
181+
response = self.session.get(f"{self.api_url}/health", timeout=10)
182+
response.raise_for_status()
183+
health_data = response.json()
184+
return health_data.get("version", "unknown")
185+
except requests.RequestException as e:
186+
self.logger.error(f"Health check request failed: {e}")
187+
raise

0 commit comments

Comments
 (0)