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.');
    }
}