File size: 3,499 Bytes
1501522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?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;
    }
}