Spaces:
Running
Running
| import os | |
| import requests | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| import json | |
| import uuid | |
| import tempfile | |
| from datetime import datetime | |
| import logging | |
| import sys | |
| from typing import Dict, Any, Optional, Union, List, Tuple | |
| from dataclasses import dataclass | |
| import time | |
| import mimetypes | |
| import shutil | |
| from pathlib import Path | |
| # Load environment variables from .env file | |
| load_dotenv() | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
| handlers=[ | |
| logging.StreamHandler(sys.stdout), | |
| logging.FileHandler('suno_generator.log') | |
| ] | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Configuration | |
| class Config: | |
| """Configuration manager""" | |
| def __init__(self): | |
| self.suno_api_key = os.environ.get("SUNO_API_KEY") or os.environ.get("SunoKey") | |
| self.suno_api_url = "https://api.sunoapi.org/api/v1/generate/upload-cover" | |
| # Your PHP callback URL - kept private in app | |
| self.php_callback_url = "https://1hit.no/cover/cb.php" # Private, not exposed | |
| # Temporary server for uploaded files (simulated or real) | |
| self.temp_server_url = os.environ.get("TEMP_SERVER_URL", "https://temp.yourdomain.com") | |
| # Local storage for uploaded files (for simulation) | |
| self.upload_dir = Path("uploads") | |
| self.upload_dir.mkdir(exist_ok=True) | |
| # Public facing URL (what users see) | |
| self.public_callback_info = "Your callback URL (configured in app backend)" | |
| # Default values | |
| self.default_model = "V4_5ALL" | |
| self.default_vocal_gender = "m" | |
| self.max_retries = 3 | |
| self.request_timeout = 30 | |
| self.max_file_size_mb = 50 # Maximum file size for uploads | |
| # Default values for UI | |
| self.defaults = { | |
| "prompt": "A calm and relaxing piano track with soft melodies", | |
| "title": "Peaceful Piano Meditation", | |
| "style": "Classical", | |
| "style_weight": 0.65, | |
| "weirdness_constraint": 0.65, | |
| "audio_weight": 0.65, | |
| "instrumental": True, | |
| "custom_mode": True | |
| } | |
| config = Config() | |
| # Data models | |
| class UploadedFile: | |
| """Information about uploaded file""" | |
| filename: str | |
| temp_url: str | |
| filepath: Path | |
| size: int | |
| mime_type: str | |
| upload_time: datetime | |
| class ApiResponse: | |
| """Structured API response""" | |
| success: bool | |
| message: str | |
| task_id: Optional[str] = None | |
| upload_url: Optional[str] = None | |
| callback_url: Optional[str] = None | |
| raw_response: Optional[Dict] = None | |
| error_type: Optional[str] = None | |
| class SunoAPIError(Exception): | |
| """Custom exception for Suno API errors""" | |
| pass | |
| class FileUploadError(Exception): | |
| """Custom exception for file upload errors""" | |
| pass | |
| # Session management for requests | |
| session = requests.Session() | |
| session.headers.update({ | |
| "User-Agent": "SunoMusicGenerator/1.0", | |
| "Accept": "application/json" | |
| }) | |
| def validate_uploaded_file(file_path: str) -> Tuple[bool, str]: | |
| """Validate uploaded file""" | |
| try: | |
| # Check if file exists | |
| if not os.path.exists(file_path): | |
| return False, "File does not exist" | |
| # Check file size | |
| file_size = os.path.getsize(file_path) | |
| max_size = config.max_file_size_mb * 1024 * 1024 | |
| if file_size > max_size: | |
| return False, f"File too large (max {config.max_file_size_mb}MB)" | |
| # Check file type | |
| mime_type, _ = mimetypes.guess_type(file_path) | |
| if mime_type not in ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav']: | |
| return False, "Only MP3 and WAV files are supported" | |
| return True, "File validated successfully" | |
| except Exception as e: | |
| return False, f"Validation error: {str(e)}" | |
| def upload_file_to_temp_server(file_path: str, original_filename: str) -> UploadedFile: | |
| """ | |
| Simulate uploading a file to a temporary server. | |
| In production, you would upload to S3, Cloud Storage, or your own server. | |
| """ | |
| try: | |
| # Create a unique filename | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| unique_id = str(uuid.uuid4())[:8] | |
| extension = Path(original_filename).suffix or '.mp3' | |
| new_filename = f"{timestamp}_{unique_id}{extension}" | |
| # Copy file to upload directory (simulating upload) | |
| dest_path = config.upload_dir / new_filename | |
| shutil.copy2(file_path, dest_path) | |
| # Generate a temporary URL (simulated) | |
| # In production, this would be a real URL to your storage service | |
| temp_url = f"{config.temp_server_url}/uploads/{new_filename}" | |
| # Get file info | |
| file_size = os.path.getsize(dest_path) | |
| mime_type, _ = mimetypes.guess_type(str(dest_path)) | |
| logger.info(f"File uploaded: {original_filename} -> {temp_url} ({file_size} bytes)") | |
| return UploadedFile( | |
| filename=new_filename, | |
| temp_url=temp_url, | |
| filepath=dest_path, | |
| size=file_size, | |
| mime_type=mime_type or 'audio/mpeg', | |
| upload_time=datetime.now() | |
| ) | |
| except Exception as e: | |
| logger.error(f"File upload failed: {e}") | |
| raise FileUploadError(f"Failed to upload file: {str(e)}") | |
| def cleanup_old_uploads(max_age_hours: int = 24): | |
| """Clean up old uploaded files""" | |
| try: | |
| cutoff_time = datetime.now().timestamp() - (max_age_hours * 3600) | |
| for file_path in config.upload_dir.glob("*"): | |
| if file_path.is_file(): | |
| if file_path.stat().st_mtime < cutoff_time: | |
| file_path.unlink() | |
| logger.info(f"Cleaned up old file: {file_path.name}") | |
| except Exception as e: | |
| logger.warning(f"Cleanup failed: {e}") | |
| def validate_inputs( | |
| prompt: str, | |
| title: str, | |
| style: str, | |
| use_reference_audio: bool, | |
| reference_audio_path: Optional[str] | |
| ) -> List[str]: | |
| """Validate user inputs and return list of errors""" | |
| errors = [] | |
| # Trim inputs | |
| prompt = prompt.strip() | |
| title = title.strip() | |
| style = style.strip() | |
| # Check required fields | |
| if len(prompt) < 5: | |
| errors.append("Prompt should be at least 5 characters") | |
| if not title: | |
| errors.append("Title is required") | |
| if not style: | |
| errors.append("Style is required") | |
| # Check reference audio if enabled | |
| if use_reference_audio and reference_audio_path: | |
| is_valid, error_msg = validate_uploaded_file(reference_audio_path) | |
| if not is_valid: | |
| errors.append(f"Reference audio: {error_msg}") | |
| return errors | |
| def make_api_request_with_retry( | |
| payload: Dict[str, Any], | |
| headers: Dict[str, str], | |
| max_retries: int = 3 | |
| ) -> requests.Response: | |
| """Make API request with retry logic""" | |
| last_exception = None | |
| for attempt in range(max_retries): | |
| try: | |
| logger.info(f"API Request Attempt {attempt + 1}/{max_retries}") | |
| response = session.post( | |
| config.suno_api_url, | |
| json=payload, | |
| headers=headers, | |
| timeout=config.request_timeout | |
| ) | |
| response.raise_for_status() | |
| return response | |
| except requests.exceptions.Timeout: | |
| logger.warning(f"Request timeout (attempt {attempt + 1})") | |
| last_exception = "Timeout" | |
| if attempt < max_retries - 1: | |
| time.sleep(2 ** attempt) # Exponential backoff | |
| except requests.exceptions.ConnectionError: | |
| logger.warning(f"Connection error (attempt {attempt + 1})") | |
| last_exception = "Connection error" | |
| if attempt < max_retries - 1: | |
| time.sleep(2 ** attempt) | |
| except requests.exceptions.HTTPError as e: | |
| logger.error(f"HTTP error: {e}") | |
| last_exception = f"HTTP {e.response.status_code}" | |
| break # Don't retry HTTP errors | |
| except Exception as e: | |
| logger.error(f"Request failed: {e}") | |
| last_exception = str(e) | |
| break # Don't retry other errors | |
| raise SunoAPIError(f"API request failed after {max_retries} attempts: {last_exception}") | |
| def generate_suno_music( | |
| prompt: str, | |
| title: str, | |
| style: str, | |
| use_reference_audio: bool, | |
| reference_audio_path: Optional[str], | |
| instrumental: bool = True, | |
| model: str = "V4_5ALL", | |
| persona_id: Optional[str] = None, | |
| negative_tags: Optional[str] = None, | |
| vocal_gender: str = "m", | |
| style_weight: float = 0.65, | |
| weirdness_constraint: float = 0.65, | |
| audio_weight: float = 0.65, | |
| custom_mode: bool = True | |
| ) -> ApiResponse: | |
| """ | |
| Generate music using Suno API with uploaded reference audio | |
| """ | |
| logger.info(f"Starting music generation: {title}") | |
| # Check if API key is available | |
| if not config.suno_api_key: | |
| error_msg = "Suno API key not found. Please set the 'SUNO_API_KEY' or 'SunoKey' environment variable." | |
| logger.error(error_msg) | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ {error_msg}", | |
| error_type="ConfigurationError" | |
| ) | |
| # Validate inputs | |
| validation_errors = validate_inputs(prompt, title, style, use_reference_audio, reference_audio_path) | |
| if validation_errors: | |
| error_msg = "Input validation failed:\n" + "\n".join([f"• {err}" for err in validation_errors]) | |
| logger.warning(f"Validation errors: {validation_errors}") | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ {error_msg}", | |
| error_type="ValidationError" | |
| ) | |
| # Handle reference audio upload | |
| upload_url = None | |
| if use_reference_audio and reference_audio_path: | |
| try: | |
| original_filename = os.path.basename(reference_audio_path) | |
| uploaded_file = upload_file_to_temp_server(reference_audio_path, original_filename) | |
| upload_url = uploaded_file.temp_url | |
| logger.info(f"Using uploaded reference audio: {upload_url}") | |
| except FileUploadError as e: | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ Failed to upload reference audio: {str(e)}", | |
| error_type="UploadError" | |
| ) | |
| # Prepare payload | |
| payload = { | |
| "customMode": custom_mode, | |
| "instrumental": instrumental, | |
| "model": model, | |
| "callBackUrl": config.php_callback_url, # Private callback URL | |
| "prompt": prompt, | |
| "style": style, | |
| "title": title, | |
| "personaId": persona_id or "", | |
| "negativeTags": negative_tags or "", | |
| "vocalGender": vocal_gender, | |
| "styleWeight": style_weight, | |
| "weirdnessConstraint": weirdness_constraint, | |
| "audioWeight": audio_weight | |
| } | |
| # Add upload URL if provided | |
| if upload_url: | |
| payload["uploadUrl"] = upload_url | |
| # Remove empty fields | |
| payload = {k: v for k, v in payload.items() if v not in ["", None]} | |
| # Prepare headers | |
| headers = { | |
| "Authorization": f"Bearer {config.suno_api_key}", | |
| "Content-Type": "application/json" | |
| } | |
| try: | |
| # Make API request | |
| response = make_api_request_with_retry(payload, headers, config.max_retries) | |
| result = response.json() | |
| # Check API response | |
| if response.status_code == 200 and result.get("code") == 200: | |
| task_id = result.get("data", {}).get("taskId", "Unknown") | |
| logger.info(f"Success! Task ID: {task_id}") | |
| return ApiResponse( | |
| success=True, | |
| message="✅ Music generation started successfully!", | |
| task_id=task_id, | |
| upload_url=upload_url, | |
| callback_url=config.php_callback_url, # Private URL, not shown to user | |
| raw_response=result | |
| ) | |
| else: | |
| error_msg = result.get('msg', 'Unknown API error') | |
| logger.error(f"API error: {error_msg}") | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ API Error: {error_msg}", | |
| raw_response=result, | |
| error_type="APIError" | |
| ) | |
| except SunoAPIError as e: | |
| logger.error(f"API request failed: {e}") | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ Request failed after retries: {str(e)}", | |
| error_type="RequestError" | |
| ) | |
| except requests.exceptions.HTTPError as e: | |
| logger.error(f"HTTP error: {e}") | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ HTTP Error {e.response.status_code}: {e.response.text}", | |
| error_type="HTTPError" | |
| ) | |
| except json.JSONDecodeError as e: | |
| logger.error(f"JSON decode error: {e}") | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ Failed to parse API response: {str(e)}", | |
| error_type="ParseError" | |
| ) | |
| except Exception as e: | |
| logger.error(f"Unexpected error: {e}") | |
| return ApiResponse( | |
| success=False, | |
| message=f"❌ Unexpected error: {str(e)}", | |
| error_type="UnexpectedError" | |
| ) | |
| def format_api_response(response: ApiResponse) -> str: | |
| """Format API response for display""" | |
| if response.success: | |
| # Don't expose the actual callback URL | |
| callback_display = "✓ Configured in app backend (private)" | |
| message = f"""{response.message} | |
| 📋 Task Details: | |
| • Task ID: {response.task_id} | |
| • Reference Audio: {response.upload_url or 'None (original generation)'} | |
| • Callback URL: {callback_display} | |
| ✅ Your music is being generated! | |
| The callback will be sent to our private endpoint. | |
| 🎧 Check your playlist for the result: | |
| https://1hit.no/cover/playlist.php | |
| 🔍 API Response Status: Success""" | |
| # Add raw response if needed for debugging | |
| if response.raw_response: | |
| message += f"\n\n📊 Raw Response (Task ID only):" | |
| # Only show task ID from raw response, not full URL | |
| safe_response = response.raw_response.copy() | |
| if 'data' in safe_response and 'callbackUrl' in safe_response['data']: | |
| safe_response['data']['callbackUrl'] = "[PRIVATE]" | |
| message += f"\n{json.dumps(safe_response, indent=2)}" | |
| return message | |
| else: | |
| error_details = f"\n\n🔍 Error Type: {response.error_type}" if response.error_type else "" | |
| return f"{response.message}{error_details}" | |
| def test_api_connection() -> str: | |
| """Test API connection and key validity""" | |
| if not config.suno_api_key: | |
| return "❌ API Key: Not found" | |
| try: | |
| # Simple test endpoint if available | |
| headers = { | |
| "Authorization": f"Bearer {config.suno_api_key}", | |
| } | |
| response = session.head( | |
| config.suno_api_url.replace("/upload-cover", ""), | |
| headers=headers, | |
| timeout=10 | |
| ) | |
| if response.status_code < 500: | |
| return f"✅ API Connection: OK" | |
| else: | |
| return f"⚠️ API Connection: May require valid payload (Status: {response.status_code})" | |
| except Exception as e: | |
| return f"❌ API Connection: Failed - {str(e)}" | |
| def load_example() -> Dict: | |
| """Load example values""" | |
| return { | |
| "prompt": config.defaults["prompt"], | |
| "title": config.defaults["title"], | |
| "style": config.defaults["style"], | |
| "use_reference_audio": False, | |
| "reference_audio": None, | |
| "instrumental": config.defaults["instrumental"], | |
| "model": config.default_model, | |
| "persona_id": "", | |
| "negative_tags": "Heavy Metal, Upbeat Drums", | |
| "vocal_gender": config.default_vocal_gender, | |
| "style_weight": config.defaults["style_weight"], | |
| "weirdness_constraint": config.defaults["weirdness_constraint"], | |
| "audio_weight": config.defaults["audio_weight"], | |
| "custom_mode": config.defaults["custom_mode"] | |
| } | |
| # Create Gradio interface | |
| with gr.Blocks( | |
| title="Suno Music Generator", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .success-box { background: #d4edda; border: 1px solid #c3e6cb; border-radius: 5px; padding: 10px; margin: 10px 0; } | |
| .error-box { background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 5px; padding: 10px; margin: 10px 0; } | |
| .info-box { background: #d1ecf1; border: 1px solid #bee5eb; border-radius: 5px; padding: 10px; margin: 10px 0; } | |
| .upload-info { background: #e7f3ff; border: 1px dashed #4a90e2; border-radius: 8px; padding: 15px; margin: 10px 0; } | |
| """ | |
| ) as app: | |
| # Custom CSS for better UI | |
| gr.HTML(""" | |
| <style> | |
| .gradio-container { max-width: 1200px !important; margin: 0 auto !important; } | |
| .success-message { color: #155724; background: #d4edda; padding: 15px; border-radius: 5px; border-left: 4px solid #28a745; margin: 10px 0; } | |
| .error-message { color: #721c24; background: #f8d7da; padding: 15px; border-radius: 5px; border-left: 4px solid #dc3545; margin: 10px 0; } | |
| .warning-message { color: #856404; background: #fff3cd; padding: 15px; border-radius: 5px; border-left: 4px solid #ffc107; margin: 10px 0; } | |
| .info-box { background: #e7f3ff; border: 1px solid #b3d7ff; border-radius: 8px; padding: 15px; margin: 15px 0; } | |
| .playlist-link { background: #007bff; color: white; padding: 8px 16px; border-radius: 4px; text-decoration: none; display: inline-block; margin: 5px; } | |
| .playlist-link:hover { background: #0056b3; color: white; text-decoration: none; } | |
| .file-info { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 10px; margin: 5px 0; } | |
| .privacy-note { background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 10px; margin: 10px 0; font-size: 0.9em; } | |
| </style> | |
| """) | |
| gr.Markdown("# 🎵 Suno Music Generator") | |
| gr.Markdown("Generate music using Suno API with optional reference audio upload") | |
| # Status Bar | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| api_status = gr.HTML( | |
| value=f""" | |
| <div class="info-box"> | |
| <h4 style="margin-top: 0;">🔧 API Status</h4> | |
| <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> | |
| <p><strong>File Uploads:</strong> <span style="color: green;">✅ Enabled</span></p> | |
| <p><strong>Max File Size:</strong> {config.max_file_size_mb}MB</p> | |
| <a href="https://1hit.no/cover/playlist.php" target="_blank" class="playlist-link">🎧 View Playlist</a> | |
| </div> | |
| """ | |
| ) | |
| # Test API button | |
| test_result = gr.Textbox(label="Connection Test", interactive=False, visible=False) | |
| test_btn = gr.Button("🔍 Test API Connection", variant="secondary") | |
| test_btn.click(test_api_connection, outputs=test_result) | |
| with gr.Column(scale=2): | |
| gr.Markdown(f""" | |
| ### 📋 About This App | |
| **Generate music with optional reference audio:** | |
| 1. Upload an MP3/WAV file (optional - for covers/remixes) | |
| 2. Configure your settings | |
| 3. Generate music | |
| 4. Check your playlist for results | |
| **🎧 Your Playlist:** [https://1hit.no/cover/playlist.php](https://1hit.no/cover/playlist.php) | |
| **🔒 Privacy Note:** | |
| - Callback URLs are kept private in the app backend | |
| - Uploaded files are temporarily stored | |
| - No sensitive URLs exposed to users | |
| **📁 Supported Files:** MP3, WAV (max {config.max_file_size_mb}MB) | |
| """) | |
| # Main Tabs | |
| with gr.Tabs(): | |
| # Basic Settings Tab | |
| with gr.TabItem("🎯 Basic Settings"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| prompt = gr.Textbox( | |
| label="Music Prompt", | |
| value=config.defaults["prompt"], | |
| placeholder="Describe the music you want to generate...", | |
| lines=3, | |
| info="Be descriptive for better results" | |
| ) | |
| title = gr.Textbox( | |
| label="Title", | |
| value=config.defaults["title"], | |
| placeholder="Title for your music track" | |
| ) | |
| style = gr.Textbox( | |
| label="Style", | |
| value=config.defaults["style"], | |
| placeholder="Music style (e.g., Classical, Pop, Rock, Jazz)", | |
| info="Examples: Lo-fi, Synthwave, Orchestral, Hip Hop" | |
| ) | |
| with gr.Column(): | |
| gr.Markdown("### 🎵 Reference Audio (Optional)") | |
| use_reference_audio = gr.Checkbox( | |
| label="Use reference audio (for covers/remixes)", | |
| value=False, | |
| info="Upload an MP3/WAV file to use as reference" | |
| ) | |
| reference_audio = gr.Audio( | |
| label="Upload Reference Audio", | |
| type="filepath", | |
| visible=False, | |
| info=f"MP3 or WAV file (max {config.max_file_size_mb}MB)" | |
| ) | |
| file_info = gr.HTML( | |
| value="<div class='upload-info'>No file uploaded</div>", | |
| visible=False | |
| ) | |
| # Show/hide audio upload based on checkbox | |
| def toggle_audio_upload(use_ref): | |
| return [ | |
| gr.update(visible=use_ref), # audio upload | |
| gr.update(visible=use_ref) # file info | |
| ] | |
| use_reference_audio.change( | |
| toggle_audio_upload, | |
| inputs=use_reference_audio, | |
| outputs=[reference_audio, file_info] | |
| ) | |
| # Update file info when file is uploaded | |
| def update_file_info(audio_path): | |
| if audio_path: | |
| try: | |
| file_size = os.path.getsize(audio_path) | |
| file_size_mb = file_size / (1024 * 1024) | |
| filename = os.path.basename(audio_path) | |
| return f""" | |
| <div class="upload-info"> | |
| <strong>📁 File Info:</strong> | |
| <div class="file-info"> | |
| <div><strong>Name:</strong> {filename}</div> | |
| <div><strong>Size:</strong> {file_size_mb:.2f} MB</div> | |
| <div><strong>Type:</strong> MP3/WAV Audio</div> | |
| <div><strong>Status:</strong> <span style="color: green;">Ready to use</span></div> | |
| </div> | |
| <small>This file will be uploaded and used as reference audio.</small> | |
| </div> | |
| """ | |
| except: | |
| return "<div class='upload-info'>Unable to read file info</div>" | |
| return "<div class='upload-info'>No file uploaded</div>" | |
| reference_audio.change( | |
| update_file_info, | |
| inputs=reference_audio, | |
| outputs=file_info | |
| ) | |
| gr.Markdown("### 🔒 Callback Information") | |
| gr.HTML(""" | |
| <div class="privacy-note"> | |
| <strong>🔐 Callback URL Protection:</strong> | |
| <p>Your callback URL is configured privately in the app backend and is not exposed to users.</p> | |
| <p>All generated music will appear in your playlist automatically.</p> | |
| </div> | |
| """) | |
| # Advanced Settings Tab | |
| with gr.TabItem("⚙️ Advanced Settings"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| model = gr.Dropdown( | |
| label="Model", | |
| choices=["V5", "V4_5ALL", "V4", "V3", "V2"], | |
| value=config.default_model, | |
| info="Latest models generally produce better results" | |
| ) | |
| instrumental = gr.Checkbox( | |
| label="Instrumental (No Vocals)", | |
| value=config.defaults["instrumental"], | |
| info="Check for instrumental music only" | |
| ) | |
| custom_mode = gr.Checkbox( | |
| label="Custom Mode", | |
| value=config.defaults["custom_mode"], | |
| info="Enable for more control over generation" | |
| ) | |
| with gr.Column(): | |
| persona_id = gr.Textbox( | |
| label="Persona ID (Optional)", | |
| placeholder="Leave empty for no persona", | |
| info="Specific vocal persona ID if needed" | |
| ) | |
| negative_tags = gr.Textbox( | |
| label="Negative Tags (Optional)", | |
| placeholder="Heavy Metal, Upbeat Drums, Distortion", | |
| info="Tags to avoid in the music, comma-separated" | |
| ) | |
| vocal_gender = gr.Dropdown( | |
| label="Vocal Gender", | |
| choices=["m", "f", "none"], | |
| value=config.default_vocal_gender, | |
| info="Gender of vocalist, or 'none' for instrumental" | |
| ) | |
| # Weight Settings Tab | |
| with gr.TabItem("🎛️ Weight Settings"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| style_weight = gr.Slider( | |
| label="Style Weight", | |
| minimum=0.0, | |
| maximum=1.0, | |
| value=config.defaults["style_weight"], | |
| step=0.05, | |
| info="How much to follow the specified style" | |
| ) | |
| weirdness_constraint = gr.Slider( | |
| label="Weirdness Constraint", | |
| minimum=0.0, | |
| maximum=1.0, | |
| value=config.defaults["weirdness_constraint"], | |
| step=0.05, | |
| info="Lower values allow more experimental results" | |
| ) | |
| audio_weight = gr.Slider( | |
| label="Audio Weight", | |
| minimum=0.0, | |
| maximum=1.0, | |
| value=config.defaults["audio_weight"], | |
| step=0.05, | |
| info="Overall audio quality emphasis" | |
| ) | |
| with gr.Column(): | |
| gr.Markdown("### ℹ️ About Weights") | |
| gr.Markdown(""" | |
| **Style Weight:** Controls how closely the output follows your specified style. | |
| **Weirdness Constraint:** Lower values allow more creative/experimental outputs. | |
| **Audio Weight:** Affects the overall audio quality and coherence. | |
| **Default values (0.65)** are usually a good starting point. | |
| **Note:** These only affect generation when Custom Mode is enabled. | |
| """) | |
| # Action Buttons | |
| with gr.Row(): | |
| submit_btn = gr.Button("🎶 Generate Music", variant="primary", size="lg", scale=2) | |
| example_btn = gr.Button("📋 Load Example", variant="secondary", scale=1) | |
| clear_btn = gr.Button("🗑️ Clear", variant="secondary", scale=1) | |
| # Output Section | |
| with gr.Row(): | |
| with gr.Column(): | |
| output = gr.Textbox( | |
| label="Generation Result", | |
| lines=15, | |
| interactive=False, | |
| show_copy_button=True | |
| ) | |
| # Progress indicator | |
| progress = gr.HTML("", visible=False) | |
| # Success message area | |
| success_html = gr.HTML("", visible=False) | |
| # Footer | |
| gr.Markdown("---") | |
| gr.Markdown(f""" | |
| ### 📝 Important Notes: | |
| **🔒 Privacy & Security:** | |
| - Callback URLs are kept private in app backend | |
| - Uploaded files are temporarily stored (auto-cleaned after 24h) | |
| - No sensitive information exposed to users | |
| **🎵 Reference Audio:** | |
| - Optional: Upload MP3/WAV files for covers/remixes | |
| - Max file size: {config.max_file_size_mb}MB | |
| - Files are uploaded to temporary storage | |
| **🔄 Processing:** | |
| - Generation time: 1-5 minutes typically | |
| - Results appear in your playlist automatically | |
| - Check: [1hit.no/cover/playlist.php](https://1hit.no/cover/playlist.php) | |
| **⚙️ Configuration:** | |
| - Set `SUNO_API_KEY` environment variable | |
| - Optional: Set `TEMP_SERVER_URL` for actual file storage | |
| - Logs: `suno_generator.log` | |
| """) | |
| # Callback functions | |
| def on_generate_click( | |
| prompt, title, style, use_reference_audio, reference_audio_path, | |
| instrumental, model, persona_id, negative_tags, | |
| vocal_gender, style_weight, weirdness_constraint, | |
| audio_weight, custom_mode | |
| ): | |
| """Handle generate button click""" | |
| # Show progress | |
| progress_html = """ | |
| <div class="info-box"> | |
| <h4>⏳ Processing Request...</h4> | |
| <div style="display: flex; align-items: center; gap: 10px;"> | |
| <div style="width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></div> | |
| <div> | |
| <p>Processing your request...</p> | |
| <div style="background: #e9ecef; height: 4px; border-radius: 2px; margin: 10px 0; width: 200px;"> | |
| <div style="background: #007bff; width: 60%; height: 100%; border-radius: 2px; animation: pulse 1.5s infinite;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <p><small>Uploading files and sending to Suno API...</small></p> | |
| </div> | |
| <style> | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } | |
| </style> | |
| """ | |
| yield progress_html, "", "" | |
| # Make API call | |
| response = generate_suno_music( | |
| prompt, title, style, use_reference_audio, reference_audio_path, | |
| instrumental, model, persona_id, negative_tags, | |
| vocal_gender, style_weight, weirdness_constraint, | |
| audio_weight, custom_mode | |
| ) | |
| # Format output | |
| output_text = format_api_response(response) | |
| # Prepare success message | |
| if response.success: | |
| success_message = f""" | |
| <div class="success-message"> | |
| <h4>✅ Generation Started Successfully!</h4> | |
| <p><strong>Task ID:</strong> {response.task_id}</p> | |
| <p><strong>Reference Audio:</strong> {"Uploaded ✓" if response.upload_url else "None (original generation)"}</p> | |
| <p><strong>Check your playlist:</strong></p> | |
| <a href="https://1hit.no/cover/playlist.php" target="_blank" class="playlist-link"> | |
| 🎵 Open Playlist (Newest songs first) | |
| </a> | |
| <p><small>Your music will appear here when processing is complete (usually 1-5 minutes).</small></p> | |
| </div> | |
| """ | |
| else: | |
| success_message = f""" | |
| <div class="error-message"> | |
| <h4>❌ Generation Failed</h4> | |
| <p>Please check the error details below and try again.</p> | |
| </div> | |
| """ | |
| yield "", output_text, success_message if response.success else "" | |
| def clear_all(): | |
| """Clear all inputs and outputs""" | |
| defaults = load_example() | |
| return { | |
| prompt: defaults["prompt"], | |
| title: defaults["title"], | |
| style: defaults["style"], | |
| use_reference_audio: False, | |
| reference_audio: None, | |
| instrumental: defaults["instrumental"], | |
| model: defaults["model"], | |
| persona_id: "", | |
| negative_tags: defaults["negative_tags"], | |
| vocal_gender: defaults["vocal_gender"], | |
| style_weight: defaults["style_weight"], | |
| weirdness_constraint: defaults["weirdness_constraint"], | |
| audio_weight: defaults["audio_weight"], | |
| custom_mode: defaults["custom_mode"], | |
| output: "", | |
| progress: "", | |
| success_html: "", | |
| file_info: "<div class='upload-info'>No file uploaded</div>" | |
| } | |
| # Connect buttons | |
| submit_btn.click( | |
| fn=on_generate_click, | |
| inputs=[ | |
| prompt, title, style, use_reference_audio, reference_audio, | |
| instrumental, model, persona_id, negative_tags, | |
| vocal_gender, style_weight, weirdness_constraint, | |
| audio_weight, custom_mode | |
| ], | |
| outputs=[progress, output, success_html] | |
| ) | |
| example_btn.click( | |
| fn=load_example, | |
| outputs=[ | |
| prompt, title, style, use_reference_audio, reference_audio, | |
| instrumental, model, persona_id, negative_tags, | |
| vocal_gender, style_weight, weirdness_constraint, | |
| audio_weight, custom_mode | |
| ] | |
| ) | |
| clear_btn.click( | |
| fn=clear_all, | |
| outputs=[ | |
| prompt, title, style, use_reference_audio, reference_audio, | |
| instrumental, model, persona_id, negative_tags, | |
| vocal_gender, style_weight, weirdness_constraint, | |
| audio_weight, custom_mode, output, progress, success_html | |
| ] | |
| ) | |
| # Initialize with example | |
| app.load(load_example, outputs=[ | |
| prompt, title, style, use_reference_audio, reference_audio, | |
| instrumental, model, persona_id, negative_tags, | |
| vocal_gender, style_weight, weirdness_constraint, | |
| audio_weight, custom_mode | |
| ]) | |
| # Launch the app | |
| if __name__ == "__main__": | |
| # Clean up old uploads on startup | |
| cleanup_old_uploads() | |
| # Check if API key is available | |
| if not config.suno_api_key: | |
| print("⚠️ Warning: Suno API key not found.") | |
| print("Please set the 'SUNO_API_KEY' environment variable:") | |
| print(" - For Hugging Face Spaces: Add as Repository Secret") | |
| print(" - For local development: Create a .env file with SUNO_API_KEY=your_key") | |
| print(" - Or set it directly: export SUNO_API_KEY=your_key") | |
| print("\nCurrent environment variables:") | |
| for key, value in os.environ.items(): | |
| if 'SUNO' in key.upper() or 'API' in key.upper(): | |
| print(f" {key}: {'*' * len(value) if 'KEY' in key.upper() else value}") | |
| # Launch settings | |
| launch_config = { | |
| "server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"), | |
| "server_port": int(os.environ.get("GRADIO_SERVER_PORT", 7860)), | |
| "share": os.environ.get("GRADIO_SHARE", "False").lower() == "true", | |
| } | |
| print(f"\n🚀 Starting Suno Music Generator...") | |
| print(f"🌐 Local URL: http://localhost:{launch_config['server_port']}") | |
| print(f"🎯 PHP Callback URL: {config.php_callback_url} (private)") | |
| print(f"📁 Upload Directory: {config.upload_dir.absolute()}") | |
| print(f"🎧 Playlist: https://1hit.no/cover/playlist.php") | |
| print(f"📝 Logs: suno_generator.log") | |
| print(f"🧹 Auto-cleanup: Uploads older than 24 hours") | |
| app.launch(**launch_config) |