![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/public_html/ |
<?php
// Set execution time limit and memory limit
set_time_limit(60); // 60 seconds max execution time
ini_set('max_execution_time', 60);
ini_set('memory_limit', '256M');
// Enable error reporting for debugging (remove in production)
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't display errors to users, but log them
ini_set('log_errors', 1);
// Start output buffering to ensure headers can be sent
ob_start();
// Set error handler to catch fatal errors and log detailed information
register_shutdown_function(function() {
$error = error_get_last();
if ($error !== NULL && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR])) {
$error_details = "FATAL ERROR in create_music.php:\n";
$error_details .= "Type: " . $error['type'] . "\n";
$error_details .= "Message: " . $error['message'] . "\n";
$error_details .= "File: " . $error['file'] . "\n";
$error_details .= "Line: " . $error['line'] . "\n";
$error_details .= "POST data: " . print_r($_POST, true) . "\n";
error_log($error_details);
if (session_status() === PHP_SESSION_ACTIVE && !headers_sent()) {
try {
$_SESSION['error'] = 'A server error occurred: ' . htmlspecialchars($error['message']) . ' (Line ' . $error['line'] . ')';
header('Location: index.php#create');
exit;
} catch (Exception $e) {
error_log("Error in shutdown function: " . $e->getMessage());
}
}
}
});
session_start();
// Include translation system
require_once __DIR__ . '/includes/translations.php';
// Log that script started - use both error_log and file_put_contents for immediate feedback
$log_msg = "[" . date('Y-m-d H:i:s') . "] create_music.php: Script started, POST method: " . $_SERVER['REQUEST_METHOD'] . ", User ID: " . ($_SESSION['user_id'] ?? 'N/A') . "\n";
error_log($log_msg);
file_put_contents(__DIR__ . '/create_music_debug.log', $log_msg, FILE_APPEND);
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
error_log("create_music.php: User not logged in, redirecting to login");
header('Location: auth/login.php');
exit;
}
error_log("create_music.php: User logged in, user_id: " . $_SESSION['user_id']);
// Check if form was submitted
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: index.php#create');
exit;
}
// SECURITY: CSRF Protection
require_once 'includes/security.php';
$csrf_token = $_POST['csrf_token'] ?? '';
if (!validateCSRFToken($csrf_token)) {
error_log("SECURITY: CSRF token validation failed in create_music.php from IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
$_SESSION['error'] = 'Security validation failed. Please refresh the page and try again.';
header('Location: index.php#create');
exit;
}
error_log("create_music.php: Loading database config");
require_once 'config/database.php';
error_log("create_music.php: Getting database connection");
try {
$pdo = getDBConnection();
if (!$pdo) {
throw new Exception('Database connection failed - getDBConnection returned null/false');
}
error_log("create_music.php: Database connection successful");
} catch (Exception $e) {
error_log("Database connection error: " . $e->getMessage());
error_log("Database connection error trace: " . $e->getTraceAsString());
$_SESSION['error'] = 'Database connection error. Please try again.';
ob_end_clean();
header('Location: index.php#create');
exit;
} catch (Error $e) {
error_log("Database connection fatal error: " . $e->getMessage());
error_log("Database connection fatal error trace: " . $e->getTraceAsString());
$_SESSION['error'] = 'Database connection fatal error: ' . htmlspecialchars($e->getMessage());
ob_end_clean();
header('Location: index.php#create');
exit;
}
// Ensure UTF-8 encoding for all input (only if mbstring extension is available)
if (function_exists('mb_internal_encoding')) {
mb_internal_encoding('UTF-8');
if (function_exists('mb_http_output')) {
mb_http_output('UTF-8');
}
// Note: mb_http_input() is used to detect input encoding, not set it
// It requires a type parameter: "G", "P", "C", "S", "I", or "L"
// We don't need to call it here as we handle encoding manually
}
// Debug: Log all POST data (with compatibility check for JSON flags)
$json_post_flags = 0;
if (defined('JSON_UNESCAPED_UNICODE')) {
$json_post_flags |= JSON_UNESCAPED_UNICODE;
}
if (defined('JSON_UNESCAPED_SLASHES')) {
$json_post_flags |= JSON_UNESCAPED_SLASHES;
}
error_log("POST data received: " . json_encode($_POST, $json_post_flags));
// Get form data - preserve special characters
$type = isset($_POST['type']) ? trim($_POST['type']) : 'music';
$prompt = isset($_POST['prompt']) ? $_POST['prompt'] : '';
$advancedPrompt = isset($_POST['advancedPrompt']) ? trim($_POST['advancedPrompt']) : '';
// Get title - preserve it even if empty (will be used for database)
$title = isset($_POST['title']) ? trim($_POST['title']) : '';
error_log("create_music.php: Title from POST: '$title' (length: " . strlen($title) . ")");
// Default to V5 (latest model) - supports up to 8 minutes
// V3_5/V4 only support 4 minutes max
$model_name = isset($_POST['model_name']) ? trim($_POST['model_name']) : 'V5';
// Duration will be parsed later by parseDuration() function, but get raw value first
$duration = $_POST['duration'] ?? '360';
// Custom Mode is always false now (removed from UI to simplify)
$customMode = 'false';
// Get advanced form fields
$genre = $_POST['genre'] ?? '';
$key = $_POST['key'] ?? '';
$tempo = $_POST['tempo'] ?? '';
$mood = $_POST['mood'] ?? '';
$instrumental = $_POST['instrumental'] ?? 'false';
error_log("create_music.php: Instrumental from POST: '$instrumental' (type: " . gettype($instrumental) . ")");
$energy = $_POST['energy'] ?? '';
$excitement = $_POST['excitement'] ?? '';
$scale = $_POST['scale'] ?? '';
$tags = $_POST['tags'] ?? '';
// Get new additional fields
$timeSignature = $_POST['timeSignature'] ?? '';
$language = $_POST['language'] ?? '';
$voiceType = $_POST['voiceType'] ?? '';
$useCase = $_POST['useCase'] ?? '';
$instruments = $_POST['instruments'] ?? '';
// Get Pro Mode fields
$proTitle = $_POST['proTitle'] ?? '';
$proModel = $_POST['proModel'] ?? '';
$proVariations = $_POST['proVariations'] ?? '';
$proKey = $_POST['proKey'] ?? '';
$proScale = $_POST['proScale'] ?? '';
$proTimeSignature = $_POST['proTimeSignature'] ?? '';
$proTempo = $_POST['proTempo'] ?? '';
$proChordProgression = $_POST['proChordProgression'] ?? '';
$proOctave = $_POST['proOctave'] ?? '';
$proGenre = $_POST['proGenre'] ?? '';
$proSubGenre = $_POST['proSubGenre'] ?? '';
$proDecade = $_POST['proDecade'] ?? '';
$proLeadInstrument = $_POST['proLeadInstrument'] ?? '';
$proRhythmSection = $_POST['proRhythmSection'] ?? '';
$proHarmonySection = $_POST['proHarmonySection'] ?? '';
$proArrangement = $_POST['proArrangement'] ?? '';
$proComplexity = $_POST['proComplexity'] ?? '';
$proDensity = $_POST['proDensity'] ?? '';
$proReverb = $_POST['proReverb'] ?? '';
$proCompression = $_POST['proCompression'] ?? '';
$proStereoWidth = $_POST['proStereoWidth'] ?? '';
$proBassLevel = $_POST['proBassLevel'] ?? '';
$proMidLevel = $_POST['proMidLevel'] ?? '';
$proTrebleLevel = $_POST['proTrebleLevel'] ?? '';
$proVoiceType = $_POST['proVoiceType'] ?? '';
$proLanguage = $_POST['proLanguage'] ?? '';
$proVocalStyle = $_POST['proVocalStyle'] ?? '';
$proLyricTheme = $_POST['proLyricTheme'] ?? '';
$proRhymeScheme = $_POST['proRhymeScheme'] ?? '';
$proHookFrequency = $_POST['proHookFrequency'] ?? '';
$proIntroLength = $_POST['proIntroLength'] ?? '';
$proVerseChorusRatio = $_POST['proVerseChorusRatio'] ?? '';
$proBridge = $_POST['proBridge'] ?? '';
$proOutroStyle = $_POST['proOutroStyle'] ?? '';
$proBuildUps = $_POST['proBuildUps'] ?? '';
$proTransitions = $_POST['proTransitions'] ?? '';
$proDuration = $_POST['proDuration'] ?? '';
$proQuality = $_POST['proQuality'] ?? '';
$proMood = $_POST['proMood'] ?? '';
$proEnergy = $_POST['proEnergy'] ?? '';
$proExcitement = $_POST['proExcitement'] ?? '';
$proDanceability = $_POST['proDanceability'] ?? '';
$proPrompt = $_POST['proPrompt'] ?? '';
$proTags = $_POST['proTags'] ?? '';
// Enable customMode when pro mode is used (pro mode allows up to 5000 characters)
// Pro mode is detected when proPrompt is provided (and not just whitespace)
if (!empty($proPrompt) && trim($proPrompt) !== '') {
$customMode = 'true';
error_log("Pro mode detected (proPrompt provided), enabling customMode to allow up to 5000 characters");
}
// Debug: Log individual fields (truncate long prompts for logging)
if (function_exists('mb_strlen')) {
$promptLen = mb_strlen($prompt);
$advancedPromptLen = mb_strlen($advancedPrompt);
$proPromptLen = mb_strlen($proPrompt);
} else {
$promptLen = strlen($prompt);
$advancedPromptLen = strlen($advancedPrompt);
$proPromptLen = strlen($proPrompt);
}
error_log("Form fields - type: $type, prompt length: $promptLen, advancedPrompt length: $advancedPromptLen, proPrompt length: $proPromptLen, title: '$title', model_name: '$model_name', duration: $duration, proDuration: " . ($proDuration ?? 'not set') . ", customMode: $customMode, proVoiceType: '$proVoiceType'");
// Use pro prompt if available, then advanced prompt, otherwise simple prompt
// Preserve all special characters - only convert encoding if actually needed
$finalPrompt = !empty($proPrompt) ? $proPrompt : (!empty($advancedPrompt) ? $advancedPrompt : $prompt);
// Only convert if it's NOT already valid UTF-8 (don't re-encode valid UTF-8 as it can corrupt the prompt)
if (function_exists('mb_check_encoding') && function_exists('mb_convert_encoding')) {
if (!mb_check_encoding($finalPrompt, 'UTF-8')) {
$finalPrompt = mb_convert_encoding($finalPrompt, 'UTF-8', 'auto');
}
}
// Decode any HTML entities that might have been encoded (like ' or ')
$finalPrompt = html_entity_decode($finalPrompt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$finalPrompt = trim($finalPrompt);
// Handle common user mistakes: Remove "./." pattern if it appears at the start of the prompt
// This pattern is sometimes mistakenly used as a command prefix but is not needed
if (preg_match('/^\.\/\.\s*/', $finalPrompt)) {
$originalPrompt = $finalPrompt;
$finalPrompt = preg_replace('/^\.\/\.\s*/', '', $finalPrompt);
$finalPrompt = trim($finalPrompt);
error_log("WARNING: Removed './.' prefix from prompt. Original: '" . substr($originalPrompt, 0, 100) . "', Cleaned: '" . substr($finalPrompt, 0, 100) . "'");
}
// Parse structured metadata from prompt (BPM, Key, Style, Vocal, Mood, Sound, Duration)
// Format: "BPM: 99 | Key: C Minor (432 Hz) | Camelot: 5A | Style: ... | Vocal: ... | Mood: ... | Sound: ... | Duration: 5:32"
$extractedMetadata = [
'bpm' => null,
'key' => null,
'style' => null,
'vocal' => null,
'mood' => null,
'sound' => null,
'duration' => null
];
// Extract BPM
if (preg_match('/bpm[:\s]+(\d+)/i', $finalPrompt, $matches)) {
$extractedMetadata['bpm'] = intval($matches[1]);
error_log("Extracted BPM from prompt: " . $extractedMetadata['bpm']);
}
// Extract Key (e.g., "Key: C Minor (432 Hz)" -> "C Minor")
if (preg_match('/key[:\s]+([^|(]+?)(?:\s*\([^)]+\))?/i', $finalPrompt, $matches)) {
$extractedMetadata['key'] = trim($matches[1]);
error_log("Extracted Key from prompt: " . $extractedMetadata['key']);
}
// Extract Style (already handled later, but store here for reference)
if (preg_match('/style[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$extractedMetadata['style'] = trim($matches[1]);
error_log("Extracted Style from prompt: " . $extractedMetadata['style']);
}
// Extract Vocal description
if (preg_match('/vocal[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$extractedMetadata['vocal'] = trim($matches[1]);
error_log("Extracted Vocal from prompt: " . $extractedMetadata['vocal']);
}
// Extract Mood
if (preg_match('/mood[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$extractedMetadata['mood'] = trim($matches[1]);
error_log("Extracted Mood from prompt: " . $extractedMetadata['mood']);
}
// Extract Sound description
if (preg_match('/sound[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$extractedMetadata['sound'] = trim($matches[1]);
error_log("Extracted Sound from prompt: " . $extractedMetadata['sound']);
}
// Extract Duration from structured format (e.g., "Duration: 5:32")
if (preg_match('/duration[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$durationText = trim($matches[1]);
// Parse duration format like "5:32" (5 minutes 32 seconds)
if (preg_match('/^(\d+):(\d+)$/', $durationText, $durMatches)) {
$extractedMetadata['duration'] = (intval($durMatches[1]) * 60) + intval($durMatches[2]);
error_log("Extracted Duration from structured format: $durationText = " . $extractedMetadata['duration'] . " seconds");
} elseif (is_numeric($durationText)) {
$extractedMetadata['duration'] = intval($durationText);
error_log("Extracted Duration from structured format: " . $extractedMetadata['duration'] . " seconds");
}
}
// Also extract duration from natural language in the prompt (e.g., "5-6 minutes", "5 to 6 min", "about 5 minutes")
// Only do this if we haven't already extracted from structured format
if (empty($extractedMetadata['duration'])) {
// Look for patterns like "5-6 minutes", "5 to 6 minutes", "5-6 min", "5-6mins", "about 5 minutes", "5 minutes"
// Also handle ranges like "5-7 minutes" -> use the higher end (7 minutes = 420 seconds)
if (preg_match('/(\d+)\s*[-–—to]\s*(\d+)\s*(?:minutes?|mins?|min)/i', $finalPrompt, $rangeMatches)) {
$minMinutes = intval($rangeMatches[1]);
$maxMinutes = intval($rangeMatches[2]);
// Use the higher end of the range to ensure we get closer to requested length
$extractedMetadata['duration'] = $maxMinutes * 60;
error_log("Extracted Duration from natural language range: {$minMinutes}-{$maxMinutes} minutes, using {$maxMinutes} minutes = " . $extractedMetadata['duration'] . " seconds");
} elseif (preg_match('/(?:about|around|approximately|roughly)?\s*(\d+)\s*(?:minutes?|mins?|min)/i', $finalPrompt, $singleMatches)) {
$minutes = intval($singleMatches[1]);
$extractedMetadata['duration'] = $minutes * 60;
error_log("Extracted Duration from natural language: {$minutes} minutes = " . $extractedMetadata['duration'] . " seconds");
} elseif (preg_match('/(\d+)\s*[-–—to]\s*(\d+)\s*(?:seconds?|secs?|sec)/i', $finalPrompt, $secRangeMatches)) {
$minSeconds = intval($secRangeMatches[1]);
$maxSeconds = intval($secRangeMatches[2]);
// Use the higher end of the range
$extractedMetadata['duration'] = $maxSeconds;
error_log("Extracted Duration from natural language range: {$minSeconds}-{$maxSeconds} seconds, using {$maxSeconds} seconds");
} elseif (preg_match('/(?:about|around|approximately|roughly)?\s*(\d+)\s*(?:seconds?|secs?|sec)/i', $finalPrompt, $secSingleMatches)) {
$seconds = intval($secSingleMatches[1]);
$extractedMetadata['duration'] = $seconds;
error_log("Extracted Duration from natural language: {$seconds} seconds");
}
}
// CRITICAL: If customMode is false, skip ALL enhancement and keep original prompt format
// This matches the successful API request format that had the full structured prompt
$isNonCustomMode = ($customMode === 'false' || $customMode === false);
if ($isNonCustomMode) {
// Non-custom mode: Keep original prompt format with all structured metadata intact
// This matches successful requests that had: "OM AH HUM (Root Awakening)\r\n\r\nBPM: 99\r\n\r\nKey: C Minor..."
// Use the original prompt before any enhancement/cleaning - restore from the raw input
$finalPrompt = !empty($proPrompt) ? $proPrompt : (!empty($advancedPrompt) ? $advancedPrompt : $prompt);
// Only decode HTML entities, don't enhance or clean - keep the structured format
$finalPrompt = html_entity_decode($finalPrompt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$finalPrompt = trim($finalPrompt);
error_log("Non-custom mode: Keeping original structured prompt format (not enhancing). Length: " . strlen($finalPrompt));
} else {
// Custom mode: Enhance the prompt by incorporating extracted metadata into natural language
// This helps the API understand the full context better
$enhancedPromptParts = [];
// Start with the original prompt (but remove the structured metadata lines to avoid duplication)
$cleanPrompt = $finalPrompt;
// Remove structured metadata lines (BPM, Key, Camelot, Style, Vocal, Mood, Sound, Duration)
// BUT: Only remove if there's other content, otherwise keep the structured format
$hasOtherContent = false;
$testClean = preg_replace('/\s*(bpm|key|camelot|style|vocal|mood|sound|duration)[:\s]+[^|\n]+(?:\s*[:\-|]|$)/i', '', $cleanPrompt);
$testClean = preg_replace('/\s*\|\s*/', ' ', $testClean);
$testClean = trim($testClean);
if (!empty($testClean)) {
$hasOtherContent = true;
$cleanPrompt = $testClean;
} else {
// If removing metadata leaves nothing, keep the original structured format
// This handles cases where user ONLY provides structured metadata
$cleanPrompt = $finalPrompt;
}
$cleanPrompt = trim($cleanPrompt);
if (!empty($cleanPrompt)) {
$enhancedPromptParts[] = $cleanPrompt;
}
// Add Vocal description if available
if (!empty($extractedMetadata['vocal'])) {
$enhancedPromptParts[] = "Vocal style: " . $extractedMetadata['vocal'];
}
// Add Sound description if available
if (!empty($extractedMetadata['sound'])) {
$enhancedPromptParts[] = "Sound design: " . $extractedMetadata['sound'];
}
// Add Mood if available
if (!empty($extractedMetadata['mood'])) {
$enhancedPromptParts[] = "Mood: " . $extractedMetadata['mood'];
}
// Add BPM and Key if available (helps with musical accuracy)
if (!empty($extractedMetadata['bpm']) || !empty($extractedMetadata['key'])) {
$tempoKeyParts = [];
if (!empty($extractedMetadata['bpm'])) {
$tempoKeyParts[] = $extractedMetadata['bpm'] . " BPM";
}
if (!empty($extractedMetadata['key'])) {
$tempoKeyParts[] = "key of " . $extractedMetadata['key'];
}
if (!empty($tempoKeyParts)) {
$enhancedPromptParts[] = "Musical parameters: " . implode(", ", $tempoKeyParts);
}
}
// Combine all parts into enhanced prompt
if (!empty($enhancedPromptParts)) {
$finalPrompt = implode(". ", $enhancedPromptParts) . ".";
error_log("Enhanced prompt created from structured metadata. Original length: " . strlen($cleanPrompt) . ", Enhanced length: " . strlen($finalPrompt));
} else {
// If no enhancement, use original (or fallback to original if cleaning removed everything)
$finalPrompt = !empty($cleanPrompt) ? $cleanPrompt : $finalPrompt;
// If still empty, use the original raw prompt before any processing
if (empty($finalPrompt)) {
$finalPrompt = !empty($proPrompt) ? $proPrompt : (!empty($advancedPrompt) ? $advancedPrompt : $prompt);
error_log("All prompt processing resulted in empty, using original raw prompt");
}
}
}
// Use extracted duration if available and no duration was provided in form
// Also use it if the form duration is the default (360) and we found a better match in the prompt
if (!empty($extractedMetadata['duration'])) {
if (empty($duration) && empty($proDuration)) {
$duration = $extractedMetadata['duration'];
error_log("Using extracted duration from prompt (no form value): $duration seconds");
} elseif ($duration == '360' && empty($proDuration)) {
// If form has default 360 but prompt specifies something else, use the prompt value
$duration = $extractedMetadata['duration'];
error_log("Using extracted duration from prompt (overriding default 360): $duration seconds");
}
}
// If prompt is empty, try to use genre/style field or other fields as fallback
// This handles cases where user puts description in genre field instead of prompt field
if (empty($finalPrompt)) {
$fallbackPrompt = '';
// Try genre field first (most likely where user put their description)
if (!empty($genre)) {
$fallbackPrompt = $genre;
error_log("Prompt is empty, using genre field as fallback: '$genre'");
} elseif (!empty($proGenre)) {
$fallbackPrompt = $proGenre;
error_log("Prompt is empty, using proGenre field as fallback: '$proGenre'");
} elseif (!empty($tags)) {
$fallbackPrompt = $tags;
error_log("Prompt is empty, using tags field as fallback: '$tags'");
} elseif (!empty($title)) {
$fallbackPrompt = $title;
error_log("Prompt is empty, using title field as fallback: '$title'");
}
// If we found a fallback, use it as the prompt
if (!empty($fallbackPrompt)) {
$finalPrompt = $fallbackPrompt;
error_log("Using fallback prompt: '$finalPrompt'");
// Re-parse metadata from the fallback prompt since it might contain structured data
// Extract BPM
if (preg_match('/bpm[:\s]+(\d+)/i', $finalPrompt, $matches)) {
$extractedMetadata['bpm'] = intval($matches[1]);
error_log("Extracted BPM from fallback prompt: " . $extractedMetadata['bpm']);
}
// Extract Key
if (preg_match('/key[:\s]+([^|(]+?)(?:\s*\([^)]+\))?/i', $finalPrompt, $matches)) {
$extractedMetadata['key'] = trim($matches[1]);
error_log("Extracted Key from fallback prompt: " . $extractedMetadata['key']);
}
// Extract Style
if (preg_match('/style[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$extractedMetadata['style'] = trim($matches[1]);
error_log("Extracted Style from fallback prompt: " . $extractedMetadata['style']);
}
// Extract Duration
if (preg_match('/duration[:\s]+([^|]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$durationText = trim($matches[1]);
if (preg_match('/^(\d+):(\d+)$/', $durationText, $durMatches)) {
$extractedMetadata['duration'] = (intval($durMatches[1]) * 60) + intval($durMatches[2]);
error_log("Extracted Duration from fallback prompt: $durationText = " . $extractedMetadata['duration'] . " seconds");
} elseif (is_numeric($durationText)) {
$extractedMetadata['duration'] = intval($durationText);
error_log("Extracted Duration from fallback prompt: " . $extractedMetadata['duration'] . " seconds");
}
}
// Also check for natural language duration
if (empty($extractedMetadata['duration'])) {
if (preg_match('/(\d+)\s*[-–—to]\s*(\d+)\s*(?:minutes?|mins?|min)/i', $finalPrompt, $rangeMatches)) {
$minMinutes = intval($rangeMatches[1]);
$maxMinutes = intval($rangeMatches[2]);
$extractedMetadata['duration'] = $maxMinutes * 60;
error_log("Extracted Duration from fallback prompt natural language: {$minMinutes}-{$maxMinutes} minutes = " . $extractedMetadata['duration'] . " seconds");
} elseif (preg_match('/(?:about|around|approximately|roughly)?\s*(\d+)\s*(?:minutes?|mins?|min)/i', $finalPrompt, $singleMatches)) {
$minutes = intval($singleMatches[1]);
$extractedMetadata['duration'] = $minutes * 60;
error_log("Extracted Duration from fallback prompt: {$minutes} minutes = " . $extractedMetadata['duration'] . " seconds");
}
}
} else {
// No fallback available, show error
error_log("ERROR: Final prompt is empty after processing and no fallback fields available!");
$_SESSION['error'] = 'Please provide a music description in the "Describe your music" field.';
ob_end_clean();
header('Location: index.php#create');
exit;
}
}
// Use pro title if available, otherwise use regular title
// IMPORTANT: Always use the title if provided, even in simple mode - it should be saved to database
$finalTitle = !empty($proTitle) ? $proTitle : $title;
// If no title was provided, try to extract it from the prompt
// Look for patterns like "2. TITLE" or "NUMBER. TITLE (Subtitle)" at the start of the prompt
if (empty($finalTitle) && !empty($finalPrompt)) {
// Match pattern: number followed by period, space, then title (possibly with parentheses)
// Example: "2. LAM (Descent into Earth)" or "1. Song Title"
if (preg_match('/^(\d+\.\s+)([^\n]+?)(?:\n|$)/', $finalPrompt, $matches)) {
$extractedTitle = trim($matches[2]); // Get the title part (without the number prefix)
// Check length using mb_strlen for proper character counting
$extractedTitleLength = function_exists('mb_strlen') ? mb_strlen($extractedTitle, 'UTF-8') : strlen($extractedTitle);
if (!empty($extractedTitle) && $extractedTitleLength <= 255) {
// Truncate to 80 chars for API compatibility (even though DB allows 255)
if ($extractedTitleLength > 80) {
$finalTitle = function_exists('mb_substr') ? mb_substr($extractedTitle, 0, 80, 'UTF-8') : substr($extractedTitle, 0, 80);
error_log("Extracted title exceeded 80 characters, truncated: '$finalTitle'");
} else {
$finalTitle = $extractedTitle;
}
// Remove the title line from the prompt
$finalPrompt = preg_replace('/^' . preg_quote($matches[0], '/') . '\s*/m', '', $finalPrompt);
$finalPrompt = trim($finalPrompt);
error_log("create_music.php: Extracted title from prompt: '$finalTitle'");
}
}
}
error_log("create_music.php: finalTitle after processing: '$finalTitle' (proTitle: '$proTitle', title: '$title')");
// For advanced mode: Incorporate all advanced settings into the prompt
// CRITICAL: Only process if we're actually in Advanced Mode (advancedPrompt has content)
// This prevents Simple Mode from accidentally using Advanced Mode default values
$hasAdvancedSettings = !empty($advancedPrompt) || !empty($tempo) || !empty($key) || !empty($scale) ||
!empty($timeSignature) || !empty($energy) || !empty($excitement) || !empty($mood) ||
!empty($language) || !empty($voiceType) || !empty($useCase) || !empty($instruments);
// IMPORTANT: Only add Advanced settings if advancedPrompt is NOT empty (user is in Advanced Mode)
// This prevents Simple Mode from getting "Musical specifications" added when Advanced fields have defaults
if (!empty($advancedPrompt) && $hasAdvancedSettings) {
// Only process advanced settings if we're in advanced mode (advancedPrompt exists and has content)
$advancedSettingsParts = [];
// Tempo/BPM (skip if default value 120)
if (!empty($tempo) && $tempo != '120') {
$advancedSettingsParts[] = $tempo . " BPM";
}
// Key
if (!empty($key)) {
$advancedSettingsParts[] = "key of " . $key;
}
// Scale
if (!empty($scale)) {
$advancedSettingsParts[] = $scale . " scale";
}
// Time Signature
if (!empty($timeSignature)) {
$advancedSettingsParts[] = $timeSignature . " time signature";
}
// Energy Level (slider 1-10)
if (!empty($energy) && $energy != '7') {
$energyLevel = intval($energy);
if ($energyLevel <= 3) {
$advancedSettingsParts[] = "Calm, low energy";
} elseif ($energyLevel <= 5) {
$advancedSettingsParts[] = "Moderate energy";
} elseif ($energyLevel >= 8) {
$advancedSettingsParts[] = "High energy, intense";
} else {
$advancedSettingsParts[] = "Energetic";
}
}
// Excitement Level (slider 1-10)
if (!empty($excitement) && $excitement != '6') {
$excitementLevel = intval($excitement);
if ($excitementLevel <= 3) {
$advancedSettingsParts[] = "Relaxed, calm";
} elseif ($excitementLevel >= 8) {
$advancedSettingsParts[] = "Thrilling, exciting";
}
}
// Mood
if (!empty($mood)) {
$advancedSettingsParts[] = ucfirst($mood) . " mood";
}
// Language
if (!empty($language) && $language !== 'instrumental') {
$advancedSettingsParts[] = ucfirst($language) . " language";
}
// Voice Type
if (!empty($voiceType)) {
switch ($voiceType) {
case 'male':
$advancedSettingsParts[] = "Male vocalist/voice";
break;
case 'female':
$advancedSettingsParts[] = "Female vocalist/voice";
break;
case 'duet':
$advancedSettingsParts[] = "Male and female duet vocals";
break;
case 'duet_female_male':
$advancedSettingsParts[] = "Female and male duet vocals";
break;
case 'choir':
$advancedSettingsParts[] = "Choir vocals";
break;
case 'child':
$advancedSettingsParts[] = "Children's voices";
break;
case 'robot':
$advancedSettingsParts[] = "Robotic/AI voice";
break;
default:
$advancedSettingsParts[] = ucfirst($voiceType) . " voice";
}
}
// Use Case
if (!empty($useCase)) {
$useCaseText = str_replace('-', ' ', $useCase);
$advancedSettingsParts[] = ucwords($useCaseText) . " use case";
}
// Primary Instruments
if (!empty($instruments)) {
$advancedSettingsParts[] = ucfirst($instruments) . " as primary instrument";
}
// REMOVED: Most settings are redundant or not useful when added as text to the prompt
// - Genre is already handled via the 'style' API parameter
// - Technical parameters (energy, excitement, use case, instruments) don't help the AI
// - Voice type and language should be mentioned naturally by the user if needed
// - Tempo/key could be useful but are often redundant with user's natural description
// Users should describe what they want naturally in their prompt instead
// if (!empty($advancedSettingsParts)) {
// $settingsText = implode(", ", $advancedSettingsParts);
// $finalPrompt = rtrim($finalPrompt, '.') . '. ' . $settingsText . '.';
// error_log("Advanced mode: Incorporated settings naturally into prompt: " . count($advancedSettingsParts) . " settings");
// }
}
// REMOVED: Voice type addition to prompt
// Users should mention voice type naturally in their prompt if they want it
// Adding it automatically can be redundant and clutter the prompt
// if (!empty($proVoiceType)) {
// ... (removed voice type addition logic)
// }
// For pro mode: Incorporate all pro settings into the prompt
// Apply pro settings even without proPrompt text - these are the dropdown/slider settings
$hasProSettings = !empty($proGenre) || !empty($proTempo) || !empty($proKey) || !empty($proScale) ||
!empty($proTimeSignature) || !empty($proChordProgression) || !empty($proLeadInstrument) ||
!empty($proRhythmSection) || !empty($proHarmonySection) || !empty($proArrangement) ||
!empty($proVocalStyle) || !empty($proLanguage) || !empty($proMood) || !empty($proEnergy) ||
!empty($proSubGenre) || !empty($proDecade) || !empty($proComplexity) || !empty($proDensity) ||
!empty($proVoiceType) || !empty($proOctave) || !empty($proLyricTheme) || !empty($proRhymeScheme) ||
!empty($proHookFrequency) || !empty($proIntroLength) || !empty($proVerseChorusRatio) ||
!empty($proBridge) || !empty($proOutroStyle) || !empty($proBuildUps) || !empty($proTransitions) ||
!empty($proExcitement) || !empty($proDanceability) || !empty($proQuality) ||
!empty($proReverb) || !empty($proCompression) || !empty($proStereoWidth) ||
!empty($proBassLevel) || !empty($proMidLevel) || !empty($proTrebleLevel);
if ($hasProSettings) {
$proSettingsParts = [];
// Voice Type (IMPORTANT - add first for emphasis)
if (!empty($proVoiceType)) {
switch ($proVoiceType) {
case 'male':
$proSettingsParts[] = "Male vocalist/voice";
break;
case 'female':
$proSettingsParts[] = "Female vocalist/voice";
break;
case 'duet':
$proSettingsParts[] = "Male and female duet vocals";
break;
case 'duet_female_male':
$proSettingsParts[] = "Female and male duet vocals";
break;
case 'choir':
$proSettingsParts[] = "Choir vocals";
break;
case 'child':
$proSettingsParts[] = "Children's voices";
break;
case 'robot':
$proSettingsParts[] = "Robotic/AI voice";
break;
case 'whisper':
$proSettingsParts[] = "Whispered vocals";
break;
case 'screaming':
$proSettingsParts[] = "Screaming vocals";
break;
case 'rapping':
$proSettingsParts[] = "Rapping vocals";
break;
case 'singing':
$proSettingsParts[] = "Singing vocals";
break;
case 'spoken':
$proSettingsParts[] = "Spoken word vocals";
break;
default:
$proSettingsParts[] = ucfirst($proVoiceType) . " voice";
}
}
// Genre (most important - already handled in style, but add to prompt for clarity)
if (!empty($proGenre)) {
$proSettingsParts[] = ucfirst($proGenre) . " genre";
}
// Tempo/BPM (skip if default value 120)
if (!empty($proTempo) && $proTempo != '120') {
$proSettingsParts[] = $proTempo . " BPM";
}
// Key
if (!empty($proKey)) {
$proSettingsParts[] = "key of " . $proKey;
}
// Scale
if (!empty($proScale)) {
$proSettingsParts[] = $proScale . " scale";
}
// Time Signature
if (!empty($proTimeSignature)) {
$proSettingsParts[] = $proTimeSignature . " time signature";
}
// Chord Progression
if (!empty($proChordProgression) && $proChordProgression !== 'custom') {
$proSettingsParts[] = $proChordProgression . " chord progression";
}
// Lead Instrument
if (!empty($proLeadInstrument)) {
$proSettingsParts[] = ucfirst($proLeadInstrument) . " as lead instrument";
}
// Rhythm Section
if (!empty($proRhythmSection)) {
$rhythmText = str_replace('-', ' ', $proRhythmSection);
$proSettingsParts[] = ucwords($rhythmText) . " rhythm section";
}
// Harmony Section
if (!empty($proHarmonySection)) {
$harmonyText = str_replace('-', ' ', $proHarmonySection);
$proSettingsParts[] = ucwords($harmonyText) . " harmony section";
}
// Arrangement
if (!empty($proArrangement)) {
$proSettingsParts[] = ucfirst($proArrangement) . " arrangement style";
}
// Vocal Style
if (!empty($proVocalStyle)) {
$vocalStyleText = str_replace('-', ' ', $proVocalStyle);
$proSettingsParts[] = ucwords($vocalStyleText) . " vocal style";
}
// Language
if (!empty($proLanguage) && $proLanguage !== 'instrumental') {
$proSettingsParts[] = ucfirst($proLanguage) . " language";
}
// Mood
if (!empty($proMood)) {
$proSettingsParts[] = ucfirst($proMood) . " mood";
}
// Energy Level (slider 1-10)
if (!empty($proEnergy) && $proEnergy != '7') {
$energyLevel = intval($proEnergy);
if ($energyLevel <= 3) {
$proSettingsParts[] = "Low energy, calm";
} elseif ($energyLevel <= 5) {
$proSettingsParts[] = "Moderate energy";
} elseif ($energyLevel >= 8) {
$proSettingsParts[] = "High energy, intense";
} else {
$proSettingsParts[] = "Energetic";
}
}
// Sub-genre
if (!empty($proSubGenre)) {
$proSettingsParts[] = ucfirst($proSubGenre) . " sub-genre";
}
// Decade style
if (!empty($proDecade)) {
$proSettingsParts[] = $proDecade . " style";
}
// Complexity
if (!empty($proComplexity)) {
$proSettingsParts[] = ucfirst($proComplexity) . " complexity";
}
// Density
if (!empty($proDensity)) {
$densityText = str_replace('-', ' ', $proDensity);
$proSettingsParts[] = ucwords($densityText) . " track density";
}
// Octave
if (!empty($proOctave)) {
$proSettingsParts[] = "Octave " . $proOctave;
}
// Lyric Theme
if (!empty($proLyricTheme)) {
$proSettingsParts[] = ucfirst($proLyricTheme) . " themed lyrics";
}
// Rhyme Scheme
if (!empty($proRhymeScheme)) {
$proSettingsParts[] = ucfirst($proRhymeScheme) . " rhyme scheme";
}
// Hook Frequency
if (!empty($proHookFrequency)) {
$hookText = str_replace('-', ' ', $proHookFrequency);
$proSettingsParts[] = ucwords($hookText) . " hooks";
}
// Intro Length
if (!empty($proIntroLength)) {
$proSettingsParts[] = ucfirst($proIntroLength) . " intro";
}
// Verse/Chorus Ratio
if (!empty($proVerseChorusRatio)) {
$ratioText = str_replace('-', ' ', $proVerseChorusRatio);
$proSettingsParts[] = ucwords($ratioText) . " verse-chorus balance";
}
// Bridge
if (!empty($proBridge)) {
$bridgeText = str_replace('-', ' ', $proBridge);
$proSettingsParts[] = ucwords($bridgeText) . " bridge section";
}
// Outro Style
if (!empty($proOutroStyle)) {
$outroText = str_replace('-', ' ', $proOutroStyle);
$proSettingsParts[] = ucwords($outroText) . " outro";
}
// Build-ups
if (!empty($proBuildUps)) {
$buildText = str_replace('-', ' ', $proBuildUps);
$proSettingsParts[] = ucwords($buildText) . " build-ups";
}
// Transitions
if (!empty($proTransitions)) {
$transText = str_replace('-', ' ', $proTransitions);
$proSettingsParts[] = ucwords($transText) . " transitions";
}
// Excitement Level (slider 1-10)
if (!empty($proExcitement) && $proExcitement != '5' && $proExcitement != '6') {
$excitementLevel = intval($proExcitement);
if ($excitementLevel <= 3) {
$proSettingsParts[] = "Calm, subdued excitement";
} elseif ($excitementLevel >= 8) {
$proSettingsParts[] = "High excitement and intensity";
}
}
// Danceability (slider 1-10)
if (!empty($proDanceability) && $proDanceability != '5') {
$danceLevel = intval($proDanceability);
if ($danceLevel <= 3) {
$proSettingsParts[] = "Low danceability, more listening-focused";
} elseif ($danceLevel >= 7) {
$proSettingsParts[] = "High danceability, danceable rhythm";
}
}
// Production Quality
if (!empty($proQuality)) {
switch ($proQuality) {
case 'demo':
$proSettingsParts[] = "Demo quality production";
break;
case 'radio':
$proSettingsParts[] = "Radio-ready production quality";
break;
case 'studio':
$proSettingsParts[] = "Professional studio production quality";
break;
case 'master':
$proSettingsParts[] = "Mastered, release-ready production quality";
break;
}
}
// Reverb Level (slider 0-10)
if (!empty($proReverb) && $proReverb != '5') {
$reverbLevel = intval($proReverb);
if ($reverbLevel <= 2) {
$proSettingsParts[] = "Dry, no reverb";
} elseif ($reverbLevel <= 4) {
$proSettingsParts[] = "Light reverb";
} elseif ($reverbLevel <= 7) {
$proSettingsParts[] = "Moderate reverb";
} else {
$proSettingsParts[] = "Heavy reverb, wet sound";
}
}
// Compression (slider 0-10)
if (!empty($proCompression) && $proCompression != '5') {
$compressionLevel = intval($proCompression);
if ($compressionLevel <= 2) {
$proSettingsParts[] = "Light compression, dynamic";
} elseif ($compressionLevel <= 4) {
$proSettingsParts[] = "Moderate compression";
} elseif ($compressionLevel <= 7) {
$proSettingsParts[] = "Heavy compression";
} else {
$proSettingsParts[] = "Very heavy compression, punchy";
}
}
// Stereo Width (slider 0-10)
if (!empty($proStereoWidth) && $proStereoWidth != '5') {
$stereoLevel = intval($proStereoWidth);
if ($stereoLevel <= 2) {
$proSettingsParts[] = "Mono or narrow stereo";
} elseif ($stereoLevel <= 4) {
$proSettingsParts[] = "Moderate stereo width";
} elseif ($stereoLevel <= 7) {
$proSettingsParts[] = "Wide stereo field";
} else {
$proSettingsParts[] = "Very wide stereo, spacious";
}
}
// Bass Level (slider 0-10)
if (!empty($proBassLevel) && $proBassLevel != '5') {
$bassLevel = intval($proBassLevel);
if ($bassLevel <= 2) {
$proSettingsParts[] = "Low bass, minimal low end";
} elseif ($bassLevel <= 4) {
$proSettingsParts[] = "Moderate bass";
} elseif ($bassLevel <= 7) {
$proSettingsParts[] = "Prominent bass";
} else {
$proSettingsParts[] = "Heavy bass, bass-heavy mix";
}
}
// Mid Level (slider 0-10)
if (!empty($proMidLevel) && $proMidLevel != '5') {
$midLevel = intval($proMidLevel);
if ($midLevel <= 2) {
$proSettingsParts[] = "Recessed mids, scooped";
} elseif ($midLevel <= 4) {
$proSettingsParts[] = "Moderate mid frequencies";
} elseif ($midLevel <= 7) {
$proSettingsParts[] = "Prominent mids";
} else {
$proSettingsParts[] = "Forward mids, mid-heavy";
}
}
// Treble Level (slider 0-10)
if (!empty($proTrebleLevel) && $proTrebleLevel != '5') {
$trebleLevel = intval($proTrebleLevel);
if ($trebleLevel <= 2) {
$proSettingsParts[] = "Dark, low treble";
} elseif ($trebleLevel <= 4) {
$proSettingsParts[] = "Moderate treble";
} elseif ($trebleLevel <= 7) {
$proSettingsParts[] = "Bright, high treble";
} else {
$proSettingsParts[] = "Very bright, treble-heavy";
}
}
// REMOVED: Most settings are redundant or not useful when added as text to the prompt
// - Genre is already handled via the 'style' API parameter
// - Technical mixing parameters (reverb, compression, stereo width, EQ) can't be controlled by the AI
// - Structural parameters (intro length, verse/chorus ratio, etc.) are too technical
// - Production quality, complexity, density are not actionable by the AI
// - Voice type and language should be mentioned naturally by the user if needed
// - Tempo/key could be useful but are often redundant with user's natural description
// Users should describe what they want naturally in their prompt instead
// The form fields remain for UI/UX but don't automatically clutter the prompt
// if (!empty($proSettingsParts)) {
// $settingsText = implode(", ", $proSettingsParts);
// $finalPrompt = rtrim($finalPrompt, '.') . '. ' . $settingsText . '.';
// error_log("Pro mode: Incorporated settings naturally into prompt: " . count($proSettingsParts) . " settings");
// }
}
// Check prompt length - if over 400 characters and customMode is not enabled, truncate to 400
// The API rejects prompts over 400 characters in non-custom mode, so we need to limit to 400
$promptLength = function_exists('mb_strlen') ? mb_strlen($finalPrompt) : strlen($finalPrompt);
error_log("Final prompt length: $promptLength characters");
error_log("Final prompt preview (first 200 chars): " . substr($finalPrompt, 0, 200));
if ($promptLength > 400 && ($customMode !== 'true' && $customMode !== true)) {
if (function_exists('mb_substr')) {
$finalPrompt = mb_substr($finalPrompt, 0, 400);
} else {
$finalPrompt = substr($finalPrompt, 0, 400);
}
$promptLength = function_exists('mb_strlen') ? mb_strlen($finalPrompt) : strlen($finalPrompt);
error_log("WARNING: Prompt length exceeded 400 characters, truncated to $promptLength characters for non-custom mode");
}
// Use pro model if available, otherwise use regular model
$finalModel = !empty($proModel) ? $proModel : $model_name;
// Parse duration - handle formats like "5:10" (5 minutes 10 seconds), "5-6 minutes", or just seconds
function parseDuration($durationInput) {
if (empty($durationInput)) {
return null;
}
// If it's already a number, return it
if (is_numeric($durationInput)) {
return intval($durationInput);
}
// Try to parse natural language ranges like "5-6 minutes" or "5 to 6 min"
// Use the higher end of the range to ensure we get closer to requested length
if (preg_match('/(\d+)\s*[-–—to]\s*(\d+)\s*(?:minutes?|mins?|min)/i', $durationInput, $rangeMatches)) {
$minMinutes = intval($rangeMatches[1]);
$maxMinutes = intval($rangeMatches[2]);
return $maxMinutes * 60; // Use higher end
}
// Try to parse single natural language like "5 minutes" or "about 5 min"
if (preg_match('/(?:about|around|approximately|roughly)?\s*(\d+)\s*(?:minutes?|mins?|min)/i', $durationInput, $singleMatches)) {
$minutes = intval($singleMatches[1]);
return $minutes * 60;
}
// Try to parse "MM:SS" or "M:SS" format (handle spaces around colon like "5 : 10")
$trimmed = trim($durationInput);
// Remove spaces around colon
$trimmed = preg_replace('/\s*:\s*/', ':', $trimmed);
if (preg_match('/^(\d+):(\d+)$/', $trimmed, $matches)) {
$minutes = intval($matches[1]);
$seconds = intval($matches[2]);
return ($minutes * 60) + $seconds;
}
// Try to parse "MM:SS:MS" format (minutes:seconds:milliseconds - ignore milliseconds, handle spaces)
$trimmed = trim($durationInput);
// Remove spaces around colons
$trimmed = preg_replace('/\s*:\s*/', ':', $trimmed);
if (preg_match('/^(\d+):(\d+):(\d+)$/', $trimmed, $matches)) {
$minutes = intval($matches[1]);
$seconds = intval($matches[2]);
return ($minutes * 60) + $seconds;
}
// Try to parse seconds ranges like "300-360 seconds"
if (preg_match('/(\d+)\s*[-–—to]\s*(\d+)\s*(?:seconds?|secs?|sec)/i', $durationInput, $secRangeMatches)) {
$minSeconds = intval($secRangeMatches[1]);
$maxSeconds = intval($secRangeMatches[2]);
return $maxSeconds; // Use higher end
}
// Try to parse single seconds like "360 seconds"
if (preg_match('/(?:about|around|approximately|roughly)?\s*(\d+)\s*(?:seconds?|secs?|sec)/i', $durationInput, $secSingleMatches)) {
return intval($secSingleMatches[1]);
}
// If it's a string with just numbers, try to parse as integer
$cleaned = preg_replace('/[^0-9]/', '', $durationInput);
if (!empty($cleaned) && is_numeric($cleaned)) {
return intval($cleaned);
}
return null;
}
// Use pro duration if available, otherwise use regular duration
// For pro mode: prioritize proDuration field, but also check duration (which JavaScript may have populated)
$parsedProDuration = !empty($proDuration) ? parseDuration($proDuration) : null;
$parsedDuration = parseDuration($duration);
// If pro mode is active (proPrompt exists), prioritize pro duration or duration field
// This ensures pro mode gets the correct duration even if proDuration field wasn't sent
if (!empty($proPrompt)) {
// Pro mode: use proDuration if available, otherwise use duration (which JavaScript should have populated)
$finalDuration = $parsedProDuration ?? $parsedDuration ?? 360;
error_log("Pro mode detected - Duration parsing: proDuration='$proDuration', duration='$duration', Parsed Pro: " . ($parsedProDuration ?? 'null') . ", Parsed: $parsedDuration, Final: $finalDuration");
} else {
// Regular mode: use proDuration if available, otherwise use regular duration
$finalDuration = $parsedProDuration ?? $parsedDuration ?? 360;
}
error_log("Duration parsing - Input duration: '$duration', Pro duration: '$proDuration', Parsed: $parsedDuration, Parsed Pro: " . ($parsedProDuration ?? 'null') . ", Final: $finalDuration");
// Validate duration based on model version (per API.box documentation)
// V3_5/V4: max 4 minutes (240 seconds)
// V4_5/V4_5PLUS/V5: max 8 minutes (480 seconds)
$maxDuration = 480; // Default to 8 minutes for latest models
if ($finalModel === 'V3_5' || $finalModel === 'V4') {
$maxDuration = 240; // 4 minutes max
} else {
// V4_5, V4_5PLUS, V5 all support up to 8 minutes
$maxDuration = 480; // 8 minutes max
}
// If requested duration exceeds model limit, automatically upgrade to a model that supports it
// This ensures users get the duration they requested
if ($finalDuration > $maxDuration) {
if ($finalModel === 'V3_5' || $finalModel === 'V4') {
// Upgrade to V4_5PLUS which supports up to 8 minutes
$finalModel = 'V4_5PLUS';
$maxDuration = 480;
error_log("WARNING: Duration ($finalDuration) exceeds model limit for $finalModel, automatically upgrading to V4_5PLUS to support requested duration");
} else {
// Already using a model that should support 8 minutes, but duration still exceeds
// Cap it to the max
error_log("WARNING: Duration ($finalDuration) exceeds model limit ($maxDuration) for $finalModel, capping to $maxDuration");
$finalDuration = $maxDuration;
}
}
// Ensure duration is at least 60 seconds (1 minute) and within model limits
// Default to 6 minutes (360 seconds) - in the 5-7 minute range
if (empty($finalDuration) || $finalDuration < 60) {
error_log("WARNING: Invalid duration ($finalDuration), defaulting to 360 seconds (6 minutes)");
$finalDuration = 360;
}
// For Simple Mode: Smart duration handling with keyword detection and 5-7 minute range
// This provides optimal song length for Simple Mode users
$isSimpleMode = empty($advancedPrompt) && empty($proPrompt);
if ($isSimpleMode) {
$minSimpleDuration = 300; // 5 minutes
$maxSimpleDuration = 420; // 7 minutes
$defaultSimpleDuration = 360; // 6 minutes (middle of range)
// Smart keyword detection for duration preference (Option 4)
$promptLower = strtolower($finalPrompt);
$durationKeyword = null;
// Check for "short" keywords - prefer 5 minutes
if (preg_match('/\b(short|brief|quick|fast|minimal|compact)\b/i', $promptLower)) {
$durationKeyword = 'short';
error_log("Simple Mode: Detected 'short' keyword in prompt, preferring 5 minutes");
}
// Check for "long" keywords - prefer 7 minutes
elseif (preg_match('/\b(long|extended|full|complete|extensive|lengthy)\b/i', $promptLower)) {
$durationKeyword = 'long';
error_log("Simple Mode: Detected 'long' keyword in prompt, preferring 7 minutes");
}
// Priority order: 1) Extracted duration from prompt, 2) Keyword preference, 3) Default
// If duration was extracted from prompt, clamp it to 5-7 minutes (Option 3)
if (!empty($extractedMetadata['duration'])) {
$extractedDuration = $extractedMetadata['duration'];
if ($extractedDuration < $minSimpleDuration) {
error_log("Simple Mode: Extracted duration ($extractedDuration) below 5 minutes, clamping to $minSimpleDuration seconds (5 minutes)");
$finalDuration = $minSimpleDuration;
} elseif ($extractedDuration > $maxSimpleDuration) {
error_log("Simple Mode: Extracted duration ($extractedDuration) above 7 minutes, clamping to $maxSimpleDuration seconds (7 minutes)");
$finalDuration = $maxSimpleDuration;
} else {
// Extracted duration is already in range, use it
$finalDuration = $extractedDuration;
error_log("Simple Mode: Using extracted duration ($extractedDuration) which is within 5-7 minute range");
}
}
// If no explicit duration was extracted but we have a keyword preference, use it (Option 4)
elseif ($durationKeyword !== null) {
if ($durationKeyword === 'short') {
$finalDuration = $minSimpleDuration; // 5 minutes
error_log("Simple Mode: Using keyword preference 'short' -> 5 minutes (300 seconds)");
} elseif ($durationKeyword === 'long') {
$finalDuration = $maxSimpleDuration; // 7 minutes
error_log("Simple Mode: Using keyword preference 'long' -> 7 minutes (420 seconds)");
}
}
// If no duration extracted and no keyword preference, use default 6 minutes
else {
// Only set default if finalDuration is still at the generic default (360) or invalid
if ($finalDuration == 360 || $finalDuration < 60) {
$finalDuration = $defaultSimpleDuration;
error_log("Simple Mode: No duration specified, using default 6 minutes (360 seconds)");
} else {
// finalDuration was set from form, but clamp it to 5-7 minutes
if ($finalDuration < $minSimpleDuration) {
error_log("Simple Mode: Form duration ($finalDuration) below 5 minutes, adjusting to $minSimpleDuration seconds");
$finalDuration = $minSimpleDuration;
} elseif ($finalDuration > $maxSimpleDuration) {
error_log("Simple Mode: Form duration ($finalDuration) above 7 minutes, adjusting to $maxSimpleDuration seconds");
$finalDuration = $maxSimpleDuration;
}
}
}
// Final safety clamp - ensure it's always in 5-7 minute range regardless of how we got here
if ($finalDuration < $minSimpleDuration) {
error_log("Simple Mode: Final safety clamp - Duration ($finalDuration) below 5 minutes, adjusting to $minSimpleDuration seconds");
$finalDuration = $minSimpleDuration;
} elseif ($finalDuration > $maxSimpleDuration) {
error_log("Simple Mode: Final safety clamp - Duration ($finalDuration) above 7 minutes, adjusting to $maxSimpleDuration seconds");
$finalDuration = $maxSimpleDuration;
}
error_log("Simple Mode: Final duration set to $finalDuration seconds (" . round($finalDuration / 60, 1) . " minutes)");
}
error_log("Final duration being sent to API: $finalDuration seconds (" . round($finalDuration / 60, 1) . " minutes) for model $finalModel (max: $maxDuration)");
// Validate input - but this should never be empty at this point due to fallback above
// If it is empty here, something went wrong with the fallback logic
if (empty($finalPrompt)) {
error_log("create_music.php: Validation failed - empty prompt after all processing");
error_log("DEBUG: prompt='$prompt', advancedPrompt='$advancedPrompt', proPrompt='$proPrompt', genre='$genre', proGenre='$proGenre'");
// Don't show error - just use a default prompt to prevent blocking
$finalPrompt = !empty($genre) ? $genre : (!empty($proGenre) ? $proGenre : 'Electronic music track');
error_log("Using emergency fallback prompt: '$finalPrompt'");
}
// Calculate credit cost - ALL MODES COST 1 CREDIT
$creditCost = 1; // All modes (Simple, Advanced, Pro) cost 1 credit
error_log("create_music.php: Credit cost set to 1 credit for all modes");
// Check if user has subscription (monthly limit) or needs credits
require_once __DIR__ . '/utils/subscription_helpers.php';
$track_check = canCreateTrack($_SESSION['user_id']);
// CRITICAL LOGGING: Track which system is being used
error_log("create_music.php: canCreateTrack() result for user {$_SESSION['user_id']}: " . json_encode($track_check));
if (!$track_check['allowed']) {
// User has reached monthly limit or subscription issue
error_log("Track creation blocked for user {$_SESSION['user_id']}: " . ($track_check['message'] ?? 'Monthly limit reached'));
$_SESSION['error'] = $track_check['message'] ?? "You've reached your monthly track limit. Please wait for next month or upgrade your plan.";
header('Location: index.php#create');
exit;
}
// If user is on subscription system, we'll increment usage after track creation
// Otherwise, check credits for credit-based plans
if (!isset($track_check['system']) || $track_check['system'] !== 'credits') {
// User is on subscription - monthly limit already checked, will increment after creation
$system_used = $track_check['system'] ?? 'subscription (default)';
error_log("create_music.php: User on subscription plan, monthly limit check passed. System: {$system_used}, tracks_used: " . ($track_check['tracks_used'] ?? 'N/A') . ", track_limit: " . ($track_check['track_limit'] ?? 'N/A'));
} else {
// User is on credit system - check credits
error_log("create_music.php: User on credit system. Checking user credits, cost: $creditCost");
$stmt = $pdo->prepare("SELECT credits, plan FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Ensure credits is an integer for comparison
$userCredits = isset($user['credits']) ? (int)$user['credits'] : 0;
error_log("create_music.php: User credits check complete, user credits: $userCredits (raw: " . ($user['credits'] ?? 'N/A') . ")");
if (!$user || $userCredits < $creditCost) {
error_log("Insufficient credits for user {$_SESSION['user_id']}: has $userCredits, needs $creditCost");
$_SESSION['error'] = "Insufficient credits. You need $creditCost credits to generate music in this mode. Please <a href='/pricing.php' style='color: #667eea; text-decoration: underline;'>purchase credits</a> to continue.";
$_SESSION['error_allow_html'] = true; // Flag to allow HTML in error message
header('Location: index.php#create');
exit;
}
}
// Create track record
$temp_task_id = 'temp_' . time() . '_' . $_SESSION['user_id'] . '_' . uniqid();
// Determine final genre that will be sent to API (we'll set this after genre extraction)
$finalGenreForMetadata = $genre ?: $proGenre ?: null;
try {
$metadata = json_encode([
'customMode' => $customMode === 'true',
'model_name' => $finalModel,
'duration' => $finalDuration,
'genre' => $finalGenreForMetadata, // Will be updated after genre extraction
'key' => $key,
'tempo' => $tempo,
'mood' => $mood,
'instrumental' => $instrumental,
'energy' => $energy,
'excitement' => $excitement,
'scale' => $scale,
'tags' => $tags,
'timeSignature' => $timeSignature,
'language' => $language,
'voiceType' => $voiceType,
'useCase' => $useCase,
'instruments' => $instruments,
// Pro Mode fields
'proTitle' => $proTitle,
'proModel' => $proModel,
'proVariations' => $proVariations,
'proKey' => $proKey,
'proScale' => $proScale,
'proTimeSignature' => $proTimeSignature,
'proTempo' => $proTempo,
'proChordProgression' => $proChordProgression,
'proOctave' => $proOctave,
'proGenre' => $proGenre,
'proSubGenre' => $proSubGenre,
'proDecade' => $proDecade,
'proLeadInstrument' => $proLeadInstrument,
'proRhythmSection' => $proRhythmSection,
'proHarmonySection' => $proHarmonySection,
'proArrangement' => $proArrangement,
'proComplexity' => $proComplexity,
'proDensity' => $proDensity,
'proReverb' => $proReverb,
'proCompression' => $proCompression,
'proStereoWidth' => $proStereoWidth,
'proBassLevel' => $proBassLevel,
'proMidLevel' => $proMidLevel,
'proTrebleLevel' => $proTrebleLevel,
'proVoiceType' => $proVoiceType,
'proLanguage' => $proLanguage,
'proVocalStyle' => $proVocalStyle,
'proLyricTheme' => $proLyricTheme,
'proRhymeScheme' => $proRhymeScheme,
'proHookFrequency' => $proHookFrequency,
'proIntroLength' => $proIntroLength,
'proVerseChorusRatio' => $proVerseChorusRatio,
'proBridge' => $proBridge,
'proOutroStyle' => $proOutroStyle,
'proBuildUps' => $proBuildUps,
'proTransitions' => $proTransitions,
'proQuality' => $proQuality,
'proMood' => $proMood,
'proEnergy' => $proEnergy,
'proExcitement' => $proExcitement,
'proDanceability' => $proDanceability,
'proPrompt' => $proPrompt,
'proTags' => $proTags
], (defined('JSON_UNESCAPED_UNICODE') ? JSON_UNESCAPED_UNICODE : 0) | (defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0));
if ($metadata === false) {
$error_msg = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Unknown JSON error (code: ' . json_last_error() . ')';
throw new Exception('Failed to encode metadata: ' . $error_msg);
}
} catch (Exception $e) {
error_log("Metadata encoding error: " . $e->getMessage());
$_SESSION['error'] = 'Error processing your request. Please try again.';
header('Location: index.php#create');
exit;
}
error_log("create_music.php: Creating track record in database");
error_log("create_music.php: user_id: " . $_SESSION['user_id']);
error_log("create_music.php: temp_task_id: $temp_task_id");
error_log("create_music.php: finalTitle length: " . strlen($finalTitle));
error_log("create_music.php: finalPrompt length: " . strlen($finalPrompt));
error_log("create_music.php: metadata length: " . strlen($metadata));
try {
$stmt = $pdo->prepare("
INSERT INTO music_tracks (user_id, task_id, title, prompt, music_type, status, metadata, is_public, created_at)
VALUES (?, ?, ?, ?, 'music', 'processing', ?, 0, NOW())
");
$result = $stmt->execute([$_SESSION['user_id'], $temp_task_id, $finalTitle, $finalPrompt, $metadata]);
if (!$result) {
$errorInfo = $stmt->errorInfo();
error_log("Database insert failed - SQL error: " . print_r($errorInfo, true));
throw new Exception('Database insert failed: ' . ($errorInfo[2] ?? 'Unknown SQL error'));
}
$track_id = $pdo->lastInsertId();
if (!$track_id) {
$errorInfo = $stmt->errorInfo();
error_log("lastInsertId returned false/0 - SQL error: " . print_r($errorInfo, true));
throw new Exception('Failed to create track record - lastInsertId returned false/0. SQL Error: ' . ($errorInfo[2] ?? 'Unknown'));
}
error_log("create_music.php: Track record created successfully, track_id: $track_id");
} catch (PDOException $e) {
error_log("Database insert PDO error: " . $e->getMessage());
error_log("Database insert PDO error code: " . $e->getCode());
error_log("Database insert error trace: " . $e->getTraceAsString());
$_SESSION['error'] = 'Error saving your track: ' . htmlspecialchars($e->getMessage()) . ' Please try again.';
ob_end_clean();
header('Location: index.php#create');
exit;
} catch (Exception $e) {
error_log("Database insert error: " . $e->getMessage());
error_log("Database insert error trace: " . $e->getTraceAsString());
$_SESSION['error'] = 'Error saving your track: ' . htmlspecialchars($e->getMessage()) . ' Please try again.';
ob_end_clean();
header('Location: index.php#create');
exit;
}
// CRITICAL: DO NOT deduct credits yet - wait for successful API response
// Credits will be deducted AFTER API confirms track creation
$shouldDeductCredits = (!isset($track_check['system']) || $track_check['system'] === 'credits');
$shouldIncrementUsage = (!isset($track_check['system']) || $track_check['system'] !== 'credits');
// CRITICAL LOGGING: Track which system will be used
error_log("create_music.php: System decision for user {$_SESSION['user_id']}: shouldDeductCredits=" . ($shouldDeductCredits ? 'true' : 'false') . ", shouldIncrementUsage=" . ($shouldIncrementUsage ? 'true' : 'false') . ", track_check system: " . ($track_check['system'] ?? 'not set'));
// Call the music generation API
$api_url = 'https://api.api.box/api/v1/generate';
// Build API data - start with prompt, we'll set style later if needed
// NOTE: API.box playground might use boolean values instead of strings - let's try both formats
$isCustomModeBool = ($customMode === 'true' || $customMode === true);
// CRITICAL: Check instrumental value - it should be 'true' string from form, or actual boolean true
$isInstrumentalBool = ($instrumental === 'true' || $instrumental === true || $instrumental === 1 || $instrumental === '1');
error_log("create_music.php: Instrumental conversion - input: '$instrumental', boolean: " . ($isInstrumentalBool ? 'TRUE' : 'FALSE'));
$api_data = [
'prompt' => $finalPrompt,
'model' => $finalModel,
'customMode' => $isCustomModeBool, // Use boolean instead of string - might improve quality
'instrumental' => $isInstrumentalBool, // Use boolean instead of string - CRITICAL for API.box
'callBackUrl' => 'https://soundstudiopro.com/callback.php'
];
error_log("create_music.php: API data instrumental value: " . ($api_data['instrumental'] ? 'TRUE (boolean)' : 'FALSE (boolean)'));
// DO NOT set style here - it will be set in customMode section if needed
// This prevents "Pop" from being set as default
// Duration: ALWAYS send duration to API.box
// NOTE: API.box may sometimes return shorter songs than requested, especially for certain styles
// This is an API limitation, not a code issue - we always send the requested duration
// CRITICAL: Ensure duration is an integer (not string) for API compatibility
$api_data['duration'] = (int)$finalDuration;
error_log("Setting duration in api_data: " . $api_data['duration'] . " seconds (type: " . gettype($api_data['duration']) . ")");
if (!empty($proPrompt)) {
error_log("PRO MODE: Duration for pro mode with lyrics: " . $api_data['duration'] . " seconds (" . round($api_data['duration'] / 60, 1) . " minutes)");
}
// According to API.box docs:
// - If customMode=true AND instrumental=true: style and title are REQUIRED
// - If customMode=true AND instrumental=false: style, prompt, and title are REQUIRED
// - If customMode=false: only prompt is required, other params should be empty
// Already set above as boolean
$isCustomMode = $isCustomModeBool;
$isInstrumental = $isInstrumentalBool;
if ($isCustomMode) {
// Custom mode: style and title are REQUIRED per API.box docs
// Priority: 1) Form genre field, 2) Pro genre field, 3) Extract from structured metadata, 4) Extract from prompt, 5) Default
$extractedGenre = '';
// First, try to use style from extracted metadata (if structured format was used)
if (!empty($extractedMetadata['style'])) {
$styleText = $extractedMetadata['style'];
error_log("Using Style from extracted metadata: '$styleText'");
$styleLower = strtolower($styleText);
// Look for multi-word genres first (higher priority) - check for exact matches
$multiWordGenres = [
'deep house', 'progressive house', 'tech house', 'deep techno', 'ambient techno',
'psy trance', 'psytrance', 'psy chill', 'drum and bass', 'drum & bass',
'hip hop', 'r&b', 'lo-fi', 'lofi', 'trip hop', 'deep techno', 'ambient house'
];
foreach ($multiWordGenres as $multiGenre) {
if (strpos($styleLower, $multiGenre) !== false) {
$extractedGenre = ucwords($multiGenre);
error_log("Extracted multi-word genre from metadata: '$extractedGenre'");
break;
}
}
// If no multi-word genre found, look for single-word genres in order of preference
if (empty($extractedGenre)) {
// Priority order: house, techno, trance, ambient, electronic, dance
// Removed 'experimental' - too broad and often incorrectly matched
$singleWordGenres = ['house', 'techno', 'trance', 'ambient', 'electronic', 'dance', 'progressive', 'chill', 'dubstep', 'trap', 'edm', 'pop', 'rock', 'jazz', 'reggae', 'country', 'folk', 'blues', 'funk', 'disco', 'metal', 'punk', 'soul', 'gospel', 'latin', 'world'];
// Check for "deep" followed by a genre (e.g., "deep house", "deep techno")
if (preg_match('/deep\s+(house|techno|trance|ambient)/i', $styleText, $deepMatch)) {
$extractedGenre = 'Deep ' . ucwords($deepMatch[1]);
error_log("Extracted 'Deep' genre from metadata: '$extractedGenre'");
} else {
// Look for genre keywords in the style text
foreach ($singleWordGenres as $genre) {
// Use word boundaries to match whole words
if (preg_match('/\b' . preg_quote($genre, '/') . '\b/i', $styleText)) {
$extractedGenre = ucwords($genre);
error_log("Extracted single-word genre from metadata: '$extractedGenre'");
break;
}
}
}
}
}
// If no genre extracted from metadata, try to extract from prompt (e.g., "Style: deep house ambient techno")
// Handle formats like: "Style: deepmale vocals deep house ambient techno tone drone dance electronic"
if (empty($extractedGenre) && preg_match('/style[:\s]+([^:|\n]+?)(?:\s*[:\-|]|$)/i', $finalPrompt, $matches)) {
$styleText = trim($matches[1]);
error_log("Found Style field in prompt: '$styleText'");
$styleLower = strtolower($styleText);
// Look for multi-word genres first (higher priority) - check for exact matches
$multiWordGenres = [
'deep house', 'progressive house', 'tech house', 'deep techno', 'ambient techno',
'psy trance', 'psytrance', 'psy chill', 'drum and bass', 'drum & bass',
'hip hop', 'r&b', 'lo-fi', 'lofi', 'trip hop', 'deep techno', 'ambient house'
];
foreach ($multiWordGenres as $multiGenre) {
if (strpos($styleLower, $multiGenre) !== false) {
$extractedGenre = ucwords($multiGenre);
error_log("Extracted multi-word genre from prompt: '$extractedGenre'");
break;
}
}
// If no multi-word genre found, look for single-word genres in order of preference
if (empty($extractedGenre)) {
// Priority order: house, techno, trance, ambient, electronic, dance
// Removed 'experimental' - too broad and often incorrectly matched
$singleWordGenres = ['house', 'techno', 'trance', 'ambient', 'electronic', 'dance', 'progressive', 'chill', 'dubstep', 'trap', 'edm', 'pop', 'rock', 'jazz', 'reggae', 'country', 'folk', 'blues', 'funk', 'disco', 'metal', 'punk', 'soul', 'gospel', 'latin', 'world'];
// Check for "deep" followed by a genre (e.g., "deep house", "deep techno")
if (preg_match('/deep\s+(house|techno|trance|ambient)/i', $styleText, $deepMatch)) {
$extractedGenre = 'Deep ' . ucwords($deepMatch[1]);
error_log("Extracted 'Deep' genre from prompt: '$extractedGenre'");
} else {
// Look for genre keywords in the style text
foreach ($singleWordGenres as $genre) {
// Use word boundaries to match whole words
if (preg_match('/\b' . preg_quote($genre, '/') . '\b/i', $styleText)) {
$extractedGenre = ucwords($genre);
error_log("Extracted single-word genre from prompt: '$extractedGenre'");
break;
}
}
}
}
if (!empty($extractedGenre)) {
error_log("Final extracted genre from prompt Style field: '$extractedGenre'");
}
}
// Use provided genre, otherwise extracted genre, otherwise default
if (!empty($genre)) {
$api_data['style'] = ucfirst($genre);
error_log("Using genre from form field: '$genre' -> '" . $api_data['style'] . "'");
} elseif (!empty($proGenre)) {
// Convert proGenre to proper format for API (e.g., "pop" -> "Pop", "hip-hop" -> "Hip-Hop")
$styleGenre = $proGenre;
// Handle special cases with hyphens
if (strpos($styleGenre, '-') !== false) {
$parts = explode('-', $styleGenre);
$styleGenre = implode('-', array_map('ucfirst', $parts));
} else {
$styleGenre = ucfirst($styleGenre);
}
$api_data['style'] = $styleGenre;
error_log("Using genre from pro form field: '$proGenre' -> '" . $api_data['style'] . "'");
} elseif (!empty($extractedGenre)) {
$api_data['style'] = $extractedGenre;
error_log("Using extracted genre from prompt: '$extractedGenre'");
} else {
// Default to "Pop" for pro mode (same as basic mode for consistency)
$api_data['style'] = 'Pop';
error_log("No genre provided in pro mode, using default style: Pop");
}
// Title is also required in custom mode
// API requires title to be max 80 characters - truncate if needed
if (!empty($finalTitle)) {
// Truncate title to 80 characters (using mb functions for proper multi-byte handling)
$titleLength = function_exists('mb_strlen') ? mb_strlen($finalTitle, 'UTF-8') : strlen($finalTitle);
if ($titleLength > 80) {
$api_data['title'] = function_exists('mb_substr') ? mb_substr($finalTitle, 0, 80, 'UTF-8') : substr($finalTitle, 0, 80);
error_log("WARNING: Title exceeded 80 characters ($titleLength), truncated to: '" . $api_data['title'] . "'");
} else {
$api_data['title'] = $finalTitle;
}
} else {
// API requires title in custom mode, generate one from prompt (max 80 chars per docs)
// Try to create a meaningful title instead of just truncating
// First, try to extract key phrases from the prompt
$generatedTitle = '';
// Look for genre/mood/theme keywords
$keywords = [];
// Extract genre mentions (e.g., "Genre : Électro-chill", "Genre: Pop")
if (preg_match('/genre\s*[:\-]?\s*([^\n•]+)/i', $finalPrompt, $matches)) {
$genrePart = trim($matches[1]);
// Clean up genre (remove "tropical", "ensoleillée" etc, keep main genre)
$genrePart = preg_replace('/\s*(tropical|ensoleillée|douce|légère).*$/i', '', $genrePart);
$genrePart = trim($genrePart);
if (!empty($genrePart) && mb_strlen($genrePart) <= 30) {
$keywords[] = $genrePart;
}
}
// Extract mood/ambiance (e.g., "Ambiance : Île tropicale", "Humeur : Romantique")
if (preg_match('/(?:ambiance|humeur|mood)\s*[:\-]?\s*([^\n•]+)/i', $finalPrompt, $matches)) {
$moodPart = trim($matches[1]);
// Take first meaningful word/phrase (up to 2-3 words)
$moodWords = preg_split('/[,\s]+/', $moodPart);
$moodPhrase = implode(' ', array_slice($moodWords, 0, 2));
if (!empty($moodPhrase) && mb_strlen($moodPhrase) <= 25) {
$keywords[] = $moodPhrase;
}
}
// Extract location/theme (e.g., "Île tropicale", "La Isla Bonita", "inspiré de")
if (preg_match('/inspiré\s+de\s+["\']([^"\']+)["\']/i', $finalPrompt, $matches)) {
// Extract song/theme name from "inspiré de" pattern
$themeName = trim($matches[1]);
if (!empty($themeName) && mb_strlen($themeName) <= 30) {
$keywords[] = $themeName;
}
} elseif (preg_match('/\b(isla|île|island|tropical|plage|beach)\s+([^\n•]{0,20})/i', $finalPrompt, $matches)) {
$locationPart = trim($matches[0]);
if (!empty($locationPart) && mb_strlen($locationPart) <= 25) {
$keywords[] = $locationPart;
}
}
// If we found keywords, combine them into a title
if (!empty($keywords)) {
$generatedTitle = implode(' • ', array_slice($keywords, 0, 2)); // Max 2 keywords
} else {
// Fallback: extract first meaningful sentence or phrase (not just first 77 chars)
// Try to find a complete phrase or sentence
$firstSentence = preg_split('/[.!?\n•]/', $finalPrompt)[0];
$firstSentence = trim($firstSentence);
// Remove common prefixes
$firstSentence = preg_replace('/^(🎶\s*)?(prompt|original prompt|description|musical|inspiré)[:\s]+/i', '', $firstSentence);
$firstSentence = trim($firstSentence);
// If first sentence is too long, try to extract key words
if (mb_strlen($firstSentence) > 60) {
// Extract first 3-4 meaningful words
$words = preg_split('/\s+/', $firstSentence);
$keyWords = array_slice($words, 0, 4);
$firstSentence = implode(' ', $keyWords);
}
$generatedTitle = $firstSentence ?: 'Generated Track';
}
// Clean up title
$generatedTitle = trim($generatedTitle);
$generatedTitle = preg_replace('/\s+/', ' ', $generatedTitle); // Remove extra spaces
$generatedTitle = $generatedTitle ?: 'Generated Track';
// Ensure generated title is max 80 characters (API requirement)
$genTitleLength = function_exists('mb_strlen') ? mb_strlen($generatedTitle, 'UTF-8') : strlen($generatedTitle);
if ($genTitleLength > 80) {
// Smart truncation: try to cut at a word boundary
$truncated = function_exists('mb_substr') ? mb_substr($generatedTitle, 0, 77, 'UTF-8') : substr($generatedTitle, 0, 77);
// Remove partial word at the end
$truncated = preg_replace('/\s+\S*$/', '', $truncated);
$api_data['title'] = $truncated;
error_log("Generated title exceeded 80 characters ($genTitleLength), truncated to: '" . $api_data['title'] . "'");
} else {
$api_data['title'] = $generatedTitle;
}
error_log("Generated title from prompt: '" . $api_data['title'] . "' (length: " . (function_exists('mb_strlen') ? mb_strlen($api_data['title'], 'UTF-8') : strlen($api_data['title'])) . ")");
}
} else {
// Non-custom mode: Based on successful API requests, we can send style even in non-custom mode
// The successful format shows: customMode:false, style:"Pop", title:""
// IMPORTANT: Always use "Pop" as style in non-custom mode - the API extracts keywords from prompt
// The style parameter doesn't need to match the prompt keywords - "Pop" works best
$api_data['style'] = 'Pop';
error_log("Non-custom mode: Using 'Pop' as style (API will extract keywords from prompt)");
// Send empty title string (not unset) to match successful request format
$api_data['title'] = '';
}
// CRITICAL: API.box ONLY accepts these parameters:
// - prompt, model, style (if customMode=true), title (if customMode=true), customMode, instrumental, duration, callBackUrl
// DO NOT send any other parameters - they interfere with the API and degrade quality!
// Remove any parameters that API.box doesn't support
$allowedParams = ['prompt', 'model', 'style', 'title', 'customMode', 'instrumental', 'duration', 'callBackUrl'];
$clean_api_data = [];
foreach ($allowedParams as $param) {
if (isset($api_data[$param])) {
$clean_api_data[$param] = $api_data[$param];
}
}
// CRITICAL: Ensure duration is ALWAYS included (even if somehow missing)
if (!isset($clean_api_data['duration'])) {
$clean_api_data['duration'] = $finalDuration;
error_log("WARNING: Duration was missing from clean_api_data, adding it: $finalDuration");
}
// CRITICAL: Ensure instrumental is ALWAYS included (even if somehow missing)
if (!isset($clean_api_data['instrumental'])) {
$clean_api_data['instrumental'] = $isInstrumentalBool;
error_log("WARNING: Instrumental was missing from clean_api_data, adding it: " . ($isInstrumentalBool ? 'TRUE' : 'FALSE'));
}
// CRITICAL: Ensure title is NEVER over 80 characters (API requirement)
if (isset($clean_api_data['title'])) {
$titleLength = function_exists('mb_strlen') ? mb_strlen($clean_api_data['title'], 'UTF-8') : strlen($clean_api_data['title']);
if ($titleLength > 80) {
$clean_api_data['title'] = function_exists('mb_substr') ? mb_substr($clean_api_data['title'], 0, 80, 'UTF-8') : substr($clean_api_data['title'], 0, 80);
error_log("CRITICAL: Title was over 80 characters ($titleLength), truncated to 80: '" . $clean_api_data['title'] . "'");
}
error_log("Final title being sent to API: '" . $clean_api_data['title'] . "' (length: " . (function_exists('mb_strlen') ? mb_strlen($clean_api_data['title'], 'UTF-8') : strlen($clean_api_data['title'])) . ")");
}
$api_data = $clean_api_data;
error_log("create_music.php: Final API data instrumental value: " . (isset($api_data['instrumental']) ? ($api_data['instrumental'] ? 'TRUE (boolean)' : 'FALSE (boolean)') : 'NOT SET'));
// Log critical information
error_log("=== API REQUEST SUMMARY ===");
error_log("Prompt: " . substr($finalPrompt, 0, 100) . (strlen($finalPrompt) > 100 ? '...' : ''));
error_log("Duration: $finalDuration seconds (" . round($finalDuration / 60, 1) . " minutes)");
error_log("Duration in api_data: " . (isset($api_data['duration']) ? $api_data['duration'] : 'NOT SET'));
error_log("CustomMode: " . ($customMode === 'true' || $customMode === true ? 'TRUE' : 'FALSE'));
error_log("Instrumental: " . ($isInstrumentalBool ? 'TRUE (boolean)' : 'FALSE (boolean)'));
error_log("Model: $finalModel");
error_log("Style: " . (isset($api_data['style']) ? $api_data['style'] : 'NOT SET'));
error_log("Title: " . (isset($api_data['title']) ? $api_data['title'] : 'NOT SET'));
error_log("Duration: " . (isset($api_data['duration']) ? $api_data['duration'] : 'NOT SET'));
error_log("===========================");
// Log the exact API data being sent (clean version)
$api_data_log = [
'timestamp' => date('Y-m-d H:i:s'),
'customMode' => $customMode,
'customMode_sent' => $isCustomModeBool ? 'true (boolean)' : 'false (boolean)',
'duration' => $finalDuration,
'prompt_length' => $promptLength,
'full_request' => $api_data
];
$api_data_log = "[" . date('Y-m-d H:i:s') . "] API Request Data:\n" . json_encode($api_data_log, JSON_PRETTY_PRINT) . "\n";
file_put_contents(__DIR__ . '/create_music_debug.log', $api_data_log, FILE_APPEND);
error_log("API Request - customMode: " . ($isCustomModeBool ? 'TRUE (boolean)' : 'FALSE (boolean)') . ", duration: $finalDuration, prompt_length: $promptLength");
error_log("API Request Data (CLEAN - only API.box supported params): " . json_encode($api_data));
error_log("API Request - Instrumental value in JSON: " . (isset($api_data['instrumental']) ? json_encode($api_data['instrumental']) : 'NOT SET'));
// NOTE: All unsupported parameters have been removed above.
// API.box ONLY accepts: prompt, model, style, title, customMode, instrumental, duration, callBackUrl
// Any other parameters will interfere with the API and degrade music quality.
$api_key = '63edba40620216c5aa2c04240ac41dbd';
// Encode API data with proper UTF-8 handling and preserve special characters
// Use JSON flags only if available (PHP version compatibility)
$json_flags = 0;
if (defined('JSON_UNESCAPED_UNICODE')) {
$json_flags |= JSON_UNESCAPED_UNICODE;
}
if (defined('JSON_UNESCAPED_SLASHES')) {
$json_flags |= JSON_UNESCAPED_SLASHES;
}
if (defined('JSON_PARTIAL_OUTPUT_ON_ERROR')) {
$json_flags |= JSON_PARTIAL_OUTPUT_ON_ERROR;
}
try {
$json_data = json_encode($api_data, $json_flags);
if ($json_data === false) {
$error_msg = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Unknown JSON error (code: ' . json_last_error() . ')';
error_log("JSON encoding error: " . $error_msg);
error_log("API data that failed to encode: " . print_r($api_data, true));
throw new Exception("JSON encoding failed: " . $error_msg);
}
} catch (Exception $e) {
error_log("JSON encoding exception: " . $e->getMessage());
$_SESSION['error'] = 'Error encoding music parameters. Please try again with simpler text.';
header('Location: index.php#create');
exit;
}
// Log the encoded data for debugging (truncate if too long)
$log_data = strlen($json_data) > 1000 ? substr($json_data, 0, 1000) . '...' : $json_data;
error_log("API request URL: $api_url");
error_log("API request JSON (first 1000 chars): " . $log_data);
error_log("API request full data structure: " . print_r($api_data, true));
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $api_key,
'Content-Type: application/json',
'User-Agent: SoundStudioPro-Music/2.0'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Total timeout
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // Connection timeout
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_ENCODING, ''); // Accept any encoding
error_log("create_music.php: Starting cURL request to API");
error_log("create_music.php: API URL: $api_url");
error_log("create_music.php: Request timeout set to 30 seconds");
$start_time = microtime(true);
$response = curl_exec($ch);
$end_time = microtime(true);
$request_duration = round($end_time - $start_time, 2);
error_log("create_music.php: cURL request completed in {$request_duration} seconds");
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
$curl_info = curl_getinfo($ch);
curl_close($ch);
error_log("create_music.php: HTTP Code: $http_code");
error_log("create_music.php: cURL Error: " . ($curl_error ?: 'None'));
error_log("create_music.php: Total time: " . ($curl_info['total_time'] ?? 'N/A') . " seconds");
// Log the API response
$api_response_log = "[" . date('Y-m-d H:i:s') . "] API Response HTTP Code: $http_code\n";
$api_response_log .= "API Response Body: " . $response . "\n";
error_log($api_response_log);
file_put_contents(__DIR__ . '/create_music_debug.log', $api_response_log, FILE_APPEND);
if ($curl_error) {
$error_msg = "[" . date('Y-m-d H:i:s') . "] create_music.php: CURL Error: $curl_error\n";
error_log($error_msg);
file_put_contents(__DIR__ . '/create_music_debug.log', $error_msg, FILE_APPEND);
$_SESSION['error'] = 'Failed to connect to music generation service. Please try again.';
ob_end_clean();
header('Location: index.php#create');
exit;
}
if ($http_code !== 200) {
$error_log_msg = "[" . date('Y-m-d H:i:s') . "] API returned non-200 status: $http_code, Response: $response\n";
error_log($error_log_msg);
file_put_contents(__DIR__ . '/create_music_debug.log', $error_log_msg, FILE_APPEND);
$_SESSION['error'] = 'Music generation service returned an error (HTTP ' . $http_code . '). Please try again.';
ob_end_clean();
header('Location: index.php#create');
exit;
}
$api_result = json_decode($response, true);
if (!$api_result) {
error_log("Failed to decode API response as JSON");
error_log("Raw response: " . $response);
error_log("Response length: " . strlen($response));
$_SESSION['error'] = 'Invalid response from music generation service. Please try again.';
header('Location: index.php#create');
exit;
}
// Log the full API response for debugging
error_log("API response decoded successfully: " . print_r($api_result, true));
// Check if API returned an error (check code field first - API returns HTTP 200 but code: 400 for errors)
if (isset($api_result['code']) && $api_result['code'] !== 200) {
$error_msg = $api_result['msg'] ?? $api_result['message'] ?? $api_result['error'] ?? 'Unknown error from API';
$api_error_code = $api_result['code'];
// Sanitize error message - remove any references to API.Box or supplier names
$error_msg = preg_replace('/\b(API\.Box|api\.box|API\.box|not found on API\.Box|not found in API\.Box|Task not found in API\.Box|Track.*not found on API\.Box)\b/i', '', $error_msg);
$error_msg = trim($error_msg);
if (empty($error_msg)) {
$error_msg = 'Generation failed';
}
// Log user's local credits for debugging
if (isset($_SESSION['user_id'])) {
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
$user_credits = $user['credits'] ?? 0;
error_log("API returned error code: $api_error_code, message: $error_msg. User has $user_credits credits in local database.");
}
// Log prompt content for debugging model errors (truncate if too long)
$promptPreview = strlen($finalPrompt) > 500 ? substr($finalPrompt, 0, 500) . '...' : $finalPrompt;
error_log("API error occurred with prompt (first 500 chars): " . $promptPreview);
// Check if prompt contains suspicious patterns that might cause issues
if (preg_match('/\.\/\.|\.\.\/\.\./', $finalPrompt)) {
error_log("WARNING: Prompt contains path-like patterns (./. or ../) that might cause API issues");
}
// CRITICAL: Update track status to 'failed' and store error metadata
$error_metadata = json_encode([
'code' => $api_error_code,
'msg' => $error_msg,
'error_type' => ($api_error_code == 400) ? 'content_violation' : ($api_error_code == 429 ? 'service_unavailable' : 'generation_failed'),
'data' => $api_result,
'timestamp' => date('Y-m-d H:i:s'),
'detected_via' => 'initial_api_response'
]);
try {
$update_stmt = $pdo->prepare("
UPDATE music_tracks
SET status = 'failed',
metadata = ?,
updated_at = NOW()
WHERE id = ?
");
$update_stmt->execute([$error_metadata, $track_id]);
error_log("✅ Track $track_id marked as FAILED due to API error code $api_error_code: $error_msg");
} catch (Exception $e) {
error_log("❌ Failed to update track $track_id to failed status: " . $e->getMessage());
}
// Special handling for 429 (insufficient credits on API side)
if ($api_error_code == 429) {
// This is an API-side credit issue, not a user account issue
// The API key doesn't have enough credits, but the user's account might be fine
error_log("API returned 429 (insufficient credits on API provider side). This is a system-level issue, not a user account issue.");
// Get user's current credits to show in the message (if available)
if (isset($user_credits) && is_numeric($user_credits)) {
// Use translation with credits parameter
$_SESSION['error'] = t('error.api.service_unavailable.message', ['credits' => $user_credits]);
} else {
// Use translation without credits parameter
$_SESSION['error'] = t('error.api.service_unavailable.message_no_credits');
}
$_SESSION['error_allow_html'] = true; // Allow HTML formatting
} else {
// For other errors, show the API's exact message (sanitized of API.Box references only)
$user_friendly_error = preg_replace('/\b(API\.Box|api\.box|API\.box|not found on API\.Box|not found in API\.Box|Task not found in API\.Box|Track.*not found on API\.Box)\b/i', '', $error_msg);
$user_friendly_error = trim($user_friendly_error);
$user_friendly_error = preg_replace('/\s+/', ' ', $user_friendly_error);
if (empty($user_friendly_error)) {
$user_friendly_error = 'Track generation failed';
}
error_log("API returned error code: $api_error_code, message: $error_msg");
$_SESSION['error'] = htmlspecialchars($user_friendly_error) . ' Your track has been marked as failed.';
}
ob_end_clean();
header('Location: index.php#create');
exit;
}
// Check if API returned an error message in other formats
$api_message = $api_result['message'] ?? $api_result['msg'] ?? '';
$has_error = isset($api_result['error']) || (isset($api_result['message']) && stripos($api_message, 'error') !== false);
if ($has_error) {
$error_msg = $api_result['error'] ?? $api_result['message'] ?? $api_result['msg'] ?? 'Unknown error';
// Sanitize error message
$error_msg = preg_replace('/\b(|||not found|not found|Task not found|Track.*not found)\b/i', '', $error_msg);
$error_msg = trim($error_msg);
if (empty($error_msg)) {
$error_msg = 'Generation failed';
}
error_log("API returned error: " . $error_msg);
// CRITICAL: Update track status to 'failed' and store error metadata
$error_metadata = json_encode([
'code' => 531,
'msg' => $error_msg,
'error_type' => 'generation_failed',
'data' => $api_result,
'timestamp' => date('Y-m-d H:i:s'),
'detected_via' => 'initial_api_response_other_format'
]);
try {
$update_stmt = $pdo->prepare("
UPDATE music_tracks
SET status = 'failed',
metadata = ?,
updated_at = NOW()
WHERE id = ?
");
$update_stmt->execute([$error_metadata, $track_id]);
error_log("✅ Track $track_id marked as FAILED due to API error: $error_msg");
} catch (Exception $e) {
error_log("❌ Failed to update track $track_id to failed status: " . $e->getMessage());
}
// Sanitize error message - remove API.Box references only
$user_friendly_error = preg_replace('/\b(API\.Box|api\.box|API\.box|not found on API\.Box|not found in API\.Box|Task not found in API\.Box|Track.*not found on API\.Box)\b/i', '', $error_msg);
$user_friendly_error = trim($user_friendly_error);
$user_friendly_error = preg_replace('/\s+/', ' ', $user_friendly_error);
if (empty($user_friendly_error)) {
$user_friendly_error = 'Track generation failed';
}
$_SESSION['error'] = htmlspecialchars($user_friendly_error) . ' Your track has been marked as failed.';
ob_end_clean();
header('Location: index.php#create');
exit;
}
// Extract task ID from response - check multiple possible locations
$real_task_id = null;
if (isset($api_result['taskId'])) {
$real_task_id = $api_result['taskId'];
} elseif (isset($api_result['id'])) {
$real_task_id = $api_result['id'];
} elseif (isset($api_result['data']['taskId'])) {
$real_task_id = $api_result['data']['taskId'];
} elseif (isset($api_result['task_id'])) {
$real_task_id = $api_result['task_id'];
} elseif (isset($api_result['data']['id'])) {
$real_task_id = $api_result['data']['id'];
} else {
// CRITICAL: API didn't return a task ID - this means the request failed
$error_log_msg = "[" . date('Y-m-d H:i:s') . "] CRITICAL ERROR: No task ID found in API response!\n";
$error_log_msg .= "Temp task ID: $temp_task_id\n";
$error_log_msg .= "API response structure: " . json_encode($api_result, JSON_PRETTY_PRINT) . "\n";
$error_log_msg .= "Full raw response: " . $response . "\n";
error_log($error_log_msg);
file_put_contents(__DIR__ . '/create_music_debug.log', $error_log_msg, FILE_APPEND);
$_SESSION['error'] = 'API did not return a task ID. The music generation request may have failed. Please try again.';
ob_end_clean();
header('Location: index.php#create');
exit;
}
error_log("API call successful. Task ID: $real_task_id, Track ID: $track_id");
// CRITICAL: Only deduct credits AFTER successful API response
// This prevents losing credits if API call fails
if ($shouldDeductCredits) {
// User is on credit system - deduct credits NOW (after API success)
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && (int)$user['credits'] >= $creditCost) {
$newCredits = (int)$user['credits'] - $creditCost;
$stmt = $pdo->prepare("UPDATE users SET credits = ? WHERE id = ?");
$stmt->execute([$newCredits, $_SESSION['user_id']]);
// Record credit transaction
// Determine mode based on form fields (all modes cost 1 credit now)
$modeDescription = 'Simple Mode'; // Default
if (!empty($_POST['proPrompt']) || !empty($_POST['proModel']) || isset($_POST['proDuration'])) {
$modeDescription = 'Pro Mode';
} elseif (!empty($_POST['advancedPrompt']) || !empty($_POST['title']) || isset($_POST['variations'])) {
$modeDescription = 'Advanced Mode';
}
$stmt = $pdo->prepare("
INSERT INTO credit_transactions (user_id, amount, type, description, created_at)
VALUES (?, ?, 'usage', 'Music track creation ($modeDescription): $finalTitle', NOW())
");
$stmt->execute([$_SESSION['user_id'], -$creditCost]);
$_SESSION['credits'] = $newCredits;
error_log("create_music.php: Deducted 1 credit AFTER API success, new balance: $newCredits");
} else {
error_log("create_music.php: WARNING - Credits insufficient after API success (race condition?)");
// This shouldn't happen, but if it does, we need to handle it
// The track was already created, so we can't rollback
// Log for investigation
}
} elseif ($shouldIncrementUsage) {
// User is on subscription - increment monthly usage AFTER API success
error_log("create_music.php: Attempting to increment monthly track usage for user {$_SESSION['user_id']}");
$increment_result = incrementMonthlyTrackUsage($_SESSION['user_id']);
if ($increment_result) {
error_log("create_music.php: Successfully incremented monthly track usage AFTER API success");
} else {
error_log("create_music.php: WARNING - Failed to increment monthly track usage for user {$_SESSION['user_id']}. Check subscription and usage record.");
}
} else {
error_log("create_music.php: NOT incrementing usage - shouldIncrementUsage=false, shouldDeductCredits=" . ($shouldDeductCredits ? 'true' : 'false') . ", track_check system: " . ($track_check['system'] ?? 'not set'));
}
// Update track with real task ID
try {
$stmt = $pdo->prepare("UPDATE music_tracks SET task_id = ? WHERE id = ?");
$stmt->execute([$real_task_id, $track_id]);
error_log("Track updated with task ID: $real_task_id");
} catch (Exception $e) {
error_log("Error updating track with task ID: " . $e->getMessage());
// Don't fail the whole request if this update fails
}
$_SESSION['success'] = t('success.music_generation.started');
$success_msg = "[" . date('Y-m-d H:i:s') . "] create_music.php: Script completed successfully, redirecting to index.php, Track ID: $track_id, Task ID: $real_task_id\n";
error_log($success_msg);
file_put_contents(__DIR__ . '/create_music_debug.log', $success_msg, FILE_APPEND);
// Clear output buffer and redirect
ob_end_clean();
header('Location: index.php#create');
exit;
?>