Save workspace changes
This commit is contained in:
@@ -26,6 +26,8 @@ final class AnalyzeArtworkAiAssistJob implements ShouldQueue
|
||||
public function __construct(
|
||||
private readonly int $artworkId,
|
||||
private readonly bool $force = false,
|
||||
private readonly ?string $intent = null,
|
||||
private readonly ?string $provider = null,
|
||||
) {
|
||||
$queue = (string) config('vision.queue', 'default');
|
||||
if ($queue !== '') {
|
||||
@@ -45,6 +47,6 @@ final class AnalyzeArtworkAiAssistJob implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
$aiAssist->analyze($artwork, $this->force);
|
||||
$aiAssist->analyze($artwork, $this->force, $this->intent, $this->provider);
|
||||
}
|
||||
}
|
||||
68
app/Jobs/GenerateAiBiographyJob.php
Normal file
68
app/Jobs/GenerateAiBiographyJob.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\AiBiography\AiBiographyService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Generates or refreshes an AI biography for a creator asynchronously.
|
||||
*
|
||||
* Dispatched by:
|
||||
* - Manual generate / regenerate API endpoints
|
||||
* - Admin batch commands
|
||||
* - Stale-detection refresh passes
|
||||
*/
|
||||
final class GenerateAiBiographyJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 2;
|
||||
|
||||
public int $timeout = 90;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $userId,
|
||||
public readonly bool $force = false,
|
||||
public readonly ?string $provider = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(AiBiographyService $biographyService): void
|
||||
{
|
||||
if ($this->provider !== null && $this->provider !== '') {
|
||||
config(['ai_biography.provider_override' => $this->provider]);
|
||||
config(['ai_biography.provider' => $this->provider]);
|
||||
}
|
||||
|
||||
$user = User::query()
|
||||
->where('id', $this->userId)
|
||||
->where('is_active', true)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($user === null) {
|
||||
Log::warning('GenerateAiBiographyJob: user not found or inactive', ['user_id' => $this->userId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $biographyService->regenerate($user, $this->force);
|
||||
|
||||
if (! $result['success']) {
|
||||
Log::warning('GenerateAiBiographyJob: generation failed', [
|
||||
'user_id' => $this->userId,
|
||||
'action' => $result['action'],
|
||||
'errors' => $result['errors'],
|
||||
'provider' => $this->provider,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,11 @@ namespace App\Jobs;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\ContentType;
|
||||
use App\Models\RankList;
|
||||
use App\Services\RankingService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
@@ -33,174 +30,40 @@ class RankBuildListsJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $timeout = 1800;
|
||||
public int $tries = 2;
|
||||
public int $timeout = 300;
|
||||
public int $tries = 1;
|
||||
|
||||
private const LIST_TYPES = ['trending', 'new_hot', 'best'];
|
||||
|
||||
public function handle(RankingService $ranking): void
|
||||
public function handle(): void
|
||||
{
|
||||
$modelVersion = config('ranking.model_version', 'rank_v1');
|
||||
$maxPerAuthor = (int) config('ranking.diversity.max_per_author', 3);
|
||||
$listSize = (int) config('ranking.diversity.list_size', 50);
|
||||
$candidatePool = (int) config('ranking.diversity.candidate_pool', 200);
|
||||
$listsBuilt = 0;
|
||||
$scopesDispatched = 0;
|
||||
|
||||
// ── 1. Global ──────────────────────────────────────────────────────
|
||||
foreach (self::LIST_TYPES as $listType) {
|
||||
$this->buildAndStore(
|
||||
$ranking, $listType, 'global', 0,
|
||||
$modelVersion, $maxPerAuthor, $listSize, $candidatePool
|
||||
);
|
||||
$listsBuilt++;
|
||||
}
|
||||
RankBuildScopeListsJob::dispatch('global', 0);
|
||||
$scopesDispatched++;
|
||||
|
||||
// ── 2. Per category ────────────────────────────────────────────────
|
||||
Category::query()
|
||||
->select(['id'])
|
||||
->where('is_active', true)
|
||||
->orderBy('id')
|
||||
->chunk(200, function ($categories) use (
|
||||
$ranking, $modelVersion, $maxPerAuthor, $listSize, $candidatePool, &$listsBuilt
|
||||
): void {
|
||||
->chunk(200, function ($categories) use (&$scopesDispatched): void {
|
||||
foreach ($categories as $cat) {
|
||||
foreach (self::LIST_TYPES as $listType) {
|
||||
$this->buildAndStore(
|
||||
$ranking, $listType, 'category', $cat->id,
|
||||
$modelVersion, $maxPerAuthor, $listSize, $candidatePool
|
||||
);
|
||||
$listsBuilt++;
|
||||
}
|
||||
RankBuildScopeListsJob::dispatch('category', (int) $cat->id);
|
||||
$scopesDispatched++;
|
||||
}
|
||||
});
|
||||
|
||||
// ── 3. Per content type ────────────────────────────────────────────
|
||||
ContentType::query()
|
||||
->select(['id'])
|
||||
->orderBy('id')
|
||||
->chunk(50, function ($ctypes) use (
|
||||
$ranking, $modelVersion, $maxPerAuthor, $listSize, $candidatePool, &$listsBuilt
|
||||
): void {
|
||||
->chunk(50, function ($ctypes) use (&$scopesDispatched): void {
|
||||
foreach ($ctypes as $ct) {
|
||||
foreach (self::LIST_TYPES as $listType) {
|
||||
$this->buildAndStore(
|
||||
$ranking, $listType, 'content_type', $ct->id,
|
||||
$modelVersion, $maxPerAuthor, $listSize, $candidatePool
|
||||
);
|
||||
$listsBuilt++;
|
||||
}
|
||||
RankBuildScopeListsJob::dispatch('content_type', (int) $ct->id);
|
||||
$scopesDispatched++;
|
||||
}
|
||||
});
|
||||
|
||||
Log::info('RankBuildListsJob: finished', [
|
||||
'lists_built' => $listsBuilt,
|
||||
'model_version' => $modelVersion,
|
||||
Log::info('RankBuildListsJob: dispatched scope rebuild jobs', [
|
||||
'scopes_dispatched' => $scopesDispatched,
|
||||
'model_version' => config('ranking.model_version', 'rank_v1'),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch candidates, apply diversity, and upsert the resulting list.
|
||||
*/
|
||||
private function buildAndStore(
|
||||
RankingService $ranking,
|
||||
string $listType,
|
||||
string $scopeType,
|
||||
int $scopeId,
|
||||
string $modelVersion,
|
||||
int $maxPerAuthor,
|
||||
int $listSize,
|
||||
int $candidatePool
|
||||
): void {
|
||||
$scoreCol = $this->scoreColumn($listType);
|
||||
$candidates = $this->fetchCandidates(
|
||||
$scopeType, $scopeId, $scoreCol, $candidatePool, $modelVersion
|
||||
);
|
||||
|
||||
$diverse = $ranking->applyDiversity(
|
||||
$candidates->all(), $maxPerAuthor, $listSize
|
||||
);
|
||||
|
||||
$ids = array_map(
|
||||
fn ($item) => (int) ($item->artwork_id ?? $item['artwork_id']),
|
||||
$diverse
|
||||
);
|
||||
|
||||
// Upsert the list (unique: scope_type + scope_id + list_type + model_version)
|
||||
DB::table('rank_lists')->upsert(
|
||||
[[
|
||||
'scope_type' => $scopeType,
|
||||
'scope_id' => $scopeId,
|
||||
'list_type' => $listType,
|
||||
'model_version' => $modelVersion,
|
||||
'artwork_ids' => json_encode($ids),
|
||||
'computed_at' => now()->toDateTimeString(),
|
||||
]],
|
||||
['scope_type', 'scope_id', 'list_type', 'model_version'],
|
||||
['artwork_ids', 'computed_at']
|
||||
);
|
||||
|
||||
// Bust Redis cache so next request picks up the new list
|
||||
$ranking->bustCache($scopeType, $scopeId === 0 ? null : $scopeId, $listType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch top N candidates (with user_id) for a given scope/score column.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, object>
|
||||
*/
|
||||
private function fetchCandidates(
|
||||
string $scopeType,
|
||||
int $scopeId,
|
||||
string $scoreCol,
|
||||
int $limit,
|
||||
string $modelVersion
|
||||
): \Illuminate\Support\Collection {
|
||||
$query = DB::table('rank_artwork_scores as ras')
|
||||
->select(['ras.artwork_id', 'a.user_id', "ras.{$scoreCol}"])
|
||||
->join('artworks as a', function ($join): void {
|
||||
$join->on('a.id', '=', 'ras.artwork_id')
|
||||
->where('a.is_public', 1)
|
||||
->where('a.is_approved', 1)
|
||||
->whereNull('a.deleted_at');
|
||||
})
|
||||
->where('ras.model_version', $modelVersion)
|
||||
->orderByDesc("ras.{$scoreCol}")
|
||||
->limit($limit);
|
||||
|
||||
if ($scopeType === 'category' && $scopeId > 0) {
|
||||
$query->join(
|
||||
'artwork_category as ac',
|
||||
fn ($j) => $j->on('ac.artwork_id', '=', 'a.id')
|
||||
->where('ac.category_id', $scopeId)
|
||||
);
|
||||
}
|
||||
|
||||
if ($scopeType === 'content_type' && $scopeId > 0) {
|
||||
$query->join(
|
||||
'artwork_category as ac',
|
||||
'ac.artwork_id', '=', 'a.id'
|
||||
)->join(
|
||||
'categories as cat',
|
||||
fn ($j) => $j->on('cat.id', '=', 'ac.category_id')
|
||||
->where('cat.content_type_id', $scopeId)
|
||||
->whereNull('cat.deleted_at')
|
||||
);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map list_type to the rank_artwork_scores column name.
|
||||
*/
|
||||
private function scoreColumn(string $listType): string
|
||||
{
|
||||
return match ($listType) {
|
||||
'new_hot' => 'score_new_hot',
|
||||
'best' => 'score_best',
|
||||
default => 'score_trending',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
150
app/Jobs/RankBuildScopeListsJob.php
Normal file
150
app/Jobs/RankBuildScopeListsJob.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\RankingService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RankBuildScopeListsJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $timeout = 300;
|
||||
public int $tries = 2;
|
||||
|
||||
private const LIST_TYPES = ['trending', 'new_hot', 'best'];
|
||||
|
||||
public function __construct(
|
||||
public readonly string $scopeType,
|
||||
public readonly int $scopeId,
|
||||
) {}
|
||||
|
||||
public function handle(RankingService $ranking): void
|
||||
{
|
||||
$modelVersion = config('ranking.model_version', 'rank_v1');
|
||||
$maxPerAuthor = (int) config('ranking.diversity.max_per_author', 3);
|
||||
$listSize = (int) config('ranking.diversity.list_size', 50);
|
||||
$candidatePool = (int) config('ranking.diversity.candidate_pool', 200);
|
||||
|
||||
foreach (self::LIST_TYPES as $listType) {
|
||||
$this->buildAndStore(
|
||||
$ranking,
|
||||
$listType,
|
||||
$this->scopeType,
|
||||
$this->scopeId,
|
||||
$modelVersion,
|
||||
$maxPerAuthor,
|
||||
$listSize,
|
||||
$candidatePool
|
||||
);
|
||||
}
|
||||
|
||||
Log::info('RankBuildScopeListsJob: finished', [
|
||||
'scope_type' => $this->scopeType,
|
||||
'scope_id' => $this->scopeId,
|
||||
'model_version' => $modelVersion,
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildAndStore(
|
||||
RankingService $ranking,
|
||||
string $listType,
|
||||
string $scopeType,
|
||||
int $scopeId,
|
||||
string $modelVersion,
|
||||
int $maxPerAuthor,
|
||||
int $listSize,
|
||||
int $candidatePool
|
||||
): void {
|
||||
$scoreCol = $this->scoreColumn($listType);
|
||||
$candidates = $this->fetchCandidates(
|
||||
$scopeType, $scopeId, $scoreCol, $candidatePool, $modelVersion
|
||||
);
|
||||
|
||||
$diverse = $ranking->applyDiversity(
|
||||
$candidates->all(), $maxPerAuthor, $listSize
|
||||
);
|
||||
|
||||
$ids = array_map(
|
||||
fn ($item) => (int) ($item->artwork_id ?? $item['artwork_id']),
|
||||
$diverse
|
||||
);
|
||||
|
||||
DB::table('rank_lists')->upsert(
|
||||
[[
|
||||
'scope_type' => $scopeType,
|
||||
'scope_id' => $scopeId,
|
||||
'list_type' => $listType,
|
||||
'model_version' => $modelVersion,
|
||||
'artwork_ids' => json_encode($ids),
|
||||
'computed_at' => now()->toDateTimeString(),
|
||||
]],
|
||||
['scope_type', 'scope_id', 'list_type', 'model_version'],
|
||||
['artwork_ids', 'computed_at']
|
||||
);
|
||||
|
||||
$ranking->bustCache($scopeType, $scopeId === 0 ? null : $scopeId, $listType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, object>
|
||||
*/
|
||||
private function fetchCandidates(
|
||||
string $scopeType,
|
||||
int $scopeId,
|
||||
string $scoreCol,
|
||||
int $limit,
|
||||
string $modelVersion
|
||||
): \Illuminate\Support\Collection {
|
||||
$query = DB::table('rank_artwork_scores as ras')
|
||||
->select(['ras.artwork_id', 'a.user_id', "ras.{$scoreCol}"])
|
||||
->join('artworks as a', function ($join): void {
|
||||
$join->on('a.id', '=', 'ras.artwork_id')
|
||||
->where('a.is_public', 1)
|
||||
->where('a.is_approved', 1)
|
||||
->whereNull('a.deleted_at');
|
||||
})
|
||||
->where('ras.model_version', $modelVersion)
|
||||
->orderByDesc("ras.{$scoreCol}")
|
||||
->limit($limit);
|
||||
|
||||
if ($scopeType === 'category' && $scopeId > 0) {
|
||||
$query->join(
|
||||
'artwork_category as ac',
|
||||
fn ($j) => $j->on('ac.artwork_id', '=', 'a.id')
|
||||
->where('ac.category_id', $scopeId)
|
||||
);
|
||||
}
|
||||
|
||||
if ($scopeType === 'content_type' && $scopeId > 0) {
|
||||
$query->join(
|
||||
'artwork_category as ac',
|
||||
'ac.artwork_id', '=', 'a.id'
|
||||
)->join(
|
||||
'categories as cat',
|
||||
fn ($j) => $j->on('cat.id', '=', 'ac.category_id')
|
||||
->where('cat.content_type_id', $scopeId)
|
||||
->whereNull('cat.deleted_at')
|
||||
);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
private function scoreColumn(string $listType): string
|
||||
{
|
||||
return match ($listType) {
|
||||
'new_hot' => 'score_new_hot',
|
||||
'best' => 'score_best',
|
||||
default => 'score_trending',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user