Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz
This commit is contained in:
29
app/Services/Vision/ArtworkVisionImageUrl.php
Normal file
29
app/Services/Vision/ArtworkVisionImageUrl.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Vision;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ThumbnailService;
|
||||
|
||||
final class ArtworkVisionImageUrl
|
||||
{
|
||||
public function fromArtwork(Artwork $artwork): ?string
|
||||
{
|
||||
return $this->fromHash(
|
||||
(string) ($artwork->hash ?? ''),
|
||||
(string) ($artwork->thumb_ext ?: 'webp')
|
||||
);
|
||||
}
|
||||
|
||||
public function fromHash(?string $hash, ?string $ext = 'webp', string $size = 'md'): ?string
|
||||
{
|
||||
$clean = strtolower((string) preg_replace('/[^a-z0-9]/', '', (string) $hash));
|
||||
if ($clean === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ThumbnailService::fromHash($clean, $ext, $size);
|
||||
}
|
||||
}
|
||||
213
app/Services/Vision/VectorGatewayClient.php
Normal file
213
app/Services/Vision/VectorGatewayClient.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Vision;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
final class VectorGatewayClient
|
||||
{
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return (bool) config('vision.vector_gateway.enabled', true)
|
||||
&& $this->baseUrl() !== ''
|
||||
&& $this->apiKey() !== '';
|
||||
}
|
||||
|
||||
public function upsertByUrl(string $imageUrl, int|string $id, array $metadata = []): array
|
||||
{
|
||||
$response = $this->postJson(
|
||||
$this->url((string) config('vision.vector_gateway.upsert_endpoint', '/vectors/upsert')),
|
||||
[
|
||||
'url' => $imageUrl,
|
||||
'id' => (string) $id,
|
||||
'metadata' => $metadata,
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException($this->failureMessage('Vector upsert', $response));
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
|
||||
*/
|
||||
public function searchByUrl(string $imageUrl, int $limit = 5): array
|
||||
{
|
||||
$response = $this->postJson(
|
||||
$this->url((string) config('vision.vector_gateway.search_endpoint', '/vectors/search')),
|
||||
[
|
||||
'url' => $imageUrl,
|
||||
'limit' => max(1, $limit),
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException($this->failureMessage('Vector search', $response));
|
||||
}
|
||||
|
||||
return $this->extractMatches($response->json());
|
||||
}
|
||||
|
||||
public function deleteByIds(array $ids): array
|
||||
{
|
||||
$response = $this->postJson(
|
||||
$this->url((string) config('vision.vector_gateway.delete_endpoint', '/vectors/delete')),
|
||||
[
|
||||
'ids' => array_values(array_map(static fn (int|string $id): string => (string) $id, $ids)),
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException($this->failureMessage('Vector delete', $response));
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
private function request(): PendingRequest
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
throw new RuntimeException('Vision vector gateway is not configured. Set VISION_VECTOR_GATEWAY_URL and VISION_VECTOR_GATEWAY_API_KEY.');
|
||||
}
|
||||
|
||||
return Http::acceptJson()
|
||||
->withHeaders([
|
||||
'X-API-Key' => $this->apiKey(),
|
||||
])
|
||||
->connectTimeout(max(1, (int) config('vision.vector_gateway.connect_timeout_seconds', 5)))
|
||||
->timeout(max(1, (int) config('vision.vector_gateway.timeout_seconds', 20)))
|
||||
->retry(
|
||||
max(0, (int) config('vision.vector_gateway.retries', 1)),
|
||||
max(0, (int) config('vision.vector_gateway.retry_delay_ms', 250)),
|
||||
throw: false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function postJson(string $url, array $payload): Response
|
||||
{
|
||||
$response = $this->request()->post($url, $payload);
|
||||
|
||||
if (! $response instanceof Response) {
|
||||
throw new RuntimeException('Vector gateway request did not return an HTTP response.');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function baseUrl(): string
|
||||
{
|
||||
return rtrim((string) config('vision.vector_gateway.base_url', ''), '/');
|
||||
}
|
||||
|
||||
private function apiKey(): string
|
||||
{
|
||||
return trim((string) config('vision.vector_gateway.api_key', ''));
|
||||
}
|
||||
|
||||
private function url(string $path): string
|
||||
{
|
||||
return $this->baseUrl() . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
private function failureMessage(string $operation, Response $response): string
|
||||
{
|
||||
$body = trim($response->body());
|
||||
|
||||
if ($body === '') {
|
||||
return $operation . ' failed with HTTP ' . $response->status() . '.';
|
||||
}
|
||||
|
||||
return $operation . ' failed with HTTP ' . $response->status() . ': ' . $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $json
|
||||
* @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
|
||||
*/
|
||||
private function extractMatches(mixed $json): array
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
if (is_array($json)) {
|
||||
$candidates = $this->extractCandidateRows($json);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($candidates as $candidate) {
|
||||
if (! is_array($candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = $candidate['id']
|
||||
?? $candidate['point_id']
|
||||
?? $candidate['payload']['id']
|
||||
?? $candidate['metadata']['id']
|
||||
?? null;
|
||||
|
||||
if (! is_int($id) && ! is_string($id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$score = $candidate['score']
|
||||
?? $candidate['similarity']
|
||||
?? $candidate['distance']
|
||||
?? 0.0;
|
||||
|
||||
$metadata = $candidate['metadata'] ?? $candidate['payload'] ?? [];
|
||||
if (! is_array($metadata)) {
|
||||
$metadata = [];
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'id' => $id,
|
||||
'score' => (float) $score,
|
||||
'metadata' => $metadata,
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $json
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function extractCandidateRows(array $json): array
|
||||
{
|
||||
$keys = ['results', 'matches', 'points', 'data'];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (! isset($json[$key]) || ! is_array($json[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $json[$key];
|
||||
if (array_is_list($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
foreach (['results', 'matches', 'points', 'items'] as $nestedKey) {
|
||||
if (isset($value[$nestedKey]) && is_array($value[$nestedKey]) && array_is_list($value[$nestedKey])) {
|
||||
return $value[$nestedKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_is_list($json) ? $json : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user