Use ZeroGPU for stem separation in song package
Browse files- Update separate_stems to support two_stem parameter for 2-pass separation
- Song package now uses GPU-accelerated Demucs for both passes
- Pass 1: vocals/instrumental (2-stem mode)
- Pass 2: drums, bass, guitar, keys, other, vocals (6-stem mode)
- Proper quota error handling for both passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -400,18 +400,26 @@ def download_youtube(url: str) -> tuple:
|
|
| 400 |
return None, str(e)
|
| 401 |
|
| 402 |
@spaces.GPU(duration=120)
|
| 403 |
-
def _separate_stems_gpu(audio_path: str, progress=None) -> dict:
|
| 404 |
"""Separate audio into stems using Demucs (GPU accelerated) - internal"""
|
| 405 |
try:
|
| 406 |
import subprocess
|
| 407 |
-
|
|
|
|
| 408 |
stems_dir.mkdir(exist_ok=True)
|
| 409 |
|
| 410 |
if progress:
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
-
# Use GPU if available
|
| 414 |
-
cmd = ['python', '-m', 'demucs', '-o', str(stems_dir), audio_path]
|
| 415 |
subprocess.run(cmd, capture_output=True, check=True)
|
| 416 |
|
| 417 |
# Find output stems
|
|
@@ -419,18 +427,24 @@ def _separate_stems_gpu(audio_path: str, progress=None) -> dict:
|
|
| 419 |
song_dir = list(model_dir.glob('*'))[0]
|
| 420 |
|
| 421 |
stems = {}
|
| 422 |
-
for stem_file in song_dir.glob('*.
|
| 423 |
stem_name = stem_file.stem
|
| 424 |
stems[stem_name] = str(stem_file)
|
| 425 |
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
except Exception as e:
|
| 428 |
return {'error': str(e)}
|
| 429 |
|
| 430 |
-
def separate_stems(audio_path: str, progress=None) -> dict:
|
| 431 |
"""Separate stems - wrapper with quota error handling"""
|
| 432 |
try:
|
| 433 |
-
return _separate_stems_gpu(audio_path, progress)
|
| 434 |
except Exception as e:
|
| 435 |
return {'error': handle_gpu_error(e)}
|
| 436 |
|
|
@@ -619,39 +633,45 @@ def create_song_package(audio, yt_url, name, lyrics, user_email, progress=gr.Pro
|
|
| 619 |
key = analysis.get('key', 'C')
|
| 620 |
log.append(f"Key: {key} | BPM: {tempo}")
|
| 621 |
|
| 622 |
-
# === PASS 1: 2-STEM SEPARATION (vocals/instrumental) ===
|
| 623 |
-
stems_pass1_dir = package_dir / "stems_pass1"
|
| 624 |
stems_pass1 = {}
|
|
|
|
| 625 |
|
| 626 |
-
progress(0.15, "Pass 1: Separating vocals/instrumental...")
|
| 627 |
try:
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
|
|
|
| 634 |
else:
|
| 635 |
-
|
|
|
|
|
|
|
| 636 |
except Exception as e:
|
| 637 |
-
log.append(f"Pass 1 error: {
|
| 638 |
|
| 639 |
-
# === PASS 2: 6-STEM SEPARATION (detailed) ===
|
| 640 |
-
stems_pass2_dir = package_dir / "stems_pass2"
|
| 641 |
stems_pass2 = {}
|
|
|
|
| 642 |
|
| 643 |
-
progress(0.35, "Pass 2: Separating detailed stems (
|
| 644 |
try:
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
|
|
|
| 651 |
else:
|
| 652 |
-
|
|
|
|
|
|
|
| 653 |
except Exception as e:
|
| 654 |
-
log.append(f"Pass 2 error: {
|
| 655 |
|
| 656 |
# === MASTER ORIGINAL (Warm preset) ===
|
| 657 |
mastered_path = None
|
|
@@ -711,14 +731,9 @@ def create_song_package(audio, yt_url, name, lyrics, user_email, progress=gr.Pro
|
|
| 711 |
progress(0.80, "Creating Reaper project...")
|
| 712 |
try:
|
| 713 |
if HAS_REAPER_MODULE:
|
| 714 |
-
# Use Pass 2 stems for Reaper (more detailed)
|
| 715 |
-
stems_for_reaper = stems_pass2_dir if stems_pass2 else
|
| 716 |
if stems_for_reaper and Path(stems_for_reaper).exists():
|
| 717 |
-
# Find the actual stems directory (Demucs creates nested dirs)
|
| 718 |
-
possible_dirs = list(Path(stems_for_reaper).glob('*/*'))
|
| 719 |
-
if possible_dirs:
|
| 720 |
-
stems_for_reaper = possible_dirs[0]
|
| 721 |
-
|
| 722 |
rpp_content = create_reaper_project(
|
| 723 |
song_name=song_name,
|
| 724 |
stems_dir=stems_for_reaper,
|
|
|
|
| 400 |
return None, str(e)
|
| 401 |
|
| 402 |
@spaces.GPU(duration=120)
|
| 403 |
+
def _separate_stems_gpu(audio_path: str, progress=None, two_stem: bool = False) -> dict:
|
| 404 |
"""Separate audio into stems using Demucs (GPU accelerated) - internal"""
|
| 405 |
try:
|
| 406 |
import subprocess
|
| 407 |
+
mode = "2stem" if two_stem else "6stem"
|
| 408 |
+
stems_dir = OUTPUT_DIR / f"stems_{mode}_{datetime.now().strftime('%H%M%S')}"
|
| 409 |
stems_dir.mkdir(exist_ok=True)
|
| 410 |
|
| 411 |
if progress:
|
| 412 |
+
if two_stem:
|
| 413 |
+
progress(0.3, "Running Demucs 2-stem separation (vocals/instrumental)...")
|
| 414 |
+
else:
|
| 415 |
+
progress(0.3, "Running Demucs 6-stem separation (drums, bass, guitar, keys, other, vocals)...")
|
| 416 |
+
|
| 417 |
+
# Build command based on mode
|
| 418 |
+
if two_stem:
|
| 419 |
+
cmd = ['python', '-m', 'demucs', '--two-stems=vocals', '-o', str(stems_dir), '--mp3', '--mp3-bitrate=320', audio_path]
|
| 420 |
+
else:
|
| 421 |
+
cmd = ['python', '-m', 'demucs', '-n', 'htdemucs_6s', '-o', str(stems_dir), '--mp3', '--mp3-bitrate=320', audio_path]
|
| 422 |
|
|
|
|
|
|
|
| 423 |
subprocess.run(cmd, capture_output=True, check=True)
|
| 424 |
|
| 425 |
# Find output stems
|
|
|
|
| 427 |
song_dir = list(model_dir.glob('*'))[0]
|
| 428 |
|
| 429 |
stems = {}
|
| 430 |
+
for stem_file in song_dir.glob('*.mp3'):
|
| 431 |
stem_name = stem_file.stem
|
| 432 |
stems[stem_name] = str(stem_file)
|
| 433 |
|
| 434 |
+
# Also check for wav files
|
| 435 |
+
for stem_file in song_dir.glob('*.wav'):
|
| 436 |
+
stem_name = stem_file.stem
|
| 437 |
+
if stem_name not in stems:
|
| 438 |
+
stems[stem_name] = str(stem_file)
|
| 439 |
+
|
| 440 |
+
return {'stems': stems, 'stems_dir': str(song_dir)}
|
| 441 |
except Exception as e:
|
| 442 |
return {'error': str(e)}
|
| 443 |
|
| 444 |
+
def separate_stems(audio_path: str, progress=None, two_stem: bool = False) -> dict:
|
| 445 |
"""Separate stems - wrapper with quota error handling"""
|
| 446 |
try:
|
| 447 |
+
return _separate_stems_gpu(audio_path, progress, two_stem)
|
| 448 |
except Exception as e:
|
| 449 |
return {'error': handle_gpu_error(e)}
|
| 450 |
|
|
|
|
| 633 |
key = analysis.get('key', 'C')
|
| 634 |
log.append(f"Key: {key} | BPM: {tempo}")
|
| 635 |
|
| 636 |
+
# === PASS 1: 2-STEM SEPARATION (vocals/instrumental) - GPU ACCELERATED ===
|
|
|
|
| 637 |
stems_pass1 = {}
|
| 638 |
+
stems_pass1_dir = None
|
| 639 |
|
| 640 |
+
progress(0.15, "Pass 1: Separating vocals/instrumental (GPU)...")
|
| 641 |
try:
|
| 642 |
+
result = separate_stems(audio_path, progress, two_stem=True)
|
| 643 |
+
if 'error' in result:
|
| 644 |
+
error_msg = result['error']
|
| 645 |
+
if is_quota_error(str(error_msg)):
|
| 646 |
+
log.append(QUOTA_ERROR_MSG)
|
| 647 |
+
else:
|
| 648 |
+
log.append(f"Pass 1 error: {error_msg}")
|
| 649 |
else:
|
| 650 |
+
stems_pass1 = result.get('stems', {})
|
| 651 |
+
stems_pass1_dir = result.get('stems_dir')
|
| 652 |
+
log.append(f"Pass 1 stems: {', '.join(stems_pass1.keys())}")
|
| 653 |
except Exception as e:
|
| 654 |
+
log.append(f"Pass 1 error: {handle_gpu_error(e)}")
|
| 655 |
|
| 656 |
+
# === PASS 2: 6-STEM SEPARATION (detailed) - GPU ACCELERATED ===
|
|
|
|
| 657 |
stems_pass2 = {}
|
| 658 |
+
stems_pass2_dir = None
|
| 659 |
|
| 660 |
+
progress(0.35, "Pass 2: Separating detailed stems (GPU)...")
|
| 661 |
try:
|
| 662 |
+
result = separate_stems(audio_path, progress, two_stem=False)
|
| 663 |
+
if 'error' in result:
|
| 664 |
+
error_msg = result['error']
|
| 665 |
+
if is_quota_error(str(error_msg)):
|
| 666 |
+
log.append(QUOTA_ERROR_MSG)
|
| 667 |
+
else:
|
| 668 |
+
log.append(f"Pass 2 error: {error_msg}")
|
| 669 |
else:
|
| 670 |
+
stems_pass2 = result.get('stems', {})
|
| 671 |
+
stems_pass2_dir = result.get('stems_dir')
|
| 672 |
+
log.append(f"Pass 2 stems: {', '.join(stems_pass2.keys())}")
|
| 673 |
except Exception as e:
|
| 674 |
+
log.append(f"Pass 2 error: {handle_gpu_error(e)}")
|
| 675 |
|
| 676 |
# === MASTER ORIGINAL (Warm preset) ===
|
| 677 |
mastered_path = None
|
|
|
|
| 731 |
progress(0.80, "Creating Reaper project...")
|
| 732 |
try:
|
| 733 |
if HAS_REAPER_MODULE:
|
| 734 |
+
# Use Pass 2 stems for Reaper (more detailed), fall back to Pass 1
|
| 735 |
+
stems_for_reaper = stems_pass2_dir if stems_pass2 else stems_pass1_dir
|
| 736 |
if stems_for_reaper and Path(stems_for_reaper).exists():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
rpp_content = create_reaper_project(
|
| 738 |
song_name=song_name,
|
| 739 |
stems_dir=stems_for_reaper,
|