diff --git a/app/Ai/Providers/ExtendedGeminiProvider.php b/app/Ai/Providers/ExtendedGeminiProvider.php new file mode 100644 index 00000000..afbc5958 --- /dev/null +++ b/app/Ai/Providers/ExtendedGeminiProvider.php @@ -0,0 +1,46 @@ + match ($quality) { + 'low', '1K' => '1K', + 'medium', '2K' => '2K', + 'high', '4K' => '4K', + default => '1K', + }, + 'aspect_ratio' => match ($size) { + '1:1' => '1:1', + '2:3' => '2:3', + '3:2' => '3:2', + '9:16' => '9:16', + '16:9' => '16:9', + '4:3' => '4:3', + '3:4' => '3:4', + '4:5' => '4:5', + '5:4' => '5:4', + '21:9' => '21:9', + default => null, + }, + ]); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 00e14fb2..281c1246 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; +use App\Ai\Providers\ExtendedGeminiProvider; use App\Listeners\StripeEventListener; use App\Models\Account; use App\Models\AiMessage; @@ -32,6 +33,7 @@ use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; @@ -43,6 +45,8 @@ use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; +use Laravel\Ai\Ai; +use Laravel\Ai\Gateway\Gemini\GeminiGateway; use Laravel\Cashier\Cashier; use Laravel\Cashier\Events\WebhookReceived; use Laravel\Nightwatch\Facades\Nightwatch; @@ -85,6 +89,7 @@ public function boot(): void { $this->configureDefaults(); $this->configureMorphMap(); + $this->configureAi(); $this->configurePostHog(); $this->configureRateLimiting(); $this->configureSocialite(); @@ -99,6 +104,17 @@ public function boot(): void Feature::discover(); } + protected function configureAi(): void + { + Ai::extend('gemini', function (array $config) { + return new ExtendedGeminiProvider( + new GeminiGateway($this->app['events']), + $config, + $this->app->make(Dispatcher::class), + ); + }); + } + protected function configureMorphMap(): void { Relation::enforceMorphMap([ diff --git a/tests/Unit/Ai/Providers/ExtendedGeminiProviderTest.php b/tests/Unit/Ai/Providers/ExtendedGeminiProviderTest.php new file mode 100644 index 00000000..560d051c --- /dev/null +++ b/tests/Unit/Ai/Providers/ExtendedGeminiProviderTest.php @@ -0,0 +1,46 @@ +provider = new ExtendedGeminiProvider( + new GeminiGateway(app(Dispatcher::class)), + ['name' => 'gemini', 'driver' => 'gemini'], + app(Dispatcher::class), + ); +}); + +test('supports social media vertical aspect ratio 9:16', function () { + $options = $this->provider->defaultImageOptions('9:16'); + + expect($options['aspect_ratio'])->toBe('9:16'); +}); + +test('supports social media horizontal aspect ratio 16:9', function () { + $options = $this->provider->defaultImageOptions('16:9'); + + expect($options['aspect_ratio'])->toBe('16:9'); +}); + +test('still supports the three original ratios from the base provider', function () { + expect($this->provider->defaultImageOptions('1:1')['aspect_ratio'])->toBe('1:1'); + expect($this->provider->defaultImageOptions('2:3')['aspect_ratio'])->toBe('2:3'); + expect($this->provider->defaultImageOptions('3:2')['aspect_ratio'])->toBe('3:2'); +}); + +test('supports additional Instagram-friendly ratios', function () { + expect($this->provider->defaultImageOptions('4:5')['aspect_ratio'])->toBe('4:5'); + expect($this->provider->defaultImageOptions('5:4')['aspect_ratio'])->toBe('5:4'); + expect($this->provider->defaultImageOptions('4:3')['aspect_ratio'])->toBe('4:3'); + expect($this->provider->defaultImageOptions('3:4')['aspect_ratio'])->toBe('3:4'); +}); + +test('drops unsupported aspect ratios silently', function () { + $options = $this->provider->defaultImageOptions('gibberish'); + + expect($options)->not->toHaveKey('aspect_ratio'); +});