MySafeCode commited on
Commit
5bca3f6
·
verified ·
1 Parent(s): dd95bb1

Create a.py

Browse files
Files changed (1) hide show
  1. a.py +781 -0
a.py ADDED
@@ -0,0 +1,781 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import gradio as gr
4
+ from dotenv import load_dotenv
5
+ import json
6
+ import uuid
7
+ import tempfile
8
+ from datetime import datetime
9
+ import logging
10
+ import sys
11
+ from typing import Dict, Any, Optional, Union, List
12
+ from dataclasses import dataclass
13
+ import time
14
+
15
+ # Load environment variables from .env file
16
+ load_dotenv()
17
+
18
+ # Configure logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
22
+ handlers=[
23
+ logging.StreamHandler(sys.stdout),
24
+ logging.FileHandler('suno_generator.log')
25
+ ]
26
+ )
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Configuration
30
+ class Config:
31
+ """Configuration manager"""
32
+ def __init__(self):
33
+ self.suno_api_key = os.environ.get("SUNO_API_KEY") or os.environ.get("SunoKey")
34
+ self.suno_api_url = "https://api.sunoapi.org/api/v1/generate/upload-cover"
35
+ self.fixed_callback_url = "https://1hit.no/cover/cb.php" # Your PHP callback URL
36
+ self.default_model = "V4_5ALL"
37
+ self.default_vocal_gender = "m"
38
+ self.max_retries = 3
39
+ self.request_timeout = 30
40
+
41
+ # Default values for UI
42
+ self.defaults = {
43
+ "prompt": "A calm and relaxing piano track with soft melodies",
44
+ "title": "Peaceful Piano Meditation",
45
+ "style": "Classical",
46
+ "style_weight": 0.65,
47
+ "weirdness_constraint": 0.65,
48
+ "audio_weight": 0.65,
49
+ "instrumental": True,
50
+ "custom_mode": True
51
+ }
52
+
53
+ config = Config()
54
+
55
+ # Data models
56
+ @dataclass
57
+ class ApiResponse:
58
+ """Structured API response"""
59
+ success: bool
60
+ message: str
61
+ task_id: Optional[str] = None
62
+ upload_url: Optional[str] = None
63
+ callback_url: Optional[str] = None
64
+ raw_response: Optional[Dict] = None
65
+ error_type: Optional[str] = None
66
+
67
+ class SunoAPIError(Exception):
68
+ """Custom exception for Suno API errors"""
69
+ pass
70
+
71
+ # Session management for requests
72
+ session = requests.Session()
73
+ session.headers.update({
74
+ "User-Agent": "SunoMusicGenerator/1.0",
75
+ "Accept": "application/json"
76
+ })
77
+
78
+ def validate_inputs(
79
+ prompt: str,
80
+ title: str,
81
+ style: str,
82
+ upload_url_type: str,
83
+ custom_upload_url: str
84
+ ) -> List[str]:
85
+ """Validate user inputs and return list of errors"""
86
+ errors = []
87
+
88
+ # Trim inputs
89
+ prompt = prompt.strip()
90
+ title = title.strip()
91
+ style = style.strip()
92
+ custom_upload_url = custom_upload_url.strip()
93
+
94
+ # Check required fields
95
+ if len(prompt) < 5:
96
+ errors.append("Prompt should be at least 5 characters")
97
+ if not title:
98
+ errors.append("Title is required")
99
+ if not style:
100
+ errors.append("Style is required")
101
+
102
+ # Check custom URL if selected
103
+ if upload_url_type == "custom":
104
+ if not custom_upload_url:
105
+ errors.append("Please provide a custom upload URL")
106
+ elif not custom_upload_url.startswith(("http://", "https://")):
107
+ errors.append("Custom URL must start with http:// or https://")
108
+
109
+ return errors
110
+
111
+ def generate_temp_upload_url() -> str:
112
+ """Generate a simulated temporary upload URL"""
113
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
114
+ unique_id = str(uuid.uuid4())[:8]
115
+ return f"https://storage.temp.example.com/uploads/{timestamp}_{unique_id}.mp3"
116
+
117
+ def make_api_request_with_retry(
118
+ payload: Dict[str, Any],
119
+ headers: Dict[str, str],
120
+ max_retries: int = 3
121
+ ) -> requests.Response:
122
+ """Make API request with retry logic"""
123
+ last_exception = None
124
+
125
+ for attempt in range(max_retries):
126
+ try:
127
+ logger.info(f"API Request Attempt {attempt + 1}/{max_retries}")
128
+ response = session.post(
129
+ config.suno_api_url,
130
+ json=payload,
131
+ headers=headers,
132
+ timeout=config.request_timeout
133
+ )
134
+ response.raise_for_status()
135
+ return response
136
+
137
+ except requests.exceptions.Timeout:
138
+ logger.warning(f"Request timeout (attempt {attempt + 1})")
139
+ last_exception = "Timeout"
140
+ if attempt < max_retries - 1:
141
+ time.sleep(2 ** attempt) # Exponential backoff
142
+
143
+ except requests.exceptions.ConnectionError:
144
+ logger.warning(f"Connection error (attempt {attempt + 1})")
145
+ last_exception = "Connection error"
146
+ if attempt < max_retries - 1:
147
+ time.sleep(2 ** attempt)
148
+
149
+ except requests.exceptions.HTTPError as e:
150
+ logger.error(f"HTTP error: {e}")
151
+ last_exception = f"HTTP {e.response.status_code}"
152
+ break # Don't retry HTTP errors
153
+
154
+ except Exception as e:
155
+ logger.error(f"Request failed: {e}")
156
+ last_exception = str(e)
157
+ break # Don't retry other errors
158
+
159
+ raise SunoAPIError(f"API request failed after {max_retries} attempts: {last_exception}")
160
+
161
+ def generate_suno_music(
162
+ prompt: str,
163
+ title: str,
164
+ style: str,
165
+ upload_url_type: str,
166
+ custom_upload_url: str,
167
+ instrumental: bool = True,
168
+ model: str = "V4_5ALL",
169
+ persona_id: Optional[str] = None,
170
+ negative_tags: Optional[str] = None,
171
+ vocal_gender: str = "m",
172
+ style_weight: float = 0.65,
173
+ weirdness_constraint: float = 0.65,
174
+ audio_weight: float = 0.65,
175
+ custom_mode: bool = True
176
+ ) -> ApiResponse:
177
+ """
178
+ Generate music using Suno API with fixed callback URL
179
+ """
180
+ logger.info(f"Starting music generation: {title}")
181
+
182
+ # Check if API key is available
183
+ if not config.suno_api_key:
184
+ error_msg = "Suno API key not found. Please set the 'SUNO_API_KEY' or 'SunoKey' environment variable."
185
+ logger.error(error_msg)
186
+ return ApiResponse(
187
+ success=False,
188
+ message=f"❌ {error_msg}",
189
+ error_type="ConfigurationError"
190
+ )
191
+
192
+ # Validate inputs
193
+ validation_errors = validate_inputs(prompt, title, style, upload_url_type, custom_upload_url)
194
+ if validation_errors:
195
+ error_msg = "Input validation failed:\n" + "\n".join([f"• {err}" for err in validation_errors])
196
+ logger.warning(f"Validation errors: {validation_errors}")
197
+ return ApiResponse(
198
+ success=False,
199
+ message=f"❌ {error_msg}",
200
+ error_type="ValidationError"
201
+ )
202
+
203
+ # Determine upload URL
204
+ if upload_url_type == "auto":
205
+ upload_url = generate_temp_upload_url()
206
+ logger.info(f"Using auto-generated upload URL: {upload_url}")
207
+ else:
208
+ upload_url = custom_upload_url.strip()
209
+ logger.info(f"Using custom upload URL: {upload_url}")
210
+
211
+ # Prepare payload
212
+ payload = {
213
+ "uploadUrl": upload_url,
214
+ "customMode": custom_mode,
215
+ "instrumental": instrumental,
216
+ "model": model,
217
+ "callBackUrl": config.fixed_callback_url, # Always use fixed callback URL
218
+ "prompt": prompt,
219
+ "style": style,
220
+ "title": title,
221
+ "personaId": persona_id or "",
222
+ "negativeTags": negative_tags or "",
223
+ "vocalGender": vocal_gender,
224
+ "styleWeight": style_weight,
225
+ "weirdnessConstraint": weirdness_constraint,
226
+ "audioWeight": audio_weight
227
+ }
228
+
229
+ # Remove empty fields
230
+ payload = {k: v for k, v in payload.items() if v not in ["", None]}
231
+
232
+ # Prepare headers
233
+ headers = {
234
+ "Authorization": f"Bearer {config.suno_api_key}",
235
+ "Content-Type": "application/json"
236
+ }
237
+
238
+ try:
239
+ # Make API request
240
+ response = make_api_request_with_retry(payload, headers, config.max_retries)
241
+ result = response.json()
242
+
243
+ # Check API response
244
+ if response.status_code == 200 and result.get("code") == 200:
245
+ task_id = result.get("data", {}).get("taskId", "Unknown")
246
+ logger.info(f"Success! Task ID: {task_id}")
247
+
248
+ return ApiResponse(
249
+ success=True,
250
+ message="✅ Music generation started successfully!",
251
+ task_id=task_id,
252
+ upload_url=upload_url,
253
+ callback_url=config.fixed_callback_url,
254
+ raw_response=result
255
+ )
256
+ else:
257
+ error_msg = result.get('msg', 'Unknown API error')
258
+ logger.error(f"API error: {error_msg}")
259
+ return ApiResponse(
260
+ success=False,
261
+ message=f"❌ API Error: {error_msg}",
262
+ raw_response=result,
263
+ error_type="APIError"
264
+ )
265
+
266
+ except SunoAPIError as e:
267
+ logger.error(f"API request failed: {e}")
268
+ return ApiResponse(
269
+ success=False,
270
+ message=f"❌ Request failed after retries: {str(e)}",
271
+ error_type="RequestError"
272
+ )
273
+
274
+ except requests.exceptions.HTTPError as e:
275
+ logger.error(f"HTTP error: {e}")
276
+ return ApiResponse(
277
+ success=False,
278
+ message=f"❌ HTTP Error {e.response.status_code}: {e.response.text}",
279
+ error_type="HTTPError"
280
+ )
281
+
282
+ except json.JSONDecodeError as e:
283
+ logger.error(f"JSON decode error: {e}")
284
+ return ApiResponse(
285
+ success=False,
286
+ message=f"❌ Failed to parse API response: {str(e)}",
287
+ error_type="ParseError"
288
+ )
289
+
290
+ except Exception as e:
291
+ logger.error(f"Unexpected error: {e}")
292
+ return ApiResponse(
293
+ success=False,
294
+ message=f"❌ Unexpected error: {str(e)}",
295
+ error_type="UnexpectedError"
296
+ )
297
+
298
+ def format_api_response(response: ApiResponse) -> str:
299
+ """Format API response for display"""
300
+ if response.success:
301
+ return f"""{response.message}
302
+
303
+ 📋 Task Details:
304
+ • Task ID: {response.task_id}
305
+ • Upload URL: {response.upload_url}
306
+ • Callback URL: {response.callback_url}
307
+
308
+ ✅ Your music is being generated!
309
+ The callback will be sent to: {config.fixed_callback_url}
310
+
311
+ 📱 You can check the playlist at: https://1hit.no/cover/playlist.php
312
+ (Newest songs appear first)
313
+
314
+ 🔍 Full API Response:
315
+ {json.dumps(response.raw_response, indent=2) if response.raw_response else 'No response data'}"""
316
+ else:
317
+ error_details = f"\n\n🔍 Error Type: {response.error_type}" if response.error_type else ""
318
+ raw_response = f"\n\n📋 Full Response:\n{json.dumps(response.raw_response, indent=2)}" if response.raw_response else ""
319
+ return f"{response.message}{error_details}{raw_response}"
320
+
321
+ def test_api_connection() -> str:
322
+ """Test API connection and key validity"""
323
+ if not config.suno_api_key:
324
+ return "❌ API Key: Not found"
325
+
326
+ try:
327
+ # Simple test endpoint if available, or use a minimal payload
328
+ test_payload = {
329
+ "uploadUrl": "https://example.com/test.mp3",
330
+ "callBackUrl": config.fixed_callback_url,
331
+ "prompt": "test",
332
+ "title": "test",
333
+ "style": "test",
334
+ "customMode": False,
335
+ "instrumental": True
336
+ }
337
+
338
+ headers = {
339
+ "Authorization": f"Bearer {config.suno_api_key}",
340
+ "Content-Type": "application/json"
341
+ }
342
+
343
+ # Use HEAD request or minimal endpoint if available
344
+ response = session.head(
345
+ config.suno_api_url.replace("/upload-cover", ""),
346
+ headers={"Authorization": f"Bearer {config.suno_api_key}"},
347
+ timeout=10
348
+ )
349
+
350
+ if response.status_code < 500:
351
+ return f"✅ API Connection: OK (Key is configured)"
352
+ else:
353
+ return f"⚠️ API Connection: May require valid payload (Status: {response.status_code})"
354
+
355
+ except Exception as e:
356
+ return f"❌ API Connection: Failed - {str(e)}"
357
+
358
+ def load_example() -> Dict:
359
+ """Load example values"""
360
+ return {
361
+ "prompt": config.defaults["prompt"],
362
+ "title": config.defaults["title"],
363
+ "style": config.defaults["style"],
364
+ "upload_url_type": "auto",
365
+ "custom_upload_url": "https://storage.example.com/upload",
366
+ "instrumental": config.defaults["instrumental"],
367
+ "model": config.default_model,
368
+ "persona_id": "",
369
+ "negative_tags": "Heavy Metal, Upbeat Drums",
370
+ "vocal_gender": config.default_vocal_gender,
371
+ "style_weight": config.defaults["style_weight"],
372
+ "weirdness_constraint": config.defaults["weirdness_constraint"],
373
+ "audio_weight": config.defaults["audio_weight"],
374
+ "custom_mode": config.defaults["custom_mode"]
375
+ }
376
+
377
+ # Create Gradio interface
378
+ with gr.Blocks(
379
+ title="Suno Music Generator",
380
+ theme=gr.themes.Soft(),
381
+ css="""
382
+ .success-box { background: #d4edda; border: 1px solid #c3e6cb; border-radius: 5px; padding: 10px; margin: 10px 0; }
383
+ .error-box { background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 5px; padding: 10px; margin: 10px 0; }
384
+ .info-box { background: #d1ecf1; border: 1px solid #bee5eb; border-radius: 5px; padding: 10px; margin: 10px 0; }
385
+ """
386
+ ) as app:
387
+
388
+ # Custom CSS for better UI
389
+ gr.HTML("""
390
+ <style>
391
+ .gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
392
+ .success-message { color: #155724; background: #d4edda; padding: 15px; border-radius: 5px; border-left: 4px solid #28a745; margin: 10px 0; }
393
+ .error-message { color: #721c24; background: #f8d7da; padding: 15px; border-radius: 5px; border-left: 4px solid #dc3545; margin: 10px 0; }
394
+ .warning-message { color: #856404; background: #fff3cd; padding: 15px; border-radius: 5px; border-left: 4px solid #ffc107; margin: 10px 0; }
395
+ .info-box { background: #e7f3ff; border: 1px solid #b3d7ff; border-radius: 8px; padding: 15px; margin: 15px 0; }
396
+ .playlist-link { background: #007bff; color: white; padding: 8px 16px; border-radius: 4px; text-decoration: none; display: inline-block; margin: 5px; }
397
+ .playlist-link:hover { background: #0056b3; color: white; text-decoration: none; }
398
+ </style>
399
+ """)
400
+
401
+ gr.Markdown("# 🎵 Suno Music Generator")
402
+ gr.Markdown(f"Generate music using Suno API. Callbacks sent to your PHP endpoint at `{config.fixed_callback_url}`")
403
+
404
+ # Status Bar
405
+ with gr.Row():
406
+ with gr.Column(scale=1):
407
+ api_status = gr.HTML(
408
+ value=f"""
409
+ <div class="info-box">
410
+ <h4 style="margin-top: 0;">🔧 API Status</h4>
411
+ <p><strong>API Key:</strong> {'<span style="color: green;">✅ Loaded</span>' if config.suno_api_key else '<span style="color: red;">❌ Not found</span>'}</p>
412
+ <p><strong>Callback URL:</strong> <code>{config.fixed_callback_url}</code></p>
413
+ <a href="https://1hit.no/cover/playlist.php" target="_blank" class="playlist-link">🎧 View Playlist</a>
414
+ </div>
415
+ """
416
+ )
417
+
418
+ # Test API button
419
+ test_result = gr.Textbox(label="Connection Test", interactive=False, visible=False)
420
+
421
+ def run_test():
422
+ return test_api_connection()
423
+
424
+ test_btn = gr.Button("🔍 Test API Connection", variant="secondary")
425
+ test_btn.click(run_test, outputs=test_result)
426
+
427
+ with gr.Column(scale=2):
428
+ gr.Markdown(f"""
429
+ ### 📋 About This App
430
+
431
+ This app generates music using the Suno API and sends callbacks to your PHP endpoint.
432
+
433
+ **How it works:**
434
+ 1. Configure your settings below
435
+ 2. Click "Generate Music"
436
+ 3. Suno processes your request
437
+ 4. Callback is sent to: `{config.fixed_callback_url}`
438
+ 5. Music appears in your playlist
439
+
440
+ **Playlist:** [https://1hit.no/cover/playlist.php](https://1hit.no/cover/playlist.php)
441
+
442
+ **Required setup:**
443
+ - Set your Suno API key as environment variable: `SUNO_API_KEY`
444
+ - For Hugging Face Spaces: Add as Repository Secret
445
+ - For local: Create `.env` file with `SUNO_API_KEY=your_key_here`
446
+ """)
447
+
448
+ # Main Tabs
449
+ with gr.Tabs():
450
+ # Basic Settings Tab
451
+ with gr.TabItem("🎯 Basic Settings"):
452
+ with gr.Row():
453
+ with gr.Column():
454
+ prompt = gr.Textbox(
455
+ label="Music Prompt",
456
+ value=config.defaults["prompt"],
457
+ placeholder="Describe the music you want to generate...",
458
+ lines=3,
459
+ info="Be descriptive for better results"
460
+ )
461
+ title = gr.Textbox(
462
+ label="Title",
463
+ value=config.defaults["title"],
464
+ placeholder="Title for your music track"
465
+ )
466
+ style = gr.Textbox(
467
+ label="Style",
468
+ value=config.defaults["style"],
469
+ placeholder="Music style (e.g., Classical, Pop, Rock, Jazz)",
470
+ info="Examples: Lo-fi, Synthwave, Orchestral, Hip Hop"
471
+ )
472
+
473
+ with gr.Column():
474
+ gr.Markdown("### 📤 Upload Settings")
475
+
476
+ upload_url_type = gr.Radio(
477
+ label="Upload URL Type",
478
+ choices=[
479
+ ("Auto-generate temporary URL", "auto"),
480
+ ("Use custom URL", "custom")
481
+ ],
482
+ value="auto",
483
+ info="Auto-generate creates a temporary URL, or provide your own storage URL"
484
+ )
485
+
486
+ custom_upload_url = gr.Textbox(
487
+ label="Custom Upload URL",
488
+ value="https://storage.example.com/upload",
489
+ placeholder="Enter your custom upload URL here",
490
+ visible=False,
491
+ info="Must be a publicly accessible URL where audio can be uploaded"
492
+ )
493
+
494
+ # Show/hide custom URL field
495
+ upload_url_type.change(
496
+ lambda x: gr.update(visible=x == "custom"),
497
+ inputs=upload_url_type,
498
+ outputs=custom_upload_url
499
+ )
500
+
501
+ gr.Markdown("### 🔗 Callback URL (Fixed)")
502
+ gr.Textbox(
503
+ value=config.fixed_callback_url,
504
+ interactive=False,
505
+ label=""
506
+ )
507
+
508
+ gr.HTML(f"""
509
+ <div class="info-box">
510
+ <strong>🎧 Your Playlist:</strong>
511
+ <p>Generated songs will appear here:</p>
512
+ <a href="https://1hit.no/cover/playlist.php" target="_blank" class="playlist-link">
513
+ 🎵 Open Playlist
514
+ </a>
515
+ </div>
516
+ """)
517
+
518
+ # Advanced Settings Tab
519
+ with gr.TabItem("⚙️ Advanced Settings"):
520
+ with gr.Row():
521
+ with gr.Column():
522
+ model = gr.Dropdown(
523
+ label="Model",
524
+ choices=["V5", "V4_5ALL", "V4", "V3", "V2"],
525
+ value=config.default_model,
526
+ info="Latest models generally produce better results"
527
+ )
528
+ instrumental = gr.Checkbox(
529
+ label="Instrumental (No Vocals)",
530
+ value=config.defaults["instrumental"],
531
+ info="Check for instrumental music only"
532
+ )
533
+ custom_mode = gr.Checkbox(
534
+ label="Custom Mode",
535
+ value=config.defaults["custom_mode"],
536
+ info="Enable for more control over generation"
537
+ )
538
+
539
+ with gr.Column():
540
+ persona_id = gr.Textbox(
541
+ label="Persona ID (Optional)",
542
+ placeholder="Leave empty for no persona",
543
+ info="Specific vocal persona ID if needed"
544
+ )
545
+ negative_tags = gr.Textbox(
546
+ label="Negative Tags (Optional)",
547
+ placeholder="Heavy Metal, Upbeat Drums, Distortion",
548
+ info="Tags to avoid in the music, comma-separated"
549
+ )
550
+ vocal_gender = gr.Dropdown(
551
+ label="Vocal Gender",
552
+ choices=["m", "f", "none"],
553
+ value=config.default_vocal_gender,
554
+ info="Gender of vocalist, or 'none' for instrumental"
555
+ )
556
+
557
+ # Weight Settings Tab
558
+ with gr.TabItem("🎛️ Weight Settings"):
559
+ with gr.Row():
560
+ with gr.Column():
561
+ style_weight = gr.Slider(
562
+ label="Style Weight",
563
+ minimum=0.0,
564
+ maximum=1.0,
565
+ value=config.defaults["style_weight"],
566
+ step=0.05,
567
+ info="How much to follow the specified style"
568
+ )
569
+ weirdness_constraint = gr.Slider(
570
+ label="Weirdness Constraint",
571
+ minimum=0.0,
572
+ maximum=1.0,
573
+ value=config.defaults["weirdness_constraint"],
574
+ step=0.05,
575
+ info="Lower values allow more experimental results"
576
+ )
577
+ audio_weight = gr.Slider(
578
+ label="Audio Weight",
579
+ minimum=0.0,
580
+ maximum=1.0,
581
+ value=config.defaults["audio_weight"],
582
+ step=0.05,
583
+ info="Overall audio quality emphasis"
584
+ )
585
+
586
+ with gr.Column():
587
+ gr.Markdown("### ℹ️ About Weights")
588
+ gr.Markdown("""
589
+ **Style Weight:** Controls how closely the output follows your specified style.
590
+
591
+ **Weirdness Constraint:** Lower values allow more creative/experimental outputs.
592
+
593
+ **Audio Weight:** Affects the overall audio quality and coherence.
594
+
595
+ **Default values (0.65)** are usually a good starting point.
596
+ """)
597
+
598
+ # Action Buttons
599
+ with gr.Row():
600
+ submit_btn = gr.Button("🎶 Generate Music", variant="primary", size="lg", scale=2)
601
+ example_btn = gr.Button("📋 Load Example", variant="secondary", scale=1)
602
+ clear_btn = gr.Button("🗑️ Clear", variant="secondary", scale=1)
603
+
604
+ # Output Section
605
+ with gr.Row():
606
+ with gr.Column():
607
+ output = gr.Textbox(
608
+ label="Generation Result",
609
+ lines=15,
610
+ interactive=False,
611
+ show_copy_button=True
612
+ )
613
+
614
+ # Progress indicator (hidden initially)
615
+ progress = gr.HTML("", visible=False)
616
+
617
+ # Success message area
618
+ success_html = gr.HTML("", visible=False)
619
+
620
+ # Footer
621
+ gr.Markdown("---")
622
+ gr.Markdown("""
623
+ ### 📝 Notes:
624
+ - **Callback URL is fixed** to your PHP endpoint
625
+ - **Processing time** varies based on length and complexity (usually 1-5 minutes)
626
+ - **Check your playlist** for new songs: [1hit.no/cover/playlist.php](https://1hit.no/cover/playlist.php)
627
+ - **Make sure** you have a valid Suno API key configured
628
+ - **Logs** are saved to `suno_generator.log`
629
+ """)
630
+
631
+ # Callback functions
632
+ def on_generate_click(
633
+ prompt, title, style, upload_url_type, custom_upload_url,
634
+ instrumental, model, persona_id, negative_tags,
635
+ vocal_gender, style_weight, weirdness_constraint,
636
+ audio_weight, custom_mode
637
+ ):
638
+ """Handle generate button click"""
639
+ # Show progress
640
+ progress_html = """
641
+ <div class="info-box">
642
+ <h4>⏳ Processing Request...</h4>
643
+ <p>Sending request to Suno API...</p>
644
+ <div style="background: #e9ecef; height: 4px; border-radius: 2px; margin: 10px 0;">
645
+ <div style="background: #007bff; width: 50%; height: 100%; border-radius: 2px; animation: pulse 1.5s infinite;"></div>
646
+ </div>
647
+ <p><small>This may take a moment. Do not close the window.</small></p>
648
+ </div>
649
+ """
650
+
651
+ yield progress_html, "", ""
652
+
653
+ # Make API call
654
+ response = generate_suno_music(
655
+ prompt, title, style, upload_url_type, custom_upload_url,
656
+ instrumental, model, persona_id, negative_tags,
657
+ vocal_gender, style_weight, weirdness_constraint,
658
+ audio_weight, custom_mode
659
+ )
660
+
661
+ # Format output
662
+ output_text = format_api_response(response)
663
+
664
+ # Prepare success message
665
+ if response.success:
666
+ success_message = f"""
667
+ <div class="success-message">
668
+ <h4>✅ Generation Started Successfully!</h4>
669
+ <p><strong>Task ID:</strong> {response.task_id}</p>
670
+ <p><strong>Check your playlist:</strong></p>
671
+ <a href="https://1hit.no/cover/playlist.php" target="_blank" class="playlist-link">
672
+ 🎵 Open Playlist (Newest songs first)
673
+ </a>
674
+ <p><small>Your music will appear here when processing is complete.</small></p>
675
+ </div>
676
+ """
677
+ else:
678
+ success_message = f"""
679
+ <div class="error-message">
680
+ <h4>❌ Generation Failed</h4>
681
+ <p>Please check the error details below and try again.</p>
682
+ </div>
683
+ """
684
+
685
+ yield "", output_text, success_message if response.success else ""
686
+
687
+ def clear_all():
688
+ """Clear all inputs and outputs"""
689
+ defaults = load_example()
690
+ return {
691
+ prompt: defaults["prompt"],
692
+ title: defaults["title"],
693
+ style: defaults["style"],
694
+ upload_url_type: "auto",
695
+ custom_upload_url: defaults["custom_upload_url"],
696
+ instrumental: defaults["instrumental"],
697
+ model: defaults["model"],
698
+ persona_id: "",
699
+ negative_tags: defaults["negative_tags"],
700
+ vocal_gender: defaults["vocal_gender"],
701
+ style_weight: defaults["style_weight"],
702
+ weirdness_constraint: defaults["weirdness_constraint"],
703
+ audio_weight: defaults["audio_weight"],
704
+ custom_mode: defaults["custom_mode"],
705
+ output: "",
706
+ progress: "",
707
+ success_html: ""
708
+ }
709
+
710
+ # Connect buttons
711
+ submit_btn.click(
712
+ fn=on_generate_click,
713
+ inputs=[
714
+ prompt, title, style, upload_url_type, custom_upload_url,
715
+ instrumental, model, persona_id, negative_tags,
716
+ vocal_gender, style_weight, weirdness_constraint,
717
+ audio_weight, custom_mode
718
+ ],
719
+ outputs=[progress, output, success_html]
720
+ )
721
+
722
+ example_btn.click(
723
+ fn=load_example,
724
+ outputs=[
725
+ prompt, title, style, upload_url_type, custom_upload_url,
726
+ instrumental, model, persona_id, negative_tags,
727
+ vocal_gender, style_weight, weirdness_constraint,
728
+ audio_weight, custom_mode
729
+ ]
730
+ )
731
+
732
+ clear_btn.click(
733
+ fn=clear_all,
734
+ outputs=[
735
+ prompt, title, style, upload_url_type, custom_upload_url,
736
+ instrumental, model, persona_id, negative_tags,
737
+ vocal_gender, style_weight, weirdness_constraint,
738
+ audio_weight, custom_mode, output, progress, success_html
739
+ ]
740
+ )
741
+
742
+ # Initialize with example
743
+ app.load(load_example, outputs=[
744
+ prompt, title, style, upload_url_type, custom_upload_url,
745
+ instrumental, model, persona_id, negative_tags,
746
+ vocal_gender, style_weight, weirdness_constraint,
747
+ audio_weight, custom_mode
748
+ ])
749
+
750
+ # Launch the app
751
+ if __name__ == "__main__":
752
+ # Check if API key is available
753
+ if not config.suno_api_key:
754
+ print("⚠️ Warning: Suno API key not found.")
755
+ print("Please set the 'SUNO_API_KEY' environment variable:")
756
+ print(" - For Hugging Face Spaces: Add as Repository Secret")
757
+ print(" - For local development: Create a .env file with SUNO_API_KEY=your_key")
758
+ print(" - Or set it directly: export SUNO_API_KEY=your_key")
759
+ print("\nCurrent environment variables:")
760
+ for key, value in os.environ.items():
761
+ if 'SUNO' in key.upper() or 'API' in key.upper():
762
+ print(f" {key}: {'*' * len(value) if 'KEY' in key.upper() else value}")
763
+
764
+ # Launch settings
765
+ launch_config = {
766
+ "server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
767
+ "server_port": int(os.environ.get("GRADIO_SERVER_PORT", 7860)),
768
+ "share": os.environ.get("GRADIO_SHARE", "False").lower() == "true",
769
+ }
770
+
771
+ # Add analytics if needed
772
+ if os.environ.get("GRADIO_ANALYTICS_ENABLED", "").lower() == "true":
773
+ launch_config["analytics_enabled"] = True
774
+
775
+ print(f"\n🚀 Starting Suno Music Generator...")
776
+ print(f"🌐 Local URL: http://localhost:{launch_config['server_port']}")
777
+ print(f"🎯 Callback URL: {config.fixed_callback_url}")
778
+ print(f"🎧 Playlist: https://1hit.no/cover/playlist.php")
779
+ print(f"📝 Logs: suno_generator.log")
780
+
781
+ app.launch(**launch_config)