| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Facades\Storage; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
|
|
| class OpenAiImageService |
| { |
| public function __construct(private ?HuggingFaceMediaStorageService $hfMediaStorage = null) |
| { |
| } |
|
|
| public function generate(string $prompt, string $aspect = 'landscape', int $count = 1): array |
| { |
| $apiKey = $this->resolveApiKey(); |
| $baseUrl = rtrim((string) config('services.openai.base_url', 'https://api.openai.com/v1'), '/'); |
| $model = (string) config('services.openai.image_model', 'gpt-image-1.5'); |
| $timeout = max(20, (int) config('services.openai.image_timeout', 120)); |
|
|
| if (trim($apiKey) === '') { |
| throw new RuntimeException('OPENAI_API_KEY belum diisi di file .env'); |
| } |
|
|
| $count = in_array($count, [1, 2, 4, 6], true) ? $count : 1; |
| $size = $this->sizeFromAspect($aspect); |
|
|
| $response = Http::timeout($timeout) |
| ->withToken($apiKey) |
| ->acceptJson() |
| ->post("{$baseUrl}/images/generations", [ |
| 'model' => $model, |
| 'prompt' => $prompt, |
| 'size' => $size, |
| 'n' => $count, |
| ]); |
|
|
| if (! $response->successful()) { |
| $errorBody = trim((string) data_get($response->json(), 'error.message', $response->body())); |
|
|
| if ($response->status() === 403) { |
| throw new RuntimeException( |
| 'OpenAI image belum diizinkan untuk project ini. Cek project permissions atau Organization Verification. Detail: '.$errorBody |
| ); |
| } |
|
|
| throw new RuntimeException('OpenAI image request gagal: '.$response->status().' - '.$errorBody); |
| } |
|
|
| $items = data_get($response->json(), 'data', []); |
| if (! is_array($items) || count($items) === 0) { |
| throw new RuntimeException('OpenAI image response kosong.'); |
| } |
|
|
| $urls = []; |
| foreach ($items as $item) { |
| $url = data_get($item, 'url'); |
| if (is_string($url) && trim($url) !== '') { |
| $urls[] = trim($url); |
| continue; |
| } |
|
|
| $b64 = data_get($item, 'b64_json'); |
| if (! is_string($b64) || trim($b64) === '') { |
| continue; |
| } |
|
|
| $binary = base64_decode($b64, true); |
| if ($binary === false) { |
| continue; |
| } |
|
|
| $filename = 'img_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.png'; |
| $path = 'images/'.$filename; |
| Storage::disk('public')->put($path, $binary); |
| $urls[] = $this->offloadPublicPath($path); |
| } |
|
|
| if (count($urls) === 0) { |
| throw new RuntimeException('Gagal membaca output gambar dari OpenAI.'); |
| } |
|
|
| return $urls; |
| } |
|
|
| private function sizeFromAspect(string $aspect): string |
| { |
| return match ($aspect) { |
| 'portrait' => '1024x1536', |
| 'square' => '1024x1024', |
| default => '1536x1024', |
| }; |
| } |
|
|
| private function resolveApiKey(): string |
| { |
| $primaryKey = trim((string) config('services.openai.api_key')); |
| $fallbackKey = trim((string) config('services.openai.chat_api_key')); |
|
|
| if ($primaryKey !== '' && ! $this->looksLikePlaceholderKey($primaryKey)) { |
| return $primaryKey; |
| } |
|
|
| return $fallbackKey !== '' ? $fallbackKey : $primaryKey; |
| } |
|
|
| private function looksLikePlaceholderKey(string $value): bool |
| { |
| $normalized = strtoupper(trim($value)); |
|
|
| return str_starts_with($normalized, 'GANTI_') |
| || str_contains($normalized, 'YOUR_') |
| || str_contains($normalized, 'PLACEHOLDER'); |
| } |
|
|
| private function offloadPublicPath(string $relativePath): string |
| { |
| try { |
| return $this->hfMedia()->offloadPublicRelativePath($relativePath); |
| } catch (\Throwable) { |
| return Storage::url($relativePath); |
| } |
| } |
|
|
| private function hfMedia(): HuggingFaceMediaStorageService |
| { |
| if ($this->hfMediaStorage instanceof HuggingFaceMediaStorageService) { |
| return $this->hfMediaStorage; |
| } |
|
|
| $service = app(HuggingFaceMediaStorageService::class); |
| if (! $service instanceof HuggingFaceMediaStorageService) { |
| throw new RuntimeException('Service HuggingFaceMediaStorageService tidak tersedia.'); |
| } |
|
|
| $this->hfMediaStorage = $service; |
|
|
| return $this->hfMediaStorage; |
| } |
| } |
|
|