| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Facades\Log; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
|
|
| class ReplicateTryOnService |
| { |
| private string $apiKey; |
| private string $baseUrl; |
| private string $model; |
| private string $version; |
| private int $timeout; |
|
|
| public function __construct() |
| { |
| $this->apiKey = (string) config('services.replicate.api_key', ''); |
| $this->baseUrl = rtrim((string) config('services.replicate.base_url', 'https://api.replicate.com/v1'), '/'); |
| $this->model = (string) config('services.replicate.tryon_model', 'cedoysch/flux-fill-redux-try-on'); |
| $this->version = (string) config('services.replicate.tryon_version', 'cf5cb07a25e726fe2fac166a8c5ab52ddccd48657741670fb09d9954d4d8446f'); |
| $this->timeout = max(60, (int) config('services.replicate.timeout', 180)); |
| } |
|
|
| public function isAvailable(): bool |
| { |
| return trim($this->apiKey) !== ''; |
| } |
|
|
| public function submit(string $modelImageUrl, array $garmentInputs, array $options = []): array |
| { |
| if (! $this->isAvailable()) { |
| throw new RuntimeException('REPLICATE_API_KEY belum diset di server.'); |
| } |
|
|
| if (trim($modelImageUrl) === '') { |
| throw new RuntimeException('Foto model kosong, jadi Try On belum bisa dijalankan.'); |
| } |
|
|
| if ($garmentInputs === []) { |
| throw new RuntimeException('Reference outfit kosong, jadi Try On belum bisa dijalankan.'); |
| } |
|
|
| $clothImageUrl = $this->resolvePrimaryClothImage($garmentInputs); |
| if ($clothImageUrl === '') { |
| throw new RuntimeException('Reference outfit utama kosong, jadi Try On belum bisa dijalankan.'); |
| } |
|
|
| $input = [ |
| 'person_image' => trim($modelImageUrl), |
| 'cloth_image' => $clothImageUrl, |
| 'output_format' => 'webp', |
| 'output_quality' => 95, |
| ]; |
|
|
| $endpoint = $this->baseUrl.'/predictions'; |
| $response = Http::withToken($this->apiKey) |
| ->timeout(30) |
| ->post($endpoint, [ |
| 'version' => $this->version, |
| 'input' => $input, |
| ]); |
|
|
| if ($response->status() === 429) { |
| sleep(3); |
| $response = Http::withToken($this->apiKey) |
| ->timeout(30) |
| ->post($endpoint, [ |
| 'version' => $this->version, |
| 'input' => $input, |
| ]); |
| } |
|
|
| if ($response->failed()) { |
| $detail = trim((string) ($response->json('detail') ?? $response->body())); |
| throw new RuntimeException('Replicate Try On submit gagal: '.Str::limit($detail, 220)); |
| } |
|
|
| $predictionId = (string) $response->json('id', ''); |
| if ($predictionId === '') { |
| throw new RuntimeException('Replicate Try On tidak mengembalikan prediction ID.'); |
| } |
|
|
| Log::info('Replicate Try On submitted.', [ |
| 'prediction_id' => $predictionId, |
| 'model' => $this->model, |
| 'version' => $this->version, |
| 'garment_keys' => array_keys($garmentInputs), |
| ]); |
|
|
| return [ |
| 'prediction_id' => $predictionId, |
| 'provider' => 'replicate-tryon', |
| 'model' => $this->model, |
| 'version' => $this->version, |
| 'garment_keys' => array_keys($garmentInputs), |
| ]; |
| } |
|
|
| public function checkStatus(string $predictionId): array |
| { |
| $response = Http::withToken($this->apiKey) |
| ->timeout(20) |
| ->get($this->baseUrl.'/predictions/'.$predictionId); |
|
|
| if ($response->failed()) { |
| $detail = trim((string) ($response->json('detail') ?? $response->body())); |
|
|
| return [ |
| 'status' => 'failed', |
| 'error' => 'Replicate Try On poll gagal: '.Str::limit($detail, 220), |
| ]; |
| } |
|
|
| $status = (string) $response->json('status', ''); |
|
|
| if (in_array($status, ['starting', 'processing'], true)) { |
| return [ |
| 'status' => 'processing', |
| 'provider' => 'replicate-tryon', |
| 'queue_status' => $status, |
| 'created_at' => (string) $response->json('created_at', ''), |
| 'started_at' => (string) $response->json('started_at', ''), |
| ]; |
| } |
|
|
| if (in_array($status, ['failed', 'canceled'], true)) { |
| return [ |
| 'status' => 'failed', |
| 'provider' => 'replicate-tryon', |
| 'error' => (string) ($response->json('error') ?? 'Replicate Try On gagal.'), |
| ]; |
| } |
|
|
| if ($status === 'succeeded') { |
| $output = $response->json('output'); |
| $url = ''; |
|
|
| if (is_array($output) && filled($output[0] ?? null)) { |
| $url = (string) $output[0]; |
| } elseif (is_string($output) && trim($output) !== '') { |
| $url = trim($output); |
| } |
|
|
| if ($url === '') { |
| return [ |
| 'status' => 'failed', |
| 'provider' => 'replicate-tryon', |
| 'error' => 'Replicate Try On selesai, tapi URL hasilnya kosong.', |
| ]; |
| } |
|
|
| return [ |
| 'status' => 'succeeded', |
| 'provider' => 'replicate-tryon', |
| 'url' => $url, |
| 'thumbnail_url' => $url, |
| ]; |
| } |
|
|
| return [ |
| 'status' => 'failed', |
| 'provider' => 'replicate-tryon', |
| 'error' => 'Status Replicate Try On tidak dikenali: '.$status, |
| ]; |
| } |
|
|
| private function resolvePrimaryClothImage(array $garmentInputs): string |
| { |
| foreach ($garmentInputs as $value) { |
| $url = trim((string) $value); |
| if ($url !== '') { |
| return $url; |
| } |
| } |
|
|
| return ''; |
| } |
| } |
|
|