feat: extend GeminiProvider with social-media aspect ratios

The stock provider only exposes 1:1, 2:3, 3:2 via defaultImageOptions()
match statement — any other ratio gets silently dropped to null.

ExtendedGeminiProvider adds 9:16, 16:9, 4:3, 3:4, 4:5, 5:4, 21:9 so
Image::of()->size('9:16') now works for Reels/Stories/TikTok/Shorts
and size('16:9') for X/LinkedIn/YouTube landscape content.

Gemini's native API supports all these ratios; the SDK restriction was
purely in the provider's match statement.
This commit is contained in:
Paulo Castellano 2026-04-16 09:50:36 -03:00
parent 3d94c57748
commit a962124097
3 changed files with 108 additions and 0 deletions

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Ai\Providers;
use Laravel\Ai\Providers\GeminiProvider;
/**
* Extends the base Gemini provider with social-media-friendly aspect ratios.
*
* The stock provider only exposes 1:1, 2:3, 3:2. Social posts need 9:16
* (Reels/Stories/TikTok/Shorts) and 16:9 (X/LinkedIn/Facebook video).
* Gemini's native API supports both, but the SDK's match statement silently
* drops anything outside its three documented ratios. We re-expose them here.
*/
class ExtendedGeminiProvider extends GeminiProvider
{
/**
* @param 'low'|'medium'|'high'|'1K'|'2K'|'4K'|null $quality
*/
public function defaultImageOptions(?string $size = null, $quality = null): array
{
return array_filter([
'image_size' => 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,
},
]);
}
}

View file

@ -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([

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use App\Ai\Providers\ExtendedGeminiProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Laravel\Ai\Gateway\Gemini\GeminiGateway;
beforeEach(function () {
$this->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');
});