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; } }