import gradio as gr import requests import os import time import json from datetime import datetime import re # Suno API key SUNO_KEY = os.environ.get("SunoKey", "") if not SUNO_KEY: print("⚠️ SunoKey not set in environment variables!") # API endpoints API_BASE = "https://api.sunoapi.org" CREDIT_URL = f"{API_BASE}/api/v1/generate/credit" GENERATION_RECORD_URL = f"{API_BASE}/api/v1/generate/record-info" VOCAL_SEPARATION_URL = f"{API_BASE}/api/v1/vocal-removal/generate" SEPARATION_RECORD_URL = f"{API_BASE}/api/v1/vocal-removal/record-info" def check_credits(): """Check available credits""" try: response = requests.get(CREDIT_URL, headers={"Authorization": f"Bearer {SUNO_KEY}"}, timeout=10) if response.status_code == 200: data = response.json() if data.get("code") == 200: credits = data.get("data", {}).get("credit_balance", 0) return f"✅ Available credits: **{credits}**" return "⚠️ Could not retrieve credits" except: return "⚠️ Credit check failed" def get_music_tasks(): """Get list of recent music generation tasks""" if not SUNO_KEY: return "❌ API key not configured", [] try: response = requests.get( GENERATION_RECORD_URL, headers={"Authorization": f"Bearer {SUNO_KEY}"}, params={"page": 1, "pageSize": 20}, timeout=30 ) if response.status_code == 200: data = response.json() if data.get("code") == 200: tasks = data.get("data", {}).get("data", []) if not tasks: return "📭 No music generation tasks found. Please generate music first.", [] task_list = [] for task in tasks: task_id = task.get("taskId", "") status = task.get("status", "") audio_urls = task.get("audioUrl", []) # Get audio IDs if available audio_info = [] if isinstance(audio_urls, list): for i, url in enumerate(audio_urls[:2]): # First 2 variants # Extract audio ID from URL or use placeholder audio_id = extract_audio_id(url) or f"variant-{i+1}" audio_info.append(f"Audio {i+1}: {audio_id}") task_info = f"Task: `{task_id[:8]}...` | Status: {status}" if audio_info: task_info += f" | {', '.join(audio_info)}" task_list.append((task_id, task_info)) return f"✅ Found {len(task_list)} recent tasks", task_list else: return f"❌ API error: {data.get('msg')}", [] else: return f"❌ HTTP error: {response.status_code}", [] except Exception as e: return f"❌ Error: {str(e)}", [] def extract_audio_id(url): """Extract audio ID from URL""" if not url: return None # Try to find UUID pattern in URL uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' match = re.search(uuid_pattern, url, re.IGNORECASE) if match: return match.group(0) # Try to find in common URL patterns patterns = [ r'audioId=([^&]+)', r'audio/([^/]+)', r'id=([^&]+)' ] for pattern in patterns: match = re.search(pattern, url) if match: return match.group(1) return None def get_audio_ids_from_task(task_id): """Get audio IDs for a specific task""" if not task_id: return "❌ Please select a task first", [] try: response = requests.get( GENERATION_RECORD_URL, headers={"Authorization": f"Bearer {SUNO_KEY}"}, params={"taskId": task_id}, timeout=30 ) if response.status_code == 200: data = response.json() if data.get("code") == 200: task_data = data.get("data", {}) audio_urls = task_data.get("audioUrl", []) if isinstance(audio_urls, list) and audio_urls: audio_options = [] for i, url in enumerate(audio_urls): audio_id = extract_audio_id(url) or f"audio-{i+1}" display_name = f"Audio {i+1}" if audio_id: display_name += f" (ID: {audio_id[:8]}...)" audio_options.append((url, display_name)) return f"✅ Found {len(audio_options)} audio tracks", audio_options else: return "⚠️ No audio tracks found for this task", [] else: return f"❌ API error: {data.get('msg')}", [] else: return f"❌ HTTP error: {response.status_code}", [] except Exception as e: return f"❌ Error: {str(e)}", [] def separate_vocals(task_id, audio_url, separation_type): """Separate vocals and instruments from selected audio""" if not SUNO_KEY: yield "❌ Error: SunoKey not configured" return if not task_id or not audio_url: yield "❌ Error: Please select both Task and Audio" return # Extract audio ID from URL audio_id = extract_audio_id(audio_url) if not audio_id: yield "❌ Could not extract Audio ID from the selected audio. Please check the URL format." return # Validate separation type if separation_type not in ["separate_vocal", "split_stem"]: yield "❌ Error: Invalid separation type" return # Submit separation task try: resp = requests.post( VOCAL_SEPARATION_URL, json={ "taskId": task_id, "audioId": audio_id, "type": separation_type, "callBackUrl": "http://dummy.com/callback" }, headers={ "Authorization": f"Bearer {SUNO_KEY}", "Content-Type": "application/json" }, timeout=30 ) if resp.status_code != 200: yield f"❌ Submission failed: HTTP {resp.status_code}" return data = resp.json() if data.get("code") != 200: yield f"❌ API error: {data.get('msg', 'Unknown')}" return separation_task_id = data["data"]["taskId"] yield f"✅ **Separation Task Submitted!**\n\n" yield f"**Separation Task ID:** `{separation_task_id}`\n" yield f"**Original Task ID:** `{task_id}`\n" yield f"**Audio ID:** `{audio_id}`\n" yield f"**Separation Type:** {separation_type}\n\n" yield f"⏳ **Processing...** (Usually takes 30-120 seconds)\n\n" # Poll for results for attempt in range(40): # 40 attempts * 5 seconds = 200 seconds max time.sleep(5) try: check = requests.get( SEPARATION_RECORD_URL, headers={"Authorization": f"Bearer {SUNO_KEY}"}, params={"taskId": separation_task_id}, timeout=30 ) if check.status_code == 200: check_data = check.json() status = check_data["data"].get("successFlag", "PENDING") if status == "SUCCESS": # Success! Extract separation results separation_info = check_data["data"].get("response", {}) # Format output based on separation type output = "🎵 **Separation Complete!**\n\n" if separation_type == "separate_vocal": output += "## 🎤 2-Stem Separation\n\n" output += f"**🎤 Vocals:** [Download]({separation_info.get('vocalUrl', 'N/A')})\n" output += f"**🎵 Instrumental:** [Download]({separation_info.get('instrumentalUrl', 'N/A')})\n" if separation_info.get('originUrl'): output += f"**📁 Original:** [Download]({separation_info.get('originUrl')})\n" elif separation_type == "split_stem": output += "## 🎛️ 12-Stem Separation\n\n" stems = [ ("🎤 Vocals", separation_info.get('vocalUrl')), ("🎤 Backing Vocals", separation_info.get('backingVocalsUrl')), ("🥁 Drums", separation_info.get('drumsUrl')), ("🎸 Bass", separation_info.get('bassUrl')), ("🎸 Guitar", separation_info.get('guitarUrl')), ("🎹 Keyboard", separation_info.get('keyboardUrl')), ("🎻 Strings", separation_info.get('stringsUrl')), ("🎺 Brass", separation_info.get('brassUrl')), ("🎷 Woodwinds", separation_info.get('woodwindsUrl')), ("🪇 Percussion", separation_info.get('percussionUrl')), ("🎹 Synth", separation_info.get('synthUrl')), ("🎛️ FX/Other", separation_info.get('fxUrl')), ("📁 Original", separation_info.get('originUrl')) ] for stem_name, stem_url in stems: if stem_url: output += f"**{stem_name}:** [Download]({stem_url})\n" output += f"\n⏱️ **Processing Time:** {(attempt + 1) * 5} seconds\n" output += f"📊 **Separation Task ID:** `{separation_task_id}`\n" output += f"⚠️ **Note:** Download links expire in 14 days\n" yield output return elif status in ["CREATE_TASK_FAILED", "GENERATE_AUDIO_FAILED", "CALLBACK_EXCEPTION"]: error = check_data["data"].get("errorMessage", "Unknown error") yield f"❌ **Separation failed:** {status}\n\n" yield f"**Error:** {error}\n" return else: # PENDING or other statuses yield (f"⏳ **Status:** {status}\n" f"📊 **Progress:** {attempt + 1}/40 attempts\n" f"⏱️ **Elapsed:** {(attempt + 1) * 5} seconds\n\n" f"Still processing...") else: yield f"⚠️ **Check error:** HTTP {check.status_code}\n" except Exception as e: yield f"⚠️ **Error checking status:** {str(e)}\n" yield "⏰ **Timeout after 200 seconds.** Try checking the task manually later.\n" yield f"**Separation Task ID:** `{separation_task_id}`\n" except Exception as e: yield f"❌ **Error submitting task:** {str(e)}" def refresh_tasks(): """Refresh the list of available tasks""" status, tasks = get_music_tasks() task_options = [("", "Select a task...")] + tasks if tasks else [("", "No tasks found")] return status, gr.Dropdown(choices=[opt[1] for opt in task_options], value=task_options[0][1]), task_options # Create the app with gr.Blocks(title="Suno Stem Separator", theme="soft") as app: gr.Markdown("# 🎵 Suno AI Stem Separator") gr.Markdown("Separate vocals and instruments from your Suno AI generated music") # Credit display at top credit_display = gr.Markdown(check_credits(), elem_id="credits") with gr.Tabs(): with gr.TabItem("🎵 Step 1: Select Your Music"): gr.Markdown(""" ### Find your generated music tracks 1. Click **Refresh Tasks** to load your recent Suno AI music generations 2. Select a task from the dropdown 3. The system will automatically find available audio tracks """) with gr.Row(): refresh_btn = gr.Button("🔄 Refresh Tasks", variant="secondary") task_status = gr.Markdown("Click 'Refresh Tasks' to load your music tasks") with gr.Row(): with gr.Column(scale=1): task_dropdown = gr.Dropdown( label="Select Music Generation Task", choices=["Select a task..."], value="Select a task...", interactive=True ) gr.Markdown(""" **What is a Task ID?** - When you generate music with Suno AI, each generation gets a unique Task ID - This ID is required to separate vocals from your music - Select your task from the dropdown above """) with gr.Column(scale=1): audio_status = gr.Markdown("Select a task to see available audio tracks") audio_dropdown = gr.Dropdown( label="Select Audio Track to Separate", choices=["First select a task..."], value="First select a task...", interactive=True ) gr.Markdown(""" **What is an Audio ID?** - Each music generation can have multiple audio variants - The Audio ID identifies which specific track to process - Usually you'll have 2-4 audio tracks per generation """) # Hidden storage for task data task_store = gr.State([]) # Stores (task_id, display_name) pairs with gr.TabItem("🎛️ Step 2: Configure Separation"): gr.Markdown(""" ### Configure how you want to separate the audio **Choose separation type:** - **🎤 Vocal Separation (1 credit)**: Separate vocals from instrumental - **🎛️ Full Stem Separation (5 credits)**: Split into 12 individual instrument stems **Stem types in full separation:** - Vocals, Backing Vocals, Drums, Bass, Guitar - Keyboard, Strings, Brass, Woodwinds, Percussion - Synthesizer, FX/Other """) separation_type = gr.Radio( label="Separation Type", choices=[ ("🎤 Vocal Separation (2 stems, 1 credit)", "separate_vocal"), ("🎛️ Full Stem Separation (12 stems, 5 credits)", "split_stem") ], value="separate_vocal", info="Choose how detailed the separation should be" ) with gr.Row(): selected_task_display = gr.Textbox( label="Selected Task ID", interactive=False, value="No task selected" ) selected_audio_display = gr.Textbox( label="Selected Audio ID", interactive=False, value="No audio selected" ) separate_btn = gr.Button("🚀 Start Separation", variant="primary", scale=1) gr.Markdown(""" **⚠️ Important Notes:** - Each separation consumes credits (shown at top of page) - Processing takes 30-120 seconds - Download links expire after 14 days - You can reuse the Separation Task ID to download files later """) with gr.TabItem("📥 Step 3: Get Results"): output = gr.Markdown( label="Separation Results", value="Your separated stems will appear here once processing is complete..." ) gr.Markdown(""" **What to expect:** 1. After clicking **Start Separation**, you'll get a Separation Task ID 2. The system will automatically check progress every 5 seconds 3. When complete, you'll see download links for all stems 4. Links remain active for **14 days** - download promptly! **Need to check a previous separation?** Use the **Check Existing Task** tab (coming soon!) """) gr.Markdown("---") gr.Markdown( """

Powered by Suno AISuno APIAPI Docs

This tool helps you separate vocals and instruments from Suno AI generated music.

""", elem_id="footer" ) # Event handlers def on_task_select(selected_display, task_store_data): """When a task is selected from dropdown""" # Find the actual task_id from the display name for task_id, display_name in task_store_data: if display_name == selected_display: # Get audio IDs for this task status, audio_options = get_audio_ids_from_task(task_id) # Update audio dropdown audio_choices = [("", "Select audio...")] + audio_options if audio_options else [("", "No audio found")] return ( status, gr.Dropdown(choices=[opt[1] for opt in audio_choices], value=audio_choices[0][1]), task_id[:12] + "..." if len(task_id) > 12 else task_id, "Select audio above..." ) return "❌ Task not found", gr.Dropdown(choices=["Task not found"], value="Task not found"), "Error", "Error" def on_audio_select(selected_audio_display, audio_options_data): """When an audio is selected""" # Extract audio ID from the selected option # The display format is "Audio 1 (ID: abc123...)" or similar import re match = re.search(r'ID:\s*([^\s)]+)', selected_audio_display) audio_id = match.group(1) if match else selected_audio_display return audio_id # Connect events refresh_btn.click( refresh_tasks, outputs=[task_status, task_dropdown, task_store] ) task_dropdown.change( on_task_select, inputs=[task_dropdown, task_store], outputs=[audio_status, audio_dropdown, selected_task_display, selected_audio_display] ) audio_dropdown.change( lambda x: x, # Simple pass-through for now inputs=[audio_dropdown], outputs=[selected_audio_display] ) separate_btn.click( separate_vocals, inputs=[selected_task_display, audio_dropdown, separation_type], outputs=[output] ) if __name__ == "__main__": print("🚀 Starting Suno Stem Separator") print(f"🔑 SunoKey: {'✅ Set' if SUNO_KEY else '❌ Not set'}") print("🌐 Open your browser to: http://localhost:7860") print("\n💡 Instructions:") print("1. Make sure you have generated music with Suno AI") print("2. Refresh tasks to load your music generations") print("3. Select a task and audio track") print("4. Choose separation type and start processing") app.launch(server_name="0.0.0.0", server_port=7860, share=False)