Save workspace changes
This commit is contained in:
192
app/Console/Commands/GenerateArtworkAiSuggestionsCommand.php
Normal file
192
app/Console/Commands/GenerateArtworkAiSuggestionsCommand.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkAiAssist;
|
||||
use App\Services\Studio\StudioAiAssistService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class GenerateArtworkAiSuggestionsCommand extends Command
|
||||
{
|
||||
protected $signature = 'artworks:ai-suggest
|
||||
{artwork_id? : Generate suggestions for a single artwork}
|
||||
{--after-id=0 : Skip artworks with ID <= this value}
|
||||
{--limit= : Stop after processing this many artworks}
|
||||
{--chunk=50 : Database chunk size}
|
||||
{--provider= : Override tag suggestion provider (lm_studio|together)}
|
||||
{--force : Regenerate even when suggestions already exist}
|
||||
{--queue : Queue generation instead of running inline}
|
||||
{--skip-existing : Skip artworks that already have stored tag suggestions}';
|
||||
|
||||
protected $description = 'Generate and store studio AI suggestions for artworks, including 10-15 suggested tags from the md thumbnail';
|
||||
|
||||
public function handle(StudioAiAssistService $aiAssist): int
|
||||
{
|
||||
$artworkId = $this->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, '...'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user