missingApiKeyMessage('Music generator')); } if ($cleanPrompt === '') { throw new RuntimeException('Prompt musik Freepik tidak boleh kosong.'); } $response = $this->client($apiKey, $timeout) ->post($this->generateEndpoint(), [ 'prompt' => $cleanPrompt, 'duration' => $duration, ]); $data = $this->extractData($response, 'Freepik music request gagal'); $audioUrl = $this->extractGeneratedUrl($data); if ($audioUrl === null) { $taskId = trim((string) data_get($data, 'task_id', data_get($data, 'id', ''))); if ($taskId === '') { throw new RuntimeException('Freepik music tidak mengembalikan task_id.'); } $audioUrl = $this->awaitGeneratedUrl($apiKey, $timeout, $taskId); } return [ 'url' => $audioUrl, 'provider' => 'freepik-music', 'duration_seconds' => $duration, 'prompt' => $cleanPrompt, ]; } private function awaitGeneratedUrl(string $apiKey, int $timeout, string $taskId): string { $deadline = microtime(true) + max(35, min(240, $timeout * 2)); $lastError = null; do { try { $response = $this->client($apiKey, min(25, $timeout)) ->get($this->statusEndpoint($taskId)); $data = $this->extractData($response, 'Gagal membaca status task Freepik music'); $generatedUrl = $this->extractGeneratedUrl($data); if ($generatedUrl !== null) { return $generatedUrl; } $status = strtoupper(trim((string) data_get($data, 'status', ''))); if (in_array($status, ['FAILED', 'ERROR', 'REJECTED', 'CANCELLED'], true)) { throw new RuntimeException('Freepik music task berakhir dengan status '.$status.'.'); } } catch (\Throwable $pollError) { $lastError = $pollError; } usleep(1500000); } while (microtime(true) < $deadline); if ($lastError !== null) { throw new RuntimeException('Timeout saat menunggu hasil musik dari Freepik. Detail terakhir: '.$lastError->getMessage()); } throw new RuntimeException('Timeout saat menunggu hasil musik dari Freepik.'); } private function client(string $apiKey, int $timeout) { return Http::connectTimeout(20) ->timeout($timeout) ->acceptJson() ->withHeaders([ 'x-freepik-api-key' => $apiKey, ]); } private function generateEndpoint(): string { $configured = trim((string) config('services.freepik.music_generate_endpoint', '')); if ($configured !== '') { return $configured; } return rtrim($this->apiOrigin(), '/').'/v1/ai/music-generation/generate'; } private function statusEndpoint(string $taskId): string { $configured = trim((string) config('services.freepik.music_status_endpoint', '')); if ($configured !== '') { return rtrim($configured, '/').'/'.$taskId; } return rtrim($this->apiOrigin(), '/').'/v1/ai/music-generation/'.$taskId; } private function apiOrigin(): string { $configured = trim((string) config('services.freepik.base_url', 'https://api.freepik.com/v1/ai/text-to-image')); $scheme = parse_url($configured, PHP_URL_SCHEME) ?: 'https'; $host = parse_url($configured, PHP_URL_HOST); $port = parse_url($configured, PHP_URL_PORT); if (! is_string($host) || $host === '') { return 'https://api.freepik.com'; } return $scheme.'://'.$host.($port ? ':'.$port : ''); } private function extractData(Response $response, string $prefix): array { if (! $response->successful()) { throw new RuntimeException($prefix.': '.$response->status().' - '.$this->responseError($response)); } $json = $response->json(); $data = data_get($json, 'data', $json); if (! is_array($data)) { throw new RuntimeException($prefix.': format response Freepik tidak dikenali.'); } return $data; } private function responseError(Response $response): string { $message = data_get($response->json(), 'message') ?? data_get($response->json(), 'error.message') ?? data_get($response->json(), 'detail') ?? $response->body(); return Str::limit(trim((string) $message), 260); } private function missingApiKeyMessage(string $feature): string { $value = strtolower(trim((string) ( config('services.huggingface.hf_only') ?? $_ENV['TOOLS_HF_ONLY'] ?? $_SERVER['TOOLS_HF_ONLY'] ?? getenv('TOOLS_HF_ONLY') ?? 'false' ))); if (in_array($value, ['1', 'true', 'yes', 'on'], true)) { return $feature.' belum tersedia di mode Hugging Face-only.'; } return 'FREEPIK_API_KEY belum diisi di file .env'; } private function extractGeneratedUrl(array $data): ?string { $candidates = data_get($data, 'generated', []); if (! is_array($candidates)) { $candidates = []; } if ($candidates === []) { $single = data_get($data, 'url', data_get($data, 'audio_url')); if (is_string($single) && Str::startsWith($single, ['http://', 'https://'])) { return trim($single); } return null; } foreach ($candidates as $candidate) { if (is_string($candidate) && Str::startsWith($candidate, ['http://', 'https://'])) { return trim($candidate); } if (is_array($candidate)) { $url = data_get($candidate, 'url', data_get($candidate, 'audio_url')); if (is_string($url) && Str::startsWith(trim($url), ['http://', 'https://'])) { return trim($url); } } } return null; } }