- AJAX upload with progress bar — browser no longer blocks during upload; redirects to status page immediately after file transfer completes - Improved Ollama prompt: clearly separates manufacturer (brand logo), product name (descriptive text) and number (article code) to fix misidentification like treating the product name as manufacturer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
4.4 KiB
PHP
124 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\AiSetting;
|
|
use App\Models\Manufacturer;
|
|
use App\Models\NailPolish;
|
|
use App\Models\NailPolishImport;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class ProcessNailPolishImage implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public int $timeout = 180;
|
|
public int $tries = 2;
|
|
|
|
public function __construct(private int $importId) {}
|
|
|
|
public function handle(): void
|
|
{
|
|
$import = NailPolishImport::findOrFail($this->importId);
|
|
$import->update(['status' => 'processing']);
|
|
|
|
try {
|
|
$ai = AiSetting::get();
|
|
$imageB64 = base64_encode(Storage::disk('public')->get($import->image_path));
|
|
|
|
$prompt = <<<PROMPT
|
|
You are analyzing a nail polish or nail product bottle/container image.
|
|
Extract exactly these three fields and respond ONLY with a valid JSON object, no explanation, no markdown:
|
|
{
|
|
"name": "the full product name or color description (e.g. 'Modeling Base Calcium Neutral Pink', 'Ruby Red', 'French White')",
|
|
"number": "the product code or article number printed on the label or cap (digits, letters, hyphens only — e.g. '12392-7', '123', 'NP-05')",
|
|
"manufacturer": "the brand or company name (the logo/brand printed on the product — e.g. 'NEONAIL', 'OPI', 'essie', 'Catrice')"
|
|
}
|
|
Rules:
|
|
- manufacturer = the brand/logo name (usually prominent on the label)
|
|
- name = the descriptive product/color name (NOT the brand)
|
|
- number = numeric or alphanumeric code (NOT the color name)
|
|
- If a value is genuinely not visible, use null
|
|
PROMPT;
|
|
|
|
$response = Http::timeout(160)->post("{$ai->ollama_url}/api/generate", [
|
|
'model' => $ai->ollama_model,
|
|
'prompt' => $prompt,
|
|
'images' => [$imageB64],
|
|
'stream' => false,
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
throw new \RuntimeException('Ollama API Fehler: ' . $response->status());
|
|
}
|
|
|
|
$raw = $response->json('response', '');
|
|
$data = $this->parseJson($raw);
|
|
|
|
if (!$data) {
|
|
throw new \RuntimeException('Ollama-Antwort konnte nicht als JSON geparst werden: ' . $raw);
|
|
}
|
|
|
|
// Hersteller suchen oder anlegen
|
|
$manufacturer = null;
|
|
if (!empty($data['manufacturer'])) {
|
|
$manufacturer = Manufacturer::firstOrCreate(
|
|
['name' => trim($data['manufacturer'])]
|
|
);
|
|
}
|
|
|
|
// Nagellack-Nummer eindeutig machen falls nötig
|
|
$number = $data['number'] ?? ('import-' . $this->importId);
|
|
if (NailPolish::where('number', $number)->exists()) {
|
|
$number .= '-' . $this->importId;
|
|
}
|
|
|
|
$nailPolish = NailPolish::create([
|
|
'name' => $data['name'] ?? 'Unbekannt',
|
|
'number' => $number,
|
|
'manufacturer_id' => $manufacturer?->id,
|
|
'image_path' => $import->image_path,
|
|
]);
|
|
|
|
// Zur Sammlung des Users hinzufügen
|
|
$import->user->nailPolishes()->syncWithoutDetaching([$nailPolish->id]);
|
|
|
|
$import->update([
|
|
'status' => 'done',
|
|
'result' => $data,
|
|
'nail_polish_id' => $nailPolish->id,
|
|
]);
|
|
|
|
} catch (\Throwable $e) {
|
|
Log::error('ProcessNailPolishImage fehlgeschlagen', [
|
|
'import_id' => $this->importId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
$import->update([
|
|
'status' => 'failed',
|
|
'error_message' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function parseJson(string $raw): ?array
|
|
{
|
|
// JSON aus der Antwort extrahieren (Ollama kann Text drumherum haben)
|
|
if (preg_match('/\{.*\}/s', $raw, $matches)) {
|
|
$decoded = json_decode($matches[0], true);
|
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
return $decoded;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|