File size: 5,003 Bytes
1501522
 
 
 
 
 
 
 
 
 
 
74acf19
 
 
 
1501522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
071a77d
 
 
 
1501522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74acf19
1501522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74acf19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<?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;
    }
}