Files
ai-stylegallery/app/Api/Plugins/LeonardoAi.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.');
}
}
}