| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Facades\Storage; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
|
|
| class HuggingFaceImageService |
| { |
| public function __construct(private ?HuggingFaceMediaStorageService $hfMediaStorage = null) |
| { |
| } |
|
|
| public function generate(string $prompt, string $aspect = 'landscape', int $count = 1): array |
| { |
| $token = trim((string) config('services.huggingface.api_token', '')); |
| $model = trim((string) config('services.huggingface.model', 'Qwen/Qwen2.5-3B-Instruct')); |
| $baseUrl = rtrim((string) config('services.huggingface.base_url', 'https://api-inference.huggingface.co/models'), '/'); |
| $timeout = max(20, (int) config('services.huggingface.timeout', 60)); |
| $count = in_array($count, [1, 2, 4, 6], true) ? $count : 1; |
|
|
| if ($token === '') { |
| throw new RuntimeException('HF_API_TOKEN belum diisi di file .env'); |
| } |
|
|
| $cleanPrompt = trim($prompt); |
| if ($cleanPrompt === '') { |
| throw new RuntimeException('Prompt Hugging Face tidak boleh kosong.'); |
| } |
|
|
| [$width, $height] = $this->sizeFromAspect($aspect); |
| $endpoint = $baseUrl.'/'.ltrim($model, '/'); |
|
|
| $urls = []; |
|
|
| for ($i = 0; $i < $count; $i++) { |
| $seed = random_int(1, 999999999); |
|
|
| $payload = [ |
| 'inputs' => $cleanPrompt, |
| 'parameters' => [ |
| 'width' => $width, |
| 'height' => $height, |
| 'num_inference_steps' => 28, |
| 'guidance_scale' => 7.5, |
| 'seed' => $seed, |
| ], |
| 'options' => [ |
| 'wait_for_model' => true, |
| 'use_cache' => false, |
| ], |
| ]; |
|
|
| $response = Http::timeout($timeout) |
| ->withHeaders([ |
| 'Authorization' => 'Bearer '.$token, |
| 'Accept' => 'image/png', |
| 'Content-Type' => 'application/json', |
| ]) |
| ->withBody((string) json_encode($payload, JSON_UNESCAPED_SLASHES), 'application/json') |
| ->send('POST', $endpoint); |
|
|
| if (! $response->successful()) { |
| $message = trim((string) data_get($response->json(), 'error', data_get($response->json(), 'message', $response->body()))); |
| throw new RuntimeException('Hugging Face image request gagal: '.$response->status().' - '.Str::limit($message, 240)); |
| } |
|
|
| $contentType = strtolower((string) $response->header('Content-Type', '')); |
|
|
| if (! Str::contains($contentType, 'image/')) { |
| $jsonError = trim((string) data_get($response->json(), 'error', data_get($response->json(), 'message', 'Output model bukan image.'))); |
| throw new RuntimeException('Hugging Face tidak mengembalikan image: '.Str::limit($jsonError, 240)); |
| } |
|
|
| $binary = $response->body(); |
| if ($binary === '') { |
| throw new RuntimeException('Output image Hugging Face kosong.'); |
| } |
|
|
| $extension = $this->extensionFromMime($contentType); |
| $filename = 'hf_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.'.$extension; |
| $relativePath = 'images/'.$filename; |
| Storage::disk('public')->put($relativePath, $binary); |
| $urls[] = $this->offloadPublicPath($relativePath); |
| } |
|
|
| if ($urls === []) { |
| throw new RuntimeException('Hugging Face tidak mengembalikan URL image.'); |
| } |
|
|
| return $urls; |
| } |
|
|
| private function sizeFromAspect(string $aspect): array |
| { |
| return match ($aspect) { |
| 'portrait' => [768, 1344], |
| 'square' => [1024, 1024], |
| default => [1344, 768], |
| }; |
| } |
|
|
| private function extensionFromMime(string $mime): string |
| { |
| return match (Str::before($mime, ';')) { |
| 'image/jpeg' => 'jpg', |
| 'image/webp' => 'webp', |
| 'image/png' => 'png', |
| default => 'png', |
| }; |
| } |
|
|
| 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; |
| } |
| } |
|
|