![]() 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/private_html/ |
<?php
// Enable error reporting for debugging
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Only set headers and handle requests if this file is being called directly (not included)
// Check if this is a direct request vs an include
$isDirectRequest = !defined('CALLBACK_INCLUDED') &&
(php_sapi_name() === 'cli' ||
(isset($_SERVER['REQUEST_METHOD']) &&
basename($_SERVER['PHP_SELF']) === 'callback.php'));
if ($isDirectRequest) {
// Set proper headers
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit(0);
}
}
// Include database configuration
require_once 'config/database.php';
// Log callback data for debugging
$logFile = 'callback_log.txt';
$timestamp = date('Y-m-d H:i:s');
$input = file_get_contents('php://input');
$headers = getallheaders();
// Log the callback
$logEntry = "[$timestamp] Callback received\n";
$logEntry .= "Headers: " . json_encode($headers) . "\n";
$logEntry .= "Body: " . $input . "\n";
$logEntry .= "Method: " . $_SERVER['REQUEST_METHOD'] . "\n";
$logEntry .= "----------------------------------------\n";
file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
// Function to properly sanitize and escape text data from DOM
function sanitizeDOMText($text) {
if (empty($text)) return $text;
// Convert to string if it's not already
$text = (string) $text;
// Handle common apostrophe issues from DOM
$text = str_replace(
['\'', '"', ''', '"', ''', '"', '’', '“', '”'],
['\'', '"', '\'', '"', '\'', '"', '\'', '"', '"'],
$text
);
// Remove any HTML entities that might cause issues
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Clean up any remaining problematic characters
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
// Ensure proper UTF-8 encoding
if (!mb_check_encoding($text, 'UTF-8')) {
$text = mb_convert_encoding($text, 'UTF-8', 'auto');
}
// Trim whitespace
$text = trim($text);
return $text;
}
// Function to sanitize array data recursively
function sanitizeDOMArray($data) {
if (is_array($data)) {
foreach ($data as $key => $value) {
if (is_string($value)) {
$data[$key] = sanitizeDOMText($value);
} elseif (is_array($value)) {
$data[$key] = sanitizeDOMArray($value);
}
}
}
return $data;
}
// Function moved to config/database.php for shared use
// Function to extract title from various API response formats
function extractTitleFromCallback($data) {
// Check multiple possible locations in the API response
// CRITICAL FIX: Also check for empty strings and trim them to ensure we don't miss valid titles
if (isset($data['title']) && !empty($data['title']) && trim($data['title']) !== '') {
return sanitizeDOMText($data['title']);
}
if (isset($data['data']['title']) && !empty($data['data']['title']) && trim($data['data']['title']) !== '') {
return sanitizeDOMText($data['data']['title']);
}
// Check in data.data array (common API.Box format)
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['title']) && !empty($item['title']) && trim($item['title']) !== '') {
return sanitizeDOMText($item['title']);
}
}
}
// Check in direct data array
if (isset($data['data']) && is_array($data['data']) && !isset($data['data']['data'])) {
foreach ($data['data'] as $item) {
if (is_array($item) && isset($item['title']) && !empty($item['title']) && trim($item['title']) !== '') {
return sanitizeDOMText($item['title']);
}
}
}
return null;
}
// Function to extract duration from various API response formats
function extractDurationFromCallback($data) {
// Check multiple possible locations in the API response
if (isset($data['duration']) && $data['duration'] !== null && $data['duration'] !== '') {
return floatval($data['duration']);
}
if (isset($data['data']['duration']) && $data['data']['duration'] !== null && $data['data']['duration'] !== '') {
return floatval($data['data']['duration']);
}
// Check in data.data array (common API.Box format)
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['duration']) && $item['duration'] !== null && $item['duration'] !== '') {
return floatval($item['duration']);
}
}
}
// Check in direct data array
if (isset($data['data']) && is_array($data['data']) && !isset($data['data']['data'])) {
foreach ($data['data'] as $item) {
if (is_array($item) && isset($item['duration']) && $item['duration'] !== null && $item['duration'] !== '') {
return floatval($item['duration']);
}
}
}
return null;
}
// Function to extract tags from various API response formats
// Collects ALL tags from ALL possible locations in the API response
function extractTagsFromCallback($data) {
$allTags = [];
$logFile = 'callback_log.txt';
// Helper function to add tags to collection
$addTags = function($tags) use (&$allTags) {
if (is_array($tags)) {
foreach ($tags as $tag) {
if (!empty(trim($tag))) {
$allTags[] = trim($tag);
}
}
} elseif (is_string($tags) && !empty(trim($tags))) {
$allTags[] = trim($tags);
}
};
// Check top-level tags
if (isset($data['tags']) && !empty($data['tags'])) {
file_put_contents($logFile, " 📍 Found tags at top level\n", FILE_APPEND | LOCK_EX);
$addTags($data['tags']);
}
// Check data.tags
if (isset($data['data']['tags']) && !empty($data['data']['tags'])) {
file_put_contents($logFile, " 📍 Found tags at data.tags\n", FILE_APPEND | LOCK_EX);
$addTags($data['data']['tags']);
}
// Check in data.data array (common API.Box format) - CHECK ALL VARIATIONS
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
file_put_contents($logFile, " 📍 Checking data.data.data array (" . count($data['data']['data']) . " items)\n", FILE_APPEND | LOCK_EX);
foreach ($data['data']['data'] as $index => $item) {
if (isset($item['tags']) && !empty($item['tags'])) {
file_put_contents($logFile, " ✅ Found tags in data.data.data[$index]\n", FILE_APPEND | LOCK_EX);
$addTags($item['tags']);
}
// Also check for tags in nested structures
if (isset($item['result']) && is_array($item['result'])) {
if (isset($item['result']['tags']) && !empty($item['result']['tags'])) {
file_put_contents($logFile, " ✅ Found tags in data.data.data[$index].result.tags\n", FILE_APPEND | LOCK_EX);
$addTags($item['result']['tags']);
}
}
}
}
// Check in direct data array
if (isset($data['data']) && is_array($data['data']) && !isset($data['data']['data'])) {
file_put_contents($logFile, " 📍 Checking direct data array\n", FILE_APPEND | LOCK_EX);
foreach ($data['data'] as $index => $item) {
if (is_array($item) && isset($item['tags']) && !empty($item['tags'])) {
file_put_contents($logFile, " ✅ Found tags in data[$index]\n", FILE_APPEND | LOCK_EX);
$addTags($item['tags']);
}
}
}
// Check result field (API.Box sometimes puts tags in result)
if (isset($data['result']) && is_array($data['result'])) {
if (isset($data['result']['tags']) && !empty($data['result']['tags'])) {
file_put_contents($logFile, " ✅ Found tags in result.tags\n", FILE_APPEND | LOCK_EX);
$addTags($data['result']['tags']);
}
}
// Check data.result
if (isset($data['data']['result']) && is_array($data['data']['result'])) {
if (isset($data['data']['result']['tags']) && !empty($data['data']['result']['tags'])) {
file_put_contents($logFile, " ✅ Found tags in data.result.tags\n", FILE_APPEND | LOCK_EX);
$addTags($data['data']['result']['tags']);
}
}
// Remove duplicates and return
$allTags = array_unique($allTags);
$allTags = array_values($allTags); // Re-index
if (!empty($allTags)) {
file_put_contents($logFile, " 🎯 Total unique tags collected: " . count($allTags) . " - " . implode(', ', $allTags) . "\n", FILE_APPEND | LOCK_EX);
// If we have multiple tags, return as array; if single string, return as array with one element
return $allTags;
}
file_put_contents($logFile, " ⚠️ No tags found in any location\n", FILE_APPEND | LOCK_EX);
return null;
}
// Function to format tags for storage in tags column
// Converts array to semicolon-separated string, preserves string format
function formatTagsForStorage($tags) {
if (!$tags) {
return null;
}
if (is_array($tags)) {
// Process each tag - if it contains semicolons, split it first
$processedTags = [];
foreach ($tags as $tag) {
$tag = trim($tag);
if (empty($tag)) continue;
// If tag contains semicolons, split it (e.g., "acoustic; deep drums" -> ["acoustic", "deep drums"])
if (strpos($tag, ';') !== false) {
$splitTags = preg_split('/\s*;\s*/', $tag);
foreach ($splitTags as $splitTag) {
$splitTag = trim($splitTag);
if (!empty($splitTag)) {
$processedTags[] = $splitTag;
}
}
} else {
$processedTags[] = $tag;
}
}
// Remove duplicates and filter out generic values
$processedTags = array_unique($processedTags);
$processedTags = array_filter($processedTags, function($tag) {
$tagLower = strtolower(trim($tag));
$genericValues = ['synthesizer', 'neutral', 'medium', 'electronic', 'c major', '4/4', 'null', 'none', ''];
return !empty($tag) && strlen($tag) > 1 && !in_array($tagLower, $genericValues);
});
if (empty($processedTags)) {
return null;
}
// Join with semicolon and space
return implode('; ', array_values($processedTags));
}
// If it's already a string, parse it to handle mixed formats
// e.g., "organic acoustic; deep drums primal energy tribal epic brass dramatic violin native flute"
$tagsString = trim($tags);
if (empty($tagsString)) {
return null;
}
// Split by semicolon first, then by spaces if needed
$tagArray = preg_split('/[,;]/', $tagsString);
$processedTags = [];
foreach ($tagArray as $tag) {
$tag = trim($tag);
if (!empty($tag) && strlen($tag) > 1) {
$tagLower = strtolower($tag);
$genericValues = ['synthesizer', 'neutral', 'medium', 'electronic', 'c major', '4/4', 'null', 'none'];
if (!in_array($tagLower, $genericValues)) {
$processedTags[] = $tag;
}
}
}
if (empty($processedTags)) {
return $tagsString; // Return original if parsing fails
}
return implode('; ', array_unique($processedTags));
}
// Function to extract model_name from various API response formats
function extractModelNameFromCallback($data) {
// Check multiple possible locations in the API response
if (isset($data['model_name']) && !empty($data['model_name'])) {
return sanitizeDOMText($data['model_name']);
}
if (isset($data['model']) && !empty($data['model'])) {
return sanitizeDOMText($data['model']);
}
if (isset($data['data']['model_name']) && !empty($data['data']['model_name'])) {
return sanitizeDOMText($data['data']['model_name']);
}
if (isset($data['data']['model']) && !empty($data['data']['model'])) {
return sanitizeDOMText($data['data']['model']);
}
// Check in data.data array (common API.Box format)
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['model_name']) && !empty($item['model_name'])) {
return sanitizeDOMText($item['model_name']);
}
if (isset($item['model']) && !empty($item['model'])) {
return sanitizeDOMText($item['model']);
}
}
}
// Check in direct data array
if (isset($data['data']) && is_array($data['data']) && !isset($data['data']['data'])) {
foreach ($data['data'] as $item) {
if (is_array($item)) {
if (isset($item['model_name']) && !empty($item['model_name'])) {
return sanitizeDOMText($item['model_name']);
}
if (isset($item['model']) && !empty($item['model'])) {
return sanitizeDOMText($item['model']);
}
}
}
}
return null;
}
// Function to extract image_url from various API response formats
function extractImageUrlFromCallback($data) {
// Check multiple possible locations in the API response
if (isset($data['image_url']) && !empty($data['image_url'])) {
return $data['image_url'];
}
if (isset($data['cover_url']) && !empty($data['cover_url'])) {
return $data['cover_url'];
}
if (isset($data['cover_image']) && !empty($data['cover_image'])) {
return $data['cover_image'];
}
if (isset($data['data']['image_url']) && !empty($data['data']['image_url'])) {
return $data['data']['image_url'];
}
if (isset($data['data']['cover_url']) && !empty($data['data']['cover_url'])) {
return $data['data']['cover_url'];
}
// Check in data.data array (common API.Box format) - get first item's image
if (isset($data['data']['data']) && is_array($data['data']['data']) && !empty($data['data']['data'])) {
$firstItem = $data['data']['data'][0];
if (isset($firstItem['image_url']) && !empty($firstItem['image_url'])) {
return $firstItem['image_url'];
}
if (isset($firstItem['cover_url']) && !empty($firstItem['cover_url'])) {
return $firstItem['cover_url'];
}
}
// Check in direct data array
if (isset($data['data']) && is_array($data['data']) && !isset($data['data']['data'])) {
foreach ($data['data'] as $item) {
if (is_array($item)) {
if (isset($item['image_url']) && !empty($item['image_url'])) {
return $item['image_url'];
}
if (isset($item['cover_url']) && !empty($item['cover_url'])) {
return $item['cover_url'];
}
}
}
}
return null;
}
// Function to download and store image files locally
function downloadAndStoreImage($imageUrl, $taskId) {
if (empty($imageUrl) || !filter_var($imageUrl, FILTER_VALIDATE_URL)) {
return null;
}
// Create image storage directory
$imageDir = 'uploads/track_covers/';
if (!is_dir($imageDir)) {
mkdir($imageDir, 0755, true);
}
// Generate filename
$extension = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'jpg';
// Validate extension
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array(strtolower($extension), $allowedExtensions)) {
$extension = 'jpg'; // Default to jpg if extension is invalid
}
$filename = "track_{$taskId}_" . time() . ".{$extension}";
$localPath = $imageDir . $filename;
$webPath = '/uploads/track_covers/' . $filename;
// Skip if file already exists (check by task_id pattern)
$existingFiles = glob($imageDir . "track_{$taskId}_*");
if (!empty($existingFiles)) {
// Use the most recent existing file
$mostRecent = end($existingFiles);
return '/' . str_replace('\\', '/', $mostRecent);
}
// Download the file using cURL for better error handling
if (function_exists('curl_init')) {
$ch = curl_init($imageUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
$imageContent = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error || $httpCode !== 200 || $imageContent === false) {
$errorMsg = "Failed to download image from: $imageUrl (HTTP $httpCode, Error: $error)";
error_log($errorMsg);
// Also log to callback log file
$logFile = 'callback_log.txt';
file_put_contents($logFile, "❌ $errorMsg for task $taskId\n", FILE_APPEND | LOCK_EX);
return null;
}
} else {
// Fallback to file_get_contents
$context = stream_context_create([
'http' => [
'timeout' => 60,
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'follow_location' => true
]
]);
$imageContent = @file_get_contents($imageUrl, false, $context);
if ($imageContent === false) {
error_log("Failed to download image from: $imageUrl");
return null;
}
}
// Validate that it's actually an image
$imageInfo = @getimagesizefromstring($imageContent);
if ($imageInfo === false) {
error_log("Downloaded content is not a valid image: $imageUrl");
return null;
}
// Save the file
if (file_put_contents($localPath, $imageContent, LOCK_EX)) {
// Set proper permissions
chmod($localPath, 0644);
// Log the download
$logFile = 'callback_log.txt';
file_put_contents($logFile, "✅ Downloaded and stored image: $webPath (from $imageUrl)\n", FILE_APPEND | LOCK_EX);
return $webPath;
} else {
error_log("Failed to save image to: $localPath");
return null;
}
}
// Function to download and store audio files locally
function downloadAndStoreAudio($audioUrl, $taskId, $type = 'main', $variationIndex = null) {
if (empty($audioUrl) || !filter_var($audioUrl, FILTER_VALIDATE_URL)) {
return null;
}
// CRITICAL FIX: musicfile.api.box URLs need .mp3 extension
// If URL doesn't have an extension, add .mp3
$parsedUrl = parse_url($audioUrl);
$path = $parsedUrl['path'] ?? '';
$extension = pathinfo($path, PATHINFO_EXTENSION);
if (empty($extension) && strpos($audioUrl, 'musicfile.api.box') !== false) {
$audioUrl .= '.mp3';
}
// Create audio storage directory
$audioDir = 'audio_files/';
if (!is_dir($audioDir)) {
mkdir($audioDir, 0755, true);
}
// Generate filename
$fileExtension = $extension ?: 'mp3';
if ($type === 'variation' && $variationIndex !== null) {
$filename = "{$taskId}_variation_{$variationIndex}.{$fileExtension}";
} elseif ($type === 'stem' && $variationIndex !== null) {
$filename = "{$taskId}_stem_{$variationIndex}.{$fileExtension}";
} else {
$filename = "{$taskId}.{$fileExtension}";
}
$localPath = $audioDir . $filename;
$webPath = '/audio_files/' . $filename;
// Download the file using cURL for better reliability
$ch = curl_init($audioUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: audio/mpeg, audio/*, */*',
'Referer: https://musicfile.api.box/'
]);
$audioContent = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($httpCode !== 200 || $audioContent === false || !empty($error) || strlen($audioContent) < 1024) {
error_log("Failed to download audio from: $audioUrl (HTTP $httpCode, Error: $error, Size: " . strlen($audioContent ?? '') . ")");
return null;
}
// Save the file
if (file_put_contents($localPath, $audioContent, LOCK_EX)) {
// Set proper permissions
chmod($localPath, 0644);
// Log the download
$downloadLog = [
'timestamp' => date('Y-m-d H:i:s'),
'action' => 'audio_downloaded',
'task_id' => $taskId,
'original_url' => $audioUrl,
'local_path' => $localPath,
'web_path' => $webPath,
'file_size' => strlen($audioContent),
'type' => $type,
'variation_index' => $variationIndex
];
$downloadLogFile = 'logs/audio_downloads.log';
file_put_contents($downloadLogFile, json_encode($downloadLog) . "\n", FILE_APPEND | LOCK_EX);
return $webPath;
}
return null;
}
// Helper function to get original prompt and title from database
function getTrackOriginalData($taskId) {
$pdo = getDBConnection();
if (!$pdo) return ['prompt' => null, 'title' => null, 'title_user_modified' => false];
try {
$stmt = $pdo->prepare("SELECT prompt, title, title_user_modified FROM music_tracks WHERE task_id = ?");
$stmt->execute([$taskId]);
$track = $stmt->fetch(PDO::FETCH_ASSOC);
if ($track) {
return [
'prompt' => $track['prompt'] ?? null,
'title' => $track['title'] ?? null,
'title_user_modified' => (bool)($track['title_user_modified'] ?? false)
];
}
} catch (Exception $e) {
error_log("Error fetching original track data: " . $e->getMessage());
}
return ['prompt' => null, 'title' => null, 'title_user_modified' => false];
}
// Function to parse prompt and extract metadata
function parsePromptForMetadata($prompt) {
if (empty($prompt)) return [];
$metadata = [];
$promptLower = strtolower($prompt);
// Extract BPM/Tempo from prompt as FALLBACK (API.box doesn't provide BPM in callbacks)
// We prioritize API BPM if available, but use prompt BPM as fallback since API doesn't provide it
// Validate BPM is in reasonable range (40-300 BPM)
if (preg_match('/bpm[:\s]+(\d+)/i', $prompt, $matches)) {
$bpm = intval($matches[1]);
if ($bpm >= 40 && $bpm <= 300) {
$metadata['bpm'] = $bpm;
}
} elseif (preg_match('/(\d+)\s*bpm/i', $prompt, $matches)) {
$bpm = intval($matches[1]);
if ($bpm >= 40 && $bpm <= 300) {
$metadata['bpm'] = $bpm;
}
} elseif (preg_match('/tempo[:\s]+(\d+)/i', $prompt, $matches)) {
$bpm = intval($matches[1]);
if ($bpm >= 40 && $bpm <= 300) {
$metadata['bpm'] = $bpm;
}
}
// Extract Genre - handle "Style:" prefix and multiple genres separated by "/"
if (preg_match('/style[:\s]+([^:]+?)(?:\s*[:\-]|$)/i', $prompt, $matches)) {
$styleText = trim($matches[1]);
// Split by "/" to get multiple genres
$styleGenres = array_map('trim', explode('/', $styleText));
foreach ($styleGenres as $styleGenre) {
$styleGenre = trim($styleGenre);
if (!empty($styleGenre)) {
// Normalize common variations
$styleGenre = str_replace(['psy ', 'psy-'], 'psy', strtolower($styleGenre));
$metadata['genre'] = ucwords($styleGenre);
break; // Use first genre found
}
}
}
// Also check for genre keywords (expanded list including psytrance, psy chill, etc.)
$genres = ['psytrance', 'psy trance', 'psy-chill', 'psy chill', 'electronic', 'house', 'techno', 'pop', 'hip hop', 'rock', 'jazz', 'classical', 'ambient', 'trance', 'dubstep', 'r&b', 'reggae', 'country', 'folk', 'blues', 'funk', 'disco', 'drum & bass', 'drum and bass', 'progressive', 'chillout', 'lofi', 'lo-fi', 'edm', 'trap', 'dance', 'indie', 'alternative', 'metal', 'punk', 'soul', 'gospel', 'latin', 'world', 'experimental'];
foreach ($genres as $genre) {
if (preg_match('/\b' . preg_quote($genre, '/') . '\b/i', $promptLower)) {
$metadata['genre'] = ucwords($genre);
break;
}
}
// Extract Mood - handle "Theme:" prefix and common mood words
if (preg_match('/theme[:\s]+([^:]+?)(?:\s*[:\-]|$)/i', $prompt, $matches)) {
$themeText = strtolower(trim($matches[1]));
// Check if theme contains mood keywords
$moods = ['happy', 'sad', 'energetic', 'chill', 'relaxing', 'aggressive', 'melancholic', 'uplifting', 'dark', 'bright', 'peaceful', 'intense', 'calm', 'excited', 'romantic', 'mysterious', 'epic', 'dramatic', 'playful', 'serious', 'neutral', 'love', 'unity', 'frequency'];
foreach ($moods as $mood) {
if (strpos($themeText, $mood) !== false) {
// Map theme words to moods
if ($mood === 'love' || $mood === 'romantic') {
$metadata['mood'] = 'Romantic';
} elseif ($mood === 'unity' || $mood === 'peaceful') {
$metadata['mood'] = 'Peaceful';
} elseif ($mood === 'frequency' || $mood === 'energetic') {
$metadata['mood'] = 'Energetic';
} else {
$metadata['mood'] = ucfirst($mood);
}
break;
}
}
}
// Also check for mood keywords directly
$moods = ['happy', 'sad', 'energetic', 'chill', 'relaxing', 'aggressive', 'melancholic', 'uplifting', 'dark', 'bright', 'peaceful', 'intense', 'calm', 'excited', 'romantic', 'mysterious', 'epic', 'dramatic', 'playful', 'serious', 'neutral'];
if (!isset($metadata['mood'])) {
foreach ($moods as $mood) {
if (preg_match('/\b' . preg_quote($mood, '/') . '\b/i', $promptLower)) {
$metadata['mood'] = ucfirst($mood);
break;
}
}
}
// Extract Key - handle "Key:" prefix and formats like "6B – D Major" or "D Major"
// First, try to extract numerical key (e.g., "6B", "1A", "12A")
if (preg_match('/key[:\s]+([0-9]+[A-G]?)\s*[–\-]?\s*([A-G][#b]?\s*(?:major|minor|maj|min))/i', $prompt, $matches)) {
$numericalKey = trim($matches[1]); // e.g., "6B"
$keyText = trim($matches[2]); // e.g., "D Major"
// Normalize
$keyText = preg_replace('/\s+/', ' ', $keyText);
$keyText = str_replace(['maj', 'min'], ['major', 'minor'], strtolower($keyText));
$metadata['key'] = ucwords($keyText);
$metadata['numerical_key'] = $numericalKey; // Store numerical key separately
} elseif (preg_match('/key[:\s]+(?:[0-9]+[A-G]?\s*[–\-]?\s*)?([A-G][#b]?\s*(?:major|minor|maj|min))/i', $prompt, $matches)) {
$keyText = trim($matches[1]);
// Normalize
$keyText = preg_replace('/\s+/', ' ', $keyText);
$keyText = str_replace(['maj', 'min'], ['major', 'minor'], strtolower($keyText));
$metadata['key'] = ucwords($keyText);
} elseif (preg_match('/\b([A-G][#b]?\s*(?:major|minor|maj|min))\b/i', $prompt, $matches)) {
$keyText = trim($matches[1]);
$keyText = preg_replace('/\s+/', ' ', $keyText);
$keyText = str_replace(['maj', 'min'], ['major', 'minor'], strtolower($keyText));
$metadata['key'] = ucwords($keyText);
}
return $metadata;
}
// Function to extract comprehensive metadata
function extractComprehensiveMetadata($data, $originalPrompt = null) {
// API.box callback structure: data.data.data[0] contains the actual track data
// Extract BPM from the nested structure first
$apiBpm = null;
$apiGenre = null;
$apiKey = null;
$apiMood = null;
// Check nested data structure (API.box format)
if (isset($data['data']['data']) && is_array($data['data']['data']) && !empty($data['data']['data'])) {
$trackData = $data['data']['data'][0]; // First variation
$apiBpm = $trackData['bpm'] ?? $trackData['tempo'] ?? null;
$apiGenre = $trackData['genre'] ?? $trackData['tags'] ?? null;
if (is_string($apiGenre)) {
// If tags is a string like "heavy bass, synth melodies...", extract first genre
$tagsArray = explode(',', $apiGenre);
$apiGenre = trim($tagsArray[0]) ?? null;
}
$apiKey = $trackData['key'] ?? $trackData['musical_key'] ?? null;
$apiMood = $trackData['mood'] ?? null;
}
// Fallback to top-level data
if (!$apiBpm) {
$apiBpm = $data['bpm'] ?? $data['tempo'] ?? null;
}
if (!$apiGenre) {
$apiGenre = $data['genre'] ?? (is_array($data['tags'] ?? null) ? ($data['tags'][0] ?? null) : $data['tags']) ?? null;
}
if (!$apiKey) {
$apiKey = $data['key'] ?? $data['musical_key'] ?? null;
}
if (!$apiMood) {
$apiMood = $data['mood'] ?? null;
}
// Log what API returned
$logFile = 'callback_log.txt';
file_put_contents($logFile, "🔍 API Metadata Check:\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " - API Genre: " . ($apiGenre ?: 'NOT PROVIDED') . "\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " - API BPM: " . ($apiBpm ?: 'NOT PROVIDED') . " (type: " . gettype($apiBpm) . ")\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " - API Key: " . ($apiKey ?: 'NOT PROVIDED') . "\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " - API Mood: " . ($apiMood ?: 'NOT PROVIDED') . "\n", FILE_APPEND | LOCK_EX);
// IMPORTANT: Check if we're accidentally using duration as BPM
if (isset($data['data']['data']) && is_array($data['data']['data']) && !empty($data['data']['data'])) {
$trackData = $data['data']['data'][0];
$duration = $trackData['duration'] ?? null;
file_put_contents($logFile, " - ⚠️ Duration from API: " . ($duration ?: 'NOT PROVIDED') . " (this is NOT BPM!)\n", FILE_APPEND | LOCK_EX);
if ($duration && $apiBpm && abs($duration - $apiBpm) < 1) {
file_put_contents($logFile, " - 🚨 ERROR: Duration ($duration) matches BPM ($apiBpm) - this is wrong!\n", FILE_APPEND | LOCK_EX);
}
}
// If API didn't provide values, try parsing the prompt
$parsedMetadata = [];
if ($originalPrompt) {
file_put_contents($logFile, " - Parsing prompt for metadata...\n", FILE_APPEND | LOCK_EX);
$parsedMetadata = parsePromptForMetadata($originalPrompt);
file_put_contents($logFile, " - Parsed from prompt: " . json_encode($parsedMetadata) . "\n", FILE_APPEND | LOCK_EX);
}
// Use API values if available, otherwise use parsed, otherwise use defaults
$finalGenre = $apiGenre ?: $parsedMetadata['genre'] ?? 'Electronic';
// BPM: API.box doesn't provide BPM in callbacks, so we use prompt BPM as fallback
// Priority: API BPM > Prompt BPM > null
$finalBpm = $apiBpm ?? $parsedMetadata['bpm'] ?? null;
$finalKey = $apiKey ?: $parsedMetadata['key'] ?? 'C major';
$finalMood = $apiMood ?: $parsedMetadata['mood'] ?? 'neutral';
// Validate BPM is in reasonable range (40-300 BPM)
if ($finalBpm !== null) {
$finalBpm = intval($finalBpm);
if ($finalBpm < 40 || $finalBpm > 300) {
file_put_contents($logFile, " - ⚠️ WARNING: Invalid BPM value ($finalBpm) - out of range 40-300, will randomize\n", FILE_APPEND | LOCK_EX);
$finalBpm = null; // Reset to null so it gets randomized
}
}
// Validate numerical key against actual API key
$finalNumericalKey = null;
if (isset($parsedMetadata['numerical_key']) && !empty($parsedMetadata['numerical_key'])) {
$promptNumericalKey = $parsedMetadata['numerical_key'];
$promptMusicalKey = $parsedMetadata['key'] ?? null;
// Normalize keys for comparison (case-insensitive, handle variations)
$normalizeKey = function($key) {
if (empty($key)) return '';
$key = strtolower(trim($key));
$key = str_replace(['maj', 'min'], ['major', 'minor'], $key);
$key = preg_replace('/\s+/', ' ', $key);
return ucwords($key);
};
$normalizedApiKey = $normalizeKey($finalKey);
$normalizedPromptKey = $normalizeKey($promptMusicalKey);
// Only use numerical key if the API's actual key matches what was in the prompt
if ($normalizedApiKey === $normalizedPromptKey) {
$finalNumericalKey = $promptNumericalKey;
file_put_contents($logFile, " - ✅ Numerical key validated: $promptNumericalKey matches API key $normalizedApiKey\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, " - ⚠️ WARNING: Numerical key mismatch!\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " Prompt said: $promptNumericalKey – $normalizedPromptKey\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " API actually generated: $normalizedApiKey\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " → Numerical key NOT stored (using API's actual key as source of truth)\n", FILE_APPEND | LOCK_EX);
// Don't store numerical key if there's a mismatch - API's actual key is the truth
}
}
// If BPM still not set, DO NOT randomize - leave it as null
// API.box doesn't provide BPM in callbacks, so we use prompt BPM as fallback
if (!$finalBpm) {
file_put_contents($logFile, " - ⚠️ BPM NOT FOUND - leaving as NULL (will not display on card)\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " - BPM was not in API response and not found in prompt\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, " - ✅ BPM found: $finalBpm (source: " . ($apiBpm ? 'API' : 'prompt') . ")\n", FILE_APPEND | LOCK_EX);
}
file_put_contents($logFile, " - FINAL VALUES: Genre=$finalGenre, BPM=" . ($finalBpm ?: 'NULL (not found)') . ", Key=$finalKey, Mood=$finalMood" . ($finalNumericalKey ? ", NumericalKey=$finalNumericalKey" : "") . "\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " - SOURCE: API=" . ($apiGenre || $apiBpm || $apiKey || $apiMood ? 'YES' : 'NO') . ", PARSED=" . (!empty($parsedMetadata) ? 'YES' : 'NO') . ", DEFAULTS=" . (!$apiGenre && !isset($parsedMetadata['genre']) ? 'YES' : 'NO') . "\n", FILE_APPEND | LOCK_EX);
return [
// Raw callback data for debugging
'raw_callback' => $data,
// Basic music information
// DEFAULT VALUES (used if API doesn't provide and prompt doesn't contain):
// - genre: 'Electronic'
// - bpm: random(80-160) if not found
// - key: 'C major'
// - mood: 'neutral'
// - time_signature: '4/4'
// - energy: 'medium'
// - instruments: ['synthesizer']
'genre' => $finalGenre,
'style' => $data['style'] ?? '',
'tags' => extractTagsFromCallback($data) ?? ($data['tags'] ?? []),
'bpm' => $finalBpm, // API BPM if available, otherwise from prompt (API.box doesn't provide BPM in callbacks)
'key' => $finalKey,
'numerical_key' => $finalNumericalKey, // Only set if validated against API's actual key
'time_signature' => $data['time_signature'] ?? '4/4',
'mood' => $finalMood,
'energy' => $data['energy'] ?? 'medium',
'instruments' => $data['instruments'] ?? ['synthesizer'],
// Audio Quality Metrics
'audio_quality' => [
'bitrate' => $data['bitrate'] ?? $data['audio_bitrate'] ?? null,
'sample_rate' => $data['sample_rate'] ?? $data['audio_sample_rate'] ?? null,
'format' => $data['format'] ?? $data['audio_format'] ?? 'mp3',
'channels' => $data['channels'] ?? $data['audio_channels'] ?? 2,
'file_size' => $data['file_size'] ?? null,
'duration' => $data['duration'] ?? null,
'audio_quality_score' => $data['audio_quality_score'] ?? null
],
// Generation Parameters
'generation_parameters' => [
'model_version' => $data['model_version'] ?? $data['model'] ?? 'v3',
'model_name' => $data['model_name'] ?? null,
'temperature' => $data['temperature'] ?? null,
'top_p' => $data['top_p'] ?? null,
'max_tokens' => $data['max_tokens'] ?? null,
'seed' => $data['seed'] ?? null,
'parameters' => $data['parameters'] ?? $data['generation_params'] ?? []
],
// Processing Information
'processing_info' => [
'processing_time' => $data['processing_time'] ?? $data['generation_time'] ?? null,
'queue_time' => $data['queue_time'] ?? null,
'total_time' => $data['total_time'] ?? null,
'start_time' => $data['start_time'] ?? null,
'end_time' => $data['end_time'] ?? null,
'server_id' => $data['server_id'] ?? null,
'worker_id' => $data['worker_id'] ?? null
],
// Cost Information
'cost_info' => [
'api_cost' => $data['api_cost'] ?? $data['cost'] ?? null,
'credits_used' => $data['credits_used'] ?? null,
'currency' => $data['currency'] ?? 'USD',
'pricing_tier' => $data['pricing_tier'] ?? null,
'cost_per_second' => $data['cost_per_second'] ?? null
],
// Waveform Data
'waveform_data' => [
'waveform' => $data['waveform'] ?? $data['waveform_data'] ?? null,
'waveform_url' => $data['waveform_url'] ?? null,
'waveform_points' => $data['waveform_points'] ?? null,
'waveform_resolution' => $data['waveform_resolution'] ?? null
],
// Spectrum Analysis
'spectrum_analysis' => [
'spectrum' => $data['spectrum'] ?? $data['spectrum_data'] ?? null,
'spectrum_url' => $data['spectrum_url'] ?? null,
'frequency_data' => $data['frequency_data'] ?? null,
'spectral_centroid' => $data['spectral_centroid'] ?? null,
'spectral_rolloff' => $data['spectral_rolloff'] ?? null,
'spectral_bandwidth' => $data['spectral_bandwidth'] ?? null
],
// Audio Segments
'audio_segments' => [
'segments' => $data['segments'] ?? $data['audio_segments'] ?? [],
'verse_timestamps' => $data['verse_timestamps'] ?? null,
'chorus_timestamps' => $data['chorus_timestamps'] ?? null,
'bridge_timestamps' => $data['bridge_timestamps'] ?? null,
'intro_timestamps' => $data['intro_timestamps'] ?? null,
'outro_timestamps' => $data['outro_timestamps'] ?? null,
'section_labels' => $data['section_labels'] ?? null
],
// Error Details (for failed generations)
'error_details' => [
'error_code' => $data['error_code'] ?? $data['code'] ?? null,
'error_message' => $data['error_message'] ?? $data['msg'] ?? null,
'error_type' => $data['error_type'] ?? null,
'error_category' => $data['error_category'] ?? null,
'error_suggestions' => $data['error_suggestions'] ?? null,
'retry_available' => $data['retry_available'] ?? null,
'error_timestamp' => $data['error_timestamp'] ?? null
],
// Additional Analysis
'audio_analysis' => [
'loudness' => $data['loudness'] ?? null,
'dynamic_range' => $data['dynamic_range'] ?? null,
'peak_amplitude' => $data['peak_amplitude'] ?? null,
'rms_amplitude' => $data['rms_amplitude'] ?? null,
'zero_crossing_rate' => $data['zero_crossing_rate'] ?? null,
'harmonic_content' => $data['harmonic_content'] ?? null,
'percussive_content' => $data['percussive_content'] ?? null
],
// System Information
'system_info' => [
'created_with' => 'AI Music Generation',
'version' => '3.0',
'callback_processed' => date('Y-m-d H:i:s'),
'api_version' => $data['api_version'] ?? null,
'api_endpoint' => $data['api_endpoint'] ?? null
]
];
}
// Only process callback if this is a direct request
if ($isDirectRequest) {
$logFile = 'callback_log.txt';
$input = file_get_contents('php://input');
try {
// Parse the callback data
$data = json_decode($input, true);
if (!$data) {
throw new Exception('Invalid JSON data received');
}
// Log the parsed data
file_put_contents($logFile, "Parsed data: " . json_encode($data, JSON_PRETTY_PRINT) . "\n", FILE_APPEND | LOCK_EX);
// Handle different types of callbacks
if (isset($data['task_id'])) {
$taskId = $data['task_id'];
$status = $data['status'] ?? 'unknown';
// Fetch the original track to get the prompt and existing title
$originalData = getTrackOriginalData($taskId);
$originalPrompt = $originalData['prompt'];
$existingTitle = $originalData['title'];
$titleUserModified = $originalData['title_user_modified'] ?? false;
file_put_contents($logFile, "📝 Original prompt: " . substr($originalPrompt ?: 'N/A', 0, 100) . "...\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, "📝 Existing title: " . ($existingTitle ?: 'N/A') . ($titleUserModified ? " (USER MODIFIED - protected)" : "") . "\n", FILE_APPEND | LOCK_EX);
// Extract comprehensive metadata (pass original prompt for parsing)
$enhanced_metadata = extractComprehensiveMetadata($data, $originalPrompt);
// Extract title from various possible locations in the API response
$apiTitle = extractTitleFromCallback($data);
if ($apiTitle) {
file_put_contents($logFile, "Found API title for task $taskId: $apiTitle\n", FILE_APPEND | LOCK_EX);
}
// Only use API title if track doesn't have a user-provided title
// Preserve user titles unless they're empty or default values
// Also check title_user_modified flag - if set, NEVER overwrite
$title = null;
if ($titleUserModified) {
// User explicitly modified the title - never overwrite
$title = $existingTitle;
file_put_contents($logFile, "🔒 User-modified title protected: $title\n", FILE_APPEND | LOCK_EX);
} elseif ($existingTitle &&
$existingTitle !== '' &&
$existingTitle !== 'Untitled Track' &&
$existingTitle !== 'Generated Track') {
// Keep the user's title
$title = $existingTitle;
file_put_contents($logFile, "✅ Preserving user title: $title\n", FILE_APPEND | LOCK_EX);
} elseif ($apiTitle) {
// Use API title if no user title exists
$title = $apiTitle;
file_put_contents($logFile, "✅ Using API title: $title\n", FILE_APPEND | LOCK_EX);
} else {
// No title available
file_put_contents($logFile, "⚠️ No title available for task $taskId\n", FILE_APPEND | LOCK_EX);
}
// Extract duration from various possible locations in the API response
$duration = extractDurationFromCallback($data);
if ($duration) {
file_put_contents($logFile, "Found duration for task $taskId: $duration\n", FILE_APPEND | LOCK_EX);
}
// Extract tags from various possible locations in the API response
$tagsRaw = extractTagsFromCallback($data);
if ($tagsRaw) {
file_put_contents($logFile, "Found tags for task $taskId: " . (is_array($tagsRaw) ? implode(', ', $tagsRaw) : $tagsRaw) . "\n", FILE_APPEND | LOCK_EX);
}
// Format tags properly for storage in tags column (semicolon-separated string)
$tags = formatTagsForStorage($tagsRaw);
// Extract model_name from various possible locations in the API response
$modelName = extractModelNameFromCallback($data);
if ($modelName) {
file_put_contents($logFile, "Found model_name for task $taskId: $modelName\n", FILE_APPEND | LOCK_EX);
}
// Extract lyrics from various possible locations in the API response
$lyrics = '';
// First, check if we have lyrics in the prompt field (this is where API.Box actually sends them)
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['prompt']) && !empty($item['prompt'])) {
$lyrics = $item['prompt'];
file_put_contents($logFile, "Found lyrics in prompt field for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
}
}
}
// Fallback to other possible locations
if (!$lyrics) {
if (isset($data['lyrics'])) {
$lyrics = $data['lyrics'];
} elseif (isset($data['lyric'])) {
$lyrics = $data['lyric'];
} elseif (isset($data['text'])) {
$lyrics = $data['text'];
} elseif (isset($data['data']['lyrics'])) {
$lyrics = $data['data']['lyrics'];
} elseif (isset($data['data']['lyric'])) {
$lyrics = $data['data']['lyric'];
} elseif (isset($data['data']['text'])) {
$lyrics = $data['data']['text'];
} elseif (isset($data['data']['data']['lyrics'])) {
$lyrics = $data['data']['data']['lyrics'];
} elseif (isset($data['data']['data']['lyric'])) {
$lyrics = $data['data']['data']['lyric'];
} elseif (isset($data['data']['data']['text'])) {
$lyrics = $data['data']['data']['text'];
} elseif (isset($data['data']['data']) && is_array($data['data']['data'])) {
// Handle nested array structure from API.Box
foreach ($data['data']['data'] as $item) {
if (isset($item['lyrics']) && !empty($item['lyrics'])) {
$lyrics = $item['lyrics'];
break;
} elseif (isset($item['lyric']) && !empty($item['lyric'])) {
$lyrics = $item['lyric'];
break;
} elseif (isset($item['text']) && !empty($item['text'])) {
$lyrics = $item['text'];
break;
}
}
}
}
// Clean up lyrics if found
if ($lyrics) {
// Remove common API formatting
$lyrics = str_replace(['[Verse]', '[Chorus]', '[Bridge]', '[Outro]', '[Intro]'], '', $lyrics);
$lyrics = preg_replace('/\[.*?\]/', '', $lyrics); // Remove any [bracketed] content
$lyrics = trim($lyrics);
// Log that we found lyrics
file_put_contents($logFile, "Found lyrics for task $taskId: " . substr($lyrics, 0, 100) . "...\n", FILE_APPEND | LOCK_EX);
} else {
// Debug: Log the data structure to see why lyrics weren't found
file_put_contents($logFile, "No lyrics found for task $taskId. Data structure:\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, "Top level keys: " . implode(', ', array_keys($data)) . "\n", FILE_APPEND | LOCK_EX);
if (isset($data['data'])) {
file_put_contents($logFile, "Data level keys: " . implode(', ', array_keys($data['data'])) . "\n", FILE_APPEND | LOCK_EX);
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
file_put_contents($logFile, "Data array has " . count($data['data']['data']) . " items\n", FILE_APPEND | LOCK_EX);
foreach ($data['data']['data'] as $index => $item) {
if (is_array($item)) {
file_put_contents($logFile, "Item $index keys: " . implode(', ', array_keys($item)) . "\n", FILE_APPEND | LOCK_EX);
}
}
}
}
file_put_contents($logFile, "No lyrics found for task $taskId\n", FILE_APPEND | LOCK_EX);
}
// Download and store audio files locally
$audioUrl = $data['audio_url'] ?? null;
$videoUrl = $data['video_url'] ?? null;
// Extract and download image URL - MUST download and store locally, NO external links
$imageUrl = extractImageUrlFromCallback($data);
$localImageUrl = null;
if ($imageUrl && $status === 'complete') {
$localImageUrl = downloadAndStoreImage($imageUrl, $taskId);
if ($localImageUrl) {
file_put_contents($logFile, "✅ Downloaded and stored image locally: $localImageUrl for task $taskId\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "❌ Failed to download image from: $imageUrl for task $taskId - NOT saving external URL\n", FILE_APPEND | LOCK_EX);
// DO NOT fallback to external URL - only save if we successfully downloaded it
$localImageUrl = null;
}
}
$localAudioUrl = null;
$localVideoUrl = null;
if ($audioUrl && $status === 'complete') {
$localAudioUrl = downloadAndStoreAudio($audioUrl, $taskId, 'main');
file_put_contents($logFile, "Downloaded main audio to: " . ($localAudioUrl ?: 'failed') . " for task $taskId\n", FILE_APPEND | LOCK_EX);
}
if ($videoUrl && $status === 'complete') {
$localVideoUrl = downloadAndStoreAudio($videoUrl, $taskId, 'video');
file_put_contents($logFile, "Downloaded video to: " . ($localVideoUrl ?: 'failed') . " for task $taskId\n", FILE_APPEND | LOCK_EX);
}
// Ensure tags are stored in metadata as well (for consistency)
// Override metadata tags with formatted tags if we have them
// Store the FULL tags string in metadata so it can be extracted later
if ($tags) {
$enhanced_metadata['tags'] = $tags;
file_put_contents($logFile, "✅ Storing tags in metadata: $tags\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "⚠️ No tags to store in metadata for task $taskId\n", FILE_APPEND | LOCK_EX);
}
$metadata = json_encode($enhanced_metadata);
// Update the track with local URLs (fallback to external if download failed), including title, duration, tags, model_name, and image_url
// Tags will be stored in both the tags column AND metadata.tags
updateMusicTrack($taskId, $status, $localAudioUrl ?: $audioUrl, $localVideoUrl ?: $videoUrl, $lyrics, $metadata, $duration, $title, $tags, $modelName, $localImageUrl);
// Store the task result for later retrieval (backup)
$resultFile = "task_results/{$taskId}.json";
// Ensure the directory exists
if (!is_dir('task_results')) {
mkdir('task_results', 0755, true);
}
// Save the result
file_put_contents($resultFile, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
// Log the saved result
file_put_contents($logFile, "Task result saved to: $resultFile\n", FILE_APPEND | LOCK_EX);
// Return success response
echo json_encode([
'success' => true,
'message' => 'Callback processed successfully',
'task_id' => $taskId,
'status' => $status,
'local_audio_url' => $localAudioUrl,
'local_video_url' => $localVideoUrl
]);
} elseif (isset($data['id'])) {
// Alternative format with 'id' instead of 'task_id'
$taskId = $data['id'];
$status = $data['status'] ?? 'unknown';
// Fetch original prompt for metadata parsing
$originalData = getTrackOriginalData($taskId);
$originalPrompt = $originalData['prompt'];
$existingTitle = $originalData['title'];
$titleUserModified = $originalData['title_user_modified'] ?? false;
// Extract comprehensive metadata (pass original prompt for parsing)
$enhanced_metadata = extractComprehensiveMetadata($data, $originalPrompt);
// Extract title from various possible locations in the API response
$apiTitle = extractTitleFromCallback($data);
if ($apiTitle) {
file_put_contents($logFile, "Found API title for task $taskId: $apiTitle\n", FILE_APPEND | LOCK_EX);
}
// Only use API title if track doesn't have a user-provided title
// Also check title_user_modified flag - if set, NEVER overwrite
$title = null;
if ($titleUserModified) {
// User explicitly modified the title - never overwrite
$title = $existingTitle;
file_put_contents($logFile, "🔒 User-modified title protected: $title\n", FILE_APPEND | LOCK_EX);
} elseif ($existingTitle &&
$existingTitle !== '' &&
$existingTitle !== 'Untitled Track' &&
$existingTitle !== 'Generated Track') {
$title = $existingTitle;
file_put_contents($logFile, "✅ Preserving user title: $title\n", FILE_APPEND | LOCK_EX);
} elseif ($apiTitle) {
$title = $apiTitle;
file_put_contents($logFile, "✅ Using API title: $title\n", FILE_APPEND | LOCK_EX);
}
// Extract duration from various possible locations in the API response
$duration = extractDurationFromCallback($data);
if ($duration) {
file_put_contents($logFile, "Found duration for task $taskId: $duration\n", FILE_APPEND | LOCK_EX);
}
// Extract tags from various possible locations in the API response
$tagsRaw = extractTagsFromCallback($data);
if ($tagsRaw) {
file_put_contents($logFile, "Found tags for task $taskId: " . (is_array($tagsRaw) ? implode(', ', $tagsRaw) : $tagsRaw) . "\n", FILE_APPEND | LOCK_EX);
}
// Format tags properly for storage in tags column (semicolon-separated string)
$tags = formatTagsForStorage($tagsRaw);
// Extract model_name from various possible locations in the API response
$modelName = extractModelNameFromCallback($data);
if ($modelName) {
file_put_contents($logFile, "Found model_name for task $taskId: $modelName\n", FILE_APPEND | LOCK_EX);
}
// Extract lyrics from various possible locations in the API response
$lyrics = '';
// First, check if we have lyrics in the prompt field (this is where API.Box actually sends them)
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['prompt']) && !empty($item['prompt'])) {
$lyrics = $item['prompt'];
file_put_contents($logFile, "Found lyrics in prompt field for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
}
}
}
// Fallback to other possible locations
if (!$lyrics) {
if (isset($data['lyrics'])) {
$lyrics = $data['lyrics'];
} elseif (isset($data['lyric'])) {
$lyrics = $data['lyric'];
} elseif (isset($data['text'])) {
$lyrics = $data['text'];
} elseif (isset($data['data']['lyrics'])) {
$lyrics = $data['data']['lyrics'];
} elseif (isset($data['data']['lyric'])) {
$lyrics = $data['data']['lyric'];
} elseif (isset($data['data']['text'])) {
$lyrics = $data['data']['text'];
} elseif (isset($data['data']['data']['lyrics'])) {
$lyrics = $data['data']['data']['lyrics'];
} elseif (isset($data['data']['data']['lyric'])) {
$lyrics = $data['data']['data']['lyric'];
} elseif (isset($data['data']['data']['text'])) {
$lyrics = $data['data']['data']['text'];
} elseif (isset($data['data']['data']) && is_array($data['data']['data'])) {
// Handle nested array structure from API.Box
foreach ($data['data']['data'] as $item) {
if (isset($item['lyrics']) && !empty($item['lyrics'])) {
$lyrics = $item['lyrics'];
break;
} elseif (isset($item['lyric']) && !empty($item['lyric'])) {
$lyrics = $item['lyric'];
break;
} elseif (isset($item['text']) && !empty($item['text'])) {
$lyrics = $item['text'];
break;
}
}
}
}
// Clean up lyrics if found
if ($lyrics) {
// Remove common API formatting
$lyrics = str_replace(['[Verse]', '[Chorus]', '[Bridge]', '[Outro]', '[Intro]'], '', $lyrics);
$lyrics = preg_replace('/\[.*?\]/', '', $lyrics); // Remove any [bracketed] content
$lyrics = trim($lyrics);
// Log that we found lyrics
file_put_contents($logFile, "Found lyrics for task $taskId: " . substr($lyrics, 0, 100) . "...\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "No lyrics found for task $taskId\n", FILE_APPEND | LOCK_EX);
}
// Download and store audio files locally
$audioUrl = $data['audio_url'] ?? null;
$videoUrl = $data['video_url'] ?? null;
// Extract and download image URL - MUST download and store locally, NO external links
$imageUrl = extractImageUrlFromCallback($data);
$localImageUrl = null;
if ($imageUrl && $status === 'complete') {
$localImageUrl = downloadAndStoreImage($imageUrl, $taskId);
if ($localImageUrl) {
file_put_contents($logFile, "✅ Downloaded and stored image locally: $localImageUrl for task $taskId\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "❌ Failed to download image from: $imageUrl for task $taskId - NOT saving external URL\n", FILE_APPEND | LOCK_EX);
// DO NOT fallback to external URL - only save if we successfully downloaded it
$localImageUrl = null;
}
}
$localAudioUrl = null;
$localVideoUrl = null;
if ($audioUrl && $status === 'complete') {
$localAudioUrl = downloadAndStoreAudio($audioUrl, $taskId, 'main');
file_put_contents($logFile, "Downloaded main audio to: " . ($localAudioUrl ?: 'failed') . " for task $taskId\n", FILE_APPEND | LOCK_EX);
}
if ($videoUrl && $status === 'complete') {
$localVideoUrl = downloadAndStoreAudio($videoUrl, $taskId, 'video');
file_put_contents($logFile, "Downloaded video to: " . ($localVideoUrl ?: 'failed') . " for task $taskId\n", FILE_APPEND | LOCK_EX);
}
$metadata = json_encode($enhanced_metadata);
// Update the track with local URLs (fallback to external if download failed), including title, duration, tags, model_name, image_url, and lyrics
updateMusicTrack($taskId, $status, $localAudioUrl ?: $audioUrl, $localVideoUrl ?: $videoUrl, $lyrics, $metadata, $duration, $title, $tags, $modelName, $localImageUrl);
// Store the task result for later retrieval (backup)
$resultFile = "task_results/{$taskId}.json";
// Ensure the directory exists
if (!is_dir('task_results')) {
mkdir('task_results', 0755, true);
}
// Save the result
file_put_contents($resultFile, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
// Log the saved result
file_put_contents($logFile, "Task result saved to: $resultFile\n", FILE_APPEND | LOCK_EX);
// Return success response
echo json_encode([
'success' => true,
'message' => 'Callback processed successfully',
'task_id' => $taskId,
'status' => $status,
'local_audio_url' => $localAudioUrl,
'local_video_url' => $localVideoUrl
]);
} elseif (isset($data['code']) && $data['code'] == 531) {
// Handle API error 531 - generation failed
$taskId = $data['data']['task_id'] ?? 'unknown';
$errorMsg = $data['msg'] ?? 'Generation failed';
// Sanitize error message - remove any references to API.Box or supplier names
$errorMsg = 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)\b/i', '', $errorMsg);
$errorMsg = trim($errorMsg);
if (empty($errorMsg)) {
$errorMsg = 'Generation failed';
}
// Extract comprehensive error metadata
$enhanced_metadata = extractComprehensiveMetadata($data);
// Sanitize metadata msg field if it exists
if (isset($enhanced_metadata['msg'])) {
$enhanced_metadata['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)\b/i', '', $enhanced_metadata['msg']);
$enhanced_metadata['msg'] = trim($enhanced_metadata['msg']);
if (empty($enhanced_metadata['msg'])) {
$enhanced_metadata['msg'] = 'Generation failed';
}
}
$metadata = json_encode($enhanced_metadata);
// Update database with failed status
updateMusicTrack($taskId, 'failed', null, null, null, $metadata);
// Log the error
file_put_contents($logFile, "API Error 531: $errorMsg for task $taskId\n", FILE_APPEND | LOCK_EX);
// Return success response (we handled the error)
echo json_encode([
'success' => true,
'message' => 'Error callback processed',
'task_id' => $taskId,
'status' => 'failed',
'error' => $errorMsg
]);
} elseif (isset($data['code']) && $data['code'] == 400) {
// Handle API error 400 - content violation (e.g., artist name restrictions)
$taskId = $data['data']['task_id'] ?? 'unknown';
$errorMsg = $data['msg'] ?? 'Content violation detected';
// Sanitize error message - remove any references to API.Box or supplier names
$errorMsg = 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)\b/i', '', $errorMsg);
$errorMsg = trim($errorMsg);
if (empty($errorMsg)) {
$errorMsg = 'Content violation detected';
}
// Extract comprehensive error metadata
$enhanced_metadata = extractComprehensiveMetadata($data);
// Sanitize metadata msg field if it exists
if (isset($enhanced_metadata['msg'])) {
$enhanced_metadata['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)\b/i', '', $enhanced_metadata['msg']);
$enhanced_metadata['msg'] = trim($enhanced_metadata['msg']);
if (empty($enhanced_metadata['msg'])) {
$enhanced_metadata['msg'] = 'Content violation detected';
}
}
$metadata = json_encode($enhanced_metadata);
// Update database with failed status and store the specific error message
updateMusicTrack($taskId, 'failed', null, null, null, $metadata);
// Log the error
file_put_contents($logFile, "API Error 400: $errorMsg for task $taskId\n", FILE_APPEND | LOCK_EX);
// Return success response (we handled the error)
echo json_encode([
'success' => true,
'message' => 'Content violation callback processed',
'task_id' => $taskId,
'status' => 'failed',
'error' => $errorMsg,
'error_type' => 'content_violation'
]);
} elseif (isset($data['code']) && isset($data['data']['callbackType'])) {
// Handle API format with callbackType (both success and error cases)
$taskId = $data['data']['task_id'] ?? 'unknown';
$callbackType = $data['data']['callbackType'];
$audioData = $data['data']['data'] ?? [];
file_put_contents($logFile, "Processing API format with callbackType: code={$data['code']}, callbackType=$callbackType, taskId=$taskId\n", FILE_APPEND | LOCK_EX);
// Handle error cases (code 400, 531, etc.)
if ($data['code'] != 200) {
$errorMsg = $data['msg'] ?? 'API error occurred';
// Sanitize error message - remove any references to API.Box or supplier names FIRST
$errorMsg = 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)\b/i', '', $errorMsg);
$errorMsg = trim($errorMsg);
if (empty($errorMsg)) {
$errorMsg = 'API error occurred';
}
// Determine error type for better user feedback
$errorType = 'api_error';
if ($data['code'] == 400) {
$errorType = 'content_violation';
// Common content violations
if (stripos($errorMsg, 'artist name') !== false) {
$errorMsg = "Content violation: " . $errorMsg . " (Avoid mentioning existing artists, bands, or copyrighted material)";
} elseif (stripos($errorMsg, 'copyright') !== false) {
$errorMsg = "Content violation: " . $errorMsg . " (Avoid copyrighted material, song titles, or lyrics)";
} elseif (stripos($errorMsg, 'inappropriate') !== false) {
$errorMsg = "Content violation: " . $errorMsg . " (Avoid explicit or inappropriate content)";
}
} elseif ($data['code'] == 531) {
$errorType = 'generation_failed';
$errorMsg = "Generation failed: " . $errorMsg . " (Technical issue - try again or contact support)";
}
// Extract comprehensive error metadata
$enhanced_metadata = extractComprehensiveMetadata($data);
// Sanitize metadata msg field if it exists
if (isset($enhanced_metadata['msg'])) {
$enhanced_metadata['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)\b/i', '', $enhanced_metadata['msg']);
$enhanced_metadata['msg'] = trim($enhanced_metadata['msg']);
if (empty($enhanced_metadata['msg'])) {
$enhanced_metadata['msg'] = $errorMsg;
}
}
$metadata = json_encode($enhanced_metadata);
// Update database with failed status and enhanced error message
updateMusicTrack($taskId, 'failed', null, null, null, $metadata);
// Log the error with enhanced details
file_put_contents($logFile, "API Error {$data['code']} ($errorType): $errorMsg for task $taskId\n", FILE_APPEND | LOCK_EX);
// Return success response (we handled the error)
echo json_encode([
'success' => true,
'message' => 'Error callback processed',
'task_id' => $taskId,
'status' => 'failed',
'error' => $errorMsg,
'error_type' => $errorType
]);
return;
}
// Handle success case (code 200)
if ($callbackType === 'complete' && !empty($audioData)) {
// Sanitize all incoming data from DOM to prevent apostrophe issues
$audioData = sanitizeDOMArray($audioData);
$data = sanitizeDOMArray($data);
// Extract comprehensive metadata
$enhanced_metadata = extractComprehensiveMetadata($data);
// Extract title using helper function (checks multiple locations)
$title = extractTitleFromCallback($data);
if ($title) {
file_put_contents($logFile, "Found title for task $taskId: $title\n", FILE_APPEND | LOCK_EX);
}
// CRITICAL FIX: Also check all audioData items for title (even if they don't have audio_url yet)
// This ensures we capture the title even if the first item doesn't have audio_url
if (!$title && is_array($audioData) && !empty($audioData)) {
foreach ($audioData as $audio) {
if (isset($audio['title']) && !empty($audio['title']) && trim($audio['title']) !== '') {
$title = sanitizeDOMText($audio['title']);
file_put_contents($logFile, "Found title in audioData for task $taskId: $title\n", FILE_APPEND | LOCK_EX);
break;
}
}
}
// Extract duration using helper function
$duration = extractDurationFromCallback($data);
if ($duration) {
file_put_contents($logFile, "Found duration for task $taskId: $duration\n", FILE_APPEND | LOCK_EX);
}
// Extract tags using helper function
$tagsRaw = extractTagsFromCallback($data);
if ($tagsRaw) {
file_put_contents($logFile, "Found tags for task $taskId: " . (is_array($tagsRaw) ? implode(', ', $tagsRaw) : $tagsRaw) . "\n", FILE_APPEND | LOCK_EX);
}
// Format tags properly for storage in tags column (semicolon-separated string)
$tags = formatTagsForStorage($tagsRaw);
// Extract model_name using helper function
$modelName = extractModelNameFromCallback($data);
if ($modelName) {
file_put_contents($logFile, "Found model_name for task $taskId: $modelName\n", FILE_APPEND | LOCK_EX);
}
// Extract lyrics from various possible locations in the API response
// CRITICAL: Lyrics are in the 'prompt' field of each audio item in the data array
$lyrics = '';
// Priority 1: Check audioData items first (most reliable source for complete callbacks)
if (is_array($audioData) && !empty($audioData)) {
foreach ($audioData as $audio) {
if (isset($audio['prompt']) && !empty($audio['prompt'])) {
$lyrics = $audio['prompt'];
file_put_contents($logFile, "✅ Found lyrics in audioData prompt field for task $taskId\n", FILE_APPEND | LOCK_EX);
break; // Use first item's lyrics
}
}
}
// Priority 2: Check data['data']['data'] array (alternative location)
if (!$lyrics && isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['prompt']) && !empty($item['prompt'])) {
$lyrics = $item['prompt'];
file_put_contents($logFile, "✅ Found lyrics in data.data.data prompt field for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
}
}
}
// Priority 3: Fallback to other possible locations (less common)
if (!$lyrics) {
if (isset($data['lyrics']) && !empty($data['lyrics'])) {
$lyrics = $data['lyrics'];
file_put_contents($logFile, "✅ Found lyrics in data.lyrics for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['lyric']) && !empty($data['lyric'])) {
$lyrics = $data['lyric'];
file_put_contents($logFile, "✅ Found lyrics in data.lyric for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['text']) && !empty($data['text'])) {
$lyrics = $data['text'];
file_put_contents($logFile, "✅ Found lyrics in data.text for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['lyrics']) && !empty($data['data']['lyrics'])) {
$lyrics = $data['data']['lyrics'];
file_put_contents($logFile, "✅ Found lyrics in data.data.lyrics for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['lyric']) && !empty($data['data']['lyric'])) {
$lyrics = $data['data']['lyric'];
file_put_contents($logFile, "✅ Found lyrics in data.data.lyric for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['text']) && !empty($data['data']['text'])) {
$lyrics = $data['data']['text'];
file_put_contents($logFile, "✅ Found lyrics in data.data.text for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['data']['lyrics']) && !empty($data['data']['data']['lyrics'])) {
$lyrics = $data['data']['data']['lyrics'];
file_put_contents($logFile, "✅ Found lyrics in data.data.data.lyrics for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['data']['lyric']) && !empty($data['data']['data']['lyric'])) {
$lyrics = $data['data']['data']['lyric'];
file_put_contents($logFile, "✅ Found lyrics in data.data.data.lyric for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['data']['text']) && !empty($data['data']['data']['text'])) {
$lyrics = $data['data']['data']['text'];
file_put_contents($logFile, "✅ Found lyrics in data.data.data.text for task $taskId\n", FILE_APPEND | LOCK_EX);
} elseif (isset($data['data']['data']) && is_array($data['data']['data'])) {
// Handle nested array structure from API.Box
foreach ($data['data']['data'] as $item) {
if (isset($item['lyrics']) && !empty($item['lyrics'])) {
$lyrics = $item['lyrics'];
file_put_contents($logFile, "✅ Found lyrics in nested item.lyrics for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
} elseif (isset($item['lyric']) && !empty($item['lyric'])) {
$lyrics = $item['lyric'];
file_put_contents($logFile, "✅ Found lyrics in nested item.lyric for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
} elseif (isset($item['text']) && !empty($item['text'])) {
$lyrics = $item['text'];
file_put_contents($logFile, "✅ Found lyrics in nested item.text for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
}
}
}
}
// Clean up lyrics if found
if ($lyrics) {
// Remove common API formatting
$lyrics = str_replace(['[Verse]', '[Chorus]', '[Bridge]', '[Outro]', '[Intro]'], '', $lyrics);
$lyrics = preg_replace('/\[.*?\]/', '', $lyrics); // Remove any [bracketed] content
$lyrics = trim($lyrics);
// Log that we found and cleaned lyrics
file_put_contents($logFile, "✅ Found and cleaned lyrics for task $taskId (length: " . strlen($lyrics) . " chars): " . substr($lyrics, 0, 100) . "...\n", FILE_APPEND | LOCK_EX);
} else {
// Log detailed debug info when lyrics are not found
file_put_contents($logFile, "❌ No lyrics found for task $taskId\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, " Debug: audioData is " . (is_array($audioData) ? "array with " . count($audioData) . " items" : "not an array") . "\n", FILE_APPEND | LOCK_EX);
if (is_array($audioData) && !empty($audioData)) {
$firstItem = $audioData[0];
file_put_contents($logFile, " Debug: First audioData item keys: " . implode(', ', array_keys($firstItem)) . "\n", FILE_APPEND | LOCK_EX);
if (isset($firstItem['prompt'])) {
file_put_contents($logFile, " Debug: First item HAS prompt field (length: " . strlen($firstItem['prompt']) . ")\n", FILE_APPEND | LOCK_EX);
}
}
}
// Extract image URL using helper function - MUST download and store locally, no external links
$imageUrl = extractImageUrlFromCallback($data);
$localImageUrl = null;
if ($imageUrl) {
$localImageUrl = downloadAndStoreImage($imageUrl, $taskId);
if ($localImageUrl) {
file_put_contents($logFile, "✅ Downloaded and stored image locally: $localImageUrl for task $taskId\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "❌ Failed to download image from: $imageUrl for task $taskId - NOT saving external URL\n", FILE_APPEND | LOCK_EX);
// DO NOT fallback to external URL - only save if we successfully downloaded it
$localImageUrl = null;
}
}
// Get the first audio file with a valid URL for the main track
$audioUrl = null;
foreach ($audioData as $audio) {
// Try different possible audio URL fields
$audioUrl = $audio['audio_url'] ?? $audio['source_audio_url'] ?? $audio['stream_audio_url'] ?? null;
// CRITICAL FIX: Extract title from audio item even if audioUrl is empty
// This ensures we capture the title even if the audio file isn't ready yet
if (!$title && isset($audio['title']) && !empty($audio['title']) && trim($audio['title']) !== '') {
$title = sanitizeDOMText($audio['title']);
file_put_contents($logFile, "✅ Found title in audioData item for task $taskId: $title\n", FILE_APPEND | LOCK_EX);
}
if (!empty($audioUrl)) {
// If duration wasn't found by helper, try from audio item
if (!$duration && isset($audio['duration']) && !empty($audio['duration'])) {
$duration = floatval($audio['duration']);
}
// If tags weren't found by helper, try from audio item
if (!$tags && isset($audio['tags']) && !empty($audio['tags'])) {
$tags = is_array($audio['tags']) ? $audio['tags'] : [$audio['tags']];
$tags = sanitizeDOMArray($tags);
}
// If model_name wasn't found by helper, try from audio item
if (!$modelName && isset($audio['model_name']) && !empty($audio['model_name'])) {
$modelName = sanitizeDOMText($audio['model_name']);
}
// If image_url wasn't found by helper, try from audio item - MUST download locally
if (!$imageUrl && isset($audio['image_url']) && !empty($audio['image_url'])) {
$imageUrl = $audio['image_url'];
$localImageUrl = downloadAndStoreImage($imageUrl, $taskId);
if ($localImageUrl) {
file_put_contents($logFile, "✅ Downloaded image from audio item to: $localImageUrl for task $taskId\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "❌ Failed to download image from audio item: $imageUrl for task $taskId - NOT saving external URL\n", FILE_APPEND | LOCK_EX);
// DO NOT fallback to external URL - only save if we successfully downloaded it
$localImageUrl = null;
}
}
file_put_contents($logFile, "Found audio URL: $audioUrl for task $taskId\n", FILE_APPEND | LOCK_EX);
break;
}
}
// Check if this is a stem separation result
$isStemSeparation = false;
$pdo = getDBConnection();
if ($pdo) {
$stmt = $pdo->prepare("SELECT music_type, metadata FROM music_tracks WHERE task_id = ?");
$stmt->execute([$taskId]);
$trackInfo = $stmt->fetch(PDO::FETCH_ASSOC);
if ($trackInfo) {
$metadataCheck = json_decode($trackInfo['metadata'] ?? '{}', true);
$isStemSeparation = ($trackInfo['music_type'] === 'stem-separation') ||
(isset($metadataCheck['separationMode']) && $metadataCheck['separationMode'] === 'split_stem');
}
}
if ($audioUrl) {
// Download and store main audio file locally
$localAudioUrl = downloadAndStoreAudio($audioUrl, $taskId, 'main');
file_put_contents($logFile, "Downloaded main audio to: " . ($localAudioUrl ?: 'failed') . " for task $taskId\n", FILE_APPEND | LOCK_EX);
// For stem separation, process all stem files
if ($isStemSeparation && is_array($audioData) && count($audioData) > 1) {
file_put_contents($logFile, "🔍 Processing full stem separation with " . count($audioData) . " stems for task $taskId\n", FILE_APPEND | LOCK_EX);
$stemFiles = [];
$stemIndex = 0;
foreach ($audioData as $stem) {
$stemAudioUrl = $stem['audio_url'] ?? $stem['source_audio_url'] ?? $stem['stream_audio_url'] ?? null;
$stemName = $stem['stem_name'] ?? $stem['name'] ?? $stem['type'] ?? "stem_$stemIndex";
if (!empty($stemAudioUrl)) {
// Download and store each stem file
$localStemUrl = downloadAndStoreAudio($stemAudioUrl, $taskId, 'stem', $stemIndex);
if ($localStemUrl) {
$stemFiles[] = [
'name' => $stemName,
'url' => $localStemUrl,
'original_url' => $stemAudioUrl,
'index' => $stemIndex
];
file_put_contents($logFile, "✅ Downloaded stem '$stemName' to: $localStemUrl\n", FILE_APPEND | LOCK_EX);
}
$stemIndex++;
}
}
// Store stem files info in metadata
if (!empty($stemFiles)) {
$enhanced_metadata['stem_files'] = $stemFiles;
$enhanced_metadata['stem_count'] = count($stemFiles);
file_put_contents($logFile, "✅ Stored " . count($stemFiles) . " stem files in metadata\n", FILE_APPEND | LOCK_EX);
}
}
// CRITICAL FIX: Ensure title is not empty string before passing to updateMusicTrack
// Convert empty strings to null so updateMusicTrack can handle them properly
if ($title !== null && trim($title) === '') {
$title = null;
file_put_contents($logFile, "⚠️ Title was empty string, converting to null for task $taskId\n", FILE_APPEND | LOCK_EX);
}
// Update database with complete status, local audio URL, duration, title, metadata, image_url, and lyrics
$metadata = json_encode($enhanced_metadata);
file_put_contents($logFile, "🔍 About to call updateMusicTrack with: taskId=$taskId, status=complete, audioUrl=" . ($localAudioUrl ?: $audioUrl) . ", duration=$duration, title=" . ($title ?: 'none') . ", imageUrl=" . ($localImageUrl ?: 'none') . ", lyrics=" . ($lyrics ? 'yes' : 'no') . "\n", FILE_APPEND | LOCK_EX);
// Test database connection before update
$pdo = getDBConnection();
if ($pdo) {
file_put_contents($logFile, "🔍 Database connection test successful\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "❌ Database connection test failed\n", FILE_APPEND | LOCK_EX);
}
// CRITICAL: Always update lyrics if we found them, even if empty string (will be cleaned)
// Pass lyrics as null if empty to avoid overwriting with empty string
$lyricsToSave = (!empty($lyrics)) ? $lyrics : null;
file_put_contents($logFile, "🔍 Calling updateMusicTrack with lyrics: " . ($lyricsToSave ? "YES (length: " . strlen($lyricsToSave) . ")" : "NULL") . "\n", FILE_APPEND | LOCK_EX);
$updateResult = updateMusicTrack($taskId, 'complete', $localAudioUrl ?: $audioUrl, null, $lyricsToSave, $metadata, $duration, $title, $tags, $modelName, $localImageUrl);
// CRITICAL FIX: If lyrics weren't found in callback but track is complete,
// try to extract from saved task_results JSON file as fallback
if ($updateResult && !$lyricsToSave) {
file_put_contents($logFile, "⚠️ Lyrics not found in callback, attempting to extract from task_results JSON...\n", FILE_APPEND | LOCK_EX);
$fallbackLyrics = extractLyricsFromTaskResults($taskId);
if ($fallbackLyrics) {
// Update track with extracted lyrics
$pdo = getDBConnection();
if ($pdo) {
$stmt = $pdo->prepare("UPDATE music_tracks SET lyrics = ? WHERE task_id = ?");
if ($stmt->execute([$fallbackLyrics, $taskId])) {
file_put_contents($logFile, "✅ Successfully extracted and saved lyrics from task_results JSON for task $taskId\n", FILE_APPEND | LOCK_EX);
$lyricsToSave = $fallbackLyrics; // Update for logging
}
}
} else {
file_put_contents($logFile, "❌ Could not extract lyrics from task_results JSON for task $taskId\n", FILE_APPEND | LOCK_EX);
}
}
if ($updateResult) {
file_put_contents($logFile, "✅ Successfully updated track $taskId to complete with audio URL: " . ($localAudioUrl ?: $audioUrl) . ", duration: $duration" . ($lyricsToSave ? ", lyrics: YES" : ", lyrics: NO") . "\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "❌ FAILED to update track $taskId to complete in database!\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, "🔍 Error details: updateMusicTrack returned false\n", FILE_APPEND | LOCK_EX);
// Try a direct database update as fallback
try {
$pdo = getDBConnection();
if ($pdo) {
$stmt = $pdo->prepare("UPDATE music_tracks SET status = 'complete', audio_url = ?, duration = ?, metadata = ?, updated_at = NOW() WHERE task_id = ?");
$directResult = $stmt->execute([$localAudioUrl ?: $audioUrl, $duration, $metadata, $taskId]);
if ($directResult) {
file_put_contents($logFile, "✅ Direct database update succeeded for task $taskId\n", FILE_APPEND | LOCK_EX);
} else {
$errorInfo = $stmt->errorInfo();
file_put_contents($logFile, "❌ Direct database update also failed: " . json_encode($errorInfo) . "\n", FILE_APPEND | LOCK_EX);
}
}
} catch (Exception $e) {
file_put_contents($logFile, "❌ Exception during direct database update: " . $e->getMessage() . "\n", FILE_APPEND | LOCK_EX);
}
}
// Store all variations in the audio_variations table
$pdo = getDBConnection();
if ($pdo) {
// Get the track ID
$stmt = $pdo->prepare("SELECT id FROM music_tracks WHERE task_id = ?");
$stmt->execute([$taskId]);
$track = $stmt->fetch();
if ($track) {
$trackId = $track['id'];
file_put_contents($logFile, "🔍 Found track ID {$trackId} for task {$taskId}, proceeding to store variations\n", FILE_APPEND | LOCK_EX);
// Clear existing variations for this track
$stmt = $pdo->prepare("DELETE FROM audio_variations WHERE track_id = ?");
$stmt->execute([$trackId]);
// CRITICAL FIX: Track actual stored variations count and use sequential index
$storedVariationsCount = 0;
$variationIndex = 0; // Sequential index for stored variations
// Insert each variation
foreach ($audioData as $originalIndex => $variation) {
// CRITICAL FIX: Check multiple possible audio URL fields (same as main audio extraction)
// This ensures we capture variations even if audio_url is empty but source_audio_url or stream_audio_url exists
$variationAudioUrl = $variation['audio_url'] ?? $variation['source_audio_url'] ?? $variation['stream_audio_url'] ?? null;
if (!empty($variationAudioUrl)) {
// Download and store variation audio file locally
$localVariationUrl = downloadAndStoreAudio($variationAudioUrl, $taskId, 'variation', $variationIndex);
// Extract variation-specific metadata
// Generate a meaningful title for the variation if none provided
$variationTitle = $variation['title'] ?? null;
if (!$variationTitle) {
// Create a descriptive title based on available metadata
$titleParts = [];
if (isset($variation['genre'])) $titleParts[] = $variation['genre'];
if (isset($variation['style'])) $titleParts[] = $variation['style'];
if (isset($variation['mood'])) $titleParts[] = $variation['mood'];
if (isset($variation['energy'])) $titleParts[] = $variation['energy'];
if (!empty($titleParts)) {
$variationTitle = implode(' ', $titleParts) . ' Variation';
} else {
$variationTitle = "AI Variation " . ($index + 1);
}
}
$variationMetadata = [
'genre' => $variation['genre'] ?? $variation['tags'][0] ?? null,
'style' => $variation['style'] ?? null,
'bpm' => $variation['bpm'] ?? $variation['tempo'] ?? null,
'key' => $variation['key'] ?? null,
'mood' => $variation['mood'] ?? null,
'energy' => $variation['energy'] ?? null,
'instruments' => $variation['instruments'] ?? null,
'tags' => $variation['tags'] ?? null,
'duration' => $variation['duration'] ?? null,
'title' => $variationTitle
];
try {
$stmt = $pdo->prepare("
INSERT INTO audio_variations
(track_id, variation_index, audio_url, duration, title, tags, image_url, source_audio_url, stream_audio_url, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$insertResult = $stmt->execute([
$trackId,
$variationIndex,
$localVariationUrl ?: $variationAudioUrl,
$variation['duration'] ?? null,
$variationTitle,
is_array($variation['tags']) ? implode(', ', $variation['tags']) : $variation['tags'],
$variation['image_url'] ?? null,
$variation['source_audio_url'] ?? null,
$variation['stream_audio_url'] ?? null,
json_encode($variationMetadata)
]);
if ($insertResult) {
$storedVariationsCount++;
$variationIndex++; // Increment for next stored variation
file_put_contents($logFile, "✅ Stored variation $variationIndex (original index $originalIndex) for track $taskId with audio URL: " . ($localVariationUrl ?: $variationAudioUrl) . " and metadata: " . json_encode($variationMetadata) . "\n", FILE_APPEND | LOCK_EX);
} else {
$errorInfo = $stmt->errorInfo();
file_put_contents($logFile, "❌ FAILED to insert variation $variationIndex (original index $originalIndex) for track $taskId: " . json_encode($errorInfo) . "\n", FILE_APPEND | LOCK_EX);
}
} catch (Exception $e) {
file_put_contents($logFile, "❌ EXCEPTION inserting variation $variationIndex (original index $originalIndex) for track $taskId: " . $e->getMessage() . "\n", FILE_APPEND | LOCK_EX);
}
} else {
// Log when a variation is skipped due to missing audio URL
file_put_contents($logFile, "⚠️ Skipping variation at original index $originalIndex for track $taskId - no valid audio URL found (checked audio_url, source_audio_url, stream_audio_url)\n", FILE_APPEND | LOCK_EX);
}
}
// CRITICAL FIX: Update the main track with ACTUAL stored variations count (not total audioData count)
// This ensures variation_count matches what's actually in audio_variations table
$stmt = $pdo->prepare("UPDATE music_tracks SET variations_count = ?, selected_variation = 0 WHERE id = ?");
$stmt->execute([$storedVariationsCount, $trackId]);
file_put_contents($logFile, "✅ Stored $storedVariationsCount variations (out of " . count($audioData) . " total audioData items) for track $taskId\n", FILE_APPEND | LOCK_EX);
} else {
// CRITICAL: Track not found - this is why variations aren't being stored!
file_put_contents($logFile, "❌ CRITICAL ERROR: Track not found for task_id {$taskId}! Variations will NOT be stored. This is why the variation button doesn't show!\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, "🔍 Attempting to find track by checking all recent tracks...\n", FILE_APPEND | LOCK_EX);
// Try to find the track - maybe task_id format is different
$stmt = $pdo->prepare("SELECT id, task_id, title, created_at FROM music_tracks WHERE task_id LIKE ? OR task_id = ? ORDER BY created_at DESC LIMIT 5");
$stmt->execute(["%{$taskId}%", $taskId]);
$similarTracks = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($similarTracks)) {
file_put_contents($logFile, "🔍 Found " . count($similarTracks) . " similar track(s):\n", FILE_APPEND | LOCK_EX);
foreach ($similarTracks as $similar) {
file_put_contents($logFile, " - Track ID: {$similar['id']}, Task ID: {$similar['task_id']}, Title: {$similar['title']}\n", FILE_APPEND | LOCK_EX);
}
} else {
file_put_contents($logFile, "❌ No similar tracks found. Track may not exist in database yet.\n", FILE_APPEND | LOCK_EX);
}
}
} else {
file_put_contents($logFile, "❌ CRITICAL ERROR: Database connection failed when trying to store variations for task {$taskId}!\n", FILE_APPEND | LOCK_EX);
}
// Return success response for complete callback
echo json_encode([
'success' => true,
'message' => 'Complete callback processed successfully',
'task_id' => $taskId,
'status' => 'complete',
'local_audio_url' => $localAudioUrl ?: $audioUrl,
'duration' => $duration
]);
return;
} else {
// No valid audio URL found
$enhanced_metadata = extractComprehensiveMetadata($data);
$metadata = json_encode($enhanced_metadata);
updateMusicTrack($taskId, 'failed', null, null, null, $metadata);
file_put_contents($logFile, "No valid audio URL found for task $taskId\n", FILE_APPEND | LOCK_EX);
}
} else {
// Other callback types (text, first, etc.) - extract all available fields and update metadata
$enhanced_metadata = extractComprehensiveMetadata($data);
$metadata = json_encode($enhanced_metadata);
// Extract title from various possible locations in the API response
$title = extractTitleFromCallback($data);
if ($title) {
file_put_contents($logFile, "Found title for task $taskId (callbackType: $callbackType): $title\n", FILE_APPEND | LOCK_EX);
}
// Extract duration from various possible locations in the API response
$duration = extractDurationFromCallback($data);
if ($duration) {
file_put_contents($logFile, "Found duration for task $taskId (callbackType: $callbackType): $duration\n", FILE_APPEND | LOCK_EX);
}
// Extract tags from various possible locations in the API response
$tagsRaw = extractTagsFromCallback($data);
if ($tagsRaw) {
file_put_contents($logFile, "Found tags for task $taskId (callbackType: $callbackType): " . (is_array($tagsRaw) ? implode(', ', $tagsRaw) : $tagsRaw) . "\n", FILE_APPEND | LOCK_EX);
}
// Format tags properly for storage in tags column (semicolon-separated string)
$tags = formatTagsForStorage($tagsRaw);
// Extract model_name from various possible locations in the API response
$modelName = extractModelNameFromCallback($data);
if ($modelName) {
file_put_contents($logFile, "Found model_name for task $taskId (callbackType: $callbackType): $modelName\n", FILE_APPEND | LOCK_EX);
}
// IMPORTANT: Extract lyrics from prompt field for "text" and "first" callbacks too
// Lyrics are often available in early callbacks, not just "complete"
$lyrics = '';
if (isset($data['data']['data']) && is_array($data['data']['data'])) {
foreach ($data['data']['data'] as $item) {
if (isset($item['prompt']) && !empty($item['prompt'])) {
$lyrics = $item['prompt'];
file_put_contents($logFile, "Found lyrics in prompt field for task $taskId (callbackType: $callbackType)\n", FILE_APPEND | LOCK_EX);
break;
}
}
}
// Clean up lyrics if found
if ($lyrics) {
// Remove common API formatting
$lyrics = str_replace(['[Verse]', '[Chorus]', '[Bridge]', '[Outro]', '[Intro]'], '', $lyrics);
$lyrics = preg_replace('/\[.*?\]/', '', $lyrics); // Remove any [bracketed] content
$lyrics = trim($lyrics);
file_put_contents($logFile, "Extracted and cleaned lyrics for task $taskId (callbackType: $callbackType): " . substr($lyrics, 0, 100) . "...\n", FILE_APPEND | LOCK_EX);
} else {
file_put_contents($logFile, "No lyrics found in prompt field for task $taskId (callbackType: $callbackType)\n", FILE_APPEND | LOCK_EX);
}
// Update track with lyrics if found, even if status is still processing
updateMusicTrack($taskId, 'processing', null, null, $lyrics, $metadata, $duration, $title, $tags, $modelName);
file_put_contents($logFile, "Updated track $taskId to processing (callbackType: $callbackType)" . ($lyrics ? " with lyrics" : "") . "\n", FILE_APPEND | LOCK_EX);
}
// Store the task result for later retrieval (backup)
$resultFile = "task_results/{$taskId}.json";
// Ensure the directory exists
if (!is_dir('task_results')) {
mkdir('task_results', 0755, true);
}
// Save the result
file_put_contents($resultFile, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
// Return success response
echo json_encode([
'success' => true,
'message' => 'Callback processed successfully',
'task_id' => $taskId,
'status' => $callbackType === 'complete' ? 'complete' : 'processing'
]);
} else {
// Unknown callback format
file_put_contents($logFile, "Unknown callback format\n", FILE_APPEND | LOCK_EX);
echo json_encode([
'success' => true,
'message' => 'Callback received (unknown format)',
'data' => $data
]);
}
} catch (Exception $e) {
// Log the error
file_put_contents($logFile, "Error: " . $e->getMessage() . "\n", FILE_APPEND | LOCK_EX);
if ($isDirectRequest) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
} // End of if ($isDirectRequest)
?>