| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Facades\Log; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class ReplicateEnhanceService |
| { |
| private string $apiKey; |
| private string $baseUrl; |
|
|
| public function __construct() |
| { |
| $this->apiKey = (string) config('services.replicate.api_key', ''); |
| $this->baseUrl = (string) config('services.replicate.base_url', 'https://api.replicate.com/v1'); |
| } |
|
|
| public function isAvailable(): bool |
| { |
| return $this->apiKey !== ''; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function upscaleImage(string $imageUrl, int $factor = 2): string |
| { |
| $normalizedFactor = match ($factor) { |
| 4 => '4x', |
| 6 => '6x', |
| default => '2x', |
| }; |
|
|
| return $this->runModel('topazlabs/image-upscale', [ |
| 'image' => $imageUrl, |
| 'upscale_factor' => $normalizedFactor, |
| 'output_format' => 'png', |
| ]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function restoreFace(string $imageUrl, float $fidelity = 0.7): string |
| { |
| return $this->runModel('sczhou/codeformer', [ |
| 'image' => $imageUrl, |
| 'codeformer_fidelity' => round($fidelity, 2), |
| 'background_enhance' => true, |
| 'face_upsample' => true, |
| 'upscale' => 2, |
| ]); |
| } |
|
|
| public function portraitEnhance(string $imageUrl, int $factor = 2, float $fidelity = 0.7): string |
| { |
| $upscaledUrl = $this->upscaleImage($imageUrl, $factor); |
|
|
| return $this->restoreFace($upscaledUrl, $fidelity); |
| } |
|
|
| |
|
|
| private function runModel(string $model, array $input): string |
| { |
| if (! $this->isAvailable()) { |
| throw new RuntimeException('REPLICATE_API_KEY belum diset.'); |
| } |
|
|
| |
| if (! str_starts_with($input['image'] ?? '', 'http')) { |
| $input['image'] = rtrim((string) config('app.url', ''), '/') . '/' . ltrim($input['image'], '/'); |
| } |
|
|
| $endpoint = rtrim($this->baseUrl, '/') . '/models/' . $model . '/predictions'; |
| $payload = ['input' => $input]; |
|
|
| $response = Http::withToken($this->apiKey)->timeout(30)->post($endpoint, $payload); |
|
|
| if ($response->status() === 429) { |
| sleep(3); |
| $response = Http::withToken($this->apiKey)->timeout(30)->post($endpoint, $payload); |
| } |
|
|
| if ($response->failed()) { |
| $status = $response->status(); |
| $detail = $response->json('detail') ?? $response->body(); |
| if ($status === 429) { |
| throw new RuntimeException('Replicate rate limit. Tunggu beberapa detik lalu coba lagi.'); |
| } |
| throw new RuntimeException('Replicate enhance gagal: ' . Str::limit((string) $detail, 200)); |
| } |
|
|
| $id = $response->json('id'); |
| if (blank($id)) { |
| throw new RuntimeException('Replicate: prediction ID tidak ada di response.'); |
| } |
|
|
| return $this->pollForUrl($id, $model); |
| } |
|
|
| private function pollForUrl(string $id, string $model): string |
| { |
| $maxWait = 120; |
| $interval = 3; |
| $elapsed = 0; |
| $endpoint = rtrim($this->baseUrl, '/') . '/predictions/' . $id; |
|
|
| while ($elapsed < $maxWait) { |
| sleep($interval); |
| $elapsed += $interval; |
|
|
| $r = Http::withToken($this->apiKey)->timeout(15)->get($endpoint); |
| if ($r->failed()) { |
| continue; |
| } |
|
|
| $status = $r->json('status'); |
|
|
| if ($status === 'succeeded') { |
| $output = $r->json('output'); |
| $url = is_array($output) ? ($output[0] ?? '') : (string) ($output ?? ''); |
| if (filled($url)) { |
| return $url; |
| } |
| throw new RuntimeException('Replicate enhance: output URL kosong.'); |
| } |
|
|
| if (in_array($status, ['failed', 'canceled'], true)) { |
| $err = $r->json('error') ?? 'Unknown error'; |
| Log::warning('ReplicateEnhanceService: failed', ['id' => $id, 'model' => $model, 'error' => $err]); |
| throw new RuntimeException('Replicate enhance gagal: ' . Str::limit($err, 150)); |
| } |
| } |
|
|
| throw new RuntimeException('Replicate enhance timeout (>2 menit). Coba lagi.'); |
| } |
| } |
|
|