Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -12,6 +12,7 @@ use App\Models\ContentType;
use App\Services\TagNormalizer;
use App\Services\TagService;
use App\Services\Vision\AiArtworkVectorSearchService;
use App\Services\Vision\ArtworkLlmTagSuggestionService;
use App\Services\Vision\VisionService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -26,11 +27,12 @@ final class StudioAiAssistService
private readonly AiArtworkVectorSearchService $similarity,
private readonly TagService $tagService,
private readonly TagNormalizer $tagNormalizer,
private readonly ArtworkLlmTagSuggestionService $llmTagSuggestions,
private readonly StudioAiAssistEventService $eventService,
) {
}
public function queueAnalysis(Artwork $artwork, bool $force = false, ?string $intent = null): ArtworkAiAssist
public function queueAnalysis(Artwork $artwork, bool $force = false, ?string $intent = null, ?string $provider = null): ArtworkAiAssist
{
$assist = $this->assistRecord($artwork);
$mode = $assist->mode ?: $this->builder->detectMode($artwork->loadMissing(['tags', 'categories.contentType']), []);
@@ -42,26 +44,26 @@ final class StudioAiAssistService
])->save();
$artwork->forceFill(['ai_status' => ArtworkAiAssist::STATUS_QUEUED])->saveQuietly();
$meta = ['force' => $force, 'direct' => false, 'intent' => $intent];
$meta = ['force' => $force, 'direct' => false, 'intent' => $intent, 'provider' => $provider];
$this->appendAction($assist, 'analysis_requested', $meta);
$this->eventService->record($artwork, 'analysis_requested', $meta, $assist);
AnalyzeArtworkAiAssistJob::dispatch($artwork->id, $force)->afterCommit();
AnalyzeArtworkAiAssistJob::dispatch($artwork->id, $force, $intent, $provider)->afterCommit();
return $assist->fresh();
}
public function analyzeDirect(Artwork $artwork, bool $force = false, ?string $intent = null): ArtworkAiAssist
public function analyzeDirect(Artwork $artwork, bool $force = false, ?string $intent = null, ?string $provider = null): ArtworkAiAssist
{
$assist = $this->assistRecord($artwork);
$meta = ['force' => $force, 'direct' => true, 'intent' => $intent];
$meta = ['force' => $force, 'direct' => true, 'intent' => $intent, 'provider' => $provider];
$this->appendAction($assist, 'analysis_requested', $meta);
$this->eventService->record($artwork, 'analysis_requested', $meta, $assist);
return $this->analyze($artwork, $force, $intent);
return $this->analyze($artwork, $force, $intent, $provider);
}
public function analyze(Artwork $artwork, bool $force = false, ?string $intent = null): ArtworkAiAssist
public function analyze(Artwork $artwork, bool $force = false, ?string $intent = null, ?string $provider = null): ArtworkAiAssist
{
$artwork->loadMissing(['tags', 'categories.contentType', 'user']);
@@ -99,7 +101,9 @@ final class StudioAiAssistService
$titleSuggestions = $this->builder->buildTitleSuggestions($artwork, $analysis, $mode);
$descriptionSuggestions = $this->builder->buildDescriptionSuggestions($artwork, $analysis, $mode);
$tagSuggestions = $this->builder->buildTagSuggestions($artwork, $analysis, $mode);
$fallbackTagSuggestions = $this->builder->buildTagSuggestions($artwork, $analysis, $mode);
$llmTagGeneration = $this->llmTagSuggestions->suggestForArtwork($artwork, 10, 15, $provider);
$tagSuggestions = $this->mergeTagSuggestions($llmTagGeneration, $fallbackTagSuggestions);
$similarCandidates = $this->buildSimilarCandidates($artwork);
$assist->forceFill([
@@ -115,6 +119,7 @@ final class StudioAiAssistService
'artwork_id' => (int) $artwork->id,
'hash' => $hash,
'intent' => $intent,
'provider' => $provider,
'force' => $force,
'current_title' => (string) ($artwork->title ?? ''),
'current_description' => (string) ($artwork->description ?? ''),
@@ -122,6 +127,7 @@ final class StudioAiAssistService
],
'vision_debug' => $visionDebug,
'analysis' => $analysis,
'tag_generation' => $llmTagGeneration,
'generated_at' => \now()->toIso8601String(),
'force' => $force,
],
@@ -134,6 +140,7 @@ final class StudioAiAssistService
'force' => $force,
'mode' => $mode,
'intent' => $intent,
'provider' => $llmTagGeneration['provider'] ?? $provider,
'title_suggestion_count' => count($titleSuggestions),
'description_suggestion_count' => count($descriptionSuggestions),
'tag_suggestion_count' => count($tagSuggestions),
@@ -326,11 +333,54 @@ final class StudioAiAssistService
'request' => $assist->raw_response_json['request'] ?? null,
'vision_debug' => $assist->raw_response_json['vision_debug'] ?? null,
'analysis' => $assist->raw_response_json['analysis'] ?? null,
'tag_generation' => $assist->raw_response_json['tag_generation'] ?? null,
'generated_at' => $assist->raw_response_json['generated_at'] ?? null,
] : null,
];
}
/**
* @param array{tags: list<string>, model: ?string, endpoint: ?string, image_url: ?string, variant: string, raw_content?: string, reason?: string, error?: string} $llmResult
* @param array<int, array{tag: string, confidence: float|null}> $fallback
* @return array<int, array{tag: string, confidence: float|null, source: string}>
*/
private function mergeTagSuggestions(array $llmResult, array $fallback, int $min = 10, int $max = 15): array
{
$rows = collect();
foreach (array_values($llmResult['tags'] ?? []) as $index => $tag) {
$rows->push([
'tag' => $tag,
'confidence' => round(max(0.55, 0.94 - ($index * 0.03)), 2),
'source' => 'llm',
]);
}
foreach ($fallback as $row) {
$rows->push([
'tag' => (string) ($row['tag'] ?? ''),
'confidence' => isset($row['confidence']) && is_numeric($row['confidence']) ? (float) $row['confidence'] : null,
'source' => 'vision',
]);
}
$merged = $rows
->filter(fn (array $row): bool => trim((string) ($row['tag'] ?? '')) !== '')
->unique('tag')
->take($max)
->values();
if ($merged->count() < $min && count($fallback) > $merged->count()) {
$merged = $rows
->filter(fn (array $row): bool => trim((string) ($row['tag'] ?? '')) !== '')
->unique('tag')
->take(max($min, min($max, $rows->count())))
->values();
}
return $merged->all();
}
private function assistRecord(Artwork $artwork): ArtworkAiAssist
{
return ArtworkAiAssist::query()->firstOrCreate(