feat(ai): add runware model search and model-constrained img2img
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-07 21:16:28 +01:00
parent c0c082975e
commit 6cc463fc70
8 changed files with 1271 additions and 49 deletions

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources\AiStyles;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Filament\Resources\AiStyles\Pages\ManageAiStyles;
use App\Models\AiStyle;
use App\Services\AiEditing\RunwareModelSearchService;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
@@ -15,6 +16,8 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
@@ -82,10 +85,74 @@ class AiStyleResource extends Resource
])
->required()
->default('runware'),
TextInput::make('provider_model')
->maxLength(120),
Select::make('provider_model')
->label('Runware model (AIR)')
->searchable()
->getSearchResultsUsing(static fn (string $search): array => app(RunwareModelSearchService::class)->searchOptions($search))
->getOptionLabelUsing(static fn (mixed $value): ?string => app(RunwareModelSearchService::class)->labelForModel($value))
->helperText('Start typing to search models from runware.ai.')
->native(false)
->live()
->afterStateUpdated(static function (Set $set, ?string $state): void {
self::applySelectedRunwareModel($set, $state);
}),
])
->columns(2),
Section::make('Runware Generation')
->schema([
TextInput::make('metadata.runware.generation.width')
->label('Width')
->numeric()
->minValue(64)
->maxValue(4096)
->step(64)
->helperText(static fn (Get $get): ?string => self::dimensionConstraintHint($get, 'width')),
TextInput::make('metadata.runware.generation.height')
->label('Height')
->numeric()
->minValue(64)
->maxValue(4096)
->step(64)
->helperText(static fn (Get $get): ?string => self::dimensionConstraintHint($get, 'height')),
TextInput::make('metadata.runware.generation.steps')
->label('Steps')
->numeric()
->minValue(1)
->maxValue(150)
->helperText(static fn (Get $get): ?string => self::rangeConstraintHint($get, 'steps')),
TextInput::make('metadata.runware.generation.cfg_scale')
->label('CFG Scale')
->numeric()
->minValue(0)
->maxValue(30)
->step(0.1)
->helperText(static fn (Get $get): ?string => self::rangeConstraintHint($get, 'cfg_scale')),
TextInput::make('metadata.runware.generation.strength')
->label('Strength')
->numeric()
->minValue(0)
->maxValue(1)
->step(0.01)
->helperText(static fn (Get $get): ?string => self::rangeConstraintHint($get, 'strength')),
Select::make('metadata.runware.generation.output_format')
->label('Output format')
->options([
'JPG' => 'JPG',
'PNG' => 'PNG',
'WEBP' => 'WEBP',
])
->default('JPG')
->native(false),
Select::make('metadata.runware.generation.delivery_method')
->label('Delivery method')
->options([
'async' => 'async (queue + poll)',
'sync' => 'sync',
])
->default('async')
->native(false),
])
->columns(3),
Section::make('Prompts')
->schema([
Textarea::make('description')
@@ -160,4 +227,79 @@ class AiStyleResource extends Resource
'index' => ManageAiStyles::route('/'),
];
}
private static function applySelectedRunwareModel(Set $set, ?string $air): void
{
if (! is_string($air) || trim($air) === '') {
return;
}
$model = app(RunwareModelSearchService::class)->findByAir($air);
if (! is_array($model)) {
return;
}
$set('metadata.runware.model.air', $model['air']);
$set('metadata.runware.model.name', $model['name']);
$set('metadata.runware.model.architecture', $model['architecture']);
$set('metadata.runware.model.category', $model['category']);
foreach ((array) ($model['constraints'] ?? []) as $key => $value) {
$set("metadata.runware.constraints.{$key}", $value);
}
self::setIfNumeric($set, 'metadata.runware.generation.width', $model['defaults']['width'] ?? null);
self::setIfNumeric($set, 'metadata.runware.generation.height', $model['defaults']['height'] ?? null);
self::setIfNumeric($set, 'metadata.runware.generation.steps', $model['defaults']['steps'] ?? null);
self::setIfNumeric($set, 'metadata.runware.generation.cfg_scale', $model['defaults']['cfg_scale'] ?? null);
}
private static function setIfNumeric(Set $set, string $path, mixed $value): void
{
if (is_numeric($value)) {
$set($path, $value);
}
}
private static function dimensionConstraintHint(Get $get, string $dimension): ?string
{
$min = $get("metadata.runware.constraints.min_{$dimension}");
$max = $get("metadata.runware.constraints.max_{$dimension}");
$step = $get("metadata.runware.constraints.{$dimension}_step");
if (! is_numeric($min) && ! is_numeric($max) && ! is_numeric($step)) {
return null;
}
$parts = [];
if (is_numeric($min) || is_numeric($max)) {
$parts[] = sprintf(
'Model range: %s - %s',
is_numeric($min) ? (string) (int) $min : '?',
is_numeric($max) ? (string) (int) $max : '?'
);
}
if (is_numeric($step) && (int) $step > 0) {
$parts[] = sprintf('Step: %d', (int) $step);
}
return $parts !== [] ? implode(' | ', $parts) : null;
}
private static function rangeConstraintHint(Get $get, string $field): ?string
{
$min = $get("metadata.runware.constraints.min_{$field}");
$max = $get("metadata.runware.constraints.max_{$field}");
if (! is_numeric($min) && ! is_numeric($max)) {
return null;
}
return sprintf(
'Model range: %s - %s',
is_numeric($min) ? trim((string) $min) : '?',
is_numeric($max) ? trim((string) $max) : '?'
);
}
}