| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use RuntimeException; |
|
|
| class FluxImageService |
| { |
| public function generate(string $prompt, string $aspect = 'landscape'): string |
| { |
| $apiKey = trim((string) config('services.fal.api_key')); |
| $model = trim((string) config('services.fal.model', 'fal-ai/flux/schnell')); |
| $pollIntervalMs = max(500, (int) config('services.fal.poll_interval_ms', 1500)); |
| $timeoutSec = max(15, (int) config('services.fal.timeout', 90)); |
|
|
| if ($apiKey === '') { |
| throw new RuntimeException('FAL_KEY belum diisi di .env'); |
| } |
|
|
| $payload = [ |
| 'prompt' => $prompt, |
| 'num_images' => 1, |
| 'aspect_ratio' => $this->normalizeAspect($aspect), |
| ]; |
|
|
| $start = microtime(true); |
|
|
| $submit = Http::timeout($timeoutSec) |
| ->withHeaders([ |
| 'Authorization' => 'Key '.$apiKey, |
| 'Content-Type' => 'application/json', |
| ]) |
| ->post('https://queue.fal.run/'.$model, $payload); |
|
|
| if (! $submit->successful()) { |
| $error = trim((string) data_get($submit->json(), 'detail', data_get($submit->json(), 'error', $submit->body()))); |
| throw new RuntimeException('FLUX request gagal: '.$submit->status().' - '.$error); |
| } |
|
|
| $submitJson = $submit->json(); |
| $directUrl = $this->extractImageUrl($submitJson); |
| if ($directUrl) { |
| return $directUrl; |
| } |
|
|
| $statusUrl = data_get($submitJson, 'status_url'); |
| if (! is_string($statusUrl) || trim($statusUrl) === '') { |
| throw new RuntimeException('FLUX tidak mengembalikan status_url.'); |
| } |
|
|
| while ((microtime(true) - $start) < $timeoutSec) { |
| usleep($pollIntervalMs * 1000); |
|
|
| $status = Http::timeout(30) |
| ->withHeaders([ |
| 'Authorization' => 'Key '.$apiKey, |
| 'Content-Type' => 'application/json', |
| ]) |
| ->get($statusUrl); |
|
|
| if (! $status->successful()) { |
| continue; |
| } |
|
|
| $statusJson = $status->json(); |
| $imageUrl = $this->extractImageUrl($statusJson); |
| if ($imageUrl) { |
| return $imageUrl; |
| } |
|
|
| $state = strtoupper((string) data_get($statusJson, 'status')); |
| if (in_array($state, ['FAILED', 'ERROR', 'CANCELLED'], true)) { |
| $error = (string) data_get($statusJson, 'error', 'unknown error'); |
| throw new RuntimeException('FLUX gagal: '.$error); |
| } |
| } |
|
|
| throw new RuntimeException('FLUX timeout, coba prompt lebih singkat atau ulangi lagi.'); |
| } |
|
|
| private function normalizeAspect(string $aspect): string |
| { |
| return match ($aspect) { |
| 'portrait' => '9:16', |
| 'square' => '1:1', |
| default => '16:9', |
| }; |
| } |
|
|
| private function extractImageUrl(array $data): ?string |
| { |
| $candidates = [ |
| data_get($data, 'images.0.url'), |
| data_get($data, 'data.images.0.url'), |
| data_get($data, 'response.images.0.url'), |
| data_get($data, 'output.images.0.url'), |
| ]; |
|
|
| foreach ($candidates as $candidate) { |
| if (is_string($candidate) && trim($candidate) !== '') { |
| return trim($candidate); |
| } |
| } |
|
|
| return null; |
| } |
| } |
|
|