initial commit

This commit is contained in:
Jurgis Sakalauskas
2025-01-08 12:45:00 +02:00
commit 6df493fab3
86 changed files with 11890 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}
@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use App\Services\Google\DTO\ImageGenerationRequest;
use App\Services\Google\DTO\ImageGenerationResponse;
use App\Services\Google\Error\APIError;
use App\Services\Google\ImageAspectRatios;
use App\Services\Google\ImageGenerationService;
use App\Services\Image\ImageService;
use Exception;
use Google\Exception as GoogleException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class GoogleImageGenerationController
{
public function __construct(
private readonly ImageService $imageService,
private readonly ImageGenerationService $imageGenerationService,
) {
}
public function generateImage(Request $request): JsonResponse|string
{
try {
$imageGenerationRequest = $this->buildImageGenerationRequest($request);
$imageGenerationResponse = $this->imageGenerationService->generateImage($imageGenerationRequest);
return new JsonResponse(
['generations' => $this->handleGenerationResponse($imageGenerationResponse)]
);
} catch (GoogleException|Exception|APIError $e) {
return new JsonResponse([
'error' => $e->getMessage(),
]);
}
}
private function buildImageGenerationRequest(Request $request): ImageGenerationRequest
{
// add nulls for optional fields
$request->merge([
'aspect_ratio' => $request->input('aspect_ratio'),
'negative_prompt' => $request->input('negative_prompt'),
'sample_count' => $request->input('sample_count'),
]);
$values = $request->validate([
'prompt' => 'required|string',
'aspect_ratio' => [Rule::enum(ImageAspectRatios::class), 'nullable'],
'negative_prompt' => 'string|nullable',
'sample_count' => 'integer|nullable',
]);
return new ImageGenerationRequest(
$values['prompt'],
$values['negative_prompt'],
$values['sample_count'],
$values['aspect_ratio'],
);
}
/**
* @throws Exception
*/
private function handleGenerationResponse(ImageGenerationResponse $generationResponseData): array
{
if (isset($generationResponseData->error)) {
throw $generationResponseData->error;
}
$generations = [];
foreach ($generationResponseData->predictions as $prediction) {
$savedImage = $this->imageService->saveImage($prediction->bytesBase64Encoded, $prediction->mimeType);
$generations[] = $savedImage;
}
return $generations;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
//
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Services\Google;
use Google\Client;
class ApiAuthService
{
/**
* @throws \Google\Exception
*/
public function getAccessToken(): string
{
$client = new Client();
$serviceAccountKeyPath = env('GOOGLE_APPLICATION_CREDENTIALS');
$client->setAuthConfig($serviceAccountKeyPath);
$client->addScope('https://www.googleapis.com/auth/cloud-platform');
$client->fetchAccessTokenWithAssertion();
// todo - cache, refresh, etc
$accessToken = $client->getAccessToken();
return $accessToken['access_token'];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Services\Google\DTO;
class ImageGenerationPrediction
{
public readonly string $bytesBase64Encoded;
public readonly string $mimeType;
public function __construct(string $bytesBase64Encoded, string $mimeType)
{
$this->bytesBase64Encoded = $bytesBase64Encoded;
$this->mimeType = $mimeType;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Services\Google\DTO;
class ImageGenerationRequest
{
public function __construct(
public readonly string $prompt,
public readonly ?string $negativePrompt = null,
public readonly ?int $sampleCount = null,
public readonly ?string $aspectRatio = null,
public readonly ?string $safetySetting = null,
)
{
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Services\Google\DTO;
use App\Services\Google\Error\APIError;
class ImageGenerationResponse
{
/**
* @var \App\Services\Google\DTO\ImageGenerationPrediction[]
*/
public readonly array $predictions;
public readonly ?APIError $error;
public function __construct(array $rawResponse)
{
if (isset($rawResponse['predictions']) && count($rawResponse['predictions']) > 0) {
$predictions = [];
foreach ($rawResponse['predictions'] as $prediction) {
$predictions[] = new ImageGenerationPrediction($prediction['bytesBase64Encoded'], $prediction['mimeType']);
}
$this->predictions = $predictions;
$this->error = null;
}
if (isset($rawResponse['error'])) {
$error = $rawResponse['error'];
$this->error = new APIError($error['message'], $error['code']);
}
if (!isset($this->predictions) && !isset($this->error)) {
$this->error = new APIError('An unknown error occurred.', 0,);
}
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services\Google;
class EndpointBuilderService
{
public function buildPredictionEndpoint(string $model): string
{
return $this->buildEndpoint($model, 'predict');
}
public function buildGenerationEndpoint(string $model): string
{
return $this->buildEndpoint($model, 'generateContent');
}
private function buildEndpoint(string $model, string $action): string
{
$projectId = env('GOOGLE_CLOUD_PROJECT');
$location = env('GOOGLE_LOCATION');
$modelPath = 'projects/' . $projectId .
'/locations/' . $location .
'/publishers/google/models/' . $model .
':' . $action;
return 'https://' . $location . '-aiplatform.googleapis.com/v1/' . $modelPath;
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Services\Google\Error;
use Exception;
class APIError extends Exception
{
public $code;
public $message;
public function __construct(string $message, int $code)
{
parent::__construct($message);
$this->code = $code;
$this->message = $message;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Services\Google;
/**
* "aspectRatio": "1:1, 9:16, 16:9, 3:4, or 4:3. default 1:1",
*/
enum ImageAspectRatios: string
{
case _1_1 = '1:1';
case _9_16 = '9:16';
case _16_9 = '16:9';
case _3_4 = '3:4';
case _4_3 = '4:3';
}
@@ -0,0 +1,14 @@
<?php
namespace App\Services\Google;
/**
* "safetySetting": "block_none, block_only_high, block_medium_and_above, block_low_and_above. default block_medium_and_above"
*/
enum ImageGenerationSafetySettings: string
{
case BLOCK_NONE = 'block_none';
case BLOCK_ONLY_HIGH = 'block_only_high';
case BLOCK_MEDIUM_AND_ABOVE = 'block_medium_and_above';
case BLOCK_LOW_AND_ABOVE = 'block_low_and_above';
}
@@ -0,0 +1,91 @@
<?php
namespace App\Services\Google;
use App\Services\Google\DTO\ImageGenerationRequest;
use App\Services\Google\DTO\ImageGenerationResponse;
/**
* ref: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api
*/
class ImageGenerationService
{
private readonly string $imagenModel;
public function __construct(
private readonly ApiAuthService $apiAuthService,
private readonly EndpointBuilderService $endpointBuilderService,
)
{
$this->imagenModel = env('GOOGLE_IMAGEN_MODEL');
}
/**
* @throws \Exception
*/
public function generateImage(ImageGenerationRequest $request): ImageGenerationResponse
{
$requestBody = $this->buildRequestBody($request);
return $this->sendGenerationRequest($requestBody);
}
private function buildRequestBody(ImageGenerationRequest $request): string {
$instance = [
'prompt' => $request->prompt,
];
if ($request->negativePrompt) {
$instance['negativePrompt'] = $request->negativePrompt;
}
$parameters = [
'sampleCount' => 1,
];
if ($request->sampleCount) {
$parameters['sampleCount'] = $request->sampleCount;
}
if ($request->aspectRatio) {
$parameters['aspectRatio'] = $request->aspectRatio;
}
if ($request->safetySetting) {
$parameters['safetySetting'] = $request->safetySetting;
}
$requestBody = [
'instances' => [$instance],
'parameters' => $parameters
];
return json_encode($requestBody);
}
/**
* @throws \Google\Exception
* @throws \Exception
*/
private function sendGenerationRequest(string $requestBody): ImageGenerationResponse
{
$endpoint = $this->endpointBuilderService->buildPredictionEndpoint($this->imagenModel);
$accessToken = $this->apiAuthService->getAccessToken();
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
]);
$response = curl_exec($ch);
if (curl_errno($ch) || $response === false) {
throw new \Exception(curl_error($ch));
}
curl_close($ch);
$responseArray = json_decode($response, true);
return new ImageGenerationResponse($responseArray);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Services\Image;
use Illuminate\Support\Facades\Storage;
class ImageService
{
/**
* @param string $base64ImageData
* @param string $mimeType
* @return array ['url', 'id']
*/
public function saveImage(string $base64ImageData, string $mimeType): array
{
$imageId = uniqid();
$imageData = base64_decode($base64ImageData);
$ext = explode('/', $mimeType)[1];
Storage::put('public/images/' . $imageId . '.' . $ext , $imageData);
$relativePath = Storage::url('public/images/' . $imageId . '.png');
return [
'url' => asset($relativePath),
'id' => $imageId,
// todo - size, width, height, thumb, etc
];
}
}