onQueue($queue); } } public function backoff(): array { return [2, 10, 30]; } public function handle(ArtworkEmbeddingClient $client, ArtworkVisionImageUrl $imageUrlBuilder): void { if (! (bool) config('recommendations.embedding.enabled', true)) { return; } $artwork = Artwork::query()->find($this->artworkId); if (! $artwork) { return; } $sourceHash = strtolower((string) preg_replace('/[^a-z0-9]/', '', (string) ($this->sourceHash ?? $artwork->hash ?? ''))); if ($sourceHash === '') { return; } $model = (string) config('recommendations.embedding.model', 'clip'); $modelVersion = (string) config('recommendations.embedding.model_version', 'v1'); $algoVersion = (string) config('recommendations.embedding.algo_version', 'clip-cosine-v1'); if (! $this->force) { $existing = ArtworkEmbedding::query() ->where('artwork_id', $artwork->id) ->where('model', $model) ->where('model_version', $modelVersion) ->first(); if ($existing && (string) ($existing->source_hash ?? '') === $sourceHash) { return; } } $lockKey = $this->lockKey($artwork->id, $model, $modelVersion); if (! $this->acquireLock($lockKey)) { return; } try { $imageUrl = $imageUrlBuilder->fromHash($sourceHash, (string) ($artwork->thumb_ext ?: 'webp')); if ($imageUrl === null) { return; } $vector = $client->embed($imageUrl, (int) $artwork->id, $sourceHash); if ($vector === []) { return; } $normalized = $this->normalize($vector); ArtworkEmbedding::query()->updateOrCreate( [ 'artwork_id' => (int) $artwork->id, 'model' => $model, 'model_version' => $modelVersion, ], [ 'algo_version' => $algoVersion, 'dim' => count($normalized), 'embedding_json' => json_encode($normalized, JSON_THROW_ON_ERROR), 'source_hash' => $sourceHash, 'is_normalized' => true, 'generated_at' => now(), 'meta' => [ 'source' => 'clip', 'image_variant' => (string) config('vision.image_variant', 'md'), ], ] ); } finally { $this->releaseLock($lockKey); } } /** * @param array $vector * @return array */ private function normalize(array $vector): array { $sumSquares = 0.0; foreach ($vector as $value) { $sumSquares += ($value * $value); } if ($sumSquares <= 0.0) { return $vector; } $norm = sqrt($sumSquares); return array_map(static fn (float $value): float => $value / $norm, $vector); } private function lockKey(int $artworkId, string $model, string $version): string { return 'artwork-embedding:lock:' . $artworkId . ':' . $model . ':' . $version; } private function acquireLock(string $key): bool { try { $didSet = Redis::setnx($key, 1); if ($didSet) { Redis::expire($key, 1800); } return (bool) $didSet; } catch (\Throwable) { return true; } } private function releaseLock(string $key): void { try { Redis::del($key); } catch (\Throwable) { // ignore } } }