Files
SkinbaseNova/app/Services/Vision/ArtworkEmbeddingClient.php
T
test 62aaa2f704 Harden vision vector search and embedding jobs.
Add point search, circuit-breaking, and safer gateway limits so artwork similarity lookups fail closed instead of hanging the request path.
2026-09-20 14:49:19 +02:00

103 lines
3.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Vision;
use Illuminate\Support\Facades\Http;
final class ArtworkEmbeddingClient
{
/**
* @return array<int, float>
*/
public function embed(string $imageUrl, int $artworkId, string $sourceHash): array
{
$base = trim((string) config('vision.clip.base_url', ''));
if ($base === '') {
return [];
}
$endpoint = (string) config('recommendations.embedding.endpoint', '/embed');
$url = rtrim($base, '/') . '/' . ltrim($endpoint, '/');
$timeout = (int) config('recommendations.embedding.timeout_seconds', 8);
$connectTimeout = (int) config('recommendations.embedding.connect_timeout_seconds', 2);
$retries = (int) config('recommendations.embedding.retries', 1);
$delay = (int) config('recommendations.embedding.retry_delay_ms', 200);
$apiKey = trim(
(string) config('vision.clip.api_key', '')
);
$response = Http::acceptJson()
->withHeaders([
'X-API-Key' => $apiKey,
])
->connectTimeout(max(1, $connectTimeout))
->timeout(max(1, $timeout))
->retry(max(0, $retries), max(0, $delay), throw: false)
->post($url, [
'url' => $imageUrl,
]);
if (! $response->ok()) {
return [];
}
return $this->extractEmbedding($response->json());
}
/**
* @param mixed $json
* @return array<int, float>
*/
private function extractEmbedding(mixed $json): array
{
$candidate = null;
if (is_array($json) && $this->isNumericVector($json)) {
$candidate = $json;
} elseif (is_array($json) && isset($json['vector']) && is_array($json['vector'])) {
$candidate = $json['vector'];
} elseif (is_array($json) && isset($json['embedding']) && is_array($json['embedding'])) {
$candidate = $json['embedding'];
} elseif (is_array($json) && isset($json['data']['embedding']) && is_array($json['data']['embedding'])) {
$candidate = $json['data']['embedding'];
}
if (! is_array($candidate) || ! $this->isNumericVector($candidate)) {
return [];
}
$vector = array_map(static fn ($value): float => (float) $value, $candidate);
$dim = count($vector);
$minDim = (int) config('recommendations.embedding.min_dim', 64);
$maxDim = (int) config('recommendations.embedding.max_dim', 4096);
if ($dim < $minDim || $dim > $maxDim) {
return [];
}
return $vector;
}
/**
* @param array<mixed> $arr
*/
private function isNumericVector(array $arr): bool
{
if ($arr === []) {
return false;
}
foreach ($arr as $value) {
if (! is_numeric($value)) {
return false;
}
}
return true;
}
}