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 !== ''; } /** * Upscale image using topazlabs/image-upscale. * @param string $imageUrl Absolute https:// URL * @param int $factor 2, 4, or 6 * @return string Output image URL from Replicate CDN */ 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', ]); } /** * Restore faces using sczhou/codeformer. * @param string $imageUrl Absolute https:// URL * @param float $fidelity 0.0 (restore more) – 1.0 (preserve original) * @return string Output image URL from Replicate CDN */ 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); } // ─── Internal ────────────────────────────────────────────────────────── private function runModel(string $model, array $input): string { if (! $this->isAvailable()) { throw new RuntimeException('REPLICATE_API_KEY belum diset.'); } // Ensure image URL is absolute 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.'); } }