File size: 5,416 Bytes
d4d07bc 74acf19 d4d07bc 74acf19 d4d07bc 74acf19 d4d07bc 74acf19 d4d07bc 74acf19 d4d07bc | 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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | <?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use RuntimeException;
/**
* Image enhancement via Replicate free-tier models.
*
* Supported models (Try-for-Free):
* - topazlabs/image-upscale β professional upscaling (2x/4x/6x)
* - sczhou/codeformer β face restoration for old/AI photos
*
* Polls synchronously (upscaling typically takes 10-40s).
*/
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 !== '';
}
/**
* 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.');
}
}
|