MySafeCode commited on
Commit
4b80684
·
verified ·
1 Parent(s): 858e9bd

Create app2.py

Browse files
Files changed (1) hide show
  1. app2.py +491 -0
app2.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import os
4
+ import time
5
+ import json
6
+ from datetime import datetime
7
+ import re
8
+
9
+ # Suno API key
10
+ SUNO_KEY = os.environ.get("SunoKey", "")
11
+ if not SUNO_KEY:
12
+ print("⚠️ SunoKey not set in environment variables!")
13
+
14
+ # API endpoints
15
+ API_BASE = "https://api.sunoapi.org"
16
+ CREDIT_URL = f"{API_BASE}/api/v1/generate/credit"
17
+ GENERATION_RECORD_URL = f"{API_BASE}/api/v1/generate/record-info"
18
+ VOCAL_SEPARATION_URL = f"{API_BASE}/api/v1/vocal-removal/generate"
19
+ SEPARATION_RECORD_URL = f"{API_BASE}/api/v1/vocal-removal/record-info"
20
+
21
+ def check_credits():
22
+ """Check available credits"""
23
+ try:
24
+ response = requests.get(CREDIT_URL, headers={"Authorization": f"Bearer {SUNO_KEY}"}, timeout=10)
25
+ if response.status_code == 200:
26
+ data = response.json()
27
+ if data.get("code") == 200:
28
+ credits = data.get("data", {}).get("credit_balance", 0)
29
+ return f"✅ Available credits: **{credits}**"
30
+ return "⚠️ Could not retrieve credits"
31
+ except:
32
+ return "⚠️ Credit check failed"
33
+
34
+ def get_music_tasks():
35
+ """Get list of recent music generation tasks"""
36
+ if not SUNO_KEY:
37
+ return "❌ API key not configured", []
38
+
39
+ try:
40
+ response = requests.get(
41
+ GENERATION_RECORD_URL,
42
+ headers={"Authorization": f"Bearer {SUNO_KEY}"},
43
+ params={"page": 1, "pageSize": 20},
44
+ timeout=30
45
+ )
46
+
47
+ if response.status_code == 200:
48
+ data = response.json()
49
+ if data.get("code") == 200:
50
+ tasks = data.get("data", {}).get("data", [])
51
+
52
+ if not tasks:
53
+ return "📭 No music generation tasks found. Please generate music first.", []
54
+
55
+ task_list = []
56
+ for task in tasks:
57
+ task_id = task.get("taskId", "")
58
+ status = task.get("status", "")
59
+ audio_urls = task.get("audioUrl", [])
60
+
61
+ # Get audio IDs if available
62
+ audio_info = []
63
+ if isinstance(audio_urls, list):
64
+ for i, url in enumerate(audio_urls[:2]): # First 2 variants
65
+ # Extract audio ID from URL or use placeholder
66
+ audio_id = extract_audio_id(url) or f"variant-{i+1}"
67
+ audio_info.append(f"Audio {i+1}: {audio_id}")
68
+
69
+ task_info = f"Task: `{task_id[:8]}...` | Status: {status}"
70
+ if audio_info:
71
+ task_info += f" | {', '.join(audio_info)}"
72
+
73
+ task_list.append((task_id, task_info))
74
+
75
+ return f"✅ Found {len(task_list)} recent tasks", task_list
76
+ else:
77
+ return f"❌ API error: {data.get('msg')}", []
78
+ else:
79
+ return f"❌ HTTP error: {response.status_code}", []
80
+
81
+ except Exception as e:
82
+ return f"❌ Error: {str(e)}", []
83
+
84
+ def extract_audio_id(url):
85
+ """Extract audio ID from URL"""
86
+ if not url:
87
+ return None
88
+
89
+ # Try to find UUID pattern in URL
90
+ uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
91
+ match = re.search(uuid_pattern, url, re.IGNORECASE)
92
+ if match:
93
+ return match.group(0)
94
+
95
+ # Try to find in common URL patterns
96
+ patterns = [
97
+ r'audioId=([^&]+)',
98
+ r'audio/([^/]+)',
99
+ r'id=([^&]+)'
100
+ ]
101
+
102
+ for pattern in patterns:
103
+ match = re.search(pattern, url)
104
+ if match:
105
+ return match.group(1)
106
+
107
+ return None
108
+
109
+ def get_audio_ids_from_task(task_id):
110
+ """Get audio IDs for a specific task"""
111
+ if not task_id:
112
+ return "❌ Please select a task first", []
113
+
114
+ try:
115
+ response = requests.get(
116
+ GENERATION_RECORD_URL,
117
+ headers={"Authorization": f"Bearer {SUNO_KEY}"},
118
+ params={"taskId": task_id},
119
+ timeout=30
120
+ )
121
+
122
+ if response.status_code == 200:
123
+ data = response.json()
124
+ if data.get("code") == 200:
125
+ task_data = data.get("data", {})
126
+ audio_urls = task_data.get("audioUrl", [])
127
+
128
+ if isinstance(audio_urls, list) and audio_urls:
129
+ audio_options = []
130
+ for i, url in enumerate(audio_urls):
131
+ audio_id = extract_audio_id(url) or f"audio-{i+1}"
132
+ display_name = f"Audio {i+1}"
133
+ if audio_id:
134
+ display_name += f" (ID: {audio_id[:8]}...)"
135
+ audio_options.append((url, display_name))
136
+
137
+ return f"✅ Found {len(audio_options)} audio tracks", audio_options
138
+ else:
139
+ return "⚠️ No audio tracks found for this task", []
140
+ else:
141
+ return f"❌ API error: {data.get('msg')}", []
142
+ else:
143
+ return f"❌ HTTP error: {response.status_code}", []
144
+
145
+ except Exception as e:
146
+ return f"❌ Error: {str(e)}", []
147
+
148
+ def separate_vocals(task_id, audio_url, separation_type):
149
+ """Separate vocals and instruments from selected audio"""
150
+ if not SUNO_KEY:
151
+ yield "❌ Error: SunoKey not configured"
152
+ return
153
+
154
+ if not task_id or not audio_url:
155
+ yield "❌ Error: Please select both Task and Audio"
156
+ return
157
+
158
+ # Extract audio ID from URL
159
+ audio_id = extract_audio_id(audio_url)
160
+ if not audio_id:
161
+ yield "❌ Could not extract Audio ID from the selected audio. Please check the URL format."
162
+ return
163
+
164
+ # Validate separation type
165
+ if separation_type not in ["separate_vocal", "split_stem"]:
166
+ yield "❌ Error: Invalid separation type"
167
+ return
168
+
169
+ # Submit separation task
170
+ try:
171
+ resp = requests.post(
172
+ VOCAL_SEPARATION_URL,
173
+ json={
174
+ "taskId": task_id,
175
+ "audioId": audio_id,
176
+ "type": separation_type,
177
+ "callBackUrl": "http://dummy.com/callback"
178
+ },
179
+ headers={
180
+ "Authorization": f"Bearer {SUNO_KEY}",
181
+ "Content-Type": "application/json"
182
+ },
183
+ timeout=30
184
+ )
185
+
186
+ if resp.status_code != 200:
187
+ yield f"❌ Submission failed: HTTP {resp.status_code}"
188
+ return
189
+
190
+ data = resp.json()
191
+ if data.get("code") != 200:
192
+ yield f"❌ API error: {data.get('msg', 'Unknown')}"
193
+ return
194
+
195
+ separation_task_id = data["data"]["taskId"]
196
+ yield f"✅ **Separation Task Submitted!**\n\n"
197
+ yield f"**Separation Task ID:** `{separation_task_id}`\n"
198
+ yield f"**Original Task ID:** `{task_id}`\n"
199
+ yield f"**Audio ID:** `{audio_id}`\n"
200
+ yield f"**Separation Type:** {separation_type}\n\n"
201
+ yield f"⏳ **Processing...** (Usually takes 30-120 seconds)\n\n"
202
+
203
+ # Poll for results
204
+ for attempt in range(40): # 40 attempts * 5 seconds = 200 seconds max
205
+ time.sleep(5)
206
+
207
+ try:
208
+ check = requests.get(
209
+ SEPARATION_RECORD_URL,
210
+ headers={"Authorization": f"Bearer {SUNO_KEY}"},
211
+ params={"taskId": separation_task_id},
212
+ timeout=30
213
+ )
214
+
215
+ if check.status_code == 200:
216
+ check_data = check.json()
217
+ status = check_data["data"].get("successFlag", "PENDING")
218
+
219
+ if status == "SUCCESS":
220
+ # Success! Extract separation results
221
+ separation_info = check_data["data"].get("response", {})
222
+
223
+ # Format output based on separation type
224
+ output = "🎵 **Separation Complete!**\n\n"
225
+
226
+ if separation_type == "separate_vocal":
227
+ output += "## 🎤 2-Stem Separation\n\n"
228
+ output += f"**🎤 Vocals:** [Download]({separation_info.get('vocalUrl', 'N/A')})\n"
229
+ output += f"**🎵 Instrumental:** [Download]({separation_info.get('instrumentalUrl', 'N/A')})\n"
230
+ if separation_info.get('originUrl'):
231
+ output += f"**📁 Original:** [Download]({separation_info.get('originUrl')})\n"
232
+
233
+ elif separation_type == "split_stem":
234
+ output += "## 🎛️ 12-Stem Separation\n\n"
235
+
236
+ stems = [
237
+ ("🎤 Vocals", separation_info.get('vocalUrl')),
238
+ ("🎤 Backing Vocals", separation_info.get('backingVocalsUrl')),
239
+ ("🥁 Drums", separation_info.get('drumsUrl')),
240
+ ("🎸 Bass", separation_info.get('bassUrl')),
241
+ ("🎸 Guitar", separation_info.get('guitarUrl')),
242
+ ("🎹 Keyboard", separation_info.get('keyboardUrl')),
243
+ ("🎻 Strings", separation_info.get('stringsUrl')),
244
+ ("🎺 Brass", separation_info.get('brassUrl')),
245
+ ("🎷 Woodwinds", separation_info.get('woodwindsUrl')),
246
+ ("🪇 Percussion", separation_info.get('percussionUrl')),
247
+ ("🎹 Synth", separation_info.get('synthUrl')),
248
+ ("🎛️ FX/Other", separation_info.get('fxUrl')),
249
+ ("📁 Original", separation_info.get('originUrl'))
250
+ ]
251
+
252
+ for stem_name, stem_url in stems:
253
+ if stem_url:
254
+ output += f"**{stem_name}:** [Download]({stem_url})\n"
255
+
256
+ output += f"\n⏱️ **Processing Time:** {(attempt + 1) * 5} seconds\n"
257
+ output += f"📊 **Separation Task ID:** `{separation_task_id}`\n"
258
+ output += f"⚠️ **Note:** Download links expire in 14 days\n"
259
+
260
+ yield output
261
+ return
262
+
263
+ elif status in ["CREATE_TASK_FAILED", "GENERATE_AUDIO_FAILED", "CALLBACK_EXCEPTION"]:
264
+ error = check_data["data"].get("errorMessage", "Unknown error")
265
+ yield f"❌ **Separation failed:** {status}\n\n"
266
+ yield f"**Error:** {error}\n"
267
+ return
268
+
269
+ else:
270
+ # PENDING or other statuses
271
+ yield (f"⏳ **Status:** {status}\n"
272
+ f"📊 **Progress:** {attempt + 1}/40 attempts\n"
273
+ f"⏱️ **Elapsed:** {(attempt + 1) * 5} seconds\n\n"
274
+ f"Still processing...")
275
+ else:
276
+ yield f"⚠️ **Check error:** HTTP {check.status_code}\n"
277
+
278
+ except Exception as e:
279
+ yield f"⚠️ **Error checking status:** {str(e)}\n"
280
+
281
+ yield "⏰ **Timeout after 200 seconds.** Try checking the task manually later.\n"
282
+ yield f"**Separation Task ID:** `{separation_task_id}`\n"
283
+
284
+ except Exception as e:
285
+ yield f"❌ **Error submitting task:** {str(e)}"
286
+
287
+ def refresh_tasks():
288
+ """Refresh the list of available tasks"""
289
+ status, tasks = get_music_tasks()
290
+ task_options = [("", "Select a task...")] + tasks if tasks else [("", "No tasks found")]
291
+ return status, gr.Dropdown(choices=[opt[1] for opt in task_options], value=task_options[0][1]), task_options
292
+
293
+ # Create the app
294
+ with gr.Blocks(title="Suno Stem Separator", theme="soft") as app:
295
+ gr.Markdown("# 🎵 Suno AI Stem Separator")
296
+ gr.Markdown("Separate vocals and instruments from your Suno AI generated music")
297
+
298
+ # Credit display at top
299
+ credit_display = gr.Markdown(check_credits(), elem_id="credits")
300
+
301
+ with gr.Tabs():
302
+ with gr.TabItem("🎵 Step 1: Select Your Music"):
303
+ gr.Markdown("""
304
+ ### Find your generated music tracks
305
+
306
+ 1. Click **Refresh Tasks** to load your recent Suno AI music generations
307
+ 2. Select a task from the dropdown
308
+ 3. The system will automatically find available audio tracks
309
+ """)
310
+
311
+ with gr.Row():
312
+ refresh_btn = gr.Button("🔄 Refresh Tasks", variant="secondary")
313
+ task_status = gr.Markdown("Click 'Refresh Tasks' to load your music tasks")
314
+
315
+ with gr.Row():
316
+ with gr.Column(scale=1):
317
+ task_dropdown = gr.Dropdown(
318
+ label="Select Music Generation Task",
319
+ choices=["Select a task..."],
320
+ value="Select a task...",
321
+ interactive=True
322
+ )
323
+
324
+ gr.Markdown("""
325
+ **What is a Task ID?**
326
+ - When you generate music with Suno AI, each generation gets a unique Task ID
327
+ - This ID is required to separate vocals from your music
328
+ - Select your task from the dropdown above
329
+ """)
330
+
331
+ with gr.Column(scale=1):
332
+ audio_status = gr.Markdown("Select a task to see available audio tracks")
333
+ audio_dropdown = gr.Dropdown(
334
+ label="Select Audio Track to Separate",
335
+ choices=["First select a task..."],
336
+ value="First select a task...",
337
+ interactive=True
338
+ )
339
+
340
+ gr.Markdown("""
341
+ **What is an Audio ID?**
342
+ - Each music generation can have multiple audio variants
343
+ - The Audio ID identifies which specific track to process
344
+ - Usually you'll have 2-4 audio tracks per generation
345
+ """)
346
+
347
+ # Hidden storage for task data
348
+ task_store = gr.State([]) # Stores (task_id, display_name) pairs
349
+
350
+ with gr.TabItem("🎛️ Step 2: Configure Separation"):
351
+ gr.Markdown("""
352
+ ### Configure how you want to separate the audio
353
+
354
+ **Choose separation type:**
355
+ - **🎤 Vocal Separation (1 credit)**: Separate vocals from instrumental
356
+ - **🎛️ Full Stem Separation (5 credits)**: Split into 12 individual instrument stems
357
+
358
+ **Stem types in full separation:**
359
+ - Vocals, Backing Vocals, Drums, Bass, Guitar
360
+ - Keyboard, Strings, Brass, Woodwinds, Percussion
361
+ - Synthesizer, FX/Other
362
+ """)
363
+
364
+ separation_type = gr.Radio(
365
+ label="Separation Type",
366
+ choices=[
367
+ ("🎤 Vocal Separation (2 stems, 1 credit)", "separate_vocal"),
368
+ ("🎛️ Full Stem Separation (12 stems, 5 credits)", "split_stem")
369
+ ],
370
+ value="separate_vocal",
371
+ info="Choose how detailed the separation should be"
372
+ )
373
+
374
+ with gr.Row():
375
+ selected_task_display = gr.Textbox(
376
+ label="Selected Task ID",
377
+ interactive=False,
378
+ value="No task selected"
379
+ )
380
+ selected_audio_display = gr.Textbox(
381
+ label="Selected Audio ID",
382
+ interactive=False,
383
+ value="No audio selected"
384
+ )
385
+
386
+ separate_btn = gr.Button("🚀 Start Separation", variant="primary", scale=1)
387
+
388
+ gr.Markdown("""
389
+ **⚠️ Important Notes:**
390
+ - Each separation consumes credits (shown at top of page)
391
+ - Processing takes 30-120 seconds
392
+ - Download links expire after 14 days
393
+ - You can reuse the Separation Task ID to download files later
394
+ """)
395
+
396
+ with gr.TabItem("📥 Step 3: Get Results"):
397
+ output = gr.Markdown(
398
+ label="Separation Results",
399
+ value="Your separated stems will appear here once processing is complete..."
400
+ )
401
+
402
+ gr.Markdown("""
403
+ **What to expect:**
404
+ 1. After clicking **Start Separation**, you'll get a Separation Task ID
405
+ 2. The system will automatically check progress every 5 seconds
406
+ 3. When complete, you'll see download links for all stems
407
+ 4. Links remain active for **14 days** - download promptly!
408
+
409
+ **Need to check a previous separation?**
410
+ Use the **Check Existing Task** tab (coming soon!)
411
+ """)
412
+
413
+ gr.Markdown("---")
414
+ gr.Markdown(
415
+ """
416
+ <div style="text-align: center; padding: 20px;">
417
+ <p>Powered by <a href="https://suno.ai" target="_blank">Suno AI</a> •
418
+ <a href="https://sunoapi.org" target="_blank">Suno API</a> •
419
+ <a href="https://docs.sunoapi.org" target="_blank">API Docs</a></p>
420
+ <p><small>This tool helps you separate vocals and instruments from Suno AI generated music.</small></p>
421
+ </div>
422
+ """,
423
+ elem_id="footer"
424
+ )
425
+
426
+ # Event handlers
427
+ def on_task_select(selected_display, task_store_data):
428
+ """When a task is selected from dropdown"""
429
+ # Find the actual task_id from the display name
430
+ for task_id, display_name in task_store_data:
431
+ if display_name == selected_display:
432
+ # Get audio IDs for this task
433
+ status, audio_options = get_audio_ids_from_task(task_id)
434
+
435
+ # Update audio dropdown
436
+ audio_choices = [("", "Select audio...")] + audio_options if audio_options else [("", "No audio found")]
437
+
438
+ return (
439
+ status,
440
+ gr.Dropdown(choices=[opt[1] for opt in audio_choices], value=audio_choices[0][1]),
441
+ task_id[:12] + "..." if len(task_id) > 12 else task_id,
442
+ "Select audio above..."
443
+ )
444
+
445
+ return "❌ Task not found", gr.Dropdown(choices=["Task not found"], value="Task not found"), "Error", "Error"
446
+
447
+ def on_audio_select(selected_audio_display, audio_options_data):
448
+ """When an audio is selected"""
449
+ # Extract audio ID from the selected option
450
+ # The display format is "Audio 1 (ID: abc123...)" or similar
451
+ import re
452
+ match = re.search(r'ID:\s*([^\s)]+)', selected_audio_display)
453
+ audio_id = match.group(1) if match else selected_audio_display
454
+
455
+ return audio_id
456
+
457
+ # Connect events
458
+ refresh_btn.click(
459
+ refresh_tasks,
460
+ outputs=[task_status, task_dropdown, task_store]
461
+ )
462
+
463
+ task_dropdown.change(
464
+ on_task_select,
465
+ inputs=[task_dropdown, task_store],
466
+ outputs=[audio_status, audio_dropdown, selected_task_display, selected_audio_display]
467
+ )
468
+
469
+ audio_dropdown.change(
470
+ lambda x: x, # Simple pass-through for now
471
+ inputs=[audio_dropdown],
472
+ outputs=[selected_audio_display]
473
+ )
474
+
475
+ separate_btn.click(
476
+ separate_vocals,
477
+ inputs=[selected_task_display, audio_dropdown, separation_type],
478
+ outputs=[output]
479
+ )
480
+
481
+ if __name__ == "__main__":
482
+ print("🚀 Starting Suno Stem Separator")
483
+ print(f"🔑 SunoKey: {'✅ Set' if SUNO_KEY else '❌ Not set'}")
484
+ print("🌐 Open your browser to: http://localhost:7860")
485
+ print("\n💡 Instructions:")
486
+ print("1. Make sure you have generated music with Suno AI")
487
+ print("2. Refresh tasks to load your music generations")
488
+ print("3. Select a task and audio track")
489
+ print("4. Choose separation type and start processing")
490
+
491
+ app.launch(server_name="0.0.0.0", server_port=7860, share=False)