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