Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import requests | |
| import os | |
| import time | |
| import json | |
| # Suno API key | |
| SUNO_KEY = os.environ.get("SunoKey", "") | |
| if not SUNO_KEY: | |
| print("⚠️ SunoKey not set!") | |
| def separate_vocals(task_id, audio_id, separation_type): | |
| """Separate vocals and instruments from Suno tracks""" | |
| if not SUNO_KEY: | |
| yield "❌ Error: SunoKey not configured in environment variables" | |
| return | |
| if not task_id.strip() or not audio_id.strip(): | |
| yield "❌ Error: Please enter both Task ID and Audio ID" | |
| 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( | |
| "https://api.sunoapi.org/api/v1/vocal-removal/generate", | |
| json={ | |
| "taskId": task_id, | |
| "audioId": audio_id, | |
| "type": separation_type, | |
| "callBackUrl": "https://1hit.no/callback.php" # Required but not used for polling | |
| }, | |
| 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"✅ **Submitted!**\nSeparation Task ID: `{separation_task_id}`\n\n⏳ Processing separation...\n" | |
| # Poll for results | |
| for attempt in range(40): # 40 attempts * 5 seconds = 200 seconds max | |
| time.sleep(5) | |
| try: | |
| check = requests.get( | |
| "https://api.sunoapi.org/api/v1/vocal-removal/record-info", | |
| 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("status", "PENDING") | |
| if status == "SUCCESS": | |
| # Success! Extract separation results | |
| separation_info = check_data["data"].get("vocal_removal_info", {}) | |
| if not separation_info: | |
| yield "✅ Completed but no separation results found" | |
| return | |
| # 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('vocal_url', 'N/A')})\n" | |
| output += f"**Instrumental:** [Download]({separation_info.get('instrumental_url', 'N/A')})\n" | |
| if separation_info.get('origin_url'): | |
| output += f"**Original:** [Download]({separation_info.get('origin_url')})\n" | |
| elif separation_type == "split_stem": | |
| output += "## 12-Stem Separation\n\n" | |
| stems = [ | |
| ("Vocals", separation_info.get('vocal_url')), | |
| ("Backing Vocals", separation_info.get('backing_vocals_url')), | |
| ("Drums", separation_info.get('drums_url')), | |
| ("Bass", separation_info.get('bass_url')), | |
| ("Guitar", separation_info.get('guitar_url')), | |
| ("Keyboard", separation_info.get('keyboard_url')), | |
| ("Strings", separation_info.get('strings_url')), | |
| ("Brass", separation_info.get('brass_url')), | |
| ("Woodwinds", separation_info.get('woodwinds_url')), | |
| ("Percussion", separation_info.get('percussion_url')), | |
| ("Synth", separation_info.get('synth_url')), | |
| ("FX/Other", separation_info.get('fx_url')), | |
| ("Instrumental", separation_info.get('instrumental_url')), | |
| ("Original", separation_info.get('origin_url')) | |
| ] | |
| for stem_name, stem_url in stems: | |
| if stem_url: | |
| output += f"**{stem_name}:** [Download]({stem_url})\n" | |
| output += f"\n⏱️ Processed in about {(attempt + 1) * 5} seconds\n" | |
| output += f"⚠️ **Note:** Download links expire in 14 days" | |
| yield output | |
| return | |
| elif status == "FAILED": | |
| error = check_data["data"].get("errorMessage", "Unknown error") | |
| yield f"❌ Separation failed: {error}" | |
| return | |
| else: | |
| # PENDING or PROCESSING | |
| yield (f"⏳ Status: {status}\n" | |
| f"Attempt: {attempt + 1}/40\n" | |
| f"Separation Task ID: `{separation_task_id}`\n\n" | |
| f"Processing... (Usually takes 30-120 seconds)") | |
| else: | |
| yield f"⚠️ Check error: HTTP {check.status_code}" | |
| except Exception as e: | |
| yield f"⚠️ Error checking status: {str(e)}" | |
| yield "⏰ Timeout after 200 seconds. Try checking again later." | |
| except Exception as e: | |
| yield f"❌ Error: {str(e)}" | |
| # Create the app | |
| with gr.Blocks(title="Suno Stem Separator", theme="soft") as app: | |
| gr.Markdown("# 🎵 Suno Stem Separator") | |
| gr.Markdown("Separate Suno AI tracks into vocal and instrument stems") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| task_id = gr.Textbox( | |
| label="Original Task ID", | |
| placeholder="Example: 5c79****be8e", | |
| info="The task ID from your Suno music generation" | |
| ) | |
| audio_id = gr.Textbox( | |
| label="Audio ID", | |
| placeholder="Example: e231****-****-****-****-****8cadc7dc", | |
| info="The specific audio ID to separate" | |
| ) | |
| separation_type = gr.Radio( | |
| label="Separation Type", | |
| choices=[ | |
| ("separate_vocal (2 stems - 1 credit)", "separate_vocal"), | |
| ("split_stem (12 stems - 5 credits)", "split_stem") | |
| ], | |
| value="separate_vocal", | |
| info="Choose separation mode" | |
| ) | |
| btn = gr.Button("🎵 Separate Stems", variant="primary", scale=1) | |
| gr.Markdown(""" | |
| **How it works:** | |
| 1. Enter Task ID and Audio ID from your Suno track | |
| 2. Choose separation type | |
| 3. Click Separate Stems | |
| 4. Wait 30-120 seconds | |
| 5. Get download links for each stem | |
| **Separation types:** | |
| - 🎤 **separate_vocal**: Vocals + Instrumental (2 stems, 1 credit) | |
| - 🎛️ **split_stem**: 12 detailed stems (5 credits) | |
| **Stem Types in split_stem:** | |
| - Vocals, Backing Vocals, Drums, Bass | |
| - Guitar, Keyboard, Strings, Brass | |
| - Woodwinds, Percussion, Synth, FX/Other | |
| ⚠️ **Important:** | |
| - Each request consumes credits | |
| - Links expire in 14 days | |
| """) | |
| with gr.Column(scale=2): | |
| output = gr.Markdown( | |
| label="Separation Results", | |
| value="Your separated stems will appear here..." | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown( | |
| """ | |
| <div style="text-align: center; padding: 20px;"> | |
| <p>Powered by <a href="https://suno.ai" target="_blank">Suno AI</a> • | |
| <a href="https://sunoapi.org" target="_blank">Suno API Docs</a> • | |
| <a href="https://docs.sunoapi.org/separate-vocals" target="_blank">Stem Separation Guide</a></p> | |
| <p><small>This app uses the Suno API to separate tracks into individual stems.</small></p> | |
| <p><small>💡 <strong>Tip:</strong> Find your Task ID and Audio ID in your Suno generation history or API responses.</small></p> | |
| </div> | |
| """, | |
| elem_id="footer" | |
| ) | |
| btn.click(separate_vocals, [task_id, audio_id, separation_type], 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") | |
| app.launch(server_name="0.0.0.0", server_port=7860, share=False) |