feat(products): generate thumbnails with OpenAI
This commit is contained in:
parent
ad7d40b308
commit
7d96e8f0f9
2 changed files with 117 additions and 0 deletions
|
|
@ -1,3 +1,116 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
Artisan::command('products:generate-thumbnails {--force : Regenerate even if thumbnail_path already exists}', function (): int {
|
||||
$disk = Storage::disk('images');
|
||||
$directory = 'products/thumbnails';
|
||||
$openAiApiKey = (string) config('services.openai.api_key', '');
|
||||
|
||||
if ($openAiApiKey === '') {
|
||||
$this->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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue