96 lines
2.7 KiB
PHP
96 lines
2.7 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);
|
|
|
|
$response = Http::acceptJson()
|
|
->connectTimeout(max(1, $connectTimeout))
|
|
->timeout(max(1, $timeout))
|
|
->retry(max(0, $retries), max(0, $delay), throw: false)
|
|
->post($url, [
|
|
'image_url' => $imageUrl,
|
|
'artwork_id' => $artworkId,
|
|
'hash' => $sourceHash,
|
|
]);
|
|
|
|
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['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;
|
|
}
|
|
}
|