Upload beautify

This commit is contained in:
2026-02-14 15:14:12 +01:00
parent e129618910
commit 79192345e3
249 changed files with 24436 additions and 1021 deletions

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
final class AggregateFeedAnalyticsCommand extends Command
{
protected $signature = 'analytics:aggregate-feed {--date= : Date (Y-m-d), defaults to yesterday}';
protected $description = 'Aggregate feed analytics into daily metrics by algo version and source';
public function handle(): int
{
$date = $this->option('date')
? (string) $this->option('date')
: now()->subDay()->toDateString();
$rows = DB::table('feed_events')
->selectRaw('algo_version, source')
->selectRaw("SUM(CASE WHEN event_type = 'feed_impression' THEN 1 ELSE 0 END) AS impressions")
->selectRaw("SUM(CASE WHEN event_type = 'feed_click' THEN 1 ELSE 0 END) AS clicks")
->selectRaw("SUM(CASE WHEN event_type = 'feed_click' AND dwell_seconds IS NOT NULL AND dwell_seconds < 5 THEN 1 ELSE 0 END) AS dwell_0_5")
->selectRaw("SUM(CASE WHEN event_type = 'feed_click' AND dwell_seconds >= 5 AND dwell_seconds < 30 THEN 1 ELSE 0 END) AS dwell_5_30")
->selectRaw("SUM(CASE WHEN event_type = 'feed_click' AND dwell_seconds >= 30 AND dwell_seconds < 120 THEN 1 ELSE 0 END) AS dwell_30_120")
->selectRaw("SUM(CASE WHEN event_type = 'feed_click' AND dwell_seconds >= 120 THEN 1 ELSE 0 END) AS dwell_120_plus")
->whereDate('event_date', $date)
->groupBy('algo_version', 'source')
->get();
foreach ($rows as $row) {
$algoVersion = (string) $row->algo_version;
$source = (string) $row->source;
$impressions = (int) ($row->impressions ?? 0);
$clicks = (int) ($row->clicks ?? 0);
$saves = $this->countSavesForGroup($date, $algoVersion, $source);
$ctr = $impressions > 0 ? $clicks / $impressions : 0.0;
$saveRate = $clicks > 0 ? $saves / $clicks : 0.0;
DB::table('feed_daily_metrics')->updateOrInsert(
[
'metric_date' => $date,
'algo_version' => $algoVersion,
'source' => $source,
],
[
'impressions' => $impressions,
'clicks' => $clicks,
'saves' => $saves,
'ctr' => $ctr,
'save_rate' => $saveRate,
'dwell_0_5' => (int) ($row->dwell_0_5 ?? 0),
'dwell_5_30' => (int) ($row->dwell_5_30 ?? 0),
'dwell_30_120' => (int) ($row->dwell_30_120 ?? 0),
'dwell_120_plus' => (int) ($row->dwell_120_plus ?? 0),
'updated_at' => now(),
'created_at' => now(),
]
);
}
$this->info("Aggregated feed analytics for {$date}.");
return self::SUCCESS;
}
private function countSavesForGroup(string $date, string $algoVersion, string $source): int
{
/** @var Collection<int, object{user_id:int,artwork_id:int}> $clickedPairs */
$clickedPairs = DB::table('feed_events')
->select('user_id', 'artwork_id')
->whereDate('event_date', $date)
->where('event_type', 'feed_click')
->where('algo_version', $algoVersion)
->where('source', $source)
->groupBy('user_id', 'artwork_id')
->get();
if ($clickedPairs->isEmpty()) {
return 0;
}
$saves = 0;
foreach ($clickedPairs as $pair) {
$hasSave = DB::table('user_discovery_events')
->whereDate('event_date', $date)
->where('user_id', (int) $pair->user_id)
->where('artwork_id', (int) $pair->artwork_id)
->where('algo_version', $algoVersion)
->whereIn('event_type', ['favorite', 'download'])
->exists();
if ($hasSave) {
$saves++;
}
}
return $saves;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
final class AggregateSimilarArtworkAnalyticsCommand extends Command
{
protected $signature = 'analytics:aggregate-similar-artworks {--date= : Date (Y-m-d), defaults to yesterday}';
protected $description = 'Aggregate similar artwork analytics into daily counts by algo version';
public function handle(): int
{
$date = $this->option('date')
? (string) $this->option('date')
: now()->subDay()->toDateString();
$rows = DB::table('similar_artwork_events')
->selectRaw('algo_version')
->selectRaw("SUM(CASE WHEN event_type = 'impression' THEN 1 ELSE 0 END) AS impressions")
->selectRaw("SUM(CASE WHEN event_type = 'click' THEN 1 ELSE 0 END) AS clicks")
->whereDate('event_date', $date)
->groupBy('algo_version')
->get();
foreach ($rows as $row) {
$impressions = (int) ($row->impressions ?? 0);
$clicks = (int) ($row->clicks ?? 0);
$ctr = $impressions > 0 ? $clicks / $impressions : 0.0;
DB::table('similar_artwork_daily_metrics')->updateOrInsert(
[
'metric_date' => $date,
'algo_version' => (string) $row->algo_version,
],
[
'impressions' => $impressions,
'clicks' => $clicks,
'ctr' => $ctr,
'updated_at' => now(),
'created_at' => now(),
]
);
}
$this->info("Aggregated similar artwork analytics for {$date}.");
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Services\AvatarService;
class AvatarsMigrate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'avatars:migrate {--force}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate legacy avatars to new WebP avatar storage';
protected $service;
public function __construct(AvatarService $service)
{
parent::__construct();
$this->service = $service;
}
public function handle()
{
$this->info('Starting avatar migration...');
// Try to read legacy data from user_profiles.avatar_legacy or users.avatar_legacy or users.icon
$rows = DB::table('user_profiles')->select('user_id', 'avatar_legacy')->whereNotNull('avatar_legacy')->get();
if ($rows->isEmpty()) {
// fallback to users table
$rows = DB::table('users')->select('user_id', 'icon as avatar_legacy')->whereNotNull('icon')->get();
}
$count = 0;
foreach ($rows as $row) {
$userId = $row->user_id;
$legacy = $row->avatar_legacy ?? null;
if (!$legacy) {
continue;
}
// Try common legacy paths
$candidates = [
public_path('user-picture/' . $legacy),
public_path('avatar/' . $userId . '/' . $legacy),
storage_path('app/public/user-picture/' . $legacy),
storage_path('app/public/avatar/' . $userId . '/' . $legacy),
];
$found = false;
foreach ($candidates as $p) {
if (file_exists($p) && is_readable($p)) {
$this->info("Processing user {$userId} from {$p}");
$hash = $this->service->storeFromLegacyFile($userId, $p);
if ($hash) {
$this->info(" -> migrated, hash={$hash}");
$count++;
$found = true;
break;
}
}
}
if (!$found) {
$this->warn("Legacy file not found for user {$userId}, filename={$legacy}");
}
}
$this->info("Migration complete. Processed: {$count}");
return 0;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Jobs\BackfillArtworkEmbeddingsJob;
use Illuminate\Console\Command;
final class BackfillArtworkEmbeddingsCommand extends Command
{
protected $signature = 'artworks:embeddings-backfill {--after-id=0 : Resume after this artwork id} {--batch=200 : Batch size for resumable fan-out} {--force : Regenerate even when source hash matches}';
protected $description = 'Queue resumable CLIP embedding backfill for artworks';
public function handle(): int
{
$afterId = max(0, (int) $this->option('after-id'));
$batch = max(1, min((int) $this->option('batch'), 1000));
$force = (bool) $this->option('force');
BackfillArtworkEmbeddingsJob::dispatch($afterId, $batch, $force);
$this->info("Queued artwork embedding backfill (after_id={$afterId}, batch={$batch}, force=" . ($force ? 'yes' : 'no') . ').');
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Services\Recommendations\FeedOfflineEvaluationService;
use Illuminate\Console\Command;
final class CompareFeedAbCommand extends Command
{
protected $signature = 'analytics:compare-feed-ab
{baseline : Baseline algo_version}
{candidate : Candidate algo_version}
{--from= : Start date (Y-m-d), defaults to last 30 days}
{--to= : End date (Y-m-d), defaults to today}
{--json : Output as JSON}';
protected $description = 'A/B helper for baseline vs candidate feed algo comparison';
public function __construct(private readonly FeedOfflineEvaluationService $evaluator)
{
parent::__construct();
}
public function handle(): int
{
$from = (string) ($this->option('from') ?: now()->subDays(29)->toDateString());
$to = (string) ($this->option('to') ?: now()->toDateString());
if ($from > $to) {
$this->error('Invalid range: --from must be <= --to');
return self::FAILURE;
}
$baseline = (string) $this->argument('baseline');
$candidate = (string) $this->argument('candidate');
$comparison = $this->evaluator->compareBaselineCandidate($baseline, $candidate, $from, $to);
if ((bool) $this->option('json')) {
$this->line((string) json_encode($comparison, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return self::SUCCESS;
}
$this->table(
['algo_version', 'ctr', 'save_rate', 'long_dwell_share', 'bounce_rate', 'objective_score'],
[[
(string) $comparison['baseline']['algo_version'],
(float) $comparison['baseline']['ctr'],
(float) $comparison['baseline']['save_rate'],
(float) $comparison['baseline']['long_dwell_share'],
(float) $comparison['baseline']['bounce_rate'],
(float) $comparison['baseline']['objective_score'],
], [
(string) $comparison['candidate']['algo_version'],
(float) $comparison['candidate']['ctr'],
(float) $comparison['candidate']['save_rate'],
(float) $comparison['candidate']['long_dwell_share'],
(float) $comparison['candidate']['bounce_rate'],
(float) $comparison['candidate']['objective_score'],
]]
);
$delta = (array) $comparison['delta'];
$this->line('Δ objective_score: ' . (string) $delta['objective_score']);
$this->line('Δ objective_lift_pct: ' . (string) ($delta['objective_lift_pct'] ?? 'n/a'));
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Services\Recommendations\FeedOfflineEvaluationService;
use Illuminate\Console\Command;
final class EvaluateFeedWeightsCommand extends Command
{
protected $signature = 'analytics:evaluate-feed-weights
{--algo= : Optional algo_version to evaluate}
{--from= : Start date (Y-m-d), defaults to last 30 days}
{--to= : End date (Y-m-d), defaults to today}
{--json : Output as JSON}';
protected $description = 'Offline feed weight evaluation using feed_daily_metrics';
public function __construct(private readonly FeedOfflineEvaluationService $evaluator)
{
parent::__construct();
}
public function handle(): int
{
$from = (string) ($this->option('from') ?: now()->subDays(29)->toDateString());
$to = (string) ($this->option('to') ?: now()->toDateString());
$algo = $this->option('algo') ? (string) $this->option('algo') : null;
if ($from > $to) {
$this->error('Invalid range: --from must be <= --to');
return self::FAILURE;
}
if ($algo !== null && $algo !== '') {
$result = $this->evaluator->evaluateAlgo($algo, $from, $to);
if ((bool) $this->option('json')) {
$this->line((string) json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
} else {
$this->table(
['algo_version', 'ctr', 'save_rate', 'long_dwell_share', 'bounce_rate', 'objective_score'],
[[
(string) $result['algo_version'],
(float) $result['ctr'],
(float) $result['save_rate'],
(float) $result['long_dwell_share'],
(float) $result['bounce_rate'],
(float) $result['objective_score'],
]]
);
}
return self::SUCCESS;
}
$results = $this->evaluator->evaluateAll($from, $to);
if ((bool) $this->option('json')) {
$this->line((string) json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return self::SUCCESS;
}
$rows = array_map(static fn (array $row): array => [
(string) $row['algo_version'],
(float) $row['ctr'],
(float) $row['save_rate'],
(float) $row['long_dwell_share'],
(float) $row['bounce_rate'],
(float) $row['objective_score'],
], $results);
$this->table(
['algo_version', 'ctr', 'save_rate', 'long_dwell_share', 'bounce_rate', 'objective_score'],
$rows
);
return self::SUCCESS;
}
}

View File

@@ -102,7 +102,7 @@ class ImportLegacyUsers extends Command
DB::table('user_profiles')->insert([
'user_id' => $legacyId,
'bio' => $row->about_me ?: $row->description ?: null,
'about' => $row->about_me ?: $row->description ?: null,
'avatar' => $row->picture ?: null,
'cover_image' => $row->cover_art ?: null,
'country' => $row->country ?: null,
@@ -115,15 +115,7 @@ class ImportLegacyUsers extends Command
'updated_at' => $now,
]);
if (!empty($row->web)) {
DB::table('user_social_links')->insert([
'user_id' => $legacyId,
'platform' => 'website',
'url' => $row->web,
'created_at' => $now,
'updated_at' => $now,
]);
}
// Do not duplicate `website` into `user_social_links` — keep canonical site in `user_profiles.website`.
DB::table('user_statistics')->insert([
'user_id' => $legacyId,