'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'; } /** * Generate image(s) — submits N predictions serially then polls all. * * @param string $prompt * @param string $aspect 'landscape'|'square'|'portrait' * @param int $count 1–4 * @param string $provider e.g. 'replicate-imagen4' * @return array Image URLs */ 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 ))), ]; } // Ideogram uses aspect_ratio directly and can take extra style inputs later. // Flux supports safety_tolerance; keep defaults $predictionIds = []; $endpoint = rtrim($this->baseUrl, '/') . '/models/' . $model . '/predictions'; for ($i = 0; $i < $count; $i++) { // Delay between submissions to respect burst=1 on low-credit accounts 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); } // ─── Internal ────────────────────────────────────────────────────────── 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; // seconds $interval = 3; $elapsed = 0; $urls = []; $pending = array_flip($ids); // id => index 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; } }