271 lines
8.1 KiB
PHP
271 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Api\Plugins;
|
|
|
|
use App\Models\ApiProvider;
|
|
use App\Models\Image;
|
|
use App\Models\Style;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class LeonardoAi implements ApiPluginInterface
|
|
{
|
|
use LoggablePlugin;
|
|
|
|
private const BASE_URL = 'https://cloud.leonardo.ai/api/rest/v2';
|
|
|
|
private const SUPPORTED_MODELS = [
|
|
'gemini-image-2', // Nano Banana Pro
|
|
'flux-pro-2.0', // FLUX.2 Pro
|
|
'seedream-4.0', // Seedream 4.0
|
|
];
|
|
|
|
public function __construct(private ApiProvider $apiProvider) {}
|
|
|
|
public function getIdentifier(): string
|
|
{
|
|
return 'leonardoai';
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'Leonardo AI';
|
|
}
|
|
|
|
public function isEnabled(): bool
|
|
{
|
|
return $this->apiProvider->enabled;
|
|
}
|
|
|
|
public function enable(): bool
|
|
{
|
|
$this->apiProvider->enabled = true;
|
|
|
|
return $this->apiProvider->save();
|
|
}
|
|
|
|
public function disable(): bool
|
|
{
|
|
$this->apiProvider->enabled = false;
|
|
|
|
return $this->apiProvider->save();
|
|
}
|
|
|
|
public function getStatus(string $imageUUID): array
|
|
{
|
|
// Not supported directly; handled via getStyledImage polling.
|
|
return ['status' => 'unknown'];
|
|
}
|
|
|
|
public function getProgress(string $imageUUID): array
|
|
{
|
|
return ['progress' => 0];
|
|
}
|
|
|
|
public function processImageStyleChange(Image $image, Style $style): array
|
|
{
|
|
$this->assertApiConfig();
|
|
|
|
$modelId = $style->aiModel?->model_id;
|
|
if (! $modelId || ! in_array($modelId, self::SUPPORTED_MODELS, true)) {
|
|
throw new RuntimeException('Unsupported Leonardo model. Please choose a v2 model (Nano Banana Pro, FLUX.2 Pro, Seedream 4.0).');
|
|
}
|
|
|
|
$referenceId = $this->uploadReferenceImage($image);
|
|
|
|
$width = $style->parameters['width'] ?? 1024;
|
|
$height = $style->parameters['height'] ?? 1024;
|
|
$styleIds = $style->parameters['style_ids'] ?? [];
|
|
$promptEnhance = $style->parameters['prompt_enhance'] ?? 'OFF';
|
|
|
|
$payload = [
|
|
'model' => $modelId,
|
|
'parameters' => [
|
|
'width' => $width,
|
|
'height' => $height,
|
|
'prompt' => $style->prompt,
|
|
'quantity' => 1,
|
|
'guidances' => [
|
|
'image_reference' => [[
|
|
'image' => [
|
|
'id' => $referenceId,
|
|
'type' => 'UPLOADED',
|
|
],
|
|
'strength' => 'MID',
|
|
]],
|
|
],
|
|
'style_ids' => $styleIds,
|
|
'prompt_enhance' => $promptEnhance,
|
|
],
|
|
'public' => false,
|
|
];
|
|
|
|
$response = $this->http()->post(self::BASE_URL.'/generations', $payload);
|
|
$data = $response->json();
|
|
|
|
$this->throwOnApiError($response, $data);
|
|
|
|
$generationId = $data['generationId'] ?? $data['id'] ?? $data['data'][0]['generationId'] ?? $data['data'][0]['id'] ?? null;
|
|
if (! $generationId) {
|
|
$this->logError('Leonardo generation response missing generationId', ['response' => $data]);
|
|
throw new RuntimeException('Leonardo generation did not return an id.');
|
|
}
|
|
|
|
// Return identifier to allow polling
|
|
return [
|
|
'prompt_id' => $generationId,
|
|
'plugin' => 'LeonardoAi',
|
|
];
|
|
}
|
|
|
|
public function getStyledImage(string $promptId): string
|
|
{
|
|
$this->assertApiConfig();
|
|
|
|
$response = $this->http()->get(self::BASE_URL.'/generations/'.$promptId);
|
|
$data = $response->json();
|
|
|
|
$this->throwOnApiError($response, $data);
|
|
|
|
$images = $data['images'] ?? $data['data'][0]['images'] ?? null;
|
|
if (! $images || ! isset($images[0]['url'])) {
|
|
throw new RuntimeException('Leonardo generation not ready yet or no image URL returned.');
|
|
}
|
|
|
|
return $images[0]['url'];
|
|
}
|
|
|
|
public function testConnection(array $data): bool
|
|
{
|
|
$apiKey = $data['token'] ?? null;
|
|
if (! $apiKey) {
|
|
throw new RuntimeException('API key fehlt.');
|
|
}
|
|
|
|
$payload = [
|
|
'model' => self::SUPPORTED_MODELS[0],
|
|
'parameters' => [
|
|
'prompt' => 'health check',
|
|
'width' => 512,
|
|
'height' => 512,
|
|
'quantity' => 1,
|
|
],
|
|
'public' => false,
|
|
];
|
|
|
|
$response = $this->http($apiKey)->post(self::BASE_URL.'/generations', $payload);
|
|
$json = $response->json();
|
|
|
|
$this->throwOnApiError($response, $json);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function searchModels(string $searchTerm): array
|
|
{
|
|
$models = [
|
|
['name' => 'Nano Banana Pro', 'id' => 'gemini-image-2'],
|
|
['name' => 'FLUX.2 Pro', 'id' => 'flux-pro-2.0'],
|
|
['name' => 'Seedream 4.0', 'id' => 'seedream-4.0'],
|
|
];
|
|
|
|
return array_values(array_filter($models, function ($model) use ($searchTerm) {
|
|
return stripos($model['name'], $searchTerm) !== false || stripos($model['id'], $searchTerm) !== false;
|
|
}));
|
|
}
|
|
|
|
public function checkAvailability(): array
|
|
{
|
|
try {
|
|
$this->testConnection($this->apiProvider->toArray());
|
|
|
|
return [
|
|
'available' => true,
|
|
'reason' => 'Connection successful',
|
|
'provider_id' => $this->apiProvider->id,
|
|
'provider_name' => $this->apiProvider->name,
|
|
];
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'available' => false,
|
|
'reason' => $e->getMessage(),
|
|
'provider_id' => $this->apiProvider->id,
|
|
'provider_name' => $this->apiProvider->name,
|
|
];
|
|
}
|
|
}
|
|
|
|
private function uploadReferenceImage(Image $image): string
|
|
{
|
|
$path = public_path('storage/'.$image->path);
|
|
|
|
if (! file_exists($path)) {
|
|
throw new RuntimeException('Bild nicht gefunden: '.$path);
|
|
}
|
|
|
|
$response = $this->http()->attach('image', file_get_contents($path), basename($path))
|
|
->post(self::BASE_URL.'/init-image', [
|
|
'extension' => pathinfo($path, PATHINFO_EXTENSION),
|
|
]);
|
|
|
|
$json = $response->json();
|
|
$this->throwOnApiError($response, $json);
|
|
|
|
$id = $json['id'] ?? $json['imageId'] ?? $json['uploadId'] ?? $json['data'][0]['id'] ?? null;
|
|
|
|
if (! $id) {
|
|
$this->logError('Leonardo upload response missing id', ['response' => $json]);
|
|
throw new RuntimeException('Upload to Leonardo failed (no id returned).');
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
private function http(?string $apiKey = null)
|
|
{
|
|
$key = $apiKey ?? $this->apiProvider->token;
|
|
|
|
if (! $key) {
|
|
throw new RuntimeException('Leonardo API key ist nicht gesetzt.');
|
|
}
|
|
|
|
return Http::withHeaders([
|
|
'Authorization' => 'Bearer '.$key,
|
|
'Accept' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
private function throwOnApiError($response, ?array $data): void
|
|
{
|
|
if ($response->successful() && empty($data['errors'])) {
|
|
return;
|
|
}
|
|
|
|
$error = $data['errors'][0] ?? null;
|
|
|
|
if ($error) {
|
|
$code = $error['code'] ?? 'error';
|
|
$message = $error['message'] ?? 'API error';
|
|
$task = $error['taskUUID'] ?? null;
|
|
$detail = trim(($code ? $code.': ' : '').$message.' '.($task ? "(task {$task})" : ''));
|
|
throw new RuntimeException($detail);
|
|
}
|
|
|
|
$status = $response->status();
|
|
$body = $response->body();
|
|
throw new RuntimeException('Leonardo API Fehler '.$status.($body ? ': '.Str::limit($body, 200) : ''));
|
|
}
|
|
|
|
private function assertApiConfig(): void
|
|
{
|
|
if (! $this->apiProvider->enabled) {
|
|
throw new RuntimeException('API Provider ist deaktiviert.');
|
|
}
|
|
|
|
if (! $this->apiProvider->token) {
|
|
throw new RuntimeException('Leonardo API key fehlt.');
|
|
}
|
|
}
|
|
}
|