1
0
Fork 0

feat(products): generate thumbnails with OpenAI

This commit is contained in:
Henrik Hautakoski 2026-04-29 17:44:13 +02:00
parent ad7d40b308
commit 7d96e8f0f9
2 changed files with 117 additions and 0 deletions

View file

@ -35,4 +35,8 @@ return [
],
],
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
],
];

View file

@ -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');