diff --git a/backend/config/services.php b/backend/config/services.php index 6a90eb8..660e046 100644 --- a/backend/config/services.php +++ b/backend/config/services.php @@ -35,4 +35,8 @@ return [ ], ], + 'openai' => [ + 'api_key' => env('OPENAI_API_KEY'), + ], + ]; diff --git a/backend/routes/console.php b/backend/routes/console.php index 50cce95..b95dace 100644 --- a/backend/routes/console.php +++ b/backend/routes/console.php @@ -1,3 +1,116 @@ error('Missing OpenAI API key. Set OPENAI_API_KEY in your environment.'); + + return self::FAILURE; + } + + $categoryLabels = [ + 'computers-laptop' => 'laptop computer', + 'computers-server' => 'rack server hardware', + 'components-motherboard' => 'computer motherboard', + 'components-graphicscard' => 'graphics card', + 'components-memory' => 'RAM memory sticks', + 'components-harddrives-ssd' => 'NVMe SSD drive', + 'components-harddrives-mechanical' => 'mechanical hard disk drive', + 'accessories-keyboard' => 'computer keyboard', + 'accessories-mouse' => 'computer mouse', + 'accessories-streaming' => 'streaming equipment', + 'accessories-bags' => 'laptop backpack', + 'monitor-gaming-monitors' => 'gaming monitor', + 'monitor-mounting' => 'monitor arm mount', + 'monitor-accessories' => 'monitor desk accessory', + 'components-soundcard' => 'USB audio interface', + 'components-tools' => 'PC repair tools', + ]; + + $products = Product::query() + ->with('category:id,slug') + ->get(); + + $updated = 0; + $skipped = 0; + $failed = 0; + + foreach ($products as $product) { + $categorySlug = $product->category?->slug; + + if (! is_string($categorySlug) || ! isset($categoryLabels[$categorySlug])) { + $failed++; + $this->warn("No category image mapping for product #{$product->id} ({$product->sku})"); + continue; + } + + $filename = Str::slug($product->sku).'.jpg'; + $path = $directory.'/'.$filename; + + if (! $this->option('force') && is_string($product->thumbnail_path) && $product->thumbnail_path !== '' && $disk->exists($product->thumbnail_path)) { + $skipped++; + continue; + } + + $prompt = sprintf( + 'Photorealistic product photo of %s for an ecommerce thumbnail. Product: %s. Clean studio lighting, centered subject, neutral background, no text, no logos, no watermark.', + $categoryLabels[$categorySlug], + $product->name, + ); + + /** @var \Illuminate\Http\Client\Response $response */ + $response = Http::timeout(120) + ->withToken($openAiApiKey) + ->post('https://api.openai.com/v1/images/generations', [ + 'model' => 'gpt-image-1', + 'prompt' => $prompt, + 'size' => '1024x1024', + ]); + + if (! $response->successful()) { + $failed++; + $error = $response->json('error.message'); + $status = $response->status(); + $message = is_string($error) && $error !== '' ? $error : $response->body(); + $this->error("Image generation failed for {$product->sku} ({$categorySlug}) [HTTP {$status}]: {$message}"); + continue; + } + + $payload = $response->json(); + $encodedImage = is_array($payload) + ? data_get($payload, 'data.0.b64_json') + : null; + + if (! is_string($encodedImage) || $encodedImage === '') { + $failed++; + $this->error("Image payload missing for {$product->sku}"); + continue; + } + + $binaryImage = base64_decode($encodedImage, true); + + if (! is_string($binaryImage) || $binaryImage === '') { + $failed++; + $this->error("Image decode failed for {$product->sku}"); + continue; + } + + $disk->put($path, $binaryImage); + $product->thumbnail_path = $path; + $product->save(); + $updated++; + } + + $this->info("Completed: updated={$updated}, skipped={$skipped}, failed={$failed}"); + + return self::SUCCESS; +})->purpose('Generate category-based product thumbnails into image storage');