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 from dataclasses import dataclass import time # 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" self.fixed_callback_url = "https://1hit.no/cover/cb.php" # Your PHP callback URL self.default_model = "V4_5ALL" self.default_vocal_gender = "m" self.max_retries = 3 self.request_timeout = 30 # 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 @dataclass 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 # Session management for requests session = requests.Session() session.headers.update({ "User-Agent": "SunoMusicGenerator/1.0", "Accept": "application/json" }) def validate_inputs( prompt: str, title: str, style: str, upload_url_type: str, custom_upload_url: str ) -> List[str]: """Validate user inputs and return list of errors""" errors = [] # Trim inputs prompt = prompt.strip() title = title.strip() style = style.strip() custom_upload_url = custom_upload_url.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 custom URL if selected if upload_url_type == "custom": if not custom_upload_url: errors.append("Please provide a custom upload URL") elif not custom_upload_url.startswith(("http://", "https://")): errors.append("Custom URL must start with http:// or https://") return errors def generate_temp_upload_url() -> str: """Generate a simulated temporary upload URL""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") unique_id = str(uuid.uuid4())[:8] return f"https://storage.temp.example.com/uploads/{timestamp}_{unique_id}.mp3" 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, upload_url_type: str, custom_upload_url: 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 fixed callback URL """ 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, upload_url_type, custom_upload_url) 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" ) # Determine upload URL if upload_url_type == "auto": upload_url = generate_temp_upload_url() logger.info(f"Using auto-generated upload URL: {upload_url}") else: upload_url = custom_upload_url.strip() logger.info(f"Using custom upload URL: {upload_url}") # Prepare payload payload = { "uploadUrl": upload_url, "customMode": custom_mode, "instrumental": instrumental, "model": model, "callBackUrl": config.fixed_callback_url, # Always use fixed 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 } # 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.fixed_callback_url, 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: return f"""{response.message} 📋 Task Details: • Task ID: {response.task_id} • Upload URL: {response.upload_url} • Callback URL: {response.callback_url} ✅ Your music is being generated! The callback will be sent to: {config.fixed_callback_url} 📱 You can check the playlist at: https://1hit.no/cover/playlist.php (Newest songs appear first) 🔍 Full API Response: {json.dumps(response.raw_response, indent=2) if response.raw_response else 'No response data'}""" else: error_details = f"\n\n🔍 Error Type: {response.error_type}" if response.error_type else "" raw_response = f"\n\n📋 Full Response:\n{json.dumps(response.raw_response, indent=2)}" if response.raw_response else "" return f"{response.message}{error_details}{raw_response}" 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, or use a minimal payload test_payload = { "uploadUrl": "https://example.com/test.mp3", "callBackUrl": config.fixed_callback_url, "prompt": "test", "title": "test", "style": "test", "customMode": False, "instrumental": True } headers = { "Authorization": f"Bearer {config.suno_api_key}", "Content-Type": "application/json" } # Use HEAD request or minimal endpoint if available response = session.head( config.suno_api_url.replace("/upload-cover", ""), headers={"Authorization": f"Bearer {config.suno_api_key}"}, timeout=10 ) if response.status_code < 500: return f"✅ API Connection: OK (Key is configured)" 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"], "upload_url_type": "auto", "custom_upload_url": "https://storage.example.com/upload", "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; } """ ) as app: # Custom CSS for better UI gr.HTML(""" """) gr.Markdown("# 🎵 Suno Music Generator") gr.Markdown(f"Generate music using Suno API. Callbacks sent to your PHP endpoint at `{config.fixed_callback_url}`") # Status Bar with gr.Row(): with gr.Column(scale=1): api_status = gr.HTML( value=f"""
API Key: {'✅ Loaded' if config.suno_api_key else '❌ Not found'}
Callback URL: {config.fixed_callback_url}
Generated songs will appear here:
🎵 Open PlaylistSending request to Suno API...
This may take a moment. Do not close the window.