teknolis / app /Services /OpenAiImageService.php
Codex
Deploy latest assistant, DeepSeek defaults, and automation updates
74acf19
Raw
History Blame Contribute Delete
4.64 kB
<?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;
}
}