| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Facades\Log; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class ReplicateImageService |
| { |
| private string $apiKey; |
| private string $baseUrl; |
|
|
| |
| private const MODELS = [ |
| 'replicate-imagen4' => 'google/imagen-4', |
| 'replicate-flux' => 'black-forest-labs/flux-1.1-pro', |
| 'replicate-ideogram' => 'ideogram-ai/ideogram-v3-quality', |
| 'replicate-flux-flex' => 'black-forest-labs/flux-2-flex', |
| ]; |
|
|
| 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 static function providerToModel(string $provider): string |
| { |
| return self::MODELS[$provider] ?? 'google/imagen-4'; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function generate( |
| string $prompt, |
| string $aspect = 'landscape', |
| int $count = 1, |
| string $provider = 'replicate-imagen4', |
| array $referenceImages = [] |
| ): array |
| { |
| if (! $this->isAvailable()) { |
| throw new RuntimeException('REPLICATE_API_KEY belum diset.'); |
| } |
|
|
| $model = self::providerToModel($provider); |
| $count = max(1, min(4, $count)); |
|
|
| $aspectRatio = match ($aspect) { |
| 'portrait' => '9:16', |
| 'square' => '1:1', |
| default => '16:9', |
| }; |
|
|
| $input = ['prompt' => $prompt, 'aspect_ratio' => $aspectRatio]; |
|
|
| if ($provider === 'replicate-flux-flex') { |
| $input = [ |
| 'prompt' => $prompt, |
| 'aspect_ratio' => $referenceImages !== [] ? 'match_input_image' : $aspectRatio, |
| 'resolution' => $referenceImages !== [] ? 'match_input_image' : '1 MP', |
| 'output_format' => 'png', |
| 'input_images' => array_values(array_filter(array_map( |
| static fn ($url) => filled($url) ? trim((string) $url) : null, |
| $referenceImages |
| ))), |
| ]; |
| } |
|
|
| |
| |
|
|
| $predictionIds = []; |
| $endpoint = rtrim($this->baseUrl, '/') . '/models/' . $model . '/predictions'; |
|
|
| for ($i = 0; $i < $count; $i++) { |
| |
| if ($i > 0) { |
| sleep(2); |
| } |
|
|
| $response = $this->post($endpoint, ['input' => $input]); |
|
|
| $id = $response->json('id'); |
| if (blank($id)) { |
| throw new RuntimeException('Replicate: prediction ID tidak ada di response.'); |
| } |
| $predictionIds[] = $id; |
| } |
|
|
| return $this->pollUntilDone($predictionIds, model: $model); |
| } |
|
|
| |
|
|
| private function post(string $endpoint, array $payload) |
| { |
| $response = Http::withToken($this->apiKey)->timeout(30)->post($endpoint, $payload); |
|
|
| if ($response->status() === 429) { |
| Log::info('ReplicateImageService: rate-limited, retrying in 3sβ¦'); |
| 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 sedang sibuk (rate limit). Tunggu beberapa detik lalu coba lagi.'); |
| } |
| throw new RuntimeException('Replicate image gagal: ' . Str::limit((string) $detail, 200)); |
| } |
|
|
| return $response; |
| } |
|
|
| private function pollUntilDone(array $ids, string $model = ''): array |
| { |
| $maxWait = 90; |
| $interval = 3; |
| $elapsed = 0; |
| $urls = []; |
| $pending = array_flip($ids); |
|
|
| while (! empty($pending) && $elapsed < $maxWait) { |
| sleep($interval); |
| $elapsed += $interval; |
|
|
| foreach (array_keys($pending) as $id) { |
| $r = Http::withToken($this->apiKey) |
| ->timeout(15) |
| ->get(rtrim($this->baseUrl, '/') . '/predictions/' . $id); |
|
|
| if ($r->failed()) { |
| continue; |
| } |
|
|
| $status = $r->json('status'); |
|
|
| if ($status === 'succeeded') { |
| $output = $r->json('output'); |
| if (is_array($output)) { |
| foreach ($output as $url) { |
| if (filled($url)) { |
| $urls[] = (string) $url; |
| } |
| } |
| } elseif (filled($output)) { |
| $urls[] = (string) $output; |
| } |
| unset($pending[$id]); |
| } elseif (in_array($status, ['failed', 'canceled'], true)) { |
| $err = $r->json('error') ?? 'Unknown error'; |
| Log::warning('ReplicateImageService: prediction failed', ['id' => $id, 'model' => $model, 'error' => $err]); |
| unset($pending[$id]); |
| } |
| } |
| } |
|
|
| if (empty($urls)) { |
| throw new RuntimeException('Replicate: Gagal menghasilkan gambar. Coba lagi dalam beberapa detik.'); |
| } |
|
|
| return $urls; |
| } |
| } |
|
|