optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\Artwork;
use App\Services\TagNormalizer;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
final class StudioAiSuggestionBuilder
{
private const GENERIC_TAGS = [
'image', 'picture', 'artwork', 'art', 'design', 'visual', 'graphic', 'photo of', 'image of',
];
public function __construct(
private readonly TagNormalizer $normalizer,
) {
}
/**
* @param array<string, mixed> $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<string, mixed> $analysis
* @return array<int, array{text: string, confidence: float}>
*/
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<string, mixed> $analysis
* @return array<int, array{variant: string, text: string, confidence: float}>
*/
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<string, mixed> $analysis
* @return array<int, array{tag: string, confidence: float|null}>
*/
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<string, mixed> $analysis
* @return array<int, string>
*/
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<string, mixed> $analysis
* @return array<int, string>
*/
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<int, string>
*/
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));
}
}