262 lines
8.0 KiB
PHP
262 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\AiEditing;
|
|
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
|
|
class RunwareModelSearchService
|
|
{
|
|
/**
|
|
* @return array<int, array{
|
|
* air: string,
|
|
* name: string,
|
|
* architecture: ?string,
|
|
* category: ?string,
|
|
* defaults: array<string, int|float|null>,
|
|
* constraints: array<string, int|float|null>
|
|
* }>
|
|
*/
|
|
public function search(string $search, ?int $limit = null): array
|
|
{
|
|
$normalizedSearch = trim($search);
|
|
$minimumSearchLength = max(1, (int) config('ai-editing.providers.runware.model_search_min_chars', 2));
|
|
|
|
if (Str::length($normalizedSearch) < $minimumSearchLength) {
|
|
return [];
|
|
}
|
|
|
|
$apiKey = $this->apiKey();
|
|
if (! $apiKey) {
|
|
return [];
|
|
}
|
|
|
|
$resolvedLimit = $this->resolveLimit($limit);
|
|
$cacheTtl = max(30, (int) config('ai-editing.providers.runware.model_search_cache_seconds', 300));
|
|
$cacheKey = sprintf(
|
|
'ai_editing.runware.model_search.%s',
|
|
sha1(Str::lower($normalizedSearch).'|'.$resolvedLimit)
|
|
);
|
|
|
|
return Cache::remember(
|
|
$cacheKey,
|
|
now()->addSeconds($cacheTtl),
|
|
fn (): array => $this->performSearch($normalizedSearch, $resolvedLimit, $apiKey),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function searchOptions(string $search, ?int $limit = null): array
|
|
{
|
|
return collect($this->search($search, $limit))
|
|
->mapWithKeys(fn (array $model): array => [$model['air'] => $this->formatLabel($model)])
|
|
->all();
|
|
}
|
|
|
|
public function labelForModel(mixed $air): ?string
|
|
{
|
|
if (! is_string($air) || trim($air) === '') {
|
|
return null;
|
|
}
|
|
|
|
$model = $this->findByAir($air);
|
|
|
|
return $model ? $this->formatLabel($model) : trim($air);
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* air: string,
|
|
* name: string,
|
|
* architecture: ?string,
|
|
* category: ?string,
|
|
* defaults: array<string, int|float|null>,
|
|
* constraints: array<string, int|float|null>
|
|
* }|null
|
|
*/
|
|
public function findByAir(string $air): ?array
|
|
{
|
|
$normalizedAir = trim($air);
|
|
if ($normalizedAir === '') {
|
|
return null;
|
|
}
|
|
|
|
$results = $this->search($normalizedAir, 50);
|
|
|
|
foreach ($results as $model) {
|
|
if (Str::lower($model['air']) === Str::lower($normalizedAir)) {
|
|
return $model;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{
|
|
* air: string,
|
|
* name: string,
|
|
* architecture: ?string,
|
|
* category: ?string,
|
|
* defaults: array<string, int|float|null>,
|
|
* constraints: array<string, int|float|null>
|
|
* }>
|
|
*/
|
|
private function performSearch(string $search, int $limit, string $apiKey): array
|
|
{
|
|
$payload = [[
|
|
'taskType' => 'modelSearch',
|
|
'taskUUID' => (string) Str::uuid(),
|
|
'search' => $search,
|
|
'limit' => $limit,
|
|
'offset' => 0,
|
|
]];
|
|
|
|
$response = Http::withToken($apiKey)
|
|
->acceptJson()
|
|
->timeout((int) config('services.runware.timeout', 90))
|
|
->post($this->baseUrl(), $payload);
|
|
|
|
if (! $response->successful()) {
|
|
return [];
|
|
}
|
|
|
|
$body = (array) $response->json();
|
|
$items = (array) ($body['data'] ?? []);
|
|
|
|
$models = [];
|
|
foreach ($items as $item) {
|
|
if (! is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$normalized = $this->normalizeModel($item);
|
|
if (! is_array($normalized)) {
|
|
continue;
|
|
}
|
|
|
|
$models[$normalized['air']] = $normalized;
|
|
}
|
|
|
|
return array_values($models);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $item
|
|
* @return array{
|
|
* air: string,
|
|
* name: string,
|
|
* architecture: ?string,
|
|
* category: ?string,
|
|
* defaults: array<string, int|float|null>,
|
|
* constraints: array<string, int|float|null>
|
|
* }|null
|
|
*/
|
|
private function normalizeModel(array $item): ?array
|
|
{
|
|
$air = trim((string) (Arr::get($item, 'air') ?? Arr::get($item, 'model') ?? ''));
|
|
if ($air === '') {
|
|
return null;
|
|
}
|
|
|
|
$name = trim((string) (Arr::get($item, 'name') ?? Arr::get($item, 'title') ?? $air));
|
|
$architecture = $this->normalizeOptionalString((string) Arr::get($item, 'architecture', ''));
|
|
$category = $this->normalizeOptionalString((string) Arr::get($item, 'category', ''));
|
|
|
|
$defaults = [
|
|
'width' => $this->normalizeInteger(Arr::get($item, 'defaultWidth')),
|
|
'height' => $this->normalizeInteger(Arr::get($item, 'defaultHeight')),
|
|
'steps' => $this->normalizeInteger(Arr::get($item, 'defaultSteps')),
|
|
'cfg_scale' => $this->normalizeFloat(Arr::get($item, 'defaultCFG')),
|
|
];
|
|
|
|
$constraints = [
|
|
'min_width' => $this->normalizeInteger(Arr::get($item, 'minWidth')),
|
|
'max_width' => $this->normalizeInteger(Arr::get($item, 'maxWidth')),
|
|
'width_step' => $this->normalizeInteger(Arr::get($item, 'widthStep')),
|
|
'min_height' => $this->normalizeInteger(Arr::get($item, 'minHeight')),
|
|
'max_height' => $this->normalizeInteger(Arr::get($item, 'maxHeight')),
|
|
'height_step' => $this->normalizeInteger(Arr::get($item, 'heightStep')),
|
|
'min_steps' => $this->normalizeInteger(Arr::get($item, 'minSteps')),
|
|
'max_steps' => $this->normalizeInteger(Arr::get($item, 'maxSteps')),
|
|
'min_cfg_scale' => $this->normalizeFloat(Arr::get($item, 'minCFG')),
|
|
'max_cfg_scale' => $this->normalizeFloat(Arr::get($item, 'maxCFG')),
|
|
'min_strength' => $this->normalizeFloat(Arr::get($item, 'minStrength')),
|
|
'max_strength' => $this->normalizeFloat(Arr::get($item, 'maxStrength')),
|
|
];
|
|
|
|
return [
|
|
'air' => $air,
|
|
'name' => $name !== '' ? $name : $air,
|
|
'architecture' => $architecture,
|
|
'category' => $category,
|
|
'defaults' => $defaults,
|
|
'constraints' => $constraints,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* air: string,
|
|
* name: string,
|
|
* architecture: ?string,
|
|
* category: ?string,
|
|
* defaults: array<string, int|float|null>,
|
|
* constraints: array<string, int|float|null>
|
|
* } $model
|
|
*/
|
|
private function formatLabel(array $model): string
|
|
{
|
|
$parts = [$model['name']];
|
|
|
|
if ($model['architecture']) {
|
|
$parts[] = $model['architecture'];
|
|
}
|
|
|
|
$parts[] = $model['air'];
|
|
|
|
return implode(' | ', array_filter($parts, static fn (mixed $value): bool => is_string($value) && trim($value) !== ''));
|
|
}
|
|
|
|
private function resolveLimit(?int $limit): int
|
|
{
|
|
$defaultLimit = max(1, (int) config('ai-editing.providers.runware.model_search_limit', 25));
|
|
$resolved = $limit ?? $defaultLimit;
|
|
|
|
return max(1, min(100, $resolved));
|
|
}
|
|
|
|
private function apiKey(): ?string
|
|
{
|
|
$apiKey = config('services.runware.api_key');
|
|
|
|
return is_string($apiKey) && trim($apiKey) !== '' ? trim($apiKey) : null;
|
|
}
|
|
|
|
private function baseUrl(): string
|
|
{
|
|
return rtrim((string) config('services.runware.base_url', 'https://api.runware.ai/v1'), '/');
|
|
}
|
|
|
|
private function normalizeOptionalString(string $value): ?string
|
|
{
|
|
$trimmed = trim($value);
|
|
|
|
return $trimmed !== '' ? $trimmed : null;
|
|
}
|
|
|
|
private function normalizeInteger(mixed $value): ?int
|
|
{
|
|
return is_numeric($value) ? (int) $value : null;
|
|
}
|
|
|
|
private function normalizeFloat(mixed $value): ?float
|
|
{
|
|
return is_numeric($value) ? (float) $value : null;
|
|
}
|
|
}
|