argument('artwork_id'); $afterId = max(0, (int) $this->option('after-id')); $limit = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null; $chunk = max(1, min(200, (int) $this->option('chunk'))); $provider = $this->normalizeProviderOption($this->option('provider')); $force = (bool) $this->option('force'); $queue = (bool) $this->option('queue'); $skipExisting = (bool) $this->option('skip-existing'); if ($provider === null && $this->option('provider') !== null) { $this->error('Invalid provider. Supported values: lm_studio, together.'); return self::FAILURE; } $processed = 0; $generated = 0; $skipped = 0; $failed = 0; $query = Artwork::query() ->with('artworkAiAssist') ->whereNull('deleted_at') ->whereNotNull('hash') ->orderBy('id'); if ($artworkId !== null) { $query->where('id', (int) $artworkId); } else { $query->where('id', '>', $afterId); } $query->chunkById($chunk, function ($artworks) use (&$processed, &$generated, &$skipped, &$failed, $limit, $skipExisting, $force, $queue, $provider, $aiAssist) { foreach ($artworks as $artwork) { if ($limit !== null && $processed >= $limit) { return false; } $processed++; $assist = $artwork->artworkAiAssist; $hasStoredTags = $assist instanceof ArtworkAiAssist && is_array($assist->tag_suggestions_json) && $assist->tag_suggestions_json !== []; if ($skipExisting && $hasStoredTags && ! $force) { $this->line("[#{$artwork->id}] skip existing suggestions"); $skipped++; continue; } $this->line("[#{$artwork->id}] {$artwork->title}"); try { if ($queue) { $result = $aiAssist->queueAnalysis($artwork, $force, 'tags', $provider); $this->line(" queued ({$result->status})"); } else { $result = $aiAssist->analyze($artwork, $force, 'tags', $provider); $this->line(" {$result->status}"); $this->renderInlineResult($result); } if ($result->status === ArtworkAiAssist::STATUS_FAILED) { $failed++; continue; } $generated++; } catch (\Throwable $exception) { $this->error(' failed: ' . $exception->getMessage()); $failed++; } } return null; }); $this->newLine(); $this->info("Done. processed={$processed} generated={$generated} skipped={$skipped} failed={$failed}"); return $failed > 0 ? self::FAILURE : self::SUCCESS; } private function normalizeProviderOption(mixed $value): ?string { if ($value === null || trim((string) $value) === '') { return null; } return match (strtolower(trim((string) $value))) { 'lm_studio', 'lm-studio', 'local', 'home' => 'lm_studio', 'together', 'together_ai' => 'together', default => null, }; } private function renderInlineResult(ArtworkAiAssist $assist): void { if ($assist->status === ArtworkAiAssist::STATUS_FAILED) { if (is_string($assist->error_message) && $assist->error_message !== '') { $this->error(' error: ' . $assist->error_message); } return; } if ($assist->status !== ArtworkAiAssist::STATUS_READY) { return; } $request = is_array($assist->raw_response_json) ? (array) ($assist->raw_response_json['request'] ?? []) : []; $tagGeneration = is_array($assist->raw_response_json) ? (array) ($assist->raw_response_json['tag_generation'] ?? []) : []; $categorySuggestions = is_array($assist->category_suggestions_json) ? $assist->category_suggestions_json : []; $provider = $tagGeneration['provider'] ?? $request['provider'] ?? config('vision.tag_suggestions.provider'); if (is_string($provider) && $provider !== '') { $this->line(' provider: ' . $provider); } $titles = collect((array) $assist->title_suggestions_json) ->pluck('text') ->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '') ->take(3) ->values() ->all(); if ($titles !== []) { $this->line(' titles: ' . implode(' | ', $titles)); } $tags = collect((array) $assist->tag_suggestions_json) ->pluck('tag') ->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '') ->take(12) ->values() ->all(); if ($tags !== []) { $this->line(' tags: ' . implode(', ', $tags)); } $contentType = $categorySuggestions['content_type']['value'] ?? null; $category = $categorySuggestions['category']['value'] ?? null; if (is_string($contentType) && $contentType !== '') { $line = ' content type: ' . $contentType; if (is_string($category) && $category !== '') { $line .= ' | category: ' . $category; } $this->line($line); } elseif (is_string($category) && $category !== '') { $this->line(' category: ' . $category); } $description = collect((array) $assist->description_suggestions_json) ->pluck('text') ->first(fn (mixed $value): bool => is_string($value) && trim($value) !== ''); if (is_string($description) && $description !== '') { $this->line(' description: ' . Str::limit($description, 140, '...')); } } }