$analysis */ public function detectMode(Artwork $artwork, array $analysis): string { $signals = Collection::make([ $artwork->title, $artwork->description, $artwork->file_name, $analysis['blip_caption'] ?? null, ...Collection::make((array) ($analysis['clip_tags'] ?? []))->pluck('tag')->all(), ...Collection::make((array) ($analysis['yolo_objects'] ?? []))->pluck('tag')->all(), ])->filter()->implode(' '); return preg_match('/\b(screenshot|screen|ui|interface|menu|hud|dashboard|settings|launcher|app|game)\b/i', $signals) === 1 ? 'screenshot' : 'artwork'; } /** * @param array $analysis * @return array */ public function buildTitleSuggestions(Artwork $artwork, array $analysis, string $mode): array { $caption = $this->cleanCaption((string) ($analysis['blip_caption'] ?? '')); $topTerms = $this->topTerms($analysis, 4); $titleSeeds = Collection::make([ $this->titleCase($caption), $this->titleCase($this->limitWords($caption, 6)), $mode === 'screenshot' ? $this->titleCase(trim(implode(' ', array_slice($topTerms, 0, 2)) . ' Screen')) : $this->titleCase(trim(implode(' ', array_slice($topTerms, 0, 3)))), $mode === 'screenshot' ? $this->titleCase(trim(implode(' ', array_slice($topTerms, 0, 2)) . ' Interface')) : $this->titleCase(trim(implode(' ', array_slice($topTerms, 0, 2)) . ' Study')), $mode === 'screenshot' ? $this->titleCase(trim(($topTerms[0] ?? 'Interface') . ' View')) : $this->titleCase(trim(($topTerms[0] ?? 'Artwork') . ' Composition')), ]) ->filter(fn (?string $value): bool => is_string($value) && trim($value) !== '') ->map(fn (string $value): string => Str::limit(trim($value), 80, '')) ->unique() ->take(5) ->values(); return $titleSeeds->map(fn (string $text, int $index): array => [ 'text' => $text, 'confidence' => round(max(0.55, 0.92 - ($index * 0.07)), 2), ])->all(); } /** * @param array $analysis * @return array */ public function buildDescriptionSuggestions(Artwork $artwork, array $analysis, string $mode): array { $caption = $this->cleanCaption((string) ($analysis['blip_caption'] ?? '')); $terms = $this->topTerms($analysis, 5); $termSentence = $terms !== [] ? implode(', ', array_slice($terms, 0, 3)) : null; $short = $caption !== '' ? Str::ucfirst(Str::finish($caption, '.')) : ($mode === 'screenshot' ? 'A clear screenshot with interface-focused visual details.' : 'A visually focused artwork with clear subject and style cues.'); $normal = $short; if ($termSentence) { $normal .= ' It highlights ' . $termSentence . ' without overclaiming details.'; } $seo = $artwork->title !== '' ? $artwork->title . ' is presented with ' . ($termSentence ?: ($mode === 'screenshot' ? 'useful interface context' : 'strong visual detail')) . ' for discovery on Skinbase.' : $normal; return [ ['variant' => 'short', 'text' => Str::limit($short, 180, ''), 'confidence' => 0.89], ['variant' => 'normal', 'text' => Str::limit($normal, 280, ''), 'confidence' => 0.85], ['variant' => 'seo', 'text' => Str::limit($seo, 220, ''), 'confidence' => 0.8], ]; } /** * @param array $analysis * @return array */ public function buildTagSuggestions(Artwork $artwork, array $analysis, string $mode): array { $rawTags = Collection::make() ->merge((array) ($analysis['clip_tags'] ?? [])) ->merge((array) ($analysis['yolo_objects'] ?? [])) ->map(function (mixed $item): array { if (is_string($item)) { return ['tag' => $item, 'confidence' => null]; } return [ 'tag' => (string) ($item['tag'] ?? ''), 'confidence' => isset($item['confidence']) && is_numeric($item['confidence']) ? (float) $item['confidence'] : null, ]; }); foreach ($this->extractCaptionTags((string) ($analysis['blip_caption'] ?? '')) as $captionTag) { $rawTags->push(['tag' => $captionTag, 'confidence' => 0.62]); } if ($mode === 'screenshot') { foreach (['screenshot', 'ui'] as $fallbackTag) { $rawTags->push(['tag' => $fallbackTag, 'confidence' => 0.58]); } } $suggestions = $rawTags ->map(function (array $row): ?array { $tag = $this->normalizer->normalize((string) ($row['tag'] ?? '')); if ($tag === '' || in_array($tag, self::GENERIC_TAGS, true)) { return null; } return [ 'tag' => $tag, 'confidence' => isset($row['confidence']) && is_numeric($row['confidence']) ? round((float) $row['confidence'], 2) : null, ]; }) ->filter() ->unique('tag') ->sortByDesc(fn (array $row): float => (float) ($row['confidence'] ?? 0.0)) ->take(15) ->values(); return $suggestions->all(); } /** * @param array $analysis * @return array */ public function buildSignals(Artwork $artwork, array $analysis): array { return Collection::make([ $artwork->title, $artwork->description, $artwork->file_name, $analysis['blip_caption'] ?? null, ...Collection::make((array) ($analysis['clip_tags'] ?? []))->pluck('tag')->all(), ...Collection::make((array) ($analysis['yolo_objects'] ?? []))->pluck('tag')->all(), ...$artwork->tags->pluck('slug')->all(), ]) ->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '') ->values() ->all(); } /** * @param array $analysis * @return array */ private function topTerms(array $analysis, int $limit): array { return Collection::make() ->merge((array) ($analysis['clip_tags'] ?? [])) ->merge((array) ($analysis['yolo_objects'] ?? [])) ->map(fn (mixed $item): string => trim((string) (is_array($item) ? ($item['tag'] ?? '') : $item))) ->filter() ->flatMap(fn (string $term): array => preg_split('/\s+/', Str::of($term)->replace('-', ' ')->value()) ?: []) ->filter(fn (string $term): bool => strlen($term) >= 3) ->map(fn (string $term): string => Str::title($term)) ->unique() ->take($limit) ->values() ->all(); } /** * @return array */ private function extractCaptionTags(string $caption): array { $clean = Str::of($caption) ->lower() ->replaceMatches('/[^a-z0-9\s\-]+/', ' ') ->replace('-', ' ') ->squish() ->value(); if ($clean === '') { return []; } $tokens = Collection::make(explode(' ', $clean)) ->filter(fn (string $value): bool => strlen($value) >= 3) ->reject(fn (string $value): bool => in_array($value, ['with', 'from', 'into', 'over', 'under', 'image', 'picture', 'artwork'], true)) ->values(); $bigrams = []; for ($index = 0; $index < $tokens->count() - 1; $index++) { $bigrams[] = $tokens[$index] . ' ' . $tokens[$index + 1]; } return $tokens->merge($bigrams)->unique()->take(10)->all(); } private function cleanCaption(string $caption): string { return Str::of($caption) ->replaceMatches('/^(a|an|the)\s+/i', '') ->replaceMatches('/^(image|photo|screenshot) of\s+/i', '') ->squish() ->value(); } private function titleCase(string $value): string { return Str::title(trim($value)); } private function limitWords(string $value, int $maxWords): string { $words = preg_split('/\s+/', trim($value)) ?: []; return implode(' ', array_slice($words, 0, $maxWords)); } }