| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Http\Client\Response; |
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
|
|
| class FreepikMusicService |
| { |
| public function generate(string $prompt, int $duration = 15): array |
| { |
| $apiKey = trim((string) config('services.freepik.api_key', '')); |
| $timeout = max(20, (int) config('services.freepik.timeout', 90)); |
| $cleanPrompt = trim($prompt); |
| $duration = max(5, min(60, $duration)); |
|
|
| if ($apiKey === '') { |
| throw new RuntimeException($this->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; |
| } |
| } |
|
|