update
This commit is contained in:
@@ -47,21 +47,73 @@ final class AiTagArtworksCommand extends Command
|
||||
// Prompt
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private const SYSTEM_PROMPT = <<<'PROMPT'
|
||||
You are an expert at analysing visual artwork and generating concise, descriptive tags.
|
||||
private const SYSTEM_PROMPT = <<<'PROMPT'
|
||||
You are a precise visual-art tagging engine for an artwork gallery.
|
||||
|
||||
Your task is to analyse an artwork image and generate high-quality search tags that are useful for discovery, filtering, and categorisation.
|
||||
|
||||
Prioritise tags that are:
|
||||
- visually evident in the image
|
||||
- concise and specific
|
||||
- useful for gallery search
|
||||
|
||||
Prefer concrete visual concepts over vague opinions.
|
||||
Do not invent details that are not clearly visible.
|
||||
Do not include artist names, brands, watermarks, or assumptions about intent unless directly visible.
|
||||
|
||||
Return tags that describe:
|
||||
- subject or scene
|
||||
- art style or genre
|
||||
- mood or atmosphere
|
||||
- colour palette
|
||||
- technique or medium if visually apparent
|
||||
- composition or notable visual elements if relevant
|
||||
|
||||
Avoid:
|
||||
- generic filler tags like "beautiful", "nice", "art", "image"
|
||||
- duplicate or near-duplicate tags
|
||||
- full sentences
|
||||
- overly broad tags when a more specific one is visible
|
||||
|
||||
Output must be deterministic, compact, and consistent.
|
||||
PROMPT;
|
||||
|
||||
private const USER_PROMPT = <<<'PROMPT'
|
||||
Analyse the artwork image and return a JSON array of relevant tags.
|
||||
Cover: art style, subject/theme, dominant colours, mood, technique, and medium where visible.
|
||||
private const USER_PROMPT = <<<'PROMPT'
|
||||
Analyse this artwork image and return a JSON array of relevant tags.
|
||||
|
||||
Rules:
|
||||
- Return ONLY a valid JSON array of lowercase strings — no markdown, no explanation.
|
||||
- Each tag must be 1–4 words, no punctuation except hyphens.
|
||||
- Between 6 and 12 tags total.
|
||||
Requirements:
|
||||
- Return ONLY a valid JSON array of lowercase strings.
|
||||
- No markdown, no explanation, no extra text.
|
||||
- Output between 8 and 14 tags.
|
||||
- Each tag must be 1 to 3 words.
|
||||
- Use only letters, numbers, spaces, and hyphens.
|
||||
- Do not end tags with punctuation.
|
||||
- Do not include duplicate or near-duplicate tags.
|
||||
- Order tags from most important to least important.
|
||||
|
||||
Example output:
|
||||
["digital painting","fantasy","portrait","dark tones","glowing eyes","detailed","dramatic lighting"]
|
||||
Focus on tags from these groups when visible:
|
||||
1. main subject or scene
|
||||
2. style or genre
|
||||
3. mood or atmosphere
|
||||
4. dominant colours
|
||||
5. medium or technique
|
||||
6. notable visual elements or composition
|
||||
|
||||
Tagging guidelines:
|
||||
- Prefer specific tags over generic ones.
|
||||
- Use searchable gallery-style tags.
|
||||
- Include only what is clearly visible or strongly implied by the image.
|
||||
- If the artwork is abstract, prioritise style, colour, mood, and composition.
|
||||
- If the artwork is representational, prioritise subject, setting, style, and mood.
|
||||
- If a detail is uncertain, leave it out.
|
||||
|
||||
Good output example:
|
||||
["fantasy portrait","digital painting","female warrior","blue tones","dramatic lighting","glowing eyes","cinematic mood","detailed armor"]
|
||||
|
||||
Bad output example:
|
||||
["art","beautiful image","very cool fantasy woman","amazing colors","masterpiece"]
|
||||
|
||||
Now return only the JSON array.
|
||||
PROMPT;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -49,7 +49,7 @@ class AvatarsMigrate extends Command
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
protected $sizes = [32, 40, 64, 128, 256, 512];
|
||||
protected $sizes = [32, 40, 64, 80, 96, 128, 256, 512];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
|
||||
163
app/Console/Commands/RecalculateUserXpCommand.php
Normal file
163
app/Console/Commands/RecalculateUserXpCommand.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\AchievementService;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RecalculateUserXpCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:recalculate-user-xp
|
||||
{user_id? : The ID of a single user to recompute}
|
||||
{--all : Recompute XP and level for all non-deleted users}
|
||||
{--chunk=1000 : Chunk size when --all is used}
|
||||
{--dry-run : Show computed values without writing}
|
||||
{--sync-achievements : Re-run achievement checks after a live recalculation}';
|
||||
|
||||
protected $description = 'Rebuild stored user XP, level, and rank from user_xp_logs';
|
||||
|
||||
public function handle(XPService $xp, AchievementService $achievements): int
|
||||
{
|
||||
$userId = $this->argument('user_id');
|
||||
$all = (bool) $this->option('all');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$syncAchievements = (bool) $this->option('sync-achievements');
|
||||
$chunk = max(1, (int) $this->option('chunk'));
|
||||
|
||||
if ($userId !== null && $all) {
|
||||
$this->error('Provide either a user_id or --all, not both.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($userId !== null) {
|
||||
return $this->recalculateSingle((int) $userId, $xp, $achievements, $dryRun, $syncAchievements);
|
||||
}
|
||||
|
||||
if ($all) {
|
||||
return $this->recalculateAll($xp, $achievements, $chunk, $dryRun, $syncAchievements);
|
||||
}
|
||||
|
||||
$this->error('Provide a user_id or use --all.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
private function recalculateSingle(
|
||||
int $userId,
|
||||
XPService $xp,
|
||||
AchievementService $achievements,
|
||||
bool $dryRun,
|
||||
bool $syncAchievements,
|
||||
): int {
|
||||
$exists = DB::table('users')->where('id', $userId)->exists();
|
||||
if (! $exists) {
|
||||
$this->error("User {$userId} not found.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$label = $dryRun ? '[DRY-RUN]' : '[LIVE]';
|
||||
$this->line("{$label} Recomputing XP for user #{$userId}...");
|
||||
|
||||
$result = $xp->recalculateStoredProgress($userId, ! $dryRun);
|
||||
$this->table(
|
||||
['Field', 'Stored', 'Computed'],
|
||||
[
|
||||
['xp', $result['previous']['xp'], $result['computed']['xp']],
|
||||
['level', $result['previous']['level'], $result['computed']['level']],
|
||||
['rank', $result['previous']['rank'], $result['computed']['rank']],
|
||||
]
|
||||
);
|
||||
|
||||
if ($dryRun) {
|
||||
if ($syncAchievements) {
|
||||
$pending = $achievements->previewUnlocks($userId);
|
||||
$this->line('Achievements preview: ' . (empty($pending) ? 'no pending unlocks' : implode(', ', $pending)));
|
||||
}
|
||||
|
||||
$this->warn('Dry-run: no changes written.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($syncAchievements) {
|
||||
$unlocked = $achievements->checkAchievements($userId);
|
||||
$this->line('Achievements checked: ' . (empty($unlocked) ? 'no new unlocks' : implode(', ', $unlocked)));
|
||||
}
|
||||
|
||||
$this->info($result['changed'] ? "XP updated for user #{$userId}." : "User #{$userId} was already in sync.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function recalculateAll(
|
||||
XPService $xp,
|
||||
AchievementService $achievements,
|
||||
int $chunk,
|
||||
bool $dryRun,
|
||||
bool $syncAchievements,
|
||||
): int {
|
||||
$total = DB::table('users')->whereNull('deleted_at')->count();
|
||||
$label = $dryRun ? '[DRY-RUN]' : '[LIVE]';
|
||||
|
||||
$this->info("{$label} Recomputing XP for {$total} users (chunk={$chunk})...");
|
||||
|
||||
$processed = 0;
|
||||
$changed = 0;
|
||||
$pendingAchievementUsers = 0;
|
||||
$pendingAchievementUnlocks = 0;
|
||||
$appliedAchievementUnlocks = 0;
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
DB::table('users')
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id')
|
||||
->chunkById($chunk, function ($users) use ($xp, $achievements, $dryRun, $syncAchievements, &$processed, &$changed, &$pendingAchievementUsers, &$pendingAchievementUnlocks, &$appliedAchievementUnlocks, $bar): void {
|
||||
foreach ($users as $user) {
|
||||
$result = $xp->recalculateStoredProgress((int) $user->id, ! $dryRun);
|
||||
|
||||
if ($result['changed']) {
|
||||
$changed++;
|
||||
}
|
||||
|
||||
if ($syncAchievements) {
|
||||
if ($dryRun) {
|
||||
$pending = $achievements->previewUnlocks((int) $user->id);
|
||||
if (! empty($pending)) {
|
||||
$pendingAchievementUsers++;
|
||||
$pendingAchievementUnlocks += count($pending);
|
||||
}
|
||||
} else {
|
||||
$unlocked = $achievements->checkAchievements((int) $user->id);
|
||||
$appliedAchievementUnlocks += count($unlocked);
|
||||
}
|
||||
}
|
||||
|
||||
$processed++;
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$summary = "Done - {$processed} users processed, {$changed} " . ($dryRun ? 'would change.' : 'updated.');
|
||||
if ($syncAchievements) {
|
||||
if ($dryRun) {
|
||||
$summary .= " Achievement preview: {$pendingAchievementUnlocks} pending unlock(s) across {$pendingAchievementUsers} user(s).";
|
||||
} else {
|
||||
$summary .= " Achievements re-checked: {$appliedAchievementUnlocks} unlock(s) applied.";
|
||||
}
|
||||
}
|
||||
|
||||
$this->info($summary);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
50
app/Console/Commands/SyncCountriesCommand.php
Normal file
50
app/Console/Commands/SyncCountriesCommand.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Countries\CountrySyncService;
|
||||
use Illuminate\Console\Command;
|
||||
use Throwable;
|
||||
|
||||
final class SyncCountriesCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:sync-countries
|
||||
{--deactivate-missing : Mark countries missing from the source as inactive}
|
||||
{--no-fallback : Fail instead of using the local fallback dataset when remote fetch fails}';
|
||||
|
||||
protected $description = 'Synchronize ISO 3166 country metadata into the local countries table.';
|
||||
|
||||
public function __construct(
|
||||
private readonly CountrySyncService $countrySyncService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
$summary = $this->countrySyncService->sync(
|
||||
allowFallback: ! (bool) $this->option('no-fallback'),
|
||||
deactivateMissing: (bool) $this->option('deactivate-missing') ? true : null,
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Countries synchronized successfully.');
|
||||
$this->line('Source: '.($summary['source'] ?? 'unknown'));
|
||||
$this->line('Fetched: '.(int) ($summary['total_fetched'] ?? 0));
|
||||
$this->line('Inserted: '.(int) ($summary['inserted'] ?? 0));
|
||||
$this->line('Updated: '.(int) ($summary['updated'] ?? 0));
|
||||
$this->line('Skipped: '.(int) ($summary['skipped'] ?? 0));
|
||||
$this->line('Invalid: '.(int) ($summary['invalid'] ?? 0));
|
||||
$this->line('Deactivated: '.(int) ($summary['deactivated'] ?? 0));
|
||||
$this->line('Backfilled users: '.(int) ($summary['backfilled_users'] ?? 0));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user